davinci-resolve-mcp 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +85 -0
- package/CHANGELOG.md +802 -0
- package/CLAUDE.md +15 -0
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/SECURITY.md +53 -0
- package/bin/davinci-resolve-mcp.mjs +376 -0
- package/docs/README.md +56 -0
- package/docs/SKILL.md +1145 -0
- package/docs/authoring/fuse-dctl-authoring.md +242 -0
- package/docs/authoring/script-plugin-authoring.md +195 -0
- package/docs/contributing.md +82 -0
- package/docs/guides/color-decision-guide.md +387 -0
- package/docs/guides/editorial-decision-guide.md +136 -0
- package/docs/guides/media-analysis-guide.md +615 -0
- package/docs/guides/multicam-setup-guide.md +138 -0
- package/docs/install.md +198 -0
- package/docs/integrations/workflow-integrations.md +120 -0
- package/docs/kernels/README.md +28 -0
- package/docs/kernels/audio-fairlight-kernel.md +86 -0
- package/docs/kernels/color-grade-kernel.md +103 -0
- package/docs/kernels/extension-authoring-kernel.md +101 -0
- package/docs/kernels/fusion-composition-kernel.md +91 -0
- package/docs/kernels/media-pool-ingest-kernel.md +147 -0
- package/docs/kernels/project-lifecycle-kernel.md +120 -0
- package/docs/kernels/render-deliver-kernel.md +92 -0
- package/docs/kernels/review-annotation-kernel.md +110 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +99 -0
- package/docs/kernels/timeline-edit-kernel.md +189 -0
- package/docs/notes/codec-plugin-notes.md +136 -0
- package/docs/notes/dctl-notes.md +234 -0
- package/docs/notes/fusion-template-notes.md +136 -0
- package/docs/notes/lut-notes.md +136 -0
- package/docs/notes/openfx-notes.md +120 -0
- package/docs/process/release-process.md +152 -0
- package/docs/reference/api-coverage.md +488 -0
- package/docs/reference/resolve_scripting_api.txt +1012 -0
- package/examples/README.md +53 -0
- package/examples/markers/README.md +81 -0
- package/examples/media/README.md +94 -0
- package/examples/timeline/README.md +98 -0
- package/install.py +1196 -0
- package/package.json +52 -0
- package/scripts/audit_api_parity.py +275 -0
- package/scripts/live_media_analysis_polish_probe.py +65 -0
- package/src/__init__.py +3 -0
- package/src/analysis_dashboard.py +4936 -0
- package/src/control_panel.py +13 -0
- package/src/granular/__init__.py +17 -0
- package/src/granular/common.py +727 -0
- package/src/granular/folder.py +287 -0
- package/src/granular/gallery.py +306 -0
- package/src/granular/graph.py +309 -0
- package/src/granular/media_pool.py +679 -0
- package/src/granular/media_pool_item.py +852 -0
- package/src/granular/media_storage.py +179 -0
- package/src/granular/project.py +1594 -0
- package/src/granular/resolve_control.py +521 -0
- package/src/granular/timeline.py +1074 -0
- package/src/granular/timeline_item.py +2251 -0
- package/src/resolve_mcp_server.py +43 -0
- package/src/server.py +15691 -0
- package/src/utils/__init__.py +3 -0
- package/src/utils/app_control.py +319 -0
- package/src/utils/audio_fairlight_live_probe.py +263 -0
- package/src/utils/cdl.py +20 -0
- package/src/utils/cloud_operations.py +192 -0
- package/src/utils/color_grade_live_probe.py +444 -0
- package/src/utils/dctl_templates.py +368 -0
- package/src/utils/extension_authoring_live_probe.py +292 -0
- package/src/utils/fuse_templates.py +1968 -0
- package/src/utils/fusion_composition_live_probe.py +284 -0
- package/src/utils/layout_presets.py +333 -0
- package/src/utils/mcp_stdio.py +32 -0
- package/src/utils/media_analysis.py +3618 -0
- package/src/utils/media_analysis_jobs.py +796 -0
- package/src/utils/media_pool_ingest_live_probe.py +592 -0
- package/src/utils/multicam.py +393 -0
- package/src/utils/object_inspection.py +287 -0
- package/src/utils/platform.py +157 -0
- package/src/utils/project_lifecycle_live_probe.py +376 -0
- package/src/utils/project_properties.py +601 -0
- package/src/utils/render_deliver_live_probe.py +384 -0
- package/src/utils/resolve_connection.py +77 -0
- package/src/utils/review_annotation_live_probe.py +352 -0
- package/src/utils/script_templates.py +1193 -0
- package/src/utils/sync_detection.py +887 -0
- package/src/utils/timeline_conform_live_probe.py +280 -0
- package/src/utils/timeline_kernel_live_probe.py +1091 -0
- package/src/utils/timeline_kernel_probe.py +185 -0
- package/src/utils/timeline_title_text.py +87 -0
- package/src/utils/update_check.py +610 -0
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
"""Timeline resources and operations."""
|
|
2
|
+
|
|
3
|
+
from src.granular.common import * # noqa: F401,F403
|
|
4
|
+
|
|
5
|
+
resolve = ResolveProxy()
|
|
6
|
+
|
|
7
|
+
@mcp.resource("resolve://timelines")
|
|
8
|
+
def list_timelines() -> List[str]:
|
|
9
|
+
"""List all timelines in the current project."""
|
|
10
|
+
logger.info("Received request to list timelines")
|
|
11
|
+
|
|
12
|
+
if resolve is None:
|
|
13
|
+
logger.error("Not connected to DaVinci Resolve")
|
|
14
|
+
return ["Error: Not connected to DaVinci Resolve"]
|
|
15
|
+
|
|
16
|
+
project_manager = resolve.GetProjectManager()
|
|
17
|
+
if not project_manager:
|
|
18
|
+
logger.error("Failed to get Project Manager")
|
|
19
|
+
return ["Error: Failed to get Project Manager"]
|
|
20
|
+
|
|
21
|
+
current_project = project_manager.GetCurrentProject()
|
|
22
|
+
if not current_project:
|
|
23
|
+
logger.error("No project currently open")
|
|
24
|
+
return ["Error: No project currently open"]
|
|
25
|
+
|
|
26
|
+
timeline_count = current_project.GetTimelineCount()
|
|
27
|
+
logger.info(f"Timeline count: {timeline_count}")
|
|
28
|
+
|
|
29
|
+
timelines = []
|
|
30
|
+
|
|
31
|
+
for i in range(1, timeline_count + 1):
|
|
32
|
+
timeline = current_project.GetTimelineByIndex(i)
|
|
33
|
+
if timeline:
|
|
34
|
+
timeline_name = timeline.GetName()
|
|
35
|
+
timelines.append(timeline_name)
|
|
36
|
+
logger.info(f"Found timeline {i}: {timeline_name}")
|
|
37
|
+
|
|
38
|
+
if not timelines:
|
|
39
|
+
logger.info("No timelines found in the current project")
|
|
40
|
+
return ["No timelines found in the current project"]
|
|
41
|
+
|
|
42
|
+
logger.info(f"Returning {len(timelines)} timelines: {', '.join(timelines)}")
|
|
43
|
+
return timelines
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@mcp.resource("resolve://current-timeline")
|
|
47
|
+
def get_current_timeline() -> Dict[str, Any]:
|
|
48
|
+
"""Get information about the current timeline."""
|
|
49
|
+
pm, current_project = get_current_project()
|
|
50
|
+
if not current_project:
|
|
51
|
+
return {"error": "No project currently open"}
|
|
52
|
+
|
|
53
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
54
|
+
if not current_timeline:
|
|
55
|
+
return {"error": "No timeline currently active"}
|
|
56
|
+
|
|
57
|
+
# Get basic timeline information
|
|
58
|
+
result = {
|
|
59
|
+
"name": current_timeline.GetName(),
|
|
60
|
+
"fps": current_timeline.GetSetting("timelineFrameRate"),
|
|
61
|
+
"resolution": {
|
|
62
|
+
"width": current_timeline.GetSetting("timelineResolutionWidth"),
|
|
63
|
+
"height": current_timeline.GetSetting("timelineResolutionHeight")
|
|
64
|
+
},
|
|
65
|
+
"duration": current_timeline.GetEndFrame() - current_timeline.GetStartFrame() + 1
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return result
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@mcp.tool()
|
|
72
|
+
def create_timeline(name: str) -> str:
|
|
73
|
+
"""Create a new timeline with the given name.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name: The name for the new timeline
|
|
77
|
+
"""
|
|
78
|
+
resolve = get_resolve()
|
|
79
|
+
if resolve is None:
|
|
80
|
+
return "Error: Not connected to DaVinci Resolve"
|
|
81
|
+
|
|
82
|
+
if not name:
|
|
83
|
+
return "Error: Timeline name cannot be empty"
|
|
84
|
+
|
|
85
|
+
project_manager = resolve.GetProjectManager()
|
|
86
|
+
if not project_manager:
|
|
87
|
+
return "Error: Failed to get Project Manager"
|
|
88
|
+
|
|
89
|
+
current_project = project_manager.GetCurrentProject()
|
|
90
|
+
if not current_project:
|
|
91
|
+
return "Error: No project currently open"
|
|
92
|
+
|
|
93
|
+
media_pool = current_project.GetMediaPool()
|
|
94
|
+
if not media_pool:
|
|
95
|
+
return "Error: Failed to get Media Pool"
|
|
96
|
+
|
|
97
|
+
timeline = media_pool.CreateEmptyTimeline(name)
|
|
98
|
+
if timeline:
|
|
99
|
+
return f"Successfully created timeline '{name}'"
|
|
100
|
+
else:
|
|
101
|
+
return f"Failed to create timeline '{name}'"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@mcp.tool()
|
|
105
|
+
def set_current_timeline(name: str) -> str:
|
|
106
|
+
"""Switch to a timeline by name.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
name: The name of the timeline to set as current
|
|
110
|
+
"""
|
|
111
|
+
resolve = get_resolve()
|
|
112
|
+
if resolve is None:
|
|
113
|
+
return "Error: Not connected to DaVinci Resolve"
|
|
114
|
+
|
|
115
|
+
if not name:
|
|
116
|
+
return "Error: Timeline name cannot be empty"
|
|
117
|
+
|
|
118
|
+
project_manager = resolve.GetProjectManager()
|
|
119
|
+
if not project_manager:
|
|
120
|
+
return "Error: Failed to get Project Manager"
|
|
121
|
+
|
|
122
|
+
current_project = project_manager.GetCurrentProject()
|
|
123
|
+
if not current_project:
|
|
124
|
+
return "Error: No project currently open"
|
|
125
|
+
|
|
126
|
+
# Find the timeline by name
|
|
127
|
+
timeline_count = current_project.GetTimelineCount()
|
|
128
|
+
for i in range(1, timeline_count + 1):
|
|
129
|
+
timeline = current_project.GetTimelineByIndex(i)
|
|
130
|
+
if timeline and timeline.GetName() == name:
|
|
131
|
+
result = current_project.SetCurrentTimeline(timeline)
|
|
132
|
+
if result:
|
|
133
|
+
return f"Successfully switched to timeline '{name}'"
|
|
134
|
+
else:
|
|
135
|
+
return f"Failed to switch to timeline '{name}'"
|
|
136
|
+
|
|
137
|
+
return f"Error: Timeline '{name}' not found"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@mcp.resource("resolve://timeline-clips")
|
|
141
|
+
def list_timeline_clips() -> List[Dict[str, Any]]:
|
|
142
|
+
"""List all clips in the current timeline."""
|
|
143
|
+
pm, current_project = get_current_project()
|
|
144
|
+
if not current_project:
|
|
145
|
+
return [{"error": "No project currently open"}]
|
|
146
|
+
|
|
147
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
148
|
+
if not current_timeline:
|
|
149
|
+
return [{"error": "No timeline currently active"}]
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
# Get all tracks in the timeline
|
|
153
|
+
# Video tracks are 1-based index (1 is first track)
|
|
154
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
155
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
156
|
+
|
|
157
|
+
clips = []
|
|
158
|
+
|
|
159
|
+
# Process video tracks
|
|
160
|
+
for track_index in range(1, video_track_count + 1):
|
|
161
|
+
track_items = current_timeline.GetItemListInTrack("video", track_index)
|
|
162
|
+
if track_items:
|
|
163
|
+
for item in track_items:
|
|
164
|
+
clips.append({
|
|
165
|
+
"name": item.GetName(),
|
|
166
|
+
"type": "video",
|
|
167
|
+
"track": track_index,
|
|
168
|
+
"start_frame": item.GetStart(),
|
|
169
|
+
"end_frame": item.GetEnd(),
|
|
170
|
+
"duration": item.GetDuration()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
# Process audio tracks
|
|
174
|
+
for track_index in range(1, audio_track_count + 1):
|
|
175
|
+
track_items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
176
|
+
if track_items:
|
|
177
|
+
for item in track_items:
|
|
178
|
+
clips.append({
|
|
179
|
+
"name": item.GetName(),
|
|
180
|
+
"type": "audio",
|
|
181
|
+
"track": track_index,
|
|
182
|
+
"start_frame": item.GetStart(),
|
|
183
|
+
"end_frame": item.GetEnd(),
|
|
184
|
+
"duration": item.GetDuration()
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
if not clips:
|
|
188
|
+
return [{"info": "No clips found in the current timeline"}]
|
|
189
|
+
|
|
190
|
+
return clips
|
|
191
|
+
except Exception as e:
|
|
192
|
+
return [{"error": f"Error listing timeline clips: {str(e)}"}]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@mcp.tool()
|
|
196
|
+
def list_timelines_tool() -> List[str]:
|
|
197
|
+
"""List all timelines in the current project as a tool."""
|
|
198
|
+
logger.info("Received request to list timelines via tool")
|
|
199
|
+
return list_timelines()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@mcp.tool()
|
|
203
|
+
def timeline_set_name(name: str) -> Dict[str, Any]:
|
|
204
|
+
"""Rename the current timeline.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
name: New name for the timeline.
|
|
208
|
+
"""
|
|
209
|
+
_, tl, err = _get_timeline()
|
|
210
|
+
if err:
|
|
211
|
+
return err
|
|
212
|
+
result = tl.SetName(name)
|
|
213
|
+
return {"success": bool(result), "name": name}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@mcp.tool()
|
|
217
|
+
def timeline_set_start_timecode(timecode: str) -> Dict[str, Any]:
|
|
218
|
+
"""Set the start timecode of the current timeline.
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
timecode: Timecode string (e.g. '01:00:00:00').
|
|
222
|
+
"""
|
|
223
|
+
_, tl, err = _get_timeline()
|
|
224
|
+
if err:
|
|
225
|
+
return err
|
|
226
|
+
result = tl.SetStartTimecode(timecode)
|
|
227
|
+
return {"success": bool(result), "timecode": timecode}
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@mcp.tool()
|
|
231
|
+
def timeline_get_current_timecode() -> Dict[str, Any]:
|
|
232
|
+
"""Get the current playhead timecode."""
|
|
233
|
+
_, tl, err = _get_timeline()
|
|
234
|
+
if err:
|
|
235
|
+
return err
|
|
236
|
+
return {"timecode": tl.GetCurrentTimecode()}
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@mcp.tool()
|
|
240
|
+
def timeline_set_current_timecode(timecode: str) -> Dict[str, Any]:
|
|
241
|
+
"""Set the playhead to a specific timecode.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
timecode: Timecode string (e.g. '01:00:05:00').
|
|
245
|
+
"""
|
|
246
|
+
_, tl, err = _get_timeline()
|
|
247
|
+
if err:
|
|
248
|
+
return err
|
|
249
|
+
result = tl.SetCurrentTimecode(timecode)
|
|
250
|
+
return {"success": bool(result), "timecode": timecode}
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@mcp.tool()
|
|
254
|
+
def timeline_add_track(
|
|
255
|
+
track_type: str,
|
|
256
|
+
audio_type: Optional[str] = None,
|
|
257
|
+
index: Optional[int] = None,
|
|
258
|
+
) -> Dict[str, Any]:
|
|
259
|
+
"""Add a new track to the timeline.
|
|
260
|
+
|
|
261
|
+
Mirrors Timeline.AddTrack(trackType, {newTrackOptions}) per docs line 327.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
265
|
+
audio_type: For audio tracks: 'mono', 'stereo', 'lrc', 'lcr', 'lrcs', 'lcrs',
|
|
266
|
+
'quad', '5.0', '5.0film', '5.1', '5.1film', '7.0', '7.0film', '7.1',
|
|
267
|
+
'7.1film', 'adaptive1' through 'adaptive36'. Defaults to 'mono' for audio
|
|
268
|
+
tracks if omitted.
|
|
269
|
+
index: 1-based track index where 1 <= index <= GetTrackCount(track_type) + 1.
|
|
270
|
+
If omitted or out of bounds, the track is appended.
|
|
271
|
+
"""
|
|
272
|
+
_, tl, err = _get_timeline()
|
|
273
|
+
if err:
|
|
274
|
+
return err
|
|
275
|
+
options: Dict[str, Any] = {}
|
|
276
|
+
if audio_type is not None:
|
|
277
|
+
options["audioType"] = audio_type
|
|
278
|
+
if index is not None:
|
|
279
|
+
options["index"] = index
|
|
280
|
+
if options:
|
|
281
|
+
result = tl.AddTrack(track_type, options)
|
|
282
|
+
else:
|
|
283
|
+
result = tl.AddTrack(track_type)
|
|
284
|
+
return {"success": bool(result), "track_type": track_type}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@mcp.tool()
|
|
288
|
+
def timeline_delete_track(track_type: str, track_index: int) -> Dict[str, Any]:
|
|
289
|
+
"""Delete a track from the timeline.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
track_type: 'video', 'audio', or 'subtitle'.
|
|
293
|
+
track_index: 1-based track index to delete.
|
|
294
|
+
"""
|
|
295
|
+
_, tl, err = _get_timeline()
|
|
296
|
+
if err:
|
|
297
|
+
return err
|
|
298
|
+
result = tl.DeleteTrack(track_type, track_index)
|
|
299
|
+
return {"success": bool(result)}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@mcp.tool()
|
|
303
|
+
def timeline_get_track_sub_type(track_type: str, track_index: int) -> Dict[str, Any]:
|
|
304
|
+
"""Get the sub-type of a track (e.g. mono, stereo for audio).
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
track_type: 'video' or 'audio'.
|
|
308
|
+
track_index: 1-based track index.
|
|
309
|
+
"""
|
|
310
|
+
_, tl, err = _get_timeline()
|
|
311
|
+
if err:
|
|
312
|
+
return err
|
|
313
|
+
sub = tl.GetTrackSubType(track_type, track_index)
|
|
314
|
+
return {"track_type": track_type, "track_index": track_index, "sub_type": sub if sub else ""}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@mcp.tool()
|
|
318
|
+
def timeline_set_track_enable(track_type: str, track_index: int, enabled: bool) -> Dict[str, Any]:
|
|
319
|
+
"""Enable or disable a track.
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
track_type: 'video' or 'audio'.
|
|
323
|
+
track_index: 1-based track index.
|
|
324
|
+
enabled: True to enable, False to disable.
|
|
325
|
+
"""
|
|
326
|
+
_, tl, err = _get_timeline()
|
|
327
|
+
if err:
|
|
328
|
+
return err
|
|
329
|
+
result = tl.SetTrackEnable(track_type, track_index, enabled)
|
|
330
|
+
return {"success": bool(result)}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@mcp.tool()
|
|
334
|
+
def timeline_get_is_track_enabled(track_type: str, track_index: int) -> Dict[str, Any]:
|
|
335
|
+
"""Check if a track is enabled.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
track_type: 'video' or 'audio'.
|
|
339
|
+
track_index: 1-based track index.
|
|
340
|
+
"""
|
|
341
|
+
_, tl, err = _get_timeline()
|
|
342
|
+
if err:
|
|
343
|
+
return err
|
|
344
|
+
enabled = tl.GetIsTrackEnabled(track_type, track_index)
|
|
345
|
+
return {"enabled": bool(enabled)}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
@mcp.tool()
|
|
349
|
+
def timeline_set_track_lock(track_type: str, track_index: int, locked: bool) -> Dict[str, Any]:
|
|
350
|
+
"""Lock or unlock a track.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
track_type: 'video' or 'audio'.
|
|
354
|
+
track_index: 1-based track index.
|
|
355
|
+
locked: True to lock, False to unlock.
|
|
356
|
+
"""
|
|
357
|
+
_, tl, err = _get_timeline()
|
|
358
|
+
if err:
|
|
359
|
+
return err
|
|
360
|
+
result = tl.SetTrackLock(track_type, track_index, locked)
|
|
361
|
+
return {"success": bool(result)}
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
@mcp.tool()
|
|
365
|
+
def timeline_get_is_track_locked(track_type: str, track_index: int) -> Dict[str, Any]:
|
|
366
|
+
"""Check if a track is locked.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
track_type: 'video' or 'audio'.
|
|
370
|
+
track_index: 1-based track index.
|
|
371
|
+
"""
|
|
372
|
+
_, tl, err = _get_timeline()
|
|
373
|
+
if err:
|
|
374
|
+
return err
|
|
375
|
+
locked = tl.GetIsTrackLocked(track_type, track_index)
|
|
376
|
+
return {"locked": bool(locked)}
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@mcp.tool()
|
|
380
|
+
def timeline_get_voice_isolation_state(track_index: int) -> Dict[str, Any]:
|
|
381
|
+
"""Get voice isolation state for an audio track.
|
|
382
|
+
|
|
383
|
+
Args:
|
|
384
|
+
track_index: 1-based audio track index.
|
|
385
|
+
"""
|
|
386
|
+
_, tl, err = _get_timeline()
|
|
387
|
+
if err:
|
|
388
|
+
return err
|
|
389
|
+
missing = _requires_method(tl, "GetVoiceIsolationState", "20.1")
|
|
390
|
+
if missing:
|
|
391
|
+
return missing
|
|
392
|
+
state = tl.GetVoiceIsolationState(track_index)
|
|
393
|
+
return {"state": state if state else {"isEnabled": False, "amount": 0}}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@mcp.tool()
|
|
397
|
+
def timeline_set_voice_isolation_state(track_index: int, state: Dict[str, Any]) -> Dict[str, Any]:
|
|
398
|
+
"""Set voice isolation state for an audio track.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
track_index: 1-based audio track index.
|
|
402
|
+
state: Dict with isEnabled (bool) and amount (0-100).
|
|
403
|
+
"""
|
|
404
|
+
_, tl, err = _get_timeline()
|
|
405
|
+
if err:
|
|
406
|
+
return err
|
|
407
|
+
missing = _requires_method(tl, "SetVoiceIsolationState", "20.1")
|
|
408
|
+
if missing:
|
|
409
|
+
return missing
|
|
410
|
+
result = tl.SetVoiceIsolationState(track_index, state)
|
|
411
|
+
return {"success": bool(result)}
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@mcp.tool()
|
|
415
|
+
def timeline_delete_clips(clip_ids: List[str], track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
416
|
+
"""Delete clips from the timeline.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
clip_ids: List of clip unique IDs to delete.
|
|
420
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
421
|
+
track_index: 1-based track index. Default: 1.
|
|
422
|
+
"""
|
|
423
|
+
_, tl, err = _get_timeline()
|
|
424
|
+
if err:
|
|
425
|
+
return err
|
|
426
|
+
items = tl.GetItemListInTrack(track_type, track_index)
|
|
427
|
+
if not items:
|
|
428
|
+
return {"error": "No items in track"}
|
|
429
|
+
to_delete = [i for i in items if i.GetUniqueId() in clip_ids]
|
|
430
|
+
if not to_delete:
|
|
431
|
+
return {"error": "No matching clips found"}
|
|
432
|
+
result = tl.DeleteClips(to_delete)
|
|
433
|
+
return {"success": bool(result), "deleted": len(to_delete)}
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@mcp.tool()
|
|
437
|
+
def timeline_set_clips_linked(clip_ids: List[str], linked: bool, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
438
|
+
"""Link or unlink clips in the timeline.
|
|
439
|
+
|
|
440
|
+
Args:
|
|
441
|
+
clip_ids: List of clip unique IDs.
|
|
442
|
+
linked: True to link, False to unlink.
|
|
443
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
444
|
+
track_index: 1-based track index. Default: 1.
|
|
445
|
+
"""
|
|
446
|
+
_, tl, err = _get_timeline()
|
|
447
|
+
if err:
|
|
448
|
+
return err
|
|
449
|
+
items = tl.GetItemListInTrack(track_type, track_index)
|
|
450
|
+
if not items:
|
|
451
|
+
return {"error": "No items in track"}
|
|
452
|
+
targets = [i for i in items if i.GetUniqueId() in clip_ids]
|
|
453
|
+
if not targets:
|
|
454
|
+
return {"error": "No matching clips found"}
|
|
455
|
+
result = tl.SetClipsLinked(targets, linked)
|
|
456
|
+
return {"success": bool(result)}
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@mcp.tool()
|
|
460
|
+
def timeline_add_marker(frame_id: int, color: str, name: str, note: str = "", duration: int = 1, custom_data: str = "") -> Dict[str, Any]:
|
|
461
|
+
"""Add a marker to the current timeline.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
frame_id: Frame number for the marker.
|
|
465
|
+
color: Marker color (Blue, Cyan, Green, Yellow, Red, Pink, Purple, Fuchsia, Rose, Lavender, Sky, Mint, Lemon, Sand, Cocoa, Cream).
|
|
466
|
+
name: Marker name.
|
|
467
|
+
note: Marker note. Default: empty.
|
|
468
|
+
duration: Duration in frames. Default: 1.
|
|
469
|
+
custom_data: Custom data string. Default: empty.
|
|
470
|
+
"""
|
|
471
|
+
_, tl, err = _get_timeline()
|
|
472
|
+
if err:
|
|
473
|
+
return err
|
|
474
|
+
result = tl.AddMarker(frame_id, color, name, note, duration, custom_data)
|
|
475
|
+
return {"success": bool(result)}
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
@mcp.tool()
|
|
479
|
+
def timeline_get_markers() -> Dict[str, Any]:
|
|
480
|
+
"""Get all markers on the current timeline."""
|
|
481
|
+
_, tl, err = _get_timeline()
|
|
482
|
+
if err:
|
|
483
|
+
return err
|
|
484
|
+
markers = tl.GetMarkers()
|
|
485
|
+
return {"markers": markers if markers else {}}
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
@mcp.tool()
|
|
489
|
+
def timeline_get_marker_by_custom_data(custom_data: str) -> Dict[str, Any]:
|
|
490
|
+
"""Find a timeline marker by its custom data.
|
|
491
|
+
|
|
492
|
+
Args:
|
|
493
|
+
custom_data: Custom data string to search for.
|
|
494
|
+
"""
|
|
495
|
+
_, tl, err = _get_timeline()
|
|
496
|
+
if err:
|
|
497
|
+
return err
|
|
498
|
+
marker = tl.GetMarkerByCustomData(custom_data)
|
|
499
|
+
return {"marker": marker if marker else {}}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@mcp.tool()
|
|
503
|
+
def timeline_update_marker_custom_data(frame_id: int, custom_data: str) -> Dict[str, Any]:
|
|
504
|
+
"""Update the custom data of a timeline marker.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
frame_id: Frame number of the marker.
|
|
508
|
+
custom_data: New custom data string.
|
|
509
|
+
"""
|
|
510
|
+
_, tl, err = _get_timeline()
|
|
511
|
+
if err:
|
|
512
|
+
return err
|
|
513
|
+
result = tl.UpdateMarkerCustomData(frame_id, custom_data)
|
|
514
|
+
return {"success": bool(result)}
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@mcp.tool()
|
|
518
|
+
def timeline_get_marker_custom_data(frame_id: int) -> Dict[str, Any]:
|
|
519
|
+
"""Get the custom data of a timeline marker.
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
frame_id: Frame number of the marker.
|
|
523
|
+
"""
|
|
524
|
+
_, tl, err = _get_timeline()
|
|
525
|
+
if err:
|
|
526
|
+
return err
|
|
527
|
+
data = tl.GetMarkerCustomData(frame_id)
|
|
528
|
+
return {"frame_id": frame_id, "custom_data": data if data else ""}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
@mcp.tool()
|
|
532
|
+
def timeline_delete_markers_by_color(color: str) -> Dict[str, Any]:
|
|
533
|
+
"""Delete all timeline markers of a specific color.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
color: Color of markers to delete. Use '' to delete all.
|
|
537
|
+
"""
|
|
538
|
+
_, tl, err = _get_timeline()
|
|
539
|
+
if err:
|
|
540
|
+
return err
|
|
541
|
+
result = tl.DeleteMarkersByColor(color)
|
|
542
|
+
return {"success": bool(result)}
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
@mcp.tool()
|
|
546
|
+
def timeline_delete_marker_at_frame(frame_id: int) -> Dict[str, Any]:
|
|
547
|
+
"""Delete a timeline marker at a specific frame.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
frame_id: Frame number of the marker.
|
|
551
|
+
"""
|
|
552
|
+
_, tl, err = _get_timeline()
|
|
553
|
+
if err:
|
|
554
|
+
return err
|
|
555
|
+
result = tl.DeleteMarkerAtFrame(frame_id)
|
|
556
|
+
return {"success": bool(result)}
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@mcp.tool()
|
|
560
|
+
def timeline_delete_marker_by_custom_data(custom_data: str) -> Dict[str, Any]:
|
|
561
|
+
"""Delete a timeline marker by custom data.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
custom_data: Custom data of the marker to delete.
|
|
565
|
+
"""
|
|
566
|
+
_, tl, err = _get_timeline()
|
|
567
|
+
if err:
|
|
568
|
+
return err
|
|
569
|
+
result = tl.DeleteMarkerByCustomData(custom_data)
|
|
570
|
+
return {"success": bool(result)}
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@mcp.tool()
|
|
574
|
+
def timeline_get_track_name(track_type: str, track_index: int) -> Dict[str, Any]:
|
|
575
|
+
"""Get the name of a track.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
track_type: 'video' or 'audio'.
|
|
579
|
+
track_index: 1-based track index.
|
|
580
|
+
"""
|
|
581
|
+
_, tl, err = _get_timeline()
|
|
582
|
+
if err:
|
|
583
|
+
return err
|
|
584
|
+
name = tl.GetTrackName(track_type, track_index)
|
|
585
|
+
return {"track_name": name if name else ""}
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
@mcp.tool()
|
|
589
|
+
def timeline_set_track_name(track_type: str, track_index: int, name: str) -> Dict[str, Any]:
|
|
590
|
+
"""Set the name of a track.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
track_type: 'video' or 'audio'.
|
|
594
|
+
track_index: 1-based track index.
|
|
595
|
+
name: New track name.
|
|
596
|
+
"""
|
|
597
|
+
_, tl, err = _get_timeline()
|
|
598
|
+
if err:
|
|
599
|
+
return err
|
|
600
|
+
result = tl.SetTrackName(track_type, track_index, name)
|
|
601
|
+
return {"success": bool(result)}
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@mcp.tool()
|
|
605
|
+
def timeline_duplicate() -> Dict[str, Any]:
|
|
606
|
+
"""Duplicate the current timeline."""
|
|
607
|
+
_, tl, err = _get_timeline()
|
|
608
|
+
if err:
|
|
609
|
+
return err
|
|
610
|
+
new_tl = tl.DuplicateTimeline()
|
|
611
|
+
if new_tl:
|
|
612
|
+
return {"success": True, "name": new_tl.GetName(), "unique_id": new_tl.GetUniqueId()}
|
|
613
|
+
return {"success": False, "error": "Failed to duplicate timeline"}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
@mcp.tool()
|
|
617
|
+
def timeline_create_compound_clip(
|
|
618
|
+
clip_ids: List[str],
|
|
619
|
+
track_type: str = "video",
|
|
620
|
+
track_index: int = 1,
|
|
621
|
+
name: Optional[str] = None,
|
|
622
|
+
start_timecode: Optional[str] = None,
|
|
623
|
+
) -> Dict[str, Any]:
|
|
624
|
+
"""Create a compound clip from selected items.
|
|
625
|
+
|
|
626
|
+
Mirrors Timeline.CreateCompoundClip([timelineItems], {clipInfo}) per docs line 369.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
clip_ids: List of timeline item unique IDs.
|
|
630
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
631
|
+
track_index: 1-based track index. Default: 1.
|
|
632
|
+
name: Optional name for the compound clip; maps to clipInfo.name.
|
|
633
|
+
start_timecode: Optional start timecode (e.g. "01:00:00:00"); maps to clipInfo.startTimecode.
|
|
634
|
+
"""
|
|
635
|
+
_, tl, err = _get_timeline()
|
|
636
|
+
if err:
|
|
637
|
+
return err
|
|
638
|
+
items = tl.GetItemListInTrack(track_type, track_index)
|
|
639
|
+
targets = [i for i in (items or []) if i.GetUniqueId() in clip_ids]
|
|
640
|
+
if not targets:
|
|
641
|
+
return {"error": "No matching items found"}
|
|
642
|
+
clip_info: Dict[str, Any] = {}
|
|
643
|
+
if name is not None:
|
|
644
|
+
clip_info["name"] = name
|
|
645
|
+
if start_timecode is not None:
|
|
646
|
+
clip_info["startTimecode"] = start_timecode
|
|
647
|
+
if clip_info:
|
|
648
|
+
result = tl.CreateCompoundClip(targets, clip_info)
|
|
649
|
+
else:
|
|
650
|
+
result = tl.CreateCompoundClip(targets)
|
|
651
|
+
if not result:
|
|
652
|
+
return {"success": False, "error": "Failed to create compound clip"}
|
|
653
|
+
return {
|
|
654
|
+
"success": True,
|
|
655
|
+
"name": result.GetName() if hasattr(result, "GetName") else None,
|
|
656
|
+
"unique_id": result.GetUniqueId() if hasattr(result, "GetUniqueId") else None,
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@mcp.tool()
|
|
661
|
+
def timeline_create_fusion_clip(clip_ids: List[str], track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
662
|
+
"""Create a Fusion clip from selected items.
|
|
663
|
+
|
|
664
|
+
Args:
|
|
665
|
+
clip_ids: List of timeline item unique IDs.
|
|
666
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
667
|
+
track_index: 1-based track index. Default: 1.
|
|
668
|
+
"""
|
|
669
|
+
_, tl, err = _get_timeline()
|
|
670
|
+
if err:
|
|
671
|
+
return err
|
|
672
|
+
items = tl.GetItemListInTrack(track_type, track_index)
|
|
673
|
+
targets = [i for i in (items or []) if i.GetUniqueId() in clip_ids]
|
|
674
|
+
if not targets:
|
|
675
|
+
return {"error": "No matching items found"}
|
|
676
|
+
result = tl.CreateFusionClip(targets)
|
|
677
|
+
if result:
|
|
678
|
+
return {"success": True}
|
|
679
|
+
return {"success": False, "error": "Failed to create Fusion clip"}
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
@mcp.tool()
|
|
683
|
+
def timeline_import_into(file_path: str, import_options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
684
|
+
"""Import content into the current timeline from a file.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
file_path: Path to the file to import (AAF, EDL, XML, etc.).
|
|
688
|
+
import_options: Optional dict of import options.
|
|
689
|
+
"""
|
|
690
|
+
_, tl, err = _get_timeline()
|
|
691
|
+
if err:
|
|
692
|
+
return err
|
|
693
|
+
if import_options:
|
|
694
|
+
result = tl.ImportIntoTimeline(file_path, import_options)
|
|
695
|
+
else:
|
|
696
|
+
result = tl.ImportIntoTimeline(file_path)
|
|
697
|
+
return {"success": bool(result)}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
@mcp.tool()
|
|
701
|
+
def timeline_export(file_path: str, export_type: str, export_subtype: str = "EXPORT_NONE") -> Dict[str, Any]:
|
|
702
|
+
"""Export the current timeline to a file.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
file_path: Output file path.
|
|
706
|
+
export_type: Export type (e.g. 'EXPORT_AAF', 'EXPORT_EDL', 'EXPORT_FCP_7_XML', 'EXPORT_FCPXML_1_10', 'EXPORT_DRT', 'EXPORT_TEXT_CSV', 'EXPORT_TEXT_TAB', 'EXPORT_OTIO', 'EXPORT_ALE').
|
|
707
|
+
export_subtype: Export subtype for AAF/EDL. Default: 'EXPORT_NONE'.
|
|
708
|
+
"""
|
|
709
|
+
_, tl, err = _get_timeline()
|
|
710
|
+
if err:
|
|
711
|
+
return err
|
|
712
|
+
# Map string constants to resolve constants
|
|
713
|
+
try:
|
|
714
|
+
etype = getattr(resolve, export_type) if hasattr(resolve, export_type) else export_type
|
|
715
|
+
esub = getattr(resolve, export_subtype) if hasattr(resolve, export_subtype) else export_subtype
|
|
716
|
+
except Exception:
|
|
717
|
+
logger.debug("Could not resolve timeline export constants", exc_info=True)
|
|
718
|
+
etype = export_type
|
|
719
|
+
esub = export_subtype
|
|
720
|
+
result = tl.Export(file_path, etype, esub)
|
|
721
|
+
return {"success": bool(result), "file_path": file_path}
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
@mcp.tool()
|
|
725
|
+
def timeline_insert_generator(generator_name: str, duration: Optional[int] = None) -> Dict[str, Any]:
|
|
726
|
+
"""Insert a generator into the timeline.
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
generator_name: Name of the generator to insert.
|
|
730
|
+
duration: Optional duration in frames.
|
|
731
|
+
"""
|
|
732
|
+
_, tl, err = _get_timeline()
|
|
733
|
+
if err:
|
|
734
|
+
return err
|
|
735
|
+
if duration:
|
|
736
|
+
result = tl.InsertGeneratorIntoTimeline(generator_name, {"duration": duration})
|
|
737
|
+
else:
|
|
738
|
+
result = tl.InsertGeneratorIntoTimeline(generator_name)
|
|
739
|
+
return {"success": result is not None}
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
@mcp.tool()
|
|
743
|
+
def timeline_insert_fusion_generator(generator_name: str) -> Dict[str, Any]:
|
|
744
|
+
"""Insert a Fusion generator into the timeline.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
generator_name: Name of the Fusion generator.
|
|
748
|
+
"""
|
|
749
|
+
_, tl, err = _get_timeline()
|
|
750
|
+
if err:
|
|
751
|
+
return err
|
|
752
|
+
result = tl.InsertFusionGeneratorIntoTimeline(generator_name)
|
|
753
|
+
return {"success": result is not None}
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@mcp.tool()
|
|
757
|
+
def timeline_insert_fusion_composition() -> Dict[str, Any]:
|
|
758
|
+
"""Insert a Fusion composition into the timeline."""
|
|
759
|
+
_, tl, err = _get_timeline()
|
|
760
|
+
if err:
|
|
761
|
+
return err
|
|
762
|
+
result = tl.InsertFusionCompositionIntoTimeline()
|
|
763
|
+
return {"success": result is not None}
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
@mcp.tool()
|
|
767
|
+
def timeline_insert_ofx_generator(generator_name: str) -> Dict[str, Any]:
|
|
768
|
+
"""Insert an OFX generator into the timeline.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
generator_name: Name of the OFX generator.
|
|
772
|
+
"""
|
|
773
|
+
_, tl, err = _get_timeline()
|
|
774
|
+
if err:
|
|
775
|
+
return err
|
|
776
|
+
result = tl.InsertOFXGeneratorIntoTimeline(generator_name)
|
|
777
|
+
return {"success": result is not None}
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
@mcp.tool()
|
|
781
|
+
def timeline_insert_title(title_name: str) -> Dict[str, Any]:
|
|
782
|
+
"""Insert a title into the timeline.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
title_name: Name of the title to insert.
|
|
786
|
+
"""
|
|
787
|
+
_, tl, err = _get_timeline()
|
|
788
|
+
if err:
|
|
789
|
+
return err
|
|
790
|
+
result = tl.InsertTitleIntoTimeline(title_name)
|
|
791
|
+
return {"success": result is not None}
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@mcp.tool()
|
|
795
|
+
def timeline_insert_fusion_title(title_name: str) -> Dict[str, Any]:
|
|
796
|
+
"""Insert a Fusion title into the timeline.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
title_name: Name of the Fusion title.
|
|
800
|
+
"""
|
|
801
|
+
_, tl, err = _get_timeline()
|
|
802
|
+
if err:
|
|
803
|
+
return err
|
|
804
|
+
result = tl.InsertFusionTitleIntoTimeline(title_name)
|
|
805
|
+
return {"success": result is not None}
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
@mcp.tool()
|
|
809
|
+
def timeline_grab_still() -> Dict[str, Any]:
|
|
810
|
+
"""Grab a still from the current frame of the timeline."""
|
|
811
|
+
_, tl, err = _get_timeline()
|
|
812
|
+
if err:
|
|
813
|
+
return err
|
|
814
|
+
result = tl.GrabStill()
|
|
815
|
+
return {"success": result is not None}
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
@mcp.tool()
|
|
819
|
+
def timeline_grab_all_stills() -> Dict[str, Any]:
|
|
820
|
+
"""Grab stills from all frames at the current position across all timelines."""
|
|
821
|
+
_, tl, err = _get_timeline()
|
|
822
|
+
if err:
|
|
823
|
+
return err
|
|
824
|
+
result = tl.GrabAllStills()
|
|
825
|
+
return {"success": result is not None}
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@mcp.tool()
|
|
829
|
+
def timeline_get_unique_id() -> Dict[str, Any]:
|
|
830
|
+
"""Get the unique ID of the current timeline."""
|
|
831
|
+
_, tl, err = _get_timeline()
|
|
832
|
+
if err:
|
|
833
|
+
return err
|
|
834
|
+
return {"unique_id": tl.GetUniqueId()}
|
|
835
|
+
|
|
836
|
+
|
|
837
|
+
@mcp.tool()
|
|
838
|
+
def timeline_create_subtitles_from_audio(
|
|
839
|
+
language: str = "auto",
|
|
840
|
+
preset: str = "default",
|
|
841
|
+
chars_per_line: Optional[int] = None,
|
|
842
|
+
line_break: Optional[str] = None,
|
|
843
|
+
gap: Optional[int] = None,
|
|
844
|
+
) -> Dict[str, Any]:
|
|
845
|
+
"""Create subtitles from audio in the current timeline.
|
|
846
|
+
|
|
847
|
+
Mirrors Timeline.CreateSubtitlesFromAudio({autoCaptionSettings}) per docs lines 718-761.
|
|
848
|
+
|
|
849
|
+
Args:
|
|
850
|
+
language: 'auto', 'danish', 'dutch', 'english', 'french', 'german', 'italian',
|
|
851
|
+
'japanese', 'korean', 'mandarin_simplified', 'mandarin_traditional',
|
|
852
|
+
'norwegian', 'portuguese', 'russian', 'spanish', 'swedish'. Default: 'auto'.
|
|
853
|
+
preset: 'default', 'teletext', 'netflix'. Default: 'default'.
|
|
854
|
+
chars_per_line: Integer 1-60. Resolve default is 42 (or 16 for Netflix preset).
|
|
855
|
+
line_break: 'single' or 'double'. Default: 'single' on Resolve side.
|
|
856
|
+
gap: Integer 0-10. Default: 0 on Resolve side.
|
|
857
|
+
"""
|
|
858
|
+
r = get_resolve()
|
|
859
|
+
if r is None:
|
|
860
|
+
return {"error": "Not connected to DaVinci Resolve"}
|
|
861
|
+
_, tl, err = _get_timeline()
|
|
862
|
+
if err:
|
|
863
|
+
return err
|
|
864
|
+
settings, settings_err = _build_subtitle_settings(
|
|
865
|
+
r, language=language, preset=preset,
|
|
866
|
+
chars_per_line=chars_per_line, line_break=line_break, gap=gap,
|
|
867
|
+
)
|
|
868
|
+
if settings_err:
|
|
869
|
+
return settings_err
|
|
870
|
+
result = tl.CreateSubtitlesFromAudio(settings)
|
|
871
|
+
return {"success": bool(result)}
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
@mcp.tool()
|
|
875
|
+
def timeline_detect_scene_cuts() -> Dict[str, Any]:
|
|
876
|
+
"""Detect scene cuts in the current timeline."""
|
|
877
|
+
_, tl, err = _get_timeline()
|
|
878
|
+
if err:
|
|
879
|
+
return err
|
|
880
|
+
result = tl.DetectSceneCuts()
|
|
881
|
+
return {"success": bool(result)}
|
|
882
|
+
|
|
883
|
+
|
|
884
|
+
@mcp.tool()
|
|
885
|
+
def timeline_convert_to_stereo() -> Dict[str, Any]:
|
|
886
|
+
"""Convert the current timeline to stereo."""
|
|
887
|
+
_, tl, err = _get_timeline()
|
|
888
|
+
if err:
|
|
889
|
+
return err
|
|
890
|
+
result = tl.ConvertTimelineToStereo()
|
|
891
|
+
return {"success": bool(result)}
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
@mcp.tool()
|
|
895
|
+
def timeline_get_node_graph() -> Dict[str, Any]:
|
|
896
|
+
"""Get the node graph for the current timeline."""
|
|
897
|
+
_, tl, err = _get_timeline()
|
|
898
|
+
if err:
|
|
899
|
+
return err
|
|
900
|
+
graph = tl.GetNodeGraph()
|
|
901
|
+
if graph:
|
|
902
|
+
return {"has_graph": True, "num_nodes": graph.GetNumNodes()}
|
|
903
|
+
return {"has_graph": False}
|
|
904
|
+
|
|
905
|
+
|
|
906
|
+
@mcp.tool()
|
|
907
|
+
def timeline_analyze_dolby_vision() -> Dict[str, Any]:
|
|
908
|
+
"""Analyze Dolby Vision for the current timeline."""
|
|
909
|
+
_, tl, err = _get_timeline()
|
|
910
|
+
if err:
|
|
911
|
+
return err
|
|
912
|
+
result = tl.AnalyzeDolbyVision()
|
|
913
|
+
return {"success": bool(result)}
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
@mcp.tool()
|
|
917
|
+
def timeline_get_current_video_item() -> Dict[str, Any]:
|
|
918
|
+
"""Get the current video item at the playhead."""
|
|
919
|
+
_, tl, err = _get_timeline()
|
|
920
|
+
if err:
|
|
921
|
+
return err
|
|
922
|
+
item = tl.GetCurrentVideoItem()
|
|
923
|
+
if item:
|
|
924
|
+
return {"name": item.GetName(), "unique_id": item.GetUniqueId(), "start": item.GetStart(), "end": item.GetEnd()}
|
|
925
|
+
return {"item": None}
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
@mcp.tool()
|
|
929
|
+
def timeline_get_current_clip_thumbnail(width: int = 320, height: int = 180) -> Dict[str, Any]:
|
|
930
|
+
"""Get thumbnail image data for the current clip.
|
|
931
|
+
|
|
932
|
+
Args:
|
|
933
|
+
width: Thumbnail width. Default: 320.
|
|
934
|
+
height: Thumbnail height. Default: 180.
|
|
935
|
+
"""
|
|
936
|
+
_, tl, err = _get_timeline()
|
|
937
|
+
if err:
|
|
938
|
+
return err
|
|
939
|
+
result = tl.GetCurrentClipThumbnailImage()
|
|
940
|
+
if result:
|
|
941
|
+
return {"success": True, "has_data": bool(result)}
|
|
942
|
+
return {"success": False}
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
@mcp.tool()
|
|
946
|
+
def timeline_get_media_pool_item() -> Dict[str, Any]:
|
|
947
|
+
"""Get the MediaPoolItem for the current timeline."""
|
|
948
|
+
_, tl, err = _get_timeline()
|
|
949
|
+
if err:
|
|
950
|
+
return err
|
|
951
|
+
mpi = tl.GetMediaPoolItem()
|
|
952
|
+
if mpi:
|
|
953
|
+
return {"name": mpi.GetName(), "unique_id": mpi.GetUniqueId()}
|
|
954
|
+
return {"media_pool_item": None}
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
@mcp.tool()
|
|
958
|
+
def timeline_get_mark_in_out() -> Dict[str, Any]:
|
|
959
|
+
"""Get mark in/out points for the current timeline."""
|
|
960
|
+
_, tl, err = _get_timeline()
|
|
961
|
+
if err:
|
|
962
|
+
return err
|
|
963
|
+
result = tl.GetMarkInOut()
|
|
964
|
+
return result if result else {}
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
@mcp.tool()
|
|
968
|
+
def timeline_set_mark_in_out(mark_in: int, mark_out: int) -> Dict[str, Any]:
|
|
969
|
+
"""Set mark in/out points for the current timeline.
|
|
970
|
+
|
|
971
|
+
Args:
|
|
972
|
+
mark_in: Mark in frame number.
|
|
973
|
+
mark_out: Mark out frame number.
|
|
974
|
+
"""
|
|
975
|
+
_, tl, err = _get_timeline()
|
|
976
|
+
if err:
|
|
977
|
+
return err
|
|
978
|
+
result = tl.SetMarkInOut(mark_in, mark_out)
|
|
979
|
+
return {"success": bool(result)}
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
@mcp.tool()
|
|
983
|
+
def timeline_clear_mark_in_out() -> Dict[str, Any]:
|
|
984
|
+
"""Clear mark in/out points for the current timeline."""
|
|
985
|
+
_, tl, err = _get_timeline()
|
|
986
|
+
if err:
|
|
987
|
+
return err
|
|
988
|
+
result = tl.ClearMarkInOut()
|
|
989
|
+
return {"success": bool(result)}
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
@mcp.tool()
|
|
993
|
+
def create_timeline_from_clips(
|
|
994
|
+
name: str,
|
|
995
|
+
clip_ids: Optional[List[str]] = None,
|
|
996
|
+
clip_infos: Optional[List[Dict[str, Any]]] = None,
|
|
997
|
+
) -> Dict[str, Any]:
|
|
998
|
+
"""Create a new timeline from specified media pool clips.
|
|
999
|
+
|
|
1000
|
+
Args:
|
|
1001
|
+
name: Name for the new timeline.
|
|
1002
|
+
clip_ids: Simple form — list of MediaPoolItem unique IDs to append end-to-end.
|
|
1003
|
+
clip_infos: Positioned form — list of dicts with keys clip_id (or
|
|
1004
|
+
media_pool_item_id), start_frame, end_frame, record_frame.
|
|
1005
|
+
record_frame is relative to the created timeline start frame by
|
|
1006
|
+
default; pass record_frame_mode="absolute" for raw Resolve values.
|
|
1007
|
+
If both are None, uses the currently selected media pool clips.
|
|
1008
|
+
"""
|
|
1009
|
+
project, mp, err = _get_mp()
|
|
1010
|
+
if err:
|
|
1011
|
+
return err
|
|
1012
|
+
if clip_infos is not None:
|
|
1013
|
+
if not isinstance(clip_infos, list) or not clip_infos:
|
|
1014
|
+
return {"error": "clip_infos must be a non-empty list"}
|
|
1015
|
+
root = mp.GetRootFolder()
|
|
1016
|
+
for i, ci in enumerate(clip_infos):
|
|
1017
|
+
_, row_err = _build_create_clip_info_dict(root, ci, i)
|
|
1018
|
+
if row_err:
|
|
1019
|
+
return row_err
|
|
1020
|
+
tl = mp.CreateEmptyTimeline(name)
|
|
1021
|
+
if not tl:
|
|
1022
|
+
return {"success": False, "error": "Failed to create timeline"}
|
|
1023
|
+
try:
|
|
1024
|
+
if project:
|
|
1025
|
+
project.SetCurrentTimeline(tl)
|
|
1026
|
+
except Exception:
|
|
1027
|
+
logger.debug("Could not set newly created timeline current", exc_info=True)
|
|
1028
|
+
timeline_start = _timeline_start_frame(tl)
|
|
1029
|
+
built = []
|
|
1030
|
+
for i, ci in enumerate(clip_infos):
|
|
1031
|
+
append_ci = dict(ci)
|
|
1032
|
+
append_ci.setdefault("track_index", append_ci.get("trackIndex", 1))
|
|
1033
|
+
row, row_err = _build_append_clip_info_dict(root, append_ci, i, timeline_start)
|
|
1034
|
+
if row_err:
|
|
1035
|
+
return row_err
|
|
1036
|
+
built.append(row)
|
|
1037
|
+
appended = mp.AppendToTimeline(built)
|
|
1038
|
+
if not appended:
|
|
1039
|
+
return {"success": False, "error": "Failed to append clip_infos to created timeline"}
|
|
1040
|
+
elif clip_ids:
|
|
1041
|
+
root = mp.GetRootFolder()
|
|
1042
|
+
clips = []
|
|
1043
|
+
for cid in clip_ids:
|
|
1044
|
+
clip = _find_clip_by_id(root, cid)
|
|
1045
|
+
if clip:
|
|
1046
|
+
clips.append(clip)
|
|
1047
|
+
else:
|
|
1048
|
+
return {"error": f"Clip not found: {cid}"}
|
|
1049
|
+
tl = mp.CreateTimelineFromClips(name, clips)
|
|
1050
|
+
else:
|
|
1051
|
+
selected = mp.GetSelectedClips()
|
|
1052
|
+
if not selected:
|
|
1053
|
+
return {"error": "No clips specified and no clips selected in media pool"}
|
|
1054
|
+
tl = mp.CreateTimelineFromClips(name, selected)
|
|
1055
|
+
if tl:
|
|
1056
|
+
return {"success": True, "timeline_name": tl.GetName(), "timeline_id": tl.GetUniqueId()}
|
|
1057
|
+
return {"success": False, "error": "Failed to create timeline from clips"}
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
@mcp.tool()
|
|
1061
|
+
def set_timeline_setting(setting_name: str, setting_value: str) -> Dict[str, Any]:
|
|
1062
|
+
"""Set a timeline setting value.
|
|
1063
|
+
|
|
1064
|
+
Args:
|
|
1065
|
+
setting_name: Name of the timeline setting to set (e.g. 'useCustomSettings', 'timelineFrameRate',
|
|
1066
|
+
'timelineResolutionWidth', 'timelineResolutionHeight', 'timelineOutputResolutionWidth',
|
|
1067
|
+
'timelineOutputResolutionHeight', 'colorSpaceTimeline', 'colorSpaceOutput').
|
|
1068
|
+
setting_value: Value to set for the setting (string).
|
|
1069
|
+
"""
|
|
1070
|
+
_, tl, err = _get_timeline()
|
|
1071
|
+
if err:
|
|
1072
|
+
return err
|
|
1073
|
+
result = tl.SetSetting(setting_name, setting_value)
|
|
1074
|
+
return {"success": bool(result), "setting_name": setting_name, "setting_value": setting_value}
|