@koda-sl/baker-cli 0.434.0-dev.ec5c827fe → 0.435.0-dev.e59c50e4b
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 +31 -16
- package/canvas/tiktok-captions-composition/index.html +5 -6
- package/canvas/tiktok-captions-composition/meta.json +5 -0
- package/dist/{chunk-7I73A72J.js → chunk-QQX5LPMQ.js} +1048 -786
- package/dist/chunk-QQX5LPMQ.js.map +1 -0
- package/dist/cli.js +6074 -5136
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.d.ts +2 -0
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-7I73A72J.js.map +0 -1
|
@@ -4405,7 +4405,10 @@ var CanvasMetadata = z4.object({
|
|
|
4405
4405
|
series: z4.object({
|
|
4406
4406
|
lead: z4.string().min(1),
|
|
4407
4407
|
run_id: z4.string().min(1),
|
|
4408
|
-
copy: z4.array(z4.object({ text: z4.string(), role: z4.string().optional() }).strict())
|
|
4408
|
+
copy: z4.array(z4.object({ text: z4.string(), role: z4.string().optional() }).strict()),
|
|
4409
|
+
design: z4.string().optional(),
|
|
4410
|
+
// A translation of the lead: the language (BCP-47) its words are in.
|
|
4411
|
+
language: z4.string().min(2).optional()
|
|
4409
4412
|
}).strict().optional()
|
|
4410
4413
|
}).strict().optional();
|
|
4411
4414
|
var CanvasSchema = z4.object({
|
|
@@ -4637,7 +4640,7 @@ function dfsCycle(u, color, stack, reverseAdj) {
|
|
|
4637
4640
|
|
|
4638
4641
|
// src/engine/engine/validator.ts
|
|
4639
4642
|
import path5 from "path";
|
|
4640
|
-
import { z as
|
|
4643
|
+
import { z as z9 } from "zod";
|
|
4641
4644
|
|
|
4642
4645
|
// src/engine/lib/url.ts
|
|
4643
4646
|
function looksLikeHttpUrl(value) {
|
|
@@ -5405,27 +5408,26 @@ function audienceRefs(value) {
|
|
|
5405
5408
|
return id ? [{ id, name: text(ref?.name) }] : [];
|
|
5406
5409
|
});
|
|
5407
5410
|
}
|
|
5408
|
-
var BUYERS = /purchas|buyer|customer|client|compra|cliente|achat|acheteur|kunde|käufer/i;
|
|
5409
|
-
var LOOKALIKE_NAME = /lookalike|\blal\b|similar|parecid/i;
|
|
5410
5411
|
var COLD = { temperature: "cold", knows: [] };
|
|
5411
|
-
function tierOf(audience, ref) {
|
|
5412
|
+
function tierOf(audience, ref, readName) {
|
|
5412
5413
|
const subtype = text(audience?.subtype).toUpperCase();
|
|
5413
5414
|
const name = text(audience?.name) || ref.name;
|
|
5414
|
-
|
|
5415
|
+
const read = readName?.(name);
|
|
5416
|
+
if (subtype === "LOOKALIKE" || !subtype && read?.lookalike) {
|
|
5415
5417
|
return { ...COLD, who: "a lookalike: strangers who resemble the seed" };
|
|
5416
5418
|
}
|
|
5417
|
-
if (
|
|
5419
|
+
if (read?.buyers) return { temperature: "hot", knows: ["brand", "offer", "product"], who: "buyers" };
|
|
5418
5420
|
if (subtype === "WEBSITE") return { temperature: "warm", knows: ["brand", "offer"], who: "site visitors" };
|
|
5419
5421
|
if (subtype === "CUSTOM") {
|
|
5420
5422
|
return { temperature: "hot", knows: ["brand", "offer", "product"], who: "an uploaded customer list" };
|
|
5421
5423
|
}
|
|
5422
5424
|
return { temperature: "warm", knows: ["brand"], who: "people who engaged with the brand" };
|
|
5423
5425
|
}
|
|
5424
|
-
function classifyAudience(adSet, audiences, campaign) {
|
|
5426
|
+
function classifyAudience(adSet, audiences, campaign, readName) {
|
|
5425
5427
|
const targeting = asRecord(adSet.targeting) ?? {};
|
|
5426
5428
|
const byId = new Map(audiences.map((audience) => [text(audience.id), audience]));
|
|
5427
5429
|
const includedRefs = audienceRefs(targeting.custom_audiences);
|
|
5428
|
-
const included = includedRefs.map((ref) => tierOf(byId.get(ref.id), ref));
|
|
5430
|
+
const included = includedRefs.map((ref) => tierOf(byId.get(ref.id), ref, readName));
|
|
5429
5431
|
const reasons = [
|
|
5430
5432
|
includedRefs.length === 0 ? "No custom audience: Meta finds new people from age, place and interests, so every viewer is a stranger." : `Reaches ${includedRefs.map((ref, i) => `"${ref.name || ref.id}" (${included[i]?.who})`).join(", ")}.`
|
|
5431
5433
|
];
|
|
@@ -5442,7 +5444,7 @@ function classifyAudience(adSet, audiences, campaign) {
|
|
|
5442
5444
|
}
|
|
5443
5445
|
const excludedRefs = audienceRefs(targeting.excluded_custom_audiences);
|
|
5444
5446
|
if (excludedRefs.length > 0) reasons.push(`Keeps out ${quoteNames(excludedRefs)}.`);
|
|
5445
|
-
if (excludedRefs.some((ref) => tierOf(byId.get(ref.id), ref).temperature === "hot")) {
|
|
5447
|
+
if (excludedRefs.some((ref) => tierOf(byId.get(ref.id), ref, readName).temperature === "hot")) {
|
|
5446
5448
|
reached = belowHot(reached);
|
|
5447
5449
|
reasons.push("Buyers are excluded, so nobody reached has used the product.");
|
|
5448
5450
|
}
|
|
@@ -5546,7 +5548,7 @@ function viewerFromKnowledge(input) {
|
|
|
5546
5548
|
}
|
|
5547
5549
|
|
|
5548
5550
|
// src/engine/scaffold/static-ad-spec.ts
|
|
5549
|
-
import { z as
|
|
5551
|
+
import { z as z8 } from "zod";
|
|
5550
5552
|
|
|
5551
5553
|
// src/engine/scaffold/lib/elements.ts
|
|
5552
5554
|
var ACTOR_SHEET_MODEL = REFERENCE_SHEET_MODEL;
|
|
@@ -5634,7 +5636,72 @@ function staticElementTodo(el, t, label, brief) {
|
|
|
5634
5636
|
}
|
|
5635
5637
|
|
|
5636
5638
|
// src/engine/scaffold/native-surface.ts
|
|
5639
|
+
import { z as z6 } from "zod";
|
|
5640
|
+
|
|
5641
|
+
// src/engine/nodes/local/lib/jev.ts
|
|
5637
5642
|
import { z as z5 } from "zod";
|
|
5643
|
+
var JEV_MODEL = "typesafe/jev-1.13";
|
|
5644
|
+
var ENDPOINT = "https://openrouter.ai/api/v1/systemone";
|
|
5645
|
+
var TIMEOUT_MS = 2500;
|
|
5646
|
+
var Reply = z5.object({
|
|
5647
|
+
answers: z5.record(z5.string(), z5.object({ noul: z5.number() })),
|
|
5648
|
+
usage: z5.object({ cost: z5.number() }).optional()
|
|
5649
|
+
});
|
|
5650
|
+
var Answer = z5.union([
|
|
5651
|
+
z5.object({ choice: z5.string(), probabilities: z5.record(z5.string(), z5.number()), confidence: z5.number() }),
|
|
5652
|
+
z5.object({ noul: z5.number() })
|
|
5653
|
+
]);
|
|
5654
|
+
var AnswersReply = z5.object({
|
|
5655
|
+
answers: z5.record(z5.string(), z5.unknown()),
|
|
5656
|
+
usage: z5.object({ cost: z5.number() }).optional()
|
|
5657
|
+
});
|
|
5658
|
+
function readAnswers(body, ids) {
|
|
5659
|
+
const parsed = AnswersReply.safeParse(body);
|
|
5660
|
+
if (!parsed.success) return null;
|
|
5661
|
+
const answers = [];
|
|
5662
|
+
for (const id of ids) {
|
|
5663
|
+
const answer = Answer.safeParse(parsed.data.answers[id]);
|
|
5664
|
+
if (!answer.success) return null;
|
|
5665
|
+
answers.push(
|
|
5666
|
+
"choice" in answer.data ? { choice: answer.data.choice, probabilities: answer.data.probabilities, confidence: answer.data.confidence } : { noul: answer.data.noul }
|
|
5667
|
+
);
|
|
5668
|
+
}
|
|
5669
|
+
return { answers, costUsd: parsed.data.usage?.cost ?? 0 };
|
|
5670
|
+
}
|
|
5671
|
+
async function askJevQuestions(questions, onSpend) {
|
|
5672
|
+
const key = openRouterKey();
|
|
5673
|
+
if (questions.length === 0) return [];
|
|
5674
|
+
if (!key) return null;
|
|
5675
|
+
const ids = questions.map((_, i) => `q${i}`);
|
|
5676
|
+
try {
|
|
5677
|
+
const response = await fetch(ENDPOINT, {
|
|
5678
|
+
method: "POST",
|
|
5679
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" },
|
|
5680
|
+
body: JSON.stringify({
|
|
5681
|
+
model: JEV_MODEL,
|
|
5682
|
+
state: {},
|
|
5683
|
+
questions: Object.fromEntries(questions.map((q, i) => [ids[i], q]))
|
|
5684
|
+
}),
|
|
5685
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
5686
|
+
});
|
|
5687
|
+
if (!response.ok) return null;
|
|
5688
|
+
const read = readAnswers(await response.json(), ids);
|
|
5689
|
+
if (!read) return null;
|
|
5690
|
+
onSpend?.({ model: JEV_MODEL, inputTokens: 0, outputTokens: 0, costUsd: read.costUsd });
|
|
5691
|
+
return read.answers;
|
|
5692
|
+
} catch {
|
|
5693
|
+
return null;
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
async function askJev(questions, onSpend) {
|
|
5697
|
+
const answers = await askJevQuestions(
|
|
5698
|
+
questions.map((q) => ({ type: "noul", instructions: q.instructions })),
|
|
5699
|
+
onSpend
|
|
5700
|
+
);
|
|
5701
|
+
if (!answers) return null;
|
|
5702
|
+
const nouls = answers.flatMap((a) => "noul" in a ? [a.noul] : []);
|
|
5703
|
+
return nouls.length === answers.length ? nouls : null;
|
|
5704
|
+
}
|
|
5638
5705
|
|
|
5639
5706
|
// src/engine/scaffold/review-platforms.ts
|
|
5640
5707
|
var REVIEW_PLATFORMS = [
|
|
@@ -5649,24 +5716,40 @@ var REVIEW_PLATFORMS = [
|
|
|
5649
5716
|
{ name: "Google", domain: "google.com", pattern: /\bgoogle\b/i, reviewOnly: false },
|
|
5650
5717
|
{ name: "the App Store", domain: "apple.com", pattern: /\bapp ?store\b/i, reviewOnly: false }
|
|
5651
5718
|
];
|
|
5652
|
-
var
|
|
5719
|
+
var RATING_SIGN = /[★☆⭐]|(?<![\d.,])[1-5][.,]\d(?![\d]|[.,]\d)/u;
|
|
5653
5720
|
function namedPlatform(text2) {
|
|
5654
5721
|
return REVIEW_PLATFORMS.find((p) => p.pattern.test(text2)) ?? null;
|
|
5655
5722
|
}
|
|
5656
5723
|
function claimedPlatform(text2) {
|
|
5657
|
-
|
|
5724
|
+
const platform = namedPlatform(text2);
|
|
5725
|
+
return platform && (platform.reviewOnly || RATING_SIGN.test(text2)) ? platform : null;
|
|
5726
|
+
}
|
|
5727
|
+
var CITES_PLATFORM = 0.5;
|
|
5728
|
+
var CITATION_QUESTION = "Does `line` cite `platform` as the source of a rating or of customers' reviews (stars, a score, reviews or opinions on it), in whatever language it is written? Only naming the platform (download it on, find us on, search on, sign in with) is not citing it for reviews.";
|
|
5729
|
+
function jevCitationJudge(onSpend) {
|
|
5730
|
+
return (items) => askJev(
|
|
5731
|
+
items.map((i) => ({ instructions: { line: i.line, platform: i.platform, question: CITATION_QUESTION } })),
|
|
5732
|
+
onSpend
|
|
5733
|
+
);
|
|
5658
5734
|
}
|
|
5659
|
-
function
|
|
5660
|
-
const
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5735
|
+
async function citedPlatforms(lines, judge = jevCitationJudge()) {
|
|
5736
|
+
const cited = /* @__PURE__ */ new Map();
|
|
5737
|
+
const asked = [];
|
|
5738
|
+
for (const line of new Set(lines)) {
|
|
5739
|
+
const shown = claimedPlatform(line);
|
|
5740
|
+
const platform = namedPlatform(line);
|
|
5741
|
+
if (shown) cited.set(line, shown);
|
|
5742
|
+
else if (platform) asked.push({ line, platform });
|
|
5743
|
+
}
|
|
5744
|
+
if (asked.length === 0) return cited;
|
|
5745
|
+
const judged = await judge(asked.map((a) => ({ line: a.line, platform: a.platform.name }))).catch(() => null);
|
|
5746
|
+
asked.forEach((a, k) => {
|
|
5747
|
+
if ((judged?.[k] ?? 0) >= CITES_PLATFORM) cited.set(a.line, a.platform);
|
|
5748
|
+
});
|
|
5749
|
+
return cited;
|
|
5750
|
+
}
|
|
5751
|
+
function citedBy(line, cited) {
|
|
5752
|
+
return cited ? cited.get(line) ?? null : claimedPlatform(line);
|
|
5670
5753
|
}
|
|
5671
5754
|
function figureForms(figure) {
|
|
5672
5755
|
const forms = [figure, figure.replace(/,/g, "."), figure.replace(/\./g, ",")];
|
|
@@ -5675,128 +5758,128 @@ function figureForms(figure) {
|
|
|
5675
5758
|
}
|
|
5676
5759
|
|
|
5677
5760
|
// src/engine/scaffold/native-surface.ts
|
|
5678
|
-
var Hex =
|
|
5679
|
-
var PhotoId =
|
|
5680
|
-
var Author =
|
|
5681
|
-
name:
|
|
5761
|
+
var Hex = z6.string().regex(/^#[0-9a-fA-F]{6}$/, "a colour is a 6-digit hex like #123551").transform((h) => h.toLowerCase());
|
|
5762
|
+
var PhotoId = z6.string().regex(/^PHOTO_\d+$/, "a photo id is PHOTO_1, PHOTO_2\u2026");
|
|
5763
|
+
var Author = z6.object({
|
|
5764
|
+
name: z6.string().trim().min(1),
|
|
5682
5765
|
/** Their handle with the @, as the platform prints it: a real one of ours, never an invented one. */
|
|
5683
|
-
handle:
|
|
5684
|
-
avatar:
|
|
5685
|
-
verified:
|
|
5766
|
+
handle: z6.string().trim().min(2).optional(),
|
|
5767
|
+
avatar: z6.union([z6.literal("logo"), z6.literal("initials"), PhotoId]).default("initials"),
|
|
5768
|
+
verified: z6.boolean().optional(),
|
|
5686
5769
|
/** A line under the name: a review site's "3 reviews · ES", a chat's "online". */
|
|
5687
|
-
subtitle:
|
|
5770
|
+
subtitle: z6.string().trim().min(1).optional()
|
|
5688
5771
|
}).strict();
|
|
5689
|
-
var Photo =
|
|
5772
|
+
var Photo = z6.object({
|
|
5690
5773
|
id: PhotoId,
|
|
5691
5774
|
/** What the photo shows, in plain words: who, doing what, where. */
|
|
5692
|
-
what:
|
|
5775
|
+
what: z6.string().trim().min(1),
|
|
5693
5776
|
/** Its shape on the screen. */
|
|
5694
|
-
ratio:
|
|
5695
|
-
look:
|
|
5777
|
+
ratio: z6.enum(["1:1", "4:3", "3:4", "16:9", "4:5"]).default("4:3"),
|
|
5778
|
+
look: z6.enum(["photo", "illustration"]).default("photo"),
|
|
5696
5779
|
/**
|
|
5697
5780
|
* A picture of ours (a path from the creative's folder, or a URL): used as it is, nothing
|
|
5698
5781
|
* rendered. There is no `avatar`: nothing grounds a rendered photo on an avatar's sheet, so a
|
|
5699
5782
|
* photo naming one would still render a made-up face (the strict schema refuses the key).
|
|
5700
5783
|
*/
|
|
5701
|
-
image:
|
|
5784
|
+
image: z6.string().trim().min(1).optional()
|
|
5702
5785
|
}).strict();
|
|
5703
|
-
var XPost =
|
|
5786
|
+
var XPost = z6.object({
|
|
5704
5787
|
author: Author,
|
|
5705
|
-
text:
|
|
5788
|
+
text: z6.string().trim().min(1),
|
|
5706
5789
|
photo: PhotoId.optional(),
|
|
5707
5790
|
/** The handle this post answers, printed above it as X does ("Replying to @alexbuilds"). */
|
|
5708
|
-
reply_to:
|
|
5791
|
+
reply_to: z6.string().trim().min(2).optional(),
|
|
5709
5792
|
/** How long ago, as X prints it: 2h, 14m, Sep 3. */
|
|
5710
|
-
time:
|
|
5711
|
-
stats:
|
|
5712
|
-
replies:
|
|
5713
|
-
reposts:
|
|
5714
|
-
likes:
|
|
5715
|
-
views:
|
|
5793
|
+
time: z6.string().trim().min(1).optional(),
|
|
5794
|
+
stats: z6.object({
|
|
5795
|
+
replies: z6.string().optional(),
|
|
5796
|
+
reposts: z6.string().optional(),
|
|
5797
|
+
likes: z6.string().optional(),
|
|
5798
|
+
views: z6.string().optional()
|
|
5716
5799
|
}).strict().optional()
|
|
5717
5800
|
}).strict();
|
|
5718
|
-
var ChatCard =
|
|
5801
|
+
var ChatCard = z6.object({
|
|
5719
5802
|
photo: PhotoId.optional(),
|
|
5720
|
-
title:
|
|
5721
|
-
subtitle:
|
|
5803
|
+
title: z6.string().trim().min(1),
|
|
5804
|
+
subtitle: z6.string().trim().min(1).optional(),
|
|
5722
5805
|
/** The card's button, as the chat prints it ("Pagar $899 MXN"). */
|
|
5723
|
-
button:
|
|
5806
|
+
button: z6.string().trim().min(1).optional()
|
|
5724
5807
|
}).strict();
|
|
5725
|
-
var ChatMessage =
|
|
5808
|
+
var ChatMessage = z6.object({
|
|
5726
5809
|
/** `them` is the other side of the chat (the contact in the header), `me` the phone's owner. */
|
|
5727
|
-
from:
|
|
5728
|
-
text:
|
|
5729
|
-
time:
|
|
5810
|
+
from: z6.enum(["them", "me"]),
|
|
5811
|
+
text: z6.string().trim().min(1).optional(),
|
|
5812
|
+
time: z6.string().trim().min(1).optional(),
|
|
5730
5813
|
card: ChatCard.optional()
|
|
5731
5814
|
}).strict().refine((m) => m.text || m.card, "a message has text or a card");
|
|
5732
|
-
var Review =
|
|
5815
|
+
var Review = z6.object({
|
|
5733
5816
|
author: Author,
|
|
5734
|
-
stars:
|
|
5735
|
-
title:
|
|
5736
|
-
text:
|
|
5737
|
-
date:
|
|
5817
|
+
stars: z6.number().int().min(1).max(5).default(5),
|
|
5818
|
+
title: z6.string().trim().min(1).optional(),
|
|
5819
|
+
text: z6.string().trim().min(1),
|
|
5820
|
+
date: z6.string().trim().min(1).optional()
|
|
5738
5821
|
}).strict();
|
|
5739
|
-
var NotesItem =
|
|
5740
|
-
var SlackMessage =
|
|
5822
|
+
var NotesItem = z6.object({ text: z6.string().trim().min(1), checked: z6.boolean().default(false) }).strict();
|
|
5823
|
+
var SlackMessage = z6.object({
|
|
5741
5824
|
author: Author,
|
|
5742
|
-
text:
|
|
5743
|
-
time:
|
|
5744
|
-
reactions:
|
|
5825
|
+
text: z6.string().trim().min(1),
|
|
5826
|
+
time: z6.string().trim().min(1).optional(),
|
|
5827
|
+
reactions: z6.array(z6.object({ emoji: z6.string().trim().min(1), count: z6.number().int().min(1) }).strict()).default([]),
|
|
5745
5828
|
/** "3 replies" under the message. */
|
|
5746
|
-
replies:
|
|
5829
|
+
replies: z6.string().trim().min(1).optional()
|
|
5747
5830
|
}).strict();
|
|
5748
|
-
var Frame =
|
|
5749
|
-
headline:
|
|
5750
|
-
caption:
|
|
5751
|
-
logo:
|
|
5831
|
+
var Frame = z6.object({
|
|
5832
|
+
headline: z6.string().trim().min(1).optional(),
|
|
5833
|
+
caption: z6.string().trim().min(1).optional(),
|
|
5834
|
+
logo: z6.boolean().default(false),
|
|
5752
5835
|
/** Where the frame's logo goes: under the screen, or over it as a badge (a review wall's anchor). */
|
|
5753
|
-
logo_position:
|
|
5836
|
+
logo_position: z6.enum(["under", "over"]).default("under"),
|
|
5754
5837
|
background: Hex.optional()
|
|
5755
5838
|
}).strict().default({ logo: false, logo_position: "under" });
|
|
5756
|
-
var Base = { photos:
|
|
5757
|
-
var NativeSurfaceSchema =
|
|
5758
|
-
|
|
5759
|
-
surface:
|
|
5760
|
-
theme:
|
|
5839
|
+
var Base = { photos: z6.array(Photo).default([]), frame: Frame };
|
|
5840
|
+
var NativeSurfaceSchema = z6.discriminatedUnion("surface", [
|
|
5841
|
+
z6.object({
|
|
5842
|
+
surface: z6.literal("x_post"),
|
|
5843
|
+
theme: z6.enum(["light", "dark"]).default("light"),
|
|
5761
5844
|
/** One post, or a thread: each after the first a reply under it, its line joining them. */
|
|
5762
|
-
posts:
|
|
5845
|
+
posts: z6.array(XPost).min(1).max(4),
|
|
5763
5846
|
...Base
|
|
5764
5847
|
}).strict(),
|
|
5765
|
-
|
|
5766
|
-
surface:
|
|
5848
|
+
z6.object({
|
|
5849
|
+
surface: z6.enum(["whatsapp", "imessage"]),
|
|
5767
5850
|
/** Who the chat is with: the header's name and picture. */
|
|
5768
5851
|
contact: Author,
|
|
5769
|
-
messages:
|
|
5852
|
+
messages: z6.array(ChatMessage).min(1).max(8),
|
|
5770
5853
|
/** The day pill above the messages ("Hoy", "Today"). */
|
|
5771
|
-
day:
|
|
5854
|
+
day: z6.string().trim().min(1).optional(),
|
|
5772
5855
|
/** Drawn inside a phone; else the screen alone. Default true. */
|
|
5773
|
-
device:
|
|
5856
|
+
device: z6.boolean().default(true),
|
|
5774
5857
|
...Base
|
|
5775
5858
|
}).strict(),
|
|
5776
|
-
|
|
5777
|
-
surface:
|
|
5859
|
+
z6.object({
|
|
5860
|
+
surface: z6.enum(["trustpilot", "google_reviews"]),
|
|
5778
5861
|
/** The rating the platform shows for the company: a real one, in `src/content`. */
|
|
5779
|
-
summary:
|
|
5862
|
+
summary: z6.object({ score: z6.string().trim().min(1), count: z6.string().trim().min(1), label: z6.string().optional() }).strict().optional(),
|
|
5780
5863
|
/**
|
|
5781
5864
|
* Every real review the wall shows, the strongest first: the first two are read as the ad's
|
|
5782
5865
|
* copy, whole in the safe area; the rest are the wall they lie on, as large and as sharp, across
|
|
5783
5866
|
* the whole frame and cut by its edges. A wall wins on how many there are (8 to 16), each one a
|
|
5784
5867
|
* real review in the company's own words.
|
|
5785
5868
|
*/
|
|
5786
|
-
reviews:
|
|
5869
|
+
reviews: z6.array(Review).min(1).max(16),
|
|
5787
5870
|
...Base
|
|
5788
5871
|
}).strict(),
|
|
5789
|
-
|
|
5790
|
-
surface:
|
|
5791
|
-
title:
|
|
5792
|
-
items:
|
|
5793
|
-
body:
|
|
5872
|
+
z6.object({
|
|
5873
|
+
surface: z6.literal("notes"),
|
|
5874
|
+
title: z6.string().trim().min(1),
|
|
5875
|
+
items: z6.array(NotesItem).default([]),
|
|
5876
|
+
body: z6.string().trim().min(1).optional(),
|
|
5794
5877
|
...Base
|
|
5795
5878
|
}).strict(),
|
|
5796
|
-
|
|
5797
|
-
surface:
|
|
5798
|
-
channel:
|
|
5799
|
-
messages:
|
|
5879
|
+
z6.object({
|
|
5880
|
+
surface: z6.literal("slack"),
|
|
5881
|
+
channel: z6.string().trim().min(1),
|
|
5882
|
+
messages: z6.array(SlackMessage).min(1).max(6),
|
|
5800
5883
|
...Base
|
|
5801
5884
|
}).strict()
|
|
5802
5885
|
]);
|
|
@@ -5858,8 +5941,8 @@ function authorsOf(surface) {
|
|
|
5858
5941
|
return [];
|
|
5859
5942
|
}
|
|
5860
5943
|
}
|
|
5861
|
-
function nativeChrome(surface, language) {
|
|
5862
|
-
const t = chromeWords(language);
|
|
5944
|
+
function nativeChrome(surface, language, words) {
|
|
5945
|
+
const t = chromeWords(language, words);
|
|
5863
5946
|
const handleParts = (h) => h ? [h, h.replace(/^@/, ""), ...h.replace(/^@/, "").split(/[_.]/)] : [];
|
|
5864
5947
|
const authors = authorsOf(surface).flatMap((a) => [a.name, ...handleParts(a.handle), a.subtitle, initials(a.name)]);
|
|
5865
5948
|
const own = (() => {
|
|
@@ -5939,7 +6022,25 @@ function nativeGateProblems(surface, records, brandName) {
|
|
|
5939
6022
|
}
|
|
5940
6023
|
return problems;
|
|
5941
6024
|
}
|
|
5942
|
-
var
|
|
6025
|
+
var WORD_MEANINGS = {
|
|
6026
|
+
today: "the day label over today's messages in a chat",
|
|
6027
|
+
online: "the status under a chat contact's name",
|
|
6028
|
+
business: "the label under a WhatsApp business contact's name",
|
|
6029
|
+
message: "the placeholder in a chat's message bar",
|
|
6030
|
+
verified: "the badge on a verified review",
|
|
6031
|
+
excellent: "the word a review site prints beside a 4.5-5 star score",
|
|
6032
|
+
reviewsWord: "the plural noun after the review count (1,200 reviews)",
|
|
6033
|
+
notes: "the title of the phone's notes app",
|
|
6034
|
+
experience: "the label before the date a reviewer had the experience",
|
|
6035
|
+
replying: "the words before the handle a post on X replies to",
|
|
6036
|
+
post: "the title over a thread on X",
|
|
6037
|
+
replyPrompt: "the placeholder of the reply bar under a thread on X",
|
|
6038
|
+
messagePrompt: "the placeholder of Slack's message bar, before the channel name"
|
|
6039
|
+
};
|
|
6040
|
+
var InterfaceWordsSchema = z6.object(
|
|
6041
|
+
Object.fromEntries(Object.keys(WORD_MEANINGS).map((k) => [k, z6.string().trim().min(1)]))
|
|
6042
|
+
);
|
|
6043
|
+
var CHECKED = {
|
|
5943
6044
|
es: {
|
|
5944
6045
|
today: "Hoy",
|
|
5945
6046
|
online: "en l\xEDnea",
|
|
@@ -5986,9 +6087,29 @@ var WORDS = {
|
|
|
5986
6087
|
messagePrompt: "Message"
|
|
5987
6088
|
}
|
|
5988
6089
|
};
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
return
|
|
6090
|
+
var primary = (language) => (language ?? "en").split(/[-_]/)[0]?.toLowerCase() ?? "en";
|
|
6091
|
+
function needsInterfaceWords(language) {
|
|
6092
|
+
return language !== void 0 && CHECKED[primary(language)] === void 0;
|
|
6093
|
+
}
|
|
6094
|
+
var INTERFACE_WORDS_PROMPT_VERSION = 1;
|
|
6095
|
+
function interfaceWordsPrompt(language) {
|
|
6096
|
+
const english = CHECKED.en;
|
|
6097
|
+
return [
|
|
6098
|
+
`These are the words WhatsApp, iMessage, X, Slack, Trustpilot, Google reviews and the phone's notes app print around what people write, in English. Give each as those apps print it in their interface for people whose language is ${language}: the apps' own wording, short, in that language's script and capitalisation.`,
|
|
6099
|
+
"",
|
|
6100
|
+
...Object.entries(WORD_MEANINGS).map(
|
|
6101
|
+
([key, meaning]) => `- "${key}": ${JSON.stringify(english[key])} (${meaning})`
|
|
6102
|
+
),
|
|
6103
|
+
"",
|
|
6104
|
+
`Reply with only this JSON, every key above with its words: {${Object.keys(WORD_MEANINGS).map((k) => `"${k}": "\u2026"`).join(", ")}}`
|
|
6105
|
+
].join("\n");
|
|
6106
|
+
}
|
|
6107
|
+
function readInterfaceWords(answer) {
|
|
6108
|
+
const parsed = InterfaceWordsSchema.safeParse(answer);
|
|
6109
|
+
return parsed.success ? parsed.data : null;
|
|
6110
|
+
}
|
|
6111
|
+
function chromeWords(language, words) {
|
|
6112
|
+
return CHECKED[primary(language)] ?? words ?? CHECKED.en;
|
|
5992
6113
|
}
|
|
5993
6114
|
function inked(assets, src, groundIsDark) {
|
|
5994
6115
|
return src && !groundIsDark && assets.lightLogos?.includes(src) ? " inked" : "";
|
|
@@ -6529,7 +6650,7 @@ function frameGround(surface, brand, size2) {
|
|
|
6529
6650
|
function nativeSurfaceHtml(surface, assets, opts) {
|
|
6530
6651
|
const w = opts.width / 2;
|
|
6531
6652
|
const h = opts.height / 2;
|
|
6532
|
-
const t = chromeWords(opts.language);
|
|
6653
|
+
const t = chromeWords(opts.language, opts.words);
|
|
6533
6654
|
const bg = backdrop(surface, opts.brand);
|
|
6534
6655
|
const ink = isDark(bg) ? "#ffffff" : opts.brand.ink ?? "#111111";
|
|
6535
6656
|
const font = assets.font ? `@font-face{font-family:BrandFont;src:url(${assets.font});font-display:block}` : "";
|
|
@@ -6637,7 +6758,7 @@ function layoutBox(safe, w, h) {
|
|
|
6637
6758
|
}
|
|
6638
6759
|
|
|
6639
6760
|
// src/engine/scaffold/static-copy.ts
|
|
6640
|
-
import { z as
|
|
6761
|
+
import { z as z7 } from "zod";
|
|
6641
6762
|
var COPY_DIRECTIVE_HEADER = "TEXT \u2014 RENDER THE COPY EXACTLY:";
|
|
6642
6763
|
var normalize = (s) => typeof s === "string" ? s.trim().toLowerCase() : "";
|
|
6643
6764
|
function textEntries(blueprint) {
|
|
@@ -6679,7 +6800,7 @@ function conceptHasLogo(blueprint) {
|
|
|
6679
6800
|
(l) => normalize(l?.function_in_image) === "advertiser_brand"
|
|
6680
6801
|
);
|
|
6681
6802
|
}
|
|
6682
|
-
var WinningMechanismSchema =
|
|
6803
|
+
var WinningMechanismSchema = z7.object({ kind: z7.string(), device: z7.string(), why_it_works: z7.string().optional() }).loose();
|
|
6683
6804
|
function winningMechanisms(blueprint) {
|
|
6684
6805
|
const raw = blueprint?.winning_mechanisms;
|
|
6685
6806
|
if (!Array.isArray(raw)) return [];
|
|
@@ -6715,58 +6836,58 @@ ${directive}${tail2}`;
|
|
|
6715
6836
|
// src/engine/scaffold/static-ad-spec.ts
|
|
6716
6837
|
var AD_SPEC_SCHEMA = "baker-static-ad/3";
|
|
6717
6838
|
var AD_SPEC_SCHEMAS = ["baker-static-ad/2", AD_SPEC_SCHEMA];
|
|
6718
|
-
var Hex2 =
|
|
6719
|
-
var AdCopyLine =
|
|
6720
|
-
text:
|
|
6721
|
-
lettering:
|
|
6839
|
+
var Hex2 = z8.string().regex(/^#[0-9a-fA-F]{6}$/, "a colour is a 6-digit hex like #123551").transform((h) => h.toLowerCase());
|
|
6840
|
+
var AdCopyLine = z8.object({
|
|
6841
|
+
text: z8.string().trim().min(1),
|
|
6842
|
+
lettering: z8.enum(["in_scene", "designed"]),
|
|
6722
6843
|
/** headline | body | cta | caption | fine_print | label. */
|
|
6723
|
-
role:
|
|
6844
|
+
role: z8.string().optional(),
|
|
6724
6845
|
/** In-scene lines: the object it is written on (a whiteboard, a phone's notes app, a sticky note). */
|
|
6725
|
-
on:
|
|
6846
|
+
on: z8.string().optional(),
|
|
6726
6847
|
/** How it looks: an in-scene line's own lettering (chunky navy marker), a designed line's size or treatment. */
|
|
6727
|
-
look:
|
|
6848
|
+
look: z8.string().optional(),
|
|
6728
6849
|
/** Where it sits, by what is around it ("top of the board", "under the headline"). */
|
|
6729
|
-
position:
|
|
6850
|
+
position: z8.string().optional()
|
|
6730
6851
|
}).strict();
|
|
6731
6852
|
var AD_REFERENCE_ROLES = ["identity", "product", "prop"];
|
|
6732
|
-
var AdReference =
|
|
6853
|
+
var AdReference = z8.object({
|
|
6733
6854
|
/** A role-based label, never a brand's: PERSON_1, ANIMAL_1, PRODUCT_1, BADGE_1, PROP_1. */
|
|
6734
|
-
id:
|
|
6855
|
+
id: z8.string().regex(
|
|
6735
6856
|
/^(PERSON|ANIMAL|PRODUCT|BADGE|PROP)_\d+$/,
|
|
6736
6857
|
"a reference id is PERSON_1, ANIMAL_1, PRODUCT_1, BADGE_1 or PROP_1"
|
|
6737
6858
|
),
|
|
6738
|
-
role:
|
|
6859
|
+
role: z8.enum(AD_REFERENCE_ROLES),
|
|
6739
6860
|
/** The canvas ingest node that holds its picture (bookkeeping, never sent to a model). */
|
|
6740
|
-
slot:
|
|
6861
|
+
slot: z8.string().optional(),
|
|
6741
6862
|
/** How it appears in this ad. */
|
|
6742
|
-
what:
|
|
6863
|
+
what: z8.string().trim().min(1),
|
|
6743
6864
|
/** What must stay exactly as in the picture, when more than the default for its role. */
|
|
6744
|
-
keep:
|
|
6865
|
+
keep: z8.string().optional(),
|
|
6745
6866
|
/** The company avatar cast in this role (`baker avatars list`): a licensed face, never an invented one. */
|
|
6746
|
-
avatar:
|
|
6867
|
+
avatar: z8.string().optional(),
|
|
6747
6868
|
/**
|
|
6748
6869
|
* A picture of ours that shows it (a path from the creative's folder, or a URL): every run
|
|
6749
6870
|
* wires it into the renders (`ref_<id>`), so a picture is named here, never hand-wired.
|
|
6750
6871
|
*/
|
|
6751
|
-
image:
|
|
6872
|
+
image: z8.string().trim().min(1).optional()
|
|
6752
6873
|
}).strict();
|
|
6753
|
-
var Market =
|
|
6874
|
+
var Market = z8.object({
|
|
6754
6875
|
/** ISO country code (ES, MX), or "global" for a version that runs in several. */
|
|
6755
|
-
country:
|
|
6756
|
-
language:
|
|
6757
|
-
currency:
|
|
6876
|
+
country: z8.string().trim().min(2),
|
|
6877
|
+
language: z8.string().trim().min(2).optional(),
|
|
6878
|
+
currency: z8.string().trim().min(1).optional(),
|
|
6758
6879
|
/** How the copy addresses the viewer: tú, usted, vos, você, du, Sie, vous… */
|
|
6759
|
-
address:
|
|
6880
|
+
address: z8.string().trim().min(1).optional()
|
|
6760
6881
|
}).strict();
|
|
6761
|
-
var Viewer =
|
|
6882
|
+
var Viewer = z8.object({
|
|
6762
6883
|
/** Who will see the ad, in the words a marketer would use. */
|
|
6763
|
-
who:
|
|
6884
|
+
who: z8.string().trim().min(1),
|
|
6764
6885
|
market: Market.optional(),
|
|
6765
6886
|
/** What they already know about the brand and the offer; a list (as _definition.md keeps it) reads as one line. */
|
|
6766
|
-
knows:
|
|
6767
|
-
temperature:
|
|
6887
|
+
knows: z8.preprocess((v) => Array.isArray(v) ? v.map(String).join("; ") || void 0 : v, z8.string().optional()),
|
|
6888
|
+
temperature: z8.enum(["cold", "warm", "hot"]).optional(),
|
|
6768
6889
|
/** Where they meet it (feed, Stories, Reels). */
|
|
6769
|
-
placement:
|
|
6890
|
+
placement: z8.string().optional()
|
|
6770
6891
|
}).strict();
|
|
6771
6892
|
var withoutFormatName = (v) => {
|
|
6772
6893
|
if (!v || typeof v !== "object" || Array.isArray(v) || typeof v.concept !== "string")
|
|
@@ -6774,104 +6895,104 @@ var withoutFormatName = (v) => {
|
|
|
6774
6895
|
const { concept: _format, ...scene } = v;
|
|
6775
6896
|
return scene;
|
|
6776
6897
|
};
|
|
6777
|
-
var SceneFields =
|
|
6778
|
-
description:
|
|
6898
|
+
var SceneFields = z8.object({
|
|
6899
|
+
description: z8.string().trim().min(1),
|
|
6779
6900
|
/** A photograph, a designed graphic, or an illustration; plain synonyms ("photography") read as them. */
|
|
6780
|
-
look:
|
|
6901
|
+
look: z8.preprocess(
|
|
6781
6902
|
(v) => typeof v !== "string" ? v : /^photo/i.test(v.trim()) ? "photo" : /^illustrat/i.test(v.trim()) ? "illustration" : /^(design|graphic)/i.test(v.trim()) ? "designed" : v,
|
|
6782
|
-
|
|
6903
|
+
z8.enum(["photo", "designed", "illustration"])
|
|
6783
6904
|
),
|
|
6784
6905
|
/** How a photo was taken, in plain words (handheld at standing height, window light, faint glare). */
|
|
6785
|
-
capture:
|
|
6786
|
-
mood:
|
|
6906
|
+
capture: z8.string().optional(),
|
|
6907
|
+
mood: z8.string().optional(),
|
|
6787
6908
|
/** How the frame is arranged, by what fills each part of it. */
|
|
6788
|
-
layout:
|
|
6909
|
+
layout: z8.string().optional()
|
|
6789
6910
|
}).strict();
|
|
6790
|
-
var Scene =
|
|
6791
|
-
var Brand =
|
|
6792
|
-
name:
|
|
6911
|
+
var Scene = z8.preprocess(withoutFormatName, SceneFields);
|
|
6912
|
+
var Brand = z8.object({
|
|
6913
|
+
name: z8.string().optional(),
|
|
6793
6914
|
/** The words the brand's logo prints (its wordmark), so a check reads them as the logo, never as copy. */
|
|
6794
|
-
lockup:
|
|
6915
|
+
lockup: z8.array(z8.string().trim().min(1)).optional(),
|
|
6795
6916
|
/** The brand's face in words, when no specimen of it is wired ("a heavy geometric sans"). */
|
|
6796
|
-
typeface:
|
|
6917
|
+
typeface: z8.string().optional(),
|
|
6797
6918
|
ground: Hex2.optional(),
|
|
6798
6919
|
ink: Hex2.optional(),
|
|
6799
6920
|
accent: Hex2.optional(),
|
|
6800
6921
|
cta: Hex2.optional(),
|
|
6801
|
-
palette:
|
|
6922
|
+
palette: z8.array(Hex2).default([])
|
|
6802
6923
|
}).strict();
|
|
6803
|
-
var ColorUse =
|
|
6804
|
-
var Mechanism =
|
|
6805
|
-
var ThirdPartyBrand =
|
|
6806
|
-
name:
|
|
6924
|
+
var ColorUse = z8.object({ object: z8.string().trim().min(1), name: z8.string().trim().min(1), hex: Hex2.optional() }).strict();
|
|
6925
|
+
var Mechanism = z8.object({ device: z8.string().trim().min(1), why_it_works: z8.string().optional() }).strict();
|
|
6926
|
+
var ThirdPartyBrand = z8.object({
|
|
6927
|
+
name: z8.string().trim().min(1),
|
|
6807
6928
|
/** Where it shows in this ad, by what carries it ("the car the loan is for"). */
|
|
6808
|
-
what:
|
|
6929
|
+
what: z8.string().optional(),
|
|
6809
6930
|
/** The reference (BADGE_1) that holds a picture of its logo, when one is wired. */
|
|
6810
|
-
logo:
|
|
6931
|
+
logo: z8.string().regex(/^(PERSON|ANIMAL|PRODUCT|BADGE|PROP)_\d+$/, "a logo is a reference id like BADGE_1").optional(),
|
|
6811
6932
|
/**
|
|
6812
6933
|
* Why its logo is on our ad: `partner` (the offer is about it), `proof` (the review platform a
|
|
6813
6934
|
* rating comes from), `category_signal` (how a stranger knows in a second what this is: the
|
|
6814
6935
|
* Instagram and TikTok marks on a social media tool's ad).
|
|
6815
6936
|
*/
|
|
6816
|
-
role:
|
|
6937
|
+
role: z8.enum(["partner", "proof", "category_signal"]).optional()
|
|
6817
6938
|
}).strict();
|
|
6818
|
-
var LoadBearing =
|
|
6939
|
+
var LoadBearing = z8.object({
|
|
6819
6940
|
/** The element, in a few words ("the free gift", "a peer's face", "the notes app"). */
|
|
6820
|
-
element:
|
|
6941
|
+
element: z8.string().trim().min(1),
|
|
6821
6942
|
/** What it takes to carry it ("something the viewer wants for free"). */
|
|
6822
|
-
needs:
|
|
6943
|
+
needs: z8.string().optional(),
|
|
6823
6944
|
/** Whether we have it as it is, can adapt it, or have nothing for it. */
|
|
6824
|
-
have:
|
|
6945
|
+
have: z8.enum(["have", "adapt", "missing"]),
|
|
6825
6946
|
/** What of ours carries it in this ad. */
|
|
6826
|
-
ours:
|
|
6947
|
+
ours: z8.string().optional()
|
|
6827
6948
|
}).strict();
|
|
6828
|
-
var Concept =
|
|
6829
|
-
reference_ad_id:
|
|
6830
|
-
why_it_wins:
|
|
6831
|
-
disguise:
|
|
6832
|
-
voice:
|
|
6833
|
-
trigger:
|
|
6834
|
-
attention:
|
|
6835
|
-
logic:
|
|
6949
|
+
var Concept = z8.object({
|
|
6950
|
+
reference_ad_id: z8.string().optional(),
|
|
6951
|
+
why_it_wins: z8.object({
|
|
6952
|
+
disguise: z8.string().optional(),
|
|
6953
|
+
voice: z8.string().optional(),
|
|
6954
|
+
trigger: z8.string().optional(),
|
|
6955
|
+
attention: z8.string().optional(),
|
|
6956
|
+
logic: z8.string().optional()
|
|
6836
6957
|
}).strict().default({}),
|
|
6837
|
-
load_bearing:
|
|
6838
|
-
verdict:
|
|
6958
|
+
load_bearing: z8.array(LoadBearing).default([]),
|
|
6959
|
+
verdict: z8.enum(["use", "adapt", "reject"]),
|
|
6839
6960
|
/** How the idea transfers to us, in a sentence. Bookkeeping: never compiled into a render. */
|
|
6840
|
-
transfer_note:
|
|
6961
|
+
transfer_note: z8.string().optional()
|
|
6841
6962
|
}).strict();
|
|
6842
|
-
var AdSpecSchema =
|
|
6843
|
-
schema:
|
|
6963
|
+
var AdSpecSchema = z8.object({
|
|
6964
|
+
schema: z8.enum(AD_SPEC_SCHEMAS),
|
|
6844
6965
|
viewer: Viewer.optional(),
|
|
6845
6966
|
/** The one thing the ad tells the viewer. Intent, never printed. */
|
|
6846
|
-
message:
|
|
6847
|
-
copy:
|
|
6967
|
+
message: z8.string().optional(),
|
|
6968
|
+
copy: z8.array(AdCopyLine).default([]),
|
|
6848
6969
|
scene: Scene,
|
|
6849
6970
|
brand: Brand.optional(),
|
|
6850
|
-
colors:
|
|
6851
|
-
references:
|
|
6971
|
+
colors: z8.array(ColorUse).default([]),
|
|
6972
|
+
references: z8.array(AdReference).default([]),
|
|
6852
6973
|
/** The ad shows the brand's logo (the real file, wired on the canvas). */
|
|
6853
|
-
logo:
|
|
6854
|
-
logo_position:
|
|
6974
|
+
logo: z8.boolean().default(false),
|
|
6975
|
+
logo_position: z8.string().optional(),
|
|
6855
6976
|
/** Other brands the offer is about, whose logos the ad may show. */
|
|
6856
|
-
third_party_brands:
|
|
6977
|
+
third_party_brands: z8.array(ThirdPartyBrand).default([]),
|
|
6857
6978
|
/**
|
|
6858
6979
|
* Words the scene prints by itself, never copy: a phone's status bar and app chrome, a code
|
|
6859
6980
|
* editor's line numbers, a product's own label, a platform's badge. The render may draw them
|
|
6860
6981
|
* and the review never calls them extra words.
|
|
6861
6982
|
*/
|
|
6862
|
-
scene_text:
|
|
6983
|
+
scene_text: z8.preprocess(
|
|
6863
6984
|
(v) => Array.isArray(v) ? v.map((w) => w && typeof w === "object" && "text" in w ? w.text : w) : v,
|
|
6864
|
-
|
|
6985
|
+
z8.array(z8.string().trim().min(1)).default([])
|
|
6865
6986
|
),
|
|
6866
6987
|
/** The formats the ad is made for; the canvas delivers them. */
|
|
6867
|
-
formats:
|
|
6988
|
+
formats: z8.array(z8.string()).optional(),
|
|
6868
6989
|
/** An optional note on direction for this concept. */
|
|
6869
|
-
direction:
|
|
6990
|
+
direction: z8.string().optional(),
|
|
6870
6991
|
/** The device the ad is built on, and why it works on a stranger. Judged, never drawn. */
|
|
6871
|
-
mechanism:
|
|
6992
|
+
mechanism: z8.preprocess((v) => {
|
|
6872
6993
|
const list = v && typeof v === "object" && !Array.isArray(v) ? [v] : v;
|
|
6873
6994
|
return Array.isArray(list) ? list.map((m) => typeof m === "string" ? { device: m } : m) : list;
|
|
6874
|
-
},
|
|
6995
|
+
}, z8.array(Mechanism).default([])),
|
|
6875
6996
|
/** Why the reference won and what this ad keeps of it (schema /3). */
|
|
6876
6997
|
concept: Concept.optional(),
|
|
6877
6998
|
/**
|
|
@@ -6879,9 +7000,9 @@ var AdSpecSchema = z7.object({
|
|
|
6879
7000
|
* chat) carries the logo as the sender or avatar and no big button; `standard` the logo large
|
|
6880
7001
|
* and a solid button. Unset: standard.
|
|
6881
7002
|
*/
|
|
6882
|
-
brand_presence:
|
|
7003
|
+
brand_presence: z8.enum(["native", "standard"]).optional(),
|
|
6883
7004
|
/** What kind of product a stranger takes it for in one second, never its format ("a trade course", "a payroll app"). */
|
|
6884
|
-
recognition:
|
|
7005
|
+
recognition: z8.string().optional(),
|
|
6885
7006
|
/**
|
|
6886
7007
|
* The ad is a screen people know (a post on X, a WhatsApp chat, a wall of Trustpilot reviews, a
|
|
6887
7008
|
* note, a Slack message), drawn exactly from HTML at every size (`native-surface.ts`); the image
|
|
@@ -6889,9 +7010,9 @@ var AdSpecSchema = z7.object({
|
|
|
6889
7010
|
*/
|
|
6890
7011
|
native: NativeSurfaceSchema.optional(),
|
|
6891
7012
|
// Bookkeeping: read by the agent and the judge's context, never compiled into a render.
|
|
6892
|
-
source:
|
|
6893
|
-
reference:
|
|
6894
|
-
notes:
|
|
7013
|
+
source: z8.unknown().optional(),
|
|
7014
|
+
reference: z8.unknown().optional(),
|
|
7015
|
+
notes: z8.unknown().optional()
|
|
6895
7016
|
}).strict();
|
|
6896
7017
|
function referenceNodeId(reference) {
|
|
6897
7018
|
return `ref_${reference.id.toLowerCase()}`;
|
|
@@ -6968,7 +7089,7 @@ var FIELD_SHAPES = {
|
|
|
6968
7089
|
};
|
|
6969
7090
|
var SPEC_SHAPES_HINT = `prompt.json shapes: ${["copy", "colors", "viewer"].map((f) => FIELD_SHAPES[f]).join("; ")}.`;
|
|
6970
7091
|
function adSpecErrorText(e) {
|
|
6971
|
-
if (!(e instanceof
|
|
7092
|
+
if (!(e instanceof z8.ZodError)) return e instanceof Error ? e.message : String(e);
|
|
6972
7093
|
const items = /* @__PURE__ */ new Map();
|
|
6973
7094
|
for (const issue of e.issues) {
|
|
6974
7095
|
const [field2, index, key] = issue.path;
|
|
@@ -7244,7 +7365,7 @@ function adSpecFromBlueprint(raw, opts = {}) {
|
|
|
7244
7365
|
};
|
|
7245
7366
|
}
|
|
7246
7367
|
function adSpecJsonSchema() {
|
|
7247
|
-
return
|
|
7368
|
+
return z8.toJSONSchema(AdSpecSchema, { io: "input", unrepresentable: "any" });
|
|
7248
7369
|
}
|
|
7249
7370
|
|
|
7250
7371
|
// src/engine/scaffold/static-brief.ts
|
|
@@ -8545,6 +8666,19 @@ function rolesById(canvas) {
|
|
|
8545
8666
|
}
|
|
8546
8667
|
return out;
|
|
8547
8668
|
}
|
|
8669
|
+
function relaidPictureRole(canvas) {
|
|
8670
|
+
const roles = rolesById(canvas);
|
|
8671
|
+
const final = nodesOf(canvas).find((n) => n.id === "final" && n.type === "collect");
|
|
8672
|
+
const labels = final?.params?.labels;
|
|
8673
|
+
const images = final?.inputs?.images;
|
|
8674
|
+
if (!Array.isArray(labels) || !Array.isArray(images)) return {};
|
|
8675
|
+
const at2 = images.findIndex((ref) => {
|
|
8676
|
+
const from = typeof ref === "string" ? /^\$ref:([^.]+)\./.exec(ref)?.[1] : void 0;
|
|
8677
|
+
return from !== void 0 && roles[from] === STATIC_MASTER_ROLE;
|
|
8678
|
+
});
|
|
8679
|
+
const format = labels[at2];
|
|
8680
|
+
return typeof format === "string" ? { [`last_${formatSlug(format)}`]: STATIC_MASTER_ROLE } : {};
|
|
8681
|
+
}
|
|
8548
8682
|
function formatSlug(format) {
|
|
8549
8683
|
return format.replace(":", "x").replace(".", "_");
|
|
8550
8684
|
}
|
|
@@ -10080,12 +10214,12 @@ function staticInputKind(ctx, value) {
|
|
|
10080
10214
|
if (!value.startsWith(REF_PREFIX)) return "value";
|
|
10081
10215
|
const target = refTarget(ctx, value);
|
|
10082
10216
|
if (!target) return "unknown";
|
|
10083
|
-
const outputSchema = unwrap(shapeOf(target.def.outputs)?.[target.expr.output] ??
|
|
10084
|
-
if (outputSchema instanceof
|
|
10217
|
+
const outputSchema = unwrap(shapeOf(target.def.outputs)?.[target.expr.output] ?? z9.unknown());
|
|
10218
|
+
if (outputSchema instanceof z9.ZodArray && target.expr.index === void 0) return "list";
|
|
10085
10219
|
const kind = refOutputKind(target);
|
|
10086
10220
|
if (kind && ASSET_KINDS.has(kind)) return kind;
|
|
10087
|
-
const plain2 = outputSchema instanceof
|
|
10088
|
-
return plain2 || outputSchema instanceof
|
|
10221
|
+
const plain2 = outputSchema instanceof z9.ZodString || outputSchema instanceof z9.ZodNumber;
|
|
10222
|
+
return plain2 || outputSchema instanceof z9.ZodBoolean ? "value" : "unknown";
|
|
10089
10223
|
}
|
|
10090
10224
|
function staticInputSource(ctx, value) {
|
|
10091
10225
|
if (typeof value !== "string" || !value.startsWith(REF_PREFIX)) return void 0;
|
|
@@ -10252,7 +10386,7 @@ function listShapeKeys(schema) {
|
|
|
10252
10386
|
}
|
|
10253
10387
|
function shapeOf(schema) {
|
|
10254
10388
|
const obj = unwrap(schema);
|
|
10255
|
-
return obj instanceof
|
|
10389
|
+
return obj instanceof z9.ZodObject ? obj.shape : void 0;
|
|
10256
10390
|
}
|
|
10257
10391
|
function unwrap(schema) {
|
|
10258
10392
|
let cur = schema;
|
|
@@ -10794,7 +10928,7 @@ import { mkdtemp, readFile as readFile5, rm, stat as stat2 } from "fs/promises";
|
|
|
10794
10928
|
import { tmpdir } from "os";
|
|
10795
10929
|
import path8 from "path";
|
|
10796
10930
|
import { promisify } from "util";
|
|
10797
|
-
import { z as
|
|
10931
|
+
import { z as z10 } from "zod";
|
|
10798
10932
|
|
|
10799
10933
|
// ../proxy/src/fetch.ts
|
|
10800
10934
|
import { lookup as dnsLookup } from "dns";
|
|
@@ -11549,7 +11683,7 @@ function mapClientError(ctx, e) {
|
|
|
11549
11683
|
|
|
11550
11684
|
// src/engine/nodes/ingest.ts
|
|
11551
11685
|
var execFile = promisify(execFileCb);
|
|
11552
|
-
var ExpectEnum =
|
|
11686
|
+
var ExpectEnum = z10.enum(["image", "video", "audio", "text", "json", "font"]);
|
|
11553
11687
|
function coercePathUrlToUrl(input) {
|
|
11554
11688
|
if (!input || typeof input !== "object") return input;
|
|
11555
11689
|
const obj = input;
|
|
@@ -11559,23 +11693,23 @@ function coercePathUrlToUrl(input) {
|
|
|
11559
11693
|
}
|
|
11560
11694
|
return input;
|
|
11561
11695
|
}
|
|
11562
|
-
var IngestParams =
|
|
11696
|
+
var IngestParams = z10.preprocess(
|
|
11563
11697
|
coercePathUrlToUrl,
|
|
11564
|
-
|
|
11565
|
-
|
|
11566
|
-
source:
|
|
11567
|
-
url:
|
|
11698
|
+
z10.discriminatedUnion("source", [
|
|
11699
|
+
z10.object({
|
|
11700
|
+
source: z10.literal("url"),
|
|
11701
|
+
url: z10.string().url(),
|
|
11568
11702
|
expect: ExpectEnum
|
|
11569
11703
|
}).strict(),
|
|
11570
|
-
|
|
11571
|
-
source:
|
|
11572
|
-
path:
|
|
11704
|
+
z10.object({
|
|
11705
|
+
source: z10.literal("path"),
|
|
11706
|
+
path: z10.string().min(1),
|
|
11573
11707
|
expect: ExpectEnum
|
|
11574
11708
|
}).strict()
|
|
11575
11709
|
])
|
|
11576
11710
|
);
|
|
11577
|
-
var IngestInputs =
|
|
11578
|
-
var IngestOutputs =
|
|
11711
|
+
var IngestInputs = z10.object({}).strict();
|
|
11712
|
+
var IngestOutputs = z10.object({ asset: AssetRef }).strict();
|
|
11579
11713
|
var RAW_TEXT_EXTENSIONS = [".txt", ".md"];
|
|
11580
11714
|
function resolveStrategy(params) {
|
|
11581
11715
|
if (params.source === "path") return "local_file";
|
|
@@ -12159,7 +12293,7 @@ ingestNode.cacheKeyExtras = async ({ params }) => {
|
|
|
12159
12293
|
};
|
|
12160
12294
|
|
|
12161
12295
|
// src/engine/nodes/local/audioTimeline.ts
|
|
12162
|
-
import { z as
|
|
12296
|
+
import { z as z11 } from "zod";
|
|
12163
12297
|
|
|
12164
12298
|
// src/engine/nodes/local/lib/cli-runner.ts
|
|
12165
12299
|
import { execFile as execFileCb2, spawn as spawn2 } from "child_process";
|
|
@@ -12378,50 +12512,50 @@ async function getToolVersion(bin) {
|
|
|
12378
12512
|
var FFMPEG_BIN = "ffmpeg";
|
|
12379
12513
|
var FORMAT_EXT = { mp3: "mp3", wav: "wav", m4a: "m4a" };
|
|
12380
12514
|
var FORMAT_CODEC = { mp3: "libmp3lame", wav: "pcm_s16le", m4a: "aac" };
|
|
12381
|
-
var Track =
|
|
12515
|
+
var Track = z11.object({
|
|
12382
12516
|
/** Input slot name (must match a wired `inputs.<slot>` audio ref). */
|
|
12383
|
-
slot:
|
|
12517
|
+
slot: z11.string().min(1),
|
|
12384
12518
|
/** When this track starts on the timeline, seconds from 0. */
|
|
12385
|
-
start_s:
|
|
12519
|
+
start_s: z11.number().min(0),
|
|
12386
12520
|
/**
|
|
12387
12521
|
* Optional hard cap on this track's length, seconds. The clip is trimmed BEFORE
|
|
12388
12522
|
* placement, so a source that runs long (an over-long voice extract, a converted
|
|
12389
12523
|
* track that came back oversized) cannot bleed into the next track's window.
|
|
12390
12524
|
*/
|
|
12391
|
-
duration_s:
|
|
12525
|
+
duration_s: z11.number().positive().optional(),
|
|
12392
12526
|
/**
|
|
12393
12527
|
* Level adjustment in dB — negative attenuates. A MUSIC BED under speech
|
|
12394
12528
|
* belongs at −20 dB (see the `describe` text: this number is a settled
|
|
12395
12529
|
* decision, not a per-cut judgement).
|
|
12396
12530
|
*/
|
|
12397
|
-
gain_db:
|
|
12531
|
+
gain_db: z11.number().optional().describe(
|
|
12398
12532
|
"Level adjustment in dB (negative attenuates, 0 = untouched). A music bed under speech sits at \u221220 dB and is sidechain-ducked as well; \u221212 dB crowded the voice and \u221213 dB was rejected by the client as too loud. If a silent tail (an end card, a logo plate) sounds empty, do NOT raise the bed \u2014 wire the same bed as a SECOND track windowed over the tail with its own higher gain. Measuring loudness across a window that includes the silent tail is what makes a correct bed look quiet."
|
|
12399
12533
|
)
|
|
12400
12534
|
}).strict();
|
|
12401
|
-
var DuckSpec =
|
|
12402
|
-
track:
|
|
12403
|
-
against:
|
|
12404
|
-
threshold:
|
|
12405
|
-
ratio:
|
|
12535
|
+
var DuckSpec = z11.object({
|
|
12536
|
+
track: z11.string().min(1).describe("Slot to duck \u2014 the music bed."),
|
|
12537
|
+
against: z11.array(z11.string().min(1)).min(1).describe("Key slots whose signal ducks it \u2014 the voice tracks."),
|
|
12538
|
+
threshold: z11.number().min(0).max(1).optional().describe("Linear 0\u20131, not dB. Lower ducks more readily. Default 0.03."),
|
|
12539
|
+
ratio: z11.number().min(1).max(20).optional().describe("How hard it ducks. Default 8; 12 for a firm hold."),
|
|
12406
12540
|
// MILLISECONDS is spelled out because a canvas shipped `attack: 0.05,
|
|
12407
12541
|
// release: 0.45` — read as seconds — which is a 0.45 ms release: the bed
|
|
12408
12542
|
// snapped back to full level between syllables and the client heard the
|
|
12409
12543
|
// music as "too loud" at a level that measured correct.
|
|
12410
|
-
attack:
|
|
12411
|
-
release:
|
|
12544
|
+
attack: z11.number().min(0.01).optional().describe("MILLISECONDS (not seconds). Typical 5\u201350; default 5. A value below 0.5 is a unit mistake."),
|
|
12545
|
+
release: z11.number().min(0.01).optional().describe(
|
|
12412
12546
|
"MILLISECONDS (not seconds). Typical 200\u2013800; default 300. Anything under ~10 makes the bed pump back up between words and is a unit mistake."
|
|
12413
12547
|
)
|
|
12414
12548
|
}).strict();
|
|
12415
|
-
var AudioTimelineParams =
|
|
12416
|
-
tracks:
|
|
12549
|
+
var AudioTimelineParams = z11.object({
|
|
12550
|
+
tracks: z11.array(Track).min(1),
|
|
12417
12551
|
/** Final track length in ms — pads short / trims long. Defaults to the natural mix length. */
|
|
12418
|
-
total_ms:
|
|
12419
|
-
output_format:
|
|
12552
|
+
total_ms: z11.number().int().positive().optional(),
|
|
12553
|
+
output_format: z11.enum(["mp3", "wav", "m4a"]).optional(),
|
|
12420
12554
|
/** Optional sidechain duck of one track under others (e.g. music under voice). */
|
|
12421
12555
|
duck: DuckSpec.optional()
|
|
12422
12556
|
}).strict();
|
|
12423
|
-
var AudioTimelineInputs =
|
|
12424
|
-
var AudioTimelineOutputs =
|
|
12557
|
+
var AudioTimelineInputs = z11.record(z11.string(), z11.unknown());
|
|
12558
|
+
var AudioTimelineOutputs = z11.object({ audio: z11.custom() }).strict();
|
|
12425
12559
|
function applyDuck(params, labelFor, filterChains) {
|
|
12426
12560
|
const duck = params.duck;
|
|
12427
12561
|
if (!duck) return;
|
|
@@ -12535,28 +12669,28 @@ var audioTimelineNode = defineNode({
|
|
|
12535
12669
|
});
|
|
12536
12670
|
|
|
12537
12671
|
// src/engine/nodes/local/avMux.ts
|
|
12538
|
-
import { z as
|
|
12672
|
+
import { z as z12 } from "zod";
|
|
12539
12673
|
var FFMPEG_BIN2 = "ffmpeg";
|
|
12540
|
-
var Loudnorm =
|
|
12541
|
-
i:
|
|
12542
|
-
tp:
|
|
12543
|
-
lra:
|
|
12674
|
+
var Loudnorm = z12.object({
|
|
12675
|
+
i: z12.number().default(-14).describe("Integrated loudness target, LUFS. -14 is the social/streaming target."),
|
|
12676
|
+
tp: z12.number().default(-1.5).describe("True-peak ceiling, dBTP."),
|
|
12677
|
+
lra: z12.number().default(11).describe("Loudness range target, LU.")
|
|
12544
12678
|
}).strict();
|
|
12545
|
-
var AvMuxParams =
|
|
12546
|
-
loudnorm:
|
|
12679
|
+
var AvMuxParams = z12.object({
|
|
12680
|
+
loudnorm: z12.union([z12.literal(false), Loudnorm]).default({ i: -14, tp: -1.5, lra: 11 }).describe(
|
|
12547
12681
|
'Loudness normalization of the soundtrack (EBU R128). The raw mix is a quiet track that reads as "no sound" in casual players, so it is on by default at -14 LUFS / -1.5 dBTP. `false` keeps the mix level.'
|
|
12548
12682
|
),
|
|
12549
|
-
audio_bitrate:
|
|
12550
|
-
sample_rate:
|
|
12551
|
-
length:
|
|
12683
|
+
audio_bitrate: z12.string().regex(/^\d+k$/, 'a bitrate like "192k"').default("192k").describe("AAC bitrate of the delivered soundtrack."),
|
|
12684
|
+
sample_rate: z12.number().int().positive().default(48e3).describe("Delivered sample rate, Hz."),
|
|
12685
|
+
length: z12.union([z12.literal("shortest"), z12.object({ total_s: z12.number().positive() }).strict()]).default("shortest").describe(
|
|
12552
12686
|
"`shortest` ends the file with the shorter stream, so a picture and mix pinned to the same timeline come out exact (a longer one is silently cut: `canvas validate` checks both against the timeline). `{ total_s }` cuts both streams at that second; they must each run at least that long."
|
|
12553
12687
|
)
|
|
12554
12688
|
}).strict();
|
|
12555
|
-
var AvMuxInputs =
|
|
12556
|
-
video:
|
|
12557
|
-
audio:
|
|
12689
|
+
var AvMuxInputs = z12.object({
|
|
12690
|
+
video: z12.custom().describe("The finished picture (its own audio, if any, is dropped)."),
|
|
12691
|
+
audio: z12.custom().describe("The mixed soundtrack, usually an `audio_timeline`.")
|
|
12558
12692
|
}).strict();
|
|
12559
|
-
var AvMuxOutputs =
|
|
12693
|
+
var AvMuxOutputs = z12.object({ video: z12.custom() }).strict();
|
|
12560
12694
|
function audioFilter(params) {
|
|
12561
12695
|
const stereo = "aformat=channel_layouts=stereo";
|
|
12562
12696
|
if (params.loudnorm === false) return stereo;
|
|
@@ -12624,7 +12758,7 @@ var avMuxNode = defineNode({
|
|
|
12624
12758
|
});
|
|
12625
12759
|
|
|
12626
12760
|
// src/engine/nodes/local/collect.ts
|
|
12627
|
-
import { z as
|
|
12761
|
+
import { z as z13 } from "zod";
|
|
12628
12762
|
var collectNode = defineNode({
|
|
12629
12763
|
id: "collect",
|
|
12630
12764
|
version: "1.0.0",
|
|
@@ -12633,9 +12767,9 @@ var collectNode = defineNode({
|
|
|
12633
12767
|
passthroughRefs: true,
|
|
12634
12768
|
summary: "Gather images from multiple upstream nodes into one ordered array \u2014 the standard terminal for multi-variant canvases whose final output is several images.",
|
|
12635
12769
|
when_to_use: "Point the canvas `output` at this node when several independent branches (e.g. one image_generate per scene/variant) must ALL be finals. Wire `inputs.images` as an array of refs like `$ref:gen_billboard_03.images#0` \u2014 each final inherits its producer node id as its label (or set `params.labels` to override), so variants stay identifiable in the dashboard and selection.",
|
|
12636
|
-
inputs:
|
|
12637
|
-
params:
|
|
12638
|
-
outputs:
|
|
12770
|
+
inputs: z13.object({ images: z13.array(ImageRef).min(1) }).strict(),
|
|
12771
|
+
params: z13.object({ labels: z13.array(z13.string().min(1)).min(1).optional() }).strict(),
|
|
12772
|
+
outputs: z13.object({ images: z13.array(ImageRef).min(1) }).strict(),
|
|
12639
12773
|
outputKinds: { images: "image" },
|
|
12640
12774
|
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
12641
12775
|
// Arity is only knowable at validate time when `images` is a literal array;
|
|
@@ -12661,18 +12795,18 @@ var collectNode = defineNode({
|
|
|
12661
12795
|
});
|
|
12662
12796
|
|
|
12663
12797
|
// src/engine/nodes/local/ffmpeg.ts
|
|
12664
|
-
import { z as
|
|
12798
|
+
import { z as z14 } from "zod";
|
|
12665
12799
|
var FFMPEG_BIN3 = "ffmpeg";
|
|
12666
|
-
var OutputDecl =
|
|
12667
|
-
kind:
|
|
12668
|
-
ext:
|
|
12800
|
+
var OutputDecl = z14.object({
|
|
12801
|
+
kind: z14.enum(["image", "video", "audio"]),
|
|
12802
|
+
ext: z14.string().min(1).max(8)
|
|
12669
12803
|
}).strict();
|
|
12670
|
-
var FfmpegParams =
|
|
12671
|
-
args:
|
|
12672
|
-
outputs:
|
|
12804
|
+
var FfmpegParams = z14.object({
|
|
12805
|
+
args: z14.array(z14.string()).min(1),
|
|
12806
|
+
outputs: z14.record(z14.string(), OutputDecl).default({})
|
|
12673
12807
|
}).strict();
|
|
12674
|
-
var FfmpegInputs =
|
|
12675
|
-
var FfmpegOutputs =
|
|
12808
|
+
var FfmpegInputs = z14.record(z14.string(), z14.unknown());
|
|
12809
|
+
var FfmpegOutputs = z14.record(z14.string(), z14.custom());
|
|
12676
12810
|
var ffmpegNode = defineNode({
|
|
12677
12811
|
id: "ffmpeg",
|
|
12678
12812
|
version: "2.0.0",
|
|
@@ -12703,7 +12837,7 @@ import { mkdtemp as mkdtemp3, rm as rm3, writeFile as writeFile4 } from "fs/prom
|
|
|
12703
12837
|
import { createRequire } from "module";
|
|
12704
12838
|
import { tmpdir as tmpdir3 } from "os";
|
|
12705
12839
|
import path11 from "path";
|
|
12706
|
-
import { z as
|
|
12840
|
+
import { z as z15 } from "zod";
|
|
12707
12841
|
|
|
12708
12842
|
// src/engine/nodes/local/lib/assets.ts
|
|
12709
12843
|
import { copyFile as copyFile3, readFile as readFile7 } from "fs/promises";
|
|
@@ -12733,15 +12867,15 @@ var DEFAULT_SPECIMEN = [
|
|
|
12733
12867
|
"abcdefghijklmnopqrstuvwxyz",
|
|
12734
12868
|
`0123456789 !?&@#$%().,:;'"-`
|
|
12735
12869
|
].join("\n");
|
|
12736
|
-
var FontSpecimenParams =
|
|
12737
|
-
text:
|
|
12738
|
-
font_size:
|
|
12739
|
-
padding:
|
|
12740
|
-
line_height:
|
|
12741
|
-
max_width:
|
|
12870
|
+
var FontSpecimenParams = z15.object({
|
|
12871
|
+
text: z15.string().min(1).max(2e3).optional().default(DEFAULT_SPECIMEN),
|
|
12872
|
+
font_size: z15.number().int().min(8).max(512).optional().default(72),
|
|
12873
|
+
padding: z15.number().int().min(0).max(512).optional().default(64),
|
|
12874
|
+
line_height: z15.number().min(0.8).max(3).optional().default(1.35),
|
|
12875
|
+
max_width: z15.number().int().min(256).max(4096).optional()
|
|
12742
12876
|
}).strict();
|
|
12743
|
-
var FontSpecimenInputs =
|
|
12744
|
-
var FontSpecimenOutputs =
|
|
12877
|
+
var FontSpecimenInputs = z15.object({ font: FontRef }).loose();
|
|
12878
|
+
var FontSpecimenOutputs = z15.object({ image: ImageRef }).strict();
|
|
12745
12879
|
var DEVICE_SCALE_FACTOR = 2;
|
|
12746
12880
|
var PAGE_TIMEOUT_MS = 3e4;
|
|
12747
12881
|
function escapeHtml(text2) {
|
|
@@ -12881,7 +13015,7 @@ import { mkdtemp as mkdtemp5, readFile as readFile12, rm as rm5, stat as stat6,
|
|
|
12881
13015
|
import { cpus, tmpdir as tmpdir5 } from "os";
|
|
12882
13016
|
import path18 from "path";
|
|
12883
13017
|
import { promisify as promisify4 } from "util";
|
|
12884
|
-
import { z as
|
|
13018
|
+
import { z as z17 } from "zod";
|
|
12885
13019
|
|
|
12886
13020
|
// src/engine/engine/composition-hash.ts
|
|
12887
13021
|
import { readdir as readdir2, readFile as readFile8, stat as stat4 } from "fs/promises";
|
|
@@ -12919,62 +13053,62 @@ async function collectFiles(root, current) {
|
|
|
12919
13053
|
// src/engine/engine/composition-meta.ts
|
|
12920
13054
|
import { readFile as readFile9 } from "fs/promises";
|
|
12921
13055
|
import path13 from "path";
|
|
12922
|
-
import { z as
|
|
12923
|
-
var InputKind =
|
|
12924
|
-
var InputSpec =
|
|
13056
|
+
import { z as z16 } from "zod";
|
|
13057
|
+
var InputKind = z16.enum(["video", "image", "audio", "json", "font"]);
|
|
13058
|
+
var InputSpec = z16.object({
|
|
12925
13059
|
kind: InputKind,
|
|
12926
|
-
required:
|
|
13060
|
+
required: z16.boolean().optional().default(false),
|
|
12927
13061
|
// Filename the composition's HTML references (e.g. `input.mp4`, `logo.png`).
|
|
12928
13062
|
// Defaults to `<key><ext>` derived from the kind.
|
|
12929
|
-
staged_as:
|
|
12930
|
-
description:
|
|
13063
|
+
staged_as: z16.string().min(1).optional(),
|
|
13064
|
+
description: z16.string().optional()
|
|
12931
13065
|
}).strict();
|
|
12932
13066
|
var ParamSpecBase = {
|
|
12933
|
-
required:
|
|
12934
|
-
description:
|
|
13067
|
+
required: z16.boolean().optional().default(false),
|
|
13068
|
+
description: z16.string().optional()
|
|
12935
13069
|
};
|
|
12936
|
-
var StringParam =
|
|
13070
|
+
var StringParam = z16.object({
|
|
12937
13071
|
...ParamSpecBase,
|
|
12938
|
-
kind:
|
|
12939
|
-
default:
|
|
12940
|
-
enum:
|
|
13072
|
+
kind: z16.literal("string"),
|
|
13073
|
+
default: z16.string().optional(),
|
|
13074
|
+
enum: z16.array(z16.string()).optional()
|
|
12941
13075
|
}).strict();
|
|
12942
|
-
var IntegerParam =
|
|
13076
|
+
var IntegerParam = z16.object({
|
|
12943
13077
|
...ParamSpecBase,
|
|
12944
|
-
kind:
|
|
12945
|
-
default:
|
|
12946
|
-
min:
|
|
12947
|
-
max:
|
|
13078
|
+
kind: z16.literal("integer"),
|
|
13079
|
+
default: z16.number().int().optional(),
|
|
13080
|
+
min: z16.number().int().optional(),
|
|
13081
|
+
max: z16.number().int().optional()
|
|
12948
13082
|
}).strict();
|
|
12949
|
-
var NumberParam =
|
|
13083
|
+
var NumberParam = z16.object({
|
|
12950
13084
|
...ParamSpecBase,
|
|
12951
|
-
kind:
|
|
12952
|
-
default:
|
|
12953
|
-
min:
|
|
12954
|
-
max:
|
|
13085
|
+
kind: z16.literal("number"),
|
|
13086
|
+
default: z16.number().optional(),
|
|
13087
|
+
min: z16.number().optional(),
|
|
13088
|
+
max: z16.number().optional()
|
|
12955
13089
|
}).strict();
|
|
12956
|
-
var BooleanParam =
|
|
13090
|
+
var BooleanParam = z16.object({
|
|
12957
13091
|
...ParamSpecBase,
|
|
12958
|
-
kind:
|
|
12959
|
-
default:
|
|
13092
|
+
kind: z16.literal("boolean"),
|
|
13093
|
+
default: z16.boolean().optional()
|
|
12960
13094
|
}).strict();
|
|
12961
|
-
var ColorParam =
|
|
13095
|
+
var ColorParam = z16.object({
|
|
12962
13096
|
...ParamSpecBase,
|
|
12963
|
-
kind:
|
|
12964
|
-
default:
|
|
13097
|
+
kind: z16.literal("color"),
|
|
13098
|
+
default: z16.string().optional()
|
|
12965
13099
|
}).strict();
|
|
12966
|
-
var ImageParam =
|
|
13100
|
+
var ImageParam = z16.object({
|
|
12967
13101
|
...ParamSpecBase,
|
|
12968
|
-
kind:
|
|
12969
|
-
default:
|
|
13102
|
+
kind: z16.literal("image"),
|
|
13103
|
+
default: z16.string().optional()
|
|
12970
13104
|
}).strict();
|
|
12971
|
-
var JsonParam =
|
|
13105
|
+
var JsonParam = z16.object({
|
|
12972
13106
|
...ParamSpecBase,
|
|
12973
|
-
kind:
|
|
12974
|
-
schema:
|
|
12975
|
-
default:
|
|
13107
|
+
kind: z16.literal("json"),
|
|
13108
|
+
schema: z16.unknown().optional(),
|
|
13109
|
+
default: z16.unknown().optional()
|
|
12976
13110
|
}).strict();
|
|
12977
|
-
var ParamSpec =
|
|
13111
|
+
var ParamSpec = z16.discriminatedUnion("kind", [
|
|
12978
13112
|
StringParam,
|
|
12979
13113
|
IntegerParam,
|
|
12980
13114
|
NumberParam,
|
|
@@ -12983,21 +13117,21 @@ var ParamSpec = z15.discriminatedUnion("kind", [
|
|
|
12983
13117
|
ImageParam,
|
|
12984
13118
|
JsonParam
|
|
12985
13119
|
]);
|
|
12986
|
-
var CompositionMetaSchema =
|
|
12987
|
-
id:
|
|
12988
|
-
title:
|
|
12989
|
-
description:
|
|
12990
|
-
width:
|
|
12991
|
-
height:
|
|
12992
|
-
fps:
|
|
12993
|
-
default_duration:
|
|
12994
|
-
inputs:
|
|
12995
|
-
params:
|
|
13120
|
+
var CompositionMetaSchema = z16.object({
|
|
13121
|
+
id: z16.string().min(1),
|
|
13122
|
+
title: z16.string().min(1),
|
|
13123
|
+
description: z16.string().optional(),
|
|
13124
|
+
width: z16.number().int().positive(),
|
|
13125
|
+
height: z16.number().int().positive(),
|
|
13126
|
+
fps: z16.number().int().positive().default(30),
|
|
13127
|
+
default_duration: z16.number().positive().default(10),
|
|
13128
|
+
inputs: z16.record(z16.string(), InputSpec).default({}),
|
|
13129
|
+
params: z16.record(z16.string(), ParamSpec).default({}),
|
|
12996
13130
|
/**
|
|
12997
13131
|
* The page reads its own staged files (a picture's pixels), so a snapshot opens it with
|
|
12998
13132
|
* file access. Off by default: Chromium grants it for every file:// path, not just these.
|
|
12999
13133
|
*/
|
|
13000
|
-
reads_staged_files:
|
|
13134
|
+
reads_staged_files: z16.boolean().optional()
|
|
13001
13135
|
}).strict();
|
|
13002
13136
|
async function loadCompositionMeta(compositionDir) {
|
|
13003
13137
|
const metaPath = path13.join(compositionDir, "meta.json");
|
|
@@ -13025,39 +13159,39 @@ function buildParamsSchema(meta) {
|
|
|
13025
13159
|
for (const [name, spec] of Object.entries(meta.params)) {
|
|
13026
13160
|
shape[name] = buildParamFieldSchema(name, spec);
|
|
13027
13161
|
}
|
|
13028
|
-
return
|
|
13162
|
+
return z16.object(shape).strict();
|
|
13029
13163
|
}
|
|
13030
13164
|
function buildParamFieldSchema(name, spec) {
|
|
13031
13165
|
switch (spec.kind) {
|
|
13032
13166
|
case "string": {
|
|
13033
|
-
const s = spec.enum && spec.enum.length > 0 ?
|
|
13167
|
+
const s = spec.enum && spec.enum.length > 0 ? z16.enum(spec.enum) : z16.string();
|
|
13034
13168
|
return finalize(s, spec.default, spec.required);
|
|
13035
13169
|
}
|
|
13036
13170
|
case "integer": {
|
|
13037
|
-
let s =
|
|
13171
|
+
let s = z16.number().int();
|
|
13038
13172
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
13039
13173
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
13040
13174
|
return finalize(s, spec.default, spec.required);
|
|
13041
13175
|
}
|
|
13042
13176
|
case "number": {
|
|
13043
|
-
let s =
|
|
13177
|
+
let s = z16.number();
|
|
13044
13178
|
if (spec.min !== void 0) s = s.min(spec.min);
|
|
13045
13179
|
if (spec.max !== void 0) s = s.max(spec.max);
|
|
13046
13180
|
return finalize(s, spec.default, spec.required);
|
|
13047
13181
|
}
|
|
13048
13182
|
case "boolean":
|
|
13049
|
-
return finalize(
|
|
13183
|
+
return finalize(z16.boolean(), spec.default, spec.required);
|
|
13050
13184
|
case "color": {
|
|
13051
|
-
const s =
|
|
13185
|
+
const s = z16.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/, {
|
|
13052
13186
|
message: `param "${name}": must be a 3/6/8-digit hex color (e.g. "#ff0066")`
|
|
13053
13187
|
});
|
|
13054
13188
|
return finalize(s, spec.default, spec.required);
|
|
13055
13189
|
}
|
|
13056
13190
|
case "image":
|
|
13057
|
-
return finalize(
|
|
13191
|
+
return finalize(z16.union([z16.string().min(1), z16.record(z16.string(), z16.unknown())]), spec.default, spec.required);
|
|
13058
13192
|
case "json":
|
|
13059
13193
|
return finalize(
|
|
13060
|
-
|
|
13194
|
+
z16.unknown().refine((v) => v !== void 0, { message: `param "${name}" is required` }),
|
|
13061
13195
|
spec.default,
|
|
13062
13196
|
spec.required
|
|
13063
13197
|
);
|
|
@@ -13653,17 +13787,17 @@ ${stderr.slice(0, 1500)}`;
|
|
|
13653
13787
|
|
|
13654
13788
|
// src/engine/nodes/local/hyperframe.ts
|
|
13655
13789
|
var execFileAsync2 = promisify4(execFile4);
|
|
13656
|
-
var HyperframeParams =
|
|
13657
|
-
composition:
|
|
13790
|
+
var HyperframeParams = z17.object({
|
|
13791
|
+
composition: z17.string().min(1),
|
|
13658
13792
|
// Output container. mp4 (default) for delivery; webm/mov render WITH
|
|
13659
13793
|
// transparency (alpha) when the composition background is transparent —
|
|
13660
13794
|
// use for motion-graphic overlays dropped into Premiere/AE/Nuke.
|
|
13661
|
-
format:
|
|
13662
|
-
timeout_ms:
|
|
13663
|
-
}).catchall(
|
|
13664
|
-
var HyperframeInputs =
|
|
13665
|
-
var HyperframeOutputs =
|
|
13666
|
-
video:
|
|
13795
|
+
format: z17.enum(["mp4", "webm", "mov"]).optional().default("mp4"),
|
|
13796
|
+
timeout_ms: z17.number().int().positive().optional().default(10 * 60 * 1e3)
|
|
13797
|
+
}).catchall(z17.unknown());
|
|
13798
|
+
var HyperframeInputs = z17.record(z17.string(), z17.custom()).optional().default({});
|
|
13799
|
+
var HyperframeOutputs = z17.object({
|
|
13800
|
+
video: z17.custom()
|
|
13667
13801
|
}).strict();
|
|
13668
13802
|
var NODE_OWNED_PARAM_KEYS = /* @__PURE__ */ new Set(["composition", "format", "timeout_ms"]);
|
|
13669
13803
|
var MIME_BY_FORMAT = {
|
|
@@ -13940,23 +14074,23 @@ import { createRequire as createRequire3 } from "module";
|
|
|
13940
14074
|
import { tmpdir as tmpdir6 } from "os";
|
|
13941
14075
|
import path19 from "path";
|
|
13942
14076
|
import { promisify as promisify5 } from "util";
|
|
13943
|
-
import { z as
|
|
14077
|
+
import { z as z18 } from "zod";
|
|
13944
14078
|
var _execFileAsync = promisify5(execFile5);
|
|
13945
14079
|
var require_3 = createRequire3(import.meta.url);
|
|
13946
|
-
var WaitForSpec =
|
|
13947
|
-
|
|
13948
|
-
|
|
13949
|
-
|
|
13950
|
-
|
|
14080
|
+
var WaitForSpec = z18.discriminatedUnion("kind", [
|
|
14081
|
+
z18.object({ kind: z18.literal("auto") }),
|
|
14082
|
+
z18.object({ kind: z18.literal("selector"), value: z18.string().min(1) }),
|
|
14083
|
+
z18.object({ kind: z18.literal("function"), value: z18.string().min(1) }),
|
|
14084
|
+
z18.object({ kind: z18.literal("timeout"), ms: z18.number().int().min(0).max(6e4) })
|
|
13951
14085
|
]);
|
|
13952
|
-
var HyperframeSnapshotParams =
|
|
13953
|
-
composition:
|
|
14086
|
+
var HyperframeSnapshotParams = z18.object({
|
|
14087
|
+
composition: z18.string().min(1),
|
|
13954
14088
|
wait_for: WaitForSpec.optional().default({ kind: "auto" }),
|
|
13955
|
-
timeout_ms:
|
|
13956
|
-
}).catchall(
|
|
13957
|
-
var HyperframeSnapshotInputs =
|
|
13958
|
-
var HyperframeSnapshotOutputs =
|
|
13959
|
-
image:
|
|
14089
|
+
timeout_ms: z18.number().int().positive().optional().default(6e4)
|
|
14090
|
+
}).catchall(z18.unknown());
|
|
14091
|
+
var HyperframeSnapshotInputs = z18.record(z18.string(), z18.custom()).optional().default({});
|
|
14092
|
+
var HyperframeSnapshotOutputs = z18.object({
|
|
14093
|
+
image: z18.custom()
|
|
13960
14094
|
}).strict();
|
|
13961
14095
|
var NODE_OWNED_PARAM_KEYS2 = /* @__PURE__ */ new Set(["composition", "wait_for", "timeout_ms"]);
|
|
13962
14096
|
var DEVICE_SCALE_FACTOR2 = 2;
|
|
@@ -14134,18 +14268,18 @@ async function waitForReady(page, waitFor, timeoutMs) {
|
|
|
14134
14268
|
// src/engine/nodes/local/imagemagick.ts
|
|
14135
14269
|
import { execFile as execFile6 } from "child_process";
|
|
14136
14270
|
import { promisify as promisify6 } from "util";
|
|
14137
|
-
import { z as
|
|
14271
|
+
import { z as z19 } from "zod";
|
|
14138
14272
|
var execFileAsync3 = promisify6(execFile6);
|
|
14139
|
-
var OutputDecl2 =
|
|
14140
|
-
kind:
|
|
14141
|
-
ext:
|
|
14273
|
+
var OutputDecl2 = z19.object({
|
|
14274
|
+
kind: z19.enum(["image", "video", "audio"]),
|
|
14275
|
+
ext: z19.string().min(1).max(8)
|
|
14142
14276
|
}).strict();
|
|
14143
|
-
var ImageMagickParams =
|
|
14144
|
-
args:
|
|
14145
|
-
outputs:
|
|
14277
|
+
var ImageMagickParams = z19.object({
|
|
14278
|
+
args: z19.array(z19.string()).min(1),
|
|
14279
|
+
outputs: z19.record(z19.string(), OutputDecl2).default({})
|
|
14146
14280
|
}).strict();
|
|
14147
|
-
var ImageMagickInputs =
|
|
14148
|
-
var ImageMagickOutputs =
|
|
14281
|
+
var ImageMagickInputs = z19.record(z19.string(), z19.unknown());
|
|
14282
|
+
var ImageMagickOutputs = z19.record(z19.string(), z19.custom());
|
|
14149
14283
|
var resolvedBin;
|
|
14150
14284
|
async function resolveBin() {
|
|
14151
14285
|
if (resolvedBin) return resolvedBin;
|
|
@@ -14188,7 +14322,7 @@ var imagemagickNode = defineNode({
|
|
|
14188
14322
|
|
|
14189
14323
|
// src/engine/nodes/local/staticNative.ts
|
|
14190
14324
|
import { createRequire as createRequire4 } from "module";
|
|
14191
|
-
import { z as
|
|
14325
|
+
import { z as z23 } from "zod";
|
|
14192
14326
|
|
|
14193
14327
|
// src/engine/nodes/local/lib/native-assets.ts
|
|
14194
14328
|
import sharp from "sharp";
|
|
@@ -14305,7 +14439,7 @@ function leadingMark(opaque, box) {
|
|
|
14305
14439
|
import { createHash as createHash2 } from "crypto";
|
|
14306
14440
|
import { readFile as readFile15 } from "fs/promises";
|
|
14307
14441
|
import sharp5 from "sharp";
|
|
14308
|
-
import { z as
|
|
14442
|
+
import { z as z22 } from "zod";
|
|
14309
14443
|
|
|
14310
14444
|
// src/engine/landing-library/visualHash.ts
|
|
14311
14445
|
import sharp2 from "sharp";
|
|
@@ -14711,7 +14845,7 @@ import { mkdtemp as mkdtemp7, readdir as readdir3, readFile as readFile14, rm as
|
|
|
14711
14845
|
import { tmpdir as tmpdir7 } from "os";
|
|
14712
14846
|
import path20 from "path";
|
|
14713
14847
|
import { promisify as promisify7 } from "util";
|
|
14714
|
-
import { z as
|
|
14848
|
+
import { z as z20 } from "zod";
|
|
14715
14849
|
var RENDER_REVIEW_MODEL = "~google/gemini-pro-latest";
|
|
14716
14850
|
var RENDER_REVIEW_FRAMES = 12;
|
|
14717
14851
|
var RENDER_REVIEW_WIDTH = 1024;
|
|
@@ -14728,6 +14862,27 @@ function usageFrom(model, body) {
|
|
|
14728
14862
|
};
|
|
14729
14863
|
}
|
|
14730
14864
|
var WATCH_MAX_BYTES = 60 * 1024 * 1024;
|
|
14865
|
+
var STATIC_REVIEW_ITEMS = [
|
|
14866
|
+
"text",
|
|
14867
|
+
"logo",
|
|
14868
|
+
"layout",
|
|
14869
|
+
"safe_zone",
|
|
14870
|
+
"picture",
|
|
14871
|
+
"subject",
|
|
14872
|
+
"person",
|
|
14873
|
+
"people",
|
|
14874
|
+
"one_idea",
|
|
14875
|
+
"thumbnail",
|
|
14876
|
+
"concept",
|
|
14877
|
+
"viewer",
|
|
14878
|
+
"proof",
|
|
14879
|
+
"sizes",
|
|
14880
|
+
"native",
|
|
14881
|
+
"other"
|
|
14882
|
+
];
|
|
14883
|
+
var FINDING_ELEMENTS = ["copy", "button", "logo", "scene_text", "picture", "other"];
|
|
14884
|
+
var FINDING_DEFECTS = ["cut", "missing", "unreadable", "other"];
|
|
14885
|
+
var FRAME_EDGES = ["top", "bottom", "left", "right"];
|
|
14731
14886
|
function sceneAt(windows, atS) {
|
|
14732
14887
|
return windows.find((w) => atS >= w.startS && atS < w.endS)?.file;
|
|
14733
14888
|
}
|
|
@@ -14753,24 +14908,24 @@ function buildFrameGrabArgs(video, atS, out) {
|
|
|
14753
14908
|
"-y"
|
|
14754
14909
|
];
|
|
14755
14910
|
}
|
|
14756
|
-
var FrameReview =
|
|
14911
|
+
var FrameReview = z20.object({
|
|
14757
14912
|
/** Something in shot that could not happen, or is being done wrong. */
|
|
14758
|
-
impossible:
|
|
14913
|
+
impossible: z20.array(z20.string()).default([]),
|
|
14759
14914
|
/** Objects present in frame that belong to no part of the scene. */
|
|
14760
|
-
strays:
|
|
14915
|
+
strays: z20.array(z20.string()).default([])
|
|
14761
14916
|
});
|
|
14762
|
-
var ReelReview =
|
|
14917
|
+
var ReelReview = z20.object({
|
|
14763
14918
|
/** Whether the people the script treats as one person stay one person. */
|
|
14764
|
-
cast_is_consistent:
|
|
14765
|
-
cast_note:
|
|
14919
|
+
cast_is_consistent: z20.boolean().default(true),
|
|
14920
|
+
cast_note: z20.string().default(""),
|
|
14766
14921
|
/** Whether the advertiser's mark is visible anywhere. */
|
|
14767
|
-
brand_mark_visible:
|
|
14922
|
+
brand_mark_visible: z20.boolean().default(false),
|
|
14768
14923
|
/** Whether the last frames close the ad rather than trailing off. */
|
|
14769
|
-
ends_on_a_close:
|
|
14770
|
-
ending_note:
|
|
14924
|
+
ends_on_a_close: z20.boolean().default(false),
|
|
14925
|
+
ending_note: z20.string().default(""),
|
|
14771
14926
|
/** Whether the ad ever cuts away from the presenter to show anything. */
|
|
14772
|
-
shows_more_than_a_face:
|
|
14773
|
-
shows_note:
|
|
14927
|
+
shows_more_than_a_face: z20.boolean().default(true),
|
|
14928
|
+
shows_note: z20.string().default("")
|
|
14774
14929
|
});
|
|
14775
14930
|
function buildFrameReviewPrompt(opts) {
|
|
14776
14931
|
const subject = opts.subject ? ` The advertiser is ${opts.subject}.` : "";
|
|
@@ -14863,20 +15018,20 @@ function buildWatchReviewPrompt(opts) {
|
|
|
14863
15018
|
4. DEFECTS. List anything else wrong, each with the exact timestamp where it happens \u2014 a moment maps to the shot that produced it, and a defect without one is a complaint rather than an instruction.
|
|
14864
15019
|
Answer JSON only: {"spoken_variety_matches":true|false,"variety_note":"...","voice_matches_mouth":true|false,"mouth_note":"...","cuts_are_clean":true|false,"cuts_note":"...","defects":[{"at":"MM:SS.s","kind":"cut|lipsync|audio|visual","what":"..."}]}`;
|
|
14865
15020
|
}
|
|
14866
|
-
var WatchReview =
|
|
14867
|
-
spoken_variety_matches:
|
|
14868
|
-
variety_note:
|
|
14869
|
-
voice_matches_mouth:
|
|
14870
|
-
mouth_note:
|
|
14871
|
-
cuts_are_clean:
|
|
14872
|
-
cuts_note:
|
|
15021
|
+
var WatchReview = z20.object({
|
|
15022
|
+
spoken_variety_matches: z20.boolean(),
|
|
15023
|
+
variety_note: z20.string().default(""),
|
|
15024
|
+
voice_matches_mouth: z20.boolean(),
|
|
15025
|
+
mouth_note: z20.string().default(""),
|
|
15026
|
+
cuts_are_clean: z20.boolean(),
|
|
15027
|
+
cuts_note: z20.string().default("")
|
|
14873
15028
|
});
|
|
14874
|
-
var WatchDefects =
|
|
14875
|
-
defects:
|
|
14876
|
-
|
|
14877
|
-
at:
|
|
14878
|
-
kind:
|
|
14879
|
-
what:
|
|
15029
|
+
var WatchDefects = z20.object({
|
|
15030
|
+
defects: z20.array(
|
|
15031
|
+
z20.object({
|
|
15032
|
+
at: z20.string().default(""),
|
|
15033
|
+
kind: z20.string().default(""),
|
|
15034
|
+
what: z20.string().default("")
|
|
14880
15035
|
})
|
|
14881
15036
|
).default([])
|
|
14882
15037
|
});
|
|
@@ -14901,10 +15056,10 @@ function classifyWatchDefects(json, windows) {
|
|
|
14901
15056
|
};
|
|
14902
15057
|
});
|
|
14903
15058
|
}
|
|
14904
|
-
var FidelityReview =
|
|
14905
|
-
delivers_the_ask:
|
|
14906
|
-
missing:
|
|
14907
|
-
contradicted:
|
|
15059
|
+
var FidelityReview = z20.object({
|
|
15060
|
+
delivers_the_ask: z20.boolean(),
|
|
15061
|
+
missing: z20.array(z20.string()).default([]),
|
|
15062
|
+
contradicted: z20.array(z20.string()).default([])
|
|
14908
15063
|
});
|
|
14909
15064
|
function buildFidelityPrompt(brief) {
|
|
14910
15065
|
return `Someone asked for an advertisement in these words:
|
|
@@ -15071,7 +15226,7 @@ async function videoDuration(video) {
|
|
|
15071
15226
|
return null;
|
|
15072
15227
|
}
|
|
15073
15228
|
}
|
|
15074
|
-
async function askGateway(model, parts, timeoutMs, spent, reasoning) {
|
|
15229
|
+
async function askGateway(model, parts, timeoutMs, spent, reasoning, extra = {}) {
|
|
15075
15230
|
const key = openRouterKey();
|
|
15076
15231
|
if (!key) return null;
|
|
15077
15232
|
try {
|
|
@@ -15082,7 +15237,8 @@ async function askGateway(model, parts, timeoutMs, spent, reasoning) {
|
|
|
15082
15237
|
model,
|
|
15083
15238
|
messages: [{ role: "user", content: parts }],
|
|
15084
15239
|
usage: { include: true },
|
|
15085
|
-
...reasoning ? { reasoning } : {}
|
|
15240
|
+
...reasoning ? { reasoning } : {},
|
|
15241
|
+
...extra
|
|
15086
15242
|
}),
|
|
15087
15243
|
signal: AbortSignal.timeout(timeoutMs)
|
|
15088
15244
|
});
|
|
@@ -15146,9 +15302,16 @@ async function askAboutImages(model, images, prompt, onSpend, timeoutMs = 6e4, r
|
|
|
15146
15302
|
reasoning
|
|
15147
15303
|
);
|
|
15148
15304
|
}
|
|
15149
|
-
async function askAboutText(model, prompt, onSpend) {
|
|
15305
|
+
async function askAboutText(model, prompt, onSpend, opts = {}) {
|
|
15150
15306
|
if (!hasOpenRouterKey()) return null;
|
|
15151
|
-
return await askGateway(
|
|
15307
|
+
return await askGateway(
|
|
15308
|
+
model,
|
|
15309
|
+
[{ type: "text", text: prompt }],
|
|
15310
|
+
opts.timeoutMs ?? 6e4,
|
|
15311
|
+
(usage) => onSpend?.(usage),
|
|
15312
|
+
void 0,
|
|
15313
|
+
opts.request
|
|
15314
|
+
);
|
|
15152
15315
|
}
|
|
15153
15316
|
async function reviewRenderedVideo(opts) {
|
|
15154
15317
|
if (!hasOpenRouterKey()) return null;
|
|
@@ -15267,14 +15430,14 @@ function unreviewedNotes(state) {
|
|
|
15267
15430
|
|
|
15268
15431
|
// src/engine/nodes/local/lib/text-boxes.ts
|
|
15269
15432
|
import sharp4 from "sharp";
|
|
15270
|
-
import { z as
|
|
15433
|
+
import { z as z21 } from "zod";
|
|
15271
15434
|
var GRID = 1e3;
|
|
15272
|
-
var ReaderItem =
|
|
15273
|
-
text:
|
|
15274
|
-
kind:
|
|
15275
|
-
box_2d:
|
|
15435
|
+
var ReaderItem = z21.object({
|
|
15436
|
+
text: z21.string(),
|
|
15437
|
+
kind: z21.enum(["text", "logo"]),
|
|
15438
|
+
box_2d: z21.array(z21.number()).length(4)
|
|
15276
15439
|
});
|
|
15277
|
-
var ReaderAnswer =
|
|
15440
|
+
var ReaderAnswer = z21.object({ items: z21.array(ReaderItem) });
|
|
15278
15441
|
var fraction = (value) => Math.min(1, Math.max(0, value / GRID));
|
|
15279
15442
|
function textBoxesFromReader(answer) {
|
|
15280
15443
|
const parsed = ReaderAnswer.safeParse(answer);
|
|
@@ -15392,32 +15555,39 @@ function judgeWinner(judge) {
|
|
|
15392
15555
|
};
|
|
15393
15556
|
return [...judge].sort(byGate)[0] ?? null;
|
|
15394
15557
|
}
|
|
15395
|
-
var StaticFinding =
|
|
15396
|
-
severity:
|
|
15397
|
-
format:
|
|
15398
|
-
what:
|
|
15558
|
+
var StaticFinding = z22.object({
|
|
15559
|
+
severity: z22.enum(["blocking", "warning"]),
|
|
15560
|
+
format: z22.string().optional(),
|
|
15561
|
+
what: z22.string().trim().min(1),
|
|
15562
|
+
item: z22.enum(STATIC_REVIEW_ITEMS).catch("other"),
|
|
15563
|
+
element: z22.enum(FINDING_ELEMENTS).catch("other"),
|
|
15564
|
+
defect: z22.enum(FINDING_DEFECTS).catch("other"),
|
|
15565
|
+
edges: z22.array(z22.enum(FRAME_EDGES)).catch([])
|
|
15399
15566
|
});
|
|
15400
|
-
var
|
|
15401
|
-
var ALWAYS_BLOCKING = /^(?:SUBJECT|PERSON):/i;
|
|
15402
|
-
var ABOUT_OWN_COPY = /^COPY:|["“«]|\b(?:headline|subhead\w*|button|cta|tagline|copy)\b/i;
|
|
15403
|
-
var BLURRED_FIGURES = /\b(?:blur\w*|obscur\w*|unreadable|illegible|smudg\w*)\b(?=[^.]*\b(?:figures?|numbers?|digits?|amounts?|prices?|savings?|values?)\b)|\b(?:figures?|numbers?|digits?|amounts?|prices?|savings?|values?)\b(?=[^.]*\b(?:blur\w*|obscur\w*|unreadable|illegible|smudg\w*)\b)/i;
|
|
15567
|
+
var ALWAYS_BLOCKING = /* @__PURE__ */ new Set(["subject", "person"]);
|
|
15404
15568
|
function classifyStaticFindings(json) {
|
|
15405
15569
|
const findings = json?.findings;
|
|
15406
15570
|
if (!Array.isArray(findings)) return [];
|
|
15407
15571
|
return findings.flatMap((raw) => {
|
|
15408
15572
|
const parsed = StaticFinding.safeParse(raw);
|
|
15409
15573
|
if (!parsed.success) return [];
|
|
15410
|
-
const { severity, format, what } = parsed.data;
|
|
15411
|
-
|
|
15412
|
-
|
|
15413
|
-
|
|
15574
|
+
const { severity, format, what, ...fields } = parsed.data;
|
|
15575
|
+
const kind = {
|
|
15576
|
+
item: fields.item,
|
|
15577
|
+
element: fields.element,
|
|
15578
|
+
defect: fields.defect,
|
|
15579
|
+
edges: [...new Set(fields.edges)]
|
|
15580
|
+
};
|
|
15581
|
+
if (kind.element === "button" && kind.defect === "missing") return [];
|
|
15582
|
+
const graded = ALWAYS_BLOCKING.has(kind.item) ? "blocking" : kind.element === "scene_text" && kind.defect === "unreadable" ? "warning" : severity;
|
|
15583
|
+
return [{ severity: graded, what: format?.trim() ? `${format.trim()}: ${what}` : what, kind }];
|
|
15414
15584
|
});
|
|
15415
15585
|
}
|
|
15416
|
-
var CopyRead =
|
|
15417
|
-
format:
|
|
15418
|
-
string:
|
|
15586
|
+
var CopyRead = z22.object({
|
|
15587
|
+
format: z22.string().trim().min(1),
|
|
15588
|
+
string: z22.string(),
|
|
15419
15589
|
/** What the model read where the string should be; null when it found nothing like it. */
|
|
15420
|
-
seen:
|
|
15590
|
+
seen: z22.string().nullable()
|
|
15421
15591
|
});
|
|
15422
15592
|
function comparableCopy(text2) {
|
|
15423
15593
|
return text2.replace(/[‘’‚′]/g, "'").replace(/[“”„″]/g, '"').replace(/[‐-―−]/g, "-").replace(/…/g, "...").replace(/[«»]/g, '"').replace(/\s+/g, " ").trim().replace(/^"([^"]*)"$/, "$1").trim().toLowerCase();
|
|
@@ -15588,12 +15758,12 @@ function measuredFindings(input) {
|
|
|
15588
15758
|
if (!input.adapted || loadBearing(l)) return [blocking(said(l))];
|
|
15589
15759
|
return l.similarity >= MEANING_MATCH ? [] : [{ severity: "warning", what: `${format}: ${said(l)}` }];
|
|
15590
15760
|
}),
|
|
15591
|
-
...extraWordFindings(format, extra, input.boxes, [...copyBoxes]),
|
|
15761
|
+
...extraWordFindings(format, extra, input.boxes, [...copyBoxes], input.offers ?? /* @__PURE__ */ new Set()),
|
|
15592
15762
|
...twice.map(
|
|
15593
15763
|
(t) => ({ severity: "warning", what: `${format}: "${t}" is on the ad twice; keep it once.` })
|
|
15594
15764
|
),
|
|
15595
15765
|
...misspelt.map((m) => blocking(`the brand's name reads "${m.read}", not "${m.name}".`)),
|
|
15596
|
-
...unbadgedClaims(input.lines, input.boxes).map(
|
|
15766
|
+
...unbadgedClaims(input.lines, input.boxes, input.cited).map(
|
|
15597
15767
|
(c) => blocking(
|
|
15598
15768
|
`"${c.line}" names ${c.platform}, but ${c.platform}'s logo is not on the ad: show its real badge, or drop the claim.`
|
|
15599
15769
|
)
|
|
@@ -15677,9 +15847,9 @@ function misspeltNames(texts, names) {
|
|
|
15677
15847
|
}
|
|
15678
15848
|
return out;
|
|
15679
15849
|
}
|
|
15680
|
-
function unbadgedClaims(lines, boxes) {
|
|
15850
|
+
function unbadgedClaims(lines, boxes, cited) {
|
|
15681
15851
|
return lines.flatMap((line) => {
|
|
15682
|
-
const platform =
|
|
15852
|
+
const platform = citedBy(line.text, cited);
|
|
15683
15853
|
if (!platform) return [];
|
|
15684
15854
|
const naming = boxes.filter((b) => b.kind === "text" && namedPlatform(b.text) === platform);
|
|
15685
15855
|
const mentions = new RegExp(platform.pattern.source, `${platform.pattern.flags}g`);
|
|
@@ -15795,10 +15965,11 @@ function contextItems(opts) {
|
|
|
15795
15965
|
opts.thumbnails ? "7. THUMBNAIL: at thumbnail size the lead line (the largest words) cannot be read. Judge only whether those words can be read, nothing else. That is blocking: most people only ever see the ad that small." : "7. THUMBNAIL: the lead line (the largest words) would be unreadable on a phone feed. That is blocking."
|
|
15796
15966
|
],
|
|
15797
15967
|
sizes: opts.formats.length > 1 ? [
|
|
15798
|
-
|
|
15968
|
+
"12. SIZES: one size loses what the others are built on: the letter sheet, the email or chat window, the post card, the comic's panels and their order, the product the ad is about, or a size that shows something the others do not (blocking; report it on the size that differs). Rearranged for its shape is fine."
|
|
15799
15969
|
] : []
|
|
15800
15970
|
};
|
|
15801
15971
|
}
|
|
15972
|
+
var FINDING_FIELDS = `For each finding also give: "item", the item above it answers, in lower case (TEXT \u2192 "text", ONE IDEA \u2192 "one_idea", the 9:16 safe-zone item \u2192 "safe_zone"); "element", what on the ad it is about ("copy" the ad's own words, "button", "logo", "scene_text" words or figures printed on something in the picture that are not the copy, "picture" anything else drawn, else "other"); "defect" ("cut" when the frame's edge cuts it, "missing" when it is not there, "unreadable" when it is too blurred or small to read, else "other"); "edges", the frame edges that cut it, [] unless "cut". Write "what" without a label.`;
|
|
15802
15973
|
function buildStaticReviewPrompt(opts) {
|
|
15803
15974
|
const brand = opts.brand ? ` for ${opts.brand}` : "";
|
|
15804
15975
|
const order = opts.formats.join(", ");
|
|
@@ -15817,9 +15988,9 @@ function buildStaticReviewPrompt(opts) {
|
|
|
15817
15988
|
...storiesItem(opts),
|
|
15818
15989
|
// Tier 0, Oct 2026: the real defects were where a size was extended; 8 of 8 hand claims checked were wrong.
|
|
15819
15990
|
"5. PICTURE: a ghost, a smear, a seam, a blurred strip, a duplicated or mirrored object, or garbled pseudo-text in focus where a viewer reads it, typically where the picture was extended to fill a size (blocking). Text out of focus in the background (a screen, a shelf of books) is the scene; an ordinary object a viewer would not stop at on a phone is not malformed.",
|
|
15820
|
-
|
|
15991
|
+
"5a. SUBJECT: the ad's subject (its pet, person or product) shown more than once as visibly different ones where the ad means one, or a second, different subject beside it that leaves a viewer asking whose story it is (blocking).",
|
|
15821
15992
|
...opts.uncastPeople && opts.uncastPeople.length > 0 ? [
|
|
15822
|
-
`5c. PERSON: the copy names ${opts.uncastPeople.join(" and ")}, real, and this ad was given no photo of them: a face shown as them or beside their name is a stranger wearing that name (blocking
|
|
15993
|
+
`5c. PERSON: the copy names ${opts.uncastPeople.join(" and ")}, real, and this ad was given no photo of them: a face shown as them or beside their name is a stranger wearing that name (blocking).`
|
|
15823
15994
|
] : [],
|
|
15824
15995
|
"5b. PEOPLE: a person's hands, face or body drawn so wrong a viewer would notice at a glance on a phone (warning). Never a detail you have to zoom in to see.",
|
|
15825
15996
|
"6. ONE IDEA: two or more elements compete equally for the eye, so no single promise leads (warning).",
|
|
@@ -15833,7 +16004,8 @@ function buildStaticReviewPrompt(opts) {
|
|
|
15833
16004
|
] : [],
|
|
15834
16005
|
"Blocking = it cannot ship as is (a broken logo, clipped content, a ghost or smear in the picture). Warning = it can ship but should improve.",
|
|
15835
16006
|
"Never report an element as missing because ads usually have one: judge only what this concept holds. An empty findings list is the right answer for a clean ad. Keep each item under twenty words.",
|
|
15836
|
-
|
|
16007
|
+
FINDING_FIELDS,
|
|
16008
|
+
`Reply with only this JSON: {"findings":[{"severity":"blocking|warning","format":"4:5","item":"${STATIC_REVIEW_ITEMS.join("|")}","element":"${FINDING_ELEMENTS.join("|")}","defect":"${FINDING_DEFECTS.join("|")}","edges":["${FRAME_EDGES.join("|")}"],"what":"..."}]${copyShape}}`
|
|
15837
16009
|
].join("\n");
|
|
15838
16010
|
}
|
|
15839
16011
|
var SCENE_PRINT_SCALE = 0.45;
|
|
@@ -15847,10 +16019,10 @@ function mostOfALine(chunk, copy) {
|
|
|
15847
16019
|
return words.length > 0 && words.filter((w) => said.has(w)).length / words.length >= LINE_REPEAT_SHARE;
|
|
15848
16020
|
});
|
|
15849
16021
|
}
|
|
15850
|
-
var
|
|
16022
|
+
var SIGN_CLAIM = /[%\p{Sc}]/u;
|
|
15851
16023
|
var PRICE = /[€$£]\s?\d|\d\s?[€$£]/;
|
|
15852
16024
|
var COPY_SIZE_SHARE = 0.8;
|
|
15853
|
-
function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
16025
|
+
function extraWordFindings(format, extra, boxes, copyBoxes, offers) {
|
|
15854
16026
|
if (extra.length === 0) return [];
|
|
15855
16027
|
const heights = copyBoxes.map((b) => b.y1 - b.y0).filter((h) => h > 0);
|
|
15856
16028
|
const smallest = heights.length > 0 ? Math.min(...heights) : 0;
|
|
@@ -15863,7 +16035,9 @@ function extraWordFindings(format, extra, boxes, copyBoxes) {
|
|
|
15863
16035
|
const usual = sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
15864
16036
|
const underCopy = extra.filter((chunk) => height(chunk) < usual * COPY_SIZE_SHARE);
|
|
15865
16037
|
const scenery = (chunk) => height(chunk) < smallest * SCENE_PRINT_SCALE || underCopy.length >= SCENE_BLOCK_LINES && underCopy.includes(chunk);
|
|
15866
|
-
const
|
|
16038
|
+
const offered = new Set([...offers].map(normalizeCopy));
|
|
16039
|
+
const claims = (chunk) => SIGN_CLAIM.test(chunk) || offered.has(normalizeCopy(chunk));
|
|
16040
|
+
const printed = (chunk) => usual > 0 && underCopy.includes(chunk) && (!claims(chunk) || scenery(chunk));
|
|
15867
16041
|
const quote = (chunks) => chunks.map((e) => `"${e}"`).join(", ");
|
|
15868
16042
|
const asCopy = extra.filter((chunk) => !printed(chunk));
|
|
15869
16043
|
const prices = extra.filter((chunk) => printed(chunk) && PRICE.test(chunk));
|
|
@@ -15928,41 +16102,29 @@ async function samePictureSizes(current, previous) {
|
|
|
15928
16102
|
return same;
|
|
15929
16103
|
}
|
|
15930
16104
|
var AT_EDGE = 0.025;
|
|
15931
|
-
var CLIPPED = /\b(?:clipped|cut off|cut by|cropped|runs? off)\b|\btouch(?:es|ing)?\s+(?:the\s+)?(?:(?:top|bottom|left|right|frame)\b|edge)/i;
|
|
15932
|
-
var ABOUT_COPY = /\b(?:headline|text|copy|line|words?|title|subhead(?:line)?|tagline|price|letters?)\b|["“«]/i;
|
|
15933
|
-
var EDGE_WORD = [
|
|
15934
|
-
["top", /\btop\b/i],
|
|
15935
|
-
["bottom", /\bbottom\b/i],
|
|
15936
|
-
["left", /\bleft\b/i],
|
|
15937
|
-
["right", /\bright\b/i]
|
|
15938
|
-
];
|
|
15939
16105
|
var clearanceTo = (b, edge) => edge === "top" ? b.y0 : edge === "bottom" ? 1 - b.y1 : edge === "left" ? b.x0 : 1 - b.x1;
|
|
16106
|
+
function measuredBoxes(element, boxes, buttons) {
|
|
16107
|
+
if (element === "logo") return boxes.filter((b) => b.kind === "logo");
|
|
16108
|
+
if (element === "button") return buttons.flatMap((l) => boxesOfLine(l, boxes));
|
|
16109
|
+
return element === "copy" ? boxes.filter((b) => b.kind === "text") : [];
|
|
16110
|
+
}
|
|
16111
|
+
var cutByFrame = (f) => f.kind?.defect === "cut";
|
|
15940
16112
|
function corroborateEdgeClaims(findings, boxesByFormat, lines) {
|
|
15941
16113
|
const buttons = lines.filter((l) => /^(cta|button|call_to_action)$/.test(roleKey2(l.role)));
|
|
15942
16114
|
return findings.map((f) => {
|
|
15943
|
-
if (f.severity !== "blocking" || !
|
|
16115
|
+
if (f.severity !== "blocking" || !f.kind || !cutByFrame(f)) return f;
|
|
15944
16116
|
const format = f.what.match(/^([\d.]+:[\d.]+):/)?.[1];
|
|
15945
16117
|
const boxes = format ? boxesByFormat[format] : void 0;
|
|
15946
16118
|
if (!boxes) return f;
|
|
15947
|
-
const element =
|
|
15948
|
-
|
|
15949
|
-
|
|
15950
|
-
const
|
|
15951
|
-
const
|
|
16119
|
+
const { element } = f.kind;
|
|
16120
|
+
const measured = measuredBoxes(element, boxes, buttons);
|
|
16121
|
+
if (measured.length === 0) return f;
|
|
16122
|
+
const named = f.kind.edges.length > 0 ? f.kind.edges : ["top", "bottom", "left", "right"];
|
|
16123
|
+
const pad = element === "button" ? BUTTON_PAD : 0;
|
|
16124
|
+
const atEdge = measured.some((b) => named.some((edge) => clearanceTo(b, edge) - pad * (b.y1 - b.y0) <= AT_EDGE));
|
|
15952
16125
|
return atEdge ? f : { ...f, severity: "warning" };
|
|
15953
16126
|
});
|
|
15954
16127
|
}
|
|
15955
|
-
function claimedEdges(what) {
|
|
15956
|
-
const edges = EDGE_WORD.filter(([, word]) => word.test(what)).map(([edge]) => edge);
|
|
15957
|
-
return edges.length > 0 ? edges : ["top", "bottom", "left", "right"];
|
|
15958
|
-
}
|
|
15959
|
-
var LOGO_CLAIM = /\b(?:logos?|wordmark|mark)\b/i;
|
|
15960
|
-
var BUTTON = /\b(button|cta|call[- ]to[- ]action)\b/i;
|
|
15961
|
-
function claimedBoxes(what, boxes, buttons) {
|
|
15962
|
-
if (LOGO_CLAIM.test(what)) return boxes.filter((b) => b.kind === "logo");
|
|
15963
|
-
if (BUTTON.test(what)) return buttons.flatMap((l) => boxesOfLine(l, boxes));
|
|
15964
|
-
return ABOUT_COPY.test(what) ? boxes.filter((b) => b.kind === "text") : [];
|
|
15965
|
-
}
|
|
15966
16128
|
async function bytesOf(source) {
|
|
15967
16129
|
if (!/^https?:\/\//i.test(source)) return await readFile15(source);
|
|
15968
16130
|
const res = await fetch(source);
|
|
@@ -16010,12 +16172,25 @@ function fragmentOf(chunk, own, opts = {}) {
|
|
|
16010
16172
|
return shared / words.length >= share;
|
|
16011
16173
|
});
|
|
16012
16174
|
}
|
|
16175
|
+
var OFFER_MADE = 0.35;
|
|
16176
|
+
var OFFER_QUESTION = "Does `words` make an offer of its own: something free, a discount, a sale, a promotion, a deal or a price, in whatever language it is written? A sign, a label, a name or a screen's words are not an offer.";
|
|
16177
|
+
var offerQuestion = (words) => ({ instructions: { words, question: OFFER_QUESTION } });
|
|
16178
|
+
async function offersAmong(chunks, judge) {
|
|
16179
|
+
const worded = chunks.filter((c) => new RegExp("\\p{L}", "u").test(c));
|
|
16180
|
+
if (worded.length === 0) return /* @__PURE__ */ new Set();
|
|
16181
|
+
const judged = await judge(worded);
|
|
16182
|
+
return new Set(worded.filter((_, i) => (judged?.[i] ?? 0) >= OFFER_MADE));
|
|
16183
|
+
}
|
|
16013
16184
|
async function checkStaticImage(image, opts) {
|
|
16014
16185
|
const reading = await (opts.read ?? ((bytes) => readTextBoxes(bytes, { onSpend: opts.onSpend })))(image);
|
|
16015
16186
|
if (reading.status === "not_checked") return reading;
|
|
16016
16187
|
const palette = opts.palette ?? [];
|
|
16017
16188
|
const inks = palette.length > 0 ? await designedInks(image, opts.lines, reading.boxes) : [];
|
|
16018
16189
|
const boxes = opts.toFrame ? opts.toFrame(reading.boxes) : reading.boxes;
|
|
16190
|
+
const offers = await offersAmong(
|
|
16191
|
+
textMismatch(opts.lines, boxes, { allowed: opts.allowed }).extra,
|
|
16192
|
+
opts.judgeOffers ?? ((chunks) => askJev(chunks.map(offerQuestion), opts.onSpend))
|
|
16193
|
+
);
|
|
16019
16194
|
const findings = measuredFindings({
|
|
16020
16195
|
format: opts.format,
|
|
16021
16196
|
boxes,
|
|
@@ -16027,7 +16202,12 @@ async function checkStaticImage(image, opts) {
|
|
|
16027
16202
|
...opts.native ? { native: true } : {},
|
|
16028
16203
|
...opts.placements ? { placements: opts.placements } : {},
|
|
16029
16204
|
...opts.band ? { band: opts.band } : {},
|
|
16030
|
-
offBrand: offBrandInks(inks, palette)
|
|
16205
|
+
offBrand: offBrandInks(inks, palette),
|
|
16206
|
+
offers,
|
|
16207
|
+
cited: opts.cited ?? await citedPlatforms(
|
|
16208
|
+
opts.lines.map((l) => l.text),
|
|
16209
|
+
opts.judgeCitations ?? jevCitationJudge(opts.onSpend)
|
|
16210
|
+
)
|
|
16031
16211
|
});
|
|
16032
16212
|
return { status: "checked", findings, boxes };
|
|
16033
16213
|
}
|
|
@@ -16091,7 +16271,7 @@ function everySizeChecks(opts) {
|
|
|
16091
16271
|
};
|
|
16092
16272
|
}
|
|
16093
16273
|
async function confirmBlocking(findings, stills, formats, ask2) {
|
|
16094
|
-
const claims = findings.filter((f) => f.severity === "blocking" && !
|
|
16274
|
+
const claims = findings.filter((f) => f.severity === "blocking" && !cutByFrame(f));
|
|
16095
16275
|
if (claims.length === 0 || stills.length === 0) return [...findings];
|
|
16096
16276
|
const prompt = [
|
|
16097
16277
|
`These are the finished sizes of one ad, in order: ${formats.map((f, i) => `Image ${i + 1} is the ${f}`).join("; ")}.`,
|
|
@@ -16120,12 +16300,17 @@ async function reviewRenderedStatic(opts) {
|
|
|
16120
16300
|
return askAboutStills(shown, prompt, opts.onSpend);
|
|
16121
16301
|
};
|
|
16122
16302
|
const asking = ask2(true);
|
|
16303
|
+
const cited = await citedPlatforms(
|
|
16304
|
+
lines.map((l) => l.text),
|
|
16305
|
+
jevCitationJudge(opts.onSpend)
|
|
16306
|
+
);
|
|
16123
16307
|
const checks = await Promise.all(
|
|
16124
16308
|
originals.map(
|
|
16125
16309
|
(bytes, i) => checkStaticImage(bytes, {
|
|
16126
16310
|
...scrolledOff(every, opts.scrolled?.[formats[i] ?? ""] ?? []),
|
|
16127
16311
|
format: formats[i] ?? "",
|
|
16128
|
-
adapted: opts.masterFormat !== void 0 && formats[i] !== opts.masterFormat
|
|
16312
|
+
adapted: opts.masterFormat !== void 0 && formats[i] !== opts.masterFormat,
|
|
16313
|
+
cited
|
|
16129
16314
|
})
|
|
16130
16315
|
)
|
|
16131
16316
|
);
|
|
@@ -16166,26 +16351,28 @@ var staticNativeNode = defineNode({
|
|
|
16166
16351
|
location: "local",
|
|
16167
16352
|
summary: "Draw a static ad that is a screen people know (a post on X, a WhatsApp or iMessage chat, a wall of Trustpilot or Google reviews, a note, a Slack message) exactly, from HTML, at one delivered size: the `native` block of the ad spec (prompt.json) is laid out in the platform's own layout, type and colours, with the brand's real logo and the photos given, fitted inside the size's safe area. Free and deterministic.",
|
|
16168
16353
|
when_to_use: "Scaffolded by `baker canvas scaffold-static-ad` for an ad spec with a `native` block, one node per size (`native_<size>`, role `static_native`). Edit prompt.json's `native` and run again: nothing is billed but the photos whose description changed.",
|
|
16169
|
-
inputs:
|
|
16354
|
+
inputs: z23.object({
|
|
16170
16355
|
spec: JsonRef,
|
|
16171
16356
|
/** The brand's logo files (every variant the brand has): the one that reads on the screen is used. */
|
|
16172
|
-
logos:
|
|
16357
|
+
logos: z23.array(ImageRef).optional(),
|
|
16173
16358
|
/** The brand's square mark, for an avatar; else cut from the logo's lockup. */
|
|
16174
16359
|
mark: ImageRef.optional(),
|
|
16175
16360
|
/** The brand's face, for the frame's headline. */
|
|
16176
16361
|
font: FontRef.optional(),
|
|
16177
16362
|
/** The photos the screen shows, in `params.photo_ids` order. */
|
|
16178
|
-
photos:
|
|
16363
|
+
photos: z23.array(ImageRef).optional()
|
|
16179
16364
|
}).strict(),
|
|
16180
|
-
params:
|
|
16181
|
-
format:
|
|
16182
|
-
placements:
|
|
16183
|
-
photo_ids:
|
|
16365
|
+
params: z23.object({
|
|
16366
|
+
format: z23.string().min(1),
|
|
16367
|
+
placements: z23.array(z23.string().min(1)).optional(),
|
|
16368
|
+
photo_ids: z23.array(z23.string().regex(/^PHOTO_\d+$/)).default([]),
|
|
16369
|
+
/** The interface's own words written for the viewer's language, when no checked ones exist (`needsInterfaceWords`). */
|
|
16370
|
+
interface_words: z23.record(z23.string(), z23.string()).optional()
|
|
16184
16371
|
}).strict(),
|
|
16185
|
-
outputs:
|
|
16372
|
+
outputs: z23.object({
|
|
16186
16373
|
image: ImageRef,
|
|
16187
16374
|
// A chat's earliest lines this size scrolled up under its header to keep the rest readable.
|
|
16188
|
-
scrolled:
|
|
16375
|
+
scrolled: z23.array(z23.string()).optional()
|
|
16189
16376
|
}).strict(),
|
|
16190
16377
|
outputKinds: { image: "image" },
|
|
16191
16378
|
cost: () => ({ credits: 0, seconds_estimate: 4 }),
|
|
@@ -16244,6 +16431,7 @@ async function nativePage(spec, files, params) {
|
|
|
16244
16431
|
const native = spec.native;
|
|
16245
16432
|
if (!native) throw new Error("static_native: prompt.json has no `native` block to draw");
|
|
16246
16433
|
const size2 = deliveredSize(params.format);
|
|
16434
|
+
const words = readInterfaceWords(params.interface_words);
|
|
16247
16435
|
const assets = await pageAssets(
|
|
16248
16436
|
files,
|
|
16249
16437
|
params.photo_ids,
|
|
@@ -16259,7 +16447,8 @@ async function nativePage(spec, files, params) {
|
|
|
16259
16447
|
...spec.brand?.ink ? { ink: spec.brand.ink } : {},
|
|
16260
16448
|
...spec.brand?.accent ? { accent: spec.brand.accent } : {}
|
|
16261
16449
|
},
|
|
16262
|
-
...spec.viewer?.market?.language ? { language: spec.viewer.market.language } : {}
|
|
16450
|
+
...spec.viewer?.market?.language ? { language: spec.viewer.market.language } : {},
|
|
16451
|
+
...words ? { words } : {}
|
|
16263
16452
|
});
|
|
16264
16453
|
}
|
|
16265
16454
|
async function screenshot(html, size2, log) {
|
|
@@ -16283,13 +16472,13 @@ async function screenshot(html, size2, log) {
|
|
|
16283
16472
|
}
|
|
16284
16473
|
|
|
16285
16474
|
// src/engine/nodes/local/staticPlace.ts
|
|
16286
|
-
import { z as
|
|
16475
|
+
import { z as z26 } from "zod";
|
|
16287
16476
|
|
|
16288
16477
|
// src/engine/nodes/remote/image.ts
|
|
16289
|
-
import { z as
|
|
16478
|
+
import { z as z25 } from "zod";
|
|
16290
16479
|
|
|
16291
16480
|
// src/engine/nodes/remote/registry-params.ts
|
|
16292
|
-
import { z as
|
|
16481
|
+
import { z as z24 } from "zod";
|
|
16293
16482
|
function registryParamShape(kind, overrides = {}) {
|
|
16294
16483
|
const specs = Object.values(MODEL_REGISTRY[kind]);
|
|
16295
16484
|
const byParam = /* @__PURE__ */ new Map();
|
|
@@ -16310,7 +16499,7 @@ function unionOf(schemas) {
|
|
|
16310
16499
|
const kinds = [...new Set(schemas.map((schema) => schema.kind))];
|
|
16311
16500
|
if (kinds.length > 1) {
|
|
16312
16501
|
const [first, second, ...rest] = kinds.map((k) => unionOf(schemas.filter((s) => s.kind === k)));
|
|
16313
|
-
return
|
|
16502
|
+
return z24.union([first, second, ...rest]);
|
|
16314
16503
|
}
|
|
16315
16504
|
switch (kinds[0]) {
|
|
16316
16505
|
case "string":
|
|
@@ -16318,32 +16507,32 @@ function unionOf(schemas) {
|
|
|
16318
16507
|
case "number":
|
|
16319
16508
|
return numberUnion(schemas);
|
|
16320
16509
|
case "boolean":
|
|
16321
|
-
return
|
|
16510
|
+
return z24.boolean();
|
|
16322
16511
|
default:
|
|
16323
|
-
return
|
|
16512
|
+
return z24.unknown();
|
|
16324
16513
|
}
|
|
16325
16514
|
}
|
|
16326
16515
|
function stringUnion(schemas) {
|
|
16327
|
-
if (schemas.some((schema) => !schema.enum)) return
|
|
16516
|
+
if (schemas.some((schema) => !schema.enum)) return z24.string();
|
|
16328
16517
|
const values = [...new Set(schemas.flatMap((schema) => schema.enum ?? []))];
|
|
16329
|
-
return
|
|
16518
|
+
return z24.enum(values);
|
|
16330
16519
|
}
|
|
16331
16520
|
function numberUnion(schemas) {
|
|
16332
|
-
if (schemas.some((schema) => !schema.enum)) return
|
|
16521
|
+
if (schemas.some((schema) => !schema.enum)) return z24.number();
|
|
16333
16522
|
const values = [...new Set(schemas.flatMap((schema) => schema.enum ?? []))].sort((a, b) => a - b);
|
|
16334
|
-
return
|
|
16523
|
+
return z24.literal(values);
|
|
16335
16524
|
}
|
|
16336
16525
|
|
|
16337
16526
|
// src/engine/nodes/remote/image.ts
|
|
16338
|
-
var imageModelEnum =
|
|
16339
|
-
var ImageGenerateParams =
|
|
16527
|
+
var imageModelEnum = z25.enum(IMAGE_GENERATE_MODELS);
|
|
16528
|
+
var ImageGenerateParams = z25.object({
|
|
16340
16529
|
model: imageModelEnum,
|
|
16341
16530
|
...registryParamShape("image_generate", {
|
|
16342
|
-
prompt:
|
|
16531
|
+
prompt: z25.string().min(1),
|
|
16343
16532
|
// Recraft v4 vector controls, sent as the provider's own `controls` block. The
|
|
16344
16533
|
// registry types them as JSON; this is their shape.
|
|
16345
|
-
rgb_colors:
|
|
16346
|
-
background_rgb_color:
|
|
16534
|
+
rgb_colors: z25.array(z25.array(z25.number().int().min(0).max(255))),
|
|
16535
|
+
background_rgb_color: z25.array(z25.number().int().min(0).max(255))
|
|
16347
16536
|
})
|
|
16348
16537
|
}).strict();
|
|
16349
16538
|
var PRO_QUALITY = "max";
|
|
@@ -16361,9 +16550,9 @@ var imageGenerateNode = delegated({
|
|
|
16361
16550
|
when_to_use: "Use for hero shots, product photography, illustrations, and vector logos. `recraft/recraft-v4.1-pro-vector` for crisp vector / logo work; `openai/gpt-image-2` for photorealistic and in-image text (NOTE: it has no `image_size`, and no 4:5 / 5:4 \u2014 route portrait ad formats through Gemini or `image_aspect_adapt`); Gemini variants for fast iteration, extreme ratios, and editing via the `reference` input. `reference` accepts ONE image or an ARRAY of images \u2014 wire several to combine references in a single generation (e.g. a subject sheet + a font specimen + the original ad). Every reference is forwarded to the model in array order. `n` (only on models that declare it, e.g. `openai/gpt-image-2.5-sunburst`) returns several candidates of the same brief from one call, billed per image like separate calls; wire the whole `images` array into `image_select` to keep the best.",
|
|
16362
16551
|
// `reference` is one image or an ordered array of images. The backend forwards
|
|
16363
16552
|
// each as a separate `image_url` to the provider (OpenRouter accepts many).
|
|
16364
|
-
inputs:
|
|
16553
|
+
inputs: z25.object({ reference: z25.union([ImageRef, z25.array(ImageRef).min(1)]).optional() }).loose(),
|
|
16365
16554
|
params: ImageGenerateParams,
|
|
16366
|
-
outputs:
|
|
16555
|
+
outputs: z25.object({ images: z25.array(ImageRef).min(1) }).strict(),
|
|
16367
16556
|
outputKinds: { images: "image" },
|
|
16368
16557
|
// Per image: `n` asks one call for several, and the provider bills each (measured: n=3
|
|
16369
16558
|
// cost exactly 3x n=1, prompt tokens included). Usage-billed, so this is only the pre-run
|
|
@@ -16493,6 +16682,9 @@ var MIN_EXTEND_SCALE = 0.6;
|
|
|
16493
16682
|
var GREY_LEVEL = 128;
|
|
16494
16683
|
var GREY = { r: GREY_LEVEL, g: GREY_LEVEL, b: GREY_LEVEL };
|
|
16495
16684
|
var FEATHER = 0.012;
|
|
16685
|
+
var KEEP_PAD = 0.3;
|
|
16686
|
+
var KEEP_FEATHER = 0.03;
|
|
16687
|
+
var MOVED = 120;
|
|
16496
16688
|
function ratioOf2(ratio) {
|
|
16497
16689
|
const [w = 1, h = 1] = ratio.split(":").map(Number);
|
|
16498
16690
|
return w / h;
|
|
@@ -16555,14 +16747,16 @@ function clearBusyMargins(rgba, frame, keep) {
|
|
|
16555
16747
|
}
|
|
16556
16748
|
return shown;
|
|
16557
16749
|
}
|
|
16558
|
-
async function mergeExtended(extended, placed, ratio) {
|
|
16750
|
+
async function mergeExtended(extended, placed, ratio, keep = []) {
|
|
16559
16751
|
const ad = await sharp7(placed).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
16560
16752
|
const { width, height } = ad.info;
|
|
16561
16753
|
const bounds = placedBounds(ad.data, width, height);
|
|
16562
16754
|
if (!bounds) return sharp7(placed).removeAlpha().png().toBuffer();
|
|
16563
16755
|
const canvas = renderCanvas({ width, height }, ratio);
|
|
16564
16756
|
const paint = await sharp7(extended).removeAlpha().resize(canvas.width, canvas.height, { fit: "fill" }).extract({ left: canvas.left, top: canvas.top, width, height }).raw().toBuffer();
|
|
16565
|
-
const
|
|
16757
|
+
const edge = edgeWeight(bounds, width, height, Math.max(1, Math.round(FEATHER * width)));
|
|
16758
|
+
const boxes = keep.length > 0 && keptInPlace(keep, ad.data, paint, width, height) ? keptWeights(keep, width, height) : null;
|
|
16759
|
+
const weight = boxes ? (x, y) => Math.min(edge(x, y), boxes[y * width + x] ?? 0) : edge;
|
|
16566
16760
|
const out = Buffer.alloc(width * height * 3);
|
|
16567
16761
|
for (let y = 0; y < height; y++) {
|
|
16568
16762
|
for (let x = 0; x < width; x++) {
|
|
@@ -16601,32 +16795,81 @@ function edgeWeight(b, width, height, feather) {
|
|
|
16601
16795
|
ramp(b.y1 - y, b.y1 === height - 1)
|
|
16602
16796
|
);
|
|
16603
16797
|
}
|
|
16798
|
+
function keptWeights(keep, width, height) {
|
|
16799
|
+
const out = new Float32Array(width * height);
|
|
16800
|
+
const feather = Math.max(1, KEEP_FEATHER * width);
|
|
16801
|
+
for (const box of keep) {
|
|
16802
|
+
const pad = KEEP_PAD * (box.y1 - box.y0) * height;
|
|
16803
|
+
const x0 = box.x0 * width - pad;
|
|
16804
|
+
const x1 = box.x1 * width + pad;
|
|
16805
|
+
const y0 = box.y0 * height - pad;
|
|
16806
|
+
const y1 = box.y1 * height + pad;
|
|
16807
|
+
const ys = Math.max(0, Math.floor(y0 - feather));
|
|
16808
|
+
const ye = Math.min(height - 1, Math.ceil(y1 + feather));
|
|
16809
|
+
const xs = Math.max(0, Math.floor(x0 - feather));
|
|
16810
|
+
const xe = Math.min(width - 1, Math.ceil(x1 + feather));
|
|
16811
|
+
for (let y = ys; y <= ye; y++) {
|
|
16812
|
+
const dy = Math.max(0, y0 - y, y - y1);
|
|
16813
|
+
for (let x = xs; x <= xe; x++) {
|
|
16814
|
+
const dx = Math.max(0, x0 - x, x - x1);
|
|
16815
|
+
const t = Math.max(0, 1 - Math.hypot(dx, dy) / feather);
|
|
16816
|
+
const w = t * t * (3 - 2 * t);
|
|
16817
|
+
const i = y * width + x;
|
|
16818
|
+
if (w > (out[i] ?? 0)) out[i] = w;
|
|
16819
|
+
}
|
|
16820
|
+
}
|
|
16821
|
+
}
|
|
16822
|
+
return out;
|
|
16823
|
+
}
|
|
16824
|
+
function keptInPlace(keep, ad, paint, width, height) {
|
|
16825
|
+
const diffs = keep.map((box) => boxDiff(box, ad, paint, width, height)).filter((d) => d !== null).sort((a, b) => a - b);
|
|
16826
|
+
return (diffs[Math.floor(diffs.length / 2)] ?? Number.POSITIVE_INFINITY) <= MOVED;
|
|
16827
|
+
}
|
|
16828
|
+
function boxDiff(box, ad, paint, width, height) {
|
|
16829
|
+
let sum = 0;
|
|
16830
|
+
let n = 0;
|
|
16831
|
+
const xs = Math.floor(box.x0 * width);
|
|
16832
|
+
const xe = Math.min(width - 1, Math.ceil(box.x1 * width));
|
|
16833
|
+
for (let y = Math.floor(box.y0 * height); y <= Math.min(height - 1, Math.ceil(box.y1 * height)); y++) {
|
|
16834
|
+
for (let x = xs; x <= xe; x++) {
|
|
16835
|
+
const i = y * width + x;
|
|
16836
|
+
if ((ad[i * 4 + 3] ?? 0) === 0) continue;
|
|
16837
|
+
for (let c = 0; c < 3; c++) sum += Math.abs((ad[i * 4 + c] ?? 0) - (paint[i * 3 + c] ?? 0));
|
|
16838
|
+
n++;
|
|
16839
|
+
}
|
|
16840
|
+
}
|
|
16841
|
+
return n > 0 ? sum / n : null;
|
|
16842
|
+
}
|
|
16604
16843
|
|
|
16605
16844
|
// src/engine/nodes/local/staticPlace.ts
|
|
16606
|
-
var Usage =
|
|
16607
|
-
var Fit =
|
|
16608
|
-
var
|
|
16609
|
-
var
|
|
16845
|
+
var Usage = z26.object({ model: z26.string(), inputTokens: z26.number(), outputTokens: z26.number(), costUsd: z26.number() }).strict();
|
|
16846
|
+
var Fit = z26.object({ scale: z26.number(), x: z26.number(), y: z26.number() }).strict();
|
|
16847
|
+
var Unit = z26.number().min(0).max(1);
|
|
16848
|
+
var Span = z26.object({ x0: Unit, y0: Unit, x1: Unit, y1: Unit }).strict();
|
|
16849
|
+
var RenderRatio = z26.string().regex(/^\d+(?:\.\d+)?:\d+(?:\.\d+)?$/);
|
|
16850
|
+
var Copy = z26.array(z26.object({ text: z26.string().min(1), role: z26.string().optional() }).strict());
|
|
16610
16851
|
var staticPlaceNode = defineNode({
|
|
16611
16852
|
id: "static_place",
|
|
16612
|
-
version: "1.
|
|
16853
|
+
version: "1.1.0",
|
|
16613
16854
|
category: "image",
|
|
16614
16855
|
location: "local",
|
|
16615
|
-
summary: "Place a finished static ad's words and logo where no interface covers them and nothing runs off the frame, for an image model to paint the margins: reads where every line and the logo sit, shrinks the ad (to 60% at most) and sets it inside the size's safe area. `placed` keeps the margins clear (for `static_extend_merge`); `shown` is the same inside the model's render shape with grey margins, to paint. `fit` is null, and both are the ad unchanged, when nothing sits outside.",
|
|
16856
|
+
summary: "Place a finished static ad's words and logo where no interface covers them and nothing runs off the frame, for an image model to paint the margins: reads where every line and the logo sit, shrinks the ad (to 60% at most) and sets it inside the size's safe area. `placed` keeps the margins clear (for `static_extend_merge`); `shown` is the same inside the model's render shape with grey margins, to paint; `keep` is where every line, the logo and the buttons read sit in `placed`, all the merge lays back of the ad's own pixels. `fit` is null, and both are the ad unchanged, when nothing sits outside.",
|
|
16616
16857
|
when_to_use: "Added by a static ad's autofix round (place-and-extend) before an `image_generate` that paints the margins and a `static_extend_merge`; never scaffolded. One box-reader call (about $0.005).",
|
|
16617
|
-
inputs:
|
|
16618
|
-
params:
|
|
16619
|
-
format:
|
|
16620
|
-
placements:
|
|
16858
|
+
inputs: z26.object({ image: ImageRef }).strict(),
|
|
16859
|
+
params: z26.object({
|
|
16860
|
+
format: z26.string().min(1),
|
|
16861
|
+
placements: z26.array(z26.string().min(1)).optional(),
|
|
16621
16862
|
render_ratio: RenderRatio,
|
|
16622
16863
|
copy: Copy.optional()
|
|
16623
16864
|
}).strict(),
|
|
16624
|
-
outputs:
|
|
16865
|
+
outputs: z26.object({
|
|
16625
16866
|
placed: ImageRef,
|
|
16626
16867
|
shown: ImageRef,
|
|
16627
16868
|
fit: Fit.nullable(),
|
|
16628
|
-
|
|
16629
|
-
|
|
16869
|
+
/** Where the words, logo and buttons read sit in `placed`: all the merge lays back of the ad's own pixels. */
|
|
16870
|
+
keep: z26.array(Span).optional(),
|
|
16871
|
+
reason: z26.enum(["not_read", "inside_band", "too_far"]).optional(),
|
|
16872
|
+
usage: z26.array(Usage).optional()
|
|
16630
16873
|
}).strict(),
|
|
16631
16874
|
outputKinds: { placed: "image", shown: "image" },
|
|
16632
16875
|
cost: () => ({ credits: 0, seconds_estimate: 5 }),
|
|
@@ -16667,20 +16910,30 @@ var staticPlaceNode = defineNode({
|
|
|
16667
16910
|
placed: placedRef,
|
|
16668
16911
|
shown: shownRef,
|
|
16669
16912
|
fit,
|
|
16913
|
+
keep: keptBoxes([...reading.boxes, ...boxes.filter((b) => !reading.boxes.includes(b))], fit),
|
|
16670
16914
|
...spend
|
|
16671
16915
|
};
|
|
16672
16916
|
}
|
|
16673
16917
|
});
|
|
16918
|
+
function keptBoxes(boxes, fit) {
|
|
16919
|
+
const at2 = (offset2, v) => Math.min(1, Math.max(0, offset2 + fit.scale * v));
|
|
16920
|
+
return boxes.map((b) => ({ x0: at2(fit.x, b.x0), y0: at2(fit.y, b.y0), x1: at2(fit.x, b.x1), y1: at2(fit.y, b.y1) }));
|
|
16921
|
+
}
|
|
16674
16922
|
var staticExtendMergeNode = defineNode({
|
|
16675
16923
|
id: "static_extend_merge",
|
|
16676
|
-
version: "1.
|
|
16924
|
+
version: "1.2.0",
|
|
16677
16925
|
category: "image",
|
|
16678
16926
|
location: "local",
|
|
16679
|
-
summary: "Have the image model paint a placed static ad's margins (`static_place`'s `shown`, with `paint`, an `image_generate`'s params) and lay the ad
|
|
16927
|
+
summary: "Have the image model paint a placed static ad's margins (`static_place`'s `shown`, with `paint`, an `image_generate`'s params) and lay the ad's own pixels back over the painting only in `keep` (`static_place`'s boxes of the words, logo and buttons, padded and feathered): the painting is the rest of the frame, one picture with no edge where the shrunk ad was. Without `keep`, or when the painting moved the words, the whole ad (`placed`) is laid back, the painting only in the margins, blended at their seam. Same pixel size as the ad. An ad that was never placed (`shown` is `placed`) passes on as it is, and nothing is painted or billed. A painting already made can be wired as `image` instead of `paint`.",
|
|
16680
16928
|
when_to_use: "Added by a static ad's autofix round after a `static_place`; never scaffolded. One picture at the painter's price, only for a size that was placed.",
|
|
16681
|
-
inputs:
|
|
16682
|
-
|
|
16683
|
-
|
|
16929
|
+
inputs: z26.object({
|
|
16930
|
+
placed: ImageRef,
|
|
16931
|
+
shown: ImageRef.optional(),
|
|
16932
|
+
image: ImageRef.optional(),
|
|
16933
|
+
keep: z26.array(Span).optional()
|
|
16934
|
+
}).strict(),
|
|
16935
|
+
params: z26.object({ render_ratio: RenderRatio, paint: z26.record(z26.string(), z26.unknown()).optional() }).strict(),
|
|
16936
|
+
outputs: z26.object({ image: ImageRef }).strict(),
|
|
16684
16937
|
outputKinds: { image: "image" },
|
|
16685
16938
|
cost: ({ params }) => params.paint ? imageGenerateNode.cost?.({ params: params.paint }) ?? NO_COST : NO_COST,
|
|
16686
16939
|
async execute({ inputs, params, ctx }) {
|
|
@@ -16688,7 +16941,7 @@ var staticExtendMergeNode = defineNode({
|
|
|
16688
16941
|
const painted = painting?.path ?? painting?.url;
|
|
16689
16942
|
const placed = inputs.placed.path ?? inputs.placed.url;
|
|
16690
16943
|
if (!painted || !placed) return { image: inputs.placed };
|
|
16691
|
-
const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio);
|
|
16944
|
+
const merged = await mergeExtended(await bytesOf(painted), await bytesOf(placed), params.render_ratio, inputs.keep);
|
|
16692
16945
|
const image = await ctx.assets.ingestBytes({ bytes: merged, kind: "image", mime: "image/png" });
|
|
16693
16946
|
return { image };
|
|
16694
16947
|
}
|
|
@@ -16712,7 +16965,7 @@ async function paintMargins(inputs, paint, ctx) {
|
|
|
16712
16965
|
|
|
16713
16966
|
// src/engine/nodes/local/staticPrefilter.ts
|
|
16714
16967
|
import { readFile as readFile16 } from "fs/promises";
|
|
16715
|
-
import { z as
|
|
16968
|
+
import { z as z27 } from "zod";
|
|
16716
16969
|
|
|
16717
16970
|
// src/engine/nodes/local/lib/variant-prefilter.ts
|
|
16718
16971
|
import { createHash as createHash3 } from "crypto";
|
|
@@ -16930,8 +17183,8 @@ The candidates are Image 1 to Image ${jpegs.length}, in the order shown. Reply w
|
|
|
16930
17183
|
}
|
|
16931
17184
|
|
|
16932
17185
|
// src/engine/nodes/local/staticPrefilter.ts
|
|
16933
|
-
var Dropped =
|
|
16934
|
-
var Usage2 =
|
|
17186
|
+
var Dropped = z27.object({ index: z27.number().int().min(0), reasons: z27.array(z27.string()) }).strict();
|
|
17187
|
+
var Usage2 = z27.object({ model: z27.string(), inputTokens: z27.number(), outputTokens: z27.number(), costUsd: z27.number() }).strict();
|
|
16935
17188
|
async function readSpec(ref) {
|
|
16936
17189
|
if (!ref?.path) return null;
|
|
16937
17190
|
try {
|
|
@@ -16950,42 +17203,42 @@ var staticPrefilterNode = defineNode({
|
|
|
16950
17203
|
passthroughRefs: true,
|
|
16951
17204
|
summary: "Rank a static ad's variants before the judge by the deterministic checks (blocking defects, copy read back, the brand's logo found, the button's words, words inside the safe area) and pass on only the best-ranked, so the judge only breaks an exact tie. Outputs them in an order seeded by the pictures (never the render order), `order` (which variant each one is) and `dropped` with each set-aside variant's reasons.",
|
|
16952
17205
|
when_to_use: "Scaffolded by `baker canvas scaffold-static-ad` between the variants and the judge; wire the variants' whole `images` list and the ad spec (`$ref:prompt.asset`). Never leaves the judge empty: when every variant fails, the one with the fewest defects passes as `flagged`, and the review names what is wrong. A variant the reader could not read passes unchecked.",
|
|
16953
|
-
inputs:
|
|
17206
|
+
inputs: z27.object({ images: z27.array(ImageRef).min(1), spec: JsonRef.optional() }).strict(),
|
|
16954
17207
|
// `master_shape` and `placements` are the ones the canvas's sizes were planned with, so the
|
|
16955
17208
|
// band the variants are checked against is the band every size is cut to keep.
|
|
16956
|
-
params:
|
|
16957
|
-
formats:
|
|
16958
|
-
master_shape:
|
|
16959
|
-
placements:
|
|
17209
|
+
params: z27.object({
|
|
17210
|
+
formats: z27.array(z27.string().min(1)).min(1),
|
|
17211
|
+
master_shape: z27.enum(MASTER_SHAPES).optional(),
|
|
17212
|
+
placements: z27.array(z27.string().min(1)).optional(),
|
|
16960
17213
|
// The judge, asked only on a real tie: its model, and the criteria compiled from prompt.json.
|
|
16961
|
-
judge_model:
|
|
16962
|
-
judge_prompt:
|
|
17214
|
+
judge_model: z27.string().min(1).optional(),
|
|
17215
|
+
judge_prompt: z27.string().min(1).optional(),
|
|
16963
17216
|
// What the checks read of prompt.json, projected on every run (`staticChecksOf`); without it, the `spec` input.
|
|
16964
|
-
checks:
|
|
16965
|
-
kind:
|
|
16966
|
-
lines:
|
|
16967
|
-
|
|
17217
|
+
checks: z27.object({
|
|
17218
|
+
kind: z27.literal("static_checks"),
|
|
17219
|
+
lines: z27.array(
|
|
17220
|
+
z27.object({ text: z27.string(), lettering: z27.string(), role: z27.string().optional() }).passthrough()
|
|
16968
17221
|
),
|
|
16969
|
-
buttons:
|
|
16970
|
-
brand:
|
|
16971
|
-
logo:
|
|
17222
|
+
buttons: z27.array(z27.string()),
|
|
17223
|
+
brand: z27.string().optional(),
|
|
17224
|
+
logo: z27.boolean()
|
|
16972
17225
|
}).optional()
|
|
16973
17226
|
}).strict(),
|
|
16974
|
-
outputs:
|
|
16975
|
-
images:
|
|
17227
|
+
outputs: z27.object({
|
|
17228
|
+
images: z27.array(ImageRef).min(1),
|
|
16976
17229
|
// Which variant (its place among the inputs) each of `images` is, so the judge's "Image k" maps back.
|
|
16977
|
-
order:
|
|
17230
|
+
order: z27.array(z27.number().int().min(0)).optional(),
|
|
16978
17231
|
// The picture every size is made from: the checks' first, or on a tie the judge's. Cut to the
|
|
16979
17232
|
// master's frame where every word stays whole when its centre would cut one, else as rendered.
|
|
16980
17233
|
image: ImageRef.optional(),
|
|
16981
17234
|
// Which variant it is (its place among the inputs).
|
|
16982
|
-
chosen:
|
|
17235
|
+
chosen: z27.number().int().min(0).optional(),
|
|
16983
17236
|
// The judge's ranking lines, when a tie was put to it.
|
|
16984
|
-
reasoning:
|
|
16985
|
-
dropped:
|
|
17237
|
+
reasoning: z27.string().optional(),
|
|
17238
|
+
dropped: z27.array(Dropped),
|
|
16986
17239
|
flagged: Dropped.optional(),
|
|
16987
17240
|
// What the reader's calls cost, as the gateway reported it (the run record carries it home).
|
|
16988
|
-
usage:
|
|
17241
|
+
usage: z27.array(Usage2).optional()
|
|
16989
17242
|
}).strict(),
|
|
16990
17243
|
outputKinds: { images: "image", image: "image" },
|
|
16991
17244
|
cost: () => ({ credits: 0, seconds_estimate: 1 }),
|
|
@@ -17031,10 +17284,10 @@ async function framed(picture, frame, size2, ctx) {
|
|
|
17031
17284
|
}
|
|
17032
17285
|
|
|
17033
17286
|
// src/engine/nodes/local/staticSafeFit.ts
|
|
17034
|
-
import { z as
|
|
17035
|
-
var Usage3 =
|
|
17036
|
-
var Fit2 =
|
|
17037
|
-
var Copy2 =
|
|
17287
|
+
import { z as z28 } from "zod";
|
|
17288
|
+
var Usage3 = z28.object({ model: z28.string(), inputTokens: z28.number(), outputTokens: z28.number(), costUsd: z28.number() }).strict();
|
|
17289
|
+
var Fit2 = z28.object({ scale: z28.number(), x: z28.number(), y: z28.number() }).strict();
|
|
17290
|
+
var Copy2 = z28.array(z28.object({ text: z28.string().min(1), role: z28.string().optional() }).strict());
|
|
17038
17291
|
var staticSafeFitNode = defineNode({
|
|
17039
17292
|
id: "static_safe_fit",
|
|
17040
17293
|
version: "1.0.0",
|
|
@@ -17042,19 +17295,19 @@ var staticSafeFitNode = defineNode({
|
|
|
17042
17295
|
location: "local",
|
|
17043
17296
|
summary: "Bring a finished static ad's words and logo inside the band its placement leaves clear (the Stories header, the Reels footer) without rendering again: reads where every line and the logo sit, then shrinks the picture a little (to 85% at most) and shifts it, carrying its plain edge into the uncovered margin. Outputs the same pixel size; `fit` is the scale and position used, or null with `reason` when the picture is passed on unchanged.",
|
|
17044
17297
|
when_to_use: "Scaffolded by `baker canvas scaffold-static-ad` after every 9:16 size (`safe_<size>`, role `static_safe_fit`). Free, besides one box-reader call (about $0.005, carried on the run record's review spend). A layout too tall or wide for the band at 85%, or whose edge is busy (cards, leaves, a scene that would smear), is passed on unchanged: the review still names what Reels or Stories covers, and the layout itself has to change (shorter copy, fewer lines).",
|
|
17045
|
-
inputs:
|
|
17298
|
+
inputs: z28.object({ image: ImageRef }).strict(),
|
|
17046
17299
|
// `placements` are the ones the canvas's sizes were planned with (the ad set's), so the band
|
|
17047
17300
|
// the fit clears is the one the review holds the size to.
|
|
17048
|
-
params:
|
|
17049
|
-
outputs:
|
|
17301
|
+
params: z28.object({ format: z28.string().min(1), placements: z28.array(z28.string().min(1)).optional(), copy: Copy2.optional() }).strict(),
|
|
17302
|
+
outputs: z28.object({
|
|
17050
17303
|
image: ImageRef,
|
|
17051
17304
|
fit: Fit2.nullable(),
|
|
17052
17305
|
// Why the picture passed unchanged: the reader could not read it, nothing was outside the
|
|
17053
17306
|
// band, clearing it needed more than the smallest shrink that still looks native, or the
|
|
17054
17307
|
// edge it would carry outward is not plain ground.
|
|
17055
|
-
reason:
|
|
17308
|
+
reason: z28.enum(["not_read", "inside_band", "too_far", "busy_edge"]).optional(),
|
|
17056
17309
|
// What the reader's call cost, as the gateway reported it (the run record carries it home).
|
|
17057
|
-
usage:
|
|
17310
|
+
usage: z28.array(Usage3).optional()
|
|
17058
17311
|
}).strict(),
|
|
17059
17312
|
outputKinds: { image: "image" },
|
|
17060
17313
|
cost: () => ({ credits: 0, seconds_estimate: 5 }),
|
|
@@ -17090,60 +17343,60 @@ var staticSafeFitNode = defineNode({
|
|
|
17090
17343
|
});
|
|
17091
17344
|
|
|
17092
17345
|
// src/engine/nodes/local/text.ts
|
|
17093
|
-
import { z as
|
|
17346
|
+
import { z as z29 } from "zod";
|
|
17094
17347
|
var textNode = defineNode({
|
|
17095
17348
|
id: "text",
|
|
17096
17349
|
version: "1.0.0",
|
|
17097
17350
|
category: "data",
|
|
17098
17351
|
location: "local",
|
|
17099
17352
|
summary: "A literal text value. Use for prompts, descriptions, copy.",
|
|
17100
|
-
inputs:
|
|
17101
|
-
params:
|
|
17102
|
-
outputs:
|
|
17353
|
+
inputs: z29.object({}).strict(),
|
|
17354
|
+
params: z29.object({ value: z29.string() }).strict(),
|
|
17355
|
+
outputs: z29.object({ text: z29.string() }).strict(),
|
|
17103
17356
|
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
17104
17357
|
execute: ({ params }) => Promise.resolve({ text: params.value })
|
|
17105
17358
|
});
|
|
17106
17359
|
|
|
17107
17360
|
// src/engine/nodes/local/videoConcat.ts
|
|
17108
|
-
import { z as
|
|
17361
|
+
import { z as z30 } from "zod";
|
|
17109
17362
|
var FFMPEG_BIN4 = "ffmpeg";
|
|
17110
|
-
var evenPixels = (axis) =>
|
|
17111
|
-
var Transition =
|
|
17112
|
-
type:
|
|
17113
|
-
duration_s:
|
|
17363
|
+
var evenPixels = (axis) => z30.number().int().positive().refine((n) => n % 2 === 0, `${axis} must be even: libx264 yuv420p rejects odd sizes`);
|
|
17364
|
+
var Transition = z30.object({
|
|
17365
|
+
type: z30.string().regex(/^[a-z]+$/, "an ffmpeg xfade transition name, e.g. fade, dissolve, zoomin, slideleft").describe("ffmpeg xfade transition name: fade, dissolve, zoomin, wipeleft, slideleft, \u2026"),
|
|
17366
|
+
duration_s: z30.number().positive().describe("Overlap length in seconds (0.4 is the scaffold default).")
|
|
17114
17367
|
}).strict();
|
|
17115
|
-
var ConcatClip =
|
|
17116
|
-
duration_s:
|
|
17368
|
+
var ConcatClip = z30.object({
|
|
17369
|
+
duration_s: z30.number().positive().describe(
|
|
17117
17370
|
"Seconds this clip occupies on the timeline. A clip with a `transition` must carry that much EXTRA footage past it (the overlap eats it), so the joined length is the sum of the clip durations."
|
|
17118
17371
|
),
|
|
17119
|
-
length:
|
|
17372
|
+
length: z30.enum(["source", "trim", "still"]).default("source").describe(
|
|
17120
17373
|
"How the clip gets its length. `source`: play the input as rendered (it must already be duration_s long, plus the transition tail). `trim`: cut duration_s (+ tail) out of a longer input, starting at `offset_s`. `still`: the input is an IMAGE held on screen for duration_s (+ tail)."
|
|
17121
17374
|
),
|
|
17122
|
-
offset_s:
|
|
17123
|
-
fit:
|
|
17375
|
+
offset_s: z30.number().min(0).optional().describe("`trim` only: where the window starts in the input, seconds."),
|
|
17376
|
+
fit: z30.enum(["fill", "pad"]).default("fill").describe(
|
|
17124
17377
|
"`fill` scales to cover the frame and center-crops (footage). `pad` fits inside and letterboxes in black (a real screenshot or designed graphic, whose edges must never be cropped)."
|
|
17125
17378
|
),
|
|
17126
17379
|
transition: Transition.optional().describe(
|
|
17127
17380
|
"The transition OUT of this clip into the next one. Absent = a hard cut."
|
|
17128
17381
|
),
|
|
17129
|
-
continues_frame:
|
|
17382
|
+
continues_frame: z30.boolean().optional().describe(
|
|
17130
17383
|
"True when this clip's first frame repeats the previous clip's last frame (a long shot split in two). `seam_dedup` drops the duplicate without shortening the timeline."
|
|
17131
17384
|
)
|
|
17132
17385
|
}).strict().refine((c) => c.offset_s === void 0 || c.length === "trim", {
|
|
17133
17386
|
message: "offset_s only applies to a `trim` clip",
|
|
17134
17387
|
path: ["offset_s"]
|
|
17135
17388
|
});
|
|
17136
|
-
var VideoConcatParams =
|
|
17389
|
+
var VideoConcatParams = z30.object({
|
|
17137
17390
|
width: evenPixels("width").describe("Output width in pixels (even). Every clip is normalized onto it."),
|
|
17138
17391
|
height: evenPixels("height").describe("Output height in pixels (even)."),
|
|
17139
|
-
fps:
|
|
17140
|
-
seam_dedup:
|
|
17392
|
+
fps: z30.number().int().min(1).max(120).default(30).describe("Output frame rate every clip is resampled to."),
|
|
17393
|
+
seam_dedup: z30.enum(["head", "tail", "off"]).default("head").describe(
|
|
17141
17394
|
"Which copy of a repeated splice frame survives on a `continues_frame` clip: `head` drops the continuing clip's first frame, `tail` drops the previous clip's last frame, `off` keeps both."
|
|
17142
17395
|
),
|
|
17143
|
-
clips:
|
|
17396
|
+
clips: z30.array(ConcatClip).min(1).describe("The clips in timeline order; clip i is wired as `inputs.c<i>`.")
|
|
17144
17397
|
}).strict();
|
|
17145
|
-
var VideoConcatInputs =
|
|
17146
|
-
var VideoConcatOutputs =
|
|
17398
|
+
var VideoConcatInputs = z30.record(z30.string(), z30.custom());
|
|
17399
|
+
var VideoConcatOutputs = z30.object({ video: z30.custom() }).strict();
|
|
17147
17400
|
function inputLengthS(clip2) {
|
|
17148
17401
|
return clip2.duration_s + (clip2.transition?.duration_s ?? 0);
|
|
17149
17402
|
}
|
|
@@ -17286,14 +17539,14 @@ var videoConcatNode = defineNode({
|
|
|
17286
17539
|
});
|
|
17287
17540
|
|
|
17288
17541
|
// src/engine/nodes/remote/audioVoiceConvert.ts
|
|
17289
|
-
import { z as
|
|
17290
|
-
var AudioVoiceConvertParams =
|
|
17291
|
-
model:
|
|
17542
|
+
import { z as z31 } from "zod";
|
|
17543
|
+
var AudioVoiceConvertParams = z31.object({
|
|
17544
|
+
model: z31.literal("elevenlabs/eleven_multilingual_sts_v2"),
|
|
17292
17545
|
/** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
|
|
17293
|
-
voice:
|
|
17294
|
-
output_format:
|
|
17546
|
+
voice: z31.string().min(1),
|
|
17547
|
+
output_format: z31.string().optional(),
|
|
17295
17548
|
/** Strip the source clip's background noise before re-voicing. */
|
|
17296
|
-
remove_background_noise:
|
|
17549
|
+
remove_background_noise: z31.boolean().optional()
|
|
17297
17550
|
}).strict();
|
|
17298
17551
|
var audioVoiceConvertNode = delegated({
|
|
17299
17552
|
id: "audio_voice_convert",
|
|
@@ -17301,12 +17554,12 @@ var audioVoiceConvertNode = delegated({
|
|
|
17301
17554
|
category: "audio",
|
|
17302
17555
|
summary: "Voice Changer / speech-to-speech via ElevenLabs (eleven_multilingual_sts_v2). Re-voices an existing audio clip in a TARGET voice while preserving timing/prosody.",
|
|
17303
17556
|
when_to_use: 'Use to normalize a generator-chosen voice (e.g. a Seedance talking-head clip\'s native audio) into ONE consistent brand voice across every scene \u2014 the cadence is preserved so any lip-sync stays valid. Wire `inputs.voice_ref: $ref:<voice_select>.voice_id` and set `params.voice: "{{voice_ref}}"`.',
|
|
17304
|
-
inputs:
|
|
17557
|
+
inputs: z31.object({
|
|
17305
17558
|
audio: AudioRef,
|
|
17306
17559
|
voice_ref: TextRef.optional()
|
|
17307
17560
|
}).strict(),
|
|
17308
17561
|
params: AudioVoiceConvertParams,
|
|
17309
|
-
outputs:
|
|
17562
|
+
outputs: z31.object({ audio: AudioRef }).strict(),
|
|
17310
17563
|
outputKinds: { audio: "audio" },
|
|
17311
17564
|
// Priced by the shared contract per minute of source audio. The quote cannot see
|
|
17312
17565
|
// the clip's length, so it assumes one minute; the charge uses the real length.
|
|
@@ -17317,33 +17570,33 @@ var audioVoiceConvertNode = delegated({
|
|
|
17317
17570
|
});
|
|
17318
17571
|
|
|
17319
17572
|
// src/engine/nodes/remote/dialogue.ts
|
|
17320
|
-
import { z as
|
|
17321
|
-
var DialogueInput =
|
|
17322
|
-
text:
|
|
17323
|
-
voice_id:
|
|
17573
|
+
import { z as z32 } from "zod";
|
|
17574
|
+
var DialogueInput = z32.object({
|
|
17575
|
+
text: z32.string().min(1),
|
|
17576
|
+
voice_id: z32.string().min(1)
|
|
17324
17577
|
});
|
|
17325
17578
|
var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
|
|
17326
|
-
var DialogueParams =
|
|
17327
|
-
model:
|
|
17579
|
+
var DialogueParams = z32.object({
|
|
17580
|
+
model: z32.enum(DIALOGUE_MODELS),
|
|
17328
17581
|
/**
|
|
17329
17582
|
* Ordered list of lines, each tagged with the voice that should speak it.
|
|
17330
17583
|
* Up to 10 unique voice_ids; total text across all lines should stay under
|
|
17331
17584
|
* ~2000 characters for best quality (ElevenLabs guidance).
|
|
17332
17585
|
*/
|
|
17333
|
-
inputs:
|
|
17334
|
-
language_code:
|
|
17586
|
+
inputs: z32.array(DialogueInput).min(1).max(50),
|
|
17587
|
+
language_code: z32.string().optional(),
|
|
17335
17588
|
/** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
|
|
17336
|
-
settings:
|
|
17337
|
-
seed:
|
|
17338
|
-
apply_text_normalization:
|
|
17589
|
+
settings: z32.record(z32.string(), z32.unknown()).optional(),
|
|
17590
|
+
seed: z32.number().int().min(0).max(4294967295).optional(),
|
|
17591
|
+
apply_text_normalization: z32.enum(["auto", "on", "off"]).optional(),
|
|
17339
17592
|
/**
|
|
17340
17593
|
* When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
|
|
17341
17594
|
* separate `timestamps` output — character-level alignment plus
|
|
17342
17595
|
* per-voice segment markers usable for captions, lipsync, or
|
|
17343
17596
|
* beat-matched cuts in ad creatives.
|
|
17344
17597
|
*/
|
|
17345
|
-
with_timestamps:
|
|
17346
|
-
output_format:
|
|
17598
|
+
with_timestamps: z32.boolean().optional(),
|
|
17599
|
+
output_format: z32.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
17347
17600
|
}).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
|
|
17348
17601
|
message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
|
|
17349
17602
|
path: ["inputs"]
|
|
@@ -17354,36 +17607,36 @@ var dialogueNode = delegated({
|
|
|
17354
17607
|
category: "audio",
|
|
17355
17608
|
summary: "Multi-voice dialogue / VO with ElevenLabs Eleven v3. Each line is tagged with a `voice_id`, so you can render two-character scripts (e.g. ad VO + customer testimonial reaction) in a single call. Setting `with_timestamps: true` adds character-level alignment for caption rendering and lipsync-friendly cuts.",
|
|
17356
17609
|
when_to_use: "Use for any ad creative or website video VO that needs more than narration \u2014 interviews, two-actor scripts, character ads, testimonial reads. For single-voice flat reads the existing `tts` node is cheaper and simpler; reach for `dialogue` when you need multiple speakers in one stitched track or word-level timing for downstream lipsync / captions.",
|
|
17357
|
-
inputs:
|
|
17610
|
+
inputs: z32.object({}).loose(),
|
|
17358
17611
|
params: DialogueParams,
|
|
17359
|
-
outputs:
|
|
17612
|
+
outputs: z32.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
17360
17613
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
17361
17614
|
// Priced by the shared contract, the same per-character rate the backend charges.
|
|
17362
17615
|
cost: ({ params }) => ({ credits: estimateNodeCredits({ nodeType: "dialogue", params }) ?? 1, seconds_estimate: 8 })
|
|
17363
17616
|
});
|
|
17364
17617
|
|
|
17365
17618
|
// src/engine/nodes/remote/imageAspectAdapt.ts
|
|
17366
|
-
import { z as
|
|
17619
|
+
import { z as z33 } from "zod";
|
|
17367
17620
|
var ASPECT_ADAPT_MODELS = [
|
|
17368
17621
|
"google/gemini-3-pro-image",
|
|
17369
17622
|
"google/gemini-3.1-flash-image",
|
|
17370
17623
|
"google/gemini-3-pro-image-preview",
|
|
17371
17624
|
"google/gemini-3.1-flash-image-preview"
|
|
17372
17625
|
];
|
|
17373
|
-
var ImageAspectAdaptParams =
|
|
17374
|
-
model:
|
|
17375
|
-
formats:
|
|
17376
|
-
platform:
|
|
17377
|
-
guidance:
|
|
17626
|
+
var ImageAspectAdaptParams = z33.object({
|
|
17627
|
+
model: z33.enum(ASPECT_ADAPT_MODELS),
|
|
17628
|
+
formats: z33.array(z33.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }).optional(),
|
|
17629
|
+
platform: z33.enum(AD_FORMAT_PLATFORMS).optional(),
|
|
17630
|
+
guidance: z33.string().min(1).optional(),
|
|
17378
17631
|
// The exact lines the ad carries: the reframe keeps each, spelled as given, once.
|
|
17379
|
-
copy:
|
|
17632
|
+
copy: z33.array(z33.string().min(1)).min(1).max(20).optional(),
|
|
17380
17633
|
// Per target format, the fractions of each edge the platform's interface covers: words,
|
|
17381
17634
|
// logo and button stay inside. The static scaffold sets both from prompt.json and `staticSafeArea`.
|
|
17382
|
-
safe_areas:
|
|
17383
|
-
|
|
17384
|
-
|
|
17635
|
+
safe_areas: z33.record(
|
|
17636
|
+
z33.string(),
|
|
17637
|
+
z33.object({ top: z33.number(), bottom: z33.number(), left: z33.number(), right: z33.number() }).strict()
|
|
17385
17638
|
).optional(),
|
|
17386
|
-
image_size:
|
|
17639
|
+
image_size: z33.enum(OPENROUTER_IMAGE_SIZES).optional()
|
|
17387
17640
|
}).strict().refine((p) => p.formats === void 0 !== (p.platform === void 0), {
|
|
17388
17641
|
message: "provide exactly one of `formats` (explicit ratios) or `platform` (a placement preset)",
|
|
17389
17642
|
path: ["formats"]
|
|
@@ -17394,9 +17647,9 @@ var imageAspectAdaptNode = delegated({
|
|
|
17394
17647
|
category: "image",
|
|
17395
17648
|
summary: "Adapt ONE creative into multiple aspect ratios (Meta: 9:16 stories, 1:1 feed, 4:5, 16:9\u2026) in a single step. Give it either an explicit `formats` list OR a `platform` preset (meta_feed, meta_feed_stories, meta_all, linkedin, google_demandgen, tiktok, youtube) that expands to that platform's canonical set. AI recomposes the layout per format \u2014 identical subject, text, logos, colors, and style; the scene is extended/restructured, never stretched or cropped. Formats that already match the source ratio pass through unchanged at zero cost. Outputs are ordered exactly as the resolved format list.",
|
|
17396
17649
|
when_to_use: "Use after a hero creative exists (image_generate, ingest, image_search) to fan it out to every placement format \u2014 wire the creative into `source` and set EITHER `formats` (exact ratios) OR `platform` (e.g. `meta_feed_stories`). Cost is estimated per format; formats matching the source ratio are free pass-throughs. Off-grid platform targets (e.g. Google 1.91:1) are not rendered here \u2014 crop them from the nearest ratio with `baker images crop`. Pick `google/gemini-3.1-flash-image` (Nano Banana 2) while iterating, `google/gemini-3-pro-image` (Nano Banana Pro) for final-quality adaptation.",
|
|
17397
|
-
inputs:
|
|
17650
|
+
inputs: z33.object({ source: ImageRef }).loose(),
|
|
17398
17651
|
params: ImageAspectAdaptParams,
|
|
17399
|
-
outputs:
|
|
17652
|
+
outputs: z33.object({ images: z33.array(ImageRef).min(1) }).strict(),
|
|
17400
17653
|
outputKinds: { images: "image" },
|
|
17401
17654
|
cost: ({ params }) => {
|
|
17402
17655
|
const p = params;
|
|
@@ -17409,18 +17662,18 @@ var imageAspectAdaptNode = delegated({
|
|
|
17409
17662
|
});
|
|
17410
17663
|
|
|
17411
17664
|
// src/engine/nodes/remote/imageBackgroundRemove.ts
|
|
17412
|
-
import { z as
|
|
17413
|
-
var ImageBackgroundRemoveParams =
|
|
17665
|
+
import { z as z34 } from "zod";
|
|
17666
|
+
var ImageBackgroundRemoveParams = z34.object({
|
|
17414
17667
|
// `fal/birefnet-v2` is the legacy id stored canvases carry. It stays the default for one
|
|
17415
17668
|
// release: every backend accepts it, while `replicate/birefnet` needs the backend that ships with
|
|
17416
17669
|
// this CLI, and the two deploy independently.
|
|
17417
|
-
model:
|
|
17418
|
-
operating_resolution:
|
|
17670
|
+
model: z34.enum(modelIdsAccepted("replicate/birefnet")).optional().default("fal/birefnet-v2"),
|
|
17671
|
+
operating_resolution: z34.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
|
|
17419
17672
|
// DEPRECATED, accepted so stored canvases still parse, and ignored: the model this
|
|
17420
17673
|
// node runs (`men1scus/birefnet`) takes only an image and a resolution, has no
|
|
17421
17674
|
// variant and returns no mask. `mask_only: true` still returns the cutout.
|
|
17422
|
-
model_variant:
|
|
17423
|
-
mask_only:
|
|
17675
|
+
model_variant: z34.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().meta({ deprecated: true, description: "Ignored: the model has no variants." }),
|
|
17676
|
+
mask_only: z34.boolean().optional().meta({ deprecated: true, description: "Ignored: the model returns the cutout only, never a mask." })
|
|
17424
17677
|
}).strict();
|
|
17425
17678
|
var imageBackgroundRemoveNode = delegated({
|
|
17426
17679
|
id: "image_background_remove",
|
|
@@ -17428,11 +17681,11 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
17428
17681
|
category: "image",
|
|
17429
17682
|
summary: "Remove the background from an image and return a transparent PNG. Powered by BiRefNet (`men1scus/birefnet` on Replicate).",
|
|
17430
17683
|
when_to_use: "Use to extract subjects from photos for use as overlays in hyperframe compositions, product shots, or compositing pipelines. It returns the alpha-cut image only; there is no mask output and no model variant to pick.",
|
|
17431
|
-
inputs:
|
|
17684
|
+
inputs: z34.object({
|
|
17432
17685
|
image: ImageRef
|
|
17433
17686
|
}).strict(),
|
|
17434
17687
|
params: ImageBackgroundRemoveParams,
|
|
17435
|
-
outputs:
|
|
17688
|
+
outputs: z34.object({ image: ImageRef }).strict(),
|
|
17436
17689
|
outputKinds: { image: "image" },
|
|
17437
17690
|
cost: ({ params }) => ({
|
|
17438
17691
|
credits: estimateNodeCredits({ nodeType: "image_background_remove", params }) ?? 1,
|
|
@@ -17441,7 +17694,7 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
17441
17694
|
});
|
|
17442
17695
|
|
|
17443
17696
|
// src/engine/nodes/remote/imageDescribe.ts
|
|
17444
|
-
import { z as
|
|
17697
|
+
import { z as z35 } from "zod";
|
|
17445
17698
|
var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
|
|
17446
17699
|
var imageDescribeNode = delegated({
|
|
17447
17700
|
id: "image_describe",
|
|
@@ -17449,27 +17702,27 @@ var imageDescribeNode = delegated({
|
|
|
17449
17702
|
category: "vision",
|
|
17450
17703
|
summary: "Reverse-engineer an image into an exhaustive, replication-grade JSON description: who the advertiser is and what they sell (source_context), composition, non-person subjects with expression/treatment, deeply detailed people, brand-identified logos (named by brand, not appearance), camera optics, lighting, color palette WITH per-color brand-ownership (brand vs borrowed-functional) and purpose, materials, visible text, ad signals (proof badges/CTA/price), the persuasion engine (ad_intent), style, post-processing.",
|
|
17451
17704
|
when_to_use: 'Use to turn a reference image into a structured blueprint you can inject into downstream prompts via `{{slot}}` \u2014 e.g. restyle a competitor ad onto your own product, lock a look across a series, or feed exact palette/lighting into image_generate. Purpose-built for market adaptation: logos are identified by brand ("Trustpilot", never "green star"), people and animals carry expression/emotion/intent detail, and each color is tagged brand vs borrowed-functional so a recolor can keep the reds/yellows that do a job. The extraction prompt is baked in; use `focus` to emphasise aspects and `context` to pass known provenance (advertiser, category, market) so source_context and color ownership are grounded. Pick `~google/gemini-pro-latest` for the densest extraction (recommended for ad / market-adaptation passes), `~google/gemini-flash-latest` for cheap/fast passes. The output is rich \u2014 raise `max_tokens` (e.g. 8000+) for dense ads so the JSON isn\'t truncated.',
|
|
17452
|
-
inputs:
|
|
17453
|
-
params:
|
|
17454
|
-
model:
|
|
17455
|
-
focus:
|
|
17456
|
-
context:
|
|
17457
|
-
temperature:
|
|
17458
|
-
max_tokens:
|
|
17705
|
+
inputs: z35.object({ image: ImageRef }).loose(),
|
|
17706
|
+
params: z35.object({
|
|
17707
|
+
model: z35.enum(IMAGE_DESCRIBE_MODELS),
|
|
17708
|
+
focus: z35.string().optional(),
|
|
17709
|
+
context: z35.string().optional(),
|
|
17710
|
+
temperature: z35.number().min(0).max(2).optional(),
|
|
17711
|
+
max_tokens: z35.number().int().positive().optional()
|
|
17459
17712
|
}).strict(),
|
|
17460
|
-
outputs:
|
|
17713
|
+
outputs: z35.object({ description: JsonRef }).strict(),
|
|
17461
17714
|
outputKinds: { description: "json" },
|
|
17462
17715
|
cost: () => ({ credits: 2, seconds_estimate: 10 })
|
|
17463
17716
|
});
|
|
17464
17717
|
|
|
17465
17718
|
// src/engine/nodes/remote/imageLayerize.ts
|
|
17466
|
-
import { z as
|
|
17467
|
-
var ImageLayerizeParams =
|
|
17719
|
+
import { z as z36 } from "zod";
|
|
17720
|
+
var ImageLayerizeParams = z36.object({
|
|
17468
17721
|
/**
|
|
17469
17722
|
* What to separate. Read as extraction instructions, not as a generation
|
|
17470
17723
|
* prompt — "keep the bottle and its shadow as one layer", not "make it blue".
|
|
17471
17724
|
*/
|
|
17472
|
-
instructions:
|
|
17725
|
+
instructions: z36.string().max(600).optional()
|
|
17473
17726
|
}).strict();
|
|
17474
17727
|
var imageLayerizeNode = delegated({
|
|
17475
17728
|
id: "image_layerize",
|
|
@@ -17477,12 +17730,12 @@ var imageLayerizeNode = delegated({
|
|
|
17477
17730
|
category: "image",
|
|
17478
17731
|
summary: "Split an image into editable layers: a transparent PNG per element, plus any copy baked into the picture recovered as real text with its typography. Powered by Seedream 5.0 Pro Layerize, with Ideogram lifting the text off first.",
|
|
17479
17732
|
when_to_use: "Use when you need ONE element of a picture on its own (a product with no background, a logo lockup), or when a shot is right but its copy is wrong and regenerating would change the picture too. The manifest gives you each layer's box and, for text, its exact wording, font, size and colour \u2014 enough to re-set a headline in the original's type. For a plain subject cutout with no text involved, `image_background_remove` is cheaper and one call.",
|
|
17480
|
-
inputs:
|
|
17733
|
+
inputs: z36.object({
|
|
17481
17734
|
image: ImageRef
|
|
17482
17735
|
}).strict(),
|
|
17483
17736
|
params: ImageLayerizeParams,
|
|
17484
|
-
outputs:
|
|
17485
|
-
layers:
|
|
17737
|
+
outputs: z36.object({
|
|
17738
|
+
layers: z36.array(ImageRef),
|
|
17486
17739
|
base: ImageRef,
|
|
17487
17740
|
manifest: JsonRef
|
|
17488
17741
|
}).strict(),
|
|
@@ -17493,24 +17746,24 @@ var imageLayerizeNode = delegated({
|
|
|
17493
17746
|
});
|
|
17494
17747
|
|
|
17495
17748
|
// src/engine/nodes/remote/imageReferenceSheet.ts
|
|
17496
|
-
import { z as
|
|
17749
|
+
import { z as z37 } from "zod";
|
|
17497
17750
|
var REFERENCE_SHEET_MODELS = [
|
|
17498
17751
|
"google/gemini-3-pro-image",
|
|
17499
17752
|
"google/gemini-3.1-flash-image",
|
|
17500
17753
|
"google/gemini-3-pro-image-preview",
|
|
17501
17754
|
"google/gemini-3.1-flash-image-preview"
|
|
17502
17755
|
];
|
|
17503
|
-
var ImageReferenceSheetParams =
|
|
17504
|
-
model:
|
|
17505
|
-
subject_description:
|
|
17756
|
+
var ImageReferenceSheetParams = z37.object({
|
|
17757
|
+
model: z37.enum(REFERENCE_SHEET_MODELS),
|
|
17758
|
+
subject_description: z37.string().min(1),
|
|
17506
17759
|
// `location` = a set/room shown from several camera ANGLES (not a rotated subject),
|
|
17507
17760
|
// so a multi-scene shoot keeps one consistent set.
|
|
17508
|
-
subject_type:
|
|
17509
|
-
views:
|
|
17510
|
-
style:
|
|
17511
|
-
prompt_override:
|
|
17512
|
-
aspect_ratio:
|
|
17513
|
-
image_size:
|
|
17761
|
+
subject_type: z37.enum(["character", "person", "product", "location"]),
|
|
17762
|
+
views: z37.array(z37.string().min(1)).min(2).max(8).optional(),
|
|
17763
|
+
style: z37.string().optional(),
|
|
17764
|
+
prompt_override: z37.string().min(1).optional(),
|
|
17765
|
+
aspect_ratio: z37.enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "1:4", "4:1", "1:8", "8:1"]).optional(),
|
|
17766
|
+
image_size: z37.enum(OPENROUTER_IMAGE_SIZES).optional()
|
|
17514
17767
|
}).strict();
|
|
17515
17768
|
var imageReferenceSheetNode = delegated({
|
|
17516
17769
|
id: "image_reference_sheet",
|
|
@@ -17518,9 +17771,9 @@ var imageReferenceSheetNode = delegated({
|
|
|
17518
17771
|
category: "image",
|
|
17519
17772
|
summary: "Fuse 1\u20136 images of a single subject (person, character, product, or location/set) into ONE multi-view reference sheet \u2014 a labeled grid in consistent style and lighting: a turnaround (FRONT / SIDE / BACK\u2026) for a person/character/product, or several camera angles of the same room (WIDE / REVERSE / DETAIL\u2026) for a location. Curated models: Gemini 3 Pro Image (best fusion + labels), Gemini 3.1 Flash Image (cheap iteration).",
|
|
17520
17773
|
when_to_use: "Use before image_generate / video_generate when a subject must stay consistent across many creatives \u2014 wire the `sheet` output into their `reference` input instead of re-describing the subject per prompt. `subject_description` should be the exact wording you reuse downstream. Pick `google/gemini-3-pro-image` for final 6-view sheets at 2K+, `google/gemini-3.1-flash-image` while iterating.",
|
|
17521
|
-
inputs:
|
|
17774
|
+
inputs: z37.object({ references: z37.array(ImageRef).min(1).max(6) }).loose(),
|
|
17522
17775
|
params: ImageReferenceSheetParams,
|
|
17523
|
-
outputs:
|
|
17776
|
+
outputs: z37.object({ sheet: ImageRef }).strict(),
|
|
17524
17777
|
outputKinds: { sheet: "image" },
|
|
17525
17778
|
cost: ({ params }) => ({
|
|
17526
17779
|
credits: imagePictureCredits(String(params?.model)),
|
|
@@ -17529,10 +17782,10 @@ var imageReferenceSheetNode = delegated({
|
|
|
17529
17782
|
});
|
|
17530
17783
|
|
|
17531
17784
|
// src/engine/nodes/remote/imageSearch.ts
|
|
17532
|
-
import { z as
|
|
17533
|
-
var ImageSearchParams =
|
|
17534
|
-
prompt:
|
|
17535
|
-
count:
|
|
17785
|
+
import { z as z38 } from "zod";
|
|
17786
|
+
var ImageSearchParams = z38.object({
|
|
17787
|
+
prompt: z38.string().min(1),
|
|
17788
|
+
count: z38.number().int().min(1).max(20).default(5)
|
|
17536
17789
|
}).strict();
|
|
17537
17790
|
var imageSearchNode = delegated({
|
|
17538
17791
|
id: "image_search",
|
|
@@ -17540,15 +17793,15 @@ var imageSearchNode = delegated({
|
|
|
17540
17793
|
category: "image",
|
|
17541
17794
|
summary: "Agentic image search across Google Images, free stock photography (Pexels and Pixabay), and Pinterest. An LLM agent picks the search tools and queries, selects the best matches, and the results are downloaded into canvas assets.",
|
|
17542
17795
|
when_to_use: "Use to gather real-world reference or inspiration images for a prompt (e.g. several photos of an australian shepherd) so a later step or the user can pick the best one. Not for creating new imagery \u2014 use image_generate for that.",
|
|
17543
|
-
inputs:
|
|
17796
|
+
inputs: z38.object({}).loose(),
|
|
17544
17797
|
params: ImageSearchParams,
|
|
17545
|
-
outputs:
|
|
17798
|
+
outputs: z38.object({ images: z38.array(ImageRef).min(1) }).strict(),
|
|
17546
17799
|
outputKinds: { images: "image" },
|
|
17547
17800
|
cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
|
|
17548
17801
|
});
|
|
17549
17802
|
|
|
17550
17803
|
// src/engine/nodes/remote/imageSelect.ts
|
|
17551
|
-
import { z as
|
|
17804
|
+
import { z as z39 } from "zod";
|
|
17552
17805
|
var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
17553
17806
|
var imageSelectNode = delegated({
|
|
17554
17807
|
id: "image_select",
|
|
@@ -17556,16 +17809,16 @@ var imageSelectNode = delegated({
|
|
|
17556
17809
|
category: "vision",
|
|
17557
17810
|
summary: "Pick the best `count` images out of the candidates with a vision LLM, judged against a prompt. Outputs a passthrough subset of the input refs (no new pixels) plus the model's comparative reasoning. A single candidate passes through unjudged, free and without reasoning.",
|
|
17558
17811
|
when_to_use: "Use after an image_generate run with `n` > 1 (wire its whole `images` array), several image_generate variants, or any pool of images, to keep only the strongest before expensive downstream steps \u2014 video generation, reference sheets, final delivery. `count` fixes the output size, so `images#0`\u2026`images#count-1` are always safe to wire. Pick `~google/gemini-flash-latest` for cheap/fast picks and `~google/gemini-pro-latest` for harder aesthetic judgement.",
|
|
17559
|
-
inputs:
|
|
17560
|
-
params:
|
|
17561
|
-
model:
|
|
17562
|
-
prompt:
|
|
17563
|
-
count:
|
|
17564
|
-
temperature:
|
|
17565
|
-
max_tokens:
|
|
17812
|
+
inputs: z39.object({ images: z39.array(ImageRef).min(1) }).loose(),
|
|
17813
|
+
params: z39.object({
|
|
17814
|
+
model: z39.enum(IMAGE_SELECT_MODELS),
|
|
17815
|
+
prompt: z39.string().min(1),
|
|
17816
|
+
count: z39.number().int().min(1).default(1),
|
|
17817
|
+
temperature: z39.number().min(0).max(2).optional(),
|
|
17818
|
+
max_tokens: z39.number().int().positive().optional()
|
|
17566
17819
|
}).strict(),
|
|
17567
17820
|
// No `reasoning` when a lone candidate passed through: there was no judgement to explain.
|
|
17568
|
-
outputs:
|
|
17821
|
+
outputs: z39.object({ images: z39.array(ImageRef).min(1), reasoning: TextRef.optional() }).strict(),
|
|
17569
17822
|
outputKinds: { images: "image", reasoning: "text" },
|
|
17570
17823
|
cost: () => ({ credits: 1, seconds_estimate: 5 }),
|
|
17571
17824
|
// Arity is only knowable at validate time when `images` is a literal array
|
|
@@ -17590,26 +17843,26 @@ var imageSelectNode = delegated({
|
|
|
17590
17843
|
});
|
|
17591
17844
|
|
|
17592
17845
|
// src/engine/nodes/remote/music.ts
|
|
17593
|
-
import { z as
|
|
17846
|
+
import { z as z40 } from "zod";
|
|
17594
17847
|
var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
|
|
17595
|
-
var MusicParams =
|
|
17596
|
-
model:
|
|
17848
|
+
var MusicParams = z40.object({
|
|
17849
|
+
model: z40.enum(MUSIC_MODELS),
|
|
17597
17850
|
/** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
|
|
17598
|
-
prompt:
|
|
17851
|
+
prompt: z40.string().optional(),
|
|
17599
17852
|
/**
|
|
17600
17853
|
* Structured composition plan (intro / hook / verse / outro sections with
|
|
17601
17854
|
* per-section styles + durations). Mutually exclusive with `prompt`.
|
|
17602
17855
|
*/
|
|
17603
|
-
composition_plan:
|
|
17856
|
+
composition_plan: z40.record(z40.string(), z40.unknown()).optional(),
|
|
17604
17857
|
/** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
|
|
17605
|
-
music_length_ms:
|
|
17606
|
-
seed:
|
|
17858
|
+
music_length_ms: z40.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
|
|
17859
|
+
seed: z40.number().int().optional(),
|
|
17607
17860
|
/** Prompt mode only — forces an instrumental (no vocals) track. */
|
|
17608
|
-
force_instrumental:
|
|
17861
|
+
force_instrumental: z40.boolean().optional(),
|
|
17609
17862
|
/** composition_plan only — honor exact section durations. */
|
|
17610
|
-
respect_sections_durations:
|
|
17863
|
+
respect_sections_durations: z40.boolean().optional(),
|
|
17611
17864
|
/** Emit word-level timestamps alongside the audio. */
|
|
17612
|
-
with_timestamps:
|
|
17865
|
+
with_timestamps: z40.boolean().optional(),
|
|
17613
17866
|
/**
|
|
17614
17867
|
* video-to-music only — short description of the desired score
|
|
17615
17868
|
* ("upbeat synth, fast cuts, 80s") used to bias the model.
|
|
@@ -17621,14 +17874,14 @@ var MusicParams = z39.object({
|
|
|
17621
17874
|
* confident") came back as a film score and the client's verdict was "la
|
|
17622
17875
|
* musica que has elegido es muy dramática".
|
|
17623
17876
|
*/
|
|
17624
|
-
description:
|
|
17877
|
+
description: z40.string().max(1e3).optional().describe(
|
|
17625
17878
|
"The score you want, in sound (instruments, tempo, texture) \u2014 not in feelings. Under speech, name ONE steady character and forbid movement: no build, no drop, no climax, no key change, nothing that 'follows the arc'. This model WATCHES the cut, so it will score the footage's emotion unless told not to \u2014 asking it to track a speaker's emotional arc is how a testimonial bed comes back dramatic. A bed is furniture; the voice carries the story."
|
|
17626
17879
|
),
|
|
17627
17880
|
/** video-to-music only — up to 10 style tags. */
|
|
17628
|
-
tags:
|
|
17881
|
+
tags: z40.array(z40.string()).max(10).optional().describe(
|
|
17629
17882
|
"Style tags. Under speech, prefer tags that hold it still (instrumental, understated, steady, no drums) over tags that move it (cinematic, epic, building, resolving, trailer)."
|
|
17630
17883
|
),
|
|
17631
|
-
output_format:
|
|
17884
|
+
output_format: z40.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
17632
17885
|
}).strict();
|
|
17633
17886
|
var musicNode = delegated({
|
|
17634
17887
|
id: "music",
|
|
@@ -17639,9 +17892,9 @@ var musicNode = delegated({
|
|
|
17639
17892
|
category: "audio",
|
|
17640
17893
|
summary: "Generate music for ad creatives and website video content. `elevenlabs/music-v1` composes from a text prompt or structured composition plan; `elevenlabs/video-background-music-v1` scores an existing video clip provided via `inputs.video`.",
|
|
17641
17894
|
when_to_use: "Use to produce background music or a full score for video ads, hero-section reels, or any motion content. Prefer the video-to-music model when you already have a cut and want music timed to it; use compose-detailed when you have only a brief or want section-level control (intro / hook / outro). Pair the resulting audio with `video_generate` or `video_lipsync` at compose time.",
|
|
17642
|
-
inputs:
|
|
17895
|
+
inputs: z40.object({ video: VideoRef.optional() }).loose(),
|
|
17643
17896
|
params: MusicParams,
|
|
17644
|
-
outputs:
|
|
17897
|
+
outputs: z40.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
17645
17898
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
17646
17899
|
// Priced by the shared contract: per second of audio, 30s when no length is pinned.
|
|
17647
17900
|
cost: ({ params }) => {
|
|
@@ -17673,25 +17926,25 @@ var musicNode = delegated({
|
|
|
17673
17926
|
});
|
|
17674
17927
|
|
|
17675
17928
|
// src/engine/nodes/remote/soundEffect.ts
|
|
17676
|
-
import { z as
|
|
17929
|
+
import { z as z41 } from "zod";
|
|
17677
17930
|
var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
|
|
17678
|
-
var SoundEffectParams =
|
|
17679
|
-
model:
|
|
17931
|
+
var SoundEffectParams = z41.object({
|
|
17932
|
+
model: z41.enum(SOUND_EFFECT_MODELS),
|
|
17680
17933
|
/** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
|
|
17681
|
-
text:
|
|
17934
|
+
text: z41.string().min(1),
|
|
17682
17935
|
/**
|
|
17683
17936
|
* Target length in seconds. 0.5–30. Leave unset to let the model pick the
|
|
17684
17937
|
* natural length for the described effect.
|
|
17685
17938
|
*/
|
|
17686
|
-
duration_seconds:
|
|
17939
|
+
duration_seconds: z41.number().min(0.5).max(30).optional(),
|
|
17687
17940
|
/**
|
|
17688
17941
|
* 0–1. Higher = stick closer to the prompt at the cost of variety; lower
|
|
17689
17942
|
* = let the model interpret more freely. Defaults to 0.3 on the provider.
|
|
17690
17943
|
*/
|
|
17691
|
-
prompt_influence:
|
|
17944
|
+
prompt_influence: z41.number().min(0).max(1).optional(),
|
|
17692
17945
|
/** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
|
|
17693
|
-
loop:
|
|
17694
|
-
output_format:
|
|
17946
|
+
loop: z41.boolean().optional(),
|
|
17947
|
+
output_format: z41.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
17695
17948
|
}).strict();
|
|
17696
17949
|
var soundEffectNode = delegated({
|
|
17697
17950
|
id: "sound_effect",
|
|
@@ -17699,9 +17952,9 @@ var soundEffectNode = delegated({
|
|
|
17699
17952
|
category: "audio",
|
|
17700
17953
|
summary: "Generate short sound effects from a text prompt via ElevenLabs Text-to-Sound. Use for whooshes, impacts, UI clicks, ambient beds, or signature stingers in ad creatives and product videos.",
|
|
17701
17954
|
when_to_use: "Reach for this when you need a punch-in SFX layered against `video_generate` or `hyperframe_render` output \u2014 e.g. a logo whoosh on a hero shot, a click on a CTA cut, a swelling ambient bed under VO. Set `loop: true` for atmospheric beds that need to tile under longer footage; leave `duration_seconds` unset and the model picks a natural length.",
|
|
17702
|
-
inputs:
|
|
17955
|
+
inputs: z41.object({}).loose(),
|
|
17703
17956
|
params: SoundEffectParams,
|
|
17704
|
-
outputs:
|
|
17957
|
+
outputs: z41.object({ audio: AudioRef }).strict(),
|
|
17705
17958
|
outputKinds: { audio: "audio" },
|
|
17706
17959
|
// Priced by the shared contract: per second of audio, 5s when the model picks the length.
|
|
17707
17960
|
cost: ({ params }) => {
|
|
@@ -17711,7 +17964,7 @@ var soundEffectNode = delegated({
|
|
|
17711
17964
|
});
|
|
17712
17965
|
|
|
17713
17966
|
// src/engine/nodes/remote/textGenerate.ts
|
|
17714
|
-
import { z as
|
|
17967
|
+
import { z as z42 } from "zod";
|
|
17715
17968
|
var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
17716
17969
|
var textGenerateNode = delegated({
|
|
17717
17970
|
id: "text_generate",
|
|
@@ -17719,58 +17972,58 @@ var textGenerateNode = delegated({
|
|
|
17719
17972
|
category: "language",
|
|
17720
17973
|
summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
|
|
17721
17974
|
when_to_use: 'Use for any short text generation step in a canvas \u2014 ad copy, hooks, headlines, JSON outputs for downstream nodes. Pick `~google/gemini-flash-latest` for cheap/fast work and `~google/gemini-pro-latest` for harder reasoning. When the output must be JSON for a downstream `{{slot}}` (e.g. the ad-blueprint transform), set `response_format: "json_object"` so the model returns clean JSON with no markdown fences or prose. Set `web_search: true` to let the model search the live web first (OpenRouter `:online`) \u2014 useful when the transform must adapt copy to the target brand\'s real facts (current pricing, the trust signals it actually has) rather than guess.',
|
|
17722
|
-
inputs:
|
|
17723
|
-
params:
|
|
17724
|
-
model:
|
|
17725
|
-
prompt:
|
|
17726
|
-
system:
|
|
17727
|
-
response_format:
|
|
17728
|
-
web_search:
|
|
17729
|
-
temperature:
|
|
17730
|
-
max_tokens:
|
|
17975
|
+
inputs: z42.object({}).loose(),
|
|
17976
|
+
params: z42.object({
|
|
17977
|
+
model: z42.enum(TEXT_GENERATE_MODELS),
|
|
17978
|
+
prompt: z42.string().min(1),
|
|
17979
|
+
system: z42.string().optional(),
|
|
17980
|
+
response_format: z42.enum(["text", "json_object"]).optional(),
|
|
17981
|
+
web_search: z42.boolean().optional(),
|
|
17982
|
+
temperature: z42.number().min(0).max(2).optional(),
|
|
17983
|
+
max_tokens: z42.number().int().positive().optional()
|
|
17731
17984
|
}).strict(),
|
|
17732
|
-
outputs:
|
|
17985
|
+
outputs: z42.object({ text: TextRef }).strict(),
|
|
17733
17986
|
outputKinds: { text: "text" },
|
|
17734
17987
|
cost: () => ({ credits: 1, seconds_estimate: 3 })
|
|
17735
17988
|
});
|
|
17736
17989
|
|
|
17737
17990
|
// src/engine/nodes/remote/tts.ts
|
|
17738
|
-
import { z as
|
|
17991
|
+
import { z as z43 } from "zod";
|
|
17739
17992
|
var TTS_MODELS = ["elevenlabs/eleven_v3"];
|
|
17740
|
-
var TtsVoiceSettings =
|
|
17741
|
-
stability:
|
|
17742
|
-
similarity_boost:
|
|
17743
|
-
style:
|
|
17744
|
-
use_speaker_boost:
|
|
17745
|
-
speed:
|
|
17993
|
+
var TtsVoiceSettings = z43.object({
|
|
17994
|
+
stability: z43.number().min(0).max(1).optional(),
|
|
17995
|
+
similarity_boost: z43.number().min(0).max(1).optional(),
|
|
17996
|
+
style: z43.number().min(0).max(1).optional(),
|
|
17997
|
+
use_speaker_boost: z43.boolean().optional(),
|
|
17998
|
+
speed: z43.number().min(0.25).max(4).optional()
|
|
17746
17999
|
}).strict();
|
|
17747
|
-
var TtsPronunciationLocator =
|
|
17748
|
-
pronunciation_dictionary_id:
|
|
17749
|
-
version_id:
|
|
18000
|
+
var TtsPronunciationLocator = z43.object({
|
|
18001
|
+
pronunciation_dictionary_id: z43.string().min(1),
|
|
18002
|
+
version_id: z43.string().nullable().optional()
|
|
17750
18003
|
}).strict();
|
|
17751
|
-
var TtsParams =
|
|
17752
|
-
model:
|
|
17753
|
-
text:
|
|
17754
|
-
voice:
|
|
18004
|
+
var TtsParams = z43.object({
|
|
18005
|
+
model: z43.enum(TTS_MODELS),
|
|
18006
|
+
text: z43.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
|
|
18007
|
+
voice: z43.string().min(1),
|
|
17755
18008
|
/** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
|
|
17756
|
-
output_format:
|
|
17757
|
-
seed:
|
|
18009
|
+
output_format: z43.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
|
|
18010
|
+
seed: z43.number().int().min(0).max(4294967295).optional(),
|
|
17758
18011
|
// Top-level shortcuts; structured form is `voice_settings`.
|
|
17759
|
-
stability:
|
|
17760
|
-
similarity_boost:
|
|
18012
|
+
stability: z43.number().min(0).max(1).optional(),
|
|
18013
|
+
similarity_boost: z43.number().min(0).max(1).optional(),
|
|
17761
18014
|
voice_settings: TtsVoiceSettings.optional(),
|
|
17762
18015
|
/** ISO 639-1 language code. eleven_v3 supports language hints. */
|
|
17763
|
-
language_code:
|
|
17764
|
-
pronunciation_dictionary_locators:
|
|
17765
|
-
apply_text_normalization:
|
|
18016
|
+
language_code: z43.string().optional(),
|
|
18017
|
+
pronunciation_dictionary_locators: z43.array(TtsPronunciationLocator).max(3).optional(),
|
|
18018
|
+
apply_text_normalization: z43.enum(["auto", "on", "off"]).optional(),
|
|
17766
18019
|
/** Currently Japanese-only. Adds latency. */
|
|
17767
|
-
apply_language_text_normalization:
|
|
18020
|
+
apply_language_text_normalization: z43.boolean().optional(),
|
|
17768
18021
|
/**
|
|
17769
18022
|
* When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
|
|
17770
18023
|
* adds a `timestamps` output (character-level alignment) for caption
|
|
17771
18024
|
* rendering, lipsync, and beat-matched cuts.
|
|
17772
18025
|
*/
|
|
17773
|
-
with_timestamps:
|
|
18026
|
+
with_timestamps: z43.boolean().optional()
|
|
17774
18027
|
}).strict();
|
|
17775
18028
|
var ttsNode = delegated({
|
|
17776
18029
|
id: "tts",
|
|
@@ -17778,9 +18031,9 @@ var ttsNode = delegated({
|
|
|
17778
18031
|
category: "audio",
|
|
17779
18032
|
summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
|
|
17780
18033
|
when_to_use: "Use for single-speaker VO \u2014 ad reads, hero-section narration, product walkthroughs. Reach for `dialogue` when you need multiple voices in one stitched track. Set `with_timestamps: true` when downstream needs character-level alignment (captions, lipsync).",
|
|
17781
|
-
inputs:
|
|
18034
|
+
inputs: z43.object({}).loose(),
|
|
17782
18035
|
params: TtsParams,
|
|
17783
|
-
outputs:
|
|
18036
|
+
outputs: z43.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
17784
18037
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
17785
18038
|
// Priced by the shared contract, the same per-character rate the backend charges.
|
|
17786
18039
|
cost: ({ params }) => ({
|
|
@@ -17790,11 +18043,11 @@ var ttsNode = delegated({
|
|
|
17790
18043
|
});
|
|
17791
18044
|
|
|
17792
18045
|
// src/engine/nodes/remote/video.ts
|
|
17793
|
-
import { z as
|
|
17794
|
-
var videoModelEnum =
|
|
17795
|
-
var VideoGenerateParams =
|
|
18046
|
+
import { z as z44 } from "zod";
|
|
18047
|
+
var videoModelEnum = z44.enum(VIDEO_GENERATE_MODELS);
|
|
18048
|
+
var VideoGenerateParams = z44.object({
|
|
17796
18049
|
model: videoModelEnum,
|
|
17797
|
-
...registryParamShape("video_generate", { prompt:
|
|
18050
|
+
...registryParamShape("video_generate", { prompt: z44.string().min(1) })
|
|
17798
18051
|
}).strict();
|
|
17799
18052
|
var videoGenerateNode = delegated({
|
|
17800
18053
|
id: "video_generate",
|
|
@@ -17802,7 +18055,7 @@ var videoGenerateNode = delegated({
|
|
|
17802
18055
|
category: "video",
|
|
17803
18056
|
summary: "Generate video for ad creatives. Curated roster: `google/gemini-omni-1.1-flash` (DEFAULT \u2014 native audio + reference images in one call, 360p-4k, 3-10s), `bytedance/seedance-2.0` (identity/product workhorse, NO photoreal human faces), `google/veo-3.1` (photoreal cine ceiling + the human-face model), `google/veo-3.1-fast` (cheap Veo iteration), `kwaivgi/kling-v3.0-pro` (motion-transfer/dynamic). Async with polling.",
|
|
17804
18057
|
when_to_use: "`google/gemini-omni-1.1-flash` is the DEFAULT: it renders picture and synchronized audio in one call, and is the only model here that takes reference images alongside a prompt. SET `duration` (3-10s) to the length the beat actually needs \u2014 it bills PER SECOND, so leaving it out renders the 10s default and charges for ten seconds of a beat that may need three. SET `resolution` too: `360p` bills a THIRD of `720p` ($0.034 vs $0.101 per second) and is the right tier while you are still choosing between takes \u2014 render the drafts at 360p, then re-render the keeper at `720p` (default), `1080p` ($0.152/s) or `4k` ($0.304/s). It accepts only `16:9` or `9:16`, and takes NO `seed`, `negative_prompt` or `generate_audio` param \u2014 sending one fails validation, and it always renders audio. Keep the default for ordinary beats; move off it only when a beat needs a length outside 3-10s or a 1:1 frame. Use `bytedance/seedance-2.0` for identity/product output WITHOUT a photoreal human face on camera. Any clip whose frame shows a photoreal human face \u2014 a UGC creator, presenter, or testimonial speaker, REAL OR AI-GENERATED \u2014 must use `google/veo-3.1`: ByteDance's filter rejects photoreal human faces on Seedance (content_policy_blocked/E005), and a synthetic AI face does NOT clear it. Use `google/veo-3.1-fast` while iterating to keep cost low; `kwaivgi/kling-v3.0-pro` for motion-transfer/hyper-dynamic beats. `baker canvas scaffold-video` auto-routes a photoreal cast to Veo. A Seedance clip that is refused for a face at runtime is NOT moved to another model: one guided only by reference images is re-rendered on the same model through a second route that accepts faces (about twice the price), and one that starts from a frame comes back as content_policy_blocked \u2014 re-run that clip on `google/veo-3.1`. Each model gates its own durations, resolutions and aspect ratios in the registry: `baker studio models --kind video --model <id>` shows them.",
|
|
17805
|
-
inputs:
|
|
18058
|
+
inputs: z44.object({
|
|
17806
18059
|
first_frame: ImageRef.optional(),
|
|
17807
18060
|
last_frame: ImageRef.optional(),
|
|
17808
18061
|
reference: ImageRef.optional(),
|
|
@@ -17812,10 +18065,10 @@ var videoGenerateNode = delegated({
|
|
|
17812
18065
|
* exclusive with `first_frame` — OpenRouter treats a request carrying both
|
|
17813
18066
|
* as image-to-video and ignores these, so wire one or the other.
|
|
17814
18067
|
*/
|
|
17815
|
-
references:
|
|
18068
|
+
references: z44.array(ImageRef).optional()
|
|
17816
18069
|
}).loose(),
|
|
17817
18070
|
params: VideoGenerateParams,
|
|
17818
|
-
outputs:
|
|
18071
|
+
outputs: z44.object({ video: VideoRef }).strict(),
|
|
17819
18072
|
outputKinds: { video: "video" },
|
|
17820
18073
|
// Priced from the shared contract, so `validate`'s quote and the charge the
|
|
17821
18074
|
// backend applies come from ONE table. A flat number here under-quoted a
|
|
@@ -17830,15 +18083,15 @@ var videoGenerateNode = delegated({
|
|
|
17830
18083
|
});
|
|
17831
18084
|
|
|
17832
18085
|
// src/engine/nodes/remote/videoBackgroundRemove.ts
|
|
17833
|
-
import { z as
|
|
17834
|
-
var VideoBackgroundRemoveParams =
|
|
18086
|
+
import { z as z45 } from "zod";
|
|
18087
|
+
var VideoBackgroundRemoveParams = z45.object({
|
|
17835
18088
|
// `fal/veed-video-background-removal` is the legacy id stored canvases carry. It stays the default for one
|
|
17836
18089
|
// release: every backend accepts it, while `replicate/birefnet-video` needs the backend that ships with
|
|
17837
18090
|
// this CLI, and the two deploy independently.
|
|
17838
|
-
model:
|
|
18091
|
+
model: z45.enum(modelIdsAccepted("replicate/birefnet-video")).optional().default("fal/veed-video-background-removal"),
|
|
17839
18092
|
// Refines the matte's soft edges against background bleed (the model's `refine_fg`).
|
|
17840
|
-
edge_refinement:
|
|
17841
|
-
output_codec:
|
|
18093
|
+
edge_refinement: z45.boolean().optional().default(true),
|
|
18094
|
+
output_codec: z45.enum(["vp9", "h264"]).optional().default("vp9")
|
|
17842
18095
|
}).strict();
|
|
17843
18096
|
var videoBackgroundRemoveNode = delegated({
|
|
17844
18097
|
id: "video_background_remove",
|
|
@@ -17846,11 +18099,11 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
17846
18099
|
category: "video",
|
|
17847
18100
|
summary: "Remove the background from a video and return a transparent VP9-with-alpha WebM (or H264 RGB+alpha pair). Drops directly into a hyperframe composition as `<video src='...'>` for chroma-keyed picture-in-picture overlays. Powered by BiRefNet Video (`sprited/birefnet-video` on Replicate).",
|
|
17848
18101
|
when_to_use: "Use when you need a talking-head or subject to float over a custom background in a hyperframe composition. Pair with hyperframe_render(composition: screencast-with-talker) for screencast-with-narrator videos. Output is `video/webm` with alpha \u2014 feed straight into `<video src>` in a composition.",
|
|
17849
|
-
inputs:
|
|
18102
|
+
inputs: z45.object({
|
|
17850
18103
|
video: VideoRef
|
|
17851
18104
|
}).strict(),
|
|
17852
18105
|
params: VideoBackgroundRemoveParams,
|
|
17853
|
-
outputs:
|
|
18106
|
+
outputs: z45.object({ video: VideoRef }).strict(),
|
|
17854
18107
|
outputKinds: { video: "video" },
|
|
17855
18108
|
// Priced by the shared contract per 30 frames; the quote assumes the same 30s of
|
|
17856
18109
|
// footage the charge falls back to when the clip carries no duration.
|
|
@@ -17861,7 +18114,7 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
17861
18114
|
});
|
|
17862
18115
|
|
|
17863
18116
|
// src/engine/nodes/remote/videoDeconstruct.ts
|
|
17864
|
-
import { z as
|
|
18117
|
+
import { z as z46 } from "zod";
|
|
17865
18118
|
var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
17866
18119
|
var videoDeconstructNode = delegated({
|
|
17867
18120
|
id: "video_deconstruct",
|
|
@@ -17869,34 +18122,34 @@ var videoDeconstructNode = delegated({
|
|
|
17869
18122
|
category: "video",
|
|
17870
18123
|
summary: 'Deconstruct a video into a replication-grade blueprint: scene boundaries, the real start/end frame of every scene (extracted from the video as images), and an exhaustive JSON analysis \u2014 per-scene action detail, camera motion, generation-ready frame/motion prompts, overlay text with full typographic style, floating elements, deeply detailed cast (perceived demographics, ethnicity/skin-tone, styling, market-recasting notes), brand-identified logos (named by brand and what they signal, not by appearance, with on-screen timestamps), dialogue with voice descriptions, music spec, SFX list, plus a word-level transcript. `mode:"index"` is the cheap structure-first pass: scene boundaries + global blueprint only (one LLM call, no frames).',
|
|
17871
18124
|
when_to_use: 'Use to reverse-engineer a reference video (e.g. a competitor ad) so a new canvas can reproduce or remix it scene by scene. Agent loop: (1) optionally run `mode:"index"` to see the structure cheaply (scene count, boundaries, transcript) before planning; (2) run the full deconstruct; (3) read `analysis` and author the reproduction canvas. The blueprint maps 1:1 onto generation nodes: `analysis.scenes[i]` aligns positionally with `start_frames#i`/`end_frames#i`; per scene, `start_frame_prompt`/`end_frame_prompt` feed image_generate (overlay text is excluded from them by contract \u2014 recomposite it from `overlays`), `motion_prompt` + the two frames feed video_generate (first_frame/last_frame), `dialogue[].voice_description` casts tts/dialogue voices, `global.music.music_prompt` feeds music, `sfx[].sound_effect_prompt` feeds sound_effect, and `overlays`/`floating_elements` drive an ffmpeg/hyperframe overlay pass. Long videos (over ~8 min single-shot): run `mode:"index"` first, then several full nodes IN PARALLEL each with a `start_s`/`end_s` window (\u2264480s, snap edges to index scene boundaries), and merge by concatenating `analysis.scenes`; over-length errors include suggested windows. Inject fields into downstream prompts via `{{slot}}`. Pick `~google/gemini-pro-latest` for the densest extraction, `~google/gemini-flash-latest` for cheap/fast passes.',
|
|
17872
|
-
inputs:
|
|
17873
|
-
params:
|
|
17874
|
-
model:
|
|
17875
|
-
mode:
|
|
17876
|
-
language:
|
|
17877
|
-
max_scenes:
|
|
17878
|
-
focus:
|
|
17879
|
-
start_s:
|
|
17880
|
-
end_s:
|
|
18125
|
+
inputs: z46.object({ video: VideoRef }).loose(),
|
|
18126
|
+
params: z46.object({
|
|
18127
|
+
model: z46.enum(VIDEO_DECONSTRUCT_MODELS),
|
|
18128
|
+
mode: z46.enum(["full", "index"]).optional(),
|
|
18129
|
+
language: z46.string().min(2).max(8).optional(),
|
|
18130
|
+
max_scenes: z46.number().int().min(1).max(60).optional(),
|
|
18131
|
+
focus: z46.string().optional(),
|
|
18132
|
+
start_s: z46.number().min(0).optional(),
|
|
18133
|
+
end_s: z46.number().positive().optional(),
|
|
17881
18134
|
// Real visual shot-cut timestamps (absolute seconds), detected locally with
|
|
17882
18135
|
// ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
|
|
17883
18136
|
// onto these and SPLITS any scene that spans one, so a scene's frames never
|
|
17884
18137
|
// straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
|
|
17885
|
-
shot_cuts:
|
|
18138
|
+
shot_cuts: z46.array(z46.number().min(0)).max(200).optional(),
|
|
17886
18139
|
// The video model's per-clip ceiling (seconds). A shot longer than this is
|
|
17887
18140
|
// split into seamless continuation sub-scenes (shared splice frame), so long
|
|
17888
18141
|
// shots reproduce in full instead of being truncated. `scaffold-video` sets
|
|
17889
18142
|
// the Seedance ceiling (15); omit to disable length splitting.
|
|
17890
|
-
max_clip_s:
|
|
18143
|
+
max_clip_s: z46.number().positive().max(60).optional(),
|
|
17891
18144
|
// Transcript provider for the blueprint's dialogue/transcript. Default
|
|
17892
18145
|
// Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
|
|
17893
|
-
transcriber:
|
|
18146
|
+
transcriber: z46.enum(["groq", "deepgram"]).optional()
|
|
17894
18147
|
}).strict(),
|
|
17895
|
-
outputs:
|
|
18148
|
+
outputs: z46.object({
|
|
17896
18149
|
analysis: JsonRef,
|
|
17897
18150
|
// Absent in mode:"index" (structure only, no Mux frame extraction).
|
|
17898
|
-
start_frames:
|
|
17899
|
-
end_frames:
|
|
18151
|
+
start_frames: z46.array(ImageRef).min(1).optional(),
|
|
18152
|
+
end_frames: z46.array(ImageRef).min(1).optional(),
|
|
17900
18153
|
transcript: JsonRef
|
|
17901
18154
|
}).strict(),
|
|
17902
18155
|
outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
|
|
@@ -17904,21 +18157,21 @@ var videoDeconstructNode = delegated({
|
|
|
17904
18157
|
});
|
|
17905
18158
|
|
|
17906
18159
|
// src/engine/nodes/remote/videoLipsync.ts
|
|
17907
|
-
import { z as
|
|
17908
|
-
var VideoLipsyncParams =
|
|
17909
|
-
model:
|
|
18160
|
+
import { z as z47 } from "zod";
|
|
18161
|
+
var VideoLipsyncParams = z47.object({
|
|
18162
|
+
model: z47.enum(modelIdsAccepted("replicate/sync-lipsync-2"))
|
|
17910
18163
|
}).strict();
|
|
17911
18164
|
var videoLipsyncNode = delegated({
|
|
17912
18165
|
id: "video_lipsync",
|
|
17913
18166
|
version: "1.0.0",
|
|
17914
18167
|
category: "video",
|
|
17915
18168
|
summary: "Lip-sync a video to an audio track (`replicate/sync-lipsync-2`: Sync Labs lipsync-2 on Replicate; the legacy id `fal/veed-lipsync` still runs). Billed at $0.40 per minute of the longer input. Runs as a background job.",
|
|
17916
|
-
inputs:
|
|
18169
|
+
inputs: z47.object({
|
|
17917
18170
|
video: VideoRef,
|
|
17918
18171
|
audio: AudioRef
|
|
17919
18172
|
}).strict(),
|
|
17920
18173
|
params: VideoLipsyncParams,
|
|
17921
|
-
outputs:
|
|
18174
|
+
outputs: z47.object({ video: VideoRef }).strict(),
|
|
17922
18175
|
outputKinds: { video: "video" },
|
|
17923
18176
|
// Priced by the shared contract per minute of output; the quote cannot see the
|
|
17924
18177
|
// clips' lengths, so it assumes the same one minute the charge falls back to.
|
|
@@ -17932,7 +18185,7 @@ var videoLipsyncNode = delegated({
|
|
|
17932
18185
|
import { mkdtemp as mkdtemp8, readFile as readFile17, rm as rm8 } from "fs/promises";
|
|
17933
18186
|
import { tmpdir as tmpdir8 } from "os";
|
|
17934
18187
|
import path21 from "path";
|
|
17935
|
-
import { z as
|
|
18188
|
+
import { z as z48 } from "zod";
|
|
17936
18189
|
|
|
17937
18190
|
// src/engine/nodes/local/lib/ffmpeg.ts
|
|
17938
18191
|
import { execFile as execFile8 } from "child_process";
|
|
@@ -18170,30 +18423,30 @@ function timeScriptWords(script, transcript, language) {
|
|
|
18170
18423
|
}
|
|
18171
18424
|
|
|
18172
18425
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
18173
|
-
var VideoTranscribeParams =
|
|
18174
|
-
language:
|
|
18426
|
+
var VideoTranscribeParams = z48.object({
|
|
18427
|
+
language: z48.string().min(2).max(8).optional(),
|
|
18175
18428
|
// Provider choice is explicit (no env-based silent branching). Default Groq
|
|
18176
18429
|
// Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
|
|
18177
18430
|
// `rich` JSON output with punctuated words + paragraph/sentence grouping.
|
|
18178
|
-
transcriber:
|
|
18179
|
-
keyterms:
|
|
18431
|
+
transcriber: z48.enum(["groq", "deepgram"]).optional(),
|
|
18432
|
+
keyterms: z48.array(z48.string().min(2).max(60)).max(30).optional().describe(
|
|
18180
18433
|
"Names the recogniser must not mangle \u2014 a product, a person, a phrase the script coins. The COMPANY'S OWN NAME is always applied server-side, so you never list it: this is for the rest. Every term is biased into the request AND its spelling enforced on the way out, because a caption prints a mishearing at full size over the ad (a real cut shipped with the brand name spelled wrong). Never patch a misspelling inside a caption composition \u2014 that fix dies with that one creative."
|
|
18181
18434
|
),
|
|
18182
|
-
script:
|
|
18435
|
+
script: z48.string().min(1).optional().describe(
|
|
18183
18436
|
"The words the track was asked to say, when there is a script: the transcript's words are then the script's, spelled as written, at the times the recogniser heard them. A misheard word stops being a typo on screen, and a word the recogniser added (a filler) is never printed. Aligned locally, never sent."
|
|
18184
18437
|
)
|
|
18185
18438
|
}).strict();
|
|
18186
|
-
var VideoTranscribeInputs =
|
|
18439
|
+
var VideoTranscribeInputs = z48.object({
|
|
18187
18440
|
// A video (audio auto-extracted locally) OR a bare audio track. The key stays
|
|
18188
18441
|
// `video` for back-compat; the backend already accepts audio-kind refs on it —
|
|
18189
18442
|
// the local extraction path has been shipping one for every video input.
|
|
18190
|
-
video:
|
|
18443
|
+
video: z48.union([VideoRef, AudioRef])
|
|
18191
18444
|
}).strict();
|
|
18192
|
-
var VideoTranscribeOutputs =
|
|
18193
|
-
transcript:
|
|
18445
|
+
var VideoTranscribeOutputs = z48.object({
|
|
18446
|
+
transcript: z48.custom(),
|
|
18194
18447
|
// Only emitted by the Deepgram path: full punctuated words + paragraph /
|
|
18195
18448
|
// sentence grouping with speaker indices. Absent for the default Groq path.
|
|
18196
|
-
rich:
|
|
18449
|
+
rich: z48.custom().optional()
|
|
18197
18450
|
}).strict();
|
|
18198
18451
|
var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
|
|
18199
18452
|
var videoTranscribeNode = defineNode({
|
|
@@ -18306,7 +18559,7 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
18306
18559
|
}
|
|
18307
18560
|
|
|
18308
18561
|
// src/engine/nodes/remote/voiceSelect.ts
|
|
18309
|
-
import { z as
|
|
18562
|
+
import { z as z49 } from "zod";
|
|
18310
18563
|
var voiceSelectNode = delegated({
|
|
18311
18564
|
id: "voice_select",
|
|
18312
18565
|
// 1.1.0: a pinned voice used to come back as an inline `{ kind, text }` with no sha256
|
|
@@ -18324,27 +18577,27 @@ var voiceSelectNode = delegated({
|
|
|
18324
18577
|
category: "audio",
|
|
18325
18578
|
summary: 'Cast an ElevenLabs voice from a natural-language description (e.g. "warm, authoritative female narrator, American accent"). Lists the account\'s voices and ranks them against the brief, emitting the best `voice_id` as a bare-string text asset plus a ranked `candidates` JSON.',
|
|
18326
18579
|
when_to_use: 'Use to turn a voice description (e.g. from a `video_deconstruct` blueprint\'s `voice_description`) into a usable ElevenLabs voice id, then feed it into a `tts` node by wiring `inputs.voice_ref: $ref:<this>.voice_id` and setting `params.voice: "{{voice_ref}}"` \u2014 the engine splices the id in at run time. Review `candidates` (json) to pick a different voice. Optional `gender`/`age`/`accent`/`language` hints sharpen the ranking. `language` is strict: when no voice on the account speaks it the node fails before anything is voiced (add a voice in that language, or pass its `voice_id`). A `gender` nobody in that language has is dropped and `baker canvas run` prints a `[warn]` naming the voice cast instead.',
|
|
18327
|
-
inputs:
|
|
18328
|
-
params:
|
|
18329
|
-
description:
|
|
18330
|
-
gender:
|
|
18331
|
-
age:
|
|
18332
|
-
accent:
|
|
18333
|
-
language:
|
|
18580
|
+
inputs: z49.object({}).loose(),
|
|
18581
|
+
params: z49.object({
|
|
18582
|
+
description: z49.string().min(1),
|
|
18583
|
+
gender: z49.string().optional(),
|
|
18584
|
+
age: z49.string().optional(),
|
|
18585
|
+
accent: z49.string().optional(),
|
|
18586
|
+
language: z49.string().optional(),
|
|
18334
18587
|
/**
|
|
18335
18588
|
* A voice already chosen — an Avatar's pinned one. Given, nothing is cast: the
|
|
18336
18589
|
* ranking is skipped and this id is emitted as-is.
|
|
18337
18590
|
*/
|
|
18338
|
-
voice_id:
|
|
18339
|
-
limit:
|
|
18591
|
+
voice_id: z49.string().min(1).max(120).optional(),
|
|
18592
|
+
limit: z49.number().int().min(1).max(20).optional()
|
|
18340
18593
|
}).strict(),
|
|
18341
|
-
outputs:
|
|
18594
|
+
outputs: z49.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
|
|
18342
18595
|
outputKinds: { voice_id: "text", candidates: "json" },
|
|
18343
18596
|
cost: () => ({ credits: 0, seconds_estimate: 5 })
|
|
18344
18597
|
});
|
|
18345
18598
|
|
|
18346
18599
|
// src/engine/schema/catalog.ts
|
|
18347
|
-
import { z as
|
|
18600
|
+
import { z as z50 } from "zod";
|
|
18348
18601
|
function generateCatalog(registry, opts = {}) {
|
|
18349
18602
|
const entries2 = registry.all().map((def) => {
|
|
18350
18603
|
const cost = catalogCost(def);
|
|
@@ -18355,9 +18608,9 @@ function generateCatalog(registry, opts = {}) {
|
|
|
18355
18608
|
summary: def.summary,
|
|
18356
18609
|
when_to_use: def.when_to_use,
|
|
18357
18610
|
location: def.location,
|
|
18358
|
-
inputs:
|
|
18359
|
-
params:
|
|
18360
|
-
outputs:
|
|
18611
|
+
inputs: z50.toJSONSchema(def.inputs, { unrepresentable: "any" }),
|
|
18612
|
+
params: z50.toJSONSchema(def.params, { unrepresentable: "any" }),
|
|
18613
|
+
outputs: z50.toJSONSchema(def.outputs, { unrepresentable: "any" }),
|
|
18361
18614
|
cost_estimate_credits: cost?.credits,
|
|
18362
18615
|
runtime_estimate_seconds: cost?.seconds_estimate
|
|
18363
18616
|
};
|
|
@@ -18566,6 +18819,33 @@ function createEngineFromEnv(opts = {}) {
|
|
|
18566
18819
|
}
|
|
18567
18820
|
|
|
18568
18821
|
export {
|
|
18822
|
+
JEV_MODEL,
|
|
18823
|
+
askJevQuestions,
|
|
18824
|
+
askJev,
|
|
18825
|
+
looksLikeHttpUrl,
|
|
18826
|
+
MAX_REMOTE_IMAGE_BYTES,
|
|
18827
|
+
fetchExternalBytes,
|
|
18828
|
+
isVimeoAuthFailure,
|
|
18829
|
+
ytDlpBlockSignal,
|
|
18830
|
+
YtDlpError,
|
|
18831
|
+
runYtDlp,
|
|
18832
|
+
probeYtDlp,
|
|
18833
|
+
isAudioOnly,
|
|
18834
|
+
durationSeconds,
|
|
18835
|
+
canonicalUrl,
|
|
18836
|
+
RunAbortedError,
|
|
18837
|
+
LayerExecutionError,
|
|
18838
|
+
describeFailureReason,
|
|
18839
|
+
runFailureCode,
|
|
18840
|
+
sha256Hex,
|
|
18841
|
+
MAX_ASSET_BYTES,
|
|
18842
|
+
isPersistedAssetRef,
|
|
18843
|
+
collectAssetRefLikes,
|
|
18844
|
+
HttpClient,
|
|
18845
|
+
coercePathUrlToUrl,
|
|
18846
|
+
canvasAssetMime,
|
|
18847
|
+
inferMimeFromPath,
|
|
18848
|
+
toModelSafeImage,
|
|
18569
18849
|
asRecord,
|
|
18570
18850
|
shapeListRows,
|
|
18571
18851
|
VIEWER_TEMPERATURE_VALUES,
|
|
@@ -18576,13 +18856,16 @@ export {
|
|
|
18576
18856
|
classifyAudience,
|
|
18577
18857
|
AWARENESS_VALUES,
|
|
18578
18858
|
viewerFromKnowledge,
|
|
18579
|
-
|
|
18859
|
+
RENDER_REVIEW_MODEL,
|
|
18860
|
+
videoPathFromOutput,
|
|
18861
|
+
RENDER_REVIEW_UNAVAILABLE,
|
|
18862
|
+
askAboutImages,
|
|
18863
|
+
askAboutText,
|
|
18864
|
+
reviewRenderedVideo,
|
|
18865
|
+
reviewContextBeside,
|
|
18866
|
+
sceneWindowsBeside,
|
|
18580
18867
|
BackendClient,
|
|
18581
18868
|
requireCredentialsFromEnv,
|
|
18582
|
-
RunAbortedError,
|
|
18583
|
-
LayerExecutionError,
|
|
18584
|
-
describeFailureReason,
|
|
18585
|
-
runFailureCode,
|
|
18586
18869
|
AD_PLATFORMS,
|
|
18587
18870
|
deliverySet,
|
|
18588
18871
|
AD_FORMAT_PLATFORMS,
|
|
@@ -18637,14 +18920,9 @@ export {
|
|
|
18637
18920
|
BlueprintFloatingElementSchema,
|
|
18638
18921
|
resolveConcurrency,
|
|
18639
18922
|
ulid,
|
|
18640
|
-
isPersistedAssetRef,
|
|
18641
|
-
collectAssetRefLikes,
|
|
18642
|
-
sha256Hex,
|
|
18643
|
-
MAX_ASSET_BYTES,
|
|
18644
18923
|
REF_PREFIX,
|
|
18645
18924
|
parseRefExpr,
|
|
18646
18925
|
nodeDependencies,
|
|
18647
|
-
looksLikeHttpUrl,
|
|
18648
18926
|
SEEDANCE_PROFILE,
|
|
18649
18927
|
clipProfileFor,
|
|
18650
18928
|
clipParamRecipe,
|
|
@@ -18658,8 +18936,9 @@ export {
|
|
|
18658
18936
|
assignElementLabels,
|
|
18659
18937
|
referenceRole,
|
|
18660
18938
|
elementTodo,
|
|
18661
|
-
|
|
18662
|
-
|
|
18939
|
+
jevCitationJudge,
|
|
18940
|
+
citedPlatforms,
|
|
18941
|
+
citedBy,
|
|
18663
18942
|
figureForms,
|
|
18664
18943
|
nativeSurfaceName,
|
|
18665
18944
|
nativeCopy,
|
|
@@ -18667,6 +18946,10 @@ export {
|
|
|
18667
18946
|
nativePhotosShown,
|
|
18668
18947
|
avatarPhotoIds,
|
|
18669
18948
|
nativeGateProblems,
|
|
18949
|
+
needsInterfaceWords,
|
|
18950
|
+
INTERFACE_WORDS_PROMPT_VERSION,
|
|
18951
|
+
interfaceWordsPrompt,
|
|
18952
|
+
readInterfaceWords,
|
|
18670
18953
|
COPY_DIRECTIVE_HEADER,
|
|
18671
18954
|
copyStrings,
|
|
18672
18955
|
inSceneText,
|
|
@@ -18715,6 +18998,7 @@ export {
|
|
|
18715
18998
|
LOGO_COMPOSITE_DEFAULT,
|
|
18716
18999
|
nodesWithRole,
|
|
18717
19000
|
rolesById,
|
|
19001
|
+
relaidPictureRole,
|
|
18718
19002
|
formatSlug,
|
|
18719
19003
|
staticMasterFormat,
|
|
18720
19004
|
staticRenderNodes,
|
|
@@ -18727,30 +19011,8 @@ export {
|
|
|
18727
19011
|
wordCount2 as wordCount,
|
|
18728
19012
|
estSpeechS,
|
|
18729
19013
|
estSpeechWindowS,
|
|
18730
|
-
MAX_REMOTE_IMAGE_BYTES,
|
|
18731
|
-
fetchExternalBytes,
|
|
18732
|
-
isVimeoAuthFailure,
|
|
18733
|
-
ytDlpBlockSignal,
|
|
18734
|
-
YtDlpError,
|
|
18735
|
-
runYtDlp,
|
|
18736
|
-
probeYtDlp,
|
|
18737
|
-
isAudioOnly,
|
|
18738
|
-
durationSeconds,
|
|
18739
|
-
canonicalUrl,
|
|
18740
|
-
coercePathUrlToUrl,
|
|
18741
|
-
canvasAssetMime,
|
|
18742
|
-
inferMimeFromPath,
|
|
18743
|
-
toModelSafeImage,
|
|
18744
19014
|
loadCompositionMeta,
|
|
18745
19015
|
perceptualHash,
|
|
18746
|
-
RENDER_REVIEW_MODEL,
|
|
18747
|
-
videoPathFromOutput,
|
|
18748
|
-
RENDER_REVIEW_UNAVAILABLE,
|
|
18749
|
-
askAboutImages,
|
|
18750
|
-
askAboutText,
|
|
18751
|
-
reviewRenderedVideo,
|
|
18752
|
-
reviewContextBeside,
|
|
18753
|
-
sceneWindowsBeside,
|
|
18754
19016
|
parseJudgeScores,
|
|
18755
19017
|
judgeWinner,
|
|
18756
19018
|
staticVerdict,
|
|
@@ -18772,4 +19034,4 @@ export {
|
|
|
18772
19034
|
defaultRegistry,
|
|
18773
19035
|
createEngineFromEnv
|
|
18774
19036
|
};
|
|
18775
|
-
//# sourceMappingURL=chunk-
|
|
19037
|
+
//# sourceMappingURL=chunk-QQX5LPMQ.js.map
|