diff --git a/.fallowrc.jsonc b/.fallowrc.jsonc index 52dc3e3adf..ba258c7e0e 100644 --- a/.fallowrc.jsonc +++ b/.fallowrc.jsonc @@ -79,6 +79,8 @@ "skills/**/fonts/**", // Golden snapshot files: data consumed by toMatchFileSnapshot, not importable modules. "packages/**/__goldens__/**", + // macOS Finder duplicates (e.g. "keyframes 2.ts") — not project source. + "**/* 2.*", "registry/**", "examples/**", "packages/sdk/examples/**", @@ -209,6 +211,18 @@ "refreshDomEditSelection", ], }, + // GestureRecordBadge: public API exported for use by external composition + // overlays; dom editing overlay integration planned in follow-up PR. + { + "file": "packages/studio/src/components/editor/GestureRecordControl.tsx", + "exports": ["GestureRecordBadge"], + }, + // DeleteConfirm: shared confirmation dialog component, consumed by the + // FileTree barrel and sibling components via dynamic resolution. + { + "file": "packages/studio/src/components/sidebar/AssetContextMenu.tsx", + "exports": ["DeleteConfirm"], + }, // domEditing barrel: re-exports consumed throughout the studio but // fallow's static analyzer can't trace re-exports through barrel files. { @@ -499,6 +513,14 @@ // scopeRootSelectors handling shifts lines and re-flags both. "packages/core/src/compiler/inlineSubCompositions.ts", "packages/core/src/compiler/compositionScoping.test.ts", + // i18n branch: pre-existing internal clones re-flagged by import-path + // line shifts (no logic changed). + "packages/studio/src/components/LintModal.tsx", + "packages/studio/src/components/editor/FileTree.tsx", + "packages/studio/src/components/editor/FileTreeNodes.tsx", + "packages/studio/src/hooks/useElementLifecycleOps.ts", + "packages/studio/src/hooks/useGroupCommits.ts", + "packages/studio/src/components/sidebar/LeftSidebar.tsx", ], }, "health": { @@ -564,10 +586,21 @@ // vs base-var args, validation throws) but small and well-tested via the // generated table's shape test; this is dev tooling, not shipped runtime. "packages/cli/scripts/sync-agent-dirs.ts", - // Files modified only for import-path updates (one-line changes to switch - // from @hyperframes/core/* subpaths to the new packages). Their complexity - // is pre-existing; the line-shift fingerprint problem makes fallow treat - // the violations as new even though no logic changed. + // check-hardcoded-strings.mjs: pre-existing complexity in scanFile, + // shouldSkipLine, looksLikeEnglish, and walk — all pre-date the lint fix + // (escaping `\-`). The line-shift re-flags them as inherited. + "packages/studio/scripts/check-hardcoded-strings.mjs", + // i18n branch: pre-existing complexity in files with import-path line + // shifts (no logic changed). + "packages/studio/src/hooks/useClipboard.ts", + "packages/studio/src/components/StudioHeader.tsx", + "packages/studio/src/hooks/useRazorSplit.ts", + "packages/studio/src/components/editor/propertyPanelSections.tsx", + "packages/studio/src/components/editor/MotionPathOverlay.tsx", + // Files modified only for import-path updates (one-line changes to switch + // from @hyperframes/core/* subpaths to the new packages). Their complexity + // is pre-existing; the line-shift fingerprint problem makes fallow treat + // the violations as new even though no logic changed. "packages/cli/src/server/studioServer.ts", // findPortAndServe's existing port-selection flow predates this PR. The // preview-lifecycle change only adds PID metadata to the config response, diff --git a/.gitignore b/.gitignore index 21053dc046..b2198bdb9a 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ videos/ # IDE .vscode/ .idea/ +.cursor/ *.swp *.swo @@ -153,5 +154,8 @@ tmp/ # Studio-generated preview thumbnails .thumbnails/ +# macOS Finder duplicates +* 2.* + # Local editor settings .zed/ diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000000..52a48f5067 --- /dev/null +++ b/README_zh.md @@ -0,0 +1,286 @@ +

