Pireel Editor Document V2架构解析:多轨时间线如何实现原子命令、Ripple编辑与撤销重做
Pireel Editor Document V2架构解析:多轨时间线如何实现原子命令、Ripple编辑与撤销重做
Pireel 是一款开源的 AI 视频编辑器,其 Editor Document V2 是支撑多轨时间线、原子命令与 Ripple(波纹)编辑的核心持久化模型。本文带你快速看懂它的架构设计、命令分发机制,以及撤销重做是如何做到"一步到位"的。
📌 一句话概括:时间线是唯一事实来源(single source of truth),所有编辑都是一条条不可变的原子命令,撤销重做则建立在文档快照栈之上。
一、为什么需要 Editor Document V2?
传统单轨剪辑模型里,"主视频"往往藏在某个魔法字段里,加一条字幕轨道、插入一段 B-roll 都要绕一圈。V2 借鉴了专业 NLE(非线性剪辑)的成熟结构:
EditorDocumentV2
├── canvas 宽、高、FPS
├── appearance 主题、字幕样式、人物特效
├── assets 媒体资产清单(以 assetId 为身份)
├── timeline
│ └── tracks[] 稳定 id、类型、语义角色、锁定、堆叠顺序
│ └── clips[] 显式的时间线位置 + 类型化负载
├── semantics 主叙事、transcript、scene、受管字幕
└── processing 文档级媒体处理设置
这套模型有几条"不可妥协"的不变量(见 EDITOR_DOCUMENT_V2.md):
| 不变量 | 通俗解释 |
|---|---|
| 空时间线是合法状态 | 新建项目无需先有素材 |
媒体身份在 assets 里 |
clip 只存 assetId,不存 blob: 这类临时地址 |
| 位置用帧表示 | startFrame / durationFrames,与工程 FPS 绑定 |
| 语义角色靠稳定 ID | "主叙事轨道""受管字幕轨道"不按数组下标找 |
| Ripple 是文档命令 | 功能代码不允许手动搬移数组 |
Composition 则降级为只读渲染投影——面板、预览、导出用它,但任何修改都直接写 V2,杜绝了"来回转换丢失数据"的经典坑。
二、原子命令:所有编辑的唯一入口
所有对 V2 的修改都从同一个门进入:commands/dispatcher.ts 中的 applyEditorCommand。UI 拖拽、AI Agent 工具、服务端 MCP 工具,走的是同一套命令引擎:
UI / Agent / 服务端工具
│
▼
applyEditorCommand(document, command) ← 唯一入口
│
▼
返回 { ok, document, receipts } ← 成功或原样返回
命令层按职责拆分在 editor-document/commands/ 目录下,常见命令包括:
- 🎬
track.insert/track.remove/track.move:轨道 CRUD - ✂️
clip.split:在 split.ts 中原子分裂 clip,并同步分裂其链接组(link group) - ➕
clips.insert:支持overwrite(覆盖)与ripple(后移)两种插入模式 - 🗑️
clips.remove/range.remove:按精确身份删除,或按帧范围 lift/ripple - 📝
narrative.patch:修改取景、调色、镜头音频,不暴露几何细节 - 🖼️
overlay.patch/audio.patch:图形与音频 clip 的稳定 ID 编辑
两条关键特性让命令"原子":
- 不可变:命令永不修改传入的
document,而是返回新文档。失败时原样返回,不会出现"改了一半"的中间状态。 - 带回执(receipt):成功结果会报告受影响的轨道、被移除/创建/平移的 clip,UI 选中、撤销、Agent 总结都不需要再去 diff 数组猜发生了什么。
以 clip.split 为例,它会先做文档校验 → 定位 track/clip → 检查分裂帧必须在 clip 内部 → 展开同一 linkGroupId 的伙伴 clip → 检查锁定轨道,任何一步不满足都整体失败,而不是分裂一半。
三、Ripple 编辑:一剪到底,全轨道联动
"Ripple"(波纹)是多轨剪辑的灵魂:删掉主轨一段视频后,后面所有轨道的同步内容自动左移闭合间隙。它的实现在 commands/range.ts:
range.remove { trackId, startFrame, endFrame, mode: 'lift' | 'ripple' }
│
├── lift → 只挖掉范围,位置不动(留下空白)
└── ripple → 清除锚点轨道 + 所有 syncLocked 轨道
同一区间的片段,然后全局左移闭合间隙
几个值得新手注意的细节:
- sync-lock 策略:轨道上有独立的
syncLocked标志(与"是否可编辑"的locked分开)。Ripple 时只联动syncLocked的轨道,音乐轨默认跟节奏走,而你想冻结的轨道可以关掉。 - 链接组扩展:通过
linkedClearTrackIds迭代扩散,把与命中片段同属一个linkGroupId的跨轨伙伴全部纳入清除范围——比如画面和它的配套图形会一起被剪掉。 - 空区间修剪:删除后,空的非主轨道会被
pruneEmptyNonPrimaryTracks清理,但主叙事轨道永远保留(空主轨是合法状态)。 - 插入的两种模式:commands/insert.ts 里
overwrite直接覆盖被占用的时间,ripple则先把后面的内容推走再落位,对应专业剪辑软件里最常见的两种拖拽手感。
也就是说,"波纹编辑"不是一段 UI 代码顺手搬数组,而是一条有明确边界条件的文档命令:它处理锁定轨道、链接组、语义 scene 更新(updateScenesForClipChanges)、Director Plan 时间补偿(withAdjustedDirectorPlan),最后一次性产出新文档。
四、撤销重做:快照栈 + 事务重放
撤销重做没有用复杂的"逆操作"算法,而是两层机制叠加:
第 1 层:快照栈(本地 Undo/Redo)
document-commit.ts 注释写得很直白:"Undo 是快照栈,但快照在这里拍,每个事务一次,而不是每个调用点拍。"
- 一次拖拽、一个 Agent 工具调用 = 一个事务(transaction)
- 提交事务前拍一张文档快照压入
undoStack,同时清空 redo 线 undo/redo以document.replace事务形式恢复快照,并重新投影出Composition
由于快照只在此处统一拍摄,拖拽过程中的高频中间态(undo: 'none')不会污染撤销历史,用户体验就是"拖一次 = 撤销一步"。
第 2 层:事务重放(跨端同步)
document-transaction.ts 把编辑定义为"一组带 JSON 输入的操作":
DocumentTransaction
├── id 客户端铸造,服务端去重(重发 = 空操作)
├── origin 'user' | 'agent' | 'system' | 'restore'
└── ops[] 每个 op 都是 (document, input) => result 的纯函数
纯函数 + 确定性 ID 带来两个好处:
- 同一事务在任何主机(浏览器、服务端、第二个客户端)重放都得到逐字节一致的文档;
- 作者只发"我做了什么",不发整份快照,两个写者改不同部分时完全不冲突。
replayDocumentTransactions 还会区分三类结果:已见过的 id 跳过(网络重发)、目标已不存在则跳过该操作(编辑与删除竞态的良性情况)、其余错误拒绝该事务但继续处理后续事务。
五、这套架构给用户的实际好处
| 你感受到的体验 | 背后的机制 |
|---|---|
| 剪切后所有轨道整齐联动,且 AI 剪的和你剪的完全一致 | 浏览器与 Agent 共用 applyEditorCommand 命令引擎 |
| 撤销不会把已解析的本地素材地址弄丢 | 运行时 URL 存在旁路 map,不进持久化文档 |
| 锁定轨道不会被误改 | 触碰锁定轨道的命令整体原子失败 |
| 导出中继续编辑也不串帧 | 导出编码前取一次 V2 快照,缓存键、片段位置、总时长同一版本 |
| 云撤销历史可恢复 | 云端 undo 行同样存储/重写 V2 文档 |
六、延伸阅读
想继续深挖,推荐按这个顺序阅读源码:
- EDITOR_DOCUMENT_V2.md —— 架构总纲,含不变量与上线门槛清单
- editor-document/types.ts ——
EditorDocumentV2、EditorTrack、各类 clip 类型定义 - editor-document/commands/dispatcher.ts —— 命令分发全表
- editor-document/commands/range.ts —— lift/ripple 与链接组联动
- document-transaction.ts —— 事务、重放与确定性 ID
- document-commit.ts —— 快照栈与提交门(committer)
Pireel 的 Editor Document V2 证明了一件事:把时间线当作"不可变文档 + 命令流"来设计,原子性、可撤销、AI 可驱动这三者可以免费获得——这正是它能被任意 AI Agent 通过 MCP 驾驭的底层原因。🚀