# Video (the merged shorts + clips engine)

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...


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 in the `video` adapter (`shared/engines/adapters/media/video`); the stateful pipeline — clip
jobs, uploads, the library, the shorts gallery — is the `/api/video` router. Both share one ffmpeg
render layer and an offline-first `say` TTS default.

## How agents call it

| Surface | How |
|---|---|
| CLI (headless, no server/auth) | `node ops/tools/run-tool/run-tool.mjs video <op> '<jsonArgs>'` |
| MCP client / chatbot / Leviathan | adapter `video` → tool/functioncall (`generateShort`, `scoreTranscript`, `capabilities`) |
| MCP (read the stateful backend) | `leumas-clips-server` server — inspects videos/clips/health of a running `/api/video` |
| Automation rule / grid cell | the three adapter ops as chainable cells |
| HTTP | `/api/video/*` (JSON + multipart, `requireAuth`) |
| Studio | **Video** domain (`/admin/video`) — Shorts + Clips tabs |
| Code | `generateShort`, `scoreTranscript` via the `video` adapter; router = `createVideoRouter` |

## The two headline HTTP calls

**Text → narrated vertical short.** `POST /api/video/shorts/generate`:

```json
{ "text": "Three facts about octopuses.", "mode": "full-gameplay",
  "voice": { "provider": "say" }, "title": "Octopus facts" }
```

`mode` ∈ `full-gameplay` (default) `| split-layout | full-media | media-gameplay-inset | auto`;
`voice.provider` ∈ `say` (default, offline OS voice) `| mock | openai | elevenlabs`. Gameplay/media
backgrounds are optional — omit them and the engine synthesizes demo placeholders so a bare `text`
generates. Enums are validated at the router (clean `400`) before the SDK runs. Returns
`201 { short, manifest }`; the playable file is at `short.video_url` (`/api/video/assets/shorts/...`).
Then `GET /api/video/shorts` (owner-scoped gallery), `GET|DELETE /api/video/shorts/:id`.

**Long video → scored clips.** `POST /api/video/videos/import-url` `{ url, title?, description?,
autoProcess?, captureDurationSeconds? }` ingests a remote/YouTube source and (unless
`autoProcess:false`) enqueues a job that transcribes → scores highlights → renders 9:16 clips with
burned captions + thumbnails. Or `POST /api/video/videos/upload` (multipart `file`). Track it with
`GET /api/video/videos`, `GET /api/video/videos/:id` (video + clips + latest job),
`GET /api/video/videos/:id/status`, `GET /api/video/videos/:id/transcript`,
`POST /api/video/videos/:id/process`; fetch results via `GET /api/video/clips/:id` and
`GET /api/video/clips/:id/download`, `GET /api/video/jobs/:id`. Public pre-gate probes:
`GET /api/video/health`, `GET /api/video/capabilities`.

## Run it from the CLI

```sh
node ops/tools/run-tool/run-tool.mjs video generateShort '{"text":"...","mode":"full-gameplay","provider":"say"}'
node ops/tools/run-tool/run-tool.mjs video scoreTranscript '{"segments":[{"start":0,"end":6,"text":"..."}],"maxClips":4}'
```

`generateShort` runs a real ffmpeg render; `scoreTranscript` (rank clip windows) and `capabilities`
are pure. No API server or auth — the same registry the API loads runs headless.

## The repointed leumas-clips-server MCP

`shared/engines/mcp/mcp-servers/leumas-clips-server` is the legacy YouTube-to-shorts MCP, repointed
at the merged backend: `getCapabilities`, `health`, `listVideos`, `getVideo {videoId}`,
`listClips {videoId}` — each takes an optional `baseUrl` (default `LEUMAS_CLIPS_BASE_URL` →
`http://127.0.0.1:3022`). It is a read/inspect client over a running `/api/video`; generation goes
through the `video` adapter's `generateShort` (auto-exposed as its own MCP tool) or the HTTP route.

## Gating

Login-only and **free** for every member out of the box. The `video` membership seam is present in
`createVideoRouter` (swap the no-op auth for `requireMembership('video')` to gate the whole engine
behind a plan) but unapplied. PassNode meters the heavy render endpoints via the
`feature:video.render` guard — a no-op until a passnode rule doc exists, so tenant metering switches
on with no code change. Rendered assets are served auth-gated and allowlisted to
`clips|shorts|thumbnails` (the data root — db index, raw uploads, transcripts — is never served).

## Studio Video domain

`/admin/video` (nav id `video`, icon [video], "Free for every logged-in member") wraps four tabs in
`VideoShell`: **HTML → MP4** + **Renders** (HyperFrames), **Shorts** (calls
`/api/video/shorts/generate` + gallery), **Clips** (calls `/api/video/videos/import-url`, upload, and
the library). Surfaces: `products/leumas-studio/src/admin/video/surfaces.jsx`.


---
Source: shared/services/knowledge/build-knowledge/video.md
Canonical: https://docs.leumas.tech/p/how-to/video