+ + + + HyperFrames + +

+ +

+ npm version + npm downloads + License + Node.js + Discord +

+ +

编写 HTML。渲染视频。为 Agent 而生。

+ +

+ 快速入门 | + 作品展示 | + Playground | + 目录 | + 文档 | + Discord +

+ +

+ HyperFrames 演示:左侧 HTML 代码转换为右侧渲染视频 +

+ +HyperFrames 是一个开源框架,可将 HTML、CSS、媒体与可寻址动画(seekable animations)转换为确定性的 MP4 视频。你可以在本地通过 CLI 使用,通过 AI 编程 Agent 配合 skills 使用,或作为托管创作工作流背后的渲染核心。 + +## 快速入门 + +### 使用 AI 编程 Agent + +安装 HyperFrames skills,然后描述你想要的视频: + +```bash +npx skills add heygen-com/hyperframes +``` + +可以尝试这样的提示词: + +> 使用 `/hyperframes`,创建一个 10 秒的产品介绍视频,包含淡入标题、背景视频和轻柔的背景音乐。 + +这些 skills 会教 Agent 掌握 HyperFrames 的制作流程:规划视频、编写合法 HTML、接入可寻址动画、添加媒体、lint、预览和渲染。它们适用于 Claude Code、Cursor、Gemini CLI、Codex 以及其他支持 skills 的编程 Agent。 + +关于视觉设计交接工作流,请参阅 [Claude Design 指南](https://hyperframes.heygen.com/guides/claude-design) 和 [Open Design 指南](https://hyperframes.heygen.com/guides/open-design)。 + +### 手动使用 CLI + +```bash +npx hyperframes init my-video +cd my-video +npx hyperframes preview # preview in browser with live reload +npx hyperframes render # render to MP4 +``` + +**要求:** Node.js 22+、FFmpeg + +## 你可以构建什么 + +需要灵感?浏览 [作品展示](https://hyperframes.heygen.com/showcase),观看、阅读、运行并改编已完成的视频。 + +- 产品发布视频与功能公告 +- 带动画代码 diff、旁白和字幕的 PR 演示 +- 数据可视化、图表竞赛与地图动画 +- 带动态字幕、叠加层和音乐的社会化视频 +- 文档转视频、PDF 转视频、网站转视频讲解 +- 用于自动化内容流水线的可复用动态图形 + +## Frame.md + +**frame.md — 你的设计系统,已为视频就绪。** + +每个品牌都有 `design.md`,但都不是为镜头而写。`frame.md` 是缺失的转换层:它接收你的 Web 上下文设计规范,并为画面(frame)做反向适配——同样的 token、同样的规则,但重写后 AI Agent 可以创作宣传视频,而无需猜测缩放或依赖 Web 界面元素。 + +输出是整套工具链都能读取的 `DESIGN.md` 超集。原子级元素保持神圣不可侵犯。构图保持自由。数值来自脚本。 + + + + + + + + + + + + + + + + + + + + + + +
+ Biennale Yellow +
Biennale Yellow +
+ BlockFrame +
BlockFrame +
+ Blue Professional +
Blue Professional +
+ Bold Poster +
Bold Poster +
+ Broadside +
Broadside +
+ Capsule +
Capsule +
+ Cartesian +
Cartesian +
+ Cobalt Grid +
Cobalt Grid +
+ Coral +
Coral +
+ Creative Mode +
Creative Mode +
+ +在 [hyperframes.dev/design](https://www.hyperframes.dev/design) 浏览并改编全部模板。 + +## 工作原理 + +将视频定义为 HTML。用 data 属性标注时间与轨道。使用 GSAP、CSS、Lottie、Three.js、Anime.js、WAAPI 或你自己的 frame adapter 实现可寻址动画。 + +```html +
+ + +

Launch day

+ + + + + +
+``` + +在浏览器中即时预览。在本地或 Docker 中渲染。渲染器在无头 Chrome 中逐帧寻址,并用 FFmpeg 编码结果,因此相同输入产生相同视频。 + +## HyperFrames 技术栈 + +HyperFrames 是开源渲染引擎,以及围绕 HTML 原生视频创作的一整套不断扩展的工具。 + +| 组件 | 状态 | 功能说明 | +| ----------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------- | +| CLI | 可用 | 搭建、预览、lint、检查并渲染本地视频项目 | +| Core / Engine / Producer | 可用 | 解析合成、驱动无头 Chrome、编码视频并混音 | +| Catalog | 可用 | 可复用的 blocks 与 components:转场、叠加层、字幕、图表、地图与特效 | +| Agent skills | 可用 | 教编程 Agent 掌握通用 Web 文档未涵盖的视频制作模式 | +| Studio | 可用,持续演进 | 用于预览与编辑合成的浏览器界面 | +| AWS Lambda rendering | 可用 | 部署分布式渲染栈,从笔记本或 CI 驱动渲染 | +| [hyperframes.dev](https://www.hyperframes.dev/) | 可用 | 社区 Playground:预览、迭代、分享并渲染 HTML 原生视频项目 | +| [frame.md](https://www.hyperframes.dev/design) | 可用 | 将设计系统反向适配为镜头语言——Agent 可据此创作视频的 DESIGN.md 超集 | + +## 目录 + +安装即用 blocks 与 components: + +```bash +npx hyperframes add flash-through-white # shader transition +npx hyperframes add instagram-follow # social overlay +npx hyperframes add data-chart # animated chart +``` + +在 [hyperframes.heygen.com/catalog](https://hyperframes.heygen.com/catalog/blocks/data-chart) 浏览目录。 + +## 为什么选择 HyperFrames? + +- **HTML 原生:** 合成即带 data 属性的 HTML 文件。无需 React,无专有时间线格式。 +- **Agent 友好:** Agent 本来就会写 HTML,且 CLI 默认非交互式。 +- **确定性:** 相同输入、相同帧、相同输出。为 CI、回归测试与自动化渲染而设计。 +- **无构建步骤:** `index.html` 合成可直接播放,并在浏览器中直接预览。 +- **基于适配器的动画:** 支持 GSAP、CSS 动画、Lottie、Three.js、Anime.js、WAAPI 或自定义运行时。 +- **开源:** Apache 2.0 许可证,无按次渲染费用或商业使用门槛。 + +## HyperFrames vs Remotion + +HyperFrames 受 [Remotion](https://www.remotion.dev) 启发。两者都用无头 Chrome 与 FFmpeg 渲染视频。主要区别在于创作模型:Remotion 押注 React 组件;HyperFrames 押注人类与 Agent 都能轻松编写的纯 HTML。 + +| | **HyperFrames** | **Remotion** | +| ------------------------ | ------------------------------------- | --------------------------------------- | +| 创作方式 | HTML + CSS + 可寻址动画 | React 组件 | +| 构建步骤 | 无;`index.html` 可直接播放 | 需要打包工具 | +| Agent 交接 | 纯 HTML 文件 | JSX / React 项目 | +| 库时钟动画 | 通过适配器可寻址、逐帧精确 | 墙钟动画模式需额外注意 | +| 分布式渲染 | 本地与 AWS Lambda 渲染路径 | Remotion Lambda,成熟的云渲染器 | +| 许可证 | Apache 2.0 | 源码可用的 Remotion License | + +完整对比见 [HyperFrames vs Remotion 指南](https://hyperframes.heygen.com/guides/hyperframes-vs-remotion)。 + +## 文档 + +完整文档:[hyperframes.heygen.com/introduction](https://hyperframes.heygen.com/introduction) + +- [快速入门](https://hyperframes.heygen.com/quickstart) +- [作品展示](https://hyperframes.heygen.com/showcase) +- [指南](https://hyperframes.heygen.com/guides/gsap-animation) +- [API 参考](https://hyperframes.heygen.com/packages/core) +- [目录](https://hyperframes.heygen.com/catalog/blocks/data-chart) +- [示例](https://hyperframes.heygen.com/examples) +- [AWS Lambda 渲染](https://hyperframes.heygen.com/deploy/aws-lambda) + +## 软件包 + +| 软件包 | 说明 | +| ---------------------------------------------------------------- | ----------------------------------------------------------------- | +| [`hyperframes`](packages/cli) | 用于创建、预览、lint 与渲染合成的 CLI | +| [`@hyperframes/core`](packages/core) | 类型、解析器、生成器、linter、运行时与 frame adapters | +| [`@hyperframes/engine`](packages/engine) | 基于 Puppeteer 与 FFmpeg 的可寻址页面到视频捕获引擎 | +| [`@hyperframes/producer`](packages/producer) | 完整渲染流水线:捕获、编码与音频混音 | +| [`@hyperframes/studio`](packages/studio) | 基于浏览器的合成编辑器 UI | +| [`@hyperframes/player`](packages/player) | 可嵌入的 `` Web 组件 | +| [`@hyperframes/shader-transitions`](packages/shader-transitions) | 用于合成的 WebGL shader 转场 | +| [`@hyperframes/aws-lambda`](packages/aws-lambda) | 用于分布式渲染的 AWS Lambda SDK 与部署面 | + +## 社区 + +HyperFrames 已在 [HeyGen](https://www.heygen.com) 投入生产使用;[tldraw](https://tldraw.com)、[TanStack](https://tanstack.com) 等团队的社区示例见 [ADOPTERS.md](ADOPTERS.md)。若你的团队在使用 HyperFrames,欢迎提交 PR。 + +- 问题与想法:[Discord](https://discord.gg/EbK98HBPdk) +- Bug 与功能请求:[GitHub Issues](https://github.com/heygen-com/hyperframes/issues) +- 安全报告:[SECURITY.md](SECURITY.md) +- 贡献指南:[CONTRIBUTING.md](CONTRIBUTING.md) + +## 开发说明 + +本仓库对 `packages/producer/tests/**/output.mp4` 下的黄金回归测试基线使用 [Git LFS](https://git-lfs.com)(约 240 MB 的 `.mp4` 文件)。若需克隆完整仓库进行开发,请先安装 Git LFS: + +```bash +# macOS +brew install git-lfs + +# Ubuntu / Debian +sudo apt install git-lfs + +# Windows +winget install GitHub.GitLFS + +# Then, once per machine +git lfs install +``` + +若只需源码文件,可跳过 LFS 内容: + +```bash +GIT_LFS_SKIP_SMUDGE=1 git clone https://github.com/heygen-com/hyperframes.git +``` + +## 许可证 + +[Apache 2.0](LICENSE) diff --git a/bun.lock b/bun.lock index 77c87f2c0b..5a496d6856 100644 --- a/bun.lock +++ b/bun.lock @@ -22,7 +22,7 @@ }, "packages/aws-lambda": { "name": "@hyperframes/aws-lambda", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@aws-sdk/client-s3": "^3.700.0", "@aws-sdk/client-sfn": "^3.700.0", @@ -54,7 +54,7 @@ }, "packages/cli": { "name": "@hyperframes/cli", - "version": "0.7.48", + "version": "0.7.57", "bin": { "hyperframes": "./dist/cli.js", }, @@ -103,7 +103,7 @@ }, "packages/core": { "name": "@hyperframes/core", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@chenglou/pretext": "^0.0.5", "@hyperframes/lint": "workspace:*", @@ -128,7 +128,7 @@ }, "packages/engine": { "name": "@hyperframes/engine", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@hono/node-server": "^1.13.0", "@hyperframes/core": "workspace:^", @@ -146,7 +146,7 @@ }, "packages/gcp-cloud-run": { "name": "@hyperframes/gcp-cloud-run", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@google-cloud/storage": "^7.14.0", "@google-cloud/workflows": "^4.2.0", @@ -166,7 +166,7 @@ }, "packages/lint": { "name": "@hyperframes/lint", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@hyperframes/parsers": "workspace:*", "htmlparser2": "^10.1.0", @@ -183,7 +183,7 @@ }, "packages/parsers": { "name": "@hyperframes/parsers", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@babel/parser": "^7.27.0", "acorn": "^8.17.0", @@ -203,7 +203,7 @@ }, "packages/player": { "name": "@hyperframes/player", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@hyperframes/core": "workspace:*", }, @@ -218,7 +218,7 @@ }, "packages/producer": { "name": "@hyperframes/producer", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@fontsource/archivo-black": "^5.2.8", "@fontsource/eb-garamond": "^5.2.7", @@ -263,7 +263,7 @@ }, "packages/sdk": { "name": "@hyperframes/sdk", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@hyperframes/core": "workspace:*", "@hyperframes/parsers": "workspace:*", @@ -293,7 +293,7 @@ }, "packages/shader-transitions": { "name": "@hyperframes/shader-transitions", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "html2canvas": "^1.4.1", }, @@ -305,7 +305,7 @@ }, "packages/studio": { "name": "@hyperframes/studio", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@codemirror/autocomplete": "^6.20.1", "@codemirror/commands": "^6.10.3", @@ -327,8 +327,10 @@ "bpm-detective": "^2.0.5", "dompurify": "^3.2.4", "gsap": "^3.13.0", + "i18next": "^26.3.6", "marked": "^14.1.4", "mediabunny": "^1.45.3", + "react-i18next": "^17.0.9", }, "devDependencies": { "@hyperframes/producer": "workspace:*", @@ -353,7 +355,7 @@ }, "packages/studio-server": { "name": "@hyperframes/studio-server", - "version": "0.7.48", + "version": "0.7.57", "dependencies": { "@hyperframes/core": "workspace:*", "@hyperframes/parsers": "workspace:*", @@ -1590,6 +1592,8 @@ "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], + "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + "html2canvas": ["html2canvas@1.4.1", "", { "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" } }, "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA=="], "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], @@ -1600,6 +1604,8 @@ "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "i18next": ["i18next@26.3.6", "", { "peerDependencies": { "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["typescript"] }, "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -1890,6 +1896,8 @@ "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="], + "react-i18next": ["react-i18next@17.0.9", "", { "dependencies": { "@babel/runtime": "^7.29.2", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 26.2.0", "react": ">= 16.8.0", "react-dom": "*", "react-native": "*", "typescript": "^5 || ^6 || ^7" }, "optionalPeers": ["react-dom", "react-native", "typescript"] }, "sha512-buLzOSqHtXxjf+qgSrLWNTXVZ1jSwO6kUv3uJqSP1roGBPgNnbhFm7OmdVwWcgf2gIbUyP0J333uPyx+Btsi3w=="], + "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], @@ -2092,6 +2100,8 @@ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "utrie": ["utrie@1.0.2", "", { "dependencies": { "base64-arraybuffer": "^1.0.2" } }, "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw=="], @@ -2104,6 +2114,8 @@ "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], diff --git a/docs/project-overview.md b/docs/project-overview.md new file mode 100644 index 0000000000..fbf8663e18 --- /dev/null +++ b/docs/project-overview.md @@ -0,0 +1,632 @@ +# HyperFrames — 开源 HTML 视频渲染框架 + +## 1. 项目定位 + +**HyperFrames** 是一个开源框架,核心理念是 **"Write HTML. Render video."**(编写 HTML,渲染视频)。它让开发者/AI Agent 用 HTML、CSS、JavaScript 编写可定位的动画页面,然后将同一份 HTML 确定性(deterministically)渲染为 MP4 视频文件。由 HeyGen 开源并用于生产环境。 + +它解决了传统视频制作的痛点:无需掌握 Premiere/After Effects,无需学习 React(像 Remotion 那样),只需写 HTML 就能生成视频。特别适合 AI Agent 自动生成视频内容、自动化内容管线、以及开发者驱动的视频制作场景。 + +## 2. 核心功能 + +- **HTML 原生视频创作** — 用 HTML + CSS + 数据属性定义视频合成(composition),无需构建工具 +- **确定性渲染** — 相同的 HTML 输入始终产生相同的逐帧输出,适合 CI/回归测试 +- **多动画框架适配** — 支持 GSAP、CSS 动画、Lottie、Three.js、Anime.js、WAAPI 等 +- **CLI 工具链** — 项目初始化、预览(热重载)、代码检查、渲染输出一条龙 +- **浏览器预览** — 在浏览器中实时预览动画,所见即所得 +- **音视频处理** — 视频帧注入、音频混合、音量包络、多轨道支持 +- **Shader 过渡** — WebGL 着色器过渡效果 +- **分布式渲染** — 支持 AWS Lambda 大规模分布式渲染 +- **HDR 渲染** — 支持 HDR10/PQ 色域、HDR 视频编码 +- **可嵌入播放器** — `` Web Component,可嵌入任何页面 +- **Studio 编辑器** — 浏览器端的合成编辑器 UI +- **Agent 技能包** — 教导 AI 编码助手(Claude Code、Cursor 等)理解视频创作模式 +- **frame.md 设计系统** — 将品牌设计规范转化为视频创作可用的设计令牌(design tokens) +- **Catalog 组件市场** — 可复用的过渡、叠加层、字幕、图表等组件 + +## 3. 项目架构 + +### 3.1 整体架构图 + +```mermaid +%% 图说明: HyperFrames 整体模块关系图 +graph TD + subgraph User["用户层"] + CLI["CLI 命令行工具
hyperframes CLI"] + Agent["AI Agent
Claude Code / Cursor"] + Studio["Studio 编辑器
浏览器 UI"] + Player["播放器
"] + end + + subgraph Core["核心层 - @hyperframes/core"] + Types["类型系统
FPS、Timeline、Canvas"] + Parser["HTML 解析器
parseHtml()"] + GSAP_Parser["GSAP 解析器
parseGsapScript()"] + Runtime["运行时
Runtime (浏览器内)"] + Linter["代码检查器
lintHyperframeHtml()"] + Generators["HTML 生成器
generateHyperframesHtml()"] + Adapters["帧适配器
GSAP / CSS / Lottie / Three / Anime / WAAPI"] + Compiler["时序编译器
compileTimingAttrs()"] + end + + subgraph Engine["捕获引擎 - @hyperframes/engine"] + Browser["浏览器管理
acquireBrowser()"] + FrameCapture["帧捕获
captureFrame()"] + Screenshot["截图服务
BeginFrame API"] + VideoExtract["视频帧提取
extractVideoFramesRange()"] + AudioMixer["音频混合器
processCompositionAudio()"] + ChunkEncoder["分块编码器
encodeFramesChunkedConcat()"] + StreamingEncoder["流式编码器
spawnStreamingEncoder()"] + HDR["HDR 捕获
initHdrReadback()"] + Parallel["并行捕获
executeParallelCapture()"] + end + + subgraph Producer["编排层 - @hyperframes/producer"] + Orchestrator["渲染编排器
executeRenderJob()"] + Server["HTTP 服务
POST /render"] + Distributed["分布式渲染
plan() / renderChunk() / assemble()"] + CompileStage["阶段1: 编译"] + ProbeStage["阶段1b: 探测"] + ExtractStage["阶段2: 视频提取"] + AudioStage["阶段3: 音频"] + CaptureStage["阶段4: 捕获"] + EncodeStage["阶段5: 编码"] + AssembleStage["阶段6: 组装"] + end + + subgraph Deploy["部署 - @hyperframes/aws-lambda"] + Lambda["AWS Lambda 渲染"] + end + + CLI --> Core + CLI --> Producer + Agent --> CLI + Studio --> Core + Player --> Core + Producer --> Engine + Core --> Parser + Core --> GSAP_Parser + Core --> Compiler + Core --> Linter + Core --> Runtime + Engine --> Browser + Engine --> FrameCapture + Producer --> Orchestrator + Orchestrator --> CompileStage + CompileStage --> ProbeStage + ProbeStage --> ExtractStage + ExtractStage --> AudioStage + AudioStage --> CaptureStage + CaptureStage --> EncodeStage + EncodeStage --> AssembleStage + Distributed --> Lambda + Server --> Orchestrator +``` + +### 3.2 核心渲染流程 + +```mermaid +%% 图说明: 从 HTML 到 MP4 的完整渲染流水线 +sequenceDiagram + participant User as 用户/Agent + participant CLI as hyperframes CLI + participant Producer as Producer (编排器) + participant Engine as Engine (捕获引擎) + participant Chrome as Headless Chrome + participant FFmpeg as FFmpeg 编码器 + + User->>CLI: hyperframes render + + CLI->>Producer: 创建 RenderJob
(fps, quality, 项目目录) + + Note over Producer: 阶段 1: 编译 + Producer->>Producer: 解析 HTML → 提取 Timeline 元素
(视频/音频/文本/合成) + Producer->>Producer: 解析 GSAP 脚本 → 提取动画定义 + + Note over Producer: 阶段 1b: 探测 + Producer->>Engine: 启动 Headless Chrome + Engine->>Chrome: 打开页面 + Chrome->>Chrome: 注入运行时脚本 (hyperframesRuntime) + Chrome-->>Engine: 返回 composition duration + + Note over Producer: 阶段 2: 提取视频帧 + Producer->>Engine: 提取所有视频元素的外部帧 + Engine->>FFmpeg: ffprobe 获取视频元数据 + Engine->>FFmpeg: ffmpeg 逐帧提取(按时间范围) + + Note over Producer: 阶段 3: 处理音频 + Producer->>Engine: 解析音频元素 + Engine->>Engine: 混合多轨音频、应用音量包络 + + Note over Producer: 阶段 4: 捕获视频帧 + loop 每帧 (fps * duration) + Producer->>Chrome: seek(time) + Chrome->>Chrome: 驱动所有动画到该时间点 + Chrome-->>Producer: 截图 (BeginFrame) + Producer->>Producer: 注入视频帧 / 合成图层 + end + + Note over Producer: 阶段 5: 编码 + Producer->>FFmpeg: 将所有帧编码为无音频视频 + FFmpeg-->>Producer: 编码后的视频流 + + Note over Producer: 阶段 6: 组装 + Producer->>FFmpeg: 混流(视频 + 音频轨道) + FFmpeg-->>Producer: 最终 MP4 文件 + + Producer-->>CLI: 渲染完成 ✓ + CLI-->>User: output.mp4 +``` + +### 3.3 运行时协议与页面适配器 + +```mermaid +%% 图说明: HyperFrames 的运行时协议 (HfProtocol) 和帧适配器架构 +classDiagram + class HfProtocol { + +duration: number + +seek(time: number): void + +media: HfMediaElement[] + +transitions: HfTransitionMeta[] + } + + class FrameAdapter { + +name: string + +type: string + +seek(time: number, duration: number): void + +parse(doc: Document): void + +getTimeline(): RuntimeTimelineLike + } + + class RuntimePlayer { + +play() + +pause() + +seek(time: number) + +getTime(): number + +getDuration(): number + } + + class TransportClock { + -baseTime: number + -playStartMs: number + +now(): number + +play(): boolean + +pause(): boolean + +seek(timeSeconds): void + } + + class GSAPAdapter { + +name: "gsap" + +seek(time, duration): void + +parse(doc): void + } + + class CssAdapter { + +name: "css" + +seek(time, duration): void + +parse(doc): void + } + + class LottieAdapter { + +name: "lottie" + +seek(time, duration): void + +parse(doc): void + } + + class ThreeAdapter { + +name: "three" + +seek(time, duration): void + +parse(doc): void + } + + class AnimeJsAdapter { + +name: "animejs" + +seek(time, duration): void + +parse(doc): void + } + + class WaapiAdapter { + +name: "waapi" + +seek(time, duration): void + +parse(doc): void + } + + Window "1" -- "1" HfProtocol : window.__hf + FrameAdapter <|-- GSAPAdapter + FrameAdapter <|-- CssAdapter + FrameAdapter <|-- LottieAdapter + FrameAdapter <|-- ThreeAdapter + FrameAdapter <|-- AnimeJsAdapter + FrameAdapter <|-- WaapiAdapter + RuntimePlayer --> TransportClock : 使用 + RuntimePlayer --> FrameAdapter : 驱动 + HfProtocol --> FrameAdapter : 通过 adapter.seek() 实现 seek() +``` + +## 4. 核心技术栈 + +| 技术 | 用途 | +| ---------------------------------------------------- | ------------------------------ | +| **TypeScript** | 全项目使用,强类型系统 | +| **Bun** | 包管理器、构建工具、测试运行器 | +| **Headless Chrome (Puppeteer)** | HTML 页面渲染和截图捕获 | +| **FFmpeg** | 视频编码、音频混合、帧提取 | +| **Chrome BeginFrame API** | 确定性帧捕获(同步帧生成) | +| **Hono** | Producer HTTP 服务框架 | +| **citty** | CLI 命令定义框架 | +| **linkedom** | 服务端 HTML 解析(轻量 DOM) | +| **GSAP** | 首推动画库(可定位时间线) | +| **Three.js / Lottie / CSS / Anime.js / WAAPI** | 其他支持的动画框架 | +| **WebGL** | 着色器过渡效果 | +| **oxlint / oxfmt** | 代码检查与格式化 | +| **tsup** | 构建打包 | +| **AWS Lambda** | 分布式渲染部署 | +| **PostgreSQL + Drizzle ORM** | Studio 后端数据存储 | +| **Lit** | Web Component(player) | +| **Vite + React** | Studio 前端 | + +## 5. 核心原理 + +### 5.1 关键设计思想:Seekable Animation(可定位动画) + +HyperFrames 最核心的设计是 **"可定位动画"** 而非传统的"逐帧播放": + +```python +def render_video(html_content: str, fps: int, duration: float) -> bytes: + """ + HyperFrames 视频渲染核心逻辑 + + 不是"逐帧播放并录像",而是"逐帧定位并截图"。 + 每一帧的渲染互相独立,因此可以并行。 + """ + # 1. 准备阶段:在 Chrome 中加载 HTML + page = open_page(html_content) + page.inject_runtime_script() # 注入 hyperframesRuntime + + # 2. 获取页面暴露的 seek 协议 + protocol = page.evaluate("window.__hf") + # protocol = { duration: 10, seek: fn, media: [...] } + + # 3. 并行渲染每一帧(帧间无依赖) + all_frames = [] + total_frames = fps * protocol.duration + + for time_s in all_frame_times(fps, protocol.duration): + # 每一帧都 seek 到精确时间点 + page.evaluate(f"window.__hf.seek({time_s})") + + # 注入外部视频帧(浏览器本身不播放