> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beta.b3os.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Videos

Record a scripted, narrated product demo as MP4, GIF, poster and captions, follow it as a job, and restyle it later without recording again.

`POST /v1/render/videos` records a demo of a web app from a script. The script is a list of **beats**: each beat says one line of narration and performs browser steps while the line is said. The result is a job.

## Record a video

**curl**

```bash
    curl -s "https://beta-api.b3os.org/v1/render/videos?wait=30" \
      -H "Authorization: Bearer $B3OS_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "source": {"url": "https://app.example.com", "profile": "acme-staging"},
        "beats": [
          {"say": "Open the billing page.",
           "do": [{"kind": "click", "target": {"role": "link", "name": "Billing"}}]},
          {"say": "Switch to annual and watch the price update.",
           "do": [{"kind": "click", "target": {"text": "Annual", "exact": true}}]}
        ],
        "voice": {"model": "kokoro-82m", "voice": "af_heart"},
        "captions": {"burn_in": "karaoke", "sidecars": ["vtt"]},
        "style": {"background": "midnight", "frame": "browser-dark", "cursor": "mac"},
        "formats": ["mp4", "gif", "poster"]
      }'
    ```
  **TypeScript**

```ts
    import { B3OS } from "@b3os/sdk";

    const b3os = new B3OS();
    // .wait() resolves with the finished job
    const done = await b3os.render
      .createVideo({
        source: { url: "https://app.example.com", profile: "acme-staging" },
        beats: [
          { say: "Open the billing page.", do: [{ kind: "click", target: { role: "link", name: "Billing" } }] },
          {
            say: "Switch to annual and watch the price update.",
            do: [{ kind: "click", target: { text: "Annual", exact: true } }],
          },
        ],
        voice: { model: "kokoro-82m", voice: "af_heart" },
        captions: { burn_in: "karaoke", sidecars: ["vtt"] },
        style: { background: "midnight", frame: "browser-dark", cursor: "mac" },
        formats: ["mp4", "gif", "poster"],
      })
      .wait();
    for (const artifact of done.result?.artifacts ?? []) console.log(artifact.kind, artifact.url);
    ```
  **Python**

```python
    from b3os import B3OS

    b3os = B3OS()
    done = b3os.render.create_video(
        {
            "source": {"url": "https://app.example.com", "profile": "acme-staging"},
            "beats": [
                {"say": "Open the billing page.",
                 "do": [{"kind": "click", "target": {"role": "link", "name": "Billing"}}]},
                {"say": "Switch to annual and watch the price update.",
                 "do": [{"kind": "click", "target": {"text": "Annual", "exact": True}}]},
            ],
            "voice": {"model": "kokoro-82m", "voice": "af_heart"},
            "captions": {"burn_in": "karaoke", "sidecars": ["vtt"]},
            "style": {"background": "midnight", "frame": "browser-dark", "cursor": "mac"},
            "formats": ["mp4", "gif", "poster"],
        },
        wait=True,  # returns the finished job
    )
    for artifact in done["result"]["artifacts"]:
        print(artifact["kind"], artifact["url"])
    ```
  **MCP**

```text
    Record a short demo of https://app.example.com: open Billing, then switch to annual pricing. Narrate each step.
    ```

    The agent writes the beats and calls `render_video` with them as `request`. The tool waits up to `wait_s` seconds and returns the artifact URLs, or the video ID while the job still runs, to call again with `job_id`. `render_video` is on the local MCP server only.

The voice `model` and `voice` come from `GET /v1/infer/models` and `GET /v1/infer/audio/voices`; the values above are examples. Narration is optional: a beat without `say` is silent, and `hold_ms` keeps the camera on a beat after its steps end.

## The script

| Field | Meaning |
|---|---|
| `source.url` | Where the recording starts |
| `source.profile` | A vision [profile](/vision/profiles) to start signed in |
| `beats` | Up to 100 beats, each with `say` (up to 2,000 characters), `do` (up to 50 steps), an optional `title` and `hold_ms` |
| `voice` | Speech `model`, `voice` and `speed` (0.5 to 2) |
| `captions` | `burn_in` (`none`, `static`, or `karaoke`, which also highlights the spoken word), `sidecars` (`vtt`, `srt`), and line limits: 42 characters, 2 lines and 20 characters per second by default |
| `style` | Background, frame, cursor, caption and click-effect presets, plus `overrides` |
| `camera` | `auto_zoom` (on by default) zooms toward the element each step acts on, up to `max_zoom` (2 by default, 3 at most) |
| `viewport` | The browser's size in CSS pixels |
| `output` | `width`, `height` and `fps` (24, 25, 30, 50 or 60) of the video |
| `formats` | Any of `mp4`, `gif` and `poster`; `mp4` and `poster` by default |
| `seed`, `start_time` | Seed the page's randomness and fix its clock, so a re-render is identical |

Steps use the browser action kinds `navigate`, `click`, `hover`, `type`, `key`, `scroll`, `select` and `wait`, with the same fields as in [browser sessions](/vision/sessions#browser-actions). A script that fails validation is refused with [`script_invalid`](/errors#script_invalid) before anything records; a step that fails while recording ends the job with [`script_step_failed`](/errors#script_step_failed), and `step` names it, such as `beats/1/do/0`.

## Follow the job

A video is a [job](/concepts/jobs). Its events stream at `GET /v1/render/videos/{video_id}/events`: the lifecycle events, `video.phase` as it moves through voice, plan, capture, compose, encode and upload, and `video.artifact` as each file becomes available, poster first. You can also wait on the create call with `?wait=`, or pass a `webhook`.

When it succeeds, `result` holds the duration, size and frame rate, and `artifacts` lists the files with signed URLs. MP4 files are H.264 with BT.709 color. `POST /v1/render/videos/{video_id}/cancel` stops the job; artifacts already made are kept and seconds already encoded are billed.

`GET /v1/render/videos/{video_id}/timeline` returns the compiled timeline once planning is done: the narration with word timings, the timed actions the recorder replayed, the cursor path, camera keyframes, captions and style. Add `frames=1` for the per-frame compositor state when you need to debug a frame.

## Restyle a video

Restyling re-composes the archived raw capture with a new style, captions, output size or formats. No browser runs and the narration is reused; camera moves do not change.

```bash
curl -s "https://beta-api.b3os.org/v1/render/videos/$VIDEO_ID/restyle?wait=30" \
  -H "Authorization: Bearer $B3OS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"style": {"background": "sunrise", "frame": "browser-light"}, "captions": {"burn_in": "static"}}'
```

The result is a new video whose `restyled_from` names the source. The raw capture lives as long as the source video, 7 days; after that, or for a video that did not succeed, restyle fails with [`restyle_unavailable`](/errors#restyle_unavailable).

## Delete

`DELETE /v1/render/videos/{video_id}` cancels the job if it is still running, then deletes the video, its artifacts and its raw capture.