@koda-sl/baker-cli 0.270.0 → 0.270.1-dev.0c9e3df08

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.
Files changed (27) hide show
  1. package/README.md +75 -4
  2. package/canvas/tiktok-captions-composition/index.html +23 -2
  3. package/dist/{chunk-2IB3IL4K.js → chunk-2IUN3S3E.js} +2 -2
  4. package/dist/{chunk-6F52WYB7.js → chunk-ARUM5SYE.js} +3 -3
  5. package/dist/{chunk-KSOGK7SJ.js → chunk-IK3GODKH.js} +4 -4
  6. package/dist/{chunk-GU7IWEBC.js → chunk-KDTHRRAC.js} +3 -3
  7. package/dist/{chunk-36KVUYVL.js → chunk-LDDTPUYV.js} +170 -39
  8. package/dist/chunk-LDDTPUYV.js.map +1 -0
  9. package/dist/{chunk-DZUVUGEP.js → chunk-WFWU3CHS.js} +9 -1
  10. package/dist/{chunk-DZUVUGEP.js.map → chunk-WFWU3CHS.js.map} +1 -1
  11. package/dist/cli.js +2366 -719
  12. package/dist/cli.js.map +1 -1
  13. package/dist/{client-VDCCDEHE.js → client-6KQHCXS2.js} +4 -4
  14. package/dist/engine/index.js +3 -3
  15. package/dist/{env-FWMZXMQK.js → env-C7YXL6C6.js} +6 -2
  16. package/dist/{output-JBYI5IVX.js → output-2BQGB5YF.js} +5 -5
  17. package/dist/{shared-GWFXJXKJ.js → shared-CAO5EVLO.js} +6 -6
  18. package/package.json +1 -1
  19. package/dist/chunk-36KVUYVL.js.map +0 -1
  20. /package/dist/{chunk-2IB3IL4K.js.map → chunk-2IUN3S3E.js.map} +0 -0
  21. /package/dist/{chunk-6F52WYB7.js.map → chunk-ARUM5SYE.js.map} +0 -0
  22. /package/dist/{chunk-KSOGK7SJ.js.map → chunk-IK3GODKH.js.map} +0 -0
  23. /package/dist/{chunk-GU7IWEBC.js.map → chunk-KDTHRRAC.js.map} +0 -0
  24. /package/dist/{client-VDCCDEHE.js.map → client-6KQHCXS2.js.map} +0 -0
  25. /package/dist/{env-FWMZXMQK.js.map → env-C7YXL6C6.js.map} +0 -0
  26. /package/dist/{output-JBYI5IVX.js.map → output-2BQGB5YF.js.map} +0 -0
  27. /package/dist/{shared-GWFXJXKJ.js.map → shared-CAO5EVLO.js.map} +0 -0
