@scenar/cli 0.9.1 → 0.11.0
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-DxmqjG-8.js +388 -0
- package/example-bundle/assets/style-B2sxnVVZ.css +1 -0
- package/example-bundle/embed.js +1 -1
- package/example-bundle/index.html +2 -2
- package/example-bundle/logo.png +0 -0
- package/example-bundle/pack-manifest.json +33 -9
- package/example-bundle/soundtrack/music.mp3 +0 -0
- package/example-bundle/soundtrack/sfx/click.mp3 +0 -0
- package/example-bundle/soundtrack/sfx/keystroke.mp3 +0 -0
- package/package.json +4 -4
- package/src/__tests__/collect-pack-shots.test.ts +27 -1
- package/src/__tests__/elevenlabs-provider.test.ts +187 -0
- package/src/__tests__/embed-snippet.test.ts +5 -0
- package/src/__tests__/find-authored-soundtrack.test.ts +54 -0
- package/src/__tests__/find-authored-title-cards.test.ts +65 -0
- package/src/__tests__/find-authored-viewport.test.ts +58 -0
- package/src/__tests__/load-bundle.test.ts +67 -0
- package/src/__tests__/narrate-command.test.ts +65 -1
- package/src/__tests__/narration-cache.test.ts +134 -0
- package/src/__tests__/pack-bundle-contract.test.ts +2 -0
- package/src/__tests__/pack-generate-embed-entry.test.ts +99 -16
- package/src/__tests__/presenter-command.test.ts +366 -0
- package/src/__tests__/render-command.test.ts +11 -0
- package/src/__tests__/render-generate-entry.test.ts +102 -0
- package/src/__tests__/resolve-pack-viewport.test.ts +46 -0
- package/src/__tests__/resolve-provider.test.ts +32 -3
- package/src/__tests__/scenario-validator.test.ts +174 -0
- package/src/__tests__/stage-assets.test.ts +269 -0
- package/src/commands/narrate.js +1 -1
- package/src/commands/narrate.js.map +1 -1
- package/src/commands/narrate.ts +1 -1
- package/src/commands/pack.d.ts.map +1 -1
- package/src/commands/pack.js +23 -4
- package/src/commands/pack.js.map +1 -1
- package/src/commands/pack.ts +26 -5
- package/src/commands/presenter.d.ts +3 -0
- package/src/commands/presenter.d.ts.map +1 -0
- package/src/commands/presenter.js +89 -0
- package/src/commands/presenter.js.map +1 -0
- package/src/commands/presenter.ts +107 -0
- package/src/commands/render.d.ts.map +1 -1
- package/src/commands/render.js +2 -0
- package/src/commands/render.js.map +1 -1
- package/src/commands/render.ts +6 -0
- package/src/embed/embed-snippet.d.ts +5 -0
- package/src/embed/embed-snippet.d.ts.map +1 -1
- package/src/embed/embed-snippet.js +6 -1
- package/src/embed/embed-snippet.js.map +1 -1
- package/src/embed/embed-snippet.ts +6 -1
- package/src/index.d.ts.map +1 -1
- package/src/index.js +2 -0
- package/src/index.js.map +1 -1
- package/src/index.ts +2 -0
- package/src/narrate/run-narrate.d.ts +15 -1
- package/src/narrate/run-narrate.d.ts.map +1 -1
- package/src/narrate/run-narrate.js +11 -4
- package/src/narrate/run-narrate.js.map +1 -1
- package/src/narrate/run-narrate.ts +15 -6
- package/src/pack/bundle-contract.d.ts +1 -0
- package/src/pack/bundle-contract.d.ts.map +1 -1
- package/src/pack/bundle-contract.js +3 -0
- package/src/pack/bundle-contract.js.map +1 -1
- package/src/pack/bundle-contract.ts +3 -0
- package/src/pack/collect-pack-shots.d.ts +16 -5
- package/src/pack/collect-pack-shots.d.ts.map +1 -1
- package/src/pack/collect-pack-shots.js +13 -4
- package/src/pack/collect-pack-shots.js.map +1 -1
- package/src/pack/collect-pack-shots.ts +35 -8
- package/src/pack/generate-embed-entry.d.ts +7 -0
- package/src/pack/generate-embed-entry.d.ts.map +1 -1
- package/src/pack/generate-embed-entry.js +105 -7
- package/src/pack/generate-embed-entry.js.map +1 -1
- package/src/pack/generate-embed-entry.ts +110 -7
- package/src/pack/run-pack.d.ts +8 -2
- package/src/pack/run-pack.d.ts.map +1 -1
- package/src/pack/run-pack.js +120 -17
- package/src/pack/run-pack.js.map +1 -1
- package/src/pack/run-pack.ts +164 -19
- package/src/pack/viewport.d.ts +17 -0
- package/src/pack/viewport.d.ts.map +1 -1
- package/src/pack/viewport.js +19 -0
- package/src/pack/viewport.js.map +1 -1
- package/src/pack/viewport.ts +29 -0
- package/src/presenter/heygen-client.d.ts +69 -0
- package/src/presenter/heygen-client.d.ts.map +1 -0
- package/src/presenter/heygen-client.js +154 -0
- package/src/presenter/heygen-client.js.map +1 -0
- package/src/presenter/heygen-client.ts +224 -0
- package/src/presenter/run-presenter.d.ts +67 -0
- package/src/presenter/run-presenter.d.ts.map +1 -0
- package/src/presenter/run-presenter.js +348 -0
- package/src/presenter/run-presenter.js.map +1 -0
- package/src/presenter/run-presenter.ts +536 -0
- package/src/render/generate-entry.d.ts +4 -0
- package/src/render/generate-entry.d.ts.map +1 -1
- package/src/render/generate-entry.js +75 -7
- package/src/render/generate-entry.js.map +1 -1
- package/src/render/generate-entry.ts +84 -7
- package/src/render/run-render.d.ts +6 -0
- package/src/render/run-render.d.ts.map +1 -1
- package/src/render/run-render.js +58 -0
- package/src/render/run-render.js.map +1 -1
- package/src/render/run-render.ts +72 -0
- package/src/render/stage-assets.d.ts +44 -0
- package/src/render/stage-assets.d.ts.map +1 -0
- package/src/render/stage-assets.js +91 -0
- package/src/render/stage-assets.js.map +1 -0
- package/src/render/stage-assets.ts +130 -0
- package/src/tts/echogarden.d.ts.map +1 -1
- package/src/tts/echogarden.js +4 -2
- package/src/tts/echogarden.js.map +1 -1
- package/src/tts/echogarden.ts +5 -2
- package/src/tts/edge-tts.d.ts.map +1 -1
- package/src/tts/edge-tts.js +2 -1
- package/src/tts/edge-tts.js.map +1 -1
- package/src/tts/edge-tts.ts +2 -1
- package/src/tts/elevenlabs.d.ts +15 -0
- package/src/tts/elevenlabs.d.ts.map +1 -0
- package/src/tts/elevenlabs.js +73 -0
- package/src/tts/elevenlabs.js.map +1 -0
- package/src/tts/elevenlabs.ts +105 -0
- package/src/tts/openai.d.ts.map +1 -1
- package/src/tts/openai.js +2 -1
- package/src/tts/openai.js.map +1 -1
- package/src/tts/openai.ts +2 -1
- package/src/tts/resolve-provider.d.ts +1 -1
- package/src/tts/resolve-provider.d.ts.map +1 -1
- package/src/tts/resolve-provider.js +10 -3
- package/src/tts/resolve-provider.js.map +1 -1
- package/src/tts/resolve-provider.ts +12 -4
- package/src/tts/types.d.ts +9 -0
- package/src/tts/types.d.ts.map +1 -1
- package/src/tts/types.ts +10 -0
- package/src/util/confirm.d.ts +11 -0
- package/src/util/confirm.d.ts.map +1 -0
- package/src/util/confirm.js +24 -0
- package/src/util/confirm.js.map +1 -0
- package/src/util/confirm.ts +23 -0
- package/src/util/load-bundle.d.ts +17 -1
- package/src/util/load-bundle.d.ts.map +1 -1
- package/src/util/load-bundle.js +19 -11
- package/src/util/load-bundle.js.map +1 -1
- package/src/util/load-bundle.ts +38 -12
- package/src/util/load-ts.d.ts +100 -0
- package/src/util/load-ts.d.ts.map +1 -1
- package/src/util/load-ts.js +176 -0
- package/src/util/load-ts.js.map +1 -1
- package/src/util/load-ts.ts +227 -0
- package/src/util/narration-cache.d.ts +13 -5
- package/src/util/narration-cache.d.ts.map +1 -1
- package/src/util/narration-cache.js +10 -8
- package/src/util/narration-cache.js.map +1 -1
- package/src/util/narration-cache.ts +22 -6
- package/src/util/presenter-cache.d.ts +44 -0
- package/src/util/presenter-cache.d.ts.map +1 -0
- package/src/util/presenter-cache.js +43 -0
- package/src/util/presenter-cache.js.map +1 -0
- package/src/util/presenter-cache.ts +101 -0
- package/src/util/scenario-assets.d.ts +47 -0
- package/src/util/scenario-assets.d.ts.map +1 -0
- package/src/util/scenario-assets.js +71 -0
- package/src/util/scenario-assets.js.map +1 -0
- package/src/util/scenario-assets.ts +111 -0
- package/src/util/soundtrack-assets.d.ts +34 -0
- package/src/util/soundtrack-assets.d.ts.map +1 -0
- package/src/util/soundtrack-assets.js +48 -0
- package/src/util/soundtrack-assets.js.map +1 -0
- package/src/util/soundtrack-assets.ts +69 -0
- package/src/validate/scenario-validator.d.ts.map +1 -1
- package/src/validate/scenario-validator.js +82 -0
- package/src/validate/scenario-validator.js.map +1 -1
- package/src/validate/scenario-validator.ts +110 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/example-bundle/assets/index-DlmG1Uh7.js +0 -388
- package/example-bundle/assets/style-BjquDnTE.css +0 -1
package/src/pack/run-pack.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { resolve, join, basename } from "node:path";
|
|
1
|
+
import { resolve, join, basename, dirname } from "node:path";
|
|
2
2
|
import { stat, mkdir, writeFile, rm, readdir, copyFile, access } from "node:fs/promises";
|
|
3
3
|
import { detectRenderExport } from "../render/detect-render-export.js";
|
|
4
4
|
import { resolveProvidersPath } from "../render/resolve-providers.js";
|
|
5
|
+
import type { AuthoredSoundtrack, AuthoredTitleCards } from "../util/load-ts.js";
|
|
6
|
+
import {
|
|
7
|
+
SFX_DEST_PATHS,
|
|
8
|
+
resolveMusicAsset,
|
|
9
|
+
resolveSfxAssetPaths,
|
|
10
|
+
} from "../util/soundtrack-assets.js";
|
|
11
|
+
import { resolveLogoAsset } from "../util/scenario-assets.js";
|
|
5
12
|
import { generateEmbedEntry, generateEmbedHtml } from "./generate-embed-entry.js";
|
|
6
13
|
import { collectPackShots, type CollectedPackShots } from "./collect-pack-shots.js";
|
|
7
14
|
import { runViteBuild } from "./build.js";
|
|
@@ -13,7 +20,7 @@ import {
|
|
|
13
20
|
verifyManifestFilesExist,
|
|
14
21
|
type PackManifest,
|
|
15
22
|
} from "./pack-manifest.js";
|
|
16
|
-
import { DEFAULT_VIEWPORT } from "./viewport.js";
|
|
23
|
+
import { DEFAULT_VIEWPORT, resolvePackViewport } from "./viewport.js";
|
|
17
24
|
|
|
18
25
|
/**
|
|
19
26
|
* Generator version stamped into scenario.json (kept in sync with the CLI).
|
|
@@ -27,9 +34,14 @@ export interface RunPackOptions {
|
|
|
27
34
|
readonly scenarioDir: string;
|
|
28
35
|
/** Output directory for the bundle (default: ./<scenario-id>-bundle). */
|
|
29
36
|
readonly outDir?: string;
|
|
30
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Canonical viewport width in px. Omitted -> the scenario's own authored
|
|
39
|
+
* viewport (`createScenario({ viewport })` or an exported `viewport`
|
|
40
|
+
* constant) applies; a scenario that authors none falls back to
|
|
41
|
+
* {@link DEFAULT_VIEWPORT}. Explicit flags always win over both.
|
|
42
|
+
*/
|
|
31
43
|
readonly width?: number;
|
|
32
|
-
/** Shell/container height in px
|
|
44
|
+
/** Shell/container height in px. Same resolution chain as `width`. */
|
|
33
45
|
readonly shellHeight?: number;
|
|
34
46
|
/**
|
|
35
47
|
* Float the scenario on a backdrop with a window shadow (screen-recording
|
|
@@ -50,6 +62,7 @@ export interface PackResult {
|
|
|
50
62
|
readonly renderFilePath: string;
|
|
51
63
|
readonly providersPath: string | null;
|
|
52
64
|
readonly hasNarration: boolean;
|
|
65
|
+
readonly hasPresenter: boolean;
|
|
53
66
|
/**
|
|
54
67
|
* The declared shot names recorded in scenario.json (empty = the scenario
|
|
55
68
|
* authoritatively declares none), or undefined when the steps module could
|
|
@@ -83,8 +96,6 @@ export async function runPack(options: RunPackOptions): Promise<PackResult> {
|
|
|
83
96
|
|
|
84
97
|
const scenarioId = basename(scenarioDir);
|
|
85
98
|
const outDir = options.outDir ? resolve(options.outDir) : resolve(`./${scenarioId}-bundle`);
|
|
86
|
-
const width = options.width ?? DEFAULT_VIEWPORT.width;
|
|
87
|
-
const shellHeight = options.shellHeight ?? DEFAULT_VIEWPORT.height;
|
|
88
99
|
|
|
89
100
|
// The entry must live inside the scenario directory so the bundler's
|
|
90
101
|
// node_modules resolution walks up into the consumer project.
|
|
@@ -94,21 +105,41 @@ export async function runPack(options: RunPackOptions): Promise<PackResult> {
|
|
|
94
105
|
const renderFilePath = await detectRenderExport(scenarioDir);
|
|
95
106
|
const providersPath = await resolveProvidersPath(scenarioDir);
|
|
96
107
|
const hasNarration = await fileExists(join(scenarioDir, "narration", "manifest.json"));
|
|
108
|
+
const hasPresenter = await fileExists(join(scenarioDir, "presenter", "manifest.json"));
|
|
97
109
|
|
|
98
110
|
onLog(`Scenario: ${scenarioId}`);
|
|
99
111
|
onLog(`Render: ${renderFilePath}`);
|
|
100
112
|
onLog(`Providers: ${providersPath ?? "none"}`);
|
|
101
113
|
onLog(`Narration: ${hasNarration ? "yes (manifest found)" : "none"}`);
|
|
114
|
+
onLog(`Presenter: ${hasPresenter ? "yes (manifest found)" : "none"}`);
|
|
102
115
|
onLog(`Output: ${outDir}`);
|
|
103
116
|
|
|
104
|
-
// 1. Discover the declared shots
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
117
|
+
// 1. Discover the declared shots — and the authored viewport — by
|
|
118
|
+
// SSR-loading the steps module (same Vite resolution as the build
|
|
119
|
+
// below). Before the build on purpose: authoring errors — bad shot
|
|
120
|
+
// names, no steps array — fail fast here, without paying for a
|
|
121
|
+
// bundle. An import failure is tolerated: the shots are then unknown
|
|
122
|
+
// and scenario.json omits the key.
|
|
109
123
|
const collectedShots = await collectPackShots(scenarioDir);
|
|
110
124
|
onLog(`Shots: ${describeCollectedShots(collectedShots)}`);
|
|
111
125
|
|
|
126
|
+
// Canonical viewport: explicit options > the scenario's authored
|
|
127
|
+
// viewport > the packer default. Authored viewports used to be silently
|
|
128
|
+
// ignored (the authoring docs demonstrated `createScenario({ viewport })`
|
|
129
|
+
// while pack only ever read its own options); the source is logged so a
|
|
130
|
+
// surprising size is one log line from its explanation.
|
|
131
|
+
const resolvedViewport = resolvePackViewport(
|
|
132
|
+
options,
|
|
133
|
+
collectedShots.recorded ? collectedShots.authoredViewport : null,
|
|
134
|
+
);
|
|
135
|
+
const { width, height: shellHeight } = resolvedViewport;
|
|
136
|
+
const viewportSourceLabel = {
|
|
137
|
+
explicit: "explicit",
|
|
138
|
+
authored: "authored by the scenario",
|
|
139
|
+
default: "packer default",
|
|
140
|
+
}[resolvedViewport.source];
|
|
141
|
+
onLog(`Viewport: ${width}x${shellHeight} (${viewportSourceLabel})`);
|
|
142
|
+
|
|
112
143
|
// 2. Generate the browser entry + index.html into the temp dir.
|
|
113
144
|
const stage = options.stage ?? false;
|
|
114
145
|
const entrySource = generateEmbedEntry({
|
|
@@ -116,6 +147,7 @@ export async function runPack(options: RunPackOptions): Promise<PackResult> {
|
|
|
116
147
|
renderFilePath,
|
|
117
148
|
scenarioId,
|
|
118
149
|
hasNarration,
|
|
150
|
+
hasPresenter,
|
|
119
151
|
providersPath,
|
|
120
152
|
canonicalWidth: width,
|
|
121
153
|
shellHeight,
|
|
@@ -135,7 +167,48 @@ export async function runPack(options: RunPackOptions): Promise<PackResult> {
|
|
|
135
167
|
// ./narration/manifest.json at runtime and resolves each clip's
|
|
136
168
|
// relative src against it, so both must ship in the bundle.
|
|
137
169
|
if (hasNarration) {
|
|
138
|
-
await
|
|
170
|
+
await copyClipTrack(scenarioDir, outDir, "narration", ".mp3");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 4a. Copy the presenter manifest + clips — the same runtime contract
|
|
174
|
+
// as narration: the embed fetches ./presenter/manifest.json and
|
|
175
|
+
// resolves each clip's relative src against it. CSP is
|
|
176
|
+
// `media-src 'self'`, so the clips must ship in the bundle.
|
|
177
|
+
if (hasPresenter) {
|
|
178
|
+
await copyClipTrack(scenarioDir, outDir, "presenter", ".mp4");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 4b. Copy the soundtrack's assets: the authored music file (at its
|
|
182
|
+
// scenario-relative path, which is what the embed references) and
|
|
183
|
+
// the built-in SFX set from @scenar/react. Discovery rides the same
|
|
184
|
+
// SSR load as the shots; when the module was not loadable, the
|
|
185
|
+
// soundtrack (if any) cannot ship — warn rather than silently
|
|
186
|
+
// packing an embed whose audio 404s.
|
|
187
|
+
if (collectedShots.recorded) {
|
|
188
|
+
if (collectedShots.authoredSoundtrack) {
|
|
189
|
+
const copied = await copySoundtrack(
|
|
190
|
+
scenarioDir,
|
|
191
|
+
outDir,
|
|
192
|
+
collectedShots.authoredSoundtrack,
|
|
193
|
+
);
|
|
194
|
+
if (copied > 0) onLog(`Soundtrack: ${copied} audio file(s) copied into the bundle`);
|
|
195
|
+
}
|
|
196
|
+
// 4c. Copy title-card logos at their scenario-relative paths (the
|
|
197
|
+
// card component references them by that same path). Discovery
|
|
198
|
+
// rides the same SSR load as the shots and the soundtrack.
|
|
199
|
+
if (collectedShots.authoredTitleCards) {
|
|
200
|
+
const copied = await copyTitleCardLogos(
|
|
201
|
+
scenarioDir,
|
|
202
|
+
outDir,
|
|
203
|
+
collectedShots.authoredTitleCards,
|
|
204
|
+
);
|
|
205
|
+
if (copied > 0) onLog(`Cards: ${copied} logo file(s) copied into the bundle`);
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
onLog(
|
|
209
|
+
"Warning: steps module not loadable under Node — if this scenario authors a " +
|
|
210
|
+
"soundtrack or title cards, their asset files were NOT copied into the bundle.",
|
|
211
|
+
);
|
|
139
212
|
}
|
|
140
213
|
|
|
141
214
|
// 5. Write the required scenario.json descriptor, recording the canonical
|
|
@@ -170,6 +243,7 @@ export async function runPack(options: RunPackOptions): Promise<PackResult> {
|
|
|
170
243
|
renderFilePath,
|
|
171
244
|
providersPath,
|
|
172
245
|
hasNarration,
|
|
246
|
+
hasPresenter,
|
|
173
247
|
shots: collectedShots.recorded ? collectedShots.shots : undefined,
|
|
174
248
|
manifest,
|
|
175
249
|
totalBytes,
|
|
@@ -182,24 +256,95 @@ export async function runPack(options: RunPackOptions): Promise<PackResult> {
|
|
|
182
256
|
}
|
|
183
257
|
|
|
184
258
|
/**
|
|
185
|
-
* Copy
|
|
186
|
-
*
|
|
187
|
-
*
|
|
259
|
+
* Copy one clip track's manifest and media (narration mp3s, presenter
|
|
260
|
+
* mp4s) from <scenarioDir>/<trackDir> into <outDir>/<trackDir>. The
|
|
261
|
+
* manifest is the runtime index the embed fetches; the clips are what it
|
|
262
|
+
* references by relative src. Cache files never ship.
|
|
188
263
|
*/
|
|
189
|
-
async function
|
|
190
|
-
|
|
191
|
-
|
|
264
|
+
async function copyClipTrack(
|
|
265
|
+
scenarioDir: string,
|
|
266
|
+
outDir: string,
|
|
267
|
+
trackDir: string,
|
|
268
|
+
extension: string,
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const srcDir = join(scenarioDir, trackDir);
|
|
271
|
+
const destDir = join(outDir, trackDir);
|
|
192
272
|
await mkdir(destDir, { recursive: true });
|
|
193
273
|
const entries = await readdir(srcDir, { withFileTypes: true });
|
|
194
274
|
for (const entry of entries) {
|
|
195
275
|
if (!entry.isFile()) continue;
|
|
196
276
|
const name = entry.name.toLowerCase();
|
|
197
|
-
if (name === "manifest.json" || name.endsWith(
|
|
277
|
+
if (name === "manifest.json" || name.endsWith(extension)) {
|
|
198
278
|
await copyFile(join(srcDir, entry.name), join(destDir, entry.name));
|
|
199
279
|
}
|
|
200
280
|
}
|
|
201
281
|
}
|
|
202
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Copy the soundtrack's audio into the bundle: the authored music file at
|
|
285
|
+
* its scenario-relative path (the embed references it by that same path),
|
|
286
|
+
* and the built-in SFX set from @scenar/react at the shared
|
|
287
|
+
* {@link SFX_DEST_PATHS} locations the generated entry points at. Returns
|
|
288
|
+
* the number of files copied. Remote music URLs copy nothing.
|
|
289
|
+
*/
|
|
290
|
+
async function copySoundtrack(
|
|
291
|
+
scenarioDir: string,
|
|
292
|
+
outDir: string,
|
|
293
|
+
soundtrack: AuthoredSoundtrack,
|
|
294
|
+
): Promise<number> {
|
|
295
|
+
let copied = 0;
|
|
296
|
+
|
|
297
|
+
if (soundtrack.musicSrc) {
|
|
298
|
+
const music = await resolveMusicAsset(scenarioDir, soundtrack.musicSrc);
|
|
299
|
+
if (music) {
|
|
300
|
+
const dest = join(outDir, music.destRelPath);
|
|
301
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
302
|
+
await copyFile(music.sourcePath, dest);
|
|
303
|
+
copied += 1;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (soundtrack.sfx) {
|
|
308
|
+
const sfxPaths = resolveSfxAssetPaths(scenarioDir);
|
|
309
|
+
for (const sound of ["click", "keystroke"] as const) {
|
|
310
|
+
const dest = join(outDir, SFX_DEST_PATHS[sound]);
|
|
311
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
312
|
+
await copyFile(sfxPaths[sound], dest);
|
|
313
|
+
copied += 1;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return copied;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Copy title-card logos into the bundle at their scenario-relative paths.
|
|
322
|
+
* A logo shared by intro and outro copies once; remote URLs copy nothing.
|
|
323
|
+
* Returns the number of files copied.
|
|
324
|
+
*/
|
|
325
|
+
async function copyTitleCardLogos(
|
|
326
|
+
scenarioDir: string,
|
|
327
|
+
outDir: string,
|
|
328
|
+
titleCards: AuthoredTitleCards,
|
|
329
|
+
): Promise<number> {
|
|
330
|
+
let copied = 0;
|
|
331
|
+
const copiedPaths = new Set<string>();
|
|
332
|
+
|
|
333
|
+
for (const side of ["intro", "outro"] as const) {
|
|
334
|
+
const logoSrc = titleCards[side]?.logoSrc;
|
|
335
|
+
if (!logoSrc) continue;
|
|
336
|
+
const logo = await resolveLogoAsset(scenarioDir, logoSrc, `titleCards.${side}.logoSrc`);
|
|
337
|
+
if (!logo || copiedPaths.has(logo.destRelPath)) continue;
|
|
338
|
+
const dest = join(outDir, logo.destRelPath);
|
|
339
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
340
|
+
await copyFile(logo.sourcePath, dest);
|
|
341
|
+
copiedPaths.add(logo.destRelPath);
|
|
342
|
+
copied += 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return copied;
|
|
346
|
+
}
|
|
347
|
+
|
|
203
348
|
/** One log-friendly line for the shot discovery outcome. */
|
|
204
349
|
function describeCollectedShots(collected: CollectedPackShots): string {
|
|
205
350
|
if (!collected.recorded) {
|
package/src/pack/viewport.d.ts
CHANGED
|
@@ -33,4 +33,21 @@ export declare const DEFAULT_VIEWPORT: Viewport;
|
|
|
33
33
|
* accept a recorded viewport while falling back cleanly for older bundles.
|
|
34
34
|
*/
|
|
35
35
|
export declare function parseViewport(value: unknown): Viewport | null;
|
|
36
|
+
/** A resolved pack viewport plus where each number came from (for the log). */
|
|
37
|
+
export interface ResolvedPackViewport extends Viewport {
|
|
38
|
+
readonly source: "explicit" | "authored" | "default";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the canonical viewport a bundle is packed at: explicit options
|
|
42
|
+
* (CLI flags / MCP params) > the scenario's own authored viewport > the
|
|
43
|
+
* packer default. Width and height resolve as a pair — an explicit *partial*
|
|
44
|
+
* override (only `--width`) still counts as explicit, filling the other axis
|
|
45
|
+
* from the authored viewport when present, else the default — and the
|
|
46
|
+
* source names the strongest contributor so the pack log can explain a
|
|
47
|
+
* surprising size in one line.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolvePackViewport(options: {
|
|
50
|
+
readonly width?: number;
|
|
51
|
+
readonly shellHeight?: number;
|
|
52
|
+
}, authored: Viewport | null): ResolvedPackViewport;
|
|
36
53
|
//# sourceMappingURL=viewport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewport.d.ts","sourceRoot":"","sources":["../../../src/pack/viewport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAsC,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAK7D"}
|
|
1
|
+
{"version":3,"file":"viewport.d.ts","sourceRoot":"","sources":["../../../src/pack/viewport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,QAAQ;IACvB,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAsC,CAAC;AAEtE;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAK7D;AAED,+EAA+E;AAC/E,MAAM,WAAW,oBAAqB,SAAQ,QAAQ;IACpD,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,GAAG,SAAS,CAAC;CACtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,EACnE,QAAQ,EAAE,QAAQ,GAAG,IAAI,GACxB,oBAAoB,CAUtB"}
|
package/src/pack/viewport.js
CHANGED
|
@@ -19,6 +19,25 @@ export function parseViewport(value) {
|
|
|
19
19
|
return null;
|
|
20
20
|
return { width, height };
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the canonical viewport a bundle is packed at: explicit options
|
|
24
|
+
* (CLI flags / MCP params) > the scenario's own authored viewport > the
|
|
25
|
+
* packer default. Width and height resolve as a pair — an explicit *partial*
|
|
26
|
+
* override (only `--width`) still counts as explicit, filling the other axis
|
|
27
|
+
* from the authored viewport when present, else the default — and the
|
|
28
|
+
* source names the strongest contributor so the pack log can explain a
|
|
29
|
+
* surprising size in one line.
|
|
30
|
+
*/
|
|
31
|
+
export function resolvePackViewport(options, authored) {
|
|
32
|
+
const width = options.width ?? authored?.width ?? DEFAULT_VIEWPORT.width;
|
|
33
|
+
const height = options.shellHeight ?? authored?.height ?? DEFAULT_VIEWPORT.height;
|
|
34
|
+
const source = options.width !== undefined || options.shellHeight !== undefined
|
|
35
|
+
? "explicit"
|
|
36
|
+
: authored
|
|
37
|
+
? "authored"
|
|
38
|
+
: "default";
|
|
39
|
+
return { width, height, source };
|
|
40
|
+
}
|
|
22
41
|
function isPositiveInt(value) {
|
|
23
42
|
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
24
43
|
}
|
package/src/pack/viewport.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viewport.js","sourceRoot":"","sources":["../../../src/pack/viewport.ts"],"names":[],"mappings":"AAsBA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAa,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAEtE;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,KAAgC,CAAC;IAC3D,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACjE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;AAC3E,CAAC"}
|
|
1
|
+
{"version":3,"file":"viewport.js","sourceRoot":"","sources":["../../../src/pack/viewport.ts"],"names":[],"mappings":"AAsBA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAa,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAEtE;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,KAAgC,CAAC;IAC3D,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;QAAE,OAAO,IAAI,CAAC;IACjE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC3B,CAAC;AAOD;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CACjC,OAAmE,EACnE,QAAyB;IAEzB,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,QAAQ,EAAE,KAAK,IAAI,gBAAgB,CAAC,KAAK,CAAC;IACzE,MAAM,MAAM,GAAG,OAAO,CAAC,WAAW,IAAI,QAAQ,EAAE,MAAM,IAAI,gBAAgB,CAAC,MAAM,CAAC;IAClF,MAAM,MAAM,GACV,OAAO,CAAC,KAAK,KAAK,SAAS,IAAI,OAAO,CAAC,WAAW,KAAK,SAAS;QAC9D,CAAC,CAAC,UAAU;QACZ,CAAC,CAAC,QAAQ;YACR,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,SAAS,CAAC;IAClB,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AACnC,CAAC;AAED,SAAS,aAAa,CAAC,KAAc;IACnC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC;AAC3E,CAAC"}
|
package/src/pack/viewport.ts
CHANGED
|
@@ -41,6 +41,35 @@ export function parseViewport(value: unknown): Viewport | null {
|
|
|
41
41
|
return { width, height };
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/** A resolved pack viewport plus where each number came from (for the log). */
|
|
45
|
+
export interface ResolvedPackViewport extends Viewport {
|
|
46
|
+
readonly source: "explicit" | "authored" | "default";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the canonical viewport a bundle is packed at: explicit options
|
|
51
|
+
* (CLI flags / MCP params) > the scenario's own authored viewport > the
|
|
52
|
+
* packer default. Width and height resolve as a pair — an explicit *partial*
|
|
53
|
+
* override (only `--width`) still counts as explicit, filling the other axis
|
|
54
|
+
* from the authored viewport when present, else the default — and the
|
|
55
|
+
* source names the strongest contributor so the pack log can explain a
|
|
56
|
+
* surprising size in one line.
|
|
57
|
+
*/
|
|
58
|
+
export function resolvePackViewport(
|
|
59
|
+
options: { readonly width?: number; readonly shellHeight?: number },
|
|
60
|
+
authored: Viewport | null,
|
|
61
|
+
): ResolvedPackViewport {
|
|
62
|
+
const width = options.width ?? authored?.width ?? DEFAULT_VIEWPORT.width;
|
|
63
|
+
const height = options.shellHeight ?? authored?.height ?? DEFAULT_VIEWPORT.height;
|
|
64
|
+
const source =
|
|
65
|
+
options.width !== undefined || options.shellHeight !== undefined
|
|
66
|
+
? "explicit"
|
|
67
|
+
: authored
|
|
68
|
+
? "authored"
|
|
69
|
+
: "default";
|
|
70
|
+
return { width, height, source };
|
|
71
|
+
}
|
|
72
|
+
|
|
44
73
|
function isPositiveInt(value: unknown): value is number {
|
|
45
74
|
return typeof value === "number" && Number.isInteger(value) && value > 0;
|
|
46
75
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal HeyGen API client for `scenar presenter` — deliberately an
|
|
3
|
+
* internal module of the CLI, not published API. Shaped so a second
|
|
4
|
+
* avatar provider could slot in behind the same function signatures,
|
|
5
|
+
* but the interface is not exported until a second provider is a
|
|
6
|
+
* concrete need (the TTS-provider precedent).
|
|
7
|
+
*
|
|
8
|
+
* Endpoints and response fields follow the live Gate 0 transcript:
|
|
9
|
+
* `POST /v3/assets` (multipart) → `data.asset_id`;
|
|
10
|
+
* `POST /v3/videos` (`type: "avatar"`, `audio_asset_id`) →
|
|
11
|
+
* `data.video_id`; `GET /v3/videos/{id}` → `data.status`
|
|
12
|
+
* (`completed`/`failed`), `data.video_url`, `data.failure_message`.
|
|
13
|
+
*
|
|
14
|
+
* BYO key, no Scenar-side proxying — identical to the TTS providers.
|
|
15
|
+
* Native fetch, no new runtime dependencies.
|
|
16
|
+
*/
|
|
17
|
+
/** Engine flag values → HeyGen's API vocabulary. */
|
|
18
|
+
export declare const HEYGEN_ENGINE_TYPES: {
|
|
19
|
+
readonly iii: "avatar_iii";
|
|
20
|
+
readonly iv: "avatar_iv";
|
|
21
|
+
};
|
|
22
|
+
export type HeygenEngineFlag = keyof typeof HEYGEN_ENGINE_TYPES;
|
|
23
|
+
/**
|
|
24
|
+
* Indicative $/minute of narration audio per engine, from the project
|
|
25
|
+
* cost analysis. The estimate the cost gate prints is indicative —
|
|
26
|
+
* HeyGen bills credits, not dollars.
|
|
27
|
+
*/
|
|
28
|
+
export declare const HEYGEN_RATE_PER_MIN: Record<HeygenEngineFlag, number>;
|
|
29
|
+
export interface HeygenRequestContext {
|
|
30
|
+
readonly apiKey: string;
|
|
31
|
+
/** Injectable for tests; defaults to a real timer. */
|
|
32
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
33
|
+
/** Rate-limit feedback sink (stderr line per retry). */
|
|
34
|
+
readonly onRetry?: (message: string) => void;
|
|
35
|
+
}
|
|
36
|
+
/** Upload one narration MP3 as a HeyGen asset (free). Returns the asset id. */
|
|
37
|
+
export declare function uploadAudioAsset(ctx: HeygenRequestContext, audio: Buffer, filename: string): Promise<string>;
|
|
38
|
+
export interface CreateAvatarVideoInput {
|
|
39
|
+
readonly avatarId: string;
|
|
40
|
+
readonly audioAssetId: string;
|
|
41
|
+
readonly engine: HeygenEngineFlag;
|
|
42
|
+
readonly resolution: string;
|
|
43
|
+
/**
|
|
44
|
+
* Deterministic title (`scenar <scenarioId> step-<N> <hash8>`) so any
|
|
45
|
+
* duplicate generation is identifiable in the HeyGen dashboard.
|
|
46
|
+
*/
|
|
47
|
+
readonly title: string;
|
|
48
|
+
}
|
|
49
|
+
/** Create one avatar video lip-synced to the uploaded audio. Returns the video id. */
|
|
50
|
+
export declare function createAvatarVideo(ctx: HeygenRequestContext, input: CreateAvatarVideoInput): Promise<string>;
|
|
51
|
+
export interface PollOutcome {
|
|
52
|
+
readonly status: "completed" | "failed" | "timeout";
|
|
53
|
+
readonly videoUrl?: string;
|
|
54
|
+
readonly failureMessage?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface PollOptions {
|
|
57
|
+
readonly intervalMs: number;
|
|
58
|
+
readonly timeoutMs: number;
|
|
59
|
+
/** Elapsed-time feedback per tick (drives the progress line). */
|
|
60
|
+
readonly onTick?: (elapsedMs: number) => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Poll a generation to completion. Polling is read-only, so transient
|
|
64
|
+
* network errors retry on the next tick inside the step timeout.
|
|
65
|
+
*/
|
|
66
|
+
export declare function pollVideo(ctx: HeygenRequestContext, videoId: string, options: PollOptions): Promise<PollOutcome>;
|
|
67
|
+
/** Download a completed clip. Returns the raw bytes. */
|
|
68
|
+
export declare function downloadVideo(url: string): Promise<Buffer>;
|
|
69
|
+
//# sourceMappingURL=heygen-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heygen-client.d.ts","sourceRoot":"","sources":["../../../src/presenter/heygen-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,oDAAoD;AACpD,eAAO,MAAM,mBAAmB;;;CAGtB,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,MAAM,OAAO,mBAAmB,CAAC;AAEhE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAGhE,CAAC;AAKF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sDAAsD;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,wDAAwD;IACxD,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9C;AA6CD,+EAA+E;AAC/E,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,oBAAoB,EACzB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAkBjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,sFAAsF;AACtF,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,oBAAoB,EACzB,KAAK,EAAE,sBAAsB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iEAAiE;IACjE,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAC7B,GAAG,EAAE,oBAAoB,EACzB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC,CAqCtB;AAED,wDAAwD;AACxD,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMhE"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal HeyGen API client for `scenar presenter` — deliberately an
|
|
3
|
+
* internal module of the CLI, not published API. Shaped so a second
|
|
4
|
+
* avatar provider could slot in behind the same function signatures,
|
|
5
|
+
* but the interface is not exported until a second provider is a
|
|
6
|
+
* concrete need (the TTS-provider precedent).
|
|
7
|
+
*
|
|
8
|
+
* Endpoints and response fields follow the live Gate 0 transcript:
|
|
9
|
+
* `POST /v3/assets` (multipart) → `data.asset_id`;
|
|
10
|
+
* `POST /v3/videos` (`type: "avatar"`, `audio_asset_id`) →
|
|
11
|
+
* `data.video_id`; `GET /v3/videos/{id}` → `data.status`
|
|
12
|
+
* (`completed`/`failed`), `data.video_url`, `data.failure_message`.
|
|
13
|
+
*
|
|
14
|
+
* BYO key, no Scenar-side proxying — identical to the TTS providers.
|
|
15
|
+
* Native fetch, no new runtime dependencies.
|
|
16
|
+
*/
|
|
17
|
+
const HEYGEN_BASE_URL = "https://api.heygen.com";
|
|
18
|
+
/** Engine flag values → HeyGen's API vocabulary. */
|
|
19
|
+
export const HEYGEN_ENGINE_TYPES = {
|
|
20
|
+
iii: "avatar_iii",
|
|
21
|
+
iv: "avatar_iv",
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Indicative $/minute of narration audio per engine, from the project
|
|
25
|
+
* cost analysis. The estimate the cost gate prints is indicative —
|
|
26
|
+
* HeyGen bills credits, not dollars.
|
|
27
|
+
*/
|
|
28
|
+
export const HEYGEN_RATE_PER_MIN = {
|
|
29
|
+
iii: 1.0,
|
|
30
|
+
iv: 4.0,
|
|
31
|
+
};
|
|
32
|
+
const MAX_ATTEMPTS_ON_429 = 3;
|
|
33
|
+
const DEFAULT_RETRY_AFTER_S = 30;
|
|
34
|
+
const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
35
|
+
/**
|
|
36
|
+
* Run a HeyGen request, honoring 429 rate limits: wait `Retry-After`
|
|
37
|
+
* (default 30 s) and retry the same call, up to 3 attempts total.
|
|
38
|
+
*/
|
|
39
|
+
async function heygenFetch(ctx, path, init) {
|
|
40
|
+
const sleep = ctx.sleep ?? realSleep;
|
|
41
|
+
let response;
|
|
42
|
+
for (let attempt = 1;; attempt++) {
|
|
43
|
+
response = await fetch(`${HEYGEN_BASE_URL}${path}`, {
|
|
44
|
+
...init,
|
|
45
|
+
headers: { "x-api-key": ctx.apiKey, ...(init.headers ?? {}) },
|
|
46
|
+
});
|
|
47
|
+
if (response.status !== 429 || attempt >= MAX_ATTEMPTS_ON_429)
|
|
48
|
+
return response;
|
|
49
|
+
const retryAfterS = Number(response.headers.get("retry-after")) || DEFAULT_RETRY_AFTER_S;
|
|
50
|
+
ctx.onRetry?.(`rate-limited, retrying in ${retryAfterS}s (${attempt}/${MAX_ATTEMPTS_ON_429})`);
|
|
51
|
+
await sleep(retryAfterS * 1000);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function readBody(response) {
|
|
55
|
+
try {
|
|
56
|
+
return (await response.json());
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function apiError(operation, response, body) {
|
|
63
|
+
return new Error(`HeyGen ${operation} failed (${response.status}): ${JSON.stringify(body)}`);
|
|
64
|
+
}
|
|
65
|
+
/** Upload one narration MP3 as a HeyGen asset (free). Returns the asset id. */
|
|
66
|
+
export async function uploadAudioAsset(ctx, audio, filename) {
|
|
67
|
+
const form = new FormData();
|
|
68
|
+
form.append("file", new Blob([new Uint8Array(audio)], { type: "audio/mpeg" }), filename);
|
|
69
|
+
const response = await heygenFetch(ctx, "/v3/assets", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: form,
|
|
72
|
+
});
|
|
73
|
+
const body = await readBody(response);
|
|
74
|
+
if (!response.ok)
|
|
75
|
+
throw apiError("asset upload", response, body);
|
|
76
|
+
const assetId = body["data"]?.["asset_id"];
|
|
77
|
+
if (typeof assetId !== "string" || assetId.length === 0) {
|
|
78
|
+
throw new Error(`HeyGen asset upload returned no asset_id: ${JSON.stringify(body)}`);
|
|
79
|
+
}
|
|
80
|
+
return assetId;
|
|
81
|
+
}
|
|
82
|
+
/** Create one avatar video lip-synced to the uploaded audio. Returns the video id. */
|
|
83
|
+
export async function createAvatarVideo(ctx, input) {
|
|
84
|
+
const response = await heygenFetch(ctx, "/v3/videos", {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
type: "avatar",
|
|
89
|
+
avatar_id: input.avatarId,
|
|
90
|
+
audio_asset_id: input.audioAssetId,
|
|
91
|
+
engine: { type: HEYGEN_ENGINE_TYPES[input.engine] },
|
|
92
|
+
output_format: "mp4",
|
|
93
|
+
aspect_ratio: "auto",
|
|
94
|
+
resolution: input.resolution,
|
|
95
|
+
title: input.title,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
const body = await readBody(response);
|
|
99
|
+
if (!response.ok)
|
|
100
|
+
throw apiError("video create", response, body);
|
|
101
|
+
const videoId = body["data"]?.["video_id"];
|
|
102
|
+
if (typeof videoId !== "string" || videoId.length === 0) {
|
|
103
|
+
throw new Error(`HeyGen video create returned no video_id: ${JSON.stringify(body)}`);
|
|
104
|
+
}
|
|
105
|
+
return videoId;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Poll a generation to completion. Polling is read-only, so transient
|
|
109
|
+
* network errors retry on the next tick inside the step timeout.
|
|
110
|
+
*/
|
|
111
|
+
export async function pollVideo(ctx, videoId, options) {
|
|
112
|
+
const sleep = ctx.sleep ?? realSleep;
|
|
113
|
+
const startedAt = Date.now();
|
|
114
|
+
while (Date.now() - startedAt < options.timeoutMs) {
|
|
115
|
+
await sleep(options.intervalMs);
|
|
116
|
+
options.onTick?.(Date.now() - startedAt);
|
|
117
|
+
let body;
|
|
118
|
+
try {
|
|
119
|
+
const response = await heygenFetch(ctx, `/v3/videos/${videoId}`, { method: "GET" });
|
|
120
|
+
body = await readBody(response);
|
|
121
|
+
if (!response.ok)
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const data = (body["data"] ?? {});
|
|
128
|
+
const status = data["status"];
|
|
129
|
+
if (status === "completed") {
|
|
130
|
+
return {
|
|
131
|
+
status: "completed",
|
|
132
|
+
videoUrl: typeof data["video_url"] === "string" ? data["video_url"] : undefined,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (status === "failed") {
|
|
136
|
+
return {
|
|
137
|
+
status: "failed",
|
|
138
|
+
failureMessage: typeof data["failure_message"] === "string"
|
|
139
|
+
? data["failure_message"]
|
|
140
|
+
: JSON.stringify(data["failure_message"] ?? "unknown failure"),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { status: "timeout" };
|
|
145
|
+
}
|
|
146
|
+
/** Download a completed clip. Returns the raw bytes. */
|
|
147
|
+
export async function downloadVideo(url) {
|
|
148
|
+
const response = await fetch(url);
|
|
149
|
+
if (!response.ok) {
|
|
150
|
+
throw new Error(`HeyGen clip download failed (${response.status}) for ${url}`);
|
|
151
|
+
}
|
|
152
|
+
return Buffer.from(await response.arrayBuffer());
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=heygen-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heygen-client.js","sourceRoot":"","sources":["../../../src/presenter/heygen-client.ts"],"names":[],"mappings":"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"}
|