@mevdragon/vidfarm-devcli 0.20.1 → 0.20.3

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.
@@ -142,9 +142,10 @@ You usually already know a clip's aspect from how you sourced it (a hunted raw's
142
142
  - `transitions` — `default`/`intro`/`outro` (map to one `set_transitions` call), `usage`.
143
143
  - `broll` — `reliance`, `sourcing`, `shot_kinds`, `cadence`. Drives whether you HUNT raws (`/clips/scan`) vs `generate_layer`, and at what cadence.
144
144
  - `audio` — `voiceover`, `music`, `sfx`, `captions_from`.
145
+ - `emotional` — HOW to edit so the FEELING survives: `target_feeling`, `tone`, `comedic_timing` (the beat/pause/hard-cut that sells the joke), `intonation` (vocal cadence to preserve when you re-voice), `vibe_anchors` (the concrete edit moves that carry the emotion — often "keep the trending sound and land the beat drop on the reveal cut"). The vibe/joke/intonation/sound are the first things a remix flattens, so treat these as must-dos.
145
146
  - `scenes[]` — each beat's `role` + `importance` + `must_keep` + `edit_bias`. `important_scenes`, `editing_bias`, `do`, `dont`.
146
147
 
147
- **How to use it:** map `caption_style` → `set_captions`, `transitions.*` → `set_transitions`, honor `pacing`, follow `broll` for footage sourcing, lay `audio`. **PROTECT** any beat in `important_scenes` or with `must_keep:true` / `importance:"critical"` — swap its subject if needed but preserve its timing, role, and caption cadence; that's where the style lives. Treat `editing_bias`/`do`/`dont` as hard constraints for this composition. It sets defaults, not a straitjacket — an explicit user instruction wins. **In local devcli** the same brief is on disk as `editor-harness.json` (under `.harness`, pulled by `vidfarm pull`/`sync`); read it there before a big edit. If `editor_harness` is absent, the fork wasn't decomposed with the harness pass — fall back to `composition_context` + the `video_context` tool.
148
+ **How to use it:** map `caption_style` → `set_captions`, `transitions.*` → `set_transitions`, honor `pacing`, follow `broll` for footage sourcing, lay `audio`, and honor `emotional` (preserve `comedic_timing`, keep `intonation` when re-voicing narration, execute every `vibe_anchors` entry). **PROTECT** any beat in `important_scenes` or with `must_keep:true` / `importance:"critical"` — swap its subject if needed but preserve its timing, role, and caption cadence; that's where the style lives. Treat `editing_bias`/`do`/`dont` as hard constraints for this composition. It sets defaults, not a straitjacket — an explicit user instruction wins. **In local devcli** the same brief is on disk as `editor-harness.json` (under `.harness`, pulled by `vidfarm pull`/`sync`); read it there before a big edit. If `editor_harness` is absent, the fork wasn't decomposed with the harness pass — fall back to `composition_context` + the `video_context` tool.
148
149
 
149
150
  ## Web-editor motion rule (hard constraint)
150
151
 
package/SKILL.director.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: vidfarm-director
3
- description: Use Vidfarm as a director. Browse/add inspiration videos, fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as a raw REST route and as a 1:1 `vidfarm-devcli` command.
3
+ description: Use Vidfarm as a director. Browse/add inspiration videos, fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
4
4
  ---
5
5
 
6
6
  # Vidfarm Director
@@ -136,6 +136,19 @@ Content-Type: application/json
136
136
 
137
137
  Returns the same fork metadata shape as `POST /api/v1/compositions` (201). devcli: `vidfarm clone <forkId> [--from current|default] [--version N] [--title …]`. In the in-editor AI chat this is the `editor_action` `fork_composition` action (`fork_from: current|default`).
138
138
 
139
+ ## Automate a template via REST
140
+
141
+ Templates now have the same job-backed REST pattern as primitives, so a script can run them repeatably without the editor UI:
142
+
143
+ ```
144
+ POST /api/v1/templates/:templateId/operations/:operationName
145
+ Content-Type: application/json
146
+
147
+ { "tracer": "my-run", "payload": { ... }, "webhook_url": "https://..." }
148
+ ```
149
+
150
+ The route returns `202` with a `job_id`, and you can poll the job with `GET /api/v1/user/me/jobs/:jobId` or the template-scoped `GET /api/v1/templates/:templateId/jobs/:jobId`. devcli wraps the same flow as `vidfarm template run <templateId> <operationName> --payload-file payload.json --wait`.
151
+
139
152
  ## Agentic editing: the three axes (SWAP ↔ REPLACE)
