@officexapp/vidfarm-devcli 0.21.10 → 0.21.11

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.
@@ -50,7 +50,7 @@ Do not use this skill to author new templates from scratch, deploy platform infr
50
50
 
51
51
  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
52
 
53
- - **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). No surprise AI spend.
53
+ - **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.
54
54
  - **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
55
  - **pure-ai** — best quality; use AI image/video/voice/music freely.
56
56
 
@@ -8,6 +8,8 @@ Use this when a coding agent is doing the work locally or the user wants a repro
8
8
  4. Make deterministic edits to `composition.html` and optionally `composition.json`.
9
9
  5. Validate with `vidfarm lint` or `vidfarm stills` when useful.
10
10
  6. Render with `vidfarm render <forkId> --dir ./work --wait`.
11
- 7. Approve the finished MP4 with `vidfarm approve --video <url> --caption "..."`.
11
+ 7. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
12
+
13
+ **Approving a locally rendered file → cloud preview link.** Approve takes media by **URL**, not bytes. A local MP4 must become a URL first: the devcli presigns, PUTs the bytes **direct to S3**, finalizes, then approves with that URL — so `vidfarm approve --video ./final.mp4` handles files up to **200 MB** and bypasses the ~6 MB Lambda request-body limit. By raw REST: `POST /api/v1/user/me/temporary-files/presign` → PUT to the returned S3 URL → `POST /api/v1/user/me/temporary-files` (finalize) → pass the durable URL in the approve `media` array. Do not multipart-POST a big file to `.../temporary-files/upload` against the cloud host (Lambda-bound, ~6 MB cap).
12
14
 
13
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 (local `--video`/`--media` auto-upload to `temp/`; prints `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; 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 |
@@ -157,7 +157,7 @@ If a local AI script rewrites text or scenes without consuming those files first
157
157
  | `vidfarm cost-mode [minimize\|hybrid\|pure-ai]` | (local `~/.vidfarm/cost-mode.json`) | show/set the money-saving preference every billed command respects; no arg = show + explain the three; `--clear` forgets it |
158
158
  | `vidfarm wallet [--job <id>\|--tracer <t>] [--limit <n>]` | `GET /api/v1/user/me/wallet` | cost log: balance + lifetime spend + recent charges. `--job <renderJobId>` prints **what that one video cost** (sums its charges); `--tracer <t>` sums a tracer. Cloud-only, paid plan. Aliases: `spend`, `costs` |
159
159
  | `vidfarm provider-keys` / `vidfarm add-provider-key <p> <secret>` | `GET`·`POST /api/v1/user/me/provider-keys` | manage AI keys |
160
- | `vidfarm upload <file> [--folder <path>]` | `POST /api/v1/user/me/temporary-files/upload` | upload → durable URL (ephemeral; prefer `--folder temp` for scratch) |
160
+ | `vidfarm upload <file> [--folder <path>]` | presign → S3 PUT → finalize (`.../temporary-files/presign` + `.../temporary-files`) | upload → durable URL (ephemeral, 30-day TTL; prefer `--folder temp` for scratch). Goes direct to S3, so large files (up to **200 MB**) bypass the ~6 MB Lambda body limit |
161
161
  | `vidfarm download <url> [dest]` | (streams any URL to disk) | download media |
162
162
  | `vidfarm download-post <url> [--quality best\|hd\|full_hd]` | `POST /api/v1/primitives/videos/download` + poll | download a social/media post into a durable MP4 or slideshow |
163
163
  | `vidfarm download-post-audio <url>` | `POST /api/v1/primitives/audio/download` + poll | download a social/media post's audio into a durable audio file |
@@ -182,13 +182,15 @@ If a local AI script rewrites text or scenes without consuming those files first
182
182
 
183
183
  **Frontend URLs are first-class output.** Every command that has a human-openable page (editor, discover, approved-post preview, share link) prints that prod frontend URL as a highlighted line. `render --wait` polls to completion and prints the final MP4 URL; `approve` prints the approved-post `share_url`. Add `--json` to any command for pure JSON (agent-friendly, no banners).
184
184
 
185
- **Uploads/downloads/My-Files writes live in the devcli** because they are genuine multi-step / streaming flows: `upload` posts the file as multipart to the **ephemeral** temporary-files route and prints the durable URL to drop into a composition or approved post; `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.
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
+
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/temporary-files/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/temporary-files` (finalize) → use the returned durable URL as the approve media `url`. `vidfarm approve --video ./final.mp4` does all of this automatically. Never POST a large file as multipart to `.../temporary-files/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).
186
188
 
