@koda-sl/baker-cli 0.321.0-dev.2d7a8f01b → 0.323.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 CHANGED
@@ -4925,6 +4925,7 @@ Word-level transcription of a video OR a bare audio track (the `video` input acc
4925
4925
  |---|---|---|---|---|
4926
4926
  | `language` | string | no | auto | ISO 639-1, 2–8 chars (e.g. `en`, `es`) |
4927
4927
  | `transcriber` | enum | no | `groq` | `groq` (Whisper) \| `deepgram` (Nova-3, adds the `rich` output) |
4928
+ | `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 |
4928
4929
 
4929
4930
  **Outputs**
4930
4931
 
@@ -4933,6 +4934,8 @@ Word-level transcription of a video OR a bare audio track (the `video` input acc
4933
4934
  | `transcript` | json | `application/json` — array of `{text, start, end}` (Deepgram prefers the punctuated word form) |
4934
4935
  | `rich` | json | `application/json` — Deepgram only: `{text, words[], paragraphs[], duration_sec, language?}` with punctuated words, sentences, and speaker indices |
4935
4936
 
4937
+ **Known names are spelled the way the brand spells them.** Every term (the company's own name, plus `keyterms`) is biased into the request — Deepgram `keyterm`, Whisper `prompt` — and its spelling is then enforced on the words that come back, because bias is a probability and a caption prints the mistake at full size over the ad. Verified against the take that shipped wrong: the same Spanish clip returns "Cubicap" bare and "Cubicup" with the name applied. Corrections are listed on the output's `brand_spelling_corrections` metadata, so a rewrite is visible rather than silent. A short name (under 6 characters) gets the bias only — at that length ordinary words sit one edit away, and rewriting those to protect a brand would corrupt the line. **Never patch a mishearing inside a caption composition**: that fix dies with that one creative.
4938
+
4936
4939
  **Cost** — 2 credits estimate; backend reports actual based on duration (Groq: $0.04/hr, 10s minimum; Deepgram Nova-3: $0.0043/min).
4937
4940
 
4938
4941
  **Requirements** — `ffmpeg` on PATH for audio extraction (already in the E2B sandbox image). Without it the node still works but uploads the full video and Groq's 100 MB cap applies.
@@ -5051,10 +5054,12 @@ Place and mix several audio clips onto one timeline — a music bed plus timed v
5051
5054
  | `tracks` | array | yes | `[{slot, start_s, duration_s?, gain_db?}]` — `slot` matches a wired input; `start_s` is absolute seconds; `duration_s` hard-caps the clip (trimmed BEFORE placement, so an over-long source can't bleed into the next track's window); `gain_db` sets the static level (e.g. `-20` for a music bed) |
5052
5055
  | `total_ms` | number | no | pins the final length (pad/trim) |
5053
5056
  | `output_format` | enum | no | `mp3` (default) / `wav` / `m4a` |
5054
- | `duck` | object | no | sidechain-duck one track under others: `{ track, against: [...], threshold?, ratio?, attack?, release? }` — the `track` (music) drops while any `against` track (voice) carries signal and recovers in the gaps |
5057
+ | `duck` | object | no | sidechain-duck one track under others: `{ track, against: [...], threshold?, ratio?, attack?, release? }` — the `track` (music) drops while any `against` track (voice) carries signal and recovers in the gaps. `threshold` is linear 0–1; **`attack` and `release` are MILLISECONDS** (typical 5–50 / 200–800, defaults 5 / 300) |
5055
5058
 
5056
5059
  **Outputs** — `audio`. **Cost** — free (local). Requires `ffmpeg`.
5057
5060
 
5061
+ **The bed under a voice is two settled numbers, and `canvas validate` holds both.** A music bed sits at **−20 dB** and is sidechain-ducked against the speech; `AUDIO_MUSIC_BED_TOO_LOUD` warns above −18 dB (a hand-built cut at −13 dB was rejected by the client as too loud) and `AUDIO_MUSIC_BED_NOT_DUCKED` warns when the bed is mixed flat under speech. If a silent tail — an end card, a logo plate — sounds empty, **add the same bed as a second windowed track** (`start_s` + `duration_s`) with its own higher gain; never raise the bed that runs under the voice, which is what measuring loudness across the silent tail invites. `AUDIO_DUCK_UNITS` **blocks** a sub-millisecond `attack`/`release`: `release: 0.45` is a seconds value, and at 0.45 ms the bed snaps back to full level between syllables — which sounds exactly like music mixed too loud. `AUDIO_MUSIC_BED_SCORED_TO_ARC` warns when a `video-background-music-v1` bed's own brief asks for movement (a build, a climax, "follows her arc") under speech — see `music`. (`music-v1` is exempt: `scaffold-video` briefs it with the reference ad's own arc on purpose.)
5062
+
5058
5063
  ---
5059
5064
 
5060
5065
  ##### `image_background_remove`
@@ -5091,6 +5096,8 @@ Strip background → transparent PNG. Powered by `men1scus/birefnet` (Replicate)
5091
5096
 
5092
5097
  Generate music from prompt OR score an existing video clip.
5093
5098
 
5099
+ **Both routes run on the `music_v2` engine** (`ELEVENLABS_MUSIC_ENGINE`), which is separate from the route names below — `elevenlabs/music-v1` still selects compose and `elevenlabs/video-background-music-v1` still selects video-to-music, so existing canvases keep their names. Measured on a real client cut before switching (same clips, same brief, only the engine different): over the 25s carrying speech v1's level wandered **16.3 dB** against v2's **3.9 dB**, v2 sits 1.7 dB further out of the 200 Hz–3 kHz speech band, and v2 ends at −32 dBFS where v1 collapses to −50 dB — that fade is what makes a correct bed read as "empty over the end card". v2 plays ~5× the note attacks, which is the one thing to listen for. Note the per-second **cost of v2 is unconfirmed**: ElevenLabs publishes no per-model music rate and returns no cost header, so `ELEVENLABS_MUSIC_USD_PER_SECOND` is still the v1 figure.
5100
+
5094
5101
  **Inputs**
5095
5102
 
5096
5103
  | Slot | Kind | Required | Notes |
@@ -5129,10 +5136,12 @@ Score an existing video. Requires `inputs.video`.
5129
5136
  | Name | Type | Required | Notes |
5130
5137
  |---|---|---|---|
5131
5138
  | `model` | literal | yes | `"elevenlabs/video-background-music-v1"` |
5132
- | `description` | string | no | max 1000 chars |
5139
+ | `description` | string | no | max 1000 chars — the score in **sound**, not in feelings (see below) |
5133
5140
  | `tags` | string[] | no | up to 10 style tags |
5134
5141
  | `output_format` | enum | no | mp3 family only (see `tts`) |
5135
5142
 
5143
+ **Under speech, a bed is furniture.** This model *watches* the cut, so it scores the footage's emotion unless told not to: name ONE steady character (instruments, tempo, texture) and forbid movement — no build, no drop, no climax, nothing that "follows the arc". A description that narrated the speaker's emotional arc ("quiet and tense while she is overwhelmed, opening as she walks, resolving warm and confident") came back as a film score, and the client's verdict was *"la musica que has elegido es muy dramática"*. Prefer tags that hold it still (`instrumental`, `understated`, `steady`, `no drums`) over tags that move it (`cinematic`, `epic`, `building`, `resolving`). The level and the ducking are separate, settled decisions — see `audio_timeline`.
5144
+
5136
5145
  ---
5137
5146
 
5138
5147
  ##### `dialogue`
@@ -6240,7 +6249,7 @@ Pushing to `main` with changes in `packages/cli/` triggers the GitHub Actions wo
6240
6249
 
6241
6250
  - **0.291.0**: **the image handover Task no longer depends on having uploaded the images.** It was built only from staged `asset.create` image ops, so a Session that audited a live account — found the brand campaign carrying 22 image extensions and the generic one carrying none, and reached for `assets attach` on assets that were *already in the library* — got Google's refusal and nothing else: no upload to hang a Task on, and a CLI hint telling the agent not to raise one by hand because "Baker stages it itself". The one step only the user can take reached them as a sentence in a chat that scrolls away. An image `assets attach` is now **skipped rather than refused**: it still never reaches Google (re-verified live with `validate_only` on v23 *and* v25 against 3865083115, against a control op that validates clean in the same request — `AD_IMAGE` → `UNSUPPORTED_FIELD_TYPE` at customer and campaign level, `MARKETING_IMAGE` → `FIELD_TYPE_INCOMPATIBLE_WITH_CAMPAIGN_TYPE`, `BUSINESS_LOGO` → `CUSTOMER_NOT_VERIFIED`, which is what proves the field type is the objection; **we are not on Google's allowlist for it**), but the skip carries the image and campaign names, recorded on the draft so the next staged op cannot recompute the Task away, and a batch that also carries sitelinks keeps them instead of failing whole. The schema stopped refusing the field type for the same reason: a boundary refusal has no chat to record a handover against.
6242
6251
 
6243
- - **0.307.2**: an ad with an avatar and cutaways stopped dubbing over her. Whether the video model voices a beat itself is now decided **per beat** — it was one switch for the whole ad, so a single `cast: false` cutaway sent HER beats down the dubbed path too, and the default video model always renders audio: her clip spoke her line while an ElevenLabs narrator read the same script over the top, lips matching neither and the timing matching nothing. Her beats keep their own audio, only the beats she is not in are read, and the ad declares `mixed` — the engine's own word for a video with on-camera lines and narrated ones, which also stops the mislabelled-blueprint safety net folding the narrator into her and merging the beats between them into one take. Two fixes to the closing card, both found by rendering it: the panel behind the mark **adapts to the mark**, measured off the file rather than guessed from its name, so a white logo no longer renders as an empty white box in the corner and again 300px tall on the card; and the CTA button is no longer painted in the palette's dominant hex, which is the colour the plate it sits on is filled with — brand-on-brand, invisible as a button, its white copy reading as one more line of the captions below it. The captions themselves are now **cleared off the closing card**, where what they captioned was the line spoken over it: the call to action, printed a second time under the button that already says it.
6252
+ - **0.321.0**: an ad with an avatar and cutaways stopped dubbing over her. Whether the video model voices a beat itself is now decided **per beat** — it was one switch for the whole ad, so a single `cast: false` cutaway sent HER beats down the dubbed path too, and the default video model always renders audio: her clip spoke her line while an ElevenLabs narrator read the same script over the top, lips matching neither and the timing matching nothing. Her beats keep their own audio, only the beats she is not in are read, and the ad declares `mixed` — the engine's own word for a video with on-camera lines and narrated ones, which also stops the mislabelled-blueprint safety net folding the narrator into her and merging the beats between them into one take. Two fixes to the closing card, both found by rendering it: the panel behind the mark **adapts to the mark**, measured off the file rather than guessed from its name, so a white logo no longer renders as an empty white box in the corner and again 300px tall on the card; and the CTA button is no longer painted in the palette's dominant hex, which is the colour the plate it sits on is filled with — brand-on-brand, invisible as a button, its white copy reading as one more line of the captions below it. The captions themselves are now **cleared off the closing card**, where what they captioned was the line spoken over it: the call to action, printed a second time under the button that already says it.
6244
6253
 
6245
6254
  Review follow-ups. **`pickBrandMark` was choosing the wrong file**, which is what made the white mark reachable at all: `\blogo\b` matches `logo-dark.svg` exactly as well as `logo.svg`, so the two tied and the tie broke alphabetically — and `-` sorts before `.`. The variant list now holds `dark`/`light` and their Spanish equivalents (`blanco`, `negativo`, `oscuro`, …), which no client repo's naming was covered by before, and ties break on the shorter name so an unlisted qualifier still loses. **The button's colour is computed against the plate**, not fixed: a near-black pill is the mirror of the bug it replaced on the black plate an ad with no palette gets (and `#14181f` is a legal brand hex), so the fill is the opposite of whatever the plate's luminance says. And **the narrator is cast off the person the ad cast** when no voice is described — naming a narrator of our own lost the fold that used to hand the closing line the presenter's own persona, and the engine's last-resort brief for a speaker in nobody's `cast` is the literal `"ad_narrator voice"`. With `--avatar` that fallback reads the avatar's own subject, persona and speech, which is the case that matters: casting somebody with the flag leaves `cast.description` empty.
6246
6255
 
@@ -17,9 +17,9 @@ import {
17
17
  shouldEscalate
18
18
  } from "./chunk-UIQJZGBG.js";
19
19
 
20
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/index.js
20
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
21
21
  var require_safe_stable_stringify = __commonJS({
22
- "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/index.js"(exports, module) {
22
+ "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
23
23
  "use strict";
24
24
  var { hasOwnProperty } = Object.prototype;
25
25
  var stringify = configure2();
@@ -1111,7 +1111,7 @@ function resolveAdaptFormats(params) {
1111
1111
  return params.formats ?? [];
1112
1112
  }
1113
1113
 
1114
- // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/509a695c9237c6e3727f1f1f99169917f329e3ff3f7877be290b684e24bf153d/node_modules/safe-stable-stringify/esm/wrapper.js
1114
+ // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1115
1115
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1116
1116
  var configure = import__.default.configure;
1117
1117
  var wrapper_default = import__.default;
@@ -3686,6 +3686,10 @@ var STAGE_CODES = {
3686
3686
  OVERLAY_OUT_OF_BOUNDS: "VIDEO_OVERLAY_OUT_OF_BOUNDS",
3687
3687
  ORPHAN_NODE: "ORPHAN_NODE",
3688
3688
  INGEST_PATH_URL_HEALED: "INGEST_PATH_URL_HEALED",
3689
+ DUCK_UNITS: "AUDIO_DUCK_UNITS",
3690
+ MUSIC_BED_TOO_LOUD: "AUDIO_MUSIC_BED_TOO_LOUD",
3691
+ MUSIC_BED_NOT_DUCKED: "AUDIO_MUSIC_BED_NOT_DUCKED",
3692
+ MUSIC_BED_SCORED_TO_ARC: "AUDIO_MUSIC_BED_SCORED_TO_ARC",
3689
3693
  PARALLEL_FORMAT_RENDERS: "IMAGE_PARALLEL_FORMAT_RENDERS"
3690
3694
  };
3691
3695
  var SPAN_MODEL_SLACK_S = 0.25;
@@ -3753,6 +3757,7 @@ function validateCanvas(input, registry) {
3753
3757
  checkOutputRef(ctx);
3754
3758
  checkOrphanNodes(ctx);
3755
3759
  checkParallelFormatRenders(ctx);
3760
+ checkAudioBedMix(ctx);
3756
3761
  checkVideoInvariants(ctx);
3757
3762
  const hasBlocking = issues.some(isBlocking);
3758
3763
  if (hasBlocking) return { ok: false, issues };
@@ -4739,6 +4744,108 @@ function checkParallelFormatRenders(ctx) {
4739
4744
  });
4740
4745
  }
4741
4746
  }
4747
+ var MUSIC_BED_MAX_GAIN_DB = -18;
4748
+ var DUCK_MIN_ATTACK_MS = 0.5;
4749
+ var DUCK_MIN_RELEASE_MS = 10;
4750
+ var SPEECH_NODE_TYPES = /* @__PURE__ */ new Set(["tts", "dialogue", "audio_voice_convert", "video_lipsync"]);
4751
+ var SPEECH_SLOT_RE = /voice|speech|\bvo\b|^vo|_vo|dialog|narration/i;
4752
+ var MUSIC_SLOT_RE = /music|score|\bbed\b/i;
4753
+ function audioTimelineTracks(node) {
4754
+ const tracks = node.params?.tracks;
4755
+ if (!Array.isArray(tracks)) return [];
4756
+ return tracks.filter(
4757
+ (t) => typeof t === "object" && t !== null && typeof t.slot === "string"
4758
+ );
4759
+ }
4760
+ function checkAudioBedMix(ctx) {
4761
+ for (const n of ctx.canvas.nodes) {
4762
+ if (n.type === "audio_timeline") checkOneAudioMix(ctx, n);
4763
+ }
4764
+ }
4765
+ function checkOneAudioMix(ctx, node) {
4766
+ const tracks = audioTimelineTracks(node);
4767
+ if (tracks.length === 0) return;
4768
+ const duck = node.params?.duck;
4769
+ checkDuckUnits(ctx, node, duck);
4770
+ const sourceType = (slot) => refNodeOf(ctx, node.inputs?.[slot])?.type ?? null;
4771
+ const speech = tracks.filter((t) => {
4772
+ const type = sourceType(t.slot);
4773
+ return type !== null && SPEECH_NODE_TYPES.has(type) || SPEECH_SLOT_RE.test(t.slot);
4774
+ });
4775
+ if (speech.length === 0) return;
4776
+ const beds = tracks.filter(
4777
+ (t) => (sourceType(t.slot) === "music" || MUSIC_SLOT_RE.test(t.slot)) && t.duration_s === void 0
4778
+ );
4779
+ if (beds.length === 0) return;
4780
+ checkBedLevels(ctx, node, beds);
4781
+ checkBedDucked(ctx, node, beds, speech, duck);
4782
+ for (const bed of beds) checkBedNotScored(ctx, node, refNodeOf(ctx, node.inputs?.[bed.slot]));
4783
+ }
4784
+ var MUSIC_ARC_RE = /\b(?:emotional arc|follows? (?:her|his|their|the) arc|swell(?:s|ing)?|crescendo|climax|build(?:s|ing) (?:to|into)|resolv(?:e|es|ing)|dramatic|epic|tense|tension)\b/i;
4785
+ function checkBedNotScored(ctx, node, source) {
4786
+ if (source?.type !== "music") return;
4787
+ const params = source.params;
4788
+ if (params?.model !== "elevenlabs/video-background-music-v1") return;
4789
+ const brief = [params?.description, params?.prompt].filter((v) => typeof v === "string").join(" ");
4790
+ const found = MUSIC_ARC_RE.exec(brief);
4791
+ if (!found) return;
4792
+ ctx.issues.push({
4793
+ path: `nodes[${ctx.idToIndex.get(source.id) ?? -1}].params.description`,
4794
+ code: STAGE_CODES.MUSIC_BED_SCORED_TO_ARC,
4795
+ severity: "warning",
4796
+ node_id: source.id,
4797
+ node_type: source.type,
4798
+ message: `"${source.id}" briefs the bed with "${found[0]}", and "${node.id}" mixes it under speech \u2014 this model watches the cut, so movement asked for here comes back as a film score (a testimonial briefed that way was rejected as "muy dram\xE1tica"). Under a voice, name ONE steady character and forbid movement: no build, no drop, no climax, nothing that follows the arc`
4799
+ });
4800
+ }
4801
+ function checkDuckUnits(ctx, node, duck) {
4802
+ const index = ctx.idToIndex.get(node.id) ?? -1;
4803
+ if (typeof duck?.attack === "number" && duck.attack < DUCK_MIN_ATTACK_MS) {
4804
+ ctx.issues.push({
4805
+ path: `nodes[${index}].params.duck.attack`,
4806
+ code: STAGE_CODES.DUCK_UNITS,
4807
+ node_id: node.id,
4808
+ node_type: node.type,
4809
+ message: `"${node.id}" ducks with attack ${duck.attack} \u2014 this parameter is MILLISECONDS, so that is instantaneous. Use 5-50 (default 5)`
4810
+ });
4811
+ }
4812
+ if (typeof duck?.release === "number" && duck.release < DUCK_MIN_RELEASE_MS) {
4813
+ ctx.issues.push({
4814
+ path: `nodes[${index}].params.duck.release`,
4815
+ code: STAGE_CODES.DUCK_UNITS,
4816
+ node_id: node.id,
4817
+ node_type: node.type,
4818
+ message: `"${node.id}" ducks with release ${duck.release} \u2014 this parameter is MILLISECONDS, so the bed jumps back to full level between syllables and the mix sounds like the music is too loud. Use 200-800 (default 300); 0.45 is a seconds value`
4819
+ });
4820
+ }
4821
+ }
4822
+ function checkBedLevels(ctx, node, beds) {
4823
+ const index = ctx.idToIndex.get(node.id) ?? -1;
4824
+ for (const bed of beds) {
4825
+ if ((bed.gain_db ?? 0) <= MUSIC_BED_MAX_GAIN_DB) continue;
4826
+ const level = bed.gain_db === void 0 ? "full level (no gain_db)" : `${bed.gain_db} dB`;
4827
+ ctx.issues.push({
4828
+ path: `nodes[${index}].params.tracks`,
4829
+ code: STAGE_CODES.MUSIC_BED_TOO_LOUD,
4830
+ severity: "warning",
4831
+ node_id: node.id,
4832
+ node_type: node.type,
4833
+ message: `"${node.id}" runs the music bed "${bed.slot}" at ${level} under speech \u2014 a bed belongs at ${MUSIC_BED_MAX_GAIN_DB} dB or below (the scaffolder pins -20; -13 was rejected by a client as too loud). If a silent tail sounds empty, add the same bed as a SECOND windowed track (start_s + duration_s) with its own higher gain instead of raising this one`
4834
+ });
4835
+ }
4836
+ }
4837
+ function checkBedDucked(ctx, node, beds, speech, duck) {
4838
+ if (beds.some((bed) => bed.slot === duck?.track)) return;
4839
+ const quoted = (list) => list.map((t) => `"${t.slot}"`).join(", ");
4840
+ ctx.issues.push({
4841
+ path: `nodes[${ctx.idToIndex.get(node.id) ?? -1}].params.duck`,
4842
+ code: STAGE_CODES.MUSIC_BED_NOT_DUCKED,
4843
+ severity: "warning",
4844
+ node_id: node.id,
4845
+ node_type: node.type,
4846
+ message: `"${node.id}" mixes a music bed (${quoted(beds)}) under speech (${quoted(speech)}) with no sidechain duck on it \u2014 a flat bed fights the voice in every gap. Add params.duck { track: "${beds[0]?.slot}", against: [${quoted(speech)}] }`
4847
+ });
4848
+ }
4742
4849
  function checkOrphanNodes(ctx) {
4743
4850
  const out = ctx.canvas.output;
4744
4851
  if (!out || !ctx.idToIndex.has(out.node)) return;
@@ -7031,16 +7138,28 @@ var Track = z6.object({
7031
7138
  * track that came back oversized) cannot bleed into the next track's window.
7032
7139
  */
7033
7140
  duration_s: z6.number().positive().optional(),
7034
- /** Optional level adjustment in dB (negative ducks, e.g. a music bed at -12). */
7035
- gain_db: z6.number().optional()
7141
+ /**
7142
+ * Level adjustment in dB — negative attenuates. A MUSIC BED under speech
7143
+ * belongs at −20 dB (see the `describe` text: this number is a settled
7144
+ * decision, not a per-cut judgement).
7145
+ */
7146
+ gain_db: z6.number().optional().describe(
7147
+ "Level adjustment in dB (negative attenuates, 0 = untouched). A music bed under speech sits at \u221220 dB and is sidechain-ducked as well; \u221212 dB crowded the voice and \u221213 dB was rejected by the client as too loud. If a silent tail (an end card, a logo plate) sounds empty, do NOT raise the bed \u2014 wire the same bed as a SECOND track windowed over the tail with its own higher gain. Measuring loudness across a window that includes the silent tail is what makes a correct bed look quiet."
7148
+ )
7036
7149
  }).strict();
7037
7150
  var DuckSpec = z6.object({
7038
- track: z6.string().min(1),
7039
- against: z6.array(z6.string().min(1)).min(1),
7040
- threshold: z6.number().min(0).max(1).optional(),
7041
- ratio: z6.number().min(1).max(20).optional(),
7042
- attack: z6.number().min(0.01).optional(),
7043
- release: z6.number().min(0.01).optional()
7151
+ track: z6.string().min(1).describe("Slot to duck \u2014 the music bed."),
7152
+ against: z6.array(z6.string().min(1)).min(1).describe("Key slots whose signal ducks it \u2014 the voice tracks."),
7153
+ threshold: z6.number().min(0).max(1).optional().describe("Linear 0\u20131, not dB. Lower ducks more readily. Default 0.03."),
7154
+ ratio: z6.number().min(1).max(20).optional().describe("How hard it ducks. Default 8; 12 for a firm hold."),
7155
+ // MILLISECONDS is spelled out because a canvas shipped `attack: 0.05,
7156
+ // release: 0.45` — read as seconds — which is a 0.45 ms release: the bed
7157
+ // snapped back to full level between syllables and the client heard the
7158
+ // music as "too loud" at a level that measured correct.
7159
+ attack: z6.number().min(0.01).optional().describe("MILLISECONDS (not seconds). Typical 5\u201350; default 5. A value below 0.5 is a unit mistake."),
7160
+ release: z6.number().min(0.01).optional().describe(
7161
+ "MILLISECONDS (not seconds). Typical 200\u2013800; default 300. Anything under ~10 makes the bed pump back up between words and is a unit mistake."
7162
+ )
7044
7163
  }).strict();
7045
7164
  var AudioTimelineParams = z6.object({
7046
7165
  tracks: z6.array(Track).min(1),
@@ -8924,15 +9043,29 @@ var MusicParams = z25.object({
8924
9043
  /**
8925
9044
  * video-to-music only — short description of the desired score
8926
9045
  * ("upbeat synth, fast cuts, 80s") used to bias the model.
9046
+ *
9047
+ * The `describe` text below is the one piece of craft this node can enforce
9048
+ * nowhere else: a bed under somebody talking must be ONE steady character.
9049
+ * A description that narrated the speaker's emotional arc ("quiet and tense
9050
+ * while she is overwhelmed, opening as she walks, resolving warm and
9051
+ * confident") came back as a film score and the client's verdict was "la
9052
+ * musica que has elegido es muy dramática".
8927
9053
  */
8928
- description: z25.string().max(1e3).optional(),
9054
+ description: z25.string().max(1e3).optional().describe(
9055
+ "The score you want, in sound (instruments, tempo, texture) \u2014 not in feelings. Under speech, name ONE steady character and forbid movement: no build, no drop, no climax, no key change, nothing that 'follows the arc'. This model WATCHES the cut, so it will score the footage's emotion unless told not to \u2014 asking it to track a speaker's emotional arc is how a testimonial bed comes back dramatic. A bed is furniture; the voice carries the story."
9056
+ ),
8929
9057
  /** video-to-music only — up to 10 style tags. */
8930
- tags: z25.array(z25.string()).max(10).optional(),
9058
+ tags: z25.array(z25.string()).max(10).optional().describe(
9059
+ "Style tags. Under speech, prefer tags that hold it still (instrumental, understated, steady, no drums) over tags that move it (cinematic, epic, building, resolving, trailer)."
9060
+ ),
8931
9061
  output_format: z25.enum(ELEVENLABS_OUTPUT_FORMATS).optional()
8932
9062
  }).strict();
8933
9063
  var musicNode = delegated({
8934
9064
  id: "music",
8935
- version: "1.1.0",
9065
+ // 1.2.0: the ElevenLabs engine moved to `music_v2` (ELEVENLABS_MUSIC_ENGINE).
9066
+ // The bump is not cosmetic — nodeVersion is part of the canvas cache key, so
9067
+ // without it a re-run would serve the v1 bed the switch was made to replace.
9068
+ version: "1.2.0",
8936
9069
  category: "audio",
8937
9070
  summary: "Generate music for ad creatives and website video content. `elevenlabs/music-v1` composes from a text prompt or structured composition plan; `elevenlabs/video-background-music-v1` scores an existing video clip provided via `inputs.video`.",
8938
9071
  when_to_use: "Use to produce background music or a full score for video ads, hero-section reels, or any motion content. Prefer the video-to-music model when you already have a cut and want music timed to it; use compose-detailed when you have only a brief or want section-level control (intro / hook / outro). Pair the resulting audio with `video_generate` or `video_lipsync` at compose time.",
@@ -9317,7 +9450,10 @@ var VideoTranscribeParams = z33.object({
9317
9450
  // Provider choice is explicit (no env-based silent branching). Default Groq
9318
9451
  // Whisper; "deepgram" routes to Deepgram Nova-3, which additionally emits a
9319
9452
  // `rich` JSON output with punctuated words + paragraph/sentence grouping.
9320
- transcriber: z33.enum(["groq", "deepgram"]).optional()
9453
+ transcriber: z33.enum(["groq", "deepgram"]).optional(),
9454
+ keyterms: z33.array(z33.string().min(2).max(60)).max(30).optional().describe(
9455
+ "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."
9456
+ )
9321
9457
  }).strict();
9322
9458
  var VideoTranscribeInputs = z33.object({
9323
9459
  // A video (audio auto-extracted locally) OR a bare audio track. The key stays
@@ -9334,10 +9470,10 @@ var VideoTranscribeOutputs = z33.object({
9334
9470
  var AUDIO_EXTRACT_TIMEOUT_MS = 6e4;
9335
9471
  var videoTranscribeNode = defineNode({
9336
9472
  id: "video_transcribe",
9337
- version: "2.3.0",
9473
+ version: "2.4.0",
9338
9474
  category: "language",
9339
9475
  location: "local",
9340
- summary: 'Transcribe a video or audio track to a word-level JSON transcript. Default `transcriber:"groq"` uses Groq Whisper Large v3 Turbo ($0.04/hr, 10s min); `transcriber:"deepgram"` uses Deepgram Nova-3 ($0.0043/min) and additionally emits a `rich` JSON output with punctuated words + paragraph/sentence grouping (and speaker indices). Automatically extracts audio locally (mono 16 kHz MP3) before uploading \u2014 reduces payload ~100\xD7 and lifts the effective duration limit well beyond Groq\'s 100 MB file cap. The `transcript` output is always an array of {text, start, end} entries ready to feed Hyperframes caption compositions (Deepgram prefers the punctuated word form).',
9476
+ summary: 'Transcribe a video or audio track to a word-level JSON transcript. Default `transcriber:"groq"` uses Groq Whisper Large v3 Turbo ($0.04/hr, 10s min); `transcriber:"deepgram"` uses Deepgram Nova-3 ($0.0043/min) and additionally emits a `rich` JSON output with punctuated words + paragraph/sentence grouping (and speaker indices). Automatically extracts audio locally (mono 16 kHz MP3) before uploading \u2014 reduces payload ~100\xD7 and lifts the effective duration limit well beyond Groq\'s 100 MB file cap. The `transcript` output is always an array of {text, start, end} entries ready to feed Hyperframes caption compositions (Deepgram prefers the punctuated word form). Known names are spelled the way the brand spells them: the company\'s own name is biased into the request and enforced on the output automatically, and `keyterms` adds any other name the script depends on.',
9341
9477
  when_to_use: 'Use to generate burned-in captions for a stitched video. Pair with `hyperframe_render` and a captions composition (e.g. `tiktok-captions`) by passing the transcript JSON through `params.variables.transcript`. Pick `params.language` to filter out non-target speech (e.g. "es" for Spanish). Pick `transcriber:"deepgram"` when you want punctuation/paragraphs (read them from the `rich` output) or speaker grouping. Requires ffmpeg on PATH for audio extraction (falls back to full video upload if unavailable).',
9342
9478
  inputs: VideoTranscribeInputs,
9343
9479
  params: VideoTranscribeParams,
@@ -9349,7 +9485,7 @@ var videoTranscribeNode = defineNode({
9349
9485
  const effectiveInputs = audioInput ?? inputs;
9350
9486
  return await callBackendExec({
9351
9487
  nodeType: "video_transcribe",
9352
- nodeVersion: "2.3.0",
9488
+ nodeVersion: "2.4.0",
9353
9489
  params,
9354
9490
  inputs: effectiveInputs,
9355
9491
  outputKinds: { transcript: "json", rich: "json" },
@@ -9666,4 +9802,4 @@ export {
9666
9802
  defaultRegistry,
9667
9803
  createEngineFromEnv
9668
9804
  };
9669
- //# sourceMappingURL=chunk-OLAB3JMW.js.map
9805
+ //# sourceMappingURL=chunk-6N67QEUF.js.map