返回技能市场
内容创作 安全

talking-head-recut

@admin/talking-head-recut

Package an existing talking-head / interview / podcast video with timed, designed GRAPHIC OVERLAY cards — kinetic titles, lower-thirds, data callouts, quotes, side panels, picture-in-picture — synced to the transcript, on a 16:9 / 9:16 / 4:5 canvas of your choice; the clip plays untouched underneath. Trigger on "graphic overlays", "on-screen graphics", "package / dress up my video". Not plain subtitles (/embedded-captions). Unclear → /hyperframes.

admin 热度 466v0.0.1
首先,保持此 skill 为最新版本——运行前先向用户确认:npx hyperframes skills update talking-head-recut。若所有内容均已是最新版本,则快速结束而不做更改;否则会在你使用之前更新此 skill 及其依赖的核心领域 skills。

口播视频再包装

口播视频再包装使用一个完整播放的本地视频,并在其上叠加一系列 经过设计、按时间编排的图形卡片——标题、下三分之一字幕条、数据标注、 引语、侧边面板、画中画——使其与讲话内容同步。agent 设计卡片(时间安排 + 内容),并直接在对话中编写每张卡片的 HTML ,然后组装成一个合成用 HTML,再通过 hyperframes 将其渲染为 MP4。没有固定的原型清单,也没有规定的卡片结构—— 叠加内容取决于转录文本实际表达的内容。

入口为 /hyperframes此 skill 使用经过设计的图形卡片(标题、下三分之一字幕条、数据标注、引语、侧边面板、PiP)包装现有的口播视频片段,而不是添加普通字幕(以文字呈现说话内容)。视频片段保持原样播放。任何其他意图——普通字幕、独立图形、从零制作视频——或任何不确定情况 → 先阅读 /hyperframes:所有路由决策均由意图层负责。
embedded-captions 配套的图形包装工具。字幕将_说话内容_
添加为便于阅读的字幕;本工具则在播放的视频之上添加_经过设计的图形_。
普通字幕 → embedded-captions。从零制作视频 → 创作
工作流(product-launch-video / faceless-explainer / …)。

通过 /hyperframes 路由时,意图层只确认输入(使用哪个视频片段),并告知用户渲染策略问题将稍后询问——宽高比、布局、风格组和卡片数量仍在步骤 7 中确定,届时根据探测到的视频素材信息和转录文本提出建议;该层有关运行形式的问题不适用。如有 BRIEF.md,其中包含已确认的输入和用户备注——请先阅读。

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

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

CLI 解析

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

此 skill 完全依靠 hyperframes CLI 及系统中的 ffmpeg / ffprobe 运行。 转录通过 hyperframes transcribe 使用本地 Whisper——无需第三方 服务、API 密钥或受速率限制的代理。

工作流程

1. 检查环境

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

