{"id":51824222,"url":"https://github.com/htlin222/claude-session-recorder","last_synced_at":"2026-07-22T07:33:37.854Z","repository":{"id":369400350,"uuid":"1283161785","full_name":"htlin222/claude-session-recorder","owner":"htlin222","description":"Record narrated, sync-verified teaching videos of a real Claude Code TUI session — deterministic terminal + explainshell panel + voice that leads the typing, with on-screen AskUserQuestion handling. MIT.","archived":false,"fork":false,"pushed_at":"2026-07-05T04:59:23.000Z","size":21693,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-05T06:14:07.633Z","etag":null,"topics":["claude","claude-code","demo-videos","developer-tools","ffmpeg","macos","python","screencast","teaching","terminal-recording","tmux","vhs"],"latest_commit_sha":null,"homepage":"https://github.com/htlin222/claude-session-recorder/tree/main/engine/experimental/session-recorder/live","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/htlin222.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-28T16:04:03.000Z","updated_at":"2026-07-05T04:59:26.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/htlin222/claude-session-recorder","commit_stats":null,"previous_names":["htlin222/claude-session-recorder"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/htlin222/claude-session-recorder","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/htlin222%2Fclaude-session-recorder","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/htlin222%2Fclaude-session-recorder/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/htlin222%2Fclaude-session-recorder/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/htlin222%2Fclaude-session-recorder/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/htlin222","download_url":"https://codeload.github.com/htlin222/claude-session-recorder/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/htlin222%2Fclaude-session-recorder/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35753457,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-22T02:00:06.236Z","response_time":124,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["claude","claude-code","demo-videos","developer-tools","ffmpeg","macos","python","screencast","teaching","terminal-recording","tmux","vhs"],"created_at":"2026-07-22T07:33:37.218Z","updated_at":"2026-07-22T07:33:37.847Z","avatar_url":"https://github.com/htlin222.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# vhs-demo — narrated, sync-verified terminal teaching videos\n\n## ⭐ `claude-session-recorder` — record narrated videos of a *real* Claude Code session\n\n[![CI](https://github.com/htlin222/claude-session-recorder/actions/workflows/test.yml/badge.svg)](https://github.com/htlin222/claude-session-recorder/actions/workflows/test.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)\n![Platform](https://img.shields.io/badge/platform-macOS-lightgrey.svg)\n![Tests](https://img.shields.io/badge/tests-98%20passing-brightgreen.svg)\n\n![Demo: the opening is a CLI lesson — `claude` is built flag-by-flag while the right panel dissects each flag with a colour-coded note](engine/experimental/session-recorder/live/assets/demo.gif)\n\nFilms a real `claude` TUI session **once**, then freeze-frame splices the static\nstretches to fit an authored voice that **leads** the typing — a deterministic\n1920×1080 video where the **terminal, the explainshell-style panel, and the voice\nstay in lock-step**. It even drives Claude's interactive **AskUserQuestion**\nselector on screen. Single `ledger.json` source of truth; a deterministic lint gate.\n\n```bash\npip install -e engine/experimental/session-recorder/live\nrecord-session /tmp/demo engine/experimental/session-recorder/live/script.example.json\n# → /tmp/demo/session_panel.mp4\n```\n\n**→ Full docs, quickstart \u0026 architecture: [`engine/experimental/session-recorder/live/`](engine/experimental/session-recorder/live/)**\n\n\u003e **Which pipeline do I want?** This repo has two unrelated engines that share\n\u003e vocabulary (`overlay.py`, \"verify\", \"5 gates\" vs. `check()`) — don't cite one\n\u003e when you mean the other.\n\u003e\n\u003e | | ⭐ `claude-session-recorder` | CLI-lesson engine |\n\u003e | --- | --- | --- |\n\u003e | Entry point | [`engine/experimental/session-recorder/live/`](engine/experimental/session-recorder/live/) | [`engine/src/`](engine/src/) |\n\u003e | Does | Live-captures a real `claude` TUI session | Scaffolds a per-slug demo (fzf/sed/awk/df/…) |\n\u003e | Verified by | `lint.py`'s `check()` | `verify_sync.py`'s \"5 gates\" |\n\u003e\n\u003e The \"Repo layout\" / \"Pipeline\" sections right below this box describe the\n\u003e **CLI-lesson engine**, not the starred one above.\n\n---\n\n## Goal\nTurn terminal activity into a polished 1920×1080 teaching video: a real terminal\non the **left**, an explainshell-style panel on the **right**, one continuous\nzh-TW **narration**, J-cut scene transitions, and sidecar `.srt` subtitles — with\nthe audio↔video sync **machine-verified, never eyeballed**. One topic = one\n**portable, self-contained folder** another agent can pick up and rebuild.\n\nThree production paths exist today, plus a fourth in progress:\n1. **CLI lesson** — type a command, dissect its flags token-by-token (rsync, jq,\n   awk, sed→gsed, grep, find, tar, fzf, ssh, rg, ifconfig, df, cp …).\n2. **mdp slideshow** — a Markdown deck as a terminal slideshow + a detail panel.\n3. **nvim code-demo** — drive an editor to write \u0026 run code live.\n4. **(WIP) live Claude Code session** — record a real `claude` TUI run and narrate\n   it from hook timecodes + transcript. See *Roadmap*.\n\n## Repo layout\n```\nengine/                 canonical render engine (TEMPLATE / source-of-truth)\n  src/  clipkit.py(S/R/CLR + load_lesson) build.py overlay.py verify_sync.py\n        bundle.py envcheck.py\n  config.toml  lesson.skel.py  CLAUDE.tmpl.md  context/(sync-model etc)\n  experimental/         slideshow/ (mdp), nvim/ (recipe), session-recorder/ (hooks\n                        + live/ = sandbox + sentinel + tape-gen for filming claude)\n\u003cslug\u003e/                 a PORTABLE clip: vendored src/ + lesson.py + config.toml +\n                        setup.sh + CLAUDE.md + \u003cslug\u003e.mp4/.srt/.html + provenance/\n.claude/\n  settings.json         session-recorder hooks (timecode logging)\n  workflows/clip.js     the /clip dynamic workflow\nmaterial/               drop topic material here for /clip\n```\nEach `\u003cslug\u003e/CLAUDE.md` is a complete per-clip handoff doc; `engine/CLAUDE.md`\ndocuments the engine; `engine/context/sync-model.md` is the sync evolution log.\n\n## Pipeline (per clip, run from its folder)\n`build.py` (edge-tts narration + `demo.tape` + `timeline.json`) → `setup.sh`\n(demo env) → `vhs demo.tape` (terminal.mp4) → `overlay.py` (panel + J-cut + fades\n→ `\u003cslug\u003e.mp4`) → `verify_sync.py` (the 5 gates) → `bundle.py` (freeze\n`provenance/`). The `/clip` workflow automates all of it (design → envcheck →\nauthor → render → verify → deliver).\n\n## How sync works (the core idea)\nONE continuous narration; on-screen actions are pinned to its **sentence\nboundaries** (edge-tts gives a real timestamp per sentence). The terminal's real\n**clear frames** are detected from the video so the panel switches on the exact\nframe the screen clears. Sync is **loop-engineered**: `verify_sync.py` gates five\nthings and nothing ships silently —\n1. structural sync (every scene locked to a real clear),\n2. narration not cut, 3. duration ≈5±1 min,\n4. J-cut never overruns the *incoming* voice, 5. no scene's voice overruns into\nthe *next* clip. A fixable desync auto-heals (guided/threshold clears → re-overlay).\n\n---\n\n## ⚠️ Gotchas (hard-won — read before changing anything)\n\n### VHS\n- **`Set TypingSpeed 45ms` ≈ 24ms/char real** (~half). The timing model uses\n  `config.timing.ts=0.024`; don't derive it from the Set value.\n- **`Hide`/`Show` cost zero timeline time** — use to clear/`setopt` invisibly.\n- **zsh doesn't treat `#` as a comment interactively** → `setopt interactive_comments`\n  in a hidden block first, else `zsh: command not found: #`.\n- **Non-deterministic TUIs (claude): use `Wait[+Screen]@timeout /regex/`, not\n  `Sleep`.** Pair with a **sentinel** the app prints when done.\n- **The terminal eats the FIRST keystroke** after launching a TUI → send a\n  throwaway `Escape` before `i`/input (cost us the nvim demo until found).\n\n### nvim (code-demo)\n- **`autoindent` is ON even with `-u NONE`** → typed indentation *compounds*. Use a\n  tiny init with `set noautoindent nosmartindent nocindent indentexpr=`.\n- **A leftover swap file** makes nvim open the recovery prompt, which **silently\n  eats your keystrokes** → `set noswapfile`.\n- `:set paste` is **deprecated/no-op in nvim 0.11** — don't rely on it.\n\n### mdp (slideshow)\n- Slide transitions are **redraws, not clears** → `detect_clears` doesn't apply;\n  anchor the panel to the **predicted `Space` times** (we drive them, so it's exact;\n  no typing during the show ⇒ no drift).\n- **Anchor the ending to the VOICE**, not the terminal length, and **don't `q`**\n  (quitting shows the shell). Hold the last slide `END_HOLD` after the voice, then\n  fade — a fixed tail gave a 7s silent ending / risked clipping the last word.\n\n### Sync / J-cut\n- **J-cut belongs at the TRANSITION, not in-scene.** v1 (in-scene, voice ahead of\n  the command) desynced badly. Transition-level (incoming intro voice leads its\n  video into the previous scene's silent hold) is correct.\n- **Every scene must hold its still frame until ITS OWN voice finishes** (adaptive\n  `end_pad`), else a long-narration scene bleeds into the next clip. This and the\n  J-cut-overrun are two opposite failures on the same transition — both gated.\n- **Sparse output** (`fzf -f`, single-line results) makes the screen dip near-empty\n  mid-scene → `detect_clears` over-counts. Falls back to **guided** selection (pick\n  the N drops nearest the predicted scene times).\n\n### edge-tts / narration\n- It splits the SRT **1:1 on `。！？`** — so each `S()` must be ONE complete\n  sentence ending in `。！？`, no mid-sentence `。`. A mismatch aborts the build\n  (\"cue/sentence mismatch\").\n- Every `star`/`ord` panel token's flag text must appear **verbatim** in one of\n  that scene's `S()` sentences, or the reveal falls out of typing-sync.\n\n### macOS BSD vs Linux/GNU\n- Default `sed`/`awk`/`date`/`stat`/`grep` are **BSD/variant** and differ from\n  Linux (BSD `sed` silently ignores `\\w`/`\\b`, `\\t` in replacements; `df` has no\n  `-T`; etc.). Teach the **GNU variant** (`gsed`/`gawk`/`gdate`) or the strict POSIX\n  subset, and **probe deterministically with `engine/src/envcheck.py`** (the `/clip`\n  EnvCheck phase). Never show output that would differ on Linux without flagging it.\n\n### ffmpeg / rendering\n- This build has **no `libass`/`drawtext`/`subtitles`** → text (panel + subs) is\n  drawn as **Pillow PNGs and `overlay`-ed**; subtitles ship as a sidecar `.srt`.\n- Don't stack dozens of `overlay` filters — **flatten each layer to one video first**\n  (concat demuxer), then a single overlay pass (~12s vs minutes).\n- `ffmpeg` is aliased to `ffmpeg-bar`; scripts use the absolute\n  `/opt/homebrew/bin/ffmpeg`.\n\n### Claude Code hooks (session-recorder)\n- **No timestamp in the hook payload** — capture your own (`time.time()`).\n- **Every** payload carries `transcript_path` → the full conversation; the **Stop**\n  hook's **last assistant message** is the turn's conclusion (the headline to narrate).\n- Settings are read at **startup** → new hooks apply to a *fresh* session, not the\n  running one.\n- **FOCUS MODE HIDES hook `systemMessage`s.** A Stop hook that prints an on-screen\n  sentinel (the VHS turn-done marker) renders *nothing* if the session is in focus\n  mode → every `Wait+Screen /sentinel/` times out. This is why live recording\n  **must** use the isolated sandbox (`live/claude_sandbox.sh`), whose fresh\n  `CLAUDE_CONFIG_DIR` starts with focus off. Same dir also kills the Chrome-extension\n  dialog (`cachedChromeExtensionInstalled:false`), the unavailable-model banner\n  (`model:opus`), and MCP/CLAUDE.md noise. See `engine/experimental/session-recorder/live/`.\n- **`--dangerously-skip-permissions` for scripted runs** — `acceptEdits` auto-accepts\n  edits but a turn that *runs* what it wrote hits a Bash prompt and hangs the tape.\n\n### git / portability\n- **`build/` is in most global gitignores** → the provenance folder is named\n  **`provenance/`** (don't use `build/`).\n- Track source + small text provenance; **media (`*.mp4/.mp3/.srt/.html`) and\n  scratch (`intermediate/`, `.venv/`) are gitignored** and regenerate from the lesson.\n- Portability cost: each slug **vendors a copy** of `engine/src` → fix engine bugs\n  in `engine/` then propagate to slugs.\n- Two self-inflicted bugs worth remembering: a **shared `intermediate/`** broke\n  parallel renders (now per-folder); and `open(p,\"w\").write(open(p).read()…)`\n  **truncates before it reads** — always read first, then write.\n\n---\n\n## Roadmap — the \"majestic Claude Code tutorial\"\nWe have the teaching scaffolding. The remaining piece is filming Claude Code\n*itself* live. Plan (VHS-based, reuses everything):\n1. ✅ **Stop sentinel** — `live/vhs_stop_sentinel.sh` prints `VHS_TURN_DONE_N` on\n   Stop so VHS can `Wait` on a specific turn (complements `timelog.py`, which logs\n   the JSONL timeline but emits nothing on screen).\n2. ✅ **Session tape** — `live/gen_session_tape.py` launches `claude` in the\n   **isolated sandbox** (`live/claude_sandbox.sh`), drives prompts, and\n   `Wait@timeout /sentinel/` between turns (no guessed `Sleep`); VHS records the\n   real TUI → `terminal.mp4`. The sandbox is what makes this filmable at all —\n   see `live/README.md` for the four config traps it defeats (focus mode first).\n3. ✅ **overlay \"session mode\"** — `live/session_panel.py`: left terminal (1200px)\n   + a 720px explainshell panel. Launch flags are dissected appearing WITH the\n   voice; each turn shows its tool actions + the Stop conclusion. Anchored to\n   VIDEO-DETECTED timecodes, NOT the hook clock — the hook wall-clock drifts ~8s\n   from VHS video time over a session (non-uniform), so it can't be mapped; tool\n   events are mapped into each detected `[submit,done]` window by wall-clock\n   fraction. (A course-assembly/montage layer is a later nicety.)\n\n## Status\n14 portable clips rendered + verified PASS; engine + `/clip` workflow + 3 clip-type\nprototypes + the session-recorder all built and committed. See git log.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhtlin222%2Fclaude-session-recorder","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhtlin222%2Fclaude-session-recorder","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhtlin222%2Fclaude-session-recorder/lists"}