@mevdragon/vidfarm-devcli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.director.md CHANGED
@@ -165,9 +165,9 @@ Response:
165
165
  { "ok": true, "mode": "smart", "scene_count": 6, "caption_count": 4, "duration_seconds": 12.4, "ghostcut_pending": true, "ghostcut_task_id": "..." }
166
166
  ```
167
167
 
168
- If `ghostcut_pending: true`, poll `POST /api/v1/compositions/:forkId/ghostcut-poll` until status transitions to `done` or `failed`.
168
+ If `ghostcut_pending: true`, poll `POST /api/v1/compositions/:forkId/remove-video-captions-poll` (legacy alias: `/ghostcut-poll`) until status transitions to `done` or `failed`.
169
169
 
170
- Read-only state (does **not** advance the job or bill): `GET /api/v1/compositions/:forkId/ghostcut`. Returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }`. This is how you read the composition's **two video sources**: `original_source_url` is the **original video** (raw upload, captions intact) and `mirrored_url` is the **decomposed video** (the processed, caption-free copy the timeline renders from). Use this when you want both URLs without triggering another poll — the AI copilot uses it to pick between the original video and the decomposed video before feeding a video into a primitive route (image extract, dedupe, trim, ai video edit, etc.). Prefer the **decomposed video** (`mirrored_url`) when `status === "done"` and the downstream call should be caption-free; prefer the **original video** (`original_source_url`) when the user explicitly wants the raw upload or when `status !== "done"`.
170
+ Read-only state (does **not** advance the job or bill): `GET /api/v1/compositions/:forkId/remove-video-captions` (legacy alias: `/ghostcut`). Returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }`. This is how you read the composition's **two video sources**: `original_source_url` is the **original video** (raw upload, captions intact) and `mirrored_url` is the **decomposed video** (the processed, caption-free copy the timeline renders from). Use this when you want both URLs without triggering another poll — the AI copilot uses it to pick between the original video and the decomposed video before feeding a video into a primitive route (image extract, dedupe, trim, ai video edit, etc.). Prefer the **decomposed video** (`mirrored_url`) when `status === "done"` and the downstream call should be caption-free; prefer the **original video** (`original_source_url`) when the user explicitly wants the raw upload or when `status !== "done"`.
171
171
 
172
172
  ## Video context (transcript + scene descriptions)
173
173
 
@@ -245,7 +245,7 @@ Content-Type: application/json
245
245
  { "title": "optional", "version": null, "tracer": "optional-trace-id", "html": "optional HTML to save to working state before rendering" }
246
246
  ```
247
247
 
248
- Returns 202 with `{ ok, renderId, status, progress, outputUrl, cost, title, version, ... }` where `version` is the snapshot version created for this publish. Failure modes: `402` if the fork owner is not on a paid plan, `409` while GhostCut subtitle removal is still `pending` (retry after `/ghostcut-poll` reports done/failed), `412` if the provider-key preflight fails. Poll:
248
+ Returns 202 with `{ ok, renderId, status, progress, outputUrl, cost, title, version, ... }` where `version` is the snapshot version created for this publish. Failure modes: `402` if the fork owner is not on a paid plan, `409` while GhostCut subtitle removal is still `pending` (retry after `/remove-video-captions-poll` reports done/failed), `412` if the provider-key preflight fails. Poll:
249
249
 
