内容
# otio-diff
两个时间线之间的结构化编辑差异。主要回答一个问题:**在剪辑 A 和剪辑 B 之间发生了什么变化** —— 哪些剪辑被添加、删除、重新计时、移动或偏移。
基于 [OpenTimelineIO](https://github.com/AcademySoftwareFoundation/OpenTimelineIO) 构建,因此它可以读取 OTIO 适配器可以解析的任何格式 —— 并继承 OTIO 稳定、版本化的模式,这使得维护成本接近零。
作为 CLI 和 MCP 工具为 AI 代理工作。
## 为什么存在
代理(或 shell 上的用户)可以每天使用 ffmpeg 转码、修剪和探测媒体。但是,从 shell 上无法回答 **“在两个剪辑之间,编辑人员做了哪些更改”** —— 这需要对时间线结构进行推理,而这正是 OTIO 建模的内容。该工具填补了这一空白,而不做其他事情。
**刻意排除在外**:差异化效果、过渡和重新计时曲线。这些以专有、工具特定的方式序列化,不能在格式之间转换 —— 追踪它们是该项目旨在避免的维护黑洞。稳定、通用的子集(剪辑、时间、顺序)是全部价值。
## 安装
需要 Python 3.9–3.13(OTIO 0.18.1 的文件适配器在 3.14 上会损坏)。
```bash
pip install -e . # CLI,.otio 文件仅
pip install -e ".[mcp]" # + MCP 服务器
pip install -e ".[dev]" # + pytest
```
`.otio` 支持已内置。其他格式来自 OTIO 适配器插件 —— 安装您需要的插件:
```bash
pip install otio-cmx3600-adapter # .edl
pip install otio-fcpx-xml-adapter # .fcpxml(FCP X / Resolve)
pip install otio-aaf-adapter # .aaf(Avid)
```
## CLI 使用
```bash
# 相同格式
otio-diff baseline.otio revised.otio
# 混合格式 — 适配器通过扩展名自动检测
otio-diff editors_cut.edl finishing.fcpxml
# 机器可读
otio-diff a.otio b.otio --json
```
工作示例 — 剪辑 B 中有一个剪辑被修剪了 12 帧:
```
$ otio-diff cut_a.edl cut_b.fcpxml
1 retimed, 1 shifted (1 unchanged)
~ B shortened by 12f (48f -> 36f)
. C shifted 12f earlier
```
五类变更:
| 类别 | 含义 |
|-----------|---------|
| `added` | 剪辑 B 中有,剪辑 A 中没有 |
| `removed` | 剪辑 A 中有,剪辑 B 中没有 |
| `retimed` | 相同的剪辑,修剪时长发生变化 |
| `moved` | 相同的剪辑,不同的序号位置(重新排序) |
| `shifted` | 相同的剪辑,仅在时间线上滑动 — 上游编辑的连锁反应,保持独立,以便一次修剪不会读取为下游的 N 次重新计时 |
JSON 输出(形状):
```json
{
"added": [ { "name": "...", "media_url": "...", "src_start": 0.0, "rate": 24.0, ... } ],
"removed": [ ... ],
"retimed": [ { "key": [...], "before": {...}, "after": {...} } ],
"moved": [ ... ],
"shifted": [ ... ],
"unchanged_count": 1
}
```
## 适用于代理
- **退出代码遵循 `diff(1)`**:`0` = 无结构变化,`1` = 找到变化,`2` = 无法读取输入。根据退出代码进行分支,而无需解析。
- **`--json` 是稳定形状的**:上述六个顶级键始终存在,包括空列表。时间是浮点数秒;`rate` 是剪辑帧率(乘以得到帧数)。
- **MCP 服务器**:将 `mcp_server.py` 注册为 stdio 服务器;它公开一个工具,`diff_timelines(path_a, path_b) -> dict`,返回与 `--json` 相同的形状。
- 错误输出到 stderr;标准输出专门用于差异结果。
## MCP 使用
在客户端配置文件中,将 `mcp_server.py` 注册为 stdio MCP 服务器:
```json
{ "otio-diff": { "command": "python", "args": ["/path/to/mcp_server.py"] } }
```
然后,代理可以询问,例如 **“将编辑器的剪辑与完成时间线进行差异,并告诉我哪些镜头在夜间添加了。”**
## 现实世界中的用途
- 统一 / VFX 拉取列表更改:哪些镜头进入或离开了剪辑。
- 夜间更改审查:编辑器自您上次查看以来进行了哪些修改。
- 健全性检查往返:什么在 EDL/AAF 导出后未能幸存。
## 工作原理(一段文字)
`read_from_file()` 将任一输入解析为 OTIO 的内存 `Timeline`。引擎将每个时间线展平为剪辑记录列表(递归到嵌套堆栈),然后 **按身份匹配剪辑** — `(media url, source in-point)`,而不是时间线位置,因为插入一个剪辑会移动下游所有时间码,并使位置差异报告所有更改为已更改。时长作为属性在匹配后比较,因此出点修剪读取为 **重新计时** 而不是删除 + 添加。重复身份配对为多集。匹配的剪辑分类为添加 / 删除 / 重新计时 / 移动 / 偏移。
## 维护合同
固定 `opentimelineio`。其核心模式是稳定且版本化的,因此预期的维护工作是每年大约一次版本更新:更新,运行 `pytest`,发布。如果您发现自己每月都在编辑,那么某些内容已经转移到超出范围的效果/过渡领域 — 将其拉回来。
## 测试
```bash
pytest
```
测试套件(`test_otio_diff.py`)在内存中构建夹具并定义验收。值得注意的案例:重复剪辑时间线(多集匹配),嵌套堆栈(展平),缺少媒体引用(无崩溃),集合返回适配器,退出代码和精确到帧的输出。
## 许可证
Apache-2.0(与 OpenTimelineIO 匹配,以保持上游 `examples/` 贡献的无缝性)。