{
  "schema": "leumas.docs.page/1",
  "id": "how-to:video",
  "slug": "how-to/video",
  "kind": "pages",
  "bucket": "how-to",
  "title": "Video (the merged shorts + clips engine)",
  "name": "Video",
  "eyebrow": "the merged shorts + clips engine",
  "chip": null,
  "summary": "The one Video capability, merged from the legacy leumas-clips (long video → transcribe → scored 9:16 clips) and leumas-clips-shorts (text → narrated vertical short) tools. Stateless primitives live...",
  "keywords": [
    "video",
    "leumas-clips",
    "transcribe",
    "offline-first",
    "narrated",
    "leumas video",
    "how to video",
    "uploads"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# Video (the merged shorts + clips engine)\n\nThe one Video capability, merged from the legacy `leumas-clips` (long video → transcribe → scored\n9:16 clips) and `leumas-clips-shorts` (text → narrated vertical short) tools. Stateless primitives\nlive in the `video` adapter (`shared/engines/adapters/media/video`); the stateful pipeline — clip\njobs, uploads, the library, the shorts gallery — is the `/api/video` router. Both share one ffmpeg\nrender layer and an offline-first `say` TTS default.\n\n## How agents call it\n\n| Surface | How |\n|---|---|\n| CLI (headless, no server/auth) | `node ops/tools/run-tool/run-tool.mjs video <op> '<jsonArgs>'` |\n| MCP client / chatbot / Leviathan | adapter `video` → tool/functioncall (`generateShort`, `scoreTranscript`, `capabilities`) |\n| MCP (read the stateful backend) | `leumas-clips-server` server — inspects videos/clips/health of a running `/api/video` |\n| Automation rule / grid cell | the three adapter ops as chainable cells |\n| HTTP | `/api/video/*` (JSON + multipart, `requireAuth`) |\n| Studio | **Video** domain (`/admin/video`) — Shorts + Clips tabs |\n| Code | `generateShort`, `scoreTranscript` via the `video` adapter; router = `createVideoRouter` |\n\n## The two headline HTTP calls\n\n**Text → narrated vertical short.** `POST /api/video/shorts/generate`:\n\n```json\n{ \"text\": \"Three facts about octopuses.\", \"mode\": \"full-gameplay\",\n  \"voice\": { \"provider\": \"say\" }, \"title\": \"Octopus facts\" }\n```\n\n`mode` ∈ `full-gameplay` (default) `| split-layout | full-media | media-gameplay-inset | auto`;\n`voice.provider` ∈ `say` (default, offline OS voice) `| mock | openai | elevenlabs`. Gameplay/media\nbackgrounds are optional — omit them and the engine synthesizes demo placeholders so a bare `text`\ngenerates. Enums are validated at the router (clean `400`) before the SDK runs. Returns\n`201 { short, manifest }`; the playable file is at `short.video_url` (`/api/video/assets/shorts/...`).\nThen `GET /api/video/shorts` (owner-scoped gallery), `GET|DELETE /api/video/shorts/:id`.\n\n**Long video → scored clips.** `POST /api/video/videos/import-url` `{ url, title?, description?,\nautoProcess?, captureDurationSeconds? }` ingests a remote/YouTube source and (unless\n`autoProcess:false`) enqueues a job that transcribes → scores highlights → renders 9:16 clips with\nburned captions + thumbnails. Or `POST /api/video/videos/upload` (multipart `file`). Track it with\n`GET /api/video/videos`, `GET /api/video/videos/:id` (video + clips + latest job),\n`GET /api/video/videos/:id/status`, `GET /api/video/videos/:id/transcript`,\n`POST /api/video/videos/:id/process`; fetch results via `GET /api/video/clips/:id` and\n`GET /api/video/clips/:id/download`, `GET /api/video/jobs/:id`. Public pre-gate probes:\n`GET /api/video/health`, `GET /api/video/capabilities`.\n\n## Run it from the CLI\n\n```sh\nnode ops/tools/run-tool/run-tool.mjs video generateShort '{\"text\":\"...\",\"mode\":\"full-gameplay\",\"provider\":\"say\"}'\nnode ops/tools/run-tool/run-tool.mjs video scoreTranscript '{\"segments\":[{\"start\":0,\"end\":6,\"text\":\"...\"}],\"maxClips\":4}'\n```\n\n`generateShort` runs a real ffmpeg render; `scoreTranscript` (rank clip windows) and `capabilities`\nare pure. No API server or auth — the same registry the API loads runs headless.\n\n## The repointed leumas-clips-server MCP\n\n`shared/engines/mcp/mcp-servers/leumas-clips-server` is the legacy YouTube-to-shorts MCP, repointed\nat the merged backend: `getCapabilities`, `health`, `listVideos`, `getVideo {videoId}`,\n`listClips {videoId}` — each takes an optional `baseUrl` (default `LEUMAS_CLIPS_BASE_URL` →\n`http://127.0.0.1:3022`). It is a read/inspect client over a running `/api/video`; generation goes\nthrough the `video` adapter's `generateShort` (auto-exposed as its own MCP tool) or the HTTP route.\n\n## Gating\n\nLogin-only and **free** for every member out of the box. The `video` membership seam is present in\n`createVideoRouter` (swap the no-op auth for `requireMembership('video')` to gate the whole engine\nbehind a plan) but unapplied. PassNode meters the heavy render endpoints via the\n`feature:video.render` guard — a no-op until a passnode rule doc exists, so tenant metering switches\non with no code change. Rendered assets are served auth-gated and allowlisted to\n`clips|shorts|thumbnails` (the data root — db index, raw uploads, transcripts — is never served).\n\n## Studio Video domain\n\n`/admin/video` (nav id `video`, icon [video], \"Free for every logged-in member\") wraps four tabs in\n`VideoShell`: **HTML → MP4** + **Renders** (HyperFrames), **Shorts** (calls\n`/api/video/shorts/generate` + gallery), **Clips** (calls `/api/video/videos/import-url`, upload, and\nthe library). Surfaces: `products/leumas-studio/src/admin/video/surfaces.jsx`.\n",
  "source": {
    "path": "shared/services/knowledge/build-knowledge/video.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 4915,
    "hash": "dd609081d102e4d7c88d1686bbe23a3bf48fa19e"
  },
  "urls": {
    "html": "/p/how-to/video",
    "json": "/docs/how-to/video.json",
    "md": "/docs/how-to/video.md"
  },
  "links": {
    "composes": [],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  }
}
