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,384 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Live Render / Deliver boundary probe.
|
|
3
|
+
|
|
4
|
+
Creates a disposable Resolve project with generated synthetic media, probes
|
|
5
|
+
render format/codec/settings/job/quick-export surfaces, writes JSON/Markdown
|
|
6
|
+
evidence reports, deletes the project, and removes generated media/render
|
|
7
|
+
outputs.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import platform
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any, Dict, Optional
|
|
21
|
+
|
|
22
|
+
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
26
|
+
if not isinstance(result, dict):
|
|
27
|
+
raise AssertionError(f"{label}: expected dict, got {result!r}")
|
|
28
|
+
if result.get("error"):
|
|
29
|
+
raise AssertionError(f"{label}: {result['error']}")
|
|
30
|
+
if "success" in result and result["success"] is not True:
|
|
31
|
+
raise AssertionError(f"{label}: expected success=True, got {result!r}")
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _record_tool_result(
|
|
36
|
+
recorder: ProbeRecorder,
|
|
37
|
+
category: str,
|
|
38
|
+
name: str,
|
|
39
|
+
result: Dict[str, Any],
|
|
40
|
+
*,
|
|
41
|
+
partial_on_false: bool = True,
|
|
42
|
+
) -> None:
|
|
43
|
+
if not isinstance(result, dict):
|
|
44
|
+
recorder.record(category, name, "error", details={"reason": "non-dict result", "result": repr(result)})
|
|
45
|
+
return
|
|
46
|
+
if result.get("error"):
|
|
47
|
+
recorder.record(category, name, "error", details={"reason": result.get("error")}, evidence=result)
|
|
48
|
+
return
|
|
49
|
+
if "success" in result and result["success"] is not True:
|
|
50
|
+
recorder.record(
|
|
51
|
+
category,
|
|
52
|
+
name,
|
|
53
|
+
"partially_supported" if partial_on_false else "unsupported",
|
|
54
|
+
details={"reason": "success returned false"},
|
|
55
|
+
evidence=result,
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
recorder.record(category, name, "supported", evidence=result)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _run_ffmpeg(args: list[str]) -> None:
|
|
62
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _make_synthetic_video(work_dir: Path) -> Path:
|
|
66
|
+
video = work_dir / "render_probe_source.mov"
|
|
67
|
+
_run_ffmpeg(
|
|
68
|
+
[
|
|
69
|
+
"-f",
|
|
70
|
+
"lavfi",
|
|
71
|
+
"-i",
|
|
72
|
+
"testsrc2=size=320x180:rate=24:duration=2",
|
|
73
|
+
"-f",
|
|
74
|
+
"lavfi",
|
|
75
|
+
"-i",
|
|
76
|
+
"sine=frequency=880:sample_rate=48000:duration=2",
|
|
77
|
+
"-shortest",
|
|
78
|
+
"-pix_fmt",
|
|
79
|
+
"yuv420p",
|
|
80
|
+
"-c:v",
|
|
81
|
+
"libx264",
|
|
82
|
+
"-c:a",
|
|
83
|
+
"aac",
|
|
84
|
+
"-y",
|
|
85
|
+
str(video),
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
return video
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _first_imported_clip(imported_items):
|
|
92
|
+
for item in imported_items or []:
|
|
93
|
+
try:
|
|
94
|
+
if item.GetUniqueId():
|
|
95
|
+
return item
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _choose_render_format_codec(server) -> Dict[str, Optional[str]]:
|
|
102
|
+
formats_result = server.render("get_formats")
|
|
103
|
+
formats = formats_result.get("formats") if isinstance(formats_result, dict) else {}
|
|
104
|
+
if not isinstance(formats, dict) or not formats:
|
|
105
|
+
return {"format": None, "codec": None, "label": None}
|
|
106
|
+
|
|
107
|
+
preferred_formats = ["mp4", "QuickTime"]
|
|
108
|
+
format_names = [fmt for fmt in preferred_formats if fmt in formats]
|
|
109
|
+
format_names.extend([fmt for fmt in formats if fmt not in format_names])
|
|
110
|
+
|
|
111
|
+
for fmt in format_names:
|
|
112
|
+
codecs_result = server.render("get_codecs", {"format": fmt})
|
|
113
|
+
codecs = codecs_result.get("codecs") if isinstance(codecs_result, dict) else {}
|
|
114
|
+
if not isinstance(codecs, dict) or not codecs:
|
|
115
|
+
continue
|
|
116
|
+
preferred = []
|
|
117
|
+
for label, codec in codecs.items():
|
|
118
|
+
text = f"{label} {codec}".lower()
|
|
119
|
+
if "h.264" in text or "h264" in text:
|
|
120
|
+
preferred.insert(0, (label, codec))
|
|
121
|
+
else:
|
|
122
|
+
preferred.append((label, codec))
|
|
123
|
+
label, codec = preferred[0]
|
|
124
|
+
return {"format": fmt, "codec": codec, "label": label}
|
|
125
|
+
return {"format": None, "codec": None, "label": None}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _render_output_files(render_dir: Path):
|
|
129
|
+
if not render_dir.exists():
|
|
130
|
+
return []
|
|
131
|
+
return sorted(str(path) for path in render_dir.iterdir() if path.is_file())
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, Any]:
|
|
135
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
work_dir = Path(tempfile.mkdtemp(prefix="mcp_render_deliver_probe_"))
|
|
137
|
+
render_dir = output_dir / "renders"
|
|
138
|
+
render_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
project_name = f"_mcp_render_deliver_probe_{int(time.time())}"
|
|
140
|
+
timeline_name = "Render Deliver Probe Timeline"
|
|
141
|
+
preset_name = f"_mcp_render_probe_preset_{int(time.time())}"
|
|
142
|
+
recorder = ProbeRecorder()
|
|
143
|
+
created_project = False
|
|
144
|
+
delete_result: Optional[Dict[str, Any]] = None
|
|
145
|
+
|
|
146
|
+
metadata: Dict[str, Any] = {
|
|
147
|
+
"title": "Render Deliver Kernel Capability Probe",
|
|
148
|
+
"timestamp_utc": utc_timestamp(),
|
|
149
|
+
"python": sys.version,
|
|
150
|
+
"platform": platform.platform(),
|
|
151
|
+
"output_dir": str(output_dir),
|
|
152
|
+
"project_name": project_name,
|
|
153
|
+
"render_dir": str(render_dir),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
version = _require_success("resolve_control.get_version", server.resolve_control("get_version"))
|
|
158
|
+
metadata.update(
|
|
159
|
+
{
|
|
160
|
+
"product": version.get("product"),
|
|
161
|
+
"version": version.get("version"),
|
|
162
|
+
"version_string": version.get("version_string"),
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
print(f"Connected to {metadata['product']} {metadata['version_string']}")
|
|
166
|
+
|
|
167
|
+
_require_success("project_manager.create", server.project_manager("create", {"name": project_name}))
|
|
168
|
+
created_project = True
|
|
169
|
+
print(f"Created disposable project: {project_name}")
|
|
170
|
+
server.resolve_control("open_page", {"page": "edit"})
|
|
171
|
+
|
|
172
|
+
video = _make_synthetic_video(work_dir)
|
|
173
|
+
metadata["synthetic_media"] = {"video": str(video)}
|
|
174
|
+
print(f"Generated synthetic media under: {work_dir}")
|
|
175
|
+
|
|
176
|
+
resolve = server.get_resolve()
|
|
177
|
+
project = resolve.GetProjectManager().GetCurrentProject()
|
|
178
|
+
media_pool = project.GetMediaPool()
|
|
179
|
+
imported = media_pool.ImportMedia([str(video)]) or []
|
|
180
|
+
clip = _first_imported_clip(imported)
|
|
181
|
+
if not clip:
|
|
182
|
+
raise AssertionError("Failed to import synthetic render media")
|
|
183
|
+
timeline = media_pool.CreateTimelineFromClips(timeline_name, [clip])
|
|
184
|
+
if not timeline:
|
|
185
|
+
raise AssertionError("Failed to create synthetic render timeline")
|
|
186
|
+
project.SetCurrentTimeline(timeline)
|
|
187
|
+
print(f"Created timeline: {timeline_name}")
|
|
188
|
+
|
|
189
|
+
chosen = _choose_render_format_codec(server)
|
|
190
|
+
metadata["chosen_format_codec"] = chosen
|
|
191
|
+
render_settings = {
|
|
192
|
+
"TargetDir": str(render_dir),
|
|
193
|
+
"CustomName": "mcp_render_probe",
|
|
194
|
+
"SelectAllFrames": True,
|
|
195
|
+
"ExportVideo": True,
|
|
196
|
+
"ExportAudio": True,
|
|
197
|
+
"FormatWidth": 320,
|
|
198
|
+
"FormatHeight": 180,
|
|
199
|
+
"FrameRate": 24,
|
|
200
|
+
"VideoQuality": "Least",
|
|
201
|
+
"EnableUpload": False,
|
|
202
|
+
}
|
|
203
|
+
safe_settings = {key: value for key, value in render_settings.items() if key != "EnableUpload"}
|
|
204
|
+
|
|
205
|
+
_record_tool_result(recorder, "capabilities", "render_capabilities", server.render("render_capabilities"))
|
|
206
|
+
_record_tool_result(recorder, "matrix", "probe_render_matrix", server.render("probe_render_matrix"))
|
|
207
|
+
settings_snapshot = server.render("probe_render_settings")
|
|
208
|
+
_record_tool_result(recorder, "settings", "probe_render_settings", settings_snapshot)
|
|
209
|
+
if isinstance(settings_snapshot.get("settings"), dict) and settings_snapshot["settings"].get("error"):
|
|
210
|
+
recorder.record(
|
|
211
|
+
"settings",
|
|
212
|
+
"get_render_settings_readback",
|
|
213
|
+
"version_or_page_dependent",
|
|
214
|
+
details={"reason": settings_snapshot["settings"]["error"]},
|
|
215
|
+
)
|
|
216
|
+
if chosen["format"] and chosen["codec"]:
|
|
217
|
+
_record_tool_result(
|
|
218
|
+
recorder,
|
|
219
|
+
"format_codec",
|
|
220
|
+
"set_format_and_codec",
|
|
221
|
+
server.render(
|
|
222
|
+
"set_format_and_codec",
|
|
223
|
+
{"format": chosen["format"], "codec": chosen["codec"]},
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
_record_tool_result(
|
|
227
|
+
recorder,
|
|
228
|
+
"format_codec",
|
|
229
|
+
"get_resolutions_chosen",
|
|
230
|
+
server.render("get_resolutions", {"format": chosen["format"], "codec": chosen["codec"]}),
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
recorder.record("format_codec", "choose_format_codec", "unsupported", details={"reason": "No render codec available"})
|
|
234
|
+
_record_tool_result(
|
|
235
|
+
recorder,
|
|
236
|
+
"settings",
|
|
237
|
+
"validate_render_settings",
|
|
238
|
+
server.render(
|
|
239
|
+
"validate_render_settings",
|
|
240
|
+
{"settings": safe_settings, "require_temp_target": True},
|
|
241
|
+
),
|
|
242
|
+
)
|
|
243
|
+
_record_tool_result(
|
|
244
|
+
recorder,
|
|
245
|
+
"settings",
|
|
246
|
+
"safe_set_render_settings",
|
|
247
|
+
server.render(
|
|
248
|
+
"safe_set_render_settings",
|
|
249
|
+
{"settings": safe_settings, "require_temp_target": True},
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
_record_tool_result(recorder, "mode", "get_mode", server.render("get_mode"))
|
|
254
|
+
current_mode = server.render("get_mode").get("mode", 0)
|
|
255
|
+
_record_tool_result(recorder, "mode", "set_mode_restore", server.render("set_mode", {"mode": current_mode}))
|
|
256
|
+
|
|
257
|
+
_record_tool_result(recorder, "presets", "list_presets", server.render("list_presets"))
|
|
258
|
+
_record_tool_result(recorder, "presets", "save_preset", server.render("save_preset", {"name": preset_name}))
|
|
259
|
+
_record_tool_result(recorder, "presets", "delete_preset", server.render("delete_preset", {"name": preset_name}))
|
|
260
|
+
|
|
261
|
+
prepare_params = {
|
|
262
|
+
"target_dir": str(render_dir),
|
|
263
|
+
"settings": safe_settings,
|
|
264
|
+
"custom_name": "mcp_render_probe_prepare",
|
|
265
|
+
"dry_run": True,
|
|
266
|
+
}
|
|
267
|
+
if chosen["format"] and chosen["codec"]:
|
|
268
|
+
prepare_params.update({"format": chosen["format"], "codec": chosen["codec"]})
|
|
269
|
+
_record_tool_result(recorder, "jobs", "prepare_render_job_dry_run", server.render("prepare_render_job", prepare_params))
|
|
270
|
+
|
|
271
|
+
lifecycle_params = {
|
|
272
|
+
"target_dir": str(render_dir),
|
|
273
|
+
"settings": safe_settings,
|
|
274
|
+
"custom_name": "mcp_render_probe_lifecycle",
|
|
275
|
+
}
|
|
276
|
+
if chosen["format"] and chosen["codec"]:
|
|
277
|
+
lifecycle_params.update({"format": chosen["format"], "codec": chosen["codec"]})
|
|
278
|
+
_record_tool_result(
|
|
279
|
+
recorder,
|
|
280
|
+
"jobs",
|
|
281
|
+
"render_job_lifecycle_probe",
|
|
282
|
+
server.render("render_job_lifecycle_probe", lifecycle_params),
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
actual_params = {
|
|
286
|
+
"target_dir": str(render_dir),
|
|
287
|
+
"settings": {**safe_settings, "CustomName": "mcp_render_probe_actual"},
|
|
288
|
+
"custom_name": "mcp_render_probe_actual",
|
|
289
|
+
}
|
|
290
|
+
if chosen["format"] and chosen["codec"]:
|
|
291
|
+
actual_params.update({"format": chosen["format"], "codec": chosen["codec"]})
|
|
292
|
+
prepared = server.render("prepare_render_job", actual_params)
|
|
293
|
+
_record_tool_result(recorder, "jobs", "prepare_render_job", prepared)
|
|
294
|
+
job_id = prepared.get("job_id") if isinstance(prepared, dict) else None
|
|
295
|
+
if job_id:
|
|
296
|
+
_record_tool_result(recorder, "jobs", "get_job_status_before_start", server.render("get_job_status", {"job_id": job_id}))
|
|
297
|
+
start_result = server.render("start", {"job_ids": [job_id], "interactive": False})
|
|
298
|
+
_record_tool_result(recorder, "jobs", "start_rendering", start_result)
|
|
299
|
+
deadline = time.time() + 45
|
|
300
|
+
while time.time() < deadline:
|
|
301
|
+
rendering = server.render("is_rendering")
|
|
302
|
+
if not rendering.get("rendering"):
|
|
303
|
+
break
|
|
304
|
+
time.sleep(1)
|
|
305
|
+
final_rendering = server.render("is_rendering")
|
|
306
|
+
if final_rendering.get("rendering"):
|
|
307
|
+
server.render("stop")
|
|
308
|
+
recorder.record(
|
|
309
|
+
"jobs",
|
|
310
|
+
"render_completion",
|
|
311
|
+
"partially_supported",
|
|
312
|
+
details={"reason": "Render exceeded 45 second probe timeout and was stopped"},
|
|
313
|
+
)
|
|
314
|
+
else:
|
|
315
|
+
files = _render_output_files(render_dir)
|
|
316
|
+
status = "supported" if files else "write_only_unverifiable"
|
|
317
|
+
recorder.record("jobs", "render_completion", status, evidence={"files": files})
|
|
318
|
+
_record_tool_result(recorder, "jobs", "get_job_status_after_start", server.render("get_job_status", {"job_id": job_id}))
|
|
319
|
+
_record_tool_result(recorder, "jobs", "delete_render_job", server.render("delete_job", {"job_id": job_id}))
|
|
320
|
+
|
|
321
|
+
quick_caps = server.render("quick_export_capabilities")
|
|
322
|
+
_record_tool_result(recorder, "quick_export", "quick_export_capabilities", quick_caps)
|
|
323
|
+
quick_presets = quick_caps.get("presets") if isinstance(quick_caps, dict) else []
|
|
324
|
+
if quick_presets:
|
|
325
|
+
preset = quick_presets[0] if isinstance(quick_presets[0], str) else quick_presets[0].get("Name")
|
|
326
|
+
_record_tool_result(
|
|
327
|
+
recorder,
|
|
328
|
+
"quick_export",
|
|
329
|
+
"safe_quick_export_dry_run",
|
|
330
|
+
server.render(
|
|
331
|
+
"safe_quick_export",
|
|
332
|
+
{
|
|
333
|
+
"preset": preset,
|
|
334
|
+
"target_dir": str(render_dir),
|
|
335
|
+
"custom_name": "mcp_quick_export_probe",
|
|
336
|
+
"dry_run": True,
|
|
337
|
+
},
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
else:
|
|
341
|
+
recorder.record("quick_export", "safe_quick_export_dry_run", "not_applicable", details={"reason": "No quick export presets"})
|
|
342
|
+
|
|
343
|
+
_record_tool_result(
|
|
344
|
+
recorder,
|
|
345
|
+
"report",
|
|
346
|
+
"export_render_boundary_report",
|
|
347
|
+
server.render("export_render_boundary_report", {"max_pairs": 20}),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if keep_open:
|
|
351
|
+
server.project_manager("save")
|
|
352
|
+
print(f"LEFT PROJECT OPEN FOR INSPECTION: {project_name}")
|
|
353
|
+
created_project = False
|
|
354
|
+
|
|
355
|
+
finally:
|
|
356
|
+
if created_project:
|
|
357
|
+
server.project_manager("save")
|
|
358
|
+
server.project_manager("close")
|
|
359
|
+
delete_result = server.project_manager("delete", {"name": project_name})
|
|
360
|
+
print(f"Deleted disposable project: {delete_result}")
|
|
361
|
+
|
|
362
|
+
report = recorder.to_report(
|
|
363
|
+
metadata,
|
|
364
|
+
{
|
|
365
|
+
"json": str(output_dir / "render-deliver-probe.json"),
|
|
366
|
+
"markdown": str(output_dir / "render-deliver-probe.md"),
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
json_path = output_dir / "render-deliver-probe.json"
|
|
370
|
+
markdown_path = output_dir / "render-deliver-probe.md"
|
|
371
|
+
json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
|
|
372
|
+
markdown_path.write_text(render_markdown_report(report), encoding="utf-8")
|
|
373
|
+
print(f"Wrote JSON report: {json_path}")
|
|
374
|
+
print(f"Wrote Markdown report: {markdown_path}")
|
|
375
|
+
print(f"Counts: {json.dumps(report['counts'], sort_keys=True)}")
|
|
376
|
+
if not keep_open:
|
|
377
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
|
378
|
+
shutil.rmtree(render_dir, ignore_errors=True)
|
|
379
|
+
print(f"Removed synthetic media directory: {work_dir}")
|
|
380
|
+
print(f"Removed render output directory: {render_dir}")
|
|
381
|
+
|
|
382
|
+
if delete_result and delete_result.get("success") is not True:
|
|
383
|
+
raise AssertionError(f"Cleanup failed for {project_name}: {delete_result!r}")
|
|
384
|
+
return report
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DaVinci Resolve Connection Utilities
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import logging
|
|
8
|
+
from .platform import get_platform, get_resolve_paths, setup_environment
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("davinci-resolve-mcp.connection")
|
|
11
|
+
|
|
12
|
+
def initialize_resolve():
|
|
13
|
+
"""Initialize connection to DaVinci Resolve application."""
|
|
14
|
+
try:
|
|
15
|
+
# Import the DaVinci Resolve scripting module
|
|
16
|
+
import DaVinciResolveScript as dvr_script
|
|
17
|
+
|
|
18
|
+
# Get the resolve object
|
|
19
|
+
resolve = dvr_script.scriptapp("Resolve")
|
|
20
|
+
|
|
21
|
+
if resolve is None:
|
|
22
|
+
logger.error("Failed to get Resolve object. Is DaVinci Resolve running?")
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
logger.info(f"Connected to DaVinci Resolve: {resolve.GetProductName()} {resolve.GetVersionString()}")
|
|
26
|
+
return resolve
|
|
27
|
+
|
|
28
|
+
except ImportError:
|
|
29
|
+
platform_name = get_platform()
|
|
30
|
+
paths = get_resolve_paths()
|
|
31
|
+
|
|
32
|
+
logger.error("Failed to import DaVinciResolveScript. Check environment variables.")
|
|
33
|
+
logger.error("RESOLVE_SCRIPT_API, RESOLVE_SCRIPT_LIB, and PYTHONPATH must be set correctly.")
|
|
34
|
+
|
|
35
|
+
if platform_name == 'darwin':
|
|
36
|
+
logger.error("On macOS, typically:")
|
|
37
|
+
logger.error(f'export RESOLVE_SCRIPT_API="{paths["api_path"]}"')
|
|
38
|
+
logger.error(f'export RESOLVE_SCRIPT_LIB="{paths["lib_path"]}"')
|
|
39
|
+
logger.error(f'export PYTHONPATH="$PYTHONPATH:{paths["modules_path"]}"')
|
|
40
|
+
elif platform_name == 'windows':
|
|
41
|
+
logger.error("On Windows, typically:")
|
|
42
|
+
logger.error(f'set RESOLVE_SCRIPT_API={paths["api_path"]}')
|
|
43
|
+
logger.error(f'set RESOLVE_SCRIPT_LIB={paths["lib_path"]}')
|
|
44
|
+
logger.error(f'set PYTHONPATH=%PYTHONPATH%;{paths["modules_path"]}')
|
|
45
|
+
elif platform_name == 'linux':
|
|
46
|
+
logger.error("On Linux, typically:")
|
|
47
|
+
logger.error(f'export RESOLVE_SCRIPT_API="{paths["api_path"]}"')
|
|
48
|
+
logger.error(f'export RESOLVE_SCRIPT_LIB="{paths["lib_path"]}"')
|
|
49
|
+
logger.error(f'export PYTHONPATH="$PYTHONPATH:{paths["modules_path"]}"')
|
|
50
|
+
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"Unexpected error initializing Resolve: {str(e)}")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def check_environment_variables():
|
|
58
|
+
"""Check if the required environment variables are set."""
|
|
59
|
+
resolve_script_api = os.environ.get("RESOLVE_SCRIPT_API")
|
|
60
|
+
resolve_script_lib = os.environ.get("RESOLVE_SCRIPT_LIB")
|
|
61
|
+
|
|
62
|
+
missing_vars = []
|
|
63
|
+
if not resolve_script_api:
|
|
64
|
+
missing_vars.append("RESOLVE_SCRIPT_API")
|
|
65
|
+
if not resolve_script_lib:
|
|
66
|
+
missing_vars.append("RESOLVE_SCRIPT_LIB")
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
"all_set": len(missing_vars) == 0,
|
|
70
|
+
"missing": missing_vars,
|
|
71
|
+
"resolve_script_api": resolve_script_api,
|
|
72
|
+
"resolve_script_lib": resolve_script_lib
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def set_default_environment_variables():
|
|
76
|
+
"""Set the default environment variables based on platform."""
|
|
77
|
+
return setup_environment()
|