mcpField
T
talking-head-recut

talking-head-recut

github.com/heygen-com创意与媒体74.7k407.5k 次安装今天更新
代码仓库

关于 talking-head-recut

Talking Head Recut 是将设计好的图文卡片叠加到现有口播视频并导出 MP4 的技能,面向需要包装访谈、讲解或社交视频的创作者。它依据本地 Whisper 转录内容安排卡片,不更改原视频播放,并由 HyperFrames CLI 完成转录与渲染。

主要功能

  1. 01提取视频元数据与音频
  2. 02使用本地 Whisper 生成逐词转录
  3. 03按转录内容规划卡片与时间
  4. 04编写卡片 HTML 并组装合成页面
  5. 05渲染为 MP4 视频

适用场景

  1. 01为口播视频添加标题和下三分之一字幕条
  2. 02根据讲述内容呈现数据说明或引言卡片
  3. 03制作侧栏或画中画图文叠加
  4. 04将现有视频包装成社交媒体成片

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/heygen-com/hyperframes --skill talking-head-recut

安装后在 Claude Code 中输入/talking-head-recut确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

media-contract.test.mjs
NOTICE.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

详细介绍

插件安装: 在 HyperFrames 插件中使用此技能时,运行设置或检查更新命令前,请遵循插件执行规则。独立安装请使用下方的更新说明。

首先,保持此技能为最新版本——运行前请先向用户确认: npx hyperframes skills update talking-head-recut。如果所有内容均为最新版本,此命令会快速执行且不做任何更改;否则,它会在你依赖这些技能之前更新此技能及其所依赖的核心领域技能。

Talking Head Recut

Talking Head Recut 会在一段从头到尾完整播放的本地视频上,叠加一系列定时呈现的设计图形卡片——标题、下三分之一字幕条、数据标注、引言、侧边栏、画中画,并与口播内容同步。Agent 负责设计卡片(时机 + 内容),并在对话中直接编写每张卡片的 HTML,然后组装成一个合成 HTML,通过 hyperframes 渲染为 MP4。没有固定的原型列表,也没有规定的卡片结构——叠加内容取决于转录文本的实际内容。

入口是 /hyperframes。 此技能会为已有的真人出镜片段添加设计好的图形卡片(标题、下三分之一字幕条、数据标注、引言、侧边栏、画中画),而不是普通字幕(即口播内容的文字)。视频片段会原样播放。 其他任何需求——普通字幕、独立图形、从零开始制作视频——或任何不确定的情况 → 请先阅读 /hyperframes:意图层负责决定所有路由。

与 embedded-captions 同属图形包装类技能。 字幕会以可读字幕的形式显示_口播内容_;此技能则在播放的视频上叠加_设计好的图形_。 普通字幕 → embedded-captions。从零开始制作视频 → 创作工作流(product-launch-video / faceless-explainer / …)。

通过 /hyperframes 路由后,意图层只确认输入(使用哪个片段),并告知用户渲染策略相关问题将延后询问——画面比例、布局、风格组和卡片数量会留到第 7 步,此时根据探测到的素材和转录文本提供建议;意图层关于运行形式的问题不适用。如果存在 BRIEF.md,其中会记录已确认的输入和用户备注——请先阅读。

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

  • metadata.json — 时长 / 宽度 / 高度 / fps
  • audio.mp3 — 提取出的音频
  • transcript.json — 扁平的词数组 [{ text, start, end }, …](Whisper;没有 segments,也没有 words 包装层)
  • storyboard.json — 轻量级卡片大纲(Agent 的计划)
  • public/cards/card-XX.html — 每张卡片对应一个 HTML 片段
  • public/index.html — 最终组装的合成文件
  • output.mp4 — 渲染后的视频

CLI 解析

# hyperframes — transcription (local Whisper) + rendering the assembled HTML to MP4
npx hyperframes --help

此技能完全通过 hyperframes CLI 和系统级 ffmpeg / ffprobe 运行。 转录使用本地 Whisper,通过 hyperframes transcribe 执行——无需第三方服务、API 密钥或有速率限制的代理。

工作流

1. 检查环境
npx hyperframes doctor          # ffmpeg, headless browser, render deps
# confirm bundled assets:
ls "<SKILL_DIR>/assets/fonts" "<SKILL_DIR>/assets/vendor/gsap.min.js"