必需项:

  • ffmpeg / ffprobe(系统)
  • <SKILL_DIR>/assets/fonts/*.woff2<SKILL_DIR>/assets/vendor/gsap.min.js(随此 skill 提供,在步骤 9 中复制到工作目录)

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

在 macOS 上使用 hyperframes render 时,强烈建议:

export PRODUCER_BROWSER_GPU_MODE=hardware

2. 创建工作目录

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

VIDEO_PATH="/absolute/path/input.mp4"
WORK_DIR="videos/$(basename "$VIDEO_PATH" | sed 's/\.[^.]*$//')"
mkdir -p "$WORK_DIR"

3. 提取音频和元数据

# metadata — duration / width / height / fps
ffprobe -v error -select_streams v:0 \
  -show_entries stream=width,height,r_frame_rate \
  -show_entries format=duration -of json "$VIDEO_PATH" > "$WORK_DIR/metadata.json"
# audio
ffmpeg -y -i "$VIDEO_PATH" -vn -acodec libmp3lame -q:a 2 "$WORK_DIR/audio.mp3"

输出:metadata.json(读取 width/height/duration;帧率 = 对 r_frame_rate 分数求值,例如 30000/1001 → 29.97)+ audio.mp3

4. 转录

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

本地 Whisper——无需 API 密钥、代理,也没有速率限制。将词级 transcript.json 写入工作目录(词的 text + start / end 时间戳)。 读取其中的词 / 句时间信息,以确定步骤 6 的卡片时间安排;如需片段级 内容块,请自行在标点 / 停顿处将词组合成 句子。

限制在媒体时长内。Whisper 返回的最后一个词的 end 可能略微超出 视频片段的实际长度——将每张卡片的 endSeccomposition.durationSeconds 限制在 metadata.json 的时长内,否则渲染结果会在视频结束后出现黑色尾帧。

5. 校正转录文本

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

  • 同音词、产品名称、技术术语、标点
  • 原地编辑词的 text保留其 start / end 时间戳
  • 没有预先分组的 segments 数组——需要片段级内容块来安排卡片时间时,自行将词组合成句子(在句末标点 / 停顿处拆分)

6. 起草轻量分镜大纲(在聊天中)

不涉及 CLI。读取 transcript.json + metadata.json,直接设计 卡片。storyboard.json 是 agent 内部的规划产物 ——没有 CLI 命令会使用它;它的作用是帮助你在编写每张卡片的 HTML 前 理清时间安排和内容。请保持 与下方示例一致的结构,以便同一份大纲能够指导 步骤 9 中编写的合成内容:

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

必填卡片字段:

| 字段 | 类型 | 用途 | | ----------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------- | | id | 字符串 | 用于卡片 HTML 和 GSAP 选择器的稳定标识符 | | intent | 字符串 | 自然语言描述;用于生成卡片 | | startSec / endSec | 数字 | 以秒为单位的时间(endSec > startSec) | | accentIndex | 0 \| 1 \| 2 \| 3 \| 4 | 此卡片使用 5 种主题强调色中的哪一种 | | zone | 枚举(见下文) | 卡片在画布上的位置 | | contentHints | 对象 | 自由格式集合;agent 将 kicker/title/detail/data/quote 放在此处 | | archetype(可选) | 字符串 | 可附加的自由格式标签,用于记住卡片的模式;缺省 = 自由形式,也是默认值 | | transition(可选) | 枚举:cut \| fade \| slide \| wipe | 声明式卡片间转场 |

五种 zone 值:

| 区域 | 解析后的边界 | 使用场景 | | ----------------- | ---------------------------------------------- | --------------------------------------- | | fullscreen | 覆盖整个画布 | 高光时刻、大数字、核心口号 | | whiteboard-area | 内缩 40px 边距(或竖屏高度的 45%) | 密集数据 / 带注释的内容 | | lower-third | 底部 30% 区域 | 在可见视频上添加注释 | | side-panel | 右侧 42%(横屏)或底部 40%(竖屏) | 一侧展示数据,另一侧播放视频 | | video-overlay | 整个画布,卡片应大部分透明 | 在满幅视频上叠加注释 |

在步骤 9 中组装合成内容时,按照上表将每张卡片的 zone 解析为 card-host 包装容器上的像素边界。 视频边界在合成层级只设置一次videoTrack.bounds); 要让视频看起来像是“在卡片之间移动”,请在合成内容的 <script> 中针对 #video-wrap 编写 GSAP 补间动画(见步骤 9)。

不规定卡片角色,也不规定叙事结构。卡片内容 取决于视频实际表达的内容——可以全部是引语,也可以全部是数据, 可以用数字开场,也可以用故事开场。让转录文本决定 节奏。

提炼多少个要点?——根据时长 + 密度自动推断。没有固定的 上限。先根据视频时长选择基础节奏,再按 信息密度调整。只有下限固定:至少 5 张卡片,以便 即使是短视频也有节奏感。

步骤 1——按时长确定基础节奏(中等密度下自然的每张卡片秒数):

| 视频时长 | 基础节奏(每张卡片的秒数) | 理由 | | ------------------ | ------------------------ | ------------------------------------------- | | < 60s(短视频) | 6–8s | 观众期望短视频快速切换 | | 60s – 3 min | 8–12s | 常规社交媒体节奏 | | 3 – 10 min | 12–20s | 留出消化空间;每张卡片承载更多内容 | | 10 – 30 min | 20–35s | 长篇讲座 / 访谈节奏 | | > 30 min | 30–60s | 分段展开,接近章节式体验 |

步骤 2——密度乘数(与基础节奏相乘):

| 转录文本中的信号 | 乘数 | 效果 | | --------------------------------------------------------------------------------------------------------------------------- | ---------- | ------------------------ | | 高密度——数字多、观点明确、节奏短促、列表式枚举,每 1–2 句话就有一个新观点 | × 0.7 | 切换更快,卡片更多 | | 中密度——数据与叙述交替出现 | × 1.0 | 基础节奏 | | 低密度——一个较长的故事、反复换角度阐述、缓慢思考式节奏、围绕单个论点展开 | × 1.5 | 切换更慢,卡片更少 |

步骤 3——计算:

secPerCard = basePace × densityMultiplier
cardCount  = max(5, round(videoDurationSec / secPerCard))

示例(注意——不设上限;长视频自然会产生更多卡片):

  • 30s 短视频,单个点睛之句(低密度) → 7 × 1.5 = 10.5s/card → round(30/10.5)=3 → 按下限取 5 张卡片
  • 60s 思考式独白(低密度) → 10 × 1.5 = 15s/card → 4 → 按下限取 5 张卡片
  • 121s 数据丰富的口播(高密度) → 10 × 0.7 = 7s/card → 17 张卡片
  • 5 min 访谈,混合密度 → 16 × 1.0 = 16s/card → 19 张卡片
  • 10 min 深度解读,高密度 → 16 × 0.7 = 11s/card → 55 张卡片
  • 30 min 讲座,中密度 → 28 × 1.0 = 28s/card → 64 张卡片
  • 1 hr 播客,低密度 → 45 × 1.5 = 67.5s/card → 53 张卡片

卡片停留时间超过约 15s 时,应规划更丰富的卡片内容(数据块、 分步揭示、通过交错 动画展开多个子要点)——静态的一句话在超过 8s 后就容易显得枯燥。对于 许多卡片超过 30s 的长视频,可以考虑将时间轴拆分为 子合成(每章一个 .html,使用 data-composition-src 挂载),使每个文件中的 GSAP 时间轴保持易于管理 ——参见 HyperFrames 的 timeline_track_too_dense 静态检查警告。

content 可以是普通字符串("Title: annualized 5.69%\nNotes: ..."),也可以是任何能够表达数据的 JSON 结构。agent 为每张卡片决定其结构。

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

7. 确定渲染策略

与用户确认视觉方向(首先完成此项)

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

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

  • 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 匹配)” ,让用户了解推荐原因。

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

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

  1. 原生澄清工具——使用下方包含 4 个问题的结构化调用。
  2. 其他原生澄清工具(例如 ask_question
  3. request_user_input、IDE 专用提示工具)——使用该工具,并采用 相同的 4 个问题文本和选项列表。保留推荐 标记及预计算值。

  4. 没有原生工具(Codex CLI、仅支持纯文本的运行环境)——**直接在
  5. 普通对话中提问。使用本节 末尾的纯文本模板。保持为一条消息、4 个编号问题** (全局上限是每轮 2–5 个问题;此处符合限制)。

适用于所有渠道的规则:

  • 每轮最多询问 2–5 个问题。此处的 4 个问题符合要求。
  • 即使缺失的信息不会阻止渲染,也要**询问一次,以确认
  • 会实质影响最终输出的参数**(比例、 布局、风格、cardCount)。

  • 如果用户已预先同意采用默认值(“直接使用默认值”、
  • “无需询问”、“全部自动选择”),要求你不要提问,或者 当前任务持续授权自主决策(“给我惊喜” / “替我决定”—— ../hyperframes/references/brief-contract.md § 1)——完全跳过 提问,并使用:recommendedRatiolayout="stack" (适用于不同比例的最稳妥默认值),根据转录文本语气在 最中性的组(editorial/data)中选择 style,以及 autoCount。用一句话告诉用户 你的选择,然后继续。

渠道 A——原生 AskUserQuestion

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

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

关于“其他”——AskUserQuestion 会自动为卡片数量问题添加“其他”选项。用户可以直接输入数字(例如“8”、“20”)作为 cardCount 目标值。将输入解析为整数:若解析成功 → 使用该值(下限为 5);若解析失败 → 回退到“自动”。

渠道 B——纯文本备用方式(Codex CLI、不提供 原生提问工具的运行环境)。将以下内容作为一条普通消息发出,然后等待 回复。使用 1/2/3/4 的列表式选项,便于解析回复:

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

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

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

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

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

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

解析纯文本回复:

  • 接受宽松格式:"1A 2C 3B 4A""A C B A"、`"16:9 / pip /
  • data / auto"、完整句子或 default`。

  • 如果任何回答存在歧义 → 只重新询问有歧义的部分(仍须
  • 保持在 2–5 个问题的上限内)。

  • 如果用户说“默认 / 自动 / 采用所有建议” → 跳过,不再重复询问。

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

  1. 根据比例回答确定输出画布——需要写入的
  2. 确切 storyboard.composition.width / height 值如下:

| 用户选择 | composition.width × 高度 | storyboard.layout 字段 | | ----------- | -------------------------- | ------------------------------------------------------------- | | 16:9 | 1920 × 1080 | "landscape" | | 9:16 | 1080 × 1920 | "portrait" | | 4:5 | 1080 × 1350 | "portrait"(结构定义将 4:5 视为竖屏——高度 > 宽度) |

对于 **references/layouts/*.html 中的 4:5 边界——这些文件 只记录了横屏(1920×1080)和竖屏(1080×1920)。对于 4:5(1080×1350),通过从竖屏 按比例缩放**推导边界:保留水平值,将垂直值乘以 1350/1920 ≈ 0.703。例如:overlay 竖屏卡片 = { x: 24, y: 1280, w: 1032, h: 564 } → 4:5 卡片 = { x: 24, y: round(1280 × 0.703), w: 1032, h: round(564 × 0.703) } = { x: 24, y: 900, w: 1032, h: 397 }

  1. 根据转录文本的语气,将风格组映射为具体风格
  2. ——选择最合适的一种,但必须保持在 用户所选的组内。如果在组内的两种具体风格之间 无法确定,请再次调用 AskUserQuestion,提供这 2–4 个 具体风格选项。

  1. 根据密度回答确定最终 cardCount

| 用户选择 | 最终 cardCount | | ----------------------- | ----------------------------------------- | | 自动(推荐) | 之前已计算的 autoCount | | 更少 | max(5, round(autoCount × 0.6)) | | 更多 | round(autoCount × 1.5)(不设上限) | | 其他 = "<n>"(整数) | max(5, parseInt(n)) | | 其他 = 任意其他内容 | 回退到 autoCount |

  1. 根据此表自动选择视频边框(边框不向用户
  2. 提问——由布局 × 风格确定):

| 布局 | warm-paper 风格(academic / whiteboard / editorial / xhs) | clinical 风格(audit / swiss / terminal / minimal) | experimental 风格(geom / spotlight) | | --------- | ----------------------------------------------------------- | ---------------------------------------------------- | -------------------------------------- | | split | polaroid | hairline | clean | | stack | polaroid | hairline | clean | | pip | clean(画中画胶囊已有装饰效果) | clean | clean | | overlay | clean(满幅视频禁止使用装饰边框) | clean | clean |

  1. 用一句话告诉用户你的选择——比例(+ 画布
  2. 尺寸)、布局、具体风格、边框和最终 cardCount——然后 继续步骤 7 的其余部分(逐卡片布局、运动模式)。

  3. 在工作记忆中记录这五个值(比例 / 布局 / 风格 / 边框 / cardCount)
  4. (无需添加结构字段);在步骤 8 编写每张卡片的 HTML 以及读取匹配的 references/<dim>/<key>.html 以获取设计变量和结构时,会用到它们。

如果用户通过“其他”输入一个不在 10 种风格库中的自由文本风格名,请将其作为提示,自行设计新的卡片 视觉效果,同时仍以所选布局的边界为基础。

渲染策略输入

在步骤 7.0 确定比例 / 布局 / 风格 / cardCount / 边框后, 剩余需要逐卡片决定的事项有:

  • 源视频在 GSAP 目标区域内的适配方式:视频元素具有
  • object-fit: cover,并裁剪到 #video-wrap 的补间动画边界内。 如果希望完全不裁剪(例如横屏画布上的竖屏素材 不应切掉顶部/底部),请将补间动画目标设为与源视频 宽高比一致的矩形,使周围画布能够 显示出来(或用卡片 / 背景填充)。

  • 每张卡片的 card.zone:根据所选合成布局推导
  • (split → side-panel、stack → lower-third、pip → fullscreen、overlay → video-overlay),或者为个别变体选择不同区域 (高光 / 引语使用 fullscreen,密集数据使用 whiteboard-area)。

  • 每张卡片的 accentIndex:每张卡片从 5 种主题强调
  • 色中选择一种。在卡片之间变换颜色以形成节奏;两张 卡片属于同一叙事节拍时,复用相同索引。

  • 运动语言:选择 2–3 种可重复使用的模式,类型来自
  • data-anim(见后面的表格),并坚持使用这些模式,使 合成内容保持连贯。

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

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

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

如需视觉模式灵感,<SKILL_DIR>/references/styles/ 提供了 10 张可独立使用的参考卡片(academic / editorial / minimal / spotlight / geom / whiteboard / audit / terminal / swiss / xhs), 你可以复制并以此为起点——但不要认为必须 照搬其中任何一种。每张卡片都由你自行设计。

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

除合成层级的 themeId 外,此 skill 还在 <SKILL_DIR>/references/ 提供了更丰富的参考 库,涵盖三个相互独立的 视觉维度,可自由组合:

Style  ×  Layout  ×  VideoFrame
 (10)      (4)         (3)

| 维度 | 键 | 决定的内容 | | ---------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | 风格 | academic editorial minimal spotlight geom whiteboard audit terminal swiss xhs | 卡片的视觉语言——字体、颜色、装饰、卡片内部布局 | | 布局 | split stack pip overlay | 源视频与卡片如何共享画布 | | 边框 | clean hairline polaroid | 视频元素周围的装饰外观 |

阅读 <SKILL_DIR>/references/DESIGN_INDEX.md 以了解完整矩阵和粗略的决策指南(访谈 / 产品发布 / 数据分析 / 社交短片 / 技术教程 / 情感故事……)。决定使用某种具体 风格 / 布局 / 边框后,请阅读对应文件:

  • references/styles/<key>.html——可独立使用的卡片片段,包含该
  • 风格的 CSS 设计变量(颜色、字体、内边距、装饰)和一个占位 要点。复制 .card[data-card-id="ref-<key>"] 样式块,将 data-card-id 改为你的卡片标识符,再将占位内容替换成 真实要点,即可完成。

  • references/layouts/<key>.html——准确的 videoBounds + cardBounds,适用于
  • 横屏和竖屏,并提供可直接复制粘贴的 JSON 片段,用于 storyboard.json 中每张卡片的 layout 字段。

  • references/frames/<key>.html——作为下列元素的同级元素添加的装饰性 HTML:
  • #video-wrap,同时提供合成内容 CSS 中的位置说明。

为每张卡片选择 style × layout × frame——只要转场流畅,就可以在卡片之间 更换全部三个维度。常见节奏为: 以 editorial × overlay × clean 开场,为数据卡片切换到 audit × split × hairline ,最后以 whiteboard × pip × polaroid 收尾。

这 10 种风格是 skill 侧的设计变量,不是合成层级的主题—— 无需在 storyboard.composition 中声明;它们位于 每张卡片的 HTML 内。themeId 字段仍可选择 合成层级的调色板(见上表),控制页面主体背景 和视频边框装饰。

布局组合(卡片 + 视频)

每张卡片的两个协同决策决定它如何与 源视频共享画布:

  • card.zone(在 storyboard.json 中声明)——5 个结构定义值
  • 之一;在步骤 9 编写 card-host 包装容器的行内 style 时, 将其解析为像素边界(参见步骤 6 的表格)。

  • 此卡片时间窗口内的 #video-wrap 边界(在合成内容的
  • GSAP 时间轴中以命令式方式声明)——agent 在每次布局转场时,将 #video-wrap 通过补间动画移动到目标矩形。

结构定义不存储每张卡片的视频边界。videoTrack.bounds 在 合成层级只设置一次(默认为整个画布)。视频在卡片之间 “移动”完全依靠在 index.html 中编写的 GSAP 动画。没有 card.layout 字段——此前版本的 文档虚构了此字段;实际结构定义只有 card.zone

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

| 合成布局 | 推荐的 card.zone | #video-wrap 的 GSAP 目标(横屏 1920×1080) | #video-wrap 的 GSAP 目标(竖屏 1080×1920) | 使用场景 | | ------------------ | ----------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------- | | split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | { left: 0, top: 960, width: 1080, height: 960 }(下半部分) | 讲话者 + 数据并排 / 50:50 权重 | | stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 }(顶部 52%) | { left: 0, top: 0, width: 1080, height: 844 }(顶部 44%) | 讲话者在上方 + 摘要卡片在下方 | | pip | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } + 添加 .framed 类 | { left: 690, top: 28, width: 360, height: 203 } + 添加 .framed | 内容丰富的卡片 + 角落画中画 | | overlay | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 }(满幅) | { left: 0, top: 0, width: 1080, height: 1920 } | 电影感 / 戏剧感 / 完整视频上的玻璃卡片 |

对于 4:5(1080×1350),将竖屏的 y/h 值乘以 1350/1920 ≈ 0.703 (参见步骤 7.0 渠道 A / 渠道 B 的 recommendedRatio 解析 表)。

个别变体可使用的其他区域值(仍使用 card.zone;没有 虚构的“layout”字段):

| zone | 解析后的边界 | 常见用途 | | ----------------- | ------------------------------------------------------ | ------------------------------------- | | fullscreen | 覆盖整个画布 | 主视觉卡片,视频通过补间动画变为隐藏/画中画 | | whiteboard-area | 内缩 40px 边距(横屏)或底部 45%(竖屏) | 密集数据卡片,留有边距 | | lower-third | 底部 30% 区域 | 口播视频注释 | | side-panel | 右侧 42%(横屏)或底部 40%(竖屏) | 侧栏 / “split”方案 | | video-overlay | 整个画布;卡片根元素应透明 | 满幅视频上的玻璃叠加层 |

可以为不同卡片混合使用方案——根据当前内容选择合适的 card.zone ,然后在卡片之间为 #video-wrap 编写 GSAP 补间动画。

分镜渲染约定

storyboard.json 是 agent 内部的规划产物——没有 CLI 命令会解析它。它用于在你编写每张卡片的 HTML 前,明确 时间安排和内容决策。请坚持使用下方的 v3 风格 结构,让同一份大纲指导步骤 9 中组装的 合成内容。

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

  • schemaVersion: 3
  • composition: { fps, width, height, durationSeconds, layout, themeId, seed }——注意,durationSeconds/fps/themeId/layout 位于 composition 内部,而非顶层
  • videoTrack: { sourcePath, startSec, endSec, bounds? }——视频边界默认为整个画布
  • subtitles: { enabled, ... }
  • cards[]——每张卡片有 6 个必填字段:idintentstartSecendSecaccentIndexzonecontentHints

规则:

  • 卡片时间保持在 composition.durationSeconds 内,除非有意安排,否则不应重叠(重叠时使用 data-track-index 控制层叠顺序)。
  • 视觉细节放在卡片 HTML 片段中(步骤 8),而非 contentHintscontentHints 是你用于设计卡片的结构化提示内容;渲染外观由 HTML 决定。
  • 保持分镜结构稳定——虽然没有程序解析它,但在编写步骤 8/9 的内容时,你会重新读取它,结构一致能确保卡片 ID 和时间安排保持同步。
  • Agent 侧的决策,例如“我选择了 overlay × geom × clean”,不得放入 storyboard.json——将它们保留在工作记忆中,并在编写卡片 HTML + GSAP 补间动画时使用。

与视频共享画布的卡片应使用透明背景。 当 GSAP 补间动画使视频在卡片后方/旁边保持可见时(overlay 方案、pip 方案,或任何 card.zone = 'lower-third' | 'video-overlay' 时刻),卡片的 .root 禁止绘制完全不透明的背景—— 否则会遮挡视频。有两种模式:

/* Pattern A: transparent root, page body provides the cream backdrop */
html,
body {
  background: var(--bg);
}
.card[data-card-id="card-X"] .root {
  background: transparent;
}

