davinci-resolve-mcp 2.23.0

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.
Files changed (92) hide show
  1. package/AGENTS.md +85 -0
  2. package/CHANGELOG.md +802 -0
  3. package/CLAUDE.md +15 -0
  4. package/LICENSE +21 -0
  5. package/README.md +159 -0
  6. package/SECURITY.md +53 -0
  7. package/bin/davinci-resolve-mcp.mjs +376 -0
  8. package/docs/README.md +56 -0
  9. package/docs/SKILL.md +1145 -0
  10. package/docs/authoring/fuse-dctl-authoring.md +242 -0
  11. package/docs/authoring/script-plugin-authoring.md +195 -0
  12. package/docs/contributing.md +82 -0
  13. package/docs/guides/color-decision-guide.md +387 -0
  14. package/docs/guides/editorial-decision-guide.md +136 -0
  15. package/docs/guides/media-analysis-guide.md +615 -0
  16. package/docs/guides/multicam-setup-guide.md +138 -0
  17. package/docs/install.md +198 -0
  18. package/docs/integrations/workflow-integrations.md +120 -0
  19. package/docs/kernels/README.md +28 -0
  20. package/docs/kernels/audio-fairlight-kernel.md +86 -0
  21. package/docs/kernels/color-grade-kernel.md +103 -0
  22. package/docs/kernels/extension-authoring-kernel.md +101 -0
  23. package/docs/kernels/fusion-composition-kernel.md +91 -0
  24. package/docs/kernels/media-pool-ingest-kernel.md +147 -0
  25. package/docs/kernels/project-lifecycle-kernel.md +120 -0
  26. package/docs/kernels/render-deliver-kernel.md +92 -0
  27. package/docs/kernels/review-annotation-kernel.md +110 -0
  28. package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
  29. package/docs/kernels/timeline-edit-kernel.md +189 -0
  30. package/docs/notes/codec-plugin-notes.md +136 -0
  31. package/docs/notes/dctl-notes.md +234 -0
  32. package/docs/notes/fusion-template-notes.md +136 -0
  33. package/docs/notes/lut-notes.md +136 -0
  34. package/docs/notes/openfx-notes.md +120 -0
  35. package/docs/process/release-process.md +152 -0
  36. package/docs/reference/api-coverage.md +488 -0
  37. package/docs/reference/resolve_scripting_api.txt +1012 -0
  38. package/examples/README.md +53 -0
  39. package/examples/markers/README.md +81 -0
  40. package/examples/media/README.md +94 -0
  41. package/examples/timeline/README.md +98 -0
  42. package/install.py +1196 -0
  43. package/package.json +52 -0
  44. package/scripts/audit_api_parity.py +275 -0
  45. package/scripts/live_media_analysis_polish_probe.py +65 -0
  46. package/src/__init__.py +3 -0
  47. package/src/analysis_dashboard.py +4936 -0
  48. package/src/control_panel.py +13 -0
  49. package/src/granular/__init__.py +17 -0
  50. package/src/granular/common.py +727 -0
  51. package/src/granular/folder.py +287 -0
  52. package/src/granular/gallery.py +306 -0
  53. package/src/granular/graph.py +309 -0
  54. package/src/granular/media_pool.py +679 -0
  55. package/src/granular/media_pool_item.py +852 -0
  56. package/src/granular/media_storage.py +179 -0
  57. package/src/granular/project.py +1594 -0
  58. package/src/granular/resolve_control.py +521 -0
  59. package/src/granular/timeline.py +1074 -0
  60. package/src/granular/timeline_item.py +2251 -0
  61. package/src/resolve_mcp_server.py +43 -0
  62. package/src/server.py +15691 -0
  63. package/src/utils/__init__.py +3 -0
  64. package/src/utils/app_control.py +319 -0
  65. package/src/utils/audio_fairlight_live_probe.py +263 -0
  66. package/src/utils/cdl.py +20 -0
  67. package/src/utils/cloud_operations.py +192 -0
  68. package/src/utils/color_grade_live_probe.py +444 -0
  69. package/src/utils/dctl_templates.py +368 -0
  70. package/src/utils/extension_authoring_live_probe.py +292 -0
  71. package/src/utils/fuse_templates.py +1968 -0
  72. package/src/utils/fusion_composition_live_probe.py +284 -0
  73. package/src/utils/layout_presets.py +333 -0
  74. package/src/utils/mcp_stdio.py +32 -0
  75. package/src/utils/media_analysis.py +3618 -0
  76. package/src/utils/media_analysis_jobs.py +796 -0
  77. package/src/utils/media_pool_ingest_live_probe.py +592 -0
  78. package/src/utils/multicam.py +393 -0
  79. package/src/utils/object_inspection.py +287 -0
  80. package/src/utils/platform.py +157 -0
  81. package/src/utils/project_lifecycle_live_probe.py +376 -0
  82. package/src/utils/project_properties.py +601 -0
  83. package/src/utils/render_deliver_live_probe.py +384 -0
  84. package/src/utils/resolve_connection.py +77 -0
  85. package/src/utils/review_annotation_live_probe.py +352 -0
  86. package/src/utils/script_templates.py +1193 -0
  87. package/src/utils/sync_detection.py +887 -0
  88. package/src/utils/timeline_conform_live_probe.py +280 -0
  89. package/src/utils/timeline_kernel_live_probe.py +1091 -0
  90. package/src/utils/timeline_kernel_probe.py +185 -0
  91. package/src/utils/timeline_title_text.py +87 -0
  92. package/src/utils/update_check.py +610 -0
