explainer · pika-labs — Claude Code skill: what it does, how to install
Turns any URL into a 60-80 second video: a real browser tours the page while an avatar with lipsync narrates what is on it.
npx skills add Pika-Labs/Pika-Plugins --skill explainer -a claude-codeWhat explainer does
Run /pika:explainer <url> and the skill drives a real browser through the page on a beat-sheet timeline, with an avatar that lipsyncs the narration. The result sits in a 1280×800 macOS Sonoma frame. The avatar is a 246-pixel circle at the bottom left, and the camera zooms onto a specific element at each middle beat.
A GitHub repo URL switches on a repo mode that scans the README and looks for a live demo. Any other URL gets a generic page walkthrough. --focus sets the angle, and --lipsync-provider pika|kling picks the lipsync engine. Captions are on unless you pass --no-captions.
Runs cost money. Before any paid call the skill shows your balance and an estimate of 50-500 credits (~$0.50-$5.00), and nothing is generated until you reply proceed. Lipsync with pika takes roughly 2-5 minutes, while kling takes 5-30.
What it helps with
- make a video walkthrough of a GitHub repo from its README
- show how a product page looks and works in a short clip
- tour a docs site with a voiceover
- turn a blog post into a short talk with a presenter
- find a repo's live demo and put it on screen
Example prompts for the agent
- Make a video walkthrough of this page:
/pika:explainer <url> - Explain this repo in a clip and focus on installation:
/pika:explainer <url> --focus "installation" - Run
/pika:explainer <url> --lipsync-provider kling --no-captions, I want no subtitles
How to install explainer
1Skills CLI (Vercel)
npx skills add Pika-Labs/Pika-Plugins --skill explainer -a claude-code2Manually: put the files in place
~/.claude/skills/explainer/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/explainer/
1Skills CLI (Vercel)
npx skills add Pika-Labs/Pika-Plugins --skill explainer -a codex2Manually: put the files in place
.agents/skills/explainer/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add Pika-Labs/Pika-Plugins --skill explainer -a cursor2Manually: put the files in place
.cursor/skills/explainer/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install Pika-Labs/Pika-Plugins explainer2Manually: put the files in place
.github/skills/explainer/SKILL.md1Skills CLI (Vercel)
npx skills add Pika-Labs/Pika-Plugins --skill explainer -a gemini-cli2Platform CLI
gemini skills install https://github.com/Pika-Labs/Pika-Plugins --path skills/explainerManually: put the files in place
.devin/skills/explainer/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add Pika-Labs/Pika-Plugins --skill explainer -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about explainer
How do I install explainer in Claude Code?
Run npx skills add Pika-Labs/Pika-Plugins --skill explainer -a claude-code. The skill ends up at ~/.claude/skills/explainer/SKILL.md.
What does explainer do?
It tours a URL in a real browser, narrates it with a lipsynced avatar, and assembles a 60-80 second clip in a macOS Sonoma frame. For GitHub links it also reads the README and looks for a live demo.
How much does one explainer run cost?
The skill estimates 50-500 credits (~$0.50-$5.00), depending on lipsync provider, narration, captions and preview mode. Paid calls begin only after you reply proceed.
Source text
SKILL.md · Apache-2.0
---
name: explainer
description: >-
Use when the user asks for explainer or a task matching the examples below.
~60-80s explainer video for any URL — GitHub repo, product page, docs site, blog post, or launch. Canonical workflow for URL walkthroughs. Use when the user asks to "explain this URL / repo / website / product", "make a walkthrough video for [url]", "demo this site", "Loom-style explainer of [url]", "explainer for github.com/...", or "explain this product link". Drives a real browser through the URL, generates an avatar lipsync, and composites in a 1280×800 macOS Sonoma frame with a 246-pixel bottom-left avatar circle. GitHub URLs activate a repo-aware mode (README scan + live-demo detection); other URLs use a generic page-walkthrough flow.
argument-hint: <url> [--focus angles] [--avatar url] [--voice id] [--live-url url] [--lipsync-provider pika|kling] [--no-captions] [--preview]
---
# /pika:explainer
Generate a ~60–80s URL explainer video: drive a real browser through the URL along a beat-sheet timeline, generate an avatar lipsync of the narration, and composite it all in a 1280×800 macOS Sonoma frame with a 240-pixel inner avatar (246-pixel outer including 3px white stroke ring) at canvas (20, 476) and element-targeted zoom on every mid-section beat. Works on any URL — product pages, docs sites, blog posts, launches. GitHub URLs activate a repo-aware mode (README scan + live-demo detection); all other URLs use a generic page-walkthrough flow.
**Usage:** `/pika:explainer <url> [--focus "angles"] [--avatar <url>] [--voice <id>] [--lipsync-provider pika|kling] [--preview] [--live-url <url>]`
## Cost transparency gate
Before any paid MCP call, call `identity_balance({verbose: true})` once. Surface the current balance, recent burn rate, and remaining runway, then gate the run with this exact message:
> Estimated cost: about 50-500 credits (~$0.50-$5.00) depending on lipsync provider, narration, captions, and preview mode. This can reach $5, so Reply `proceed` to continue or `cancel` to stop.
Do not call any paid MCP tool until the user replies `proceed`. If the user replies `cancel`, stop without generating. The gate runs after the URL and optional flags are known, before avatar generation, speech, lipsync, captions, or video composition.
## Behavior
### Defaults — fire fast, no unnecessary mid-flow confirmation
- **Resolve avatar / voice silently.** Never ask "should I use your avatar?" or "which voice?" before firing. Honor explicit overrides (`--avatar`, `--voice`) when supplied; otherwise generate a presenter avatar and pick a default voice, and proceed. See Step 1 for the full resolution waterfall.
- **Only the cost transparency gate asks for `proceed`.** Step 5 preview is normally **opt-in via `--preview`** for explicit avatars, but it becomes mandatory auto-preview when the avatar source is a `generated` or `regenerated` fallback. After the cost gate, the flow runs end-to-end except for that required fallback-avatar preview guardrail.
- **Do not solicit `--focus` either.** Make a confident first attempt from page structure; users re-run with `--focus "X"` if the angle missed.
These defaults match industry standard for media-gen tools (Midjourney / Sora / Runway / HeyGen / Pika.art): submit → render → return. Account credit balance + provider failover (Step 9) are the canonical guardrails.
### Local avatar images on Claude Desktop
Claude Desktop can't pass inline-pasted images to MCP tools yet (Anthropic-side limitation). If the user pastes a photo inline, or mentions a local file they want as `--avatar`, pause Step 1 and kindly send them this — something like:
> Heads up — pasted images don't reach MCP tools on Claude Desktop yet (Anthropic limitation). Two easy options for your avatar:
>
> - **Paste a URL** if it's already hosted (Imgur, S3, your site) — fastest
> - **Attach the image file** so I can upload it before generation.
When a local file arrives, convert it to a public URL with `upload_asset` and use the returned `public_url` as `--avatar <url>` before Step 1. Already-hosted `https://...` URLs work as-is and skip this entirely. If no avatar is supplied at all, a presenter portrait is generated silently (Step 1).
### Step 0 — Resolve URL (empty-args menu)
Strip flags (`--focus`, `--avatar`, `--voice`, `--live-url`, `--lipsync-provider`, `--no-captions`, `--preview`, `--skip-preview`, `--yes`) and `key=value` parameters from `$ARGUMENTS`. **If what remains contains no `https://...` URL** (or is empty / whitespace-only), print this menu **verbatim** as your full response, then **stop and wait for the user's next message**. Calling a tool here risks recording or explaining the wrong page. If `$ARGUMENTS` already carries a URL, skip this step silently and proceed to Step 1.
> **Which URL would you like me to walk through?** Works on any of:
>
> - **A GitHub repo** — e.g. `https://github.com/anthropics/claude-code` (activates repo-aware mode: README scan + live-demo detection)
> - **A product page / launch page** — e.g. `https://pika.art`
> - **A docs site** — e.g. `https://docs.anthropic.com`
> - **A blog post / article URL**
>
> Output: 1280×800 macOS Sonoma frame with a bottom-left avatar lipsync and element-targeted zoom on every mid-section beat. Default flow runs end-to-end after the cost gate; pass `--preview` if you want a 3-second lipsync sanity check for an explicit avatar. Generated / regenerated fallback avatars auto-run that preview guardrail.
>
> Reply with the URL and I'll start.
>
> *Tip: you don't need to type `/pika:explainer` — just say things like "walk me through <url>", "make a demo video of <url>", or "explain this repo: <github-url>" and I'll fire this skill automatically.*
When the user replies with a URL, treat it as the resolved input and proceed to Step 1. Do not re-prompt.
### Step 1 — Parse input + detect mode
Required: `url` (must be `https://...`).
Optional: `--avatar <url>` (the presenter photo; if omitted, one is generated), `--voice <minimax-voice-id>`, `--focus "..."` (editorial guidance woven into vo_text), `--live-url <url>` (force-supply live demo URL — GitHub mode only), `--lipsync-provider <pika|kling>` (defaults to **`pika`** — parrot a2v, ~2-5 min wall-clock, slightly more dramatic head motion. Pass `kling` for tighter face-centered output at ~5-30 min wall-clock — Kling produces minimal-head-motion presenter shots but is the long-pole stage; reserve for high-stakes renders), `--no-captions` (skip the Step 11 caption burn — default is captions on), `--preview` (opt-in to the Step 5 preview gate for explicit avatars; generated and regenerated fallback avatars auto-run the preview gate before full lipsync). `--skip-preview` and `--yes` are accepted as no-ops for backward compatibility.
**Mode detection:**
- **GitHub mode** — URL host is `github.com` AND path matches `/{owner}/{repo}` (no further path segments past the repo root). Activates the repo-aware extras: README scan, live-demo detection, GitHub-specific selectors.
- **Generic-URL mode** — anything else (a product page, docs site, blog post, deeper GitHub path like `/blob/HEAD/path`). Skips the GitHub extras; uses generic CSS selectors and walks through the URL itself.
**Avatar resolution (silent — never ask the user):**
1. If `--avatar <url>` was passed, use it.
2. Else call `generate_image` once with prompt `"professional presenter, friendly tech narrator, studio portrait, 1:1, natural lighting"` and use the returned URL. Do **not** ask the user "should I generate one?" — just generate silently.
Track `avatar_source` as one of `explicit`, `generated`, or `regenerated`.
**Avatar suitability gate (mandatory before any lipsync spend):**
Call `analyze_media(media=<avatar>, query=<gate_query>)` once on the resolved avatar image before Step 4 TTS and before any `generate_lipsync` preview/full call. This is the one case where avatar analysis is required, because the avatar is the central presenter asset and a bad generated avatar can burn the whole render.
Gate query:
```
Return JSON only: {
"is_single_front_facing_coherent_human_face": boolean,
"has_visible_mouth": boolean,
"is_faceless_or_masked": boolean,
"is_mascot_illustration_or_non_human": boolean,
"face_suitability_score": 0-100,
"apparent_gender": "female" | "male" | "unclear",
"reason": string
}
Is this image suitable for talking-head lipsync: a single front-facing coherent human face with a visible mouth? Flag faceless, masked, mascot, illustration-only, non-human, or distorted avatars.
```
Pass only if `is_single_front_facing_coherent_human_face == true`, `has_visible_mouth == true`, `is_faceless_or_masked == false`, `is_mascot_illustration_or_non_human == false`, and `face_suitability_score >= 75`.
If the avatar fails and `avatar_source` is `explicit`, stop before paid lipsync and ask for `--avatar <real-looking-photo-url>` or permission to generate a presenter portrait. If the avatar fails and `avatar_source` is `generated`, call `generate_image` once with prompt `"realistic professional presenter portrait, single front-facing coherent human face, visible mouth, friendly tech narrator, neutral studio background, 1:1, natural lighting"`; set `avatar_source = "regenerated"` and re-run this suitability gate on the regenerated avatar. If the regenerated avatar also fails, abort with a clear error instead of attempting lipsync.
Set `avatar_auto_preview_required = true` whenever `avatar_source` is `generated` or `regenerated`; otherwise false unless the user passed `--preview`.
**Voice resolution (silent — never ask the user):**
1. If `--voice <id>` was passed, use it.
2. Else pick a casual MiniMax `speech-2.8-hd` preset matching the resolved avatar's apparent gender:
- **Female-coded avatar** → `English_PlayfulGirl` (warm, casual, clearly female-voiced — verified)
- **Male-coded avatar** → `English_Jovialman` (warm, casual male)
- **Unclear / gender-neutral** → `English_Jovialman` (default)
Infer gender from the avatar suitability gate result (`apparent_gender`). Do **not** ask the user.
**Do NOT use `English_FriendlyPerson`** — despite being categorized under "female" in MiniMax's catalog, its display name is "Friendly Guy" and it reads as male in playback. `English_PlayfulGirl` is the canonical casual-female pick. Other verified-female alternates: `English_Upbeat_Woman`, `English_LovelyGirl`, `English_radiant_girl`.
The flow below is annotated per step: **GitHub-only**, **Generic-only**, or **Both** modes.
### Step 2 — Read source (no MCP call)
**Both modes:** use Claude's `WebFetch` on the input URL to pull the page's main content (h1, hero section, headings, primary copy).
Build `proper_noun_glossary` during this source read. Include canonical spellings of product, repo, company, model, framework, and proper nouns from the URL/domain, page title, h1, README headings, repo metadata, package names, and repeated capitalized tokens. For GitHub repos, preserve exact spellings surfaced by README/source scan, e.g. `Ollama`, `Llama`, `DeepSeek`, `Gemma`. Use this glossary later when authoring narration and when burning manual captions so caption text does not phonetically drift into misspellings like "Olama" or "DeepSeq".
**GitHub mode additions:** also fetch top-level file tree, (best-effort) `package.json` / `pyproject.toml`, and GitHub API repo metadata via `gh api repos/{owner}/{repo}` for `homepage`, `description`, `language`, `topics`. Detect a candidate `live_url` in this priority:
1. User-supplied `--live-url`.
2. **GitHub API `meta.homepage` field** — set when the maintainer configured the repo's homepage in GitHub settings.
3. `package.json` `"homepage"` field.
4. First match in README of `https?://[^\s)\"'<>]+(?:vercel\.app|netlify\.app|github\.io|fly\.dev|railway\.app|render\.com|herokuapp\.com|surge\.sh)[^\s)\"'<>]*`.
5. **Any other URL in README that the badge area / "Live Demo" / "Project Page" / "Demo" text points at.** The allowlist regex above misses arbitrary custom domains (e.g. `<project>-project-page.com`); when the README explicitly designates a project page, prefer that over the github.io fallback.
6. GitHub Pages convention `https://{owner}.github.io/{repo}` — but only if the deep tree contains a frontend signal (one of `index.html`, `App.tsx`, `App.jsx`, `App.vue`, `app.py`, `main.py`).
If no candidate resolves, the beat sheet skips beats 6–7.
**Generic-URL mode:** the input URL itself is the only URL the beats walk through — no `live_url` inference, no extra metadata fetches. Skip Step 2.5 and Step 3.0; jump straight to Step 3.
### Step 2.5 — Verify `live_url` reachability (GitHub mode only, no MCP call)
If a candidate `live_url` was selected, verify it serves real content **before** authoring beats 6–7. Use `WebFetch` on the candidate and check the response:
- If the response status is 4xx / 5xx, **drop `live_url` to None** and skip beats 6–7. The github.io fallback in particular is reachable as a hostname but often returns 404 ("There isn't a GitHub Pages site here") for repos that haven't enabled Pages — recording that 404 page wastes ~12s of the explainer on wrong content.
- If the response renders the GitHub Pages "404 — There isn't a GitHub Pages site here." template (heuristic: response body contains `"There isn't a GitHub Pages site here"`), drop `live_url` and skip beats 6–7.
- Otherwise, keep `live_url` for beats 6–7.
This mirrors the legacy reachability gate that checked `live_url` with a short timeout and followed redirects.
### Step 2.6 — Generic-URL pre-flight (Generic-URL mode only, no MCP call)
Before authoring beats for a non-GitHub URL, WebFetch the input URL and inspect the response. This step prevents three common Generic-URL failure modes: (a) recording a captcha / bot-block page instead of content, (b) the cookie/consent banner eating the first ~3 seconds of video, (c) generic CSS selectors missing the page's actual hero / sections.
**A. Bot-block / captcha detection — abort if matched:**
If the response body contains any of:
- `"Verify you are human"` / `"verify you are not a robot"`
- `"captcha"` / `"CAPTCHA"` / `"reCAPTCHA"`
- `"403 Forbidden"` / `"Access Denied"`
- `"Just a moment"` + `cf-chl-bypass` (Cloudflare challenge)
- `"We're sorry, something went wrong"` (Amazon-style bot block)
- A `<title>` or h1 of just "Robot Check" / "Are you a robot?"
→ **ABORT** with a clear error to the user: "Generic-URL mode can't render this site — the page is showing a bot-detection / captcha challenge under headless Chrome. Try a different URL, or run a real-user version of the page first to verify it loads cleanly."
**B. Cookie / consent-banner detection — defuse with `extra_css` + optional click:**
Scan the response for these patterns (case-insensitive):
- IDs / classes starting with `onetrust-`, `truste-`, `cookie-banner`, `cookie-consent`, `gdpr-`, `consent-`, `cmp-`
- Buttons matching `(?i)accept (all )?cookies` / `(?i)agree.{0,10}cookies` / `(?i)i (accept|agree)`
- Apple-specific banner: id `ac-gdpr-banner` or class `as-globalfooter-curtain`
- Google consent: `[role="dialog"]` with text "Before you continue"
If detected, set `cookie_banner_present = true`. Defense in depth — the recording uses BOTH:
1. **CSS injection (`extra_css`)** in the `capture_website` call to hide common banners universally — even if the click below misses, the banner is visually gone.
2. **A `click` `timed_action`** at `at_s: 0.0` against the most likely dismissal selector (extracted from the WebFetch DOM, e.g. `#onetrust-accept-btn-handler`, `[aria-label*="Accept all" i]`, `button[id*="accept"]`).
The `extra_css` payload (use this verbatim — covers ~80% of consent platforms):
```
#onetrust-banner-sdk, #onetrust-pc-sdk, #onetrust-consent-sdk { display: none !important; }
#truste-consent-track, #truste-consent-content, .truste_box_overlay { display: none !important; }
[id*="gdpr-cookie"], [id*="cookie-consent"], [id*="cookie-banner"] { display: none !important; }
[class*="cookie-banner"], [class*="cookie-consent"], [class*="consent-banner"] { display: none !important; }
[class*="CookieBanner"], [class*="CookieConsent"], [class*="ConsentBanner"] { display: none !important; }
#ac-gdpr-banner, .as-globalfooter-curtain { display: none !important; } /* Apple */
[role="dialog"][aria-label*="cookie" i], [role="dialog"][aria-label*="consent" i] { display: none !important; }
.cmp-container, .cmp-modal, .cmp-banner { display: none !important; }
```
**C. Real-DOM element identification — emit concrete selectors:**
Generic CSS selectors (`h1`, `[class*="hero"]`, `section h2`) work on semantic / well-marked-up sites but miss obfuscated class names on big-name corporate sites (apple.com uses `tile-headline` / `as-headline-section-title`, not `hero-*`). For each beat, prefer the **actual DOM elements** observed in the WebFetch:
- Read the rendered HTML/markdown WebFetch returned. Note the page's actual primary `<h1>` text and class.
- Note the page's section structure (h2 headings + their parent containers).
- Note any prominent CTA / signup / pricing element.
- Apply this **selector ladder** in order for every Generic-URL `zoom_target.selector`:
1. A stable id or class observed in the rendered DOM (`#hero`, `.tile-headline`, `.as-headline-section-title`, `.pricing-card`) when it is human-readable and specific.
2. Accessibility / link attributes (`[aria-label*="Get started" i]`, `a[href*="pricing"]`, `a[href="/new"]`) for CTAs or nav beats.
3. Semantic structure (`main > section:nth-of-type(N) h2`, `main section:nth-of-type(N) [role="img"]`, `footer h2`) when class names look generated (Tailwind `_1a2b3c`, CSS modules `module__hero___xYz`).
4. Broad fallback (`h1`, `main`, `section h2`, `button`, `a[href]`) only when the first three options are unavailable.
All emitted selectors must be vanilla CSS that `document.querySelector` can resolve. **Do not emit `:contains(...)`, `:has-text(...)`, `text=...`, or XPath**; those are Playwright/text-query conveniences and will not work in `capture_website`'s smooth-scroll path. If text matching is needed, target a nearby stable `href`, `aria-label`, id/class, or positional selector instead.
**D. SPA / lazy-render detection — bump initial wait:**
If the WebFetch response has fewer than 3 visible headings / minimal text content, the page may be SPA-rendered post-`domcontentloaded`. Emit a longer initial `wait` action (`{type: "wait", at_s: 0.0, ms: 2500}`) before any beat fires, instead of the default 600ms settle.
**E. `--focus` is honored when supplied (do not solicit):**
Without `--focus`, select beats from generic structure cues — proceed silently with a confident first attempt. Do **not** ask the user "what should I focus on?" before firing; users iterate by re-running with `--focus "the X feature"` if the first pass misses the angle they wanted. With `--focus` supplied, anchor beat selection on the phrase: uses concrete page sections that match it, ignores irrelevant marketing chrome.
### Step 3.0 — Required README section scan (GitHub mode only, no MCP call)
Before authoring the beat sheet, **scan the README** (case-insensitive, full-text) for any of these section names. If a match is found, you **must** add a dedicated beat for that section in Step 3, replacing one of the generic beats 4–5 if necessary:
| README contains... | Required beat |
|---|---|
| `overview` / `what is` | scroll_to that heading; zoom `.markdown-heading:has(#user-content-overview) .heading-element` (or the matching slug) |
| `how it works` | scroll_to that heading; zoom `.markdown-heading:has(#user-content-how-it-works) .heading-element` |
| `audio layer` / `audio timeline` | scroll_to the audio-layer diagram; zoom on the rendered figure or its surrounding heading |
| `claude code` / `mcp integration` | scroll_to that section; zoom `article pre` or `.highlight` (terminal screenshot / code block) |
| `architecture` / `system design` | scroll_to that section; zoom `.markdown-heading:has(#user-content-architecture) .heading-element` |
| `features` (when prominent at top) | scroll_to that heading; zoom `.markdown-heading:has(#user-content-features) .heading-element` |
| `getting started` / `quick start` / `installation` | scroll_to that heading; zoom `.markdown-heading:has(#user-content-installation) .heading-element` (or the matching slug) — falls back to `article pre` if you want the install code block instead |
| `usage` / `examples` | scroll_to that heading; zoom `.markdown-heading:has(#user-content-usage) .heading-element` (or the matching slug) — or the first code block under it |
**GitHub heading slug rule:** lowercase, spaces → dashes, strip non-`[a-z0-9-]` characters. So "How it works" → `#user-content-how-it-works`, "Quick Start" → `#user-content-quick-start`. GitHub currently renders README headings as `.markdown-heading` wrappers with a `.heading-element` and a sibling permalink anchor. Target the wrapper by slug, then the heading: `.markdown-heading:has(#user-content-<slug>) .heading-element`.
**GitHub DOM verified on 2026-05-28:** current repo-root pages expose the repo name at `strong[itemprop="name"] a`; the rendered README body at `.markdown-body`; the README title at `.markdown-body h1.heading-element`; and README section headings at `.markdown-heading:has(#user-content-<slug>) .heading-element`, with `.markdown-body h2.heading-element` as the broad section-heading fallback. During verification, if any `action_bboxes[].found` value is false for a GitHub beat, stop and re-verify GitHub's current DOM before continuing; do not rely on default-position fallback for repo walkthrough visuals.
**Selector contract:** `bbox_selector` needs to be vanilla CSS that resolves via `document.querySelector` (`capture_website` runs the post-action smooth-scroll JS via `page.evaluate`, which uses the browser's native selector engine). Avoid Playwright extensions like `:has-text("...")`, `text=...`, or `:visible`: those resolve in Playwright's `page.query_selector` (so the bbox capture finds the element) but silently fail in the smooth-scroll's `document.querySelector` (so the page never scrolls to the target, and `bbox.y` ends up at document-Y instead of `top - 60 px`, which trips Step 8b's `bbox.y > recording_viewport.h` degenerate filter and falls back to default-position zoom). CSS Level 4 `:has(...)` is vanilla and supported in modern Chromium.
These sections are the highest-information visuals in most explainer-worthy repos. Missing them produces a generic walkthrough; including them gives the explainer a concrete "show, don't tell" beat. Step 3.0 treats these as hard requirements rather than incidental guidance and includes seven high-signal headings common in OSS READMEs.
### Step 3 — Author beat sheet (main thread, no MCP call)
Write a JSON array of 8–10 beats, **with a hard total duration of 65–80 seconds and a hard total word count of 165–200 words** (assuming a speaking rate of 2.5 words/sec). Each beat:
```jsonc
{
"t_start": 0.0,
"t_end": 7.5,
"action": { "type": "navigate" | "scroll_to" | "hover", "url": "...", "selector": "..." },
"zoom_target": { "selector": "...", "description": "..." },
"vo_text": "exact words to speak — 1 to 2 conversational sentences"
}
```
**Hard constraints (validate before emitting the beat sheet — reject the draft if any fails):**
1. Every beat needs all five fields: `t_start`, `t_end`, `action` (with `type` and `url`), `zoom_target` (with `selector`), `vo_text`. Missing fields ⇒ reject and re-author.
2. `t_start` of beat 0 = 0.0; `t_end[i] == t_start[i+1]` (continuity).
3. `len(vo_text.split()) / 2.5` ≈ `t_end - t_start` per beat. Aim for ±10% of this estimate; if your draft is denser than 2.5 wps, tighten the `vo_text` until it fits.
4. **Total `t_end` of last beat ≤ 80 seconds.** (Reference output is 86.5s including intro; lipsync audio is ~83s. Kling avatar/image2video stalls reliably past ~90s of audio under current load — going over 80s risks a 20-min Kling timeout.)
5. **Total spoken word count between 165 and 200 words.**
6. Every beat's `zoom_target.selector` needs to be a valid CSS selector for the page that beat lands on. **GitHub mode prefers** current GitHub repo/README selectors: `strong[itemprop="name"] a`, `.markdown-body h1.heading-element`, `.markdown-heading:has(#user-content-<slug>) .heading-element`, `.markdown-body h2.heading-element`, `.blob-code-inner`, `.highlight`, `.octicon-star`, `nav`. **Generic-URL mode prefers** robust generic selectors: `h1`, `[role="main"]`, `main`, `header`, `nav`, `.hero`, `.feature`, `section h2`, `[class*="cta"]`, `[class*="hero"]`, `button`, `a[href]`. **Selectors need to resolve on the rendered page after the beat's action settles** — verify against the DOM you can see via WebFetch before emitting.
7. `vo_text` is 1-2 conversational sentences. Dev voice. No stage directions. No markdown.
8. `action.url` is a valid `https://...` URL when `action.type == "navigate"`; required.
**Self-check before Step 4:** verify `total_words` is in `[165, 200]` AND `total_seconds` (= `beats[-1].t_end`) is in `[65, 80]`. If either misses bounds, re-author the beat sheet — do not proceed to TTS. (No need to "print" anywhere — this is an internal draft validation; just reject the draft and re-author until it passes.)
**Structural skeleton — GitHub mode (load-bearing for the visual contract — match origin, but Step 3.0 overrides if applicable):**
- **Beat 1:** `navigate` repo root, zoom `strong[itemprop="name"] a` (repo title), hook sentence.
- **Beats 2–3:** `navigate` to specific source files (`https://github.com/{owner}/{repo}/blob/HEAD/<path>`), zoom `.blob-code-inner` or `.highlight`. Pick files that match the narration's claim — don't navigate to a file you won't talk about.
- **Beats 4–5:** `scroll_to` README sections, zoom `.markdown-heading:has(#user-content-<slug>) .heading-element`, `.markdown-body h2.heading-element`, or `.markdown-body`. **If Step 3.0 surfaced required sections, replace these slots with the required ones.**
- **Beats 6–7 (only if `live_url` survived Step 2.5):** `navigate` to `live_url`, zoom `nav` / `h1` / `.hero` / `main` / `button` / `.feature`.
- **Beat 8:** back to repo root, zoom `.octicon-star`, outro.
**Structural skeleton — Generic-URL mode:**
- **Beat 1:** `navigate` to the input URL, zoom `h1` or `[class*="hero"] h1` (the page's primary headline), hook sentence.
- **Beats 2–3:** `scroll_to` the page's hero / value-prop / first feature section. Zoom `.hero`, `[class*="hero"]`, `[class*="feature"]`, or `section:nth-of-type(1) h2`. Pick visible elements the narration references.
- **Beats 4–5:** `scroll_to` deeper sections — feature lists, screenshots, pricing, social proof. Zoom `section h2`, `[class*="feature"] img`, `[class*="testimonial"]`, `[class*="pricing"]`, or any prominent semantic element on the page.
- **Beats 6–7:** `scroll_to` CTA / signup / demo embed. Zoom `[class*="cta"]`, `button`, `a[class*="button"]`, or `[id*="signup"]`. (No live-demo navigation in generic mode — the input URL IS the demo.)
- **Beat 8:** `scroll_to` footer / closing element, zoom `footer h2`, `footer`, or back to top with `h1`. Outro sentence.
If `--focus` is supplied, weave its angles into `vo_text` without mutating the structural skeleton. Prefer **CSS selectors over `text_content`** in `zoom_target.selector` — bbox capture is selector-only (see Known gaps).
### Step 4 — TTS
Call `generate_speech` with `provider: "minimax-tts"`, `text: <full vo_text join>`, optional `voice_id` (from `--voice` or the Step 1 default preset). Capture `result.audio_url` (the dispatcher returns audio under `audio_url`, not `url`) and `result.duration_seconds`.
**Stale-voice fallback detection:** the dispatcher retries once with the default `Calm_Woman` voice on Minimax `status_code:2054` (voice id not found — typically a per-agent workspace pointer that Minimax auto-deleted after 7 days of inactivity). On retry success the response carries two extra fields beyond the documented schema (passthrough): `voice_id_requested` (the planted-but-stale id the worker tried first) and `fallback_reason: "invalid_minimax_voice_id"`. **If you see `fallback_reason == "invalid_minimax_voice_id"` in the response, surface a one-line note to the user along the lines of:** "your registered voice expired on Minimax (auto-GC'd after 7 days of inactivity); we used the system default. Re-clone via `clone_voice` if you want personalization back." The render does NOT fail — it just uses the default voice — so this is informational, not a retry trigger.
**Cookie-banner audio padding (Generic-URL mode with `cookie_banner_present == true` from Step 2.6 §B):** when the Step 4 call uses the default MiniMax path, prepend MiniMax's pause marker `<#1.5#>` to the `text:` argument **before** calling `generate_speech`. MiniMax's `speech-2.8-hd` honors `<#N#>` as N-second silence; the returned `audio_url` and `duration_seconds` include the 1.5s lead-in natively. If Step 4 uses `provider="elevenlabs"`, skip the MiniMax marker and use the fallback audio-mix padding path below. This aligns the audio with the screen recording's cookie-dismissal +1.5s offset applied in Step 4.5.
**Fallback** (only if smoke-test shows the marker is ignored on this voice): call `generate_speech` normally, then `edit_audio_mix` to overlay the result onto a 1.5s silent base at offset 1.5s. **Then call `analyze_media(url=<padded_audio_url>)` to probe the padded duration and rebind `duration_seconds = result.duration_seconds`** before Step 4.5 consumes it. `analyze_media` is the **single authoritative duration probe** — do not rely on `edit_audio_mix`'s return payload (its duration field is not contractually guaranteed).
### Step 4.5 — Audio length verification + beat-sheet rescale
Applied to `audio_duration_seconds` post Step 4 (which includes any cookie lead-in pad). End state: `be...
© Pika-Labs · Apache-2.0 · Pika-Labs/Pika-Plugins
Similar skills
Everything for Marketing and content →explainer by other authors comes first.