关于 vtake-cut
VTake 是一套技能与命令行工作流,可将本地视频转为卡片式 компози компози并渲染为 MP4。卡片依据转录内容规划并以 HTML 编写,处理中间文件可供检查。
主要功能
- 01提取音频和视频元数据
- 02转录并校正语音内容
- 03根据转录规划卡片
- 04编写卡片及合成 HTML
- 05将合成内容渲染为 MP4
适用场景
- 01将本地视频制作成卡片式剪辑
- 02按转录内容设计视觉分镜
- 03检查或校正带时间戳的转录
- 04将组装后的 HTML 渲染为 MP4
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ notedit/ vtake-skills --skill vtake-cut安装后在 Claude Code 中输入/vtake-cut确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/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详细介绍
VTake 本地工作流
VTake 会将本地输入视频转换为基于卡片的合成画面。Agent
负责设计卡片(时间安排 + 内容),并直接在对话中编写每张卡片的 HTML,
然后组装成一个合成画面 HTML,并通过 hyperframes 渲染为 MP4。这里没有固定的原型列表,也没有
规定的卡片结构——卡片会根据转录内容实际表达的信息自然形成。
工作目录中可检查的中间文件:
metadata.json— 时长 / 宽度 / 高度 / fpsaudio.mp3— 提取的音频transcript.json— 带时间戳的片段 + 单词storyboard.json— 轻量级卡片大纲(Agent 的计划)public/cards/card-XX.html— 每张卡片对应一个 HTML 片段public/index.html— 最终组装的合成画面output.mp4— 渲染后的视频
CLI 解析
# vtake CLI — auto-downloaded from npm on first run
npx -y @notedit/vtake@latest --help
# hyperframes — for rendering the assembled HTML to MP4
npx hyperframes render --help
以下每个
vtake …命令都是npx -y @notedit/vtake@latest …的简写。
工作流
1. 检查环境
npx -y @notedit/vtake@latest doctor
# 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 步暂存到工作目录)
可选:
ELEVEN_API_KEY— 设置后,vtake transcribe会直接连接 ElevenLabs,绕过限速代理。未设置时,则会回退到https://vtake.app/api/transcribe,该服务对每个 IP 每分钟限制 3 次请求。可通过VTAKE_TRANSCRIBE_ENDPOINT覆盖代理 URL(例如用于本地 Wrangler 开发)。
强烈建议在 macOS 上使用以下配置运行 hyperframes render:
export PRODUCER_BROWSER_GPU_MODE=hardware
2. 创建工作目录
VIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR=".vtake-work/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"
3. 提取音频和元数据
npx -y @notedit/vtake@latest extract "$VIDEO_PATH" --out-dir "$WORK_DIR"
输出:metadata.json(时长、宽度、高度、fps)+ audio.mp3。
4. 转录
npx -y @notedit/vtake@latest transcribe "$WORK_DIR/audio.mp3" --out-dir "$WORK_DIR" --asr elevenlabs
输出:包含 { segments, words, raw } 的 transcript.json。
**限速(仅代理模式 — 未设置 ELEVEN_API_KEY):**服务器对每个 IP 每分钟允许 3 次请求。如果看到以
rate_limited: 或 service_busy: 开头的错误,不要自动重试——请停止并告知
用户需要等待多少秒(消息中包含重试提示),然后在用户再次提出请求时从此步骤继续。
5. 修正转录内容
阅读 transcript.json 并修正明显的 ASR 错误:
- 同音词、产品名称、技术术语、标点
- 保留所有
start/end时间戳 - 优先只编辑
segments[].text - 仅在明确的一对一替换时编辑单个
words[].word
6. 起草轻量级故事板(在聊天中)
**不涉及 CLI。**阅读 transcript.json + metadata.json 并直接设计卡片。storyboard.json 是
Agent 内部的规划产物——没有 vtake 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": "晚上 11 点的灵魂提问",
"detail": "客户六十秒语音:「人民币会升值,我的美金保单是不是亏惨了?」"
}
}
]
}
必需的卡片字段:
| 字段 | 类型 | 用途 |
|---|---|---|
id | 字符串 | 在卡片 HTML 和 GSAP 选择器中使用的稳定 ID |
intent | 字符串 | 自然语言描述;用于卡片合成 |
startSec / endSec | 数字 | 以秒为单位的时间(endSec > startSec) |
accentIndex | 0 | 1 | 2 | 3 | 4 | 此卡片使用的 5 种主题强调色之一 |
zone | 枚举(见下文) | 卡片在画布中的位置 |
contentHints | 对象 | 自由格式的内容集合;Agent 在此填写提示语、标题、详情、数据、引言 |
archetype(可选) | 字符串 | 可附加的自由格式标签,用于记住卡片的模式;缺省时为自由格式,这是默认值 |
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 短视频,只有一个重点包袱(低密度) → 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 可以是纯字符串(“标题:年化 5.69%\n说明:...”),也可以是任何能表达数据的 JSON
结构。Agent 会为每张卡片决定具体结构。
最后一张卡片——始终追加 card-cta-vtake。
在所有内容卡片之后,向 cards 数组添加一张固定的品牌结尾卡片:
{
"id": "card-cta-vtake",
"intent": "Powered By vTake brand outro",
"startSec": <last content card's endSec>,
"endSec": <last content card's endSec + 2.0>,
"accentIndex": 3,
"zone": "fullscreen",
"contentHints": {}
}
另请将 composition.durationSeconds 延长 2.0 秒:将其设为与 card-cta-vtake.endSec 相同的值。源视频轨道会在自然结束时停止;这段 2 秒的尾段是纯图形品牌展示,CTA 卡片后方不显示视频。CTA 卡片采用 1 秒的电影感入场序列(“Editorial Cinema”),随后保持约 0.7 秒,再淡出约 0.3 秒。
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
在推荐选项的标签后添加“ (推荐 · 匹配源视频 X:Y)”, 让用户了解推荐理由。
-
根据步骤 6 计算
autoCount(max(5, round(videoSec / (basePace × densityMultiplier))),以便在“自动”选项的标签中显示具体数量。
环境兼容性——选择最佳可用的问题渠道。 并非所有运行环境都提供相同的结构化提问工具。按以下顺序处理:
AskUserQuestion(Claude Code、Anthropic Console)——使用下方的结构化 4 问题调用。- 其他原生澄清工具(例如
ask_question、request_user_input、IDE 专用提示)——使用该工具提出相同的 4 个问题和选项。保留推荐标记和预先计算的值。 - 没有原生工具(Codex CLI、纯文本运行环境)——在普通对话中直接提问。使用本节末尾的纯文本模板。保持为一条消息,包含 4 个编号问题 (每轮问题总上限为 2–5 个;此处的 4 个符合要求)。
适用于所有渠道的规则:
- 每轮最多提问 2–5 个问题。此处的 4 个符合要求。
- 即使缺少信息不会阻碍渲染,也要询问一次,以确认会实质影响最终输出的参数(宽高比、布局、样式、cardCount)。
- 如果用户已预先同意使用默认值(“直接使用默认值”“无需询问”“全部自动选择”),或要求你不要提问,则完全跳过提问,并使用:
recommendedRatio、layout="stack"(最适合不同宽高比的默认选项)、根据转录文本语气从最中性的分组(editorial/数据)中选择style,以及autoCount。用一句话告诉用户你选择了什么,然后继续。
渠道 A——原生 AskUserQuestion:
// Precompute before the call:
// recommendedRatio = "16:9" | "9:16" | "4:5"
// autoCount = integer (from Step 6)
AskUserQuestion({
questions: [
{
question: "输出视频比例 (画幅):",
header: "画幅",
multiSelect: false,
// Reorder so the recommended option appears FIRST (per AskUserQuestion convention).
// Append " (推荐 · 匹配源视频 W×H)" to the recommended option's label.
options: [
{ label: "16:9 (1920×1080) 横屏", description: "TV / YouTube / 电脑播放。源视频已经是横屏时最自然,画幅最宽。" },
{ label: "9:16 (1080×1920) 竖屏", description: "抖音 / 小红书 / TikTok / Reels。源视频竖屏时最自然;移动端原生体验。" },
{ label: "4:5 (1080×1350) 方屏偏竖", description: "Instagram feed / 微信朋友圈。近方形源视频或想兼顾两种平台时最稳。" }
]
},
{
question: "选择整体布局:视频和卡片在画面里如何共存?",
header: "布局",
multiSelect: false,
options: [
{ label: "左右分屏 (split)", description: "video 和 card 各占画面一半。访谈 / 数据并列时最稳,画面分隔清晰。" },
{ label: "上下分屏 (stack)", description: "video 在上方 (~52%),card 在下方。说话人头像 + 总结句的经典组合,竖屏也好用。" },
{ label: "画中画 (pip)", description: "card 满屏,video 缩成圆角小窗在右上角。内容为主、speaker 为辅时用。" },
{ label: "全屏浮层 (overlay)", description: "video 全屏播放,card 作为玻璃浮层落在画面上。情绪 / 电影感强烈。" }
]
},
{
question: "选择卡片视觉风格 (style):",
header: "风格大类",
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)", description: "academic 学术笔记 · editorial 大字编辑 · whiteboard 手写白板 · xhs 小红书。适合访谈反思、产品发布、生活方式、情绪故事。" },
{ label: "冷峻临床 (clinical)", description: "audit 审计杂志 · swiss 瑞士网格 · terminal CLI · minimal 现代极简。适合财报分析、调查报告、技术教程、严肃陈述。" },
{ label: "实验前卫 (experimental)", description: "geom 撞色几何 · spotlight 暗色聚光。适合短视频高光、产品发布、强烈情绪、电影质感。" }
]
},
{
question: "卡片数量 (takeaway 节奏):要切多少张?",
header: "卡片数量",
multiSelect: false,
options: [
{ label: "自动 (推荐) · 约 N 张", description: "按视频时长和信息密度自动推断 (见 Step 6 规则)。本次推断约 N 张。带 N 进 label —— N 是你刚算出的 autoCount。" },
{ label: "少量 · 约 round(N × 0.6) 张", description: "切得稀疏一点,每张卡停留更久,适合 reflective / 慢节奏。" },
{ label: "更多 · 约 round(N × 1.5) 张", description: "切得更紧凑,节奏更快,适合 staccato / 数据密集 / 短视频高光。" }
]
}
]
})
关于“Other”——AskUserQuestion 会自动为“卡片数量”问题添加“Other”选项,
用户可以直接输入数字(例如“8”“20”)作为 cardCount 目标值。将输入解析为整数:
若解析成功 → 直接使用该值(至少 5 张);若解析失败 → 回退到“自动”。
渠道 B——纯文本备用方案(Codex CLI、没有原生提问工具的运行环境)。将以下内容作为一条普通消息发送,然后等待回复。采用 1/2/3/4 的项目符号格式,便于解析回复:
我需要先和你确认四个视觉决策再开始切卡片:
1) 输出比例 (画幅):
A. 16:9 横屏 (1920×1080) — TV / YouTube / 电脑播放
B. 9:16 竖屏 (1080×1920) — 抖音 / 小红书 / TikTok
C. 4:5 方屏偏竖 (1080×1350) — Instagram feed / 兼顾两端
▸ 我的推荐: <recommendedRatio> (匹配源视频 W×H = <sourceW>×<sourceH>)
2) 整体布局 (video & card 怎么共存):
A. split 左右分屏 (50/50)
B. stack 上下分屏 (video 顶, card 底)
C. pip 画中画 (card 满屏, video 圆角小窗)
D. overlay 全屏浮层 (video 全屏, card 玻璃浮层)
3) 卡片风格大类 (与 frame 自动矩阵同构,3 选 1):
A. 温暖纸感 warm-paper (academic / editorial / whiteboard / xhs)
B. 冷峻临床 clinical (audit / swiss / terminal / minimal)
C. 实验前卫 experimental (geom / spotlight)
4) 卡片数量 (takeaway 节奏):
A. 自动 (推荐) — 约 <autoCount> 张
B. 少量 — 约 round(<autoCount> × 0.6) 张
C. 更多 — 约 round(<autoCount> × 1.5) 张
D. 直接给我一个数字 (如 "8"、"20")
回复格式: "1A 2C 3B 4A" 或自然语言均可。
若你想全部用推荐默认值,回复 "默认" / "auto" / "都用推荐" 即可。
解析纯文本回复:
- 接受宽松格式:“1A 2C 3B 4A”、“A C B A”、“16:9 / pip / 数据 / 自动”、完整句子,或“默认”。
- 如果任何回答含糊 → 仅重新询问含糊的部分(仍须满足 2–5 个问题的限制)。
- 如果用户表示“默认 / auto / 都用推荐” → 跳过,不要重新询问。
用户回答后(适用于任何渠道):
-
根据宽高比回答确定输出画布——以下是要写入的确切
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 自动 (推荐) 已计算出的 autoCount少量 max(5, round(autoCount × 0.6))更多 round(autoCount × 1.5)(不设上限)Other = ""(整数) max(5, parseInt(n))Other = 其他任何值 回退到 autoCount -
根据此表自动选择视频帧(无需询问用户帧样式——它由布局 × 风格决定):
布局 暖纸质风格(academic / whiteboard / editorial / xhs) 临床风格(audit / swiss / terminal / minimal) 实验风格(geom / spotlight) splitpolaroidhairlinecleanstackpolaroidhairlinecleanpipclean(pip pill 已自带装饰)cleancleanoverlayclean(满幅布局禁止装饰边框)cleanclean -
用一句话告知用户你的选择——比例(+ 画布尺寸)、布局、具体风格、帧样式和最终的 cardCount——然后继续执行步骤 7 的其余部分(逐卡布局、动态模式)。
-
在工作记忆中记录这五个值(比例 / 布局 / 风格 / 帧样式 / cardCount);无需添加 schema 字段。编写步骤 8 中每张卡片的 HTML,以及读取匹配的
references/<dim>/<key>.html以获取令牌和结构时,都要参考这些值。
如果用户通过“Other”选择了一个不属于 10 种风格库的自由文本风格名称,请将其视为设计新卡片视觉效果的提示,但仍须以所选布局的边界为准。
渲染策略输入
在第 7.0 步锁定比例/布局/样式/卡片数量/画幅后, 每张卡片剩下需要决定的事项如下:
- 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 个独立的参考卡片(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,并附带可复制粘贴到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 种风格是技能侧的设计令牌,不是构图级主题——
无需在 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% |
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 内部的规划产物——没有任何 vtake 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 区域的卡片(split 配方),卡片容器本来就只占画布的一半,因此可以使用不透明的卡片背景——它只会覆盖自己所在的一半。
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">字</span>
<span class="char">幕</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>标签 - 不得使用外部 URL 作为
src=/href=(不得使用 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。
card-cta-vtake:固定品牌片尾卡
将此固定模板保存为 $WORK_DIR/public/cards/card-cta-vtake.html
——请勿修改。设计语言:“编辑电影感”——取景框角标、顶部/底部电影片尾字幕信息条、环绕 vT 标记的双旋转圆环、两侧带菱形的分隔线、斜体标语,以及琥珀色闪光粒子。入场序列在 1.00s 内完成;环境呼吸效果(圆环旋转、粒子脉动)会在剩余约 1.0s 内持续。
<div class="card" data-card-id="card-cta-vtake">
<style>
.card[data-card-id="card-cta-vtake"] .root {
width: 100%; height: 100%;
background:
radial-gradient(ellipse 60% 80% at 50% 50%, rgba(232,144,52,0.12) 0%, transparent 55%),
radial-gradient(ellipse 120% 40% at 50% 100%, rgba(201,169,97,0.10) 0%, transparent 60%),
#0D0B08;
position: relative; overflow: hidden;
container-type: inline-size;
font-family: 'Inter', ui-sans-serif, sans-serif;
color: #F5EFE1;
}
/* fractal noise overlay — kills the "plastic" look */
.card[data-card-id="card-cta-vtake"] .root::after {
content: '';
position: absolute; inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.06 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
mix-blend-mode: overlay;
opacity: 0.5;
pointer-events: none;
}
/* viewfinder corner marks (1.5px hairline) */
.card[data-card-id="card-cta-vtake"] .corner {
position: absolute;
width: 3.5cqi; height: 3.5cqi;
border-color: rgba(245,239,225,0.18);
pointer-events: none;
}
.card[data-card-id="card-cta-vtake"] .corner.tl { top: 3.5cqi; left: 3.5cqi; border-top: 1.5px solid; border-left: 1.5px solid; }
.card[data-card-id="card-cta-vtake"] .corner.tr { top: 3.5cqi; right: 3.5cqi; border-top: 1.5px solid; border-right: 1.5px solid; }
.card[data-card-id="card-cta-vtake"] .corner.bl { bottom: 3.5cqi; left: 3.5cqi; border-bottom: 1.5px solid; border-left: 1.5px solid; }
.card[data-card-id="card-cta-vtake"] .corner.br { bottom: 3.5cqi; right: 3.5cqi; border-bottom: 1.5px solid; border-right: 1.5px solid; }
/* top film-credit strip */
.card[data-card-id="card-cta-vtake"] .top-meta {
position: absolute; top: 4.8cqi; left: 0; right: 0;
display: flex; justify-content: center;
gap: clamp(14px, 2cqi, 36px);
font-size: clamp(9px, 0.85cqi, 16px);
font-weight: 500;
letter-spacing: 0.36em;
text-transform: uppercase;
color: rgba(245,239,225,0.18);
}
.card[data-card-id="card-cta-vtake"] .top-meta .sep { color: #E89034; opacity: 0.7; }
/* bottom film-credit strip */
.card[data-card-id="card-cta-vtake"] .bot-meta {
position: absolute; bottom: 5cqi; left: 0; right: 0;
display: flex; justify-content: space-between;
padding: 0 7cqi;
font-size: clamp(9px, 0.8cqi, 15px);
font-weight: 500;
letter-spacing: 0.32em;
text-transform: uppercase;
color: rgba(245,239,225,0.18);
}
.card[data-card-id="card-cta-vtake"] .bot-meta .right { color: #E89034; opacity: 0.85; }
/* center stage */
.card[data-card-id="card-cta-vtake"] .stage {
position: absolute; top: 50%; left: 50%;
transform: translate(-50%, -50%);
display: flex; flex-direction: column; align-items: center;
z-index: 2;
}
/* "Powered by" row with short flanking lines */
.card[data-card-id="card-cta-vtake"] .powered-row {
display: flex; align-items: center;
gap: clamp(10px, 1.4cqi, 24px);
margin-bottom: clamp(24px, 3cqi, 56px);
}
.card[data-card-id="card-cta-vtake"] .powered-row .line {
height: 1px;
background: rgba(245,239,225,0.18);
width: 0;
}
.card[data-card-id="card-cta-vtake"] .powered-row .label {
font-size: clamp(11px, 1.05cqi, 20px);
font-weight: 600;
letter-spacing: 0.46em;
text-transform: uppercase;
color: rgba(245,239,225,0.40);
}
/* vT mark wrapper + dual concentric rings */
.card[data-card-id="card-cta-vtake"] .mark-wrap {
position: relative;
width: clamp(160px, 15cqi, 280px);
height: clamp(160px, 15cqi, 280px);
display: flex; align-items: center; justify-content: center;
}
.card[data-card-id="card-cta-vtake"] .mark-ring {
position: absolute; inset: 0;
border-radius: 50%;
border: 1.5px dashed rgba(232,144,52,0.22);
animation: ctaRingSpin 32s linear infinite;
}
.card[data-card-id="card-cta-vtake"] .mark-ring.inner {
inset: 8%;
border: 1px solid rgba(245,239,225,0.10);
animation: ctaRingSpin 32s linear infinite reverse;
}
.card[data-card-id="card-cta-vtake"] .vtake-mark {
width: 60%; height: auto;
color: #F5EFE1;
position: relative; z-index: 1;
overflow: visible;
}
/* main brand name */
.card[data-card-id="card-cta-vtake"] .vtake-name {
font-size: clamp(48px, 6cqi, 112px);
font-weight: 900;
letter-spacing: -0.05em;
margin-top: clamp(20px, 2.5cqi, 48px);
line-height: 1;
overflow: hidden;
clip-path: inset(0 100% 0 0);
}
.card[data-card-id="card-cta-vtake"] .name-accent { color: #E89034; }
/* decorative divider: ── ◆ ── */
.card[data-card-id="card-cta-vtake"] .divider {
display: flex; align-items: center;
gap: clamp(6px, 0.7cqi, 14px);
margin-top: clamp(18px, 2.2cqi, 40px);
}
.card[data-card-id="card-cta-vtake"] .divider .seg {
width: 0; height: 1px;
background: rgba(232,144,52,0.5);
}
.card[data-card-id="card-cta-vtake"] .divider .diamond {
width: clamp(4px, 0.45cqi, 8px);
height: clamp(4px, 0.45cqi, 8px);
background: #E89034;
transform: rotate(45deg) scale(0);
}
/* tagline below divider */
.card[data-card-id="card-cta-vtake"] .tagline {
margin-top: clamp(14px, 1.8cqi, 32px);
font-size: clamp(10px, 0.9cqi, 17px);
font-weight: 500;
letter-spacing: 0.56em;
text-transform: uppercase;
color: rgba(245,239,225,0.40);
padding-left: 0.5em;
}
/* amber sparkle particles */
.card[data-card-id="card-cta-vtake"] .spark {
position: absolute;
width: clamp(2px, 0.2cqi, 3px);
height: clamp(2px, 0.2cqi, 3px);
background: #E89034;
border-radius: 50%;
box-shadow: 0 0 14px #E89034;
opacity: 0;
}
.card[data-card-id="card-cta-vtake"] .spark.s1 { top: 32%; left: 22%; animation: ctaSparkle 3.5s ease-in-out 1.0s infinite; }
.card[data-card-id="card-cta-vtake"] .spark.s2 { top: 64%; left: 76%; animation: ctaSparkle 4.2s ease-in-out 1.4s infinite; }
.card[data-card-id="card-cta-vtake"] .spark.s3 { top: 26%; right: 22%; animation: ctaSparkle 3.8s ease-in-out 1.6s infinite; }
.card[data-card-id="card-cta-vtake"] .spark.s4 { bottom: 32%; left: 38%; animation: ctaSparkle 4.6s ease-in-out 0.8s infinite; }
.card[data-card-id="card-cta-vtake"] .spark.s5 { top: 48%; right: 14%; animation: ctaSparkle 3.2s ease-in-out 2.0s infinite; }
@keyframes ctaRingSpin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes ctaSparkle {
0%, 100% { opacity: 0; transform: scale(0.5); }
50% { opacity: 0.85; transform: scale(1.3); }
}
</style>
<div class="root">
<span class="corner tl" id="cta-corner-tl"
data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
<span class="corner tr" id="cta-corner-tr"
data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
<span class="corner bl" id="cta-corner-bl"
data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
<span class="corner br" id="cta-corner-br"
data-anim="fade-in" data-anim-at="0.20" data-anim-duration="0.50"></span>
<div class="top-meta" id="cta-top-meta"
data-anim="fade-in" data-anim-at="0.25" data-anim-duration="0.50">
<span>VTAKE™</span>
<span class="sep">♦</span>
<span>Cinematic Postproduction</span>
<span class="sep">♦</span>
<span>Est. 2026</span>
</div>
<div class="bot-meta" id="cta-bot-meta"
data-anim="fade-in" data-anim-at="0.30" data-anim-duration="0.50">
<span>© 2026 vTake Studio</span>
<span class="right">vtake.app</span>
</div>
<span class="spark s1"></span>
<span class="spark s2"></span>
<span class="spark s3"></span>
<span class="spark s4"></span>
<span class="spark s5"></span>
<div class="stage">
<div class="powered-row" id="cta-powered-row">
<span class="line" id="cta-line-l"
data-anim="grow-x" data-anim-at="0.18"
data-anim-duration="0.40" data-anim-target-w="56"></span>
<span class="label" id="cta-powered-label"
data-anim="fade-in" data-anim-at="0.15"
data-anim-duration="0.45">Powered by</span>
<span class="line" id="cta-line-r"
data-anim="grow-x" data-anim-at="0.18"
data-anim-duration="0.40" data-anim-target-w="56"></span>
</div>
<div class="mark-wrap">
<span class="mark-ring" id="cta-ring-outer"
data-anim="fade-in" data-anim-at="0.55" data-anim-duration="0.50"></span>
<span class="mark-ring inner" id="cta-ring-inner"
data-anim="fade-in" data-anim-at="0.60" data-anim-duration="0.50"></span>
<svg class="vtake-mark" viewBox="0 0 200 130" fill="none"
aria-label="vTake" role="img">
<g transform="skewX(-14) translate(16 0)"
stroke="currentColor" stroke-width="14"
stroke-linecap="round" stroke-linejoin="round">
<path id="cta-path-v" d="M24 32 L56 100 L88 32"
data-anim="draw-path" data-anim-at="0.05" data-anim-duration="0.40"/>
<path id="cta-path-tbar" d="M100 32 L164 32"
data-anim="draw-path" data-anim-at="0.30" data-anim-duration="0.25"/>
<path id="cta-path-tstem" d="M132 32 L132 100"
data-anim="draw-path" data-anim-at="0.50" data-anim-duration="0.25"/>
</g>
<circle id="cta-dot" cx="48" cy="118" r="7.2" fill="#E89034"
data-anim="scale-pop" data-anim-at="0.65" data-anim-duration="0.25"/>
</svg>
</div>
<div id="cta-vtake-name" class="vtake-name"
data-anim="mask-reveal" data-anim-at="0.40"
data-anim-duration="0.45" data-anim-direction="bottom">
<span class="name-accent">v</span>Take
</div>
<div class="divider">
<span class="seg" id="cta-seg-l"
data-anim="grow-x" data-anim-at="0.75"
data-anim-duration="0.30" data-anim-target-w="56"></span>
<span class="diamond" id="cta-diamond"
data-anim="scale-pop" data-anim-at="0.85" data-anim-duration="0.30"></span>
<span class="seg" id="cta-seg-r"
data-anim="grow-x" data-anim-at="0.75"
data-anim-duration="0.30" data-anim-target-w="56"></span>
</div>
<div class="tagline" id="cta-tagline"
data-anim="fade-in" data-anim-at="0.90" data-anim-duration="0.45">
Visual Take · Reimagined
</div>
</div>
</div>
</div>
动画时间线(1.00s 入场,所有 data-anim-at 值均相对于
card-cta-vtake.startSec):
| 时间 | 元素 | 动画 | 时长 |
|---|---|---|---|
| 0.05 | V 形描边 | draw-path | 0.40 |
| 0.15 | “Powered by” 标签 | fade-in | 0.45 |
| 0.18 | 两侧线条 | grow-x (→56px) | 0.40 |
| 0.20 | 取景框角标 ×4 | fade-in | 0.50 |
| 0.25 | 顶部电影片尾字幕信息条 | fade-in | 0.50 |
| 0.30 | 底部电影片尾字幕信息条 | fade-in | 0.50 |
| 0.30 | T 形横杠描边 | draw-path | 0.25 |
| 0.40 | “vTake” 主名称 | mask-reveal | 0.45 |
| 0.50 | T 形竖杆描边 | draw-path | 0.25 |
| 0.55 | 外侧虚线圆环 | fade-in (+ 32s 旋转) | 0.50 |
| 0.60 | 内侧实线圆环 | fade-in (+ 32s 反向旋转) | 0.50 |
| 0.65 | 琥珀色圆点 | scale-pop (弹性) | 0.25 |
| 0.75 | 分隔线段 | grow-x (→56px) | 0.30 |
| 0.85 | 分隔菱形 | scale-pop | 0.30 |
| 0.90 | 标语 | fade-in | 0.45 |
| 1.00+ | 环境效果 | 圆环旋转 + 闪光循环 | — |
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 so the composition can reference it by relative path
ln -f "$VIDEO_PATH" "$WORK_DIR/public/input-video.mp4" 2>/dev/null \
|| cp "$VIDEO_PATH" "$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="vtake"
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.0000);
tl.fromTo('.card-host[data-card-id="card-01"]',
{ opacity: 0 }, { opacity: 1, duration: 0.4000, ease: 'power2.out' }, 1.0000);
// 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.5000, ease: 'power2.out', stagger: 0.0400 },
1.3000);
tl.fromTo('.card[data-card-id="card-01"] #card-01-line',
{ width: 0 }, { width: 420, duration: 0.5000, ease: 'power2.out' }, 1.6500);
// Exit (fade out over 0.35s, ending at endSec)
tl.to('.card-host[data-card-id="card-01"]',
{ opacity: 0, duration: 0.3500, ease: 'power2.in' }, 7.1500);
tl.set('.card-host[data-card-id="card-01"]', { visibility: 'hidden' }, 7.5000);
// ── 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["vtake"] = 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)return;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/,了解每种边框的放置 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 画面时,切换 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["vtake"]。 - 在页面加载时同步构建时间线;不要使用
async、setTimeout、Promise 或媒体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)。卡片内部仍可使用var(--font-family),因为其中的@font-face声明会加载。
card-cta-vtake:固定 GSAP 动画代码块
在 GSAP 时间线代码块末尾(紧邻最终的 window.__timelines
注册代码之前),追加此固定代码块。将 CTA_START 替换为
card-cta-vtake.startSec,将 CTA_END 替换为 card-cta-vtake.endSec:
// ── card-cta-vtake: Editorial Cinema brand outro (2.0s total) ──
// Sequence: 1.0s entrance · 0.7s hold (CSS ambient breathing) · 0.3s fade-out
const PREFIX = '.card[data-card-id="card-cta-vtake"]';
// Fade source video out at the start of the CTA
tl.to('#video-wrap', { opacity: 0, duration: 0.30, ease: 'power2.in' }, CTA_START);
// Card host enter
tl.set('.card-host[data-card-id="card-cta-vtake"]', { visibility: 'visible' }, CTA_START);
tl.fromTo('.card-host[data-card-id="card-cta-vtake"]',
{ opacity: 0 }, { opacity: 1, duration: 0.30, ease: 'power2.out' }, CTA_START);
// V-shape stroke draw
(function(){
const sel = PREFIX + ' #cta-path-v';
const el = document.querySelector(sel); if(!el) return;
const L = el.getTotalLength();
tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.05);
tl.to(sel, { strokeDashoffset: 0, duration: 0.40, ease: 'power2.inOut' }, CTA_START + 0.05);
})();
// "Powered by" label
tl.fromTo(PREFIX + ' #cta-powered-label',
{ opacity: 0 }, { opacity: 1, duration: 0.45, ease: 'power2.out' }, CTA_START + 0.15);
// Flanking lines grow-x
tl.fromTo(PREFIX + ' #cta-line-l',
{ width: 0 }, { width: 56, duration: 0.40, ease: 'power2.out' }, CTA_START + 0.18);
tl.fromTo(PREFIX + ' #cta-line-r',
{ width: 0 }, { width: 56, duration: 0.40, ease: 'power2.out' }, CTA_START + 0.18);
// Viewfinder corners ×4 fade-in
['tl','tr','bl','br'].forEach(pos => {
tl.fromTo(PREFIX + ' #cta-corner-' + pos,
{ opacity: 0, scale: 0.7 },
{ opacity: 1, scale: 1, duration: 0.50, ease: 'power2.out' },
CTA_START + 0.20);
});
// Top/bottom film-credit meta strips
tl.fromTo(PREFIX + ' #cta-top-meta',
{ opacity: 0, y: 6 }, { opacity: 1, y: 0, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.25);
tl.fromTo(PREFIX + ' #cta-bot-meta',
{ opacity: 0, y: -6 }, { opacity: 1, y: 0, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.30);
// T-bar stroke draw
(function(){
const sel = PREFIX + ' #cta-path-tbar';
const el = document.querySelector(sel); if(!el) return;
const L = el.getTotalLength();
tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.30);
tl.to(sel, { strokeDashoffset: 0, duration: 0.25, ease: 'power2.inOut' }, CTA_START + 0.30);
})();
// "vTake" main name mask-reveal (left → right)
tl.fromTo(PREFIX + ' #cta-vtake-name',
{ clipPath: 'inset(0 100% 0 0)' },
{ clipPath: 'inset(0 0% 0 0)', duration: 0.45, ease: 'power2.inOut' },
CTA_START + 0.40);
// T-stem stroke draw
(function(){
const sel = PREFIX + ' #cta-path-tstem';
const el = document.querySelector(sel); if(!el) return;
const L = el.getTotalLength();
tl.set(sel, { strokeDasharray: L, strokeDashoffset: L }, CTA_START + 0.50);
tl.to(sel, { strokeDashoffset: 0, duration: 0.25, ease: 'power2.inOut' }, CTA_START + 0.50);
})();
// Dual concentric rings fade-in (CSS @keyframes handles the slow spin)
tl.fromTo(PREFIX + ' #cta-ring-outer',
{ opacity: 0 }, { opacity: 1, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.55);
tl.fromTo(PREFIX + ' #cta-ring-inner',
{ opacity: 0 }, { opacity: 1, duration: 0.50, ease: 'power2.out' }, CTA_START + 0.60);
// Amber dot scale-pop (elastic)
tl.fromTo(PREFIX + ' #cta-dot',
{ opacity: 0, scale: 0.4, transformOrigin: '48px 118px' },
{ opacity: 1, scale: 1, duration: 0.25, ease: 'back.out(1.7)' },
CTA_START + 0.65);
// Decorative divider segments grow-x
tl.fromTo(PREFIX + ' #cta-seg-l',
{ width: 0 }, { width: 56, duration: 0.30, ease: 'power2.out' }, CTA_START + 0.75);
tl.fromTo(PREFIX + ' #cta-seg-r',
{ width: 0 }, { width: 56, duration: 0.30, ease: 'power2.out' }, CTA_START + 0.75);
// Divider diamond pop
tl.fromTo(PREFIX + ' #cta-diamond',
{ rotate: 45, scale: 0 },
{ rotate: 45, scale: 1, duration: 0.30, ease: 'back.out(1.7)' },
CTA_START + 0.85);
// Tagline fade-in
tl.fromTo(PREFIX + ' #cta-tagline',
{ opacity: 0, y: 4 }, { opacity: 1, y: 0, duration: 0.45, ease: 'power2.out' }, CTA_START + 0.90);
// Card exit — fade-out in the last 0.30s of the 2.0s window
tl.to('.card-host[data-card-id="card-cta-vtake"]',
{ opacity: 0, duration: 0.30, ease: 'power2.in' }, CTA_END - 0.30);
tl.set('.card-host[data-card-id="card-cta-vtake"]', { visibility: 'hidden' }, CTA_END);
10. 渲染为 MP4
cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
-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 --out snapshot-5s.png
11. 报告结果
告知用户:
- 工作目录路径
storyboard.json(你设计的卡片大纲)public/cards/*.html(每张卡片对应一个 HTML 文件)public/index.html(组装后的合成内容)output.mp4(最终视频)- 使用的 ASR 提供方
- 卡片数量及选择方式(用 1 句话说明)
- 缺失的密钥或质量方面的注意事项
除非用户提出要求,否则不要删除工作目录。