250
250
  ```
251
251
  GET /api/v1/compositions/:forkId/renders/:renderId
@@ -419,7 +419,7 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
419
419
  | `vidfarm generate <image\|video> --prompt "…"` | `POST /api/v1/primitives/{images,videos}/generate` (polls job) | generate AI media → finished URL |
420
420
  | `vidfarm place <dir> --src <url> [--at\|--replace]` | (edits local composition.html) | drop media into a gap / over a scene |
421
421
  | `vidfarm decompose <forkId>` | `POST .../compositions/:forkId/auto-decompose` | split source into scenes |
422
- | `vidfarm ghostcut <forkId>` | `GET .../compositions/:forkId/ghostcut` | subtitle-removal status |
422
+ | `vidfarm remove-video-captions <forkId>` (alias: `ghostcut`) | `GET .../compositions/:forkId/remove-video-captions` | subtitle-removal status |
423
423
  | `vidfarm snapshot <forkId>` | `POST .../compositions/:forkId/versions` | save composition fork version |
424
424
  | `vidfarm versions <forkId>` | `GET .../compositions/:forkId/versions` | list versions |
425
425
  | `vidfarm render <forkId> [--wait]` | `POST .../compositions/:forkId/render` | render video to MP4 |
@@ -468,7 +468,9 @@ npx -y @mevdragon/vidfarm-devcli serve <template_id>
468
468
  # → auto-provisions a local user and opens the editor, pre-authed
469
469
  ```
470
470
 
471
- Everything runs locally (`RECORDS_DRIVER=local`, `STORAGE_DRIVER=local`) no AWS touched **except render**, which submits to the deployed cloud renderer when `VIDFARM_JOB_STATE_MACHINE_ARN` + `HYPERFRAMES_RENDER_*` are set (copy them from `.env.staging`); otherwise the editor's render button returns a clear `render_unavailable` message. Media referenced by the composition stays on its cloud URLs.
471
+ 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 referenced by the composition stays on its cloud URLs. Pass `--no-cloud` for a fully-offline box.
472
+
473
+ **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 HyperFrames 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.
472
474
 
473
475
  How the loop works:
474
476
  - 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.
@@ -477,7 +479,7 @@ How the loop works:
477
479
 
478
480
  Seeding from the cloud: `serve <template_id>` pulls the template's **default fork** (which may be another user's public decomposition); `serve --fork <id>` pulls a specific fork. Only the composition + records are localized — media stays on cloud URLs. Re-serving keeps local edits unless you pass `--refetch`.
479
481
 
480
- Flags: `--port` (default 3000), `--dir` (default `./.vidfarm-local`), `--key` (bootstrap/browser key, or `VIDFARM_API_KEY`), `--fork <id>`, `--host` (cloud host to pull from, default `https://vidfarm.cc`), `--api-key` (cloud key for the pull), `--refetch`, `--no-open`. `vidfarm <template_id>` is an alias for `serve <template_id>`.
482
+ Flags: `--port` (default 3000), `--dir` (default `./.vidfarm-local`), `--key` (bootstrap/browser key, or `VIDFARM_API_KEY`), `--fork <id>`, `--host` (cloud host to mirror + pull from, default `https://vidfarm.cc`), `--api-key` (cloud key for pulls, `/library`, and cloud render — defaults to `VIDFARM_API_KEY`), `--refetch`, `--no-cloud` (fully offline: no cloud catalog, seeding, or cloud render), `--no-open`. `vidfarm <template_id>` is an alias for `serve <template_id>`.
481
483
 
482
484
  To *analyze* the source media locally (videos, transcript, recurring cast), read the `video-context.json` / `cast.json` routes, or pull a media URL with `vidfarm download <url>`.
483
485
 
@@ -543,6 +545,25 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/images/remove-background" \
543
545
  -d '{"source_image_url": "https://cdn.example.com/photo.jpg"}'
