Skip to main content

hunk-launch-video

Produces Hunk videos by driving the real TUI headlessly in a PTY, compositing captioned 1080p frames in Chromium, and encoding with ffmpeg. Use for feature demos, workflow explainers, announcements, launch videos, and full-release roundups.

Datos de origen

Repositorio
modem-dev/hunk
Última actividad en el origen
19 de octubre de 2026 a las 21:22
Idioma detectado de SKILL.md
inglés
Estrellas
9683
Forks
323

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
name
hunk-launch-video
description
Produces Hunk videos by driving the real TUI headlessly in a PTY, compositing captioned 1080p frames in Chromium, and encoding with ffmpeg. Use for feature demos, workflow explainers, announcements, launch videos, and full-release roundups.
metadata
{"internal":true}
# Hunk video pipeline Source-checkout only: the pipeline lives in `scripts/launch-video/`, which never ships to npm. It can be used for pull-request demos as well as maintainer release videos. Unix-only — the capture scripts exec `/bin/bash`. Generates product videos where every terminal frame is the real Hunk TUI — no screen recording, no mockups. Three stages: ```text capture.ts (bun) drive Hunk over a PTY, snap styled keyframes to PNG compose.mjs (node + Playwright) render each PNG on a 1920x1080 HTML stage in Chromium ffmpeg encode the composited PNGs at 30fps to mp4/webm ``` Playwright controls headless Chromium: for each planned frame it loads the terminal PNG onto the HTML stage, applies the window chrome, cards, captions, and transition state, then screenshots the completed stage back to PNG. ffmpeg sequences those composited screenshots into the final videos. The generic machinery (PTY driving, keyframe rendering, storyboard planning, Chromium compositing, the stage template) is the `@hunk/term-video` workspace package in `packages/term-video/`; `scripts/launch-video/` holds only Hunk's scenes, captions, and cards on top of it. ## Choosing a recipe The pipeline is not release-specific. Choose the editorial scope, then use the same capture → composite → encode stages: - **Single feature:** a short demonstration of one capability or workflow. Use the single-feature recipe below and capture only the required scene. - **Full release:** the social promo for a release, in Hunk's house style (`scripts/launch-video/promo/`). Use the full-release recipe: write one storyboard file for the release; the style kit stays unchanged. - **Custom video:** author any set of scenes and `SHOTS` for tutorials, comparisons, announcements, or workflow explainers; follow the scene and storyboard rules below. ## Creating a video Expect ~3–6 min for capture and ~2–4 min for compose — run both with a long timeout (or in the background); each logs per-snap / per-shot progress. `compose.mjs` needs node ≥ 18 on PATH (bun alone is not enough). ```sh # 0. dependencies (tuistory is a devDependency; ghostty-opentui arrives transitively) bun install # if a postinstall hook fails in a sandbox, retry with --ignore-scripts # 1. capture the current storyboard's default scenes. Output defaults to # <repo>/.video-work/ regardless of cwd (pass a path argument to override), # but the PROCESS must run from the repo root — see gotchas. bun run scripts/launch-video/capture.ts # 2. one-time portable compositor setup. Playwright installs a Chromium build # that exactly matches its browser driver (see gotchas to reuse a system or # sandbox browser instead). printf '{"name":"hunk-video-work","private":true}\n' > .video-work/package.json cd .video-work bun add playwright playwright-core bunx playwright install chromium cd .. # 3. composite the storyboard node scripts/launch-video/compose.mjs .video-work # 4. encode cd .video-work ffmpeg -y -f concat -safe 0 -i concat.txt -vf "format=yuv420p" -r 30 \ -c:v libx264 -preset slow -crf 18 -movflags +faststart launch.mp4 ffmpeg -y -f concat -safe 0 -i concat.txt -vf "format=yuv420p" -r 30 \ -c:v libvpx-vp9 -b:v 0 -crf 32 -row-mt 1 launch.webm ``` Override the default scene set to iterate on one or more scenes: ```sh SCENES=review bun run scripts/launch-video/capture.ts # comma-separated scene names ``` Scene names are the `wants("...")` guards in `capture.ts`'s `main()`. `SCENES=` replaces the canonical default scene set; `compose.mjs` preflights that every frame its `SHOTS` table references exists in `frames/` and fails fast listing any missing ones, so a full composite still needs every referenced scene captured at least once. ## Single-feature recipe For a short test, demo, or one-feature announcement, capture and composite only the scene for that feature: 1. Pick one user-visible capability and find its scene name in the `wants("...")` guards. If it does not have a scene, author one using the guidance below. Capture only that scene: ```sh SCENES=review bun run scripts/launch-video/capture.ts ``` 2. Make a scratch compositor beside the canonical one so its imports and repo-relative paths continue to work: ```sh cp scripts/launch-video/compose.mjs scripts/launch-video/compose-one-feature.mjs ``` 3. In the scratch copy, trim `SHOTS` to an opening card, only the selected feature's frames, and an outro card. Rewrite those cards and captions for the scoped cut. Sequence lengths must still match the captured frame names. 4. Composite into the same work directory, then run the normal ffmpeg commands with descriptive output names: ```sh node scripts/launch-video/compose-one-feature.mjs .video-work cd .video-work ffmpeg -y -f concat -safe 0 -i concat.txt -vf "format=yuv420p" -r 30 \ -c:v libx264 -preset slow -crf 18 -movflags +faststart hunk-feature-demo.mp4 ffmpeg -y -f concat -safe 0 -i concat.txt -vf "format=yuv420p" -r 30 \ -c:v libvpx-vp9 -b:v 0 -crf 32 -row-mt 1 hunk-feature-demo.webm cd .. rm scripts/launch-video/compose-one-feature.mjs ``` Keep the scratch compositor uncommitted. The canonical `compose.mjs` remains the checked-in reference storyboard. If you added a capture scene only to make PR evidence, revert that scene after encoding; retain it only when it is useful checked-in demo coverage and belongs to the submitted change. ## Full-release recipe Release videos use the promo style kit in `scripts/launch-video/promo/`, so every release looks like part of one series. The kit renders through `@hunk/term-video/timeline`: ```text storyboards/<version>.mjs release content: chapters, captions, montage, credits range engine.js + stage.html the style: sting, countdown hook, chapter cards, terminal scenes, montage, contributors h, outro, transitions contributors.mjs git range -> contributors.json + avatars/ render.mjs storyboard + frames + contributors -> promo-<version>.mp4 ``` 1. **Pick the headlines.** Read the release section in `CHANGELOG.md` (or its hand-written **Highlights**) and choose 4–6 chapters ordered by impact: new capabilities first, then visible UI/UX changes, then smaller conveniences. Per-PR entries that are too small for a chapter go in the montage. Check that each claim is accurate: a Patch-level fix can earn a chapter, but name what it does rather than calling it new. Confirm the shortlist with the user before capturing. 2. **Start the storyboard** by copying the newest one, for example `cp storyboards/0.24.0.mjs storyboards/0.25.0.mjs`. Set `version`, rewrite `chapters` and `montage`, set `montage.fixes.count` from the changelog's patch entries, and point `contributors.from`/`to` at the previous release tag and this release's tag. Leave `aliases` from older releases out unless they still apply. 3. **Capture** each chapter's keyframes, reusing or adding scenes in `capture.ts` (see "Authoring scenes"); every `frames[].img` must name a captured keyframe. `SCENES=a,b bun run scripts/launch-video/capture.ts`. 4. **Credits.** Fetch tags and history (`git fetch --tags --unshallow` in a shallow clone), then run `node scripts/launch-video/promo/contributors.mjs .video-work`. Review the printed table: everyone who authored or co-authored a commit in the range appears once, bots and AI co-author trailers excluded. Map unresolved people to logins with `contributors.aliases` (email or name to a login, or `{ login, id }` when the GitHub users API is unavailable) and rerun; anyone still unresolved is credited by name with an initials tile. Never guess an account. 5. **Work dir.** Besides `playwright-core` (see gotchas), the kit needs the brand font: `cd .video-work && bun add @fontsource-variable/jetbrains-mono`. 6. **Stills first.** `node scripts/launch-video/promo/render.mjs .video-work --stills 2.6,8,14,...` renders review frames in seconds; check every chapter's callouts and captions before the full render. `render.mjs` validates the storyboard and lists any missing keyframes up front. 7. **Render** with `node scripts/launch-video/promo/render.mjs .video-work` (about 3 minutes for a minute of video) and verify the cut (see "Verification"). When `hunk-release` invoked this workflow, return the approved MP4 to that skill for versioned naming and embedding; do not upload or edit the public release without its confirmation gate. ### Chapter reference Times are seconds from the start of the chapter's terminal scene; camera, highlight, and label coordinates are fractions of the captured frame. - `label`, `title` (lines; prefix a word with `*` to color it), `sub`, `cmd` (typed into the title card's command chip), `winTitle`. - `frames: [{ img, dur, key?, fade? }]` plays keyframes in order. `key` shows that keycap pressing as the frame appears, so one frame per keypress reads as typing; `fade` crossfades from the previous frame (use it for layout swaps). The `steps()` helper in the 0.24.0 storyboard builds key-walk runs. - `cameras: [[t, { x, y, scale }, motionSeconds?]]` must start at time 0. - `highlights: [{ t0, t1, x, y, w, h }]` and `labels: [{ t0, t1, x, y, text }]` outline and annotate source regions; keep edges in blank rows and labels off terminal text (see "Camera and callout composition"). - `captions: [[t, html]]` with `<b>` for the accent color. - `bodyH` gives short shell output a compact window instead of a full screen. ### Extending the style Keep release copy out of `engine.js`: it reads everything release-specific from the storyboard, and its defaults hold only brand constants (tagline, install command, site). When a release needs something the kit cannot show, add a reusable scene type or storyboard field to the engine and document it here rather than special-casing one release. The brand geometry in the engine follows `website/public/brand/`; update both together. The caption storyboard below (`compose.mjs` on the shared stage) remains the tool for single-feature demos, PR evidence, and custom explainers. ## Per-video editorial surface This section covers the caption storyboard used for single-feature, PR, and custom videos; release promos keep their content in `promo/storyboards/`. The capture machinery is reusable, but the storyboard is editorial content for one video. Rewrite it to match the video's scope. The checked-in reference is the 0.24 release roundup, not a frozen release artifact: - `compose.mjs`: the whole `SHOTS` table; opening, feature, and outro cards; every caption; camera targets; and callout rectangles. A `NEW` badge is a claim about the video at hand, so drop or move it as features age. - `capture.ts`: `SCENES` selects from a reusable scene library. With no override it captures only the current storyboard's default scene; update that default whenever the canonical storyboard changes. Hunk-side glue (`launchHunk`, `launchHunkShell`, demo repositories, and the keyboard probe) remains reusable. Reusable machinery lives in `@hunk/term-video` (`packages/term-video/`) — extend it there, don't fork it into the scripts: `createKeyframer`, `launchApp`/`launchShell`, `createCommandWrapper`, `typeCommand`, `ensureKeyboardIsLive`, `makeSceneFilter` (`src/capture.ts`); the unit-tested storyboard planner with the caption/timing semantics (`src/plan.mjs`); `composeStoryboard` with font/Chromium resolution and the missing-keyframe preflight (`src/compose.mjs`); the stage template (`src/stage.html`); and `renderTimeline` for free-form motion pages such as the promo kit (`src/timeline.mjs`). ## Environment gotchas Sandbox-specific bullets are marked; each cost real debugging time. - **Run `capture.ts` with bun from the repo root.** tuistory uses subpath self-imports (`tuistory/pty`) that only resolve inside this repo's `node_modules`; running the script from elsewhere resolves tuistory from bun's global cache and crashes. (Output location is unaffected — it defaults to `<repo>/.video-work/` via `import.meta.url`.) - **ghostty-opentui is a transitive dep** (via tuistory) with an exports map: import `"ghostty-opentui/image"` (not `.../dist/image.js`), resolved relative to tuistory — `@hunk/term-video/capture`'s `createKeyframer` does the `Bun.resolveSync` dance. - **Playwright must match the Chromium it drives.** The portable setup above installs `playwright` and `playwright-core` together, then downloads their matching Chromium build. It works on macOS and Linux and is preferred when bandwidth and browser downloads are available; leave `CHROMIUM_PATH` unset so Playwright uses that managed browser. On Linux, if Chromium reports missing system libraries, run `bunx playwright install-deps chromium` (it may require sudo) before retrying. To reuse an existing system, CI, or sandbox Chromium instead, install the `playwright-core` version provided by that environment and set its executable explicitly: ```sh cd .video-work bun add playwright-core@<matching-version> cd .. CHROMIUM_PATH=/path/to/chromium node scripts/launch-video/compose.mjs .video-work ``` Common executable locations include `$(command -v chromium)` or `$(command -v google-chrome)` on Linux and `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome` on macOS. In an environment with a preinstalled Playwright toolchain, read that toolchain's `package.json` to get the exact driver version. For example, the Anthropic sandbox exposes it through `/opt/pw-browsers/.links/*`: ```sh cat "$(cat /opt/pw-browsers/.links/* | head -1)/package.json" | grep '"version"' # e.g. "1.56.1" -> bun add playwright-core@1.56.1 ``` `compose.mjs` picks its browser as `$CHROMIUM_PATH`, then `/opt/pw-browsers/chromium` when present, then Playwright's managed browser. If a dependency update leaves Playwright asking for an uninstalled browser revision, either rerun `bunx playwright install chromium` in `.video-work/` or set `CHROMIUM_PATH` to a known system browser such as `/usr/bin/chromium`. - **Give `.video-work/` its own `package.json` before `bun add`.** Without one, bun walks up and installs into the repo's `package.json` — revert with `git checkout package.json bun.lock` if that happens. - **Chromium needs `--allow-file-access-from-files`** (already in `compose.mjs`): the stage samples each keyframe through a canvas to color-match the window background, and file:// images taint the canvas without it. - **mp4 needs an ffmpeg with libx264.** Sandbox: `apt-get install ffmpeg` (run `apt-get update` first if packages 404). macOS: `brew install ffmpeg`. Verify `ffmpeg -encoders | grep -E 'libx264|libvpx-vp9'` shows both before encoding — playwright's bundled `ffmpeg-*/ffmpeg-linux` only does VP8/WebM and cannot produce the mp4. - **Caption font**: JetBrains Mono ships inside ghostty-opentui; `findCaptionFont` in `packages/term-video/src/compose.mjs` searches bun's isolated layout (`node_modules/.bun/node_modules/…`) then a hoisted `node_modules/…`. If it still throws `caption font not found`, locate the file with `find node_modules -name jetbrains-mono-nerd.ttf` and pass it as `fontPath` to `composeStoryboard`. ## Authoring scenes (capture.ts) - Shared geometry is 140x32 cells rendered at fontSize 16 / dpr 2 → 2688x1536 PNGs. Keep every scene at this size so all frames fit one window. - Keep time-sensitive fixtures recent and timezone-stable. The canonical history scene derives commit dates from the current UTC day and launches Hunk with `TZ=UTC`, so day groups and relative-age labels remain useful on future runs. Preserve that policy in custom history scenes. - Helpers: `createDemoRepo()` and `launchHunkShell()` are hunk-side glue in the script (git repo built from `examples/2-mini-app-refactor`; interactive bash with a real `hunk` command on PATH and a clean `❯` prompt); `snap`, `typeCommand`, `launchApp`/`launchShell`, and `createCommandWrapper` come from `@hunk/term-video/capture`. - Always `waitForText` on scene-specific content before the first snap, and call `ensureKeyboardIsLive()` before scripted keypresses — the first key after startup can be dropped (real race, the helper toggles `?` to prove keys land). - **Animation = one snap per keypress.** Cursor walks and typing effects are just every `j`/`k`/character captured as its own frame and played back at 0.2–0.3s per frame. Prefer this over sparse keyframes: three stills read as a slideshow, per-press frames read as motion. - `renderTerminalToImage` auto-trims trailing blank rows, so short outputs (CLI scenes) produce short PNGs — the stage handles this by sampling the image's bottom-left pixel and painting the window body to match. - `manifest.json` is a capture-side inventory of the _current run_ only; `compose.mjs` ignores it (frames resolve by name from `SHOTS`), and after a `SCENES=` run it is partial while `frames/` stays cumulative. - Demo content that must exist: STML notes come from `examples/9-agent-markup-notes` (launch with `--experimental`), extension scenes from `examples/extensions/` loaded via `--extension <path>` (explicit paths skip the repo trust prompt). The pager pipe is `git diff | hunk pager` — bare `hunk` on piped stdin prints help. Sidebar toggle is `s`; comment draft is `c`, save with Ctrl+S (`\x13`). ## Storyboard model (compose.mjs) - `SHOTS` is the whole edit: one entry per shot, `dur` in seconds, played as unique frames + per-frame durations in an ffmpeg concat list (holds cost one frame, so runtime is dominated by transitions, not length). - `capKey` is caption identity: the caption slides in only when `capKey` changes, and continuation shots that share a `capKey` without restating `caption` keep the previous caption on screen. Sequences (walks, typing) are generated with `Array.from` spreads. - **Sequence lengths must match capture loop bounds**: the open capture's four-step tree walk plus its select and expand presses produce
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub