@koda-sl/baker-cli 0.260.0-dev.07acdacde → 0.262.0-dev.c998276fe

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
@@ -5796,6 +5796,8 @@ This CLI is designed for AI agent consumption. Key patterns:
5796
5796
  - **0.252.0**: avatar voices removed. `--voice-id` / `--voice-description` are gone from `baker avatars create|update`, and an avatar no longer holds a voice at all — every clip is voiced by the video model as it renders, directed by the profile's `--accent`. The picker they replaced offered a voice, played its preview, and then shipped a clip that did not sound like it: the re-voice was speech-to-speech, which replaces timbre and keeps pronunciation, so the accent the user picked could never survive. Three other routes were built and judged on video before removing it — TTS + lip-sync (mouth), audio-driven avatar models (picture), and Seedance 2.5 driven by audio (refuses AI faces). Reasoning and the re-open condition: `docs/adr/0005-an-avatar-is-a-face-not-a-voice.md`.
5797
5797
  - **0.253.0**: the pre-render gate runs `hyperframes check` — lint, runtime, layout, motion and contrast in one browser session, sampled at transition seams — instead of the static `lint` + `inspect` pair, falling back to them when the installed binary predates `check`. The browser half is signal the parser could never produce: measured on HyperFrames' own `warm-grain` example, lint/runtime/layout/motion all reported `ok: true` while the composition rendered an almost entirely blank ten-second video, the only trace being six `GSAP target #a-roll not found` warnings — a tween pointed at an element that does not exist, so nothing it animates ever appears. Baker promotes that warning to blocking, alongside `sweep_static` ("the timeline never advanced", i.e. a still image billed as a video). Warnings are grouped by code with a count (one run returned 62, 53 of them identical) and the highest-value findings carry advice on what to do rather than only what was seen. Requires `hyperframes@0.8.17` in the sandbox, bumped from 0.7.5 in the same change. Reasoning: `docs/adr/0007-the-render-gate-runs-in-a-browser.md`.
5798
5798
  - **0.254.0**: the nested-composition smoke test now looks at the frames instead of trusting the exit code — exiting 0 is what a blank render does too. `hyperframes snapshot --describe` sends the captured frames to Gemini and writes back what is actually in them, and the credential Studio already uses for Gemini and Omni (`GOOGLE_GENERATIVE_AI_API_KEY`) is the `GEMINI_API_KEY` it reads; the backend now ships it to the sandbox by omission, so a deployment without one keeps the plain smoke test. Verified against a real render: the pass reported "the image is completely empty" and named the single visible element without being told what to look for. Frame verdicts are condensed into the node log, one line per frame — a file the agent would have to know to open is a file it does not read.
5799
+ - **0.262.0**: `scaffold-ad` carries the brand. The spec takes `brand.palette` (hex tokens) and `brand.logo` (repo path to the mark), and emits them as the `style.palette` and `branding` the engine already reads to colour its brand plate and CTA. The palette dresses the plate and the type and is deliberately kept OUT of the shot briefs — hex in a frame prompt makes the model paint the whole scene that colour, which reads as a filter. Also takes `market`, since an ad that does not say where it is set is an ad the model sets wherever it likes.
5800
+ - **0.261.0**: four defects that reached a finished ad, fixed in `scaffold-ad` where they originate. Frames render on **`openai/gpt-image-2`**, pinned rather than inheriting the catalogue default (Gemini Flash — right for cheap high-volume work, wrong for the opening still a five-second clip is built from). Every shot brief now states the MARKET, inferred from the voice language when unstated: an ad for Spain came back full of British houses because nothing said where it was. Every shot brief also rules out the impossible — paper and screens are opaque, objects are at real-world scale — after a bill rendered with the text from its reverse showing through, and a solar panel five metres tall. And beat durations snap to lengths the video model can actually render: free-form durations were rounded down per clip, so 11 beats summed to 27.0s of picture against a 31.4s timeline and the 4.4s difference rendered as a black tail. The read rate was re-measured off a real Spanish ad (81 words in 26.48s = 3.06 w/s); the old 2.6 over-predicted every beat by 18%.
5799
5801
  - **0.260.0**: burned-in captions break where the script punctuates instead of every N words. The karaoke composition sliced the transcript into fixed groups, and a word count knows nothing about the sentence it is cutting — an ad the engine rendered showed cards reading "OTRA VEZ, EN" and "TU CASA GENERA", half-thoughts ending on a preposition. A card now closes when the clause does; the word count stays as a ceiling so a long clause is still split, and a linking word ("de", "al", "y") never ends a card because it belongs to what follows. Same rule the pre-render gate already checked for, now enforced where the cards are actually built.
5800
5802
  - **0.259.0**: `baker canvas scaffold-ad` is listed in `baker canvas --help` with a description, and the three video routes are separated in the skill. It had a usage line and no description row, so an agent reading help saw two scaffolders explained and a bare name — and three runs from the same prompt took three different wrong paths: hand-directing the flow, reproducing a competitor ad found via `winning-ads`, and hand-authoring a canvas. The routes now state their own boundary: `scaffold-ad` is the default for a new ad, `scaffold-video` only reproduces a video the USER supplied, and `video-flow.md` opens by saying so rather than calling itself the default eleven times.