package/README.md CHANGED
@@ -1248,7 +1248,7 @@ Shared flags: `--days <n>` (default 30) or `--start-date` / `--end-date` (`YYYY-
1248
1248
  - The `sparkline` on `landings` is dropped unless `--full`, and resampled to 30 points when present. It is bucketed by `--granularity` upstream, so ninety days at `hour` is 2,160 numbers per page.
1249
1249
  - Past ~25 rows the response carries a hint naming the row count and pointing at `--output md`.
1250
1250
 
1251
- **Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the dashboard's People screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did.
1251
+ **Paging the list reports.** `people`, `submissions` and `delivery` are lists of individual things rather than breakdowns — `delivery` and `submissions` are the ones the CLI exposes, `people` backs the People section of the dashboard's landing Analytics screen over the same wire — and only they take `--page <n>` (1-based) and `--page-size <n>` (up to 200, default 50). The response carries `pageInfo` with `hasMore` — when it is true there **are** more rows, so a total must never be reported from one page. Pass neither flag and a list comes back at the size it always did.
1252
1252
 
1253
1253
  **A conversion is a company-level named event, not a property of a Form.** `baker analytics conversions` is the only place "what counts" is decided, and an empty `definitions` list means every conversion number in every other report is zero — not because nobody converted, but because nothing is named as an outcome and Baker never guesses which event is the point of a page. `--candidates` lists every event these pages actually produced — each Form step and trigger, every `data-baker-*` event, every outbound destination — with volume and whether it is already counted; that list is where an event key comes from, so never invent one. `--event <key> --name "Booked a call"` starts counting it **immediately and retroactively**: the whole stored history is rescored, so naming the right event today also fixes last month. Nothing here is staged and publishing is not involved. Two events given the same `--name` become one row and one number, which is how a call booked on three different Forms reads as one outcome. `--count-mode every_time` is for the outcomes people genuinely repeat (a guide downloaded twice is two downloads); the default counts once per visit. One outcome per call — repeating `--event` is refused rather than silently keeping the last one.
1254
1254
 
@@ -2820,7 +2820,7 @@ baker avatars create --name "Marta" --subject "…" \
2820
2820
  --source-image "<imageId1>,<imageId2>" --likeness-confirmed
2821
2821
  ```
2822
2822
 
2823
- Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--voice-id`, `--voice-description`. Returns `{ avatarId, handle, likeness, status }`.
2823
+ Optional: `--handle`, `--speech`, `--motion`, `--setting`, `--accent`. Returns `{ avatarId, handle, likeness, status }`.
2824
2824
 
2825
2825
  #### `baker avatars update <handle>`
2826
2826
 
@@ -2829,7 +2829,7 @@ Edit an avatar. Profile flags are merged over the stored profile, so `--persona`
2829
2829
  ```bash
2830
2830
  baker avatars update marta --wardrobe "charcoal blazer over a white tee"
2831
2831
  baker avatars update marta --subject "woman in her early 50s, silver bob, …" --regenerate-sheet
2832
- baker avatars update marta --new-handle marta-founder --voice-id <voiceId>
2832
+ baker avatars update marta --new-handle marta-founder --accent "Spanish from Spain, Madrid accent"
2833
2833
  ```
2834
2834
 
2835
2835
  #### `baker avatars delete <handle>`
@@ -3521,6 +3521,7 @@ Reach for it when you are relaying the **client's** own words. When you are writ
3521
3521
 
3522
3522
  ```bash
3523
3523
  baker studio animate "slow push in, model turns to camera and smiles" --image j57abc123def456ghi789
3524
+ baker studio animate "she looks to camera and says: \"Hola, soy Elena\"" --avatar elena --quality 720p --aspect-ratio 9:16
3524
3525
  baker studio animate "handheld drift right, steam rising from the cup" \
3525
3526
  --image ./out/hero.png --duration 6 --quality 1080p
3526
3527
  baker studio animate "product rotates once on a turntable" --image j57abc…,j57def… --from references
@@ -3670,6 +3671,65 @@ baker canvas set-prompt my-canvas.json s0_start --text "Close-up, flushed and br
3670
3671
  baker canvas set-prompt my-canvas.json s0_start --text-file ./hook-frame.txt
3671
3672
  ```
3672
3673
 
3674
+ #### The brand, the ending and the cast are drawn, not generated
3675
+
3676
+ `baker canvas scaffold-ad` renders everything with a known, fixed appearance locally,
3677
+ because a video model garbles a wordmark every time:
3678
+
3679
+ - **`brand.logo`** — the repo path to the mark. It is copied into the overlay composition
3680
+ and drawn as a real `<img>`: small top-left for the whole ad, large on the closing card.
3681
+ - **`end_card`** — on by default whenever the spec carries a brand. The last beat's PICTURE
3682
+ becomes a flat brand colour plate (rendered by ffmpeg, not generated), with the mark and
3683
+ the call to action over it. `end_card: false` keeps the footage; `end_card: { cta }` sets
3684
+ the words on the button, which otherwise default to the last line of the script.
3685
+ - **The brand fills itself.** When the spec leaves `brand.logo` or `brand.palette` empty,
3686
+ they are read from the workspace — `src/brand/BRAND.md` for the colours (in document
3687
+ order, page-furniture whites and blacks dropped) and `src/brand/logos/` for the mark
3688
+ (vector first, plain mark over a mono/white/icon variant). Pass them yourself only when
3689
+ the ad needs something other than the client's default.
3690
+ - **The spoken language is read off the script** when the spec does not declare it, so a
3691
+ Spanish ad is cast from Spanish voices without anyone having to say so.
3692
+ - **`sound`** (per beat) — one sound effect for that shot, rendered by ElevenLabs
3693
+ alongside the music bed. Most beats should not have one: it belongs where the picture
3694
+ shows a specific physical event (a drill, a door, rain on glass), not on a talking head
3695
+ or a calm lifestyle shot. A bed plus two well-placed effects beats eight.
3696
+ - **A real brand mark is required.** `scaffold-ad` refuses to build without one in
3697
+ `src/brand/logos/` (SVG or PNG): the mark is drawn on every frame and on the closing
3698
+ card, so a stand-in ships an ad wearing the wrong brand.
3699
+ - **An avatar with no pinned voice cannot speak on camera.** Their lines would be read by
3700
+ a separately cast voice, which is dubbing and looks like it.
3701
+ - **`voiceover: false`** — a music-led ad. No voice, no transcription: the `say` lines
3702
+ become on-screen text and are captioned straight from the script, so the words are
3703
+ exact. Give it `music` too.
3704
+ - **`--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.
3705
+ - **`cast.avatar`** — the handle of a cast Avatar (`baker avatars list`). The ad grounds
3706
+ every beat they appear in on that avatar's identity sheet and copies its subject
3707
+ description verbatim, so the ad shows the same face as the rest of the company's work.
3708
+ This is the whole journey: `baker avatars create` → `cast.avatar` in the spec → render.
3709
+ - **`cast.description`** — a person described in words, when there is no avatar.
3710
+
3711
+ Each of these also removes a generation: the closing beat costs no image and no clip.
3712
+
3713
+ #### The finished video is reviewed
3714
+
3715
+ When a run's output is a video, `baker canvas run` samples twelve frames across it and
3716
+ looks at them before reporting success. It reports, never blocks — the clips are already
3717
+ paid for by the time it runs — and the findings come back as `review` and `hints`:
3718
+
3719
+ - **Per frame:** anything that could not physically happen, or shows a job being done
3720
+ incorrectly, plus objects floating unattached in shot. Malformed hands and limbs are
3721
+ rolled up into one finding across the whole render rather than repeated per frame.
3722
+ - **Across the reel:** whether the cast stays the same people, whether a brand mark is
3723
+ visible anywhere, and whether the last frames close the ad or simply stop.
3724
+
3725
+ Each per-frame finding names the **scene file** that produced that second, so a defect is
3726
+ an edit rather than a hunt — and re-running re-renders only the scenes you changed.
3727
+
3728
+ It needs `GEMINI_API_KEY` (the Runtime receives it automatically). Without one the run
3729
+ says the video was **not** reviewed rather than reporting it clean, and a frame that
3730
+ could not be read is reported as unchecked for the same reason.
3731
+
3732
+
3673
3733
  > Scaffolded canvases use **workspace-relative** composition/asset paths and are validated
3674
3734
  > against the engine's hyperframe pre-render gate at `validate` time, so a broken overlay
3675
3735
  > (or a moved/renamed workspace) is caught before any billed clip. The scaffold also adds
@@ -5831,7 +5891,18 @@ This CLI is designed for AI agent consumption. Key patterns:
5831
5891
 
5832
5892
  - **0.217.0**: new family `baker avatars list|get|create|update|delete`, plus `--avatar <handle>` on `baker studio generate` and `baker studio animate` — company-scoped reusable presenters, typed from the shared `@baker/api` avatars contract. `--avatar` is the way to cast one: it grounds the render on the identity sheet, reuses the subject description verbatim, and records the avatar on the batch. Passing the sheet through `--reference` instead does none of those and leaves the batch unattributed, so the avatar can never show what it has appeared in; the route refuses a handle that is not `ready`, because generating a stranger is worse than an error. `get` returns the sheet URL and that description; every command's `hints` carry the next move (still building → re-check with `get`; changed `--subject` without `--regenerate-sheet` → the sheet is stale). `create` refuses `--source-image` without `--likeness-confirmed` and returns an `error.fix` telling the agent to ask the user for that permission, since only they can give it; an unknown handle returns an `error.fix` pointing at `baker avatars list`. Profile flags on `update` merge over the stored profile instead of replacing it.
5833
5893
 
5834
- ## Publishing
5894
+ - **0.250.1**: `--avatar <handle>` on `baker studio generate` and `baker studio animate` never worked. Both were declared in `registerSchema` and read at the call site, but neither was in the command's own citty `args` — and citty does not reject an undeclared flag, it keeps it and sets it to `true`. So `--avatar elena` reached the route as `avatar: true` with `elena` dropped as a stray positional, and the body schema (`z.string()`) refused it. Documented since 0.217.0, broken since 0.217.0: casting an avatar into a render was not possible from the CLI at all, which is why a clip of a named presenter came back with neither the identity sheet nor the voice. Both flags are now declared. A new test in `command-reference-integrity` compares every registered schema against the args of the command that parses it, so the two cannot drift again — it found a third case on the way, `actions status`, whose schema advertised a `--refs` flag for what the command actually takes as positionals.
5895
+ - **0.251.0**: `--accent` on `baker avatars create|update` — the region an avatar's voice comes from, e.g. `"Spanish from Spain, Madrid accent"`. Its own field rather than a sentence inside `--speech`, because that is where it went to die: asked for "pace, register, accent", the agent wrote *"clear Spanish accent"* for a Madrid presenter, the video model read that as no accent, and the clip came out sounding like nowhere. A place can be checked; an adjective cannot. It reaches the render through `buildDeliveryDirection` as its own `ACCENT:` instruction, and `thinProfileHint` now names it first among the missing fields since it is the only one that fails silently — a clip with no accent still looks finished.
5896
+ - **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`.
5897
+ - **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`.
5898
+ - **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.
5899
+ - **0.263.0**: shot briefs refuse the frame that contradicts its own caption, and the vision pass reads the numbers back. Observed verbatim: a phone screen reading "$215.40 SAVED THIS MONTH" under a caption reading "Save over £900 a year" — wrong currency, and a figure that contradicts the claim it sits beneath, since 215.40 a month is 2,584 a year. The model invents a number whenever a screen or document is in shot, so the frames no longer carry legible text at all: the caption track is where an ad's numbers live. Same brief also pins each object's real-world FORM after a phone rendered with a second screen on its back, and names the market's currency for anything unavoidably visible. The frame-vision question now reads out every number and word inside the picture and says whether it contradicts the caption — the defect only exists between the two, so neither alone can see it.
5900
+ - **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.
5901
+ - **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%.
5902
+ - **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.
5903
+ - **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.
5904
+ - **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.
5905
+ - **0.256.0**: the pre-render gate reports caption cards that end mid-clause. The first cut of this check asked for consistency and got it the wrong way round — stripping terminal punctuation makes the cards agree and leaves them wrong, because "En solo nueve días tu casa" is not a line anyone wrote, it is a sentence halved by a word count. Captions split where the script punctuates; each card is then a clause carrying its own mark, grammatical, and consistent as a by-product. The last card is exempt, since a CTA legitimately ends bare. Reported, never rewritten — the copy belongs to whoever wrote it.
5835
5906
 
5836
5907
  ### Auto-publish (CI)
5837
5908
 
@@ -35,6 +35,10 @@
35
35
 
36
36
  .caption-group {
37
37
  position: absolute;
38
+ /* Absolutely positioned with no width, a card sizes to its content and
39
+ runs off the 1080px frame rather than wrapping. Matches the container's
40
+ 60px gutters, so a long clause becomes two lines instead of an overflow. */
41
+ max-width: 960px;
38
42
  font-family: 'Arial Black', 'Helvetica Neue', sans-serif;
39
43
  font-size: {{font_size}}px;
40
44
  font-weight: 900;
@@ -80,10 +84,27 @@
80
84
  const container = document.getElementById('captions');
81
85
  const tl = gsap.timeline({ paused: true });
82
86
 
87
+ // A card closes where the SCRIPT closes, not on a word count. Slicing every
88
+ // N words gave cards reading "OTRA VEZ, EN" — half a thought ending on a
89
+ // preposition. The count stays as a ceiling (a long clause still has to be
90
+ // split), and a linking word never ends a card: it belongs to what follows.
91
+ const CLOSES = /[.,;:!?\u2026]$/;
92
+ const CARRIES_ON = new Set([
93
+ 'de','del','al','a','en','y','e','o','u','que','la','el','los','las','un','una',
94
+ 'con','por','para','su','tu','mi','más','mas','the','of','to','and','in','for','your',
95
+ ]);
96
+ const carriesOn = (w) => CARRIES_ON.has(String(w.text || '').replace(/[.,;:!?\u2026]$/, '').toLowerCase());
97
+
83
98
  const groups = [];
84
- for (let i = 0; i < transcript.length; i += WORDS_PER_GROUP) {
85
- groups.push(transcript.slice(i, i + WORDS_PER_GROUP));
99
+ let current = [];
100
+ for (const word of transcript) {
101
+ current.push(word);
102
+ const last = current[current.length - 1];
103
+ const closes = CLOSES.test(String(last.text || '')) ||
104
+ (current.length >= WORDS_PER_GROUP && !carriesOn(last));
105
+ if (closes) { groups.push(current); current = []; }
86
106
  }
107
+ if (current.length > 0) groups.push(current);
87
108
 
88
109
  groups.forEach((group, gi) => {
89
110
  const el = document.createElement('div');
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  debugLogSetting
3
- } from "./chunk-DZUVUGEP.js";
3
+ } from "./chunk-WFWU3CHS.js";
4
4
 
5
5
  // src/debugLog.ts
6
6
  import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
@@ -150,4 +150,4 @@ export {
150
150
  readBodyForLog,
151
151
  installStreamTaps
152
152
  };
153
- //# sourceMappingURL=chunk-2IB3IL4K.js.map
153
+ //# sourceMappingURL=chunk-2IUN3S3E.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  ApiError,
3
3
  apiGet
4
- } from "./chunk-GU7IWEBC.js";
4
+ } from "./chunk-KDTHRRAC.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-DZUVUGEP.js";
7
+ } from "./chunk-WFWU3CHS.js";
8
8
 