140
153
 
141
154
  Almost every editor session is a director taking a template / fork / project and **re-working** it, and a re-work only ever touches three independent axes — **SCENES** (the video/image clips carrying the visuals), **AUDIO** (narration/voiceover, music, SFX), and **TEXT** (captions, titles, overlays). On each axis the intent sits on a **SWAP ↔ REPLACE** spectrum, and the three axes in one request can sit at different points — decide each before you act:
@@ -187,7 +200,7 @@ Content-Type: application/json
187
200
 
188
201
  Two modes:
189
202
 
190
- - **`smart`** — uses the caller's Gemini/OpenAI/OpenRouter key. Samples frames, extracts scenes with labels, literal visual **descriptions**, and viral notes, extracts on-screen captions with positions, **transcribes the audio track verbatim** (timestamped segments; Gemini first, OpenAI Whisper fallback; skipped when the video has no audio), and detects viral DNA (hook, retention, payoff). Takes 30-60 seconds. Auto-fires GhostCut for subtitle removal in the background.
203
+ - **`smart`** — uses the caller's Gemini/OpenAI/OpenRouter key. Samples frames, extracts scenes with labels, literal visual **descriptions**, and viral notes, extracts on-screen captions with positions, **transcribes the audio track verbatim** (timestamped segments; Gemini first, OpenAI Whisper fallback; skipped when the video has no audio), and detects viral DNA (hook, retention, payoff, plus **`emotional_punch`** — the vibe/joke/intonation that makes it FEEL, reasoned over BOTH the frames and the transcript so the trending sound and vocal delivery are captured, not just the visuals). Takes 30-60 seconds. Auto-fires GhostCut for subtitle removal in the background.
191
204
  - **`time-slice`** — deterministic equal-duration split. Instant. No AI provider needed.
192
205
 
193
206
  **Source length cap.** Vidfarm caps auto-decompose (and all inspiration ingest) at **120 seconds** of source video. If the source is longer, the API responds with `400 { ok: false, code: "source_too_long", duration_seconds, max_duration_seconds }` and no scenes are written. Trim the source before forking / ingesting.
@@ -226,12 +239,25 @@ Read-only state (does **not** advance the job or bill): `GET /api/v1/composition
226
239
  "viral_note": "...",
227
240
  "transcript_excerpt": "what is spoken during this scene"
228
241
  }],
229
- "viral_dna": { ... }
242
+ "viral_dna": {
243
+ "trend_tagline": "...", "hook": "...", "retention": "...", "payoff": "...",
244
+ "emotional_punch": {
245
+ "core_emotion": "smug relief-laugh", "tone": "deadpan comedic",
246
+ "arc": "flat setup → mounting absurdity → sudden turn", "peak_moment": "the reveal at ~0:07",
247
+ "mechanism": "subversion + trending-sound beat drop synced to the cut",
248
+ "humor": "the joke is X — it works because Y (rebuild the SAME joke around a new subject)",
249
+ "delivery": "flat VO cadence, hold the beat before the punch, beat drop lands on the reveal",
250
+ "preserve": ["keep the beat drop on the reveal cut", "keep the flat unbothered delivery"]
251
+ },
252
+ "preserve": ["..."], "avoid": ["..."], "promotions": ["..."], "keywords": ["..."]
253
+ }
230
254
  }
231
255
  ```
232
256
 
233
257
  `status: "none"` means the fork was never smart-decomposed — run `POST /auto-decompose` with `mode: "smart"` first. `transcript` is `null` when the source has no audio track or transcription failed.
234
258
 
259
+ **`viral_dna.emotional_punch`** is the FEELING of the format and what makes it land — the part a remix most often flattens. Where hook/retention/payoff are the mechanical structure, this captures the vibe, the joke, and the intonation. It is now **audio-aware**: the decompose feeds the transcript into the reasoning, so `mechanism`/`delivery` account for the trending sound, the music-bed drop, and vocal cadence (on short-form the sound is frequently the biggest driver of the punch). When you re-theme or remix, rebuild `humor`'s joke around the new subject rather than dropping it, match `tone`/`delivery` so intonation and comedic timing survive, and never trade the `peak_moment` payoff for a flat product plug.
260
+
235
261
  Use it whenever you need to know what the video says or shows: writing/translating captions, matching hooks or dubs to spoken audio, or planning scene-level edits.
236
262
 
