Metadata-Version: 2.4
Name: tourcast
Version: 0.1.0
Summary: Record a hands-free walkthrough of a web app as an MP4
Project-URL: Homepage, https://github.com/AzuraLight/tourcast
Project-URL: Source, https://github.com/AzuraLight/tourcast
Project-URL: Issues, https://github.com/AzuraLight/tourcast/issues
Author: AzuraLight
License: MIT License
        
        Copyright (c) 2026 AzuraLight
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: demo,documentation,playwright,screencast,video,walkthrough
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Multimedia :: Video :: Capture
Classifier: Topic :: Software Development :: Documentation
Requires-Python: >=3.11
Requires-Dist: pillow>=10.0
Requires-Dist: playwright>=1.40
Requires-Dist: pyyaml>=6.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# tourcast

<!-- The logo needs a width, which Markdown image syntax cannot set; the URL is absolute because
     this README is also the package description, where a relative path would not resolve. -->
<!-- markdownlint-disable-next-line MD033 -->
<img src="https://raw.githubusercontent.com/AzuraLight/tourcast/main/assets/logo.svg" width="96" height="96" alt="An aperture with a cursor at its centre">

**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.

```bash
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

```text
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.

```text
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

```bash
pipx install tourcast          # or: pip install tourcast
playwright install chromium
```

`ffmpeg` must be on `PATH`:

```bash
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:

```bash
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](docs/reference/scenario.md) | Every scenario key, `click_any`, camera directives, validation, exit codes |
| [docs/reference/profile.md](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](docs/decisions/INDEX.md) | **Start here.** Every decision in one line with its consequence and a pointer |
| [docs/decisions/](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

```bash
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](.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.