9
9
  // src/error-handler.ts
10
10
  var REQUEST_CONNECTION_PLATFORM = {
@@ -295,4 +295,4 @@ export {
295
295
  writeAdsOutput,
296
296
  resolveCustomerId
297
297
  };
298
- //# sourceMappingURL=chunk-6F52WYB7.js.map
298
+ //# sourceMappingURL=chunk-ARUM5SYE.js.map
@@ -2,13 +2,13 @@ import {
2
2
  handleConnectionError,
3
3
  needsConnectionFix,
4
4
  writeAdsJson
5
- } from "./chunk-6F52WYB7.js";
5
+ } from "./chunk-ARUM5SYE.js";
6
6
  import {
7
7
  ApiError
8
- } from "./chunk-GU7IWEBC.js";
8
+ } from "./chunk-KDTHRRAC.js";
9
9
  import {
10
10
  getEnv
11
- } from "./chunk-DZUVUGEP.js";
11
+ } from "./chunk-WFWU3CHS.js";
12
12
 
13
13
  // src/commands/ads/meta/shared.ts
14
14
  var DAY_MS = 864e5;
@@ -108,4 +108,4 @@ export {
108
108
  csvOrJson,
109
109
  resolveEffectiveStatus
110
110
  };
111
- //# sourceMappingURL=chunk-KSOGK7SJ.js.map
111
+ //# sourceMappingURL=chunk-IK3GODKH.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-2IB3IL4K.js";
4
+ } from "./chunk-2IUN3S3E.js";
5
5
  import {
6
6
  getEnv
7
- } from "./chunk-DZUVUGEP.js";
7
+ } from "./chunk-WFWU3CHS.js";
8
8
 
