@mevdragon/vidfarm-devcli 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +166 -0
  2. package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
  3. package/.agents/skills/vidfarm-media/SKILL.md +43 -4
  4. package/SKILL.director.md +190 -18
  5. package/SKILL.platform.md +8 -4
  6. package/demo/dist/app.css +1 -1
  7. package/demo/dist/app.js +77 -75
  8. package/demo/dist/favicon.ico +0 -0
  9. package/dist/src/app.js +3031 -300
  10. package/dist/src/cli.js +1549 -69
  11. package/dist/src/config.js +7 -0
  12. package/dist/src/devcli/clip-store.js +29 -2
  13. package/dist/src/devcli/clips.js +55 -9
  14. package/dist/src/devcli/composition-edit.js +658 -8
  15. package/dist/src/devcli/local-backend.js +123 -0
  16. package/dist/src/devcli/migrate-local.js +140 -0
  17. package/dist/src/devcli/sync.js +311 -0
  18. package/dist/src/devcli/timeline-edit.js +490 -0
  19. package/dist/src/editor-chat.js +56 -17
  20. package/dist/src/frontend/discover-client.js +130 -0
  21. package/dist/src/frontend/discover-store.js +23 -0
  22. package/dist/src/frontend/file-directory.js +995 -0
  23. package/dist/src/frontend/homepage-view.js +3 -3
  24. package/dist/src/frontend/template-editor-chat.js +28 -22
  25. package/dist/src/landing-page.js +24 -7
  26. package/dist/src/page-shell.js +26 -2
  27. package/dist/src/primitive-registry.js +409 -4
  28. package/dist/src/reskin/agency-page.js +1 -1
  29. package/dist/src/reskin/calendar-page.js +2 -1
  30. package/dist/src/reskin/chat-page.js +420 -85
  31. package/dist/src/reskin/discover-page.js +731 -39
  32. package/dist/src/reskin/document.js +1311 -387
  33. package/dist/src/reskin/help-page.js +1 -0
  34. package/dist/src/reskin/inpaint-clipper-page.js +649 -0
  35. package/dist/src/reskin/inpaint-page.js +2459 -446
  36. package/dist/src/reskin/inpaint-video-page.js +1339 -0
  37. package/dist/src/reskin/library-page.js +1168 -228
  38. package/dist/src/reskin/login-page.js +6 -6
  39. package/dist/src/reskin/pricing-page.js +2 -0
  40. package/dist/src/reskin/settings-page.js +55 -10
  41. package/dist/src/reskin/theme.js +365 -20
  42. package/dist/src/services/billing.js +4 -0
  43. package/dist/src/services/clip-curation/gemini.js +5 -0
  44. package/dist/src/services/clip-curation/hunt.js +81 -1
  45. package/dist/src/services/clip-curation/index.js +2 -1
  46. package/dist/src/services/clip-curation/local-agent.js +4 -3
  47. package/dist/src/services/clip-curation/media-select.js +85 -0
  48. package/dist/src/services/clip-curation/query.js +5 -1
  49. package/dist/src/services/clip-curation/refine.js +50 -20
  50. package/dist/src/services/clip-curation/scan.js +10 -3
  51. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  52. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  53. package/dist/src/services/clip-records.js +42 -1
  54. package/dist/src/services/clip-search.js +43 -13
  55. package/dist/src/services/file-directory.js +117 -0
  56. package/dist/src/services/hyperframes.js +283 -3
  57. package/dist/src/services/serverless-records.js +43 -0
  58. package/dist/src/services/storage.js +47 -2
  59. package/dist/src/services/upstream.js +5 -5
  60. package/dist/src/template-editor-shell.js +16 -2
  61. package/package.json +1 -1
  62. package/public/assets/discover-client-app.js +1 -0
  63. package/public/assets/file-directory-app.js +2 -0
  64. package/public/assets/homepage-client-app.js +12 -12
  65. package/public/assets/page-runtime-client-app.js +24 -24
  66. package/public/assets/placeholders/scene-placeholder.png +0 -0
  67. package/src/assets/favicon.ico +0 -0
  68. package/src/assets/logo-vidfarm.png +0 -0
package/SKILL.director.md CHANGED
@@ -115,11 +115,40 @@ Content-Type: application/json
115
115
  Take the returned `fork_id` and open the Trackpad Editor at:
116
116
 
117
117
  ```
118
- /editor/<templateId>?fork=<forkId>
118
+ /editor/<templateId>/fork/<forkId>
119
119
  ```
120
120
 
121
121
  The editor loads `composition.html` and `composition.json` from the fork's working state.
122
122
 
123
+ ### New fork from an existing fork (branch this copy, or start fresh)
124
+
125
+ Once you already hold a fork, spawn ANOTHER editable copy — a branch — with one unified endpoint. This is what the editor's ⋯ menu **New fork** modal and the devcli `clone --from` command both call:
126
+
127
+ ```
128
+ POST /api/v1/compositions/:forkId/fork
129
+ Content-Type: application/json
130
+
131
+ { "from": "current" | "default", "parent_version": <optional N>, "title": "Optional" }
132
+ ```
133
+
134
+ - `from: "current"` (default) — branch a new private fork from **this** fork's working copy; all current edits carry over. Optionally pin `parent_version` to clone a saved version instead of the live working state. (The legacy `POST /:forkId/clone` route is an alias for exactly this.)
135
+ - `from: "default"` — start a **fresh** fork from the template's canonical default composition, discarding the current fork's edits (they stay in the fork you branched from).
136
+
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
+
139
+ ## Agentic editing: the three axes (SWAP ↔ REPLACE)
140
+
141
+ 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:
142
+
143
+ - **SWAP (light, cheap, common)** — keep the structure, change content **in place**: rewrite a caption/text layer (`set_layer_text` / `set_captions`), swap one clip's media for another of the same kind keeping its timing + geometry (`set_layer_media`), or re-voice narration (`vidfarm speech regenerate` / `/audio/regenerate-speech` → mute the old span, `place`/`add_layer` the new audio). Many jobs are **only** a text swap — a two-minute change; just do it.
144
+ - **REPLACE (heavy)** — throw out that axis's content and rebuild it: restructure the scenes outright (new clip count / new beats via `remove_layer` + `add_layer`/`generate_layer`, or one `replace_composition_html`), lay a brand-new audio bed, or rewrite every caption. The **hardest** end is a full **re-theme** where the ONLY thing preserved is the **viral DNA** (hook shape, pacing, scene-count rhythm, transition/caption style) while every scene, every word, and the audio are all replaced for the director's new subject.
145
+
146
+ Name the plan back in these terms ("I'll SWAP the captions and REPLACE the scenes"), then execute axis by axis. Be **proactive at the heavy end** — carry the whole transformation, don't wait to be micro-managed one layer at a time.
147
+
148
+ **Fuel a scene REPLACE with raw clips, not expensive AI video.** A heavy scenes-axis replace needs footage; sources in cost order: (1) the director's own library — search `/raws` and `/files` (`vidfarm raws search …`, `vidfarm files --search …` / `browse_files`); (2) **HUNT new raws** out of a long-form source (podcast/VOD/webinar or any YouTube/TikTok/IG/X URL) — `vidfarm raws scan <src> --prompt "<what the new scenes need>" --aspect <canvas> [--duration N --no-text --range …]` (local-first, free compute) or the async `POST /clips/scan` / `/raws/scan`; then reuse the picks (`set_layer_media` / `vidfarm set-media` to swap in place, `add_layer`/`vidfarm place` for net-new scenes); (3) `generate_layer` / `vidfarm generate` AI generation — the **expensive last resort**, only for scenes no real clip can cover. When a big scene re-work is asked for but no footage is given, **ask for a source to hunt (or point at the raws library) before AI-generating** — see [Raws](#raws-long-form--short-form-raws) and [Generate AI media …](#generate-ai-media-and-drop-it-on-the-timeline).
149
+
150
+ **Audio is natively multi-track — overlay narration + music + SFX, each at its own volume.** A composition mixes UNLIMITED simultaneous `<audio>` layers; each sits on its own `data-track-index` and carries its own `data-volume` (0–2, default 1), and the runtime mixes them with per-track volume honored identically in the preview and the exported MP4 (a real ffmpeg `amix` of every audio layer at render). So you never need a pre-mixed file — lay **narration/voiceover at ~1.0 on one track, a music bed at ~0.1–0.2 on a separate track, and SFX on their own tracks**, each via `add_layer kind=audio` (web) / `vidfarm place --kind audio --volume …` (devcli), tuning levels later with the Inspector's Volume slider or `set_layer_media` (`volume`, `muted`). **The key move when recreating a template whose original baked music + narration into ONE audio track: rebuild it as TWO independent tracks** — a fresh narration track (`/audio/speech`, or same-voice reword via `/audio/regenerate-speech` / `vidfarm speech regenerate`) at ~1.0 and a separate real music track at ~0.1–0.2 — then mute/remove the original combined source-audio layer. This gives the director independent voice and music volume, and works around AI TTS being unable to emit narration+music in one file: you compose the mix on the timeline. Honesty: you can't un-mix / stem-separate the original's baked audio — the two tracks are a fresh narration track **plus** a real music file (owned / user-provided / `browse_files`), never a faked or duplicated voice layer.
151
+
123
152
  ## Edit in the Trackpad Editor
