mcpField
V
vtake-cut

vtake-cut

github.com/notedit创意与媒体12056 次安装21 周前更新
代码仓库

关于 vtake-cut

VTake 是一套技能与命令行工作流,可将本地视频转为卡片式 компози компози并渲染为 MP4。卡片依据转录内容规划并以 HTML 编写,处理中间文件可供检查。

主要功能

  1. 01提取音频和视频元数据
  2. 02转录并校正语音内容
  3. 03根据转录规划卡片
  4. 04编写卡片及合成 HTML
  5. 05将合成内容渲染为 MP4

适用场景

  1. 01将本地视频制作成卡片式剪辑
  2. 02按转录内容设计视觉分镜
  3. 03检查或校正带时间戳的转录
  4. 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.js
references/
DESIGN_INDEX.md
references/frames/
clean.html
hairline.html
polaroid.html
references/layouts/
overlay.html
pip.html
split.html
stack.html
references/styles/
academic.html
audit.html
editorial.html
geom.html
minimal.html
spotlight.html
swiss.html
terminal.html
whiteboard.html
xhs.html

详细介绍

VTake 本地工作流

VTake 会将本地输入视频转换为基于卡片的合成画面。Agent 负责设计卡片(时间安排 + 内容),并直接在对话中编写每张卡片的 HTML, 然后组装成一个合成画面 HTML,并通过 hyperframes 渲染为 MP4。这里没有固定的原型列表,也没有 规定的卡片结构——卡片会根据转录内容实际表达的信息自然形成。

工作目录中可检查的中间文件:

  • metadata.json — 时长 / 宽度 / 高度 / fps
  • audio.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)
accentIndex0 | 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 min8–12s常见的社交媒体节奏
3 – 10 min12–20s留出喘息空间;每张卡片承载更多内容
10 – 30 min20–35s长篇讲座/访谈的节奏
> 30 min30–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. 决定渲染策略
与用户确认视觉方向(先执行此项)

在开始设计卡片或确定边界之前,请先让用户选择输出宽高比、布局、样式和卡片密度预设。帧会根据所选的布局 × 样式组合自动选择(见下方“自动选择帧”表格)。发送问题之前,请预先计算以下两项:

  1. 根据源视频的宽高比(metadata.json 的宽度 / 高度)计算 recommendedRatio:

    • sourceAspect = width / height
    • sourceAspect ≥ 1.5(≥ 约 3:2 宽屏)→ 推荐 16:9
    • sourceAspect ≤ 0.7(≤ 约 9:13 竖屏)→ 推荐 9:16
    • 0.7 < sourceAspect < 1.5(接近正方形)→ 推荐 4:5

    在推荐选项的标签后添加“ (推荐 · 匹配源视频 X:Y)”, 让用户了解推荐理由。

  2. 根据步骤 6 计算 autoCount(max(5, round(videoSec / (basePace × densityMultiplier))),以便在“自动”选项的标签中显示具体数量。

环境兼容性——选择最佳可用的问题渠道。 并非所有运行环境都提供相同的结构化提问工具。按以下顺序处理:

  1. AskUserQuestion(Claude Code、Anthropic Console)——使用下方的结构化 4 问题调用。
  2. 其他原生澄清工具(例如 ask_question、 request_user_input、IDE 专用提示)——使用该工具提出相同的 4 个问题和选项。保留推荐标记和预先计算的值。
  3. 没有原生工具(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 / 都用推荐” → 跳过,不要重新询问。

用户回答后(适用于任何渠道):

  1. 根据宽高比回答确定输出画布——以下是要写入的确切 storyboard.composition.width / height 值:

    用户选项composition.width × heightstoryboard.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 }。
  2. 将风格组映射到具体风格,根据逐字稿的语气选择最合适的风格,但必须属于用户选择的风格组。如果无法确定该组中的两种具体风格哪个更合适,请通过第二个 AskUserQuestion 提供这 2–4 种具体风格选项。

  3. 根据密度答案确定最终的 cardCount:

    用户选择最终 cardCount
    自动 (推荐)已计算出的 autoCount
    少量max(5, round(autoCount × 0.6))
    更多round(autoCount × 1.5)(不设上限)
    Other = ""(整数)max(5, parseInt(n))
    Other = 其他任何值回退到 autoCount
  4. 根据此表自动选择视频帧(无需询问用户帧样式——它由布局 × 风格决定):

    布局暖纸质风格(academic / whiteboard / editorial / xhs)临床风格(audit / swiss / terminal / minimal)实验风格(geom / spotlight)
    splitpolaroidhairlineclean
    stackpolaroidhairlineclean
    pipclean(pip pill 已自带装饰)cleanclean
    overlayclean(满幅布局禁止装饰边框)cleanclean
  5. 用一句话告知用户你的选择——比例(+ 画布尺寸)、布局、具体风格、帧样式和最终的 cardCount——然后继续执行步骤 7 的其余部分(逐卡布局、动态模式)。

  6. 在工作记忆中记录这五个值(比例 / 布局 / 风格 / 帧样式 / 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)适用场景
splitside-panel{ left: 960, top: 0, width: 960, height: 1080 }{ left: 0, top: 960, width: 1080, height: 960 }(下半部分)演讲者与数据并排 / 权重各占 50%
stacklower-third{ left: 14, top: 14, width: 1892, height: 548 }(上方 52%){ left: 0, top: 0, width: 1080, height: 844 }(上方 44%)演讲者在上方 + 摘要卡片在下方
pipfullscreen{ left: 1480, top: 760, width: 400, height: 300 } + 添加 .framed 类{ left: 690, top: 28, width: 360, height: 203 } + 添加 .framed内容丰富的卡片 + 角落画中画
overlayvideo-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: 3
  • composition: { 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–96px88–132px×1.35
细节/正文24–30px30–40px×1.30
引导语/标签文字14–16px18–22px×1.30
时间码/元信息12–14px16–18px×1.30
数据区块主要数字48–60px64–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-pathSVG 路径显现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&trade;</span>
      <span class="sep">&diams;</span>
      <span>Cinematic Postproduction</span>
      <span class="sep">&diams;</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>&copy; 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 &middot; Reimagined
      </div>
    </div>
  </div>
</div>

动画时间线(1.00s 入场,所有 data-anim-at 值均相对于 card-cta-vtake.startSec):

时间元素动画时长
0.05V 形描边draw-path0.40
0.15“Powered by” 标签fade-in0.45
0.18两侧线条grow-x (→56px)0.40
0.20取景框角标 ×4fade-in0.50
0.25顶部电影片尾字幕信息条fade-in0.50
0.30底部电影片尾字幕信息条fade-in0.50
0.30T 形横杠描边draw-path0.25
0.40“vTake” 主名称mask-reveal0.45
0.50T 形竖杆描边draw-path0.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-pop0.30
0.90标语fade-in0.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-animGSAP 语句模板
fade-intl.fromTo(SEL, { opacity: 0 }, { opacity: 1, duration: D, ease: 'power2.out' }, T);
fade-outtl.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-poptl.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 类
splitside-panel{ left: 960, top: 0, width: 960, height: 1080 }—
stacklower-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 句话说明)
  • 缺失的密钥或质量方面的注意事项

除非用户提出要求,否则不要删除工作目录。

评论

支持 Markdown