---
name: render-video
description: |
  Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.

  Use this skill as soon as the user mentions any of these intents (after assets are already prepared):
  - Render the video, composite the video, export the video
  - Turn the prepared assets into the final clip
  - Render with Remotion

  Prerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.

  ⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering.
triggers:
  - Render the video, composite the video, export the video
  - Turn the prepared assets into the final clip
  - Render with Remotion
  - Agent has a job_id from prepare_video_assets and needs the final video
---

# Render Video Skill

Loads a persisted RenderPlan from the database (or a local file fallback), compiles the timeline, drives the **Remotion** engine, and emits the final playable video file.

> **Prerequisite**: assets must already be generated via `prepare_video_assets`. That skill returns a `job_id` integer; pass it here.

> ⚠️ **Stop-and-confirm gate (mandatory, do not skip).**
> **Never** call this skill until the user has explicitly confirmed the assets shown after
> `prepare_video_assets`. If those assets were prepared in the current turn and the user has not
> replied since, **stop and ask** — do not render. This step spends render credits and uploads the
> result; it is the one action in the pipeline that cannot be walked back cheaply.
>
> "Confirmed" means the user said so **in a message of their own** (e.g. "confirm", "go ahead",
> "looks good", "continue"). An end-to-end instruction from earlier in the conversation
> ("make me a video about X") is **not** confirmation — it authorizes the pipeline, not the
> skipping of its review steps. Stopping to ask **is** the correct completion of this step.

## Render pipeline (this skill's part only)

```
job_id  ─► RenderPlan loaded from DB
              │
              ▼
       [4] Timeline Compiler ─► Remotion props + frame numbers
              │
              ▼
       [5] Renderer ─► Remotion ─► MP4
              │
              ▼ (auto-upload by default; --no-upload skips)
       [6] Uploader ─► Alibaba OSS ─► playback URL
```

Phases 1–3 (DSL validation, template binding, asset resolution) are owned by **`prepare_video_assets`**. This skill picks up at phase 4.

## Authentication & environment

Rendering supports two modes:

- **local** (opt-in via `REMOTION_RENDER_MODE=local`): runs Remotion CLI locally; requires Node.js 18+, a headless Chrome, and the renderer directory present on the host.
- **remote** (default when `REMOTION_RENDER_MODE` is unset or empty): calls the standalone **remotion-renderer** service (ab-render) — best for environments without Node.js, long videos, or to avoid consuming local resources. Only an explicit `REMOTION_RENDER_MODE=local` selects local rendering.

| Env var | Description | Default |
|---------|-------------|---------|
| `PRIV_TOKEN` | Tianyan token (needed to load the RenderPlan from the DB and to upload). | (none) |
| `MM_BACKEND_API_URL` | Backend API root URL (RenderPlan / VOD lookups). | `https://api.remixmate.ai/api` |
| `REMOTION_RENDER_API_URL` | Remote renderer base URL (ab-render). Falls back to `RENDER_API_URL` (same value across skills). | `https://api-render.remixmate.ai` |
| `REMOTION_RENDER_MODE` | Default render mode (`remote` / `local`); CLI `--renderer` overrides it. | `remote` |
| `REMOTION_OUTPUT_DIR` | Render output directory. | `./output/` |
| `REMOTION_CONCURRENCY` | Local Remotion render concurrency. | `2` |
| `ASSET_CACHE_DIR` | Asset cache directory (read-only for this skill — assets are already resolved). | `./.asset-cache/` |
| `REMOTION_REMOTE_POLL_TIMEOUT` | Remote-render polling timeout (seconds). | `1800` |
| `REMOTION_REMOTE_POLL_INTERVAL` | Remote-render polling interval (seconds). | `5` |

## Canonical usage (database job mode)

> ⚠️ **`job_id` must be a positive integer.** It is the value N from `prepare_video_assets`'s stdout line `📦 render job jobId: N`. Never pass `0`, a placeholder string, or descriptive text.

```bash
python3 <SkillDir>/scripts/render_video.py --job-id <jobId>
```

`--save-job` is on by default. After the render, the Manifest is written back into the database under the same `jobId`.

### Fallback: file mode (single-user / local debug)

When the DB is unavailable, the script accepts a pre-generated RenderPlan file:

```bash
python3 <SkillDir>/scripts/render_video.py --render-plan <path>.render-plan.json
```

> ⚠️ **Do NOT blindly point `--render-plan` at a shared `output/render-plan.json`.**
> Under the default token-present path, `prepare_video_assets` / `render_video`
> persist the plan to the **database (jobId)** only — they do **not** write
> `output/render-plan.json`. Any file sitting there is likely a leftover from a
> previous, unrelated run (the `output/` dir is gitignored scratch), and rendering
> it silently produces the wrong video.
>
> To get a RenderPlan file on disk, generate it explicitly:
> ```bash
> python3 <SkillDir>/scripts/render_video.py --dsl video.dsl.json --template-id <id> \
>   --resolve-only --save-render-plan --render-plan-output video.render-plan.json
> ```
> Or skip the file entirely and render in one shot:
> `render_video.py --dsl video.dsl.json --template-id <id>`.
>
> On load, the script now prints a `↳ plan: templateId=… composition=… duration=… title=…`
> summary, and warns if the plan file is older than a `--dsl` you also passed —
> check that line matches what you intend before the render proceeds.

### Render and upload to Alibaba Cloud OSS (default behavior)

```bash
python3 <SkillDir>/scripts/render_video.py --job-id 42 --upload-title "My video"
```

After rendering, the script auto-uploads by default; `render-manifest.json` then contains an
`upload.fileUrl` field, and `upload.playbackUrl` (transcoded) once VOD processing finishes.