/* Pattern B: explicit per-card background ONLY for fullscreen cards */
.card[data-card-id="card-hero"] .root {
  background: var(--bg);
}
.card[data-card-id="card-overlay"] .root {
  background: transparent;
}

对于区域为 side-panel 的卡片(split 方案),card-host 本身已经 只占画布的一半,因此卡片可以使用不透明背景——它只覆盖自己所在的 一半。

8. 编写每张卡片的 HTML

为每张卡片创建 $WORK_DIR/public/cards/{card-id}.html。每个文件 包含一个单根 HTML 片段,遵循以下约定:

卡片 HTML 约定

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

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

硬性规则(违反规则会被 hyperframes 静态检查拒绝):

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

动画采用声明式定义,不直接编写动画代码。只能使用 data-anim-* 属性 ;禁止编写 <script> 来实现动画。在步骤 9 中,将每个 data-anim-* 声明编译到单一的主 GSAP 时间轴中。

卡片尺寸——竖屏以移动端为先

10 references/styles/*.html 的尺寸针对 1920×1080 横屏 预览设计。在 storyboard.layout = "portrait"(1080×1920,社交 / 移动端的主要 场景)下,放大所有视觉尺寸——手机屏幕 观看距离近,同样的像素数量看起来会比 横屏 TV 式画布更小。

| 设计变量 | 横屏基准 | 竖屏目标 | 缩放比例 | | ------------------------- | ------------------ | ------------------- | ------------- | | 标题(h1/h2 主视觉) | 64–96px | 88–132px | ×1.35 | | 详情 / 正文 | 24–30px | 30–40px | ×1.30 | | 引题 / 标签块文字 | 14–16px | 18–22px | ×1.30 | | 时间码 / 辅助信息 | 12–14px | 16–18px | ×1.30 | | 数据块主数字 | 48–60px | 64–88px | ×1.40 | | 行高乘数 | 1.05–1.5 | 相同 | (不要缩放) |

经验法则:portraitPx = round(landscapePx × 1.3),然后向下 取整为附近的 4px 倍数,以形成视觉节奏。主视觉标题最多可放大到 ×1.4;小号辅助文本保持在 ×1.2,以避免拥挤。

竖屏下内边距略微缩小——卡片较窄,较大的 横屏内边距(40–64px)会占用过多宽度。竖屏使用 24–36px 的水平 内边距。

如果制作的单张卡片必须同时适用于两种布局, 优先在卡片根元素上使用 @container 查询,避免硬编码尺寸:

.card[data-card-id="X"] .root {
  container-type: inline-size;
}
.card[data-card-id="X"] .title {
  font-size: clamp(64px, 8.5cqi, 132px);
}
.card[data-card-id="X"] .detail {
  font-size: clamp(24px, 3.2cqi, 40px);
}

但对大多数卡片而言,选择一种布局即可——只需选取 尺寸表中与分镜 layout 字段匹配的那一列。

可用的 data-anim 类型

此列表有意采用封闭集合:卡片是一个 HTML 片段,其动画由此 skill 在步骤 9 中编译到共享叠加层时间轴上(参见该处的 GSAP 映射表 )。因此,此工作流不会像合成工作流那样搜索 HyperFrames 组件注册表 ——npx hyperframes catalog 返回的是带有各自时间轴的独立 合成内容,而卡片没有可挂载此类内容的位置。对于 下列类型无法表达的效果,可在卡片限定作用域的 <style> 中使用普通 CSS 实现。

| 类型 | 用途 | 关键参数 | | --------------- | ------------------- | ----------------------------------------------------------------------------------------------- | | fade-in | 入场 | at, duration, ease? | | fade-out | 退场 | at, duration, ease? | | slide-in | 滑动入场 | at, duration, from=left\|right\|top\|bottom, distance | | kinetic-chars | 逐字符弹出 | atdurationstaggerpattern=pop\|fade——元素需要 <span class="char"> 子元素 | | typewriter | 逐字符淡入 | 与 kinetic-chars 相同,但默认交错间隔更长 | | count-up | 数字动画 | at, duration, from, to, format=.0f\|.1f\|.2f\|,d | | draw-path | SVG 路径逐步显示 | atduration——元素应为 <path> | | grow-y | 柱形高度 | atdurationtarget-h(px)——元素初始状态为 height:0 | | grow-x | 条形宽度 | atdurationtarget-w(px)——元素初始状态为 width:0 | | scale-pop | 弹出入场 | at, duration | | blur-in | 失焦 → 聚焦 | at, duration | | mask-reveal | 裁剪揭示 | at, duration, direction=left\|right\|top\|bottom | | morph-to | 对任意 CSS 进行补间动画 | at, duration, props='{...JSON...}' |

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

9. 组装合成 HTML

准备资源并编写 $WORK_DIR/public/index.html

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

mkdir -p "$WORK_DIR/public/fonts" "$WORK_DIR/public/vendor" "$WORK_DIR/public/cards"
cp -n "$SKILL_DIR/assets/fonts/"*            "$WORK_DIR/public/fonts/"
cp -n "$SKILL_DIR/assets/vendor/gsap.min.js" "$WORK_DIR/public/vendor/"
# stage the input video — RE-ENCODE with dense keyframes. Sources with a sparse GOP
# (keyframe interval > ~1s) freeze on seek in the renderer (a frozen frame under the
# overlays); -g / -keyint_min set to your composition fps make every frame seekable.
# (Set both to your fps — 30 shown; use 24/25/60 to match.)
ffmpeg -y -i "$VIDEO_PATH" -c:v libx264 -crf 18 -g 30 -keyint_min 30 \
  -pix_fmt yuv420p -movflags +faststart -c:a aac "$WORK_DIR/public/input-video.mp4"

合成模板

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <style>
      @font-face {
        font-family: "Caveat";
        src: url("fonts/Caveat-400-latin.woff2") format("woff2");
        font-weight: 400;
        font-display: block;
      }
      @font-face {
        font-family: "Caveat";
        src: url("fonts/Caveat-700-latin.woff2") format("woff2");
        font-weight: 700;
        font-display: block;
      }
      @font-face {
        font-family: "LXGW WenKai TC";
        src: url("fonts/LXGWWenKaiTC-400-latin.woff2") format("woff2");
        font-weight: 400;
        font-display: block;
      }
      @font-face {
        font-family: "Inter";
        src: url("fonts/Inter-400-latin.woff2") format("woff2");
        font-weight: 400;
        font-display: block;
      }
      @font-face {
        font-family: "Inter";
        src: url("fonts/Inter-700-latin.woff2") format("woff2");
        font-weight: 700;
        font-display: block;
      }
      @font-face {
        font-family: "Virgil";
        src: url("fonts/Virgil.woff2") format("woff2");
        font-display: block;
      }

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

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

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

      /* Subtle drop shadow + rounded corners for non-fullscreen video framings */
      .video-wrapper.framed {
        border-radius: 16px;
        box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
      }
    </style>
  </head>
  <body>
    <div
      id="stage"
      data-composition-id="talking-head-recut"
      data-start="0"
      data-duration="121.2"
      data-fps="30"
      data-width="1920"
      data-height="1080"
    >
      <!-- Layer 1: source video — initial position matches card-01's layout -->
      <div class="video-wrapper" id="video-wrap">
        <video
          id="bg-video"
          src="input-video.mp4"
          muted
          playsinline
          data-start="0"
          data-duration="121.2"
          data-track-index="1"
        ></video>
      </div>
      <!-- Preserve the source program audio while the visual video stays muted. -->
      <audio
        id="source-audio"
        src="input-video.mp4"
        data-start="0"
        data-duration="121.2"
        data-track-index="10"
        data-volume="1"
      ></audio>

      <!-- Layer 2: each card-host sits at the bounds dictated by its layout. -->
      <!-- IMPORTANT: every card-host MUST carry BOTH "card-host" and "clip" classes. -->
      <!--   - "card-host"  → our positioning + pointer-events styles                 -->
      <!--   - "clip"       → the marker Studio and the linter use to recognise a     -->
      <!--                    clip. Visibility itself comes from data-start /         -->
      <!--                    data-duration, which the runtime honours with or        -->
      <!--                    without this class                                      -->
      <!--                    (lint: timed_element_missing_clip_class, a warning).    -->
      <!-- Example: card-01 with zone="fullscreen" → card-host covers (0,0,1920,1080) -->
      <div
        class="card-host clip"
        data-card-id="card-01"
        data-start="1.0000"
        data-duration="6.5000"
        data-track-index="2"
        style="left:0;top:0;width:1920px;height:1080px;visibility:hidden;opacity:0;"
      >
        <!-- paste the contents of public/cards/card-01.html here -->
      </div>

      <!-- Example: card-02 with zone="side-panel" (split composition layout) → card on left half -->
      <div
        class="card-host clip"
        data-card-id="card-02"
        data-start="8.0000"
        data-duration="12.0000"
        data-track-index="2"
        style="left:0;top:0;width:960px;height:1080px;visibility:hidden;opacity:0;"
      >
        <!-- card-02 HTML -->
      </div>

      <!-- ...one "card-host clip" per card with inline bounds matching resolveZoneBounds(card.zone)... -->

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

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

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

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

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

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

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

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

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

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

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

