@motionharness/bridge-davinci 0.0.22 → 0.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NOTICE CHANGED
@@ -1,4 +1,4 @@
1
- Hotion Harness DaVinci Resolve Bridge (@motionharness/bridge-davinci)
1
+ Motion Harness Bridge Component for DaVinci Resolve (@motionharness/bridge-davinci)
2
2
  Copyright 2026 MH contributors
3
3
 
4
4
  This product includes software developed by MH contributors.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Hotion Harness · DaVinci Resolve 可选组件
1
+ # Motion Harness 桥接组件(Motion Harness Bridge Component)
2
2
 
3
- 时间线承载完整的普通视频,动效源码仍在 Hotion Harness 中编辑。主 CLI 提供 `mh bridge davinci`,Agent 同时使用达芬奇官方 MCP 处理实拍剪辑、音轨和原生变速。组件不启动另一套 MCP 服务。安装组件时一并安装固定菜单、简洁小窗和侧栏 OFX;给时间线片段自动附加 OFX 尚未提供。
3
+ 素材池原素材(`MediaPoolItem`)通过 `sourceId + mediaPoolItemId` 关联 Motion Harness 工程。时间线片段只是操作入口:菜单从所选片段取得原素材,侧栏从实际输入视频核对已有素材关联。同一原素材的复制、裁切及多轨道引用打开同一工程、一起更新。时间线承载普通视频,动效源码仍在 Motion Harness 中编辑。主 CLI 提供 `mh bridge davinci`,Agent 同时使用达芬奇官方 MCP 处理实拍剪辑、音轨和原生变速。组件不启动另一套 MCP 服务。安装组件时一并安装固定菜单、简洁小窗和侧栏 OFX;给时间线片段自动附加 OFX 尚未提供。
4
4
 
5
5
  ## 支持范围
6
6
 
@@ -35,41 +35,41 @@ mh bridge davinci uninstall
35
35
 
36
36
  ## 侧栏入口
37
37
 
38
- 运行 `mh bridge davinci install` 会一并安装菜单、小窗和 OFX。OFX 默认安装到 `/Library/OFX/Plugins`;系统目录需要权限时会请求 macOS 管理员授权,取消或失败会报告未完成,不能当作完整安装成功。安装后保存工程并重启 Resolve,给普通视频附加 Hotion Harness 滤镜。侧栏提供“编辑 Hotion Harness 工程”“更新此素材”“查看状态”和“更多…”。更新直接核对并固定目标,复用已有更新流程,影响该素材在当前工程中的全部引用。立即显示状态提示,重复点击复用正在进行的请求;不需要先打开管理窗口。效果中保存的 ID 和当前时间线选择都不能替代源路径、素材池身份及元数据的交叉核对,无法唯一确定时停止。
38
+ 运行 `mh bridge davinci install` 会一并安装菜单、小窗和 OFX。OFX 默认安装到 `/Library/OFX/Plugins`;系统目录需要权限时会请求 macOS 管理员授权,取消或失败会报告未完成,不能当作完整安装成功。安装后保存工程并重启 Resolve,给普通视频附加 Motion Harness 桥接组件。侧栏提供“编辑 Motion Harness 工程”“更新此素材”“查看状态”和“更多…”。更新直接核对并固定目标,复用已有更新流程,影响该素材在当前工程中的全部引用。立即显示状态提示,重复点击复用正在进行的请求;不需要先打开管理窗口。效果中保存的 ID 和当前时间线选择都不能替代源路径、素材池身份及元数据的交叉核对,无法唯一确定时停止。
39
39
 
40
40
  自动附加、自动恢复被移除的效果和“不可单独删除”不在当前范围;用户按图文步骤自行添加侧栏,缺少视觉入口时仍可从固定菜单编辑、更新片段。新旧入口协议在升级期间并存,旧二进制仍可能给出过期路径提示,需重新启动 Resolve 才会加载新的原生入口。
41
41
 
42
42
  ## Studio 流程
43
43
 
44
- 日常操作直接使用侧栏的编辑、更新和状态按钮。没有添加侧栏 OFX 时,选中普通视频后仍可从「工作区 → 脚本 → Utility → Hotion Harness」使用“编辑工程”和“更新到达芬奇”。独立的“管理所选片段”菜单已移除;升级仅清理未被用户修改的自有旧菜单。
44
+ 日常操作直接使用侧栏的编辑、更新和状态按钮。没有添加侧栏 OFX 时,选中普通视频后仍可从「工作区 → 脚本 → Utility → Motion Harness 桥接组件」使用“编辑工程”和“更新到达芬奇”。独立的“管理所选片段”菜单已移除;升级仅清理未被用户修改的自有旧菜单。
45
45
 
46
46
  组件安装后,Studio 导出面板并列显示“发送到达芬奇”。未安装时隐藏该按钮,旁边的说明提供安装命令。先打开目标 Resolve 时间线,再选择导出格式发送。已关联的源在同一位置提供“更新到达芬奇”。发送前先显示工程、时间线、空闲视频轨和播放头位置;更新前显示实际目标与本次核对的引用数量。确认后按该目标在后台完成渲染、完整解码验证和导入;动效内容或目标剪辑改变会报冲突,不覆盖用户操作。
47
47
 
48
- 发送成功后提供可关闭的侧栏入口提示,图文步骤包含特效库搜索、拖入片段的真实界面截图,快捷键作为补充。选择“不再提示”后,仍可从导出区的桥接说明或侧栏“更多…”窗口底部打开图文步骤。CLI 完成发送的任务结果包含可选的 `sidebarHint`,供 Agent 在交付时说明;`mh bridge davinci sidebar-guide` 返回本机图文页位置。帮助页不修改 Resolve,也不意味着侧栏已自动附加。
48
+ 发送成功后提供可关闭的侧栏入口提示,图文步骤包含特效库搜索、拖入片段的布局示意图,快捷键作为补充。选择“不再提示”后,仍可从导出区的桥接说明或侧栏“更多…”窗口底部打开图文步骤。CLI 完成发送的任务结果包含可选的 `sidebarHint`,供 Agent 在交付时说明;`mh bridge davinci sidebar-guide` 返回本机图文页位置。帮助页不修改 Resolve,也不意味着侧栏已自动附加。
49
49
 