必需项:

  • ffmpeg / ffprobe(系统级)
  • <SKILL_DIR>/assets/fonts/*.woff2、<SKILL_DIR>/assets/vendor/gsap.min.js(随此技能打包,并在第 9 步暂存到工作目录)

转录无需密钥——hyperframes transcribe 会在本地运行 Whisper(第 4 步)。

强烈建议在 macOS 上为 hyperframes render 安装:

export PRODUCER_BROWSER_GPU_MODE=hardware
2. 创建工作目录

所有产物都保存在 videos/<project-name>/ 下——与其他视频工作流(product-launch-video / faceless-explainer / pr-to-video)使用相同的约定。请将当前工作目录保持在工作区根目录;以下所有操作都会写入这一个子目录。

VIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR="videos/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"
3. 提取音频和元数据
# metadata — duration / width / height / fps
ffprobe -v error -select_streams v:0 \
  -show_entries stream=width,height,r_frame_rate \
  -show_entries format=duration -of json "$VIDEO_PATH" > "$WORK_DIR/metadata.json"
# audio
ffmpeg -y -i "$VIDEO_PATH" -vn -acodec libmp3lame -q:a 2 "$WORK_DIR/audio.mp3"

输出:metadata.json(读取 width/height/duration;fps 为计算后的 r_frame_rate 分数,例如 30000/1001 → 29.97)+ audio.mp3。

4. 转录
npx hyperframes transcribe "$WORK_DIR/audio.mp3" -d "$WORK_DIR" --json --model small.en

本地运行 Whisper——无需 API 密钥、无需代理、没有速率限制。会在工作目录中写入按词划分的 transcript.json(包含每个词的 text 和 start / end 时间戳)。 读取文件,获取驱动第 6 步卡片计时所需的词语 / 句子时间;如果需要按片段划分的数据块, 请自行根据标点 / 停顿将词语分组成句子。

将时间限制在媒体时长内。 Whisper 返回的最后一个词的 end 可能会略微超出 实际片段时长——将每张卡片的 endSec 和 composition.durationSeconds 限制在 metadata.json 中记录的时长内,否则渲染结果会在视频结束后出现一段黑屏。

5. 校正转录文本

transcript.json 是一个扁平的词对象数组——[{ "text": "...", "start": s, "end": s }, …](没有 segments 数组,也没有 words 包装;每个词的键是 text)。读取文件并修正明显的 ASR 错误:

  • 同音词、产品名称、技术术语、标点
  • 直接编辑词语的 text;保留其 start / end 时间戳
  • 文件中没有预先分组的 segments 数组——需要用于卡片计时的片段级数据块时,请自行将词语分组成句子(在句末标点 / 停顿处分开)
6. 草拟轻量级分镜(在聊天中)

无需使用命令行。 读取 transcript.json + metadata.json 并直接设计 卡片。storyboard.json 是仅供 Agent 内部使用的规划材料 ——没有命令行命令会读取它;它的作用是在编写每张卡片的 HTML 之前,帮助你理清 时间安排和内容。保持结构与下方示例一致,以便用同一份大纲来指导 你在第 9 步中编写的合成内容:

{
  "schemaVersion": 3,
  "composition": {
    "fps": 30,
    "width": 1080,
    "height": 1920,
    "durationSeconds": 121.2,
    "layout": "portrait",
    "themeId": "noir",
    "seed": 42
  },
  "videoTrack": {
    "sourcePath": "input-video.mp4",
    "startSec": 0,
    "endSec": 121.2,
    "bounds": { "x": 0, "y": 0, "width": 1080, "height": 1920 }
  },
  "subtitles": { "enabled": false },
  "cards": [
    {
      "id": "card-01",
      "intent": "Hook with the speaker's anxious midnight question",
      "startSec": 0.5,
      "endSec": 13.0,
      "accentIndex": 0,
      "zone": "fullscreen",
      "contentHints": {
        "kicker": "AN HONEST QUESTION",
        "title": "The soul-searching question at 11 PM",
        "detail": "Client's 60-second voice message: 'If the RMB appreciates, does that mean my USD policy is a terrible loss?'"
      }
    }
  ]
}

必需的卡片字段:

字段类型用途
idstring在卡片 HTML 和 GSAP 选择器中使用的稳定 ID
intentstring自然语言描述;会传入卡片合成流程
startSec / endSecnumber以秒为单位的时间(endSec > startSec)
accentIndex0 | 1 | 2 | 3 | 4此卡片使用的 5 种主题强调色中的哪一种
zone枚举(见下文)卡片在画布上的位置
contentHintsobject自由格式的内容容器;Agent 在此填写 kicker/title/detail/data/quote
archetype(可选)string可附加的自由格式标签,用于记住卡片的模式;缺省时为自由格式,这是默认值
transition(可选)枚举:cut | fade | slide | wipe声明卡片之间的转场

五种 zone 值:

区域解析后的边界适用场景
fullscreen覆盖整个画布开场重点、大数字、信念箴言
whiteboard-area内缩 40px 边距(或纵向画布高度的 45%)密集数据/带注释的内容
lower-third底部 30% 的区域在可见视频上添加注释
side-panel横向画布右侧 42%,或纵向画布底部 40%一侧放数据,另一侧放视频
video-overlay整个画布,预期使用大部分透明的卡片在铺满画面的全幅视频上叠加注释

在第 9 步组装构图时,根据上表将每张卡片的 zone 转换为卡片承载容器上的像素边界。 视频边界在构图级别设置一次(videoTrack.bounds); 如果要让视频看起来像是在“不同卡片间移动”,请在构图的 <script> 中针对 #video-wrap 编写 GSAP 补间动画(见第 9 步)。

不规定卡片角色,也不规定叙事弧线。 卡片应根据视频实际表达的内容生成——可以全是引言,也可以全是数据, 可以用一个数字开场,也可以用一个故事开场。让转录文本决定节奏。

需要多少个要点?——根据时长和信息密度自动推断。 没有固定上限。先根据视频时长选择基础节奏, 再根据信息密度进行调整。唯一固定的下限:至少 5 张卡片,确保短视频也有节奏。

第 1 步——根据时长确定基础节奏(中等信息密度下每张卡片的自然时长,单位:秒):

视频时长基础节奏(每张卡片秒数)原因
< 60s(短视频)6–8s观众期待短视频采用快速剪辑
60s – 3 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 短视频,只有一句 punchline(低密度) → 7 × 1.5 = 每张卡片 10.5s → round(30/10.5)=3 → 向下取整至至少 5 张卡片
  • 60s 反思式独白(低密度) → 10 × 1.5 = 每张卡片 15s → 4 → 向下取整至至少 5 张卡片
  • 121s 数据丰富的正面讲述(高密度) → 10 × 0.7 = 每张卡片 7s → 17 张卡片
  • 5 min 访谈,中等信息密度 → 16 × 1.0 = 每张卡片 16s → 19 张卡片
  • 10 min 深度解读,高密度 → 16 × 0.7 = 每张卡片 11s → 55 张卡片
  • 30 min 讲座,中等信息密度 → 28 × 1.0 = 每张卡片 28s → 64 张卡片
  • 1 hr 播客,低密度 → 45 × 1.5 = 每张卡片 67.5s → 53 张卡片 当单张卡片持续超过约 15s 时,应设计更丰富的卡片(数据块、多步揭示、通过错落动画逐步展开的多个子要点)——静态的一行内容超过 8s 就会显得无聊。对于卡片中有许多内容超过 30s 的长视频,可以考虑将时间线拆分为多个子组合(每章一个 .html,通过 data-composition-src 挂载),这样每个文件中的 GSAP 时间线都更易于管理——请参阅 HyperFrames lint 警告 timeline_track_too_dense。

contentHints 是一个自由形式的对象,其键和嵌套结构可以采用任何适合承载数据的形式。Agent 会根据每张卡片决定其结构。

可选片尾。 此技能不提供固定品牌片尾。如果用户需要结束卡片,请自行设计中性的卡片(字标 + 一行标语,约 1.5-2s,淡入 -> 短暂停留 -> 淡出),将其追加到 cards[],并把 composition.durationSeconds 延长至其 endSec。否则,请以最后一张内容卡片结尾。

7. 决定渲染策略
与用户确认视觉方向(首先执行)

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

  1. 根据源视频的宽高比(metadata.json 中的 width / height)计算 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

    在推荐选项的标签中标注“ (recommended · matches source video X:Y)”, 让用户了解推荐理由。

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

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

  1. 原生澄清工具——使用下面的结构化 4 问题调用。
  2. 其他原生澄清工具(例如 ask_question、 request_user_input、IDE 特定的提示)——使用该工具提出相同的 4 个问题,并提供相同的选项列表。保留推荐标记和预先计算的值。
  3. 没有原生工具(Codex CLI、纯文本运行时)——直接在普通对话中提问。使用本节末尾的纯文本模板。保持为一条消息、4 个编号问题 (每轮问题数上限为 2–5 个;这里的 4 个符合要求)。

适用于所有渠道的规则:

  • 每轮最多提出 2–5 个问题。这里的 4 个符合要求。
  • 即使缺少的信息不会妨碍渲染,也要询问一次,以确认会实质影响最终输出的参数(比例、布局、风格、cardCount)。
  • 如果用户已预先批准默认值(“直接使用默认值”“不用问”“全部自动选择”),要求你不要提问,或本次运行带有持续生效的自主决策信号(“给我惊喜” / “替我决定”——../hyperframes/references/brief-contract.md § 1),则完全跳过提问,并使用:recommendedRatio、layout="stack"(跨比例最稳妥的默认值)、根据转录语气从最中性的类别(编辑 / 数据)中选择的 style,以及 autoCount。用一句话告知用户你的选择,然后继续。

渠道 A——原生 AskUserQuestion:

// Precompute before the call:
//   recommendedRatio = "16:9" | "9:16" | "4:5"
//   autoCount        = integer (from Step 6)

AskUserQuestion({
  questions: [
    {
      question: "Output video aspect ratio (canvas):",
      header: "Aspect ratio",
      multiSelect: false,
      // Reorder so the recommended option appears FIRST (per AskUserQuestion convention).
      // Append " (recommended · matches source video W×H)" to the recommended option's label.
      options: [
        { label: "16:9 (1920×1080) landscape", description: "TV / YouTube / desktop playback. Most natural when the source video is already landscape; widest canvas." },
        { label: "9:16 (1080×1920) portrait", description: "TikTok / Reels / short-form mobile. Most natural for portrait source; native mobile experience." },
        { label: "4:5 (1080×1350) near-portrait", description: "Instagram feed / WeChat Moments. Best when source is near-square or you want to cover both platforms." }
      ]
    },
    {
      question: "Choose the overall layout: how should the video and cards coexist on the canvas?",
      header: "Layout",
      multiSelect: false,
      options: [
        { label: "side-by-side (split)",  description: "Video and card each take half the canvas. Most stable for interview / data side-by-side; clear visual separation." },
        { label: "top-bottom (stack)",    description: "Video on top (~52%), card below. Classic combo of speaker face + summary card; works well in portrait too." },
        { label: "picture-in-picture (pip)", description: "Card fills the canvas, video shrinks to a rounded corner window. Use when content is primary and speaker is secondary." },
        { label: "full-screen overlay (overlay)", description: "Video plays full-bleed, card floats as a glass layer on top. Strong cinematic / emotional feel." }
      ]
    },
    {
      question: "Choose the card visual style (style):",
      header: "Style group",
      multiSelect: false,
      // NOTE: these 3 groups intentionally match the frame auto-pick matrix
      // rows below, so picking a group resolves both `style` group AND the
      // frame matrix column in one step. Memberships are mutually exclusive.
      options: [
        { label: "warm paper (warm-paper)", description: "academic notebook · editorial big-type · whiteboard hand-drawn · xhs social. Best for interview reflections, product launches, lifestyle, emotional stories." },
        { label: "clinical / cold (clinical)",   description: "audit magazine · swiss grid · terminal CLI · minimal modern. Best for financial analysis, investigative reports, technical tutorials, serious presentations." },
        { label: "experimental / avant-garde (experimental)", description: "geom color-clash geometry · spotlight dark-background. Best for short-form highlights, product launches, strong emotion, cinematic feel." }
      ]
    },
    {
      question: "Card count (takeaway pacing): how many cards to cut?",
      header: "Card count",
      multiSelect: false,
      options: [
        { label: "Auto (recommended) · approx N cards", description: "Inferred automatically from video duration and information density (see Step 6 rules). This run estimates approx N cards. Substitute the real N (your autoCount) into the label." },
        { label: "Fewer · approx round(N × 0.6) cards", description: "Sparser cuts, each card holds longer — suits reflective / slow-paced content." },
        { label: "More · approx round(N × 1.5) cards", description: "Tighter cuts, faster rhythm — suits staccato / data-dense / short-form highlight content." }
      ]
    }
  ]
})

关于“其他”——AskUserQuestion 会自动为卡片数量问题添加一个“其他”选项。用户可以直接输入数字(例如“8”“20”)作为 cardCount 目标。将输入解析为整数:如果解析成功 → 使用该值(最低为 5);如果解析失败 → 回退为“自动”。 通道 B — 纯文本备用方案(Codex CLI、不支持原生提问工具的运行时)。将以下内容作为一条普通消息发送,然后等待回复。使用项目符号列出 1/2/3/4,可让回复更易解析:

I need to confirm four visual decisions with you before I start cutting cards:

1) Output aspect ratio (canvas):
   A. 16:9 landscape (1920×1080) — TV / YouTube / desktop playback
   B. 9:16 portrait (1080×1920) — TikTok / Reels / short-form mobile
   C. 4:5 near-portrait (1080×1350) — Instagram feed / works for both platforms
   ▸ My recommendation:  <recommendedRatio>  (matches source video W×H = <sourceW>×<sourceH>)

2) Overall layout (how video & card coexist):
   A. split   side-by-side (50/50)
   B. stack   top-bottom (video top, card bottom)
   C. pip     picture-in-picture (card full canvas, video rounded corner window)
   D. overlay full-screen glass overlay (video full-bleed, card glass layer)

3) Card style group (maps to frame auto-pick matrix, pick 1 of 3):
   A. warm paper (warm-paper)      (academic / editorial / whiteboard / xhs)
   B. clinical / cold (clinical)   (audit / swiss / terminal / minimal)
   C. experimental (experimental)  (geom / spotlight)

4) Card count (takeaway pacing):
   A. Auto (recommended) — approx <autoCount> cards
   B. Fewer — approx round(<autoCount> × 0.6) cards
   C. More — approx round(<autoCount> × 1.5) cards
   D. Give me a specific number (e.g. "8", "20")

Reply format: "1A 2C 3B 4A" or natural language is fine.
If you want all recommended defaults, reply "default" / "auto" / "use all recommendations".

解析纯文本回复:

  • 接受宽松格式:"1A 2C 3B 4A"、"A C B A"、"16:9 / pip / data / auto"、完整句子或 default。
  • 如果任何答案有歧义 → 只重新询问有歧义的项目(仍需遵守 2–5 项的上限)。
  • 如果用户说 "default / auto / use all recommendations" → 跳过,不再重新询问。

用户通过任意通道回答后:

  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
    Auto(推荐)已计算出的 autoCount
    Fewermax(5, round(autoCount × 0.6))
    Moreround(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 胶囊已有装饰边框)cleanclean
    overlayclean(全出血布局不允许装饰边框)cleanclean
  5. 用一句话告诉用户你的选择:比例(+画布尺寸)、布局、具体风格、画框和最终的 cardCount——然后继续完成步骤 7 的其余部分(每张卡片的布局、动态模式)。

  6. 在工作记忆中记录这五个值(ratio / layout / style / frame / cardCount),无需添加 schema 字段;编写步骤 8 中每张卡片的 HTML,以及读取对应的 references/<dim>/<key>.html 以了解令牌和结构时,都要参考这些值。

如果用户通过“Other”选择了一个不在 10 种风格库中的自定义风格名称,请将其视为设计全新卡片视觉效果的提示,但仍须遵循所选布局的边界。

渲染策略输入

步骤 7.0 中锁定 ratio / layout / style / cardCount / frame 后,其余每张卡片的决策如下:

  • GSAP 目标中的源视频适配方式:视频元素设置了 object-fit: cover,并会裁剪到 #video-wrap 的补间边界。如果不想裁剪(例如,横向画布上的纵向源视频不应被裁掉上下部分),请将补间目标设为与源视频宽高比相符的矩形,并让周围画布透出(或用卡片/背景填充)。
  • 每张卡片的 card.zone:根据所选的构图布局推导(split → side-panel、stack → lower-third、pip → fullscreen、overlay → video-overlay),也可以为个别变体选择不同的区域(hero/quote 用 fullscreen,密集数据用 whiteboard-area)。
  • 每张卡片的 accentIndex:每张卡片从 5 个主题强调色中选一个。通过变化来营造节奏;属于同一叙事节拍的两张卡片则复用相同索引。
  • 动态效果词汇:从 data-anim 类型中选择 2–3 种可重复使用的模式(见后面的表格),并保持一致,让构图更协调。

从以下 themeId 调色板中选择(在构图的 <style> 块中将它们用作 --accent-N / --bg / --text CSS 变量):

themeId强调色调色板(5 种颜色)白板背景文本
classic#1971c2 #e03131 #2f9e44 #e8590c #9c36b5#FFF9E3(纸张)#1e1e1e
noir#4cc9f0 #f72585 #4ade80 #fb923c #a78bfa#1a1a1a#f1f1f1
mint#0077b6 #d62828 #2d6a4f #e76f51 #7209b7#e8faf0#1b4332
craft#bf5700 #d62728 #6c757d #e9b54a #3d5a80#f6efe1#2d2d2d
slate#0ea5e9 #ef4444 #22c55e #f97316 #a855f7#1e293b#f1f5f9
mono#000 #555 #888 #aaa #ccc#fff#000

可用字体(woff2 文件位于 <SKILL_DIR>/assets/fonts/,并在步骤 9 中暂存到工作目录):Caveat(手写体)、 LXGW WenKai TC(中文手写体)、Inter(现代无衬线体)、Virgil (几何手绘体)。可通过 @font-face 或直接使用 font-family 引用。

如需视觉模式灵感,<SKILL_DIR>/references/styles/ 中提供了 10 张独立的参考卡片(academic / editorial / minimal / spotlight / geom / whiteboard / audit / terminal / swiss / xhs),可将它们用作起点——但不必受限于这些风格。每张卡片都由你自行设计。

视觉设计库(<SKILL_DIR>/references/)

除了组合级别的 themeId,该技能还提供了更丰富的参考库,位于 <SKILL_DIR>/references/,涵盖三个可自由组合的正交视觉维度:

Style  ×  Layout  ×  VideoFrame
 (10)      (4)         (3)
维度键决定的内容
样式academic editorial minimal spotlight geom whiteboard audit terminal swiss xhs卡片的视觉语言——字体、颜色、装饰、卡片内部布局
布局split stack pip overlay视频源与卡片如何共享画布
边框clean hairline polaroid视频元素周围的装饰性边框

阅读 <SKILL_DIR>/references/DESIGN_INDEX.md ,查看完整矩阵和宽泛的选择指南(访谈 / 产品发布 / 数据分析 / 社交短片 / 技术教程 / 情感故事……)。决定采用特定的 样式 / 布局 / 边框后,阅读对应文件:

  • references/styles/<key>.html — 自包含的卡片片段,包含该 样式的 CSS 令牌(颜色、字体、内边距、装饰)以及占位摘要。复制 .card[data-card-id="ref-<key>"] 样式块,将 data-card-id 改为卡片的 id,将占位内容替换为实际摘要,即可完成。
  • references/layouts/<key>.html — 横屏和竖屏下准确的 videoBounds + cardBounds。分镜仅记录 card.zone。
  • references/frames/<key>.html — 添加为 #video-wrap 同级元素的装饰性 HTML,以及组合 CSS 的放置说明。

在用户选择的样式组内,可以逐张卡片调整样式,并为每张卡片 选择 布局 × 边框,前提是转场衔接自然。

这 10 种样式是技能侧的设计令牌,不是组合级别的主题—— 无需在 storyboard.composition 中声明;它们位于每张卡片的 HTML 内。themeId 字段仍可选择 组合级别的调色板(见上表),用于控制页面主体背景和视频边框。

布局组合(卡片 + 视频)

每张卡片有两项相互配合的决策,决定它如何与源视频共享画布:

  • card.zone(在 storyboard.json 中声明)——5 个架构值之一;在第 9 步编写卡片容器包装器的内联 style 时,将其解析为像素边界(参见第 6 步中的表格)。
  • 此卡片时间窗口内 #video-wrap 的边界(在组合的 GSAP 时间轴中以命令式方式声明)——Agent 会在每次布局转换时,将 #video-wrap 缓动至目标矩形区域。

架构中不会存储每张卡片的视频边界。videoTrack.bounds 在组合级别只设置一次(默认为全画布)。视频在卡片之间“移动”完全由 index.html 中编写的 GSAP 动画实现。没有 card.layout 字段;架构中只有 card.zone。

4 种组合布局(来自 references/layouts/)——每种都是将 zone 与 #video-wrap 缓动目标配对的方案:

组合布局推荐的 card.zone#video-wrap 的 GSAP 目标(横屏 1920×1080)#video-wrap 的 GSAP 目标(竖屏 1080×1920)适用场景
splitside-panel{ left: 960, top: 0, width: 960, height: 1080 }{ left: 0, top: 960, width: 1080, height: 960 }(下半部分)演讲者与数据并排 / 50: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 分辨率表)。

一次性变体的其他 zone 值(仍使用 card.zone;不要伪造“layout”字段):

zone解析后的边界常见用途
fullscreen覆盖整个画布主视觉卡片,视频缓动至隐藏 / 画中画
whiteboard-area内缩 40px 边距(横屏)或底部 45% 区域(竖屏)密集数据卡片,保留空白边距
lower-third底部 30% 区域访谈头像注释
side-panel右侧 42%(横屏)或底部 40%(竖屏)侧边栏 / “split”方案
video-overlay全画布;卡片根元素应为透明全屏视频上的玻璃叠层

可以为每张卡片混用不同方案——根据当前时刻选择合适的 card.zone,然后编写卡片之间 #video-wrap 的 GSAP 缓动动画。

故事板渲染规范

storyboard.json 是 Agent 内部的规划产物——没有 CLI 命令会解析它。它用于在编写每张卡片的 HTML 前,明确记录你的时间安排和内容决策。 请遵循下方 v3 风格的结构,以便在第 9 步组装画面时,使用同一份大纲。

必需结构(完整示例见第 6 步):

  • schemaVersion: 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 区域的卡片(分屏方案),卡片容器本身已经只占画布的一半,因此可以使用不透明的卡片背景——它只会覆盖自己所在的一半。

8. 编写每张卡片的 HTML

为每张卡片创建 $WORK_DIR/public/cards/{card-id}.html。每个文件都包含一个符合以下规范的单根 HTML 片段:

卡片 HTML 规范
<div class="card" data-card-id="{cardId}">
  <style>
    /* MUST: every rule starts with .card[data-card-id="{cardId}"] */
    .card[data-card-id="card-01"] .root {
      width: 100%; height: 100%;
      display: flex; ...;
      font-family: 'Caveat', 'LXGW WenKai TC', serif;
      color: var(--text);
      background: var(--bg);
    }
    .card[data-card-id="card-01"] .title { font-size: 84px; ... }
  </style>

  <div class="root">
    <h1
      id="card-01-title"
      data-anim="kinetic-chars"
      data-anim-at="0.3"
      data-anim-duration="0.5"
      data-anim-stagger="0.04"
      data-anim-pattern="pop"
    >
      <span class="char">S</span>
      <span class="char">u</span>
    </h1>
    <div
      id="card-01-line"
      data-anim="grow-x"
      data-anim-at="0.65"
      data-anim-duration="0.5"
      data-anim-target-w="420"
      style="width:0;height:8px;background:var(--accent-0);border-radius:4px;"
    ></div>
  </div>