GSAP 语句速查表

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

| data-anim | GSAP 语句模板 | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | fade-in | tl.fromTo(SEL, { opacity: 0 }, { opacity: 1, duration: D, ease: 'power2.out' }, T); | | fade-out | tl.to(SEL, { opacity: 0, duration: D, ease: 'power2.in' }, T); | | slide-in (from=left, dist=80) | tl.fromTo(SEL, { opacity: 0, x: -80 }, { opacity: 1, x: 0, duration: D, ease: 'power2.out' }, T); | | kinetic-chars(弹出) | tl.from(SEL + ' .char', { opacity: 0, y: 8, scale: 0.8, duration: D, ease: 'power2.out', stagger: S }, T); | | count-up | (function(){const o={v:FROM};tl.to(o,{v:TO,duration:D,ease:'power2.out',onUpdate:function(){const el=document.querySelector(SEL);if(el)el.textContent=__fmt(o.v,'FMT');}},T);})(); | | draw-path | (function(){const el=document.querySelector(SEL);if(el){const L=el.getTotalLength();tl.set(SEL,{strokeDasharray:L,strokeDashoffset:L},T);tl.to(SEL,{strokeDashoffset:0,duration:D,ease:'power2.inOut'},T);}})(); | | grow-x (target-w=W) | tl.fromTo(SEL, { width: 0 }, { width: W, duration: D, ease: 'power2.out' }, T); | | grow-y (target-h=H) | tl.fromTo(SEL, { height: 0 }, { height: H, duration: D, ease: 'power2.out' }, T); | | scale-pop | tl.fromTo(SEL, { opacity: 0, scale: 0.6 }, { opacity: 1, scale: 1, duration: D, ease: 'back.out(1.6)' }, T); | | mask-reveal (direction=left) | tl.fromTo(SEL, { clipPath: 'inset(0 100% 0 0)' }, { clipPath: 'inset(0 0 0 0)', duration: D, ease: 'power2.inOut' }, T); |

