@koda-sl/baker-cli 0.421.0-dev.8ef74f67e → 0.421.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -10
- package/dist/{chunk-BGAU2ZQF.js → chunk-EXWIIEZP.js} +4 -4
- package/dist/{chunk-6YAPMFMC.js → chunk-FKFJKE75.js} +43 -4
- package/dist/chunk-FKFJKE75.js.map +1 -0
- package/dist/{chunk-PFN3A4VQ.js → chunk-GLPQQP5C.js} +780 -408
- package/dist/chunk-GLPQQP5C.js.map +1 -0
- package/dist/{chunk-KBIEU3CK.js → chunk-LRRWIOJG.js} +3 -3
- package/dist/{chunk-QUFIEBEP.js → chunk-YZLLC7DL.js} +25 -4
- package/dist/chunk-YZLLC7DL.js.map +1 -0
- package/dist/{chunk-EXXI274U.js → chunk-ZADYWHL5.js} +3 -3
- package/dist/cli.js +3105 -1532
- package/dist/cli.js.map +1 -1
- package/dist/{client-LYDB4ONI.js → client-CUHMSNK5.js} +4 -4
- package/dist/engine/index.js +3 -3
- package/dist/{env-37XB6PGZ.js → env-PJTJTW3N.js} +2 -2
- package/dist/{output-DYBL2XBN.js → output-A2ASA36U.js} +5 -5
- package/dist/{shared-XHOQLVMN.js → shared-LV6PIKFE.js} +6 -6
- package/package.json +1 -1
- package/dist/chunk-6YAPMFMC.js.map +0 -1
- package/dist/chunk-PFN3A4VQ.js.map +0 -1
- package/dist/chunk-QUFIEBEP.js.map +0 -1
- /package/dist/{chunk-BGAU2ZQF.js.map → chunk-EXWIIEZP.js.map} +0 -0
- /package/dist/{chunk-KBIEU3CK.js.map → chunk-LRRWIOJG.js.map} +0 -0
- /package/dist/{chunk-EXXI274U.js.map → chunk-ZADYWHL5.js.map} +0 -0
- /package/dist/{client-LYDB4ONI.js.map → client-CUHMSNK5.js.map} +0 -0
- /package/dist/{env-37XB6PGZ.js.map → env-PJTJTW3N.js.map} +0 -0
- /package/dist/{output-DYBL2XBN.js.map → output-A2ASA36U.js.map} +0 -0
- /package/dist/{shared-XHOQLVMN.js.map → shared-LV6PIKFE.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
debugLogHttp,
|
|
3
3
|
readBodyForLog
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FKFJKE75.js";
|
|
5
5
|
import {
|
|
6
6
|
API_KEY_FORMAT_MESSAGE,
|
|
7
7
|
DIRECT_ROUTE,
|
|
@@ -15,16 +15,17 @@ import {
|
|
|
15
15
|
isChallengeBody,
|
|
16
16
|
isPrivateAddress,
|
|
17
17
|
isProxyFailure,
|
|
18
|
+
nextPlannedRoute,
|
|
18
19
|
openRouterKey,
|
|
19
20
|
plannedRoutes,
|
|
20
21
|
refuseNonPublicUrl,
|
|
21
22
|
requireConnection,
|
|
22
23
|
shouldEscalate
|
|
23
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-YZLLC7DL.js";
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
+
// ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
|
|
26
27
|
var require_safe_stable_stringify = __commonJS({
|
|
27
|
-
"
|
|
28
|
+
"../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
|
|
28
29
|
"use strict";
|
|
29
30
|
var { hasOwnProperty } = Object.prototype;
|
|
30
31
|
var stringify = configure2();
|
|
@@ -5173,6 +5174,8 @@ var chatChangeTypeSchema = z14.enum([
|
|
|
5173
5174
|
// A review site listing feeding the testimonials library, keyed on the
|
|
5174
5175
|
// source's id. Immediate — see convex/testimonials/mutations.ts.
|
|
5175
5176
|
"testimonial-source",
|
|
5177
|
+
// A Credential stored or removed, keyed on its name. Immediate — see convex/credentials/mutations.ts.
|
|
5178
|
+
"credential",
|
|
5176
5179
|
// A Google spreadsheet created for a Form's leads. Immediate — see
|
|
5177
5180
|
// convex/flows/destinations/mutations.ts.
|
|
5178
5181
|
"spreadsheet",
|
|
@@ -10145,7 +10148,7 @@ function resolveAdaptFormats(params) {
|
|
|
10145
10148
|
return params.formats ?? [];
|
|
10146
10149
|
}
|
|
10147
10150
|
|
|
10148
|
-
//
|
|
10151
|
+
// ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
|
|
10149
10152
|
var import__ = __toESM(require_safe_stable_stringify(), 1);
|
|
10150
10153
|
var configure = import__.default.configure;
|
|
10151
10154
|
var wrapper_default = import__.default;
|
|
@@ -10389,6 +10392,22 @@ var IMAGE_GENERATE_MODELS = [
|
|
|
10389
10392
|
"google/gemini-3-pro-image-preview"
|
|
10390
10393
|
];
|
|
10391
10394
|
var DEFAULT_IMAGE_GENERATE_MODEL = "google/gemini-3.1-flash-image";
|
|
10395
|
+
var IMAGE_PRICE_TIER = {
|
|
10396
|
+
"openai/gpt-image-2.5-sunburst": "standard",
|
|
10397
|
+
"openai/gpt-image-2": "standard",
|
|
10398
|
+
"openai/gpt-5.4-image-2": "standard",
|
|
10399
|
+
"google/gemini-3.1-flash-image": "standard",
|
|
10400
|
+
"google/gemini-3.1-flash-lite-image": "standard",
|
|
10401
|
+
"google/gemini-3-pro-image": "pro",
|
|
10402
|
+
"recraft/recraft-v4.1-pro-vector": "standard",
|
|
10403
|
+
"google/gemini-3.1-flash-image-preview": "standard",
|
|
10404
|
+
"google/gemini-3-pro-image-preview": "pro"
|
|
10405
|
+
};
|
|
10406
|
+
var IMAGE_TIER_CREDITS = { pro: 20, standard: 5 };
|
|
10407
|
+
function imagePictureCredits(model) {
|
|
10408
|
+
const tier = IMAGE_PRICE_TIER[model] ?? "standard";
|
|
10409
|
+
return IMAGE_TIER_CREDITS[tier];
|
|
10410
|
+
}
|
|
10392
10411
|
var VIDEO_GENERATE_MODELS = [
|
|
10393
10412
|
"bytedance/seedance-2.0",
|
|
10394
10413
|
"bytedance/seedance-2.5",
|
|
@@ -11947,6 +11966,22 @@ function estimateNodeCredits(input) {
|
|
|
11947
11966
|
return usd === null ? null : usdToCredits(usd);
|
|
11948
11967
|
}
|
|
11949
11968
|
|
|
11969
|
+
// ../canvas-contract/src/personRealism.ts
|
|
11970
|
+
var PHONE_CAMERA = "Shot on a phone, an iPhone or a Samsung Galaxy, with the main camera in the default photo mode (not portrait mode): slightly wide angle, held by a friend about a metre away or at arm's length, the background mostly in focus the way phone photos are, the phone's own processing \u2014 HDR evening out a bright window, a touch of over-sharpening, smoothed noise in the shadows, punchy everyday colour. Slightly imperfect framing. Not a professional camera: no bokeh or creamy background blur, no lens compression, no studio or flash lighting, no colour grade.";
|
|
11971
|
+
var REAL_SKIN = "Real skin and a real face: visible pores, fine lines, uneven tone, small marks, stray hairs, slight asymmetry. Clothes with real creases.";
|
|
11972
|
+
var NOT_A_MODEL = "Not stock photography, not a model or an influencer: no beauty retouching, no glowing or plastic skin, no perfectly symmetrical features.";
|
|
11973
|
+
function candidPersonPortraitPrompt(p) {
|
|
11974
|
+
return [
|
|
11975
|
+
`An unretouched candid phone photo of one ordinary, real person: ${p.subject}`,
|
|
11976
|
+
p.wardrobe?.trim() ? `Wearing: ${p.wardrobe.trim()}` : null,
|
|
11977
|
+
`Where: ${p.setting?.trim() || "an everyday place that fits who they are, in daylight"}, available light only. They look toward the lens mid-conversation with a relaxed, unposed expression. Framed from the head to mid-torso, face clearly visible.`,
|
|
11978
|
+
PHONE_CAMERA,
|
|
11979
|
+
REAL_SKIN,
|
|
11980
|
+
NOT_A_MODEL,
|
|
11981
|
+
"No text, logos or watermarks. One person only."
|
|
11982
|
+
].filter((line) => line !== null).join("\n");
|
|
11983
|
+
}
|
|
11984
|
+
|
|
11950
11985
|
// ../canvas-contract/src/validateParams.ts
|
|
11951
11986
|
function isManagedNodeKind(nodeType) {
|
|
11952
11987
|
return Object.keys(MODEL_REGISTRY).includes(nodeType);
|
|
@@ -13647,8 +13682,10 @@ var GPT_IMAGE_BRIEF = {
|
|
|
13647
13682
|
texture: "real textures, true-to-life light and colour",
|
|
13648
13683
|
purpose: ""
|
|
13649
13684
|
},
|
|
13650
|
-
// The user: people look "too AI", faces too dark; wants natural, real-looking people (BENCH.md § "E1 v2")
|
|
13651
|
-
|
|
13685
|
+
// The user: people look "too AI", faces too dark; wants natural, real-looking people (BENCH.md § "E1 v2"),
|
|
13686
|
+
// and again on the final sets, "more natural please" (§ "Fix round"); and real people's photos are
|
|
13687
|
+
// taken on a phone, not a professional camera (AV2, scripts/creative-evals/avatar-realism.mjs).
|
|
13688
|
+
people: "candid people shot on a phone: real skin, natural light, relaxed expressions"
|
|
13652
13689
|
},
|
|
13653
13690
|
// n 1: the variants are one call per direction, not one n=3 call. Same cost (n is billed per
|
|
13654
13691
|
// image, E0) and returns 3/3 either way, but one n=3 call gave >=2 distinct options in 25% of
|
|
@@ -13683,7 +13720,7 @@ var GEMINI_IMAGE_BRIEF = {
|
|
|
13683
13720
|
purpose: "because a polished picture reads as an ad and this one has to read as a real post"
|
|
13684
13721
|
},
|
|
13685
13722
|
produced: { photo: "a real photograph", texture: "real textures and true-to-life light and colour", purpose: "" },
|
|
13686
|
-
people: "the people look real, with natural skin
|
|
13723
|
+
people: "the people look real and candid, shot on a phone, with natural skin, natural light and relaxed expressions"
|
|
13687
13724
|
},
|
|
13688
13725
|
// No `n` on Gemini image (gemini §0.4); Pro 2K costs the same as 1K and small text blurs
|
|
13689
13726
|
// at 1K (gemini §0.3).
|
|
@@ -13769,14 +13806,15 @@ function imageRenderParams(model, want) {
|
|
|
13769
13806
|
var STORIES = { top: 0.16, bottom: 0.22, left: 0.06, right: 0.06 };
|
|
13770
13807
|
var REELS_SAFE_BOTTOM = 0.37;
|
|
13771
13808
|
var FEED = { top: 0.06, bottom: 0.06, left: 0.07, right: 0.07 };
|
|
13809
|
+
var FEED_IMAGE = { top: 0.04, bottom: 0.04, left: 0.04, right: 0.04 };
|
|
13772
13810
|
var EVERY_SURFACE = /* @__PURE__ */ new Set(["automatic", "facebook", "instagram"]);
|
|
13773
13811
|
function staticSafeArea(format, placements) {
|
|
13774
|
-
if (format !== "9:16") return { ...
|
|
13812
|
+
if (format !== "9:16") return { ...FEED_IMAGE, placement: "feed" };
|
|
13775
13813
|
const named = (placements ?? []).map((p) => p.toLowerCase());
|
|
13776
13814
|
const reels = named.length === 0 || named.some((p) => p.includes("reels") || EVERY_SURFACE.has(p));
|
|
13777
13815
|
if (reels) return { ...STORIES, bottom: REELS_SAFE_BOTTOM, placement: "reels" };
|
|
13778
13816
|
if (named.some((p) => p.includes("stor"))) return { ...STORIES, placement: "stories" };
|
|
13779
|
-
return { ...
|
|
13817
|
+
return { ...FEED_IMAGE, placement: "feed" };
|
|
13780
13818
|
}
|
|
13781
13819
|
function videoSafeArea(format) {
|
|
13782
13820
|
return format === "9:16" ? { ...STORIES, bottom: REELS_SAFE_BOTTOM } : FEED;
|
|
@@ -14057,18 +14095,17 @@ function readAdSpec(raw) {
|
|
|
14057
14095
|
const advertiser = raw?.reference?.advertiser;
|
|
14058
14096
|
return adSpecFromBlueprint(raw, { scrub: typeof advertiser === "string" ? [advertiser] : [] });
|
|
14059
14097
|
}
|
|
14060
|
-
var
|
|
14061
|
-
var
|
|
14062
|
-
|
|
14063
|
-
|
|
14064
|
-
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
|
|
14071
|
-
if (line.lettering !== "in_scene" || (words.get(surface(line, i)) ?? 0) <= IN_SCENE_MAX_WORDS) return line;
|
|
14098
|
+
var HAND_LETTERED = /\b(?:hand[- ]?(?:written|writing|lettered|lettering|drawn)|handwrit\w*|scrawl\w*|scribbl\w*|markers?|felt[- ]?(?:tip|pen)s?|chalked|(?:written|drawn|lettered|painted) by hand)\b/i;
|
|
14099
|
+
var HANDWRITING = new RegExp(
|
|
14100
|
+
`${HAND_LETTERED.source}|\\b(?:chalk\\w*|whiteboards?|blackboards?|sticky[- ]notes?|post[- ]?its?|notepads?|notebooks?|napkins?)\\b`,
|
|
14101
|
+
"i"
|
|
14102
|
+
);
|
|
14103
|
+
var handwritten = (line) => !line.on?.trim() || HANDWRITING.test(`${line.on} ${line.look ?? ""} ${line.position ?? ""}`);
|
|
14104
|
+
function withRenderedLettering(spec) {
|
|
14105
|
+
const designed = (line) => line.lettering === "in_scene" && handwritten(line);
|
|
14106
|
+
if (!spec.copy.some(designed)) return spec;
|
|
14107
|
+
const copy = spec.copy.map((line) => {
|
|
14108
|
+
if (!designed(line)) return line;
|
|
14072
14109
|
const { on, look: _look, position, ...rest } = line;
|
|
14073
14110
|
const over = position ?? (on ? `over ${on}` : void 0);
|
|
14074
14111
|
return { ...rest, lettering: "designed", ...over ? { position: over } : {} };
|
|
@@ -14169,14 +14206,22 @@ function referencesFromBlueprint(bp, scrub) {
|
|
|
14169
14206
|
({ collection, entry }) => str(entry.reference_image) && !(collection === "brands_logos" && str(entry.function_in_image) === "advertiser_brand")
|
|
14170
14207
|
);
|
|
14171
14208
|
const counters = /* @__PURE__ */ new Map();
|
|
14172
|
-
|
|
14209
|
+
const pictured = labelled.map(({ collection, entry }) => {
|
|
14173
14210
|
const { role, prefix } = referenceRoleOf(collection, entry);
|
|
14174
14211
|
const n = (counters.get(prefix) ?? 0) + 1;
|
|
14175
14212
|
counters.set(prefix, n);
|
|
14176
|
-
const
|
|
14177
|
-
const what = plainText(
|
|
14213
|
+
const described2 = entry.description ?? entry.apparent_identity_or_role ?? entry.what_it_represents;
|
|
14214
|
+
const what = plainText(described2, scrub) || `the ${role === "identity" ? "subject" : role} in this picture`;
|
|
14178
14215
|
return { id: `${prefix}_${n}`, role, slot: sanitizeId(`el_${str(entry.reference_image)}`, "el"), what };
|
|
14179
14216
|
});
|
|
14217
|
+
const described = entries(bp.subjects).flatMap((entry) => {
|
|
14218
|
+
const what = plainText(entry.description, scrub);
|
|
14219
|
+
if (str(entry.reference_image) || !what || referenceRoleOf("subjects", entry).role !== "product") return [];
|
|
14220
|
+
const n = (counters.get("PRODUCT") ?? 0) + 1;
|
|
14221
|
+
counters.set("PRODUCT", n);
|
|
14222
|
+
return [{ id: `PRODUCT_${n}`, role: "product", what }];
|
|
14223
|
+
});
|
|
14224
|
+
return [...pictured, ...described];
|
|
14180
14225
|
}
|
|
14181
14226
|
function logoPositionOf(bp, scrub) {
|
|
14182
14227
|
const own = entries(bp.brands_logos).find((e) => str(e.function_in_image) === "advertiser_brand");
|
|
@@ -14354,6 +14399,7 @@ function copyAsLineRefs(text, copy) {
|
|
|
14354
14399
|
}
|
|
14355
14400
|
function unwanted(clause, k) {
|
|
14356
14401
|
if (k.logo && /\bno (logos?|wordmarks?|branding)\b/i.test(clause)) return true;
|
|
14402
|
+
if (HAND_LETTERED.test(clause)) return true;
|
|
14357
14403
|
if (/\bsource (ad|composition)\b|\bORIGINAL\b|\bcompetitor|\breference ad\b/.test(clause)) return true;
|
|
14358
14404
|
return k.positive && NEGATION.test(clause.replace(/"[^"\n]*"/g, ""));
|
|
14359
14405
|
}
|
|
@@ -14376,7 +14422,10 @@ function clip(text, max) {
|
|
|
14376
14422
|
if (wordCount(out + s) > max) break;
|
|
14377
14423
|
out += s;
|
|
14378
14424
|
}
|
|
14379
|
-
|
|
14425
|
+
if (out.trim()) return sentence(out.trim().replace(/[\s,;:]+$/, ""));
|
|
14426
|
+
const head = text.split(/\s+/).slice(0, max).join(" ");
|
|
14427
|
+
const clause = head.match(/^(.*?)[,;:—–](?:\s[^,;:—–]*)?$/)?.[1];
|
|
14428
|
+
return sentence((clause?.trim() || head).replace(/[\s,;:]+$/, ""));
|
|
14380
14429
|
}
|
|
14381
14430
|
function neutralName(l) {
|
|
14382
14431
|
if (l > 0.95) return "white";
|
|
@@ -14500,7 +14549,9 @@ function logoInstruction(p, ctx) {
|
|
|
14500
14549
|
const at2 = position ? `, ${position.replace(/[\s,;:.!?]+$/, "")}` : "";
|
|
14501
14550
|
const plate = ctx.plates.length > 0 ? ctx.plates.map((u) => `${u.name} (${u.hex})`).join(" or ") : "white or black";
|
|
14502
14551
|
const where = `at least a fifth of the frame wide, on an area it contrasts with or a small ${plate} plate`;
|
|
14503
|
-
|
|
14552
|
+
if (ctx.short)
|
|
14553
|
+
return "the brand's real logo. Reproduce it once, exactly as drawn, at least a fifth of the frame wide.";
|
|
14554
|
+
return ctx.flowing ? `the brand's real logo. Reproduce it once, apart from the product${at2}, exactly as drawn in Image ${p.index}, ${where}.` : `the brand's real logo. Reproduce it once, never on the product${at2}, exactly as drawn, ${where}.`;
|
|
14504
14555
|
}
|
|
14505
14556
|
function identityInstruction(p, what, keep, flowing) {
|
|
14506
14557
|
if (p.spec && personOrAnimal(p.spec) === "person") {
|
|
@@ -14600,6 +14651,7 @@ var ROLE_WORDS = {
|
|
|
14600
14651
|
discount: "the discount",
|
|
14601
14652
|
cta: "the button",
|
|
14602
14653
|
button: "the button",
|
|
14654
|
+
call_to_action: "the button",
|
|
14603
14655
|
caption: "a caption",
|
|
14604
14656
|
fine_print: "the fine print",
|
|
14605
14657
|
legal: "the fine print",
|
|
@@ -14640,7 +14692,8 @@ function textNotes(copy, profile) {
|
|
|
14640
14692
|
notes.push(`Keep every accent and mark exactly: ${marks.join(" ")}.`);
|
|
14641
14693
|
return notes;
|
|
14642
14694
|
}
|
|
14643
|
-
var
|
|
14695
|
+
var BUTTON_ROLES = /* @__PURE__ */ new Set(["cta", "button", "call_to_action"]);
|
|
14696
|
+
var isButton = (l) => BUTTON_ROLES.has(roleKey(l.role)) || /button/i.test(l.look ?? "");
|
|
14644
14697
|
var FIGURE_ROLES = /* @__PURE__ */ new Set(["price", "stat", "figure", "number", "discount"]);
|
|
14645
14698
|
var HEADLINE_ROLES = /* @__PURE__ */ new Set(["headline", "title"]);
|
|
14646
14699
|
var FINE_PRINT_ROLES = /* @__PURE__ */ new Set(["fine_print", "legal", "disclaimer", "footnote"]);
|
|
@@ -14651,7 +14704,7 @@ function fullHierarchy(ranked, lead) {
|
|
|
14651
14704
|
const { designed, figure, headline, button } = ranked;
|
|
14652
14705
|
const size = lead === figure ? "very large" : "at least half the frame wide";
|
|
14653
14706
|
return [
|
|
14654
|
-
...lead ? [`line ${lead.n} largest (${size}), read first
|
|
14707
|
+
...lead ? [`line ${lead.n} largest (${size}), read first`] : [],
|
|
14655
14708
|
...lead && figure && headline ? [`then line ${headline.n}`] : [],
|
|
14656
14709
|
...button ? [`line ${button.n} ${BUTTON_SIZE}`] : [],
|
|
14657
14710
|
...designed.length > 1 ? ["every line readable at a glance"] : [],
|
|
@@ -14699,11 +14752,11 @@ function brandRoleColours(spec, brand) {
|
|
|
14699
14752
|
return roles.flatMap(([applies, object, hex]) => applies && hex ? [{ object, name: colourName(hex), hex }] : []);
|
|
14700
14753
|
}
|
|
14701
14754
|
var NATIVE_CAPTURE = /\b(selfie|phone[- ]camera|phone photo|shot on (?:a )?phone|taken on (?:a )?phone|smartphone photo|ugc|user[- ]generated|webcam|front[- ]camera|snapshot|native caption)\b/i;
|
|
14702
|
-
var
|
|
14755
|
+
var PHONE_CAMERA2 = /\b(phone|smartphone|iphone)\b/i;
|
|
14703
14756
|
function isNativeCapture(spec) {
|
|
14704
14757
|
if (spec.scene.look !== "photo") return false;
|
|
14705
14758
|
const capture = spec.scene.capture ?? "";
|
|
14706
|
-
return NATIVE_CAPTURE.test(`${spec.scene.description} ${capture}`) ||
|
|
14759
|
+
return NATIVE_CAPTURE.test(`${spec.scene.description} ${capture}`) || PHONE_CAMERA2.test(capture);
|
|
14707
14760
|
}
|
|
14708
14761
|
function colourUses(spec, brand, k) {
|
|
14709
14762
|
const allowed = brand ? new Set(
|
|
@@ -14747,10 +14800,10 @@ function variantDirection(c) {
|
|
|
14747
14800
|
}
|
|
14748
14801
|
var DEPARTS = "It leads where it differs from the scene and line notes above.";
|
|
14749
14802
|
var DEPARTS_SHORT = "It leads over the notes above.";
|
|
14750
|
-
var
|
|
14803
|
+
var isSupport = (l) => isButton(l) || FINE_PRINT_ROLES.has(roleKey(l.role)) || roleKey(l.role) === "attribution";
|
|
14751
14804
|
function hookDirection(c) {
|
|
14752
14805
|
const hooks = c.spec.copy.flatMap(
|
|
14753
|
-
(l, i) => l.lettering === "designed" && !
|
|
14806
|
+
(l, i) => l.lettering === "designed" && !isSupport(l) ? [{ n: i + 1, text: l.text }] : []
|
|
14754
14807
|
);
|
|
14755
14808
|
const [first, ...rest] = hooks;
|
|
14756
14809
|
const lead = rest.find((l) => /\d/.test(l.text)) ?? rest[0];
|
|
@@ -14806,7 +14859,7 @@ function pictureDirection(c) {
|
|
|
14806
14859
|
];
|
|
14807
14860
|
}
|
|
14808
14861
|
function compileStaticBrief(written, brandOverride, format, viewerOverride, profile, opts = {}) {
|
|
14809
|
-
const spec =
|
|
14862
|
+
const spec = withRenderedLettering(written);
|
|
14810
14863
|
const job = opts.job ?? "generate";
|
|
14811
14864
|
const designedLines2 = spec.copy.flatMap((l, i) => l.lettering === "designed" ? [i + 1] : []);
|
|
14812
14865
|
const pics = pictures(spec, opts, job, profile, designedLines2.length > 0);
|
|
@@ -14844,7 +14897,9 @@ function compileStaticBrief(written, brandOverride, format, viewerOverride, prof
|
|
|
14844
14897
|
words: wordCount(prompt)
|
|
14845
14898
|
};
|
|
14846
14899
|
}
|
|
14847
|
-
var MAX_TRIM =
|
|
14900
|
+
var MAX_TRIM = 16;
|
|
14901
|
+
var LAST_RESORT_LEVEL = 16;
|
|
14902
|
+
var PRODUCT_NOTE_WORDS = 10;
|
|
14848
14903
|
var EXTRAS_SHORT_LEVEL = 15;
|
|
14849
14904
|
var CAPTURE_LEVEL = 9;
|
|
14850
14905
|
var SHORT_DIRECTION_LEVEL = 10;
|
|
@@ -14919,6 +14974,7 @@ function picturesSection(c) {
|
|
|
14919
14974
|
plates: plateColours(c.spec, c.brand, c.clean),
|
|
14920
14975
|
thirdParty: c.spec.third_party_brands,
|
|
14921
14976
|
flowing: c.flowing,
|
|
14977
|
+
short: c.level >= LAST_RESORT_LEVEL,
|
|
14922
14978
|
...c.spec.logo_position ? { logoPosition: c.spec.logo_position } : {}
|
|
14923
14979
|
};
|
|
14924
14980
|
const lines = c.pics.map((p) => {
|
|
@@ -14945,8 +15001,12 @@ function photoLook(c) {
|
|
|
14945
15001
|
const purpose = r.purpose ? `, ${r.purpose}` : "";
|
|
14946
15002
|
return `It is ${r.photo}${capture ? `, ${capture}` : ""}, with ${r.texture}${purpose}.`;
|
|
14947
15003
|
}
|
|
15004
|
+
function animalsText(c) {
|
|
15005
|
+
if (!showsAnimals(c.spec) || c.level >= EXTRAS_SHORT_LEVEL) return "";
|
|
15006
|
+
return c.flowing ? " The animals look real, with real fur, natural light and a candid moment." : " Animals look real: real fur, natural light, a candid moment.";
|
|
15007
|
+
}
|
|
14948
15008
|
function lookText(c) {
|
|
14949
|
-
const people = showsPeople(c.spec) ? peopleText(c) : ""
|
|
15009
|
+
const people = `${showsPeople(c.spec) ? peopleText(c) : ""}${animalsText(c)}`;
|
|
14950
15010
|
if (c.spec.scene.look === "photo") return `${photoLook(c)}${people}`;
|
|
14951
15011
|
const look = c.spec.scene.look === "illustration" ? ILLUSTRATION_LOOK : DESIGNED_LOOK;
|
|
14952
15012
|
return `${c.flowing ? look.flowing : look.labelled}${people}`;
|
|
@@ -14965,27 +15025,66 @@ function peopleText(c) {
|
|
|
14965
15025
|
const said = c.level >= EXTRAS_SHORT_LEVEL ? "People look real, faces well lit." : sentence(capitalize(cue));
|
|
14966
15026
|
return ` ${said}${target2 ? ` Their gaze or pose leads toward line ${target2.n}.` : ""}`;
|
|
14967
15027
|
}
|
|
15028
|
+
var looksRendered = (spec) => showsPeople(spec) || showsAnimals(spec) || showsScreen(spec);
|
|
15029
|
+
var ANIMALS = /\b(dogs?|puppy|puppies|cats?|kittens?|pets?|horses?|birds?|rabbits?|animals?)\b/i;
|
|
15030
|
+
function showsAnimals(spec) {
|
|
15031
|
+
if (spec.scene.look === "illustration") return false;
|
|
15032
|
+
if (spec.references.some((r) => r.id.startsWith("ANIMAL_"))) return true;
|
|
15033
|
+
return spec.scene.look === "photo" && ANIMALS.test(`${spec.scene.description} ${spec.scene.layout ?? ""}`);
|
|
15034
|
+
}
|
|
14968
15035
|
var SCREEN = /\b(apps?|screens?|lock[- ]screen|dashboard|interface|mockup|notification|laptop|tablet|browser|website|chat bubbles?|ui)\b/i;
|
|
14969
15036
|
function showsScreen(spec) {
|
|
14970
15037
|
const ons = spec.copy.map((l) => l.on ?? "").join(" ");
|
|
14971
15038
|
return SCREEN.test(`${spec.scene.description} ${spec.scene.layout ?? ""} ${ons}`);
|
|
14972
15039
|
}
|
|
14973
15040
|
var SCREENS = {
|
|
14974
|
-
labelled: "Screens:
|
|
14975
|
-
flowing: "Every phone or app screen
|
|
15041
|
+
labelled: "Screens: a real phone photographed, or a real screenshot: real app density, a real status bar, flat true-to-life colours; any words on them from the quoted lines.",
|
|
15042
|
+
flowing: "Every phone or app screen looks like a real phone photographed or a real screenshot, with real app density, a real status bar and flat true-to-life colours, any words on it from the quoted lines."
|
|
14976
15043
|
};
|
|
15044
|
+
var SCREENSHOT = /\b(screenshots?|screen ?grabs?|screens?)\b/i;
|
|
15045
|
+
var screenshotOf = (pics) => pics.find((p) => p.spec && p.role !== "identity" && SCREENSHOT.test(p.spec.what));
|
|
14977
15046
|
function screensText(c) {
|
|
15047
|
+
const shot = screenshotOf(c.pics);
|
|
15048
|
+
if (shot) {
|
|
15049
|
+
return c.flowing ? ` Every screen shows Image ${shot.index} exactly as it is, on a real phone, photographed.` : `
|
|
15050
|
+
Screens: every screen shows Image ${shot.index} exactly as it is, on a real phone, photographed.`;
|
|
15051
|
+
}
|
|
14978
15052
|
if (!showsScreen(c.spec) || c.level >= EXTRAS_SHORT_LEVEL) return "";
|
|
14979
15053
|
return c.flowing ? ` ${SCREENS.flowing}` : `
|
|
14980
15054
|
${SCREENS.labelled}`;
|
|
14981
15055
|
}
|
|
15056
|
+
function unwiredProductsText(c) {
|
|
15057
|
+
const wired = new Set(c.pics.map((p) => p.label));
|
|
15058
|
+
return c.spec.references.filter((r) => r.role === "product" && !wired.has(r.id)).map((r) => {
|
|
15059
|
+
const what = plain(r.what, c.clean).replace(/[.!?]+$/, "");
|
|
15060
|
+
const told = c.level >= LAST_RESORT_LEVEL ? clipNote(what.split(/[,;]/)[0] ?? what, PRODUCT_NOTE_WORDS) : what;
|
|
15061
|
+
return ` The product, ${told}, is in the frame, whole and clearly visible.`;
|
|
15062
|
+
}).join("");
|
|
15063
|
+
}
|
|
15064
|
+
function unwiredPeopleText(c) {
|
|
15065
|
+
const wired = new Set(c.pics.map((p) => p.label));
|
|
15066
|
+
return c.spec.references.filter((r) => r.role === "identity" && r.id.startsWith("PERSON_") && !wired.has(r.id)).map((r) => {
|
|
15067
|
+
const what = plain(r.what, c.clean).replace(/[.!?]+$/, "");
|
|
15068
|
+
const told = c.level >= LAST_RESORT_LEVEL ? clipNote(what.split(/[,;]/)[0] ?? what, PRODUCT_NOTE_WORDS) : what;
|
|
15069
|
+
return ` The person, ${told}, is an invented, ordinary real person \u2014 real skin, unposed, never a model.`;
|
|
15070
|
+
}).join("");
|
|
15071
|
+
}
|
|
15072
|
+
var BEFORE_AFTER = /\bbefore\b[^.]{0,40}\bafter\b|\bantes\b[^.]{0,30}\bdespu[eé]s\b|\bavant\b[^.]{0,30}\bapr[eè]s\b|\bvorher\b[^.]{0,30}\bnachher\b|\bprima\b[^.]{0,30}\bdopo\b/i;
|
|
15073
|
+
function isBeforeAfter(spec) {
|
|
15074
|
+
const told = [spec.scene.description, spec.scene.layout, spec.direction, ...spec.mechanism.map((m) => m.device)];
|
|
15075
|
+
return BEFORE_AFTER.test(told.filter(Boolean).join(" \n "));
|
|
15076
|
+
}
|
|
15077
|
+
var BEFORE_AFTER_TEXT = " The two states it compares differ clearly at a glance, even as a thumbnail; only the result changes between them.";
|
|
14982
15078
|
function sceneSection(c) {
|
|
14983
15079
|
if (c.job !== "generate") return [];
|
|
14984
15080
|
const scene = c.spec.scene;
|
|
14985
|
-
const
|
|
15081
|
+
const products = `${unwiredProductsText(c)}${unwiredPeopleText(c)}${isBeforeAfter(c.spec) ? BEFORE_AFTER_TEXT : ""}`;
|
|
15082
|
+
const told = normalized(products);
|
|
15083
|
+
const own = (sentence(plain(scene.description, c.clean)).match(/[^.!?]+[.!?]*/g) ?? []).filter((s) => !products || !told.includes(normalized(s.replace(/^\s*in the frame:?/i, "")))).join("").trim();
|
|
15084
|
+
const description = capitalize(clip(own || sentence(plain(scene.description, c.clean)), SCENE_CAP[c.level] ?? 15));
|
|
14986
15085
|
const mood = scene.mood && c.level < 1 ? ` The mood is ${sentence(plain(scene.mood, c.clean))}` : "";
|
|
14987
15086
|
const out = [
|
|
14988
|
-
c.flowing ? `${description}${mood} ${lookText(c)}${screensText(c)}${otherBrandsText(c)}` : `Scene: ${description}${mood}
|
|
15087
|
+
c.flowing ? `${description}${products}${mood} ${lookText(c)}${screensText(c)}${otherBrandsText(c)}` : `Scene: ${description}${products}${mood}
|
|
14989
15088
|
${lookText(c)}${screensText(c)}${otherBrandsText(c)}`
|
|
14990
15089
|
];
|
|
14991
15090
|
const layout = scene.layout && c.level < 2 ? unsaid(plain(scene.layout, c.clean), plain(scene.description, c.clean)) : "";
|
|
@@ -15009,7 +15108,11 @@ function textSection(c) {
|
|
|
15009
15108
|
...textNotes(copy, c.profile)
|
|
15010
15109
|
].join("\n");
|
|
15011
15110
|
}
|
|
15012
|
-
|
|
15111
|
+
function productLabel(c) {
|
|
15112
|
+
const shot = screenshotOf(c.pics);
|
|
15113
|
+
if (shot) return ` besides the words on Image ${shot.index}'s screen`;
|
|
15114
|
+
return c.pics.some((p) => p.role === "product") ? " besides the product's own label" : "";
|
|
15115
|
+
}
|
|
15013
15116
|
function colourSection(c) {
|
|
15014
15117
|
if (c.job !== "generate") return [];
|
|
15015
15118
|
const uses = colourUses(c.spec, c.brand, c.clean);
|
|
@@ -15022,7 +15125,7 @@ function layoutSection(c) {
|
|
|
15022
15125
|
c.pics.some((p) => p.role === "logo"),
|
|
15023
15126
|
c.spec.copy.some(isButton)
|
|
15024
15127
|
);
|
|
15025
|
-
const safe = c.opts.band?.margin ? marginText(c.opts.band, items, c.profile.brief.structure) : c.opts.band ? bandText(c.opts.band, items, c.flowing, staticSafeArea(c.format, c.opts.placements).placement !== "feed") : safeAreaText(c.format, c.opts.placements, items, c.profile.brief.structure);
|
|
15128
|
+
const safe = c.opts.zoned ? "" : c.opts.band?.margin ? marginText(c.opts.band, items, c.profile.brief.structure) : c.opts.band ? bandText(c.opts.band, items, c.flowing, staticSafeArea(c.format, c.opts.placements).placement !== "feed") : safeAreaText(c.format, c.opts.placements, items, c.profile.brief.structure);
|
|
15026
15129
|
const out = [safe, legibilityText(c), fillText(c)].filter(Boolean).join(" ");
|
|
15027
15130
|
return c.flowing ? out : `Layout: ${out}`;
|
|
15028
15131
|
}
|
|
@@ -15052,8 +15155,8 @@ function positiveConstraints(c) {
|
|
|
15052
15155
|
const others = thirdPartyNames(c.spec);
|
|
15053
15156
|
if (n === 0)
|
|
15054
15157
|
return `The only words in the finished image are none at all: it is picture only${logo ? `, with the one logo from Image ${logo.index}` : ""}.`;
|
|
15055
|
-
const label = productLabel(c) ?
|
|
15056
|
-
const onlyLogo = !logo ? "" : others ? `, and the logos are the one in Image ${logo.index} and ${others}` : `, and the one logo is the one in Image ${logo.index}`;
|
|
15158
|
+
const label = productLabel(c) ? `,${productLabel(c)}` : "";
|
|
15159
|
+
const onlyLogo = !logo ? "" : others ? `, and the logos are the one in Image ${logo.index} and ${others}, the only brand marks anywhere, vehicles, packaging and clothing included` : `, and the one logo is the one in Image ${logo.index}, the only brand mark anywhere, vehicles, packaging and clothing included`;
|
|
15057
15160
|
return `The only words in the finished image are the ${n} quoted lines above, each exactly once; every other surface carries picture only${label}${onlyLogo}.`;
|
|
15058
15161
|
}
|
|
15059
15162
|
function constraintsSection(c) {
|
|
@@ -15066,12 +15169,13 @@ function constraintsSection(c) {
|
|
|
15066
15169
|
];
|
|
15067
15170
|
const avoid = [
|
|
15068
15171
|
n > 0 ? `any words other than the ${n} quoted lines` : "any words or letters",
|
|
15069
|
-
|
|
15172
|
+
// A Toyota badge appeared on a car-loan ad's SUV (BENCH.md § "Final"): the allow-list covers the scene's things too.
|
|
15070
15173
|
// The words and the identity's "show them once" are said above; brief v2 spends the words on the logo and the button.
|
|
15071
|
-
|
|
15174
|
+
`${logos.length > 0 ? `any logo, badge, icon, seal or sticker other than ${logos.join(" and ")}` : "any logo or wordmark, badge, icon, seal or sticker"}, on vehicles, packaging and clothing too`,
|
|
15072
15175
|
"watermarks",
|
|
15073
15176
|
...c.job === "generate" && isNativeCapture(c.spec) ? ["a staged, retouched or studio-lit look"] : [],
|
|
15074
|
-
|
|
15177
|
+
// The user found the people, a dog and a phone screen "very AI" (BENCH.md § "Fix round").
|
|
15178
|
+
...c.job !== "generate" ? [] : showsPeople(c.spec) ? ["glossy, CGI or airbrushed skin; faces in shadow"] : looksRendered(c.spec) ? ["a glossy CGI look"] : []
|
|
15075
15179
|
];
|
|
15076
15180
|
return `Avoid: ${avoid.join("; ")}.`;
|
|
15077
15181
|
}
|
|
@@ -15108,6 +15212,9 @@ function judgeBrief(brief, opts = {}) {
|
|
|
15108
15212
|
brief
|
|
15109
15213
|
].join("\n");
|
|
15110
15214
|
}
|
|
15215
|
+
var NUMBERED_COPY = /^(\d+\. )"(.*)"( — )/gm;
|
|
15216
|
+
var withoutCopy = (prompt) => prompt.replace(NUMBERED_COPY, '$1""$3');
|
|
15217
|
+
var briefWords = (prompt) => wordCount(withoutCopy(prompt));
|
|
15111
15218
|
|
|
15112
15219
|
// src/engine/scaffold/static-sizes.ts
|
|
15113
15220
|
function ratioValue2(ratio) {
|
|
@@ -15271,24 +15378,26 @@ function logoSlot(frame, safe, opts = {}) {
|
|
|
15271
15378
|
var STATIC_ADAPT_FORMAT_ROLE = "static_adapt_format";
|
|
15272
15379
|
function adaptPlan(model, format, placements) {
|
|
15273
15380
|
const renderRatio = nearestSupportedAspectRatio("image_generate", model, format);
|
|
15381
|
+
return {
|
|
15382
|
+
format,
|
|
15383
|
+
...placements && placements.length > 0 ? { placements: [...placements] } : {},
|
|
15384
|
+
renderRatio,
|
|
15385
|
+
size: deliveredSize(format),
|
|
15386
|
+
safe: safeAreaOnFrame(renderRatio, format, placements)
|
|
15387
|
+
};
|
|
15388
|
+
}
|
|
15389
|
+
function safeAreaOnFrame(frame, format, placements) {
|
|
15274
15390
|
const own = staticSafeArea(format, placements);
|
|
15275
|
-
const rendered = ratioValue2(
|
|
15391
|
+
const rendered = ratioValue2(frame);
|
|
15276
15392
|
const delivered = ratioValue2(format);
|
|
15277
15393
|
const keepH = rendered < delivered ? rendered / delivered : 1;
|
|
15278
15394
|
const keepW = rendered > delivered ? delivered / rendered : 1;
|
|
15279
|
-
|
|
15395
|
+
return {
|
|
15280
15396
|
top: (1 - keepH) / 2 + own.top * keepH,
|
|
15281
15397
|
bottom: (1 - keepH) / 2 + own.bottom * keepH,
|
|
15282
15398
|
left: (1 - keepW) / 2 + own.left * keepW,
|
|
15283
15399
|
right: (1 - keepW) / 2 + own.right * keepW
|
|
15284
15400
|
};
|
|
15285
|
-
return {
|
|
15286
|
-
format,
|
|
15287
|
-
...placements && placements.length > 0 ? { placements: [...placements] } : {},
|
|
15288
|
-
renderRatio,
|
|
15289
|
-
size: deliveredSize(format),
|
|
15290
|
-
safe
|
|
15291
|
-
};
|
|
15292
15401
|
}
|
|
15293
15402
|
function trimmedRenderBand(plan) {
|
|
15294
15403
|
const own = staticSafeArea(plan.format, plan.placements);
|
|
@@ -15322,65 +15431,119 @@ function adaptNoun(plan) {
|
|
|
15322
15431
|
if (r === 1) return "square feed ad";
|
|
15323
15432
|
return r < 1 ? "vertical feed ad" : "landscape ad";
|
|
15324
15433
|
}
|
|
15325
|
-
function adaptDeliverable(plan,
|
|
15434
|
+
function adaptDeliverable(plan, spec, flowing, ground, short) {
|
|
15435
|
+
const brand = spec.brand?.name;
|
|
15326
15436
|
const noun = `${adaptNoun(plan)}${brand ? ` for ${brand}` : ""}`;
|
|
15327
|
-
|
|
15328
|
-
|
|
15329
|
-
|
|
15330
|
-
|
|
15437
|
+
if (short && !flowing) {
|
|
15438
|
+
const keep = [
|
|
15439
|
+
...showsPeople2(spec) ? ["each person the same face and clothes"] : [],
|
|
15440
|
+
...isBeforeAfter(spec) ? ["the two states as different"] : []
|
|
15441
|
+
];
|
|
15442
|
+
return `Deliverable: the ad in Image 1, re-laid out as a finished ${noun}: the same message, words, people, product, logo, colours and typeface, the scene filling the frame edge to edge, never Image 1 shrunk onto a background. Every person, product and line appears whole and once, as in Image 1${keep.length > 0 ? `, ${keep.join(", ")}` : ""}; add nothing new.`;
|
|
15443
|
+
}
|
|
15444
|
+
const cast = [
|
|
15445
|
+
SAME_CAST,
|
|
15446
|
+
...showsPeople2(spec) ? [SAME_PERSON] : [],
|
|
15447
|
+
...isBeforeAfter(spec) ? [SAME_CHANGE] : [],
|
|
15448
|
+
...ground ? [GROUND_ONLY] : []
|
|
15449
|
+
].join(" ");
|
|
15450
|
+
return flowing ? `Image 1 is a finished ad. Re-lay out this exact ad as a ${noun}: the same message, words, people, product, logo, colours and typeface in a new layout for this frame, the scene filling it edge to edge rather than Image 1 set on a background as a card, panel or square. ${cast}` : `Deliverable: the ad in Image 1, re-laid out as a finished ${noun}: the same message, words, people, product, logo, colours and typeface in a new layout for this frame, the scene filling it edge to edge, never Image 1 shrunk onto a background or set as a card, panel or square. ${cast}`;
|
|
15451
|
+
}
|
|
15452
|
+
var SAME_CAST = "Every person, product and line appears whole and once, the same individuals as in Image 1; add no person, animal, product or object that Image 1 does not show.";
|
|
15453
|
+
var SAME_PERSON = "Each person is the same person as in Image 1: same face, same hair, same clothes.";
|
|
15454
|
+
var SAME_CHANGE = "The two states it compares stay as clearly different as in Image 1.";
|
|
15455
|
+
var showsPeople2 = (spec) => spec.references.some((r) => r.id.startsWith("PERSON_")) || spec.scene.look !== "illustration" && PEOPLE.test(`${spec.scene.description} ${spec.scene.layout ?? ""}`);
|
|
15331
15456
|
var GROUND_ONLY = "Image 1 shows no people and no product, so the room this frame adds carries more of Image 1's own background and the brand's colours, never a new scene.";
|
|
15332
15457
|
function wordsOnly(spec) {
|
|
15333
15458
|
return spec.references.length === 0 && spec.scene.look !== "photo" && !PEOPLE.test(`${spec.scene.description} ${spec.scene.layout ?? ""}`);
|
|
15334
15459
|
}
|
|
15335
|
-
|
|
15336
|
-
|
|
15337
|
-
|
|
15338
|
-
|
|
15460
|
+
var isTall = (plan) => ratioValue2(plan.format) < 0.7;
|
|
15461
|
+
var LIST_ROLES = /^(list_item|bullet|row|item)$/i;
|
|
15462
|
+
var TABLE = /\b(tables?|columns?|rows?|checklists?|lists?)\b/i;
|
|
15463
|
+
var hasRows = (spec) => spec.copy.some((l) => LIST_ROLES.test(l.role ?? "")) || TABLE.test(`${spec.scene.description} ${spec.scene.layout ?? ""}`);
|
|
15464
|
+
function scaleText(plan, spec, flowing, zoned, short) {
|
|
15465
|
+
if (short && !flowing) {
|
|
15466
|
+
return isTall(plan) && zoned ? "Scale: larger than in Image 1, the headline across most of the band's width, the words spread through the band." : "Scale: larger than in Image 1, the largest line across most of the width.";
|
|
15467
|
+
}
|
|
15468
|
+
if (!(isTall(plan) && zoned)) {
|
|
15469
|
+
return flowing ? "Every line is set at the size this frame needs rather than at its size in Image 1: the largest line spans most of the width between the margins, and the words fill their space." : "Scale: set every line for this frame, not at its size in Image 1: the largest line spans most of the width between the margins; no small text in a large empty area.";
|
|
15470
|
+
}
|
|
15471
|
+
const rows = hasRows(spec) ? flowing ? " Rows, columns and lists are restacked for the tall frame, one item per line, each larger." : " Rows, columns and lists are restacked for the tall frame, one item per line, each larger, rather than shrunk." : "";
|
|
15472
|
+
return flowing ? `The words are set for this frame, larger than in Image 1: the headline large enough to fill most of the band's width, and every line, the logo and the button spread through the band's height, inside its edges.${rows}` : `Scale: larger than in Image 1: the headline set large enough to fill most of the band's width; every line, the logo and the button spread through the band's height, inside its edges, never a small block in its middle.${rows}`;
|
|
15339
15473
|
}
|
|
15340
15474
|
var ZONE_HEADROOM = 0.02;
|
|
15341
15475
|
var pct = (share) => `${Math.round(share * 100)}%`;
|
|
15342
|
-
function zonesText(plan, flowing,
|
|
15476
|
+
function zonesText(plan, flowing, ground, short) {
|
|
15343
15477
|
const top = plan.safe.top + ZONE_HEADROOM;
|
|
15344
15478
|
const bottom = plan.safe.bottom + ZONE_HEADROOM;
|
|
15345
|
-
const
|
|
15479
|
+
const side = Math.max(plan.safe.left, plan.safe.right) + ZONE_HEADROOM;
|
|
15480
|
+
const band = `from ${pct(top)} to ${pct(1 - bottom)} of the height, at least ${pct(side)} in from each side`;
|
|
15346
15481
|
const covered = staticSafeArea(plan.format, plan.placements).placement !== "feed";
|
|
15347
|
-
|
|
15348
|
-
|
|
15349
|
-
|
|
15350
|
-
|
|
15482
|
+
if (short && !flowing) {
|
|
15483
|
+
const around = isTall(plan) && covered ? "only the ad's own picture, larger, never a flat colour, a panel or a square" : "only the ad's own picture";
|
|
15484
|
+
return `Zones: every word, the logo and the button sit ${band}; around them, ${around}.`;
|
|
15485
|
+
}
|
|
15486
|
+
if (!(isTall(plan) && covered)) {
|
|
15487
|
+
return flowing ? `The logo, every line and the button sit ${band}; around them the frame shows only the ad's own picture.` : `Zones: the logo, every line and the button sit ${band}; around them, only the ad's own picture.`;
|
|
15488
|
+
}
|
|
15489
|
+
const picture = ground ? "Image 1's background with its shapes and graphics enlarged in the brand's colours" : "Image 1's people, product and setting extended";
|
|
15490
|
+
return flowing ? `The words' band runs ${band}, holding the logo, every line and the button. Above and below it the app's interface covers the frame, so there it shows only the ad's own picture, larger: ${picture}, edge to edge.` : `Zones: the logo, every line and the button sit in one band, ${band}; the top ${pct(top)} and the bottom ${pct(bottom)} carry neither words, the logo nor the button. Above and below it, where the app's interface covers the frame, only the ad's own picture, larger: ${picture}, never a flat empty colour, a panel or a square on a background.`;
|
|
15351
15491
|
}
|
|
15352
|
-
var isButton2 = (line) => /cta|button/i.test(`${line.role ?? ""} ${line.look ?? ""}`);
|
|
15353
15492
|
function adaptBrief(spec, profile, opts) {
|
|
15354
15493
|
const { plan, images } = opts;
|
|
15355
15494
|
const flowing = profile.brief.structure === "flowing_sentences";
|
|
15356
15495
|
const { logo_position: _logoPosition, ...rest } = spec;
|
|
15357
15496
|
const unplaced = { ...rest, copy: spec.copy.map(({ position: _position, ...line }) => line) };
|
|
15358
15497
|
const band = trimmedRenderBand(plan);
|
|
15359
|
-
const
|
|
15360
|
-
job: "relay",
|
|
15361
|
-
images: {
|
|
15362
|
-
base: images.base,
|
|
15363
|
-
...images.logo ? { logo: images.logo } : {},
|
|
15364
|
-
...images.type ? { type: images.type } : {}
|
|
15365
|
-
},
|
|
15366
|
-
...plan.placements ? { placements: plan.placements } : {},
|
|
15367
|
-
...band ? { band } : {}
|
|
15368
|
-
});
|
|
15369
|
-
const paragraphs = compiled.prompt.split("\n\n");
|
|
15498
|
+
const zoned = opts.zones !== false;
|
|
15370
15499
|
const ground = wordsOnly(spec);
|
|
15371
|
-
|
|
15372
|
-
|
|
15373
|
-
|
|
15374
|
-
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
|
|
15379
|
-
|
|
15500
|
+
const compile = (wordBudget) => compileStaticBrief(
|
|
15501
|
+
unplaced,
|
|
15502
|
+
void 0,
|
|
15503
|
+
plan.format,
|
|
15504
|
+
void 0,
|
|
15505
|
+
{ ...profile, brief: { ...profile.brief, wordBudget } },
|
|
15506
|
+
{
|
|
15507
|
+
job: "relay",
|
|
15508
|
+
images: {
|
|
15509
|
+
base: images.base,
|
|
15510
|
+
...images.logo ? { logo: images.logo } : {},
|
|
15511
|
+
...images.type ? { type: images.type } : {},
|
|
15512
|
+
refs: images.refs ?? {}
|
|
15513
|
+
},
|
|
15514
|
+
...plan.placements ? { placements: plan.placements } : {},
|
|
15515
|
+
...band ? { band } : {},
|
|
15516
|
+
...zoned ? { zoned } : {}
|
|
15517
|
+
}
|
|
15518
|
+
);
|
|
15519
|
+
const relaid = (compiled2, short = false) => {
|
|
15520
|
+
const paragraphs = compiled2.prompt.split("\n\n");
|
|
15521
|
+
paragraphs[0] = adaptDeliverable(plan, spec, flowing, ground && !zoned, short);
|
|
15522
|
+
const first = spec.copy[0] ? quotedCopy(spec.copy[0].text) : void 0;
|
|
15523
|
+
const textAt = first ? paragraphs.findIndex((p) => p.includes(first)) : -1;
|
|
15524
|
+
if (textAt >= 0) {
|
|
15525
|
+
const added = [
|
|
15526
|
+
...zoned ? [zonesText(plan, flowing, ground, short)] : [],
|
|
15527
|
+
scaleText(plan, spec, flowing, zoned, short)
|
|
15528
|
+
];
|
|
15529
|
+
paragraphs.splice(textAt + 1, 0, ...added);
|
|
15530
|
+
} else if (zoned) {
|
|
15531
|
+
const layoutAt = paragraphs.findIndex((p) => p.startsWith("Layout:"));
|
|
15532
|
+
paragraphs.splice(layoutAt > 0 ? layoutAt : 1, 0, zonesText(plan, flowing, ground, short));
|
|
15533
|
+
}
|
|
15534
|
+
return paragraphs.join("\n\n");
|
|
15535
|
+
};
|
|
15536
|
+
const budget = profile.brief.wordBudget;
|
|
15537
|
+
let compiled = compile(budget);
|
|
15538
|
+
let prompt = relaid(compiled);
|
|
15539
|
+
for (let pass = 0; pass < 3 && briefWords(prompt) > budget; pass++) {
|
|
15540
|
+
compiled = compile(compiled.words - (briefWords(prompt) - budget));
|
|
15541
|
+
prompt = relaid(compiled);
|
|
15380
15542
|
}
|
|
15381
|
-
|
|
15382
|
-
return { ...compiled, prompt, words: prompt
|
|
15543
|
+
if (briefWords(prompt) > budget) prompt = relaid(compiled, true);
|
|
15544
|
+
return { ...compiled, prompt, words: wordsIn(prompt) };
|
|
15383
15545
|
}
|
|
15546
|
+
var wordsIn = (text) => text.split(/\s+/).filter(Boolean).length;
|
|
15384
15547
|
|
|
15385
15548
|
// src/engine/scaffold/static-topology.ts
|
|
15386
15549
|
var STATIC_VARIANTS_ROLE = "static_variants";
|
|
@@ -15390,6 +15553,7 @@ var STATIC_PICK_ROLE = "pick";
|
|
|
15390
15553
|
var STATIC_MASTER_ROLE = "static_master";
|
|
15391
15554
|
var STATIC_SIZE_ROLE = "static_size";
|
|
15392
15555
|
var STATIC_ADAPT_ROLE = "static_adapt";
|
|
15556
|
+
var STATIC_SAFE_FIT_ROLE = "static_safe_fit";
|
|
15393
15557
|
var LOGO_COMPOSITE_ROLE = "logo_composite";
|
|
15394
15558
|
var STATIC_FIX_ROLE = "static_fix";
|
|
15395
15559
|
var LOGO_COMPOSITE_DEFAULT = false;
|
|
@@ -15423,8 +15587,9 @@ function staticMasterFormat(canvas) {
|
|
|
15423
15587
|
if (at2 < 0 || !match) return null;
|
|
15424
15588
|
return formatOfSize({ width: Number(match[1]), height: Number(match[2]) });
|
|
15425
15589
|
}
|
|
15590
|
+
var KNOWN_FORMATS = ["9:16", "4:5", "1:1", "16:9", "1.91:1", ...ASPECT_ADAPT_FORMATS];
|
|
15426
15591
|
function formatOfSize(size) {
|
|
15427
|
-
for (const format of
|
|
15592
|
+
for (const format of KNOWN_FORMATS) {
|
|
15428
15593
|
const d = deliveredSize(format);
|
|
15429
15594
|
if (d.width === size.width && d.height === size.height) return format;
|
|
15430
15595
|
}
|
|
@@ -15432,9 +15597,17 @@ function formatOfSize(size) {
|
|
|
15432
15597
|
const g = gcd(size.width, size.height);
|
|
15433
15598
|
return `${size.width / g}:${size.height / g}`;
|
|
15434
15599
|
}
|
|
15435
|
-
function safeAreaOf(
|
|
15436
|
-
const { top, bottom, left, right } = staticSafeArea(
|
|
15437
|
-
return
|
|
15600
|
+
function safeAreaOf(adaptFormat, delivered) {
|
|
15601
|
+
const { top, bottom, left, right } = staticSafeArea(adaptFormat);
|
|
15602
|
+
return delivered.map((f) => safeAreaOnFrame(adaptFormat, f)).reduce(
|
|
15603
|
+
(a, b) => ({
|
|
15604
|
+
top: Math.max(a.top, b.top),
|
|
15605
|
+
bottom: Math.max(a.bottom, b.bottom),
|
|
15606
|
+
left: Math.max(a.left, b.left),
|
|
15607
|
+
right: Math.max(a.right, b.right)
|
|
15608
|
+
}),
|
|
15609
|
+
{ top, bottom, left, right }
|
|
15610
|
+
);
|
|
15438
15611
|
}
|
|
15439
15612
|
function magick(id, role, image, args, extra = {}) {
|
|
15440
15613
|
return {
|
|
@@ -15503,7 +15676,15 @@ function staticRenderNodes(g) {
|
|
|
15503
15676
|
model: g.adaptModel,
|
|
15504
15677
|
formats: adaptFormats,
|
|
15505
15678
|
image_size: ADAPT_IMAGE_SIZE,
|
|
15506
|
-
safe_areas: Object.fromEntries(
|
|
15679
|
+
safe_areas: Object.fromEntries(
|
|
15680
|
+
adaptFormats.map((f) => [
|
|
15681
|
+
f,
|
|
15682
|
+
safeAreaOf(
|
|
15683
|
+
f,
|
|
15684
|
+
adapted.filter((s) => (s.adaptFormat ?? s.format) === f).map((s) => s.format)
|
|
15685
|
+
)
|
|
15686
|
+
])
|
|
15687
|
+
)
|
|
15507
15688
|
},
|
|
15508
15689
|
tags: { role: STATIC_ADAPT_ROLE }
|
|
15509
15690
|
});
|
|
@@ -15513,7 +15694,9 @@ function staticRenderNodes(g) {
|
|
|
15513
15694
|
const slug = formatSlug(s.format);
|
|
15514
15695
|
const size = sizeNode(s, adaptFormats);
|
|
15515
15696
|
if (size) nodes.push(size);
|
|
15516
|
-
const
|
|
15697
|
+
const fit = safeFitNode(s.format, size ? `$ref:${size.id}.image` : "$ref:master.image", plan.placements);
|
|
15698
|
+
if (fit) nodes.push(fit);
|
|
15699
|
+
const ref = fit ? `$ref:${fit.id}.image` : size ? `$ref:${size.id}.image` : "$ref:master.image";
|
|
15517
15700
|
if (!g.compositeLogo) return ref;
|
|
15518
15701
|
const slot = logoSlot(s.size, staticSafeArea(s.format)).rect;
|
|
15519
15702
|
nodes.push(
|
|
@@ -15548,6 +15731,16 @@ function staticRenderNodes(g) {
|
|
|
15548
15731
|
});
|
|
15549
15732
|
return nodes;
|
|
15550
15733
|
}
|
|
15734
|
+
function safeFitNode(format, image, placements) {
|
|
15735
|
+
if (staticSafeArea(format, placements).placement === "feed") return null;
|
|
15736
|
+
return {
|
|
15737
|
+
id: `safe_${formatSlug(format)}`,
|
|
15738
|
+
type: "static_safe_fit",
|
|
15739
|
+
inputs: { image },
|
|
15740
|
+
params: { format, ...placements ? { placements: [...placements] } : {} },
|
|
15741
|
+
tags: { role: STATIC_SAFE_FIT_ROLE }
|
|
15742
|
+
};
|
|
15743
|
+
}
|
|
15551
15744
|
function sizeNode(s, adaptFormats) {
|
|
15552
15745
|
const id = `size_${formatSlug(s.format)}`;
|
|
15553
15746
|
if (s.from === "crop" && s.crop) {
|
|
@@ -15898,9 +16091,13 @@ function checkReferenceCompleteness(ctx, meta) {
|
|
|
15898
16091
|
const elements = (meta.elements ?? []).filter((el) => CHECKED_ELEMENT_TYPES.has(el.type.toLowerCase()));
|
|
15899
16092
|
const keyworded = elements.map((el) => ({ el, keywords: keywordsForElement(el) })).filter((e) => e.keywords.length > 0);
|
|
15900
16093
|
if (keyworded.length === 0) return;
|
|
16094
|
+
const sheetSources = JSON.stringify(
|
|
16095
|
+
ctx.canvas.nodes.filter((n) => n.type === "image_reference_sheet").map((n) => n.inputs ?? {})
|
|
16096
|
+
);
|
|
15901
16097
|
for (let i = 0; i < ctx.canvas.nodes.length; i++) {
|
|
15902
16098
|
const n = ctx.canvas.nodes[i];
|
|
15903
|
-
if (n?.type
|
|
16099
|
+
if (n?.type !== "image_generate" || sheetSources.includes(`"$ref:${n.id}.`)) continue;
|
|
16100
|
+
checkFrameReferences(ctx, n, i, keyworded);
|
|
15904
16101
|
}
|
|
15905
16102
|
}
|
|
15906
16103
|
function checkUnusableLastFrame(ctx) {
|
|
@@ -17799,9 +17996,12 @@ async function fetchWithEscalation(url, init, options) {
|
|
|
17799
17996
|
let originResponse = null;
|
|
17800
17997
|
let originTier = "direct";
|
|
17801
17998
|
let lastError;
|
|
17802
|
-
|
|
17999
|
+
const routes = plannedRoutes(url, options.credentials, options.routeOptions);
|
|
18000
|
+
const tried = [];
|
|
18001
|
+
for (let route = routes[0] ?? null; route !== null; route = nextPlannedRoute(routes, tried)) {
|
|
17803
18002
|
const rung = await attemptRoute(url, init, route, options);
|
|
17804
18003
|
attempts.push(rung.attempt);
|
|
18004
|
+
tried.push({ tier: rung.attempt.tier, proxyFailure: rung.attempt.outcome === "proxy_unavailable" });
|
|
17805
18005
|
if (rung.response !== null) {
|
|
17806
18006
|
originResponse = rung.response;
|
|
17807
18007
|
originTier = rung.attempt.tier;
|
|
@@ -18585,22 +18785,22 @@ function inferMimeFromPath(absPath, sniffBytes) {
|
|
|
18585
18785
|
return "application/octet-stream";
|
|
18586
18786
|
}
|
|
18587
18787
|
async function rasterizeSvgToPng(bytes) {
|
|
18588
|
-
const { default:
|
|
18589
|
-
const base = await
|
|
18788
|
+
const { default: sharp6 } = await import("sharp");
|
|
18789
|
+
const base = await sharp6(bytes).metadata();
|
|
18590
18790
|
const longest = Math.max(base.width ?? 0, base.height ?? 0);
|
|
18591
18791
|
let density = SVG_BASE_DENSITY;
|
|
18592
18792
|
if (longest > 0) {
|
|
18593
18793
|
const scaled = Math.round(SVG_BASE_DENSITY * SVG_TARGET_LONGEST_PX / longest);
|
|
18594
18794
|
density = Math.min(SVG_MAX_DENSITY, Math.max(SVG_BASE_DENSITY, scaled));
|
|
18595
18795
|
}
|
|
18596
|
-
return await
|
|
18796
|
+
return await sharp6(bytes, { density }).png({ force: true, palette: false }).toBuffer();
|
|
18597
18797
|
}
|
|
18598
18798
|
var MODEL_SAFE_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
18599
18799
|
var MAX_MODEL_REFERENCE_BYTES = 30 * 1024 * 1024;
|
|
18600
18800
|
async function fitWithinReferenceLimit(bytes, mime, maxBytes) {
|
|
18601
18801
|
if (bytes.byteLength <= maxBytes) return { bytes, mime };
|
|
18602
|
-
const { default:
|
|
18603
|
-
const read = () =>
|
|
18802
|
+
const { default: sharp6 } = await import("sharp");
|
|
18803
|
+
const read = () => sharp6(bytes, { animated: true });
|
|
18604
18804
|
const meta = await read().metadata();
|
|
18605
18805
|
const animated = (meta.pages ?? 1) > 1;
|
|
18606
18806
|
const keepAlpha = meta.hasAlpha === true;
|
|
@@ -18638,11 +18838,11 @@ async function toModelSafeImage(bytes, opts) {
|
|
|
18638
18838
|
if (sniffSvg(bytes)) {
|
|
18639
18839
|
return await fit(await rasterizeSvgToPng(bytes), "image/png", "svg");
|
|
18640
18840
|
}
|
|
18641
|
-
const { default:
|
|
18841
|
+
const { default: sharp6 } = await import("sharp");
|
|
18642
18842
|
let png;
|
|
18643
18843
|
let format;
|
|
18644
18844
|
try {
|
|
18645
|
-
const img =
|
|
18845
|
+
const img = sharp6(bytes);
|
|
18646
18846
|
format = (await img.metadata()).format ?? "unknown";
|
|
18647
18847
|
png = await img.png({ force: true }).toBuffer();
|
|
18648
18848
|
} catch (e) {
|
|
@@ -18660,8 +18860,8 @@ function normalizationNote(image) {
|
|
|
18660
18860
|
}
|
|
18661
18861
|
return parts.length > 0 ? parts.join("; ") : null;
|
|
18662
18862
|
}
|
|
18663
|
-
function hasAscii(buf,
|
|
18664
|
-
return buf.length >=
|
|
18863
|
+
function hasAscii(buf, offset2, sig) {
|
|
18864
|
+
return buf.length >= offset2 + sig.length && buf.toString("ascii", offset2, offset2 + sig.length) === sig;
|
|
18665
18865
|
}
|
|
18666
18866
|
var HEIC_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "heim", "heis", "hevc", "hevx", "mif1", "msf1", "heif"]);
|
|
18667
18867
|
function sniffIsoBmff(buf) {
|
|
@@ -18682,21 +18882,21 @@ function sniffImageMime(buf) {
|
|
|
18682
18882
|
return sniffIsoBmff(buf);
|
|
18683
18883
|
}
|
|
18684
18884
|
function findBoxPayload(buf, start, end, type) {
|
|
18685
|
-
let
|
|
18686
|
-
while (
|
|
18687
|
-
let size = buf.readUInt32BE(
|
|
18688
|
-
const boxType = buf.toString("ascii",
|
|
18885
|
+
let offset2 = start;
|
|
18886
|
+
while (offset2 + 8 <= end) {
|
|
18887
|
+
let size = buf.readUInt32BE(offset2);
|
|
18888
|
+
const boxType = buf.toString("ascii", offset2 + 4, offset2 + 8);
|
|
18689
18889
|
let headerLen = 8;
|
|
18690
18890
|
if (size === 1) {
|
|
18691
|
-
if (
|
|
18692
|
-
size = Number(buf.readBigUInt64BE(
|
|
18891
|
+
if (offset2 + 16 > end) return null;
|
|
18892
|
+
size = Number(buf.readBigUInt64BE(offset2 + 8));
|
|
18693
18893
|
headerLen = 16;
|
|
18694
18894
|
} else if (size === 0) {
|
|
18695
|
-
size = end -
|
|
18895
|
+
size = end - offset2;
|
|
18696
18896
|
}
|
|
18697
|
-
if (size < headerLen ||
|
|
18698
|
-
if (boxType === type) return { start:
|
|
18699
|
-
|
|
18897
|
+
if (size < headerLen || offset2 + size > end) return null;
|
|
18898
|
+
if (boxType === type) return { start: offset2 + headerLen, end: offset2 + size };
|
|
18899
|
+
offset2 += size;
|
|
18700
18900
|
}
|
|
18701
18901
|
return null;
|
|
18702
18902
|
}
|
|
@@ -20972,6 +21172,7 @@ import sharp4 from "sharp";
|
|
|
20972
21172
|
|
|
20973
21173
|
// src/engine/nodes/local/lib/band-check.ts
|
|
20974
21174
|
var BOX_TOLERANCE = 0.01;
|
|
21175
|
+
var FRAME_CUT = 5e-3;
|
|
20975
21176
|
function bandViolations(boxes, format, opts = {}) {
|
|
20976
21177
|
const band = opts.band ?? staticSafeArea(format, opts.placements);
|
|
20977
21178
|
const tolerance = opts.tolerance ?? BOX_TOLERANCE;
|
|
@@ -20984,7 +21185,16 @@ function bandViolations(boxes, format, opts = {}) {
|
|
|
20984
21185
|
};
|
|
20985
21186
|
const edges = Object.keys(reach).filter((edge) => reach[edge] > tolerance);
|
|
20986
21187
|
if (edges.length === 0) return [];
|
|
20987
|
-
|
|
21188
|
+
const toFrame = { top: box.y0, bottom: 1 - box.y1, left: box.x0, right: 1 - box.x1 };
|
|
21189
|
+
return [
|
|
21190
|
+
{
|
|
21191
|
+
text: box.text,
|
|
21192
|
+
kind: box.kind,
|
|
21193
|
+
edges,
|
|
21194
|
+
overshoot: Math.max(...edges.map((edge) => reach[edge])),
|
|
21195
|
+
clearance: Math.max(0, Math.min(...edges.map((edge) => toFrame[edge])))
|
|
21196
|
+
}
|
|
21197
|
+
];
|
|
20988
21198
|
});
|
|
20989
21199
|
}
|
|
20990
21200
|
|
|
@@ -21030,11 +21240,25 @@ function assign(targets, words, claimed, threshold) {
|
|
|
21030
21240
|
}
|
|
21031
21241
|
return matches;
|
|
21032
21242
|
}
|
|
21033
|
-
function
|
|
21243
|
+
function hyphenatedCompounds(texts) {
|
|
21244
|
+
const out = /* @__PURE__ */ new Set();
|
|
21245
|
+
for (const text of texts) {
|
|
21246
|
+
for (const [compound] of text.normalize("NFKC").toLowerCase().matchAll(new RegExp("\\p{L}+(?:[-\u2010\u2011]\\p{L}+)+", "gu"))) {
|
|
21247
|
+
const words = compound.split(/[-‐‑]/u);
|
|
21248
|
+
for (let i = 1; i < words.length; i++) out.add(`${words[i - 1]}-${words[i]}`);
|
|
21249
|
+
}
|
|
21250
|
+
}
|
|
21251
|
+
return out;
|
|
21252
|
+
}
|
|
21253
|
+
function joinHyphenBreaks(texts, compounds) {
|
|
21034
21254
|
const out = [];
|
|
21035
21255
|
let carry = "";
|
|
21036
21256
|
for (const text of texts) {
|
|
21037
|
-
const
|
|
21257
|
+
const next = text.trim();
|
|
21258
|
+
const before = carry.match(new RegExp("\\p{L}+$", "u"))?.[0] ?? "";
|
|
21259
|
+
const after = next.match(new RegExp("^\\p{L}+", "u"))?.[0] ?? "";
|
|
21260
|
+
const own = carry !== "" && compounds.has(`${before}-${after}`.normalize("NFKC").toLowerCase());
|
|
21261
|
+
const line = carry + (own ? "-" : "") + next;
|
|
21038
21262
|
const broken = line.match(new RegExp("^(.*\\p{L})[-\u2010\u2011]$", "u"));
|
|
21039
21263
|
if (broken) carry = broken[1] ?? "";
|
|
21040
21264
|
else {
|
|
@@ -21045,23 +21269,36 @@ function joinHyphenBreaks(texts) {
|
|
|
21045
21269
|
if (carry) out.push(`${carry}-`);
|
|
21046
21270
|
return out;
|
|
21047
21271
|
}
|
|
21272
|
+
function lineParts(text) {
|
|
21273
|
+
const parts = text.split(/\r?\n/).filter((part) => part.trim().length > 0);
|
|
21274
|
+
return parts.length > 0 ? parts : [text];
|
|
21275
|
+
}
|
|
21048
21276
|
function textMismatch(expected, boxes, opts = {}) {
|
|
21049
|
-
const
|
|
21277
|
+
const compounds = hyphenatedCompounds(expected.map((line) => line.text));
|
|
21278
|
+
const texts = boxes.filter((b) => b.kind === "text").map((b) => b.text);
|
|
21279
|
+
const words = joinHyphenBreaks(texts, compounds).flatMap(
|
|
21050
21280
|
(text, box) => normalizeCopy(text).split(" ").filter((word) => word.length > 0).map((word) => ({ word, box }))
|
|
21051
21281
|
);
|
|
21052
21282
|
const claimed = /* @__PURE__ */ new Set();
|
|
21283
|
+
const parts = expected.flatMap(
|
|
21284
|
+
(line, lineIndex) => lineParts(line.text).map((text) => ({ lineIndex, text: normalizeCopy(text) }))
|
|
21285
|
+
);
|
|
21053
21286
|
const matches = assign(
|
|
21054
|
-
|
|
21287
|
+
parts.map((part) => part.text),
|
|
21055
21288
|
words,
|
|
21056
21289
|
claimed,
|
|
21057
21290
|
MIN_MATCH
|
|
21058
21291
|
);
|
|
21059
21292
|
assign((opts.allowed ?? []).map(normalizeCopy), words, claimed, ALLOWED_MATCH);
|
|
21060
21293
|
const lines = expected.flatMap((line, index) => {
|
|
21061
|
-
const
|
|
21062
|
-
|
|
21294
|
+
const lineMatches = parts.flatMap(
|
|
21295
|
+
(part, partIndex) => part.lineIndex === index && part.text.length > 0 ? [matches.get(partIndex)] : []
|
|
21296
|
+
);
|
|
21297
|
+
if (lineMatches.length === 0) return [];
|
|
21298
|
+
const score = Math.min(...lineMatches.map((match) => match?.similarity ?? 0));
|
|
21063
21299
|
if (score >= 1 - MAX_LINE_DISTANCE) return [];
|
|
21064
|
-
|
|
21300
|
+
const read = lineMatches.flatMap((match) => match ? [match.span.text] : []).join(" ");
|
|
21301
|
+
return [{ ...line, read: read.length > 0 ? read : null, similarity: score }];
|
|
21065
21302
|
});
|
|
21066
21303
|
const extra = [];
|
|
21067
21304
|
words.forEach(({ word, box }, index) => {
|
|
@@ -21873,14 +22110,24 @@ function outsideBand(v, placement) {
|
|
|
21873
22110
|
if (placement === "reels") return `${what} sits where Reels covers it (${edges}).`;
|
|
21874
22111
|
if (placement === "stories") return `${what} sits where Stories covers it (${edges}).`;
|
|
21875
22112
|
if (placement === "master") return `${what} sits outside the space every size keeps clear (${edges}).`;
|
|
21876
|
-
return `${what}
|
|
22113
|
+
if (v.clearance <= FRAME_CUT) return `${what} runs off the edge of the picture (${edges}).`;
|
|
22114
|
+
return `${what} sits close to the edge (${edges}).`;
|
|
22115
|
+
}
|
|
22116
|
+
function bandSeverity(v, placement) {
|
|
22117
|
+
return placement !== "feed" || v.clearance <= FRAME_CUT ? "blocking" : "warning";
|
|
21877
22118
|
}
|
|
21878
22119
|
var EDGE_ORDER = ["top", "bottom", "left", "right"];
|
|
21879
22120
|
function oneLogo(violations) {
|
|
21880
22121
|
const logo = violations.filter((v) => v.kind === "logo");
|
|
21881
22122
|
if (logo.length <= 1) return [...violations];
|
|
21882
22123
|
const edges = EDGE_ORDER.filter((e) => logo.some((v) => v.edges.includes(e)));
|
|
21883
|
-
const merged = {
|
|
22124
|
+
const merged = {
|
|
22125
|
+
text: "",
|
|
22126
|
+
kind: "logo",
|
|
22127
|
+
edges,
|
|
22128
|
+
overshoot: Math.max(...logo.map((v) => v.overshoot)),
|
|
22129
|
+
clearance: Math.min(...logo.map((v) => v.clearance))
|
|
22130
|
+
};
|
|
21884
22131
|
const first = violations.indexOf(logo[0]);
|
|
21885
22132
|
const rest = violations.filter((v) => v.kind !== "logo");
|
|
21886
22133
|
return [...rest.slice(0, first), merged, ...rest.slice(first)];
|
|
@@ -21901,7 +22148,9 @@ function measuredFindings(input) {
|
|
|
21901
22148
|
(l) => blocking(l.read === null ? `Text: "${l.text}" is missing.` : `Text: "${l.text}" reads "${l.read}".`)
|
|
21902
22149
|
),
|
|
21903
22150
|
...text.extra.length > 0 ? [blocking(`Text: extra words nobody asked for: ${text.extra.map((e) => `"${e}"`).join(", ")}.`)] : [],
|
|
21904
|
-
...oneLogo(band).map(
|
|
22151
|
+
...oneLogo(band).map(
|
|
22152
|
+
(v) => ({ severity: bandSeverity(v, placement), what: `${format}: ${outsideBand(v, placement)}` })
|
|
22153
|
+
),
|
|
21905
22154
|
...(input.offBrand ?? []).map(
|
|
21906
22155
|
(ink) => ({
|
|
21907
22156
|
severity: "warning",
|
|
@@ -21916,8 +22165,10 @@ function staticVerdict(findings) {
|
|
|
21916
22165
|
return findings.length > 0 ? "ready_with_notes" : "ready";
|
|
21917
22166
|
}
|
|
21918
22167
|
function textInstructions(copy, measured) {
|
|
22168
|
+
const wordless = "This ad has no words on purpose: the picture is the ad. Do not report missing copy, a missing headline or a missing call to action.";
|
|
21919
22169
|
if (measured) {
|
|
21920
22170
|
return [
|
|
22171
|
+
...copy.length === 0 ? [wordless] : [],
|
|
21921
22172
|
"The words were already read back and measured by another check: do not transcribe them or report their spelling, extra words or position.",
|
|
21922
22173
|
"1. TEXT: garbled or malformed letters, or letters that do not read as words (a brand name inside a drawn logo is fine)."
|
|
21923
22174
|
];
|
|
@@ -21929,10 +22180,7 @@ function textInstructions(copy, measured) {
|
|
|
21929
22180
|
"1. TEXT: extra words that are not in the list, a string shown twice, or garbled letters (a brand name inside a drawn logo is fine)."
|
|
21930
22181
|
];
|
|
21931
22182
|
}
|
|
21932
|
-
return [
|
|
21933
|
-
"This ad has no words on purpose: the picture is the ad. Do not report missing copy, a missing headline or a missing call to action.",
|
|
21934
|
-
"1. TEXT: any letters or words that appear anyway (invented or garbled text is blocking)."
|
|
21935
|
-
];
|
|
22183
|
+
return [wordless, "1. TEXT: any letters or words that appear anyway (invented or garbled text is blocking)."];
|
|
21936
22184
|
}
|
|
21937
22185
|
function buildStaticReviewPrompt(opts) {
|
|
21938
22186
|
const brand = opts.brand ? ` for ${opts.brand}` : "";
|
|
@@ -22091,13 +22339,13 @@ function rankOf(boxes, blocking, want) {
|
|
|
22091
22339
|
const logoFound = boxes.some((b) => b.kind === "logo" || brand !== null && normalizeCopy(b.text) === brand);
|
|
22092
22340
|
const noLogo = want.logo && !logoFound ? 1 : 0;
|
|
22093
22341
|
const noButton = off.some((l) => want.buttons.has(l.text)) ? 1 : 0;
|
|
22094
|
-
const outOfBand = bandViolations(boxes, want.format,
|
|
22342
|
+
const outOfBand = bandViolations(boxes, want.format, want.grade).length;
|
|
22095
22343
|
return {
|
|
22096
22344
|
rank: [blocking.length, off.length, noLogo, noButton, outOfBand],
|
|
22097
22345
|
shortfalls: noLogo ? [NO_LOGO] : []
|
|
22098
22346
|
};
|
|
22099
22347
|
}
|
|
22100
|
-
var
|
|
22348
|
+
var isButton2 = (line) => /cta|button/i.test(`${line.role ?? ""} ${line.look ?? ""}`);
|
|
22101
22349
|
async function prefilterVariants(variants, spec, formats, opts = {}) {
|
|
22102
22350
|
const everything = {
|
|
22103
22351
|
kept: seededOrder(
|
|
@@ -22114,7 +22362,7 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
|
|
|
22114
22362
|
try {
|
|
22115
22363
|
const parsed = readAdSpec(spec);
|
|
22116
22364
|
lines = parsed.copy.map((l) => ({ text: l.text, lettering: l.lettering }));
|
|
22117
|
-
buttons = new Set(parsed.copy.filter(
|
|
22365
|
+
buttons = new Set(parsed.copy.filter(isButton2).map((l) => l.text));
|
|
22118
22366
|
brand = parsed.brand?.name;
|
|
22119
22367
|
logo = parsed.logo === true;
|
|
22120
22368
|
} catch {
|
|
@@ -22126,6 +22374,7 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
|
|
|
22126
22374
|
...opts.placements ? { placements: opts.placements } : {}
|
|
22127
22375
|
});
|
|
22128
22376
|
const format = plan.master.format;
|
|
22377
|
+
const grade = plan.masterShape === "relayout" ? plan.placements ? { placements: plan.placements } : {} : { band: plan.band };
|
|
22129
22378
|
const checked = await Promise.all(
|
|
22130
22379
|
variants.map(async (variant) => {
|
|
22131
22380
|
const source = variant.path ?? variant.url;
|
|
@@ -22137,7 +22386,7 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
|
|
|
22137
22386
|
format,
|
|
22138
22387
|
lines,
|
|
22139
22388
|
allowed: brand ? [brand] : [],
|
|
22140
|
-
|
|
22389
|
+
...grade,
|
|
22141
22390
|
toFrame: (boxes) => coverFitBoxes(boxes, { width, height }, plan.master.size),
|
|
22142
22391
|
...opts.read ? { read: opts.read } : {},
|
|
22143
22392
|
...opts.onSpend ? { onSpend: opts.onSpend } : {}
|
|
@@ -22145,7 +22394,7 @@ async function prefilterVariants(variants, spec, formats, opts = {}) {
|
|
|
22145
22394
|
if (check.status === "not_checked") return { index: variant.index, blocking: null };
|
|
22146
22395
|
const prefix = `${format}: `;
|
|
22147
22396
|
const blocking = check.findings.filter((f) => f.severity === "blocking").map((f) => f.what.startsWith(prefix) ? f.what.slice(prefix.length) : f.what);
|
|
22148
|
-
const want = { lines, buttons, ...brand ? { brand } : {}, logo, format,
|
|
22397
|
+
const want = { lines, buttons, ...brand ? { brand } : {}, logo, format, grade };
|
|
22149
22398
|
return { index: variant.index, blocking, ...rankOf(check.boxes, blocking, want) };
|
|
22150
22399
|
} catch {
|
|
22151
22400
|
return { index: variant.index, blocking: null };
|
|
@@ -22250,61 +22499,191 @@ var staticPrefilterNode = defineNode({
|
|
|
22250
22499
|
}
|
|
22251
22500
|
});
|
|
22252
22501
|
|
|
22253
|
-
// src/engine/nodes/local/
|
|
22502
|
+
// src/engine/nodes/local/staticSafeFit.ts
|
|
22254
22503
|
import { z as z58 } from "zod";
|
|
22504
|
+
|
|
22505
|
+
// src/engine/nodes/local/lib/safe-fit.ts
|
|
22506
|
+
import sharp5 from "sharp";
|
|
22507
|
+
var FIT_HEADROOM = 0.01;
|
|
22508
|
+
var MIN_FIT_SCALE = 0.85;
|
|
22509
|
+
function planSafeFit(boxes, format, opts = {}) {
|
|
22510
|
+
if (bandViolations(boxes, format, opts).length === 0) return null;
|
|
22511
|
+
const area = staticSafeArea(format, opts.placements);
|
|
22512
|
+
const union = {
|
|
22513
|
+
x0: Math.min(...boxes.map((b) => b.x0)),
|
|
22514
|
+
y0: Math.min(...boxes.map((b) => b.y0)),
|
|
22515
|
+
x1: Math.max(...boxes.map((b) => b.x1)),
|
|
22516
|
+
y1: Math.max(...boxes.map((b) => b.y1))
|
|
22517
|
+
};
|
|
22518
|
+
const band = {
|
|
22519
|
+
x0: area.left + FIT_HEADROOM,
|
|
22520
|
+
y0: area.top + FIT_HEADROOM,
|
|
22521
|
+
x1: 1 - area.right - FIT_HEADROOM,
|
|
22522
|
+
y1: 1 - area.bottom - FIT_HEADROOM
|
|
22523
|
+
};
|
|
22524
|
+
const scale = Math.min(1, (band.y1 - band.y0) / (union.y1 - union.y0), (band.x1 - band.x0) / (union.x1 - union.x0));
|
|
22525
|
+
if (scale < MIN_FIT_SCALE) return null;
|
|
22526
|
+
return {
|
|
22527
|
+
scale,
|
|
22528
|
+
x: offset(scale, union.x0, union.x1, band.x0, band.x1),
|
|
22529
|
+
y: offset(scale, union.y0, union.y1, band.y0, band.y1)
|
|
22530
|
+
};
|
|
22531
|
+
}
|
|
22532
|
+
function offset(scale, from, to, bandFrom, bandTo) {
|
|
22533
|
+
const lo = bandFrom - scale * from;
|
|
22534
|
+
const hi = bandTo - scale * to;
|
|
22535
|
+
if (lo > hi) return (lo + hi) / 2;
|
|
22536
|
+
return Math.min(hi, Math.max(lo, (1 - scale) / 2));
|
|
22537
|
+
}
|
|
22538
|
+
var MARGIN_BLUR = 0.012;
|
|
22539
|
+
var SEAM_FEATHER = 0.015;
|
|
22540
|
+
async function fitPicture(image, fit) {
|
|
22541
|
+
const { width = 0, height = 0 } = await sharp5(image).metadata();
|
|
22542
|
+
const sw = Math.max(1, Math.round(width * fit.scale));
|
|
22543
|
+
const sh = Math.max(1, Math.round(height * fit.scale));
|
|
22544
|
+
const px = Math.round(fit.x * width);
|
|
22545
|
+
const py = Math.round(fit.y * height);
|
|
22546
|
+
const left = Math.max(0, -px);
|
|
22547
|
+
const top = Math.max(0, -py);
|
|
22548
|
+
const w = Math.min(sw, width - px) - left;
|
|
22549
|
+
const h = Math.min(sh, height - py) - top;
|
|
22550
|
+
const shrunk = await sharp5(image).removeAlpha().resize(sw, sh, { fit: "fill", kernel: "lanczos3" }).extract({ left, top, width: w, height: h }).toBuffer();
|
|
22551
|
+
const pad = { left: Math.max(0, px), top: Math.max(0, py) };
|
|
22552
|
+
const extend = { ...pad, right: width - pad.left - w, bottom: height - pad.top - h };
|
|
22553
|
+
const extended = await sharp5(shrunk).extend({ ...extend, extendWith: "mirror" }).raw().toBuffer({ resolveWithObject: true });
|
|
22554
|
+
const frame = extended.data;
|
|
22555
|
+
const channels = extended.info.channels;
|
|
22556
|
+
const blurred = await sharp5(frame, { raw: { width, height, channels } }).blur(Math.max(0.3, MARGIN_BLUR * width)).raw().toBuffer();
|
|
22557
|
+
const feather = Math.max(1, SEAM_FEATHER * width);
|
|
22558
|
+
const keep = (at2, from, to, padFrom, padTo) => {
|
|
22559
|
+
const inFrom = padFrom ? (at2 - from) / feather + 0.5 : 1;
|
|
22560
|
+
const inTo = padTo ? (to - at2) / feather + 0.5 : 1;
|
|
22561
|
+
return Math.min(1, Math.max(0, Math.min(inFrom, inTo)));
|
|
22562
|
+
};
|
|
22563
|
+
const out = Buffer.alloc(frame.length);
|
|
22564
|
+
const x0 = pad.left;
|
|
22565
|
+
const x1 = pad.left + w;
|
|
22566
|
+
const y0 = pad.top;
|
|
22567
|
+
const y1 = pad.top + h;
|
|
22568
|
+
for (let y = 0; y < height; y++) {
|
|
22569
|
+
const ky = keep(y + 0.5, y0, y1, extend.top > 0, extend.bottom > 0);
|
|
22570
|
+
for (let x = 0; x < width; x++) {
|
|
22571
|
+
const k = Math.min(ky, keep(x + 0.5, x0, x1, extend.left > 0, extend.right > 0));
|
|
22572
|
+
const at2 = (y * width + x) * channels;
|
|
22573
|
+
for (let c = 0; c < channels; c++) {
|
|
22574
|
+
out[at2 + c] = Math.round(k * (frame[at2 + c] ?? 0) + (1 - k) * (blurred[at2 + c] ?? 0));
|
|
22575
|
+
}
|
|
22576
|
+
}
|
|
22577
|
+
}
|
|
22578
|
+
return sharp5(out, { raw: { width, height, channels } }).png().toBuffer();
|
|
22579
|
+
}
|
|
22580
|
+
|
|
22581
|
+
// src/engine/nodes/local/staticSafeFit.ts
|
|
22582
|
+
var Usage2 = z58.object({ model: z58.string(), inputTokens: z58.number(), outputTokens: z58.number(), costUsd: z58.number() }).strict();
|
|
22583
|
+
var Fit = z58.object({ scale: z58.number(), x: z58.number(), y: z58.number() }).strict();
|
|
22584
|
+
var staticSafeFitNode = defineNode({
|
|
22585
|
+
id: "static_safe_fit",
|
|
22586
|
+
version: "1.0.0",
|
|
22587
|
+
category: "image",
|
|
22588
|
+
location: "local",
|
|
22589
|
+
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, filling the uncovered margin by mirroring the picture beside it. Outputs the same pixel size; `fit` is the scale and position used, or null with `reason` when the picture is passed on unchanged.",
|
|
22590
|
+
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% is passed on unchanged: the review still names what Reels or Stories covers, and the layout itself has to change (shorter copy, fewer lines).",
|
|
22591
|
+
inputs: z58.object({ image: ImageRef }).strict(),
|
|
22592
|
+
// `placements` are the ones the canvas's sizes were planned with (the ad set's), so the band
|
|
22593
|
+
// the fit clears is the one the review holds the size to.
|
|
22594
|
+
params: z58.object({ format: z58.string().min(1), placements: z58.array(z58.string().min(1)).optional() }).strict(),
|
|
22595
|
+
outputs: z58.object({
|
|
22596
|
+
image: ImageRef,
|
|
22597
|
+
fit: Fit.nullable(),
|
|
22598
|
+
// Why the picture passed unchanged: the reader could not read it, nothing was outside the
|
|
22599
|
+
// band, or clearing it needed more than the smallest shrink that still looks native.
|
|
22600
|
+
reason: z58.enum(["not_read", "inside_band", "too_far"]).optional(),
|
|
22601
|
+
// What the reader's call cost, as the gateway reported it (the run record carries it home).
|
|
22602
|
+
usage: z58.array(Usage2).optional()
|
|
22603
|
+
}).strict(),
|
|
22604
|
+
outputKinds: { image: "image" },
|
|
22605
|
+
cost: () => ({ credits: 0, seconds_estimate: 5 }),
|
|
22606
|
+
async execute({ inputs, params, ctx }) {
|
|
22607
|
+
const source = inputs.image.path ?? inputs.image.url;
|
|
22608
|
+
if (!source) return { image: inputs.image, fit: null, reason: "not_read" };
|
|
22609
|
+
const bytes = await bytesOf(source);
|
|
22610
|
+
const usage = [];
|
|
22611
|
+
const reading = await readTextBoxes(bytes, { onSpend: (u) => usage.push(u) });
|
|
22612
|
+
const spend = usage.length > 0 ? { usage } : {};
|
|
22613
|
+
if (reading.status !== "read") return { image: inputs.image, fit: null, reason: "not_read", ...spend };
|
|
22614
|
+
const placements = params.placements ? { placements: params.placements } : {};
|
|
22615
|
+
const fit = planSafeFit(reading.boxes, params.format, placements);
|
|
22616
|
+
if (!fit) {
|
|
22617
|
+
const outside = bandViolations(reading.boxes, params.format, placements).length > 0;
|
|
22618
|
+
return {
|
|
22619
|
+
image: inputs.image,
|
|
22620
|
+
fit: null,
|
|
22621
|
+
reason: outside ? "too_far" : "inside_band",
|
|
22622
|
+
...spend
|
|
22623
|
+
};
|
|
22624
|
+
}
|
|
22625
|
+
const fitted = await fitPicture(bytes, fit);
|
|
22626
|
+
ctx.log(`fit ${params.format} at scale ${fit.scale.toFixed(3)}, offset ${fit.x.toFixed(3)}, ${fit.y.toFixed(3)}`);
|
|
22627
|
+
const image = await ctx.assets.ingestBytes({ bytes: fitted, kind: "image", mime: "image/png" });
|
|
22628
|
+
return { image, fit, ...spend };
|
|
22629
|
+
}
|
|
22630
|
+
});
|
|
22631
|
+
|
|
22632
|
+
// src/engine/nodes/local/text.ts
|
|
22633
|
+
import { z as z59 } from "zod";
|
|
22255
22634
|
var textNode = defineNode({
|
|
22256
22635
|
id: "text",
|
|
22257
22636
|
version: "1.0.0",
|
|
22258
22637
|
category: "data",
|
|
22259
22638
|
location: "local",
|
|
22260
22639
|
summary: "A literal text value. Use for prompts, descriptions, copy.",
|
|
22261
|
-
inputs:
|
|
22262
|
-
params:
|
|
22263
|
-
outputs:
|
|
22640
|
+
inputs: z59.object({}).strict(),
|
|
22641
|
+
params: z59.object({ value: z59.string() }).strict(),
|
|
22642
|
+
outputs: z59.object({ text: z59.string() }).strict(),
|
|
22264
22643
|
cost: () => ({ credits: 0, seconds_estimate: 0 }),
|
|
22265
22644
|
execute: ({ params }) => Promise.resolve({ text: params.value })
|
|
22266
22645
|
});
|
|
22267
22646
|
|
|
22268
22647
|
// src/engine/nodes/local/videoConcat.ts
|
|
22269
|
-
import { z as
|
|
22648
|
+
import { z as z60 } from "zod";
|
|
22270
22649
|
var FFMPEG_BIN4 = "ffmpeg";
|
|
22271
|
-
var evenPixels = (axis) =>
|
|
22272
|
-
var Transition =
|
|
22273
|
-
type:
|
|
22274
|
-
duration_s:
|
|
22650
|
+
var evenPixels = (axis) => z60.number().int().positive().refine((n) => n % 2 === 0, `${axis} must be even: libx264 yuv420p rejects odd sizes`);
|
|
22651
|
+
var Transition = z60.object({
|
|
22652
|
+
type: z60.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"),
|
|
22653
|
+
duration_s: z60.number().positive().describe("Overlap length in seconds (0.4 is the scaffold default).")
|
|
22275
22654
|
}).strict();
|
|
22276
|
-
var ConcatClip =
|
|
22277
|
-
duration_s:
|
|
22655
|
+
var ConcatClip = z60.object({
|
|
22656
|
+
duration_s: z60.number().positive().describe(
|
|
22278
22657
|
"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."
|
|
22279
22658
|
),
|
|
22280
|
-
length:
|
|
22659
|
+
length: z60.enum(["source", "trim", "still"]).default("source").describe(
|
|
22281
22660
|
"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)."
|
|
22282
22661
|
),
|
|
22283
|
-
offset_s:
|
|
22284
|
-
fit:
|
|
22662
|
+
offset_s: z60.number().min(0).optional().describe("`trim` only: where the window starts in the input, seconds."),
|
|
22663
|
+
fit: z60.enum(["fill", "pad"]).default("fill").describe(
|
|
22285
22664
|
"`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)."
|
|
22286
22665
|
),
|
|
22287
22666
|
transition: Transition.optional().describe(
|
|
22288
22667
|
"The transition OUT of this clip into the next one. Absent = a hard cut."
|
|
22289
22668
|
),
|
|
22290
|
-
continues_frame:
|
|
22669
|
+
continues_frame: z60.boolean().optional().describe(
|
|
22291
22670
|
"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."
|
|
22292
22671
|
)
|
|
22293
22672
|
}).strict().refine((c) => c.offset_s === void 0 || c.length === "trim", {
|
|
22294
22673
|
message: "offset_s only applies to a `trim` clip",
|
|
22295
22674
|
path: ["offset_s"]
|
|
22296
22675
|
});
|
|
22297
|
-
var VideoConcatParams =
|
|
22676
|
+
var VideoConcatParams = z60.object({
|
|
22298
22677
|
width: evenPixels("width").describe("Output width in pixels (even). Every clip is normalized onto it."),
|
|
22299
22678
|
height: evenPixels("height").describe("Output height in pixels (even)."),
|
|
22300
|
-
fps:
|
|
22301
|
-
seam_dedup:
|
|
22679
|
+
fps: z60.number().int().min(1).max(120).default(30).describe("Output frame rate every clip is resampled to."),
|
|
22680
|
+
seam_dedup: z60.enum(["head", "tail", "off"]).default("head").describe(
|
|
22302
22681
|
"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."
|
|
22303
22682
|
),
|
|
22304
|
-
clips:
|
|
22683
|
+
clips: z60.array(ConcatClip).min(1).describe("The clips in timeline order; clip i is wired as `inputs.c<i>`.")
|
|
22305
22684
|
}).strict();
|
|
22306
|
-
var VideoConcatInputs =
|
|
22307
|
-
var VideoConcatOutputs =
|
|
22685
|
+
var VideoConcatInputs = z60.record(z60.string(), z60.custom());
|
|
22686
|
+
var VideoConcatOutputs = z60.object({ video: z60.custom() }).strict();
|
|
22308
22687
|
function inputLengthS(clip2) {
|
|
22309
22688
|
return clip2.duration_s + (clip2.transition?.duration_s ?? 0);
|
|
22310
22689
|
}
|
|
@@ -22317,8 +22696,8 @@ function clipOps(clip2, params) {
|
|
|
22317
22696
|
const normalize3 = `${scale},format=yuv420p,fps=${fps},setsar=1,settb=AVTB`;
|
|
22318
22697
|
const len = inputLengthS(clip2).toFixed(3);
|
|
22319
22698
|
if (clip2.length === "trim") {
|
|
22320
|
-
const
|
|
22321
|
-
const start =
|
|
22699
|
+
const offset2 = clip2.offset_s ?? 0;
|
|
22700
|
+
const start = offset2 > 0 ? `start=${offset2.toFixed(3)}:` : "";
|
|
22322
22701
|
return `${normalize3},trim=${start}duration=${len},setpts=PTS-STARTPTS`;
|
|
22323
22702
|
}
|
|
22324
22703
|
if (clip2.length === "still") return `${normalize3},trim=duration=${len}`;
|
|
@@ -22350,9 +22729,9 @@ function blendedJoins(params) {
|
|
|
22350
22729
|
const out = k === clips.length - 2 ? "v" : `j${k + 1}`;
|
|
22351
22730
|
const nextLen = inputLengthS(clips[k + 1]);
|
|
22352
22731
|
if (transition) {
|
|
22353
|
-
const
|
|
22732
|
+
const offset2 = Math.max(0, accLen - transition.duration_s);
|
|
22354
22733
|
joins.push(
|
|
22355
|
-
`[${cur}][${next}]xfade=transition=${transition.type}:duration=${transition.duration_s.toFixed(3)}:offset=${
|
|
22734
|
+
`[${cur}][${next}]xfade=transition=${transition.type}:duration=${transition.duration_s.toFixed(3)}:offset=${offset2.toFixed(3)}[${out}]`
|
|
22356
22735
|
);
|
|
22357
22736
|
accLen = accLen - transition.duration_s + nextLen;
|
|
22358
22737
|
} else {
|
|
@@ -22447,14 +22826,14 @@ var videoConcatNode = defineNode({
|
|
|
22447
22826
|
});
|
|
22448
22827
|
|
|
22449
22828
|
// src/engine/nodes/remote/audioVoiceConvert.ts
|
|
22450
|
-
import { z as
|
|
22451
|
-
var AudioVoiceConvertParams =
|
|
22452
|
-
model:
|
|
22829
|
+
import { z as z61 } from "zod";
|
|
22830
|
+
var AudioVoiceConvertParams = z61.object({
|
|
22831
|
+
model: z61.literal("elevenlabs/eleven_multilingual_sts_v2"),
|
|
22453
22832
|
/** Target voice id. Splice an upstream `voice_select` via `"{{voice_ref}}"`. */
|
|
22454
|
-
voice:
|
|
22455
|
-
output_format:
|
|
22833
|
+
voice: z61.string().min(1),
|
|
22834
|
+
output_format: z61.string().optional(),
|
|
22456
22835
|
/** Strip the source clip's background noise before re-voicing. */
|
|
22457
|
-
remove_background_noise:
|
|
22836
|
+
remove_background_noise: z61.boolean().optional()
|
|
22458
22837
|
}).strict();
|
|
22459
22838
|
var audioVoiceConvertNode = delegated({
|
|
22460
22839
|
id: "audio_voice_convert",
|
|
@@ -22462,12 +22841,12 @@ var audioVoiceConvertNode = delegated({
|
|
|
22462
22841
|
category: "audio",
|
|
22463
22842
|
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.",
|
|
22464
22843
|
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}}"`.',
|
|
22465
|
-
inputs:
|
|
22844
|
+
inputs: z61.object({
|
|
22466
22845
|
audio: AudioRef,
|
|
22467
22846
|
voice_ref: TextRef.optional()
|
|
22468
22847
|
}).strict(),
|
|
22469
22848
|
params: AudioVoiceConvertParams,
|
|
22470
|
-
outputs:
|
|
22849
|
+
outputs: z61.object({ audio: AudioRef }).strict(),
|
|
22471
22850
|
outputKinds: { audio: "audio" },
|
|
22472
22851
|
// Priced by the shared contract per minute of source audio. The quote cannot see
|
|
22473
22852
|
// the clip's length, so it assumes one minute; the charge uses the real length.
|
|
@@ -22478,33 +22857,33 @@ var audioVoiceConvertNode = delegated({
|
|
|
22478
22857
|
});
|
|
22479
22858
|
|
|
22480
22859
|
// src/engine/nodes/remote/dialogue.ts
|
|
22481
|
-
import { z as
|
|
22482
|
-
var DialogueInput =
|
|
22483
|
-
text:
|
|
22484
|
-
voice_id:
|
|
22860
|
+
import { z as z62 } from "zod";
|
|
22861
|
+
var DialogueInput = z62.object({
|
|
22862
|
+
text: z62.string().min(1),
|
|
22863
|
+
voice_id: z62.string().min(1)
|
|
22485
22864
|
});
|
|
22486
22865
|
var DIALOGUE_MODELS = ["elevenlabs/eleven_v3"];
|
|
22487
|
-
var DialogueParams =
|
|
22488
|
-
model:
|
|
22866
|
+
var DialogueParams = z62.object({
|
|
22867
|
+
model: z62.enum(DIALOGUE_MODELS),
|
|
22489
22868
|
/**
|
|
22490
22869
|
* Ordered list of lines, each tagged with the voice that should speak it.
|
|
22491
22870
|
* Up to 10 unique voice_ids; total text across all lines should stay under
|
|
22492
22871
|
* ~2000 characters for best quality (ElevenLabs guidance).
|
|
22493
22872
|
*/
|
|
22494
|
-
inputs:
|
|
22495
|
-
language_code:
|
|
22873
|
+
inputs: z62.array(DialogueInput).min(1).max(50),
|
|
22874
|
+
language_code: z62.string().optional(),
|
|
22496
22875
|
/** ElevenLabs voice/model settings passthrough (e.g. `{ stability: 0.5 }`). */
|
|
22497
|
-
settings:
|
|
22498
|
-
seed:
|
|
22499
|
-
apply_text_normalization:
|
|
22876
|
+
settings: z62.record(z62.string(), z62.unknown()).optional(),
|
|
22877
|
+
seed: z62.number().int().min(0).max(4294967295).optional(),
|
|
22878
|
+
apply_text_normalization: z62.enum(["auto", "on", "off"]).optional(),
|
|
22500
22879
|
/**
|
|
22501
22880
|
* When true, hits `/v1/text-to-dialogue/with-timestamps` and emits a
|
|
22502
22881
|
* separate `timestamps` output — character-level alignment plus
|
|
22503
22882
|
* per-voice segment markers usable for captions, lipsync, or
|
|
22504
22883
|
* beat-matched cuts in ad creatives.
|
|
22505
22884
|
*/
|
|
22506
|
-
with_timestamps:
|
|
22507
|
-
output_format:
|
|
22885
|
+
with_timestamps: z62.boolean().optional(),
|
|
22886
|
+
output_format: z62.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
22508
22887
|
}).strict().refine((p) => p.inputs.reduce((sum, line) => sum + line.text.length, 0) <= ELEVENLABS_MAX_TEXT_CHARS, {
|
|
22509
22888
|
message: `total dialogue text exceeds ${ELEVENLABS_MAX_TEXT_CHARS} characters`,
|
|
22510
22889
|
path: ["inputs"]
|
|
@@ -22515,19 +22894,19 @@ var dialogueNode = delegated({
|
|
|
22515
22894
|
category: "audio",
|
|
22516
22895
|
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.",
|
|
22517
22896
|
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.",
|
|
22518
|
-
inputs:
|
|
22897
|
+
inputs: z62.object({}).loose(),
|
|
22519
22898
|
params: DialogueParams,
|
|
22520
|
-
outputs:
|
|
22899
|
+
outputs: z62.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
22521
22900
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
22522
22901
|
// Priced by the shared contract, the same per-character rate the backend charges.
|
|
22523
22902
|
cost: ({ params }) => ({ credits: estimateNodeCredits({ nodeType: "dialogue", params }) ?? 1, seconds_estimate: 8 })
|
|
22524
22903
|
});
|
|
22525
22904
|
|
|
22526
22905
|
// src/engine/nodes/remote/image.ts
|
|
22527
|
-
import { z as
|
|
22906
|
+
import { z as z64 } from "zod";
|
|
22528
22907
|
|
|
22529
22908
|
// src/engine/nodes/remote/registry-params.ts
|
|
22530
|
-
import { z as
|
|
22909
|
+
import { z as z63 } from "zod";
|
|
22531
22910
|
function registryParamShape(kind, overrides = {}) {
|
|
22532
22911
|
const specs = Object.values(MODEL_REGISTRY[kind]);
|
|
22533
22912
|
const byParam = /* @__PURE__ */ new Map();
|
|
@@ -22548,7 +22927,7 @@ function unionOf(schemas) {
|
|
|
22548
22927
|
const kinds = [...new Set(schemas.map((schema) => schema.kind))];
|
|
22549
22928
|
if (kinds.length > 1) {
|
|
22550
22929
|
const [first, second, ...rest] = kinds.map((k) => unionOf(schemas.filter((s) => s.kind === k)));
|
|
22551
|
-
return
|
|
22930
|
+
return z63.union([first, second, ...rest]);
|
|
22552
22931
|
}
|
|
22553
22932
|
switch (kinds[0]) {
|
|
22554
22933
|
case "string":
|
|
@@ -22556,41 +22935,37 @@ function unionOf(schemas) {
|
|
|
22556
22935
|
case "number":
|
|
22557
22936
|
return numberUnion(schemas);
|
|
22558
22937
|
case "boolean":
|
|
22559
|
-
return
|
|
22938
|
+
return z63.boolean();
|
|
22560
22939
|
default:
|
|
22561
|
-
return
|
|
22940
|
+
return z63.unknown();
|
|
22562
22941
|
}
|
|
22563
22942
|
}
|
|
22564
22943
|
function stringUnion(schemas) {
|
|
22565
|
-
if (schemas.some((schema) => !schema.enum)) return
|
|
22944
|
+
if (schemas.some((schema) => !schema.enum)) return z63.string();
|
|
22566
22945
|
const values = [...new Set(schemas.flatMap((schema) => schema.enum ?? []))];
|
|
22567
|
-
return
|
|
22946
|
+
return z63.enum(values);
|
|
22568
22947
|
}
|
|
22569
22948
|
function numberUnion(schemas) {
|
|
22570
|
-
if (schemas.some((schema) => !schema.enum)) return
|
|
22949
|
+
if (schemas.some((schema) => !schema.enum)) return z63.number();
|
|
22571
22950
|
const values = [...new Set(schemas.flatMap((schema) => schema.enum ?? []))].sort((a, b) => a - b);
|
|
22572
|
-
return
|
|
22951
|
+
return z63.literal(values);
|
|
22573
22952
|
}
|
|
22574
22953
|
|
|
22575
22954
|
// src/engine/nodes/remote/image.ts
|
|
22576
|
-
var imageModelEnum =
|
|
22577
|
-
var ImageGenerateParams =
|
|
22955
|
+
var imageModelEnum = z64.enum(IMAGE_GENERATE_MODELS);
|
|
22956
|
+
var ImageGenerateParams = z64.object({
|
|
22578
22957
|
model: imageModelEnum,
|
|
22579
22958
|
...registryParamShape("image_generate", {
|
|
22580
|
-
prompt:
|
|
22959
|
+
prompt: z64.string().min(1),
|
|
22581
22960
|
// Recraft v4 vector controls, sent as the provider's own `controls` block. The
|
|
22582
22961
|
// registry types them as JSON; this is their shape.
|
|
22583
|
-
rgb_colors:
|
|
22584
|
-
background_rgb_color:
|
|
22962
|
+
rgb_colors: z64.array(z64.array(z64.number().int().min(0).max(255))),
|
|
22963
|
+
background_rgb_color: z64.array(z64.number().int().min(0).max(255))
|
|
22585
22964
|
})
|
|
22586
22965
|
}).strict();
|
|
22587
|
-
var PRO_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
22588
|
-
"google/gemini-3-pro-image",
|
|
22589
|
-
"google/gemini-3-pro-image-preview"
|
|
22590
|
-
]);
|
|
22591
22966
|
var PRO_QUALITY = "max";
|
|
22592
22967
|
function creditsPerImage(params) {
|
|
22593
|
-
return
|
|
22968
|
+
return params.quality === PRO_QUALITY ? IMAGE_TIER_CREDITS.pro : imagePictureCredits(String(params.model));
|
|
22594
22969
|
}
|
|
22595
22970
|
function imagesPerCall(params) {
|
|
22596
22971
|
return "n" in params && typeof params.n === "number" ? params.n : 1;
|
|
@@ -22603,9 +22978,9 @@ var imageGenerateNode = delegated({
|
|
|
22603
22978
|
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.",
|
|
22604
22979
|
// `reference` is one image or an ordered array of images. The backend forwards
|
|
22605
22980
|
// each as a separate `image_url` to the provider (OpenRouter accepts many).
|
|
22606
|
-
inputs:
|
|
22981
|
+
inputs: z64.object({ reference: z64.union([ImageRef, z64.array(ImageRef).min(1)]).optional() }).loose(),
|
|
22607
22982
|
params: ImageGenerateParams,
|
|
22608
|
-
outputs:
|
|
22983
|
+
outputs: z64.object({ images: z64.array(ImageRef).min(1) }).strict(),
|
|
22609
22984
|
outputKinds: { images: "image" },
|
|
22610
22985
|
// Per image: `n` asks one call for several, and the provider bills each (measured: n=3
|
|
22611
22986
|
// cost exactly 3x n=1, prompt tokens included). Usage-billed, so this is only the pre-run
|
|
@@ -22617,31 +22992,27 @@ var imageGenerateNode = delegated({
|
|
|
22617
22992
|
});
|
|
22618
22993
|
|
|
22619
22994
|
// src/engine/nodes/remote/imageAspectAdapt.ts
|
|
22620
|
-
import { z as
|
|
22995
|
+
import { z as z65 } from "zod";
|
|
22621
22996
|
var ASPECT_ADAPT_MODELS = [
|
|
22622
22997
|
"google/gemini-3-pro-image",
|
|
22623
22998
|
"google/gemini-3.1-flash-image",
|
|
22624
22999
|
"google/gemini-3-pro-image-preview",
|
|
22625
23000
|
"google/gemini-3.1-flash-image-preview"
|
|
22626
23001
|
];
|
|
22627
|
-
var
|
|
22628
|
-
|
|
22629
|
-
"
|
|
22630
|
-
|
|
22631
|
-
|
|
22632
|
-
model: z64.enum(ASPECT_ADAPT_MODELS),
|
|
22633
|
-
formats: z64.array(z64.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }).optional(),
|
|
22634
|
-
platform: z64.enum(AD_FORMAT_PLATFORMS).optional(),
|
|
22635
|
-
guidance: z64.string().min(1).optional(),
|
|
23002
|
+
var ImageAspectAdaptParams = z65.object({
|
|
23003
|
+
model: z65.enum(ASPECT_ADAPT_MODELS),
|
|
23004
|
+
formats: z65.array(z65.enum(ASPECT_ADAPT_FORMATS)).min(1).max(6).refine((formats) => new Set(formats).size === formats.length, { message: "formats must be unique" }).optional(),
|
|
23005
|
+
platform: z65.enum(AD_FORMAT_PLATFORMS).optional(),
|
|
23006
|
+
guidance: z65.string().min(1).optional(),
|
|
22636
23007
|
// The exact lines the ad carries: the reframe keeps each, spelled as given, once.
|
|
22637
|
-
copy:
|
|
23008
|
+
copy: z65.array(z65.string().min(1)).min(1).max(20).optional(),
|
|
22638
23009
|
// Per target format, the fractions of each edge the platform's interface covers: words,
|
|
22639
23010
|
// logo and button stay inside. The static scaffold sets both from prompt.json and `staticSafeArea`.
|
|
22640
|
-
safe_areas:
|
|
22641
|
-
|
|
22642
|
-
|
|
23011
|
+
safe_areas: z65.record(
|
|
23012
|
+
z65.string(),
|
|
23013
|
+
z65.object({ top: z65.number(), bottom: z65.number(), left: z65.number(), right: z65.number() }).strict()
|
|
22643
23014
|
).optional(),
|
|
22644
|
-
image_size:
|
|
23015
|
+
image_size: z65.enum(OPENROUTER_IMAGE_SIZES).optional()
|
|
22645
23016
|
}).strict().refine((p) => p.formats === void 0 !== (p.platform === void 0), {
|
|
22646
23017
|
message: "provide exactly one of `formats` (explicit ratios) or `platform` (a placement preset)",
|
|
22647
23018
|
path: ["formats"]
|
|
@@ -22652,33 +23023,33 @@ var imageAspectAdaptNode = delegated({
|
|
|
22652
23023
|
category: "image",
|
|
22653
23024
|
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.",
|
|
22654
23025
|
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.",
|
|
22655
|
-
inputs:
|
|
23026
|
+
inputs: z65.object({ source: ImageRef }).loose(),
|
|
22656
23027
|
params: ImageAspectAdaptParams,
|
|
22657
|
-
outputs:
|
|
23028
|
+
outputs: z65.object({ images: z65.array(ImageRef).min(1) }).strict(),
|
|
22658
23029
|
outputKinds: { images: "image" },
|
|
22659
23030
|
cost: ({ params }) => {
|
|
22660
23031
|
const p = params;
|
|
22661
23032
|
const count = Math.max(1, resolveAdaptFormats(p ?? {}).length);
|
|
22662
23033
|
return {
|
|
22663
|
-
credits: count * (
|
|
23034
|
+
credits: count * imagePictureCredits(String(p?.model)),
|
|
22664
23035
|
seconds_estimate: 30 + 30 * count
|
|
22665
23036
|
};
|
|
22666
23037
|
}
|
|
22667
23038
|
});
|
|
22668
23039
|
|
|
22669
23040
|
// src/engine/nodes/remote/imageBackgroundRemove.ts
|
|
22670
|
-
import { z as
|
|
22671
|
-
var ImageBackgroundRemoveParams =
|
|
23041
|
+
import { z as z66 } from "zod";
|
|
23042
|
+
var ImageBackgroundRemoveParams = z66.object({
|
|
22672
23043
|
// `fal/birefnet-v2` is the legacy id stored canvases carry. It stays the default for one
|
|
22673
23044
|
// release: every backend accepts it, while `replicate/birefnet` needs the backend that ships with
|
|
22674
23045
|
// this CLI, and the two deploy independently.
|
|
22675
|
-
model:
|
|
22676
|
-
operating_resolution:
|
|
23046
|
+
model: z66.enum(modelIdsAccepted("replicate/birefnet")).optional().default("fal/birefnet-v2"),
|
|
23047
|
+
operating_resolution: z66.enum(["1024x1024", "2048x2048", "2304x2304"]).optional(),
|
|
22677
23048
|
// DEPRECATED, accepted so stored canvases still parse, and ignored: the model this
|
|
22678
23049
|
// node runs (`men1scus/birefnet`) takes only an image and a resolution, has no
|
|
22679
23050
|
// variant and returns no mask. `mask_only: true` still returns the cutout.
|
|
22680
|
-
model_variant:
|
|
22681
|
-
mask_only:
|
|
23051
|
+
model_variant: z66.enum(["General Use (Light)", "General Use (Heavy)", "Matting", "Portrait", "DIS", "HRSOD", "COD"]).optional().meta({ deprecated: true, description: "Ignored: the model has no variants." }),
|
|
23052
|
+
mask_only: z66.boolean().optional().meta({ deprecated: true, description: "Ignored: the model returns the cutout only, never a mask." })
|
|
22682
23053
|
}).strict();
|
|
22683
23054
|
var imageBackgroundRemoveNode = delegated({
|
|
22684
23055
|
id: "image_background_remove",
|
|
@@ -22686,11 +23057,11 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
22686
23057
|
category: "image",
|
|
22687
23058
|
summary: "Remove the background from an image and return a transparent PNG. Powered by BiRefNet (`men1scus/birefnet` on Replicate).",
|
|
22688
23059
|
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.",
|
|
22689
|
-
inputs:
|
|
23060
|
+
inputs: z66.object({
|
|
22690
23061
|
image: ImageRef
|
|
22691
23062
|
}).strict(),
|
|
22692
23063
|
params: ImageBackgroundRemoveParams,
|
|
22693
|
-
outputs:
|
|
23064
|
+
outputs: z66.object({ image: ImageRef }).strict(),
|
|
22694
23065
|
outputKinds: { image: "image" },
|
|
22695
23066
|
cost: ({ params }) => ({
|
|
22696
23067
|
credits: estimateNodeCredits({ nodeType: "image_background_remove", params }) ?? 1,
|
|
@@ -22699,7 +23070,7 @@ var imageBackgroundRemoveNode = delegated({
|
|
|
22699
23070
|
});
|
|
22700
23071
|
|
|
22701
23072
|
// src/engine/nodes/remote/imageDescribe.ts
|
|
22702
|
-
import { z as
|
|
23073
|
+
import { z as z67 } from "zod";
|
|
22703
23074
|
var IMAGE_DESCRIBE_MODELS = ["~google/gemini-pro-latest", "~google/gemini-flash-latest"];
|
|
22704
23075
|
var imageDescribeNode = delegated({
|
|
22705
23076
|
id: "image_describe",
|
|
@@ -22707,27 +23078,27 @@ var imageDescribeNode = delegated({
|
|
|
22707
23078
|
category: "vision",
|
|
22708
23079
|
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.",
|
|
22709
23080
|
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.',
|
|
22710
|
-
inputs:
|
|
22711
|
-
params:
|
|
22712
|
-
model:
|
|
22713
|
-
focus:
|
|
22714
|
-
context:
|
|
22715
|
-
temperature:
|
|
22716
|
-
max_tokens:
|
|
23081
|
+
inputs: z67.object({ image: ImageRef }).loose(),
|
|
23082
|
+
params: z67.object({
|
|
23083
|
+
model: z67.enum(IMAGE_DESCRIBE_MODELS),
|
|
23084
|
+
focus: z67.string().optional(),
|
|
23085
|
+
context: z67.string().optional(),
|
|
23086
|
+
temperature: z67.number().min(0).max(2).optional(),
|
|
23087
|
+
max_tokens: z67.number().int().positive().optional()
|
|
22717
23088
|
}).strict(),
|
|
22718
|
-
outputs:
|
|
23089
|
+
outputs: z67.object({ description: JsonRef }).strict(),
|
|
22719
23090
|
outputKinds: { description: "json" },
|
|
22720
23091
|
cost: () => ({ credits: 2, seconds_estimate: 10 })
|
|
22721
23092
|
});
|
|
22722
23093
|
|
|
22723
23094
|
// src/engine/nodes/remote/imageLayerize.ts
|
|
22724
|
-
import { z as
|
|
22725
|
-
var ImageLayerizeParams =
|
|
23095
|
+
import { z as z68 } from "zod";
|
|
23096
|
+
var ImageLayerizeParams = z68.object({
|
|
22726
23097
|
/**
|
|
22727
23098
|
* What to separate. Read as extraction instructions, not as a generation
|
|
22728
23099
|
* prompt — "keep the bottle and its shadow as one layer", not "make it blue".
|
|
22729
23100
|
*/
|
|
22730
|
-
instructions:
|
|
23101
|
+
instructions: z68.string().max(600).optional()
|
|
22731
23102
|
}).strict();
|
|
22732
23103
|
var imageLayerizeNode = delegated({
|
|
22733
23104
|
id: "image_layerize",
|
|
@@ -22735,12 +23106,12 @@ var imageLayerizeNode = delegated({
|
|
|
22735
23106
|
category: "image",
|
|
22736
23107
|
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.",
|
|
22737
23108
|
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.",
|
|
22738
|
-
inputs:
|
|
23109
|
+
inputs: z68.object({
|
|
22739
23110
|
image: ImageRef
|
|
22740
23111
|
}).strict(),
|
|
22741
23112
|
params: ImageLayerizeParams,
|
|
22742
|
-
outputs:
|
|
22743
|
-
layers:
|
|
23113
|
+
outputs: z68.object({
|
|
23114
|
+
layers: z68.array(ImageRef),
|
|
22744
23115
|
base: ImageRef,
|
|
22745
23116
|
manifest: JsonRef
|
|
22746
23117
|
}).strict(),
|
|
@@ -22751,28 +23122,24 @@ var imageLayerizeNode = delegated({
|
|
|
22751
23122
|
});
|
|
22752
23123
|
|
|
22753
23124
|
// src/engine/nodes/remote/imageReferenceSheet.ts
|
|
22754
|
-
import { z as
|
|
23125
|
+
import { z as z69 } from "zod";
|
|
22755
23126
|
var REFERENCE_SHEET_MODELS = [
|
|
22756
23127
|
"google/gemini-3-pro-image",
|
|
22757
23128
|
"google/gemini-3.1-flash-image",
|
|
22758
23129
|
"google/gemini-3-pro-image-preview",
|
|
22759
23130
|
"google/gemini-3.1-flash-image-preview"
|
|
22760
23131
|
];
|
|
22761
|
-
var
|
|
22762
|
-
|
|
22763
|
-
|
|
22764
|
-
]);
|
|
22765
|
-
var ImageReferenceSheetParams = z68.object({
|
|
22766
|
-
model: z68.enum(REFERENCE_SHEET_MODELS),
|
|
22767
|
-
subject_description: z68.string().min(1),
|
|
23132
|
+
var ImageReferenceSheetParams = z69.object({
|
|
23133
|
+
model: z69.enum(REFERENCE_SHEET_MODELS),
|
|
23134
|
+
subject_description: z69.string().min(1),
|
|
22768
23135
|
// `location` = a set/room shown from several camera ANGLES (not a rotated subject),
|
|
22769
23136
|
// so a multi-scene shoot keeps one consistent set.
|
|
22770
|
-
subject_type:
|
|
22771
|
-
views:
|
|
22772
|
-
style:
|
|
22773
|
-
prompt_override:
|
|
22774
|
-
aspect_ratio:
|
|
22775
|
-
image_size:
|
|
23137
|
+
subject_type: z69.enum(["character", "person", "product", "location"]),
|
|
23138
|
+
views: z69.array(z69.string().min(1)).min(2).max(8).optional(),
|
|
23139
|
+
style: z69.string().optional(),
|
|
23140
|
+
prompt_override: z69.string().min(1).optional(),
|
|
23141
|
+
aspect_ratio: z69.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(),
|
|
23142
|
+
image_size: z69.enum(OPENROUTER_IMAGE_SIZES).optional()
|
|
22776
23143
|
}).strict();
|
|
22777
23144
|
var imageReferenceSheetNode = delegated({
|
|
22778
23145
|
id: "image_reference_sheet",
|
|
@@ -22780,21 +23147,21 @@ var imageReferenceSheetNode = delegated({
|
|
|
22780
23147
|
category: "image",
|
|
22781
23148
|
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).",
|
|
22782
23149
|
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.",
|
|
22783
|
-
inputs:
|
|
23150
|
+
inputs: z69.object({ references: z69.array(ImageRef).min(1).max(6) }).loose(),
|
|
22784
23151
|
params: ImageReferenceSheetParams,
|
|
22785
|
-
outputs:
|
|
23152
|
+
outputs: z69.object({ sheet: ImageRef }).strict(),
|
|
22786
23153
|
outputKinds: { sheet: "image" },
|
|
22787
23154
|
cost: ({ params }) => ({
|
|
22788
|
-
credits:
|
|
23155
|
+
credits: imagePictureCredits(String(params?.model)),
|
|
22789
23156
|
seconds_estimate: 30
|
|
22790
23157
|
})
|
|
22791
23158
|
});
|
|
22792
23159
|
|
|
22793
23160
|
// src/engine/nodes/remote/imageSearch.ts
|
|
22794
|
-
import { z as
|
|
22795
|
-
var ImageSearchParams =
|
|
22796
|
-
prompt:
|
|
22797
|
-
count:
|
|
23161
|
+
import { z as z70 } from "zod";
|
|
23162
|
+
var ImageSearchParams = z70.object({
|
|
23163
|
+
prompt: z70.string().min(1),
|
|
23164
|
+
count: z70.number().int().min(1).max(20).default(5)
|
|
22798
23165
|
}).strict();
|
|
22799
23166
|
var imageSearchNode = delegated({
|
|
22800
23167
|
id: "image_search",
|
|
@@ -22802,15 +23169,15 @@ var imageSearchNode = delegated({
|
|
|
22802
23169
|
category: "image",
|
|
22803
23170
|
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.",
|
|
22804
23171
|
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.",
|
|
22805
|
-
inputs:
|
|
23172
|
+
inputs: z70.object({}).loose(),
|
|
22806
23173
|
params: ImageSearchParams,
|
|
22807
|
-
outputs:
|
|
23174
|
+
outputs: z70.object({ images: z70.array(ImageRef).min(1) }).strict(),
|
|
22808
23175
|
outputKinds: { images: "image" },
|
|
22809
23176
|
cost: ({ params }) => ({ credits: Math.ceil(2 + params.count / 2), seconds_estimate: 30 })
|
|
22810
23177
|
});
|
|
22811
23178
|
|
|
22812
23179
|
// src/engine/nodes/remote/imageSelect.ts
|
|
22813
|
-
import { z as
|
|
23180
|
+
import { z as z71 } from "zod";
|
|
22814
23181
|
var IMAGE_SELECT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
22815
23182
|
var imageSelectNode = delegated({
|
|
22816
23183
|
id: "image_select",
|
|
@@ -22818,16 +23185,16 @@ var imageSelectNode = delegated({
|
|
|
22818
23185
|
category: "vision",
|
|
22819
23186
|
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.",
|
|
22820
23187
|
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.",
|
|
22821
|
-
inputs:
|
|
22822
|
-
params:
|
|
22823
|
-
model:
|
|
22824
|
-
prompt:
|
|
22825
|
-
count:
|
|
22826
|
-
temperature:
|
|
22827
|
-
max_tokens:
|
|
23188
|
+
inputs: z71.object({ images: z71.array(ImageRef).min(1) }).loose(),
|
|
23189
|
+
params: z71.object({
|
|
23190
|
+
model: z71.enum(IMAGE_SELECT_MODELS),
|
|
23191
|
+
prompt: z71.string().min(1),
|
|
23192
|
+
count: z71.number().int().min(1).default(1),
|
|
23193
|
+
temperature: z71.number().min(0).max(2).optional(),
|
|
23194
|
+
max_tokens: z71.number().int().positive().optional()
|
|
22828
23195
|
}).strict(),
|
|
22829
23196
|
// No `reasoning` when a lone candidate passed through: there was no judgement to explain.
|
|
22830
|
-
outputs:
|
|
23197
|
+
outputs: z71.object({ images: z71.array(ImageRef).min(1), reasoning: TextRef.optional() }).strict(),
|
|
22831
23198
|
outputKinds: { images: "image", reasoning: "text" },
|
|
22832
23199
|
cost: () => ({ credits: 1, seconds_estimate: 5 }),
|
|
22833
23200
|
// Arity is only knowable at validate time when `images` is a literal array
|
|
@@ -22852,26 +23219,26 @@ var imageSelectNode = delegated({
|
|
|
22852
23219
|
});
|
|
22853
23220
|
|
|
22854
23221
|
// src/engine/nodes/remote/music.ts
|
|
22855
|
-
import { z as
|
|
23222
|
+
import { z as z72 } from "zod";
|
|
22856
23223
|
var MUSIC_MODELS = ["elevenlabs/music-v1", "elevenlabs/video-background-music-v1"];
|
|
22857
|
-
var MusicParams =
|
|
22858
|
-
model:
|
|
23224
|
+
var MusicParams = z72.object({
|
|
23225
|
+
model: z72.enum(MUSIC_MODELS),
|
|
22859
23226
|
/** Free-form prompt. Used by `elevenlabs/music-v1` (compose-detailed). */
|
|
22860
|
-
prompt:
|
|
23227
|
+
prompt: z72.string().optional(),
|
|
22861
23228
|
/**
|
|
22862
23229
|
* Structured composition plan (intro / hook / verse / outro sections with
|
|
22863
23230
|
* per-section styles + durations). Mutually exclusive with `prompt`.
|
|
22864
23231
|
*/
|
|
22865
|
-
composition_plan:
|
|
23232
|
+
composition_plan: z72.record(z72.string(), z72.unknown()).optional(),
|
|
22866
23233
|
/** Target length when using `prompt`. 3000–454545ms (capped by the $10 per-node cost limit). */
|
|
22867
|
-
music_length_ms:
|
|
22868
|
-
seed:
|
|
23234
|
+
music_length_ms: z72.number().int().min(3e3).max(ELEVENLABS_MAX_MUSIC_LENGTH_MS).optional(),
|
|
23235
|
+
seed: z72.number().int().optional(),
|
|
22869
23236
|
/** Prompt mode only — forces an instrumental (no vocals) track. */
|
|
22870
|
-
force_instrumental:
|
|
23237
|
+
force_instrumental: z72.boolean().optional(),
|
|
22871
23238
|
/** composition_plan only — honor exact section durations. */
|
|
22872
|
-
respect_sections_durations:
|
|
23239
|
+
respect_sections_durations: z72.boolean().optional(),
|
|
22873
23240
|
/** Emit word-level timestamps alongside the audio. */
|
|
22874
|
-
with_timestamps:
|
|
23241
|
+
with_timestamps: z72.boolean().optional(),
|
|
22875
23242
|
/**
|
|
22876
23243
|
* video-to-music only — short description of the desired score
|
|
22877
23244
|
* ("upbeat synth, fast cuts, 80s") used to bias the model.
|
|
@@ -22883,14 +23250,14 @@ var MusicParams = z71.object({
|
|
|
22883
23250
|
* confident") came back as a film score and the client's verdict was "la
|
|
22884
23251
|
* musica que has elegido es muy dramática".
|
|
22885
23252
|
*/
|
|
22886
|
-
description:
|
|
23253
|
+
description: z72.string().max(1e3).optional().describe(
|
|
22887
23254
|
"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."
|
|
22888
23255
|
),
|
|
22889
23256
|
/** video-to-music only — up to 10 style tags. */
|
|
22890
|
-
tags:
|
|
23257
|
+
tags: z72.array(z72.string()).max(10).optional().describe(
|
|
22891
23258
|
"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)."
|
|
22892
23259
|
),
|
|
22893
|
-
output_format:
|
|
23260
|
+
output_format: z72.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
22894
23261
|
}).strict();
|
|
22895
23262
|
var musicNode = delegated({
|
|
22896
23263
|
id: "music",
|
|
@@ -22901,9 +23268,9 @@ var musicNode = delegated({
|
|
|
22901
23268
|
category: "audio",
|
|
22902
23269
|
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`.",
|
|
22903
23270
|
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.",
|
|
22904
|
-
inputs:
|
|
23271
|
+
inputs: z72.object({ video: VideoRef.optional() }).loose(),
|
|
22905
23272
|
params: MusicParams,
|
|
22906
|
-
outputs:
|
|
23273
|
+
outputs: z72.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
22907
23274
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
22908
23275
|
// Priced by the shared contract: per second of audio, 30s when no length is pinned.
|
|
22909
23276
|
cost: ({ params }) => {
|
|
@@ -22935,25 +23302,25 @@ var musicNode = delegated({
|
|
|
22935
23302
|
});
|
|
22936
23303
|
|
|
22937
23304
|
// src/engine/nodes/remote/soundEffect.ts
|
|
22938
|
-
import { z as
|
|
23305
|
+
import { z as z73 } from "zod";
|
|
22939
23306
|
var SOUND_EFFECT_MODELS = ["elevenlabs/eleven_text_to_sound_v2"];
|
|
22940
|
-
var SoundEffectParams =
|
|
22941
|
-
model:
|
|
23307
|
+
var SoundEffectParams = z73.object({
|
|
23308
|
+
model: z73.enum(SOUND_EFFECT_MODELS),
|
|
22942
23309
|
/** Prompt describing the SFX ("metal door slam", "soft UI tap", "ocean waves"). */
|
|
22943
|
-
text:
|
|
23310
|
+
text: z73.string().min(1),
|
|
22944
23311
|
/**
|
|
22945
23312
|
* Target length in seconds. 0.5–30. Leave unset to let the model pick the
|
|
22946
23313
|
* natural length for the described effect.
|
|
22947
23314
|
*/
|
|
22948
|
-
duration_seconds:
|
|
23315
|
+
duration_seconds: z73.number().min(0.5).max(30).optional(),
|
|
22949
23316
|
/**
|
|
22950
23317
|
* 0–1. Higher = stick closer to the prompt at the cost of variety; lower
|
|
22951
23318
|
* = let the model interpret more freely. Defaults to 0.3 on the provider.
|
|
22952
23319
|
*/
|
|
22953
|
-
prompt_influence:
|
|
23320
|
+
prompt_influence: z73.number().min(0).max(1).optional(),
|
|
22954
23321
|
/** Only valid on `eleven_text_to_sound_v2` — produce a seamless loop. */
|
|
22955
|
-
loop:
|
|
22956
|
-
output_format:
|
|
23322
|
+
loop: z73.boolean().optional(),
|
|
23323
|
+
output_format: z73.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
|
|
22957
23324
|
}).strict();
|
|
22958
23325
|
var soundEffectNode = delegated({
|
|
22959
23326
|
id: "sound_effect",
|
|
@@ -22961,9 +23328,9 @@ var soundEffectNode = delegated({
|
|
|
22961
23328
|
category: "audio",
|
|
22962
23329
|
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.",
|
|
22963
23330
|
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.",
|
|
22964
|
-
inputs:
|
|
23331
|
+
inputs: z73.object({}).loose(),
|
|
22965
23332
|
params: SoundEffectParams,
|
|
22966
|
-
outputs:
|
|
23333
|
+
outputs: z73.object({ audio: AudioRef }).strict(),
|
|
22967
23334
|
outputKinds: { audio: "audio" },
|
|
22968
23335
|
// Priced by the shared contract: per second of audio, 5s when the model picks the length.
|
|
22969
23336
|
cost: ({ params }) => {
|
|
@@ -22973,7 +23340,7 @@ var soundEffectNode = delegated({
|
|
|
22973
23340
|
});
|
|
22974
23341
|
|
|
22975
23342
|
// src/engine/nodes/remote/textGenerate.ts
|
|
22976
|
-
import { z as
|
|
23343
|
+
import { z as z74 } from "zod";
|
|
22977
23344
|
var TEXT_GENERATE_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
22978
23345
|
var textGenerateNode = delegated({
|
|
22979
23346
|
id: "text_generate",
|
|
@@ -22981,58 +23348,58 @@ var textGenerateNode = delegated({
|
|
|
22981
23348
|
category: "language",
|
|
22982
23349
|
summary: "Single-turn LLM text generation via OpenRouter. Returns a text response.",
|
|
22983
23350
|
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.',
|
|
22984
|
-
inputs:
|
|
22985
|
-
params:
|
|
22986
|
-
model:
|
|
22987
|
-
prompt:
|
|
22988
|
-
system:
|
|
22989
|
-
response_format:
|
|
22990
|
-
web_search:
|
|
22991
|
-
temperature:
|
|
22992
|
-
max_tokens:
|
|
23351
|
+
inputs: z74.object({}).loose(),
|
|
23352
|
+
params: z74.object({
|
|
23353
|
+
model: z74.enum(TEXT_GENERATE_MODELS),
|
|
23354
|
+
prompt: z74.string().min(1),
|
|
23355
|
+
system: z74.string().optional(),
|
|
23356
|
+
response_format: z74.enum(["text", "json_object"]).optional(),
|
|
23357
|
+
web_search: z74.boolean().optional(),
|
|
23358
|
+
temperature: z74.number().min(0).max(2).optional(),
|
|
23359
|
+
max_tokens: z74.number().int().positive().optional()
|
|
22993
23360
|
}).strict(),
|
|
22994
|
-
outputs:
|
|
23361
|
+
outputs: z74.object({ text: TextRef }).strict(),
|
|
22995
23362
|
outputKinds: { text: "text" },
|
|
22996
23363
|
cost: () => ({ credits: 1, seconds_estimate: 3 })
|
|
22997
23364
|
});
|
|
22998
23365
|
|
|
22999
23366
|
// src/engine/nodes/remote/tts.ts
|
|
23000
|
-
import { z as
|
|
23367
|
+
import { z as z75 } from "zod";
|
|
23001
23368
|
var TTS_MODELS = ["elevenlabs/eleven_v3"];
|
|
23002
|
-
var TtsVoiceSettings =
|
|
23003
|
-
stability:
|
|
23004
|
-
similarity_boost:
|
|
23005
|
-
style:
|
|
23006
|
-
use_speaker_boost:
|
|
23007
|
-
speed:
|
|
23369
|
+
var TtsVoiceSettings = z75.object({
|
|
23370
|
+
stability: z75.number().min(0).max(1).optional(),
|
|
23371
|
+
similarity_boost: z75.number().min(0).max(1).optional(),
|
|
23372
|
+
style: z75.number().min(0).max(1).optional(),
|
|
23373
|
+
use_speaker_boost: z75.boolean().optional(),
|
|
23374
|
+
speed: z75.number().min(0.25).max(4).optional()
|
|
23008
23375
|
}).strict();
|
|
23009
|
-
var TtsPronunciationLocator =
|
|
23010
|
-
pronunciation_dictionary_id:
|
|
23011
|
-
version_id:
|
|
23376
|
+
var TtsPronunciationLocator = z75.object({
|
|
23377
|
+
pronunciation_dictionary_id: z75.string().min(1),
|
|
23378
|
+
version_id: z75.string().nullable().optional()
|
|
23012
23379
|
}).strict();
|
|
23013
|
-
var TtsParams =
|
|
23014
|
-
model:
|
|
23015
|
-
text:
|
|
23016
|
-
voice:
|
|
23380
|
+
var TtsParams = z75.object({
|
|
23381
|
+
model: z75.enum(TTS_MODELS),
|
|
23382
|
+
text: z75.string().min(1).max(ELEVENLABS_MAX_TEXT_CHARS),
|
|
23383
|
+
voice: z75.string().min(1),
|
|
23017
23384
|
/** Provider output_format (mp3 family only — assets are stored as audio/mpeg). */
|
|
23018
|
-
output_format:
|
|
23019
|
-
seed:
|
|
23385
|
+
output_format: z75.enum(ELEVENLABS_OUTPUT_FORMATS).optional(),
|
|
23386
|
+
seed: z75.number().int().min(0).max(4294967295).optional(),
|
|
23020
23387
|
// Top-level shortcuts; structured form is `voice_settings`.
|
|
23021
|
-
stability:
|
|
23022
|
-
similarity_boost:
|
|
23388
|
+
stability: z75.number().min(0).max(1).optional(),
|
|
23389
|
+
similarity_boost: z75.number().min(0).max(1).optional(),
|
|
23023
23390
|
voice_settings: TtsVoiceSettings.optional(),
|
|
23024
23391
|
/** ISO 639-1 language code. eleven_v3 supports language hints. */
|
|
23025
|
-
language_code:
|
|
23026
|
-
pronunciation_dictionary_locators:
|
|
23027
|
-
apply_text_normalization:
|
|
23392
|
+
language_code: z75.string().optional(),
|
|
23393
|
+
pronunciation_dictionary_locators: z75.array(TtsPronunciationLocator).max(3).optional(),
|
|
23394
|
+
apply_text_normalization: z75.enum(["auto", "on", "off"]).optional(),
|
|
23028
23395
|
/** Currently Japanese-only. Adds latency. */
|
|
23029
|
-
apply_language_text_normalization:
|
|
23396
|
+
apply_language_text_normalization: z75.boolean().optional(),
|
|
23030
23397
|
/**
|
|
23031
23398
|
* When true, hits `/v1/text-to-speech/{voice_id}/with-timestamps` and
|
|
23032
23399
|
* adds a `timestamps` output (character-level alignment) for caption
|
|
23033
23400
|
* rendering, lipsync, and beat-matched cuts.
|
|
23034
23401
|
*/
|
|
23035
|
-
with_timestamps:
|
|
23402
|
+
with_timestamps: z75.boolean().optional()
|
|
23036
23403
|
}).strict();
|
|
23037
23404
|
var ttsNode = delegated({
|
|
23038
23405
|
id: "tts",
|
|
@@ -23040,9 +23407,9 @@ var ttsNode = delegated({
|
|
|
23040
23407
|
category: "audio",
|
|
23041
23408
|
summary: "Single-voice text-to-speech via ElevenLabs Eleven v3. Optional character-level timestamps for caption rendering and beat-matched cuts.",
|
|
23042
23409
|
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).",
|
|
23043
|
-
inputs:
|
|
23410
|
+
inputs: z75.object({}).loose(),
|
|
23044
23411
|
params: TtsParams,
|
|
23045
|
-
outputs:
|
|
23412
|
+
outputs: z75.object({ audio: AudioRef, timestamps: JsonRef.optional() }).strict(),
|
|
23046
23413
|
outputKinds: { audio: "audio", timestamps: "json" },
|
|
23047
23414
|
// Priced by the shared contract, the same per-character rate the backend charges.
|
|
23048
23415
|
cost: ({ params }) => ({
|
|
@@ -23052,11 +23419,11 @@ var ttsNode = delegated({
|
|
|
23052
23419
|
});
|
|
23053
23420
|
|
|
23054
23421
|
// src/engine/nodes/remote/video.ts
|
|
23055
|
-
import { z as
|
|
23056
|
-
var videoModelEnum =
|
|
23057
|
-
var VideoGenerateParams =
|
|
23422
|
+
import { z as z76 } from "zod";
|
|
23423
|
+
var videoModelEnum = z76.enum(VIDEO_GENERATE_MODELS);
|
|
23424
|
+
var VideoGenerateParams = z76.object({
|
|
23058
23425
|
model: videoModelEnum,
|
|
23059
|
-
...registryParamShape("video_generate", { prompt:
|
|
23426
|
+
...registryParamShape("video_generate", { prompt: z76.string().min(1) })
|
|
23060
23427
|
}).strict();
|
|
23061
23428
|
var videoGenerateNode = delegated({
|
|
23062
23429
|
id: "video_generate",
|
|
@@ -23064,7 +23431,7 @@ var videoGenerateNode = delegated({
|
|
|
23064
23431
|
category: "video",
|
|
23065
23432
|
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.",
|
|
23066
23433
|
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.",
|
|
23067
|
-
inputs:
|
|
23434
|
+
inputs: z76.object({
|
|
23068
23435
|
first_frame: ImageRef.optional(),
|
|
23069
23436
|
last_frame: ImageRef.optional(),
|
|
23070
23437
|
reference: ImageRef.optional(),
|
|
@@ -23074,10 +23441,10 @@ var videoGenerateNode = delegated({
|
|
|
23074
23441
|
* exclusive with `first_frame` — OpenRouter treats a request carrying both
|
|
23075
23442
|
* as image-to-video and ignores these, so wire one or the other.
|
|
23076
23443
|
*/
|
|
23077
|
-
references:
|
|
23444
|
+
references: z76.array(ImageRef).optional()
|
|
23078
23445
|
}).loose(),
|
|
23079
23446
|
params: VideoGenerateParams,
|
|
23080
|
-
outputs:
|
|
23447
|
+
outputs: z76.object({ video: VideoRef }).strict(),
|
|
23081
23448
|
outputKinds: { video: "video" },
|
|
23082
23449
|
// Priced from the shared contract, so `validate`'s quote and the charge the
|
|
23083
23450
|
// backend applies come from ONE table. A flat number here under-quoted a
|
|
@@ -23092,15 +23459,15 @@ var videoGenerateNode = delegated({
|
|
|
23092
23459
|
});
|
|
23093
23460
|
|
|
23094
23461
|
// src/engine/nodes/remote/videoBackgroundRemove.ts
|
|
23095
|
-
import { z as
|
|
23096
|
-
var VideoBackgroundRemoveParams =
|
|
23462
|
+
import { z as z77 } from "zod";
|
|
23463
|
+
var VideoBackgroundRemoveParams = z77.object({
|
|
23097
23464
|
// `fal/veed-video-background-removal` is the legacy id stored canvases carry. It stays the default for one
|
|
23098
23465
|
// release: every backend accepts it, while `replicate/birefnet-video` needs the backend that ships with
|
|
23099
23466
|
// this CLI, and the two deploy independently.
|
|
23100
|
-
model:
|
|
23467
|
+
model: z77.enum(modelIdsAccepted("replicate/birefnet-video")).optional().default("fal/veed-video-background-removal"),
|
|
23101
23468
|
// Refines the matte's soft edges against background bleed (the model's `refine_fg`).
|
|
23102
|
-
edge_refinement:
|
|
23103
|
-
output_codec:
|
|
23469
|
+
edge_refinement: z77.boolean().optional().default(true),
|
|
23470
|
+
output_codec: z77.enum(["vp9", "h264"]).optional().default("vp9")
|
|
23104
23471
|
}).strict();
|
|
23105
23472
|
var videoBackgroundRemoveNode = delegated({
|
|
23106
23473
|
id: "video_background_remove",
|
|
@@ -23108,11 +23475,11 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
23108
23475
|
category: "video",
|
|
23109
23476
|
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).",
|
|
23110
23477
|
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.",
|
|
23111
|
-
inputs:
|
|
23478
|
+
inputs: z77.object({
|
|
23112
23479
|
video: VideoRef
|
|
23113
23480
|
}).strict(),
|
|
23114
23481
|
params: VideoBackgroundRemoveParams,
|
|
23115
|
-
outputs:
|
|
23482
|
+
outputs: z77.object({ video: VideoRef }).strict(),
|
|
23116
23483
|
outputKinds: { video: "video" },
|
|
23117
23484
|
// Priced by the shared contract per 30 frames; the quote assumes the same 30s of
|
|
23118
23485
|
// footage the charge falls back to when the clip carries no duration.
|
|
@@ -23123,7 +23490,7 @@ var videoBackgroundRemoveNode = delegated({
|
|
|
23123
23490
|
});
|
|
23124
23491
|
|
|
23125
23492
|
// src/engine/nodes/remote/videoDeconstruct.ts
|
|
23126
|
-
import { z as
|
|
23493
|
+
import { z as z78 } from "zod";
|
|
23127
23494
|
var VIDEO_DECONSTRUCT_MODELS = ["~google/gemini-flash-latest", "~google/gemini-pro-latest"];
|
|
23128
23495
|
var videoDeconstructNode = delegated({
|
|
23129
23496
|
id: "video_deconstruct",
|
|
@@ -23131,34 +23498,34 @@ var videoDeconstructNode = delegated({
|
|
|
23131
23498
|
category: "video",
|
|
23132
23499
|
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).',
|
|
23133
23500
|
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.',
|
|
23134
|
-
inputs:
|
|
23135
|
-
params:
|
|
23136
|
-
model:
|
|
23137
|
-
mode:
|
|
23138
|
-
language:
|
|
23139
|
-
max_scenes:
|
|
23140
|
-
focus:
|
|
23141
|
-
start_s:
|
|
23142
|
-
end_s:
|
|
23501
|
+
inputs: z78.object({ video: VideoRef }).loose(),
|
|
23502
|
+
params: z78.object({
|
|
23503
|
+
model: z78.enum(VIDEO_DECONSTRUCT_MODELS),
|
|
23504
|
+
mode: z78.enum(["full", "index"]).optional(),
|
|
23505
|
+
language: z78.string().min(2).max(8).optional(),
|
|
23506
|
+
max_scenes: z78.number().int().min(1).max(60).optional(),
|
|
23507
|
+
focus: z78.string().optional(),
|
|
23508
|
+
start_s: z78.number().min(0).optional(),
|
|
23509
|
+
end_s: z78.number().positive().optional(),
|
|
23143
23510
|
// Real visual shot-cut timestamps (absolute seconds), detected locally with
|
|
23144
23511
|
// ffmpeg before the deconstruct. The backend SNAPS its LLM scene boundaries
|
|
23145
23512
|
// onto these and SPLITS any scene that spans one, so a scene's frames never
|
|
23146
23513
|
// straddle a hard cut. `scaffold-video` populates this; omit for LLM-only cuts.
|
|
23147
|
-
shot_cuts:
|
|
23514
|
+
shot_cuts: z78.array(z78.number().min(0)).max(200).optional(),
|
|
23148
23515
|
// The video model's per-clip ceiling (seconds). A shot longer than this is
|
|
23149
23516
|
// split into seamless continuation sub-scenes (shared splice frame), so long
|
|
23150
23517
|
// shots reproduce in full instead of being truncated. `scaffold-video` sets
|
|
23151
23518
|
// the Seedance ceiling (15); omit to disable length splitting.
|
|
23152
|
-
max_clip_s:
|
|
23519
|
+
max_clip_s: z78.number().positive().max(60).optional(),
|
|
23153
23520
|
// Transcript provider for the blueprint's dialogue/transcript. Default
|
|
23154
23521
|
// Groq Whisper; "deepgram" routes to Nova-3 so words carry punctuation.
|
|
23155
|
-
transcriber:
|
|
23522
|
+
transcriber: z78.enum(["groq", "deepgram"]).optional()
|
|
23156
23523
|
}).strict(),
|
|
23157
|
-
outputs:
|
|
23524
|
+
outputs: z78.object({
|
|
23158
23525
|
analysis: JsonRef,
|
|
23159
23526
|
// Absent in mode:"index" (structure only, no Mux frame extraction).
|
|
23160
|
-
start_frames:
|
|
23161
|
-
end_frames:
|
|
23527
|
+
start_frames: z78.array(ImageRef).min(1).optional(),
|
|
23528
|
+
end_frames: z78.array(ImageRef).min(1).optional(),
|
|
23162
23529
|
transcript: JsonRef
|
|
23163
23530
|
}).strict(),
|
|
23164
23531
|
outputKinds: { analysis: "json", start_frames: "image", end_frames: "image", transcript: "json" },
|
|
@@ -23166,21 +23533,21 @@ var videoDeconstructNode = delegated({
|
|
|
23166
23533
|
});
|
|
23167
23534
|
|
|
23168
23535
|
// src/engine/nodes/remote/videoLipsync.ts
|
|
23169
|
-
import { z as
|
|
23170
|
-
var VideoLipsyncParams =
|
|
23171
|
-
model:
|
|
23536
|
+
import { z as z79 } from "zod";
|
|
23537
|
+
var VideoLipsyncParams = z79.object({
|
|
23538
|
+
model: z79.enum(modelIdsAccepted("replicate/sync-lipsync-2"))
|
|
23172
23539
|
}).strict();
|
|
23173
23540
|
var videoLipsyncNode = delegated({
|
|
23174
23541
|
id: "video_lipsync",
|
|
23175
23542
|
version: "1.0.0",
|
|
23176
23543
|
category: "video",
|
|
23177
23544
|
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.",
|
|
23178
|
-
inputs:
|
|
23545
|
+
inputs: z79.object({
|
|
23179
23546
|
video: VideoRef,
|
|
23180
23547
|
audio: AudioRef
|
|
23181
23548
|
}).strict(),
|
|
23182
23549
|
params: VideoLipsyncParams,
|
|
23183
|
-
outputs:
|
|
23550
|
+
outputs: z79.object({ video: VideoRef }).strict(),
|
|
23184
23551
|
outputKinds: { video: "video" },
|
|
23185
23552
|
// Priced by the shared contract per minute of output; the quote cannot see the
|
|
23186
23553
|
// clips' lengths, so it assumes the same one minute the charge falls back to.
|
|
@@ -23194,7 +23561,7 @@ var videoLipsyncNode = delegated({
|
|
|
23194
23561
|
import { mkdtemp as mkdtemp8, readFile as readFile17, rm as rm8 } from "fs/promises";
|
|
23195
23562
|
import { tmpdir as tmpdir8 } from "os";
|
|
23196
23563
|
import path21 from "path";
|
|
23197
|
-
import { z as
|
|
23564
|
+
import { z as z80 } from "zod";
|
|
23198
23565
|
|
|
23199
23566
|
// src/engine/nodes/local/lib/ffmpeg.ts
|
|
23200
23567
|
import { execFile as execFile8 } from "child_process";
|
|
@@ -23432,30 +23799,30 @@ function timeScriptWords(script, transcript, language) {
|
|
|
23432
23799
|
}
|
|
23433
23800
|
|
|
23434
23801
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
23435
|
-
var VideoTranscribeParams =
|
|
23436
|
-
language:
|
|
23802
|
+
var VideoTranscribeParams = z80.object({
|
|
23803
|
+
language: z80.string().min(2).max(8).optional(),
|
|
23437
23804
|
// Provider choice is explicit (no env-based silent branching). Default Groq
|
|
23438
23805
|
// Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
|
|
23439
23806
|
// `rich` JSON output with punctuated words + paragraph/sentence grouping.
|
|
23440
|
-
transcriber:
|
|
23441
|
-
keyterms:
|
|
23807
|
+
transcriber: z80.enum(["groq", "deepgram"]).optional(),
|
|
23808
|
+
keyterms: z80.array(z80.string().min(2).max(60)).max(30).optional().describe(
|
|
23442
23809
|
"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."
|
|
23443
23810
|
),
|
|
23444
|
-
script:
|
|
23811
|
+
script: z80.string().min(1).optional().describe(
|
|
23445
23812
|
"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."
|
|
23446
23813
|
)
|
|
23447
23814
|
}).strict();
|
|
23448
|
-
var VideoTranscribeInputs =
|
|
23815
|
+
var VideoTranscribeInputs = z80.object({
|
|
23449
23816
|
// A video (audio auto-extracted locally) OR a bare audio track. The key stays
|
|
23450
23817
|
// `video` for back-compat; the backend already accepts audio-kind refs on it —
|
|
23451
23818
|
// the local extraction path has been shipping one for every video input.
|
|
23452
|
-
video:
|
|
23819
|
+
video: z80.union([VideoRef, AudioRef])
|
|
23453
23820
|
}).strict();
|
|
23454
|
-
var VideoTranscribeOutputs =
|
|
23455
|
-
transcript:
|
|
23821
|
+
var VideoTranscribeOutputs = z80.object({
|
|
23822
|
+
transcript: z80.custom(),
|
|
23456
23823
|
// Only emitted by the Deepgram path: full punctuated words + paragraph /
|
|
23457
23824
|
// sentence grouping with speaker indices. Absent for the default Groq path.
|
|
23458
|
-
rich:
|
|
23825
|
+
rich: z80.custom().optional()
|
|
23459
23826
|
}).strict();
|
|
23460
23827
|
var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
|
|
23461
23828
|
var videoTranscribeNode = defineNode({
|
|
@@ -23568,7 +23935,7 @@ async function tryExtractAudio(inputs, ctx) {
|
|
|
23568
23935
|
}
|
|
23569
23936
|
|
|
23570
23937
|
// src/engine/nodes/remote/voiceSelect.ts
|
|
23571
|
-
import { z as
|
|
23938
|
+
import { z as z81 } from "zod";
|
|
23572
23939
|
var voiceSelectNode = delegated({
|
|
23573
23940
|
id: "voice_select",
|
|
23574
23941
|
// 1.1.0: a pinned voice used to come back as an inline `{ kind, text }` with no sha256
|
|
@@ -23586,27 +23953,27 @@ var voiceSelectNode = delegated({
|
|
|
23586
23953
|
category: "audio",
|
|
23587
23954
|
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.',
|
|
23588
23955
|
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.',
|
|
23589
|
-
inputs:
|
|
23590
|
-
params:
|
|
23591
|
-
description:
|
|
23592
|
-
gender:
|
|
23593
|
-
age:
|
|
23594
|
-
accent:
|
|
23595
|
-
language:
|
|
23956
|
+
inputs: z81.object({}).loose(),
|
|
23957
|
+
params: z81.object({
|
|
23958
|
+
description: z81.string().min(1),
|
|
23959
|
+
gender: z81.string().optional(),
|
|
23960
|
+
age: z81.string().optional(),
|
|
23961
|
+
accent: z81.string().optional(),
|
|
23962
|
+
language: z81.string().optional(),
|
|
23596
23963
|
/**
|
|
23597
23964
|
* A voice already chosen — an Avatar's pinned one. Given, nothing is cast: the
|
|
23598
23965
|
* ranking is skipped and this id is emitted as-is.
|
|
23599
23966
|
*/
|
|
23600
|
-
voice_id:
|
|
23601
|
-
limit:
|
|
23967
|
+
voice_id: z81.string().min(1).max(120).optional(),
|
|
23968
|
+
limit: z81.number().int().min(1).max(20).optional()
|
|
23602
23969
|
}).strict(),
|
|
23603
|
-
outputs:
|
|
23970
|
+
outputs: z81.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
|
|
23604
23971
|
outputKinds: { voice_id: "text", candidates: "json" },
|
|
23605
23972
|
cost: () => ({ credits: 0, seconds_estimate: 5 })
|
|
23606
23973
|
});
|
|
23607
23974
|
|
|
23608
23975
|
// src/engine/schema/catalog.ts
|
|
23609
|
-
import { z as
|
|
23976
|
+
import { z as z82 } from "zod";
|
|
23610
23977
|
function generateCatalog(registry, opts = {}) {
|
|
23611
23978
|
const entries2 = registry.all().map((def) => {
|
|
23612
23979
|
const cost = catalogCost(def);
|
|
@@ -23617,9 +23984,9 @@ function generateCatalog(registry, opts = {}) {
|
|
|
23617
23984
|
summary: def.summary,
|
|
23618
23985
|
when_to_use: def.when_to_use,
|
|
23619
23986
|
location: def.location,
|
|
23620
|
-
inputs:
|
|
23621
|
-
params:
|
|
23622
|
-
outputs:
|
|
23987
|
+
inputs: z82.toJSONSchema(def.inputs, { unrepresentable: "any" }),
|
|
23988
|
+
params: z82.toJSONSchema(def.params, { unrepresentable: "any" }),
|
|
23989
|
+
outputs: z82.toJSONSchema(def.outputs, { unrepresentable: "any" }),
|
|
23623
23990
|
cost_estimate_credits: cost?.credits,
|
|
23624
23991
|
runtime_estimate_seconds: cost?.seconds_estimate
|
|
23625
23992
|
};
|
|
@@ -23768,7 +24135,8 @@ var LOCAL_NODES = [
|
|
|
23768
24135
|
videoConcatNode,
|
|
23769
24136
|
avMuxNode,
|
|
23770
24137
|
collectNode,
|
|
23771
|
-
staticPrefilterNode
|
|
24138
|
+
staticPrefilterNode,
|
|
24139
|
+
staticSafeFitNode
|
|
23772
24140
|
];
|
|
23773
24141
|
var REMOTE_NODES = [
|
|
23774
24142
|
textGenerateNode,
|
|
@@ -23926,6 +24294,7 @@ export {
|
|
|
23926
24294
|
testimonialSourcesRemoveRequestSchema,
|
|
23927
24295
|
testimonialSourcesScheduleRequestSchema,
|
|
23928
24296
|
videosIngestRequestSchema,
|
|
24297
|
+
HttpClient,
|
|
23929
24298
|
BackendClient,
|
|
23930
24299
|
requireCredentialsFromEnv,
|
|
23931
24300
|
RunAbortedError,
|
|
@@ -23977,6 +24346,7 @@ export {
|
|
|
23977
24346
|
supportedAspectRatios,
|
|
23978
24347
|
nearestSupportedAspectRatio,
|
|
23979
24348
|
estimateVideoCredits,
|
|
24349
|
+
candidPersonPortraitPrompt,
|
|
23980
24350
|
VideoBlueprintSchema,
|
|
23981
24351
|
BlueprintOverlaySchema,
|
|
23982
24352
|
BlueprintFloatingElementSchema,
|
|
@@ -24014,7 +24384,7 @@ export {
|
|
|
24014
24384
|
AD_SPEC_SCHEMA,
|
|
24015
24385
|
AdSpecSchema,
|
|
24016
24386
|
readAdSpec,
|
|
24017
|
-
|
|
24387
|
+
withRenderedLettering,
|
|
24018
24388
|
conceptClassOf,
|
|
24019
24389
|
plainText,
|
|
24020
24390
|
adSpecFromBlueprint,
|
|
@@ -24035,6 +24405,7 @@ export {
|
|
|
24035
24405
|
STATIC_MASTER_ROLE,
|
|
24036
24406
|
STATIC_SIZE_ROLE,
|
|
24037
24407
|
STATIC_ADAPT_ROLE,
|
|
24408
|
+
STATIC_SAFE_FIT_ROLE,
|
|
24038
24409
|
LOGO_COMPOSITE_ROLE,
|
|
24039
24410
|
STATIC_FIX_ROLE,
|
|
24040
24411
|
LOGO_COMPOSITE_DEFAULT,
|
|
@@ -24043,6 +24414,7 @@ export {
|
|
|
24043
24414
|
formatSlug,
|
|
24044
24415
|
staticMasterFormat,
|
|
24045
24416
|
staticRenderNodes,
|
|
24417
|
+
safeFitNode,
|
|
24046
24418
|
RENDER_GRAPH_IDS,
|
|
24047
24419
|
regenerateTargets,
|
|
24048
24420
|
elementMentionKeywords,
|
|
@@ -24087,4 +24459,4 @@ export {
|
|
|
24087
24459
|
defaultRegistry,
|
|
24088
24460
|
createEngineFromEnv
|
|
24089
24461
|
};
|
|
24090
|
-
//# sourceMappingURL=chunk-
|
|
24462
|
+
//# sourceMappingURL=chunk-GLPQQP5C.js.map
|