HyperFrames v0.6.114 深度解析:端到端 Slideshow 交互式演示的完整实现
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
本篇文章围绕 HyperFrames v0.6.114(2026-06-19 发布)的核心新特性展开:该版本为项目引入了端到端的 Slideshow(幻灯片演示)能力,从播放器侧的控制器状态机与<hyperframes-slideshow>Web 组件、Studio 中的分支编辑器面板与 Manifest 持久化,到面向 Agent 的创作技能指导与示例 Deck,一次交付了"编写 HTML、渲染视频"项目中的全新交互形态。读完本文,你将理解 Slideshow 与普通线性视频的本质区别,掌握hyperframes present的命令行用法、Manifest(JSON island)的完整字段结构、控制器状态机的导航与分支逻辑,以及如何在 Studio 中构建主路径、片段揭示(fragment)、分支(branch)与热点(hotspot)。
版本概览:v0.6.114 交付了什么
v0.6.114 是 HyperFrames 项目中 Slideshow 功能的一次集中交付,其核心主张是:幻灯片演示是"可现场导航的演示"(a live, navigable presentation),而不是一段线性视频。该版本的提交内容可划分为四个层面:
| 层面 | 交付内容 | 对应源码/文档 |
|---|---|---|
| Player | <hyperframes-slideshow>Web 组件 + presenter 布局 | packages/player/src/slideshow/hyperframes-slideshow.ts |
| Player | Slideshow 控制器状态机(导航/分支/恢复) | packages/player/src/slideshow/SlideshowController.ts |
| Studio | Slideshow 分支编辑器面板 UI + Manifest 持久化与面板辅助函数 | packages/studio/src/components/panels/SlideshowPanel.tsx、packages/studio/src/hooks/useSlideshowPersist.ts |
| 技能/示例 | Slideshow 创作技能指导 + 独立 harness 参考 + 示例 Deck(airbnb 版、startup pitch、fixture) | skills/hyperframes/references/routes/slideshow.md |
发布说明同时包含两轮代码评审修复(PR #1580–1584 与 #1594),保证拆分提交后的功能完整性。
理解 Slideshow:可交互 Deck,而非线性 MP4
在深入源码前,先明确该功能的使用边界。HyperFrames 的 Slideshow 与普通视频工作流有本质区别:
- Slideshow 保持交互性。结果是"实时的、可导航的 Deck",包含逐键到达的 fragment、跳转到分支幻灯片的 hotspot、精确返回离开时片段的返回控件、带演讲者备注与计时器的 presenter 模式。
- Slideshow 目前不能导出为单段线性 MP4。如果最终产物需要从头到尾自动播放为 MP4,应改用视频工作流。
- 切勿对 Deck 执行
hyperframes render。该命令不会失败——因为 Deck 没有主根合成(master root composition),渲染只会解析第一张幻灯片,并写出一段静默截断的 MP4(例如 40 秒的 Deck 只得到 6 秒,且无任何警告)。现场 Deck 应使用hyperframes present,逐帧静态图应使用hyperframes snapshot。
这一约束在 CLI 实现中也有印证:hyperframes present命令会先检查项目index.html中是否存在 slideshow island(<script type="application/hyperframes-slideshow+json">),不存在或 JSON 非法时直接报错退出,而不是静默产出残缺文件(见 packages/cli/src/commands/present.ts)。
Slideshow Manifest:JSON island 的数据契约
Slideshow 的所有编排数据都存放在合成 HTML 里的一个 JSON island 中,其字段定义在 packages/parsers/src/slideshow/slideshow.types.ts。当前 schema 版本号为1(SLIDESHOW_MANIFEST_VERSION = 1),持久化时会打上版本戳,以便未来 schema 变更时检测并迁移旧 island。
顶层结构
export interface SlideshowManifest { version?: number; // schema 版本,缺失视为 1 slides: SlideRef[]; // 主路径(main line)幻灯片 slideSequences?: SlideSequence[]; // 分支序列(branch sequences) }幻灯片引用 SlideRef
export interface SlideRef { sceneId: string; // 引用的场景 ID startTime?: number; // 可选:显式起始时间 endTime?: number; // 可选:显式结束时间 notes?: string; // 演讲者备注 fragments?: number[]; // 片段揭示的停留时间点(hold-points) hotspots?: SlideHotspot[]; autoplay?: boolean; // true 时进入幻灯片自动播放其首个 <video> // 保留字段(TTS 已推迟):解析并携带,但从不消费 ttsScript?: string; ttsAudioUrl?: string; ttsDurationMs?: number; }关于autoplay的语义,源码注释给出了精确的边界:它只在进入幻灯片时自动播放该幻灯片的首个<video>(适合"视频即幻灯片主体内容"的场景),Slideshow 仍然保持停留、绝不自走——是否前进始终由 presenter 点击 Next 决定;它不应用于背景/氛围类剪辑。
热点与分支
export interface SlideHotspot { id: string; label: string; target: string; // 引用某个 SlideSequence.id region?: { x: number; y: number; w: number; h: number }; // 幻灯片内百分比坐标 } export interface SlideSequence { id: string; label: string; slides: SlideRef[]; }解析后的类型
export interface ResolvedSlide extends SlideRef { start: number; end: number; fragments: number[]; // 恒存在、已排序、默认为 [] hotspots: SlideHotspot[]; // 恒存在、默认为 [] }解析阶段会把每条SlideRef与播放器场景(scene)的时间范围绑定,生成ResolvedSlideshow:包含主路径slides与按 sequence id 索引的sequences。
Manifest 解析与校验:底层实现的严谨性
parseSlideshow模块(packages/parsers/src/slideshow/parseSlideshow.ts)承担了 island 提取、类型校验、时间解析与错误收集:
- island 提取:
slideshowIslandRegex(flags)是一个工厂函数,每次调用返回新的 RegExp。这是刻意设计——带g标志的正则持有可变的lastIndex,调用方需要g时必须每次新建实例,避免共享实例的跨调用状态污染。 - 类型守卫:
isSlideRef、isSlideSequence、isManifest逐字段验证结构(sceneId必须为字符串、fragments必须为可选的 number 数组、autoplay必须为可选的布尔值等),非法结构直接抛出 "slideshow island is not a valid SlideshowManifest"。 - 时间解析规则(
resolveTimeRange):startTime与endTime都显式给出 → 直接采用,无需场景;- 两者都缺失 → 从场景解析
{ start: scene.start, end: scene.start + scene.duration }; - 只给其一 → 缺失边界从场景补齐,若场景不存在则报出明确错误("slide X sets startTime but endTime cannot be resolved")。
- fragment 范围校验:每个 fragment 必须在
[start, end]区间内,且去重排序。 - 热点目标校验:hotspot 的
target必须指向存在的 sequence,且该 sequence 不能为空。 - 主路径重叠校验:按 start 排序后相邻比较,主路径幻灯片不允许时间重叠(
main-line slides "a" and "b" overlap)。
所有错误以数组返回而非中断,解析器尽力产出可用的resolved结果,由上层决定降级策略(例如dropInvalidSlides会剔除end <= start的幻影幻灯片——它们只会出现在引用了不存在场景的半成品 ref 中)。
控制器状态机:导航的核心逻辑
SlideshowController(packages/player/src/slideshow/SlideshowController.ts)是导航状态机的核心。它通过PlayerPort接口与播放器解耦:
export interface PlayerPort { seek(t: number): void; play(): void; pause(): void; stopMedia?(): void; playSceneMedia?(sceneId: string): void; // autoplay 幻灯片进入时播放场景内 <video> readonly currentTime: number; onTimeUpdate(cb: (t: number) => void): () => void; }导航栈模型
控制器内部维护一个导航栈StackFrame[],每个栈帧记录{ sequenceId, slideIndex, fragmentIndex }:
interface StackFrame { sequenceId: string; slideIndex: number; fragmentIndex: number; // -1 = 第一个 fragment 之前 / 幻灯片起点 }初始栈为[{ sequenceId: "main", slideIndex: 0, fragmentIndex: -1 }]。"main"是主路径的保留序列名。栈的设计使分支导航天然可回溯:进入分支时 push 新帧,back()弹出帧并恢复父帧保存的fragmentIndex(而非重置为 -1),从而精确还原 presenter 进入分支前的位置。
关键行为:只停留、不自动前进
playTo(t)是决定"Slideshow 永不自动前进"的底层机制:
private playTo(t: number): void { this.player.seek(t); // 纯同步 seek,无持续播放 }player.seek(t)直接驱动合成的 GSAP 时间线(播放器通过同源 iframe 的__timelines访问),而 GSAP 的.seek()会同步渲染该帧并保持时间线暂停。因此一次 seek 同时完成"重绘"与"停留"——确定性地在每个窗口(包括后台窗口)生效。源码注释还记录了此前的缺陷修复:旧实现"先播放一帧、再在 timeupdate 上暂停"会在未聚焦的观众窗口上持续播放,造成自动前进/单侧冻结的抖动问题;现在fragmentIndex由调用方直接设置,而不是依赖播放的 tick。
进入与恢复幻灯片
enterSlide(index):跳到幻灯片首个停留点并停留。有 fragment 时停在fragments[0];无 fragment 时停在restFrame(slide),即slide.start + (slide.end - slide.start) * 0.5(中点而非slide.end——end是下一场景开始的边界,停在 end 会导致第一张幻灯片渲染出第二张的内容)。若幻灯片声明autoplay,则调用playSceneMedia播放其剪辑。resumeSlide(index, fragmentIndex):按保存的位置恢复,用于back()/backToMain()/syncTo(),恢复规则与向前进入保持一致(已保存 fragment → 该 fragment 的停留时间;有 fragment 但未到第一个 →slide.start;无 fragment → 中点)。
导航 API 一览
| 方法 | 行为 |
|---|---|
next() | 有剩余 fragment 则揭示下一个;否则进入下一张幻灯片;分支末尾则back()回父时间线 |
prev() | 同序列内上一张;分支首张则back() |
goToSlide(index) | 当前序列内跳到指定索引 |
enterBranch(sequenceId) | push 新栈帧并进入分支第一张 |
back() | pop 栈帧,恢复父帧的精确位置 |
backToMain() | 栈重置为仅主路径,恢复主路径上的位置 |
syncTo(seq, slide, frag) | 无动画跳转到绝对位置(观众镜像用),先重根栈再静态恢复 |
此外控制器暴露canPrev/canNext(用于 UI 控制按钮的可用态)、counter(index/total)与breadcrumb(当前导航路径,用于面包屑展示)。
<hyperframes-slideshow>Web 组件:播放器侧的完整实现
packages/player/src/slideshow/hyperframes-slideshow.ts(约 1470 行)实现了整个 Web 组件,它负责:解析 island、等待播放器就绪与场景加载、绑定控制器、渲染控制台(nav cluster)、热点胶囊按钮、presenter 模式布局,以及 presenter ↔ audience 的双向同步。
初始化流程(init)
- 在
connectedCallback中用setTimeout(0)宏任务推迟初始化,确保流式解析时子元素(<hyperframes-player>)已就位。 - 解析
innerHTML中的 island;失败则优雅降级(不渲染控制台)。 waitForReady(player)等待播放器就绪(5 秒超时兜底)。waitForScenes(player, 2500)轮询player.scenes(每 100ms 一次迭代)直到至少有一个场景;超时则返回[],此时依赖显式startTime/endTime的幻灯片仍可工作。resolveSlideshow(manifest, scenes)解析并打印错误;dropInvalidSlides剔除零时长幻灯片。- 构造
PlayerPort并bindController(new SlideshowController(port, cleaned))。 - 针对慢 iframe 的恢复机制:若场景尚未 post 到位(scenes 为空),监听一次性的
scenes事件后重新 init,避免场景驱动的幻灯片永久丢失。
交互细节
- 自动添加
interactive属性:组件内的<hyperframes-player>默认是pointer-events: none,为了让合成 iframe 内的可点击控件、链接、原生媒体控件收到指针事件,组件会机械地给子 player 设置interactive属性(幂等,保留宿主已声明的值),并通过MutationObserver覆盖动态添加的 player。 - iframe 键盘转发:交互式 Deck 中 presenter 点击幻灯片后焦点进入 iframe,顶层 window 的 keydown 不再触发。组件对同源 iframe 转发 keydown 事件,并在每次 iframe
load时重新挂载(导航会清空父窗口添加的监听器)。 - 键盘快捷键:
→下一项、←上一项、空格下一项、Backspace上一项、F全屏、P进入 Present 模式。文本输入控件内(INPUT/TEXTAREA/SELECT/contentEditable)按键不会触发导航,避免打字时翻页;多实例同页时只有聚焦的 Deck 响应按键。 - 触摸手势:要求水平主导手势(
|deltaX| > 40且|deltaX| > |deltaY|),防止斜向页面滚动误触导航。 - 热点胶囊按钮:浮动按钮锚定热点区域的左上角(
region.x/y百分比定位),内容自适应大小(忽略 w/h 尺寸),带hf-hotspot-pulse呼吸动画,并遵循prefers-reduced-motion。所有用户输入经escHtml转义以防 XSS。 - 全局静音:
sound属性控制是否显示静音按钮;mutedgetter 反映data-hf-muted属性,静音会应用到所有子 player 与已登记的媒体元素。
观众模式与媒体同步
组件通过mode属性或 URL 查询参数(?mode=audience)解析自身模式:
- Presenter(默认):每次位置变化时向 BroadcastChannel 广播
goto消息(含sequenceId/slideIndex/fragmentIndex);present 开始时还会做 5 次递增延迟(250ms/750ms/1500ms/3000ms/5000ms)的位置突发广播,确保新打开的观众窗口能追上当前状态。 - Audience:监听
goto消息调用controller.syncTo()无动画镜像位置;不渲染导航控件,仅保留全屏按钮。
频道名通过slideshowChannelName()生成:hf-slideshow:${location.pathname}。以 pathname 作为 key 使同一 Deck 的 presenter 与观众窗口配对,同时隔离同源上其他 Deck 的串扰(固定名会导致互相干扰)。
媒体同步是双向的:presenter 将各媒体元素的play/pause/seeking/seeked/ratechange/volumechange/ended/timeupdate动作广播到观众端(timeupdate有 450ms 节流)。观众端因浏览器自动播放策略可能被阻止播放(NotAllowedError),此时会以静音播放作为兜底,并显示 "Play audience media muted" 解锁按钮供观众手动恢复。OwnedMediaRegistry负责登记/释放媒体元素上的监听器,避免重复监听与 iframe 节点移除后的泄漏。
自动播放的健壮实现
playSceneDocumentMedia(sceneId)应对两类时序风险:(1) 构造时剪辑可能尚未进入 iframe DOM;(2) 播放器引导期与进入时的时间线 seek 都会暂停剪辑(并拒绝 in-flight 的play()为AbortError),单次play()会输掉竞态。因此实现采用短定时器轮询:定位剪辑([data-composition-id="${sceneId}"] video),持续断言play(),直到剪辑在两个 tick 间真实推进(advancingTicks >= 2)才停止。整个过程由autoplayToken守卫——离开幻灯片或断开连接即取消,防止残留的 re-assert 重放已离开的剪辑。
Presenter 模式:演讲者视图与观众视图
Presenter 布局由 packages/player/src/slideshow/slideshowPresenter.ts 的buildPresenterLayout构建。present()打开观众标签页时,将当前页面 URL 追加?mode=audience(使用 URL API 而非字符串拼接,避免页面 URL 含#fragment时参数落入 fragment 内导致观众窗口以"未同步的第二个 presenter"启动)。打开方式是有意选择锚点点击(rel="noopener noreferrer")而非window.open(features)——非空 features 字符串倾向创建弹出窗口,全屏遮挡屏幕共享时会冻结。
Presenter 视图的布局:
- 上方 68%:实时幻灯片(letterboxed 完整可见,底部不会被面板遮挡),通过
playerEl.style.bottom = "32%"固定; - 下方 32%面板:
- 可编辑的演讲者备注
<textarea>(编辑实时持久化到localStorage,key 为hf-slideshow:presenter-notes:v1:前缀 + Deck key + 序列/幻灯片/sceneId 的 JSON 序列化); - "Up next" 下一张预览(取下一张备注的首行);
- Branches 列表:当前幻灯片的可点击分支入口(按
data-hotspot-id关联enterBranch())——因为胶囊定位与 letterboxed 幻灯片对齐不可靠,分支入口以列表形式放在控制台中; - Slide 计数器与 Elapsed 计时器(
mm:ss格式,formatElapsed每秒更新一次)。
- 可编辑的演讲者备注
计时器更新刻意只重写 elapsed 读数而不是重渲染整个 chrome——旧实现每秒重建导航按钮 DOM,导致按钮闪烁、点击落在重建中途被丢弃。
CLI:hyperframes present命令
从命令行以 presenter 模式起服务并打开浏览器:
npx hyperframes present <project-directory>该命令定义在 packages/cli/src/commands/present.ts,参数如下:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
dir | positional | 必需 | 项目目录 |
--port | string | 3004 | 服务端口(占用时自动换用空闲端口) |
--open | boolean | true | 是否自动打开浏览器 |
--browser-path | string | — | 浏览器可执行文件路径 |
--user-data-dir | string | — | Chromium 兼容的用户数据目录(需--browser-path) |
--remote-debugging-port | string | — | Chromium 远程调试端口(需--browser-path与--user-data-dir) |
命令执行流程(源码可验证的关键步骤):
- 解析项目、校验
--user-data-dir/--remote-debugging-port的依赖关系(缺--browser-path直接报错退出)。 - 解析
@hyperframes/player与@hyperframes/player/slideshow的构建产物路径,缺失时提示先运行bun run --cwd packages/player build。 - 从项目的
index.html提取 slideshow island,缺失或 JSON 非法都会失败退出——这是对"静默截断渲染"问题的 CLI 侧防线。 - 启动 Hono +
@hono/node-server服务:/player.js与/slideshow.js提供打包产物;/composition/*按项目目录解析文件路径,经isSafePath规范化与目录逃逸防护(符号链接与同级目录前缀共享都不可逃逸)后返回;/返回 presenter 包装页,presenter 窗口与观众窗口(?mode=audience)加载同一页面,组件从 URL 读取模式。
- 输出提示:Google Meet 中分享观众标签页("Share screen → A tab");Zoom 桌面端把观众标签拖成独立窗口再共享该窗口(保持至少部分可见)。
Studio:分支编辑器面板与 Manifest 持久化
Studio 侧的实现位于 packages/studio/src/components/panels/SlideshowPanel.tsx。当活动合成包含 slideshow manifest 时,Studio 显示Slideshow标签。面板由四个子表面构成:
- Slide list(幻灯片列表):勾选属于主演示的场景,用上下箭头排序;
- Slide Inspector(幻灯片检查器):备注文本框 + fragment 停留点(把播放头移到需要暂停的位置后点击Mark,该时刻成为选中幻灯片的 fragment hold-point);
- Branch tree(分支树):创建/重命名序列、为分支分配场景;
- Hotspot tool(热点工具):把画布上选中的元素标记为当前幻灯片的热点(选择目标分支、添加标签、点击Make hotspot)。
状态管理的关键点(源码注释明确):manifest 在挂载与每次compHtml变化时从当前合成 HTML 解析;每次编辑调用onPersist(manifest)并更新本地状态;所有 manifest 变换都是纯函数辅助(slideshowPanelHelpers.ts,并导出为可测试单元)。持久化层面:备注在短暂停顿后保存;其他编辑立即保存并纳入项目历史(见 packages/studio/src/hooks/useSlideshowPersist.ts)。
分支的删除语义值得注意:删除分支会同时移除指向它的所有 hotspot,因此 Studio 会先请求确认;源场景仍然保留在项目中。
Agent 创作技能:从路由到交付
Slideshow 创作已被纳入 Agent 技能体系。hyperframes技能(skills/hyperframes/SKILL.md)的创建路由表中,优先级 2即为/slideshow:当请求是"作者一个演示文稿、pitch deck 或可导航的交互式 Deck"时路由至此。其路由契约定义在 skills/hyperframes/references/routes/slideshow.md:
- 输入:一段简报、大纲或现有页面,用于创作演示文稿/pitch deck/交互式 Deck。若 "slides"/"deck"/"convert this page" 语义含糊,应在创作前向用户确认是否要 HyperFrames slideshow;
- 输出:可运行的 HyperFrames 合成 + 供
SlideshowController使用的 JSON island:离散幻灯片、fragment 揭示、分支、热点、presenter 模式与演讲者备注。交付物是可导航 Deck,而非 MP4; - 触发词:"make a pitch deck"、"interactive presentation"、"convert this page into slides"、"slideshow with presenter mode"。
文档(docs/guides/slideshow.mdx)给出了向 Agent 提需求的最小范式——提供素材与演示需要支持的决策,例如:
Using /hyperframes, turn this product strategy outline into a presentation for our leadership review: [paste outline]Agent 会确认受众、核心结论以及 Deck 是否需要揭示、可选分支或 presenter 备注,而无须事先规定每一张幻灯片。配套的审查清单包括:每张幻灯片只有一个明确观点、揭示有助于讲解、热点醒目而不干扰、离开幻灯片时内嵌媒体停止、presenter 备注与观众视图分离、完整 Deck 在键盘/触摸/可见控件下均可用。
端到端工作流:从创作到现场演示
综合文档与源码,完整的 Slideshow 工作流如下:
- 创作:通过 Agent(
/slideshow技能路由)或直接在 Studio 的 Slideshow 面板中构建。主路径先行:勾选场景、排序、每张幻灯片确认"只表达一个完整观点",在 Slide Inspector 中添加帮助 presenter 而非复述幻灯片的备注。 - 添加揭示:需要逐步揭示时,把播放头移到暂停时刻并Mark为 fragment。不需要每一条要点都设停留点。
- 添加可选路径:Branches 中命名分支并分配幻灯片;在出现选择的幻灯片上选中可见按钮/对象,用 Hotspot Tool 选择目标分支、添加标签、Make hotspot;始终保持返回主故事的清晰路径。
- 以 presenter 身份测试:运行
npx hyperframes present <project-directory>,测试 next/prev 导航、每个 fragment、每个 hotspot、每个分支及返回路径。点击Present或按P打开同步的观众视图。 - 线上分享:Google Meet 分享观众标签页;Zoom 将观众标签拖入独立窗口共享。
总结
HyperFrames v0.6.114 通过四个紧密耦合的层面(播放器状态机与 Web 组件、CLI present 服务、Studio 分支编辑面板、Agent 创作技能)把 Slideshow 从概念落成可用的端到端能力。其工程实现有几个值得借鉴的设计决策:以"纯同步 seek + 时间线保持暂停"实现确定性停留;以导航栈模型让分支回溯精确到 fragment 级;以 pathname 隔离的 BroadcastChannel 实现 presenter↔audience 免配置同步;以"媒体事件镜像 + 静音兜底"规避观众端自动播放限制。对于需要现场演示的交互式内容,Slideshow 是比线性渲染更合适的选择——但务必记住:现场用hyperframes present,不要对 Deck 执行hyperframes render。
【免费下载链接】hyperframesWrite HTML. Render video. Built for agents.项目地址: https://gitcode.com/GitHub_Trending/hy/hyperframes
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考