544
546
  ```
545
547
 
548
+ ## Primitive: video_remove_captions
549
+
550
+ Remove burned-in captions/subtitles/on-screen text from any video URL (GhostCut-powered — the same pipeline auto-decompose uses for its caption-free mirror, exposed as a standalone reusable primitive). Result is a durable caption-free MP4 stored at a Vidfarm URL.
551
+
552
+ - `POST /api/v1/primitives/videos/remove-captions` (alias: `POST /api/v1/primitives/remove-video-captions`)
553
+ - Body: `{ "tracer": "...", "payload": { "source_video_url": "https://..." }, "webhook_url"?: "..." }`
554
+ - Response: standard primitive job (`202 { job_id }`). The job probes the source, submits to GhostCut, polls to completion server-side (typically a few minutes), and mirrors the result. Poll `GET /api/v1/primitives/jobs/:jobId` until `status: "succeeded"`, then read `primary_file_url` / `video.file_url` (`captionsRemovedVideoUrl` in the output).
555
+ - Billing: `~$0.10 per 30 seconds` of source video (per-chunk, 1-chunk minimum; `ghostcut_subtitle_removal` cost center), plus the small probe cost.
556
+ - For the CURRENT fork's decompose flow, don't re-run this primitive — the caption-free mirror already exists via `GET /api/v1/compositions/:forkId/remove-video-captions`.
557
+
558
+ Example:
559
+
560
+ ```bash
561
+ curl -X POST "$VIDFARM_BASE/api/v1/primitives/videos/remove-captions" \
562
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
563
+ -H "content-type: application/json" \
564
+ -d '{"tracer": "demo-remove-captions", "payload": {"source_video_url": "https://cdn.example.com/clip.mp4"}}'
565
+ ```
566
+
546
567
  ## Primitive: media_dedupe
547
568
 
548
569
  Apply subtle camouflage transforms (zoom, tilt, rotate, saturation, playback speed, contrast, brightness, hue rotate, blur, horizontal flip, tint overlay) to any source **image or video** URL. Useful when reposting existing media and you want small perceptual differences to reduce duplicate-detection.
@@ -635,7 +656,7 @@ Analyze a source video and identify where a product can be organically placed so
635
656
 
636
657
  - `POST /api/v1/primitives/brainstorm/product_placement`
637
658
  - Body: `{ "tracer": "...", "payload": { "source_video_url": "...", "offer_description": "...", "count"?: 8, "provider"?: "gemini" }, "webhook_url"?: "..." }`
638
- - `source_video_url` (required, URL) — the exact durable/public video to analyze. Aliases accepted: `video_url`, `source_url`, `url`. For a fork's composition, call `GET /api/v1/compositions/:forkId/ghostcut` first and pass the correct URL — the **original video** (`original_source_url`) or the **decomposed video** (`mirrored_url`).
659
+ - `source_video_url` (required, URL) — the exact durable/public video to analyze. Aliases accepted: `video_url`, `source_url`, `url`. For a fork's composition, call `GET /api/v1/compositions/:forkId/remove-video-captions` first and pass the correct URL — the **original video** (`original_source_url`) or the **decomposed video** (`mirrored_url`).
639
660
  - `offer_description` (required) — the product/offer to place. Aliases: `offer`, `description`, `details`.
640
661
  - `count` (optional, 3–30, default 8) — number of opportunities; only send when the user asks for a specific number.
641
662
  - Response: standard primitive job. Poll `GET /api/v1/primitives/jobs/:jobId` to completion, then read `result.json` (also surfaced on the job output as `opportunities[]`), where each entry has `timestamp`, `scene`, `placement_type`, `placement_idea`, and `why_it_works`.
package/SKILL.platform.md CHANGED
@@ -221,9 +221,11 @@ GhostCut is an external AI subtitle-removal service. Vidfarm proxies to it via `
221
221
  - `pollStatus(taskId)` — check status (`pending` | `processing` | `queued` | `done` | `failed`)
222
222
  - `mirrorGhostcutVideoToStorage()` — copy the clean output back to Vidfarm S3
223
223
 
224
- State is persisted per fork at `compositions/forks/<forkId>/working/ghostcut.json`. The client polls `POST /api/v1/compositions/:forkId/ghostcut-poll` every few seconds during smart decompose.
224
+ State is persisted per fork at `compositions/forks/<forkId>/working/ghostcut.json`. The client polls `POST /api/v1/compositions/:forkId/remove-video-captions-poll` (legacy alias: `/ghostcut-poll`) every few seconds during smart decompose.
225
225
 
