---
name: hyper-animator
version: 2.15.1
commit: cee07f6
description: |
  Video/animation creation pipeline from natural language. Use ONLY when the
  user explicitly asks for a VIDEO, ANIMATION, or HTML animation output —
  e.g. "create a video", "make an animation", "render HTML animation",
  "generate a product demo video", "code terminal animation", "podcast caption
  overlay video", "data visualization animation", "social media short/reel".
  Do NOT trigger for text documents, articles, markdown files, web pages,
  company introductions, or any request where the output is a document rather
  than an animation/video. "介绍" alone is NOT a trigger — only "动画介绍"
  or "视频介绍" is.
---

# Hyper-Animator Skill

## 1. Overview

**At the start of every pipeline run**, read the YAML frontmatter and display version + commit:

```bash
grep -E "^(version|commit):" ~/.claude/skills/hyper-animator/SKILL.md
```

Display as: `hyper-animator v<version> (commit <commit>)` in the first message to the user. Example: `hyper-animator v1.10.1 (f6b3697)`. This helps with debugging — knowing exactly which code is running. The commit hash links to the source at `https://github.com/realpkuasule/hyper-animator-2/commit/<commit>`.

The hyper-animator skill implements a dual-mode architecture for turning natural language video/animation requests into renderable HyperFrames HTML compositions. The two modes serve different user needs:

- **assemble_existing_catalog_items**: Reuses installed HyperFrames catalog blocks and components via `data-composition-src` wrappers. Use this when the user wants quick assembly from existing visual assets -- "combine these blocks", "use the catalog", "quickly compose a demo".

- **generate_new_hyperframes_html**: Generates a complete, original HyperFrames HTML composition referencing catalog candidates for inspiration, following the patterns in `references/HyperFrames-AI-Generation-Patterns-codex.md`. Use this when the user wants custom visuals -- "write a new HTML animation", "make something unique", "match my brand".

### When NOT to Use This Skill

**This skill produces HTML animations and videos. It does NOT produce text documents.** Before invoking, check the user's request against this exclusion list:

| User asks for... | Use instead |
|---|---|
| "写一份公司介绍" / "生成一份文档" / "做个简介" | Write the document directly — no skill needed |
| "做一个网站" / "写个网页" / "做个 landing page" | Use `frontend-design` or write HTML directly |
| "设计一个 logo" / "画一张图" | Use image generation tools |
| "查一下这家公司" / "搜索..." | Use web search directly |
| "介绍" alone without "动画" or "视频" | This is a document request — do NOT use this skill |

**Trigger words that DO justify this skill**: "动画", "视频", "渲染", "motion", "animation", "video", "render", "GSAP", "HyperFrames", "产品发布视频", "代码演示动画", "数据可视化动画", "播客字幕", "社媒短视频", "短视频", "reels", "shorts".

If in doubt, ask the user: "你想要的是文字文档，还是动画/视频？" before invoking this skill.

**Language Policy**: ALL skill interaction is in **中文（简体中文）**. Round questions,
narration generation, subtitles, UI text, and error messages default to Chinese.
Only switch to another language (en/ja/etc.) when the user **explicitly** requests
it in their prompt ("generate in English", "英文", "日本語で").

**Font Policy**: Do NOT use Google Fonts (`fonts.googleapis.com`) or other foreign CDN fonts. Use Chinese system font stack as default: `font-family: 'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB', 'Noto Sans SC', sans-serif`. For monospace (preview controls, code): `'JetBrains Mono', 'Cascadia Code', monospace`. Instant loading, no network dependency, works in China.

### Pipeline

The skill follows a 16-step pipeline, proceeding linearly with conditional branches for clarification rounds and revision loops:

0. Setup Git workspace for version control and rollback. ← NEW
1. Receive and parse the user's natural language request.
2. Extract initial intent profile: purpose, format, style, motion, roles, generation mode hints.
3. If purpose or format is missing or unknown, run AskUserQuestion Round 1.
4. Load the catalog map and score all items against the current intent profile.
5. Select the top candidate plan: main scene block, optional components (captions, effects), optional outro.
6. If generation mode is ambiguous, run AskUserQuestion clarification.
7. Run AskUserQuestion Round 2 for style and motion preferences, including candidate context.
8. Run AskUserQuestion Round 3 for SFX and BGM preferences. ← NEW
9. If BGM selected, run beat-detector CLI to analyze music file. ← NEW
10. Generate HTML in the assigned mode (with beat data and audio injected if configured). ← UPDATED
11. Run pre-render quality gates (10 checks).
12. Present plan summary and HTML preview for user validation.
13. Revise based on user feedback (loop to relevant step).
14. Render video via `hyperframes` CLI.
15. Report output file path to user.

Each step is designed so that ambiguity is resolved through structured AskUserQuestion calls rather than agent guesswork, producing consistent, renderable output every time.

## 2. Prerequisites

Before running the pipeline, verify that the `hyperframes` CLI tool is installed and accessible:

```bash
hyperframes --version
```

If the command fails or is not found, tell the user to install it:

```bash
npm install -g @hyperframes/cli
```

Or if they have a local install:

```bash
npx hyperframes --version
```

### Audio Tool Dependencies (Conditional)

All audio tools are bundled — no external dependencies needed beyond Python stdlib.

**Beat detection**: `scripts/beat-detector.py` — pure Python (stdlib `wave` + `math`), no librosa/pip install needed. Energy onset detection + autocorrelation for BPM, beat timestamps, structure inference. Always works.

The skill also requires that the reference files exist at their expected paths:

- `/Users/zhichao/.claude/skills/hyper-animator/references/hyperframes-catalog-map.json` -- 133 catalog items with scoring model and taxonomies.
- `/Users/zhichao/.claude/skills/hyper-animator/references/HyperFrames-AI-Generation-Patterns-codex.md` -- 778 lines of generation patterns (required for generate mode).

If either file is missing, the pipeline cannot proceed beyond catalog scoring or HTML generation respectively.

## 3. Step 0: Git Workspace Setup

At the start of every pipeline run, check whether the current working directory is a Git repository. Version control enables rollback of generated files, tracking of changes across revisions, and safe experimentation.

### Check

```bash
git rev-parse --git-dir 2>/dev/null && echo "GIT_REPO" || echo "NO_GIT"
```

### If NOT a Git Repository

Ask the user with `AskUserQuestion`:

```
Question: "当前目录还不是 Git 仓库。是否初始化 Git 以便版本管理和回滚？"
  - "是，初始化 Git 仓库"
  - "不需要，跳过版本管理"
```

If the user chooses "是":

```bash
git init
git add -A
git commit -m "init: hyper-animator workspace"
```

If the user chooses "不需要", skip all git operations for this session. The pipeline proceeds without version control.

### Per-File Commits

After writing each output file, stage and commit it with a descriptive message:

| Trigger | Commit Message |
|---------|---------------|
| Beat JSON generated | `data: beat detection — <composition-name>` |
| BGM/SFX files generated | `asset: audio generated via <source>` |
| Render HTML written | `feat: render HTML — <composition-name>` |
| Video rendered | `feat: rendered video — <composition-name>.mp4` |
| Revision applied | `fix: revision — <user feedback summary>` |

```bash
git add <file>
git commit -m "<message>"
```

Never use `git add -A` after the initial commit — always stage specific files. This prevents accidentally committing unrelated changes the user may have made.

### Rollback

If the user asks to roll back changes, present the recent commit history and let them choose:

```bash
git log --oneline -10
```

Ask the user which commit to roll back to (by hash or relative reference like `HEAD~2`), then:

```bash
git reset --hard <commit>
```

Warn the user that `git reset --hard` discards uncommitted changes. All committed checkpoints remain in `git reflog` for recovery.

**Do NOT create branches or worktrees.** All work stays on the default branch.

### File Detection: Outline and Narration Script

In addition to Git setup, scan the current directory for content files that drive the animation. Two file types are recognized:

**Outline** (`outline.md` or auto-detected):
- A Markdown document describing what the animation should contain — sections, key points, structure.
- Used as the primary content source for HTML animation generation (Step 8).
- Detection: look for `outline.md` first; if not found, look for any `.md` file that contains section headers (`##`) and is NOT a narration script.

**Narration Script** (`narration.json` or auto-detected):
- A JSON file defining per-scene voiceover text, sync hints, and voice settings.
- Used by `scripts/tts-gen.py` to generate scene narration audio clips.
- Detection: look for `narration.json` first; if not found, look for any `.json` file matching the narration schema (has `"scenes"` array with `"narration"` fields).

**Narration Script Format:**

```json
{
  "voice": "<voice_id>",
  "scenes": [
    {
      "scene": 1,
      "title": "开场",
      "sync_hint": "intro_hero",
      "narration": "欢迎来到紫微斗数十四主星的介绍。",
      "duration_estimate": 5
    },
    {
      "scene": 2,
      "title": "紫微星",
      "sync_hint": "ziwei_detail",
      "narration": "紫微星，北斗帝星，主掌权柄与尊贵。",
      "duration_estimate": 8
    }
  ]
}
```

