@scenar/cli 0.0.2 → 0.0.3
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scenar/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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.
|
|
27
|
+
"@scenar/sdk": "0.0.3",
|
|
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 {
|
|
6
|
-
import type { NarrationManifest, TtsProvider } from "../tts/types.js";
|
|
6
|
+
import type { TtsProvider } from "../tts/types.js";
|
|
7
7
|
|
|
8
|
-
vi.mock("../util/load-
|
|
9
|
-
|
|
8
|
+
vi.mock("../util/load-ts.js", () => ({
|
|
9
|
+
loadStepsFromTs: vi.fn(),
|
|
10
10
|
}));
|
|
11
11
|
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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-
|
|
63
|
-
{ audio: Buffer.from("audio-
|
|
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, "
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
124
|
+
mockDiscover.mockResolvedValue([
|
|
125
|
+
{ id: "cached-demo", stepsPath: join(scenariosDir, "cached-demo", "steps.ts") },
|
|
126
|
+
]);
|
|
77
127
|
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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("
|
|
105
|
-
expect(process.exitCode).toBeUndefined();
|
|
193
|
+
expect(stderrData).toContain("no narration");
|
|
106
194
|
expect(provider.synthesize).not.toHaveBeenCalled();
|
|
107
195
|
});
|
|
108
196
|
|
|
109
|
-
it("
|
|
110
|
-
|
|
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
|
-
|
|
204
|
+
mockResolveProvider.mockResolvedValue(provider);
|
|
114
205
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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;
|
|
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"}
|
package/src/commands/narrate.js
CHANGED
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
.
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
54
|
-
|
|
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:
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
};
|
|
71
|
-
|
|
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":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"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"}
|