187
189
  ## Cost mode — the devcli's money-saving guardrail
188
190
 
189
- `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).
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.
190
192
 
191
- - **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.
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.
192
194
  - **hybrid** *(default recommendation)* — billed ops run but print a one-line cost notice each.
193
195
  - **pure-ai** — billed ops run without gating; cost is still printed.
194
196
 
@@ -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.
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/temporary-files/presign` → PUT the bytes straight to the returned S3 URL → `POST /api/v1/user/me/temporary-files` to finalize → use the returned durable URL 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 `.../temporary-files/upload` against the cloud host — that path routes through Lambda and caps near 6 MB (it is only for local-storage `vidfarm serve` boxes). Temp files carry a 30-day TTL.
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,18 +141,23 @@ Same ownership rule, resolved through the file directory:
141
141
 
142
142
  ### Free media catalog — sources & keys
143
143
 
144
- Still no customer signup: the catalog runs on **keyless** sources and **platform-held** keys, never a per-user account.
144
+ Icons + CC images/audio are **keyless**. Pixabay (stock photos/vectors/video) is
145
+ **BYOK** — each user saves their own free Pixabay key, exactly like their OpenAI /
146
+ Gemini keys, through the same provider-key surface (Settings → Bring your own keys,
147
+ `POST /api/v1/user/me/provider-keys` with `provider: "pixabay"`, or
148
+ `vidfarm add-provider-key pixabay <key>`). It is a stock-media key, not an AI key —
149
+ it never counts as a qualified AI provider and never touches editor chat.
145
150
 
146
151
  | Source | Serves | Key |
147
152
  | --- | --- | --- |
148
153
  | **iconify** | `icon` (SVG) | keyless — always on |
149
154
  | **openverse** | `image` (CC), `bgm`, `sfx` (CC/CC0 audio) | keyless — always on |
150
- | **pixabay** | `image`, `vector`, `video` (photos/illustrations/stock video) | needs a **platform** `PIXABAY_API_KEY` |
151
-
152
- **Recommend keys when a search comes up short.** If `image`/`vector`/`video`
153
- returns nothing (or `providers_used` omits `pixabay`), the platform's Pixabay key
154
- isn't set — tell the operator they can unlock stock photos/vectors/video with a
155
- **free** key from <https://pixabay.com/api/docs/> (set `PIXABAY_API_KEY` server-side;
156
- `OPENVERSE_API_TOKEN` is optional and only raises rate limits). Icons, CC images,
157
- and bgm/sfx keep working without any key. This is an operator/platform key — the
158
- end user still never signs up for anything.
155
+ | **pixabay** | `image`, `vector`, `video` (photos/illustrations/stock video) | **BYOK** — the user's own free `pixabay` provider key |
156
+
157
+ **Recommend the key when a search comes up short.** If `image`/`vector`/`video`
158
+ returns nothing (or `providers_used` omits `pixabay`), the caller hasn't saved a
159
+ Pixabay key — tell them to grab a **free** one at <https://pixabay.com/api/docs/>
160
+ and save it (`vidfarm add-provider-key pixabay <key>`, or Settings in the web app).
161
+ Icons, CC images, and bgm/sfx keep working without any key. (An operator may still
162
+ set a platform `PIXABAY_API_KEY` server-side as a fallback for users who haven't
163
+ saved their own, but the default posture is bring-your-own.)
package/SKILL.director.md CHANGED
@@ -50,7 +50,7 @@ Do not use this skill to author new templates from scratch, deploy platform infr
50
50
 
51
51
  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
52
 
53
- - **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). No surprise AI spend.
53
+ - **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.
54
54
  - **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
55
  - **pure-ai** — best quality; use AI image/video/voice/music freely.
56
56
 
@@ -1116,7 +1116,7 @@ If a local AI script rewrites text or scenes without consuming those files first
1116
1116
  | `vidfarm visibility <forkId> <private\|public>` | `PATCH .../compositions/:forkId/visibility` | set visibility |
1117
1117
  | `vidfarm clone <forkId>` | `POST .../compositions/:forkId/clone` | clone a fork |