量化:T = Math.round(absSec * fps) / fps。在 30fps 下,最小 步长为 1/30 ≈ 0.0333s;在 JS 字面量中 四舍五入保留 4 位小数(.toFixed(4))即可。

视频构图参考(按 layout 值)

视频容器的选择器为 #video-wrap。使用 tl.to('#video-wrap', { ...bounds }, T) 在卡片之间为其 边界添加动画。 初始边界应以内联方式设置在元素上,使其与 card-01 的 布局匹配。选择 0.5–0.7s 的转场时长,并使用 ease: 'power2.inOut'

装饰边框clean / hairline / polaroid)作为 #video-wrap同级元素存在,并在布局转场时跟随它移动。 参见 [references/frames/](references/frames/),了解各边框的位置 HTML、建议的 CSS,以及适配的布局。简单规则: overlay 布局不显示装饰边框(满幅视频 与装饰效果冲突);PiP 布局已有自身的胶囊式处理 (圆角 + 白色描边 + 阴影),因此只在 split / stack 之上添加装饰边框。

各合成布局下 #video-wrapGSAP 目标查找表 (横屏 1920×1080——竖屏和 4:5 请参见 references/layouts/*.html ,其中列出了全部三种比例):

| 合成布局 | 典型 card.zone | #video-wrap 的 GSAP 目标 | 附加 css 类 | | ------------------------------------ | ----------------- | ------------------------------------------------------------------------- | ------------------------------------------ | | split | side-panel | { left: 960, top: 0, width: 960, height: 1080 } | — | | stack | lower-third | { left: 14, top: 14, width: 1892, height: 548 }(顶部 52%) | — | | pip(右下角) | fullscreen | { left: 1480, top: 760, width: 400, height: 300 } | pip-pill(圆角 + 描边 + 阴影) | | pip(左上角) | fullscreen | { left: 40, top: 40, width: 400, height: 300 } | pip-pill | | overlay(视频满幅) | video-overlay | { left: 0, top: 0, width: 1920, height: 1080 }(与默认值一致) | — | | 隐藏视频(纯图形时刻) | fullscreen | { opacity: 0 }(或移出画布) | — |

