@motionharness/bridge-davinci 0.0.22 → 0.0.25

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.
@@ -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
@@ -29,8 +29,6 @@ import instance_runtime as instances
29
29
  import legacy_conversion as legacy
30
30
  from runtime_io import atomic_json, file_digest
31
31
 
32
- # Frame boundaries were read back on this exact build; do not extrapolate to unknown builds.
33
- VALIDATED_BUILD = "21.1.0.14"
34
32
  SUPPORTED_INSPECTOR_EFFECTS = {
35
33
  "Transform", "Source Transform", "Immersive Transform", "Cropping", "Source Cropping",
36
34
  "Source Looks", "Dynamic Zoom", "Composite", "Lens Correction", "Retime and Scaling",
@@ -39,10 +37,6 @@ SUPPORTED_INSPECTOR_EFFECTS = {
39
37
  ORIGINAL_PLAYBACK = {"perfProxyMediaMode": "0", "perfOptimisedMediaOn": "0", "perfRenderCacheMode": "none"}
40
38
 
41
39
 
42
- def validated_host(resolve):
43
- return resolve.GetVersionString() == VALIDATED_BUILD and sys.platform == "darwin" and platform.machine() == "arm64"
44
-
45
-
46
40
  class BridgeFailure(Exception):
47
41
  def __init__(self, code, message, **details):
48
42
  super().__init__(message)
@@ -88,8 +82,7 @@ def project_for(resolve, requested=None, render_proof=None):
88
82
  # GetCurrentDatabase returns None throughout some renders on 21.1.0.14.
89
83
  # Monitoring/cancelling an already-started job may use its previously
90
84
  # verified project + complete queue fingerprint; starting still requires DB.
91
- if (render_proof and requested and not manager.GetCurrentDatabase()
92
- and validated_host(resolve) and project.IsRenderingInProgress()):
85
+ if render_proof and requested and not manager.GetCurrentDatabase() and project.IsRenderingInProgress():
93
86
  require(project.GetUniqueId() == requested["projectId"], "VERSION_CONFLICT", "运行中的任务已不属于原工程。")
94
87
  job = render_job(project, render_proof["jobId"])
95
88
  require(render_proof.get("expectedJobFingerprint") and fingerprint(job) == render_proof["expectedJobFingerprint"],
@@ -127,91 +120,36 @@ def media_for(project, media_id):
127
120
 
128
121
 
129
122
  def ofx_source_target(resolve, params):
130
- """Prove an OFX host source maps to one media item, without UI selection.
123
+ """Match the actual input file to its registered media-pool identity.
131
124
 
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.
125
+ Read file paths once to reject ambiguous imports. Only the target's bridge
126
+ metadata matters; unrelated subtitles, generators and metadata are ignored.
136
127
  """
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
128
  requested = params.get("scope")
129
+ media_id, raw_path = params.get("mediaPoolItemId"), params.get("path")
130
+ valid = lambda value: isinstance(value, str) and bool(value.strip()) and "\0" not in value
145
131
  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))
132
+ and all(valid(value) for value in requested.values()) and valid(media_id),
133
+ "INVALID_ARGUMENT", "侧栏入口需要明确的数据库、工程和素材池对象 ID。")
134
+ require(valid(raw_path) and os.path.isabs(raw_path), "INVALID_ARGUMENT", "侧栏源路径必须是非空绝对路径。")
135
+ path = os.path.realpath(raw_path)
136
+ project, scope = project_for(resolve, requested)
137
+ matches = []
138
+ for media in all_media(project.GetMediaPool().GetRootFolder()):
139
+ try:
140
+ properties = media.GetClipProperty() or {}
141
+ source_path = properties.get("File Path")
142
+ except Exception:
143
+ require(media.GetUniqueId() != media_id, "CAPABILITY_UNAVAILABLE", "无法读取目标素材的文件路径。")
144
+ continue
145
+ if valid(source_path) and os.path.isabs(source_path) and os.path.realpath(source_path) == path:
146
+ matches.append(media)
147
+ require(len(matches) == 1 and matches[0].GetUniqueId() == media_id,
148
+ "VERSION_CONFLICT", "侧栏源路径没有对应唯一且匹配的素材池对象;请从固定菜单管理片段。")
149
+ result = media_identity(project, scope, matches[0])
150
+ require(os.path.realpath(result["path"]) == path, "VERSION_CONFLICT", "目标素材已被重新链接。")
151
+ project_for(resolve, requested)
152
+ return {**result, "path": path, "unique": True}
215
153
 
216
154
 
217
155
  def fps_value(value):
@@ -234,13 +172,13 @@ def sample_end_time(start, timeline_frames, source_fps, timeline_fps, source_fra
234
172
  return min(source_frames / source_rate, start / source_rate + (timeline_frames - 1) / timeline_rate + 1 / source_rate)
235
173
 
236
174
 
237
- def item_snapshot(item, timeline_id, track, validated=False, media_index=None, timeline_fps=None):
175
+ def item_snapshot(item, timeline_id, track, media_index=None, timeline_fps=None):
238
176
  media = item.GetMediaPoolItem()
239
- intrinsic_entry = intrinsic.read_entry(item, media_index or {}) if validated and not media else None
177
+ intrinsic_entry = intrinsic.read_entry(item, media_index or {}) if not media else None
240
178
  if intrinsic_entry:
241
179
  media = media_index[intrinsic_entry['identity']['mediaPoolItemId']]
242
- unsupported = [] if validated else ["host-build-and-frame-conventions-unverified"]
243
- kind = item.GetType() if validated else "unknown"
180
+ unsupported = []
181
+ kind = item.GetType()
244
182
  if not media and kind != "transition":
245
183
  unsupported.append("no-plain-media-source")
246
184
  fusion_count = item.GetFusionCompCount() if media else 0
@@ -254,7 +192,7 @@ def item_snapshot(item, timeline_id, track, validated=False, media_index=None, t
254
192
  unsupported.append("take-selector-or-multicam")
255
193
  props = media.GetClipProperty() if media else {}
256
194
  speed, properties, fades = None, None, None
257
- if validated and media:
195
+ if media:
258
196
  if media.GetTimeline():
259
197
  unsupported.append("nested-or-compound-timeline")
260
198
  if kind != "video":
@@ -313,12 +251,12 @@ def item_snapshot(item, timeline_id, track, validated=False, media_index=None, t
313
251
  "sourceIn": source_in, "sourceOut": source_out,
314
252
  "mediaPoolItemId": media.GetUniqueId() if media else None, "unsupported": unsupported,
315
253
  "kind": kind, "speed": speed, "properties": properties, "fades": fades,
316
- "enabled": item.GetClipEnabled() if validated and media else None,
317
- "sourceStartTime": item.GetSourceStartTime() if validated and media else None,
318
- "sourceEndTime": item.GetSourceEndTime() if validated and media else None,
319
- "leftOffset": item.GetLeftOffset() if validated and media else None,
320
- "rightOffset": item.GetRightOffset() if validated and media else None,
321
- "markers": item.GetMarkers() if validated and media else None,
254
+ "enabled": item.GetClipEnabled() if media else None,
255
+ "sourceStartTime": item.GetSourceStartTime() if media else None,
256
+ "sourceEndTime": item.GetSourceEndTime() if media else None,
257
+ "leftOffset": item.GetLeftOffset() if media else None,
258
+ "rightOffset": item.GetRightOffset() if media else None,
259
+ "markers": item.GetMarkers() if media else None,
322
260
  **({"inspectorEntry": entry} if entry else {}),
323
261
  **({"intrinsicEntry": intrinsic_entry} if intrinsic_entry else {}),
324
262
  }
@@ -575,9 +513,6 @@ def grade_evidence(resolve, timeline, native_items, intrinsic_ids=None):
575
513
 
576
514
  @contextmanager
577
515
  def timeline_context(resolve, timeline):
578
- if resolve is None or not validated_host(resolve):
579
- yield
580
- return
581
516
  project = resolve.GetProjectManager().GetCurrentProject()
582
517
  previous = project.GetCurrentTimeline()
583
518
  try:
@@ -593,42 +528,41 @@ def timeline_context(resolve, timeline):
593
528
  await_timeline(project, previous, resolve=resolve)
594
529
 
595
530
 
596
- def scan_timeline(timeline, resolve=None, grade_media_id=None):
531
+ def scan_timeline(timeline, resolve, grade_media_id=None):
597
532
  with timeline_context(resolve, timeline):
598
533
  return scan_current_timeline(timeline, resolve, grade_media_id)
599
534
 
600
535
 
601
- def scan_current_timeline(timeline, resolve=None, grade_media_id=None):
602
- validated = resolve is not None and validated_host(resolve)
536
+ def scan_current_timeline(timeline, resolve, grade_media_id=None):
603
537
  items, native_items, paths = [], [], {}
604
538
  media_index = None
605
539
  for track in range(1, timeline.GetTrackCount("video") + 1):
606
540
  native = timeline.GetItemListInTrack("video", track) or []
607
541
  native_items.extend(native)
608
542
  for item in native:
609
- if validated and not item.GetMediaPoolItem() and item.GetFusionCompCount() > 0 and media_index is None:
543
+ if not item.GetMediaPoolItem() and item.GetFusionCompCount() > 0 and media_index is None:
610
544
  project = resolve.GetProjectManager().GetCurrentProject()
611
545
  media_index = {m.GetUniqueId(): m for m in all_media(project.GetMediaPool().GetRootFolder())}
612
- snapshot = item_snapshot(item, timeline.GetUniqueId(), track, validated, media_index, fps_value(timeline.GetSetting("timelineFrameRate")) if validated else None)
613
- snapshot.update(trackEnabled=timeline.GetIsTrackEnabled("video", track) if validated else None,
614
- trackLocked=timeline.GetIsTrackLocked("video", track) if validated else None)
546
+ snapshot = item_snapshot(item, timeline.GetUniqueId(), track, media_index, fps_value(timeline.GetSetting("timelineFrameRate")))
547
+ snapshot.update(trackEnabled=timeline.GetIsTrackEnabled("video", track),
548
+ trackLocked=timeline.GetIsTrackLocked("video", track))
615
549
  items.append(snapshot)
616
- if validated and item.GetMediaPoolItem():
550
+ if item.GetMediaPoolItem():
617
551
  paths[snapshot["id"]] = item.GetMediaPoolItem().GetClipProperty("File Path") or ""
618
552
  elif snapshot.get('intrinsicEntry'):
619
553
  paths[snapshot['id']] = media_index[snapshot['mediaPoolItemId']].GetClipProperty('File Path') or ''
620
- evidence = otio_evidence(resolve, timeline) if validated and items else {"complete": not items, "reasons": [], "hash": None}
621
- if validated and items and evidence.get("clipCount") != sum(1 for item in items if item["kind"] == "video" and item["mediaPoolItemId"]):
554
+ evidence = otio_evidence(resolve, timeline) if items else {"complete": True, "reasons": [], "hash": None}
555
+ if items and evidence.get("clipCount") != sum(1 for item in items if item["kind"] == "video" and item["mediaPoolItemId"]):
622
556
  evidence["reasons"] = sorted(set(evidence["reasons"] + ["otio-item-coverage-mismatch"]))
623
557
  evidence["globalReasons"] = sorted(set(evidence.get("globalReasons", evidence["reasons"]) + ["otio-item-coverage-mismatch"]))
624
- scoped_reasons = otio_item_reasons(evidence, items, paths, timeline.GetStartFrame(), fps_value(timeline.GetSetting("timelineFrameRate"))) if validated and items else {}
558
+ scoped_reasons = otio_item_reasons(evidence, items, paths, timeline.GetStartFrame(), fps_value(timeline.GetSetting("timelineFrameRate"))) if items else {}
625
559
  # A media readback retains grades only for that media's uses. Keep every
626
560
  # native item and OTIO check for indirect references, but avoid exporting
627
561
  # unrelated LUTs. Whole-timeline snapshots still inspect all grades.
628
562
  grade_items = [native for native, item in zip(native_items, items)
629
563
  if grade_media_id is None or item["mediaPoolItemId"] == grade_media_id]
630
- grades = grade_evidence(resolve, timeline, grade_items, {i['id'] for i in items if i.get('intrinsicEntry')}) if validated and grade_items and any(i["kind"] == "video" for i in items) else {}
631
- entry_scope = scope_for(resolve.GetProjectManager(), resolve.GetProjectManager().GetCurrentProject()) if validated and any(i.get("inspectorEntry") or i.get('intrinsicEntry') for i in items) else None
564
+ grades = grade_evidence(resolve, timeline, grade_items, {i['id'] for i in items if i.get('intrinsicEntry')}) if grade_items and any(i["kind"] == "video" for i in items) else {}
565
+ entry_scope = scope_for(resolve.GetProjectManager(), resolve.GetProjectManager().GetCurrentProject()) if any(i.get("inspectorEntry") or i.get('intrinsicEntry') for i in items) else None
632
566
  for item in items:
633
567
  item["otioHash"] = evidence["hash"]
634
568
  item["unsupported"] = sorted(set(item["unsupported"] + scoped_reasons.get(item["id"], evidence["reasons"])))
@@ -640,10 +574,9 @@ def scan_current_timeline(timeline, resolve=None, grade_media_id=None):
640
574
  owner = entry["identity"]
641
575
  if any(owner[key] != value for key, value in entry_scope.items()):
642
576
  item["unsupported"] = sorted(set(item["unsupported"] + ["inspector-identity-conflict"]))
643
- if validated:
644
- graph = timeline.GetNodeGraph()
645
- if graph and any(graph.GetToolsInNode(index) for index in range(1, graph.GetNumNodes() + 1)):
646
- for item in items: item["unsupported"] = sorted(set(item["unsupported"] + ["timeline-grade-unverified"]))
577
+ graph = timeline.GetNodeGraph()
578
+ if graph and any(graph.GetToolsInNode(index) for index in range(1, graph.GetNumNodes() + 1)):
579
+ for item in items: item["unsupported"] = sorted(set(item["unsupported"] + ["timeline-grade-unverified"]))
647
580
  return items
648
581
 
649
582
 
@@ -685,8 +618,28 @@ def publish_playback(path, publication, before_path):
685
618
  os.unlink(pending)
686
619
 
687
620
 
688
- def media_snapshot(project, scope, media, resolve=None):
689
- properties = media.GetClipProperty() or {}
621
+ def media_identity(project, scope, media):
622
+ """The original media owns the binding, independently of its timeline uses."""
623
+ try:
624
+ properties = media.GetClipProperty() or {}
625
+ metadata = media.GetThirdPartyMetadata() or {}
626
+ except Exception as error:
627
+ raise BridgeFailure("CAPABILITY_UNAVAILABLE", "无法读取目标原素材的身份或关联。", diagnostic=str(error))
628
+ require(isinstance(properties, dict) and isinstance(metadata, dict)
629
+ and all(isinstance(key, str) and isinstance(value, str) for key, value in metadata.items()),
630
+ "CAPABILITY_UNAVAILABLE", "无法读取目标素材的身份或关联。")
631
+ data = {"scope": scope, "projectName": project.GetName(), "mediaPoolItemId": media.GetUniqueId(),
632
+ "path": properties.get("File Path", ""), "metadata": metadata,
633
+ "uses": [], "usageScanComplete": False,
634
+ "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")}}
635
+ data["fingerprint"] = fingerprint({"scope": scope, "mediaPoolItemId": data["mediaPoolItemId"], "path": data["path"],
636
+ "bridgeMetadata": metadata.get("motionharness.bridge"),
637
+ "properties": {key: value for key, value in data["clipProperties"].items() if key != "Clip Name"}})
638
+ return data
639
+
640
+
641
+ def media_snapshot(project, scope, media, resolve):
642
+ data = media_identity(project, scope, media)
690
643
  media_id = media.GetUniqueId()
691
644
  uses = []
692
645
  complete = True
@@ -697,23 +650,20 @@ def media_snapshot(project, scope, media, resolve=None):
697
650
  items = scan_timeline(timeline, resolve, grade_media_id=media_id)
698
651
  uses.extend(i for i in items if i["mediaPoolItemId"] == media_id)
699
652
  # A nested timeline could reference this media indirectly.
700
- 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):
653
+ 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")) for i in items):
701
654
  complete = False
702
- 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})
655
+ settings = project.GetSettings()
656
+ data.update({"uses": uses, "usageScanComplete": complete,
657
+ "playbackPolicy": {key: settings.get(key) for key in ("perfProxyMediaMode", "perfOptimisedMediaOn", "perfRenderCacheMode")}})
708
658
  return data
709
659
 
710
660
 
711
- def timeline_snapshot(project, scope, timeline, resolve=None):
661
+ def timeline_snapshot(project, scope, timeline, resolve):
712
662
  with timeline_context(resolve, timeline):
713
663
  return current_timeline_snapshot(project, scope, timeline, resolve)
714
664
 
715
665
 
716
- def current_timeline_snapshot(project, scope, timeline, resolve=None):
666
+ def current_timeline_snapshot(project, scope, timeline, resolve):
717
667
  items = scan_timeline(timeline, resolve)
718
668
  sources = []
719
669
  # Rebuild once per snapshot, not once per source. Do not cache across calls:
@@ -729,8 +679,8 @@ def current_timeline_snapshot(project, scope, timeline, resolve=None):
729
679
  props = media.GetClipProperty() or {}
730
680
  sources.append({"mediaPoolItemId": media_id, "properties": {key: props.get(key) for key in ("File Path", "FPS", "Frames", "Resolution", "Input Color Space", "Input Gamma", "Data Level", "Proxy Media Path")},
731
681
  "bridgeMetadata": (media.GetThirdPartyMetadata() or {}).get("motionharness.bridge")})
732
- settings = project.GetSettings() if resolve and validated_host(resolve) else {}
733
- data = {"scope": scope, "timelineId": timeline.GetUniqueId(), "name": timeline.GetName(), "validatedHost": bool(resolve and validated_host(resolve)),
682
+ settings = project.GetSettings()
683
+ data = {"scope": scope, "timelineId": timeline.GetUniqueId(), "name": timeline.GetName(),
734
684
  "fps": fps_value(timeline.GetSetting("timelineFrameRate")), "startFrame": timeline.GetStartFrame(),
735
685
  "resolution": {"width": int(timeline.GetSetting("timelineResolutionWidth")), "height": int(timeline.GetSetting("timelineResolutionHeight"))},
736
686
  "endFrame": timeline.GetEndFrame(), "items": items, "sources": sources,
@@ -828,7 +778,6 @@ def seek_timecode(project, timeline, requested, timeout=3.0, resolve=None):
828
778
 
829
779
 
830
780
  def preview_frame(resolve, project, scope, params, before=None):
831
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该平台与构建的画面捕获尚未验证。")
832
781
  require_active_project(resolve, project)
833
782
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出。")
834
783
  timeline = timeline_for(project, params["timelineId"])
@@ -885,7 +834,7 @@ def playback_policy(project):
885
834
 
886
835
 
887
836
  def render_preset_evidence(resolve, project):
888
- name = "Hotion Harness Bridge delivery check " + str(uuid.uuid4())
837
+ name = "MotionHarness Bridge delivery check " + str(uuid.uuid4())
889
838
  require(project.SaveAsNewRenderPreset(name), "CAPABILITY_UNAVAILABLE", "无法核对导出预设的缓存策略。")
890
839
  try:
891
840
  with tempfile.TemporaryDirectory(prefix="mh-bridge-preset-") as directory:
@@ -899,7 +848,6 @@ def render_preset_evidence(resolve, project):
899
848
 
900
849
 
901
850
  def create_render(resolve, project, scope, params):
902
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证指定任务导出。")
903
851
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 已有输出任务在运行。")
904
852
  timeline = timeline_for(project, params["timelineId"])
905
853
  expected = params["expectedTimelineFingerprint"]
@@ -910,7 +858,7 @@ def create_render(resolve, project, scope, params):
910
858
  previous = project.GetCurrentTimeline()
911
859
  before_jobs = project.GetRenderJobList() or []
912
860
  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())
861
+ backup = "MotionHarness Bridge restore " + str(uuid.uuid4())
914
862
  require(project.SaveAsNewRenderPreset(backup), "CAPABILITY_UNAVAILABLE", "无法保存原有导出配置,未修改设置。")
915
863
  job_id = None
916
864
  try:
@@ -958,7 +906,6 @@ def create_render(resolve, project, scope, params):
958
906
 
959
907
 
960
908
  def start_render(resolve, project, scope, params):
961
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证指定任务导出。")
962
909
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在执行其他渲染;未启动队列。")
963
910
  job = render_job(project, params["jobId"])
964
911
  require(params.get("expectedJobFingerprint") and fingerprint(job) == params["expectedJobFingerprint"], "VERSION_CONFLICT", "队列任务的输出设置已改变或没有核对记录,未启动。")
@@ -985,7 +932,6 @@ def start_render(resolve, project, scope, params):
985
932
 
986
933
 
987
934
  def finish_render(resolve, project, params):
988
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证输出恢复。")
989
935
  render_job(project, params["jobId"])
990
936
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "仍有渲染运行,保留原始画面模式直至可核对的终态。")
991
937
  expected, actual = params["playbackPolicy"], playback_policy(project)
@@ -997,7 +943,7 @@ def finish_render(resolve, project, params):
997
943
 
998
944
 
999
945
  def export_references(resolve, project, scope, params):
1000
- require(validated_host(resolve) and not project.IsRenderingInProgress(), "RESOLVE_BUSY", "输出后画面核对需要已完成渲染的目标工程。")
946
+ require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "输出后画面核对需要已完成渲染的目标工程。")
1001
947
  timeline = timeline_for(project, params["timelineId"])
1002
948
  before = timeline_snapshot(project, scope, timeline, resolve)
1003
949
  require(before["fingerprint"] == params["expectedTimelineFingerprint"], "VERSION_CONFLICT", "输出后的时间线已改变,不能生成原回执的参考画面。")
@@ -1024,7 +970,6 @@ def export_references(resolve, project, scope, params):
1024
970
 
1025
971
 
1026
972
  def preview_original(resolve, project, scope, params):
1027
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证原始媒体合成检查。")
1028
973
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "不能在导出期间改变合成画面检查策略。")
1029
974
  timeline = timeline_for(project, params["timelineId"])
1030
975
  before = timeline_snapshot(project, scope, timeline, resolve)
@@ -1046,8 +991,6 @@ def preview_original(resolve, project, scope, params):
1046
991
 
1047
992
 
1048
993
  def selection_snapshot(resolve, plain_media_only=False):
1049
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证时间线选择读取。")
1050
-
1051
994
  def plain_item(item):
1052
995
  media = item.GetMediaPoolItem()
1053
996
  require(media is not None and media is not False, "CAPABILITY_UNAVAILABLE", "请选择有直接视频源的普通片段;生成器入口暂不支持此操作。")
@@ -1131,8 +1074,7 @@ def entry_target(resolve, project, scope, params):
1131
1074
  source = ofx_source_target(resolve, {"scope": scope, "mediaPoolItemId": selection.get("mediaPoolItemId"), "path": selection["path"]})
1132
1075
  if not selection.get("timelineId") and not selection.get("itemId"):
1133
1076
  require(source is not None, "INVALID_REQUEST", "素材级入口需要源视频路径。")
1134
- media = media_for(project, selection.get("mediaPoolItemId"))
1135
- return {**source, "projectName": project.GetName(), "name": media.GetName()}
1077
+ return {**source, "name": source["clipProperties"].get("Clip Name") or ""}
1136
1078
  timeline = timeline_for(project, selection.get("timelineId"))
1137
1079
  found = [item for track in range(1, timeline.GetTrackCount("video") + 1)
1138
1080
  for item in timeline.GetItemListInTrack("video", track) or []
@@ -1162,7 +1104,7 @@ def inspector_operation(resolve, params):
1162
1104
  # A composition imported through the API is not an Edit-page Fusion effect.
1163
1105
  # Keep the graph experiment separate from the product's Inspector entry.
1164
1106
  require(params.get("remove") or params.get("compositionOnly") is True, "CAPABILITY_UNAVAILABLE",
1165
- "ImportFusionComp 不能创建 Edit 检查器效果,请通过原生特效库应用 Hotion Harness Bridge 模板。")
1107
+ "ImportFusionComp 不能创建 Edit 检查器效果,请通过原生特效库应用 MotionHarness Bridge 模板。")
1166
1108
  identity = params["identity"]
1167
1109
  require(identity.get("schemaVersion") == 1, "INVALID_REQUEST", "检查器入口身份版本无效。")
1168
1110
  selected = selection_snapshot(resolve)
@@ -1197,7 +1139,7 @@ def inspector_operation(resolve, params):
1197
1139
  before = timeline_snapshot(project, scope, timeline, resolve)
1198
1140
  targets = [value for value in before["items"] if value["id"] == item.GetUniqueId()]
1199
1141
  require(len(targets) == 1 and not targets[0]["unsupported"] and not targets[0].get("trackLocked"),
1200
- "UNSUPPORTED_TIMELINE_ITEM", "实验入口只接受完整读回通过、未锁定的普通 Hotion Harness 片段。")
1142
+ "UNSUPPORTED_TIMELINE_ITEM", "实验入口只接受完整读回通过、未锁定的普通 MotionHarness 片段。")
1201
1143
  with open(params["compositionPath"], "rb") as stream:
1202
1144
  data = stream.read()
1203
1145
  require(hashlib.sha256(data).hexdigest() == params["compositionSha256"] and data.startswith(b"-- MH Resolve Bridge inspector v1\n"),
@@ -1238,6 +1180,8 @@ def inspector_operation(resolve, params):
1238
1180
 
1239
1181
 
1240
1182
  def dispatch(resolve, operation, params):
1183
+ # Resolve version and process architecture are diagnostic information only.
1184
+ # Operations use the host APIs directly and report actual failures.
1241
1185
  if operation == "ofx_source_target":
1242
1186
  return ofx_source_target(resolve, params)
1243
1187
  if operation == "selection":
@@ -1272,22 +1216,19 @@ def dispatch(resolve, operation, params):
1272
1216
  "scope": scope_for(manager, project) if project else None,
1273
1217
  "timelines": [{"id": t.GetUniqueId(), "name": t.GetName()} for t in timelines(project)] if project else [],
1274
1218
  "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",
1276
- "frameConvention": "half-open" if validated_host(resolve) else "unverified",
1277
- "profile": {"platform": sys.platform, "architecture": platform.machine(), "basicBridgeValidated": validated_host(resolve)},
1219
+ "replacement": "stable-playback-file-with-media-identity",
1220
+ "frameConvention": "half-open",
1221
+ "profile": {"platform": sys.platform, "architecture": platform.machine()},
1278
1222
  "timelineReadback": "top-level-video; native speed/properties/fades and conservative OTIO structure check"}
1279
1223
  proof = params if operation in ("render_status", "cancel_render") else None
1280
1224
  project, scope = project_for(resolve, params.get("scope"), proof)
1281
1225
  if operation == "entry_target":
1282
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证所选片段管理。")
1283
1226
  return entry_target(resolve, project, scope, params)
1284
1227
  if operation in ("legacy_preview", "legacy_convert", "legacy_check"):
1285
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证旧生成器结构转换。")
1286
1228
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出。")
1287
1229
  action = {"legacy_preview": legacy.preview, "legacy_convert": legacy.convert, "legacy_check": legacy.check}[operation]
1288
1230
  return action(resolve, project, scope, params, globals())
1289
1231
  if operation == "duplicate_instance":
1290
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证单实例独立副本。")
1291
1232
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出。")
1292
1233
  return instances.duplicate_instance(resolve, project, scope, params, globals())
1293
1234
  if operation == "timeline":
@@ -1332,8 +1273,6 @@ def dispatch(resolve, operation, params):
1332
1273
  require(len(found) <= 1, "RECOVERY_CONFLICT", "多个媒体对象拥有同一绑定身份,未自动选择。")
1333
1274
  return media_snapshot(project, scope, found[0], resolve) if found else None
1334
1275
  if operation in ("render_status", "cancel_render"):
1335
- if operation == "cancel_render":
1336
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该构建尚未验证指定任务取消。")
1337
1276
  render_job(project, params["jobId"])
1338
1277
  status = project.GetRenderJobStatus(params["jobId"])
1339
1278
  if operation == "cancel_render" and status.get("JobStatus") == "Rendering":
@@ -1344,25 +1283,18 @@ def dispatch(resolve, operation, params):
1344
1283
  return status
1345
1284
  if operation == "media_identity":
1346
1285
  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)}
1286
+ result = media_identity(project, scope, media)
1352
1287
  require_active_project(resolve, project)
1353
1288
  return result
1354
1289
  if operation in ("media", "bind", "replace"):
1355
1290
  media = media_for(project, params["mediaPoolItemId"])
1356
- before = media_snapshot(project, scope, media, resolve)
1357
1291
  if operation == "media":
1358
- return before
1292
+ return media_snapshot(project, scope, media, resolve)
1293
+ before = media_identity(project, scope, media)
1359
1294
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出,桥接没有修改媒体。")
1360
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证自动修改的帧边界。")
1361
- require(before["fingerprint"] == params["expectedFingerprint"], "VERSION_CONFLICT", "Resolve 媒体或使用位置已变化。")
1295
+ require(before["fingerprint"] == params["expectedFingerprint"], "VERSION_CONFLICT", "Resolve 原素材的身份、关联或媒体规格已变化。")
1362
1296
  if operation == "replace":
1363
1297
  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
1298
  path = os.path.realpath(params["filePath"])
1367
1299
  publication = params.get("publication")
1368
1300
  require(publication or os.path.isfile(path), "ARTIFACT_MISSING", "新产物不存在。")
@@ -1372,22 +1304,21 @@ def dispatch(resolve, operation, params):
1372
1304
  "RECOVERY_CONFLICT", "离线搬迁修复只允许原绑定、原产物身份和已记录的旧路径。")
1373
1305
  require(file_digest(publication["sourcePath"] if publication else path) == repair["sha256"],
1374
1306
  "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
1307
  require(not before["clipProperties"].get("Proxy Media Path"), "PROXY_UNSUPPORTED", "当前替换路径尚未验证已关联代理。")
1380
1308
  require(os.path.basename(path) == os.path.basename(before["path"]), "BASENAME_CONFLICT", "安全重连要求版本目录中的文件名保持一致;未修改现有素材。")
1381
1309
  if publication:
1382
1310
  require_active_project(resolve, project)
1311
+ require(media_identity(project, scope, media)["fingerprint"] == before["fingerprint"],
1312
+ "VERSION_CONFLICT", "发布前原素材的身份或关联已改变。")
1383
1313
  # Require the still-current metadata; no selection or effect-stored ID is consulted.
1384
1314
  old_link = json.loads(before["metadata"].get("motionharness.bridge") or "{}")
1385
1315
  new_link = json.loads(params["metadata"].get("motionharness.bridge") or "{}")
1386
1316
  require(not old_link or all(old_link.get(k) == new_link.get(k) for k in ("schemaVersion", "bindingId", "sourceId")),
1387
1317
  "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
1318
  publish_playback(path, publication, before["path"])
1319
+ require_active_project(resolve, project)
1320
+ require(media_identity(project, scope, media)["fingerprint"] == before["fingerprint"],
1321
+ "APPLY_OUTCOME_UNKNOWN" if publication else "VERSION_CONFLICT", "重连前原素材的身份或关联已改变。")
1391
1322
  require(project.GetMediaPool().RelinkClips([media], os.path.dirname(path)), "APPLY_OUTCOME_UNKNOWN", "Resolve 未确认重连版本目录,需读回核对后处理。")
1392
1323
  # Clip interpretation belongs to the existing cut. Relinking may
1393
1324
  # re-read container defaults; preserve the user's native settings.
@@ -1400,27 +1331,15 @@ def dispatch(resolve, operation, params):
1400
1331
  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
1332
  "APPLY_OUTCOME_UNKNOWN", "媒体解释读回与替换前不同,需核对原生状态。")
1402
1333
  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)
1334
+ after = media_identity(project, scope, media)
1335
+ require_active_project(resolve, project)
1414
1336
  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", "替换后的路径或剪辑使用位置读回不一致。")
1337
+ require(os.path.realpath(after["path"]) == path, "APPLY_OUTCOME_UNKNOWN", "替换后的原素材路径读回不一致。")
1418
1338
  for key in ("FPS", "Frames", "Resolution", "Audio Ch", "Input Color Space", "Input Gamma", "Data Level"):
1419
1339
  require(before["clipProperties"].get(key) == after["clipProperties"].get(key), "APPLY_OUTCOME_UNKNOWN", "替换后媒体规格或颜色解释改变。", property=key)
1420
1340
  return after
1421
1341
  if operation == "insert":
1422
1342
  require(not project.IsRenderingInProgress(), "RESOLVE_BUSY", "Resolve 正在导出。")
1423
- require(validated_host(resolve), "CAPABILITY_UNAVAILABLE", "该 Resolve 平台与构建尚未验证插入的帧边界。")
1424
1343
  timeline = timeline_for(project, params["timelineId"])
1425
1344
  before = timeline_snapshot(project, scope, timeline, resolve)
1426
1345
  require(before["fingerprint"] == params["expectedTimelineFingerprint"], "VERSION_CONFLICT", "时间线在插入前发生变化。")
@@ -1476,7 +1395,7 @@ def dispatch(resolve, operation, params):
1476
1395
  "mediaType": 1, "trackIndex": track, "recordFrame": record}]) or []
1477
1396
  require(len(created) == 1, "APPLY_OUTCOME_UNKNOWN", "Resolve 未返回唯一插入对象,需核对后恢复。")
1478
1397
  save_insert_progress(params, before, "appended", media, created[0])
1479
- item = item_snapshot(created[0], timeline.GetUniqueId(), track, True, {media.GetUniqueId(): media}, before["fps"])
1398
+ item = item_snapshot(created[0], timeline.GetUniqueId(), track, {media.GetUniqueId(): media}, before["fps"])
1480
1399
  sampling_matches = item["sourceIn"] == start and item["sourceOut"] == end
1481
1400
  if spec["fps"] != before["fps"]:
1482
1401
  expected_start = start * spec["fps"]["den"] / spec["fps"]["num"]