- `voice`: Minimax TTS voice ID. Selected by user in Round 3 from `tts-gen.py --list-voices`.
- `sync_hint`: Links to the corresponding section ID in the outline, ensuring the HTML scene timing matches the narration audio.
- `duration_estimate`: Approximate duration in seconds. Used to size the GSAP scene timeline so animation and narration finish together.
- `direction` (optional): Visual direction hints per scene, filled by AI after Round 2 style/motion are confirmed:
  ```json
  "direction": {
    "mood": "dark cinematic reveal",
    "camera": "slow zoom 1.0→1.15x",
    "techniques": ["opacity+fade", "vignette overlay", "particle system"],
    "transitionOut": "domain-warp 0.7s",
    "assets": ["assets/hero-bg.png", "assets/logo.svg"]
  }
  ```
  - `mood`: One-sentence visual feel for the scene
  - `camera`: Lens motion (zoom/pan/static/dolly)
  - `techniques`: 1-3 GSAP patterns or visual effects to use
  - `transitionOut`: How this scene exits to the next (shader name + duration, or `cut`)
  - `assets`: Paths to specific images/icons needed for this scene

If no script file is found, AskUserQuestion: "没有找到口播稿文件。是否需要口播稿？" — the user may want a silent animation, or they may want to create a script first.

**FRAME.md** — Per-beat creative direction file (from `/hyperframes-creative`).
- When present, this **IS** the creative plan. Round 1-2 AskUserQuestion is **SKIPPED**.
- Detection: look for `FRAME.md` (case-insensitive: `FRAME.md`, `frame.md`).
- If both FRAME.md and narration.json exist: FRAME.md provides creative direction; narration.json still used for TTS timing and subtitles.

**FRAME.md Format:**

```markdown
# Visual Theme
Dark tech, high contrast, cinematic neon accents

## Colors
- Primary: #0A0A14 (deep void)
- Accent: #06E3FA (cyan glow) / #FF4FD8 (magenta pulse)
- Text: #FFFFFF / rgba(255,255,255,0.7)

## Fonts
- Display: PingFang SC 900 (72-96px titles)
- Body: PingFang SC 600 (24-32px subtitles)

## Beat 1: Opening (0.0s - 8.4s)
- Mood: dramatic reveal, slow zoom
- Camera: 1.0 → 1.15x over 8s
- Narration: "多量巨慧——数据的力量"
- Scene: Company name hero with particle background
- Techniques: opacity+fade, vignette overlay, particle float
- TransitionOut: domain-warp 0.7s
- Assets: none

## Beat 2: Data Story (8.4s - 20.0s)
- Mood: analytical, data-driven
- Camera: static, slight parallax
- Narration: "海量データの力"
- Scene: Data visualization with floating numbers
- Techniques: number count-up, chart stagger reveal
- TransitionOut: cinematic-zoom 0.7s
```

### Step 0.5: Content Mode Selection — Outline & Narration

After file detection, always confirm with the user before proceeding. Never auto-generate narration — narrations are optional, and many animations work better silent.

#### Content Mode Decision Flow

| Scenario | AskUserQuestion | Options |
|----------|----------------|---------|
| Only outline found | "是否需要人声口播？" | 仅大纲 / 大纲+生成口播稿 |
| Only narration found | "如何继续？" | 先生成大纲 / 仅口播稿 |
| Both found | "使用哪种内容源？" | 仅大纲 / 大纲+口播稿 / 仅口播稿 |
| Neither found | — | Proceed silently |

**Before generating narration**: Language defaults to `zh` (中文). Only ask for language if the user explicitly requested a non-Chinese language (en/ja/etc.). Always ask for style (formal/casual/humorous/poetic/passionate/restrained — see Step 0.5c for mapping).

**After generating either file**: present with AskUserQuestion for user confirmation before proceeding.

**Narration JSON format** — key fields only:
```json
{
  "voice": "<voice_id>", "emotion": "calm",
  "language": "zh", "style": "formal",
  "scenes": [{"scene": 1, "title": "...", "narration": "...",
    "sync_hint": "...", "duration_estimate": 5,
    "subtitles": [{"text": "...", "start": 0, "end": 3}]}]
}
```
Fields: `voice` (TTS voice ID), `emotion` (calm/fluent/happy/...), `language` (zh/en/ja), `style` (formal/casual/...), `sync_hint` (links to outline section), `duration_estimate` (~4 chars/sec CN), `subtitles` (optional, per-scene timed text), `direction` (optional, per-scene visual hints: mood/camera/techniques/transitionOut/assets).

### Generate Narration from Outline

1. Read the outline sections.
2. For each section, write a narration sentence in the selected language and style.
3. Estimate `duration_estimate` (~4 chars/sec for Chinese, ~15 chars/sec for English).
4. Create `sync_hint` from section heading (kebab-case).
5. Assemble JSON and write to `narration.json`.

Validate: every `sync_hint` matches a section in the outline. Every scene has non-empty narration.

### Generate Outline from Narration

1. Use `scene.title` as section headings.
2. Use `sync_hint` as section anchor IDs.
3. Extract 2-3 key points from each scene's narration text.
4. Write `outline.md`.

```markdown
# <derived from first scene context>

## <scene.title>
<!-- sync_hint: <scene.sync_hint> -->
- <key point from narration>
- <key point from narration>
```

Validate: section count matches scene count. Every `sync_hint` in the script has a matching outline section.

## 4. Step 1: Receive and Extract Intent

Parse the user's natural language request into a structured `IntentProfile`. This is not a full NLU parse -- it uses regex and keyword matching to extract the minimum actionable signal. The extraction rules mirror the `extractInitialIntent()` function from the agent pseudocode.

### Purpose Detection

Match keywords against the five known purpose domains. The first match wins; if multiple keywords match, prioritize in this order:

- `product_launch`: triggered by keywords like `产品`, `app`, `发布`, `功能`, `launch`, `product`, `feature`, `demo` -- covers product reveals, app showcases, feature walkthroughs.
- `developer_demo`: triggered by `代码`, `编程`, `cli`, `终端`, `vscode`, `code`, `terminal`, `programming`, `developer`, `sdk` -- covers code walkthroughs, terminal recordings, technical demos.
- `data_visualization`: triggered by `数据`, `图表`, `地图`, `增长`, `复盘`, `data`, `chart`, `map`, `statistics`, `growth`, `dashboard` -- covers charts, maps, growth reports, analytics.
- `podcast_interview`: triggered by `播客`, `采访`, `嘉宾`, `lower third`, `podcast`, `interview`, `guest`, `caption`, `talk` -- covers interview overlays, podcast captions, speaker titles.
- `social_media`: triggered by `shorts`, `reels`, `tiktok`, `社媒`, `社交媒体`, `短视频`, `social`, `viral`, `trending` -- covers short-form social media content.

If none match, leave purpose undefined -- Round 1 clarification will handle it.

### Format Detection

Detect target aspect ratio from keywords:

- `portrait_9_16`: triggered by `竖屏`, `9:16`, `tiktok`, `shorts`, `reels`, `portrait`, `vertical`, `手机`.
- `landscape_16_9`: triggered by `横屏`, `16:9`, `youtube`, `b站`, `bilibili`, `landscape`, `widescreen`, `desktop`.
- `square`: triggered by `square`, `方形`, `instagram`, `1:1`.
- If none match, set format to `unknown` -- Round 1 will clarify.

Format matters because it directly constrains which catalog items are viable (portrait items for portrait requests) and determines default dimensions in Step 5.

### Role Detection

Identify which component roles the user needs:

- `caption`: triggered by `字幕`, `口播`, `caption`, `subtitles`, `karaoke`, `lyrics`, `台词`.
- `transition`: triggered by `转场`, `切换`, `transition`, `wipe`, `scene change`.
- `outro`: triggered by `logo`, `片尾`, `outro`, `ending`, `结尾`, `brand`, `watermark`.
- `device_showcase`: triggered by `device`, `手机`, `设备`, `mockup`, `phone frame`, `browser`.
- `overlay`: triggered by `overlay`, `覆盖`, `grain`, `vignette`, `effect`.

These roles map directly to the `assetRoles` taxonomy and influence which items score higher during catalog matching.

### Generation Mode Hints

Detect explicit user preferences about how the animation should be produced:

- `assemble_existing_catalog_items`: triggered by `用现有`, `catalog`, `调用`, `组合`, `combine`, `use catalog`, `assemble`, `quick`, `拼`.
- `generate_new_hyperframes_html`: triggered by `写`, `生成`, `自定义`, `新的`, `html`, `动画`, `generate`, `create`, `new`, `custom`, `brand`.

If both or neither set of keywords match, leave generation mode undefined -- the decision logic in Step 6 will resolve it.

### Style Hint Extraction

Extract early style signals from user language. These are initial guesses only; Round 2 clarification will refine them:

- `apple_like`, `premium`: triggered by `apple`, `苹果`, `高级`, `premium`, `elegant`, `sleek`.
- `cinematic`, `dark`: triggered by `电影`, `cinematic`, `暗调`, `dark`, `dramatic`, `cinema`.
- `minimal`: triggered by `极简`, `clean`, `minimal`, `simple`, `简约`, `清爽`.
- `cyberpunk`: triggered by `故障`, `赛博`, `glitch`, `cyber`, `cyberpunk`, `neon`.
- `editorial`: triggered by `editorial`, `杂志`, `news`, `news`, `editor`, `报道`.
- `playful`: triggered by `playful`, `fun`, `活泼`, `有趣`.

