@koda-sl/baker-cli 0.70.0 → 0.71.2
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 +11 -3
- package/canvas/video-overlay-composition/index.html +65 -191
- package/canvas/video-overlay-composition/meta.json +4 -15
- package/dist/{chunk-XFDZVKLF.js → chunk-JIDZ37KG.js} +67 -3
- package/dist/chunk-JIDZ37KG.js.map +1 -0
- package/dist/cli.js +391 -87
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +14 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-XFDZVKLF.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
defaultRegistry,
|
|
10
10
|
generateCatalog,
|
|
11
11
|
validateCanvasDeep
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-JIDZ37KG.js";
|
|
13
13
|
|
|
14
14
|
// src/cli.ts
|
|
15
15
|
import { defineCommand as defineCommand138, runMain } from "citty";
|
|
@@ -147,9 +147,9 @@ async function handleResponse(response) {
|
|
|
147
147
|
throw new ApiError("INTERNAL_ERROR", "Failed to parse API response as JSON");
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
|
-
async function apiGet(
|
|
150
|
+
async function apiGet(path7, params) {
|
|
151
151
|
const env = getEnv();
|
|
152
|
-
const url = new URL(
|
|
152
|
+
const url = new URL(path7, env.BAKER_API_URL);
|
|
153
153
|
if (params) {
|
|
154
154
|
const clean = sanitizeParams(params);
|
|
155
155
|
for (const [key, value] of Object.entries(clean)) {
|
|
@@ -174,12 +174,12 @@ async function apiGet(path6, params) {
|
|
|
174
174
|
}
|
|
175
175
|
return handleResponse(response);
|
|
176
176
|
}
|
|
177
|
-
async function apiPost(
|
|
177
|
+
async function apiPost(path7, body, opts) {
|
|
178
178
|
const env = getEnv();
|
|
179
179
|
const timeoutMs = opts?.timeoutMs ?? 6e4;
|
|
180
180
|
let response;
|
|
181
181
|
try {
|
|
182
|
-
response = await fetchWithRateLimitRetry(new URL(
|
|
182
|
+
response = await fetchWithRateLimitRetry(new URL(path7, env.BAKER_API_URL).toString(), {
|
|
183
183
|
method: "POST",
|
|
184
184
|
headers: {
|
|
185
185
|
Authorization: `Bearer ${env.BAKER_API_KEY}`,
|
|
@@ -918,31 +918,31 @@ function cachePath(category, key) {
|
|
|
918
918
|
return join(dir, `${hashKey(key)}.json`);
|
|
919
919
|
}
|
|
920
920
|
function cacheGet(category, key) {
|
|
921
|
-
const
|
|
922
|
-
if (!existsSync(
|
|
921
|
+
const path7 = cachePath(category, key);
|
|
922
|
+
if (!existsSync(path7)) {
|
|
923
923
|
return null;
|
|
924
924
|
}
|
|
925
925
|
try {
|
|
926
|
-
const raw = readFileSync(
|
|
926
|
+
const raw = readFileSync(path7, "utf-8");
|
|
927
927
|
const entry = JSON.parse(raw);
|
|
928
928
|
if (entry.expiresAt < Date.now()) {
|
|
929
|
-
rmSync(
|
|
929
|
+
rmSync(path7, { force: true });
|
|
930
930
|
return null;
|
|
931
931
|
}
|
|
932
932
|
return entry;
|
|
933
933
|
} catch {
|
|
934
|
-
rmSync(
|
|
934
|
+
rmSync(path7, { force: true });
|
|
935
935
|
return null;
|
|
936
936
|
}
|
|
937
937
|
}
|
|
938
938
|
function cacheSet(category, key, data, ttlMs, fields) {
|
|
939
|
-
const
|
|
939
|
+
const path7 = cachePath(category, key);
|
|
940
940
|
const entry = {
|
|
941
941
|
expiresAt: Date.now() + ttlMs,
|
|
942
942
|
data,
|
|
943
943
|
fields
|
|
944
944
|
};
|
|
945
|
-
writeFileSync(
|
|
945
|
+
writeFileSync(path7, JSON.stringify(entry), "utf-8");
|
|
946
946
|
}
|
|
947
947
|
var HOUR = 60 * 60 * 1e3;
|
|
948
948
|
var MINUTE = 60 * 1e3;
|
|
@@ -7714,6 +7714,24 @@ async function probeDuration(filePath) {
|
|
|
7714
7714
|
import { readFile as readFile2 } from "fs/promises";
|
|
7715
7715
|
import path2 from "path";
|
|
7716
7716
|
import { defineCommand as defineCommand74 } from "citty";
|
|
7717
|
+
|
|
7718
|
+
// src/commands/canvas/placeholders.ts
|
|
7719
|
+
function unsuppliedPlaceholderAssets(canvas) {
|
|
7720
|
+
const nodes = canvas?.nodes;
|
|
7721
|
+
if (!Array.isArray(nodes)) return [];
|
|
7722
|
+
const out = [];
|
|
7723
|
+
for (const n of nodes) {
|
|
7724
|
+
const node = n;
|
|
7725
|
+
if (node?.type !== "ingest") continue;
|
|
7726
|
+
const params = node.params;
|
|
7727
|
+
if (params?.source === "path" && typeof params.path === "string" && params.path.includes("[TODO")) {
|
|
7728
|
+
out.push({ node: String(node.id ?? "?"), placeholder: params.path });
|
|
7729
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
return out;
|
|
7732
|
+
}
|
|
7733
|
+
|
|
7734
|
+
// src/commands/canvas/run.ts
|
|
7717
7735
|
var runCommand = defineCommand74({
|
|
7718
7736
|
meta: { name: "run", description: "Validate and execute a canvas JSON file." },
|
|
7719
7737
|
args: {
|
|
@@ -7735,6 +7753,25 @@ var runCommand = defineCommand74({
|
|
|
7735
7753
|
`);
|
|
7736
7754
|
process.exit(2);
|
|
7737
7755
|
}
|
|
7756
|
+
const pending = unsuppliedPlaceholderAssets(parsed);
|
|
7757
|
+
if (pending.length > 0) {
|
|
7758
|
+
process.stderr.write(
|
|
7759
|
+
`${JSON.stringify(
|
|
7760
|
+
{
|
|
7761
|
+
ok: false,
|
|
7762
|
+
error: {
|
|
7763
|
+
code: "unsupplied_assets",
|
|
7764
|
+
message: "This canvas still has placeholder asset slots \u2014 supply a real image (or video) at each before running. Each `el_*` ingest is a [TODO] you fill with the real logo/subject/product, then re-run.",
|
|
7765
|
+
assets: pending
|
|
7766
|
+
}
|
|
7767
|
+
},
|
|
7768
|
+
null,
|
|
7769
|
+
2
|
|
7770
|
+
)}
|
|
7771
|
+
`
|
|
7772
|
+
);
|
|
7773
|
+
process.exit(2);
|
|
7774
|
+
}
|
|
7738
7775
|
const engine = createEngineFromEnv({
|
|
7739
7776
|
cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
|
|
7740
7777
|
outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
|
|
@@ -8192,7 +8229,7 @@ var scaffoldStaticAdCommand = defineCommand75({
|
|
|
8192
8229
|
|
|
8193
8230
|
// src/commands/canvas/scaffold-video.ts
|
|
8194
8231
|
import { cp, mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
8195
|
-
import
|
|
8232
|
+
import path5 from "path";
|
|
8196
8233
|
import { defineCommand as defineCommand76 } from "citty";
|
|
8197
8234
|
|
|
8198
8235
|
// src/engine/scaffold/video.ts
|
|
@@ -8200,7 +8237,19 @@ import { z as z3 } from "zod";
|
|
|
8200
8237
|
var FIXED_TTS_MODEL = "elevenlabs/eleven_v3";
|
|
8201
8238
|
var FIXED_SFX_MODEL = "elevenlabs/eleven_text_to_sound_v2";
|
|
8202
8239
|
var FIXED_MUSIC_MODEL = "elevenlabs/music-v1";
|
|
8240
|
+
var FIXED_LIPSYNC_MODEL = "fal/veed-lipsync";
|
|
8203
8241
|
var MUSIC_BED_GAIN_DB = -12;
|
|
8242
|
+
var NARRATOR_SPEAKERS = /* @__PURE__ */ new Set([
|
|
8243
|
+
"voiceover",
|
|
8244
|
+
"voice_over",
|
|
8245
|
+
"narrator",
|
|
8246
|
+
"narration",
|
|
8247
|
+
"vo",
|
|
8248
|
+
"announcer",
|
|
8249
|
+
"off_screen",
|
|
8250
|
+
"offscreen",
|
|
8251
|
+
"off-screen"
|
|
8252
|
+
]);
|
|
8204
8253
|
var SHARED_ASPECT_RATIOS = /* @__PURE__ */ new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"]);
|
|
8205
8254
|
var EDGES = ["start", "end"];
|
|
8206
8255
|
function snapToSeedance(durationS) {
|
|
@@ -8216,11 +8265,39 @@ function snapToSeedance(durationS) {
|
|
|
8216
8265
|
}
|
|
8217
8266
|
return best;
|
|
8218
8267
|
}
|
|
8268
|
+
function ceilToSeedance(durationS) {
|
|
8269
|
+
const max = SEEDANCE_DURATIONS[SEEDANCE_DURATIONS.length - 1];
|
|
8270
|
+
if (!Number.isFinite(durationS) || durationS <= 0) return SEEDANCE_DURATIONS[0];
|
|
8271
|
+
for (const d of SEEDANCE_DURATIONS) if (d >= durationS) return d;
|
|
8272
|
+
return max;
|
|
8273
|
+
}
|
|
8274
|
+
function sceneDurationS(scene) {
|
|
8275
|
+
const raw = scene.duration_s ?? (scene.end_s != null && scene.start_s != null ? scene.end_s - scene.start_s : 5);
|
|
8276
|
+
const max = SEEDANCE_DURATIONS[SEEDANCE_DURATIONS.length - 1];
|
|
8277
|
+
return Math.min(Math.max(raw, 0.5), max);
|
|
8278
|
+
}
|
|
8279
|
+
function trimArgs(durationS) {
|
|
8280
|
+
return [
|
|
8281
|
+
"-i",
|
|
8282
|
+
"{{in.clip}}",
|
|
8283
|
+
"-t",
|
|
8284
|
+
durationS.toFixed(3),
|
|
8285
|
+
"-an",
|
|
8286
|
+
"-c:v",
|
|
8287
|
+
"libx264",
|
|
8288
|
+
"-pix_fmt",
|
|
8289
|
+
"yuv420p",
|
|
8290
|
+
"{{out.video}}"
|
|
8291
|
+
];
|
|
8292
|
+
}
|
|
8219
8293
|
var FrameAsset = z3.object({ url: z3.string().optional() }).loose().optional();
|
|
8220
8294
|
var DialogueLine = z3.object({
|
|
8221
8295
|
speaker: z3.string().optional(),
|
|
8222
8296
|
line: z3.string().optional(),
|
|
8297
|
+
// Absolute seconds on the source timeline (the deconstruct emits both).
|
|
8223
8298
|
start_s: z3.number().optional(),
|
|
8299
|
+
end_s: z3.number().optional(),
|
|
8300
|
+
delivery: z3.string().optional(),
|
|
8224
8301
|
voice_description: z3.string().optional()
|
|
8225
8302
|
}).loose();
|
|
8226
8303
|
var Sfx = z3.object({
|
|
@@ -8260,7 +8337,12 @@ var VideoBlueprint = z3.object({
|
|
|
8260
8337
|
identified_track: z3.object({ title: z3.string().optional(), artist: z3.string().optional() }).loose().nullish()
|
|
8261
8338
|
}).loose().optional(),
|
|
8262
8339
|
cast: z3.array(z3.object({ id: z3.string().optional(), description: z3.string().optional() }).loose()).optional(),
|
|
8263
|
-
voiceover: z3.object({
|
|
8340
|
+
voiceover: z3.object({
|
|
8341
|
+
// on_camera | mixed → mouths are on screen (lip-sync candidates);
|
|
8342
|
+
// voiceover | none → narration over the picture (no lip-sync).
|
|
8343
|
+
mode: z3.string().optional(),
|
|
8344
|
+
voice_description: z3.string().optional()
|
|
8345
|
+
}).loose().optional()
|
|
8264
8346
|
}).loose().optional(),
|
|
8265
8347
|
scenes: z3.array(Scene).min(1)
|
|
8266
8348
|
}).loose();
|
|
@@ -8392,7 +8474,17 @@ function buildFramePrompt(edge, sceneIndex, framePrompt, present, hasAnchor) {
|
|
|
8392
8474
|
].join("\n");
|
|
8393
8475
|
const description = framePrompt?.trim() || `the ${edge} frame of scene ${sceneIndex + 1} \u2014 describe the full composition, subjects, setting, action, lighting, and palette here. (Edit this line to change ONLY this frame.)`;
|
|
8394
8476
|
return [
|
|
8395
|
-
`Render the ${EDGE} frame of scene ${sceneIndex + 1} as a single still image. This prompt is self-contained and edit-per-frame: change the FRAME DESCRIPTION below to alter ONLY this frame
|
|
8477
|
+
`Render the ${EDGE} frame of scene ${sceneIndex + 1} as a single still image. This prompt is self-contained and edit-per-frame: change the FRAME DESCRIPTION below to alter ONLY this frame.`,
|
|
8478
|
+
"",
|
|
8479
|
+
"CRITICAL \u2014 RENDER A CLEAN PLATE WITH ZERO TEXT OR GRAPHICS:",
|
|
8480
|
+
"This frame is a background plate. ALL words, captions, headlines, lower-third bars,",
|
|
8481
|
+
"news tickers/crawls, chyrons, on-screen logos/wordmarks, station bugs, watermarks,",
|
|
8482
|
+
"subtitles, UI and numbers are added afterwards as a separate HTML layer. Render NONE",
|
|
8483
|
+
"of them \u2014 no legible text anywhere in the image, not even in the background, on the",
|
|
8484
|
+
"news desk, on screens, or as part of a 'broadcast look'. If a reference image (a logo,",
|
|
8485
|
+
"a desk, a studio) contains any text or graphics, DO NOT reproduce that text \u2014 render",
|
|
8486
|
+
"the subject/scene only, with blank surfaces where text would be. Imperfect/garbled",
|
|
8487
|
+
"letterforms are the worst outcome; leave those areas clean.",
|
|
8396
8488
|
"",
|
|
8397
8489
|
"REFERENCE IMAGES (in the order provided):",
|
|
8398
8490
|
legend,
|
|
@@ -8400,7 +8492,7 @@ function buildFramePrompt(edge, sceneIndex, framePrompt, present, hasAnchor) {
|
|
|
8400
8492
|
"FRAME DESCRIPTION (this frame's editable prompt):",
|
|
8401
8493
|
description,
|
|
8402
8494
|
"",
|
|
8403
|
-
"Keep every recurring element identical to its reference image across all frames. Use the GLOBAL STYLE REFERENCE only for shared cast identity, palette, typography mood, and aspect ratio \u2014 do NOT copy another scene's composition from it; this frame's content is the FRAME DESCRIPTION above.",
|
|
8495
|
+
"Keep every recurring element identical to its reference image across all frames. Use the GLOBAL STYLE REFERENCE only for shared cast identity, palette, typography mood, and aspect ratio \u2014 do NOT copy another scene's composition from it; this frame's content is the FRAME DESCRIPTION above. Again: NO rendered text or graphic overlays \u2014 clean plate only.",
|
|
8404
8496
|
"",
|
|
8405
8497
|
"GLOBAL STYLE REFERENCE (shared across frames; not this frame's content):",
|
|
8406
8498
|
"{{target_blueprint}}"
|
|
@@ -8451,7 +8543,7 @@ function buildSeedancePrompt(scene, sceneIndex, present) {
|
|
|
8451
8543
|
if (transcript) parts.push(`Transcript: ${transcript}`);
|
|
8452
8544
|
return parts.join("\n");
|
|
8453
8545
|
}
|
|
8454
|
-
function buildSceneVisuals(blueprint, slots, opts, nodes) {
|
|
8546
|
+
function buildSceneVisuals(blueprint, slots, opts, nodes, sceneTurns) {
|
|
8455
8547
|
const ar = aspectRatioParam(blueprint);
|
|
8456
8548
|
const reuse = opts.frames === "reuse";
|
|
8457
8549
|
const clipRefs = [];
|
|
@@ -8473,10 +8565,11 @@ function buildSceneVisuals(blueprint, slots, opts, nodes) {
|
|
|
8473
8565
|
ctx,
|
|
8474
8566
|
nodes
|
|
8475
8567
|
);
|
|
8568
|
+
const dur = sceneDurationS(scene);
|
|
8476
8569
|
const clipParams = {
|
|
8477
8570
|
model: opts.videoModel,
|
|
8478
8571
|
prompt: buildSeedancePrompt(scene, i, slotsForScene(slots, i)),
|
|
8479
|
-
duration:
|
|
8572
|
+
duration: ceilToSeedance(dur)
|
|
8480
8573
|
};
|
|
8481
8574
|
if (ar) clipParams.aspect_ratio = ar;
|
|
8482
8575
|
nodes.push({
|
|
@@ -8485,7 +8578,29 @@ function buildSceneVisuals(blueprint, slots, opts, nodes) {
|
|
|
8485
8578
|
inputs: { first_frame: firstFrame, last_frame: lastFrame },
|
|
8486
8579
|
params: clipParams
|
|
8487
8580
|
});
|
|
8488
|
-
|
|
8581
|
+
let base = `$ref:s${i}_clip.video`;
|
|
8582
|
+
const onCam = (sceneTurns.get(i) ?? []).filter((t) => t.onCamera);
|
|
8583
|
+
const solo = onCam.length === 1 ? onCam[0] : void 0;
|
|
8584
|
+
if (solo) {
|
|
8585
|
+
nodes.push({
|
|
8586
|
+
id: `s${i}_lipsync`,
|
|
8587
|
+
type: "video_lipsync",
|
|
8588
|
+
inputs: { video: base, audio: solo.audioRef },
|
|
8589
|
+
params: { model: FIXED_LIPSYNC_MODEL }
|
|
8590
|
+
});
|
|
8591
|
+
base = `$ref:s${i}_lipsync.video`;
|
|
8592
|
+
}
|
|
8593
|
+
if (ceilToSeedance(dur) === dur) {
|
|
8594
|
+
clipRefs.push(base);
|
|
8595
|
+
} else {
|
|
8596
|
+
nodes.push({
|
|
8597
|
+
id: `s${i}_trim`,
|
|
8598
|
+
type: "ffmpeg",
|
|
8599
|
+
inputs: { clip: base },
|
|
8600
|
+
params: { args: trimArgs(dur), outputs: { video: { kind: "video", ext: "mp4" } } }
|
|
8601
|
+
});
|
|
8602
|
+
clipRefs.push(`$ref:s${i}_trim.video`);
|
|
8603
|
+
}
|
|
8489
8604
|
});
|
|
8490
8605
|
return clipRefs;
|
|
8491
8606
|
}
|
|
@@ -8498,8 +8613,21 @@ function musicBedPrompt(blueprint, musicPrompt) {
|
|
|
8498
8613
|
|
|
8499
8614
|
Reference vibe: the original used "${title}"${by} (identified via AudD). Match its mood, tempo, and energy with ORIGINAL music \u2014 do not reproduce the track.`;
|
|
8500
8615
|
}
|
|
8501
|
-
function
|
|
8616
|
+
function onCameraDialogue(blueprint) {
|
|
8617
|
+
const mode = blueprint.global?.voiceover?.mode;
|
|
8618
|
+
return mode !== "voiceover" && mode !== "none";
|
|
8619
|
+
}
|
|
8620
|
+
var castIdSet = (blueprint) => new Set((blueprint.global?.cast ?? []).map((c) => c.id).filter((id) => Boolean(id)));
|
|
8621
|
+
function isOnCameraSpeaker(speaker, casts, cameraOn) {
|
|
8622
|
+
if (!cameraOn) return false;
|
|
8623
|
+
if (NARRATOR_SPEAKERS.has(speaker.toLowerCase())) return false;
|
|
8624
|
+
return casts.has(speaker);
|
|
8625
|
+
}
|
|
8626
|
+
function buildDialogue(blueprint, nodes) {
|
|
8502
8627
|
const tracks = [];
|
|
8628
|
+
const sceneTurns = /* @__PURE__ */ new Map();
|
|
8629
|
+
const casts = castIdSet(blueprint);
|
|
8630
|
+
const cameraOn = onCameraDialogue(blueprint);
|
|
8503
8631
|
const voiceNodeBySpeaker = /* @__PURE__ */ new Map();
|
|
8504
8632
|
const speakerDescription = (speaker) => {
|
|
8505
8633
|
for (const scene of blueprint.scenes) {
|
|
@@ -8518,41 +8646,62 @@ function buildAudio(blueprint, nodes) {
|
|
|
8518
8646
|
voiceNodeBySpeaker.set(speaker, id);
|
|
8519
8647
|
return id;
|
|
8520
8648
|
};
|
|
8521
|
-
const scriptBySpeaker = /* @__PURE__ */ new Map();
|
|
8522
|
-
const orderedSpeakers = [];
|
|
8523
|
-
for (const scene of blueprint.scenes) {
|
|
8524
|
-
for (const line of scene.dialogue ?? []) {
|
|
8525
|
-
if (!line.line) continue;
|
|
8526
|
-
const speaker = line.speaker ?? "voiceover";
|
|
8527
|
-
const start = line.start_s ?? scene.start_s ?? 0;
|
|
8528
|
-
const existing = scriptBySpeaker.get(speaker);
|
|
8529
|
-
if (existing) {
|
|
8530
|
-
existing.lines.push(line.line);
|
|
8531
|
-
existing.start = Math.min(existing.start, start);
|
|
8532
|
-
} else {
|
|
8533
|
-
scriptBySpeaker.set(speaker, { lines: [line.line], start });
|
|
8534
|
-
orderedSpeakers.push(speaker);
|
|
8535
|
-
}
|
|
8536
|
-
}
|
|
8537
|
-
}
|
|
8538
8649
|
const usedVoIds = /* @__PURE__ */ new Set();
|
|
8539
|
-
|
|
8540
|
-
const
|
|
8541
|
-
if (
|
|
8542
|
-
const
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8650
|
+
blueprint.scenes.forEach((scene, sceneIndex) => {
|
|
8651
|
+
const lines = (scene.dialogue ?? []).filter((l) => Boolean(l.line?.trim())).slice().sort((a, b) => (a.start_s ?? 0) - (b.start_s ?? 0));
|
|
8652
|
+
if (lines.length === 0) return;
|
|
8653
|
+
const groups = [];
|
|
8654
|
+
for (const line of lines) {
|
|
8655
|
+
const speaker = line.speaker ?? "voiceover";
|
|
8656
|
+
const last = groups[groups.length - 1];
|
|
8657
|
+
if (last && last.speaker === speaker) last.lines.push(line);
|
|
8658
|
+
else groups.push({ speaker, lines: [line] });
|
|
8659
|
+
}
|
|
8660
|
+
const list = [];
|
|
8661
|
+
groups.forEach((group, gi) => {
|
|
8662
|
+
const first = group.lines[0];
|
|
8663
|
+
const last = group.lines[group.lines.length - 1];
|
|
8664
|
+
if (!first || !last) return;
|
|
8665
|
+
const start = first.start_s ?? scene.start_s ?? 0;
|
|
8666
|
+
const end = last.end_s ?? last.start_s ?? scene.end_s ?? start;
|
|
8667
|
+
const voiceNode = ensureVoiceNode(group.speaker);
|
|
8668
|
+
let id = sanitizeId2(`vo_s${sceneIndex}_${group.speaker}`, `vo_${sceneIndex}_${gi}`);
|
|
8669
|
+
if (usedVoIds.has(id)) {
|
|
8670
|
+
let n = 2;
|
|
8671
|
+
while (usedVoIds.has(`${id}_${n}`)) n++;
|
|
8672
|
+
id = `${id}_${n}`;
|
|
8673
|
+
}
|
|
8674
|
+
usedVoIds.add(id);
|
|
8675
|
+
nodes.push({
|
|
8676
|
+
id,
|
|
8677
|
+
type: "tts",
|
|
8678
|
+
inputs: { voice_ref: `$ref:${voiceNode}.voice_id` },
|
|
8679
|
+
// Lines join with a space; each keeps its terminal punctuation so eleven_v3
|
|
8680
|
+
// reads the sentence boundaries (and their pauses) within the one turn.
|
|
8681
|
+
params: {
|
|
8682
|
+
model: FIXED_TTS_MODEL,
|
|
8683
|
+
text: group.lines.map((l) => l.line.trim()).join(" "),
|
|
8684
|
+
voice: "{{voice_ref}}"
|
|
8685
|
+
}
|
|
8686
|
+
});
|
|
8687
|
+
const turn = {
|
|
8688
|
+
sceneIndex,
|
|
8689
|
+
speaker: group.speaker,
|
|
8690
|
+
onCamera: isOnCameraSpeaker(group.speaker, casts, cameraOn),
|
|
8691
|
+
start_s: start,
|
|
8692
|
+
end_s: end,
|
|
8693
|
+
ttsId: id,
|
|
8694
|
+
audioRef: `$ref:${id}.audio`
|
|
8695
|
+
};
|
|
8696
|
+
list.push(turn);
|
|
8697
|
+
tracks.push({ slot: id, ref: turn.audioRef, start_s: start, end_s: end, kind: "vo" });
|
|
8553
8698
|
});
|
|
8554
|
-
|
|
8699
|
+
sceneTurns.set(sceneIndex, list);
|
|
8555
8700
|
});
|
|
8701
|
+
return { tracks, sceneTurns };
|
|
8702
|
+
}
|
|
8703
|
+
function buildSfxMusic(blueprint, nodes) {
|
|
8704
|
+
const tracks = [];
|
|
8556
8705
|
blueprint.scenes.forEach((scene, i) => {
|
|
8557
8706
|
(scene.sfx ?? []).forEach((sfx, k) => {
|
|
8558
8707
|
const text = sfx.sound_effect_prompt ?? sfx.description;
|
|
@@ -8561,7 +8710,12 @@ function buildAudio(blueprint, nodes) {
|
|
|
8561
8710
|
const params = { model: FIXED_SFX_MODEL, text };
|
|
8562
8711
|
if (typeof sfx.duration_s === "number") params.duration_seconds = Math.min(Math.max(sfx.duration_s, 0.5), 30);
|
|
8563
8712
|
nodes.push({ id, type: "sound_effect", params });
|
|
8564
|
-
tracks.push({
|
|
8713
|
+
tracks.push({
|
|
8714
|
+
slot: `sfx_s${i}_${k}`,
|
|
8715
|
+
ref: `$ref:${id}.audio`,
|
|
8716
|
+
start_s: sfx.at_s ?? scene.start_s ?? 0,
|
|
8717
|
+
kind: "sfx"
|
|
8718
|
+
});
|
|
8565
8719
|
});
|
|
8566
8720
|
});
|
|
8567
8721
|
const musicPrompt = blueprint.global?.music?.music_prompt;
|
|
@@ -8573,10 +8727,90 @@ function buildAudio(blueprint, nodes) {
|
|
|
8573
8727
|
type: "music",
|
|
8574
8728
|
params: { model: FIXED_MUSIC_MODEL, prompt: musicBedPrompt(blueprint, musicPrompt), music_length_ms: musicMs }
|
|
8575
8729
|
});
|
|
8576
|
-
tracks.
|
|
8730
|
+
tracks.push({ slot: "music", ref: "$ref:music_bed.audio", start_s: 0, gain_db: MUSIC_BED_GAIN_DB, kind: "music" });
|
|
8577
8731
|
}
|
|
8578
8732
|
return tracks;
|
|
8579
8733
|
}
|
|
8734
|
+
var OverlayStyle = z3.object({ color_hex: z3.string().optional(), background: z3.string().optional(), size: z3.string().optional() }).loose();
|
|
8735
|
+
var Overlay = z3.object({
|
|
8736
|
+
text: z3.string().optional(),
|
|
8737
|
+
appears_at_s: z3.number().optional(),
|
|
8738
|
+
duration_s: z3.number().optional(),
|
|
8739
|
+
position: z3.string().optional(),
|
|
8740
|
+
role: z3.string().optional(),
|
|
8741
|
+
animation: z3.string().optional(),
|
|
8742
|
+
animation_detail: z3.string().optional(),
|
|
8743
|
+
style: OverlayStyle.optional()
|
|
8744
|
+
}).loose();
|
|
8745
|
+
var FloatingElement = z3.object({
|
|
8746
|
+
kind: z3.string().optional(),
|
|
8747
|
+
description: z3.string().optional(),
|
|
8748
|
+
brand_name: z3.string().nullish(),
|
|
8749
|
+
what_it_represents: z3.string().optional(),
|
|
8750
|
+
appears_at_s: z3.number().optional(),
|
|
8751
|
+
duration_s: z3.number().optional(),
|
|
8752
|
+
position: z3.string().optional()
|
|
8753
|
+
}).loose();
|
|
8754
|
+
function escapeHtml(s) {
|
|
8755
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8756
|
+
}
|
|
8757
|
+
function commentSafe(s) {
|
|
8758
|
+
return escapeHtml(s).replace(/-{2,}/g, "\u2013");
|
|
8759
|
+
}
|
|
8760
|
+
var SUPPORTED_ANIMS = /* @__PURE__ */ new Set(["fade", "slide_up", "slide_down", "pop"]);
|
|
8761
|
+
function normalizeAnim(animation) {
|
|
8762
|
+
if (!animation || animation === "none") return void 0;
|
|
8763
|
+
const mapped = animation === "slide" ? "slide_up" : animation;
|
|
8764
|
+
return SUPPORTED_ANIMS.has(mapped) ? mapped : void 0;
|
|
8765
|
+
}
|
|
8766
|
+
function positionClass(position) {
|
|
8767
|
+
const p = (position ?? "bottom_center").toLowerCase().replace(/[^a-z]+/g, "-");
|
|
8768
|
+
return `pos-${p}`;
|
|
8769
|
+
}
|
|
8770
|
+
function overlayElement(ov, sceneStart) {
|
|
8771
|
+
if (!ov.text?.trim()) return "";
|
|
8772
|
+
const at = ov.appears_at_s ?? sceneStart;
|
|
8773
|
+
const dur = ov.duration_s ?? 2.5;
|
|
8774
|
+
const role = ov.role ? ` data-role="${escapeHtml(ov.role)}"` : "";
|
|
8775
|
+
const normAnim = normalizeAnim(ov.animation);
|
|
8776
|
+
const anim = normAnim ? ` data-anim="${normAnim}"` : "";
|
|
8777
|
+
const detail = ov.animation_detail ? ` data-anim-detail="${escapeHtml(ov.animation_detail)}"` : "";
|
|
8778
|
+
return `<div class="ov ${positionClass(ov.position)}" data-start="${at}" data-dur="${dur}"${role}${anim}${detail}>${escapeHtml(ov.text.trim())}</div>`;
|
|
8779
|
+
}
|
|
8780
|
+
function floatingStub(fe, sceneStart) {
|
|
8781
|
+
const at = fe.appears_at_s ?? sceneStart;
|
|
8782
|
+
const dur = fe.duration_s ?? 2.5;
|
|
8783
|
+
const kind = commentSafe(fe.kind ?? "element");
|
|
8784
|
+
const label = commentSafe(fe.brand_name || fe.what_it_represents || fe.description || fe.kind || "element");
|
|
8785
|
+
const slug = (fe.kind ?? "element").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "element";
|
|
8786
|
+
return [
|
|
8787
|
+
`<!-- ${kind}: ${label} @ ${at}s for ${dur}s (${positionClass(fe.position)}). Drop an image in this dir and uncomment:`,
|
|
8788
|
+
`<img class="ov ${positionClass(fe.position)}" src="your-${slug}.png" data-start="${at}" data-dur="${dur}" alt="" /> -->`
|
|
8789
|
+
].join("\n");
|
|
8790
|
+
}
|
|
8791
|
+
function buildOverlayHtml(input) {
|
|
8792
|
+
const blueprint = VideoBlueprint.parse(input);
|
|
8793
|
+
const blocks = [
|
|
8794
|
+
[
|
|
8795
|
+
"<!-- \u2B07 OVERLAY LAYER \u2014 this is YOUR HTML to paint. The reference's overlays are",
|
|
8796
|
+
" seeded below as plain elements (text + position class + data-start/data-dur).",
|
|
8797
|
+
" Restyle freely in <style>, regroup, animate, swap a logo placeholder for a",
|
|
8798
|
+
" real <img> you drop in this dir. The runtime only shows/hides by timestamp;",
|
|
8799
|
+
" it makes NO styling decisions. Positions: edit the .pos-* classes or add your own. -->"
|
|
8800
|
+
].join("\n")
|
|
8801
|
+
];
|
|
8802
|
+
for (const scene of blueprint.scenes) {
|
|
8803
|
+
const sceneStart = scene.start_s ?? 0;
|
|
8804
|
+
const overlays = z3.array(Overlay).safeParse(scene.overlays ?? []);
|
|
8805
|
+
const floats = z3.array(FloatingElement).safeParse(scene.floating_elements ?? []);
|
|
8806
|
+
const parts = [
|
|
8807
|
+
...overlays.success ? overlays.data.map((ov) => overlayElement(ov, sceneStart)) : [],
|
|
8808
|
+
...floats.success ? floats.data.map((fe) => floatingStub(fe, sceneStart)) : []
|
|
8809
|
+
].filter(Boolean);
|
|
8810
|
+
if (parts.length > 0) blocks.push(parts.join("\n"));
|
|
8811
|
+
}
|
|
8812
|
+
return blocks.join("\n\n");
|
|
8813
|
+
}
|
|
8580
8814
|
function lastSceneEnd(blueprint) {
|
|
8581
8815
|
let end = 0;
|
|
8582
8816
|
for (const s of blueprint.scenes) end = Math.max(end, s.end_s ?? 0);
|
|
@@ -8608,7 +8842,8 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
8608
8842
|
params: { source: "path", path: todoPath2(elements[i], slot.label), expect: "image" }
|
|
8609
8843
|
});
|
|
8610
8844
|
});
|
|
8611
|
-
const
|
|
8845
|
+
const { tracks: voTracks, sceneTurns } = buildDialogue(blueprint, nodes);
|
|
8846
|
+
const clipRefs = buildSceneVisuals(blueprint, slots, opts, nodes, sceneTurns);
|
|
8612
8847
|
const concatInputs = {};
|
|
8613
8848
|
clipRefs.forEach((ref, i) => {
|
|
8614
8849
|
concatInputs[`c${i}`] = ref;
|
|
@@ -8628,12 +8863,12 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
8628
8863
|
id: "overlaid",
|
|
8629
8864
|
type: "hyperframe_render",
|
|
8630
8865
|
inputs: { background: videoRef },
|
|
8631
|
-
params: { composition: opts.overlayCompositionPath
|
|
8866
|
+
params: { composition: opts.overlayCompositionPath }
|
|
8632
8867
|
});
|
|
8633
8868
|
videoRef = "$ref:overlaid.video";
|
|
8634
8869
|
videoNode = "overlaid";
|
|
8635
8870
|
}
|
|
8636
|
-
const tracks =
|
|
8871
|
+
const tracks = [...voTracks, ...buildSfxMusic(blueprint, nodes)];
|
|
8637
8872
|
if (tracks.length > 0) {
|
|
8638
8873
|
const mixInputs = {};
|
|
8639
8874
|
for (const t of tracks) mixInputs[t.slot] = t.ref;
|
|
@@ -8666,8 +8901,18 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
8666
8901
|
"1:a:0",
|
|
8667
8902
|
"-c:v",
|
|
8668
8903
|
"copy",
|
|
8904
|
+
// The raw mix is a quiet mono track (tts + ducked bed), which reads as
|
|
8905
|
+
// "no sound" in casual players. Normalize integrated loudness to the
|
|
8906
|
+
// social/broadcast target (-14 LUFS, -1.5 dBTP) and upmix to stereo so
|
|
8907
|
+
// every rendered ad is loud and plays everywhere.
|
|
8908
|
+
"-af",
|
|
8909
|
+
"loudnorm=I=-14:TP=-1.5:LRA=11,aformat=channel_layouts=stereo",
|
|
8669
8910
|
"-c:a",
|
|
8670
8911
|
"aac",
|
|
8912
|
+
"-b:a",
|
|
8913
|
+
"192k",
|
|
8914
|
+
"-ar",
|
|
8915
|
+
"48000",
|
|
8671
8916
|
"-shortest",
|
|
8672
8917
|
"{{out.video}}"
|
|
8673
8918
|
],
|
|
@@ -8681,43 +8926,69 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
8681
8926
|
metadata: {
|
|
8682
8927
|
name: "video reproduction",
|
|
8683
8928
|
description: VIDEO_GUIDE,
|
|
8684
|
-
todo: buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts)
|
|
8929
|
+
todo: buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts),
|
|
8930
|
+
// The timing plan `baker canvas validate` checks before any billed render:
|
|
8931
|
+
// sequenced voiceover turns (no overlap), audio ≈ video length, and which
|
|
8932
|
+
// scenes must be lip-synced.
|
|
8933
|
+
video: buildVideoMeta(blueprint, sceneTurns)
|
|
8685
8934
|
},
|
|
8686
8935
|
nodes,
|
|
8687
8936
|
output: { node: videoNode, output: "video" }
|
|
8688
8937
|
};
|
|
8689
8938
|
}
|
|
8939
|
+
function buildVideoMeta(blueprint, sceneTurns) {
|
|
8940
|
+
const vo_segments = [];
|
|
8941
|
+
const talking_scenes = [];
|
|
8942
|
+
for (const [scene, turns] of [...sceneTurns.entries()].sort((a, b) => a[0] - b[0])) {
|
|
8943
|
+
for (const t of turns) {
|
|
8944
|
+
vo_segments.push({ slot: t.ttsId, start_s: t.start_s, end_s: t.end_s, scene, speaker: t.speaker });
|
|
8945
|
+
}
|
|
8946
|
+
if (turns.filter((t) => t.onCamera).length === 1) {
|
|
8947
|
+
talking_scenes.push({ scene, lipsync_node: `s${scene}_lipsync` });
|
|
8948
|
+
}
|
|
8949
|
+
}
|
|
8950
|
+
return {
|
|
8951
|
+
duration_s: blueprint.source?.duration_s ?? lastSceneEnd(blueprint),
|
|
8952
|
+
vo_segments,
|
|
8953
|
+
talking_scenes
|
|
8954
|
+
};
|
|
8955
|
+
}
|
|
8690
8956
|
var VIDEO_GUIDE = [
|
|
8691
|
-
"Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video.
|
|
8957
|
+
"Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video. Per scene: two AI-generated CLEAN-PLATE frames (no baked text) \u2192 a clip \u2192 trimmed to the real scene length so the picture stays on the audio timeline \u2192 optional lip-sync \u2192 concatenated. On-screen text is a separate HTML layer you paint; audio is sequenced voiceover + SFX + a ducked music bed, normalized stereo. It is a DRAFT: edit it, supply the real assets, then validate and run.",
|
|
8692
8958
|
"",
|
|
8693
8959
|
"WHAT TO DO NEXT:",
|
|
8694
8960
|
"1. Edit each frame's prompt IN PLACE. Every `s<i>_start` / `s<i>_end` node has its OWN self-contained `params.prompt` (the FRAME DESCRIPTION) \u2014 editing one changes only that frame. Rewrite the cast, product, claims, palette into the ad you want.",
|
|
8695
|
-
"2. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) is reused across every frame it appears in, so the same identity stays consistent.",
|
|
8696
|
-
"3. Confirm the `voice_select` casting (one per speaker).
|
|
8697
|
-
"4.
|
|
8698
|
-
"5.
|
|
8961
|
+
"2. Drop ONE real source image at each `el_*` ingest `[TODO]` path. Each recurring element (person/product/logo) is reused across every frame it appears in, so the same identity stays consistent. `baker canvas run` REFUSES to start until every `[TODO]` slot holds a real source \u2014 so this is mandatory, not optional.",
|
|
8962
|
+
"3. Confirm the `voice_select` casting (one per speaker). Voiceover is SEQUENCED: each contiguous same-speaker turn is its own `tts` placed at its real start, so dialogue alternates instead of stacking. Edit a turn's `params.text` (punctuation / ALL-CAPS / line breaks are read verbatim by eleven_v3 for emphasis and pauses) to shape delivery; re-author the words to be TRUE for your brand.",
|
|
8963
|
+
"4. Lip-sync: scenes with a single on-camera speaker route their clip through `video_lipsync` (~20 cr each) so the mouth matches the line. Two-speaker scenes are left un-synced \u2014 split them or pick a primary speaker if you want sync. Drop the node to skip.",
|
|
8964
|
+
"5. Overlays are REAL HTML you paint. Open `video-overlay-composition/index.html`: the reference's overlays are seeded inside `#overlay-root` as plain elements (text + a `.pos-*` class + `data-start`/`data-dur`). Restyle the CSS freely \u2014 build lower-thirds, a ticker, whatever the look needs \u2014 and replace a logo placeholder with a real `<img>` you drop in that dir. The runtime only shows/hides by timestamp; it makes no styling decisions. Drop `brand-bold.otf` / `brand-regular.otf` there for on-brand type.",
|
|
8965
|
+
"6. `baker canvas validate` (proves audio/lip-sync timing for free) then `baker canvas run` (generates many billed image/video/audio assets \u2014 not free).",
|
|
8699
8966
|
"",
|
|
8700
8967
|
"Tip: `prompt.json` is the deconstruction provenance + the demoted GLOBAL STYLE REFERENCE each frame reads for shared palette/cast cohesion. It is NOT the per-frame editing surface \u2014 the frame nodes are."
|
|
8701
8968
|
].join("\n");
|
|
8702
8969
|
function buildVideoTodo(report, overlayCount, floatingCount, opts) {
|
|
8703
8970
|
return {
|
|
8704
|
-
edit_frames_in_place: "Each s<i>_start / s<i>_end node has its own editable params.prompt (FRAME DESCRIPTION). Edit per frame; the blueprint is only a shared style reference.",
|
|
8971
|
+
edit_frames_in_place: "Each s<i>_start / s<i>_end node has its own editable params.prompt (FRAME DESCRIPTION). Edit per frame; the blueprint is only a shared style reference. Frames are CLEAN PLATES \u2014 they render no on-screen text; all text is the overlay HTML layer.",
|
|
8705
8972
|
frames_mode: opts.frames ?? "generate",
|
|
8973
|
+
assets_required: "MANDATORY: drop a real image at every el_* [TODO] ingest before running \u2014 `baker canvas run` refuses to start (and bills nothing) until each placeholder holds a real source.",
|
|
8706
8974
|
recurring_elements_to_supply: report.elements,
|
|
8975
|
+
text_strategy: "Decide per ad: text is either baked by the generated creative OR painted via the overlay HTML \u2014 not both. Default here is clean text-free frames + the HTML overlay layer (video-overlay-composition/index.html) as the single text source, which you fully control.",
|
|
8976
|
+
timeline: "Automatic: each clip is generated at >= its scene length then trimmed back to the real scene duration, so the concatenated picture stays on the same timeline as the absolute-timed audio (this is what makes the lips line up). You don't manage it.",
|
|
8707
8977
|
voices_to_confirm: report.dialogue.map((d) => ({
|
|
8708
8978
|
scene: d.scene,
|
|
8709
8979
|
speaker: d.speaker,
|
|
8710
8980
|
voice_description: d.voice_description,
|
|
8711
8981
|
line: d.line
|
|
8712
8982
|
})),
|
|
8713
|
-
voiceover_note: "
|
|
8983
|
+
voiceover_note: "Sequenced: one tts per contiguous same-speaker TURN, placed at its real start_s so turns alternate (no parallel monologues); same voice locked via voice_select.voice_id. Edit a turn's params.text (punctuation / ALL CAPS / line breaks read verbatim) to shape delivery.",
|
|
8984
|
+
lip_sync_note: "Scenes with a single on-camera speaker route their clip through video_lipsync (~20 cr each) so the mouth matches the line. Two-speaker scenes are left un-synced (one track can't drive two faces) \u2014 split or pick a primary. `baker canvas validate` checks every talking scene is synced.",
|
|
8714
8985
|
text_overlays: {
|
|
8715
8986
|
count: overlayCount,
|
|
8716
|
-
note: "
|
|
8987
|
+
note: "Seeded as editable HTML inside `#overlay-root` in video-overlay-composition/index.html (text + a .pos-* class + data-start/data-dur). PAINT it: restyle the CSS, build lower-thirds/tickers, drop brand-*.otf for on-brand type. The runtime only shows/hides by timestamp."
|
|
8717
8988
|
},
|
|
8718
8989
|
floating_elements: {
|
|
8719
8990
|
count: floatingCount,
|
|
8720
|
-
note: floatingCount > 0 ? "
|
|
8991
|
+
note: floatingCount > 0 ? "Seeded as labeled placeholders in index.html \u2014 replace each with a real <img> you drop into video-overlay-composition/. Recurring logos are also handled well as an el_* element baked into frames." : "none detected"
|
|
8721
8992
|
},
|
|
8722
8993
|
sound_effects: { count: report.sfx_count },
|
|
8723
8994
|
music: {
|
|
@@ -8769,8 +9040,24 @@ function videoReport(input, elementsInput) {
|
|
|
8769
9040
|
};
|
|
8770
9041
|
}
|
|
8771
9042
|
|
|
9043
|
+
// src/commands/canvas/composition-path.ts
|
|
9044
|
+
import { existsSync as existsSync3 } from "fs";
|
|
9045
|
+
import path4 from "path";
|
|
9046
|
+
function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
|
|
9047
|
+
const rel = path4.join("canvas", name);
|
|
9048
|
+
let dir = startDir;
|
|
9049
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
9050
|
+
const candidate = path4.join(dir, rel);
|
|
9051
|
+
if (exists(path4.join(candidate, "meta.json"))) return candidate;
|
|
9052
|
+
const parent = path4.dirname(dir);
|
|
9053
|
+
if (parent === dir) break;
|
|
9054
|
+
dir = parent;
|
|
9055
|
+
}
|
|
9056
|
+
return path4.resolve(startDir, "../../../", rel);
|
|
9057
|
+
}
|
|
9058
|
+
|
|
8772
9059
|
// src/commands/canvas/scaffold-video.ts
|
|
8773
|
-
var SHIPPED_COMPOSITION_DIR =
|
|
9060
|
+
var SHIPPED_COMPOSITION_DIR = resolveShippedCanvasDir("video-overlay-composition", import.meta.dirname);
|
|
8774
9061
|
function resolveModel2(kind, preferred) {
|
|
8775
9062
|
const ids = Object.keys(MODEL_REGISTRY[kind]);
|
|
8776
9063
|
return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
|
|
@@ -8891,13 +9178,19 @@ var scaffoldVideoCommand = defineCommand76({
|
|
|
8891
9178
|
"video-model": { type: "string", description: "Override the video_generate model id for clips" }
|
|
8892
9179
|
},
|
|
8893
9180
|
async run({ args }) {
|
|
8894
|
-
const videoPath =
|
|
8895
|
-
const base =
|
|
8896
|
-
const outPath = args.out ?
|
|
8897
|
-
const outDir =
|
|
8898
|
-
const blueprintPath =
|
|
9181
|
+
const videoPath = path5.resolve(String(args.file));
|
|
9182
|
+
const base = path5.basename(videoPath, path5.extname(videoPath));
|
|
9183
|
+
const outPath = args.out ? path5.resolve(String(args.out)) : path5.join(path5.dirname(videoPath), `${base}.video.canvas.json`);
|
|
9184
|
+
const outDir = path5.dirname(outPath);
|
|
9185
|
+
const blueprintPath = path5.join(outDir, "prompt.json");
|
|
8899
9186
|
const frames = args.frames === "reuse" ? "reuse" : "generate";
|
|
8900
9187
|
const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
|
|
9188
|
+
if (Number.isFinite(maxScenes)) {
|
|
9189
|
+
process.stderr.write(
|
|
9190
|
+
`\u26A0\uFE0F --max-scenes ${maxScenes} caps the deconstruct: any scenes beyond ${maxScenes} are MERGED away, reducing fidelity (fewer cuts, lost beats). Omit it to reproduce every scene.
|
|
9191
|
+
`
|
|
9192
|
+
);
|
|
9193
|
+
}
|
|
8901
9194
|
const { deconstructModel, selectModel, imageModel, videoModel } = resolveModels2(args);
|
|
8902
9195
|
const analysisCanvas = buildAnalysisCanvas(videoPath, deconstructModel, selectModel, {
|
|
8903
9196
|
maxScenes: Number.isFinite(maxScenes) ? maxScenes : void 0,
|
|
@@ -8909,8 +9202,19 @@ var scaffoldVideoCommand = defineCommand76({
|
|
|
8909
9202
|
const annotated = annotateBlueprintWithElements(blueprint, elements);
|
|
8910
9203
|
await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
8911
9204
|
`, "utf8");
|
|
8912
|
-
const compositionDest =
|
|
9205
|
+
const compositionDest = path5.join(outDir, "video-overlay-composition");
|
|
8913
9206
|
await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
|
|
9207
|
+
const indexPath = path5.join(compositionDest, "index.html");
|
|
9208
|
+
const overlayHtml = buildOverlayHtml(blueprint);
|
|
9209
|
+
const indexHtml = await readFile4(indexPath, "utf8");
|
|
9210
|
+
const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
|
|
9211
|
+
if (injected === indexHtml && overlayHtml.trim()) {
|
|
9212
|
+
fail2(
|
|
9213
|
+
"composition_marker_missing",
|
|
9214
|
+
`video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
|
|
9215
|
+
);
|
|
9216
|
+
}
|
|
9217
|
+
await writeFile2(indexPath, injected, "utf8");
|
|
8914
9218
|
const opts = {
|
|
8915
9219
|
imageModel,
|
|
8916
9220
|
videoModel,
|
|
@@ -8953,7 +9257,7 @@ var scaffoldVideoCommand = defineCommand76({
|
|
|
8953
9257
|
run_estimated_credits: validation.estimatedCredits
|
|
8954
9258
|
},
|
|
8955
9259
|
checklist: {
|
|
8956
|
-
edit_prompt: `Edit ${
|
|
9260
|
+
edit_prompt: `Edit ${path5.basename(blueprintPath)} \u2014 the blueprint deconstructed from your video; rewrite it into the ad you want (cast, palette, copy, claims). Every scene frame reads it via target_blueprint.`,
|
|
8957
9261
|
recurring_elements_to_supply: report.elements,
|
|
8958
9262
|
voices_to_confirm: report.dialogue.map((d) => ({
|
|
8959
9263
|
scene: d.scene,
|
|
@@ -8978,7 +9282,7 @@ var scaffoldVideoCommand = defineCommand76({
|
|
|
8978
9282
|
|
|
8979
9283
|
// src/commands/canvas/validate.ts
|
|
8980
9284
|
import { readFile as readFile5 } from "fs/promises";
|
|
8981
|
-
import
|
|
9285
|
+
import path6 from "path";
|
|
8982
9286
|
import { defineCommand as defineCommand77 } from "citty";
|
|
8983
9287
|
var validateCommand = defineCommand77({
|
|
8984
9288
|
meta: {
|
|
@@ -8987,7 +9291,7 @@ var validateCommand = defineCommand77({
|
|
|
8987
9291
|
},
|
|
8988
9292
|
args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
|
|
8989
9293
|
async run({ args }) {
|
|
8990
|
-
const filePath =
|
|
9294
|
+
const filePath = path6.resolve(String(args.file));
|
|
8991
9295
|
const raw = await readFile5(filePath, "utf8");
|
|
8992
9296
|
let parsed;
|
|
8993
9297
|
try {
|
|
@@ -9222,7 +9526,7 @@ Examples:
|
|
|
9222
9526
|
});
|
|
9223
9527
|
|
|
9224
9528
|
// src/commands/ga4/query.ts
|
|
9225
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
9529
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
9226
9530
|
import { resolve as resolve2 } from "path";
|
|
9227
9531
|
import { defineCommand as defineCommand81 } from "citty";
|
|
9228
9532
|
|
|
@@ -9296,7 +9600,7 @@ function writeRowsToFile2(filePath, rows, append) {
|
|
|
9296
9600
|
const fields = extractFields2(rows);
|
|
9297
9601
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
9298
9602
|
if (ext === "csv") {
|
|
9299
|
-
if (!append || !
|
|
9603
|
+
if (!append || !existsSync4(filePath)) {
|
|
9300
9604
|
writeFileSync4(filePath, `${toCsvRow(fields)}
|
|
9301
9605
|
`, "utf-8");
|
|
9302
9606
|
}
|
|
@@ -9307,12 +9611,12 @@ function writeRowsToFile2(filePath, rows, append) {
|
|
|
9307
9611
|
const lines = rows.map((row) => JSON.stringify(row));
|
|
9308
9612
|
const content = `${lines.join("\n")}
|
|
9309
9613
|
`;
|
|
9310
|
-
if (append &&
|
|
9614
|
+
if (append && existsSync4(filePath)) {
|
|
9311
9615
|
appendFileSync2(filePath, content, "utf-8");
|
|
9312
9616
|
} else {
|
|
9313
9617
|
writeFileSync4(filePath, content, "utf-8");
|
|
9314
9618
|
}
|
|
9315
|
-
} else if (append &&
|
|
9619
|
+
} else if (append && existsSync4(filePath)) {
|
|
9316
9620
|
const existing = JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
9317
9621
|
writeFileSync4(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
|
|
9318
9622
|
} else {
|
|
@@ -9453,7 +9757,7 @@ Examples:
|
|
|
9453
9757
|
import { defineCommand as defineCommand86 } from "citty";
|
|
9454
9758
|
|
|
9455
9759
|
// src/commands/gsc/query.ts
|
|
9456
|
-
import { appendFileSync as appendFileSync3, existsSync as
|
|
9760
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
9457
9761
|
import { resolve as resolve3 } from "path";
|
|
9458
9762
|
import { defineCommand as defineCommand83 } from "citty";
|
|
9459
9763
|
|
|
@@ -9582,7 +9886,7 @@ function writeRowsToFile3(filePath, rows, append) {
|
|
|
9582
9886
|
const fields = extractFields3(rows);
|
|
9583
9887
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
9584
9888
|
if (ext === "csv") {
|
|
9585
|
-
if (!append || !
|
|
9889
|
+
if (!append || !existsSync5(filePath)) {
|
|
9586
9890
|
writeFileSync5(filePath, `${toCsvRow(fields)}
|
|
9587
9891
|
`, "utf-8");
|
|
9588
9892
|
}
|
|
@@ -9592,12 +9896,12 @@ function writeRowsToFile3(filePath, rows, append) {
|
|
|
9592
9896
|
} else if (ext === "jsonl") {
|
|
9593
9897
|
const content = `${rows.map((row) => JSON.stringify(row)).join("\n")}
|
|
9594
9898
|
`;
|
|
9595
|
-
if (append &&
|
|
9899
|
+
if (append && existsSync5(filePath)) {
|
|
9596
9900
|
appendFileSync3(filePath, content, "utf-8");
|
|
9597
9901
|
} else {
|
|
9598
9902
|
writeFileSync5(filePath, content, "utf-8");
|
|
9599
9903
|
}
|
|
9600
|
-
} else if (append &&
|
|
9904
|
+
} else if (append && existsSync5(filePath)) {
|
|
9601
9905
|
const existing = JSON.parse(readFileSync7(filePath, "utf-8"));
|
|
9602
9906
|
writeFileSync5(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
|
|
9603
9907
|
} else {
|
|
@@ -9915,9 +10219,9 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
9915
10219
|
}
|
|
9916
10220
|
return readFile6(pathOrUrl);
|
|
9917
10221
|
}
|
|
9918
|
-
async function isDirectory(
|
|
10222
|
+
async function isDirectory(path7) {
|
|
9919
10223
|
try {
|
|
9920
|
-
const s = await stat2(
|
|
10224
|
+
const s = await stat2(path7);
|
|
9921
10225
|
return s.isDirectory();
|
|
9922
10226
|
} catch {
|
|
9923
10227
|
return false;
|