50
50
  MP4/H.264 用于不透明视频。MOV/ProRes 4444 或 4444 XQ 支持 straight alpha。源中的音轨当前仍由桥接明确拒绝,请让 Agent 在 Resolve 中管理音轨,避免静默丢声。
51
51
 
52
- 确认并提交后返回任务 ID;真实阶段为 queued、preparing、rendering、validating、applying,再到 completed、failed、cancelled 或 outcome-unknown。重复提交同一幂等键返回原任务。网络丢失回执时,当前 Studio 页会保留已确认请求,重新打开导出面板或刷新页面只按原键查询,不自动重发。暂时查不到任务也不会改用新键;“重试原请求”明确使用原目标、版本条件和幂等键。已知任务 ID 丢失时只允许继续核对,不盲目重发。重开导出面板会核对当前源内容、已应用版本和原生关联;该轻量核对不扫描时间线或切换达芬奇页面,完整引用检查仅在明确准备更新时进行。其他素材、多目标或纯渲染任务可继续观察,面板不会把它们当成当前素材已经更新。
52
+ 确认并提交后返回任务 ID;真实阶段为 queued、preparing、rendering、validating、applying,再到 completed、failed、cancelled 或 outcome-unknown。重复提交同一幂等键返回原任务。网络丢失回执时,当前 Studio 页会保留已确认请求,重新打开导出面板或刷新页面只按原键查询,不自动重发。暂时查不到任务也不会改用新键;“重试原请求”明确使用原目标、版本条件和幂等键。已知任务 ID 丢失时只允许继续核对,不盲目重发。重开导出面板只核对原素材身份、工程目录是否存在和已记录关联;不启动源工程内容检查、不计算整段视频摘要,也不扫描时间线或切换达芬奇页面。更新确认只说明影响全部引用,不为统计引用数量扫描工程。其他素材、多目标或纯渲染任务可继续观察,面板不会把它们当成当前素材已经更新。
53
53
 
54
54
  ## Agent 从 Resolve 开始
55
55
 
56
56
  1. 用达芬奇官方 MCP 读取明确的工程和时间线,完成实拍剪辑,并取得插入目标。
57
- 2. 用主 CLI 创建或修改可编辑 Hotion Harness 工程,匹配输出尺寸、帧率和时长。
57
+ 2. 用主 CLI 创建或修改可编辑 Motion Harness 工程,匹配输出尺寸、帧率和时长。
58
58
  3. 可先调用 `mh bridge davinci action prepare --project <源目录> --json '{"operation":"send","idempotencyKey":"..."}'` 获取固定目标及版本条件,将返回的 `request` 原样交给 `send`,并保留同一 `--project`。无需人工确认的 Agent 流程也可直接调用 `mh bridge davinci send --project <源目录> --json '<请求>'`。请求至少包含 `idempotencyKey`,可包含 `export`(与 `mh render` 相同的格式/编码/透明度等选项)以及 `scope`、`timelineId`、`track`、`recordFrame`。
59
59
  4. 用 `mh bridge davinci task --json '{"taskId":"..."}'` 查询完成结果。也可传 `idempotencyKey` 恢复丢失的任务 ID。
60
60
  5. 用 Resolve MCP 对普通视频做原生剪辑。修改动效源后,调用 `update`,指定 `bindingId`、可选 `expectedRevision` 和新的 `idempotencyKey`。
61
61
 
62
- `inspect --json '{"bindingId":"...","mode":"status"}'` 只核对当前源版本与素材身份,不读取全部剪辑;默认 `inspect` 仍执行完整引用检查。`action prepare` 的结果是只读确认资料,不创建任务,也不渲染视频。
62
+ `inspect --json '{"bindingId":"...","mode":"status"}'` 只核对目标原素材与已记录关联;默认 `inspect` 采用相同的轻量检查。返回的源观察是缓存资料,`contentChecked:false` 表示本次未重新检查源内容。打开只要求源工程存在。源版本和文件完整性在实际更新、恢复或导出前检查。`action prepare` 的结果是只读确认资料,不创建任务,也不渲染视频。
63
63
 
64
- 显式管理接口还有 `bind`、`render`、`adopt`、`insert`、`apply`、`sync`、`duplicate`、`inspect`、`timeline`、`selection`、`cancel` 和 `recover`。每个动作使用 `--json`、`--file` 或 `--stdin`。用 `mh bridge davinci describe --json '{"operation":"insert"}'` 查看已安装适配器的完整请求结构。修改动作需要幂等键;支持范围以 `doctor` 和实际任务结果为准。规格变化不会自动重剪已有时间线,应发送为新素材。固定播放文件通过原生 RelinkClips 刷新,并逐次核对全部引用,不要求关闭默认全局设置。恒定变速、倒放和定格的剪辑状态会保留,但刷新结果存在以下预览限制。已关联代理和未知时间映射继续明确阻止自动更新。
64
+ 显式管理接口还有 `bind`、`render`、`adopt`、`insert`、`apply`、`sync`、`duplicate`、`inspect`、`timeline`、`selection`、`cancel` 和 `recover`。每个动作使用 `--json`、`--file` 或 `--stdin`。用 `mh bridge davinci describe --json '{"operation":"insert"}'` 查看已安装适配器的完整请求结构。修改动作需要幂等键;支持范围以 `doctor` 和实际任务结果为准。规格变化不会自动重剪已有时间线,应发送为新素材。固定播放文件通过原生 RelinkClips 刷新,只操作绑定的原素材,不重建、修改或逐一检查时间线片段,不要求关闭默认全局设置。位置、裁切、变速、倒放、定格和特效由达芬奇保留,但刷新结果存在以下预览限制。已关联代理的原素材仍阻止自动更新;时间线实例的编辑差异不作为关联失效条件。
65
65
 
66
66
  `duplicate` 可仅复制编辑源,也可明确指定 `bindingId`、`itemId`、`timelineId` 及当前媒体/时间线指纹,复制编辑源与不可变视频后让这一个时间线实例使用新关联;其他实例继续引用原素材。当前原生独立实例流程限定源与时间线均为 30 fps、无已有 Take Selector、无关联音轨和曲线变速,临时归一化不可超出源时长。原生步骤写入恢复记录;发生外部改动时停止,不能凭旧状态盲目回滚。
67
67
 
68
68
  ## 入口与历史版本
69
69
 