### Motion Hint Extraction

Similarly extract motion hints:

- `reveal`: triggered by `reveal`, `展示`, `出现`.
- `zoom`: triggered by `zoom`, `缩放`, `zoom in`.
- `particle`: triggered by `particle`, `粒子`.
- `typing`: triggered by `typing`, `打字`, `code`.
- `glitch`: triggered by `glitch`, `故障`.
- `wipe`: triggered by `wipe`, `擦除`, `过渡`.
- `blur`: triggered by `blur`, `模糊`.

### Visual Richness Detection

Default to `"rich"` — use shader transitions, VFX layers, and complex animations.
Flip to `"plain"` ONLY when the user explicitly requests minimal/clean/simple:

- `"plain"` triggers: `极简`, `朴素`, `平淡`, `干净`, `简单`, `不需要特效`, `不想太花`, `plain`, `minimal`, `simple`, `clean`, `no effects`, `subtle`
- Default: `"rich"`

```json
{
  "visualRichness": "rich"
}
```

### Content Inputs Extraction

Extract any explicit content provided by the user:

- `productName`: text following "product" or "app name" or "产品名".
- `script`: any multi-line text that looks like a script or narration.
- `code`: code blocks in the request.
- `data`: arrays or structured data for visualization.
- `logo`: mention of a logo file or brand mark.

### Explicit Catalog Item References

**CRITICAL**: When the user explicitly names a specific catalog item in their request (e.g., "希望用到 VFX text cursor", "use the iOS 26 Liquid Glass block", "加一个 vfx-text-cursor"), you MUST capture it. These are non-negotiable — the item MUST appear in the final composition.

Detection keywords: `用到`, `use`, `加一个`, `加上`, `包含`, `include`, `希望有`, `想要...效果`, any quoted catalog item name, any catalog item ID (kebab-case matching `[a-z][a-z0-9-]+`).

Search the catalog map at `references/hyperframes-catalog-map.json` for items matching the user's description by `id`, `title`, or `naturalLanguageTriggers`. Store matches as:

```json
{
  "explicitCatalogRefs": [
    {"id": "vfx-text-cursor", "title": "VFX Text Cursor", "matchType": "user_named"}
  ]
}
```

### Result: IntentProfile

Combine everything into a structured profile object. **After finalizing the intent profile, write it to `hyperframes-output/intent-profile.json`** — this file is consumed by `validate-quality.sh` Gate 13 to enforce explicit catalog item references.

```json
{
  "rawRequest": "Create a product launch video for our new app in landscape 16:9",
  "purpose": "product_launch",
  "format": "landscape_16_9",
  "generationMode": "generate_new_hyperframes_html",
  "styleTags": ["apple_like", "premium"],
  "motionTags": ["reveal"],
  "neededRoles": ["main_scene", "outro"],
  "durationTarget": 30,
  "contentInputs": {
    "productName": "our new app"
  },
  "explicitCatalogRefs": [],
  "visualRichness": "rich"
}
```

### FRAME.md Priority Shortcut

If `FRAME.md` was detected in Step 0.5, extract the intent profile directly from it
instead of parsing the natural language request. The FRAME.md **IS** the creative plan.

**Extraction mapping:**
- `purpose` → `"product_launch"` (default for FRAME.md-driven projects)
- `format` → `"landscape_16_9"` (default, unless FRAME.md specifies portrait/vertical)
- `styleTags` → from Visual Theme + Colors section keywords
- `motionTags` → from Beat mood + camera descriptions
- `transitionMode` → `"auto"` (FRAME.md specifies per-beat transitions explicitly)
- `visualRichness` → `"rich"` (FRAME.md implies rich by default)
- `contentInputs.productName` → from first heading or directory name

**After extraction, SKIP Round 1 and Round 2.** Proceed directly to Step 3
(catalog scoring) using the extracted profile. Round 3 (audio/BGM) still runs.

If the user's prompt contains additional instructions beyond `/hyper-animator`
("but make it faster", "change the mood to warm"), apply those as overrides
on top of the FRAME.md extraction.

## 4. Step 2: Round 1 AskUserQuestion

If `purpose` is undefined or `format` is `unknown`, you MUST ask the user for clarification before proceeding to catalog scoring. Do not guess -- premature guessing compounds errors through scoring, selection, and generation.

Use Claude Code's `AskUserQuestion` tool with the following structured payload:

```json
{
  "purpose": "clarify_intent_and_format",
  "questions": [
    {
      "id": "purpose",
      "question": "你这次视频/动画主要用于什么场景？",
      "choices": [
        "产品发布/功能展示",
        "技术教程/代码演示",
        "数据报告/市场复盘",
        "播客/采访/字幕包装",
        "社交媒体短视频"
      ]
    },
    {
      "id": "format",
      "question": "目标画幅是什么？",
      "choices": [
        "横屏 16:9",
        "竖屏 9:16",
        "方形",
        "还不确定"
      ]
    }
  ]
}
```

Mapping from user-facing choices to internal values:
- "产品发布/功能展示" -> `product_launch`
- "技术教程/代码演示" -> `developer_demo`
- "数据报告/市场复盘" -> `data_visualization`
- "播客/采访/字幕包装" -> `podcast_interview`
- "社交媒体短视频" -> `social_media`
- "横屏 16:9" -> `landscape_16_9`
- "竖屏 9:16" -> `portrait_9_16`
- "方形" -> `square`
- "还不确定" -> set to `landscape_16_9` as default (most common output format)

**Skip this step entirely** if both purpose and format were successfully extracted in Step 1. Only ask what is missing -- if purpose is clear but format is unknown, only ask the format question.

After receiving the answers, merge them into the intent profile. Update style and motion tags if the answers provide additional clues (e.g., "产品发布" strongly suggests `product_launch` domain but also lowers ambiguity about needing an outro for branding).

## 5. Step 3: Catalog Scoring

Run the scoring script to rank all catalog items against the current intent profile:

```bash
python3 ~/.claude/skills/hyper-animator/scripts/score-catalog.py \
  --catalog ~/.claude/skills/hyper-animator/references/hyperframes-catalog-map.json \
  --scoring ~/.claude/skills/hyper-animator/references/scoring.json \
  --purpose <purpose> \
  --format <format> \
  --style <style_tags_comma_separated> \
  --motion <motion_tags_comma_separated> \
  --roles <needed_roles_comma_separated> \
  --raw-request "<raw_request>" \
  --top 30 \
  --output json
```

The script applies the 7-factor weighted scoring model defined in `scoring.json`.
Weights: keywordMatch (0.30) + intentDomainMatch (0.25) + formatMatch (0.15) +
assetRoleMatch (0.10) + styleMatch (0.10) + motionMatch (0.05) + constraintsMatch (0.05).
See `references/scoring.json` for full weight details and hard constraints.

Results are sorted by score descending. `code-morph` is automatically excluded.
Each result includes: id, title, type, score, reasons[], assetRoles, intentDomains, styleTags, format.

## 6. Step 4: Candidate Plan Selection

From the scored catalog items, select a coherent plan -- the set of blocks and components that together form the user's animation. The plan follows a standard composition structure:

### Selection Rules

**Main scene block (required)**: Pick the highest-scoring item with `type: "block"` and `assetRoles` including `main_scene`. This is the visual core of the animation -- a product showcase, data chart, code terminal, etc. If no block scores above 0, fall back to generate mode.

**Optional captions (top 1)**: If `neededRoles` includes `caption`, pick the highest-scoring caption component. These are typically `caption-*` items with `assetRoles: ["caption"]` and `type: "component"`. Only one caption component is selected to keep the composition clean.

**Optional effects (top 2)**: If the request calls for visual effects or if the main scene benefits from overlays, pick up to 2 effect components (items with `assetRoles` including `effect` and `type: "component"`). Examples: `grain-overlay`, `vignette`, `shimmer`. Limit to 2 to avoid visual clutter.

**Optional outro**: If `neededRoles` includes `outro`, pick the highest-scoring item with `assetRoles` including `outro`. If no explicit outro is needed but the purpose is `product_launch` or `branding_outro`, add an outro by default for brand presence.

### Dimension Determination

Delegates to the main scene block's format if available:

- If a main scene block was found AND it has explicit `format.width` and `format.height`: use those.
- If no main block: use defaults based on format -- `1920x1080` for landscape_16_9, `1080x1920` for portrait_9_16, `1080x1080` for square, `1920x1080` for unknown.

### Duration Determination

Calculate duration as the sum of all selected items' `format.durationSeconds`. If this sum is 0 (non-timed components like overlays) or unreasonably short:

- Use the user's `durationTarget` from the intent profile if provided.
- Fall back to 10 seconds as a sensible minimum for a complete composition.
- If items produce a very long total (over 120 seconds) for a short-form request, clamp or ask the user.