</div>

硬性规则(hyperframes lint 会拒绝不符合规则的内容):

  • 单个根元素 <div class="card" data-card-id="{cardId}">
  • 内联 <style> 规则必须以以上作用域选择器作为前缀
  • 禁止使用 <script> 标签
  • src= / href= 中禁止使用外部 URL(不得使用 CDN 或远程字体)
  • 禁止使用内联事件处理程序(例如 onclick=)
  • 所有资源都必须通过相对路径引用,并位于同一 public/ 目录中
  • 颜色必须使用 var(--accent-N) 等变量,以便兼容不同主题

**动画通过声明定义,而非编码实现。**只能使用 data-anim-* 属性;绝不能编写 <script> 来实现动画。你会在第 9 步将每个 data-anim-* 声明编译到同一个 GSAP 主时间轴中。

卡片尺寸调整——竖屏优先

这 10 个 references/styles/*.html 文件按 1920×1080 横屏 预览设计。当 storyboard.layout = "portrait"(1080×1920,这是社交媒体 / 移动设备的主流场景)时,所有视觉尺寸都要放大——手机通常拿得离屏幕较近,同样的像素数量在横屏电视风格画布上看起来会更大。

token横屏基准竖屏目标缩放比例
标题(h1/h2 主标题)64–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 类型

此列表是封闭的,这是有意为之:卡片是一个 HTML 片段,其动画由本技能在步骤 9 中编译到共享叠加时间轴(参见该步骤中的 GSAP 映射表)。因此,此工作流不会像合成工作流那样搜索 HyperFrames 组件目录——npx hyperframes catalog 返回的是自带时间轴的独立合成,而卡片没有可挂载它的位置。若要实现下列类型无法在卡片作用域内的纯 CSS <style> 中表达的效果,请使用这些类型。

类型用途关键参数
fade-in入场at、duration、ease?
fade-out退场at、duration、ease?
slide-in滑动入场at、duration、from=left|right|top|bottom、distance
kinetic-chars逐字符弹出at、duration、stagger、pattern=pop|fade — 元素需要包含 <span class="char"> 子元素
typewriter逐字符淡入与 kinetic-chars 相同,但默认的 stagger 更慢
count-up数字动画at、duration、from、to、format=.0f|.1f|.2f|,d
draw-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补间任意 CSSat、duration、props='{...JSON...}'

data-anim-at 是相对于卡片 startSec 的秒数——在步骤 9 中将每个声明编译到 GSAP 时间轴时,加上卡片的 startSec 得到绝对时间,并量化到 1/fps。

9. 组装合成 HTML

暂存资源并编写 $WORK_DIR/public/index.html:

# SKILL_DIR is injected by the host ("Base directory for this skill: …")
SKILL_DIR="<SKILL_DIR>"

mkdir -p "$WORK_DIR/public/fonts" "$WORK_DIR/public/vendor" "$WORK_DIR/public/cards"
cp -n "$SKILL_DIR/assets/fonts/"*            "$WORK_DIR/public/fonts/"
cp -n "$SKILL_DIR/assets/vendor/gsap.min.js" "$WORK_DIR/public/vendor/"
# stage the input video — RE-ENCODE with dense keyframes. Sources with a sparse GOP
# (keyframe interval > ~1s) freeze on seek in the renderer (a frozen frame under the
# overlays); -g / -keyint_min set to your composition fps make every frame seekable.
# (Set both to your fps — 30 shown; use 24/25/60 to match.)
ffmpeg -y -i "$VIDEO_PATH" -c:v libx264 -crf 18 -g 30 -keyint_min 30 \
  -pix_fmt yuv420p -movflags +faststart -c:a aac "$WORK_DIR/public/input-video.mp4"
合成模板
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <style>
      @font-face {
        font-family: "Caveat";
        src: url("fonts/Caveat-400-latin.woff2") format("woff2");
        font-weight: 400;
        font-display: block;
      }
      @font-face {
        font-family: "Caveat";
        src: url("fonts/Caveat-700-latin.woff2") format("woff2");
        font-weight: 700;
        font-display: block;
      }
      @font-face {
        font-family: "LXGW WenKai TC";
        src: url("fonts/LXGWWenKaiTC-400-latin.woff2") format("woff2");
        font-weight: 400;
        font-display: block;
      }
      @font-face {
        font-family: "Inter";
        src: url("fonts/Inter-400-latin.woff2") format("woff2");
        font-weight: 400;
        font-display: block;
      }
      @font-face {
        font-family: "Inter";
        src: url("fonts/Inter-700-latin.woff2") format("woff2");
        font-weight: 700;
        font-display: block;
      }
      @font-face {
        font-family: "Virgil";
        src: url("fonts/Virgil.woff2") format("woff2");
        font-display: block;
      }

      :root {
        /* Pick from the themeId palette table in Step 7 — example: classic */
        --bg: #fff9e3;
        --text: #1e1e1e;
        --accent-0: #1971c2;
        --accent-1: #e03131;
        --accent-2: #2f9e44;
        --accent-3: #e8590c;
        --accent-4: #9c36b5;
        --font-family: "Caveat", "LXGW WenKai TC", serif;
      }
      * {
        box-sizing: border-box;
      }
      /* Body font-family MUST list concrete font names (not just var(--font-family)) —
   the HyperFrames renderer's static analyzer doesn't expand CSS variables when
   resolving fonts, so a var-only chain triggers `font_family_without_font_face`
   lint and falls back to a generic. Use the concrete chain here; cards that
   want the theme font can still reference var(--font-family) internally. */
      html,
      body {
        margin: 0;
        padding: 0;
        width: 100%;
        height: 100%;
        overflow: hidden;
        background: #000;
        font-family: "Inter", "Caveat", "LXGW WenKai TC", ui-sans-serif, system-ui, sans-serif;
      }
      #stage {
        position: relative;
        width: 100%;
        height: 100%;
        overflow: hidden;
      }

      /* video-wrapper holds the source video. Its position / size are animated
   over time by the master timeline (one tween per layout transition). */
      .video-wrapper {
        position: absolute;
        left: 0;
        top: 0;
        width: 1920px;
        height: 1080px;
        overflow: hidden;
        border-radius: 0;
        box-shadow: none;
      }
      .video-wrapper video {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }

      .card-host {
        position: absolute;
        pointer-events: none;
        overflow: hidden;
      }
      .card-host .card {
        position: relative;
        width: 100%;
        height: 100%;
        overflow: hidden;
      }
      .card-host .char {
        display: inline-block;
        visibility: visible;
      }

      /* Subtle drop shadow + rounded corners for non-fullscreen video framings */
      .video-wrapper.framed {
        border-radius: 16px;
        box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
      }
    </style>
  </head>
  <body>
    <div
      id="stage"
      data-composition-id="talking-head-recut"
      data-start="0"
      data-duration="121.2"
      data-fps="30"
      data-width="1920"
      data-height="1080"
    >
      
      <div class="video-wrapper" id="video-wrap">
        <video
          id="bg-video"
          src="input-video.mp4"
          playsinline
          data-has-audio="true"
          data-start="0"
          data-duration="121.2"
          data-track-index="1"
        ></video>
      </div>

      
      
      
      
      
      
      
      
      
      <div
        class="card-host clip"
        data-card-id="card-01"
        data-start="1.0000"
        data-duration="6.5000"
        data-track-index="2"
        style="left:0;top:0;width:1920px;height:1080px;visibility:hidden;opacity:0;"
      >
        
      </div>

      
      <div
        class="card-host clip"
        data-card-id="card-02"
        data-start="8.0000"
        data-duration="12.0000"
        data-track-index="2"
        style="left:0;top:0;width:960px;height:1080px;visibility:hidden;opacity:0;"
      >
        
      </div>

      

      <script src="vendor/gsap.min.js"></script>
      <script>
        (function () {
          // count-up formatter helper
          window.__fmt = function (v, fmt) {
            if (typeof fmt === "string" && /^\.[0-9]+f$/.test(fmt)) {
              return Number(v).toFixed(Number(fmt.slice(1, -1)));
            }
            if (fmt === ",d") return Math.round(v).toLocaleString();
            return String(Math.round(v));
          };

          const tl = window.gsap.timeline({ paused: true });

          // ── Card lifecycle (one block per card) ──
          // Example for card-01 [1.0, 7.5] with kinetic-chars at +0.3, grow-x at +0.65:

          // Enter (fade in over 0.4s)
          tl.set('.card-host[data-card-id="card-01"]', { visibility: "visible" }, 1.0);
          tl.fromTo(
            '.card-host[data-card-id="card-01"]',
            { opacity: 0 },
            { opacity: 1, duration: 0.4, ease: "power2.out" },
            1.0,
          );

          // Card-internal anims (compile each data-anim-* declaration here)
          tl.from(
            '.card[data-card-id="card-01"] #card-01-title .char',
            { opacity: 0, y: 8, scale: 0.8, duration: 0.5, ease: "power2.out", stagger: 0.04 },
            1.3,
          );
          tl.fromTo(
            '.card[data-card-id="card-01"] #card-01-line',
            { width: 0 },
            { width: 420, duration: 0.5, ease: "power2.out" },
            1.65,
          );

          // Exit (fade out over 0.35s, ending at endSec)
          tl.to(
            '.card-host[data-card-id="card-01"]',
            { opacity: 0, duration: 0.35, ease: "power2.in" },
            7.15,
          );
          tl.set('.card-host[data-card-id="card-01"]', { visibility: "hidden" }, 7.5);

          // ── Video framing transitions ──
          // When the next card uses a different composition layout, animate the
          // video-wrapper to its new bounds. Example: card-01 = fullscreen
          // (video hidden behind), card-02 = split composition (zone="side-panel"
          // → video on right, card on left).

          // Card-02 enters at 8.0s with the split composition. Animate video to
          // the right half during the card-01 → card-02 gap (between 7.5 and 8.0s).
          tl.set("#video-wrap", { className: "video-wrapper framed" }, 7.5);
          tl.to(
            "#video-wrap",
            { left: 960, top: 0, width: 960, height: 1080, duration: 0.6, ease: "power2.inOut" },
            7.5,
          );

          // Card-02 enter — same pattern as card-01
          tl.set('.card-host[data-card-id="card-02"]', { visibility: "visible" }, 8.0);
          tl.fromTo(
            '.card-host[data-card-id="card-02"]',
            { opacity: 0 },
            { opacity: 1, duration: 0.4, ease: "power2.out" },
            8.0,
          );
          // ...card-02 internal anims...

          // ── repeat for each card; if the NEXT card's layout differs,
          //    insert another tl.to('#video-wrap', ...) tween before its enter ──

          window.__timelines = window.__timelines || {};
          window.__timelines["talking-head-recut"] = tl;
        })();
      </script>
    </div>
  </body>
