@koda-sl/baker-cli 0.301.0-dev.50ab1c583 → 0.305.0-dev.2c645fe6b
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-SSXER76B.js → chunk-7TL4VPK5.js} +118 -14
- package/dist/chunk-7TL4VPK5.js.map +1 -0
- package/dist/cli.js +207 -61
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-SSXER76B.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
YtDlpError,
|
|
23
23
|
bestSupportedQuality,
|
|
24
24
|
canonicalUrl,
|
|
25
|
+
clipAudioMode,
|
|
26
|
+
clipDurationsFor,
|
|
25
27
|
clipParamRecipe,
|
|
26
28
|
clipProfileFor,
|
|
27
29
|
collectAssetRefLikes,
|
|
@@ -63,7 +65,7 @@ import {
|
|
|
63
65
|
ulid,
|
|
64
66
|
validateCanvasDeep,
|
|
65
67
|
ytDlpBlockSignal
|
|
66
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-7TL4VPK5.js";
|
|
67
69
|
import {
|
|
68
70
|
csvOrJson,
|
|
69
71
|
daysAgoIso,
|
|
@@ -2380,7 +2382,6 @@ var IMAGE_ASSET_LINK_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
|
2380
2382
|
"PORTRAIT_MARKETING_IMAGE",
|
|
2381
2383
|
"AD_IMAGE"
|
|
2382
2384
|
]);
|
|
2383
|
-
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.";
|
|
2384
2385
|
function isImageAssetLinkFieldType(fieldType) {
|
|
2385
2386
|
return IMAGE_ASSET_LINK_FIELD_TYPES.has(fieldType);
|
|
2386
2387
|
}
|
|
@@ -3384,7 +3385,6 @@ var assetLinkAttachSchema = z5.object({
|
|
|
3384
3385
|
if (value.fieldType === "BUSINESS_LOGO" && value.level === "adGroup") {
|
|
3385
3386
|
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["fieldType"], message: BUSINESS_LOGO_LEVEL_MESSAGE });
|
|
3386
3387
|
} else if (isImageAssetLinkFieldType(value.fieldType)) {
|
|
3387
|
-
ctx.addIssue({ code: z5.ZodIssueCode.custom, path: ["fieldType"], message: IMAGE_ASSET_LINK_MESSAGE });
|
|
3388
3388
|
} else if (!ASSET_FIELD_TYPES.includes(value.fieldType)) {
|
|
3389
3389
|
ctx.addIssue({
|
|
3390
3390
|
code: z5.ZodIssueCode.custom,
|
|
@@ -4464,7 +4464,13 @@ var avatarsCreateRequestSchema = z10.object({
|
|
|
4464
4464
|
sourceImageIds: z10.array(z10.string().min(1)).max(6).optional(),
|
|
4465
4465
|
/** The company's statement that it may use this person's likeness in ads. */
|
|
4466
4466
|
likenessConfirmed: z10.boolean().optional(),
|
|
4467
|
-
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()
|
|
4468
4474
|
}).refine((body) => !body.sourceImageIds?.length || body.likenessConfirmed === true, {
|
|
4469
4475
|
message: "likenessConfirmed is required when building an avatar from photographs of a real person",
|
|
4470
4476
|
path: ["likenessConfirmed"]
|
|
@@ -16592,8 +16598,8 @@ function imageAssetHints(payload) {
|
|
|
16592
16598
|
return [];
|
|
16593
16599
|
}
|
|
16594
16600
|
return [
|
|
16595
|
-
"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
|
|
16596
|
-
"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`.)"
|
|
16597
16603
|
];
|
|
16598
16604
|
}
|
|
16599
16605
|
var assetsUpdateCommand = defineCommand31({
|
|
@@ -16636,7 +16642,7 @@ var assetsUpdateCommand = defineCommand31({
|
|
|
16636
16642
|
var assetsCommand = defineCommand31({
|
|
16637
16643
|
meta: {
|
|
16638
16644
|
name: "assets",
|
|
16639
|
-
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."
|
|
16640
16646
|
},
|
|
16641
16647
|
subCommands: {
|
|
16642
16648
|
create: fileCreateCommand(
|
|
@@ -16647,7 +16653,7 @@ var assetsCommand = defineCommand31({
|
|
|
16647
16653
|
),
|
|
16648
16654
|
update: assetsUpdateCommand,
|
|
16649
16655
|
attach: fileCreateCommand("attach", "google.assetLink.attach", "Attach an asset to a campaign/adGroup/customer", [
|
|
16650
|
-
"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`)."
|
|
16651
16657
|
]),
|
|
16652
16658
|
detach: statusCommand2("google.assetLink.detach", "asset link")
|
|
16653
16659
|
}
|
|
@@ -30245,7 +30251,10 @@ var createCommand2 = defineCommand96({
|
|
|
30245
30251
|
...sourceImageIds ? { sourceImageIds, likenessConfirmed: true } : {},
|
|
30246
30252
|
...args["voice-id"] ? { voiceId: args["voice-id"] } : {},
|
|
30247
30253
|
...args["voice-description"] ? { voiceDescription: args["voice-description"] } : {},
|
|
30248
|
-
...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
|
|
30249
30258
|
};
|
|
30250
30259
|
const data = await writeAvatars("/api/avatars", body);
|
|
30251
30260
|
writeJson({ ok: true, data, hints: creationHints({ ...data, faceChoice }, body.profile) });
|
|
@@ -33462,7 +33471,7 @@ function sameSpeakerTrackEntries(segments) {
|
|
|
33462
33471
|
};
|
|
33463
33472
|
});
|
|
33464
33473
|
}
|
|
33465
|
-
function buildPerSpeakerVoiceConversion(segments, totalMs, nodes) {
|
|
33474
|
+
function buildPerSpeakerVoiceConversion(segments, totalMs, nodes, keepRenderedVoice = false) {
|
|
33466
33475
|
const bySpeaker = /* @__PURE__ */ new Map();
|
|
33467
33476
|
for (const seg of segments) {
|
|
33468
33477
|
const arr = bySpeaker.get(seg.voiceNode) ?? [];
|
|
@@ -33471,7 +33480,7 @@ function buildPerSpeakerVoiceConversion(segments, totalMs, nodes) {
|
|
|
33471
33480
|
}
|
|
33472
33481
|
const tracks = [];
|
|
33473
33482
|
for (const [voiceNode, segs] of bySpeaker) {
|
|
33474
|
-
const trackId = `${voiceNode}_track`;
|
|
33483
|
+
const trackId = keepRenderedVoice ? `${voiceNode}_conv` : `${voiceNode}_track`;
|
|
33475
33484
|
const convId = `${voiceNode}_conv`;
|
|
33476
33485
|
const sorted = [...segs].sort((a, b) => a.start_s - b.start_s);
|
|
33477
33486
|
const mixInputs = {};
|
|
@@ -33487,12 +33496,14 @@ function buildPerSpeakerVoiceConversion(segments, totalMs, nodes) {
|
|
|
33487
33496
|
total_ms: totalMs
|
|
33488
33497
|
}
|
|
33489
33498
|
});
|
|
33490
|
-
|
|
33491
|
-
|
|
33492
|
-
|
|
33493
|
-
|
|
33494
|
-
|
|
33495
|
-
|
|
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
|
+
}
|
|
33496
33507
|
tracks.push({ slot: convId, ref: `$ref:${convId}.audio`, start_s: 0, duration_s: totalMs / 1e3, kind: "vo" });
|
|
33497
33508
|
}
|
|
33498
33509
|
return tracks;
|
|
@@ -34632,7 +34643,8 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
34632
34643
|
graphicScenes: fullFrameGraphicScenes(blueprint),
|
|
34633
34644
|
surfaceIngests: /* @__PURE__ */ new Map(),
|
|
34634
34645
|
ttsLanguageCode: deriveTtsLanguageCode(blueprint),
|
|
34635
|
-
spokenAccent: blueprint.global?.voiceover?.accent?.trim() || void 0
|
|
34646
|
+
spokenAccent: blueprint.global?.voiceover?.accent?.trim() || void 0,
|
|
34647
|
+
keepRenderedVoice: opts.keepRenderedVoice
|
|
34636
34648
|
};
|
|
34637
34649
|
const out = {
|
|
34638
34650
|
routes: /* @__PURE__ */ new Map(),
|
|
@@ -34689,7 +34701,7 @@ function buildTimeline(blueprint, slots, opts, nodes) {
|
|
|
34689
34701
|
prevEndFrame = emitBrollScene(scene, i, i === lastIndex, env, nodes, out, prevEndFrame);
|
|
34690
34702
|
});
|
|
34691
34703
|
const totalMs = Math.round(env.clock.totalS * 1e3);
|
|
34692
|
-
out.voTracks.push(...buildPerSpeakerVoiceConversion(out.nativeSegments, totalMs, nodes));
|
|
34704
|
+
out.voTracks.push(...buildPerSpeakerVoiceConversion(out.nativeSegments, totalMs, nodes, env.keepRenderedVoice));
|
|
34693
34705
|
return {
|
|
34694
34706
|
clips: out.clips,
|
|
34695
34707
|
voTracks: out.voTracks,
|
|
@@ -35127,6 +35139,18 @@ function emitCaptionChain(blueprint, tracks, clock, opts, videoRef, nodes) {
|
|
|
35127
35139
|
});
|
|
35128
35140
|
return { videoRef: "$ref:captions.video", videoNode: "captions" };
|
|
35129
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
|
+
}
|
|
35130
35154
|
function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
35131
35155
|
const blueprint = VideoBlueprint.parse(input);
|
|
35132
35156
|
injectHookPhysicality(blueprint);
|
|
@@ -35240,6 +35264,7 @@ function scaffoldVideoCanvas(input, elementsInput, opts) {
|
|
|
35240
35264
|
...aspectRemapTodo(resolveAspect(blueprint.source?.aspect_ratio, opts.aspect, genAspectsFor(opts.videoModel))),
|
|
35241
35265
|
model_constraints: buildModelConstraints()
|
|
35242
35266
|
};
|
|
35267
|
+
dropUncastVoices(nodes);
|
|
35243
35268
|
return {
|
|
35244
35269
|
schema: "baker-canvas/1",
|
|
35245
35270
|
metadata: {
|
|
@@ -36361,6 +36386,14 @@ var RENDER_REVIEW_MODEL = "gemini-pro-latest";
|
|
|
36361
36386
|
var RENDER_REVIEW_FRAMES = 12;
|
|
36362
36387
|
var RENDER_REVIEW_WIDTH = 1024;
|
|
36363
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
|
+
}
|
|
36364
36397
|
var WATCH_MAX_BYTES = 60 * 1024 * 1024;
|
|
36365
36398
|
function sceneAt(windows, atS) {
|
|
36366
36399
|
return windows.find((w) => atS >= w.startS && atS < w.endS)?.file;
|
|
@@ -36622,6 +36655,12 @@ function videoPathFromOutput(output) {
|
|
|
36622
36655
|
}
|
|
36623
36656
|
return found;
|
|
36624
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
|
+
}
|
|
36625
36664
|
function classifyAudio(volumedetect, hasAudioStream) {
|
|
36626
36665
|
if (!hasAudioStream) {
|
|
36627
36666
|
return [
|
|
@@ -36631,9 +36670,17 @@ function classifyAudio(volumedetect, hasAudioStream) {
|
|
|
36631
36670
|
}
|
|
36632
36671
|
];
|
|
36633
36672
|
}
|
|
36634
|
-
const mean =
|
|
36635
|
-
const max =
|
|
36673
|
+
const mean = parseVolume(volumedetect, "mean_volume");
|
|
36674
|
+
const max = parseVolume(volumedetect, "max_volume");
|
|
36636
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
|
+
}
|
|
36637
36684
|
if (Number.isFinite(mean) && mean < -30) {
|
|
36638
36685
|
findings.push({
|
|
36639
36686
|
severity: "warning",
|
|
@@ -36691,7 +36738,7 @@ async function videoDuration(video) {
|
|
|
36691
36738
|
return null;
|
|
36692
36739
|
}
|
|
36693
36740
|
}
|
|
36694
|
-
async function askVideo(video, prompt) {
|
|
36741
|
+
async function askVideo(video, prompt, spent) {
|
|
36695
36742
|
const key = frameVisionKey();
|
|
36696
36743
|
if (!key) return null;
|
|
36697
36744
|
try {
|
|
@@ -36714,13 +36761,15 @@ async function askVideo(video, prompt) {
|
|
|
36714
36761
|
);
|
|
36715
36762
|
if (!response.ok) return null;
|
|
36716
36763
|
const body = await response.json();
|
|
36764
|
+
const usage = usageFrom(RENDER_WATCH_MODEL, body);
|
|
36765
|
+
if (usage) spent(usage);
|
|
36717
36766
|
const text2 = body.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("");
|
|
36718
36767
|
return text2 ? parseReviewJson(text2) : null;
|
|
36719
36768
|
} catch {
|
|
36720
36769
|
return null;
|
|
36721
36770
|
}
|
|
36722
36771
|
}
|
|
36723
|
-
async function ask(images, prompt) {
|
|
36772
|
+
async function ask(images, prompt, spent) {
|
|
36724
36773
|
const key = frameVisionKey();
|
|
36725
36774
|
if (!key) return null;
|
|
36726
36775
|
const parts = [{ text: prompt }];
|
|
@@ -36737,6 +36786,8 @@ async function ask(images, prompt) {
|
|
|
36737
36786
|
);
|
|
36738
36787
|
if (!response.ok) return null;
|
|
36739
36788
|
const body = await response.json();
|
|
36789
|
+
const usage = usageFrom(RENDER_REVIEW_MODEL, body);
|
|
36790
|
+
if (usage) spent(usage);
|
|
36740
36791
|
const text2 = body.candidates?.[0]?.content?.parts?.map((p) => p.text ?? "").join("");
|
|
36741
36792
|
return text2 ? parseReviewJson(text2) : null;
|
|
36742
36793
|
} catch {
|
|
@@ -36747,6 +36798,7 @@ async function reviewRenderedVideo(opts) {
|
|
|
36747
36798
|
if (!hasFrameVisionKey()) return null;
|
|
36748
36799
|
const duration = await videoDuration(opts.video);
|
|
36749
36800
|
if (duration === null) return null;
|
|
36801
|
+
const spent = (usage) => opts.onSpend?.(usage);
|
|
36750
36802
|
const dir = await mkdtemp(path15.join(tmpdir(), "baker-review-"));
|
|
36751
36803
|
try {
|
|
36752
36804
|
const grabbed = [];
|
|
@@ -36761,17 +36813,22 @@ async function reviewRenderedVideo(opts) {
|
|
|
36761
36813
|
if (grabbed.length === 0) return null;
|
|
36762
36814
|
const reel = await ask(
|
|
36763
36815
|
grabbed.map((f) => f.image),
|
|
36764
|
-
buildReelReviewPrompt({ subject: opts.subject, script: opts.script })
|
|
36816
|
+
buildReelReviewPrompt({ subject: opts.subject, script: opts.script }),
|
|
36817
|
+
spent
|
|
36765
36818
|
);
|
|
36766
36819
|
const framePrompt = buildFrameReviewPrompt({ subject: opts.subject });
|
|
36767
36820
|
const perFrame = await mapWithLimit(grabbed, REVIEW_CONCURRENCY, async (f) => ({
|
|
36768
36821
|
at: f.at,
|
|
36769
|
-
json: await ask([f.image], framePrompt)
|
|
36822
|
+
json: await ask([f.image], framePrompt, spent)
|
|
36770
36823
|
}));
|
|
36771
36824
|
const unread = perFrame.filter((f) => f.json === null).length;
|
|
36772
36825
|
if (unread === perFrame.length && reel === null) return null;
|
|
36773
|
-
const watch = await askVideo(
|
|
36774
|
-
|
|
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;
|
|
36775
36832
|
const findings = [
|
|
36776
36833
|
...classifyFrameFindings(perFrame, opts.windows ?? []),
|
|
36777
36834
|
...classifyReelFindings(reel),
|
|
@@ -36781,12 +36838,15 @@ async function reviewRenderedVideo(opts) {
|
|
|
36781
36838
|
// Measured, not judged: a model looking at frames cannot hear the mix.
|
|
36782
36839
|
...await measureAudio(opts.video)
|
|
36783
36840
|
];
|
|
36784
|
-
|
|
36785
|
-
|
|
36786
|
-
|
|
36787
|
-
|
|
36788
|
-
|
|
36789
|
-
|
|
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
|
+
);
|
|
36790
36850
|
return findings;
|
|
36791
36851
|
} finally {
|
|
36792
36852
|
await rm5(dir, { recursive: true, force: true });
|
|
@@ -36810,6 +36870,28 @@ async function sceneWindowsBeside(canvasPath) {
|
|
|
36810
36870
|
}
|
|
36811
36871
|
return windows;
|
|
36812
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
|
+
}
|
|
36813
36895
|
|
|
36814
36896
|
// src/commands/canvas/run.ts
|
|
36815
36897
|
var runCommand = defineCommand107({
|
|
@@ -37124,10 +37206,12 @@ ${describeRewrites(healed.rewrites)}
|
|
|
37124
37206
|
`));
|
|
37125
37207
|
}
|
|
37126
37208
|
const reviewable = videoPathFromOutput(result.output);
|
|
37209
|
+
const reviewUsage = [];
|
|
37127
37210
|
const review = reviewable ? await reviewRenderedVideo({
|
|
37128
37211
|
video: reviewable,
|
|
37129
37212
|
windows: await sceneWindowsBeside(filePath),
|
|
37130
|
-
brief: opts.brief?.trim() || void 0
|
|
37213
|
+
brief: opts.brief?.trim() || void 0,
|
|
37214
|
+
onSpend: (usage) => reviewUsage.push(usage)
|
|
37131
37215
|
}) : null;
|
|
37132
37216
|
const hints2 = reviewable ? review === null ? [RENDER_REVIEW_UNAVAILABLE] : review.length === 0 ? ["Reviewed the finished video frame by frame \u2014 nothing to report."] : [
|
|
37133
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.`,
|
|
@@ -37141,7 +37225,12 @@ ${describeRewrites(healed.rewrites)}
|
|
|
37141
37225
|
// `review` is null only when nobody looked (no key, no ffmpeg, unreadable video);
|
|
37142
37226
|
// an empty array is a render that was reviewed and came back clean. The record
|
|
37143
37227
|
// has to keep those apart.
|
|
37144
|
-
buildRunRecord(
|
|
37228
|
+
buildRunRecord(
|
|
37229
|
+
result,
|
|
37230
|
+
{ ...recordMeta, ...review ? { review } : {}, ...reviewUsage.length > 0 ? { reviewUsage } : {} },
|
|
37231
|
+
progress?.planInfo(),
|
|
37232
|
+
finalLabels
|
|
37233
|
+
)
|
|
37145
37234
|
);
|
|
37146
37235
|
}
|
|
37147
37236
|
process.stdout.write(
|
|
@@ -38252,6 +38341,12 @@ function pinnedModelHint(input) {
|
|
|
38252
38341
|
if (!speaks) return null;
|
|
38253
38342
|
return `You pinned --model ${input.model} on a speaking avatar. Without it this clip routes to ${DEFAULT_VIDEO_GENERATE_MODEL}, which was chosen for exactly this case: the better picture, both orientations, and audio always on. Drop --model unless the user asked for a specific one \u2014 and note ${input.model} may not take the length or the reference you are counting on.`;
|
|
38254
38343
|
}
|
|
38344
|
+
function silentModelHint(model, prompt) {
|
|
38345
|
+
if (!model || !prompt) return null;
|
|
38346
|
+
if (clipAudioMode(model) !== "never") return null;
|
|
38347
|
+
if (spokenWordsIn(prompt) === 0) return null;
|
|
38348
|
+
return `${model} renders no audio \u2014 the line in this prompt will be mouthed and never heard. For a shot where someone speaks use ${DEFAULT_VIDEO_GENERATE_MODEL} (audio always on), or alibaba/wan-3.0 past 10 seconds. Keep this model for shots with no voice in them.`;
|
|
38349
|
+
}
|
|
38255
38350
|
|
|
38256
38351
|
// src/commands/canvas/scaffold-ad.ts
|
|
38257
38352
|
import { copyFile, cp, mkdir as mkdir7, readFile as readFile16, stat as stat4, writeFile as writeFile9 } from "fs/promises";
|
|
@@ -38461,6 +38556,7 @@ var CURRENCY_BY_MARKET = {
|
|
|
38461
38556
|
"the united kingdom": "\xA3 (pounds sterling)",
|
|
38462
38557
|
"the united states": "$ (US dollars)"
|
|
38463
38558
|
};
|
|
38559
|
+
var MARKERS_TO_NAME_A_PLACE = 3;
|
|
38464
38560
|
var MARKET_BY_LANGUAGE = {
|
|
38465
38561
|
es: "Spain",
|
|
38466
38562
|
"es-es": "Spain",
|
|
@@ -38499,6 +38595,9 @@ var SCRIPT_LANGUAGE_MARKERS = [
|
|
|
38499
38595
|
["english", /\b(the|a|an|of|to|for|with|your|more|already|but|because|until|without)\b/gi]
|
|
38500
38596
|
];
|
|
38501
38597
|
function detectScriptLanguage(lines) {
|
|
38598
|
+
return bestScriptLanguage(lines)?.name;
|
|
38599
|
+
}
|
|
38600
|
+
function bestScriptLanguage(lines) {
|
|
38502
38601
|
const text2 = lines.join(" ");
|
|
38503
38602
|
if (text2.trim().length < 12) return void 0;
|
|
38504
38603
|
let best = null;
|
|
@@ -38506,7 +38605,7 @@ function detectScriptLanguage(lines) {
|
|
|
38506
38605
|
const hits = (text2.match(re2) ?? []).length;
|
|
38507
38606
|
if (hits > 0 && (!best || hits > best.hits)) best = { name, hits };
|
|
38508
38607
|
}
|
|
38509
|
-
return best
|
|
38608
|
+
return best ?? void 0;
|
|
38510
38609
|
}
|
|
38511
38610
|
function voiceLanguageFor(spec) {
|
|
38512
38611
|
const declared = spec.voice?.language?.trim().toLowerCase();
|
|
@@ -38519,12 +38618,19 @@ function voiceLanguageFor(spec) {
|
|
|
38519
38618
|
function marketFor(spec) {
|
|
38520
38619
|
if (spec.market) return spec.market;
|
|
38521
38620
|
const lang = spec.voice?.language?.trim().toLowerCase();
|
|
38522
|
-
|
|
38523
|
-
|
|
38524
|
-
|
|
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) {
|
|
38525
38631
|
const words2 = line.trim().split(/\s+/).length;
|
|
38526
38632
|
const spoken = words2 / 3.06;
|
|
38527
|
-
const allowed = [...SEEDANCE_DURATIONS].sort((a, b) => a - b);
|
|
38633
|
+
const allowed = (videoModel ? clipDurationsFor(videoModel) : void 0) ?? [...SEEDANCE_DURATIONS].sort((a, b) => a - b);
|
|
38528
38634
|
return allowed.find((d) => d >= spoken) ?? allowed[allowed.length - 1];
|
|
38529
38635
|
}
|
|
38530
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.";
|
|
@@ -38553,18 +38659,21 @@ function castedAvatarOnRoster(ad, roster) {
|
|
|
38553
38659
|
if (ad.cast?.avatar) return null;
|
|
38554
38660
|
const haystack = [ad.cast?.description ?? "", ...(ad.beats ?? []).map((b) => b.show ?? "")].join(" ");
|
|
38555
38661
|
if (!haystack.trim()) return null;
|
|
38556
|
-
const
|
|
38557
|
-
haystack.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean)
|
|
38558
|
-
);
|
|
38662
|
+
const written = new Set(haystack.split(/[^\p{L}\p{N}]+/u).filter(Boolean));
|
|
38559
38663
|
for (const person of roster) {
|
|
38560
|
-
|
|
38561
|
-
const token = label.trim().toLowerCase();
|
|
38562
|
-
if (!token || token.includes("-") || token.length < 3) continue;
|
|
38563
|
-
if (words2.has(token)) return person.handle;
|
|
38564
|
-
}
|
|
38664
|
+
if (labelWrittenAsName(person.name, written) || labelWrittenAsName(person.handle, written)) return person.handle;
|
|
38565
38665
|
}
|
|
38566
38666
|
return null;
|
|
38567
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
|
+
}
|
|
38568
38677
|
function adSpecCastElements(spec, avatar) {
|
|
38569
38678
|
const lastIndex = spec.beats.length - 1;
|
|
38570
38679
|
const speakingScenes = spec.beats.map((_, i) => i).filter((i) => !(endCardWanted(spec) && i === lastIndex));
|
|
@@ -38606,11 +38715,11 @@ function motionPrompt(beat, toCamera, isClosingCard, place, physics) {
|
|
|
38606
38715
|
if (toCamera) return shot;
|
|
38607
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.`;
|
|
38608
38717
|
}
|
|
38609
|
-
function staticTranscript(spec) {
|
|
38718
|
+
function staticTranscript(spec, videoModel) {
|
|
38610
38719
|
const out = [];
|
|
38611
38720
|
let clock = 0;
|
|
38612
38721
|
for (const beat of spec.beats) {
|
|
38613
|
-
const duration = beatSeconds(beat.say);
|
|
38722
|
+
const duration = beatSeconds(beat.say, videoModel);
|
|
38614
38723
|
const words2 = beat.say.trim().split(/\s+/).filter(Boolean);
|
|
38615
38724
|
const each = words2.length > 0 ? duration / words2.length : duration;
|
|
38616
38725
|
words2.forEach((text2, i) => {
|
|
@@ -38676,9 +38785,12 @@ function voiceoverMode(spec, nativeSpeech) {
|
|
|
38676
38785
|
function castRole(spec) {
|
|
38677
38786
|
if (!spec.cast) return void 0;
|
|
38678
38787
|
const described = spec.cast.description?.trim();
|
|
38679
|
-
|
|
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`;
|
|
38680
38790
|
}
|
|
38681
|
-
|
|
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;
|
|
38682
38794
|
const total = spec.beats.length;
|
|
38683
38795
|
const market = marketFor(spec);
|
|
38684
38796
|
const place = market ? ` Set in ${market}: the architecture, streets and styling are ${market}'s.` : "";
|
|
@@ -38689,7 +38801,7 @@ function adSpecToBlueprint(spec, avatarAccent) {
|
|
|
38689
38801
|
let clock = 0;
|
|
38690
38802
|
const scenes = spec.beats.map((beat, i) => {
|
|
38691
38803
|
const isClosingCard = closesOnCard && i === total - 1;
|
|
38692
|
-
const duration = beatSeconds(beat.say);
|
|
38804
|
+
const duration = beatSeconds(beat.say, opts.videoModel);
|
|
38693
38805
|
const start = clock;
|
|
38694
38806
|
clock += duration;
|
|
38695
38807
|
return {
|
|
@@ -39160,6 +39272,18 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39160
39272
|
avatarError = `Could not read avatar \`${handle}\`: ${e instanceof Error ? e.message : String(e)}. \`baker avatars list\` shows who there is.`;
|
|
39161
39273
|
}
|
|
39162
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
|
+
}
|
|
39163
39287
|
const fromWorkspace = await readBrandFromWorkspace();
|
|
39164
39288
|
const filledLogo = !spec.data.brand?.logo && fromWorkspace.logo ? fromWorkspace.logo : void 0;
|
|
39165
39289
|
const filledPalette = !spec.data.brand?.palette?.length && fromWorkspace.palette?.length ? fromWorkspace.palette : void 0;
|
|
@@ -39238,7 +39362,7 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39238
39362
|
process.exit(1);
|
|
39239
39363
|
return;
|
|
39240
39364
|
}
|
|
39241
|
-
const blueprint = adSpecToBlueprint(spec.data, avatar?.profile?.accent);
|
|
39365
|
+
const blueprint = adSpecToBlueprint(spec.data, { avatarAccent: avatar?.profile?.accent, videoModel: chosenVideoModel });
|
|
39242
39366
|
const slug = args.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
|
|
39243
39367
|
const outPath = args.out ?? (args.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));
|
|
39244
39368
|
const outDir = path24.dirname(outPath);
|
|
@@ -39251,10 +39375,10 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39251
39375
|
const blueprintStylePath = path24.join(outDir, "prompt.style.json");
|
|
39252
39376
|
await writeSceneFiles(outDir, blueprint);
|
|
39253
39377
|
await writeFile9(blueprintStylePath, renderStyleProjectionFromValue(blueprint), "utf8");
|
|
39254
|
-
const logoPath = spec.data.brand?.logo?.trim();
|
|
39378
|
+
const logoPath = noBrand ? void 0 : spec.data.brand?.logo?.trim();
|
|
39255
39379
|
const transcriptPath = path24.join(outDir, "transcript.json");
|
|
39256
39380
|
if (spec.data.voiceover === false) {
|
|
39257
|
-
await writeFile9(transcriptPath, `${JSON.stringify(staticTranscript(spec.data), null, 2)}
|
|
39381
|
+
await writeFile9(transcriptPath, `${JSON.stringify(staticTranscript(spec.data, chosenVideoModel), null, 2)}
|
|
39258
39382
|
`, "utf-8");
|
|
39259
39383
|
}
|
|
39260
39384
|
const opts = {
|
|
@@ -39283,6 +39407,12 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39283
39407
|
// its accent. The clip speaks in the voice the video model renders, and the region is
|
|
39284
39408
|
// directed in the prompt, which is the only place a pronunciation can come from.
|
|
39285
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 } : {},
|
|
39286
39416
|
...spec.data.voiceover === false ? { staticTranscriptPath: path24.relative(outDir, transcriptPath) } : {}
|
|
39287
39417
|
};
|
|
39288
39418
|
const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data, avatar), opts);
|
|
@@ -39298,7 +39428,7 @@ var scaffoldAdCommand = defineCommand111({
|
|
|
39298
39428
|
}
|
|
39299
39429
|
}
|
|
39300
39430
|
const closing = blueprint.scenes[blueprint.scenes.length - 1];
|
|
39301
|
-
const overlayHtml = endCardWanted(spec.data) && closing && renderedDir ? buildAdBrandOverlay({
|
|
39431
|
+
const overlayHtml = !noBrand && endCardWanted(spec.data) && closing && renderedDir ? buildAdBrandOverlay({
|
|
39302
39432
|
logo: staged,
|
|
39303
39433
|
cta: endCardCta(spec.data),
|
|
39304
39434
|
cardAtS: closing.start_s,
|
|
@@ -50871,6 +51001,20 @@ function blankAstroFrontmatter(text2) {
|
|
|
50871
51001
|
}
|
|
50872
51002
|
return kept.join("") + text2.slice(cut);
|
|
50873
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
|
+
}
|
|
50874
51018
|
function frontmatterLength(text2) {
|
|
50875
51019
|
if (!text2.startsWith("---")) return 0;
|
|
50876
51020
|
const lines = text2.split("\n");
|
|
@@ -51244,7 +51388,7 @@ function distinctCompetitorNames(competitors) {
|
|
|
51244
51388
|
return [...byFold.values()];
|
|
51245
51389
|
}
|
|
51246
51390
|
function mentionsInSource(source, names) {
|
|
51247
|
-
const text2 =
|
|
51391
|
+
const text2 = blankUnrenderedSource(source.text);
|
|
51248
51392
|
const visible = blankHtmlKeepingOffsets(text2);
|
|
51249
51393
|
const tables = tableRanges(text2);
|
|
51250
51394
|
const out = [];
|
|
@@ -51284,7 +51428,7 @@ function escapeRegExp(value) {
|
|
|
51284
51428
|
}
|
|
51285
51429
|
function detectSource(source) {
|
|
51286
51430
|
const file = source.path;
|
|
51287
|
-
const text2 =
|
|
51431
|
+
const text2 = blankUnrenderedSource(source.text);
|
|
51288
51432
|
const findings = [];
|
|
51289
51433
|
const lines = text2.split("\n");
|
|
51290
51434
|
for (const matcher of LINE_MATCHERS) {
|
|
@@ -51302,7 +51446,7 @@ function detectSource(source) {
|
|
|
51302
51446
|
}
|
|
51303
51447
|
function detectPage(sources) {
|
|
51304
51448
|
if (sources.length === 0) return [];
|
|
51305
|
-
const combined = sources.map((s) =>
|
|
51449
|
+
const combined = sources.map((s) => blankUnrenderedSource(s.text)).join("\n");
|
|
51306
51450
|
const pageFile = sources.find((s) => s.path === "index.astro" || s.path.endsWith("/index.astro"))?.path ?? sources[0]?.path ?? "index.astro";
|
|
51307
51451
|
const findings = [];
|
|
51308
51452
|
for (const analyzer of ANALYZERS) {
|
|
@@ -51382,7 +51526,7 @@ function describeCounts(findings) {
|
|
|
51382
51526
|
// src/commands/landing/snapshot.ts
|
|
51383
51527
|
import { mkdir as mkdir10, rename as rename2, writeFile as writeFile14 } from "fs/promises";
|
|
51384
51528
|
import path34 from "path";
|
|
51385
|
-
var CRITIC_VERSION = "
|
|
51529
|
+
var CRITIC_VERSION = "4";
|
|
51386
51530
|
function critiqueCacheDir(projectRoot) {
|
|
51387
51531
|
return path34.join(projectRoot, ".cache", "landing-critique");
|
|
51388
51532
|
}
|
|
@@ -57902,6 +58046,8 @@ function costHintsFor(body) {
|
|
|
57902
58046
|
if (pinned) hints2.push(pinned);
|
|
57903
58047
|
const pace = spokenPaceHint(body.prompt, body.duration, model);
|
|
57904
58048
|
if (pace) hints2.push(pace);
|
|
58049
|
+
const silent = silentModelHint(model, body.prompt);
|
|
58050
|
+
if (silent) hints2.push(silent);
|
|
57905
58051
|
if (count > 1) {
|
|
57906
58052
|
hints2.push(
|
|
57907
58053
|
"Ask for more than one take only when the MOTION is the risk \u2014 for a look you are unsure about, iterate on the still first."
|