The reason for summing durations: HyperFrames compositions play each block/component sequentially within the wrapper. The total duration must cover all nested elements.

### Plan Summary

Present the plan as a structured object:

```json
{
  "generationMode": "assemble_existing_catalog_items",
  "selectedItems": [
    {"id": "app-showcase", "title": "App Showcase", "type": "block", "score": 0.82, "role": "main_scene", "reasons": ["intent: product_launch", "format: landscape_16_9"]},
    {"id": "caption-pill-karaoke", "title": "Caption Pill Karaoke", "type": "component", "score": 0.68, "role": "caption_reference"}
  ],
  "width": 1920,
  "height": 1080,
  "duration": 20,
  "assumptions": [
    "Using app-showcase as main visual reference",
    "Adding caption overlay for feature callouts"
  ]
}
```

## 7. Step 5: Generation Mode Decision

Determine whether to assemble existing catalog items or generate new HTML. The decision follows explicit rules based on the user's language and the candidate plan's technical constraints.

### Rule 1: User Explicitly States a Mode

If the user's request contains unambiguous mode keywords, respect them directly:

| User says | Mode |
|-----------|------|
| "用现有 catalog", "快速拼一个", "调用 block", "组合这些条目", "use catalog", "assemble", "combine existing" | `assemble_existing_catalog_items` |
| "写 HTML 动画", "生成一个新效果", "自定义样式", "按我的品牌做", "write HTML", "generate new", "create custom", "brand new animation" | `generate_new_hyperframes_html` |

### Rule 2: Paste Comment Constraint

If any selected component's `install.includeSnippet` is a `<!-- paste from ... -->` comment rather than actual HTML, the component cannot be directly used in assemble mode. The paste comment is not renderable HTML. In this case:

- Try to resolve the snippet by reading the actual file from `install.path` (relative to the hyperframes installation directory). If the real file exists, read its content and use it.
- If the real file cannot be resolved, force `generate_new_hyperframes_html` -- you cannot use an unresolved paste comment in the final render.
- This rule exists because paste comments are developer instructions, not visual content.

### Rule 3: Technical Complexity

If the request requires any of the following, prefer `generate_new_hyperframes_html` even if the user did not explicitly request it:

- Custom captions with specific styling, fonts, or karaoke effects.
- WebGL, Three.js, or shader-based visuals.
- Complex 3D scenes or GLTF models.
- A tightly coordinated timeline across multiple visual layers.
- Brand-specific colors, fonts, or design tokens not present in any catalog item.

The reason: assemble mode can only compose existing independent blocks. It cannot create new coordinated animation logic.

### Rule 4: Ambiguity Resolution

If the generation mode is still ambiguous after rules 1-3 (neither set of keywords matched, and no technical constraint forced a decision), ask the user:

```json
{
  "purpose": "clarify_generation_mode",
  "question": "你希望这次是快速组合现有 HyperFrames catalog 条目，还是生成一个新的完整 HTML 动画？",
  "choices": ["快速组合现有条目", "生成新的完整 HTML 动画"],
  "recommendation": "generate_new_hyperframes_html"
}
```

Map responses: "快速组合现有条目" -> `assemble_existing_catalog_items`, "生成新的完整 HTML 动画" -> `generate_new_hyperframes_html`.

Set the `recommendation` field based on: if the plan has components with paste-only snippets, recommend generate mode. If all selected items are real blocks with valid `includeSnippet` HTML, recommend assemble mode.

## 8. Step 6: Round 2 AskUserQuestion

After candidates are scored and the draft plan exists, ask the user about their visual preferences. The questions provide both style and motion choices, and include the candidate context so the user can make informed decisions.

```json
{
  "purpose": "clarify_style_motion",
  "candidateContext": [
    "app-showcase (score: 0.82)",
    "vfx-iphone-device (score: 0.71)",
    "logo-outro (score: 0.65)"
  ],
  "questions": [
    {
      "id": "style",
      "question": "这次更想要哪种视觉方向？",
      "choices": [
        "Apple 风高级产品感",
        "电影感/暗调科技",
        "极简清爽",
        "社媒高能/动感",
        "赛博/故障风",
        "杂志编辑风"
      ]
    },
    {
      "id": "motion",
      "question": "动效更偏哪种节奏？",
      "choices": [
        "稳重高级",
        "快节奏冲击",
        "柔和流动",
        "故障/赛博"
      ]
    },
    {
      "id": "transition",
      "question": "场景切换需要什么转场效果？（GPU 加速 @hyperframes/shader-transitions）",
      "choices": [
        "让 AI 根据风格自动选择",
        "流畅高级（cinematic-zoom, light-leak, sdf-iris）",
        "快节奏冲击（glitch, whip-pan, chromatic-split）",
        "不需要转场（直接切）"
      ]
    }
  ]
}
```

Mapping from choices to internal tags:
- "Apple 风高级产品感" -> `["apple_like", "premium"]`
- "电影感/暗调科技" -> `["cinematic", "dark"]`
- "极简清爽" -> `["minimal"]`
- "社媒高能/动感" -> `["social_dynamic"]`
- "赛博/故障风" -> `["cyberpunk"]`
- "杂志编辑风" -> `["editorial"]`
- "稳重高级" -> `["steady_premium"]`
- "快节奏冲击" -> `["fast_impact"]`
- "柔和流动" -> `["soft_fluid"]`
- "故障/赛博" -> `["glitch_cyber"]`

Transition choices mapping:
- "不需要转场" -> `transitionMode: "none"` — skip shader transitions
- "流畅高级" -> `transitionMode: "smooth_premium"` — cinematic-zoom, light-leak, sdf-iris, domain-warp, flash-through-white
- "快节奏冲击" -> `transitionMode: "fast_impact"` — glitch, whip-pan, chromatic-split, ridged-burn, swirl-vortex
- "让 AI 自动选择" -> `transitionMode: "auto"` — AI selects from all 14 based on styleTags/motionTags match

