@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/util/load-ts.ts
CHANGED
|
@@ -35,6 +35,207 @@ export function findStepsArray(exports: Record<string, unknown>): ImportedStep[]
|
|
|
35
35
|
return null;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** A scenario-authored canonical viewport, in CSS pixels. */
|
|
39
|
+
export interface AuthoredViewport {
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isViewportShape(value: unknown): value is AuthoredViewport {
|
|
45
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
46
|
+
const record = value as Record<string, unknown>;
|
|
47
|
+
return (
|
|
48
|
+
typeof record["width"] === "number" &&
|
|
49
|
+
Number.isFinite(record["width"]) &&
|
|
50
|
+
record["width"] > 0 &&
|
|
51
|
+
typeof record["height"] === "number" &&
|
|
52
|
+
Number.isFinite(record["height"]) &&
|
|
53
|
+
record["height"] > 0
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isStepsShape(value: unknown): boolean {
|
|
58
|
+
return (
|
|
59
|
+
Array.isArray(value) &&
|
|
60
|
+
value.length > 0 &&
|
|
61
|
+
typeof value[0] === "object" &&
|
|
62
|
+
value[0] !== null &&
|
|
63
|
+
"delayMs" in value[0]
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Find the scenario's authored canonical viewport in a loaded module's
|
|
69
|
+
* exports, or null when the module authors none (the common case — the
|
|
70
|
+
* packer then falls back to CLI flags or its default).
|
|
71
|
+
*
|
|
72
|
+
* Two authored forms are honored, mirroring the two authoring surfaces:
|
|
73
|
+
*
|
|
74
|
+
* 1. A scenario-shaped export — an object carrying both a `viewport` and a
|
|
75
|
+
* delayMs-bearing `steps` array, i.e. what `createScenario()` returns.
|
|
76
|
+
* The viewport rides the scenario, so only a real scenario export
|
|
77
|
+
* qualifies; a stray `viewport` field on an unrelated object does not.
|
|
78
|
+
* 2. An export *named* `viewport` with `{ width, height }` — the
|
|
79
|
+
* directory-form counterpart, for authors of a raw steps array.
|
|
80
|
+
*
|
|
81
|
+
* Precedence between the two follows specificity: a scenario-shaped export
|
|
82
|
+
* wins, because its viewport is inseparable from the steps it sizes.
|
|
83
|
+
*/
|
|
84
|
+
export function findAuthoredViewport(exports: Record<string, unknown>): AuthoredViewport | null {
|
|
85
|
+
for (const value of Object.values(exports)) {
|
|
86
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
87
|
+
const record = value as Record<string, unknown>;
|
|
88
|
+
if (isViewportShape(record["viewport"]) && isStepsShape(record["steps"])) {
|
|
89
|
+
const viewport = record["viewport"] as AuthoredViewport;
|
|
90
|
+
return { width: viewport.width, height: viewport.height };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const named = exports["viewport"];
|
|
94
|
+
if (isViewportShape(named)) {
|
|
95
|
+
return { width: named.width, height: named.height };
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* A scenario-authored soundtrack, mirroring the proto `SoundtrackConfig`
|
|
102
|
+
* in camelCase (the same shape as `@scenar/core`'s `Soundtrack`).
|
|
103
|
+
*/
|
|
104
|
+
export interface AuthoredSoundtrack {
|
|
105
|
+
musicSrc?: string;
|
|
106
|
+
musicVolume?: number;
|
|
107
|
+
duckingVolume?: number;
|
|
108
|
+
sfx?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The soundtrack's known field names — used to pick a clean copy. */
|
|
112
|
+
const SOUNDTRACK_FIELDS = ["musicSrc", "musicVolume", "duckingVolume", "sfx"] as const;
|
|
113
|
+
|
|
114
|
+
function pickSoundtrack(value: unknown): AuthoredSoundtrack | null {
|
|
115
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
116
|
+
const record = value as Record<string, unknown>;
|
|
117
|
+
const soundtrack: Record<string, unknown> = {};
|
|
118
|
+
for (const field of SOUNDTRACK_FIELDS) {
|
|
119
|
+
if (record[field] !== undefined) soundtrack[field] = record[field];
|
|
120
|
+
}
|
|
121
|
+
return soundtrack as AuthoredSoundtrack;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Find the scenario's authored soundtrack in a loaded module's exports,
|
|
126
|
+
* or null when the module authors none.
|
|
127
|
+
*
|
|
128
|
+
* Mirrors {@link findAuthoredViewport}'s two authored forms and
|
|
129
|
+
* precedence exactly:
|
|
130
|
+
*
|
|
131
|
+
* 1. A scenario-shaped export — an object carrying both a `soundtrack`
|
|
132
|
+
* and a delayMs-bearing `steps` array (what `createScenario()`
|
|
133
|
+
* returns when the author passes a soundtrack).
|
|
134
|
+
* 2. An export *named* `soundtrack` — the directory-form counterpart,
|
|
135
|
+
* for authors of a raw steps array.
|
|
136
|
+
*
|
|
137
|
+
* Field validity (volume ranges, non-empty musicSrc) is the validator's
|
|
138
|
+
* concern, not discovery's — this only locates the authored object.
|
|
139
|
+
*/
|
|
140
|
+
export function findAuthoredSoundtrack(
|
|
141
|
+
exports: Record<string, unknown>,
|
|
142
|
+
): AuthoredSoundtrack | null {
|
|
143
|
+
for (const value of Object.values(exports)) {
|
|
144
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
145
|
+
const record = value as Record<string, unknown>;
|
|
146
|
+
if (record["soundtrack"] !== undefined && isStepsShape(record["steps"])) {
|
|
147
|
+
return pickSoundtrack(record["soundtrack"]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (exports["soundtrack"] !== undefined) {
|
|
151
|
+
return pickSoundtrack(exports["soundtrack"]);
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A scenario-authored title card, mirroring the proto `TitleCard`
|
|
158
|
+
* in camelCase (the same shape as `@scenar/core`'s `TitleCard`).
|
|
159
|
+
*/
|
|
160
|
+
export interface AuthoredTitleCard {
|
|
161
|
+
title?: string;
|
|
162
|
+
subtitle?: string;
|
|
163
|
+
logoSrc?: string;
|
|
164
|
+
ctaText?: string;
|
|
165
|
+
durationMs?: number;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* A scenario-authored title-cards config, mirroring the proto
|
|
170
|
+
* `TitleCardsConfig` in camelCase.
|
|
171
|
+
*/
|
|
172
|
+
export interface AuthoredTitleCards {
|
|
173
|
+
intro?: AuthoredTitleCard;
|
|
174
|
+
outro?: AuthoredTitleCard;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The card's known field names — used to pick a clean copy. */
|
|
178
|
+
const TITLE_CARD_FIELDS = [
|
|
179
|
+
"title",
|
|
180
|
+
"subtitle",
|
|
181
|
+
"logoSrc",
|
|
182
|
+
"ctaText",
|
|
183
|
+
"durationMs",
|
|
184
|
+
] as const;
|
|
185
|
+
|
|
186
|
+
function pickTitleCard(value: unknown): AuthoredTitleCard | null {
|
|
187
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
188
|
+
const record = value as Record<string, unknown>;
|
|
189
|
+
const card: Record<string, unknown> = {};
|
|
190
|
+
for (const field of TITLE_CARD_FIELDS) {
|
|
191
|
+
if (record[field] !== undefined) card[field] = record[field];
|
|
192
|
+
}
|
|
193
|
+
return card as AuthoredTitleCard;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function pickTitleCards(value: unknown): AuthoredTitleCards | null {
|
|
197
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
198
|
+
const record = value as Record<string, unknown>;
|
|
199
|
+
const cards: AuthoredTitleCards = {};
|
|
200
|
+
const intro = pickTitleCard(record["intro"]);
|
|
201
|
+
const outro = pickTitleCard(record["outro"]);
|
|
202
|
+
if (intro) cards.intro = intro;
|
|
203
|
+
if (outro) cards.outro = outro;
|
|
204
|
+
return cards;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Find the scenario's authored title cards in a loaded module's exports,
|
|
209
|
+
* or null when the module authors none.
|
|
210
|
+
*
|
|
211
|
+
* Mirrors {@link findAuthoredSoundtrack}'s two authored forms and
|
|
212
|
+
* precedence exactly:
|
|
213
|
+
*
|
|
214
|
+
* 1. A scenario-shaped export — an object carrying both a `titleCards`
|
|
215
|
+
* and a delayMs-bearing `steps` array (what `createScenario()`
|
|
216
|
+
* returns when the author passes title cards).
|
|
217
|
+
* 2. An export *named* `titleCards` — the directory-form counterpart,
|
|
218
|
+
* for authors of a raw steps array.
|
|
219
|
+
*
|
|
220
|
+
* Field validity (non-empty title, positive duration) is the validator's
|
|
221
|
+
* concern, not discovery's — this only locates the authored object.
|
|
222
|
+
*/
|
|
223
|
+
export function findAuthoredTitleCards(
|
|
224
|
+
exports: Record<string, unknown>,
|
|
225
|
+
): AuthoredTitleCards | null {
|
|
226
|
+
for (const value of Object.values(exports)) {
|
|
227
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
228
|
+
const record = value as Record<string, unknown>;
|
|
229
|
+
if (record["titleCards"] !== undefined && isStepsShape(record["steps"])) {
|
|
230
|
+
return pickTitleCards(record["titleCards"]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (exports["titleCards"] !== undefined) {
|
|
234
|
+
return pickTitleCards(exports["titleCards"]);
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
38
239
|
/**
|
|
39
240
|
* Dynamically import a TypeScript steps file and extract the
|
|
40
241
|
* steps array by duck-typing ({@link findStepsArray}).
|
|
@@ -51,3 +252,29 @@ export async function loadStepsFromTs(filePath: string): Promise<ImportedStep[]>
|
|
|
51
252
|
}
|
|
52
253
|
return steps;
|
|
53
254
|
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Dynamically import a TypeScript steps file and extract the authored
|
|
258
|
+
* soundtrack ({@link findAuthoredSoundtrack}), or null when the module
|
|
259
|
+
* authors none. Node caches the module, so calling this alongside
|
|
260
|
+
* {@link loadStepsFromTs} costs one import.
|
|
261
|
+
*/
|
|
262
|
+
export async function loadSoundtrackFromTs(
|
|
263
|
+
filePath: string,
|
|
264
|
+
): Promise<AuthoredSoundtrack | null> {
|
|
265
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
266
|
+
return findAuthoredSoundtrack(mod.default ?? mod);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Dynamically import a TypeScript steps file and extract the authored
|
|
271
|
+
* title cards ({@link findAuthoredTitleCards}), or null when the module
|
|
272
|
+
* authors none. Node caches the module, so calling this alongside
|
|
273
|
+
* {@link loadStepsFromTs} costs one import.
|
|
274
|
+
*/
|
|
275
|
+
export async function loadTitleCardsFromTs(
|
|
276
|
+
filePath: string,
|
|
277
|
+
): Promise<AuthoredTitleCards | null> {
|
|
278
|
+
const mod = await import(pathToFileURL(filePath).href);
|
|
279
|
+
return findAuthoredTitleCards(mod.default ?? mod);
|
|
280
|
+
}
|
|
@@ -3,20 +3,28 @@ interface CacheEntry {
|
|
|
3
3
|
durationMs: number;
|
|
4
4
|
}
|
|
5
5
|
interface CacheFile {
|
|
6
|
+
/**
|
|
7
|
+
* The generating provider's cache identity (see `TtsProvider.fingerprint`).
|
|
8
|
+
* Audio from one provider/model/default-voice configuration must never be
|
|
9
|
+
* served as a cache hit for another. Legacy cache files predate this field;
|
|
10
|
+
* they never match and regenerate once.
|
|
11
|
+
*/
|
|
12
|
+
fingerprint: string;
|
|
6
13
|
voice: string;
|
|
7
14
|
steps: (CacheEntry | null)[];
|
|
8
15
|
}
|
|
9
|
-
export declare function computeHash(narration: string, voice: string): string;
|
|
16
|
+
export declare function computeHash(narration: string, voice: string, fingerprint: string): string;
|
|
10
17
|
export declare function loadCache(outputDir: string): Promise<CacheFile | null>;
|
|
11
18
|
export declare function saveCache(outputDir: string, cache: CacheFile): Promise<void>;
|
|
12
19
|
export declare function fileExists(filePath: string): Promise<boolean>;
|
|
13
20
|
/**
|
|
14
|
-
* Check whether a step's narration is already cached
|
|
15
|
-
*
|
|
21
|
+
* Check whether a step's narration is already cached: the cache file was
|
|
22
|
+
* written by the same provider configuration (fingerprint) and voice, the
|
|
23
|
+
* step's hash matches, and the MP3 file still exists on disk.
|
|
16
24
|
*/
|
|
17
|
-
export declare function isCached(cache: CacheFile | null, stepIndex: number, hash: string, voice: string): boolean;
|
|
25
|
+
export declare function isCached(cache: CacheFile | null, stepIndex: number, hash: string, voice: string, fingerprint: string): boolean;
|
|
18
26
|
export declare function getCachedDuration(cache: CacheFile, stepIndex: number): number;
|
|
19
|
-
export declare function buildCacheFile(voice: string, totalSteps: number, entries: Map<number, {
|
|
27
|
+
export declare function buildCacheFile(fingerprint: string, voice: string, totalSteps: number, entries: Map<number, {
|
|
20
28
|
hash: string;
|
|
21
29
|
durationMs: number;
|
|
22
30
|
}>): CacheFile;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"narration-cache.d.ts","sourceRoot":"","sources":["../../../src/util/narration-cache.ts"],"names":[],"mappings":"AAIA,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,SAAS;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,CAAC,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC;CAC9B;AAID,wBAAgB,WAAW,
|
|
1
|
+
{"version":3,"file":"narration-cache.d.ts","sourceRoot":"","sources":["../../../src/util/narration-cache.ts"],"names":[],"mappings":"AAIA,UAAU,UAAU;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,SAAS;IACjB;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,CAAC,UAAU,GAAG,IAAI,CAAC,EAAE,CAAC;CAC9B;AAID,wBAAgB,WAAW,CACzB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,CAIR;AAED,wBAAsB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAO5E;AAED,wBAAsB,SAAS,CAC7B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAOnE;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CACtB,KAAK,EAAE,SAAS,GAAG,IAAI,EACvB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,OAAO,CAMT;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,SAAS,EAChB,SAAS,EAAE,MAAM,GAChB,MAAM,CAER;AAED,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GACzD,SAAS,CASX"}
|
|
@@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { readFile, writeFile, access } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
const CACHE_FILENAME = ".narration-cache.json";
|
|
5
|
-
export function computeHash(narration, voice) {
|
|
5
|
+
export function computeHash(narration, voice, fingerprint) {
|
|
6
6
|
return createHash("sha256")
|
|
7
|
-
.update(`${voice}\0${narration}`)
|
|
7
|
+
.update(`${fingerprint}\0${voice}\0${narration}`)
|
|
8
8
|
.digest("hex");
|
|
9
9
|
}
|
|
10
10
|
export async function loadCache(outputDir) {
|
|
@@ -29,23 +29,25 @@ export async function fileExists(filePath) {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
* Check whether a step's narration is already cached
|
|
33
|
-
*
|
|
32
|
+
* Check whether a step's narration is already cached: the cache file was
|
|
33
|
+
* written by the same provider configuration (fingerprint) and voice, the
|
|
34
|
+
* step's hash matches, and the MP3 file still exists on disk.
|
|
34
35
|
*/
|
|
35
|
-
export function isCached(cache, stepIndex, hash, voice) {
|
|
36
|
-
if (!cache || cache.voice !== voice)
|
|
36
|
+
export function isCached(cache, stepIndex, hash, voice, fingerprint) {
|
|
37
|
+
if (!cache || cache.fingerprint !== fingerprint || cache.voice !== voice) {
|
|
37
38
|
return false;
|
|
39
|
+
}
|
|
38
40
|
const entry = cache.steps[stepIndex];
|
|
39
41
|
return entry !== null && entry !== undefined && entry.hash === hash;
|
|
40
42
|
}
|
|
41
43
|
export function getCachedDuration(cache, stepIndex) {
|
|
42
44
|
return cache.steps[stepIndex].durationMs;
|
|
43
45
|
}
|
|
44
|
-
export function buildCacheFile(voice, totalSteps, entries) {
|
|
46
|
+
export function buildCacheFile(fingerprint, voice, totalSteps, entries) {
|
|
45
47
|
const steps = Array.from({ length: totalSteps }, () => null);
|
|
46
48
|
for (const [idx, entry] of entries) {
|
|
47
49
|
steps[idx] = entry;
|
|
48
50
|
}
|
|
49
|
-
return { voice, steps };
|
|
51
|
+
return { fingerprint, voice, steps };
|
|
50
52
|
}
|
|
51
53
|
//# sourceMappingURL=narration-cache.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"narration-cache.js","sourceRoot":"","sources":["../../../src/util/narration-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"narration-cache.js","sourceRoot":"","sources":["../../../src/util/narration-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAmBjC,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAE/C,MAAM,UAAU,WAAW,CACzB,SAAiB,EACjB,KAAa,EACb,WAAmB;IAEnB,OAAO,UAAU,CAAC,QAAQ,CAAC;SACxB,MAAM,CAAC,GAAG,WAAW,KAAK,KAAK,KAAK,SAAS,EAAE,CAAC;SAChD,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,SAAiB;IAC/C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC;QACrE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAc,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,SAAiB,EACjB,KAAgB;IAEhB,MAAM,SAAS,CACb,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAC/B,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CACtC,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAgB;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAuB,EACvB,SAAiB,EACjB,IAAY,EACZ,KAAa,EACb,WAAmB;IAEnB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,WAAW,KAAK,WAAW,IAAI,KAAK,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;QACzE,OAAO,KAAK,CAAC;IACf,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACrC,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAgB,EAChB,SAAiB;IAEjB,OAAO,KAAK,CAAC,KAAK,CAAC,SAAS,CAAE,CAAC,UAAU,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,WAAmB,EACnB,KAAa,EACb,UAAkB,EAClB,OAA0D;IAE1D,MAAM,KAAK,GAA0B,KAAK,CAAC,IAAI,CAC7C,EAAE,MAAM,EAAE,UAAU,EAAE,EACtB,GAAG,EAAE,CAAC,IAAI,CACX,CAAC;IACF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QACnC,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AACvC,CAAC"}
|
|
@@ -8,15 +8,26 @@ interface CacheEntry {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
interface CacheFile {
|
|
11
|
+
/**
|
|
12
|
+
* The generating provider's cache identity (see `TtsProvider.fingerprint`).
|
|
13
|
+
* Audio from one provider/model/default-voice configuration must never be
|
|
14
|
+
* served as a cache hit for another. Legacy cache files predate this field;
|
|
15
|
+
* they never match and regenerate once.
|
|
16
|
+
*/
|
|
17
|
+
fingerprint: string;
|
|
11
18
|
voice: string;
|
|
12
19
|
steps: (CacheEntry | null)[];
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
const CACHE_FILENAME = ".narration-cache.json";
|
|
16
23
|
|
|
17
|
-
export function computeHash(
|
|
24
|
+
export function computeHash(
|
|
25
|
+
narration: string,
|
|
26
|
+
voice: string,
|
|
27
|
+
fingerprint: string,
|
|
28
|
+
): string {
|
|
18
29
|
return createHash("sha256")
|
|
19
|
-
.update(`${voice}\0${narration}`)
|
|
30
|
+
.update(`${fingerprint}\0${voice}\0${narration}`)
|
|
20
31
|
.digest("hex");
|
|
21
32
|
}
|
|
22
33
|
|
|
@@ -49,16 +60,20 @@ export async function fileExists(filePath: string): Promise<boolean> {
|
|
|
49
60
|
}
|
|
50
61
|
|
|
51
62
|
/**
|
|
52
|
-
* Check whether a step's narration is already cached
|
|
53
|
-
*
|
|
63
|
+
* Check whether a step's narration is already cached: the cache file was
|
|
64
|
+
* written by the same provider configuration (fingerprint) and voice, the
|
|
65
|
+
* step's hash matches, and the MP3 file still exists on disk.
|
|
54
66
|
*/
|
|
55
67
|
export function isCached(
|
|
56
68
|
cache: CacheFile | null,
|
|
57
69
|
stepIndex: number,
|
|
58
70
|
hash: string,
|
|
59
71
|
voice: string,
|
|
72
|
+
fingerprint: string,
|
|
60
73
|
): boolean {
|
|
61
|
-
if (!cache || cache.voice !== voice)
|
|
74
|
+
if (!cache || cache.fingerprint !== fingerprint || cache.voice !== voice) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
62
77
|
const entry = cache.steps[stepIndex];
|
|
63
78
|
return entry !== null && entry !== undefined && entry.hash === hash;
|
|
64
79
|
}
|
|
@@ -71,6 +86,7 @@ export function getCachedDuration(
|
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
export function buildCacheFile(
|
|
89
|
+
fingerprint: string,
|
|
74
90
|
voice: string,
|
|
75
91
|
totalSteps: number,
|
|
76
92
|
entries: Map<number, { hash: string; durationMs: number }>,
|
|
@@ -82,5 +98,5 @@ export function buildCacheFile(
|
|
|
82
98
|
for (const [idx, entry] of entries) {
|
|
83
99
|
steps[idx] = entry;
|
|
84
100
|
}
|
|
85
|
-
return { voice, steps };
|
|
101
|
+
return { fingerprint, voice, steps };
|
|
86
102
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presenter clip cache — the `.narration-cache.json` pattern applied to
|
|
3
|
+
* `scenar presenter`, where a cache hit saves real money (a HeyGen
|
|
4
|
+
* generation costs dollars, not milliseconds).
|
|
5
|
+
*
|
|
6
|
+
* The per-step hash keys on the narration MP3 **bytes**: the audio is
|
|
7
|
+
* the entire upstream state of a clip, so re-narrating a step (new
|
|
8
|
+
* voice, new text, new provider) invalidates its presenter clip
|
|
9
|
+
* automatically — no coordination needed between the two commands.
|
|
10
|
+
*/
|
|
11
|
+
interface PresenterCacheEntry {
|
|
12
|
+
hash: string;
|
|
13
|
+
durationMs: number;
|
|
14
|
+
}
|
|
15
|
+
export interface PresenterCacheFile {
|
|
16
|
+
/**
|
|
17
|
+
* The generation configuration this cache was written under:
|
|
18
|
+
* `heygen/<engine>/<resolution>/<container>/<avatarId>`. Any change
|
|
19
|
+
* regenerates everything — a clip from one avatar or quality tier
|
|
20
|
+
* must never be served as a hit for another. The container is
|
|
21
|
+
* recorded even though v1 is mp4-only, so a future webm option
|
|
22
|
+
* invalidates correctly.
|
|
23
|
+
*/
|
|
24
|
+
fingerprint: string;
|
|
25
|
+
steps: (PresenterCacheEntry | null)[];
|
|
26
|
+
}
|
|
27
|
+
/** Compose the cache fingerprint for one generation configuration. */
|
|
28
|
+
export declare function presenterFingerprint(engine: string, resolution: string, container: string, avatarId: string): string;
|
|
29
|
+
/** Hash a step's narration audio bytes — the clip's entire upstream state. */
|
|
30
|
+
export declare function computePresenterHash(audioBytes: Buffer): string;
|
|
31
|
+
export declare function loadPresenterCache(outputDir: string): Promise<PresenterCacheFile | null>;
|
|
32
|
+
export declare function savePresenterCache(outputDir: string, cache: PresenterCacheFile): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Whether a step's clip is already cached under this fingerprint. The
|
|
35
|
+
* caller still verifies the clip file exists on disk — a cache entry
|
|
36
|
+
* for a deleted file regenerates.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isPresenterCached(cache: PresenterCacheFile | null, stepIndex: number, hash: string, fingerprint: string): boolean;
|
|
39
|
+
export declare function buildPresenterCacheFile(fingerprint: string, totalSteps: number, entries: Map<number, {
|
|
40
|
+
hash: string;
|
|
41
|
+
durationMs: number;
|
|
42
|
+
}>): PresenterCacheFile;
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=presenter-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presenter-cache.d.ts","sourceRoot":"","sources":["../../../src/util/presenter-cache.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AAEH,UAAU,mBAAmB;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;;OAOG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,CAAC,mBAAmB,GAAG,IAAI,CAAC,EAAE,CAAC;CACvC;AAID,sEAAsE;AACtE,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,GACf,MAAM,CAER;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAE/D;AAED,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAOpC;AAED,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,kBAAkB,GAAG,IAAI,EAChC,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAIT;AAED,wBAAgB,uBAAuB,CACrC,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,GACzD,kBAAkB,CASpB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
const CACHE_FILENAME = ".presenter-cache.json";
|
|
5
|
+
/** Compose the cache fingerprint for one generation configuration. */
|
|
6
|
+
export function presenterFingerprint(engine, resolution, container, avatarId) {
|
|
7
|
+
return `heygen/${engine}/${resolution}/${container}/${avatarId}`;
|
|
8
|
+
}
|
|
9
|
+
/** Hash a step's narration audio bytes — the clip's entire upstream state. */
|
|
10
|
+
export function computePresenterHash(audioBytes) {
|
|
11
|
+
return createHash("sha256").update(audioBytes).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
export async function loadPresenterCache(outputDir) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await readFile(join(outputDir, CACHE_FILENAME), "utf-8");
|
|
16
|
+
return JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function savePresenterCache(outputDir, cache) {
|
|
23
|
+
await writeFile(join(outputDir, CACHE_FILENAME), JSON.stringify(cache, null, 2) + "\n");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether a step's clip is already cached under this fingerprint. The
|
|
27
|
+
* caller still verifies the clip file exists on disk — a cache entry
|
|
28
|
+
* for a deleted file regenerates.
|
|
29
|
+
*/
|
|
30
|
+
export function isPresenterCached(cache, stepIndex, hash, fingerprint) {
|
|
31
|
+
if (!cache || cache.fingerprint !== fingerprint)
|
|
32
|
+
return false;
|
|
33
|
+
const entry = cache.steps[stepIndex];
|
|
34
|
+
return entry !== null && entry !== undefined && entry.hash === hash;
|
|
35
|
+
}
|
|
36
|
+
export function buildPresenterCacheFile(fingerprint, totalSteps, entries) {
|
|
37
|
+
const steps = Array.from({ length: totalSteps }, () => null);
|
|
38
|
+
for (const [idx, entry] of entries) {
|
|
39
|
+
steps[idx] = entry;
|
|
40
|
+
}
|
|
41
|
+
return { fingerprint, steps };
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=presenter-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presenter-cache.js","sourceRoot":"","sources":["../../../src/util/presenter-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AA+BjC,MAAM,cAAc,GAAG,uBAAuB,CAAC;AAE/C,sEAAsE;AACtE,MAAM,UAAU,oBAAoB,CAClC,MAAc,EACd,UAAkB,EAClB,SAAiB,EACjB,QAAgB;IAEhB,OAAO,UAAU,MAAM,IAAI,UAAU,IAAI,SAAS,IAAI,QAAQ,EAAE,CAAC;AACnE,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,oBAAoB,CAAC,UAAkB;IACrD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC/D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAiB;IAEjB,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,CAAC;QACrE,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAuB,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAiB,EACjB,KAAyB;IAEzB,MAAM,SAAS,CACb,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,EAC/B,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CACtC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAgC,EAChC,SAAiB,EACjB,IAAY,EACZ,WAAmB;IAEnB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,WAAW,KAAK,WAAW;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACrC,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,uBAAuB,CACrC,WAAmB,EACnB,UAAkB,EAClB,OAA0D;IAE1D,MAAM,KAAK,GAAmC,KAAK,CAAC,IAAI,CACtD,EAAE,MAAM,EAAE,UAAU,EAAE,EACtB,GAAG,EAAE,CAAC,IAAI,CACX,CAAC;IACF,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,OAAO,EAAE,CAAC;QACnC,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACrB,CAAC;IACD,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Presenter clip cache — the `.narration-cache.json` pattern applied to
|
|
7
|
+
* `scenar presenter`, where a cache hit saves real money (a HeyGen
|
|
8
|
+
* generation costs dollars, not milliseconds).
|
|
9
|
+
*
|
|
10
|
+
* The per-step hash keys on the narration MP3 **bytes**: the audio is
|
|
11
|
+
* the entire upstream state of a clip, so re-narrating a step (new
|
|
12
|
+
* voice, new text, new provider) invalidates its presenter clip
|
|
13
|
+
* automatically — no coordination needed between the two commands.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface PresenterCacheEntry {
|
|
17
|
+
hash: string;
|
|
18
|
+
durationMs: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface PresenterCacheFile {
|
|
22
|
+
/**
|
|
23
|
+
* The generation configuration this cache was written under:
|
|
24
|
+
* `heygen/<engine>/<resolution>/<container>/<avatarId>`. Any change
|
|
25
|
+
* regenerates everything — a clip from one avatar or quality tier
|
|
26
|
+
* must never be served as a hit for another. The container is
|
|
27
|
+
* recorded even though v1 is mp4-only, so a future webm option
|
|
28
|
+
* invalidates correctly.
|
|
29
|
+
*/
|
|
30
|
+
fingerprint: string;
|
|
31
|
+
steps: (PresenterCacheEntry | null)[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CACHE_FILENAME = ".presenter-cache.json";
|
|
35
|
+
|
|
36
|
+
/** Compose the cache fingerprint for one generation configuration. */
|
|
37
|
+
export function presenterFingerprint(
|
|
38
|
+
engine: string,
|
|
39
|
+
resolution: string,
|
|
40
|
+
container: string,
|
|
41
|
+
avatarId: string,
|
|
42
|
+
): string {
|
|
43
|
+
return `heygen/${engine}/${resolution}/${container}/${avatarId}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Hash a step's narration audio bytes — the clip's entire upstream state. */
|
|
47
|
+
export function computePresenterHash(audioBytes: Buffer): string {
|
|
48
|
+
return createHash("sha256").update(audioBytes).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function loadPresenterCache(
|
|
52
|
+
outputDir: string,
|
|
53
|
+
): Promise<PresenterCacheFile | null> {
|
|
54
|
+
try {
|
|
55
|
+
const raw = await readFile(join(outputDir, CACHE_FILENAME), "utf-8");
|
|
56
|
+
return JSON.parse(raw) as PresenterCacheFile;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function savePresenterCache(
|
|
63
|
+
outputDir: string,
|
|
64
|
+
cache: PresenterCacheFile,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
await writeFile(
|
|
67
|
+
join(outputDir, CACHE_FILENAME),
|
|
68
|
+
JSON.stringify(cache, null, 2) + "\n",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether a step's clip is already cached under this fingerprint. The
|
|
74
|
+
* caller still verifies the clip file exists on disk — a cache entry
|
|
75
|
+
* for a deleted file regenerates.
|
|
76
|
+
*/
|
|
77
|
+
export function isPresenterCached(
|
|
78
|
+
cache: PresenterCacheFile | null,
|
|
79
|
+
stepIndex: number,
|
|
80
|
+
hash: string,
|
|
81
|
+
fingerprint: string,
|
|
82
|
+
): boolean {
|
|
83
|
+
if (!cache || cache.fingerprint !== fingerprint) return false;
|
|
84
|
+
const entry = cache.steps[stepIndex];
|
|
85
|
+
return entry !== null && entry !== undefined && entry.hash === hash;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildPresenterCacheFile(
|
|
89
|
+
fingerprint: string,
|
|
90
|
+
totalSteps: number,
|
|
91
|
+
entries: Map<number, { hash: string; durationMs: number }>,
|
|
92
|
+
): PresenterCacheFile {
|
|
93
|
+
const steps: (PresenterCacheEntry | null)[] = Array.from(
|
|
94
|
+
{ length: totalSteps },
|
|
95
|
+
() => null,
|
|
96
|
+
);
|
|
97
|
+
for (const [idx, entry] of entries) {
|
|
98
|
+
steps[idx] = entry;
|
|
99
|
+
}
|
|
100
|
+
return { fingerprint, steps };
|
|
101
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scenario-local asset reference resolved for shipping: where the
|
|
3
|
+
* authored file is, and where it lands in a staged public dir or packed
|
|
4
|
+
* bundle (the authored path with its leading `./` removed, so the
|
|
5
|
+
* runtime reference resolves onto the shipped file in both outputs).
|
|
6
|
+
*/
|
|
7
|
+
export interface ScenarioAsset {
|
|
8
|
+
/** Absolute path of the authored file. */
|
|
9
|
+
readonly sourcePath: string;
|
|
10
|
+
/** Destination path relative to the public dir / bundle root. */
|
|
11
|
+
readonly destRelPath: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* True when an asset src is a remote URL (has a scheme) rather than a
|
|
15
|
+
* scenario-relative path. Remote assets are referenced, never copied.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isRemoteUrl(src: string): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a scenario-relative asset reference against its scenario
|
|
20
|
+
* directory — the shared containment-and-existence contract behind
|
|
21
|
+
* every shipped asset (soundtrack music, title-card logos).
|
|
22
|
+
*
|
|
23
|
+
* Returns `null` for remote URLs (used from where they live). Throws
|
|
24
|
+
* with an actionable message when the path escapes the scenario
|
|
25
|
+
* directory or the file does not exist — both would otherwise surface
|
|
26
|
+
* as a broken reference in the packed embed or the rendered video.
|
|
27
|
+
*
|
|
28
|
+
* @param configPath - The authored field, for error messages
|
|
29
|
+
* (e.g. "soundtrack.musicSrc").
|
|
30
|
+
* @param expectation - What a valid reference looks like, appended to
|
|
31
|
+
* the error (e.g. 'an MP3 inside the scenario directory').
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveScenarioAsset(scenarioDir: string, src: string, configPath: string, expectation: string): Promise<ScenarioAsset | null>;
|
|
34
|
+
/**
|
|
35
|
+
* The image extensions the packed-embed deploy contract serves —
|
|
36
|
+
* derived from the canonical extension table so this can never drift
|
|
37
|
+
* from the server's allowlist. Notably NO svg (active content).
|
|
38
|
+
*/
|
|
39
|
+
export declare const IMAGE_EXTENSIONS: readonly string[];
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a title card's `logo_src` against its scenario directory,
|
|
42
|
+
* additionally enforcing the deploy contract's image formats — an
|
|
43
|
+
* unservable extension must fail at build time, not as a 404 (or a
|
|
44
|
+
* rejected deploy) later.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveLogoAsset(scenarioDir: string, logoSrc: string, configPath: string): Promise<ScenarioAsset | null>;
|
|
47
|
+
//# sourceMappingURL=scenario-assets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scenario-assets.d.ts","sourceRoot":"","sources":["../../../src/util/scenario-assets.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,0CAA0C;IAC1C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,iEAAiE;IACjE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEhD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,oBAAoB,CACxC,WAAW,EAAE,MAAM,EACnB,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAuB/B;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAIZ,CAAC;AAEnC;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAmB/B"}
|