关于 talking-head-recut
Talking Head Recut 是将设计好的图文卡片叠加到现有口播视频并导出 MP4 的技能,面向需要包装访谈、讲解或社交视频的创作者。它依据本地 Whisper 转录内容安排卡片,不更改原视频播放,并由 HyperFrames CLI 完成转录与渲染。
主要功能
- 01提取视频元数据与音频
- 02使用本地 Whisper 生成逐词转录
- 03按转录内容规划卡片与时间
- 04编写卡片 HTML 并组装合成页面
- 05渲染为 MP4 视频
适用场景
- 01为口播视频添加标题和下三分之一字幕条
- 02根据讲述内容呈现数据说明或引言卡片
- 03制作侧栏或画中画图文叠加
- 04将现有视频包装成社交媒体成片
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ heygen-com/ hyperframes --skill talking-head-recut安装后在 Claude Code 中输入/talking-head-recut确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
media-contract.test.mjsNOTICE.mdassets/vendor/gsap.min.jsreferences/DESIGN_INDEX.mdreferences/frames/clean.htmlhairline.htmlpolaroid.htmlreferences/layouts/overlay.htmlpip.htmlsplit.htmlstack.htmlreferences/styles/academic.htmlaudit.htmleditorial.htmlgeom.htmlminimal.htmlspotlight.htmlswiss.htmlterminal.htmlwhiteboard.htmlxhs.html详细介绍
插件安装: 在 HyperFrames 插件中使用此技能时,运行设置或检查更新命令前,请遵循插件执行规则。独立安装请使用下方的更新说明。
首先,保持此技能为最新版本——运行前请先向用户确认:
npx hyperframes skills update talking-head-recut。如果所有内容均为最新版本,此命令会快速执行且不做任何更改;否则,它会在你依赖这些技能之前更新此技能及其所依赖的核心领域技能。
Talking Head Recut
Talking Head Recut 会在一段从头到尾完整播放的本地视频上,叠加一系列定时呈现的设计图形卡片——标题、下三分之一字幕条、数据标注、引言、侧边栏、画中画,并与口播内容同步。Agent 负责设计卡片(时机 + 内容),并在对话中直接编写每张卡片的 HTML,然后组装成一个合成 HTML,通过 hyperframes 渲染为 MP4。没有固定的原型列表,也没有规定的卡片结构——叠加内容取决于转录文本的实际内容。
入口是
/hyperframes。 此技能会为已有的真人出镜片段添加设计好的图形卡片(标题、下三分之一字幕条、数据标注、引言、侧边栏、画中画),而不是普通字幕(即口播内容的文字)。视频片段会原样播放。 其他任何需求——普通字幕、独立图形、从零开始制作视频——或任何不确定的情况 → 请先阅读/hyperframes:意图层负责决定所有路由。
与
embedded-captions同属图形包装类技能。 字幕会以可读字幕的形式显示_口播内容_;此技能则在播放的视频上叠加_设计好的图形_。 普通字幕 →embedded-captions。从零开始制作视频 → 创作工作流(product-launch-video/faceless-explainer/ …)。
通过 /hyperframes 路由后,意图层只确认输入(使用哪个片段),并告知用户渲染策略相关问题将延后询问——画面比例、布局、风格组和卡片数量会留到第 7 步,此时根据探测到的素材和转录文本提供建议;意图层关于运行形式的问题不适用。如果存在 BRIEF.md,其中会记录已确认的输入和用户备注——请先阅读。
工作目录中的可检查中间文件:
metadata.json— 时长 / 宽度 / 高度 / fpsaudio.mp3— 提取出的音频transcript.json— 扁平的词数组[{ text, start, end }, …](Whisper;没有segments,也没有words包装层)storyboard.json— 轻量级卡片大纲(Agent 的计划)public/cards/card-XX.html— 每张卡片对应一个 HTML 片段public/index.html— 最终组装的合成文件output.mp4— 渲染后的视频
CLI 解析
# hyperframes — transcription (local Whisper) + rendering the assembled HTML to MP4
npx hyperframes --help
此技能完全通过 hyperframes CLI 和系统级 ffmpeg / ffprobe 运行。
转录使用本地 Whisper,通过 hyperframes transcribe 执行——无需第三方服务、API 密钥或有速率限制的代理。
工作流
1. 检查环境
npx hyperframes doctor # ffmpeg, headless browser, render deps
# confirm bundled assets:
ls "<SKILL_DIR>/assets/fonts" "<SKILL_DIR>/assets/vendor/gsap.min.js"
必需项:
ffmpeg/ffprobe(系统级)<SKILL_DIR>/assets/fonts/*.woff2、<SKILL_DIR>/assets/vendor/gsap.min.js(随此技能打包,并在第 9 步暂存到工作目录)
转录无需密钥——hyperframes transcribe 会在本地运行 Whisper(第 4 步)。
强烈建议在 macOS 上为 hyperframes render 安装:
export PRODUCER_BROWSER_GPU_MODE=hardware
2. 创建工作目录
所有产物都保存在 videos/<project-name>/ 下——与其他视频工作流(product-launch-video / faceless-explainer / pr-to-video)使用相同的约定。请将当前工作目录保持在工作区根目录;以下所有操作都会写入这一个子目录。
VIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR="videos/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"
3. 提取音频和元数据
# metadata — duration / width / height / fps
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate \
-show_entries format=duration -of json "$VIDEO_PATH" > "$WORK_DIR/metadata.json"
# audio
ffmpeg -y -i "$VIDEO_PATH" -vn -acodec libmp3lame -q:a 2 "$WORK_DIR/audio.mp3"
输出:metadata.json(读取 width/height/duration;fps 为计算后的 r_frame_rate 分数,例如 30000/1001 → 29.97)+ audio.mp3。
4. 转录
npx hyperframes transcribe "$WORK_DIR/audio.mp3" -d "$WORK_DIR" --json --model small.en
本地运行 Whisper——无需 API 密钥、无需代理、没有速率限制。会在工作目录中写入按词划分的
transcript.json(包含每个词的 text 和 start / end 时间戳)。
读取文件,获取驱动第 6 步卡片计时所需的词语 / 句子时间;如果需要按片段划分的数据块,
请自行根据标点 / 停顿将词语分组成句子。
将时间限制在媒体时长内。 Whisper 返回的最后一个词的 end 可能会略微超出
实际片段时长——将每张卡片的 endSec 和 composition.durationSeconds 限制在
metadata.json 中记录的时长内,否则渲染结果会在视频结束后出现一段黑屏。
5. 校正转录文本
transcript.json 是一个扁平的词对象数组——[{ "text": "...", "start": s, "end": s }, …](没有 segments 数组,也没有 words 包装;每个词的键是 text)。读取文件并修正明显的 ASR 错误:
- 同音词、产品名称、技术术语、标点
- 直接编辑词语的
text;保留其start/end时间戳 - 文件中没有预先分组的
segments数组——需要用于卡片计时的片段级数据块时,请自行将词语分组成句子(在句末标点 / 停顿处分开)
6. 草拟轻量级分镜(在聊天中)
无需使用命令行。 读取 transcript.json + metadata.json 并直接设计
卡片。storyboard.json 是仅供 Agent 内部使用的规划材料
——没有命令行命令会读取它;它的作用是在编写每张卡片的 HTML 之前,帮助你理清
时间安排和内容。保持结构与下方示例一致,以便用同一份大纲来指导
你在第 9 步中编写的合成内容:
{
"schemaVersion": 3,
"composition": {
"fps": 30,
"width": 1080,
"height": 1920,
"durationSeconds": 121.2,
"layout": "portrait",
"themeId": "noir",
"seed": 42
},
"videoTrack": {
"sourcePath": "input-video.mp4",
"startSec": 0,
"endSec": 121.2,
"bounds": { "x": 0, "y": 0, "width": 1080, "height": 1920 }
},
"subtitles": { "enabled": false },
"cards": [
{
"id": "card-01",
"intent": "Hook with the speaker's anxious midnight question",
"startSec": 0.5,
"endSec": 13.0,
"accentIndex": 0,
"zone": "fullscreen",
"contentHints": {
"kicker": "AN HONEST QUESTION",
"title": "The soul-searching question at 11 PM",
"detail": "Client's 60-second voice message: 'If the RMB appreciates, does that mean my USD policy is a terrible loss?'"
}
}
]
}
必需的卡片字段:
| 字段 | 类型 | 用途 |
|---|---|---|
id | string | 在卡片 HTML 和 GSAP 选择器中使用的稳定 ID |
intent | string | 自然语言描述;会传入卡片合成流程 |
startSec / endSec | number | 以秒为单位的时间(endSec > startSec) |
accentIndex | 0 | 1 | 2 | 3 | 4 | 此卡片使用的 5 种主题强调色中的哪一种 |
zone | 枚举(见下文) | 卡片在画布上的位置 |
contentHints | object | 自由格式的内容容器;Agent 在此填写 kicker/title/detail/data/quote |
archetype(可选) | string | 可附加的自由格式标签,用于记住卡片的模式;缺省时为自由格式,这是默认值 |
transition(可选) | 枚举:cut | fade | slide | wipe | 声明卡片之间的转场 |
五种 zone 值:
| 区域 | 解析后的边界 | 适用场景 |
|---|---|---|
fullscreen | 覆盖整个画布 | 开场重点、大数字、信念箴言 |
whiteboard-area | 内缩 40px 边距(或纵向画布高度的 45%) | 密集数据/带注释的内容 |
lower-third | 底部 30% 的区域 | 在可见视频上添加注释 |
side-panel | 横向画布右侧 42%,或纵向画布底部 40% | 一侧放数据,另一侧放视频 |
video-overlay | 整个画布,预期使用大部分透明的卡片 | 在铺满画面的全幅视频上叠加注释 |
在第 9 步组装构图时,根据上表将每张卡片的 zone 转换为卡片承载容器上的像素边界。
视频边界在构图级别设置一次(videoTrack.bounds);
如果要让视频看起来像是在“不同卡片间移动”,请在构图的 <script> 中针对
#video-wrap 编写 GSAP 补间动画(见第 9 步)。
不规定卡片角色,也不规定叙事弧线。 卡片应根据视频实际表达的内容生成——可以全是引言,也可以全是数据, 可以用一个数字开场,也可以用一个故事开场。让转录文本决定节奏。
需要多少个要点?——根据时长和信息密度自动推断。 没有固定上限。先根据视频时长选择基础节奏, 再根据信息密度进行调整。唯一固定的下限:至少 5 张卡片,确保短视频也有节奏。
第 1 步——根据时长确定基础节奏(中等信息密度下每张卡片的自然时长,单位:秒):
| 视频时长 | 基础节奏(每张卡片秒数) | 原因 |
|---|---|---|
| < 60s(短视频) | 6–8s | 观众期待短视频采用快速剪辑 |
| 60s – 3 min | 8–12s | 常规社交媒体节奏 |
| 3 – 10 min | 12–20s | 留出停顿空间;每张卡片承载更多内容 |
| 10 – 30 min | 20–35s | 长篇讲座/访谈的节奏 |
| > 30 min | 30–60s | 连载式,接近章节划分的感觉 |
第 2 步——信息密度乘数(与基础节奏相乘):
| 转录文本中的信号 | 乘数 | 效果 |
|---|---|---|
| 高密度——数字多、观点鲜明、节奏紧凑、列举内容多,每 1–2 句话就有一个新观点 | × 0.7 | 剪辑更快、卡片更多 |
| 中等密度——节奏丰富,兼有数据和叙事 | × 1.0 | 基础节奏 |
| 低密度——一个完整的故事、反复换角度阐述、节奏缓慢且具有反思性、围绕单个论点逐步展开 | × 1.5 | 剪辑更慢、卡片更少 |
第 3 步——计算:
secPerCard = basePace × densityMultiplier
cardCount = max(5, round(videoDurationSec / secPerCard))
示例(注意——没有上限;长视频自然会生成更多卡片):
- 30s 短视频,只有一句 punchline(低密度) → 7 × 1.5 = 每张卡片 10.5s → round(30/10.5)=3 → 向下取整至至少 5 张卡片
- 60s 反思式独白(低密度) → 10 × 1.5 = 每张卡片 15s → 4 → 向下取整至至少 5 张卡片
- 121s 数据丰富的正面讲述(高密度) → 10 × 0.7 = 每张卡片 7s → 17 张卡片
- 5 min 访谈,中等信息密度 → 16 × 1.0 = 每张卡片 16s → 19 张卡片
- 10 min 深度解读,高密度 → 16 × 0.7 = 每张卡片 11s → 55 张卡片
- 30 min 讲座,中等信息密度 → 28 × 1.0 = 每张卡片 28s → 64 张卡片
- 1 hr 播客,低密度 → 45 × 1.5 = 每张卡片 67.5s → 53 张卡片
当单张卡片持续超过约 15s 时,应设计更丰富的卡片(数据块、多步揭示、通过错落动画逐步展开的多个子要点)——静态的一行内容超过 8s 就会显得无聊。对于卡片中有许多内容超过 30s 的长视频,可以考虑将时间线拆分为多个子组合(每章一个 .html,通过
data-composition-src挂载),这样每个文件中的 GSAP 时间线都更易于管理——请参阅 HyperFrames lint 警告timeline_track_too_dense。
contentHints 是一个自由形式的对象,其键和嵌套结构可以采用任何适合承载数据的形式。Agent 会根据每张卡片决定其结构。
可选片尾。 此技能不提供固定品牌片尾。如果用户需要结束卡片,请自行设计中性的卡片(字标 + 一行标语,约 1.5-2s,淡入 -> 短暂停留 -> 淡出),将其追加到 cards[],并把 composition.durationSeconds 延长至其 endSec。否则,请以最后一张内容卡片结尾。
7. 决定渲染策略
与用户确认视觉方向(首先执行)
在开始设计卡片或确定边界之前,请用户选择输出比例、布局、风格和卡片密度预设。系统会根据所选的布局 × 风格组合自动选择画框(见下方“自动选择画框”表)。发送问题前,预先计算以下两项:
-
根据源视频的宽高比(
metadata.json中的 width / height)计算recommendedRatio:sourceAspect = width / heightsourceAspect ≥ 1.5(≥ 约 3:2 的横向画面)→ 推荐16:9sourceAspect ≤ 0.7(≤ 约 9:13 的纵向画面)→ 推荐9:160.7 < sourceAspect < 1.5(接近正方形)→ 推荐4:5
在推荐选项的标签中标注“ (recommended · matches source video X:Y)”, 让用户了解推荐理由。
-
根据步骤 6 计算
autoCount(max(5, round(videoSec / (basePace × densityMultiplier))),以便“自动”选项的标签显示具体数量。
环境兼容性——选择最佳可用的问题渠道。 并非所有运行时都提供相同的结构化提问工具。按以下顺序操作:
- 原生澄清工具——使用下面的结构化 4 问题调用。
- 其他原生澄清工具(例如
ask_question、request_user_input、IDE 特定的提示)——使用该工具提出相同的 4 个问题,并提供相同的选项列表。保留推荐标记和预先计算的值。 - 没有原生工具(Codex CLI、纯文本运行时)——直接在普通对话中提问。使用本节末尾的纯文本模板。保持为一条消息、4 个编号问题 (每轮问题数上限为 2–5 个;这里的 4 个符合要求)。
适用于所有渠道的规则:
- 每轮最多提出 2–5 个问题。这里的 4 个符合要求。
- 即使缺少的信息不会妨碍渲染,也要询问一次,以确认会实质影响最终输出的参数(比例、布局、风格、cardCount)。
- 如果用户已预先批准默认值(“直接使用默认值”“不用问”“全部自动选择”),要求你不要提问,或本次运行带有持续生效的自主决策信号(“给我惊喜” / “替我决定”——
../hyperframes/references/brief-contract.md§ 1),则完全跳过提问,并使用:recommendedRatio、layout="stack"(跨比例最稳妥的默认值)、根据转录语气从最中性的类别(编辑 / 数据)中选择的style,以及autoCount。用一句话告知用户你的选择,然后继续。
渠道 A——原生 AskUserQuestion:
// Precompute before the call:
// recommendedRatio = "16:9" | "9:16" | "4:5"
// autoCount = integer (from Step 6)
AskUserQuestion({
questions: [
{
question: "Output video aspect ratio (canvas):",
header: "Aspect ratio",
multiSelect: false,
// Reorder so the recommended option appears FIRST (per AskUserQuestion convention).
// Append " (recommended · matches source video W×H)" to the recommended option's label.
options: [
{ label: "16:9 (1920×1080) landscape", description: "TV / YouTube / desktop playback. Most natural when the source video is already landscape; widest canvas." },
{ label: "9:16 (1080×1920) portrait", description: "TikTok / Reels / short-form mobile. Most natural for portrait source; native mobile experience." },
{ label: "4:5 (1080×1350) near-portrait", description: "Instagram feed / WeChat Moments. Best when source is near-square or you want to cover both platforms." }
]
},
{
question: "Choose the overall layout: how should the video and cards coexist on the canvas?",
header: "Layout",
multiSelect: false,
options: [
{ label: "side-by-side (split)", description: "Video and card each take half the canvas. Most stable for interview / data side-by-side; clear visual separation." },
{ label: "top-bottom (stack)", description: "Video on top (~52%), card below. Classic combo of speaker face + summary card; works well in portrait too." },
{ label: "picture-in-picture (pip)", description: "Card fills the canvas, video shrinks to a rounded corner window. Use when content is primary and speaker is secondary." },
{ label: "full-screen overlay (overlay)", description: "Video plays full-bleed, card floats as a glass layer on top. Strong cinematic / emotional feel." }
]
},
{
question: "Choose the card visual style (style):",
header: "Style group",
multiSelect: false,
// NOTE: these 3 groups intentionally match the frame auto-pick matrix
// rows below, so picking a group resolves both `style` group AND the
// frame matrix column in one step. Memberships are mutually exclusive.
options: [
{ label: "warm paper (warm-paper)", description: "academic notebook · editorial big-type · whiteboard hand-drawn · xhs social. Best for interview reflections, product launches, lifestyle, emotional stories." },
{ label: "clinical / cold (clinical)", description: "audit magazine · swiss grid · terminal CLI · minimal modern. Best for financial analysis, investigative reports, technical tutorials, serious presentations." },
{ label: "experimental / avant-garde (experimental)", description: "geom color-clash geometry · spotlight dark-background. Best for short-form highlights, product launches, strong emotion, cinematic feel." }
]
},
{
question: "Card count (takeaway pacing): how many cards to cut?",
header: "Card count",
multiSelect: false,
options: [
{ label: "Auto (recommended) · approx N cards", description: "Inferred automatically from video duration and information density (see Step 6 rules). This run estimates approx N cards. Substitute the real N (your autoCount) into the label." },
{ label: "Fewer · approx round(N × 0.6) cards", description: "Sparser cuts, each card holds longer — suits reflective / slow-paced content." },
{ label: "More · approx round(N × 1.5) cards", description: "Tighter cuts, faster rhythm — suits staccato / data-dense / short-form highlight content." }
]
}
]
})
关于“其他”——AskUserQuestion 会自动为卡片数量问题添加一个“其他”选项。用户可以直接输入数字(例如“8”“20”)作为 cardCount 目标。将输入解析为整数:如果解析成功 → 使用该值(最低为 5);如果解析失败 → 回退为“自动”。
通道 B — 纯文本备用方案(Codex CLI、不支持原生提问工具的运行时)。将以下内容作为一条普通消息发送,然后等待回复。使用项目符号列出 1/2/3/4,可让回复更易解析:
I need to confirm four visual decisions with you before I start cutting cards:
1) Output aspect ratio (canvas):
A. 16:9 landscape (1920×1080) — TV / YouTube / desktop playback
B. 9:16 portrait (1080×1920) — TikTok / Reels / short-form mobile
C. 4:5 near-portrait (1080×1350) — Instagram feed / works for both platforms
▸ My recommendation: <recommendedRatio> (matches source video W×H = <sourceW>×<sourceH>)
2) Overall layout (how video & card coexist):
A. split side-by-side (50/50)
B. stack top-bottom (video top, card bottom)
C. pip picture-in-picture (card full canvas, video rounded corner window)
D. overlay full-screen glass overlay (video full-bleed, card glass layer)
3) Card style group (maps to frame auto-pick matrix, pick 1 of 3):
A. warm paper (warm-paper) (academic / editorial / whiteboard / xhs)
B. clinical / cold (clinical) (audit / swiss / terminal / minimal)
C. experimental (experimental) (geom / spotlight)
4) Card count (takeaway pacing):
A. Auto (recommended) — approx <autoCount> cards
B. Fewer — approx round(<autoCount> × 0.6) cards
C. More — approx round(<autoCount> × 1.5) cards
D. Give me a specific number (e.g. "8", "20")
Reply format: "1A 2C 3B 4A" or natural language is fine.
If you want all recommended defaults, reply "default" / "auto" / "use all recommendations".
解析纯文本回复:
- 接受宽松格式:
"1A 2C 3B 4A"、"A C B A"、"16:9 / pip / data / auto"、完整句子或default。 - 如果任何答案有歧义 → 只重新询问有歧义的项目(仍需遵守 2–5 项的上限)。
- 如果用户说
"default / auto / use all recommendations"→ 跳过,不再重新询问。
用户通过任意通道回答后:
-
确定输出画布,根据宽高比答案填写精确的
storyboard.composition.width / height值:用户选择 composition.width × height storyboard.layout 字段 16:91920 × 1080 "landscape"9:161080 × 1920 "portrait"4:51080 × 1350 "portrait"(架构将 4:5 视为纵向布局 — 高度 > 宽度)对于
references/layouts/*.html中的 4:5 边界,这些文件只记录了横向(1920×1080)和纵向(1080×1920)的布局。对于 4:5(1080×1350),按比例缩放纵向布局以推导边界:保留水平值,将垂直值乘以1350/1920 ≈ 0.703。例如:overlay纵向卡片 ={ x: 24, y: 1280, w: 1032, h: 564 }→ 4:5 卡片 ={ x: 24, y: round(1280 × 0.703), w: 1032, h: round(564 × 0.703) }={ x: 24, y: 900, w: 1032, h: 397 }。 -
将风格组映射到具体风格,根据文字稿的语气选择最合适的风格,但不得超出用户选择的组。如果无法确定该组中的两种具体风格哪种更合适,请发送第二个
AskUserQuestion,列出这 2–4 种具体风格供选择。 -
根据密度答案确定最终的 cardCount:
用户选择 最终 cardCount Auto(推荐) 已计算出的 autoCountFewer max(5, round(autoCount × 0.6))More round(autoCount × 1.5)(不设上限)Other = ""(整数) max(5, parseInt(n))Other = 其他任何内容 回退到 autoCount -
根据此表自动选择视频边框(边框不询问用户 — 根据布局 × 风格决定):
布局 暖纸张风格(academic / whiteboard / editorial / xhs) 临床风格(audit / swiss / terminal / minimal) 实验性风格(geom / spotlight) splitpolaroidhairlinecleanstackpolaroidhairlinecleanpipclean(pip 胶囊已有装饰边框)cleancleanoverlayclean(全出血布局不允许装饰边框)cleanclean -
用一句话告诉用户你的选择:比例(+画布尺寸)、布局、具体风格、画框和最终的 cardCount——然后继续完成步骤 7 的其余部分(每张卡片的布局、动态模式)。
-
在工作记忆中记录这五个值(ratio / layout / style / frame / cardCount),无需添加 schema 字段;编写步骤 8 中每张卡片的 HTML,以及读取对应的
references/<dim>/<key>.html以了解令牌和结构时,都要参考这些值。
如果用户通过“Other”选择了一个不在 10 种风格库中的自定义风格名称,请将其视为设计全新卡片视觉效果的提示,但仍须遵循所选布局的边界。
渲染策略输入
步骤 7.0 中锁定 ratio / layout / style / cardCount / frame 后,其余每张卡片的决策如下:
- GSAP 目标中的源视频适配方式:视频元素设置了
object-fit: cover,并会裁剪到#video-wrap的补间边界。如果不想裁剪(例如,横向画布上的纵向源视频不应被裁掉上下部分),请将补间目标设为与源视频宽高比相符的矩形,并让周围画布透出(或用卡片/背景填充)。 - 每张卡片的
card.zone:根据所选的构图布局推导(split → side-panel、stack → lower-third、pip → fullscreen、overlay → video-overlay),也可以为个别变体选择不同的区域(hero/quote 用 fullscreen,密集数据用 whiteboard-area)。 - 每张卡片的
accentIndex:每张卡片从 5 个主题强调色中选一个。通过变化来营造节奏;属于同一叙事节拍的两张卡片则复用相同索引。 - 动态效果词汇:从
data-anim类型中选择 2–3 种可重复使用的模式(见后面的表格),并保持一致,让构图更协调。
从以下 themeId 调色板中选择(在构图的 <style> 块中将它们用作 --accent-N / --bg / --text CSS 变量):
| themeId | 强调色调色板(5 种颜色) | 白板背景 | 文本 |
|---|---|---|---|
| classic | #1971c2 #e03131 #2f9e44 #e8590c #9c36b5 | #FFF9E3(纸张) | #1e1e1e |
| noir | #4cc9f0 #f72585 #4ade80 #fb923c #a78bfa | #1a1a1a | #f1f1f1 |
| mint | #0077b6 #d62828 #2d6a4f #e76f51 #7209b7 | #e8faf0 | #1b4332 |
| craft | #bf5700 #d62728 #6c757d #e9b54a #3d5a80 | #f6efe1 | #2d2d2d |
| slate | #0ea5e9 #ef4444 #22c55e #f97316 #a855f7 | #1e293b | #f1f5f9 |
| mono | #000 #555 #888 #aaa #ccc | #fff | #000 |
可用字体(woff2 文件位于 <SKILL_DIR>/assets/fonts/,并在步骤 9 中暂存到工作目录):Caveat(手写体)、
LXGW WenKai TC(中文手写体)、Inter(现代无衬线体)、Virgil
(几何手绘体)。可通过 @font-face 或直接使用 font-family 引用。
如需视觉模式灵感,<SKILL_DIR>/references/styles/
中提供了 10 张独立的参考卡片(academic / editorial / minimal
/ spotlight / geom / whiteboard / audit / terminal / swiss / xhs),可将它们用作起点——但不必受限于这些风格。每张卡片都由你自行设计。
视觉设计库(<SKILL_DIR>/references/)
除了组合级别的 themeId,该技能还提供了更丰富的参考库,位于 <SKILL_DIR>/references/,涵盖三个可自由组合的正交视觉维度:
Style × Layout × VideoFrame
(10) (4) (3)
| 维度 | 键 | 决定的内容 |
|---|---|---|
| 样式 | academic editorial minimal spotlight geom whiteboard audit terminal swiss xhs | 卡片的视觉语言——字体、颜色、装饰、卡片内部布局 |
| 布局 | split stack pip overlay | 视频源与卡片如何共享画布 |
| 边框 | clean hairline polaroid | 视频元素周围的装饰性边框 |
阅读 <SKILL_DIR>/references/DESIGN_INDEX.md
,查看完整矩阵和宽泛的选择指南(访谈 / 产品发布 / 数据分析 /
社交短片 / 技术教程 / 情感故事……)。决定采用特定的
样式 / 布局 / 边框后,阅读对应文件:
references/styles/<key>.html— 自包含的卡片片段,包含该 样式的 CSS 令牌(颜色、字体、内边距、装饰)以及占位摘要。复制.card[data-card-id="ref-<key>"]样式块,将 data-card-id 改为卡片的 id,将占位内容替换为实际摘要,即可完成。references/layouts/<key>.html— 横屏和竖屏下准确的videoBounds+cardBounds。分镜仅记录card.zone。references/frames/<key>.html— 添加为#video-wrap同级元素的装饰性 HTML,以及组合 CSS 的放置说明。
在用户选择的样式组内,可以逐张卡片调整样式,并为每张卡片
选择 布局 × 边框,前提是转场衔接自然。
这 10 种样式是技能侧的设计令牌,不是组合级别的主题——
无需在 storyboard.composition 中声明;它们位于每张卡片的 HTML 内。themeId 字段仍可选择
组合级别的调色板(见上表),用于控制页面主体背景和视频边框。
布局组合(卡片 + 视频)
每张卡片有两项相互配合的决策,决定它如何与源视频共享画布:
card.zone(在storyboard.json中声明)——5 个架构值之一;在第 9 步编写卡片容器包装器的内联style时,将其解析为像素边界(参见第 6 步中的表格)。- 此卡片时间窗口内
#video-wrap的边界(在组合的 GSAP 时间轴中以命令式方式声明)——Agent 会在每次布局转换时,将#video-wrap缓动至目标矩形区域。
架构中不会存储每张卡片的视频边界。videoTrack.bounds 在组合级别只设置一次(默认为全画布)。视频在卡片之间“移动”完全由 index.html 中编写的 GSAP 动画实现。没有 card.layout 字段;架构中只有 card.zone。
4 种组合布局(来自 references/layouts/)——每种都是将 zone 与 #video-wrap 缓动目标配对的方案:
| 组合布局 | 推荐的 card.zone | #video-wrap 的 GSAP 目标(横屏 1920×1080) | #video-wrap 的 GSAP 目标(竖屏 1080×1920) | 适用场景 |
|---|---|---|---|---|
split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | { left: 0, top: 960, width: 1080, height: 960 }(下半部分) | 演讲者与数据并排 / 50:50 比例 |
stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 }(上方 52%) | { left: 0, top: 0, width: 1080, height: 844 }(上方 44%) | 演讲者在上方 + 摘要卡片在下方 |
pip | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } + 添加 .framed 类 | { left: 690, top: 28, width: 360, height: 203 } + 添加 .framed | 内容为主的卡片 + 角落画中画 |
overlay | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 }(全画幅) | { left: 0, top: 0, width: 1080, height: 1920 } | 电影感 / 戏剧化 / 全屏视频上的玻璃卡片 |
对于 4:5(1080×1350),将竖屏 y/h 值乘以 1350/1920 ≈ 0.703 缩放(参见第 7.0 步“频道 A / 频道 B”中的 recommendedRatio 分辨率表)。
一次性变体的其他 zone 值(仍使用 card.zone;不要伪造“layout”字段):
zone | 解析后的边界 | 常见用途 |
|---|---|---|
fullscreen | 覆盖整个画布 | 主视觉卡片,视频缓动至隐藏 / 画中画 |
whiteboard-area | 内缩 40px 边距(横屏)或底部 45% 区域(竖屏) | 密集数据卡片,保留空白边距 |
lower-third | 底部 30% 区域 | 访谈头像注释 |
side-panel | 右侧 42%(横屏)或底部 40%(竖屏) | 侧边栏 / “split”方案 |
video-overlay | 全画布;卡片根元素应为透明 | 全屏视频上的玻璃叠层 |
可以为每张卡片混用不同方案——根据当前时刻选择合适的 card.zone,然后编写卡片之间 #video-wrap 的 GSAP 缓动动画。
故事板渲染规范
storyboard.json 是 Agent 内部的规划产物——没有 CLI
命令会解析它。它用于在编写每张卡片的 HTML 前,明确记录你的时间安排和内容决策。
请遵循下方 v3 风格的结构,以便在第 9 步组装画面时,使用同一份大纲。
必需结构(完整示例见第 6 步):
schemaVersion: 3composition: { fps, width, height, durationSeconds, layout, themeId, seed }— 注意,durationSeconds/fps/themeId/layout位于composition内部,而非顶层videoTrack: { sourcePath, startSec, endSec, bounds? }— 视频边界默认覆盖整个画布subtitles: { enabled, ... }cards[]— 每张卡片包含 6 个必需字段:id、intent、startSec、endSec、accentIndex、zone、contentHints
规则:
- 卡片时间必须位于
composition.durationSeconds范围内;除非有意重叠,否则不应重叠(如有重叠,使用data-track-index控制 z 轴顺序)。 - 视觉细节应写在卡片 HTML 片段中(第 8 步),而非
contentHints。contentHints是你用于设计卡片的结构化提示词;最终渲染效果由 HTML 决定。 - 保持故事板结构稳定——虽然没有程序会解析它,但你在编写第 8/9 步时会回看它,保持一致有助于同步卡片 ID 和时间安排。
- “我选了 overlay × geom × clean”这类 Agent 侧决策不应写入
storyboard.json——请将其保留在工作记忆中,并在编写卡片 HTML 和 GSAP 补间时使用。
与视频共用画布的卡片应使用透明背景。
当 GSAP 补间让卡片后方或旁边的视频保持可见时(overlay
方案、pip 方案,或任何 card.zone = 'lower-third' | 'video-overlay'
场景),卡片的 .root 绝不能绘制完全不透明的背景——否则会遮挡视频。以下是两种模式:
/* Pattern A: transparent root, page body provides the cream backdrop */
html,
body {
background: var(--bg);
}
.card[data-card-id="card-X"] .root {
background: transparent;
}
/* Pattern B: explicit per-card background ONLY for fullscreen cards */
.card[data-card-id="card-hero"] .root {
background: var(--bg);
}
.card[data-card-id="card-overlay"] .root {
background: transparent;
}
对于 side-panel 区域的卡片(分屏方案),卡片容器本身已经只占画布的一半,因此可以使用不透明的卡片背景——它只会覆盖自己所在的一半。
8. 编写每张卡片的 HTML
为每张卡片创建 $WORK_DIR/public/cards/{card-id}.html。每个文件都包含一个符合以下规范的单根 HTML 片段:
卡片 HTML 规范
<div class="card" data-card-id="{cardId}">
<style>
/* MUST: every rule starts with .card[data-card-id="{cardId}"] */
.card[data-card-id="card-01"] .root {
width: 100%; height: 100%;
display: flex; ...;
font-family: 'Caveat', 'LXGW WenKai TC', serif;
color: var(--text);
background: var(--bg);
}
.card[data-card-id="card-01"] .title { font-size: 84px; ... }
</style>
<div class="root">
<h1
id="card-01-title"
data-anim="kinetic-chars"
data-anim-at="0.3"
data-anim-duration="0.5"
data-anim-stagger="0.04"
data-anim-pattern="pop"
>
<span class="char">S</span>
<span class="char">u</span>
</h1>
<div
id="card-01-line"
data-anim="grow-x"
data-anim-at="0.65"
data-anim-duration="0.5"
data-anim-target-w="420"
style="width:0;height:8px;background:var(--accent-0);border-radius:4px;"
></div>
</div>
</div>
硬性规则(hyperframes lint 会拒绝不符合规则的内容):
- 单个根元素
<div class="card" data-card-id="{cardId}"> - 内联
<style>规则必须以以上作用域选择器作为前缀 - 禁止使用
<script>标签 src=/href=中禁止使用外部 URL(不得使用 CDN 或远程字体)- 禁止使用内联事件处理程序(例如
onclick=) - 所有资源都必须通过相对路径引用,并位于同一
public/目录中 - 颜色必须使用
var(--accent-N)等变量,以便兼容不同主题
**动画通过声明定义,而非编码实现。**只能使用 data-anim-* 属性;绝不能编写 <script> 来实现动画。你会在第 9 步将每个 data-anim-* 声明编译到同一个 GSAP 主时间轴中。
卡片尺寸调整——竖屏优先
这 10 个 references/styles/*.html 文件按 1920×1080 横屏
预览设计。当 storyboard.layout = "portrait"(1080×1920,这是社交媒体 / 移动设备的主流场景)时,所有视觉尺寸都要放大——手机通常拿得离屏幕较近,同样的像素数量在横屏电视风格画布上看起来会更大。
| token | 横屏基准 | 竖屏目标 | 缩放比例 |
|---|---|---|---|
| 标题(h1/h2 主标题) | 64–96px | 88–132px | ×1.35 |
| 详情 / 正文 | 24–30px | 30–40px | ×1.30 |
| 引导语 / 标签文字 | 14–16px | 18–22px | ×1.30 |
| 时间码 / 元信息 | 12–14px | 16–18px | ×1.30 |
| 数据块主数值 | 48–60px | 64–88px | ×1.40 |
| 行高倍数 | 1.05–1.5 | 相同 | (不要缩放) |
经验法则: portraitPx = round(landscapePx × 1.3),然后向下取整到邻近的 4px 倍数,以保持视觉节奏。主标题可放大到 ×1.4;小号元信息文字保持 ×1.2,避免拥挤。
竖屏下的内边距应略微缩小——卡片更窄,较大的横屏内边距(40–64px)会占用过多宽度。竖屏下水平方向使用 24–36px 内边距。
如果要制作一张必须适用于两种布局的卡片,请优先在卡片根元素上使用 @container 查询,而不是硬编码尺寸:
.card[data-card-id="X"] .root {
container-type: inline-size;
}
.card[data-card-id="X"] .title {
font-size: clamp(64px, 8.5cqi, 132px);
}
.card[data-card-id="X"] .detail {
font-size: clamp(24px, 3.2cqi, 40px);
}
不过,大多数卡片只需选择一种布局即可——只要选用与分镜 layout 字段对应的尺寸表列。
可用的 data-anim 类型
此列表是封闭的,这是有意为之:卡片是一个 HTML 片段,其动画由本技能在步骤 9 中编译到共享叠加时间轴(参见该步骤中的 GSAP 映射表)。因此,此工作流不会像合成工作流那样搜索 HyperFrames 组件目录——npx hyperframes catalog 返回的是自带时间轴的独立合成,而卡片没有可挂载它的位置。若要实现下列类型无法在卡片作用域内的纯 CSS <style> 中表达的效果,请使用这些类型。
| 类型 | 用途 | 关键参数 |
|---|---|---|
fade-in | 入场 | at、duration、ease? |
fade-out | 退场 | at、duration、ease? |
slide-in | 滑动入场 | at、duration、from=left|right|top|bottom、distance |
kinetic-chars | 逐字符弹出 | at、duration、stagger、pattern=pop|fade — 元素需要包含 <span class="char"> 子元素 |
typewriter | 逐字符淡入 | 与 kinetic-chars 相同,但默认的 stagger 更慢 |
count-up | 数字动画 | at、duration、from、to、format=.0f|.1f|.2f|,d |
draw-path | SVG 路径显现 | at、duration — 元素应为 <path> |
grow-y | 柱状图高度 | at、duration、target-h(px)— 元素初始为 height:0 |
grow-x | 柱状图宽度 | at、duration、target-w(px)— 元素初始为 width:0 |
scale-pop | 弹出入场 | at、duration |
blur-in | 从模糊到清晰 | at、duration |
mask-reveal | 遮罩显现 | at、duration、direction=left|right|top|bottom |
morph-to | 补间任意 CSS | at、duration、props='{...JSON...}' |
data-anim-at 是相对于卡片 startSec 的秒数——在步骤 9 中将每个声明编译到 GSAP 时间轴时,加上卡片的 startSec 得到绝对时间,并量化到 1/fps。
9. 组装合成 HTML
暂存资源并编写 $WORK_DIR/public/index.html:
# SKILL_DIR is injected by the host ("Base directory for this skill: …")
SKILL_DIR="<SKILL_DIR>"
mkdir -p "$WORK_DIR/public/fonts" "$WORK_DIR/public/vendor" "$WORK_DIR/public/cards"
cp -n "$SKILL_DIR/assets/fonts/"* "$WORK_DIR/public/fonts/"
cp -n "$SKILL_DIR/assets/vendor/gsap.min.js" "$WORK_DIR/public/vendor/"
# stage the input video — RE-ENCODE with dense keyframes. Sources with a sparse GOP
# (keyframe interval > ~1s) freeze on seek in the renderer (a frozen frame under the
# overlays); -g / -keyint_min set to your composition fps make every frame seekable.
# (Set both to your fps — 30 shown; use 24/25/60 to match.)
ffmpeg -y -i "$VIDEO_PATH" -c:v libx264 -crf 18 -g 30 -keyint_min 30 \
-pix_fmt yuv420p -movflags +faststart -c:a aac "$WORK_DIR/public/input-video.mp4"
合成模板
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style>
@font-face {
font-family: "Caveat";
src: url("fonts/Caveat-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Caveat";
src: url("fonts/Caveat-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
@font-face {
font-family: "LXGW WenKai TC";
src: url("fonts/LXGWWenKaiTC-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Inter";
src: url("fonts/Inter-400-latin.woff2") format("woff2");
font-weight: 400;
font-display: block;
}
@font-face {
font-family: "Inter";
src: url("fonts/Inter-700-latin.woff2") format("woff2");
font-weight: 700;
font-display: block;
}
@font-face {
font-family: "Virgil";
src: url("fonts/Virgil.woff2") format("woff2");
font-display: block;
}
:root {
/* Pick from the themeId palette table in Step 7 — example: classic */
--bg: #fff9e3;
--text: #1e1e1e;
--accent-0: #1971c2;
--accent-1: #e03131;
--accent-2: #2f9e44;
--accent-3: #e8590c;
--accent-4: #9c36b5;
--font-family: "Caveat", "LXGW WenKai TC", serif;
}
* {
box-sizing: border-box;
}
/* Body font-family MUST list concrete font names (not just var(--font-family)) —
the HyperFrames renderer's static analyzer doesn't expand CSS variables when
resolving fonts, so a var-only chain triggers `font_family_without_font_face`
lint and falls back to a generic. Use the concrete chain here; cards that
want the theme font can still reference var(--font-family) internally. */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background: #000;
font-family: "Inter", "Caveat", "LXGW WenKai TC", ui-sans-serif, system-ui, sans-serif;
}
#stage {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
/* video-wrapper holds the source video. Its position / size are animated
over time by the master timeline (one tween per layout transition). */
.video-wrapper {
position: absolute;
left: 0;
top: 0;
width: 1920px;
height: 1080px;
overflow: hidden;
border-radius: 0;
box-shadow: none;
}
.video-wrapper video {
width: 100%;
height: 100%;
object-fit: cover;
}
.card-host {
position: absolute;
pointer-events: none;
overflow: hidden;
}
.card-host .card {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
}
.card-host .char {
display: inline-block;
visibility: visible;
}
/* Subtle drop shadow + rounded corners for non-fullscreen video framings */
.video-wrapper.framed {
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
}
</style>
</head>
<body>
<div
id="stage"
data-composition-id="talking-head-recut"
data-start="0"
data-duration="121.2"
data-fps="30"
data-width="1920"
data-height="1080"
>
<div class="video-wrapper" id="video-wrap">
<video
id="bg-video"
src="input-video.mp4"
playsinline
data-has-audio="true"
data-start="0"
data-duration="121.2"
data-track-index="1"
></video>
</div>
<div
class="card-host clip"
data-card-id="card-01"
data-start="1.0000"
data-duration="6.5000"
data-track-index="2"
style="left:0;top:0;width:1920px;height:1080px;visibility:hidden;opacity:0;"
>
</div>
<div
class="card-host clip"
data-card-id="card-02"
data-start="8.0000"
data-duration="12.0000"
data-track-index="2"
style="left:0;top:0;width:960px;height:1080px;visibility:hidden;opacity:0;"
>
</div>
<script src="vendor/gsap.min.js"></script>
<script>
(function () {
// count-up formatter helper
window.__fmt = function (v, fmt) {
if (typeof fmt === "string" && /^\.[0-9]+f$/.test(fmt)) {
return Number(v).toFixed(Number(fmt.slice(1, -1)));
}
if (fmt === ",d") return Math.round(v).toLocaleString();
return String(Math.round(v));
};
const tl = window.gsap.timeline({ paused: true });
// ── Card lifecycle (one block per card) ──
// Example for card-01 [1.0, 7.5] with kinetic-chars at +0.3, grow-x at +0.65:
// Enter (fade in over 0.4s)
tl.set('.card-host[data-card-id="card-01"]', { visibility: "visible" }, 1.0);
tl.fromTo(
'.card-host[data-card-id="card-01"]',
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: "power2.out" },
1.0,
);
// Card-internal anims (compile each data-anim-* declaration here)
tl.from(
'.card[data-card-id="card-01"] #card-01-title .char',
{ opacity: 0, y: 8, scale: 0.8, duration: 0.5, ease: "power2.out", stagger: 0.04 },
1.3,
);
tl.fromTo(
'.card[data-card-id="card-01"] #card-01-line',
{ width: 0 },
{ width: 420, duration: 0.5, ease: "power2.out" },
1.65,
);
// Exit (fade out over 0.35s, ending at endSec)
tl.to(
'.card-host[data-card-id="card-01"]',
{ opacity: 0, duration: 0.35, ease: "power2.in" },
7.15,
);
tl.set('.card-host[data-card-id="card-01"]', { visibility: "hidden" }, 7.5);
// ── Video framing transitions ──
// When the next card uses a different composition layout, animate the
// video-wrapper to its new bounds. Example: card-01 = fullscreen
// (video hidden behind), card-02 = split composition (zone="side-panel"
// → video on right, card on left).
// Card-02 enters at 8.0s with the split composition. Animate video to
// the right half during the card-01 → card-02 gap (between 7.5 and 8.0s).
tl.set("#video-wrap", { className: "video-wrapper framed" }, 7.5);
tl.to(
"#video-wrap",
{ left: 960, top: 0, width: 960, height: 1080, duration: 0.6, ease: "power2.inOut" },
7.5,
);
// Card-02 enter — same pattern as card-01
tl.set('.card-host[data-card-id="card-02"]', { visibility: "visible" }, 8.0);
tl.fromTo(
'.card-host[data-card-id="card-02"]',
{ opacity: 0 },
{ opacity: 1, duration: 0.4, ease: "power2.out" },
8.0,
);
// ...card-02 internal anims...
// ── repeat for each card; if the NEXT card's layout differs,
// insert another tl.to('#video-wrap', ...) tween before its enter ──
window.__timelines = window.__timelines || {};
window.__timelines["talking-head-recut"] = tl;
})();
</script>
</div>
</body>
</html>
GSAP 语句速查表
将每个 data-anim 属性编译为一条 GSAP 语句。时间为
绝对秒数 = card.startSec + data-anim-at,并按 1/fps 量化。
选择器为 .card[data-card-id="X"] #elementId。
| data-anim | GSAP 语句模板 |
|---|---|
fade-in | tl.fromTo(SEL, { opacity: 0 }, { opacity: 1, duration: D, ease: 'power2.out' }, T); |
fade-out | tl.to(SEL, { opacity: 0, duration: D, ease: 'power2.in' }, T); |
slide-in (from=left, dist=80) | tl.fromTo(SEL, { opacity: 0, x: -80 }, { opacity: 1, x: 0, duration: D, ease: 'power2.out' }, T); |
kinetic-chars (pop) | tl.from(SEL + ' .char', { opacity: 0, y: 8, scale: 0.8, duration: D, ease: 'power2.out', stagger: S }, T); |
count-up | (function(){const o={v:FROM};tl.to(o,{v:TO,duration:D,ease:'power2.out',onUpdate:function(){const el=document.querySelector(SEL);if(el)el.textContent=__fmt(o.v,'FMT');}},T);})(); |
draw-path | (function(){const el=document.querySelector(SEL);if(el){const L=el.getTotalLength();tl.set(SEL,{strokeDasharray:L,strokeDashoffset:L},T);tl.to(SEL,{strokeDashoffset:0,duration:D,ease:'power2.inOut'},T);}})(); |
grow-x (target-w=W) | tl.fromTo(SEL, { width: 0 }, { width: W, duration: D, ease: 'power2.out' }, T); |
grow-y (target-h=H) | tl.fromTo(SEL, { height: 0 }, { height: H, duration: D, ease: 'power2.out' }, T); |
scale-pop | tl.fromTo(SEL, { opacity: 0, scale: 0.6 }, { opacity: 1, scale: 1, duration: D, ease: 'back.out(1.6)' }, T); |
mask-reveal (direction=left) | tl.fromTo(SEL, { clipPath: 'inset(0 100% 0 0)' }, { clipPath: 'inset(0 0 0 0)', duration: D, ease: 'power2.inOut' }, T); |
量化:T = Math.round(absSec * fps) / fps。在 30fps 下,最小步长为
1/30 ≈ 0.0333s;在 JS 字面量中四舍五入到 4 位小数(.toFixed(4))即可。
视频画面布局参考(按 layout 值)
视频容器的选择器是 #video-wrap。使用 tl.to('#video-wrap', { ...bounds }, T) 在卡片之间为其边界添加动画。应在元素上以内联方式设置初始边界,使其与 card-01 的布局一致。选择 0.5–0.7s 的过渡时长,并使用 ease: 'power2.inOut'。
装饰边框(clean / hairline / polaroid)作为 #video-wrap 的同级元素,并在布局过渡期间跟随它移动。有关各边框的放置 HTML、建议的 CSS,以及适配的布局,请参阅 references/frames/。快速规则:overlay 布局不显示装饰边框(全出血视频会与边框冲突);画中画布局已有自己的胶囊式样式(圆角 + 白色描边 + 阴影),因此只在 split / stack 上添加装饰边框。
#video-wrap 的 GSAP 目标查找表,按合成布局列出(横屏 1920×1080;竖屏布局见 references/layouts/*.html,4:5 布局根据上述比例缩放方式从竖屏布局推导):
| 合成布局 | 典型 card.zone | #video-wrap GSAP 目标 | 额外 css 类 |
|---|---|---|---|
split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | — |
stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 }(顶部 52%) | — |
pip(右下角) | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } | pip-pill(圆角 + 描边 + 阴影) |
pip(左上角) | fullscreen | { left: 40, top: 40, width: 400, height: 300 } | pip-pill |
overlay(视频全出血) | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 }(保持默认值不变) | — |
| 隐藏视频(纯图形画面) | fullscreen | { opacity: 0 }(或移出画布) | — |
进入或离开画中画画面时,切换 pip-pill 样式(圆角 + 白色描边 + 投影):
// Enter pip — add chrome
tl.set("#video-wrap", { className: "video-wrapper pip-pill" }, T);
tl.to(
"#video-wrap",
{ left: 1480, top: 760, width: 400, height: 300, duration: 0.6, ease: "power2.inOut" },
T,
);
// Leave pip — back to clean full-bleed
tl.set("#video-wrap", { className: "video-wrapper" }, T_NEXT);
tl.to(
"#video-wrap",
{ left: 0, top: 0, width: 1920, height: 1080, duration: 0.6, ease: "power2.inOut" },
T_NEXT,
);
卡片宿主边界与区域一致。使用步骤 6 顶部的表格,将卡片的 zone 转换为像素边界,然后将其写入卡片宿主的内联 style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..."。对于 video-overlay 区域(overlay 配方),卡片宿主会铺满整个画布;实际可见卡片的位置由 .card .root 内部的 CSS 决定。
HyperFrames 布局 / 动画 QA 规则
- 先为每张卡片构建静态主视觉帧:卡片完整显示且内容清晰可读的画面。
- 确认视频、卡片、字幕和图表不会发生意外重叠。
- 确认隐藏的视频区域会被画面边界裁切,不会显示在预期范围之外。
- 将一个暂停的主时间线注册为
window.__timelines["talking-head-recut"]。 - 在页面加载时同步构建时间线;不要使用
async、setTimeout、Promises 或媒体play()调用。 - 不要在渲染路径中使用
Math.random()或Date.now()。 - 仅当根元素设置了有限的
data-duration时才允许使用repeat: -1;对于必须更早结束的循环,应根据视频时长计算有限的重复次数。 - 动画优先使用 GSAP 变换和透明度(
x、y、scale、rotation、opacity),而非布局属性(top、left、width、height)。 - 为
#video-wrap等包装元素设置动画,而不要直接调整视频元素的尺寸。 - 避免通过多个时间线同时对同一元素的同一属性设置动画。
- 使用
data-track-index,而不是data-layer;使用data-duration,而不是data-end。 - 每个定时元素(
card-host、子合成等)都应在自身类名旁包含class="clip";例如:class="card-host clip"。可见性由data-start/data-duration控制:无论是否包含此类,运行时都会将每个[data-start]元素限制在其时间窗口内。.clip是 Studio 和 GSAP 剪辑归属规则用于识别剪辑的标记;省略该类会增加编辑和 lint 检查的难度(lint:timed_element_missing_clip_class,警告)。 - 对于 body / 全局
font-family,请列出具体字体名称('Inter'、'Caveat'等),不要使用var(--font-family)这样的 CSS 变量。HyperFrames 字体解析器在静态分析时不会展开 CSS 变量(lint:font_family_without_font_face)。卡片内部仍可使用var(--font-family),因为它们的@font-face声明会被加载。
10. 渲染为 MP4
cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
--skill=talking-head-recut \
-o output.mp4 \
--fps 30
hyperframes render <dir> 会读取 <dir>/index.html 并生成 MP4。
源节目的音频保留在 #bg-video 上,因此渲染出的 MP4 无需手动重新合并音视频,即可保留人物对镜讲话的音频。
在 macOS 上强烈建议使用标志 PRODUCER_BROWSER_GPU_MODE=hardware(或 --browser-gpu)——仅使用软件渲染的 Chrome 在大多数笔记本电脑上都会超时。
在完整渲染前,可在指定时间戳截取单帧以进行基本检查:
npx hyperframes snapshot public --at 5 # → public/.hyperframes/artifacts/snapshots/<id>/frame-00-at-5s.png (a single --at ignores --out)
11. 汇报结果
告知用户:
- 工作目录路径
storyboard.json(你设计的卡片大纲)public/cards/*.html(每张卡片一个 HTML 文件)public/index.html(组装后的合成内容)output.mp4(最终视频)- 使用的 ASR 提供方
- 卡片数量及选择方式(用 1 句话说明)
- 缺失的密钥或质量方面的注意事项
可选的实时预览(仅在用户提出要求时)。 剪辑在 public/index.html 中保持原样播放,叠加层显示在上方,因此预览效果与最终视频一致。运行期间不要打开它。 用户提出要求时,请在渲染完成后启动一个长期运行的服务并提供 URL:
(cd "$WORK_DIR/public" && npx hyperframes preview --background) # or `npx hyperframes play` for a shareable link
除非用户提出要求,否则不要删除工作目录。