124
153
 
125
154
  The editor is a full timeline surface:
@@ -127,7 +156,7 @@ The editor is a full timeline surface:
127
156
  - **Timeline** — multi-track, drag/trim/split clips, group/ungroup layers
128
157
  - **Inspector panel** — per-layer property editor (text, color, font, position, size, media source, playback start, volume). Image layers also expose an **Animate (Ken Burns)** dropdown — a slow pan/zoom over the clip's full duration (see below). Text/caption layers expose an **Animated captions** preset picker — word-by-word TikTok/CapCut caption styles (see below) — plus active-word color, highlight color, and uppercase controls.
129
158
  - **Canvas frame overlay** — 8-handle resize + WYSIWYG positioning
130
- - **Chat panel** — AI assistant that can programmatically edit the composition (`add_layer`, `remove_layer`, `set_layer_timing/visual/style/media/text`, `set_captions`, `group_layers`, `generate_layer`, `replace_composition_html`, ...). The assistant uses the caller's saved provider keys. **`generate_layer`** is the one-shot "generate AI media and place it": it submits the `/videos/generate` or `/images/generate` primitive job, drops a placeholder clip into the target slot (fill a blank gap, replace a scene, or overlay), and auto-swaps in the finished media when the job settles — no manual polling. In-flight generations surface in `editor_context.pending_generations`.
159
+ - **Chat panel** — AI assistant that can programmatically edit the composition (`add_layer`, `remove_layer`, `set_layer_timing/visual/style/media/text`, `set_layer_keyframes` — script-free CSS keyframe motion on one layer — `set_captions`, `group_layers`, `nudge_layers`, `ripple_edit`, `trim_layer`, `set_layer_zindex`, `generate_layer`, `replace_composition_html`, ...). The assistant uses the caller's saved provider keys, and reads an enriched `editor_context` (per-layer transitions / Ken Burns / custom-animation state; `recent_action_results` now reports successes as well as failures). See "Script-free keyframe motion & timeline verbs" below for the newer motion/timeline actions. **`generate_layer`** is the one-shot "generate AI media and place it": it submits the `/videos/generate` or `/images/generate` primitive job, drops a placeholder clip into the target slot (fill a blank gap, replace a scene, or overlay), and auto-swaps in the finished media when the job settles — no manual polling. In-flight generations surface in `editor_context.pending_generations`.
131
160
  - **Auto-save** — every commit writes composition.html + composition.json to the working state. No explicit save button.
132
161
 
133
162
  The editor persists to:
@@ -208,6 +237,34 @@ Use it whenever you need to know what the video says or shows: writing/translati
208
237
  - **Editor chat (frontend AI)** exposes this as the optional `video_context` tool — the copilot calls it on demand.
209
238
  - **Desktop agents (Claude Code / Codex)**: fetch the `video-context.json` route directly (e.g. `vidfarm api GET /api/v1/compositions/<forkId>/video-context.json`) to ground edits in what the video says and shows.
210
239
 
240
+ ## Editor harness (how to edit like this)
241
+
242
+ `GET /api/v1/compositions/:forkId/editor-harness.json` — read-only, non-billing. The decompose pass's **technical editing direction**: *how* to edit so the result recreates the source's STYLE. It's the "how to edit like this" companion to the viral DNA's "why it works". Read it before any re-theme, multi-scene rebuild, or restyle.
243
+
244
+ ```
245
+ {
246
+ "ok": true,
247
+ "status": "ready" | "empty" | "none",
248
+ "harness": {
249
+ "one_liner": "Fast first-person VO over gameplay b-roll, karaoke captions, hard cuts.",
250
+ "format": "reddit_story_gameplay",
251
+ "aspect_ratio": "9:16",
252
+ "pacing": { "cut_rhythm": "fast", "avg_scene_seconds": 2.4, "cuts_per_10s": 4, "energy_curve": "front-loaded hook then steady" },
253
+ "typography": { "caption_style": "karaoke", "placement": "center-lower", "font_character": "bold condensed all-caps", "emphasis": "active-word yellow", "text_density": "heavy" },
254
+ "broll": { "reliance": "heavy", "sourcing": "screen-recording", "shot_kinds": ["b_roll"], "cadence": "new visual ~every 2s" },
255
+ "transitions": { "default": "cut", "intro": "fade", "outro": "none", "usage": "hard cuts only" },
256
+ "audio": { "voiceover": "first-person narration", "music": "upbeat lofi bed, low", "sfx": "whoosh on cuts", "captions_from": "voiceover" },
257
+ "scenes": [{ "role": "hook", "importance": "critical", "must_keep": true, "note": "...", "edit_bias": "swap subject, keep timing+caption cadence" }],
258
+ "important_scenes": ["..."], "editing_bias": ["..."], "do": ["..."], "dont": ["..."]
259
+ }
260
+ }
261
+ ```
262
+
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.
264
+
265
+ - **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
+ - **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.
267
+
211
268
  ## Raws (long-form → short-form raws)
212
269
 