70
- macOS 原生包安装“编辑工程”和“更新到达芬奇”两个菜单脚本,入口为 Resolve「工作区 → 脚本 → Utility → Hotion Harness」。主 CLI `edit` 可按选中片段的稳定关联打开源工程。
70
+ macOS 原生包安装“编辑工程”和“更新到达芬奇”两个菜单脚本,入口为 Resolve「工作区 → 脚本 → Utility → Motion Harness 桥接组件」。主 CLI `edit` 可按选中片段的稳定关联打开源工程。
71
71
 
72
- 侧栏“更多…”按效果自身的源路径固定素材身份,后来切换选择不会把操作转给另一段素材。窗口直接提供该关联确实应用过的历史版本、指定时间线实例的独立副本(多处引用时先明确选择具体片段)、重新定位源目录和丢失元数据时的关联修复;目录面板只在明确点击相应操作后出现。恢复版本前显示会影响的引用数量,独立副本则明确只改变一个时间线实例。结果不明时先核对原任务,再明确选择继续原任务;原任务可能包含多个目标,不会被管理窗口缩成当前一段。
72
+ 侧栏“更多…”按效果自身的源路径固定素材身份,后来切换选择不会把操作转给另一段素材。窗口直接提供该关联确实应用过的历史版本、重新定位源目录和丢失元数据时的关联修复。点击“创建独立副本…”后才扫描引用并选择具体片段;日常打开管理窗口不列举全部时间线实例。目录面板只在明确点击相应操作后出现。恢复版本前说明影响该素材的全部引用,独立副本则明确只改变一个时间线实例。结果不明时先核对原任务,再明确选择继续原任务;原任务可能包含多个目标,不会被管理窗口缩成当前一段。
73
73
 
74
74
  默认构建和安装包同时包含菜单、小窗和图像透传 OFX。插件仅提供源工程入口和更新状态,不承载动效渲染。普通视频不会自动附加 OFX,`entry restore` 当前返回能力不可用;缺少或删除视觉入口不影响源关联,可以继续通过固定菜单和 CLI 管理。安装 OFX 与自动附加 OFX 是两个独立能力。
75
75
 
@@ -110,6 +110,14 @@ macOS 原生构建使用 `node packages/bridge/davinci/native/build.mjs`,生
110
110
 
111
111
  ### 固定视频与更新恢复
112
112
 
113
- 每个关联使用 `artifacts/media/<bindingId>/<文件名>` 作为播放文件,`artifacts/<artifactId>/` 保存独立的历史版本。渲染和完整视频校验成功后,先记录任务的目标、旧版本和新版本,再由原生脚本核对素材身份与剪辑状态,在同一目录原子替换播放文件,并通过 `RelinkClips` 刷新。不会覆盖历史文件或使用硬链接。渲染失败保留旧画面;发布后结果不明时保留原任务,不自动重新提交。通过管理窗口或 `recover` 核对已应用版本,只有原目标仍匹配才允许继续。
113
+ 每个关联使用 `artifacts/media/<bindingId>/<文件名>` 作为播放文件,`artifacts/<artifactId>/` 保存独立的历史版本。渲染和完整视频校验成功后,先记录任务的目标、旧版本和新版本,再由原生脚本核对绑定的原素材身份、关联和媒体规格,在同一目录原子替换播放文件,并通过 `RelinkClips` 刷新。不会覆盖历史文件或使用硬链接。渲染失败保留旧画面;发布后结果不明时保留原任务,不自动重新提交。通过管理窗口或 `recover` 核对已应用版本,只有原目标仍匹配才允许继续。
114
114
 
115
- 固定路径不保证所有未知宿主版本和代理组合的行为。组件沿用已验证宿主范围,已关联代理、无法核对的媒体或不支持的时间线结构仍拒绝更新。普通视频暂停时可能保留旧帧,播放或移动播放头后才显示新画面;定格画面也可能保留旧版本。定格不因此阻止更新。状态小窗及管理窗口以一行次要文字提示“预览未刷新请播放;定格可能保留旧画面。”任务完成代表视频文件与关联已更新,不保证宿主预览已经重绘。管理窗口保留恢复版本、重新定位、修复关联等低频功能。
115
+ 固定路径不保证所有未知宿主版本和代理组合的行为。组件沿用已验证宿主范围,已关联代理或无法核对身份、版本及文件完整性的原素材仍拒绝更新。时间线结构检查只用于插入、独立实例副本和最终导出等操作。普通视频暂停时可能保留旧帧,播放或移动播放头后才显示新画面;定格画面也可能保留旧版本。定格不因此阻止更新。状态小窗及管理窗口以一行次要文字提示“预览未刷新请播放;定格可能保留旧画面。”任务完成代表视频文件与关联已更新,不保证宿主预览已经重绘。管理窗口保留恢复版本、重新定位、修复关联等低频功能。
116
+
117
+ ### 名称与语言兼容
118
+
119
+ 侧栏显示本地连接状态,缺少 CLI、配置或状态窗口时会在首次点击立即提示。通过 npm 安装时保存固定 CLI 启动入口,每次操作自动定位当前原生程序,CLI 升级或平台包位置变化后无需重装 OFX。点击“修复连接”可刷新已有启动配置并检查达芬奇连接,不重装 OFX、不重启达芬奇,也不改素材关联。组件文件缺失或配置被用户修改时,提示运行 `mh bridge davinci install` 修复。
120
+
121
+ 组件在中文界面显示为 **Motion Harness 桥接组件**,英文显示为 **Motion Harness Bridge Component**。原生侧栏与小窗口跟随 macOS 首选中英文,固定菜单在安装或修复时使用同一语言;Studio 的图文帮助跟随 Studio 当前语言。`MH_UI_LOCALE=zh-CN` 或 `MH_UI_LOCALE=en` 可显式指定原生入口语言。更改系统语言后,重新安装组件并重启 Resolve 即可刷新菜单及侧栏名称。
122
+
123
+ 旧版本误拼写对应的 OFX 插件 ID、参数键、二进制文件名、通知标识和 `Application Support/Hotion Harness` 配置路径继续保留,以兼容已有工程、入口和安装记录。`legacy-inspector-v1.lua` 是旧入口逐字节识别所需的原始模板,不用于生成新的界面。升级只移除已登记且未修改的旧菜单,保留用户定制内容。
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "name": "@motionharness/bridge-davinci",
4
- "version": "0.0.22",
4
+ "version": "0.0.23",
5
5
  "protocol": 1,
