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
bashcurl -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"] }'
tsimport { B3OS } from "@b3os/sdk";const b3os = new B3OS();// .wait() resolves with the finished jobconst 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);
pythonfrom b3os import B3OSb3os = 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"])
textRecord 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 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. A script that fails validation is refused with script_invalid before anything records; a step that fails while recording ends the job with script_step_failed, and step names it, such as beats/1/do/0.
Follow the job
A video is a job. 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.
bashcurl -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.
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.