1118
1118
  | `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 (local `--video`/`--media` auto-upload to `temp/`; prints `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; prints `share_url` |
1120
1120
  | `vidfarm posts` / `vidfarm post <id>` | `GET /api/v1/approved/posts[/:id]` | browse approved posts |
1121
1121
  | `vidfarm schedule <postId> --at <iso> --to <dest>` | `POST /api/v1/approved/posts/:postId/schedules` | schedule a post |
1122
1122
  | `vidfarm schedules <postId>` | `GET /api/v1/approved/posts/:postId/schedules` | browse scheduled posts |
@@ -1125,7 +1125,7 @@ If a local AI script rewrites text or scenes without consuming those files first
1125
1125
  | `vidfarm cost-mode [minimize\|hybrid\|pure-ai]` | (local `~/.vidfarm/cost-mode.json`) | show/set the money-saving preference every billed command respects; no arg = show + explain the three; `--clear` forgets it |
1126
1126
  | `vidfarm wallet [--job <id>\|--tracer <t>] [--limit <n>]` | `GET /api/v1/user/me/wallet` | cost log: balance + lifetime spend + recent charges. `--job <renderJobId>` prints **what that one video cost** (sums its charges); `--tracer <t>` sums a tracer. Cloud-only, paid plan. Aliases: `spend`, `costs` |
1127
1127
  | `vidfarm provider-keys` / `vidfarm add-provider-key <p> <secret>` | `GET`·`POST /api/v1/user/me/provider-keys` | manage AI keys |
1128
- | `vidfarm upload <file> [--folder <path>]` | `POST /api/v1/user/me/temporary-files/upload` | upload → durable URL (ephemeral; prefer `--folder temp` for scratch) |
1128
+ | `vidfarm upload <file> [--folder <path>]` | presign → S3 PUT → finalize (`.../temporary-files/presign` + `.../temporary-files`) | upload → durable URL (ephemeral, 30-day TTL; prefer `--folder temp` for scratch). Goes direct to S3, so large files (up to **200 MB**) bypass the ~6 MB Lambda body limit |
1129
1129
  | `vidfarm download <url> [dest]` | (streams any URL to disk) | download media |
1130
1130
  | `vidfarm download-post <url> [--quality best\|hd\|full_hd]` | `POST /api/v1/primitives/videos/download` + poll | download a social/media post into a durable MP4 or slideshow |
1131
1131
  | `vidfarm download-post-audio <url>` | `POST /api/v1/primitives/audio/download` + poll | download a social/media post's audio into a durable audio file |
@@ -1150,13 +1150,15 @@ If a local AI script rewrites text or scenes without consuming those files first
1150
1150
 
1151
1151
  **Frontend URLs are first-class output.** Every command that has a human-openable page (editor, discover, approved-post preview, share link) prints that prod frontend URL as a highlighted line. `render --wait` polls to completion and prints the final MP4 URL; `approve` prints the approved-post `share_url`. Add `--json` to any command for pure JSON (agent-friendly, no banners).
1152
1152
 