213
270
  Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/X URL, or an upload) into a library of tagged, searchable **raws**. This is the `/raws` surface — browse it at `https://vidfarm.cc/library/raws` (the Library page's "Approved / Raws" tabs).
@@ -240,6 +297,7 @@ Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/
240
297
  - **Billing** — **AWS compute only** (`clip_scan_lambda` GB-seconds + Step Functions transitions, a fraction of a cent for typical hunts; the 202 response includes a `compute_estimate`). The AI tagging/refine runs on **your saved provider key (BYOK)** and is never wallet-billed. Submission is wallet-gated (402 when empty).
241
298
  - **The original stays temporary** — URL-ingested and uploaded sources live ONLY in your temp folder (`users/…/temporary/clip-sources/…`) with a hard **30-day TTL** (auto-deleted from S3 + the temp-file list). The hunted raws themselves are durable.
242
299
  - **Captions rule** — "no captions / no on-screen text" hunts are handled by scene SELECTION (the refine pass drops text-heavy scenes). **Never run GhostCut caption removal on a long-form source** (it is hard-capped at ~15 minutes); to actually erase burned-in text, apply `POST /api/v1/primitives/videos/remove-captions` to individual FINISHED raws afterwards.
300
+ - **Clip an EXACT subrange (no AI hunt)** — when you want one precise hand-picked in/out rather than an open-ended hunt, use the **Clipper**: the web page `https://vidfarm.cc/tools/clipper` (previews the source straight from its URL, HH:MM:SS:NNNN in/out, live length estimate, multi-clip session grouped by tracer), the endpoint `POST /raws/clip-range` `{ source_url|preview_url|temp_file_id, start_sec, end_sec, tracer?, folder_path?, name? }` (trims exactly that window into one raw — compute-only, no AI/key), or the devcli twin `vidfarm clipper <video-url|file> --start <t> --end <t> [--tracer NAME --folder NAME --name TXT]`. The Library "Import source" button opens the Clipper; each save lands in `/raws/<folder>` (defaults to the tracer). Reach for `/raws/scan` for multi-clip hunts, `/raws/clip-range` for surgical single cuts. (The masked-image and time-scoped-video editors live alongside it at `/tools/image` and `/tools/video`.)
243
301
 
244
302
  **devcli (local-first — this is the default way to hunt on your own machine):**
245
303
 
@@ -266,14 +324,14 @@ Both surfaces can generate a NEW AI video/image (optionally conditioned on refer
266
324
 
267
325
  **Cost gate (see the Cost spectrum section):** AI **video** generation is the expensive end of the spectrum ($1–$10+/video) — **ask the director's permission before generating video**, and prefer reusing existing footage (My Files / their computer / a web search) or filming when it would do. **Image** generation is cheap — use it freely without asking. When a director wants specificity that only AI video can deliver and their budget allows, this is the right tool; just confirm first.
268
326
 
269
- - **Editor web copilot**: use the `generate_layer` editor_action — it does the whole flow (submit job → placeholder clip → auto-swap on completion) and reads `pending_generations` from `editor_context`. Set `media_type`, `prompt`, `aspect_ratio` (match the canvas), and `intent` (`fill_gap` with `start`/`duration`, `replace_layer` with `replace_layer_key`, or `add`). For character consistency pass a cast `reference_url` — or the character's `character_sprite_card.png` viewUrl from My Files (see "Character consistency") — in `input_references` (video) / `prompt_attachments` (image).
327
+ - **Editor web copilot**: use the `generate_layer` editor_action — it does the whole flow (submit job → placeholder clip → auto-swap on completion) and reads `pending_generations` from `editor_context`. Set `media_type`, `prompt`, `aspect_ratio` (match the canvas), and `intent` (`fill_gap` with `start`/`duration`, `replace_layer` with `replace_layer_key`, or `add`). For character consistency pass a cast `reference_url` — or the character's `character_sprite_card.png` viewUrl from `/files/characters/<slug>/` (see "Recurring characters are first-class") — in `input_references` (video) / `prompt_attachments` (image).
270
328
 
271
329
  - **Desktop agents (Claude Code / Codex)** — two devcli commands:
272
330
  ```bash
273
331
  # 1) Sync the fork to disk and see where the blank space and scenes are
274
332
  vidfarm pull <forkId> --dir ./work
275
333
  # → prints timeline gaps (e.g. 4-7s) and scene/layer keys, canvas aspect ratio,
276
- # and whether video-context.json / cast.json are present for grounding.
334
+ # and whether video-context.json / cast.json / editor-harness.json are present for grounding.
277
335
 
278
336
  # 2) Generate + place in one call (fill a gap)
279
337
  vidfarm generate video --prompt "her walking through the door" \
@@ -290,6 +348,20 @@ Both surfaces can generate a NEW AI video/image (optionally conditioned on refer
290
348
  ```
291
349
  `--ref @localfile` (or a bare path) auto-uploads the image and uses its durable URL as a reference. `generate` prints the media URL (drop `--place` to just get the URL); `place` can be run separately on any media URL (`vidfarm place ./work --src <url> --at <time>|--replace <layer_key>`). Placement produces the exact same clip markup the browser editor makes, so a human can finish it in the Trackpad Editor.
292
350
 
351
+ ### Fitting imperfect-dimension media (aspect mismatch)
352
+
353
+ A clip/image whose **native aspect differs from the canvas** — a 16:9 landscape source on a 9:16 vertical frame is the classic case, also a tall screenshot on a wide frame, a square logo — will NOT look good on the default `object-fit:cover`, which fills the frame and **silently center-crops** the overflow (landscape → loses left/right, often cutting the subject in half; portrait → loses top/bottom). Decide the fit deliberately:
354
+
355
+ - **`object_fit:"cover"`** (default) fills + crops — right for most social footage **when paired with a subject-aware `object_position`**.
356
+ - **`object_position`** aims WHERE the cover-crop lands: a keyword (`left`, `right`, `top`, `bottom`, `"top left"`, …) or a **percentage pair** (`"30% 50%"` = 30% from the left, 50% down). Landscape clip with the subject on the left → `"left"`/`"25% 50%"`; tall screenshot with key text up top → `"top"`.
357
+ - **`object_fit:"contain"`** shows the WHOLE media with black letterbox/pillarbox bars — use ONLY when nothing may be cropped (an infographic, a whole screenshot, a chart, a logo); a 16:9 source full-canvas on a 9:16 frame reads unfinished in `contain`.
358
+ - **Blurred-letterbox band** for a whole landscape clip without ugly bars: place it as a centered band (`width:100, height:~34, y:~33` on 9:16) with `contain`, plus a DUPLICATE full-canvas copy on a lower track behind it at `cover` + heavy blur.
359
+ - Never `object_fit:"fill"` (stretch/distort) unless intentional. You usually know a clip's aspect from how you sourced it (a hunted raw's `--aspect`, a generated clip's `--aspect-ratio`); if you truly need exact dims, `vidfarm probe` / `GET /videos/probe`. When you REPLACE a full-canvas scene, keep it full canvas + `cover` + a focal `object_position` rather than shrinking; when you GENERATE, request the canvas aspect so no crop is needed.
360
+
361
+ Two surfaces:
362
+ - **Editor web/serve copilot** — `editor_action` `action_type=set_layer_media` with `object_fit` and `object_position` (both also seedable on `add_layer`/`generate_layer`). `editor_context.layers[]` reports each clip's current `object_fit`/`object_position` so you can read before re-cropping.
363
+ - **Desktop agents (devcli)** — `vidfarm place ./work --src <url> --object-fit cover --object-position "25% 50%"` (also on `--replace <layer_key>`).
364
+
293
365
  ### Ken Burns — animate still images (slow pan/zoom)
294
366
 
295
367
  Still images can carry a **Ken Burns effect**: a slow pan or zoom that runs across the clip's full duration, in the editor preview and both render paths identically. This is what makes a slideshow of stills feel like motion footage. It is a first-class layer property — the image layer's `<img>` gets `data-kenburns="<preset>"`; there is no separate keyframe authoring.
@@ -319,6 +391,36 @@ Four surfaces:
319
391
 
320
392
  The plain STT primitive grew the same hook: `POST /api/v1/primitives/audio/transcribe` accepts `word_timestamps: true` to attach word-level timings to each transcript segment (real on an OpenAI key via whisper-1; other providers stay segment-level).
321
393
 
394
+ ### Script-free keyframe motion & timeline verbs
395
+
396
+ Beyond the Ken Burns / transition / animated-caption presets, the copilot can hand-author motion and re-time layers directly (all `editor_action` verbs; all preview AND render identically):
397
+
398
+ - **`set_layer_keyframes`** — author a script-free CSS `@keyframes` animation on ONE layer: `opacity`, `translate_x` / `translate_y` (% of the layer's own box), `scale`, and `rotate` (deg), keyed across the clip. Params: `layer_key`, `keyframes: [{ offset 0..1, opacity?, translate_x?, translate_y?, scale?, rotate? }]`, optional `keyframe_easing`, optional `keyframe_duration` (defaults to the clip's duration). This is the durable way to hand-craft motion beyond the Ken Burns / transition / caption preset vocabulary.
399
+ - **`nudge_layers`** — relative timeline move: `layer_key` (or `layer_keys`) + `delta_start` (seconds) and/or `delta_track` (lanes). Group-aware.
400
+ - **`ripple_edit`** — insert (`delta_start > 0`) or close (`delta_start < 0`) time at `at_time`, shifting every downstream clip.
401
+ - **`trim_layer`** — move one edge: `edge=start|end` + `to_time` (seconds). A left trim also advances the media in-point for video/audio.
402
+ - **`set_layer_zindex`** — restack: `z_order=front|back|forward|backward` (stacking == track index) or an explicit `track`.
403
+
404
+ **Named layer-edit verbs (web `editor_action` ↔ devcli command parity).** Every fine edit has a devcli twin that mutates a pulled `composition.html` on disk (same `data-*` contract, so a human can finish it in the editor):
405
+
406
+ | Intent | Web `editor_action` | devcli command |
407
+ |---|---|---|
408
+ | Rewrite a text/caption layer's words | `set_layer_text` | `vidfarm set-text <dir> --layer <k> --text "…"` |
409
+ | Restyle text/color/typography (+ `line_height`, `letter_spacing`, `opacity`) | `set_layer_style` | `vidfarm set-style <dir> --layer <k> [--color --background --background-style --font-family --font-weight --italic --underline --text-align --font-size --border-radius --line-height --letter-spacing --opacity]` |
410
+ | Geometry + constant opacity | `set_layer_visual` | `vidfarm set-visual <dir> --layer <k> [--x --y --width --height --font-size --border-radius --opacity]` |
411
+ | Replace/edit a media layer **IN PLACE** (src swap + subrange + audio + fit + ken-burns + transitions) | `set_layer_media` | `vidfarm set-media <dir> --layer <k> [--src <url\|/raws/…\|file> --playback-start --source-out --duration --volume --muted --object-fit --object-position --ken-burns --transition --transition-out]` |
412
+ | Set the stable slug/note handle | `set_layer_identity` | `vidfarm set-identity <dir> --layer <k> [--slug --note]` |
413
+ | Clone a layer | `duplicate_layer` | `vidfarm duplicate <dir> --layer <k> [--start --track --new-key]` |
414
+ | Cut a clip in two | `split_layer` | `vidfarm split <dir> --layer <k> --at <sec>` |
415
+ | Set timing directly (absolute) | `set_layer_timing` | `vidfarm retime <dir> --layer <k> [--start --duration --track --playback-start]` |
416
+ | **Canvas / theme** (whole composition) | `set_composition` | `vidfarm set-composition <dir> [--width --height --duration --background]` |
417
+
418
+ `set-media` edits the media layer **in place** — the src swap keeps the same node/key/geometry/timing (the `set_layer_media` twin), so downstream references and the layer's DNA survive. Reach for `place --replace <layer_key>` only when you *want* a fresh clip that inherits the slot's timing+geometry (it deletes the node and mints a new key). Subrange is the web in/out picker in one gesture: `--playback-start 3 --source-out 8` = "use seconds 3–8 of the source" (sets the in-point + a 5s timeline duration). `--opacity` is a **constant** level (a ghosted underlay); a fade over time is `set_layer_keyframes` / `vidfarm keyframes`. `line_height`/`letter_spacing` reach typography the presets can't (condensed all-caps, tight/loose leading). `set-composition` resizes the frame (`--width`/`--height`), retargets total render length (`--duration`), or recolors the canvas (`--background`) — written on the composition root per the hyperframes contract. Every command takes `--json` and writes the file in place.
419
+
420
+ **Web editor = declarative/CSS motion only.** In the web editor, motion is CSS-only. The JS runtime adapters (anime.js, GSAP, Lottie, Three.js, TypeGPU) are a **local-devcli-only** capability — they are stripped from the composition on save, and `replace_composition_html` now **rejects `<script>`-bearing HTML** in the web editor. On the web, author motion with CSS `@keyframes` (via `set_layer_keyframes` or a `<style>` block) plus the preset vocabulary (Ken Burns, transitions, animated captions). Scripted / adapter-driven compositions belong to the desktop `vidfarm serve` (or pulled-fork) flow, which renders them correctly through `vidfarm render`.
421
+
422
+ **Enriched editor context.** The per-layer snapshot the copilot reads now includes each layer's `transition` / `transition_out` / `transition_duration`, its `ken_burns` preset, and `animation` (the custom CSS keyframe name it authored), and `recent_action_results` now reports successes (`ok: true` + a summary) as well as failures — so the model can see the effect of what it just did instead of only what broke. For exact markup it can also `GET /api/v1/compositions/:forkId/composition.html`.
423
+
322
424
  ### Local file paths as media (skip the S3 upload)
323
425
 
324
426
  `vidfarm place --src` (and `approve --video/--media`) accept a **local file path**, not just a URL — so a power user bulk-building compositions from a folder of clips on their own machine never has to upload every asset to Vidfarm storage:
@@ -516,13 +618,18 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
516
618
  | `vidfarm inspiration-rm <id>` | `DELETE /discover/templates/:id` | remove a private one |
517
619
  | `vidfarm inspiration-decompose <id>` | `POST /api/v1/inspirations/:id/decompose` | AI-decompose an inspiration |
518
620
  | `vidfarm fork <template_id>` | `POST /api/v1/compositions` | fork a template |
519
- | `vidfarm pull <forkId> [--dir <p>]` | `GET .../compositions/:forkId/{composition.html,json,video-context.json,cast.json}` | sync a fork to disk + print gaps/scene keys |
621
+ | `vidfarm pull <forkId> [--dir <p>]` | `GET .../compositions/:forkId/{composition.html,json,video-context.json,cast.json,scene-annotations.json,editor-harness.json}` | sync a fork to disk + print gaps/scene keys + grounding (incl. editor-harness style brief) |
520
622
  | `vidfarm generate <image\|video> --prompt "…"` | `POST /api/v1/primitives/{images,videos}/generate` (polls job) | generate AI media → finished URL |
623
+ | `vidfarm inpaint <image> --mask <png> --prompt "…" [--region "label=…"] [--ref …] [--out <f>]` | `POST /api/v1/primitives/images/inpaint` (polls job) | masked image EDIT — replace ONLY the transparent-mask region, keep everything else (devcli twin of the /inpaint page) |
624
+ | `vidfarm create-overlay "<subject>" [--key-color #00FF00] [--aspect-ratio 1:1] [--place <dir>] [--out <f>]` | `POST /api/v1/primitives/images/create-overlay` (polls job) | **Vox-style** transparent OVERLAY — AI image on a forced key-color background, chroma-keyed out in one job → ready-to-composite transparent PNG |
625
+ | `vidfarm remove-background-greenscreen <image> [--key-color #00FF00] [--tolerance 0.3] [--out <f>]` | `POST /api/v1/primitives/images/remove-background-greenscreen` (polls job) | cheap chroma-key removal of a FLAT solid background → transparent PNG (cloud primitive) |
521
626
  | `vidfarm tts "…" [--style "…"] [--voice <v>] [--out <file>]` | (LOCAL-FIRST: your own OPENAI/GEMINI/OPENROUTER_API_KEY → audio file on disk; `--cloud` = `POST /api/v1/primitives/audio/speech` + poll) | text → narration audio with a promptable voice style |
522
627
  | `vidfarm stt <file\|url> [--out <base>] [--no-diarize]` (alias: `transcribe`) | (LOCAL-FIRST: local ffmpeg demux + your own key; `--cloud` = `POST /api/v1/primitives/audio/transcribe` + poll) | video/audio → transcript in BOTH formats: simple subtitles (txt + SRT) and multi-speaker segments (json) |
523
628
  | `vidfarm place <dir> --src <url\|file> [--at\|--replace]` | (edits local composition.html; local files → serve disk store or temp upload) | drop media (URL **or local file**) into a gap / over a scene |
524
629
  | `vidfarm captions generate <dir> [--style <preset>] [--audio <f>\|--srt <f>\|--text "…"]` | (LOCAL-FIRST: STT on your own key — OpenAI = real word timestamps — then edits local composition.html) | transcribe narration → animated word-by-word caption cues |
525
630
  | `vidfarm captions style <dir> --style <preset>` / `captions list` / `captions clear` | (edits local composition.html) | restyle / inspect / remove animated captions |
631
+ | `vidfarm keyframes` / `move`\|`nudge` / `ripple` / `trim` / `restack`\|`zindex` `<dir> …` | (edits local composition.html) | script-free CSS keyframe motion + timeline verbs (see "Script-free keyframe motion & timeline verbs") |
632
+ | `vidfarm set-text` / `set-style` / `set-visual` / `set-identity` / `duplicate` / `split` / `retime` / `set-composition` `<dir> …` | (edits local composition.html) | named layer-edit verbs — devcli twins of the web `set_layer_*` / `set_composition` (opacity, line-height, letter-spacing, canvas resize/duration/background); see parity table above |
526
633
  | `vidfarm decompose <forkId>` | `POST .../compositions/:forkId/auto-decompose` | split source into scenes |
527
634
  | `vidfarm remove-video-captions <forkId>` (alias: `ghostcut`) | `GET .../compositions/:forkId/remove-video-captions` | subtitle-removal status |
528
635
  | `vidfarm snapshot <forkId>` | `POST .../compositions/:forkId/versions` | save composition fork version |
@@ -555,7 +662,7 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
555
662
  | `vidfarm doctor` | (local environment triage) | check ffmpeg/node/keys/agent CLI/poisoned env before debugging anything else |
556
663
  | `vidfarm skills list\|add <name>\|update` | `GET /skill-pack/index.json` · `/skill-pack/:name/*` | install/refresh skill packs (see "Skill packs — import on demand") |
557
664
  | `vidfarm tts "…" --engine local` / `vidfarm stt <file> --engine whisper` | (keyless LOCAL engines: Kokoro-82M TTS, whisper.cpp STT) | narration + word-timestamp transcripts with zero keys and zero accounts |
558
- | `vidfarm remove-background <video\|image>` | (local ONNX matting — free) | transparent-subject media for occlusion captions/cutouts |
665
+ | `vidfarm remove-background <video\|image>` | (local ONNX matting — free) | transparent-subject media for occlusion captions/cutouts (arbitrary/messy background; for a FLAT solid background use `remove-background-greenscreen`) |
559
666
  | `vidfarm capture <url>` | (local headless-Chrome capture) | website screenshots/assets for website-to-video flows |
560
667
  | `vidfarm beats <audio>` | (local beat detection → JSON) | music beat timestamps to sync cuts/transitions/captions |
561
668
 
@@ -576,22 +683,51 @@ Each user has a persistent **My Files** filesystem — their own uploaded videos
576
683
 
577
684
  Every My Files entry carries an optional **`notes`** field — free-form metadata describing what the file *is*, who/what it depicts, and when to use it. Notes are **vector-embedded** on save (same BYOK embedding seam as the raws library: gemini → openai key auto-pick; no key fail-softs to keyword-only), so the library is searchable by *meaning*, not just filename:
578
685
 
579
- - **Annotate** — `vidfarm annotate-file <id|name> --notes "Sprite card for Zara, our mascot: front/side/back views, teal jacket. Use as the reference image whenever generating Zara."` (`PATCH /me/attachments/:id`, body `{ notes }`; empty string clears). Or set notes at write time: `vidfarm put-file zara.png --folder acme/characters/zara --notes "…"`. Web copilot: `browse_files action=annotate` (or `notes` on `action=write`).
686
+ - **Annotate** — `vidfarm annotate-file <id|name> --notes "Sprite card for Zara, our mascot: front/side/back views, teal jacket. Use as the reference image whenever generating Zara."` (`PATCH /me/attachments/:id`, body `{ notes }`; empty string clears). Or set notes at write time: `vidfarm put-file zara.png --folder characters/zara --notes "…"`. Web copilot: `browse_files action=annotate` (or `notes` on `action=write`).
580
687
  - **Search** — `vidfarm files --search "the fox mascot reference sheet"` (`POST /me/attachments/search`, body `{ query, folder_path?, limit? }`) returns ranked hits with `similarity` + `keyword_match`, mirroring `/raws/search`: keyword survivors over name/folder/notes are semantically re-ranked, and when nothing keyword-matches it relaxes to a pure semantic pass. Web copilot: `browse_files action=search`.
581
688
  - **Annotate what you'll want back.** Filenames alone don't survive months of accumulation — any asset a future session should find (character refs, logo variants, recurring backgrounds, key briefs) deserves notes at save time. `list`/`read` responses include each file's `notes`, so a scan of a folder doubles as a manifest of what's in it.
582
689
 
583
- ### Character consistency (recurring characters, mascots, spokespeople)
584
-
585
- When a director has a **recurring character** that must look the same across videos, persist the character to My Files under the offer's folder (e.g. `acme-skincare/characters/zara/`) as a **pair of files**:
586
-
587
- 1. **`about_character.md`** — name, role, personality, physical description (face, build, wardrobe, signature colors), voice/tone, and do/don'ts. This is the text you pull wording from when prompting.
588
- 2. **`character_sprite_card.png`** — ONE reference-sheet image showing the character consistently: full body front/side/back plus a face close-up on a neutral background. If the director has no sprite card, generate one with `POST /api/v1/primitives/images/generate` (image gen is cheap — pass their best character photos as `prompt_attachments`, ask for a "character reference sheet / sprite card" layout), then persist the finished job's URL: `vidfarm download <url> zara.png && vidfarm put-file zara.png --as character_sprite_card.png --folder acme/characters/zara` (web copilot: `browse_files action=write` with `source_url`).
589
-
590
- **Annotate both files** with notes naming the character so `files --search "our mascot"` finds them from any phrasing. From then on, character consistency is mechanical: **always pass the sprite card's `viewUrl` as the reference input** on every generation featuring the character — `prompt_attachments` for image generation/edit, `input_references` for `videos/generate` and the editor's `generate_layer` — and lift the description from `about_character.md` into the prompt. Before generating a recurring character, **search My Files for an existing sprite card first**; never re-imagine a character from memory when a reference exists (that's how characters drift off-model between videos).
690
+ ### Recurring characters are first-class (mascots, spokespeople, avatars)
691
+
692
+ Recurring characters that must look the same across videos have a **dedicated, browsable home**: the **`/files/characters/`** folder, one subfolder per character keyed by a URL-safe **slug** (lowercase, hyphenated) e.g. `/files/characters/zara/`. This is where the editor copilot looks first, and where a director organizes their cast. Each character is a **trio of files** in that folder:
693
+
694
+ 1. **`<character_id>.json`** — the machine-readable **manifest**, the source of truth the agent reads to hold identity. The file is named after the character's **id**, which is `character_` + the slug (slug `zara` → id `character_zara` → file `character_zara.json`). The id already carries the `character_` prefix — don't double it (never `character_character_zara.json`):
695
+ ```json
696
+ {
697
+ "id": "character_zara",
698
+ "slug": "zara",
699
+ "name": "Zara",
700
+ "role": "brand mascot / friendly fox guide",
701
+ "appearance": "anthropomorphic red fox, large amber eyes, cream chest fluff, expressive brows",
702
+ "wardrobe": "teal bomber jacket, white tee, small satchel",
703
+ "palette": ["#E8622C", "#12B8A6", "#FDF6EC"],
704
+ "voice": "warm, upbeat, slightly cheeky; mid-tempo",
705
+ "do": ["keep the teal jacket", "front-lit, soft shadows"],
706
+ "dont": ["never photorealistic human", "no other jacket colors"],
707
+ "sprite_card_path": "/files/characters/zara/character_sprite_card.png",
708
+ "about_path": "/files/characters/zara/character_about.md",
709
+ "created_at": "2026-07-09"
710
+ }
711
+ ```
712
+ 2. **`character_sprite_card.png`** — ONE reference-sheet image showing the character consistently: full body front/side/back plus a face close-up on a neutral background.
713
+ 3. **`character_about.md`** — the prose the manifest summarizes (personality, backstory, do/don'ts), for richer wording when prompting.
714
+
715
+ **Awareness — look before you generate.** When a user refers to "our mascot", "the same character", "the fox", or names a character, **first `browse_files list path='/files/characters'`** (or `search`) to see who exists, then read that character's manifest + about. Never re-imagine a saved character from memory — that's how characters drift off-model between videos.
716
+
717
+ **Consistency is then mechanical.** On every generation featuring the character, **pass the sprite card's `viewUrl` as the reference input** — `prompt_attachments` for image generation/edit, `input_references` for `videos/generate` and the editor's `generate_layer` — and lift wording from the manifest/`character_about.md` into the prompt.
718
+
719
+ **Creating a new character (walkthrough).** If no folder exists yet, guide the director through it and persist as you go:
720
+ 1. Agree on a **name** → derive a **slug** (lowercase, hyphens); the folder is `/files/characters/<slug>/`.
721
+ 2. Gather the description conversationally (appearance, wardrobe, signature colors, personality, voice, do/don'ts) — pull from any reference photos they have.
722
+ 3. **Sprite card** — if they don't already have one, offer to generate it: `POST /api/v1/primitives/images/generate` (image gen is cheap — pass their best photos as `prompt_attachments`, ask for a "character reference sheet / sprite card" layout), then persist the finished URL. devcli: `vidfarm download <url> zara.png && vidfarm put-file zara.png --as character_sprite_card.png --folder characters/zara`. Web copilot: `browse_files action=write` with `source_url` + `file_name='character_sprite_card.png'` + `folder_path='characters/zara'`.
723
+ 4. **Write `character_about.md` and `<character_id>.json`** (e.g. `character_zara.json`) into the same folder (`browse_files action=write` with `content`, or `vidfarm put-file --content`).
724
+ 5. **Annotate all three** with notes naming the character so `files --search "our mascot"` finds them from any phrasing.
725
+
726
+ **Renaming / fixing up.** Character folders and files are renamable in place — `browse_files action=rename` (web copilot) or `vidfarm directory rename <path> <new-name> [--file-id <id>]` (devcli); the explorer UI has a **Rename** action on every file/folder row. If you rename a character's folder, update the `id`, `sprite_card_path`, and `about_path` inside its manifest to match (and rename the `<character_id>.json` file itself).
591
727
 
592
728
  **Assume My Files is multi-offer.** A user often runs more than one product, offer, brand, or region, and namescopes assets into folders accordingly — by product (`acme-skincare/`, `zensleep/`), by offer/campaign (`summer-sale/`), by region (`us/`, `eu/`), by asset type (`logos/`, `ugc-clips/`), or any arbitrary scheme. There is **no fixed layout** — read the `folders` tree first and reason about how this user organized things. Before pulling assets for a task, infer which folder(s) match the product/offer/region the current composition is about (match folder names to the composition title, video context, and what the user said), then scope reads to that folder so you never mix one brand's logo/product-shot/music into another brand's video. If the target offer is ambiguous, ask which product/offer/region (or which folder) this work is for rather than guessing across offers.
593
729
 
594
- In the editor web copilot the same filesystem is exposed via the **`browse_files` tool** (`action=list` / `action=search` / `action=read` / `action=write` / `action=annotate`), so the copilot follows the identical reasoning: search or list to find the right offer's folder, then read an asset — or `write` a text doc (About.md, awareness-levels.md, …) or import a media URL (`source_url`) into that folder, annotating anything worth finding again. The devcli equivalents are `vidfarm files [--search]` / `get-file` / `put-file [--notes]` / `annotate-file`.
730
+ In the editor web copilot the same filesystem is exposed via the **`browse_files` tool** (`action=list` / `action=search` / `action=read` / `action=write` / `action=annotate` / `action=move` / `action=rename`), so the copilot follows the identical reasoning: search or list to find the right offer's folder, then read an asset — or `write` a text doc (About.md, awareness-levels.md, …) or import a media URL (`source_url`) into that folder, annotating anything worth finding again. `action=rename` renames a `/files` or `/temp` file or folder in place (pass `path` + `new_name`, plus `file_id` for a file) — use it to keep character folders and asset names tidy. `browse_files list` defaults to `path='/'` when `path` is omitted, so the `/raws` (hunted raws) and `/temp` (scratch) roots surface alongside the My Files folders instead of being hidden; a `/raws` listing also accepts a `content_type` filter (exact shot-kind — `talking_head`, `b_roll`, `product_shot`, `screen_recording`, …), and every listing paginates via `offset` / `limit`. The devcli equivalents are `vidfarm files [--search]` / `get-file` / `put-file [--notes]` / `annotate-file` / `directory rename`.
595
731
 
596
732
  ## Local dev loop (`vidfarm serve`)
597
733
 
@@ -645,7 +781,7 @@ Import by task:
645
781
  Ground rules:
646
782
 
647
783
  - **Local coding agents** (Claude Code / Codex on a `vidfarm serve` box or a pulled fork) get the FULL packs — install, read, and follow them. Scripted/GSAP compositions authored this way render correctly through `vidfarm render` (local and cloud); note that the **web editor strips `<script>` on save**, so keep compositions that must round-trip through the browser editor declarative (the built-in Ken Burns / transitions / animated-captions vocabulary).
648
- - **The web copilot** never installs packs — it has a `load_skill` tool that reads the same content on demand from the vidfarm mirror. Nothing to do there; it is already wired.
784
+ - **The web copilot** never installs packs — it has a `load_skill` tool that reads the same content on demand from the vidfarm mirror (served from `.agents/skills/`; the `.claude/skills/*` entries are just Claude Code discovery symlinks to the same dirs). It can `load_skill` any hyperframes pack (`hyperframes`, `hyperframes-core`, `hyperframes-animation`, `hyperframes-keyframes`, `hyperframes-creative`, `hyperframes-cli`) plus the workflow packs (`embedded-captions`, `product-launch-video`, `faceless-explainer`, `website-to-video`, `general-video`, `motion-graphics`, `slideshow`, `talking-head-recut`, `vidfarm-media`). Nothing to install; it is already wired. **In the web editor, apply only the CSS / `@keyframes` + declarative-preset half of `hyperframes-animation` / `hyperframes-keyframes`** — their JS-adapter techniques are devcli-only (script is stripped on save).
649
785
  - Vidfarm-managed environments set `HYPERFRAMES_SKIP_SKILLS=1` and `HYPERFRAMES_NO_TELEMETRY=1`, so `npx hyperframes init` never overwrites the vidfarm-scrubbed packs and the bundled CLI never phones home. Do not run `hyperframes auth`, `hyperframes cloud`, `hyperframes publish`, `hyperframes play`, or `hyperframes feedback` — vidfarm's own render/share/telemetry surfaces cover all of them.
650
786
 
651
787
  ## What NOT to do
@@ -673,7 +809,7 @@ The point of onboarding is to build **durable, reusable context** in My Files, n
673
809
  2. **Awareness level** (Eugene Schwartz — problem-aware, solution-unaware, …) → `awareness-levels.md`. If it's genuinely unknown after thinking it through, note that ads for **every** level should be made and tested. Use `brainstorm/awareness_stages`.
674
810
  3. **Persuasive angles** → `persuasive-angles.md`, via `brainstorm/angles`.
675
811
  4. **Hooks** → `ad-hooks.md`, via `brainstorm/hooks`.
676
- 5. **Brand assets & demos** — ask if they have logos/mascots/themes (suggest a `/brand-assets/` folder, e.g. `/brand-assets/logo.png`) or product demos / screen recordings (suggest a `/product-demos/` folder). `browse_files list` / `vidfarm files` first to see what they already uploaded; filenames should be descriptive and every asset worth finding later should get **notes** (`annotate-file` / `browse_files annotate`) so `files --search` works months from now. If they have a recurring character/mascot, set up its `about_character.md` + `character_sprite_card.png` pair now (see "Character consistency").
812
+ 5. **Brand assets & demos** — ask if they have logos/mascots/themes (suggest a `/brand-assets/` folder, e.g. `/brand-assets/logo.png`) or product demos / screen recordings (suggest a `/product-demos/` folder). `browse_files list` / `vidfarm files` first to see what they already uploaded; filenames should be descriptive and every asset worth finding later should get **notes** (`annotate-file` / `browse_files annotate`) so `files --search` works months from now. If they have a recurring character/mascot, set up its `/files/characters/<slug>/` trio now — `<character_id>.json` (e.g. `character_zara.json`) + `character_sprite_card.png` + `character_about.md` (see "Recurring characters are first-class").
677
813
  6. **Budget** — ask roughly what they want to spend per video, and map it to the Cost spectrum (free reuse+local render → pennies for cloud render → ~$1 for some AI scenes → $10+ for heavy AI gen). This sets which approach you default to and whether AI **video** generation is on the table (ask permission before using it; image gen is cheap and fine). Budget can also be revisited per editor project.
678
814
  7. **Recommend & adapt a template** — pair what you now know about the offer against the decomposed template catalog (`GET /discover/feed?q=<offer>`, read each result's `promotions`/`keywords`/`summary`), recommend the best 3-6, then fork and **modify** the winner to fit their offer. Prefer already-decomposed templates so the director skips the ~$0.10 decompose cost.
679
815
 
@@ -700,6 +836,7 @@ Remove the background from any image URL. Result is a transparent PNG/WebP store
700
836
  - Body: `{ "source_image_url": "https://...", "output_format": "webp" | "png" | "jpeg" }` (`output_format` defaults to `webp`)
701
837
  - Response: standard primitive job; poll for completion, then read `image.file_url` / `primary_file_url`
702
838
  - Billing: flat pass-through fee via RapidAPI (`rapidapi_remove_background` cost center)
839
+ - Reach for this only for an **arbitrary photo with a messy background**. If the image already sits on a **flat solid background** (anything you generated on a controlled backdrop), use the far cheaper `image_remove_background_greenscreen` below instead.
703
840
 
704
841
  Example:
705
842
 
@@ -710,6 +847,41 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/images/remove-background" \
710
847
  -d '{"source_image_url": "https://cdn.example.com/photo.jpg"}'
711
848
  ```
712
849
 
850
+ ## Primitive: image_remove_background_greenscreen
851
+
852
+ Cheap **local chroma-key** background removal — no third-party API, just `sharp` doing per-pixel color-distance keying, so it costs a fraction of `image_remove_background`. Use it whenever the source sits on a **flat, solid, evenly-lit background**: a green screen, or any single fill color you pass via `key_color` (e.g. `#FFFFFF` for a white background, `#0047BB` for blue). The result is a transparent PNG/WebP at a durable Vidfarm URL.
853
+
854
+ - `POST /api/v1/primitives/images/remove-background-greenscreen` (flat alias: `POST /api/v1/primitives/remove-background-greenscreen`)
855
+ - Body: `{ "tracer": "...", "payload": { "source_image_url": "https://...", "key_color"?: "#00FF00", "tolerance"?: 0.3, "softness"?: 0.1, "despill"?: true, "output_format"?: "png" | "webp" }, "webhook_url"?: "..." }`
856
+ - `key_color` — the background color to key out (hex / `rgb(...)` / named `green`/`white`/`blue`/`black`). Default `#00FF00`.
857
+ - `tolerance` (0–1, default `0.3`) — how close a pixel must be to `key_color` to be removed. Raise it if fringe survives, lower it if the subject is being eaten.
858
+ - `softness` (0–1, default `0.1`) — feather band beyond `tolerance` for anti-aliased edges.
859
+ - `despill` (default `true`) — suppresses the residual key-color rim on the subject's edges (only applied for chromatic keys like green/blue).
860
+ - Response: standard primitive job; poll for completion, then read `image.file_url` / `primary_file_url`. `keyed_fraction` in the output tells you what share of pixels were removed (near-0 usually means the wrong `key_color` or too-low `tolerance`).
861
+ - Billing: small compute-only wallet fee (`greenscreen_chroma_key` cost center) — cheaper than the RapidAPI route.
862
+
863
+ ## Primitive: media_overlay ("create media overlay" — Vox-style)
864
+
865
+ **The go-to way to mint a transparent overlay graphic to float over a composition.** So-called "Vox-style" animations are really just AI-generated illustrations with transparent backgrounds animated in HTML over the timeline — this primitive is that two-step process fused into one job: it (1) generates an AI image of your subject on a **forced flat key-color background** (using the caller's own BYOK image keys, with the greenscreen instruction injected into the prompt automatically) and (2) chroma-keys that background out, returning a ready-to-composite transparent PNG/WebP. **Prefer this over generate-then-manually-remove**, and reach for it *frequently* whenever a scene wants a floating illustration, prop, icon, sticker, or cut-out character.
866
+
867
+ - `POST /api/v1/primitives/images/create-overlay` (flat alias: `POST /api/v1/primitives/create-media-overlay`)
868
+ - Body: `{ "tracer": "...", "payload": { "prompt": "a cartoon rocket ship, flat vector illustration", "provider"?: "...", "model"?: "...", "prompt_attachments"?: ["https://..."], "aspect_ratio"?: "1:1", "image_size"?: "1K"|"2K"|"4K", "key_color"?: "#00FF00", "tolerance"?: 0.3, "softness"?: 0.1, "despill"?: true, "output_format"?: "png"|"webp" }, "webhook_url"?: "..." }`
869
+ - Just describe the **subject** in `prompt` — do NOT describe the background; the primitive appends the flat key-color background requirement for you.
870
+ - `key_color` lets you key against something other than green if your subject is itself green-heavy (e.g. key against `#FF00FF` magenta for a green frog).
871
+ - The chroma-key params (`tolerance`/`softness`/`despill`) behave exactly as in `image_remove_background_greenscreen`.
872
+ - Response: `primary_file_url` is the finished transparent overlay; `greenscreen_source_url` is the raw pre-key frame (kept so you can re-key at a different tolerance without paying for another generation).
873
+ - Billing: small platform wallet fee (`media_overlay` cost center) **plus** the BYOK image-generation cost on the caller's own provider key.
874
+ - Placement: once the job settles, `add_layer` (or the editor's `generate_layer`/`add_layer`) the `primary_file_url` as an image layer over the video and animate it (Ken Burns, keyframes, entrance transitions) like any other overlay.
875
+
876
+ Example:
877
+
878
+ ```bash
879
+ curl -X POST "$VIDFARM_BASE/api/v1/primitives/images/create-overlay" \
880
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
881
+ -H "content-type: application/json" \
882
+ -d '{"tracer": "vox-overlay", "payload": {"prompt": "a friendly cartoon lightbulb mascot, flat vector illustration", "aspect_ratio": "1:1"}}'
883
+ ```
884
+
713
885
  ## Primitive: video_remove_captions
714
886
 
715
887
  Remove burned-in captions/subtitles/on-screen text from any video URL (GhostCut-powered — the same pipeline auto-decompose uses for its caption-free mirror, exposed as a standalone reusable primitive). Result is a durable caption-free MP4 stored at a Vidfarm URL.
package/SKILL.platform.md CHANGED
@@ -191,7 +191,7 @@ The same runtime is used inside the Trackpad Editor iframe AND when a published
191
191
  - Renders the composition into a preview iframe using a `blob:` URL
192
192
  - Bidirectionally syncs timeline drag → HTML `data-start`/`data-duration` mutations, and inspector edits → text/style changes
193
193
  - Persists every commit back to the API as PUT `composition.html` and PATCH `composition.json`
194
- - Exposes an **Editor Action API** so the Chat panel can invoke typed actions (`add_layer`, `remove_layer`, `set_layer_start`, `set_layer_duration`, `set_layer_style`, `set_captions` — animated word-by-word caption runs — `group_layers`, `ungroup_layers`, `replace_composition_html`, etc.)
194
+ - Exposes an **Editor Action API** so the Chat panel can invoke typed actions (`add_layer`, `remove_layer`, `set_layer_start`, `set_layer_duration`, `set_layer_style`, `set_layer_keyframes` — script-free CSS `@keyframes` motion on one layer (opacity/translate/scale/rotate over the clip) — `set_captions` — animated word-by-word caption runs — `group_layers`, `ungroup_layers`, `nudge_layers` (relative timeline move), `ripple_edit` (insert/close time at a point, shifting downstream clips), `trim_layer` (move one edge; a left-trim advances the media in-point), `set_layer_zindex` (restack == track index), `replace_composition_html`, etc.)
195
195
  - Runs the composition-runtime script inside the preview iframe to keep timeline scrub in sync with the current time. That runtime also scrubs **Ken Burns** image animations (CSS pan/zoom on `[data-kenburns]` `<img>` layers) and **animated captions** (word-by-word `vf-cap-*` animations on `[data-caption-animation]` caption layers — Ken Burns normalizes to clip progress, captions seek to the absolute offset `time − data-start` because each word's inline delay/duration is its spoken window) to the playhead, so the preview matches the render frame-for-frame. Presets and keyframes live in `src/hyperframes/composition.ts` (`KEN_BURNS_CSS`, `CAPTION_ANIM_CSS` + `CAPTION_STYLE_PRESETS`), mirrored into the editor bundle and the serve-time HTML rewrite; `normalizePublishHtml` re-injects both stylesheets (marker-guarded) into legacy compositions at render time. Caption gotcha: the layer's inline `text-transform`/`overflow` must be set on the element (the stylesheet's `[data-caption-uppercase]` rule loses to inline styles), and cue text edits must rebuild the word spans (`refreshCaptionWordSpans`) rather than setting `textContent`. Speech→captions REST surfaces: `primitive:captions` (`POST /api/v1/primitives/audio/captions`, src/primitive-registry.ts) is the composition-agnostic job — STT with `wordTimestamps` (openai/whisper-1 real, others estimated via `estimateCaptionWords`) paged by `src/services/captions.ts` into cues + a ready `set_captions_action`; `POST /api/v1/compositions/:forkId/captions` (src/app.ts, next to the composition.html PUT) is the one-step variant that resolves the fork's narration, transcribes on raw customer keys (`loadCustomerVisionKeys` + `transcribeSpeechWithKey`), applies via `insertCaptionLayers` (shared with the devcli), and persists through the same sanitize path — intentionally NOT watch-suppressed so serve tabs live-morph; not for open cloud editor sessions (auto-save clobber). Scrub-stability guard (both `syncCaptionClip` copies — `src/composition-runtime.ts` + the editor's `runtimeScript()`): Chrome permanently drops a CSS-paused `fill:none` animation once its `currentTime` is pushed past the effect end, so the scrub parks `fill:none` word animations at 0 when the playhead is past their window (visually identical resting state, keeps them alive for scrub-back) while `fill:forwards` presets overshoot to hold their final keyframe; missing word animations are restarted by cycling `animation-name` through `none`. Live element patches that rebuild word spans must re-scrub via the `__player.syncCaptions()` runtime API (the editor calls it after `patchLiveElement`/`previewLiveElement`) or fresh spans sit paused-at-0 (invisible for word-pop/stack-up) until the next seek.
196
196
 
197
197
  The editor is a single monolithic React component (~9k lines). It builds cleanly to a static bundle deployed to CloudFront under `/editor/`.
@@ -255,6 +255,7 @@ Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-thr
255
255
  - **Failure reconciliation** — the workflow has no failure hook into records, so `GET /raws/scan/:scanId` lazily `DescribeExecution`s a running scan and flips the scan+source records to `failed` (or `complete`) so pollers converge.
256
256
  - **devcli is LOCAL-FIRST** — `vidfarm raws scan` runs local ffmpeg and, by default, the user's local claude/codex CLI subscription (`LocalAgentClipClient`, implementing the same `ClipHuntModelClient` interface as the API providers; refine's model call lives on the client as `decideScenes` to make them swappable). Provider keys are the fallback; `--cloud` is the explicit backup that presign-uploads to the temp folder and drives this pipeline.
257
257
  - **Bundling gotcha** — the clip-scan and primitive-media Lambdas bundle `ffmpeg-static`/`ffprobe-static` with `forceDockerBundling: true`: ffmpeg-static downloads the binary for the platform it is installed on, so a mac-local bundle ships a darwin ffmpeg the Lambda cannot exec (`exit 126`). They also need `VIDFARM_DATA_DIR=/tmp/vidfarm` because `config.ts` mkdirs at import.
258
+ - **Exact-subrange clipper** — separate from the AI hunt, the **Clipper** cuts one precise `[start,end]` into a single raw. `POST /raws/resolve-source` `{ source_url }` returns a DIRECT `preview_url` (via `pickBestVideoMedia`, no download/re-host) so the web `/tools/clipper` page can play the source and pick an in/out; `POST /raws/clip-range` `{ source_url|preview_url|temp_file_id, start_sec, end_sec, tracer?, folder_path?, name? }` runs `importSubrangeAsRaw` inline on the request Lambda (fetch source → `extractClip` ffmpeg trim → `storage.putFile clips/<owner>/<id>.mp4` + thumbnail + `clipRecords.putClip`), landing one raw under `/raws/<folder>` (defaults to the tracer). Compute-only (RapidAPI billed only when a social `source_url` must be resolved). Same operation is exposed to the copilot (system prompt in `src/editor-chat.ts`) and devcli (`vidfarm clipper`). The three creative tools now live under `/tools/*` (`/tools/image` masked-image inpaint, `/tools/video` time-scoped video edits, `/tools/clipper`); the legacy `/inpaint/*` paths 302-redirect there, and the `images/inpaint` primitive keeps its name.
258
259
 
259
260
  ## Editor Chat
260
261
 
@@ -266,7 +267,7 @@ Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-thr
266
267
 
267
268
  The chat model uses the **caller's provider keys**. No Vidfarm-owned model calls happen in this path.
268
269
 
269
- Beyond the Editor Action API, the chat harness registers shared tools defined in **two synced copies** (`src/app.ts` for the in-process dev server and `infra/lambda/editor-chat.ts` for the deployed Lambda — keep them in lockstep): `http_request` (allowlisted REST/primitive calls), `frontend_action` (tracer/thread state), `video_context`, `load_skill`, and `browse_files`. `load_skill` is the copilot's knowledge-on-demand seam: it fetches vendored skill-pack content (SKILL.md or a reference file) from the host's public `/skill/:name` + `/skill-pack/:name/files/*` mirror routes (backed by `.agents/skills/`, shipped in the api Docker image), size-capped, with the pack catalog + one-liners defined once in `EDITOR_CHAT_SKILL_PACKS` (`src/editor-chat.ts`) and shared by both copies. The stream-loop guardrails (`redactUnverifiedJobIds`, `buildEditorChatGuardrailNotice`, async-job handoff text) also live in `src/editor-chat.ts` and are imported by both copies — never re-implement them locally. `editor_action`'s `replace_composition_html` is lint-gated in the tool `execute` via `src/services/composition-lint.ts` (a rejected result carries `rejected: true` + `lint_errors` and omits `action_type`, so the browser never applies it). `browse_files` reads **and writes** the caller's My Files library — `action=list`/`search`/`read`/`write`/`annotate`. `write` saves a text file (md/txt/csv/json/srt/vtt) via `POST /me/attachments/upload`, or **imports a durable media URL** into the library when `source_url` is passed (how a generated `character_sprite_card.png` gets persisted instead of stranding in a job result). `annotate` sets free-form metadata `notes` on any entry (`PATCH /me/attachments/:id`); notes are **vector-embedded server-side** (BYOK, same gemini→openai auto-pick seam as clip search; no key fail-softs to keyword-only) and `search` runs the hybrid keyword+semantic lookup over every file's name/folder/notes via `POST /me/attachments/search` — the My Files mirror of `/raws/search`. Write+annotate are what let the copilot persist Getting Started context (About.md, awareness-levels.md, persuasive-angles.md, ad-hooks.md) and character-consistency pairs (`about_character.md` + `character_sprite_card.png`) namescoped under a product/offer folder; the system prompt in `src/editor-chat.ts` drives when to save and annotate. The devcli mirrors are `vidfarm put-file [--notes]`, `annotate-file`, and `files --search`.
270
+ Beyond the Editor Action API, the chat harness registers shared tools defined in **two synced copies** (`src/app.ts` for the in-process dev server and `infra/lambda/editor-chat.ts` for the deployed Lambda — keep them in lockstep): `http_request` (allowlisted REST/primitive calls), `frontend_action` (tracer/thread state), `video_context`, `load_skill`, and `browse_files`. `load_skill` is the copilot's knowledge-on-demand seam: it fetches vendored skill-pack content (SKILL.md or a reference file) from the host's public `/skill/:name` + `/skill-pack/:name/files/*` mirror routes (backed by `.agents/skills/`, shipped in the api Docker image), size-capped, with the pack catalog + one-liners defined once in `EDITOR_CHAT_SKILL_PACKS` (`src/editor-chat.ts`) and shared by both copies. Loadable packs are the hyperframes suite (`hyperframes`, `hyperframes-core`, `hyperframes-animation`, `hyperframes-keyframes`, `hyperframes-creative`, `hyperframes-cli`) plus the workflow packs (`embedded-captions`, `product-launch-video`, `faceless-explainer`, `website-to-video`, `general-video`, `motion-graphics`, `slideshow`, `talking-head-recut`, `vidfarm-media`); the `.claude/skills/*` entries are just Claude Code discovery symlinks to the same `.agents/skills/` dirs. In the web editor the copilot applies only the CSS / `@keyframes` + declarative-preset half of the animation/keyframes packs — their JS-adapter techniques are devcli-only. The stream-loop guardrails (`redactUnverifiedJobIds`, `buildEditorChatGuardrailNotice`, async-job handoff text) also live in `src/editor-chat.ts` and are imported by both copies — never re-implement them locally. `editor_action`'s `replace_composition_html` is lint-gated in the tool `execute` via `src/services/composition-lint.ts` (a rejected result carries `rejected: true` + `lint_errors` and omits `action_type`, so the browser never applies it). Web-editor motion is **declarative/CSS only**: the JS runtime adapters (anime.js / GSAP / Lottie / Three.js / TypeGPU) are stripped on save and `replace_composition_html` additionally rejects any `<script>`-bearing HTML, so the copilot authors motion with `set_layer_keyframes` (a CSS `@keyframes` animation on one layer) / `<style>` `@keyframes` + the preset vocabulary; scripted / adapter-driven compositions are a devcli-only (`vidfarm render`) capability. The `editor_context` snapshot the model receives now carries each layer's `transition` / `transition_out` / `transition_duration`, `ken_burns` preset, and `animation` (custom CSS keyframe name), and `recent_action_results` reports successes (`ok: true` + summary) as well as failures; the model can also GET `…/compositions/:forkId/composition.html` for exact markup. `browse_files` reads **and writes** the caller's file directory — `action=list`/`search`/`read`/`write`/`annotate`/`move`/`rename`. `action=rename` renames a file or **folder** in place via `POST /me/directory/rename` (`path` + `new_name`, plus `file_id` for a file — a metadata-only rename that leaves the S3 key/view URL intact). File rename is `/files`/`/temp` only, but **folder rename works in every root including `/raws` and `/approved`** — renaming a `/raws` folder re-points every clip inside it (`clipRecords.moveClipsFolder`) and renaming an `/approved` folder re-points every post (`serverlessRecords.moveReadyPostFolder`), which is how an auto-named folder like `/raws/lvlldspajcg` or `/approved/q3-promo` gets a real name; the `/library/raws` and `/library/approved` explorers each expose the same rename on every folder tile's kebab. `action=move` reorganizes a `/raws` clip (`PATCH /raws/:id`) or an `/approved` post (`PATCH /api/v1/approved/posts/:id`) between folders. There are now **four roots**: `/files`, `/temp`, `/raws`, and `/approved` (finished ready-to-post cuts, foldered — new posts self-file into a folder named after their tracer; `ReadyPostRecord.folderPath`). `action=list` defaults to `path='/'` when `path` is omitted, so the `/raws` (hunted raws), `/temp` (scratch), and `/approved` (published cuts) roots surface alongside the My Files folders instead of being hidden; a `/raws` listing accepts a `content_type` shot-kind filter (`talking_head`/`b_roll`/`product_shot`/`screen_recording`/…) and paginates via `offset`/`limit`. `write` saves a text file (md/txt/csv/json/srt/vtt) via `POST /me/attachments/upload`, or **imports a durable media URL** into the library when `source_url` is passed (how a generated `character_sprite_card.png` gets persisted instead of stranding in a job result). `annotate` sets free-form metadata `notes` on any entry (`PATCH /me/attachments/:id`); notes are **vector-embedded server-side** (BYOK, same gemini→openai auto-pick seam as clip search; no key fail-softs to keyword-only) and `search` runs the hybrid keyword+semantic lookup over every file's name/folder/notes via `POST /me/attachments/search` — the My Files mirror of `/raws/search`. Write+annotate are what let the copilot persist Getting Started context (About.md, awareness-levels.md, persuasive-angles.md, ad-hooks.md) and **recurring characters** — a first-class `/files/characters/<slug>/` trio (`<character_id>.json` manifest — named after the id `character_`+slug, e.g. `character_zara.json` + `character_sprite_card.png` + `character_about.md`) the copilot lists/reads before generating a saved character; the system prompt in `src/editor-chat.ts` drives when to save, annotate, and rename. The devcli mirrors are `vidfarm put-file [--notes]`, `annotate-file`, `files --search`, and `directory rename`.
270
271
 
271
272
  ## Primitives
272
273
 
@@ -274,13 +275,15 @@ Primitives are hosted operations that are NOT templates — they're reusable bui
274
275
 
275
276
  - `create_image`, `edit_image`, `inpaint_image` — image generation
276
277
  - `create_html_image` — HyperFrames HTML → PNG (in-process headless Chromium)
277
- - `remove_background` — RapidAPI-backed image background removal → durable PNG/WebP
278
+ - `remove_background` — RapidAPI-backed AI matting for arbitrary/messy-background photos → durable PNG/WebP
279
+ - `image_remove_background_greenscreen` — CHEAP local `sharp` chroma-key removal for flat solid backgrounds (green screen or any `key_color`) → durable transparent PNG/WebP; compute-only, prefer over `remove_background` when the background is a controlled solid fill
280
+ - `media_overlay` ("create media overlay", Vox-style) — one job: BYOK AI image gen on a forced flat key-color background + local chroma key → ready-to-composite transparent overlay PNG/WebP. The go-to primitive for floating illustration/prop/cut-out overlays
278
281
  - `download_media_video` — social/media URL → durable MP4
279
282
  - `hyperframes_render` — composition HTML → MP4 (the render primitive publishing uses)
280
283
  - `brainstorm` — LLM brainstorm route
281
284
  - `tts` / `stt` — speech, BYOK on the caller's saved provider keys. `POST /api/v1/primitives/audio/speech` (`primitive:tts`) turns text into narration audio with a promptable voice style (`instructions`); `POST /api/v1/primitives/audio/transcribe` (`primitive:stt`) transcribes any video or audio URL (video demuxes through the primitive-media ffmpeg seam first, 64kbps mono, ~40min/20MB cap) and returns **two formats**: a simple subtitle transcript (plain text + SRT cues) and an advanced multi-speaker version (speaker-attributed timed segments; diarization is Gemini-only — JSON-mode audio transcription in `src/services/speech.ts`). The raw provider speech calls live in `src/services/speech.ts`, shared by `ProviderService` (leased BYOK keys, usage metering) and the devcli `vidfarm tts`/`vidfarm stt` commands, which run LOCAL-FIRST on the user's own env key (local ffmpeg demux, no job, no wallet) with `--cloud` as the explicit backup route.
282
285
 
283
- Directors call primitives when they want a one-off asset before feeding it into a template. Templates call primitives internally as workflow steps. All primitives are metered against the caller's wallet (BYOK AI calls excepted e.g. tts/stt model spend rides the caller's own provider key; only compute like the stt demux step wallet-bills).
286
+ Directors call primitives when they want a one-off asset before feeding it into a template. Templates call primitives internally as workflow steps. Each primitive declares one of three billing modes: **WALLET** a metered platform charge (e.g. `remove_background` / `image_remove_background` RapidAPI pass-through, `video_remove_captions` GhostCut, cloud `hyperframes_render`, the local `image_remove_background_greenscreen` chroma key, and `media_overlay` which layers a small platform fee on top of its BYOK image-gen leg); **BYOK** — the AI spend rides the caller's own provider key and is never wallet-billed (tts/stt, smart decompose, clip tagging/embedding); or **COMPUTE-ONLY** — only the AWS Lambda / Step-Functions compute bills (clip hunts, the stt audio-demux step). So "all primitives are metered against the caller's wallet" holds only for the compute/platform legs — every BYOK AI call is excepted.
284
287
 
285
288
  ## Billing
286
289
 
@@ -387,6 +390,7 @@ Legacy but still active:
387
390
  ## Where to look for things
388
391
 
389
392
  - Composition endpoints — `src/app.ts`: grep for `COMPOSITIONS_PREFIX` (the block runs from `app.post(COMPOSITIONS_PREFIX` fork creation through the `versions` routes; line numbers drift, always grep the symbol)
393
+ - Fork creation has ONE source-of-truth helper trio in `src/app.ts` — `seedForkWorkingFromFork` (copies the 5 canonical working files, optionally from a pinned version) + `forkCompositionFromTemplateDefault` (fresh from the template's canonical default) + `forkCompositionFromExistingFork` (branch a working copy + snapshot). `POST /compositions` (template entry), `POST /:forkId/fork { from: current|default }` (the unified New-fork endpoint behind the ⋯ menu / AI `fork_composition` / devcli `clone --from`), and the legacy `POST /:forkId/clone` alias all funnel through them, so seed logic stays uniform. grep the helper names.
390
394
  - Fork CRUD & permissions — `src/services/serverless-records.ts`, `src/services/fork-access.ts`
391
395
  - Storage keys + public-read prefix list — `src/services/storage.ts`
392
396
  - Fork manifest.json helper — `src/services/fork-manifest.ts`