@mevdragon/vidfarm-devcli 0.3.2 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.director.md +95 -43
- package/SKILL.platform.md +31 -14
- package/dist/src/cli.js +8 -3
- package/package.json +7 -6
package/SKILL.director.md
CHANGED
|
@@ -26,7 +26,7 @@ A Vidfarm composition is an **HTML document + JSON metadata**. Each layer (video
|
|
|
26
26
|
|
|
27
27
|
Every director workflow goes through three concepts:
|
|
28
28
|
|
|
29
|
-
1. **Template** — a published composition anyone can fork.
|
|
29
|
+
1. **Template** — a published composition anyone can fork. Template ids start with `template_`; each template opens in the editor at `/editor/:templateId` and has a source video, a decomposed timeline, and metadata (viral DNA, captions, scenes).
|
|
30
30
|
2. **Composition fork** — a private editable copy of a template. Every fork has a `forkId`, an owner, a working state, and a version history. The Trackpad Editor operates on a fork.
|
|
31
31
|
3. **Publish** — render the current fork state to MP4 via a Lambda render job. Each publish snapshots the current working state as an immutable version.
|
|
32
32
|
|
|
@@ -39,42 +39,46 @@ The current API base URLs are:
|
|
|
39
39
|
- staging: `https://staging.vidfarm.cc`
|
|
40
40
|
- production: `https://vidfarm.cc`
|
|
41
41
|
|
|
42
|
-
Every browser action is authenticated by the session cookie set at `/login`.
|
|
42
|
+
Every browser action is authenticated by the session cookie set at `/login`. Headless API-key auth uses the `vidfarm-api-key` header set to the caller's API key (from Settings → API Keys).
|
|
43
|
+
|
|
44
|
+
**Auth contract: API-key auth is the `vidfarm-api-key: <key>` request header and nothing else. `Authorization: Bearer <key>` is NOT supported — the server never reads the `authorization` header (see `requireAuth` in `src/app.ts`).**
|
|
43
45
|
|
|
44
46
|
```
|
|
45
|
-
vidfarm-api-key: vf_key_<
|
|
47
|
+
vidfarm-api-key: vf_key_<id>
|
|
46
48
|
```
|
|
47
49
|
|
|
50
|
+
Scope note (verified against `src/app.ts`): the `vidfarm-api-key` header authenticates **every** authenticated surface — the `requireAuth` routes (`/api/v1/user/*`, `/api/v1/agency/*`, `/api/v1/templates/*`, `/api/v1/primitives/*`, `/api/v1/rate-limit-status`, editor-chat threads) **and** the `/api/v1/compositions/*` + `/api/v1/videos/*` + editor page routes, where an explicit API key takes precedence over any browser session cookie. Cookies remain how the web UI authenticates; headless callers never need one. Reads on composition routes also work without any credential because the `forkId` acts as an unguessable view bearer token, and share-link tokens (header `vidfarm-share-token` or `?share=` query) can grant `viewer`/`editor`/`publisher` roles.
|
|
51
|
+
|
|
48
52
|
## Login flow
|
|
49
53
|
|
|
50
54
|
Directors log in with OTP:
|
|
51
55
|
|
|
52
|
-
1. `POST /api/v1/
|
|
53
|
-
2. `POST /api/v1/
|
|
56
|
+
1. `POST /api/v1/user/request-otp { email }` — sends a code to email; returns `{ ok, delivery }` (plus `dev_code` when delivery is `console` outside production)
|
|
57
|
+
2. `POST /api/v1/user/verify-otp { email, code, name? }` — returns `{ customer, apiKey }` and mints a fresh API key
|
|
54
58
|
|
|
55
|
-
Save the `apiKey` in the client and use it for automation.
|
|
59
|
+
Save the `apiKey` in the client and use it for automation (as the `vidfarm-api-key` header). The browser login form uses the session-cookie variants `POST /login/otp/request` and `POST /login/otp/verify` instead.
|
|
56
60
|
|
|
57
61
|
## Provider keys
|
|
58
62
|
|
|
59
63
|
Vidfarm executes many features (smart auto-decompose, editor chat) with the **caller's own AI provider keys**. Directors save keys once via:
|
|
60
64
|
|
|
61
65
|
- UI: Settings → Provider Keys
|
|
62
|
-
- API: `
|
|
66
|
+
- API: `POST /api/v1/user/me/provider-keys { provider, secret, label? }` — one key per call; `provider` is `"openai" | "gemini" | "openrouter" | "perplexity"`. The key is validated against the provider before it is saved. List saved keys with `GET /api/v1/user/me/provider-keys`.
|
|
63
67
|
|
|
64
68
|
Priority: Gemini → OpenAI → OpenRouter. If a director has no keys, smart decompose falls back to a deterministic time-based split.
|
|
65
69
|
|
|
66
70
|
## Discover templates
|
|
67
71
|
|
|
68
|
-
Templates are listed on the Vidfarm homepage
|
|
72
|
+
Templates are listed on the Vidfarm homepage and `/discover`. Each has a `template_...` id, a preview MP4, source video metadata, and viral DNA.
|
|
69
73
|
|
|
70
|
-
- Browser: browse
|
|
71
|
-
- API: `GET /
|
|
74
|
+
- Browser: browse `/discover`
|
|
75
|
+
- API: `GET /discover/feed` — returns `{ templates: [{ templateId, slugId, title, previewUrl, viralDna, durationSeconds, sourceType, ... }], next_cursor }`
|
|
72
76
|
|
|
73
77
|
Each template exposes a public preview:
|
|
74
78
|
|
|
75
|
-
- `GET /
|
|
76
|
-
- `GET /
|
|
77
|
-
- `GET /
|
|
79
|
+
- `GET /editor/:templateId` — opens the Trackpad Editor for the template (redirects to your fork of it, or to `/login`)
|
|
80
|
+
- `GET /editor/:templateId/composition` — raw composition HTML built from the template's source video
|
|
81
|
+
- `GET /discover/skills/:templateId` — the template's SKILL.md content as JSON (registry templates only)
|
|
78
82
|
|
|
79
83
|
Directors don't edit templates directly. They fork.
|
|
80
84
|
|
|
@@ -84,19 +88,19 @@ Directors don't edit templates directly. They fork.
|
|
|
84
88
|
POST /api/v1/compositions
|
|
85
89
|
Content-Type: application/json
|
|
86
90
|
|
|
87
|
-
{ "
|
|
91
|
+
{ "template_id": "template_<...>", "title": "Optional title" }
|
|
88
92
|
```
|
|
89
93
|
|
|
90
|
-
Returns the new fork's metadata:
|
|
94
|
+
`template_id` is required and must start with `template_` (the handler also accepts the same value under the key `source`). Returns the new fork's metadata (201):
|
|
91
95
|
|
|
92
96
|
```
|
|
93
|
-
{ "
|
|
97
|
+
{ "fork_id": "<forkId>", "template_id": "template_...", "parent_fork_id": "...", "parent_version": 1, "title": "...", "visibility": "private", "latest_version": 0, "composition_url": "...", "composition_data_url": "...", "versions_url": "...", "role": "owner", "capabilities": { ... } }
|
|
94
98
|
```
|
|
95
99
|
|
|
96
|
-
Take the returned `
|
|
100
|
+
Take the returned `fork_id` and open the Trackpad Editor at:
|
|
97
101
|
|
|
98
102
|
```
|
|
99
|
-
/editor
|
|
103
|
+
/editor/<templateId>?fork=<forkId>
|
|
100
104
|
```
|
|
101
105
|
|
|
102
106
|
The editor loads `composition.html` and `composition.json` from the fork's working state.
|
|
@@ -139,9 +143,11 @@ Content-Type: application/json
|
|
|
139
143
|
|
|
140
144
|
Two modes:
|
|
141
145
|
|
|
142
|
-
- **`smart`** — uses the caller's Gemini/OpenAI/OpenRouter key. Samples frames, extracts scenes with labels and viral notes, extracts on-screen captions with positions, and detects viral DNA (hook, retention, payoff). Takes 30-60 seconds. Auto-fires GhostCut for subtitle removal in the background.
|
|
146
|
+
- **`smart`** — uses the caller's Gemini/OpenAI/OpenRouter key. Samples frames, extracts scenes with labels, literal visual **descriptions**, and viral notes, extracts on-screen captions with positions, **transcribes the audio track verbatim** (timestamped segments; Gemini first, OpenAI Whisper fallback; skipped when the video has no audio), and detects viral DNA (hook, retention, payoff). Takes 30-60 seconds. Auto-fires GhostCut for subtitle removal in the background.
|
|
143
147
|
- **`time-slice`** — deterministic equal-duration split. Instant. No AI provider needed.
|
|
144
148
|
|
|
149
|
+
**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.
|
|
150
|
+
|
|
145
151
|
Response:
|
|
146
152
|
|
|
147
153
|
```
|
|
@@ -150,24 +156,61 @@ Response:
|
|
|
150
156
|
|
|
151
157
|
If `ghostcut_pending: true`, poll `POST /api/v1/compositions/:forkId/ghostcut-poll` until status transitions to `done` or `failed`.
|
|
152
158
|
|
|
159
|
+
Read-only state (does **not** advance the job or bill): `GET /api/v1/compositions/:forkId/ghostcut`. Returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }`. Use this when you want to know both video URLs without triggering another poll — the AI copilot uses it to pick between the raw upload and the caption-free mirror before feeding a video into a primitive route (image extract, dedupe, trim, ai video edit, etc.). Prefer `mirrored_url` when `status === "done"` and the downstream call should be caption-free; prefer `original_source_url` when the user explicitly wants the raw upload or when `status !== "done"`.
|
|
160
|
+
|
|
161
|
+
## Video context (transcript + scene descriptions)
|
|
162
|
+
|
|
163
|
+
`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**:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
{
|
|
167
|
+
"ok": true,
|
|
168
|
+
"status": "ready" | "none",
|
|
169
|
+
"summary": "...",
|
|
170
|
+
"duration_seconds": 12.4,
|
|
171
|
+
"transcript": {
|
|
172
|
+
"text": "full verbatim transcript",
|
|
173
|
+
"language": "en",
|
|
174
|
+
"provider": "gemini",
|
|
175
|
+
"segments": [{ "start": 0.0, "end": 2.4, "text": "..." }]
|
|
176
|
+
},
|
|
177
|
+
"scenes": [{
|
|
178
|
+
"slug": "hook_reveal",
|
|
179
|
+
"start": 0, "duration": 2.5,
|
|
180
|
+
"label": "Hook",
|
|
181
|
+
"description": "Literal visual description of what's on screen in this scene",
|
|
182
|
+
"viral_note": "...",
|
|
183
|
+
"transcript_excerpt": "what is spoken during this scene"
|
|
184
|
+
}],
|
|
185
|
+
"viral_dna": { ... }
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`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.
|
|
190
|
+
|
|
191
|
+
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.
|
|
192
|
+
|
|
193
|
+
- **Editor chat (frontend AI)** exposes this as the optional `video_context` tool — the copilot calls it on demand.
|
|
194
|
+
- **Desktop agents (Claude Code / Codex)**: `vidfarm edit <template_id>` syncs the same payload to `video-context.json` on disk next to `composition.html` (use `--refetch` to refresh it after a new decompose), or fetch the route directly.
|
|
195
|
+
|
|
153
196
|
## Publish
|
|
154
197
|
|
|
155
|
-
Publishing renders the fork's current working state to an MP4 using shared Remotion Lambda.
|
|
198
|
+
Publishing renders the fork's current working state to an MP4 using shared Remotion Lambda. The route is `/export`:
|
|
156
199
|
|
|
157
200
|
```
|
|
158
|
-
POST /api/v1/compositions/:forkId/
|
|
201
|
+
POST /api/v1/compositions/:forkId/export
|
|
159
202
|
Content-Type: application/json
|
|
160
203
|
|
|
161
|
-
{ "title": "optional", "version": null, "tracer": "optional-
|
|
204
|
+
{ "title": "optional", "version": null, "tracer": "optional-trace-id", "html": "optional HTML to save to working state before rendering" }
|
|
162
205
|
```
|
|
163
206
|
|
|
164
|
-
Returns `{
|
|
207
|
+
Returns 202 with `{ ok, renderId, status, progress, outputUrl, cost, title, version, ... }` where `version` is the snapshot version created for this publish. Failure modes: `402` if the fork owner is not on a paid plan, `409` while GhostCut subtitle removal is still `pending` (retry after `/ghostcut-poll` reports done/failed), `412` if the provider-key preflight fails. Poll:
|
|
165
208
|
|
|
166
209
|
```
|
|
167
210
|
GET /api/v1/compositions/:forkId/renders/:renderId
|
|
168
211
|
```
|
|
169
212
|
|
|
170
|
-
Response includes `{
|
|
213
|
+
Response includes `{ ok, renderId, status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, framesRendered, totalFrames, cost, outputUrl, outputS3Uri, errors }` (`progress` is 0..1). On success, `outputUrl` is the durable MP4 URL.
|
|
171
214
|
|
|
172
215
|
Every publish creates an immutable version snapshot at `versions/<N>/composition.html` and `versions/<N>/composition.json`.
|
|
173
216
|
|
|
@@ -177,7 +220,7 @@ Every publish creates an immutable version snapshot at `versions/<N>/composition
|
|
|
177
220
|
GET /api/v1/compositions/:forkId/versions?limit=100
|
|
178
221
|
```
|
|
179
222
|
|
|
180
|
-
Returns
|
|
223
|
+
Returns `{ versions: [{ version, reason, message, created_by, created_at, render_job_id, composition_url, composition_data_url }], next_cursor }`. Reason is `publish`, `clone`, `manual`, or `migration`. To snapshot the working state manually, `POST /api/v1/compositions/:forkId/versions { "message": "optional" }`.
|
|
181
224
|
|
|
182
225
|
To view a past cut without reverting:
|
|
183
226
|
|
|
@@ -195,12 +238,12 @@ Every fork has a visibility: `private` (default), `unlisted` (legacy), or `publi
|
|
|
195
238
|
PATCH /api/v1/compositions/:forkId/visibility { "visibility": "public" }
|
|
196
239
|
```
|
|
197
240
|
|
|
198
|
-
Public forks are accessible at `/editor
|
|
241
|
+
Public forks are accessible at `/editor/<templateId>?fork=<forkId>` (view-only for non-owners). Public visibility is the simplest sharing surface.
|
|
199
242
|
|
|
200
|
-
Fine-grained sharing uses per-user permissions and share links:
|
|
243
|
+
Fine-grained sharing uses per-user permissions and share links (all body fields are snake_case):
|
|
201
244
|
|
|
202
|
-
- `POST /api/v1/compositions/:forkId/permissions {
|
|
203
|
-
- `POST /api/v1/compositions/:forkId/share-links { role,
|
|
245
|
+
- `POST /api/v1/compositions/:forkId/permissions { grantee_email | grantee_customer_id, role, expires_at? }` — grant a specific registered user access. Exactly one of `grantee_email` or `grantee_customer_id` is required. Roles: `viewer`, `editor`, `publisher`. Returns `{ permission_id, fork_id, grantee_type, grantee_id, role, granted_by, expires_at, created_at }`.
|
|
246
|
+
- `POST /api/v1/compositions/:forkId/share-links { role, expires_at? }` — create a token URL. Anyone with the token gets the role. Returns `{ token, share_url, role, expires_at, ... }`.
|
|
204
247
|
- `GET /api/v1/compositions/shared/:token` — access via share link
|
|
205
248
|
- `DELETE /api/v1/compositions/:forkId/permissions/:permissionId` — revoke a grant
|
|
206
249
|
- `DELETE /api/v1/compositions/:forkId/share-links/:token` — revoke a link
|
|
@@ -210,10 +253,13 @@ Fine-grained sharing uses per-user permissions and share links:
|
|
|
210
253
|
Directors can clone another fork (their own or a shared one) as a new starting point:
|
|
211
254
|
|
|
212
255
|
```
|
|
213
|
-
POST /api/v1/compositions
|
|
256
|
+
POST /api/v1/compositions/:forkId/clone
|
|
257
|
+
Content-Type: application/json
|
|
258
|
+
|
|
259
|
+
{ "parent_version": "<N or omit for latest>", "title": "optional" }
|
|
214
260
|
```
|
|
215
261
|
|
|
216
|
-
The new fork inherits the parent's HTML/JSON at the specified version and creates a version-1 snapshot with `reason: "clone"`.
|
|
262
|
+
The `:forkId` in the path is the fork being cloned. The new fork inherits the parent's HTML/JSON at the specified version and creates a version-1 snapshot with `reason: "clone"`.
|
|
217
263
|
|
|
218
264
|
## Delete a fork
|
|
219
265
|
|
|
@@ -227,8 +273,8 @@ Soft-delete only. The fork's `deletedAt` is set. Versions and share links are pr
|
|
|
227
273
|
|
|
228
274
|
Vidfarm charges directly in USD from the caller's wallet. There are no credits.
|
|
229
275
|
|
|
230
|
-
- **Wallet top-up** — Stripe checkout via `POST /
|
|
231
|
-
- **Balance** — `GET /api/v1/user/me/wallet`
|
|
276
|
+
- **Wallet top-up** — Stripe checkout via the Settings UI, backed by `POST /settings/wallet/funding-link` (browser session; returns `{ checkout_url, client_reference_id }`)
|
|
277
|
+
- **Balance & history** — shown on the Settings page; billing events via `GET /u/:customerId/settings/wallet/events` (browser session). There is no `/api/v1/user/me/wallet` REST endpoint.
|
|
232
278
|
- **Cost anchors**:
|
|
233
279
|
- Publish (Remotion Lambda render): typically $0.01 – $0.10 per MP4 depending on length/resolution
|
|
234
280
|
- Auto-decompose smart mode: pass-through of caller's AI provider spend
|
|
@@ -245,14 +291,14 @@ Framing:
|
|
|
245
291
|
|
|
246
292
|
For agents that operate Vidfarm headlessly, the typical loop is:
|
|
247
293
|
|
|
248
|
-
1. `POST /api/v1/compositions {
|
|
294
|
+
1. `POST /api/v1/compositions { template_id }` → fork
|
|
249
295
|
2. `PUT /api/v1/compositions/:forkId/composition.html` → apply edits
|
|
250
296
|
3. `PATCH /api/v1/compositions/:forkId/composition.json` → update metadata
|
|
251
|
-
4. `POST /api/v1/compositions/:forkId/
|
|
297
|
+
4. `POST /api/v1/compositions/:forkId/export { tracer }` → render
|
|
252
298
|
5. Poll `GET /api/v1/compositions/:forkId/renders/:renderId` until `SUCCEEDED`
|
|
253
|
-
6. Use `
|
|
299
|
+
6. Use `outputUrl` in the next stage
|
|
254
300
|
|
|
255
|
-
|
|
301
|
+
Send a stable `tracer` on export so retries are traceable and filterable in job history — but note that submission is **not idempotent**: every POST creates a new job (and a new charge) even with the same tracer. Do not blind-retry expensive submissions; check the render status first.
|
|
256
302
|
|
|
257
303
|
## Local dev loop with `vidfarm-devcli`
|
|
258
304
|
|
|
@@ -271,11 +317,16 @@ Flags: `--port`, `--dir`, `--host`, `--fork`, `--api-key` (or set `VIDFARM_API_K
|
|
|
271
317
|
|
|
272
318
|
The dev source is persisted in `localStorage.VIDFARM_DEV_SOURCE`, so you don't need to keep the `?dev=` param in the URL after the first load.
|
|
273
319
|
|
|
320
|
+
`dev-serve` (`vidfarm-devcli` 0.3.2+) is realtime-safe:
|
|
321
|
+
- Writes coming through `PUT /composition.html` / `PATCH /composition.json` **do not loop back** as reload events — the server suppresses filesystem notifications for the just-written file for 500ms so the editor's own save never triggers a self-reload.
|
|
322
|
+
- The SSE `hello` event carries an `instance` id + a monotonic `cursor`. If the client reconnects after a server restart the browser detects the new instance and fires a `vidfarm:dev-reload` so the editor resyncs from disk. A 20s heartbeat keeps the SSE connection alive through intermediate proxies.
|
|
323
|
+
- Port collisions surface as a clear `[dev-serve] port <n> is already in use` message instead of a raw stack trace. Request bodies are capped at 8MB and time out after 15s so a stuck client can't hang the server. File paths are canonicalised so `../etc/passwd`-style paths cannot escape `--dir`.
|
|
324
|
+
|
|
274
325
|
## What NOT to do
|
|
275
326
|
|
|
276
327
|
- Do **not** manipulate composition HTML by string concatenation. Always parse, edit, and re-serialize the DOM. The editor and runtime rely on `data-start`, `data-duration`, `data-track-index`, `data-hf-id`, `data-composition-id` attributes being well-formed.
|
|
277
328
|
- Do **not** save AI provider keys in the composition HTML or JSON. They belong in the caller's provider-keys record.
|
|
278
|
-
- Do **not** call Remotion directly. Publishing goes through
|
|
329
|
+
- Do **not** call Remotion directly. Publishing goes through `POST /api/v1/compositions/:forkId/export` so billing, retries, cost caps, and Lambda quotas are enforced.
|
|
279
330
|
- Do **not** re-invent scene manipulation as REST commands (`AddScene`, `RemoveScene`, `UpdateSceneField`). The Trackpad Editor is the surface for that. The pattern is: mutate the composition HTML DOM, PUT it back.
|
|
280
331
|
|
|
281
332
|
## Alpha posture
|
|
@@ -283,7 +334,7 @@ The dev source is persisted in `localStorage.VIDFARM_DEV_SOURCE`, so you don't n
|
|
|
283
334
|
Vidfarm is in active development. Endpoints, response shapes, and the editor UI can change. When something behaves unexpectedly:
|
|
284
335
|
|
|
285
336
|
- prefer `GET /api/v1/compositions/:forkId` over cached state
|
|
286
|
-
- treat `
|
|
337
|
+
- treat `latest_version` as the source of truth for what the editor loaded
|
|
287
338
|
- if a publish fails, check `renders/:renderId` for the error phase and stderr before retrying
|
|
288
339
|
|
|
289
340
|
## Default assistance pattern
|
|
@@ -291,7 +342,7 @@ Vidfarm is in active development. Endpoints, response shapes, and the editor UI
|
|
|
291
342
|
When a director asks "make me a video", the default sequence is:
|
|
292
343
|
|
|
293
344
|
1. Ask what source material and what template style they want
|
|
294
|
-
2. Discover a matching template with `GET /
|
|
345
|
+
2. Discover a matching template with `GET /discover/feed`
|
|
295
346
|
3. Fork it
|
|
296
347
|
4. If they want an entirely new source video, guide them to `auto-decompose` first
|
|
297
348
|
5. Open the Trackpad Editor and let them make edits
|
|
@@ -312,7 +363,7 @@ Example:
|
|
|
312
363
|
|
|
313
364
|
```bash
|
|
314
365
|
curl -X POST "$VIDFARM_BASE/api/v1/primitives/images/remove-background" \
|
|
315
|
-
-H "
|
|
366
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" \
|
|
316
367
|
-H "content-type: application/json" \
|
|
317
368
|
-d '{"source_image_url": "https://cdn.example.com/photo.jpg"}'
|
|
318
369
|
```
|
|
@@ -323,6 +374,7 @@ Apply subtle camouflage transforms (zoom, tilt, rotate, saturation, playback spe
|
|
|
323
374
|
|
|
324
375
|
- `POST /api/v1/primitives/media/dedupe`
|
|
325
376
|
- Body: `{ "tracer": "...", "payload": { ...fields... }, "webhook_url"?: "..." }`
|
|
377
|
+
- Note: webhook delivery is not yet active — `webhook_url` is accepted and persisted on the job but never fired. Poll the job endpoints (`GET /api/v1/primitives/jobs/:jobId`) for completion.
|
|
326
378
|
- Response: standard primitive job. Poll to completion, then read `primary_file_url` (also `video.file_url` for MP4 or `image.file_url` for stills)
|
|
327
379
|
- Billing: metered as a Remotion render (via `remotion_lambda` on prod, local fallback in dev)
|
|
328
380
|
|
|
@@ -353,7 +405,7 @@ Video example (camouflage a reused clip):
|
|
|
353
405
|
|
|
354
406
|
```bash
|
|
355
407
|
curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
|
|
356
|
-
-H "
|
|
408
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" \
|
|
357
409
|
-H "content-type: application/json" \
|
|
358
410
|
-d '{
|
|
359
411
|
"tracer": "dedupe-2026-07-01-abc",
|
|
@@ -379,7 +431,7 @@ Image example:
|
|
|
379
431
|
|
|
380
432
|
```bash
|
|
381
433
|
curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
|
|
382
|
-
-H "
|
|
434
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" \
|
|
383
435
|
-H "content-type: application/json" \
|
|
384
436
|
-d '{
|
|
385
437
|
"tracer": "dedupe-still-01",
|
package/SKILL.platform.md
CHANGED
|
@@ -66,7 +66,8 @@ compositions/forks/<forkId>/
|
|
|
66
66
|
│ ├── composition.html # current editor state
|
|
67
67
|
│ ├── composition.json # current metadata (scenes, captions, viral DNA)
|
|
68
68
|
│ ├── manifest.json # fork identity + genealogy pointer
|
|
69
|
-
│
|
|
69
|
+
│ ├── ghostcut.json # GhostCut subtitle-removal task state (optional)
|
|
70
|
+
│ └── smart-decompose.json # raw smart-decompose snapshot: scenes (with visual descriptions), captions, viral DNA, audio transcript (optional)
|
|
70
71
|
└── versions/<version>/
|
|
71
72
|
├── composition.html
|
|
72
73
|
├── composition.json
|
|
@@ -161,6 +162,7 @@ All three files (`composition.html`, `composition.json`, `manifest.json`) are tr
|
|
|
161
162
|
- **Player adapter** — exposes `window.__player = { play(), pause(), seek(t), setTime(t), duration, playing, playbackRate, previewMuted, refreshClips() }` for the editor to control the iframe
|
|
162
163
|
- **Media coordination** — respects `data-playback-start`, `data-volume`, and clip-specific mute
|
|
163
164
|
- **Selection sync** — listens for `postMessage({ source: "hf-parent", ... })` from the parent editor to highlight/deselect clips
|
|
165
|
+
- **Origin-scoped messaging** — the runtime rejects inbound `message` events unless `event.source === window.parent`, so sibling iframes / popups cannot spoof play/pause/seek by forging `data.source`. Outbound `window.parent.postMessage(...)` calls are pinned to the parent's origin (seeded from `document.referrer` and refreshed from the first accepted control message) instead of `"*"`, so preview frame coordinates and layer ids no longer leak to any embedding third-party page.
|
|
164
166
|
|
|
165
167
|
The same runtime is used inside the Trackpad Editor iframe AND when a published fork is embedded elsewhere. It is agnostic to whether it's being edited or viewed.
|
|
166
168
|
|
|
@@ -182,13 +184,13 @@ The editor is a single monolithic React component (~9k lines). It builds cleanly
|
|
|
182
184
|
|
|
183
185
|
Publish flow:
|
|
184
186
|
|
|
185
|
-
1. Client `POST /api/v1/compositions/:forkId/publish
|
|
187
|
+
1. Client `POST /api/v1/compositions/:forkId/export` (the route is named `export`; the concept is "publish")
|
|
186
188
|
2. API stamps `data-composition-id="<forkId>@v<N>"` into the composition HTML, PUTs the stamped HTML to working, and reads composition.json
|
|
187
189
|
3. API resolves the caller's provider keys (preflight check for required models)
|
|
188
190
|
4. API creates a `job` record and invokes the `hyperframes_render` primitive Lambda
|
|
189
191
|
5. The primitive Lambda uploads the composition as a Remotion site, calls `renderMediaOnLambda` with the shared Remotion render Lambda ARN, waits for the MP4 to land in S3
|
|
190
192
|
6. On success, the primitive returns `{ primary_file_url: "s3://.../output.mp4" }`, the API creates a `ForkVersionRecord` with `reason: "publish"`, writes the snapshot to `versions/<N>/`, and updates the fork's `latestVersion` and `currentRenderJobId`
|
|
191
|
-
7. Client polls `GET /api/v1/compositions/:forkId/renders/:renderId` for `{
|
|
193
|
+
7. Client polls `GET /api/v1/compositions/:forkId/renders/:renderId` for `{ status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, outputUrl, cost }` (see `adaptJobToPublishStatus` in `src/app.ts`)
|
|
192
194
|
|
|
193
195
|
The Remotion Lambda is **shared across all templates and forks** — it's an infrastructure primitive, not a per-template deployment. Its ARN is passed to the Vidfarm stack as an env var.
|
|
194
196
|
|
|
@@ -197,9 +199,19 @@ The Remotion Lambda is **shared across all templates and forks** — it's an inf
|
|
|
197
199
|
`POST /api/v1/compositions/:forkId/auto-decompose` splits a raw source video into a Trackpad-editable composition. Two modes:
|
|
198
200
|
|
|
199
201
|
- **`time-slice`** (deterministic) — `src/hyperframes/composition.ts`'s `decomposeUndecomposedComposition()` splits the source duration into N equal-length scenes, wraps each in a `<div data-vf-timeline-proxy="video">` layer with `data-start`/`data-duration`/`data-playback-start`. Instant. No AI.
|
|
200
|
-
- **`smart`** (AI) — `src/services/hyperframes.ts`'s `smartDecomposeVideo()` downloads the source, samples frames, calls Gemini/OpenAI/OpenRouter vision (in priority order) using the caller's saved keys, extracts scenes with labels and viral notes, extracts on-screen captions with position, and generates viral DNA. Then it also submits an async **GhostCut** subtitle-removal task if the source has hardcoded subs. Takes 30-60s + async ghostcut poll.
|
|
202
|
+
- **`smart`** (AI) — `src/services/hyperframes.ts`'s `smartDecomposeVideo()` downloads the source, samples frames, calls Gemini/OpenAI/OpenRouter vision (in priority order) using the caller's saved keys, extracts scenes with labels, literal visual **descriptions**, and viral notes, extracts on-screen captions with position, and generates viral DNA. It also demuxes the audio track to WAV and **transcribes it verbatim** with timestamped segments (`transcribeAudioWithFallback()`: Gemini inline-audio first, OpenAI Whisper fallback; non-fatal — a silent video or transcription failure yields `transcript: null`). Then it also submits an async **GhostCut** subtitle-removal task if the source has hardcoded subs. Takes 30-60s + async ghostcut poll.
|
|
201
203
|
|
|
202
|
-
Both modes update the composition.html and composition.json in place, and create a fork version snapshot with `reason: "manual"`.
|
|
204
|
+
Both modes update the composition.html and composition.json in place, and create a fork version snapshot with `reason: "manual"`. Smart mode additionally persists the raw result — scenes with descriptions, captions, viral DNA, and the transcript — to `working/smart-decompose.json`.
|
|
205
|
+
|
|
206
|
+
### Video context (transcript + scene descriptions)
|
|
207
|
+
|
|
208
|
+
`GET /api/v1/compositions/:forkId/video-context.json` assembles a read-only, non-billing view over `smart-decompose.json`: the full verbatim transcript (`{ text, language, segments[] }`), every scene with its literal visual `description` and a computed `transcript_excerpt` (transcript segments overlapping the scene's time window), plus summary and viral DNA. Returns `{ status: "none" }` for forks that were never smart-decomposed. Same surface, three consumers:
|
|
209
|
+
|
|
210
|
+
1. **Editor chat** — the optional `video_context` tool (registered alongside `http_request` in both `src/app.ts` and `infra/lambda/editor-chat.ts`) fetches it on demand; the shared system prompt in `src/editor-chat.ts` tells the model when to reach for it (what does the video say/show, caption/dub matching, scene-level edit planning).
|
|
211
|
+
2. **`http_request`** — the route lives under `COMPOSITIONS_PREFIX`, so chat can also GET it directly (e.g. for a different fork).
|
|
212
|
+
3. **Desktop agents (Claude Code / Codex)** — the devcli (`src/cli.ts`) syncs it to `video-context.json` on disk next to `composition.html` during `vidfarm edit`; `--refetch` refreshes it after a new decompose.
|
|
213
|
+
|
|
214
|
+
**Source-duration guardrail.** `probeMediaAsset()` in `src/services/media-processing.ts` enforces a hard cap of `MAX_PROBE_DURATION_SECONDS` (120s). If the probed source exceeds it, the probe throws `MediaDurationExceededError` and the route responds with `400 { code: "source_too_long", duration_seconds, max_duration_seconds }`. The same cap applies to inspiration ingest (`ensureInspirationReadyThenFork`) — over-long submissions land as `InspirationRecord { status: "failed", errorMessage }` without ever creating a Template or fork. This is baked into the probe (not the route) so every consumer — auto-decompose, ingest, `video_probe` / `audio_probe` primitives — is protected from repeated probes of long videos being used to burn Lambda / ffmpeg / vision-API budget.
|
|
203
215
|
|
|
204
216
|
## GhostCut integration
|
|
205
217
|
|
|
@@ -211,6 +223,8 @@ GhostCut is an external AI subtitle-removal service. Vidfarm proxies to it via `
|
|
|
211
223
|
|
|
212
224
|
State is persisted per fork at `compositions/forks/<forkId>/working/ghostcut.json`. The client polls `POST /api/v1/compositions/:forkId/ghostcut-poll` every few seconds during smart decompose.
|
|
213
225
|
|
|
226
|
+
Read-only companion: `GET /api/v1/compositions/:forkId/ghostcut` returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }` without touching GhostCut. Non-billing. Exposed for two reasons: (1) the editor chat harness / AI copilot uses it to look up both video URLs on demand so downstream primitive calls can pick raw-upload vs caption-free-mirror without hardcoding either in chat context; (2) it's `http_request`-allowlisted under `COMPOSITIONS_PREFIX` in `src/app.ts`, which is why the editor chat system prompt (`src/editor-chat.ts`) directs the model to call it before feeding a video into a primitive route. The AI is instructed **not** to call `POST /ghostcut-poll` unless the user explicitly asks to advance subtitle removal.
|
|
227
|
+
|
|
214
228
|
Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-through cost: `GHOSTCUT_USD_PER_30S` (default 0.10).
|
|
215
229
|
|
|
216
230
|
## Editor Chat
|
|
@@ -253,7 +267,7 @@ The billing Lambda has its own custom domain because Stripe webhooks require a s
|
|
|
253
267
|
|
|
254
268
|
There are two kinds of things directors can fork:
|
|
255
269
|
|
|
256
|
-
1. **Draft templates**
|
|
270
|
+
1. **Draft templates** — pre-decomposed compositions checked into the repo under `templates/vidfarm_template_XXXX/`. Each has a `composition.html`, `composition.json`, `template.config.json`, a `SKILL.md`, and a `src/` if it needs custom Remotion. Registered templates open in the browser at `/editor/:templateId` (there is no `/hyperframes/:slug` route).
|
|
257
271
|
2. **Submitted videos** — raw uploads from directors, decomposed on demand via auto-decompose.
|
|
258
272
|
|
|
259
273
|
Templates are static. Compositions/forks are the mutable working copies.
|
|
@@ -286,10 +300,10 @@ CDK stacks are under `infra/cdk/`:
|
|
|
286
300
|
Deploy commands are in `package.json`:
|
|
287
301
|
|
|
288
302
|
- `npm run cdk:synth:staging-serverless`
|
|
289
|
-
- `npm run cdk:deploy:staging-serverless`
|
|
290
|
-
- `npm run cdk:deploy:prod`
|
|
303
|
+
- `npm run cdk:deploy:staging-serverless` (and `npm run cdk:deploy:staging-serverless:hotswap` for Lambda-code-only iteration)
|
|
304
|
+
- `npm run cdk:deploy:prod-serverless` (and `npm run cdk:deploy:prod-serverless:hotswap`)
|
|
291
305
|
|
|
292
|
-
Never call bare `cdk deploy`. Always go through the npm scripts
|
|
306
|
+
Never call bare `cdk deploy`. Always go through the npm scripts — the `cdk:*` scripts load `.env.<stage>` via `dotenv-cli` (`dotenv -e .env.<stage> -- npx aws-cdk ...`) and pick the right stack app.
|
|
293
307
|
|
|
294
308
|
Deploy order: **staging first**, then production. Never skip staging.
|
|
295
309
|
|
|
@@ -314,7 +328,7 @@ Top-level:
|
|
|
314
328
|
- `templates/` — draft template sources
|
|
315
329
|
- `auto-create-hyperframe-templates/` — extractor prompts / schemas for new template creation
|
|
316
330
|
- `scripts/` — one-shot ops scripts and migration scripts
|
|
317
|
-
- `data/` — local dev
|
|
331
|
+
- `data/` — local dev storage (gitignored). Also holds leftover `*.sqlite` / `vidfarm.db` files from the retired SQLite/EC2 era — the runtime no longer reads SQLite anywhere; DynamoDB is the only database.
|
|
318
332
|
- `.env.staging`, `.env.production` — stage config (gitignored)
|
|
319
333
|
|
|
320
334
|
Legacy but still active:
|
|
@@ -332,7 +346,7 @@ Legacy but still active:
|
|
|
332
346
|
|
|
333
347
|
## Where to look for things
|
|
334
348
|
|
|
335
|
-
- Composition endpoints — `src/app.ts`
|
|
349
|
+
- 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)
|
|
336
350
|
- Fork CRUD & permissions — `src/services/serverless-records.ts`, `src/services/fork-access.ts`
|
|
337
351
|
- Storage keys + public-read prefix list — `src/services/storage.ts`
|
|
338
352
|
- Fork manifest.json helper — `src/services/fork-manifest.ts`
|
|
@@ -363,10 +377,13 @@ vidfarm-devcli dev-serve --dir ./my-fork --port 4321
|
|
|
363
377
|
|
|
364
378
|
Edit flags: `--port`, `--dir`, `--host`, `--fork`, `--api-key` (or `VIDFARM_API_KEY`), `--share`, `--refetch`.
|
|
365
379
|
|
|
366
|
-
`dev-serve` behavior:
|
|
380
|
+
`dev-serve` behavior (`vidfarm-devcli` 0.3.2+):
|
|
367
381
|
- Serves `composition.html` / `composition.json` / `manifest.json` (and `/versions/{n}/…`) with CORS `*`.
|
|
368
382
|
- Accepts `PUT /composition.html` and `PATCH /composition.json`.
|
|
369
|
-
- Watches the dir with `fs.watch`, coalesces bursts, fans out reload events via `GET /_events` SSE.
|
|
383
|
+
- Watches the dir with `fs.watch`, coalesces bursts (80ms), fans out reload events via `GET /_events` SSE.
|
|
384
|
+
- Reload loop prevention: any successful `PUT` / `PATCH` marks the target file "self-write suppressed" for 500ms so the fs.watch event fired by the write we just performed does NOT round-trip back to the editor as a reload event.
|
|
385
|
+
- SSE `hello` includes `{ at, instance, cursor }`. `instance` changes on every dev-serve restart so a reconnecting client can detect a restart and force a resync; `cursor` is a monotonic counter that bumps on every broadcast reload. Heartbeat `: heartbeat <ts>` every 20s keeps intermediate proxies from timing out the connection.
|
|
386
|
+
- Safety rails: request bodies capped at 8MB with a 15s body-read timeout; path resolution runs through `path.relative(rootDir, ...)` so `versions/../../etc/passwd`-style paths cannot escape the served directory; `EADDRINUSE` produces a plain-English error instead of a raw stack; broadcast iterates a snapshot of the client set so a mid-broadcast disconnect can't skip healthy clients; `SIGINT` + `SIGTERM` both close cleanly with a 2s hard-exit fallback.
|
|
370
387
|
|
|
371
|
-
The editor side lives in `demo/src/dev-mode.ts` — a self-installing module imported at the top of `demo/src/main.tsx`. It reads `?dev=<url>` (persisted to `localStorage.VIDFARM_DEV_SOURCE`), monkey-patches `window.fetch` to rewrite `/api/v1/compositions/{forkId}/*` → `${devSource}/*`, injects a persistent orange banner with an Exit button, and dispatches `vidfarm:dev-reload` on SSE reload events. `HyperframesStudioEditor.tsx` listens for that event and re-runs the composition fetch effect with `cache: "reload"`.
|
|
388
|
+
The editor side lives in `demo/src/dev-mode.ts` — a self-installing module imported at the top of `demo/src/main.tsx`. It reads `?dev=<url>` (persisted to `localStorage.VIDFARM_DEV_SOURCE`), monkey-patches `window.fetch` to rewrite `/api/v1/compositions/{forkId}/*` → `${devSource}/*`, injects a persistent orange banner with an Exit button, and dispatches `vidfarm:dev-reload` on SSE reload events. It also caches the `hello.instance` and re-dispatches `vidfarm:dev-reload` with `{ reason: "server-restart" }` when a reconnect sees a different instance. `HyperframesStudioEditor.tsx` listens for that event and re-runs the composition fetch effect with `cache: "reload"`.
|
|
372
389
|
- Lambdas — `infra/lambda/*.ts`
|
package/dist/src/cli.js
CHANGED
|
@@ -127,7 +127,10 @@ async function resolveForkId(input) {
|
|
|
127
127
|
return null;
|
|
128
128
|
}
|
|
129
129
|
async function fetchCompositionFiles(input) {
|
|
130
|
-
|
|
130
|
+
// video-context.json carries the source video's audio transcript and
|
|
131
|
+
// per-scene visual descriptions so desktop agents (Claude Code, Codex)
|
|
132
|
+
// can ground edits in what the video actually says and shows.
|
|
133
|
+
const files = ["composition.html", "composition.json", "manifest.json", "video-context.json"];
|
|
131
134
|
for (const filename of files) {
|
|
132
135
|
const target = path.join(input.dir, filename);
|
|
133
136
|
if (existsSync(target) && !input.refetch) {
|
|
@@ -147,10 +150,12 @@ async function fetchCompositionFiles(input) {
|
|
|
147
150
|
}
|
|
148
151
|
function buildAuthHeaders(input) {
|
|
149
152
|
const headers = { accept: "text/html,application/json" };
|
|
153
|
+
// The server reads exactly these header names (see requireAuth and
|
|
154
|
+
// readShareToken in src/app.ts) — Bearer/authorization is not supported.
|
|
150
155
|
if (input.apiKey)
|
|
151
|
-
headers
|
|
156
|
+
headers["vidfarm-api-key"] = input.apiKey;
|
|
152
157
|
if (input.shareToken)
|
|
153
|
-
headers["
|
|
158
|
+
headers["vidfarm-share-token"] = input.shareToken;
|
|
154
159
|
return headers;
|
|
155
160
|
}
|
|
156
161
|
function printEditorBanner(input) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mevdragon/vidfarm-devcli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. Point it at a template id, edit composition.html on disk (Claude Code, Codex, etc.), preview live in the browser at https://vidfarm.cc/editor/.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"build": "npm run build:frontend && npm run build:hyperframes-editor && node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
40
40
|
"start": "node --import ./dist/src/instrument.js --enable-source-maps dist/src/index.js",
|
|
41
41
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
42
|
-
"cdk:
|
|
43
|
-
"cdk:
|
|
44
|
-
"cdk:deploy:
|
|
45
|
-
"cdk:
|
|
46
|
-
"cdk:deploy:
|
|
42
|
+
"cdk:deploy:prod-serverless": "npm run build && dotenv -e .env.production -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js'",
|
|
43
|
+
"cdk:synth:staging-serverless": "npm run build && dotenv -e .env.staging -- npx aws-cdk synth --app 'node dist/infra/cdk/bin/vidfarm-serverless-staging.js'",
|
|
44
|
+
"cdk:deploy:staging-serverless": "npm run build && dotenv -e .env.staging -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-serverless-staging.js'",
|
|
45
|
+
"cdk:deploy:staging-serverless:hotswap": "npm run build && dotenv -e .env.staging -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-serverless-staging.js' --hotswap-fallback --require-approval never",
|
|
46
|
+
"cdk:deploy:prod-serverless:hotswap": "npm run build && dotenv -e .env.production -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js' --hotswap-fallback --require-approval never",
|
|
47
47
|
"prepack": "npm run build"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
@@ -92,6 +92,7 @@
|
|
|
92
92
|
"@types/react-dom": "^18.3.7",
|
|
93
93
|
"aws-cdk-lib": "^2.196.0",
|
|
94
94
|
"constructs": "^10.4.2",
|
|
95
|
+
"dotenv-cli": "^11.0.0",
|
|
95
96
|
"tsx": "^4.19.4",
|
|
96
97
|
"typescript": "^5.8.3",
|
|
97
98
|
"zustand": "^5.0.13"
|