@koda-sl/baker-cli 0.302.0-dev.3ebf34fad → 0.305.0-dev.4e8ef1cd7
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 +5 -1
- package/dist/{chunk-KKJT24WA.js → chunk-7TL4VPK5.js} +107 -14
- package/dist/chunk-7TL4VPK5.js.map +1 -0
- package/dist/cli.js +198 -61
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-KKJT24WA.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
bestSupportedQuality,
|
|
24
24
|
canonicalUrl,
|
|
25
25
|
clipAudioMode,
|
|
26
|
+
clipDurationsFor,
|
|
26
27
|
clipParamRecipe,
|
|
27
28
|
clipProfileFor,
|
|
28
29
|
collectAssetRefLikes,
|
|
@@ -64,7 +65,7 @@ import {
|
|
|
64
65
|
ulid,
|
|
65
66
|
validateCanvasDeep,
|
|
66
67
|
ytDlpBlockSignal
|
|
67
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-7TL4VPK5.js";
|
|
68
69
|
import {
|
|
69
70
|
csvOrJson,
|
|
70
71
|
daysAgoIso,
|
|
@@ -2381,7 +2382,6 @@ var IMAGE_ASSET_LINK_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
|
2381
2382
|
"PORTRAIT_MARKETING_IMAGE",
|
|
2382
2383
|
"AD_IMAGE"
|
|
2383
2384
|
]);
|
|
2384
|
-
var IMAGE_ASSET_LINK_MESSAGE = "Google refuses the LINK, not the upload \u2014 so do not stop at this refusal, finish the handover. `assets create` with an image DOES publish: the image lands in the advertiser's Google Ads asset library and is theirs to use. Only this link is refused (MARKETING_IMAGE is a Performance Max asset-group slot \u2014 use `asset-groups attach`; AD_IMAGE comes back UNSUPPORTED_FIELD_TYPE at every level, on every API version). So: stage the image assets anyway, drop this link, and tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets \u2014 name them. Baker stages the Task for that step itself, alongside the image, so it is already in this Session's changes \u2014 do not stage a second one with `baker actions`. Never tell them to upload the images; they are uploaded. The one image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO at campaign or customer level, on a verified account.";
|
|
2385
2385
|
function isImageAssetLinkFieldType(fieldType) {
|
|
2386
2386
|
return IMAGE_ASSET_LINK_FIELD_TYPES.has(fieldType);
|
|
2387
2387
|
}
|
|
@@ -3385,7 +3385,6 @@ var assetLinkAttachSchema = z5.object({
|
|
|
3385
3385
|
if (value.fieldType === "BUSINESS_LOGO" && value.level === "adGroup") {
|
|
3386
3386
|
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["fieldType"], message: BUSINESS_LOGO_LEVEL_MESSAGE });
|
|
3387
3387
|
} else if (isImageAssetLinkFieldType(value.fieldType)) {
|
|
3388
|
-
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["fieldType"], message: IMAGE_ASSET_LINK_MESSAGE });
|
|
3389
3388
|
} else if (!ASSET_FIELD_TYPES.includes(value.fieldType)) {
|
|
3390
3389
|
ctx.addIssue({
|
|
3391
3390
|
code: z5.ZodIssueCode.custom,
|
|
@@ -4465,7 +4464,13 @@ var avatarsCreateRequestSchema = z10.object({
|
|
|
4465
4464
|
sourceImageIds: z10.array(z10.string().min(1)).max(6).optional(),
|
|
4466
4465
|
/** The company's statement that it may use this person's likeness in ads. */
|
|
4467
4466
|
likenessConfirmed: z10.boolean().optional(),
|
|
4468
|
-
chatId: z10.string().optional()
|
|
4467
|
+
chatId: z10.string().optional(),
|
|
4468
|
+
/**
|
|
4469
|
+
* Who picks this avatar's face. `ask` (the default) means a picker is about to open,
|
|
4470
|
+
* so the sheet waits for the chosen portrait instead of being fused from one nobody
|
|
4471
|
+
* saw — two renders that were always going to be thrown away.
|
|
4472
|
+
*/
|
|
4473
|
+
faceChoice: z10.enum(["ask", "auto"]).optional()
|
|
4469
4474
|
}).refine((body) => !body.sourceImageIds?.length || body.likenessConfirmed === true, {
|
|
4470
4475
|
message: "likenessConfirmed is required when building an avatar from photographs of a real person",
|
|
4471
4476
|
path: ["likenessConfirmed"]
|
|
@@ -16593,8 +16598,8 @@ function imageAssetHints(payload) {
|
|
|
16593
16598
|
return [];
|
|
16594
16599
|
}
|
|
16595
16600
|
return [
|
|
16596
|
-
"This image publishes into the advertiser's Google Ads asset library \u2014 that half works. What Google refuses is the link that puts it on a Search campaign, so
|
|
16597
|
-
"Finish the handover rather than reporting a limitation: tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets, and name the ones you staged. Baker stages the Task for that step itself the moment you stage the image, so it is already in this chat's changes \u2014 do not stage a second one with `baker actions`. Never tell them to upload images they already have. (A Performance Max asset group is different \u2014 images do link there, with `asset-groups attach`.)"
|
|
16601
|
+
"This image publishes into the advertiser's Google Ads asset library \u2014 that half works. What Google refuses is the link that puts it on a Search campaign, so the `assets attach` for it never stores: it is skipped with the reason, and staging it anyway is what tells Baker which campaign these images belong on.",
|
|
16602
|
+
"Finish the handover rather than reporting a limitation: tell the user the images are already in their Google Ads account and only need adding to the campaign under Assets, and name the ones you staged. Baker stages the Task for that step itself the moment you stage the image, so it is already in this chat's changes \u2014 do not stage a second one with `baker actions`. Stage the `assets attach` too if you know which campaign they belong on: it is skipped rather than stored, and it is what puts the campaign's name in that Task. Never tell them to upload images they already have. (A Performance Max asset group is different \u2014 images do link there, with `asset-groups attach`.)"
|
|
16598
16603
|
];
|
|
16599
16604
|
}
|
|
16600
16605
|
var assetsUpdateCommand = defineCommand31({
|
|
@@ -16637,7 +16642,7 @@ var assetsUpdateCommand = defineCommand31({
|
|
|
16637
16642
|
var assetsCommand = defineCommand31({
|
|
16638
16643
|
meta: {
|
|
16639
16644
|
name: "assets",
|
|
16640
|
-
description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts and \u22651 structured snippet per campaign. Image extensions are the one thing you cannot finish here \u2014 `create` uploads the image into the advertiser's account, but Google refuses the link that puts it on a Search campaign, so hand that one step over rather than telling them to upload anything. The image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO, at campaign or customer level, on a verified account."
|
|
16645
|
+
description: "Stage asset create/update + asset-link attach/detach (via --file). For a high-performance Search campaign add \u22654 sitelinks, \u22653 callouts and \u22651 structured snippet per campaign. Image extensions are the one thing you cannot finish here \u2014 `create` uploads the image into the advertiser's account, but Google refuses the link that puts it on a Search campaign, so hand that one step over rather than telling them to upload anything (stage the `attach` regardless: it is skipped, and it is what records the handover Task). The image a Search campaign does take through a link is the advertiser's logo \u2014 BUSINESS_LOGO, at campaign or customer level, on a verified account."
|
|
16641
16646
|
},
|
|
16642
16647
|
subCommands: {
|
|
16643
16648
|
create: fileCreateCommand(
|
|
@@ -16648,7 +16653,7 @@ var assetsCommand = defineCommand31({
|
|
|
16648
16653
|
),
|
|
16649
16654
|
update: assetsUpdateCommand,
|
|
16650
16655
|
attach: fileCreateCommand("attach", "google.assetLink.attach", "Attach an asset to a campaign/adGroup/customer", [
|
|
16651
|
-
"Image extensions
|
|
16656
|
+
"Image extensions cannot be linked \u2014 Google refuses AD_IMAGE and every marketing-image field type at every level, on every API version \u2014 but stage the link anyway: it is skipped rather than stored, the rest of a batch is unaffected, and it is what records the handover Task naming those images and that campaign. That is the ONLY record when the images are already in the account and you are uploading nothing. Then tell the user they are already in their Google Ads account and only need adding to the campaign under Assets; do not stage a second Task with `baker actions`. Two images that DO link: the advertiser's logo through this command (`BUSINESS_LOGO`, campaign or customer level, on a verified account), and Performance Max asset-group images, which are a different mechanism (`asset-groups attach`)."
|
|
16652
16657
|
]),
|
|
16653
16658
|
detach: statusCommand2("google.assetLink.detach", "asset link")
|
|
16654
16659
|
}
|
|
@@ -30246,7 +30251,10 @@ var createCommand2 = defineCommand96({
|
|
|
30246
30251
|
...sourceImageIds ? { sourceImageIds, likenessConfirmed: true } : {},
|
|
30247
30252
|
...args["voice-id"] ? { voiceId: args["voice-id"] } : {},
|
|
30248
30253
|
...args["voice-description"] ? { voiceDescription: args["voice-description"] } : {},
|
|
30249
|
-
...chatIdFromEnv() ? { chatId: chatIdFromEnv() } : {}
|
|
30254
|
+
...chatIdFromEnv() ? { chatId: chatIdFromEnv() } : {},
|
|
30255
|
+
// Sent so the backend can hold the sheet until the chosen portrait exists —
|
|
30256
|
+
// otherwise it fuses one from a face nobody picked and pays for it twice.
|
|
30257
|
+
faceChoice
|
|
30250
30258
|
};
|
|
30251
30259
|
const data = await writeAvatars("/api/avatars", body);
|
|
30252
30260
|
writeJson({ ok: true, data, hints: creationHints({ ...data, faceChoice }, body.profile) });
|
|
@@ -33463,7 +33471,7 @@ function sameSpeakerTrackEntries(segments) {
|
|
|
33463
33471
|
};
|
|
33464
33472
|
});
|
|
33465
33473
|
}
|
|
33466
|
-
function buildPerSpeakerVoiceConversion(segments, totalMs, nodes) {
|
|
33474
|
+
function buildPerSpeakerVoiceConversion(segments, totalMs, nodes, keepRenderedVoice = false) {
|
|
33467
33475
|
const bySpeaker = /* @__PURE__ */ new Map();
|
|
33468
33476
|
for (const seg of segments) {
|
|
33469
33477
|
const arr = bySpeaker.get(seg.voiceNode) ?? [];
|
|
@@ -33472,7 +33480,7 @@ function buildPerSpeakerVoiceConversion(segments, totalMs, nodes) {
|
|
|
33472
33480
|
}
|
|
33473
33481
|
const tracks = [];
|
|
33474
33482
|
for (const [voiceNode, segs] of bySpeaker) {
|
|
33475
|
-
const trackId = `${voiceNode}_track`;
|
|
33483
|
+
const trackId = keepRenderedVoice ? `${voiceNode}_conv` : `${voiceNode}_track`;
|
|
33476
33484
|
const convId = `${voiceNode}_conv`;
|
|
33477
33485
|
const sorted = [...segs].sort((a, b) => a.start_s - b.start_s);
|
|
33478
33486
|
const mixInputs = {};
|
|
@@ -33488,12 +33496,14 @@ function buildPerSpeakerVoiceConversion(segments, totalMs, nodes) {
|
|
|
33488
33496
|
total_ms: totalMs
|
|
33489
33497
|
}
|
|
33490
33498
|
});
|
|
33491
|
-
|
|
33492
|
-
|
|
33493
|
-
|
|
33494
|
-
|
|
33495
|
-
|
|
33496
|
-
|
|
33499
|
+
if (!keepRenderedVoice) {
|
|
33500
|
+
nodes.push({
|
|
33501
|
+
id: convId,
|
|
33502
|
+
type: "audio_voice_convert",
|
|
33503
|
+
inputs: { audio: `$ref:${trackId}.audio`, voice_ref: `$ref:${voiceNode}.voice_id` },
|
|
33504
|
+
params: { model: FIXED_VOICE_CONVERT_MODEL, voice: "{{voice_ref}}" }
|
|
33505
|
+
});
|
|
33506
|
+
}
|
|
33497
33507
|
tracks.push({ slot: convId, ref: `$ref:${convId}.audio`, start_s: 0, duration_s: totalMs / 1e3, kind: "vo" });
|
|
33498
33508
|
}
|
|
33499
33509
|
return tracks;
|
|
@@ -34633,7 +34643,8 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
34633
34643
|
graphicScenes: fullFrameGraphicScenes(blueprint),
|
|
34634
34644
|
surfaceIngests: /* @__PURE__ */ new Map(),
|
|
34635
34645
|
ttsLanguageCode: deriveTtsLanguageCode(blueprint),
|
|
34636
|
-
spokenAccent: blueprint.global?.voiceover?.accent?.trim() || void 0
|
|
34646
|
+
spokenAccent: blueprint.global?.voiceover?.accent?.trim() || void 0,
|
|
34647
|
+
keepRenderedVoice: opts.keepRenderedVoice
|
|
34637
34648
|
};
|
|
34638
34649
|
const out = {
|
|
34639
34650
|
routes: /* @__PURE__ */ new Map(),
|
|
@@ -34690,7 +34701,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
34690
34701
|
prevEndFrame = emitBrollScene(scene, i, i === lastIndex, env, nodes, out, prevEndFrame);
|
|
34691
34702
|
});
|
|
34692
34703
|
const totalMs = Math.round(env.clock.totalS * 1e3);
|
|
34693
|
-
out.voTracks.push(...buildPerSpeakerVoiceConversion(out.nativeSegments, totalMs, nodes));
|
|
34704
|
+
out.voTracks.push(...buildPerSpeakerVoiceConversion(out.nativeSegments, totalMs, nodes, env.keepRenderedVoice));
|
|
34694
34705
|
return {
|
|
34695
34706
|
clips: out.clips,
|
|
34696
34707
|
voTracks: out.voTracks,
|
|
@@ -35128,6 +35139,18 @@ function emitCaptionChain(blueprint, tracks, clock, opts, videoRef, nodes) {
|
|
|
35128
35139
|
});
|
|
35129
35140
|
return { videoRef: "$ref:captions.video", videoNode: "captions" };
|
|
35130
35141
|
}
|
|
35142
|
+
function dropUncastVoices(nodes) {
|
|
35143
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
35144
|
+
for (const node of nodes) {
|
|
35145
|
+
for (const ref of Object.values(node.inputs ?? {})) {
|
|
35146
|
+
if (typeof ref === "string" && ref.startsWith("$ref:")) referenced.add(ref.slice(5).split(".")[0] ?? "");
|
|
35147
|
+
}
|
|
35148
|
+
}
|
|
35149
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
35150
|
+
const node = nodes[i];
|
|
35151
|
+
if (node && node.type === "voice_select" && !referenced.has(node.id)) nodes.splice(i, 1);
|
|
35152
|
+
}
|
|
35153
|
+
}
|
|
35131
35154
|
function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
35132
35155
|
const blueprint = VideoBlueprint.parse(input);
|
|
35133
35156
|
injectHookPhysicality(blueprint);
|
|
@@ -35241,6 +35264,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
35241
35264
|
...aspectRemapTodo(resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel))),
|
|
35242
35265
|
model_constraints: buildModelConstraints()
|
|
35243
35266
|
};
|
|
35267
|
+
dropUncastVoices(nodes);
|
|
35244
35268
|
return {
|
|
35245
35269
|
schema: "baker-canvas/1",
|
|
35246
35270
|
metadata: {
|
|
@@ -36362,6 +36386,14 @@ var RENDER_REVIEW_MODEL = "gemini-pro-latest";
|
|
|
36362
36386
|
var RENDER_REVIEW_FRAMES = 12;
|
|
36363
36387
|
var RENDER_REVIEW_WIDTH = 1024;
|
|
36364
36388
|
var RENDER_WATCH_MODEL = "gemini-3.7-flash";
|
|
36389
|
+
function usageFrom(model, body) {
|
|
36390
|
+
const meta = body?.usageMetadata;
|
|
36391
|
+
if (!meta) return null;
|
|
36392
|
+
const inputTokens = meta.promptTokenCount ?? 0;
|
|
36393
|
+
const outputTokens = meta.candidatesTokenCount ?? 0;
|
|
36394
|
+
if (inputTokens === 0 && outputTokens === 0) return null;
|
|
36395
|
+
return { model, inputTokens, outputTokens };
|
|
36396
|
+
}
|
|
36365
36397
|
var WATCH_MAX_BYTES = 60 * 1024 * 1024;
|
|
36366
36398
|
function sceneAt(windows, atS) {
|
|
36367
36399
|
return windows.find((w) => atS >= w.startS && atS < w.endS)?.file;
|
|
@@ -36623,6 +36655,12 @@ function videoPathFromOutput(output) {
|
|
|
36623
36655
|
}
|
|
36624
36656
|
return found;
|
|
36625
36657
|
}
|
|
36658
|
+
function parseVolume(volumedetect, key) {
|
|
36659
|
+
const raw = new RegExp(`${key}:\\s*(-?(?:inf|[\\d.]+)) dB`).exec(volumedetect)?.[1];
|
|
36660
|
+
if (raw === void 0) return Number.NaN;
|
|
36661
|
+
if (raw === "-inf") return Number.NEGATIVE_INFINITY;
|
|
36662
|
+
return Number.parseFloat(raw);
|
|
36663
|
+
}
|
|
36626
36664
|
function classifyAudio(volumedetect, hasAudioStream) {
|
|
36627
36665
|
if (!hasAudioStream) {
|
|
36628
36666
|
return [
|
|
@@ -36632,9 +36670,17 @@ function classifyAudio(volumedetect, hasAudioStream) {
|
|
|
36632
36670
|
}
|
|
36633
36671
|
];
|
|
36634
36672
|
}
|
|
36635
|
-
const mean =
|
|
36636
|
-
const max =
|
|
36673
|
+
const mean = parseVolume(volumedetect, "mean_volume");
|
|
36674
|
+
const max = parseVolume(volumedetect, "max_volume");
|
|
36637
36675
|
const findings = [];
|
|
36676
|
+
if (mean === Number.NEGATIVE_INFINITY || max === Number.NEGATIVE_INFINITY) {
|
|
36677
|
+
return [
|
|
36678
|
+
{
|
|
36679
|
+
severity: "blocking",
|
|
36680
|
+
what: "The finished video has an audio track, but it is completely silent \u2014 digital zero from start to end. An ad that plays silent in a feed is not an ad."
|
|
36681
|
+
}
|
|
36682
|
+
];
|
|
36683
|
+
}
|
|
36638
36684
|
if (Number.isFinite(mean) && mean < -30) {
|
|
36639
36685
|
findings.push({
|
|
36640
36686
|
severity: "warning",
|
|
@@ -36692,7 +36738,7 @@ async function videoDuration(video) {
|
|
|
36692
36738
|
return null;
|
|
36693
36739
|
}
|
|
36694
36740
|
}
|
|
36695
|
-
async function askVideo(video, prompt) {
|
|
36741
|
+
async function askVideo(video, prompt, spent) {
|
|
36696
36742
|
const key = frameVisionKey();
|
|
36697
36743
|
if (!key) return null;
|
|
36698
36744
|
try {
|
|
@@ -36715,13 +36761,15 @@ async function askVideo(video, prompt) {
|
|
|
36715
36761
|
);
|
|
36716
36762
|
if (!response.ok) return null;
|
|
36717
36763
|
const body = await response.json();
|
|
36764
|
+
const usage = usageFrom(RENDER_WATCH_MODEL, body);
|
|
36765
|
+
if (usage) spent(usage);
|
|
36718
36766
|
const text2 = body.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("");
|
|
36719
36767
|
return text2 ? parseReviewJson(text2) : null;
|
|
36720
36768
|
} catch {
|
|
36721
36769
|
return null;
|
|
36722
36770
|
}
|
|
36723
36771
|
}
|
|
36724
|
-
async function ask(images, prompt) {
|
|
36772
|
+
async function ask(images, prompt, spent) {
|
|
36725
36773
|
const key = frameVisionKey();
|
|
36726
36774
|
if (!key) return null;
|
|
36727
36775
|
const parts = [{ text: prompt }];
|
|
@@ -36738,6 +36786,8 @@ async function ask(images, prompt) {
|
|
|
36738
36786
|
);
|
|
36739
36787
|
if (!response.ok) return null;
|
|
36740
36788
|
const body = await response.json();
|
|
36789
|
+
const usage = usageFrom(RENDER_REVIEW_MODEL, body);
|
|
36790
|
+
if (usage) spent(usage);
|
|
36741
36791
|
const text2 = body.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("");
|
|
36742
36792
|
return text2 ? parseReviewJson(text2) : null;
|
|
36743
36793
|
} catch {
|
|
@@ -36748,6 +36798,7 @@ async function reviewRenderedVideo(opts) {
|
|
|
36748
36798
|
if (!hasFrameVisionKey()) return null;
|
|
36749
36799
|
const duration = await videoDuration(opts.video);
|
|
36750
36800
|
if (duration === null) return null;
|
|
36801
|
+
const spent = (usage) => opts.onSpend?.(usage);
|
|
36751
36802
|
const dir = await mkdtemp(path15.join(tmpdir(), "baker-review-"));
|
|
36752
36803
|
try {
|
|
36753
36804
|
const grabbed = [];
|
|
@@ -36762,17 +36813,22 @@ async function reviewRenderedVideo(opts) {
|
|
|
36762
36813
|
if (grabbed.length === 0) return null;
|
|
36763
36814
|
const reel = await ask(
|
|
36764
36815
|
grabbed.map((f) => f.image),
|
|
36765
|
-
buildReelReviewPrompt({ subject: opts.subject, script: opts.script })
|
|
36816
|
+
buildReelReviewPrompt({ subject: opts.subject, script: opts.script }),
|
|
36817
|
+
spent
|
|
36766
36818
|
);
|
|
36767
36819
|
const framePrompt = buildFrameReviewPrompt({ subject: opts.subject });
|
|
36768
36820
|
const perFrame = await mapWithLimit(grabbed, REVIEW_CONCURRENCY, async (f) => ({
|
|
36769
36821
|
at: f.at,
|
|
36770
|
-
json: await ask([f.image], framePrompt)
|
|
36822
|
+
json: await ask([f.image], framePrompt, spent)
|
|
36771
36823
|
}));
|
|
36772
36824
|
const unread = perFrame.filter((f) => f.json === null).length;
|
|
36773
36825
|
if (unread === perFrame.length && reel === null) return null;
|
|
36774
|
-
const watch = await askVideo(
|
|
36775
|
-
|
|
36826
|
+
const watch = await askVideo(
|
|
36827
|
+
opts.video,
|
|
36828
|
+
buildWatchReviewPrompt({ market: opts.market, script: opts.script }),
|
|
36829
|
+
spent
|
|
36830
|
+
);
|
|
36831
|
+
const fidelity = opts.brief?.trim() ? await askVideo(opts.video, buildFidelityPrompt(opts.brief), spent) : null;
|
|
36776
36832
|
const findings = [
|
|
36777
36833
|
...classifyFrameFindings(perFrame, opts.windows ?? []),
|
|
36778
36834
|
...classifyReelFindings(reel),
|
|
@@ -36782,12 +36838,15 @@ async function reviewRenderedVideo(opts) {
|
|
|
36782
36838
|
// Measured, not judged: a model looking at frames cannot hear the mix.
|
|
36783
36839
|
...await measureAudio(opts.video)
|
|
36784
36840
|
];
|
|
36785
|
-
|
|
36786
|
-
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
|
|
36790
|
-
|
|
36841
|
+
findings.push(
|
|
36842
|
+
...unreviewedNotes({
|
|
36843
|
+
watched: watch !== null,
|
|
36844
|
+
fidelityAsked: Boolean(opts.brief?.trim()),
|
|
36845
|
+
fidelityRead: fidelity !== null,
|
|
36846
|
+
unread,
|
|
36847
|
+
frames: perFrame.length
|
|
36848
|
+
})
|
|
36849
|
+
);
|
|
36791
36850
|
return findings;
|
|
36792
36851
|
} finally {
|
|
36793
36852
|
await rm5(dir, { recursive: true, force: true });
|
|
@@ -36811,6 +36870,28 @@ async function sceneWindowsBeside(canvasPath) {
|
|
|
36811
36870
|
}
|
|
36812
36871
|
return windows;
|
|
36813
36872
|
}
|
|
36873
|
+
function unreviewedNotes(state) {
|
|
36874
|
+
const notes = [];
|
|
36875
|
+
if (!state.watched) {
|
|
36876
|
+
notes.push({
|
|
36877
|
+
severity: "warning",
|
|
36878
|
+
what: "Nobody watched the finished video end to end \u2014 the accent, the voice against the mouth and the cuts are unchecked. The frame-by-frame review still ran."
|
|
36879
|
+
});
|
|
36880
|
+
}
|
|
36881
|
+
if (state.fidelityAsked && !state.fidelityRead) {
|
|
36882
|
+
notes.push({
|
|
36883
|
+
severity: "warning",
|
|
36884
|
+
what: "The video was not checked against what you asked for \u2014 only against whether it is well made. A well-made ad of the wrong brief passes every other check here."
|
|
36885
|
+
});
|
|
36886
|
+
}
|
|
36887
|
+
if (state.unread > 0) {
|
|
36888
|
+
notes.push({
|
|
36889
|
+
severity: "warning",
|
|
36890
|
+
what: `${state.unread} of ${state.frames} frames could not be reviewed \u2014 that part of the video is unchecked.`
|
|
36891
|
+
});
|
|
36892
|
+
}
|
|
36893
|
+
return notes;
|
|
36894
|
+
}
|
|
36814
36895
|
|
|
36815
36896
|
// src/commands/canvas/run.ts
|
|
36816
36897
|
var runCommand = defineCommand107({
|
|
@@ -37125,10 +37206,12 @@ ${describeRewrites(healed.rewrites)}
|
|
|
37125
37206
|
`));
|
|
37126
37207
|
}
|
|
37127
37208
|
const reviewable = videoPathFromOutput(result.output);
|
|
37209
|
+
const reviewUsage = [];
|
|
37128
37210
|
const review = reviewable ? await reviewRenderedVideo({
|
|
37129
37211
|
video: reviewable,
|
|
37130
37212
|
windows: await sceneWindowsBeside(filePath),
|
|
37131
|
-
brief: opts.brief?.trim() || void 0
|
|
37213
|
+
brief: opts.brief?.trim() || void 0,
|
|
37214
|
+
onSpend: (usage) => reviewUsage.push(usage)
|
|
37132
37215
|
}) : null;
|
|
37133
37216
|
const hints2 = reviewable ? review === null ? [RENDER_REVIEW_UNAVAILABLE] : review.length === 0 ? ["Reviewed the finished video frame by frame \u2014 nothing to report."] : [
|
|
37134
37217
|
`Reviewed the finished video and found ${review.length} thing${review.length === 1 ? "" : "s"} to fix. Show the user what is wrong before you show them the ad.`,
|
|
@@ -37142,7 +37225,12 @@ ${describeRewrites(healed.rewrites)}
|
|
|
37142
37225
|
// `review` is null only when nobody looked (no key, no ffmpeg, unreadable video);
|
|
37143
37226
|
// an empty array is a render that was reviewed and came back clean. The record
|
|
37144
37227
|
// has to keep those apart.
|
|
37145
|
-
buildRunRecord(
|
|
37228
|
+
buildRunRecord(
|
|
37229
|
+
result,
|
|
37230
|
+
{ ...recordMeta, ...review ? { review } : {}, ...reviewUsage.length > 0 ? { reviewUsage } : {} },
|
|
37231
|
+
progress?.planInfo(),
|
|
37232
|
+
finalLabels
|
|
37233
|
+
)
|
|
37146
37234
|
);
|
|
37147
37235
|
}
|
|
37148
37236
|
process.stdout.write(
|
|
@@ -38468,6 +38556,7 @@ var CURRENCY_BY_MARKET = {
|
|
|
38468
38556
|
"the united kingdom": "\xA3 (pounds sterling)",
|
|
38469
38557
|
"the united states": "$ (US dollars)"
|
|
38470
38558
|
};
|
|
38559
|
+
var MARKERS_TO_NAME_A_PLACE = 3;
|
|
38471
38560
|
var MARKET_BY_LANGUAGE = {
|
|
38472
38561
|
es: "Spain",
|
|
38473
38562
|
"es-es": "Spain",
|
|
@@ -38506,6 +38595,9 @@ var SCRIPT_LANGUAGE_MARKERS = [
|
|
|
38506
38595
|
["english", /\b(the|a|an|of|to|for|with|your|more|already|but|because|until|without)\b/gi]
|
|
38507
38596
|
];
|
|
38508
38597
|
function detectScriptLanguage(lines) {
|
|
38598
|
+
return bestScriptLanguage(lines)?.name;
|
|
38599
|
+
}
|
|
38600
|
+
function bestScriptLanguage(lines) {
|
|
38509
38601
|
const text2 = lines.join(" ");
|
|
38510
38602
|
if (text2.trim().length < 12) return void 0;
|
|
38511
38603
|
let best = null;
|
|
@@ -38513,7 +38605,7 @@ function detectScriptLanguage(lines) {
|
|
|
38513
38605
|
const hits = (text2.match(re2) ?? []).length;
|
|
38514
38606
|
if (hits > 0 && (!best || hits > best.hits)) best = { name, hits };
|
|
38515
38607
|
}
|
|
38516
|
-
return best
|
|
38608
|
+
return best ?? void 0;
|
|
38517
38609
|
}
|
|
38518
38610
|
function voiceLanguageFor(spec) {
|
|
38519
38611
|
const declared = spec.voice?.language?.trim().toLowerCase();
|
|
@@ -38526,12 +38618,19 @@ function voiceLanguageFor(spec) {
|
|
|
38526
38618
|
function marketFor(spec) {
|
|
38527
38619
|
if (spec.market) return spec.market;
|
|
38528
38620
|
const lang = spec.voice?.language?.trim().toLowerCase();
|
|
38529
|
-
|
|
38530
|
-
|
|
38531
|
-
|
|
38621
|
+
const declared = lang ? MARKET_BY_LANGUAGE[lang] : void 0;
|
|
38622
|
+
if (declared) return declared;
|
|
38623
|
+
const read = bestScriptLanguage(spec.beats.map((b) => b.say));
|
|
38624
|
+
if (!read || read.hits < MARKERS_TO_NAME_A_PLACE) return void 0;
|
|
38625
|
+
const detected = read.name;
|
|
38626
|
+
if (detected.toLowerCase() === "english") return void 0;
|
|
38627
|
+
const code = detected ? Object.entries(VOICE_LANGUAGE_NAME).find(([, name]) => name.toLowerCase() === detected.toLowerCase())?.[0] : void 0;
|
|
38628
|
+
return code ? MARKET_BY_LANGUAGE[code] : void 0;
|
|
38629
|
+
}
|
|
38630
|
+
function beatSeconds(line, videoModel) {
|
|
38532
38631
|
const words2 = line.trim().split(/\s+/).length;
|
|
38533
38632
|
const spoken = words2 / 3.06;
|
|
38534
|
-
const allowed = [...SEEDANCE_DURATIONS].sort((a, b) => a - b);
|
|
38633
|
+
const allowed = (videoModel ? clipDurationsFor(videoModel) : void 0) ?? [...SEEDANCE_DURATIONS].sort((a, b) => a - b);
|
|
38535
38634
|
return allowed.find((d) => d >= spoken) ?? allowed[allowed.length - 1];
|
|
38536
38635
|
}
|
|
38537
38636
|
var BRAND_PLATE = "A solid flat brand colour plate \u2014 an end card. No people, no objects, no scenery, no text of any kind.";
|
|
@@ -38560,18 +38659,21 @@ function castedAvatarOnRoster(ad, roster) {
|
|
|
38560
38659
|
if (ad.cast?.avatar) return null;
|
|
38561
38660
|
const haystack = [ad.cast?.description ?? "", ...(ad.beats ?? []).map((b) => b.show ?? "")].join(" ");
|
|
38562
38661
|
if (!haystack.trim()) return null;
|
|
38563
|
-
const
|
|
38564
|
-
haystack.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean)
|
|
38565
|
-
);
|
|
38662
|
+
const written = new Set(haystack.split(/[^\p{L}\p{N}]+/u).filter(Boolean));
|
|
38566
38663
|
for (const person of roster) {
|
|
38567
|
-
|
|
38568
|
-
const token = label.trim().toLowerCase();
|
|
38569
|
-
if (!token || token.includes("-") || token.length < 3) continue;
|
|
38570
|
-
if (words2.has(token)) return person.handle;
|
|
38571
|
-
}
|
|
38664
|
+
if (labelWrittenAsName(person.name, written) || labelWrittenAsName(person.handle, written)) return person.handle;
|
|
38572
38665
|
}
|
|
38573
38666
|
return null;
|
|
38574
38667
|
}
|
|
38668
|
+
function labelWrittenAsName(label, written) {
|
|
38669
|
+
const token = label.trim();
|
|
38670
|
+
if (!token || token.includes("-") || token.length < 3) return false;
|
|
38671
|
+
const folded = token.toLowerCase();
|
|
38672
|
+
for (const word of written) {
|
|
38673
|
+
if (word.toLowerCase() === folded && word[0] === word[0]?.toUpperCase()) return true;
|
|
38674
|
+
}
|
|
38675
|
+
return false;
|
|
38676
|
+
}
|
|
38575
38677
|
function adSpecCastElements(spec, avatar) {
|
|
38576
38678
|
const lastIndex = spec.beats.length - 1;
|
|
38577
38679
|
const speakingScenes = spec.beats.map((_, i) => i).filter((i) => !(endCardWanted(spec) && i === lastIndex));
|
|
@@ -38613,11 +38715,11 @@ function motionPrompt(beat, toCamera, isClosingCard, place, physics) {
|
|
|
38613
38715
|
if (toCamera) return shot;
|
|
38614
38716
|
return `${shot} Nobody in frame is speaking: mouths CLOSED and STILL throughout, no talking, no lip movement. Whoever is in shot is looking, reacting or working \u2014 never addressing the camera.`;
|
|
38615
38717
|
}
|
|
38616
|
-
function staticTranscript(spec) {
|
|
38718
|
+
function staticTranscript(spec, videoModel) {
|
|
38617
38719
|
const out = [];
|
|
38618
38720
|
let clock = 0;
|
|
38619
38721
|
for (const beat of spec.beats) {
|
|
38620
|
-
const duration = beatSeconds(beat.say);
|
|
38722
|
+
const duration = beatSeconds(beat.say, videoModel);
|
|
38621
38723
|
const words2 = beat.say.trim().split(/\s+/).filter(Boolean);
|
|
38622
38724
|
const each = words2.length > 0 ? duration / words2.length : duration;
|
|
38623
38725
|
words2.forEach((text2, i) => {
|
|
@@ -38683,9 +38785,12 @@ function voiceoverMode(spec, nativeSpeech) {
|
|
|
38683
38785
|
function castRole(spec) {
|
|
38684
38786
|
if (!spec.cast) return void 0;
|
|
38685
38787
|
const described = spec.cast.description?.trim();
|
|
38686
|
-
|
|
38788
|
+
if (!described) return "a customer telling their own story, not a worker";
|
|
38789
|
+
return NAMES_A_JOB.test(described) ? described : `${described} \u2014 a customer telling their own story, not a worker`;
|
|
38687
38790
|
}
|
|
38688
|
-
|
|
38791
|
+
var NAMES_A_JOB = /\b(installer|instalador[ao]?|técnic[oa]|tecnic[oa]|technician|engineer|ingenier[oa]|employee|emplead[oa]|worker|obrer[oa]|staff|clinician|doctor|médic[oa]|medic[oa]|nurse|enfermer[oa]|chef|cocioner[oa]|cociner[oa]|barista|stylist|estilista|trainer|entrenador[ao]?|founder|fundador[ao]?|owner|dueñ[oa]|our team|nuestro equipo|on site|en obra)\b/i;
|
|
38792
|
+
function adSpecToBlueprint(spec, opts = {}) {
|
|
38793
|
+
const avatarAccent = opts.avatarAccent;
|
|
38689
38794
|
const total = spec.beats.length;
|
|
38690
38795
|
const market = marketFor(spec);
|
|
38691
38796
|
const place = market ? ` Set in ${market}: the architecture, streets and styling are ${market}'s.` : "";
|
|
@@ -38696,7 +38801,7 @@ function adSpecToBlueprint(spec, avatarAccent) {
|
|
|
38696
38801
|
let clock = 0;
|
|
38697
38802
|
const scenes = spec.beats.map((beat, i) => {
|
|
38698
38803
|
const isClosingCard = closesOnCard && i === total - 1;
|
|
38699
|
-
const duration = beatSeconds(beat.say);
|
|
38804
|
+
const duration = beatSeconds(beat.say, opts.videoModel);
|
|
38700
38805
|
const start = clock;
|
|
38701
38806
|
clock += duration;
|
|
38702
38807
|
return {
|
|
@@ -39167,6 +39272,18 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39167
39272
|
avatarError = `Could not read avatar \`${handle}\`: ${e instanceof Error ? e.message : String(e)}. \`baker avatars list\` shows who there is.`;
|
|
39168
39273
|
}
|
|
39169
39274
|
}
|
|
39275
|
+
if (avatarError && !spec.data.cast?.description?.trim()) {
|
|
39276
|
+
writeJson({
|
|
39277
|
+
ok: false,
|
|
39278
|
+
error: {
|
|
39279
|
+
code: "VALIDATION_ERROR",
|
|
39280
|
+
message: avatarError,
|
|
39281
|
+
fix: `This ad casts \`${handle}\` and describes nobody else, so without them every shot renders a person nobody chose. Wait for the avatar and re-run \u2014 \`baker avatars get ${handle} --wait\` returns the moment it is ready \u2014 or cast someone who is (\`baker avatars list\`). To build it with an invented person instead, put a written description in \`cast.description\`.`
|
|
39282
|
+
}
|
|
39283
|
+
});
|
|
39284
|
+
process.exit(1);
|
|
39285
|
+
return;
|
|
39286
|
+
}
|
|
39170
39287
|
const fromWorkspace = await readBrandFromWorkspace();
|
|
39171
39288
|
const filledLogo = !spec.data.brand?.logo && fromWorkspace.logo ? fromWorkspace.logo : void 0;
|
|
39172
39289
|
const filledPalette = !spec.data.brand?.palette?.length && fromWorkspace.palette?.length ? fromWorkspace.palette : void 0;
|
|
@@ -39245,7 +39362,7 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39245
39362
|
process.exit(1);
|
|
39246
39363
|
return;
|
|
39247
39364
|
}
|
|
39248
|
-
const blueprint = adSpecToBlueprint(spec.data, avatar?.profile?.accent);
|
|
39365
|
+
const blueprint = adSpecToBlueprint(spec.data, { avatarAccent: avatar?.profile?.accent, videoModel: chosenVideoModel });
|
|
39249
39366
|
const slug = args.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
|
|
39250
39367
|
const outPath = args.out ?? (args.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));
|
|
39251
39368
|
const outDir = path24.dirname(outPath);
|
|
@@ -39258,10 +39375,10 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39258
39375
|
const blueprintStylePath = path24.join(outDir, "prompt.style.json");
|
|
39259
39376
|
await writeSceneFiles(outDir, blueprint);
|
|
39260
39377
|
await writeFile9(blueprintStylePath, renderStyleProjectionFromValue(blueprint), "utf8");
|
|
39261
|
-
const logoPath = spec.data.brand?.logo?.trim();
|
|
39378
|
+
const logoPath = noBrand ? void 0 : spec.data.brand?.logo?.trim();
|
|
39262
39379
|
const transcriptPath = path24.join(outDir, "transcript.json");
|
|
39263
39380
|
if (spec.data.voiceover === false) {
|
|
39264
|
-
await writeFile9(transcriptPath, `${JSON.stringify(staticTranscript(spec.data), null, 2)}
|
|
39381
|
+
await writeFile9(transcriptPath, `${JSON.stringify(staticTranscript(spec.data, chosenVideoModel), null, 2)}
|
|
39265
39382
|
`, "utf-8");
|
|
39266
39383
|
}
|
|
39267
39384
|
const opts = {
|
|
@@ -39290,6 +39407,12 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39290
39407
|
// its accent. The clip speaks in the voice the video model renders, and the region is
|
|
39291
39408
|
// directed in the prompt, which is the only place a pronunciation can come from.
|
|
39292
39409
|
...avatar?.profile?.accent?.trim() ? { spokenAccent: avatar.profile.accent.trim() } : {},
|
|
39410
|
+
// Same decision, one step further: with an avatar on camera the clip's own voice is
|
|
39411
|
+
// kept, not re-voiced. The re-voice is the speech-to-speech route ADR 0005 measured
|
|
39412
|
+
// and rejected — it would swap the voice that matches the mouth and the directed
|
|
39413
|
+
// region for one cast off an English stock list. Only the on-camera voice; a
|
|
39414
|
+
// voiceover read has no rendered voice to keep and is still cast.
|
|
39415
|
+
...avatar ? { keepRenderedVoice: true } : {},
|
|
39293
39416
|
...spec.data.voiceover === false ? { staticTranscriptPath: path24.relative(outDir, transcriptPath) } : {}
|
|
39294
39417
|
};
|
|
39295
39418
|
const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data, avatar), opts);
|
|
@@ -39305,7 +39428,7 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39305
39428
|
}
|
|
39306
39429
|
}
|
|
39307
39430
|
const closing = blueprint.scenes[blueprint.scenes.length - 1];
|
|
39308
|
-
const overlayHtml = endCardWanted(spec.data) && closing && renderedDir ? buildAdBrandOverlay({
|
|
39431
|
+
const overlayHtml = !noBrand && endCardWanted(spec.data) && closing && renderedDir ? buildAdBrandOverlay({
|
|
39309
39432
|
logo: staged,
|
|
39310
39433
|
cta: endCardCta(spec.data),
|
|
39311
39434
|
cardAtS: closing.start_s,
|
|
@@ -50878,6 +51001,20 @@ function blankAstroFrontmatter(text2) {
|
|
|
50878
51001
|
}
|
|
50879
51002
|
return kept.join("") + text2.slice(cut);
|
|
50880
51003
|
}
|
|
51004
|
+
function blankUnrenderedSource(text2) {
|
|
51005
|
+
return blankTemplateComments(blankAstroFrontmatter(text2));
|
|
51006
|
+
}
|
|
51007
|
+
function blankTemplateComments(text2) {
|
|
51008
|
+
const out = text2.split("");
|
|
51009
|
+
const pattern = /\{\s*\/\*[\s\S]*?\*\/\s*\}|<!--[\s\S]*?-->/g;
|
|
51010
|
+
for (const m of text2.matchAll(pattern)) {
|
|
51011
|
+
const start = m.index ?? 0;
|
|
51012
|
+
for (let i = start; i < start + m[0].length && i < out.length; i++) {
|
|
51013
|
+
if (out[i] !== "\n") out[i] = " ";
|
|
51014
|
+
}
|
|
51015
|
+
}
|
|
51016
|
+
return out.join("");
|
|
51017
|
+
}
|
|
50881
51018
|
function frontmatterLength(text2) {
|
|
50882
51019
|
if (!text2.startsWith("---")) return 0;
|
|
50883
51020
|
const lines = text2.split("\n");
|
|
@@ -51251,7 +51388,7 @@ function distinctCompetitorNames(competitors) {
|
|
|
51251
51388
|
return [...byFold.values()];
|
|
51252
51389
|
}
|
|
51253
51390
|
function mentionsInSource(source, names) {
|
|
51254
|
-
const text2 =
|
|
51391
|
+
const text2 = blankUnrenderedSource(source.text);
|
|
51255
51392
|
const visible = blankHtmlKeepingOffsets(text2);
|
|
51256
51393
|
const tables = tableRanges(text2);
|
|
51257
51394
|
const out = [];
|
|
@@ -51291,7 +51428,7 @@ function escapeRegExp(value) {
|
|
|
51291
51428
|
}
|
|
51292
51429
|
function detectSource(source) {
|
|
51293
51430
|
const file = source.path;
|
|
51294
|
-
const text2 =
|
|
51431
|
+
const text2 = blankUnrenderedSource(source.text);
|
|
51295
51432
|
const findings = [];
|
|
51296
51433
|
const lines = text2.split("\n");
|
|
51297
51434
|
for (const matcher of LINE_MATCHERS) {
|
|
@@ -51309,7 +51446,7 @@ function detectSource(source) {
|
|
|
51309
51446
|
}
|
|
51310
51447
|
function detectPage(sources) {
|
|
51311
51448
|
if (sources.length === 0) return [];
|
|
51312
|
-
const combined = sources.map((s) =>
|
|
51449
|
+
const combined = sources.map((s) => blankUnrenderedSource(s.text)).join("\n");
|
|
51313
51450
|
const pageFile = sources.find((s) => s.path === "index.astro" || s.path.endsWith("/index.astro"))?.path ?? sources[0]?.path ?? "index.astro";
|
|
51314
51451
|
const findings = [];
|
|
51315
51452
|
for (const analyzer of ANALYZERS) {
|
|
@@ -51389,7 +51526,7 @@ function describeCounts(findings) {
|
|
|
51389
51526
|
// src/commands/landing/snapshot.ts
|
|
51390
51527
|
import { mkdir as mkdir10, rename as rename2, writeFile as writeFile14 } from "fs/promises";
|
|
51391
51528
|
import path34 from "path";
|
|
51392
|
-
var CRITIC_VERSION = "
|
|
51529
|
+
var CRITIC_VERSION = "4";
|
|
51393
51530
|
function critiqueCacheDir(projectRoot) {
|
|
51394
51531
|
return path34.join(projectRoot, ".cache", "landing-critique");
|
|
51395
51532
|
}
|