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(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(), set_range(), set_date(), set_color(), set_files(), press(), check(), uncheck(), and select_option().

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%",
)

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)

List available Edge voices:

from demo_video_recorder import EdgeTTSBackend

tts = EdgeTTSBackend(save_dir="out/voices")
print("\n".join(tts.list_speakers()))

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.1.tar.gz (141.4 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.1-py3-none-any.whl (50.8 kB view details)

Uploaded Python 3

File details

Details for the file demo_video_recorder-0.1.1.tar.gz.

File metadata

  • Download URL: demo_video_recorder-0.1.1.tar.gz
  • Upload date:
  • Size: 141.4 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.1.tar.gz
Algorithm Hash digest
SHA256 81dc71bbefbee1a5dd1a215d5a3977b00ec2eab05c30b99f56b9bd2cbb730b25
MD5 603b9abebe00116630ac1a2ea84323e8
BLAKE2b-256 8aaca61b8b11b47ceb69fe35634b843ecff025240489b7c9346efea6eecb3c67

See more details on using hashes here.

File details

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

File metadata

File hashes

Hashes for demo_video_recorder-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 9495daa3223689f6c45b9dbdc12682fdfde65cf525fd7d4ff906f0263fb76b12
MD5 2584ac452265cc8c9777cc7b3f011f3d
BLAKE2b-256 210d57a4f84cdf1d3a7fba28e6583ddf2417bb04ffe219815e0a18dc875c427b

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