Skip to main content

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 with clear or cls.
  • 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") and expect_regex(r"..."): wait for real app output.
  • mark_output() and output_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 become https://...; relative paths use the served folder.
  • find(...): bs4-style visible element lookup.
  • find_optional(...): returns None instead of raising when an element is absent.
  • find_input(...): finds input and textarea controls.
  • find_select(...): finds select controls.
  • Element methods include highlight(), click(), double_click(), hover(), wait(), text(), and attribute().
  • 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(), and select_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. Use select_text(...) for visible mouse-drag selection, or select_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

demo_video_recorder-0.1.3.tar.gz (152.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

demo_video_recorder-0.1.3-py3-none-any.whl (59.1 kB view details)

Uploaded Python 3

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

Hashes for demo_video_recorder-0.1.3.tar.gz
Algorithm Hash digest
SHA256 188f13ae05932f416c28bd66dc65239a27cbd1ba197168354df3ca5fad55ac7a
MD5 58992f66bf860b29765f2f479a7b486d
BLAKE2b-256 3db54530930068442087587c7834e6644bc55e888503e891fc4237138749f8ee

See more details on using hashes here.

File details

Details for the file demo_video_recorder-0.1.3-py3-none-any.whl.

File metadata

File hashes

Hashes for demo_video_recorder-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 b74d3bd79f832f7279a44a245614c5bc0d61b807a392364ac5cb797c77904152
MD5 48ab941bd3805c5f1922bf8fa2dcec7b
BLAKE2b-256 1dfee7accdfa6e66703f95382ac8cb954406fcaeb9a0ffbf6ac3c989aa5d1315

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page