首先,保持此 skill 为最新版本——运行前先向用户确认:npx hyperframes skills update talking-head-recut。若所有内容均已是最新版本,则快速结束而不做更改;否则会在你使用之前更新此 skill 及其依赖的核心领域 skills。
口播视频再包装
口播视频再包装使用一个完整播放的本地视频,并在其上叠加一系列 经过设计、按时间编排的图形卡片——标题、下三分之一字幕条、数据标注、 引语、侧边面板、画中画——使其与讲话内容同步。agent 设计卡片(时间安排 + 内容),并直接在对话中编写每张卡片的 HTML ,然后组装成一个合成用 HTML,再通过 hyperframes 将其渲染为 MP4。没有固定的原型清单,也没有规定的卡片结构—— 叠加内容取决于转录文本实际表达的内容。
入口为/hyperframes。此 skill 使用经过设计的图形卡片(标题、下三分之一字幕条、数据标注、引语、侧边面板、PiP)包装现有的口播视频片段,而不是添加普通字幕(以文字呈现说话内容)。视频片段保持原样播放。任何其他意图——普通字幕、独立图形、从零制作视频——或任何不确定情况 → 先阅读/hyperframes:所有路由决策均由意图层负责。
与 embedded-captions 配套的图形包装工具。字幕将_说话内容_
添加为便于阅读的字幕;本工具则在播放的视频之上添加_经过设计的图形_。
普通字幕 → embedded-captions。从零制作视频 → 创作
工作流(product-launch-video/faceless-explainer/ …)。
通过 /hyperframes 路由时,意图层只确认输入(使用哪个视频片段),并告知用户渲染策略问题将稍后询问——宽高比、布局、风格组和卡片数量仍在步骤 7 中确定,届时根据探测到的视频素材信息和转录文本提出建议;该层有关运行形式的问题不适用。如有 BRIEF.md,其中包含已确认的输入和用户备注——请先阅读。
工作目录中可检查的中间文件:
metadata.json——时长 / 宽度 / 高度 / 帧率audio.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
此 skill 完全依靠 hyperframes CLI 及系统中的 ffmpeg / ffprobe 运行。 转录通过 hyperframes transcribe 使用本地 Whisper——无需第三方 服务、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(随此 skill 提供,在步骤 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;帧率 = 对 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. 起草轻量分镜大纲(在聊天中)
不涉及 CLI。读取 transcript.json + metadata.json,直接设计 卡片。storyboard.json 是 agent 内部的规划产物 ——没有 CLI 命令会使用它;它的作用是帮助你在编写每张卡片的 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 | 字符串 | 用于卡片 HTML 和 GSAP 选择器的稳定标识符 | | intent | 字符串 | 自然语言描述;用于生成卡片 | | startSec / endSec | 数字 | 以秒为单位的时间(endSec > startSec) | | accentIndex | 0 \| 1 \| 2 \| 3 \| 4 | 此卡片使用 5 种主题强调色中的哪一种 | | zone | 枚举(见下文) | 卡片在画布上的位置 | | contentHints | 对象 | 自由格式集合;agent 将 kicker/title/detail/data/quote 放在此处 | | archetype(可选) | 字符串 | 可附加的自由格式标签,用于记住卡片的模式;缺省 = 自由形式,也是默认值 | | transition(可选) | 枚举:cut \| fade \| slide \| wipe | 声明式卡片间转场 |
五种 zone 值:
| 区域 | 解析后的边界 | 使用场景 | | ----------------- | ---------------------------------------------- | --------------------------------------- | | fullscreen | 覆盖整个画布 | 高光时刻、大数字、核心口号 | | whiteboard-area | 内缩 40px 边距(或竖屏高度的 45%) | 密集数据 / 带注释的内容 | | lower-third | 底部 30% 区域 | 在可见视频上添加注释 | | side-panel | 右侧 42%(横屏)或底部 40%(竖屏) | 一侧展示数据,另一侧播放视频 | | video-overlay | 整个画布,卡片应大部分透明 | 在满幅视频上叠加注释 |
在步骤 9 中组装合成内容时,按照上表将每张卡片的 zone 解析为 card-host 包装容器上的像素边界。 视频边界在合成层级只设置一次(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 短视频,单个点睛之句(低密度) → 7 × 1.5 = 10.5s/card → round(30/10.5)=3 → 按下限取 5 张卡片
- 60s 思考式独白(低密度) → 10 × 1.5 = 15s/card → 4 → 按下限取 5 张卡片
- 121s 数据丰富的口播(高密度) → 10 × 0.7 = 7s/card → 17 张卡片
- 5 min 访谈,混合密度 → 16 × 1.0 = 16s/card → 19 张卡片
- 10 min 深度解读,高密度 → 16 × 0.7 = 11s/card → 55 张卡片
- 30 min 讲座,中密度 → 28 × 1.0 = 28s/card → 64 张卡片
- 1 hr 播客,低密度 → 45 × 1.5 = 67.5s/card → 53 张卡片
卡片停留时间超过约 15s 时,应规划更丰富的卡片内容(数据块、 分步揭示、通过交错 动画展开多个子要点)——静态的一句话在超过 8s 后就容易显得枯燥。对于 许多卡片超过 30s 的长视频,可以考虑将时间轴拆分为 子合成(每章一个 .html,使用 data-composition-src 挂载),使每个文件中的 GSAP 时间轴保持易于管理 ——参见 HyperFrames 的 timeline_track_too_dense 静态检查警告。
content 可以是普通字符串("Title: annualized 5.69%\nNotes: ..."),也可以是任何能够表达数据的 JSON 结构。agent 为每张卡片决定其结构。
可选片尾。此 skill 不提供固定的品牌片尾。如果用户需要结束卡片,请自行设计一张中性的卡片(文字标识 + 一行宣传语,约 1.5-2s,淡入 -> 短暂停留 -> 淡出),将其追加到 cards[],并将 composition.durationSeconds 延长至它的 endSec。否则,以最后一张内容卡片结束。
7. 确定渲染策略
与用户确认视觉方向(首先完成此项)
开始设计卡片或确定边界之前,请用户 选择输出比例、布局、风格和卡片密度 预设。边框根据所选布局 × 风格 组合自动选择(见下方“自动选择边框”表)。发出 问题前,预先计算两项内容:
- 根据源视频宽高比计算
recommendedRatio
(metadata.json 中的宽度 / 高度):
sourceAspect = width / heightsourceAspect ≥ 1.5(≥ 约 3:2 的横向比例)→ 推荐16:9sourceAspect ≤ 0.7(≤ 约 9:13 的纵向比例)→ 推荐9:160.7 < sourceAspect < 1.5(接近正方形)→ 推荐4:5
在推荐选项的标签后标注“(推荐 · 与源视频 X:Y 匹配)” ,让用户了解推荐原因。
- 根据步骤 6 计算
autoCount(`max(5, round(videoSec / (basePace ×
densityMultiplier)))`),使“自动”选项的标签能够显示 具体数量。
环境兼容性——选择可用的最佳提问渠道。 并非所有运行环境都提供相同的结构化提问工具。请按以下 顺序选择:
- 原生澄清工具——使用下方包含 4 个问题的结构化调用。
- 其他原生澄清工具(例如
ask_question、 - 没有原生工具(Codex CLI、仅支持纯文本的运行环境)——**直接在
request_user_input、IDE 专用提示工具)——使用该工具,并采用 相同的 4 个问题文本和选项列表。保留推荐 标记及预计算值。
普通对话中提问。使用本节 末尾的纯文本模板。保持为一条消息、4 个编号问题** (全局上限是每轮 2–5 个问题;此处符合限制)。
适用于所有渠道的规则:
- 每轮最多询问 2–5 个问题。此处的 4 个问题符合要求。
- 即使缺失的信息不会阻止渲染,也要**询问一次,以确认
- 如果用户已预先同意采用默认值(“直接使用默认值”、
会实质影响最终输出的参数**(比例、 布局、风格、cardCount)。
“无需询问”、“全部自动选择”),要求你不要提问,或者 当前任务持续授权自主决策(“给我惊喜” / “替我决定”—— ../hyperframes/references/brief-contract.md § 1)——完全跳过 提问,并使用:recommendedRatio、layout="stack" (适用于不同比例的最稳妥默认值),根据转录文本语气在 最中性的组(editorial/data)中选择 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 个问题的上限内)。
用户回答后(适用于任何渠道):
- 根据比例回答确定输出画布——需要写入的
确切 storyboard.composition.width / height 值如下:
| 用户选择 | composition.width × 高度 | storyboard.layout 字段 | | ----------- | -------------------------- | ------------------------------------------------------------- | | 16:9 | 1920 × 1080 | "landscape" | | 9:16 | 1080 × 1920 | "portrait" | | 4:5 | 1080 × 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 | | ----------------------- | ----------------------------------------- | | 自动(推荐) | 之前已计算的 autoCount | | 更少 | max(5, round(autoCount × 0.6)) | | 更多 | round(autoCount × 1.5)(不设上限) | | 其他 = "<n>"(整数) | max(5, parseInt(n)) | | 其他 = 任意其他内容 | 回退到 autoCount |
- 根据此表自动选择视频边框(边框不向用户
提问——由布局 × 风格确定):
| 布局 | warm-paper 风格(academic / whiteboard / editorial / xhs) | clinical 风格(audit / swiss / terminal / minimal) | experimental 风格(geom / spotlight) | | --------- | ----------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------- | | split | polaroid | hairline | clean | | stack | polaroid | hairline | clean | | pip | clean(画中画胶囊已有装饰效果) | clean | clean | | overlay | clean(满幅视频禁止使用装饰边框) | clean | clean |
- 用一句话告诉用户你的选择——比例(+ 画布
- 在工作记忆中记录这五个值(比例 / 布局 / 风格 / 边框 / cardCount)
尺寸)、布局、具体风格、边框和最终 cardCount——然后 继续步骤 7 的其余部分(逐卡片布局、运动模式)。
(无需添加结构字段);在步骤 8 编写每张卡片的 HTML 以及读取匹配的 references/<dim>/<key>.html 以获取设计变量和结构时,会用到它们。
如果用户通过“其他”输入一个不在 10 种风格库中的自由文本风格名,请将其作为提示,自行设计新的卡片 视觉效果,同时仍以所选布局的边界为基础。
渲染策略输入
在步骤 7.0 确定比例 / 布局 / 风格 / cardCount / 边框后, 剩余需要逐卡片决定的事项有:
- 源视频在 GSAP 目标区域内的适配方式:视频元素具有
- 每张卡片的
card.zone:根据所选合成布局推导 - 每张卡片的
accentIndex:每张卡片从 5 种主题强调 - 运动语言:选择 2–3 种可重复使用的模式,类型来自
object-fit: cover,并裁剪到 #video-wrap 的补间动画边界内。 如果希望完全不裁剪(例如横屏画布上的竖屏素材 不应切掉顶部/底部),请将补间动画目标设为与源视频 宽高比一致的矩形,使周围画布能够 显示出来(或用卡片 / 背景填充)。
(split → side-panel、stack → lower-third、pip → fullscreen、overlay → video-overlay),或者为个别变体选择不同区域 (高光 / 引语使用 fullscreen,密集数据使用 whiteboard-area)。
色中选择一种。在卡片之间变换颜色以形成节奏;两张 卡片属于同一叙事节拍时,复用相同索引。
data-anim(见后面的表格),并坚持使用这些模式,使 合成内容保持连贯。
从以下 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 还在 <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——可独立使用的卡片片段,包含该references/layouts/<key>.html——准确的videoBounds+cardBounds,适用于references/frames/<key>.html——作为下列元素的同级元素添加的装饰性 HTML:
风格的 CSS 设计变量(颜色、字体、内边距、装饰)和一个占位 要点。复制 .card[data-card-id="ref-<key>"] 样式块,将 data-card-id 改为你的卡片标识符,再将占位内容替换成 真实要点,即可完成。
横屏和竖屏,并提供可直接复制粘贴的 JSON 片段,用于 storyboard.json 中每张卡片的 layout 字段。
#video-wrap,同时提供合成内容 CSS 中的位置说明。
为每张卡片选择 style × layout × frame——只要转场流畅,就可以在卡片之间 更换全部三个维度。常见节奏为: 以 editorial × overlay × clean 开场,为数据卡片切换到 audit × split × hairline ,最后以 whiteboard × pip × polaroid 收尾。
这 10 种风格是 skill 侧的设计变量,不是合成层级的主题—— 无需在 storyboard.composition 中声明;它们位于 每张卡片的 HTML 内。themeId 字段仍可选择 合成层级的调色板(见上表),控制页面主体背景 和视频边框装饰。
布局组合(卡片 + 视频)
每张卡片的两个协同决策决定它如何与 源视频共享画布:
card.zone(在storyboard.json中声明)——5 个结构定义值- 此卡片时间窗口内的
#video-wrap边界(在合成内容的
之一;在步骤 9 编写 card-host 包装容器的行内 style 时, 将其解析为像素边界(参见步骤 6 的表格)。
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 解析 表)。
个别变体可使用的其他区域值(仍使用 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控制层叠顺序)。 - 视觉细节放在卡片 HTML 片段中(步骤 8),而非
contentHints。contentHints是你用于设计卡片的结构化提示内容;渲染外观由 HTML 决定。 - 保持分镜结构稳定——虽然没有程序解析它,但在编写步骤 8/9 的内容时,你会重新读取它,结构一致能确保卡片 ID 和时间安排保持同步。
- Agent 侧的决策,例如“我选择了 overlay × geom × clean”,不得放入
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 的卡片(split 方案),card-host 本身已经 只占画布的一半,因此卡片可以使用不透明背景——它只覆盖自己所在的 一半。
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 静态检查拒绝):
- 单一根元素
<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,社交 / 移动端的主要 场景)下,放大所有视觉尺寸——手机屏幕 观看距离近,同样的像素数量看起来会比 横屏 TV 式画布更小。
| 设计变量 | 横屏基准 | 竖屏目标 | 缩放比例 | | ------------------------- | ------------------ | ------------------- | ------------- | | 标题(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 片段,其动画由此 skill 在步骤 9 中编译到共享叠加层时间轴上(参见该处的 GSAP 映射表 )。因此,此工作流不会像合成工作流那样搜索 HyperFrames 组件注册表 ——npx hyperframes catalog 返回的是带有各自时间轴的独立 合成内容,而卡片没有可挂载此类内容的位置。对于 下列类型无法表达的效果,可在卡片限定作用域的 <style> 中使用普通 CSS 实现。
| 类型 | 用途 | 关键参数 | | --------------- | ------------------- | ----------------------------------------------------------------------------------------------- | | 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 相同,但默认交错间隔更长 | | 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"
>
<!-- Layer 1: source video — initial position matches card-01's layout -->
<div class="video-wrapper" id="video-wrap">
<video
id="bg-video"
src="input-video.mp4"
muted
playsinline
data-start="0"
data-duration="121.2"
data-track-index="1"
></video>
</div>
<!-- Preserve the source program audio while the visual video stays muted. -->
<audio
id="source-audio"
src="input-video.mp4"
data-start="0"
data-duration="121.2"
data-track-index="10"
data-volume="1"
></audio>
<!-- Layer 2: each card-host sits at the bounds dictated by its layout. -->
<!-- IMPORTANT: every card-host MUST carry BOTH "card-host" and "clip" classes. -->
<!-- - "card-host" → our positioning + pointer-events styles -->
<!-- - "clip" → the marker Studio and the linter use to recognise a -->
<!-- clip. Visibility itself comes from data-start / -->
<!-- data-duration, which the runtime honours with or -->
<!-- without this class -->
<!-- (lint: timed_element_missing_clip_class, a warning). -->
<!-- Example: card-01 with zone="fullscreen" → card-host covers (0,0,1920,1080) -->
<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;"
>
<!-- paste the contents of public/cards/card-01.html here -->
</div>
<!-- Example: card-02 with zone="side-panel" (split composition layout) → card on left half -->
<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;"
>
<!-- card-02 HTML -->
</div>
<!-- ...one "card-host clip" per card with inline bounds matching resolveZoneBounds(card.zone)... -->
<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(弹出) | 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 的同级元素存在,并在布局转场时跟随它移动。 参见 [references/frames/](references/frames/),了解各边框的位置 HTML、建议的 CSS,以及适配的布局。简单规则: overlay 布局不显示装饰边框(满幅视频 与装饰效果冲突);PiP 布局已有自身的胶囊式处理 (圆角 + 白色描边 + 阴影),因此只在 split / stack 之上添加装饰边框。
各合成布局下 #video-wrap 的 GSAP 目标查找表 (横屏 1920×1080——竖屏和 4:5 请参见 references/layouts/*.html ,其中列出了全部三种比例):
| 合成布局 | 典型 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,
);
Card-host 边界与区域匹配。使用步骤 6 开头的表格,将卡片的 zone 解析为 像素边界,然后将其 写入 card-host 的行内 style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..."。对于 video-overlay 区域(overlay 方案), card-host 填满整个画布——.card .root 内的 CSS 决定实际可见卡片所在的位置。
HyperFrames 布局 / 动画 QA 规则
- 先构建每张卡片的静态主视觉帧:即卡片完全可见且清晰可读的时刻。
- 确认视频、卡片、字幕和图表没有意外重叠。
- 确认隐藏的视频区域已被边框裁剪,不会显示在预定边界之外。
- 将一个暂停状态的主时间轴注册为
window.__timelines["talking-head-recut"]。 - 在页面加载时同步构建时间轴;禁止使用
async、setTimeout、Promises 或媒体play()调用。 - 禁止在渲染路径中使用
Math.random()或Date.now()。 - 禁止使用
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]元素限制在其时间窗口内。Studio 和 GSAP 片段归属规则会读取.clip标记以识别片段,因此缺少它会增加编辑和静态检查的难度(静态检查:timed_element_missing_clip_class,警告级别)。 - 对于主体 / 全局
font-family,请列出具体字体名称('Inter', 'Caveat', …),不要使用var(--font-family)这样的 CSS 变量。HyperFrames 字体解析器在静态分析时不会展开 CSS 变量(静态检查: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。 规范的合成内容会将视觉 <video> 保持静音,并将相同的 源文件挂载为根级 #source-audio 音轨,因此渲染后的 MP4 能保留 口播音频,无需手动重新封装。这里使用独立音轨 而非 data-has-audio="true",使其音量和自动压低音量效果仍可在时间轴上独立 控制。 在 macOS 上强烈建议使用标志 PRODUCER_BROWSER_GPU_MODE=hardware(或 --browser-gpu) ——仅通过软件进行 Chrome 渲染在大多数笔记本上会 超时。
完整渲染前进行快速检查时,可在 指定时间戳截取单帧:
npx hyperframes snapshot public --at 5 # → public/snapshots/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
除非用户提出要求,否则禁止删除工作目录。