@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9

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 (157) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +117 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +308 -0
  13. package/SKILL.director.md +517 -250
  14. package/dist/src/cli.js +563 -99
  15. package/dist/src/devcli/clips.js +29 -12
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +14 -2
  20. package/dist/src/services/clip-curation/hunt.js +56 -0
  21. package/dist/src/services/clip-curation/index.js +1 -1
  22. package/dist/src/services/clip-curation/scan.js +5 -3
  23. package/package.json +30 -10
  24. package/SKILL.platform.md +0 -432
  25. package/demo/README.md +0 -28
  26. package/demo/dist/app.css +0 -1
  27. package/demo/dist/app.js +0 -1850
  28. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  29. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  30. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  31. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  32. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  33. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  34. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  35. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  36. package/demo/dist/favicon.ico +0 -0
  37. package/demo/dist/icons/timeline/audio.svg +0 -7
  38. package/demo/dist/icons/timeline/captions.svg +0 -5
  39. package/demo/dist/icons/timeline/composition.svg +0 -12
  40. package/demo/dist/icons/timeline/image.svg +0 -18
  41. package/demo/dist/icons/timeline/music.svg +0 -10
  42. package/demo/dist/icons/timeline/text.svg +0 -3
  43. package/demo/dist/index.html +0 -15
  44. package/dist/src/account-pages-legacy.js +0 -9396
  45. package/dist/src/account-pages.js +0 -61
  46. package/dist/src/app.js +0 -21603
  47. package/dist/src/composition-runtime.js +0 -1053
  48. package/dist/src/config.js +0 -217
  49. package/dist/src/context.js +0 -447
  50. package/dist/src/dev-app-legacy.js +0 -739
  51. package/dist/src/dev-app.js +0 -6
  52. package/dist/src/devcli/migrate-local.js +0 -140
  53. package/dist/src/devcli/sync.js +0 -368
  54. package/dist/src/domain.js +0 -5
  55. package/dist/src/editor-chat-history.js +0 -82
  56. package/dist/src/editor-chat.js +0 -828
  57. package/dist/src/editor-dark-theme.js +0 -1128
  58. package/dist/src/frontend/debug.js +0 -71
  59. package/dist/src/frontend/discover-client.js +0 -130
  60. package/dist/src/frontend/discover-store.js +0 -23
  61. package/dist/src/frontend/file-directory.js +0 -1018
  62. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  63. package/dist/src/frontend/homepage-client.js +0 -446
  64. package/dist/src/frontend/homepage-shared.js +0 -201
  65. package/dist/src/frontend/homepage-store.js +0 -66
  66. package/dist/src/frontend/homepage-view.js +0 -705
  67. package/dist/src/frontend/page-runtime-client.js +0 -132
  68. package/dist/src/frontend/page-runtime-store.js +0 -9
  69. package/dist/src/frontend/sentry.js +0 -42
  70. package/dist/src/frontend/template-editor-chat.js +0 -4181
  71. package/dist/src/help-page.js +0 -346
  72. package/dist/src/homepage.js +0 -1458
  73. package/dist/src/index.js +0 -16
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/landing-page.js +0 -384
  76. package/dist/src/page-runtime.js +0 -2
  77. package/dist/src/page-shell.js +0 -1452
  78. package/dist/src/primitive-context.js +0 -416
  79. package/dist/src/primitive-registry.js +0 -3940
  80. package/dist/src/primitive-sdk.js +0 -4
  81. package/dist/src/primitives/hyperframes-media.js +0 -108
  82. package/dist/src/react-page-shell.js +0 -35
  83. package/dist/src/ready-post-schedule-component.js +0 -1540
  84. package/dist/src/registry.js +0 -296
  85. package/dist/src/reskin/agency-page.js +0 -299
  86. package/dist/src/reskin/calendar-page.js +0 -568
  87. package/dist/src/reskin/chat-page.js +0 -942
  88. package/dist/src/reskin/discover-page.js +0 -1788
  89. package/dist/src/reskin/document.js +0 -1587
  90. package/dist/src/reskin/help-page.js +0 -357
  91. package/dist/src/reskin/index-page.js +0 -62
  92. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  93. package/dist/src/reskin/inpaint-page.js +0 -2554
  94. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  95. package/dist/src/reskin/job-runs-page.js +0 -477
  96. package/dist/src/reskin/library-page.js +0 -1634
  97. package/dist/src/reskin/login-page.js +0 -262
  98. package/dist/src/reskin/portfolio-page.js +0 -687
  99. package/dist/src/reskin/pricing-page.js +0 -390
  100. package/dist/src/reskin/settings-page.js +0 -732
  101. package/dist/src/reskin/theme.js +0 -711
  102. package/dist/src/runtime.js +0 -35
  103. package/dist/src/services/api-call-history.js +0 -249
  104. package/dist/src/services/auth.js +0 -152
  105. package/dist/src/services/billing-pricing.js +0 -39
  106. package/dist/src/services/billing.js +0 -241
  107. package/dist/src/services/cast.js +0 -127
  108. package/dist/src/services/chat-threads.js +0 -92
  109. package/dist/src/services/clip-records.js +0 -250
  110. package/dist/src/services/clip-search.js +0 -77
  111. package/dist/src/services/clip-vectors.js +0 -125
  112. package/dist/src/services/composition-sanitize.js +0 -124
  113. package/dist/src/services/composition-watch.js +0 -79
  114. package/dist/src/services/elevenlabs.js +0 -222
  115. package/dist/src/services/file-directory.js +0 -117
  116. package/dist/src/services/fork-access.js +0 -93
  117. package/dist/src/services/fork-manifest.js +0 -42
  118. package/dist/src/services/ghostcut.js +0 -179
  119. package/dist/src/services/hyperframes.js +0 -3654
  120. package/dist/src/services/job-capacity.js +0 -14
  121. package/dist/src/services/job-logs.js +0 -197
  122. package/dist/src/services/jobs.js +0 -136
  123. package/dist/src/services/local-dynamo.js +0 -0
  124. package/dist/src/services/media-processing.js +0 -766
  125. package/dist/src/services/primitive-media-lambda.js +0 -280
  126. package/dist/src/services/providers.js +0 -2748
  127. package/dist/src/services/rate-limits.js +0 -262
  128. package/dist/src/services/scene-annotations.js +0 -32
  129. package/dist/src/services/serverless-auth.js +0 -382
  130. package/dist/src/services/serverless-jobs.js +0 -1084
  131. package/dist/src/services/serverless-provider-keys.js +0 -409
  132. package/dist/src/services/serverless-records.js +0 -1515
  133. package/dist/src/services/serverless-template-configs.js +0 -75
  134. package/dist/src/services/storage.js +0 -461
  135. package/dist/src/services/swipe-customize.js +0 -437
  136. package/dist/src/services/template-certification.js +0 -413
  137. package/dist/src/services/template-loader.js +0 -99
  138. package/dist/src/services/template-runtime-bundles.js +0 -217
  139. package/dist/src/services/template-sources.js +0 -1017
  140. package/dist/src/services/upstream.js +0 -248
  141. package/dist/src/services/video-normalization.js +0 -2
  142. package/dist/src/services/webhooks.js +0 -62
  143. package/dist/src/template-editor-pages.js +0 -2576
  144. package/dist/src/template-editor-shell.js +0 -2893
  145. package/dist/src/template-sdk.js +0 -4
  146. package/dist/src/worker.js +0 -17
  147. package/public/assets/discover-client-app.js +0 -1
  148. package/public/assets/file-directory-app.js +0 -3
  149. package/public/assets/homepage-app.js +0 -54
  150. package/public/assets/homepage-client-app.js +0 -80
  151. package/public/assets/page-runtime-client-app.js +0 -94
  152. package/public/assets/placeholders/scene-placeholder.png +0 -0
  153. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  154. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  155. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  156. package/src/assets/favicon.ico +0 -0
  157. package/src/assets/logo-vidfarm.png +0 -0