</html>
GSAP 语句速查表

将每个 data-anim 属性编译为一条 GSAP 语句。时间为 绝对秒数 = card.startSec + data-anim-at,并按 1/fps 量化。 选择器为 .card[data-card-id="X"] #elementId。

data-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){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 的同级元素,并在布局过渡期间跟随它移动。有关各边框的放置 HTML、建议的 CSS,以及适配的布局,请参阅 references/frames/。快速规则:overlay 布局不显示装饰边框(全出血视频会与边框冲突);画中画布局已有自己的胶囊式样式(圆角 + 白色描边 + 阴影),因此只在 split / stack 上添加装饰边框。

#video-wrap 的 GSAP 目标查找表,按合成布局列出(横屏 1920×1080;竖屏布局见 references/layouts/*.html,4:5 布局根据上述比例缩放方式从竖屏布局推导):

合成布局典型 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-pill 样式(圆角 + 白色描边 + 投影):

// Enter pip — add chrome
tl.set("#video-wrap", { className: "video-wrapper pip-pill" }, T);
tl.to(
  "#video-wrap",
  { left: 1480, top: 760, width: 400, height: 300, duration: 0.6, ease: "power2.inOut" },
  T,
);

// Leave pip — back to clean full-bleed
tl.set("#video-wrap", { className: "video-wrapper" }, T_NEXT);
tl.to(
  "#video-wrap",
  { left: 0, top: 0, width: 1920, height: 1080, duration: 0.6, ease: "power2.inOut" },
  T_NEXT,
);

卡片宿主边界与区域一致。使用步骤 6 顶部的表格,将卡片的 zone 转换为像素边界,然后将其写入卡片宿主的内联 style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..."。对于 video-overlay 区域(overlay 配方),卡片宿主会铺满整个画布;实际可见卡片的位置由 .card .root 内部的 CSS 决定。

HyperFrames 布局 / 动画 QA 规则
  • 先为每张卡片构建静态主视觉帧:卡片完整显示且内容清晰可读的画面。
  • 确认视频、卡片、字幕和图表不会发生意外重叠。
  • 确认隐藏的视频区域会被画面边界裁切,不会显示在预期范围之外。
  • 将一个暂停的主时间线注册为 window.__timelines["talking-head-recut"]。
  • 在页面加载时同步构建时间线;不要使用 async、setTimeout、Promises 或媒体 play() 调用。
  • 不要在渲染路径中使用 Math.random() 或 Date.now()。
  • 仅当根元素设置了有限的 data-duration 时才允许使用 repeat: -1;对于必须更早结束的循环,应根据视频时长计算有限的重复次数。
  • 动画优先使用 GSAP 变换和透明度(x、y、scale、rotation、opacity),而非布局属性(top、left、width、height)。
  • 为 #video-wrap 等包装元素设置动画,而不要直接调整视频元素的尺寸。
  • 避免通过多个时间线同时对同一元素的同一属性设置动画。
  • 使用 data-track-index,而不是 data-layer;使用 data-duration,而不是 data-end。
  • 每个定时元素(card-host、子合成等)都应在自身类名旁包含 class="clip";例如:class="card-host clip"。可见性由 data-start / data-duration 控制:无论是否包含此类,运行时都会将每个 [data-start] 元素限制在其时间窗口内。.clip 是 Studio 和 GSAP 剪辑归属规则用于识别剪辑的标记;省略该类会增加编辑和 lint 检查的难度(lint:timed_element_missing_clip_class,警告)。
  • 对于 body / 全局 font-family,请列出具体字体名称('Inter'、'Caveat' 等),不要使用 var(--font-family) 这样的 CSS 变量。HyperFrames 字体解析器在静态分析时不会展开 CSS 变量(lint:font_family_without_font_face)。卡片内部仍可使用 var(--font-family),因为它们的 @font-face 声明会被加载。
10. 渲染为 MP4
cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
  --skill=talking-head-recut \
  -o output.mp4 \
  --fps 30

hyperframes render <dir> 会读取 <dir>/index.html 并生成 MP4。 源节目的音频保留在 #bg-video 上,因此渲染出的 MP4 无需手动重新合并音视频,即可保留人物对镜讲话的音频。 在 macOS 上强烈建议使用标志 PRODUCER_BROWSER_GPU_MODE=hardware(或 --browser-gpu)——仅使用软件渲染的 Chrome 在大多数笔记本电脑上都会超时。

在完整渲染前,可在指定时间戳截取单帧以进行基本检查:

npx hyperframes snapshot public --at 5    # → public/.hyperframes/artifacts/snapshots/<id>/frame-00-at-5s.png (a single --at ignores --out)
11. 汇报结果

告知用户:

  • 工作目录路径
  • storyboard.json(你设计的卡片大纲)
  • public/cards/*.html(每张卡片一个 HTML 文件)
  • public/index.html(组装后的合成内容)
  • output.mp4(最终视频)
  • 使用的 ASR 提供方
  • 卡片数量及选择方式(用 1 句话说明)
  • 缺失的密钥或质量方面的注意事项

可选的实时预览(仅在用户提出要求时)。 剪辑在 public/index.html 中保持原样播放,叠加层显示在上方,因此预览效果与最终视频一致。运行期间不要打开它。 用户提出要求时,请在渲染完成后启动一个长期运行的服务并提供 URL:

(cd "$WORK_DIR/public" && npx hyperframes preview --background)   # or `npx hyperframes play` for a shareable link

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

评论

支持 Markdown