@koda-sl/baker-cli 0.389.0 → 0.391.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 +33 -13
- package/dist/{chunk-IOWWKZRF.js → chunk-SZKTVOR7.js} +205 -9
- package/dist/chunk-SZKTVOR7.js.map +1 -0
- package/dist/cli.js +4178 -3822
- package/dist/cli.js.map +1 -1
- package/dist/engine/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IOWWKZRF.js.map +0 -1
package/README.md
CHANGED
|
@@ -4067,7 +4067,7 @@ baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
|
|
|
4067
4067
|
`baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
|
|
4068
4068
|
because a video model garbles a wordmark every time:
|
|
4069
4069
|
|
|
4070
|
-
- **`brand.logo`** — the repo path to the mark. It is copied into the
|
|
4070
|
+
- **`brand.logo`** — the repo path to the mark. It is copied into the caption composition
|
|
4071
4071
|
and drawn as a real `<img>`: small top-left for the whole ad, large on the closing card.
|
|
4072
4072
|
- **`end_card`** — on by default whenever the spec carries a brand. The last beat's PICTURE
|
|
4073
4073
|
becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
|
|
@@ -4087,9 +4087,24 @@ because a video model garbles a wordmark every time:
|
|
|
4087
4087
|
alongside the music bed. Most beats should not have one: it belongs where the picture
|
|
4088
4088
|
shows a specific physical event (a drill, a door, rain on glass), not on a talking head
|
|
4089
4089
|
or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
|
|
4090
|
-
- **
|
|
4091
|
-
|
|
4092
|
-
|
|
4090
|
+
- **The logo is optional.** With no mark in `src/brand/logos/` (and none confirmed for the
|
|
4091
|
+
company's own domain), the ad is built without the corner mark, and one hint says so and
|
|
4092
|
+
how to add one: put the client's logo file in `src/brand/logos/` and re-run. Meta shows
|
|
4093
|
+
the page name and picture above every ad either way. The closing card still carries the
|
|
4094
|
+
call to action when the spec has a `cta`; with neither a mark nor a `cta`, the last beat
|
|
4095
|
+
keeps its footage. A typed stand-in (an SVG wordmark in live `<text>`) is still refused,
|
|
4096
|
+
and `--no-brand` still means no brand layer at all: no mark and no closing card.
|
|
4097
|
+
- **Captions and the mark sit inside the safe area.** On 9:16 the caption cards end above
|
|
4098
|
+
the bottom 37% of the frame (Reels covers the bottom 35%) and the mark starts below the
|
|
4099
|
+
top 16% (the Stories and Reels header); 4:5 and 1:1 use modest feed margins. The closing
|
|
4100
|
+
card is centred in the same area. Every composition is sized to the spec's
|
|
4101
|
+
`format.aspect_ratio`, so a 4:5 ad lays its captions out on a 4:5 frame.
|
|
4102
|
+
- **Captions and the closing button are set in the brand's face**: a font file in
|
|
4103
|
+
`src/brand/fonts/`, else the typeface `tokens.json` or `BRAND.md` names, loaded as a web
|
|
4104
|
+
font. With neither, they keep the default heavy sans.
|
|
4105
|
+
- **A spoken ad's captions print the script.** The voice track is transcribed only to time
|
|
4106
|
+
the words: each caption word is the `say` line as written, at the moment it was said, so
|
|
4107
|
+
a word the recogniser mishears never reaches the screen.
|
|
4093
4108
|
- **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
|
|
4094
4109
|
a separately cast voice, which is dubbing and looks like it.
|
|
4095
4110
|
- **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
|
|
@@ -5075,6 +5090,7 @@ Word-level transcription of a video OR a bare audio track (the `video` input acc
|
|
|
5075
5090
|
| `language` | string | no | auto | ISO 639-1, 2–8 chars (e.g. `en`, `es`) |
|
|
5076
5091
|
| `transcriber` | enum | no | `groq` | `groq` (Whisper) \| `deepgram` (Nova-3, adds the `rich` output) |
|
|
5077
5092
|
| `keyterms` | string[] | no | — | Up to 30 names the recogniser must not mangle — a product, a person, a phrase the script coins. The **company's own name is always applied server-side**, so it never needs listing here |
|
|
5093
|
+
| `script` | string | no | — | The words the track was asked to say. Given, the `transcript` output is the script's words, spelled as written, at the times the recogniser heard them: aligned locally (misheard words, fillers, accents and numbers said as words are all tolerated), never sent to the recogniser. `scaffold-ad` sets it on every spoken ad |
|
|
5078
5094
|
|
|
5079
5095
|
**Outputs**
|
|
5080
5096
|
|
|
@@ -5632,7 +5648,7 @@ It then scaffolds a canvas that draws **only what `prompt.json` holds**; no elem
|
|
|
5632
5648
|
|
|
5633
5649
|
**Placement formats.** The canvas delivers the platform's full placement set, each format at its exact size, collected under `final` with the format as its label. The set is derived from `--platform` (`meta` → 4:5 + 9:16, `google` → 1:1 + 4:5 + 16:9, `linkedin` → 1:1, `tiktok` → 9:16, `youtube` → 16:9); override it with `--placements <preset>` or `--placements none` for a single format. The report's `aspect_ratio` is the ratio the plates really render at (the model's nearest to the lead format), and `format_plan` says how each format is made.
|
|
5634
5650
|
|
|
5635
|
-
**After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format, and one vision pass that reads each word against the copy `prompt.json` holds and checks one dominant idea, clipping, artifacts, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-plate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed.
|
|
5651
|
+
**After a run** the CLI reads the finished ad back on what makes a static win, not a checklist of elements: every delivered image's pixel shape against its format, and one vision pass that reads each word against the copy `prompt.json` holds and checks one dominant idea, clipping, artifacts, the brand with the logo covered and the concept's mechanisms, with every format shown a second time at phone-feed thumbnail size (240px wide, same call): a lead message unreadable at that size is blocking. A missing logo, CTA or copy is never a finding; a logo that is drawn and broken is. stdout carries `verdict` (`ready` | `ready_with_notes` | `needs_fixes` | `not_reviewed`), `judge` (the per-plate scores) and hints that end with what the run made; the findings are stored on the run as its `review`. `needs_fixes` means the ad is not done even though every node completed; its hints name the cheapest fix (overlapping or unreadable copy is fixed with a smaller `size`, a `highlight` or `sticker` treatment or shorter words, which re-draw the formats from the cached plates). **Render budget:** a static ad gets 3 renders per creative in a chat before the person reacts; the fourth exits 2 with `RENDER_BUDGET` without rendering, and `baker creatives feedback record` or `baker creatives select` on that creative opens a new round. Resuming an interrupted run does not count again, and a run that died only on an outage (timeout, network, a provider that says retry) gives its render back.
|
|
5636
5652
|
|
|
5637
5653
|
The logo is never a slot to fill: use the company's committed logo in `src/brand/logos/` (per `BRAND.md`), never `baker images logo <domain>`, whose Brandfetch CDN often serves an outdated mark.
|
|
5638
5654
|
|
|
@@ -6158,27 +6174,31 @@ import {
|
|
|
6158
6174
|
|
|
6159
6175
|
## Creatives
|
|
6160
6176
|
|
|
6161
|
-
Publish an approved canvas render as a first-class Baker creative.
|
|
6177
|
+
Publish an approved canvas render as a first-class Baker creative: a static image (PNG, JPG, WebP) or a finished video (MP4, MOV, WebM). An image uploads to the Baker image library (tagged `creative`); a video uploads to the Baker video library and is saved once it plays (bounded at 3 minutes). A creative record is created/updated, and the command prints the creative reference JSON the dashboard renders in chat.
|
|
6162
6178
|
|
|
6163
6179
|
```bash
|
|
6164
6180
|
baker creatives publish ./canvas/<run_id>/<final>.png --title "Spring Offer 4x5" \
|
|
6165
6181
|
--slug spring-offer-4x5 --run-id r_01JXYZ... \
|
|
6166
6182
|
--source-reference-url "https://www.facebook.com/ads/library/?id=..."
|
|
6183
|
+
baker creatives publish ./canvas/<run_id>/final.mp4 --title "Spring Offer Reel" \
|
|
6184
|
+
--slug spring-offer-reel --run-id r_01JXYZ...
|
|
6167
6185
|
```
|
|
6168
6186
|
|
|
6169
6187
|
| Flag | Effect |
|
|
6170
6188
|
|---|---|
|
|
6171
6189
|
| `--title <text>` | Required. Human title for the creative. |
|
|
6172
|
-
| `--slug <slug>` | Creative slug (`src/creatives/<slug>/`)
|
|
6190
|
+
| `--slug <slug>` | Creative slug (`src/creatives/<slug>/`): attaches the image or video to that creative's row, marks it `published`. Required for a video. |
|
|
6173
6191
|
| `--run-id <r_…>` | Pins the approved generation from the creative's run history as the published one. |
|
|
6174
6192
|
| `--source-reference-url <url>` | Original reference ad URL, recorded on the creative. |
|
|
6175
|
-
| `--context <text>` | Optional describe context for the uploaded image
|
|
6193
|
+
| `--context <text>` | Optional describe context for the uploaded image or video. |
|
|
6176
6194
|
|
|
6177
|
-
|
|
6195
|
+
An image prints `{ ok, data: { creativeId, imageId, reference } }`. A video prints `{ ok, data: { creativeId, videoId, reference }, hints }`, where `reference.asset.kind` is `video` (with `videoId`, `muxPlaybackId`, the poster as `thumbnailUrl`, size and duration) and `hints` names the next step to put it in a Meta ad: `baker ads meta media upload --kind video --video-id <videoId>`.
|
|
6196
|
+
|
|
6197
|
+
Without `--slug` an image behaves as before (one creative record per published image). With `--slug` it upserts the repo-convention row — the same one the dashboard's Creatives tab and the `src/creatives/{slug}/` folder describe — so publish, repo sync, and run history all land on a single record regardless of order.
|
|
6178
6198
|
|
|
6179
6199
|
### `baker creatives performance`
|
|
6180
6200
|
|
|
6181
|
-
How each published creative is doing in the live Meta ads that show it. A creative is linked to an ad when a chat's publish uploads its image to Meta and creates an ad with it (or swaps an existing ad onto it); the results are read live from Meta.
|
|
6201
|
+
How each published creative is doing in the live Meta ads that show it. A creative is linked to an ad when a chat's publish uploads its image or video to Meta and creates an ad with it (or swaps an existing ad onto it); the results are read live from Meta.
|
|
6182
6202
|
|
|
6183
6203
|
```bash
|
|
6184
6204
|
baker creatives performance # every creative in an ad, last 30 days
|
|
@@ -6190,9 +6210,9 @@ baker creatives performance --days 14 --full # + ids, last-7-day figures, on
|
|
|
6190
6210
|
|---|---|
|
|
6191
6211
|
| `--slug <slug>` | Only this creative. |
|
|
6192
6212
|
| `--days <n>` | Window ending today, 1-90 (default 30). |
|
|
6193
|
-
| `--full` | Adds creative and
|
|
6213
|
+
| `--full` | Adds creative, image and video ids, result type, days live, last-7-day CTR and cost per result, a video's watch funnel (`video`: 3-second views, ThruPlays, 25/50/75/100% watched), and one row per ad. |
|
|
6194
6214
|
|
|
6195
|
-
Each creative reports `spend`, `impressions`, `ctr` (percent), `cpc`, `results`, `costPerResult`, `frequency`, a `fatigue` verdict (`fresh`, `fading`, `fatigued`, `dead`, `too_early`, `no_delivery`) with its reason, and `definition`: the angle, hook, mechanism, dominantSlot, persona, awareness, hypothesis and reference fields read from the local `src/creatives/<slug>/_definition.md`. `hints` name worn-out creatives, the angle, mechanism and dominant slot with the lowest cost per result (each ranked once two or more values have results), and unreadable ad accounts.
|
|
6215
|
+
Each creative reports `spend`, `impressions`, `ctr` (percent), `cpc`, `results`, `costPerResult`, `frequency`, a `fatigue` verdict (`fresh`, `fading`, `fatigued`, `dead`, `too_early`, `no_delivery`) with its reason, and `definition`: the angle, hook, mechanism, dominantSlot, persona, awareness, hypothesis and reference fields read from the local `src/creatives/<slug>/_definition.md`. A video creative also reports `hookRate` (percent of impressions that watched 3 seconds) and `holdRate` (percent of those that reached a ThruPlay); a static one carries neither. `hints` name worn-out creatives, videos whose hook rate is under 25% (the first 3 seconds need new hooks) or whose hold rate is under 30% behind a working hook (the body loses people), the angle, mechanism and dominant slot with the lowest cost per result (each ranked once two or more values have results), and unreadable ad accounts.
|
|
6196
6216
|
|
|
6197
6217
|
### `baker creatives feedback [--slug <slug>] [--full]`
|
|
6198
6218
|
|
|
@@ -6221,7 +6241,7 @@ baker creatives feedback record --slug spring-offer --run r_01K… --option 2 --
|
|
|
6221
6241
|
| `--run <r_…>` | Required. The version. |
|
|
6222
6242
|
| `--verdict approved\|rejected` | Required. |
|
|
6223
6243
|
| `--option <o>` | One option of a version that made several: its label, its number (`2`) or its slot. |
|
|
6224
|
-
| `--reasons <a,b>` | Comma-separated: `text_mistakes`, `logo_or_brand_wrong`, `weak_hook`, `wrong_message`, `looks_fake`, `not_our_style`, `other`. A rejection needs reasons or a note. |
|
|
6244
|
+
| `--reasons <a,b>` | Comma-separated: `text_mistakes`, `logo_or_brand_wrong`, `weak_hook`, `wrong_message`, `looks_fake`, `not_our_style`, `voice_or_sound`, `pacing`, `other`. The two video reasons: `voice_or_sound` (the voiceover, music or mix) and `pacing` (too slow, too fast, the hook lands late). A rejection needs reasons or a note. |
|
|
6225
6245
|
| `--note <text>` | The person's own words (stored up to 1000 characters). |
|
|
6226
6246
|
|
|
6227
6247
|
After a rejection, `hints[]` restates what to change, starting from that version.
|
|
@@ -1023,6 +1023,10 @@ function runFailureCode(e) {
|
|
|
1023
1023
|
const reasons = e instanceof LayerExecutionError ? e.failures.map((f) => f.reason) : [e];
|
|
1024
1024
|
return reasons.length > 0 && reasons.every(isInputRejection) ? "input_invalid" : "runtime";
|
|
1025
1025
|
}
|
|
1026
|
+
function failedOnlyOnTransientErrors(e) {
|
|
1027
|
+
const reasons = e instanceof LayerExecutionError ? e.failures.map((f) => f.reason) : [e];
|
|
1028
|
+
return reasons.length > 0 && reasons.every((r) => r instanceof NodeExecutionError && failureDetail(r).retryable === true);
|
|
1029
|
+
}
|
|
1026
1030
|
function failureDetail(reason) {
|
|
1027
1031
|
if (reason instanceof NodeExecutionError) {
|
|
1028
1032
|
const c = reason.cause;
|
|
@@ -3657,13 +3661,13 @@ function spineInputFlags(work, lenS) {
|
|
|
3657
3661
|
function spineInputOps(work, lenS, dims) {
|
|
3658
3662
|
const fit = work.still?.fit ?? "fill";
|
|
3659
3663
|
const scale = fit === "pad" ? `scale=${dims.w}:${dims.h}:force_original_aspect_ratio=decrease,pad=${dims.w}:${dims.h}:(ow-iw)/2:(oh-ih)/2:color=black` : `scale=${dims.w}:${dims.h}:force_original_aspect_ratio=increase,crop=${dims.w}:${dims.h}`;
|
|
3660
|
-
const
|
|
3664
|
+
const normalize2 = `${scale},format=yuv420p,fps=30,setsar=1,settb=AVTB`;
|
|
3661
3665
|
if (work.trim) {
|
|
3662
3666
|
const start = work.trim.offset_s > 0 ? `start=${work.trim.offset_s.toFixed(3)}:` : "";
|
|
3663
|
-
return `${
|
|
3667
|
+
return `${normalize2},trim=${start}duration=${lenS.toFixed(3)},setpts=PTS-STARTPTS`;
|
|
3664
3668
|
}
|
|
3665
|
-
if (work.still) return `${
|
|
3666
|
-
return
|
|
3669
|
+
if (work.still) return `${normalize2},trim=duration=${lenS.toFixed(3)}`;
|
|
3670
|
+
return normalize2;
|
|
3667
3671
|
}
|
|
3668
3672
|
function parseSpineInputLens(args) {
|
|
3669
3673
|
const lens = /* @__PURE__ */ new Map();
|
|
@@ -7382,10 +7386,10 @@ function applyDuck(params, labelFor, filterChains) {
|
|
|
7382
7386
|
}
|
|
7383
7387
|
const th = duck.threshold ?? 0.03;
|
|
7384
7388
|
const ra = duck.ratio ?? 8;
|
|
7385
|
-
const
|
|
7389
|
+
const at2 = duck.attack ?? 5;
|
|
7386
7390
|
const re = duck.release ?? 300;
|
|
7387
7391
|
filterChains.push(
|
|
7388
|
-
`[${labelFor[trackIdx]}]${keyOut}sidechaincompress=threshold=${th}:ratio=${ra}:attack=${
|
|
7392
|
+
`[${labelFor[trackIdx]}]${keyOut}sidechaincompress=threshold=${th}:ratio=${ra}:attack=${at2}:release=${re}[ducked]`
|
|
7389
7393
|
);
|
|
7390
7394
|
labelFor[trackIdx] = "ducked";
|
|
7391
7395
|
}
|
|
@@ -8069,6 +8073,9 @@ var FINDING_ADVICE = {
|
|
|
8069
8073
|
console_error: "the composition threw in the browser, so whatever that script was driving did not happen.",
|
|
8070
8074
|
page_error: "a script failed to evaluate \u2014 animations and any JS-driven layout in this composition did not run.",
|
|
8071
8075
|
http_error: "an asset 404ed at render time; it will be missing from the frame, not substituted.",
|
|
8076
|
+
// The most common reason a static ad failed its render in production (Sept 2026), and the
|
|
8077
|
+
// agent answered it by re-painting all three pictures when only the composition was wrong.
|
|
8078
|
+
content_overlap: "two lines of copy collide. In a static ad, make one smaller (`size` in its layout region's `text` in prompt.json) or shorten it: the formats re-draw from the cached pictures. Moving a layout region instead re-paints every picture.",
|
|
8072
8079
|
console_warning: "if this names a GSAP target, the element it animates does not exist, so that tween is a no-op and whatever it was meant to reveal never appears. Fix the selector or add the element before rendering."
|
|
8073
8080
|
};
|
|
8074
8081
|
function explainGateFinding(finding) {
|
|
@@ -9656,6 +9663,165 @@ ${detail.slice(-4e3)}`);
|
|
|
9656
9663
|
}
|
|
9657
9664
|
}
|
|
9658
9665
|
|
|
9666
|
+
// src/engine/lib/spell-number.ts
|
|
9667
|
+
import { toCardinal as nwAr } from "n2words/ar-SA";
|
|
9668
|
+
import { toCardinal as nwDe } from "n2words/de-DE";
|
|
9669
|
+
import { toCardinal as nwEn } from "n2words/en-US";
|
|
9670
|
+
import { toCardinal as nwEs } from "n2words/es-ES";
|
|
9671
|
+
import { toCardinal as nwFr } from "n2words/fr-FR";
|
|
9672
|
+
import { toCardinal as nwHi } from "n2words/hi-IN";
|
|
9673
|
+
import { toCardinal as nwIt } from "n2words/it-IT";
|
|
9674
|
+
import { toCardinal as nwJa } from "n2words/ja-JP";
|
|
9675
|
+
import { toCardinal as nwKo } from "n2words/ko-KR";
|
|
9676
|
+
import { toCardinal as nwNl } from "n2words/nl-NL";
|
|
9677
|
+
import { toCardinal as nwPl } from "n2words/pl-PL";
|
|
9678
|
+
import { toCardinal as nwPt } from "n2words/pt-PT";
|
|
9679
|
+
var INTEGER_SPELLERS = {
|
|
9680
|
+
fr: nwFr,
|
|
9681
|
+
es: nwEs,
|
|
9682
|
+
en: nwEn,
|
|
9683
|
+
de: nwDe,
|
|
9684
|
+
it: nwIt,
|
|
9685
|
+
pt: nwPt,
|
|
9686
|
+
nl: nwNl,
|
|
9687
|
+
pl: nwPl,
|
|
9688
|
+
ar: nwAr,
|
|
9689
|
+
ja: nwJa,
|
|
9690
|
+
ko: nwKo,
|
|
9691
|
+
hi: nwHi
|
|
9692
|
+
};
|
|
9693
|
+
function canSpellNumbers(langCode) {
|
|
9694
|
+
return langCode in INTEGER_SPELLERS;
|
|
9695
|
+
}
|
|
9696
|
+
function spellNumber(langCode, n) {
|
|
9697
|
+
const spell = langCode ? INTEGER_SPELLERS[langCode] : void 0;
|
|
9698
|
+
if (!spell || !Number.isFinite(n)) return String(n);
|
|
9699
|
+
try {
|
|
9700
|
+
return spell(n);
|
|
9701
|
+
} catch {
|
|
9702
|
+
return String(n);
|
|
9703
|
+
}
|
|
9704
|
+
}
|
|
9705
|
+
|
|
9706
|
+
// src/engine/nodes/local/lib/script-word-timing.ts
|
|
9707
|
+
var UNBOUNDED_WORD_S = 0.3;
|
|
9708
|
+
var EXACT = 3;
|
|
9709
|
+
var NEAR = 2;
|
|
9710
|
+
var SUBSTITUTE = -1;
|
|
9711
|
+
var SKIP = -2;
|
|
9712
|
+
var NEAR_SIMILARITY = 0.7;
|
|
9713
|
+
function normalize(word) {
|
|
9714
|
+
return word.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
9715
|
+
}
|
|
9716
|
+
function tokensOf(word, index, language) {
|
|
9717
|
+
const key = normalize(word);
|
|
9718
|
+
if (!key) return [];
|
|
9719
|
+
if (language && /^\d{1,9}$/.test(key)) {
|
|
9720
|
+
const spoken = spellNumber(language, Number.parseInt(key, 10));
|
|
9721
|
+
if (spoken !== key) {
|
|
9722
|
+
const parts = spoken.split(/[\s-]+/).map(normalize).filter(Boolean);
|
|
9723
|
+
if (parts.length > 0) return parts.map((part) => ({ key: part, word: index }));
|
|
9724
|
+
}
|
|
9725
|
+
}
|
|
9726
|
+
return [{ key, word: index }];
|
|
9727
|
+
}
|
|
9728
|
+
function similarity(a, b) {
|
|
9729
|
+
if (a === b) return 1;
|
|
9730
|
+
const longest = Math.max(a.length, b.length);
|
|
9731
|
+
if (longest === 0) return 1;
|
|
9732
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
9733
|
+
for (let i = 1; i <= a.length; i++) {
|
|
9734
|
+
const row = [i];
|
|
9735
|
+
for (let j = 1; j <= b.length; j++) {
|
|
9736
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
9737
|
+
row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
9738
|
+
}
|
|
9739
|
+
prev = row;
|
|
9740
|
+
}
|
|
9741
|
+
return 1 - (prev[b.length] ?? longest) / longest;
|
|
9742
|
+
}
|
|
9743
|
+
function pairScore(a, b) {
|
|
9744
|
+
if (a === b) return EXACT;
|
|
9745
|
+
return similarity(a, b) >= NEAR_SIMILARITY ? NEAR : SUBSTITUTE;
|
|
9746
|
+
}
|
|
9747
|
+
var at = (grid, i, j) => grid[i]?.[j] ?? 0;
|
|
9748
|
+
function diagonal(grid, script, transcript, i, j) {
|
|
9749
|
+
return at(grid, i - 1, j - 1) + pairScore(script[i - 1]?.key ?? "", transcript[j - 1]?.key ?? "");
|
|
9750
|
+
}
|
|
9751
|
+
function scoreGrid(script, transcript) {
|
|
9752
|
+
const grid = Array.from(
|
|
9753
|
+
{ length: script.length + 1 },
|
|
9754
|
+
(_, i) => Array.from({ length: transcript.length + 1 }, (_2, j) => i === 0 ? j * SKIP : i * SKIP)
|
|
9755
|
+
);
|
|
9756
|
+
for (let i = 1; i <= script.length; i++) {
|
|
9757
|
+
const row = grid[i];
|
|
9758
|
+
for (let j = 1; j <= transcript.length; j++) {
|
|
9759
|
+
row[j] = Math.max(diagonal(grid, script, transcript, i, j), at(grid, i - 1, j) + SKIP, at(grid, i, j - 1) + SKIP);
|
|
9760
|
+
}
|
|
9761
|
+
}
|
|
9762
|
+
return grid;
|
|
9763
|
+
}
|
|
9764
|
+
function align(script, transcript) {
|
|
9765
|
+
const grid = scoreGrid(script, transcript);
|
|
9766
|
+
const pairs = new Array(script.length).fill(-1);
|
|
9767
|
+
let i = script.length;
|
|
9768
|
+
let j = transcript.length;
|
|
9769
|
+
while (i > 0 && j > 0) {
|
|
9770
|
+
const here = at(grid, i, j);
|
|
9771
|
+
if (here === diagonal(grid, script, transcript, i, j)) {
|
|
9772
|
+
pairs[i - 1] = j - 1;
|
|
9773
|
+
i--;
|
|
9774
|
+
j--;
|
|
9775
|
+
} else if (here === at(grid, i - 1, j) + SKIP) {
|
|
9776
|
+
i--;
|
|
9777
|
+
} else {
|
|
9778
|
+
j--;
|
|
9779
|
+
}
|
|
9780
|
+
}
|
|
9781
|
+
return pairs;
|
|
9782
|
+
}
|
|
9783
|
+
var round = (s) => Math.round(s * 1e3) / 1e3;
|
|
9784
|
+
function pairedSpans(wordCount, scriptTokens, heardTokens, pairs, transcript) {
|
|
9785
|
+
const spans = new Array(wordCount).fill(null);
|
|
9786
|
+
pairs.forEach((heardIndex, t) => {
|
|
9787
|
+
const token = scriptTokens[t];
|
|
9788
|
+
const said = heardIndex >= 0 ? transcript[heardTokens[heardIndex]?.word ?? -1] : void 0;
|
|
9789
|
+
if (!token || !said) return;
|
|
9790
|
+
const current = spans[token.word];
|
|
9791
|
+
spans[token.word] = current ? { start: Math.min(current.start, said.start), end: Math.max(current.end, said.end) } : { start: said.start, end: said.end };
|
|
9792
|
+
});
|
|
9793
|
+
return spans;
|
|
9794
|
+
}
|
|
9795
|
+
function fillRun(spans, first, end) {
|
|
9796
|
+
const before = first > 0 ? spans[first - 1] : null;
|
|
9797
|
+
const after = end < spans.length ? spans[end] : null;
|
|
9798
|
+
const count = end - first;
|
|
9799
|
+
const from = before ? before.end : Math.max(0, (after?.start ?? 0) - count * UNBOUNDED_WORD_S);
|
|
9800
|
+
const to = after ? Math.max(after.start, from) : from + count * UNBOUNDED_WORD_S;
|
|
9801
|
+
const each = (to - from) / count;
|
|
9802
|
+
for (let w = first; w < end; w++) spans[w] = { start: from + (w - first) * each, end: from + (w - first + 1) * each };
|
|
9803
|
+
}
|
|
9804
|
+
function fillDropped(spans) {
|
|
9805
|
+
const filled = [...spans];
|
|
9806
|
+
let i = 0;
|
|
9807
|
+
while (i < filled.length) {
|
|
9808
|
+
let end = i;
|
|
9809
|
+
while (end < filled.length && !filled[end]) end++;
|
|
9810
|
+
if (end > i) fillRun(filled, i, end);
|
|
9811
|
+
i = end + 1;
|
|
9812
|
+
}
|
|
9813
|
+
return filled.map((span) => span ?? { start: 0, end: 0 });
|
|
9814
|
+
}
|
|
9815
|
+
function timeScriptWords(script, transcript, language) {
|
|
9816
|
+
const words = script.split(/\s+/).filter(Boolean);
|
|
9817
|
+
if (words.length === 0 || transcript.length === 0) return [];
|
|
9818
|
+
const scriptTokens = words.flatMap((word, i) => tokensOf(word, i, language));
|
|
9819
|
+
const heardTokens = transcript.flatMap((w, i) => tokensOf(String(w.text ?? ""), i, language));
|
|
9820
|
+
const pairs = align(scriptTokens, heardTokens);
|
|
9821
|
+
const spans = fillDropped(pairedSpans(words.length, scriptTokens, heardTokens, pairs, transcript));
|
|
9822
|
+
return words.map((text, i) => ({ text, start: round(spans[i]?.start ?? 0), end: round(spans[i]?.end ?? 0) }));
|
|
9823
|
+
}
|
|
9824
|
+
|
|
9659
9825
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
9660
9826
|
var VideoTranscribeParams = z33.object({
|
|
9661
9827
|
language: z33.string().min(2).max(8).optional(),
|
|
@@ -9665,6 +9831,9 @@ var VideoTranscribeParams = z33.object({
|
|
|
9665
9831
|
transcriber: z33.enum(["groq", "deepgram"]).optional(),
|
|
9666
9832
|
keyterms: z33.array(z33.string().min(2).max(60)).max(30).optional().describe(
|
|
9667
9833
|
"Names the recogniser must not mangle \u2014 a product, a person, a phrase the script coins. The COMPANY'S OWN NAME is always applied server-side, so you never list it: this is for the rest. Every term is biased into the request AND its spelling enforced on the way out, because a caption prints a mishearing at full size over the ad (a real cut shipped with the brand name spelled wrong). Never patch a misspelling inside a caption composition \u2014 that fix dies with that one creative."
|
|
9834
|
+
),
|
|
9835
|
+
script: z33.string().min(1).optional().describe(
|
|
9836
|
+
"The words the track was asked to say, when there is a script: the transcript's words are then the script's, spelled as written, at the times the recogniser heard them. A misheard word stops being a typo on screen, and a word the recogniser added (a filler) is never printed. Aligned locally, never sent."
|
|
9668
9837
|
)
|
|
9669
9838
|
}).strict();
|
|
9670
9839
|
var VideoTranscribeInputs = z33.object({
|
|
@@ -9695,16 +9864,40 @@ var videoTranscribeNode = defineNode({
|
|
|
9695
9864
|
async execute({ inputs, params, ctx }) {
|
|
9696
9865
|
const audioInput = await tryExtractAudio(inputs, ctx);
|
|
9697
9866
|
const effectiveInputs = audioInput ?? inputs;
|
|
9698
|
-
|
|
9867
|
+
const { script, ...recogniserParams } = params;
|
|
9868
|
+
const outputs = await callBackendExec({
|
|
9699
9869
|
nodeType: "video_transcribe",
|
|
9700
9870
|
nodeVersion: "2.4.0",
|
|
9701
|
-
params,
|
|
9871
|
+
params: recogniserParams,
|
|
9702
9872
|
inputs: effectiveInputs,
|
|
9703
9873
|
outputKinds: { transcript: "json", rich: "json" },
|
|
9704
9874
|
ctx
|
|
9705
9875
|
});
|
|
9876
|
+
if (!script) return outputs;
|
|
9877
|
+
return { ...outputs, transcript: await scriptTranscript(outputs.transcript, script, params.language, ctx) };
|
|
9706
9878
|
}
|
|
9707
9879
|
});
|
|
9880
|
+
async function scriptTranscript(heardRef, script, language, ctx) {
|
|
9881
|
+
const local = heardRef.path ? heardRef : await ctx.assets.ingestRemote({
|
|
9882
|
+
kind: "json",
|
|
9883
|
+
url: String(heardRef.url),
|
|
9884
|
+
sha256: heardRef.sha256,
|
|
9885
|
+
mime: heardRef.mime
|
|
9886
|
+
});
|
|
9887
|
+
const heard = JSON.parse(await readFile13(String(local.path), "utf8"));
|
|
9888
|
+
const words = Array.isArray(heard) ? heard.filter(isTimedWord) : [];
|
|
9889
|
+
const timed = timeScriptWords(script, words, language);
|
|
9890
|
+
ctx.log(`video_transcribe: ${timed.length} script words timed off ${words.length} heard`);
|
|
9891
|
+
const bytes = Buffer.from(JSON.stringify(timed));
|
|
9892
|
+
const metadata = { ...heardRef.metadata, text_of_record: "script", heard_sha256: heardRef.sha256 };
|
|
9893
|
+
return uploadAndIngest({ bytes, kind: "json", mime: "application/json", metadata, ctx }).catch(
|
|
9894
|
+
() => ctx.assets.ingestBytes({ bytes, kind: "json", mime: "application/json", metadata })
|
|
9895
|
+
);
|
|
9896
|
+
}
|
|
9897
|
+
function isTimedWord(value) {
|
|
9898
|
+
const w = value;
|
|
9899
|
+
return Boolean(w) && typeof w?.start === "number" && typeof w?.end === "number" && w?.text !== void 0;
|
|
9900
|
+
}
|
|
9708
9901
|
async function tryExtractAudio(inputs, ctx) {
|
|
9709
9902
|
const video = inputs.video;
|
|
9710
9903
|
if (!video.path || !video.mime?.startsWith("video/")) return null;
|
|
@@ -10025,6 +10218,7 @@ export {
|
|
|
10025
10218
|
LayerExecutionError,
|
|
10026
10219
|
describeFailureReason,
|
|
10027
10220
|
runFailureCode,
|
|
10221
|
+
failedOnlyOnTransientErrors,
|
|
10028
10222
|
AD_FORMAT_PLATFORMS,
|
|
10029
10223
|
platformFormats,
|
|
10030
10224
|
resolveAdaptFormats,
|
|
@@ -10084,6 +10278,8 @@ export {
|
|
|
10084
10278
|
coercePathUrlToUrl,
|
|
10085
10279
|
toModelSafeImage,
|
|
10086
10280
|
loadCompositionMeta,
|
|
10281
|
+
canSpellNumbers,
|
|
10282
|
+
spellNumber,
|
|
10087
10283
|
catalogView,
|
|
10088
10284
|
BackendClient2,
|
|
10089
10285
|
Engine2 as Engine,
|
|
@@ -10095,4 +10291,4 @@ export {
|
|
|
10095
10291
|
defaultRegistry,
|
|
10096
10292
|
createEngineFromEnv
|
|
10097
10293
|
};
|
|
10098
|
-
//# sourceMappingURL=chunk-
|
|
10294
|
+
//# sourceMappingURL=chunk-SZKTVOR7.js.map
|