@@ -0,0 +1,365 @@
1
+ ## Agentic editing: the three axes (SWAP ↔ REPLACE)
2
+
3
+ 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:
4
+
5
+ - **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.
6
+ - **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.
7
+
8
+ 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.
9
+
10
+ **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).
11
+
12
+ **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` / generated with `/api/v1/primitives/music/generate` (`vidfarm music`)), never a faked or duplicated voice layer.
13
+
14
+ ## The three paintbrushes & two replication harnesses
15
+
16
+ Vidfarm is deliberately founder-friendly and thrift-first: **we do not spend expensive AI credits on every scene.** The three axes above tell you *what* is being re-worked; the **three paintbrushes** tell you *how* to paint each scene once you've decided to REPLACE it. A **technical replication decomposition** is exactly this — for the target template, name **when / where / what** each beat should be painted with.
17
+
18
+ > **This decomposition is now a materialized artifact.** Every decomposed fork carries a **replication harness** (`replication-harness.json` / `editor_context.replication_harness` / `GET .../replication-harness.json`) that already contains BOTH plans as concrete per-beat brush assignments plus reusable-asset guidance and viral-DNA guardrails. When it's present, **execute its beats** instead of re-deriving them; the methodology below is how it was built and the fallback when it's absent (older forks / free tier). See the route section further down.
19
+
20
+ 1. **Raw clips** — cut + remix existing long-form / short-form footage. The workhorse. Source in cost order: the director's own `/raws` + `/files` library → HUNT new raws out of a URL/VOD (`vidfarm raws scan`) → only then generate. Combining a **background video + a foreground video** (greenscreen / picture-in-picture) covers a huge share of "video meme" formats with zero generation.
21
+ 2. **HTML/JS hyperframes** — video-from-HTML. Animate text, image elements, logos, stickers, charts and lower-thirds with CSS/declarative motion (web editor) or JS adapters (anime.js/GSAP/Lottie/Three, devcli only). Deterministic, cheap, re-themeable — the right brush for titles, kinetic captions, data beats, brand cards, and any "graphic" scene.
22
+ 3. **Pure AI generation** — AI image / video / voice / music. The most expensive brush (AI **video** especially, $1–$10+); the last resort for a beat no clip or hyperframe can cover.
23
+
24
+ **Reusable asset library.** Directors accumulate logos, stickers, reactions, b-roll, a-roll, and a brand media kit. The decomposition should have an **opinion on when and where to reuse these** — reactions and stickers for beats, the media kit for brand identity, b-roll to cover cuts. But usage can quietly break the format: reskinning past a load-bearing beat kills what made it go viral. **Anchor every asset/paintbrush call to the harness** (`viral_dna`, `viral_dna.emotional_punch`, `editor-harness.json`, `static_vs_pivot`) — reuse and re-skin the *dressing*, preserve the *DNA*.
25
+
26
+ For any replication, offer the director **two harnesses** and recommend (A) unless they've asked for premium or the budget covers it:
27
+
28
+ **(A) Cheap & efficient** — the default. Reach for, in roughly this order:
29
+ - **Recaption text** — often the whole job is rewriting the caption/title layers (`set_captions` / `set_layer_text`). A two-minute change.
30
+ - **Background + foreground video** memes — composite two clips instead of generating one.
31
+ - **Animate HTML + image elements** with hyperframes (Ken Burns on a still, kinetic type, animated logo/sticker) instead of AI motion.
32
+ - **Reuse media elements** from the library; **AI-generate a reusable element once** (a sticker, a logo treatment, a character sprite) then reuse it across scenes rather than regenerating per-scene.
33
+ - **Greenscreen** — chroma-key a subject onto new backgrounds (`image_remove_background_greenscreen` / `media_overlay`).
34
+ - **Raw-clip long-form and remix** — hunt + rearrange existing footage.
35
+ - Lean on the **library of memes / reactions / b-roll / a-roll** and the **brand media kit**.
36
+ - **Only if genuinely needed**, use AI image → and, last, AI video / voice / music.
37
+
38
+ **(B) Best quality** — when the director wants premium and budget allows:
39
+ - **AI video generation by default** for hero scenes.
40
+ - **Storyboard with AI image first** (cheap stills to lock composition/subject), then generate motion from those references for consistency.
41
+ - **Adversarially grade** the output with a coding agent (Claude Code / Codex / any capable AI agent) — render, critique against the harness, iterate — rather than shipping the first generation.
42
+
43
+ **Be generous to free-tier / no-account directors.** Vidfarm's replication harness is open source, and an everyday capable AI agent should be able to follow this decomposition and rebuild a template **without a Vidfarm wallet** — clipping, hyperframes, and local render (`vidfarm serve`) all run on free compute. Keep the recommendation self-contained and pragmatic; don't gate the cheap path behind paid primitives.
44
+
45
+ **Free tier does its OWN decomposition.** The catch on the free tier (local devcli, no account): nobody has pre-decomposed the reference video for them. There is no `video-context.json`, `editor-harness.json`, `replication-harness.json`, or `scene-annotations.json` to `vidfarm pull` — **the user and their agent must watch the video, break it into scenes/audio/text, read the viral DNA, and choose the three paintbrushes by hand.** Coach them through that; the harness above is the method to do it. **Paid Vidfarm accounts skip the manual pass**: they get a large library of **pre-decomposed viral templates** (the decompose passes already run) plus **prompt-harness best practices distilled from platform-scale learnings** — so a fork arrives already carrying its DNA, editor harness, **replication harness (both cheap + best-quality plans, per-beat brush assignments)**, and per-scene recreation annotations. It's fair to surface that upgrade when a free-tier user is decomposing by hand.
46
+
47
+ ## Edit in the Trackpad Editor
48
+
49
+ The editor is a full timeline surface:
50
+
51
+ - **Timeline** — multi-track, drag/trim/split clips, group/ungroup layers
52
+ - **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.
53
+ - **Canvas frame overlay** — 8-handle resize + WYSIWYG positioning
54
+ - **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`.
55
+ - **Auto-save** — every commit writes composition.html + composition.json to the working state. No explicit save button.
56
+
57
+ The editor persists to:
58
+
59
+ - `PUT /api/v1/compositions/:forkId/composition.html` — replace HTML
60
+ - `PATCH /api/v1/compositions/:forkId/composition.json` — patch metadata
61
+
62
+ Automation that needs to bypass the UI can call these endpoints directly.
63
+
64
+ Reads also work anonymously since the `compositions/forks/*` S3 prefix is public-read — clients that only need to fetch (not mutate) can bypass the API and hit S3 directly:
65
+
66
+ - `GET https://<bucket>.s3.<region>.amazonaws.com/compositions/forks/<forkId>/working/composition.html`
67
+ - `GET .../working/composition.json`
68
+ - `GET .../working/manifest.json` — fork identity + genealogy (`parent_fork_id`, `parent_version`, `latest_version`, direct URLs for html/json/parent manifest). Also served via `GET /api/v1/compositions/:forkId/manifest.json` and `.../versions/:v/manifest.json`.
69
+
70
+ Treat the `forkId` as an unguessable bearer token — anyone with it can view the composition.
71
+
72
+ ## Auto-decompose
73
+
74
+ If the fork's source is a raw video (not a pre-decomposed template), call auto-decompose to split it into scenes:
75
+
76
+ ```
77
+ POST /api/v1/compositions/:forkId/auto-decompose
78
+ Content-Type: application/json
79
+
80
+ { "mode": "smart" | "time-slice", "html": "<optional current html>", "requested_scenes": null, "target_scene_seconds": null, "user_prompt": "optional guidance" }
81
+ ```
82
+
83
+ Two modes:
84
+
85
+ - **`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.
86
+ - **`time-slice`** — deterministic equal-duration split. Instant. No AI provider needed.
87
+
88
+ **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.
89
+
90
+ Response:
91
+
92
+ ```
93
+ { "ok": true, "mode": "smart", "scene_count": 6, "caption_count": 4, "duration_seconds": 12.4, "ghostcut_pending": true, "ghostcut_task_id": "..." }
94
+ ```
95
+
96
+ If `ghostcut_pending: true`, poll `POST /api/v1/compositions/:forkId/remove-video-captions-poll` (legacy alias: `/ghostcut-poll`) until status transitions to `done` or `failed`.
97
+
98
+ Read-only state (does **not** advance the job or bill): `GET /api/v1/compositions/:forkId/remove-video-captions` (legacy alias: `/ghostcut`). Returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }`. This is how you read the composition's **two video sources**: `original_source_url` is the **original video** (raw upload, captions intact) and `mirrored_url` is the **decomposed video** (the processed, caption-free copy the timeline renders from). Use this when you want both URLs without triggering another poll — the AI copilot uses it to pick between the original video and the decomposed video before feeding a video into a primitive route (image extract, dedupe, trim, ai video edit, etc.). Prefer the **decomposed video** (`mirrored_url`) when `status === "done"` and the downstream call should be caption-free; prefer the **original video** (`original_source_url`) when the user explicitly wants the raw upload or when `status !== "done"`.
99
+
100
+ ## Video context (transcript + scene descriptions)
101
+
102
+ `GET /api/v1/compositions/:forkId/video-context.json` — read-only, non-billing. Returns everything the smart decompose learned about the source video, so an agent can ground its work in what the video actually **says** and **shows**:
103
+
104
+ ```
105
+ {
106
+ "ok": true,
107
+ "status": "ready" | "none",
108
+ "summary": "...",
109
+ "duration_seconds": 12.4,
110
+ "transcript": {
111
+ "text": "full verbatim transcript",
112
+ "language": "en",
113
+ "provider": "gemini",
114
+ "segments": [{ "start": 0.0, "end": 2.4, "text": "..." }]
115
+ },
116
+ "scenes": [{
117
+ "slug": "hook_reveal",
118
+ "start": 0, "duration": 2.5,
119
+ "label": "Hook",
120
+ "description": "Literal visual description of what's on screen in this scene",
121
+ "viral_note": "...",
122
+ "transcript_excerpt": "what is spoken during this scene"
123
+ }],
124
+ "viral_dna": {
125
+ "trend_tagline": "...", "hook": "...", "retention": "...", "payoff": "...",
126
+ "emotional_punch": {
127
+ "core_emotion": "smug relief-laugh", "tone": "deadpan comedic",
128
+ "arc": "flat setup → mounting absurdity → sudden turn", "peak_moment": "the reveal at ~0:07",
129
+ "mechanism": "subversion + trending-sound beat drop synced to the cut",
130
+ "humor": "the joke is X — it works because Y (rebuild the SAME joke around a new subject)",
131
+ "delivery": "flat VO cadence, hold the beat before the punch, beat drop lands on the reveal",
132
+ "preserve": ["keep the beat drop on the reveal cut", "keep the flat unbothered delivery"]
133
+ },
134
+ "static_vs_pivot": {
135
+ "summary": "Keep the reveal structure and audio swagger; swap the subject-specific visuals and copy.",
136
+ "scene_replacement": {
137
+ "overall": "replace_some",
138
+ "reason": "the proof/reveal beats are load-bearing, but the subject matter can pivot",
139
+ "must_keep": ["final reveal beat", "setup-to-payoff contrast"],
140
+ "should_replace": ["subject-specific graphics", "brand-specific screenshots"],
141
+ "load_bearing_scenes": ["hook setup", "payoff reveal"]
142
+ },
143
+ "narration": {
144
+ "overall": "customize_script",
145
+ "reason": "the spoken words are source-specific and stop making sense after a subject swap",
146
+ "same_timing_required": true,
147
+ "use_premium_tts_when_customizing": true
148
+ },
149
+ "music": {
150
+ "overall": "recreate_under_narration",
151
+ "reason": "the music energy matters, but the original combined track should not stay under a rewritten script",
152
+ "volume_guidance": "keep low under VO unless the beat-drop is the joke engine"
153
+ },
154
+ "captions": {
155
+ "overall": "retime_to_new_narration",
156
+ "reason": "caption animation is tied to the spoken words",
157
+ "animation_dependency": "word-by-word timing follows narration cadence"
158
+ },
159
+ "visual_rebuild": {
160
+ "default_strategy": "ai_images_with_ken_burns",
161
+ "reason": "most replacement scenes are graphic/still-like and do not need expensive motion video"
162
+ },
163
+ "viral_dna": {
164
+ "must_keep": ["hook structure", "payoff timing", "audio attitude"],
165
+ "flexible": ["subject matter", "product imagery", "copy details"],
166
+ "do_not_change": ["core reveal mechanic", "caption cadence"]
167
+ }
168
+ },
169
+ "preserve": ["..."], "avoid": ["..."], "promotions": ["..."], "keywords": ["..."]
170
+ }
171
+ }
172
+ ```
173
+
174
+ `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.
175
+
176
+ **`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.
177
+
178
+ **`viral_dna.static_vs_pivot`** is the first-class lazy-prompt playbook. It tells you what must stay versus what can pivot when the user gives a thin prompt like "recreate this for my business" and little else. Use it to decide:
179
+
180
+ - whether most scenes should be replaced or only some
181
+ - whether narration must be customized or the original audio should stay
182
+ - whether background music must be recreated under a new voice
183
+ - whether animated captions need fresh timing against new narration
184
+ - whether cheap AI images + Ken Burns are sufficient, or whether real/AI motion footage is required
185
+ - which beats are load-bearing viral DNA versus merely subject-specific dressing
186
+
187
+ 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.
188
+
189
+ - **Editor chat (frontend AI)** exposes this as the optional `video_context` tool — the copilot calls it on demand.
190
+ - **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.
191
+
192
+ ## Editor harness (how to edit like this)
193
+
194
+ `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.
195
+
196
+ ```
197
+ {
198
+ "ok": true,
199
+ "status": "ready" | "empty" | "none",
200
+ "harness": {
201
+ "one_liner": "Fast first-person VO over gameplay b-roll, karaoke captions, hard cuts.",
202
+ "format": "reddit_story_gameplay",
203
+ "aspect_ratio": "9:16",
204
+ "pacing": { "cut_rhythm": "fast", "avg_scene_seconds": 2.4, "cuts_per_10s": 4, "energy_curve": "front-loaded hook then steady" },
205
+ "typography": { "caption_style": "karaoke", "placement": "center-lower", "font_character": "bold condensed all-caps", "emphasis": "active-word yellow", "text_density": "heavy" },
206
+ "broll": { "reliance": "heavy", "sourcing": "screen-recording", "shot_kinds": ["b_roll"], "cadence": "new visual ~every 2s" },
207
+ "transitions": { "default": "cut", "intro": "fade", "outro": "none", "usage": "hard cuts only" },
208
+ "audio": { "voiceover": "first-person narration", "music": "upbeat lofi bed, low", "sfx": "whoosh on cuts", "captions_from": "voiceover" },
209
+ "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"] },
210
+ "scenes": [{ "role": "hook", "importance": "critical", "must_keep": true, "note": "...", "edit_bias": "swap subject, keep timing+caption cadence" }],
211
+ "important_scenes": ["..."], "editing_bias": ["..."], "do": ["..."], "dont": ["..."]
212
+ }
213
+ }
214
+ ```
215
+
216
+ **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). Pair that with `viral_dna.static_vs_pivot`: if narration is `customize_script`, revoice and then retime captions/scenes that depend on the old VO; if visual_rebuild is `ai_images_with_ken_burns`, do the cheap still+motion path before reaching for AI video; if a scene is listed under `load_bearing_scenes`, preserve its beat even when you swap the subject. **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.
217
+
218
+ - **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`.
219
+ - **Desktop agents (Claude Code / Codex)**: `vidfarm pull` writes `editor-harness.json` to the fork dir alongside `video-context.json`, and also materializes a merged agent bundle at `.harness/context.json` plus `.harness/agent-guide.md`. This deliberately does **not** write a top-level `AGENTS.md`, because many user repos already own that file. For local scripting or agentic edits, read `.harness/agent-guide.md` and `.harness/context.json` FIRST; do not freestyle from `composition.html` alone. The `pull` grounding line reports the generated brief paths.
220
+
221
+ ### `GET /api/v1/compositions/:forkId/replication-harness.json` — the technical replication analysis
222
+
223
+ Read-only, non-billing. The **BUILD** companion to the editor harness (which is STYLE): the same "three paintbrushes & two replication harnesses" methodology above, but materialized per-beat for THIS template. Where `editor-harness.json` says *how to edit to feel right*, this says *which tool to reach for* — so an agent (or a free-tier user) rebuilds thrift-first instead of AI-generating every scene.
224
+
225
+ ```
226
+ {
227
+ "ok": true,
228
+ "status": "ready" | "empty" | "none",
229
+ "harness": {
230
+ "summary": "Recaption + reuse: 90% of this is raw clips + hyperframes captions; AI only for the impossible reveal shot.",
231
+ "recommended_strategy": "cheap_efficient",
232
+ "paintbrush_rationale": "Talking-head + text overlays — footage is huntable and every graphic is HTML; AI video buys almost nothing here.",
233
+ "motion_style": { "animation_feel": "punchy kinetic type, hard cuts, sticker pop-ins", "graphic_elements": ["count-up counter", "arrow pointer", "sticker reactions"], "hyperframes_candidates": ["all captions", "the stat counter", "the end-card logo lockup"], "typography_motion": "per-word pop, active word highlighted" },
234
+ "asset_opportunities": ["reuse the brand-kit logo lockup on the end card", "a 'mind-blown' sticker for the reveal beat"],
235
+ "viral_dna_guardrails": ["never cover the reveal frame with a sticker", "keep the trending sound; land the beat drop on the reveal cut"],
236
+ "free_tier_note": "ffmpeg to clip the source, an HTML→video renderer (hyperframes) for every caption/graphic, chroma-key for greenscreen, a local TTS for narration — no paid primitive needed.",
237
+ "cheap": { "strategy": "cheap_efficient", "one_liner": "...", "method_mix": "80% raw_clip + hyperframes; ai_gen only the reveal", "brush_breakdown": { "raw_clip": "...", "hyperframes": "...", "reusable_asset": "...", "ai_gen": "..." },
238
+ "scenes": [{ "role": "hook", "timestamp": "0:00-0:03", "method": "raw_clip", "fallback_method": "hyperframes", "technique": "recaption over a hunted talking-head clip", "instructions": "...", "assets_needed": ["hook clip from /raws"], "viral_dna_guard": "keep the on-cam energy + first 3 words", "est_credits": "low" }],
239
+ "reusable_assets": [{ "asset_type": "sticker", "where": "reveal beat", "source": "user_library", "purpose": "punch up the reveal", "reuse_note": "reuse across the series", "viral_dna_risk": "do NOT let it cover the reveal frame" }],
240
+ "pipeline": ["hunt hook+payoff raws", "recaption in hyperframes", "reuse logo end-card", "AI-gen only the reveal"], "ai_spend": "one image→video for the reveal only", "tradeoffs": "...", "do": ["..."], "dont": ["..."] },
241
+ "quality": { "strategy": "best_quality", "one_liner": "AI-gen hero scenes, storyboard-first", ... }
242
+ }
243
+ }
244
+ ```
245
+
246
+ **Use it:** when the user wants this cheap / "without burning credits" / in bulk, execute `harness.cheap.scenes[]` beat by beat — `raw_clip` → hunt via `/raws/scan` + `set_layer_media`; `hyperframes` → build the text/graphic as a CSS/anime.js layer (NEVER AI video for on-screen text); `reusable_asset` → pull from `/files` or generate ONCE and reuse; `ai_gen` → only where the beat says so. When they want premium, follow `harness.quality`. **Always** honor each scene's `viral_dna_guard` and the top-level `viral_dna_guardrails` — that is the discipline that stops an eager sticker/logo/footage-swap from flattening the template. `replication_harness` is delivered inline to the editor chat and pulled to disk as `replication-harness.json` (rendered into `.harness/agent-guide.md`) for desktop agents. `status:"none"` → run `POST /auto-decompose` first.
247
+
248
+ ## Generate AI media and drop it on the timeline
249
+
250
+ Both surfaces can generate a NEW AI video/image (optionally conditioned on reference images) and place it into the timeline — filling a blank gap or replacing a scene — using the async generation primitives (`POST /api/v1/primitives/videos/generate`, `/images/generate`). Generation is a **queued job**: the POST returns `202 { job_id }`, and the finished media URL only appears after the job settles (`result.primary_file_url` / `video.file_url` / an artifact `public_url`). Poll `GET /api/v1/user/me/jobs/:jobId` until `status: "succeeded"`.
251
+
252
+ **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.
253
+
254
+ - **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).
255
+
256
+ - **Desktop agents (Claude Code / Codex)** — two devcli commands:
257
+ ```bash
258
+ # 1) Sync the fork to disk and see where the blank space and scenes are
259
+ vidfarm pull <forkId> --dir ./work
260
+ # → prints timeline gaps (e.g. 4-7s) and scene/layer keys, canvas aspect ratio,
261
+ # and whether video-context.json / cast.json / editor-harness.json are present for grounding.
262
+
263
+ # 2) Generate + place in one call (fill a gap)
264
+ vidfarm generate video --prompt "her walking through the door" \
265
+ --ref ./cast/girl.png --aspect-ratio 9:16 --duration 4 \
266
+ --place ./work --at 4
267
+ # → polls the job to the finished MP4, then inserts it at 4s in ./work/composition.html
268
+
269
+ # …or replace a scene with a generated still
270
+ vidfarm generate image --prompt "product hero on marble" --aspect-ratio 9:16 \
271
+ --place ./work --replace <layer_key>
272
+
273
+ # 3) Push it back (or it live-morphs automatically under `vidfarm serve`)
274
+ vidfarm publish --dir ./work --fork <forkId>
275
+ ```
276
+ `--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.
277
+
278
+ ### Fitting imperfect-dimension media (aspect mismatch)
279
+
280
+ 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:
281
+
282
+ - **`object_fit:"cover"`** (default) fills + crops — right for most social footage **when paired with a subject-aware `object_position`**.
283
+ - **`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"`.
284
+ - **`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`.
285
+ - **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.
286
+ - 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.
287
+
288
+ Two surfaces:
289
+ - **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.
290
+ - **Desktop agents (devcli)** — `vidfarm place ./work --src <url> --object-fit cover --object-position "25% 50%"` (also on `--replace <layer_key>`).
291
+
292
+ ### Ken Burns — animate still images (slow pan/zoom)
293
+
294
+ 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.
295
+
296
+ **Presets:** `zoom-in`, `zoom-out`, `pan-left`, `pan-right`, `pan-up`, `pan-down`, `zoom-in-left`, `zoom-in-right`. Optional **intensity** `0.04`–`0.5` (default `0.18`; ~`0.1` subtle, ~`0.3` dramatic) controls how far it travels/zooms.
297
+
298
+ When the director says **"animate the images"**, **"make the photos move"**, **"add Ken Burns"**, or similar, apply a preset to **every** still-image layer — vary presets across adjacent clips (zoom-in, then pan-left, then zoom-out, …) so consecutive stills don't repeat the same motion. Zoom presets suit centered subjects (products, faces); pan presets suit wide scenery or tall screenshots. Only images support it — never set it on video, text, or audio.
299
+
300
+ Three surfaces:
301
+ - **Editor web copilot** — `editor_action` with `action_type=set_layer_media`, `ken_burns=<preset>` (`none` removes it), optional `ken_burns_intensity`. One call per image layer. You can also seed it on `add_layer` (kind=image) or `generate_layer` (media_type=image) so a freshly placed still arrives already animated.
302
+ - **Editor UI (human)** — the Inspector's **Animate (Ken Burns)** dropdown on any selected image.
303
+ - **Desktop agents (devcli)** — `vidfarm place ./work --src <img> --kind image --ken-burns zoom-in [--ken-burns-intensity 0.3]`, or fused with generation: `vidfarm generate image --prompt "…" --place ./work --ken-burns pan-left`.
304
+
305
+ ### Animated captions — word-by-word caption styles (TikTok/CapCut)
306
+
307
+ Compositions support **first-class animated captions**: caption layers whose words animate one at a time, synced to the spoken audio, identical in the editor preview and both render paths. A caption *run* is one layer per cue (a page of ~3-5 words) on a shared track; each layer carries `data-caption-animation="<preset>"` and per-word `<span data-cap-word>` timing — no keyframe authoring, and the text stays editable like any text layer.
308
+
309
+ **Preset styles** (each = an animation + a look; every field individually overridable): `spotlight` (active word gets a rounded highlight pill on bold outlined text — the classic Hormozi/CapCut look), `karaoke` (words fill with color as spoken and stay lit), `word-pop` (one word at a time, punchy scale-in), `stack-up` (words fade/rise in and accumulate), `neon` (active word glow pulse), `bounce` (active word bounces). Overridables: `caption_active_color` (spoken-word color), `caption_highlight_color` (spotlight pill), `caption_uppercase`, plus the normal text fields (`color`, `background`, `background_style`, `font_family`, `font_size`).
310
+
311
+ **Word timings**: real word-level timestamps come from OpenAI STT (whisper-1); Gemini/OpenRouter transcripts get char-weighted estimated word windows, which still read well. Hand-typed text is auto-paged and estimated.
312
+
313
+ Four surfaces:
314
+ - **Editor web copilot** — for narration that needs TRANSCRIBING (a TTS voiceover layer, replaced audio, word-accurate karaoke), call the **captions primitive**: `POST /api/v1/primitives/audio/captions` `{ tracer, payload: { source_url: <narration URL>, style } }` — one job transcribes with word-level timings and returns `captions` (the cue array) plus `set_captions_action` (fully-formed `editor_action` arguments); apply verbatim with `editor_action action_type=set_captions`. For speech that is the decomposed source video's own audio, skip the job: derive cues from `video_context`'s timestamped segments and call `set_captions` directly with `captions=[{text,start,duration,words?}]`, or just `text` (+ `start`/`duration` window) to auto-page. Pick `caption_style`; the call **replaces** all existing animated caption layers, so it's also the "restyle everything" verb. Per-layer: `set_layer_style` with `caption_*` fields restyles one cue, `set_layer_text` rewrites its words, `caption_animation="none"` flattens back to static text. Never hand-build word-by-word captions from many `add_layer` calls.
315
+ - **REST one-step (headless/automation)** — `POST /api/v1/compositions/:forkId/captions` `{ audio_url?|text?, style?, animation?, active_color?, highlight_color?, uppercase?, max_words_per_cue?, offset_sec?, provider?, language? }` transcribes the fork's own narration (auto-resolves: explicit `audio_url` → audio layer → backing video → source video) and writes the animated caption layers into the working composition **server-side** in one synchronous call — BYOK STT, non-billing. On a local `vidfarm serve` box, open editor tabs live-morph the result. Do **not** use it while a cloud editor session is open (the editor's next auto-save would overwrite it) — in-editor, mutate through `set_captions` instead.
316
+ - **Editor UI (human)** — right-click → **Insert Captions** drops a starter cue; the Inspector's **Animated captions** picker switches styles on any selected text/caption layer, with active-word/highlight color and uppercase controls next to it.
317
+ - **Desktop agents (devcli)** — `vidfarm captions generate ./work [--style spotlight]` transcribes the composition's own narration LOCAL-FIRST on your key (prefers OpenAI for real word timestamps) and writes the cue layers into `composition.html` on disk (a running `serve` live-morphs it). Alternates: `--audio <file|url>`, `--srt <file>` (e.g. from `vidfarm stt`), or `--text "<script>"`. Then `vidfarm captions style ./work --style karaoke` (restyle, same text/timing), `captions list`, `captions clear`.
318
+
319
+ 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).
320
+
321
+ ### Script-free keyframe motion & timeline verbs
322
+
323
+ 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):
324
+
325
+ - **`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.
326
+ - **`nudge_layers`** — relative timeline move: `layer_key` (or `layer_keys`) + `delta_start` (seconds) and/or `delta_track` (lanes). Group-aware.
327
+ - **`ripple_edit`** — insert (`delta_start > 0`) or close (`delta_start < 0`) time at `at_time`, shifting every downstream clip.
328
+ - **`trim_layer`** — move one edge: `edge=start|end` + `to_time` (seconds). A left trim also advances the media in-point for video/audio.
329
+ - **`set_layer_zindex`** — restack: `z_order=front|back|forward|backward` (stacking == track index) or an explicit `track`.
330
+
331
+ **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):
332
+
333
+ | Intent | Web `editor_action` | devcli command |
334
+ |---|---|---|
335
+ | Rewrite a text/caption layer's words | `set_layer_text` | `vidfarm set-text <dir> --layer <k> --text "…"` |
336
+ | 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]` |
337
+ | Geometry + constant opacity | `set_layer_visual` | `vidfarm set-visual <dir> --layer <k> [--x --y --width --height --font-size --border-radius --opacity]` |
338
+ | 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]` |
339
+ | Set the stable slug/note handle | `set_layer_identity` | `vidfarm set-identity <dir> --layer <k> [--slug --note]` |
340
+ | Clone a layer | `duplicate_layer` | `vidfarm duplicate <dir> --layer <k> [--start --track --new-key]` |
341
+ | Cut a clip in two | `split_layer` | `vidfarm split <dir> --layer <k> --at <sec>` |
342
+ | Set timing directly (absolute) | `set_layer_timing` | `vidfarm retime <dir> --layer <k> [--start --duration --track --playback-start]` |
343
+ | **Canvas / theme** (whole composition) | `set_composition` | `vidfarm set-composition <dir> [--width --height --duration --background]` |
344
+
345
+ `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.
346
+
347
+ **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`.
348
+
349
+ **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`.
350
+
351
+ ### Local file paths as media (skip the S3 upload)
352
+
353
+ `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:
354
+
355
+ - **On a `vidfarm serve` box (the free bulk workflow)** — `place`'s target composition lives under `<data-dir>/storage/compositions/forks/<forkId>/working/`, so a local `--src` file is **copied straight onto that box's own disk store** (`.vidfarm-local/storage/users/local-media/…`) and referenced by the box's own `http://localhost:3000/storage/…` URL. **Nothing touches S3.** The in-process local renderer fetches it over localhost for free. This is the intended path for bulk-generating many videos from local assets without bloating your durable library.
356
+ ```bash
357
+ # serve box running on :3000 — reference clips straight off disk, render locally, free
358
+ vidfarm place ./.vidfarm-local/storage/compositions/forks/<forkId>/working \
359
+ --src ~/footage/hook-042.mp4 --at 0
360
+ vidfarm render <forkId> # local, $0.00
361
+ ```
362
+ If you ran `serve --port <N>`, pass `--base-url http://localhost:<N>` so the reference URL points at the right origin. A localhost `/storage` URL only resolves against the serve box that holds the file, so these compositions render/preview **locally**; to render in the cloud or share them, upload the media instead (below).
363
+ - **Anywhere else (e.g. a `vidfarm pull` dir headed for cloud render/publish)** — the cloud renderer can't reach your disk, so a local `--src` file is **uploaded to the ephemeral temp store** first, namescoped under a throwaway `temp/` folder by default (override with `--folder`). Use this when the output must be a durable, shareable URL.
364
+
365
+ **`/temp` folder convention.** When you *do* need to upload throwaway media to Vidfarm (cloud render, `approve`, one-off URLs), keep it under a dedicated `temp/` folder — `vidfarm upload clip.mp4 --folder temp`, `vidfarm place … --folder temp`, `vidfarm approve … --folder temp` — so scratch assets stay quarantined in one place you can periodically purge, instead of cluttering your persistent My Files library or the temp-store root.
@@ -0,0 +1,28 @@
1
+ ## Getting started / onboarding a new director
2
+
3
+ Only run this flow when the director signals they **don't know where to start** ("getting started", "help me set up", "I don't know where to begin"). If they already know what they want, skip straight to it — never force onboarding. Directors can also jump to any single step (e.g. "just find me a good template for X" → go straight to template discovery).
4
+
5
+ The point of onboarding is to build **durable, reusable context** in My Files, namescoped under the director's product/offer folder, so future chats and agents can read it back. Save each artifact as Markdown with `browse_files` write (web copilot) or `vidfarm put-file` (devcli):
6
+
7
+ 1. **Product context** → `About.md` (basic offer/product) or a deeper `Interview.md` if they want depth. Drive the interview questions with `brainstorm/coldstart`.
8
+ 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`.
9
+ 3. **Persuasive angles** → `persuasive-angles.md`, via `brainstorm/angles`.
10
+ 4. **Hooks** → `ad-hooks.md`, via `brainstorm/hooks`.
11
+ 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").
12
+ 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.
13
+ 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.
14
+
15
+ **Assume multiple offers.** My Files is multi-offer (see the My Files section) — namescope every onboarding artifact under the right product/offer/region folder (`acme-skincare/About.md`, not a bare `About.md`) so one brand's context never bleeds into another's.
16
+
17
+ ## Default assistance pattern
18
+
19
+ When a director asks "make me a video", the default sequence is:
20
+
21
+ 1. Ask what source material and what template style they want
22
+ 2. Discover a matching template with `GET /discover/feed`
23
+ 3. Fork it
24
+ 4. If they want an entirely new source video, guide them to `auto-decompose` first
25
+ 5. Open the Trackpad Editor and let them make edits
26
+ 6. Publish and share
27
+
28
+ Prefer specific templates over primitives when a template exists that already captures the desired production pattern.