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,280 @@
1
+ #!/usr/bin/env python3
2
+ """Live Timeline Conform / Interchange boundary probe."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import platform
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import tempfile
12
+ import time
13
+ from pathlib import Path
14
+ from typing import Any, Dict, Optional
15
+
16
+ from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
17
+
18
+
19
+ def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
20
+ if not isinstance(result, dict):
21
+ raise AssertionError(f"{label}: expected dict, got {result!r}")
22
+ if result.get("error"):
23
+ raise AssertionError(f"{label}: {result['error']}")
24
+ if "success" in result and result["success"] is not True:
25
+ raise AssertionError(f"{label}: expected success=True, got {result!r}")
26
+ return result
27
+
28
+
29
+ def _record_tool_result(
30
+ recorder: ProbeRecorder,
31
+ category: str,
32
+ name: str,
33
+ result: Dict[str, Any],
34
+ *,
35
+ expected_status: Optional[str] = None,
36
+ ) -> None:
37
+ if not isinstance(result, dict):
38
+ recorder.record(category, name, "error", details={"reason": "non-dict result", "result": repr(result)})
39
+ return
40
+ if result.get("error"):
41
+ recorder.record(category, name, expected_status or "error", details={"reason": result.get("error")}, evidence=result)
42
+ return
43
+ if "success" in result and result["success"] is not True:
44
+ recorder.record(
45
+ category,
46
+ name,
47
+ expected_status or "partially_supported",
48
+ details={"reason": "success returned false"},
49
+ evidence=result,
50
+ )
51
+ return
52
+ recorder.record(category, name, expected_status or "supported", evidence=result)
53
+
54
+
55
+ def _run_ffmpeg(args: list[str]) -> None:
56
+ subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True)
57
+
58
+
59
+ def _make_synthetic_video(work_dir: Path, name: str, source: str, frequency: int) -> Path:
60
+ video = work_dir / name
61
+ _run_ffmpeg(
62
+ [
63
+ "-f",
64
+ "lavfi",
65
+ "-i",
66
+ f"{source}=size=640x360:rate=24:duration=4",
67
+ "-f",
68
+ "lavfi",
69
+ "-i",
70
+ f"sine=frequency={frequency}:sample_rate=48000:duration=4",
71
+ "-shortest",
72
+ "-pix_fmt",
73
+ "yuv420p",
74
+ "-c:v",
75
+ "libx264",
76
+ "-c:a",
77
+ "aac",
78
+ "-y",
79
+ str(video),
80
+ ]
81
+ )
82
+ return video
83
+
84
+
85
+ def _first_imported(imported_items):
86
+ for item in imported_items or []:
87
+ try:
88
+ if item.GetUniqueId():
89
+ return item
90
+ except Exception:
91
+ pass
92
+ return None
93
+
94
+
95
+ def _clip_id(clip) -> Optional[str]:
96
+ try:
97
+ return str(clip.GetUniqueId())
98
+ except Exception:
99
+ return None
100
+
101
+
102
+ def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, Any]:
103
+ output_dir.mkdir(parents=True, exist_ok=True)
104
+ work_dir = Path(tempfile.mkdtemp(prefix="mcp_timeline_conform_probe_"))
105
+ interchange_dir = output_dir / "interchange"
106
+ interchange_dir.mkdir(parents=True, exist_ok=True)
107
+ project_name = f"_mcp_timeline_conform_probe_{int(time.time())}"
108
+ timeline_name = "Timeline Conform Probe"
109
+ recorder = ProbeRecorder()
110
+ created_project = False
111
+ delete_result: Optional[Dict[str, Any]] = None
112
+
113
+ metadata: Dict[str, Any] = {
114
+ "title": "Timeline Conform / Interchange Kernel Capability Probe",
115
+ "timestamp_utc": utc_timestamp(),
116
+ "python": sys.version,
117
+ "platform": platform.platform(),
118
+ "output_dir": str(output_dir),
119
+ "project_name": project_name,
120
+ }
121
+
122
+ try:
123
+ version = _require_success("resolve_control.get_version", server.resolve_control("get_version"))
124
+ metadata.update(
125
+ {
126
+ "product": version.get("product"),
127
+ "version": version.get("version"),
128
+ "version_string": version.get("version_string"),
129
+ }
130
+ )
131
+ print(f"Connected to {metadata['product']} {metadata['version_string']}")
132
+
133
+ _require_success("project_manager.create", server.project_manager("create", {"name": project_name}))
134
+ created_project = True
135
+ print(f"Created disposable project: {project_name}")
136
+ server.resolve_control("open_page", {"page": "edit"})
137
+
138
+ video_a = _make_synthetic_video(work_dir, "conform_A.mov", "testsrc2", 440)
139
+ video_b = _make_synthetic_video(work_dir, "conform_B.mov", "testsrc", 660)
140
+ metadata["synthetic_media"] = {"video_a": str(video_a), "video_b": str(video_b)}
141
+ print(f"Generated synthetic media under: {work_dir}")
142
+
143
+ resolve = server.get_resolve()
144
+ project = resolve.GetProjectManager().GetCurrentProject()
145
+ media_pool = project.GetMediaPool()
146
+ imported = media_pool.ImportMedia([str(video_a), str(video_b)]) or []
147
+ if len(imported) < 2:
148
+ raise AssertionError("Failed to import synthetic conform media")
149
+ clip_a = _first_imported([imported[0]])
150
+ clip_b = _first_imported([imported[1]])
151
+ if not clip_a or not clip_b:
152
+ raise AssertionError("Failed to resolve imported MediaPoolItems")
153
+
154
+ timeline = media_pool.CreateTimelineFromClips(
155
+ timeline_name,
156
+ [{"mediaPoolItem": clip_a, "startFrame": 0, "endFrame": 48, "recordFrame": 86400}],
157
+ )
158
+ if not timeline:
159
+ raise AssertionError("Failed to create conform timeline")
160
+ project.SetCurrentTimeline(timeline)
161
+ media_pool.AppendToTimeline(
162
+ [
163
+ {"mediaPoolItem": clip_b, "startFrame": 0, "endFrame": 48, "recordFrame": 86472, "trackIndex": 1, "mediaType": 1},
164
+ {"mediaPoolItem": clip_b, "startFrame": 0, "endFrame": 48, "recordFrame": 86400, "trackIndex": 1, "mediaType": 2},
165
+ ]
166
+ )
167
+ print(f"Created timeline: {timeline_name}")
168
+
169
+ _record_tool_result(recorder, "capabilities", "conform_capabilities", server.timeline("conform_capabilities"))
170
+ _record_tool_result(recorder, "inspection", "probe_timeline_structure", server.timeline("probe_timeline_structure"))
171
+ _record_tool_result(recorder, "analysis", "detect_gaps_overlaps", server.timeline("detect_gaps_overlaps"))
172
+ _record_tool_result(recorder, "analysis", "source_range_report", server.timeline("source_range_report", {"handles": 8}))
173
+ _record_tool_result(recorder, "analysis", "detect_missing_media_initial", server.timeline("detect_missing_media"))
174
+ _record_tool_result(
175
+ recorder,
176
+ "analysis",
177
+ "build_relink_plan_initial",
178
+ server.timeline("build_relink_plan", {"search_roots": [str(work_dir)]}),
179
+ )
180
+ _record_tool_result(recorder, "report", "conform_boundary_report", server.timeline("conform_boundary_report", {"handles": 8}))
181
+
182
+ for fmt, suffix in (("fcpxml", ".fcpxml"), ("drt", ".drt"), ("edl", ".edl"), ("aaf", ".aaf"), ("otio", ".otio")):
183
+ result = server.timeline(
184
+ "export_timeline_checked",
185
+ {"format": fmt, "path": str(interchange_dir / f"conform_probe_{fmt}{suffix}")},
186
+ )
187
+ _record_tool_result(
188
+ recorder,
189
+ "interchange.export",
190
+ f"export_{fmt}",
191
+ result,
192
+ expected_status=None if result.get("success") else "version_or_page_dependent",
193
+ )
194
+
195
+ for fmt in ("fcpxml", "drt"):
196
+ result = server.timeline(
197
+ "probe_interchange_roundtrip",
198
+ {
199
+ "format": fmt,
200
+ "output_dir": str(interchange_dir),
201
+ "cleanup_imported": True,
202
+ "import_source_clips": False,
203
+ "include_clip_properties": False,
204
+ },
205
+ )
206
+ roundtrip_status = None
207
+ if result.get("success") and (result.get("comparison") or {}).get("difference_count", 0):
208
+ roundtrip_status = "partially_supported"
209
+ elif not result.get("success"):
210
+ roundtrip_status = "version_or_page_dependent"
211
+ _record_tool_result(
212
+ recorder,
213
+ "interchange.roundtrip",
214
+ f"roundtrip_{fmt}",
215
+ result,
216
+ expected_status=roundtrip_status,
217
+ )
218
+
219
+ # Exercise synthetic-only missing-media and relink surfaces through the
220
+ # existing Media Pool safe wrappers. These operate on generated media.
221
+ clip_b_id = _clip_id(clip_b)
222
+ if clip_b_id:
223
+ _record_tool_result(
224
+ recorder,
225
+ "missing_media.synthetic",
226
+ "safe_unlink_synthetic_clip",
227
+ server.media_pool("safe_unlink", {"clip_ids": [clip_b_id]}),
228
+ )
229
+ _record_tool_result(
230
+ recorder,
231
+ "missing_media.synthetic",
232
+ "detect_missing_media_after_unlink",
233
+ server.timeline("detect_missing_media"),
234
+ )
235
+ _record_tool_result(
236
+ recorder,
237
+ "missing_media.synthetic",
238
+ "build_relink_plan_after_unlink",
239
+ server.timeline("build_relink_plan", {"search_roots": [str(work_dir)]}),
240
+ )
241
+ _record_tool_result(
242
+ recorder,
243
+ "missing_media.synthetic",
244
+ "safe_relink_synthetic_clip",
245
+ server.media_pool("safe_relink", {"clip_ids": [clip_b_id], "folder_path": str(work_dir)}),
246
+ )
247
+
248
+ if keep_open:
249
+ server.project_manager("save")
250
+ print(f"LEFT PROJECT OPEN FOR INSPECTION: {project_name}")
251
+ created_project = False
252
+
253
+ finally:
254
+ if created_project:
255
+ server.project_manager("save")
256
+ server.project_manager("close")
257
+ delete_result = server.project_manager("delete", {"name": project_name})
258
+ print(f"Deleted disposable project: {delete_result}")
259
+
260
+ report = recorder.to_report(
261
+ metadata,
262
+ {
263
+ "json": str(output_dir / "timeline-conform-probe.json"),
264
+ "markdown": str(output_dir / "timeline-conform-probe.md"),
265
+ },
266
+ )
267
+ json_path = output_dir / "timeline-conform-probe.json"
268
+ markdown_path = output_dir / "timeline-conform-probe.md"
269
+ json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
270
+ markdown_path.write_text(render_markdown_report(report), encoding="utf-8")
271
+ print(f"Wrote JSON report: {json_path}")
272
+ print(f"Wrote Markdown report: {markdown_path}")
273
+ print(f"Counts: {json.dumps(report['counts'], sort_keys=True)}")
274
+ if not keep_open:
275
+ shutil.rmtree(work_dir, ignore_errors=True)
276
+ print(f"Removed synthetic media directory: {work_dir}")
277
+
278
+ if delete_result and delete_result.get("success") is not True:
279
+ raise AssertionError(f"Cleanup failed for {project_name}: {delete_result!r}")
280
+ return report