tourcast
Record a hands-free walkthrough of a web app as an MP4.
Write the tour as YAML. Playwright walks the app, a synthetic cursor moves to each target, and the camera zooms and dims to direct attention. When the product changes, run it again — the video is regenerated, not re-shot. Demo-as-Code.
python -m http.server 8765 --directory examples/demo-site &
tourcast record examples/scenarios/demo-tour.yaml -o demo.mp4
That example needs nothing but this repository: the demo site is checked in.
Commands
Four of them, and every one takes a single scenario file — tourcast <verb> <scenario>.
| Command | What it does |
|---|---|
tourcast record <scenario> |
records the tour |
tourcast check <scenario> |
validates the spec without opening a browser |
tourcast probe <scenario> |
surveys the target app; records nothing |
tourcast init <name> |
scaffolds a profile and a scenario |
probe exists because scenarios should be written from measurement, not guesswork. It walks
the sidebar, clicks the first row of every table and reports what actually happened —
whether the row opens a detail page or a modal, how far each screen scrolls, which tabs exist.
How it works
scenario.yaml -> world (Playwright) -> frames (CDP screencast) -> camera -> MP4
| |
| navigation, clicks, the real pointer | crop, dim,
| | cursor, timing
Two decisions shape everything else.
The camera never touches the page. Zoom is a crop rectangle over captured frames, not a CSS transform. A transform would re-evaluate viewport units and media queries, so the video would no longer show the app as users see it — and it would break differently in every app, which makes a shared tool impossible.
The cursor is composited after capture. It is not injected into the DOM. Its path is recorded as data and drawn onto each output frame, so its motion is independent of browser paint timing. That also means it works unchanged inside iframes.
Profiles keep it app-agnostic
Everything the engine would otherwise need to know about an app lives in a profile.
profiles/<app>.yaml base URL, login recipe, selector aliases, ready states
scenarios/*.yaml order of movement, dwell times, emphasis
tourcast/ the engine — knows no app
Recording a new app means adding one profile and one scenario. The engine is not touched — the claim was checked by recording a second, deliberately different app (no login, direct page entry instead of an iframe): one profile and one scenario were added and no engine line changed.
Scenarios never contain raw selectors — only aliases the profile defines. When a selector breaks, exactly one file changes.
Install
pipx install tourcast # or: pip install tourcast
playwright install chromium
ffmpeg must be on PATH:
brew install ffmpeg # macOS
sudo apt install ffmpeg # Debian/Ubuntu
winget install Gyan.FFmpeg # Windows
Fonts are the machine's job, not the tool's. tourcast draws no text, and Chromium can only use fonts the OS knows about — so recording an app in a language whose font is missing produces a valid video full of empty boxes, with no error. On a bare container, install them and verify:
sudo apt install fonts-noto-core fonts-noto-cjk
python tools/check-glyphs.py # exits 1 if a script renders as tofu
Non-goals
Kept out on purpose, so a request to add them has a clear answer.
| Not doing | Why |
|---|---|
| Interactive HTML demos | That is Arcade/Storylane territory. The asset here is the spec-to-video pipeline, not an editor |
| A no-code GUI editor | Scenarios must be text for diffs, review and CI to work |
| An E2E test framework | Add assertions and you are reinventing Playwright. A failed step means "stop recording", nothing more |
| Branching and loops in YAML | A sequential tour does not need them. If they are ever needed, the answer is a Python API, not a bigger YAML |
| Captions and narration | Out of scope for now; the rationale is in docs/decisions/02-ARCHITECTURE.md |
Status
Working end to end. Measured on the pipeline itself:
| Area | Verified | Measurement |
|---|---|---|
| Capture | CDP + PNG beats Playwright's recordVideo |
PSNR 53.12dB vs 48.43dB, no queue backlog at 3840x2160 |
| Cursor | lands on the element it is explaining | 3/3 on target, 0.5px error, legible on light and dark |
| Timing | the video preserves real time | length within +4ms of plan; intervals within one frame |
| Camera | zoom leaves the page untouched and stays lossless | DOM metrics identical, 0 upscaled frames at 2.0x |
| Spotlight | attention is actually directed | target 200 vs surroundings 90 luma — exactly the requested opacity |
| Portability | a new app needs only a profile | second app recorded with no engine change |
Documentation
Split by kind, not by language — a translated journal would only diverge from the original.
Reference — what you read to use the tool. English, single source.
| Document | Contents |
|---|---|
| docs/reference/scenario.md | Every scenario key, click_any, camera directives, validation, exit codes |
| docs/reference/profile.md | Profile keys, login, sessions, aliases, and how to survey an app |
Decisions — why it is built this way. A working journal in Korean, with an English index.
| Document | Contents |
|---|---|
| docs/decisions/INDEX.md | Start here. Every decision in one line with its consequence and a pointer |
| docs/decisions/ | Measurements, failed approaches, and phase-by-phase acceptance criteria |
The index is worth a minute before changing anything in tourcast/: several entries are
invariants that fail silently when broken — the supersampling launch flag being the clearest one.
Development
python3 -m venv .venv
.venv/bin/pip install playwright pyyaml pillow pytest
.venv/bin/playwright install chromium
.venv/bin/python -m pytest tests/ -q # no browser needed, runs in under a second
python -m http.server 8765 --directory examples/demo-site &
.venv/bin/python -m tourcast.cli record examples/scenarios/demo-tour.yaml -o out/demo.mp4
Profiles and scenarios that carry credentials are named *.local.yaml and are gitignored.
CI (.github/workflows/ci.yml) runs the tests on 3.11–3.13, then records the example tour on Linux and checks the run report — a blurry zoom or a cursor that never moved fails the build, since neither shows up as a non-zero exit code.
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 tourcast-0.1.0.tar.gz.
File metadata
- Download URL: tourcast-0.1.0.tar.gz
- Upload date:
- Size: 67.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
928d81df660eb0affb342df2769b448c45c95d873ff2291cac1e3cf2178174c1
|
|
| MD5 |
4289fe6229722bc30d2480384de71151
|
|
| BLAKE2b-256 |
f97c5a5acd57463dd8dddbdf57cb637bbff551165a9450a711fb79969de9f495
|
Provenance
The following attestation bundles were made for tourcast-0.1.0.tar.gz:
Publisher:
publish.yml on AzuraLight/tourcast
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tourcast-0.1.0.tar.gz -
Subject digest:
928d81df660eb0affb342df2769b448c45c95d873ff2291cac1e3cf2178174c1 - Sigstore transparency entry: 2288174443
- Sigstore integration time:
-
Permalink:
AzuraLight/tourcast@215675fcf39b00fc0482b89f5584d867eaac2146 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/AzuraLight
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@215675fcf39b00fc0482b89f5584d867eaac2146 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file tourcast-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tourcast-0.1.0-py3-none-any.whl
- Upload date:
- Size: 62.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0e1ec78423d4f0223b899c500406f8b3296933e7d832b78dfb58b0b8e0df6828
|
|
| MD5 |
1b8d93934f65f425d2e4df7d83451c2d
|
|
| BLAKE2b-256 |
d03d945470cc8076b952752589df78a16b22d315cdcbdd85699ebd9e036fb30f
|
Provenance
The following attestation bundles were made for tourcast-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on AzuraLight/tourcast
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tourcast-0.1.0-py3-none-any.whl -
Subject digest:
0e1ec78423d4f0223b899c500406f8b3296933e7d832b78dfb58b0b8e0df6828 - Sigstore transparency entry: 2288174461
- Sigstore integration time:
-
Permalink:
AzuraLight/tourcast@215675fcf39b00fc0482b89f5584d867eaac2146 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/AzuraLight
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@215675fcf39b00fc0482b89f5584d867eaac2146 -
Trigger Event:
workflow_dispatch
-
Statement type: