@koda-sl/baker-cli 0.389.0 → 0.390.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 +32 -12
- package/dist/{chunk-IOWWKZRF.js → chunk-6UE43HEQ.js} +197 -9
- package/dist/chunk-6UE43HEQ.js.map +1 -0
- package/dist/cli.js +3995 -3725
- 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
|
|
|
@@ -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.
|
|
@@ -3657,13 +3657,13 @@ function spineInputFlags(work, lenS) {
|
|
|
3657
3657
|
function spineInputOps(work, lenS, dims) {
|
|
3658
3658
|
const fit = work.still?.fit ?? "fill";
|
|
3659
3659
|
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
|
|
3660
|
+
const normalize2 = `${scale},format=yuv420p,fps=30,setsar=1,settb=AVTB`;
|
|
3661
3661
|
if (work.trim) {
|
|
3662
3662
|
const start = work.trim.offset_s > 0 ? `start=${work.trim.offset_s.toFixed(3)}:` : "";
|
|
3663
|
-
return `${
|
|
3663
|
+
return `${normalize2},trim=${start}duration=${lenS.toFixed(3)},setpts=PTS-STARTPTS`;
|
|
3664
3664
|
}
|
|
3665
|
-
if (work.still) return `${
|
|
3666
|
-
return
|
|
3665
|
+
if (work.still) return `${normalize2},trim=duration=${lenS.toFixed(3)}`;
|
|
3666
|
+
return normalize2;
|
|
3667
3667
|
}
|
|
3668
3668
|
function parseSpineInputLens(args) {
|
|
3669
3669
|
const lens = /* @__PURE__ */ new Map();
|
|
@@ -7382,10 +7382,10 @@ function applyDuck(params, labelFor, filterChains) {
|
|
|
7382
7382
|
}
|
|
7383
7383
|
const th = duck.threshold ?? 0.03;
|
|
7384
7384
|
const ra = duck.ratio ?? 8;
|
|
7385
|
-
const
|
|
7385
|
+
const at2 = duck.attack ?? 5;
|
|
7386
7386
|
const re = duck.release ?? 300;
|
|
7387
7387
|
filterChains.push(
|
|
7388
|
-
`[${labelFor[trackIdx]}]${keyOut}sidechaincompress=threshold=${th}:ratio=${ra}:attack=${
|
|
7388
|
+
`[${labelFor[trackIdx]}]${keyOut}sidechaincompress=threshold=${th}:ratio=${ra}:attack=${at2}:release=${re}[ducked]`
|
|
7389
7389
|
);
|
|
7390
7390
|
labelFor[trackIdx] = "ducked";
|
|
7391
7391
|
}
|
|
@@ -9656,6 +9656,165 @@ ${detail.slice(-4e3)}`);
|
|
|
9656
9656
|
}
|
|
9657
9657
|
}
|
|
9658
9658
|
|
|
9659
|
+
// src/engine/lib/spell-number.ts
|
|
9660
|
+
import { toCardinal as nwAr } from "n2words/ar-SA";
|
|
9661
|
+
import { toCardinal as nwDe } from "n2words/de-DE";
|
|
9662
|
+
import { toCardinal as nwEn } from "n2words/en-US";
|
|
9663
|
+
import { toCardinal as nwEs } from "n2words/es-ES";
|
|
9664
|
+
import { toCardinal as nwFr } from "n2words/fr-FR";
|
|
9665
|
+
import { toCardinal as nwHi } from "n2words/hi-IN";
|
|
9666
|
+
import { toCardinal as nwIt } from "n2words/it-IT";
|
|
9667
|
+
import { toCardinal as nwJa } from "n2words/ja-JP";
|
|
9668
|
+
import { toCardinal as nwKo } from "n2words/ko-KR";
|
|
9669
|
+
import { toCardinal as nwNl } from "n2words/nl-NL";
|
|
9670
|
+
import { toCardinal as nwPl } from "n2words/pl-PL";
|
|
9671
|
+
import { toCardinal as nwPt } from "n2words/pt-PT";
|
|
9672
|
+
var INTEGER_SPELLERS = {
|
|
9673
|
+
fr: nwFr,
|
|
9674
|
+
es: nwEs,
|
|
9675
|
+
en: nwEn,
|
|
9676
|
+
de: nwDe,
|
|
9677
|
+
it: nwIt,
|
|
9678
|
+
pt: nwPt,
|
|
9679
|
+
nl: nwNl,
|
|
9680
|
+
pl: nwPl,
|
|
9681
|
+
ar: nwAr,
|
|
9682
|
+
ja: nwJa,
|
|
9683
|
+
ko: nwKo,
|
|
9684
|
+
hi: nwHi
|
|
9685
|
+
};
|
|
9686
|
+
function canSpellNumbers(langCode) {
|
|
9687
|
+
return langCode in INTEGER_SPELLERS;
|
|
9688
|
+
}
|
|
9689
|
+
function spellNumber(langCode, n) {
|
|
9690
|
+
const spell = langCode ? INTEGER_SPELLERS[langCode] : void 0;
|
|
9691
|
+
if (!spell || !Number.isFinite(n)) return String(n);
|
|
9692
|
+
try {
|
|
9693
|
+
return spell(n);
|
|
9694
|
+
} catch {
|
|
9695
|
+
return String(n);
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
9698
|
+
|
|
9699
|
+
// src/engine/nodes/local/lib/script-word-timing.ts
|
|
9700
|
+
var UNBOUNDED_WORD_S = 0.3;
|
|
9701
|
+
var EXACT = 3;
|
|
9702
|
+
var NEAR = 2;
|
|
9703
|
+
var SUBSTITUTE = -1;
|
|
9704
|
+
var SKIP = -2;
|
|
9705
|
+
var NEAR_SIMILARITY = 0.7;
|
|
9706
|
+
function normalize(word) {
|
|
9707
|
+
return word.normalize("NFD").replace(new RegExp("\\p{M}", "gu"), "").toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
9708
|
+
}
|
|
9709
|
+
function tokensOf(word, index, language) {
|
|
9710
|
+
const key = normalize(word);
|
|
9711
|
+
if (!key) return [];
|
|
9712
|
+
if (language && /^\d{1,9}$/.test(key)) {
|
|
9713
|
+
const spoken = spellNumber(language, Number.parseInt(key, 10));
|
|
9714
|
+
if (spoken !== key) {
|
|
9715
|
+
const parts = spoken.split(/[\s-]+/).map(normalize).filter(Boolean);
|
|
9716
|
+
if (parts.length > 0) return parts.map((part) => ({ key: part, word: index }));
|
|
9717
|
+
}
|
|
9718
|
+
}
|
|
9719
|
+
return [{ key, word: index }];
|
|
9720
|
+
}
|
|
9721
|
+
function similarity(a, b) {
|
|
9722
|
+
if (a === b) return 1;
|
|
9723
|
+
const longest = Math.max(a.length, b.length);
|
|
9724
|
+
if (longest === 0) return 1;
|
|
9725
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
9726
|
+
for (let i = 1; i <= a.length; i++) {
|
|
9727
|
+
const row = [i];
|
|
9728
|
+
for (let j = 1; j <= b.length; j++) {
|
|
9729
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
9730
|
+
row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
|
|
9731
|
+
}
|
|
9732
|
+
prev = row;
|
|
9733
|
+
}
|
|
9734
|
+
return 1 - (prev[b.length] ?? longest) / longest;
|
|
9735
|
+
}
|
|
9736
|
+
function pairScore(a, b) {
|
|
9737
|
+
if (a === b) return EXACT;
|
|
9738
|
+
return similarity(a, b) >= NEAR_SIMILARITY ? NEAR : SUBSTITUTE;
|
|
9739
|
+
}
|
|
9740
|
+
var at = (grid, i, j) => grid[i]?.[j] ?? 0;
|
|
9741
|
+
function diagonal(grid, script, transcript, i, j) {
|
|
9742
|
+
return at(grid, i - 1, j - 1) + pairScore(script[i - 1]?.key ?? "", transcript[j - 1]?.key ?? "");
|
|
9743
|
+
}
|
|
9744
|
+
function scoreGrid(script, transcript) {
|
|
9745
|
+
const grid = Array.from(
|
|
9746
|
+
{ length: script.length + 1 },
|
|
9747
|
+
(_, i) => Array.from({ length: transcript.length + 1 }, (_2, j) => i === 0 ? j * SKIP : i * SKIP)
|
|
9748
|
+
);
|
|
9749
|
+
for (let i = 1; i <= script.length; i++) {
|
|
9750
|
+
const row = grid[i];
|
|
9751
|
+
for (let j = 1; j <= transcript.length; j++) {
|
|
9752
|
+
row[j] = Math.max(diagonal(grid, script, transcript, i, j), at(grid, i - 1, j) + SKIP, at(grid, i, j - 1) + SKIP);
|
|
9753
|
+
}
|
|
9754
|
+
}
|
|
9755
|
+
return grid;
|
|
9756
|
+
}
|
|
9757
|
+
function align(script, transcript) {
|
|
9758
|
+
const grid = scoreGrid(script, transcript);
|
|
9759
|
+
const pairs = new Array(script.length).fill(-1);
|
|
9760
|
+
let i = script.length;
|
|
9761
|
+
let j = transcript.length;
|
|
9762
|
+
while (i > 0 && j > 0) {
|
|
9763
|
+
const here = at(grid, i, j);
|
|
9764
|
+
if (here === diagonal(grid, script, transcript, i, j)) {
|
|
9765
|
+
pairs[i - 1] = j - 1;
|
|
9766
|
+
i--;
|
|
9767
|
+
j--;
|
|
9768
|
+
} else if (here === at(grid, i - 1, j) + SKIP) {
|
|
9769
|
+
i--;
|
|
9770
|
+
} else {
|
|
9771
|
+
j--;
|
|
9772
|
+
}
|
|
9773
|
+
}
|
|
9774
|
+
return pairs;
|
|
9775
|
+
}
|
|
9776
|
+
var round = (s) => Math.round(s * 1e3) / 1e3;
|
|
9777
|
+
function pairedSpans(wordCount, scriptTokens, heardTokens, pairs, transcript) {
|
|
9778
|
+
const spans = new Array(wordCount).fill(null);
|
|
9779
|
+
pairs.forEach((heardIndex, t) => {
|
|
9780
|
+
const token = scriptTokens[t];
|
|
9781
|
+
const said = heardIndex >= 0 ? transcript[heardTokens[heardIndex]?.word ?? -1] : void 0;
|
|
9782
|
+
if (!token || !said) return;
|
|
9783
|
+
const current = spans[token.word];
|
|
9784
|
+
spans[token.word] = current ? { start: Math.min(current.start, said.start), end: Math.max(current.end, said.end) } : { start: said.start, end: said.end };
|
|
9785
|
+
});
|
|
9786
|
+
return spans;
|
|
9787
|
+
}
|
|
9788
|
+
function fillRun(spans, first, end) {
|
|
9789
|
+
const before = first > 0 ? spans[first - 1] : null;
|
|
9790
|
+
const after = end < spans.length ? spans[end] : null;
|
|
9791
|
+
const count = end - first;
|
|
9792
|
+
const from = before ? before.end : Math.max(0, (after?.start ?? 0) - count * UNBOUNDED_WORD_S);
|
|
9793
|
+
const to = after ? Math.max(after.start, from) : from + count * UNBOUNDED_WORD_S;
|
|
9794
|
+
const each = (to - from) / count;
|
|
9795
|
+
for (let w = first; w < end; w++) spans[w] = { start: from + (w - first) * each, end: from + (w - first + 1) * each };
|
|
9796
|
+
}
|
|
9797
|
+
function fillDropped(spans) {
|
|
9798
|
+
const filled = [...spans];
|
|
9799
|
+
let i = 0;
|
|
9800
|
+
while (i < filled.length) {
|
|
9801
|
+
let end = i;
|
|
9802
|
+
while (end < filled.length && !filled[end]) end++;
|
|
9803
|
+
if (end > i) fillRun(filled, i, end);
|
|
9804
|
+
i = end + 1;
|
|
9805
|
+
}
|
|
9806
|
+
return filled.map((span) => span ?? { start: 0, end: 0 });
|
|
9807
|
+
}
|
|
9808
|
+
function timeScriptWords(script, transcript, language) {
|
|
9809
|
+
const words = script.split(/\s+/).filter(Boolean);
|
|
9810
|
+
if (words.length === 0 || transcript.length === 0) return [];
|
|
9811
|
+
const scriptTokens = words.flatMap((word, i) => tokensOf(word, i, language));
|
|
9812
|
+
const heardTokens = transcript.flatMap((w, i) => tokensOf(String(w.text ?? ""), i, language));
|
|
9813
|
+
const pairs = align(scriptTokens, heardTokens);
|
|
9814
|
+
const spans = fillDropped(pairedSpans(words.length, scriptTokens, heardTokens, pairs, transcript));
|
|
9815
|
+
return words.map((text, i) => ({ text, start: round(spans[i]?.start ?? 0), end: round(spans[i]?.end ?? 0) }));
|
|
9816
|
+
}
|
|
9817
|
+
|
|
9659
9818
|
// src/engine/nodes/remote/videoTranscribe.ts
|
|
9660
9819
|
var VideoTranscribeParams = z33.object({
|
|
9661
9820
|
language: z33.string().min(2).max(8).optional(),
|
|
@@ -9665,6 +9824,9 @@ var VideoTranscribeParams = z33.object({
|
|
|
9665
9824
|
transcriber: z33.enum(["groq", "deepgram"]).optional(),
|
|
9666
9825
|
keyterms: z33.array(z33.string().min(2).max(60)).max(30).optional().describe(
|
|
9667
9826
|
"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."
|
|
9827
|
+
),
|
|
9828
|
+
script: z33.string().min(1).optional().describe(
|
|
9829
|
+
"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
9830
|
)
|
|
9669
9831
|
}).strict();
|
|
9670
9832
|
var VideoTranscribeInputs = z33.object({
|
|
@@ -9695,16 +9857,40 @@ var videoTranscribeNode = defineNode({
|
|
|
9695
9857
|
async execute({ inputs, params, ctx }) {
|
|
9696
9858
|
const audioInput = await tryExtractAudio(inputs, ctx);
|
|
9697
9859
|
const effectiveInputs = audioInput ?? inputs;
|
|
9698
|
-
|
|
9860
|
+
const { script, ...recogniserParams } = params;
|
|
9861
|
+
const outputs = await callBackendExec({
|
|
9699
9862
|
nodeType: "video_transcribe",
|
|
9700
9863
|
nodeVersion: "2.4.0",
|
|
9701
|
-
params,
|
|
9864
|
+
params: recogniserParams,
|
|
9702
9865
|
inputs: effectiveInputs,
|
|
9703
9866
|
outputKinds: { transcript: "json", rich: "json" },
|
|
9704
9867
|
ctx
|
|
9705
9868
|
});
|
|
9869
|
+
if (!script) return outputs;
|
|
9870
|
+
return { ...outputs, transcript: await scriptTranscript(outputs.transcript, script, params.language, ctx) };
|
|
9706
9871
|
}
|
|
9707
9872
|
});
|
|
9873
|
+
async function scriptTranscript(heardRef, script, language, ctx) {
|
|
9874
|
+
const local = heardRef.path ? heardRef : await ctx.assets.ingestRemote({
|
|
9875
|
+
kind: "json",
|
|
9876
|
+
url: String(heardRef.url),
|
|
9877
|
+
sha256: heardRef.sha256,
|
|
9878
|
+
mime: heardRef.mime
|
|
9879
|
+
});
|
|
9880
|
+
const heard = JSON.parse(await readFile13(String(local.path), "utf8"));
|
|
9881
|
+
const words = Array.isArray(heard) ? heard.filter(isTimedWord) : [];
|
|
9882
|
+
const timed = timeScriptWords(script, words, language);
|
|
9883
|
+
ctx.log(`video_transcribe: ${timed.length} script words timed off ${words.length} heard`);
|
|
9884
|
+
const bytes = Buffer.from(JSON.stringify(timed));
|
|
9885
|
+
const metadata = { ...heardRef.metadata, text_of_record: "script", heard_sha256: heardRef.sha256 };
|
|
9886
|
+
return uploadAndIngest({ bytes, kind: "json", mime: "application/json", metadata, ctx }).catch(
|
|
9887
|
+
() => ctx.assets.ingestBytes({ bytes, kind: "json", mime: "application/json", metadata })
|
|
9888
|
+
);
|
|
9889
|
+
}
|
|
9890
|
+
function isTimedWord(value) {
|
|
9891
|
+
const w = value;
|
|
9892
|
+
return Boolean(w) && typeof w?.start === "number" && typeof w?.end === "number" && w?.text !== void 0;
|
|
9893
|
+
}
|
|
9708
9894
|
async function tryExtractAudio(inputs, ctx) {
|
|
9709
9895
|
const video = inputs.video;
|
|
9710
9896
|
if (!video.path || !video.mime?.startsWith("video/")) return null;
|
|
@@ -10084,6 +10270,8 @@ export {
|
|
|
10084
10270
|
coercePathUrlToUrl,
|
|
10085
10271
|
toModelSafeImage,
|
|
10086
10272
|
loadCompositionMeta,
|
|
10273
|
+
canSpellNumbers,
|
|
10274
|
+
spellNumber,
|
|
10087
10275
|
catalogView,
|
|
10088
10276
|
BackendClient2,
|
|
10089
10277
|
Engine2 as Engine,
|
|
@@ -10095,4 +10283,4 @@ export {
|
|
|
10095
10283
|
defaultRegistry,
|
|
10096
10284
|
createEngineFromEnv
|
|
10097
10285
|
};
|
|
10098
|
-
//# sourceMappingURL=chunk-
|
|
10286
|
+
//# sourceMappingURL=chunk-6UE43HEQ.js.map
|