**Do not retype the video URL into your reply.** On success the script prints a structured
`__render_video_asset__` line; the host reads the authoritative URL from there and renders a
player for the user. Tell the user the video is ready — the player appears on its own.

Reproducing a 32-char opaque URL from memory is unreliable: on 2026-08-30 a single character
was dropped (`…c1c20102` → `…c1c2012`), so the user got a 404 while the file sat fine on the
CDN. The URL in your prose is redundant with the structured asset and is the only copy that
can be wrong — the UI now renders it as plain, non-clickable text for exactly this reason.

### Render but skip the upload

```bash
python3 <SkillDir>/scripts/render_video.py --job-id 42 --no-upload
```

### Upload an existing video standalone

```bash
python3 <SkillDir>/scripts/upload_video.py --file output/video.mp4 --title "My video"
```

`upload_video.py` is a standalone upload tool that uploads any local video file to Alibaba Cloud OSS and returns the usable URL. Use cases:

- Manual upload after a `--no-upload` render.
- Uploading video files not produced by Remotion.

| Flag | Description | Default |
|------|-------------|---------|
| `--file` | Local video file path. | required |
| `--title` | Upload title. | filename |
| `--priv-token` | Override the token. | env var |

### Specify the output path

```bash
python3 <SkillDir>/scripts/render_video.py --job-id 42 --output final-video.mp4
```

## Common CLI flags

| Flag | Description | Default |
|------|-------------|---------|
| `--job-id` | RenderPlan job id loaded from the database (primary input). | — |
| `--render-plan` | Existing RenderPlan file path (single-user / local debug fallback). | — |
| `--save-job` | Persist Manifest to the database; pass `--no-save-job` to disable. | on (auto-degrades when token missing) |
| `-o` / `--output` | Output video file path. | `output/video.mp4` |
| `--asset-cache-dir` | Asset cache directory. | `.asset-cache/` |
| `--private-token` | Tianyan token. | env var |
| `--upload` | Auto-upload to Alibaba Cloud OSS after rendering (local mode only). | **on** |
| `--no-upload` | Skip the upload (local mode only). | off |
| `--upload-title` | Upload title (defaults to filename). | — |
| `--renderer` | Render mode: `remote` (default) / `local`. | `remote` |
| `--remote-poll-timeout` | Remote-render polling timeout (seconds). | `1800` |
| `--remote-poll-interval` | Remote-render polling interval (seconds). | `5` |

The script also accepts `--resolve-only`, `--dsl`, `--dsl-json`, `--template-id`, `--binding`, `--binding-json`, `--stub-image-url`, `--stub-video-url` for shell-level convenience and back-compat, but **the MCP tool surface of this skill exposes only the rendering parameters**. To resolve / regenerate assets, call `prepare_video_assets` instead.

## Render outputs

After rendering, the output directory contains:

```
output/
├── video.mp4              # Final video
├── render-plan.json       # RenderPlan snapshot
├── render-manifest.json   # Render metadata (duration, resolution, frame count; includes upload.fileUrl by default)
└── remotion-props.json    # The full props passed to Remotion (debug)
```

## Remote-render mode

When to use:

- The local machine does not have Node.js 18+ / headless Chrome.
- Long or high-resolution videos where you do not want to tie up the local machine.
- CI / batch rendering.

### Example invocation

```bash
python3 <SkillDir>/scripts/render_video.py --job-id 42 --renderer remote --upload-title "Remote render test"
```

In remote mode:

- `POST /render` returns a `taskId` immediately; the script polls `POST /renderStatus`.
- **No** local `output/video.mp4` is produced (the MP4 is rendered on the server and uploaded directly to OSS).
- `render-manifest.json.upload.fileUrl` comes from the remote response.
- `render-plan.json` gains a `renderMode: "remote"` field and a `remoteTaskId`.
- `--upload` / `--no-upload` are ineffective in remote mode (the server handles the upload).

### Fallback on failure

If the remote service is unreachable or auth fails, the script exits with a detailed message. Re-run the same `--job-id` with `--renderer local` as a fallback.

### Service deployment

The Remotion render project lives at the monorepo root under `<monorepo-root>/remotion-renderer/` and is deployed as a **standalone HTTP service**. The client script in this skill only sends POST requests and does not participate in server-side code sync.

- Server code + deployment docs: `<monorepo-root>/remotion-renderer/README.md`.
- Production (default): the client uses `https://api-render.remixmate.ai` when `REMOTION_RENDER_API_URL` is unset — no config needed.
- Local debugging: in the renderer directory run `npm install && npm run server:dev`, then point the client at it with `REMOTION_RENDER_API_URL=http://localhost:3000`.

## Credits

Every run charges credits. The CLI prints a footer on stdout when it does:

```
💳 Charged 31 credits · balance 1,240
```

Relay it to the user whenever it appears — it is the only signal they get about what a
generation cost, and the balance is the only warning before a run fails with
`insufficient_credits`. Do not drop it from your summary.

## Error handling

- **`job_id` missing or not assets-ready**: the script exits non-zero with a message like "render job N is not in assets-ready state". The agent should call `prepare_video_assets` again with the updated DSL to regenerate, then re-call this skill with the new `job_id`.
- **DSL validation failed** (only reachable via `--render-plan` file mode if the plan is malformed): pre-validate with `gen-script --validate`.
- **Remotion render failed**: check the Node.js version (18+ required). When deps are missing the script auto-runs `npm install`; if that fails, run it manually inside `<monorepo-root>/remotion-renderer/` (overridable via `REMOTION_RENDERER_DIR`).
- **Remote render failed**: inspect `render-plan.json.errors` and the server log; fall back to `--renderer local` if needed.
- **Timeouts**: remote rendering accepts `--remote-poll-timeout`.
