@koda-sl/baker-cli 0.319.0 → 0.321.0-dev.2d7a8f01b
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-OLAB3JMW.js} +12 -7
- package/dist/chunk-OLAB3JMW.js.map +1 -0
- package/dist/cli.js +213 -72
- 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-OLAB3JMW.js";
|
|
71
71
|
import {
|
|
72
72
|
csvOrJson,
|
|
73
73
|
daysAgoIso,
|
|
@@ -14808,6 +14808,14 @@ var analyticsQueryRequestSchema = z31.object({
|
|
|
14808
14808
|
* came for — a deal a CRM posted names the person and carries no cookie.
|
|
14809
14809
|
*/
|
|
14810
14810
|
anonId: z31.string().max(120).optional(),
|
|
14811
|
+
/**
|
|
14812
|
+
* `stream` and `session`: one person by any key the store recognises them
|
|
14813
|
+
* by — a contact digest, a CRM id, a click id, a platform cookie — for the
|
|
14814
|
+
* rows a system posted about somebody no browser has named yet. A browser
|
|
14815
|
+
* wins when both are given.
|
|
14816
|
+
*/
|
|
14817
|
+
personKind: z31.string().max(40).optional(),
|
|
14818
|
+
personValue: z31.string().max(255).optional(),
|
|
14811
14819
|
/**
|
|
14812
14820
|
* `people` only: keep only the visitors who converted.
|
|
14813
14821
|
*
|
|
@@ -14844,9 +14852,9 @@ var analyticsQueryRequestSchema = z31.object({
|
|
|
14844
14852
|
message: "The 'page' preset needs --path",
|
|
14845
14853
|
path: ["path"]
|
|
14846
14854
|
}).refine(
|
|
14847
|
-
(value) => value.preset !== "session" || typeof value.sessionId === "string" || typeof value.anonId === "string",
|
|
14855
|
+
(value) => value.preset !== "session" || typeof value.sessionId === "string" || typeof value.anonId === "string" || typeof value.personValue === "string",
|
|
14848
14856
|
{
|
|
14849
|
-
message: "The 'session' preset needs --session or a
|
|
14857
|
+
message: "The 'session' preset needs --session, a browser id or a person key",
|
|
14850
14858
|
path: ["sessionId"]
|
|
14851
14859
|
}
|
|
14852
14860
|
).refine((value) => value.property === void 0 || typeof value.eventName === "string", {
|
|
@@ -14877,6 +14885,13 @@ var analyticsTotalsSchema = z31.object({
|
|
|
14877
14885
|
* {@link convertingSessions}, and of `sessions`.
|
|
14878
14886
|
*/
|
|
14879
14887
|
conversions: z31.number().int().nonnegative(),
|
|
14888
|
+
/**
|
|
14889
|
+
* Conversions in the window that no landing earned — by people none of the
|
|
14890
|
+
* company's pages saw. Not in `conversions`, which is a page metric; shown
|
|
14891
|
+
* beside it so a deal a CRM reported is never a silent zero. Zero under any
|
|
14892
|
+
* scope.
|
|
14893
|
+
*/
|
|
14894
|
+
unplacedConversions: z31.number().int().nonnegative().default(0),
|
|
14880
14895
|
/** Visits that produced at least one conversion. Never above `sessions`. */
|
|
14881
14896
|
convertingSessions: z31.number().int().nonnegative(),
|
|
14882
14897
|
/**
|
|
@@ -15369,8 +15384,14 @@ var analyticsConversionRowSchema = z31.object({
|
|
|
15369
15384
|
* accident; saying so is what makes it findable.
|
|
15370
15385
|
*/
|
|
15371
15386
|
countMode: z31.string(),
|
|
15372
|
-
/** Counted the way each definition asked to be counted. */
|
|
15387
|
+
/** Counted the way each definition asked to be counted, and credited to a landing. */
|
|
15373
15388
|
conversions: z31.number(),
|
|
15389
|
+
/**
|
|
15390
|
+
* Times it happened that no landing earned: by people none of the company's
|
|
15391
|
+
* pages saw, within 90 days. Zero under a landing or an ad scope, where the
|
|
15392
|
+
* gap would mean something else. Absent on a row built before it existed.
|
|
15393
|
+
*/
|
|
15394
|
+
unplaced: z31.number().default(0),
|
|
15374
15395
|
/** Distinct visits that produced it, never mode-adjusted. The gap between the two is the repeat behaviour. */
|
|
15375
15396
|
convertingVisits: z31.number(),
|
|
15376
15397
|
/** Null when it has never fired. */
|
|
@@ -30352,6 +30373,7 @@ function mergeStoredWithCounts(stored, reported) {
|
|
|
30352
30373
|
// would make it indistinguishable from one nobody has named.
|
|
30353
30374
|
conversions: counted?.conversions ?? 0,
|
|
30354
30375
|
convertingVisits: counted?.convertingVisits ?? 0,
|
|
30376
|
+
unplaced: counted?.unplaced ?? 0,
|
|
30355
30377
|
lastSeen: counted?.lastSeen ?? null,
|
|
30356
30378
|
doors: counted?.doors ?? [],
|
|
30357
30379
|
...rows.some((row) => row.staged === true) ? { staged: true } : {}
|
|
@@ -41911,6 +41933,7 @@ function endCardCta(spec) {
|
|
|
41911
41933
|
return declared.toLowerCase().replace(/[.!?]$/, "") === closing.replace(/[.!?]$/, "") ? null : declared;
|
|
41912
41934
|
}
|
|
41913
41935
|
var AD_CAST_ID = "customer";
|
|
41936
|
+
var AD_NARRATOR_ID = "ad_narrator";
|
|
41914
41937
|
var AD_SET_ID = "set";
|
|
41915
41938
|
function castedAvatarOnRoster(ad, roster) {
|
|
41916
41939
|
if (ad.cast?.avatar) return null;
|
|
@@ -41966,9 +41989,9 @@ function adSpecCastElements(spec, avatar) {
|
|
|
41966
41989
|
});
|
|
41967
41990
|
return elements;
|
|
41968
41991
|
}
|
|
41969
|
-
function motionPrompt(beat, toCamera, isClosingCard, place
|
|
41992
|
+
function motionPrompt(beat, toCamera, isClosingCard, place) {
|
|
41970
41993
|
if (isClosingCard) return BRAND_PLATE;
|
|
41971
|
-
const shot = `${beat.show}${place}
|
|
41994
|
+
const shot = `${beat.show}${place}`;
|
|
41972
41995
|
if (toCamera) return shot;
|
|
41973
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.`;
|
|
41974
41997
|
}
|
|
@@ -42016,11 +42039,9 @@ function beatsIntroducingSpeakerOffCamera(spec) {
|
|
|
42016
42039
|
(beat, i) => lineIntroducesSpeaker(beat.say) && !beatToCamera(spec, beat, closesOnCard && i === last) ? [i] : []
|
|
42017
42040
|
);
|
|
42018
42041
|
}
|
|
42019
|
-
function
|
|
42042
|
+
function beatSpeaksNatively(spec, beat, isClosingCard) {
|
|
42020
42043
|
if (!spec.cast || spec.voiceover === false) return false;
|
|
42021
|
-
|
|
42022
|
-
const speaking = spec.beats.filter((_, i) => !(closesOnCard && i === last));
|
|
42023
|
-
return speaking.length > 0 && speaking.every((beat) => beatToCamera(spec, beat, false) && beat.cast !== false);
|
|
42044
|
+
return beatToCamera(spec, beat, isClosingCard) && beat.cast !== false;
|
|
42024
42045
|
}
|
|
42025
42046
|
function beatDialogue(spec, beat, window2) {
|
|
42026
42047
|
if (spec.voiceover === false) return [];
|
|
@@ -42037,14 +42058,38 @@ function beatDialogue(spec, beat, window2) {
|
|
|
42037
42058
|
// by definition never on camera — so every beat took the dubbed path: a clip generated
|
|
42038
42059
|
// with one mouth and a separate ElevenLabs track laid over it. That is the mismatch
|
|
42039
42060
|
// behind every "the mouth does not match" in this ad's history.
|
|
42040
|
-
|
|
42041
|
-
|
|
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) } : {}
|
|
42042
42077
|
}
|
|
42043
42078
|
];
|
|
42044
42079
|
}
|
|
42045
|
-
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) {
|
|
42046
42088
|
if (spec.voiceover === false) return "none";
|
|
42047
|
-
|
|
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";
|
|
42048
42093
|
}
|
|
42049
42094
|
function castRole(spec) {
|
|
42050
42095
|
if (!spec.cast) return void 0;
|
|
@@ -42061,7 +42106,8 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
42061
42106
|
const currency = market ? CURRENCY_BY_MARKET[market.toLowerCase()] : void 0;
|
|
42062
42107
|
const physics = frameRealismDirection({ currency, role: castRole(spec) });
|
|
42063
42108
|
const closesOnCard = endCardWanted(spec);
|
|
42064
|
-
const
|
|
42109
|
+
const nativeByBeat = nativeSpeechByBeat(spec, closesOnCard);
|
|
42110
|
+
const spokenMode = voiceoverMode(spec, nativeByBeat, closesOnCard);
|
|
42065
42111
|
let clock = 0;
|
|
42066
42112
|
const scenes = spec.beats.map((beat, i) => {
|
|
42067
42113
|
const isClosingCard = closesOnCard && i === total - 1;
|
|
@@ -42090,9 +42136,10 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
42090
42136
|
start: Math.round(start * 100) / 100,
|
|
42091
42137
|
end: Math.round(clock * 100) / 100,
|
|
42092
42138
|
isClosingCard,
|
|
42093
|
-
nativeSpeech
|
|
42139
|
+
nativeSpeech: nativeByBeat[i] === true,
|
|
42140
|
+
avatarVoice: opts.avatarVoice
|
|
42094
42141
|
}),
|
|
42095
|
-
motion_prompt: motionPrompt(beat, beatToCamera(spec, beat, isClosingCard), isClosingCard, place
|
|
42142
|
+
motion_prompt: motionPrompt(beat, beatToCamera(spec, beat, isClosingCard), isClosingCard, place),
|
|
42096
42143
|
start_frame_prompt: isClosingCard ? BRAND_PLATE : `${beat.show}${place}${physics}`
|
|
42097
42144
|
};
|
|
42098
42145
|
});
|
|
@@ -42104,9 +42151,9 @@ function adSpecToBlueprint(spec, opts = {}) {
|
|
|
42104
42151
|
// the script itself, so an ad that never named a place still says where it is set.
|
|
42105
42152
|
...market ? { market } : {},
|
|
42106
42153
|
voiceover: {
|
|
42107
|
-
mode:
|
|
42154
|
+
mode: spokenMode,
|
|
42108
42155
|
...spokenVariety(spec, avatarAccent) ? { accent: spokenVariety(spec, avatarAccent) } : {},
|
|
42109
|
-
...spec.
|
|
42156
|
+
...narratorVoice(spec, opts.avatarVoice) ? { voice_description: narratorVoice(spec, opts.avatarVoice) } : {}
|
|
42110
42157
|
},
|
|
42111
42158
|
...spec.cast ? {
|
|
42112
42159
|
cast: [
|
|
@@ -42136,15 +42183,10 @@ function beatsTooShortToPlay(spec) {
|
|
|
42136
42183
|
// src/engine/scaffold/ad-brand-overlay.ts
|
|
42137
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()))}}))"` : "";
|
|
42138
42185
|
var escapeHtml2 = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
42139
|
-
function firstHex(palette) {
|
|
42140
|
-
const hex = (palette ?? []).find((h) => /^#?[0-9a-fA-F]{6}$/.test(h.trim()));
|
|
42141
|
-
if (!hex) return null;
|
|
42142
|
-
const trimmed = hex.trim();
|
|
42143
|
-
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
|
|
42144
|
-
}
|
|
42145
42186
|
function buildAdBrandOverlay(opts) {
|
|
42146
|
-
const { logo, cta, cardAtS, cardDurationS, totalS
|
|
42187
|
+
const { logo, cta, cardAtS, cardDurationS, totalS } = opts;
|
|
42147
42188
|
if (!logo && !cta?.trim()) return "";
|
|
42189
|
+
const inkClass = opts.markIsLight ? " on-dark" : "";
|
|
42148
42190
|
const parts = [
|
|
42149
42191
|
"<!-- Brand layer, emitted by `baker canvas scaffold-ad`. The mark and the closing",
|
|
42150
42192
|
" card are drawn HERE, by the composition \u2014 never generated as a picture, because",
|
|
@@ -42154,18 +42196,19 @@ function buildAdBrandOverlay(opts) {
|
|
|
42154
42196
|
];
|
|
42155
42197
|
if (logo) {
|
|
42156
42198
|
parts.push(
|
|
42157
|
-
`<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)} />`
|
|
42158
42200
|
);
|
|
42159
42201
|
}
|
|
42160
42202
|
const cardBits = [];
|
|
42161
42203
|
if (logo) {
|
|
42162
42204
|
cardBits.push(
|
|
42163
|
-
`<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)} />`
|
|
42164
42206
|
);
|
|
42165
42207
|
}
|
|
42166
42208
|
if (cta?.trim()) {
|
|
42167
|
-
|
|
42168
|
-
|
|
42209
|
+
cardBits.push(
|
|
42210
|
+
`<div class="endcard-cta${opts.plateIsDark ? " on-dark-plate" : ""}">${escapeHtml2(cta.trim())}</div>`
|
|
42211
|
+
);
|
|
42169
42212
|
}
|
|
42170
42213
|
parts.push(
|
|
42171
42214
|
`<div class="clip endcard" data-start="${cardAtS}" data-dur="${cardDurationS}" data-role="cta">`,
|
|
@@ -42182,6 +42225,8 @@ function brandLayerDriver() {
|
|
|
42182
42225
|
" var els = Array.prototype.slice.call(document.querySelectorAll('.brand-mark, .endcard'));",
|
|
42183
42226
|
" if (!els.length) return;",
|
|
42184
42227
|
" els.forEach(function (el) { gsap.set(el, { visibility: 'hidden' }); });",
|
|
42228
|
+
" var card = document.querySelector('.endcard');",
|
|
42229
|
+
" var captions = document.getElementById('captions');",
|
|
42185
42230
|
" function attach(tl) {",
|
|
42186
42231
|
" if (!tl) return;",
|
|
42187
42232
|
" els.forEach(function (el) {",
|
|
@@ -42190,6 +42235,18 @@ function brandLayerDriver() {
|
|
|
42190
42235
|
" tl.set(el, { visibility: 'visible' }, at);",
|
|
42191
42236
|
" if (dur > 0) tl.set(el, { visibility: 'hidden' }, at + dur);",
|
|
42192
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
|
+
" }",
|
|
42193
42250
|
" }",
|
|
42194
42251
|
" var store = window.__timelines || {};",
|
|
42195
42252
|
" var main = store.main;",
|
|
@@ -42214,11 +42271,12 @@ function adBrandOverlayCss() {
|
|
|
42214
42271
|
" viewBox is wider than it is tall rendered clipped \u2014 the first real ad drew",
|
|
42215
42272
|
" `greenlea`. Constraining the height and letting the width follow keeps any",
|
|
42216
42273
|
" aspect ratio whole, and the max-width stops a very wide mark spanning the frame. */",
|
|
42217
|
-
" /* A LIGHT panel behind the mark
|
|
42218
|
-
" \u2014
|
|
42219
|
-
" and the brand-coloured closing plate erased it completely: the
|
|
42220
|
-
" out as an empty green frame with a single leaf floating in it. A
|
|
42221
|
-
" 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. */",
|
|
42222
42280
|
" .brand-mark { position: absolute; top: 90px; left: 56px; height: 52px; width: auto;",
|
|
42223
42281
|
" max-width: 420px; object-fit: contain; padding: 14px 22px; border-radius: 18px;",
|
|
42224
42282
|
" background: rgba(255,255,255,0.94);",
|
|
@@ -42232,6 +42290,14 @@ function adBrandOverlayCss() {
|
|
|
42232
42290
|
" .endcard-logo { width: 62%; height: auto; max-height: 300px; object-fit: contain;",
|
|
42233
42291
|
" padding: 46px 56px; border-radius: 40px; background: rgba(255,255,255,0.96);",
|
|
42234
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); }",
|
|
42235
42301
|
" /* The name, when the mark itself could not be drawn. */",
|
|
42236
42302
|
" .brand-wordmark { display: inline-block; font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
|
|
42237
42303
|
" font-weight: 900; color: #14231b; letter-spacing: -0.01em; }",
|
|
@@ -42246,10 +42312,17 @@ function adBrandOverlayCss() {
|
|
|
42246
42312
|
" first attempt led with 'BrandFont', which the overlay composition defines and",
|
|
42247
42313
|
" the caption composition does not: HyperFrames refuses an unmapped family and",
|
|
42248
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. */",
|
|
42249
42321
|
" .endcard-cta { font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
|
|
42250
42322
|
" font-size: 64px; font-weight: 900; text-transform: uppercase;",
|
|
42251
42323
|
" letter-spacing: 0.01em; color: #fff; padding: 28px 58px; border-radius: 56px;",
|
|
42252
|
-
" 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; }"
|
|
42253
42326
|
].join("\n");
|
|
42254
42327
|
}
|
|
42255
42328
|
function injectAdBrandOverlay(indexHtml, overlayHtml) {
|
|
@@ -42266,6 +42339,7 @@ function injectAdBrandOverlay(indexHtml, overlayHtml) {
|
|
|
42266
42339
|
// src/engine/scaffold/ad-brand-source.ts
|
|
42267
42340
|
import { readdir as readdir8, readFile as readFile15 } from "fs/promises";
|
|
42268
42341
|
import path22 from "path";
|
|
42342
|
+
import sharp from "sharp";
|
|
42269
42343
|
var BRAND_DIR = "src/brand";
|
|
42270
42344
|
function paletteFromBrandDoc(markdown) {
|
|
42271
42345
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -42274,15 +42348,33 @@ function paletteFromBrandDoc(markdown) {
|
|
|
42274
42348
|
const hex = `#${match[1].toLowerCase()}`;
|
|
42275
42349
|
if (seen.has(hex)) continue;
|
|
42276
42350
|
seen.add(hex);
|
|
42277
|
-
const
|
|
42278
|
-
|
|
42279
|
-
const b = Number.parseInt(hex.slice(5, 7), 16);
|
|
42280
|
-
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
42281
|
-
if (luma > 236 || luma < 14) continue;
|
|
42351
|
+
const luma = hexLuma(hex);
|
|
42352
|
+
if (luma === null || luma > 236 || luma < 14) continue;
|
|
42282
42353
|
out.push(hex);
|
|
42283
42354
|
}
|
|
42284
42355
|
return out;
|
|
42285
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/;
|
|
42286
42378
|
function pickBrandMark(files) {
|
|
42287
42379
|
const usable = files.filter((f) => /\.(svg|png|webp)$/i.test(f));
|
|
42288
42380
|
if (usable.length === 0) return null;
|
|
@@ -42291,10 +42383,10 @@ function pickBrandMark(files) {
|
|
|
42291
42383
|
let s = 0;
|
|
42292
42384
|
if (/\.(png|webp)$/.test(name)) s += 1;
|
|
42293
42385
|
if (/\b(logo|wordmark|lockup)\b/.test(name)) s += 2;
|
|
42294
|
-
if (
|
|
42386
|
+
if (MARK_VARIANT_NAMES.test(name)) s -= 3;
|
|
42295
42387
|
return s;
|
|
42296
42388
|
};
|
|
42297
|
-
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;
|
|
42298
42390
|
}
|
|
42299
42391
|
async function readBrandFromWorkspace(root = ".") {
|
|
42300
42392
|
const found = {};
|
|
@@ -42349,6 +42441,27 @@ async function markDirectoryIsReal(dir) {
|
|
|
42349
42441
|
}
|
|
42350
42442
|
return true;
|
|
42351
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
|
+
}
|
|
42352
42465
|
|
|
42353
42466
|
// src/commands/canvas/composition-path.ts
|
|
42354
42467
|
import { existsSync as existsSync5 } from "fs";
|
|
@@ -42412,6 +42525,11 @@ registerSchema({
|
|
|
42412
42525
|
}
|
|
42413
42526
|
}
|
|
42414
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
|
+
}
|
|
42415
42533
|
var scaffoldAdCommand = defineCommand114({
|
|
42416
42534
|
meta: {
|
|
42417
42535
|
name: "scaffold-ad",
|
|
@@ -42654,7 +42772,12 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42654
42772
|
spec.data.cast = { ...spec.data.cast, description: withoutCastName(spec.data.cast.description, names) };
|
|
42655
42773
|
}
|
|
42656
42774
|
}
|
|
42657
|
-
const
|
|
42775
|
+
const avatarVoice = avatarVoiceBrief(avatar);
|
|
42776
|
+
const blueprint = adSpecToBlueprint(spec.data, {
|
|
42777
|
+
avatarAccent: avatar?.profile?.accent,
|
|
42778
|
+
videoModel: chosenVideoModel,
|
|
42779
|
+
...avatarVoice ? { avatarVoice } : {}
|
|
42780
|
+
});
|
|
42658
42781
|
const slug = args4.slug ?? path24.basename(specPath).replace(/\.[^.]+$/, "");
|
|
42659
42782
|
const outPath = args4.out ?? (args4.slug ? path24.join("src/creatives", slug, `${slug}.canvas.json`) : path24.join(path24.dirname(specPath), `${slug}.canvas.json`));
|
|
42660
42783
|
const outDir = path24.dirname(outPath);
|
|
@@ -42704,6 +42827,19 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42704
42827
|
// and rejected — it would swap the voice that matches the mouth and the directed
|
|
42705
42828
|
// region for one cast off an English stock list. Only the on-camera voice; a
|
|
42706
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.
|
|
42707
42843
|
...avatar ? { keepRenderedVoice: true } : {},
|
|
42708
42844
|
...spec.data.voiceover === false ? { staticTranscriptPath: path24.relative(outDir, transcriptPath) } : {}
|
|
42709
42845
|
};
|
|
@@ -42711,11 +42847,13 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42711
42847
|
const renderNode4 = canvas.nodes.find((n) => n.type === "hyperframe_render");
|
|
42712
42848
|
const renderedDir = renderNode4 ? path24.join(outDir, String(renderNode4.params?.composition ?? "")) : null;
|
|
42713
42849
|
let staged = null;
|
|
42850
|
+
let markIsLight = false;
|
|
42714
42851
|
if (logoPath && renderedDir) {
|
|
42715
42852
|
const ext = path24.extname(logoPath) || ".png";
|
|
42716
42853
|
try {
|
|
42717
42854
|
await copyFile(logoPath, path24.join(renderedDir, `brand-mark${ext}`));
|
|
42718
42855
|
staged = { file: `brand-mark${ext}`, alt: spec.data.brand?.name ?? "brand" };
|
|
42856
|
+
markIsLight = await markInkIsLight(logoPath);
|
|
42719
42857
|
} catch {
|
|
42720
42858
|
}
|
|
42721
42859
|
}
|
|
@@ -42726,7 +42864,10 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42726
42864
|
cardAtS: closing.start_s,
|
|
42727
42865
|
cardDurationS: closing.duration_s,
|
|
42728
42866
|
totalS: blueprint.estimated_duration_s,
|
|
42729
|
-
|
|
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)
|
|
42730
42871
|
}) : "";
|
|
42731
42872
|
if (overlayHtml && renderedDir) {
|
|
42732
42873
|
const indexPath = path24.join(renderedDir, "index.html");
|
|
@@ -42801,7 +42942,7 @@ var scaffoldAdCommand = defineCommand114({
|
|
|
42801
42942
|
// ad with no palette and no mark anywhere in 28 seconds.
|
|
42802
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.`
|
|
42803
42944
|
],
|
|
42804
|
-
"`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."
|
|
42805
42946
|
]
|
|
42806
42947
|
});
|
|
42807
42948
|
}
|
|
@@ -51301,9 +51442,9 @@ import { defineCommand as defineCommand169 } from "citty";
|
|
|
51301
51442
|
import { defineCommand as defineCommand143 } from "citty";
|
|
51302
51443
|
|
|
51303
51444
|
// src/lib/image/crop-sprite.ts
|
|
51304
|
-
import
|
|
51445
|
+
import sharp2 from "sharp";
|
|
51305
51446
|
function cropSprite(input, region) {
|
|
51306
|
-
return
|
|
51447
|
+
return sharp2(input).extract({
|
|
51307
51448
|
left: Math.round(region.x),
|
|
51308
51449
|
top: Math.round(region.y),
|
|
51309
51450
|
width: Math.round(region.width),
|
|
@@ -53563,7 +53704,7 @@ function solidifyEdges(data, saturationBoost = 0.25) {
|
|
|
53563
53704
|
}
|
|
53564
53705
|
|
|
53565
53706
|
// src/lib/image/image-processor.ts
|
|
53566
|
-
import
|
|
53707
|
+
import sharp3 from "sharp";
|
|
53567
53708
|
function hasGradient(data) {
|
|
53568
53709
|
const uniqueColors = /* @__PURE__ */ new Set();
|
|
53569
53710
|
for (let i = 0; i < data.length; i += 4) {
|
|
@@ -53595,10 +53736,10 @@ function createSvgRecolorer(target, palette) {
|
|
|
53595
53736
|
async function processGradient(data, info) {
|
|
53596
53737
|
if (!hasGradient(data)) return { data, info };
|
|
53597
53738
|
const solidified = solidifyEdges(data, 0.1);
|
|
53598
|
-
const blurred = await
|
|
53739
|
+
const blurred = await sharp3(solidified, {
|
|
53599
53740
|
raw: { channels: 4, width: info.width, height: info.height }
|
|
53600
53741
|
}).png().blur(0.3).toBuffer();
|
|
53601
|
-
const result = await
|
|
53742
|
+
const result = await sharp3(blurred).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
53602
53743
|
return { data: result.data, info: result.info };
|
|
53603
53744
|
}
|
|
53604
53745
|
function isSvgBuffer(buffer) {
|
|
@@ -53607,13 +53748,13 @@ function isSvgBuffer(buffer) {
|
|
|
53607
53748
|
}
|
|
53608
53749
|
async function processInternal(inputBuffer, isSVG, options) {
|
|
53609
53750
|
const stages = [];
|
|
53610
|
-
const metadata = await
|
|
53751
|
+
const metadata = await sharp3(inputBuffer).metadata();
|
|
53611
53752
|
let alreadyTransparent = false;
|
|
53612
53753
|
if (metadata.hasAlpha) {
|
|
53613
|
-
const { data: alphaData } = await
|
|
53754
|
+
const { data: alphaData } = await sharp3(inputBuffer).raw().toBuffer({ resolveWithObject: true });
|
|
53614
53755
|
alreadyTransparent = hasTransparency(alphaData, 0.05);
|
|
53615
53756
|
}
|
|
53616
|
-
let { data: processedData, info } = await
|
|
53757
|
+
let { data: processedData, info } = await sharp3(inputBuffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
53617
53758
|
if (options.color) {
|
|
53618
53759
|
stages.push("recolor");
|
|
53619
53760
|
processedData = applyQuantization(processedData, 8);
|
|
@@ -53684,7 +53825,7 @@ async function processInternal(inputBuffer, isSVG, options) {
|
|
|
53684
53825
|
processedData = result.data;
|
|
53685
53826
|
info = result.info;
|
|
53686
53827
|
}
|
|
53687
|
-
let pipeline2 =
|
|
53828
|
+
let pipeline2 = sharp3(processedData, {
|
|
53688
53829
|
raw: { channels: 4, width: info.width, height: info.height }
|
|
53689
53830
|
}).png();
|
|
53690
53831
|
if (options.shrinkToContent) {
|
|
@@ -53697,12 +53838,12 @@ async function processInternal(inputBuffer, isSVG, options) {
|
|
|
53697
53838
|
function applyResize(buffer, resize) {
|
|
53698
53839
|
const transparent = { r: 0, g: 0, b: 0, alpha: 0 };
|
|
53699
53840
|
if (resize.size) {
|
|
53700
|
-
return
|
|
53841
|
+
return sharp3(buffer).resize(resize.size.width, resize.size.height, {
|
|
53701
53842
|
fit: resize.fit ?? "contain",
|
|
53702
53843
|
background: transparent
|
|
53703
53844
|
}).toBuffer();
|
|
53704
53845
|
}
|
|
53705
|
-
return
|
|
53846
|
+
return sharp3(buffer).resize(resize.width ?? null, resize.height ?? null, { fit: "inside", withoutEnlargement: false }).toBuffer();
|
|
53706
53847
|
}
|
|
53707
53848
|
async function processImage(inputBuffer, options = {}) {
|
|
53708
53849
|
const stages = [];
|
|
@@ -53711,7 +53852,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
53711
53852
|
if (options.resize) {
|
|
53712
53853
|
stages.push("resize");
|
|
53713
53854
|
const resized = await applyResize(inputBuffer, options.resize);
|
|
53714
|
-
const meta3 = await
|
|
53855
|
+
const meta3 = await sharp3(resized).metadata();
|
|
53715
53856
|
return {
|
|
53716
53857
|
buffer: resized,
|
|
53717
53858
|
format: "png",
|
|
@@ -53720,7 +53861,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
53720
53861
|
stages
|
|
53721
53862
|
};
|
|
53722
53863
|
}
|
|
53723
|
-
const meta2 = await
|
|
53864
|
+
const meta2 = await sharp3(inputBuffer).metadata();
|
|
53724
53865
|
return {
|
|
53725
53866
|
buffer: inputBuffer,
|
|
53726
53867
|
format: "svg",
|
|
@@ -53732,7 +53873,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
53732
53873
|
let workingBuffer = inputBuffer;
|
|
53733
53874
|
if (isSVG) {
|
|
53734
53875
|
stages.push("rasterize-svg");
|
|
53735
|
-
workingBuffer = await
|
|
53876
|
+
workingBuffer = await sharp3(inputBuffer).png({ compressionLevel: 0, force: true, palette: false, quality: 100 }).toBuffer();
|
|
53736
53877
|
}
|
|
53737
53878
|
const pass = await processInternal(workingBuffer, isSVG, options);
|
|
53738
53879
|
stages.push(...pass.stages);
|
|
@@ -53741,7 +53882,7 @@ async function processImage(inputBuffer, options = {}) {
|
|
|
53741
53882
|
stages.push("resize");
|
|
53742
53883
|
processed = await applyResize(processed, options.resize);
|
|
53743
53884
|
}
|
|
53744
|
-
const meta = await
|
|
53885
|
+
const meta = await sharp3(processed).metadata();
|
|
53745
53886
|
return {
|
|
53746
53887
|
buffer: processed,
|
|
53747
53888
|
format: "png",
|
|
@@ -57360,7 +57501,7 @@ function routeKey(route) {
|
|
|
57360
57501
|
}
|
|
57361
57502
|
|
|
57362
57503
|
// src/engine/landing-library/fidelity.ts
|
|
57363
|
-
import
|
|
57504
|
+
import sharp4 from "sharp";
|
|
57364
57505
|
var COMPARISON_SIZE = 32;
|
|
57365
57506
|
function pixelSimilarity(a, b) {
|
|
57366
57507
|
const length = Math.min(a.length, b.length);
|
|
@@ -57372,7 +57513,7 @@ function pixelSimilarity(a, b) {
|
|
|
57372
57513
|
return 1 - total / (length * 255);
|
|
57373
57514
|
}
|
|
57374
57515
|
async function scoreFidelity(live, rendered) {
|
|
57375
|
-
const [liveMeta, renderedMeta] = await Promise.all([
|
|
57516
|
+
const [liveMeta, renderedMeta] = await Promise.all([sharp4(live).metadata(), sharp4(rendered).metadata()]);
|
|
57376
57517
|
const liveSize = { width: liveMeta.width ?? 0, height: liveMeta.height ?? 0 };
|
|
57377
57518
|
const renderedSize = { width: renderedMeta.width ?? 0, height: renderedMeta.height ?? 0 };
|
|
57378
57519
|
const [livePixels, renderedPixels] = await Promise.all([toGreyGrid(live), toGreyGrid(rendered)]);
|
|
@@ -57386,7 +57527,7 @@ async function scoreFidelity(live, rendered) {
|
|
|
57386
57527
|
};
|
|
57387
57528
|
}
|
|
57388
57529
|
async function toGreyGrid(image) {
|
|
57389
|
-
const raw = await
|
|
57530
|
+
const raw = await sharp4(image).greyscale().resize(COMPARISON_SIZE, COMPARISON_SIZE, { fit: "fill" }).raw().toBuffer();
|
|
57390
57531
|
return new Uint8Array(raw);
|
|
57391
57532
|
}
|
|
57392
57533
|
|
|
@@ -57598,7 +57739,7 @@ var inPageCollectMotion = (selector) => {
|
|
|
57598
57739
|
};
|
|
57599
57740
|
|
|
57600
57741
|
// src/engine/landing-library/motionTake.ts
|
|
57601
|
-
import
|
|
57742
|
+
import sharp5 from "sharp";
|
|
57602
57743
|
|
|
57603
57744
|
// src/engine/landing-library/certificate.ts
|
|
57604
57745
|
function formatDate(unixSeconds) {
|
|
@@ -58265,8 +58406,8 @@ async function hideFloatingFurniture(page, selector) {
|
|
|
58265
58406
|
}, selector);
|
|
58266
58407
|
}
|
|
58267
58408
|
async function composeFilmstrip(frames) {
|
|
58268
|
-
const scaled = await Promise.all(frames.map((frame) =>
|
|
58269
|
-
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();
|
|
58270
58411
|
const frameHeight = first.height ?? 300;
|
|
58271
58412
|
const cellHeight = frameHeight + LABEL_HEIGHT;
|
|
58272
58413
|
const rows = Math.ceil(scaled.length / GRID_COLUMNS);
|
|
@@ -58285,7 +58426,7 @@ async function composeFilmstrip(frames) {
|
|
|
58285
58426
|
{ input: frame, left: column * FRAME_WIDTH, top: row * cellHeight + LABEL_HEIGHT }
|
|
58286
58427
|
];
|
|
58287
58428
|
});
|
|
58288
|
-
return await
|
|
58429
|
+
return await sharp5({
|
|
58289
58430
|
create: {
|
|
58290
58431
|
width,
|
|
58291
58432
|
height: cellHeight * rows,
|
|
@@ -58474,12 +58615,12 @@ var DEFAULT_SEGMENT_OPTIONS = {
|
|
|
58474
58615
|
};
|
|
58475
58616
|
|
|
58476
58617
|
// src/engine/landing-library/visualHash.ts
|
|
58477
|
-
import
|
|
58618
|
+
import sharp6 from "sharp";
|
|
58478
58619
|
var HASH_WIDTH = 9;
|
|
58479
58620
|
var HASH_HEIGHT = 8;
|
|
58480
58621
|
async function perceptualHash(image) {
|
|
58481
58622
|
try {
|
|
58482
|
-
const raw = await
|
|
58623
|
+
const raw = await sharp6(image).greyscale().resize(HASH_WIDTH, HASH_HEIGHT, { fit: "fill" }).raw().toBuffer();
|
|
58483
58624
|
return bitsToHex(rowGradientBits(new Uint8Array(raw)));
|
|
58484
58625
|
} catch {
|
|
58485
58626
|
return null;
|
|
@@ -58993,7 +59134,7 @@ import { defineCommand as defineCommand176 } from "citty";
|
|
|
58993
59134
|
// src/commands/landing/inspiration/shot.ts
|
|
58994
59135
|
import { mkdir as mkdir13, writeFile as writeFile18 } from "fs/promises";
|
|
58995
59136
|
import path42 from "path";
|
|
58996
|
-
import
|
|
59137
|
+
import sharp7 from "sharp";
|
|
58997
59138
|
var READABLE_SHOT = {
|
|
58998
59139
|
maxWidth: 1440,
|
|
58999
59140
|
maxHeight: 4e3,
|
|
@@ -59002,7 +59143,7 @@ var READABLE_SHOT = {
|
|
|
59002
59143
|
};
|
|
59003
59144
|
async function toReadableShot(original) {
|
|
59004
59145
|
try {
|
|
59005
|
-
return await
|
|
59146
|
+
return await sharp7(original).resize({
|
|
59006
59147
|
width: READABLE_SHOT.maxWidth,
|
|
59007
59148
|
height: READABLE_SHOT.maxHeight,
|
|
59008
59149
|
fit: "inside",
|