mcpField
G
graphic-overlays

graphic-overlays

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

关于 graphic-overlays

为现有访谈视频添加定时图形卡片的技能,不改动原始画面。它依据转录内容设计卡片,并通过 hyperframes 将合成内容渲染为 MP4。

主要功能

  1. 01使用 Whisper 在本地转录音频
  2. 02依据转录内容安排图形卡片时序
  3. 03创建灵活的 HTML 卡片设计
  4. 04将合成内容渲染为 MP4
  5. 05检查中间文件与设计提纲

适用场景

  1. 01为访谈视频添加标题和下三分之一字幕条
  2. 02在视频片段上呈现引语和数据提示
  3. 03为访谈视频编排定时图形面板

安装

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

$npx skills add https://github.com/heygen-com/hyperframes --skill graphic-overlays

安装后在 Claude Code 中输入/graphic-overlays确认已加载。

包含的文件

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

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

详细介绍

图形叠加层

Graphic Overlays 会处理一段完整播放的本地视频,并在其上叠加一系列定时出现的精心设计的图形卡片——标题、下三分之一字幕、数据标注、引言、侧边面板、画中画——与讲述内容同步。Agent 负责设计卡片(时间和内容),并在对话中直接编写每张卡片的 HTML,然后组装成一个合成 HTML 文件,再通过 hyperframes 渲染为 MP4。这里没有固定的类型列表,也没有规定的卡片结构——叠加内容根据转录文本实际讲述的内容生成。

构建前请确认工作流。 此技能会为现有的对镜讲述片段添加精心设计的图形卡片(标题、下三分之一字幕、数据标注、引言、侧边面板、PiP)。如果用户想要的是纯字幕(将口述内容显示为文字)→ /embedded-captions;如果是单个简短且无旁白的元素(单个 Logo 音效画面 / 下三分之一字幕)→ /motion-graphics。片段会原样播放——调整时间、颜色、画面构图、顺序或音频属于 NLE 编辑,不在此技能范围内。从 URL / 主题 / PR 开始制作 → 使用创作工作流。不确定该用叠加层还是字幕?先阅读 /hyperframes。

这是 embedded-captions 的图形包装版本。 字幕会将_口述内容_ 添加为易读的字幕;此技能会在播放的视频上叠加_设计好的图形_。 纯字幕 → embedded-captions。从头制作视频 → 创作工作流 (product-launch-video / faceless-explainer / …)。

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

  • metadata.json — 时长 / 宽度 / 高度 / 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 运行。 转录使用 hyperframes transcribe 在本地运行 Whisper——无需第三方服务、API 密钥或限速代理。

工作流

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

