@koda-sl/baker-cli 0.272.1-dev.1f1c09c80 → 0.276.0-dev.1f1c09c80

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
@@ -3648,6 +3648,9 @@ because a video model garbles a wordmark every time:
3648
3648
  the ad needs something other than the client's default.
3649
3649
  - **The spoken language is read off the script** when the spec does not declare it, so a
3650
3650
  Spanish ad is cast from Spanish voices without anyone having to say so.
3651
+ - **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
3652
+ become on-screen text and are captioned straight from the script, so the words are
3653
+ exact. Give it `music` too.
3651
3654
  - **`--avatar <handle>`** — the same flag as on `studio generate` / `studio animate`, and the way to cast a person into an ad. Overrides `cast.avatar` in the spec.
3652
3655
  - **`cast.avatar`** — the handle of a cast Avatar (`baker avatars list`). The ad grounds
3653
3656
  every beat they appear in on that avatar's identity sheet and copies its subject
package/dist/cli.js CHANGED
@@ -29566,9 +29566,32 @@ function emitBlueprintIngests(opts, nodes) {
29566
29566
  params: { source: "path", path: opts.blueprintStylePath ?? "./prompt.style.json", expect: "json" }
29567
29567
  });
29568
29568
  }
29569
+ function emitStaticCaptions(opts, videoRef, nodes) {
29570
+ if (!opts.staticTranscriptPath) return { videoRef };
29571
+ const wordsPerGroup = opts.captionWordsPerGroup ? { words_per_group: opts.captionWordsPerGroup } : {};
29572
+ nodes.push({
29573
+ id: "captions_transcript",
29574
+ type: "ingest",
29575
+ params: { source: "path", path: opts.staticTranscriptPath, expect: "json" }
29576
+ });
29577
+ const overlaid = nodes.find((n) => n.id === "overlaid");
29578
+ if (overlaid) {
29579
+ overlaid.inputs = { ...overlaid.inputs, transcript: "$ref:captions_transcript.json" };
29580
+ overlaid.params = { ...overlaid.params, ...wordsPerGroup };
29581
+ return { videoRef };
29582
+ }
29583
+ nodes.push({
29584
+ id: "captions",
29585
+ type: "hyperframe_render",
29586
+ inputs: { background: videoRef, transcript: "$ref:captions_transcript.json" },
29587
+ params: { composition: opts.captionsCompositionPath, ...wordsPerGroup }
29588
+ });
29589
+ return { videoRef: "$ref:captions.video", videoNode: "captions" };
29590
+ }
29569
29591
  function emitCaptionChain(blueprint, tracks, clock, opts, videoRef, nodes) {
29570
29592
  const voTrackList = tracks.filter((t) => t.kind === "vo");
29571
- if (!opts.captionsCompositionPath || voTrackList.length === 0) return { videoRef };
29593
+ if (!opts.captionsCompositionPath) return { videoRef };
29594
+ if (voTrackList.length === 0) return emitStaticCaptions(opts, videoRef, nodes);
29572
29595
  const voiceMixInputs = {};
29573
29596
  for (const t of voTrackList) voiceMixInputs[t.slot] = t.ref;
29574
29597
  nodes.push({
@@ -30333,7 +30356,8 @@ function buildRunRecord(result, meta, plan, finalLabels) {
30333
30356
  durationMs: result.stats.duration_ms
30334
30357
  },
30335
30358
  nodes,
30336
- finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0
30359
+ finalOutputs: finalOutputs.length > 0 ? finalOutputs : void 0,
30360
+ ...meta.review && meta.review.length > 0 ? { review: meta.review.slice(0, 40) } : {}
30337
30361
  };
30338
30362
  }
30339
30363
  function buildInitialRunRecord(runId, meta) {
@@ -30978,7 +31002,7 @@ function classifyReelFindings(json) {
30978
31002
  if (!r.brand_mark_visible) {
30979
31003
  findings.push({
30980
31004
  severity: "warning",
30981
- what: "No brand mark is visible in any frame. `baker canvas scaffold-ad` draws one from `brand.logo` (the repo path in src/brand/BRAND.md) \u2014 top-left throughout and large on the closing card \u2014 so a mark missing here means the spec carried no logo, or the path did not resolve and the scaffold said so."
31005
+ what: "No brand mark is visible in any frame. THIS IS NOT A RE-RENDER: the mark is drawn by the caption composition as an ordinary `<img>`, never generated as a picture \u2014 so fix the mark itself (a bad path, or an SVG whose geometry clips its own wordmark) or the `.brand-mark` / `.endcard-logo` rules in `<composition>/index.html`, then re-run. Every clip comes back from the cache and only the composition pass renders again, so it costs seconds and no generation."
30982
31006
  });
30983
31007
  }
30984
31008
  if (!r.shows_more_than_a_face) {
@@ -31394,7 +31418,6 @@ ${describeRewrites(healed.rewrites)}
31394
31418
  } : void 0
31395
31419
  });
31396
31420
  await clearRunMarker(outputsDir, filePath);
31397
- if (poster) await poster.flush(buildRunRecord(result, recordMeta, progress?.planInfo(), finalLabels));
31398
31421
  const keepRuns = opts.keepRuns;
31399
31422
  if (keepRuns !== void 0 && Number.isFinite(keepRuns)) {
31400
31423
  await pruneOldRuns(outputsDir, keepRuns, result.run_id, (line) => process.stdout.write(`${line}
@@ -31406,9 +31429,14 @@ ${describeRewrites(healed.rewrites)}
31406
31429
  `Reviewed the finished video and found ${review.length} thing${review.length === 1 ? "" : "s"} to fix. Show the user what is wrong before you show them the ad.`,
31407
31430
  ...review.map((f) => `${f.severity === "blocking" ? "MUST FIX" : "SHOULD FIX"} \u2014 ${f.what}`),
31408
31431
  ...review.some((f) => f.scene) ? [
31409
- `Fix them, then re-run: edit the \`show\` of the scene files named above \u2014 a shot with a person handling equipment is where limbs and objects go wrong, so widen it, move the hands out of frame, or drop the person from that beat. Re-running re-renders ONLY the scenes you edited; everything else comes from the cache (this run: ${result.stats?.cached_nodes ?? 0} of ${result.stats?.total_nodes ?? 0} nodes were cached).`
31432
+ `Anything about the MARK or the closing card is a composition fix, not a re-render: both are drawn as HTML by the caption composition, so editing it and re-running re-renders only that pass \u2014 the clips are cached and no generation is paid for again. Fix them, then re-run: edit the \`show\` of the scene files named above \u2014 a shot with a person handling equipment is where limbs and objects go wrong, so widen it, move the hands out of frame, or drop the person from that beat. Re-running re-renders ONLY the scenes you edited; everything else comes from the cache (this run: ${result.stats?.cached_nodes ?? 0} of ${result.stats?.total_nodes ?? 0} nodes were cached).`
31410
31433
  ] : []
31411
31434
  ] : [];
31435
+ if (poster) {
31436
+ await poster.flush(
31437
+ buildRunRecord(result, { ...recordMeta, ...review ? { review } : {} }, progress?.planInfo(), finalLabels)
31438
+ );
31439
+ }
31412
31440
  process.stdout.write(
31413
31441
  `${JSON.stringify(
31414
31442
  {
@@ -32490,12 +32518,16 @@ import { defineCommand as defineCommand104 } from "citty";
32490
32518
  // src/engine/scaffold/ad-spec.ts
32491
32519
  import { z as z30 } from "zod";
32492
32520
  var oneClause = (line) => (line.match(/[.!?](\s|$)/g) ?? []).length <= 1;
32521
+ var isSpeech = (line) => new RegExp("\\p{L}\\p{L}", "u").test(line);
32493
32522
  var Beat = z30.object({
32494
32523
  /**
32495
32524
  * The line the voice says in this beat — ONE clause, ending in its own
32496
32525
  * punctuation. It becomes the caption card verbatim.
32497
32526
  */
32498
- say: z30.string().min(1).refine(oneClause, "a beat is one clause: split a second sentence into its own beat"),
32527
+ say: z30.string().min(1).refine(
32528
+ isSpeech,
32529
+ "a beat's line has to be words someone says \u2014 a dot or a dash is not a line, and an ad with no voiceover is not supported yet"
32530
+ ).refine(oneClause, "a beat is one clause: split a second sentence into its own beat"),
32499
32531
  /** What is on screen while it is said, as a shot brief for the video model. */
32500
32532
  show: z30.string().min(1),
32501
32533
  /**
@@ -32597,6 +32629,24 @@ var AdSpec = z30.object({
32597
32629
  cta: z30.string().min(1).optional()
32598
32630
  })
32599
32631
  ]).optional(),
32632
+ /**
32633
+ * Whether a voice reads the lines.
32634
+ *
32635
+ * On by default. Turned off, the ad carries its music and its pictures and the `say`
32636
+ * lines become ON-SCREEN text instead of spoken ones — which is how most music-led
32637
+ * social ads are actually built.
32638
+ *
32639
+ * It exists because an agent wanted exactly this and had no way to ask: it wrote "·"
32640
+ * as every line with a voice description reading "No voiceover; music only". The dot
32641
+ * was sent to the voice model, the near-silence that came back was transcribed, and
32642
+ * the transcriber hallucinated "Thank you." — which became the ad's only caption card,
32643
+ * held over all sixteen seconds, in English, in a Spanish ad.
32644
+ *
32645
+ * The captions are BETTER on this path, not worse: they come from the script itself
32646
+ * rather than from transcribing a rendered read, so the text is exact and no speech
32647
+ * model is between the copy and the screen.
32648
+ */
32649
+ voiceover: z30.boolean().default(true),
32600
32650
  beats: z30.array(Beat).min(2)
32601
32651
  });
32602
32652
  function presenterIsInEveryShot(spec) {
@@ -32687,6 +32737,10 @@ function beatSeconds(line) {
32687
32737
  return allowed.find((d) => d >= spoken) ?? allowed[allowed.length - 1];
32688
32738
  }
32689
32739
  var BRAND_PLATE = "A solid flat brand colour plate \u2014 an end card. No people, no objects, no scenery, no text of any kind.";
32740
+ var ASKS_FOR_LEGIBLE = /\b(focus on the (number|amount|total|price|figure|text)|readable|legible|clearly (shows?|visible)|highlighted (number|amount|total)|showing the (number|amount|total|price))\b/i;
32741
+ function briefWantsLegibleText(show) {
32742
+ return ASKS_FOR_LEGIBLE.test(show);
32743
+ }
32690
32744
  function endCardWanted(spec) {
32691
32745
  if (spec.end_card === false) return false;
32692
32746
  return Boolean(spec.brand?.name || spec.brand?.logo);
@@ -32718,6 +32772,30 @@ function adSpecCastElements(spec, avatar) {
32718
32772
  }
32719
32773
  ];
32720
32774
  }
32775
+ function motionPrompt(beat, isClosingCard, place, physics) {
32776
+ if (isClosingCard) return BRAND_PLATE;
32777
+ const shot = `${beat.show}${place}${physics}`;
32778
+ return beat.on_camera ? shot : `${shot} Nobody in frame is speaking \u2014 mouths closed, no dialogue.`;
32779
+ }
32780
+ function staticTranscript(spec) {
32781
+ const out = [];
32782
+ let clock = 0;
32783
+ for (const beat of spec.beats) {
32784
+ const duration = beatSeconds(beat.say);
32785
+ const words2 = beat.say.trim().split(/\s+/).filter(Boolean);
32786
+ const each = words2.length > 0 ? duration / words2.length : duration;
32787
+ words2.forEach((text2, i) => {
32788
+ const start = clock + i * each;
32789
+ out.push({
32790
+ text: text2,
32791
+ start: Math.round(start * 100) / 100,
32792
+ end: Math.round((start + each) * 100) / 100
32793
+ });
32794
+ });
32795
+ clock += duration;
32796
+ }
32797
+ return out;
32798
+ }
32721
32799
  function adSpecToBlueprint(spec) {
32722
32800
  const total = spec.beats.length;
32723
32801
  const market = marketFor(spec);
@@ -32742,7 +32820,10 @@ function adSpecToBlueprint(spec) {
32742
32820
  // `on_camera` is the engine's own switch for "this line is narration" —
32743
32821
  // absent, it guesses from who is in frame, which is how a still photograph
32744
32822
  // once got lip-synced.
32745
- dialogue: [
32823
+ // No dialogue at all when nobody speaks: an empty array is what stops the engine
32824
+ // wiring a voice track, and no voice track is what makes this a music-led ad
32825
+ // rather than one with a silent narrator.
32826
+ dialogue: spec.voiceover === false ? [] : [
32746
32827
  {
32747
32828
  line: beat.say,
32748
32829
  start_s: Math.round(start * 100) / 100,
@@ -32754,11 +32835,7 @@ function adSpecToBlueprint(spec) {
32754
32835
  ...spec.voice?.description ? { voice_description: spec.voice.description } : {}
32755
32836
  }
32756
32837
  ],
32757
- motion_prompt: isClosingCard ? BRAND_PLATE : beat.on_camera ? `${beat.show}${place}${physics}` : (
32758
- // The subject must not be forming words: the voice belongs to the
32759
- // narration, and a mouth moving under it reads as dubbing.
32760
- `${beat.show}${place}${physics} Nobody in frame is speaking \u2014 mouths closed, no dialogue.`
32761
- ),
32838
+ motion_prompt: motionPrompt(beat, isClosingCard, place, physics),
32762
32839
  start_frame_prompt: isClosingCard ? BRAND_PLATE : `${beat.show}${place}${physics}`
32763
32840
  };
32764
32841
  });
@@ -32768,8 +32845,9 @@ function adSpecToBlueprint(spec) {
32768
32845
  transcript: spec.beats.map((b) => b.say).join(" "),
32769
32846
  voiceover: {
32770
32847
  // Narration over the picture. Never `on_camera`: that is what tells the
32771
- // engine a mouth is a lip-sync candidate.
32772
- mode: "voiceover",
32848
+ // engine a mouth is a lip-sync candidate. A music-led ad says `none`, which is
32849
+ // what keeps a voice out of it entirely.
32850
+ mode: spec.voiceover === false ? "none" : "voiceover",
32773
32851
  ...spec.voice?.description ? { voice_description: spec.voice.description } : {}
32774
32852
  },
32775
32853
  ...spec.brand?.name || spec.brand?.logo ? {
@@ -32786,6 +32864,7 @@ function adSpecToBlueprint(spec) {
32786
32864
  }
32787
32865
 
32788
32866
  // src/engine/scaffold/ad-brand-overlay.ts
32867
+ var markFallback = (name) => name.trim() ? ` onerror="this.replaceWith(Object.assign(document.createElement('span'),{className:this.className+' brand-wordmark',textContent:${JSON.stringify(name.trim())}}))"` : "";
32789
32868
  var escapeHtml2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
32790
32869
  function firstHex(palette) {
32791
32870
  const hex = (palette ?? []).find((h) => /^#?[0-9a-fA-F]{6}$/.test(h.trim()));
@@ -32805,12 +32884,14 @@ function buildAdBrandOverlay(opts) {
32805
32884
  ];
32806
32885
  if (logo) {
32807
32886
  parts.push(
32808
- `<img class="clip brand-mark" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" data-start="0" data-dur="${totalS}" />`
32887
+ `<img class="clip brand-mark" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" data-start="0" data-dur="${totalS}"${markFallback(logo.alt)} />`
32809
32888
  );
32810
32889
  }
32811
32890
  const cardBits = [];
32812
32891
  if (logo) {
32813
- cardBits.push(`<img class="endcard-logo" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}" />`);
32892
+ cardBits.push(
32893
+ `<img class="endcard-logo" src="${escapeHtml2(logo.file)}" alt="${escapeHtml2(logo.alt)}"${markFallback(logo.alt)} />`
32894
+ );
32814
32895
  }
32815
32896
  if (cta?.trim()) {
32816
32897
  const style = accent ? ` style="background:${escapeHtml2(accent)}"` : "";
@@ -32881,6 +32962,11 @@ function adBrandOverlayCss() {
32881
32962
  " .endcard-logo { width: 62%; height: auto; max-height: 300px; object-fit: contain;",
32882
32963
  " padding: 46px 56px; border-radius: 40px; background: rgba(255,255,255,0.96);",
32883
32964
  " box-shadow: 0 10px 40px rgba(0,0,0,0.28); }",
32965
+ " /* The name, when the mark itself could not be drawn. */",
32966
+ " .brand-wordmark { display: inline-block; font-family: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;",
32967
+ " font-weight: 900; color: #14231b; letter-spacing: -0.01em; }",
32968
+ " .brand-mark.brand-wordmark { font-size: 34px; line-height: 1; height: auto; }",
32969
+ " .endcard-logo.brand-wordmark { font-size: 96px; line-height: 1; width: auto; }",
32884
32970
  " /* Capped and wrapping, not sized to its content: measured in a real browser,",
32885
32971
  " `GET YOUR FREE QUOTE` renders 948px wide against a 1080px canvas, so the next",
32886
32972
  " slightly longer call to action runs off the frame. Two lines beat an edge. */",
@@ -32983,7 +33069,7 @@ registerSchema({
32983
33069
  spec: {
32984
33070
  type: "string",
32985
33071
  required: true,
32986
- description: 'Path to the ad spec JSON. Shape: { format?, market?, brand?, cast?, end_card?, voice?, music?, beats: [{ say, show, on_camera?, cast? }] }. Fill `brand` from src/brand/BRAND.md \u2014 `palette` (its hex tokens, most important first) and `logo` (the repo path to the mark) are what make the ad look like the client rather than like stock. `market` is where the ad is SET; omitted, it is inferred from the voice language, and getting it wrong is what fills a Spanish ad with British houses. `say` is ONE clause ending in its own punctuation \u2014 it becomes a caption card verbatim, so two sentences in one beat produce a card holding both. `show` is the shot brief for that line. Set `on_camera` ONLY when that beat\'s subject talks to camera. `cast` is WHO the ad is about, and an ad with people in it needs one: `{ "avatar": "marta" }` names a cast avatar (`baker avatars list`) and every beat is grounded on that avatar\'s identity sheet with its subject description copied verbatim \u2014 the same face here as in the rest of the company\'s work. Without it each beat invents its own stranger, which is how one 28-second ad came back with five different men playing one customer. `{ "description": "..." }` is the fallback when there is no avatar, and it scaffolds a canvas that asks you to drop a photo before it can run \u2014 so prefer the avatar. A beat sets `cast: false` for a shot they are not in. `end_card` is on by default whenever `brand` is set: the last beat becomes a flat brand plate with the mark and a call to action drawn over it. `{ "cta": "..." }` sets the button copy, `false` keeps the footage.'
33072
+ description: 'Path to the ad spec JSON. Shape: { format?, market?, brand?, cast?, end_card?, voice?, music?, beats: [{ say, show, on_camera?, cast? }] }. Fill `brand` from src/brand/BRAND.md \u2014 `palette` (its hex tokens, most important first) and `logo` (the repo path to the mark) are what make the ad look like the client rather than like stock. `market` is where the ad is SET; omitted, it is inferred from the voice language, and getting it wrong is what fills a Spanish ad with British houses. `say` is ONE clause ending in its own punctuation \u2014 it becomes a caption card verbatim, so two sentences in one beat produce a card holding both. `show` is the shot brief for that line. Set `on_camera` ONLY when that beat\'s subject talks to camera. `voiceover: false` makes it a MUSIC-LED ad: nobody speaks, the `say` lines become on-screen text captioned straight from the script, and it needs `music`. `cast` is WHO the ad is about, and an ad with people in it needs one: `{ "avatar": "marta" }` names a cast avatar (`baker avatars list`) and every beat is grounded on that avatar\'s identity sheet with its subject description copied verbatim \u2014 the same face here as in the rest of the company\'s work. Without it each beat invents its own stranger, which is how one 28-second ad came back with five different men playing one customer. `{ "description": "..." }` is the fallback when there is no avatar, and it scaffolds a canvas that asks you to drop a photo before it can run \u2014 so prefer the avatar. A beat sets `cast: false` for a shot they are not in. `end_card` is on by default whenever `brand` is set: the last beat becomes a flat brand plate with the mark and a call to action drawn over it. `{ "cta": "..." }` sets the button copy, `false` keeps the footage.'
32987
33073
  },
32988
33074
  avatar: {
32989
33075
  type: "string",
@@ -33051,6 +33137,19 @@ var scaffoldAdCommand = defineCommand104({
33051
33137
  if (flagAvatar) {
33052
33138
  spec.data.cast = { ...spec.data.cast, avatar: flagAvatar };
33053
33139
  }
33140
+ const contradicts = spec.data.beats.map((beat, i) => ({ i, show: beat.show })).filter(({ show }) => briefWantsLegibleText(show));
33141
+ if (contradicts.length > 0) {
33142
+ writeJson({
33143
+ ok: false,
33144
+ error: {
33145
+ code: "VALIDATION_ERROR",
33146
+ message: `Beat ${contradicts.map(({ i }) => i + 1).join(", ")} asks the picture for a readable figure (e.g. "${contradicts[0]?.show.trim().slice(0, 90)}"). Every frame also carries "no readable text or numbers", so the two fight and the picture wins \u2014 one such brief rendered a bill reading $58,085.00 over a total of $139.00.`,
33147
+ fix: "Say what the shot LOOKS like, not what it reads: 'a bill in her hands, her face falling'. The ad's numbers belong in the `say` line, where the caption track renders them correctly."
33148
+ }
33149
+ });
33150
+ process.exit(1);
33151
+ return;
33152
+ }
33054
33153
  const handle = flagAvatar || spec.data.cast?.avatar?.trim();
33055
33154
  let avatar = null;
33056
33155
  let avatarError = null;
@@ -33095,6 +33194,11 @@ var scaffoldAdCommand = defineCommand104({
33095
33194
  await writeSceneFiles(outDir, blueprint);
33096
33195
  await writeFile9(blueprintStylePath, renderStyleProjectionFromValue(blueprint), "utf8");
33097
33196
  const logoPath = spec.data.brand?.logo?.trim();
33197
+ const transcriptPath = path24.join(outDir, "transcript.json");
33198
+ if (spec.data.voiceover === false) {
33199
+ await writeFile9(transcriptPath, `${JSON.stringify(staticTranscript(spec.data), null, 2)}
33200
+ `, "utf-8");
33201
+ }
33098
33202
  const opts = {
33099
33203
  imageModel: AD_IMAGE_MODEL,
33100
33204
  // The sheet on the SAME model as the frames it feeds. A sheet from another
@@ -33114,7 +33218,8 @@ var scaffoldAdCommand = defineCommand104({
33114
33218
  captionWordsPerGroup: 8,
33115
33219
  // Told, not guessed: the voice description is written in the ad's own language
33116
33220
  // and the engine's trait parser reads English only.
33117
- voiceLanguage: voiceLanguageFor(spec.data)
33221
+ voiceLanguage: voiceLanguageFor(spec.data),
33222
+ ...spec.data.voiceover === false ? { staticTranscriptPath: path24.relative(outDir, transcriptPath) } : {}
33118
33223
  };
33119
33224
  const canvas = scaffoldVideoCanvas(blueprint, adSpecCastElements(spec.data, avatar), opts);
33120
33225
  const renderNode3 = canvas.nodes.find((n) => n.type === "hyperframe_render");
@@ -33178,6 +33283,9 @@ var scaffoldAdCommand = defineCommand104({
33178
33283
  ...spec.data.cast && !avatar ? [
33179
33284
  "This cast has a description but no avatar, so the canvas asks you to drop a real source photo before it can run. `baker avatars list` \u2014 casting one and passing `cast.avatar` fills that slot for you and keeps the face consistent across every piece of work."
33180
33285
  ] : [],
33286
+ ...spec.data.voiceover === false ? [
33287
+ "Nobody speaks in this ad, so the `say` lines are ON-SCREEN text: they are captioned straight from the script, exactly as written, with no voice and no transcription in between. Give it `music` \u2014 a music-led ad with no bed is a silent one."
33288
+ ] : [],
33181
33289
  "The caption cards are your `say` lines verbatim \u2014 read them back before running; that is the copy the viewer sees.",
33182
33290
  "Every clip renders with nobody speaking unless a beat set `on_camera`. If a beat needs a presenter talking, set it there rather than describing it in `show`.",
33183
33291
  ...spec.data.brand?.palette?.length && spec.data.brand?.logo ? [] : [