237
263
  - **Editor chat (frontend AI)** exposes this as the optional `video_context` tool — the copilot calls it on demand.
@@ -254,13 +280,14 @@ Use it whenever you need to know what the video says or shows: writing/translati
254
280
  "broll": { "reliance": "heavy", "sourcing": "screen-recording", "shot_kinds": ["b_roll"], "cadence": "new visual ~every 2s" },
255
281
  "transitions": { "default": "cut", "intro": "fade", "outro": "none", "usage": "hard cuts only" },
256
282
  "audio": { "voiceover": "first-person narration", "music": "upbeat lofi bed, low", "sfx": "whoosh on cuts", "captions_from": "voiceover" },
283
+ "emotional": { "target_feeling": "smug relief-laugh", "tone": "deadpan comedic", "comedic_timing": "hold a beat, then hard cut on the punch word", "intonation": "flat unbothered VO, no upspeak", "vibe_anchors": ["keep the trending sound, land the beat drop on the reveal cut", "hold silence before the payoff"] },
257
284
  "scenes": [{ "role": "hook", "importance": "critical", "must_keep": true, "note": "...", "edit_bias": "swap subject, keep timing+caption cadence" }],
258
285
  "important_scenes": ["..."], "editing_bias": ["..."], "do": ["..."], "dont": ["..."]
259
286
  }
260
287
  }
261
288
  ```
262
289
 
263
- **Use it to pick concrete moves:** `typography.caption_style` maps to a `set_captions` preset; `transitions.*` to a `set_transitions` call; keep `pacing` (cut rhythm / avg_scene_seconds) when adding or splitting scenes; follow `broll.reliance`/`sourcing` to decide HUNT raws (`/raws/scan`) vs generate; lay `audio.*`. **Protect** any beat in `important_scenes` or with `must_keep:true` / `importance:"critical"` — swap the subject but preserve its timing, role, and caption cadence. Treat `editing_bias`/`do`/`dont` as hard constraints; an explicit user instruction still wins. `status:"none"` → run `POST /auto-decompose` first.
290
+ **Use it to pick concrete moves:** `typography.caption_style` maps to a `set_captions` preset; `transitions.*` to a `set_transitions` call; keep `pacing` (cut rhythm / avg_scene_seconds) when adding or splitting scenes; follow `broll.reliance`/`sourcing` to decide HUNT raws (`/raws/scan`) vs generate; lay `audio.*`. **`emotional`** is HOW to keep the FEELING while you swap the subject — the vibe/joke/intonation/sound are the first things a remix flattens, so honor `emotional.comedic_timing` (the held beat / hard cut that sells the joke), preserve `emotional.intonation` when you re-voice narration, and treat every `emotional.vibe_anchors` entry as a must-do (on short-form the trending sound / beat drop is often the biggest carrier of the punch — keep it and land it on the same cut). **Protect** any beat in `important_scenes` or with `must_keep:true` / `importance:"critical"` — swap the subject but preserve its timing, role, and caption cadence. Treat `editing_bias`/`do`/`dont` as hard constraints; an explicit user instruction still wins. `status:"none"` → run `POST /auto-decompose` first.
264
291
 
265
292
  - **Editor chat (frontend AI)** already receives the harness inline in `editor_context.editor_harness` (no tool call needed); it can also fetch this route via `http_request`.
266
293
  - **Desktop agents (Claude Code / Codex)**: `vidfarm pull` writes `editor-harness.json` to the fork dir (the brief is under `.harness`) alongside `video-context.json`; read it before a big edit. The `pull` grounding line reports whether it's present.
@@ -439,7 +466,7 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
439
466
 
440
467
  ## Render (publish to MP4)
441
468
 
442
- Rendering publishes the fork's current working state to an MP4 using HyperFrames (cloud Lambda fan-out on the deployed host, or the free in-process renderer on a local `vidfarm serve` box). In the Trackpad Editor this is the **Render** button. The REST route is `/render`; the older `/export` path is kept as a deprecated alias for already-published devcli clients (treat "Render", "publish", `/render`, and `/export` as the same operation). The devcli exposes it as `vidfarm render <forkId> [--wait]`.
469
+ Rendering publishes the fork's current working state to an MP4 using HyperFrames (cloud Lambda fan-out on the deployed host, or the free in-process renderer on a local `vidfarm serve` box). In the Trackpad Editor this is the **Render** button. The REST route is `/render`; the older `/export` path is kept as a deprecated alias for already-published devcli clients (treat "Render", "publish", `/render`, and `/export` as the same operation). The devcli exposes it as `vidfarm render <forkId> [--wait]`; scripted local dirs can use `vidfarm render <forkId> --dir ./work --wait`.
443
470
 
444
471
  ```
