@scenar/cli 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/__tests__/narrate-command.test.ts +150 -51
- package/src/commands/narrate.d.ts +0 -12
- package/src/commands/narrate.d.ts.map +1 -1
- package/src/commands/narrate.js +164 -49
- package/src/commands/narrate.js.map +1 -1
- package/src/commands/narrate.ts +260 -58
- package/src/index.js +1 -1
- package/src/index.js.map +1 -1
- package/src/index.ts +1 -1
- package/src/tts/types.d.ts +5 -1
- package/src/tts/types.d.ts.map +1 -1
- package/src/tts/types.ts +5 -1
- package/src/util/discover-scenarios.d.ts +12 -0
- package/src/util/discover-scenarios.d.ts.map +1 -0
- package/src/util/discover-scenarios.js +28 -0
- package/src/util/discover-scenarios.js.map +1 -0
- package/src/util/discover-scenarios.ts +30 -0
- package/src/util/load-ts.d.ts +20 -0
- package/src/util/load-ts.d.ts.map +1 -0
- package/src/util/load-ts.js +25 -0
- package/src/util/load-ts.js.map +1 -0
- package/src/util/load-ts.ts +39 -0
- package/src/util/narration-cache.d.ts +24 -0
- package/src/util/narration-cache.d.ts.map +1 -0
- package/src/util/narration-cache.js +51 -0
- package/src/util/narration-cache.js.map +1 -0
- package/src/util/narration-cache.ts +86 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/commands/narrate.ts
CHANGED
|
@@ -1,111 +1,313 @@
|
|
|
1
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { mkdir, writeFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join, resolve, extname, basename, dirname } from "node:path";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import { loadScenarioYaml } from "../util/load-yaml.js";
|
|
5
|
+
import { loadStepsFromTs } from "../util/load-ts.js";
|
|
6
|
+
import { discoverScenarios } from "../util/discover-scenarios.js";
|
|
7
|
+
import {
|
|
8
|
+
computeHash,
|
|
9
|
+
loadCache,
|
|
10
|
+
saveCache,
|
|
11
|
+
fileExists,
|
|
12
|
+
isCached,
|
|
13
|
+
getCachedDuration,
|
|
14
|
+
buildCacheFile,
|
|
15
|
+
} from "../util/narration-cache.js";
|
|
5
16
|
import { validateScenario } from "../validate/scenario-validator.js";
|
|
6
17
|
import { resolveProvider } from "../tts/resolve-provider.js";
|
|
7
|
-
import type {
|
|
18
|
+
import type { TtsProvider } from "../tts/types.js";
|
|
8
19
|
|
|
9
20
|
interface NarrateOptions {
|
|
10
21
|
tts: string;
|
|
11
|
-
out
|
|
22
|
+
out?: string;
|
|
12
23
|
voice?: string;
|
|
24
|
+
baseUrl?: string;
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
export function registerNarrateCommand(program: Command): void {
|
|
16
28
|
program
|
|
17
29
|
.command("narrate")
|
|
18
|
-
.description(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
.description(
|
|
31
|
+
"Generate narration audio from scenario files.\n\n" +
|
|
32
|
+
"Accepts a YAML file, a TypeScript steps file, or a directory\n" +
|
|
33
|
+
"containing scenario subdirectories (each with a steps.ts).",
|
|
34
|
+
)
|
|
35
|
+
.argument("<file-or-dir>", "path to scenario file (.yaml/.ts) or directory")
|
|
36
|
+
.option("--tts <provider>", "TTS provider: echogarden, edge-tts, or openai", "echogarden")
|
|
37
|
+
.option("--out <dir>", "output directory for audio files")
|
|
22
38
|
.option("--voice <voice>", "voice name (provider-specific)")
|
|
23
|
-
.
|
|
39
|
+
.option("--base-url <path>", "URL path prefix for src fields in manifest")
|
|
40
|
+
.action(async (fileOrDir: string, options: NarrateOptions) => {
|
|
24
41
|
const provider = await resolveProvider(options.tts);
|
|
25
|
-
|
|
42
|
+
const resolved = resolve(fileOrDir);
|
|
43
|
+
const info = await stat(resolved);
|
|
44
|
+
|
|
45
|
+
if (info.isDirectory()) {
|
|
46
|
+
await runNarrateDirectory(resolved, options, provider);
|
|
47
|
+
} else {
|
|
48
|
+
await runNarrateSingleFile(resolved, options, provider);
|
|
49
|
+
}
|
|
26
50
|
});
|
|
27
51
|
}
|
|
28
52
|
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Step extraction
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
29
57
|
interface StepWithNarration {
|
|
30
58
|
index: number;
|
|
31
59
|
text: string;
|
|
32
60
|
}
|
|
33
61
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
62
|
+
function extractNarratedSteps(
|
|
63
|
+
steps: Array<{ narration?: string; narrationText?: string }>,
|
|
64
|
+
): StepWithNarration[] {
|
|
65
|
+
const result: StepWithNarration[] = [];
|
|
66
|
+
for (let i = 0; i < steps.length; i++) {
|
|
67
|
+
const text = steps[i]!.narration ?? steps[i]!.narrationText;
|
|
68
|
+
if (typeof text === "string" && text.length > 0) {
|
|
69
|
+
result.push({ index: i, text });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
// Runtime manifest (matches @scenar/core NarrationManifest)
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
interface RuntimeManifestEntry {
|
|
80
|
+
src: string;
|
|
81
|
+
durationMs: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface RuntimeManifest {
|
|
85
|
+
steps: (RuntimeManifestEntry | null)[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function buildRuntimeManifest(
|
|
89
|
+
totalSteps: number,
|
|
90
|
+
entries: Map<number, { src: string; durationMs: number }>,
|
|
91
|
+
): RuntimeManifest {
|
|
92
|
+
const steps: (RuntimeManifestEntry | null)[] = Array.from(
|
|
93
|
+
{ length: totalSteps },
|
|
94
|
+
() => null,
|
|
95
|
+
);
|
|
96
|
+
for (const [idx, entry] of entries) {
|
|
97
|
+
steps[idx] = entry;
|
|
98
|
+
}
|
|
99
|
+
return { steps };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Single-file narration
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
async function loadStepsFromFile(
|
|
107
|
+
filePath: string,
|
|
108
|
+
): Promise<{ steps: Array<{ narration?: string; narrationText?: string }>; totalSteps: number }> {
|
|
109
|
+
const ext = extname(filePath).toLowerCase();
|
|
110
|
+
|
|
111
|
+
if (ext === ".ts" || ext === ".tsx") {
|
|
112
|
+
const steps = await loadStepsFromTs(filePath);
|
|
113
|
+
return { steps, totalSteps: steps.length };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const scenario = await loadScenarioYaml(filePath);
|
|
117
|
+
const validation = validateScenario(scenario);
|
|
118
|
+
|
|
119
|
+
if (!validation.valid) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`Scenario has ${validation.errors.length} error(s). Run 'scenar validate ${filePath}' for details.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const steps = (scenario as Record<string, unknown>)["steps"] as
|
|
126
|
+
| Array<Record<string, unknown>>
|
|
127
|
+
| undefined;
|
|
128
|
+
if (!steps) {
|
|
129
|
+
throw new Error("No steps found in scenario.");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { steps: steps as Array<{ narrationText?: string }>, totalSteps: steps.length };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function runNarrateSingleFile(
|
|
136
|
+
filePath: string,
|
|
40
137
|
options: NarrateOptions,
|
|
41
138
|
provider: TtsProvider,
|
|
42
139
|
): Promise<void> {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
140
|
+
const { steps, totalSteps } = await loadStepsFromFile(filePath);
|
|
141
|
+
const narratedSteps = extractNarratedSteps(steps);
|
|
45
142
|
|
|
46
|
-
if (
|
|
47
|
-
process.stderr.write(
|
|
48
|
-
process.exitCode = 1;
|
|
143
|
+
if (narratedSteps.length === 0) {
|
|
144
|
+
process.stderr.write("\x1b[33m!\x1b[0m No steps contain narration text. Nothing to generate.\n");
|
|
49
145
|
return;
|
|
50
146
|
}
|
|
51
147
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
148
|
+
const outDir = options.out ?? join(dirname(filePath), "narration");
|
|
149
|
+
const voice = options.voice ?? "";
|
|
150
|
+
|
|
151
|
+
await generateNarration({
|
|
152
|
+
scenarioId: basename(dirname(filePath)),
|
|
153
|
+
narratedSteps,
|
|
154
|
+
totalSteps,
|
|
155
|
+
outDir,
|
|
156
|
+
voice,
|
|
157
|
+
baseUrl: options.baseUrl,
|
|
158
|
+
provider,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Directory-mode narration
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
async function runNarrateDirectory(
|
|
167
|
+
dirPath: string,
|
|
168
|
+
options: NarrateOptions,
|
|
169
|
+
provider: TtsProvider,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const scenarios = await discoverScenarios(dirPath);
|
|
172
|
+
|
|
173
|
+
if (scenarios.length === 0) {
|
|
174
|
+
process.stderr.write("\x1b[33m!\x1b[0m No scenario directories with steps.ts found.\n");
|
|
56
175
|
return;
|
|
57
176
|
}
|
|
58
177
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
178
|
+
process.stderr.write(`Discovered ${scenarios.length} scenario(s)\n\n`);
|
|
179
|
+
|
|
180
|
+
let totalGenerated = 0;
|
|
181
|
+
let totalCached = 0;
|
|
182
|
+
let totalSkipped = 0;
|
|
183
|
+
let scenariosWithNarration = 0;
|
|
184
|
+
const errors: Array<{ id: string; error: unknown }> = [];
|
|
185
|
+
|
|
186
|
+
for (const scenario of scenarios) {
|
|
187
|
+
process.stderr.write(` ${scenario.id}\n`);
|
|
188
|
+
try {
|
|
189
|
+
const steps = await loadStepsFromTs(scenario.stepsPath);
|
|
190
|
+
const narratedSteps = extractNarratedSteps(steps);
|
|
191
|
+
|
|
192
|
+
if (narratedSteps.length === 0) {
|
|
193
|
+
process.stderr.write(" (no narration)\n");
|
|
194
|
+
totalSkipped++;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
scenariosWithNarration++;
|
|
199
|
+
|
|
200
|
+
const outDir = options.out
|
|
201
|
+
? join(options.out, scenario.id)
|
|
202
|
+
: join(dirPath, scenario.id, "narration");
|
|
203
|
+
|
|
204
|
+
const stats = await generateNarration({
|
|
205
|
+
scenarioId: scenario.id,
|
|
206
|
+
narratedSteps,
|
|
207
|
+
totalSteps: steps.length,
|
|
208
|
+
outDir,
|
|
209
|
+
voice: options.voice ?? "",
|
|
210
|
+
baseUrl: options.baseUrl,
|
|
211
|
+
provider,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
totalGenerated += stats.generated;
|
|
215
|
+
totalCached += stats.cached;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
process.stderr.write(` \x1b[31mfailed\x1b[0m: ${error}\n`);
|
|
218
|
+
errors.push({ id: scenario.id, error });
|
|
64
219
|
}
|
|
65
220
|
}
|
|
66
221
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
222
|
+
process.stderr.write("\n");
|
|
223
|
+
process.stderr.write(`Scenarios with narration: ${scenariosWithNarration}\n`);
|
|
224
|
+
process.stderr.write(`Audio files generated: ${totalGenerated}\n`);
|
|
225
|
+
process.stderr.write(`Audio files cached: ${totalCached}\n`);
|
|
226
|
+
process.stderr.write(`Scenarios skipped: ${totalSkipped}\n`);
|
|
227
|
+
|
|
228
|
+
if (errors.length > 0) {
|
|
229
|
+
process.stderr.write(`\n\x1b[31m${errors.length} scenario(s) failed:\x1b[0m\n`);
|
|
230
|
+
for (const { id, error } of errors) {
|
|
231
|
+
process.stderr.write(` - ${id}: ${error}\n`);
|
|
232
|
+
}
|
|
233
|
+
process.exitCode = 1;
|
|
234
|
+
} else {
|
|
235
|
+
process.stderr.write("\nDone\n");
|
|
70
236
|
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// Core generation logic (shared by single-file and directory modes)
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
interface GenerateOptions {
|
|
244
|
+
scenarioId: string;
|
|
245
|
+
narratedSteps: StepWithNarration[];
|
|
246
|
+
totalSteps: number;
|
|
247
|
+
outDir: string;
|
|
248
|
+
voice: string;
|
|
249
|
+
baseUrl?: string;
|
|
250
|
+
provider: TtsProvider;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
interface GenerateStats {
|
|
254
|
+
generated: number;
|
|
255
|
+
cached: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function generateNarration(opts: GenerateOptions): Promise<GenerateStats> {
|
|
259
|
+
const { scenarioId, narratedSteps, totalSteps, outDir, voice, baseUrl, provider } = opts;
|
|
71
260
|
|
|
72
|
-
await mkdir(
|
|
261
|
+
await mkdir(outDir, { recursive: true });
|
|
73
262
|
|
|
74
|
-
const
|
|
263
|
+
const existingCache = await loadCache(outDir);
|
|
264
|
+
const stats: GenerateStats = { generated: 0, cached: 0 };
|
|
265
|
+
const manifestEntries = new Map<number, { src: string; durationMs: number }>();
|
|
266
|
+
const cacheEntries = new Map<number, { hash: string; durationMs: number }>();
|
|
75
267
|
|
|
76
|
-
for (
|
|
77
|
-
const
|
|
268
|
+
for (const step of narratedSteps) {
|
|
269
|
+
const hash = computeHash(step.text, voice);
|
|
78
270
|
const fileName = `step-${step.index}.mp3`;
|
|
79
|
-
const
|
|
271
|
+
const mp3Path = join(outDir, fileName);
|
|
80
272
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
273
|
+
const srcPrefix = baseUrl
|
|
274
|
+
? `${baseUrl.replace(/\/$/, "")}/${scenarioId}`
|
|
275
|
+
: `.`;
|
|
276
|
+
const src = `${srcPrefix}/${fileName}`;
|
|
277
|
+
|
|
278
|
+
if (isCached(existingCache, step.index, hash, voice) && await fileExists(mp3Path)) {
|
|
279
|
+
const durationMs = getCachedDuration(existingCache!, step.index);
|
|
280
|
+
manifestEntries.set(step.index, { src, durationMs });
|
|
281
|
+
cacheEntries.set(step.index, { hash, durationMs });
|
|
282
|
+
stats.cached++;
|
|
283
|
+
process.stderr.write(` step ${step.index}: cached\n`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
84
286
|
|
|
287
|
+
process.stderr.write(` step ${step.index}: generating...\n`);
|
|
85
288
|
const result = await provider.synthesize(step.text, {
|
|
86
|
-
voice:
|
|
289
|
+
voice: voice || undefined,
|
|
87
290
|
});
|
|
88
291
|
|
|
89
|
-
await writeFile(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
durationMs: result.durationMs,
|
|
95
|
-
text: step.text,
|
|
96
|
-
});
|
|
292
|
+
await writeFile(mp3Path, result.audio);
|
|
293
|
+
manifestEntries.set(step.index, { src, durationMs: result.durationMs });
|
|
294
|
+
cacheEntries.set(step.index, { hash, durationMs: result.durationMs });
|
|
295
|
+
stats.generated++;
|
|
296
|
+
process.stderr.write(` step ${step.index}: ${result.durationMs}ms (${result.audio.length} bytes)\n`);
|
|
97
297
|
}
|
|
98
298
|
|
|
99
|
-
const manifest
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
299
|
+
const manifest = buildRuntimeManifest(totalSteps, manifestEntries);
|
|
300
|
+
await writeFile(
|
|
301
|
+
join(outDir, "manifest.json"),
|
|
302
|
+
JSON.stringify(manifest, null, 2) + "\n",
|
|
303
|
+
);
|
|
104
304
|
|
|
105
|
-
const
|
|
106
|
-
await
|
|
305
|
+
const cache = buildCacheFile(voice, totalSteps, cacheEntries);
|
|
306
|
+
await saveCache(outDir, cache);
|
|
107
307
|
|
|
108
308
|
process.stderr.write(
|
|
109
|
-
`\
|
|
309
|
+
`\x1b[32m+\x1b[0m ${scenarioId}: ${stats.generated} generated, ${stats.cached} cached\n`,
|
|
110
310
|
);
|
|
311
|
+
|
|
312
|
+
return stats;
|
|
111
313
|
}
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export function createProgram() {
|
|
|
5
5
|
const program = new Command();
|
|
6
6
|
program
|
|
7
7
|
.name("scenar")
|
|
8
|
-
.description("Scenar CLI — validate
|
|
8
|
+
.description("Scenar CLI — validate scenarios and generate narration audio.")
|
|
9
9
|
.version("0.0.1");
|
|
10
10
|
registerValidateCommand(program);
|
|
11
11
|
registerNarrateCommand(program);
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,QAAQ,CAAC;SACd,WAAW,CAAC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,UAAU,aAAa;IAC3B,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,OAAO;SACJ,IAAI,CAAC,QAAQ,CAAC;SACd,WAAW,CAAC,+DAA+D,CAAC;SAC5E,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpB,uBAAuB,CAAC,OAAO,CAAC,CAAC;IACjC,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAEhC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,GAAG,CAAC,IAAc;IAChC,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC"}
|
package/src/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ export function createProgram(): Command {
|
|
|
7
7
|
|
|
8
8
|
program
|
|
9
9
|
.name("scenar")
|
|
10
|
-
.description("Scenar CLI — validate
|
|
10
|
+
.description("Scenar CLI — validate scenarios and generate narration audio.")
|
|
11
11
|
.version("0.0.1");
|
|
12
12
|
|
|
13
13
|
registerValidateCommand(program);
|
package/src/tts/types.d.ts
CHANGED
|
@@ -25,7 +25,11 @@ export interface TtsResult {
|
|
|
25
25
|
durationMs: number;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
28
|
+
* @deprecated Legacy CLI manifest shape. The narrate command now writes
|
|
29
|
+
* manifests matching `@scenar/core`'s `NarrationManifest` type directly
|
|
30
|
+
* (positional array of `{ src, durationMs } | null`).
|
|
31
|
+
*
|
|
32
|
+
* Kept temporarily for test compatibility during the transition.
|
|
29
33
|
*/
|
|
30
34
|
export interface NarrationManifest {
|
|
31
35
|
generatedAt: string;
|
package/src/tts/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/tts/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/tts/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,WAAW,WAAW;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,UAAU;IACzB,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,qBAAqB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd"}
|
package/src/tts/types.ts
CHANGED
|
@@ -29,7 +29,11 @@ export interface TtsResult {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* @deprecated Legacy CLI manifest shape. The narrate command now writes
|
|
33
|
+
* manifests matching `@scenar/core`'s `NarrationManifest` type directly
|
|
34
|
+
* (positional array of `{ src, durationMs } | null`).
|
|
35
|
+
*
|
|
36
|
+
* Kept temporarily for test compatibility during the transition.
|
|
33
37
|
*/
|
|
34
38
|
export interface NarrationManifest {
|
|
35
39
|
generatedAt: string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan a directory for scenario subdirectories. A subdirectory
|
|
3
|
+
* qualifies if it contains a `steps.ts` file.
|
|
4
|
+
*
|
|
5
|
+
* Returns sorted scenario entries with the directory name (used as
|
|
6
|
+
* the scenario ID) and the absolute path to `steps.ts`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function discoverScenarios(dir: string): Promise<Array<{
|
|
9
|
+
id: string;
|
|
10
|
+
stepsPath: string;
|
|
11
|
+
}>>;
|
|
12
|
+
//# sourceMappingURL=discover-scenarios.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover-scenarios.d.ts","sourceRoot":"","sources":["../../../src/util/discover-scenarios.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAiBnD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Scan a directory for scenario subdirectories. A subdirectory
|
|
5
|
+
* qualifies if it contains a `steps.ts` file.
|
|
6
|
+
*
|
|
7
|
+
* Returns sorted scenario entries with the directory name (used as
|
|
8
|
+
* the scenario ID) and the absolute path to `steps.ts`.
|
|
9
|
+
*/
|
|
10
|
+
export async function discoverScenarios(dir) {
|
|
11
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
12
|
+
const scenarios = [];
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (!entry.isDirectory())
|
|
15
|
+
continue;
|
|
16
|
+
const stepsPath = join(dir, entry.name, "steps.ts");
|
|
17
|
+
try {
|
|
18
|
+
const { access } = await import("node:fs/promises");
|
|
19
|
+
await access(stepsPath);
|
|
20
|
+
scenarios.push({ id: entry.name, stepsPath });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// No steps.ts — skip.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return scenarios.sort((a, b) => a.id.localeCompare(b.id));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=discover-scenarios.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discover-scenarios.js","sourceRoot":"","sources":["../../../src/util/discover-scenarios.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,GAAW;IAEX,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5D,MAAM,SAAS,GAA6C,EAAE,CAAC;IAE/D,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,SAAS;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QACpD,IAAI,CAAC;YACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAC;YACpD,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;YACxB,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,sBAAsB;QACxB,CAAC;IACH,CAAC;IAED,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Scan a directory for scenario subdirectories. A subdirectory
|
|
6
|
+
* qualifies if it contains a `steps.ts` file.
|
|
7
|
+
*
|
|
8
|
+
* Returns sorted scenario entries with the directory name (used as
|
|
9
|
+
* the scenario ID) and the absolute path to `steps.ts`.
|
|
10
|
+
*/
|
|
11
|
+
export async function discoverScenarios(
|
|
12
|
+
dir: string,
|
|
13
|
+
): Promise<Array<{ id: string; stepsPath: string }>> {
|
|
14
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
15
|
+
const scenarios: Array<{ id: string; stepsPath: string }> = [];
|
|
16
|
+
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (!entry.isDirectory()) continue;
|
|
19
|
+
const stepsPath = join(dir, entry.name, "steps.ts");
|
|
20
|
+
try {
|
|
21
|
+
const { access } = await import("node:fs/promises");
|
|
22
|
+
await access(stepsPath);
|
|
23
|
+
scenarios.push({ id: entry.name, stepsPath });
|
|
24
|
+
} catch {
|
|
25
|
+
// No steps.ts — skip.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return scenarios.sort((a, b) => a.id.localeCompare(b.id));
|
|
30
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal shape extracted from each step object. The loader
|
|
3
|
+
* duck-types the exported array — any array of objects with
|
|
4
|
+
* `delayMs` qualifies as a steps array.
|
|
5
|
+
*/
|
|
6
|
+
export interface ImportedStep {
|
|
7
|
+
delayMs: number;
|
|
8
|
+
narration?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Dynamically import a TypeScript steps file and extract the
|
|
12
|
+
* steps array by duck-typing (looks for the first exported array
|
|
13
|
+
* whose elements have a `delayMs` property).
|
|
14
|
+
*
|
|
15
|
+
* Requires the caller's Node process to have a TypeScript loader
|
|
16
|
+
* active (e.g. running via `tsx`). The CLI itself does not depend
|
|
17
|
+
* on any TS compilation tool.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadStepsFromTs(filePath: string): Promise<ImportedStep[]>;
|
|
20
|
+
//# sourceMappingURL=load-ts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-ts.d.ts","sourceRoot":"","sources":["../../../src/util/load-ts.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAiB/E"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
/**
|
|
3
|
+
* Dynamically import a TypeScript steps file and extract the
|
|
4
|
+
* steps array by duck-typing (looks for the first exported array
|
|
5
|
+
* whose elements have a `delayMs` property).
|
|
6
|
+
*
|
|
7
|
+
* Requires the caller's Node process to have a TypeScript loader
|
|
8
|
+
* active (e.g. running via `tsx`). The CLI itself does not depend
|
|
9
|
+
* on any TS compilation tool.
|
|
10
|
+
*/
|
|
11
|
+
export async function loadStepsFromTs(filePath) {
|
|
12
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
13
|
+
const exports = mod.default ?? mod;
|
|
14
|
+
for (const value of Object.values(exports)) {
|
|
15
|
+
if (Array.isArray(value) &&
|
|
16
|
+
value.length > 0 &&
|
|
17
|
+
typeof value[0] === "object" &&
|
|
18
|
+
value[0] !== null &&
|
|
19
|
+
"delayMs" in value[0]) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
throw new Error(`No steps array found in ${filePath}`);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=load-ts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-ts.js","sourceRoot":"","sources":["../../../src/util/load-ts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAYzC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAgB;IACpD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC;IAEnC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3C,IACE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YACpB,KAAK,CAAC,MAAM,GAAG,CAAC;YAChB,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,QAAQ;YAC5B,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI;YACjB,SAAS,IAAI,KAAK,CAAC,CAAC,CAAC,EACrB,CAAC;YACD,OAAO,KAAuB,CAAC;QACjC,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;AACzD,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal shape extracted from each step object. The loader
|
|
5
|
+
* duck-types the exported array — any array of objects with
|
|
6
|
+
* `delayMs` qualifies as a steps array.
|
|
7
|
+
*/
|
|
8
|
+
export interface ImportedStep {
|
|
9
|
+
delayMs: number;
|
|
10
|
+
narration?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dynamically import a TypeScript steps file and extract the
|
|
15
|
+
* steps array by duck-typing (looks for the first exported array
|
|
16
|
+
* whose elements have a `delayMs` property).
|
|
17
|
+
*
|
|
18
|
+
* Requires the caller's Node process to have a TypeScript loader
|
|
19
|
+
* active (e.g. running via `tsx`). The CLI itself does not depend
|
|
20
|
+
* on any TS compilation tool.
|
|
21
|
+
*/
|
|
22
|
+
export async function loadStepsFromTs(filePath: string): Promise<ImportedStep[]> {
|
|
23
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
24
|
+
const exports = mod.default ?? mod;
|
|
25
|
+
|
|
26
|
+
for (const value of Object.values(exports)) {
|
|
27
|
+
if (
|
|
28
|
+
Array.isArray(value) &&
|
|
29
|
+
value.length > 0 &&
|
|
30
|
+
typeof value[0] === "object" &&
|
|
31
|
+
value[0] !== null &&
|
|
32
|
+
"delayMs" in value[0]
|
|
33
|
+
) {
|
|
34
|
+
return value as ImportedStep[];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new Error(`No steps array found in ${filePath}`);
|
|
39
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface CacheEntry {
|
|
2
|
+
hash: string;
|
|
3
|
+
durationMs: number;
|
|
4
|
+
}
|
|
5
|
+
interface CacheFile {
|
|
6
|
+
voice: string;
|
|
7
|
+
steps: (CacheEntry | null)[];
|
|
8
|
+
}
|
|
9
|
+
export declare function computeHash(narration: string, voice: string): string;
|
|
10
|
+
export declare function loadCache(outputDir: string): Promise<CacheFile | null>;
|
|
11
|
+
export declare function saveCache(outputDir: string, cache: CacheFile): Promise<void>;
|
|
12
|
+
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Check whether a step's narration is already cached (matching hash
|
|
15
|
+
* and the MP3 file still exists on disk).
|
|
16
|
+
*/
|
|
17
|
+
export declare function isCached(cache: CacheFile | null, stepIndex: number, hash: string, voice: string): boolean;
|
|
18
|
+
export declare function getCachedDuration(cache: CacheFile, stepIndex: number): number;
|
|
19
|
+
export declare function buildCacheFile(voice: string, totalSteps: number, entries: Map<number, {
|
|
20
|
+
hash: string;
|
|
21
|
+
durationMs: number;
|
|
22
|
+
}>): CacheFile;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=narration-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"narration-cache.d.ts","sourceRoot":"","sources":["../../../src/util/narration-cache.ts"],"names":[],"mappings":"AAIA,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,CAAC,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC;CAC9B;AAID,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAIpE;AAED,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAO5E;AAED,wBAAsB,SAAS,CAC7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,SAAS,GAAG,IAAI,EACvB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAIT;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,CAER;AAED,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GACzD,SAAS,CASX"}
|