226
- Read-only companion: `GET /api/v1/compositions/:forkId/ghostcut` returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }` without touching GhostCut. Non-billing. Exposed for two reasons: (1) the editor chat harness / AI copilot uses it to look up both video URLs on demand so downstream primitive calls can pick raw-upload vs caption-free-mirror without hardcoding either in chat context; (2) it's `http_request`-allowlisted under `COMPOSITIONS_PREFIX` in `src/app.ts`, which is why the editor chat system prompt (`src/editor-chat.ts`) directs the model to call it before feeding a video into a primitive route. The AI is instructed **not** to call `POST /ghostcut-poll` unless the user explicitly asks to advance subtitle removal.
226
+ Read-only companion: `GET /api/v1/compositions/:forkId/remove-video-captions` (legacy alias: `/ghostcut`) returns `{ status, task_id, original_source_url, mirrored_url, submitted_at_ms, completed_at_ms, failure_reason }` without touching GhostCut. Non-billing. Exposed for two reasons: (1) the editor chat harness / AI copilot uses it to look up both video URLs on demand so downstream primitive calls can pick raw-upload vs caption-free-mirror without hardcoding either in chat context; (2) it's `http_request`-allowlisted under `COMPOSITIONS_PREFIX` in `src/app.ts`, which is why the editor chat system prompt (`src/editor-chat.ts`) directs the model to call it before feeding a video into a primitive route. The AI is instructed **not** to call `POST /remove-video-captions-poll` unless the user explicitly asks to advance subtitle removal.
227
+
228
+ Standalone primitive: `POST /api/v1/primitives/videos/remove-captions` (alias: `POST /api/v1/primitives/remove-video-captions`; id `primitive:video_remove_captions` in `src/primitive-registry.ts`) exposes the same GhostCut pipeline for ANY video URL, decoupled from forks and decompose. The job probes the source (duration prices the task), submits, polls GhostCut to completion inside the job runner (12-minute budget, vs the fork routes where the client drives completion), mirrors `captions-removed.mp4` into primitive storage, and bills the `ghostcut_subtitle_removal` cost center per 30s chunk.
227
229
 
228
230
  Configuration: `GHOSTCUT_KEY` and `GHOSTCUT_SECRET` env vars per stage. Pass-through cost: `GHOSTCUT_USD_PER_30S` (default 0.10).
229
231
 
@@ -378,13 +380,15 @@ vidfarm-devcli serve <template_id>
378
380
  vidfarm-devcli serve --fork <forkId> --host https://vidfarm.cc --api-key <key>
379
381
  ```
380
382
 
381
- Flags: `--port` (3000), `--dir` (`./.vidfarm-local`), `--key` (local bootstrap/browser key, or `VIDFARM_API_KEY`), `--fork`, `--host`, `--api-key` (cloud pull key), `--refetch`, `--no-open`. `vidfarm <template_id>` aliases `serve <template_id>`.
383
+ Flags: `--port` (3000), `--dir` (`./.vidfarm-local`), `--key` (local bootstrap/browser key, or `VIDFARM_API_KEY`), `--fork`, `--host`, `--api-key` (cloud key for pulls, /library, and cloud render), `--refetch`, `--no-cloud`, `--no-open`. `vidfarm <template_id>` aliases `serve <template_id>`.
384
+
385
+ **Cloud passthrough** (`src/services/upstream.ts`; on unless `--no-cloud`): only the editor's data is local — the box mirrors the upstream host (`VIDFARM_UPSTREAM_HOST` + `VIDFARM_UPSTREAM_API_KEY`, set by serve from `--host`/`--api-key`). `/discover/feed` and `GET /api/v1/videos` merge the upstream catalog behind local entries (dedup by id); `/library` and `GET /api/v1/approved/posts` merge the cloud account's approved posts, and `:postId` actions on posts that don't resolve locally proxy through (`maybeProxyUnknownReadyPostToUpstream`); `POST /discover/templates` (Add Template) proxies so ingest lands in the cloud account; `/editor/:templateId` seeds an unknown template/fork from upstream on first open (`seedFromUpstream` — same path as boot seeding, cloud fork id reused locally). The editor boot gains `localRender` + `cloudRenderAvailable`, which turn the Render button into a popover: **Render Local (Free)** vs **Render in Cloud**. `render_target: "cloud"` on `POST /render` resolves (or clones, once) a publishable upstream fork (mapping persisted in the fork's working `upstream-link.json`), starts the render upstream with the submitted html, writes a `cloud-render-<renderId>.json` marker, and `GET /renders/:renderId` proxies status through that marker. serve force-clears `VIDFARM_JOB_STATE_MACHINE_ARN` so local renders never silently route to a cloud state machine picked up from a dev-shell `.env`.
382
386
 
