@hybridlabor-api/bdb-antigravity-skills 1.2.0 → 1.2.2
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/Project-overview.html +5 -4
- package/installer.js +1 -0
- package/mcp_config.json +4 -0
- package/mcps/__pycache__/adobe_mcp.cpython-313.pyc +0 -0
- package/mcps/adobe_mcp.py +31 -1
- package/mcps/davinci-resolve-mcp-free/LICENSE +21 -0
- package/mcps/davinci-resolve-mcp-free/README.md +220 -0
- package/mcps/davinci-resolve-mcp-free/examples/mcp.json +8 -0
- package/mcps/davinci-resolve-mcp-free/requirements.txt +5 -0
- package/mcps/davinci-resolve-mcp-free/src/CursorBridge.py +2910 -0
- package/mcps/davinci-resolve-mcp-free/src/resolve_mcp_bridge.py +2529 -0
- package/package.json +1 -1
- package/skills/global_config/bdb-adobe-suite-mcp.md +106 -0
- package/skills/global_config/bdb-after-effects-mcp.md +117 -0
- package/skills/global_config/bdb-blender-mcp.md +130 -0
- package/skills/global_config/bdb-computer-use-mcp.md +119 -0
- package/skills/global_config/bdb-davinci-mcp.md +139 -0
- package/skills/global_config/bdb-grandma3-mcp.md +109 -0
- package/skills/global_config/bdb-resolume-mcp.md +103 -0
- package/skills/global_config/bdb-rhino-mcp.md +153 -0
- package/skills/global_config/bdb-touchdesigner-mcp.md +116 -0
- package/skills/global_config/bdb-unreal-mcp.md +130 -0
- package/skills/global_config/bdb-vectorworks-mcp.md +110 -0
|
@@ -0,0 +1,2529 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DaVinci Resolve MCP Bridge Server
|
|
4
|
+
|
|
5
|
+
Connects to the CursorBridge HTTP server running inside DaVinci Resolve.
|
|
6
|
+
The bridge script must be started first: Workspace > Scripts > CursorBridge.
|
|
7
|
+
|
|
8
|
+
Exposes read AND write tools so Cursor can both query and manipulate Resolve.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import urllib.request
|
|
18
|
+
import urllib.error
|
|
19
|
+
from typing import Dict, Any, List, Optional
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _find_ffmpeg() -> Optional[str]:
|
|
23
|
+
"""Locate ffmpeg binary, checking PATH and common install locations."""
|
|
24
|
+
found = shutil.which("ffmpeg")
|
|
25
|
+
if found:
|
|
26
|
+
return found
|
|
27
|
+
candidates = [
|
|
28
|
+
os.path.join(os.environ.get("LOCALAPPDATA", ""), "AutoSubs", "ffmpeg.exe"),
|
|
29
|
+
os.path.join(os.environ.get("PROGRAMFILES", ""), "ffmpeg", "bin", "ffmpeg.exe"),
|
|
30
|
+
os.path.join(os.environ.get("USERPROFILE", ""), "ffmpeg", "bin", "ffmpeg.exe"),
|
|
31
|
+
]
|
|
32
|
+
for c in candidates:
|
|
33
|
+
if c and os.path.isfile(c):
|
|
34
|
+
return c
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
FFMPEG_BIN = _find_ffmpeg()
|
|
39
|
+
|
|
40
|
+
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs")
|
|
41
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
42
|
+
logging.basicConfig(
|
|
43
|
+
level=logging.INFO,
|
|
44
|
+
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
45
|
+
handlers=[logging.FileHandler(os.path.join(log_dir, "bridge_mcp.log"))],
|
|
46
|
+
)
|
|
47
|
+
logger = logging.getLogger("resolve-bridge-mcp")
|
|
48
|
+
|
|
49
|
+
from mcp.server.fastmcp import FastMCP
|
|
50
|
+
|
|
51
|
+
BRIDGE_URL = "http://127.0.0.1:9876"
|
|
52
|
+
|
|
53
|
+
CONN_ERROR = (
|
|
54
|
+
"Cannot reach the CursorBridge inside DaVinci Resolve. "
|
|
55
|
+
"Make sure DaVinci Resolve is open and you have started the bridge "
|
|
56
|
+
"via Workspace > Scripts > CursorBridge."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
mcp = FastMCP(
|
|
60
|
+
"DaVinciResolveBridge",
|
|
61
|
+
instructions=(
|
|
62
|
+
"DaVinci Resolve MCP Bridge — provides full read AND write access to "
|
|
63
|
+
"DaVinci Resolve via an internal HTTP bridge.\n"
|
|
64
|
+
"Before using these tools, the user must start the CursorBridge script "
|
|
65
|
+
"inside DaVinci Resolve (Workspace > Scripts > CursorBridge).\n"
|
|
66
|
+
"If tools return connection errors, remind the user to start the bridge script.\n\n"
|
|
67
|
+
"WRITE OPERATIONS: This bridge can modify the Resolve project — add markers, "
|
|
68
|
+
"import media, insert titles, change clip properties, start renders, and more. "
|
|
69
|
+
"Always confirm destructive operations with the user before executing."
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# HTTP helpers
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def _get(endpoint: str, params: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
|
79
|
+
url = f"{BRIDGE_URL}{endpoint}"
|
|
80
|
+
if params:
|
|
81
|
+
qs = "&".join(f"{k}={v}" for k, v in params.items())
|
|
82
|
+
url = f"{url}?{qs}"
|
|
83
|
+
try:
|
|
84
|
+
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
|
85
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
86
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
87
|
+
except urllib.error.HTTPError as e:
|
|
88
|
+
try:
|
|
89
|
+
body = json.loads(e.read().decode("utf-8"))
|
|
90
|
+
if e.code == 404:
|
|
91
|
+
body["hint"] = "The CursorBridge may be outdated. Restart DaVinci Resolve and re-run CursorBridge."
|
|
92
|
+
return body
|
|
93
|
+
except Exception:
|
|
94
|
+
return {"error": f"Bridge returned HTTP {e.code}"}
|
|
95
|
+
except urllib.error.URLError:
|
|
96
|
+
return {"error": CONN_ERROR}
|
|
97
|
+
except Exception as e:
|
|
98
|
+
return {"error": f"Bridge request failed: {e}"}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _post(endpoint: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
102
|
+
url = f"{BRIDGE_URL}{endpoint}"
|
|
103
|
+
data = json.dumps(body or {}).encode("utf-8")
|
|
104
|
+
try:
|
|
105
|
+
req = urllib.request.Request(
|
|
106
|
+
url, data=data,
|
|
107
|
+
headers={"Content-Type": "application/json", "Accept": "application/json"},
|
|
108
|
+
method="POST",
|
|
109
|
+
)
|
|
110
|
+
with urllib.request.urlopen(req, timeout=15) as resp:
|
|
111
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
112
|
+
except urllib.error.HTTPError as e:
|
|
113
|
+
try:
|
|
114
|
+
body = json.loads(e.read().decode("utf-8"))
|
|
115
|
+
if e.code in (404, 501):
|
|
116
|
+
body["hint"] = "The CursorBridge may be outdated. Restart DaVinci Resolve and re-run CursorBridge."
|
|
117
|
+
return body
|
|
118
|
+
except Exception:
|
|
119
|
+
return {"error": f"Bridge returned HTTP {e.code}"}
|
|
120
|
+
except urllib.error.URLError:
|
|
121
|
+
return {"error": CONN_ERROR}
|
|
122
|
+
except Exception as e:
|
|
123
|
+
return {"error": f"Bridge request failed: {e}"}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
127
|
+
# READ TOOLS
|
|
128
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
129
|
+
|
|
130
|
+
@mcp.tool()
|
|
131
|
+
def get_resolve_status() -> Dict[str, Any]:
|
|
132
|
+
"""Check whether the CursorBridge is running and DaVinci Resolve is connected.
|
|
133
|
+
Call this first to verify the bridge is active before using other tools."""
|
|
134
|
+
return _get("/status")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@mcp.tool()
|
|
138
|
+
def get_project_info() -> Dict[str, Any]:
|
|
139
|
+
"""Get information about the currently open DaVinci Resolve project.
|
|
140
|
+
Returns the project name, resolution, frame rate, color science, and timeline count."""
|
|
141
|
+
return _get("/project")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@mcp.tool()
|
|
145
|
+
def get_current_page() -> Dict[str, Any]:
|
|
146
|
+
"""Get which page the user is currently viewing in DaVinci Resolve.
|
|
147
|
+
Returns one of: media, cut, edit, fusion, color, fairlight, deliver."""
|
|
148
|
+
return _get("/page")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@mcp.tool()
|
|
152
|
+
def get_timeline_info() -> Dict[str, Any]:
|
|
153
|
+
"""Get detailed information about the current timeline.
|
|
154
|
+
Returns the timeline name, duration, frame rate, track counts,
|
|
155
|
+
current playhead timecode, track names, and in/out mark positions."""
|
|
156
|
+
return _get("/timeline")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@mcp.tool()
|
|
160
|
+
def get_timeline_clips(track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
161
|
+
"""Get the list of clips on a specific track in the current timeline.
|
|
162
|
+
Args:
|
|
163
|
+
track_type: 'video', 'audio', or 'subtitle'. Defaults to 'video'.
|
|
164
|
+
track_index: 1-based track index. Defaults to 1.
|
|
165
|
+
Returns clip names, durations, positions, file paths, colors, and enabled state."""
|
|
166
|
+
return _get("/timeline/clips", {"track_type": track_type, "track_index": str(track_index)})
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@mcp.tool()
|
|
170
|
+
def get_timeline_markers() -> Dict[str, Any]:
|
|
171
|
+
"""Get all markers on the current timeline.
|
|
172
|
+
Returns marker positions, colors, names, notes, and durations."""
|
|
173
|
+
return _get("/timeline/markers")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@mcp.tool()
|
|
177
|
+
def get_render_settings() -> Dict[str, Any]:
|
|
178
|
+
"""Get the current render configuration for the project.
|
|
179
|
+
Returns render format, codec, render mode, job list, and rendering status."""
|
|
180
|
+
return _get("/render")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@mcp.tool()
|
|
184
|
+
def get_media_pool() -> Dict[str, Any]:
|
|
185
|
+
"""List clips and subfolders in the current media pool folder.
|
|
186
|
+
Returns clip names, colors, and media IDs."""
|
|
187
|
+
return _get("/mediapool")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@mcp.tool()
|
|
191
|
+
def get_clip_properties(
|
|
192
|
+
track_type: str = "video", track_index: int = 1, clip_index: int = 0
|
|
193
|
+
) -> Dict[str, Any]:
|
|
194
|
+
"""Get the inspector/transform properties of a timeline clip (zoom, pan, tilt, opacity, crop, etc.).
|
|
195
|
+
Args:
|
|
196
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
197
|
+
track_index: 1-based track index.
|
|
198
|
+
clip_index: 0-based clip position on that track."""
|
|
199
|
+
return _get(f"/clip/properties?track_type={track_type}&track_index={track_index}&clip_index={clip_index}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
203
|
+
# WRITE TOOLS — Navigation
|
|
204
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
205
|
+
|
|
206
|
+
@mcp.tool()
|
|
207
|
+
def open_page(page: str) -> Dict[str, Any]:
|
|
208
|
+
"""Switch DaVinci Resolve to a different page.
|
|
209
|
+
Args:
|
|
210
|
+
page: One of 'media', 'cut', 'edit', 'fusion', 'color', 'fairlight', 'deliver'."""
|
|
211
|
+
return _post("/page", {"page": page})
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@mcp.tool()
|
|
215
|
+
def set_playhead(timecode: str) -> Dict[str, Any]:
|
|
216
|
+
"""Move the playhead to a specific timecode in the current timeline.
|
|
217
|
+
Args:
|
|
218
|
+
timecode: Timecode string, e.g. '01:00:05:00' or '00:00:30:00'."""
|
|
219
|
+
return _post("/playhead", {"timecode": timecode})
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
223
|
+
# WRITE TOOLS — Timeline Markers
|
|
224
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
225
|
+
|
|
226
|
+
@mcp.tool()
|
|
227
|
+
def add_marker(
|
|
228
|
+
frameId: int,
|
|
229
|
+
color: str = "Blue",
|
|
230
|
+
name: str = "",
|
|
231
|
+
note: str = "",
|
|
232
|
+
duration: int = 1,
|
|
233
|
+
customData: str = "",
|
|
234
|
+
) -> Dict[str, Any]:
|
|
235
|
+
"""Add a marker to the current timeline.
|
|
236
|
+
Args:
|
|
237
|
+
frameId: Frame number (relative to timeline start) where the marker is placed.
|
|
238
|
+
color: Marker color — 'Blue', 'Cyan', 'Green', 'Yellow', 'Red', 'Pink',
|
|
239
|
+
'Purple', 'Fuchsia', 'Rose', 'Lavender', 'Sky', 'Mint', 'Lemon',
|
|
240
|
+
'Sand', 'Cocoa', 'Cream'.
|
|
241
|
+
name: Marker name/title.
|
|
242
|
+
note: Marker note/description.
|
|
243
|
+
duration: Duration in frames (default 1).
|
|
244
|
+
customData: Optional custom data string for scripting use."""
|
|
245
|
+
return _post("/marker/add", {
|
|
246
|
+
"frameId": frameId, "color": color, "name": name,
|
|
247
|
+
"note": note, "duration": duration, "customData": customData,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@mcp.tool()
|
|
252
|
+
def delete_markers(frameId: Optional[int] = None, color: Optional[str] = None) -> Dict[str, Any]:
|
|
253
|
+
"""Delete timeline markers by frame position or by color.
|
|
254
|
+
Args:
|
|
255
|
+
frameId: Delete the specific marker at this frame number.
|
|
256
|
+
color: Delete all markers of this color. Use 'All' to delete every marker."""
|
|
257
|
+
body: Dict[str, Any] = {}
|
|
258
|
+
if frameId is not None:
|
|
259
|
+
body["frameId"] = frameId
|
|
260
|
+
if color is not None:
|
|
261
|
+
body["color"] = color
|
|
262
|
+
return _post("/marker/delete", body)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
266
|
+
# WRITE TOOLS — Timeline Management
|
|
267
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
268
|
+
|
|
269
|
+
@mcp.tool()
|
|
270
|
+
def switch_timeline(index: int) -> Dict[str, Any]:
|
|
271
|
+
"""Switch to a different timeline in the project.
|
|
272
|
+
Args:
|
|
273
|
+
index: 1-based timeline index. Use get_project_info() to see timelineCount."""
|
|
274
|
+
return _post("/timeline/switch", {"index": index})
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@mcp.tool()
|
|
278
|
+
def create_timeline(name: str) -> Dict[str, Any]:
|
|
279
|
+
"""Create a new empty timeline in the media pool.
|
|
280
|
+
Args:
|
|
281
|
+
name: Name for the new timeline."""
|
|
282
|
+
return _post("/timeline/create", {"name": name})
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@mcp.tool()
|
|
286
|
+
def rename_timeline(name: str) -> Dict[str, Any]:
|
|
287
|
+
"""Rename the current timeline.
|
|
288
|
+
Args:
|
|
289
|
+
name: New name for the timeline."""
|
|
290
|
+
return _post("/timeline/rename", {"name": name})
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@mcp.tool()
|
|
294
|
+
def duplicate_timeline(name: str = "") -> Dict[str, Any]:
|
|
295
|
+
"""Duplicate the current timeline.
|
|
296
|
+
Args:
|
|
297
|
+
name: Optional name for the duplicated timeline."""
|
|
298
|
+
return _post("/timeline/duplicate", {"name": name})
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
302
|
+
# WRITE TOOLS — Track Management
|
|
303
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
304
|
+
|
|
305
|
+
@mcp.tool()
|
|
306
|
+
def add_track(track_type: str, sub_track_type: str = "") -> Dict[str, Any]:
|
|
307
|
+
"""Add a new track to the current timeline.
|
|
308
|
+
Args:
|
|
309
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
310
|
+
sub_track_type: For audio tracks: 'mono', 'stereo', '5.1', '7.1', etc. Defaults to 'mono'."""
|
|
311
|
+
return _post("/track/add", {"trackType": track_type, "subTrackType": sub_track_type})
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@mcp.tool()
|
|
315
|
+
def delete_track(track_type: str, track_index: int) -> Dict[str, Any]:
|
|
316
|
+
"""Delete a track from the current timeline.
|
|
317
|
+
Args:
|
|
318
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
319
|
+
track_index: 1-based index of the track to delete."""
|
|
320
|
+
return _post("/track/delete", {"trackType": track_type, "trackIndex": track_index})
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
@mcp.tool()
|
|
324
|
+
def set_track_enable(track_type: str, track_index: int, enabled: bool) -> Dict[str, Any]:
|
|
325
|
+
"""Enable or disable a track in the current timeline.
|
|
326
|
+
Args:
|
|
327
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
328
|
+
track_index: 1-based track index.
|
|
329
|
+
enabled: True to enable, False to disable."""
|
|
330
|
+
return _post("/track/enable", {"trackType": track_type, "trackIndex": track_index, "enabled": enabled})
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@mcp.tool()
|
|
334
|
+
def set_track_lock(track_type: str, track_index: int, locked: bool) -> Dict[str, Any]:
|
|
335
|
+
"""Lock or unlock a track in the current timeline.
|
|
336
|
+
Args:
|
|
337
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
338
|
+
track_index: 1-based track index.
|
|
339
|
+
locked: True to lock, False to unlock."""
|
|
340
|
+
return _post("/track/lock", {"trackType": track_type, "trackIndex": track_index, "locked": locked})
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@mcp.tool()
|
|
344
|
+
def set_track_name(track_type: str, track_index: int, name: str) -> Dict[str, Any]:
|
|
345
|
+
"""Rename a track in the current timeline.
|
|
346
|
+
Args:
|
|
347
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
348
|
+
track_index: 1-based track index.
|
|
349
|
+
name: New name for the track."""
|
|
350
|
+
return _post("/track/rename", {"trackType": track_type, "trackIndex": track_index, "name": name})
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
354
|
+
# WRITE TOOLS — Media Management
|
|
355
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
356
|
+
|
|
357
|
+
@mcp.tool()
|
|
358
|
+
def import_media(file_paths: List[str]) -> Dict[str, Any]:
|
|
359
|
+
"""Import media files into the current media pool folder.
|
|
360
|
+
Args:
|
|
361
|
+
file_paths: List of absolute file paths (Windows paths as seen by Resolve,
|
|
362
|
+
e.g. ['C:\\\\Users\\\\user\\\\Videos\\\\clip.mp4'])."""
|
|
363
|
+
return _post("/media/import", {"filePaths": file_paths})
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@mcp.tool()
|
|
367
|
+
def append_to_timeline(clip_name: str) -> Dict[str, Any]:
|
|
368
|
+
"""Append a media pool clip to the end of the current timeline.
|
|
369
|
+
Args:
|
|
370
|
+
clip_name: Name of the clip in the media pool (as returned by get_media_pool)."""
|
|
371
|
+
return _post("/media/append", {"clipName": clip_name})
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@mcp.tool()
|
|
375
|
+
def insert_to_timeline(
|
|
376
|
+
clip_name: str,
|
|
377
|
+
track_index: int = 1,
|
|
378
|
+
record_frame: int = 0,
|
|
379
|
+
start_frame: int = -1,
|
|
380
|
+
end_frame: int = -1,
|
|
381
|
+
) -> Dict[str, Any]:
|
|
382
|
+
"""Insert a media pool clip at a specific track and timeline position.
|
|
383
|
+
Args:
|
|
384
|
+
clip_name: Name of the clip in the media pool.
|
|
385
|
+
track_index: 1-based video track index to place the clip on.
|
|
386
|
+
record_frame: Timeline frame position where the clip should start.
|
|
387
|
+
start_frame: Source clip in-point frame (-1 = from beginning).
|
|
388
|
+
end_frame: Source clip out-point frame (-1 = to end)."""
|
|
389
|
+
payload: Dict[str, Any] = {
|
|
390
|
+
"clipName": clip_name,
|
|
391
|
+
"trackIndex": track_index,
|
|
392
|
+
"recordFrame": record_frame,
|
|
393
|
+
}
|
|
394
|
+
if start_frame >= 0:
|
|
395
|
+
payload["startFrame"] = start_frame
|
|
396
|
+
if end_frame >= 0:
|
|
397
|
+
payload["endFrame"] = end_frame
|
|
398
|
+
return _post("/media/insert", payload)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
402
|
+
# WRITE TOOLS — Clip Operations
|
|
403
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
404
|
+
|
|
405
|
+
@mcp.tool()
|
|
406
|
+
def set_clip_color(
|
|
407
|
+
track_type: str, track_index: int, clip_index: int, color: str = ""
|
|
408
|
+
) -> Dict[str, Any]:
|
|
409
|
+
"""Set the color label of a clip on the timeline.
|
|
410
|
+
Args:
|
|
411
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
412
|
+
track_index: 1-based track index.
|
|
413
|
+
clip_index: 0-based clip position on that track.
|
|
414
|
+
color: Color name (e.g. 'Orange', 'Teal', 'Lime'). Empty string clears the color."""
|
|
415
|
+
return _post("/clip/color", {
|
|
416
|
+
"trackType": track_type, "trackIndex": track_index,
|
|
417
|
+
"clipIndex": clip_index, "color": color,
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@mcp.tool()
|
|
422
|
+
def set_clip_enabled(
|
|
423
|
+
track_type: str, track_index: int, clip_index: int, enabled: bool
|
|
424
|
+
) -> Dict[str, Any]:
|
|
425
|
+
"""Enable or disable a clip on the timeline.
|
|
426
|
+
Args:
|
|
427
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
428
|
+
track_index: 1-based track index.
|
|
429
|
+
clip_index: 0-based clip position on that track.
|
|
430
|
+
enabled: True to enable, False to disable."""
|
|
431
|
+
return _post("/clip/enabled", {
|
|
432
|
+
"trackType": track_type, "trackIndex": track_index,
|
|
433
|
+
"clipIndex": clip_index, "enabled": enabled,
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
@mcp.tool()
|
|
438
|
+
def set_clip_properties(
|
|
439
|
+
track_type: str, track_index: int, clip_index: int,
|
|
440
|
+
properties: Dict[str, Any] = {},
|
|
441
|
+
) -> Dict[str, Any]:
|
|
442
|
+
"""Set transform and compositing properties on a timeline clip.
|
|
443
|
+
Args:
|
|
444
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
445
|
+
track_index: 1-based track index.
|
|
446
|
+
clip_index: 0-based clip position on that track.
|
|
447
|
+
properties: Dict of property key-value pairs. Supported keys include:
|
|
448
|
+
'Pan', 'Tilt' (float), 'ZoomX', 'ZoomY' (0-100),
|
|
449
|
+
'RotationAngle' (-360 to 360), 'Opacity' (0-100),
|
|
450
|
+
'CropLeft', 'CropRight', 'CropTop', 'CropBottom' (float),
|
|
451
|
+
'FlipX', 'FlipY' (bool), 'Distortion' (-1 to 1),
|
|
452
|
+
'AnchorPointX', 'AnchorPointY' (float),
|
|
453
|
+
'CompositeMode' (int: 0=Normal, 4=Multiply, 5=Screen, 6=Overlay, etc.),
|
|
454
|
+
'Scaling' (int: 0=Project, 1=Crop, 2=Fit, 3=Fill, 4=Stretch).
|
|
455
|
+
Example: {'ZoomX': 50, 'ZoomY': 50, 'Pan': -200, 'Opacity': 80}"""
|
|
456
|
+
return _post("/clip/properties", {
|
|
457
|
+
"trackType": track_type, "trackIndex": track_index,
|
|
458
|
+
"clipIndex": clip_index, "properties": properties,
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
463
|
+
# WRITE TOOLS — Titles & Generators
|
|
464
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
465
|
+
|
|
466
|
+
@mcp.tool()
|
|
467
|
+
def insert_title(title_name: str, fusion_title: bool = False) -> Dict[str, Any]:
|
|
468
|
+
"""Insert a title at the playhead in the current timeline.
|
|
469
|
+
Args:
|
|
470
|
+
title_name: Name of the title template (e.g. 'Text+', 'Scroll', 'Lower Third').
|
|
471
|
+
fusion_title: If True, inserts a Fusion title instead of a standard title."""
|
|
472
|
+
return _post("/title/insert", {"titleName": title_name, "fusionTitle": fusion_title})
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
@mcp.tool()
|
|
476
|
+
def insert_generator(generator_name: str, fusion_generator: bool = False) -> Dict[str, Any]:
|
|
477
|
+
"""Insert a generator at the playhead in the current timeline.
|
|
478
|
+
Args:
|
|
479
|
+
generator_name: Name of the generator (e.g. 'Solid Color', '10 Step', 'Grey Scale').
|
|
480
|
+
fusion_generator: If True, inserts a Fusion generator instead of a standard one."""
|
|
481
|
+
return _post("/generator/insert", {"generatorName": generator_name, "fusionGenerator": fusion_generator})
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
@mcp.tool()
|
|
485
|
+
def insert_fusion_composition() -> Dict[str, Any]:
|
|
486
|
+
"""Insert an empty Fusion composition at the playhead in the current timeline.
|
|
487
|
+
Opens a blank Fusion comp that can be edited in the Fusion page."""
|
|
488
|
+
return _post("/fusion/insert", {})
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
492
|
+
# WRITE TOOLS — Rendering
|
|
493
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
494
|
+
|
|
495
|
+
@mcp.tool()
|
|
496
|
+
def set_render_settings(settings: Dict[str, Any]) -> Dict[str, Any]:
|
|
497
|
+
"""Configure render settings for the project.
|
|
498
|
+
Args:
|
|
499
|
+
settings: Dict of render settings. Supported keys include:
|
|
500
|
+
'TargetDir' (str), 'CustomName' (str), 'SelectAllFrames' (bool),
|
|
501
|
+
'MarkIn' (int), 'MarkOut' (int), 'ExportVideo' (bool),
|
|
502
|
+
'ExportAudio' (bool), 'FormatWidth' (int), 'FormatHeight' (int),
|
|
503
|
+
'FrameRate' (float), 'VideoQuality' (int or str like 'Best'),
|
|
504
|
+
'AudioCodec' (str), 'AudioBitDepth' (int), 'AudioSampleRate' (int),
|
|
505
|
+
'ExportAlpha' (bool), 'NetworkOptimization' (bool).
|
|
506
|
+
Example: {'TargetDir': 'C:\\\\output', 'CustomName': 'final', 'SelectAllFrames': True}"""
|
|
507
|
+
return _post("/render/settings", {"settings": settings})
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@mcp.tool()
|
|
511
|
+
def set_render_format(format: str, codec: str) -> Dict[str, Any]:
|
|
512
|
+
"""Set the render output format and codec.
|
|
513
|
+
Args:
|
|
514
|
+
format: Render format (e.g. 'mp4', 'mov', 'mxf'). Use get_render_formats() to see options.
|
|
515
|
+
codec: Codec name (e.g. 'H264', 'H265'). Use get_render_formats(format) to see codecs."""
|
|
516
|
+
return _post("/render/format", {"format": format, "codec": codec})
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@mcp.tool()
|
|
520
|
+
def get_render_formats(format: str = "") -> Dict[str, Any]:
|
|
521
|
+
"""List available render formats, or codecs for a specific format.
|
|
522
|
+
Args:
|
|
523
|
+
format: If provided, returns available codecs for this format. Otherwise lists all formats."""
|
|
524
|
+
return _post("/render/formats", {"format": format})
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
@mcp.tool()
|
|
528
|
+
def add_render_job() -> Dict[str, Any]:
|
|
529
|
+
"""Add a render job to the queue based on current render settings.
|
|
530
|
+
Returns the job ID if successful. Configure settings first with set_render_settings()."""
|
|
531
|
+
return _post("/render/job/add", {})
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@mcp.tool()
|
|
535
|
+
def start_rendering(job_ids: List[str] = []) -> Dict[str, Any]:
|
|
536
|
+
"""Start rendering queued jobs.
|
|
537
|
+
Args:
|
|
538
|
+
job_ids: Optional list of specific job IDs to render. Empty = render all queued jobs."""
|
|
539
|
+
return _post("/render/start", {"jobIds": job_ids})
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@mcp.tool()
|
|
543
|
+
def stop_rendering() -> Dict[str, Any]:
|
|
544
|
+
"""Stop any currently active render process."""
|
|
545
|
+
return _post("/render/stop", {})
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
@mcp.tool()
|
|
549
|
+
def delete_render_job(job_id: str = "", all: bool = False) -> Dict[str, Any]:
|
|
550
|
+
"""Delete render job(s) from the queue.
|
|
551
|
+
Args:
|
|
552
|
+
job_id: ID of a specific job to delete.
|
|
553
|
+
all: If True, deletes all render jobs in the queue."""
|
|
554
|
+
return _post("/render/job/delete", {"jobId": job_id, "all": all})
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
558
|
+
# WRITE TOOLS — Project & Settings
|
|
559
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
560
|
+
|
|
561
|
+
@mcp.tool()
|
|
562
|
+
def save_project() -> Dict[str, Any]:
|
|
563
|
+
"""Save the currently open DaVinci Resolve project."""
|
|
564
|
+
return _post("/project/save", {})
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
@mcp.tool()
|
|
568
|
+
def set_project_setting(key: str, value: str) -> Dict[str, Any]:
|
|
569
|
+
"""Set a project-level setting.
|
|
570
|
+
Args:
|
|
571
|
+
key: Setting name (e.g. 'timelineResolutionWidth', 'timelineFrameRate', 'superScale').
|
|
572
|
+
value: Setting value as string."""
|
|
573
|
+
return _post("/project/setting", {"key": key, "value": value})
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
@mcp.tool()
|
|
577
|
+
def set_timeline_setting(key: str, value: str) -> Dict[str, Any]:
|
|
578
|
+
"""Set a timeline-level setting on the current timeline.
|
|
579
|
+
Args:
|
|
580
|
+
key: Setting name (e.g. 'timelineResolutionWidth', 'timelineResolutionHeight', 'timelineFrameRate').
|
|
581
|
+
value: Setting value as string."""
|
|
582
|
+
return _post("/timeline/setting", {"key": key, "value": value})
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
@mcp.tool()
|
|
586
|
+
def export_current_frame(file_path: str) -> Dict[str, Any]:
|
|
587
|
+
"""Export the current frame (at playhead) as a still image.
|
|
588
|
+
Args:
|
|
589
|
+
file_path: Absolute Windows path with extension (e.g. 'C:\\\\output\\\\frame.png').
|
|
590
|
+
Supported formats: .dpx, .cin, .tif, .jpg, .png, .ppm, .bmp, .xpm."""
|
|
591
|
+
return _post("/project/export-frame", {"filePath": file_path})
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
@mcp.tool()
|
|
595
|
+
def create_subtitles_from_audio() -> Dict[str, Any]:
|
|
596
|
+
"""[STUDIO ONLY] Auto-generate subtitles from the audio in the current timeline using DaVinci Resolve's built-in speech-to-text (Neural Engine).
|
|
597
|
+
Not available in DaVinci Resolve Free. For transcription on Free, use the local AI tool transcribe_timeline instead."""
|
|
598
|
+
return _post("/timeline/subtitles", {})
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
@mcp.tool()
|
|
602
|
+
def detect_scene_cuts() -> Dict[str, Any]:
|
|
603
|
+
"""Automatically detect and create scene cuts along the current timeline."""
|
|
604
|
+
return _post("/timeline/scene-cuts", {})
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
608
|
+
# MEDIA POOL — Deep Access
|
|
609
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
610
|
+
|
|
611
|
+
@mcp.tool()
|
|
612
|
+
def get_media_pool_structure(include_clips: bool = False, max_depth: int = 10) -> Dict[str, Any]:
|
|
613
|
+
"""Get the full media pool folder tree structure.
|
|
614
|
+
Args:
|
|
615
|
+
include_clips: If True, includes clip names in each folder. False by default to keep output small.
|
|
616
|
+
max_depth: Maximum folder depth to traverse (default 10).
|
|
617
|
+
Returns the folder hierarchy with clip counts, and the name of the currently selected folder."""
|
|
618
|
+
return _get("/mediapool/structure", {
|
|
619
|
+
"include_clips": str(include_clips).lower(),
|
|
620
|
+
"max_depth": str(max_depth),
|
|
621
|
+
})
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
@mcp.tool()
|
|
625
|
+
def navigate_media_pool(path: str) -> Dict[str, Any]:
|
|
626
|
+
"""Navigate to a specific folder in the media pool.
|
|
627
|
+
Args:
|
|
628
|
+
path: Folder path using slash separators, e.g. 'Footage/Day1/A-Cam'.
|
|
629
|
+
Use 'root' or '/' to navigate to the root folder.
|
|
630
|
+
Sets the current media pool folder so subsequent operations target it."""
|
|
631
|
+
return _post("/mediapool/navigate", {"path": path})
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
@mcp.tool()
|
|
635
|
+
def create_media_pool_folder(name: str, parent_path: str = "") -> Dict[str, Any]:
|
|
636
|
+
"""Create a new subfolder in the media pool.
|
|
637
|
+
Args:
|
|
638
|
+
name: Name for the new folder.
|
|
639
|
+
parent_path: Path to the parent folder (e.g. 'Footage/Day1'). Empty = current folder."""
|
|
640
|
+
return _post("/mediapool/folder/create", {"name": name, "parentPath": parent_path})
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
@mcp.tool()
|
|
644
|
+
def get_clip_metadata(clip_name: str) -> Dict[str, Any]:
|
|
645
|
+
"""Get all metadata for a media pool clip.
|
|
646
|
+
Args:
|
|
647
|
+
clip_name: Name of the clip in the media pool.
|
|
648
|
+
Returns standard metadata (Description, Comments, Shot, Scene, Take, etc.)
|
|
649
|
+
and any third-party metadata attached to the clip."""
|
|
650
|
+
return _get("/mediapool/clip/metadata", {"clip_name": clip_name})
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@mcp.tool()
|
|
654
|
+
def set_clip_metadata(clip_name: str, metadata: Dict[str, str]) -> Dict[str, Any]:
|
|
655
|
+
"""Set metadata on a media pool clip.
|
|
656
|
+
Args:
|
|
657
|
+
clip_name: Name of the clip in the media pool.
|
|
658
|
+
metadata: Dict of metadata key-value pairs to set, e.g.
|
|
659
|
+
{'Description': 'Wide shot', 'Comments': 'Best take', 'Shot': 'A001'}.
|
|
660
|
+
Common metadata keys: Description, Comments, Keywords, Shot, Scene, Take, Good Take, Angle."""
|
|
661
|
+
return _post("/mediapool/clip/metadata", {"clipName": clip_name, "metadata": metadata})
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@mcp.tool()
|
|
665
|
+
def get_clip_info(clip_name: str) -> Dict[str, Any]:
|
|
666
|
+
"""Get detailed properties for a media pool clip including flags, markers, and all clip attributes.
|
|
667
|
+
Args:
|
|
668
|
+
clip_name: Name of the clip in the media pool.
|
|
669
|
+
Returns clip color, flags, markers, mark in/out points, and all clip properties
|
|
670
|
+
(File Path, Resolution, FPS, Duration, Codec, Audio channels, etc.)."""
|
|
671
|
+
return _get("/mediapool/clip/info", {"clip_name": clip_name})
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
@mcp.tool()
|
|
675
|
+
def set_pool_clip_property(clip_name: str, property_name: str, property_value: str) -> Dict[str, Any]:
|
|
676
|
+
"""Set a property on a media pool clip.
|
|
677
|
+
Args:
|
|
678
|
+
clip_name: Name of the clip in the media pool.
|
|
679
|
+
property_name: Property key, e.g. 'Super Scale', 'Clip Name'.
|
|
680
|
+
property_value: Value to set (as string)."""
|
|
681
|
+
return _post("/mediapool/clip/property", {
|
|
682
|
+
"clipName": clip_name, "propertyName": property_name, "propertyValue": property_value,
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@mcp.tool()
|
|
687
|
+
def delete_media_pool_clips(clip_names: List[str]) -> Dict[str, Any]:
|
|
688
|
+
"""Delete clips from the media pool.
|
|
689
|
+
Args:
|
|
690
|
+
clip_names: List of clip names to delete.
|
|
691
|
+
Returns the count of deleted clips and any names not found."""
|
|
692
|
+
return _post("/mediapool/clips/delete", {"clipNames": clip_names})
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
@mcp.tool()
|
|
696
|
+
def move_media_pool_clips(clip_names: List[str], target_folder: str) -> Dict[str, Any]:
|
|
697
|
+
"""Move clips to a different folder in the media pool.
|
|
698
|
+
Args:
|
|
699
|
+
clip_names: List of clip names to move.
|
|
700
|
+
target_folder: Destination folder path, e.g. 'Footage/Selects'."""
|
|
701
|
+
return _post("/mediapool/clips/move", {"clipNames": clip_names, "targetFolder": target_folder})
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
@mcp.tool()
|
|
705
|
+
def relink_media_pool_clips(clip_names: List[str], folder_path: str) -> Dict[str, Any]:
|
|
706
|
+
"""Relink media pool clips to a new filesystem folder.
|
|
707
|
+
Args:
|
|
708
|
+
clip_names: List of clip names to relink.
|
|
709
|
+
folder_path: Absolute filesystem path to the new media location."""
|
|
710
|
+
return _post("/mediapool/clips/relink", {"clipNames": clip_names, "folderPath": folder_path})
|
|
711
|
+
|
|
712
|
+
|
|
713
|
+
@mcp.tool()
|
|
714
|
+
def unlink_media_pool_clips(clip_names: List[str]) -> Dict[str, Any]:
|
|
715
|
+
"""Unlink media pool clips from their source files.
|
|
716
|
+
Args:
|
|
717
|
+
clip_names: List of clip names to unlink."""
|
|
718
|
+
return _post("/mediapool/clips/unlink", {"clipNames": clip_names})
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
@mcp.tool()
|
|
722
|
+
def auto_sync_audio(clip_names: List[str], settings: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
723
|
+
"""Auto-sync audio to video clips in the media pool.
|
|
724
|
+
Requires at least one video clip and one audio clip.
|
|
725
|
+
Args:
|
|
726
|
+
clip_names: List of at least 2 clip names (video + audio clips to sync).
|
|
727
|
+
settings: Optional sync settings dict. Keys:
|
|
728
|
+
'mode' ('waveform' or 'timecode', default 'timecode'),
|
|
729
|
+
'channelNumber' (int, for waveform mode),
|
|
730
|
+
'retainEmbeddedAudio' (bool), 'retainVideoMetadata' (bool)."""
|
|
731
|
+
return _post("/mediapool/audio-sync", {"clipNames": clip_names, "settings": settings})
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
@mcp.tool()
|
|
735
|
+
def import_timeline_from_file(file_path: str, import_options: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
736
|
+
"""Import a timeline from an AAF, EDL, XML, FCPXML, DRT, ADL, or OTIO file.
|
|
737
|
+
Args:
|
|
738
|
+
file_path: Absolute path to the timeline file.
|
|
739
|
+
import_options: Optional dict with keys:
|
|
740
|
+
'timelineName' (str), 'importSourceClips' (bool, default True),
|
|
741
|
+
'sourceClipsPath' (str, fallback path for missing media),
|
|
742
|
+
'interlaceProcessing' (bool, AAF only)."""
|
|
743
|
+
return _post("/mediapool/timeline/import", {"filePath": file_path, "importOptions": import_options})
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
@mcp.tool()
|
|
747
|
+
def export_metadata(file_name: str, clip_names: List[str] = []) -> Dict[str, Any]:
|
|
748
|
+
"""Export clip metadata from the media pool to a CSV file.
|
|
749
|
+
Args:
|
|
750
|
+
file_name: Absolute path for the output CSV file.
|
|
751
|
+
clip_names: Optional list of specific clip names. Empty = export all clips."""
|
|
752
|
+
return _post("/mediapool/metadata/export", {"fileName": file_name, "clipNames": clip_names})
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
@mcp.tool()
|
|
756
|
+
def import_media_from_storage(file_paths: List[str]) -> Dict[str, Any]:
|
|
757
|
+
"""Import media files from Resolve's Media Storage into the current media pool folder.
|
|
758
|
+
Uses Media Storage paths (volumes mounted in Resolve) rather than direct filesystem paths.
|
|
759
|
+
Args:
|
|
760
|
+
file_paths: List of absolute file paths as seen in Resolve's Media Storage."""
|
|
761
|
+
return _post("/media/import-storage", {"filePaths": file_paths})
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
765
|
+
# PER-CLIP MARKERS & FLAGS
|
|
766
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
767
|
+
|
|
768
|
+
@mcp.tool()
|
|
769
|
+
def add_clip_marker(
|
|
770
|
+
track_type: str, track_index: int, clip_index: int,
|
|
771
|
+
frameId: int, color: str = "Blue", name: str = "",
|
|
772
|
+
note: str = "", duration: int = 1, customData: str = "",
|
|
773
|
+
) -> Dict[str, Any]:
|
|
774
|
+
"""Add a marker to a specific clip on the timeline (not a timeline marker).
|
|
775
|
+
Args:
|
|
776
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
777
|
+
track_index: 1-based track index.
|
|
778
|
+
clip_index: 0-based clip position on the track.
|
|
779
|
+
frameId: Frame position relative to clip start.
|
|
780
|
+
color: Marker color (Blue, Green, Red, Yellow, etc.).
|
|
781
|
+
name: Marker name/title.
|
|
782
|
+
note: Marker note/description.
|
|
783
|
+
duration: Duration in frames (default 1).
|
|
784
|
+
customData: Optional custom data string."""
|
|
785
|
+
return _post("/clip/marker/add", {
|
|
786
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
787
|
+
"frameId": frameId, "color": color, "name": name,
|
|
788
|
+
"note": note, "duration": duration, "customData": customData,
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
@mcp.tool()
|
|
793
|
+
def get_clip_markers(track_type: str = "video", track_index: int = 1, clip_index: int = 0) -> Dict[str, Any]:
|
|
794
|
+
"""Get all markers on a specific timeline clip.
|
|
795
|
+
Args:
|
|
796
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
797
|
+
track_index: 1-based track index.
|
|
798
|
+
clip_index: 0-based clip position on the track.
|
|
799
|
+
Returns the clip name and list of markers with frame positions, colors, names, and notes."""
|
|
800
|
+
return _get("/clip/markers", {
|
|
801
|
+
"track_type": track_type, "track_index": str(track_index), "clip_index": str(clip_index),
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
@mcp.tool()
|
|
806
|
+
def delete_clip_markers(
|
|
807
|
+
track_type: str, track_index: int, clip_index: int,
|
|
808
|
+
frameId: Optional[int] = None, color: Optional[str] = None, customData: Optional[str] = None,
|
|
809
|
+
) -> Dict[str, Any]:
|
|
810
|
+
"""Delete markers from a specific timeline clip.
|
|
811
|
+
Args:
|
|
812
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
813
|
+
track_index: 1-based track index.
|
|
814
|
+
clip_index: 0-based clip position on the track.
|
|
815
|
+
frameId: Delete the marker at this specific frame.
|
|
816
|
+
color: Delete all markers of this color. Use 'All' to delete every marker.
|
|
817
|
+
customData: Delete the first marker matching this custom data string.
|
|
818
|
+
Provide one of frameId, color, or customData."""
|
|
819
|
+
body: Dict[str, Any] = {"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index}
|
|
820
|
+
if frameId is not None:
|
|
821
|
+
body["frameId"] = frameId
|
|
822
|
+
if color is not None:
|
|
823
|
+
body["color"] = color
|
|
824
|
+
if customData is not None:
|
|
825
|
+
body["customData"] = customData
|
|
826
|
+
return _post("/clip/marker/delete", body)
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
@mcp.tool()
|
|
830
|
+
def add_clip_flag(track_type: str, track_index: int, clip_index: int, color: str) -> Dict[str, Any]:
|
|
831
|
+
"""Add a flag to a timeline clip. Flags are colored labels visible in the timeline.
|
|
832
|
+
Args:
|
|
833
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
834
|
+
track_index: 1-based track index.
|
|
835
|
+
clip_index: 0-based clip position on the track.
|
|
836
|
+
color: Flag color (e.g. 'Blue', 'Green', 'Red', 'Yellow', 'Cyan', 'Pink', 'Purple')."""
|
|
837
|
+
return _post("/clip/flag/add", {
|
|
838
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index, "color": color,
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
@mcp.tool()
|
|
843
|
+
def get_clip_flags(track_type: str = "video", track_index: int = 1, clip_index: int = 0) -> Dict[str, Any]:
|
|
844
|
+
"""Get all flags on a specific timeline clip.
|
|
845
|
+
Args:
|
|
846
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
847
|
+
track_index: 1-based track index.
|
|
848
|
+
clip_index: 0-based clip position on the track.
|
|
849
|
+
Returns the clip name and list of flag colors."""
|
|
850
|
+
return _get("/clip/flags", {
|
|
851
|
+
"track_type": track_type, "track_index": str(track_index), "clip_index": str(clip_index),
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
@mcp.tool()
|
|
856
|
+
def clear_clip_flags(
|
|
857
|
+
track_type: str, track_index: int, clip_index: int, color: str = "All",
|
|
858
|
+
) -> Dict[str, Any]:
|
|
859
|
+
"""Clear flags from a timeline clip.
|
|
860
|
+
Args:
|
|
861
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
862
|
+
track_index: 1-based track index.
|
|
863
|
+
clip_index: 0-based clip position on the track.
|
|
864
|
+
color: Color of flags to clear, or 'All' to clear every flag."""
|
|
865
|
+
return _post("/clip/flag/clear", {
|
|
866
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index, "color": color,
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
871
|
+
# TIMELINE CLIP MANIPULATION
|
|
872
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
873
|
+
|
|
874
|
+
@mcp.tool()
|
|
875
|
+
def delete_timeline_clips(clips: List[Dict[str, Any]], ripple: bool = False) -> Dict[str, Any]:
|
|
876
|
+
"""Delete clips from the timeline.
|
|
877
|
+
Args:
|
|
878
|
+
clips: List of clip references, each a dict with keys:
|
|
879
|
+
'trackType' ('video'/'audio'/'subtitle'), 'trackIndex' (1-based), 'clipIndex' (0-based).
|
|
880
|
+
Example: [{'trackType': 'video', 'trackIndex': 1, 'clipIndex': 0}]
|
|
881
|
+
ripple: If True, performs ripple delete (closes gaps). Default False."""
|
|
882
|
+
return _post("/timeline/clips/delete", {"clips": clips, "ripple": ripple})
|
|
883
|
+
|
|
884
|
+
|
|
885
|
+
@mcp.tool()
|
|
886
|
+
def link_timeline_clips(clips: List[Dict[str, Any]], linked: bool = True) -> Dict[str, Any]:
|
|
887
|
+
"""Link or unlink timeline clips. Linked clips move together when dragged.
|
|
888
|
+
Args:
|
|
889
|
+
clips: List of at least 2 clip references (same format as delete_timeline_clips).
|
|
890
|
+
linked: True to link, False to unlink."""
|
|
891
|
+
return _post("/timeline/clips/link", {"clips": clips, "linked": linked})
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
@mcp.tool()
|
|
895
|
+
def create_compound_clip(
|
|
896
|
+
clips: List[Dict[str, Any]], name: str = "", start_timecode: str = "",
|
|
897
|
+
) -> Dict[str, Any]:
|
|
898
|
+
"""Create a compound clip from selected timeline items.
|
|
899
|
+
A compound clip nests multiple clips into a single item on the timeline.
|
|
900
|
+
Args:
|
|
901
|
+
clips: List of clip references to merge.
|
|
902
|
+
name: Optional name for the compound clip.
|
|
903
|
+
start_timecode: Optional start timecode (e.g. '00:00:00:00')."""
|
|
904
|
+
body: Dict[str, Any] = {"clips": clips}
|
|
905
|
+
if name:
|
|
906
|
+
body["name"] = name
|
|
907
|
+
if start_timecode:
|
|
908
|
+
body["startTimecode"] = start_timecode
|
|
909
|
+
return _post("/timeline/compound-clip", body)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
@mcp.tool()
|
|
913
|
+
def create_fusion_clip(clips: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
914
|
+
"""Create a Fusion clip from selected timeline items.
|
|
915
|
+
A Fusion clip allows complex compositing of multiple source clips in the Fusion page.
|
|
916
|
+
Args:
|
|
917
|
+
clips: List of clip references to merge into a Fusion clip."""
|
|
918
|
+
return _post("/timeline/fusion-clip", {"clips": clips})
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
@mcp.tool()
|
|
922
|
+
def get_current_video_item() -> Dict[str, Any]:
|
|
923
|
+
"""Get information about the clip currently under the playhead.
|
|
924
|
+
Returns the clip name, duration, start/end frames, enabled state, color,
|
|
925
|
+
track position, and source file properties.
|
|
926
|
+
Useful for identifying what the user is looking at before performing operations."""
|
|
927
|
+
return _get("/timeline/current-item")
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
@mcp.tool()
|
|
931
|
+
def get_clip_thumbnail() -> Dict[str, Any]:
|
|
932
|
+
"""Get a thumbnail image of the current clip at the playhead position.
|
|
933
|
+
Only works when on the Color page.
|
|
934
|
+
Returns width, height, format, and base64-encoded RGB image data."""
|
|
935
|
+
return _get("/timeline/thumbnail")
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
@mcp.tool()
|
|
939
|
+
def export_timeline(
|
|
940
|
+
file_name: str, export_type: str, export_subtype: str = "EXPORT_NONE",
|
|
941
|
+
) -> Dict[str, Any]:
|
|
942
|
+
"""Export the current timeline to a file (AAF, EDL, FCPXML, OTIO, etc.).
|
|
943
|
+
Args:
|
|
944
|
+
file_name: Absolute output file path.
|
|
945
|
+
export_type: One of: 'AAF', 'DRT', 'EDL', 'FCP_7_XML', 'FCPXML_1_8',
|
|
946
|
+
'FCPXML_1_9', 'FCPXML_1_10', 'HDR_10_PROFILE_A', 'HDR_10_PROFILE_B',
|
|
947
|
+
'TEXT_CSV', 'TEXT_TAB', 'DOLBY_VISION_VER_2_9', 'DOLBY_VISION_VER_4_0',
|
|
948
|
+
'DOLBY_VISION_VER_5_1', 'OTIO', 'ALE', 'ALE_CDL'.
|
|
949
|
+
export_subtype: Required for AAF ('EXPORT_AAF_NEW' or 'EXPORT_AAF_EXISTING')
|
|
950
|
+
and EDL ('EXPORT_CDL', 'EXPORT_SDL', 'EXPORT_MISSING_CLIPS', or 'EXPORT_NONE').
|
|
951
|
+
For other formats, leave as default 'EXPORT_NONE'."""
|
|
952
|
+
return _post("/timeline/export", {
|
|
953
|
+
"fileName": file_name, "exportType": export_type, "exportSubtype": export_subtype,
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
958
|
+
# GALLERY & STILLS
|
|
959
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
960
|
+
|
|
961
|
+
@mcp.tool()
|
|
962
|
+
def get_gallery_albums() -> Dict[str, Any]:
|
|
963
|
+
"""List all gallery still albums and PowerGrade albums.
|
|
964
|
+
Returns the currently selected album name, and lists of still albums
|
|
965
|
+
and PowerGrade albums with their names, indices, and still counts."""
|
|
966
|
+
return _get("/gallery/albums")
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
@mcp.tool()
|
|
970
|
+
def get_album_stills(album_index: int = 0, album_type: str = "still") -> Dict[str, Any]:
|
|
971
|
+
"""List stills in a gallery album.
|
|
972
|
+
Args:
|
|
973
|
+
album_index: 1-based album index (from get_gallery_albums). 0 = current album.
|
|
974
|
+
album_type: 'still' or 'powergrade'.
|
|
975
|
+
Returns the album name and list of stills with their indices and labels."""
|
|
976
|
+
return _get("/gallery/stills", {"album_index": str(album_index), "album_type": album_type})
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
@mcp.tool()
|
|
980
|
+
def set_current_album(album_index: int, album_type: str = "still") -> Dict[str, Any]:
|
|
981
|
+
"""Set the active gallery album. Stills will be grabbed into this album.
|
|
982
|
+
Args:
|
|
983
|
+
album_index: 1-based album index (from get_gallery_albums).
|
|
984
|
+
album_type: 'still' or 'powergrade'."""
|
|
985
|
+
return _post("/gallery/album/set", {"albumIndex": album_index, "albumType": album_type})
|
|
986
|
+
|
|
987
|
+
|
|
988
|
+
@mcp.tool()
|
|
989
|
+
def create_gallery_album(album_type: str = "still", name: str = "") -> Dict[str, Any]:
|
|
990
|
+
"""Create a new gallery album.
|
|
991
|
+
Args:
|
|
992
|
+
album_type: 'still' for a Still album, 'powergrade' for a PowerGrade album.
|
|
993
|
+
name: Optional name for the new album."""
|
|
994
|
+
body: Dict[str, Any] = {"albumType": album_type}
|
|
995
|
+
if name:
|
|
996
|
+
body["name"] = name
|
|
997
|
+
return _post("/gallery/album/create", body)
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
@mcp.tool()
|
|
1001
|
+
def grab_still() -> Dict[str, Any]:
|
|
1002
|
+
"""Grab a still from the current clip at the playhead position.
|
|
1003
|
+
The still is saved into the currently active gallery album.
|
|
1004
|
+
Must be on the Color page for this to work."""
|
|
1005
|
+
return _post("/gallery/grab", {})
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
@mcp.tool()
|
|
1009
|
+
def grab_all_stills(still_frame_source: int = 2) -> Dict[str, Any]:
|
|
1010
|
+
"""Grab stills from all clips on the timeline.
|
|
1011
|
+
Args:
|
|
1012
|
+
still_frame_source: 1 = first frame of each clip, 2 = middle frame (default).
|
|
1013
|
+
Must be on the Color page. Returns the count of stills grabbed."""
|
|
1014
|
+
return _post("/gallery/grab-all", {"stillFrameSource": still_frame_source})
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
@mcp.tool()
|
|
1018
|
+
def export_stills(
|
|
1019
|
+
folder_path: str, file_prefix: str = "still", format: str = "png",
|
|
1020
|
+
album_index: int = 0, album_type: str = "still", still_indices: List[int] = [],
|
|
1021
|
+
) -> Dict[str, Any]:
|
|
1022
|
+
"""Export stills from a gallery album to disk.
|
|
1023
|
+
Args:
|
|
1024
|
+
folder_path: Absolute path to the output directory.
|
|
1025
|
+
file_prefix: Filename prefix for exported stills (default 'still').
|
|
1026
|
+
format: Export format: 'dpx', 'cin', 'tif', 'jpg', 'png' (default), 'ppm', 'bmp', 'xpm', 'drx'.
|
|
1027
|
+
album_index: 1-based album index. 0 = current album.
|
|
1028
|
+
album_type: 'still' or 'powergrade'.
|
|
1029
|
+
still_indices: Optional list of 1-based still indices to export. Empty = export all."""
|
|
1030
|
+
return _post("/gallery/stills/export", {
|
|
1031
|
+
"folderPath": folder_path, "filePrefix": file_prefix, "format": format,
|
|
1032
|
+
"albumIndex": album_index, "albumType": album_type, "stillIndices": still_indices,
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
@mcp.tool()
|
|
1037
|
+
def import_stills(file_paths: List[str]) -> Dict[str, Any]:
|
|
1038
|
+
"""Import stills (grade references) into the current gallery album.
|
|
1039
|
+
Args:
|
|
1040
|
+
file_paths: List of absolute file paths to import (DPX, TIFF, JPG, PNG, DRX, etc.)."""
|
|
1041
|
+
return _post("/gallery/stills/import", {"filePaths": file_paths})
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
@mcp.tool()
|
|
1045
|
+
def delete_stills(
|
|
1046
|
+
still_indices: List[int], album_index: int = 0, album_type: str = "still",
|
|
1047
|
+
) -> Dict[str, Any]:
|
|
1048
|
+
"""Delete stills from a gallery album.
|
|
1049
|
+
Args:
|
|
1050
|
+
still_indices: List of 1-based still indices to delete (from get_album_stills).
|
|
1051
|
+
album_index: 1-based album index. 0 = current album.
|
|
1052
|
+
album_type: 'still' or 'powergrade'."""
|
|
1053
|
+
return _post("/gallery/stills/delete", {
|
|
1054
|
+
"stillIndices": still_indices, "albumIndex": album_index, "albumType": album_type,
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
@mcp.tool()
|
|
1059
|
+
def set_still_label(still_index: int, label: str) -> Dict[str, Any]:
|
|
1060
|
+
"""Set the label on a gallery still in the current album.
|
|
1061
|
+
Args:
|
|
1062
|
+
still_index: 1-based still index (from get_album_stills).
|
|
1063
|
+
label: Label text to set on the still."""
|
|
1064
|
+
return _post("/gallery/stills/label", {"stillIndex": still_index, "label": label})
|
|
1065
|
+
|
|
1066
|
+
|
|
1067
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1068
|
+
# COLOR GRADING / NODE GRAPH / LUT / CDL
|
|
1069
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1070
|
+
|
|
1071
|
+
@mcp.tool()
|
|
1072
|
+
def get_node_graph(
|
|
1073
|
+
track_type: str = "video", track_index: int = 1, clip_index: int = 0,
|
|
1074
|
+
scope: str = "clip", layer_index: int = 1,
|
|
1075
|
+
) -> Dict[str, Any]:
|
|
1076
|
+
"""Get the color node graph for a clip or the timeline.
|
|
1077
|
+
Args:
|
|
1078
|
+
track_type: 'video', 'audio', or 'subtitle' (ignored if scope='timeline').
|
|
1079
|
+
track_index: 1-based track index.
|
|
1080
|
+
clip_index: 0-based clip position.
|
|
1081
|
+
scope: 'clip' to get a clip's node graph, 'timeline' for the timeline's graph.
|
|
1082
|
+
layer_index: Node stack layer (1-based, default 1).
|
|
1083
|
+
Returns list of nodes with index, label, LUT path, tools, and cache mode."""
|
|
1084
|
+
params = {"scope": scope, "track_type": track_type, "track_index": str(track_index),
|
|
1085
|
+
"clip_index": str(clip_index), "layer_index": str(layer_index)}
|
|
1086
|
+
return _get("/clip/node-graph", params)
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
@mcp.tool()
|
|
1090
|
+
def set_lut(
|
|
1091
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1092
|
+
node_index: int, lut_path: str, layer_index: int = 1,
|
|
1093
|
+
) -> Dict[str, Any]:
|
|
1094
|
+
"""Apply a LUT to a specific node in a clip's color graph.
|
|
1095
|
+
Args:
|
|
1096
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1097
|
+
track_index: 1-based track index.
|
|
1098
|
+
clip_index: 0-based clip position.
|
|
1099
|
+
node_index: 1-based node index in the graph.
|
|
1100
|
+
lut_path: Absolute or relative path to the LUT file (.cube, etc.).
|
|
1101
|
+
Resolve must have discovered the LUT (use refresh_lut_list if needed).
|
|
1102
|
+
layer_index: Node stack layer (default 1)."""
|
|
1103
|
+
return _post("/color/set-lut", {
|
|
1104
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1105
|
+
"nodeIndex": node_index, "lutPath": lut_path, "layerIndex": layer_index,
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
@mcp.tool()
|
|
1110
|
+
def get_lut(
|
|
1111
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1112
|
+
node_index: int, layer_index: int = 1,
|
|
1113
|
+
) -> Dict[str, Any]:
|
|
1114
|
+
"""Get the LUT applied to a specific node.
|
|
1115
|
+
Args:
|
|
1116
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1117
|
+
track_index: 1-based track index.
|
|
1118
|
+
clip_index: 0-based clip position.
|
|
1119
|
+
node_index: 1-based node index.
|
|
1120
|
+
layer_index: Node stack layer (default 1)."""
|
|
1121
|
+
return _post("/color/get-lut", {
|
|
1122
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1123
|
+
"nodeIndex": node_index, "layerIndex": layer_index,
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
@mcp.tool()
|
|
1128
|
+
def set_node_enabled(
|
|
1129
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1130
|
+
node_index: int, enabled: bool, layer_index: int = 1,
|
|
1131
|
+
) -> Dict[str, Any]:
|
|
1132
|
+
"""Enable or disable a node in the color graph.
|
|
1133
|
+
Args:
|
|
1134
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1135
|
+
track_index: 1-based track index.
|
|
1136
|
+
clip_index: 0-based clip position.
|
|
1137
|
+
node_index: 1-based node index.
|
|
1138
|
+
enabled: True to enable, False to disable.
|
|
1139
|
+
layer_index: Node stack layer (default 1)."""
|
|
1140
|
+
return _post("/color/set-node-enabled", {
|
|
1141
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1142
|
+
"nodeIndex": node_index, "enabled": enabled, "layerIndex": layer_index,
|
|
1143
|
+
})
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
@mcp.tool()
|
|
1147
|
+
def apply_grade_from_drx(
|
|
1148
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1149
|
+
drx_path: str, grade_mode: int = 0, layer_index: int = 1,
|
|
1150
|
+
) -> Dict[str, Any]:
|
|
1151
|
+
"""Apply a color grade from a DRX still file to a clip.
|
|
1152
|
+
Args:
|
|
1153
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1154
|
+
track_index: 1-based track index.
|
|
1155
|
+
clip_index: 0-based clip position.
|
|
1156
|
+
drx_path: Absolute path to the .drx still file.
|
|
1157
|
+
grade_mode: 0 = No keyframes, 1 = Source Timecode aligned, 2 = Start Frames aligned.
|
|
1158
|
+
layer_index: Node stack layer (default 1)."""
|
|
1159
|
+
return _post("/color/apply-drx", {
|
|
1160
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1161
|
+
"drxPath": drx_path, "gradeMode": grade_mode, "layerIndex": layer_index,
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
@mcp.tool()
|
|
1166
|
+
def reset_all_grades(
|
|
1167
|
+
track_type: str, track_index: int, clip_index: int, layer_index: int = 1,
|
|
1168
|
+
) -> Dict[str, Any]:
|
|
1169
|
+
"""Reset all color grades on a clip's node graph back to default.
|
|
1170
|
+
Args:
|
|
1171
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1172
|
+
track_index: 1-based track index.
|
|
1173
|
+
clip_index: 0-based clip position.
|
|
1174
|
+
layer_index: Node stack layer (default 1)."""
|
|
1175
|
+
return _post("/color/reset-grades", {
|
|
1176
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1177
|
+
"layerIndex": layer_index,
|
|
1178
|
+
})
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
@mcp.tool()
|
|
1182
|
+
def apply_arri_cdl_lut(
|
|
1183
|
+
track_type: str, track_index: int, clip_index: int, layer_index: int = 1,
|
|
1184
|
+
) -> Dict[str, Any]:
|
|
1185
|
+
"""Apply ARRI CDL and LUT to a clip.
|
|
1186
|
+
Args:
|
|
1187
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1188
|
+
track_index: 1-based track index.
|
|
1189
|
+
clip_index: 0-based clip position.
|
|
1190
|
+
layer_index: Node stack layer (default 1)."""
|
|
1191
|
+
return _post("/color/apply-arri-cdl", {
|
|
1192
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1193
|
+
"layerIndex": layer_index,
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
@mcp.tool()
|
|
1198
|
+
def set_cdl(
|
|
1199
|
+
track_type: str, track_index: int, clip_index: int, cdl: Dict[str, str],
|
|
1200
|
+
) -> Dict[str, Any]:
|
|
1201
|
+
"""Set CDL (Color Decision List) values on a clip.
|
|
1202
|
+
Args:
|
|
1203
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1204
|
+
track_index: 1-based track index.
|
|
1205
|
+
clip_index: 0-based clip position.
|
|
1206
|
+
cdl: CDL map with keys 'NodeIndex' (string, 1-based), 'Slope' (e.g. '0.5 0.4 0.2'),
|
|
1207
|
+
'Offset' (e.g. '0.4 0.3 0.2'), 'Power' (e.g. '0.6 0.7 0.8'), 'Saturation' (e.g. '0.65').
|
|
1208
|
+
Example: {'NodeIndex': '1', 'Slope': '1.0 1.0 1.0', 'Offset': '0 0 0', 'Power': '1 1 1', 'Saturation': '1.0'}"""
|
|
1209
|
+
return _post("/color/set-cdl", {
|
|
1210
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index, "cdl": cdl,
|
|
1211
|
+
})
|
|
1212
|
+
|
|
1213
|
+
|
|
1214
|
+
@mcp.tool()
|
|
1215
|
+
def export_lut(
|
|
1216
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1217
|
+
path: str, export_type: str = "33PTCUBE",
|
|
1218
|
+
) -> Dict[str, Any]:
|
|
1219
|
+
"""Export a LUT from a clip's color grading.
|
|
1220
|
+
Args:
|
|
1221
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1222
|
+
track_index: 1-based track index.
|
|
1223
|
+
clip_index: 0-based clip position.
|
|
1224
|
+
path: Output file path (include filename; extension auto-appended if wrong).
|
|
1225
|
+
export_type: '17PTCUBE', '33PTCUBE' (default), '65PTCUBE', or 'PANASONICVLUT'."""
|
|
1226
|
+
return _post("/color/export-lut", {
|
|
1227
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1228
|
+
"path": path, "exportType": export_type,
|
|
1229
|
+
})
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
@mcp.tool()
|
|
1233
|
+
def copy_grades(source: Dict[str, Any], targets: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
1234
|
+
"""Copy color grades from one clip to others.
|
|
1235
|
+
Args:
|
|
1236
|
+
source: Source clip reference: {trackType, trackIndex, clipIndex}.
|
|
1237
|
+
targets: List of target clip references (same format)."""
|
|
1238
|
+
return _post("/color/copy-grades", {"source": source, "targets": targets})
|
|
1239
|
+
|
|
1240
|
+
|
|
1241
|
+
@mcp.tool()
|
|
1242
|
+
def reset_node_colors(track_type: str, track_index: int, clip_index: int) -> Dict[str, Any]:
|
|
1243
|
+
"""Reset node colors for all nodes in a clip's active color version.
|
|
1244
|
+
Args:
|
|
1245
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1246
|
+
track_index: 1-based track index.
|
|
1247
|
+
clip_index: 0-based clip position."""
|
|
1248
|
+
return _post("/color/reset-node-colors", {
|
|
1249
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1254
|
+
# COLOR VERSIONS
|
|
1255
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1256
|
+
|
|
1257
|
+
@mcp.tool()
|
|
1258
|
+
def get_color_versions(
|
|
1259
|
+
track_type: str = "video", track_index: int = 1, clip_index: int = 0,
|
|
1260
|
+
) -> Dict[str, Any]:
|
|
1261
|
+
"""Get all color versions for a clip (local and remote).
|
|
1262
|
+
Args:
|
|
1263
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1264
|
+
track_index: 1-based track index.
|
|
1265
|
+
clip_index: 0-based clip position.
|
|
1266
|
+
Returns the current version, and lists of local and remote version names."""
|
|
1267
|
+
return _get("/clip/color-versions", {
|
|
1268
|
+
"track_type": track_type, "track_index": str(track_index), "clip_index": str(clip_index),
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
|
|
1272
|
+
@mcp.tool()
|
|
1273
|
+
def add_color_version(
|
|
1274
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1275
|
+
version_name: str, version_type: int = 0,
|
|
1276
|
+
) -> Dict[str, Any]:
|
|
1277
|
+
"""Add a new color version to a clip.
|
|
1278
|
+
Args:
|
|
1279
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1280
|
+
track_index: 1-based track index.
|
|
1281
|
+
clip_index: 0-based clip position.
|
|
1282
|
+
version_name: Name for the new version.
|
|
1283
|
+
version_type: 0 = local (default), 1 = remote."""
|
|
1284
|
+
return _post("/color/version/add", {
|
|
1285
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1286
|
+
"versionName": version_name, "versionType": version_type,
|
|
1287
|
+
})
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
@mcp.tool()
|
|
1291
|
+
def load_color_version(
|
|
1292
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1293
|
+
version_name: str, version_type: int = 0,
|
|
1294
|
+
) -> Dict[str, Any]:
|
|
1295
|
+
"""Load/activate a named color version on a clip.
|
|
1296
|
+
Args:
|
|
1297
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1298
|
+
track_index: 1-based track index.
|
|
1299
|
+
clip_index: 0-based clip position.
|
|
1300
|
+
version_name: Name of the version to load.
|
|
1301
|
+
version_type: 0 = local, 1 = remote."""
|
|
1302
|
+
return _post("/color/version/load", {
|
|
1303
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1304
|
+
"versionName": version_name, "versionType": version_type,
|
|
1305
|
+
})
|
|
1306
|
+
|
|
1307
|
+
|
|
1308
|
+
@mcp.tool()
|
|
1309
|
+
def delete_color_version(
|
|
1310
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1311
|
+
version_name: str, version_type: int = 0,
|
|
1312
|
+
) -> Dict[str, Any]:
|
|
1313
|
+
"""Delete a color version from a clip.
|
|
1314
|
+
Args:
|
|
1315
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1316
|
+
track_index: 1-based track index.
|
|
1317
|
+
clip_index: 0-based clip position.
|
|
1318
|
+
version_name: Name of the version to delete.
|
|
1319
|
+
version_type: 0 = local, 1 = remote."""
|
|
1320
|
+
return _post("/color/version/delete", {
|
|
1321
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1322
|
+
"versionName": version_name, "versionType": version_type,
|
|
1323
|
+
})
|
|
1324
|
+
|
|
1325
|
+
|
|
1326
|
+
@mcp.tool()
|
|
1327
|
+
def rename_color_version(
|
|
1328
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1329
|
+
old_name: str, new_name: str, version_type: int = 0,
|
|
1330
|
+
) -> Dict[str, Any]:
|
|
1331
|
+
"""Rename a color version on a clip.
|
|
1332
|
+
Args:
|
|
1333
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1334
|
+
track_index: 1-based track index.
|
|
1335
|
+
clip_index: 0-based clip position.
|
|
1336
|
+
old_name: Current version name.
|
|
1337
|
+
new_name: New version name.
|
|
1338
|
+
version_type: 0 = local, 1 = remote."""
|
|
1339
|
+
return _post("/color/version/rename", {
|
|
1340
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1341
|
+
"oldName": old_name, "newName": new_name, "versionType": version_type,
|
|
1342
|
+
})
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1346
|
+
# COLOR GROUPS
|
|
1347
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1348
|
+
|
|
1349
|
+
@mcp.tool()
|
|
1350
|
+
def get_color_groups() -> Dict[str, Any]:
|
|
1351
|
+
"""List all color groups in the current project."""
|
|
1352
|
+
return _get("/color/groups")
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
@mcp.tool()
|
|
1356
|
+
def add_color_group(group_name: str) -> Dict[str, Any]:
|
|
1357
|
+
"""Create a new color group. Group name must be unique.
|
|
1358
|
+
Args:
|
|
1359
|
+
group_name: Name for the new color group."""
|
|
1360
|
+
return _post("/color/group/add", {"groupName": group_name})
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
@mcp.tool()
|
|
1364
|
+
def delete_color_group(group_name: str) -> Dict[str, Any]:
|
|
1365
|
+
"""Delete a color group. Clips in the group become ungrouped.
|
|
1366
|
+
Args:
|
|
1367
|
+
group_name: Name of the color group to delete."""
|
|
1368
|
+
return _post("/color/group/delete", {"groupName": group_name})
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
@mcp.tool()
|
|
1372
|
+
def assign_to_color_group(
|
|
1373
|
+
track_type: str, track_index: int, clip_index: int, group_name: str,
|
|
1374
|
+
) -> Dict[str, Any]:
|
|
1375
|
+
"""Assign a timeline clip to a color group.
|
|
1376
|
+
Args:
|
|
1377
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1378
|
+
track_index: 1-based track index.
|
|
1379
|
+
clip_index: 0-based clip position.
|
|
1380
|
+
group_name: Name of the existing color group."""
|
|
1381
|
+
return _post("/color/group/assign", {
|
|
1382
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1383
|
+
"groupName": group_name,
|
|
1384
|
+
})
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
@mcp.tool()
|
|
1388
|
+
def remove_from_color_group(track_type: str, track_index: int, clip_index: int) -> Dict[str, Any]:
|
|
1389
|
+
"""Remove a clip from its color group.
|
|
1390
|
+
Args:
|
|
1391
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1392
|
+
track_index: 1-based track index.
|
|
1393
|
+
clip_index: 0-based clip position."""
|
|
1394
|
+
return _post("/color/group/remove", {
|
|
1395
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1396
|
+
})
|
|
1397
|
+
|
|
1398
|
+
|
|
1399
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1400
|
+
# FUSION COMPOSITION MANAGEMENT (per-clip)
|
|
1401
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1402
|
+
|
|
1403
|
+
@mcp.tool()
|
|
1404
|
+
def get_fusion_comps(
|
|
1405
|
+
track_type: str = "video", track_index: int = 1, clip_index: int = 0,
|
|
1406
|
+
) -> Dict[str, Any]:
|
|
1407
|
+
"""List Fusion compositions on a timeline clip.
|
|
1408
|
+
Args:
|
|
1409
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1410
|
+
track_index: 1-based track index.
|
|
1411
|
+
clip_index: 0-based clip position.
|
|
1412
|
+
Returns the count and names of Fusion compositions."""
|
|
1413
|
+
return _get("/clip/fusion-comps", {
|
|
1414
|
+
"track_type": track_type, "track_index": str(track_index), "clip_index": str(clip_index),
|
|
1415
|
+
})
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
@mcp.tool()
|
|
1419
|
+
def add_fusion_comp_to_clip(track_type: str, track_index: int, clip_index: int) -> Dict[str, Any]:
|
|
1420
|
+
"""Add a new blank Fusion composition to a timeline clip.
|
|
1421
|
+
Args:
|
|
1422
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1423
|
+
track_index: 1-based track index.
|
|
1424
|
+
clip_index: 0-based clip position."""
|
|
1425
|
+
return _post("/clip/fusion/add", {
|
|
1426
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1427
|
+
})
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
@mcp.tool()
|
|
1431
|
+
def import_fusion_comp_to_clip(
|
|
1432
|
+
track_type: str, track_index: int, clip_index: int, path: str,
|
|
1433
|
+
) -> Dict[str, Any]:
|
|
1434
|
+
"""Import a Fusion composition from file into a timeline clip.
|
|
1435
|
+
Args:
|
|
1436
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1437
|
+
track_index: 1-based track index.
|
|
1438
|
+
clip_index: 0-based clip position.
|
|
1439
|
+
path: Absolute path to the .comp file."""
|
|
1440
|
+
return _post("/clip/fusion/import", {
|
|
1441
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index, "path": path,
|
|
1442
|
+
})
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
@mcp.tool()
|
|
1446
|
+
def export_fusion_comp_from_clip(
|
|
1447
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1448
|
+
path: str, comp_index: int = 1,
|
|
1449
|
+
) -> Dict[str, Any]:
|
|
1450
|
+
"""Export a Fusion composition from a timeline clip to a file.
|
|
1451
|
+
Args:
|
|
1452
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1453
|
+
track_index: 1-based track index.
|
|
1454
|
+
clip_index: 0-based clip position.
|
|
1455
|
+
path: Output file path.
|
|
1456
|
+
comp_index: 1-based composition index (default 1)."""
|
|
1457
|
+
return _post("/clip/fusion/export", {
|
|
1458
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1459
|
+
"path": path, "compIndex": comp_index,
|
|
1460
|
+
})
|
|
1461
|
+
|
|
1462
|
+
|
|
1463
|
+
@mcp.tool()
|
|
1464
|
+
def delete_fusion_comp_on_clip(
|
|
1465
|
+
track_type: str, track_index: int, clip_index: int, comp_name: str,
|
|
1466
|
+
) -> Dict[str, Any]:
|
|
1467
|
+
"""Delete a named Fusion composition from a timeline clip.
|
|
1468
|
+
Args:
|
|
1469
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1470
|
+
track_index: 1-based track index.
|
|
1471
|
+
clip_index: 0-based clip position.
|
|
1472
|
+
comp_name: Name of the Fusion composition to delete."""
|
|
1473
|
+
return _post("/clip/fusion/delete", {
|
|
1474
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1475
|
+
"compName": comp_name,
|
|
1476
|
+
})
|
|
1477
|
+
|
|
1478
|
+
|
|
1479
|
+
@mcp.tool()
|
|
1480
|
+
def load_fusion_comp_on_clip(
|
|
1481
|
+
track_type: str, track_index: int, clip_index: int, comp_name: str,
|
|
1482
|
+
) -> Dict[str, Any]:
|
|
1483
|
+
"""Load a named Fusion composition as the active one on a clip.
|
|
1484
|
+
Args:
|
|
1485
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1486
|
+
track_index: 1-based track index.
|
|
1487
|
+
clip_index: 0-based clip position.
|
|
1488
|
+
comp_name: Name of the Fusion composition to load."""
|
|
1489
|
+
return _post("/clip/fusion/load", {
|
|
1490
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1491
|
+
"compName": comp_name,
|
|
1492
|
+
})
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
@mcp.tool()
|
|
1496
|
+
def rename_fusion_comp_on_clip(
|
|
1497
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1498
|
+
old_name: str, new_name: str,
|
|
1499
|
+
) -> Dict[str, Any]:
|
|
1500
|
+
"""Rename a Fusion composition on a timeline clip.
|
|
1501
|
+
Args:
|
|
1502
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1503
|
+
track_index: 1-based track index.
|
|
1504
|
+
clip_index: 0-based clip position.
|
|
1505
|
+
old_name: Current composition name.
|
|
1506
|
+
new_name: New composition name."""
|
|
1507
|
+
return _post("/clip/fusion/rename", {
|
|
1508
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1509
|
+
"oldName": old_name, "newName": new_name,
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1514
|
+
# SMART FEATURES (Studio)
|
|
1515
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1516
|
+
|
|
1517
|
+
@mcp.tool()
|
|
1518
|
+
def create_magic_mask(
|
|
1519
|
+
track_type: str, track_index: int, clip_index: int, mode: str = "F",
|
|
1520
|
+
) -> Dict[str, Any]:
|
|
1521
|
+
"""[STUDIO ONLY] Create a Magic Mask on a timeline clip using DaVinci Neural Engine.
|
|
1522
|
+
Not available in DaVinci Resolve Free. For background removal on Free, use the local AI tool remove_background_clip instead.
|
|
1523
|
+
Args:
|
|
1524
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1525
|
+
track_index: 1-based track index.
|
|
1526
|
+
clip_index: 0-based clip position.
|
|
1527
|
+
mode: 'F' = forward, 'B' = backward, 'BI' = bidirectional."""
|
|
1528
|
+
return _post("/clip/magic-mask", {
|
|
1529
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index, "mode": mode,
|
|
1530
|
+
})
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
@mcp.tool()
|
|
1534
|
+
def regenerate_magic_mask(track_type: str, track_index: int, clip_index: int) -> Dict[str, Any]:
|
|
1535
|
+
"""[STUDIO ONLY] Regenerate an existing Magic Mask on a clip using DaVinci Neural Engine.
|
|
1536
|
+
Not available in DaVinci Resolve Free.
|
|
1537
|
+
Args:
|
|
1538
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1539
|
+
track_index: 1-based track index.
|
|
1540
|
+
clip_index: 0-based clip position."""
|
|
1541
|
+
return _post("/clip/magic-mask/regenerate", {
|
|
1542
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1545
|
+
|
|
1546
|
+
@mcp.tool()
|
|
1547
|
+
def stabilize_clip(track_type: str, track_index: int, clip_index: int) -> Dict[str, Any]:
|
|
1548
|
+
"""[STUDIO ONLY] Stabilize a timeline clip using DaVinci Neural Engine enhanced stabilization.
|
|
1549
|
+
Not available in DaVinci Resolve Free.
|
|
1550
|
+
Args:
|
|
1551
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1552
|
+
track_index: 1-based track index.
|
|
1553
|
+
clip_index: 0-based clip position."""
|
|
1554
|
+
return _post("/clip/stabilize", {
|
|
1555
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1556
|
+
})
|
|
1557
|
+
|
|
1558
|
+
|
|
1559
|
+
@mcp.tool()
|
|
1560
|
+
def smart_reframe_clip(track_type: str, track_index: int, clip_index: int) -> Dict[str, Any]:
|
|
1561
|
+
"""[STUDIO ONLY] Apply Smart Reframe to a clip using DaVinci Neural Engine for automatic aspect ratio adjustment.
|
|
1562
|
+
Not available in DaVinci Resolve Free.
|
|
1563
|
+
Args:
|
|
1564
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1565
|
+
track_index: 1-based track index.
|
|
1566
|
+
clip_index: 0-based clip position."""
|
|
1567
|
+
return _post("/clip/smart-reframe", {
|
|
1568
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1569
|
+
})
|
|
1570
|
+
|
|
1571
|
+
|
|
1572
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1573
|
+
# AUDIO / FAIRLIGHT
|
|
1574
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1575
|
+
|
|
1576
|
+
@mcp.tool()
|
|
1577
|
+
def get_fairlight_presets() -> Dict[str, Any]:
|
|
1578
|
+
"""List available Fairlight audio presets."""
|
|
1579
|
+
return _get("/fairlight/presets")
|
|
1580
|
+
|
|
1581
|
+
|
|
1582
|
+
@mcp.tool()
|
|
1583
|
+
def apply_fairlight_preset(preset_name: str) -> Dict[str, Any]:
|
|
1584
|
+
"""Apply a Fairlight preset to the current timeline.
|
|
1585
|
+
Args:
|
|
1586
|
+
preset_name: Name of the Fairlight preset (from get_fairlight_presets)."""
|
|
1587
|
+
return _post("/audio/fairlight-preset", {"presetName": preset_name})
|
|
1588
|
+
|
|
1589
|
+
|
|
1590
|
+
@mcp.tool()
|
|
1591
|
+
def insert_audio_at_playhead(
|
|
1592
|
+
media_path: str, start_offset_in_samples: int = 0, duration_in_samples: int = 0,
|
|
1593
|
+
) -> Dict[str, Any]:
|
|
1594
|
+
"""Insert audio at the playhead on a selected track in the Fairlight page.
|
|
1595
|
+
Args:
|
|
1596
|
+
media_path: Absolute path to the audio file.
|
|
1597
|
+
start_offset_in_samples: Start offset in audio samples.
|
|
1598
|
+
duration_in_samples: Duration in audio samples."""
|
|
1599
|
+
return _post("/audio/insert-at-playhead", {
|
|
1600
|
+
"mediaPath": media_path, "startOffsetInSamples": start_offset_in_samples,
|
|
1601
|
+
"durationInSamples": duration_in_samples,
|
|
1602
|
+
})
|
|
1603
|
+
|
|
1604
|
+
|
|
1605
|
+
@mcp.tool()
|
|
1606
|
+
def get_voice_isolation_state(
|
|
1607
|
+
scope: str = "clip", track_type: str = "video", track_index: int = 1, clip_index: int = 0,
|
|
1608
|
+
) -> Dict[str, Any]:
|
|
1609
|
+
"""[STUDIO ONLY] Get Resolve's native voice isolation state for a clip or audio track.
|
|
1610
|
+
Not available in DaVinci Resolve Free. For voice isolation on Free, use the local AI tool voice_isolate instead.
|
|
1611
|
+
Args:
|
|
1612
|
+
scope: 'clip' for a timeline item, 'track' for an audio track.
|
|
1613
|
+
track_type: 'video'/'audio'/'subtitle' (for scope='clip').
|
|
1614
|
+
track_index: 1-based track index.
|
|
1615
|
+
clip_index: 0-based clip position (for scope='clip').
|
|
1616
|
+
Returns {isEnabled, amount} state."""
|
|
1617
|
+
return _get("/audio/voice-isolation", {
|
|
1618
|
+
"scope": scope, "track_type": track_type,
|
|
1619
|
+
"track_index": str(track_index), "clip_index": str(clip_index),
|
|
1620
|
+
})
|
|
1621
|
+
|
|
1622
|
+
|
|
1623
|
+
@mcp.tool()
|
|
1624
|
+
def set_voice_isolation_state(
|
|
1625
|
+
scope: str = "clip", track_type: str = "video", track_index: int = 1, clip_index: int = 0,
|
|
1626
|
+
is_enabled: bool = True, amount: int = 100,
|
|
1627
|
+
) -> Dict[str, Any]:
|
|
1628
|
+
"""[STUDIO ONLY] Set Resolve's native voice isolation state on a clip or audio track.
|
|
1629
|
+
Not available in DaVinci Resolve Free. For voice isolation on Free, use the local AI tool voice_isolate instead.
|
|
1630
|
+
Args:
|
|
1631
|
+
scope: 'clip' or 'track'.
|
|
1632
|
+
track_type: 'video'/'audio'/'subtitle' (for scope='clip').
|
|
1633
|
+
track_index: 1-based track/audio track index.
|
|
1634
|
+
clip_index: 0-based clip position (for scope='clip').
|
|
1635
|
+
is_enabled: Enable/disable voice isolation.
|
|
1636
|
+
amount: Isolation amount 0-100."""
|
|
1637
|
+
body: Dict[str, Any] = {
|
|
1638
|
+
"scope": scope, "trackType": track_type, "trackIndex": track_index,
|
|
1639
|
+
"clipIndex": clip_index, "state": {"isEnabled": is_enabled, "amount": amount},
|
|
1640
|
+
}
|
|
1641
|
+
return _post("/audio/voice-isolation", body)
|
|
1642
|
+
|
|
1643
|
+
|
|
1644
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1645
|
+
# TAKE SELECTOR
|
|
1646
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1647
|
+
|
|
1648
|
+
@mcp.tool()
|
|
1649
|
+
def get_takes(
|
|
1650
|
+
track_type: str = "video", track_index: int = 1, clip_index: int = 0,
|
|
1651
|
+
) -> Dict[str, Any]:
|
|
1652
|
+
"""Get all takes for a clip (take selector).
|
|
1653
|
+
Args:
|
|
1654
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1655
|
+
track_index: 1-based track index.
|
|
1656
|
+
clip_index: 0-based clip position.
|
|
1657
|
+
Returns the count, selected take index, and list of takes with frame ranges."""
|
|
1658
|
+
return _get("/clip/takes", {
|
|
1659
|
+
"track_type": track_type, "track_index": str(track_index), "clip_index": str(clip_index),
|
|
1660
|
+
})
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
@mcp.tool()
|
|
1664
|
+
def add_take(
|
|
1665
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1666
|
+
media_pool_clip_name: str, start_frame: Optional[int] = None, end_frame: Optional[int] = None,
|
|
1667
|
+
) -> Dict[str, Any]:
|
|
1668
|
+
"""Add a media pool clip as a new take to a timeline clip.
|
|
1669
|
+
Args:
|
|
1670
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1671
|
+
track_index: 1-based track index.
|
|
1672
|
+
clip_index: 0-based clip position.
|
|
1673
|
+
media_pool_clip_name: Name of the media pool clip to add as a take.
|
|
1674
|
+
start_frame: Optional source start frame.
|
|
1675
|
+
end_frame: Optional source end frame."""
|
|
1676
|
+
body: Dict[str, Any] = {
|
|
1677
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1678
|
+
"mediaPoolClipName": media_pool_clip_name,
|
|
1679
|
+
}
|
|
1680
|
+
if start_frame is not None:
|
|
1681
|
+
body["startFrame"] = start_frame
|
|
1682
|
+
if end_frame is not None:
|
|
1683
|
+
body["endFrame"] = end_frame
|
|
1684
|
+
return _post("/clip/take/add", body)
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
@mcp.tool()
|
|
1688
|
+
def select_take(track_type: str, track_index: int, clip_index: int, take_index: int) -> Dict[str, Any]:
|
|
1689
|
+
"""Select a take by index.
|
|
1690
|
+
Args:
|
|
1691
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1692
|
+
track_index: 1-based track index.
|
|
1693
|
+
clip_index: 0-based clip position.
|
|
1694
|
+
take_index: 1-based take index."""
|
|
1695
|
+
return _post("/clip/take/select", {
|
|
1696
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1697
|
+
"takeIndex": take_index,
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
|
|
1701
|
+
@mcp.tool()
|
|
1702
|
+
def delete_take(track_type: str, track_index: int, clip_index: int, take_index: int) -> Dict[str, Any]:
|
|
1703
|
+
"""Delete a take by index.
|
|
1704
|
+
Args:
|
|
1705
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1706
|
+
track_index: 1-based track index.
|
|
1707
|
+
clip_index: 0-based clip position.
|
|
1708
|
+
take_index: 1-based take index to delete."""
|
|
1709
|
+
return _post("/clip/take/delete", {
|
|
1710
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1711
|
+
"takeIndex": take_index,
|
|
1712
|
+
})
|
|
1713
|
+
|
|
1714
|
+
|
|
1715
|
+
@mcp.tool()
|
|
1716
|
+
def finalize_take(track_type: str, track_index: int, clip_index: int) -> Dict[str, Any]:
|
|
1717
|
+
"""Finalize the take selection on a clip, committing the chosen take.
|
|
1718
|
+
Args:
|
|
1719
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1720
|
+
track_index: 1-based track index.
|
|
1721
|
+
clip_index: 0-based clip position."""
|
|
1722
|
+
return _post("/clip/take/finalize", {
|
|
1723
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1724
|
+
})
|
|
1725
|
+
|
|
1726
|
+
|
|
1727
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1728
|
+
# PROXY / CACHE / CLIP UTILITIES
|
|
1729
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1730
|
+
|
|
1731
|
+
@mcp.tool()
|
|
1732
|
+
def link_proxy_media(clip_name: str, proxy_media_file_path: str) -> Dict[str, Any]:
|
|
1733
|
+
"""Link a proxy media file to a media pool clip.
|
|
1734
|
+
Args:
|
|
1735
|
+
clip_name: Name of the clip in the media pool.
|
|
1736
|
+
proxy_media_file_path: Absolute path to the proxy media file."""
|
|
1737
|
+
return _post("/mediapool/proxy/link", {"clipName": clip_name, "proxyMediaFilePath": proxy_media_file_path})
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
@mcp.tool()
|
|
1741
|
+
def unlink_proxy_media(clip_name: str) -> Dict[str, Any]:
|
|
1742
|
+
"""Unlink proxy media from a media pool clip.
|
|
1743
|
+
Args:
|
|
1744
|
+
clip_name: Name of the clip in the media pool."""
|
|
1745
|
+
return _post("/mediapool/proxy/unlink", {"clipName": clip_name})
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
@mcp.tool()
|
|
1749
|
+
def replace_clip(clip_name: str, file_path: str) -> Dict[str, Any]:
|
|
1750
|
+
"""Replace a media pool clip's underlying source file.
|
|
1751
|
+
Args:
|
|
1752
|
+
clip_name: Name of the clip in the media pool.
|
|
1753
|
+
file_path: Absolute path to the new source media file."""
|
|
1754
|
+
return _post("/mediapool/clip/replace", {"clipName": clip_name, "filePath": file_path})
|
|
1755
|
+
|
|
1756
|
+
|
|
1757
|
+
@mcp.tool()
|
|
1758
|
+
def set_clip_cache(
|
|
1759
|
+
track_type: str, track_index: int, clip_index: int,
|
|
1760
|
+
cache_type: str = "color", cache_value: int = 1,
|
|
1761
|
+
) -> Dict[str, Any]:
|
|
1762
|
+
"""Set render cache mode for a timeline clip.
|
|
1763
|
+
Args:
|
|
1764
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1765
|
+
track_index: 1-based track index.
|
|
1766
|
+
clip_index: 0-based clip position.
|
|
1767
|
+
cache_type: 'color' or 'fusion'.
|
|
1768
|
+
cache_value: For color: 0=disabled, 1=enabled. For fusion: -1=auto, 0=disabled, 1=enabled."""
|
|
1769
|
+
return _post("/clip/cache", {
|
|
1770
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1771
|
+
"cacheType": cache_type, "cacheValue": cache_value,
|
|
1772
|
+
})
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
@mcp.tool()
|
|
1776
|
+
def update_sidecar(track_type: str, track_index: int, clip_index: int) -> Dict[str, Any]:
|
|
1777
|
+
"""Update sidecar file for BRAW clips or RMD file for R3D clips.
|
|
1778
|
+
Args:
|
|
1779
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1780
|
+
track_index: 1-based track index.
|
|
1781
|
+
clip_index: 0-based clip position."""
|
|
1782
|
+
return _post("/clip/sidecar", {
|
|
1783
|
+
"trackType": track_type, "trackIndex": track_index, "clipIndex": clip_index,
|
|
1784
|
+
})
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
@mcp.tool()
|
|
1788
|
+
def get_linked_items(
|
|
1789
|
+
track_type: str = "video", track_index: int = 1, clip_index: int = 0,
|
|
1790
|
+
) -> Dict[str, Any]:
|
|
1791
|
+
"""Get items linked to a timeline clip (e.g. audio linked to video).
|
|
1792
|
+
Args:
|
|
1793
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
1794
|
+
track_index: 1-based track index.
|
|
1795
|
+
clip_index: 0-based clip position.
|
|
1796
|
+
Returns the clip name and list of linked items with their names and track positions."""
|
|
1797
|
+
return _get("/clip/linked-items", {
|
|
1798
|
+
"track_type": track_type, "track_index": str(track_index), "clip_index": str(clip_index),
|
|
1799
|
+
})
|
|
1800
|
+
|
|
1801
|
+
|
|
1802
|
+
@mcp.tool()
|
|
1803
|
+
def set_timeline_mark_in_out(mark_in: int, mark_out: int, type: str = "all") -> Dict[str, Any]:
|
|
1804
|
+
"""Set mark in/out points on the current timeline.
|
|
1805
|
+
Args:
|
|
1806
|
+
mark_in: Frame number for the in point.
|
|
1807
|
+
mark_out: Frame number for the out point.
|
|
1808
|
+
type: 'video', 'audio', or 'all' (default)."""
|
|
1809
|
+
return _post("/timeline/mark-in-out", {"markIn": mark_in, "markOut": mark_out, "type": type})
|
|
1810
|
+
|
|
1811
|
+
|
|
1812
|
+
@mcp.tool()
|
|
1813
|
+
def clear_timeline_mark_in_out(type: str = "all") -> Dict[str, Any]:
|
|
1814
|
+
"""Clear mark in/out points on the current timeline.
|
|
1815
|
+
Args:
|
|
1816
|
+
type: 'video', 'audio', or 'all' (default)."""
|
|
1817
|
+
return _post("/timeline/clear-mark-in-out", {"type": type})
|
|
1818
|
+
|
|
1819
|
+
|
|
1820
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1821
|
+
# PROJECT MANAGER
|
|
1822
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1823
|
+
|
|
1824
|
+
@mcp.tool()
|
|
1825
|
+
def get_project_list() -> Dict[str, Any]:
|
|
1826
|
+
"""List projects and folders in the current database folder.
|
|
1827
|
+
Returns the current folder name, current database info, project names, and subfolder names."""
|
|
1828
|
+
return _get("/projects")
|
|
1829
|
+
|
|
1830
|
+
|
|
1831
|
+
@mcp.tool()
|
|
1832
|
+
def get_database_list() -> Dict[str, Any]:
|
|
1833
|
+
"""List all databases configured in Resolve (Disk and PostgreSQL).
|
|
1834
|
+
Returns the current database and list of all databases."""
|
|
1835
|
+
return _get("/databases")
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
@mcp.tool()
|
|
1839
|
+
def load_project(project_name: str) -> Dict[str, Any]:
|
|
1840
|
+
"""Load/open a project by name.
|
|
1841
|
+
Args:
|
|
1842
|
+
project_name: Name of the project to open."""
|
|
1843
|
+
return _post("/projects/load", {"projectName": project_name})
|
|
1844
|
+
|
|
1845
|
+
|
|
1846
|
+
@mcp.tool()
|
|
1847
|
+
def create_project(project_name: str) -> Dict[str, Any]:
|
|
1848
|
+
"""Create a new project.
|
|
1849
|
+
Args:
|
|
1850
|
+
project_name: Name for the new project (must be unique)."""
|
|
1851
|
+
return _post("/projects/create", {"projectName": project_name})
|
|
1852
|
+
|
|
1853
|
+
|
|
1854
|
+
@mcp.tool()
|
|
1855
|
+
def delete_project(project_name: str) -> Dict[str, Any]:
|
|
1856
|
+
"""Delete a project (cannot be the currently loaded project).
|
|
1857
|
+
Args:
|
|
1858
|
+
project_name: Name of the project to delete."""
|
|
1859
|
+
return _post("/projects/delete", {"projectName": project_name})
|
|
1860
|
+
|
|
1861
|
+
|
|
1862
|
+
@mcp.tool()
|
|
1863
|
+
def archive_project(
|
|
1864
|
+
project_name: str, file_path: str,
|
|
1865
|
+
archive_src_media: bool = True, archive_render_cache: bool = True,
|
|
1866
|
+
archive_proxy_media: bool = False,
|
|
1867
|
+
) -> Dict[str, Any]:
|
|
1868
|
+
"""Archive a project to a file.
|
|
1869
|
+
Args:
|
|
1870
|
+
project_name: Name of the project to archive.
|
|
1871
|
+
file_path: Output archive file path.
|
|
1872
|
+
archive_src_media: Include source media (default True).
|
|
1873
|
+
archive_render_cache: Include render cache (default True).
|
|
1874
|
+
archive_proxy_media: Include proxy media (default False)."""
|
|
1875
|
+
return _post("/projects/archive", {
|
|
1876
|
+
"projectName": project_name, "filePath": file_path,
|
|
1877
|
+
"archiveSrcMedia": archive_src_media, "archiveRenderCache": archive_render_cache,
|
|
1878
|
+
"archiveProxyMedia": archive_proxy_media,
|
|
1879
|
+
})
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
@mcp.tool()
|
|
1883
|
+
def export_project(
|
|
1884
|
+
project_name: str, file_path: str, with_stills_and_luts: bool = True,
|
|
1885
|
+
) -> Dict[str, Any]:
|
|
1886
|
+
"""Export a project to a .drp file.
|
|
1887
|
+
Args:
|
|
1888
|
+
project_name: Name of the project to export.
|
|
1889
|
+
file_path: Output file path.
|
|
1890
|
+
with_stills_and_luts: Include stills and LUTs (default True)."""
|
|
1891
|
+
return _post("/projects/export", {
|
|
1892
|
+
"projectName": project_name, "filePath": file_path, "withStillsAndLUTs": with_stills_and_luts,
|
|
1893
|
+
})
|
|
1894
|
+
|
|
1895
|
+
|
|
1896
|
+
@mcp.tool()
|
|
1897
|
+
def import_project(file_path: str, project_name: str = "") -> Dict[str, Any]:
|
|
1898
|
+
"""Import a project from a .drp file.
|
|
1899
|
+
Args:
|
|
1900
|
+
file_path: Path to the .drp file.
|
|
1901
|
+
project_name: Optional name for the imported project."""
|
|
1902
|
+
body: Dict[str, Any] = {"filePath": file_path}
|
|
1903
|
+
if project_name:
|
|
1904
|
+
body["projectName"] = project_name
|
|
1905
|
+
return _post("/projects/import", body)
|
|
1906
|
+
|
|
1907
|
+
|
|
1908
|
+
@mcp.tool()
|
|
1909
|
+
def navigate_project_folder(action: str, folder_name: str = "") -> Dict[str, Any]:
|
|
1910
|
+
"""Navigate the project folder hierarchy.
|
|
1911
|
+
Args:
|
|
1912
|
+
action: 'root' (go to root), 'parent' (go up), 'open' (enter folder),
|
|
1913
|
+
'create' (create folder), 'delete' (delete folder).
|
|
1914
|
+
folder_name: Required for 'open', 'create', and 'delete' actions."""
|
|
1915
|
+
return _post("/projects/folder", {"action": action, "folderName": folder_name})
|
|
1916
|
+
|
|
1917
|
+
|
|
1918
|
+
@mcp.tool()
|
|
1919
|
+
def set_database(db_type: str, db_name: str, ip_address: str = "127.0.0.1") -> Dict[str, Any]:
|
|
1920
|
+
"""Switch to a different database.
|
|
1921
|
+
Args:
|
|
1922
|
+
db_type: 'Disk' or 'PostgreSQL'.
|
|
1923
|
+
db_name: Database name.
|
|
1924
|
+
ip_address: PostgreSQL server IP (default '127.0.0.1', ignored for Disk)."""
|
|
1925
|
+
db_info: Dict[str, str] = {"DbType": db_type, "DbName": db_name}
|
|
1926
|
+
if db_type == "PostgreSQL":
|
|
1927
|
+
db_info["IpAddress"] = ip_address
|
|
1928
|
+
return _post("/projects/database", {"dbInfo": db_info})
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1932
|
+
# RESOLVE-LEVEL / PRESETS / RENDER MONITORING
|
|
1933
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
1934
|
+
|
|
1935
|
+
@mcp.tool()
|
|
1936
|
+
def layout_preset(action: str, preset_name: str = "", preset_file_path: str = "") -> Dict[str, Any]:
|
|
1937
|
+
"""Manage UI layout presets.
|
|
1938
|
+
Args:
|
|
1939
|
+
action: 'load', 'save', 'update', 'delete', 'export', or 'import'.
|
|
1940
|
+
preset_name: Preset name (required for all except 'import').
|
|
1941
|
+
preset_file_path: File path (required for 'export' and 'import')."""
|
|
1942
|
+
return _post("/resolve/layout-preset", {
|
|
1943
|
+
"action": action, "presetName": preset_name, "presetFilePath": preset_file_path,
|
|
1944
|
+
})
|
|
1945
|
+
|
|
1946
|
+
|
|
1947
|
+
@mcp.tool()
|
|
1948
|
+
def render_preset(
|
|
1949
|
+
action: str, preset_name: str = "", preset_path: str = "",
|
|
1950
|
+
) -> Dict[str, Any]:
|
|
1951
|
+
"""Manage render presets.
|
|
1952
|
+
Args:
|
|
1953
|
+
action: 'load', 'saveAs', 'delete', 'list', 'import', or 'export'.
|
|
1954
|
+
preset_name: Preset name (for load/saveAs/delete/export).
|
|
1955
|
+
preset_path: File path (for import/export)."""
|
|
1956
|
+
return _post("/render/preset", {"action": action, "presetName": preset_name, "presetPath": preset_path})
|
|
1957
|
+
|
|
1958
|
+
|
|
1959
|
+
@mcp.tool()
|
|
1960
|
+
def burnin_preset(action: str, preset_name: str = "", preset_path: str = "") -> Dict[str, Any]:
|
|
1961
|
+
"""Manage data burn-in presets.
|
|
1962
|
+
Args:
|
|
1963
|
+
action: 'load', 'import', or 'export'.
|
|
1964
|
+
preset_name: Preset name (for load/export).
|
|
1965
|
+
preset_path: File path (for import/export)."""
|
|
1966
|
+
return _post("/resolve/burnin-preset", {
|
|
1967
|
+
"action": action, "presetName": preset_name, "presetPath": preset_path,
|
|
1968
|
+
})
|
|
1969
|
+
|
|
1970
|
+
|
|
1971
|
+
@mcp.tool()
|
|
1972
|
+
def get_keyframe_mode() -> Dict[str, Any]:
|
|
1973
|
+
"""Get the current keyframe mode (All, Color, or Sizing)."""
|
|
1974
|
+
return _get("/keyframe-mode")
|
|
1975
|
+
|
|
1976
|
+
|
|
1977
|
+
@mcp.tool()
|
|
1978
|
+
def set_keyframe_mode(mode: int) -> Dict[str, Any]:
|
|
1979
|
+
"""Set the keyframe mode.
|
|
1980
|
+
Args:
|
|
1981
|
+
mode: 0 = All, 1 = Color, 2 = Sizing."""
|
|
1982
|
+
return _post("/resolve/keyframe-mode", {"mode": mode})
|
|
1983
|
+
|
|
1984
|
+
|
|
1985
|
+
@mcp.tool()
|
|
1986
|
+
def get_render_job_status(job_id: str) -> Dict[str, Any]:
|
|
1987
|
+
"""Get the status and progress of a specific render job.
|
|
1988
|
+
Args:
|
|
1989
|
+
job_id: Job ID string (from add_render_job)."""
|
|
1990
|
+
return _post("/render/job/status", {"jobId": job_id})
|
|
1991
|
+
|
|
1992
|
+
|
|
1993
|
+
@mcp.tool()
|
|
1994
|
+
def get_render_resolutions(format: str = "", codec: str = "") -> Dict[str, Any]:
|
|
1995
|
+
"""Get available render resolutions, optionally filtered by format and codec.
|
|
1996
|
+
Args:
|
|
1997
|
+
format: Render format (optional).
|
|
1998
|
+
codec: Render codec (optional)."""
|
|
1999
|
+
params: Dict[str, str] = {}
|
|
2000
|
+
if format:
|
|
2001
|
+
params["format"] = format
|
|
2002
|
+
if codec:
|
|
2003
|
+
params["codec"] = codec
|
|
2004
|
+
return _get("/render/resolutions", params)
|
|
2005
|
+
|
|
2006
|
+
|
|
2007
|
+
@mcp.tool()
|
|
2008
|
+
def get_quick_export_presets() -> Dict[str, Any]:
|
|
2009
|
+
"""List available Quick Export render presets (YouTube, Vimeo, etc.)."""
|
|
2010
|
+
return _get("/render/quick-export-presets")
|
|
2011
|
+
|
|
2012
|
+
|
|
2013
|
+
@mcp.tool()
|
|
2014
|
+
def quick_export(preset_name: str, params: Dict[str, Any] = {}) -> Dict[str, Any]:
|
|
2015
|
+
"""Quick Export the current timeline using a preset.
|
|
2016
|
+
Args:
|
|
2017
|
+
preset_name: Preset name (from get_quick_export_presets).
|
|
2018
|
+
params: Optional dict with 'TargetDir', 'CustomName', 'VideoQuality', 'EnableUpload' keys."""
|
|
2019
|
+
return _post("/render/quick-export", {"presetName": preset_name, "params": params})
|
|
2020
|
+
|
|
2021
|
+
|
|
2022
|
+
@mcp.tool()
|
|
2023
|
+
def set_render_mode(render_mode: int) -> Dict[str, Any]:
|
|
2024
|
+
"""Set the render mode.
|
|
2025
|
+
Args:
|
|
2026
|
+
render_mode: 0 = Individual clips, 1 = Single clip."""
|
|
2027
|
+
return _post("/render/mode", {"renderMode": render_mode})
|
|
2028
|
+
|
|
2029
|
+
|
|
2030
|
+
@mcp.tool()
|
|
2031
|
+
def refresh_lut_list() -> Dict[str, Any]:
|
|
2032
|
+
"""Refresh the LUT list so Resolve discovers newly added LUT files."""
|
|
2033
|
+
return _post("/render/refresh-luts", {})
|
|
2034
|
+
|
|
2035
|
+
|
|
2036
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
2037
|
+
# MEDIA STORAGE
|
|
2038
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
2039
|
+
|
|
2040
|
+
@mcp.tool()
|
|
2041
|
+
def get_media_storage(folder_path: str = "") -> Dict[str, Any]:
|
|
2042
|
+
"""Browse Resolve's Media Storage.
|
|
2043
|
+
Args:
|
|
2044
|
+
folder_path: Absolute path to browse. Empty = just list mounted volumes.
|
|
2045
|
+
Returns mounted volumes, and if folder_path is given, its subfolders and files."""
|
|
2046
|
+
params: Dict[str, str] = {}
|
|
2047
|
+
if folder_path:
|
|
2048
|
+
params["folder_path"] = folder_path
|
|
2049
|
+
return _get("/media-storage", params)
|
|
2050
|
+
|
|
2051
|
+
|
|
2052
|
+
@mcp.tool()
|
|
2053
|
+
def reveal_in_storage(path: str) -> Dict[str, Any]:
|
|
2054
|
+
"""Expand and reveal a file or folder in Resolve's Media Storage panel.
|
|
2055
|
+
Args:
|
|
2056
|
+
path: Absolute path to reveal."""
|
|
2057
|
+
return _post("/media-storage/reveal", {"path": path})
|
|
2058
|
+
|
|
2059
|
+
|
|
2060
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
2061
|
+
# AI: VOICE ISOLATION (local Demucs — replaces Studio Voice Isolation)
|
|
2062
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
2063
|
+
|
|
2064
|
+
_demucs_model = None
|
|
2065
|
+
_demucs_model_name = None
|
|
2066
|
+
|
|
2067
|
+
|
|
2068
|
+
def _load_demucs(model_name: str = "htdemucs"):
|
|
2069
|
+
global _demucs_model, _demucs_model_name
|
|
2070
|
+
if _demucs_model and _demucs_model_name == model_name:
|
|
2071
|
+
return _demucs_model
|
|
2072
|
+
try:
|
|
2073
|
+
from demucs.pretrained import get_model
|
|
2074
|
+
except ImportError:
|
|
2075
|
+
return None
|
|
2076
|
+
logger.info("Loading Demucs model '%s' (first load downloads ~80MB)...", model_name)
|
|
2077
|
+
_demucs_model = get_model(model_name)
|
|
2078
|
+
_demucs_model.eval()
|
|
2079
|
+
_demucs_model_name = model_name
|
|
2080
|
+
logger.info("Demucs model '%s' loaded.", model_name)
|
|
2081
|
+
return _demucs_model
|
|
2082
|
+
|
|
2083
|
+
|
|
2084
|
+
def _run_voice_isolation(
|
|
2085
|
+
file_path: str,
|
|
2086
|
+
model_name: str = "htdemucs",
|
|
2087
|
+
two_stems: str = "vocals",
|
|
2088
|
+
output_dir: str = "",
|
|
2089
|
+
) -> Dict[str, Any]:
|
|
2090
|
+
try:
|
|
2091
|
+
import torch
|
|
2092
|
+
import numpy as np
|
|
2093
|
+
import soundfile as sf
|
|
2094
|
+
from demucs.apply import apply_model
|
|
2095
|
+
except ImportError as e:
|
|
2096
|
+
return {"error": f"Missing dependency: {e}. Run: pip install demucs soundfile"}
|
|
2097
|
+
|
|
2098
|
+
if not os.path.isfile(file_path):
|
|
2099
|
+
return {"error": f"File not found: {file_path}"}
|
|
2100
|
+
|
|
2101
|
+
model = _load_demucs(model_name)
|
|
2102
|
+
if model is None:
|
|
2103
|
+
return {"error": "demucs is not installed. Run: pip install demucs"}
|
|
2104
|
+
|
|
2105
|
+
if not output_dir:
|
|
2106
|
+
output_dir = os.path.join(os.path.dirname(file_path), "davinci-mcp-output", "voice-isolation")
|
|
2107
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
2108
|
+
|
|
2109
|
+
logger.info("Voice isolation starting: %s (model=%s, stems=%s)", file_path, model_name, two_stems)
|
|
2110
|
+
|
|
2111
|
+
try:
|
|
2112
|
+
wav_np, sr = sf.read(file_path, dtype="float32")
|
|
2113
|
+
if wav_np.ndim == 1:
|
|
2114
|
+
wav_np = np.stack([wav_np, wav_np], axis=-1)
|
|
2115
|
+
|
|
2116
|
+
wav_tensor = torch.from_numpy(wav_np.T).float()
|
|
2117
|
+
|
|
2118
|
+
target_sr = model.samplerate
|
|
2119
|
+
if sr != target_sr:
|
|
2120
|
+
import torchaudio.functional as F
|
|
2121
|
+
wav_tensor = F.resample(wav_tensor, sr, target_sr)
|
|
2122
|
+
|
|
2123
|
+
ref = wav_tensor.mean(0)
|
|
2124
|
+
wav_tensor = (wav_tensor - ref.mean()) / ref.std()
|
|
2125
|
+
sources = apply_model(model, wav_tensor[None], device="cpu")[0]
|
|
2126
|
+
sources = sources * ref.std() + ref.mean()
|
|
2127
|
+
|
|
2128
|
+
stem_idx = model.sources.index(two_stems) if two_stems in model.sources else 0
|
|
2129
|
+
stem_audio = sources[stem_idx].detach().cpu().numpy()
|
|
2130
|
+
|
|
2131
|
+
other_indices = [i for i in range(len(model.sources)) if i != stem_idx]
|
|
2132
|
+
nostem_audio = sources[other_indices].sum(0).detach().cpu().numpy()
|
|
2133
|
+
|
|
2134
|
+
basename = os.path.splitext(os.path.basename(file_path))[0]
|
|
2135
|
+
stem_dir = os.path.join(output_dir, basename)
|
|
2136
|
+
os.makedirs(stem_dir, exist_ok=True)
|
|
2137
|
+
|
|
2138
|
+
stem_path = os.path.join(stem_dir, f"{two_stems}.wav")
|
|
2139
|
+
nostem_path = os.path.join(stem_dir, f"no_{two_stems}.wav")
|
|
2140
|
+
|
|
2141
|
+
sf.write(stem_path, stem_audio.T, target_sr)
|
|
2142
|
+
sf.write(nostem_path, nostem_audio.T, target_sr)
|
|
2143
|
+
|
|
2144
|
+
logger.info("Voice isolation complete: %s, %s", stem_path, nostem_path)
|
|
2145
|
+
return {
|
|
2146
|
+
"success": True,
|
|
2147
|
+
"model": model_name,
|
|
2148
|
+
"stems": {two_stems: stem_path, f"no_{two_stems}": nostem_path},
|
|
2149
|
+
"output_dir": stem_dir,
|
|
2150
|
+
}
|
|
2151
|
+
except Exception as e:
|
|
2152
|
+
logger.exception("Voice isolation failed")
|
|
2153
|
+
return {"error": f"Demucs separation failed: {e}"}
|
|
2154
|
+
|
|
2155
|
+
|
|
2156
|
+
@mcp.tool()
|
|
2157
|
+
def voice_isolate(
|
|
2158
|
+
file_path: str,
|
|
2159
|
+
model: str = "htdemucs",
|
|
2160
|
+
stems: str = "vocals",
|
|
2161
|
+
output_dir: str = "",
|
|
2162
|
+
) -> Dict[str, Any]:
|
|
2163
|
+
"""[FREE + STUDIO · LOCAL AI] Separate vocals from background audio using Demucs (open-source replacement for Studio Voice Isolation).
|
|
2164
|
+
Works on DaVinci Resolve Free — no Studio license needed. Downloads the model on first use (~150MB). Runs on CPU (~1.5x real-time).
|
|
2165
|
+
Args:
|
|
2166
|
+
file_path: Absolute path to audio/video file.
|
|
2167
|
+
model: Demucs model — 'htdemucs' (default, best quality), 'htdemucs_ft' (slower, slightly better),
|
|
2168
|
+
'mdx_extra' (good alternative). First run downloads the model.
|
|
2169
|
+
stems: Which stem to isolate — 'vocals' (default, outputs vocals + no_vocals),
|
|
2170
|
+
'drums', or 'bass'.
|
|
2171
|
+
output_dir: Optional output directory. Defaults to a folder next to the source file.
|
|
2172
|
+
Returns paths to the separated audio stems (e.g. vocals.wav, no_vocals.wav)."""
|
|
2173
|
+
return _run_voice_isolation(file_path, model, stems, output_dir)
|
|
2174
|
+
|
|
2175
|
+
|
|
2176
|
+
@mcp.tool()
|
|
2177
|
+
def voice_isolate_timeline(
|
|
2178
|
+
model: str = "htdemucs",
|
|
2179
|
+
stems: str = "vocals",
|
|
2180
|
+
output_dir: str = "",
|
|
2181
|
+
) -> Dict[str, Any]:
|
|
2182
|
+
"""[FREE + STUDIO · LOCAL AI] Isolate vocals from the current timeline's audio track using Demucs.
|
|
2183
|
+
Works on DaVinci Resolve Free — no Studio license needed. Automatically detects the audio file from audio track 1.
|
|
2184
|
+
Args:
|
|
2185
|
+
model: Demucs model — 'htdemucs' (default), 'htdemucs_ft', 'mdx_extra'.
|
|
2186
|
+
stems: Which stem to isolate — 'vocals' (default), 'drums', 'bass'.
|
|
2187
|
+
output_dir: Optional output directory.
|
|
2188
|
+
Returns paths to the separated audio stems."""
|
|
2189
|
+
clips = _get("/timeline/clips", {"track_type": "audio", "track_index": "1"})
|
|
2190
|
+
if "error" in clips:
|
|
2191
|
+
return clips
|
|
2192
|
+
clip_list = clips.get("clips", [])
|
|
2193
|
+
if not clip_list:
|
|
2194
|
+
return {"error": "No audio clips found on audio track 1"}
|
|
2195
|
+
file_path = clip_list[0].get("File Path", "")
|
|
2196
|
+
if not file_path:
|
|
2197
|
+
return {"error": "Could not determine audio file path from the timeline clip"}
|
|
2198
|
+
return _run_voice_isolation(file_path, model, stems, output_dir)
|
|
2199
|
+
|
|
2200
|
+
|
|
2201
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
2202
|
+
# AI: BACKGROUND REMOVAL (local rembg — replaces Studio Magic Mask)
|
|
2203
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
2204
|
+
|
|
2205
|
+
_rembg_session = None
|
|
2206
|
+
_rembg_model_name = None
|
|
2207
|
+
|
|
2208
|
+
|
|
2209
|
+
def _load_rembg(model_name: str = "birefnet-general"):
|
|
2210
|
+
global _rembg_session, _rembg_model_name
|
|
2211
|
+
if _rembg_session and _rembg_model_name == model_name:
|
|
2212
|
+
return _rembg_session
|
|
2213
|
+
try:
|
|
2214
|
+
from rembg import new_session
|
|
2215
|
+
except ImportError:
|
|
2216
|
+
return None
|
|
2217
|
+
logger.info("Loading rembg model '%s' (first load downloads the model)...", model_name)
|
|
2218
|
+
_rembg_session = new_session(model_name)
|
|
2219
|
+
_rembg_model_name = model_name
|
|
2220
|
+
logger.info("rembg model '%s' loaded.", model_name)
|
|
2221
|
+
return _rembg_session
|
|
2222
|
+
|
|
2223
|
+
|
|
2224
|
+
def _remove_bg_single(input_path: str, output_path: str, model_name: str, alpha_matte: bool) -> bool:
|
|
2225
|
+
session = _load_rembg(model_name)
|
|
2226
|
+
if session is None:
|
|
2227
|
+
return False
|
|
2228
|
+
from rembg import remove
|
|
2229
|
+
from PIL import Image
|
|
2230
|
+
|
|
2231
|
+
img = Image.open(input_path)
|
|
2232
|
+
result = remove(img, session=session, alpha_matting=alpha_matte)
|
|
2233
|
+
result.save(output_path)
|
|
2234
|
+
return True
|
|
2235
|
+
|
|
2236
|
+
|
|
2237
|
+
@mcp.tool()
|
|
2238
|
+
def remove_background(
|
|
2239
|
+
file_path: str,
|
|
2240
|
+
model: str = "birefnet-general",
|
|
2241
|
+
output_path: str = "",
|
|
2242
|
+
alpha_matting: bool = False,
|
|
2243
|
+
) -> Dict[str, Any]:
|
|
2244
|
+
"""[FREE + STUDIO · LOCAL AI] Remove background from a single image (open-source replacement for Studio Magic Mask).
|
|
2245
|
+
Works on DaVinci Resolve Free — no Studio license needed. Downloads the model on first use (~170MB). Runs on CPU.
|
|
2246
|
+
Args:
|
|
2247
|
+
file_path: Absolute path to the image file.
|
|
2248
|
+
model: Segmentation model — 'birefnet-general' (default, best quality),
|
|
2249
|
+
'birefnet-general-lite' (faster), 'u2net' (classic), 'u2net_human_seg' (people only),
|
|
2250
|
+
'isnet-general-use', 'silueta' (smallest).
|
|
2251
|
+
output_path: Where to save the result. Defaults to <input>_nobg.png.
|
|
2252
|
+
alpha_matting: If True, applies alpha matting for smoother edges (slower).
|
|
2253
|
+
Returns the path to the output image with transparent background."""
|
|
2254
|
+
try:
|
|
2255
|
+
from rembg import remove
|
|
2256
|
+
from PIL import Image
|
|
2257
|
+
except ImportError:
|
|
2258
|
+
return {"error": "rembg is not installed. Run: pip install 'rembg[cpu]'"}
|
|
2259
|
+
|
|
2260
|
+
if not os.path.isfile(file_path):
|
|
2261
|
+
return {"error": f"File not found: {file_path}"}
|
|
2262
|
+
|
|
2263
|
+
if not output_path:
|
|
2264
|
+
base, _ = os.path.splitext(file_path)
|
|
2265
|
+
output_path = f"{base}_nobg.png"
|
|
2266
|
+
|
|
2267
|
+
session = _load_rembg(model)
|
|
2268
|
+
if session is None:
|
|
2269
|
+
return {"error": "Failed to load rembg model"}
|
|
2270
|
+
|
|
2271
|
+
logger.info("Removing background: %s", file_path)
|
|
2272
|
+
try:
|
|
2273
|
+
img = Image.open(file_path)
|
|
2274
|
+
result = remove(img, session=session, alpha_matting=alpha_matting)
|
|
2275
|
+
result.save(output_path)
|
|
2276
|
+
logger.info("Background removed: %s", output_path)
|
|
2277
|
+
return {"success": True, "output_path": output_path}
|
|
2278
|
+
except Exception as e:
|
|
2279
|
+
return {"error": f"Background removal failed: {e}"}
|
|
2280
|
+
|
|
2281
|
+
|
|
2282
|
+
@mcp.tool()
|
|
2283
|
+
def remove_background_video(
|
|
2284
|
+
file_path: str,
|
|
2285
|
+
model: str = "birefnet-general",
|
|
2286
|
+
output_dir: str = "",
|
|
2287
|
+
output_format: str = "png_sequence",
|
|
2288
|
+
alpha_matting: bool = False,
|
|
2289
|
+
) -> Dict[str, Any]:
|
|
2290
|
+
"""[FREE + STUDIO · LOCAL AI] Remove background from every frame of a video file (open-source replacement for Studio Magic Mask).
|
|
2291
|
+
Works on DaVinci Resolve Free — no Studio license needed. Extracts frames with ffmpeg, processes each with AI, reassembles the result.
|
|
2292
|
+
Args:
|
|
2293
|
+
file_path: Absolute path to the video file.
|
|
2294
|
+
model: Segmentation model — 'birefnet-general' (default), 'birefnet-general-lite' (faster),
|
|
2295
|
+
'u2net', 'u2net_human_seg'.
|
|
2296
|
+
output_dir: Where to save output frames. Defaults to a folder next to the source file.
|
|
2297
|
+
output_format: 'png_sequence' (default, PNG frames with alpha) or 'matte_video'
|
|
2298
|
+
(grayscale matte as MP4, white=foreground).
|
|
2299
|
+
alpha_matting: If True, applies alpha matting per frame (slower, smoother edges).
|
|
2300
|
+
Returns the output directory path and frame count. Processing is ~0.5-2s per frame on CPU."""
|
|
2301
|
+
try:
|
|
2302
|
+
from rembg import remove
|
|
2303
|
+
from PIL import Image
|
|
2304
|
+
except ImportError:
|
|
2305
|
+
return {"error": "rembg is not installed. Run: pip install 'rembg[cpu]'"}
|
|
2306
|
+
|
|
2307
|
+
if not os.path.isfile(file_path):
|
|
2308
|
+
return {"error": f"File not found: {file_path}"}
|
|
2309
|
+
|
|
2310
|
+
basename = os.path.splitext(os.path.basename(file_path))[0]
|
|
2311
|
+
if not output_dir:
|
|
2312
|
+
output_dir = os.path.join(os.path.dirname(file_path), "davinci-mcp-output", "background-removal", basename)
|
|
2313
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
2314
|
+
|
|
2315
|
+
frames_dir = os.path.join(output_dir, "_frames")
|
|
2316
|
+
os.makedirs(frames_dir, exist_ok=True)
|
|
2317
|
+
|
|
2318
|
+
ffmpeg = FFMPEG_BIN
|
|
2319
|
+
if not ffmpeg:
|
|
2320
|
+
return {"error": "ffmpeg not found. Install ffmpeg or place it in your PATH."}
|
|
2321
|
+
|
|
2322
|
+
ffprobe = os.path.join(os.path.dirname(ffmpeg), "ffprobe" + (".exe" if sys.platform == "win32" else ""))
|
|
2323
|
+
if not os.path.isfile(ffprobe):
|
|
2324
|
+
ffprobe = shutil.which("ffprobe") or "ffprobe"
|
|
2325
|
+
|
|
2326
|
+
logger.info("Extracting frames from: %s (ffmpeg: %s)", file_path, ffmpeg)
|
|
2327
|
+
try:
|
|
2328
|
+
subprocess.run(
|
|
2329
|
+
[ffmpeg, "-y", "-i", file_path, "-qscale:v", "2", os.path.join(frames_dir, "frame_%06d.png")],
|
|
2330
|
+
capture_output=True, text=True, check=True,
|
|
2331
|
+
)
|
|
2332
|
+
except FileNotFoundError:
|
|
2333
|
+
return {"error": f"ffmpeg not found at: {ffmpeg}"}
|
|
2334
|
+
except subprocess.CalledProcessError as e:
|
|
2335
|
+
return {"error": f"ffmpeg frame extraction failed: {e.stderr[:500]}"}
|
|
2336
|
+
|
|
2337
|
+
frame_files = sorted(f for f in os.listdir(frames_dir) if f.endswith(".png"))
|
|
2338
|
+
if not frame_files:
|
|
2339
|
+
return {"error": "No frames extracted from video"}
|
|
2340
|
+
|
|
2341
|
+
total = len(frame_files)
|
|
2342
|
+
logger.info("Processing %d frames with model '%s'...", total, model)
|
|
2343
|
+
|
|
2344
|
+
session = _load_rembg(model)
|
|
2345
|
+
if session is None:
|
|
2346
|
+
return {"error": "Failed to load rembg model"}
|
|
2347
|
+
|
|
2348
|
+
output_frames_dir = os.path.join(output_dir, "frames")
|
|
2349
|
+
os.makedirs(output_frames_dir, exist_ok=True)
|
|
2350
|
+
|
|
2351
|
+
for i, fname in enumerate(frame_files):
|
|
2352
|
+
if (i + 1) % 50 == 0 or i == 0:
|
|
2353
|
+
logger.info(" Frame %d / %d", i + 1, total)
|
|
2354
|
+
try:
|
|
2355
|
+
img = Image.open(os.path.join(frames_dir, fname))
|
|
2356
|
+
result = remove(img, session=session, alpha_matting=alpha_matting)
|
|
2357
|
+
|
|
2358
|
+
if output_format == "matte_video":
|
|
2359
|
+
alpha = result.split()[-1] if result.mode == "RGBA" else result.convert("L")
|
|
2360
|
+
alpha.save(os.path.join(output_frames_dir, fname))
|
|
2361
|
+
else:
|
|
2362
|
+
result.save(os.path.join(output_frames_dir, fname))
|
|
2363
|
+
except Exception as e:
|
|
2364
|
+
logger.warning(" Frame %d failed: %s", i + 1, e)
|
|
2365
|
+
|
|
2366
|
+
shutil.rmtree(frames_dir, ignore_errors=True)
|
|
2367
|
+
|
|
2368
|
+
response: Dict[str, Any] = {
|
|
2369
|
+
"success": True,
|
|
2370
|
+
"model": model,
|
|
2371
|
+
"total_frames": total,
|
|
2372
|
+
"output_dir": output_frames_dir,
|
|
2373
|
+
"output_format": output_format,
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
if output_format == "matte_video":
|
|
2377
|
+
matte_path = os.path.join(output_dir, f"{basename}_matte.mp4")
|
|
2378
|
+
try:
|
|
2379
|
+
probe = subprocess.run(
|
|
2380
|
+
[ffprobe, "-v", "error", "-select_streams", "v:0",
|
|
2381
|
+
"-show_entries", "stream=r_frame_rate", "-of", "csv=p=0", file_path],
|
|
2382
|
+
capture_output=True, text=True,
|
|
2383
|
+
)
|
|
2384
|
+
fps = probe.stdout.strip() or "30"
|
|
2385
|
+
|
|
2386
|
+
subprocess.run(
|
|
2387
|
+
[ffmpeg, "-y", "-framerate", fps,
|
|
2388
|
+
"-i", os.path.join(output_frames_dir, "frame_%06d.png"),
|
|
2389
|
+
"-c:v", "libx264", "-pix_fmt", "yuv420p", matte_path],
|
|
2390
|
+
capture_output=True, text=True, check=True,
|
|
2391
|
+
)
|
|
2392
|
+
response["matte_video"] = matte_path
|
|
2393
|
+
except Exception as e:
|
|
2394
|
+
logger.warning("Matte video assembly failed: %s", e)
|
|
2395
|
+
response["matte_video_error"] = str(e)
|
|
2396
|
+
|
|
2397
|
+
logger.info("Background removal complete: %d frames processed", total)
|
|
2398
|
+
return response
|
|
2399
|
+
|
|
2400
|
+
|
|
2401
|
+
@mcp.tool()
|
|
2402
|
+
def remove_background_clip(
|
|
2403
|
+
track_type: str = "video",
|
|
2404
|
+
track_index: int = 1,
|
|
2405
|
+
clip_index: int = 0,
|
|
2406
|
+
model: str = "birefnet-general",
|
|
2407
|
+
output_format: str = "png_sequence",
|
|
2408
|
+
) -> Dict[str, Any]:
|
|
2409
|
+
"""[FREE + STUDIO · LOCAL AI] Remove background from a specific timeline clip's source video (open-source replacement for Studio Magic Mask).
|
|
2410
|
+
Works on DaVinci Resolve Free — no Studio license needed. Finds the clip's source file from the timeline, processes it, returns output paths.
|
|
2411
|
+
Args:
|
|
2412
|
+
track_type: 'video' or 'audio'. Defaults to 'video'.
|
|
2413
|
+
track_index: 1-based track index. Defaults to 1.
|
|
2414
|
+
clip_index: 0-based clip position on the track. Defaults to 0.
|
|
2415
|
+
model: Segmentation model — 'birefnet-general' (default), 'birefnet-general-lite' (faster).
|
|
2416
|
+
output_format: 'png_sequence' (PNG with alpha) or 'matte_video' (B/W matte MP4).
|
|
2417
|
+
Returns the output directory and frame count."""
|
|
2418
|
+
clips = _get("/timeline/clips", {"track_type": track_type, "track_index": str(track_index)})
|
|
2419
|
+
if "error" in clips:
|
|
2420
|
+
return clips
|
|
2421
|
+
clip_list = clips.get("clips", [])
|
|
2422
|
+
if not clip_list:
|
|
2423
|
+
return {"error": f"No clips on {track_type} track {track_index}"}
|
|
2424
|
+
if clip_index < 0 or clip_index >= len(clip_list):
|
|
2425
|
+
return {"error": f"clip_index {clip_index} out of range (0-{len(clip_list) - 1})"}
|
|
2426
|
+
file_path = clip_list[clip_index].get("File Path", "")
|
|
2427
|
+
if not file_path:
|
|
2428
|
+
return {"error": "Could not determine file path for this clip"}
|
|
2429
|
+
return remove_background_video(file_path, model=model, output_format=output_format)
|
|
2430
|
+
|
|
2431
|
+
|
|
2432
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
2433
|
+
# TRANSCRIPTION (local Whisper via faster-whisper)
|
|
2434
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
2435
|
+
|
|
2436
|
+
_whisper_model = None
|
|
2437
|
+
_whisper_model_size = None
|
|
2438
|
+
|
|
2439
|
+
|
|
2440
|
+
def _load_whisper(model_size: str = "small"):
|
|
2441
|
+
global _whisper_model, _whisper_model_size
|
|
2442
|
+
if _whisper_model and _whisper_model_size == model_size:
|
|
2443
|
+
return _whisper_model
|
|
2444
|
+
try:
|
|
2445
|
+
from faster_whisper import WhisperModel
|
|
2446
|
+
except ImportError:
|
|
2447
|
+
return None
|
|
2448
|
+
logger.info("Loading Whisper model '%s' (first load downloads ~483MB)...", model_size)
|
|
2449
|
+
_whisper_model = WhisperModel(model_size, device="cpu", compute_type="int8", cpu_threads=4)
|
|
2450
|
+
_whisper_model_size = model_size
|
|
2451
|
+
logger.info("Whisper model '%s' loaded.", model_size)
|
|
2452
|
+
return _whisper_model
|
|
2453
|
+
|
|
2454
|
+
|
|
2455
|
+
def _run_transcription(file_path: str, model_size: str = "small", language: Optional[str] = None) -> Dict[str, Any]:
|
|
2456
|
+
model = _load_whisper(model_size)
|
|
2457
|
+
if model is None:
|
|
2458
|
+
return {"error": "faster-whisper is not installed. Run: pip install faster-whisper"}
|
|
2459
|
+
|
|
2460
|
+
try:
|
|
2461
|
+
kwargs: Dict[str, Any] = {"beam_size": 5, "word_timestamps": True}
|
|
2462
|
+
if language:
|
|
2463
|
+
kwargs["language"] = language
|
|
2464
|
+
|
|
2465
|
+
segments_gen, info = model.transcribe(file_path, **kwargs)
|
|
2466
|
+
|
|
2467
|
+
segments = []
|
|
2468
|
+
full_text_parts = []
|
|
2469
|
+
for seg in segments_gen:
|
|
2470
|
+
words = []
|
|
2471
|
+
if seg.words:
|
|
2472
|
+
words = [{"word": w.word.strip(), "start": round(w.start, 2), "end": round(w.end, 2),
|
|
2473
|
+
"probability": round(w.probability, 2)} for w in seg.words]
|
|
2474
|
+
segments.append({
|
|
2475
|
+
"start": round(seg.start, 2),
|
|
2476
|
+
"end": round(seg.end, 2),
|
|
2477
|
+
"text": seg.text.strip(),
|
|
2478
|
+
"words": words,
|
|
2479
|
+
})
|
|
2480
|
+
full_text_parts.append(seg.text.strip())
|
|
2481
|
+
|
|
2482
|
+
return {
|
|
2483
|
+
"language": info.language,
|
|
2484
|
+
"language_probability": round(info.language_probability, 2),
|
|
2485
|
+
"duration": round(info.duration, 2),
|
|
2486
|
+
"full_text": " ".join(full_text_parts),
|
|
2487
|
+
"segments": segments,
|
|
2488
|
+
}
|
|
2489
|
+
except Exception as e:
|
|
2490
|
+
return {"error": f"Transcription failed: {e}"}
|
|
2491
|
+
|
|
2492
|
+
|
|
2493
|
+
@mcp.tool()
|
|
2494
|
+
def transcribe_timeline(model_size: str = "small", language: str = "") -> Dict[str, Any]:
|
|
2495
|
+
"""[FREE + STUDIO · LOCAL AI] Transcribe the audio from the current timeline using local Whisper (open-source replacement for Studio speech-to-text).
|
|
2496
|
+
Works on DaVinci Resolve Free — no Studio license needed. Downloads the model on first use (~483MB for 'small'). Runs entirely on CPU.
|
|
2497
|
+
Args:
|
|
2498
|
+
model_size: Whisper model size - 'tiny', 'base', 'small', 'medium', or 'large-v3'.
|
|
2499
|
+
'small' is the default (good accuracy/speed balance).
|
|
2500
|
+
language: Optional language code (e.g. 'en', 'es', 'fr'). Auto-detected if empty.
|
|
2501
|
+
Returns segments with timestamps and the full transcript text."""
|
|
2502
|
+
clips = _get("/timeline/clips", {"track_type": "audio", "track_index": "1"})
|
|
2503
|
+
if "error" in clips:
|
|
2504
|
+
return clips
|
|
2505
|
+
clip_list = clips.get("clips", [])
|
|
2506
|
+
if not clip_list:
|
|
2507
|
+
return {"error": "No audio clips found on audio track 1"}
|
|
2508
|
+
file_path = clip_list[0].get("File Path", "")
|
|
2509
|
+
if not file_path:
|
|
2510
|
+
return {"error": "Could not determine audio file path"}
|
|
2511
|
+
return _run_transcription(file_path, model_size, language or None)
|
|
2512
|
+
|
|
2513
|
+
|
|
2514
|
+
@mcp.tool()
|
|
2515
|
+
def transcribe_file(file_path: str, model_size: str = "small", language: str = "") -> Dict[str, Any]:
|
|
2516
|
+
"""[FREE + STUDIO · LOCAL AI] Transcribe any audio or video file using local Whisper (open-source replacement for Studio speech-to-text).
|
|
2517
|
+
Works on DaVinci Resolve Free — no Studio license needed.
|
|
2518
|
+
Args:
|
|
2519
|
+
file_path: Absolute path to the audio/video file (Windows path).
|
|
2520
|
+
model_size: Whisper model size - 'tiny', 'base', 'small', 'medium', or 'large-v3'.
|
|
2521
|
+
language: Optional language code. Auto-detected if empty.
|
|
2522
|
+
Returns segments with timestamps and the full transcript text."""
|
|
2523
|
+
return _run_transcription(file_path, model_size, language or None)
|
|
2524
|
+
|
|
2525
|
+
|
|
2526
|
+
# ---------------------------------------------------------------------------
|
|
2527
|
+
if __name__ == "__main__":
|
|
2528
|
+
logger.info("Starting DaVinci Resolve MCP Bridge Server (read + write + AI tools)")
|
|
2529
|
+
mcp.run()
|