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,2251 @@
|
|
|
1
|
+
"""Timeline item property, keyframe, and clip-level tools."""
|
|
2
|
+
|
|
3
|
+
from src.granular.common import * # noqa: F401,F403
|
|
4
|
+
|
|
5
|
+
resolve = ResolveProxy()
|
|
6
|
+
|
|
7
|
+
@mcp.resource("resolve://timeline-item/{timeline_item_id}")
|
|
8
|
+
def get_timeline_item_properties(timeline_item_id: str) -> Dict[str, Any]:
|
|
9
|
+
"""Get properties of a specific timeline item by ID.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
timeline_item_id: The ID of the timeline item to get properties for
|
|
13
|
+
"""
|
|
14
|
+
pm, current_project = get_current_project()
|
|
15
|
+
if not current_project:
|
|
16
|
+
return {"error": "No project currently open"}
|
|
17
|
+
|
|
18
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
19
|
+
if not current_timeline:
|
|
20
|
+
return {"error": "No timeline currently active"}
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
# Find the timeline item by ID
|
|
24
|
+
# We'll need to get all items from all tracks and check their IDs
|
|
25
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
26
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
27
|
+
|
|
28
|
+
timeline_item = None
|
|
29
|
+
|
|
30
|
+
# Search video tracks
|
|
31
|
+
for track_index in range(1, video_track_count + 1):
|
|
32
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
33
|
+
if items:
|
|
34
|
+
for item in items:
|
|
35
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
36
|
+
timeline_item = item
|
|
37
|
+
break
|
|
38
|
+
if timeline_item:
|
|
39
|
+
break
|
|
40
|
+
|
|
41
|
+
# If not found, search audio tracks
|
|
42
|
+
if not timeline_item:
|
|
43
|
+
for track_index in range(1, audio_track_count + 1):
|
|
44
|
+
items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
45
|
+
if items:
|
|
46
|
+
for item in items:
|
|
47
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
48
|
+
timeline_item = item
|
|
49
|
+
break
|
|
50
|
+
if timeline_item:
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
if not timeline_item:
|
|
54
|
+
return {"error": f"Timeline item with ID '{timeline_item_id}' not found"}
|
|
55
|
+
|
|
56
|
+
# Get basic properties
|
|
57
|
+
properties = {
|
|
58
|
+
"id": timeline_item_id,
|
|
59
|
+
"name": timeline_item.GetName(),
|
|
60
|
+
"type": timeline_item.GetType(),
|
|
61
|
+
"start_frame": timeline_item.GetStart(),
|
|
62
|
+
"end_frame": timeline_item.GetEnd(),
|
|
63
|
+
"duration": timeline_item.GetDuration()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# Get additional properties if it's a video item
|
|
67
|
+
if timeline_item.GetType() == "Video":
|
|
68
|
+
# Transform properties
|
|
69
|
+
properties["transform"] = {
|
|
70
|
+
"position": {
|
|
71
|
+
"x": timeline_item.GetProperty("Pan"),
|
|
72
|
+
"y": timeline_item.GetProperty("Tilt")
|
|
73
|
+
},
|
|
74
|
+
"zoom": timeline_item.GetProperty("ZoomX"), # ZoomX/ZoomY can be different for non-uniform scaling
|
|
75
|
+
"zoom_x": timeline_item.GetProperty("ZoomX"),
|
|
76
|
+
"zoom_y": timeline_item.GetProperty("ZoomY"),
|
|
77
|
+
"rotation": timeline_item.GetProperty("Rotation"),
|
|
78
|
+
"anchor_point": {
|
|
79
|
+
"x": timeline_item.GetProperty("AnchorPointX"),
|
|
80
|
+
"y": timeline_item.GetProperty("AnchorPointY")
|
|
81
|
+
},
|
|
82
|
+
"pitch": timeline_item.GetProperty("Pitch"),
|
|
83
|
+
"yaw": timeline_item.GetProperty("Yaw")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Crop properties
|
|
87
|
+
properties["crop"] = {
|
|
88
|
+
"left": timeline_item.GetProperty("CropLeft"),
|
|
89
|
+
"right": timeline_item.GetProperty("CropRight"),
|
|
90
|
+
"top": timeline_item.GetProperty("CropTop"),
|
|
91
|
+
"bottom": timeline_item.GetProperty("CropBottom")
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Composite properties
|
|
95
|
+
properties["composite"] = {
|
|
96
|
+
"mode": timeline_item.GetProperty("CompositeMode"),
|
|
97
|
+
"opacity": timeline_item.GetProperty("Opacity")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Dynamic zoom properties
|
|
101
|
+
properties["dynamic_zoom"] = {
|
|
102
|
+
"enabled": timeline_item.GetProperty("DynamicZoomEnable"),
|
|
103
|
+
"mode": timeline_item.GetProperty("DynamicZoomMode")
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Retime properties
|
|
107
|
+
properties["retime"] = {
|
|
108
|
+
"speed": timeline_item.GetProperty("Speed"),
|
|
109
|
+
"process": timeline_item.GetProperty("RetimeProcess")
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Stabilization properties
|
|
113
|
+
properties["stabilization"] = {
|
|
114
|
+
"enabled": timeline_item.GetProperty("StabilizationEnable"),
|
|
115
|
+
"method": timeline_item.GetProperty("StabilizationMethod"),
|
|
116
|
+
"strength": timeline_item.GetProperty("StabilizationStrength")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Audio-specific properties
|
|
120
|
+
if timeline_item.GetType() == "Audio" or timeline_item.GetMediaType() == "Audio":
|
|
121
|
+
properties["audio"] = {
|
|
122
|
+
"volume": timeline_item.GetProperty("Volume"),
|
|
123
|
+
"pan": timeline_item.GetProperty("Pan"),
|
|
124
|
+
"eq_enabled": timeline_item.GetProperty("EQEnable"),
|
|
125
|
+
"normalize_enabled": timeline_item.GetProperty("NormalizeEnable"),
|
|
126
|
+
"normalize_level": timeline_item.GetProperty("NormalizeLevel")
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return properties
|
|
130
|
+
|
|
131
|
+
except Exception as e:
|
|
132
|
+
return {"error": f"Error getting timeline item properties: {str(e)}"}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.resource("resolve://timeline-items")
|
|
136
|
+
def get_timeline_items() -> List[Dict[str, Any]]:
|
|
137
|
+
"""Get all items in the current timeline with their IDs and basic properties."""
|
|
138
|
+
pm, current_project = get_current_project()
|
|
139
|
+
if not current_project:
|
|
140
|
+
return [{"error": "No project currently open"}]
|
|
141
|
+
|
|
142
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
143
|
+
if not current_timeline:
|
|
144
|
+
return [{"error": "No timeline currently active"}]
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# Get all tracks in the timeline
|
|
148
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
149
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
150
|
+
|
|
151
|
+
items = []
|
|
152
|
+
|
|
153
|
+
# Process video tracks
|
|
154
|
+
for track_index in range(1, video_track_count + 1):
|
|
155
|
+
track_items = current_timeline.GetItemListInTrack("video", track_index)
|
|
156
|
+
if track_items:
|
|
157
|
+
for item in track_items:
|
|
158
|
+
items.append({
|
|
159
|
+
"id": str(item.GetUniqueId()),
|
|
160
|
+
"name": item.GetName(),
|
|
161
|
+
"type": "video",
|
|
162
|
+
"track": track_index,
|
|
163
|
+
"start_frame": item.GetStart(),
|
|
164
|
+
"end_frame": item.GetEnd(),
|
|
165
|
+
"duration": item.GetDuration()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
# Process audio tracks
|
|
169
|
+
for track_index in range(1, audio_track_count + 1):
|
|
170
|
+
track_items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
171
|
+
if track_items:
|
|
172
|
+
for item in track_items:
|
|
173
|
+
items.append({
|
|
174
|
+
"id": str(item.GetUniqueId()),
|
|
175
|
+
"name": item.GetName(),
|
|
176
|
+
"type": "audio",
|
|
177
|
+
"track": track_index,
|
|
178
|
+
"start_frame": item.GetStart(),
|
|
179
|
+
"end_frame": item.GetEnd(),
|
|
180
|
+
"duration": item.GetDuration()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
if not items:
|
|
184
|
+
return [{"info": "No items found in the current timeline"}]
|
|
185
|
+
|
|
186
|
+
return items
|
|
187
|
+
except Exception as e:
|
|
188
|
+
return [{"error": f"Error listing timeline items: {str(e)}"}]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@mcp.tool(annotations=DESTRUCTIVE_TOOL)
|
|
192
|
+
def set_timeline_item_transform(timeline_item_id: str,
|
|
193
|
+
property_name: str,
|
|
194
|
+
property_value: float) -> str:
|
|
195
|
+
"""Set a transform property for a timeline item.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
timeline_item_id: The ID of the timeline item to modify
|
|
199
|
+
property_name: The name of the property to set. Options include:
|
|
200
|
+
'Pan', 'Tilt', 'ZoomX', 'ZoomY', 'Rotation', 'AnchorPointX',
|
|
201
|
+
'AnchorPointY', 'Pitch', 'Yaw'
|
|
202
|
+
property_value: The value to set for the property
|
|
203
|
+
"""
|
|
204
|
+
pm, current_project = get_current_project()
|
|
205
|
+
if not current_project:
|
|
206
|
+
return "Error: No project currently open"
|
|
207
|
+
|
|
208
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
209
|
+
if not current_timeline:
|
|
210
|
+
return "Error: No timeline currently active"
|
|
211
|
+
|
|
212
|
+
# Validate property name
|
|
213
|
+
valid_properties = [
|
|
214
|
+
'Pan', 'Tilt', 'ZoomX', 'ZoomY', 'Rotation',
|
|
215
|
+
'AnchorPointX', 'AnchorPointY', 'Pitch', 'Yaw'
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
if property_name not in valid_properties:
|
|
219
|
+
return f"Error: Invalid property name. Must be one of: {', '.join(valid_properties)}"
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
# Find the timeline item by ID
|
|
223
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
224
|
+
|
|
225
|
+
timeline_item = None
|
|
226
|
+
|
|
227
|
+
# Search video tracks
|
|
228
|
+
for track_index in range(1, video_track_count + 1):
|
|
229
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
230
|
+
if items:
|
|
231
|
+
for item in items:
|
|
232
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
233
|
+
timeline_item = item
|
|
234
|
+
break
|
|
235
|
+
if timeline_item:
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
if not timeline_item:
|
|
239
|
+
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
240
|
+
|
|
241
|
+
if timeline_item.GetType() != "Video":
|
|
242
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
243
|
+
|
|
244
|
+
# Set the property
|
|
245
|
+
result = timeline_item.SetProperty(property_name, property_value)
|
|
246
|
+
if result:
|
|
247
|
+
return f"Successfully set {property_name} to {property_value} for timeline item '{timeline_item.GetName()}'"
|
|
248
|
+
else:
|
|
249
|
+
return f"Failed to set {property_name} for timeline item '{timeline_item.GetName()}'"
|
|
250
|
+
except Exception as e:
|
|
251
|
+
return f"Error setting timeline item property: {str(e)}"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@mcp.tool()
|
|
255
|
+
def set_timeline_item_crop(timeline_item_id: str,
|
|
256
|
+
crop_type: str,
|
|
257
|
+
crop_value: float) -> str:
|
|
258
|
+
"""Set a crop property for a timeline item.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
timeline_item_id: The ID of the timeline item to modify
|
|
262
|
+
crop_type: The type of crop to set. Options: 'Left', 'Right', 'Top', 'Bottom'
|
|
263
|
+
crop_value: The value to set for the crop (typically 0.0 to 1.0)
|
|
264
|
+
"""
|
|
265
|
+
pm, current_project = get_current_project()
|
|
266
|
+
if not current_project:
|
|
267
|
+
return "Error: No project currently open"
|
|
268
|
+
|
|
269
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
270
|
+
if not current_timeline:
|
|
271
|
+
return "Error: No timeline currently active"
|
|
272
|
+
|
|
273
|
+
# Validate crop type
|
|
274
|
+
valid_crop_types = ['Left', 'Right', 'Top', 'Bottom']
|
|
275
|
+
|
|
276
|
+
if crop_type not in valid_crop_types:
|
|
277
|
+
return f"Error: Invalid crop type. Must be one of: {', '.join(valid_crop_types)}"
|
|
278
|
+
|
|
279
|
+
property_name = f"Crop{crop_type}"
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
# Find the timeline item by ID
|
|
283
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
284
|
+
|
|
285
|
+
timeline_item = None
|
|
286
|
+
|
|
287
|
+
# Search video tracks
|
|
288
|
+
for track_index in range(1, video_track_count + 1):
|
|
289
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
290
|
+
if items:
|
|
291
|
+
for item in items:
|
|
292
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
293
|
+
timeline_item = item
|
|
294
|
+
break
|
|
295
|
+
if timeline_item:
|
|
296
|
+
break
|
|
297
|
+
|
|
298
|
+
if not timeline_item:
|
|
299
|
+
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
300
|
+
|
|
301
|
+
if timeline_item.GetType() != "Video":
|
|
302
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
303
|
+
|
|
304
|
+
# Set the property
|
|
305
|
+
result = timeline_item.SetProperty(property_name, crop_value)
|
|
306
|
+
if result:
|
|
307
|
+
return f"Successfully set crop {crop_type.lower()} to {crop_value} for timeline item '{timeline_item.GetName()}'"
|
|
308
|
+
else:
|
|
309
|
+
return f"Failed to set crop {crop_type.lower()} for timeline item '{timeline_item.GetName()}'"
|
|
310
|
+
except Exception as e:
|
|
311
|
+
return f"Error setting timeline item crop: {str(e)}"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@mcp.tool()
|
|
315
|
+
def set_timeline_item_composite(timeline_item_id: str,
|
|
316
|
+
composite_mode: str = None,
|
|
317
|
+
opacity: float = None) -> str:
|
|
318
|
+
"""Set composite properties for a timeline item.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
timeline_item_id: The ID of the timeline item to modify
|
|
322
|
+
composite_mode: Optional composite mode to set (e.g., 'Normal', 'Add', 'Multiply')
|
|
323
|
+
opacity: Optional opacity value to set (0.0 to 1.0)
|
|
324
|
+
"""
|
|
325
|
+
pm, current_project = get_current_project()
|
|
326
|
+
if not current_project:
|
|
327
|
+
return "Error: No project currently open"
|
|
328
|
+
|
|
329
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
330
|
+
if not current_timeline:
|
|
331
|
+
return "Error: No timeline currently active"
|
|
332
|
+
|
|
333
|
+
# Validate inputs
|
|
334
|
+
if composite_mode is None and opacity is None:
|
|
335
|
+
return "Error: Must specify at least one of composite_mode or opacity"
|
|
336
|
+
|
|
337
|
+
# Valid composite modes
|
|
338
|
+
valid_composite_modes = [
|
|
339
|
+
'Normal', 'Add', 'Subtract', 'Difference', 'Multiply', 'Screen',
|
|
340
|
+
'Overlay', 'Hardlight', 'Softlight', 'Darken', 'Lighten', 'ColorDodge',
|
|
341
|
+
'ColorBurn', 'Exclusion', 'Hue', 'Saturation', 'Color', 'Luminosity'
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
if composite_mode and composite_mode not in valid_composite_modes:
|
|
345
|
+
return f"Error: Invalid composite mode. Must be one of: {', '.join(valid_composite_modes)}"
|
|
346
|
+
|
|
347
|
+
if opacity is not None and (opacity < 0.0 or opacity > 1.0):
|
|
348
|
+
return "Error: Opacity must be between 0.0 and 1.0"
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
# Find the timeline item by ID
|
|
352
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
353
|
+
|
|
354
|
+
timeline_item = None
|
|
355
|
+
|
|
356
|
+
# Search video tracks
|
|
357
|
+
for track_index in range(1, video_track_count + 1):
|
|
358
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
359
|
+
if items:
|
|
360
|
+
for item in items:
|
|
361
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
362
|
+
timeline_item = item
|
|
363
|
+
break
|
|
364
|
+
if timeline_item:
|
|
365
|
+
break
|
|
366
|
+
|
|
367
|
+
if not timeline_item:
|
|
368
|
+
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
369
|
+
|
|
370
|
+
if timeline_item.GetType() != "Video":
|
|
371
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
372
|
+
|
|
373
|
+
success = True
|
|
374
|
+
|
|
375
|
+
# Set composite mode if specified
|
|
376
|
+
if composite_mode:
|
|
377
|
+
result = timeline_item.SetProperty("CompositeMode", composite_mode)
|
|
378
|
+
if not result:
|
|
379
|
+
success = False
|
|
380
|
+
|
|
381
|
+
# Set opacity if specified
|
|
382
|
+
if opacity is not None:
|
|
383
|
+
result = timeline_item.SetProperty("Opacity", opacity)
|
|
384
|
+
if not result:
|
|
385
|
+
success = False
|
|
386
|
+
|
|
387
|
+
if success:
|
|
388
|
+
changes = []
|
|
389
|
+
if composite_mode:
|
|
390
|
+
changes.append(f"composite mode to '{composite_mode}'")
|
|
391
|
+
if opacity is not None:
|
|
392
|
+
changes.append(f"opacity to {opacity}")
|
|
393
|
+
|
|
394
|
+
return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
|
|
395
|
+
else:
|
|
396
|
+
return f"Failed to set some composite properties for timeline item '{timeline_item.GetName()}'"
|
|
397
|
+
except Exception as e:
|
|
398
|
+
return f"Error setting timeline item composite properties: {str(e)}"
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
@mcp.tool()
|
|
402
|
+
def set_timeline_item_retime(timeline_item_id: str,
|
|
403
|
+
speed: float = None,
|
|
404
|
+
process: str = None) -> str:
|
|
405
|
+
"""Set retiming properties for a timeline item.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
timeline_item_id: The ID of the timeline item to modify
|
|
409
|
+
speed: Optional speed factor (e.g., 0.5 for 50%, 2.0 for 200%)
|
|
410
|
+
process: Optional retime process. Options: 'NearestFrame', 'FrameBlend', 'OpticalFlow'
|
|
411
|
+
"""
|
|
412
|
+
pm, current_project = get_current_project()
|
|
413
|
+
if not current_project:
|
|
414
|
+
return "Error: No project currently open"
|
|
415
|
+
|
|
416
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
417
|
+
if not current_timeline:
|
|
418
|
+
return "Error: No timeline currently active"
|
|
419
|
+
|
|
420
|
+
# Validate inputs
|
|
421
|
+
if speed is None and process is None:
|
|
422
|
+
return "Error: Must specify at least one of speed or process"
|
|
423
|
+
|
|
424
|
+
if speed is not None and speed <= 0:
|
|
425
|
+
return "Error: Speed must be greater than 0"
|
|
426
|
+
|
|
427
|
+
valid_processes = ['NearestFrame', 'FrameBlend', 'OpticalFlow']
|
|
428
|
+
if process and process not in valid_processes:
|
|
429
|
+
return f"Error: Invalid retime process. Must be one of: {', '.join(valid_processes)}"
|
|
430
|
+
|
|
431
|
+
try:
|
|
432
|
+
# Find the timeline item by ID
|
|
433
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
434
|
+
|
|
435
|
+
timeline_item = None
|
|
436
|
+
|
|
437
|
+
# Search video tracks
|
|
438
|
+
for track_index in range(1, video_track_count + 1):
|
|
439
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
440
|
+
if items:
|
|
441
|
+
for item in items:
|
|
442
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
443
|
+
timeline_item = item
|
|
444
|
+
break
|
|
445
|
+
if timeline_item:
|
|
446
|
+
break
|
|
447
|
+
|
|
448
|
+
if not timeline_item:
|
|
449
|
+
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
450
|
+
|
|
451
|
+
success = True
|
|
452
|
+
|
|
453
|
+
# Set speed if specified
|
|
454
|
+
if speed is not None:
|
|
455
|
+
result = timeline_item.SetProperty("Speed", speed)
|
|
456
|
+
if not result:
|
|
457
|
+
success = False
|
|
458
|
+
|
|
459
|
+
# Set retime process if specified
|
|
460
|
+
if process:
|
|
461
|
+
result = timeline_item.SetProperty("RetimeProcess", process)
|
|
462
|
+
if not result:
|
|
463
|
+
success = False
|
|
464
|
+
|
|
465
|
+
if success:
|
|
466
|
+
changes = []
|
|
467
|
+
if speed is not None:
|
|
468
|
+
changes.append(f"speed to {speed}x")
|
|
469
|
+
if process:
|
|
470
|
+
changes.append(f"retime process to '{process}'")
|
|
471
|
+
|
|
472
|
+
return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
|
|
473
|
+
else:
|
|
474
|
+
return f"Failed to set some retime properties for timeline item '{timeline_item.GetName()}'"
|
|
475
|
+
except Exception as e:
|
|
476
|
+
return f"Error setting timeline item retime properties: {str(e)}"
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@mcp.tool()
|
|
480
|
+
def set_timeline_item_stabilization(timeline_item_id: str,
|
|
481
|
+
enabled: bool = None,
|
|
482
|
+
method: str = None,
|
|
483
|
+
strength: float = None) -> str:
|
|
484
|
+
"""Set stabilization properties for a timeline item.
|
|
485
|
+
|
|
486
|
+
Args:
|
|
487
|
+
timeline_item_id: The ID of the timeline item to modify
|
|
488
|
+
enabled: Optional boolean to enable/disable stabilization
|
|
489
|
+
method: Optional stabilization method. Options: 'Perspective', 'Similarity', 'Translation'
|
|
490
|
+
strength: Optional strength value (0.0 to 1.0)
|
|
491
|
+
"""
|
|
492
|
+
pm, current_project = get_current_project()
|
|
493
|
+
if not current_project:
|
|
494
|
+
return "Error: No project currently open"
|
|
495
|
+
|
|
496
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
497
|
+
if not current_timeline:
|
|
498
|
+
return "Error: No timeline currently active"
|
|
499
|
+
|
|
500
|
+
# Validate inputs
|
|
501
|
+
if enabled is None and method is None and strength is None:
|
|
502
|
+
return "Error: Must specify at least one parameter to modify"
|
|
503
|
+
|
|
504
|
+
valid_methods = ['Perspective', 'Similarity', 'Translation']
|
|
505
|
+
if method and method not in valid_methods:
|
|
506
|
+
return f"Error: Invalid stabilization method. Must be one of: {', '.join(valid_methods)}"
|
|
507
|
+
|
|
508
|
+
if strength is not None and (strength < 0.0 or strength > 1.0):
|
|
509
|
+
return "Error: Strength must be between 0.0 and 1.0"
|
|
510
|
+
|
|
511
|
+
try:
|
|
512
|
+
# Find the timeline item by ID
|
|
513
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
514
|
+
|
|
515
|
+
timeline_item = None
|
|
516
|
+
|
|
517
|
+
# Search video tracks
|
|
518
|
+
for track_index in range(1, video_track_count + 1):
|
|
519
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
520
|
+
if items:
|
|
521
|
+
for item in items:
|
|
522
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
523
|
+
timeline_item = item
|
|
524
|
+
break
|
|
525
|
+
if timeline_item:
|
|
526
|
+
break
|
|
527
|
+
|
|
528
|
+
if not timeline_item:
|
|
529
|
+
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
530
|
+
|
|
531
|
+
if timeline_item.GetType() != "Video":
|
|
532
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
533
|
+
|
|
534
|
+
success = True
|
|
535
|
+
|
|
536
|
+
# Set enabled if specified
|
|
537
|
+
if enabled is not None:
|
|
538
|
+
result = timeline_item.SetProperty("StabilizationEnable", 1 if enabled else 0)
|
|
539
|
+
if not result:
|
|
540
|
+
success = False
|
|
541
|
+
|
|
542
|
+
# Set method if specified
|
|
543
|
+
if method:
|
|
544
|
+
result = timeline_item.SetProperty("StabilizationMethod", method)
|
|
545
|
+
if not result:
|
|
546
|
+
success = False
|
|
547
|
+
|
|
548
|
+
# Set strength if specified
|
|
549
|
+
if strength is not None:
|
|
550
|
+
result = timeline_item.SetProperty("StabilizationStrength", strength)
|
|
551
|
+
if not result:
|
|
552
|
+
success = False
|
|
553
|
+
|
|
554
|
+
if success:
|
|
555
|
+
changes = []
|
|
556
|
+
if enabled is not None:
|
|
557
|
+
changes.append(f"stabilization {'enabled' if enabled else 'disabled'}")
|
|
558
|
+
if method:
|
|
559
|
+
changes.append(f"stabilization method to '{method}'")
|
|
560
|
+
if strength is not None:
|
|
561
|
+
changes.append(f"stabilization strength to {strength}")
|
|
562
|
+
|
|
563
|
+
return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
|
|
564
|
+
else:
|
|
565
|
+
return f"Failed to set some stabilization properties for timeline item '{timeline_item.GetName()}'"
|
|
566
|
+
except Exception as e:
|
|
567
|
+
return f"Error setting timeline item stabilization properties: {str(e)}"
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
@mcp.tool()
|
|
571
|
+
def set_timeline_item_audio(timeline_item_id: str,
|
|
572
|
+
volume: float = None,
|
|
573
|
+
pan: float = None,
|
|
574
|
+
eq_enabled: bool = None) -> str:
|
|
575
|
+
"""Set audio properties for a timeline item.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
timeline_item_id: The ID of the timeline item to modify
|
|
579
|
+
volume: Optional volume level (usually 0.0 to 2.0, where 1.0 is unity gain)
|
|
580
|
+
pan: Optional pan value (-1.0 to 1.0, where -1.0 is left, 0 is center, 1.0 is right)
|
|
581
|
+
eq_enabled: Optional boolean to enable/disable EQ
|
|
582
|
+
"""
|
|
583
|
+
pm, current_project = get_current_project()
|
|
584
|
+
if not current_project:
|
|
585
|
+
return "Error: No project currently open"
|
|
586
|
+
|
|
587
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
588
|
+
if not current_timeline:
|
|
589
|
+
return "Error: No timeline currently active"
|
|
590
|
+
|
|
591
|
+
# Validate inputs
|
|
592
|
+
if volume is None and pan is None and eq_enabled is None:
|
|
593
|
+
return "Error: Must specify at least one parameter to modify"
|
|
594
|
+
|
|
595
|
+
if volume is not None and volume < 0.0:
|
|
596
|
+
return "Error: Volume must be greater than or equal to 0.0"
|
|
597
|
+
|
|
598
|
+
if pan is not None and (pan < -1.0 or pan > 1.0):
|
|
599
|
+
return "Error: Pan must be between -1.0 and 1.0"
|
|
600
|
+
|
|
601
|
+
try:
|
|
602
|
+
# Find the timeline item by ID
|
|
603
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
604
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
605
|
+
|
|
606
|
+
timeline_item = None
|
|
607
|
+
is_audio = False
|
|
608
|
+
|
|
609
|
+
# Search audio tracks first
|
|
610
|
+
for track_index in range(1, audio_track_count + 1):
|
|
611
|
+
items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
612
|
+
if items:
|
|
613
|
+
for item in items:
|
|
614
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
615
|
+
timeline_item = item
|
|
616
|
+
is_audio = True
|
|
617
|
+
break
|
|
618
|
+
if timeline_item:
|
|
619
|
+
break
|
|
620
|
+
|
|
621
|
+
# If not found in audio tracks, search video tracks (might be a video clip with audio)
|
|
622
|
+
if not timeline_item:
|
|
623
|
+
for track_index in range(1, video_track_count + 1):
|
|
624
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
625
|
+
if items:
|
|
626
|
+
for item in items:
|
|
627
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
628
|
+
timeline_item = item
|
|
629
|
+
break
|
|
630
|
+
if timeline_item:
|
|
631
|
+
break
|
|
632
|
+
|
|
633
|
+
if not timeline_item:
|
|
634
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' not found"
|
|
635
|
+
|
|
636
|
+
# Check if the item has audio capabilities
|
|
637
|
+
if not is_audio and timeline_item.GetMediaType() != "Audio":
|
|
638
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' does not have audio properties"
|
|
639
|
+
|
|
640
|
+
success = True
|
|
641
|
+
|
|
642
|
+
# Set volume if specified
|
|
643
|
+
if volume is not None:
|
|
644
|
+
result = timeline_item.SetProperty("Volume", volume)
|
|
645
|
+
if not result:
|
|
646
|
+
success = False
|
|
647
|
+
|
|
648
|
+
# Set pan if specified
|
|
649
|
+
if pan is not None:
|
|
650
|
+
result = timeline_item.SetProperty("Pan", pan)
|
|
651
|
+
if not result:
|
|
652
|
+
success = False
|
|
653
|
+
|
|
654
|
+
# Set EQ enabled if specified
|
|
655
|
+
if eq_enabled is not None:
|
|
656
|
+
result = timeline_item.SetProperty("EQEnable", 1 if eq_enabled else 0)
|
|
657
|
+
if not result:
|
|
658
|
+
success = False
|
|
659
|
+
|
|
660
|
+
if success:
|
|
661
|
+
changes = []
|
|
662
|
+
if volume is not None:
|
|
663
|
+
changes.append(f"volume to {volume}")
|
|
664
|
+
if pan is not None:
|
|
665
|
+
changes.append(f"pan to {pan}")
|
|
666
|
+
if eq_enabled is not None:
|
|
667
|
+
changes.append(f"EQ {'enabled' if eq_enabled else 'disabled'}")
|
|
668
|
+
|
|
669
|
+
return f"Successfully set {' and '.join(changes)} for timeline item '{timeline_item.GetName()}'"
|
|
670
|
+
else:
|
|
671
|
+
return f"Failed to set some audio properties for timeline item '{timeline_item.GetName()}'"
|
|
672
|
+
except Exception as e:
|
|
673
|
+
return f"Error setting timeline item audio properties: {str(e)}"
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
@mcp.resource("resolve://timeline-item/{timeline_item_id}/keyframes/{property_name}")
|
|
677
|
+
def get_timeline_item_keyframes(timeline_item_id: str, property_name: str) -> Dict[str, Any]:
|
|
678
|
+
"""Get keyframes for a specific timeline item by ID.
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
timeline_item_id: The ID of the timeline item to get keyframes for
|
|
682
|
+
property_name: Optional property name to filter keyframes (e.g., 'Pan', 'ZoomX')
|
|
683
|
+
"""
|
|
684
|
+
pm, current_project = get_current_project()
|
|
685
|
+
if not current_project:
|
|
686
|
+
return {"error": "No project currently open"}
|
|
687
|
+
|
|
688
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
689
|
+
if not current_timeline:
|
|
690
|
+
return {"error": "No timeline currently active"}
|
|
691
|
+
|
|
692
|
+
try:
|
|
693
|
+
# Find the timeline item by ID
|
|
694
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
695
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
696
|
+
|
|
697
|
+
timeline_item = None
|
|
698
|
+
|
|
699
|
+
# Search video tracks
|
|
700
|
+
for track_index in range(1, video_track_count + 1):
|
|
701
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
702
|
+
if items:
|
|
703
|
+
for item in items:
|
|
704
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
705
|
+
timeline_item = item
|
|
706
|
+
break
|
|
707
|
+
if timeline_item:
|
|
708
|
+
break
|
|
709
|
+
|
|
710
|
+
# If not found, search audio tracks
|
|
711
|
+
if not timeline_item:
|
|
712
|
+
for track_index in range(1, audio_track_count + 1):
|
|
713
|
+
items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
714
|
+
if items:
|
|
715
|
+
for item in items:
|
|
716
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
717
|
+
timeline_item = item
|
|
718
|
+
break
|
|
719
|
+
if timeline_item:
|
|
720
|
+
break
|
|
721
|
+
|
|
722
|
+
if not timeline_item:
|
|
723
|
+
return {"error": f"Timeline item with ID '{timeline_item_id}' not found"}
|
|
724
|
+
|
|
725
|
+
# Get all keyframeable properties for this item
|
|
726
|
+
keyframeable_properties = []
|
|
727
|
+
keyframes = {}
|
|
728
|
+
|
|
729
|
+
# Common keyframeable properties for video items
|
|
730
|
+
video_properties = [
|
|
731
|
+
'Pan', 'Tilt', 'ZoomX', 'ZoomY', 'Rotation', 'AnchorPointX', 'AnchorPointY',
|
|
732
|
+
'Pitch', 'Yaw', 'Opacity', 'CropLeft', 'CropRight', 'CropTop', 'CropBottom'
|
|
733
|
+
]
|
|
734
|
+
|
|
735
|
+
# Audio-specific keyframeable properties
|
|
736
|
+
audio_properties = ['Volume', 'Pan']
|
|
737
|
+
|
|
738
|
+
# Check if it's a video item
|
|
739
|
+
if timeline_item.GetType() == "Video":
|
|
740
|
+
# Check each property to see if it has keyframes
|
|
741
|
+
for prop in video_properties:
|
|
742
|
+
if timeline_item.GetKeyframeCount(prop) > 0:
|
|
743
|
+
keyframeable_properties.append(prop)
|
|
744
|
+
|
|
745
|
+
# Get all keyframes for this property
|
|
746
|
+
keyframes[prop] = []
|
|
747
|
+
keyframe_count = timeline_item.GetKeyframeCount(prop)
|
|
748
|
+
|
|
749
|
+
for i in range(keyframe_count):
|
|
750
|
+
# Get the frame position and value of the keyframe
|
|
751
|
+
frame_pos = timeline_item.GetKeyframeAtIndex(prop, i)["frame"]
|
|
752
|
+
value = timeline_item.GetPropertyAtKeyframeIndex(prop, i)
|
|
753
|
+
|
|
754
|
+
keyframes[prop].append({
|
|
755
|
+
"frame": frame_pos,
|
|
756
|
+
"value": value
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
# Check if it has audio properties (could be video with audio or audio-only)
|
|
760
|
+
if timeline_item.GetType() == "Audio" or timeline_item.GetMediaType() == "Audio":
|
|
761
|
+
# Check each audio property for keyframes
|
|
762
|
+
for prop in audio_properties:
|
|
763
|
+
if timeline_item.GetKeyframeCount(prop) > 0:
|
|
764
|
+
keyframeable_properties.append(prop)
|
|
765
|
+
|
|
766
|
+
# Get all keyframes for this property
|
|
767
|
+
keyframes[prop] = []
|
|
768
|
+
keyframe_count = timeline_item.GetKeyframeCount(prop)
|
|
769
|
+
|
|
770
|
+
for i in range(keyframe_count):
|
|
771
|
+
# Get the frame position and value of the keyframe
|
|
772
|
+
frame_pos = timeline_item.GetKeyframeAtIndex(prop, i)["frame"]
|
|
773
|
+
value = timeline_item.GetPropertyAtKeyframeIndex(prop, i)
|
|
774
|
+
|
|
775
|
+
keyframes[prop].append({
|
|
776
|
+
"frame": frame_pos,
|
|
777
|
+
"value": value
|
|
778
|
+
})
|
|
779
|
+
|
|
780
|
+
# Filter by property_name if specified
|
|
781
|
+
if property_name:
|
|
782
|
+
if property_name in keyframes:
|
|
783
|
+
return {
|
|
784
|
+
"item_id": timeline_item_id,
|
|
785
|
+
"item_name": timeline_item.GetName(),
|
|
786
|
+
"properties": [property_name],
|
|
787
|
+
"keyframes": {property_name: keyframes[property_name]}
|
|
788
|
+
}
|
|
789
|
+
else:
|
|
790
|
+
return {
|
|
791
|
+
"item_id": timeline_item_id,
|
|
792
|
+
"item_name": timeline_item.GetName(),
|
|
793
|
+
"properties": [],
|
|
794
|
+
"keyframes": {}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
# Return all keyframes
|
|
798
|
+
return {
|
|
799
|
+
"item_id": timeline_item_id,
|
|
800
|
+
"item_name": timeline_item.GetName(),
|
|
801
|
+
"properties": keyframeable_properties,
|
|
802
|
+
"keyframes": keyframes
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
except Exception as e:
|
|
806
|
+
return {"error": f"Error getting timeline item keyframes: {str(e)}"}
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
@mcp.tool()
|
|
810
|
+
def add_keyframe(timeline_item_id: str, property_name: str, frame: int, value: float) -> str:
|
|
811
|
+
"""Add a keyframe at the specified frame for a timeline item property.
|
|
812
|
+
|
|
813
|
+
Args:
|
|
814
|
+
timeline_item_id: The ID of the timeline item to add keyframe to
|
|
815
|
+
property_name: The name of the property to keyframe (e.g., 'Pan', 'ZoomX')
|
|
816
|
+
frame: Frame position for the keyframe
|
|
817
|
+
value: Value to set at the keyframe
|
|
818
|
+
"""
|
|
819
|
+
pm, current_project = get_current_project()
|
|
820
|
+
if not current_project:
|
|
821
|
+
return "Error: No project currently open"
|
|
822
|
+
|
|
823
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
824
|
+
if not current_timeline:
|
|
825
|
+
return "Error: No timeline currently active"
|
|
826
|
+
|
|
827
|
+
# Valid keyframeable properties
|
|
828
|
+
video_properties = [
|
|
829
|
+
'Pan', 'Tilt', 'ZoomX', 'ZoomY', 'Rotation', 'AnchorPointX', 'AnchorPointY',
|
|
830
|
+
'Pitch', 'Yaw', 'Opacity', 'CropLeft', 'CropRight', 'CropTop', 'CropBottom'
|
|
831
|
+
]
|
|
832
|
+
|
|
833
|
+
audio_properties = ['Volume', 'Pan']
|
|
834
|
+
|
|
835
|
+
valid_properties = video_properties + audio_properties
|
|
836
|
+
|
|
837
|
+
if property_name not in valid_properties:
|
|
838
|
+
return f"Error: Invalid property name. Must be one of: {', '.join(valid_properties)}"
|
|
839
|
+
|
|
840
|
+
try:
|
|
841
|
+
# Find the timeline item by ID
|
|
842
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
843
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
844
|
+
|
|
845
|
+
timeline_item = None
|
|
846
|
+
is_audio = False
|
|
847
|
+
|
|
848
|
+
# Search video tracks
|
|
849
|
+
for track_index in range(1, video_track_count + 1):
|
|
850
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
851
|
+
if items:
|
|
852
|
+
for item in items:
|
|
853
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
854
|
+
timeline_item = item
|
|
855
|
+
break
|
|
856
|
+
if timeline_item:
|
|
857
|
+
break
|
|
858
|
+
|
|
859
|
+
# If not found, search audio tracks
|
|
860
|
+
if not timeline_item:
|
|
861
|
+
for track_index in range(1, audio_track_count + 1):
|
|
862
|
+
items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
863
|
+
if items:
|
|
864
|
+
for item in items:
|
|
865
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
866
|
+
timeline_item = item
|
|
867
|
+
is_audio = True
|
|
868
|
+
break
|
|
869
|
+
if timeline_item:
|
|
870
|
+
break
|
|
871
|
+
|
|
872
|
+
if not timeline_item:
|
|
873
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' not found"
|
|
874
|
+
|
|
875
|
+
# Check if the specified property is valid for this item type
|
|
876
|
+
if is_audio and property_name not in audio_properties:
|
|
877
|
+
return f"Error: Property '{property_name}' is not available for audio items"
|
|
878
|
+
|
|
879
|
+
if not is_audio and property_name not in video_properties and timeline_item.GetType() != "Video":
|
|
880
|
+
return f"Error: Property '{property_name}' is not available for this item type"
|
|
881
|
+
|
|
882
|
+
# Validate frame is within the item's range
|
|
883
|
+
start_frame = timeline_item.GetStart()
|
|
884
|
+
end_frame = timeline_item.GetEnd()
|
|
885
|
+
|
|
886
|
+
if frame < start_frame or frame > end_frame:
|
|
887
|
+
return f"Error: Frame {frame} is outside the item's range ({start_frame} to {end_frame})"
|
|
888
|
+
|
|
889
|
+
# Add the keyframe
|
|
890
|
+
result = timeline_item.AddKeyframe(property_name, frame, value)
|
|
891
|
+
|
|
892
|
+
if result:
|
|
893
|
+
return f"Successfully added keyframe for {property_name} at frame {frame} with value {value}"
|
|
894
|
+
else:
|
|
895
|
+
return f"Failed to add keyframe for {property_name} at frame {frame}"
|
|
896
|
+
|
|
897
|
+
except Exception as e:
|
|
898
|
+
return f"Error adding keyframe: {str(e)}"
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
@mcp.tool()
|
|
902
|
+
def modify_keyframe(timeline_item_id: str, property_name: str, frame: int, new_value: float = None, new_frame: int = None) -> str:
|
|
903
|
+
"""Modify an existing keyframe by changing its value or frame position.
|
|
904
|
+
|
|
905
|
+
Args:
|
|
906
|
+
timeline_item_id: The ID of the timeline item
|
|
907
|
+
property_name: The name of the property with keyframe
|
|
908
|
+
frame: Current frame position of the keyframe to modify
|
|
909
|
+
new_value: Optional new value for the keyframe
|
|
910
|
+
new_frame: Optional new frame position for the keyframe
|
|
911
|
+
"""
|
|
912
|
+
pm, current_project = get_current_project()
|
|
913
|
+
if not current_project:
|
|
914
|
+
return "Error: No project currently open"
|
|
915
|
+
|
|
916
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
917
|
+
if not current_timeline:
|
|
918
|
+
return "Error: No timeline currently active"
|
|
919
|
+
|
|
920
|
+
if new_value is None and new_frame is None:
|
|
921
|
+
return "Error: Must specify at least one of new_value or new_frame"
|
|
922
|
+
|
|
923
|
+
try:
|
|
924
|
+
# Find the timeline item by ID
|
|
925
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
926
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
927
|
+
|
|
928
|
+
timeline_item = None
|
|
929
|
+
|
|
930
|
+
# Search video tracks
|
|
931
|
+
for track_index in range(1, video_track_count + 1):
|
|
932
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
933
|
+
if items:
|
|
934
|
+
for item in items:
|
|
935
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
936
|
+
timeline_item = item
|
|
937
|
+
break
|
|
938
|
+
if timeline_item:
|
|
939
|
+
break
|
|
940
|
+
|
|
941
|
+
# If not found, search audio tracks
|
|
942
|
+
if not timeline_item:
|
|
943
|
+
for track_index in range(1, audio_track_count + 1):
|
|
944
|
+
items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
945
|
+
if items:
|
|
946
|
+
for item in items:
|
|
947
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
948
|
+
timeline_item = item
|
|
949
|
+
break
|
|
950
|
+
if timeline_item:
|
|
951
|
+
break
|
|
952
|
+
|
|
953
|
+
if not timeline_item:
|
|
954
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' not found"
|
|
955
|
+
|
|
956
|
+
# Check if the property has keyframes
|
|
957
|
+
keyframe_count = timeline_item.GetKeyframeCount(property_name)
|
|
958
|
+
if keyframe_count == 0:
|
|
959
|
+
return f"Error: No keyframes found for property '{property_name}'"
|
|
960
|
+
|
|
961
|
+
# Find the keyframe at the specified frame
|
|
962
|
+
keyframe_index = -1
|
|
963
|
+
for i in range(keyframe_count):
|
|
964
|
+
kf = timeline_item.GetKeyframeAtIndex(property_name, i)
|
|
965
|
+
if kf["frame"] == frame:
|
|
966
|
+
keyframe_index = i
|
|
967
|
+
break
|
|
968
|
+
|
|
969
|
+
if keyframe_index == -1:
|
|
970
|
+
return f"Error: No keyframe found at frame {frame} for property '{property_name}'"
|
|
971
|
+
|
|
972
|
+
if new_frame is not None:
|
|
973
|
+
# Check if new frame is within the item's range
|
|
974
|
+
start_frame = timeline_item.GetStart()
|
|
975
|
+
end_frame = timeline_item.GetEnd()
|
|
976
|
+
|
|
977
|
+
if new_frame < start_frame or new_frame > end_frame:
|
|
978
|
+
return f"Error: New frame {new_frame} is outside the item's range ({start_frame} to {end_frame})"
|
|
979
|
+
|
|
980
|
+
# Delete the keyframe at the current frame
|
|
981
|
+
current_value = timeline_item.GetPropertyAtKeyframeIndex(property_name, keyframe_index)
|
|
982
|
+
timeline_item.DeleteKeyframe(property_name, frame)
|
|
983
|
+
|
|
984
|
+
# Add a new keyframe at the new frame position with the current value (or new value if specified)
|
|
985
|
+
value = new_value if new_value is not None else current_value
|
|
986
|
+
result = timeline_item.AddKeyframe(property_name, new_frame, value)
|
|
987
|
+
|
|
988
|
+
if result:
|
|
989
|
+
return f"Successfully moved keyframe for {property_name} from frame {frame} to frame {new_frame}"
|
|
990
|
+
else:
|
|
991
|
+
return f"Failed to move keyframe for {property_name}"
|
|
992
|
+
else:
|
|
993
|
+
# Only changing the value, not the frame position
|
|
994
|
+
# We need to delete and re-add the keyframe with the new value
|
|
995
|
+
timeline_item.DeleteKeyframe(property_name, frame)
|
|
996
|
+
result = timeline_item.AddKeyframe(property_name, frame, new_value)
|
|
997
|
+
|
|
998
|
+
if result:
|
|
999
|
+
return f"Successfully updated keyframe value for {property_name} at frame {frame} to {new_value}"
|
|
1000
|
+
else:
|
|
1001
|
+
return f"Failed to update keyframe value for {property_name} at frame {frame}"
|
|
1002
|
+
|
|
1003
|
+
except Exception as e:
|
|
1004
|
+
return f"Error modifying keyframe: {str(e)}"
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
@mcp.tool()
|
|
1008
|
+
def delete_keyframe(timeline_item_id: str, property_name: str, frame: int) -> str:
|
|
1009
|
+
"""Delete a keyframe at the specified frame for a timeline item property.
|
|
1010
|
+
|
|
1011
|
+
Args:
|
|
1012
|
+
timeline_item_id: The ID of the timeline item
|
|
1013
|
+
property_name: The name of the property with keyframe to delete
|
|
1014
|
+
frame: Frame position of the keyframe to delete
|
|
1015
|
+
"""
|
|
1016
|
+
pm, current_project = get_current_project()
|
|
1017
|
+
if not current_project:
|
|
1018
|
+
return "Error: No project currently open"
|
|
1019
|
+
|
|
1020
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
1021
|
+
if not current_timeline:
|
|
1022
|
+
return "Error: No timeline currently active"
|
|
1023
|
+
|
|
1024
|
+
try:
|
|
1025
|
+
# Find the timeline item by ID
|
|
1026
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
1027
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
1028
|
+
|
|
1029
|
+
timeline_item = None
|
|
1030
|
+
|
|
1031
|
+
# Search video tracks
|
|
1032
|
+
for track_index in range(1, video_track_count + 1):
|
|
1033
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
1034
|
+
if items:
|
|
1035
|
+
for item in items:
|
|
1036
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
1037
|
+
timeline_item = item
|
|
1038
|
+
break
|
|
1039
|
+
if timeline_item:
|
|
1040
|
+
break
|
|
1041
|
+
|
|
1042
|
+
# If not found, search audio tracks
|
|
1043
|
+
if not timeline_item:
|
|
1044
|
+
for track_index in range(1, audio_track_count + 1):
|
|
1045
|
+
items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
1046
|
+
if items:
|
|
1047
|
+
for item in items:
|
|
1048
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
1049
|
+
timeline_item = item
|
|
1050
|
+
break
|
|
1051
|
+
if timeline_item:
|
|
1052
|
+
break
|
|
1053
|
+
|
|
1054
|
+
if not timeline_item:
|
|
1055
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' not found"
|
|
1056
|
+
|
|
1057
|
+
# Check if the property has keyframes
|
|
1058
|
+
keyframe_count = timeline_item.GetKeyframeCount(property_name)
|
|
1059
|
+
if keyframe_count == 0:
|
|
1060
|
+
return f"Error: No keyframes found for property '{property_name}'"
|
|
1061
|
+
|
|
1062
|
+
# Check if there's a keyframe at the specified frame
|
|
1063
|
+
keyframe_exists = False
|
|
1064
|
+
for i in range(keyframe_count):
|
|
1065
|
+
kf = timeline_item.GetKeyframeAtIndex(property_name, i)
|
|
1066
|
+
if kf["frame"] == frame:
|
|
1067
|
+
keyframe_exists = True
|
|
1068
|
+
break
|
|
1069
|
+
|
|
1070
|
+
if not keyframe_exists:
|
|
1071
|
+
return f"Error: No keyframe found at frame {frame} for property '{property_name}'"
|
|
1072
|
+
|
|
1073
|
+
# Delete the keyframe
|
|
1074
|
+
result = timeline_item.DeleteKeyframe(property_name, frame)
|
|
1075
|
+
|
|
1076
|
+
if result:
|
|
1077
|
+
return f"Successfully deleted keyframe for {property_name} at frame {frame}"
|
|
1078
|
+
else:
|
|
1079
|
+
return f"Failed to delete keyframe for {property_name} at frame {frame}"
|
|
1080
|
+
|
|
1081
|
+
except Exception as e:
|
|
1082
|
+
return f"Error deleting keyframe: {str(e)}"
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
@mcp.tool()
|
|
1086
|
+
def set_keyframe_interpolation(timeline_item_id: str, property_name: str, frame: int, interpolation_type: str) -> str:
|
|
1087
|
+
"""Set the interpolation type for a keyframe.
|
|
1088
|
+
|
|
1089
|
+
Args:
|
|
1090
|
+
timeline_item_id: The ID of the timeline item
|
|
1091
|
+
property_name: The name of the property with keyframe
|
|
1092
|
+
frame: Frame position of the keyframe
|
|
1093
|
+
interpolation_type: Type of interpolation. Options: 'Linear', 'Bezier', 'Ease-In', 'Ease-Out'
|
|
1094
|
+
"""
|
|
1095
|
+
pm, current_project = get_current_project()
|
|
1096
|
+
if not current_project:
|
|
1097
|
+
return "Error: No project currently open"
|
|
1098
|
+
|
|
1099
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
1100
|
+
if not current_timeline:
|
|
1101
|
+
return "Error: No timeline currently active"
|
|
1102
|
+
|
|
1103
|
+
# Validate interpolation type
|
|
1104
|
+
valid_interpolation_types = ['Linear', 'Bezier', 'Ease-In', 'Ease-Out']
|
|
1105
|
+
if interpolation_type not in valid_interpolation_types:
|
|
1106
|
+
return f"Error: Invalid interpolation type. Must be one of: {', '.join(valid_interpolation_types)}"
|
|
1107
|
+
|
|
1108
|
+
try:
|
|
1109
|
+
# Find the timeline item by ID
|
|
1110
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
1111
|
+
audio_track_count = current_timeline.GetTrackCount("audio")
|
|
1112
|
+
|
|
1113
|
+
timeline_item = None
|
|
1114
|
+
|
|
1115
|
+
# Search video tracks
|
|
1116
|
+
for track_index in range(1, video_track_count + 1):
|
|
1117
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
1118
|
+
if items:
|
|
1119
|
+
for item in items:
|
|
1120
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
1121
|
+
timeline_item = item
|
|
1122
|
+
break
|
|
1123
|
+
if timeline_item:
|
|
1124
|
+
break
|
|
1125
|
+
|
|
1126
|
+
# If not found, search audio tracks
|
|
1127
|
+
if not timeline_item:
|
|
1128
|
+
for track_index in range(1, audio_track_count + 1):
|
|
1129
|
+
items = current_timeline.GetItemListInTrack("audio", track_index)
|
|
1130
|
+
if items:
|
|
1131
|
+
for item in items:
|
|
1132
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
1133
|
+
timeline_item = item
|
|
1134
|
+
break
|
|
1135
|
+
if timeline_item:
|
|
1136
|
+
break
|
|
1137
|
+
|
|
1138
|
+
if not timeline_item:
|
|
1139
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' not found"
|
|
1140
|
+
|
|
1141
|
+
# Check if the property has keyframes
|
|
1142
|
+
keyframe_count = timeline_item.GetKeyframeCount(property_name)
|
|
1143
|
+
if keyframe_count == 0:
|
|
1144
|
+
return f"Error: No keyframes found for property '{property_name}'"
|
|
1145
|
+
|
|
1146
|
+
# Check if there's a keyframe at the specified frame
|
|
1147
|
+
keyframe_exists = False
|
|
1148
|
+
for i in range(keyframe_count):
|
|
1149
|
+
kf = timeline_item.GetKeyframeAtIndex(property_name, i)
|
|
1150
|
+
if kf["frame"] == frame:
|
|
1151
|
+
keyframe_exists = True
|
|
1152
|
+
break
|
|
1153
|
+
|
|
1154
|
+
if not keyframe_exists:
|
|
1155
|
+
return f"Error: No keyframe found at frame {frame} for property '{property_name}'"
|
|
1156
|
+
|
|
1157
|
+
# Set the interpolation type
|
|
1158
|
+
interpolation_map = {
|
|
1159
|
+
'Linear': 0,
|
|
1160
|
+
'Bezier': 1,
|
|
1161
|
+
'Ease-In': 2,
|
|
1162
|
+
'Ease-Out': 3
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
# Get current keyframe value
|
|
1166
|
+
value = None
|
|
1167
|
+
for i in range(keyframe_count):
|
|
1168
|
+
kf = timeline_item.GetKeyframeAtIndex(property_name, i)
|
|
1169
|
+
if kf["frame"] == frame:
|
|
1170
|
+
value = timeline_item.GetPropertyAtKeyframeIndex(property_name, i)
|
|
1171
|
+
break
|
|
1172
|
+
|
|
1173
|
+
# Delete the old keyframe
|
|
1174
|
+
timeline_item.DeleteKeyframe(property_name, frame)
|
|
1175
|
+
|
|
1176
|
+
# Add a new keyframe with the same value but different interpolation
|
|
1177
|
+
result = timeline_item.AddKeyframe(property_name, frame, value, interpolation_map[interpolation_type])
|
|
1178
|
+
|
|
1179
|
+
if result:
|
|
1180
|
+
return f"Successfully set interpolation for {property_name} keyframe at frame {frame} to {interpolation_type}"
|
|
1181
|
+
else:
|
|
1182
|
+
return f"Failed to set interpolation for {property_name} keyframe at frame {frame}"
|
|
1183
|
+
|
|
1184
|
+
except Exception as e:
|
|
1185
|
+
return f"Error setting keyframe interpolation: {str(e)}"
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
@mcp.tool()
|
|
1189
|
+
def enable_keyframes(timeline_item_id: str, keyframe_mode: str = "All") -> str:
|
|
1190
|
+
"""Enable keyframe mode for a timeline item.
|
|
1191
|
+
|
|
1192
|
+
Args:
|
|
1193
|
+
timeline_item_id: The ID of the timeline item
|
|
1194
|
+
keyframe_mode: Keyframe mode to enable. Options: 'All', 'Color', 'Sizing'
|
|
1195
|
+
"""
|
|
1196
|
+
pm, current_project = get_current_project()
|
|
1197
|
+
if not current_project:
|
|
1198
|
+
return "Error: No project currently open"
|
|
1199
|
+
|
|
1200
|
+
current_timeline = current_project.GetCurrentTimeline()
|
|
1201
|
+
if not current_timeline:
|
|
1202
|
+
return "Error: No timeline currently active"
|
|
1203
|
+
|
|
1204
|
+
# Validate keyframe mode
|
|
1205
|
+
valid_keyframe_modes = ['All', 'Color', 'Sizing']
|
|
1206
|
+
if keyframe_mode not in valid_keyframe_modes:
|
|
1207
|
+
return f"Error: Invalid keyframe mode. Must be one of: {', '.join(valid_keyframe_modes)}"
|
|
1208
|
+
|
|
1209
|
+
try:
|
|
1210
|
+
# Find the timeline item by ID
|
|
1211
|
+
video_track_count = current_timeline.GetTrackCount("video")
|
|
1212
|
+
|
|
1213
|
+
timeline_item = None
|
|
1214
|
+
|
|
1215
|
+
# Search video tracks
|
|
1216
|
+
for track_index in range(1, video_track_count + 1):
|
|
1217
|
+
items = current_timeline.GetItemListInTrack("video", track_index)
|
|
1218
|
+
if items:
|
|
1219
|
+
for item in items:
|
|
1220
|
+
if str(item.GetUniqueId()) == timeline_item_id:
|
|
1221
|
+
timeline_item = item
|
|
1222
|
+
break
|
|
1223
|
+
if timeline_item:
|
|
1224
|
+
break
|
|
1225
|
+
|
|
1226
|
+
if not timeline_item:
|
|
1227
|
+
return f"Error: Video timeline item with ID '{timeline_item_id}' not found"
|
|
1228
|
+
|
|
1229
|
+
if timeline_item.GetType() != "Video":
|
|
1230
|
+
return f"Error: Timeline item with ID '{timeline_item_id}' is not a video item"
|
|
1231
|
+
|
|
1232
|
+
# Set the keyframe mode
|
|
1233
|
+
keyframe_mode_map = {
|
|
1234
|
+
'All': 0,
|
|
1235
|
+
'Color': 1,
|
|
1236
|
+
'Sizing': 2
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
result = timeline_item.SetProperty("KeyframeMode", keyframe_mode_map[keyframe_mode])
|
|
1240
|
+
|
|
1241
|
+
if result:
|
|
1242
|
+
return f"Successfully enabled {keyframe_mode} keyframe mode for timeline item '{timeline_item.GetName()}'"
|
|
1243
|
+
else:
|
|
1244
|
+
return f"Failed to enable {keyframe_mode} keyframe mode for timeline item '{timeline_item.GetName()}'"
|
|
1245
|
+
|
|
1246
|
+
except Exception as e:
|
|
1247
|
+
return f"Error enabling keyframe mode: {str(e)}"
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
@mcp.tool()
|
|
1251
|
+
def ti_get_info(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1252
|
+
"""Get comprehensive info about a timeline item.
|
|
1253
|
+
|
|
1254
|
+
Args:
|
|
1255
|
+
item_index: 0-based item index. Default: 0.
|
|
1256
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
1257
|
+
track_index: 1-based track index. Default: 1.
|
|
1258
|
+
"""
|
|
1259
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1260
|
+
if err:
|
|
1261
|
+
return err
|
|
1262
|
+
return {
|
|
1263
|
+
"name": item.GetName(), "duration": item.GetDuration(),
|
|
1264
|
+
"start": item.GetStart(), "end": item.GetEnd(),
|
|
1265
|
+
"left_offset": item.GetLeftOffset(), "right_offset": item.GetRightOffset(),
|
|
1266
|
+
"source_start_frame": item.GetSourceStartFrame(), "source_end_frame": item.GetSourceEndFrame(),
|
|
1267
|
+
"unique_id": item.GetUniqueId(), "clip_enabled": item.GetClipEnabled()
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
@mcp.tool()
|
|
1272
|
+
def ti_set_name(name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1273
|
+
"""Rename a timeline item.
|
|
1274
|
+
|
|
1275
|
+
Args:
|
|
1276
|
+
name: New timeline item name.
|
|
1277
|
+
item_index: 0-based item index. Default: 0.
|
|
1278
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
1279
|
+
track_index: 1-based track index. Default: 1.
|
|
1280
|
+
"""
|
|
1281
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1282
|
+
if err:
|
|
1283
|
+
return err
|
|
1284
|
+
missing = _requires_method(item, "SetName", "20.2")
|
|
1285
|
+
if missing:
|
|
1286
|
+
return missing
|
|
1287
|
+
result = item.SetName(name)
|
|
1288
|
+
return {"success": bool(result), "name": name}
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
@mcp.tool()
|
|
1292
|
+
def ti_get_source_start_time(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1293
|
+
"""Get source start time of a timeline item.
|
|
1294
|
+
|
|
1295
|
+
Args:
|
|
1296
|
+
item_index: 0-based item index. Default: 0.
|
|
1297
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
1298
|
+
track_index: 1-based track index. Default: 1.
|
|
1299
|
+
"""
|
|
1300
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1301
|
+
if err:
|
|
1302
|
+
return err
|
|
1303
|
+
return {"source_start_time": item.GetSourceStartTime(), "source_end_time": item.GetSourceEndTime()}
|
|
1304
|
+
|
|
1305
|
+
|
|
1306
|
+
@mcp.tool()
|
|
1307
|
+
def ti_set_property(property_name: str, property_value: Any, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1308
|
+
"""Set a property on a timeline item.
|
|
1309
|
+
|
|
1310
|
+
Args:
|
|
1311
|
+
property_name: Property name (Pan, Tilt, ZoomX, ZoomY, RotationAngle, Opacity, CropLeft, CropRight, CropTop, CropBottom, etc.).
|
|
1312
|
+
property_value: Value to set.
|
|
1313
|
+
item_index: 0-based item index. Default: 0.
|
|
1314
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
1315
|
+
track_index: 1-based track index. Default: 1.
|
|
1316
|
+
"""
|
|
1317
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1318
|
+
if err:
|
|
1319
|
+
return err
|
|
1320
|
+
result = item.SetProperty(property_name, property_value)
|
|
1321
|
+
return {"success": bool(result)}
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
@mcp.tool()
|
|
1325
|
+
def ti_get_property(property_name: str = "", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1326
|
+
"""Get property of a timeline item.
|
|
1327
|
+
|
|
1328
|
+
Args:
|
|
1329
|
+
property_name: Property name, or empty for all. Default: ''.
|
|
1330
|
+
item_index: 0-based item index. Default: 0.
|
|
1331
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
1332
|
+
track_index: 1-based track index. Default: 1.
|
|
1333
|
+
"""
|
|
1334
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1335
|
+
if err:
|
|
1336
|
+
return err
|
|
1337
|
+
if property_name:
|
|
1338
|
+
result = item.GetProperty(property_name)
|
|
1339
|
+
else:
|
|
1340
|
+
result = item.GetProperty()
|
|
1341
|
+
return {"property": result}
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
@mcp.tool()
|
|
1345
|
+
def ti_add_marker(frame_id: int, color: str, name: str, note: str = "", duration: int = 1, custom_data: str = "", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1346
|
+
"""Add a marker to a timeline item.
|
|
1347
|
+
|
|
1348
|
+
Args:
|
|
1349
|
+
frame_id: Frame offset within the item.
|
|
1350
|
+
color: Marker color.
|
|
1351
|
+
name: Marker name.
|
|
1352
|
+
note: Marker note. Default: ''.
|
|
1353
|
+
duration: Duration in frames. Default: 1.
|
|
1354
|
+
custom_data: Custom data. Default: ''.
|
|
1355
|
+
item_index: 0-based item index. Default: 0.
|
|
1356
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
1357
|
+
track_index: 1-based track index. Default: 1.
|
|
1358
|
+
"""
|
|
1359
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1360
|
+
if err:
|
|
1361
|
+
return err
|
|
1362
|
+
result = item.AddMarker(frame_id, color, name, note, duration, custom_data)
|
|
1363
|
+
return {"success": bool(result)}
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
@mcp.tool()
|
|
1367
|
+
def ti_get_markers(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1368
|
+
"""Get all markers on a timeline item.
|
|
1369
|
+
|
|
1370
|
+
Args:
|
|
1371
|
+
item_index: 0-based item index. Default: 0.
|
|
1372
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
1373
|
+
track_index: 1-based track index. Default: 1.
|
|
1374
|
+
"""
|
|
1375
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1376
|
+
if err:
|
|
1377
|
+
return err
|
|
1378
|
+
return {"markers": item.GetMarkers() or {}}
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
@mcp.tool()
|
|
1382
|
+
def ti_delete_markers_by_color(color: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1383
|
+
"""Delete markers by color on a timeline item.
|
|
1384
|
+
|
|
1385
|
+
Args:
|
|
1386
|
+
color: Color to delete. '' for all.
|
|
1387
|
+
item_index: 0-based item index. Default: 0.
|
|
1388
|
+
"""
|
|
1389
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1390
|
+
if err:
|
|
1391
|
+
return err
|
|
1392
|
+
return {"success": bool(item.DeleteMarkersByColor(color))}
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
@mcp.tool()
|
|
1396
|
+
def ti_delete_marker_at_frame(frame_id: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1397
|
+
"""Delete a marker at a frame on a timeline item.
|
|
1398
|
+
|
|
1399
|
+
Args:
|
|
1400
|
+
frame_id: Frame number.
|
|
1401
|
+
item_index: 0-based item index. Default: 0.
|
|
1402
|
+
"""
|
|
1403
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1404
|
+
if err:
|
|
1405
|
+
return err
|
|
1406
|
+
return {"success": bool(item.DeleteMarkerAtFrame(frame_id))}
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
@mcp.tool()
|
|
1410
|
+
def ti_delete_marker_by_custom_data(custom_data: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1411
|
+
"""Delete a marker by custom data on a timeline item.
|
|
1412
|
+
|
|
1413
|
+
Args:
|
|
1414
|
+
custom_data: Custom data of the marker.
|
|
1415
|
+
item_index: 0-based item index. Default: 0.
|
|
1416
|
+
"""
|
|
1417
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1418
|
+
if err:
|
|
1419
|
+
return err
|
|
1420
|
+
return {"success": bool(item.DeleteMarkerByCustomData(custom_data))}
|
|
1421
|
+
|
|
1422
|
+
|
|
1423
|
+
@mcp.tool()
|
|
1424
|
+
def ti_get_marker_by_custom_data(custom_data: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1425
|
+
"""Find marker by custom data.
|
|
1426
|
+
|
|
1427
|
+
Args:
|
|
1428
|
+
custom_data: Custom data to search for.
|
|
1429
|
+
item_index: 0-based item index. Default: 0.
|
|
1430
|
+
"""
|
|
1431
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1432
|
+
if err:
|
|
1433
|
+
return err
|
|
1434
|
+
return {"marker": item.GetMarkerByCustomData(custom_data) or {}}
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
@mcp.tool()
|
|
1438
|
+
def ti_update_marker_custom_data(frame_id: int, custom_data: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1439
|
+
"""Update marker custom data.
|
|
1440
|
+
|
|
1441
|
+
Args:
|
|
1442
|
+
frame_id: Frame number.
|
|
1443
|
+
custom_data: New custom data.
|
|
1444
|
+
item_index: 0-based item index. Default: 0.
|
|
1445
|
+
"""
|
|
1446
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1447
|
+
if err:
|
|
1448
|
+
return err
|
|
1449
|
+
return {"success": bool(item.UpdateMarkerCustomData(frame_id, custom_data))}
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
@mcp.tool()
|
|
1453
|
+
def ti_get_marker_custom_data(frame_id: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1454
|
+
"""Get marker custom data.
|
|
1455
|
+
|
|
1456
|
+
Args:
|
|
1457
|
+
frame_id: Frame number.
|
|
1458
|
+
item_index: 0-based item index. Default: 0.
|
|
1459
|
+
"""
|
|
1460
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1461
|
+
if err:
|
|
1462
|
+
return err
|
|
1463
|
+
return {"custom_data": item.GetMarkerCustomData(frame_id) or ""}
|
|
1464
|
+
|
|
1465
|
+
|
|
1466
|
+
@mcp.tool()
|
|
1467
|
+
def ti_add_flag(color: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1468
|
+
"""Add a flag to a timeline item.
|
|
1469
|
+
|
|
1470
|
+
Args:
|
|
1471
|
+
color: Flag color.
|
|
1472
|
+
item_index: 0-based item index. Default: 0.
|
|
1473
|
+
"""
|
|
1474
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1475
|
+
if err:
|
|
1476
|
+
return err
|
|
1477
|
+
return {"success": bool(item.AddFlag(color))}
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
@mcp.tool()
|
|
1481
|
+
def ti_get_flag_list(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1482
|
+
"""Get flags on a timeline item.
|
|
1483
|
+
|
|
1484
|
+
Args:
|
|
1485
|
+
item_index: 0-based item index. Default: 0.
|
|
1486
|
+
"""
|
|
1487
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1488
|
+
if err:
|
|
1489
|
+
return err
|
|
1490
|
+
return {"flags": item.GetFlagList() or []}
|
|
1491
|
+
|
|
1492
|
+
|
|
1493
|
+
@mcp.tool()
|
|
1494
|
+
def ti_clear_flags(color: str = "", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1495
|
+
"""Clear flags from a timeline item.
|
|
1496
|
+
|
|
1497
|
+
Args:
|
|
1498
|
+
color: Color to clear, or '' for all.
|
|
1499
|
+
item_index: 0-based item index. Default: 0.
|
|
1500
|
+
"""
|
|
1501
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1502
|
+
if err:
|
|
1503
|
+
return err
|
|
1504
|
+
return {"success": bool(item.ClearFlags(color))}
|
|
1505
|
+
|
|
1506
|
+
|
|
1507
|
+
@mcp.tool()
|
|
1508
|
+
def ti_get_clip_color(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1509
|
+
"""Get clip color of a timeline item.
|
|
1510
|
+
|
|
1511
|
+
Args:
|
|
1512
|
+
item_index: 0-based item index. Default: 0.
|
|
1513
|
+
"""
|
|
1514
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1515
|
+
if err:
|
|
1516
|
+
return err
|
|
1517
|
+
return {"clip_color": item.GetClipColor() or ""}
|
|
1518
|
+
|
|
1519
|
+
|
|
1520
|
+
@mcp.tool()
|
|
1521
|
+
def ti_set_clip_color(color: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1522
|
+
"""Set clip color of a timeline item.
|
|
1523
|
+
|
|
1524
|
+
Args:
|
|
1525
|
+
color: Color name.
|
|
1526
|
+
item_index: 0-based item index. Default: 0.
|
|
1527
|
+
"""
|
|
1528
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1529
|
+
if err:
|
|
1530
|
+
return err
|
|
1531
|
+
return {"success": bool(item.SetClipColor(color))}
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
@mcp.tool()
|
|
1535
|
+
def ti_clear_clip_color(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1536
|
+
"""Clear clip color from a timeline item.
|
|
1537
|
+
|
|
1538
|
+
Args:
|
|
1539
|
+
item_index: 0-based item index. Default: 0.
|
|
1540
|
+
"""
|
|
1541
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1542
|
+
if err:
|
|
1543
|
+
return err
|
|
1544
|
+
return {"success": bool(item.ClearClipColor())}
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
@mcp.tool()
|
|
1548
|
+
def ti_add_fusion_comp(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1549
|
+
"""Add a new Fusion composition to a timeline item.
|
|
1550
|
+
|
|
1551
|
+
Args:
|
|
1552
|
+
item_index: 0-based item index. Default: 0.
|
|
1553
|
+
"""
|
|
1554
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1555
|
+
if err:
|
|
1556
|
+
return err
|
|
1557
|
+
return {"success": bool(item.AddFusionComp())}
|
|
1558
|
+
|
|
1559
|
+
|
|
1560
|
+
@mcp.tool()
|
|
1561
|
+
def ti_import_fusion_comp(file_path: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1562
|
+
"""Import a Fusion composition from file.
|
|
1563
|
+
|
|
1564
|
+
Args:
|
|
1565
|
+
file_path: Path to the .comp file.
|
|
1566
|
+
item_index: 0-based item index. Default: 0.
|
|
1567
|
+
"""
|
|
1568
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1569
|
+
if err:
|
|
1570
|
+
return err
|
|
1571
|
+
return {"success": bool(item.ImportFusionComp(file_path))}
|
|
1572
|
+
|
|
1573
|
+
|
|
1574
|
+
@mcp.tool()
|
|
1575
|
+
def ti_export_fusion_comp(file_path: str, comp_index: int = 1, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1576
|
+
"""Export a Fusion composition to file.
|
|
1577
|
+
|
|
1578
|
+
Args:
|
|
1579
|
+
file_path: Output path for the .comp file.
|
|
1580
|
+
comp_index: 1-based Fusion comp index. Default: 1.
|
|
1581
|
+
item_index: 0-based item index. Default: 0.
|
|
1582
|
+
"""
|
|
1583
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1584
|
+
if err:
|
|
1585
|
+
return err
|
|
1586
|
+
comp = item.GetFusionCompByIndex(comp_index)
|
|
1587
|
+
if not comp:
|
|
1588
|
+
return {"error": f"No Fusion comp at index {comp_index}"}
|
|
1589
|
+
return {"success": bool(item.ExportFusionComp(file_path, comp_index))}
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
@mcp.tool()
|
|
1593
|
+
def ti_delete_fusion_comp(comp_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1594
|
+
"""Delete a Fusion composition by name.
|
|
1595
|
+
|
|
1596
|
+
Args:
|
|
1597
|
+
comp_name: Name of the Fusion composition.
|
|
1598
|
+
item_index: 0-based item index. Default: 0.
|
|
1599
|
+
"""
|
|
1600
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1601
|
+
if err:
|
|
1602
|
+
return err
|
|
1603
|
+
return {"success": bool(item.DeleteFusionCompByName(comp_name))}
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
@mcp.tool()
|
|
1607
|
+
def ti_load_fusion_comp(comp_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1608
|
+
"""Load a Fusion composition by name.
|
|
1609
|
+
|
|
1610
|
+
Args:
|
|
1611
|
+
comp_name: Name of the Fusion composition.
|
|
1612
|
+
item_index: 0-based item index. Default: 0.
|
|
1613
|
+
"""
|
|
1614
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1615
|
+
if err:
|
|
1616
|
+
return err
|
|
1617
|
+
return {"success": bool(item.LoadFusionCompByName(comp_name))}
|
|
1618
|
+
|
|
1619
|
+
|
|
1620
|
+
@mcp.tool()
|
|
1621
|
+
def ti_rename_fusion_comp(old_name: str, new_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1622
|
+
"""Rename a Fusion composition.
|
|
1623
|
+
|
|
1624
|
+
Args:
|
|
1625
|
+
old_name: Current name.
|
|
1626
|
+
new_name: New name.
|
|
1627
|
+
item_index: 0-based item index. Default: 0.
|
|
1628
|
+
"""
|
|
1629
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1630
|
+
if err:
|
|
1631
|
+
return err
|
|
1632
|
+
return {"success": bool(item.RenameFusionCompByName(old_name, new_name))}
|
|
1633
|
+
|
|
1634
|
+
|
|
1635
|
+
@mcp.tool()
|
|
1636
|
+
def ti_get_fusion_comp_info(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1637
|
+
"""Get Fusion composition info for a timeline item.
|
|
1638
|
+
|
|
1639
|
+
Args:
|
|
1640
|
+
item_index: 0-based item index. Default: 0.
|
|
1641
|
+
"""
|
|
1642
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1643
|
+
if err:
|
|
1644
|
+
return err
|
|
1645
|
+
return {
|
|
1646
|
+
"comp_count": item.GetFusionCompCount(),
|
|
1647
|
+
"comp_names": item.GetFusionCompNameList() or {}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
|
|
1651
|
+
@mcp.tool()
|
|
1652
|
+
def ti_add_version(version_name: str, version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1653
|
+
"""Add a new color version to a timeline item.
|
|
1654
|
+
|
|
1655
|
+
Args:
|
|
1656
|
+
version_name: Name for the new version.
|
|
1657
|
+
version_type: 0=Local, 1=Remote. Default: 0.
|
|
1658
|
+
item_index: 0-based item index. Default: 0.
|
|
1659
|
+
"""
|
|
1660
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1661
|
+
if err:
|
|
1662
|
+
return err
|
|
1663
|
+
return {"success": bool(item.AddVersion(version_name, version_type))}
|
|
1664
|
+
|
|
1665
|
+
|
|
1666
|
+
@mcp.tool()
|
|
1667
|
+
def ti_get_current_version(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1668
|
+
"""Get the current color version of a timeline item.
|
|
1669
|
+
|
|
1670
|
+
Args:
|
|
1671
|
+
item_index: 0-based item index. Default: 0.
|
|
1672
|
+
"""
|
|
1673
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1674
|
+
if err:
|
|
1675
|
+
return err
|
|
1676
|
+
return {"version": item.GetCurrentVersion() or {}}
|
|
1677
|
+
|
|
1678
|
+
|
|
1679
|
+
@mcp.tool()
|
|
1680
|
+
def ti_delete_version(version_name: str, version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1681
|
+
"""Delete a color version.
|
|
1682
|
+
|
|
1683
|
+
Args:
|
|
1684
|
+
version_name: Name of the version.
|
|
1685
|
+
version_type: 0=Local, 1=Remote. Default: 0.
|
|
1686
|
+
item_index: 0-based item index. Default: 0.
|
|
1687
|
+
"""
|
|
1688
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1689
|
+
if err:
|
|
1690
|
+
return err
|
|
1691
|
+
return {"success": bool(item.DeleteVersionByName(version_name, version_type))}
|
|
1692
|
+
|
|
1693
|
+
|
|
1694
|
+
@mcp.tool()
|
|
1695
|
+
def ti_load_version(version_name: str, version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1696
|
+
"""Load a color version.
|
|
1697
|
+
|
|
1698
|
+
Args:
|
|
1699
|
+
version_name: Name of the version.
|
|
1700
|
+
version_type: 0=Local, 1=Remote. Default: 0.
|
|
1701
|
+
item_index: 0-based item index. Default: 0.
|
|
1702
|
+
"""
|
|
1703
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1704
|
+
if err:
|
|
1705
|
+
return err
|
|
1706
|
+
return {"success": bool(item.LoadVersionByName(version_name, version_type))}
|
|
1707
|
+
|
|
1708
|
+
|
|
1709
|
+
@mcp.tool()
|
|
1710
|
+
def ti_rename_version(old_name: str, new_name: str, version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1711
|
+
"""Rename a color version.
|
|
1712
|
+
|
|
1713
|
+
Args:
|
|
1714
|
+
old_name: Current version name.
|
|
1715
|
+
new_name: New version name.
|
|
1716
|
+
version_type: 0=Local, 1=Remote. Default: 0.
|
|
1717
|
+
item_index: 0-based item index. Default: 0.
|
|
1718
|
+
"""
|
|
1719
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1720
|
+
if err:
|
|
1721
|
+
return err
|
|
1722
|
+
return {"success": bool(item.RenameVersionByName(old_name, new_name, version_type))}
|
|
1723
|
+
|
|
1724
|
+
|
|
1725
|
+
@mcp.tool()
|
|
1726
|
+
def ti_get_version_name_list(version_type: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1727
|
+
"""Get list of version names.
|
|
1728
|
+
|
|
1729
|
+
Args:
|
|
1730
|
+
version_type: 0=Local, 1=Remote. Default: 0.
|
|
1731
|
+
item_index: 0-based item index. Default: 0.
|
|
1732
|
+
"""
|
|
1733
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1734
|
+
if err:
|
|
1735
|
+
return err
|
|
1736
|
+
return {"versions": item.GetVersionNameList(version_type) or []}
|
|
1737
|
+
|
|
1738
|
+
|
|
1739
|
+
@mcp.tool()
|
|
1740
|
+
def ti_set_cdl(cdl: Dict[str, Any], item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1741
|
+
"""Set CDL (Color Decision List) values on a timeline item.
|
|
1742
|
+
|
|
1743
|
+
Args:
|
|
1744
|
+
cdl: Dict with CDL values: {'NodeIndex': str, 'Slope': str, 'Offset': str, 'Power': str, 'Saturation': str}.
|
|
1745
|
+
item_index: 0-based item index. Default: 0.
|
|
1746
|
+
"""
|
|
1747
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1748
|
+
if err:
|
|
1749
|
+
return err
|
|
1750
|
+
return {"success": bool(item.SetCDL(_normalize_cdl(cdl)))}
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
@mcp.tool()
|
|
1754
|
+
def ti_add_take(media_pool_item_id: str, start_frame: int = 0, end_frame: int = 0, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1755
|
+
"""Add a take to a timeline item.
|
|
1756
|
+
|
|
1757
|
+
Args:
|
|
1758
|
+
media_pool_item_id: Unique ID of the MediaPoolItem to use as take.
|
|
1759
|
+
start_frame: Start frame. Default: 0.
|
|
1760
|
+
end_frame: End frame. Default: 0.
|
|
1761
|
+
item_index: 0-based item index. Default: 0.
|
|
1762
|
+
"""
|
|
1763
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1764
|
+
if err:
|
|
1765
|
+
return err
|
|
1766
|
+
_, mp, mp_err = _get_mp()
|
|
1767
|
+
if mp_err:
|
|
1768
|
+
return mp_err
|
|
1769
|
+
mpi = _find_clip_by_id(mp.GetRootFolder(), media_pool_item_id)
|
|
1770
|
+
if not mpi:
|
|
1771
|
+
return {"error": f"MediaPoolItem {media_pool_item_id} not found"}
|
|
1772
|
+
return {"success": bool(item.AddTake(mpi, start_frame, end_frame))}
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
@mcp.tool()
|
|
1776
|
+
def ti_get_takes_info(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1777
|
+
"""Get takes info for a timeline item.
|
|
1778
|
+
|
|
1779
|
+
Args:
|
|
1780
|
+
item_index: 0-based item index. Default: 0.
|
|
1781
|
+
"""
|
|
1782
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1783
|
+
if err:
|
|
1784
|
+
return err
|
|
1785
|
+
count = item.GetTakesCount()
|
|
1786
|
+
selected = item.GetSelectedTakeIndex()
|
|
1787
|
+
takes = []
|
|
1788
|
+
for i in range(count):
|
|
1789
|
+
take = item.GetTakeByIndex(i + 1)
|
|
1790
|
+
takes.append(take if take else {})
|
|
1791
|
+
return {"takes_count": count, "selected_take_index": selected, "takes": takes}
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
@mcp.tool()
|
|
1795
|
+
def ti_select_take(take_index: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1796
|
+
"""Select a take by index.
|
|
1797
|
+
|
|
1798
|
+
Args:
|
|
1799
|
+
take_index: 1-based take index.
|
|
1800
|
+
item_index: 0-based item index. Default: 0.
|
|
1801
|
+
"""
|
|
1802
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1803
|
+
if err:
|
|
1804
|
+
return err
|
|
1805
|
+
return {"success": bool(item.SelectTakeByIndex(take_index))}
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
@mcp.tool()
|
|
1809
|
+
def ti_delete_take(take_index: int, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1810
|
+
"""Delete a take by index.
|
|
1811
|
+
|
|
1812
|
+
Args:
|
|
1813
|
+
take_index: 1-based take index.
|
|
1814
|
+
item_index: 0-based item index. Default: 0.
|
|
1815
|
+
"""
|
|
1816
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1817
|
+
if err:
|
|
1818
|
+
return err
|
|
1819
|
+
return {"success": bool(item.DeleteTakeByIndex(take_index))}
|
|
1820
|
+
|
|
1821
|
+
|
|
1822
|
+
@mcp.tool()
|
|
1823
|
+
def ti_finalize_take(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1824
|
+
"""Finalize the selected take.
|
|
1825
|
+
|
|
1826
|
+
Args:
|
|
1827
|
+
item_index: 0-based item index. Default: 0.
|
|
1828
|
+
"""
|
|
1829
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1830
|
+
if err:
|
|
1831
|
+
return err
|
|
1832
|
+
return {"success": bool(item.FinalizeTake())}
|
|
1833
|
+
|
|
1834
|
+
|
|
1835
|
+
@mcp.tool()
|
|
1836
|
+
def ti_copy_grades(target_item_indices: List[int], track_type: str = "video", track_index: int = 1, source_item_index: int = 0) -> Dict[str, Any]:
|
|
1837
|
+
"""Copy grades from one timeline item to others.
|
|
1838
|
+
|
|
1839
|
+
Args:
|
|
1840
|
+
target_item_indices: List of 0-based indices of target items.
|
|
1841
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
1842
|
+
track_index: 1-based track index. Default: 1.
|
|
1843
|
+
source_item_index: 0-based source item index. Default: 0.
|
|
1844
|
+
"""
|
|
1845
|
+
_, tl, err = _get_timeline()
|
|
1846
|
+
if err:
|
|
1847
|
+
return err
|
|
1848
|
+
items = tl.GetItemListInTrack(track_type, track_index)
|
|
1849
|
+
if not items:
|
|
1850
|
+
return {"error": "No items in track"}
|
|
1851
|
+
source = items[source_item_index] if source_item_index < len(items) else None
|
|
1852
|
+
if not source:
|
|
1853
|
+
return {"error": "Source item not found"}
|
|
1854
|
+
targets = [items[i] for i in target_item_indices if i < len(items)]
|
|
1855
|
+
if not targets:
|
|
1856
|
+
return {"error": "No target items found"}
|
|
1857
|
+
result = source.CopyGrades(targets)
|
|
1858
|
+
return {"success": bool(result)}
|
|
1859
|
+
|
|
1860
|
+
|
|
1861
|
+
@mcp.tool()
|
|
1862
|
+
def ti_set_clip_enabled(enabled: bool, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1863
|
+
"""Enable or disable a timeline item.
|
|
1864
|
+
|
|
1865
|
+
Args:
|
|
1866
|
+
enabled: True to enable, False to disable.
|
|
1867
|
+
item_index: 0-based item index. Default: 0.
|
|
1868
|
+
"""
|
|
1869
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1870
|
+
if err:
|
|
1871
|
+
return err
|
|
1872
|
+
return {"success": bool(item.SetClipEnabled(enabled))}
|
|
1873
|
+
|
|
1874
|
+
|
|
1875
|
+
@mcp.tool()
|
|
1876
|
+
def ti_update_sidecar(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1877
|
+
"""Update sidecar file for a timeline item.
|
|
1878
|
+
|
|
1879
|
+
Args:
|
|
1880
|
+
item_index: 0-based item index. Default: 0.
|
|
1881
|
+
"""
|
|
1882
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1883
|
+
if err:
|
|
1884
|
+
return err
|
|
1885
|
+
return {"success": bool(item.UpdateSidecar())}
|
|
1886
|
+
|
|
1887
|
+
|
|
1888
|
+
@mcp.tool()
|
|
1889
|
+
def ti_load_burn_in_preset(preset_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1890
|
+
"""Load a burn-in preset for a timeline item.
|
|
1891
|
+
|
|
1892
|
+
Args:
|
|
1893
|
+
preset_name: Burn-in preset name.
|
|
1894
|
+
item_index: 0-based item index. Default: 0.
|
|
1895
|
+
"""
|
|
1896
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1897
|
+
if err:
|
|
1898
|
+
return err
|
|
1899
|
+
return {"success": bool(item.LoadBurnInPreset(preset_name))}
|
|
1900
|
+
|
|
1901
|
+
|
|
1902
|
+
@mcp.tool()
|
|
1903
|
+
def ti_create_magic_mask(mode: str = "Forward", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1904
|
+
"""Create a Magic Mask on a timeline item.
|
|
1905
|
+
|
|
1906
|
+
Args:
|
|
1907
|
+
mode: 'Forward' or 'Backward'. Default: 'Forward'.
|
|
1908
|
+
item_index: 0-based item index. Default: 0.
|
|
1909
|
+
"""
|
|
1910
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1911
|
+
if err:
|
|
1912
|
+
return err
|
|
1913
|
+
return {"success": bool(item.CreateMagicMask(mode))}
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
@mcp.tool()
|
|
1917
|
+
def ti_regenerate_magic_mask(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1918
|
+
"""Regenerate Magic Mask on a timeline item.
|
|
1919
|
+
|
|
1920
|
+
Args:
|
|
1921
|
+
item_index: 0-based item index. Default: 0.
|
|
1922
|
+
"""
|
|
1923
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1924
|
+
if err:
|
|
1925
|
+
return err
|
|
1926
|
+
return {"success": bool(item.RegenerateMagicMask())}
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
@mcp.tool()
|
|
1930
|
+
def ti_stabilize(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1931
|
+
"""Stabilize a timeline item.
|
|
1932
|
+
|
|
1933
|
+
Args:
|
|
1934
|
+
item_index: 0-based item index. Default: 0.
|
|
1935
|
+
"""
|
|
1936
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1937
|
+
if err:
|
|
1938
|
+
return err
|
|
1939
|
+
return {"success": bool(item.Stabilize())}
|
|
1940
|
+
|
|
1941
|
+
|
|
1942
|
+
@mcp.tool()
|
|
1943
|
+
def ti_smart_reframe(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1944
|
+
"""Apply Smart Reframe to a timeline item.
|
|
1945
|
+
|
|
1946
|
+
Args:
|
|
1947
|
+
item_index: 0-based item index. Default: 0.
|
|
1948
|
+
"""
|
|
1949
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1950
|
+
if err:
|
|
1951
|
+
return err
|
|
1952
|
+
return {"success": bool(item.SmartReframe())}
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
@mcp.tool()
|
|
1956
|
+
def ti_get_voice_isolation_state(item_index: int = 0, track_type: str = "audio", track_index: int = 1) -> Dict[str, Any]:
|
|
1957
|
+
"""Get voice isolation state for a timeline item.
|
|
1958
|
+
|
|
1959
|
+
Args:
|
|
1960
|
+
item_index: 0-based item index. Default: 0.
|
|
1961
|
+
track_type: 'audio' or 'video'. Default: 'audio'.
|
|
1962
|
+
track_index: 1-based track index. Default: 1.
|
|
1963
|
+
"""
|
|
1964
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1965
|
+
if err:
|
|
1966
|
+
return err
|
|
1967
|
+
missing = _requires_method(item, "GetVoiceIsolationState", "20.1")
|
|
1968
|
+
if missing:
|
|
1969
|
+
return missing
|
|
1970
|
+
state = item.GetVoiceIsolationState()
|
|
1971
|
+
return {"state": state if state else {"isEnabled": False, "amount": 0}}
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
@mcp.tool()
|
|
1975
|
+
def ti_set_voice_isolation_state(state: Dict[str, Any], item_index: int = 0, track_type: str = "audio", track_index: int = 1) -> Dict[str, Any]:
|
|
1976
|
+
"""Set voice isolation state for a timeline item.
|
|
1977
|
+
|
|
1978
|
+
Args:
|
|
1979
|
+
state: Dict with isEnabled (bool) and amount (0-100).
|
|
1980
|
+
item_index: 0-based item index. Default: 0.
|
|
1981
|
+
track_type: 'audio' or 'video'. Default: 'audio'.
|
|
1982
|
+
track_index: 1-based track index. Default: 1.
|
|
1983
|
+
"""
|
|
1984
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
1985
|
+
if err:
|
|
1986
|
+
return err
|
|
1987
|
+
missing = _requires_method(item, "SetVoiceIsolationState", "20.1")
|
|
1988
|
+
if missing:
|
|
1989
|
+
return missing
|
|
1990
|
+
result = item.SetVoiceIsolationState(state)
|
|
1991
|
+
return {"success": bool(result)}
|
|
1992
|
+
|
|
1993
|
+
|
|
1994
|
+
@mcp.tool()
|
|
1995
|
+
def ti_reset_all_node_colors(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
1996
|
+
"""Reset node colors for all nodes in the active clip version.
|
|
1997
|
+
|
|
1998
|
+
Args:
|
|
1999
|
+
item_index: 0-based item index. Default: 0.
|
|
2000
|
+
track_type: 'video' or 'audio'. Default: 'video'.
|
|
2001
|
+
track_index: 1-based track index. Default: 1.
|
|
2002
|
+
"""
|
|
2003
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2004
|
+
if err:
|
|
2005
|
+
return err
|
|
2006
|
+
missing = _requires_method(item, "ResetAllNodeColors", "20.2")
|
|
2007
|
+
if missing:
|
|
2008
|
+
return missing
|
|
2009
|
+
result = item.ResetAllNodeColors()
|
|
2010
|
+
return {"success": bool(result)}
|
|
2011
|
+
|
|
2012
|
+
|
|
2013
|
+
@mcp.tool()
|
|
2014
|
+
def ti_get_node_graph(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2015
|
+
"""Get the color node graph for a timeline item.
|
|
2016
|
+
|
|
2017
|
+
Args:
|
|
2018
|
+
item_index: 0-based item index. Default: 0.
|
|
2019
|
+
"""
|
|
2020
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2021
|
+
if err:
|
|
2022
|
+
return err
|
|
2023
|
+
graph = item.GetNodeGraph()
|
|
2024
|
+
if graph:
|
|
2025
|
+
return {"has_graph": True, "num_nodes": graph.GetNumNodes()}
|
|
2026
|
+
return {"has_graph": False}
|
|
2027
|
+
|
|
2028
|
+
|
|
2029
|
+
@mcp.tool()
|
|
2030
|
+
def ti_get_color_group(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2031
|
+
"""Get the color group for a timeline item.
|
|
2032
|
+
|
|
2033
|
+
Args:
|
|
2034
|
+
item_index: 0-based item index. Default: 0.
|
|
2035
|
+
"""
|
|
2036
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2037
|
+
if err:
|
|
2038
|
+
return err
|
|
2039
|
+
group = item.GetColorGroup()
|
|
2040
|
+
if group:
|
|
2041
|
+
return {"group_name": group.GetName()}
|
|
2042
|
+
return {"group_name": None}
|
|
2043
|
+
|
|
2044
|
+
|
|
2045
|
+
@mcp.tool()
|
|
2046
|
+
def ti_assign_to_color_group(group_name: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2047
|
+
"""Assign a timeline item to a color group.
|
|
2048
|
+
|
|
2049
|
+
Args:
|
|
2050
|
+
group_name: Name of the color group.
|
|
2051
|
+
item_index: 0-based item index. Default: 0.
|
|
2052
|
+
"""
|
|
2053
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2054
|
+
if err:
|
|
2055
|
+
return err
|
|
2056
|
+
project = resolve.GetProjectManager().GetCurrentProject()
|
|
2057
|
+
groups = project.GetColorGroupsList()
|
|
2058
|
+
target = None
|
|
2059
|
+
if groups:
|
|
2060
|
+
for g in groups:
|
|
2061
|
+
if g.GetName() == group_name:
|
|
2062
|
+
target = g
|
|
2063
|
+
break
|
|
2064
|
+
if not target:
|
|
2065
|
+
return {"error": f"Color group '{group_name}' not found"}
|
|
2066
|
+
return {"success": bool(item.AssignToColorGroup(target))}
|
|
2067
|
+
|
|
2068
|
+
|
|
2069
|
+
@mcp.tool()
|
|
2070
|
+
def ti_remove_from_color_group(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2071
|
+
"""Remove a timeline item from its color group.
|
|
2072
|
+
|
|
2073
|
+
Args:
|
|
2074
|
+
item_index: 0-based item index. Default: 0.
|
|
2075
|
+
"""
|
|
2076
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2077
|
+
if err:
|
|
2078
|
+
return err
|
|
2079
|
+
return {"success": bool(item.RemoveFromColorGroup())}
|
|
2080
|
+
|
|
2081
|
+
|
|
2082
|
+
@mcp.tool()
|
|
2083
|
+
def ti_export_lut(export_type: str, path: str, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2084
|
+
"""Export LUT from a timeline item.
|
|
2085
|
+
|
|
2086
|
+
Args:
|
|
2087
|
+
export_type: LUT type ('EXPORT_LUT_17PTCUBE', 'EXPORT_LUT_33PTCUBE', 'EXPORT_LUT_65PTCUBE', 'EXPORT_LUT_PANASONICVLUT').
|
|
2088
|
+
path: Output file path.
|
|
2089
|
+
item_index: 0-based item index. Default: 0.
|
|
2090
|
+
"""
|
|
2091
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2092
|
+
if err:
|
|
2093
|
+
return err
|
|
2094
|
+
try:
|
|
2095
|
+
etype = getattr(resolve, export_type) if hasattr(resolve, export_type) else export_type
|
|
2096
|
+
except Exception:
|
|
2097
|
+
etype = export_type
|
|
2098
|
+
return {"success": bool(item.ExportLUT(etype, path))}
|
|
2099
|
+
|
|
2100
|
+
|
|
2101
|
+
@mcp.tool()
|
|
2102
|
+
def ti_get_linked_items(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2103
|
+
"""Get items linked to a timeline item.
|
|
2104
|
+
|
|
2105
|
+
Args:
|
|
2106
|
+
item_index: 0-based item index. Default: 0.
|
|
2107
|
+
"""
|
|
2108
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2109
|
+
if err:
|
|
2110
|
+
return err
|
|
2111
|
+
linked = item.GetLinkedItems()
|
|
2112
|
+
if linked:
|
|
2113
|
+
return {"linked_items": [{"name": li.GetName(), "unique_id": li.GetUniqueId()} for li in linked]}
|
|
2114
|
+
return {"linked_items": []}
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
@mcp.tool()
|
|
2118
|
+
def ti_get_track_type_and_index(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2119
|
+
"""Get the track type and index for a timeline item.
|
|
2120
|
+
|
|
2121
|
+
Args:
|
|
2122
|
+
item_index: 0-based item index. Default: 0.
|
|
2123
|
+
"""
|
|
2124
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2125
|
+
if err:
|
|
2126
|
+
return err
|
|
2127
|
+
result = item.GetTrackTypeAndIndex()
|
|
2128
|
+
return {"track_type": result[0] if result else "", "track_index": result[1] if result and len(result) > 1 else 0}
|
|
2129
|
+
|
|
2130
|
+
|
|
2131
|
+
@mcp.tool()
|
|
2132
|
+
def ti_get_source_audio_channel_mapping(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2133
|
+
"""Get source audio channel mapping for a timeline item.
|
|
2134
|
+
|
|
2135
|
+
Args:
|
|
2136
|
+
item_index: 0-based item index. Default: 0.
|
|
2137
|
+
"""
|
|
2138
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2139
|
+
if err:
|
|
2140
|
+
return err
|
|
2141
|
+
mapping = item.GetSourceAudioChannelMapping()
|
|
2142
|
+
return {"audio_channel_mapping": mapping if mapping else ""}
|
|
2143
|
+
|
|
2144
|
+
|
|
2145
|
+
@mcp.tool()
|
|
2146
|
+
def ti_get_stereo_convergence_values(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2147
|
+
"""Get stereo convergence values for a timeline item.
|
|
2148
|
+
|
|
2149
|
+
Args:
|
|
2150
|
+
item_index: 0-based item index. Default: 0.
|
|
2151
|
+
"""
|
|
2152
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2153
|
+
if err:
|
|
2154
|
+
return err
|
|
2155
|
+
return {"convergence": item.GetStereoConvergenceValues() or {}}
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
@mcp.tool()
|
|
2159
|
+
def ti_get_stereo_floating_window_params(eye: str = "left", item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2160
|
+
"""Get stereo floating window parameters.
|
|
2161
|
+
|
|
2162
|
+
Args:
|
|
2163
|
+
eye: 'left' or 'right'. Default: 'left'.
|
|
2164
|
+
item_index: 0-based item index. Default: 0.
|
|
2165
|
+
"""
|
|
2166
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2167
|
+
if err:
|
|
2168
|
+
return err
|
|
2169
|
+
if eye == "left":
|
|
2170
|
+
return {"params": item.GetStereoLeftFloatingWindowParams() or {}}
|
|
2171
|
+
else:
|
|
2172
|
+
return {"params": item.GetStereoRightFloatingWindowParams() or {}}
|
|
2173
|
+
|
|
2174
|
+
|
|
2175
|
+
@mcp.tool()
|
|
2176
|
+
def ti_get_media_pool_item(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2177
|
+
"""Get the MediaPoolItem associated with a timeline item.
|
|
2178
|
+
|
|
2179
|
+
Args:
|
|
2180
|
+
item_index: 0-based item index. Default: 0.
|
|
2181
|
+
"""
|
|
2182
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2183
|
+
if err:
|
|
2184
|
+
return err
|
|
2185
|
+
mpi = item.GetMediaPoolItem()
|
|
2186
|
+
if mpi:
|
|
2187
|
+
return {"name": mpi.GetName(), "unique_id": mpi.GetUniqueId()}
|
|
2188
|
+
return {"media_pool_item": None}
|
|
2189
|
+
|
|
2190
|
+
|
|
2191
|
+
@mcp.tool()
|
|
2192
|
+
def ti_get_cache_status(item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2193
|
+
"""Get cache status for a timeline item.
|
|
2194
|
+
|
|
2195
|
+
Args:
|
|
2196
|
+
item_index: 0-based item index. Default: 0.
|
|
2197
|
+
"""
|
|
2198
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2199
|
+
if err:
|
|
2200
|
+
return err
|
|
2201
|
+
return {
|
|
2202
|
+
"color_output_cache_enabled": bool(item.GetIsColorOutputCacheEnabled()),
|
|
2203
|
+
"fusion_output_cache_enabled": bool(item.GetIsFusionOutputCacheEnabled())
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
|
|
2207
|
+
@mcp.tool()
|
|
2208
|
+
def ti_set_color_output_cache(enabled: bool, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2209
|
+
"""Enable/disable color output cache for a timeline item.
|
|
2210
|
+
|
|
2211
|
+
Args:
|
|
2212
|
+
enabled: True to enable, False to disable.
|
|
2213
|
+
item_index: 0-based item index. Default: 0.
|
|
2214
|
+
"""
|
|
2215
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2216
|
+
if err:
|
|
2217
|
+
return err
|
|
2218
|
+
return {"success": bool(item.SetColorOutputCache(enabled))}
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
@mcp.tool()
|
|
2222
|
+
def ti_set_fusion_output_cache(enabled: bool, item_index: int = 0, track_type: str = "video", track_index: int = 1) -> Dict[str, Any]:
|
|
2223
|
+
"""Enable/disable Fusion output cache for a timeline item.
|
|
2224
|
+
|
|
2225
|
+
Args:
|
|
2226
|
+
enabled: True to enable, False to disable.
|
|
2227
|
+
item_index: 0-based item index. Default: 0.
|
|
2228
|
+
"""
|
|
2229
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2230
|
+
if err:
|
|
2231
|
+
return err
|
|
2232
|
+
return {"success": bool(item.SetFusionOutputCache(enabled))}
|
|
2233
|
+
|
|
2234
|
+
|
|
2235
|
+
@mcp.tool()
|
|
2236
|
+
def get_fusion_comp_by_name(comp_name: str, track_type: str = "video", track_index: int = 1, item_index: int = 0) -> Dict[str, Any]:
|
|
2237
|
+
"""Get a Fusion composition from a timeline item by name.
|
|
2238
|
+
|
|
2239
|
+
Args:
|
|
2240
|
+
comp_name: Name of the Fusion composition to retrieve.
|
|
2241
|
+
track_type: Track type ('video', 'audio', 'subtitle').
|
|
2242
|
+
track_index: Track index (1-based).
|
|
2243
|
+
item_index: Item index on the track (0-based).
|
|
2244
|
+
"""
|
|
2245
|
+
item, err = _get_timeline_item(track_type, track_index, item_index)
|
|
2246
|
+
if err:
|
|
2247
|
+
return err
|
|
2248
|
+
comp = item.GetFusionCompByName(comp_name)
|
|
2249
|
+
if comp:
|
|
2250
|
+
return {"success": True, "comp_name": comp_name, "comp_available": True}
|
|
2251
|
+
return {"success": False, "error": f"Fusion composition '{comp_name}' not found on this timeline item"}
|