6
- "cliVersion": "0.0.22",
6
+ "cliVersion": "0.0.23",
7
7
  "files": [
8
8
  {
9
9
  "path": "dist/inspector_runtime.py",
@@ -19,15 +19,15 @@
19
19
  },
20
20
  {
21
21
  "path": "dist/legacy_conversion.py",
22
- "sha256": "0581d63ac5c67a489e10887fba4fae0abdaf89f1640f9c450b7e1b911251769b"
22
+ "sha256": "3c595b7f5a34611d20f59bdd8181414b5191aadd27fee02667dfdd6c59f6d464"
23
23
  },
24
24
  {
25
25
  "path": "dist/resolve_rpc.py",
26
- "sha256": "cfb622ec26f3d33dc961fb498b7774330eaa9bd3455752b4a582af78ba92852a"
26
+ "sha256": "f10440c19783605f0aec00b206a6520d7a3dafd6f28ddf307cae6b7e08802691"
27
27
  },
28
28
  {
29
29
  "path": "dist/runner.mjs",
30
- "sha256": "e1c08679917fff1acc6d88af1671783b398045ee987393d4b580c5e5f304bf3b"
30
+ "sha256": "8a7c2c89b465ed32ce0e3e275edf8e4e8b0b2c5cd61b01dbf7690daaf2e235d5"
31
31
  },
32
32
  {
33
33
  "path": "dist/runtime_io.py",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  {
41
41
  "path": "NOTICE",
42
- "sha256": "dbd3fd6e2ae4bed6332946051cfced0b9f3b559ec72f9f27d1ea9d4959d51d65"
42
+ "sha256": "08da1a97f3e888dc31f14a8489f4ddbc2a8471a588831c6c1a784c57b55d2d88"
43
43
  },
44
44
  {
45
45
  "path": "OPENFX_LICENSE",
@@ -47,16 +47,16 @@
47
47
  },
48
48
  {
49
49
  "path": "README.md",
50
- "sha256": "bfded59d2028d419e7d34961971bb69b0ebc161f9ec5ba4284463c7c9a91687f"
50
+ "sha256": "a60beff9902034f0d59739ae714659feb00c7e4e2bb747c8b36d1ed25bec6451"
51
51
  },
52
52
  {
53
53
  "path": "resources/HotionHarness.ofx.bundle/Contents/Info.plist",
54
- "sha256": "8e856f10d851400c6a0d192724d4a01bfe5a3c9920f5c1044dfcb1fc1c0a1a25",
54
+ "sha256": "a5316a00995683460edc87123ad958425cd1be36a2dc64edd26b1063b6acefed",
55
55
  "executable": false
56
56
  },
57
57
  {
58
58
  "path": "resources/HotionHarness.ofx.bundle/Contents/MacOS/HotionHarness.ofx",
59
- "sha256": "825783410ce6bd1b00cf0dbf3ee1e2a82d13bf5ad7bd5b628f99ba3ee729ba69",
59
+ "sha256": "750369116eac4aec476a903685690bb488088f881f82d7b3ec0596e9b38a1aa2",
60
60
  "executable": true
61
61
  },
62
62
  {
@@ -66,17 +66,27 @@
66
66
  },
67
67
  {
68
68
  "path": "resources/HotionHarnessStatus.app/Contents/Info.plist",
69
- "sha256": "a56fa4c4bbc618f9e0a362474637afa4e2463046677bd8396f5c5b7043c3dc71",
69
+ "sha256": "ec9e19f3f74cbc09c076382a1fe49c2553a8592887680449d8645ed4adca3fd9",
70
70
  "executable": false
71
71
  },
72
72
  {
73
73
  "path": "resources/HotionHarnessStatus.app/Contents/MacOS/HotionHarnessStatus",
74
- "sha256": "dda084047882a6de416d572a3a1f5df855d6bf3de14e4197cbd6b403cfc1429e",
74
+ "sha256": "8894fd5809549ab4281adc525d447d0c847c8720dd5705e4df4be40098cacd08",
75
75
  "executable": true
76
76
  },
77
+ {
78
+ "path": "resources/HotionHarnessStatus.app/Contents/Resources/en.lproj/InfoPlist.strings",
79
+ "sha256": "a2ace2734255bc9751b7ea734d7cf19b1d1386604b8cb838f0e529839846f419",
80
+ "executable": false
81
+ },
82
+ {
83
+ "path": "resources/HotionHarnessStatus.app/Contents/Resources/zh-Hans.lproj/InfoPlist.strings",
84
+ "sha256": "834876e910caa01c120735fc3cb927db473014cc336e4977aae8d390591e1be2",
85
+ "executable": false
86
+ },
77
87
  {
78
88
  "path": "resources/legacy-forwarder.lua",
79
- "sha256": "778edbabdd56947743f6028ac7f6ec212c8685f04a55244b18672bdb7f32e861",
89
+ "sha256": "342b396ac9fd68dbb9e8cd166c2e99b8d0740c7dc01cac3a5064d6fd092b8900",
80
90
  "executable": false
81
91
  },
82
92
  {
@@ -86,17 +96,17 @@
86
96
  },
87
97
  {
88
98
  "path": "resources/menu-entry.py",
89
- "sha256": "08b4cb619d39735a4bdb468e6b8c00feb454c6c11ece14905c043b564d26398d",
99
+ "sha256": "15f8dafb4d3c4a8020007b20764d4e3c1700f9585be107769efefd8ba4877e94",
90
100
  "executable": false
91
101
  },
92
102
  {
93
- "path": "resources/sidebar-help/index.html",
94
- "sha256": "4c6e51cd63b5310816af724877f4ef42d7f6293a41a97fbcb89a966b84f73db7",
103
+ "path": "resources/sidebar-help/en.html",
104
+ "sha256": "838c7af75c332e6427d78c97a67d4cd44f142633e97dc5200c2a079145d0d4cd",
95
105
  "executable": false
96
106
  },
97
107
  {
98
- "path": "resources/sidebar-help/resolve-sidebar.png",
99
- "sha256": "78a18d0047315a59217a897bf4958e9c4cd2445b7ea66039491c0b3721608985",
108
+ "path": "resources/sidebar-help/index.html",
109
+ "sha256": "ad201ec81af3728ec4a3d0219ca55b5d00fdba2bacb694dcaf47652d368eb8a4",
100
110
  "executable": false
101
111
  },
102
112
  {
@@ -276,7 +276,7 @@ def _plan(resolve, project, scope, params, api):
276
276
  continue
277
277
  reasons = []
278
278
  if not entry:
279
- reasons.append(("legacy-identity-unrecognized", "不是完整识别的 Hotion Harness 旧生成器,保持原结构。"))
279
+ reasons.append(("legacy-identity-unrecognized", "不是完整识别的 Motion Harness 旧生成器,保持原结构。"))
280
280
  candidate = {"itemId": item["id"], "name": item["name"], "track": item["track"], "startFrame": item["startFrame"], "endFrame": item["endFrame"]}
281
281
  if entry:
282
282
  identity = entry["identity"]
@@ -415,7 +415,7 @@ def convert(resolve, project, scope, params, api):
415
415
  if name:
416
416
  require(name not in names, "TARGET_OCCUPIED", "隔离副本名称已经存在,请选择新名称;未调用 DuplicateTimeline。")
417
417
  else:
418
- base = plan["name"] + " · Hotion Harness 转换"
418
+ base = plan["name"] + " · Motion Harness 转换"
419
419
  name, suffix = base, 2
420
420
  while name in names:
421
421
  name = base + " " + str(suffix)
@@ -2,8 +2,8 @@
2
2
  """One JSON request over stdin. Only documented Resolve scripting APIs are used.
3
3
 
4
4
  Only explicit targets and owned render IDs can be modified. Resolve's database
5
- is never edited directly. Unsupported effects exposed by public readback block
6
- automated replacement; Edit-page OFX omitted from OTIO cannot be fully enumerated.
5
+ is never edited directly. Updating a bound media-pool item leaves its timeline
6
+ instances and effects under Resolve ownership.
7
7
  """
8
8
  import hashlib
9
9
  import importlib
@@ -127,91 +127,36 @@ def media_for(project, media_id):
127
127
 
128
128
 
129
129
  def ofx_source_target(resolve, params):
130
- """Prove an OFX host source maps to one media item, without UI selection.
130
+ """Match the actual input file to its registered media-pool identity.
131
131
 
132
- Two complete pool reads detect changes visible during discovery. This is a
133
- read-only observation, not a lock; callers must revalidate before mutation.
134
- File-less timeline/compound entries are excluded only when GetTimeline()
135
- returns an identifiable timeline. Other unclassifiable entries fail closed.
132
+ Read file paths once to reject ambiguous imports. Only the target's bridge
133
+ metadata matters; unrelated subtitles, generators and metadata are ignored.
136
134
  """
137
- def identifier(value):
138
- return isinstance(value, str) and bool(value.strip()) and "\0" not in value
139
-
140
- def canonical_path(value, code):
141
- require(identifier(value) and os.path.isabs(value), code, "侧栏源路径必须是非空绝对路径。")
142
- return os.path.realpath(value)
143
-
144
135
  requested = params.get("scope")
136
+ media_id, raw_path = params.get("mediaPoolItemId"), params.get("path")
137
+ valid = lambda value: isinstance(value, str) and bool(value.strip()) and "\0" not in value
145
138
  require(isinstance(requested, dict) and set(requested) == {"databaseId", "projectId"}
146
- and all(identifier(value) for value in requested.values()),
147
- "INVALID_ARGUMENT", "侧栏入口需要明确的数据库和工程作用域。")
148
- media_id = params.get("mediaPoolItemId")
149
- require(identifier(media_id), "INVALID_ARGUMENT", "侧栏入口需要明确的素材池对象 ID。")
150
- path = canonical_path(params.get("path"), "INVALID_ARGUMENT")
151
-
152
- def read_media(media):
153
- identity = media.GetUniqueId()
154
- require(identifier(identity), "CAPABILITY_UNAVAILABLE", "素材池对象缺少可验证的 ID。")
155
- properties = media.GetClipProperty()
156
- require(isinstance(properties, dict) and "File Path" in properties,
157
- "CAPABILITY_UNAVAILABLE", "无法完整读取素材池文件路径,不能证明侧栏源唯一。")
158
- raw_path = properties["File Path"]
159
- if raw_path == "":
160
- timeline = media.GetTimeline()
161
- timeline_id = timeline.GetUniqueId() if timeline else None
162
- require(identifier(timeline_id), "CAPABILITY_UNAVAILABLE",
163
- "素材池包含无法识别来源的无文件素材,不能证明侧栏源唯一。")
164
- return identity, {"timelineId": timeline_id}
165
- source_path = canonical_path(raw_path, "CAPABILITY_UNAVAILABLE")
166
- metadata = media.GetThirdPartyMetadata()
167
- require(isinstance(metadata, dict) and all(isinstance(key, str) and isinstance(value, str)
168
- for key, value in metadata.items()), "CAPABILITY_UNAVAILABLE", "无法完整读取素材关联元数据。")
169
- # Copy values so a host response or test double cannot mutate our proof.
170
- return identity, {"path": source_path, "metadata": dict(metadata)}
171
-
172
- def scan(project):
173
- pool = project.GetMediaPool()
174
- root = pool.GetRootFolder() if pool else None
175
- require(root is not None, "CAPABILITY_UNAVAILABLE", "无法读取素材池根目录。")
176
- folders, media_states, objects = {}, {}, {}
177
- pending = [root]
178
- while pending:
179
- folder = pending.pop()
180
- folder_id = folder.GetUniqueId()
181
- require(identifier(folder_id) and folder_id not in folders,
182
- "CAPABILITY_UNAVAILABLE", "素材池目录 ID 缺失、重复或存在循环。")
183
- clips, children = folder.GetClipList(), folder.GetSubFolderList()
184
- require(isinstance(clips, list) and isinstance(children, list),
185
- "CAPABILITY_UNAVAILABLE", "素材池目录读取不完整,不能证明侧栏源唯一。")
186
- folders[folder_id] = []
187
- for media in clips:
188
- identity, state = read_media(media)
189
- require(identity not in media_states, "CAPABILITY_UNAVAILABLE", "素材池对象 ID 重复,不能证明侧栏源唯一。")
190
- media_states[identity], objects[identity] = state, media
191
- folders[folder_id].append(identity)
192
- folders[folder_id].sort()
193
- pending.extend(children)
194
- matches = [identity for identity, state in media_states.items() if state.get("path") == path]
195
- require(len(matches) == 1 and matches[0] == media_id, "VERSION_CONFLICT",
196
- "侧栏源路径没有对应唯一且匹配的素材池对象;请从固定菜单管理片段。")
197
- return {"folders": folders, "media": media_states}, objects[media_id]
198
-
199
- try:
200
- project, scope = project_for(resolve, requested)
201
- first, _ = scan(project)
202
- project_for(resolve, requested)
203
- second, target = scan(project)
204
- require(first == second, "VERSION_CONFLICT", "素材池在侧栏身份核对期间发生变化。")
205
- identity, final_state = read_media(target)
206
- require(identity == media_id and final_state == second["media"][media_id],
207
- "VERSION_CONFLICT", "素材路径或关联元数据在侧栏身份核对期间发生变化。")
208
- project_for(resolve, requested)
209
- return {"scope": scope, "mediaPoolItemId": media_id, "path": path,
210
- "metadata": final_state["metadata"], "unique": True}
211
- except BridgeFailure:
212
- raise
213
- except Exception as error:
214
- raise BridgeFailure("CAPABILITY_UNAVAILABLE", "无法完整核对侧栏素材身份。", diagnostic=str(error))
139
+ and all(valid(value) for value in requested.values()) and valid(media_id),
140
+ "INVALID_ARGUMENT", "侧栏入口需要明确的数据库、工程和素材池对象 ID。")
141
+ require(valid(raw_path) and os.path.isabs(raw_path), "INVALID_ARGUMENT", "侧栏源路径必须是非空绝对路径。")
142
+ path = os.path.realpath(raw_path)
143
+ project, scope = project_for(resolve, requested)
144
+ matches = []
145
+ for media in all_media(project.GetMediaPool().GetRootFolder()):
146
+ try:
147
+ properties = media.GetClipProperty() or {}
148
+ source_path = properties.get("File Path")
149
+ except Exception:
150
+ require(media.GetUniqueId() != media_id, "CAPABILITY_UNAVAILABLE", "无法读取目标素材的文件路径。")
151
+ continue
152
+ if valid(source_path) and os.path.isabs(source_path) and os.path.realpath(source_path) == path:
153
+ matches.append(media)
154
+ require(len(matches) == 1 and matches[0].GetUniqueId() == media_id,
155
+ "VERSION_CONFLICT", "侧栏源路径没有对应唯一且匹配的素材池对象;请从固定菜单管理片段。")
156
+ result = media_identity(project, scope, matches[0])
157
+ require(os.path.realpath(result["path"]) == path, "VERSION_CONFLICT", "目标素材已被重新链接。")
158
+ project_for(resolve, requested)
159
+ return {**result, "path": path, "unique": True}
215
160
 
216
161
 
217
162
  def fps_value(value):
@@ -685,8 +630,28 @@ def publish_playback(path, publication, before_path):
685
630
  os.unlink(pending)
686
631
 
687
632
 
633
+ def media_identity(project, scope, media):
634
+ """The original media owns the binding, independently of its timeline uses."""
635
+ try:
636
+ properties = media.GetClipProperty() or {}
637
+ metadata = media.GetThirdPartyMetadata() or {}
638
+ except Exception as error:
639
+ raise BridgeFailure("CAPABILITY_UNAVAILABLE", "无法读取目标原素材的身份或关联。", diagnostic=str(error))
640
+ require(isinstance(properties, dict) and isinstance(metadata, dict)
641
+ and all(isinstance(key, str) and isinstance(value, str) for key, value in metadata.items()),
642
+ "CAPABILITY_UNAVAILABLE", "无法读取目标素材的身份或关联。")
643
+ data = {"scope": scope, "projectName": project.GetName(), "mediaPoolItemId": media.GetUniqueId(),
644
+ "path": properties.get("File Path", ""), "metadata": metadata,
645
+ "uses": [], "usageScanComplete": False,
646
+ "clipProperties": {key: properties.get(key) for key in ("FPS", "Frames", "Resolution", "Audio Ch", "Proxy Media Path", "Input Color Space", "Input Gamma", "Data Level", "Alpha mode", "PAR", "Field Dominance", "Clip Name")}}
647
+ data["fingerprint"] = fingerprint({"scope": scope, "mediaPoolItemId": data["mediaPoolItemId"], "path": data["path"],
648
+ "bridgeMetadata": metadata.get("motionharness.bridge"),
649
+ "properties": {key: value for key, value in data["clipProperties"].items() if key != "Clip Name"}})
650
+ return data
651
+
652
+
688
653
  def media_snapshot(project, scope, media, resolve=None):
689
- properties = media.GetClipProperty() or {}
654
+ data = media_identity(project, scope, media)
690
655
  media_id = media.GetUniqueId()
691
656
  uses = []
692
657
  complete = True
@@ -700,11 +665,8 @@ def media_snapshot(project, scope, media, resolve=None):
700
665
  if any(any(reason in i["unsupported"] for reason in ("nested-or-compound-timeline", "no-plain-media-source", "nested-or-unknown-otio-item", "nested-media-reference", "otio-inspection-unavailable", "otio-item-coverage-mismatch", "host-build-and-frame-conventions-unverified")) for i in items):
701
666
  complete = False
702
667
  settings = project.GetSettings() if resolve and validated_host(resolve) else {}
703
- data = {"scope": scope, "projectName": project.GetName(), "mediaPoolItemId": media_id, "path": properties.get("File Path", ""),
704
- "metadata": media.GetThirdPartyMetadata() or {}, "uses": uses, "usageScanComplete": complete,
705
- "clipProperties": {key: properties.get(key) for key in ("FPS", "Frames", "Resolution", "Audio Ch", "Proxy Media Path", "Input Color Space", "Input Gamma", "Data Level", "Alpha mode", "PAR", "Field Dominance", "Clip Name")},
706
- "playbackPolicy": {key: settings.get(key) for key in ("perfProxyMediaMode", "perfOptimisedMediaOn", "perfRenderCacheMode")}}
707
- data["fingerprint"] = fingerprint({"properties": properties, **data})
668
+ data.update({"uses": uses, "usageScanComplete": complete,
669
+ "playbackPolicy": {key: settings.get(key) for key in ("perfProxyMediaMode", "perfOptimisedMediaOn", "perfRenderCacheMode")}})
708
670
  return data
709
671
 
710
672
 
@@ -885,7 +847,7 @@ def playback_policy(project):
885
847
 
886
848
 
887
849
  def render_preset_evidence(resolve, project):
888
- name = "Hotion Harness Bridge delivery check " + str(uuid.uuid4())
850
+ name = "Motion Harness Bridge delivery check " + str(uuid.uuid4())
889
851
  require(project.SaveAsNewRenderPreset(name), "CAPABILITY_UNAVAILABLE", "无法核对导出预设的缓存策略。")
890
852
  try:
891
853
  with tempfile.TemporaryDirectory(prefix="mh-bridge-preset-") as directory:
@@ -910,7 +872,7 @@ def create_render(resolve, project, scope, params):
910
872
  previous = project.GetCurrentTimeline()
911
873
  before_jobs = project.GetRenderJobList() or []
912
874
  require(not any(os.path.realpath(job.get("TargetDir", "")) == os.path.realpath(os.path.dirname(path)) and job.get("OutputFilename") == os.path.basename(path) for job in before_jobs), "TARGET_OCCUPIED", "输出位置已被其他队列项占用。")
913
- backup = "Hotion Harness Bridge restore " + str(uuid.uuid4())
875
+ backup = "Motion Harness Bridge restore " + str(uuid.uuid4())
914
876
  require(project.SaveAsNewRenderPreset(backup), "CAPABILITY_UNAVAILABLE", "无法保存原有导出配置,未修改设置。")
915
877
  job_id = None
916
878
  try:
@@ -1131,8 +1093,7 @@ def entry_target(resolve, project, scope, params):
1131
1093
  source = ofx_source_target(resolve, {"scope": scope, "mediaPoolItemId": selection.get("mediaPoolItemId"), "path": selection["path"]})
1132
1094
  if not selection.get("timelineId") and not selection.get("itemId"):
1133
1095
  require(source is not None, "INVALID_REQUEST", "素材级入口需要源视频路径。")
1134
- media = media_for(project, selection.get("mediaPoolItemId"))
1135
- return {**source, "projectName": project.GetName(), "name": media.GetName()}
1096
+ return {**source, "name": source["clipProperties"].get("Clip Name") or ""}
1136
1097
  timeline = timeline_for(project, selection.get("timelineId"))
1137
1098
  found = [item for track in range(1, timeline.GetTrackCount("video") + 1)
1138
1099
  for item in timeline.GetItemListInTrack("video", track) or []
@@ -1162,7 +1123,7 @@ def inspector_operation(resolve, params):
1162
1123
  # A composition imported through the API is not an Edit-page Fusion effect.
1163
1124
  # Keep the graph experiment separate from the product's Inspector entry.
1164
1125
  require(params.get("remove") or params.get("compositionOnly") is True, "CAPABILITY_UNAVAILABLE",
1165
- "ImportFusionComp 不能创建 Edit 检查器效果,请通过原生特效库应用 Hotion Harness Bridge 模板。")
1126
+ "ImportFusionComp 不能创建 Edit 检查器效果,请通过原生特效库应用 Motion Harness Bridge 模板。")
1166
1127
  identity = params["identity"]
1167
1128
  require(identity.get("schemaVersion") == 1, "INVALID_REQUEST", "检查器入口身份版本无效。")
1168
1129
  selected = selection_snapshot(resolve)
@@ -1197,7 +1158,7 @@ def inspector_operation(resolve, params):
1197
1158
  before = timeline_snapshot(project, scope, timeline, resolve)
1198
1159
  targets = [value for value in before["items"] if value["id"] == item.GetUniqueId()]
1199
1160
  require(len(targets) == 1 and not targets[0]["unsupported"] and not targets[0].get("trackLocked"),
1200
- "UNSUPPORTED_TIMELINE_ITEM", "实验入口只接受完整读回通过、未锁定的普通 Hotion Harness 片段。")
1161
+ "UNSUPPORTED_TIMELINE_ITEM", "实验入口只接受完整读回通过、未锁定的普通 Motion Harness 片段。")
1201
1162
  with open(params["compositionPath"], "rb") as stream:
1202
1163
  data = stream.read()
1203
1164
  require(hashlib.sha256(data).hexdigest() == params["compositionSha256"] and data.startswith(b"-- MH Resolve Bridge inspector v1\n"),
@@ -1272,7 +1233,7 @@ def dispatch(resolve, operation, params):
1272
1233
  "scope": scope_for(manager, project) if project else None,
1273
1234
  "timelines": [{"id": t.GetUniqueId(), "name": t.GetName()} for t in timelines(project)] if project else [],
1274
1235
  "adapter": "documented-python-scripting", "officialMcp": "not-tested-by-scripting",
1275
- "replacement": "stable-playback-file-with-native-speed-and-otio-check" if validated_host(resolve) else "host-build-unverified",
1236
+ "replacement": "stable-playback-file-with-media-identity" if validated_host(resolve) else "host-build-unverified",
1276
1237
  "frameConvention": "half-open" if validated_host(resolve) else "unverified",
1277
1238
  "profile": {"platform": sys.platform, "architecture": platform.machine(), "basicBridgeValidated": validated_host(resolve)},
1278
1239
  "timelineReadback": "top-level-video; native speed/properties/fades and conservative OTIO structure check"}
@@ -1344,25 +1305,19 @@ def dispatch(resolve, operation, params):
1344
1305
  return status
1345
1306
  if operation == "media_identity":
1346
1307
  media = media_for(project, params["mediaPoolItemId"])
1347
- properties = media.GetClipProperty() or {}
1348
- result = {"scope": scope, "projectName": project.GetName(), "mediaPoolItemId": media.GetUniqueId(),
1349
- "path": properties.get("File Path", ""), "metadata": media.GetThirdPartyMetadata() or {},
1350
- "clipProperties": {key: properties.get(key) for key in ("Proxy Media Path", "Clip Name")},
1351
- "validatedHost": validated_host(resolve)}
1308
+ result = media_identity(project, scope, media)
1352
1309
  require_active_project(resolve, project)
1353
1310
  return result
1354
1311
  if operation in ("media", "bind", "replace"):
1355
1312
  media = media_for(project, params["mediaPoolItemId"])
1356
- before = media_snapshot(project, scope, media, resolve)
1357
1313
  if operation == "media":
1358
- return before
1314
+ return media_snapshot(project, scope, media, resolve)
1315
+ before = media_identity(project, scope, media)
1359
1316
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出,桥接没有修改媒体。")
1360
1317
  require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证自动修改的帧边界。")
1361
- require(before["fingerprint"] == params["expectedFingerprint"], "VERSION_CONFLICT", "Resolve 媒体或使用位置已变化。")
1318
+ require(before["fingerprint"] == params["expectedFingerprint"], "VERSION_CONFLICT", "Resolve 原素材的身份、关联或媒体规格已变化。")
1362
1319
  if operation == "replace":
1363
1320
  repair = params.get("offlineRepair")
1364
- allowed = {"offline-media-reference"} if repair else set()
1365
- require(before["usageScanComplete"] and not any(set(i["unsupported"]) - allowed for i in before["uses"]), "UNSUPPORTED_TIMELINE_ITEM", "已剪片段包含不能完整核对的处理,未重连新版本。")
1366
1321
  path = os.path.realpath(params["filePath"])
1367
1322
  publication = params.get("publication")
1368
1323
  require(publication or os.path.isfile(path), "ARTIFACT_MISSING", "新产物不存在。")
@@ -1372,22 +1327,21 @@ def dispatch(resolve, operation, params):
1372
1327
  "RECOVERY_CONFLICT", "离线搬迁修复只允许原绑定、原产物身份和已记录的旧路径。")
1373
1328
  require(file_digest(publication["sourcePath"] if publication else path) == repair["sha256"],
1374
1329
  "ARTIFACT_CHANGED", "搬迁媒体与记录摘要不同。")
1375
- policy = before["playbackPolicy"]
1376
- require(str(policy.get("perfProxyMediaMode")) in ("0", "1", "2") and str(policy.get("perfOptimisedMediaOn")) in ("0", "1")
1377
- and policy.get("perfRenderCacheMode") in ("none", "smart", "user"),
1378
- "PLAYBACK_POLICY_UNVERIFIED", "原生代理、优化媒体或缓存设置无法完整读回,未修改工程设置。", policy=policy)
1379
1330
  require(not before["clipProperties"].get("Proxy Media Path"), "PROXY_UNSUPPORTED", "当前替换路径尚未验证已关联代理。")
1380
1331
  require(os.path.basename(path) == os.path.basename(before["path"]), "BASENAME_CONFLICT", "安全重连要求版本目录中的文件名保持一致;未修改现有素材。")
1381
1332
  if publication:
1382
1333
  require_active_project(resolve, project)
1334
+ require(media_identity(project, scope, media)["fingerprint"] == before["fingerprint"],
1335
+ "VERSION_CONFLICT", "发布前原素材的身份或关联已改变。")
1383
1336
  # Require the still-current metadata; no selection or effect-stored ID is consulted.
1384
1337
  old_link = json.loads(before["metadata"].get("motionharness.bridge") or "{}")
1385
1338
  new_link = json.loads(params["metadata"].get("motionharness.bridge") or "{}")
1386
1339
  require(not old_link or all(old_link.get(k) == new_link.get(k) for k in ("schemaVersion", "bindingId", "sourceId")),
1387
1340
  "BINDING_CONFLICT", "素材的关联身份已经改变。")
1388
- if old_link and os.path.realpath(before["path"]) == path:
1389
- ofx_source_target(resolve, {"scope": scope, "mediaPoolItemId": media.GetUniqueId(), "path": path})
1390
1341
  publish_playback(path, publication, before["path"])
1342
+ require_active_project(resolve, project)
1343
+ require(media_identity(project, scope, media)["fingerprint"] == before["fingerprint"],
1344
+ "APPLY_OUTCOME_UNKNOWN" if publication else "VERSION_CONFLICT", "重连前原素材的身份或关联已改变。")
1391
1345
  require(project.GetMediaPool().RelinkClips([media], os.path.dirname(path)), "APPLY_OUTCOME_UNKNOWN", "Resolve 未确认重连版本目录,需读回核对后处理。")
1392
1346
  # Clip interpretation belongs to the existing cut. Relinking may
1393
1347
  # re-read container defaults; preserve the user's native settings.
@@ -1400,21 +1354,10 @@ def dispatch(resolve, operation, params):
1400
1354
  require(all(readback.get(key) == before["clipProperties"].get(key) for key in ("Alpha mode", "Input Color Space", "Input Gamma", "Data Level", "PAR", "Field Dominance", "Clip Name") if key in before["clipProperties"]),
1401
1355
  "APPLY_OUTCOME_UNKNOWN", "媒体解释读回与替换前不同,需核对原生状态。")
1402
1356
  set_metadata(media, {**before["metadata"], **params["metadata"]})
1403
- if operation == 'replace':
1404
- for use in before['uses']:
1405
- if not use.get('intrinsicEntry'):
1406
- continue
1407
- target = timeline_for(project, use['timelineId'])
1408
- with timeline_context(resolve, target):
1409
- matches = [i for i in target.GetItemListInTrack('video', use['track']) or [] if i.GetUniqueId() == use['id']]
1410
- require(len(matches) == 1 and intrinsic.read_entry(matches[0], {media.GetUniqueId(): media}),
1411
- 'APPLY_OUTCOME_UNKNOWN', '素材已替换,但原生入口或画面引用在更新期间发生变化。')
1412
- intrinsic.refresh_media(matches[0], media)
1413
- after = media_snapshot(project, scope, media, resolve)
1357
+ after = media_identity(project, scope, media)
1358
+ require_active_project(resolve, project)
1414
1359
  if operation == "replace":
1415
- def edits(uses):
1416
- return [{**item, "unsupported": [reason for reason in item["unsupported"] if not repair or reason != "offline-media-reference"]} for item in uses]
1417
- require(os.path.realpath(after["path"]) == path and edits(before["uses"]) == edits(after["uses"]), "APPLY_OUTCOME_UNKNOWN", "替换后的路径或剪辑使用位置读回不一致。")
1360
+ require(os.path.realpath(after["path"]) == path, "APPLY_OUTCOME_UNKNOWN", "替换后的原素材路径读回不一致。")
1418
1361
  for key in ("FPS", "Frames", "Resolution", "Audio Ch", "Input Color Space", "Input Gamma", "Data Level"):
1419
1362
  require(before["clipProperties"].get(key) == after["clipProperties"].get(key), "APPLY_OUTCOME_UNKNOWN", "替换后媒体规格或颜色解释改变。", property=key)
1420
1363
  return after