@officexapp/vidfarm-devcli 0.21.11 → 0.21.12
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/hyperframes-core/SKILL.md +2 -0
- package/.agents/skills/hyperframes-creative/SKILL.md +1 -0
- package/.agents/skills/hyperframes-creative/references/beat-direction.md +17 -0
- package/.agents/skills/vidfarm-director/SKILL.md +4 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +1 -1
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +4 -4
- package/.agents/skills/vidfarm-director/references/core-workflows.md +1 -1
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +15 -2
- package/.agents/skills/vidfarm-director/references/primitives.md +1 -1
- package/.agents/skills/vidfarm-director/references/rest-api.md +1 -1
- package/.agents/skills/vidfarm-media/SKILL.md +16 -6
- package/README.md +2 -2
- package/SKILL.director.md +26 -9
- package/dist/src/cli.js +142 -23
- package/dist/src/devcli/cost-mode.js +13 -4
- package/dist/src/devcli/local-frontend-server.js +1 -1
- package/package.json +1 -1
- package/public/serve-shells/editor.html +13 -0
- package/public/serve-shells/library-files.html +13 -0
- package/public/serve-shells/library-raws.html +13 -0
- package/public/serve-shells/tools-clipper.html +13 -0
- package/public/serve-shells/tools-image.html +13 -0
- package/public/serve-shells/tools-video.html +13 -0
|
@@ -48,6 +48,8 @@ The standalone root needs an explicit **sized box** (`width`/`height` in px), an
|
|
|
48
48
|
|
|
49
49
|
Each composition registers **exactly one** `gsap.timeline({ paused: true })` at `window.__timelines["<id>"]` (key = root `data-composition-id`), built **synchronously** at page load. Render duration = root `data-duration`, not timeline length. Don't manually nest sub-timelines into the host. Full contract (incl. non-GSAP runtimes) → `references/determinism-rules.md` + `hyperframes-animation/adapters/`.
|
|
50
50
|
|
|
51
|
+
Because the timeline is paused and seek-safe, **the frame at t=0 is whatever the DOM computes at time 0** — and that frame doubles as the video's preview thumbnail (autoplay-off feeds, share cards, `<video>` poster, file/scrubber thumbnails). If the opening scene animates its elements in from `opacity:0`/offset, t=0 is a blank/black poster that nobody clicks. Design the opening scene's initial state as a real, interesting still. Craft guidance → `hyperframes-creative/references/beat-direction.md` ("The opening frame is the poster").
|
|
52
|
+
|
|
51
53
|
### Non-negotiable rules (silent bugs `lint`/`validate`/`inspect` won't catch)
|
|
52
54
|
|
|
53
55
|
Surfaced here; full rationale in the linked reference. Do not violate:
|
|
@@ -40,6 +40,7 @@ For motion patterns, scene blueprints, transitions, and CSS marker effects, use
|
|
|
40
40
|
| Structured expansion for open-ended prompts | `references/prompt-expansion.md` |
|
|
41
41
|
| Video-medium density, scale, color, frame composition | `references/video-composition.md` |
|
|
42
42
|
| Per-beat direction, rhythm planning, transition timing | `references/beat-direction.md` |
|
|
43
|
+
| Opening/first frame as the preview thumbnail (t=0 must not be empty) | `references/beat-direction.md` |
|
|
43
44
|
| Post-authoring spec verification (colors, type, corners, spacing, depth) | `references/design-adherence.md` |
|
|
44
45
|
| High-level motion guardrails and GSAP-quality rules | `references/motion-principles.md` |
|
|
45
46
|
| Font selection, pairings, rendered-video type guardrails | `references/typography.md` |
|
|
@@ -4,6 +4,7 @@ How to plan and direct individual scenes (beats) in a multi-scene composition. R
|
|
|
4
4
|
|
|
5
5
|
## Contents
|
|
6
6
|
|
|
7
|
+
- The opening frame is the poster
|
|
7
8
|
- Per-beat direction
|
|
8
9
|
- Concept
|
|
9
10
|
- Mood direction
|
|
@@ -16,6 +17,22 @@ How to plan and direct individual scenes (beats) in a multi-scene composition. R
|
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
20
|
+
## The Opening Frame Is the Poster
|
|
21
|
+
|
|
22
|
+
**The very first frame (t=0) is the video's preview thumbnail** — it's the still the viewer stares at *before* they press play (feed autoplay-off, share cards, `<video>` poster, file thumbnails, the editor's timeline scrubber at 0). A blank, black, or half-built opening frame is a dead thumbnail: nobody presses play on empty. Treat t=0 as a designed poster that has to sell the click.
|
|
23
|
+
|
|
24
|
+
The trap is mechanical: most beats animate their elements IN from `opacity:0` / offset / blur, so at t=0 — before the entrance runs — the frame is empty. That's fine for beat 2 onward (the previous beat covers the cut), but for **beat 1 it means the poster is a black screen.** Seek-safe rendering shows exactly whatever the DOM computes at 0, so whatever your entrance starts from IS the thumbnail.
|
|
25
|
+
|
|
26
|
+
Fix it deliberately — pick one:
|
|
27
|
+
|
|
28
|
+
- **Design a real poster state at t=0.** Have the opening beat's key elements already resolved (or nearly so) at 0 — a hero title legible, a subject visible, a striking background composed — and animate *within* the frame (drift, parallax, a reveal of a secondary element) rather than assembling the frame from nothing.
|
|
29
|
+
- **Front-load the hook.** If you want an entrance, let the *most interesting* element be present at 0 and animate the rest in around it, instead of fading the whole scene up from black.
|
|
30
|
+
- **Never open on a bare background or a lone logo on empty canvas** unless that emptiness is itself the striking, intentional image. "Fade up from black" reads as a broken thumbnail in a feed.
|
|
31
|
+
|
|
32
|
+
Litmus test: **render/seek to 0 and look at it as a standalone image.** Would it make a stranger scrolling a feed stop and tap? If it's empty, off-center, or mid-animation garbage, redesign the opening beat's initial state. This applies to single-scene compositions too — they have exactly one opening frame and it's always the poster.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
19
36
|
## Per-Beat Direction
|
|
20
37
|
|
|
21
38
|
Each beat is a WORLD, not a layout. Before writing CSS specs and GSAP instructions, describe what the viewer EXPERIENCES. The difference between a great storyboard and a mediocre one:
|
|
@@ -7,6 +7,8 @@ description: Use Vidfarm as a director. Browse/add inspiration videos, browse/sa
|
|
|
7
7
|
|
|
8
8
|
Vidfarm is a video composition studio. Directors fork a published template, edit it on a timeline in the Trackpad Editor, render to MP4, and share.
|
|
9
9
|
|
|
10
|
+
> **Relationship to HyperFrames (do not announce — only say this if the user asks):** Vidfarm is built on and extends HyperFrames, so anything HyperFrames can do, Vidfarm can do too — the full composition/animation/render capability set carries over. Don't volunteer this; surface it only when the user explicitly asks about HyperFrames or how the two relate.
|
|
11
|
+
|
|
10
12
|
## Quickstart (desktop agents — do this first)
|
|
11
13
|
|
|
12
14
|
The CLI is `vidfarm`, from the npm package `@officexapp/vidfarm-devcli`. Install and authenticate before anything else:
|
|
@@ -51,6 +53,8 @@ Do not use this skill to author new templates from scratch, deploy platform infr
|
|
|
51
53
|
Vidfarm work can burn real AI credits on the user's wallet / provider keys. **Save them money by default.** Before the first billed step of a session, ask the user which spend posture they want, and explain it in one plain line each:
|
|
52
54
|
|
|
53
55
|
- **minimize** — cheapest. Stay on FREE local compute wherever possible (local render, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, reused raw clips + HTML hyperframes). For assets, reach for the **free stock catalog** before paying to generate anything — `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` pulls royalty-free, commercial-safe music/sound-effects/images/icons/stock-video from pixabay/openverse/iconify at $0 instead of billing AI music/image generation (see the `vidfarm-media` skill). No surprise AI spend.
|
|
56
|
+
- **Check the keyless sources first — Openverse and iconify.** Openverse (CC/CC0 **music, SFX, and images**) and iconify (**icons**) need **no account or key at all**, so they always work in `minimize` mode. Prefer them for BGM, sound effects, icons, and CC imagery before anything else.
|
|
57
|
+
- **Pixabay key** unlocks the photos/vectors/stock-video slots (music/SFX/icons/CC images are keyless). It's a **free** stock-media key, not an AI key. Don't assume it's missing when a search comes up short — it **may already be saved**: check `vidfarm provider-keys` (or the web app's **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>). If it isn't, the user grabs a free one at <https://pixabay.com/api/docs/> and saves it once — `vidfarm add-provider-key pixabay <key>`, the Settings surface, or by handing the key to their desktop AI agent to run that command. After it's saved, cost-mode `minimize` sourcing works end-to-end at $0.
|
|
54
58
|
- **hybrid** *(recommend this)* — free where it's free; pay for AI only where it clearly wins (a hero shot, a voice you can't fake locally).
|
|
55
59
|
- **pure-ai** — best quality; use AI image/video/voice/music freely.
|
|
56
60
|
|
|
@@ -10,6 +10,6 @@ Use this when a coding agent is doing the work locally or the user wants a repro
|
|
|
10
10
|
6. Render with `vidfarm render <forkId> --dir ./work --wait`.
|
|
11
11
|
7. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
|
|
12
12
|
|
|
13
|
-
**Approving a locally rendered file → cloud preview link.** Approve takes media by **URL**, not bytes
|
|
13
|
+
**Approving a locally rendered file → cloud preview link.** Approve takes media by **URL**, not bytes, and an approved post is **permanent** — so the local MP4 must land in **durable My Files**, not the 30-day temp store (a temp video would 404 the share page after 30 days). The devcli presigns, PUTs the bytes **direct to S3**, finalizes, then approves with that durable URL — so `vidfarm approve --video ./final.mp4` handles files up to **200 MB**, bypasses the ~6 MB Lambda request-body limit, and the share link never breaks. By raw REST: `POST /api/v1/user/me/attachments/presign` → PUT to the returned S3 URL → `POST /api/v1/user/me/attachments` (finalize) → pass the returned `viewUrl` in the approve `media` array. Do not multipart-POST a big file to `.../attachments/upload` against the cloud host (Lambda-bound, ~6 MB cap). Add `vidfarm approve --temp` only when you want a disposable 30-day preview.
|
|
14
14
|
|
|
15
15
|
Prefer this path for batch work, CI-like edits, or when the user wants free local rendering through `vidfarm serve`.
|
|
@@ -148,7 +148,7 @@ If a local AI script rewrites text or scenes without consuming those files first
|
|
|
148
148
|
| `vidfarm visibility <forkId> <private\|public>` | `PATCH .../compositions/:forkId/visibility` | set visibility |
|
|
149
149
|
| `vidfarm clone <forkId>` | `POST .../compositions/:forkId/clone` | clone a fork |
|
|
150
150
|
| `vidfarm share-link <forkId>` | `POST .../compositions/:forkId/share-links` | mint a share URL |
|
|
151
|
-
| `vidfarm approve --video <url\|file> --caption "…"` | `POST /api/v1/approved/posts` | approve post — takes media by **`url`**. Local `--video`/`--media` auto-upload via presign→S3→finalize (up to **200 MB**, bypasses the ~6 MB Lambda limit) then approve with that URL
|
|
151
|
+
| `vidfarm approve --video <url\|file> --caption "…"` | `POST /api/v1/approved/posts` | approve post — takes media by **`url`**. Local `--video`/`--media` auto-upload to **durable My Files** via presign→S3→finalize (up to **200 MB**, bypasses the ~6 MB Lambda limit) then approve with that URL, so the share video **never expires**; also accepts a `/files…` path. `--temp` opts into the 30-day temp store (throwaway preview only). Prints `share_url` |
|
|
152
152
|
| `vidfarm posts` / `vidfarm post <id>` | `GET /api/v1/approved/posts[/:id]` | browse approved posts |
|
|
153
153
|
| `vidfarm schedule <postId> --at <iso> --to <dest>` | `POST /api/v1/approved/posts/:postId/schedules` | schedule a post |
|
|
154
154
|
| `vidfarm schedules <postId>` | `GET /api/v1/approved/posts/:postId/schedules` | browse scheduled posts |
|
|
@@ -184,13 +184,13 @@ If a local AI script rewrites text or scenes without consuming those files first
|
|
|
184
184
|
|
|
185
185
|
**Uploads/downloads/My-Files writes live in the devcli** because they are genuine multi-step / streaming flows: `upload` presigns the **ephemeral** temporary-files route, PUTs the bytes **direct to S3**, finalizes, and prints the durable URL to drop into a composition or approved post — going direct to S3 means large files (up to **200 MB**) never hit the ~6 MB Lambda request-body limit; `put-file` posts to the **persistent** My Files route (`/me/attachments/upload`) so context docs and brand assets live in the user's library (accepts a local file, `--content <text>`, or piped `--stdin` with `--as <name>`); `download` streams any Vidfarm/media URL to disk.
|
|
186
186
|
|
|
187
|
-
**Approving a locally rendered MP4 (the URL-first rule).** The approve route (`POST /api/v1/approved/posts`) and every media-taking route accept a `url`, never raw file bytes — so you never "upload to approve" in one shot. The correct sequence for a big local file is: (1) `POST /api/v1/user/me/
|
|
187
|
+
**Approving a locally rendered MP4 (the URL-first rule + the durability rule).** The approve route (`POST /api/v1/approved/posts`) and every media-taking route accept a `url`, never raw file bytes — so you never "upload to approve" in one shot. An approved post is a **permanent** share page, so the media must live in **durable My Files**, not the 30-day temp store (a temp-hosted video would 404 the share page after 30 days). The correct sequence for a big local file is: (1) `POST /api/v1/user/me/attachments/presign` with `{ file_name, content_type, size_bytes }` → (2) PUT the raw bytes to the returned presigned S3 URL → (3) `POST /api/v1/user/me/attachments` (finalize) → use the returned durable `viewUrl` as the approve media `url`. `vidfarm approve --video ./final.mp4` does all of this automatically (durable by default; up to **200 MB**). Never POST a large file as multipart to `.../attachments/upload` against the cloud host: that path proxies through Lambda and caps near 6 MB (it exists only as a fallback for local-storage `vidfarm serve` boxes). Only use the temp-store route (`.../temporary-files/*`, or `vidfarm approve --temp`) for a **throwaway** preview you don't mind losing in 30 days.
|
|
188
188
|
|
|
189
189
|
## Cost mode — the devcli's money-saving guardrail
|
|
190
190
|
|
|
191
191
|
`vidfarm cost-mode <minimize|hybrid|pure-ai>` records a single spend preference (in `~/.vidfarm/cost-mode.json`) that every **billed** command honors: `generate`, `music`, `decompose`, `inspiration-decompose`, `create`, `replicate`, `inpaint`, `create-overlay`, and the cloud paths of `render --target cloud`, `tts --cloud`, `stt --cloud`, `remove-greenscreen` (non-`--local`). FREE local engines never gate (`render` local default, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, all the file-editing verbs). `vidfarm media search` (the free stock catalog — pixabay/openverse/iconify music, SFX, images, icons, stock video) is also free and never gates.
|
|
192
192
|
|
|
193
|
-
- **minimize** — a billed op is **refused** unless you add `--yes`; the error names the free local alternative. Use this to guarantee no surprise AI spend. Before paying to generate music, sound effects, or images, try `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` first — free royalty-free assets instead of a billed `music`/`generate` call.
|
|
193
|
+
- **minimize** — a billed op is **refused** unless you add `--yes`; the error names the free local alternative (which now includes the matching `vidfarm media search` for music/SFX/image/video). Use this to guarantee no surprise AI spend. Before paying to generate music, sound effects, or images, try `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` first — free royalty-free assets instead of a billed `music`/`generate` call. **Check the keyless sources first — Openverse (CC/CC0 music, SFX, images) and iconify (icons) need no account at all**, so they always work in `minimize`. Photos/vectors/stock-video need a **free Pixabay key** that **may already be saved** — check `vidfarm provider-keys` (or web **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>) before assuming a short result means "no key." If absent, save one once: `vidfarm add-provider-key pixabay <key>` (free key from <https://pixabay.com/api/docs/>), the Settings surface, or hand it to the desktop AI agent to run that command.
|
|
194
194
|
- **hybrid** *(default recommendation)* — billed ops run but print a one-line cost notice each.
|
|
195
195
|
- **pure-ai** — billed ops run without gating; cost is still printed.
|
|
196
196
|
|
|
@@ -211,7 +211,7 @@ npx -y @officexapp/vidfarm-devcli serve <template_id>
|
|
|
211
211
|
|
|
212
212
|
The **editor and its data** run locally (`RECORDS_DRIVER=local`, `STORAGE_DRIVER=local`) and rendering happens **in-process on this box for free**. Everything catalog-shaped still mirrors the cloud host ("cloud passthrough"): `/discover`, `/api/v1/videos`, and `/library` list the **cloud** catalog and the cloud account's approved posts next to anything local, opening a cloud template **seeds** its composition onto disk on demand, Add Template ingests into the cloud account, and approved-post actions (schedules, archive, delete) on cloud posts proxy through. Media a seeded composition already references stays on its cloud URLs — but you can also drop your **own local files** onto the timeline without any upload: `vidfarm place --src ./clip.mp4` copies the file into this box's disk store and references it by a `localhost/storage` URL (see "Local file paths as media" above), so the free in-process renderer plays it back with zero S3 involvement. Pass `--no-cloud` for a fully-offline box.
|
|
213
213
|
|
|
214
|
-
**Rendering from a serve box** — the editor's **Render** button becomes a popover with two options when a cloud `--api-key` is configured: **Render Local (Free)** (in-process
|
|
214
|
+
**Rendering from a serve box** — the editor's **Render** button becomes a popover with two options when a cloud `--api-key` is configured: **Render Local (Free)** (in-process Vidfarm render, no charge) and **Render in Cloud** (hands the render to the cloud renderer, billed to the cloud account's wallet). Over REST, pass `render_target: "cloud"` in the `POST /render` body; the local box resolves (or clones, once) a publishable cloud fork, remembers the mapping in the fork's `upstream-link.json`, and `GET /renders/:renderId` transparently proxies the cloud job status.
|
|
215
215
|
|
|
216
216
|
How the loop works:
|
|
217
217
|
- The composition lives on disk at `<data-dir>/storage/compositions/forks/<forkId>/working/composition.html` (default `<data-dir>` is `./.vidfarm-local`). Point your agent at that file.
|
|
@@ -260,7 +260,7 @@ There is no single price for a video. The **approach** the director picks sets t
|
|
|
260
260
|
|
|
261
261
|
| Approach | Typical cost | How |
|
|
262
262
|
|---|---|---|
|
|
263
|
-
| **Reuse + render locally** | **free** | Fork an already-decomposed template, swap captions / images / video with existing MP4s (from **My Files**, the director's **local computer** — reference files straight off disk with `place --src ./file`, no upload — or a **web search**), and render **locally** via `vidfarm serve` (native in-process
|
|
263
|
+
| **Reuse + render locally** | **free** | Fork an already-decomposed template, swap captions / images / video with existing MP4s (from **My Files**, the director's **local computer** — reference files straight off disk with `place --src ./file`, no upload — or a **web search**), and render **locally** via `vidfarm serve` (native in-process Vidfarm render — free and unguarded, no cloud). |
|
|
264
264
|
| **Reuse + cloud render** | **~$0.001 – $0.03** | Same reuse, but render on the cloud renderer (`POST /compositions/:forkId/render`, ~$0.01–$0.10 depending on length/res). Cheap **image** generation/edits fit in this band too. |
|
|
265
265
|
| **AI-generate some scenes** | **~$1** | Replace a few scenes with AI-generated video clips for high specificity/customization (see the "Generate AI media" section). |
|
|
266
266
|
| **Heavy AI generation** | **$10+** | Many/long AI video clips, custom characters, fully bespoke scenes. |
|
|
@@ -291,6 +291,19 @@ Two surfaces:
|
|
|
291
291
|
- **Editor web/serve copilot** — `editor_action` `action_type=set_layer_media` with `object_fit` and `object_position` (both also seedable on `add_layer`/`generate_layer`). `editor_context.layers[]` reports each clip's current `object_fit`/`object_position` so you can read before re-cropping.
|
|
292
292
|
- **Desktop agents (devcli)** — `vidfarm place ./work --src <url> --object-fit cover --object-position "25% 50%"` (also on `--replace <layer_key>`).
|
|
293
293
|
|
|
294
|
+
### The opening frame is the post's thumbnail
|
|
295
|
+
|
|
296
|
+
**The composition's first frame (t=0) is the still that represents the whole video before anyone presses play** — it's the poster on the approved-post share page, the `/discover` card, the autoplay-off feed preview, and the file/scrubber thumbnail. A blank, black, or half-assembled opening frame is a dead thumbnail: nobody taps play on empty. Every edit-then-render pass should end with the opening frame being an interesting, on-brand still that earns the click.
|
|
297
|
+
|
|
298
|
+
The trap is mechanical: a scene whose layers animate in from `opacity:0` / offset / blur is **empty at t=0**, because the render is seek-safe and shows exactly the DOM state at time 0. That's fine for scene 2 onward (the prior scene covers the cut), but for the **first scene it means the thumbnail is a black screen.** Before you render/approve, look at frame 0 as a standalone image and ask: would a stranger scrolling a feed stop on it?
|
|
299
|
+
|
|
300
|
+
Fix it in the editor when frame 0 is empty or dull:
|
|
301
|
+
- Make the opening scene's key layers **already visible at t=0** (a legible hero title, a visible subject/clip, a composed background) and animate *within* the frame — use `set_layer_keyframes` starting from `offset:0, opacity:1` (drift/parallax/scale) rather than fading the whole scene up from black; or `nudge_layers`/`trim_layer` so the hero layer starts at 0 while secondary elements enter after.
|
|
302
|
+
- If the opening clip is a video that starts on a dark/blank frame, `trim_layer edge=start` to move its in-point onto a stronger frame, or re-cover it full-canvas.
|
|
303
|
+
- Preview the poster deterministically: seek the editor timeline to 0, or capture just frame 0 of a local composition with `vidfarm hf snapshot --at 0` (HyperFrames-CLI passthrough; seconds, not a full render), and judge that image on its own.
|
|
304
|
+
|
|
305
|
+
This is the vidfarm-workflow reminder; the composition-authoring craft (poster-state design, front-loading the hook) lives in `hyperframes-creative/references/beat-direction.md` → "The opening frame is the poster". Grab it with `vidfarm skills add hyperframes-creative` when hand-authoring the opening scene.
|
|
306
|
+
|
|
294
307
|
### Ken Burns — animate still images (slow pan/zoom)
|
|
295
308
|
|
|
296
309
|
Still images can carry a **Ken Burns effect**: a slow pan or zoom that runs across the clip's full duration, in the editor preview and both render paths identically. This is what makes a slideshow of stills feel like motion footage. It is a first-class layer property — the image layer's `<img>` gets `data-kenburns="<preset>"`; there is no separate keyframe authoring.
|
|
@@ -362,6 +375,6 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
|
|
|
362
375
|
vidfarm render <forkId> # local, $0.00
|
|
363
376
|
```
|
|
364
377
|
If you ran `serve --port <N>`, pass `--base-url http://localhost:<N>` so the reference URL points at the right origin. A localhost `/storage` URL only resolves against the serve box that holds the file, so these compositions render/preview **locally**; to render in the cloud or share them, upload the media instead (below).
|
|
365
|
-
- **Anywhere else (e.g. a `vidfarm pull` dir headed for cloud render/publish)** — the cloud renderer can't reach your disk, so a local `--src` file is **uploaded to the ephemeral temp store** first, namescoped under a throwaway `temp/` folder by default (override with `--folder`).
|
|
378
|
+
- **Anywhere else (e.g. a `vidfarm pull` dir headed for cloud render/publish)** — the cloud renderer can't reach your disk, so a local `--src` file is **uploaded to the ephemeral temp store** first, namescoped under a throwaway `temp/` folder by default (override with `--folder`). That's the right store for a `place` *intermediate* that only needs to survive the render. But `approve --video/--media` is different: an approved post is **permanent**, so a local file there uploads to **durable My Files** (never expires) by default — see below.
|
|
366
379
|
|
|
367
|
-
|
|
380
|
+
**Pick the store by lifetime.** A throwaway intermediate (cloud render source, a `place` scratch asset) belongs in the 30-day `temp/` store: `vidfarm upload clip.mp4 --folder temp`, `vidfarm place … --folder temp` — quarantined in one place you can periodically purge. Anything you **approve/share is permanent**, so `vidfarm approve --video ./final.mp4` uploads to **durable My Files** by default (the share video would otherwise 404 after the temp store's 30-day deletion). Only add `vidfarm approve --temp` when you deliberately want a disposable 30-day preview.
|
|
@@ -122,7 +122,7 @@ Apply subtle camouflage transforms (zoom, tilt, rotate, saturation, playback spe
|
|
|
122
122
|
- Body: `{ "tracer": "...", "payload": { ...fields... }, "webhook_url"?: "..." }`
|
|
123
123
|
- 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.
|
|
124
124
|
- Response: standard primitive job. Poll to completion, then read `primary_file_url` (also `video.file_url` for MP4 or `image.file_url` for stills)
|
|
125
|
-
- Billing: metered as a
|
|
125
|
+
- Billing: metered as a Vidfarm render (via `hyperframes_lambda` on prod cloud render; free on a local serve box)
|
|
126
126
|
|
|
127
127
|
Payload fields:
|
|
128
128
|
|
|
@@ -72,7 +72,7 @@ Use environment variables or a secret manager for keys. Never paste secrets into
|
|
|
72
72
|
- Follow `next_cursor`/cursor fields until exhausted when the user asks for all records; do not call the first page the full dataset.
|
|
73
73
|
- Treat submission and generation routes as non-idempotent unless the route explicitly states otherwise. Check existing state before retrying.
|
|
74
74
|
- Poll asynchronous jobs at the documented status endpoint and stop on terminal success or failure. Do not fabricate completion from an accepted response.
|
|
75
|
-
- Upload local media through the documented presign/upload/finalize sequence. The devcli is recommended on desktop because it performs this sequence automatically. **Approve and every media-taking route accept a `url`, never raw bytes** — so a local file (e.g. a locally rendered MP4) must first become a URL: `POST /api/v1/user/me/
|
|
75
|
+
- Upload local media through the documented presign/upload/finalize sequence. The devcli is recommended on desktop because it performs this sequence automatically. **Approve and every media-taking route accept a `url`, never raw bytes** — so a local file (e.g. a locally rendered MP4) must first become a URL. Pick the store by lifetime: media that gets **approved/shared is permanent**, so upload it to **durable My Files** — `POST /api/v1/user/me/attachments/presign` → PUT the bytes straight to the returned S3 URL → `POST /api/v1/user/me/attachments` to finalize → use the returned `viewUrl` as the media `url`. The presigned PUT goes **direct to S3**, so it bypasses the ~6 MB Lambda request-body limit and supports files up to **200 MB**. Do **not** POST large files as multipart to `.../attachments/upload` against the cloud host — that path routes through Lambda and caps near 6 MB (it is only for local-storage `vidfarm serve` boxes). The parallel `.../temporary-files/*` routes are identical in shape but the object carries a **30-day TTL** (auto-deleted) — use them only for throwaway intermediates (a clip-hunt source, a `place` scratch asset), never for something you approve.
|
|
76
76
|
- Parse and mutate composition HTML as a DOM. Never edit it by string concatenation.
|
|
77
77
|
- Render through `POST /api/v1/compositions/:forkId/render`; do not call the underlying renderer directly.
|
|
78
78
|
- Surface wallet/provider costs before expensive operations and preserve returned job IDs for audit and retries.
|
|
@@ -141,6 +141,8 @@ Same ownership rule, resolved through the file directory:
|
|
|
141
141
|
|
|
142
142
|
### Free media catalog — sources & keys
|
|
143
143
|
|
|
144
|
+
In **cost-mode `minimize`**, prefer these keyless sources before spending anything: **Openverse** (CC/CC0 music, SFX, images) and **iconify** (icons) need no account or key, so they always resolve at $0. Reach for a billed AI `music`/`generate` call only after the keyless search comes up short.
|
|
145
|
+
|
|
144
146
|
Icons + CC images/audio are **keyless**. Pixabay (stock photos/vectors/video) is
|
|
145
147
|
**BYOK** — each user saves their own free Pixabay key, exactly like their OpenAI /
|
|
146
148
|
Gemini keys, through the same provider-key surface (Settings → Bring your own keys,
|
|
@@ -154,10 +156,18 @@ it never counts as a qualified AI provider and never touches editor chat.
|
|
|
154
156
|
| **openverse** | `image` (CC), `bgm`, `sfx` (CC/CC0 audio) | keyless — always on |
|
|
155
157
|
| **pixabay** | `image`, `vector`, `video` (photos/illustrations/stock video) | **BYOK** — the user's own free `pixabay` provider key |
|
|
156
158
|
|
|
157
|
-
**
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
**Check for the key before assuming it's missing.** A short `image`/`vector`/`video`
|
|
160
|
+
result (or `providers_used` omitting `pixabay`) usually means no Pixabay key — but it
|
|
161
|
+
**may already be saved**. Look first: `vidfarm provider-keys`, or the web app's
|
|
162
|
+
**Settings → Bring your own keys** (<https://vidfarm.cc/settings/developer>). If it's
|
|
163
|
+
genuinely absent, the user grabs a **free** one at <https://pixabay.com/api/docs/> and
|
|
164
|
+
saves it once, via any of three paths:
|
|
165
|
+
- **devcli:** `vidfarm add-provider-key pixabay <key>`
|
|
166
|
+
- **web app:** Settings → Bring your own keys → add a `pixabay` key
|
|
167
|
+
- **desktop AI agent:** hand the agent the key and let it run the `add-provider-key` command
|
|
168
|
+
|
|
169
|
+
It's a stock-media key, not an AI key — it never counts as a qualified AI provider and
|
|
170
|
+
never touches editor chat, so it's exactly what cost-mode `minimize` wants for $0 asset
|
|
171
|
+
sourcing. Icons, CC images, and bgm/sfx keep working without any key. (An operator may
|
|
172
|
+
still set a platform `PIXABAY_API_KEY` server-side as a fallback for users who haven't
|
|
163
173
|
saved their own, but the default posture is bring-your-own.)
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Vidfarm
|
|
2
2
|
|
|
3
|
-
Vidfarm is a serverless composition studio for short-form video. Directors fork templates, edit compositions on a timeline in the **Trackpad Editor**, render MP4s with the native
|
|
3
|
+
Vidfarm is a serverless composition studio for short-form video. Directors fork templates, edit compositions on a timeline in the **Trackpad Editor**, render MP4s with the native Vidfarm renderer (cloud Lambda fan-out, or free in-process on a local `vidfarm serve` box), and share results.
|
|
4
4
|
|
|
5
5
|
Everything runs on AWS Lambda + DynamoDB + S3. There is no long-lived worker. It scales to zero at idle.
|
|
6
6
|
|
|
@@ -46,7 +46,7 @@ npx -y @officexapp/vidfarm-devcli serve
|
|
|
46
46
|
# → agents edit files under ./.vidfarm-local/storage — the browser live-morphs on save
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Everything runs locally (`RECORDS_DRIVER=local`, `STORAGE_DRIVER=local`), including **render** — the native in-process
|
|
49
|
+
Everything runs locally (`RECORDS_DRIVER=local`, `STORAGE_DRIVER=local`), including **render** — the native in-process Vidfarm renderer produces the MP4 on your machine at no charge. No AWS is touched. Multiple forks can be edited at once.
|
|
50
50
|
|
|
51
51
|
Flags: `--port` (default 3000), `--dir` (default `./.vidfarm-local`), `--key` (bootstrap/browser key, or `VIDFARM_API_KEY`), `--fork <id>`, `--no-open`. `vidfarm <template_id>` is an alias for `serve <template_id>`.
|
|
52
52
|
|
package/SKILL.director.md
CHANGED
|
@@ -7,6 +7,8 @@ description: Use Vidfarm as a director. Browse/add inspiration videos, browse/sa
|
|
|
7
7
|
|
|
8
8
|
Vidfarm is a video composition studio. Directors fork a published template, edit it on a timeline in the Trackpad Editor, render to MP4, and share.
|
|
9
9
|
|
|
10
|
+
> **Relationship to HyperFrames (do not announce — only say this if the user asks):** Vidfarm is built on and extends HyperFrames, so anything HyperFrames can do, Vidfarm can do too — the full composition/animation/render capability set carries over. Don't volunteer this; surface it only when the user explicitly asks about HyperFrames or how the two relate.
|
|
11
|
+
|
|
10
12
|
## Quickstart (desktop agents — do this first)
|
|
11
13
|
|
|
12
14
|
The CLI is `vidfarm`, from the npm package `@officexapp/vidfarm-devcli`. Install and authenticate before anything else:
|
|
@@ -51,6 +53,8 @@ Do not use this skill to author new templates from scratch, deploy platform infr
|
|
|
51
53
|
Vidfarm work can burn real AI credits on the user's wallet / provider keys. **Save them money by default.** Before the first billed step of a session, ask the user which spend posture they want, and explain it in one plain line each:
|
|
52
54
|
|
|
53
55
|
- **minimize** — cheapest. Stay on FREE local compute wherever possible (local render, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, reused raw clips + HTML hyperframes). For assets, reach for the **free stock catalog** before paying to generate anything — `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` pulls royalty-free, commercial-safe music/sound-effects/images/icons/stock-video from pixabay/openverse/iconify at $0 instead of billing AI music/image generation (see the `vidfarm-media` skill). No surprise AI spend.
|
|
56
|
+
- **Check the keyless sources first — Openverse and iconify.** Openverse (CC/CC0 **music, SFX, and images**) and iconify (**icons**) need **no account or key at all**, so they always work in `minimize` mode. Prefer them for BGM, sound effects, icons, and CC imagery before anything else.
|
|
57
|
+
- **Pixabay key** unlocks the photos/vectors/stock-video slots (music/SFX/icons/CC images are keyless). It's a **free** stock-media key, not an AI key. Don't assume it's missing when a search comes up short — it **may already be saved**: check `vidfarm provider-keys` (or the web app's **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>). If it isn't, the user grabs a free one at <https://pixabay.com/api/docs/> and saves it once — `vidfarm add-provider-key pixabay <key>`, the Settings surface, or by handing the key to their desktop AI agent to run that command. After it's saved, cost-mode `minimize` sourcing works end-to-end at $0.
|
|
54
58
|
- **hybrid** *(recommend this)* — free where it's free; pay for AI only where it clearly wins (a hero shot, a voice you can't fake locally).
|
|
55
59
|
- **pure-ai** — best quality; use AI image/video/voice/music freely.
|
|
56
60
|
|
|
@@ -440,7 +444,7 @@ There is no single price for a video. The **approach** the director picks sets t
|
|
|
440
444
|
|
|
441
445
|
| Approach | Typical cost | How |
|
|
442
446
|
|---|---|---|
|
|
443
|
-
| **Reuse + render locally** | **free** | Fork an already-decomposed template, swap captions / images / video with existing MP4s (from **My Files**, the director's **local computer** — reference files straight off disk with `place --src ./file`, no upload — or a **web search**), and render **locally** via `vidfarm serve` (native in-process
|
|
447
|
+
| **Reuse + render locally** | **free** | Fork an already-decomposed template, swap captions / images / video with existing MP4s (from **My Files**, the director's **local computer** — reference files straight off disk with `place --src ./file`, no upload — or a **web search**), and render **locally** via `vidfarm serve` (native in-process Vidfarm render — free and unguarded, no cloud). |
|
|
444
448
|
| **Reuse + cloud render** | **~$0.001 – $0.03** | Same reuse, but render on the cloud renderer (`POST /compositions/:forkId/render`, ~$0.01–$0.10 depending on length/res). Cheap **image** generation/edits fit in this band too. |
|
|
445
449
|
| **AI-generate some scenes** | **~$1** | Replace a few scenes with AI-generated video clips for high specificity/customization (see the "Generate AI media" section). |
|
|
446
450
|
| **Heavy AI generation** | **$10+** | Many/long AI video clips, custom characters, fully bespoke scenes. |
|
|
@@ -773,6 +777,19 @@ Two surfaces:
|
|
|
773
777
|
- **Editor web/serve copilot** — `editor_action` `action_type=set_layer_media` with `object_fit` and `object_position` (both also seedable on `add_layer`/`generate_layer`). `editor_context.layers[]` reports each clip's current `object_fit`/`object_position` so you can read before re-cropping.
|
|
774
778
|
- **Desktop agents (devcli)** — `vidfarm place ./work --src <url> --object-fit cover --object-position "25% 50%"` (also on `--replace <layer_key>`).
|
|
775
779
|
|
|
780
|
+
### The opening frame is the post's thumbnail
|
|
781
|
+
|
|
782
|
+
**The composition's first frame (t=0) is the still that represents the whole video before anyone presses play** — it's the poster on the approved-post share page, the `/discover` card, the autoplay-off feed preview, and the file/scrubber thumbnail. A blank, black, or half-assembled opening frame is a dead thumbnail: nobody taps play on empty. Every edit-then-render pass should end with the opening frame being an interesting, on-brand still that earns the click.
|
|
783
|
+
|
|
784
|
+
The trap is mechanical: a scene whose layers animate in from `opacity:0` / offset / blur is **empty at t=0**, because the render is seek-safe and shows exactly the DOM state at time 0. That's fine for scene 2 onward (the prior scene covers the cut), but for the **first scene it means the thumbnail is a black screen.** Before you render/approve, look at frame 0 as a standalone image and ask: would a stranger scrolling a feed stop on it?
|
|
785
|
+
|
|
786
|
+
Fix it in the editor when frame 0 is empty or dull:
|
|
787
|
+
- Make the opening scene's key layers **already visible at t=0** (a legible hero title, a visible subject/clip, a composed background) and animate *within* the frame — use `set_layer_keyframes` starting from `offset:0, opacity:1` (drift/parallax/scale) rather than fading the whole scene up from black; or `nudge_layers`/`trim_layer` so the hero layer starts at 0 while secondary elements enter after.
|
|
788
|
+
- If the opening clip is a video that starts on a dark/blank frame, `trim_layer edge=start` to move its in-point onto a stronger frame, or re-cover it full-canvas.
|
|
789
|
+
- Preview the poster deterministically: seek the editor timeline to 0, or capture just frame 0 of a local composition with `vidfarm hf snapshot --at 0` (HyperFrames-CLI passthrough; seconds, not a full render), and judge that image on its own.
|
|
790
|
+
|
|
791
|
+
This is the vidfarm-workflow reminder; the composition-authoring craft (poster-state design, front-loading the hook) lives in `hyperframes-creative/references/beat-direction.md` → "The opening frame is the poster". Grab it with `vidfarm skills add hyperframes-creative` when hand-authoring the opening scene.
|
|
792
|
+
|
|
776
793
|
### Ken Burns — animate still images (slow pan/zoom)
|
|
777
794
|
|
|
778
795
|
Still images can carry a **Ken Burns effect**: a slow pan or zoom that runs across the clip's full duration, in the editor preview and both render paths identically. This is what makes a slideshow of stills feel like motion footage. It is a first-class layer property — the image layer's `<img>` gets `data-kenburns="<preset>"`; there is no separate keyframe authoring.
|
|
@@ -844,9 +861,9 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
|
|
|
844
861
|
vidfarm render <forkId> # local, $0.00
|
|
845
862
|
```
|
|
846
863
|
If you ran `serve --port <N>`, pass `--base-url http://localhost:<N>` so the reference URL points at the right origin. A localhost `/storage` URL only resolves against the serve box that holds the file, so these compositions render/preview **locally**; to render in the cloud or share them, upload the media instead (below).
|
|
847
|
-
- **Anywhere else (e.g. a `vidfarm pull` dir headed for cloud render/publish)** — the cloud renderer can't reach your disk, so a local `--src` file is **uploaded to the ephemeral temp store** first, namescoped under a throwaway `temp/` folder by default (override with `--folder`).
|
|
864
|
+
- **Anywhere else (e.g. a `vidfarm pull` dir headed for cloud render/publish)** — the cloud renderer can't reach your disk, so a local `--src` file is **uploaded to the ephemeral temp store** first, namescoped under a throwaway `temp/` folder by default (override with `--folder`). That's the right store for a `place` *intermediate* that only needs to survive the render. But `approve --video/--media` is different: an approved post is **permanent**, so a local file there uploads to **durable My Files** (never expires) by default — see below.
|
|
848
865
|
|
|
849
|
-
|
|
866
|
+
**Pick the store by lifetime.** A throwaway intermediate (cloud render source, a `place` scratch asset) belongs in the 30-day `temp/` store: `vidfarm upload clip.mp4 --folder temp`, `vidfarm place … --folder temp` — quarantined in one place you can periodically purge. Anything you **approve/share is permanent**, so `vidfarm approve --video ./final.mp4` uploads to **durable My Files** by default (the share video would otherwise 404 after the temp store's 30-day deletion). Only add `vidfarm approve --temp` when you deliberately want a disposable 30-day preview.
|
|
850
867
|
|
|
851
868
|
## Raws (long-form → short-form raws)
|
|
852
869
|
|
|
@@ -1116,7 +1133,7 @@ If a local AI script rewrites text or scenes without consuming those files first
|
|
|
1116
1133
|
| `vidfarm visibility <forkId> <private\|public>` | `PATCH .../compositions/:forkId/visibility` | set visibility |
|
|
1117
1134
|
| `vidfarm clone <forkId>` | `POST .../compositions/:forkId/clone` | clone a fork |
|
|
1118
1135
|
| `vidfarm share-link <forkId>` | `POST .../compositions/:forkId/share-links` | mint a share URL |
|
|
1119
|
-
| `vidfarm approve --video <url\|file> --caption "…"` | `POST /api/v1/approved/posts` | approve post — takes media by **`url`**. Local `--video`/`--media` auto-upload via presign→S3→finalize (up to **200 MB**, bypasses the ~6 MB Lambda limit) then approve with that URL
|
|
1136
|
+
| `vidfarm approve --video <url\|file> --caption "…"` | `POST /api/v1/approved/posts` | approve post — takes media by **`url`**. Local `--video`/`--media` auto-upload to **durable My Files** via presign→S3→finalize (up to **200 MB**, bypasses the ~6 MB Lambda limit) then approve with that URL, so the share video **never expires**; also accepts a `/files…` path. `--temp` opts into the 30-day temp store (throwaway preview only). Prints `share_url` |
|
|
1120
1137
|
| `vidfarm posts` / `vidfarm post <id>` | `GET /api/v1/approved/posts[/:id]` | browse approved posts |
|
|
1121
1138
|
| `vidfarm schedule <postId> --at <iso> --to <dest>` | `POST /api/v1/approved/posts/:postId/schedules` | schedule a post |
|
|
1122
1139
|
| `vidfarm schedules <postId>` | `GET /api/v1/approved/posts/:postId/schedules` | browse scheduled posts |
|
|
@@ -1152,13 +1169,13 @@ If a local AI script rewrites text or scenes without consuming those files first
|
|
|
1152
1169
|
|
|
1153
1170
|
**Uploads/downloads/My-Files writes live in the devcli** because they are genuine multi-step / streaming flows: `upload` presigns the **ephemeral** temporary-files route, PUTs the bytes **direct to S3**, finalizes, and prints the durable URL to drop into a composition or approved post — going direct to S3 means large files (up to **200 MB**) never hit the ~6 MB Lambda request-body limit; `put-file` posts to the **persistent** My Files route (`/me/attachments/upload`) so context docs and brand assets live in the user's library (accepts a local file, `--content <text>`, or piped `--stdin` with `--as <name>`); `download` streams any Vidfarm/media URL to disk.
|
|
1154
1171
|
|
|
1155
|
-
**Approving a locally rendered MP4 (the URL-first rule).** The approve route (`POST /api/v1/approved/posts`) and every media-taking route accept a `url`, never raw file bytes — so you never "upload to approve" in one shot. The correct sequence for a big local file is: (1) `POST /api/v1/user/me/
|
|
1172
|
+
**Approving a locally rendered MP4 (the URL-first rule + the durability rule).** The approve route (`POST /api/v1/approved/posts`) and every media-taking route accept a `url`, never raw file bytes — so you never "upload to approve" in one shot. An approved post is a **permanent** share page, so the media must live in **durable My Files**, not the 30-day temp store (a temp-hosted video would 404 the share page after 30 days). The correct sequence for a big local file is: (1) `POST /api/v1/user/me/attachments/presign` with `{ file_name, content_type, size_bytes }` → (2) PUT the raw bytes to the returned presigned S3 URL → (3) `POST /api/v1/user/me/attachments` (finalize) → use the returned durable `viewUrl` as the approve media `url`. `vidfarm approve --video ./final.mp4` does all of this automatically (durable by default; up to **200 MB**). Never POST a large file as multipart to `.../attachments/upload` against the cloud host: that path proxies through Lambda and caps near 6 MB (it exists only as a fallback for local-storage `vidfarm serve` boxes). Only use the temp-store route (`.../temporary-files/*`, or `vidfarm approve --temp`) for a **throwaway** preview you don't mind losing in 30 days.
|
|
1156
1173
|
|
|
1157
1174
|
## Cost mode — the devcli's money-saving guardrail
|
|
1158
1175
|
|
|
1159
1176
|
`vidfarm cost-mode <minimize|hybrid|pure-ai>` records a single spend preference (in `~/.vidfarm/cost-mode.json`) that every **billed** command honors: `generate`, `music`, `decompose`, `inspiration-decompose`, `create`, `replicate`, `inpaint`, `create-overlay`, and the cloud paths of `render --target cloud`, `tts --cloud`, `stt --cloud`, `remove-greenscreen` (non-`--local`). FREE local engines never gate (`render` local default, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, all the file-editing verbs). `vidfarm media search` (the free stock catalog — pixabay/openverse/iconify music, SFX, images, icons, stock video) is also free and never gates.
|
|
1160
1177
|
|
|
1161
|
-
- **minimize** — a billed op is **refused** unless you add `--yes`; the error names the free local alternative. Use this to guarantee no surprise AI spend. Before paying to generate music, sound effects, or images, try `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` first — free royalty-free assets instead of a billed `music`/`generate` call.
|
|
1178
|
+
- **minimize** — a billed op is **refused** unless you add `--yes`; the error names the free local alternative (which now includes the matching `vidfarm media search` for music/SFX/image/video). Use this to guarantee no surprise AI spend. Before paying to generate music, sound effects, or images, try `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` first — free royalty-free assets instead of a billed `music`/`generate` call. **Check the keyless sources first — Openverse (CC/CC0 music, SFX, images) and iconify (icons) need no account at all**, so they always work in `minimize`. Photos/vectors/stock-video need a **free Pixabay key** that **may already be saved** — check `vidfarm provider-keys` (or web **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>) before assuming a short result means "no key." If absent, save one once: `vidfarm add-provider-key pixabay <key>` (free key from <https://pixabay.com/api/docs/>), the Settings surface, or hand it to the desktop AI agent to run that command.
|
|
1162
1179
|
- **hybrid** *(default recommendation)* — billed ops run but print a one-line cost notice each.
|
|
1163
1180
|
- **pure-ai** — billed ops run without gating; cost is still printed.
|
|
1164
1181
|
|
|
@@ -1179,7 +1196,7 @@ npx -y @officexapp/vidfarm-devcli serve <template_id>
|
|
|
1179
1196
|
|
|
1180
1197
|
The **editor and its data** run locally (`RECORDS_DRIVER=local`, `STORAGE_DRIVER=local`) and rendering happens **in-process on this box for free**. Everything catalog-shaped still mirrors the cloud host ("cloud passthrough"): `/discover`, `/api/v1/videos`, and `/library` list the **cloud** catalog and the cloud account's approved posts next to anything local, opening a cloud template **seeds** its composition onto disk on demand, Add Template ingests into the cloud account, and approved-post actions (schedules, archive, delete) on cloud posts proxy through. Media a seeded composition already references stays on its cloud URLs — but you can also drop your **own local files** onto the timeline without any upload: `vidfarm place --src ./clip.mp4` copies the file into this box's disk store and references it by a `localhost/storage` URL (see "Local file paths as media" above), so the free in-process renderer plays it back with zero S3 involvement. Pass `--no-cloud` for a fully-offline box.
|
|
1181
1198
|
|
|
1182
|
-
**Rendering from a serve box** — the editor's **Render** button becomes a popover with two options when a cloud `--api-key` is configured: **Render Local (Free)** (in-process
|
|
1199
|
+
**Rendering from a serve box** — the editor's **Render** button becomes a popover with two options when a cloud `--api-key` is configured: **Render Local (Free)** (in-process Vidfarm render, no charge) and **Render in Cloud** (hands the render to the cloud renderer, billed to the cloud account's wallet). Over REST, pass `render_target: "cloud"` in the `POST /render` body; the local box resolves (or clones, once) a publishable cloud fork, remembers the mapping in the fork's `upstream-link.json`, and `GET /renders/:renderId` transparently proxies the cloud job status.
|
|
1183
1200
|
|
|
1184
1201
|
How the loop works:
|
|
1185
1202
|
- The composition lives on disk at `<data-dir>/storage/compositions/forks/<forkId>/working/composition.html` (default `<data-dir>` is `./.vidfarm-local`). Point your agent at that file.
|
|
@@ -1398,7 +1415,7 @@ Apply subtle camouflage transforms (zoom, tilt, rotate, saturation, playback spe
|
|
|
1398
1415
|
- Body: `{ "tracer": "...", "payload": { ...fields... }, "webhook_url"?: "..." }`
|
|
1399
1416
|
- 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.
|
|
1400
1417
|
- Response: standard primitive job. Poll to completion, then read `primary_file_url` (also `video.file_url` for MP4 or `image.file_url` for stills)
|
|
1401
|
-
- Billing: metered as a
|
|
1418
|
+
- Billing: metered as a Vidfarm render (via `hyperframes_lambda` on prod cloud render; free on a local serve box)
|
|
1402
1419
|
|
|
1403
1420
|
Payload fields:
|
|
1404
1421
|
|
|
@@ -1629,7 +1646,7 @@ Use this when a coding agent is doing the work locally or the user wants a repro
|
|
|
1629
1646
|
6. Render with `vidfarm render <forkId> --dir ./work --wait`.
|
|
1630
1647
|
7. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
|
|
1631
1648
|
|
|
1632
|
-
**Approving a locally rendered file → cloud preview link.** Approve takes media by **URL**, not bytes
|
|
1649
|
+
**Approving a locally rendered file → cloud preview link.** Approve takes media by **URL**, not bytes, and an approved post is **permanent** — so the local MP4 must land in **durable My Files**, not the 30-day temp store (a temp video would 404 the share page after 30 days). The devcli presigns, PUTs the bytes **direct to S3**, finalizes, then approves with that durable URL — so `vidfarm approve --video ./final.mp4` handles files up to **200 MB**, bypasses the ~6 MB Lambda request-body limit, and the share link never breaks. By raw REST: `POST /api/v1/user/me/attachments/presign` → PUT to the returned S3 URL → `POST /api/v1/user/me/attachments` (finalize) → pass the returned `viewUrl` in the approve `media` array. Do not multipart-POST a big file to `.../attachments/upload` against the cloud host (Lambda-bound, ~6 MB cap). Add `vidfarm approve --temp` only when you want a disposable 30-day preview.
|
|
1633
1650
|
|
|
1634
1651
|
Prefer this path for batch work, CI-like edits, or when the user wants free local rendering through `vidfarm serve`.
|
|
1635
1652
|
|
package/dist/src/cli.js
CHANGED
|
@@ -504,15 +504,19 @@ Local media engines & toolchain (all local, free, no account — no cloud key ne
|
|
|
504
504
|
|
|
505
505
|
Approve & schedule (publish a finished MP4 as a shareable post):
|
|
506
506
|
approve Approve media into a preview/share page → POST /api/v1/approved/posts
|
|
507
|
-
--video <url|file> Primary MP4 (URL, or
|
|
508
|
-
--media <url|file> Extra media (repeatable; local files
|
|
509
|
-
--folder <path>
|
|
507
|
+
--video <url|file> Primary MP4 (URL, /files path, or local file → durable My Files)
|
|
508
|
+
--media <url|file> Extra media (repeatable; local files → durable My Files)
|
|
509
|
+
--folder <path> Destination subfolder for uploaded local files (default: approved)
|
|
510
|
+
--temp Upload local files to the 30-day TEMP store instead of
|
|
511
|
+
durable My Files (throwaway preview; share video dies at day 30)
|
|
510
512
|
--caption <text> Post caption (required)
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
513
|
+
An approved post is PERMANENT, so a local file uploads
|
|
514
|
+
to DURABLE My Files (never expires) by default —
|
|
515
|
+
presigned + PUT straight to S3 + finalized (up to
|
|
516
|
+
200 MB, bypassing the ~6 MB Lambda body). A locally
|
|
517
|
+
rendered MP4 gets a share link that never breaks:
|
|
515
518
|
'vidfarm approve --video ./final.mp4 --caption "..."'
|
|
519
|
+
(add --temp only for a disposable 30-day preview.)
|
|
516
520
|
posts List your approved posts → GET /api/v1/approved/posts
|
|
517
521
|
post <postId> Read one approved post (prints share URL) → GET /api/v1/approved/posts/:postId
|
|
518
522
|
schedule <postId> Schedule an approved post to a channel → POST /api/v1/approved/posts/:postId/schedules
|
|
@@ -606,7 +610,11 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
|
|
|
606
610
|
Local file paths: to avoid uploading assets to Vidfarm S3 at all, reference
|
|
607
611
|
them straight from disk with 'place --src ./clip.mp4' on a 'serve' box (copied
|
|
608
612
|
to that box's local disk store, free local render). When you DO need a durable
|
|
609
|
-
URL
|
|
613
|
+
URL, mind the store: a 'place'/edit intermediate can live in the throwaway
|
|
614
|
+
'temp/' folder (30-day TTL), but anything you APPROVE is permanent — 'approve'
|
|
615
|
+
uploads local files to DURABLE My Files by default so the share link never dies
|
|
616
|
+
(the 'temp/' store would delete the video after 30 days; use --temp only for a
|
|
617
|
+
disposable preview).
|
|
610
618
|
|
|
611
619
|
Cost spectrum (default to the cheapest approach that works; see SKILL.director.md):
|
|
612
620
|
free Reuse a decomposed template, swap captions/images/existing MP4s
|
|
@@ -1551,7 +1559,7 @@ function printServeBanner(input) {
|
|
|
1551
1559
|
console.log(line);
|
|
1552
1560
|
console.log(` server ${input.base}`);
|
|
1553
1561
|
console.log(` data dir ${input.dataDir}`);
|
|
1554
|
-
console.log(` render ${DIM}local (in-process
|
|
1562
|
+
console.log(` render ${DIM}local (in-process Vidfarm render — free, no cloud charge)${RESET}`);
|
|
1555
1563
|
if (input.login) {
|
|
1556
1564
|
console.log(` login ${GREEN}${input.login.email}${RESET} ${input.login.isPaidPlan ? DIM + "(paid)" + RESET : DIM + "(free)" + RESET}`);
|
|
1557
1565
|
}
|
|
@@ -2824,7 +2832,8 @@ async function runCreateCommand(argv) {
|
|
|
2824
2832
|
guardBilled(ctx, {
|
|
2825
2833
|
label: "create (AI base-VIDEO generation + decompose)",
|
|
2826
2834
|
estimate: "expensive, ~$1+ (AI video)",
|
|
2827
|
-
freeAlternative: "fork a template and paint scenes with raw clips + HTML hyperframes
|
|
2835
|
+
freeAlternative: "fork a template and paint scenes with raw clips + HTML hyperframes, or drop in free stock via " +
|
|
2836
|
+
'vidfarm media search "<meaning>" --type video|image (Pixabay/Openverse, $0) instead of AI video'
|
|
2828
2837
|
});
|
|
2829
2838
|
const refs = await resolveReferenceUrls(ctx, parsed.values.ref);
|
|
2830
2839
|
// 1. Generate the base video from the prompt.
|
|
@@ -3140,7 +3149,7 @@ async function runRenderCommand(argv) {
|
|
|
3140
3149
|
console.log(`${DIM}Media prep: ${prepCuts.map((n) => `${n.action} ×${n.refs} (${n.url.slice(0, 60)}…)`).join("; ")}${RESET}`);
|
|
3141
3150
|
}
|
|
3142
3151
|
console.log(`${GREEN}Rendered locally in ${(result.durationMs / 1000).toFixed(1)}s → ${result.outputPath}${RESET}`);
|
|
3143
|
-
console.log(`${DIM}$0.00 — local render, no cloud charge.${RESET}`);
|
|
3152
|
+
console.log(`${DIM}$0.00 — local Vidfarm render, no cloud charge.${RESET}`);
|
|
3144
3153
|
}
|
|
3145
3154
|
return;
|
|
3146
3155
|
}
|
|
@@ -3494,6 +3503,92 @@ async function uploadLocalToTempStore(ctx, absPath, folder) {
|
|
|
3494
3503
|
throw new Error("local media upload returned no durable URL.");
|
|
3495
3504
|
return url;
|
|
3496
3505
|
}
|
|
3506
|
+
// Pull the durable view URL out of an /me/attachments finalize (or presign)
|
|
3507
|
+
// response. Finalize returns { attachment: { viewUrl } }; presign returns a
|
|
3508
|
+
// top-level view_url before the bytes land.
|
|
3509
|
+
function readAttachmentUrl(json) {
|
|
3510
|
+
const a = json?.attachment ?? {};
|
|
3511
|
+
return a.viewUrl ?? a.view_url ?? a.publicUrl ?? a.public_url ?? json?.view_url ?? json?.s3_url ?? null;
|
|
3512
|
+
}
|
|
3513
|
+
// Upload a local file to DURABLE My Files (root `files`, via /me/attachments),
|
|
3514
|
+
// optionally namescoped under a folder, and return its permanent URL.
|
|
3515
|
+
//
|
|
3516
|
+
// This is the durable twin of uploadLocalToTempStore: same presign → PUT-to-S3
|
|
3517
|
+
// → finalize shape (so a big MP4 never traverses the ~6 MB Lambda body), but the
|
|
3518
|
+
// object is NOT temp-tagged, so it never hits the 30-day lifecycle deletion.
|
|
3519
|
+
// Use it for anything that must outlive 30 days — chiefly a locally rendered MP4
|
|
3520
|
+
// being approved into a shareable cloud post.
|
|
3521
|
+
async function uploadLocalToMyFiles(ctx, absPath, folder) {
|
|
3522
|
+
if (!existsSync(absPath))
|
|
3523
|
+
throw new Error(`No such local file: ${absPath}`);
|
|
3524
|
+
const buffer = readFileSync(absPath);
|
|
3525
|
+
const fileName = path.basename(absPath);
|
|
3526
|
+
const contentType = guessContentType(fileName);
|
|
3527
|
+
// Step 1: presign. On an S3-backed box this returns a direct PUT URL.
|
|
3528
|
+
const presign = await fetch(new URL("/api/v1/user/me/attachments/presign", ctx.host), {
|
|
3529
|
+
method: "POST",
|
|
3530
|
+
headers: { ...buildAuthHeaders(ctx.auth), "content-type": "application/json" },
|
|
3531
|
+
body: JSON.stringify({ file_name: fileName, content_type: contentType, size_bytes: buffer.byteLength, folder_path: folder })
|
|
3532
|
+
});
|
|
3533
|
+
const presignJson = await presign.json().catch(() => null);
|
|
3534
|
+
if (presign.ok && presignJson?.transport === "presigned" && presignJson?.upload?.url) {
|
|
3535
|
+
// Step 2a: PUT the bytes straight to S3 (bypasses the Lambda body limit).
|
|
3536
|
+
const put = await fetch(presignJson.upload.url, {
|
|
3537
|
+
method: presignJson.upload.method || "PUT",
|
|
3538
|
+
headers: presignJson.upload.headers || {},
|
|
3539
|
+
body: new Uint8Array(buffer)
|
|
3540
|
+
});
|
|
3541
|
+
if (!put.ok)
|
|
3542
|
+
throw new Error(`My Files upload failed with HTTP ${put.status}.`);
|
|
3543
|
+
// Step 3: finalize — record the durable attachment now that the bytes landed.
|
|
3544
|
+
const finalize = await fetch(new URL("/api/v1/user/me/attachments", ctx.host), {
|
|
3545
|
+
method: "POST",
|
|
3546
|
+
headers: { ...buildAuthHeaders(ctx.auth), "content-type": "application/json" },
|
|
3547
|
+
body: JSON.stringify({
|
|
3548
|
+
attachment_id: presignJson.attachment_id,
|
|
3549
|
+
file_name: presignJson.file_name || fileName,
|
|
3550
|
+
content_type: presignJson.content_type || contentType,
|
|
3551
|
+
size_bytes: buffer.byteLength,
|
|
3552
|
+
storage_key: presignJson.storage_key,
|
|
3553
|
+
folder_path: presignJson.folder_path
|
|
3554
|
+
})
|
|
3555
|
+
});
|
|
3556
|
+
const finalizeText = await finalize.text();
|
|
3557
|
+
let finalizeJson = null;
|
|
3558
|
+
try {
|
|
3559
|
+
finalizeJson = finalizeText ? JSON.parse(finalizeText) : null;
|
|
3560
|
+
}
|
|
3561
|
+
catch {
|
|
3562
|
+
finalizeJson = null;
|
|
3563
|
+
}
|
|
3564
|
+
if (!finalize.ok)
|
|
3565
|
+
throw new Error(`My Files upload finalize failed (${finalize.status}): ${finalizeText.slice(0, 200)}`);
|
|
3566
|
+
const url = readAttachmentUrl(finalizeJson) ?? presignJson.view_url ?? null;
|
|
3567
|
+
if (!url)
|
|
3568
|
+
throw new Error("My Files upload returned no durable URL.");
|
|
3569
|
+
return url;
|
|
3570
|
+
}
|
|
3571
|
+
// Step 2b (fallback): local-storage box — POST the bytes as multipart form.
|
|
3572
|
+
const form = new FormData();
|
|
3573
|
+
form.append("file", new Blob([buffer], { type: contentType }), fileName);
|
|
3574
|
+
if (folder)
|
|
3575
|
+
form.append("folder_path", folder);
|
|
3576
|
+
const res = await fetch(new URL("/api/v1/user/me/attachments/upload", ctx.host), { method: "POST", headers: buildAuthHeaders(ctx.auth), body: form });
|
|
3577
|
+
const text = await res.text();
|
|
3578
|
+
let json = null;
|
|
3579
|
+
try {
|
|
3580
|
+
json = text ? JSON.parse(text) : null;
|
|
3581
|
+
}
|
|
3582
|
+
catch {
|
|
3583
|
+
json = null;
|
|
3584
|
+
}
|
|
3585
|
+
if (!res.ok)
|
|
3586
|
+
throw new Error(`My Files upload failed (${res.status}): ${text.slice(0, 200)}`);
|
|
3587
|
+
const url = readAttachmentUrl(json);
|
|
3588
|
+
if (!url)
|
|
3589
|
+
throw new Error("My Files upload returned no durable URL.");
|
|
3590
|
+
return url;
|
|
3591
|
+
}
|
|
3497
3592
|
// A value like "/raws/demos/hero.mp4" or "/files/logos/logo.png" is a My Files
|
|
3498
3593
|
// directory path (one of the five unified roots), NOT a URL or a local disk
|
|
3499
3594
|
// path — resolve it through the directory API. (Prefixes are inlined, not a
|
|
@@ -3632,9 +3727,17 @@ async function runGenerateCommand(argv) {
|
|
|
3632
3727
|
? {
|
|
3633
3728
|
label: "AI video generation",
|
|
3634
3729
|
estimate: "expensive, ~$1+",
|
|
3635
|
-
freeAlternative:
|
|
3636
|
-
|
|
3637
|
-
|
|
3730
|
+
freeAlternative: 'free stock video — vidfarm media search "<meaning>" --type video (Pixabay, $0; needs a free ' +
|
|
3731
|
+
"pixabay key — it may already be saved: check vidfarm provider-keys), reuse a raw clip (vidfarm " +
|
|
3732
|
+
"raws), or author the beat as an HTML hyperframe"
|
|
3733
|
+
}
|
|
3734
|
+
: {
|
|
3735
|
+
label: "AI image generation",
|
|
3736
|
+
estimate: "cheap, ~$0.01–$0.05",
|
|
3737
|
+
freeAlternative: 'free stock image/vector — vidfarm media search "<meaning>" --type image|vector ($0). ' +
|
|
3738
|
+
"Openverse (CC images) is keyless and always on — check it first; a free Pixabay key adds " +
|
|
3739
|
+
"photos/vectors and may already be saved (check vidfarm provider-keys)"
|
|
3740
|
+
});
|
|
3638
3741
|
const refs = await resolveReferenceUrls(ctx, parsed.values.ref);
|
|
3639
3742
|
const payload = { prompt };
|
|
3640
3743
|
if (parsed.values["aspect-ratio"])
|
|
@@ -4516,7 +4619,8 @@ async function runMusicCommand(argv) {
|
|
|
4516
4619
|
guardBilled(ctx, {
|
|
4517
4620
|
label: `AI music generation${parsed.values["own-key"] ? " (your ElevenLabs key)" : ""}`,
|
|
4518
4621
|
estimate: "billed (ElevenLabs)",
|
|
4519
|
-
freeAlternative:
|
|
4622
|
+
freeAlternative: 'free stock music/SFX — vidfarm media search "<meaning>" --type bgm|sfx (Openverse CC/CC0, $0, ' +
|
|
4623
|
+
"no key needed), or reuse a track from My Files / your library"
|
|
4520
4624
|
});
|
|
4521
4625
|
let musicLengthMs;
|
|
4522
4626
|
const lengthMs = parsed.values["length-ms"]?.trim();
|
|
@@ -5618,6 +5722,7 @@ async function runApproveCommand(argv) {
|
|
|
5618
5722
|
title: { type: "string" },
|
|
5619
5723
|
pinned: { type: "string" },
|
|
5620
5724
|
folder: { type: "string" },
|
|
5725
|
+
temp: { type: "boolean" },
|
|
5621
5726
|
tracer: { type: "string" }
|
|
5622
5727
|
}
|
|
5623
5728
|
});
|
|
@@ -5625,15 +5730,29 @@ async function runApproveCommand(argv) {
|
|
|
5625
5730
|
const caption = parsed.values.caption;
|
|
5626
5731
|
if (!caption)
|
|
5627
5732
|
throw new Error("approve requires --caption <text>.");
|
|
5628
|
-
// An approved post is a shareable cloud page, so
|
|
5629
|
-
//
|
|
5630
|
-
//
|
|
5631
|
-
|
|
5733
|
+
// An approved post is a PERMANENT shareable cloud page, so its media must live
|
|
5734
|
+
// as long as the page. A local file therefore uploads to DURABLE My Files by
|
|
5735
|
+
// default (never expires). `--temp` opts into the 30-day temp store for a
|
|
5736
|
+
// throwaway preview — the share video then stops loading after 30 days.
|
|
5737
|
+
const useTemp = Boolean(parsed.values.temp);
|
|
5738
|
+
const uploadFolder = parsed.values.folder ?? (useTemp ? "temp" : "approved");
|
|
5632
5739
|
const resolveApproveMedia = async (value) => {
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5636
|
-
|
|
5740
|
+
const v = value.trim();
|
|
5741
|
+
if (/^https?:\/\//i.test(v))
|
|
5742
|
+
return v;
|
|
5743
|
+
// A My Files path (/files/… · /raws/… · /projects/… · /approved/… · /temp/…)
|
|
5744
|
+
// already lives server-side — resolve it to its view URL instead of
|
|
5745
|
+
// re-uploading. (A /temp/… path stays 30-day; the caller chose it.)
|
|
5746
|
+
if (looksLikeMyFilesPath(v)) {
|
|
5747
|
+
const resolved = await resolveMyFilesPathToUrl(ctx, v);
|
|
5748
|
+
if (!resolved)
|
|
5749
|
+
throw new Error(`approve media "${value}" looks like a My Files path but no file was found there. Browse it with \`vidfarm directory ls ${v.replace(/\/[^/]*$/, "") || "/"}\` (add --cloud if it lives on vidfarm.cc), or pass the file's view URL.`);
|
|
5750
|
+
return resolved.url;
|
|
5751
|
+
}
|
|
5752
|
+
const abs = path.resolve(process.cwd(), v);
|
|
5753
|
+
return useTemp
|
|
5754
|
+
? uploadLocalToTempStore(ctx, abs, uploadFolder)
|
|
5755
|
+
: uploadLocalToMyFiles(ctx, abs, uploadFolder);
|
|
5637
5756
|
};
|
|
5638
5757
|
const media = [];
|
|
5639
5758
|
if (parsed.values.video)
|
|
@@ -87,8 +87,12 @@ export function resolveCostMode(values) {
|
|
|
87
87
|
}
|
|
88
88
|
export const COST_MODE_BLURB = {
|
|
89
89
|
minimize: "Minimize costs — stay on FREE local compute wherever possible (local render, " +
|
|
90
|
-
"local TTS/STT, local greenscreen/matting, reused clips + HTML motion).
|
|
91
|
-
|
|
90
|
+
"local TTS/STT, local greenscreen/matting, reused clips + HTML motion). For assets, " +
|
|
91
|
+
'reach for the free stock catalog first — vidfarm media search "<meaning>" --type ' +
|
|
92
|
+
"bgm|sfx|image|vector|icon|video. Check the KEYLESS sources first: Openverse (CC/CC0 " +
|
|
93
|
+
"music, SFX, images) and iconify (icons) need no account at all. A free Pixabay key adds " +
|
|
94
|
+
"photos/vectors/stock-video and may already be saved (check vidfarm provider-keys). " +
|
|
95
|
+
"Billed cloud/AI generation is refused unless you re-confirm it (--yes).",
|
|
92
96
|
hybrid: "Hybrid (recommended) — free where it's free, spend AI credits only where they " +
|
|
93
97
|
"clearly win (a hero shot, a voice you can't fake locally). Billed ops run but " +
|
|
94
98
|
"each prints its cost so nothing is a surprise.",
|
|
@@ -104,9 +108,14 @@ export function costModeSummaryLine(resolved) {
|
|
|
104
108
|
export function costModeExplainer() {
|
|
105
109
|
return [
|
|
106
110
|
"How much do you want Vidfarm to spend on AI credits?",
|
|
107
|
-
" • minimize — cheapest: free local compute, no surprise AI spend.",
|
|
111
|
+
" • minimize — cheapest: free local compute + free stock media, no surprise AI spend.",
|
|
108
112
|
" • hybrid — recommended: free where free, pay AI only where it clearly wins.",
|
|
109
|
-
" • pure-ai — best quality: use AI image/video/voice/music freely."
|
|
113
|
+
" • pure-ai — best quality: use AI image/video/voice/music freely.",
|
|
114
|
+
"Tip: before paying to generate music/SFX/images/video, try the free stock catalog —",
|
|
115
|
+
' vidfarm media search "<meaning>" --type bgm|sfx|image|vector|icon|video.',
|
|
116
|
+
" Check the keyless sources first — Openverse (CC/CC0 music, SFX, images) and iconify (icons)",
|
|
117
|
+
" need no account. A free Pixabay key adds photos/vectors/stock-video (it may already be saved —",
|
|
118
|
+
" check `vidfarm provider-keys` or vidfarm.cc/settings/developer; add via `vidfarm add-provider-key pixabay <key>`)."
|
|
110
119
|
].join("\n");
|
|
111
120
|
}
|
|
112
121
|
export class CostModeBlockedError extends Error {
|
|
@@ -420,7 +420,7 @@ function localRenderStatusJson(renderId, record) {
|
|
|
420
420
|
title: record.title,
|
|
421
421
|
status: record.status,
|
|
422
422
|
progress,
|
|
423
|
-
cost: "$0.00 (local render)",
|
|
423
|
+
cost: "$0.00 (local Vidfarm render)",
|
|
424
424
|
outputUrl: record.status === "SUCCEEDED" ? `/local-renders/${encodeURIComponent(renderId)}.mp4` : undefined,
|
|
425
425
|
error: record.error,
|
|
426
426
|
fatalErrorEncountered: record.status === "FAILED" ? true : undefined
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@officexapp/vidfarm-devcli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.12",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -1266,12 +1266,25 @@ html,body{margin:0;background:#050604;color:#fffbe6}
|
|
|
1266
1266
|
// Desktop-agents nudge banner: a full-width alert pinned to the top of the
|
|
1267
1267
|
// /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
|
|
1268
1268
|
// this chrome script, so gate strictly by pathname. Dismissible per session.
|
|
1269
|
+
// The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
|
|
1270
|
+
// a desktop AI agent). A client reviewing a shared video in the editor should
|
|
1271
|
+
// NOT be told to go install Claude Code / Codex — so on the editor we suppress
|
|
1272
|
+
// it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
|
|
1269
1273
|
(function mountDesktopAgentsBanner() {
|
|
1270
1274
|
try {
|
|
1271
1275
|
var path = location.pathname;
|
|
1272
1276
|
var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
|
|
1273
1277
|
var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
|
|
1274
1278
|
if (!onEditor && !onChat) return;
|
|
1279
|
+
if (onEditor) {
|
|
1280
|
+
var bootEl = document.getElementById('hf-boot');
|
|
1281
|
+
if (bootEl) {
|
|
1282
|
+
try {
|
|
1283
|
+
var boot = JSON.parse(bootEl.textContent || '{}');
|
|
1284
|
+
if (boot && boot.freeTier) return;
|
|
1285
|
+
} catch (e) {}
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1275
1288
|
if (document.querySelector('.vf-topbanner')) return;
|
|
1276
1289
|
if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
|
|
1277
1290
|
var GUIDE = '/blog/desktop-ai-agents';
|
|
@@ -1575,12 +1575,25 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
|
|
|
1575
1575
|
// Desktop-agents nudge banner: a full-width alert pinned to the top of the
|
|
1576
1576
|
// /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
|
|
1577
1577
|
// this chrome script, so gate strictly by pathname. Dismissible per session.
|
|
1578
|
+
// The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
|
|
1579
|
+
// a desktop AI agent). A client reviewing a shared video in the editor should
|
|
1580
|
+
// NOT be told to go install Claude Code / Codex — so on the editor we suppress
|
|
1581
|
+
// it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
|
|
1578
1582
|
(function mountDesktopAgentsBanner() {
|
|
1579
1583
|
try {
|
|
1580
1584
|
var path = location.pathname;
|
|
1581
1585
|
var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
|
|
1582
1586
|
var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
|
|
1583
1587
|
if (!onEditor && !onChat) return;
|
|
1588
|
+
if (onEditor) {
|
|
1589
|
+
var bootEl = document.getElementById('hf-boot');
|
|
1590
|
+
if (bootEl) {
|
|
1591
|
+
try {
|
|
1592
|
+
var boot = JSON.parse(bootEl.textContent || '{}');
|
|
1593
|
+
if (boot && boot.freeTier) return;
|
|
1594
|
+
} catch (e) {}
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1584
1597
|
if (document.querySelector('.vf-topbanner')) return;
|
|
1585
1598
|
if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
|
|
1586
1599
|
var GUIDE = '/blog/desktop-ai-agents';
|
|
@@ -2529,12 +2529,25 @@ button.rk-clips-thumb{appearance:none;-webkit-appearance:none;border:0;margin:0;
|
|
|
2529
2529
|
// Desktop-agents nudge banner: a full-width alert pinned to the top of the
|
|
2530
2530
|
// /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
|
|
2531
2531
|
// this chrome script, so gate strictly by pathname. Dismissible per session.
|
|
2532
|
+
// The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
|
|
2533
|
+
// a desktop AI agent). A client reviewing a shared video in the editor should
|
|
2534
|
+
// NOT be told to go install Claude Code / Codex — so on the editor we suppress
|
|
2535
|
+
// it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
|
|
2532
2536
|
(function mountDesktopAgentsBanner() {
|
|
2533
2537
|
try {
|
|
2534
2538
|
var path = location.pathname;
|
|
2535
2539
|
var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
|
|
2536
2540
|
var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
|
|
2537
2541
|
if (!onEditor && !onChat) return;
|
|
2542
|
+
if (onEditor) {
|
|
2543
|
+
var bootEl = document.getElementById('hf-boot');
|
|
2544
|
+
if (bootEl) {
|
|
2545
|
+
try {
|
|
2546
|
+
var boot = JSON.parse(bootEl.textContent || '{}');
|
|
2547
|
+
if (boot && boot.freeTier) return;
|
|
2548
|
+
} catch (e) {}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2538
2551
|
if (document.querySelector('.vf-topbanner')) return;
|
|
2539
2552
|
if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
|
|
2540
2553
|
var GUIDE = '/blog/desktop-ai-agents';
|
|
@@ -2116,12 +2116,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2116
2116
|
// Desktop-agents nudge banner: a full-width alert pinned to the top of the
|
|
2117
2117
|
// /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
|
|
2118
2118
|
// this chrome script, so gate strictly by pathname. Dismissible per session.
|
|
2119
|
+
// The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
|
|
2120
|
+
// a desktop AI agent). A client reviewing a shared video in the editor should
|
|
2121
|
+
// NOT be told to go install Claude Code / Codex — so on the editor we suppress
|
|
2122
|
+
// it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
|
|
2119
2123
|
(function mountDesktopAgentsBanner() {
|
|
2120
2124
|
try {
|
|
2121
2125
|
var path = location.pathname;
|
|
2122
2126
|
var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
|
|
2123
2127
|
var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
|
|
2124
2128
|
if (!onEditor && !onChat) return;
|
|
2129
|
+
if (onEditor) {
|
|
2130
|
+
var bootEl = document.getElementById('hf-boot');
|
|
2131
|
+
if (bootEl) {
|
|
2132
|
+
try {
|
|
2133
|
+
var boot = JSON.parse(bootEl.textContent || '{}');
|
|
2134
|
+
if (boot && boot.freeTier) return;
|
|
2135
|
+
} catch (e) {}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2125
2138
|
if (document.querySelector('.vf-topbanner')) return;
|
|
2126
2139
|
if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
|
|
2127
2140
|
var GUIDE = '/blog/desktop-ai-agents';
|
|
@@ -3709,12 +3709,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
3709
3709
|
// Desktop-agents nudge banner: a full-width alert pinned to the top of the
|
|
3710
3710
|
// /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
|
|
3711
3711
|
// this chrome script, so gate strictly by pathname. Dismissible per session.
|
|
3712
|
+
// The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
|
|
3713
|
+
// a desktop AI agent). A client reviewing a shared video in the editor should
|
|
3714
|
+
// NOT be told to go install Claude Code / Codex — so on the editor we suppress
|
|
3715
|
+
// it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
|
|
3712
3716
|
(function mountDesktopAgentsBanner() {
|
|
3713
3717
|
try {
|
|
3714
3718
|
var path = location.pathname;
|
|
3715
3719
|
var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
|
|
3716
3720
|
var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
|
|
3717
3721
|
if (!onEditor && !onChat) return;
|
|
3722
|
+
if (onEditor) {
|
|
3723
|
+
var bootEl = document.getElementById('hf-boot');
|
|
3724
|
+
if (bootEl) {
|
|
3725
|
+
try {
|
|
3726
|
+
var boot = JSON.parse(bootEl.textContent || '{}');
|
|
3727
|
+
if (boot && boot.freeTier) return;
|
|
3728
|
+
} catch (e) {}
|
|
3729
|
+
}
|
|
3730
|
+
}
|
|
3718
3731
|
if (document.querySelector('.vf-topbanner')) return;
|
|
3719
3732
|
if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
|
|
3720
3733
|
var GUIDE = '/blog/desktop-ai-agents';
|
|
@@ -2529,12 +2529,25 @@ body.vf-has-topbanner.rk-has-sidebar .rk-content{padding-top:var(--vf-topbanner-
|
|
|
2529
2529
|
// Desktop-agents nudge banner: a full-width alert pinned to the top of the
|
|
2530
2530
|
// /editor and /chat surfaces pointing to the setup guide. Both surfaces embed
|
|
2531
2531
|
// this chrome script, so gate strictly by pathname. Dismissible per session.
|
|
2532
|
+
// The nudge is aimed at the DIRECTOR (the paid operator who drives Vidfarm with
|
|
2533
|
+
// a desktop AI agent). A client reviewing a shared video in the editor should
|
|
2534
|
+
// NOT be told to go install Claude Code / Codex — so on the editor we suppress
|
|
2535
|
+
// it for free-tier (client / reviewer) sessions, read from the editor boot JSON.
|
|
2532
2536
|
(function mountDesktopAgentsBanner() {
|
|
2533
2537
|
try {
|
|
2534
2538
|
var path = location.pathname;
|
|
2535
2539
|
var onEditor = path === '/editor' || path.indexOf('/editor/') === 0;
|
|
2536
2540
|
var onChat = path === '/chat' || path.indexOf('/chat/') === 0;
|
|
2537
2541
|
if (!onEditor && !onChat) return;
|
|
2542
|
+
if (onEditor) {
|
|
2543
|
+
var bootEl = document.getElementById('hf-boot');
|
|
2544
|
+
if (bootEl) {
|
|
2545
|
+
try {
|
|
2546
|
+
var boot = JSON.parse(bootEl.textContent || '{}');
|
|
2547
|
+
if (boot && boot.freeTier) return;
|
|
2548
|
+
} catch (e) {}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2538
2551
|
if (document.querySelector('.vf-topbanner')) return;
|
|
2539
2552
|
if (sessionStorage.getItem('vf-topbanner-dismissed') === '1') return;
|
|
2540
2553
|
var GUIDE = '/blog/desktop-ai-agents';
|