---
name: pollinations/polli-video
source: https://app.decimal.ai/s/pollinations-polli-video@1/SKILL.md
source_sha256: f3462b60277e
---

# polli-video — record polli CLI demos

VHS records silent terminal video. Polli generates audio. ffmpeg merges. VHS cannot capture system audio — either capture it live via BlackHole, or overlay narration in post with `adelay`.

## When to use

- Demo video for polli CLI, pollinations.ai, or terminal workflows
- LinkedIn / social clip with voiceover and lofi background
- Iterating on an existing `.tape` script

## Quick start

```bash
mkdir -p temp/vhs-demo && cp .claude/skills/polli-video/example/* temp/vhs-demo/
cd temp/vhs-demo
# Set system default output → BlackHole 2ch (see Pipeline A)
./render.sh
```

Produces `demo.mp4`. Edit `demo.tape` to change content.

## Pipeline A — BlackHole live capture (recommended)

Single command, no `adelay` math, perfect sync. Requires [BlackHole 2ch](https://existential.audio/blackhole/) and default system output set to it (Option+click menu-bar speaker, or `SwitchAudioSource -s "BlackHole 2ch"`).

See `example/render.sh` — archives old renders, captures while VHS runs, muxes trimmed to video length with a 5s audio pre-roll shift (ffmpeg startup + API roundtrip offset) and a 3s audio fade-out at the end. Copy and adapt.

**Capture tool: `sox` over `ffmpeg avfoundation`.** ffmpeg dropped samples on long recordings (choppy audio) even with `-thread_queue_size 4096`, `-async 1`, `nice -20`. sox + coreaudio is stable end-to-end, addresses device by name ("BlackHole 2ch") instead of a flaky index, and needs no post-trim:

```bash
AUDIODRIVER=coreaudio sox -q -c 2 -r 48000 -t coreaudio "BlackHole 2ch" \
    -c 2 -r 48000 -b 16 captured.wav 2>/tmp/sox-capture.log &
```

Music gen must use `--play` so BlackHole captures it. Switch default output back to Speakers after.

## Pipeline B — post-hoc ffmpeg overlay (fallback)

When BlackHole unavailable. Pre-generate narration + music, overlay with manual timestamps:

```bash
vhs demo.tape   # produces demo-silent.mp4
ffmpeg -y -i demo-silent.mp4 -i speech.mp3 -i music.mp3 \
  -filter_complex "[1:a]adelay=16000|16000[narr]; \
                   [2:a]volume=0.18,atrim=end=75[bg]; \
                   [narr][bg]amix=inputs=2:duration=longest[a]" \
  -map 0:v -map "[a]" -c:v copy -c:a aac demo.mp4
```

`adelay=16000|16000` = ms, `left|right` (must be stereo pair). Tune to tape timestamp where narration should hit. Ducking: `0.12–0.20` under narration, `0.25–0.35` music-only. **Never pass `-shortest`** — truncates video to shortest audio.

## VHS tape conventions

- **`Set Shell "bash"` is mandatory.** zsh + long `Type` lines triggers a VHS command-concatenation bug: two consecutive long commands get glued onto a single prompt line and the `Enter` between them is lost. Root cause is zsh's ZLE + VHS's wall-clock-timed CDP keystroke injection (no prompt-wait primitive). Bash's simpler line editor has no such race. Not fixable with `Wait+Line` (regex fails on colored prompts), `precmd` hooks ([VHS #691](https://github.com/charmbracelet/vhs/issues/691)), longer `Sleep`s, or `Ctrl+L` substitution. Just use bash.
- 960×720 or 960×640, Menlo 16pt — reads on mobile, fits `polli gen text --model X "..."`.
- Absolute path in `Hide`-block `cd`; relative path in `Output` (parser splits on `/`).
- Always `export FORCE_COLOR=1` in `Hide` — VHS pty fails `isTTY`, chalk strips colors otherwise.
- Scene titles via ANSI from shell (no VHS primitive). `\033[1;38;5;141m` = polli purple.
- `announce()` pattern: one function that prints the bold title AND fires backgrounded TTS. Every later scene is a one-liner. See `example/demo.tape`.

**Canonical bash `Hide` block** (copy verbatim — fixes job notices, heredoc prompts, prompt trailing-line):

```
Set Shell "bash"
Hide
Type "cd /absolute/path/to/working/dir"
Enter
Type "export FORCE_COLOR=1"
Enter
Type "export PS1='» '"
Enter
Type "export PS2='» '"
Enter
Type "PROMPT_COMMAND='echo'"
Enter
Type "set +m"
Enter
Type "clear"
Enter
Show
```

- `PS2='» '` — hides heredoc/continuation `>` prompts that leak on wrapped `Type` lines.
- `PROMPT_COMMAND='echo'` — trailing blank line between scenes so output doesn't crash into the next prompt.
- `set +m` — disables job-control, suppresses `[1] 12345` notices from `&` backgrounding.
- Wrap backgrounded commands in a subshell anyway: `( cmd & )` never prints a PID even in interactive shells.

## The `announce` function

One-liner per scene: prints a bold purple title AND fires backgrounded TTS. The `sed` strip removes ElevenLabs emotion cues (`[whispers]`, `[excited]`, …) from the *printed* title while keeping them in the *spoken* audio.

```bash
announce() {
  printf '\n\033[1;38;5;141m»» %s\033[0m\n\n' "$(sed -E 's/\[[^]]*\] *//g' <<< "$1")"
  ( polli gen audio --play --output /tmp/narr-$$-$RANDOM.mp3 "$1" >/dev/null 2>&1 & )
}
```

Usage:

```
Type `announce "[excited] first — we need some backing music"`
Enter
```

**ElevenLabs emotion cues** (must use ElevenLabs-backed voice, e.g. default `sage`): `[whispers]`, `[excited]`, `[confident]`, `[curious]`, `[sighs]`, `[laughs]`. Place inline — the TTS engine interprets them; the sed strip keeps the on-screen text clean.

**Why subshell `( cmd & )`, not bare `&` or `disown`:**
- Bare `&` in a non-interactive bash (VHS) still prints `[1] 12345` — even with `set +m`, occasional leakage.
- `disown` after `&` still shows the job-start notice.
- `( cmd & )` spawns the background job inside a subshell that exits immediately, so no job record is ever attached to the parent shell — nothing to print.

## Gotchas

| Symptom | Cause | Fix |
|---|---|---|
| Two commands merge on one line, `Enter` eaten | zsh + long `Type` (ZLE race) | `Set Shell "bash"` |
| `heredoc>` or `>` prompts visible | Wrapped line, zsh/bash PROMPT2 | `export PS2='» '` |
| `[1] 12345` leaks on screen | Bash job-control notice | `set +m` + `( cmd & )` subshell |
| Scene collides with previous output | No trailing newline | `PROMPT_COMMAND='echo'` |
| Nested `temp/vhs-demo/temp/vhs-demo/` | Absolute path in `Output` | Use relative; `cd` in `Hide` |
| Typing takes forever | Default TypingSpeed 50ms | Set `12–20ms` |
| Colors stripped | VHS pty, no FORCE_COLOR | `export FORCE_COLOR=1` |
| Font renders wrong | JetBrains Mono not on macOS | Use `Menlo` |
| FontSize >18 wraps prompts | 960 width | Stick with 16 |
| BG output corrupts next command | `&` with stdout live | `cmd >/dev/null 2>&1 &` |
| `↑` / multibyte char eats adjacent chars | VHS `Type` + Unicode edge case | Drop the arrow; use plain ASCII |
| Choppy/dropped audio in long capture | ffmpeg avfoundation | Use `sox -t coreaudio "BlackHole 2ch"` |
| Video only 2s long | `-shortest` on ffmpeg | Drop `-shortest` |
| No sound in final mp4 | Missing audio map | `-map 0:v -map "[a]"` |
| BlackHole not found by ffmpeg | Wrong device index | Use sox + device name instead |
| Disk fills during capture | Uncompressed wav, forgot to trap | `trap "kill $FFPID" EXIT` |
| Final MP4 narration lands too late | ffmpeg startup + API pre-roll | In mux: `ffmpeg -ss 5 -i captured.wav …` (tune 3–6s) |
| Music cuts abruptly at end | No fade filter | Add `-af "afade=t=out:st=$((VID_DUR-3)):d=3"` |
| `afade` filter errors `No option name near '3'` on `st=66,36` | Locale uses comma for decimals | Wrap the `awk` computing `FADE_START` with `LC_ALL=C` |
| Punctuation/wording cached across renders | Pollinations text cache keys on exact string | Change a character (swap `·` ↔ `;` ↔ `,`, add `!`) each re-render |

## Voice selection

Default: `sage`. Others: `fin`, `callum`, `onyx`, `rachel`. Preview:

```bash
polli gen audio --voice <name> --output sample.mp3 "test line" && afplay sample.mp3
```

Full list: `polli models --type audio --json | jq '.[].voices'`.

## Music (elevenmusic)

Deterministic cache — same prompt+duration = same bytes. Always `--instrumental`. Match duration to `ffprobe demo-silent.mp4`. Example prompt: `"lofi instrumental hip hop, warm analog, jazzy bass"`.

## Demo design principles

- Open silent with `polli --help` — orients without narration.
- One audio moment per scene, not many.
- Hold scenes: help ~4s, streaming text 10–20s, ASCII outro 15–17s.
- Payoff produces something reusable (the post, an image, a doc).
- Streaming > buffered — lean into polli's default streaming reveal (drop `--no-stream` when used in a recorded tape so the audience sees generation happening live).
- End with a punchy ASCII wordmark outro (see "Streaming outros" section).
- 960×720 or 960×640 beats 1920×1080 on mobile feeds.

## Brand voice for generated content

See `social/prompts/tone/linkedin.md`, `social/prompts/brand/about.md`.

- Dry, information-dense, anti-corporate.
- No "excited to announce", "game-changing", hashtag spam.
- Plain text for LinkedIn (markdown renders raw).

## Generating the announcement post (lessons)

**Model selection (ranked for this task):**
- `claude-fast` — **pick this for live demos.** Consistent ~4s latency, never leaks scaffolding in this prompt shape. Voice slightly flatter than glm but reliability wins when a `Sleep 30000ms` hold has to cover the call.
- `glm` — best voice quality when it works. But latency is wildly variable (12s first call → 31s next → sometimes >60s); not safe behind a fixed `Sleep` budget. Use for offline one-shots, not live-tape renders.
- `kimi` — strong rhythm, hacker-genz feel. Solid backup.
- `openai` — leaks prompt scaffolding verbatim (e.g. `sign-off:`, `110w`, `your own line-1 hook`), overshoots word count. Avoid.
- `gemini-search` / `perplexity-fast` — web-search adds citation markers `[1][2]`; hallucinate features, inflate corpo voice. Avoid for this.

**Call flags:**
- Use `--no-stream`. Streaming + `> file` hangs on long prompts; `--no-stream` returns in 3–5s.
- Never wrap polli in `timeout N` — it breaks the stdout flush and always kills the call.

**Input context (what to pipe in):**
- `polli --help` only — leanest, forces the model to describe the command surface without repackaging README bullet points.
- Feeding the README causes models to repeat README-specific phrases ("humans, AI agents, and everything in between") and hallucinate flags (`--budget` etc).
- `--help` limitation: doesn't expose npm package name → model guesses `polli-cli`. Patch install line manually.

**Prompt shape (compressed, declarative, no imperative wordiness):**
- State the task (`announce polli on LinkedIn`), not a procedure.
- Voice in 1 line with `·` separators. Let the model interpret rather than listing banned phrases.
- Don't put counted claims in the prompt (`7-verb CLI`) — every model echoes them. Just list the verbs.
- Don't say "scroll-stopper" or "your own line-1 hook" — models paste those labels literally.
- Keep `no md · no 🚀 · no corpo` as the only negative constraints. More bans → defensive, boring output.
- Ask for `sign-off` not `CTA`.

**Final prompt used (LinkedIn launch post):**

```
↑ polli --help for polli CLI.
task: announce polli on LinkedIn. reformat this into a post.
voice: hacker-genz .nfo irc-drop · dry · emojis 🐝💾⚡🔮🧪👁️🌀 scattered
100-130w · sharp sign-off · no md · no 🚀 · no corpo
```

Pipe in via: `polli --help | polli gen text --model claude-fast --no-stream "<prompt>"`.

**For live tape renders** (where a `Sleep Xms` must cover the call): use `claude-fast` — ~4s predictable. For offline post generation without timing constraints, `glm` gives slightly better voice but `polli --help | polli gen text --model glm --no-stream …` may take 10–60s.

**Parallel batching gotcha:** 3+ concurrent calls to the same model hit Cloudflare 520. Run sequentially for reliable batches.

## Streaming outros (ASCII demoscene)

`polli gen text` streams by default — no `animate` helper or frame splitter needed. Pipe straight to the terminal and the characters appear as generated:

```
Type "clear"
Enter
Sleep 400ms
Type `polli gen text --model claude "ascii frame 50×20; spell POLLI big block letters centered. organic evolving mycelium decor — demoscene."`
Enter
Sleep 17000ms   # hold long enough for full generation + read time
```

**Prompt engineering notes for ASCII wordmarks:**
- Small prompts work best. Every added constraint (CA rules, 6 frames, palette, emoji) pushes the model away from the actual word. Keep it one sentence.
- Strict letter constraints ("P O L L I — two L's") drift anyway with claude-fast. Use `claude` (non-fast) for final renders; `claude-fast` often outputs "POLL" / "PELL" / random block shapes.
- Canvas 50×20 is the sweet spot for 960-wide terminals at FontSize 16. 60×40 overflows vertically.
- No color/palette mentions — ASCII is monochrome. Confuses the model.
- One keyword for "evolving" (e.g. `mycelium`, `cellular automaton`) > a full description. But `cellular automaton` sometimes confuses the model into abandoning the word entirely — `mycelium` is more reliable.

## Layered emotion cues in announce()

ElevenLabs handles multiple `[cue]` markers in one line, changing tone mid-sentence. Useful for punchy payoff lines:

```
announce "[playful] confused? just ask your agent to [whispers] install me... [curious] install me? [excited] install me!!"
```

The `sed` strip removes all cues from the printed text, so the screen just shows `»» confused? just ask your agent to install me... install me? install me!!` while the audio layers whisper → curious → excited across three repetitions. The `...` and `?` / `!!` punctuation gives ElevenLabs natural beats to change register.

## Files

- `example/demo.tape` — canonical working tape (polli launch demo, 5 scenes incl. streaming ASCII outro, bash, `claude`)
- `example/render.sh` — canonical pipeline (sox BlackHole capture + mux with 5s audio pre-roll + 3s fade-out)
- `example/.gitignore` — ignores `*.mp4`, `*.wav`, `music.mp3`, `speech.mp3`, `.DS_Store`, timestamped archives

Render artifacts (regenerable, gitignored): `demo.mp4`, `demo-silent.mp4`, `captured.wav`, `music.mp3`, `speech.mp3`. Previous renders are auto-archived to `<base>-vHHMM.<ext>` by `render.sh`.