445
472
  POST /api/v1/compositions/:forkId/render
@@ -448,16 +475,18 @@ Content-Type: application/json
448
475
  { "title": "optional", "version": null, "tracer": "optional-trace-id", "html": "optional HTML to save to working state before rendering" }
449
476
  ```
450
477
 
451
- Returns 202 with `{ ok, renderId, status, progress, outputUrl, cost, title, version, ... }` where `version` is the snapshot version created for this publish. Failure modes: `402` if the fork owner is not on a paid plan, `409` while GhostCut subtitle removal is still `pending` (retry after `/remove-video-captions-poll` reports done/failed), `412` if the provider-key preflight fails. Poll:
478
+ Returns 202 with `{ ok, renderId, status, progress, expectedOutputPublicUrl, outputUrl, cost, title, version, ... }` where `expectedOutputPublicUrl` is the deterministic public MP4 URL and `version` is the snapshot version created for this publish. Failure modes: `402` if the fork owner is not on a paid plan, `409` while GhostCut subtitle removal is still `pending` (retry after `/remove-video-captions-poll` reports done/failed), `412` if the provider-key preflight fails. Poll:
452
479
 
453
480
  ```
454
481
  GET /api/v1/compositions/:forkId/renders/:renderId
455
482
  ```
456
483
 
457
- Response includes `{ ok, renderId, status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, framesRendered, totalFrames, cost, outputUrl, outputS3Uri, errors }` (`progress` is 0..1). On success, `outputUrl` is the durable MP4 URL.
484
+ Response includes `{ ok, renderId, status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, framesRendered, totalFrames, cost, expectedOutputPublicUrl, outputUrl, outputS3Uri, errors }` (`progress` is 0..1). On success, `expectedOutputPublicUrl` is the durable public MP4 URL and `outputUrl` remains the completion-time field.
458
485
 
459
486
  Every publish creates an immutable version snapshot at `versions/<N>/composition.html` and `versions/<N>/composition.json`.
460
487
 
488
+ The Web UI **Render** button and devcli render both use this same endpoint. The fast `202` response includes the deterministic `expectedOutputPublicUrl` so a caller can store or pass along the final public S3 URL before the render has completed, then poll by `renderId` until `status` settles.
489
+
461
490
  ## Approve a finished post
462
491
 
463
492
  A render produces a bare MP4 URL. **Approving** wraps that MP4 (plus caption, title, pinned comment, and any carousel slides) into a shareable preview page — the phone-mockup page a human opens to review and copy the post.
@@ -600,16 +629,73 @@ For agents that operate Vidfarm headlessly, the typical loop is:
600
629
  3. `PATCH /api/v1/compositions/:forkId/composition.json` → update metadata
601
630
  4. `POST /api/v1/compositions/:forkId/render { tracer }` → render
602
631
  5. Poll `GET /api/v1/compositions/:forkId/renders/:renderId` until `SUCCEEDED`
603
- 6. Use `outputUrl` in the next stage
632
+ 6. Use `expectedOutputPublicUrl` in the next stage when you need the stable public URL immediately; otherwise fall back to `outputUrl` after completion
604
633
 
605
634
  Send a stable `tracer` on export so retries are traceable and filterable in job history — but note that submission is **not idempotent**: every POST creates a new job (and a new charge) even with the same tracer. Do not blind-retry expensive submissions; check the render status first.
606
635
 
636
+ ## Scripting mode
637
+
638
+ **Scripting mode** is the recommended posture for repeatable template automation. Use it when a director wants to take a template they like, agree on a base fork, and then drive bulk or one-off edits entirely through REST or `vidfarm api` from a script, Lambda, or local machine.
639
+
640
+ Contract:
641
+
642
+ 1. Pick the template once, then fork once and treat the resulting `forkId` as the stable base.
643
+ 2. Read the working state with `GET /api/v1/compositions/:forkId/composition.html` and `GET /api/v1/compositions/:forkId/composition.json`.
644
+ 3. Modify the composition deterministically in your script.
645
+ 4. Write back with `PUT /api/v1/compositions/:forkId/composition.html` and `PATCH /api/v1/compositions/:forkId/composition.json`.
646
+ 5. Snapshot with `POST /api/v1/compositions/:forkId/versions` before or after render, depending on whether you want the version to capture the exact render input or the post-render state.
647
+ 6. Render with `POST /api/v1/compositions/:forkId/render`, choosing local or cloud based on the host and runtime.
648
+
649
+ Devcli shortcut:
650
+
651
+ ```bash
652
+ vidfarm render "$FORK_ID" --dir ./work --tracer "batch-2026-07-09-row-42" --wait
653
+ ```
654
+
655
+ That command validates `./work/composition.html`, shallow-patches `./work/composition.json` if present, then submits the same `POST /api/v1/compositions/:forkId/render` call used by the Web UI. `--dir` may also point directly at `composition.html`.
656
+
657
+ Best practices:
658
+
659
+ - Prefer one canonical base fork per automation run, then branch from that fork if you need variants.
660
+ - Treat `composition.json` as a shallow merged metadata document. The server merges the posted JSON object into the existing object; it is not RFC 6902 JSON Patch.
661
+ - Keep edits idempotent in your script. Re-run the script against the same fork only if it computes the same desired state.
662
+ - Use a stable `tracer` on render jobs so logs and job history stay searchable.
663
+ - Use the same REST sequence in Lambda, CI, or a local shell. The only thing that changes is the host and auth.
664
+
665
+ Canonical shell shape:
666
+
667
+ ```bash
668
+ BASE_TEMPLATE_ID="template_..."
669
+ FORK_ID="$(vidfarm api POST /api/v1/compositions --data "{\"template_id\":\"${BASE_TEMPLATE_ID}\"}" --json | jq -r '.fork_id')"
670
+
671
+ vidfarm api GET "/api/v1/compositions/${FORK_ID}/composition.html" --raw > /tmp/composition.html
672
+ vidfarm api GET "/api/v1/compositions/${FORK_ID}/composition.json" --raw > /tmp/composition.json
673
+
674
+ # edit /tmp/composition.html and /tmp/composition.json in your script
675
+
676
+ vidfarm api PUT "/api/v1/compositions/${FORK_ID}/composition.html" --body-file /tmp/composition.html --content-type "text/html; charset=utf-8"
677
+ vidfarm api PATCH "/api/v1/compositions/${FORK_ID}/composition.json" --data-file /tmp/composition.json
678
+ vidfarm api POST "/api/v1/compositions/${FORK_ID}/versions" --data '{"message":"scripting mode snapshot"}'
679
+ vidfarm api POST "/api/v1/compositions/${FORK_ID}/render" --data '{"tracer":"scripting-mode"}'
680
+ ```
681
+
682
+ If you need many variants, keep the base fork fixed and fan out by cloning that fork or by reapplying the same edit function to multiple fork ids. Use the raw REST routes directly when you want maximum control; use `vidfarm-devcli` when you want auth, polling, and file helpers without writing the plumbing yourself.
683
+
607
684
  ## `vidfarm-devcli` — full command surface
608
685
 
609
- `@mevdragon/vidfarm-devcli` wraps the **entire director REST flow** as CLI commands. It is a thin shell over the REST API, not a second implementation: **every named command maps 1:1 to exactly one REST route**, and all it adds is fork auto-resolution, the `vidfarm-api-key` header, prod frontend links in the output, render-poll waiting, and the multi-step upload/download flows. Auth via `--api-key <key>` or `VIDFARM_API_KEY`.
686
+ `@mevdragon/vidfarm-devcli` wraps the **entire director REST flow** as CLI commands. It is a thin shell over the REST API, not a second implementation: most named commands map 1:1 to one REST route, and the file-backed commands compose the documented routes for upload/download + render-polling. Auth via `--api-key <key>` or `VIDFARM_API_KEY`.
610
687
 
611
688
  **Raw REST vs devcli — your choice.** If you want full control, call the REST API directly (or use the raw passthrough `vidfarm api <METHOD> <path> [--data <json>] [--query k=v]`). If you want ergonomics + openable frontend URLs, use the named commands. They are interchangeable — pick per call.
612
689
 
690
+ **Scripting mode recommendation.** For automation, prefer:
691
+
692
+ - `vidfarm api` for the actual REST calls
693
+ - `--data-file` for file-backed payloads
694
+ - a single pinned base fork id per template family
695
+ - `POST /api/v1/compositions/:forkId/versions` as the versioning boundary
696
+ - `POST /api/v1/compositions/:forkId/render` with a stable `tracer`, or `vidfarm render <forkId> --dir ./work --wait` for the same composed flow
697
+ - `render_target: "cloud"` when you want cloud handoff from a local `vidfarm serve` box, or the default local renderer when you want free in-process output
698
+
613
699
  | Command | REST route | Flow step |
614
700
  |---|---|---|
615
701
  | `vidfarm discover [query]` | `GET /discover/feed[?q=]` | browse/search templates |
@@ -636,7 +722,7 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
636
722
  | `vidfarm remove-video-captions <forkId>` (alias: `ghostcut`) | `GET .../compositions/:forkId/remove-video-captions` | subtitle-removal status |
637
723
  | `vidfarm snapshot <forkId>` | `POST .../compositions/:forkId/versions` | save composition fork version |
638
724
  | `vidfarm versions <forkId>` | `GET .../compositions/:forkId/versions` | list versions |
639
- | `vidfarm render <forkId> [--wait]` | `POST .../compositions/:forkId/render` | render video to MP4 |
725
+ | `vidfarm render <forkId> [--dir <dir\|composition.html>] [--wait] [--target local\|cloud]` | `PUT/PATCH working files, then POST .../compositions/:forkId/render` | script-friendly render to MP4; no `--dir` uses the fork's current working state |
640
726
  | `vidfarm render-status <forkId> <renderId>` | `GET .../compositions/:forkId/renders/:renderId` | poll a render |
641
727
  | `vidfarm visibility <forkId> <private\|public>` | `PATCH .../compositions/:forkId/visibility` | set visibility |
642
728
  | `vidfarm clone <forkId>` | `POST .../compositions/:forkId/clone` | clone a fork |
package/SKILL.platform.md CHANGED
@@ -160,7 +160,7 @@ A composition is a single HTML document plus a JSON sidecar.
160
160
  "summary": "...",
161
161
  "scenes": [{ "slug": "hook_reveal", "start": 0, "duration": 1.5, "label": "...", "viral_note": "..." }],
162
162
  "captions": [{ "slug": "caption_1", "text": "...", "start": 0, "duration": 1.5, "x": 5, "y": 60 }],
163
- "viralDna": { "hook": "...", "retention": "...", "payoff": "...", "preserve": [...], "avoid": [...] },
163
+ "viralDna": { "hook": "...", "retention": "...", "payoff": "...", "emotional_punch": { "core_emotion": "...", "tone": "...", "mechanism": "...", "humor": "...", "delivery": "...", "preserve": [...] }, "preserve": [...], "avoid": [...] },
164
164
  "width": 1080,
165
165
  "height": 1920,
166
166
  "durationSeconds": 10.5,
@@ -205,8 +205,8 @@ Publish flow:
205
205
  3. API resolves the caller's provider keys (preflight check for required models)
206
206
  4. API creates a `job` record and invokes the `hyperframes_render` primitive Lambda
207
207
  5. The renderer runs HyperFrames: on the deployed host it stages the composition and dispatches to the shared HyperFrames render state machine (chunked Lambda fan-out); on a local `vidfarm serve` box with no cloud stack it renders in-process (headless Chromium → ffmpeg) for free. See `HyperframesService` in `src/services/hyperframes.ts`.
208
- 6. On success, the primitive returns `{ primary_file_url: "s3://.../output.mp4" }`, the API creates a `ForkVersionRecord` with `reason: "publish"`, writes the snapshot to `versions/<N>/`, and updates the fork's `latestVersion` and `currentRenderJobId`
209
- 7. Client polls `GET /api/v1/compositions/:forkId/renders/:renderId` for `{ status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, outputUrl, cost }` (see `adaptJobToPublishStatus` in `src/app.ts`)
208
+ 6. On success, the primitive returns `{ primary_file_url: "s3://.../output.mp4" }`, the API creates a `ForkVersionRecord` with `reason: "publish"`, writes the snapshot to `versions/<N>/`, updates the fork's `latestVersion` and `currentRenderJobId`, and now also exposes the deterministic public artifact URL immediately as `expected_output_public_url`
209
+ 7. Client polls `GET /api/v1/compositions/:forkId/renders/:renderId` for `{ status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, expectedOutputPublicUrl, outputUrl, cost }` (see `adaptJobToPublishStatus` in `src/app.ts`); `expectedOutputPublicUrl` is the stable public S3 URL, while `outputUrl` remains the completion-time field
210
210
 
211
211
  The HyperFrames render state machine is **shared across all templates and forks** — it's an infrastructure primitive, not a per-template deployment. Its ARN/bucket are passed to the Vidfarm stack as env vars (`HYPERFRAMES_RENDER_*`). Remotion has been fully removed.
212
212