关于 graphic-overlays
为现有访谈视频添加定时图形卡片的技能,不改动原始画面。它依据转录内容设计卡片,并通过 hyperframes 将合成内容渲染为 MP4。
主要功能
- 01使用 Whisper 在本地转录音频
- 02依据转录内容安排图形卡片时序
- 03创建灵活的 HTML 卡片设计
- 04将合成内容渲染为 MP4
- 05检查中间文件与设计提纲
适用场景
- 01为访谈视频添加标题和下三分之一字幕条
- 02在视频片段上呈现引语和数据提示
- 03为访谈视频编排定时图形面板
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ heygen-com/ hyperframes --skill graphic-overlays安装后在 Claude Code 中输入/graphic-overlays确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
NOTICE.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详细介绍
图形叠加层
Graphic Overlays 会处理一段完整播放的本地视频,并在其上叠加一系列定时出现的精心设计的图形卡片——标题、下三分之一字幕、数据标注、引言、侧边面板、画中画——与讲述内容同步。Agent 负责设计卡片(时间和内容),并在对话中直接编写每张卡片的 HTML,然后组装成一个合成 HTML 文件,再通过 hyperframes 渲染为 MP4。这里没有固定的类型列表,也没有规定的卡片结构——叠加内容根据转录文本实际讲述的内容生成。
构建前请确认工作流。 此技能会为现有的对镜讲述片段添加精心设计的图形卡片(标题、下三分之一字幕、数据标注、引言、侧边面板、PiP)。如果用户想要的是纯字幕(将口述内容显示为文字)→
/embedded-captions;如果是单个简短且无旁白的元素(单个 Logo 音效画面 / 下三分之一字幕)→/motion-graphics。片段会原样播放——调整时间、颜色、画面构图、顺序或音频属于 NLE 编辑,不在此技能范围内。从 URL / 主题 / PR 开始制作 → 使用创作工作流。不确定该用叠加层还是字幕?先阅读/hyperframes。
这是
embedded-captions的图形包装版本。 字幕会将_口述内容_ 添加为易读的字幕;此技能会在播放的视频上叠加_设计好的图形_。 纯字幕 →embedded-captions。从头制作视频 → 创作工作流 (product-launch-video/faceless-explainer/ …)。
工作目录中的可检查中间文件:
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 运行。
转录使用 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(此技能内捆绑的文件,会在步骤 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)采用相同的约定。将 cwd 保持在工作区根目录;以下所有操作都会写入这一个子目录。
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. 起草轻量级分镜脚本(在聊天中)
无需使用 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 选择器中使用的稳定 ID |
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 值:
| 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 短视频,只有一句点睛之语(低密度) → 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。
content 可以是纯字符串("Title: annualized 5.69%\nNotes: ..."),也可以是能呈现数据的任意 JSON 结构。Agent 会为每张卡片决定结构。
可选片尾。 此技能不提供固定的品牌片尾。如果用户想要结束卡片,请自行设计中性的片尾卡片(字标 + 一行标语,约 1.5-2s,淡入 → 短暂停留 → 淡出),将其追加到 cards[],并将 composition.durationSeconds 延长至其 endSec。否则,以最后一张内容卡片结束。
7. 决定渲染策略
与用户确认视觉方向(首先执行此步骤)
在开始设计卡片或确定边界之前,请先让用户选择输出比例、布局、风格和卡片密度预设。根据所选的布局 × 风格组合自动选择画框(见下方“自动选择画框”表)。发送问题之前,预先计算以下两项:
-
根据源视频的宽高比(
metadata.json中的宽度 / 高度)计算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)))),以便“auto”选项的标签显示具体数量。
**环境兼容性 — 选择最合适的问题渠道。**并非每种运行环境都提供相同的结构化提问工具。按以下顺序选择:
AskUserQuestion(Claude Code、Anthropic Console)——使用下方的结构化四问题调用。- 其他原生澄清工具(例如
ask_question、request_user_input、IDE 专用提示)——使用该工具,并提供相同的四个问题文本和选项列表。保留推荐标记和预先计算的数值。 - 没有原生工具(Codex CLI、纯文本运行环境)——在普通对话中直接提问。使用本节末尾的纯文本模板。将内容控制在一条消息、4 个编号问题(每轮问题总数上限为 2–5 个;这里符合要求)。
适用于所有渠道的规则:
- 每轮最多提问 2–5 个问题。这里的 4 个问题符合要求。
- 即使缺少信息不会阻碍渲染,也要提问一次,确认会显著影响最终输出的参数(比例、布局、风格、cardCount)。
- 如果用户已经预先同意使用默认值(“直接使用默认值”“不用问”“全部自动选择”),或要求你不要提问,则完全跳过提问,并使用:
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." }
]
}
]
})
关于“Other”——AskUserQuestion 会自动在卡片数量问题中添加一个“Other”选项。用户可以直接输入数字(例如“8”“20”)作为 cardCount 目标值。将输入解析为整数:若解析成功 → 使用该值(最低为 5);若解析失败 → 回退到“auto”。
渠道 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 的其余部分(逐卡片布局、运动模式)。
-
在工作记忆中记录这五个值(比例 / 布局 / 风格 / 边框 / cardCount), 无需添加模式字段;在步骤 8 编写每张卡片的 HTML,以及读取匹配的
references/<dim>/<key>.html以获取令牌和结构时,会用到这些值。
如果用户通过“Other”选择了 10 种风格库中没有的自由文本风格名称, 请将其视为设计全新卡片视觉效果的提示,但仍须以所选布局的边界范围为准。
渲染策略输入
在第 7.0 步锁定比例 / 布局 / 样式 / cardCount / frame 后, 每张卡片还需确定以下内容:
- GSAP 目标内部的源视频适配方式:视频元素设置了
object-fit: cover,并裁剪到#video-wrap的补间边界内。 如果不希望裁剪(例如,横向画布上的纵向源视频不应被截掉顶部或底部), 请将补间目标设为与源视频宽高比相符的矩形,并让画布周围部分透出 (或使用卡片 / 背景填充)。 - 每张卡片的
card.zone:根据所选的构图布局确定 (分屏 → 侧边栏,堆叠 → 下三分之一,画中画 → 全屏,叠加 → 视频叠加),或者为个别变体选择其他区域 (主视觉 / 引言使用全屏,密集数据使用白板区域)。 - 每张卡片的
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 |
可用字体(位于 <SKILL_DIR>/assets/fonts/ 的 woff2 文件,在第 9 步复制到工作目录):Caveat(手写体)、
LXGW WenKai TC(中文手写体)、Inter(现代无衬线体)、Virgil
(几何手绘体)。可通过 @font-face 或直接使用 font-family 引用。
如需参考视觉模式,<SKILL_DIR>/references/styles/
提供了 10 张独立的参考卡片(学术 / 编辑风 / 极简 / 聚光 / 几何 / 白板 / 审计 / 终端 / 瑞士风 / 小红书),
可复制它们作为起点,但不必拘泥于这些风格。每张卡片都由你自行设计。
视觉设计库(<SKILL_DIR>/references/)
除了组合级别的 themeId,该技能还提供了一个更丰富的参考库,位于 <SKILL_DIR>/references/,涵盖三个可自由组合的正交视觉维度:
Style × Layout × VideoFrame
(10) (4) (3)
| 维度 | 键 | 决定的内容 |
|---|---|---|
| style | academic editorial minimal spotlight geom whiteboard audit terminal swiss xhs | 卡片的视觉语言——字体、颜色、装饰元素、卡片内布局 |
| layout | split stack pip overlay | 源视频和卡片如何共用画布 |
| frame | clean hairline polaroid | 视频元素周围的装饰边框 |
阅读 <SKILL_DIR>/references/DESIGN_INDEX.md,
了解完整矩阵和宽泛的选择指南(访谈/产品发布/数据分析/
社交短片/技术教程/情感故事……)。决定使用特定的
style / layout / frame 后,阅读对应文件:
references/styles/<key>.html— 自包含的卡片片段,包含该 风格的 CSS 令牌(颜色、字体、内边距、装饰元素)和占位要点。复制.card[data-card-id="ref-<key>"]样式块,将 data-card-id 重命名为卡片的 id,把占位内容替换为 实际要点即可。references/layouts/<key>.html— 横屏和竖屏对应的精确videoBounds+cardBounds,并附有可复制粘贴到storyboard.json每张卡片layout字段中的 JSON 片段。references/frames/<key>.html— 添加为#video-wrap同级元素的装饰性 HTML,以及组合 CSS 的放置说明。
可按卡片选择 style × layout × frame——只要过渡自然,就可以在不同卡片之间更改这三项。常见节奏:
以 editorial × overlay × clean 开场,数据卡切换到 audit × split × hairline,
最后以 whiteboard × pip × polaroid 收尾。
10 种 style 是技能侧的设计令牌,不是组合级别的主题——
无需在 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 步中 Channel A / Channel 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 类型
| 类型 | 用途 | 关键参数 |
|---|---|---|
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="graphic-overlays"
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"
muted
playsinline
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["graphic-overlays"] = 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 布局不使用装饰边框(全出血视频会与装饰元素冲突);画中画布局已有自己的胶囊样式(border-radius + 白色描边 + 阴影),因此只在 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,
);
卡片容器边界与区域匹配。使用第 6 步顶部的表格,将卡片的 zone 换算为像素边界,然后将这些值写入卡片容器的内联 style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..."。对于 video-overlay 区域(overlay 方案),卡片容器会填满整个画布 — 实际可见卡片的位置由 .card .root 内部的 CSS 决定。
HyperFrames 布局 / 动画 QA 规则
- 先构建每张卡片的静态主视觉帧:即卡片完整显示且清晰可读的时刻。
- 确认视频、卡片、字幕 / 说明文字和图表没有意外重叠。
- 确认隐藏的视频区域已被画面边框裁剪,不会显示在预期边界之外。
- 将一个暂停的主时间线注册为
window.__timelines["graphic-overlays"]。 - 在页面加载时同步构建时间线;不要使用
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"。HyperFrames 运行时使用.clip,将元素的可见性限制在data-start … data-start+data-duration时间范围内。若缺少此类名,元素会在整个视频期间可见(lint:timed_element_missing_clip_class)。 - 对于 body / 全局
font-family,请列出具体字体名称('Inter', 'Caveat', …),不要使用var(--font-family)之类的 CSS 变量。HyperFrames 字体解析器在静态分析期间不会展开 CSS 变量(lint:font_family_without_font_face)。由于卡片会加载自己的@font-face声明,因此内部仍可使用var(--font-family)。
10. 渲染为 MP4
cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
--skill=graphic-overlays \
-o output.mp4 \
--fps 30
hyperframes render <dir> 会读取 <dir>/index.html 并生成 MP4。
强烈建议在 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) # or `npx hyperframes play` for a shareable link
除非用户提出要求,否则不要删除工作目录。