5801
5803
  - **0.257.0**: the frame-vision pass now asks whether what is on screen could physically happen, not just whether the frame is empty. The defect that motivated it was a generated shot of a solar panel roughly five metres tall being lifted onto a roof by one person — well lit, on brief, perfectly legible, and impossible. That is what reads loudest as "AI-generated", and the previous question could not see it: nothing was missing, something present was the wrong size. Asked the new question, the pass called the rejected shot *"physically implausible due to the immense weight and surface area"* and the replacement *"consistent with what two people could reasonably maneuver"* — the same verdict a person reached, unprompted.
package/dist/cli.js CHANGED
@@ -32174,12 +32174,30 @@ var AdSpec = z29.object({
32174
32174
  brand: z29.object({
32175
32175
  name: z29.string().optional(),
32176
32176
  /** Figures to lift typographically — the one gold number every ad has. */
32177
- highlight: z29.array(z29.string()).optional()
32177
+ highlight: z29.array(z29.string()).optional(),
32178
+ /**
32179
+ * Brand hex colours, most important first. They dress the brand plate and
32180
+ * the type — never the scenes: pushing hex into a shot brief makes the
32181
+ * model paint the whole frame that colour, which reads as a filter.
32182
+ */
32183
+ palette: z29.array(z29.string()).optional(),
32184
+ /** Repo path to the brand mark, so the CTA carries it instead of a name in a font. */
32185
+ logo: z29.string().optional()
32178
32186
  }).optional(),
32179
32187
  voice: z29.object({
32180
32188
  description: z29.string().min(1),
32181
32189
  language: z29.string().optional()
32182
32190
  }).optional(),
32191
+ /**
32192
+ * Where the ad is set, as a place a model can picture: "Spain", "Mexico",
32193
+ * "the UK". Inferred from the voice's language when omitted.
32194
+ *
32195
+ * It exists because nothing else in a shot brief says where it is, and a model
32196
+ * with no location picks its own. An ad for Spain came back full of British
32197
+ * houses — sash windows, slate roofs, English suburbs — in every single frame,
32198
+ * from briefs that were otherwise perfectly good.
32199
+ */
32200
+ market: z29.string().min(1).optional(),
32183
32201
  /**
32184
32202
  * A bed, described in words. Omitted leaves the ad dry — the engine only wires
32185
32203
  * music when asked, and silence is a legitimate choice.
@@ -32196,15 +32214,37 @@ function narrativeRole(index, total) {
32196
32214
  if (index === total - 1) return "cta";
32197
32215
  return index < total / 2 ? "problem" : "proof";
32198
32216
  }
32199
- function estimateSeconds(line) {
32217
+ var MARKET_BY_LANGUAGE = {
32218
+ es: "Spain",
32219
+ "es-es": "Spain",
32220
+ "es-mx": "Mexico",
32221
+ pt: "Portugal",
32222
+ "pt-br": "Brazil",
32223
+ fr: "France",
32224
+ it: "Italy",
32225
+ de: "Germany",
32226
+ en: "the United Kingdom",
32227
+ "en-us": "the United States"
32228
+ };
32229
+ function marketFor(spec) {
32230
+ if (spec.market) return spec.market;
32231
+ const lang = spec.voice?.language?.trim().toLowerCase();
32232
+ return lang ? MARKET_BY_LANGUAGE[lang] : void 0;
32233
+ }
32234
+ function beatSeconds(line) {
32200
32235
  const words2 = line.trim().split(/\s+/).length;
32201
- return Math.max(1.6, Math.round(words2 / 2.6 * 10) / 10);
32236
+ const spoken = words2 / 3.06;
32237
+ const allowed = [...SEEDANCE_DURATIONS].sort((a, b) => a - b);
32238
+ return allowed.find((d) => d >= spoken) ?? allowed[allowed.length - 1];
32202
32239
  }
32203
32240
  function adSpecToBlueprint(spec) {
32204
32241
  const total = spec.beats.length;
32242
+ const market = marketFor(spec);
32243
+ const place = market ? ` Set in ${market}: the architecture, streets and styling are ${market}'s.` : "";
32244
+ const physics = " Everything obeys real-world physics: paper, card and screens are OPAQUE with nothing showing through from behind, and every object is at believable real-world scale next to the people handling it.";
32205
32245
  let clock = 0;
32206
32246
  const scenes = spec.beats.map((beat, i) => {
32207
- const duration = estimateSeconds(beat.say);
32247
+ const duration = beatSeconds(beat.say);
32208
32248
  const start = clock;
32209
32249
  clock += duration;
32210
32250
  return {
@@ -32227,15 +32267,16 @@ function adSpecToBlueprint(spec) {
32227
32267
  ...spec.voice?.description ? { voice_description: spec.voice.description } : {}
32228
32268
  }
32229
32269
  ],
32230
- motion_prompt: beat.on_camera ? beat.show : (
32270
+ motion_prompt: beat.on_camera ? `${beat.show}${place}${physics}` : (
32231
32271
  // The subject must not be forming words: the voice belongs to the
32232
32272
  // narration, and a mouth moving under it reads as dubbing.
32233
- `${beat.show} Nobody in frame is speaking \u2014 mouths closed, no dialogue.`
32273
+ `${beat.show}${place}${physics} Nobody in frame is speaking \u2014 mouths closed, no dialogue.`
32234
32274
  ),
32235
- start_frame_prompt: beat.show
32275
+ start_frame_prompt: `${beat.show}${place}${physics}`
32236
32276
  };
32237
32277
  });
32238
32278
  return {
32279
+ estimated_duration_s: Math.round(clock * 100) / 100,
32239
32280
  global: {
32240
32281
  transcript: spec.beats.map((b) => b.say).join(" "),
32241
32282
  voiceover: {
@@ -32244,7 +32285,13 @@ function adSpecToBlueprint(spec) {
32244
32285
  mode: "voiceover",
32245
32286
  ...spec.voice?.description ? { voice_description: spec.voice.description } : {}
32246
32287
  },
32247
- ...spec.brand?.name ? { brand: { name: spec.brand.name } } : {},
32288
+ ...spec.brand?.name || spec.brand?.logo ? {
32289
+ branding: {
32290
+ ...spec.brand?.name ? { name: spec.brand.name } : {},
32291
+ ...spec.brand?.logo ? { logo_path: spec.brand.logo } : {}
32292
+ }
32293
+ } : {},
32294
+ ...spec.brand?.palette?.length ? { style: { palette: spec.brand.palette.map((hex) => ({ hex })) } } : {},
32248
32295
  ...spec.music ? { music: { music_prompt: spec.music } } : {}
32249
32296
  },
32250
32297
  scenes
@@ -32268,6 +32315,7 @@ function resolveShippedCanvasDir(name, startDir, exists = existsSync4, maxDepth
32268
32315
  }
32269
32316
 
32270
32317
  // src/commands/canvas/scaffold-ad.ts
32318
+ var AD_IMAGE_MODEL = "openai/gpt-image-2";
32271
32319
  var SHIPPED_COMPOSITION_DIR = resolveShippedCanvasDir("video-overlay-composition", import.meta.dirname);
32272
32320
  var SHIPPED_CAPTIONS_DIR = resolveShippedCanvasDir("tiktok-captions-composition", import.meta.dirname);
32273
32321
  registerSchema({
@@ -32277,7 +32325,7 @@ registerSchema({
32277
32325
  spec: {
32278
32326
  type: "string",
32279
32327
  required: true,
32280
- description: "Path to the ad spec JSON. Shape: { format?, brand?, voice?, beats: [{ say, show, on_camera? }] }. `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."
32328
+ description: "Path to the ad spec JSON. Shape: { format?, market?, brand?, voice?, music?, beats: [{ say, show, on_camera? }] }. 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."
32281
32329
  },
32282
32330
  slug: {
32283
32331
  type: "string",
@@ -32349,7 +32397,7 @@ var scaffoldAdCommand = defineCommand104({
32349
32397
  await writeSceneFiles(outDir, blueprint);
32350
32398
  await writeFile9(blueprintStylePath, renderStyleProjectionFromValue(blueprint), "utf8");
32351
32399
  const opts = {
32352
- imageModel: DEFAULT_IMAGE_GENERATE_MODEL,
32400
+ imageModel: AD_IMAGE_MODEL,
32353
32401
  videoModel: DEFAULT_VIDEO_GENERATE_MODEL,
32354
32402
  overlayCompositionPath: path22.relative(outDir, compositionDest),
32355
32403
  captionsCompositionPath: path22.relative(outDir, captionsDest),
@@ -32366,8 +32414,10 @@ var scaffoldAdCommand = defineCommand104({
32366
32414
  data: { canvas: outPath, beats: spec.data.beats.length, slug },
32367
32415
  hints: [
32368
32416
  `Wrote ${spec.data.beats.length} beats to ${outPath}. Run it with \`baker canvas run ${outPath}\`.`,
32417
+ `These beats run about ${blueprint.estimated_duration_s}s. If the ask was longer, add beats \u2014 the length is the sum of the lines, so a 25s ad needs roughly 25s of script and cannot be stretched by holding shots.`,
32369
32418
  "The caption cards are your `say` lines verbatim \u2014 read them back before running; that is the copy the viewer sees.",
32370
- "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`."
32419
+ "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`.",
32420
+ "If the spec carried no `brand`, read src/brand/BRAND.md and re-run with its palette and logo \u2014 an unbranded ad is the client's message in someone else's clothes."
32371
32421
  ]
32372
32422
  });
32373
32423
  }