| name | mkt-hyperframe-talking-head-video-16-9 |
| description | Build a TikTok/Reels/YouTube **landscape 16:9 (1920×1080)** short video from a pre-recorded talking-head MP4 using HyperFrames — transcribe Vietnamese audio, scaffold a complete preview-ready 1920×1080 project with a slide pane (1200px) on the left and a floating avatar frame on the right, GSAP timeline có SPLIT↔PIP transitions (slide full-screen + avatar PIP corner thumbnail), breathing yoyo zoom + beat-driven punch-in trên avatar, 5 scene compositions kiểu "tier-row" pattern (post-stack hook / chats-stack problem / hero-orb solution / counter-row recap / terminal-row CTA), optional cream-paper editorial infographic image slots với `<img onerror>` fallback, 6 SFX wired tại scene boundaries, **NO captions** (giọng avatar là spine). USE WHEN user nói "edit video 16:9 từ HeyGen MP4", "scaffold project landscape", "podcast keynote editor", "talking head + slide editor", "build hyperframes 1920×1080", "tạo project 16:9 từ avatar mp4", "scaffold 16:9 hyperframes", "hyperframe landscape video editor", "edit 16:9 từ source.mp4 + voiceover.mp3", or has a HeyGen avatar MP4 + ElevenLabs voiceover MP3 + script.txt và muốn ra preview-ready 16:9 HyperFrames project. ALSO trigger as Phase 3 delegate cho `mkt-full-video-with-11-hyperframe-heygen-16-9` orchestrator. KHÁC với sibling `mkt-hyperframe-talking-head-video` (sibling là 9:16 vertical với captions là spine; skill này là 16:9 landscape với avatar voice là spine, không captions). |
mkt-hyperframe-talking-head-video-16-9
Dedicated 16:9 (1920×1080) landscape editor — input là 1 folder workspace có sẵn voiceover.mp3 + source.mp4 + script.txt, output là cùng folder đó được scaffold đầy đủ thành HyperFrames preview project sẵn npx hyperframes preview.
Đây là layer operational dưới orchestrator mkt-full-video-with-11-hyperframe-heygen-16-9. Orchestrator handle Phase 1 (TTS) + Phase 2 (HeyGen avatar) → handoff folder cho skill này lo Phase 3 (transcribe + scaffold + lint + preview).
What this skill produces
<workspace_folder>/
├── voiceover.mp3 # input (untouched)
├── source.mp4 # input (HeyGen avatar lip-sync, untouched)
├── script.txt # input (untouched)
├── transcript.json # generated — word-level Whisper output
├── transcript-cleaned.json # generated — VN typo-cleaned
├── scenes-outline.json # generated — pre-checkpoint outline (5 scenes)
├── scenes.json # generated — final per-scene metadata
├── index.html # generated — root composition 1920×1080
├── compositions/
│ ├── scene-1.html # 1 file per scene, regardless of N
│ ├── scene-2.html # authored by parallel LLM sub-agents (Phase 3d)
│ ├── scene-3.html # NOT generated by Python templates (deprecated)
│ ├── ...
│ └── scene-N.html
├── sfx/ # 6 SFX symlink/copy from skill assets
│ ├── camera-flash.mp3
│ ├── Whoosh sound effect (1).mp3
│ ├── búng tay.mp3
│ ├── Laser.mp3
│ ├── ting.mp3
│ └── Discord Notification - Sound Effect.mp3
└── (optional) infographics/
├── 1.png 2.png 3.png # if --with-infographics
└── prompts.md # cream-paper editorial prompts per scene
Canvas:
- Root:
data-width="1920" data-height="1080".
- Avatar frame default SPLIT:
top:100, left:1290, width:540, height:880, borderRadius:32.
- Avatar frame PIP corner:
top:600, left:1540, width:320, height:420, borderRadius:20.
- Slide-mount:
1200×1080 SPLIT, animatable to 1920×1080 during PIP.
- Each composition's intrinsic size:
data-width="1200" data-height="1080" (fills 100% of mount, expands when mount widens).
Inputs needed (workspace folder convention)
| File | Required | Note |
|---|
voiceover.mp3 | Yes | ElevenLabs TTS output (Hoàng's voice). |
source.mp4 | Yes | HeyGen avatar lip-sync. Currently 9:16 portrait — landscape frame uses object-fit: cover; object-position: center 25% để crop hợp lý. See references/source-mp4-cropping.md. |
script.txt | Yes | Vietnamese script that produced voiceover.mp3. Used for typo correction + scene heading derivation. |
scenes-outline.json | No | If user pre-defined 5 scenes, skip auto-detection. Schema: see Step 3. |
Slug = basename of the workspace folder (no derivation needed). All scripts read/write within that folder. Scripts accept --workspace <path> flag and default to CWD.
The pipeline (sequential script invocation)
voiceover.mp3 + source.mp4 + script.txt
│
▼
1. transcribe_audio.py ── Whisper VN → transcript.json (word-level)
│
▼
2. clean_transcript.py ── strip Whisper artifacts, apply VN dict
│
▼
3. detect_scenes.py ── transcript + script.txt → scenes-outline.json (5 scenes)
│
▼
4. [optional CHECKPOINT] ── user duyệt outline / chỉnh kicker, heading
│
▼
5. scaffold_project.py ── writes scenes.json (full schema), creates sfx/ + logos/ + avatar.jpg + yt-lower-third.html
│
▼
6. 🔥 LLM SUB-AGENT FANOUT 🔥 ── orchestrator spawns N parallel general-purpose
Agents (1 per scene) in a single message, each
authoring compositions/scene-{num}.html from
visual-plan.json + references/composition-patterns.md.
NOT generate_compositions.py (Jinja2 generator
DEPRECATED — collapses to 5 archetypes, can't adapt
scene-specific metaphors, breaks listicle 6+ scenes).
│
▼
7. generate_root_index.py ── writes index.html with slide-mount (mounts scene-{num}.html
for ALL scenes, no kind→fs-lesson mapping) +
avatar-frame (claude-orange #d97757 brand border) +
GSAP timeline (SPLIT↔PIP, breathing, punch-in,
PIP_EVENTS, hybrid hook zoom) + 6 SFX wired +
cream-paper b-roll img layer at root (full 1920×1080)
│
▼
8. [optional] scaffold_infographic_slots.py
── add <img onerror> placeholders + write prompts.md
(cream-paper editorial per scene metaphor)
│
▼
9. lint_and_preview.py ── npx hyperframes lint → 0 errors, then preview
│
▼
HAND-OFF — Studio URL printed; user reviews; orchestrator decides render.
Preview-first rule (non-negotiable): skill kết thúc ở Step 9 với Studio URL. Render MP4 chỉ chạy sau user explicit confirm — that step belongs to orchestrator hoặc user manual.
Step 1: transcribe_audio.py
python3 scripts/transcribe_audio.py --workspace <folder>
Wraps existing skill mkt-ai-video-extract-srt-segment if available locally; otherwise falls back to whisper CLI directly:
whisper voiceover.mp3 --model medium --language vi --word_timestamps True \
--output_format json --output_dir .
KHÔNG bao giờ dùng --model medium.en cho audio Việt — .en translates instead of transcribes. Always --language vi.
Output: transcript.json — array of {text, start, end} word-level entries.
Step 2: clean_transcript.py
Re-uses logic từ sibling 9:16 editor (mkt-hyperframe-talking-head-video/scripts/clean_transcript.py). Strips U+FFFD, applies VN word-level + phrase-level dict (alphabit→Alphabet, Cod→Code, etc.), groups into 3-5 word phrases. Output: transcript-cleaned.json. (No caption-groups.json — captions are OFF in 16:9.)
Step 3: detect_scenes.py
Đọc transcript-cleaned.json + script.txt → infer 5 scene boundaries dùng standard knowledge-video skeleton:
| # | Kind | Default kicker hint | Variant |
|---|
| 1 | hook | First sentence of script | tier-row (post-stack feel via kind=hook) |
| 2 | problem | Where script transitions to "ChatGPT/AI cũ", "khó", "đứt mạch", "vấn đề"… | chats-stack |
| 3 | solution | Where script introduces tool/method | hero-orb |
| 4 | recap | Where script shows numerical result ("X tiếng", "Y khách hàng") | counter-row |
| 5 | cta | Last sentence with comment/lưu/follow trigger | terminal-row |
If script không có 5 phần rõ ràng, output ít hơn (min 3) và warn user. Heuristic: split transcript by sentence boundaries into ~5 chunks weighted 1.5x to scene 3 (solution thường dài nhất).
Output scenes-outline.json:
[
{ "num": 1, "kind": "hook", "start": 0.07, "end": 13.73, "kicker": "...", "heading": "...", "variant": "tier-row" },
{ "num": 2, "kind": "problem", "start": 13.73, "end": 27.37, "kicker": "...", "heading": "...", "variant": "chats-stack" },
{ "num": 3, "kind": "solution", "start": 27.37, "end": 47.75, "kicker": "...", "heading": "...", "variant": "hero-orb" },
{ "num": 4, "kind": "recap", "start": 47.75, "end": 53.80, "kicker": "...", "heading": "...", "variant": "counter-row" },
{ "num": 5, "kind": "cta", "start": 53.80, "end": 60.68, "kicker": "...", "heading": "...", "variant": "terminal-row" }
]
CHECKPOINT — print outline cho user duyệt. User có thể edit kicker/heading/variant inline rồi continue. Skip checkpoint nếu skill được gọi với --auto (orchestrator mode).
Step 4: scaffold_project.py
Reads scenes-outline.json → enriches each scene with:
accent_words — derive from heading (extract numbers + last 2-3 nouns)
content — pattern-specific schema (see assets/templates/)
badges — 1-3 floating badges per scene
brollEnd, hasBreath — for SPLIT↔PIP scheduling
pip_events — derived from accent_words timing within scene
Writes scenes.json. Also auto-copies these self-contained assets from skill-bundled assets/ into the workspace:
| Asset | Source | Destination | Purpose |
|---|
| 6 SFX files | assets/sfx/ | <workspace>/sfx/ | Beat sounds (camera-flash, whoosh, búng-tay, Laser, ting, Discord-notification) |
| Brand logos | assets/logos/{chatgpt,gemini,claude,claude-code}.{png,jpg} | <workspace>/assets/logos/ | Auto-attached to compositions (chats-stack avatars, hero-orb, tier-row tool-badge) |
| Avatar | assets/avatar.jpg | <workspace>/assets/avatar.jpg | Channel thumbnail for yt-lower-third subscribe banner |
| YT lower-third | assets/compositions/yt-lower-third.html | <workspace>/compositions/yt-lower-third.html | Subscribe banner mount, last 3s of video |
If a source asset is missing in the skill bundle, falls back to legacy workspace path (workspace/assets/...) silently. If neither is found, that asset is skipped (compositions degrade gracefully — text avatars instead of logos, no lower-third).
Step 5: 🔥 LLM SUB-AGENT FANOUT (composition authoring) 🔥
generate_compositions.py is DEPRECATED. Do not call it. The Python Jinja2 generator collapses every scene into 1 of 5 fixed archetypes (hook → tier-row, problem → chats-stack, solution → hero-orb, recap → counter-row, cta → terminal-row), can't adapt to scene-specific metaphor (e.g., "scroll-tape-wasted", "two-clock-comparison", "right-tool-vehicles"), and breaks on listicle 6+ scenes (multiple tip-N collapse to same filename → overwrite).
Canonical method: orchestrator (or user) spawns N parallel general-purpose Agents (1 per scene) in a single message so they run concurrent. Each sub-agent:
- Reads its scene block from
visual-plan.json (path: <workspace>/visual-plan.json, scene index num-1)
- Reads references from the orchestrator skill:
references/composition-patterns.md (8 production-shipped patterns), references/slide-design-tokens.md (palette + typography)
- Picks the right pattern for its scene's metaphor — adapts freely (hero-orb can become vault+spokes, stats-3card can become two-clock-comparison)
- Authors
compositions/scene-{num}.html from scratch with: 1200×1080 viewport, scoped CSS via [data-composition-id="scene-{num}"], GSAP timeline registered to window.__timelines["scene-{num}"], no !important, no embedded image-slot (cream-paper image lives at root layer only)
The result: each scene gets a unique, metaphor-fit composition file. No collisions, no boring 5-archetype lock-in.
See orchestrator's mkt-full-video-with-11-hyperframe-heygen-16-9/SKILL.md Step 4d for the full sub-agent prompt template.
Brand logos in compositions (sub-agents reference these)
Sub-agents embed brand logos when their pattern needs them. Logos live at <workspace>/assets/logos/:
| Pattern usage | Brand element |
|---|
| Hook tier-row "after" | tool-badge pill (top-right) — Claude logo + name. |
| Problem chats-stack fail rows | ChatGPT/Gemini logos auto-attached when name matches keyword (chatgpt/gpt → chatgpt.png; gemini/google → gemini.jpg). |
| Solution hero-orb / vault | Brand logo inside the orb glow (default Claude). |
| CTA terminal-row | No logos by default. |
Sub-agents fall back to text-avatar if assets/logos/ doesn't exist (no flicker).
Pattern reference (8 archetypes, sub-agents pick + adapt)
The orchestrator's references/composition-patterns.md ships 8 production-tested patterns:
| Pattern | Best for | Internal motif |
|---|
tier-row before-after | Hook contrast | 2 tier rows (before rose/red + after lime/green) with letter + items |
chats-stack | Problem / fail / Edit-Regen demo | User row + 2-3 AI fail rows + broken-chain stamp |
hero-orb + spec-trio | Solution / mechanism / vault central | Glowing orb + task icons + 3 spec tier rows |
counter-row | Recap / number proof | Big counter (FROM → TO) + clients + delta tag |
comment-terminal | CTA / "comment X" | Tier-letter + macOS terminal mock + 2 gift cards |
stats-3card | Knowledge intro / 3-fact reveal | 3 columns landing-style with // LBL + grad number |
comparison-2col | "When to use which" | 2-column Yes/Yes chips |
image-slot | (DEPRECATED — image at root layer) | — |
Sub-agents adapt freely: e.g., hero-orb → "vault + 5 chat spokes", stats-3card → "two-clock-comparison", comparison-2col → "3-tier vehicle cards". The pattern doc gives the skeleton (HTML + CSS + GSAP), the LLM does the creative tailoring.
Step 6: generate_root_index.py
Reads scenes.json + total_duration from transcript → renders assets/templates/index.html.j2:
- 1920×1080 root with
#slide-bg, #heygen-bg, #avatar-frame > .avatar-breathing > .avatar-punch > #v-source
<audio id="a-source"> tracks original source.mp4 audio (track 1)
- 6
<audio> SFX wired with absolute data-start (scene boundaries)
- Brand mark top-left
- GSAP block:
- Initial entrance (avatar fade-in, brand fade-in)
- SPLIT/PIP constants +
goPIP(t) / goSplit(t) helpers
PIP_EVENTS array driven by per-scene pip_events from scenes.json
- "Hybrid Hook" visual zoom strategy on
.avatar-breathing:
- HOOK scene: slow ramp 1.0 → 1.10 over scene duration (drama build,
power2.in)
- HOOK→BODY transition: snap reset 1.10 → 1.0 (release tension, 0.5s
power3.in)
- BODY scenes (2..N-1) at PIP-IN events: 1.06 punch beats (rhythm,
power2.out → power3.out)
- CTA scene: gentle 1.0 → 1.04 push (close,
sine.inOut)
- Per-scene-start small punch (1.06) on
.avatar-punch for transition beats
<div class="clip slide-mount"> × N mounts wired with data-composition-src + data-start + data-duration + data-track-index
- Cream-paper b-roll PIP-swap layer: 1
<img class="broll-image" id="broll-N"> per scene, full stage 1920×1080, object-fit: contain with cream #F0EEE6 letterbox (blends into image bg). Fades in during PIP windows of its scene, fades out otherwise. When N.png doesn't exist on disk yet, browser shows broken image at opacity 0 — no flicker.
- YT subscribe lower-third (optional, default ON): if
compositions/yt-lower-third.html exists, mounts at last 3s with z-index 60 (pointer-events:none). Disable with --no-yt-lower-third. Custom duration via --yt-lower-third-duration <sec>.
Step 7: scaffold_infographic_slots.py (DEPRECATED)
This script injected <img class="infographic-slot"> inside each composition's HTML — that's wrong. The cream-paper image should live at the root layer (full stage 1920×1080) and fade in/out during PIP windows, not be embedded inside the 1200×1080 composition (which would shrink the 16:9 image into a small box).
The root broll layer is now auto-baked by generate_root_index.py (Step 6) — it writes 1 <img class="broll-image" id="broll-N"> per scene at z-index 25 between slide-mount and avatar-frame, with GSAP fade in/out keyed off PIP_EVENTS.
Cream-paper prompts (prompts.md) are written by the planner skill (mkt-plan-short-video-edit-16-9/scripts/render_infographic_prompts.py) — not by this editor skill. User generates 1.png-N.png via AI33/Nano Banana Pro afterward; root broll layer picks them up automatically when files appear on disk.
If you have a project that previously ran scaffold_infographic_slots.py, those embedded infographic-slot divs in compositions/ should be removed (they bóp 16:9 cream-paper images into small boxes). Spawn an LLM sub-agent per composition to clean them up and recompose the freed space.
Step 8: lint_and_preview.py
cd <workspace>
npx hyperframes lint
npx hyperframes preview &
Lint posture:
- Errors: 0 acceptable.
- Warnings:
composition_self_attribute_selector is cosmetic and acceptable (templates use scoped [data-composition-id="..."] selectors).
- Errors that must be fixed:
gsap_animates_clip_element — chỉ animate opacity trên clip elements
root_composition_missing_data_start — sub-comps phải có data-start="0" trên inner [data-composition-id] div
media_missing_data_start — <audio> SFX phải có absolute data-start
Print Studio URL (typically http://localhost:3002) and exit. Don't block.
Integration with mkt-plan-short-video-edit-16-9 (visual planner)
When visual-plan.json exists in the workspace, scaffold_project.py reads it and skips the manual scenes.json fill step. The planner already decided variant / tier_letter / items / badges / broll metaphors / pip_events. Editor just translates plan → scenes.json (via the planner's apply_plan_to_scenes.py shim).
visual-plan.json ──► apply_plan_to_scenes.py ──► scenes.json (editor format) ──► 🔥 LLM SUB-AGENT FANOUT 🔥 ──► compositions/scene-{1..N}.html
──► generate_root_index.py ──► index.html
When visual-plan.json does NOT exist, fall back to current behavior: heuristic detect_scenes.py + LLM-fills-content checkpoint. The planner is the recommended entry point (orchestrator Phase 3b) — this fallback exists for standalone editor invocation.
The planner skill ships its own references/visual-thinking-library.md with 25 metaphor entries — when the planner is involved, ignore the editor's older "manually fill content" guidance below (it's superseded).
Key architectural decisions (link to references/)
-
references/pip-event-scheduling.md — When/why to fire goPIP(t) and goSplit(t). Default rule: each scene fires 1 PIP event around its first tier-letter glow scale-in (~scene_start + 1.0–1.5s in scene's local time + offset to absolute) and exits 2.5–4s later. Multiple letters = stagger 2-3s each.
-
references/pattern-selection.md — Decision tree: archetype (Hook/Problem/Solution/Recap/CTA) → recommended variant. Includes when to override default mapping.
-
references/source-mp4-cropping.md — HeyGen renders 9:16 portrait by default. CSS-only fallback (object-fit: cover; object-position: center 25%) works in 540×880 SPLIT and 320×420 PIP, but for tightest framing user should regenerate HeyGen with portrait-tighter-crop or true landscape avatar. Document tradeoffs.
Differences from sibling 9:16 editor
| Concern | 9:16 (mkt-hyperframe-talking-head-video) | 16:9 (this skill) |
|---|
| Canvas | 1080×1920 (vertical) | 1920×1080 (landscape) |
| Layout | Face full-screen with overlay scenes; b-roll fills frame | Slide pane (1200px) left + avatar frame (540×880) right |
| Avatar | Always full-frame; "split-screen" splits face top/bottom | Floating frame; PIP corner during emphasis |
| Captions | Required — caption track is the spine | Skipped — avatar voice is the spine |
| PIP transitions | None (face dominates) | SPLIT↔PIP at every emphasis beat |
| Breathing zoom | None | Yes — .avatar-breathing yoyo |
| Punch-in | None | Yes — .avatar-punch at each scene start |
| Compositions | 7 abstract mockup variants (post-stack/ai-window/etc.) | 5 tier-row patterns (hook/problem/solution/recap/CTA) |
| Infographic style | Glassmorphic dark + neon | Cream-paper editorial (#F0EEE6 + slate ink + hand-drawn) |
| SFX wiring | 6 SFX same mapping | 6 SFX same mapping (identical files) |
| Lint warnings allowed | composition_self_attribute_selector | Same |
Common pitfalls
-
!important on slide-mount internal width breaks PIP — DO NOT use width: 1200px !important inside slide-mount. Use plain width: 1200px. GSAP must be able to animate it to 1920px during goPIP().
-
Composition root selector — use width: 100%; height: 100% (NOT fixed pixels) on [data-composition-id="<id>"] so it fills the mount when mount expands during PIP.
-
Avatar zoom layer separation — DO NOT scale #avatar-frame directly for breathing/punch. Use 2 nested wrappers (.avatar-breathing + .avatar-punch) inside the frame. Otherwise breathing tween fights with PIP top/left/width tween.
-
PIP timing collision with scene transitions — goSplit(t_out) should fire before the next scene's first PIP event. If they overlap, last goSplit/goPIP call wins (overwrite: 'auto' is set).
-
Source MP4 audio track — HeyGen MP4 already contains the lip-synced TTS audio. Use <audio data-start="0" src="source.mp4"> (NOT voiceover.mp3) so audio matches lip movement. Use voiceover.mp3 only if you're doing custom audio post-processing.
-
No audio for voiceover.mp3 separately — duplicating audio = double-volume garbage. Source.mp4 is canonical. voiceover.mp3 is kept in workspace for record-keeping only.
-
Caption mount missing — DO NOT add compositions/captions.html. This is a deliberate skip. Documented above.
-
Infographic slot onerror fallback — placeholders must use onerror="this.style.display='none'" so missing PNGs don't show broken-image icons. User generates infographics later with mkt-broll-image.
-
Lint composition_self_attribute_selector — these are info-only warnings about scoped [data-composition-id="..."] selectors. Templates intentionally use them. DO NOT refactor to remove.
-
SFX volume at avatar voice level — keep 0.18-0.32 range. Above 0.4, SFX drown the avatar.
How orchestrator delegates to this skill
mkt-full-video-with-11-hyperframe-heygen-16-9 Phase 3 invocation:
cd <workspace_folder>
python3 ~/.claude/skills/mkt-hyperframe-talking-head-video-16-9/scripts/transcribe_audio.py --workspace .
python3 ~/.claude/skills/mkt-hyperframe-talking-head-video-16-9/scripts/clean_transcript.py transcript.json
python3 ~/.claude/skills/mkt-plan-short-video-edit-16-9/scripts/plan_visuals.py --workspace . --brand claude
python3 ~/.claude/skills/mkt-plan-short-video-edit-16-9/scripts/render_infographic_prompts.py --workspace .
python3 ~/.claude/skills/mkt-plan-short-video-edit-16-9/scripts/apply_plan_to_scenes.py --workspace .
python3 ~/.claude/skills/mkt-hyperframe-talking-head-video-16-9/scripts/scaffold_project.py --workspace .
python3 ~/.claude/skills/mkt-hyperframe-talking-head-video-16-9/scripts/generate_root_index.py --workspace .
python3 ~/.claude/skills/mkt-hyperframe-talking-head-video-16-9/scripts/lint_and_preview.py --workspace .
Or, equivalently, the orchestrator can call this skill via the Skill tool with one shot — this SKILL.md tells it the order.
This skill does NOT know about the orchestrator. Cross-reference is one-way.
Reference: production project this skill must reproduce
Canonical 16:9 project shipped:
/Users/tonyhoang/Documents/GitHub/claudeclaw-os/workspace/content/2026-05-08/loi-ich-claude-ai/
Given that folder's voiceover.mp3 + source.mp4 + script.txt, running the pipeline above must regenerate index.html + compositions/*.html matching the shipped version (give or take cosmetic differences). When implementing scripts, scan that project before reinventing.
Output handoff template
After Step 9, print to user:
## Video preview sẵn sàng (16:9 landscape)
**Studio URL:** http://localhost:3002
**Project structure:**
- Root: `<slug>/index.html` (1920×1080, ~230 dòng)
- Compositions: `<slug>/compositions/` (5 scenes, 1200×1080 each)
- SFX: `<slug>/sfx/` (6 files)
- Infographic slots: <K> wired with onerror fallback
**Đã apply:**
- Slide pane (1200px) trái + floating avatar frame (540×880) phải
- 5 SPLIT↔PIP transitions tại tier-letter glow moments
- Breathing yoyo + punch-in trên avatar
- 5 scene compositions: hook (tier-row) / problem (chats-stack) / solution (hero-orb) / recap (counter-row) / CTA (terminal-row)
- 6 SFX (camera-flash hook, whoosh L1, snap L2, laser L3, ting recap, discord CTA)
- KHÔNG có caption mount — avatar voice là spine
**Refresh Studio để xem.** Nói "render" khi OK, hoặc báo cụ thể chỗ cần chỉnh.
Stop here. Render is orchestrator's call.