383
- **Architecture** (fully local; render is the only cloud dependency):
387
+ **Architecture** (fully local editor; cloud passthrough is read/proxy-only):
384
388
  - **Records/auth** — `src/services/local-dynamo.ts` is a drop-in `.send()` shim for the DynamoDB DocumentClient used by `serverless-records.ts` + `serverless-auth.ts`, servicing Get/Put/Delete/Query/Scan/Update against JSON files under `<data-dir>/dynamo/<table>/`. Selected by `config.RECORDS_DRIVER` (`z.enum(["local","dynamo"]).default("dynamo")` — deployed envs are unaffected). The 25 call sites are unchanged; only the client construction switches.
385
389
  - **Storage** — `StorageService` already writes to disk when `STORAGE_DRIVER=local` / `AWS_S3_BUCKET=""` (files under `<data-dir>/storage/`). Composition public URLs become `${PUBLIC_BASE_URL}/storage/<key>`.
386
390
  - **Auth** — no seeding: `VIDFARM_API_KEY` + the existing `tryBootstrapFromEnv` auto-provisions a paid customer; `serve` opens `/auto-login?api_key=…&redirect=/editor/…` to plant the session cookie so the browser lands pre-authed.
387
391
  - **Live push** — `src/services/composition-watch.ts` (`fs.watch` on the forks dir, only when `STORAGE_DRIVER=local`) coalesces bursts (80ms) and broadcasts `{ forkId, file }` to `GET /api/v1/dev/events` (Hono `streamSSE`, cookie-authed). The editor opens an `EventSource` there gated on the boot flag `liveReload`, and on a `reload` for its fork refetches `composition.html` and runs `applyExternalCompositionHtml` → `morphLiveCompositionDom` (keyed on `data-hf-id`; media buffers + playback survive). The composition write routes call `compositionWatch.suppressFork(forkId)` so the editor's own `PUT` doesn't echo.
388
392
  - **Cloud-seed** — `serve` pulls the composition for `--fork` (or the template's default fork via `resolveForkId`, which follows the cloud `/editor` redirect and grabs even another user's public decomposition), then materializes a local template + fork record and writes the composition to the working key so `/editor/:templateId?fork=:id` resolves offline. Media stays on cloud URLs.
389
- - **Render** — cloud only (no local job runner; hyperframes render is a Lambda fan-out). `publishCompositionToRenderer` returns `render_unavailable` (501) when `VIDFARM_JOB_STATE_MACHINE_ARN` is unset; wire the `HYPERFRAMES_RENDER_*` + state-machine env from `.env.staging` to submit to the deployed renderer.
393
+ - **Render** — in-process on this box (free, unguarded `runPrimitiveJobInProcess` + headless-Chromium HyperFrames). Cloud rendering is the explicit `render_target: "cloud"` upstream handoff described above; the ambient `VIDFARM_JOB_STATE_MACHINE_ARN` is force-cleared by serve.
390
394
  - Lambdas — `infra/lambda/*.ts`