进入或离开画中画时,切换 pip-pill 装饰效果(圆角 + 白色描边 + 投影) 的方法:

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

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

Card-host 边界与区域匹配。使用步骤 6 开头的表格,将卡片的 zone 解析为 像素边界,然后将其 写入 card-host 的行内 style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..."。对于 video-overlay 区域(overlay 方案), card-host 填满整个画布——.card .root 内的 CSS 决定实际可见卡片所在的位置。

HyperFrames 布局 / 动画 QA 规则

  • 先构建每张卡片的静态主视觉帧:即卡片完全可见且清晰可读的时刻。
  • 确认视频、卡片、字幕和图表没有意外重叠。
  • 确认隐藏的视频区域已被边框裁剪,不会显示在预定边界之外。
  • 将一个暂停状态的主时间轴注册为 window.__timelines["talking-head-recut"]
  • 在页面加载时同步构建时间轴;禁止使用 asyncsetTimeout、Promises 或媒体 play() 调用。
  • 禁止在渲染路径中使用 Math.random()Date.now()
  • 禁止使用 repeat: -1;根据视频时长计算有限的重复次数。
  • 实现运动时,优先使用 GSAP 变换和透明度(xyscalerotationopacity),而非布局属性(topleftwidthheight)。
  • #video-wrap 等包装容器添加动画,而不是直接改变视频元素的尺寸。
  • 避免在同一时间通过多个时间轴对同一元素的同一属性添加动画。
  • 使用 data-track-index,不使用 data-layer;使用 data-duration,不使用 data-end
  • 每个定时元素(card-host、子合成等)都应在自身类之外包含 class="clip"——例如 class="card-host clip"。可见性本身由 data-start / data-duration 驱动:无论是否存在此类,运行时都会将每个 [data-start] 元素限制在其时间窗口内。Studio 和 GSAP 片段归属规则会读取 .clip 标记以识别片段,因此缺少它会增加编辑和静态检查的难度(静态检查:timed_element_missing_clip_class,警告级别)。
  • 对于主体 / 全局 font-family,请列出具体字体名称'Inter', 'Caveat', …),不要使用 var(--font-family) 这样的 CSS 变量。HyperFrames 字体解析器在静态分析时不会展开 CSS 变量(静态检查:font_family_without_font_face)。卡片内部仍可使用 var(--font-family),因为其 @font-face 声明已加载。

10. 渲染为 MP4

cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
  --skill=talking-head-recut \
  -o output.mp4 \
  --fps 30

hyperframes render <dir> 读取 <dir>/index.html 并生成 MP4。 规范的合成内容会将视觉 <video> 保持静音,并将相同的 源文件挂载为根级 #source-audio 音轨,因此渲染后的 MP4 能保留 口播音频,无需手动重新封装。这里使用独立音轨 而非 data-has-audio="true",使其音量和自动压低音量效果仍可在时间轴上独立 控制。 在 macOS 上强烈建议使用标志 PRODUCER_BROWSER_GPU_MODE=hardware(或 --browser-gpu) ——仅通过软件进行 Chrome 渲染在大多数笔记本上会 超时。

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

npx hyperframes snapshot public --at 5    # → public/snapshots/frame-00-at-5s.png (a single --at ignores --out)

11. 报告结果

告知用户:

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

可选实时预览(仅在用户要求时)。视频片段在 public/index.html 内保持原样播放,叠加层显示在其上方,因此预览能忠实呈现效果。禁止在执行过程中打开预览。用户提出要求时,在渲染之后启动长期运行的服务器,并报告 URL:

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

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

qianwen skills install @admin/talking-head-recut