@mevdragon/vidfarm-devcli 0.20.6 → 0.20.8
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/.agents/skills/editor-capabilities/SKILL.md +15 -1
- package/.agents/skills/vidfarm-director/SKILL.md +106 -0
- package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
- package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
- package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
- package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
- package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
- package/SKILL.director.md +476 -260
- package/dist/src/cli.js +362 -96
- package/dist/src/devcli/clips.js +15 -10
- package/dist/src/devcli/doctor.js +2 -2
- package/dist/src/devcli/local-backend.js +46 -2
- package/dist/src/services/clip-curation/cost.js +5 -1
- package/dist/src/services/clip-curation/gemini.js +10 -0
- package/package.json +30 -10
- package/SKILL.platform.md +0 -432
- package/demo/README.md +0 -28
- package/demo/dist/app.css +0 -1
- package/demo/dist/app.js +0 -1850
- package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
- package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
- package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
- package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
- package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
- package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
- package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
- package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +0 -7
- package/demo/dist/icons/timeline/captions.svg +0 -5
- package/demo/dist/icons/timeline/composition.svg +0 -12
- package/demo/dist/icons/timeline/image.svg +0 -18
- package/demo/dist/icons/timeline/music.svg +0 -10
- package/demo/dist/icons/timeline/text.svg +0 -3
- package/demo/dist/index.html +0 -15
- package/dist/src/account-pages-legacy.js +0 -9396
- package/dist/src/account-pages.js +0 -61
- package/dist/src/app.js +0 -21603
- package/dist/src/composition-runtime.js +0 -1053
- package/dist/src/config.js +0 -217
- package/dist/src/context.js +0 -447
- package/dist/src/dev-app-legacy.js +0 -739
- package/dist/src/dev-app.js +0 -6
- package/dist/src/devcli/migrate-local.js +0 -140
- package/dist/src/devcli/sync.js +0 -368
- package/dist/src/domain.js +0 -5
- package/dist/src/editor-chat-history.js +0 -82
- package/dist/src/editor-chat.js +0 -828
- package/dist/src/editor-dark-theme.js +0 -1128
- package/dist/src/frontend/debug.js +0 -71
- package/dist/src/frontend/discover-client.js +0 -130
- package/dist/src/frontend/discover-store.js +0 -23
- package/dist/src/frontend/file-directory.js +0 -1018
- package/dist/src/frontend/flockposter-cache-store.js +0 -124
- package/dist/src/frontend/homepage-client.js +0 -446
- package/dist/src/frontend/homepage-shared.js +0 -201
- package/dist/src/frontend/homepage-store.js +0 -66
- package/dist/src/frontend/homepage-view.js +0 -705
- package/dist/src/frontend/page-runtime-client.js +0 -132
- package/dist/src/frontend/page-runtime-store.js +0 -9
- package/dist/src/frontend/sentry.js +0 -42
- package/dist/src/frontend/template-editor-chat.js +0 -4181
- package/dist/src/help-page.js +0 -346
- package/dist/src/homepage.js +0 -1458
- package/dist/src/index.js +0 -16
- package/dist/src/instrument.js +0 -30
- package/dist/src/landing-page.js +0 -384
- package/dist/src/page-runtime.js +0 -2
- package/dist/src/page-shell.js +0 -1452
- package/dist/src/primitive-context.js +0 -416
- package/dist/src/primitive-registry.js +0 -3940
- package/dist/src/primitive-sdk.js +0 -4
- package/dist/src/primitives/hyperframes-media.js +0 -108
- package/dist/src/react-page-shell.js +0 -35
- package/dist/src/ready-post-schedule-component.js +0 -1540
- package/dist/src/registry.js +0 -296
- package/dist/src/reskin/agency-page.js +0 -299
- package/dist/src/reskin/calendar-page.js +0 -568
- package/dist/src/reskin/chat-page.js +0 -942
- package/dist/src/reskin/discover-page.js +0 -1788
- package/dist/src/reskin/document.js +0 -1587
- package/dist/src/reskin/help-page.js +0 -357
- package/dist/src/reskin/index-page.js +0 -62
- package/dist/src/reskin/inpaint-clipper-page.js +0 -890
- package/dist/src/reskin/inpaint-page.js +0 -2554
- package/dist/src/reskin/inpaint-video-page.js +0 -1339
- package/dist/src/reskin/job-runs-page.js +0 -477
- package/dist/src/reskin/library-page.js +0 -1634
- package/dist/src/reskin/login-page.js +0 -262
- package/dist/src/reskin/portfolio-page.js +0 -687
- package/dist/src/reskin/pricing-page.js +0 -390
- package/dist/src/reskin/settings-page.js +0 -732
- package/dist/src/reskin/theme.js +0 -711
- package/dist/src/runtime.js +0 -35
- package/dist/src/services/api-call-history.js +0 -249
- package/dist/src/services/auth.js +0 -152
- package/dist/src/services/billing-pricing.js +0 -39
- package/dist/src/services/billing.js +0 -241
- package/dist/src/services/cast.js +0 -127
- package/dist/src/services/chat-threads.js +0 -92
- package/dist/src/services/clip-records.js +0 -250
- package/dist/src/services/clip-search.js +0 -77
- package/dist/src/services/clip-vectors.js +0 -125
- package/dist/src/services/composition-sanitize.js +0 -124
- package/dist/src/services/composition-watch.js +0 -79
- package/dist/src/services/elevenlabs.js +0 -222
- package/dist/src/services/file-directory.js +0 -117
- package/dist/src/services/fork-access.js +0 -93
- package/dist/src/services/fork-manifest.js +0 -42
- package/dist/src/services/ghostcut.js +0 -179
- package/dist/src/services/hyperframes.js +0 -3654
- package/dist/src/services/job-capacity.js +0 -14
- package/dist/src/services/job-logs.js +0 -197
- package/dist/src/services/jobs.js +0 -136
- package/dist/src/services/local-dynamo.js +0 -0
- package/dist/src/services/media-processing.js +0 -766
- package/dist/src/services/primitive-media-lambda.js +0 -280
- package/dist/src/services/providers.js +0 -2748
- package/dist/src/services/rate-limits.js +0 -262
- package/dist/src/services/scene-annotations.js +0 -32
- package/dist/src/services/serverless-auth.js +0 -382
- package/dist/src/services/serverless-jobs.js +0 -1084
- package/dist/src/services/serverless-provider-keys.js +0 -409
- package/dist/src/services/serverless-records.js +0 -1515
- package/dist/src/services/serverless-template-configs.js +0 -75
- package/dist/src/services/storage.js +0 -461
- package/dist/src/services/swipe-customize.js +0 -437
- package/dist/src/services/template-certification.js +0 -413
- package/dist/src/services/template-loader.js +0 -99
- package/dist/src/services/template-runtime-bundles.js +0 -217
- package/dist/src/services/template-sources.js +0 -1017
- package/dist/src/services/upstream.js +0 -248
- package/dist/src/services/video-normalization.js +0 -2
- package/dist/src/services/webhooks.js +0 -62
- package/dist/src/template-editor-pages.js +0 -2576
- package/dist/src/template-editor-shell.js +0 -2893
- package/dist/src/template-sdk.js +0 -4
- package/dist/src/worker.js +0 -17
- package/public/assets/discover-client-app.js +0 -1
- package/public/assets/file-directory-app.js +0 -3
- package/public/assets/homepage-app.js +0 -54
- package/public/assets/homepage-client-app.js +0 -80
- package/public/assets/page-runtime-client-app.js +0 -94
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
- package/src/assets/SELLING_WITH_HOOKS.md +0 -377
- package/src/assets/SELLING_WITH_VSLS.md +0 -606
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
package/SKILL.platform.md
DELETED
|
@@ -1,432 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: vidfarm-platform
|
|
3
|
-
description: Vidfarm platform architecture reference. Use this for questions about how compositions, forks, versions, storage, DynamoDB records, the Trackpad Editor runtime, HyperFrames render, editor-chat, primitives, and billing fit together. This is not a director skill (see SKILL.director.md) and not a runbook — it explains the system.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Vidfarm Platform Architecture
|
|
7
|
-
|
|
8
|
-
Vidfarm is a **serverless composition studio** for short-form video. Directors fork templates, edit compositions on a timeline (the Trackpad Editor), publish MP4s via HyperFrames render (shared cloud Lambda fan-out, or free in-process render on a local serve box), and share results.
|
|
9
|
-
|
|
10
|
-
This document is the architectural reference — what runs where, what data lives where, and how the pieces connect. Use it when planning changes, debugging cross-cutting behavior, or onboarding to the platform.
|
|
11
|
-
|
|
12
|
-
For customer-facing usage patterns, see `SKILL.director.md`.
|
|
13
|
-
|
|
14
|
-
## Runtime shape
|
|
15
|
-
|
|
16
|
-
Vidfarm runs entirely on AWS serverless:
|
|
17
|
-
|
|
18
|
-
- **API** — a single Hono app deployed as a container Lambda behind an API Gateway custom domain per stage
|
|
19
|
-
- **Editor Chat** — a separate Lambda + custom domain for streaming AI editor sessions
|
|
20
|
-
- **Primitive Media** — a separate Lambda + custom domain for standalone media primitives (image gen, video generation, etc.)
|
|
21
|
-
- **Ready Post Zip** — a separate Lambda + custom domain for scheduled/approved-post ZIP downloads
|
|
22
|
-
- **Billing** — a separate Lambda + custom domain for Stripe webhooks, wallet top-ups, and idempotent charge processing
|
|
23
|
-
- **Job Runner** — a Lambda invoked by the API to execute template operations
|
|
24
|
-
- **Job Sweeper** — a scheduled Lambda that stalls / retries / cleans up in-flight jobs
|
|
25
|
-
|
|
26
|
-
Stages:
|
|
27
|
-
|
|
28
|
-
- `staging` — `staging.vidfarm.cc` (+ sibling subdomains for billing, editor-chat, primitive-media, zip)
|
|
29
|
-
- `production` — `vidfarm.cc`
|
|
30
|
-
|
|
31
|
-
Every stage is a separate CDK stack with fully separate DynamoDB tables, S3 buckets, Lambdas, and DNS.
|
|
32
|
-
|
|
33
|
-
There is no EC2, no SQLite, no long-lived worker. Everything scales to zero at idle.
|
|
34
|
-
|
|
35
|
-
## Data model
|
|
36
|
-
|
|
37
|
-
### DynamoDB — single-table
|
|
38
|
-
|
|
39
|
-
One main table per stage: `VidfarmMainTable`. Partitioned by `entity_type`. Records include:
|
|
40
|
-
|
|
41
|
-
- `customer` — director account, wallet, provider keys reference, timestamps
|
|
42
|
-
- `customer_api_key` — API key hash + salt
|
|
43
|
-
- `template` — imported template registration (slug, source repo, active release pointer)
|
|
44
|
-
- `template_release` — a specific artifact for a template
|
|
45
|
-
- `job` — an in-flight or completed job execution
|
|
46
|
-
- `job_log_event` — structured log lines per job (transient — pruned)
|
|
47
|
-
- `api_call_history` — request audit trail (transient)
|
|
48
|
-
- `editor_chat_message` — chat history (transient)
|
|
49
|
-
- `serverless_record#composition_fork` — an editable composition fork
|
|
50
|
-
- `serverless_record#fork_version` — an immutable version snapshot for a fork
|
|
51
|
-
- `serverless_record#fork_permission` — a per-user grant on a fork
|
|
52
|
-
- `serverless_record#fork_share_link` — a share-token URL for a fork
|
|
53
|
-
- `submitted_video` — a source video uploaded by a director
|
|
54
|
-
- `provider_key_lease` — short-lived AI key leases for job runners
|
|
55
|
-
- `billing_event` — wallet transactions and Stripe events
|
|
56
|
-
|
|
57
|
-
Query patterns use GSIs on `customerId`, `forkId`, `granteeId`, `slug`, and `status`.
|
|
58
|
-
|
|
59
|
-
### S3 — composition storage
|
|
60
|
-
|
|
61
|
-
Bucket per stage. Layout is **ownership-agnostic** — the customerId does not appear in the S3 key.
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
compositions/forks/<forkId>/
|
|
65
|
-
├── working/
|
|
66
|
-
│ ├── composition.html # current editor state
|
|
67
|
-
│ ├── composition.json # current metadata (scenes, captions, viral DNA)
|
|
68
|
-
│ ├── manifest.json # fork identity + genealogy pointer
|
|
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)
|
|
71
|
-
└── versions/<version>/
|
|
72
|
-
├── composition.html
|
|
73
|
-
├── composition.json
|
|
74
|
-
└── manifest.json # frozen version manifest
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
The `compositions/forks/*` prefix is **public-read** in the bucket resource policy — any client with a `forkId` can fetch `composition.html` / `composition.json` / `manifest.json` directly from S3 without a Lambda proxy hop. The `forkId` is treated as an unguessable bearer token (revoking share = generating a new fork). Write access is still gated at the API by `assertForkAccess(..., "edit" | "publish" | "share" | "delete")`. See `isPublicStorageKey` in `src/services/storage.ts`.
|
|
78
|
-
|
|
79
|
-
Additional prefixes:
|
|
80
|
-
|
|
81
|
-
- `renders/vidfarm/<slug>/<render-id>/output.mp4` — published MP4s
|
|
82
|
-
- `submissions/<videoId>/...` — raw source video uploads
|
|
83
|
-
- `template-assets/<slug>/...` — template media
|
|
84
|
-
- `user-attachments/<customerId>/...` — chat panel uploads (only ownership-keyed area remaining)
|
|
85
|
-
- `users/<customerId>/temporary/<folder>/<fileId>/<name>` — ephemeral temp-store uploads (`POST /me/temporary-files/upload`, 90-day TTL). The devcli namescopes throwaway media under a `temp/` folder here so scratch assets stay quarantined and purgeable rather than polluting the durable library.
|
|
86
|
-
- `users/local-media/<uuid>/<name>` — **local-only**, never on S3. On a `vidfarm serve` box (`STORAGE_DRIVER=local`), `vidfarm place --src ./file` copies a local media file into the box's disk store under this key and references it by the box's own `http://localhost:<port>/storage/<key>` URL, so power users can bulk-build compositions from local assets and render in-process for free without uploading anything to S3. These localhost URLs only resolve against the serve box that holds the file (local render/preview only). See `importLocalMediaToServeStore` / `resolvePlaceableMediaSrc` in `src/cli.ts`.
|
|
87
|
-
|
|
88
|
-
Old ownership-keyed composition paths (`compositions/users/<customerId>/forks/...`) were migrated away by `scripts/migrate-compositions-to-ownership-agnostic.ts`. Old `decomposition.json` was renamed to `composition.json` by `scripts/migrate-decomposition-to-composition.ts`. Forks written before the manifest rollout won't have `manifest.json` until their next mutation — reads must tolerate 404.
|
|
89
|
-
|
|
90
|
-
### manifest.json
|
|
91
|
-
|
|
92
|
-
Written next to `composition.html` / `composition.json` in both `working/` and `versions/<n>/`. Lets clients traverse fork genealogy and jump between versions without a DynamoDB round-trip.
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"fork_id": "...",
|
|
97
|
-
"customer_id": "...",
|
|
98
|
-
"is_working": true,
|
|
99
|
-
"version": null,
|
|
100
|
-
"latest_version": 3,
|
|
101
|
-
"parent_fork_id": "fork_abc" | null,
|
|
102
|
-
"parent_version": 2 | null,
|
|
103
|
-
"source_slug_id": "...",
|
|
104
|
-
"source_video_id": "..." | null,
|
|
105
|
-
"visibility": "private" | "unlisted" | "public",
|
|
106
|
-
"title": "...",
|
|
107
|
-
"composition_url": "https://.../composition.html",
|
|
108
|
-
"composition_json_url": "https://.../composition.json",
|
|
109
|
-
"manifest_url": "https://.../manifest.json",
|
|
110
|
-
"parent_manifest_url": "https://.../versions/2/manifest.json" | null,
|
|
111
|
-
"created_at": "...",
|
|
112
|
-
"updated_at": "..."
|
|
113
|
-
}
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
Writes are centralized in `writeForkManifest()` in `src/services/fork-manifest.ts`. `snapshotForkVersion()` writes both the version manifest AND refreshes the working manifest (so `latest_version` stays in sync). Every working-only mutation site (create, PUT html, PATCH json, visibility, decompose, ghostcut swap, export) also calls it.
|
|
117
|
-
|
|
118
|
-
## Composition model
|
|
119
|
-
|
|
120
|
-
A composition is a single HTML document plus a JSON sidecar.
|
|
121
|
-
|
|
122
|
-
**composition.html** is a self-contained document. Top-level structure:
|
|
123
|
-
|
|
124
|
-
```html
|
|
125
|
-
<div data-composition-id="<forkId>@v<version>" data-viral-dna="{...}">
|
|
126
|
-
<div data-vf-timeline-proxy="video" data-src="..." data-start="0" data-duration="12" />
|
|
127
|
-
<div data-hf-id="clip_1" data-layer-kind="video"
|
|
128
|
-
data-start="0" data-duration="3.5" data-track-index="0"
|
|
129
|
-
data-playback-start="0.25" data-src="...">
|
|
130
|
-
...
|
|
131
|
-
</div>
|
|
132
|
-
<div data-hf-id="caption_1" data-layer-kind="caption"
|
|
133
|
-
data-start="0.5" data-duration="2.0" data-track-index="1"
|
|
134
|
-
style="position:absolute;left:5%;top:60%;...">
|
|
135
|
-
HOOK LINE
|
|
136
|
-
</div>
|
|
137
|
-
<!-- ANIMATED caption cue (word-by-word): the layer carries the preset +
|
|
138
|
-
per-cue vars; each word is a span whose inline animation-delay/duration
|
|
139
|
-
IS its spoken window relative to the layer's data-start. -->
|
|
140
|
-
<div data-hf-id="element_cap_x_0" data-layer-kind="caption"
|
|
141
|
-
data-caption-animation="highlight-pop" data-caption-uppercase="1"
|
|
142
|
-
data-start="3.0" data-duration="1.8" data-track-index="2"
|
|
143
|
-
style="...;--vf-cap-hl:#7C3AED;text-transform:uppercase;overflow:visible">
|
|
144
|
-
<span data-vf-text-lines="true"><span data-vf-text-inline="true">
|
|
145
|
-
<span data-cap-word="true" data-word-start="0" data-word-end="0.4"
|
|
146
|
-
style="animation-delay:0s;animation-duration:0.4s">Stop</span>
|
|
147
|
-
<span data-cap-word="true" data-word-start="0.4" data-word-end="1.1"
|
|
148
|
-
style="animation-delay:0.4s;animation-duration:0.7s">scrolling</span>
|
|
149
|
-
</span></span>
|
|
150
|
-
</div>
|
|
151
|
-
<!-- more layers... -->
|
|
152
|
-
<script type="module"><!-- composition-runtime.ts injected inline --></script>
|
|
153
|
-
</div>
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
**composition.json** carries the semantic metadata parsed once at auto-decompose time and used by the editor sidebar, chat context, and publish payload:
|
|
157
|
-
|
|
158
|
-
```json
|
|
159
|
-
{
|
|
160
|
-
"summary": "...",
|
|
161
|
-
"scenes": [{ "slug": "hook_reveal", "start": 0, "duration": 1.5, "label": "...", "viral_note": "..." }],
|
|
162
|
-
"captions": [{ "slug": "caption_1", "text": "...", "start": 0, "duration": 1.5, "x": 5, "y": 60 }],
|
|
163
|
-
"viralDna": { "hook": "...", "retention": "...", "payoff": "...", "emotional_punch": { "core_emotion": "...", "tone": "...", "mechanism": "...", "humor": "...", "delivery": "...", "preserve": [...] }, "preserve": [...], "avoid": [...] },
|
|
164
|
-
"width": 1080,
|
|
165
|
-
"height": 1920,
|
|
166
|
-
"durationSeconds": 10.5,
|
|
167
|
-
"source_assets": [{ "type": "video", "url": "...", "width": ..., "height": ..., "duration": ... }]
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
All three files (`composition.html`, `composition.json`, `manifest.json`) are treated as immutable per version. The working state is mutable.
|
|
172
|
-
|
|
173
|
-
## Composition runtime
|
|
174
|
-
|
|
175
|
-
`src/composition-runtime.ts` generates a browser-side script that gets injected into every composition HTML. It provides:
|
|
176
|
-
|
|
177
|
-
- **Playback control** — reads all `[data-start][data-duration][data-track-index]` layers, toggles their visibility based on the current time, and syncs media element `currentTime` per clip
|
|
178
|
-
- **Player adapter** — exposes `window.__player = { play(), pause(), seek(t), setTime(t), duration, playing, playbackRate, previewMuted, refreshClips() }` for the editor to control the iframe
|
|
179
|
-
- **Media coordination** — respects `data-playback-start`, `data-volume`, and clip-specific mute
|
|
180
|
-
- **Selection sync** — listens for `postMessage({ source: "hf-parent", ... })` from the parent editor to highlight/deselect clips
|
|
181
|
-
- **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.
|
|
182
|
-
|
|
183
|
-
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.
|
|
184
|
-
|
|
185
|
-
## The Trackpad Editor
|
|
186
|
-
|
|
187
|
-
`demo/src/HyperframesStudioEditor.tsx` is the timeline editor. It:
|
|
188
|
-
|
|
189
|
-
- Renders a `<Timeline>` and `<Player>` from `@hyperframes/studio` — a shared package with the timeline UI, drag/trim logic, and player state store (Zustand)
|
|
190
|
-
- Loads `composition.html` + `composition.json` from the fork's working state
|
|
191
|
-
- Renders the composition into a preview iframe using a `blob:` URL
|
|
192
|
-
- Bidirectionally syncs timeline drag → HTML `data-start`/`data-duration` mutations, and inspector edits → text/style changes
|
|
193
|
-
- Persists every commit back to the API as PUT `composition.html` and PATCH `composition.json`
|
|
194
|
-
- Exposes an **Editor Action API** so the Chat panel can invoke typed actions (`add_layer`, `remove_layer`, `set_layer_start`, `set_layer_duration`, `set_layer_style`, `set_layer_keyframes` — script-free CSS `@keyframes` motion on one layer (opacity/translate/scale/rotate over the clip) — `set_captions` — animated word-by-word caption runs — `group_layers`, `ungroup_layers`, `nudge_layers` (relative timeline move), `ripple_edit` (insert/close time at a point, shifting downstream clips), `trim_layer` (move one edge; a left-trim advances the media in-point), `set_layer_zindex` (restack == track index), `replace_composition_html`, etc.)
|
|
195
|
-
- Runs the composition-runtime script inside the preview iframe to keep timeline scrub in sync with the current time. That runtime also scrubs **Ken Burns** image animations (CSS pan/zoom on `[data-kenburns]` `<img>` layers) and **animated captions** (word-by-word `vf-cap-*` animations on `[data-caption-animation]` caption layers — Ken Burns normalizes to clip progress, captions seek to the absolute offset `time − data-start` because each word's inline delay/duration is its spoken window) to the playhead, so the preview matches the render frame-for-frame. Presets and keyframes live in `src/hyperframes/composition.ts` (`KEN_BURNS_CSS`, `CAPTION_ANIM_CSS` + `CAPTION_STYLE_PRESETS`), mirrored into the editor bundle and the serve-time HTML rewrite; `normalizePublishHtml` re-injects both stylesheets (marker-guarded) into legacy compositions at render time. Caption gotcha: the layer's inline `text-transform`/`overflow` must be set on the element (the stylesheet's `[data-caption-uppercase]` rule loses to inline styles), and cue text edits must rebuild the word spans (`refreshCaptionWordSpans`) rather than setting `textContent`. Speech→captions REST surfaces: `primitive:captions` (`POST /api/v1/primitives/audio/captions`, src/primitive-registry.ts) is the composition-agnostic job — STT with `wordTimestamps` (openai/whisper-1 real, others estimated via `estimateCaptionWords`) paged by `src/services/captions.ts` into cues + a ready `set_captions_action`; `POST /api/v1/compositions/:forkId/captions` (src/app.ts, next to the composition.html PUT) is the one-step variant that resolves the fork's narration, transcribes on raw customer keys (`loadCustomerVisionKeys` + `transcribeSpeechWithKey`), applies via `insertCaptionLayers` (shared with the devcli), and persists through the same sanitize path — intentionally NOT watch-suppressed so serve tabs live-morph; not for open cloud editor sessions (auto-save clobber). Scrub-stability guard (both `syncCaptionClip` copies — `src/composition-runtime.ts` + the editor's `runtimeScript()`): Chrome permanently drops a CSS-paused `fill:none` animation once its `currentTime` is pushed past the effect end, so the scrub parks `fill:none` word animations at 0 when the playhead is past their window (visually identical resting state, keeps them alive for scrub-back) while `fill:forwards` presets overshoot to hold their final keyframe; missing word animations are restarted by cycling `animation-name` through `none`. Live element patches that rebuild word spans must re-scrub via the `__player.syncCaptions()` runtime API (the editor calls it after `patchLiveElement`/`previewLiveElement`) or fresh spans sit paused-at-0 (invisible for word-pop/stack-up) until the next seek.
|
|
196
|
-
|
|
197
|
-
The editor is a single monolithic React component (~9k lines). It builds cleanly to a static bundle deployed to CloudFront under `/editor/`.
|
|
198
|
-
|
|
199
|
-
## Publishing (render pipeline)
|
|
200
|
-
|
|
201
|
-
Publish flow:
|
|
202
|
-
|
|
203
|
-
1. Client `POST /api/v1/compositions/:forkId/render` (`/export` is a deprecated alias; the editor button and devcli command are both **Render**; treat "render", "publish", `/render`, and `/export` as the same operation)
|
|
204
|
-
2. API stamps `data-composition-id="<forkId>@v<N>"` into the composition HTML, PUTs the stamped HTML to working, and reads composition.json
|
|
205
|
-
3. API resolves the caller's provider keys (preflight check for required models)
|
|
206
|
-
4. API creates a `job` record and invokes the `hyperframes_render` primitive Lambda
|
|
207
|
-
5. The renderer runs HyperFrames: on the deployed host it stages the composition and dispatches to the shared HyperFrames render state machine (chunked Lambda fan-out); on a local `vidfarm serve` box with no cloud stack it renders in-process (headless Chromium → ffmpeg) for free. See `HyperframesService` in `src/services/hyperframes.ts`.
|
|
208
|
-
6. On success, the primitive returns `{ primary_file_url: "s3://.../output.mp4" }`, the API creates a `ForkVersionRecord` with `reason: "publish"`, writes the snapshot to `versions/<N>/`, updates the fork's `latestVersion` and `currentRenderJobId`, and now also exposes the deterministic public artifact URL immediately as `expected_output_public_url`
|
|
209
|
-
7. Client polls `GET /api/v1/compositions/:forkId/renders/:renderId` for `{ status: "RUNNING" | "SUCCEEDED" | "FAILED", progress, expectedOutputPublicUrl, outputUrl, cost }` (see `adaptJobToPublishStatus` in `src/app.ts`); `expectedOutputPublicUrl` is the stable public S3 URL, while `outputUrl` remains the completion-time field
|
|
210
|
-
|
|
211
|
-
The HyperFrames render state machine is **shared across all templates and forks** — it's an infrastructure primitive, not a per-template deployment. Its ARN/bucket are passed to the Vidfarm stack as env vars (`HYPERFRAMES_RENDER_*`). Remotion has been fully removed.
|
|
212
|
-
|
|
213
|
-
## Auto-decompose
|
|
214
|
-
|
|
215
|
-
`POST /api/v1/compositions/:forkId/auto-decompose` splits a raw source video into a Trackpad-editable composition. Two modes:
|
|
216
|
-
|
|
217
|
-
- **`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.
|
|
218
|
-
- **`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.
|
|
219
|
-
|
|
220
|
-
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`.
|
|
221
|
-
|
|
222
|
-
### Video context (transcript + scene descriptions)
|
|
223
|
-
|
|
224
|
-
`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:
|
|
225
|
-
|
|
226
|
-
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).
|
|
227
|
-
2. **`http_request`** — the route lives under `COMPOSITIONS_PREFIX`, so chat can also GET it directly (e.g. for a different fork).
|
|
228
|
-
3. **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 on-disk edits in what the video says and shows.
|
|
229
|
-
|
|
230
|
-
**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.
|
|
231
|
-
|
|
232
|
-
## GhostCut integration
|
|
233
|
-
|
|
234
|
-
GhostCut is an external AI subtitle-removal service. Vidfarm proxies to it via `src/services/ghostcut.ts`:
|
|
235
|
-
|
|
236
|
-
- `submitSubtitleRemoval(sourceUrl)` — queue the removal task
|
|
237
|
-
- `pollStatus(taskId)` — check status (`pending` | `processing` | `queued` | `done` | `failed`)
|
|
238
|
-
- `mirrorGhostcutVideoToStorage()` — copy the clean output back to Vidfarm S3
|
|
239
|
-
|
|
240
|
-
State is persisted per fork at `compositions/forks/<forkId>/working/ghostcut.json`. The client polls `POST /api/v1/compositions/:forkId/remove-video-captions-poll` (legacy alias: `/ghostcut-poll`) every few seconds during smart decompose.
|
|
241
|
-
|
|
242
|
-
Read-only companion: `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 }` 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 /remove-video-captions-poll` unless the user explicitly asks to advance subtitle removal.
|
|
243
|
-
|
|
244
|
-
Standalone primitive: `POST /api/v1/primitives/videos/remove-captions` (alias: `POST /api/v1/primitives/remove-video-captions`; id `primitive:video_remove_captions` in `src/primitive-registry.ts`) exposes the same GhostCut pipeline for ANY video URL, decoupled from forks and decompose. The job probes the source (duration prices the task), submits, polls GhostCut to completion inside the job runner (12-minute budget, vs the fork routes where the client drives completion), mirrors `captions-removed.mp4` into primitive storage, and bills the `ghostcut_subtitle_removal` cost center per 30s chunk.
|
|
245
|
-
|
|
246
|
-
Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-through cost: `GHOSTCUT_USD_PER_30S` (default 0.10). **Guard:** the primitive hard-fails sources longer than `GHOSTCUT_MAX_DURATION_SEC` (default 900s) — GhostCut is a per-finished-clip tool; long-form "no captions" requests are served by the clip-hunt `avoid_text` scene-selection filter instead, never by laundering a whole long video through GhostCut.
|
|
247
|
-
|
|
248
|
-
## Clip hunting pipeline (long-form → short-form)
|
|
249
|
-
|
|
250
|
-
`POST /raws/scan` starts a Step Functions STANDARD workflow (`…ClipScanWorkflow`): `probe_and_detect` → `Map(process_scene, maxConcurrency 5)` → `finalize`, all three states dispatching the same `infra/lambda/clip-scan.ts` on an `action` field. Business logic lives in the shared `src/services/clip-curation/` core (also consumed by devcli and the Hono API), with the hunt controls in `hunt.ts`:
|
|
251
|
-
|
|
252
|
-
- **`hunt_spec`** — windows (time ranges; ffmpeg input-seeks per window so only the requested footage is DECODED — the actual cost saving), a soft `duration_band` (`resolveDurationBand`: 10→5-20s, 30→20-40s via ±target/3), output `aspect` + `crop_focus` (smart-centre ffmpeg crop applied to clips, thumbnails, AND tagging keyframes so the model judges the framed output), `avoid_text` (rides the keep/drop/merge refine pass as a selection filter), and `source_url` (RapidAPI ingest → the user's temp folder). The spec travels as ONE opaque JsonPath object through the state machine, so new hunt fields are Lambda-code-only changes — no state-machine redeploy.
|
|
253
|
-
- **Sources are temporary** — URL ingests and uploads land at `users/{id}/temporary/clip-sources/…` with a hard 30-day TTL enforced three ways: tag-scoped S3 lifecycle (`vidfarm-temp=1`, tagged server-side at temp-file finalize/upload), native DynamoDB TTL (`expires_at_epoch_seconds`, top-level on the item), and the list-route sweep. Hunted clips (`clips/{owner}/…`) are durable.
|
|
254
|
-
- **Billing: AWS compute ONLY.** Every Lambda invocation post-hoc bills its GB-seconds to the `clip_scan_lambda` cost center (idempotent on `awsRequestId`); `finalize` adds `step_functions_standard` scaled by scene count (3 + N transitions); URL ingest adds the existing `rapidapi_video_download` pass-through. `jobId = scan_id`, and the caller's `tracer` rides every event (gsi2-tracer rollups). The BYOK tagging/embedding spend (gemini/openai/openrouter keys in the state input) is deliberately never wallet-billed. Submission is wallet-gated (`assertWalletCanQueueJobs` → 402); the 202 response carries a pre-scan `compute_estimate`.
|
|
255
|
-
- **Failure reconciliation** — the workflow has no failure hook into records, so `GET /raws/scan/:scanId` lazily `DescribeExecution`s a running scan and flips the scan+source records to `failed` (or `complete`) so pollers converge.
|
|
256
|
-
- **devcli is LOCAL-FIRST** — `vidfarm raws scan` runs local ffmpeg and, by default, the user's local claude/codex CLI subscription (`LocalAgentClipClient`, implementing the same `ClipHuntModelClient` interface as the API providers; refine's model call lives on the client as `decideScenes` to make them swappable). Provider keys are the fallback; `--cloud` is the explicit backup that presign-uploads to the temp folder and drives this pipeline.
|
|
257
|
-
- **Bundling gotcha** — the clip-scan and primitive-media Lambdas bundle `ffmpeg-static`/`ffprobe-static` with `forceDockerBundling: true`: ffmpeg-static downloads the binary for the platform it is installed on, so a mac-local bundle ships a darwin ffmpeg the Lambda cannot exec (`exit 126`). They also need `VIDFARM_DATA_DIR=/tmp/vidfarm` because `config.ts` mkdirs at import.
|
|
258
|
-
- **Exact-subrange clipper** — separate from the AI hunt, the **Clipper** cuts one precise `[start,end]` into a single raw. `POST /raws/resolve-source` `{ source_url }` returns a DIRECT `preview_url` (via `pickBestVideoMedia`, no download/re-host) so the web `/tools/clipper` page can play the source and pick an in/out; `POST /raws/clip-range` `{ source_url|preview_url|temp_file_id, start_sec, end_sec, tracer?, folder_path?, name? }` runs `importSubrangeAsRaw` inline on the request Lambda (fetch source → `extractClip` ffmpeg trim → `storage.putFile clips/<owner>/<id>.mp4` + thumbnail + `clipRecords.putClip`), landing one raw under `/raws/<folder>` (defaults to the tracer). Compute-only (RapidAPI billed only when a social `source_url` must be resolved). Same operation is exposed to the copilot (system prompt in `src/editor-chat.ts`) and devcli (`vidfarm clipper`). The three creative tools now live under `/tools/*` (`/tools/image` masked-image inpaint, `/tools/video` time-scoped video edits, `/tools/clipper`); the legacy `/inpaint/*` paths 302-redirect there, and the `images/inpaint` primitive keeps its name.
|
|
259
|
-
|
|
260
|
-
## Editor Chat
|
|
261
|
-
|
|
262
|
-
`infra/lambda/editor-chat.ts` is a separate Lambda that streams AI-driven edits to the Trackpad Editor. Client opens an SSE stream to `/api/v1/editor-chat`, sends the current composition snapshot + user prompt + attachments, and receives:
|
|
263
|
-
|
|
264
|
-
- assistant text
|
|
265
|
-
- tool calls that map to the Editor Action API (`add_layer`, `set_layer_start`, ...)
|
|
266
|
-
- token usage + billing events
|
|
267
|
-
|
|
268
|
-
The chat model uses the **caller's provider keys**. No Vidfarm-owned model calls happen in this path.
|
|
269
|
-
|
|
270
|
-
Beyond the Editor Action API, the chat harness registers shared tools defined in **two synced copies** (`src/app.ts` for the in-process dev server and `infra/lambda/editor-chat.ts` for the deployed Lambda — keep them in lockstep): `http_request` (allowlisted REST/primitive calls), `frontend_action` (tracer/thread state), `video_context`, `load_skill`, and `browse_files`. `load_skill` is the copilot's knowledge-on-demand seam: it fetches vendored skill-pack content (SKILL.md or a reference file) from the host's public `/skill/:name` + `/skill-pack/:name/files/*` mirror routes (backed by `.agents/skills/`, shipped in the api Docker image), size-capped, with the pack catalog + one-liners defined once in `EDITOR_CHAT_SKILL_PACKS` (`src/editor-chat.ts`) and shared by both copies. Loadable packs are the hyperframes suite (`hyperframes`, `hyperframes-core`, `hyperframes-animation`, `hyperframes-keyframes`, `hyperframes-creative`, `hyperframes-cli`) plus the workflow packs (`embedded-captions`, `product-launch-video`, `faceless-explainer`, `website-to-video`, `general-video`, `motion-graphics`, `slideshow`, `talking-head-recut`, `vidfarm-media`); the `.claude/skills/*` entries are just Claude Code discovery symlinks to the same `.agents/skills/` dirs. In the web editor the copilot applies only the CSS / `@keyframes` + declarative-preset half of the animation/keyframes packs — their JS-adapter techniques are devcli-only. The stream-loop guardrails (`redactUnverifiedJobIds`, `buildEditorChatGuardrailNotice`, async-job handoff text) also live in `src/editor-chat.ts` and are imported by both copies — never re-implement them locally. `editor_action`'s `replace_composition_html` is lint-gated in the tool `execute` via `src/services/composition-lint.ts` (a rejected result carries `rejected: true` + `lint_errors` and omits `action_type`, so the browser never applies it). Web-editor motion is **declarative/CSS only**: the JS runtime adapters (anime.js / GSAP / Lottie / Three.js / TypeGPU) are stripped on save and `replace_composition_html` additionally rejects any `<script>`-bearing HTML, so the copilot authors motion with `set_layer_keyframes` (a CSS `@keyframes` animation on one layer) / `<style>` `@keyframes` + the preset vocabulary; scripted / adapter-driven compositions are a devcli-only (`vidfarm render`) capability. The `editor_context` snapshot the model receives now carries each layer's `transition` / `transition_out` / `transition_duration`, `ken_burns` preset, and `animation` (custom CSS keyframe name), and `recent_action_results` reports successes (`ok: true` + summary) as well as failures; the model can also GET `…/compositions/:forkId/composition.html` for exact markup. `browse_files` reads **and writes** the caller's file directory — `action=list`/`search`/`read`/`write`/`annotate`/`move`/`rename`. `action=rename` renames a file or **folder** in place via `POST /me/directory/rename` (`path` + `new_name`, plus `file_id` for a file — a metadata-only rename that leaves the S3 key/view URL intact). File rename is `/files`/`/temp` only, but **folder rename works in every root including `/raws` and `/approved`** — renaming a `/raws` folder re-points every clip inside it (`clipRecords.moveClipsFolder`) and renaming an `/approved` folder re-points every post (`serverlessRecords.moveReadyPostFolder`), which is how an auto-named folder like `/raws/lvlldspajcg` or `/approved/q3-promo` gets a real name; the `/library/raws` and `/library/approved` explorers each expose the same rename on every folder tile's kebab. `action=move` reorganizes a `/raws` clip (`PATCH /raws/:id`) or an `/approved` post (`PATCH /api/v1/approved/posts/:id`) between folders. There are now **four roots**: `/files`, `/temp`, `/raws`, and `/approved` (finished ready-to-post cuts, foldered — new posts self-file into a folder named after their tracer; `ReadyPostRecord.folderPath`). `action=list` defaults to `path='/'` when `path` is omitted, so the `/raws` (hunted raws), `/temp` (scratch), and `/approved` (published cuts) roots surface alongside the My Files folders instead of being hidden; a `/raws` listing accepts a `content_type` shot-kind filter (`talking_head`/`b_roll`/`product_shot`/`screen_recording`/…) and paginates via `offset`/`limit`. `write` saves a text file (md/txt/csv/json/srt/vtt) via `POST /me/attachments/upload`, or **imports a durable media URL** into the library when `source_url` is passed (how a generated `character_sprite_card.png` gets persisted instead of stranding in a job result). `annotate` sets free-form metadata `notes` on any entry (`PATCH /me/attachments/:id`); notes are **vector-embedded server-side** (BYOK, same gemini→openai auto-pick seam as clip search; no key fail-softs to keyword-only) and `search` runs the hybrid keyword+semantic lookup over every file's name/folder/notes via `POST /me/attachments/search` — the My Files mirror of `/raws/search`. Write+annotate are what let the copilot persist Getting Started context (About.md, awareness-levels.md, persuasive-angles.md, ad-hooks.md) and **recurring characters** — a first-class `/files/characters/<slug>/` trio (`<character_id>.json` manifest — named after the id `character_`+slug, e.g. `character_zara.json` + `character_sprite_card.png` + `character_about.md`) the copilot lists/reads before generating a saved character; the system prompt in `src/editor-chat.ts` drives when to save, annotate, and rename. The devcli mirrors are `vidfarm put-file [--notes]`, `annotate-file`, `files --search`, and `directory rename`.
|
|
271
|
-
|
|
272
|
-
## Primitives
|
|
273
|
-
|
|
274
|
-
Primitives are hosted operations that are NOT templates — they're reusable building blocks:
|
|
275
|
-
|
|
276
|
-
- `create_image`, `edit_image`, `inpaint_image` — image generation
|
|
277
|
-
- `create_html_image` — HyperFrames HTML → PNG (in-process headless Chromium)
|
|
278
|
-
- `remove_background` — RapidAPI-backed AI matting for arbitrary/messy-background photos → durable PNG/WebP
|
|
279
|
-
- `image_remove_background_greenscreen` — CHEAP local `sharp` chroma-key removal for flat solid backgrounds (green screen or any `key_color`) → durable transparent PNG/WebP; compute-only, prefer over `remove_background` when the background is a controlled solid fill
|
|
280
|
-
- `media_overlay` ("create media overlay", Vox-style) — one job: BYOK AI image gen on a forced flat key-color background + local chroma key → ready-to-composite transparent overlay PNG/WebP. The go-to primitive for floating illustration/prop/cut-out overlays
|
|
281
|
-
- `download_media_video` — social/media URL → durable MP4
|
|
282
|
-
- `hyperframes_render` — composition HTML → MP4 (the render primitive publishing uses)
|
|
283
|
-
- `brainstorm` — LLM brainstorm route
|
|
284
|
-
- `music` / `tts` / `stt` — ElevenLabs audio (music, narration, transcription) with a `use_wallet_credits` flag that **defaults true**. `POST /api/v1/primitives/music/generate` (`primitive:music`) generates music from a prompt or composition plan. `POST /api/v1/primitives/audio/speech` (`primitive:tts`) turns text into narration with a promptable style (`instructions`) and an ElevenLabs `voice_id` (browse via `GET /api/v1/primitives/audio/voices` → `providers.listElevenLabsVoices`). `POST /api/v1/primitives/audio/transcribe` (`primitive:stt`) transcribes any video/audio URL (video demuxes through the primitive-media ffmpeg seam first, 64kbps mono, ~40min/20MB cap) into **two formats** — simple subtitles (text + SRT) and speaker-attributed segments. **Routing (`src/services/providers.ts` `resolveAudioAuthPlan`):** `use_wallet_credits: true` (default) → the platform `ELEVENLABS_API_KEY`, billed to the wallet (`elevenlabs_music`/`elevenlabs_tts`/`elevenlabs_stt` cost centers); `false` → the customer's own saved `elevenlabs` key (no charge), else a BYOK gemini/openai/openrouter key (TTS/STT only), else the platform key + wallet. The ElevenLabs calls live in `src/services/elevenlabs.ts`; the openai/gemini/openrouter speech calls in `src/services/speech.ts`. The `elevenlabs` provider key is **audio-only** — it never counts as a qualified text/image/video key. devcli: `vidfarm music`, `vidfarm voices`, and `vidfarm tts`/`vidfarm stt` (LOCAL-FIRST on the user's env key; `--cloud` hits ElevenLabs, `--own-key` uses the user's own key).
|
|
285
|
-
|
|
286
|
-
Directors call primitives when they want a one-off asset before feeding it into a template. Templates call primitives internally as workflow steps. Each primitive declares one of three billing modes: **WALLET** — a metered platform charge (e.g. `remove_background` / `image_remove_background` RapidAPI pass-through, `video_remove_captions` GhostCut, cloud `hyperframes_render`, the local `image_remove_background_greenscreen` chroma key, `media_overlay`, and **platform ElevenLabs music/TTS/STT when `use_wallet_credits` is on** — the default); **BYOK** — the AI spend rides the caller's own provider key and is never wallet-billed (smart decompose, clip tagging/embedding, and music/TTS/STT run with `use_wallet_credits: false` on the user's own ElevenLabs/openai/gemini/openrouter key); or **COMPUTE-ONLY** — only the AWS Lambda / Step-Functions compute bills (clip hunts, the stt audio-demux step). So "all primitives are metered against the caller's wallet" holds only for the compute/platform legs — every BYOK AI call is excepted.
|
|
287
|
-
|
|
288
|
-
## Billing
|
|
289
|
-
|
|
290
|
-
Billing is USD wallet-based, not credits. Every metered operation writes a `billing_event` record and debits the wallet in the same DynamoDB transaction. Stripe webhooks land on the billing Lambda, which validates the signature, marks the invoice as processed idempotently, and credits the wallet.
|
|
291
|
-
|
|
292
|
-
Key mechanics:
|
|
293
|
-
|
|
294
|
-
- Every wallet mutation carries a `tracer` (idempotency key)
|
|
295
|
-
- Provider spend is passed through at cost — no markup on AI provider calls
|
|
296
|
-
- Platform infrastructure (cloud render, GhostCut, storage) has a small safety buffer applied
|
|
297
|
-
- Clip hunts bill **AWS compute only** (`clip_scan_lambda` GB-seconds + `step_functions_standard` transitions, keyed to `jobId = scan_id`); the hunt's AI tagging runs on the caller's own provider keys (BYOK) and never touches the wallet
|
|
298
|
-
- Failed jobs are refunded automatically when the failure is on the platform side
|
|
299
|
-
- **Local `vidfarm serve` render is free + unguarded** — `publishCompositionToRenderer` runs the render in-process (`runPrimitiveJobInProcess`) and skips the wallet/paid guard when no cloud state machine is wired; only cloud render/templates are paid + wallet-guarded
|
|
300
|
-
|
|
301
|
-
**Cost spectrum (director-facing framing; the same guidance lives in `SKILL.director.md` and the editor-chat system prompt).** Per-video cost is set by the *approach*, not a fixed price: FREE (reuse a decomposed template, swap captions/images/existing MP4s, render locally) → ~$0.001–$0.03 (same reuse + cloud render ~$0.01–$0.10) → ~$1 (AI-generate a few scenes) → $10+ (heavy AI generation). Image generation is cheap; AI **video** generation is the expensive end — the copilot is instructed to ask permission before video gen but not before image gen. Smart decompose is a ~$0.10 one-time per new source (provider passthrough + GhostCut) and is avoided by forking already-decomposed catalog templates. The AI defaults to the cheapest approach that meets the goal and surfaces the tradeoff.
|
|
302
|
-
|
|
303
|
-
The billing Lambda has its own custom domain because Stripe webhooks require a stable URL not tied to the main API.
|
|
304
|
-
|
|
305
|
-
## Template model
|
|
306
|
-
|
|
307
|
-
There are two kinds of things directors can fork:
|
|
308
|
-
|
|
309
|
-
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/` for any custom assets. Registered templates open in the browser at `/editor/:templateId` (there is no `/hyperframes/:slug` route).
|
|
310
|
-
2. **Submitted videos** — raw uploads from directors, decomposed on demand via auto-decompose.
|
|
311
|
-
|
|
312
|
-
Templates are static. Compositions/forks are the mutable working copies.
|
|
313
|
-
|
|
314
|
-
## Configuration and env
|
|
315
|
-
|
|
316
|
-
Per-stage config is loaded via `.env.<stage>`. Critical variables:
|
|
317
|
-
|
|
318
|
-
- `AWS_REGION`, AWS credentials
|
|
319
|
-
- `VIDFARM_<STAGE>_PUBLIC_BASE_URL` — the API base
|
|
320
|
-
- `VIDFARM_<STAGE>_ENCRYPTION_SECRET` — DynamoDB field encryption
|
|
321
|
-
- `VIDFARM_<STAGE>_API_KEY_SALT` — API key hashing. Rotating it invalidates previously-issued keys on that stage only; the shared staging table can hold keys hashed under several salts (deployed stage vs local dev boxes). `getVisibleApiKey` (src/app.ts) is salt-aware: it returns the newest active key whose `key_hash` validates under THIS server's salt (minting one if none do), so boot pages / Settings never hand the browser a key the serving box would reject — the editor chat and all its server-executed tools authenticate with that embedded key.
|
|
322
|
-
- `VIDFARM_<STAGE>_BOOTSTRAP_*` — initial admin user
|
|
323
|
-
- `VIDFARM_<STAGE>_RESEND_*` — email
|
|
324
|
-
- `VIDFARM_<STAGE>_SENTRY_*` — Sentry DSN
|
|
325
|
-
- `GHOSTCUT_KEY`, `GHOSTCUT_SECRET` — GhostCut credentials
|
|
326
|
-
- `RAPIDAPI_KEY` — shared RapidAPI credential used by the `video_download` and `image_remove_background` primitives. Same key is set in both `.env.staging` and `.env.production`.
|
|
327
|
-
- Provider fallback keys for platform-owned features that don't have a caller yet (bootstrap only)
|
|
328
|
-
- `HYPERFRAMES_NO_TELEMETRY=1` + `HYPERFRAMES_SKIP_SKILLS=1` — baked into every vidfarm-managed environment (api Docker image, CDK `hyperframesLambdaEnvironment`, devcli serve/spawns). The bundled hyperframes CLI defaults telemetry ON and auto-refreshes skills from its upstream GitHub org; these kill both. Never unset them, and never run `hyperframes auth`/`cloud`/`publish`/`play`/`feedback` — those are third-party account/hosting surfaces vidfarm replaces.
|
|
329
|
-
|
|
330
|
-
Never commit `.env.staging` or `.env.production` — both are gitignored.
|
|
331
|
-
|
|
332
|
-
## Skill packs (vendored mirror)
|
|
333
|
-
|
|
334
|
-
`.agents/skills/` holds the vidfarm-scrubbed skill packs (the HyperFrames suite with every upstream-vendor dependency replaced by vidfarm primitives, plus `vidfarm-media` — the shared audio engine implementing the neutral `audio_request.json` → `audio_engine_meta.json` contract). `.claude/skills/*` are symlinks into it. The api serves the packs publicly: `GET /skill/:name` (SKILL.md), `GET /skill-pack/index.json` (catalog), `GET /skill-pack/:name/manifest.json` (file list + sha256), `GET /skill-pack/:name/files/<path>` (bytes, path-guarded) — `src/app.ts` next to `PUBLIC_SKILL_FILES`. Consumers: `vidfarm skills list|add|update` (devcli installer, writes `skills-lock.json` with reproducible folder hashes) and the web copilot's `load_skill` tool. When editing a pack, edit `.agents/skills/<name>` directly (the canonical copy), keep it free of upstream vendor accounts/endpoints, and re-run `vidfarm skills update`-side hash pinning as needed.
|
|
335
|
-
|
|
336
|
-
## Deployment
|
|
337
|
-
|
|
338
|
-
CDK stacks are under `infra/cdk/`:
|
|
339
|
-
|
|
340
|
-
- `bin/vidfarm-serverless-staging.ts` → `lib/vidfarm-serverless-staging-stack.ts`
|
|
341
|
-
- `bin/vidfarm-prod.ts` → `lib/vidfarm-prod-stack.ts`
|
|
342
|
-
|
|
343
|
-
Deploy commands are in `package.json`:
|
|
344
|
-
|
|
345
|
-
- `npm run cdk:synth:staging-serverless`
|
|
346
|
-
- `npm run cdk:deploy:staging-serverless` (and `npm run cdk:deploy:staging-serverless:hotswap` for Lambda-code-only iteration)
|
|
347
|
-
- `npm run cdk:deploy:prod-serverless` (and `npm run cdk:deploy:prod-serverless:hotswap`)
|
|
348
|
-
|
|
349
|
-
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.
|
|
350
|
-
|
|
351
|
-
Deploy order: **staging first**, then production. Never skip staging.
|
|
352
|
-
|
|
353
|
-
## Migration scripts
|
|
354
|
-
|
|
355
|
-
One-shot scripts under `scripts/`:
|
|
356
|
-
|
|
357
|
-
- `migrate-sqlite-to-dynamodb.ts` — one-time, completed on 2026-06-22. Do not re-run.
|
|
358
|
-
- `migrate-compositions-to-ownership-agnostic.ts` — moves forks from `compositions/users/<customerId>/forks/<forkId>/` to `compositions/forks/<forkId>/working/`. Idempotent. Must be run against each stage where the old layout ever existed.
|
|
359
|
-
- `migrate-decomposition-to-composition.ts` — renames `decomposition.json` → `composition.json` in S3 for all forks + versions. Idempotent. Supports `--delete-old` for cleanup.
|
|
360
|
-
- `copy-dynamodb-prod-to-staging.ts` — copies prod DynamoDB items into staging for testing. Excludes transient rows by default.
|
|
361
|
-
|
|
362
|
-
Run migrations with `--dry-run` first. Always.
|
|
363
|
-
|
|
364
|
-
## Directory reference
|
|
365
|
-
|
|
366
|
-
Top-level:
|
|
367
|
-
|
|
368
|
-
- `src/` — API server + all backend services
|
|
369
|
-
- `demo/` — Trackpad Editor React app
|
|
370
|
-
- `infra/` — CDK stacks and Lambda entry points
|
|
371
|
-
- `templates/` — draft template sources
|
|
372
|
-
- `auto-create-hyperframe-templates/` — extractor prompts / schemas for new template creation
|
|
373
|
-
- `scripts/` — one-shot ops scripts and migration scripts
|
|
374
|
-
- `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.
|
|
375
|
-
- `.env.staging`, `.env.production` — stage config (gitignored)
|
|
376
|
-
|
|
377
|
-
Legacy but still active:
|
|
378
|
-
|
|
379
|
-
- `auto-create-templates/` — extractor prompts for the older template creation flow. Only `extractor-system-prompt.md` and `production-graph.schema.json` are still live (loaded by `demo/scripts/dev.mjs`); the rest is inert docs kept for reference.
|
|
380
|
-
- `src/account-pages-legacy.ts`, `src/dev-app-legacy.ts` — HTML-string page renderers wrapped by `.tsx` React shells (`account-pages.tsx`, `dev-app.tsx`). Still active.
|
|
381
|
-
|
|
382
|
-
## Common failure modes
|
|
383
|
-
|
|
384
|
-
- **Fork loads 404** — check ownership-agnostic migration ran; the fork may still be at the old `compositions/users/.../forks/` path.
|
|
385
|
-
- **Publish stuck in RUNNING** — check the primitive Lambda's CloudWatch logs. HyperFrames asset preflight failures usually mean a broken media URL in composition.html.
|
|
386
|
-
- **Smart decompose falls back to time-slice** — the caller has no provider keys, or all their keys returned errors. Check `provider_key_lease` records.
|
|
387
|
-
- **GhostCut task hangs** — client shows `pending` forever. Check the fork's `ghostcut.json` and the GhostCut dashboard. There is currently no cancel UI; the poll times out at 15 minutes.
|
|
388
|
-
- **Editor edits don't persist** — verify `PUT /composition.html` is 200. If the composition.html doesn't include `data-composition-id`, the API rejects the PUT at publish time.
|
|
389
|
-
|
|
390
|
-
## Where to look for things
|
|
391
|
-
|
|
392
|
-
- Composition endpoints — `src/app.ts`: grep for `COMPOSITIONS_PREFIX` (the block runs from `app.post(COMPOSITIONS_PREFIX` fork creation through the `versions` routes; line numbers drift, always grep the symbol)
|
|
393
|
-
- Fork creation has ONE source-of-truth helper trio in `src/app.ts` — `seedForkWorkingFromFork` (copies the 5 canonical working files, optionally from a pinned version) + `forkCompositionFromTemplateDefault` (fresh from the template's canonical default) + `forkCompositionFromExistingFork` (branch a working copy + snapshot). `POST /compositions` (template entry), `POST /:forkId/fork { from: current|default }` (the unified New-fork endpoint behind the ⋯ menu / AI `fork_composition` / devcli `clone --from`), and the legacy `POST /:forkId/clone` alias all funnel through them, so seed logic stays uniform. grep the helper names.
|
|
394
|
-
- Fork CRUD & permissions — `src/services/serverless-records.ts`, `src/services/fork-access.ts`
|
|
395
|
-
- Storage keys + public-read prefix list — `src/services/storage.ts`
|
|
396
|
-
- Fork manifest.json helper — `src/services/fork-manifest.ts`
|
|
397
|
-
- Composition runtime script — `src/composition-runtime.ts`
|
|
398
|
-
- Trackpad Editor — `demo/src/HyperframesStudioEditor.tsx` (the same-origin live-reload `EventSource` on `/api/v1/dev/events` lives here)
|
|
399
|
-
- Local records/auth disk shim — `src/services/local-dynamo.ts` (`RECORDS_DRIVER=local`)
|
|
400
|
-
- Local live-reload watcher — `src/services/composition-watch.ts` (`fs.watch` → SSE, `STORAGE_DRIVER=local`)
|
|
401
|
-
- Devcli (`vidfarm-devcli`) — `src/cli.ts` (`serve` = boot the full app locally + cloud-seed, the REST-wrapper commands, and the shared request helper)
|
|
402
|
-
- Auto-decompose logic — `src/hyperframes/composition.ts` (time-slice), `src/services/hyperframes.ts` (smart)
|
|
403
|
-
- Publish flow — `src/app.ts` `publishCompositionToRenderer()`
|
|
404
|
-
- Ghostcut — `src/services/ghostcut.ts`
|
|
405
|
-
- CDK stack — `infra/cdk/lib/vidfarm-serverless-staging-stack.ts`, `infra/cdk/lib/vidfarm-prod-stack.ts`
|
|
406
|
-
|
|
407
|
-
## Local editor dev loop (`vidfarm serve`)
|
|
408
|
-
|
|
409
|
-
`@mevdragon/vidfarm-devcli` (`src/cli.ts`) has two roles. First, `serve` boots the **full** Vidfarm backend locally — single origin, disk-backed records + storage — so Claude Code, Codex, or a human can edit `composition.html` on disk and see the browser live-morph the change (detailed below). Second, it wraps the **entire director REST flow** as 1:1 CLI commands — `discover`, `inspiration-add`, `fork`, `decompose`, `snapshot`, `render` (→ `/render`), `approve`, `schedule`, `posts`/`schedules`, `upload`/`download`, `files`/`get-file`/`put-file`/`annotate-file` (My Files read+write+notes; `files --search` is the hybrid keyword+vector lookup over names/folders/notes), `login`/`whoami`/`provider-keys`, plus a raw `api <METHOD> <path>` passthrough. Each command prints its prod frontend URL as a first-class output, and `--json` gives pure JSON for agents. The command↔route table lives in `SKILL.director.md`.
|
|
410
|
-
|
|
411
|
-
```bash
|
|
412
|
-
# Boot the full editor locally, pull a template's default fork onto disk, open pre-authed.
|
|
413
|
-
vidfarm-devcli serve <template_id>
|
|
414
|
-
# → http://localhost:3000, records + storage under ./.vidfarm-local
|
|
415
|
-
# → composition at ./.vidfarm-local/storage/compositions/forks/<forkId>/working/composition.html
|
|
416
|
-
|
|
417
|
-
# Pull a specific cloud fork instead of the template default:
|
|
418
|
-
vidfarm-devcli serve --fork <forkId> --host https://vidfarm.cc --api-key <key>
|
|
419
|
-
```
|
|
420
|
-
|
|
421
|
-
Flags: `--port` (3000), `--dir` (`./.vidfarm-local`), `--key` (local bootstrap/browser key, or `VIDFARM_API_KEY`), `--fork`, `--host`, `--api-key` (cloud key for pulls, /library, and cloud render), `--refetch`, `--no-cloud`, `--no-open`. `vidfarm <template_id>` aliases `serve <template_id>`.
|
|
422
|
-
|
|
423
|
-
**Cloud passthrough** (`src/services/upstream.ts`; on unless `--no-cloud`): only the editor's data is local — the box mirrors the upstream host (`VIDFARM_UPSTREAM_HOST` + `VIDFARM_UPSTREAM_API_KEY`, set by serve from `--host`/`--api-key`). `/discover/feed` and `GET /api/v1/videos` merge the upstream catalog behind local entries (dedup by id); `/library` and `GET /api/v1/approved/posts` merge the cloud account's approved posts, and `:postId` actions on posts that don't resolve locally proxy through (`maybeProxyUnknownReadyPostToUpstream`); `POST /discover/templates` (Add Template) proxies so ingest lands in the cloud account — its `/upload/presign` sibling proxies too, so file uploads PUT straight to cloud S3 (the multipart fallback `/discover/templates/upload` never proxies: the proxy is text-only; a locally-stored upload key is the one Add Template body handled locally); `/editor/:templateId` seeds an unknown template/fork from upstream on first open (`seedFromUpstream` — same path as boot seeding, cloud fork id reused locally). The editor boot gains `localRender` + `cloudRenderAvailable`, which turn the Render button into a popover: **Render Local (Free)** vs **Render in Cloud**. `render_target: "cloud"` on `POST /render` resolves (or clones, once) a publishable upstream fork (mapping persisted in the fork's working `upstream-link.json`), starts the render upstream with the submitted html, writes a `cloud-render-<renderId>.json` marker, and `GET /renders/:renderId` proxies status through that marker. serve force-clears `VIDFARM_JOB_STATE_MACHINE_ARN` so local renders never silently route to a cloud state machine picked up from a dev-shell `.env`.
|
|
424
|
-
|
|
425
|
-
**Architecture** (fully local editor; cloud passthrough is read/proxy-only):
|
|
426
|
-
- **Records/auth** — `src/services/local-dynamo.ts` is a drop-in `.send()` shim for the DynamoDB DocumentClient used by `serverless-records.ts` + `serverless-auth.ts`, servicing Get/Put/Delete/Query/Scan/Update against JSON files under `<data-dir>/dynamo/<table>/`. Selected by `config.RECORDS_DRIVER` (`z.enum(["local","dynamo"]).default("dynamo")` — deployed envs are unaffected). The 25 call sites are unchanged; only the client construction switches.
|
|
427
|
-
- **Storage** — `StorageService` already writes to disk when `STORAGE_DRIVER=local` / `AWS_S3_BUCKET=""` (files under `<data-dir>/storage/`). Composition public URLs become `${PUBLIC_BASE_URL}/storage/<key>`. This is also what makes **local file paths as media** free: `vidfarm place --src ./file` against a serve working copy copies the file to `<data-dir>/storage/users/local-media/<uuid>/<name>` and references the box's own `/storage/<key>` URL — no S3, the in-process renderer fetches it over localhost (`resolvePlaceableMediaSrc` in `src/cli.ts` detects the serve tree structurally). Off a serve box, `place`/`approve` fall back to uploading a local file to the ephemeral temp store under a `temp/` folder.
|
|
428
|
-
- **Auth** — no seeding: `VIDFARM_API_KEY` + the existing `tryBootstrapFromEnv` auto-provisions a paid customer; `serve` opens `/auto-login?api_key=…&redirect=/editor/…` to plant the session cookie so the browser lands pre-authed.
|
|
429
|
-
- **Live push** — `src/services/composition-watch.ts` (`fs.watch` on the forks dir, only when `STORAGE_DRIVER=local`) coalesces bursts (80ms) and broadcasts `{ forkId, file }` to `GET /api/v1/dev/events` (Hono `streamSSE`, cookie-authed). The editor opens an `EventSource` there gated on the boot flag `liveReload`, and on a `reload` for its fork refetches `composition.html` and runs `applyExternalCompositionHtml` → `morphLiveCompositionDom` (keyed on `data-hf-id`; media buffers + playback survive). The composition write routes call `compositionWatch.suppressFork(forkId)` so the editor's own `PUT` doesn't echo.
|
|
430
|
-
- **Cloud-seed** — `serve` pulls the composition for `--fork` (or the template's default fork via `resolveForkId`, which follows the cloud `/editor` redirect and grabs even another user's public decomposition), then materializes a local template + fork record and writes the composition to the working key so `/editor/:templateId?fork=:id` resolves offline. Media stays on cloud URLs.
|
|
431
|
-
- **Render** — in-process on this box (free, unguarded — `runPrimitiveJobInProcess` + headless-Chromium HyperFrames). Cloud rendering is the explicit `render_target: "cloud"` upstream handoff described above; the ambient `VIDFARM_JOB_STATE_MACHINE_ARN` is force-cleared by serve.
|
|
432
|
-
- Lambdas — `infra/lambda/*.ts`
|
package/demo/README.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# HyperFrames Layer Editor Demo
|
|
2
|
-
|
|
3
|
-
This is a self-contained React demo for decomposing a short vertical video into editable HyperFrames-style layers.
|
|
4
|
-
|
|
5
|
-
Run it from this folder:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm run dev
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
The editor currently supports:
|
|
12
|
-
|
|
13
|
-
- Editable media URLs for video and image layers.
|
|
14
|
-
- Editable text, timing, track, z-index, size, position, opacity, and colors.
|
|
15
|
-
- A live 9:16 stage preview.
|
|
16
|
-
- A scrub/play timeline with layer tracks.
|
|
17
|
-
- Generated HyperFrames-style HTML using `data-start`, `data-duration`, and `data-track-index`.
|
|
18
|
-
- A generated project URL targeting `https://hyperframes.heygen.com/` with the layer payload encoded in the query string.
|
|
19
|
-
- Local MP4 decomposition via `POST /api/decompose`.
|
|
20
|
-
- Gemini-powered extraction of viral DNA, visual DNA, production graph intent, and editable semantic layer timelines.
|
|
21
|
-
|
|
22
|
-
The decomposition demo reads:
|
|
23
|
-
|
|
24
|
-
```text
|
|
25
|
-
../drafts/inspiration-posts/best-at-what-cost.mp4
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
It loads `GEMINI_API_KEY` from `../.env.production` and defaults to `gemini-3.5-flash`. Use the `Decompose MP4` button in the UI to sample frames, call Gemini, and replace the editor layers with the returned semantic timeline.
|