9
9
  // src/client.ts
10
10
  var MAX_RATE_LIMIT_RETRIES = 3;
@@ -205,4 +205,4 @@ export {
205
205
  apiGet,
206
206
  apiPost
207
207
  };
208
- //# sourceMappingURL=chunk-GU7IWEBC.js.map
208
+ //# sourceMappingURL=chunk-KDTHRRAC.js.map
@@ -1,24 +1,25 @@
1
1
  import {
2
2
  debugLogHttp,
3
3
  readBodyForLog
4
- } from "./chunk-2IB3IL4K.js";
4
+ } from "./chunk-2IUN3S3E.js";
5
5
  import {
6
6
  DIRECT_ROUTE,
7
7
  __commonJS,
8
8
  __toESM,
9
9
  captureProxyCredentials,
10
10
  childEnvWith,
11
+ hasFrameVisionKey,
11
12
  isChallengeBody,
12
13
  isPrivateAddress,
13
14
  isProxyFailure,
14
15
  plannedRoutes,
15
16
  refuseNonPublicUrl,
16
17
  shouldEscalate
17
- } from "./chunk-DZUVUGEP.js";
18
+ } from "./chunk-WFWU3CHS.js";
18
19
 
19
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js
20
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/index.js
20
21
  var require_safe_stable_stringify = __commonJS({
21
- "../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/index.js"(exports, module) {
22
+ "../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/index.js"(exports, module) {
22
23
  "use strict";
23
24
  var { hasOwnProperty } = Object.prototype;
24
25
  var stringify = configure2();
@@ -1033,12 +1034,17 @@ function failureDetail(reason) {
1033
1034
  }
1034
1035
  return { message: describeFailureReason(reason) };
1035
1036
  }
1037
+ function retryVerdict(retryable) {
1038
+ if (retryable === true) return "transient \u2014 retry the run";
1039
+ if (retryable === false) return "not retryable \u2014 retrying will not help, fix the input";
1040
+ return "retryability unknown";
1041
+ }
1036
1042
  function describeCause(c) {
1037
1043
  switch (c.kind) {
1038
1044
  case "local":
1039
1045
  return c.cause instanceof Error ? c.cause.message : String(c.cause);
1040
1046
  case "remote":
1041
- return `[${c.code}] ${c.message}${c.provider ? ` (${c.provider})` : ""}`;
1047
+ return `[${c.code}] ${c.message}${c.provider ? ` (${c.provider})` : ""} \u2014 ${retryVerdict(c.retryable)}`;
1042
1048
  case "timeout":
1043
1049
  return `timeout${c.provider ? ` (${c.provider})` : ""}`;
1044
1050
  case "network":
@@ -1092,7 +1098,7 @@ function resolveAdaptFormats(params) {
1092
1098
  return params.formats ?? [];
1093
1099
  }
1094
1100
 
1095
- // ../../node_modules/.pnpm/safe-stable-stringify@2.5.0/node_modules/safe-stable-stringify/esm/wrapper.js
1101
+ // ../../.pnpm-store/v10/links/@/safe-stable-stringify/2.5.0/88b6d0c76c4dff408de4044be85791896193f2435af7c280bc59312c3fb9649c/node_modules/safe-stable-stringify/esm/wrapper.js
1096
1102
  var import__ = __toESM(require_safe_stable_stringify(), 1);
1097
1103
  var configure = import__.default.configure;
1098
1104
  var wrapper_default = import__.default;
@@ -3135,7 +3141,7 @@ var VEO_DURATIONS = [4, 6, 8];
3135
3141
  var KLING_DURATIONS2 = [5, 10];
3136
3142
  var KLING_NEGATIVE_PROMPT = "warped face, distorted hands, extra fingers, morphing, flicker, on-screen text, watermark, low quality";
3137
3143
  var KLING_CFG_SCALE = 0.7;
3138
- var SPEAKS_PROSE = (line) => `The person speaks to camera; lip-sync follows the dialogue verbatim, with delivery and emotion carried in the wording itself (no bracketed cues). Dialogue: "${line}"`;
3144
+ var SPEAKS_PROSE = (line, accent) => `The person speaks to camera; lip-sync follows the dialogue verbatim, with delivery and emotion carried in the wording itself (no bracketed cues).${accent ? ` The speech is in ${accent}.` : ""} Dialogue: "${line}"`;
3139
3145
  var SEEDANCE_PROFILE = {
3140
3146
  id: "seedance",
3141
3147
  dialogueDirective: SPEAKS_PROSE,
@@ -7253,6 +7259,24 @@ import { execFile as execFile3 } from "child_process";
7253
7259
  import { readFile as readFile9 } from "fs/promises";
7254
7260
  import path12 from "path";
7255
7261
  import { promisify as promisify3 } from "util";
7262
+
7263
+ // src/engine/nodes/local/lib/caption-style.ts
7264
+ var TERMINAL = /[.,;:!?…]$/;
7265
+ function captionTextsFrom(html) {
7266
+ const cards = html.match(/<div[^>]*class="[^"]*\bcaption\b[^"]*"[\s\S]*?<\/div>/g) ?? [];
7267
+ return cards.map(
7268
+ (card) => card.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim()
7269
+ ).filter((text) => text.length > 0);
7270
+ }
7271
+ function fragmentedCaptions(texts) {
7272
+ if (texts.length < 2) return null;
7273
+ const fragments = texts.slice(0, -1).filter((t) => !TERMINAL.test(t));
7274
+ if (fragments.length === 0) return null;
7275
+ const example = fragments[0];
7276
+ return `${fragments.length} of ${texts.length} caption cards end mid-clause, e.g. "${example}". Split captions where the script punctuates, not on a word count \u2014 a card that ends bare is a sentence cut in half by a timing decision, and it reads as one.`;
7277
+ }
7278
+
7279
+ // src/engine/nodes/local/lib/hyperframe-check.ts
7256
7280
  var execFileAsync = promisify3(execFile3);
7257
7281
  var NEVER_BLOCK = [
7258
7282
  /contrast/i,
@@ -7321,6 +7345,74 @@ function classifyCheckOutput(lintRaw, inspectRaw) {
7321
7345
  warnings: findings.filter((f) => f.severity === "warning")
7322
7346
  };
7323
7347
  }
7348
+ var GATE_SECTIONS = ["lint", "runtime", "layout", "motion", "contrast"];
7349
+ function buildCheckArgs(dir, samples) {
7350
+ return ["hyperframes", "check", dir, "--json", "--samples", String(samples), "--at-transitions"];
7351
+ }
7352
+ var PROMOTE_TO_BLOCKING = /GSAP target [^ ]+ not found|no deterministic font mapping/i;
7353
+ function toGateFinding(section, entry) {
7354
+ const f = entry;
7355
+ const code = typeof f.code === "string" ? f.code : "unknown";
7356
+ const message2 = typeof f.message === "string" ? f.message : "";
7357
+ const isError = f.severity === "error" || PROMOTE_TO_BLOCKING.test(message2);
7358
+ const fatal = PROMOTE_TO_BLOCKING.test(message2);
7359
+ return {
7360
+ source: section,
7361
+ code,
7362
+ message: message2,
7363
+ severity: fatal || isError && !isAdvisory(code, message2) ? "blocking" : "warning"
7364
+ };
7365
+ }
7366
+ function classifyGateOutput(raw) {
7367
+ const parsed = parseCheckJson(raw);
7368
+ const findings = GATE_SECTIONS.flatMap((section) => {
7369
+ const block = parsed?.[section];
7370
+ const entries = Array.isArray(block?.findings) ? block.findings : [];
7371
+ return entries.map((entry) => toGateFinding(section, entry));
7372
+ });
7373
+ return {
7374
+ blocking: findings.filter((f) => f.severity === "blocking"),
7375
+ warnings: findings.filter((f) => f.severity === "warning")
7376
+ };
7377
+ }
7378
+ function summariseWarnings(warnings) {
7379
+ const groups = /* @__PURE__ */ new Map();
7380
+ for (const w of warnings) {
7381
+ const key = `${w.source}/${w.code}`;
7382
+ const seen = groups.get(key);
7383
+ if (seen) seen.count += 1;
7384
+ else groups.set(key, { count: 1, message: w.message });
7385
+ }
7386
+ return [...groups.entries()].sort((a, b) => b[1].count - a[1].count).map(([key, g]) => `${g.count}\xD7 [${key}] ${g.message}`);
7387
+ }
7388
+ var FINDING_ADVICE = {
7389
+ sweep_static: "the timeline never advanced, so this renders as a STILL IMAGE while billing like a video \u2014 and every other green verdict in this run is unreliable. Check the composition registers a paused timeline on `window.__timelines[<composition-id>]` and that the tweens target elements that exist.",
7390
+ console_error: "the composition threw in the browser, so whatever that script was driving did not happen.",
7391
+ page_error: "a script failed to evaluate \u2014 animations and any JS-driven layout in this composition did not run.",
7392
+ http_error: "an asset 404ed at render time; it will be missing from the frame, not substituted.",
7393
+ console_warning: "if this names a GSAP target, the element it animates does not exist, so that tween is a no-op and whatever it was meant to reveal never appears. Fix the selector or add the element before rendering."
7394
+ };
7395
+ function explainGateFinding(finding) {
7396
+ const advice = FINDING_ADVICE[finding.code];
7397
+ return advice ? `${finding.message} \u2014 ${advice}` : finding.message;
7398
+ }
7399
+ function condenseFrameDescriptions(markdown) {
7400
+ const lines = [];
7401
+ let frame = null;
7402
+ for (const line of markdown.split("\n")) {
7403
+ const heading = line.match(/^## (.+)$/);
7404
+ if (heading) {
7405
+ frame = heading[1] ?? null;
7406
+ continue;
7407
+ }
7408
+ const text = line.trim();
7409
+ if (frame && text) {
7410
+ lines.push(`${frame}: ${text}`);
7411
+ frame = null;
7412
+ }
7413
+ }
7414
+ return lines;
7415
+ }
7324
7416
  function buildLintArgs(dir) {
7325
7417
  return ["hyperframes", "lint", dir, "--json"];
7326
7418
  }
@@ -7352,8 +7444,13 @@ async function lintStructuralIssues(dir, timeoutMs) {
7352
7444
  function buildInspectArgs(dir, samples) {
7353
7445
  return ["hyperframes", "inspect", dir, "--json", "--samples", String(samples)];
7354
7446
  }
7355
- function buildSnapshotArgs(dir, frames) {
7356
- return ["hyperframes", "snapshot", dir, "--frames", String(frames), "--describe", "false"];
7447
+ function buildSnapshotArgs(dir, frames, vision) {
7448
+ const base = ["hyperframes", "snapshot", dir, "--frames", String(frames)];
7449
+ return vision ? [
7450
+ ...base,
7451
+ "--describe",
7452
+ "Answer four things about this frame. (1) Is it empty or near-empty? Name every element actually visible. (2) Could what is shown physically happen \u2014 object sizes, human scale, and whether every object has its real-world form (a phone has ONE screen, on its front). Say so plainly if something is impossible. (3) Read out every number, currency symbol and word visible INSIDE the picture \u2014 phone screens, documents, signage \u2014 and say whether any of it contradicts the caption on this frame. (4) Is the caption legible against what is behind it?"
7453
+ ] : [...base, "--describe", "false"];
7357
7454
  }
7358
7455
  function usesNestedCompositions(indexHtml) {
7359
7456
  const withoutComments = indexHtml.replace(/<!--[\s\S]*?-->/g, "");
@@ -7382,44 +7479,66 @@ async function runSnapshotSmoke(args, timeoutMs) {
7382
7479
  return { ok: false, unavailable: false, message: (err.stderr || err.message || "snapshot failed").slice(0, 800) };
7383
7480
  }
7384
7481
  }
7385
- async function runHyperframesCheck(opts) {
7386
- const { dir, nodeId, ctx, timeoutMs, samples = 5 } = opts;
7482
+ async function runNestedCompositionSmoke(opts) {
7483
+ const { dir, nodeId, ctx, timeoutMs, samples } = opts;
7484
+ const indexHtml = await readFile9(path12.join(dir, "index.html"), "utf-8").catch(() => "");
7485
+ if (!indexHtml || !usesNestedCompositions(indexHtml)) return;
7486
+ const snap = await runSnapshotSmoke(
7487
+ buildSnapshotArgs(dir, Math.min(samples, 3), hasFrameVisionKey()),
7488
+ Math.max(timeoutMs, 12e4)
7489
+ );
7490
+ if (snap.unavailable) {
7491
+ ctx.log(`${nodeId}: hyperframes snapshot unavailable \u2014 skipping nested-composition smoke test`);
7492
+ return;
7493
+ }
7494
+ if (!snap.ok) {
7495
+ throw new Error(
7496
+ `${nodeId}: nested-composition smoke test failed \u2014 an embedded block did not render. Check the host\u2194block id match, that the block's <style>/<script> live inside its <template>, and that it styles #root (not a class).
7497
+ ${snap.message}`
7498
+ );
7499
+ }
7500
+ const described = await readFile9(path12.join(dir, "snapshots", "descriptions.md"), "utf-8").catch(() => "");
7501
+ for (const line of condenseFrameDescriptions(described)) {
7502
+ ctx.log(`${nodeId}: frame check \u2014 ${line}`);
7503
+ }
7504
+ ctx.log(`${nodeId}: nested-composition smoke test passed`);
7505
+ }
7506
+ async function runGate(dir, timeoutMs, samples) {
7507
+ const checkRaw = await runOne(buildCheckArgs(dir, samples), timeoutMs);
7508
+ if (checkRaw !== null) return classifyGateOutput(checkRaw);
7387
7509
  const [lintRaw, inspectRaw] = await Promise.all([
7388
7510
  runOne(buildLintArgs(dir), timeoutMs),
7389
7511
  runOne(buildInspectArgs(dir, samples), timeoutMs)
7390
7512
  ]);
7391
- if (lintRaw === null && inspectRaw === null) {
7392
- ctx.log(`${nodeId}: hyperframes lint/inspect unavailable \u2014 skipping pre-render check`);
7513
+ if (lintRaw === null && inspectRaw === null) return null;
7514
+ return classifyCheckOutput(lintRaw ?? "", inspectRaw ?? "");
7515
+ }
7516
+ async function runHyperframesCheck(opts) {
7517
+ const { dir, nodeId, ctx, timeoutMs, samples = 5 } = opts;
7518
+ const gate = await runGate(dir, timeoutMs, samples);
7519
+ if (gate === null) {
7520
+ ctx.log(`${nodeId}: hyperframes check/lint/inspect unavailable \u2014 skipping pre-render check`);
7393
7521
  return;
7394
7522
  }
7395
- const { blocking, warnings } = classifyCheckOutput(lintRaw ?? "", inspectRaw ?? "");
7396
- for (const w of warnings) {
7397
- ctx.log(`${nodeId}: hyperframe check warning [${w.source}/${w.code}] ${w.message}`);
7523
+ const { blocking, warnings } = gate;
7524
+ for (const line of summariseWarnings(warnings)) {
7525
+ ctx.log(`${nodeId}: hyperframe check warning ${line}`);
7398
7526
  }
7399
7527
  if (blocking.length > 0) {
7400
- const detail = blocking.map((b) => `\u2022 [${b.source}/${b.code}] ${b.message}`).join("\n");
7401
- throw new Error(`${nodeId}: pre-render check failed (${blocking.length} blocking)
7402
- ${detail}`);
7403
- }
7404
- let indexHtml = "";
7405
- try {
7406
- indexHtml = await readFile9(path12.join(dir, "index.html"), "utf-8");
7407
- } catch {
7408
- indexHtml = "";
7528
+ const seen = /* @__PURE__ */ new Map();
7529
+ for (const b of blocking) seen.set(`${b.source}/${b.code}/${b.message}`, b);
7530
+ const detail = [...seen.values()].map((b) => `\u2022 [${b.source}/${b.code}] ${explainGateFinding(b)}`).join("\n");
7531
+ throw new Error(
7532
+ `${nodeId}: pre-render check failed (${blocking.length} blocking, ${seen.size} distinct)
7533
+ ${detail}`
7534
+ );
7409
7535
  }
7410
- if (indexHtml && usesNestedCompositions(indexHtml)) {
7411
- const snap = await runSnapshotSmoke(buildSnapshotArgs(dir, Math.min(samples, 3)), Math.max(timeoutMs, 12e4));
7412
- if (snap.unavailable) {
7413
- ctx.log(`${nodeId}: hyperframes snapshot unavailable \u2014 skipping nested-composition smoke test`);
7414
- } else if (!snap.ok) {
7415
- throw new Error(
7416
- `${nodeId}: nested-composition smoke test failed \u2014 an embedded block did not render. Check the host\u2194block id match, that the block's <style>/<script> live inside its <template>, and that it styles #root (not a class).
7417
- ${snap.message}`
7418
- );
7419
- } else {
7420
- ctx.log(`${nodeId}: nested-composition smoke test passed`);
7421
- }
7536
+ const composition = await readFile9(path12.join(dir, "index.html"), "utf-8").catch(() => "");
7537
+ const captionComplaint = fragmentedCaptions(captionTextsFrom(composition));
7538
+ if (captionComplaint) {
7539
+ ctx.log(`${nodeId}: caption style \u2014 ${captionComplaint}`);
7422
7540
  }
7541
+ await runNestedCompositionSmoke({ dir, nodeId, ctx, timeoutMs, samples });
7423
7542
  ctx.log(`${nodeId}: pre-render check passed (${warnings.length} warning${warnings.length === 1 ? "" : "s"})`);
7424
7543
  }
7425
7544
 
@@ -8926,7 +9045,14 @@ async function tryExtractAudio(inputs, ctx) {
8926
9045
  import { z as z34 } from "zod";
8927
9046
  var voiceSelectNode = delegated({
8928
9047
  id: "voice_select",
8929
- version: "1.0.0",
9048
+ // 1.1.0: a pinned voice used to come back as an inline `{ kind, text }` with no sha256
9049
+ // and no mime, which every consumer taking the id as an INPUT rejected —
9050
+ // `audio_voice_convert` died on `voice_ref.sha256: expected string, received undefined`.
9051
+ // The backend now uploads it as a real asset like the ranked branch, and the version has
9052
+ // to move with it: the cache key is built from this string, so entries written under
9053
+ // 1.0.0 kept replaying the broken shape long after the fix was deployed. A node whose
9054
+ // OUTPUT shape changes has to bump, not only one whose behaviour does.
9055
+ version: "1.1.0",
8930
9056
  category: "audio",
8931
9057
  summary: 'Cast an ElevenLabs voice from a natural-language description (e.g. "warm, authoritative female narrator, American accent"). Lists the account\'s voices and ranks them against the brief, emitting the best `voice_id` as a bare-string text asset plus a ranked `candidates` JSON.',
8932
9058
  when_to_use: 'Use to turn a voice description (e.g. from a `video_deconstruct` blueprint\'s `voice_description`) into a usable ElevenLabs voice id, then feed it into a `tts` node by wiring `inputs.voice_ref: $ref:<this>.voice_id` and setting `params.voice: "{{voice_ref}}"` \u2014 the engine splices the id in at run time. Review `candidates` (json) to pick a different voice. Optional `gender`/`age`/`accent`/`language` hints sharpen the ranking.',
@@ -8937,6 +9063,11 @@ var voiceSelectNode = delegated({
8937
9063
  age: z34.string().optional(),
8938
9064
  accent: z34.string().optional(),
8939
9065
  language: z34.string().optional(),
9066
+ /**
9067
+ * A voice already chosen — an Avatar's pinned one. Given, nothing is cast: the
9068
+ * ranking is skipped and this id is emitted as-is.
9069
+ */
9070
+ voice_id: z34.string().min(1).max(120).optional(),
8940
9071
  limit: z34.number().int().min(1).max(20).optional()
8941
9072
  }).strict(),
8942
9073
  outputs: z34.object({ voice_id: TextRef, candidates: JsonRef }).strict(),
@@ -9154,4 +9285,4 @@ export {
9154
9285
  defaultRegistry,
9155
9286
  createEngineFromEnv
9156
9287
  };
9157
- //# sourceMappingURL=chunk-36KVUYVL.js.map
9288
+ //# sourceMappingURL=chunk-LDDTPUYV.js.map