{"id":"3fef0689-378a-4e48-af31-aff806379d24","entityType":"agent","slug":"clawhub-psyb0t-flickies","name":"flickies","canonicalUrl":"https://www.xpersona.co/agent/clawhub-psyb0t-flickies","canonicalPath":"/agent/clawhub-psyb0t-flickies","generatedAt":"2026-10-11T11:24:00.975Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T09:22:31.530Z","emptyReason":null},"description":"Self-hosted video REST + MCP API. POST JSON, get a video back. Lipsync (LatentSync 1.5 + Wav2Lip/Wav2Lip-GAN) at /v1/video/lipsync, GFPGAN face restore at /v1/video/restore, pure-ffmpeg ops (trim, concat, transcode incl. gif + fps + codec, scale, mux_audio, extract_audio, thumbnail_grid) under /v1/video/*, and ffprobe metadata at /v1/video/info. file_path (staged) xor file_url in; output_path xor output_url out. Fire-and-forget async jobs (async_job=true → 202 → poll /v1/jobs/{id}) with HMAC-signed webhooks. 11 MCP tools at /v1/mcp. Bearer-token auth. CPU + CUDA images. Use when the user wants to lipsync a face to audio, restore faces in footage, trim/concat/transcode/scale/mux/extract/thumbnail video, probe a video's metadata, or drive any of that from an LLM over MCP.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:flickies","sourceUrl":"https://clawhub.ai/psyb0t/flickies","homepage":"https://clawhub.ai/psyb0t/skills/flickies","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/psyb0t/flickies","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/psyb0t/skills/flickies","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":61,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"flickies technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T09:22:31.530Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T09:22:31.530Z","emptyReason":null},"stars":null,"forks":null,"downloads":1101,"packageName":null,"latestVersion":"0.3.17","tractionLabel":"1.1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T09:22:31.455Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T09:22:31.530Z","lastCrawledAt":"2026-10-11T09:22:31.455Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T09:22:31.455Z","lastVerifiedAt":null,"highlights":[{"version":"0.3.17","createdAt":"2026-10-10T15:07:51.846Z","changelog":"- Removed the documentation file skill-card.md. - No changes to the skill code or functionality. - Documentation and user guidance now rely on other files (e.g., SKILL.md, references/setup.md).","fileCount":5,"zipByteSize":16459},{"version":"0.3.16","createdAt":"2026-08-01T20:45:53.539Z","changelog":"- Removed the sample skill card file (skill-card.md) for cleanup. - No changes to functionality or API; this update removes extra documentation only.","fileCount":5,"zipByteSize":16668},{"version":"0.3.15","createdAt":"2026-07-27T23:55:10.456Z","changelog":"flickies 0.3.15 - Removed the file skill-card.md from the project. - No user-facing features or functionality were changed in this version.","fileCount":5,"zipByteSize":16825},{"version":"0.3.14","createdAt":"2026-07-27T23:33:40.264Z","changelog":"- Removed the file skill-card.md. - No other user-visible changes.","fileCount":5,"zipByteSize":16662},{"version":"0.3.13","createdAt":"2026-07-27T15:42:49.547Z","changelog":"- Removed the redundant skill-card.md file. - No changes to API, features, or documentation were made in this version.","fileCount":5,"zipByteSize":16734},{"version":"0.3.12","createdAt":"2026-07-27T13:43:33.221Z","changelog":"- Removed the file skill-card.md. - No changes to features or functionality.","fileCount":5,"zipByteSize":16788},{"version":"0.3.11","createdAt":"2026-07-26T12:16:49.659Z","changelog":"- Removed the file: skill-card.md - No user-facing functionality changes.","fileCount":5,"zipByteSize":16672},{"version":"0.3.10","createdAt":"2026-07-26T03:11:06.574Z","changelog":"de-duplicated security docs (SAFE)","fileCount":5,"zipByteSize":16757}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:flickies","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17fq93tmpky791n7516jcn08n83sfn2:flickies` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/psyb0t/flickies before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T11:24:00.969Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-psyb0t-flickies/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T09:22:31.530Z","emptyReason":null},"readme":"Skill: flickies\n\nOwner: psyb0t\n\nSummary: Self-hosted video REST + MCP API. POST JSON, get a video back. Lipsync (LatentSync 1.5 + Wav2Lip/Wav2Lip-GAN) at /v1/video/lipsync, GFPGAN face restore at /v1/video/restore, pure-ffmpeg ops (trim, concat, transcode incl. gif + fps + codec, scale, mux_audio, extract_audio, thumbnail_grid) under /v1/video/*, and ffprobe metadata at /v1/video/info. file_path (staged) xor file_url in; output_path xor output_url out. Fire-and-forget async jobs (async_job=true → 202 → poll /v1/jobs/{id}) with HMAC-signed webhooks. 11 MCP tools at /v1/mcp. Bearer-token auth. CPU + CUDA images. Use when the user wants to lipsync a face to audio, restore faces in footage, trim/concat/transcode/scale/mux/extract/thumbnail video, probe a video's metadata, or drive any of that from an LLM over MCP.\n\nTags: latest:0.3.17\n\nVersion history:\n\nv0.3.17 | 2026-10-10T15:07:51.846Z | auto\n\n- Removed the documentation file skill-card.md.\n- No changes to the skill code or functionality.\n- Documentation and user guidance now rely on other files (e.g., SKILL.md, references/setup.md).\n\nv0.3.16 | 2026-08-01T20:45:53.539Z | auto\n\n- Removed the sample skill card file (skill-card.md) for cleanup.\n- No changes to functionality or API; this update removes extra documentation only.\n\nv0.3.15 | 2026-07-27T23:55:10.456Z | auto\n\nflickies 0.3.15\n\n- Removed the file skill-card.md from the project.\n- No user-facing features or functionality were changed in this version.\n\nv0.3.14 | 2026-07-27T23:33:40.264Z | auto\n\n- Removed the file skill-card.md.  \n- No other user-visible changes.\n\nv0.3.13 | 2026-07-27T15:42:49.547Z | auto\n\n- Removed the redundant skill-card.md file.\n- No changes to API, features, or documentation were made in this version.\n\nv0.3.12 | 2026-07-27T13:43:33.221Z | auto\n\n- Removed the file skill-card.md.\n- No changes to features or functionality.\n\nv0.3.11 | 2026-07-26T12:16:49.659Z | auto\n\n- Removed the file: skill-card.md\n- No user-facing functionality changes.\n\nv0.3.10 | 2026-07-26T03:11:06.574Z | user\n\nde-duplicated security docs (SAFE)\n\nv0.3.9 | 2026-07-26T02:39:35.589Z | auto\n\n- Permissions are now clearly described and condensed; wording improved for clarity.\n- Security and safety section is rewritten for brevity, still warning about auth and destructive ops.\n- Admin/destructive endpoint guidelines are condensed and less verbose.\n- References to engine/files destructive actions are shorter but retain key cautions.\n- The removed skill-card.md file is not replaced; documentation is streamlined.\n- No functional or API changes; this update is documentation and permission metadata cleanup.\n\nv0.3.8 | 2026-07-26T01:39:58.672Z | auto\n\n- Security warnings for destructive endpoints and authentication have been expanded for clarity and emphasis.\n- Explicit caution added about UNAUTHENTICATED API/MCP surface when `FLICKIES_AUTH_TOKEN` is unset—strongly warning never to expose such an instance.\n- Clarified agent guardrails for DELETE actions: these should only be performed on explicit user command and never on agent initiative, with practical scoping advice.\n- Removed the redundant skill-card.md file.\n- Documentation edits to reinforce safe usage, especially around engine eviction and file deletion.\n\nv0.3.7 | 2026-07-25T23:39:21.994Z | auto\n\nflickies 0.3.7\n\n- Added a permissions section specifying allowed network, shell, and filesystem actions.\n- Expanded and clarified security guidance, including explicit agent guardrails for destructive endpoints.\n- Removed the legacy skill-card.md file.\n\nv0.3.6 | 2026-07-25T22:35:31.579Z | auto\n\n**Security warning and destructive endpoint clarifications added.**\n\n- Added a dedicated \"Security & safety\" section detailing that by default the API has no authentication and is wide open.\n- Warned explicitly about the risks of the destructive endpoints: DELETE /v1/engines/{slug} and DELETE /v1/files/{path}.\n- Stressed that the two destructive endpoints should not be exposed to untrusted users, even with auth enabled.\n- Removed the sample file skill-card.md. \n- No API or functionality changes — documentation update for safer usage and clearer operator guidance.\n\nv0.3.5 | 2026-07-25T20:27:11.779Z | auto\n\n- Removed the file skill-card.md from the project.\n- No user-facing features or API changes in this release.\n- Maintenance update only; functionality and usage remain unchanged.\n\nv0.3.4 | 2026-07-25T19:55:46.592Z | auto\n\n## flickies 0.3.4\n\n- Removed the `skill-card.md` file from the repository.\n- No other changes to skill functionality or documentation.\n\nv0.3.3 | 2026-07-25T19:13:07.991Z | auto\n\n- Expanded documentation in SKILL.md with detailed usage instructions, examples, and endpoint descriptions.\n- Clarified input/output contract for all video endpoints: exactly one of file_path or file_url in, output_path or output_url out.\n- Documented available video operations: lipsync, face restore, ffmpeg-based trim/concat/transcode/scale/mux/extract/thumbnail, and metadata probe.\n- Added guidance on async jobs, HMAC-signed webhooks, and MCP tools endpoint.\n- Outlined usage scenarios, setup steps, limitations (e.g., real-time not supported, engine hot-swap, CUDA requirements).\n- Included copy-paste quickstart examples for common workflows.\n\nArchive index:\n\nArchive v0.3.17: 5 files, 16459 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (2109b), SKILL.md (23846b), _meta.json (128b)\n\nFile v0.3.17:SKILL.md\n\n---\nname: flickies\ndescription: Self-hosted video REST + MCP API. POST JSON, get a video back. Lipsync (LatentSync 1.5 + Wav2Lip/Wav2Lip-GAN) at /v1/video/lipsync, GFPGAN face restore at /v1/video/restore, pure-ffmpeg ops (trim, concat, transcode incl. gif + fps + codec, scale, mux_audio, extract_audio, thumbnail_grid) under /v1/video/*, and ffprobe metadata at /v1/video/info. file_path (staged) xor file_url in; output_path xor output_url out. Fire-and-forget async jobs (async_job=true → 202 → poll /v1/jobs/{id}) with HMAC-signed webhooks. 11 MCP tools at /v1/mcp. Bearer-token auth. CPU + CUDA images. Use when the user wants to lipsync a face to audio, restore faces in footage, trim/concat/transcode/scale/mux/extract/thumbnail video, probe a video's metadata, or drive any of that from an LLM over MCP.\nhomepage: https://github.com/psyb0t/docker-flickies\nuser-invocable: true\npermissions:\n  - network: outbound HTTP to the configured FLICKIES_URL, plus server-side fetch of file_url, delivery to output_url, and HMAC-signed webhook callbacks\n  - shell: the documented examples invoke local curl / docker\n  - filesystem: manages server-side staged files (upload, fetch, remove) and engine lifecycle (load, evict) on the configured instance\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🎬\", \"primaryEnv\": \"FLICKIES_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# flickies\n\nSelf-hosted video toolkit — lipsync, face restore, and ffmpeg ops in one container. POST a JSON body, get a video back. Every video endpoint takes the same input/output contract; drive it from curl, the generated Go/Python clients, or point a function-calling LLM at the MCP endpoint.\n\nLipsync (`POST /v1/video/lipsync`): drive a face video/image from an audio track. Engines: `latentsync-1.5` (ByteDance, Apache-2.0, commercial-safe default, CUDA-only) and `wav2lip` / `wav2lip-gan` (Rudrabha, fast/low-VRAM, LRS2 non-commercial — refused unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set in the **server** env). `restore_face=true` chains GFPGAN over the result.\n\nFace restore (`POST /v1/video/restore`): GFPGAN v1.4 (`gfpgan`, Apache-2.0) — clean up a Wav2Lip mouth crop or old footage, standalone.\n\nffmpeg ops (pure CPU, no engine): `POST /v1/video/trim`, `/concat`, `/transcode` (mp4/webm/mov/mkv + gif + fps + codec change), `/scale`, `/mux_audio`, `/extract_audio`, `/thumbnail_grid`. Metadata: `POST /v1/video/info` (ffprobe).\n\nExtras: async jobs (`async_job=true` → 202 + `job_id` → poll `GET /v1/jobs/{job_id}`), HMAC-signed webhooks on async completion, server-side file staging, engine load/evict control, an MCP endpoint at `/v1/mcp` with 11 tools, optional bearer-token auth.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Auth is off by default** — `FLICKIES_AUTH_TOKEN` is unset out of the box, so the whole API is open to anyone who can reach the port. Set it for any deployment beyond localhost, pass `Authorization: Bearer <token>` once it's set, and bind to loopback / behind an authenticating proxy. Never expose an unauthenticated instance on a network.\n- **File and engine management** — the staging and engine endpoints include remove and evict operations. Like any state change, only run them against a resource the current task created, only when the user asked, and not against a shared instance others depend on.\n\n## When To Use\n\n- Lipsync a face (video or still image) to a driving audio track — `latentsync-1.5` (commercial-safe, CUDA) or `wav2lip` / `wav2lip-gan` (non-commercial gate).\n- Restore / sharpen faces in a video with GFPGAN, standalone or chained after a Wav2Lip pass (`restore_face=true`).\n- Trim a clip, concatenate several clips, or transcode to another container/codec (incl. animated GIF).\n- Change frame rate, re-encode with a specific codec/CRF/preset, scale dimensions, mux an audio track in, extract the audio out, or generate a thumbnail sprite-sheet.\n- Probe a video for duration, codec, fps, dimensions, bitrate (`/v1/video/info`).\n- Run any long operation fire-and-forget: submit with `async_job=true`, poll `/v1/jobs/{id}`, or receive an HMAC-signed webhook on completion.\n- Drive the whole pipeline from a function-calling LLM (Claude, LibreChat, Cursor) via MCP at `/v1/mcp`.\n\n## When NOT To Use\n\n- Real-time / streaming video output — every endpoint is request/response; long jobs go async + poll, not stream.\n- `latentsync-1.5` on the CPU image — it's CUDA-only (`cuda_only: true`) and the CPU image refuses to load it. GFPGAN is also CUDA-only in practice. Use the `:latest-cuda` image for those.\n- `wav2lip` / `wav2lip-gan` anywhere unless the **server** was started with `FLICKIES_ENABLE_NONCOMMERCIAL=1` — otherwise the request returns 403 `NONCOMMERCIAL_GATE_REFUSED`. This is a server-side env flag; you cannot flip it per-request.\n- Two engines resident at once — one model is resident at a time. A different engine request triggers hot-swap eviction of the current one. If you need two simultaneously, run two containers.\n- `output_url` via the MCP tools — MCP tools only accept `output_path` (they write under FILES_DIR). Use the REST endpoint if you need presigned-PUT `output_url` delivery.\n- Multipart upload on the video endpoints — only `PUT /v1/files/{path}` accepts a raw-body upload. Video endpoints take JSON with `file_path` or `file_url`.\n\n## Setup\n\nThe container should already be running. Set the base URL:\n\n```bash\nexport FLICKIES_URL=http://localhost:8000\n```\n\nIf the server has `FLICKIES_AUTH_TOKEN` set, export it too:\n\n```bash\nexport FLICKIES_AUTH_TOKEN=<your-token>\n# every request below then needs: -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"\n```\n\n**Verify:** `curl $FLICKIES_URL/healthz` returns `{\"status\": \"ok\"}` (unversioned, always auth-exempt). For the richer discovery payload — device, ffmpeg version, available/enabled/loaded engines, non-commercial flag — hit `GET /v1/health`:\n\n```bash\ncurl -s $FLICKIES_URL/v1/health | jq\n# { \"status\": \"ok\", \"version\": \"...\", \"device\": \"cuda\", \"ffmpeg\": \"...\",\n#   \"available_engines\": [...], \"enabled_engines\": [...],\n#   \"loaded_engine\": null, \"noncommercial_enabled\": false }\n```\n\nFor install / configuration / env vars / CPU vs CUDA images / engine weights, see [references/setup.md](references/setup.md).\n\n## Quick Start\n\nThe input/output contract is uniform across every video endpoint:\n\n- **Input** — exactly one of `file_path` (FILES_DIR-relative, staged via `/v1/files`) or `file_url` (any HTTP/HTTPS URL the server fetches).\n- **Output** — exactly one of `output_path` (server writes to FILES_DIR/<path>, response `{path, size, ...}`; download via `GET /v1/files/<path>`) or `output_url` (server PUTs to a presigned/PUT-accepting URL, response `{url, size, ...}`). In async mode both are optional — the server auto-stages to `jobs/{id}.{ext}`.\n\n```bash\n# Probe a video (stage it first, then reference by path).\ncurl -s -X PUT --data-binary @clip.mp4 \\\n  -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" \\\n  \"$FLICKIES_URL/v1/files/uploads/clip.mp4\"\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq\n\n# Trim seconds 5–12, write the result under FILES_DIR, download it.\ncurl -s -X POST \"$FLICKIES_URL/v1/video/trim\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"file_path\": \"uploads/clip.mp4\",\n        \"start_sec\": 5, \"end_sec\": 12, \"precise\": true,\n        \"output_path\": \"out/clip-trimmed.mp4\"\n      }' | jq\ncurl -s \"$FLICKIES_URL/v1/files/out/clip-trimmed.mp4\" --output clip-trimmed.mp4\n\n# Lipsync a face to an audio track (LatentSync, commercial-safe default, CUDA).\ncurl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"output_path\": \"out/lipsynced.mp4\"\n      }' | jq\n```\n\nAdd `-H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"` to every call if the server has a token set. `/healthz` is the only always-exempt route.\n\n## API — `POST /v1/video/lipsync`\n\nDrive a face from an audio track. JSON body.\n\n### Request Fields\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `face_path` / `face_url` | exactly one | — | Driving face — a video OR a still image. `face_path` is FILES_DIR-relative; `face_url` is an HTTP(S) URL the server fetches. |\n| `audio_path` / `audio_url` | exactly one | — | Driving audio (wav/mp3/m4a). `audio_path` FILES_DIR-relative; `audio_url` an HTTP(S) URL. |\n| `engine` | no | `latentsync-1.5` | `latentsync-1.5` (Apache-2.0, CUDA-only, commercial-safe) / `wav2lip` / `wav2lip-gan`. The two `wav2lip*` slugs require `FLICKIES_ENABLE_NONCOMMERCIAL=1` on the server → else 403. |\n| `restore_face` | no | `false` | Chain GFPGAN over the output to clean up the face region. Recommended after `wav2lip*` (fixes the soft 96×96 mouth crop). |\n| `output_path` / `output_url` | one (sync) | — | Sync mode requires exactly one. Async mode: both optional (auto-staged). |\n| `output_format` | no | `mp4` | `mp4` / `webm` / `mov` / `mkv`. |\n| `async_job` | no | `false` | `true` → 202 + `job_id`; poll `/v1/jobs/{id}`. |\n| `webhook_url` | no | — | Async only. HMAC-signed POST on completion — see [Async Job Lifecycle](#async-job-lifecycle). |\n\n### Response\n\n`200` (sync) — one of:\n\n```json\n{ \"path\": \"out/lipsynced.mp4\", \"size\": 4823110 }\n```\n```json\n{ \"url\": \"https://bucket.example.com/out.mp4?...\", \"size\": 4823110 }\n```\n\n`StagedOutputResponse` may also carry `sha256`, `duration_sec`, `width`, `height`. `202` (async) — `{ \"job_id\": \"<uuid>\", \"status\": \"accepted\" }`.\n\n### Error Contract\n\n| Status | `code` | When |\n|---|---|---|\n| 400 | `BAD_REQUEST` | not exactly one of `face_*`, not exactly one of `audio_*`, `output_path`+`output_url` both set, or neither in sync mode |\n| 401 | `UNAUTHORIZED` | `FLICKIES_AUTH_TOKEN` set, missing/wrong bearer |\n| 403 | `NONCOMMERCIAL_GATE_REFUSED` | `wav2lip` / `wav2lip-gan` requested but server has no `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| 404 | `NOT_FOUND` | engine slug or referenced `file_path` missing |\n| 422 | `VALIDATION_FAILED` | Pydantic validation (missing/wrong-typed fields) |\n\nError body is always `{ \"code\": \"UPPER_SNAKE\", \"message\": \"...\", \"details\"?: {...} }`.\n\n## API — `POST /v1/video/restore`\n\nGFPGAN face restoration on a video. Input via `file_path` / `file_url`, output via `output_path` / `output_url` (same contract).\n\n### Request Fields\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source video. |\n| `engine` | no | `gfpgan` | Only `gfpgan`. |\n| `output_path` / `output_url` | one (sync) | — | Standard output contract. |\n| `output_format` / `async_job` / `webhook_url` | no | `mp4` / `false` / — | As above. |\n\n### Response\n\n`200` → `StagedOutputResponse` or `UrlOutputResponse` (as lipsync). `202` → `JobAcceptedResponse`.\n\n### Error Contract\n\n`400` `BAD_REQUEST`, `401` `UNAUTHORIZED`, `422` `VALIDATION_FAILED`. (GFPGAN carries no non-commercial gate.)\n\n## API — ffmpeg ops (`POST /v1/video/{trim,concat,transcode,scale,mux_audio,extract_audio,thumbnail_grid}`)\n\nPure ffmpeg, CPU. Each takes the standard input/output contract plus op-specific fields. All return `StagedOutputResponse` xor `UrlOutputResponse` on `200`. All support `async_job` / `webhook_url` except where noted (the info-shaped ones — `extract_audio`, `thumbnail_grid` — carry no `BaseVideoOutputRequest`; they take `output_path` / `output_url` directly and run sync).\n\n### `trim` — cut `[start_sec, end_sec]`\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `start_sec` | yes | — | ≥ 0. |\n| `end_sec` | yes | — | ≥ 0. |\n| `precise` | no | `false` | `false`: `-c copy`, fast, but `start_sec` snaps to the nearest keyframe (can eat up to one GOP of leading content). `true`: re-encode H.264 + AAC for frame-accurate boundaries (slower, visually transparent). |\n\n### `concat` — join ≥2 videos in order\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `inputs_paths` / `inputs_urls` | exactly one | — | Array, `minItems: 2`. FILES_DIR paths or HTTP(S) URLs. |\n| `precise` | no | `false` | `false`: concat demuxer + `-c copy` — requires identical codec/timebase/SAR across inputs. `true`: re-encode to uniform H.264 + AAC so mixed inputs join cleanly (slower). |\n\n### `transcode` — universal re-encode\n\n`output_format` (from the output contract, `mp4`/`webm`/`mov`/`mkv`) drives the filter graph. Additional:\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `video_codec` | no | — | e.g. `libx264`, `libx265`, `libvpx-vp9`, `libaom-av1`. |\n| `audio_codec` | no | — | e.g. `aac`, `libopus`, `copy`. |\n| `crf` | no | — | 0–51. |\n| `preset` | no | — | e.g. `ultrafast`, `fast`, `medium`, `slow`. |\n| `fps` | no | — | 1–240. Applies to all output formats. |\n| `gif_options` | no | — | Only consulted for GIF output: `{ width?, loop? (0 = infinite), palette_mode? (full/diff/single) }`. |\n\n### `scale` — resize\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `width` | yes | — | ≥ 16. |\n| `height` | yes | — | ≥ 16. |\n| `keep_aspect` | no | `true` | Pad/crop to maintain source aspect. |\n\n### `mux_audio` — replace / merge the audio track\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `video_path_or_url` | yes | — | Video source (path or URL — the server sniffs `http(s)://`). |\n| `audio_path_or_url` | yes | — | Audio source (path or URL). |\n| `replace_existing_audio` | no | `true` | `false` merges instead of replacing. |\n\n### `extract_audio` — pull the audio out\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `audio_format` | no | `wav` | `wav` / `mp3` / `m4a` / `ogg` / `flac`. |\n| `output_path` / `output_url` | — | — | Output target (this op has no async/webhook fields). |\n\n### `thumbnail_grid` — sprite-sheet PNG\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `rows` | yes | — | 1–16. |\n| `cols` | yes | — | 1–16. |\n| `cell_width` | no | `320` | Per-cell width. |\n| `cell_height` | no | `180` | Per-cell height. |\n| `output_path` / `output_url` | — | — | Output target (no async/webhook fields). |\n\n### Error Contract (ffmpeg ops)\n\nSame envelope. `400` `BAD_REQUEST` (bad input xor, constraint violation), `422` `VALIDATION_FAILED` (missing `start_sec`/`end_sec`, `rows`/`cols`, `width`/`height`, arrays under `minItems`), `401` when auth is on.\n\n## API — `POST /v1/video/info`\n\nffprobe metadata. Input via `file_path` / `file_url`; no output fields.\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq\n```\n\n### Response\n\n```json\n{\n  \"duration_sec\": 12.4, \"width\": 1920, \"height\": 1080, \"fps\": 30.0,\n  \"video_codec\": \"h264\", \"audio_codec\": \"aac\", \"bitrate\": 4200000,\n  \"container_format\": \"mov,mp4,m4a,3gp,3g2,mj2\", \"size_bytes\": 6510022\n}\n```\n\n`audio_codec`, `bitrate`, `container_format` are nullable. Errors: `400` `BAD_REQUEST`, `401` `UNAUTHORIZED`.\n\n## Async Job Lifecycle\n\nAny video-producing endpoint (`lipsync`, `restore`, and the ffmpeg ops that carry `async_job`) runs fire-and-forget when you set `async_job: true`. Lipsync/restore are the ones worth doing async — they're the slow ones.\n\n**1. Submit** — POST with `async_job: true`. Output is optional; if you omit both `output_path` and `output_url`, the server auto-stages the result to `jobs/{job_id}.{ext}` under FILES_DIR. Response is `202`:\n\n```bash\nJOB=$(curl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"async_job\": true\n      }' | jq -r .job_id)\n```\n\n**2. Poll** — `GET /v1/jobs/{job_id}`:\n\n```bash\ncurl -s \"$FLICKIES_URL/v1/jobs/$JOB\" | jq\n# { \"job_id\": \"...\", \"status\": \"running\", \"result\": null, \"error\": null }\n```\n\n`status` ∈ `pending` / `running` / `complete` / `failed` / `cancelled`. On `complete`, `result` holds the output payload (`{path, size}` or `{url, size}`). On `failed`, `error` holds `{code, message}`.\n\n**3. Fetch** — when `status: complete`, download the auto-staged result:\n\n```bash\ncurl -s \"$FLICKIES_URL/v1/files/jobs/$JOB.mp4\" --output result.mp4\n```\n\n**Webhook alternative** — pass `webhook_url` on the async submit and the server POSTs the final job state (`{job_id, status, result, error}`) to that URL on completion instead of making you poll:\n\n- HMAC-SHA256 over `timestamp + \".\" + body`, keyed by the server's `FLICKIES_WEBHOOK_SECRET`.\n- Headers: `X-Webhook-Timestamp: <unix-ts>`, `X-Webhook-Signature: t=<ts>,v1=<hex>`.\n- Retried on non-2xx / transport error with exponential backoff (30s, 1m, 5m, 30m, 2h, 12h), then dead-lettered to the server log.\n- Receiver MUST verify the signature and de-dupe on `(timestamp, signature)`.\n\n`GET /v1/jobs/{job_id}` returns `404` `NOT_FOUND` for an unknown id. The queue is in-process — jobs don't survive a container restart.\n\n## MCP Endpoint\n\nflickies mounts a [Model Context Protocol](https://modelcontextprotocol.io) server at `/v1/mcp` (streamable-HTTP JSON-RPC, same FastAPI process, same auth middleware). Point a function-calling LLM at it and it drives the pipeline.\n\nEleven tools mirror the REST surface: `list_engines`, `info`, `lipsync`, `restore`, `transcode`, `trim`, `concat`, `scale`, `mux_audio`, `extract_audio`, `thumbnail_grid`. Argument shapes match the REST bodies, with one difference: **MCP tools accept `output_path` only** (they write under FILES_DIR; `output_url` presigned-PUT delivery is REST-only). MCP tools run synchronously — there's no `async_job` on the MCP side.\n\nWire it into Claude Code:\n\n```bash\nclaude mcp add --transport http flickies $FLICKIES_URL/v1/mcp\n# with auth:\nclaude mcp add --transport http flickies $FLICKIES_URL/v1/mcp \\\n  --header \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"\n```\n\nThe transport requires `Accept: application/json, text/event-stream`. Raw JSON-RPC over HTTP POST for debugging / non-MCP callers:\n\n```bash\n# tools/list\ncurl -s \"$FLICKIES_URL/v1/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\"}'\n\n# tools/call — lipsync\ncurl -s \"$FLICKIES_URL/v1/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"lipsync\",\n      \"arguments\": {\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"output_path\": \"out/lipsynced.mp4\"\n      }\n    }\n  }'\n```\n\nThe canonical mount path carries a trailing slash (`/v1/mcp/`); bare `/v1/mcp` redirects to it. With auth on, every MCP call needs the same `Authorization: Bearer` header.\n\n## Bearer-Token Auth\n\nIf `FLICKIES_AUTH_TOKEN` is set on the server, every route except `/healthz` (and CORS preflight) requires `Authorization: Bearer <token>`. Wrong/missing token returns `401` `UNAUTHORIZED`.\n\n```bash\ncurl -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" $FLICKIES_URL/v1/engines\n```\n\nWith `FLICKIES_AUTH_TOKEN` unset the API/MCP surface is unauthenticated — anyone who can reach it gets full access. Set the token and bind to loopback / behind an authenticating proxy; for untrusted networks add a reverse proxy doing TLS + rate limiting. See [references/setup.md](references/setup.md).\n\n## Typical Workflows\n\n### Lipsync + face restore in one shot\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_path\": \"uploads/portrait.png\",\n        \"audio_path\": \"uploads/line.wav\",\n        \"engine\": \"wav2lip-gan\",\n        \"restore_face\": true,\n        \"output_path\": \"out/talking.mp4\"\n      }' | jq\n# NOTE: wav2lip-gan needs FLICKIES_ENABLE_NONCOMMERCIAL=1 on the server.\n```\n\n### Stage once, run several ops off the same file\n\n```bash\ncurl -s -X PUT --data-binary @raw.mp4 \"$FLICKIES_URL/v1/files/uploads/raw.mp4\"\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/scale\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\":\"uploads/raw.mp4\",\"width\":1280,\"height\":720,\"output_path\":\"out/720p.mp4\"}' | jq\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/transcode\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\":\"uploads/raw.mp4\",\"output_format\":\"gif\",\"fps\":12,\"gif_options\":{\"width\":480}}' \\\n  --fail | jq   # async auto-stages if you omit output_path\n```\n\n### Concatenate several clips (mixed sources → re-encode)\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/concat\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"inputs_urls\": [\"https://ex.com/a.mp4\",\"https://ex.com/b.mp4\"],\n        \"precise\": true,\n        \"output_path\": \"out/joined.mp4\"\n      }' | jq\n```\n\n### Long lipsync, async + poll to completion\n\n```bash\nFLICKIES_URL=$FLICKIES_URL FLICKIES_AUTH_TOKEN=$FLICKIES_AUTH_TOKEN \\\n  bash scripts/flickies.sh lipsync \\\n  '{\"face_url\":\"https://ex.com/f.mp4\",\"audio_url\":\"https://ex.com/v.wav\",\"engine\":\"latentsync-1.5\"}' \\\n  result.mp4\n```\n\nSee [`scripts/flickies.sh`](scripts/flickies.sh) — submits any endpoint async, polls `/v1/jobs/{id}` to a terminal state, and downloads the staged result.\n\n### Free VRAM after a job\n\nFrees the resident engine from VRAM. Rarely needed — engines hot-swap on demand — so only evict one the current task loaded, and remember a shared instance may have another caller using it.\n\n```bash\ncurl -s -X DELETE \"$FLICKIES_URL/v1/engines/latentsync-1.5\"   # evict from VRAM (204)\n```\n\n## Tips\n\n1. **`file_url` over staging** — if the source is already at a URL, pass `file_url` and skip the upload round-trip.\n2. **`precise=false` is the fast path** for trim/concat but snaps to keyframes / needs matching codecs. Flip to `precise=true` when you need frame accuracy or are joining mismatched inputs — it re-encodes.\n3. **One engine resident at a time** — a lipsync request after a restore evicts the restore engine (hot-swap). `restore_face=true` intentionally chains GFPGAN second, evicting the lipsync model to free VRAM.\n4. **`async_job=true` for lipsync/restore** — they're slow. Submit, then poll `/v1/jobs/{id}` or take a webhook. ffmpeg ops are fast enough to run sync.\n5. **Async output is auto-staged** — omit `output_path`/`output_url` on an async submit and fetch from `jobs/{job_id}.{ext}`.\n6. **`wav2lip*` is gated at the server** — 403 `NONCOMMERCIAL_GATE_REFUSED` means the operator hasn't set `FLICKIES_ENABLE_NONCOMMERCIAL=1`. You can't override it per-request; `latentsync-1.5` is the ungated default.\n7. **CPU image can't run `latentsync-1.5` or `gfpgan`** — both are CUDA-only. Wav2Lip-CPU works (slow) if the gate is set. Use `:latest-cuda` for the full engine set.\n8. **`GET /v1/health`** shows `device`, `loaded_engine`, and `noncommercial_enabled` — check it before you get a surprise 403 or a CPU-refusal.\n9. **Idempotency-Key** — pass an `Idempotency-Key` header on a POST for safe retries; the server replays the cached response for a repeat `(key, method, path)`.\n10. **`X-Request-Id`** — send one to correlate logs across hops; the server echoes it back and mints a UUID4 if you don't.\n\nFile v0.3.17:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"flickies\",\n  \"version\": \"0.3.17\",\n  \"publishedAt\": 1791644871846\n}\n\nFile v0.3.17:references/setup.md\n\n# flickies setup\n\n## Requirements\n\n- Docker\n- Optional: NVIDIA GPU + NVIDIA Container Toolkit for the CUDA image (required for `latentsync-1.5` and `gfpgan`; Wav2Lip runs on CPU too, slowly)\n- A bind-mounted `/data` volume for model weights + staged files (weights live in the standard HuggingFace cache layout and are reusable across containers)\n- Tested GPU ceiling: **RTX 3060 12 GB** — fits LatentSync 1.5 (~8 GB) with headroom; the Wav2Lip + GFPGAN chain peaks at ~5 GB. One engine resident at a time.\n\n## Quick Install\n\n### CPU\n\nRuns every ffmpeg op (trim / concat / transcode incl. gif / scale / mux / extract / thumbnail-grid / info) plus Wav2Lip-CPU (~44s for a 3s clip — fine for short clips, and only when the non-commercial gate is set). GFPGAN and LatentSync 1.5 are CUDA-only — the CPU image refuses to load them.\n\n```bash\ndocker run -d --name flickies \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest\n```\n\n### CUDA\n\nRuns every engine at usable speed (LatentSync 1.5, Wav2Lip / Wav2Lip-GAN, GFPGAN) plus all ffmpeg ops. Requires the NVIDIA Container Toolkit on the host.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\nBoth images `EXPOSE 8000` and bind `0.0.0.0:8000` inside the container (the entrypoint forces `FLICKIES_HOST=0.0.0.0`). Control network exposure at `docker run` time with `-p` (see [Ports](#ports)).\n\n**Verify:** `curl http://localhost:8000/healthz` returns `{\"status\": \"ok\"}` once boot is done. `curl http://localhost:8000/v1/health | jq` gives the richer discovery payload (device, ffmpeg version, available/enabled/loaded engines, non-commercial flag).\n\n### Enable the non-commercial engines (Wav2Lip)\n\nWav2Lip / Wav2Lip-GAN are trained on LRS2 (non-commercial). The server refuses to load them unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set. LatentSync 1.5 (Apache-2.0) is the commercial-safe default and needs no gate.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\n## Model Weights\n\nWeights live in the standard HuggingFace cache layout under `/data/hf/hub/models--<org>--<name>/…` (content-addressed blobs + snapshot symlinks), reusable by any HF-aware tool sharing the bind mount.\n\n| engine | HF repo | license | gate |\n|---|---|---|---|\n| `latentsync-1.5` | `ByteDance/LatentSync-1.5` | Apache-2.0 | none (CUDA-only) |\n| `wav2lip` / `wav2lip-gan` | `Nekochu/Wav2Lip` | LRS2 non-commercial | `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| `gfpgan` | `leonelhs/gfpgan` | Apache-2.0 | none (CUDA-only) |\n| S3FD detector | `ByteDance/LatentSync-1.5` (bundled) | — | — |\n\n**Lazy by default** — each engine fetches its repo on first request. To pull at boot before the server accepts requests, set `FLICKIES_ENABLED_ENGINES=wav2lip,gfpgan` (prefetch just those) or `FLICKIES_PREFETCH_ALL=1` (prefetch all; CUDA engines pulled only when the device is CUDA). `FLICKIES_OFFLINE=1` skips auto-download entirely (operator stages the snapshot dir manually).\n\n## Environment Variables\n\nAll server-side (set at `docker run` time). Everything defaults sensibly; the two you'll actually touch are `FLICKIES_AUTH_TOKEN` and `FLICKIES_ENABLE_NONCOMMERCIAL`.\n\n### Core\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_AUTH_TOKEN` | (empty = no auth) | Bearer token required on every route except `/healthz`. Empty/unset = wide open: the API/MCP surface is unauthenticated and anyone who can reach the port gets full access. When set, `Authorization: Bearer <token>` is required on every HTTP request and MCP call. Set it for any deployment beyond localhost, and bind to loopback / behind an authenticating proxy. See [Security & safety](../SKILL.md#security--safety) in the skill doc. |\n| `FLICKIES_ENABLE_NONCOMMERCIAL` | (unset = refuse) | Set to `1` / `true` / `yes` / `on` to allow the `wav2lip` / `wav2lip-gan` engines to load (LRS2 non-commercial training data). Unset → those slugs return 403 `NONCOMMERCIAL_GATE_REFUSED`. |\n| `FLICKIES_DEVICE` | `auto` | `auto` picks `cuda` if available else `cpu`. Also `cpu` / `cuda`. |\n| `FLICKIES_DATA_DIR` | `/data` | Base data dir. Staged files → `<data>/uploads` + FILES_DIR; model snapshots → `<data>/hf` cache; async job outputs → `<data>/jobs/`. Bind-mount to persist across restarts. |\n\n### Engines + prefetch\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_ENGINES_FILE` | `/app/engines.json` | Path to the engine registry JSON. Override to ship a custom subset. |\n| `FLICKIES_ENABLED_ENGINES` | (empty = all, lazy) | Comma-separated engine slug whitelist to prefetch at boot. Empty → download lazily on first request. |\n| `FLICKIES_PREFETCH_ALL` | (unset) | `1` → prefetch every engine in `engines.json` at boot (CUDA engines only when the device is CUDA). |\n| `FLICKIES_OFFLINE` | (unset) | `1` → skip prefetch; weights must be staged manually. |\n| `FLICKIES_IDLE_UNLOAD_SECS` | `600` | Idle seconds before the background sweeper unloads a resident engine from VRAM. Set high to keep a model warm. |\n\n### Webhooks\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_WEBHOOK_SECRET` | (empty) | HMAC-SHA256 signing key for async-completion webhooks. When set, `X-Webhook-Signature: t=<ts>,v1=<hex>` is computed over `timestamp + \".\" + body`; empty → signature header sent empty. Receivers verify with this shared secret. |\n\n### Logging\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_LOG_LEVEL` | `INFO` | `DEBUG` gives reconstruction-grade tracing (every ffmpeg/ffprobe command + result, engine timing, job lifecycle). |\n| `FLICKIES_LOG_FILE` | `<data>/logs/flickies.log` | Rotating JSON log file (in addition to stderr). |\n\n### Bind (usually leave alone)\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_HOST` | `0.0.0.0` (forced by entrypoint) | Bind address inside the container. Control external exposure via `-p` at `docker run` time, not this. |\n| `FLICKIES_PORT` | `8000` | Bind port inside the container. |\n\n### HuggingFace token (private repos only)\n\n`HF_TOKEN` / `HUGGINGFACE_TOKEN` are aliased to each other by the entrypoint. Set one if any engine repo is private. `TORCH_HOME` defaults to `<data>/torch_cache`.\n\n## Ports\n\n| Port | Service |\n| ---- | ------- |\n| 8000 | HTTP REST API + MCP (`/v1/mcp`) on the same port |\n\nThe container binds `0.0.0.0:8000` unconditionally. Use `-p` at `docker run` time:\n\n- `-p 127.0.0.1:8000:8000` — loopback-only on the host.\n- `-p 8000:8000` — all host interfaces.\n- For untrusted networks, combine `FLICKIES_AUTH_TOKEN` with a reverse proxy doing TLS + rate limiting.\n\n## Common Configurations\n\n```bash\n# Bearer auth + non-commercial engines on a CUDA host.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_AUTH_TOKEN=$(openssl rand -hex 32) \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Prefetch weights at boot so the first request doesn't pay the download tax.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_ENABLED_ENGINES=latentsync-1.5,gfpgan \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Keep a model resident forever (disable idle unload).\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_IDLE_UNLOAD_SECS=999999999 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Webhooks: sign async-completion callbacks.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_WEBHOOK_SECRET=$(openssl rand -hex 32) \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Loopback only (rely on a reverse proxy for external access).\ndocker run -d --name flickies -p 127.0.0.1:8000:8000 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest\n```\n\n## Custom Engine Registry\n\nThe image ships `engines.json` baked at `/app/engines.json`. Override without rebuilding by bind-mounting your own or pointing `FLICKIES_ENGINES_FILE` at a different path inside the container:\n\n```bash\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -v $HOME/flickies-data:/data \\\n  -v $PWD/my-engines.json:/app/engines.json:ro \\\n  psyb0t/flickies:latest-cuda\n```\n\nEach engine entry carries `executor`, optional `variant`, `weights_file`, `cuda_only`, `noncommercial`, `vram_gb_min`, and `description`.\n\n## Management\n\n```bash\ndocker logs -f flickies                  # tail logs\ndocker stop flickies                     # stop\ndocker rm flickies                       # remove\ndocker pull psyb0t/flickies:latest       # update (CPU)\ndocker pull psyb0t/flickies:latest-cuda  # update (CUDA)\n```\n\nInspect + control resident engines over the API:\n\n```bash\ncurl -s http://localhost:8000/v1/engines | jq              # list + load state + idle age\ncurl -s -X DELETE http://localhost:8000/v1/engines/latentsync-1.5   # evict a resident engine from VRAM (204)\n```\n\nEngine eviction and staged-file removal are state-changing operations — run them only against a resource the current task created, and only when the user asked. On a shared instance, evicting an engine can interrupt another caller who is mid-request with it.\n\n## OpenClaw / ClawHub Config\n\n```bash\nexport FLICKIES_URL=http://localhost:8000\nexport FLICKIES_AUTH_TOKEN=<token>  # only if the server requires it\n```\n\nOr via `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"flickies\": {\n        \"env\": {\n          \"FLICKIES_URL\": \"http://localhost:8000\",\n          \"FLICKIES_AUTH_TOKEN\": \"<token>\"\n        }\n      }\n    }\n  }\n}\n```\n\nThe skill talks to an instance the operator already runs. It never provisions, installs, or escalates on the caller's machine — it only sends requests to `FLICKIES_URL`.\n\nFile v0.3.17:skill-card.md\n\n## Description:\n\nHelps agents use a self-hosted video API to lip-sync footage, restore faces, edit and transcode media, and inspect video metadata through REST or MCP.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[psyb0t](https://clawhub.ai/user/psyb0t)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and media creators use this skill to direct a self-hosted video service to lip-sync faces, restore footage, perform common editing operations, and retrieve metadata or processed files.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Default container setup may expose an unauthenticated video-processing API to the network.\n\nMitigation: Run on a trusted host, bind the published port to loopback, set FLICKIES_AUTH_TOKEN, and do not expose the API directly to untrusted networks.\n\nRisk: Uploads, remote media inputs, output destinations, and webhook URLs can share media or results beyond the local host.\n\nMitigation: Use only approved local media and explicitly authorized URLs for remote inputs, outputs, and callbacks.\n\nRisk: Unpinned container images can change between deployments.\n\nMitigation: Pin Docker images to a version or digest when possible.\n\n## Reference(s):\n\n- [flickies setup guide](references/setup.md)\n- [ClawHub skill listing](https://clawhub.ai/psyb0t/skills/flickies)\n- [flickies project homepage](https://github.com/psyb0t/docker-flickies)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Configuration instructions, API calls]\n\n**Output Format:** [Markdown with JSON and shell examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May guide retrieval of processed video, audio, thumbnails, and metadata from the configured service.]\n\n## Skill Version(s):\n\n0.3.17 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.3.16: 5 files, 16668 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (2588b), SKILL.md (23846b), _meta.json (128b)\n\nFile v0.3.16:SKILL.md\n\n---\nname: flickies\ndescription: Self-hosted video REST + MCP API. POST JSON, get a video back. Lipsync (LatentSync 1.5 + Wav2Lip/Wav2Lip-GAN) at /v1/video/lipsync, GFPGAN face restore at /v1/video/restore, pure-ffmpeg ops (trim, concat, transcode incl. gif + fps + codec, scale, mux_audio, extract_audio, thumbnail_grid) under /v1/video/*, and ffprobe metadata at /v1/video/info. file_path (staged) xor file_url in; output_path xor output_url out. Fire-and-forget async jobs (async_job=true → 202 → poll /v1/jobs/{id}) with HMAC-signed webhooks. 11 MCP tools at /v1/mcp. Bearer-token auth. CPU + CUDA images. Use when the user wants to lipsync a face to audio, restore faces in footage, trim/concat/transcode/scale/mux/extract/thumbnail video, probe a video's metadata, or drive any of that from an LLM over MCP.\nhomepage: https://github.com/psyb0t/docker-flickies\nuser-invocable: true\npermissions:\n  - network: outbound HTTP to the configured FLICKIES_URL, plus server-side fetch of file_url, delivery to output_url, and HMAC-signed webhook callbacks\n  - shell: the documented examples invoke local curl / docker\n  - filesystem: manages server-side staged files (upload, fetch, remove) and engine lifecycle (load, evict) on the configured instance\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🎬\", \"primaryEnv\": \"FLICKIES_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# flickies\n\nSelf-hosted video toolkit — lipsync, face restore, and ffmpeg ops in one container. POST a JSON body, get a video back. Every video endpoint takes the same input/output contract; drive it from curl, the generated Go/Python clients, or point a function-calling LLM at the MCP endpoint.\n\nLipsync (`POST /v1/video/lipsync`): drive a face video/image from an audio track. Engines: `latentsync-1.5` (ByteDance, Apache-2.0, commercial-safe default, CUDA-only) and `wav2lip` / `wav2lip-gan` (Rudrabha, fast/low-VRAM, LRS2 non-commercial — refused unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set in the **server** env). `restore_face=true` chains GFPGAN over the result.\n\nFace restore (`POST /v1/video/restore`): GFPGAN v1.4 (`gfpgan`, Apache-2.0) — clean up a Wav2Lip mouth crop or old footage, standalone.\n\nffmpeg ops (pure CPU, no engine): `POST /v1/video/trim`, `/concat`, `/transcode` (mp4/webm/mov/mkv + gif + fps + codec change), `/scale`, `/mux_audio`, `/extract_audio`, `/thumbnail_grid`. Metadata: `POST /v1/video/info` (ffprobe).\n\nExtras: async jobs (`async_job=true` → 202 + `job_id` → poll `GET /v1/jobs/{job_id}`), HMAC-signed webhooks on async completion, server-side file staging, engine load/evict control, an MCP endpoint at `/v1/mcp` with 11 tools, optional bearer-token auth.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Auth is off by default** — `FLICKIES_AUTH_TOKEN` is unset out of the box, so the whole API is open to anyone who can reach the port. Set it for any deployment beyond localhost, pass `Authorization: Bearer <token>` once it's set, and bind to loopback / behind an authenticating proxy. Never expose an unauthenticated instance on a network.\n- **File and engine management** — the staging and engine endpoints include remove and evict operations. Like any state change, only run them against a resource the current task created, only when the user asked, and not against a shared instance others depend on.\n\n## When To Use\n\n- Lipsync a face (video or still image) to a driving audio track — `latentsync-1.5` (commercial-safe, CUDA) or `wav2lip` / `wav2lip-gan` (non-commercial gate).\n- Restore / sharpen faces in a video with GFPGAN, standalone or chained after a Wav2Lip pass (`restore_face=true`).\n- Trim a clip, concatenate several clips, or transcode to another container/codec (incl. animated GIF).\n- Change frame rate, re-encode with a specific codec/CRF/preset, scale dimensions, mux an audio track in, extract the audio out, or generate a thumbnail sprite-sheet.\n- Probe a video for duration, codec, fps, dimensions, bitrate (`/v1/video/info`).\n- Run any long operation fire-and-forget: submit with `async_job=true`, poll `/v1/jobs/{id}`, or receive an HMAC-signed webhook on completion.\n- Drive the whole pipeline from a function-calling LLM (Claude, LibreChat, Cursor) via MCP at `/v1/mcp`.\n\n## When NOT To Use\n\n- Real-time / streaming video output — every endpoint is request/response; long jobs go async + poll, not stream.\n- `latentsync-1.5` on the CPU image — it's CUDA-only (`cuda_only: true`) and the CPU image refuses to load it. GFPGAN is also CUDA-only in practice. Use the `:latest-cuda` image for those.\n- `wav2lip` / `wav2lip-gan` anywhere unless the **server** was started with `FLICKIES_ENABLE_NONCOMMERCIAL=1` — otherwise the request returns 403 `NONCOMMERCIAL_GATE_REFUSED`. This is a server-side env flag; you cannot flip it per-request.\n- Two engines resident at once — one model is resident at a time. A different engine request triggers hot-swap eviction of the current one. If you need two simultaneously, run two containers.\n- `output_url` via the MCP tools — MCP tools only accept `output_path` (they write under FILES_DIR). Use the REST endpoint if you need presigned-PUT `output_url` delivery.\n- Multipart upload on the video endpoints — only `PUT /v1/files/{path}` accepts a raw-body upload. Video endpoints take JSON with `file_path` or `file_url`.\n\n## Setup\n\nThe container should already be running. Set the base URL:\n\n```bash\nexport FLICKIES_URL=http://localhost:8000\n```\n\nIf the server has `FLICKIES_AUTH_TOKEN` set, export it too:\n\n```bash\nexport FLICKIES_AUTH_TOKEN=<your-token>\n# every request below then needs: -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"\n```\n\n**Verify:** `curl $FLICKIES_URL/healthz` returns `{\"status\": \"ok\"}` (unversioned, always auth-exempt). For the richer discovery payload — device, ffmpeg version, available/enabled/loaded engines, non-commercial flag — hit `GET /v1/health`:\n\n```bash\ncurl -s $FLICKIES_URL/v1/health | jq\n# { \"status\": \"ok\", \"version\": \"...\", \"device\": \"cuda\", \"ffmpeg\": \"...\",\n#   \"available_engines\": [...], \"enabled_engines\": [...],\n#   \"loaded_engine\": null, \"noncommercial_enabled\": false }\n```\n\nFor install / configuration / env vars / CPU vs CUDA images / engine weights, see [references/setup.md](references/setup.md).\n\n## Quick Start\n\nThe input/output contract is uniform across every video endpoint:\n\n- **Input** — exactly one of `file_path` (FILES_DIR-relative, staged via `/v1/files`) or `file_url` (any HTTP/HTTPS URL the server fetches).\n- **Output** — exactly one of `output_path` (server writes to FILES_DIR/<path>, response `{path, size, ...}`; download via `GET /v1/files/<path>`) or `output_url` (server PUTs to a presigned/PUT-accepting URL, response `{url, size, ...}`). In async mode both are optional — the server auto-stages to `jobs/{id}.{ext}`.\n\n```bash\n# Probe a video (stage it first, then reference by path).\ncurl -s -X PUT --data-binary @clip.mp4 \\\n  -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" \\\n  \"$FLICKIES_URL/v1/files/uploads/clip.mp4\"\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq\n\n# Trim seconds 5–12, write the result under FILES_DIR, download it.\ncurl -s -X POST \"$FLICKIES_URL/v1/video/trim\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"file_path\": \"uploads/clip.mp4\",\n        \"start_sec\": 5, \"end_sec\": 12, \"precise\": true,\n        \"output_path\": \"out/clip-trimmed.mp4\"\n      }' | jq\ncurl -s \"$FLICKIES_URL/v1/files/out/clip-trimmed.mp4\" --output clip-trimmed.mp4\n\n# Lipsync a face to an audio track (LatentSync, commercial-safe default, CUDA).\ncurl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"output_path\": \"out/lipsynced.mp4\"\n      }' | jq\n```\n\nAdd `-H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"` to every call if the server has a token set. `/healthz` is the only always-exempt route.\n\n## API — `POST /v1/video/lipsync`\n\nDrive a face from an audio track. JSON body.\n\n### Request Fields\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `face_path` / `face_url` | exactly one | — | Driving face — a video OR a still image. `face_path` is FILES_DIR-relative; `face_url` is an HTTP(S) URL the server fetches. |\n| `audio_path` / `audio_url` | exactly one | — | Driving audio (wav/mp3/m4a). `audio_path` FILES_DIR-relative; `audio_url` an HTTP(S) URL. |\n| `engine` | no | `latentsync-1.5` | `latentsync-1.5` (Apache-2.0, CUDA-only, commercial-safe) / `wav2lip` / `wav2lip-gan`. The two `wav2lip*` slugs require `FLICKIES_ENABLE_NONCOMMERCIAL=1` on the server → else 403. |\n| `restore_face` | no | `false` | Chain GFPGAN over the output to clean up the face region. Recommended after `wav2lip*` (fixes the soft 96×96 mouth crop). |\n| `output_path` / `output_url` | one (sync) | — | Sync mode requires exactly one. Async mode: both optional (auto-staged). |\n| `output_format` | no | `mp4` | `mp4` / `webm` / `mov` / `mkv`. |\n| `async_job` | no | `false` | `true` → 202 + `job_id`; poll `/v1/jobs/{id}`. |\n| `webhook_url` | no | — | Async only. HMAC-signed POST on completion — see [Async Job Lifecycle](#async-job-lifecycle). |\n\n### Response\n\n`200` (sync) — one of:\n\n```json\n{ \"path\": \"out/lipsynced.mp4\", \"size\": 4823110 }\n```\n```json\n{ \"url\": \"https://bucket.example.com/out.mp4?...\", \"size\": 4823110 }\n```\n\n`StagedOutputResponse` may also carry `sha256`, `duration_sec`, `width`, `height`. `202` (async) — `{ \"job_id\": \"<uuid>\", \"status\": \"accepted\" }`.\n\n### Error Contract\n\n| Status | `code` | When |\n|---|---|---|\n| 400 | `BAD_REQUEST` | not exactly one of `face_*`, not exactly one of `audio_*`, `output_path`+`output_url` both set, or neither in sync mode |\n| 401 | `UNAUTHORIZED` | `FLICKIES_AUTH_TOKEN` set, missing/wrong bearer |\n| 403 | `NONCOMMERCIAL_GATE_REFUSED` | `wav2lip` / `wav2lip-gan` requested but server has no `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| 404 | `NOT_FOUND` | engine slug or referenced `file_path` missing |\n| 422 | `VALIDATION_FAILED` | Pydantic validation (missing/wrong-typed fields) |\n\nError body is always `{ \"code\": \"UPPER_SNAKE\", \"message\": \"...\", \"details\"?: {...} }`.\n\n## API — `POST /v1/video/restore`\n\nGFPGAN face restoration on a video. Input via `file_path` / `file_url`, output via `output_path` / `output_url` (same contract).\n\n### Request Fields\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source video. |\n| `engine` | no | `gfpgan` | Only `gfpgan`. |\n| `output_path` / `output_url` | one (sync) | — | Standard output contract. |\n| `output_format` / `async_job` / `webhook_url` | no | `mp4` / `false` / — | As above. |\n\n### Response\n\n`200` → `StagedOutputResponse` or `UrlOutputResponse` (as lipsync). `202` → `JobAcceptedResponse`.\n\n### Error Contract\n\n`400` `BAD_REQUEST`, `401` `UNAUTHORIZED`, `422` `VALIDATION_FAILED`. (GFPGAN carries no non-commercial gate.)\n\n## API — ffmpeg ops (`POST /v1/video/{trim,concat,transcode,scale,mux_audio,extract_audio,thumbnail_grid}`)\n\nPure ffmpeg, CPU. Each takes the standard input/output contract plus op-specific fields. All return `StagedOutputResponse` xor `UrlOutputResponse` on `200`. All support `async_job` / `webhook_url` except where noted (the info-shaped ones — `extract_audio`, `thumbnail_grid` — carry no `BaseVideoOutputRequest`; they take `output_path` / `output_url` directly and run sync).\n\n### `trim` — cut `[start_sec, end_sec]`\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `start_sec` | yes | — | ≥ 0. |\n| `end_sec` | yes | — | ≥ 0. |\n| `precise` | no | `false` | `false`: `-c copy`, fast, but `start_sec` snaps to the nearest keyframe (can eat up to one GOP of leading content). `true`: re-encode H.264 + AAC for frame-accurate boundaries (slower, visually transparent). |\n\n### `concat` — join ≥2 videos in order\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `inputs_paths` / `inputs_urls` | exactly one | — | Array, `minItems: 2`. FILES_DIR paths or HTTP(S) URLs. |\n| `precise` | no | `false` | `false`: concat demuxer + `-c copy` — requires identical codec/timebase/SAR across inputs. `true`: re-encode to uniform H.264 + AAC so mixed inputs join cleanly (slower). |\n\n### `transcode` — universal re-encode\n\n`output_format` (from the output contract, `mp4`/`webm`/`mov`/`mkv`) drives the filter graph. Additional:\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `video_codec` | no | — | e.g. `libx264`, `libx265`, `libvpx-vp9`, `libaom-av1`. |\n| `audio_codec` | no | — | e.g. `aac`, `libopus`, `copy`. |\n| `crf` | no | — | 0–51. |\n| `preset` | no | — | e.g. `ultrafast`, `fast`, `medium`, `slow`. |\n| `fps` | no | — | 1–240. Applies to all output formats. |\n| `gif_options` | no | — | Only consulted for GIF output: `{ width?, loop? (0 = infinite), palette_mode? (full/diff/single) }`. |\n\n### `scale` — resize\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `width` | yes | — | ≥ 16. |\n| `height` | yes | — | ≥ 16. |\n| `keep_aspect` | no | `true` | Pad/crop to maintain source aspect. |\n\n### `mux_audio` — replace / merge the audio track\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `video_path_or_url` | yes | — | Video source (path or URL — the server sniffs `http(s)://`). |\n| `audio_path_or_url` | yes | — | Audio source (path or URL). |\n| `replace_existing_audio` | no | `true` | `false` merges instead of replacing. |\n\n### `extract_audio` — pull the audio out\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `audio_format` | no | `wav` | `wav` / `mp3` / `m4a` / `ogg` / `flac`. |\n| `output_path` / `output_url` | — | — | Output target (this op has no async/webhook fields). |\n\n### `thumbnail_grid` — sprite-sheet PNG\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `rows` | yes | — | 1–16. |\n| `cols` | yes | — | 1–16. |\n| `cell_width` | no | `320` | Per-cell width. |\n| `cell_height` | no | `180` | Per-cell height. |\n| `output_path` / `output_url` | — | — | Output target (no async/webhook fields). |\n\n### Error Contract (ffmpeg ops)\n\nSame envelope. `400` `BAD_REQUEST` (bad input xor, constraint violation), `422` `VALIDATION_FAILED` (missing `start_sec`/`end_sec`, `rows`/`cols`, `width`/`height`, arrays under `minItems`), `401` when auth is on.\n\n## API — `POST /v1/video/info`\n\nffprobe metadata. Input via `file_path` / `file_url`; no output fields.\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq\n```\n\n### Response\n\n```json\n{\n  \"duration_sec\": 12.4, \"width\": 1920, \"height\": 1080, \"fps\": 30.0,\n  \"video_codec\": \"h264\", \"audio_codec\": \"aac\", \"bitrate\": 4200000,\n  \"container_format\": \"mov,mp4,m4a,3gp,3g2,mj2\", \"size_bytes\": 6510022\n}\n```\n\n`audio_codec`, `bitrate`, `container_format` are nullable. Errors: `400` `BAD_REQUEST`, `401` `UNAUTHORIZED`.\n\n## Async Job Lifecycle\n\nAny video-producing endpoint (`lipsync`, `restore`, and the ffmpeg ops that carry `async_job`) runs fire-and-forget when you set `async_job: true`. Lipsync/restore are the ones worth doing async — they're the slow ones.\n\n**1. Submit** — POST with `async_job: true`. Output is optional; if you omit both `output_path` and `output_url`, the server auto-stages the result to `jobs/{job_id}.{ext}` under FILES_DIR. Response is `202`:\n\n```bash\nJOB=$(curl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"async_job\": true\n      }' | jq -r .job_id)\n```\n\n**2. Poll** — `GET /v1/jobs/{job_id}`:\n\n```bash\ncurl -s \"$FLICKIES_URL/v1/jobs/$JOB\" | jq\n# { \"job_id\": \"...\", \"status\": \"running\", \"result\": null, \"error\": null }\n```\n\n`status` ∈ `pending` / `running` / `complete` / `failed` / `cancelled`. On `complete`, `result` holds the output payload (`{path, size}` or `{url, size}`). On `failed`, `error` holds `{code, message}`.\n\n**3. Fetch** — when `status: complete`, download the auto-staged result:\n\n```bash\ncurl -s \"$FLICKIES_URL/v1/files/jobs/$JOB.mp4\" --output result.mp4\n```\n\n**Webhook alternative** — pass `webhook_url` on the async submit and the server POSTs the final job state (`{job_id, status, result, error}`) to that URL on completion instead of making you poll:\n\n- HMAC-SHA256 over `timestamp + \".\" + body`, keyed by the server's `FLICKIES_WEBHOOK_SECRET`.\n- Headers: `X-Webhook-Timestamp: <unix-ts>`, `X-Webhook-Signature: t=<ts>,v1=<hex>`.\n- Retried on non-2xx / transport error with exponential backoff (30s, 1m, 5m, 30m, 2h, 12h), then dead-lettered to the server log.\n- Receiver MUST verify the signature and de-dupe on `(timestamp, signature)`.\n\n`GET /v1/jobs/{job_id}` returns `404` `NOT_FOUND` for an unknown id. The queue is in-process — jobs don't survive a container restart.\n\n## MCP Endpoint\n\nflickies mounts a [Model Context Protocol](https://modelcontextprotocol.io) server at `/v1/mcp` (streamable-HTTP JSON-RPC, same FastAPI process, same auth middleware). Point a function-calling LLM at it and it drives the pipeline.\n\nEleven tools mirror the REST surface: `list_engines`, `info`, `lipsync`, `restore`, `transcode`, `trim`, `concat`, `scale`, `mux_audio`, `extract_audio`, `thumbnail_grid`. Argument shapes match the REST bodies, with one difference: **MCP tools accept `output_path` only** (they write under FILES_DIR; `output_url` presigned-PUT delivery is REST-only). MCP tools run synchronously — there's no `async_job` on the MCP side.\n\nWire it into Claude Code:\n\n```bash\nclaude mcp add --transport http flickies $FLICKIES_URL/v1/mcp\n# with auth:\nclaude mcp add --transport http flickies $FLICKIES_URL/v1/mcp \\\n  --header \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"\n```\n\nThe transport requires `Accept: application/json, text/event-stream`. Raw JSON-RPC over HTTP POST for debugging / non-MCP callers:\n\n```bash\n# tools/list\ncurl -s \"$FLICKIES_URL/v1/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\"}'\n\n# tools/call — lipsync\ncurl -s \"$FLICKIES_URL/v1/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"lipsync\",\n      \"arguments\": {\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"output_path\": \"out/lipsynced.mp4\"\n      }\n    }\n  }'\n```\n\nThe canonical mount path carries a trailing slash (`/v1/mcp/`); bare `/v1/mcp` redirects to it. With auth on, every MCP call needs the same `Authorization: Bearer` header.\n\n## Bearer-Token Auth\n\nIf `FLICKIES_AUTH_TOKEN` is set on the server, every route except `/healthz` (and CORS preflight) requires `Authorization: Bearer <token>`. Wrong/missing token returns `401` `UNAUTHORIZED`.\n\n```bash\ncurl -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" $FLICKIES_URL/v1/engines\n```\n\nWith `FLICKIES_AUTH_TOKEN` unset the API/MCP surface is unauthenticated — anyone who can reach it gets full access. Set the token and bind to loopback / behind an authenticating proxy; for untrusted networks add a reverse proxy doing TLS + rate limiting. See [references/setup.md](references/setup.md).\n\n## Typical Workflows\n\n### Lipsync + face restore in one shot\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_path\": \"uploads/portrait.png\",\n        \"audio_path\": \"uploads/line.wav\",\n        \"engine\": \"wav2lip-gan\",\n        \"restore_face\": true,\n        \"output_path\": \"out/talking.mp4\"\n      }' | jq\n# NOTE: wav2lip-gan needs FLICKIES_ENABLE_NONCOMMERCIAL=1 on the server.\n```\n\n### Stage once, run several ops off the same file\n\n```bash\ncurl -s -X PUT --data-binary @raw.mp4 \"$FLICKIES_URL/v1/files/uploads/raw.mp4\"\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/scale\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\":\"uploads/raw.mp4\",\"width\":1280,\"height\":720,\"output_path\":\"out/720p.mp4\"}' | jq\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/transcode\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\":\"uploads/raw.mp4\",\"output_format\":\"gif\",\"fps\":12,\"gif_options\":{\"width\":480}}' \\\n  --fail | jq   # async auto-stages if you omit output_path\n```\n\n### Concatenate several clips (mixed sources → re-encode)\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/concat\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"inputs_urls\": [\"https://ex.com/a.mp4\",\"https://ex.com/b.mp4\"],\n        \"precise\": true,\n        \"output_path\": \"out/joined.mp4\"\n      }' | jq\n```\n\n### Long lipsync, async + poll to completion\n\n```bash\nFLICKIES_URL=$FLICKIES_URL FLICKIES_AUTH_TOKEN=$FLICKIES_AUTH_TOKEN \\\n  bash scripts/flickies.sh lipsync \\\n  '{\"face_url\":\"https://ex.com/f.mp4\",\"audio_url\":\"https://ex.com/v.wav\",\"engine\":\"latentsync-1.5\"}' \\\n  result.mp4\n```\n\nSee [`scripts/flickies.sh`](scripts/flickies.sh) — submits any endpoint async, polls `/v1/jobs/{id}` to a terminal state, and downloads the staged result.\n\n### Free VRAM after a job\n\nFrees the resident engine from VRAM. Rarely needed — engines hot-swap on demand — so only evict one the current task loaded, and remember a shared instance may have another caller using it.\n\n```bash\ncurl -s -X DELETE \"$FLICKIES_URL/v1/engines/latentsync-1.5\"   # evict from VRAM (204)\n```\n\n## Tips\n\n1. **`file_url` over staging** — if the source is already at a URL, pass `file_url` and skip the upload round-trip.\n2. **`precise=false` is the fast path** for trim/concat but snaps to keyframes / needs matching codecs. Flip to `precise=true` when you need frame accuracy or are joining mismatched inputs — it re-encodes.\n3. **One engine resident at a time** — a lipsync request after a restore evicts the restore engine (hot-swap). `restore_face=true` intentionally chains GFPGAN second, evicting the lipsync model to free VRAM.\n4. **`async_job=true` for lipsync/restore** — they're slow. Submit, then poll `/v1/jobs/{id}` or take a webhook. ffmpeg ops are fast enough to run sync.\n5. **Async output is auto-staged** — omit `output_path`/`output_url` on an async submit and fetch from `jobs/{job_id}.{ext}`.\n6. **`wav2lip*` is gated at the server** — 403 `NONCOMMERCIAL_GATE_REFUSED` means the operator hasn't set `FLICKIES_ENABLE_NONCOMMERCIAL=1`. You can't override it per-request; `latentsync-1.5` is the ungated default.\n7. **CPU image can't run `latentsync-1.5` or `gfpgan`** — both are CUDA-only. Wav2Lip-CPU works (slow) if the gate is set. Use `:latest-cuda` for the full engine set.\n8. **`GET /v1/health`** shows `device`, `loaded_engine`, and `noncommercial_enabled` — check it before you get a surprise 403 or a CPU-refusal.\n9. **Idempotency-Key** — pass an `Idempotency-Key` header on a POST for safe retries; the server replays the cached response for a repeat `(key, method, path)`.\n10. **`X-Request-Id`** — send one to correlate logs across hops; the server echoes it back and mints a UUID4 if you don't.\n\nFile v0.3.16:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"flickies\",\n  \"version\": \"0.3.16\",\n  \"publishedAt\": 1785617153539\n}\n\nFile v0.3.16:references/setup.md\n\n# flickies setup\n\n## Requirements\n\n- Docker\n- Optional: NVIDIA GPU + NVIDIA Container Toolkit for the CUDA image (required for `latentsync-1.5` and `gfpgan`; Wav2Lip runs on CPU too, slowly)\n- A bind-mounted `/data` volume for model weights + staged files (weights live in the standard HuggingFace cache layout and are reusable across containers)\n- Tested GPU ceiling: **RTX 3060 12 GB** — fits LatentSync 1.5 (~8 GB) with headroom; the Wav2Lip + GFPGAN chain peaks at ~5 GB. One engine resident at a time.\n\n## Quick Install\n\n### CPU\n\nRuns every ffmpeg op (trim / concat / transcode incl. gif / scale / mux / extract / thumbnail-grid / info) plus Wav2Lip-CPU (~44s for a 3s clip — fine for short clips, and only when the non-commercial gate is set). GFPGAN and LatentSync 1.5 are CUDA-only — the CPU image refuses to load them.\n\n```bash\ndocker run -d --name flickies \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest\n```\n\n### CUDA\n\nRuns every engine at usable speed (LatentSync 1.5, Wav2Lip / Wav2Lip-GAN, GFPGAN) plus all ffmpeg ops. Requires the NVIDIA Container Toolkit on the host.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\nBoth images `EXPOSE 8000` and bind `0.0.0.0:8000` inside the container (the entrypoint forces `FLICKIES_HOST=0.0.0.0`). Control network exposure at `docker run` time with `-p` (see [Ports](#ports)).\n\n**Verify:** `curl http://localhost:8000/healthz` returns `{\"status\": \"ok\"}` once boot is done. `curl http://localhost:8000/v1/health | jq` gives the richer discovery payload (device, ffmpeg version, available/enabled/loaded engines, non-commercial flag).\n\n### Enable the non-commercial engines (Wav2Lip)\n\nWav2Lip / Wav2Lip-GAN are trained on LRS2 (non-commercial). The server refuses to load them unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set. LatentSync 1.5 (Apache-2.0) is the commercial-safe default and needs no gate.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\n## Model Weights\n\nWeights live in the standard HuggingFace cache layout under `/data/hf/hub/models--<org>--<name>/…` (content-addressed blobs + snapshot symlinks), reusable by any HF-aware tool sharing the bind mount.\n\n| engine | HF repo | license | gate |\n|---|---|---|---|\n| `latentsync-1.5` | `ByteDance/LatentSync-1.5` | Apache-2.0 | none (CUDA-only) |\n| `wav2lip` / `wav2lip-gan` | `Nekochu/Wav2Lip` | LRS2 non-commercial | `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| `gfpgan` | `leonelhs/gfpgan` | Apache-2.0 | none (CUDA-only) |\n| S3FD detector | `ByteDance/LatentSync-1.5` (bundled) | — | — |\n\n**Lazy by default** — each engine fetches its repo on first request. To pull at boot before the server accepts requests, set `FLICKIES_ENABLED_ENGINES=wav2lip,gfpgan` (prefetch just those) or `FLICKIES_PREFETCH_ALL=1` (prefetch all; CUDA engines pulled only when the device is CUDA). `FLICKIES_OFFLINE=1` skips auto-download entirely (operator stages the snapshot dir manually).\n\n## Environment Variables\n\nAll server-side (set at `docker run` time). Everything defaults sensibly; the two you'll actually touch are `FLICKIES_AUTH_TOKEN` and `FLICKIES_ENABLE_NONCOMMERCIAL`.\n\n### Core\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_AUTH_TOKEN` | (empty = no auth) | Bearer token required on every route except `/healthz`. Empty/unset = wide open: the API/MCP surface is unauthenticated and anyone who can reach the port gets full access. When set, `Authorization: Bearer <token>` is required on every HTTP request and MCP call. Set it for any deployment beyond localhost, and bind to loopback / behind an authenticating proxy. See [Security & safety](../SKILL.md#security--safety) in the skill doc. |\n| `FLICKIES_ENABLE_NONCOMMERCIAL` | (unset = refuse) | Set to `1` / `true` / `yes` / `on` to allow the `wav2lip` / `wav2lip-gan` engines to load (LRS2 non-commercial training data). Unset → those slugs return 403 `NONCOMMERCIAL_GATE_REFUSED`. |\n| `FLICKIES_DEVICE` | `auto` | `auto` picks `cuda` if available else `cpu`. Also `cpu` / `cuda`. |\n| `FLICKIES_DATA_DIR` | `/data` | Base data dir. Staged files → `<data>/uploads` + FILES_DIR; model snapshots → `<data>/hf` cache; async job outputs → `<data>/jobs/`. Bind-mount to persist across restarts. |\n\n### Engines + prefetch\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_ENGINES_FILE` | `/app/engines.json` | Path to the engine registry JSON. Override to ship a custom subset. |\n| `FLICKIES_ENABLED_ENGINES` | (empty = all, lazy) | Comma-separated engine slug whitelist to prefetch at boot. Empty → download lazily on first request. |\n| `FLICKIES_PREFETCH_ALL` | (unset) | `1` → prefetch every engine in `engines.json` at boot (CUDA engines only when the device is CUDA). |\n| `FLICKIES_OFFLINE` | (unset) | `1` → skip prefetch; weights must be staged manually. |\n| `FLICKIES_IDLE_UNLOAD_SECS` | `600` | Idle seconds before the background sweeper unloads a resident engine from VRAM. Set high to keep a model warm. |\n\n### Webhooks\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_WEBHOOK_SECRET` | (empty) | HMAC-SHA256 signing key for async-completion webhooks. When set, `X-Webhook-Signature: t=<ts>,v1=<hex>` is computed over `timestamp + \".\" + body`; empty → signature header sent empty. Receivers verify with this shared secret. |\n\n### Logging\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_LOG_LEVEL` | `INFO` | `DEBUG` gives reconstruction-grade tracing (every ffmpeg/ffprobe command + result, engine timing, job lifecycle). |\n| `FLICKIES_LOG_FILE` | `<data>/logs/flickies.log` | Rotating JSON log file (in addition to stderr). |\n\n### Bind (usually leave alone)\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_HOST` | `0.0.0.0` (forced by entrypoint) | Bind address inside the container. Control external exposure via `-p` at `docker run` time, not this. |\n| `FLICKIES_PORT` | `8000` | Bind port inside the container. |\n\n### HuggingFace token (private repos only)\n\n`HF_TOKEN` / `HUGGINGFACE_TOKEN` are aliased to each other by the entrypoint. Set one if any engine repo is private. `TORCH_HOME` defaults to `<data>/torch_cache`.\n\n## Ports\n\n| Port | Service |\n| ---- | ------- |\n| 8000 | HTTP REST API + MCP (`/v1/mcp`) on the same port |\n\nThe container binds `0.0.0.0:8000` unconditionally. Use `-p` at `docker run` time:\n\n- `-p 127.0.0.1:8000:8000` — loopback-only on the host.\n- `-p 8000:8000` — all host interfaces.\n- For untrusted networks, combine `FLICKIES_AUTH_TOKEN` with a reverse proxy doing TLS + rate limiting.\n\n## Common Configurations\n\n```bash\n# Bearer auth + non-commercial engines on a CUDA host.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_AUTH_TOKEN=$(openssl rand -hex 32) \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Prefetch weights at boot so the first request doesn't pay the download tax.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_ENABLED_ENGINES=latentsync-1.5,gfpgan \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Keep a model resident forever (disable idle unload).\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_IDLE_UNLOAD_SECS=999999999 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Webhooks: sign async-completion callbacks.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_WEBHOOK_SECRET=$(openssl rand -hex 32) \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Loopback only (rely on a reverse proxy for external access).\ndocker run -d --name flickies -p 127.0.0.1:8000:8000 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest\n```\n\n## Custom Engine Registry\n\nThe image ships `engines.json` baked at `/app/engines.json`. Override without rebuilding by bind-mounting your own or pointing `FLICKIES_ENGINES_FILE` at a different path inside the container:\n\n```bash\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -v $HOME/flickies-data:/data \\\n  -v $PWD/my-engines.json:/app/engines.json:ro \\\n  psyb0t/flickies:latest-cuda\n```\n\nEach engine entry carries `executor`, optional `variant`, `weights_file`, `cuda_only`, `noncommercial`, `vram_gb_min`, and `description`.\n\n## Management\n\n```bash\ndocker logs -f flickies                  # tail logs\ndocker stop flickies                     # stop\ndocker rm flickies                       # remove\ndocker pull psyb0t/flickies:latest       # update (CPU)\ndocker pull psyb0t/flickies:latest-cuda  # update (CUDA)\n```\n\nInspect + control resident engines over the API:\n\n```bash\ncurl -s http://localhost:8000/v1/engines | jq              # list + load state + idle age\ncurl -s -X DELETE http://localhost:8000/v1/engines/latentsync-1.5   # evict a resident engine from VRAM (204)\n```\n\nEngine eviction and staged-file removal are state-changing operations — run them only against a resource the current task created, and only when the user asked. On a shared instance, evicting an engine can interrupt another caller who is mid-request with it.\n\n## OpenClaw / ClawHub Config\n\n```bash\nexport FLICKIES_URL=http://localhost:8000\nexport FLICKIES_AUTH_TOKEN=<token>  # only if the server requires it\n```\n\nOr via `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"flickies\": {\n        \"env\": {\n          \"FLICKIES_URL\": \"http://localhost:8000\",\n          \"FLICKIES_AUTH_TOKEN\": \"<token>\"\n        }\n      }\n    }\n  }\n}\n```\n\nThe skill talks to an instance the operator already runs. It never provisions, installs, or escalates on the caller's machine — it only sends requests to `FLICKIES_URL`.\n\nFile v0.3.16:skill-card.md\n\n## Description:\n\nflickies helps agents and developers operate a self-hosted video REST and MCP API for lipsync, face restoration, ffmpeg video operations, metadata probing, async jobs, and webhook-backed workflows.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[psyb0t](https://clawhub.ai/user/psyb0t)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and external operators use flickies to run video processing jobs against a configured self-hosted service, including lipsync, face restoration, trim, concat, transcode, scale, audio mux or extraction, thumbnail grids, metadata probing, and MCP-driven workflows.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The default setup can expose an unauthenticated network service.\n\nMitigation: Set a strong FLICKIES_AUTH_TOKEN before any non-local deployment and bind the service to loopback or place it behind an authenticated reverse proxy.\n\nRisk: file_url, output_url, and webhook_url can cause the server to fetch from or send data to network locations.\n\nMitigation: Avoid untrusted internal URLs and use only intended external endpoints or storage URLs for fetches, uploads, and webhook callbacks.\n\nRisk: The documented Docker examples use mutable latest tags.\n\nMitigation: Prefer pinned Docker image digests for repeatable deployments.\n\nRisk: File staging and engine management endpoints can remove files or evict loaded engines on a shared instance.\n\nMitigation: Run state-changing operations only for resources created by the current task and avoid evicting engines used by other callers.\n\n## Reference(s):\n\n- [flickies setup](references/setup.md)\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/flickies)\n- [Project homepage](https://github.com/psyb0t/docker-flickies)\n- [Model Context Protocol](https://modelcontextprotocol.io)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Configuration, JSON, API calls]\n\n**Output Format:** [Markdown guidance with shell commands and JSON request bodies]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May produce curl commands, Docker commands, MCP setup commands, endpoint guidance, and staged file paths for a configured flickies service.]\n\n## Skill Version(s):\n\n0.3.16 (source: server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v0.3.15: 5 files, 16825 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (3045b), SKILL.md (23846b), _meta.json (128b)\n\nFile v0.3.15:SKILL.md\n\n---\nname: flickies\ndescription: Self-hosted video REST + MCP API. POST JSON, get a video back. Lipsync (LatentSync 1.5 + Wav2Lip/Wav2Lip-GAN) at /v1/video/lipsync, GFPGAN face restore at /v1/video/restore, pure-ffmpeg ops (trim, concat, transcode incl. gif + fps + codec, scale, mux_audio, extract_audio, thumbnail_grid) under /v1/video/*, and ffprobe metadata at /v1/video/info. file_path (staged) xor file_url in; output_path xor output_url out. Fire-and-forget async jobs (async_job=true → 202 → poll /v1/jobs/{id}) with HMAC-signed webhooks. 11 MCP tools at /v1/mcp. Bearer-token auth. CPU + CUDA images. Use when the user wants to lipsync a face to audio, restore faces in footage, trim/concat/transcode/scale/mux/extract/thumbnail video, probe a video's metadata, or drive any of that from an LLM over MCP.\nhomepage: https://github.com/psyb0t/docker-flickies\nuser-invocable: true\npermissions:\n  - network: outbound HTTP to the configured FLICKIES_URL, plus server-side fetch of file_url, delivery to output_url, and HMAC-signed webhook callbacks\n  - shell: the documented examples invoke local curl / docker\n  - filesystem: manages server-side staged files (upload, fetch, remove) and engine lifecycle (load, evict) on the configured instance\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🎬\", \"primaryEnv\": \"FLICKIES_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# flickies\n\nSelf-hosted video toolkit — lipsync, face restore, and ffmpeg ops in one container. POST a JSON body, get a video back. Every video endpoint takes the same input/output contract; drive it from curl, the generated Go/Python clients, or point a function-calling LLM at the MCP endpoint.\n\nLipsync (`POST /v1/video/lipsync`): drive a face video/image from an audio track. Engines: `latentsync-1.5` (ByteDance, Apache-2.0, commercial-safe default, CUDA-only) and `wav2lip` / `wav2lip-gan` (Rudrabha, fast/low-VRAM, LRS2 non-commercial — refused unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set in the **server** env). `restore_face=true` chains GFPGAN over the result.\n\nFace restore (`POST /v1/video/restore`): GFPGAN v1.4 (`gfpgan`, Apache-2.0) — clean up a Wav2Lip mouth crop or old footage, standalone.\n\nffmpeg ops (pure CPU, no engine): `POST /v1/video/trim`, `/concat`, `/transcode` (mp4/webm/mov/mkv + gif + fps + codec change), `/scale`, `/mux_audio`, `/extract_audio`, `/thumbnail_grid`. Metadata: `POST /v1/video/info` (ffprobe).\n\nExtras: async jobs (`async_job=true` → 202 + `job_id` → poll `GET /v1/jobs/{job_id}`), HMAC-signed webhooks on async completion, server-side file staging, engine load/evict control, an MCP endpoint at `/v1/mcp` with 11 tools, optional bearer-token auth.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Auth is off by default** — `FLICKIES_AUTH_TOKEN` is unset out of the box, so the whole API is open to anyone who can reach the port. Set it for any deployment beyond localhost, pass `Authorization: Bearer <token>` once it's set, and bind to loopback / behind an authenticating proxy. Never expose an unauthenticated instance on a network.\n- **File and engine management** — the staging and engine endpoints include remove and evict operations. Like any state change, only run them against a resource the current task created, only when the user asked, and not against a shared instance others depend on.\n\n## When To Use\n\n- Lipsync a face (video or still image) to a driving audio track — `latentsync-1.5` (commercial-safe, CUDA) or `wav2lip` / `wav2lip-gan` (non-commercial gate).\n- Restore / sharpen faces in a video with GFPGAN, standalone or chained after a Wav2Lip pass (`restore_face=true`).\n- Trim a clip, concatenate several clips, or transcode to another container/codec (incl. animated GIF).\n- Change frame rate, re-encode with a specific codec/CRF/preset, scale dimensions, mux an audio track in, extract the audio out, or generate a thumbnail sprite-sheet.\n- Probe a video for duration, codec, fps, dimensions, bitrate (`/v1/video/info`).\n- Run any long operation fire-and-forget: submit with `async_job=true`, poll `/v1/jobs/{id}`, or receive an HMAC-signed webhook on completion.\n- Drive the whole pipeline from a function-calling LLM (Claude, LibreChat, Cursor) via MCP at `/v1/mcp`.\n\n## When NOT To Use\n\n- Real-time / streaming video output — every endpoint is request/response; long jobs go async + poll, not stream.\n- `latentsync-1.5` on the CPU image — it's CUDA-only (`cuda_only: true`) and the CPU image refuses to load it. GFPGAN is also CUDA-only in practice. Use the `:latest-cuda` image for those.\n- `wav2lip` / `wav2lip-gan` anywhere unless the **server** was started with `FLICKIES_ENABLE_NONCOMMERCIAL=1` — otherwise the request returns 403 `NONCOMMERCIAL_GATE_REFUSED`. This is a server-side env flag; you cannot flip it per-request.\n- Two engines resident at once — one model is resident at a time. A different engine request triggers hot-swap eviction of the current one. If you need two simultaneously, run two containers.\n- `output_url` via the MCP tools — MCP tools only accept `output_path` (they write under FILES_DIR). Use the REST endpoint if you need presigned-PUT `output_url` delivery.\n- Multipart upload on the video endpoints — only `PUT /v1/files/{path}` accepts a raw-body upload. Video endpoints take JSON with `file_path` or `file_url`.\n\n## Setup\n\nThe container should already be running. Set the base URL:\n\n```bash\nexport FLICKIES_URL=http://localhost:8000\n```\n\nIf the server has `FLICKIES_AUTH_TOKEN` set, export it too:\n\n```bash\nexport FLICKIES_AUTH_TOKEN=<your-token>\n# every request below then needs: -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"\n```\n\n**Verify:** `curl $FLICKIES_URL/healthz` returns `{\"status\": \"ok\"}` (unversioned, always auth-exempt). For the richer discovery payload — device, ffmpeg version, available/enabled/loaded engines, non-commercial flag — hit `GET /v1/health`:\n\n```bash\ncurl -s $FLICKIES_URL/v1/health | jq\n# { \"status\": \"ok\", \"version\": \"...\", \"device\": \"cuda\", \"ffmpeg\": \"...\",\n#   \"available_engines\": [...], \"enabled_engines\": [...],\n#   \"loaded_engine\": null, \"noncommercial_enabled\": false }\n```\n\nFor install / configuration / env vars / CPU vs CUDA images / engine weights, see [references/setup.md](references/setup.md).\n\n## Quick Start\n\nThe input/output contract is uniform across every video endpoint:\n\n- **Input** — exactly one of `file_path` (FILES_DIR-relative, staged via `/v1/files`) or `file_url` (any HTTP/HTTPS URL the server fetches).\n- **Output** — exactly one of `output_path` (server writes to FILES_DIR/<path>, response `{path, size, ...}`; download via `GET /v1/files/<path>`) or `output_url` (server PUTs to a presigned/PUT-accepting URL, response `{url, size, ...}`). In async mode both are optional — the server auto-stages to `jobs/{id}.{ext}`.\n\n```bash\n# Probe a video (stage it first, then reference by path).\ncurl -s -X PUT --data-binary @clip.mp4 \\\n  -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" \\\n  \"$FLICKIES_URL/v1/files/uploads/clip.mp4\"\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq\n\n# Trim seconds 5–12, write the result under FILES_DIR, download it.\ncurl -s -X POST \"$FLICKIES_URL/v1/video/trim\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"file_path\": \"uploads/clip.mp4\",\n        \"start_sec\": 5, \"end_sec\": 12, \"precise\": true,\n        \"output_path\": \"out/clip-trimmed.mp4\"\n      }' | jq\ncurl -s \"$FLICKIES_URL/v1/files/out/clip-trimmed.mp4\" --output clip-trimmed.mp4\n\n# Lipsync a face to an audio track (LatentSync, commercial-safe default, CUDA).\ncurl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"output_path\": \"out/lipsynced.mp4\"\n      }' | jq\n```\n\nAdd `-H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"` to every call if the server has a token set. `/healthz` is the only always-exempt route.\n\n## API — `POST /v1/video/lipsync`\n\nDrive a face from an audio track. JSON body.\n\n### Request Fields\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `face_path` / `face_url` | exactly one | — | Driving face — a video OR a still image. `face_path` is FILES_DIR-relative; `face_url` is an HTTP(S) URL the server fetches. |\n| `audio_path` / `audio_url` | exactly one | — | Driving audio (wav/mp3/m4a). `audio_path` FILES_DIR-relative; `audio_url` an HTTP(S) URL. |\n| `engine` | no | `latentsync-1.5` | `latentsync-1.5` (Apache-2.0, CUDA-only, commercial-safe) / `wav2lip` / `wav2lip-gan`. The two `wav2lip*` slugs require `FLICKIES_ENABLE_NONCOMMERCIAL=1` on the server → else 403. |\n| `restore_face` | no | `false` | Chain GFPGAN over the output to clean up the face region. Recommended after `wav2lip*` (fixes the soft 96×96 mouth crop). |\n| `output_path` / `output_url` | one (sync) | — | Sync mode requires exactly one. Async mode: both optional (auto-staged). |\n| `output_format` | no | `mp4` | `mp4` / `webm` / `mov` / `mkv`. |\n| `async_job` | no | `false` | `true` → 202 + `job_id`; poll `/v1/jobs/{id}`. |\n| `webhook_url` | no | — | Async only. HMAC-signed POST on completion — see [Async Job Lifecycle](#async-job-lifecycle). |\n\n### Response\n\n`200` (sync) — one of:\n\n```json\n{ \"path\": \"out/lipsynced.mp4\", \"size\": 4823110 }\n```\n```json\n{ \"url\": \"https://bucket.example.com/out.mp4?...\", \"size\": 4823110 }\n```\n\n`StagedOutputResponse` may also carry `sha256`, `duration_sec`, `width`, `height`. `202` (async) — `{ \"job_id\": \"<uuid>\", \"status\": \"accepted\" }`.\n\n### Error Contract\n\n| Status | `code` | When |\n|---|---|---|\n| 400 | `BAD_REQUEST` | not exactly one of `face_*`, not exactly one of `audio_*`, `output_path`+`output_url` both set, or neither in sync mode |\n| 401 | `UNAUTHORIZED` | `FLICKIES_AUTH_TOKEN` set, missing/wrong bearer |\n| 403 | `NONCOMMERCIAL_GATE_REFUSED` | `wav2lip` / `wav2lip-gan` requested but server has no `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| 404 | `NOT_FOUND` | engine slug or referenced `file_path` missing |\n| 422 | `VALIDATION_FAILED` | Pydantic validation (missing/wrong-typed fields) |\n\nError body is always `{ \"code\": \"UPPER_SNAKE\", \"message\": \"...\", \"details\"?: {...} }`.\n\n## API — `POST /v1/video/restore`\n\nGFPGAN face restoration on a video. Input via `file_path` / `file_url`, output via `output_path` / `output_url` (same contract).\n\n### Request Fields\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source video. |\n| `engine` | no | `gfpgan` | Only `gfpgan`. |\n| `output_path` / `output_url` | one (sync) | — | Standard output contract. |\n| `output_format` / `async_job` / `webhook_url` | no | `mp4` / `false` / — | As above. |\n\n### Response\n\n`200` → `StagedOutputResponse` or `UrlOutputResponse` (as lipsync). `202` → `JobAcceptedResponse`.\n\n### Error Contract\n\n`400` `BAD_REQUEST`, `401` `UNAUTHORIZED`, `422` `VALIDATION_FAILED`. (GFPGAN carries no non-commercial gate.)\n\n## API — ffmpeg ops (`POST /v1/video/{trim,concat,transcode,scale,mux_audio,extract_audio,thumbnail_grid}`)\n\nPure ffmpeg, CPU. Each takes the standard input/output contract plus op-specific fields. All return `StagedOutputResponse` xor `UrlOutputResponse` on `200`. All support `async_job` / `webhook_url` except where noted (the info-shaped ones — `extract_audio`, `thumbnail_grid` — carry no `BaseVideoOutputRequest`; they take `output_path` / `output_url` directly and run sync).\n\n### `trim` — cut `[start_sec, end_sec]`\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `start_sec` | yes | — | ≥ 0. |\n| `end_sec` | yes | — | ≥ 0. |\n| `precise` | no | `false` | `false`: `-c copy`, fast, but `start_sec` snaps to the nearest keyframe (can eat up to one GOP of leading content). `true`: re-encode H.264 + AAC for frame-accurate boundaries (slower, visually transparent). |\n\n### `concat` — join ≥2 videos in order\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `inputs_paths` / `inputs_urls` | exactly one | — | Array, `minItems: 2`. FILES_DIR paths or HTTP(S) URLs. |\n| `precise` | no | `false` | `false`: concat demuxer + `-c copy` — requires identical codec/timebase/SAR across inputs. `true`: re-encode to uniform H.264 + AAC so mixed inputs join cleanly (slower). |\n\n### `transcode` — universal re-encode\n\n`output_format` (from the output contract, `mp4`/`webm`/`mov`/`mkv`) drives the filter graph. Additional:\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `video_codec` | no | — | e.g. `libx264`, `libx265`, `libvpx-vp9`, `libaom-av1`. |\n| `audio_codec` | no | — | e.g. `aac`, `libopus`, `copy`. |\n| `crf` | no | — | 0–51. |\n| `preset` | no | — | e.g. `ultrafast`, `fast`, `medium`, `slow`. |\n| `fps` | no | — | 1–240. Applies to all output formats. |\n| `gif_options` | no | — | Only consulted for GIF output: `{ width?, loop? (0 = infinite), palette_mode? (full/diff/single) }`. |\n\n### `scale` — resize\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `width` | yes | — | ≥ 16. |\n| `height` | yes | — | ≥ 16. |\n| `keep_aspect` | no | `true` | Pad/crop to maintain source aspect. |\n\n### `mux_audio` — replace / merge the audio track\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `video_path_or_url` | yes | — | Video source (path or URL — the server sniffs `http(s)://`). |\n| `audio_path_or_url` | yes | — | Audio source (path or URL). |\n| `replace_existing_audio` | no | `true` | `false` merges instead of replacing. |\n\n### `extract_audio` — pull the audio out\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `audio_format` | no | `wav` | `wav` / `mp3` / `m4a` / `ogg` / `flac`. |\n| `output_path` / `output_url` | — | — | Output target (this op has no async/webhook fields). |\n\n### `thumbnail_grid` — sprite-sheet PNG\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `rows` | yes | — | 1–16. |\n| `cols` | yes | — | 1–16. |\n| `cell_width` | no | `320` | Per-cell width. |\n| `cell_height` | no | `180` | Per-cell height. |\n| `output_path` / `output_url` | — | — | Output target (no async/webhook fields). |\n\n### Error Contract (ffmpeg ops)\n\nSame envelope. `400` `BAD_REQUEST` (bad input xor, constraint violation), `422` `VALIDATION_FAILED` (missing `start_sec`/`end_sec`, `rows`/`cols`, `width`/`height`, arrays under `minItems`), `401` when auth is on.\n\n## API — `POST /v1/video/info`\n\nffprobe metadata. Input via `file_path` / `file_url`; no output fields.\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq\n```\n\n### Response\n\n```json\n{\n  \"duration_sec\": 12.4, \"width\": 1920, \"height\": 1080, \"fps\": 30.0,\n  \"video_codec\": \"h264\", \"audio_codec\": \"aac\", \"bitrate\": 4200000,\n  \"container_format\": \"mov,mp4,m4a,3gp,3g2,mj2\", \"size_bytes\": 6510022\n}\n```\n\n`audio_codec`, `bitrate`, `container_format` are nullable. Errors: `400` `BAD_REQUEST`, `401` `UNAUTHORIZED`.\n\n## Async Job Lifecycle\n\nAny video-producing endpoint (`lipsync`, `restore`, and the ffmpeg ops that carry `async_job`) runs fire-and-forget when you set `async_job: true`. Lipsync/restore are the ones worth doing async — they're the slow ones.\n\n**1. Submit** — POST with `async_job: true`. Output is optional; if you omit both `output_path` and `output_url`, the server auto-stages the result to `jobs/{job_id}.{ext}` under FILES_DIR. Response is `202`:\n\n```bash\nJOB=$(curl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"async_job\": true\n      }' | jq -r .job_id)\n```\n\n**2. Poll** — `GET /v1/jobs/{job_id}`:\n\n```bash\ncurl -s \"$FLICKIES_URL/v1/jobs/$JOB\" | jq\n# { \"job_id\": \"...\", \"status\": \"running\", \"result\": null, \"error\": null }\n```\n\n`status` ∈ `pending` / `running` / `complete` / `failed` / `cancelled`. On `complete`, `result` holds the output payload (`{path, size}` or `{url, size}`). On `failed`, `error` holds `{code, message}`.\n\n**3. Fetch** — when `status: complete`, download the auto-staged result:\n\n```bash\ncurl -s \"$FLICKIES_URL/v1/files/jobs/$JOB.mp4\" --output result.mp4\n```\n\n**Webhook alternative** — pass `webhook_url` on the async submit and the server POSTs the final job state (`{job_id, status, result, error}`) to that URL on completion instead of making you poll:\n\n- HMAC-SHA256 over `timestamp + \".\" + body`, keyed by the server's `FLICKIES_WEBHOOK_SECRET`.\n- Headers: `X-Webhook-Timestamp: <unix-ts>`, `X-Webhook-Signature: t=<ts>,v1=<hex>`.\n- Retried on non-2xx / transport error with exponential backoff (30s, 1m, 5m, 30m, 2h, 12h), then dead-lettered to the server log.\n- Receiver MUST verify the signature and de-dupe on `(timestamp, signature)`.\n\n`GET /v1/jobs/{job_id}` returns `404` `NOT_FOUND` for an unknown id. The queue is in-process — jobs don't survive a container restart.\n\n## MCP Endpoint\n\nflickies mounts a [Model Context Protocol](https://modelcontextprotocol.io) server at `/v1/mcp` (streamable-HTTP JSON-RPC, same FastAPI process, same auth middleware). Point a function-calling LLM at it and it drives the pipeline.\n\nEleven tools mirror the REST surface: `list_engines`, `info`, `lipsync`, `restore`, `transcode`, `trim`, `concat`, `scale`, `mux_audio`, `extract_audio`, `thumbnail_grid`. Argument shapes match the REST bodies, with one difference: **MCP tools accept `output_path` only** (they write under FILES_DIR; `output_url` presigned-PUT delivery is REST-only). MCP tools run synchronously — there's no `async_job` on the MCP side.\n\nWire it into Claude Code:\n\n```bash\nclaude mcp add --transport http flickies $FLICKIES_URL/v1/mcp\n# with auth:\nclaude mcp add --transport http flickies $FLICKIES_URL/v1/mcp \\\n  --header \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"\n```\n\nThe transport requires `Accept: application/json, text/event-stream`. Raw JSON-RPC over HTTP POST for debugging / non-MCP callers:\n\n```bash\n# tools/list\ncurl -s \"$FLICKIES_URL/v1/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\"}'\n\n# tools/call — lipsync\ncurl -s \"$FLICKIES_URL/v1/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"lipsync\",\n      \"arguments\": {\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"output_path\": \"out/lipsynced.mp4\"\n      }\n    }\n  }'\n```\n\nThe canonical mount path carries a trailing slash (`/v1/mcp/`); bare `/v1/mcp` redirects to it. With auth on, every MCP call needs the same `Authorization: Bearer` header.\n\n## Bearer-Token Auth\n\nIf `FLICKIES_AUTH_TOKEN` is set on the server, every route except `/healthz` (and CORS preflight) requires `Authorization: Bearer <token>`. Wrong/missing token returns `401` `UNAUTHORIZED`.\n\n```bash\ncurl -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" $FLICKIES_URL/v1/engines\n```\n\nWith `FLICKIES_AUTH_TOKEN` unset the API/MCP surface is unauthenticated — anyone who can reach it gets full access. Set the token and bind to loopback / behind an authenticating proxy; for untrusted networks add a reverse proxy doing TLS + rate limiting. See [references/setup.md](references/setup.md).\n\n## Typical Workflows\n\n### Lipsync + face restore in one shot\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_path\": \"uploads/portrait.png\",\n        \"audio_path\": \"uploads/line.wav\",\n        \"engine\": \"wav2lip-gan\",\n        \"restore_face\": true,\n        \"output_path\": \"out/talking.mp4\"\n      }' | jq\n# NOTE: wav2lip-gan needs FLICKIES_ENABLE_NONCOMMERCIAL=1 on the server.\n```\n\n### Stage once, run several ops off the same file\n\n```bash\ncurl -s -X PUT --data-binary @raw.mp4 \"$FLICKIES_URL/v1/files/uploads/raw.mp4\"\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/scale\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\":\"uploads/raw.mp4\",\"width\":1280,\"height\":720,\"output_path\":\"out/720p.mp4\"}' | jq\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/transcode\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\":\"uploads/raw.mp4\",\"output_format\":\"gif\",\"fps\":12,\"gif_options\":{\"width\":480}}' \\\n  --fail | jq   # async auto-stages if you omit output_path\n```\n\n### Concatenate several clips (mixed sources → re-encode)\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/concat\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"inputs_urls\": [\"https://ex.com/a.mp4\",\"https://ex.com/b.mp4\"],\n        \"precise\": true,\n        \"output_path\": \"out/joined.mp4\"\n      }' | jq\n```\n\n### Long lipsync, async + poll to completion\n\n```bash\nFLICKIES_URL=$FLICKIES_URL FLICKIES_AUTH_TOKEN=$FLICKIES_AUTH_TOKEN \\\n  bash scripts/flickies.sh lipsync \\\n  '{\"face_url\":\"https://ex.com/f.mp4\",\"audio_url\":\"https://ex.com/v.wav\",\"engine\":\"latentsync-1.5\"}' \\\n  result.mp4\n```\n\nSee [`scripts/flickies.sh`](scripts/flickies.sh) — submits any endpoint async, polls `/v1/jobs/{id}` to a terminal state, and downloads the staged result.\n\n### Free VRAM after a job\n\nFrees the resident engine from VRAM. Rarely needed — engines hot-swap on demand — so only evict one the current task loaded, and remember a shared instance may have another caller using it.\n\n```bash\ncurl -s -X DELETE \"$FLICKIES_URL/v1/engines/latentsync-1.5\"   # evict from VRAM (204)\n```\n\n## Tips\n\n1. **`file_url` over staging** — if the source is already at a URL, pass `file_url` and skip the upload round-trip.\n2. **`precise=false` is the fast path** for trim/concat but snaps to keyframes / needs matching codecs. Flip to `precise=true` when you need frame accuracy or are joining mismatched inputs — it re-encodes.\n3. **One engine resident at a time** — a lipsync request after a restore evicts the restore engine (hot-swap). `restore_face=true` intentionally chains GFPGAN second, evicting the lipsync model to free VRAM.\n4. **`async_job=true` for lipsync/restore** — they're slow. Submit, then poll `/v1/jobs/{id}` or take a webhook. ffmpeg ops are fast enough to run sync.\n5. **Async output is auto-staged** — omit `output_path`/`output_url` on an async submit and fetch from `jobs/{job_id}.{ext}`.\n6. **`wav2lip*` is gated at the server** — 403 `NONCOMMERCIAL_GATE_REFUSED` means the operator hasn't set `FLICKIES_ENABLE_NONCOMMERCIAL=1`. You can't override it per-request; `latentsync-1.5` is the ungated default.\n7. **CPU image can't run `latentsync-1.5` or `gfpgan`** — both are CUDA-only. Wav2Lip-CPU works (slow) if the gate is set. Use `:latest-cuda` for the full engine set.\n8. **`GET /v1/health`** shows `device`, `loaded_engine`, and `noncommercial_enabled` — check it before you get a surprise 403 or a CPU-refusal.\n9. **Idempotency-Key** — pass an `Idempotency-Key` header on a POST for safe retries; the server replays the cached response for a repeat `(key, method, path)`.\n10. **`X-Request-Id`** — send one to correlate logs across hops; the server echoes it back and mints a UUID4 if you don't.\n\nFile v0.3.15:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"flickies\",\n  \"version\": \"0.3.15\",\n  \"publishedAt\": 1785196510456\n}\n\nFile v0.3.15:references/setup.md\n\n# flickies setup\n\n## Requirements\n\n- Docker\n- Optional: NVIDIA GPU + NVIDIA Container Toolkit for the CUDA image (required for `latentsync-1.5` and `gfpgan`; Wav2Lip runs on CPU too, slowly)\n- A bind-mounted `/data` volume for model weights + staged files (weights live in the standard HuggingFace cache layout and are reusable across containers)\n- Tested GPU ceiling: **RTX 3060 12 GB** — fits LatentSync 1.5 (~8 GB) with headroom; the Wav2Lip + GFPGAN chain peaks at ~5 GB. One engine resident at a time.\n\n## Quick Install\n\n### CPU\n\nRuns every ffmpeg op (trim / concat / transcode incl. gif / scale / mux / extract / thumbnail-grid / info) plus Wav2Lip-CPU (~44s for a 3s clip — fine for short clips, and only when the non-commercial gate is set). GFPGAN and LatentSync 1.5 are CUDA-only — the CPU image refuses to load them.\n\n```bash\ndocker run -d --name flickies \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest\n```\n\n### CUDA\n\nRuns every engine at usable speed (LatentSync 1.5, Wav2Lip / Wav2Lip-GAN, GFPGAN) plus all ffmpeg ops. Requires the NVIDIA Container Toolkit on the host.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\nBoth images `EXPOSE 8000` and bind `0.0.0.0:8000` inside the container (the entrypoint forces `FLICKIES_HOST=0.0.0.0`). Control network exposure at `docker run` time with `-p` (see [Ports](#ports)).\n\n**Verify:** `curl http://localhost:8000/healthz` returns `{\"status\": \"ok\"}` once boot is done. `curl http://localhost:8000/v1/health | jq` gives the richer discovery payload (device, ffmpeg version, available/enabled/loaded engines, non-commercial flag).\n\n### Enable the non-commercial engines (Wav2Lip)\n\nWav2Lip / Wav2Lip-GAN are trained on LRS2 (non-commercial). The server refuses to load them unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set. LatentSync 1.5 (Apache-2.0) is the commercial-safe default and needs no gate.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\n## Model Weights\n\nWeights live in the standard HuggingFace cache layout under `/data/hf/hub/models--<org>--<name>/…` (content-addressed blobs + snapshot symlinks), reusable by any HF-aware tool sharing the bind mount.\n\n| engine | HF repo | license | gate |\n|---|---|---|---|\n| `latentsync-1.5` | `ByteDance/LatentSync-1.5` | Apache-2.0 | none (CUDA-only) |\n| `wav2lip` / `wav2lip-gan` | `Nekochu/Wav2Lip` | LRS2 non-commercial | `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| `gfpgan` | `leonelhs/gfpgan` | Apache-2.0 | none (CUDA-only) |\n| S3FD detector | `ByteDance/LatentSync-1.5` (bundled) | — | — |\n\n**Lazy by default** — each engine fetches its repo on first request. To pull at boot before the server accepts requests, set `FLICKIES_ENABLED_ENGINES=wav2lip,gfpgan` (prefetch just those) or `FLICKIES_PREFETCH_ALL=1` (prefetch all; CUDA engines pulled only when the device is CUDA). `FLICKIES_OFFLINE=1` skips auto-download entirely (operator stages the snapshot dir manually).\n\n## Environment Variables\n\nAll server-side (set at `docker run` time). Everything defaults sensibly; the two you'll actually touch are `FLICKIES_AUTH_TOKEN` and `FLICKIES_ENABLE_NONCOMMERCIAL`.\n\n### Core\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_AUTH_TOKEN` | (empty = no auth) | Bearer token required on every route except `/healthz`. Empty/unset = wide open: the API/MCP surface is unauthenticated and anyone who can reach the port gets full access. When set, `Authorization: Bearer <token>` is required on every HTTP request and MCP call. Set it for any deployment beyond localhost, and bind to loopback / behind an authenticating proxy. See [Security & safety](../SKILL.md#security--safety) in the skill doc. |\n| `FLICKIES_ENABLE_NONCOMMERCIAL` | (unset = refuse) | Set to `1` / `true` / `yes` / `on` to allow the `wav2lip` / `wav2lip-gan` engines to load (LRS2 non-commercial training data). Unset → those slugs return 403 `NONCOMMERCIAL_GATE_REFUSED`. |\n| `FLICKIES_DEVICE` | `auto` | `auto` picks `cuda` if available else `cpu`. Also `cpu` / `cuda`. |\n| `FLICKIES_DATA_DIR` | `/data` | Base data dir. Staged files → `<data>/uploads` + FILES_DIR; model snapshots → `<data>/hf` cache; async job outputs → `<data>/jobs/`. Bind-mount to persist across restarts. |\n\n### Engines + prefetch\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_ENGINES_FILE` | `/app/engines.json` | Path to the engine registry JSON. Override to ship a custom subset. |\n| `FLICKIES_ENABLED_ENGINES` | (empty = all, lazy) | Comma-separated engine slug whitelist to prefetch at boot. Empty → download lazily on first request. |\n| `FLICKIES_PREFETCH_ALL` | (unset) | `1` → prefetch every engine in `engines.json` at boot (CUDA engines only when the device is CUDA). |\n| `FLICKIES_OFFLINE` | (unset) | `1` → skip prefetch; weights must be staged manually. |\n| `FLICKIES_IDLE_UNLOAD_SECS` | `600` | Idle seconds before the background sweeper unloads a resident engine from VRAM. Set high to keep a model warm. |\n\n### Webhooks\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_WEBHOOK_SECRET` | (empty) | HMAC-SHA256 signing key for async-completion webhooks. When set, `X-Webhook-Signature: t=<ts>,v1=<hex>` is computed over `timestamp + \".\" + body`; empty → signature header sent empty. Receivers verify with this shared secret. |\n\n### Logging\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_LOG_LEVEL` | `INFO` | `DEBUG` gives reconstruction-grade tracing (every ffmpeg/ffprobe command + result, engine timing, job lifecycle). |\n| `FLICKIES_LOG_FILE` | `<data>/logs/flickies.log` | Rotating JSON log file (in addition to stderr). |\n\n### Bind (usually leave alone)\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_HOST` | `0.0.0.0` (forced by entrypoint) | Bind address inside the container. Control external exposure via `-p` at `docker run` time, not this. |\n| `FLICKIES_PORT` | `8000` | Bind port inside the container. |\n\n### HuggingFace token (private repos only)\n\n`HF_TOKEN` / `HUGGINGFACE_TOKEN` are aliased to each other by the entrypoint. Set one if any engine repo is private. `TORCH_HOME` defaults to `<data>/torch_cache`.\n\n## Ports\n\n| Port | Service |\n| ---- | ------- |\n| 8000 | HTTP REST API + MCP (`/v1/mcp`) on the same port |\n\nThe container binds `0.0.0.0:8000` unconditionally. Use `-p` at `docker run` time:\n\n- `-p 127.0.0.1:8000:8000` — loopback-only on the host.\n- `-p 8000:8000` — all host interfaces.\n- For untrusted networks, combine `FLICKIES_AUTH_TOKEN` with a reverse proxy doing TLS + rate limiting.\n\n## Common Configurations\n\n```bash\n# Bearer auth + non-commercial engines on a CUDA host.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_AUTH_TOKEN=$(openssl rand -hex 32) \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Prefetch weights at boot so the first request doesn't pay the download tax.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_ENABLED_ENGINES=latentsync-1.5,gfpgan \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Keep a model resident forever (disable idle unload).\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_IDLE_UNLOAD_SECS=999999999 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Webhooks: sign async-completion callbacks.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_WEBHOOK_SECRET=$(openssl rand -hex 32) \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Loopback only (rely on a reverse proxy for external access).\ndocker run -d --name flickies -p 127.0.0.1:8000:8000 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest\n```\n\n## Custom Engine Registry\n\nThe image ships `engines.json` baked at `/app/engines.json`. Override without rebuilding by bind-mounting your own or pointing `FLICKIES_ENGINES_FILE` at a different path inside the container:\n\n```bash\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -v $HOME/flickies-data:/data \\\n  -v $PWD/my-engines.json:/app/engines.json:ro \\\n  psyb0t/flickies:latest-cuda\n```\n\nEach engine entry carries `executor`, optional `variant`, `weights_file`, `cuda_only`, `noncommercial`, `vram_gb_min`, and `description`.\n\n## Management\n\n```bash\ndocker logs -f flickies                  # tail logs\ndocker stop flickies                     # stop\ndocker rm flickies                       # remove\ndocker pull psyb0t/flickies:latest       # update (CPU)\ndocker pull psyb0t/flickies:latest-cuda  # update (CUDA)\n```\n\nInspect + control resident engines over the API:\n\n```bash\ncurl -s http://localhost:8000/v1/engines | jq              # list + load state + idle age\ncurl -s -X DELETE http://localhost:8000/v1/engines/latentsync-1.5   # evict a resident engine from VRAM (204)\n```\n\nEngine eviction and staged-file removal are state-changing operations — run them only against a resource the current task created, and only when the user asked. On a shared instance, evicting an engine can interrupt another caller who is mid-request with it.\n\n## OpenClaw / ClawHub Config\n\n```bash\nexport FLICKIES_URL=http://localhost:8000\nexport FLICKIES_AUTH_TOKEN=<token>  # only if the server requires it\n```\n\nOr via `~/.openclaw/openclaw.json`:\n\n```json\n{\n  \"skills\": {\n    \"entries\": {\n      \"flickies\": {\n        \"env\": {\n          \"FLICKIES_URL\": \"http://localhost:8000\",\n          \"FLICKIES_AUTH_TOKEN\": \"<token>\"\n        }\n      }\n    }\n  }\n}\n```\n\nThe skill talks to an instance the operator already runs. It never provisions, installs, or escalates on the caller's machine — it only sends requests to `FLICKIES_URL`.\n\nFile v0.3.15:skill-card.md\n\n## Description: <br>\nflickies helps agents use a self-hosted video REST and MCP service for lipsync, face restoration, ffmpeg video operations, metadata probing, async jobs, and file delivery. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[psyb0t](https://clawhub.ai/user/psyb0t) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and agent users use flickies to submit video processing requests to a trusted Flickies server from REST, MCP, curl, or the bundled shell helper. It is suited for lipsync, face restore, trim, concat, transcode, scale, mux or extract audio, thumbnail grids, metadata probing, and async job workflows. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: An unauthenticated or broadly exposed Flickies server can give network users access to the video API and MCP surface. <br>\nMitigation: Use the skill only with a Flickies server you control or trust, set FLICKIES_AUTH_TOKEN beyond localhost, and prefer loopback binding or an authenticated proxy. <br>\nRisk: file_url, output_url, and webhook_url can cause the server to fetch from or send data to external or internal network locations. <br>\nMitigation: Avoid untrusted internal-network URLs and confirm destinations before using URL fetches, presigned output delivery, or webhooks. <br>\nRisk: Staged-file removal and engine eviction are state-changing operations that can affect shared server instances. <br>\nMitigation: Only remove staged files or evict engines when the user asked and the resource belongs to the current task, especially on shared instances. <br>\nRisk: Wav2Lip and Wav2Lip-GAN are gated as non-commercial engines and may be inappropriate for normal commercial workflows. <br>\nMitigation: Use the commercial-safe LatentSync default unless the server operator intentionally enabled FLICKIES_ENABLE_NONCOMMERCIAL for an allowed use. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/psyb0t/skills/flickies) <br>\n- [flickies setup](references/setup.md) <br>\n- [docker-flickies homepage](https://github.com/psyb0t/docker-flickies) <br>\n- [Model Context Protocol](https://modelcontextprotocol.io) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Guidance, Shell commands, Configuration, API calls, Code] <br>\n**Output Format:** [Markdown guidance with bash commands, JSON request bodies, REST and MCP examples, and configuration values] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May direct a trusted Flickies server to create, fetch, upload, download, or remove staged video files depending on the requested operation.] <br>\n\n## Skill Version(s): <br>\n0.3.15 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v0.3.14: 5 files, 16662 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (2620b), SKILL.md (23846b), _meta.json (128b)\n\nFile v0.3.14:SKILL.md\n\n---\nname: flickies\ndescription: Self-hosted video REST + MCP API. POST JSON, get a video back. Lipsync (LatentSync 1.5 + Wav2Lip/Wav2Lip-GAN) at /v1/video/lipsync, GFPGAN face restore at /v1/video/restore, pure-ffmpeg ops (trim, concat, transcode incl. gif + fps + codec, scale, mux_audio, extract_audio, thumbnail_grid) under /v1/video/*, and ffprobe metadata at /v1/video/info. file_path (staged) xor file_url in; output_path xor output_url out. Fire-and-forget async jobs (async_job=true → 202 → poll /v1/jobs/{id}) with HMAC-signed webhooks. 11 MCP tools at /v1/mcp. Bearer-token auth. CPU + CUDA images. Use when the user wants to lipsync a face to audio, restore faces in footage, trim/concat/transcode/scale/mux/extract/thumbnail video, probe a video's metadata, or drive any of that from an LLM over MCP.\nhomepage: https://github.com/psyb0t/docker-flickies\nuser-invocable: true\npermissions:\n  - network: outbound HTTP to the configured FLICKIES_URL, plus server-side fetch of file_url, delivery to output_url, and HMAC-signed webhook callbacks\n  - shell: the documented examples invoke local curl / docker\n  - filesystem: manages server-side staged files (upload, fetch, remove) and engine lifecycle (load, evict) on the configured instance\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🎬\", \"primaryEnv\": \"FLICKIES_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# flickies\n\nSelf-hosted video toolkit — lipsync, face restore, and ffmpeg ops in one container. POST a JSON body, get a video back. Every video endpoint takes the same input/output contract; drive it from curl, the generated Go/Python clients, or point a function-calling LLM at the MCP endpoint.\n\nLipsync (`POST /v1/video/lipsync`): drive a face video/image from an audio track. Engines: `latentsync-1.5` (ByteDance, Apache-2.0, commercial-safe default, CUDA-only) and `wav2lip` / `wav2lip-gan` (Rudrabha, fast/low-VRAM, LRS2 non-commercial — refused unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set in the **server** env). `restore_face=true` chains GFPGAN over the result.\n\nFace restore (`POST /v1/video/restore`): GFPGAN v1.4 (`gfpgan`, Apache-2.0) — clean up a Wav2Lip mouth crop or old footage, standalone.\n\nffmpeg ops (pure CPU, no engine): `POST /v1/video/trim`, `/concat`, `/transcode` (mp4/webm/mov/mkv + gif + fps + codec change), `/scale`, `/mux_audio`, `/extract_audio`, `/thumbnail_grid`. Metadata: `POST /v1/video/info` (ffprobe).\n\nExtras: async jobs (`async_job=true` → 202 + `job_id` → poll `GET /v1/jobs/{job_id}`), HMAC-signed webhooks on async completion, server-side file staging, engine load/evict control, an MCP endpoint at `/v1/mcp` with 11 tools, optional bearer-token auth.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Auth is off by default** — `FLICKIES_AUTH_TOKEN` is unset out of the box, so the whole API is open to anyone who can reach the port. Set it for any deployment beyond localhost, pass `Authorization: Bearer <token>` once it's set, and bind to loopback / behind an authenticating proxy. Never expose an unauthenticated instance on a network.\n- **File and engine management** — the staging and engine endpoints include remove and evict operations. Like any state change, only run them against a resource the current task created, only when the user asked, and not against a shared instance others depend on.\n\n## When To Use\n\n- Lipsync a face (video or still image) to a driving audio track — `latentsync-1.5` (commercial-safe, CUDA) or `wav2lip` / `wav2lip-gan` (non-commercial gate).\n- Restore / sharpen faces in a video with GFPGAN, standalone or chained after a Wav2Lip pass (`restore_face=true`).\n- Trim a clip, concatenate several clips, or transcode to another container/codec (incl. animated GIF).\n- Change frame rate, re-encode with a specific codec/CRF/preset, scale dimensions, mux an audio track in, extract the audio out, or generate a thumbnail sprite-sheet.\n- Probe a video for duration, codec, fps, dimensions, bitrate (`/v1/video/info`).\n- Run any long operation fire-and-forget: submit with `async_job=true`, poll `/v1/jobs/{id}`, or receive an HMAC-signed webhook on completion.\n- Drive the whole pipeline from a function-calling LLM (Claude, LibreChat, Cursor) via MCP at `/v1/mcp`.\n\n## When NOT To Use\n\n- Real-time / streaming video output — every endpoint is request/response; long jobs go async + poll, not stream.\n- `latentsync-1.5` on the CPU image — it's CUDA-only (`cuda_only: true`) and the CPU image refuses to load it. GFPGAN is also CUDA-only in practice. Use the `:latest-cuda` image for those.\n- `wav2lip` / `wav2lip-gan` anywhere unless the **server** was started with `FLICKIES_ENABLE_NONCOMMERCIAL=1` — otherwise the request returns 403 `NONCOMMERCIAL_GATE_REFUSED`. This is a server-side env flag; you cannot flip it per-request.\n- Two engines resident at once — one model is resident at a time. A different engine request triggers hot-swap eviction of the current one. If you need two simultaneously, run two containers.\n- `output_url` via the MCP tools — MCP tools only accept `output_path` (they write under FILES_DIR). Use the REST endpoint if you need presigned-PUT `output_url` delivery.\n- Multipart upload on the video endpoints — only `PUT /v1/files/{path}` accepts a raw-body upload. Video endpoints take JSON with `file_path` or `file_url`.\n\n## Setup\n\nThe container should already be running. Set the base URL:\n\n```bash\nexport FLICKIES_URL=http://localhost:8000\n```\n\nIf the server has `FLICKIES_AUTH_TOKEN` set, export it too:\n\n```bash\nexport FLICKIES_AUTH_TOKEN=<your-token>\n# every request below then needs: -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"\n```\n\n**Verify:** `curl $FLICKIES_URL/healthz` returns `{\"status\": \"ok\"}` (unversioned, always auth-exempt). For the richer discovery payload — device, ffmpeg version, available/enabled/loaded engines, non-commercial flag — hit `GET /v1/health`:\n\n```bash\ncurl -s $FLICKIES_URL/v1/health | jq\n# { \"status\": \"ok\", \"version\": \"...\", \"device\": \"cuda\", \"ffmpeg\": \"...\",\n#   \"available_engines\": [...], \"enabled_engines\": [...],\n#   \"loaded_engine\": null, \"noncommercial_enabled\": false }\n```\n\nFor install / configuration / env vars / CPU vs CUDA images / engine weights, see [references/setup.md](references/setup.md).\n\n## Quick Start\n\nThe input/output contract is uniform across every video endpoint:\n\n- **Input** — exactly one of `file_path` (FILES_DIR-relative, staged via `/v1/files`) or `file_url` (any HTTP/HTTPS URL the server fetches).\n- **Output** — exactly one of `output_path` (server writes to FILES_DIR/<path>, response `{path, size, ...}`; download via `GET /v1/files/<path>`) or `output_url` (server PUTs to a presigned/PUT-accepting URL, response `{url, size, ...}`). In async mode both are optional — the server auto-stages to `jobs/{id}.{ext}`.\n\n```bash\n# Probe a video (stage it first, then reference by path).\ncurl -s -X PUT --data-binary @clip.mp4 \\\n  -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" \\\n  \"$FLICKIES_URL/v1/files/uploads/clip.mp4\"\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq\n\n# Trim seconds 5–12, write the result under FILES_DIR, download it.\ncurl -s -X POST \"$FLICKIES_URL/v1/video/trim\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"file_path\": \"uploads/clip.mp4\",\n        \"start_sec\": 5, \"end_sec\": 12, \"precise\": true,\n        \"output_path\": \"out/clip-trimmed.mp4\"\n      }' | jq\ncurl -s \"$FLICKIES_URL/v1/files/out/clip-trimmed.mp4\" --output clip-trimmed.mp4\n\n# Lipsync a face to an audio track (LatentSync, commercial-safe default, CUDA).\ncurl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"output_path\": \"out/lipsynced.mp4\"\n      }' | jq\n```\n\nAdd `-H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"` to every call if the server has a token set. `/healthz` is the only always-exempt route.\n\n## API — `POST /v1/video/lipsync`\n\nDrive a face from an audio track. JSON body.\n\n### Request Fields\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `face_path` / `face_url` | exactly one | — | Driving face — a video OR a still image. `face_path` is FILES_DIR-relative; `face_url` is an HTTP(S) URL the server fetches. |\n| `audio_path` / `audio_url` | exactly one | — | Driving audio (wav/mp3/m4a). `audio_path` FILES_DIR-relative; `audio_url` an HTTP(S) URL. |\n| `engine` | no | `latentsync-1.5` | `latentsync-1.5` (Apache-2.0, CUDA-only, commercial-safe) / `wav2lip` / `wav2lip-gan`. The two `wav2lip*` slugs require `FLICKIES_ENABLE_NONCOMMERCIAL=1` on the server → else 403. |\n| `restore_face` | no | `false` | Chain GFPGAN over the output to clean up the face region. Recommended after `wav2lip*` (fixes the soft 96×96 mouth crop). |\n| `output_path` / `output_url` | one (sync) | — | Sync mode requires exactly one. Async mode: both optional (auto-staged). |\n| `output_format` | no | `mp4` | `mp4` / `webm` / `mov` / `mkv`. |\n| `async_job` | no | `false` | `true` → 202 + `job_id`; poll `/v1/jobs/{id}`. |\n| `webhook_url` | no | — | Async only. HMAC-signed POST on completion — see [Async Job Lifecycle](#async-job-lifecycle). |\n\n### Response\n\n`200` (sync) — one of:\n\n```json\n{ \"path\": \"out/lipsynced.mp4\", \"size\": 4823110 }\n```\n```json\n{ \"url\": \"https://bucket.example.com/out.mp4?...\", \"size\": 4823110 }\n```\n\n`StagedOutputResponse` may also carry `sha256`, `duration_sec`, `width`, `height`. `202` (async) — `{ \"job_id\": \"<uuid>\", \"status\": \"accepted\" }`.\n\n### Error Contract\n\n| Status | `code` | When |\n|---|---|---|\n| 400 | `BAD_REQUEST` | not exactly one of `face_*`, not exactly one of `audio_*`, `output_path`+`output_url` both set, or neither in sync mode |\n| 401 | `UNAUTHORIZED` | `FLICKIES_AUTH_TOKEN` set, missing/wrong bearer |\n| 403 | `NONCOMMERCIAL_GATE_REFUSED` | `wav2lip` / `wav2lip-gan` requested but server has no `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| 404 | `NOT_FOUND` | engine slug or referenced `file_path` missing |\n| 422 | `VALIDATION_FAILED` | Pydantic validation (missing/wrong-typed fields) |\n\nError body is always `{ \"code\": \"UPPER_SNAKE\", \"message\": \"...\", \"details\"?: {...} }`.\n\n## API — `POST /v1/video/restore`\n\nGFPGAN face restoration on a video. Input via `file_path` / `file_url`, output via `output_path` / `output_url` (same contract).\n\n### Request Fields\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source video. |\n| `engine` | no | `gfpgan` | Only `gfpgan`. |\n| `output_path` / `output_url` | one (sync) | — | Standard output contract. |\n| `output_format` / `async_job` / `webhook_url` | no | `mp4` / `false` / — | As above. |\n\n### Response\n\n`200` → `StagedOutputResponse` or `UrlOutputResponse` (as lipsync). `202` → `JobAcceptedResponse`.\n\n### Error Contract\n\n`400` `BAD_REQUEST`, `401` `UNAUTHORIZED`, `422` `VALIDATION_FAILED`. (GFPGAN carries no non-commercial gate.)\n\n## API — ffmpeg ops (`POST /v1/video/{trim,concat,transcode,scale,mux_audio,extract_audio,thumbnail_grid}`)\n\nPure ffmpeg, CPU. Each takes the standard input/output contract plus op-specific fields. All return `StagedOutputResponse` xor `UrlOutputResponse` on `200`. All support `async_job` / `webhook_url` except where noted (the info-shaped ones — `extract_audio`, `thumbnail_grid` — carry no `BaseVideoOutputRequest`; they take `output_path` / `output_url` directly and run sync).\n\n### `trim` — cut `[start_sec, end_sec]`\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `start_sec` | yes | — | ≥ 0. |\n| `end_sec` | yes | — | ≥ 0. |\n| `precise` | no | `false` | `false`: `-c copy`, fast, but `start_sec` snaps to the nearest keyframe (can eat up to one GOP of leading content). `true`: re-encode H.264 + AAC for frame-accurate boundaries (slower, visually transparent). |\n\n### `concat` — join ≥2 videos in order\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `inputs_paths` / `inputs_urls` | exactly one | — | Array, `minItems: 2`. FILES_DIR paths or HTTP(S) URLs. |\n| `precise` | no | `false` | `false`: concat demuxer + `-c copy` — requires identical codec/timebase/SAR across inputs. `true`: re-encode to uniform H.264 + AAC so mixed inputs join cleanly (slower). |\n\n### `transcode` — universal re-encode\n\n`output_format` (from the output contract, `mp4`/`webm`/`mov`/`mkv`) drives the filter graph. Additional:\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `video_codec` | no | — | e.g. `libx264`, `libx265`, `libvpx-vp9`, `libaom-av1`. |\n| `audio_codec` | no | — | e.g. `aac`, `libopus`, `copy`. |\n| `crf` | no | — | 0–51. |\n| `preset` | no | — | e.g. `ultrafast`, `fast`, `medium`, `slow`. |\n| `fps` | no | — | 1–240. Applies to all output formats. |\n| `gif_options` | no | — | Only consulted for GIF output: `{ width?, loop? (0 = infinite), palette_mode? (full/diff/single) }`. |\n\n### `scale` — resize\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `width` | yes | — | ≥ 16. |\n| `height` | yes | — | ≥ 16. |\n| `keep_aspect` | no | `true` | Pad/crop to maintain source aspect. |\n\n### `mux_audio` — replace / merge the audio track\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `video_path_or_url` | yes | — | Video source (path or URL — the server sniffs `http(s)://`). |\n| `audio_path_or_url` | yes | — | Audio source (path or URL). |\n| `replace_existing_audio` | no | `true` | `false` merges instead of replacing. |\n\n### `extract_audio` — pull the audio out\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `audio_format` | no | `wav` | `wav` / `mp3` / `m4a` / `ogg` / `flac`. |\n| `output_path` / `output_url` | — | — | Output target (this op has no async/webhook fields). |\n\n### `thumbnail_grid` — sprite-sheet PNG\n\n| Field | Required | Default | Notes |\n|---|---|---|---|\n| `file_path` / `file_url` | exactly one | — | Source. |\n| `rows` | yes | — | 1–16. |\n| `cols` | yes | — | 1–16. |\n| `cell_width` | no | `320` | Per-cell width. |\n| `cell_height` | no | `180` | Per-cell height. |\n| `output_path` / `output_url` | — | — | Output target (no async/webhook fields). |\n\n### Error Contract (ffmpeg ops)\n\nSame envelope. `400` `BAD_REQUEST` (bad input xor, constraint violation), `422` `VALIDATION_FAILED` (missing `start_sec`/`end_sec`, `rows`/`cols`, `width`/`height`, arrays under `minItems`), `401` when auth is on.\n\n## API — `POST /v1/video/info`\n\nffprobe metadata. Input via `file_path` / `file_url`; no output fields.\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq\n```\n\n### Response\n\n```json\n{\n  \"duration_sec\": 12.4, \"width\": 1920, \"height\": 1080, \"fps\": 30.0,\n  \"video_codec\": \"h264\", \"audio_codec\": \"aac\", \"bitrate\": 4200000,\n  \"container_format\": \"mov,mp4,m4a,3gp,3g2,mj2\", \"size_bytes\": 6510022\n}\n```\n\n`audio_codec`, `bitrate`, `container_format` are nullable. Errors: `400` `BAD_REQUEST`, `401` `UNAUTHORIZED`.\n\n## Async Job Lifecycle\n\nAny video-producing endpoint (`lipsync`, `restore`, and the ffmpeg ops that carry `async_job`) runs fire-and-forget when you set `async_job: true`. Lipsync/restore are the ones worth doing async — they're the slow ones.\n\n**1. Submit** — POST with `async_job: true`. Output is optional; if you omit both `output_path` and `output_url`, the server auto-stages the result to `jobs/{job_id}.{ext}` under FILES_DIR. Response is `202`:\n\n```bash\nJOB=$(curl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"async_job\": true\n      }' | jq -r .job_id)\n```\n\n**2. Poll** — `GET /v1/jobs/{job_id}`:\n\n```bash\ncurl -s \"$FLICKIES_URL/v1/jobs/$JOB\" | jq\n# { \"job_id\": \"...\", \"status\": \"running\", \"result\": null, \"error\": null }\n```\n\n`status` ∈ `pending` / `running` / `complete` / `failed` / `cancelled`. On `complete`, `result` holds the output payload (`{path, size}` or `{url, size}`). On `failed`, `error` holds `{code, message}`.\n\n**3. Fetch** — when `status: complete`, download the auto-staged result:\n\n```bash\ncurl -s \"$FLICKIES_URL/v1/files/jobs/$JOB.mp4\" --output result.mp4\n```\n\n**Webhook alternative** — pass `webhook_url` on the async submit and the server POSTs the final job state (`{job_id, status, result, error}`) to that URL on completion instead of making you poll:\n\n- HMAC-SHA256 over `timestamp + \".\" + body`, keyed by the server's `FLICKIES_WEBHOOK_SECRET`.\n- Headers: `X-Webhook-Timestamp: <unix-ts>`, `X-Webhook-Signature: t=<ts>,v1=<hex>`.\n- Retried on non-2xx / transport error with exponential backoff (30s, 1m, 5m, 30m, 2h, 12h), then dead-lettered to the server log.\n- Receiver MUST verify the signature and de-dupe on `(timestamp, signature)`.\n\n`GET /v1/jobs/{job_id}` returns `404` `NOT_FOUND` for an unknown id. The queue is in-process — jobs don't survive a container restart.\n\n## MCP Endpoint\n\nflickies mounts a [Model Context Protocol](https://modelcontextprotocol.io) server at `/v1/mcp` (streamable-HTTP JSON-RPC, same FastAPI process, same auth middleware). Point a function-calling LLM at it and it drives the pipeline.\n\nEleven tools mirror the REST surface: `list_engines`, `info`, `lipsync`, `restore`, `transcode`, `trim`, `concat`, `scale`, `mux_audio`, `extract_audio`, `thumbnail_grid`. Argument shapes match the REST bodies, with one difference: **MCP tools accept `output_path` only** (they write under FILES_DIR; `output_url` presigned-PUT delivery is REST-only). MCP tools run synchronously — there's no `async_job` on the MCP side.\n\nWire it into Claude Code:\n\n```bash\nclaude mcp add --transport http flickies $FLICKIES_URL/v1/mcp\n# with auth:\nclaude mcp add --transport http flickies $FLICKIES_URL/v1/mcp \\\n  --header \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\"\n```\n\nThe transport requires `Accept: application/json, text/event-stream`. Raw JSON-RPC over HTTP POST for debugging / non-MCP callers:\n\n```bash\n# tools/list\ncurl -s \"$FLICKIES_URL/v1/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"tools/list\"}'\n\n# tools/call — lipsync\ncurl -s \"$FLICKIES_URL/v1/mcp/\" \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\": \"2.0\", \"id\": 2, \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"lipsync\",\n      \"arguments\": {\n        \"face_url\": \"https://example.com/face.mp4\",\n        \"audio_url\": \"https://example.com/voice.wav\",\n        \"engine\": \"latentsync-1.5\",\n        \"output_path\": \"out/lipsynced.mp4\"\n      }\n    }\n  }'\n```\n\nThe canonical mount path carries a trailing slash (`/v1/mcp/`); bare `/v1/mcp` redirects to it. With auth on, every MCP call needs the same `Authorization: Bearer` header.\n\n## Bearer-Token Auth\n\nIf `FLICKIES_AUTH_TOKEN` is set on the server, every route except `/healthz` (and CORS preflight) requires `Authorization: Bearer <token>`. Wrong/missing token returns `401` `UNAUTHORIZED`.\n\n```bash\ncurl -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" $FLICKIES_URL/v1/engines\n```\n\nWith `FLICKIES_AUTH_TOKEN` unset the API/MCP surface is unauthenticated — anyone who can reach it gets full access. Set the token and bind to loopback / behind an authenticating proxy; for untrusted networks add a reverse proxy doing TLS + rate limiting. See [references/setup.md](references/setup.md).\n\n## Typical Workflows\n\n### Lipsync + face restore in one shot\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/lipsync\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"face_path\": \"uploads/portrait.png\",\n        \"audio_path\": \"uploads/line.wav\",\n        \"engine\": \"wav2lip-gan\",\n        \"restore_face\": true,\n        \"output_path\": \"out/talking.mp4\"\n      }' | jq\n# NOTE: wav2lip-gan needs FLICKIES_ENABLE_NONCOMMERCIAL=1 on the server.\n```\n\n### Stage once, run several ops off the same file\n\n```bash\ncurl -s -X PUT --data-binary @raw.mp4 \"$FLICKIES_URL/v1/files/uploads/raw.mp4\"\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/scale\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\":\"uploads/raw.mp4\",\"width\":1280,\"height\":720,\"output_path\":\"out/720p.mp4\"}' | jq\n\ncurl -s -X POST \"$FLICKIES_URL/v1/video/transcode\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\":\"uploads/raw.mp4\",\"output_format\":\"gif\",\"fps\":12,\"gif_options\":{\"width\":480}}' \\\n  --fail | jq   # async auto-stages if you omit output_path\n```\n\n### Concatenate several clips (mixed sources → re-encode)\n\n```bash\ncurl -s -X POST \"$FLICKIES_URL/v1/video/concat\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n        \"inputs_urls\": [\"https://ex.com/a.mp4\",\"https://ex.com/b.mp4\"],\n        \"precise\": true,\n        \"output_path\": \"out/joined.mp4\"\n      }' | jq\n```\n\n### Long lipsync, async + poll to completion\n\n```bash\nFLICKIES_URL=$FLICKIES_URL FLICKIES_AUTH_TOKEN=$FLICKIES_AUTH_TOKEN \\\n  bash scripts/flickies.sh lipsync \\\n  '{\"face_url\":\"https://ex.com/f.mp4\",\"audio_url\":\"https://ex.com/v.wav\",\"engine\":\"latentsync-1.5\"}' \\\n  result.mp4\n```\n\nSee [`scripts/flickies.sh`](scripts/flickies.sh) — submits any endpoint async, polls `/v1/jobs/{id}` to a terminal state, and downloads the staged result.\n\n### Free VRAM after a job\n\nFrees the resident engine from VRAM. Rarely needed — engines hot-swap on demand — so only evict one the current task loaded, and remember a shared instance may have another caller using it.\n\n```bash\ncurl -s -X DELETE \"$FLICKIES_URL/v1/engines/latentsync-1.5\"   # evict from VRAM (204)\n```\n\n## Tips\n\n1. **`file_url` over staging** — if the source is already at a URL, pass `file_url` and skip the upload round-trip.\n2. **`precise=false` is the fast path** for trim/concat but snaps to keyframes / needs matching codecs. Flip to `precise=true` when you need frame accuracy or are joining mismatched inputs — it re-encodes.\n3. **One engine resident at a time** — a lipsync request after a restore evicts the restore engine (hot-swap). `restore_face=true` intentionally chains GFPGAN second, evicting the lipsync model to free VRAM.\n4. **`async_job=true` for lipsync/restore** — they're slow. Submit, then poll `/v1/jobs/{id}` or take a webhook. ffmpeg ops are fast enough to run sync.\n5. **Async output is auto-staged** — omit `output_path`/`output_url` on an async submit and fetch from `jobs/{job_id}.{ext}`.\n6. **`wav2lip*` is gated at the server** — 403 `NONCOMMERCIAL_GATE_REFUSED` means the operator hasn't set `FLICKIES_ENABLE_NONCOMMERCIAL=1`. You can't override it per-request; `latentsync-1.5` is the ungated default.\n7. **CPU image can't run `latentsync-1.5` or `gfpgan`** — both are CUDA-only. Wav2Lip-CPU works (slow) if the gate is set. Use `:latest-cuda` for the full engine set.\n8. **`GET /v1/health`** shows `device`, `loaded_engine`, and `noncommercial_enabled` — check it before you get a surprise 403 or a CPU-refusal.\n9. **Idempotency-Key** — pass an `Idempotency-Key` header on a POST for safe retries; the server replays the cached response for a repeat `(key, method, path)`.\n10. **`X-Request-Id`** — send one to correlate logs across hops; the server echoes it back and mints a UUID4 if you don't.\n\nFile v0.3.14:_meta.json\n\n{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"flickies\",\n  \"version\": \"0.3.14\",\n  \"publishedAt\": 1785195220264\n}\n\nFile v0.3.14:references/setup.md\n\n# flickies setup\n\n## Requirements\n\n- Docker\n- Optional: NVIDIA GPU + NVIDIA Container Toolkit for the CUDA image (required for `latentsync-1.5` and `gfpgan`; Wav2Lip runs on CPU too, slowly)\n- A bind-mounted `/data` volume for model weights + staged files (weights live in the standard HuggingFace cache layout and are reusable across containers)\n- Tested GPU ceiling: **RTX 3060 12 GB** — fits LatentSync 1.5 (~8 GB) with headroom; the Wav2Lip + GFPGAN chain peaks at ~5 GB. One engine resident at a time.\n\n## Quick Install\n\n### CPU\n\nRuns every ffmpeg op (trim / concat / transcode incl. gif / scale / mux / extract / thumbnail-grid / info) plus Wav2Lip-CPU (~44s for a 3s clip — fine for short clips, and only when the non-commercial gate is set). GFPGAN and LatentSync 1.5 are CUDA-only — the CPU image refuses to load them.\n\n```bash\ndocker run -d --name flickies \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest\n```\n\n### CUDA\n\nRuns every engine at usable speed (LatentSync 1.5, Wav2Lip / Wav2Lip-GAN, GFPGAN) plus all ffmpeg ops. Requires the NVIDIA Container Toolkit on the host.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\nBoth images `EXPOSE 8000` and bind `0.0.0.0:8000` inside the container (the entrypoint forces `FLICKIES_HOST=0.0.0.0`). Control network exposure at `docker run` time with `-p` (see [Ports](#ports)).\n\n**Verify:** `curl http://localhost:8000/healthz` returns `{\"status\": \"ok\"}` once boot is done. `curl http://localhost:8000/v1/health | jq` gives the richer discovery payload (device, ffmpeg version, available/enabled/loaded engines, non-commercial flag).\n\n### Enable the non-commercial engines (Wav2Lip)\n\nWav2Lip / Wav2Lip-GAN are trained on LRS2 (non-commercial). The server refuses to load them unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set. LatentSync 1.5 (Apache-2.0) is the commercial-safe default and needs no gate.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\n## Model Weights\n\nWeights live in the standard HuggingFace cache layout under `/data/hf/hub/models--<org>--<name>/…` (content-addressed blobs + snapshot symlinks), reusable by any HF-aware tool sharing the bind mount.\n\n| engine | HF repo | license | gate |\n|---|---|---|---|\n| `latentsync-1.5` | `ByteDance/LatentSync-1.5` | Apache-2.0 | none (CUDA-only) |\n| `wav2lip` / `wav2lip-gan` | `Nekochu/Wav2Lip` | LRS2 non-commercial | `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| `gfpgan` | `leonelhs/gfpgan` | Apache-2.0 | none (CUDA-only) |\n| S3FD detector | `ByteDance/LatentSync-1.5` (bundled) | — | — |\n\n**Lazy by default** — each engine fetches its repo on first request. To pull at boot before the server accepts requests, set `FLICKIES_ENABLED_ENGINES=wav2lip,gfpgan` (prefetch just those) or `FLICKIES_PREFETCH_ALL=1` (prefetch all; CUDA engines pulled only when the device is CUDA). `FLICKIES_OFFLINE=1` skips auto-download entirely (operator stages the snapshot dir manually).\n\n## Environment Variables\n\nAll server-side (set at `docker run` time). Everything defaults sensibly; the two you'll actually touch are `FLICKIES_AUTH_TOKEN` and `FLICKIES_ENABLE_NONCOMMERCIAL`.\n\n### Core\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_AUTH_TOKEN` | (empty = no auth) | Bearer token required on every route except `/healthz`. Empty/unset = wide open: the API/MCP surface is unauthenticated and anyone who can reach the port gets full access. When set, `Authorization: Bearer <token>` is required on every HTTP request and MCP call. Set it for any deployment beyond localhost, and bind to loopback / behind an authenticating proxy. See [Security & safety](../SKILL.md#security--safety) in the skill doc. |\n| `FLICKIES_ENABLE_NONCOMMERCIAL` | (unset = refuse) | Set to `1` / `true` / `yes` / `on` to allow the `wav2lip` / `wav2lip-gan` engines to load (LRS2 non-commercial training data). Unset → those slugs return 403 `NONCOMMERCIAL_GATE_REFUSED`. |\n| `FLICKIES_DEVICE` | `auto` | `auto` picks `cuda` if available else `cpu`. Also `cpu` / `cuda`. |\n| `FLICKIES_DATA_DIR` | `/data` | Base data dir. Staged files → `<data>/uploads` + FILES_DIR; model snapshots → `<data>/hf` cache; async job outputs → `<data>/jobs/`. Bind-mount to persist across restarts. |\n\n### Engines + prefetch\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_ENGINES_FILE` | `/app/engines.json` | Path to the engine registry JSON. Override to ship a custom subset. |\n| `FLICKIES_ENABLED_ENGINES` | (empty = all, lazy) | Comma-separated engine slug whitelist to prefetch at boot. Empty → download lazily on first request. |\n| `FLICKIES_PREFETCH_ALL` | (unset) | `1` → prefetch every engine in `engines.json` at boot (CUDA engines only when the device is CUDA). |\n| `FLICKIES_OFFLINE` | (unset) | `1` → skip prefetch; weights must be staged manually. |\n| `FLICKIES_IDLE_UNLOAD_SECS` | `600` | Idle seconds before the background sweeper unloads a resident engine from VRAM. Set high to keep a model warm. |\n\n### Webhooks\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_WEBHOOK_SECRET` | (empty) | HMAC-SHA256 signing key for async-completion webhooks. When set, `X-Webhook-Signature: t=<ts>,v1=<hex>` is computed over `timestamp + \".\" + body`; empty → signature header sent empty. Receivers verify with this shared secret. |\n\n### Logging\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_LOG_LEVEL` | `INFO` | `DEBUG` gives reconstruction-grade tracing (every ffmpeg/ffprobe command + result, engine timing, job lifecycle). |\n| `FLICKIES_LOG_FILE` | `<data>/logs/flickies.log` | Rotating JSON log file (in addition to stderr). |\n\n### Bind (usually leave alone)\n\n| Var | Default | What it does |\n|---|---|---|\n| `FLICKIES_HOST` | `0.0.0.0` (forced by entrypoint) | Bind address inside the container. Control external exposure via `-p` at `docker run` time, not this. |\n| `FLICKIES_PORT` | `8000` | Bind port inside the container. |\n\n### HuggingFace token (private repos only)\n\n`HF_TOKEN` / `HUGGINGFACE_TOKEN` are aliased to each other by the entrypoint. Set one if any engine repo is private. `TORCH_HOME` defaults to `<data>/torch_cache`.\n\n## Ports\n\n| Port | Service |\n| ---- | ------- |\n| 8000 | HTTP REST API + MCP (`/v1/mcp`) on the same port |\n\nThe container binds `0.0.0.0:8000` unconditionally. Use `-p` at `docker run` time:\n\n- `-p 127.0.0.1:8000:8000` — loopback-only on the host.\n- `-p 8000:8000` — all host interfaces.\n- For untrusted networks, combine `FLICKIES_AUTH_TOKEN` with a reverse proxy doing TLS + rate limiting.\n\n## Common Configurations\n\n```bash\n# Bearer auth + non-commercial engines on a CUDA host.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_AUTH_TOKEN=$(openssl rand -hex 32) \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Prefetch weights at boot so the first request doesn't pay the download tax.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_ENABLED_ENGINES=latentsync-1.5,gfpgan \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Keep a model resident forever (disable idle unload).\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_IDLE_UNLOAD_SECS=999999999 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Webhooks: sign async-completion callbacks.\ndocker run -d --name flickies --gpus all -p 8000:8000 \\\n  -e FLICKIES_WEBHOOK_SECRET=$(openssl rand -hex 32) \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest-cuda\n\n# Loopback only (rely on a reverse proxy for external access).\ndocker run -d --name flickies -p 127.0.0.1:8000:8000 \\\n  -v $HOME/flickies-data:/data \\\n  psyb0t/flickies:latest\n```\n\n## Custom Engine\n\nArchive v0.3.13: 5 files, 16734 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (2878b), SKILL.md (23846b), _meta.json (128b)\n\nArchive v0.3.12: 5 files, 16788 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (2933b), SKILL.md (23846b), _meta.json (128b)\n\nArchive v0.3.11: 5 files, 16672 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (2629b), SKILL.md (23846b), _meta.json (128b)\n\nArchive v0.3.10: 5 files, 16757 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (2823b), SKILL.md (23846b), _meta.json (128b)\n\nArchive v0.3.9: 5 files, 16639 bytes\n\nFiles: references/setup.md (9829b), scripts/flickies.sh (5107b), skill-card.md (2618b), SKILL.md (23846b), _meta.json (127b)\n\nArchive v0.3.8: 5 files, 17789 bytes\n\nFiles: references/setup.md (10938b), scripts/flickies.sh (5107b), skill-card.md (2340b), SKILL.md (26917b), _meta.json (127b)","readmeExcerpt":"Skill: flickies Owner: psyb0t Summary: Self-hosted video REST + MCP API. POST JSON, get a video back. Lipsync (LatentSync 1.5 + Wav2Lip/Wav2Lip-GAN) at /v1/video/lipsync, GFPGAN face restore at /v1/video/restore, pure-ffmpeg ops (trim, concat, transcode incl. gif + fps + codec, scale, mux_audio, extract_audio, thumbnail_grid) under /v1/video/*, and ffprobe metadata at /v1/video/info. file_path (staged) xor file_url i","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"export FLICKIES_URL=http://localhost:8000"},{"language":"bash","snippet":"export FLICKIES_AUTH_TOKEN=<your-token>\n# every request below then needs: -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\""},{"language":"bash","snippet":"curl -s $FLICKIES_URL/v1/health | jq"},{"language":"bash","snippet":"curl -s $FLICKIES_URL/v1/health | jq\n# { \"status\": \"ok\", \"version\": \"...\", \"device\": \"cuda\", \"ffmpeg\": \"...\",\n#   \"available_engines\": [...], \"enabled_engines\": [...],\n#   \"loaded_engine\": null, \"noncommercial_enabled\": false }"},{"language":"bash","snippet":"curl -s -X PUT --data-binary @clip.mp4 \\\n  -H \"Authorization: Bearer $FLICKIES_AUTH_TOKEN\" \\"},{"language":"bash","snippet":"curl -s -X POST \"$FLICKIES_URL/v1/video/info\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"file_path\": \"uploads/clip.mp4\"}' | jq"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: flickies\ndescription: Self-hosted video REST + MCP API. POST JSON, get a video back. Lipsync (LatentSync 1.5 + Wav2Lip/Wav2Lip-GAN) at /v1/video/lipsync, GFPGAN face restore at /v1/video/restore, pure-ffmpeg ops (trim, concat, transcode incl. gif + fps + codec, scale, mux_audio, extract_audio, thumbnail_grid) under /v1/video/*, and ffprobe metadata at /v1/video/info. file_path (staged) xor file_url in; output_path xor output_url out. Fire-and-forget async jobs (async_job=true → 202 → poll /v1/jobs/{id}) with HMAC-signed webhooks. 11 MCP tools at /v1/mcp. Bearer-token auth. CPU + CUDA images. Use when the user wants to lipsync a face to audio, restore faces in footage, trim/concat/transcode/scale/mux/extract/thumbnail video, probe a video's metadata, or drive any of that from an LLM over MCP.\nhomepage: https://github.com/psyb0t/docker-flickies\nuser-invocable: true\npermissions:\n  - network: outbound HTTP to the configured FLICKIES_URL, plus server-side fetch of file_url, delivery to output_url, and HMAC-signed webhook callbacks\n  - shell: the documented examples invoke local curl / docker\n  - filesystem: manages server-side staged files (upload, fetch, remove) and engine lifecycle (load, evict) on the configured instance\nmetadata:\n  { \"openclaw\": { \"emoji\": \"🎬\", \"primaryEnv\": \"FLICKIES_URL\", \"requires\": { \"bins\": [\"docker\", \"curl\"] } } }\n---\n\n# flickies\n\nSelf-hosted video toolkit — lipsync, face restore, and ffmpeg ops in one container. POST a JSON body, get a video back. Every video endpoint takes the same input/output contract; drive it from curl, the generated Go/Python clients, or point a function-calling LLM at the MCP endpoint.\n\nLipsync (`POST /v1/video/lipsync`): drive a face video/image from an audio track. Engines: `latentsync-1.5` (ByteDance, Apache-2.0, commercial-safe default, CUDA-only) and `wav2lip` / `wav2lip-gan` (Rudrabha, fast/low-VRAM, LRS2 non-commercial — refused unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set in the **server** env). `restore_face=true` chains GFPGAN over the result.\n\nFace restore (`POST /v1/video/restore`): GFPGAN v1.4 (`gfpgan`, Apache-2.0) — clean up a Wav2Lip mouth crop or old footage, standalone.\n\nffmpeg ops (pure CPU, no engine): `POST /v1/video/trim`, `/concat`, `/transcode` (mp4/webm/mov/mkv + gif + fps + codec change), `/scale`, `/mux_audio`, `/extract_audio`, `/thumbnail_grid`. Metadata: `POST /v1/video/info` (ffprobe).\n\nExtras: async jobs (`async_job=true` → 202 + `job_id` → poll `GET /v1/jobs/{job_id}`), HMAC-signed webhooks on async completion, server-side file staging, engine load/evict control, an MCP endpoint at `/v1/mcp` with 11 tools, optional bearer-token auth.\n\nFor installation, configuration, and container setup, see [references/setup.md](references/setup.md).\n\n## Security & safety\n\n- **Auth is off by default** — `FLICKIES_AUTH_TOKEN` is unset out of the box, so the whole API is open to anyone who can reach the port. Set it for any deployment beyond localhost, pass `Authorization"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn79dhvmpjng4rp2jjk8k0v5xx80ccbk\",\n  \"slug\": \"flickies\",\n  \"version\": \"0.3.17\",\n  \"publishedAt\": 1791644871846\n}"},{"path":"references/setup.md","content":"# flickies setup\n\n## Requirements\n\n- Docker\n- Optional: NVIDIA GPU + NVIDIA Container Toolkit for the CUDA image (required for `latentsync-1.5` and `gfpgan`; Wav2Lip runs on CPU too, slowly)\n- A bind-mounted `/data` volume for model weights + staged files (weights live in the standard HuggingFace cache layout and are reusable across containers)\n- Tested GPU ceiling: **RTX 3060 12 GB** — fits LatentSync 1.5 (~8 GB) with headroom; the Wav2Lip + GFPGAN chain peaks at ~5 GB. One engine resident at a time.\n\n## Quick Install\n\n### CPU\n\nRuns every ffmpeg op (trim / concat / transcode incl. gif / scale / mux / extract / thumbnail-grid / info) plus Wav2Lip-CPU (~44s for a 3s clip — fine for short clips, and only when the non-commercial gate is set). GFPGAN and LatentSync 1.5 are CUDA-only — the CPU image refuses to load them.\n\n```bash\ndocker run -d --name flickies \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest\n```\n\n### CUDA\n\nRuns every engine at usable speed (LatentSync 1.5, Wav2Lip / Wav2Lip-GAN, GFPGAN) plus all ffmpeg ops. Requires the NVIDIA Container Toolkit on the host.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\nBoth images `EXPOSE 8000` and bind `0.0.0.0:8000` inside the container (the entrypoint forces `FLICKIES_HOST=0.0.0.0`). Control network exposure at `docker run` time with `-p` (see [Ports](#ports)).\n\n**Verify:** `curl http://localhost:8000/healthz` returns `{\"status\": \"ok\"}` once boot is done. `curl http://localhost:8000/v1/health | jq` gives the richer discovery payload (device, ffmpeg version, available/enabled/loaded engines, non-commercial flag).\n\n### Enable the non-commercial engines (Wav2Lip)\n\nWav2Lip / Wav2Lip-GAN are trained on LRS2 (non-commercial). The server refuses to load them unless `FLICKIES_ENABLE_NONCOMMERCIAL=1` is set. LatentSync 1.5 (Apache-2.0) is the commercial-safe default and needs no gate.\n\n```bash\ndocker run -d --name flickies \\\n  --gpus all \\\n  -e FLICKIES_ENABLE_NONCOMMERCIAL=1 \\\n  -v $HOME/flickies-data:/data \\\n  -p 8000:8000 \\\n  psyb0t/flickies:latest-cuda\n```\n\n## Model Weights\n\nWeights live in the standard HuggingFace cache layout under `/data/hf/hub/models--<org>--<name>/…` (content-addressed blobs + snapshot symlinks), reusable by any HF-aware tool sharing the bind mount.\n\n| engine | HF repo | license | gate |\n|---|---|---|---|\n| `latentsync-1.5` | `ByteDance/LatentSync-1.5` | Apache-2.0 | none (CUDA-only) |\n| `wav2lip` / `wav2lip-gan` | `Nekochu/Wav2Lip` | LRS2 non-commercial | `FLICKIES_ENABLE_NONCOMMERCIAL=1` |\n| `gfpgan` | `leonelhs/gfpgan` | Apache-2.0 | none (CUDA-only) |\n| S3FD detector | `ByteDance/LatentSync-1.5` (bundled) | — | — |\n\n**Lazy by default** — each engine fetches its repo on first request. To pull at boot before the server accepts requests, set `FLICKIES_ENABLED_ENGINES=wav2lip,gfpgan` (prefetch just those) or `FLICKIES_PREFETCH_ALL=1` (prefetch all; CUDA engines "},{"path":"skill-card.md","content":"## Description:\n\nHelps agents use a self-hosted video API to lip-sync footage, restore faces, edit and transcode media, and inspect video metadata through REST or MCP.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[psyb0t](https://clawhub.ai/user/psyb0t)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and media creators use this skill to direct a self-hosted video service to lip-sync faces, restore footage, perform common editing operations, and retrieve metadata or processed files.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Default container setup may expose an unauthenticated video-processing API to the network.\n\nMitigation: Run on a trusted host, bind the published port to loopback, set FLICKIES_AUTH_TOKEN, and do not expose the API directly to untrusted networks.\n\nRisk: Uploads, remote media inputs, output destinations, and webhook URLs can share media or results beyond the local host.\n\nMitigation: Use only approved local media and explicitly authorized URLs for remote inputs, outputs, and callbacks.\n\nRisk: Unpinned container images can change between deployments.\n\nMitigation: Pin Docker images to a version or digest when possible.\n\n## Reference(s):\n\n- [flickies setup guide](references/setup.md)\n- [ClawHub skill listing](https://clawhub.ai/psyb0t/skills/flickies)\n- [flickies project homepage](https://github.com/psyb0t/docker-flickies)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Configuration instructions, API calls]\n\n**Output Format:** [Markdown with JSON and shell examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May guide retrieval of processed video, audio, thumbnails, and metadata from the configured service.]\n\n## Skill Version(s):\n\n0.3.17 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1705,"uniquenessScore":41,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T09:22:31.530Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T09:22:31.530Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T11:24:00.975Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}