必需项:

  • ffmpeg / ffprobe(系统)
  • <SKILL_DIR>/assets/fonts/*.woff2、<SKILL_DIR>/assets/vendor/gsap.min.js(此技能内捆绑的文件,会在步骤 9 中暂存到工作目录)

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

在 macOS 上运行 hyperframes render 时,强烈建议安装:

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

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

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

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

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

本地运行 Whisper——无需 API 密钥、代理或速率限制。会在工作目录中生成词级别的 transcript.json(词语 text 以及 start / end 时间戳)。阅读该文件,获取驱动步骤 6 卡片定时的词语 / 句子时间;如果需要句子级片段,请自行根据标点 / 停顿将词语分组。

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

5. 校正转录文本

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

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

无需使用 CLI。 阅读 transcript.json + metadata.json 并直接设计卡片。storyboard.json 是 Agent 内部的规划文件 — 没有 CLI 命令会读取它;它的作用是在为每张卡片编写 HTML 之前,帮助你清晰地思考时间安排和内容。请保持结构与下方示例一致,这样你在第 9 步创作的内容就可以沿用同一份大纲:

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

必需的卡片字段:

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

五种 zone 值:

zone对应边界适用场景
fullscreen覆盖整个画布主视觉时刻、大数字、箴言
whiteboard-area内缩 40px 边距(或纵向画布高度的 45%)密集数据 / 带注释的内容
lower-third底部 30% 区域在可见视频上叠加注释
side-panel右侧 42%(横向画布)或底部 40%(纵向画布)一侧显示数据,另一侧显示视频
video-overlay整个画布;卡片应大部分透明在全出血视频上叠加注释

在第 9 步组装合成内容时,请根据上表将每张卡片的 zone 解析为卡片宿主包装元素上的像素边界。 视频边界只在合成层级设置一次(videoTrack.bounds); 若要让视频看起来像是在“卡片间移动”,请在合成内容的 <script> 中针对 #video-wrap 编写 GSAP 补间动画(见第 9 步)。 不预设卡片角色,也不预设叙事弧线。 卡片根据视频的实际内容生成——可以全是引语,也可以全是数据;可以用一个数字开场,也可以从一个故事讲起。让转录文本决定节奏。

要点有多少个?——根据时长和信息密度自动推断。 不设固定上限。先根据视频时长确定基础节奏,再根据信息密度调整。唯一固定的下限:至少 5 张卡片,这样即使是短视频也有节奏感。

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

视频时长基础节奏(每张卡片的秒数)原因
< 60s(短视频)6–8s观众期待短视频采用快速剪辑
60s – 3 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 可以是纯字符串("Title: annualized 5.69%\nNotes: ..."),也可以是能呈现数据的任意 JSON 结构。Agent 会为每张卡片决定结构。

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

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

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

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

**环境兼容性 — 选择最合适的问题渠道。**并非每种运行环境都提供相同的结构化提问工具。按以下顺序选择:

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

适用于所有渠道的规则:

  • 每轮最多提问 2–5 个问题。这里的 4 个问题符合要求。
  • 即使缺少信息不会阻碍渲染,也要提问一次,确认会显著影响最终输出的参数(比例、布局、风格、cardCount)。
  • 如果用户已经预先同意使用默认值(“直接使用默认值”“不用问”“全部自动选择”),或要求你不要提问,则完全跳过提问,并使用:recommendedRatio、layout="stack"(最稳妥的跨比例默认值)、根据转录内容语气从最中性的类别(editorial/data)中选择的 style,以及 autoCount。用一句话告知用户你的选择,然后继续。

渠道 A — 原生 AskUserQuestion:

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

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

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

渠道 B — 纯文本备用方案(Codex CLI、没有原生提问工具的运行环境)。将以下内容作为一条普通消息发送,然后等待回复。采用 1/2/3/4 的项目符号格式,可便于解析回复:

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

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

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

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

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

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

解析纯文本回复:

  • 接受较宽松的格式:“1A 2C 3B 4A”、“A C B A”、“16:9 / pip / data / auto”、完整句子或 default。
  • 如果任何答案不明确 → 仅重新询问不明确的部分(仍须遵守 2–5 个问题的上限)。
  • 如果用户说“default / auto / use all recommendations” → 跳过提问,不要再次确认。

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

  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. 在工作记忆中记录这五个值(比例 / 布局 / 风格 / 边框 / cardCount), 无需添加模式字段;在步骤 8 编写每张卡片的 HTML,以及读取匹配的 references/<dim>/<key>.html 以获取令牌和结构时,会用到这些值。

如果用户通过“Other”选择了 10 种风格库中没有的自由文本风格名称, 请将其视为设计全新卡片视觉效果的提示,但仍须以所选布局的边界范围为准。

渲染策略输入

在第 7.0 步锁定比例 / 布局 / 样式 / cardCount / frame 后, 每张卡片还需确定以下内容:

  • GSAP 目标内部的源视频适配方式:视频元素设置了 object-fit: cover,并裁剪到 #video-wrap 的补间边界内。 如果不希望裁剪(例如,横向画布上的纵向源视频不应被截掉顶部或底部), 请将补间目标设为与源视频宽高比相符的矩形,并让画布周围部分透出 (或使用卡片 / 背景填充)。
  • 每张卡片的 card.zone:根据所选的构图布局确定 (分屏 → 侧边栏,堆叠 → 下三分之一,画中画 → 全屏,叠加 → 视频叠加),或者为个别变体选择其他区域 (主视觉 / 引言使用全屏,密集数据使用白板区域)。
  • 每张卡片的 accentIndex:每张卡片会从 5 种主题强调色中选取一种。 可在不同卡片间变化以形成节奏;同一叙事段落中的两张卡片可重复使用相同索引。
  • 动态效果词汇:从 data-anim 类型中选取 2–3 种可重复使用的模式 (见后文表格),并保持一致,让构图风格协调。

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

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

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

如需参考视觉模式,<SKILL_DIR>/references/styles/ 提供了 10 张独立的参考卡片(学术 / 编辑风 / 极简 / 聚光 / 几何 / 白板 / 审计 / 终端 / 瑞士风 / 小红书), 可复制它们作为起点,但不必拘泥于这些风格。每张卡片都由你自行设计。

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

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

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

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

  • references/styles/<key>.html — 自包含的卡片片段,包含该 风格的 CSS 令牌(颜色、字体、内边距、装饰元素)和占位要点。复制 .card[data-card-id="ref-<key>"] 样式块,将 data-card-id 重命名为卡片的 id,把占位内容替换为 实际要点即可。
  • references/layouts/<key>.html — 横屏和竖屏对应的精确 videoBounds + cardBounds,并附有可复制粘贴到 storyboard.json 每张卡片 layout 字段中的 JSON 片段。
  • references/frames/<key>.html — 添加为 #video-wrap 同级元素的装饰性 HTML,以及组合 CSS 的放置说明。

可按卡片选择 style × layout × frame——只要过渡自然,就可以在不同卡片之间更改这三项。常见节奏: 以 editorial × overlay × clean 开场,数据卡切换到 audit × split × hairline, 最后以 whiteboard × pip × polaroid 收尾。

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

布局组合(卡片 + 视频)

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

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

模式中不会存储每张卡片的视频边界。videoTrack.bounds 在组合级别仅设置一次(默认为整个画布)。视频在不同卡片之间“移动”完全由 index.html 中编写的 GSAP 动画实现。没有 card.layout 字段——本文档早期版本曾虚构过这个字段;实际模式中只有 card.zone。

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

组合布局建议的 card.zone#video-wrap 的 GSAP 目标(横屏 1920×1080)#video-wrap 的 GSAP 目标(竖屏 1080×1920)适用场景
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 步中 Channel A / Channel B 的 recommendedRatio 分辨率表)。

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

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

可以为不同卡片混合使用不同方案——根据具体场景选择合适的 card.zone,然后编写卡片之间 #video-wrap 的 GSAP 缓动。

故事板渲染规范

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

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

  • schemaVersion: 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 类型
类型用途关键参数
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="graphic-overlays"
      data-start="0"
      data-duration="121.2"
      data-fps="30"
      data-width="1920"
      data-height="1080"
    >
      
      <div class="video-wrapper" id="video-wrap">
        <video
          id="bg-video"
          src="input-video.mp4"
          muted
          playsinline
          data-start="0"
          data-duration="121.2"
          data-track-index="1"
        ></video>
      </div>

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

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

      

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

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

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

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

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

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

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

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

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

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

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

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

data-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 布局不使用装饰边框(全出血视频会与装饰元素冲突);画中画布局已有自己的胶囊样式(border-radius + 白色描边 + 阴影),因此只在 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-pill 样式(圆角 + 白色描边 + 投影):

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

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

卡片容器边界与区域匹配。使用第 6 步顶部的表格,将卡片的 zone 换算为像素边界,然后将这些值写入卡片容器的内联 style="left:Xpx;top:Ypx;width:Wpx; height:Hpx;..."。对于 video-overlay 区域(overlay 方案),卡片容器会填满整个画布 — 实际可见卡片的位置由 .card .root 内部的 CSS 决定。

HyperFrames 布局 / 动画 QA 规则
  • 先构建每张卡片的静态主视觉帧:即卡片完整显示且清晰可读的时刻。
  • 确认视频、卡片、字幕 / 说明文字和图表没有意外重叠。
  • 确认隐藏的视频区域已被画面边框裁剪,不会显示在预期边界之外。
  • 将一个暂停的主时间线注册为 window.__timelines["graphic-overlays"]。
  • 在页面加载时同步构建时间线;不要使用 async、setTimeout、Promises 或媒体 play() 调用。
  • 不要在渲染路径中使用 Math.random() 或 Date.now()。
  • 不要使用 repeat: -1;应根据视频时长计算有限的重复次数。
  • 动画优先使用 GSAP 变换和不透明度(x、y、scale、rotation、opacity),而不是布局属性(top、left、width、height)。
  • 对 #video-wrap 等包装元素进行动画,而不是直接调整视频元素的尺寸。
  • 避免在多个时间线中同时为同一元素的同一属性添加动画。
  • 使用 data-track-index,不要使用 data-layer;使用 data-duration,不要使用 data-end。
  • 每个定时元素(card-host、子合成等)都必须在自身类名之外包含 class="clip",例如 class="card-host clip"。HyperFrames 运行时使用 .clip,将元素的可见性限制在 data-start … data-start+data-duration 时间范围内。若缺少此类名,元素会在整个视频期间可见(lint:timed_element_missing_clip_class)。
  • 对于 body / 全局 font-family,请列出具体字体名称('Inter', 'Caveat', …),不要使用 var(--font-family) 之类的 CSS 变量。HyperFrames 字体解析器在静态分析期间不会展开 CSS 变量(lint:font_family_without_font_face)。由于卡片会加载自己的 @font-face 声明,因此内部仍可使用 var(--font-family)。
10. 渲染为 MP4
cd "$WORK_DIR"
PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
  --skill=graphic-overlays \
  -o output.mp4 \
  --fps 30

hyperframes render <dir> 会读取 <dir>/index.html 并生成 MP4。 强烈建议在 macOS 上使用 PRODUCER_BROWSER_GPU_MODE=hardware(或 --browser-gpu)标志——仅使用软件渲染的 Chrome 会在大多数笔记本电脑上超时。

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

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

告知用户:

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

可选实时预览(仅按需提供)。 片段会在 public/index.html 中原样播放,并在上方叠加图层,因此预览效果忠实还原成片。运行期间不要打开该页面。 用户提出要求时,在渲染完成后启动长时间运行的服务,并告知 URL:

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

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

评论

支持 Markdown