@koda-sl/baker-cli 0.320.0 → 0.321.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 +8 -0
- package/dist/{chunk-WONWYE7B.js → chunk-WHUKLAGW.js} +9 -4
- package/dist/chunk-WHUKLAGW.js.map +1 -0
- package/dist/cli.js +188 -69
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-WONWYE7B.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-WHUKLAGW.js";
|
|
71
71
|
import {
|
|
72
72
|
csvOrJson,
|
|
73
73
|
daysAgoIso,
|
|
@@ -41933,6 +41933,7 @@ function endCardCta(spec) {
|
|
|
41933
41933
|
return declared.toLowerCase().replace(/[.!?]$/, "") === closing.replace(/[.!?]$/, "") ? null : declared;
|
|
41934
41934
|
}
|
|
41935
41935
|
var AD_CAST_ID = "customer";
|
|
41936
|
+
var AD_NARRATOR_ID = "ad_narrator";
|
|
41936
41937
|
var AD_SET_ID = "set";
|
|
41937
41938
|
function castedAvatarOnRoster(ad, roster) {
|
|
41938
41939
|
if (ad.cast?.avatar) return null;
|
|
@@ -41988,9 +41989,9 @@ function adSpecCastElements(spec, avatar) {
|
|
|
41988
41989
|
});
|
|
41989
41990
|
return elements;
|
|
41990
41991
|
}
|
|
41991
|
-
function motionPrompt(beat, toCamera, isClosingCard, place
|
|
41992
|
+
function motionPrompt(beat, toCamera, isClosingCard, place) {
|
|
41992
41993
|
if (isClosingCard) return BRAND_PLATE;
|
|
41993
|
-
const shot = `${beat.show}${place}
|
|
41994
|
+
const shot = `${beat.show}${place}`;
|
|
41994
41995
|
if (toCamera) return shot;
|
|
41995
41996
|
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.`;
|
|
41996
41997
|
}
|
|
@@ -42038,11 +42039,9 @@ function beatsIntroducingSpeakerOffCamera(spec) {
|
|
|
42038
42039
|
(beat, i) => lineIntroducesSpeaker(beat.say) && !beatToCamera(spec, beat, closesOnCard && i === last) ? [i] : []
|
|
42039
42040
|
);
|
|
42040
42041
|
}
|
|
42041
|
-
function
|
|
42042
|
+
function beatSpeaksNatively(spec, beat, isClosingCard) {
|
|
42042
42043
|
if (!spec.cast || spec.voiceover === false) return false;
|
|
42043
|
-
|
|
42044
|
-
const speaking = spec.beats.filter((_, i) => !(closesOnCard && i === last));
|
|
42045
|
-
return speaking.length > 0 && speaking.every((beat) => beatToCamera(spec, beat, false) && beat.cast !== false);
|
|
42044
|
+
return beatToCamera(spec, beat, isClosingCard) && beat.cast !== false;
|
|
42046
42045
|
}
|
|
42047
42046
|
function beatDialogue(spec, beat, window2) {
|
|
42048
42047
|
if (spec.voiceover === false) return [];
|
|
@@ -42059,14 +42058,38 @@ function beatDialogue(spec, beat, window2) {
|
|
|
42059
42058
|
// by definition never on camera — so every beat took the dubbed path: a clip generated
|
|
42060
42059
|
// with one mouth and a separate ElevenLabs track laid over it. That is the mismatch
|
|
42061
42060
|
// behind every "the mouth does not match" in this ad's history.
|
|
42062
|
-
|
|
42063
|
-
|
|
42061
|
+
//
|
|
42062
|
+
// A beat that is NOT hers names the ad's OWN narrator (`AD_NARRATOR_ID`), rather
|
|
42063
|
+
// than leaving the label off. A different speaker is what breaks the phrase, and a
|
|
42064
|
+
// phrase break is what keeps each beat its own clip: left unlabelled, the engine read
|
|
42065
|
+
// the cutaway as her narrating over an insert in one continuous take and merged the
|
|
42066
|
+
// beats around it into a single native read with the dubbed one laid on top.
|
|
42067
|
+
speaker: window2.nativeSpeech ? AD_CAST_ID : AD_NARRATOR_ID,
|
|
42068
|
+
// The narrator is cast from the ad's voice, and failing that from the CAST's own
|
|
42069
|
+
// description. Naming a speaker of our own cost this: the engine casts an unnamed
|
|
42070
|
+
// narrator by folding it into the lone presenter and reusing her casting, so the
|
|
42071
|
+
// closing line inherited her persona for free. Named, it is a speaker in nobody's
|
|
42072
|
+
// `cast`, and the engine's last-resort description for one of those is the literal
|
|
42073
|
+
// "<speaker> voice" — so an ad with an avatar, a closing card and no `voice`
|
|
42074
|
+
// (a perfectly ordinary spec) had its closing tagline cast off the words
|
|
42075
|
+
// "ad_narrator voice".
|
|
42076
|
+
...narratorVoice(spec, window2.avatarVoice) ? { voice_description: narratorVoice(spec, window2.avatarVoice) } : {}
|
|
42064
42077
|
}
|
|
42065
42078
|
];
|
|
42066
42079
|
}
|
|
42067
|
-
function
|
|
42080
|
+
function narratorVoice(spec, avatarVoice) {
|
|
42081
|
+
return spec.voice?.description?.trim() || avatarVoice?.trim() || spec.cast?.description?.trim() || void 0;
|
|
42082
|
+
}
|
|
42083
|
+
function nativeSpeechByBeat(spec, closesOnCard) {
|
|
42084
|
+
const last = spec.beats.length - 1;
|
|
42085
|
+
return spec.beats.map((beat, i) => beatSpeaksNatively(spec, beat, closesOnCard && i === last));
|
|
42086
|
+
}
|
|
42087
|
+
function voiceoverMode(spec, nativeByBeat, closesOnCard) {
|
|
42068
42088
|
if (spec.voiceover === false) return "none";
|
|
42069
|
-
|
|
42089
|
+
const spoken = closesOnCard ? nativeByBeat.slice(0, -1) : nativeByBeat;
|
|
42090
|
+
const native = spoken.filter(Boolean).length;
|
|
42091
|
+
if (native === 0) return "voiceover";
|
|
42092
|
+
return native === spoken.length ? "on_camera" : "mixed";
|
|
42070
42093
|
}
|
|
42071
42094
|
function castRole(spec) {
|
|
42072
42095
|
if (!spec.cast) return void 0;
|
|
@@ -42083,7 +42106,8 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
42083
42106
|
const currency = market ? CURRENCY_BY_MARKET[market.toLowerCase()] : void 0;
|
|
42084
42107
|
const physics = frameRealismDirection({ currency, role: castRole(spec) });
|
|
42085
42108
|
const closesOnCard = endCardWanted(spec);
|
|
42086
|
-
const
|
|
42109
|
+
const nativeByBeat = nativeSpeechByBeat(spec, closesOnCard);
|
|
42110
|
+
const spokenMode = voiceoverMode(spec, nativeByBeat, closesOnCard);
|
|
42087
42111
|
let clock = 0;
|
|
42088
42112
|
const scenes = spec.beats.map((beat, i) => {
|
|
42089
42113
|
const isClosingCard = closesOnCard && i === total - 1;
|
|
@@ -42112,9 +42136,10 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
42112
42136
|
start: Math.round(start * 100) / 100,
|
|
42113
42137
|
end: Math.round(clock * 100) / 100,
|
|
42114
42138
|
isClosingCard,
|
|
42115
|
-
nativeSpeech
|
|
42139
|
+
nativeSpeech: nativeByBeat[i] === true,
|
|
42140
|
+
avatarVoice: opts.avatarVoice
|
|
42116
42141
|
}),
|
|
42117
|
-
motion_prompt: motionPrompt(beat, beatToCamera(spec, beat, isClosingCard), isClosingCard, place
|
|
42142
|
+
motion_prompt: motionPrompt(beat, beatToCamera(spec, beat, isClosingCard), isClosingCard, place),
|
|
42118
42143
|
start_frame_prompt: isClosingCard ? BRAND_PLATE : `${beat.show}${place}${physics}`
|
|
42119
42144
|
};
|
|
42120
42145
|
});
|
|
@@ -42126,9 +42151,9 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
42126
42151
|
// the script itself, so an ad that never named a place still says where it is set.
|
|
42127
42152
|
...market ? { market } : {},
|
|
42128
42153
|
voiceover: {
|
|
42129
|
-
mode:
|
|
42154
|
+
mode: spokenMode,
|
|
42130
42155
|
...spokenVariety(spec, avatarAccent) ? { accent: spokenVariety(spec, avatarAccent) } : {},
|
|
42131
|
-
...spec.
|
|
42156
|
+
...narratorVoice(spec, opts.avatarVoice) ? { voice_description: narratorVoice(spec, opts.avatarVoice) } : {}
|
|
42132
42157
|
},
|
|
42133
42158
|
...spec.cast ? {
|
|
42134
42159
|
cast: [
|
|
@@ -42158,15 +42183,10 @@ function beatsTooShortToPlay(spec) {
|
|
|
42158
42183
|
// src/engine/scaffold/ad-brand-overlay.ts
|
|
42159
42184
|
var markFallback = (name) => name.trim() ? ` onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:this.className+' brand-wordmark',textContent:${escapeHtml2(JSON.stringify(name.trim()))}}))"` : "";
|
|
42160
42185
|
var escapeHtml2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
42161
|
-
function firstHex(palette) {
|
|
42162
|
-
const hex = (palette ?? []).find((h) => /^#?[0-9a-fA-F]{6}$/.test(h.trim()));
|
|
42163
|
-
if (!hex) return null;
|
|
42164
|
-
const trimmed = hex.trim();
|
|
42165
|
-
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
42166
|
-
}
|
|
42167
42186
|
function buildAdBrandOverlay(opts) {
|
|
42168
|
-
const { logo, cta, cardAtS, cardDurationS, totalS
|
|
42187
|
+
const { logo, cta, cardAtS, cardDurationS, totalS } = opts;
|
|
42169
42188
|
if (!logo && !cta?.trim()) return "";
|
|
42189
|
+
const inkClass = opts.markIsLight ? " on-dark" : "";
|
|
42170
42190
|
const parts = [
|
|
42171
42191
|
"<!-- Brand layer, emitted by `baker canvas scaffold-ad`. The mark and the closing",
|
|
42172
42192
|
" card are drawn HERE, by the composition \u2014 never generated as a picture, because",
|
|
@@ -42176,18 +42196,19 @@ function buildAdBrandOverlay(opts) {
|
|
|
42176
42196
|
];
|
|
42177
42197
|
if (logo) {
|
|
42178
42198
|
parts.push(
|
|
42179
|
-
`<img class="clip brand-mark" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" data-start="0" data-dur="${totalS}"${markFallback(logo.alt)} />`
|
|
42199
|
+
`<img class="clip brand-mark${inkClass}" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" data-start="0" data-dur="${totalS}"${markFallback(logo.alt)} />`
|
|
42180
42200
|
);
|
|
42181
42201
|
}
|
|
42182
42202
|
const cardBits = [];
|
|
42183
42203
|
if (logo) {
|
|
42184
42204
|
cardBits.push(
|
|
42185
|
-
`<img class="endcard-logo" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}"${markFallback(logo.alt)} />`
|
|
42205
|
+
`<img class="endcard-logo${inkClass}" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}"${markFallback(logo.alt)} />`
|
|
42186
42206
|
);
|
|
42187
42207
|
}
|
|
42188
42208
|
if (cta?.trim()) {
|
|
42189
|
-
|
|
42190
|
-
|
|
42209
|
+
cardBits.push(
|
|
42210
|
+
`<div class="endcard-cta${opts.plateIsDark ? " on-dark-plate" : ""}">${escapeHtml2(cta.trim())}</div>`
|
|
42211
|
+
);
|
|
42191
42212
|
}
|
|
42192
42213
|
parts.push(
|
|
42193
42214
|
`<div class="clip endcard" data-start="${cardAtS}" data-dur="${cardDurationS}" data-role="cta">`,
|
|
@@ -42204,6 +42225,8 @@ function brandLayerDriver() {
|
|
|
42204
42225
|
" var els = Array.prototype.slice.call(document.querySelectorAll('.brand-mark, .endcard'));",
|
|
42205
42226
|
" if (!els.length) return;",
|
|
42206
42227
|
" els.forEach(function (el) { gsap.set(el, { visibility: 'hidden' }); });",
|
|
42228
|
+
" var card = document.querySelector('.endcard');",
|
|
42229
|
+
" var captions = document.getElementById('captions');",
|
|
42207
42230
|
" function attach(tl) {",
|
|
42208
42231
|
" if (!tl) return;",
|
|
42209
42232
|
" els.forEach(function (el) {",
|
|
@@ -42212,6 +42235,18 @@ function brandLayerDriver() {
|
|
|
42212
42235
|
" tl.set(el, { visibility: 'visible' }, at);",
|
|
42213
42236
|
" if (dur > 0) tl.set(el, { visibility: 'hidden' }, at + dur);",
|
|
42214
42237
|
" });",
|
|
42238
|
+
" // The closing card owns the frame it lands on. The caption track otherwise",
|
|
42239
|
+
" // keeps running underneath it, and what it captions there is the line spoken",
|
|
42240
|
+
" // OVER the card \u2014 the call to action, printed a second time in smaller letters",
|
|
42241
|
+
" // directly below the button that already says it. Hidden on the CONTAINER and",
|
|
42242
|
+
" // with `display`, because each caption group sets its own `visibility: visible`",
|
|
42243
|
+
" // and would win against anything set on its parent.",
|
|
42244
|
+
" if (card && captions) {",
|
|
42245
|
+
" var cardAt = parseFloat(card.getAttribute('data-start') || '0') || 0;",
|
|
42246
|
+
" var cardDur = parseFloat(card.getAttribute('data-dur') || '0') || 0;",
|
|
42247
|
+
" tl.set(captions, { display: 'none' }, cardAt);",
|
|
42248
|
+
" if (cardDur > 0) tl.set(captions, { display: '' }, cardAt + cardDur);",
|
|
42249
|
+
" }",
|
|
42215
42250
|
" }",
|
|
42216
42251
|
" var store = window.__timelines || {};",
|
|
42217
42252
|
" var main = store.main;",
|
|
@@ -42236,11 +42271,12 @@ function adBrandOverlayCss() {
|
|
|
42236
42271
|
" viewBox is wider than it is tall rendered clipped \u2014 the first real ad drew",
|
|
42237
42272
|
" `greenlea`. Constraining the height and letting the width follow keeps any",
|
|
42238
42273
|
" aspect ratio whole, and the max-width stops a very wide mark spanning the frame. */",
|
|
42239
|
-
" /* A LIGHT panel behind the mark
|
|
42240
|
-
" \u2014
|
|
42241
|
-
" and the brand-coloured closing plate erased it completely: the
|
|
42242
|
-
" out as an empty green frame with a single leaf floating in it. A
|
|
42243
|
-
" panel is the
|
|
42274
|
+
" /* A LIGHT panel behind the mark by DEFAULT. A brand mark is usually drawn to sit",
|
|
42275
|
+
" on white \u2014 the first one here was #1D5D47 on transparent \u2014 so a dark backing hid",
|
|
42276
|
+
" it on the corner and the brand-coloured closing plate erased it completely: the",
|
|
42277
|
+
" end card came out as an empty green frame with a single leaf floating in it. A",
|
|
42278
|
+
" near-white panel is the background most marks in a brand kit are designed",
|
|
42279
|
+
" against; the ones that are not get `.on-dark` below. */",
|
|
42244
42280
|
" .brand-mark { position: absolute; top: 90px; left: 56px; height: 52px; width: auto;",
|
|
42245
42281
|
" max-width: 420px; object-fit: contain; padding: 14px 22px; border-radius: 18px;",
|
|
42246
42282
|
" background: rgba(255,255,255,0.94);",
|
|
@@ -42254,6 +42290,14 @@ function adBrandOverlayCss() {
|
|
|
42254
42290
|
" .endcard-logo { width: 62%; height: auto; max-height: 300px; object-fit: contain;",
|
|
42255
42291
|
" padding: 46px 56px; border-radius: 40px; background: rgba(255,255,255,0.96);",
|
|
42256
42292
|
" box-shadow: 0 10px 40px rgba(0,0,0,0.28); }",
|
|
42293
|
+
" /* A mark whose own ink is LIGHT gets the opposite panel. The near-white panel",
|
|
42294
|
+
" above is right for the mark a brand kit usually ships \u2014 drawn to sit on white",
|
|
42295
|
+
" \u2014 and it erases the one it does not: a client's white `logo-dark.svg` rendered",
|
|
42296
|
+
" as an empty white box in the corner, and again 300px tall on the closing card.",
|
|
42297
|
+
" Which one a mark is gets MEASURED off the file (`markInkIsLight`), never read",
|
|
42298
|
+
" off its name: `logo-dark` names the background it is for in one brand kit and",
|
|
42299
|
+
" the ink itself in the next. */",
|
|
42300
|
+
" .brand-mark.on-dark, .endcard-logo.on-dark { background: rgba(16,20,18,0.92); }",
|
|
42257
42301
|
" /* The name, when the mark itself could not be drawn. */",
|
|
42258
42302
|
" .brand-wordmark { display: inline-block; font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
|
|
42259
42303
|
" font-weight: 900; color: #14231b; letter-spacing: -0.01em; }",
|
|
@@ -42268,10 +42312,17 @@ function adBrandOverlayCss() {
|
|
|
42268
42312
|
" first attempt led with 'BrandFont', which the overlay composition defines and",
|
|
42269
42313
|
" the caption composition does not: HyperFrames refuses an unmapped family and",
|
|
42270
42314
|
" the whole render died with `No deterministic font mapping for: BrandFont`. */",
|
|
42315
|
+
" /* The button is the OPPOSITE of the plate it sits on. Filling it with the brand",
|
|
42316
|
+
" colour made it invisible \u2014 the plate is filled with that same colour, so the",
|
|
42317
|
+
" button was brand-on-brand and its white copy read as another caption line \u2014",
|
|
42318
|
+
" and a fixed near-black fill is that defect mirrored, on the black plate an ad",
|
|
42319
|
+
" with no palette gets. The plate carries the brand; the button carries the",
|
|
42320
|
+
" contrast. Light copy on the near-black default, dark copy on the light one. */",
|
|
42271
42321
|
" .endcard-cta { font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
|
|
42272
42322
|
" font-size: 64px; font-weight: 900; text-transform: uppercase;",
|
|
42273
42323
|
" letter-spacing: 0.01em; color: #fff; padding: 28px 58px; border-radius: 56px;",
|
|
42274
|
-
" max-width: 900px; line-height: 1.15; background: rgba(0,0,0,0.82); }"
|
|
42324
|
+
" max-width: 900px; line-height: 1.15; background: rgba(0,0,0,0.82); }",
|
|
42325
|
+
" .endcard-cta.on-dark-plate { background: rgba(255,255,255,0.96); color: #14231b; }"
|
|
42275
42326
|
].join("\n");
|
|
42276
42327
|
}
|
|
42277
42328
|
function injectAdBrandOverlay(indexHtml, overlayHtml) {
|
|
@@ -42288,6 +42339,7 @@ function injectAdBrandOverlay(indexHtml, overlayHtml) {
|
|
|
42288
42339
|
// src/engine/scaffold/ad-brand-source.ts
|
|
42289
42340
|
import { readdir as readdir8, readFile as readFile15 } from "fs/promises";
|
|
42290
42341
|
import path22 from "path";
|
|
42342
|
+
import sharp from "sharp";
|
|
42291
42343
|
var BRAND_DIR = "src/brand";
|
|
42292
42344
|
function paletteFromBrandDoc(markdown) {
|
|
42293
42345
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -42296,15 +42348,33 @@ function paletteFromBrandDoc(markdown) {
|
|
|
42296
42348
|
const hex = `#${match[1].toLowerCase()}`;
|
|
42297
42349
|
if (seen.has(hex)) continue;
|
|
42298
42350
|
seen.add(hex);
|
|
42299
|
-
const
|
|
42300
|
-
|
|
42301
|
-
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
42302
|
-
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
42303
|
-
if (luma > 236 || luma < 14) continue;
|
|
42351
|
+
const luma = hexLuma(hex);
|
|
42352
|
+
if (luma === null || luma > 236 || luma < 14) continue;
|
|
42304
42353
|
out.push(hex);
|
|
42305
42354
|
}
|
|
42306
42355
|
return out;
|
|
42307
42356
|
}
|
|
42357
|
+
function hexLuma(hex) {
|
|
42358
|
+
const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
|
|
42359
|
+
if (!m) return null;
|
|
42360
|
+
const v = m[1];
|
|
42361
|
+
const r = Number.parseInt(v.slice(0, 2), 16);
|
|
42362
|
+
const g = Number.parseInt(v.slice(2, 4), 16);
|
|
42363
|
+
const b = Number.parseInt(v.slice(4, 6), 16);
|
|
42364
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
42365
|
+
}
|
|
42366
|
+
function firstHex(palette) {
|
|
42367
|
+
const hex = (palette ?? []).find((h) => /^#?[0-9a-fA-F]{6}$/.test(h.trim()));
|
|
42368
|
+
if (!hex) return null;
|
|
42369
|
+
const trimmed = hex.trim();
|
|
42370
|
+
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
42371
|
+
}
|
|
42372
|
+
function plateIsDark(palette) {
|
|
42373
|
+
const luma = hexLuma(firstHex(palette) ?? "");
|
|
42374
|
+
return luma === null || luma < PLATE_DARK_LUMA;
|
|
42375
|
+
}
|
|
42376
|
+
var PLATE_DARK_LUMA = 140;
|
|
42377
|
+
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/;
|
|
42308
42378
|
function pickBrandMark(files) {
|
|
42309
42379
|
const usable = files.filter((f) => /\.(svg|png|webp)$/i.test(f));
|
|
42310
42380
|
if (usable.length === 0) return null;
|
|
@@ -42313,10 +42383,10 @@ function pickBrandMark(files) {
|
|
|
42313
42383
|
let s = 0;
|
|
42314
42384
|
if (/\.(png|webp)$/.test(name)) s += 1;
|
|
42315
42385
|
if (/\b(logo|wordmark|lockup)\b/.test(name)) s += 2;
|
|
42316
|
-
if (
|
|
42386
|
+
if (MARK_VARIANT_NAMES.test(name)) s -= 3;
|
|
42317
42387
|
return s;
|
|
42318
42388
|
};
|
|
42319
|
-
return [...usable].sort((a, b) => score(b) - score(a) || a.localeCompare(b))[0] ?? null;
|
|
42389
|
+
return [...usable].sort((a, b) => score(b) - score(a) || a.length - b.length || a.localeCompare(b))[0] ?? null;
|
|
42320
42390
|
}
|
|
42321
42391
|
async function readBrandFromWorkspace(root = ".") {
|
|
42322
42392
|
const found = {};
|
|
@@ -42371,6 +42441,27 @@ async function markDirectoryIsReal(dir) {
|
|
|
42371
42441
|
}
|
|
42372
42442
|
return true;
|
|
42373
42443
|
}
|
|
42444
|
+
var INK_ALPHA = 128;
|
|
42445
|
+
function inkLuma(rgba) {
|
|
42446
|
+
let sum = 0;
|
|
42447
|
+
let count = 0;
|
|
42448
|
+
for (let i = 0; i + 3 < rgba.length; i += 4) {
|
|
42449
|
+
if (rgba[i + 3] < INK_ALPHA) continue;
|
|
42450
|
+
sum += 0.2126 * rgba[i] + 0.7152 * rgba[i + 1] + 0.0722 * rgba[i + 2];
|
|
42451
|
+
count += 1;
|
|
42452
|
+
}
|
|
42453
|
+
return count === 0 ? null : sum / count;
|
|
42454
|
+
}
|
|
42455
|
+
var LIGHT_INK_LUMA = 170;
|
|
42456
|
+
async function markInkIsLight(markPath) {
|
|
42457
|
+
try {
|
|
42458
|
+
const { data } = await sharp(markPath, { density: 144 }).resize(96, 96, { fit: "inside", withoutEnlargement: true }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
42459
|
+
const luma = inkLuma(data);
|
|
42460
|
+
return luma !== null && luma > LIGHT_INK_LUMA;
|
|
42461
|
+
} catch {
|
|
42462
|
+
return false;
|
|
42463
|
+
}
|
|
42464
|
+
}
|
|
42374
42465
|
|
|
42375
42466
|
// src/commands/canvas/composition-path.ts
|
|
42376
42467
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -42434,6 +42525,11 @@ registerSchema({
|
|
|
42434
42525
|
}
|
|
42435
42526
|
}
|
|
42436
42527
|
});
|
|
42528
|
+
function avatarVoiceBrief(avatar) {
|
|
42529
|
+
if (!avatar) return void 0;
|
|
42530
|
+
const parts = [avatar.subjectDescription, avatar.profile?.persona, avatar.profile?.speech].map((p) => p?.trim()).filter((p) => Boolean(p));
|
|
42531
|
+
return parts.length > 0 ? parts.join("; ") : void 0;
|
|
42532
|
+
}
|
|
42437
42533
|
var scaffoldAdCommand = defineCommand114({
|
|
42438
42534
|
meta: {
|
|
42439
42535
|
name: "scaffold-ad",
|
|
@@ -42676,7 +42772,12 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42676
42772
|
spec.data.cast = { ...spec.data.cast, description: withoutCastName(spec.data.cast.description, names) };
|
|
42677
42773
|
}
|
|
42678
42774
|
}
|
|
42679
|
-
const
|
|
42775
|
+
const avatarVoice = avatarVoiceBrief(avatar);
|
|
42776
|
+
const blueprint = adSpecToBlueprint(spec.data, {
|
|
42777
|
+
avatarAccent: avatar?.profile?.accent,
|
|
42778
|
+
videoModel: chosenVideoModel,
|
|
42779
|
+
...avatarVoice ? { avatarVoice } : {}
|
|
42780
|
+
});
|
|
42680
42781
|
const slug = args4.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
|
|
42681
42782
|
const outPath = args4.out ?? (args4.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));
|
|
42682
42783
|
const outDir = path24.dirname(outPath);
|
|
@@ -42726,6 +42827,19 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42726
42827
|
// and rejected — it would swap the voice that matches the mouth and the directed
|
|
42727
42828
|
// region for one cast off an English stock list. Only the on-camera voice; a
|
|
42728
42829
|
// voiceover read has no rendered voice to keep and is still cast.
|
|
42830
|
+
//
|
|
42831
|
+
// The KNOWN RISK this carries, recorded here because it is decided here: keeping the
|
|
42832
|
+
// rendered voice also means no pass unifies her beats, and each beat is its own
|
|
42833
|
+
// generation, so nothing guarantees two of them sound like the same person. The
|
|
42834
|
+
// reference image fixes the face; nothing fixes the voice. Measured on a finished
|
|
42835
|
+
// three-beat ad (her, a cutaway, her): median speaking pitch 210.5 Hz and 216.2 Hz,
|
|
42836
|
+
// near-identical spreads — the same voice as far as pitch can say — and the two
|
|
42837
|
+
// extracts were then listened to side by side and judged to be the same person. Not
|
|
42838
|
+
// proof for every ad: one listener, one ad, and pitch is one dimension of a voice.
|
|
42839
|
+
//
|
|
42840
|
+
// If it does show up, the fix is not to drop `keepRenderedVoice` — that brings back
|
|
42841
|
+
// the stock voice this exists to avoid. It is to convert the LATER extracts onto the
|
|
42842
|
+
// first beat of the same speaker, which keeps a voice the clip really produced.
|
|
42729
42843
|
...avatar ? { keepRenderedVoice: true } : {},
|
|
42730
42844
|
...spec.data.voiceover === false ? { staticTranscriptPath: path24.relative(outDir, transcriptPath) } : {}
|
|
42731
42845
|
};
|
|
@@ -42733,11 +42847,13 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42733
42847
|
const renderNode4 = canvas.nodes.find((n) => n.type === "hyperframe_render");
|
|
42734
42848
|
const renderedDir = renderNode4 ? path24.join(outDir, String(renderNode4.params?.composition ?? "")) : null;
|
|
42735
42849
|
let staged = null;
|
|
42850
|
+
let markIsLight = false;
|
|
42736
42851
|
if (logoPath && renderedDir) {
|
|
42737
42852
|
const ext = path24.extname(logoPath) || ".png";
|
|
42738
42853
|
try {
|
|
42739
42854
|
await copyFile(logoPath, path24.join(renderedDir, `brand-mark${ext}`));
|
|
42740
42855
|
staged = { file: `brand-mark${ext}`, alt: spec.data.brand?.name ?? "brand" };
|
|
42856
|
+
markIsLight = await markInkIsLight(logoPath);
|
|
42741
42857
|
} catch {
|
|
42742
42858
|
}
|
|
42743
42859
|
}
|
|
@@ -42748,7 +42864,10 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42748
42864
|
cardAtS: closing.start_s,
|
|
42749
42865
|
cardDurationS: closing.duration_s,
|
|
42750
42866
|
totalS: blueprint.estimated_duration_s,
|
|
42751
|
-
|
|
42867
|
+
markIsLight,
|
|
42868
|
+
// The card's button is coloured against the plate the engine will fill from
|
|
42869
|
+
// this same palette — including the black plate an ad with no palette gets.
|
|
42870
|
+
plateIsDark: plateIsDark(spec.data.brand?.palette)
|
|
42752
42871
|
}) : "";
|
|
42753
42872
|
if (overlayHtml && renderedDir) {
|
|
42754
42873
|
const indexPath = path24.join(renderedDir, "index.html");
|
|
@@ -42823,7 +42942,7 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42823
42942
|
// ad with no palette and no mark anywhere in 28 seconds.
|
|
42824
42943
|
`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.`
|
|
42825
42944
|
],
|
|
42826
|
-
"`brand.palette`
|
|
42945
|
+
"`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."
|
|
42827
42946
|
]
|
|
42828
42947
|
});
|
|
42829
42948
|
}
|
|
@@ -51323,9 +51442,9 @@ import { defineCommand as defineCommand169 } from "citty";
|
|
|
51323
51442
|
import { defineCommand as defineCommand143 } from "citty";
|
|
51324
51443
|
|
|
51325
51444
|
// src/lib/image/crop-sprite.ts
|
|
51326
|
-
import
|
|
51445
|
+
import sharp2 from "sharp";
|
|
51327
51446
|
function cropSprite(input, region) {
|
|
51328
|
-
return
|
|
51447
|
+
return sharp2(input).extract({
|
|
51329
51448
|
left: Math.round(region.x),
|
|
51330
51449
|
top: Math.round(region.y),
|
|
51331
51450
|
width: Math.round(region.width),
|
|
@@ -53585,7 +53704,7 @@ function solidifyEdges(data, saturationBoost = 0.25) {
|
|
|
53585
53704
|
}
|
|
53586
53705
|
|
|
53587
53706
|
// src/lib/image/image-processor.ts
|
|
53588
|
-
import
|
|
53707
|
+
import sharp3 from "sharp";
|
|
53589
53708
|
function hasGradient(data) {
|
|
53590
53709
|
const uniqueColors = /* @__PURE__ */ new Set();
|
|
53591
53710
|
for (let i = 0; i < data.length; i += 4) {
|
|
@@ -53617,10 +53736,10 @@ function createSvgRecolorer(target, palette) {
|
|
|
53617
53736
|
async function processGradient(data, info) {
|
|
53618
53737
|
if (!hasGradient(data)) return { data, info };
|
|
53619
53738
|
const solidified = solidifyEdges(data, 0.1);
|
|
53620
|
-
const blurred = await
|
|
53739
|
+
const blurred = await sharp3(solidified, {
|
|
53621
53740
|
raw: { channels: 4, width: info.width, height: info.height }
|
|
53622
53741
|
}).png().blur(0.3).toBuffer();
|
|
53623
|
-
const result = await
|
|
53742
|
+
const result = await sharp3(blurred).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
53624
53743
|
return { data: result.data, info: result.info };
|
|
53625
53744
|
}
|
|
53626
53745
|
function isSvgBuffer(buffer) {
|
|
@@ -53629,13 +53748,13 @@ function isSvgBuffer(buffer) {
|
|
|
53629
53748
|
}
|
|
53630
53749
|
async function processInternal(inputBuffer, isSVG, options) {
|
|
53631
53750
|
const stages = [];
|
|
53632
|
-
const metadata = await
|
|
53751
|
+
const metadata = await sharp3(inputBuffer).metadata();
|
|
53633
53752
|
let alreadyTransparent = false;
|
|
53634
53753
|
if (metadata.hasAlpha) {
|
|
53635
|
-
const { data: alphaData } = await
|
|
53754
|
+
const { data: alphaData } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
|
|
53636
53755
|
alreadyTransparent = hasTransparency(alphaData, 0.05);
|
|
53637
53756
|
}
|
|
53638
|
-
let { data: processedData, info } = await
|
|
53757
|
+
let { data: processedData, info } = await sharp3(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
53639
53758
|
if (options.color) {
|
|
53640
53759
|
stages.push("recolor");
|
|
53641
53760
|
processedData = applyQuantization(processedData, 8);
|
|
@@ -53706,7 +53825,7 @@ async function processInternal(inputBuffer, isSVG, options) {
|
|
|
53706
53825
|
processedData = result.data;
|
|
53707
53826
|
info = result.info;
|
|
53708
53827
|
}
|
|
53709
|
-
let pipeline2 =
|
|
53828
|
+
let pipeline2 = sharp3(processedData, {
|
|
53710
53829
|
raw: { channels: 4, width: info.width, height: info.height }
|
|
53711
53830
|
}).png();
|
|
53712
53831
|
if (options.shrinkToContent) {
|
|
@@ -53719,12 +53838,12 @@ async function processInternal(inputBuffer, isSVG, options) {
|
|
|
53719
53838
|
function applyResize(buffer, resize) {
|
|
53720
53839
|
const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
53721
53840
|
if (resize.size) {
|
|
53722
|
-
return
|
|
53841
|
+
return sharp3(buffer).resize(resize.size.width, resize.size.height, {
|
|
53723
53842
|
fit: resize.fit ?? "contain",
|
|
53724
53843
|
background: transparent
|
|
53725
53844
|
}).toBuffer();
|
|
53726
53845
|
}
|
|
53727
|
-
return
|
|
53846
|
+
return sharp3(buffer).resize(resize.width ?? null, resize.height ?? null, { fit: "inside", withoutEnlargement: false }).toBuffer();
|
|
53728
53847
|
}
|
|
53729
53848
|
async function processImage(inputBuffer, options = {}) {
|
|
53730
53849
|
const stages = [];
|
|
@@ -53733,7 +53852,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
53733
53852
|
if (options.resize) {
|
|
53734
53853
|
stages.push("resize");
|
|
53735
53854
|
const resized = await applyResize(inputBuffer, options.resize);
|
|
53736
|
-
const meta3 = await
|
|
53855
|
+
const meta3 = await sharp3(resized).metadata();
|
|
53737
53856
|
return {
|
|
53738
53857
|
buffer: resized,
|
|
53739
53858
|
format: "png",
|
|
@@ -53742,7 +53861,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
53742
53861
|
stages
|
|
53743
53862
|
};
|
|
53744
53863
|
}
|
|
53745
|
-
const meta2 = await
|
|
53864
|
+
const meta2 = await sharp3(inputBuffer).metadata();
|
|
53746
53865
|
return {
|
|
53747
53866
|
buffer: inputBuffer,
|
|
53748
53867
|
format: "svg",
|
|
@@ -53754,7 +53873,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
53754
53873
|
let workingBuffer = inputBuffer;
|
|
53755
53874
|
if (isSVG) {
|
|
53756
53875
|
stages.push("rasterize-svg");
|
|
53757
|
-
workingBuffer = await
|
|
53876
|
+
workingBuffer = await sharp3(inputBuffer).png({ compressionLevel: 0, force: true, palette: false, quality: 100 }).toBuffer();
|
|
53758
53877
|
}
|
|
53759
53878
|
const pass = await processInternal(workingBuffer, isSVG, options);
|
|
53760
53879
|
stages.push(...pass.stages);
|
|
@@ -53763,7 +53882,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
53763
53882
|
stages.push("resize");
|
|
53764
53883
|
processed = await applyResize(processed, options.resize);
|
|
53765
53884
|
}
|
|
53766
|
-
const meta = await
|
|
53885
|
+
const meta = await sharp3(processed).metadata();
|
|
53767
53886
|
return {
|
|
53768
53887
|
buffer: processed,
|
|
53769
53888
|
format: "png",
|
|
@@ -57382,7 +57501,7 @@ function routeKey(route) {
|
|
|
57382
57501
|
}
|
|
57383
57502
|
|
|
57384
57503
|
// src/engine/landing-library/fidelity.ts
|
|
57385
|
-
import
|
|
57504
|
+
import sharp4 from "sharp";
|
|
57386
57505
|
var COMPARISON_SIZE = 32;
|
|
57387
57506
|
function pixelSimilarity(a, b) {
|
|
57388
57507
|
const length = Math.min(a.length, b.length);
|
|
@@ -57394,7 +57513,7 @@ function pixelSimilarity(a, b) {
|
|
|
57394
57513
|
return 1 - total / (length * 255);
|
|
57395
57514
|
}
|
|
57396
57515
|
async function scoreFidelity(live, rendered) {
|
|
57397
|
-
const [liveMeta, renderedMeta] = await Promise.all([
|
|
57516
|
+
const [liveMeta, renderedMeta] = await Promise.all([sharp4(live).metadata(), sharp4(rendered).metadata()]);
|
|
57398
57517
|
const liveSize = { width: liveMeta.width ?? 0, height: liveMeta.height ?? 0 };
|
|
57399
57518
|
const renderedSize = { width: renderedMeta.width ?? 0, height: renderedMeta.height ?? 0 };
|
|
57400
57519
|
const [livePixels, renderedPixels] = await Promise.all([toGreyGrid(live), toGreyGrid(rendered)]);
|
|
@@ -57408,7 +57527,7 @@ async function scoreFidelity(live, rendered) {
|
|
|
57408
57527
|
};
|
|
57409
57528
|
}
|
|
57410
57529
|
async function toGreyGrid(image) {
|
|
57411
|
-
const raw = await
|
|
57530
|
+
const raw = await sharp4(image).greyscale().resize(COMPARISON_SIZE, COMPARISON_SIZE, { fit: "fill" }).raw().toBuffer();
|
|
57412
57531
|
return new Uint8Array(raw);
|
|
57413
57532
|
}
|
|
57414
57533
|
|
|
@@ -57620,7 +57739,7 @@ var inPageCollectMotion = (selector) => {
|
|
|
57620
57739
|
};
|
|
57621
57740
|
|
|
57622
57741
|
// src/engine/landing-library/motionTake.ts
|
|
57623
|
-
import
|
|
57742
|
+
import sharp5 from "sharp";
|
|
57624
57743
|
|
|
57625
57744
|
// src/engine/landing-library/certificate.ts
|
|
57626
57745
|
function formatDate(unixSeconds) {
|
|
@@ -58287,8 +58406,8 @@ async function hideFloatingFurniture(page, selector) {
|
|
|
58287
58406
|
}, selector);
|
|
58288
58407
|
}
|
|
58289
58408
|
async function composeFilmstrip(frames) {
|
|
58290
|
-
const scaled = await Promise.all(frames.map((frame) =>
|
|
58291
|
-
const first = await
|
|
58409
|
+
const scaled = await Promise.all(frames.map((frame) => sharp5(frame).resize({ width: FRAME_WIDTH }).png().toBuffer()));
|
|
58410
|
+
const first = await sharp5(scaled[0]).metadata();
|
|
58292
58411
|
const frameHeight = first.height ?? 300;
|
|
58293
58412
|
const cellHeight = frameHeight + LABEL_HEIGHT;
|
|
58294
58413
|
const rows = Math.ceil(scaled.length / GRID_COLUMNS);
|
|
@@ -58307,7 +58426,7 @@ async function composeFilmstrip(frames) {
|
|
|
58307
58426
|
{ input: frame, left: column * FRAME_WIDTH, top: row * cellHeight + LABEL_HEIGHT }
|
|
58308
58427
|
];
|
|
58309
58428
|
});
|
|
58310
|
-
return await
|
|
58429
|
+
return await sharp5({
|
|
58311
58430
|
create: {
|
|
58312
58431
|
width,
|
|
58313
58432
|
height: cellHeight * rows,
|
|
@@ -58496,12 +58615,12 @@ var DEFAULT_SEGMENT_OPTIONS = {
|
|
|
58496
58615
|
};
|
|
58497
58616
|
|
|
58498
58617
|
// src/engine/landing-library/visualHash.ts
|
|
58499
|
-
import
|
|
58618
|
+
import sharp6 from "sharp";
|
|
58500
58619
|
var HASH_WIDTH = 9;
|
|
58501
58620
|
var HASH_HEIGHT = 8;
|
|
58502
58621
|
async function perceptualHash(image) {
|
|
58503
58622
|
try {
|
|
58504
|
-
const raw = await
|
|
58623
|
+
const raw = await sharp6(image).greyscale().resize(HASH_WIDTH, HASH_HEIGHT, { fit: "fill" }).raw().toBuffer();
|
|
58505
58624
|
return bitsToHex(rowGradientBits(new Uint8Array(raw)));
|
|
58506
58625
|
} catch {
|
|
58507
58626
|
return null;
|
|
@@ -59015,7 +59134,7 @@ import { defineCommand as defineCommand176 } from "citty";
|
|
|
59015
59134
|
// src/commands/landing/inspiration/shot.ts
|
|
59016
59135
|
import { mkdir as mkdir13, writeFile as writeFile18 } from "fs/promises";
|
|
59017
59136
|
import path42 from "path";
|
|
59018
|
-
import
|
|
59137
|
+
import sharp7 from "sharp";
|
|
59019
59138
|
var READABLE_SHOT = {
|
|
59020
59139
|
maxWidth: 1440,
|
|
59021
59140
|
maxHeight: 4e3,
|
|
@@ -59024,7 +59143,7 @@ var READABLE_SHOT = {
|
|
|
59024
59143
|
};
|
|
59025
59144
|
async function toReadableShot(original) {
|
|
59026
59145
|
try {
|
|
59027
|
-
return await
|
|
59146
|
+
return await sharp7(original).resize({
|
|
59028
59147
|
width: READABLE_SHOT.maxWidth,
|
|
59029
59148
|
height: READABLE_SHOT.maxHeight,
|
|
59030
59149
|
fit: "inside",
|