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.
- package/AGENTS.md +85 -0
- package/CHANGELOG.md +802 -0
- package/CLAUDE.md +15 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/SECURITY.md +53 -0
- package/bin/davinci-resolve-mcp.mjs +376 -0
- package/docs/README.md +56 -0
- package/docs/SKILL.md +1145 -0
- package/docs/authoring/fuse-dctl-authoring.md +242 -0
- package/docs/authoring/script-plugin-authoring.md +195 -0
- package/docs/contributing.md +82 -0
- package/docs/guides/color-decision-guide.md +387 -0
- package/docs/guides/editorial-decision-guide.md +136 -0
- package/docs/guides/media-analysis-guide.md +615 -0
- package/docs/guides/multicam-setup-guide.md +138 -0
- package/docs/install.md +198 -0
- package/docs/integrations/workflow-integrations.md +120 -0
- package/docs/kernels/README.md +28 -0
- package/docs/kernels/audio-fairlight-kernel.md +86 -0
- package/docs/kernels/color-grade-kernel.md +103 -0
- package/docs/kernels/extension-authoring-kernel.md +101 -0
- package/docs/kernels/fusion-composition-kernel.md +91 -0
- package/docs/kernels/media-pool-ingest-kernel.md +147 -0
- package/docs/kernels/project-lifecycle-kernel.md +120 -0
- package/docs/kernels/render-deliver-kernel.md +92 -0
- package/docs/kernels/review-annotation-kernel.md +110 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
- package/docs/kernels/timeline-edit-kernel.md +189 -0
- package/docs/notes/codec-plugin-notes.md +136 -0
- package/docs/notes/dctl-notes.md +234 -0
- package/docs/notes/fusion-template-notes.md +136 -0
- package/docs/notes/lut-notes.md +136 -0
- package/docs/notes/openfx-notes.md +120 -0
- package/docs/process/release-process.md +152 -0
- package/docs/reference/api-coverage.md +488 -0
- package/docs/reference/resolve_scripting_api.txt +1012 -0
- package/examples/README.md +53 -0
- package/examples/markers/README.md +81 -0
- package/examples/media/README.md +94 -0
- package/examples/timeline/README.md +98 -0
- package/install.py +1196 -0
- package/package.json +52 -0
- package/scripts/audit_api_parity.py +275 -0
- package/scripts/live_media_analysis_polish_probe.py +65 -0
- package/src/__init__.py +3 -0
- package/src/analysis_dashboard.py +4936 -0
- package/src/control_panel.py +13 -0
- package/src/granular/__init__.py +17 -0
- package/src/granular/common.py +727 -0
- package/src/granular/folder.py +287 -0
- package/src/granular/gallery.py +306 -0
- package/src/granular/graph.py +309 -0
- package/src/granular/media_pool.py +679 -0
- package/src/granular/media_pool_item.py +852 -0
- package/src/granular/media_storage.py +179 -0
- package/src/granular/project.py +1594 -0
- package/src/granular/resolve_control.py +521 -0
- package/src/granular/timeline.py +1074 -0
- package/src/granular/timeline_item.py +2251 -0
- package/src/resolve_mcp_server.py +43 -0
- package/src/server.py +15691 -0
- package/src/utils/__init__.py +3 -0
- package/src/utils/app_control.py +319 -0
- package/src/utils/audio_fairlight_live_probe.py +263 -0
- package/src/utils/cdl.py +20 -0
- package/src/utils/cloud_operations.py +192 -0
- package/src/utils/color_grade_live_probe.py +444 -0
- package/src/utils/dctl_templates.py +368 -0
- package/src/utils/extension_authoring_live_probe.py +292 -0
- package/src/utils/fuse_templates.py +1968 -0
- package/src/utils/fusion_composition_live_probe.py +284 -0
- package/src/utils/layout_presets.py +333 -0
- package/src/utils/mcp_stdio.py +32 -0
- package/src/utils/media_analysis.py +3618 -0
- package/src/utils/media_analysis_jobs.py +796 -0
- package/src/utils/media_pool_ingest_live_probe.py +592 -0
- package/src/utils/multicam.py +393 -0
- package/src/utils/object_inspection.py +287 -0
- package/src/utils/platform.py +157 -0
- package/src/utils/project_lifecycle_live_probe.py +376 -0
- package/src/utils/project_properties.py +601 -0
- package/src/utils/render_deliver_live_probe.py +384 -0
- package/src/utils/resolve_connection.py +77 -0
- package/src/utils/review_annotation_live_probe.py +352 -0
- package/src/utils/script_templates.py +1193 -0
- package/src/utils/sync_detection.py +887 -0
- package/src/utils/timeline_conform_live_probe.py +280 -0
- package/src/utils/timeline_kernel_live_probe.py +1091 -0
- package/src/utils/timeline_kernel_probe.py +185 -0
- package/src/utils/timeline_title_text.py +87 -0
- 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
|