**visualRichness → transitionMode default:**
- `visualRichness="rich"` → `transitionMode: "auto"` (if user didn't answer)
- `visualRichness="plain"` → `transitionMode: "none"` (skip shader transitions)
- If user explicitly answers Round 2, their choice overrides the default.

**After receiving answers**: Re-score the catalog items using the updated `styleTags` and `motionTags` in the intent profile. The new style and motion tags may change the ranking of candidates. Re-select the candidate plan (Step 4) with the updated scores. This ensures the final plan reflects the user's stated preferences rather than initial guesses.

The `candidateContext` field is important -- it gives the user visibility into what was scored highly, so they can correct your assumptions. If the user challenges a specific candidate's inclusion or exclusion, adjust the plan accordingly.

## Step 6.5: Round 3 AskUserQuestion — SFX and BGM

After style and motion are confirmed, ask the user about audio preferences. Use Claude Code's `AskUserQuestion` tool:

```json
{
  "purpose": "clarify_audio",
  "questions": [
    {
      "id": "sfx",
      "question": "是否需要为转场/动画添加音效？",
      "choices": ["不需要", "需要（我将提供音效文件路径）", "需要（帮我搜索/生成音效）"]
    },
    {
      "id": "bgm",
      "question": "是否需要添加背景音乐？",
      "choices": ["不需要", "需要（我将提供音乐文件路径）", "需要（帮我搜索/生成背景音乐）"]
    }
  ]
}
```

**If a narration script (`narration.json`) was found in the directory**, add voice selection. First, list available voices from the API — this shows both system voices and the group's cloned voices:

```bash
python3 ~/.claude/skills/hyper-animator/scripts/tts-gen.py --list-voices
```

The output groups voices by type:
- **System Voices** — built-in, always available (e.g., `Chinese (Mandarin)_Reliable_Executive`)
- **Cloned Voices** — voices cloned by your group via the voice cloning API (highest priority — these sound most natural for your content)
- **AI-Generated Voices** — voices created via text-to-voice design

Present the grouped list to the user with `AskUserQuestion`. Show cloned voices first as the recommended options, then a curated set of system voices:

```json
{
  "id": "voice",
  "question": "选择旁白音色。以下为本组织已克隆的音色和推荐系统音色：",
  "choices": [
    "<cloned_voice_1_id>（已克隆）",
    "<cloned_voice_2_id>（已克隆）",
    "Chinese (Mandarin)_Reliable_Executive（系统·可靠男声）",
    "XiaoR_001（系统·默认女声）",
    "其他音色（请说明voice_id）"
  ]
}
```

Dynamically populate the first 2-3 choices with the group's cloned voices (from the `voice_cloning` array in the API response). Then add 2-3 recommended system voices. The user can also type a custom `voice_id`.

#### Subtitle Styling

If the user chose to use narration, ask about subtitle preferences:

```json
{
  "id": "subtitles",
  "question": "字幕样式偏好？字幕将对应口播稿生成SRT文件+HTML字幕层。",
  "choices": [
    "白色字 + 半透明黑底 + 底部（默认）",
    "白色字 + 无背景 + 底部",
    "黄色字 + 黑底 + 底部",
    "无字幕"
  ]
}
```

Map choices to `subtitle_style` values:
| Choice | font_size | color | background | position |
|--------|-----------|-------|------------|----------|
| 白色黑底（默认） | 36 | #ffffff | rgba(0,0,0,0.6) | bottom |
| 无背景 | 36 | #ffffff | transparent | bottom |
| 黄色黑底 | 32 | #ffff00 | rgba(0,0,0,0.7) | bottom |
| 无字幕 | — | — | — | — |

#### Audio Volume Mixing

When BOTH narration and BGM/SFX are present, the narration must be clearly audible over the background. Add a volume configuration question after voice selection:

```json
{
  "id": "volumes",
  "question": "音量设置。默认人声100%，背景音和音效30%以确保旁白清晰。",
  "choices": [
    "使用默认值（人声100%，BGM/SFX 30%）",
    "人声80%，BGM/SFX 50%",
    "人声100%，BGM 20%，SFX 40%",
    "自定义各轨道音量"
  ]
}
```

Map choices to `data-volume` values for `<audio>` elements:

| Track | Default | track-index |
|-------|---------|-------------|
| Narration | `1.0` | 11 |
| BGM | `0.3` | 10 |
| SFX | `0.3` | 12 |

The `data-track-index` convention separates audio layers: BGM=10 (lowest), Narration=11 (middle, mixed above BGM), SFX=12 (highest). This ensures the renderer stacks audio correctly regardless of element order in HTML.

### Choice Mapping

Map user choices to internal values:

| User Choice | Value |
|---|---|
| "不需要" | `none` |
| "需要（我将提供音效文件路径）" | `user_provided` |
| "需要（帮我搜索/生成音效）" | `ai_or_search` |

### Next Actions by Choice

**Sound Effects:**
- `none` → No SFX files needed. Skip SFX setup.
- `user_provided` → Ask the user for file paths. Copy files to `hyperframes-output/`. Store paths in `sfxPaths` array.
- `ai_or_search` → Use web search (e.g., Firecrawl) to find royalty-free SFX, or generate via available audio tools. Common SFX needs: transition whoosh, UI click, reveal chime, impact hit. Save to `hyperframes-output/sfx-<type>.<ext>`. Store paths.

**Background Music:**
- `none` → Skip beat detection entirely. HTML generation uses Path C (no audio).
- `user_provided` → Ask user for the music file path. Copy to `hyperframes-output/bgm.<ext>`. Proceed to Step 6.6 Beat Detection. Set `bgmPath`.
- `ai_or_search` → Search for or generate suitable BGM (matching the style and energy of the composition). Save to `hyperframes-output/bgm.<ext>`. Proceed to Step 6.6 Beat Detection. Set `bgmPath`.

**Skip this step entirely** if the user explicitly said "no sound" or "no music" in their initial request — default both to `none`.

## Step 6.6a: Audio Generation

Only execute if `bgmMode` is `user_provided` or `ai_or_search`. The goal is to produce a BGM audio file at `hyperframes-output/assets/bgm-full.<ext>`.

### Generation Priority

```
1. Minimax music-2.6 API (if MINIMAX_API_KEY configured)
   ↓ failure / rate limit / no key
2. BGM skipped (silent composition)
```

### Method 1: Minimax API (Only Method)

**Critical Rule -- Script Only**: Never hand-assemble Minimax HTTP requests in shell commands or Python inline code. All Minimax API calls MUST go through `scripts/minimax-gen.py`. The script owns: request validation, retry policy, error normalization, audio download, silence trimming. Always run `--dry-run` first to validate configuration before any real call.

**API Key Setup**: Two ways to provide credentials (checked in order):

1. **Environment variable** (recommended for CI/automation):
   ```bash
   export MINIMAX_API_KEY=eyJ...
   ```
2. **`.env` file** (recommended for local use, set up by `npm install` automatically):
   ```
   ~/.claude/skills/hyper-animator/.env:
   MINIMAX_API_KEY=eyJ...
   MINIMAX_GROUP_ID=...  # optional
   ```

If neither is configured, guide the user:
- Register at https://platform.minimaxi.com
- Create API key in Console → API Keys
- Either export the env var, or edit `~/.claude/skills/hyper-animator/.env`

```bash
python3 ~/.claude/skills/hyper-animator/scripts/minimax-gen.py \
  --style "<comma-separated style tags>" \
  --motion "<comma-separated motion tags>" \
  --bpm <bpm> --duration <seconds> \
  -o hyperframes-output/assets/bgm-full.mp3
```

Style/motion tags are mapped to Minimax prompt keywords per `references/scoring.json` (`promptMappings`). The script handles: .env reading, prompt construction, API call, rate-limit retry, response parsing, download. Exit 0 = success, exit 1 = failure.

### Method 2: BGM Skipped

If minimax-gen.py exits with code 1, the composition proceeds **without BGM**.
Report to the user and continue with silent composition.

**CRITICAL — Do NOT attempt local BGM generation.** Never:
- Write or run `generate_bgm.py`, `make_bgm.py`, or any ad-hoc Python BGM script
- Use Python `wave` module for BGM synthesis
- Use `ffmpeg` tone generators as BGM fallback
- Call any script other than `scripts/minimax-gen.py` for background music

Minimax API is the ONLY BGM source. If it fails, the composition is silent.

### SFX Files

If `sfxMode != none`, generate SFX via Python wave snippets (whoosh, glitch, hit). Place in `hyperframes-output/assets/sfx-<type>.wav`.

## Step 6.6b: Beat Detection

Run on the generated audio file from Step 6.6a.

### Prerequisite

The vendored `music-beat-detector` (librosa-based, high-accuracy BPM + structure analysis) is bundled at `vendor/music-beat-detector/`. The wrapper script `scripts/beat-detector.py` tries the vendored version first, falls back to pure-Python.

For best accuracy, optionally install:
```bash
pip install librosa numpy
```

### Run Analysis

```bash
python3 ~/.claude/skills/hyper-animator/scripts/beat-detector.py \
  -i hyperframes-output/assets/bgm-full.wav \
  -o hyperframes-output/<composition-name>-beat.json \
  --pretty
```

If BGM is .mp3 (Minimax output), convert to WAV first:
```bash
python3 -c "import wave, struct; print('wav module ready')"
# If needed: ffmpeg -i bgm-full.mp3 bgm-full.wav
```

Store the output path in the intent profile: `beatDataPath = "hyperframes-output/<composition-name>-beat.json"`. This path is used in Step 8 to inline beat timestamps into the HTML.

### Extract Rhythm Context

From the beat JSON, extract these key facts for the Step 8 HTML generation prompt:

1. **BPM and tempo feel**: e.g., "128 BPM — fast tempo, use dense quick-cut timing"
2. **Structure map**: e.g., "intro(0-8s) → build-up(8-16s) → drop(16-32s) → outro(32-40s)"
3. **Energy peaks at**: e.g., "16.5s, 32.0s" — match major transitions
4. **Silence regions**: avoid starting animations during these
5. **First 32 beat timestamps (ms)**: For inlining into `__beats` array

### Handle Detection Failures

If `beat-detector` fails, fall back to BPM-based beat calculation using the BPM specified in the Minimax prompt (or a default of 120 BPM). Generate `__beats` array manually.

## Step 6.6c: Narration TTS Generation

Only execute if a narration script (`narration.json`) was detected in Step 0.

### Script

Call the bundled `scripts/tts-gen.py` — one API call per scene, downloads WAV files:

```bash
python3 ~/.claude/skills/hyper-animator/scripts/tts-gen.py \
  --script narration.json \
  --voice <voice_id> \
  -o hyperframes-output/assets/
```

- Uses Minimax TTS API (`/v1/t2a_v2`, model `speech-2.8-turbo`)
- Generates `scene-1.wav`, `scene-2.wav`, ... — one file per scene
- WAV format, 32kHz mono, compatible with HyperFrames `<audio>` element
- Exit code 0 = all scenes generated; 1 = some failed (continue with available clips)

### Narration Audio in HTML

Each scene clip is added as an `<audio>` element with `data-start` matching the GSAP scene timeline:

```html
<!-- Narration clips — one per scene, time-aligned to GSAP keyframes -->
<audio class="clip" data-start="0" data-duration="5" data-track-index="11"
       data-volume="1.0" src="assets/scene-1.wav"></audio>
<audio class="clip" data-start="5" data-duration="8" data-track-index="11"
       data-volume="1.0" src="assets/scene-2.wav"></audio>
```

The `data-start` for each scene MUST match the GSAP timeline keyframe where that scene begins. Use `sync_hint` from the script JSON to cross-reference with the outline sections. The `data-duration` from TTS `extra_info.audio_length` ensures the audio element spans exactly the narration clip length.

In the **preview HTML**, narration plays in sync with scene animations and BGM. In the **render HTML**, all `<audio>` elements are captured by the HyperFrames renderer — no ffmpeg needed.

## 9. Step 7: HTML Generation -- Assemble Mode

In assemble mode, compose a wrapper HTML document that includes existing catalog items by reference. The goal is NOT to rewrite visual content but to orchestrate existing pieces.

### Wrapper Composition Structure

```html
<div data-composition-id="wrapper" data-start="0" data-duration="<total>" data-width="1920" data-height="1080">
  <!-- Block includes with data-composition-src -->
  <!-- Component snippets (resolved from paste comments) -->
</div>
```

The wrapper's `data-width`, `data-height`, and `data-duration` must match the values from Step 4. See `references/checklist.md` items 1 and 4 for root attribute requirements.

### Block Handling

For each selected block item:
- If `install.includeSnippet` starts with a `<div` tag (valid HTML composition-src), use it directly: `<div data-composition-src="compositions/block-id.html" ...></div>`.
- If the snippet is a paste comment, attempt to read the real source from `install.path` resolved relative to the hyperframes installation directory. The path is relative to the HyperFrames root.

### Component Snippet Resolution

Component items are the critical path. Their `install.includeSnippet` may be one of:

1. **Real HTML**: A complete `<div>...</div>` or `<style>...</style>` block -- use directly.
2. **Paste comment**: `<!-- paste from compositions/caption-pill-karaoke.html -->` -- you MUST resolve this:
   - Read the file at `install.path` relative to the hyperframes install directory.
   - If resolution succeeds, inline the real snippet content.
   - If resolution fails, emit `<!-- MISSING SNIPPET: item-id; switch to generate mode -->` AND switch to `generate_new_hyperframes_html` mode.
3. **Attach function**: Some components provide `window.attachComponentName(tl, options)` -- include the script and call the attach function after the timeline is created.

The reason component resolution matters: paste comments are developer notes, not renderable content. A renderer cannot execute "paste from X" instructions.

### Wrapper Timeline

The wrapper still needs a GSAP timeline for the HyperFrames runtime to control playback:

- Create `gsap.timeline({ paused: true })`.
- Set `window.__timelines["wrapper-comp"] = tl;`.
- Pad the timeline to the declared total duration: `tl.to({}, { duration: totalDuration }, 0)`.
- If components provide attach functions, call them to integrate into the wrapper timeline.

### Dimension & Duration Consistency

The wrapper's `data-width`, `data-height`, and `data-duration` must match the values determined in Step 4. Blocks included via `data-composition-src` should declare their own durations; the wrapper duration must be at least as long as the sum of all nested content.

## 10. Step 8: HTML Generation -- Generate Mode

In generate mode, you are creating a completely new HyperFrames HTML composition. This is the more complex path -- you are writing original animation code that must be renderable, deterministic, and compliant with the HyperFrames runtime contract.

### Load the Patterns Reference

FIRST, load `references/HyperFrames-AI-Generation-Patterns-codex.md`. This 778-line document contains the hard-won patterns from scanning 132 actual HyperFrames source files. It covers:

- Block vs Component structural requirements (sections 3.1, 3.2).
- Timeline registration patterns for different scenarios (section 4): standard sync, font-dependent, asset-dependent, WebGL fallback.
- GSAP usage patterns (section 5): set initial states, staggered reveals, easing conventions, data-track-index for multi-clip compositions.
- CSS scoping conventions (section 6).
- SVG and filter patterns (section 7).
- Canvas and WebGL patterns with fallback strategies (sections 8, 9).
- Caption/karaoke component patterns (section 10).
- Production checklist (section 11).

Do not skip this step. The patterns document exists because the agent pseudocode delegates to it via `generateWithLLM`. You must internalize its rules before writing HTML.

### Load Top Catalog Items' Source Code

The catalog scoring only matched keywords — it told you WHICH items are relevant but didn't show you their code. You must now read the real HyperFrames HTML source of the top-3 candidates from the local source cache.

The cache was populated during `npm install` by `scripts/sync-catalog.py`. Source files are at:

```
~/.claude/skills/hyper-animator/references/source-cache/
  blocks/<id>.html       (108 items)
  components/<id>.html   (25 items)
```

For each of the top-3 items from Step 4 (main block + up to 2 components), read the cached source:

```bash
CACHE=~/.claude/skills/hyper-animator/references/source-cache

# Read the main block source (300-500 lines of real HyperFrames code)
cat "$CACHE/blocks/<item-id>.html"

# Read component sources if available
cat "$CACHE/components/<component-id>.html"
```

These files are real HyperFrames blocks with:
- Working GSAP timelines (`paused: true`, `window.__timelines[id]`)
- Production CSS scoping under `[data-composition-id="..."]`
- 4-layer structure, `data-track-index`, `data-start`, `data-duration`
- Font loading, async resource patterns, WebGL fallback strategies

**Why this matters**: The LLM generates orders-of-magnitude better HTML when it can imitate real HyperFrames code rather than guessing from item names. The patterns codex gives rules; the cached source files show those rules in action.

If a cache file is missing or empty, fall back to `hyperframes add <id>` to download it (network-dependent but works). Run `python3 scripts/sync-catalog.py --check` to verify cache integrity.

Always load at least the main block source. Aim for top-3 items total.

### Pre-Generation Check (when BGM is active)

Before writing HTML, confirm:
1. Beat detection JSON exists at `hyperframes-output/<name>-beat.json`.
2. `meta.bpm` and `beats[]` array have been read.
3. Scene boundaries are expressed in beat indices (not seconds) for the GSAP timeline.

### Pre-Generation: Load GSAP Skills

Before writing ANY GSAP code, invoke these four skills to load official animation
patterns. Each skill provides best practices that improve animation quality:

1. **`gsap-skills:gsap-core`** — easing curves, stagger defaults, duration guidelines
2. **`gsap-skills:gsap-timeline`** — position parameter, nested timelines, playback
3. **`gsap-skills:gsap-plugins`** — MotionPath, DrawSVG, MorphSVG (use when needed)
4. **`gsap-skills:gsap-performance`** — will-change, layer promotion, RAF optimization

These skills define HOW to write quality GSAP. The constraints below
(HyperFrames requirements) define WHAT must be true for correct rendering.

**GSAP patterns adaptation for HyperFrames:**
- Autoplay patterns → wrap in `{ paused: true }`
- `ScrollTrigger` patterns → ignore (not applicable to video)
- Performance tips (`will-change`, `backface-visibility`) → add to generated CSS
- All timelines → register on `window.__timelines` with correct `data-composition-id`

### Critical Checklist

Follow the 12-item checklist in `references/checklist.md`. Key rules that must be memorized:

- **GSAP skills loaded**: gsap-core, gsap-timeline, and gsap-performance skills were invoked before writing GSAP code. Animation patterns follow official GSAP best practices.

- Fixed `data-composition-id`, `data-width`, `data-height`, `data-duration` on root element
- `gsap.timeline({ paused: true })` + `window.__timelines[id]` registration
- CSS scoped under `[data-composition-id="..."]` or unique component ID
- Timeline padded to declared duration
- No `Date.now()`, `setInterval()`, or `Math.random()` — use seeded PRNG for randomness
- Async resources (fonts, images, WebGL) loaded before timeline registration
- 4-layer visual structure: Atmosphere → Subject → Motion Accent → Focus/Finish
- **Preview with hyperframes play**: Do NOT inject player HTML into the composition. After generation, run `preview-gen.py --input <html> --output <dir>` to fix audio ids and create an `index.html` symlink. Preview with `hyperframes play <dir>` — HyperFrames' built-in `<hyperframes-player>` web component handles all playback controls. The composition HTML stays pure for rendering.
- Audio: `<audio>` elements with `data-start`/`data-duration`/`data-track-index` (see Step 6.6a-6.6c)
- Beat sync (when BGM present): in-line `__beats` array + `beat(n)` function from beat detection JSON. ALL scene transitions and major reveals use `beat(n)` instead of hardcoded seconds. See `references/checklist.md` item 12 for the mandatory rule.
- **Explicit catalog item references**: If `explicitCatalogRefs` is non-empty, EVERY referenced item MUST be incorporated into the composition. For each ref, either embed it via `data-composition-src` or inline its source code with proper attribution. Missing an explicitly requested catalog item is a generation failure — do NOT present the HTML to the user if any `explicitCatalogRefs` items are absent.

### Motion Vocabulary → GSAP Mapping

Map user-facing style descriptions to concrete GSAP parameters. Apply these in
Step 10 when building scene animations. Source: HyperFrames Prompt Guide.

**Motion & easing:**

| User says | GSAP ease | Duration | Feels like |
|-----------|-----------|----------|------------|
| smooth / 柔和 | `power1.out` (`sine.out`) | 0.4s | Natural deceleration |
| snappy / 利落 | `power4.out` | 0.3s | Quick and decisive |
| bouncy / 弹跳 | `back.out` | 0.5s | Overshoots then settles |
| springy / 弹性 | `elastic.out` | 0.6s | Oscillates into place |
| dramatic / 戏剧 | `expo.out` | 0.8s | Fast start, long glide |
| dreamy / 梦幻 | `sine.inOut` | 1.0s | Slow, symmetrical |

**Timing shorthand:** fast (0.2–0.3s) = energy, medium (0.4–0.5s) = professional,
slow (0.6–0.8s) = luxury, very slow (1–2s) = cinematic.

**Transition energy → shader match:**

| Energy | Shader | CSS fallback |
|--------|--------|-------------|
| Calm / 柔和 | cross-warp-morph, light-leak | blur crossfade |
| Medium / 中速 | whip-pan, sdf-iris | push slide |
| High / 高速 | glitch, ridged-burn, chromatic-split | zoom through |

**Anti-patterns (from Prompt Guide):**
- Don't skip entrance animations — elements appearing without animation feel broken on video.
- Don't skip transitions between scenes — jump cuts are almost always unintentional.
- Don't leave static images sitting unanimated for >2s — add subtle scale/opacity drift.

### Visual Richness Requirements (when `visualRichness = "rich"`)

Unless the user explicitly requested minimal/plain/simple, the generated
composition MUST fulfil all four requirements below:

**1. Shader transitions between EVERY scene.** No plain cuts. Default to
`smooth_premium` set (cinematic-zoom, light-leak, sdf-iris, domain-warp,
flash-through-white) unless user style matches `fast_impact` (then use
glitch, whip-pan, chromatic-split). Include the CDN script and init call.

**2. Complex entrance animations.** Main title/hero elements use
`elastic.out`/`back.out`/`expo.out` eases with stagger delays or piece-by-piece
assembly. Secondary elements use `power4.out` with 80-150ms stagger.
No plain `fadeIn` without scale/position shift unless the element
genuinely benefits from subtlety.

**3. VFX layers (minimum 2 per scene).** Pick from:
- Grain overlay (animated CSS `background-image` with noise)
- Cinematic vignette (radial gradient edge darkening)
- Particle system (canvas-based float, data stream, spark)
- Light leak / lens flare accent (warm gradient with blend-mode)
- Chromatic aberration / edge glow (CSS `text-shadow` with color channels)
- Frosted glass panels (backdrop-filter blur on semi-transparent cards)
- Scanline / CRT overlay (repeating-linear-gradient)

**4. Audio energy.** BGM defaults to 120-140 BPM instrumental with beat-synced
visual pulses (`beat(n)` for every major reveal). SFX on key moments
(whoosh on transitions, impact on titles, chime on reveals).

**When `visualRichness = "plain"`:** skip all of the above. Simple fade/cut
transitions, straightforward fade/slide entrances, no VFX layers, calm BGM.

### Render HTML (`<name>.html`)

Generate the composition HTML. Write to `hyperframes-output/<name>.html`. See `references/checklist.md` for the full checklist.

### Generation Workflow

1. Write the core composition HTML (layers, GSAP keyframes, audio elements) to `hyperframes-output/<name>.html`
2. Run `preview-gen.py` to fix audio ids and prepare for preview:
   ```bash
   python3 ~/.claude/skills/hyper-animator/scripts/preview-gen.py \
     --input hyperframes-output/<name>.html \
     --output hyperframes-output/
   ```
3. Preview with `hyperframes play hyperframes-output/` — opens browser with built-in `<hyperframes-player>` controls
4. Render from `<name>.html` via `hyperframes render`

### Preview Options

**Quick preview:** `hyperframes play <dir>` — lightweight browser player with
play/pause/seek/volume controls. Use for first-pass review of timing and audio.

**Debug/Edit:** `hyperframes preview <dir>` — full Studio with timeline editor,
element selection, real-time lint, and hot reload. When the user reports a
visual issue, they can click the problematic element and you query it:

```bash
hyperframes preview --context --json --context-fields selection
```

Returns the selected element's `hfId`, `selector`, `boundingBox`, and
`textContent` — enabling precise targeted fixes instead of guessing.

**11. Audio Integration (conditional):** Based on Round 3 choices, add audio to the generated HTML.

### Shader Transitions (when transitionMode != "none")

When shader transitions are active, include the CDN script and call `HyperframesShaderTransitions.init()` to add GPU-accelerated scene transitions. The init function attaches to an existing GSAP timeline and returns the augmented one.

```html
<script src="https://cdn.jsdelivr.net/npm/@hyperframes/shader-transitions/dist/index.global.js"></script>
```

```javascript
// After building all scene animations on 'tl':
var finalTl = HyperframesShaderTransitions.init({
  bgColor: "#0a0a14",      // composition background color
  scenes: ["scene-1", "scene-2", "scene-3", ...],
  transitions: [
    { time: <scene_boundary_seconds>, shader: "<shader_name>", duration: 0.7 },
    ...
  ],
  timeline: tl,
  compositionId: "<data-composition-id>",
});
window.__timelines["<id>"] = finalTl;
```

**Shader selection by mode:**
- `smooth_premium`: cinematic-zoom, light-leak, sdf-iris, domain-warp, flash-through-white
- `fast_impact`: glitch, whip-pan, chromatic-split, ridged-burn, swirl-vortex
- `auto`: AI selects from all 14 shaders based on matching styleTags/motionTags

**Duration:** 0.6-0.9s per transition, calculated from adjacent scene durations.
If a shader is unavailable (WebGL not supported), the timeline falls back to normal playback.

**Nested compositions:** External composition files loaded via `data-composition-src`
must wrap their content in a `<template id="...">` tag. Inline nested compositions
(defined directly inside the parent `<div>`) do not use `<template>`.

**Critical: HyperFrames captures audio via `<audio>` HTML elements — NOT Web Audio API (AudioContext).** The frame-screenshot renderer cannot capture real-time AudioContext synthesis. All audio must be pre-rendered as WAV files and referenced via `<audio>` elements with `data-start`, `data-duration`, and `data-track-index` attributes. These attributes tell the renderer when to mix each audio track into the final video.

**Audio element format:**

```html
<audio id="bgm-track" class="clip"
       data-start="0" data-duration="72" data-track-index="10"
       data-volume="0.3"
       src="assets/bgm-full.wav"></audio>
```

- `data-start`: time (seconds) when audio begins playing
- `data-duration`: length of audio segment to use
- `data-track-index`: render layer ordering (use a high number like 10 so audio sits above visual layers)
- `data-volume`: optional, 0.0–1.0 (default 1.0)
- `src`: path to WAV file, relative to the HTML file's directory

**Path A — SFX only (sfxMode != none, bgmMode = none):**

Generate individual SFX WAV files using Python's `wave` module. Place `<audio>` elements at the correct start times matching visual transitions:

```html
<!-- SFX: placed at transition timestamps -->
<audio class="clip" data-start="5.0" data-duration="0.4" data-track-index="10"
       data-volume="0.4" src="assets/sfx-whoosh.wav"></audio>
<audio class="clip" data-start="9.45" data-duration="0.3" data-track-index="10"
       data-volume="0.5" src="assets/sfx-hit.wav"></audio>
```

The `data-start` value MUST match the GSAP timeline keyframe time where the transition occurs. No JavaScript audio code needed — the renderer handles playback.

**Path B — SFX + BGM with beat sync (bgmMode != none):**

Pre-generate a single combined WAV file containing BGM + all SFX mixed at their correct positions. This is the simplest and most reliable approach. Use Python `wave` module:

```python
"""Generate combined audio track (BGM + SFX) as a single WAV file."""
import wave, struct, math, random, os

SR = 44100
OUT = 'assets'
BPM = 110  # from beat detection or user preference
BEAT = 60 / BPM
DURATION = 72  # match HTML data-duration

# Sound generators: kick, snare, hihat, bass, whoosh, glitch, hit
def kick(vol=1.0):
    n = int(SR * 0.2)
    return [math.sin(2*math.pi*(150-110*i/n)*i/SR)*(1-i/n)*vol for i in range(n)]

def snare(vol=0.6):
    n = int(SR * 0.15)
    s = [(random.random()*2-1)*0.6 + math.sin(2*math.pi*200*i/SR)*0.4*(1-i/n) for i in range(n)]
    return [x*vol for x in s]

def hihat(vol=0.25):
    n = int(SR * 0.04)
    s = [(random.random()*2-1)*0.5 for _ in range(n)]
    s = [s[i]-s[i-1] if i>0 else s[i] for i in range(len(s))]
    return [s[i]*(1-i/n)*vol for i in range(n)]

def whoosh(dur=0.35, vol=0.45):
    n = int(SR * dur)
    s = [(random.random()*2-1) for _ in range(n)]
    return [s[i]*math.sin(2*math.pi*(2000-1800*i/n)*i/SR)*(0.8-0.7*i/n)*vol for i in range(n)]

def hit_sfx(vol=0.7):
    dur=0.3; n=int(SR*dur)
    return [math.sin(2*math.pi*(80-50*i/n)*i/SR)*(1-i/n)*vol*0.7 +
            (random.random()*2-1)*(1-i/dur)*vol*0.25 for i in range(n)]

def glitch_sfx(dur=0.07, vol=0.4):
    n = int(SR * dur)
    s = [(random.random()*2-1)*(1-i/n) for i in range(n)]
    return [s[i]*math.sin(2*math.pi*800*i/SR)*vol for i in range(n)]

def mix_into(track, sound, start_t, volume=1.0):
    idx = int(SR * start_t)
    for i, s in enumerate(sound):
        if idx + i >= len(track):
            track.extend([0.0] * (idx + i - len(track) + 1))
        track[idx + i] += s * volume

# Initialize track + build BGM drum loop + mix SFX at transition times
track = [0.0] * int(SR * DURATION)
# ... (build BGM beat loop, mix SFX at each transition time)

# Normalize, fade in/out, write WAV
max_val = max(abs(s) for s in track)
track = [s/max_val*0.7 for s in track]
# Fade in/out: track[i] *= i/fade_samples
# Write: wave.open('assets/bgm-full.wav', 'w') ...
print(f"Written: assets/bgm-full.wav")
```

Then in HTML, a single `<audio>` element:

```html
<!-- AUDIO TRACK: combined BGM + SFX, pre-mixed to match visual timing -->
<audio id="bgm-track" class="clip"
       data-start="0" data-duration="72" data-track-index="10"
       data-volume="0.3"
       src="assets/bgm-full.wav"></audio>
```

The beat data STILL drives visual timing (use `beat(n)` for keyframes), but the audio itself is pre-rendered. This guarantees perfect sync — the audio WAV was built to match the same beat structure.

**Path C — No audio (sfxMode = none AND bgmMode = none):**

No `<audio>` elements. Silent composition — existing behavior, remains the default.

### Write the HTML File

Generate a single self-contained HTML file. Write to `hyperframes-output/<name>.html`. No player UI is injected — the composition stays pure for rendering.

```bash
mkdir -p hyperframes-output/

# Write the composition HTML
cat > hyperframes-output/<name>.html << 'HTML_EOF'
<!doctype html>
<!-- Composition — layers, GSAP, audio elements -->
...
HTML_EOF
```

Preview is handled separately via `hyperframes play` (see Generation Workflow above).

**When revising** (Step 11): regenerate `<name>.html` and re-run `preview-gen.py` to fix audio ids and update the index.html symlink.


## 11. Step 9: Pre-Render Quality Gates

Before presenting HTML to the user or rendering it, run all quality gates against the generated output. These gates are defined in `references/quality-gates.yaml` and encoded in `scripts/validate-quality.sh`.

### Automated Check

```bash
bash ~/.claude/skills/hyper-animator/scripts/validate-quality.sh <html-file> <mode> [intent-profile-json]
```

The script returns exit 0 if all gates pass, 1 with failure details.

**Always pass the intent profile** as the third argument so Gate 13 can verify that all user-requested catalog items are present:

```bash
bash ~/.claude/skills/hyper-animator/scripts/validate-quality.sh \
  hyperframes-output/<name>.html \
  generate_new_hyperframes_html \
  hyperframes-output/intent-profile.json
```

If any gate fails, fix the issue and re-run. Do NOT present the HTML to the user until all gates pass.

```bash
bash /Users/zhichao/.claude/skills/hyper-animator/scripts/validate-quality.sh <html-file> <generation-mode>
```

The script returns exit code 0 if all gates pass, or 1 with failure details.

### Manual Check Details

Failure messages and fix guidance in `references/checklist.md` (Quality Gate Failures appendix).

## 12. Step 10: User Validation

After quality gates pass, the user MUST preview the composition before confirming render.
This is mandatory — never skip the preview step.

### Step 10a: Launch Preview (MANDATORY)

After quality gates pass and before asking about rendering, ALWAYS run `hyperframes play`:

```bash
hyperframes play hyperframes-output/
```

This opens the browser with the built-in `<hyperframes-player>`. The user watches
the animation, checks timing, verifies audio sync. Report the preview URL to
the user: `http://localhost:3003`.

Only AFTER the user has seen the preview, proceed to Step 10b.

### Step 10b: User Validation

Use Claude Code's `AskUserQuestion` tool:

```json
{
  "purpose": "user_validation_before_render",
  "summary": {
    "generationMode": "generate_new_hyperframes_html",
    "selectedItems": [
      {"id": "app-showcase", "title": "App Showcase", "score": 0.82, "reasons": ["intent: product_launch", "format: landscape_16_9"]},
      {"id": "caption-pill-karaoke", "title": "Caption Pill Karaoke", "score": 0.68, "reasons": ["role: caption"]}
    ],
    "width": 1920,
    "height": 1080,
    "duration": 30,
    "styleTags": ["apple_like", "premium"],
    "motionTags": ["steady_premium"]
  },
  "questions": [
    {
      "id": "approved",
      "question": "已经预览过动画效果，可以进入渲染吗？",
      "choices": ["可以，开始渲染", "需要修改"]
    },
    {
      "id": "feedback",
      "question": "如果需要修改，请说明要改的地方。",
      "optional": true
    }
  ]
}
```

Present the user with:
1. **Generation mode**: Whether we are assembling or generating new HTML, and why.
2. **Selected items**: Which catalog items were selected/referenced, with their scores and reasons.
3. **Dimensions**: Width, height, duration.
4. **Style and motion tags**: The visual direction and motion rhythm.
5. **Summary**: A concise description of what the final output will look like.

## 13. Step 11: Revision Loop

If the user responds with "需要修改" (needs changes), process their feedback:

1. **Parse the feedback**: Extract what needs to change -- style, content, dimensions, selected items, or timing.
2. **Update the plan**: Modify the intent profile or plan accordingly.
3. **Re-generate**: Re-run the relevant pipeline steps (score update, HTML generation, quality gates).
4. **Re-validate**: Run quality gates again on the new HTML.
5. **Re-present**: Ask for user validation again with the updated content.

Common revision types and how to handle them:

- **Style change**: User wants different visual direction. Re-run Step 6 style/motion input (skip catalog re-scoring if the change is purely cosmetic -- just regenerate HTML with new style parameters).
- **Content change**: User provides different product name, script, data. Update `contentInputs` in intent profile and regenerate.
- **Dimension change**: User wants different aspect ratio. Update format and regenerate. May need to re-select catalog items if format changes from landscape to portrait.
- **Item replacement**: User wants a different block or component as reference. Adjust the selected items in the plan and regenerate.
- **Duration change**: User wants shorter or longer video. Adjust duration and pad or compress the timeline accordingly.

The revision loop can repeat multiple times. There is no hard limit, but note that each cycle requires regeneration and quality checking. If the user is making very small iterative changes, batch the feedback if possible.

**After each revision**: commit the changes with `git add <updated-files> && git commit -m "fix: revision — <summary>"`. This creates a checkpoint before the next round of feedback.

**Rollback**: If the user wants to undo changes, run `git log --oneline -10`, let them choose a commit, then `git reset --hard <commit>`. All commits are preserved in `git reflog` for recovery. Do NOT create branches or worktrees.

## 14. Step 12: Render via CLI

Once the user approves ("可以"), render the HTML to video using the HyperFrames CLI:

1. **Render from `<name>.html`**: The self-contained HTML file is the single source of truth. HyperFrames expects `index.html` as entry point — create a symlink:

```bash
cd hyperframes-output
ln -sf <name>.html index.html
hyperframes render . -o ./<name>.mp4 --skill hyper-animator
rm -f index.html
```

2. **Audio rendering**: HyperFrames captures audio via `<audio>` HTML elements with `data-start`/`data-duration`/`data-track-index` attributes. Pre-generated WAV files referenced by `src` are mixed into the final video. The render output's `hasAudio` flag will be `true` and `audioCount` will reflect the number of `<audio>` elements found.

**IMPORTANT**: Do NOT use Web Audio API (AudioContext, `new Audio().play()`) — the frame-screenshot renderer cannot capture real-time JS audio synthesis. Always pre-generate WAV files (using Python `wave` module) and reference them via `<audio>` elements.

3. **Present the output path** to the user:

```json
{
  "renderOutput": "./hyperframes-output/my-composition.mp4",
  "compositionId": "my-composition",
  "dimensions": "1920x1080",
  "duration": 30,
  "status": "rendered",
  "sourceFile": "./hyperframes-output/my-composition.html",
  "hasAudio": true,
  "audioCount": 1
}
```
```

### Error Handling

- **CLI not found**: Inform the user that `hyperframes` CLI is not installed and guide them through installation. The HTML file is still valid and can be rendered later.
- **Render failure**: If the CLI returns an error, capture the error message and present it to the user. Common causes: missing dependencies, invalid HTML, resource timeouts.
- **Partial success**: Some compositions may render with warnings. Present the output path but note any warnings.
- **Fallback**: If rendering fails entirely, the user can still use the HTML file in their browser or save it for later rendering. The generated HTML is a valid, functional web page independent of the renderer.

### Output Organization

All output files live in `hyperframes-output/` within the project directory:

```
hyperframes-output/
├── <name>.html          # Self-contained HTML (composition + embedded player)
├── <name>.mp4           # Rendered video
├── <name>-beat.json     # Beat detection data (if BGM selected)
└── assets/              # Audio + image assets
```

This keeps the project self-contained — nothing in `/tmp`, nothing to lose on reboot.

## 15. References

- **Scoring formula, prompt mappings, taxonomies, quality gates quickref**: `references/scoring.json`
- **Step 8 generation checklist**: `references/checklist.md`
- **Quality gates (full contract)**: `references/quality-gates.yaml`
- **Generation patterns**: `references/HyperFrames-AI-Generation-Patterns-codex.md`
- **Catalog map (133 items)**: `references/hyperframes-catalog-map.json`
