@koda-sl/baker-cli 0.123.0-dev.4a85b9f30 → 0.123.0-dev.70bf43ce4
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/README.md +13 -27
- package/dist/{chunk-43KBQLP5.js → chunk-VSVGPYJK.js} +34 -143
- package/dist/chunk-VSVGPYJK.js.map +1 -0
- package/dist/cli.js +64 -373
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +0 -33
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-43KBQLP5.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
IMAGE_GENERATE_MODELS,
|
|
6
6
|
LayerExecutionError,
|
|
7
7
|
MODEL_REGISTRY,
|
|
8
|
-
REF_PREFIX,
|
|
9
8
|
SEEDANCE_DURATIONS,
|
|
10
9
|
ValidationError,
|
|
11
10
|
collectAssetRefLikes,
|
|
@@ -15,14 +14,13 @@ import {
|
|
|
15
14
|
elementMentionKeywords,
|
|
16
15
|
generateCatalog,
|
|
17
16
|
isPersistedAssetRef,
|
|
18
|
-
parseRefExpr,
|
|
19
17
|
requireCredentialsFromEnv,
|
|
20
18
|
resolveConcurrency,
|
|
21
19
|
sha256Hex,
|
|
22
20
|
toModelSafeImage,
|
|
23
21
|
ulid,
|
|
24
22
|
validateCanvasDeep
|
|
25
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-VSVGPYJK.js";
|
|
26
24
|
import {
|
|
27
25
|
csvOrJson,
|
|
28
26
|
daysAgoIso,
|
|
@@ -14660,7 +14658,6 @@ var RunRecordPoster = class {
|
|
|
14660
14658
|
latest = null;
|
|
14661
14659
|
inflight = null;
|
|
14662
14660
|
warned = false;
|
|
14663
|
-
keepaliveTimer = null;
|
|
14664
14661
|
constructor(post) {
|
|
14665
14662
|
this.post = post;
|
|
14666
14663
|
}
|
|
@@ -14669,36 +14666,12 @@ var RunRecordPoster = class {
|
|
|
14669
14666
|
this.latest = payload;
|
|
14670
14667
|
if (!this.inflight) this.inflight = this.pump();
|
|
14671
14668
|
}
|
|
14672
|
-
/**
|
|
14673
|
-
* Re-post the latest snapshot on an interval even with no new node events, so
|
|
14674
|
-
* the backend's `canvasRuns.updatedAt` heartbeat stays fresh during a long
|
|
14675
|
-
* single-clip poll (a video_generate clip can run minutes with no
|
|
14676
|
-
* intervening node events). When this process dies the keepalive stops → the
|
|
14677
|
-
* run's `updatedAt` goes stale → the backend reconciliation sweep force-fails
|
|
14678
|
-
* it as interrupted and surfaces the clips that finished. `produce` returns
|
|
14679
|
-
* null before the plan lands (nothing worth posting yet).
|
|
14680
|
-
*/
|
|
14681
|
-
startKeepalive(produce, intervalMs = 6e4) {
|
|
14682
|
-
if (this.keepaliveTimer) return;
|
|
14683
|
-
this.keepaliveTimer = setInterval(() => {
|
|
14684
|
-
const snapshot = produce();
|
|
14685
|
-
if (snapshot) this.enqueue(snapshot);
|
|
14686
|
-
}, intervalMs);
|
|
14687
|
-
this.keepaliveTimer.unref?.();
|
|
14688
|
-
}
|
|
14689
|
-
stopKeepalive() {
|
|
14690
|
-
if (this.keepaliveTimer) {
|
|
14691
|
-
clearInterval(this.keepaliveTimer);
|
|
14692
|
-
this.keepaliveTimer = null;
|
|
14693
|
-
}
|
|
14694
|
-
}
|
|
14695
14669
|
/**
|
|
14696
14670
|
* Post the terminal record (awaited, errors surfaced to the caller). Any
|
|
14697
14671
|
* queued progress snapshot is superseded — the terminal record is the full
|
|
14698
14672
|
* state — but an in-flight POST is awaited first so it can't land after.
|
|
14699
14673
|
*/
|
|
14700
14674
|
async flush(terminal) {
|
|
14701
|
-
this.stopKeepalive();
|
|
14702
14675
|
this.latest = null;
|
|
14703
14676
|
if (this.inflight) await this.inflight;
|
|
14704
14677
|
await this.post(terminal);
|
|
@@ -14801,10 +14774,6 @@ var runCommand = defineCommand88({
|
|
|
14801
14774
|
description: "Ignore any interrupted-run marker and start a new run id instead of resuming"
|
|
14802
14775
|
},
|
|
14803
14776
|
"cache-policy": { type: "string", description: "read_write | bypass | read_only" },
|
|
14804
|
-
regenerate: {
|
|
14805
|
-
type: "string",
|
|
14806
|
-
description: "Comma-separated node ids to force fresh THIS run (e.g. --regenerate gen_4x5,gen_9x16), bypassing the content cache for just those nodes + everything downstream. For a persistent re-render, bump a node's `regenerate` field in the canvas JSON instead."
|
|
14807
|
-
},
|
|
14808
14777
|
concurrency: {
|
|
14809
14778
|
type: "string",
|
|
14810
14779
|
description: "Max nodes per layer in flight at once (default 5; env BAKER_CANVAS_CONCURRENCY)"
|
|
@@ -14862,34 +14831,6 @@ var runCommand = defineCommand88({
|
|
|
14862
14831
|
);
|
|
14863
14832
|
process.exit(2);
|
|
14864
14833
|
}
|
|
14865
|
-
let regenerate;
|
|
14866
|
-
if (args.regenerate !== void 0) {
|
|
14867
|
-
const requested = String(args.regenerate).split(",").map((id) => id.trim()).filter((id) => id.length > 0);
|
|
14868
|
-
const known = new Set(canvasNodeIds(parsed));
|
|
14869
|
-
const unknown = requested.filter((id) => !known.has(id));
|
|
14870
|
-
if (unknown.length > 0) {
|
|
14871
|
-
process.stderr.write(
|
|
14872
|
-
`${JSON.stringify(
|
|
14873
|
-
{
|
|
14874
|
-
ok: false,
|
|
14875
|
-
error: {
|
|
14876
|
-
code: "unknown_regenerate_node",
|
|
14877
|
-
message: `--regenerate names node id(s) not in this canvas: ${unknown.join(", ")}. Known ids: ${[...known].join(", ")}`
|
|
14878
|
-
}
|
|
14879
|
-
},
|
|
14880
|
-
null,
|
|
14881
|
-
2
|
|
14882
|
-
)}
|
|
14883
|
-
`
|
|
14884
|
-
);
|
|
14885
|
-
process.exit(2);
|
|
14886
|
-
}
|
|
14887
|
-
if (requested.length > 0) {
|
|
14888
|
-
regenerate = new Set(requested);
|
|
14889
|
-
process.stdout.write(`[regenerate] forcing fresh this run: ${[...regenerate].join(", ")} (+ downstream)
|
|
14890
|
-
`);
|
|
14891
|
-
}
|
|
14892
|
-
}
|
|
14893
14834
|
const remoteCache = args["remote-cache"] !== void 0 ? String(args["remote-cache"]) !== "off" : void 0;
|
|
14894
14835
|
const engine = createEngineFromEnv({
|
|
14895
14836
|
cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
|
|
@@ -14919,9 +14860,6 @@ var runCommand = defineCommand88({
|
|
|
14919
14860
|
const record = args.record === false ? null : buildRecorder();
|
|
14920
14861
|
const progress = record ? new RunProgressTracker(runId, recordMeta) : null;
|
|
14921
14862
|
const poster = record ? new RunRecordPoster(record) : null;
|
|
14922
|
-
if (progress && poster) {
|
|
14923
|
-
poster.startKeepalive(() => progress.hasPlan() ? progress.snapshot() : null);
|
|
14924
|
-
}
|
|
14925
14863
|
try {
|
|
14926
14864
|
const policy = args["cache-policy"] ?? "read_write";
|
|
14927
14865
|
const result = await engine.run(parsed, {
|
|
@@ -14932,7 +14870,6 @@ var runCommand = defineCommand88({
|
|
|
14932
14870
|
(args.concurrency ?? args.parallel) !== void 0 ? String(args.concurrency ?? args.parallel) : void 0,
|
|
14933
14871
|
process.env.BAKER_CANVAS_CONCURRENCY
|
|
14934
14872
|
),
|
|
14935
|
-
regenerate,
|
|
14936
14873
|
onProgress: progress && poster ? (event) => {
|
|
14937
14874
|
progress.apply(event);
|
|
14938
14875
|
if (progress.hasPlan()) poster.enqueue(progress.snapshot());
|
|
@@ -14986,11 +14923,6 @@ var runCommand = defineCommand88({
|
|
|
14986
14923
|
}
|
|
14987
14924
|
}
|
|
14988
14925
|
});
|
|
14989
|
-
function canvasNodeIds(parsed) {
|
|
14990
|
-
const nodes = parsed?.nodes;
|
|
14991
|
-
if (!Array.isArray(nodes)) return [];
|
|
14992
|
-
return nodes.map((node) => node?.id).filter((id) => typeof id === "string");
|
|
14993
|
-
}
|
|
14994
14926
|
function buildRecorder() {
|
|
14995
14927
|
return async (payload) => {
|
|
14996
14928
|
try {
|
|
@@ -15258,79 +15190,6 @@ function isValidScaffoldSlug(slug) {
|
|
|
15258
15190
|
return slug.length <= SCAFFOLD_SLUG_MAX_LENGTH && SCAFFOLD_SLUG_PATTERN.test(slug);
|
|
15259
15191
|
}
|
|
15260
15192
|
|
|
15261
|
-
// src/commands/canvas/definition-graph.ts
|
|
15262
|
-
var MAX_NODES = 300;
|
|
15263
|
-
function walkStrings(value, cb) {
|
|
15264
|
-
if (typeof value === "string") {
|
|
15265
|
-
cb(value);
|
|
15266
|
-
return;
|
|
15267
|
-
}
|
|
15268
|
-
if (Array.isArray(value)) {
|
|
15269
|
-
for (const v of value) walkStrings(v, cb);
|
|
15270
|
-
return;
|
|
15271
|
-
}
|
|
15272
|
-
if (value && typeof value === "object") {
|
|
15273
|
-
for (const v of Object.values(value)) walkStrings(v, cb);
|
|
15274
|
-
}
|
|
15275
|
-
}
|
|
15276
|
-
function canvasToDefinitionGraph(canvas) {
|
|
15277
|
-
const rawNodes = canvas?.nodes;
|
|
15278
|
-
if (!Array.isArray(rawNodes)) return null;
|
|
15279
|
-
const parsed = [];
|
|
15280
|
-
for (const raw of rawNodes) {
|
|
15281
|
-
const id = raw?.id;
|
|
15282
|
-
const type = raw?.type;
|
|
15283
|
-
if (typeof id !== "string" || typeof type !== "string") continue;
|
|
15284
|
-
parsed.push({ id, type, inputs: raw.inputs, params: raw.params });
|
|
15285
|
-
if (parsed.length >= MAX_NODES) break;
|
|
15286
|
-
}
|
|
15287
|
-
if (parsed.length === 0) return null;
|
|
15288
|
-
const ids = new Set(parsed.map((n) => n.id));
|
|
15289
|
-
const nodes = parsed.map(({ id, type, inputs, params }) => {
|
|
15290
|
-
const deps = /* @__PURE__ */ new Set();
|
|
15291
|
-
const collect = (s) => {
|
|
15292
|
-
if (!s.startsWith(REF_PREFIX)) return;
|
|
15293
|
-
const expr = parseRefExpr(s);
|
|
15294
|
-
if (expr && expr.nodeId !== id && ids.has(expr.nodeId)) deps.add(expr.nodeId);
|
|
15295
|
-
};
|
|
15296
|
-
walkStrings(inputs, collect);
|
|
15297
|
-
walkStrings(params, collect);
|
|
15298
|
-
return deps.size > 0 ? { id, type, deps: [...deps] } : { id, type };
|
|
15299
|
-
});
|
|
15300
|
-
const rawOutput = canvas?.output;
|
|
15301
|
-
const outNode = rawOutput?.node;
|
|
15302
|
-
const outSlot = rawOutput?.output;
|
|
15303
|
-
const output = typeof outNode === "string" && typeof outSlot === "string" ? { node: outNode, output: outSlot } : void 0;
|
|
15304
|
-
return { nodes, output };
|
|
15305
|
-
}
|
|
15306
|
-
|
|
15307
|
-
// src/commands/canvas/sync-definition.ts
|
|
15308
|
-
async function syncCreativeDefinitionBestEffort(input) {
|
|
15309
|
-
const chatId = process.env.BAKER_CHAT_ID;
|
|
15310
|
-
if (!chatId) return;
|
|
15311
|
-
const graph = canvasToDefinitionGraph(input.canvas);
|
|
15312
|
-
if (!graph || graph.nodes.length === 0) return;
|
|
15313
|
-
try {
|
|
15314
|
-
const creds = requireCredentialsFromEnv();
|
|
15315
|
-
const client = new BackendClient({ baseUrl: creds.url, apiKey: creds.apiKey });
|
|
15316
|
-
await client.syncCreativeDefinition({
|
|
15317
|
-
slug: input.slug,
|
|
15318
|
-
title: input.title,
|
|
15319
|
-
platform: input.platform,
|
|
15320
|
-
formats: input.formats,
|
|
15321
|
-
sourceReferenceUrl: input.sourceReferenceUrl,
|
|
15322
|
-
graph,
|
|
15323
|
-
chatId
|
|
15324
|
-
});
|
|
15325
|
-
process.stdout.write(`[definition] synced workflow graph (${graph.nodes.length} nodes) \u2014 view it in the dashboard
|
|
15326
|
-
`);
|
|
15327
|
-
} catch (e) {
|
|
15328
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
15329
|
-
process.stderr.write(`[warn] workflow graph not synced (${msg})
|
|
15330
|
-
`);
|
|
15331
|
-
}
|
|
15332
|
-
}
|
|
15333
|
-
|
|
15334
15193
|
// src/commands/canvas/scaffold-static-ad.ts
|
|
15335
15194
|
async function fileExists(target) {
|
|
15336
15195
|
try {
|
|
@@ -15623,16 +15482,6 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15623
15482
|
"utf8"
|
|
15624
15483
|
);
|
|
15625
15484
|
}
|
|
15626
|
-
if (slug) {
|
|
15627
|
-
await syncCreativeDefinitionBestEffort({
|
|
15628
|
-
slug,
|
|
15629
|
-
title: args.title ? String(args.title) : titleFromSlug(slug),
|
|
15630
|
-
platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
|
|
15631
|
-
formats: resolveFormats(args.aspect ? String(args.aspect) : report.aspect_ratio),
|
|
15632
|
-
sourceReferenceUrl: imageIsUrl ? imageSource : void 0,
|
|
15633
|
-
canvas
|
|
15634
|
-
});
|
|
15635
|
-
}
|
|
15636
15485
|
process.stdout.write(
|
|
15637
15486
|
`${JSON.stringify(
|
|
15638
15487
|
{
|
|
@@ -15654,7 +15503,7 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15654
15503
|
edit_prompt: `Edit ${path9.basename(blueprintPath)} \u2014 it is the blueprint generated from your image; rewrite it into the ad you want (palette, copy, claims, subjects). It feeds the generator directly.`,
|
|
15655
15504
|
assets_to_supply: report.elements,
|
|
15656
15505
|
font_slot: report.includes_font ? "Drop a brand font at the [TODO] brandfont path, or delete the brandfont + type_ref nodes to skip it." : "skipped (--skip-font)",
|
|
15657
|
-
note: "
|
|
15506
|
+
note: "Replace every [TODO] ingest path with a real file, then `baker canvas validate` and `baker canvas run`. Running generates a billed image \u2014 it is not free."
|
|
15658
15507
|
}
|
|
15659
15508
|
},
|
|
15660
15509
|
null,
|
|
@@ -15666,8 +15515,7 @@ var scaffoldStaticAdCommand = defineCommand89({
|
|
|
15666
15515
|
});
|
|
15667
15516
|
|
|
15668
15517
|
// src/commands/canvas/scaffold-video.ts
|
|
15669
|
-
import {
|
|
15670
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
15518
|
+
import { cp, mkdir as mkdir3, readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
|
|
15671
15519
|
import path12 from "path";
|
|
15672
15520
|
import { defineCommand as defineCommand90 } from "citty";
|
|
15673
15521
|
|
|
@@ -16376,13 +16224,6 @@ function slimBlueprintForSelection(blueprintInput) {
|
|
|
16376
16224
|
}
|
|
16377
16225
|
return out;
|
|
16378
16226
|
}
|
|
16379
|
-
function slimBlueprintForFrameStyle(blueprintInput) {
|
|
16380
|
-
if (!blueprintInput || typeof blueprintInput !== "object" || Array.isArray(blueprintInput)) return blueprintInput;
|
|
16381
|
-
const bp = blueprintInput;
|
|
16382
|
-
const out = {};
|
|
16383
|
-
for (const k of ["version", "source", "global", "reference_elements"]) if (k in bp) out[k] = bp[k];
|
|
16384
|
-
return out;
|
|
16385
|
-
}
|
|
16386
16227
|
function roleForType2(type) {
|
|
16387
16228
|
switch (type.toLowerCase()) {
|
|
16388
16229
|
case "logo":
|
|
@@ -16640,10 +16481,9 @@ function buildFrameRef(edge, url, framePrompt, present, ctx, nodes) {
|
|
|
16640
16481
|
id: genId,
|
|
16641
16482
|
type: "image_generate",
|
|
16642
16483
|
// `params.prompt` is this frame's authoritative, edit-per-frame description.
|
|
16643
|
-
// `target_blueprint` is the
|
|
16644
|
-
//
|
|
16645
|
-
|
|
16646
|
-
inputs: { target_blueprint: "$ref:prompt_style.asset", ...reference.length > 0 ? { reference } : {} },
|
|
16484
|
+
// `target_blueprint` is the shared ad spec (cast identity, palette, brand, type)
|
|
16485
|
+
// the frame must stay consistent with — editing one frame never touches another.
|
|
16486
|
+
inputs: { target_blueprint: "$ref:prompt.asset", ...reference.length > 0 ? { reference } : {} },
|
|
16647
16487
|
params: genParams
|
|
16648
16488
|
});
|
|
16649
16489
|
return `$ref:${genId}.images#0`;
|
|
@@ -17165,24 +17005,18 @@ function emitFlashHold(i, scene, slots, ctx, lengths, out, outAr, nodes, clips)
|
|
|
17165
17005
|
});
|
|
17166
17006
|
clips.push({ ref: `$ref:s${i}_clip.video`, scene_s: lengths.dur, out });
|
|
17167
17007
|
}
|
|
17168
|
-
function emitScreenScene(i, scene, lengths, out, outAr,
|
|
17169
|
-
const
|
|
17170
|
-
const
|
|
17171
|
-
|
|
17172
|
-
|
|
17173
|
-
|
|
17174
|
-
|
|
17175
|
-
|
|
17176
|
-
|
|
17177
|
-
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
path: `[TODO: supply the REAL screen for "${label}" \u2014 NEVER AI-generate a UI. Capture a clean, text-free screenshot with \`baker images screenshot https://<brand-domain>/<path>\` (image-library skill); spoken/overlay text rides the overlay layer, not the screenshot]`,
|
|
17181
|
-
expect: "image"
|
|
17182
|
-
}
|
|
17183
|
-
});
|
|
17184
|
-
if (surfaceId) surfaceIngests.set(surfaceId, refId);
|
|
17185
|
-
}
|
|
17008
|
+
function emitScreenScene(i, scene, lengths, out, outAr, nodes, clips) {
|
|
17009
|
+
const label = commentSafe((scene.summary || scene.start_frame_prompt || "the app screen").slice(0, 120));
|
|
17010
|
+
const refId = `s${i}_screen_ref`;
|
|
17011
|
+
nodes.push({
|
|
17012
|
+
id: refId,
|
|
17013
|
+
type: "ingest",
|
|
17014
|
+
params: {
|
|
17015
|
+
source: "path",
|
|
17016
|
+
path: `[TODO: supply the REAL screen for "${label}" \u2014 NEVER AI-generate a UI. Capture a clean, text-free screenshot with \`baker images screenshot https://<brand-domain>/<path>\` (image-library skill); spoken/overlay text rides the overlay layer, not the screenshot]`,
|
|
17017
|
+
expect: "image"
|
|
17018
|
+
}
|
|
17019
|
+
});
|
|
17186
17020
|
nodes.push({
|
|
17187
17021
|
id: `s${i}_clip`,
|
|
17188
17022
|
type: "ffmpeg",
|
|
@@ -17429,7 +17263,6 @@ function makePresenterPresent(slots, canonical, opts = {}) {
|
|
|
17429
17263
|
var PAUSE_GAP_S = 0.6;
|
|
17430
17264
|
var SEEDANCE_SAFE_MAX_S = SEEDANCE_DURATIONS.find((d) => d >= 10) ?? 10;
|
|
17431
17265
|
var PHRASE_MAX_S = SEEDANCE_SAFE_MAX_S;
|
|
17432
|
-
var SEEDANCE_MAX_WORDS_PER_TAKE = 16;
|
|
17433
17266
|
var JOIN_DEDUP_MAX_WORDS = 4;
|
|
17434
17267
|
function joinKey(word) {
|
|
17435
17268
|
return word.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
|
|
@@ -17533,11 +17366,7 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17533
17366
|
const breakRun = !cur || cur.speaker !== ln.speaker || ln.start - cur.end > PAUSE_GAP_S || // Cap by SCENE COVERAGE span, not line end — a presenter run whose sliced scenes span
|
|
17534
17367
|
// more than one Seedance clip splits into the next take here (at this scene's
|
|
17535
17368
|
// boundary, never mid-scene), so no segment ever reads past the generated clip.
|
|
17536
|
-
Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S
|
|
17537
|
-
// monologue Seedance can't lip-sync cleanly. Break before this line pushes the run
|
|
17538
|
-
// past the word cap (a fresh run always accepts its own first line, so one long line
|
|
17539
|
-
// is never blocked — it just can't be sub-split without a mid-word cut).
|
|
17540
|
-
cur.words + wordCount(ln.text) > SEEDANCE_MAX_WORDS_PER_TAKE;
|
|
17369
|
+
Math.max(cur.coverEnd, lineCover) - Math.min(cur.clipStart, lineClipStart) > PHRASE_MAX_S;
|
|
17541
17370
|
if (breakRun || !cur) {
|
|
17542
17371
|
flush();
|
|
17543
17372
|
cur = {
|
|
@@ -17548,7 +17377,6 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17548
17377
|
coverEnd: lineCover,
|
|
17549
17378
|
clipStart: lineClipStart,
|
|
17550
17379
|
texts: [ln.text],
|
|
17551
|
-
words: wordCount(ln.text),
|
|
17552
17380
|
shown: /* @__PURE__ */ new Set()
|
|
17553
17381
|
};
|
|
17554
17382
|
} else {
|
|
@@ -17556,7 +17384,6 @@ function buildPhrases(blueprint, canonical, compositeScenes, presenterPresent, p
|
|
|
17556
17384
|
cur.end = Math.max(cur.end, ln.end);
|
|
17557
17385
|
cur.coverEnd = Math.max(cur.coverEnd, lineCover);
|
|
17558
17386
|
cur.clipStart = Math.min(cur.clipStart, lineClipStart);
|
|
17559
|
-
cur.words += wordCount(ln.text);
|
|
17560
17387
|
}
|
|
17561
17388
|
if (ln.shown) cur.shown.add(ln.sceneIndex);
|
|
17562
17389
|
}
|
|
@@ -17664,12 +17491,19 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
17664
17491
|
inputs: { clip: clipRef },
|
|
17665
17492
|
params: { args: audioExtractArgs(extractLen, speechOffset), outputs: { audio: { kind: "audio", ext: "mp3" } } }
|
|
17666
17493
|
});
|
|
17667
|
-
const convId =
|
|
17668
|
-
|
|
17669
|
-
|
|
17670
|
-
|
|
17494
|
+
const convId = `s${anchor}_conv`;
|
|
17495
|
+
nodes.push({
|
|
17496
|
+
id: convId,
|
|
17497
|
+
type: "audio_voice_convert",
|
|
17498
|
+
inputs: { audio: `$ref:s${anchor}_voextract.audio`, voice_ref: `$ref:${voiceNode}.voice_id` },
|
|
17499
|
+
params: { model: FIXED_VOICE_CONVERT_MODEL, voice: "{{voice_ref}}" }
|
|
17500
|
+
});
|
|
17501
|
+
out.voTracks.push({
|
|
17502
|
+
slot: convId,
|
|
17503
|
+
ref: `$ref:${convId}.audio`,
|
|
17671
17504
|
start_s: phrase.start_s,
|
|
17672
|
-
end_s: phrase.
|
|
17505
|
+
end_s: phrase.end_s,
|
|
17506
|
+
kind: "vo"
|
|
17673
17507
|
});
|
|
17674
17508
|
out.voSegments.push({
|
|
17675
17509
|
slot: convId,
|
|
@@ -17685,32 +17519,18 @@ function emitPhraseClip(phrase, voiceNode, env, nodes, out) {
|
|
|
17685
17519
|
est_speech_s: Math.round(estSpeechWindowS(phrase.text, phrase.start_s, phrase.end_s) * 100) / 100,
|
|
17686
17520
|
speech_words: wordCount(phrase.text)
|
|
17687
17521
|
});
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
while (r < shown.length) {
|
|
17694
|
-
const first = shown[r];
|
|
17695
|
-
let last = first;
|
|
17696
|
-
while (r + 1 < shown.length && shown[r + 1] === last + 1) last = shown[++r];
|
|
17697
|
-
r++;
|
|
17698
|
-
const firstSc = env.blueprint.scenes[first];
|
|
17699
|
-
if (!firstSc) continue;
|
|
17700
|
-
const firstStart = firstSc.start_s ?? clipStart;
|
|
17701
|
-
const rawOffset = firstStart - clipStart;
|
|
17702
|
-
const runEnd = env.blueprint.scenes[last]?.end_s ?? firstStart + sceneDurationS(firstSc);
|
|
17703
|
-
out.sceneSlice.set(first, {
|
|
17522
|
+
for (const s of phrase.shownScenes) {
|
|
17523
|
+
const sc = env.blueprint.scenes[s];
|
|
17524
|
+
if (!sc) continue;
|
|
17525
|
+
const rawOffset = (sc.start_s ?? clipStart) - clipStart;
|
|
17526
|
+
out.sceneSlice.set(s, {
|
|
17704
17527
|
clipRef,
|
|
17705
|
-
// Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a
|
|
17706
|
-
//
|
|
17528
|
+
// Snap a sub-frame offset (line-start vs scene-start drift) to 0 so a single-scene
|
|
17529
|
+
// phrase hits the whole-clip fast path instead of a needless re-encode + tiny shift.
|
|
17707
17530
|
offset: rawOffset < 0.05 ? 0 : rawOffset,
|
|
17708
|
-
len:
|
|
17531
|
+
len: sceneDurationS(sc),
|
|
17709
17532
|
clipDur: genDur
|
|
17710
17533
|
});
|
|
17711
|
-
for (let s = first + 1; s <= last; s++) {
|
|
17712
|
-
out.sceneSlice.set(s, { clipRef, offset: 0, len: 0, clipDur: genDur, skip: true });
|
|
17713
|
-
}
|
|
17714
17534
|
}
|
|
17715
17535
|
}
|
|
17716
17536
|
function emitPhraseTts(phrase, voiceNode, idx, used, nodes, out, languageCode) {
|
|
@@ -17853,7 +17673,7 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17853
17673
|
return void 0;
|
|
17854
17674
|
}
|
|
17855
17675
|
if (!env.reuse && sceneIsFullScreenUi(scene, present)) {
|
|
17856
|
-
emitScreenScene(i, scene, lengths, lengths.out, env.outAr,
|
|
17676
|
+
emitScreenScene(i, scene, lengths, lengths.out, env.outAr, nodes, out.clips);
|
|
17857
17677
|
return void 0;
|
|
17858
17678
|
}
|
|
17859
17679
|
const isCta = scene.narrative_role?.trim() === "cta" || isLast;
|
|
@@ -17916,22 +17736,6 @@ function emitBrollScene(scene, i, isLast, env, nodes, out, prevEndFrame) {
|
|
|
17916
17736
|
out.clips.push(clip);
|
|
17917
17737
|
return last;
|
|
17918
17738
|
}
|
|
17919
|
-
function emitPresenterSliceClip(i, slice, env, nodes, out) {
|
|
17920
|
-
if (slice.skip) return;
|
|
17921
|
-
const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
|
|
17922
|
-
const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
|
|
17923
|
-
if (whole) {
|
|
17924
|
-
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
|
|
17925
|
-
return;
|
|
17926
|
-
}
|
|
17927
|
-
nodes.push({
|
|
17928
|
-
id: `s${i}_seg`,
|
|
17929
|
-
type: "ffmpeg",
|
|
17930
|
-
inputs: { clip: slice.clipRef },
|
|
17931
|
-
params: { args: trimArgs(slice.len, slice.offset, normDims), outputs: { video: { kind: "video", ext: "mp4" } } }
|
|
17932
|
-
});
|
|
17933
|
-
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
|
|
17934
|
-
}
|
|
17935
17739
|
function buildTimeline(blueprint, slots, opts, nodes) {
|
|
17936
17740
|
const reuse = opts.frames === "reuse";
|
|
17937
17741
|
const uiRouted = uiRoutedSceneSet(blueprint);
|
|
@@ -18004,7 +17808,22 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
18004
17808
|
}
|
|
18005
17809
|
const slice = out.sceneSlice.get(i);
|
|
18006
17810
|
if (slice) {
|
|
18007
|
-
|
|
17811
|
+
const normDims = env.genAr !== env.outAr ? canvasDims(env.outAr) : void 0;
|
|
17812
|
+
const whole = slice.offset === 0 && Math.abs(slice.len - slice.clipDur) <= 0.05 && !normDims;
|
|
17813
|
+
if (whole) {
|
|
17814
|
+
out.clips.push({ ref: slice.clipRef, scene_s: slice.len, out: null });
|
|
17815
|
+
} else {
|
|
17816
|
+
nodes.push({
|
|
17817
|
+
id: `s${i}_seg`,
|
|
17818
|
+
type: "ffmpeg",
|
|
17819
|
+
inputs: { clip: slice.clipRef },
|
|
17820
|
+
params: {
|
|
17821
|
+
args: trimArgs(slice.len, slice.offset, normDims),
|
|
17822
|
+
outputs: { video: { kind: "video", ext: "mp4" } }
|
|
17823
|
+
}
|
|
17824
|
+
});
|
|
17825
|
+
out.clips.push({ ref: `$ref:s${i}_seg.video`, scene_s: slice.len, out: null });
|
|
17826
|
+
}
|
|
18008
17827
|
prevEndFrame = void 0;
|
|
18009
17828
|
return;
|
|
18010
17829
|
}
|
|
@@ -18345,24 +18164,16 @@ function buildSpine(clips, nodes) {
|
|
|
18345
18164
|
});
|
|
18346
18165
|
return "$ref:spine.video";
|
|
18347
18166
|
}
|
|
18348
|
-
function emitBlueprintIngests(opts, nodes) {
|
|
18349
|
-
nodes.push({
|
|
18350
|
-
id: "prompt",
|
|
18351
|
-
type: "ingest",
|
|
18352
|
-
params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
|
|
18353
|
-
});
|
|
18354
|
-
nodes.push({
|
|
18355
|
-
id: "prompt_style",
|
|
18356
|
-
type: "ingest",
|
|
18357
|
-
params: { source: "path", path: opts.blueprintStylePath ?? "./prompt.style.json", expect: "json" }
|
|
18358
|
-
});
|
|
18359
|
-
}
|
|
18360
18167
|
function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
18361
18168
|
const blueprint = VideoBlueprint.parse(input);
|
|
18362
18169
|
injectHookPhysicality(blueprint);
|
|
18363
18170
|
const elements = RecurringElements.parse(elementsInput);
|
|
18364
18171
|
const nodes = [];
|
|
18365
|
-
|
|
18172
|
+
nodes.push({
|
|
18173
|
+
id: "prompt",
|
|
18174
|
+
type: "ingest",
|
|
18175
|
+
params: { source: "path", path: opts.blueprintPath ?? "./prompt.json", expect: "json" }
|
|
18176
|
+
});
|
|
18366
18177
|
const slots = buildElementSlots(elements);
|
|
18367
18178
|
extendPresenceByPromptMentions(slots, blueprint);
|
|
18368
18179
|
slots.forEach((slot, i) => {
|
|
@@ -18961,56 +18772,6 @@ function fail2(code, message) {
|
|
|
18961
18772
|
`);
|
|
18962
18773
|
process.exit(2);
|
|
18963
18774
|
}
|
|
18964
|
-
var VIDEO_EXT_BY_MIME = {
|
|
18965
|
-
"video/mp4": ".mp4",
|
|
18966
|
-
"video/quicktime": ".mov",
|
|
18967
|
-
"video/webm": ".webm",
|
|
18968
|
-
"video/x-matroska": ".mkv"
|
|
18969
|
-
};
|
|
18970
|
-
function referenceVideoExt(url, contentType) {
|
|
18971
|
-
const fromPath = path12.extname(new URL(url).pathname).toLowerCase();
|
|
18972
|
-
if (fromPath && fromPath.length <= 5) return fromPath;
|
|
18973
|
-
const mime = (contentType ?? "").split(";")[0]?.trim().toLowerCase();
|
|
18974
|
-
return mime && VIDEO_EXT_BY_MIME[mime] || ".mp4";
|
|
18975
|
-
}
|
|
18976
|
-
async function fileExists2(target) {
|
|
18977
|
-
return access2(target).then(
|
|
18978
|
-
() => true,
|
|
18979
|
-
() => false
|
|
18980
|
-
);
|
|
18981
|
-
}
|
|
18982
|
-
function videoSourceReference(blueprint, fileArg2) {
|
|
18983
|
-
const bp = blueprint ?? {};
|
|
18984
|
-
const durable = typeof bp.source?.url === "string" ? bp.source.url : void 0;
|
|
18985
|
-
const original = /^https?:\/\//i.test(fileArg2) ? fileArg2 : void 0;
|
|
18986
|
-
const brand = bp.global?.branding?.brand_name;
|
|
18987
|
-
return { url: durable ?? original, advertiser: typeof brand === "string" && brand.trim() ? brand.trim() : void 0 };
|
|
18988
|
-
}
|
|
18989
|
-
function videoDefinitionDescription(blueprint) {
|
|
18990
|
-
const g = (blueprint ?? {}).global ?? {};
|
|
18991
|
-
const notes = g.reproduction_notes;
|
|
18992
|
-
if (typeof notes === "string" && notes.trim()) return notes.trim();
|
|
18993
|
-
const product = g.branding?.product;
|
|
18994
|
-
return typeof product === "string" && product.trim() ? product.trim() : void 0;
|
|
18995
|
-
}
|
|
18996
|
-
async function materializeReferenceVideo(fileArg2) {
|
|
18997
|
-
if (!/^https?:\/\//i.test(fileArg2)) return path12.resolve(fileArg2);
|
|
18998
|
-
let res;
|
|
18999
|
-
try {
|
|
19000
|
-
res = await fetch(fileArg2);
|
|
19001
|
-
} catch (e) {
|
|
19002
|
-
throw new Error(`failed to download reference video: ${e instanceof Error ? e.message : String(e)}`);
|
|
19003
|
-
}
|
|
19004
|
-
if (!res.ok) throw new Error(`failed to download reference video (${res.status} ${res.statusText})`);
|
|
19005
|
-
const bytes = Buffer.from(await res.arrayBuffer());
|
|
19006
|
-
if (bytes.length === 0) throw new Error("reference video download was empty");
|
|
19007
|
-
const dest = path12.join(
|
|
19008
|
-
tmpdir2(),
|
|
19009
|
-
`baker-ref-${sha256Hex(bytes).slice(0, 16)}${referenceVideoExt(fileArg2, res.headers.get("content-type"))}`
|
|
19010
|
-
);
|
|
19011
|
-
await writeFile3(dest, bytes);
|
|
19012
|
-
return dest;
|
|
19013
|
-
}
|
|
19014
18775
|
function resolveModels2(args) {
|
|
19015
18776
|
const pick = (flag, kind, fallback) => args[flag] ? String(args[flag]) : resolveModel2(kind, fallback);
|
|
19016
18777
|
return {
|
|
@@ -19107,11 +18868,7 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19107
18868
|
description: "Turn a reference video into a runnable reproduction canvas in one command. Runs billed passes \u2014 video_deconstruct (the full scene-by-scene blueprint + transcript, baked to prompt.json as the editable 'prompt') and an AI selection of the video's RECURRING identity elements (person/animal/product/logo) \u2014 then scaffolds a pipeline where every scene boundary is a static-ad-grade frame (the blueprint as target_blueprint, a reference legend, the real frame as anchor) and each recurring element gets ONE shared [TODO] ingest slot wired into every frame it appears in. The clips feed Seedance an ultra-detailed motion brief (action, camera, dialogue, transcript). Edit prompt.json, drop the real source images, then `baker canvas run`."
|
|
19108
18869
|
},
|
|
19109
18870
|
args: {
|
|
19110
|
-
file: {
|
|
19111
|
-
type: "positional",
|
|
19112
|
-
required: true,
|
|
19113
|
-
description: "Reference video \u2014 a local path OR an http(s) URL (e.g. a winning-ads link). A URL is downloaded for you; pass --slug or --out with it."
|
|
19114
|
-
},
|
|
18871
|
+
file: { type: "positional", required: true, description: "Path to the reference video" },
|
|
19115
18872
|
out: { type: "string", description: "Output canvas path (default <video-dir>/<name>.video.canvas.json)" },
|
|
19116
18873
|
slug: {
|
|
19117
18874
|
type: "string",
|
|
@@ -19129,14 +18886,6 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19129
18886
|
},
|
|
19130
18887
|
language: { type: "string", description: "Transcript/dialogue language hint (e.g. fr, en)" },
|
|
19131
18888
|
focus: { type: "string", description: "Known provenance/emphasis to ground the deconstruct" },
|
|
19132
|
-
advertiser: {
|
|
19133
|
-
type: "string",
|
|
19134
|
-
description: "Source advertiser recorded in _definition.md (default: the brand the deconstruct identified)"
|
|
19135
|
-
},
|
|
19136
|
-
platform: {
|
|
19137
|
-
type: "string",
|
|
19138
|
-
description: "Ad platform for _definition.md (meta|google|linkedin|tiktok|youtube|x|other; default meta)"
|
|
19139
|
-
},
|
|
19140
18889
|
"deconstruct-model": { type: "string", description: "Override the video_deconstruct model id" },
|
|
19141
18890
|
"select-model": { type: "string", description: "Override the text_generate model id for element selection" },
|
|
19142
18891
|
"image-model": { type: "string", description: "Override the image_generate model id for frames" },
|
|
@@ -19151,7 +18900,8 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19151
18900
|
}
|
|
19152
18901
|
},
|
|
19153
18902
|
async run({ args }) {
|
|
19154
|
-
const
|
|
18903
|
+
const videoPath = path12.resolve(String(args.file));
|
|
18904
|
+
const base = path12.basename(videoPath, path12.extname(videoPath));
|
|
19155
18905
|
const slug = args.slug ? String(args.slug) : void 0;
|
|
19156
18906
|
if (slug && !isValidScaffoldSlug(slug)) {
|
|
19157
18907
|
process.stderr.write(
|
|
@@ -19160,24 +18910,9 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19160
18910
|
);
|
|
19161
18911
|
process.exit(2);
|
|
19162
18912
|
}
|
|
19163
|
-
const isUrl = /^https?:\/\//i.test(fileArg2);
|
|
19164
|
-
if (isUrl && !slug && !args.out) {
|
|
19165
|
-
return fail2(
|
|
19166
|
-
"missing_output_target",
|
|
19167
|
-
"When the reference is a URL, pass --slug (writes src/creatives/<slug>/) or --out <path> so the scaffolded canvas has a home in the repo."
|
|
19168
|
-
);
|
|
19169
|
-
}
|
|
19170
|
-
let videoPath;
|
|
19171
|
-
try {
|
|
19172
|
-
videoPath = await materializeReferenceVideo(fileArg2);
|
|
19173
|
-
} catch (e) {
|
|
19174
|
-
return fail2("download", e instanceof Error ? e.message : String(e));
|
|
19175
|
-
}
|
|
19176
|
-
const base = path12.basename(videoPath, path12.extname(videoPath));
|
|
19177
18913
|
const outPath = args.out ? path12.resolve(String(args.out)) : slug ? path12.join(process.cwd(), "src", "creatives", slug, `${slug}.canvas.json`) : path12.join(path12.dirname(videoPath), `${base}.video.canvas.json`);
|
|
19178
18914
|
const outDir = path12.dirname(outPath);
|
|
19179
18915
|
const blueprintPath = path12.join(outDir, "prompt.json");
|
|
19180
|
-
const blueprintStylePath = path12.join(outDir, "prompt.style.json");
|
|
19181
18916
|
const frames = args.frames === "reuse" ? "reuse" : "generate";
|
|
19182
18917
|
const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
|
|
19183
18918
|
if (Number.isFinite(maxScenes)) {
|
|
@@ -19200,12 +18935,6 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19200
18935
|
const annotated = annotateBlueprintWithElements(blueprint, elements);
|
|
19201
18936
|
await writeFile3(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
19202
18937
|
`, "utf8");
|
|
19203
|
-
await writeFile3(
|
|
19204
|
-
blueprintStylePath,
|
|
19205
|
-
`${JSON.stringify(slimBlueprintForFrameStyle(annotated), null, 2)}
|
|
19206
|
-
`,
|
|
19207
|
-
"utf8"
|
|
19208
|
-
);
|
|
19209
18938
|
let aspect;
|
|
19210
18939
|
try {
|
|
19211
18940
|
aspect = resolveAspect(
|
|
@@ -19245,7 +18974,6 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19245
18974
|
overlayCompositionPath: path12.relative(outDir, compositionDest),
|
|
19246
18975
|
captionsCompositionPath: captions.compositionPath ? path12.relative(outDir, captions.compositionPath) : void 0,
|
|
19247
18976
|
blueprintPath: path12.relative(outDir, blueprintPath),
|
|
19248
|
-
blueprintStylePath: path12.relative(outDir, blueprintStylePath),
|
|
19249
18977
|
frames,
|
|
19250
18978
|
ambient: Boolean(args.ambient),
|
|
19251
18979
|
...args.aspect ? { aspect: String(args.aspect) } : {},
|
|
@@ -19286,42 +19014,12 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19286
19014
|
process.exit(2);
|
|
19287
19015
|
}
|
|
19288
19016
|
await ensureGitignore(process.cwd(), ["canvas/", ".context/"]);
|
|
19289
|
-
const sourceRef = videoSourceReference(blueprint, fileArg2);
|
|
19290
|
-
if (slug) {
|
|
19291
|
-
const definitionPath = path12.join(outDir, "_definition.md");
|
|
19292
|
-
if (!await fileExists2(definitionPath)) {
|
|
19293
|
-
await writeFile3(
|
|
19294
|
-
definitionPath,
|
|
19295
|
-
buildCreativeDefinition({
|
|
19296
|
-
title: titleFromSlug(slug),
|
|
19297
|
-
kind: "video",
|
|
19298
|
-
platform: resolvePlatform(args.platform ? String(args.platform) : void 0),
|
|
19299
|
-
formats: resolveFormats(aspect.outAr),
|
|
19300
|
-
sourceReferenceUrl: sourceRef.url,
|
|
19301
|
-
sourceAdvertiser: args.advertiser ? String(args.advertiser) : sourceRef.advertiser,
|
|
19302
|
-
sourceKind: "video",
|
|
19303
|
-
description: videoDefinitionDescription(blueprint)
|
|
19304
|
-
}),
|
|
19305
|
-
"utf8"
|
|
19306
|
-
);
|
|
19307
|
-
}
|
|
19308
|
-
}
|
|
19309
|
-
if (slug) {
|
|
19310
|
-
await syncCreativeDefinitionBestEffort({
|
|
19311
|
-
slug,
|
|
19312
|
-
title: titleFromSlug(slug),
|
|
19313
|
-
formats: [aspect.outAr],
|
|
19314
|
-
canvas,
|
|
19315
|
-
sourceReferenceUrl: sourceRef.url
|
|
19316
|
-
});
|
|
19317
|
-
}
|
|
19318
19017
|
process.stdout.write(
|
|
19319
19018
|
`${JSON.stringify(
|
|
19320
19019
|
{
|
|
19321
19020
|
ok: true,
|
|
19322
19021
|
canvas_path: outPath,
|
|
19323
19022
|
prompt_path: blueprintPath,
|
|
19324
|
-
source_reference: sourceRef.url,
|
|
19325
19023
|
composition_dir: compositionDest,
|
|
19326
19024
|
output: canvas.output,
|
|
19327
19025
|
frames_mode: frames,
|
|
@@ -19347,13 +19045,6 @@ var scaffoldVideoCommand = defineCommand90({
|
|
|
19347
19045
|
scenes_clamped_to_15s: report.clamped_scenes,
|
|
19348
19046
|
oversize_scenes: report.oversize_scenes,
|
|
19349
19047
|
overstuffed_scenes: report.overstuffed_scenes,
|
|
19350
|
-
// A photoreal on-camera person/animal on Seedance can trip ByteDance's
|
|
19351
|
-
// real-person-likeness filter (422 content_policy_blocked, NON-retryable — no
|
|
19352
|
-
// prompt reframe clears it). Surface the escape BEFORE the billed run so a
|
|
19353
|
-
// face-heavy ad isn't discovered broken mid-render.
|
|
19354
|
-
...report.elements.some((e) => e.type === "person" || e.type === "animal") && /seedance/i.test(videoModel) ? {
|
|
19355
|
-
content_policy_risk: "This ad has a photoreal on-camera cast generating on Seedance. ByteDance's real-person-likeness filter can reject a photoreal AI face with a NON-retryable 422 (content_policy_blocked) \u2014 no prompt change clears it. If clips fail that way, regenerate on Veo (re-run with `--video-model google/veo-3.1-fast`) or make the frame less photoreal."
|
|
19356
|
-
} : {},
|
|
19357
19048
|
note: "Drop ONE real source image at each el_* [TODO] (reused across every frame that element appears in), confirm each voice_select casting, then `baker canvas validate` and `baker canvas run`. Running generates many billed image/video/audio assets \u2014 it is not free."
|
|
19358
19049
|
}
|
|
19359
19050
|
},
|