@@ -0,0 +1,592 @@
1
+ #!/usr/bin/env python3
2
+ """Live Media Pool / ingest boundary probe.
3
+
4
+ Creates a disposable Resolve project with generated synthetic media, probes
5
+ Media Storage, Media Pool, Folder, MediaPoolItem, and annotation surfaces,
6
+ writes JSON/Markdown evidence reports, and deletes the project unless
7
+ --keep-open is provided.
8
+
9
+ Run with Python 3.10-3.12 against a running Resolve Studio instance:
10
+
11
+ python3.11 tests/live_media_pool_ingest_validation.py --output-dir /tmp/media-pool-ingest-probe
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import platform
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ import tempfile
22
+ import time
23
+ from pathlib import Path
24
+ from typing import Any, Dict, Optional
25
+
26
+ from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
27
+
28
+
29
+ def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
30
+ if not isinstance(result, dict):
31
+ raise AssertionError(f"{label}: expected dict, got {result!r}")
32
+ if result.get("error"):
33
+ raise AssertionError(f"{label}: {result['error']}")
34
+ if "success" in result and result["success"] is not True:
35
+ raise AssertionError(f"{label}: expected success=True, got {result!r}")
36
+ return result
37
+
38
+
39
+ def _record_tool_result(
40
+ recorder: ProbeRecorder,
41
+ category: str,
42
+ name: str,
43
+ result: Dict[str, Any],
44
+ *,
45
+ expected_boundary: bool = False,
46
+ ) -> None:
47
+ if not isinstance(result, dict):
48
+ recorder.record(category, name, "error", details={"reason": "non-dict result", "result": repr(result)})
49
+ return
50
+ if result.get("error"):
51
+ recorder.record(
52
+ category,
53
+ name,
54
+ "unsupported" if expected_boundary else "error",
55
+ details={"reason": result.get("error"), "expected_boundary": expected_boundary},
56
+ evidence=result,
57
+ )
58
+ return
59
+ if "success" in result and result["success"] is not True:
60
+ recorder.record(
61
+ category,
62
+ name,
63
+ "partially_supported" if not expected_boundary else "unsupported",
64
+ details={"reason": "success returned false", "expected_boundary": expected_boundary},
65
+ evidence=result,
66
+ )
67
+ return
68
+ if expected_boundary and result.get("imported") == 0:
69
+ recorder.record(
70
+ category,
71
+ name,
72
+ "unsupported",
73
+ details={"reason": "Resolve returned zero imported items for expected negative fixture"},
74
+ evidence=result,
75
+ )
76
+ return
77
+ recorder.record(category, name, "supported", evidence=result)
78
+
79
+
80
+ def _run_ffmpeg(args: list[str]) -> None:
81
+ subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True)
82
+
83
+
84
+ def _make_synthetic_assets(work_dir: Path) -> Dict[str, Path]:
85
+ video = work_dir / "ingest_probe_video.mov"
86
+ video_alt = work_dir / "ingest_probe_video_alt.mov"
87
+ audio = work_dir / "ingest_probe_audio.wav"
88
+ still = work_dir / "ingest_probe_still.png"
89
+ sequence_dir = work_dir / "sequence"
90
+ sequence_dir.mkdir(parents=True, exist_ok=True)
91
+
92
+ _run_ffmpeg(
93
+ [
94
+ "-f",
95
+ "lavfi",
96
+ "-i",
97
+ "testsrc2=size=320x180:rate=24:duration=5",
98
+ "-f",
99
+ "lavfi",
100
+ "-i",
101
+ "sine=frequency=880:sample_rate=48000:duration=5",
102
+ "-shortest",
103
+ "-pix_fmt",
104
+ "yuv420p",
105
+ "-c:v",
106
+ "libx264",
107
+ "-c:a",
108
+ "aac",
109
+ "-y",
110
+ str(video),
111
+ ]
112
+ )
113
+ _run_ffmpeg(
114
+ [
115
+ "-f",
116
+ "lavfi",
117
+ "-i",
118
+ "testsrc=size=320x180:rate=24:duration=5",
119
+ "-f",
120
+ "lavfi",
121
+ "-i",
122
+ "sine=frequency=660:sample_rate=48000:duration=5",
123
+ "-shortest",
124
+ "-pix_fmt",
125
+ "yuv420p",
126
+ "-c:v",
127
+ "libx264",
128
+ "-c:a",
129
+ "aac",
130
+ "-y",
131
+ str(video_alt),
132
+ ]
133
+ )
134
+ _run_ffmpeg(["-f", "lavfi", "-i", "sine=frequency=440:sample_rate=48000:duration=3", "-y", str(audio)])
135
+ _run_ffmpeg(["-f", "lavfi", "-i", "color=c=red:s=96x54", "-frames:v", "1", "-y", str(still)])
136
+ for index, color in enumerate(("blue", "green", "yellow"), start=1):
137
+ frame_path = sequence_dir / f"ingest_probe_seq_{index:03d}.png"
138
+ _run_ffmpeg(["-f", "lavfi", "-i", f"color=c={color}:s=96x54", "-frames:v", "1", "-y", str(frame_path)])
139
+
140
+ unsupported = work_dir / "not_media.txt"
141
+ unsupported.write_text("synthetic non-media boundary fixture\n", encoding="utf-8")
142
+ return {
143
+ "video": video,
144
+ "video_alt": video_alt,
145
+ "audio": audio,
146
+ "still": still,
147
+ "sequence_pattern": sequence_dir / "ingest_probe_seq_%03d.png",
148
+ "unsupported": unsupported,
149
+ }
150
+
151
+
152
+ def _first_imported_clip_id(imported_items) -> Optional[str]:
153
+ for item in imported_items or []:
154
+ try:
155
+ item_id = item.GetUniqueId()
156
+ except Exception:
157
+ item_id = None
158
+ if item_id:
159
+ return str(item_id)
160
+ return None
161
+
162
+
163
+ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, Any]:
164
+ output_dir.mkdir(parents=True, exist_ok=True)
165
+ work_dir = Path(tempfile.mkdtemp(prefix="mcp_media_pool_ingest_probe_"))
166
+ project_name = f"_mcp_media_pool_ingest_probe_{int(time.time())}"
167
+ recorder = ProbeRecorder()
168
+ created_project = False
169
+ delete_result: Optional[Dict[str, Any]] = None
170
+
171
+ metadata: Dict[str, Any] = {
172
+ "title": "Media Pool Ingest Kernel Capability Probe",
173
+ "timestamp_utc": utc_timestamp(),
174
+ "python": sys.version,
175
+ "platform": platform.platform(),
176
+ "output_dir": str(output_dir),
177
+ "project_name": project_name,
178
+ }
179
+
180
+ try:
181
+ version = _require_success("resolve_control.get_version", server.resolve_control("get_version"))
182
+ metadata.update(
183
+ {
184
+ "product": version.get("product"),
185
+ "version": version.get("version"),
186
+ "version_string": version.get("version_string"),
187
+ }
188
+ )
189
+ print(f"Connected to {metadata['product']} {metadata['version_string']}")
190
+
191
+ _require_success("project_manager.create", server.project_manager("create", {"name": project_name}))
192
+ created_project = True
193
+ print(f"Created disposable project: {project_name}")
194
+ server.resolve_control("open_page", {"page": "media"})
195
+
196
+ assets = _make_synthetic_assets(work_dir)
197
+ metadata["synthetic_media"] = {key: str(value) for key, value in assets.items()}
198
+ print(f"Generated synthetic media under: {work_dir}")
199
+
200
+ resolve = server.get_resolve()
201
+ project = resolve.GetProjectManager().GetCurrentProject()
202
+ media_pool = project.GetMediaPool()
203
+
204
+ imported_video = media_pool.ImportMedia([str(assets["video"])]) or []
205
+ video_id = _first_imported_clip_id(imported_video)
206
+ if not video_id:
207
+ raise AssertionError("Failed to import synthetic video")
208
+ metadata["video_clip_id"] = video_id
209
+ print(f"Imported video clip: {video_id}")
210
+
211
+ imported_target = media_pool.ImportMedia([str(assets["video_alt"])]) or []
212
+ target_video_id = _first_imported_clip_id(imported_target)
213
+ if not target_video_id:
214
+ raise AssertionError("Failed to import target synthetic video")
215
+ metadata["target_video_clip_id"] = target_video_id
216
+
217
+ imports = {
218
+ "audio": server.media_pool("import_media", {"paths": [str(assets["audio"])]}),
219
+ "still": server.media_pool("import_media", {"paths": [str(assets["still"])]}),
220
+ "sequence": server.media_pool(
221
+ "import_media",
222
+ {
223
+ "clip_infos": [
224
+ {
225
+ "FilePath": str(assets["sequence_pattern"]),
226
+ "StartIndex": 1,
227
+ "EndIndex": 3,
228
+ }
229
+ ]
230
+ },
231
+ ),
232
+ "unsupported_text": server.media_pool("import_media", {"paths": [str(assets["unsupported"])]}),
233
+ }
234
+ for name, result in imports.items():
235
+ _record_tool_result(recorder, "imports", name, result, expected_boundary=(name == "unsupported_text"))
236
+
237
+ kernel_imports = {
238
+ "safe_import_media_dry_run": server.media_pool(
239
+ "safe_import_media",
240
+ {"paths": [str(assets["audio"])], "dry_run": True},
241
+ ),
242
+ "safe_import_media": server.media_pool(
243
+ "safe_import_media",
244
+ {"paths": [str(assets["audio"])]},
245
+ ),
246
+ "safe_import_sequence": server.media_pool(
247
+ "safe_import_sequence",
248
+ {
249
+ "pattern": str(assets["sequence_pattern"]),
250
+ "start_index": 1,
251
+ "end_index": 3,
252
+ },
253
+ ),
254
+ "safe_import_folder_dry_run": server.media_pool(
255
+ "safe_import_folder",
256
+ {"path": str(work_dir), "dry_run": True},
257
+ ),
258
+ }
259
+ for name, result in kernel_imports.items():
260
+ _record_tool_result(recorder, "kernel_imports", name, result)
261
+
262
+ _record_tool_result(recorder, "storage", "get_volumes", server.media_storage("get_volumes"))
263
+ _record_tool_result(
264
+ recorder,
265
+ "storage",
266
+ "get_files_work_dir",
267
+ server.media_storage("get_files", {"path": str(work_dir)}),
268
+ )
269
+ _record_tool_result(recorder, "capabilities", "ingest_capabilities", server.media_pool("ingest_capabilities"))
270
+ _record_tool_result(recorder, "probe", "probe_media_pool", server.media_pool("probe_media_pool", {"depth": 2}))
271
+ _record_tool_result(
272
+ recorder,
273
+ "probe",
274
+ "probe_ingest_item_by_id",
275
+ server.media_pool("probe_ingest_item", {"clip_ids": [video_id]}),
276
+ )
277
+ _record_tool_result(
278
+ recorder,
279
+ "probe",
280
+ "probe_clip_properties",
281
+ server.media_pool("probe_clip_properties", {"clip_ids": [video_id]}),
282
+ )
283
+ _record_tool_result(
284
+ recorder,
285
+ "probe",
286
+ "media_pool_boundary_report",
287
+ server.media_pool("media_pool_boundary_report", {"depth": 1, "clip_ids": [video_id]}),
288
+ )
289
+
290
+ _record_tool_result(
291
+ recorder,
292
+ "organization",
293
+ "set_selected",
294
+ server.media_pool("set_selected", {"clip_id": video_id}),
295
+ )
296
+ _record_tool_result(recorder, "organization", "get_selected", server.media_pool("get_selected"))
297
+ _record_tool_result(
298
+ recorder,
299
+ "probe",
300
+ "probe_ingest_item_selected",
301
+ server.media_pool("probe_ingest_item", {"selected": True}),
302
+ )
303
+
304
+ subfolder = server.media_pool("add_subfolder", {"name": "Ingest Probe"})
305
+ _record_tool_result(recorder, "organization", "add_subfolder", subfolder)
306
+ _record_tool_result(
307
+ recorder,
308
+ "organization",
309
+ "set_current_folder",
310
+ server.media_pool("set_current_folder", {"path": "Master/Ingest Probe"}),
311
+ )
312
+ _record_tool_result(recorder, "folder", "get_current_folder", server.media_pool("get_current_folder"))
313
+ _record_tool_result(recorder, "folder", "folder_get_subfolders", server.folder("get_subfolders", {"path": "Master"}))
314
+ _record_tool_result(recorder, "folder", "folder_is_stale", server.folder("is_stale", {"path": "Master"}))
315
+ _record_tool_result(recorder, "organization", "reset_current_folder", server.media_pool("set_current_folder", {"path": "Master"}))
316
+ _record_tool_result(
317
+ recorder,
318
+ "organization",
319
+ "organize_clips_dry_run",
320
+ server.media_pool(
321
+ "organize_clips",
322
+ {"clip_ids": [target_video_id], "target_path": "Master/Ingest Probe", "dry_run": True},
323
+ ),
324
+ )
325
+ _record_tool_result(
326
+ recorder,
327
+ "organization",
328
+ "organize_clips",
329
+ server.media_pool(
330
+ "organize_clips",
331
+ {"clip_ids": [target_video_id], "target_path": "Master/Ingest Probe"},
332
+ ),
333
+ )
334
+
335
+ _record_tool_result(
336
+ recorder,
337
+ "metadata",
338
+ "set_metadata_scalar",
339
+ server.media_pool_item("set_metadata", {"clip_id": video_id, "key": "Comments", "value": "MCP ingest probe"}),
340
+ )
341
+ _record_tool_result(
342
+ recorder,
343
+ "metadata",
344
+ "get_metadata_comments",
345
+ server.media_pool_item("get_metadata", {"clip_id": video_id, "key": "Comments"}),
346
+ )
347
+ _record_tool_result(
348
+ recorder,
349
+ "metadata",
350
+ "set_third_party_metadata",
351
+ server.media_pool_item(
352
+ "set_third_party_metadata",
353
+ {"clip_id": video_id, "key": "mcp_ingest_probe", "value": "supported"},
354
+ ),
355
+ )
356
+ _record_tool_result(
357
+ recorder,
358
+ "metadata",
359
+ "get_third_party_metadata",
360
+ server.media_pool_item("get_third_party_metadata", {"clip_id": video_id, "key": "mcp_ingest_probe"}),
361
+ )
362
+ _record_tool_result(
363
+ recorder,
364
+ "metadata",
365
+ "get_clip_property_all",
366
+ server.media_pool_item("get_clip_property", {"clip_id": video_id}),
367
+ )
368
+ _record_tool_result(
369
+ recorder,
370
+ "metadata",
371
+ "get_clip_property_file_path",
372
+ server.media_pool_item("get_clip_property", {"clip_id": video_id, "key": "File Path"}),
373
+ )
374
+ _record_tool_result(
375
+ recorder,
376
+ "metadata",
377
+ "normalize_metadata",
378
+ server.media_pool(
379
+ "normalize_metadata",
380
+ {
381
+ "clip_ids": [target_video_id],
382
+ "metadata": {"Comments": "MCP normalized metadata"},
383
+ "third_party_metadata": {"mcp_ingest_normalized": "true"},
384
+ },
385
+ ),
386
+ )
387
+ _record_tool_result(
388
+ recorder,
389
+ "metadata",
390
+ "copy_metadata",
391
+ server.media_pool(
392
+ "copy_metadata",
393
+ {"source_clip_id": video_id, "target_clip_ids": [target_video_id], "include_third_party": True},
394
+ ),
395
+ )
396
+
397
+ _record_tool_result(
398
+ recorder,
399
+ "annotations",
400
+ "set_clip_color",
401
+ server.media_pool_item("set_clip_color", {"clip_id": video_id, "color": "Blue"}),
402
+ )
403
+ _record_tool_result(
404
+ recorder,
405
+ "annotations",
406
+ "get_clip_color",
407
+ server.media_pool_item("get_clip_color", {"clip_id": video_id}),
408
+ )
409
+ _record_tool_result(
410
+ recorder,
411
+ "annotations",
412
+ "marker_add",
413
+ server.media_pool_item_markers(
414
+ "add",
415
+ {
416
+ "clip_id": video_id,
417
+ "frame": 12,
418
+ "color": "Blue",
419
+ "name": "Ingest Probe",
420
+ "note": "Synthetic media marker",
421
+ "duration": 1,
422
+ "custom_data": "mcp-ingest-probe",
423
+ },
424
+ ),
425
+ )
426
+ _record_tool_result(
427
+ recorder,
428
+ "annotations",
429
+ "marker_get_custom_data",
430
+ server.media_pool_item_markers(
431
+ "get_custom_data",
432
+ {"clip_id": video_id, "frame": 12},
433
+ ),
434
+ )
435
+ _record_tool_result(
436
+ recorder,
437
+ "annotations",
438
+ "marker_update_custom_data",
439
+ server.media_pool_item_markers(
440
+ "update_custom_data",
441
+ {"clip_id": video_id, "frame": 12, "custom_data": "mcp-ingest-probe-updated"},
442
+ ),
443
+ )
444
+ _record_tool_result(
445
+ recorder,
446
+ "annotations",
447
+ "flag_add",
448
+ server.media_pool_item_markers("add_flag", {"clip_id": video_id, "color": "Blue"}),
449
+ )
450
+ _record_tool_result(
451
+ recorder,
452
+ "annotations",
453
+ "flag_get",
454
+ server.media_pool_item_markers("get_flags", {"clip_id": video_id}),
455
+ )
456
+ _record_tool_result(
457
+ recorder,
458
+ "annotations",
459
+ "set_mark_in_out",
460
+ server.media_pool_item("set_mark_in_out", {"clip_id": video_id, "mark_in": 0, "mark_out": 24}),
461
+ )
462
+ _record_tool_result(
463
+ recorder,
464
+ "annotations",
465
+ "get_mark_in_out",
466
+ server.media_pool_item("get_mark_in_out", {"clip_id": video_id}),
467
+ )
468
+ _record_tool_result(
469
+ recorder,
470
+ "annotations",
471
+ "set_clip_marks",
472
+ server.media_pool("set_clip_marks", {"clip_ids": [target_video_id], "mark_in": 0, "mark_out": 24}),
473
+ )
474
+ _record_tool_result(
475
+ recorder,
476
+ "annotations",
477
+ "copy_clip_annotations",
478
+ server.media_pool(
479
+ "copy_clip_annotations",
480
+ {"source_clip_id": video_id, "target_clip_ids": [target_video_id]},
481
+ ),
482
+ )
483
+
484
+ _record_tool_result(
485
+ recorder,
486
+ "links",
487
+ "link_proxy",
488
+ server.media_pool_item("link_proxy", {"clip_id": video_id, "proxy_path": str(assets["video"])}),
489
+ )
490
+ _record_tool_result(
491
+ recorder,
492
+ "links",
493
+ "unlink_proxy",
494
+ server.media_pool_item("unlink_proxy", {"clip_id": video_id}),
495
+ )
496
+ _record_tool_result(
497
+ recorder,
498
+ "links",
499
+ "link_proxy_checked",
500
+ server.media_pool("link_proxy_checked", {"clip_id": video_id, "proxy_path": str(assets["video"])}),
501
+ )
502
+ _record_tool_result(
503
+ recorder,
504
+ "links",
505
+ "safe_relink_dry_run",
506
+ server.media_pool("safe_relink", {"clip_ids": [video_id], "folder_path": str(work_dir), "dry_run": True}),
507
+ )
508
+ _record_tool_result(
509
+ recorder,
510
+ "links",
511
+ "safe_unlink_dry_run",
512
+ server.media_pool("safe_unlink", {"clip_ids": [video_id], "dry_run": True}),
513
+ )
514
+ _record_tool_result(
515
+ recorder,
516
+ "links",
517
+ "link_full_resolution_checked",
518
+ server.media_pool("link_full_resolution_checked", {"clip_id": video_id, "path": str(assets["video"])}),
519
+ )
520
+
521
+ metadata_path = output_dir / "ingest_metadata.csv"
522
+ _record_tool_result(
523
+ recorder,
524
+ "exports",
525
+ "export_metadata",
526
+ server.media_pool("export_metadata", {"path": str(metadata_path), "clip_ids": [video_id]}),
527
+ )
528
+
529
+ _record_tool_result(
530
+ recorder,
531
+ "cleanup_mutations",
532
+ "clear_clip_color",
533
+ server.media_pool_item("clear_clip_color", {"clip_id": video_id}),
534
+ )
535
+ _record_tool_result(
536
+ recorder,
537
+ "cleanup_mutations",
538
+ "clear_flags",
539
+ server.media_pool_item_markers("clear_flags", {"clip_id": video_id, "color": "Blue"}),
540
+ )
541
+ _record_tool_result(
542
+ recorder,
543
+ "cleanup_mutations",
544
+ "delete_marker",
545
+ server.media_pool_item_markers("delete_at_frame", {"clip_id": video_id, "frame": 12}),
546
+ )
547
+ _record_tool_result(
548
+ recorder,
549
+ "cleanup_mutations",
550
+ "clear_mark_in_out",
551
+ server.media_pool_item("clear_mark_in_out", {"clip_id": video_id}),
552
+ )
553
+ _record_tool_result(
554
+ recorder,
555
+ "cleanup_mutations",
556
+ "clear_clip_marks",
557
+ server.media_pool("clear_clip_marks", {"clip_ids": [target_video_id]}),
558
+ )
559
+
560
+ if keep_open:
561
+ server.project_manager("save")
562
+ print(f"LEFT PROJECT OPEN FOR INSPECTION: {project_name}")
563
+ created_project = False
564
+
565
+ finally:
566
+ if created_project:
567
+ server.project_manager("save")
568
+ server.project_manager("close")
569
+ delete_result = server.project_manager("delete", {"name": project_name})
570
+ print(f"Deleted disposable project: {delete_result}")
571
+
572
+ report = recorder.to_report(
573
+ metadata,
574
+ {
575
+ "json": str(output_dir / "media-pool-ingest-probe.json"),
576
+ "markdown": str(output_dir / "media-pool-ingest-probe.md"),
577
+ },
578
+ )
579
+ json_path = output_dir / "media-pool-ingest-probe.json"
580
+ markdown_path = output_dir / "media-pool-ingest-probe.md"
581
+ json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
582
+ markdown_path.write_text(render_markdown_report(report), encoding="utf-8")
583
+ print(f"Wrote JSON report: {json_path}")
584
+ print(f"Wrote Markdown report: {markdown_path}")
585
+ print(f"Counts: {json.dumps(report['counts'], sort_keys=True)}")
586
+ if not keep_open:
587
+ shutil.rmtree(work_dir, ignore_errors=True)
588
+ print(f"Removed synthetic media directory: {work_dir}")
589
+
590
+ if delete_result and delete_result.get("success") is not True:
591
+ raise AssertionError(f"Cleanup failed for {project_name}: {delete_result!r}")
592
+ return report