@koda-sl/baker-cli 0.70.0 → 0.74.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/README.md +65 -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 +753 -91
- 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,10 +9,10 @@ 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
|
-
import { defineCommand as
|
|
15
|
+
import { defineCommand as defineCommand141, runMain } from "citty";
|
|
16
16
|
|
|
17
17
|
// src/commands/actions/index.ts
|
|
18
18
|
import { defineCommand as defineCommand12 } 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}`,
|
|
@@ -314,6 +314,52 @@ function testimonialNormalizer(record, full) {
|
|
|
314
314
|
}
|
|
315
315
|
return compactTestimonial(record);
|
|
316
316
|
}
|
|
317
|
+
function round2(value) {
|
|
318
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
return Math.round(value * 100) / 100;
|
|
322
|
+
}
|
|
323
|
+
function asRecord(value) {
|
|
324
|
+
return value && typeof value === "object" ? value : {};
|
|
325
|
+
}
|
|
326
|
+
function compactWinningAd(record) {
|
|
327
|
+
const dna = asRecord(record.dna);
|
|
328
|
+
return {
|
|
329
|
+
advertiser: String(record.advertiser ?? ""),
|
|
330
|
+
platform: String(record.platform ?? ""),
|
|
331
|
+
format: String(record.format ?? ""),
|
|
332
|
+
relevance: round2(record.relevance),
|
|
333
|
+
winner_score: round2(record.winner_score),
|
|
334
|
+
summary: String(dna.creative_concept ?? ""),
|
|
335
|
+
advertiser_id: String(record.advertiser_id ?? ""),
|
|
336
|
+
ad_id: String(record.ad_id ?? ""),
|
|
337
|
+
media_url: String(record.media_url ?? "")
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function fullWinningAd(record) {
|
|
341
|
+
const compact = compactWinningAd(record);
|
|
342
|
+
const dna = asRecord(record.dna);
|
|
343
|
+
return {
|
|
344
|
+
...compact,
|
|
345
|
+
winner_category: String(record.winner_category ?? ""),
|
|
346
|
+
media_kind: record.media_kind ?? null,
|
|
347
|
+
days_active: record.days_active ?? null,
|
|
348
|
+
reach: record.reach ?? null,
|
|
349
|
+
active: typeof record.active === "boolean" ? record.active : null,
|
|
350
|
+
angle: dna.angle ?? null,
|
|
351
|
+
awareness_stage: dna.awareness_stage ?? null,
|
|
352
|
+
target_persona: dna.target_persona ?? null,
|
|
353
|
+
hook_archetype: dna.hook_archetype ?? null,
|
|
354
|
+
industry: dna.industry ?? null
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function winningAdNormalizer(record, full) {
|
|
358
|
+
if (full) {
|
|
359
|
+
return fullWinningAd(record);
|
|
360
|
+
}
|
|
361
|
+
return compactWinningAd(record);
|
|
362
|
+
}
|
|
317
363
|
function applyFieldMask(data, fields) {
|
|
318
364
|
const result = {};
|
|
319
365
|
for (const field of fields) {
|
|
@@ -918,31 +964,31 @@ function cachePath(category, key) {
|
|
|
918
964
|
return join(dir, `${hashKey(key)}.json`);
|
|
919
965
|
}
|
|
920
966
|
function cacheGet(category, key) {
|
|
921
|
-
const
|
|
922
|
-
if (!existsSync(
|
|
967
|
+
const path7 = cachePath(category, key);
|
|
968
|
+
if (!existsSync(path7)) {
|
|
923
969
|
return null;
|
|
924
970
|
}
|
|
925
971
|
try {
|
|
926
|
-
const raw = readFileSync(
|
|
972
|
+
const raw = readFileSync(path7, "utf-8");
|
|
927
973
|
const entry = JSON.parse(raw);
|
|
928
974
|
if (entry.expiresAt < Date.now()) {
|
|
929
|
-
rmSync(
|
|
975
|
+
rmSync(path7, { force: true });
|
|
930
976
|
return null;
|
|
931
977
|
}
|
|
932
978
|
return entry;
|
|
933
979
|
} catch {
|
|
934
|
-
rmSync(
|
|
980
|
+
rmSync(path7, { force: true });
|
|
935
981
|
return null;
|
|
936
982
|
}
|
|
937
983
|
}
|
|
938
984
|
function cacheSet(category, key, data, ttlMs, fields) {
|
|
939
|
-
const
|
|
985
|
+
const path7 = cachePath(category, key);
|
|
940
986
|
const entry = {
|
|
941
987
|
expiresAt: Date.now() + ttlMs,
|
|
942
988
|
data,
|
|
943
989
|
fields
|
|
944
990
|
};
|
|
945
|
-
writeFileSync(
|
|
991
|
+
writeFileSync(path7, JSON.stringify(entry), "utf-8");
|
|
946
992
|
}
|
|
947
993
|
var HOUR = 60 * 60 * 1e3;
|
|
948
994
|
var MINUTE = 60 * 1e3;
|
|
@@ -5633,10 +5679,10 @@ var IDENTITY_FIELDS_BY_LEVEL = {
|
|
|
5633
5679
|
};
|
|
5634
5680
|
function composeFields2(intent, level) {
|
|
5635
5681
|
const intentFields = INSIGHTS_INTENTS[intent].fields;
|
|
5636
|
-
const
|
|
5682
|
+
const identity2 = IDENTITY_FIELDS_BY_LEVEL[level];
|
|
5637
5683
|
const seen = /* @__PURE__ */ new Set();
|
|
5638
5684
|
const out = [];
|
|
5639
|
-
for (const f of [...
|
|
5685
|
+
for (const f of [...identity2, ...intentFields]) {
|
|
5640
5686
|
if (!seen.has(f)) {
|
|
5641
5687
|
seen.add(f);
|
|
5642
5688
|
out.push(f);
|
|
@@ -7714,6 +7760,24 @@ async function probeDuration(filePath) {
|
|
|
7714
7760
|
import { readFile as readFile2 } from "fs/promises";
|
|
7715
7761
|
import path2 from "path";
|
|
7716
7762
|
import { defineCommand as defineCommand74 } from "citty";
|
|
7763
|
+
|
|
7764
|
+
// src/commands/canvas/placeholders.ts
|
|
7765
|
+
function unsuppliedPlaceholderAssets(canvas) {
|
|
7766
|
+
const nodes = canvas?.nodes;
|
|
7767
|
+
if (!Array.isArray(nodes)) return [];
|
|
7768
|
+
const out = [];
|
|
7769
|
+
for (const n of nodes) {
|
|
7770
|
+
const node = n;
|
|
7771
|
+
if (node?.type !== "ingest") continue;
|
|
7772
|
+
const params = node.params;
|
|
7773
|
+
if (params?.source === "path" && typeof params.path === "string" && params.path.includes("[TODO")) {
|
|
7774
|
+
out.push({ node: String(node.id ?? "?"), placeholder: params.path });
|
|
7775
|
+
}
|
|
7776
|
+
}
|
|
7777
|
+
return out;
|
|
7778
|
+
}
|
|
7779
|
+
|
|
7780
|
+
// src/commands/canvas/run.ts
|
|
7717
7781
|
var runCommand = defineCommand74({
|
|
7718
7782
|
meta: { name: "run", description: "Validate and execute a canvas JSON file." },
|
|
7719
7783
|
args: {
|
|
@@ -7735,6 +7799,25 @@ var runCommand = defineCommand74({
|
|
|
7735
7799
|
`);
|
|
7736
7800
|
process.exit(2);
|
|
7737
7801
|
}
|
|
7802
|
+
const pending = unsuppliedPlaceholderAssets(parsed);
|
|
7803
|
+
if (pending.length > 0) {
|
|
7804
|
+
process.stderr.write(
|
|
7805
|
+
`${JSON.stringify(
|
|
7806
|
+
{
|
|
7807
|
+
ok: false,
|
|
7808
|
+
error: {
|
|
7809
|
+
code: "unsupplied_assets",
|
|
7810
|
+
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.",
|
|
7811
|
+
assets: pending
|
|
7812
|
+
}
|
|
7813
|
+
},
|
|
7814
|
+
null,
|
|
7815
|
+
2
|
|
7816
|
+
)}
|
|
7817
|
+
`
|
|
7818
|
+
);
|
|
7819
|
+
process.exit(2);
|
|
7820
|
+
}
|
|
7738
7821
|
const engine = createEngineFromEnv({
|
|
7739
7822
|
cacheDir: args["cache-dir"] ? String(args["cache-dir"]) : void 0,
|
|
7740
7823
|
outputsDir: args["outputs-dir"] ? String(args["outputs-dir"]) : void 0,
|
|
@@ -8192,7 +8275,7 @@ var scaffoldStaticAdCommand = defineCommand75({
|
|
|
8192
8275
|
|
|
8193
8276
|
// src/commands/canvas/scaffold-video.ts
|
|
8194
8277
|
import { cp, mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
8195
|
-
import
|
|
8278
|
+
import path5 from "path";
|
|
8196
8279
|
import { defineCommand as defineCommand76 } from "citty";
|
|
8197
8280
|
|
|
8198
8281
|
// src/engine/scaffold/video.ts
|
|
@@ -8200,7 +8283,19 @@ import { z as z3 } from "zod";
|
|
|
8200
8283
|
var FIXED_TTS_MODEL = "elevenlabs/eleven_v3";
|
|
8201
8284
|
var FIXED_SFX_MODEL = "elevenlabs/eleven_text_to_sound_v2";
|
|
8202
8285
|
var FIXED_MUSIC_MODEL = "elevenlabs/music-v1";
|
|
8286
|
+
var FIXED_LIPSYNC_MODEL = "fal/veed-lipsync";
|
|
8203
8287
|
var MUSIC_BED_GAIN_DB = -12;
|
|
8288
|
+
var NARRATOR_SPEAKERS = /* @__PURE__ */ new Set([
|
|
8289
|
+
"voiceover",
|
|
8290
|
+
"voice_over",
|
|
8291
|
+
"narrator",
|
|
8292
|
+
"narration",
|
|
8293
|
+
"vo",
|
|
8294
|
+
"announcer",
|
|
8295
|
+
"off_screen",
|
|
8296
|
+
"offscreen",
|
|
8297
|
+
"off-screen"
|
|
8298
|
+
]);
|
|
8204
8299
|
var SHARED_ASPECT_RATIOS = /* @__PURE__ */ new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "21:9"]);
|
|
8205
8300
|
var EDGES = ["start", "end"];
|
|
8206
8301
|
function snapToSeedance(durationS) {
|
|
@@ -8216,11 +8311,39 @@ function snapToSeedance(durationS) {
|
|
|
8216
8311
|
}
|
|
8217
8312
|
return best;
|
|
8218
8313
|
}
|
|
8314
|
+
function ceilToSeedance(durationS) {
|
|
8315
|
+
const max = SEEDANCE_DURATIONS[SEEDANCE_DURATIONS.length - 1];
|
|
8316
|
+
if (!Number.isFinite(durationS) || durationS <= 0) return SEEDANCE_DURATIONS[0];
|
|
8317
|
+
for (const d of SEEDANCE_DURATIONS) if (d >= durationS) return d;
|
|
8318
|
+
return max;
|
|
8319
|
+
}
|
|
8320
|
+
function sceneDurationS(scene) {
|
|
8321
|
+
const raw = scene.duration_s ?? (scene.end_s != null && scene.start_s != null ? scene.end_s - scene.start_s : 5);
|
|
8322
|
+
const max = SEEDANCE_DURATIONS[SEEDANCE_DURATIONS.length - 1];
|
|
8323
|
+
return Math.min(Math.max(raw, 0.5), max);
|
|
8324
|
+
}
|
|
8325
|
+
function trimArgs(durationS) {
|
|
8326
|
+
return [
|
|
8327
|
+
"-i",
|
|
8328
|
+
"{{in.clip}}",
|
|
8329
|
+
"-t",
|
|
8330
|
+
durationS.toFixed(3),
|
|
8331
|
+
"-an",
|
|
8332
|
+
"-c:v",
|
|
8333
|
+
"libx264",
|
|
8334
|
+
"-pix_fmt",
|
|
8335
|
+
"yuv420p",
|
|
8336
|
+
"{{out.video}}"
|
|
8337
|
+
];
|
|
8338
|
+
}
|
|
8219
8339
|
var FrameAsset = z3.object({ url: z3.string().optional() }).loose().optional();
|
|
8220
8340
|
var DialogueLine = z3.object({
|
|
8221
8341
|
speaker: z3.string().optional(),
|
|
8222
8342
|
line: z3.string().optional(),
|
|
8343
|
+
// Absolute seconds on the source timeline (the deconstruct emits both).
|
|
8223
8344
|
start_s: z3.number().optional(),
|
|
8345
|
+
end_s: z3.number().optional(),
|
|
8346
|
+
delivery: z3.string().optional(),
|
|
8224
8347
|
voice_description: z3.string().optional()
|
|
8225
8348
|
}).loose();
|
|
8226
8349
|
var Sfx = z3.object({
|
|
@@ -8260,7 +8383,12 @@ var VideoBlueprint = z3.object({
|
|
|
8260
8383
|
identified_track: z3.object({ title: z3.string().optional(), artist: z3.string().optional() }).loose().nullish()
|
|
8261
8384
|
}).loose().optional(),
|
|
8262
8385
|
cast: z3.array(z3.object({ id: z3.string().optional(), description: z3.string().optional() }).loose()).optional(),
|
|
8263
|
-
voiceover: z3.object({
|
|
8386
|
+
voiceover: z3.object({
|
|
8387
|
+
// on_camera | mixed → mouths are on screen (lip-sync candidates);
|
|
8388
|
+
// voiceover | none → narration over the picture (no lip-sync).
|
|
8389
|
+
mode: z3.string().optional(),
|
|
8390
|
+
voice_description: z3.string().optional()
|
|
8391
|
+
}).loose().optional()
|
|
8264
8392
|
}).loose().optional(),
|
|
8265
8393
|
scenes: z3.array(Scene).min(1)
|
|
8266
8394
|
}).loose();
|
|
@@ -8392,7 +8520,17 @@ function buildFramePrompt(edge, sceneIndex, framePrompt, present, hasAnchor) {
|
|
|
8392
8520
|
].join("\n");
|
|
8393
8521
|
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
8522
|
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
|
|
8523
|
+
`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.`,
|
|
8524
|
+
"",
|
|
8525
|
+
"CRITICAL \u2014 RENDER A CLEAN PLATE WITH ZERO TEXT OR GRAPHICS:",
|
|
8526
|
+
"This frame is a background plate. ALL words, captions, headlines, lower-third bars,",
|
|
8527
|
+
"news tickers/crawls, chyrons, on-screen logos/wordmarks, station bugs, watermarks,",
|
|
8528
|
+
"subtitles, UI and numbers are added afterwards as a separate HTML layer. Render NONE",
|
|
8529
|
+
"of them \u2014 no legible text anywhere in the image, not even in the background, on the",
|
|
8530
|
+
"news desk, on screens, or as part of a 'broadcast look'. If a reference image (a logo,",
|
|
8531
|
+
"a desk, a studio) contains any text or graphics, DO NOT reproduce that text \u2014 render",
|
|
8532
|
+
"the subject/scene only, with blank surfaces where text would be. Imperfect/garbled",
|
|
8533
|
+
"letterforms are the worst outcome; leave those areas clean.",
|
|
8396
8534
|
"",
|
|
8397
8535
|
"REFERENCE IMAGES (in the order provided):",
|
|
8398
8536
|
legend,
|
|
@@ -8400,7 +8538,7 @@ function buildFramePrompt(edge, sceneIndex, framePrompt, present, hasAnchor) {
|
|
|
8400
8538
|
"FRAME DESCRIPTION (this frame's editable prompt):",
|
|
8401
8539
|
description,
|
|
8402
8540
|
"",
|
|
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.",
|
|
8541
|
+
"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
8542
|
"",
|
|
8405
8543
|
"GLOBAL STYLE REFERENCE (shared across frames; not this frame's content):",
|
|
8406
8544
|
"{{target_blueprint}}"
|
|
@@ -8451,7 +8589,7 @@ function buildSeedancePrompt(scene, sceneIndex, present) {
|
|
|
8451
8589
|
if (transcript) parts.push(`Transcript: ${transcript}`);
|
|
8452
8590
|
return parts.join("\n");
|
|
8453
8591
|
}
|
|
8454
|
-
function buildSceneVisuals(blueprint, slots, opts, nodes) {
|
|
8592
|
+
function buildSceneVisuals(blueprint, slots, opts, nodes, sceneTurns) {
|
|
8455
8593
|
const ar = aspectRatioParam(blueprint);
|
|
8456
8594
|
const reuse = opts.frames === "reuse";
|
|
8457
8595
|
const clipRefs = [];
|
|
@@ -8473,10 +8611,11 @@ function buildSceneVisuals(blueprint, slots, opts, nodes) {
|
|
|
8473
8611
|
ctx,
|
|
8474
8612
|
nodes
|
|
8475
8613
|
);
|
|
8614
|
+
const dur = sceneDurationS(scene);
|
|
8476
8615
|
const clipParams = {
|
|
8477
8616
|
model: opts.videoModel,
|
|
8478
8617
|
prompt: buildSeedancePrompt(scene, i, slotsForScene(slots, i)),
|
|
8479
|
-
duration:
|
|
8618
|
+
duration: ceilToSeedance(dur)
|
|
8480
8619
|
};
|
|
8481
8620
|
if (ar) clipParams.aspect_ratio = ar;
|
|
8482
8621
|
nodes.push({
|
|
@@ -8485,7 +8624,29 @@ function buildSceneVisuals(blueprint, slots, opts, nodes) {
|
|
|
8485
8624
|
inputs: { first_frame: firstFrame, last_frame: lastFrame },
|
|
8486
8625
|
params: clipParams
|
|
8487
8626
|
});
|
|
8488
|
-
|
|
8627
|
+
let base = `$ref:s${i}_clip.video`;
|
|
8628
|
+
const onCam = (sceneTurns.get(i) ?? []).filter((t) => t.onCamera);
|
|
8629
|
+
const solo = onCam.length === 1 ? onCam[0] : void 0;
|
|
8630
|
+
if (solo) {
|
|
8631
|
+
nodes.push({
|
|
8632
|
+
id: `s${i}_lipsync`,
|
|
8633
|
+
type: "video_lipsync",
|
|
8634
|
+
inputs: { video: base, audio: solo.audioRef },
|
|
8635
|
+
params: { model: FIXED_LIPSYNC_MODEL }
|
|
8636
|
+
});
|
|
8637
|
+
base = `$ref:s${i}_lipsync.video`;
|
|
8638
|
+
}
|
|
8639
|
+
if (ceilToSeedance(dur) === dur) {
|
|
8640
|
+
clipRefs.push(base);
|
|
8641
|
+
} else {
|
|
8642
|
+
nodes.push({
|
|
8643
|
+
id: `s${i}_trim`,
|
|
8644
|
+
type: "ffmpeg",
|
|
8645
|
+
inputs: { clip: base },
|
|
8646
|
+
params: { args: trimArgs(dur), outputs: { video: { kind: "video", ext: "mp4" } } }
|
|
8647
|
+
});
|
|
8648
|
+
clipRefs.push(`$ref:s${i}_trim.video`);
|
|
8649
|
+
}
|
|
8489
8650
|
});
|
|
8490
8651
|
return clipRefs;
|
|
8491
8652
|
}
|
|
@@ -8498,8 +8659,21 @@ function musicBedPrompt(blueprint, musicPrompt) {
|
|
|
8498
8659
|
|
|
8499
8660
|
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
8661
|
}
|
|
8501
|
-
function
|
|
8662
|
+
function onCameraDialogue(blueprint) {
|
|
8663
|
+
const mode = blueprint.global?.voiceover?.mode;
|
|
8664
|
+
return mode !== "voiceover" && mode !== "none";
|
|
8665
|
+
}
|
|
8666
|
+
var castIdSet = (blueprint) => new Set((blueprint.global?.cast ?? []).map((c) => c.id).filter((id) => Boolean(id)));
|
|
8667
|
+
function isOnCameraSpeaker(speaker, casts, cameraOn) {
|
|
8668
|
+
if (!cameraOn) return false;
|
|
8669
|
+
if (NARRATOR_SPEAKERS.has(speaker.toLowerCase())) return false;
|
|
8670
|
+
return casts.has(speaker);
|
|
8671
|
+
}
|
|
8672
|
+
function buildDialogue(blueprint, nodes) {
|
|
8502
8673
|
const tracks = [];
|
|
8674
|
+
const sceneTurns = /* @__PURE__ */ new Map();
|
|
8675
|
+
const casts = castIdSet(blueprint);
|
|
8676
|
+
const cameraOn = onCameraDialogue(blueprint);
|
|
8503
8677
|
const voiceNodeBySpeaker = /* @__PURE__ */ new Map();
|
|
8504
8678
|
const speakerDescription = (speaker) => {
|
|
8505
8679
|
for (const scene of blueprint.scenes) {
|
|
@@ -8518,41 +8692,62 @@ function buildAudio(blueprint, nodes) {
|
|
|
8518
8692
|
voiceNodeBySpeaker.set(speaker, id);
|
|
8519
8693
|
return id;
|
|
8520
8694
|
};
|
|
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
8695
|
const usedVoIds = /* @__PURE__ */ new Set();
|
|
8539
|
-
|
|
8540
|
-
const
|
|
8541
|
-
if (
|
|
8542
|
-
const
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8548
|
-
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8696
|
+
blueprint.scenes.forEach((scene, sceneIndex) => {
|
|
8697
|
+
const lines = (scene.dialogue ?? []).filter((l) => Boolean(l.line?.trim())).slice().sort((a, b) => (a.start_s ?? 0) - (b.start_s ?? 0));
|
|
8698
|
+
if (lines.length === 0) return;
|
|
8699
|
+
const groups = [];
|
|
8700
|
+
for (const line of lines) {
|
|
8701
|
+
const speaker = line.speaker ?? "voiceover";
|
|
8702
|
+
const last = groups[groups.length - 1];
|
|
8703
|
+
if (last && last.speaker === speaker) last.lines.push(line);
|
|
8704
|
+
else groups.push({ speaker, lines: [line] });
|
|
8705
|
+
}
|
|
8706
|
+
const list = [];
|
|
8707
|
+
groups.forEach((group, gi) => {
|
|
8708
|
+
const first = group.lines[0];
|
|
8709
|
+
const last = group.lines[group.lines.length - 1];
|
|
8710
|
+
if (!first || !last) return;
|
|
8711
|
+
const start = first.start_s ?? scene.start_s ?? 0;
|
|
8712
|
+
const end = last.end_s ?? last.start_s ?? scene.end_s ?? start;
|
|
8713
|
+
const voiceNode = ensureVoiceNode(group.speaker);
|
|
8714
|
+
let id = sanitizeId2(`vo_s${sceneIndex}_${group.speaker}`, `vo_${sceneIndex}_${gi}`);
|
|
8715
|
+
if (usedVoIds.has(id)) {
|
|
8716
|
+
let n = 2;
|
|
8717
|
+
while (usedVoIds.has(`${id}_${n}`)) n++;
|
|
8718
|
+
id = `${id}_${n}`;
|
|
8719
|
+
}
|
|
8720
|
+
usedVoIds.add(id);
|
|
8721
|
+
nodes.push({
|
|
8722
|
+
id,
|
|
8723
|
+
type: "tts",
|
|
8724
|
+
inputs: { voice_ref: `$ref:${voiceNode}.voice_id` },
|
|
8725
|
+
// Lines join with a space; each keeps its terminal punctuation so eleven_v3
|
|
8726
|
+
// reads the sentence boundaries (and their pauses) within the one turn.
|
|
8727
|
+
params: {
|
|
8728
|
+
model: FIXED_TTS_MODEL,
|
|
8729
|
+
text: group.lines.map((l) => l.line.trim()).join(" "),
|
|
8730
|
+
voice: "{{voice_ref}}"
|
|
8731
|
+
}
|
|
8732
|
+
});
|
|
8733
|
+
const turn = {
|
|
8734
|
+
sceneIndex,
|
|
8735
|
+
speaker: group.speaker,
|
|
8736
|
+
onCamera: isOnCameraSpeaker(group.speaker, casts, cameraOn),
|
|
8737
|
+
start_s: start,
|
|
8738
|
+
end_s: end,
|
|
8739
|
+
ttsId: id,
|
|
8740
|
+
audioRef: `$ref:${id}.audio`
|
|
8741
|
+
};
|
|
8742
|
+
list.push(turn);
|
|
8743
|
+
tracks.push({ slot: id, ref: turn.audioRef, start_s: start, end_s: end, kind: "vo" });
|
|
8553
8744
|
});
|
|
8554
|
-
|
|
8745
|
+
sceneTurns.set(sceneIndex, list);
|
|
8555
8746
|
});
|
|
8747
|
+
return { tracks, sceneTurns };
|
|
8748
|
+
}
|
|
8749
|
+
function buildSfxMusic(blueprint, nodes) {
|
|
8750
|
+
const tracks = [];
|
|
8556
8751
|
blueprint.scenes.forEach((scene, i) => {
|
|
8557
8752
|
(scene.sfx ?? []).forEach((sfx, k) => {
|
|
8558
8753
|
const text = sfx.sound_effect_prompt ?? sfx.description;
|
|
@@ -8561,7 +8756,12 @@ function buildAudio(blueprint, nodes) {
|
|
|
8561
8756
|
const params = { model: FIXED_SFX_MODEL, text };
|
|
8562
8757
|
if (typeof sfx.duration_s === "number") params.duration_seconds = Math.min(Math.max(sfx.duration_s, 0.5), 30);
|
|
8563
8758
|
nodes.push({ id, type: "sound_effect", params });
|
|
8564
|
-
tracks.push({
|
|
8759
|
+
tracks.push({
|
|
8760
|
+
slot: `sfx_s${i}_${k}`,
|
|
8761
|
+
ref: `$ref:${id}.audio`,
|
|
8762
|
+
start_s: sfx.at_s ?? scene.start_s ?? 0,
|
|
8763
|
+
kind: "sfx"
|
|
8764
|
+
});
|
|
8565
8765
|
});
|
|
8566
8766
|
});
|
|
8567
8767
|
const musicPrompt = blueprint.global?.music?.music_prompt;
|
|
@@ -8573,10 +8773,90 @@ function buildAudio(blueprint, nodes) {
|
|
|
8573
8773
|
type: "music",
|
|
8574
8774
|
params: { model: FIXED_MUSIC_MODEL, prompt: musicBedPrompt(blueprint, musicPrompt), music_length_ms: musicMs }
|
|
8575
8775
|
});
|
|
8576
|
-
tracks.
|
|
8776
|
+
tracks.push({ slot: "music", ref: "$ref:music_bed.audio", start_s: 0, gain_db: MUSIC_BED_GAIN_DB, kind: "music" });
|
|
8577
8777
|
}
|
|
8578
8778
|
return tracks;
|
|
8579
8779
|
}
|
|
8780
|
+
var OverlayStyle = z3.object({ color_hex: z3.string().optional(), background: z3.string().optional(), size: z3.string().optional() }).loose();
|
|
8781
|
+
var Overlay = z3.object({
|
|
8782
|
+
text: z3.string().optional(),
|
|
8783
|
+
appears_at_s: z3.number().optional(),
|
|
8784
|
+
duration_s: z3.number().optional(),
|
|
8785
|
+
position: z3.string().optional(),
|
|
8786
|
+
role: z3.string().optional(),
|
|
8787
|
+
animation: z3.string().optional(),
|
|
8788
|
+
animation_detail: z3.string().optional(),
|
|
8789
|
+
style: OverlayStyle.optional()
|
|
8790
|
+
}).loose();
|
|
8791
|
+
var FloatingElement = z3.object({
|
|
8792
|
+
kind: z3.string().optional(),
|
|
8793
|
+
description: z3.string().optional(),
|
|
8794
|
+
brand_name: z3.string().nullish(),
|
|
8795
|
+
what_it_represents: z3.string().optional(),
|
|
8796
|
+
appears_at_s: z3.number().optional(),
|
|
8797
|
+
duration_s: z3.number().optional(),
|
|
8798
|
+
position: z3.string().optional()
|
|
8799
|
+
}).loose();
|
|
8800
|
+
function escapeHtml(s) {
|
|
8801
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
8802
|
+
}
|
|
8803
|
+
function commentSafe(s) {
|
|
8804
|
+
return escapeHtml(s).replace(/-{2,}/g, "\u2013");
|
|
8805
|
+
}
|
|
8806
|
+
var SUPPORTED_ANIMS = /* @__PURE__ */ new Set(["fade", "slide_up", "slide_down", "pop"]);
|
|
8807
|
+
function normalizeAnim(animation) {
|
|
8808
|
+
if (!animation || animation === "none") return void 0;
|
|
8809
|
+
const mapped = animation === "slide" ? "slide_up" : animation;
|
|
8810
|
+
return SUPPORTED_ANIMS.has(mapped) ? mapped : void 0;
|
|
8811
|
+
}
|
|
8812
|
+
function positionClass(position) {
|
|
8813
|
+
const p = (position ?? "bottom_center").toLowerCase().replace(/[^a-z]+/g, "-");
|
|
8814
|
+
return `pos-${p}`;
|
|
8815
|
+
}
|
|
8816
|
+
function overlayElement(ov, sceneStart) {
|
|
8817
|
+
if (!ov.text?.trim()) return "";
|
|
8818
|
+
const at = ov.appears_at_s ?? sceneStart;
|
|
8819
|
+
const dur = ov.duration_s ?? 2.5;
|
|
8820
|
+
const role = ov.role ? ` data-role="${escapeHtml(ov.role)}"` : "";
|
|
8821
|
+
const normAnim = normalizeAnim(ov.animation);
|
|
8822
|
+
const anim = normAnim ? ` data-anim="${normAnim}"` : "";
|
|
8823
|
+
const detail = ov.animation_detail ? ` data-anim-detail="${escapeHtml(ov.animation_detail)}"` : "";
|
|
8824
|
+
return `<div class="ov ${positionClass(ov.position)}" data-start="${at}" data-dur="${dur}"${role}${anim}${detail}>${escapeHtml(ov.text.trim())}</div>`;
|
|
8825
|
+
}
|
|
8826
|
+
function floatingStub(fe, sceneStart) {
|
|
8827
|
+
const at = fe.appears_at_s ?? sceneStart;
|
|
8828
|
+
const dur = fe.duration_s ?? 2.5;
|
|
8829
|
+
const kind = commentSafe(fe.kind ?? "element");
|
|
8830
|
+
const label = commentSafe(fe.brand_name || fe.what_it_represents || fe.description || fe.kind || "element");
|
|
8831
|
+
const slug = (fe.kind ?? "element").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "element";
|
|
8832
|
+
return [
|
|
8833
|
+
`<!-- ${kind}: ${label} @ ${at}s for ${dur}s (${positionClass(fe.position)}). Drop an image in this dir and uncomment:`,
|
|
8834
|
+
`<img class="ov ${positionClass(fe.position)}" src="your-${slug}.png" data-start="${at}" data-dur="${dur}" alt="" /> -->`
|
|
8835
|
+
].join("\n");
|
|
8836
|
+
}
|
|
8837
|
+
function buildOverlayHtml(input) {
|
|
8838
|
+
const blueprint = VideoBlueprint.parse(input);
|
|
8839
|
+
const blocks = [
|
|
8840
|
+
[
|
|
8841
|
+
"<!-- \u2B07 OVERLAY LAYER \u2014 this is YOUR HTML to paint. The reference's overlays are",
|
|
8842
|
+
" seeded below as plain elements (text + position class + data-start/data-dur).",
|
|
8843
|
+
" Restyle freely in <style>, regroup, animate, swap a logo placeholder for a",
|
|
8844
|
+
" real <img> you drop in this dir. The runtime only shows/hides by timestamp;",
|
|
8845
|
+
" it makes NO styling decisions. Positions: edit the .pos-* classes or add your own. -->"
|
|
8846
|
+
].join("\n")
|
|
8847
|
+
];
|
|
8848
|
+
for (const scene of blueprint.scenes) {
|
|
8849
|
+
const sceneStart = scene.start_s ?? 0;
|
|
8850
|
+
const overlays = z3.array(Overlay).safeParse(scene.overlays ?? []);
|
|
8851
|
+
const floats = z3.array(FloatingElement).safeParse(scene.floating_elements ?? []);
|
|
8852
|
+
const parts = [
|
|
8853
|
+
...overlays.success ? overlays.data.map((ov) => overlayElement(ov, sceneStart)) : [],
|
|
8854
|
+
...floats.success ? floats.data.map((fe) => floatingStub(fe, sceneStart)) : []
|
|
8855
|
+
].filter(Boolean);
|
|
8856
|
+
if (parts.length > 0) blocks.push(parts.join("\n"));
|
|
8857
|
+
}
|
|
8858
|
+
return blocks.join("\n\n");
|
|
8859
|
+
}
|
|
8580
8860
|
function lastSceneEnd(blueprint) {
|
|
8581
8861
|
let end = 0;
|
|
8582
8862
|
for (const s of blueprint.scenes) end = Math.max(end, s.end_s ?? 0);
|
|
@@ -8608,7 +8888,8 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
8608
8888
|
params: { source: "path", path: todoPath2(elements[i], slot.label), expect: "image" }
|
|
8609
8889
|
});
|
|
8610
8890
|
});
|
|
8611
|
-
const
|
|
8891
|
+
const { tracks: voTracks, sceneTurns } = buildDialogue(blueprint, nodes);
|
|
8892
|
+
const clipRefs = buildSceneVisuals(blueprint, slots, opts, nodes, sceneTurns);
|
|
8612
8893
|
const concatInputs = {};
|
|
8613
8894
|
clipRefs.forEach((ref, i) => {
|
|
8614
8895
|
concatInputs[`c${i}`] = ref;
|
|
@@ -8628,12 +8909,12 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
8628
8909
|
id: "overlaid",
|
|
8629
8910
|
type: "hyperframe_render",
|
|
8630
8911
|
inputs: { background: videoRef },
|
|
8631
|
-
params: { composition: opts.overlayCompositionPath
|
|
8912
|
+
params: { composition: opts.overlayCompositionPath }
|
|
8632
8913
|
});
|
|
8633
8914
|
videoRef = "$ref:overlaid.video";
|
|
8634
8915
|
videoNode = "overlaid";
|
|
8635
8916
|
}
|
|
8636
|
-
const tracks =
|
|
8917
|
+
const tracks = [...voTracks, ...buildSfxMusic(blueprint, nodes)];
|
|
8637
8918
|
if (tracks.length > 0) {
|
|
8638
8919
|
const mixInputs = {};
|
|
8639
8920
|
for (const t of tracks) mixInputs[t.slot] = t.ref;
|
|
@@ -8666,8 +8947,18 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
8666
8947
|
"1:a:0",
|
|
8667
8948
|
"-c:v",
|
|
8668
8949
|
"copy",
|
|
8950
|
+
// The raw mix is a quiet mono track (tts + ducked bed), which reads as
|
|
8951
|
+
// "no sound" in casual players. Normalize integrated loudness to the
|
|
8952
|
+
// social/broadcast target (-14 LUFS, -1.5 dBTP) and upmix to stereo so
|
|
8953
|
+
// every rendered ad is loud and plays everywhere.
|
|
8954
|
+
"-af",
|
|
8955
|
+
"loudnorm=I=-14:TP=-1.5:LRA=11,aformat=channel_layouts=stereo",
|
|
8669
8956
|
"-c:a",
|
|
8670
8957
|
"aac",
|
|
8958
|
+
"-b:a",
|
|
8959
|
+
"192k",
|
|
8960
|
+
"-ar",
|
|
8961
|
+
"48000",
|
|
8671
8962
|
"-shortest",
|
|
8672
8963
|
"{{out.video}}"
|
|
8673
8964
|
],
|
|
@@ -8681,43 +8972,69 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
8681
8972
|
metadata: {
|
|
8682
8973
|
name: "video reproduction",
|
|
8683
8974
|
description: VIDEO_GUIDE,
|
|
8684
|
-
todo: buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts)
|
|
8975
|
+
todo: buildVideoTodo(videoReport(input, elementsInput), overlays.length, floating.length, opts),
|
|
8976
|
+
// The timing plan `baker canvas validate` checks before any billed render:
|
|
8977
|
+
// sequenced voiceover turns (no overlap), audio ≈ video length, and which
|
|
8978
|
+
// scenes must be lip-synced.
|
|
8979
|
+
video: buildVideoMeta(blueprint, sceneTurns)
|
|
8685
8980
|
},
|
|
8686
8981
|
nodes,
|
|
8687
8982
|
output: { node: videoNode, output: "video" }
|
|
8688
8983
|
};
|
|
8689
8984
|
}
|
|
8985
|
+
function buildVideoMeta(blueprint, sceneTurns) {
|
|
8986
|
+
const vo_segments = [];
|
|
8987
|
+
const talking_scenes = [];
|
|
8988
|
+
for (const [scene, turns] of [...sceneTurns.entries()].sort((a, b) => a[0] - b[0])) {
|
|
8989
|
+
for (const t of turns) {
|
|
8990
|
+
vo_segments.push({ slot: t.ttsId, start_s: t.start_s, end_s: t.end_s, scene, speaker: t.speaker });
|
|
8991
|
+
}
|
|
8992
|
+
if (turns.filter((t) => t.onCamera).length === 1) {
|
|
8993
|
+
talking_scenes.push({ scene, lipsync_node: `s${scene}_lipsync` });
|
|
8994
|
+
}
|
|
8995
|
+
}
|
|
8996
|
+
return {
|
|
8997
|
+
duration_s: blueprint.source?.duration_s ?? lastSceneEnd(blueprint),
|
|
8998
|
+
vo_segments,
|
|
8999
|
+
talking_scenes
|
|
9000
|
+
};
|
|
9001
|
+
}
|
|
8690
9002
|
var VIDEO_GUIDE = [
|
|
8691
|
-
"Scaffolded by `baker canvas scaffold-video` \u2014 a runnable reproduction of your reference video.
|
|
9003
|
+
"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
9004
|
"",
|
|
8693
9005
|
"WHAT TO DO NEXT:",
|
|
8694
9006
|
"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.
|
|
9007
|
+
"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.",
|
|
9008
|
+
"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.",
|
|
9009
|
+
"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.",
|
|
9010
|
+
"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.",
|
|
9011
|
+
"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
9012
|
"",
|
|
8700
9013
|
"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
9014
|
].join("\n");
|
|
8702
9015
|
function buildVideoTodo(report, overlayCount, floatingCount, opts) {
|
|
8703
9016
|
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.",
|
|
9017
|
+
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
9018
|
frames_mode: opts.frames ?? "generate",
|
|
9019
|
+
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
9020
|
recurring_elements_to_supply: report.elements,
|
|
9021
|
+
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.",
|
|
9022
|
+
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
9023
|
voices_to_confirm: report.dialogue.map((d) => ({
|
|
8708
9024
|
scene: d.scene,
|
|
8709
9025
|
speaker: d.speaker,
|
|
8710
9026
|
voice_description: d.voice_description,
|
|
8711
9027
|
line: d.line
|
|
8712
9028
|
})),
|
|
8713
|
-
voiceover_note: "
|
|
9029
|
+
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.",
|
|
9030
|
+
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
9031
|
text_overlays: {
|
|
8715
9032
|
count: overlayCount,
|
|
8716
|
-
note: "
|
|
9033
|
+
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
9034
|
},
|
|
8718
9035
|
floating_elements: {
|
|
8719
9036
|
count: floatingCount,
|
|
8720
|
-
note: floatingCount > 0 ? "
|
|
9037
|
+
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
9038
|
},
|
|
8722
9039
|
sound_effects: { count: report.sfx_count },
|
|
8723
9040
|
music: {
|
|
@@ -8769,8 +9086,24 @@ function videoReport(input, elementsInput) {
|
|
|
8769
9086
|
};
|
|
8770
9087
|
}
|
|
8771
9088
|
|
|
9089
|
+
// src/commands/canvas/composition-path.ts
|
|
9090
|
+
import { existsSync as existsSync3 } from "fs";
|
|
9091
|
+
import path4 from "path";
|
|
9092
|
+
function resolveShippedCanvasDir(name, startDir, exists = existsSync3, maxDepth = 8) {
|
|
9093
|
+
const rel = path4.join("canvas", name);
|
|
9094
|
+
let dir = startDir;
|
|
9095
|
+
for (let i = 0; i < maxDepth; i++) {
|
|
9096
|
+
const candidate = path4.join(dir, rel);
|
|
9097
|
+
if (exists(path4.join(candidate, "meta.json"))) return candidate;
|
|
9098
|
+
const parent = path4.dirname(dir);
|
|
9099
|
+
if (parent === dir) break;
|
|
9100
|
+
dir = parent;
|
|
9101
|
+
}
|
|
9102
|
+
return path4.resolve(startDir, "../../../", rel);
|
|
9103
|
+
}
|
|
9104
|
+
|
|
8772
9105
|
// src/commands/canvas/scaffold-video.ts
|
|
8773
|
-
var SHIPPED_COMPOSITION_DIR =
|
|
9106
|
+
var SHIPPED_COMPOSITION_DIR = resolveShippedCanvasDir("video-overlay-composition", import.meta.dirname);
|
|
8774
9107
|
function resolveModel2(kind, preferred) {
|
|
8775
9108
|
const ids = Object.keys(MODEL_REGISTRY[kind]);
|
|
8776
9109
|
return ids.includes(preferred) ? preferred : ids[0] ?? preferred;
|
|
@@ -8891,13 +9224,19 @@ var scaffoldVideoCommand = defineCommand76({
|
|
|
8891
9224
|
"video-model": { type: "string", description: "Override the video_generate model id for clips" }
|
|
8892
9225
|
},
|
|
8893
9226
|
async run({ args }) {
|
|
8894
|
-
const videoPath =
|
|
8895
|
-
const base =
|
|
8896
|
-
const outPath = args.out ?
|
|
8897
|
-
const outDir =
|
|
8898
|
-
const blueprintPath =
|
|
9227
|
+
const videoPath = path5.resolve(String(args.file));
|
|
9228
|
+
const base = path5.basename(videoPath, path5.extname(videoPath));
|
|
9229
|
+
const outPath = args.out ? path5.resolve(String(args.out)) : path5.join(path5.dirname(videoPath), `${base}.video.canvas.json`);
|
|
9230
|
+
const outDir = path5.dirname(outPath);
|
|
9231
|
+
const blueprintPath = path5.join(outDir, "prompt.json");
|
|
8899
9232
|
const frames = args.frames === "reuse" ? "reuse" : "generate";
|
|
8900
9233
|
const maxScenes = args["max-scenes"] ? Number(args["max-scenes"]) : void 0;
|
|
9234
|
+
if (Number.isFinite(maxScenes)) {
|
|
9235
|
+
process.stderr.write(
|
|
9236
|
+
`\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.
|
|
9237
|
+
`
|
|
9238
|
+
);
|
|
9239
|
+
}
|
|
8901
9240
|
const { deconstructModel, selectModel, imageModel, videoModel } = resolveModels2(args);
|
|
8902
9241
|
const analysisCanvas = buildAnalysisCanvas(videoPath, deconstructModel, selectModel, {
|
|
8903
9242
|
maxScenes: Number.isFinite(maxScenes) ? maxScenes : void 0,
|
|
@@ -8909,8 +9248,19 @@ var scaffoldVideoCommand = defineCommand76({
|
|
|
8909
9248
|
const annotated = annotateBlueprintWithElements(blueprint, elements);
|
|
8910
9249
|
await writeFile2(blueprintPath, `${JSON.stringify(annotated, null, 2)}
|
|
8911
9250
|
`, "utf8");
|
|
8912
|
-
const compositionDest =
|
|
9251
|
+
const compositionDest = path5.join(outDir, "video-overlay-composition");
|
|
8913
9252
|
await cp(SHIPPED_COMPOSITION_DIR, compositionDest, { recursive: true });
|
|
9253
|
+
const indexPath = path5.join(compositionDest, "index.html");
|
|
9254
|
+
const overlayHtml = buildOverlayHtml(blueprint);
|
|
9255
|
+
const indexHtml = await readFile4(indexPath, "utf8");
|
|
9256
|
+
const injected = indexHtml.replace("<!--OVERLAYS-->", () => overlayHtml);
|
|
9257
|
+
if (injected === indexHtml && overlayHtml.trim()) {
|
|
9258
|
+
fail2(
|
|
9259
|
+
"composition_marker_missing",
|
|
9260
|
+
`video-overlay-composition/index.html is missing the <!--OVERLAYS--> marker \u2014 cannot inject the overlay layer`
|
|
9261
|
+
);
|
|
9262
|
+
}
|
|
9263
|
+
await writeFile2(indexPath, injected, "utf8");
|
|
8914
9264
|
const opts = {
|
|
8915
9265
|
imageModel,
|
|
8916
9266
|
videoModel,
|
|
@@ -8953,7 +9303,7 @@ var scaffoldVideoCommand = defineCommand76({
|
|
|
8953
9303
|
run_estimated_credits: validation.estimatedCredits
|
|
8954
9304
|
},
|
|
8955
9305
|
checklist: {
|
|
8956
|
-
edit_prompt: `Edit ${
|
|
9306
|
+
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
9307
|
recurring_elements_to_supply: report.elements,
|
|
8958
9308
|
voices_to_confirm: report.dialogue.map((d) => ({
|
|
8959
9309
|
scene: d.scene,
|
|
@@ -8978,7 +9328,7 @@ var scaffoldVideoCommand = defineCommand76({
|
|
|
8978
9328
|
|
|
8979
9329
|
// src/commands/canvas/validate.ts
|
|
8980
9330
|
import { readFile as readFile5 } from "fs/promises";
|
|
8981
|
-
import
|
|
9331
|
+
import path6 from "path";
|
|
8982
9332
|
import { defineCommand as defineCommand77 } from "citty";
|
|
8983
9333
|
var validateCommand = defineCommand77({
|
|
8984
9334
|
meta: {
|
|
@@ -8987,7 +9337,7 @@ var validateCommand = defineCommand77({
|
|
|
8987
9337
|
},
|
|
8988
9338
|
args: { file: { type: "positional", required: true, description: "Path to canvas JSON" } },
|
|
8989
9339
|
async run({ args }) {
|
|
8990
|
-
const filePath =
|
|
9340
|
+
const filePath = path6.resolve(String(args.file));
|
|
8991
9341
|
const raw = await readFile5(filePath, "utf8");
|
|
8992
9342
|
let parsed;
|
|
8993
9343
|
try {
|
|
@@ -9222,7 +9572,7 @@ Examples:
|
|
|
9222
9572
|
});
|
|
9223
9573
|
|
|
9224
9574
|
// src/commands/ga4/query.ts
|
|
9225
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
9575
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
|
|
9226
9576
|
import { resolve as resolve2 } from "path";
|
|
9227
9577
|
import { defineCommand as defineCommand81 } from "citty";
|
|
9228
9578
|
|
|
@@ -9296,7 +9646,7 @@ function writeRowsToFile2(filePath, rows, append) {
|
|
|
9296
9646
|
const fields = extractFields2(rows);
|
|
9297
9647
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
9298
9648
|
if (ext === "csv") {
|
|
9299
|
-
if (!append || !
|
|
9649
|
+
if (!append || !existsSync4(filePath)) {
|
|
9300
9650
|
writeFileSync4(filePath, `${toCsvRow(fields)}
|
|
9301
9651
|
`, "utf-8");
|
|
9302
9652
|
}
|
|
@@ -9307,12 +9657,12 @@ function writeRowsToFile2(filePath, rows, append) {
|
|
|
9307
9657
|
const lines = rows.map((row) => JSON.stringify(row));
|
|
9308
9658
|
const content = `${lines.join("\n")}
|
|
9309
9659
|
`;
|
|
9310
|
-
if (append &&
|
|
9660
|
+
if (append && existsSync4(filePath)) {
|
|
9311
9661
|
appendFileSync2(filePath, content, "utf-8");
|
|
9312
9662
|
} else {
|
|
9313
9663
|
writeFileSync4(filePath, content, "utf-8");
|
|
9314
9664
|
}
|
|
9315
|
-
} else if (append &&
|
|
9665
|
+
} else if (append && existsSync4(filePath)) {
|
|
9316
9666
|
const existing = JSON.parse(readFileSync6(filePath, "utf-8"));
|
|
9317
9667
|
writeFileSync4(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
|
|
9318
9668
|
} else {
|
|
@@ -9453,7 +9803,7 @@ Examples:
|
|
|
9453
9803
|
import { defineCommand as defineCommand86 } from "citty";
|
|
9454
9804
|
|
|
9455
9805
|
// src/commands/gsc/query.ts
|
|
9456
|
-
import { appendFileSync as appendFileSync3, existsSync as
|
|
9806
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
9457
9807
|
import { resolve as resolve3 } from "path";
|
|
9458
9808
|
import { defineCommand as defineCommand83 } from "citty";
|
|
9459
9809
|
|
|
@@ -9582,7 +9932,7 @@ function writeRowsToFile3(filePath, rows, append) {
|
|
|
9582
9932
|
const fields = extractFields3(rows);
|
|
9583
9933
|
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
9584
9934
|
if (ext === "csv") {
|
|
9585
|
-
if (!append || !
|
|
9935
|
+
if (!append || !existsSync5(filePath)) {
|
|
9586
9936
|
writeFileSync5(filePath, `${toCsvRow(fields)}
|
|
9587
9937
|
`, "utf-8");
|
|
9588
9938
|
}
|
|
@@ -9592,12 +9942,12 @@ function writeRowsToFile3(filePath, rows, append) {
|
|
|
9592
9942
|
} else if (ext === "jsonl") {
|
|
9593
9943
|
const content = `${rows.map((row) => JSON.stringify(row)).join("\n")}
|
|
9594
9944
|
`;
|
|
9595
|
-
if (append &&
|
|
9945
|
+
if (append && existsSync5(filePath)) {
|
|
9596
9946
|
appendFileSync3(filePath, content, "utf-8");
|
|
9597
9947
|
} else {
|
|
9598
9948
|
writeFileSync5(filePath, content, "utf-8");
|
|
9599
9949
|
}
|
|
9600
|
-
} else if (append &&
|
|
9950
|
+
} else if (append && existsSync5(filePath)) {
|
|
9601
9951
|
const existing = JSON.parse(readFileSync7(filePath, "utf-8"));
|
|
9602
9952
|
writeFileSync5(filePath, JSON.stringify([...existing, ...rows], null, 2), "utf-8");
|
|
9603
9953
|
} else {
|
|
@@ -9915,9 +10265,9 @@ async function readImageBuffer(pathOrUrl) {
|
|
|
9915
10265
|
}
|
|
9916
10266
|
return readFile6(pathOrUrl);
|
|
9917
10267
|
}
|
|
9918
|
-
async function isDirectory(
|
|
10268
|
+
async function isDirectory(path7) {
|
|
9919
10269
|
try {
|
|
9920
|
-
const s = await stat2(
|
|
10270
|
+
const s = await stat2(path7);
|
|
9921
10271
|
return s.isDirectory();
|
|
9922
10272
|
} catch {
|
|
9923
10273
|
return false;
|
|
@@ -14296,6 +14646,317 @@ Examples:
|
|
|
14296
14646
|
}
|
|
14297
14647
|
});
|
|
14298
14648
|
|
|
14649
|
+
// src/commands/winning-ads/index.ts
|
|
14650
|
+
import { defineCommand as defineCommand140 } from "citty";
|
|
14651
|
+
|
|
14652
|
+
// src/commands/winning-ads/advertisers.ts
|
|
14653
|
+
import { defineCommand as defineCommand138 } from "citty";
|
|
14654
|
+
registerSchema({
|
|
14655
|
+
command: "winning-ads.advertisers",
|
|
14656
|
+
description: "Resolve a brand name to advertiser_id(s) in the ad-dna corpus \u2014 to find your OWN advertiser (to --exclude-advertiser) or a competitor (to --advertiser-id).",
|
|
14657
|
+
args: {
|
|
14658
|
+
query: { type: "string", description: "Brand / advertiser name to match (case-insensitive)", required: false },
|
|
14659
|
+
limit: { type: "number", description: "Max results (default 20)", required: false, default: 20 },
|
|
14660
|
+
platform: { type: "string", description: "Filter to a single platform", required: false }
|
|
14661
|
+
}
|
|
14662
|
+
});
|
|
14663
|
+
function identity(record) {
|
|
14664
|
+
return record;
|
|
14665
|
+
}
|
|
14666
|
+
var advertisersCommand2 = defineCommand138({
|
|
14667
|
+
meta: {
|
|
14668
|
+
name: "advertisers",
|
|
14669
|
+
description: 'Resolve a brand name to advertiser_id(s). Use it to find your own advertiser for --exclude-advertiser, or a competitor for --advertiser-id. Example: baker winning-ads advertisers "Deel" --output md'
|
|
14670
|
+
},
|
|
14671
|
+
args: {
|
|
14672
|
+
query: { type: "positional", description: "Brand / advertiser name", required: false },
|
|
14673
|
+
limit: { type: "string", description: "Max results (default 20)", required: false },
|
|
14674
|
+
platform: { type: "string", description: "Filter to a single platform", required: false },
|
|
14675
|
+
output: { type: "string", description: "Output format: json|files|md", required: false, default: "json" },
|
|
14676
|
+
fields: { type: "string", description: "Comma-separated field names to include", required: false }
|
|
14677
|
+
},
|
|
14678
|
+
run: async ({ args }) => {
|
|
14679
|
+
try {
|
|
14680
|
+
const params = {};
|
|
14681
|
+
const query = args.query;
|
|
14682
|
+
if (query) {
|
|
14683
|
+
params.q = query;
|
|
14684
|
+
}
|
|
14685
|
+
if (args.limit) {
|
|
14686
|
+
params.limit = String(args.limit);
|
|
14687
|
+
}
|
|
14688
|
+
if (args.platform) {
|
|
14689
|
+
params.platform = String(args.platform);
|
|
14690
|
+
}
|
|
14691
|
+
const data = await apiGet("/api/winning-ads/advertisers", params);
|
|
14692
|
+
const output = args.output || "json";
|
|
14693
|
+
if (output === "json") {
|
|
14694
|
+
writeJson({ ok: true, data });
|
|
14695
|
+
return;
|
|
14696
|
+
}
|
|
14697
|
+
const list = Array.isArray(data?.advertisers) ? data.advertisers : [];
|
|
14698
|
+
writeOutput(
|
|
14699
|
+
{ ok: true, data: list },
|
|
14700
|
+
output,
|
|
14701
|
+
args.fields ? args.fields.split(",") : void 0,
|
|
14702
|
+
false,
|
|
14703
|
+
identity
|
|
14704
|
+
);
|
|
14705
|
+
} catch (err) {
|
|
14706
|
+
if (err instanceof ApiError) {
|
|
14707
|
+
writeJson({ ok: false, error: { code: err.code, message: err.message } });
|
|
14708
|
+
process.exit(1);
|
|
14709
|
+
}
|
|
14710
|
+
writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
|
|
14711
|
+
process.exit(1);
|
|
14712
|
+
}
|
|
14713
|
+
}
|
|
14714
|
+
});
|
|
14715
|
+
|
|
14716
|
+
// src/commands/winning-ads/search.ts
|
|
14717
|
+
import { defineCommand as defineCommand139 } from "citty";
|
|
14718
|
+
registerSchema({
|
|
14719
|
+
command: "winning-ads.search",
|
|
14720
|
+
description: "Search the ad-dna corpus of scored winning ads. Returns a lean shortlist (advertiser, summary, scores, media_url) to pick a reference to reproduce.",
|
|
14721
|
+
args: {
|
|
14722
|
+
query: { type: "string", description: "Free-text natural-language query", required: false },
|
|
14723
|
+
"ref-ad-id": {
|
|
14724
|
+
type: "string",
|
|
14725
|
+
description: "Find ads similar to this ad id (instead of free text)",
|
|
14726
|
+
required: false
|
|
14727
|
+
},
|
|
14728
|
+
limit: { type: "number", description: "Max results 1-100 (default 10)", required: false, default: 10 },
|
|
14729
|
+
"max-per-advertiser": {
|
|
14730
|
+
type: "number",
|
|
14731
|
+
description: "Cap results per advertiser 1-50 (default 3)",
|
|
14732
|
+
required: false,
|
|
14733
|
+
default: 3
|
|
14734
|
+
},
|
|
14735
|
+
"min-relevance": { type: "number", description: "Relevance floor 0-1; trims weak matches", required: false },
|
|
14736
|
+
platform: {
|
|
14737
|
+
type: "string",
|
|
14738
|
+
description: "Comma list: meta,tiktok,linkedin,google_search,google_display,youtube,reddit,x,pinterest,snapchat",
|
|
14739
|
+
required: false
|
|
14740
|
+
},
|
|
14741
|
+
format: { type: "string", description: "Comma list: video,static,carousel", required: false },
|
|
14742
|
+
"winner-category": {
|
|
14743
|
+
type: "string",
|
|
14744
|
+
description: "Comma list: winner,scaled_winner,evergreen,rising,untested,dud,\u2026",
|
|
14745
|
+
required: false
|
|
14746
|
+
},
|
|
14747
|
+
awareness: {
|
|
14748
|
+
type: "string",
|
|
14749
|
+
description: "Comma list: unaware,problem_aware,solution_aware,product_aware,most_aware",
|
|
14750
|
+
required: false
|
|
14751
|
+
},
|
|
14752
|
+
country: { type: "string", description: "Comma list of country codes", required: false },
|
|
14753
|
+
language: { type: "string", description: "Comma list of language codes", required: false },
|
|
14754
|
+
"advertiser-id": {
|
|
14755
|
+
type: "string",
|
|
14756
|
+
description: "Restrict to these advertiser ids (browse one brand's winners)",
|
|
14757
|
+
required: false
|
|
14758
|
+
},
|
|
14759
|
+
"exclude-advertiser": {
|
|
14760
|
+
type: "string",
|
|
14761
|
+
description: "Drop these advertiser ids \u2014 your own brand + already-used references",
|
|
14762
|
+
required: false
|
|
14763
|
+
},
|
|
14764
|
+
"first-seen-after": {
|
|
14765
|
+
type: "string",
|
|
14766
|
+
description: "ISO datetime; only ads first seen after this",
|
|
14767
|
+
required: false
|
|
14768
|
+
},
|
|
14769
|
+
"first-seen-before": {
|
|
14770
|
+
type: "string",
|
|
14771
|
+
description: "ISO datetime; only ads first seen before this",
|
|
14772
|
+
required: false
|
|
14773
|
+
}
|
|
14774
|
+
}
|
|
14775
|
+
});
|
|
14776
|
+
function splitList(value) {
|
|
14777
|
+
if (!value) {
|
|
14778
|
+
return [];
|
|
14779
|
+
}
|
|
14780
|
+
return value.split(",").map((v) => v.trim()).filter(Boolean);
|
|
14781
|
+
}
|
|
14782
|
+
function setNumber(body, key, value) {
|
|
14783
|
+
if (value !== void 0 && value !== "") {
|
|
14784
|
+
body[key] = Number(value);
|
|
14785
|
+
}
|
|
14786
|
+
}
|
|
14787
|
+
function setList(target, key, value) {
|
|
14788
|
+
const list = splitList(value);
|
|
14789
|
+
if (list.length) {
|
|
14790
|
+
target[key] = list;
|
|
14791
|
+
}
|
|
14792
|
+
}
|
|
14793
|
+
function setString(target, key, value) {
|
|
14794
|
+
if (value) {
|
|
14795
|
+
target[key] = value;
|
|
14796
|
+
}
|
|
14797
|
+
}
|
|
14798
|
+
function buildSearchBody(args) {
|
|
14799
|
+
const body = {};
|
|
14800
|
+
if (args.query) {
|
|
14801
|
+
body.free_text_query = args.query;
|
|
14802
|
+
}
|
|
14803
|
+
if (args.refAdId) {
|
|
14804
|
+
body.ref_ad_id = args.refAdId;
|
|
14805
|
+
}
|
|
14806
|
+
setNumber(body, "limit", args.limit);
|
|
14807
|
+
setNumber(body, "max_per_advertiser", args.maxPerAdvertiser);
|
|
14808
|
+
setNumber(body, "min_relevance", args.minRelevance);
|
|
14809
|
+
const hardFilters = {};
|
|
14810
|
+
setList(hardFilters, "platform", args.platform);
|
|
14811
|
+
setList(hardFilters, "format", args.format);
|
|
14812
|
+
setList(hardFilters, "winner_category", args.winnerCategory);
|
|
14813
|
+
setList(hardFilters, "awareness_stage", args.awareness);
|
|
14814
|
+
setList(hardFilters, "country", args.country);
|
|
14815
|
+
setList(hardFilters, "language", args.language);
|
|
14816
|
+
setList(hardFilters, "advertiser_ids", args.advertiserId);
|
|
14817
|
+
setList(hardFilters, "exclude_advertiser_ids", args.excludeAdvertiser);
|
|
14818
|
+
setString(hardFilters, "first_seen_after", args.firstSeenAfter);
|
|
14819
|
+
setString(hardFilters, "first_seen_before", args.firstSeenBefore);
|
|
14820
|
+
if (Object.keys(hardFilters).length) {
|
|
14821
|
+
body.hard_filters = hardFilters;
|
|
14822
|
+
}
|
|
14823
|
+
return body;
|
|
14824
|
+
}
|
|
14825
|
+
var searchCommand4 = defineCommand139({
|
|
14826
|
+
meta: {
|
|
14827
|
+
name: "search",
|
|
14828
|
+
description: "Search winning reference ads. Example: baker winning-ads search 'B2B SaaS before/after AI automation' --platform meta --format static --winner-category winner --exclude-advertiser adv_123 --output md"
|
|
14829
|
+
},
|
|
14830
|
+
args: {
|
|
14831
|
+
query: { type: "positional", description: "Free-text search query", required: false },
|
|
14832
|
+
"ref-ad-id": { type: "string", description: "Find ads similar to this ad id", required: false },
|
|
14833
|
+
limit: {
|
|
14834
|
+
type: "string",
|
|
14835
|
+
description: "Max results 1-100 (default 10 \u2014 a shortlist)",
|
|
14836
|
+
required: false,
|
|
14837
|
+
default: "10"
|
|
14838
|
+
},
|
|
14839
|
+
"max-per-advertiser": {
|
|
14840
|
+
type: "string",
|
|
14841
|
+
description: "Cap results per advertiser 1-50 (default 3)",
|
|
14842
|
+
required: false
|
|
14843
|
+
},
|
|
14844
|
+
"min-relevance": { type: "string", description: "Relevance floor 0-1", required: false },
|
|
14845
|
+
platform: {
|
|
14846
|
+
type: "string",
|
|
14847
|
+
description: "Comma list of platforms (meta,linkedin,tiktok,\u2026) \u2014 search one or many",
|
|
14848
|
+
required: false
|
|
14849
|
+
},
|
|
14850
|
+
format: { type: "string", description: "Comma list of formats (video,static,carousel)", required: false },
|
|
14851
|
+
"winner-category": {
|
|
14852
|
+
type: "string",
|
|
14853
|
+
description: "Comma list of winner categories (default: all)",
|
|
14854
|
+
required: false
|
|
14855
|
+
},
|
|
14856
|
+
awareness: { type: "string", description: "Comma list of awareness stages", required: false },
|
|
14857
|
+
country: { type: "string", description: "Comma list of country codes", required: false },
|
|
14858
|
+
language: { type: "string", description: "Comma list of language codes", required: false },
|
|
14859
|
+
"advertiser-id": { type: "string", description: "Restrict to these advertiser ids", required: false },
|
|
14860
|
+
"exclude-advertiser": {
|
|
14861
|
+
type: "string",
|
|
14862
|
+
description: "Drop these advertiser ids (your own + already-used)",
|
|
14863
|
+
required: false
|
|
14864
|
+
},
|
|
14865
|
+
"first-seen-after": { type: "string", description: "ISO datetime lower bound", required: false },
|
|
14866
|
+
"first-seen-before": { type: "string", description: "ISO datetime upper bound", required: false },
|
|
14867
|
+
output: { type: "string", description: "Output format: json|files|md", required: false, default: "json" },
|
|
14868
|
+
fields: { type: "string", description: "Comma-separated field names to include", required: false },
|
|
14869
|
+
full: {
|
|
14870
|
+
type: "boolean",
|
|
14871
|
+
description: "Include DNA detail (angle, persona, hook) + longevity",
|
|
14872
|
+
required: false,
|
|
14873
|
+
default: false
|
|
14874
|
+
}
|
|
14875
|
+
},
|
|
14876
|
+
run: async ({ args }) => {
|
|
14877
|
+
try {
|
|
14878
|
+
const searchArgs = {
|
|
14879
|
+
query: args.query,
|
|
14880
|
+
refAdId: args["ref-ad-id"],
|
|
14881
|
+
limit: args.limit,
|
|
14882
|
+
maxPerAdvertiser: args["max-per-advertiser"],
|
|
14883
|
+
minRelevance: args["min-relevance"],
|
|
14884
|
+
platform: args.platform,
|
|
14885
|
+
format: args.format,
|
|
14886
|
+
winnerCategory: args["winner-category"],
|
|
14887
|
+
awareness: args.awareness,
|
|
14888
|
+
country: args.country,
|
|
14889
|
+
language: args.language,
|
|
14890
|
+
advertiserId: args["advertiser-id"],
|
|
14891
|
+
excludeAdvertiser: args["exclude-advertiser"],
|
|
14892
|
+
firstSeenAfter: args["first-seen-after"],
|
|
14893
|
+
firstSeenBefore: args["first-seen-before"]
|
|
14894
|
+
};
|
|
14895
|
+
const body = buildSearchBody(searchArgs);
|
|
14896
|
+
if (!("free_text_query" in body) && !("ref_ad_id" in body) && !("hard_filters" in body)) {
|
|
14897
|
+
writeJson({
|
|
14898
|
+
ok: false,
|
|
14899
|
+
error: { code: "VALIDATION_ERROR", message: "Provide a query, --ref-ad-id, or a filter flag" }
|
|
14900
|
+
});
|
|
14901
|
+
process.exit(1);
|
|
14902
|
+
}
|
|
14903
|
+
const data = await apiPost(
|
|
14904
|
+
"/api/winning-ads/search",
|
|
14905
|
+
body
|
|
14906
|
+
);
|
|
14907
|
+
const output = args.output || "json";
|
|
14908
|
+
const full = args.full;
|
|
14909
|
+
const rawResults = Array.isArray(data?.results) ? data.results : [];
|
|
14910
|
+
if (output === "json") {
|
|
14911
|
+
const results = rawResults.map((r) => winningAdNormalizer(r, full));
|
|
14912
|
+
writeJson({
|
|
14913
|
+
ok: true,
|
|
14914
|
+
data: { results, pool_size: data?.pool_size ?? null, match_confidence: data?.match_confidence ?? null }
|
|
14915
|
+
});
|
|
14916
|
+
return;
|
|
14917
|
+
}
|
|
14918
|
+
writeOutput(
|
|
14919
|
+
{ ok: true, data: rawResults },
|
|
14920
|
+
output,
|
|
14921
|
+
args.fields ? args.fields.split(",") : void 0,
|
|
14922
|
+
full,
|
|
14923
|
+
winningAdNormalizer
|
|
14924
|
+
);
|
|
14925
|
+
} catch (err) {
|
|
14926
|
+
if (err instanceof ApiError) {
|
|
14927
|
+
writeJson({ ok: false, error: { code: err.code, message: err.message } });
|
|
14928
|
+
process.exit(1);
|
|
14929
|
+
}
|
|
14930
|
+
writeJson({ ok: false, error: { code: "INTERNAL_ERROR", message: "Unexpected error" } });
|
|
14931
|
+
process.exit(1);
|
|
14932
|
+
}
|
|
14933
|
+
}
|
|
14934
|
+
});
|
|
14935
|
+
|
|
14936
|
+
// src/commands/winning-ads/index.ts
|
|
14937
|
+
var winningAdsCommand = defineCommand140({
|
|
14938
|
+
meta: {
|
|
14939
|
+
name: "winning-ads",
|
|
14940
|
+
description: `Search the ad-dna corpus of scored "winning" ads for reference creatives to reproduce. Proxied through the Baker backend (BAKER_API_KEY) \u2014 no separate token needed.
|
|
14941
|
+
|
|
14942
|
+
Auth: BAKER_API_KEY (must start with bk_) + BAKER_API_URL (your Convex .convex.site URL).
|
|
14943
|
+
|
|
14944
|
+
Subcommands:
|
|
14945
|
+
baker winning-ads search "<free text>" \u2014 semantic search; returns a lean shortlist (advertiser, summary, scores, media_url)
|
|
14946
|
+
baker winning-ads advertisers "<brand>" \u2014 resolve a brand \u2192 advertiser_id(s), to --exclude-advertiser (your own) or --advertiser-id (a competitor)
|
|
14947
|
+
|
|
14948
|
+
Examples:
|
|
14949
|
+
baker winning-ads search "B2B SaaS ad: before/after of an overworked team replaced by AI automation" --platform meta --format static
|
|
14950
|
+
baker winning-ads search "skincare UGC testimonial" --platform tiktok --format video --output md
|
|
14951
|
+
baker winning-ads search "fintech onboarding" --winner-category winner --exclude-advertiser adv_ourbrand,adv_usedbefore --output md
|
|
14952
|
+
baker winning-ads advertisers "Acme" --output md # find our own advertiser id to exclude`
|
|
14953
|
+
},
|
|
14954
|
+
subCommands: {
|
|
14955
|
+
search: searchCommand4,
|
|
14956
|
+
advertisers: advertisersCommand2
|
|
14957
|
+
}
|
|
14958
|
+
});
|
|
14959
|
+
|
|
14299
14960
|
// src/version.ts
|
|
14300
14961
|
import { readFileSync as readFileSync8 } from "fs";
|
|
14301
14962
|
function packageJsonUrl() {
|
|
@@ -14313,7 +14974,7 @@ function getCliVersion() {
|
|
|
14313
14974
|
}
|
|
14314
14975
|
|
|
14315
14976
|
// src/cli.ts
|
|
14316
|
-
var main =
|
|
14977
|
+
var main = defineCommand141({
|
|
14317
14978
|
meta: {
|
|
14318
14979
|
name: "baker",
|
|
14319
14980
|
version: getCliVersion(),
|
|
@@ -14336,6 +14997,7 @@ Introspection: Run 'baker schema <command>' to inspect argument schemas.`
|
|
|
14336
14997
|
videos: videosCommand,
|
|
14337
14998
|
testimonials: testimonialsCommand,
|
|
14338
14999
|
canvas: canvasCommand,
|
|
15000
|
+
"winning-ads": winningAdsCommand,
|
|
14339
15001
|
schema: schemaCommand
|
|
14340
15002
|
}
|
|
14341
15003
|
});
|