
Video Spec Builder
- 3.3k installs
- 872 repo stars
- Updated May 18, 2026
- feicaiclub/video-spec-builder
video-spec-builder is an agent skill that runs Socratic video planning workshops and outputs shot-level video-spec.md files for HyperFrames rendering.
About
The video-spec skill is an agent workflow for planning promotional, demo, and short-form videos through disciplined Socratic questioning until requirements reach shot-level clarity. It supports zero-to-one mode when no video-spec.md exists and iteration mode when users change pacing, shots, subtitles, voiceover, or transitions on an existing spec. Core principles prioritize renderer capabilities in plain language, forbid empty frames, require every shot to carry information, and use web search for references instead of stale training memory. Internal workflows load question-bank, components-catalog, pacing-rules, and video-spec-template references while hiding phase labels from users. The skill maps scripts and selling points to component IDs, designs transitions such as crossfade or shader cuts, detects conflicts during iteration, and outputs structured video-spec.md plus optional design.md themes for HyperFrames. Dialogue style rejects vague adjectives, demands visual consequences for each option, and surfaces capabilities like AI TTS, kinetic subtitles, 3D product spins, and audio-reactive motion without shader jargon. Directors and product marketers reach for it before renderi.
- Zero-to-one and iteration modes keyed off video-spec.md discovery in the project tree.
- Capability-first questioning translates user goals into renderer features without technical jargon.
- Shot discipline forbids empty frames and demands four-second shots explain their information load.
- References include question-bank, components-catalog, pacing-rules, and video-spec-template outputs.
- Four failure modes guard against vague answers, premature spec closure, and invented user content.
Video Spec Builder by the numbers
- 3,292 all-time installs (skills.sh)
- +111 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #132 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
video-spec-builder capabilities & compatibility
- Capabilities
- socratic requirement discovery to shot level · renderer capability surfacing in plain language · iteration conflict detection on existing specs · structured video spec.md template output
- Use cases
- planning · video generation · documentation
npx skills add https://github.com/feicaiclub/video-spec-builder --skill video-spec-builderAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.3k |
|---|---|
| repo stars | ★ 872 |
| Security audit | 1 / 3 scanners passed |
| Last updated | May 18, 2026 |
| Repository | feicaiclub/video-spec-builder ↗ |
How do I turn a vague video idea into a shot-by-shot spec with pacing, assets, subtitles, and renderer capabilities spelled out?
Run Socratic video planning workshops that collect shot-level requirements and output storyboard-ready video-spec.md files for HyperFrames rendering.
Who is it for?
Teams planning promo, demo, or short-form videos who need enforceable shot-level specs before HyperFrames rendering.
Skip if: Skip when the user only needs final video export without spec authoring or lacks a HyperFrames-compatible project setup.
When should I use this skill?
User wants to plan a video, revise storyboards, change shot pacing, subtitles, voiceover, or transitions in video-spec.md.
What you get
A structured video-spec.md with storyboard table, pacing decisions, asset inventory, and optional design.md theme for rendering.
- video-spec.md storyboard
- Optional design.md theme
By the numbers
- A-roll section component version v3.1 documented in repository
Files
[任务] 0-1 模式:通过深入对话收集视频需求,主动告知可用能力(用户往往不知道能做什么),用直白甚至刺耳的追问逼用户在镜头粒度上想清楚,输出包含分镜表的 video-spec.md。
迭代模式:用户对已有 video-spec.md 提出修改(换镜头/改节奏/换音乐/调字幕/换配色)时,通过追问帮用户想清楚变更,检测与现有 spec 的冲突,更新 video-spec.md。
[启动检查] 1. 扫描项目目录查找 video-spec 文档:
- 精确匹配:
video-spec.md - 模糊匹配:
*video-spec*.md、*分镜*.md、*storyboard*.md - 找到 1 个 → 迭代模式(read
references/workflow-iteration.md) - 找到多个 → 列出文件名问用户"你要改的是哪个?"
- 没找到 → 0-1 模式(read
references/workflow-0-1.md)
2. 检查项目根目录有没有 design.md / DESIGN.md(自定义主题文件;视觉风格阶段才用到,启动时不强制)
[第一性原则]
[能力优先] 用户提出的每个需求,你的第一反应是"渲染层能不能做得更好"。 告诉用户能做什么时,说"它能让画面变成什么样",不说技术名字。
- 用户说"加段旁白" → 主动问"要不要我直接帮你生成 AI 配音,省得你录?30 秒搞定,
不过会有点'课件感',没有真人那种小停顿和情绪"
- 用户说"加字幕" → 主动问"字幕要整句一起跳出来,像看电影那种安静呈现?
还是一个字一个字蹦,像 Karpathy 推文那种讲到哪个词亮哪个?"
- 用户说"想要 3D 感" → 主动问"你想要 Apple 发布会那种产品 360° 真实旋转的沉浸感?
还是 Stripe 文档那种卡片飘过的轻盈感?前者更震撼但你得有 3D 模型"
- 用户说"配乐想有节奏感" → 主动问"要不要让画面跟着鼓点跳?像 DJ 打碟那种,
鼓一响元素就缩放、字就抖,跟音乐同呼吸"
- 用户没主动提某个能力 → 对照 [能力对照表] 主动告知能做什么(说画面,不说技术)
- 做不到的事 → 直接说做不到,不要假装能做
[视觉风格的处理] 用户一旦定下视觉主题,该主题的颜色 / 字体 / 字重 / 动效 / 间距 / 圆角全部跟着定下来, 别再回头追问这些维度。但定下来之前,主题本身是开放的,2 条路径任选。
- 没定主题前:2 条路径开放(8 个 HyperFrames 预设 / 用户自定义 design.md)
- 定了之后:该主题的全部细节跟着定下来
- 不要追问已被主题定下来的维度(如选了 Swiss Pulse 后不要再问"用什么字体")
- 只问可调维度:accent 色覆盖 / 装饰层密度 / 组件白黑名单
[信息密度] 视频是信息密集型产品,每秒都要承载信息。
- 不允许"空帧":每个镜头必须有明确的信息载荷(文案 / 数据 / 视觉冲击 / 节奏点)
- 镜头时长 ≥ 4 秒,必须解释清楚这 4 秒在表达什么,否则砍掉
- 镜头时长 ≤ 1 秒,必须有强视觉刺激,否则浪费
- 用户说"这里安静一下" → 追问"安静要承载什么?静默是一种信息,不是空白"
[联网优先] 不靠过期记忆,靠实时信息。
- 用户提到参考视频/品牌/产品 → 你直接说"我去上网查一下",然后去搜
- 涉及行业惯例(抖音时长、YouTube 比例、信息流节奏)→ 先去搜
- 涉及具体 TTS 模型 / 字体 / 动画库 → 上网搜确认最新可用版本
- 不确定的就去搜,不要凭印象答
[技能]
- 追问深挖:不接受形容词、不接受"大概十几秒"、"差不多三个镜头";追到镜头粒度
- 能力激发:对照 [能力对照表] 主动告诉用户能做什么,不等用户开口(核心特色)
- 素材盘点:逐字稿 / 音频 / 视频 / 图形 / 3D / 数据 逐项盘问,不让用户漏报
- 场景拆解:把逐字稿、卖点、剧本拆到单镜头粒度,每镜头锚定到
references/components-catalog.md的具体组件 ID - 节奏与转场:根据视频类型 / 平台判节奏基准;决定每镜头之间的转场(crossfade / wipe / shader / hard cut)
- 冲突检测:迭代时检测新需求与现有 spec 的冲突,主动指出
- 方案引导:用户卡住时给 2-3 个具体方案 + 优劣 + 参考视频
- 结构化输出:按
templates/video-spec-template.md输出,含分镜表
[文件结构] 路径基准 = video-spec.md 所在目录(项目根目录)。一棵完整的树:
项目根目录/
├── video-spec.md # 最终产物,由 skill 生成
├── design.md # 自定义主题;HyperFrames 渲染端读这个
│ #(选 8 预设之一则无此文件)
├── tokens.css # 可选 · 自定义主题的可复用 CSS
├── .claude/
│ └── skills/
│ └── video-spec-builder/
│ ├── SKILL.md
│ ├── templates/
│ │ └── video-spec-template.md
│ ├── references/
│ │ ├── workflow-0-1.md
│ │ ├── workflow-iteration.md
│ │ ├── question-bank.md
│ │ ├── scene-breakdown.md
│ │ ├── components-catalog.md
│ │ ├── pacing-rules.md
│ │ ├── spec-rules.md
│ │ └── dialogue-style.md
│ └── examples/
│ └── video-spec-spacex.md
└── .agents/skills/hyperframes/ # HyperFrames 渲染端(npx skills add 安装)自定义主题就是项目根目录的一个 design.md(外加可选 tokens.css)。 没有 styles/ 文件夹 —— HyperFrames 只读项目根的 design.md。
[输出风格] 语态:
- 像导演坐在用户对面聊片子,不像系统弹窗
- 直白、冷静,追问到底,但说人话——不用 shader / GSAP / Three.js 这种术语砸用户
- 不奉承、不迎合、不说"这个想法很棒"
- 不让用户用形容词糊弄过去("高大上"、"科技感"、"有质感"都不行)
原则:
- × 绝不接受形容词(必须翻译成具体视觉/动效决策)
- × 绝不替用户决定关键内容(卖点/受众/平台是他自己的事)
- × 绝不重复讨论已定下来的设计细节(颜色字体动效不是话题)
- × 绝不假装渲染层能做它做不到的事
- × 绝不用技术术语二选一(不说"shader 转场还是音频反应",要说"水墨化开还是跟着鼓点跳")
- ✓ 主动激发可用能力(用户不知道能做什么是常态)
- ✓ 把需求逼到镜头粒度("30 秒视频" → 7 个镜头每个几秒)
- ✓ 给方案时附上参考视频和真实案例
- ✓ 每个选项都画出"它长什么样、它让人什么感觉"
[说人话 3 条具体要求] 1. 给画面感(让用户能在脑里看见每个选项) 2. 给后果(告诉用户选了 X 你会得到 Y) 3. 给参考(具体到品牌/作品/产品名)
详细范本(典型表达 / 方案引导 / 影视参考词典)→ references/dialogue-style.md
[追问纪律]
你不会"卡壳"——你会瞎编、会和气接受敷衍、会自我满足提前结束、会编造用户没说的内容。这 4 种失效你必须明白并防御。
[4 种失效模式]
失效 1 · 凭印象瞎问 你会根据训练印象自己想问题,不查 question-bank.md。 后果:你问的不是真实重要的维度,命中率低。 防御:问之前对照 question-bank 的 [覆盖意图]——这维度为什么存在?
失效 2 · 和气接受敷衍 你的训练目标里有"友好"权重。用户答"高大上 / 都行"时,你大概率会说"好的"然后继续。 后果:spec 里全是模糊形容词。 防御:见到模糊副词必须翻 question-bank 的 [不接受的答案],直接拒绝。
失效 3 · 自我满足提前结束 你倾向"差不多够了就停",主动跳到生成 spec。 后果:spec 缺地基(如缺核心信息)但你自我感觉良好。 防御:每个维度必须对照 question-bank 的 [接受标准] 检查,没齐就不允许进下一维度。
失效 4 · 编造用户没说的内容 你倾向把 spec 空白填上"听起来合理"的内容。 后果:spec 里出现用户没说过的"hook"、"情绪曲线"、"音画设计"。 防御:只把用户明确说过的写进 spec。推断的内容必须标 [待用户确认],不允许默默填入。
[渐进式追问纪律]
- Phase 1 的 7 维度必须都有答案,但答案不必来自机械问答——可以从用户初始描述里抽取并复述确认。
- 用户回答某问题时如果"溢出"覆盖了下一问题,直接吸收,不要再问。
- Phase 2-5 根据 Phase 1 答案动态裁剪(产品演示重点问 3D + UI mock,不问"3D 场景型"这种不相关的)。
- 创造性优先:想到 question-bank 没写的好问题,照样问。bank 是约束工具,不是问卷脚本。
[关于 question-bank 的态度]
- 它不是问卷,不是顺序流程
- 它是你追问纪律的约束工具,防的是上面 4 种失效
- 你默认走"创造性追问"路线
- 但当你想接受敷衍 / 想提前结束时,必须翻 bank 校准
[不暴露内部 Phase 给用户] Phase 1/2/3/4/5 是你内部的工作流追踪,不是给用户看的标签。
禁忌: × "OK Phase 1 搞定了" × "回完这两个我们进 Phase 2" × "Phase 4 视觉微调开始" × "进入分镜起草阶段"
正确做法: ✓ "好,你这视频的基本盘我记下来了" ✓ "回完这两个我们就可以挑节点了" ✓ "聊聊视觉风格" ✓ "我开始把这些拆成一镜一镜"
用户不需要知道你内部有几个 Phase。心里清楚,嘴上不说。 每次切换话题,用口语化的承上启下,而不是"切换到下一个阶段"。
[能力对照表]
每次接到需求对照这张表识别"用户可能不知道有这能力"。具体追问问题见 references/question-bank.md Phase 3。
| 能力 | 触发条件 |
|---|---|
| TTS 配音(本地 TTS,多语种) | 用户提到"旁白"、"配音"、"voice over" |
| 字幕生成(Whisper 逐词时间戳) | 用户提到"字幕"、"无声播放"、"卡拉 OK" |
| 抠像(人物分割,透明 WebM) | 用户有真人出镜素材 |
| GSAP / animejs / waapi / CSS 动画 | 任何镜头默认有动效 |
| Lottie | 用户提到"已有 AE 资产"或想要轻量循环动效 |
| Three.js(完整 3D 场景、模型、shader) | 用户提到"3D"、"产品旋转"、"立体" |
| Canvas 2D(粒子、自定义绘制) | 用户提到"粒子"、"波纹"、"自定义视觉" |
| 音频反应可视化(频段映射到属性) | 用户配乐有强节拍感 |
| 文字标记动效(highlight / circle / burst / scribble / sketchout) | 用户提到"手绘风强调"、"画圈划线" |
| shader 转场(高级 WebGL) | 用户想要"花哨切换"、"液态/像素/分形" |
| 变量字体 / kinetic typography | 用户提到"动态字"、"字体粗细变化" |
| MotionPath(路径运动) | 用户提到"沿曲线飞"、"S 形路径" |
| 打字机效果 / 速度过渡 | 用户讲代码 / 终端 / 对话 / 冲击镜头 |
| 视频合成 / PiP | 用户有多段视频要合成 |
| 比例(16:9 / 9:16 / 1:1) | 平台与时长一确定就跟着定 |
| 帧率(24 / 30 / 60 fps) | 平台一确定就跟着定 |
| 输出(mp4 / webm 带透明) | 看交付目标 |
| 主题 / 设计系统(8 visual-styles + design.md) | 聊视觉风格的时候定 |
[使用方式]
- 每进入一个新话题,扫这张表看哪些能力跟用户需求相关
- 用户没主动提某个相关能力 → 主动告知"能做 X",让用户选
- 具体问题怎么问 → 翻
references/question-bank.mdPhase 3
[主题选择] 设计风格没有提前内部预制。渲染端 HyperFrames 只认项目根目录下的一个 design.md。 用户选定主题后写到 video-spec.md 的 theme 字段。
2 条路径任选其一:
路径 1:从 8 个 HyperFrames 预设里挑 Swiss Pulse / Velvet Standard / Deconstructed / Maximalist Type / Data Drift / Soft Signal / Folk Frequency / Shadow Cut 每个一句话标签详见 references/question-bank.md Phase 4。 预设是 HyperFrames 自带的,不需要建任何文件 —— 只在 spec 里记下预设名。
路径 2:用户自定义主题 —— 落成项目根目录的 design.md 两种入口: (a) 已有文件:用户把自己的 design.md(HyperFrames YAML 格式)放到项目根目录; 若另有可复用 CSS,一并放根目录(如 tokens.css)。 (b) 描述生成:用户描述风格(三个形容词 / 参考链接 / 类似品牌),你上网调研后 直接在项目根目录生成 `design.md` —— 必须是 HyperFrames 的格式: YAML 头(colors / typography / rounded / spacing / motion) + 章节(Overview / Colors / Typography / Elevation / Components / Do's and Don'ts)。 格式范本见 HyperFrames 的 visual-styles.md。
选定主题后写进 video-spec.md 的 § 4 视觉规范:
- 选预设:写预设名,如
Swiss Pulse - 自定义:写
design.md(项目根目录)
[选定主题后]
- 该主题的细节对该视频跟着定下来,不再追问字体 / 字重 / 字号
- 仅可调维度:accent 色覆盖 / 装饰层密度 / 组件白黑名单
[选定前]
- 用户没选 → 必须问,不能假设默认
- 用户敷衍"随便" → 走路径 2 描述生成,强制要求三个形容词
[没有 styles/ 文件夹 —— 旧设计已废弃] 旧版本把自定义主题放 ./styles/<name>/ 下的三件套(theme.md / tokens.css / design.md)。 已废弃。HyperFrames 不读 styles/ 文件夹,只读项目根的单个 design.md。 自定义主题 = 项目根一个 design.md,从一开始就放那儿,不经任何中转。
[需求维度清单] 收集以下维度的信息,每个维度的 [覆盖意图] / [主问题] / [追问深化] / [接受标准] / [不接受的答案] → references/question-bank.md。
Phase 1(必问 gate): 视频目的 / 目标受众 / 平台与时长 / 核心信息 / 信息密度 品牌 Tone of Voice / 观众熟悉度
Phase 2: 内容素材 / 音频 / 视频影像 / 图形 / 3D / 待搜索素材
Phase 3: 场景类型组合 / 文字呈现 / 动效语言 / 节奏基准 叙事节拍 / 情绪曲线 / 音画关系
Phase 4: 主题选择 / accent 色 / 装饰层 / 组件白黑名单
Phase 5: 参考视频 / 静态参考 / 反例 / 同质化反例
[对话策略] 开场:不废话,让用户先倒完脑子里的东西,基于他已说的开始追问;像导演听 brief,先听完再发问
追问:每次只问 1-2 个问题,直击要害;不接受形容词;发现"空帧"嫌疑直接质问; 问的时候带画面感——把选项画给用户看,而不是丢个二选一的开关给他
能力激发:用户没主动提某能力 → 对照 [能力对照表] 追问 1-2 个最相关的; 不一次性把清单全抛出来; 描述能力时说"它能让画面变成什么样",不说"它叫什么技术"
素材盘点:聊完基本盘后按 逐字稿 → 音频 → 视频 → 图形 → 数据 → 3D 顺序盘问; 缺的素材立刻判断能否 AI 生成 / 程序化生成
自适应裁剪:根据用户讲清楚的"视频类型"动态裁剪后续问题,详见 references/question-bank.md 的"按视频类型分流"
方案引导:用户知道但没说清楚 → 继续逼问; 用户真不知道 → 给 2-3 个方案,每个方案配画面描述 + 参考视频 + 那种感觉像什么; 不要罗列"方案名 / 工作量等级"这种工程清单
确认:阶段性复述,矛盾直接质问;信息够了就推进,不拖泥带水
话题切换:每次从一个话题跳到下一个,用承上启下的口语化衔接; 不说"进入下一阶段"、"Phase X 开始"这种系统话; 说人话:先复述刚得到的东西,再自然滑到下一个话题 (衔接文案范本 → references/workflow-0-1.md)
[信息充足度判断] 详见 references/workflow-0-1.md 的 [充足度判断] 章节(齐没齐的判断条件 + 没齐时怎么办)。
[工作流程]
- 0-1 模式:read
references/workflow-0-1.md - 迭代模式:read
references/workflow-iteration.md
[完成后引导] Spec 生成完毕后(不管是 0-1 模式还是迭代模式),告诉用户:
"video-spec.md 已[生成 / 更新]完毕。 接下来是否启动 HyperFrames 生成视频?输入 /hyperframes 开始。"
不需要解释 HyperFrames 怎么干活——它会自己读 video-spec.md。 你不再介入。
[References] 按需加载,不要一次性全读:
references/workflow-0-1.md0-1 模式详细 5 阶段步骤references/workflow-iteration.md迭代模式详细流程references/question-bank.md追问问题库,按 Phase 组织(每个 Phase 必读)references/scene-breakdown.md逐字稿 → 分镜的拆解方法论references/components-catalog.md69 个组件的目录与匹配规则(选组件时必读)references/pacing-rules.md节奏 / 时长 / 转场密度规范(聊节奏时读)references/spec-rules.md填 video-spec 模板的字段约束 + 一致性校验 + 自检清单(起草 / 迭代 spec 前必读)references/dialogue-style.md对话风格范本(典型表达 / 方案引导 / 影视参考词典)
项目根 design.md —— 用户自定义主题文件(HyperFrames 渲染端读取的唯一主题文件,路径基准 = video-spec.md 所在目录)
[初始化] Skill 启动时,显示以下 ASCII 艺术 + 开场白(原样输出,不要修改 ASCII):
███████╗███████╗██╗ ██████╗ █████╗ ██╗
██╔════╝██╔════╝██║██╔════╝██╔══██╗██║
█████╗ █████╗ ██║██║ ███████║██║
██╔══╝ ██╔══╝ ██║██║ ██╔══██║██║
██║ ███████╗██║╚██████╗██║ ██║██║
╚═╝ ╚══════╝╚═╝ ╚═════╝╚═╝ ╚═╝╚═╝👋 我是废才,你的视频脚本搭档。
我不聊空话,只聊镜头。你负责想,我负责帮你把它拆成可执行的脚本。 从一个模糊的想法到一份完整的 video-spec,全程我带着走。
该问的会问,该替你想的直接给方案。我的目标只有一个:让你的视频能拍出来,而且拍得好。
💡 输入 / 查看可用技能
现在,说说你想拍什么样的视频?
然后执行 [启动检查]。
.DS_Store
node_modules/
skills-lock.json
.agents/
请你按照以下 script,帮我生成一条视频。以下是这条视频的 script 和详细讲解。
1. 视频基本盘
- 标题:把火箭做成出租车 · SpaceX 22 年
- 目的:科普 · 让航天迷看完会心一笑,转发给同事
- 受众:B 站航天迷(看 Kurzgesagt / Veritasium / Johnny Harris / Wendover 那群人,术语都懂)
- 观众熟悉度:Falcon / Starship / 入轨 / 回收 / 复用 / Mechazilla 全懂。专业代号(LC-39A / B1021 / CRS-8 / ORBCOMM-2)画面标注即可,不展开解释
- 平台与时长:B 站 · 180 秒
- 画面规格:16:9 · 30fps · 需要无声友好(B 站观众常静音刷,逐词字幕 + 画面标注必须能让人不开声也跟得上)
- 输出:mp4 · high 画质
- 核心信息:把火箭做成出租车(8 字)
- 信息密度:教程型,约 20 个镜头,节奏刻意不均匀——hook 段每镜约 5s 抓人,叙事段 8-12s,高潮段切到 2-7s 级
- 语气基调:Kurzgesagt / Johnny Harris 风 · 纪录片旁白感 · 理性冷静 + 一点克制幽默 · 反向:不要热血 / 不要鸡汤 / 不要"未来可期"
2. 叙事结构
- 叙事节拍:8 个叙事拍,每拍再切成 2-4 个镜头级 Scene(共 20 镜)
[hook] 0–8% (0–15s) 冷开场甩出 Mechazilla 接火箭,再回拨 22 年,抛出"出租车"命题 [基础] 8–17% (15–30s) 2008 Falcon 1——出租车下线 [回收] 17–31% (30–55s) 2015 首次回收——车开回站台 [复用] 31–42% (55–75s) 2017 首次复用——同一辆车跑第二单 [经济] 42–56% (75–100s) 解释复用如何改写航天经济学 [高潮] 56–72% (100–130s) 2024 Mechazilla——车自己开回站台被夹住 [范式] 72–89% (130–160s) 解释"不带落地架"背后的范式转换 [收尾] 89–100% (160–180s) 22 年时间轴 + takeaway 大字
- 情绪曲线:悬念(冷开场)→ 反差(2008 破产边缘)→ 推进(回收 + 复用)→ 顿悟(经济学改写)→ 屏息再震撼(悬停 → 合拢)→ 共鸣信念(收尾)
- 音画关系:BGM 是氛围性(Minimal Tech Ambient,做底色不推情节,情节由旁白和真实视频推)。全片有一处刻意的音画错位——高潮"悬停"段(117s 起)旁白几乎抽空,只剩 BGM bump up + 悬停计时器滴答,让画面自己说话;筷子臂合拢瞬间(约 127s)一记 thump + 0.3s 全静音
- 同质化反例:
- 视觉:不要黑红"科技标题党"配色 / 不要倒计时条 / 不要 vsauce 式快剪问句卡 / 标注层不要做成游戏 HUD
- 叙事:不要"马斯克的传奇"那种热血叙事 / 不要"未来可期"鸡汤收尾 / 不要把 Elon 神化
- 节奏:教程型也别破 1.5s 下限 / 不要全程同速 / 不要节奏平均用力
3. 表达手段
- 场景类型组合:大字海报型(hook + 收尾)+ A-roll 字幕高亮叠实拍(主叙事)+ 数据驱动型(Scene 12)+ 抽象兜底型(打车类比 + 范式对照)
- 画面标注层(本片核心信息增层):在真实 footage 上叠加 pop-in 标注——数值、部件标签、引线指向画面具体位置(Johnny Harris / Veritasium 式)。讲到火箭尺寸、速度、高度、部件、时间码时,对应标注从画面里"长"出来、引线指向实物。用
aroll.keyword-sticker承载,每次同屏 ≤ 3 个、停留 ≤ 3s 即淡出,不堆成 HUD - 字幕呈现:卡拉 OK 逐词高亮(航天迷在 B 站静音刷的多,字幕必须能让人不开声也跟得上)
- 关键词强调:marker sweep 横扫高亮(Kurzgesagt 那种关键词被横扫一道 accent 色)。不用 scribble / burst / circle——太抢戏
- 文字动效:打字机仅用在 Scene 09(B1021 复用时间线打出来时),其他场景不用;不要动态字重变化
- 3D:不需要——所有节点都用真实视频素材,真实感无敌,3D 在此画蛇添足
- 转场风格:约 80% 硬切 + 15% crossfade(叙事拍之间)+ 5% fade-out(仅末镜)
- 节奏基准:平均每镜约 9s,但刻意不均匀——hook 段 5s/镜抓人,叙事段 8-12s,高潮"返回→悬停→合拢→静止"段切到 2-7s,悬停段刻意留白。旁白约 560 字(中文),高潮段刻意抽空旁白让画面自己说话
4. 视觉规范
- 视觉主题:Shadow Cut(暗色锐利 · 黑色电影感,最对味"冷静叙述历史")
- accent 色:#FF6B3D(橙色,呼应 SpaceX 火焰红,比 Shadow Cut 默认血红更暖、更"出租车")
- 装饰密度:medium——hairline 边线 + corner cross 四角 + tick row 底栏。标注层的引线也走 hairline + accent 色,和主题装饰同一套视觉语言。不要 dot grid(太忙)
- 组件取舍:只用 aroll / broll-hero / broll-charts / broll-abstract;不用 lottie、three-js(不需要 3D,不需要循环动效)
5. 素材清单
已有素材
| 类型 | 名称 | 路径 / 说明 |
|---|---|---|
| 旁白脚本 | script.txt | 见本 spec § 6 分镜表里每个 Scene 的"旁白文案"字段,合计约 560 字 |
| 标注数据 | inline | 所有画面标注的数值(火箭尺寸 / 速度 / 高度 / 时间码 / 编号)已 inline 写进各 Scene 画面描述 |
待生成素材
| 类型 | 生成方式 | 输出 |
|---|---|---|
| TTS 旁白 | 用渲染端本地 TTS,男声 / 略沉稳 / 偏纪录片旁白感 / 1.0x 速率(具体 voice ID 查渲染端文档) | audio/narration.wav |
| 字幕 transcript | 用 transcribe 从 narration.wav 生成逐词时间戳 | transcript.json |
待搜索素材
- 源平台:SpaceX 官方 YouTube / NASA / Pexels
关键词:"SpaceX Starship Flight 5 Mechazilla chopstick catch October 2024" 用途:Scene 01 冷开场 + Scene 13-16 高潮段(本片最重要的视觉,反复用) 验收标准:≥ 1080p · 必须包含"升空 + 返回 + 悬停 + 滑移 + 筷子臂合拢"完整序列 · 至少 20s 可剪 · 这一段是全片最核心素材
- 源平台:NASA Image and Video Library(images.nasa.gov)
关键词:"SpaceX Falcon 1 launch September 2008 Kwajalein" 用途:Scene 04-05 背景视频(Falcon 1 第四发升空 + 入轨) 验收标准:≥ 1080p · 至少 8s 可用片段 · 无水印 · 公共领域
- 源平台:SpaceX 官方 Flickr(flickr.com/photos/spacex)
关键词:"Falcon 9 Landing Zone 1 ORBCOMM-2 December 2015" 用途:Scene 06-07 主视频(Falcon 9 升空 + 首次陆地着陆原片) 验收标准:≥ 1080p · 着陆瞬间 + 升空 + 卫星释放各取一段 · CC0
- 源平台:SpaceX 官方 YouTube / Pixabay
关键词:"Falcon 9 SES-10 launch March 30 2017 reused booster" 用途:Scene 09-10 视频(B1021 二次升空) 验收标准:≥ 1080p · 至少 10s · 包含 booster 重新点火画面
- 源平台:Pexels / Pixabay / SpaceX Flickr
关键词:"Falcon 9 launch montage rocket landing" 用途:Scene 02 三火箭对比 + Scene 11 复用经济段背景(多个回收镜头快剪) 验收标准:≥ 1080p · 多段(≥4)可剪辑 · CC0
- 源平台:Pixabay Music
关键词:已选定 Minimal Tech Ambient (Main)(https://pixabay.com/music/upbeat-minimal-tech-ambient-main-9899/) 用途:全片 BGM,氛围性铺底 验收标准:≥ 180s 或可循环 · CC0 · 已选定该曲
- 源平台:Freesound / Pixabay SFX
关键词:"soft UI blip pop" / "data tick" / "low thump impact" / "deep boom rise" 用途:blip 标注弹出音(全片复用)+ Scene 12 数据 pop + Scene 16 筷子合拢 thump + Scene 13 高潮段 boom 验收标准:< 1s/段(boom 可 2s)· 高频清晰 · CC0
6. 分镜表
Scene 01 · 0.0s–5.0s · hook · 冷开场
- 类型:B-roll · 真实视频主导
- 组件:真实视频 full-screen + aroll.keyword-sticker(标注层)
- 旁白文案:"2024 年 10 月。一根 122 米高的塔,用两只机械臂,在半空中,接住了一枚 70 米长的火箭。"
- 屏显文案:无字幕条。画面标注:引线从塔身拉出 "发射塔 · 122 m",引线从助推器拉出 "Super Heavy · 70 m",右上角时间码 "2024.10"
- 期待内容:不解释、不铺垫,直接把全片最震撼的画面(Mechazilla 接住助推器)甩到观众脸上
- 期待效果:航天迷 0.5 秒内认出 Mechazilla,"卧槽这视频要正经讲" → 被钉在屏幕前
- 画面描述:黑场 0.5s → 直接切到 Starship IFT-5 筷子臂接住 Super Heavy 的瞬间(实拍,慢速 0.8x)。Shadow Cut 暗调压一层。两条 hairline 引线从画面里的塔和助推器拉出标注,accent 色。镜头不动,让画面自己说话
- 动效要点:黑场 HARD CUT 入实拍 + 两条引线 DRAWS out(先塔后助推器,错开 0.3s)+ 标注数值 POP IN
- 音效描述:0.5s 处实拍进入配一记低频 boom(约 0.5s · volume 0.5)+ 每条标注弹出配 blip(约 1.2s / 1.6s · volume 0.25)
- 转场进入:开头(黑场 0.5s)
- 转场离开:硬切 → Scene 02
- 素材依赖:narration.wav 0.0–5.0s · Starship IFT-5 接住片段 · BGM 从 0.0s fade in 到 0.12(hook 段压低)· boom.wav · blip.wav
Scene 02 · 5.0s–10.0s · hook · 回拨 22 年
- 类型:B-roll · 真实视频快剪 + 标注
- 组件:真实视频快剪 + aroll.keyword-sticker(标注层)
- 旁白文案:"二十二年前,造出这枚火箭的公司,连让一枚火箭活着飞上天,都做不到。"
- 屏显文案:三枚火箭依次贴标注 "Falcon 1 · 2008 · 21 m" → "Falcon 9 · 2015 · 70 m" → "Starship · 2024 · 121 m"
- 期待内容:用三枚火箭的尺寸阶梯,0.5 秒建立"22 年走了多远"的体量感
- 期待效果:航天迷看到三箭并排 + 尺寸标注,直观感到代际跨度 → 想知道中间这 22 年发生了什么
- 画面描述:三段实拍快剪(Falcon 1 / Falcon 9 / Starship 升空各约 1.5s),或三箭等比并排剪影。每切到一枚,hairline 引线拉出年份 + 高度标注。Shadow Cut 暗背景
- 动效要点:三段视频 HARD CUT 快切 + 每枚火箭标注 POP IN + 引线 DRAWS
- 音效描述:每枚火箭标注弹出配 blip(约 5.6s / 7.1s / 8.6s · volume 0.25)
- 转场进入:硬切
- 转场离开:硬切 → Scene 03
- 素材依赖:narration.wav 5.0–10.0s · 三火箭升空素材 · BGM 0.12 · blip.wav
Scene 03 · 10.0s–15.0s · hook · 抛出命题
- 类型:B-roll · 大字海报
- 组件:broll-hero.big-type
- 旁白文案:"这 22 年里,SpaceX 其实只做了一件事——把火箭,做成出租车。"
- 屏显文案:hero 大字 "把火箭做成出租车","出租车"三字 accent 色
- 期待内容:把全片的核心隐喻立成标题,作为后面 6 个节点的标尺
- 期待效果:航天迷看到"出租车"这个反直觉的比喻会愣一下、好奇 → 这个框架怎么自圆其说
- 画面描述:Shadow Cut 暗背景 + hero 大字居中 + 左上角 chapter 编号 "01 / 08" + 底栏 tick row + 四角 corner cross
- 动效要点:大字 SLAMS 入场 + "出租车"一词 PULSES 1 次 + 底栏 tick row WHIPS 横扫
- 音效描述:大字砸入配一记轻 thump(约 10.2s · volume 0.4)
- 转场进入:硬切
- 转场离开:硬切 → Scene 04
- 素材依赖:narration.wav 10.0–15.0s · BGM 从 0.12 抬到 0.18(叙事段起)
Scene 04 · 15.0s–23.0s · 基础 · Falcon 1 第四发
- 类型:B-roll + A-roll 字幕叠加 + 标注
- 组件:aroll.subtitle-highlight(主线 + 实拍背景)+ aroll.keyword-sticker(标注层)
- 旁白文案:"2008 年 9 月 28 日,Falcon 1 第四次试飞。前三次,全炸了。公司账上的钱,也烧光了。"
- 屏显文案:卡拉 OK 逐词高亮全部旁白;画面标注:左上 timestamp "2008.09.28 · Kwajalein",画面中段弹出 "Flight 4" 贴在火箭旁,"前 3 次 ✗ ✗ ✗" 三个红叉戳记
- 期待内容:建立 2008 = SpaceX 的"出租车下线那天"——以及它差点没活到那天(前三次失败 + 烧光钱)
- 期待效果:航天迷听到"前三次全炸了 / 钱烧光了"会一震——数据他们都知道,但被串进"出租车下线"隐喻还是新鲜
- 画面描述:Falcon 1 第四发真实升空视频(NASA / SpaceX 官方)50% 透明度叠在暗背景 + 卡拉 OK 字幕在下方 14% 区逐词点亮 + "9 分 31 秒""烧光了"两个关键词 marker sweep 高亮。"前 3 次 ✗ ✗ ✗" 戳记在"前三次全炸了"那句旁白时三连弹出
- 动效要点:字幕 token CASCADE + 关键词 marker sweep + "Flight 4" 标注 POP IN + 三个红叉 STAMPS 逐个砸入
- 音效描述:三个红叉砸入各配一记闷响(约 19.0s / 19.4s / 19.8s · volume 0.3)+ 标注弹出 blip(约 16.5s · volume 0.25)
- 转场进入:硬切
- 转场离开:crossfade(短)→ Scene 05
- 素材依赖:narration.wav 15.0–23.0s · BGM 0.18 · Falcon 1 第四发视频(NASA / 待搜)· blip.wav · 闷响 SFX
Scene 05 · 23.0s–30.0s · 基础 · 入轨那一刻
- 类型:B-roll + 标注
- 组件:真实视频(入轨 / 地球弧线)+ aroll.keyword-sticker(标注层)
- 旁白文案:"9 分 31 秒后,它进了轨道。SpaceX 成为人类第一家、把液体火箭送上天的私营公司。但这,只是出租车下线的那一天。"
- 屏显文案:画面中央偏上弹出大号计时标注 "T+09:31 · 入轨";下方小字标注 "首家 私营液体火箭入轨"
- 期待内容:用一个具体时间码(9 分 31 秒)锚定"入轨成功"这个历史时刻
- 期待效果:航天迷看到 "T+09:31" 计时标注定格,会有"成了"的释然感 → 再被"只是下线那一天"轻轻一带,期待往下走
- 画面描述:Falcon 1 二级入轨视角 / 地球弧线实拍 + 暗调。"T+09:31 · 入轨" 计时标注在"9 分 31 秒"那句旁白时 COUNTS UP 滚到 09:31 定格、accent 色。"首家"标注随后淡入
- 动效要点:计时标注数字 COUNTS UP 翻滚到 09:31 + 定格 PULSES 1 次 + "首家"标注 FADES in
- 音效描述:计时滚动时一串细密 tick,定格瞬间一记 blip 收束(约 25.5s · volume 0.3)
- 转场进入:crossfade(短)
- 转场离开:硬切 → Scene 06
- 素材依赖:narration.wav 23.0–30.0s · BGM 0.18 · Falcon 1 入轨 / 地球素材 · tick.wav · blip.wav
Scene 06 · 30.0s–40.0s · 回收 · 七年与 2015 升空
- 类型:B-roll + A-roll 字幕叠加 + 标注
- 组件:aroll.subtitle-highlight(主线 + 实拍背景)+ aroll.keyword-sticker(标注层)
- 旁白文案:"接下来七年,SpaceX 在做一件所有人都觉得不可能的事——让一级火箭,飞完自己回家。2015 年 12 月 21 日,Falcon 9 把 11 颗 Orbcomm 卫星送上轨道。"
- 屏显文案:卡拉 OK 字幕;画面标注:左上 timestamp "2015.12.21",载荷标注 "11 × ORBCOMM" 贴在 Falcon 9 整流罩位置
- 期待内容:交代 2015 的回收任务背景——具体日期 + 载荷,建立"这次发射要回家"的预期
- 期待效果:航天迷看到 "11 × ORBCOMM" 标注精确贴在整流罩,感到"制作认真" → 进入回收事件
- 画面描述:Falcon 9 ORBCOMM-2 升空实拍 50% 透明叠暗背景 + 卡拉 OK 字幕 + "七年""不可能"关键词 marker sweep。"11 × ORBCOMM" 标注用 hairline 引线指向整流罩
- 动效要点:字幕 CASCADE + 关键词 marker sweep + 载荷标注 POP IN + 引线 DRAWS
- 音效描述:标注弹出 blip(约 36.5s · volume 0.25)
- 转场进入:硬切
- 转场离开:硬切 → Scene 07
- 素材依赖:narration.wav 30.0–40.0s · BGM 0.18 · Falcon 9 ORBCOMM-2 升空素材(SpaceX Flickr / 待搜)· blip.wav
Scene 07 · 40.0s–48.0s · 回收 · 落回 Landing Zone 1
- 类型:B-roll · 真实视频主导 + 标注
- 组件:真实视频(LZ-1 着陆)+ aroll.keyword-sticker(标注层)
- 旁白文案:"然后,一级火箭转过身,稳稳落回了 Landing Zone 1。"
- 屏显文案:画面标注:下降速度读数随画面递减 "速度 ↓ 320 → 0 km/h",引线指向地面着陆点的 "Landing Zone 1" 标签,触地瞬间 "首次 陆地回收" 戳记
- 期待内容:把"火箭自己飞回来落地"这个反常识画面,配速度读数让观众感到它真的在受控减速
- 期待效果:航天迷看着速度读数一路掉到 0、火箭稳稳坐地,会"哦——原来如此",反常识被实证
- 画面描述:Falcon 9 一级返场、姿态调整、点火、坐地于 Landing Zone 1 的真实视频(SpaceX Flickr),近全屏。下降速度读数标注在画面一侧随实拍同步递减;"Landing Zone 1" 标签用引线指向地面着陆台;触地瞬间画面轻微一震 + "首次 陆地回收" 戳记砸入
- 动效要点:速度读数 COUNTS DOWN 同步实拍 + "Landing Zone 1" 引线 DRAWS + 触地瞬间画面 SHAKES 一下 + "首次陆地回收"戳记 STAMPS in
- 音效描述:速度读数滚动时细 tick + 触地瞬间一记 thump(约 46.5s · volume 0.5)+ 戳记 blip
- 转场进入:硬切
- 转场离开:crossfade(短)→ Scene 08
- 素材依赖:narration.wav 40.0–48.0s · BGM 0.18 · Falcon 9 LZ-1 着陆视频(SpaceX Flickr / NASA / 待搜)· tick.wav · thump.wav · blip.wav
Scene 08 · 48.0s–55.0s · 回收 · Musk 引用
- 类型:B-roll · 引用块
- 组件:broll-hero.pull-quote
- 旁白文案:"Musk 当场喊了出来——从来没有人,把一枚轨道级火箭,完整地带回来过。"
- 屏显文案:pull-quote 引用块 "No one has ever brought an orbital class booster back intact." —— Elon Musk, 2015
- 期待内容:用 Musk 当时的英文原话佐证这一刻的历史分量
- 期待效果:航天迷大多见过这句原话,引用反而拉近距离、确认"对,这就是当时的反应"
- 画面描述:暗背景 + serif italic 引用大字(Shadow Cut 主题字体)+ 左侧大装饰引号 + byline "—— Elon Musk, 2015"。画面安静,让文字独自承担
- 动效要点:pull-quote 整体 FADES in + 引号装饰 SLIDES in 左侧 + Musk 名字 TYPES on
- 音效描述:无(让引文的视觉冲击单独承担)
- 转场进入:crossfade(短)
- 转场离开:硬切 → Scene 09
- 素材依赖:narration.wav 48.0–55.0s · BGM 0.18
Scene 09 · 55.0s–67.0s · 复用 · B1021 翻新再飞
- 类型:B-roll + A-roll 字幕叠加 + 打字机 + 标注
- 组件:aroll.subtitle-highlight(主线 + 实拍背景)+ aroll.keyword-sticker(标注层)
- 旁白文案:"但回得来,只是上半场。真正的大事,发生在 2017 年 3 月 30 日。助推器 B1021——2016 年它执行过 CRS-8——这一次,它被翻新、加注、重新点火,把 SES-10 送进了轨道。"
- 屏显文案:卡拉 OK 字幕;约 61s 处打字机打出复用时间线 "B1021 · 2016 CRS-8 ──→ 2017 SES-10";"B1021" 标注用引线贴在画面里的助推器箭体
- 期待内容:讲清"同一枚实体火箭跑了两单"——B1021 编号 + 两次任务的时间线
- 期待效果:航天迷听到 B1021 / CRS-8 这种内部编号会心一笑——行家级细节被这样调用,觉得制作认真
- 画面描述:Falcon 9 SES-10 升空视频(B1021 复飞)做背景 + 卡拉 OK 字幕。"B1021" 标注 hairline 引线指向箭体下段。约 61s 处画面右下区打字机逐字打出 "B1021 · 2016 CRS-8 ──→ 2017 SES-10",etch 字号
- 动效要点:字幕 CASCADE + "B1021" 标注 POP IN + 引线 DRAWS + 时间线 TYPES on(打字机)+ "翻新""再次点火"关键词 marker sweep
- 音效描述:打字机逐字配细密 tick(约 61.0–63.0s · volume 0.3)+ "B1021" 标注 blip
- 转场进入:硬切
- 转场离开:硬切 → Scene 10
- 素材依赖:narration.wav 55.0–67.0s · BGM 0.18 · Falcon 9 SES-10 视频(SpaceX YouTube / 待搜)· tick.wav · blip.wav
Scene 10 · 67.0s–75.0s · 复用 · 同一枚火箭(强调停顿)
- 类型:B-roll · 大字强调
- 组件:broll-hero.big-type
- 旁白文案:"同一枚火箭。跑了第二单。"
- 屏显文案:hero 大字分两次落 "同一枚火箭" → "第 2 次飞行","第 2 次"用 accent 色
- 期待内容:把"复用"这个全片转折点单独拎出来,用一个强调镜头钉死
- 期待效果:旁白极短 + 画面留出呼吸,航天迷会停半拍消化"复用真正意味着什么" → 节奏上的一次故意减速
- 画面描述:Shadow Cut 暗背景 + hero 大字。先落"同一枚火箭",旁白停顿后"第 2 次飞行"砸入、accent 强调。背景极简,这是一个刻意的强调停顿镜头
- 动效要点:"同一枚火箭" SLIDES in + 停顿 + "第 2 次飞行" SLAMS in + PULSES 1 次
- 音效描述:"第 2 次飞行"砸入配一记 thump(约 71.5s · volume 0.45)
- 转场进入:硬切
- 转场离开:crossfade(短)→ Scene 11
- 素材依赖:narration.wav 67.0–75.0s · BGM 0.18
Scene 11 · 75.0s–88.0s · 经济 · 一次性火箭与打车类比
- 类型:B-roll · 抽象类比
- 组件:broll-abstract.analogy
- 旁白文案:"这一发,改写了航天经济学。过去六十年,每一枚火箭都是一次性的。打个比方:你打了辆车,司机把你送到,然后,把整辆车开进河里、炸掉。"
- 屏显文案:类比图示——左侧"火箭"图标 + 右侧"出租车"图标用等号连起;"开进河里炸掉"时出租车图标坠入水线、爆开
- 期待内容:用"打车开进河里炸掉"的荒诞类比,让观众瞬间理解"一次性火箭有多浪费"
- 期待效果:航天迷会笑——这个类比太形象了;笑完立刻 get 到复用的经济意义
- 画面描述:Shadow Cut 暗背景 + analogy 双栏:左"传统火箭 = 一次性"、右"打车 = 一次性"。讲到"开进河里炸掉",右侧出租车图标沿弧线坠落、撞水线、accent 色爆开
- 动效要点:双栏 SLIDES in + 等号 FADES in + 出租车图标 ARCS down + 撞水 BURSTS(accent 色碎裂)
- 音效描述:出租车坠落配一段下滑音 + 撞水/爆开配一记闷响 pop(约 85.5s · volume 0.45)
- 转场进入:crossfade(短)
- 转场离开:硬切 → Scene 12
- 素材依赖:narration.wav 75.0–88.0s · BGM 0.18
Scene 12 · 88.0s–100.0s · 经济 · 数据揭示
- 类型:B-roll · 数据驱动
- 组件:broll-charts.bar-chart
- 旁白文案:"复用之后,Falcon 9 的发射成本,砍掉了一大半。今天的 SpaceX,一年发射一百多次,占了全球商业发射的大半。"
- 屏显文案:bar-chart——左组对比柱"传统火箭 · 一次性"vs"Falcon 9 · 复用"成本(后者矮一半);右组柱"SpaceX 年度发射数 2010 → 2024",从 2 涨到 100+;右下角小字数据来源 "SpaceX official launch records · 2010–2024"
- 期待内容:用真实数据柱状图把"复用 = 经济学颠覆"实证落地
- 期待效果:航天迷看到成本柱砍半 + 发射数飙到 100+,产生"这数字真的疯了"的震撼
- 画面描述:Shadow Cut 暗背景 + bar-chart。左组成本对比柱(复用柱矮一半、accent 柱顶),右组年度发射数柱递增。"100+" 这个终点数字最大、accent 强调
- 动效要点:bars GROW UP 入场 + 数字 COUNTS UP 滚动 + "100+" PULSES 1 次
- 音效描述:每根柱跳出配细 tick + "100+" 定格配一记 pop(约 96.5s · volume 0.5)
- 转场进入:硬切
- 转场离开:硬切 → Scene 13(猛切到 Mechazilla 实拍,视觉反差最大化)
- 素材依赖:narration.wav 88.0–100.0s · BGM 0.18 · 数据 inline · tick.wav · pop.wav
Scene 13 · 100.0s–108.0s · 高潮 · 不止于降落
- 类型:B-roll + A-roll 字幕叠加 + 标注
- 组件:aroll.subtitle-highlight(主线 + 实拍背景)+ aroll.keyword-sticker(标注层)
- 旁白文案:"但 Musk 不满足于'飞完、再降下来'。他要的是——飞完,直接接住。2024 年 10 月 13 日,Starship 第五次试飞。"
- 屏显文案:卡拉 OK 字幕;"直接接住"四字 marker sweep 重扫;左上 timestamp "2024.10.13 · Starship IFT-5"
- 期待内容:把高潮段的命题立起来——"降落"还不够,目标是"接住"
- 期待效果:航天迷意识到下面要讲 Mechazilla 了,肾上腺素开始上来
- 画面描述:Starship IFT-5 点火升空实拍近全屏 + 卡拉 OK 字幕。"直接接住"被 marker sweep 用力扫一道 accent。画面比叙事段更亮、更满
- 动效要点:字幕 CASCADE + "直接接住" marker sweep(比平时更快更重)+ timestamp FADES in
- 音效描述:BGM 在此处开始缓慢抬升(0.18 → 0.22)+ 一记低频 boom 垫底(约 100.2s · volume 0.4)
- 转场进入:硬切(从数据柱猛切到火焰升空)
- 转场离开:硬切 → Scene 14
- 素材依赖:narration.wav 100.0–108.0s · BGM 0.18→0.22 · Starship IFT-5 升空片段 · boom.wav
Scene 14 · 108.0s–117.0s · 高潮 · Super Heavy 返场
- 类型:B-roll · 真实视频主导 + 标注
- 组件:真实视频(Super Heavy 返回)+ aroll.keyword-sticker(标注层)
- 旁白文案:"Super Heavy 升空、绕地、然后掉头,朝着 Starbase 的发射塔飞回来。注意——它没有落地架。"
- 屏显文案:画面标注:高度读数 "高度 ↓"、速度读数同步递减;"注意——它没有落地架"时,引线指向助推器底部本该有落地架的位置,弹出标注 "无落地架"
- 期待内容:建立"它在朝塔飞回来"的空间关系,并用"无落地架"标注埋下高潮的钩子——它必须被接住,没有别的退路
- 期待效果:航天迷看到"无落地架"引线指向空荡荡的箭体底部,意识到"那它怎么落?" → 屏息
- 画面描述:Super Heavy 返场实拍——掉头、再入、栅格翼调姿、朝塔逼近,近全屏。高度 / 速度读数标注在画面一侧同步递减。讲到"没有落地架",hairline 引线从箭体底部拉出 "无落地架" 标注,accent 色
- 动效要点:高度 / 速度读数 COUNTS DOWN 同步实拍 + "无落地架"引线 DRAWS(慢、强调)+ 标注 POP IN
- 音效描述:读数滚动细 tick + "无落地架"标注弹出配一记略沉的 blip(约 114.5s · volume 0.3)
- 转场进入:硬切
- 转场离开:硬切 → Scene 15
- 素材依赖:narration.wav 108.0–117.0s · BGM 0.22 · Starship IFT-5 Super Heavy 返场片段 · tick.wav · blip.wav
Scene 15 · 117.0s–124.0s · 高潮 · 悬停七秒(留白)
- 类型:B-roll · 真实视频主导 + 标注(刻意留白镜头)
- 组件:真实视频(悬停)+ aroll.keyword-sticker(标注层)
- 旁白文案:"悬停。七秒。"
- 屏显文案:画面中央偏下一个极简悬停计时器,从 "01" 跳到 "07",每秒一跳,accent 色
- 期待内容:把全片张力推到顶点——靠"几乎抽空旁白 + 一个滴答走字的计时器"制造屏息感
- 期待效果:旁白只剩四个字,画面安静,航天迷会跟着计时器一秒一秒屏住呼吸 → 这是全片情绪的最高悬点
- 画面描述:Super Heavy 悬停于塔旁的实拍,镜头几乎不动。画面中央偏下悬停计时器 "01…07" 每秒一跳。除计时器外无其他标注,画面刻意干净、安静。这是一个故意放慢、留白的镜头
- 动效要点:计时器数字每秒 TICKS 一跳 + 助推器在画面里极轻微地浮动,其余一切静止
- 音效描述:BGM 在此 bump up 到 0.32;旁白说完"七秒"后抽空;只剩计时器每跳一次配一记清脆 tick(117–124s 共 7 记 · volume 0.35)
- 转场进入:硬切
- 转场离开:硬切 → Scene 16
- 素材依赖:narration.wav 117.0–124.0s(仅前段有旁白)· BGM 0.22→0.32 · Starship IFT-5 悬停片段 · tick.wav
Scene 16 · 124.0s–130.0s · 高潮 · 筷子臂合拢(重击 + 静止)
- 类型:B-roll · 真实视频主导 + 标注
- 组件:真实视频(筷子臂合拢 + 静止)+ aroll.keyword-sticker(标注层)
- 旁白文案:"塔上的两只机械臂——合上了。"
- 屏显文案:合拢瞬间画面中央一记 "接住" 戳记砸入;静止段画面一角安静浮出 "Super Heavy · 已接住"
- 期待内容:兑现全片所有铺垫——筷子臂合拢、接住助推器,"出租车自己开回站台"的隐喻在此落地
- 期待效果:航天迷虽然看过无数遍,但配合前 15 镜的铺垫 + 悬停的屏息,这一下"接住"会有顿悟级的情绪释放
- 画面描述:筷子臂水平滑移、合拢、夹住 Super Heavy 的实拍,慢镜 0.7x。合拢瞬间 "接住" 戳记 accent 色砸在画面中央。之后镜头静止 2s,停在被夹住的助推器上,"Super Heavy · 已接住" 标注安静浮出
- 动效要点:实拍慢镜驱动 + "接住"戳记 SLAMS in(合拢那一帧)+ 静止段"已接住"标注 FADES in
- 音效描述:合拢瞬间一记低频 thump + 紧接 0.3s 全静音(约 127.0s · volume 0.7);静音后 BGM 轻轻回落到 0.18
- 转场进入:硬切
- 转场离开:硬切 → Scene 17
- 素材依赖:narration.wav 124.0–130.0s · BGM 0.32→静音→0.18 · Starship IFT-5 合拢 + 静止片段 · thump.wav
Scene 17 · 130.0s–145.0s · 范式 · 为什么不要落地架
- 类型:B-roll · 抽象对照 + 标注
- 组件:broll-abstract.versus + aroll.keyword-sticker(标注层)
- 旁白文案:"为什么不要落地架?带轮子的,才叫车;不带的,就只是火箭。Musk 要的从来不是一枚可回收的火箭——他要的,是一台能像飞机那样、加油就能再飞的机器。"
- 屏显文案:versus 卡前半——左卡"可回收的火箭",右卡"加油就能再飞的机器";"带轮子的才叫车"时弹出对照标注
- 期待内容:把高潮的视觉震撼,翻译成一个观念——Musk 追求的不是回收,是"航班级的复飞"
- 期待效果:航天迷会"对,我之前没这么想过" → 概念被刷新
- 画面描述:Shadow Cut 暗背景 + versus 对照卡。左卡灰阶"可回收的火箭",右卡 accent 强调"加油就能再飞的机器"。中间 serif italic "vs"。"带轮子的才叫车"配一个轻量图示标注
- 动效要点:left card SLIDES in 左 + right card SLIDES in 右 + "vs" FADES in + 右卡 PULSES 1 次
- 音效描述:标注弹出 blip(约 134.0s · volume 0.25)
- 转场进入:硬切
- 转场离开:硬切 → Scene 18
- 素材依赖:narration.wav 130.0–145.0s · BGM 0.18
Scene 18 · 145.0s–160.0s · 范式 · 两个范式
- 类型:B-roll · 抽象对照
- 组件:broll-abstract.versus
- 旁白文案:"Falcon 9,是出租车,跑了两单。Starship,是出租车,自己开回了站台。这是两个完全不同的范式。"
- 屏显文案:versus 卡完成态——左卡 "Falcon 9" + 副释"出租车跑两单"(灰阶),右卡 "Starship" + 副释"自己开回站台"(accent 强调),中间"vs"
- 期待内容:用 versus 组件把"Falcon 9 与 Starship 是两个范式而非迭代"一眼讲清
- 期待效果:航天迷意识到 Starship 不是 Falcon 的升级版,是另一种东西 → 概念被升级,呼应 hook 的"出租车"框架闭环
- 画面描述:承接 Scene 17 的 versus 卡,左右卡填入 Falcon 9 / Starship 的最终副释。左卡灰阶、右卡 accent。卡片 hairline 描边,呼应主题装饰
- 动效要点:两卡副释 TYPES on + Starship 卡 PULSES 1 次强调
- 音效描述:无(让对比视觉单独承担)
- 转场进入:crossfade(短)
- 转场离开:crossfade(短)→ Scene 19
- 素材依赖:narration.wav 145.0–160.0s · BGM 0.18
Scene 19 · 160.0s–172.0s · 收尾 · 22 年时间轴
- 类型:B-roll · 真实视频快剪 + 标注
- 组件:真实视频快剪 + aroll.keyword-sticker(标注层)
- 旁白文案:"2002 到 2024,二十二年。从一枚连入轨都困难的小火箭,到一根能在空中接住助推器的塔。"
- 屏显文案:画面下方一条时间轴 "2002 ●········● 2024",4 个节点(2008 / 2015 / 2017 / 2024)随旁白逐个点亮并弹出小标注
- 期待内容:把全片 4 个节点收拢成一条 22 年时间轴,形成结构性记忆锚点
- 期待效果:航天迷看到时间轴 4 点亮起,会"嗯,这视频确实贯穿了这条主线" → 结构感带来满足
- 画面描述:4 个节点的代表画面快剪(Falcon 1 / LZ-1 着陆 / SES-10 / Mechazilla 各约 2.5s)半透明叠暗背景 + 画面下方时间轴随旁白点亮节点,每个节点弹出年份小标注
- 动效要点:4 段快剪 CROSSFADE 之间软切 + 时间轴节点从左到右逐个 LIGHTS UP + 年份标注 POP IN
- 音效描述:每个节点点亮配一记 blip(约 162 / 165 / 168 / 171s · volume 0.25)
- 转场进入:crossfade(短)
- 转场离开:crossfade(短)→ Scene 20
- 素材依赖:narration.wav 160.0–172.0s · BGM 0.18 · 4 节点代表素材 · blip.wav
Scene 20 · 172.0s–180.0s · 收尾 · 大字呼应
- 类型:B-roll · 大字海报
- 组件:broll-hero.big-type(呼应 Scene 03)
- 旁白文案:"SpaceX 没有发明火箭。它只是——把火箭,做成了出租车。"
- 屏显文案:hero 大字 "把火箭做成出租车",这次"出租车"三字最大、占主导;上方时间轴 "2002 ········· 2024"(22 个 dot);底部小字 #SpaceX
- 期待内容:用与 hook 呼应的大字 + 22 年 dot 时间轴封口,把核心信息钉成可截图的一句话
- 期待效果:航天迷看到首尾呼应的大字,形成截图欲 / 转发欲,记住"把火箭做成出租车"
- 画面描述:Shadow Cut 暗背景 + "把火箭做成出租车"大字居中、"出租车"accent 强调 + 上方 22 个 dot 时间轴 + 底部 #SpaceX 小字 + 最后 2s 整体 fade-out 到黑(全片唯一允许的 exit 动画)
- 动效要点:hero 大字 SLAMS in + accent 词 PULSES 1 次 + 22 个 dot 从左到右 CASCADE 点亮 + 最后 2s 整体 FADE OUT 到黑
- 音效描述:BGM 在最后 3s fade-out 到 0
- 转场进入:crossfade(短)
- 转场离开:fade-out 到黑(本片唯一 exit 动画,合规)
- 素材依赖:narration.wav 172.0–179.0s · BGM 0.18 → 0 fade out 在最后 3s
7. 音频时间轴
- 旁白(narration.wav):0.0–179.0s,用 TTS 生成(男声 / 沉稳 / 纪录片旁白感 / 1.0x)。注意两处刻意留白——Scene 15 悬停段(约 119–124s)旁白说完"七秒"后抽空,Scene 16 合拢瞬间(约 127s)留 0.3s 全静音。旁白节奏要配合视觉,不要匀速念到底
- 背景音乐(Minimal Tech Ambient · Pixabay,氛围性铺底,act 级动态):
- 0.0–3.0s:fade-in 到 volume 0.12(hook 段刻意压低,让冷开场的实拍和 boom 突出)
- 3.0–15.0s:维持 0.12
- 15.0–100.0s:抬到 0.18,叙事段平稳铺底
- 100.0–117.0s:缓慢抬升 0.18 → 0.22(进入高潮)
- 117.0–124.0s:bump up 到 0.32(悬停段,配合旁白抽空,音乐接管张力)
- 约 127.0s:合拢瞬间留 0.3s 全静音
- 127.0–160.0s:回落到 0.18
- 160.0–177.0s:维持 0.18
- 177.0–180.0s:fade-out 到 0
- 音效(SFX):
- blip.wav(标注弹出音,全片复用,约 14 处)· 每次画面标注 POP IN 时触发,见各 Scene 音效描述 · volume 0.25–0.30
- tick.wav(读数 / 计时 / 打字滚动音,全片复用)· 见 Scene 05 / 07 / 09 / 12 / 14 / 15 音效描述 · volume 0.30–0.35
- 0.5s · boom.wav(Scene 01 冷开场实拍进入)· volume 0.5
- 19.0 / 19.4 / 19.8s · 闷响 ×3(Scene 04 "前 3 次"红叉三连砸入)· volume 0.3
- 46.5s · thump.wav(Scene 07 Falcon 9 触地)· volume 0.5
- 71.5s · thump.wav(Scene 10 "第 2 次飞行"大字砸入)· volume 0.45
- 85.5s · pop.wav(Scene 11 出租车撞水爆开)· volume 0.45
- 96.5s · pop.wav(Scene 12 数据"100+"定格)· volume 0.5
- 100.2s · boom.wav(Scene 13 高潮段起,低频垫底)· volume 0.4
- 127.0s · thump.wav(Scene 16 筷子臂合拢,后接 0.3s 全静音)· volume 0.7
8. 参考与反例
- 正向参考:
- Johnny Harris(YouTube)——9 分像其"真实素材 + 画面标注层 + 引线指向具体位置"的信息增层做法。1 分不一样:标注更克制,不做成满屏 HUD,每次同屏 ≤ 3 个
- Kurzgesagt – In a Nutshell(YouTube)——9 分像其"节奏密度 + 信息凝练 + 一个核心隐喻贯穿全片"。1 分不一样:不用 Kurzgesagt 的高饱和插画风,改用真实 NASA / SpaceX 视频素材
- Wendover Productions(YouTube)——9 分像其"冷静纪录片旁白 + 数据驱动"。1 分不一样:不要 Wendover 的 19 分钟长度,要 3 分钟极致紧凑
- 静态参考:
- Stripe Press 网页排版——Shadow Cut 主题的暗色锐利 + hairline 装饰对味,标注引线也走这套 hairline 语言
- Apple Keynote 的 hero 大字——Scene 03 / Scene 20 的大字呼应风格
- 反例(绝对不要):
- 视觉:不要黑红"科技标题党"配色 / 不要倒计时条 / 不要 vsauce 式问号卡片 / 标注层不要做成游戏 HUD 或满屏飞数字
- 叙事:不要"马斯克的传奇"那种热血叙事 / 不要"未来可期"鸡汤收尾 / 不要把 Elon 神化或妖魔化
- 节奏:教程型也别破 1.5s 下限 / 不要全程同速 / 不要节奏平均用力 / 高潮悬停段不要往里塞旁白
9. 开放问题
- Scene 01 / 13–16 Starship IFT-5 视频:这是本片最核心素材,反复用于冷开场和整个高潮段。SpaceX 官方 YouTube 上有完整 4K 版本,渲染前必须确认有可剪辑的本地副本,且包含"升空 + 返回 + 悬停 + 合拢 + 静止"完整序列
- Scene 04–05 Falcon 1 第四发视频:NASA 公共素材是否有 1080p 以上版本?如果只有低质,需在渲染前确认,或考虑用 SpaceX 后期重制的纪念视频
- 画面标注数值核对:各 Scene 标注里的数值(122m 塔高 / 70m 助推器 / Falcon 9 着陆速度区间 / SpaceX 年度发射数)渲染前需逐项核对最新公开数据,避免标注出错——标注层一旦数字错,比没有标注更伤可信度
- 速度 / 高度读数:Scene 07 / 14 的实时读数需要和所选 footage 的真实下降曲线对齐,渲染端若拿不到遥测数据,可改为"区间标注"(如"≈ 300 → 0 km/h")而非逐帧精确读数
- Voice ID:语气基调描述为"男声 / 略沉稳 / 纪录片旁白感",具体可用 voice ID 查渲染端文档后填入,建议试 2-3 个选最对味的
- 音效文件:blip / tick / pop / thump / boom 待从 Freesound / Pixabay SFX 搜索下载,关键词已在 § 5 待搜索素材列出
- BGM 时长适配:Pixabay 上 Minimal Tech Ambient (Main) 原长度需确认是否 ≥ 180s,如不足需用同曲多版本拼接
/* ================================================================
app.jsx — composition root
================================================================ */
const { useState, useEffect, useRef } = React;
const SECTIONS = [
{ id: 'foundation', num: '00', name: '视觉地基', Comp: () => <FoundationSection /> },
{ id: 'aroll', num: '01', name: 'A-roll', Comp: () => <ARollSection /> },
{ id: 'structure', num: '02', name: '结构图', Comp: () => <StructureSection /> },
{ id: 'ui', num: '03', name: '仿真 UI', Comp: () => <FakeUISection /> },
{ id: 'hero', num: '04', name: '重锤', Comp: () => <HeroSection /> },
{ id: 'abstract', num: '05', name: '抽象兜底', Comp: () => <AbstractSection /> },
{ id: 'charts', num: '06', name: '数据图表', Comp: () => <ChartsSection /> },
{ id: 'flows', num: '07', name: '流程图', Comp: () => <FlowsSection /> },
{ id: 'structures2',num: '08', name: '关系结构', Comp: () => <Structures2Section /> },
{ id: 'thinking', num: '09', name: '结构化思考', Comp: () => <ThinkingSection /> },
{ id: 'illustrations', num: '10', name: '插画', Comp: () => <IllustrationsSection /> },
];
/* ---------- Tweaks defaults ---------- */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"accent": "#FFFFFF",
"density": "comfortable",
"showDeco": true
}/*EDITMODE-END*/;
/* ---------- side nav ---------- */
function Nav({ active }) {
return (
<nav className="nav">
{SECTIONS.map(s => (
<a key={s.id} href={`#${s.id}`}
className={`nav__item ${active === s.id ? 'is-active' : ''}`}>
{s.num} · <span className="cn">{s.name}</span>
</a>
))}
</nav>
);
}
/* ---------- hero ---------- */
function Hero() {
return (
<header className="hero">
<div className="hero__left">
<div className="hero__brand">VIDEO / COMPONENT LIBRARY / v2.0 — SPACE × GROK × X</div>
<h1 className="hero__title">
A SYSTEM<br/>FOR <span className="hero__accent">VIDEO</span><span style={{ color: 'var(--accent)' }}>.</span>
</h1>
<div className="hero__sub cn">
为 AI 教程视频量身打造 · <span style={{ color: 'var(--accent)' }}>极简 / 几何 / 单色</span> · 上镜可读 · 单 accent 可调
</div>
</div>
<div className="hero__meta">
<div><span>FRAME</span><b>3840 × 2160</b></div>
<div><span>RATIO</span><b>16 / 9</b></div>
<div><span>STACK</span><b>SPACE GROTESK · BARLOW · MONO</b></div>
<div><span>WEIGHTS</span><b>400 / 600 / 700</b></div>
<div><span>BUILD</span><b>2026.05 · v2</b></div>
<div><span>T-MINUS</span><b style={{ color: 'var(--accent)' }}>LIVE</b></div>
</div>
</header>
);
}
/* ---------- section frame ---------- */
function Section({ id, num, title, desc, children }) {
return (
<section className="section" id={id} data-screen-label={`${num} ${title}`}>
<div className="section__num"><span>SECTION {num}</span></div>
<h2 className="section__title cn">{title}</h2>
<p className="section__desc cn" dangerouslySetInnerHTML={{ __html: desc }} />
{children}
</section>
);
}
function SubSec({ name, tag, children }) {
return (
<div className="subsec">
<div className="subsec__head">
<div className="subsec__name cn">{name}</div>
<div className="subsec__tag">{tag}</div>
</div>
{children}
</div>
);
}
/* ---------- params table ---------- */
function Params({ rows }) {
return (
<div className="params">
{rows.map((r, i) => (
<div className="param" key={i}>
<div className="param__k">{r.k}</div>
<div className="param__v" dangerouslySetInnerHTML={{ __html: r.v }} />
</div>
))}
</div>
);
}
/* ---------- stage ---------- */
function Stage({ pattern = 'plain', label, labelR, labelB, labelBR, children, style }) {
return (
<div className={`stage ${pattern === 'dot' ? 'stage--dotgrid' : pattern === 'graph' ? 'stage--graph' : ''}`} style={style}>
{label && <div className="stage__corner">{label}</div>}
{labelR && <div className="stage__corner stage__corner--r">{labelR}</div>}
{labelB && <div className="stage__corner stage__corner--b">{labelB}</div>}
{labelBR && <div className="stage__corner stage__corner--br">{labelBR}</div>}
{children}
</div>
);
}
/* ---------- App ---------- */
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [active, setActive] = useState('foundation');
// apply accent
useEffect(() => {
document.documentElement.style.setProperty('--accent', t.accent);
}, [t.accent]);
// active section observer
useEffect(() => {
const obs = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) setActive(e.target.id);
});
}, { rootMargin: '-40% 0px -50% 0px' });
SECTIONS.forEach(s => {
const el = document.getElementById(s.id);
if (el) obs.observe(el);
});
return () => obs.disconnect();
}, []);
return (
<div className="shell">
<Nav active={active} />
<Hero />
{SECTIONS.map(s => (
<s.Comp key={s.id} />
))}
<footer className="outro">
<div className="outro__copy">
PURE BLACK.<br/>
PURE GEOMETRY.<br/>
<span style={{ color: 'var(--accent)' }}>PURE SIGNAL.</span>
</div>
<div className="outro__meta">
<div>v2 · 2026.05</div>
<div>FOR YOUTUBE · 4K · 16:9</div>
<div style={{ marginTop: 14, color: 'var(--accent)' }}>END / TRANSMISSION</div>
</div>
</footer>
<TweaksPanel title="Tweaks">
<TweakSection title="Accent">
<TweakColor
label="主色"
value={t.accent}
onChange={(v) => setTweak('accent', v)}
options={['#FFFFFF', '#FF6B3D', '#1D9BF0', '#E8C547', '#00E0FF', '#FF3333']}
/>
</TweakSection>
<TweakSection title="Layout">
<TweakToggle
label="装饰元素(角标·tick·grid)"
value={t.showDeco}
onChange={(v) => setTweak('showDeco', v)}
/>
</TweakSection>
</TweaksPanel>
</div>
);
}
/* expose for inter-script use */
Object.assign(window, { Section, SubSec, Params, Stage });
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
/* ================================================================
sections/aroll.jsx — 01 · A-roll 出镜讲解(v3.1 编辑感升级)
"克制" 版:hairline + 字号悬崖 + 0 阴影 + ornament 而非装饰
================================================================ */
function ARollSection() {
return (
<Section
id="aroll"
num="01"
title="A-roll · 出镜讲解"
desc="出镜时叠在画面上的三大件:<b>字幕高亮</b>、<b>关键词贴纸</b>、<b>概念卡</b>。<em>每个组件只做一件事</em> —— 不堆装饰、不加阴影、不层叠卡片。"
>
<SubtitleHighlight />
<KeywordSticker />
<ConceptCard />
</Section>
);
}
/* ---------- 字幕高亮 ---------- */
function SubtitleHighlight() {
const tokens = ['让我们聊聊', '上下文', '工程', '不是', '提示词', '魔法'];
const highlighted = [1, 2, 4];
const [active, setActive] = React.useState(2);
React.useEffect(() => {
const id = setInterval(() => setActive(a => (a + 1) % tokens.length), 900);
return () => clearInterval(id);
}, []);
return (
<SubSec name="A · 字幕高亮 · Subtitle Highlight" tag="SPOKEN-WORD CAPTIONS">
<Stage pattern="dot" label="● A-ROLL · LIVE" labelR="01.A">
{/* 左上:讲者标签 */}
<div style={{ position: 'absolute', top: '8%', left: '6%', display: 'flex', alignItems: 'center', gap: 10 }}>
<span className="dot-pulse" />
<span className="mono" style={{ fontSize: 11, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>SPEAKER · LIVE</span>
</div>
{/* 右上:时间码 */}
<div className="mono" style={{ position: 'absolute', top: '8%', right: '6%', fontSize: 11, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>
00:14:22 · CC ON
</div>
{/* 字幕主体 */}
<div style={{
position: 'absolute', left: '8%', right: '8%', bottom: '14%',
display: 'flex', flexWrap: 'wrap', gap: '0 18px',
fontSize: 'clamp(28px, 4.4vw, 56px)', fontWeight: 800,
letterSpacing: '-0.018em', lineHeight: 1.1,
}}>
{tokens.map((tk, i) => {
const isHot = highlighted.includes(i) && i === active;
const isPast = highlighted.includes(i) && i < active;
return (
<span key={i} className="cn" style={{
color: isHot ? 'var(--accent)' : isPast ? 'var(--fg)' : 'var(--fg-3)',
position: 'relative',
transition: 'color 280ms var(--ease-out)',
}}>
{tk}
{isHot && (
<span style={{
position: 'absolute', left: 0, right: 0, bottom: '-12px',
height: 3, background: 'var(--accent)',
animation: 'lineIn 280ms var(--ease-out) forwards',
transformOrigin: 'left',
}} />
)}
</span>
);
})}
</div>
{/* 底部 hairline */}
<div style={{ position: 'absolute', bottom: '8%', left: '8%', right: '8%', display: 'flex', alignItems: 'center', gap: 12 }}>
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>EN / CN</span>
<span style={{ flex: 1, height: 1, background: 'var(--line)' }} />
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>3 / 6 WORDS HOT</span>
</div>
<style>{`@keyframes lineIn { from { transform: scaleX(0) } to { transform: scaleX(1) } }`}</style>
</Stage>
<Params rows={[
{ k: 'FONT', v: '思源黑体 800 · clamp 28-56px' },
{ k: 'COLOR', v: '默认 fg-3 · 念到 accent · 念过 fg' },
{ k: 'ACCENT', v: '<b>仅 3px 底线</b> · 无底色块 · scaleX in' },
{ k: 'CHROME', v: '左讲者 · 右时码 · 底 hairline + 计数' },
{ k: 'POSITION', v: '下 14% · 左右 8% padding' },
]} />
</SubSec>
);
}
/* ---------- 关键词贴纸 ---------- */
function KeywordSticker() {
const items = [
{ txt: 'Context', en: true, pos: { top: '20%', left: '8%' }, tilt: -1.5 },
{ txt: '不是魔法', en: false, pos: { top: '34%', right: '12%' }, tilt: 1 },
{ txt: '✱ Engineering', en: true, pos: { top: '62%', left: '14%' }, tilt: -0.5 },
];
const [shown, setShown] = React.useState([0]);
React.useEffect(() => {
const id = setInterval(() => {
setShown(s => {
const next = (s[s.length - 1] + 1) % items.length;
if (s.length >= items.length) return [next];
return [...s, next];
});
}, 1100);
return () => clearInterval(id);
}, []);
return (
<SubSec name="B · 关键词贴纸 · Keyword Sticker" tag="POP-IN LABELS">
<Stage pattern="dot" label="● A-ROLL · STICKER" labelR="01.B">
{/* 左上 eyebrow */}
<div className="mono" style={{ position: 'absolute', top: '8%', left: '6%', fontSize: 11, letterSpacing: '0.2em', color: 'var(--accent)' }}>● KEYWORDS · 3</div>
{items.map((it, i) => {
if (!shown.includes(i)) return null;
const isLast = shown[shown.length - 1] === i;
return (
<div key={i} className={it.en ? '' : 'cn'} style={{
position: 'absolute', ...it.pos,
padding: '14px 22px',
background: isLast ? 'var(--fg)' : 'var(--bg-card)',
color: isLast ? 'var(--bg)' : 'var(--fg)',
border: isLast ? 'none' : '1px solid var(--line-2)',
fontFamily: it.en ? 'var(--f-sans)' : 'var(--f-cn)',
fontWeight: it.en ? 600 : 700,
fontSize: 22, letterSpacing: it.en ? '-0.005em' : '0.01em',
borderRadius: 6,
animation: `stickIn 320ms var(--ease-spring) forwards`,
transform: `rotate(${it.tilt}deg)`,
transformOrigin: it.pos.left ? 'left center' : 'right center',
}}>{it.txt}</div>
);
})}
{/* 右下:scrollback meta */}
<div className="mono" style={{ position: 'absolute', bottom: '8%', right: '6%', fontSize: 11, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>
DUR · 320ms · SPRING
</div>
<style>{`
@keyframes stickIn {
from { opacity: 0; transform: scale(0.92) rotate(-1.5deg); }
to { opacity: 1; transform: scale(1) rotate(0); }
}
`}</style>
</Stage>
<Params rows={[
{ k: 'BG', v: '反白 fg / 卡 bg-card · 二选一' },
{ k: 'BORDER', v: '1px line-2 · 或无' },
{ k: 'PADDING', v: '14 / 22px' },
{ k: 'RADIUS', v: '6px · tilt ±1.5°' },
{ k: 'ENTER', v: 'scale .92→1 + tilt 1.5°→0 · 320ms spring' },
{ k: 'RULE', v: '<b>同屏 ≤ 3 个 · 至少 200px 间距</b>' },
]} />
</SubSec>
);
}
/* ---------- 概念卡 · 升级版 ---------- */
function ConceptCard() {
return (
<SubSec name="C · 概念卡 · Concept Card" tag="EXPLAINER CARD · REFINED">
<Stage pattern="dot" label="● A-ROLL · CARD" labelR="01.C">
{/* 左侧:模拟讲者侧 / 空白 — 让卡片"贴右" */}
<div className="mono" style={{ position: 'absolute', top: '14%', left: '6%', fontSize: 11, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>
← SPEAKER FRAME
</div>
{/* 概念卡主体 */}
<div style={{
position: 'absolute', top: '12%', right: '6%',
width: '50%',
}}>
{/* 头部:编号 + 时间码 */}
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 14 }}>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 14 }}>
<span className="mono" style={{ fontSize: 11, letterSpacing: '0.2em', color: 'var(--accent)' }}>● CONCEPT</span>
<span className="mono" style={{ fontSize: 11, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>02 / 12</span>
</div>
<span className="mono" style={{ fontSize: 11, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>00:14:22</span>
</div>
{/* 主卡片 */}
<div className="frame" style={{
background: 'var(--bg-card)',
border: '1px solid var(--line)',
borderRadius: 8,
padding: '32px 36px',
position: 'relative',
}}>
<Crosses />
{/* 标题 */}
<div className="cn" style={{ fontSize: 38, fontWeight: 800, letterSpacing: '-0.015em', lineHeight: 1.12, marginBottom: 14 }}>
Context 是<span style={{ color: 'var(--accent)' }}>材料</span>,
<br />不是<span className="serif" style={{ fontWeight: 400, fontStyle: 'italic', color: 'var(--fg-2)' }}>提示</span>。
</div>
{/* 分隔 */}
<div style={{ width: 28, height: 2, background: 'var(--accent)', marginBottom: 14 }} />
{/* 正文 */}
<div className="cn" style={{ fontSize: 16, fontWeight: 400, color: 'var(--fg-2)', lineHeight: 1.65 }}>
把"足够相关"的资料喂给模型,比把"足够聪明"的提示给模型更有效。
</div>
{/* 底部:来源 */}
<div style={{ marginTop: 22, paddingTop: 14, borderTop: '1px solid var(--line)', display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>REF · ANTHROPIC ENG BLOG</span>
<span className="mono" style={{ fontSize: 10, letterSpacing: '0.2em', color: 'var(--fg-3)' }}>HOLD · 4S</span>
</div>
</div>
</div>
</Stage>
{/* 用法说明 */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 16, marginBottom: 16 }}>
<UseCase label="01 · USE WHEN" body="抛出一个名词的<em>新定义</em>,需要在画面驻留 3-5s" />
<UseCase label="02 · DON'T" body="不要叠加 glow / 阴影 / 渐变 · 不要双卡同屏" />
<UseCase label="03 · ENTER" body="opacity 0→1 + y 12→0 · 700ms ease-out · 出场反向" />
</div>
<Params rows={[
{ k: 'BG', v: 'var(--bg-card) #111114' },
{ k: 'BORDER', v: '1px hairline + 4× 角十字针脚' },
{ k: 'RADIUS', v: '8px' },
{ k: 'PADDING', v: '32 / 36px · 宽度 50% 画面' },
{ k: 'TYPE', v: 'mono 11 caps · cn 38/800 · cn 16/400 · 一字 serif italic' },
{ k: 'RULE', v: '一卡只讲一个概念 · 不超过 3 行正文' },
{ k: 'SHADOW', v: '<b>0</b> · 强调靠换色与字号悬崖' },
{ k: 'ENTER', v: '700ms ease-out · 卡 + 角标错峰 80ms' },
]} />
</SubSec>
);
}
/* ---------- helpers ---------- */
function Crosses() {
return (
<>
<span className="cross cross--tl" />
<span className="cross cross--tr" />
<span className="cross cross--bl" />
<span className="cross cross--br" />
</>
);
}
function UseCase({ label, body }) {
return (
<div style={{
background: 'var(--bg-card)', border: '1px solid var(--line)',
borderRadius: 8, padding: '18px 20px',
}}>
<div className="mono" style={{ fontSize: 11, letterSpacing: '0.2em', color: 'var(--accent)', marginBottom: 10 }}>{label}</div>
<div className="cn" style={{ fontSize: 13, color: 'var(--fg-2)', lineHeight: 1.6 }} dangerouslySetInnerHTML={{ __html: body.replace(/<em>(.*?)<\/em>/g, '<em style="color:var(--accent);font-style:normal">$1</em>') }} />
</div>
);
}
Object.assign(window, { ARollSection });
/* ================================================================
sections/broll-abstract.jsx — 05 · B-roll · 抽象兜底
讲 AI 时多数概念没有具象图标 —— 这里是"以形会意"的通用版式
================================================================ */
function AbstractSection() {
return (
<Section id="abstract" num="05" title="B-roll · 抽象兜底"
desc="当概念<em>没有具象图标</em>可用时的通用版式 —— <b>类比</b>、<b>黑盒</b>、<b>等式</b>、<b>光谱</b>、<b>冰山</b>、<b>对照</b>、<b>占位</b>。这是讲 AI 时最常用的一类,因为<em>抽象概念远多于具象图标</em>。">
<Analogy />
<BlackBox />
<Equation />
<AbstractSpectrum />
<Iceberg />
<Versus />
<Placeholder />
</Section>
);
}
/* ---------- A · Analogy ---------- */
function Analogy() {
return (
<SubSec name="类比框 · Analogy" tag="UNFAMILIAR ≈ FAMILIAR">
<Stage pattern="dot" label="● B-ROLL" labelR="05.A">
<div style={{ position: 'absolute', inset: '16% 6%', display: 'grid', gridTemplateColumns: '1fr 80px 1fr', alignItems: 'center', gap: 0 }}>
<AnalogyCard side="未知" tone="accent" big="RAG" sub="检索增强生成 · 模型 + 外挂资料库" />
<div style={{ textAlign: 'center' }}>
<div className="serif" style={{ fontSize: 76, color: 'var(--accent)', lineHeight: 1 }}>≈</div>
<div className="meta" style={{ marginTop: 6 }}>就像</div>
</div>
<AnalogyCard side="熟悉" big="开卷考试" sub="不用背 · 翻书查 · 临场组织答案" />
</div>
</Stage>
<Params rows={[
{ k: 'LAYOUT', v: '左 = 未知 · 右 = 熟悉' },
{ k: 'CONNECTOR', v: '≈ · Instrument Serif italic · 76px · accent' },
{ k: 'SUB-LABEL', v: '"就像" 在 ≈ 下方做语义提示' },
{ k: 'CARDS', v: '完全对称 hairline 卡 · 左卡 accent 标签强化区分' },
]} />
</SubSec>
);
}
function AnalogyCard({ side, tone, big, sub }) {
return (
<div style={{ background: 'var(--bg-card)', border: '1px solid var(--line)', borderRadius: 8, padding: '36px 36px', position: 'relative' }}>
<Bracket size={18} color="var(--line-3)" thick={1} />
<div className="meta" style={{ color: tone === 'accent' ? 'var(--accent)' : 'var(--fg-3)', marginBottom: 18 }}>{side}</div>
<div className="cn" style={{ fontSize: 44, fontWeight: 800, letterSpacing: '-0.018em', lineHeight: 1, marginBottom: 14 }}>{big}</div>
<div className="cn" style={{ fontSize: 26, color: 'var(--fg-2)', lineHeight: 1.5 }}>{sub}</div>
</div>
);
}
/* ---------- B · Black Box ---------- */
function BlackBox() {
return (
<SubSec name="黑盒图 · Black Box" tag="INPUT → ? → OUTPUT">
<Stage pattern="dot" label="● B-ROLL" labelR="05.B">
<div style={{ position: 'absolute', inset: '24% 6%', display: 'grid', gridTemplateColumns: '1fr 56px 1.5fr 56px 1fr', alignItems: 'center', gap: 0 }}>
<Slot label="INPUT" cn="提示词" />
<Arrow />
<div style={{ position: 'relative', background: 'var(--bg-card)', border: '1px dashed var(--accent)', borderRadius: 8, padding: '36px 32px', textAlign: 'center' }}>
<Bracket size={14} color="var(--accent)" thick={1} />
<div className="meta" style={{ color: 'var(--accent)', marginBottom: 14 }}>BLACK BOX</div>
<div className="big-num" style={{ fontSize: 84, color: 'var(--accent)', lineHeight: 0.85 }}>?</div>
<div className="cn" style={{ fontSize: 26, color: 'var(--fg-2)', marginTop: 14 }}>175B 参数 · 不可解释</div>
</div>
<Arrow />
<Slot label="OUTPUT" cn="回答" />
</div>
</Stage>
<Params rows={[
{ k: 'BOX', v: 'dashed accent 描边(区别 hairline)+ 四角 bracket' },
{ k: '?', v: '84px · big-num · accent' },
{ k: 'USE', v: '讲"内部不可知"型概念' },
{ k: 'ARROW', v: 'hairline + 锐角三角 · line-3' },
]} />
</SubSec>
);
}
function Slot({ label, cn }) {
return (
<div style={{ background: 'var(--bg-card)', border: '1px solid var(--line)', borderRadius: 8, padding: '30px 32px', textAlign: 'center' }}>
<div className="meta" style={{ marginBottom: 12 }}>{label}</div>
<div className="cn" style={{ fontSize: 38, fontWeight: 800, letterSpacing: '-0.012em' }}>{cn}</div>
</div>
);
}
function Arrow() {
return (
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ height: 1, flex: 1, background: 'var(--line-3)' }} />
<div style={{ width: 0, height: 0, borderLeft: '8px solid var(--line-3)', borderTop: '5px solid transparent', borderBottom: '5px solid transparent' }} />
</div>
);
}
/* ---------- C · Equation ---------- */
function Equation() {
return (
<SubSec name="概念等式 · Concept Equation" tag="A + B = C">
<Stage pattern="dot" label="● B-ROLL" labelR="05.C">
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 24, padding: '0 6%' }}>
<EqBox big="模型" sub="reasoning" />
<EqOp ch="+" />
<EqBox big="资料" sub="context" accent />
<EqOp ch="=" />
<EqBox big="可靠回答" sub="output" />
</div>
{/* 顶部细注释线,提升"教科书等式"感 */}
<div style={{ position: 'absolute', top: '22%', left: '6%', right: '6%', display: 'flex', alignItems: 'center' }}>
<span className="meta" style={{ color: 'var(--fg-3)' }}>EQ · 概念组合</span>
<span style={{ flex: 1, height: 1, background: 'var(--line)', marginLeft: 12 }} />
</div>
</Stage>
<Params rows={[
{ k: 'LAYOUT', v: '横向居中 · 等距' },
{ k: 'OPERATOR', v: 'serif italic · 56px · accent' },
{ k: 'EMPHASIS', v: '关键项 accent 边框' },
{ k: 'HEADER', v: '顶部 EQ · hairline 注释栏(教科书味)' },
]} />
</SubSec>
);
}
function EqBox({ big, sub, accent }) {
return (
<div style={{ background: 'var(--bg-card)', border: `1px solid ${accent ? 'var(--accent)' : 'var(--line)'}`, borderRadius: 8, padding: '30px 36px', textAlign: 'center', minWidth: 160 }}>
<div className="cn" style={{ fontSize: 38, fontWeight: 800, letterSpacing: '-0.012em', marginBottom: 8 }}>{big}</div>
<div className="meta" style={{ color: accent ? 'var(--accent)' : 'var(--fg-3)' }}>{sub}</div>
</div>
);
}
function EqOp({ ch }) {
return <div className="serif" style={{ fontSize: 56, color: 'var(--accent)', lineHeight: 1 }}>{ch}</div>;
}
/* ---------- D · Spectrum ---------- */
function AbstractSpectrum() {
return (
<SubSec name="光谱 · Spectrum" tag="ONE AXIS · TWO POLES">
<Stage pattern="dot" label="● B-ROLL" labelR="05.D">
<div style={{ position: 'absolute', inset: '24% 8%', display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 18 }}>
{/* 左右极标签 */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
<div>
<div className="meta" style={{ marginBottom: 6 }}>左极</div>
<div className="cn" style={{ fontSize: 34, fontWeight: 800, letterSpacing: '-0.012em' }}>纯背诵</div>
</div>
<div style={{ textAlign: 'right' }}>
<div className="meta" style={{ marginBottom: 6, color: 'var(--accent)' }}>右极</div>
<div className="cn" style={{ fontSize: 34, fontWeight: 800, letterSpacing: '-0.012em' }}>纯检索</div>
</div>
</div>
{/* 轴 */}
<div style={{ position: 'relative', height: 24, display: 'flex', alignItems: 'center' }}>
{/* ticks */}
<div style={{ position: 'absolute', inset: 0, display: 'flex', justifyContent: 'space-between' }}>
{Array.from({ length: 11 }).map((_, i) => (
<span key={i} style={{ width: 1, height: i % 5 === 0 ? 12 : 6, background: 'var(--line-3)', alignSelf: 'flex-end' }} />
))}
</div>
{/* main rule */}
<div style={{ position: 'absolute', left: 0, right: 0, height: 1, background: 'var(--fg-3)', bottom: 0 }} />
{/* marker */}
<div style={{ position: 'absolute', left: '68%', bottom: -8, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
<span className="meta" style={{ color: 'var(--accent)', whiteSpace: 'nowrap' }}>RAG · 0.68</span>
<div style={{ width: 0, height: 0, borderLeft: '7px solid transparent', borderRight: '7px solid transparent', borderTop: '10px solid var(--accent)' }} />
</div>
</div>
{/* legend */}
<div style={{ display: 'flex', justifyContent: 'space-between', color: 'var(--fg-3)', marginTop: 28 }}>
<span className="cn" style={{ fontSize: 18 }}>fine-tuning</span>
<span className="cn" style={{ fontSize: 18 }}>vector search</span>
</div>
</div>
</Stage>
<Params rows={[
{ k: 'AXIS', v: '0 – 1 · 11 个 tick · 5n 主刻度' },
{ k: 'MARKER', v: '倒三角 · accent · 上方 mono 标签' },
{ k: 'POLES', v: '左 fg · 右 accent meta · 强调右极' },
{ k: 'USE', v: '"X 在 A 和 B 之间偏哪边" 类概念' },
]} />
</SubSec>
);
}
/* ---------- E · Iceberg ---------- */
function Iceberg() {
return (
<SubSec name="冰山 · Iceberg" tag="VISIBLE / HIDDEN">
<Stage pattern="dot" label="● B-ROLL" labelR="05.E">
<div style={{ position: 'absolute', inset: '12% 8%', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 32, alignItems: 'stretch' }}>
{/* 左侧 SVG 冰山 */}
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<svg viewBox="0 0 200 200" width="100%" style={{ maxWidth: 320 }}>
{/* 水面线 */}
<line x1="0" y1="80" x2="200" y2="80" stroke="var(--accent)" strokeWidth="1" strokeDasharray="3 3" />
<text x="2" y="76" fontSize="6" fill="var(--accent)" fontFamily="var(--f-mono)" letterSpacing="1">— WATERLINE</text>
{/* 上半(可见) */}
<polygon points="100,30 130,80 70,80" fill="none" stroke="var(--fg)" strokeWidth="1.2" />
{/* 下半(隐藏) */}
<polygon points="55,80 145,80 165,160 130,185 70,185 35,160" fill="rgba(255,255,255,0.04)" stroke="var(--fg-3)" strokeWidth="1" strokeDasharray="2 3" />
{/* 指引线 */}
<line x1="120" y1="55" x2="185" y2="40" stroke="var(--fg-3)" strokeWidth="0.6" />
<line x1="135" y1="140" x2="190" y2="155" stroke="var(--fg-3)" strokeWidth="0.6" />
<circle cx="120" cy="55" r="1.2" fill="var(--fg)" />
<circle cx="135" cy="140" r="1.2" fill="var(--fg-3)" />
</svg>
</div>
{/* 右侧文字标注 */}
<div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', gap: 28 }}>
<div>
<div className="meta" style={{ color: 'var(--accent)', marginBottom: 10 }}>● 可见 · 10%</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, letterSpacing: '-0.012em', marginBottom: 8 }}>对话界面</div>
<div className="cn" style={{ fontSize: 22, color: 'var(--fg-2)', lineHeight: 1.5 }}>你看到的输入框和回答</div>
</div>
<div style={{ height: 1, background: 'var(--line)' }} />
<div>
<div className="meta" style={{ marginBottom: 10 }}>○ 隐藏 · 90%</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, letterSpacing: '-0.012em', marginBottom: 8, color: 'var(--fg-2)' }}>权重 · 训练数据 · RLHF · 推理基建</div>
<div className="cn" style={{ fontSize: 22, color: 'var(--fg-3)', lineHeight: 1.5 }}>真正决定回答质量的部分</div>
</div>
</div>
</div>
</Stage>
<Params rows={[
{ k: 'WATERLINE', v: 'accent 虚线 + WATERLINE 标签' },
{ k: 'ABOVE', v: '实线 · 10% 标注 · accent' },
{ k: 'BELOW', v: '虚线 + 轻填充 · 90% · 灰阶' },
{ k: 'USE', v: '"看得见 / 看不见" 比例悬殊型概念' },
]} />
</SubSec>
);
}
/* ---------- F · Versus ---------- */
function Versus() {
return (
<SubSec name="对照 · Versus" tag="A vs B · DELTA">
<Stage pattern="dot" label="● B-ROLL" labelR="05.F">
<div style={{ position: 'absolute', inset: '14% 8%', display: 'grid', gridTemplateColumns: '1fr 100px 1fr', alignItems: 'stretch', gap: 0 }}>
<VsCard side="A" big="预训练" rows={[
{ k: '数据', v: '海量公开语料' },
{ k: '时长', v: '数月 · 数千卡' },
{ k: '产出', v: '通用基础模型' },
]} />
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
<div style={{ height: '100%', width: 1, background: 'var(--line)', position: 'absolute', top: 0 }} />
<div className="serif" style={{ fontSize: 56, color: 'var(--accent)', background: 'var(--bg)', padding: '4px 12px', position: 'relative', zIndex: 1 }}>vs</div>
</div>
<VsCard side="B" accent big="微调" rows={[
{ k: '数据', v: '少量任务数据' },
{ k: '时长', v: '小时 · 单卡' },
{ k: '产出', v: '专业化变体' },
]} />
</div>
</Stage>
<Params rows={[
{ k: 'LAYOUT', v: '左右等宽 · 中竖线 + vs serif' },
{ k: 'ROWS', v: '同序键值 · 行行对齐(方便逐行对比)' },
{ k: 'SIDE LABEL', v: '左 fg-3 · 右 accent · A / B' },
{ k: 'USE', v: '两个方案 / 两种概念逐项对比' },
]} />
</SubSec>
);
}
function VsCard({ side, accent, big, rows }) {
return (
<div style={{ background: 'var(--bg-card)', border: `1px solid ${accent ? 'var(--accent)' : 'var(--line)'}`, borderRadius: 8, padding: '28px 32px' }}>
<div className="meta" style={{ color: accent ? 'var(--accent)' : 'var(--fg-3)', marginBottom: 14 }}>{side}</div>
<div className="cn" style={{ fontSize: 38, fontWeight: 800, letterSpacing: '-0.012em', marginBottom: 18 }}>{big}</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{rows.map(r => (
<div key={r.k} style={{ display: 'grid', gridTemplateColumns: '70px 1fr', gap: 16, alignItems: 'baseline', paddingBottom: 8, borderBottom: '1px solid var(--line)' }}>
<span className="meta">{r.k}</span>
<span className="cn" style={{ fontSize: 22, color: 'var(--fg-2)' }}>{r.v}</span>
</div>
))}
</div>
</div>
);
}
/* ---------- G · Placeholder ---------- */
function Placeholder() {
return (
<SubSec name="占位框 · Placeholder" tag="WHEN YOU LACK AN ASSET">
<Stage pattern="dot" label="● B-ROLL" labelR="05.G">
<div style={{ position: 'absolute', inset: '18% 18%', background: 'transparent', border: '1px solid var(--line-2)', borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', backgroundImage: 'repeating-linear-gradient(-45deg, transparent 0, transparent 14px, rgba(255,255,255,0.04) 14px, rgba(255,255,255,0.04) 15px)', position: 'relative' }}>
<Bracket size={20} color="var(--line-3)" thick={1} />
<div className="meta" style={{ color: 'var(--accent)', marginBottom: 14 }}>[ DROP HERE ]</div>
<div className="cn" style={{ fontSize: 38, fontWeight: 800, letterSpacing: '-0.012em' }}>GPT-4 屏幕录像</div>
<div className="mono" style={{ fontSize: 22, color: 'var(--fg-3)', marginTop: 10 }}>1920 × 1080 · ≤ 8s · prores</div>
</div>
</Stage>
<Params rows={[
{ k: 'BG', v: '45° 斜条纹(4% 白)' },
{ k: 'BORDER', v: '1px line-2 + 四角 bracket' },
{ k: 'LABEL', v: '[ DROP HERE ] mono caps · accent' },
{ k: 'SPEC', v: '尺寸 · 时长 · 编码格式' },
]} />
</SubSec>
);
}
Object.assign(window, { AbstractSection });
/* ================================================================
sections/broll-charts.jsx — 06 · A 类 · 数据图表 12 款
line / multi-line / bar / hbar / stacked / area / donut /
scatter / heatmap / gauge / sparkline / sankey
================================================================ */
function ChartsSection() {
return (
<Section id="charts" num="06" title="B-roll · 数据图表"
desc="讲<b>数字 · 趋势 · 占比 · 流量</b>时用。所有图共用一套渲染语法:<em>hairline 坐标轴 · mono 数字 · 单 accent 高亮关键点</em>。每屏最多 1 个主信息 + 2-3 个数据点。">
<LineChart /><MultiLine /><BarChart /><HBarChart />
<StackedBar /><AreaChart /><Donut /><Scatter />
<Heatmap /><Gauge /><Sparkline /><Sankey />
</Section>
);
}
/* ── shared axis ── */
function Axis({ xLabels, yMax = 100, yStep = 25, padX = 80, padY = 60, w = 1000, h = 500 }) {
const ys = [];
for (let v = 0; v <= yMax; v += yStep) ys.push(v);
const innerH = h - padY * 2;
const innerW = w - padX * 2;
return (
<g>
{/* y grid + labels */}
{ys.map(v => {
const y = h - padY - (v / yMax) * innerH;
return (
<g key={v}>
<line x1={padX} x2={w - padX} y1={y} y2={y} stroke="rgba(255,255,255,.06)" strokeWidth="1" />
<text x={padX - 14} y={y + 6} textAnchor="end" fontSize="16" fontFamily="var(--f-mono)" fill="rgba(255,255,255,.42)">{v}</text>
</g>
);
})}
{/* x labels */}
{xLabels.map((l, i) => {
const x = padX + (i / (xLabels.length - 1)) * innerW;
return <text key={i} x={x} y={h - padY + 30} textAnchor="middle" fontSize="16" fontFamily="var(--f-mono)" letterSpacing="0.08em" fill="rgba(255,255,255,.42)">{l}</text>;
})}
</g>
);
}
/* ── A1 · 折线图 ── */
function LineChart() {
const pts = [12, 28, 22, 45, 38, 62, 78, 71, 88];
const labels = ['W1','W2','W3','W4','W5','W6','W7','W8','W9'];
const padX = 80, padY = 60, w = 1000, h = 500;
const innerW = w - padX * 2, innerH = h - padY * 2;
const path = pts.map((v, i) => `${i === 0 ? 'M' : 'L'} ${padX + (i / (pts.length - 1)) * innerW} ${h - padY - (v / 100) * innerH}`).join(' ');
return (
<SubSec name="A1 · 折线图 · Line Chart" tag="TREND OVER TIME">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A1">
<div style={{ position: 'absolute', top: '8%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>WEEKLY · ACTIVE USERS</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, marginTop: 6 }}>9 周增长 +633%</div>
</div>
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
<Axis xLabels={labels} />
<path d={path} fill="none" stroke="var(--accent)" strokeWidth="3" strokeLinejoin="round" strokeLinecap="round" />
{pts.map((v, i) => {
const x = padX + (i / (pts.length - 1)) * innerW;
const y = h - padY - (v / 100) * innerH;
const last = i === pts.length - 1;
return <g key={i}>
<circle cx={x} cy={y} r={last ? 8 : 4} fill={last ? 'var(--accent)' : 'var(--bg)'} stroke="var(--accent)" strokeWidth="2" />
{last && <text x={x + 16} y={y + 6} fontSize="20" fontWeight="800" fontFamily="var(--f-mono)" fill="var(--accent)">{v}</text>}
</g>;
})}
</svg>
</Stage>
<Params rows={[
{ k: 'AXIS', v: '1px rgba(255,255,255,.06) hairline' },
{ k: 'LINE', v: '3px accent · round join' },
{ k: 'POINT', v: '4px (常规) · 8px (端点高亮)' },
{ k: 'LABEL', v: '末端标数字 + mono 字体' },
]} />
</SubSec>
);
}
/* ── A2 · 多线对比 ── */
function MultiLine() {
const series = [
{ name: 'GPT-4', data: [22, 38, 45, 58, 64, 71, 78, 82, 86], color: 'var(--accent)' },
{ name: 'Claude 3.5', data: [18, 32, 41, 52, 60, 68, 74, 79, 83], color: 'rgba(255,255,255,.7)' },
{ name: 'Gemini', data: [15, 24, 30, 38, 44, 50, 55, 58, 62], color: 'rgba(255,255,255,.35)' },
];
const labels = ['1','2','3','4','5','6','7','8','9'];
const padX = 80, padY = 60, w = 1000, h = 500;
const innerW = w - padX * 2, innerH = h - padY * 2;
return (
<SubSec name="A2 · 多线对比 · Multi-line" tag="MODEL COMPARISON">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A2">
<div style={{ position: 'absolute', top: '8%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>BENCHMARK · MMLU SCORE</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, marginTop: 6 }}>三家模型 9 月成绩</div>
</div>
<div style={{ position: 'absolute', top: '8%', right: '6%', display: 'flex', flexDirection: 'column', gap: 10 }}>
{series.map(s => (
<div key={s.name} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ width: 18, height: 3, background: s.color }} />
<span className="mono" style={{ fontSize: 16, color: s.color }}>{s.name}</span>
</div>
))}
</div>
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
<Axis xLabels={labels} />
{series.map((s, si) => {
const path = s.data.map((v, i) => `${i === 0 ? 'M' : 'L'} ${padX + (i / (s.data.length - 1)) * innerW} ${h - padY - (v / 100) * innerH}`).join(' ');
return <path key={si} d={path} fill="none" stroke={s.color} strokeWidth={si === 0 ? 3 : 2} strokeLinejoin="round" />;
})}
</svg>
</Stage>
<Params rows={[
{ k: 'HIERARCHY', v: '主线 accent 3px · 次线 70% 白 2px · 三线 35% 白 2px' },
{ k: 'LEGEND', v: '右上角 · 18px 横杠 + mono 名' },
{ k: 'RULE', v: '<b>最多 3 条线</b> · 4 条以上拆图' },
]} />
</SubSec>
);
}
/* ── A3 · 柱形图 ── */
function BarChart() {
const data = [
{ l: 'JAN', v: 38 }, { l: 'FEB', v: 52 }, { l: 'MAR', v: 41 },
{ l: 'APR', v: 67 }, { l: 'MAY', v: 84 }, { l: 'JUN', v: 72 },
];
const max = 100;
return (
<SubSec name="A3 · 柱形图 · Bar Chart" tag="DISCRETE QUANTITIES">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A3">
<div style={{ position: 'absolute', top: '8%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>MONTHLY · API CALLS (M)</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, marginTop: 6 }}>5 月峰值 84M</div>
</div>
<div style={{ position: 'absolute', inset: '34% 8% 14% 8%', display: 'flex', alignItems: 'flex-end', gap: 24 }}>
{data.map((d, i) => {
const hot = d.v === Math.max(...data.map(x => x.v));
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', height: '100%' }}>
<div className="mono" style={{ fontSize: 18, fontWeight: 600, color: hot ? 'var(--accent)' : 'var(--fg-2)', marginBottom: 8 }}>{d.v}</div>
<div style={{ width: '100%', flex: 1, display: 'flex', alignItems: 'flex-end' }}>
<div style={{ width: '100%', height: `${(d.v / max) * 100}%`, background: hot ? 'var(--accent)' : 'rgba(255,255,255,.18)', borderRadius: '2px 2px 0 0' }} />
</div>
<div className="meta" style={{ marginTop: 14, color: hot ? 'var(--accent)' : 'var(--fg-3)' }}>{d.l}</div>
</div>
);
})}
</div>
</Stage>
<Params rows={[
{ k: 'BAR', v: '默认 18% 白 · 峰值 accent' },
{ k: 'GAP', v: '24px · 不超过柱宽 50%' },
{ k: 'VALUE', v: '柱顶 mono · 峰值字色 accent' },
{ k: 'RADIUS', v: '顶部 2px (柔化)' },
]} />
</SubSec>
);
}
/* ── A4 · 横向条形 ── */
function HBarChart() {
const data = [
{ l: 'GitHub Copilot', v: 92 },
{ l: 'ChatGPT', v: 88 },
{ l: 'Cursor', v: 74 },
{ l: 'Claude Code', v: 68 },
{ l: 'Windsurf', v: 41 },
];
return (
<SubSec name="A4 · 横向条形 · H-Bar" tag="RANKING">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A4">
<div style={{ position: 'absolute', top: '8%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>2025 DEV TOOL ADOPTION %</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, marginTop: 6 }}>开发者用得最多的 AI 工具</div>
</div>
<div style={{ position: 'absolute', inset: '30% 8% 10% 8%', display: 'flex', flexDirection: 'column', gap: 18 }}>
{data.map((d, i) => (
<div key={i} style={{ display: 'grid', gridTemplateColumns: '180px 1fr 60px', alignItems: 'center', gap: 18 }}>
<div className="cn" style={{ fontSize: 18, fontWeight: 600, color: i === 0 ? 'var(--fg)' : 'var(--fg-2)' }}>{d.l}</div>
<div style={{ height: 18, background: 'rgba(255,255,255,.05)', position: 'relative' }}>
<div style={{ width: `${d.v}%`, height: '100%', background: i === 0 ? 'var(--accent)' : 'rgba(255,255,255,.22)' }} />
</div>
<div className="mono" style={{ fontSize: 18, fontWeight: 600, color: i === 0 ? 'var(--accent)' : 'var(--fg-2)', textAlign: 'right' }}>{d.v}%</div>
</div>
))}
</div>
</Stage>
<Params rows={[
{ k: 'LAYOUT', v: '3 列:标签 / 条 / 数值' },
{ k: 'SORT', v: '降序 · 第一名 accent' },
{ k: 'BAR HEIGHT', v: '18px (粗壮便于看清)' },
{ k: 'TRACK', v: '5% 白底打底 (空槽可见)' },
]} />
</SubSec>
);
}
/* ── A5 · 堆叠条/柱 ── */
function StackedBar() {
const months = [
{ l: 'Q1', segs: [42, 28, 18] },
{ l: 'Q2', segs: [58, 31, 22] },
{ l: 'Q3', segs: [71, 38, 27] },
{ l: 'Q4', segs: [88, 42, 34] },
];
const colors = ['var(--accent)', 'rgba(255,255,255,.55)', 'rgba(255,255,255,.22)'];
const names = ['Inference', 'Training', 'Storage'];
return (
<SubSec name="A5 · 堆叠柱 · Stacked" tag="COMPOSITION OVER TIME">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A5">
<div style={{ position: 'absolute', top: '8%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>COMPUTE COST · $M</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, marginTop: 6 }}>四季度构成变化</div>
</div>
<div style={{ position: 'absolute', top: '8%', right: '6%', display: 'flex', flexDirection: 'column', gap: 10 }}>
{names.map((n, i) => (
<div key={n} style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ width: 14, height: 14, background: colors[i] }} />
<span className="mono" style={{ fontSize: 16, color: 'var(--fg-2)' }}>{n}</span>
</div>
))}
</div>
<div style={{ position: 'absolute', inset: '36% 8% 14% 8%', display: 'flex', alignItems: 'flex-end', gap: 36 }}>
{months.map((m, i) => {
const total = m.segs.reduce((s, v) => s + v, 0);
const max = 180;
return (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<div className="mono" style={{ fontSize: 18, fontWeight: 600, color: 'var(--fg)', marginBottom: 8 }}>${total}</div>
<div style={{ width: '100%', height: 220, display: 'flex', flexDirection: 'column-reverse' }}>
{m.segs.map((s, si) => (
<div key={si} style={{ height: `${(s / max) * 100}%`, background: colors[si] }} />
))}
</div>
<div className="meta" style={{ marginTop: 14 }}>{m.l}</div>
</div>
);
})}
</div>
</Stage>
<Params rows={[
{ k: 'COLOR ORDER', v: '主项 accent · 次项 55% 白 · 三项 22% 白' },
{ k: 'TOTAL', v: '柱顶 mono · 累计值' },
{ k: 'STACK', v: '主项<b>放底部</b>视觉锚定' },
]} />
</SubSec>
);
}
/* ── A6 · 面积图 ── */
function AreaChart() {
const pts = [10, 18, 28, 22, 38, 52, 48, 68, 84];
const labels = ['1','2','3','4','5','6','7','8','9'];
const padX = 80, padY = 60, w = 1000, h = 500;
const innerW = w - padX * 2, innerH = h - padY * 2;
const pathLine = pts.map((v, i) => `${i === 0 ? 'M' : 'L'} ${padX + (i / (pts.length - 1)) * innerW} ${h - padY - (v / 100) * innerH}`).join(' ');
const pathFill = pathLine + ` L ${w - padX} ${h - padY} L ${padX} ${h - padY} Z`;
return (
<SubSec name="A6 · 面积图 · Area" tag="ACCUMULATED VOLUME">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A6">
<div style={{ position: 'absolute', top: '8%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>CONTEXT WINDOW · K TOKENS</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, marginTop: 6 }}>模型上下文增长曲线</div>
</div>
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
<Axis xLabels={labels} />
<defs>
<linearGradient id="areaGrad" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="var(--accent)" stopOpacity="0.42" />
<stop offset="100%" stopColor="var(--accent)" stopOpacity="0" />
</linearGradient>
</defs>
<path d={pathFill} fill="url(#areaGrad)" />
<path d={pathLine} fill="none" stroke="var(--accent)" strokeWidth="3" />
</svg>
</Stage>
<Params rows={[
{ k: 'FILL', v: 'accent 42% → 0% 渐变 (唯一允许的渐变)' },
{ k: 'STROKE', v: '顶线 3px accent' },
{ k: 'USE', v: '强调"累积量 / 容量"' },
]} />
</SubSec>
);
}
/* ── A7 · 环形图 ── */
function Donut() {
const data = [
{ l: 'Retrieval', v: 52, c: 'var(--accent)' },
{ l: 'Generate', v: 28, c: 'rgba(255,255,255,.65)' },
{ l: 'Rank', v: 14, c: 'rgba(255,255,255,.32)' },
{ l: 'Other', v: 6, c: 'rgba(255,255,255,.14)' },
];
const r = 140, cx = 160, cy = 160, stroke = 36;
const C = 2 * Math.PI * r;
let acc = 0;
return (
<SubSec name="A7 · 环形图 · Donut" tag="PROPORTION (≤ 4 SLICES)">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A7">
<div style={{ position: 'absolute', inset: 0, display: 'grid', gridTemplateColumns: '1fr 1fr', alignItems: 'center', padding: '6% 8%' }}>
<div style={{ position: 'relative', width: 320, height: 320 }}>
<svg viewBox="0 0 320 320">
{data.map((d, i) => {
const len = (d.v / 100) * C;
const dasharray = `${len} ${C - len}`;
const offset = -acc;
acc += len;
return <circle key={i} cx={cx} cy={cy} r={r} fill="none" stroke={d.c} strokeWidth={stroke} strokeDasharray={dasharray} strokeDashoffset={offset} transform={`rotate(-90 ${cx} ${cy})`} />;
})}
</svg>
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
<div className="mono" style={{ fontSize: 56, fontWeight: 800, color: 'var(--accent)' }}>52%</div>
<div className="meta" style={{ marginTop: 6 }}>RETRIEVAL</div>
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
<div className="meta" style={{ color: 'var(--accent)', marginBottom: 6 }}>LATENCY BUDGET BREAKDOWN</div>
{data.map((d, i) => (
<div key={i} style={{ display: 'grid', gridTemplateColumns: '24px 1fr 60px', alignItems: 'center', gap: 14 }}>
<span style={{ width: 18, height: 18, background: d.c }} />
<div className="cn" style={{ fontSize: 20, fontWeight: i === 0 ? 800 : 400, color: i === 0 ? 'var(--fg)' : 'var(--fg-2)' }}>{d.l}</div>
<div className="mono" style={{ fontSize: 20, fontWeight: 600, color: i === 0 ? 'var(--accent)' : 'var(--fg-2)', textAlign: 'right' }}>{d.v}%</div>
</div>
))}
</div>
</div>
</Stage>
<Params rows={[
{ k: 'RING', v: '36px stroke · 半径 140' },
{ k: 'CENTER', v: '主项数字 mono 800 · 56px · accent' },
{ k: 'LEGEND', v: '右栏 3 列:色块 / 名 / 数值' },
{ k: 'RULE', v: '<b>≤ 4 块</b>,多了换柱形' },
]} />
</SubSec>
);
}
/* ── A8 · 散点图 ── */
function Scatter() {
// x = cost (0-100), y = quality (0-100), size = adoption
const pts = [
{ x: 22, y: 38, r: 10, l: 'A', dim: true },
{ x: 35, y: 56, r: 14, l: 'B', dim: true },
{ x: 48, y: 72, r: 22, l: 'C', dim: true },
{ x: 68, y: 88, r: 28, l: 'D' }, // hot
{ x: 82, y: 78, r: 16, l: 'E', dim: true },
{ x: 28, y: 22, r: 8, l: 'F', dim: true },
{ x: 58, y: 48, r: 12, l: 'G', dim: true },
];
const padX = 90, padY = 70, w = 1000, h = 500;
const innerW = w - padX * 2, innerH = h - padY * 2;
return (
<SubSec name="A8 · 散点图 · Scatter" tag="CORRELATION / DISTRIBUTION">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A8">
<div style={{ position: 'absolute', top: '8%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>COST × QUALITY · MODEL LANDSCAPE</div>
<div className="cn" style={{ fontSize: 32, fontWeight: 800, marginTop: 6 }}>模型 D 是甜蜜点</div>
</div>
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
<Axis xLabels={['LOW','','','MID','','','HIGH']} />
<text x={padX - 50} y={h / 2} fontSize="14" fontFamily="var(--f-mono)" letterSpacing="0.16em" fill="rgba(255,255,255,.42)" transform={`rotate(-90 ${padX - 50} ${h / 2})`}>QUALITY →</text>
{pts.map((p, i) => {
const cx = padX + (p.x / 100) * innerW;
const cy = h - padY - (p.y / 100) * innerH;
return (
<g key={i}>
<circle cx={cx} cy={cy} r={p.r} fill={p.dim ? 'rgba(255,255,255,.14)' : 'var(--accent)'} stroke={p.dim ? 'rgba(255,255,255,.32)' : 'var(--accent)'} strokeWidth="1.5" />
<text x={cx} y={cy + 5} textAnchor="middle" fontSize="14" fontFamily="var(--f-mono)" fontWeight="800" fill={p.dim ? 'var(--fg)' : 'var(--bg)'}>{p.l}</text>
</g>
);
})}
</svg>
</Stage>
<Params rows={[
{ k: 'AXES', v: '双坐标 · 角落注 LOW/HIGH' },
{ k: 'POINT SIZE', v: '映射第三维度(如采用量)' },
{ k: 'HIGHLIGHT', v: '主点 accent · 其余 14% 白填' },
]} />
</SubSec>
);
}
/* ── A9 · 热力图 ── */
function Heatmap() {
const rows = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
const cols = ['0','3','6','9','12','15','18','21'];
// 7x8 grid intensity 0-100
const grid = rows.map((_, r) => cols.map((_, c) => {
// peak around Tue-Thu / 9-15
const dr = Math.abs(r - 2.5), dc = Math.abs(c - 4);
const base = Math.max(0, 100 - dr * 18 - dc * 14);
return Math.min(100, Math.round(base + (Math.sin(r * c) * 12)));
}));
return (
<SubSec name="A9 · 热力图 · Heatmap" tag="2D INTENSITY">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A9">
<div style={{ position: 'absolute', top: '6%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>API USAGE · WEEKDAY × HOUR</div>
<div className="cn" style={{ fontSize: 28, fontWeight: 800, marginTop: 6 }}>工作日中午为高峰</div>
</div>
<div style={{ position: 'absolute', inset: '32% 8% 10% 8%', display: 'grid', gridTemplateRows: 'repeat(7, 1fr)', gap: 4 }}>
{grid.map((row, ri) => (
<div key={ri} style={{ display: 'grid', gridTemplateColumns: '40px repeat(8, 1fr)', gap: 4, alignItems: 'center' }}>
<div className="meta" style={{ color: 'var(--fg-3)' }}>{rows[ri]}</div>
{row.map((v, ci) => (
<div key={ci} title={`${rows[ri]} ${cols[ci]}: ${v}`} style={{
aspectRatio: '1.6 / 1',
background: v > 70 ? 'var(--accent)' : `rgba(255,255,255,${Math.max(0.04, v / 200)})`,
borderRadius: 2,
}} />
))}
</div>
))}
<div style={{ display: 'grid', gridTemplateColumns: '40px repeat(8, 1fr)', gap: 4, marginTop: 4 }}>
<div />
{cols.map(c => <div key={c} className="meta" style={{ textAlign: 'center', color: 'var(--fg-3)' }}>{c}</div>)}
</div>
</div>
</Stage>
<Params rows={[
{ k: 'CELL', v: '阶梯填色:< 70 灰阶 · ≥ 70 accent' },
{ k: 'GAP', v: '4px · 让格子边界清晰' },
{ k: 'LABELS', v: '行 · 列 mono caps 14px' },
]} />
</SubSec>
);
}
/* ── A10 · 仪表盘 ── */
function Gauge() {
const value = 73;
const startA = -200, endA = 20; // sweep 220°
const r = 130, cx = 160, cy = 170, stroke = 22;
const C = (220 / 360) * 2 * Math.PI * r;
const filled = (value / 100) * C;
const arc = (a) => {
const rad = (a * Math.PI) / 180;
return [cx + r * Math.cos(rad), cy + r * Math.sin(rad)];
};
const [x1, y1] = arc(startA), [x2, y2] = arc(endA);
return (
<SubSec name="A10 · 仪表盘 · Gauge" tag="SINGLE METRIC">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A10">
<div style={{ position: 'absolute', inset: 0, display: 'grid', gridTemplateColumns: '1fr 1fr', alignItems: 'center', padding: '6% 10%' }}>
<div style={{ position: 'relative', width: 340, height: 240 }}>
<svg viewBox="0 0 340 240">
<path d={`M ${x1} ${y1} A ${r} ${r} 0 1 1 ${x2} ${y2}`} fill="none" stroke="rgba(255,255,255,.1)" strokeWidth={stroke} strokeLinecap="round" />
<path d={`M ${x1} ${y1} A ${r} ${r} 0 1 1 ${x2} ${y2}`} fill="none" stroke="var(--accent)" strokeWidth={stroke} strokeLinecap="round" strokeDasharray={`${filled} ${C}`} />
</svg>
<div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'flex-end', paddingBottom: 14 }}>
<div className="mono" style={{ fontSize: 72, fontWeight: 800, color: 'var(--accent)', lineHeight: 1 }}>73<span style={{ fontSize: 28, color: 'var(--fg-2)' }}>%</span></div>
<div className="meta" style={{ marginTop: 8 }}>HEALTHY · TARGET 80%</div>
</div>
</div>
<div>
<div className="meta" style={{ color: 'var(--accent)', marginBottom: 10 }}>RAG · ANSWER FIDELITY</div>
<div className="cn" style={{ fontSize: 30, fontWeight: 800, lineHeight: 1.2, marginBottom: 14 }}>距离目标还差 <span style={{ color: 'var(--accent)' }}>7 个点</span></div>
<div className="cn" style={{ fontSize: 18, color: 'var(--fg-2)', lineHeight: 1.6 }}>本周新版 retrieval 上线后从 64 → 73。<br/>计划下周做 reranker 验证。</div>
</div>
</div>
</Stage>
<Params rows={[
{ k: 'SWEEP', v: '220° · 起始 -200° → 终止 20°' },
{ k: 'STROKE', v: '22px round cap · 留底色 10% 白' },
{ k: 'NUMBER', v: '72px mono 800 · accent' },
]} />
</SubSec>
);
}
/* ── A11 · 迷你图 ── */
function Sparkline() {
const cards = [
{ l: 'DAU', v: '12.4K', d: '+8.2%', up: true, pts: [40, 38, 52, 48, 62, 70, 78] },
{ l: 'LATENCY', v: '342ms', d: '-12.4%', up: true, pts: [78, 82, 70, 58, 52, 48, 42] },
{ l: 'ERROR', v: '0.42%', d: '+0.08', up: false, pts: [22, 18, 28, 32, 28, 38, 42] },
{ l: 'COST', v: '$3.8K', d: '+2.1%', up: false, pts: [42, 48, 44, 52, 50, 58, 62] },
];
return (
<SubSec name="A11 · 迷你图 · Sparkline" tag="DENSE METRIC CARDS">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A11">
<div style={{ position: 'absolute', top: '8%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>WEEKLY DASHBOARD</div>
<div className="cn" style={{ fontSize: 28, fontWeight: 800, marginTop: 6 }}>4 项关键指标</div>
</div>
<div style={{ position: 'absolute', inset: '32% 6% 12% 6%', display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
{cards.map((c, i) => {
const max = Math.max(...c.pts), min = Math.min(...c.pts);
const W = 200, H = 60;
const path = c.pts.map((v, j) => `${j === 0 ? 'M' : 'L'} ${(j / (c.pts.length - 1)) * W} ${H - ((v - min) / (max - min)) * H}`).join(' ');
return (
<div key={i} style={{ background: 'var(--bg-card)', border: '1px solid var(--line)', borderRadius: 6, padding: 22 }}>
<div className="meta" style={{ marginBottom: 10 }}>{c.l}</div>
<div className="mono" style={{ fontSize: 30, fontWeight: 800, color: 'var(--fg)', marginBottom: 4 }}>{c.v}</div>
<div className="mono" style={{ fontSize: 14, color: c.up ? 'var(--green)' : 'var(--accent)', marginBottom: 14 }}>{c.d}</div>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" height="60" preserveAspectRatio="none">
<path d={path} fill="none" stroke={c.up ? 'var(--green)' : 'var(--accent)'} strokeWidth="2.5" />
</svg>
</div>
);
})}
</div>
</Stage>
<Params rows={[
{ k: 'CARD', v: '1px hairline · 22px padding · 6px radius' },
{ k: 'METRIC', v: 'mono 30/800 主数 + 14px delta' },
{ k: 'LINE', v: '2.5px · 颜色映射趋势 (绿=好 / 橙=糟)' },
]} />
</SubSec>
);
}
/* ── A12 · Sankey 流图 ── */
function Sankey() {
// 3 sources -> 2 mid -> 1 sink, simplified rectangles + bezier
const w = 1000, h = 460;
const cols = [
{ x: 80, nodes: [{ y: 80, h: 140, l: '搜索 · 56%' }, { y: 240, h: 80, l: '社交 · 24%' }, { y: 340, h: 60, l: '直接 · 20%' }] },
{ x: 470, nodes: [{ y: 80, h: 180, l: '试用 · 65%' }, { y: 280, h: 120, l: '流失 · 35%' }] },
{ x: 860, nodes: [{ y: 80, h: 220, l: '留存 · 28%' }] },
];
const flows = [
{ a: [0, 0], b: [1, 0], w: 100, hot: true },
{ a: [0, 0], b: [1, 1], w: 40 },
{ a: [0, 1], b: [1, 0], w: 50, hot: true },
{ a: [0, 1], b: [1, 1], w: 30 },
{ a: [0, 2], b: [1, 0], w: 30, hot: true },
{ a: [0, 2], b: [1, 1], w: 30 },
{ a: [1, 0], b: [2, 0], w: 180, hot: true },
];
const NW = 18;
const flowPath = (f) => {
const A = cols[f.a[0]].nodes[f.a[1]];
const B = cols[f.b[0]].nodes[f.b[1]];
const x1 = cols[f.a[0]].x + NW, x2 = cols[f.b[0]].x;
const y1 = A.y + A.h / 2, y2 = B.y + B.h / 2;
const mx = (x1 + x2) / 2;
return `M ${x1} ${y1 - f.w / 2} C ${mx} ${y1 - f.w / 2}, ${mx} ${y2 - f.w / 2}, ${x2} ${y2 - f.w / 2} L ${x2} ${y2 + f.w / 2} C ${mx} ${y2 + f.w / 2}, ${mx} ${y1 + f.w / 2}, ${x1} ${y1 + f.w / 2} Z`;
};
return (
<SubSec name="A12 · Sankey 流图" tag="FLOW DISTRIBUTION">
<Stage pattern="graph" label="● B-ROLL · DATA" labelR="06.A12">
<div style={{ position: 'absolute', top: '6%', left: '6%' }}>
<div className="meta" style={{ color: 'var(--accent)' }}>USER ACQUISITION FUNNEL</div>
<div className="cn" style={{ fontSize: 28, fontWeight: 800, marginTop: 6 }}>10,000 流量到 28% 留存</div>
</div>
<svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="xMidYMid meet" style={{ position: 'absolute', inset: '26% 0 0 0', width: '100%', height: '74%' }}>
{flows.map((f, i) => (
<path key={i} d={flowPath(f)} fill={f.hot ? 'var(--accent)' : 'rgba(255,255,255,.12)'} opacity={f.hot ? 0.32 : 0.42} />
))}
{cols.map((c, ci) => c.nodes.map((n, ni) => {
const hot = ci === 2 || (ci === 0) || (ci === 1 && ni === 0);
return (
<g key={ci + '-' + ni}>
<rect x={c.x} y={n.y} width={NW} height={n.h} fill={hot ? 'var(--accent)' : 'var(--fg-2)'} />
<text x={ci === 2 ? c.x - 14 : c.x + NW + 14} y={n.y + n.h / 2 + 6} fontSize="18" fontWeight="600" fontFamily="var(--f-cn)" fill={hot ? 'var(--accent)' : 'var(--fg)'} textAnchor={ci === 2 ? 'end' : 'start'}>{n.l}</text>
</g>
);
}))}
</svg>
</Stage>
<Params rows={[
{ k: 'NODE', v: '18px 宽矩形 · accent / 62% 白' },
{ k: 'FLOW', v: 'bezier · accent 32% / 白 42% opacity' },
{ k: 'WIDTH', v: '映射流量大小' },
{ k: 'USE', v: '讲漏斗 / 转化 / 资源分配' },
]} />
</SubSec>
);
}
Object.assign(window, { ChartsSection });
/* ================================================================
tokens.css — 视频组件库 v2 · SpaceX × Grok × X 视觉语言
纯黑 / 纯白 · 几何 sans · condensed 数字 · spec-sheet 美学
================================================================ */
:root {
/* ---------- color · pure mono ---------- */
--bg: #000000; /* pure black · 太空黑 */
--bg-card: #0A0A0A; /* 卡片层 */
--bg-elev: #141414; /* 抬起层 */
--bg-flash: #FFFFFF; /* 反白 cut-in */
--fg: #FFFFFF; /* pure white */
--fg-2: rgba(255,255,255,0.66);
--fg-3: rgba(255,255,255,0.42);
--fg-4: rgba(255,255,255,0.18);
--line: rgba(255,255,255,0.08);
--line-2: rgba(255,255,255,0.16);
--line-3: rgba(255,255,255,0.28);
/* single accent — Tweaks 可换 · 默认就是白 (Grok-style mono) */
--accent: #FFFFFF;
--accent-2: color-mix(in oklab, var(--accent) 40%, transparent);
--accent-3: color-mix(in oklab, var(--accent) 14%, transparent);
/* status · 极少使用 · 仅图表 */
--green: #00E07A; /* SpaceX 仪表绿 */
--red: #FF3333; /* abort red */
--yellow: #FFC700; /* caution yellow */
/* ---------- type · SpaceX × X × Grok stack ---------- */
--f-sans: "Space Grotesk", "Inter Tight", "PingFang SC", "Noto Sans SC", -apple-system, sans-serif;
--f-cond: "Barlow Semi Condensed", "Oswald", "Space Grotesk", sans-serif; /* big numbers / display */
--f-mono: "JetBrains Mono", "IBM Plex Mono", "Geist Mono", ui-monospace, monospace;
--f-cn: "Noto Sans SC", "PingFang SC", "HarmonyOS Sans SC", sans-serif;
/* type ramp — half-scale of 4K target */
--t-display: 96px; /* hero · spec sheet hero */
--t-h1: 56px; /* big numbers */
--t-h2: 32px;
--t-h3: 22px;
--t-h4: 17px;
--t-body: 15px;
--t-small: 13px;
--t-cap: 11px;
--t-meta: 10px;
/* weights — 跳过 500/700,对比悬崖 */
--w-reg: 400;
--w-mid: 600;
--w-bold: 700;
--w-heavy: 800;
/* letter-spacing — SpaceX 偏紧 + caps 偏松 */
--ls-display: -0.03em;
--ls-tight: -0.018em;
--ls-normal: -0.005em;
--ls-caps: 0.18em;
--ls-meta: 0.22em;
--ls-mission: 0.32em; /* T-MINUS 等任务字串极宽 */
/* ---------- motion ---------- */
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-in: cubic-bezier(0.55, 0, 1, 0.45);
--ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.36, 0.64, 1);
--d-1: 200ms;
--d-2: 400ms;
--d-3: 700ms;
--d-4: 1100ms;
/* ---------- radii — SpaceX 几何感:极小或 0 ---------- */
--r-0: 0px;
--r-1: 2px;
--r-2: 4px;
--r-3: 8px;
--space-1: 8px;
--space-2: 16px;
--space-3: 24px;
--space-4: 40px;
--space-5: 64px;
--space-6: 96px;
}
/* ---------- reset ---------- */
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--fg); }
body {
font-family: var(--f-sans);
font-size: var(--t-body);
font-weight: var(--w-reg);
letter-spacing: var(--ls-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-feature-settings: "ss01", "cv11", "tnum";
}
:lang(zh), .cn { font-family: var(--f-cn); }
.mono { font-family: var(--f-mono); letter-spacing: 0; font-variant-ligatures: none; }
.cond { font-family: var(--f-cond); letter-spacing: var(--ls-tight); font-feature-settings: "tnum" 1; }
.caps { text-transform: uppercase; letter-spacing: var(--ls-caps); font-family: var(--f-mono); font-weight: var(--w-mid); }
.meta { font-family: var(--f-mono); font-size: var(--t-meta); letter-spacing: var(--ls-meta); text-transform: uppercase; color: var(--fg-3); }
.mission { font-family: var(--f-mono); letter-spacing: var(--ls-mission); text-transform: uppercase; }
.serif { font-family: var(--f-sans); font-weight: var(--w-reg); font-style: italic; }
MIT License
Copyright (c) 2026 feicaiclub
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Related skills
How it compares
Use video-spec-builder over generic writing skills when the deliverable is a shoot-ready video specification with A-roll, B-roll, and on-screen visual components.
FAQ
What file does video-spec-builder produce?
video-spec.md with a storyboard table, plus optional design.md and tokens.css for custom HyperFrames themes.
How does it handle vague creative requests?
It rejects adjectives like high-end or techy and forces concrete visual, pacing, and per-shot information decisions.
Is Video Spec Builder safe to install?
skills.sh reports 1 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.