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,192 @@
|
|
|
1
|
+
"""DaVinci Resolve cloud project helpers.
|
|
2
|
+
|
|
3
|
+
Mirrors the four documented ProjectManager cloud methods at docs lines 138-145
|
|
4
|
+
and the {cloudSettings} dict spec at lines 576-594. Maps user-friendly snake_case
|
|
5
|
+
arguments to the resolve.CLOUD_SETTING_* / CLOUD_SYNC_* constants required by
|
|
6
|
+
the Resolve scripting API.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("davinci-resolve-mcp.cloud_operations")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_SYNC_MODE_SUFFIXES = {
|
|
16
|
+
"none": "NONE",
|
|
17
|
+
"proxy_only": "PROXY_ONLY",
|
|
18
|
+
"proxy-only": "PROXY_ONLY",
|
|
19
|
+
"proxy_and_orig": "PROXY_AND_ORIG",
|
|
20
|
+
"proxy-and-orig": "PROXY_AND_ORIG",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _build_cloud_settings(
|
|
25
|
+
resolve_obj,
|
|
26
|
+
project_name: Optional[str] = None,
|
|
27
|
+
project_media_path: Optional[str] = None,
|
|
28
|
+
is_collab: Optional[bool] = None,
|
|
29
|
+
sync_mode: Optional[str] = None,
|
|
30
|
+
is_camera_access: Optional[bool] = None,
|
|
31
|
+
):
|
|
32
|
+
"""Build the {cloudSettings} dict for ProjectManager cloud methods.
|
|
33
|
+
|
|
34
|
+
Returns (settings_dict, None) on success or (None, error_dict) on validation
|
|
35
|
+
failure. Per docs lines 576-594, all keys default on the Resolve side; we only
|
|
36
|
+
include keys the caller explicitly set.
|
|
37
|
+
"""
|
|
38
|
+
settings: Dict[Any, Any] = {}
|
|
39
|
+
if project_name is not None:
|
|
40
|
+
settings[resolve_obj.CLOUD_SETTING_PROJECT_NAME] = project_name
|
|
41
|
+
if project_media_path is not None:
|
|
42
|
+
settings[resolve_obj.CLOUD_SETTING_PROJECT_MEDIA_PATH] = project_media_path
|
|
43
|
+
if is_collab is not None:
|
|
44
|
+
settings[resolve_obj.CLOUD_SETTING_IS_COLLAB] = bool(is_collab)
|
|
45
|
+
if sync_mode is not None:
|
|
46
|
+
suffix = _SYNC_MODE_SUFFIXES.get(str(sync_mode).strip().lower())
|
|
47
|
+
if not suffix:
|
|
48
|
+
valid = sorted(set(_SYNC_MODE_SUFFIXES.keys()))
|
|
49
|
+
return None, {"error": f"Unknown sync_mode '{sync_mode}'. Valid: {valid}"}
|
|
50
|
+
settings[resolve_obj.CLOUD_SETTING_SYNC_MODE] = getattr(resolve_obj, f"CLOUD_SYNC_{suffix}")
|
|
51
|
+
if is_camera_access is not None:
|
|
52
|
+
settings[resolve_obj.CLOUD_SETTING_IS_CAMERA_ACCESS] = bool(is_camera_access)
|
|
53
|
+
return settings, None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _project_manager(resolve_obj, method_name: str):
|
|
57
|
+
"""Look up the project manager and confirm a Resolve method exists.
|
|
58
|
+
|
|
59
|
+
Returns (pm, None) on success or (None, error_dict) on failure.
|
|
60
|
+
"""
|
|
61
|
+
if resolve_obj is None:
|
|
62
|
+
return None, {"success": False, "error": "Not connected to DaVinci Resolve"}
|
|
63
|
+
pm = resolve_obj.GetProjectManager()
|
|
64
|
+
if not pm:
|
|
65
|
+
return None, {"success": False, "error": "Failed to get Project Manager"}
|
|
66
|
+
if not hasattr(pm, method_name):
|
|
67
|
+
return None, {
|
|
68
|
+
"success": False,
|
|
69
|
+
"error": f"{method_name} not available in this version of DaVinci Resolve",
|
|
70
|
+
}
|
|
71
|
+
return pm, None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def create_cloud_project(
|
|
75
|
+
resolve_obj,
|
|
76
|
+
project_name: Optional[str] = None,
|
|
77
|
+
project_media_path: Optional[str] = None,
|
|
78
|
+
is_collab: Optional[bool] = None,
|
|
79
|
+
sync_mode: Optional[str] = None,
|
|
80
|
+
is_camera_access: Optional[bool] = None,
|
|
81
|
+
) -> Dict[str, Any]:
|
|
82
|
+
"""Create a cloud project. Mirrors ProjectManager.CreateCloudProject({cloudSettings})."""
|
|
83
|
+
pm, err = _project_manager(resolve_obj, "CreateCloudProject")
|
|
84
|
+
if err:
|
|
85
|
+
return err
|
|
86
|
+
settings, settings_err = _build_cloud_settings(
|
|
87
|
+
resolve_obj, project_name, project_media_path, is_collab, sync_mode, is_camera_access,
|
|
88
|
+
)
|
|
89
|
+
if settings_err:
|
|
90
|
+
return settings_err
|
|
91
|
+
try:
|
|
92
|
+
project = pm.CreateCloudProject(settings)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
logger.error(f"CreateCloudProject failed: {exc}")
|
|
95
|
+
return {"success": False, "error": f"CreateCloudProject failed: {exc}"}
|
|
96
|
+
if not project:
|
|
97
|
+
return {"success": False, "error": "Failed to create cloud project"}
|
|
98
|
+
return {
|
|
99
|
+
"success": True,
|
|
100
|
+
"project_name": project.GetName(),
|
|
101
|
+
"project_id": project.GetUniqueId() if hasattr(project, "GetUniqueId") else None,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def load_cloud_project(
|
|
106
|
+
resolve_obj,
|
|
107
|
+
project_name: Optional[str] = None,
|
|
108
|
+
project_media_path: Optional[str] = None,
|
|
109
|
+
sync_mode: Optional[str] = None,
|
|
110
|
+
) -> Dict[str, Any]:
|
|
111
|
+
"""Load a cloud project. Mirrors ProjectManager.LoadCloudProject({cloudSettings}).
|
|
112
|
+
|
|
113
|
+
Per docs line 585, only PROJECT_NAME, PROJECT_MEDIA_PATH, and SYNC_MODE are
|
|
114
|
+
honoured; subsequent loads on the same system honour only PROJECT_NAME.
|
|
115
|
+
"""
|
|
116
|
+
pm, err = _project_manager(resolve_obj, "LoadCloudProject")
|
|
117
|
+
if err:
|
|
118
|
+
return err
|
|
119
|
+
settings, settings_err = _build_cloud_settings(
|
|
120
|
+
resolve_obj, project_name=project_name,
|
|
121
|
+
project_media_path=project_media_path, sync_mode=sync_mode,
|
|
122
|
+
)
|
|
123
|
+
if settings_err:
|
|
124
|
+
return settings_err
|
|
125
|
+
try:
|
|
126
|
+
project = pm.LoadCloudProject(settings)
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
logger.error(f"LoadCloudProject failed: {exc}")
|
|
129
|
+
return {"success": False, "error": f"LoadCloudProject failed: {exc}"}
|
|
130
|
+
if not project:
|
|
131
|
+
return {"success": False, "error": "No matching cloud project found"}
|
|
132
|
+
return {
|
|
133
|
+
"success": True,
|
|
134
|
+
"project_name": project.GetName(),
|
|
135
|
+
"project_id": project.GetUniqueId() if hasattr(project, "GetUniqueId") else None,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def import_cloud_project(
|
|
140
|
+
resolve_obj,
|
|
141
|
+
file_path: str,
|
|
142
|
+
project_name: Optional[str] = None,
|
|
143
|
+
project_media_path: Optional[str] = None,
|
|
144
|
+
is_collab: Optional[bool] = None,
|
|
145
|
+
sync_mode: Optional[str] = None,
|
|
146
|
+
is_camera_access: Optional[bool] = None,
|
|
147
|
+
) -> Dict[str, Any]:
|
|
148
|
+
"""Import a cloud project. Mirrors ProjectManager.ImportCloudProject(filePath, {cloudSettings})."""
|
|
149
|
+
pm, err = _project_manager(resolve_obj, "ImportCloudProject")
|
|
150
|
+
if err:
|
|
151
|
+
return err
|
|
152
|
+
if not file_path:
|
|
153
|
+
return {"success": False, "error": "file_path is required"}
|
|
154
|
+
settings, settings_err = _build_cloud_settings(
|
|
155
|
+
resolve_obj, project_name, project_media_path, is_collab, sync_mode, is_camera_access,
|
|
156
|
+
)
|
|
157
|
+
if settings_err:
|
|
158
|
+
return settings_err
|
|
159
|
+
try:
|
|
160
|
+
ok = pm.ImportCloudProject(file_path, settings)
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
logger.error(f"ImportCloudProject failed: {exc}")
|
|
163
|
+
return {"success": False, "error": f"ImportCloudProject failed: {exc}"}
|
|
164
|
+
return {"success": bool(ok)}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def restore_cloud_project(
|
|
168
|
+
resolve_obj,
|
|
169
|
+
folder_path: str,
|
|
170
|
+
project_name: Optional[str] = None,
|
|
171
|
+
project_media_path: Optional[str] = None,
|
|
172
|
+
is_collab: Optional[bool] = None,
|
|
173
|
+
sync_mode: Optional[str] = None,
|
|
174
|
+
is_camera_access: Optional[bool] = None,
|
|
175
|
+
) -> Dict[str, Any]:
|
|
176
|
+
"""Restore a cloud project. Mirrors ProjectManager.RestoreCloudProject(folderPath, {cloudSettings})."""
|
|
177
|
+
pm, err = _project_manager(resolve_obj, "RestoreCloudProject")
|
|
178
|
+
if err:
|
|
179
|
+
return err
|
|
180
|
+
if not folder_path:
|
|
181
|
+
return {"success": False, "error": "folder_path is required"}
|
|
182
|
+
settings, settings_err = _build_cloud_settings(
|
|
183
|
+
resolve_obj, project_name, project_media_path, is_collab, sync_mode, is_camera_access,
|
|
184
|
+
)
|
|
185
|
+
if settings_err:
|
|
186
|
+
return settings_err
|
|
187
|
+
try:
|
|
188
|
+
ok = pm.RestoreCloudProject(folder_path, settings)
|
|
189
|
+
except Exception as exc:
|
|
190
|
+
logger.error(f"RestoreCloudProject failed: {exc}")
|
|
191
|
+
return {"success": False, "error": f"RestoreCloudProject failed: {exc}"}
|
|
192
|
+
return {"success": bool(ok)}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Live Color / Grade boundary probe."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
from src.utils.timeline_kernel_probe import ProbeRecorder, render_markdown_report, utc_timestamp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _require_success(label: str, result: Dict[str, Any]) -> Dict[str, Any]:
|
|
21
|
+
if not isinstance(result, dict):
|
|
22
|
+
raise AssertionError(f"{label}: expected dict, got {result!r}")
|
|
23
|
+
if result.get("error"):
|
|
24
|
+
raise AssertionError(f"{label}: {result['error']}")
|
|
25
|
+
if "success" in result and result["success"] is not True:
|
|
26
|
+
raise AssertionError(f"{label}: expected success=True, got {result!r}")
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _record_tool_result(
|
|
31
|
+
recorder: ProbeRecorder,
|
|
32
|
+
category: str,
|
|
33
|
+
name: str,
|
|
34
|
+
result: Dict[str, Any],
|
|
35
|
+
*,
|
|
36
|
+
expected_status: Optional[str] = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
if not isinstance(result, dict):
|
|
39
|
+
recorder.record(category, name, "error", details={"reason": "non-dict result", "result": repr(result)})
|
|
40
|
+
return
|
|
41
|
+
if result.get("error"):
|
|
42
|
+
recorder.record(
|
|
43
|
+
category,
|
|
44
|
+
name,
|
|
45
|
+
expected_status or "error",
|
|
46
|
+
details={"reason": result.get("error"), "expected_status": expected_status},
|
|
47
|
+
evidence=result,
|
|
48
|
+
)
|
|
49
|
+
return
|
|
50
|
+
if "success" in result and result["success"] is not True:
|
|
51
|
+
recorder.record(
|
|
52
|
+
category,
|
|
53
|
+
name,
|
|
54
|
+
expected_status or "partially_supported",
|
|
55
|
+
details={"reason": "success returned false", "expected_status": expected_status},
|
|
56
|
+
evidence=result,
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
recorder.record(category, name, expected_status or "supported", evidence=result)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _run_ffmpeg(args: list[str]) -> None:
|
|
63
|
+
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", *args], check=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _make_synthetic_video(work_dir: Path) -> Path:
|
|
67
|
+
video = work_dir / "color_grade_probe.mov"
|
|
68
|
+
_run_ffmpeg(
|
|
69
|
+
[
|
|
70
|
+
"-f",
|
|
71
|
+
"lavfi",
|
|
72
|
+
"-i",
|
|
73
|
+
"smptebars=size=640x360:rate=24:duration=4",
|
|
74
|
+
"-f",
|
|
75
|
+
"lavfi",
|
|
76
|
+
"-i",
|
|
77
|
+
"sine=frequency=660:sample_rate=48000:duration=4",
|
|
78
|
+
"-shortest",
|
|
79
|
+
"-pix_fmt",
|
|
80
|
+
"yuv420p",
|
|
81
|
+
"-c:v",
|
|
82
|
+
"libx264",
|
|
83
|
+
"-c:a",
|
|
84
|
+
"aac",
|
|
85
|
+
"-y",
|
|
86
|
+
str(video),
|
|
87
|
+
]
|
|
88
|
+
)
|
|
89
|
+
return video
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _first_imported_clip(imported_items):
|
|
93
|
+
for item in imported_items or []:
|
|
94
|
+
try:
|
|
95
|
+
if item.GetUniqueId():
|
|
96
|
+
return item
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _timeline_video_items(timeline):
|
|
103
|
+
try:
|
|
104
|
+
return timeline.GetItemListInTrack("video", 1) or []
|
|
105
|
+
except Exception:
|
|
106
|
+
return []
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _cleanup_exported_files(files):
|
|
110
|
+
folders = set()
|
|
111
|
+
for file_info in files or []:
|
|
112
|
+
path = file_info.get("path")
|
|
113
|
+
if not path:
|
|
114
|
+
continue
|
|
115
|
+
folders.add(os.path.dirname(path))
|
|
116
|
+
try:
|
|
117
|
+
os.remove(path)
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
for folder in folders:
|
|
121
|
+
try:
|
|
122
|
+
if os.path.isdir(folder) and not os.listdir(folder):
|
|
123
|
+
os.rmdir(folder)
|
|
124
|
+
except OSError:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _redact_file_payloads(result: Dict[str, Any]) -> Dict[str, Any]:
|
|
129
|
+
if not isinstance(result, dict):
|
|
130
|
+
return result
|
|
131
|
+
redacted = dict(result)
|
|
132
|
+
files = []
|
|
133
|
+
for file_info in result.get("files", []) or []:
|
|
134
|
+
if not isinstance(file_info, dict):
|
|
135
|
+
files.append(file_info)
|
|
136
|
+
continue
|
|
137
|
+
files.append({key: value for key, value in file_info.items() if key not in {"data", "data_base64"}})
|
|
138
|
+
if "files" in redacted:
|
|
139
|
+
redacted["files"] = files
|
|
140
|
+
return redacted
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def run_probe(server, output_dir: Path, keep_open: bool = False) -> Dict[str, Any]:
|
|
144
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
work_dir = Path(tempfile.mkdtemp(prefix="mcp_color_grade_probe_"))
|
|
146
|
+
project_name = f"_mcp_color_grade_probe_{int(time.time())}"
|
|
147
|
+
timeline_name = "Color Grade Probe Timeline"
|
|
148
|
+
recorder = ProbeRecorder()
|
|
149
|
+
created_project = False
|
|
150
|
+
delete_result: Optional[Dict[str, Any]] = None
|
|
151
|
+
exported_gallery_files = []
|
|
152
|
+
|
|
153
|
+
metadata: Dict[str, Any] = {
|
|
154
|
+
"title": "Color Grade Kernel Capability Probe",
|
|
155
|
+
"timestamp_utc": utc_timestamp(),
|
|
156
|
+
"python": sys.version,
|
|
157
|
+
"platform": platform.platform(),
|
|
158
|
+
"output_dir": str(output_dir),
|
|
159
|
+
"project_name": project_name,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
version = _require_success("resolve_control.get_version", server.resolve_control("get_version"))
|
|
164
|
+
metadata.update(
|
|
165
|
+
{
|
|
166
|
+
"product": version.get("product"),
|
|
167
|
+
"version": version.get("version"),
|
|
168
|
+
"version_string": version.get("version_string"),
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
print(f"Connected to {metadata['product']} {metadata['version_string']}")
|
|
172
|
+
|
|
173
|
+
_require_success("project_manager.create", server.project_manager("create", {"name": project_name}))
|
|
174
|
+
created_project = True
|
|
175
|
+
print(f"Created disposable project: {project_name}")
|
|
176
|
+
|
|
177
|
+
video = _make_synthetic_video(work_dir)
|
|
178
|
+
metadata["synthetic_media"] = {"video": str(video)}
|
|
179
|
+
print(f"Generated synthetic media under: {work_dir}")
|
|
180
|
+
|
|
181
|
+
resolve = server.get_resolve()
|
|
182
|
+
project = resolve.GetProjectManager().GetCurrentProject()
|
|
183
|
+
media_pool = project.GetMediaPool()
|
|
184
|
+
imported = media_pool.ImportMedia([str(video)]) or []
|
|
185
|
+
clip = _first_imported_clip(imported)
|
|
186
|
+
if not clip:
|
|
187
|
+
raise AssertionError("Failed to import synthetic color media")
|
|
188
|
+
timeline = media_pool.CreateTimelineFromClips(timeline_name, [clip])
|
|
189
|
+
if not timeline:
|
|
190
|
+
raise AssertionError("Failed to create color grade timeline")
|
|
191
|
+
project.SetCurrentTimeline(timeline)
|
|
192
|
+
media_pool.AppendToTimeline([clip])
|
|
193
|
+
timeline.SetCurrentTimecode("01:00:00:01")
|
|
194
|
+
server.resolve_control("open_page", {"page": "color"})
|
|
195
|
+
print(f"Created timeline: {timeline_name}")
|
|
196
|
+
|
|
197
|
+
items = _timeline_video_items(timeline)
|
|
198
|
+
metadata["timeline_item_count"] = len(items)
|
|
199
|
+
target_id = items[1].GetUniqueId() if len(items) > 1 else None
|
|
200
|
+
|
|
201
|
+
scope = {"track_type": "video", "track_index": 1, "item_index": 0}
|
|
202
|
+
_record_tool_result(recorder, "capabilities", "grade_capabilities", server.timeline_item_color("grade_capabilities", scope))
|
|
203
|
+
_record_tool_result(
|
|
204
|
+
recorder,
|
|
205
|
+
"inspection",
|
|
206
|
+
"probe_grade_item",
|
|
207
|
+
server.timeline_item_color("probe_grade_item", {**scope, "max_nodes": 3}),
|
|
208
|
+
)
|
|
209
|
+
_record_tool_result(
|
|
210
|
+
recorder,
|
|
211
|
+
"inspection",
|
|
212
|
+
"probe_item_node_graph",
|
|
213
|
+
server.timeline_item_color("probe_node_graph", {**scope, "source": "item", "max_nodes": 3}),
|
|
214
|
+
)
|
|
215
|
+
_record_tool_result(
|
|
216
|
+
recorder,
|
|
217
|
+
"inspection",
|
|
218
|
+
"probe_timeline_node_graph",
|
|
219
|
+
server.timeline_item_color("probe_node_graph", {**scope, "source": "timeline", "include_nodes": False}),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
cdl = {
|
|
223
|
+
"NodeIndex": 1,
|
|
224
|
+
"Slope": [1.05, 1.0, 0.95],
|
|
225
|
+
"Offset": [0.0, 0.0, 0.0],
|
|
226
|
+
"Power": [1.0, 1.0, 1.0],
|
|
227
|
+
"Saturation": 1.05,
|
|
228
|
+
}
|
|
229
|
+
_record_tool_result(
|
|
230
|
+
recorder,
|
|
231
|
+
"cdl",
|
|
232
|
+
"safe_set_cdl_dry_run",
|
|
233
|
+
server.timeline_item_color("safe_set_cdl", {**scope, "cdl": cdl, "dry_run": True}),
|
|
234
|
+
)
|
|
235
|
+
_record_tool_result(
|
|
236
|
+
recorder,
|
|
237
|
+
"cdl",
|
|
238
|
+
"safe_set_cdl_apply",
|
|
239
|
+
server.timeline_item_color("safe_set_cdl", {**scope, "cdl": cdl}),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
_record_tool_result(
|
|
243
|
+
recorder,
|
|
244
|
+
"versions",
|
|
245
|
+
"grade_version_snapshot_before",
|
|
246
|
+
server.timeline_item_color("grade_version_snapshot", scope),
|
|
247
|
+
)
|
|
248
|
+
_record_tool_result(
|
|
249
|
+
recorder,
|
|
250
|
+
"versions",
|
|
251
|
+
"add_version",
|
|
252
|
+
server.timeline_item_color("add_version", {**scope, "name": "MCP Probe Look", "type": 0}),
|
|
253
|
+
)
|
|
254
|
+
_record_tool_result(
|
|
255
|
+
recorder,
|
|
256
|
+
"versions",
|
|
257
|
+
"rename_version",
|
|
258
|
+
server.timeline_item_color(
|
|
259
|
+
"rename_version",
|
|
260
|
+
{**scope, "old_name": "MCP Probe Look", "new_name": "MCP Probe Look Renamed", "type": 0},
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
_record_tool_result(
|
|
264
|
+
recorder,
|
|
265
|
+
"versions",
|
|
266
|
+
"grade_version_restore",
|
|
267
|
+
server.timeline_item_color("grade_version_restore", {**scope, "name": "MCP Probe Look Renamed", "type": 0}),
|
|
268
|
+
)
|
|
269
|
+
_record_tool_result(
|
|
270
|
+
recorder,
|
|
271
|
+
"versions",
|
|
272
|
+
"load_default_version",
|
|
273
|
+
server.timeline_item_color("load_version", {**scope, "name": "Version 1", "type": 0}),
|
|
274
|
+
)
|
|
275
|
+
_record_tool_result(
|
|
276
|
+
recorder,
|
|
277
|
+
"versions",
|
|
278
|
+
"delete_version",
|
|
279
|
+
server.timeline_item_color("delete_version", {**scope, "name": "MCP Probe Look Renamed", "type": 0}),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if target_id:
|
|
283
|
+
_record_tool_result(
|
|
284
|
+
recorder,
|
|
285
|
+
"copy",
|
|
286
|
+
"safe_copy_grade",
|
|
287
|
+
server.timeline_item_color("safe_copy_grade", {**scope, "target_ids": [target_id]}),
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
recorder.record("copy", "safe_copy_grade", "not_applicable", details={"reason": "No second video item"})
|
|
291
|
+
|
|
292
|
+
lut_path = str(work_dir / "probe_look.cube")
|
|
293
|
+
_record_tool_result(
|
|
294
|
+
recorder,
|
|
295
|
+
"lut",
|
|
296
|
+
"safe_export_lut",
|
|
297
|
+
server.timeline_item_color("safe_export_lut", {**scope, "type": "33ptcube", "path": lut_path}),
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
group_name = f"MCP Probe Group {int(time.time())}"
|
|
301
|
+
group_created = server.project_settings("add_color_group", {"name": group_name})
|
|
302
|
+
_record_tool_result(recorder, "color_groups", "add_color_group", group_created)
|
|
303
|
+
if not group_created.get("error") and group_created.get("success"):
|
|
304
|
+
_record_tool_result(
|
|
305
|
+
recorder,
|
|
306
|
+
"color_groups",
|
|
307
|
+
"assign_color_group",
|
|
308
|
+
server.timeline_item_color("assign_color_group", {**scope, "group_name": group_name}),
|
|
309
|
+
)
|
|
310
|
+
_record_tool_result(
|
|
311
|
+
recorder,
|
|
312
|
+
"color_groups",
|
|
313
|
+
"color_group_capabilities",
|
|
314
|
+
server.timeline_item_color("color_group_capabilities", scope),
|
|
315
|
+
)
|
|
316
|
+
_record_tool_result(
|
|
317
|
+
recorder,
|
|
318
|
+
"color_groups",
|
|
319
|
+
"probe_color_group_pre",
|
|
320
|
+
server.timeline_item_color(
|
|
321
|
+
"probe_node_graph",
|
|
322
|
+
{**scope, "source": "color_group_pre", "group_name": group_name, "include_nodes": False},
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
_record_tool_result(
|
|
326
|
+
recorder,
|
|
327
|
+
"color_groups",
|
|
328
|
+
"probe_color_group_post",
|
|
329
|
+
server.timeline_item_color(
|
|
330
|
+
"probe_node_graph",
|
|
331
|
+
{**scope, "source": "color_group_post", "group_name": group_name, "include_nodes": False},
|
|
332
|
+
),
|
|
333
|
+
)
|
|
334
|
+
_record_tool_result(
|
|
335
|
+
recorder,
|
|
336
|
+
"color_groups",
|
|
337
|
+
"remove_from_color_group",
|
|
338
|
+
server.timeline_item_color("remove_from_color_group", scope),
|
|
339
|
+
)
|
|
340
|
+
_record_tool_result(
|
|
341
|
+
recorder,
|
|
342
|
+
"color_groups",
|
|
343
|
+
"delete_color_group",
|
|
344
|
+
server.project_settings("delete_color_group", {"name": group_name}),
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
_record_tool_result(
|
|
348
|
+
recorder,
|
|
349
|
+
"gallery",
|
|
350
|
+
"gallery_capabilities",
|
|
351
|
+
server.timeline_item_color("gallery_capabilities", scope),
|
|
352
|
+
)
|
|
353
|
+
_record_tool_result(recorder, "gallery", "get_still_albums", server.gallery("get_still_albums"))
|
|
354
|
+
_record_tool_result(recorder, "gallery", "create_still_album", server.gallery("create_still_album"))
|
|
355
|
+
drx_path = None
|
|
356
|
+
frame_drx_path = str(work_dir / "current_frame.drx")
|
|
357
|
+
frame_drx_result = server.project_settings("export_frame_as_still", {"path": frame_drx_path})
|
|
358
|
+
frame_drx_expected = None
|
|
359
|
+
if isinstance(frame_drx_result, dict) and frame_drx_result.get("success") is not True:
|
|
360
|
+
frame_drx_expected = "version_or_page_dependent"
|
|
361
|
+
_record_tool_result(
|
|
362
|
+
recorder,
|
|
363
|
+
"drx",
|
|
364
|
+
"export_current_frame_as_still_drx",
|
|
365
|
+
frame_drx_result,
|
|
366
|
+
expected_status=frame_drx_expected,
|
|
367
|
+
)
|
|
368
|
+
if isinstance(frame_drx_result, dict) and frame_drx_result.get("success") and os.path.isfile(frame_drx_path):
|
|
369
|
+
drx_path = frame_drx_path
|
|
370
|
+
|
|
371
|
+
still_result = server.gallery_stills(
|
|
372
|
+
"grab_and_export",
|
|
373
|
+
{"folder_path": str(work_dir / "stills"), "prefix": "color_grade_probe", "format": "jpg", "cleanup": False},
|
|
374
|
+
)
|
|
375
|
+
gallery_expected = None
|
|
376
|
+
if isinstance(still_result, dict) and still_result.get("error"):
|
|
377
|
+
gallery_expected = "version_or_page_dependent"
|
|
378
|
+
_record_tool_result(
|
|
379
|
+
recorder,
|
|
380
|
+
"gallery",
|
|
381
|
+
"grab_and_export",
|
|
382
|
+
_redact_file_payloads(still_result),
|
|
383
|
+
expected_status=gallery_expected,
|
|
384
|
+
)
|
|
385
|
+
exported_gallery_files = still_result.get("files", []) if isinstance(still_result, dict) else []
|
|
386
|
+
if not drx_path:
|
|
387
|
+
for file_info in exported_gallery_files:
|
|
388
|
+
if str(file_info.get("path", "")).lower().endswith(".drx"):
|
|
389
|
+
drx_path = file_info["path"]
|
|
390
|
+
break
|
|
391
|
+
if drx_path:
|
|
392
|
+
apply_params = {**scope, "path": drx_path, "grade_mode": 0}
|
|
393
|
+
if not str(drx_path).startswith(str(work_dir)):
|
|
394
|
+
apply_params["require_temp_path"] = False
|
|
395
|
+
_record_tool_result(
|
|
396
|
+
recorder,
|
|
397
|
+
"drx",
|
|
398
|
+
"safe_apply_drx",
|
|
399
|
+
server.timeline_item_color("safe_apply_drx", apply_params),
|
|
400
|
+
)
|
|
401
|
+
else:
|
|
402
|
+
recorder.record("drx", "safe_apply_drx", "not_applicable", details={"reason": "No DRX was exported by gallery probe"})
|
|
403
|
+
|
|
404
|
+
_record_tool_result(
|
|
405
|
+
recorder,
|
|
406
|
+
"report",
|
|
407
|
+
"grade_boundary_report",
|
|
408
|
+
server.timeline_item_color("grade_boundary_report", {**scope, "include_timeline_graph": True}),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if keep_open:
|
|
412
|
+
server.project_manager("save")
|
|
413
|
+
print(f"LEFT PROJECT OPEN FOR INSPECTION: {project_name}")
|
|
414
|
+
created_project = False
|
|
415
|
+
|
|
416
|
+
finally:
|
|
417
|
+
_cleanup_exported_files(exported_gallery_files)
|
|
418
|
+
if created_project:
|
|
419
|
+
server.project_manager("save")
|
|
420
|
+
server.project_manager("close")
|
|
421
|
+
delete_result = server.project_manager("delete", {"name": project_name})
|
|
422
|
+
print(f"Deleted disposable project: {delete_result}")
|
|
423
|
+
|
|
424
|
+
report = recorder.to_report(
|
|
425
|
+
metadata,
|
|
426
|
+
{
|
|
427
|
+
"json": str(output_dir / "color-grade-probe.json"),
|
|
428
|
+
"markdown": str(output_dir / "color-grade-probe.md"),
|
|
429
|
+
},
|
|
430
|
+
)
|
|
431
|
+
json_path = output_dir / "color-grade-probe.json"
|
|
432
|
+
markdown_path = output_dir / "color-grade-probe.md"
|
|
433
|
+
json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
|
|
434
|
+
markdown_path.write_text(render_markdown_report(report), encoding="utf-8")
|
|
435
|
+
print(f"Wrote JSON report: {json_path}")
|
|
436
|
+
print(f"Wrote Markdown report: {markdown_path}")
|
|
437
|
+
print(f"Counts: {json.dumps(report['counts'], sort_keys=True)}")
|
|
438
|
+
if not keep_open:
|
|
439
|
+
shutil.rmtree(work_dir, ignore_errors=True)
|
|
440
|
+
print(f"Removed synthetic media directory: {work_dir}")
|
|
441
|
+
|
|
442
|
+
if delete_result and delete_result.get("success") is not True:
|
|
443
|
+
raise AssertionError(f"Cleanup failed for {project_name}: {delete_result!r}")
|
|
444
|
+
return report
|