@scenar/cli 0.1.20 → 0.2.1
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/example-bundle/assets/index-BLQ14SEo.js +147 -0
- package/example-bundle/assets/logo-f-5LjVqJ.png +0 -0
- package/example-bundle/assets/style-Dnzx_jUO.css +1 -0
- package/example-bundle/index.html +13 -0
- package/example-bundle/pack-manifest.json +36 -0
- package/example-bundle/scenario.json +9 -0
- package/package.json +17 -5
- package/src/__tests__/pack-generate-embed-entry.test.ts +59 -6
- package/src/__tests__/publish-command.test.ts +46 -0
- package/src/__tests__/publish-flow.test.ts +220 -0
- package/src/__tests__/read-viewport.test.ts +45 -0
- package/src/__tests__/run-publish.test.ts +92 -0
- package/src/__tests__/serve-command.test.ts +44 -0
- package/src/__tests__/serve-request-path.test.ts +67 -0
- package/src/__tests__/serve-static-server.test.ts +88 -0
- package/src/__tests__/try-command.test.ts +21 -0
- package/src/api.d.ts +31 -0
- package/src/api.d.ts.map +1 -0
- package/src/api.js +28 -0
- package/src/api.js.map +1 -0
- package/src/api.ts +55 -0
- package/src/bundle/read-viewport.d.ts +23 -0
- package/src/bundle/read-viewport.d.ts.map +1 -0
- package/src/bundle/read-viewport.js +25 -0
- package/src/bundle/read-viewport.js.map +1 -0
- package/src/bundle/read-viewport.ts +36 -0
- package/src/commands/deploy.d.ts.map +1 -1
- package/src/commands/deploy.js +8 -24
- package/src/commands/deploy.js.map +1 -1
- package/src/commands/deploy.ts +10 -28
- package/src/commands/narrate.d.ts.map +1 -1
- package/src/commands/narrate.js +39 -170
- package/src/commands/narrate.js.map +1 -1
- package/src/commands/narrate.ts +48 -289
- package/src/commands/pack.d.ts.map +1 -1
- package/src/commands/pack.js +12 -112
- package/src/commands/pack.js.map +1 -1
- package/src/commands/pack.ts +12 -128
- package/src/commands/publish.d.ts +3 -0
- package/src/commands/publish.d.ts.map +1 -0
- package/src/commands/publish.js +64 -0
- package/src/commands/publish.js.map +1 -0
- package/src/commands/publish.ts +86 -0
- package/src/commands/render.d.ts.map +1 -1
- package/src/commands/render.js +12 -194
- package/src/commands/render.js.map +1 -1
- package/src/commands/render.ts +19 -286
- package/src/commands/serve.d.ts +3 -0
- package/src/commands/serve.d.ts.map +1 -0
- package/src/commands/serve.js +91 -0
- package/src/commands/serve.js.map +1 -0
- package/src/commands/serve.ts +112 -0
- package/src/commands/try.d.ts +3 -0
- package/src/commands/try.d.ts.map +1 -0
- package/src/commands/try.js +99 -0
- package/src/commands/try.js.map +1 -0
- package/src/commands/try.ts +122 -0
- package/src/index.d.ts.map +1 -1
- package/src/index.js +6 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +6 -0
- package/src/narrate/run-narrate.d.ts +43 -0
- package/src/narrate/run-narrate.d.ts.map +1 -0
- package/src/narrate/run-narrate.js +174 -0
- package/src/narrate/run-narrate.js.map +1 -0
- package/src/narrate/run-narrate.ts +298 -0
- package/src/pack/generate-embed-entry.d.ts +11 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -1
- package/src/pack/generate-embed-entry.js +81 -10
- package/src/pack/generate-embed-entry.js.map +1 -1
- package/src/pack/generate-embed-entry.ts +81 -10
- package/src/pack/run-pack.d.ts +34 -0
- package/src/pack/run-pack.d.ts.map +1 -0
- package/src/pack/run-pack.js +117 -0
- package/src/pack/run-pack.js.map +1 -0
- package/src/pack/run-pack.ts +163 -0
- package/src/publish/github.d.ts +11 -0
- package/src/publish/github.d.ts.map +1 -0
- package/src/publish/github.js +197 -0
- package/src/publish/github.js.map +1 -0
- package/src/publish/github.ts +216 -0
- package/src/publish/publish-flow.d.ts +110 -0
- package/src/publish/publish-flow.d.ts.map +1 -0
- package/src/publish/publish-flow.js +117 -0
- package/src/publish/publish-flow.js.map +1 -0
- package/src/publish/publish-flow.ts +199 -0
- package/src/publish/run-publish.d.ts +41 -0
- package/src/publish/run-publish.d.ts.map +1 -0
- package/src/publish/run-publish.js +77 -0
- package/src/publish/run-publish.js.map +1 -0
- package/src/publish/run-publish.ts +131 -0
- package/src/render/run-render.d.ts +31 -0
- package/src/render/run-render.d.ts.map +1 -0
- package/src/render/run-render.js +196 -0
- package/src/render/run-render.js.map +1 -0
- package/src/render/run-render.ts +287 -0
- package/src/serve/request-path.d.ts +23 -0
- package/src/serve/request-path.d.ts.map +1 -0
- package/src/serve/request-path.js +52 -0
- package/src/serve/request-path.js.map +1 -0
- package/src/serve/request-path.ts +55 -0
- package/src/serve/run-serve.d.ts +31 -0
- package/src/serve/run-serve.d.ts.map +1 -0
- package/src/serve/run-serve.js +35 -0
- package/src/serve/run-serve.js.map +1 -0
- package/src/serve/run-serve.ts +68 -0
- package/src/serve/static-server.d.ts +38 -0
- package/src/serve/static-server.d.ts.map +1 -0
- package/src/serve/static-server.js +91 -0
- package/src/serve/static-server.js.map +1 -0
- package/src/serve/static-server.ts +118 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/commands/deploy.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { resolve, join } from "node:path";
|
|
|
2
2
|
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import type { PackManifest } from "../pack/pack-manifest.js";
|
|
5
|
-
import { PACK_MANIFEST_FILE
|
|
6
|
-
import {
|
|
5
|
+
import { PACK_MANIFEST_FILE } from "../pack/pack-manifest.js";
|
|
6
|
+
import { readBundleViewport } from "../bundle/read-viewport.js";
|
|
7
7
|
import { createBackendClients } from "../deploy/client.js";
|
|
8
8
|
import { putFile } from "../deploy/upload.js";
|
|
9
9
|
import { buildEmbedSnippet } from "../deploy/embed-snippet.js";
|
|
@@ -91,7 +91,14 @@ export function registerDeployCommand(program: Command): void {
|
|
|
91
91
|
// piping). Responsive iframe at the bundle's canonical aspect ratio
|
|
92
92
|
// (DD-002/DD-004); falls back to the default viewport for bundles packed
|
|
93
93
|
// before the viewport was recorded.
|
|
94
|
-
const viewport = await
|
|
94
|
+
const { viewport, recorded } = await readBundleViewport(resolvedDir);
|
|
95
|
+
if (!recorded) {
|
|
96
|
+
process.stderr.write(
|
|
97
|
+
` Note: no recorded viewport for "${name}"; ` +
|
|
98
|
+
`snippet uses the default ${viewport.width}x${viewport.height}. ` +
|
|
99
|
+
"Re-pack with the current CLI to embed at the exact aspect ratio.\n",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
95
102
|
process.stderr.write("\n Embed snippet (paste into any page):\n\n");
|
|
96
103
|
process.stderr.write(`${indent(buildEmbedSnippet({ embedUrl, viewport, title: name }))}\n`);
|
|
97
104
|
} catch (error) {
|
|
@@ -156,31 +163,6 @@ function makeDeps(
|
|
|
156
163
|
};
|
|
157
164
|
}
|
|
158
165
|
|
|
159
|
-
/**
|
|
160
|
-
* Read the canonical viewport recorded in the bundle's scenario.json (DD-004).
|
|
161
|
-
* Falls back to {@link DEFAULT_VIEWPORT} for bundles packed before the viewport
|
|
162
|
-
* was recorded, or if the file is missing/malformed — a snippet with sensible
|
|
163
|
-
* proportions always beats no snippet. `scenarioLabel` is only for the warning.
|
|
164
|
-
*/
|
|
165
|
-
async function readScenarioViewport(
|
|
166
|
-
bundleDir: string,
|
|
167
|
-
scenarioLabel: string,
|
|
168
|
-
): Promise<Viewport> {
|
|
169
|
-
try {
|
|
170
|
-
const raw = await readFile(join(bundleDir, SCENARIO_JSON_FILE), "utf-8");
|
|
171
|
-
const viewport = parseViewport((JSON.parse(raw) as { viewport?: unknown }).viewport);
|
|
172
|
-
if (viewport) return viewport;
|
|
173
|
-
} catch {
|
|
174
|
-
// Missing/unreadable/invalid scenario.json — fall through to the default.
|
|
175
|
-
}
|
|
176
|
-
process.stderr.write(
|
|
177
|
-
` Note: no recorded viewport for "${scenarioLabel}"; ` +
|
|
178
|
-
`snippet uses the default ${DEFAULT_VIEWPORT.width}x${DEFAULT_VIEWPORT.height}. ` +
|
|
179
|
-
"Re-pack with the current CLI to embed at the exact aspect ratio.\n",
|
|
180
|
-
);
|
|
181
|
-
return DEFAULT_VIEWPORT;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
166
|
/** Indent every line by two spaces (for nesting a block under a heading). */
|
|
185
167
|
function indent(block: string): string {
|
|
186
168
|
return block.replace(/^/gm, " ");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"narrate.d.ts","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"narrate.d.ts","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA6D7D"}
|
package/src/commands/narrate.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join, resolve, extname, basename, dirname } from "node:path";
|
|
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";
|
|
7
|
-
import { validateScenario } from "../validate/scenario-validator.js";
|
|
8
|
-
import { resolveProvider } from "../tts/resolve-provider.js";
|
|
1
|
+
import { runNarrate } from "../narrate/run-narrate.js";
|
|
9
2
|
export function registerNarrateCommand(program) {
|
|
10
3
|
program
|
|
11
4
|
.command("narrate")
|
|
@@ -18,173 +11,49 @@ export function registerNarrateCommand(program) {
|
|
|
18
11
|
.option("--voice <voice>", "voice name (provider-specific)")
|
|
19
12
|
.option("--base-url <path>", "URL path prefix for src fields in manifest")
|
|
20
13
|
.action(async (fileOrDir, options) => {
|
|
21
|
-
const provider = await resolveProvider(options.tts);
|
|
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
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
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);
|
|
59
|
-
const validation = validateScenario(scenario);
|
|
60
|
-
if (!validation.valid) {
|
|
61
|
-
throw new Error(`Scenario has ${validation.errors.length} error(s). Run 'scenar validate ${filePath}' for details.`);
|
|
62
|
-
}
|
|
63
|
-
const steps = scenario["steps"];
|
|
64
|
-
if (!steps) {
|
|
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");
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
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
14
|
try {
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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 ?? "",
|
|
15
|
+
const result = await runNarrate({
|
|
16
|
+
target: fileOrDir,
|
|
17
|
+
tts: options.tts,
|
|
18
|
+
out: options.out,
|
|
19
|
+
voice: options.voice,
|
|
123
20
|
baseUrl: options.baseUrl,
|
|
124
|
-
|
|
21
|
+
onLog: (message) => process.stderr.write(`${message}\n`),
|
|
125
22
|
});
|
|
126
|
-
|
|
127
|
-
|
|
23
|
+
if (result.mode === "file") {
|
|
24
|
+
const only = result.scenarios[0];
|
|
25
|
+
if (only?.skipped) {
|
|
26
|
+
process.stderr.write("\x1b[33m!\x1b[0m No steps contain narration text. Nothing to generate.\n");
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
// Directory mode: print the aggregate summary.
|
|
31
|
+
if (result.scenarios.length === 0) {
|
|
32
|
+
process.stderr.write("\x1b[33m!\x1b[0m No scenario directories with steps.ts found.\n");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const withNarration = result.scenarios.filter((s) => !s.skipped).length;
|
|
36
|
+
process.stderr.write("\n");
|
|
37
|
+
process.stderr.write(`Scenarios with narration: ${withNarration}\n`);
|
|
38
|
+
process.stderr.write(`Audio files generated: ${result.totalGenerated}\n`);
|
|
39
|
+
process.stderr.write(`Audio files cached: ${result.totalCached}\n`);
|
|
40
|
+
process.stderr.write(`Scenarios skipped: ${result.totalSkipped}\n`);
|
|
41
|
+
if (result.errors.length > 0) {
|
|
42
|
+
process.stderr.write(`\n\x1b[31m${result.errors.length} scenario(s) failed:\x1b[0m\n`);
|
|
43
|
+
for (const { id, message } of result.errors) {
|
|
44
|
+
process.stderr.write(` - ${id}: ${message}\n`);
|
|
45
|
+
}
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
process.stderr.write("\nDone\n");
|
|
50
|
+
}
|
|
128
51
|
}
|
|
129
52
|
catch (error) {
|
|
130
|
-
|
|
131
|
-
|
|
53
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
54
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
55
|
+
process.exitCode = 1;
|
|
132
56
|
}
|
|
133
|
-
}
|
|
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");
|
|
148
|
-
}
|
|
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);
|
|
159
|
-
const fileName = `step-${step.index}.mp3`;
|
|
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`);
|
|
174
|
-
const result = await provider.synthesize(step.text, {
|
|
175
|
-
voice: voice || undefined,
|
|
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`);
|
|
182
|
-
}
|
|
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;
|
|
57
|
+
});
|
|
189
58
|
}
|
|
190
59
|
//# sourceMappingURL=narrate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"narrate.js","sourceRoot":"","sources":["../../../src/commands/narrate.ts"],"names":[],"mappings":"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"}
|
package/src/commands/narrate.ts
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
import { mkdir, writeFile, stat } from "node:fs/promises";
|
|
2
|
-
import { join, resolve, extname, basename, dirname } from "node:path";
|
|
3
1
|
import { Command } from "commander";
|
|
4
|
-
import {
|
|
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";
|
|
16
|
-
import { validateScenario } from "../validate/scenario-validator.js";
|
|
17
|
-
import { resolveProvider } from "../tts/resolve-provider.js";
|
|
18
|
-
import type { TtsProvider } from "../tts/types.js";
|
|
2
|
+
import { runNarrate } from "../narrate/run-narrate.js";
|
|
19
3
|
|
|
20
4
|
interface NarrateOptions {
|
|
21
5
|
tts: string;
|
|
@@ -29,8 +13,8 @@ export function registerNarrateCommand(program: Command): void {
|
|
|
29
13
|
.command("narrate")
|
|
30
14
|
.description(
|
|
31
15
|
"Generate narration audio from scenario files.\n\n" +
|
|
32
|
-
|
|
33
|
-
|
|
16
|
+
"Accepts a YAML file, a TypeScript steps file, or a directory\n" +
|
|
17
|
+
"containing scenario subdirectories (each with a steps.ts).",
|
|
34
18
|
)
|
|
35
19
|
.argument("<file-or-dir>", "path to scenario file (.yaml/.ts) or directory")
|
|
36
20
|
.option("--tts <provider>", "TTS provider: echogarden, edge-tts, or openai", "echogarden")
|
|
@@ -38,276 +22,51 @@ export function registerNarrateCommand(program: Command): void {
|
|
|
38
22
|
.option("--voice <voice>", "voice name (provider-specific)")
|
|
39
23
|
.option("--base-url <path>", "URL path prefix for src fields in manifest")
|
|
40
24
|
.action(async (fileOrDir: string, options: NarrateOptions) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
25
|
+
try {
|
|
26
|
+
const result = await runNarrate({
|
|
27
|
+
target: fileOrDir,
|
|
28
|
+
tts: options.tts,
|
|
29
|
+
out: options.out,
|
|
30
|
+
voice: options.voice,
|
|
31
|
+
baseUrl: options.baseUrl,
|
|
32
|
+
onLog: (message) => process.stderr.write(`${message}\n`),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (result.mode === "file") {
|
|
36
|
+
const only = result.scenarios[0];
|
|
37
|
+
if (only?.skipped) {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
"\x1b[33m!\x1b[0m No steps contain narration text. Nothing to generate.\n",
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Directory mode: print the aggregate summary.
|
|
46
|
+
if (result.scenarios.length === 0) {
|
|
47
|
+
process.stderr.write("\x1b[33m!\x1b[0m No scenario directories with steps.ts found.\n");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const withNarration = result.scenarios.filter((s) => !s.skipped).length;
|
|
51
|
+
process.stderr.write("\n");
|
|
52
|
+
process.stderr.write(`Scenarios with narration: ${withNarration}\n`);
|
|
53
|
+
process.stderr.write(`Audio files generated: ${result.totalGenerated}\n`);
|
|
54
|
+
process.stderr.write(`Audio files cached: ${result.totalCached}\n`);
|
|
55
|
+
process.stderr.write(`Scenarios skipped: ${result.totalSkipped}\n`);
|
|
56
|
+
|
|
57
|
+
if (result.errors.length > 0) {
|
|
58
|
+
process.stderr.write(`\n\x1b[31m${result.errors.length} scenario(s) failed:\x1b[0m\n`);
|
|
59
|
+
for (const { id, message } of result.errors) {
|
|
60
|
+
process.stderr.write(` - ${id}: ${message}\n`);
|
|
61
|
+
}
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
} else {
|
|
64
|
+
process.stderr.write("\nDone\n");
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
68
|
+
process.stderr.write(`\x1b[31mError:\x1b[0m ${msg}\n`);
|
|
69
|
+
process.exitCode = 1;
|
|
49
70
|
}
|
|
50
71
|
});
|
|
51
72
|
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Step extraction
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
interface StepWithNarration {
|
|
58
|
-
index: number;
|
|
59
|
-
text: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
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,
|
|
137
|
-
options: NarrateOptions,
|
|
138
|
-
provider: TtsProvider,
|
|
139
|
-
): Promise<void> {
|
|
140
|
-
const { steps, totalSteps } = await loadStepsFromFile(filePath);
|
|
141
|
-
const narratedSteps = extractNarratedSteps(steps);
|
|
142
|
-
|
|
143
|
-
if (narratedSteps.length === 0) {
|
|
144
|
-
process.stderr.write("\x1b[33m!\x1b[0m No steps contain narration text. Nothing to generate.\n");
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
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");
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
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 });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
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");
|
|
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;
|
|
260
|
-
|
|
261
|
-
await mkdir(outDir, { recursive: true });
|
|
262
|
-
|
|
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 }>();
|
|
267
|
-
|
|
268
|
-
for (const step of narratedSteps) {
|
|
269
|
-
const hash = computeHash(step.text, voice);
|
|
270
|
-
const fileName = `step-${step.index}.mp3`;
|
|
271
|
-
const mp3Path = join(outDir, fileName);
|
|
272
|
-
|
|
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
|
-
}
|
|
286
|
-
|
|
287
|
-
process.stderr.write(` step ${step.index}: generating...\n`);
|
|
288
|
-
const result = await provider.synthesize(step.text, {
|
|
289
|
-
voice: voice || undefined,
|
|
290
|
-
});
|
|
291
|
-
|
|
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`);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const manifest = buildRuntimeManifest(totalSteps, manifestEntries);
|
|
300
|
-
await writeFile(
|
|
301
|
-
join(outDir, "manifest.json"),
|
|
302
|
-
JSON.stringify(manifest, null, 2) + "\n",
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
const cache = buildCacheFile(voice, totalSteps, cacheEntries);
|
|
306
|
-
await saveCache(outDir, cache);
|
|
307
|
-
|
|
308
|
-
process.stderr.write(
|
|
309
|
-
`\x1b[32m+\x1b[0m ${scenarioId}: ${stats.generated} generated, ${stats.cached} cached\n`,
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
return stats;
|
|
313
|
-
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pack.d.ts","sourceRoot":"","sources":["../../../src/commands/pack.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"pack.d.ts","sourceRoot":"","sources":["../../../src/commands/pack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiBpC,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAiD1D"}
|