1153
- **Uploads/downloads/My-Files writes live in the devcli** because they are genuine multi-step / streaming flows: `upload` posts the file as multipart to the **ephemeral** temporary-files route and prints the durable URL to drop into a composition or approved post; `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.
1153
+ **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
+
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/temporary-files/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/temporary-files` (finalize) → use the returned durable URL as the approve media `url`. `vidfarm approve --video ./final.mp4` does all of this automatically. Never POST a large file as multipart to `.../temporary-files/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).
1154
1156
 
1155
1157
  ## Cost mode — the devcli's money-saving guardrail
1156
1158
 
1157
- `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).
1159
+ `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.
1158
1160
 
1159
- - **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.
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.
1160
1162
  - **hybrid** *(default recommendation)* — billed ops run but print a one-line cost notice each.
1161
1163
  - **pure-ai** — billed ops run without gating; cost is still printed.
1162
1164
 
@@ -1625,7 +1627,9 @@ Use this when a coding agent is doing the work locally or the user wants a repro
1625
1627
  4. Make deterministic edits to `composition.html` and optionally `composition.json`.
1626
1628
  5. Validate with `vidfarm lint` or `vidfarm stills` when useful.
1627
1629
  6. Render with `vidfarm render <forkId> --dir ./work --wait`.
1628
- 7. Approve the finished MP4 with `vidfarm approve --video <url> --caption "..."`.
1630
+ 7. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
1631
+
1632
+ **Approving a locally rendered file → cloud preview link.** Approve takes media by **URL**, not bytes. A local MP4 must become a URL first: the devcli presigns, PUTs the bytes **direct to S3**, finalizes, then approves with that URL — so `vidfarm approve --video ./final.mp4` handles files up to **200 MB** and bypasses the ~6 MB Lambda request-body limit. By raw REST: `POST /api/v1/user/me/temporary-files/presign` → PUT to the returned S3 URL → `POST /api/v1/user/me/temporary-files` (finalize) → pass the durable URL in the approve `media` array. Do not multipart-POST a big file to `.../temporary-files/upload` against the cloud host (Lambda-bound, ~6 MB cap).
1629
1633
 
1630
1634
  Prefer this path for batch work, CI-like edits, or when the user wants free local rendering through `vidfarm serve`.
1631
1635
 
package/dist/src/cli.js CHANGED
@@ -508,7 +508,10 @@ Approve & schedule (publish a finished MP4 as a shareable post):
508
508
  --media <url|file> Extra media (repeatable; local files auto-upload to temp/)
509
509
  --folder <path> Temp-store folder for uploaded local files (default: temp)
510
510
  --caption <text> Post caption (required)
511
- Direct local-file upload is supported here:
511
+ Approve takes media by URL; a local file is
512
+ presigned + PUT straight to S3 + finalized into a
513
+ durable URL first (up to 200 MB — bypasses the ~6 MB
514
+ Lambda body limit), so a locally rendered MP4 works:
512
515
  'vidfarm approve --video ./final.mp4 --caption "..."'
513
516
  posts List your approved posts → GET /api/v1/approved/posts
514
517
  post <postId> Read one approved post (prints share URL) → GET /api/v1/approved/posts/:postId
@@ -532,10 +535,12 @@ Agent skill (install the latest director skill so your AI agent can act):
532
535
  (copy/share artifact; not the canonical install)
533
536
 
534
537
  Files (multi-step flows the devcli handles for you):
535
- upload <file> Upload to the EPHEMERAL temp store, print URL → POST /api/v1/user/me/temporary-files/upload
538
+ upload <file> Upload to the EPHEMERAL temp store, print URL → presign + S3 PUT + finalize
536
539
  --folder <path> Namescope the upload under a folder. Prefer a
537
540
  throwaway 'temp' folder (--folder temp) for scratch
538
541
  media so it never clutters your durable library.
542
+ Bytes go direct to S3, so large files (up to 200 MB)
543
+ skip the ~6 MB Lambda request-body limit.
539
544
  put-file [file] Write a file INTO My Files (persistent lib) → POST /api/v1/user/me/attachments/upload
540
545
  --folder <path> Namescope under a product/offer folder (e.g. acme-skincare)
541
546
  --as <name> Name the saved file (required for --content/--stdin)
@@ -3412,11 +3417,65 @@ function serveStorageUrl(baseUrl, key) {
3412
3417
  }
3413
3418
  // Upload a local file to the EPHEMERAL temp store on ctx.host, optionally
3414
3419
  // namescoped under a folder, and return its durable URL.
3420
+ //
3421
+ // Prefers the presign → PUT-to-S3 → finalize path so the bytes never traverse
3422
+ // the Lambda request body (which caps at ~6 MB). This is what lets a locally
3423
+ // rendered MP4 (up to 200 MB) be uploaded to the cloud for approval + a
3424
+ // shareable preview link. Falls back to the multipart POST when the box runs on
3425
+ // local storage (presign transport === "server"), where there is no S3.
3415
3426
  async function uploadLocalToTempStore(ctx, absPath, folder) {
3416
3427
  if (!existsSync(absPath))
3417
3428
  throw new Error(`No such local file: ${absPath}`);
3429
+ const buffer = readFileSync(absPath);
3430
+ const fileName = path.basename(absPath);
3431
+ const contentType = guessContentType(fileName);
3432
+ // Step 1: presign. On an S3-backed box this returns a direct PUT URL.
3433
+ const presign = await fetch(new URL("/api/v1/user/me/temporary-files/presign", ctx.host), {
3434
+ method: "POST",
3435
+ headers: { ...buildAuthHeaders(ctx.auth), "content-type": "application/json" },
3436
+ body: JSON.stringify({ file_name: fileName, content_type: contentType, size_bytes: buffer.byteLength, folder_path: folder })
3437
+ });
3438
+ const presignJson = await presign.json().catch(() => null);
3439
+ if (presign.ok && presignJson?.transport === "presigned" && presignJson?.upload?.url) {
3440
+ // Step 2a: PUT the bytes straight to S3 (bypasses the Lambda body limit).
3441
+ const put = await fetch(presignJson.upload.url, {
3442
+ method: presignJson.upload.method || "PUT",
3443
+ headers: presignJson.upload.headers || {},
3444
+ body: new Uint8Array(buffer)
3445
+ });
3446
+ if (!put.ok)
3447
+ throw new Error(`local media upload failed with HTTP ${put.status}.`);
3448
+ // Step 3: finalize — record the temp file now that the bytes have landed.
3449
+ const finalize = await fetch(new URL("/api/v1/user/me/temporary-files", ctx.host), {
3450
+ method: "POST",
3451
+ headers: { ...buildAuthHeaders(ctx.auth), "content-type": "application/json" },
3452
+ body: JSON.stringify({
3453
+ file_id: presignJson.file_id,
3454
+ file_name: presignJson.file_name || fileName,
3455
+ content_type: presignJson.content_type || contentType,
3456
+ size_bytes: buffer.byteLength,
3457
+ storage_key: presignJson.storage_key,
3458
+ folder_path: presignJson.folder_path
3459
+ })
3460
+ });
3461
+ const finalizeText = await finalize.text();
3462
+ let finalizeJson = null;
3463
+ try {
3464
+ finalizeJson = finalizeText ? JSON.parse(finalizeText) : null;
3465
+ }
3466
+ catch {
3467
+ finalizeJson = null;
3468
+ }
3469
+ if (!finalize.ok)
3470
+ throw new Error(`local media upload finalize failed (${finalize.status}): ${finalizeText.slice(0, 200)}`);
3471
+ const url = readUploadedFileUrl(finalizeJson) ?? presignJson.view_url ?? presignJson.s3_url ?? null;
3472
+ if (!url)
3473
+ throw new Error("local media upload returned no durable URL.");
3474
+ return url;
3475
+ }
3476
+ // Step 2b (fallback): local-storage box — POST the bytes as multipart form.
3418
3477
  const form = new FormData();
3419
- form.append("file", new Blob([readFileSync(absPath)]), path.basename(absPath));
3478
+ form.append("file", new Blob([buffer], { type: contentType }), fileName);
3420
3479
  if (folder)
3421
3480
  form.append("folder_path", folder);
3422
3481
  const res = await fetch(new URL("/api/v1/user/me/temporary-files/upload", ctx.host), { method: "POST", headers: buildAuthHeaders(ctx.auth), body: form });
@@ -4599,7 +4658,7 @@ async function runMediaCommand(argv) {
4599
4658
  if (!items.length) {
4600
4659
  console.log(`${DIM}No results. Try a broader query, another --type, or a specific --provider (pixabay|openverse|iconify).${RESET}`);
4601
4660
  if (type === "image" || type === "vector" || type === "video") {
4602
- console.log(`${DIM}(photos/vectors/video need the platform PIXABAY_API_KEY set server-side; openverse covers CC images keyless.)${RESET}`);
4661
+ console.log(`${DIM}(photos/vectors/video need YOUR OWN free Pixabay key — get one at https://pixabay.com/api/docs/ then save it: 'vidfarm add-provider-key pixabay <key>'. openverse covers CC images keyless.)${RESET}`);
4603
4662
  }
4604
4663
  return;
4605
4664
  }
@@ -5868,7 +5927,7 @@ async function runAddProviderKeyCommand(argv) {
5868
5927
  const provider = parsed.positionals[0];
5869
5928
  const secret = parsed.positionals[1];
5870
5929
  if (!provider || !secret)
5871
- throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity|nvidia|elevenlabs).");
5930
+ throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity|nvidia|elevenlabs|pixabay).");
5872
5931
  const ctx = commonContext(parsed.values);
5873
5932
  const result = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/user/me/provider-keys", auth: ctx.auth, body: { provider, secret, label: parsed.values.label } });
5874
5933
  assertApiOk(result, "add-provider-key");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/vidfarm-devcli",
3
- "version": "0.21.10",
3
+ "version": "0.21.11",
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": {