# vibevideo 用 HTML 写视频。一个视频 = 一个 `project.yaml`;组件、模板、素材全站共享。 渲染引擎是 [HyperFrames](https://github.com/heygen-com/hyperframes)(Apache-2.0,HeyGen 开源), **但它只占整个系统约 5% 的代码**,全部关在 `src/cli/hv.mjs` 里,随时可换。 --- ## 30 秒上手 ```bash node src/cli/hv.mjs 体检 # 环境体检 node src/cli/hv.mjs 新建 ep001 # 建项目 $EDITOR projects/ep001/project.yaml # 写内容 node src/cli/hv.mjs 生成 ep001 # 生成 index.html node src/cli/hv.mjs 看帧 ep001 # 抽几帧看效果(快) node src/cli/hv.mjs 渲染 ep001 # 出片 ``` 所有子命令都有英文别名(`hv new` / `hv build` / `hv render` …),中文只是习惯问题。 --- ## 一个项目长什么样 ``` projects/ep001/ ├── project.yaml ← 你唯一要写的文件 ├── BRIEF.md ← 选题、受众、一句话信息 ├── script.md ← 逐场景台词 ├── assets/project/ ← 私有素材(录屏、截图) └── out/ ← 成片 ``` `project.yaml`: ```yaml title: 三个 Cursor 实用技巧 aspect: 横屏 # 横屏 1920x1080 / 竖屏 1080x1920 / 方形 1080x1080 / 超宽 fps: 60 # 24 / 30 / 60 lang: zh-CN scenes: - component: intro duration: 3.5 props: kicker: 技巧合集 title: 三个 Cursor 实用技巧 subtitle: 让你写代码少一半时间 - component: codedemo duration: 8 props: filename: fib.py language: python highlight: "3,4,5" # 高亮这几行 dimOthers: "true" # 压暗其他行 code: | def fib(n): if n < 2: return n ``` 跑 `hv 生成` 后,它会编译成时间轴、挂载组件、同步资源。 --- ## 命令 | 命令 | 作用 | |---|---| | `hv 新建 <名> [--template 模板] [--aspect 画幅]` | 建项目 | | `hv 生成 <名>` | `project.yaml` → `index.html` | | `hv 检查 <名>` | 校验参数/时长 + 运行时/对比度/布局检查 | | `hv 渲染 <名> [--quality 草稿\|正式\|交付]` | 出片到 `out/` | | `hv 预览 <名>` | 浏览器实时预览 | | `hv 看帧 <名> [--时间 1,3,5]` | 抽帧,比整条渲染快得多 | | `hv 说明 <组件>` | 看某组件有哪些参数 | | `hv 列表 [项目\|组件\|模板]` | 列出已有内容 | | `hv 体检` | 依赖/字体/组件检查 | --- ## 组件库 | 组件 | 说明 | |---|---| | `intro` | 片头:标题、副标题入场 | | `outro` | 片尾:引导关注 | | `codedemo` | 代码演示:逐行打字、语法高亮、重点行高亮 | | `terminal` | 终端:命令逐字输入、输出分类着色 | | `lowerthird` | 下方字幕条 | | `textblock` | 文字块,支持 `**加粗**` 和 `[[强调色]]` | | `cursor` | 鼠标指针:移动、点击涟漪 | | `cad3d` | **三维钩子建模全过程**:草图→拉伸→切除→圆角 | | `stress` | 自由度压测:9 项超纲特性的渲染探针 | `hv 说明 codedemo` 看参数。组件的参数表直接写在组件自己的 `data-composition-variables` 里,带中文标签,`hv 检查` 会校验你传错的名字。 --- ## 分辨率与画幅 改 `project.yaml` 的 `aspect` 即可,**组件代码一行不用动**。 ```yaml aspect: 竖屏 # 1080x1920 # 或自定义 width: 2560 height: 1080 ``` 原理:所有尺寸都写成 `calc(N * var(--u))`,`--u` = 短边 / 1080, 所以 1080p 横竖都是 `1px`,4K 自动放大一倍。 > ⚠️ 组件里写 `width: var(--stage-w)` 是**非法的**(缺单位,浏览器会整条丢弃)。 > 无单位数字用在 `width/height` 上必须补 `* 1px`。这个坑踩过一次。 --- ## 写新组件 `src/components/<名字>/<名字>.html`,只有一个硬性要求: ```js window.__timelines["你的组件名"] = gsap.timeline({ paused: true }); ``` **就这一条。** 满足它,组件就能在任何渲染器里跑。 ### 两条铁律 1. **画面必须是时间 t 的纯函数。** 给定第 37 帧,直接画出第 37 帧长什么样,不依赖前面播过什么。 这是缓存、并行、秒级拖动预览的基础。 - ✅ `gsap` 时间轴 + `onUpdate` 里重画 - ✅ 3D 每帧手动清空重画 - ❌ `requestAnimationFrame` 累积状态 - ❌ CSS `@keyframes`(真实时间跑,抓到的是任意中间态) 2. **不准 import 渲染引擎的私有 API。** 组件只允许依赖:浏览器 + GSAP + 上面那条注册约定。 违反这条,将来换渲染引擎就得重写。 --- ## 当前能力边界(实测结论) **能做**:WebGL / Three.js、真实布尔运算(CSG)、mix-blend-mode、clip-path、 mask-image、3D 变换、SVG 描边动画、canvas 逐帧重绘、字体排版、音频混流。 **约束只有一条**:确定性渲染(见上面铁律 1)。 这不是某个框架的限制——Remotion 也一样,只是它把这个约束藏在你写 React 组件里, 你察觉不到。 **本机的性能实测**(12 核 i5,1080p): - 10s @ 30fps → 11.8s(5 个并行 worker,约等于实时) - 16s @ 30fps → 22s - 60fps 帧数翻倍,耗时也翻倍 --- ## 目录 ``` ├── src/ │ ├── cli/hv.mjs ★ 适配层:CLI + 时间轴编译(约 600 行) │ ├── cli/shot.mjs 抽帧 │ ├── components/ 组件库 │ ├── brand/ 设计规范:色板、字体、基准单位 │ ├── templates/ 视频结构套路 │ ├── audio/ Python:TTS 接口 + 程序化音效 │ └── vendor/ gsap / three / highlight.js ├── projects/ 视频项目 ├── tools/ 构建 vendor 的脚本 └── node_modules/ ``` --- ## 音频 配音、点击音、背景音都归这条链路管。**核心设计:场景时长 = 配音时长**, 所以你不用手工对轴——写完台词,跑一条命令,时间轴自己就对了。 ### 用法 ```bash python3 src/audio/cli.py 检查 # 音频侧体检 python3 src/audio/cli.py 配音 <项目> # 合成配音 + 对齐场景时长 python3 src/audio/cli.py 音效 <项目> # 合成音效 python3 src/audio/cli.py 混音 <项目> --bgm bgm.mp3 # 混成一条轨 node src/cli/hv.mjs 渲染 <项目> # 音轨自动挂上 python3 src/audio/verify_sync.py <项目> # 验证音画真的对齐 ``` 在 `project.yaml` 的场景里写 `narration` 即可: ```yaml - component: codedemo duration: 8 narration: "第二步拉伸,把这条轮廓沿 Z 轴扫成实体。" # 时长会被这句覆盖 sfx: "pop@1.2" # 第 1.2 秒一个音效 ``` 跑完 `配音` 后,时长会变成 `朗读时长 + 0.35s 留白`,并回写 `project.yaml`。 ### 换 TTS Provider 是抽象的,换服务只写一个类: ```python from tts import TTSProvider, AudioClip, register class MyTTS(TTSProvider): name = "我的服务" def synthesize(self, text, voice="", out_dir=None) -> AudioClip: path = call_your_api(text, voice) return AudioClip(path=path, duration=real_duration, text=text, voice=voice, provider=self.name) register("mine", MyTTS) ``` 然后 `配音 <项目> --provider mine`。**画面侧一行都不用动。** 已内置: | Provider | 说明 | |---|---| | `dummy` | 不发声,但产出**精确时长的合法 wav**。链路先跑通,不是 mock | | `kokoro` | 本地离线 Kokoro-82M,不联网、结果可复现 | > ⚠️ `duration` 必须真实,不能估算——整条时间轴的对齐都依赖它。 > DummyTTS 的价值就在于:现在就能验证音画链路,不用等真实 TTS。 ### 音效是算出来的,不是找来的 `src/audio/sfx/synth.py` 用 numpy 程序化合成:点击、提示音、转场、落位、气泡。 体积为零、参数随便调、能跟着画面的强调色走。 ### 验证对齐 `verify_sync.py` 会在每个场景起点铺一个可闻的标记音,渲染成片后逐点测能量峰。 长度对但偏移了照样是错的,所以必须这样测。 --- ## 为什么用 HyperFrames | | HyperFrames | 自己造 / Remotion | |---|---|---| | 成熟度 | Apache-2.0,HeyGen 在用 | 从零三个月起步 | | 许可证 | 免费 | Remotion 按人头 $25/月,含 agent 写码 | | 并行渲染 | 5 worker 自带 | 自己实现 | | 对比度/布局检查 | `check` 自带 | 自己实现 | **但真正的理由是隔离**:引擎的全部知识只存在于 `src/cli/hv.mjs`。 你的组件不知道引擎的存在。哪天要换,改一个文件,组件全部原样保留。