davinci-resolve-mcp 2.67.1 → 2.68.1
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/CHANGELOG.md +147 -0
- package/README.md +27 -2
- package/docs/SKILL.md +20 -0
- package/install.py +1 -1
- package/package.json +1 -1
- package/scripts/bridge_differential.py +431 -0
- package/scripts/install_resolve_bridge.py +260 -0
- package/scripts/resolve_bridge_launcher.py +202 -0
- package/scripts/resolve_bridge_probe.py +162 -0
- package/scripts/resolve_capability_probe.py +270 -0
- package/src/granular/common.py +6 -1
- package/src/server.py +378 -41
- package/src/utils/bridge_differential.py +375 -0
- package/src/utils/captions.py +323 -0
- package/src/utils/colorimetry.py +196 -0
- package/src/utils/delivery_targets.py +225 -0
- package/src/utils/edit_engine.py +181 -7
- package/src/utils/image_qc.py +499 -0
- package/src/utils/page_lock.py +5 -1
- package/src/utils/resolve_bridge.py +557 -0
- package/src/utils/resolve_bridge_client.py +423 -0
- package/src/utils/resolve_bridge_ops.py +714 -0
- package/src/utils/resolve_connection.py +29 -1
- package/src/utils/silence_ripple.py +256 -8
- package/src/utils/strata_analyzers.py +7 -0
- package/src/utils/strata_faces.py +6 -0
- package/src/utils/transcript_edit.py +372 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Resolve Scripts-menu capability probe: what does THIS edition's API expose?
|
|
2
|
+
|
|
3
|
+
Run from **Workspace ▸ Scripts ▸ resolve_capability_probe**, on whichever
|
|
4
|
+
edition you want to characterise.
|
|
5
|
+
|
|
6
|
+
The free edition gates *features* (noise reduction, Magic Mask, Super Scale,
|
|
7
|
+
some codecs, some resolutions). What it does to the *scripting API* is a
|
|
8
|
+
separate question and is not documented anywhere authoritative: a method may be
|
|
9
|
+
absent, or present and always return False, or present and work. Those three
|
|
10
|
+
cases need completely different handling in a connector, and guessing which is
|
|
11
|
+
which is how you ship a tool that lies about what it did.
|
|
12
|
+
|
|
13
|
+
So this asks the running application, on the edition in front of you.
|
|
14
|
+
|
|
15
|
+
**Strictly read-only.** It enumerates methods, calls only getters, and creates,
|
|
16
|
+
modifies and deletes nothing. Safe to run on a real project — though it reads
|
|
17
|
+
whatever project is open, so use a scratch one if you would rather not have your
|
|
18
|
+
timeline names in the report.
|
|
19
|
+
|
|
20
|
+
Writes to ``~/.config/davinci-resolve-mcp/capability-probe-<edition>.json`` and
|
|
21
|
+
prints a summary. Self-contained: no repository imports.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
|
|
29
|
+
REPORT_DIR = os.path.expanduser("~/.config/davinci-resolve-mcp")
|
|
30
|
+
|
|
31
|
+
#: Methods worth knowing about, grouped by the object they live on. Chosen for
|
|
32
|
+
#: what a connector actually needs, not for completeness.
|
|
33
|
+
SURFACE = {
|
|
34
|
+
"Resolve": [
|
|
35
|
+
"GetProductName", "GetVersionString", "GetCurrentPage", "OpenPage",
|
|
36
|
+
"GetProjectManager", "GetMediaStorage", "ImportRenderPreset",
|
|
37
|
+
"ExportRenderPreset", "ImportBurnInPreset", "Quit",
|
|
38
|
+
],
|
|
39
|
+
"ProjectManager": [
|
|
40
|
+
"GetCurrentProject", "GetProjectListInCurrentFolder", "GetCurrentFolder",
|
|
41
|
+
"GetCurrentDatabase", "GetDatabaseList", "CreateProject", "LoadProject",
|
|
42
|
+
"SaveProject", "CloseProject", "DeleteProject", "ExportProject",
|
|
43
|
+
"ImportProject", "CreateFolder", "ArchiveProject",
|
|
44
|
+
],
|
|
45
|
+
"Project": [
|
|
46
|
+
"GetName", "GetTimelineCount", "GetTimelineByIndex", "GetCurrentTimeline",
|
|
47
|
+
"SetCurrentTimeline", "GetMediaPool", "GetGallery", "GetSetting",
|
|
48
|
+
"SetSetting", "GetRenderFormats", "GetRenderCodecs",
|
|
49
|
+
"GetCurrentRenderFormatAndCodec", "GetRenderPresetList", "AddRenderJob",
|
|
50
|
+
"StartRendering", "IsRenderingInProgress", "GetRenderJobList",
|
|
51
|
+
"GetQuickExportRenderPresets", "RefreshLUTList", "GetUniqueId",
|
|
52
|
+
],
|
|
53
|
+
"MediaPool": [
|
|
54
|
+
"GetRootFolder", "GetCurrentFolder", "AddSubFolder", "ImportMedia",
|
|
55
|
+
"CreateEmptyTimeline", "CreateTimelineFromClips", "AppendToTimeline",
|
|
56
|
+
"DeleteTimelines", "DeleteClips", "ImportTimelineFromFile",
|
|
57
|
+
"GetTimelineMatte", "RefreshFolders",
|
|
58
|
+
],
|
|
59
|
+
"Timeline": [
|
|
60
|
+
"GetName", "SetName", "GetStartFrame", "GetEndFrame", "GetTrackCount",
|
|
61
|
+
"GetItemListInTrack", "GetTrackName", "GetIsTrackEnabled",
|
|
62
|
+
"AddTrack", "DeleteTrack", "AddMarker", "GetMarkers",
|
|
63
|
+
"DuplicateTimeline", "Export", "GetCurrentTimecode", "SetCurrentTimecode",
|
|
64
|
+
"InsertFusionTitleIntoTimeline", "InsertTitleIntoTimeline",
|
|
65
|
+
"CreateSubtitlesFromAudio", "DetectSceneCuts", "GetSetting",
|
|
66
|
+
],
|
|
67
|
+
"TimelineItem": [
|
|
68
|
+
"GetName", "GetDuration", "GetStart", "GetEnd", "GetProperty",
|
|
69
|
+
"SetProperty", "GetNumNodes", "SetLUT", "GetLUT", "SetCDL",
|
|
70
|
+
"AddMarker", "GetMarkers", "GetFusionCompCount", "AddFusionComp",
|
|
71
|
+
"SetClipEnabled", "GetMediaPoolItem", "CreateMagicMask",
|
|
72
|
+
"SmartReframe", "GetNodeLabel",
|
|
73
|
+
],
|
|
74
|
+
"MediaPoolItem": [
|
|
75
|
+
"GetName", "GetClipProperty", "SetClipProperty", "GetMetadata",
|
|
76
|
+
"SetMetadata", "GetMediaId", "AddMarker", "GetMarkers", "LinkProxyMedia",
|
|
77
|
+
"TranscribeAudio", "ClearTranscription",
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#: Getters that are safe to actually call, to separate "method exists" from
|
|
82
|
+
#: "method works". A method can be present and still be inert on this edition.
|
|
83
|
+
SAFE_CALLS = {
|
|
84
|
+
"Resolve": ["GetProductName", "GetVersionString", "GetCurrentPage"],
|
|
85
|
+
"ProjectManager": ["GetProjectListInCurrentFolder", "GetCurrentFolder"],
|
|
86
|
+
"Project": [
|
|
87
|
+
"GetName", "GetTimelineCount", "GetRenderFormats",
|
|
88
|
+
"GetCurrentRenderFormatAndCodec", "GetRenderPresetList",
|
|
89
|
+
"IsRenderingInProgress", "GetRenderJobList",
|
|
90
|
+
],
|
|
91
|
+
"MediaPool": ["GetRootFolder", "GetCurrentFolder"],
|
|
92
|
+
"Timeline": [
|
|
93
|
+
"GetName", "GetStartFrame", "GetEndFrame", "GetCurrentTimecode", "GetMarkers",
|
|
94
|
+
],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def acquire_resolve():
|
|
99
|
+
candidate = globals().get("resolve")
|
|
100
|
+
if candidate is not None:
|
|
101
|
+
return candidate
|
|
102
|
+
bmd = globals().get("bmd")
|
|
103
|
+
if bmd is not None:
|
|
104
|
+
try:
|
|
105
|
+
return bmd.scriptapp("Resolve")
|
|
106
|
+
except Exception:
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
import DaVinciResolveScript as script_module
|
|
110
|
+
return script_module.scriptapp("Resolve")
|
|
111
|
+
except Exception:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def classify(obj, names):
|
|
116
|
+
"""Which of `names` exist on `obj`."""
|
|
117
|
+
if obj is None:
|
|
118
|
+
return {"reachable": False, "note": "object not sampled in this context",
|
|
119
|
+
"present": [], "missing": sorted(names)}
|
|
120
|
+
present, missing = [], []
|
|
121
|
+
for name in names:
|
|
122
|
+
(present if callable(getattr(obj, name, None)) else missing).append(name)
|
|
123
|
+
return {"reachable": True, "present": sorted(present), "missing": sorted(missing)}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def try_calls(obj, names):
|
|
127
|
+
"""Call safe getters; record value shape or the failure. Never mutates."""
|
|
128
|
+
out = {}
|
|
129
|
+
if obj is None:
|
|
130
|
+
return out
|
|
131
|
+
for name in names:
|
|
132
|
+
fn = getattr(obj, name, None)
|
|
133
|
+
if not callable(fn):
|
|
134
|
+
out[name] = {"ok": False, "why": "absent"}
|
|
135
|
+
continue
|
|
136
|
+
try:
|
|
137
|
+
value = fn()
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
out[name] = {"ok": False, "why": "raised: %s" % type(exc).__name__}
|
|
140
|
+
continue
|
|
141
|
+
if value is None:
|
|
142
|
+
out[name] = {"ok": False, "why": "returned None"}
|
|
143
|
+
elif isinstance(value, bool):
|
|
144
|
+
# A bool getter answering False is a real answer, not a failure.
|
|
145
|
+
# Scoring it as one is how "nothing is rendering" reads as "broken".
|
|
146
|
+
out[name] = {"ok": True, "type": "bool", "value": value}
|
|
147
|
+
elif isinstance(value, dict):
|
|
148
|
+
out[name] = {"ok": True, "type": "dict", "size": len(value),
|
|
149
|
+
"sample": sorted(list(value)[:8])}
|
|
150
|
+
elif isinstance(value, (list, tuple)):
|
|
151
|
+
out[name] = {"ok": True, "type": "list", "size": len(value),
|
|
152
|
+
"sample": [str(v)[:40] for v in list(value)[:8]]}
|
|
153
|
+
else:
|
|
154
|
+
out[name] = {"ok": True, "type": type(value).__name__, "value": str(value)[:120]}
|
|
155
|
+
return out
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main():
|
|
159
|
+
resolve = acquire_resolve()
|
|
160
|
+
if resolve is None:
|
|
161
|
+
print("Could not get the resolve object. Run this from Workspace > Scripts.")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
report = {"probed_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
165
|
+
"interpreter": sys.executable}
|
|
166
|
+
try:
|
|
167
|
+
product = resolve.GetProductName()
|
|
168
|
+
report["product"] = product
|
|
169
|
+
report["version"] = resolve.GetVersionString()
|
|
170
|
+
report["edition"] = "studio" if "studio" in str(product).lower() else "free"
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
report["product_error"] = type(exc).__name__
|
|
173
|
+
report["edition"] = "unknown"
|
|
174
|
+
|
|
175
|
+
manager = getattr(resolve, "GetProjectManager", lambda: None)()
|
|
176
|
+
project = getattr(manager, "GetCurrentProject", lambda: None)() if manager else None
|
|
177
|
+
pool = getattr(project, "GetMediaPool", lambda: None)() if project else None
|
|
178
|
+
timeline = getattr(project, "GetCurrentTimeline", lambda: None)() if project else None
|
|
179
|
+
|
|
180
|
+
item = None
|
|
181
|
+
if timeline is not None:
|
|
182
|
+
try:
|
|
183
|
+
for index in range(1, int(timeline.GetTrackCount("video") or 0) + 1):
|
|
184
|
+
items = timeline.GetItemListInTrack("video", index) or []
|
|
185
|
+
if items:
|
|
186
|
+
item = items[0]
|
|
187
|
+
break
|
|
188
|
+
except Exception:
|
|
189
|
+
item = None
|
|
190
|
+
media_item = None
|
|
191
|
+
if pool is not None:
|
|
192
|
+
try:
|
|
193
|
+
clips = pool.GetRootFolder().GetClipList() or []
|
|
194
|
+
media_item = clips[0] if clips else None
|
|
195
|
+
except Exception:
|
|
196
|
+
media_item = None
|
|
197
|
+
|
|
198
|
+
objects = {
|
|
199
|
+
"Resolve": resolve, "ProjectManager": manager, "Project": project,
|
|
200
|
+
"MediaPool": pool, "Timeline": timeline, "TimelineItem": item,
|
|
201
|
+
"MediaPoolItem": media_item,
|
|
202
|
+
}
|
|
203
|
+
report["surface"] = {k: classify(objects.get(k), v) for k, v in SURFACE.items()}
|
|
204
|
+
report["calls"] = {k: try_calls(objects.get(k), v) for k, v in SAFE_CALLS.items()}
|
|
205
|
+
report["context"] = {
|
|
206
|
+
"project_open": project is not None,
|
|
207
|
+
"timeline_open": timeline is not None,
|
|
208
|
+
"timeline_item_sampled": item is not None,
|
|
209
|
+
"media_item_sampled": media_item is not None,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
# The render matrix is where free/Studio differ most visibly.
|
|
213
|
+
if project is not None:
|
|
214
|
+
try:
|
|
215
|
+
formats = project.GetRenderFormats() or {}
|
|
216
|
+
matrix = {}
|
|
217
|
+
for label in sorted(formats)[:40]:
|
|
218
|
+
try:
|
|
219
|
+
codecs = project.GetRenderCodecs(formats[label]) or {}
|
|
220
|
+
matrix[label] = sorted(codecs)
|
|
221
|
+
except Exception:
|
|
222
|
+
matrix[label] = ["<error>"]
|
|
223
|
+
report["render_matrix"] = matrix
|
|
224
|
+
report["render_format_count"] = len(formats)
|
|
225
|
+
report["render_pair_count"] = sum(len(v) for v in matrix.values())
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
report["render_matrix_error"] = type(exc).__name__
|
|
228
|
+
|
|
229
|
+
edition = report.get("edition", "unknown")
|
|
230
|
+
path = os.path.join(REPORT_DIR, "capability-probe-%s.json" % edition)
|
|
231
|
+
try:
|
|
232
|
+
if not os.path.isdir(REPORT_DIR):
|
|
233
|
+
os.makedirs(REPORT_DIR)
|
|
234
|
+
with open(path, "w") as handle:
|
|
235
|
+
json.dump(report, handle, indent=2, sort_keys=True)
|
|
236
|
+
written = path
|
|
237
|
+
except (OSError, IOError) as exc:
|
|
238
|
+
written = "could not write (%s)" % type(exc).__name__
|
|
239
|
+
|
|
240
|
+
print("=" * 70)
|
|
241
|
+
print("Resolve capability probe - %s %s (%s)" % (
|
|
242
|
+
report.get("product", "?"), report.get("version", "?"), edition))
|
|
243
|
+
print("=" * 70)
|
|
244
|
+
for group, result in report["surface"].items():
|
|
245
|
+
if not result.get("reachable"):
|
|
246
|
+
print(" %-16s not sampled (%s)" % (group, result.get("note")))
|
|
247
|
+
continue
|
|
248
|
+
total = len(result["present"]) + len(result["missing"])
|
|
249
|
+
print(" %-16s %d/%d present" % (group, len(result["present"]), total))
|
|
250
|
+
if result["missing"]:
|
|
251
|
+
print(" missing: %s" % ", ".join(result["missing"]))
|
|
252
|
+
print()
|
|
253
|
+
failed = [(g, n, r["why"]) for g, calls in report["calls"].items()
|
|
254
|
+
for n, r in calls.items() if not r["ok"]]
|
|
255
|
+
print(" safe getter calls: %d ok, %d not" % (
|
|
256
|
+
sum(1 for g, c in report["calls"].items() for r in c.values() if r["ok"]), len(failed)))
|
|
257
|
+
for group, name, why in failed:
|
|
258
|
+
print(" %s.%s -> %s" % (group, name, why))
|
|
259
|
+
if "render_format_count" in report:
|
|
260
|
+
print()
|
|
261
|
+
print(" render matrix: %d formats, %d format/codec pairs" % (
|
|
262
|
+
report["render_format_count"], report["render_pair_count"]))
|
|
263
|
+
print()
|
|
264
|
+
print(" context: %s" % json.dumps(report["context"]))
|
|
265
|
+
print(" report: %s" % written)
|
|
266
|
+
print("=" * 70)
|
|
267
|
+
return report
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
main()
|
package/src/granular/common.py
CHANGED
|
@@ -85,7 +85,7 @@ if not logging.getLogger().handlers:
|
|
|
85
85
|
handlers=[logging.StreamHandler()],
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
VERSION = "2.
|
|
88
|
+
VERSION = "2.68.1"
|
|
89
89
|
logger = logging.getLogger("davinci-resolve-mcp")
|
|
90
90
|
logger.info(f"Starting DaVinci Resolve MCP Server v{VERSION}")
|
|
91
91
|
logger.info(f"Detected platform: {get_platform()}")
|
|
@@ -234,6 +234,11 @@ mcp.tool = _tool_with_default_annotations
|
|
|
234
234
|
|
|
235
235
|
resolve = None
|
|
236
236
|
dvr_script = None
|
|
237
|
+
#: `dvr_script` may be None when Resolve is not installed; every use passes it
|
|
238
|
+
#: to `connect_resolve()`, which treats None as "not available" and returns None.
|
|
239
|
+
_OPTIONAL_DEPENDENCY_CONTRACT = (
|
|
240
|
+
"DaVinciResolveScript: always routed through connect_resolve(), which is None-tolerant"
|
|
241
|
+
)
|
|
237
242
|
|
|
238
243
|
try:
|
|
239
244
|
import DaVinciResolveScript as dvr_script # type: ignore
|