An open API service indexing awesome lists of open source software.

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

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.