@koda-sl/baker-cli 0.307.1-dev.62f0eca7d → 0.307.2-dev.4a3ed026b
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 +8 -0
- package/dist/{chunk-ZBC4NFST.js → chunk-GOVZCSVQ.js} +9 -4
- package/dist/chunk-GOVZCSVQ.js.map +1 -0
- package/dist/cli.js +189 -70
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-ZBC4NFST.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
validateCanvasDeep,
|
|
68
68
|
withoutCastName,
|
|
69
69
|
ytDlpBlockSignal
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-GOVZCSVQ.js";
|
|
71
71
|
import {
|
|
72
72
|
csvOrJson,
|
|
73
73
|
daysAgoIso,
|
|
@@ -30378,7 +30378,7 @@ var args = {
|
|
|
30378
30378
|
},
|
|
30379
30379
|
id: {
|
|
30380
30380
|
type: "string",
|
|
30381
|
-
description: "The send's id, when changing one that exists (`sends[].id
|
|
30381
|
+
description: "The send's id, when changing one that exists (`sends[].id`, like s1). Left out, a new id is minted and a new send is staged",
|
|
30382
30382
|
required: false
|
|
30383
30383
|
},
|
|
30384
30384
|
remove: {
|
|
@@ -41073,6 +41073,7 @@ function endCardCta(spec) {
|
|
|
41073
41073
|
return declared.toLowerCase().replace(/[.!?]$/, "") === closing.replace(/[.!?]$/, "") ? null : declared;
|
|
41074
41074
|
}
|
|
41075
41075
|
var AD_CAST_ID = "customer";
|
|
41076
|
+
var AD_NARRATOR_ID = "ad_narrator";
|
|
41076
41077
|
var AD_SET_ID = "set";
|
|
41077
41078
|
function castedAvatarOnRoster(ad, roster) {
|
|
41078
41079
|
if (ad.cast?.avatar) return null;
|
|
@@ -41128,9 +41129,9 @@ function adSpecCastElements(spec, avatar) {
|
|
|
41128
41129
|
});
|
|
41129
41130
|
return elements;
|
|
41130
41131
|
}
|
|
41131
|
-
function motionPrompt(beat, toCamera, isClosingCard, place
|
|
41132
|
+
function motionPrompt(beat, toCamera, isClosingCard, place) {
|
|
41132
41133
|
if (isClosingCard) return BRAND_PLATE;
|
|
41133
|
-
const shot = `${beat.show}${place}
|
|
41134
|
+
const shot = `${beat.show}${place}`;
|
|
41134
41135
|
if (toCamera) return shot;
|
|
41135
41136
|
return `${shot} Nobody in frame is speaking: mouths CLOSED and STILL throughout, no talking, no lip movement. Whoever is in shot is looking, reacting or working \u2014 never addressing the camera.`;
|
|
41136
41137
|
}
|
|
@@ -41178,11 +41179,9 @@ function beatsIntroducingSpeakerOffCamera(spec) {
|
|
|
41178
41179
|
(beat, i) => lineIntroducesSpeaker(beat.say) && !beatToCamera(spec, beat, closesOnCard && i === last) ? [i] : []
|
|
41179
41180
|
);
|
|
41180
41181
|
}
|
|
41181
|
-
function
|
|
41182
|
+
function beatSpeaksNatively(spec, beat, isClosingCard) {
|
|
41182
41183
|
if (!spec.cast || spec.voiceover === false) return false;
|
|
41183
|
-
|
|
41184
|
-
const speaking = spec.beats.filter((_, i) => !(closesOnCard && i === last));
|
|
41185
|
-
return speaking.length > 0 && speaking.every((beat) => beatToCamera(spec, beat, false) && beat.cast !== false);
|
|
41184
|
+
return beatToCamera(spec, beat, isClosingCard) && beat.cast !== false;
|
|
41186
41185
|
}
|
|
41187
41186
|
function beatDialogue(spec, beat, window2) {
|
|
41188
41187
|
if (spec.voiceover === false) return [];
|
|
@@ -41199,14 +41198,38 @@ function beatDialogue(spec, beat, window2) {
|
|
|
41199
41198
|
// by definition never on camera — so every beat took the dubbed path: a clip generated
|
|
41200
41199
|
// with one mouth and a separate ElevenLabs track laid over it. That is the mismatch
|
|
41201
41200
|
// behind every "the mouth does not match" in this ad's history.
|
|
41202
|
-
|
|
41203
|
-
|
|
41201
|
+
//
|
|
41202
|
+
// A beat that is NOT hers names the ad's OWN narrator (`AD_NARRATOR_ID`), rather
|
|
41203
|
+
// than leaving the label off. A different speaker is what breaks the phrase, and a
|
|
41204
|
+
// phrase break is what keeps each beat its own clip: left unlabelled, the engine read
|
|
41205
|
+
// the cutaway as her narrating over an insert in one continuous take and merged the
|
|
41206
|
+
// beats around it into a single native read with the dubbed one laid on top.
|
|
41207
|
+
speaker: window2.nativeSpeech ? AD_CAST_ID : AD_NARRATOR_ID,
|
|
41208
|
+
// The narrator is cast from the ad's voice, and failing that from the CAST's own
|
|
41209
|
+
// description. Naming a speaker of our own cost this: the engine casts an unnamed
|
|
41210
|
+
// narrator by folding it into the lone presenter and reusing her casting, so the
|
|
41211
|
+
// closing line inherited her persona for free. Named, it is a speaker in nobody's
|
|
41212
|
+
// `cast`, and the engine's last-resort description for one of those is the literal
|
|
41213
|
+
// "<speaker> voice" — so an ad with an avatar, a closing card and no `voice`
|
|
41214
|
+
// (a perfectly ordinary spec) had its closing tagline cast off the words
|
|
41215
|
+
// "ad_narrator voice".
|
|
41216
|
+
...narratorVoice(spec, window2.avatarVoice) ? { voice_description: narratorVoice(spec, window2.avatarVoice) } : {}
|
|
41204
41217
|
}
|
|
41205
41218
|
];
|
|
41206
41219
|
}
|
|
41207
|
-
function
|
|
41220
|
+
function narratorVoice(spec, avatarVoice) {
|
|
41221
|
+
return spec.voice?.description?.trim() || avatarVoice?.trim() || spec.cast?.description?.trim() || void 0;
|
|
41222
|
+
}
|
|
41223
|
+
function nativeSpeechByBeat(spec, closesOnCard) {
|
|
41224
|
+
const last = spec.beats.length - 1;
|
|
41225
|
+
return spec.beats.map((beat, i) => beatSpeaksNatively(spec, beat, closesOnCard && i === last));
|
|
41226
|
+
}
|
|
41227
|
+
function voiceoverMode(spec, nativeByBeat, closesOnCard) {
|
|
41208
41228
|
if (spec.voiceover === false) return "none";
|
|
41209
|
-
|
|
41229
|
+
const spoken = closesOnCard ? nativeByBeat.slice(0, -1) : nativeByBeat;
|
|
41230
|
+
const native = spoken.filter(Boolean).length;
|
|
41231
|
+
if (native === 0) return "voiceover";
|
|
41232
|
+
return native === spoken.length ? "on_camera" : "mixed";
|
|
41210
41233
|
}
|
|
41211
41234
|
function castRole(spec) {
|
|
41212
41235
|
if (!spec.cast) return void 0;
|
|
@@ -41223,7 +41246,8 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
41223
41246
|
const currency = market ? CURRENCY_BY_MARKET[market.toLowerCase()] : void 0;
|
|
41224
41247
|
const physics = frameRealismDirection({ currency, role: castRole(spec) });
|
|
41225
41248
|
const closesOnCard = endCardWanted(spec);
|
|
41226
|
-
const
|
|
41249
|
+
const nativeByBeat = nativeSpeechByBeat(spec, closesOnCard);
|
|
41250
|
+
const spokenMode = voiceoverMode(spec, nativeByBeat, closesOnCard);
|
|
41227
41251
|
let clock = 0;
|
|
41228
41252
|
const scenes = spec.beats.map((beat, i) => {
|
|
41229
41253
|
const isClosingCard = closesOnCard && i === total - 1;
|
|
@@ -41252,9 +41276,10 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
41252
41276
|
start: Math.round(start * 100) / 100,
|
|
41253
41277
|
end: Math.round(clock * 100) / 100,
|
|
41254
41278
|
isClosingCard,
|
|
41255
|
-
nativeSpeech
|
|
41279
|
+
nativeSpeech: nativeByBeat[i] === true,
|
|
41280
|
+
avatarVoice: opts.avatarVoice
|
|
41256
41281
|
}),
|
|
41257
|
-
motion_prompt: motionPrompt(beat, beatToCamera(spec, beat, isClosingCard), isClosingCard, place
|
|
41282
|
+
motion_prompt: motionPrompt(beat, beatToCamera(spec, beat, isClosingCard), isClosingCard, place),
|
|
41258
41283
|
start_frame_prompt: isClosingCard ? BRAND_PLATE : `${beat.show}${place}${physics}`
|
|
41259
41284
|
};
|
|
41260
41285
|
});
|
|
@@ -41266,9 +41291,9 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
41266
41291
|
// the script itself, so an ad that never named a place still says where it is set.
|
|
41267
41292
|
...market ? { market } : {},
|
|
41268
41293
|
voiceover: {
|
|
41269
|
-
mode:
|
|
41294
|
+
mode: spokenMode,
|
|
41270
41295
|
...spokenVariety(spec, avatarAccent) ? { accent: spokenVariety(spec, avatarAccent) } : {},
|
|
41271
|
-
...spec.
|
|
41296
|
+
...narratorVoice(spec, opts.avatarVoice) ? { voice_description: narratorVoice(spec, opts.avatarVoice) } : {}
|
|
41272
41297
|
},
|
|
41273
41298
|
...spec.cast ? {
|
|
41274
41299
|
cast: [
|
|
@@ -41298,15 +41323,10 @@ function beatsTooShortToPlay(spec) {
|
|
|
41298
41323
|
// src/engine/scaffold/ad-brand-overlay.ts
|
|
41299
41324
|
var markFallback = (name) => name.trim() ? ` onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:this.className+' brand-wordmark',textContent:${escapeHtml2(JSON.stringify(name.trim()))}}))"` : "";
|
|
41300
41325
|
var escapeHtml2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
41301
|
-
function firstHex(palette) {
|
|
41302
|
-
const hex = (palette ?? []).find((h) => /^#?[0-9a-fA-F]{6}$/.test(h.trim()));
|
|
41303
|
-
if (!hex) return null;
|
|
41304
|
-
const trimmed = hex.trim();
|
|
41305
|
-
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
41306
|
-
}
|
|
41307
41326
|
function buildAdBrandOverlay(opts) {
|
|
41308
|
-
const { logo, cta, cardAtS, cardDurationS, totalS
|
|
41327
|
+
const { logo, cta, cardAtS, cardDurationS, totalS } = opts;
|
|
41309
41328
|
if (!logo && !cta?.trim()) return "";
|
|
41329
|
+
const inkClass = opts.markIsLight ? " on-dark" : "";
|
|
41310
41330
|
const parts = [
|
|
41311
41331
|
"<!-- Brand layer, emitted by `baker canvas scaffold-ad`. The mark and the closing",
|
|
41312
41332
|
" card are drawn HERE, by the composition \u2014 never generated as a picture, because",
|
|
@@ -41316,18 +41336,19 @@ function buildAdBrandOverlay(opts) {
|
|
|
41316
41336
|
];
|
|
41317
41337
|
if (logo) {
|
|
41318
41338
|
parts.push(
|
|
41319
|
-
`<img class="clip brand-mark" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" data-start="0" data-dur="${totalS}"${markFallback(logo.alt)} />`
|
|
41339
|
+
`<img class="clip brand-mark${inkClass}" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" data-start="0" data-dur="${totalS}"${markFallback(logo.alt)} />`
|
|
41320
41340
|
);
|
|
41321
41341
|
}
|
|
41322
41342
|
const cardBits = [];
|
|
41323
41343
|
if (logo) {
|
|
41324
41344
|
cardBits.push(
|
|
41325
|
-
`<img class="endcard-logo" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}"${markFallback(logo.alt)} />`
|
|
41345
|
+
`<img class="endcard-logo${inkClass}" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}"${markFallback(logo.alt)} />`
|
|
41326
41346
|
);
|
|
41327
41347
|
}
|
|
41328
41348
|
if (cta?.trim()) {
|
|
41329
|
-
|
|
41330
|
-
|
|
41349
|
+
cardBits.push(
|
|
41350
|
+
`<div class="endcard-cta${opts.plateIsDark ? " on-dark-plate" : ""}">${escapeHtml2(cta.trim())}</div>`
|
|
41351
|
+
);
|
|
41331
41352
|
}
|
|
41332
41353
|
parts.push(
|
|
41333
41354
|
`<div class="clip endcard" data-start="${cardAtS}" data-dur="${cardDurationS}" data-role="cta">`,
|
|
@@ -41344,6 +41365,8 @@ function brandLayerDriver() {
|
|
|
41344
41365
|
" var els = Array.prototype.slice.call(document.querySelectorAll('.brand-mark, .endcard'));",
|
|
41345
41366
|
" if (!els.length) return;",
|
|
41346
41367
|
" els.forEach(function (el) { gsap.set(el, { visibility: 'hidden' }); });",
|
|
41368
|
+
" var card = document.querySelector('.endcard');",
|
|
41369
|
+
" var captions = document.getElementById('captions');",
|
|
41347
41370
|
" function attach(tl) {",
|
|
41348
41371
|
" if (!tl) return;",
|
|
41349
41372
|
" els.forEach(function (el) {",
|
|
@@ -41352,6 +41375,18 @@ function brandLayerDriver() {
|
|
|
41352
41375
|
" tl.set(el, { visibility: 'visible' }, at);",
|
|
41353
41376
|
" if (dur > 0) tl.set(el, { visibility: 'hidden' }, at + dur);",
|
|
41354
41377
|
" });",
|
|
41378
|
+
" // The closing card owns the frame it lands on. The caption track otherwise",
|
|
41379
|
+
" // keeps running underneath it, and what it captions there is the line spoken",
|
|
41380
|
+
" // OVER the card \u2014 the call to action, printed a second time in smaller letters",
|
|
41381
|
+
" // directly below the button that already says it. Hidden on the CONTAINER and",
|
|
41382
|
+
" // with `display`, because each caption group sets its own `visibility: visible`",
|
|
41383
|
+
" // and would win against anything set on its parent.",
|
|
41384
|
+
" if (card && captions) {",
|
|
41385
|
+
" var cardAt = parseFloat(card.getAttribute('data-start') || '0') || 0;",
|
|
41386
|
+
" var cardDur = parseFloat(card.getAttribute('data-dur') || '0') || 0;",
|
|
41387
|
+
" tl.set(captions, { display: 'none' }, cardAt);",
|
|
41388
|
+
" if (cardDur > 0) tl.set(captions, { display: '' }, cardAt + cardDur);",
|
|
41389
|
+
" }",
|
|
41355
41390
|
" }",
|
|
41356
41391
|
" var store = window.__timelines || {};",
|
|
41357
41392
|
" var main = store.main;",
|
|
@@ -41376,11 +41411,12 @@ function adBrandOverlayCss() {
|
|
|
41376
41411
|
" viewBox is wider than it is tall rendered clipped \u2014 the first real ad drew",
|
|
41377
41412
|
" `greenlea`. Constraining the height and letting the width follow keeps any",
|
|
41378
41413
|
" aspect ratio whole, and the max-width stops a very wide mark spanning the frame. */",
|
|
41379
|
-
" /* A LIGHT panel behind the mark
|
|
41380
|
-
" \u2014
|
|
41381
|
-
" and the brand-coloured closing plate erased it completely: the
|
|
41382
|
-
" out as an empty green frame with a single leaf floating in it. A
|
|
41383
|
-
" panel is the
|
|
41414
|
+
" /* A LIGHT panel behind the mark by DEFAULT. A brand mark is usually drawn to sit",
|
|
41415
|
+
" on white \u2014 the first one here was #1D5D47 on transparent \u2014 so a dark backing hid",
|
|
41416
|
+
" it on the corner and the brand-coloured closing plate erased it completely: the",
|
|
41417
|
+
" end card came out as an empty green frame with a single leaf floating in it. A",
|
|
41418
|
+
" near-white panel is the background most marks in a brand kit are designed",
|
|
41419
|
+
" against; the ones that are not get `.on-dark` below. */",
|
|
41384
41420
|
" .brand-mark { position: absolute; top: 90px; left: 56px; height: 52px; width: auto;",
|
|
41385
41421
|
" max-width: 420px; object-fit: contain; padding: 14px 22px; border-radius: 18px;",
|
|
41386
41422
|
" background: rgba(255,255,255,0.94);",
|
|
@@ -41394,6 +41430,14 @@ function adBrandOverlayCss() {
|
|
|
41394
41430
|
" .endcard-logo { width: 62%; height: auto; max-height: 300px; object-fit: contain;",
|
|
41395
41431
|
" padding: 46px 56px; border-radius: 40px; background: rgba(255,255,255,0.96);",
|
|
41396
41432
|
" box-shadow: 0 10px 40px rgba(0,0,0,0.28); }",
|
|
41433
|
+
" /* A mark whose own ink is LIGHT gets the opposite panel. The near-white panel",
|
|
41434
|
+
" above is right for the mark a brand kit usually ships \u2014 drawn to sit on white",
|
|
41435
|
+
" \u2014 and it erases the one it does not: a client's white `logo-dark.svg` rendered",
|
|
41436
|
+
" as an empty white box in the corner, and again 300px tall on the closing card.",
|
|
41437
|
+
" Which one a mark is gets MEASURED off the file (`markInkIsLight`), never read",
|
|
41438
|
+
" off its name: `logo-dark` names the background it is for in one brand kit and",
|
|
41439
|
+
" the ink itself in the next. */",
|
|
41440
|
+
" .brand-mark.on-dark, .endcard-logo.on-dark { background: rgba(16,20,18,0.92); }",
|
|
41397
41441
|
" /* The name, when the mark itself could not be drawn. */",
|
|
41398
41442
|
" .brand-wordmark { display: inline-block; font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
|
|
41399
41443
|
" font-weight: 900; color: #14231b; letter-spacing: -0.01em; }",
|
|
@@ -41408,10 +41452,17 @@ function adBrandOverlayCss() {
|
|
|
41408
41452
|
" first attempt led with 'BrandFont', which the overlay composition defines and",
|
|
41409
41453
|
" the caption composition does not: HyperFrames refuses an unmapped family and",
|
|
41410
41454
|
" the whole render died with `No deterministic font mapping for: BrandFont`. */",
|
|
41455
|
+
" /* The button is the OPPOSITE of the plate it sits on. Filling it with the brand",
|
|
41456
|
+
" colour made it invisible \u2014 the plate is filled with that same colour, so the",
|
|
41457
|
+
" button was brand-on-brand and its white copy read as another caption line \u2014",
|
|
41458
|
+
" and a fixed near-black fill is that defect mirrored, on the black plate an ad",
|
|
41459
|
+
" with no palette gets. The plate carries the brand; the button carries the",
|
|
41460
|
+
" contrast. Light copy on the near-black default, dark copy on the light one. */",
|
|
41411
41461
|
" .endcard-cta { font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
|
|
41412
41462
|
" font-size: 64px; font-weight: 900; text-transform: uppercase;",
|
|
41413
41463
|
" letter-spacing: 0.01em; color: #fff; padding: 28px 58px; border-radius: 56px;",
|
|
41414
|
-
" max-width: 900px; line-height: 1.15; background: rgba(0,0,0,0.82); }"
|
|
41464
|
+
" max-width: 900px; line-height: 1.15; background: rgba(0,0,0,0.82); }",
|
|
41465
|
+
" .endcard-cta.on-dark-plate { background: rgba(255,255,255,0.96); color: #14231b; }"
|
|
41415
41466
|
].join("\n");
|
|
41416
41467
|
}
|
|
41417
41468
|
function injectAdBrandOverlay(indexHtml, overlayHtml) {
|
|
@@ -41428,6 +41479,7 @@ function injectAdBrandOverlay(indexHtml, overlayHtml) {
|
|
|
41428
41479
|
// src/engine/scaffold/ad-brand-source.ts
|
|
41429
41480
|
import { readdir as readdir8, readFile as readFile15 } from "fs/promises";
|
|
41430
41481
|
import path22 from "path";
|
|
41482
|
+
import sharp from "sharp";
|
|
41431
41483
|
var BRAND_DIR = "src/brand";
|
|
41432
41484
|
function paletteFromBrandDoc(markdown) {
|
|
41433
41485
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -41436,15 +41488,33 @@ function paletteFromBrandDoc(markdown) {
|
|
|
41436
41488
|
const hex = `#${match[1].toLowerCase()}`;
|
|
41437
41489
|
if (seen.has(hex)) continue;
|
|
41438
41490
|
seen.add(hex);
|
|
41439
|
-
const
|
|
41440
|
-
|
|
41441
|
-
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
41442
|
-
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
41443
|
-
if (luma > 236 || luma < 14) continue;
|
|
41491
|
+
const luma = hexLuma(hex);
|
|
41492
|
+
if (luma === null || luma > 236 || luma < 14) continue;
|
|
41444
41493
|
out.push(hex);
|
|
41445
41494
|
}
|
|
41446
41495
|
return out;
|
|
41447
41496
|
}
|
|
41497
|
+
function hexLuma(hex) {
|
|
41498
|
+
const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
|
|
41499
|
+
if (!m) return null;
|
|
41500
|
+
const v = m[1];
|
|
41501
|
+
const r = Number.parseInt(v.slice(0, 2), 16);
|
|
41502
|
+
const g = Number.parseInt(v.slice(2, 4), 16);
|
|
41503
|
+
const b = Number.parseInt(v.slice(4, 6), 16);
|
|
41504
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
41505
|
+
}
|
|
41506
|
+
function firstHex(palette) {
|
|
41507
|
+
const hex = (palette ?? []).find((h) => /^#?[0-9a-fA-F]{6}$/.test(h.trim()));
|
|
41508
|
+
if (!hex) return null;
|
|
41509
|
+
const trimmed = hex.trim();
|
|
41510
|
+
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
41511
|
+
}
|
|
41512
|
+
function plateIsDark(palette) {
|
|
41513
|
+
const luma = hexLuma(firstHex(palette) ?? "");
|
|
41514
|
+
return luma === null || luma < PLATE_DARK_LUMA;
|
|
41515
|
+
}
|
|
41516
|
+
var PLATE_DARK_LUMA = 140;
|
|
41517
|
+
var MARK_VARIANT_NAMES = /\b(mono|white|black|blanco|negro|negra|dark|light|oscuro|oscura|claro|clara|negativo|negativa|positivo|inverse|inverso|inversa|inv|reverse|reversed|alt|outline|transparent|watermark|icon|favicon|mark-only|isotipo|isotype)\b/;
|
|
41448
41518
|
function pickBrandMark(files) {
|
|
41449
41519
|
const usable = files.filter((f) => /\.(svg|png|webp)$/i.test(f));
|
|
41450
41520
|
if (usable.length === 0) return null;
|
|
@@ -41453,10 +41523,10 @@ function pickBrandMark(files) {
|
|
|
41453
41523
|
let s = 0;
|
|
41454
41524
|
if (/\.(png|webp)$/.test(name)) s += 1;
|
|
41455
41525
|
if (/\b(logo|wordmark|lockup)\b/.test(name)) s += 2;
|
|
41456
|
-
if (
|
|
41526
|
+
if (MARK_VARIANT_NAMES.test(name)) s -= 3;
|
|
41457
41527
|
return s;
|
|
41458
41528
|
};
|
|
41459
|
-
return [...usable].sort((a, b) => score(b) - score(a) || a.localeCompare(b))[0] ?? null;
|
|
41529
|
+
return [...usable].sort((a, b) => score(b) - score(a) || a.length - b.length || a.localeCompare(b))[0] ?? null;
|
|
41460
41530
|
}
|
|
41461
41531
|
async function readBrandFromWorkspace(root = ".") {
|
|
41462
41532
|
const found = {};
|
|
@@ -41511,6 +41581,27 @@ async function markDirectoryIsReal(dir) {
|
|
|
41511
41581
|
}
|
|
41512
41582
|
return true;
|
|
41513
41583
|
}
|
|
41584
|
+
var INK_ALPHA = 128;
|
|
41585
|
+
function inkLuma(rgba) {
|
|
41586
|
+
let sum = 0;
|
|
41587
|
+
let count = 0;
|
|
41588
|
+
for (let i = 0; i + 3 < rgba.length; i += 4) {
|
|
41589
|
+
if (rgba[i + 3] < INK_ALPHA) continue;
|
|
41590
|
+
sum += 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
|
|
41591
|
+
count += 1;
|
|
41592
|
+
}
|
|
41593
|
+
return count === 0 ? null : sum / count;
|
|
41594
|
+
}
|
|
41595
|
+
var LIGHT_INK_LUMA = 170;
|
|
41596
|
+
async function markInkIsLight(markPath) {
|
|
41597
|
+
try {
|
|
41598
|
+
const { data } = await sharp(markPath, { density: 144 }).resize(96, 96, { fit: "inside", withoutEnlargement: true }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
41599
|
+
const luma = inkLuma(data);
|
|
41600
|
+
return luma !== null && luma > LIGHT_INK_LUMA;
|
|
41601
|
+
} catch {
|
|
41602
|
+
return false;
|
|
41603
|
+
}
|
|
41604
|
+
}
|
|
41514
41605
|
|
|
41515
41606
|
// src/commands/canvas/composition-path.ts
|
|
41516
41607
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -41574,6 +41665,11 @@ registerSchema({
|
|
|
41574
41665
|
}
|
|
41575
41666
|
}
|
|
41576
41667
|
});
|
|
41668
|
+
function avatarVoiceBrief(avatar) {
|
|
41669
|
+
if (!avatar) return void 0;
|
|
41670
|
+
const parts = [avatar.subjectDescription, avatar.profile?.persona, avatar.profile?.speech].map((p) => p?.trim()).filter((p) => Boolean(p));
|
|
41671
|
+
return parts.length > 0 ? parts.join("; ") : void 0;
|
|
41672
|
+
}
|
|
41577
41673
|
var scaffoldAdCommand = defineCommand113({
|
|
41578
41674
|
meta: {
|
|
41579
41675
|
name: "scaffold-ad",
|
|
@@ -41816,7 +41912,12 @@ var scaffoldAdCommand = defineCommand113({
|
|
|
41816
41912
|
spec.data.cast = { ...spec.data.cast, description: withoutCastName(spec.data.cast.description, names) };
|
|
41817
41913
|
}
|
|
41818
41914
|
}
|
|
41819
|
-
const
|
|
41915
|
+
const avatarVoice = avatarVoiceBrief(avatar);
|
|
41916
|
+
const blueprint = adSpecToBlueprint(spec.data, {
|
|
41917
|
+
avatarAccent: avatar?.profile?.accent,
|
|
41918
|
+
videoModel: chosenVideoModel,
|
|
41919
|
+
...avatarVoice ? { avatarVoice } : {}
|
|
41920
|
+
});
|
|
41820
41921
|
const slug = args4.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
|
|
41821
41922
|
const outPath = args4.out ?? (args4.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));
|
|
41822
41923
|
const outDir = path24.dirname(outPath);
|
|
@@ -41866,6 +41967,19 @@ var scaffoldAdCommand = defineCommand113({
|
|
|
41866
41967
|
// and rejected — it would swap the voice that matches the mouth and the directed
|
|
41867
41968
|
// region for one cast off an English stock list. Only the on-camera voice; a
|
|
41868
41969
|
// voiceover read has no rendered voice to keep and is still cast.
|
|
41970
|
+
//
|
|
41971
|
+
// The KNOWN RISK this carries, recorded here because it is decided here: keeping the
|
|
41972
|
+
// rendered voice also means no pass unifies her beats, and each beat is its own
|
|
41973
|
+
// generation, so nothing guarantees two of them sound like the same person. The
|
|
41974
|
+
// reference image fixes the face; nothing fixes the voice. Measured on a finished
|
|
41975
|
+
// three-beat ad (her, a cutaway, her): median speaking pitch 210.5 Hz and 216.2 Hz,
|
|
41976
|
+
// near-identical spreads — the same voice as far as pitch can say — and the two
|
|
41977
|
+
// extracts were then listened to side by side and judged to be the same person. Not
|
|
41978
|
+
// proof for every ad: one listener, one ad, and pitch is one dimension of a voice.
|
|
41979
|
+
//
|
|
41980
|
+
// If it does show up, the fix is not to drop `keepRenderedVoice` — that brings back
|
|
41981
|
+
// the stock voice this exists to avoid. It is to convert the LATER extracts onto the
|
|
41982
|
+
// first beat of the same speaker, which keeps a voice the clip really produced.
|
|
41869
41983
|
...avatar ? { keepRenderedVoice: true } : {},
|
|
41870
41984
|
...spec.data.voiceover === false ? { staticTranscriptPath: path24.relative(outDir, transcriptPath) } : {}
|
|
41871
41985
|
};
|
|
@@ -41873,11 +41987,13 @@ var scaffoldAdCommand = defineCommand113({
|
|
|
41873
41987
|
const renderNode4 = canvas.nodes.find((n) => n.type === "hyperframe_render");
|
|
41874
41988
|
const renderedDir = renderNode4 ? path24.join(outDir, String(renderNode4.params?.composition ?? "")) : null;
|
|
41875
41989
|
let staged = null;
|
|
41990
|
+
let markIsLight = false;
|
|
41876
41991
|
if (logoPath && renderedDir) {
|
|
41877
41992
|
const ext = path24.extname(logoPath) || ".png";
|
|
41878
41993
|
try {
|
|
41879
41994
|
await copyFile(logoPath, path24.join(renderedDir, `brand-mark${ext}`));
|
|
41880
41995
|
staged = { file: `brand-mark${ext}`, alt: spec.data.brand?.name ?? "brand" };
|
|
41996
|
+
markIsLight = await markInkIsLight(logoPath);
|
|
41881
41997
|
} catch {
|
|
41882
41998
|
}
|
|
41883
41999
|
}
|
|
@@ -41888,7 +42004,10 @@ var scaffoldAdCommand = defineCommand113({
|
|
|
41888
42004
|
cardAtS: closing.start_s,
|
|
41889
42005
|
cardDurationS: closing.duration_s,
|
|
41890
42006
|
totalS: blueprint.estimated_duration_s,
|
|
41891
|
-
|
|
42007
|
+
markIsLight,
|
|
42008
|
+
// The card's button is coloured against the plate the engine will fill from
|
|
42009
|
+
// this same palette — including the black plate an ad with no palette gets.
|
|
42010
|
+
plateIsDark: plateIsDark(spec.data.brand?.palette)
|
|
41892
42011
|
}) : "";
|
|
41893
42012
|
if (overlayHtml && renderedDir) {
|
|
41894
42013
|
const indexPath = path24.join(renderedDir, "index.html");
|
|
@@ -41963,7 +42082,7 @@ var scaffoldAdCommand = defineCommand113({
|
|
|
41963
42082
|
// ad with no palette and no mark anywhere in 28 seconds.
|
|
41964
42083
|
`This spec has no ${!spec.data.brand?.palette?.length ? "`brand.palette`" : ""}${!spec.data.brand?.palette?.length && !spec.data.brand?.logo ? " and no " : ""}${!spec.data.brand?.logo ? "`brand.logo`" : ""}. Read src/brand/BRAND.md and re-run with them \u2014 a name alone leaves the ad in someone else's clothes.`
|
|
41965
42084
|
],
|
|
41966
|
-
"`brand.palette`
|
|
42085
|
+
"`brand.palette` colours the closing plate the card sits on; `brand.logo` is the repo path to the mark, and it is DRAWN by the composition \u2014 top-left throughout and large on the closing card \u2014 never generated as a picture, so it cannot come back garbled. Give it whenever the client has one."
|
|
41967
42086
|
]
|
|
41968
42087
|
});
|
|
41969
42088
|
}
|
|
@@ -50203,9 +50322,9 @@ import { defineCommand as defineCommand168 } from "citty";
|
|
|
50203
50322
|
import { defineCommand as defineCommand142 } from "citty";
|
|
50204
50323
|
|
|
50205
50324
|
// src/lib/image/crop-sprite.ts
|
|
50206
|
-
import
|
|
50325
|
+
import sharp2 from "sharp";
|
|
50207
50326
|
function cropSprite(input, region) {
|
|
50208
|
-
return
|
|
50327
|
+
return sharp2(input).extract({
|
|
50209
50328
|
left: Math.round(region.x),
|
|
50210
50329
|
top: Math.round(region.y),
|
|
50211
50330
|
width: Math.round(region.width),
|
|
@@ -52465,7 +52584,7 @@ function solidifyEdges(data, saturationBoost = 0.25) {
|
|
|
52465
52584
|
}
|
|
52466
52585
|
|
|
52467
52586
|
// src/lib/image/image-processor.ts
|
|
52468
|
-
import
|
|
52587
|
+
import sharp3 from "sharp";
|
|
52469
52588
|
function hasGradient(data) {
|
|
52470
52589
|
const uniqueColors = /* @__PURE__ */ new Set();
|
|
52471
52590
|
for (let i = 0; i < data.length; i += 4) {
|
|
@@ -52497,10 +52616,10 @@ function createSvgRecolorer(target, palette) {
|
|
|
52497
52616
|
async function processGradient(data, info) {
|
|
52498
52617
|
if (!hasGradient(data)) return { data, info };
|
|
52499
52618
|
const solidified = solidifyEdges(data, 0.1);
|
|
52500
|
-
const blurred = await
|
|
52619
|
+
const blurred = await sharp3(solidified, {
|
|
52501
52620
|
raw: { channels: 4, width: info.width, height: info.height }
|
|
52502
52621
|
}).png().blur(0.3).toBuffer();
|
|
52503
|
-
const result = await
|
|
52622
|
+
const result = await sharp3(blurred).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
52504
52623
|
return { data: result.data, info: result.info };
|
|
52505
52624
|
}
|
|
52506
52625
|
function isSvgBuffer(buffer) {
|
|
@@ -52509,13 +52628,13 @@ function isSvgBuffer(buffer) {
|
|
|
52509
52628
|
}
|
|
52510
52629
|
async function processInternal(inputBuffer, isSVG, options) {
|
|
52511
52630
|
const stages = [];
|
|
52512
|
-
const metadata = await
|
|
52631
|
+
const metadata = await sharp3(inputBuffer).metadata();
|
|
52513
52632
|
let alreadyTransparent = false;
|
|
52514
52633
|
if (metadata.hasAlpha) {
|
|
52515
|
-
const { data: alphaData } = await
|
|
52634
|
+
const { data: alphaData } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
|
|
52516
52635
|
alreadyTransparent = hasTransparency(alphaData, 0.05);
|
|
52517
52636
|
}
|
|
52518
|
-
let { data: processedData, info } = await
|
|
52637
|
+
let { data: processedData, info } = await sharp3(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
52519
52638
|
if (options.color) {
|
|
52520
52639
|
stages.push("recolor");
|
|
52521
52640
|
processedData = applyQuantization(processedData, 8);
|
|
@@ -52586,7 +52705,7 @@ async function processInternal(inputBuffer, isSVG, options) {
|
|
|
52586
52705
|
processedData = result.data;
|
|
52587
52706
|
info = result.info;
|
|
52588
52707
|
}
|
|
52589
|
-
let pipeline2 =
|
|
52708
|
+
let pipeline2 = sharp3(processedData, {
|
|
52590
52709
|
raw: { channels: 4, width: info.width, height: info.height }
|
|
52591
52710
|
}).png();
|
|
52592
52711
|
if (options.shrinkToContent) {
|
|
@@ -52599,12 +52718,12 @@ async function processInternal(inputBuffer, isSVG, options) {
|
|
|
52599
52718
|
function applyResize(buffer, resize) {
|
|
52600
52719
|
const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
52601
52720
|
if (resize.size) {
|
|
52602
|
-
return
|
|
52721
|
+
return sharp3(buffer).resize(resize.size.width, resize.size.height, {
|
|
52603
52722
|
fit: resize.fit ?? "contain",
|
|
52604
52723
|
background: transparent
|
|
52605
52724
|
}).toBuffer();
|
|
52606
52725
|
}
|
|
52607
|
-
return
|
|
52726
|
+
return sharp3(buffer).resize(resize.width ?? null, resize.height ?? null, { fit: "inside", withoutEnlargement: false }).toBuffer();
|
|
52608
52727
|
}
|
|
52609
52728
|
async function processImage(inputBuffer, options = {}) {
|
|
52610
52729
|
const stages = [];
|
|
@@ -52613,7 +52732,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
52613
52732
|
if (options.resize) {
|
|
52614
52733
|
stages.push("resize");
|
|
52615
52734
|
const resized = await applyResize(inputBuffer, options.resize);
|
|
52616
|
-
const meta3 = await
|
|
52735
|
+
const meta3 = await sharp3(resized).metadata();
|
|
52617
52736
|
return {
|
|
52618
52737
|
buffer: resized,
|
|
52619
52738
|
format: "png",
|
|
@@ -52622,7 +52741,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
52622
52741
|
stages
|
|
52623
52742
|
};
|
|
52624
52743
|
}
|
|
52625
|
-
const meta2 = await
|
|
52744
|
+
const meta2 = await sharp3(inputBuffer).metadata();
|
|
52626
52745
|
return {
|
|
52627
52746
|
buffer: inputBuffer,
|
|
52628
52747
|
format: "svg",
|
|
@@ -52634,7 +52753,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
52634
52753
|
let workingBuffer = inputBuffer;
|
|
52635
52754
|
if (isSVG) {
|
|
52636
52755
|
stages.push("rasterize-svg");
|
|
52637
|
-
workingBuffer = await
|
|
52756
|
+
workingBuffer = await sharp3(inputBuffer).png({ compressionLevel: 0, force: true, palette: false, quality: 100 }).toBuffer();
|
|
52638
52757
|
}
|
|
52639
52758
|
const pass = await processInternal(workingBuffer, isSVG, options);
|
|
52640
52759
|
stages.push(...pass.stages);
|
|
@@ -52643,7 +52762,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
52643
52762
|
stages.push("resize");
|
|
52644
52763
|
processed = await applyResize(processed, options.resize);
|
|
52645
52764
|
}
|
|
52646
|
-
const meta = await
|
|
52765
|
+
const meta = await sharp3(processed).metadata();
|
|
52647
52766
|
return {
|
|
52648
52767
|
buffer: processed,
|
|
52649
52768
|
format: "png",
|
|
@@ -56262,7 +56381,7 @@ function routeKey(route) {
|
|
|
56262
56381
|
}
|
|
56263
56382
|
|
|
56264
56383
|
// src/engine/landing-library/fidelity.ts
|
|
56265
|
-
import
|
|
56384
|
+
import sharp4 from "sharp";
|
|
56266
56385
|
var COMPARISON_SIZE = 32;
|
|
56267
56386
|
function pixelSimilarity(a, b) {
|
|
56268
56387
|
const length = Math.min(a.length, b.length);
|
|
@@ -56274,7 +56393,7 @@ function pixelSimilarity(a, b) {
|
|
|
56274
56393
|
return 1 - total / (length * 255);
|
|
56275
56394
|
}
|
|
56276
56395
|
async function scoreFidelity(live, rendered) {
|
|
56277
|
-
const [liveMeta, renderedMeta] = await Promise.all([
|
|
56396
|
+
const [liveMeta, renderedMeta] = await Promise.all([sharp4(live).metadata(), sharp4(rendered).metadata()]);
|
|
56278
56397
|
const liveSize = { width: liveMeta.width ?? 0, height: liveMeta.height ?? 0 };
|
|
56279
56398
|
const renderedSize = { width: renderedMeta.width ?? 0, height: renderedMeta.height ?? 0 };
|
|
56280
56399
|
const [livePixels, renderedPixels] = await Promise.all([toGreyGrid(live), toGreyGrid(rendered)]);
|
|
@@ -56288,7 +56407,7 @@ async function scoreFidelity(live, rendered) {
|
|
|
56288
56407
|
};
|
|
56289
56408
|
}
|
|
56290
56409
|
async function toGreyGrid(image) {
|
|
56291
|
-
const raw = await
|
|
56410
|
+
const raw = await sharp4(image).greyscale().resize(COMPARISON_SIZE, COMPARISON_SIZE, { fit: "fill" }).raw().toBuffer();
|
|
56292
56411
|
return new Uint8Array(raw);
|
|
56293
56412
|
}
|
|
56294
56413
|
|
|
@@ -56500,7 +56619,7 @@ var inPageCollectMotion = (selector) => {
|
|
|
56500
56619
|
};
|
|
56501
56620
|
|
|
56502
56621
|
// src/engine/landing-library/motionTake.ts
|
|
56503
|
-
import
|
|
56622
|
+
import sharp5 from "sharp";
|
|
56504
56623
|
|
|
56505
56624
|
// src/engine/landing-library/certificate.ts
|
|
56506
56625
|
function formatDate(unixSeconds) {
|
|
@@ -57167,8 +57286,8 @@ async function hideFloatingFurniture(page, selector) {
|
|
|
57167
57286
|
}, selector);
|
|
57168
57287
|
}
|
|
57169
57288
|
async function composeFilmstrip(frames) {
|
|
57170
|
-
const scaled = await Promise.all(frames.map((frame) =>
|
|
57171
|
-
const first = await
|
|
57289
|
+
const scaled = await Promise.all(frames.map((frame) => sharp5(frame).resize({ width: FRAME_WIDTH }).png().toBuffer()));
|
|
57290
|
+
const first = await sharp5(scaled[0]).metadata();
|
|
57172
57291
|
const frameHeight = first.height ?? 300;
|
|
57173
57292
|
const cellHeight = frameHeight + LABEL_HEIGHT;
|
|
57174
57293
|
const rows = Math.ceil(scaled.length / GRID_COLUMNS);
|
|
@@ -57187,7 +57306,7 @@ async function composeFilmstrip(frames) {
|
|
|
57187
57306
|
{ input: frame, left: column * FRAME_WIDTH, top: row * cellHeight + LABEL_HEIGHT }
|
|
57188
57307
|
];
|
|
57189
57308
|
});
|
|
57190
|
-
return await
|
|
57309
|
+
return await sharp5({
|
|
57191
57310
|
create: {
|
|
57192
57311
|
width,
|
|
57193
57312
|
height: cellHeight * rows,
|
|
@@ -57376,12 +57495,12 @@ var DEFAULT_SEGMENT_OPTIONS = {
|
|
|
57376
57495
|
};
|
|
57377
57496
|
|
|
57378
57497
|
// src/engine/landing-library/visualHash.ts
|
|
57379
|
-
import
|
|
57498
|
+
import sharp6 from "sharp";
|
|
57380
57499
|
var HASH_WIDTH = 9;
|
|
57381
57500
|
var HASH_HEIGHT = 8;
|
|
57382
57501
|
async function perceptualHash(image) {
|
|
57383
57502
|
try {
|
|
57384
|
-
const raw = await
|
|
57503
|
+
const raw = await sharp6(image).greyscale().resize(HASH_WIDTH, HASH_HEIGHT, { fit: "fill" }).raw().toBuffer();
|
|
57385
57504
|
return bitsToHex(rowGradientBits(new Uint8Array(raw)));
|
|
57386
57505
|
} catch {
|
|
57387
57506
|
return null;
|
|
@@ -57895,7 +58014,7 @@ import { defineCommand as defineCommand175 } from "citty";
|
|
|
57895
58014
|
// src/commands/landing/inspiration/shot.ts
|
|
57896
58015
|
import { mkdir as mkdir13, writeFile as writeFile18 } from "fs/promises";
|
|
57897
58016
|
import path42 from "path";
|
|
57898
|
-
import
|
|
58017
|
+
import sharp7 from "sharp";
|
|
57899
58018
|
var READABLE_SHOT = {
|
|
57900
58019
|
maxWidth: 1440,
|
|
57901
58020
|
maxHeight: 4e3,
|
|
@@ -57904,7 +58023,7 @@ var READABLE_SHOT = {
|
|
|
57904
58023
|
};
|
|
57905
58024
|
async function toReadableShot(original) {
|
|
57906
58025
|
try {
|
|
57907
|
-
return await
|
|
58026
|
+
return await sharp7(original).resize({
|
|
57908
58027
|
width: READABLE_SHOT.maxWidth,
|
|
57909
58028
|
height: READABLE_SHOT.maxHeight,
|
|
57910
58029
|
fit: "inside",
|