https://github.com/openhands/demorec
Record cli and web-based demos from a script
https://github.com/openhands/demorec
Last synced: about 1 month ago
JSON representation
Record cli and web-based demos from a script
- Host: GitHub
- URL: https://github.com/openhands/demorec
- Owner: OpenHands
- License: mit
- Created: 2026-03-17T22:47:32.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-04-03T19:51:31.000Z (3 months ago)
- Last Synced: 2026-04-03T20:19:57.505Z (3 months ago)
- Language: Python
- Size: 11 MB
- Stars: 2
- Watchers: 0
- Forks: 0
- Open Issues: 8
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# demorec π¬
**Proof of work that's easy on the eye.**
> πΊ [View full demo with script](https://htmlpreview.github.io/?https://github.com/jpshackelford/demorec/blob/main/demo/index.html) | [Download video](demo/demorec-intro.mp4)
demorec is a declarative tool for creating professional demo videos that seamlessly mix terminal and browser interactionsβperfect for product demos, tutorials, and PR walkthroughs.
Inspired by [charmbracelet/vhs](https://github.com/charmbracelet/vhs), but unified across CLI and web.
## Why demorec?
Real-world demos often involve both terminal and browser:
- Start a server via CLI β show the web UI
- Run a build command β verify results in browser
- Configure via terminal β interact with the app
Existing tools make you record these separately and stitch manually. **demorec handles it all in one script.**
### Key Features
- **Unified Recording**: Seamlessly switch between terminal and browser in a single script
- **Multiple Terminal Sessions**: Run servers, clients, and utilities in independent named sessions
- **Persistent State**: Terminal state (working directory, environment variables, running processes) persists across mode switches
- **Terminal Sub-modes**: Use `@mode terminal:vim` for vim command expansion
- **AI Narration**: Add voiceover with Edge TTS (free) or ElevenLabs
- **Vim Primitives**: High-level commands for code review demos (`Open`, `Highlight`, `Goto`, `Close`)
## Quick Example
```tape
# my-demo.demorec
Output demo.mp4
Set Width 1280
Set Height 720
# @voice edge:jenny
# βββββββββββββββββββββββββββββββββββββ
# TERMINAL: Install and start server
# βββββββββββββββββββββββββββββββββββββ
@mode terminal
Set Theme "Dracula"
# @narrate:before "Let's install the CLI tool."
Type "pip install myapp"
Enter
Sleep 2s
# @narrate:before "Now let's start the dev server."
Type "myapp serve --port 3000"
Enter
Sleep 3s
# βββββββββββββββββββββββββββββββββββββ
# BROWSER: Show the web interface
# βββββββββββββββββββββββββββββββββββββ
@mode browser
# @narrate:before "The web interface is now live."
Navigate "http://localhost:3000"
Sleep 2s
Click "#create-new"
Type "#name" "My Project"
Click "#save"
Sleep 2s
# βββββββββββββββββββββββββββββββββββββ
# TERMINAL: Show the logs
# βββββββββββββββββββββββββββββββββββββ
@mode terminal
# @narrate:after "And we can see the request in the terminal logs."
Sleep 2s
Ctrl+C
Sleep 1s
```
```bash
demorec record my-demo.demorec
```
## Installation
### System Dependencies
demorec requires several system tools to be installed:
| Dependency | Required | Purpose | Installation |
|------------|----------|---------|--------------|
| **FFmpeg** | β
Yes | Video/audio processing | `sudo apt install ffmpeg` (Ubuntu) or `brew install ffmpeg` (macOS) |
| **ttyd** | β
Yes | Terminal PTY server | See below |
| **tmux** | β
Yes | Persistent terminal sessions | `sudo apt install tmux` (Ubuntu) or `brew install tmux` (macOS) |
| **Chromium** | β
Yes | Browser automation | Installed via `playwright install chromium` |
| **vim** | Optional | Only for vim primitives (`Open`, `Highlight`, etc.) | `sudo apt install vim` (Ubuntu) or `brew install vim` (macOS) |
| **Marp CLI** | Optional | Only for presentation mode | `npm install -g @marp-team/marp-cli` |
#### Installing ttyd
ttyd is a terminal sharing tool that provides the PTY backend. Install it with:
```bash
# Linux (x86_64)
wget -qO /tmp/ttyd https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64
chmod +x /tmp/ttyd
sudo mv /tmp/ttyd /usr/local/bin/ttyd
# macOS
brew install ttyd
```
### Python Package
```bash
# With uv (recommended)
uv tool install demorec
# Or with pip
pip install demorec
# Install Playwright browser (Chromium)
playwright install chromium
```
### Optional: ElevenLabs TTS
For premium voice quality, install the ElevenLabs extras:
```bash
pip install "demorec[tts]"
```
This requires an `ELEVENLABS_API_KEY` environment variable. Edge TTS (included by default) works without any API key.
## CLI Usage
```bash
# Record a demo
demorec record my-demo.demorec
# Record with options
demorec record my-demo.demorec -o output.mp4 --voice adam
# Validate syntax without recording
demorec validate my-demo.demorec
# List available TTS voices
demorec voices
# Show version
demorec --version
```
## Agent Workflow Tools
demorec includes commands designed for AI agents creating vim-based code review demos.
### Stage Directions
Calculate optimal vim commands to display specific line ranges:
```bash
# Get vim commands for highlighting specific line ranges
demorec stage --rows 30 --highlights "6-8,11-16,27-35,63-73"
# Output formats: text (default), json, demorec
demorec stage --rows 30 --highlights "6-8,27-35" --format json
demorec stage --rows 30 --highlights "6-8,27-35" --format demorec
```
Example output:
```
Stage Directions (30 rows)
Block 1: lines 6-8 (3 lines)
Goto: 6G
Center: zz
Select: V8G
Rationale: Block fits in viewport, using zz to center
Block 2: lines 27-35 (9 lines)
Goto: 31G
Center: zz
Select: V27G then 35G
Rationale: Block fits in viewport, centering on middle line
```
### Preview
Run through a script and verify checkpoints without recording video:
```bash
# Preview with verification (screenshots only on errors)
demorec preview script.demorec --rows 30
# Always capture screenshots at checkpoints
demorec preview script.demorec --rows 30 --screenshots
# Never capture screenshots (fastest)
demorec preview script.demorec --rows 30 --no-screenshots
# Capture frame-by-frame snapshots for AI debugging
demorec preview script.demorec --rows 30 -o ./frames
# Capture frames without output directory (disable frame capture explicitly)
demorec preview script.demorec --rows 30 -o ./frames --no-frames
```
Preview auto-detects "show moments" (visual selections in vim) and verifies that expected lines are visible:
```
[PASS] Checkpoint 1 (line 11): lines 6-8 visible
[PASS] Checkpoint 2 (line 33): lines 27-35 visible
Frames captured: 15 frames to ./frames
Summary: 2/2 passed
```
#### Frame-by-Frame Capture
When `--output-dir` is specified (or `--frames` is used), preview captures the terminal/browser state at every step:
- **Terminal frames**: Saved as `.txt` files containing the visible terminal buffer
- **Browser frames**: Saved as `.png` screenshots
Frame naming convention: `frame_{NNNN}_{SSSS.ss}.{ext}`
- `NNNN`: Zero-padded 4-digit frame number (0001, 0002, ...)
- `SSSS.ss`: Elapsed time in seconds with 2 decimal places (0000.00, 0001.25, ...)
- `ext`: File extension (`.txt` for terminal, `.png` for browser)
Example output:
```
frames/
βββ frame_0001_0000.00.txt # Initial terminal state
βββ frame_0002_0000.05.txt # After first command
βββ frame_0003_0000.28.txt # After Type "vim file.py"
βββ frame_0004_0001.52.txt # After Enter
βββ ...
```
This is useful for AI agents debugging recordings and verifying terminal output at each step.
### Checkpoints
Analyze a script to find natural checkpoint locations:
```bash
# List detected checkpoints
demorec checkpoints script.demorec
# JSON output for programmatic use
demorec checkpoints script.demorec --format json
```
### Terminal Sizing
Control terminal dimensions for consistent viewport sizing using segment settings:
```tape
@mode terminal
rows 30 # Exact row count (10-100)
size "medium" # Or use a preset size
theme "Dracula"
---
Type "vim myfile.py"
Enter
```
**Size presets:**
| Preset | Rows | Best for |
|--------|------|----------|
| `large` | 24 | Classic terminal, easy to read |
| `medium` | 36 | Balanced readability and content |
| `small` | 44 | Default xterm.js density |
| `tiny` | 50 | Maximum content, smaller text |
### High-Level Vim Primitives
For AI agents creating code review demos, these commands handle vim complexity internally:
```tape
@mode terminal:vim
rows 30
---
Open "src/api.py" # Open file with line numbers enabled
Highlight "10-20" # Navigate to lines and select visually
Highlight "45-55" # Jump to next highlight
Goto 100 # Jump to line with centering
Close # Exit vim cleanly
```
| Command | Description | Example |
|---------|-------------|---------|
| `Open ""` | Open file in vim with line numbers | `Open "src/api.py"` |
| `Highlight ""` | Navigate to lines and select visually | `Highlight "10-20"` |
| `Goto ` | Jump to specific line with centering | `Goto 50` |
| `Close` | Exit vim cleanly | `Close` |
### Complete Agent Workflow
```bash
# 1. View file with line numbers
cat -n examples/sample_code.py
# 2. Get stage directions for highlights
demorec stage --rows 30 --highlights "6-8,11-16,27-35"
# 3. Write the .demorec script using generated vim commands
# 4. Preview to verify checkpoints
demorec preview script.demorec --rows 30
# 5. If issues, adjust script and re-preview
# 6. Record final video
demorec record script.demorec
```
## DSL Reference
### Global Settings
```tape
Output demo.mp4 # Output file (.mp4, .webm)
Set Width 1280 # Video width
Set Height 720 # Video height
Set Framerate 30 # Video framerate
```
### Mode Switching & Multiple Terminal Sessions
demorec supports switching between terminal and browser modes, with **persistent terminal sessions** that maintain state across mode switches.
```tape
@mode terminal # Default terminal session
@mode terminal:vim # Terminal with vim sub-mode (command expansion)
@mode browser # Browser recording
# Named sessions use the 'name' setting:
@mode terminal
name "server"
---
# Commands for the "server" session...
# Note: The old @mode terminal:server syntax is no longer supported.
# Use the 'name' setting instead (as shown above).
```
#### Segment Settings Syntax
Settings can be specified immediately after `@mode`. Use a blank line or `---` delimiter to end settings and begin commands:
```tape
@mode terminal:vim
rows 30
theme "Dracula"
name "editor"
---
Open "file.py"
Highlight "6-8"
Close
```
Supported settings: `rows`, `size`, `theme`, `name`
Sub-modes (e.g., `terminal:vim`) and session names can be combined, as shown in the example above.
#### Terminal Sub-modes
Sub-modes enable specialized command expansion:
| Sub-mode | Syntax | Purpose |
|----------|--------|---------|
| `vim` | `@mode terminal:vim` | Enables vim command expansion for `Open`, `Highlight`, `Goto`, `Close` |
```tape
@mode terminal:vim
rows 30
---
Open "src/api.py"
Highlight "10-20"
Close
```
#### Session Persistence
Each terminal session is backed by tmux, which means:
| What Persists | Example |
|---------------|---------|
| Working directory | `cd /app` stays in `/app` after switching modes |
| Environment variables | `export API_KEY=xxx` remains set |
| Running processes | `python server.py &` keeps running |
| Command history | Up arrow recalls previous commands |
#### Named Sessions vs Default Session
| Session | Syntax | Use Case |
|---------|--------|----------|
| Default | `@mode terminal` | General commands, setup |
| Named | `@mode terminal` + `name "server"` | Long-running server process |
| Named | `@mode terminal` + `name "client"` | Client/testing commands |
| Named | `@mode terminal` + `name "logs"` | Tail logs or monitoring |
Named sessions are **completely independent**βeach has its own shell process, environment, and working directory. The default session (`@mode terminal`) is also persistent but separate from named sessions.
#### Typical Multi-Session Workflow
```tape
# 1. Start server in dedicated session
@mode terminal
name "server"
---
Type "npm run dev"
Enter
Sleep 2s
# 2. Switch to browser - server keeps running!
@mode browser
Navigate "http://localhost:3000"
Sleep 2s
# 3. Make API calls from client session
@mode terminal
name "client"
---
Type "curl localhost:3000/api/health"
Enter
# 4. Return to server session - see the request logs
@mode terminal
name "server"
---
Sleep 1s
```
#### Tips for Effective Use
1. **Set up state early:** Initialize environment variables and working directories at the startβthey persist throughout.
2. **Use named sessions for servers:** Start long-running processes in a named session so switching modes won't kill them.
3. **Show state preservation explicitly:** Run `pwd` or `echo $VAR` after switching back to demonstrate persistenceβviewers love this!
4. **Clean up gracefully:** Use `Ctrl+C` in server terminals before ending to show clean shutdown.
5. **Use meaningful names:** Prefer descriptive names like `"api"`, `"frontend"`, `"logs"` for clarity.
### Terminal Commands
| Command | Description | Example |
|---------|-------------|---------|
| `Set Theme ""` | Terminal theme | `Set Theme "Dracula"` |
| `Type ""` | Type text with delay | `Type "echo hello"` |
| `Enter` | Press Enter | `Enter` |
| `Run "" [wait]` | Type, execute, and wait | `Run "npm test" 3s` |
| `Sleep
### Browser Commands
| Command | Description | Example |
|---------|-------------|---------|
| `Navigate ""` | Go to URL | `Navigate "http://localhost:3000"` |
| `Click ""` | Click element | `Click "#submit-btn"` |
| `Type "" ""` | Type into element | `Type "#email" "user@example.com"` |
| `Fill "" ""` | Fill instantly | `Fill "#name" "John"` |
| `Press ""` | Press key | `Press "Enter"` |
| `Sleep
### Narration (AI Voice-Over)
```tape
# Set the voice
# @voice edge:jenny # Microsoft Edge TTS (recommended, free)
# @voice eleven:rachel # ElevenLabs (requires API key)
# Narration modes
# @narrate:before "Spoken before the next action"
# @narrate:during "Spoken while action runs"
# @narrate:after "Spoken after action completes"
```
**Microsoft Edge TTS voices (free, high quality - recommended):**
| Voice | Description |
|-------|-------------|
| `edge:jenny` | Female, US (default) |
| `edge:guy` | Male, US |
| `edge:aria` | Female, US |
| `edge:davis` | Male, US |
| `edge:emma` | Female, US |
| `edge:brian` | Male, US |
| `edge:sonia` | Female, UK |
| `edge:ryan` | Male, UK |
| `edge:natasha` | Female, AU |
| `edge:william` | Male, AU |
**ElevenLabs voices (requires paid API subscription):**
`eleven:rachel`, `eleven:adam`, `eleven:josh`, `eleven:bella`, `eleven:sam`, `eleven:antoni`, `eleven:arnold`, `eleven:domi`, `eleven:elli`
### Time Formats
- Seconds: `2s`, `1.5s`
- Milliseconds: `500ms`, `100ms`
## Environment Variables
| Variable | Description |
|----------|-------------|
| `ELEVENLABS_API_KEY` | ElevenLabs API key (only needed for ElevenLabs voices) |
Edge TTS works without any API key and is recommended for most use cases.
## Architecture
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β my-demo.demorec β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β parse
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Segment Plan β
β [terminal:0-15s] β [browser:15-45s] β [terminal:45-55s] β
ββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββΌββββββββββββββββ
βΌ βΌ βΌ
βββββββββββββ βββββββββββββ βββββββββββββ
β Terminal β β Browser β β Terminal β
β (xterm) β β(Playwright)β β (xterm) β
βββββββ¬ββββββ βββββββ¬ββββββ βββββββ¬ββββββ
β β β
βΌ βΌ βΌ
segment_0.mp4 segment_1.mp4 segment_2.mp4
β β β
βββββββββββββββββΌββββββββββββββββ
β
βΌ
βββββββββββββββββββββββ
β FFmpeg Concat + β
β TTS Audio Mix β
ββββββββββββ¬βββββββββββ
β
βΌ
demo.mp4 (final)
```
**Key insight:** Both terminal and browser recording use Playwright. Terminal segments render via xterm.js in a headless browser, enabling seamless video concatenation.
For detailed architecture documentation including terminal size management, persistent sessions, preview verification, and more, see **[docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)**.
## Examples
### Bug Fix Demo
```tape
Output bugfix-demo.mp4
Set Width 1280
Set Height 720
# @voice edge:guy
@mode terminal
Set Theme "Dracula"
# @narrate:before "This demo shows the fix for issue 42."
Type "git checkout fix/issue-42"
Enter
Sleep 1s
Type "npm test"
Enter
Sleep 3s
# @narrate:after "All tests pass. The bug is fixed!"
Sleep 2s
```
### Full Stack Demo
```tape
Output fullstack-demo.mp4
# @voice edge:jenny
@mode terminal
Set Theme "GitHub Dark"
# @narrate:before "Let's start the backend server."
Type "cd backend && npm start"
Enter
Sleep 3s
@mode browser
# @narrate:before "Now let's see the frontend."
Navigate "http://localhost:3000"
Sleep 2s
# @narrate:during "I'll create a new user account."
Click "a.signup"
Type "#email" "demo@example.com"
Type "#password" "SecurePass123"
Click "#submit"
Sleep 3s
# @narrate:after "Account created successfully!"
Sleep 2s
```
### Multiple Terminal Sessions
This example demonstrates a server/client workflow with persistent state:
```tape
Output multi-terminal-demo.mp4
Set Width 1280
Set Height 720
# βββββββββββββββββββββββββββββββββββββ
# Set up environment in default terminal
# βββββββββββββββββββββββββββββββββββββ
@mode terminal
Type "export API_KEY='demo-key-123'"
Enter
Type "cd /tmp && mkdir -p myapp && cd myapp"
Enter
Sleep 500ms
# βββββββββββββββββββββββββββββββββββββ
# Start server in named terminal
# βββββββββββββββββββββββββββββββββββββ
@mode terminal
name "server"
---
Type "cd /tmp/myapp"
Enter
Type "echo '
Hello World
' > index.html"
Enter
Type "python3 -m http.server 3000"
Enter
Sleep 2s
# βββββββββββββββββββββββββββββββββββββ
# View in browser (server keeps running!)
# βββββββββββββββββββββββββββββββββββββ
@mode browser
Navigate "http://localhost:3000"
Sleep 2s
# βββββββββββββββββββββββββββββββββββββ
# Test from client terminal
# βββββββββββββββββββββββββββββββββββββ
@mode terminal
name "client"
---
Type "curl http://localhost:3000/"
Enter
Sleep 1s
# βββββββββββββββββββββββββββββββββββββ
# Check server logs (session preserved)
# βββββββββββββββββββββββββββββββββββββ
@mode terminal
name "server"
---
# We see the server still running with request logs
Sleep 2s
# βββββββββββββββββββββββββββββββββββββ
# Original terminal state is intact!
# βββββββββββββββββββββββββββββββββββββ
@mode terminal
Type "echo $API_KEY && pwd"
Enter
# Shows: demo-key-123 and /tmp/myapp
Sleep 1s
# βββββββββββββββββββββββββββββββββββββ
# Clean up
# βββββββββββββββββββββββββββββββββββββ
@mode terminal
name "server"
---
Ctrl+C
Sleep 500ms
```
**Key points demonstrated:**
- State in the default terminal (env vars, working dir) persists across all mode switches
- Server in the `"server"` session keeps running while you switch to browser and client
- Each named session is independentβ`"client"` doesn't share state with `"server"`
- Returning to any terminal reconnects to the same session
### Code Review Demo (Vim Primitives)
```tape
Output code-review.mp4
Set Width 1280
Set Height 720
# @voice edge:jenny
@mode terminal:vim
rows 30
theme "Dracula"
---
# Open the file using high-level primitives
Open "src/api.py"
Sleep 0.5s
# @narrate:after "Let's review this API client code."
Sleep 1s
# Highlight the imports
Highlight "4-7"
Sleep 0.5s
# @narrate:after "First, notice the imports for dataclasses and typing."
Sleep 1s
# Highlight the main class
Highlight "10-25"
Sleep 0.5s
# @narrate:after "Here's our User dataclass with type hints."
Sleep 1s
# Highlight error handling
Highlight "45-55"
Sleep 0.5s
# @narrate:after "The error handling follows best practices."
Sleep 1s
# Exit cleanly
Close
Sleep 0.5s
# @narrate:after "That's a quick tour of the code!"
Sleep 1s
```
---
## Prior Art
- [charmbracelet/vhs](https://github.com/charmbracelet/vhs) - Terminal GIF recorder (inspiration)
- [fnando/demotape](https://github.com/fnando/demotape) - Ruby terminal recorder
- [Playwright](https://playwright.dev/) - Browser automation
- [xterm.js](https://xtermjs.org/) - Terminal emulator for the web
## License
MIT
## Support
This is an OpenHands Sandbox project, meaning that it is a preview of technology and not yet supported for production use.