@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 +1 -1
- package/README.md +21 -13
- package/bridge-manifest.json +27 -17
- package/dist/legacy_conversion.py +2 -2
- package/dist/resolve_rpc.py +69 -126
- package/dist/runner.mjs +640 -334
- package/package.json +2 -2
- package/resources/HotionHarness.ofx.bundle/Contents/Info.plist +1 -1
- package/resources/HotionHarness.ofx.bundle/Contents/MacOS/HotionHarness.ofx +0 -0
- package/resources/HotionHarnessStatus.app/Contents/Info.plist +2 -2
- package/resources/HotionHarnessStatus.app/Contents/MacOS/HotionHarnessStatus +0 -0
- package/resources/HotionHarnessStatus.app/Contents/Resources/en.lproj/InfoPlist.strings +2 -0
- package/resources/HotionHarnessStatus.app/Contents/Resources/zh-Hans.lproj/InfoPlist.strings +2 -0
- package/resources/legacy-forwarder.lua +2 -2
- package/resources/menu-entry.py +35 -21
- package/resources/sidebar-help/en.html +15 -0
- package/resources/sidebar-help/index.html +10 -30
- package/resources/sidebar-help/resolve-sidebar.png +0 -0
package/NOTICE
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Motion Harness 桥接组件(Motion Harness Bridge Component)
|
|
2
2
|
|
|
3
|
-
|
|
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,给普通视频附加
|
|
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 →
|
|
44
|
+
日常操作直接使用侧栏的编辑、更新和状态按钮。没有添加侧栏 OFX 时,选中普通视频后仍可从「工作区 → 脚本 → Utility → Motion Harness 桥接组件」使用“编辑工程”和“更新到达芬奇”。独立的“管理所选片段”菜单已移除;升级仅清理未被用户修改的自有旧菜单。
|
|
45
45
|
|
|
46
46
|
组件安装后,Studio 导出面板并列显示“发送到达芬奇”。未安装时隐藏该按钮,旁边的说明提供安装命令。先打开目标 Resolve 时间线,再选择导出格式发送。已关联的源在同一位置提供“更新到达芬奇”。发送前先显示工程、时间线、空闲视频轨和播放头位置;更新前显示实际目标与本次核对的引用数量。确认后按该目标在后台完成渲染、完整解码验证和导入;动效内容或目标剪辑改变会报冲突,不覆盖用户操作。
|
|
47
47
|
|
|
48
|
-
|
|
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 创建或修改可编辑
|
|
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"}'`
|
|
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 →
|
|
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>/`
|
|
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` 是旧入口逐字节识别所需的原始模板,不用于生成新的界面。升级只移除已登记且未修改的旧菜单,保留用户定制内容。
|
package/bridge-manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"name": "@motionharness/bridge-davinci",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.23",
|
|
5
5
|
"protocol": 1,
|
|
6
|
-
"cliVersion": "0.0.
|
|
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": "
|
|
22
|
+
"sha256": "3c595b7f5a34611d20f59bdd8181414b5191aadd27fee02667dfdd6c59f6d464"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "dist/resolve_rpc.py",
|
|
26
|
-
"sha256": "
|
|
26
|
+
"sha256": "f10440c19783605f0aec00b206a6520d7a3dafd6f28ddf307cae6b7e08802691"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "dist/runner.mjs",
|
|
30
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
50
|
+
"sha256": "a60beff9902034f0d59739ae714659feb00c7e4e2bb747c8b36d1ed25bec6451"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"path": "resources/HotionHarness.ofx.bundle/Contents/Info.plist",
|
|
54
|
-
"sha256": "
|
|
54
|
+
"sha256": "a5316a00995683460edc87123ad958425cd1be36a2dc64edd26b1063b6acefed",
|
|
55
55
|
"executable": false
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"path": "resources/HotionHarness.ofx.bundle/Contents/MacOS/HotionHarness.ofx",
|
|
59
|
-
"sha256": "
|
|
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": "
|
|
69
|
+
"sha256": "ec9e19f3f74cbc09c076382a1fe49c2553a8592887680449d8645ed4adca3fd9",
|
|
70
70
|
"executable": false
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"path": "resources/HotionHarnessStatus.app/Contents/MacOS/HotionHarnessStatus",
|
|
74
|
-
"sha256": "
|
|
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": "
|
|
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": "
|
|
99
|
+
"sha256": "15f8dafb4d3c4a8020007b20764d4e3c1700f9585be107769efefd8ba4877e94",
|
|
90
100
|
"executable": false
|
|
91
101
|
},
|
|
92
102
|
{
|
|
93
|
-
"path": "resources/sidebar-help/
|
|
94
|
-
"sha256": "
|
|
103
|
+
"path": "resources/sidebar-help/en.html",
|
|
104
|
+
"sha256": "838c7af75c332e6427d78c97a67d4cd44f142633e97dc5200c2a079145d0d4cd",
|
|
95
105
|
"executable": false
|
|
96
106
|
},
|
|
97
107
|
{
|
|
98
|
-
"path": "resources/sidebar-help/
|
|
99
|
-
"sha256": "
|
|
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", "不是完整识别的
|
|
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"] + " ·
|
|
418
|
+
base = plan["name"] + " · Motion Harness 转换"
|
|
419
419
|
name, suffix = base, 2
|
|
420
420
|
while name in names:
|
|
421
421
|
name = base + " " + str(suffix)
|
package/dist/resolve_rpc.py
CHANGED
|
@@ -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.
|
|
6
|
-
|
|
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
|
-
"""
|
|
130
|
+
"""Match the actual input file to its registered media-pool identity.
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
|
|
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(
|
|
147
|
-
"INVALID_ARGUMENT", "
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
|
704
|
-
|
|
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 = "
|
|
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 = "
|
|
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
|
-
|
|
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 检查器效果,请通过原生特效库应用
|
|
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", "实验入口只接受完整读回通过、未锁定的普通
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
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
|