@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenar/cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Scenar CLI — validate scenario YAML and generate narration audio.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -24,7 +24,7 @@
24
24
  "main": "./src/index.js",
25
25
  "types": "./src/index.d.ts",
26
26
  "dependencies": {
27
- "@scenar/sdk": "0.0.2",
27
+ "@scenar/sdk": "0.0.4",
28
28
  "commander": "^13.0.0",
29
29
  "yaml": "^2.7.0"
30
30
  },
@@ -1,17 +1,29 @@
1
1
  import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { writeFileSync, mkdirSync } from "node:fs";
2
3
  import { join } from "node:path";
3
4
  import { tmpdir } from "node:os";
4
5
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
5
- import { runNarrate } from "../commands/narrate.js";
6
- import type { NarrationManifest, TtsProvider } from "../tts/types.js";
6
+ import type { TtsProvider } from "../tts/types.js";
7
7
 
8
- vi.mock("../util/load-yaml.js", () => ({
9
- loadScenarioYaml: vi.fn(),
8
+ vi.mock("../util/load-ts.js", () => ({
9
+ loadStepsFromTs: vi.fn(),
10
10
  }));
11
11
 
12
- import { loadScenarioYaml } from "../util/load-yaml.js";
12
+ vi.mock("../util/discover-scenarios.js", () => ({
13
+ discoverScenarios: vi.fn(),
14
+ }));
15
+
16
+ vi.mock("../tts/resolve-provider.js", () => ({
17
+ resolveProvider: vi.fn(),
18
+ }));
13
19
 
14
- const mockLoad = vi.mocked(loadScenarioYaml);
20
+ import { loadStepsFromTs } from "../util/load-ts.js";
21
+ import { discoverScenarios } from "../util/discover-scenarios.js";
22
+ import { resolveProvider } from "../tts/resolve-provider.js";
23
+
24
+ const mockLoadTs = vi.mocked(loadStepsFromTs);
25
+ const mockDiscover = vi.mocked(discoverScenarios);
26
+ const mockResolveProvider = vi.mocked(resolveProvider);
15
27
 
16
28
  function createMockProvider(responses: Array<{ audio: Buffer; durationMs: number }>): TtsProvider {
17
29
  let callIndex = 0;
@@ -26,7 +38,7 @@ function createMockProvider(responses: Array<{ audio: Buffer; durationMs: number
26
38
  };
27
39
  }
28
40
 
29
- describe("scenar narrate", () => {
41
+ describe("scenar narrate — runtime manifest format", () => {
30
42
  let stderrData: string;
31
43
  let originalExitCode: number | undefined;
32
44
  let tempDir: string;
@@ -49,71 +61,158 @@ describe("scenar narrate", () => {
49
61
  rmSync(tempDir, { recursive: true, force: true });
50
62
  });
51
63
 
52
- it("generates audio files and manifest for narrated steps", async () => {
53
- mockLoad.mockResolvedValue({
54
- steps: [
55
- { view: "intro", delayMs: 0, narrationText: "Welcome to the demo." },
56
- { view: "settings", delayMs: 1500, narrationText: "" },
57
- { view: "finish", delayMs: 2000, narrationText: "That's it!" },
58
- ],
59
- });
64
+ it("generates manifest in @scenar/core format with caching", async () => {
65
+ const scenariosDir = join(tempDir, "scenarios");
66
+ mkdirSync(join(scenariosDir, "my-demo"), { recursive: true });
67
+ writeFileSync(join(scenariosDir, "my-demo", "steps.ts"), "");
68
+
69
+ mockDiscover.mockResolvedValue([
70
+ { id: "my-demo", stepsPath: join(scenariosDir, "my-demo", "steps.ts") },
71
+ ]);
72
+
73
+ mockLoadTs.mockResolvedValue([
74
+ { delayMs: 0, narration: "Welcome to the demo." },
75
+ { delayMs: 1500 },
76
+ { delayMs: 2000, narration: "That's it!" },
77
+ ]);
60
78
 
61
79
  const provider = createMockProvider([
62
- { audio: Buffer.from("audio-data-0"), durationMs: 2340 },
63
- { audio: Buffer.from("audio-data-2"), durationMs: 1870 },
80
+ { audio: Buffer.from("audio-0"), durationMs: 2340 },
81
+ { audio: Buffer.from("audio-2"), durationMs: 1870 },
64
82
  ]);
83
+ mockResolveProvider.mockResolvedValue(provider);
65
84
 
66
- const outDir = join(tempDir, "narration");
67
- await runNarrate("demo.yaml", { tts: "echogarden", out: outDir }, provider);
85
+ const outDir = join(tempDir, "out");
86
+
87
+ const { registerNarrateCommand } = await import("../commands/narrate.js");
88
+ const { Command } = await import("commander");
89
+ const program = new Command();
90
+ registerNarrateCommand(program);
91
+
92
+ await program.parseAsync([
93
+ "node", "scenar", "narrate", scenariosDir,
94
+ "--out", outDir, "--tts", "edge-tts", "--base-url", "/demos",
95
+ ]);
96
+
97
+ expect(existsSync(join(outDir, "my-demo", "step-0.mp3"))).toBe(true);
98
+ expect(existsSync(join(outDir, "my-demo", "step-2.mp3"))).toBe(true);
99
+ expect(existsSync(join(outDir, "my-demo", "step-1.mp3"))).toBe(false);
100
+
101
+ const manifest = JSON.parse(readFileSync(join(outDir, "my-demo", "manifest.json"), "utf-8"));
102
+ expect(manifest.steps).toHaveLength(3);
103
+ expect(manifest.steps[0]).toEqual({ src: "/demos/my-demo/step-0.mp3", durationMs: 2340 });
104
+ expect(manifest.steps[1]).toBeNull();
105
+ expect(manifest.steps[2]).toEqual({ src: "/demos/my-demo/step-2.mp3", durationMs: 1870 });
106
+
107
+ expect(manifest).not.toHaveProperty("generatedAt");
108
+ expect(manifest).not.toHaveProperty("ttsProvider");
109
+
110
+ const cache = JSON.parse(readFileSync(join(outDir, "my-demo", ".narration-cache.json"), "utf-8"));
111
+ expect(cache.steps).toHaveLength(3);
112
+ expect(cache.steps[0]).toHaveProperty("hash");
113
+ expect(cache.steps[0]).toHaveProperty("durationMs", 2340);
114
+ expect(cache.steps[1]).toBeNull();
68
115
 
69
116
  expect(provider.synthesize).toHaveBeenCalledTimes(2);
117
+ });
70
118
 
71
- expect(existsSync(join(outDir, "step-0.mp3"))).toBe(true);
72
- expect(existsSync(join(outDir, "step-2.mp3"))).toBe(true);
73
- expect(existsSync(join(outDir, "step-1.mp3"))).toBe(false);
119
+ it("uses cache on second run when narration text is unchanged", async () => {
120
+ const scenariosDir = join(tempDir, "scenarios");
121
+ mkdirSync(join(scenariosDir, "cached-demo"), { recursive: true });
122
+ writeFileSync(join(scenariosDir, "cached-demo", "steps.ts"), "");
74
123
 
75
- const rawAudio = readFileSync(join(outDir, "step-0.mp3"), "utf-8");
76
- expect(rawAudio).toBe("audio-data-0");
124
+ mockDiscover.mockResolvedValue([
125
+ { id: "cached-demo", stepsPath: join(scenariosDir, "cached-demo", "steps.ts") },
126
+ ]);
77
127
 
78
- const manifestRaw = readFileSync(join(outDir, "manifest.json"), "utf-8");
79
- const manifest: NarrationManifest = JSON.parse(manifestRaw);
128
+ mockLoadTs.mockResolvedValue([
129
+ { delayMs: 0, narration: "Hello world." },
130
+ { delayMs: 1000 },
131
+ ]);
132
+
133
+ const provider1 = createMockProvider([
134
+ { audio: Buffer.from("audio-hello"), durationMs: 1200 },
135
+ ]);
136
+ mockResolveProvider.mockResolvedValue(provider1);
80
137
 
81
- expect(manifest.ttsProvider).toBe("test-tts");
82
- expect(manifest.steps).toHaveLength(2);
83
- expect(manifest.steps[0]!.index).toBe(0);
84
- expect(manifest.steps[0]!.file).toBe("step-0.mp3");
85
- expect(manifest.steps[0]!.durationMs).toBe(2340);
86
- expect(manifest.steps[0]!.text).toBe("Welcome to the demo.");
87
- expect(manifest.steps[1]!.index).toBe(2);
88
- expect(manifest.steps[1]!.durationMs).toBe(1870);
138
+ const outDir = join(tempDir, "out");
89
139
 
90
- expect(stderrData).toContain("Generated 2 audio file(s)");
140
+ const { registerNarrateCommand } = await import("../commands/narrate.js");
141
+ const { Command } = await import("commander");
142
+
143
+ const program1 = new Command();
144
+ registerNarrateCommand(program1);
145
+ await program1.parseAsync([
146
+ "node", "scenar", "narrate", scenariosDir,
147
+ "--out", outDir, "--tts", "edge-tts", "--base-url", "/demos",
148
+ ]);
149
+
150
+ expect(provider1.synthesize).toHaveBeenCalledTimes(1);
151
+
152
+ const provider2 = createMockProvider([]);
153
+ mockResolveProvider.mockResolvedValue(provider2);
154
+
155
+ const program2 = new Command();
156
+ registerNarrateCommand(program2);
157
+ await program2.parseAsync([
158
+ "node", "scenar", "narrate", scenariosDir,
159
+ "--out", outDir, "--tts", "edge-tts", "--base-url", "/demos",
160
+ ]);
161
+
162
+ expect(provider2.synthesize).not.toHaveBeenCalled();
163
+ expect(stderrData).toContain("cached");
91
164
  });
92
165
 
93
- it("warns when no steps have narration text", async () => {
94
- mockLoad.mockResolvedValue({
95
- steps: [
96
- { view: "intro", delayMs: 0 },
97
- { view: "finish", delayMs: 1000 },
98
- ],
99
- });
166
+ it("warns when no scenarios have narration", async () => {
167
+ const scenariosDir = join(tempDir, "scenarios");
168
+ mkdirSync(join(scenariosDir, "empty-demo"), { recursive: true });
169
+ writeFileSync(join(scenariosDir, "empty-demo", "steps.ts"), "");
170
+
171
+ mockDiscover.mockResolvedValue([
172
+ { id: "empty-demo", stepsPath: join(scenariosDir, "empty-demo", "steps.ts") },
173
+ ]);
174
+
175
+ mockLoadTs.mockResolvedValue([
176
+ { delayMs: 0 },
177
+ { delayMs: 1000 },
178
+ ]);
100
179
 
101
180
  const provider = createMockProvider([]);
102
- await runNarrate("demo.yaml", { tts: "echogarden", out: tempDir }, provider);
181
+ mockResolveProvider.mockResolvedValue(provider);
182
+
183
+ const { registerNarrateCommand } = await import("../commands/narrate.js");
184
+ const { Command } = await import("commander");
185
+ const program = new Command();
186
+ registerNarrateCommand(program);
187
+
188
+ await program.parseAsync([
189
+ "node", "scenar", "narrate", scenariosDir,
190
+ "--out", join(tempDir, "out"), "--tts", "edge-tts",
191
+ ]);
103
192
 
104
- expect(stderrData).toContain("No steps contain narration text");
105
- expect(process.exitCode).toBeUndefined();
193
+ expect(stderrData).toContain("no narration");
106
194
  expect(provider.synthesize).not.toHaveBeenCalled();
107
195
  });
108
196
 
109
- it("exits with error if scenario is invalid", async () => {
110
- mockLoad.mockResolvedValue({ steps: [] });
197
+ it("warns when no scenario directories found", async () => {
198
+ const emptyDir = join(tempDir, "empty");
199
+ mkdirSync(emptyDir, { recursive: true });
200
+
201
+ mockDiscover.mockResolvedValue([]);
111
202
 
112
203
  const provider = createMockProvider([]);
113
- await runNarrate("bad.yaml", { tts: "echogarden", out: tempDir }, provider);
204
+ mockResolveProvider.mockResolvedValue(provider);
114
205
 
115
- expect(stderrData).toContain("error");
116
- expect(process.exitCode).toBe(1);
117
- expect(provider.synthesize).not.toHaveBeenCalled();
206
+ const { registerNarrateCommand } = await import("../commands/narrate.js");
207
+ const { Command } = await import("commander");
208
+ const program = new Command();
209
+ registerNarrateCommand(program);
210
+
211
+ await program.parseAsync([
212
+ "node", "scenar", "narrate", emptyDir,
213
+ "--out", join(tempDir, "out"), "--tts", "edge-tts",
214
+ ]);
215
+
216
+ expect(stderrData).toContain("No scenario directories");
118
217
  });
119
218
  });
@@ -1,15 +1,3 @@
1
1
  import { Command } from "commander";
2
- import type { TtsProvider } from "../tts/types.js";
3
- interface NarrateOptions {
4
- tts: string;
5
- out: string;
6
- voice?: string;
7
- }
8
2
  export declare function registerNarrateCommand(program: Command): void;
9
- /**
10
- * Core narration logic, separated from provider resolution for testability.
11
- * The command handler resolves the provider, then delegates here.
12
- */
13
- export declare function runNarrate(file: string, options: NarrateOptions, provider: TtsProvider): Promise<void>;
14
- export {};
15
3
  //# sourceMappingURL=narrate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"narrate.d.ts","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,OAAO,KAAK,EAA4C,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE7F,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAY7D;AAOD;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,cAAc,EACvB,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,IAAI,CAAC,CAqEf"}
1
+ {"version":3,"file":"narrate.d.ts","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAwB7D"}
@@ -1,75 +1,190 @@
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 { loadScenarioYaml } from "../util/load-yaml.js";
4
+ import { loadStepsFromTs } from "../util/load-ts.js";
5
+ import { discoverScenarios } from "../util/discover-scenarios.js";
6
+ import { computeHash, loadCache, saveCache, fileExists, isCached, getCachedDuration, buildCacheFile, } from "../util/narration-cache.js";
4
7
  import { validateScenario } from "../validate/scenario-validator.js";
5
8
  import { resolveProvider } from "../tts/resolve-provider.js";
6
9
  export function registerNarrateCommand(program) {
7
10
  program
8
11
  .command("narrate")
9
- .description("Generate narration audio from a scenario YAML file.")
10
- .argument("<file>", "path to scenario YAML file")
11
- .option("--tts <provider>", "TTS provider: echogarden (default) or openai", "echogarden")
12
- .option("--out <dir>", "output directory for audio files", "./narration")
12
+ .description("Generate narration audio from scenario files.\n\n" +
13
+ "Accepts a YAML file, a TypeScript steps file, or a directory\n" +
14
+ "containing scenario subdirectories (each with a steps.ts).")
15
+ .argument("<file-or-dir>", "path to scenario file (.yaml/.ts) or directory")
16
+ .option("--tts <provider>", "TTS provider: echogarden, edge-tts, or openai", "echogarden")
17
+ .option("--out <dir>", "output directory for audio files")
13
18
  .option("--voice <voice>", "voice name (provider-specific)")
14
- .action(async (file, options) => {
19
+ .option("--base-url <path>", "URL path prefix for src fields in manifest")
20
+ .action(async (fileOrDir, options) => {
15
21
  const provider = await resolveProvider(options.tts);
16
- await runNarrate(file, options, provider);
22
+ const resolved = resolve(fileOrDir);
23
+ const info = await stat(resolved);
24
+ if (info.isDirectory()) {
25
+ await runNarrateDirectory(resolved, options, provider);
26
+ }
27
+ else {
28
+ await runNarrateSingleFile(resolved, options, provider);
29
+ }
17
30
  });
18
31
  }
19
- /**
20
- * Core narration logic, separated from provider resolution for testability.
21
- * The command handler resolves the provider, then delegates here.
22
- */
23
- export async function runNarrate(file, options, provider) {
24
- const scenario = await loadScenarioYaml(file);
32
+ function extractNarratedSteps(steps) {
33
+ const result = [];
34
+ for (let i = 0; i < steps.length; i++) {
35
+ const text = steps[i].narration ?? steps[i].narrationText;
36
+ if (typeof text === "string" && text.length > 0) {
37
+ result.push({ index: i, text });
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ function buildRuntimeManifest(totalSteps, entries) {
43
+ const steps = Array.from({ length: totalSteps }, () => null);
44
+ for (const [idx, entry] of entries) {
45
+ steps[idx] = entry;
46
+ }
47
+ return { steps };
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // Single-file narration
51
+ // ---------------------------------------------------------------------------
52
+ async function loadStepsFromFile(filePath) {
53
+ const ext = extname(filePath).toLowerCase();
54
+ if (ext === ".ts" || ext === ".tsx") {
55
+ const steps = await loadStepsFromTs(filePath);
56
+ return { steps, totalSteps: steps.length };
57
+ }
58
+ const scenario = await loadScenarioYaml(filePath);
25
59
  const validation = validateScenario(scenario);
26
60
  if (!validation.valid) {
27
- process.stderr.write(`\x1b[31m✗\x1b[0m Scenario has ${validation.errors.length} error(s). Run 'scenar validate ${file}' for details.\n`);
28
- process.exitCode = 1;
29
- return;
61
+ throw new Error(`Scenario has ${validation.errors.length} error(s). Run 'scenar validate ${filePath}' for details.`);
30
62
  }
31
63
  const steps = scenario["steps"];
32
64
  if (!steps) {
33
- process.stderr.write("\x1b[31m✗\x1b[0m No steps found in scenario.\n");
34
- process.exitCode = 1;
65
+ throw new Error("No steps found in scenario.");
66
+ }
67
+ return { steps: steps, totalSteps: steps.length };
68
+ }
69
+ async function runNarrateSingleFile(filePath, options, provider) {
70
+ const { steps, totalSteps } = await loadStepsFromFile(filePath);
71
+ const narratedSteps = extractNarratedSteps(steps);
72
+ if (narratedSteps.length === 0) {
73
+ process.stderr.write("\x1b[33m!\x1b[0m No steps contain narration text. Nothing to generate.\n");
35
74
  return;
36
75
  }
37
- const narratedSteps = [];
38
- for (let i = 0; i < steps.length; i++) {
39
- const text = steps[i]["narrationText"];
40
- if (typeof text === "string" && text.length > 0) {
41
- narratedSteps.push({ index: i, text });
76
+ const outDir = options.out ?? join(dirname(filePath), "narration");
77
+ const voice = options.voice ?? "";
78
+ await generateNarration({
79
+ scenarioId: basename(dirname(filePath)),
80
+ narratedSteps,
81
+ totalSteps,
82
+ outDir,
83
+ voice,
84
+ baseUrl: options.baseUrl,
85
+ provider,
86
+ });
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // Directory-mode narration
90
+ // ---------------------------------------------------------------------------
91
+ async function runNarrateDirectory(dirPath, options, provider) {
92
+ const scenarios = await discoverScenarios(dirPath);
93
+ if (scenarios.length === 0) {
94
+ process.stderr.write("\x1b[33m!\x1b[0m No scenario directories with steps.ts found.\n");
95
+ return;
96
+ }
97
+ process.stderr.write(`Discovered ${scenarios.length} scenario(s)\n\n`);
98
+ let totalGenerated = 0;
99
+ let totalCached = 0;
100
+ let totalSkipped = 0;
101
+ let scenariosWithNarration = 0;
102
+ const errors = [];
103
+ for (const scenario of scenarios) {
104
+ process.stderr.write(` ${scenario.id}\n`);
105
+ try {
106
+ const steps = await loadStepsFromTs(scenario.stepsPath);
107
+ const narratedSteps = extractNarratedSteps(steps);
108
+ if (narratedSteps.length === 0) {
109
+ process.stderr.write(" (no narration)\n");
110
+ totalSkipped++;
111
+ continue;
112
+ }
113
+ scenariosWithNarration++;
114
+ const outDir = options.out
115
+ ? join(options.out, scenario.id)
116
+ : join(dirPath, scenario.id, "narration");
117
+ const stats = await generateNarration({
118
+ scenarioId: scenario.id,
119
+ narratedSteps,
120
+ totalSteps: steps.length,
121
+ outDir,
122
+ voice: options.voice ?? "",
123
+ baseUrl: options.baseUrl,
124
+ provider,
125
+ });
126
+ totalGenerated += stats.generated;
127
+ totalCached += stats.cached;
128
+ }
129
+ catch (error) {
130
+ process.stderr.write(` \x1b[31mfailed\x1b[0m: ${error}\n`);
131
+ errors.push({ id: scenario.id, error });
42
132
  }
43
133
  }
44
- if (narratedSteps.length === 0) {
45
- process.stderr.write("\x1b[33m⚠\x1b[0m No steps contain narration text. Nothing to generate.\n");
46
- return;
134
+ process.stderr.write("\n");
135
+ process.stderr.write(`Scenarios with narration: ${scenariosWithNarration}\n`);
136
+ process.stderr.write(`Audio files generated: ${totalGenerated}\n`);
137
+ process.stderr.write(`Audio files cached: ${totalCached}\n`);
138
+ process.stderr.write(`Scenarios skipped: ${totalSkipped}\n`);
139
+ if (errors.length > 0) {
140
+ process.stderr.write(`\n\x1b[31m${errors.length} scenario(s) failed:\x1b[0m\n`);
141
+ for (const { id, error } of errors) {
142
+ process.stderr.write(` - ${id}: ${error}\n`);
143
+ }
144
+ process.exitCode = 1;
145
+ }
146
+ else {
147
+ process.stderr.write("\nDone\n");
47
148
  }
48
- await mkdir(options.out, { recursive: true });
49
- const manifestSteps = [];
50
- for (let i = 0; i < narratedSteps.length; i++) {
51
- const step = narratedSteps[i];
149
+ }
150
+ async function generateNarration(opts) {
151
+ const { scenarioId, narratedSteps, totalSteps, outDir, voice, baseUrl, provider } = opts;
152
+ await mkdir(outDir, { recursive: true });
153
+ const existingCache = await loadCache(outDir);
154
+ const stats = { generated: 0, cached: 0 };
155
+ const manifestEntries = new Map();
156
+ const cacheEntries = new Map();
157
+ for (const step of narratedSteps) {
158
+ const hash = computeHash(step.text, voice);
52
159
  const fileName = `step-${step.index}.mp3`;
53
- const outputPath = join(options.out, fileName);
54
- process.stderr.write(` [${i + 1}/${narratedSteps.length}] Generating audio for step ${step.index}...\n`);
160
+ const mp3Path = join(outDir, fileName);
161
+ const srcPrefix = baseUrl
162
+ ? `${baseUrl.replace(/\/$/, "")}/${scenarioId}`
163
+ : `.`;
164
+ const src = `${srcPrefix}/${fileName}`;
165
+ if (isCached(existingCache, step.index, hash, voice) && await fileExists(mp3Path)) {
166
+ const durationMs = getCachedDuration(existingCache, step.index);
167
+ manifestEntries.set(step.index, { src, durationMs });
168
+ cacheEntries.set(step.index, { hash, durationMs });
169
+ stats.cached++;
170
+ process.stderr.write(` step ${step.index}: cached\n`);
171
+ continue;
172
+ }
173
+ process.stderr.write(` step ${step.index}: generating...\n`);
55
174
  const result = await provider.synthesize(step.text, {
56
- voice: options.voice,
57
- });
58
- await writeFile(outputPath, result.audio);
59
- manifestSteps.push({
60
- index: step.index,
61
- file: fileName,
62
- durationMs: result.durationMs,
63
- text: step.text,
175
+ voice: voice || undefined,
64
176
  });
177
+ await writeFile(mp3Path, result.audio);
178
+ manifestEntries.set(step.index, { src, durationMs: result.durationMs });
179
+ cacheEntries.set(step.index, { hash, durationMs: result.durationMs });
180
+ stats.generated++;
181
+ process.stderr.write(` step ${step.index}: ${result.durationMs}ms (${result.audio.length} bytes)\n`);
65
182
  }
66
- const manifest = {
67
- generatedAt: new Date().toISOString(),
68
- ttsProvider: provider.name,
69
- steps: manifestSteps,
70
- };
71
- const manifestPath = join(options.out, "manifest.json");
72
- await writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
73
- process.stderr.write(`\n\x1b[32m✓\x1b[0m Generated ${manifestSteps.length} audio file(s) in ${options.out}/\n`);
183
+ const manifest = buildRuntimeManifest(totalSteps, manifestEntries);
184
+ await writeFile(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
185
+ const cache = buildCacheFile(voice, totalSteps, cacheEntries);
186
+ await saveCache(outDir, cache);
187
+ process.stderr.write(`\x1b[32m+\x1b[0m ${scenarioId}: ${stats.generated} generated, ${stats.cached} cached\n`);
188
+ return stats;
74
189
  }
75
190
  //# sourceMappingURL=narrate.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"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"}