Scriptable demo video recording for apps, terminals, and AI agents.
Project description
demo-video-recorder
Scriptable demo video recording for Python agents and humans.
demo-video-recorder helps you write small Python scripts that drive a CLI, browser UI, or native app window and turn that interaction into an MP4. It handles screen or browser capture, subtitle timing, optional burned-in subtitles, and optional narration audio through TTS.
It is especially useful when a coding agent needs to inspect a project, write a deterministic record_demo.py, react to app output, and produce a clean demo video without hand-recording the workflow.
Install
pip install demo-video-recorder
With uv:
uv add demo-video-recorder
Recording depends on ffmpeg and ffprobe being available:
ffmpeg -version
ffprobe -version
For browser demos, install the Playwright browser binaries once in the environment where you installed the package:
python -m playwright install chromium
Linux capture is not implemented yet. Windows capture uses gdigrab; macOS capture uses avfoundation. WebUIRecorder defaults to Playwright video capture, so headless browser demos do not need macOS Screen Recording permission unless you explicitly use video_backend="ffmpeg".
macOS Notes
On macOS, the first real screen recording (except playwright recording for webui apps) may require granting Screen Recording permission to Terminal, iTerm, your IDE, or whichever Python host runs the script. You can preflight this from Python:
from demo_video_recorder import check_screen_recording_access
result = check_screen_recording_access(prompt=True)
print(result)
High-quality burned subtitles require an ffmpeg build with the subtitles filter, which depends on libass. If your active ffmpeg does not support it, install a libass-enabled build such as ffmpeg-full and put it on PATH:
brew install ffmpeg-full
export PATH="/opt/homebrew/opt/ffmpeg-full/bin:$PATH"
ffmpeg -hide_banner -filters | rg subtitles
Quick Start: CLI Demo
from demo_video_recorder import CLIDemoRecorder
def main():
r = CLIDemoRecorder("out/cli-demo.mp4")
try:
r.open_terminal(
title="CLI Demo",
window_size=(1200, 900),
start_recording=True,
clear=True,
)
r.explain("We'll run the app and use its help command.")
r.run(["python", "app.py"], interactive=True, command_label="python app.py")
r.expect_output(">")
r.input("help")
r.expect_regex(r"Commands?:")
r.explain("The app prints the available commands, so the demo can keep going from real output.")
r.input("quit")
r.stop_app()
finally:
r.close()
if r.is_recording:
r.stop_recording()
if __name__ == "__main__":
main()
Useful CLI helpers:
open_terminal(...): configures the terminal and can start recording.clear(): clears the current terminal withclearorcls.run(..., interactive=True): starts a CLI app and streams stdout/stderr to the recorded terminal.input("text"): types into the active CLI app.expect_output("text")andexpect_regex(r"..."): wait for real app output.mark_output()andoutput_since(marker): isolate output caused by one action.explain("..."): adds narration subtitles and optional spoken narration.stop_recording(): finalizes the MP4.
When new_window=True is used, the recorder re-runs the script in a dedicated terminal session. On Windows it opens a new console. On macOS it opens a new Terminal.app window and captures that window when bounds are available. Worker stdout and stderr are mirrored to out/<name>.worker.log.
Quick Start: Web UI Demo
WebUIRecorder is built for browser demos. It defaults to Playwright's own page video recorder, which works in headless browser contexts and then passes the raw MP4 through the same subtitle and narration pipeline.
from demo_video_recorder import WebUIRecorder
def main():
r = WebUIRecorder("out/web-demo.mp4", headless=True, viewport=(1280, 720))
try:
r.serve("dist", 8000)
r.open_web("/")
r.explain("The local web app is open.")
r.find_input(label="Email address").fill("ada@example.com")
r.find_select(label="Plan").select_option(label="Pro")
r.find_input(label="Notes").select_clear_paste(0.5)
r.find(role="button", name="Continue").click()
r.find("main", text="Welcome").highlight()
r.explain("The workflow is complete and the confirmation is visible.")
finally:
r.close()
if r.is_recording:
r.stop_recording()
if __name__ == "__main__":
main()
Useful Web UI helpers:
serve(path, port=8000): serves a static folder over localhost.open_web(url=None): opens a URL. Bare domains becomehttps://...; relative paths use the served folder.find(...): bs4-style visible element lookup.find_optional(...): returnsNoneinstead of raising when an element is absent.find_input(...): findsinputandtextareacontrols.find_select(...): findsselectcontrols.- Element methods include
highlight(),click(),double_click(),hover(),wait(),text(), andattribute(). - Input methods include
fill(),type(),clear(),edit_text(),select_text(),select_all(),clear_selection(),copy(),cut(),paste(),select_clear(),select_paste(),select_clear_paste(),set_range(),set_date(),set_color(),set_files(),press(),check(),uncheck(), andselect_option(). - Use
edit_text()when you want a correction to look human: it finds the smallest text changes, presses Backspace for removed characters, then types inserted text. Useselect_text(...)for visible mouse-drag selection, orselect_clear_paste(0.5)for clipboard-style demos with pauses between selection, clearing, and pasting.
find() accepts Beautiful Soup style names and attrs plus Playwright-friendly selectors:
r.find("button", text="Save")
r.find("input", {"name": "email"})
r.find("input", _class="field-control")
r.find(selector="[data-testid='submit']")
r.find(role="button", name="Continue")
r.find(label="Email address").fill("ada@example.com")
Prefer robust selectors in this order: role and accessible name, label or placeholder, test id, then CSS selector.
Quick Start: Native App Window
from demo_video_recorder import DemoVideoRecorder
def main():
r = DemoVideoRecorder("out/app-demo.mp4")
try:
r.open_app(["notepad.exe"], title_hint="Untitled - Notepad", capture_window=True)
r.start_capture_window()
r.explain("The app window is open and being captured.")
finally:
r.close()
if r.is_recording:
r.stop_recording()
if __name__ == "__main__":
main()
Narration Audio
Add EdgeTTSBackend when you want spoken narration in addition to subtitles:
from demo_video_recorder import CLIDemoRecorder, EdgeTTSBackend
tts = EdgeTTSBackend(
save_dir="out/demo.tts",
speaker="en-US-AvaMultilingualNeural",
speed="+0%",
volume="+0%",
cache=True,
)
r = CLIDemoRecorder("out/demo.mp4", tts=tts)
When TTS is enabled, explain() uses the generated audio duration instead of estimating from word count. If synthesis latency would show up as dead air in the capture, pre-generate longer narration:
prepared = r.synthesize_if_tts_enabled(
"This narration is prepared before the visible interaction begins."
)
r.explain(prepared)
For several known cues, prepare them on the recorder instead of retyping async glue:
intro, finish = r.prepare_cues(
["The app is open.", "The result is now visible."],
async_tts=True,
)
List available Edge voices:
from demo_video_recorder import EdgeTTSBackend
tts = EdgeTTSBackend(save_dir="out/voices")
print("\n".join(tts.list_speakers()))
If Edge TTS repeatedly fails for a service or network reason, you can fall back to native OS speech on macOS or Windows:
from demo_video_recorder import NativeTTSBackend
tts = NativeTTSBackend(save_dir="out/demo.tts", cache=True)
Defaults
from demo_video_recorder import DEFAULTS, FAST_SMOKE_TEST_DEFAULTS
DEFAULTS.words_per_minute # 170
DEFAULTS.min_pause_seconds # 2.0
DEFAULTS.command_lead_seconds # 0.0
DEFAULTS.typed_character_delay # 0.018
DEFAULTS.capture_framerate # 15
DEFAULTS.video_scale_width # 1280
Use FAST_SMOKE_TEST_DEFAULTS for quick local script checks, not polished final videos.
FOR AI AGENT: PLEASE READ
This package ships a complete guide for coding agents. Before writing a recording script, read it from the installed package:
python -c "import importlib.resources as r; print((r.files('demo_video_recorder') / 'AGENT.md').read_text())"
The guide covers environment checks, macOS permissions, subtitle support, CLI and Web UI recording patterns, output-aware interactions, TTS pre-synthesis, and final video verification.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file demo_video_recorder-0.1.3.tar.gz.
File metadata
- Download URL: demo_video_recorder-0.1.3.tar.gz
- Upload date:
- Size: 152.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
188f13ae05932f416c28bd66dc65239a27cbd1ba197168354df3ca5fad55ac7a
|
|
| MD5 |
58992f66bf860b29765f2f479a7b486d
|
|
| BLAKE2b-256 |
3db54530930068442087587c7834e6644bc55e888503e891fc4237138749f8ee
|
File details
Details for the file demo_video_recorder-0.1.3-py3-none-any.whl.
File metadata
- Download URL: demo_video_recorder-0.1.3-py3-none-any.whl
- Upload date:
- Size: 59.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b74d3bd79f832f7279a44a245614c5bc0d61b807a392364ac5cb797c77904152
|
|
| MD5 |
48ab941bd3805c5f1922bf8fa2dcec7b
|
|
| BLAKE2b-256 |
1dfee7accdfa6e66703f95382ac8cb954406fcaeb9a0ffbf6ac3c989aa5d1315
|