@synthetic-ai/premiere-mcp 2.0.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/CHANGELOG.md +73 -0
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/cep-plugin/.debug +8 -0
- package/cep-plugin/CSInterface.js +70 -0
- package/cep-plugin/CSXS/manifest.xml +49 -0
- package/cep-plugin/host.jsx +7 -0
- package/cep-plugin/index.html +419 -0
- package/cep-plugin/main.js +327 -0
- package/cep-plugin/premiere.jsx +24947 -0
- package/cep-plugin/synthetic-logo.png +0 -0
- package/dist/bridge/file-bridge.d.ts +29 -0
- package/dist/bridge/file-bridge.js +145 -0
- package/dist/bridge/script-builder.d.ts +19 -0
- package/dist/bridge/script-builder.js +408 -0
- package/dist/http-server.d.ts +21 -0
- package/dist/http-server.js +140 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +70 -0
- package/dist/resources/extendscript-reference.d.ts +2 -0
- package/dist/resources/extendscript-reference.js +219 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +276 -0
- package/dist/tools/advanced.d.ts +530 -0
- package/dist/tools/advanced.js +923 -0
- package/dist/tools/audio-advanced.d.ts +847 -0
- package/dist/tools/audio-advanced.js +1653 -0
- package/dist/tools/audio.d.ts +84 -0
- package/dist/tools/audio.js +123 -0
- package/dist/tools/captions.d.ts +30 -0
- package/dist/tools/captions.js +73 -0
- package/dist/tools/clipboard.d.ts +153 -0
- package/dist/tools/clipboard.js +381 -0
- package/dist/tools/color-advanced.d.ts +821 -0
- package/dist/tools/color-advanced.js +772 -0
- package/dist/tools/discovery.d.ts +128 -0
- package/dist/tools/discovery.js +445 -0
- package/dist/tools/effects.d.ts +211 -0
- package/dist/tools/effects.js +554 -0
- package/dist/tools/export-presets.d.ts +333 -0
- package/dist/tools/export-presets.js +490 -0
- package/dist/tools/export.d.ts +320 -0
- package/dist/tools/export.js +618 -0
- package/dist/tools/health.d.ts +9 -0
- package/dist/tools/health.js +25 -0
- package/dist/tools/inspection.d.ts +179 -0
- package/dist/tools/inspection.js +1089 -0
- package/dist/tools/keyframes.d.ts +253 -0
- package/dist/tools/keyframes.js +611 -0
- package/dist/tools/markers.d.ts +117 -0
- package/dist/tools/markers.js +206 -0
- package/dist/tools/media.d.ts +297 -0
- package/dist/tools/media.js +493 -0
- package/dist/tools/metadata.d.ts +178 -0
- package/dist/tools/metadata.js +278 -0
- package/dist/tools/multicam.d.ts +199 -0
- package/dist/tools/multicam.js +212 -0
- package/dist/tools/playback.d.ts +34 -0
- package/dist/tools/playback.js +63 -0
- package/dist/tools/playhead.d.ts +77 -0
- package/dist/tools/playhead.js +185 -0
- package/dist/tools/project-manager.d.ts +65 -0
- package/dist/tools/project-manager.js +81 -0
- package/dist/tools/project.d.ts +390 -0
- package/dist/tools/project.js +596 -0
- package/dist/tools/scripting.d.ts +98 -0
- package/dist/tools/scripting.js +434 -0
- package/dist/tools/selection.d.ts +120 -0
- package/dist/tools/selection.js +334 -0
- package/dist/tools/sequence-advanced.d.ts +84 -0
- package/dist/tools/sequence-advanced.js +471 -0
- package/dist/tools/sequence-settings.d.ts +397 -0
- package/dist/tools/sequence-settings.js +967 -0
- package/dist/tools/sequence.d.ts +211 -0
- package/dist/tools/sequence.js +378 -0
- package/dist/tools/source-monitor.d.ts +95 -0
- package/dist/tools/source-monitor.js +180 -0
- package/dist/tools/text.d.ts +97 -0
- package/dist/tools/text.js +159 -0
- package/dist/tools/timeline.d.ts +265 -0
- package/dist/tools/timeline.js +575 -0
- package/dist/tools/track-management.d.ts +482 -0
- package/dist/tools/track-management.js +787 -0
- package/dist/tools/track-targeting.d.ts +666 -0
- package/dist/tools/track-targeting.js +1318 -0
- package/dist/tools/tracks.d.ts +90 -0
- package/dist/tools/tracks.js +183 -0
- package/dist/tools/transitions-motion.d.ts +788 -0
- package/dist/tools/transitions-motion.js +680 -0
- package/dist/tools/transitions.d.ts +124 -0
- package/dist/tools/transitions.js +267 -0
- package/dist/tools/utility.d.ts +469 -0
- package/dist/tools/utility.js +1118 -0
- package/dist/tools/workspace.d.ts +25 -0
- package/dist/tools/workspace.js +61 -0
- package/package.json +66 -0
- package/scripts/build-chat-dmg.sh +210 -0
- package/scripts/build-chat-zip.sh +291 -0
- package/scripts/install-cep-windows.ps1 +99 -0
- package/scripts/install-cep.sh +66 -0
- package/scripts/install-chat-plugin.bat +67 -0
- package/scripts/install-chat-plugin.sh +85 -0
- package/scripts/install-windows.bat +95 -0
- package/scripts/package.mjs +218 -0
|
@@ -0,0 +1,1118 @@
|
|
|
1
|
+
import { buildToolScript, escapeForExtendScript } from "../bridge/script-builder.js";
|
|
2
|
+
import { sendCommand } from "../bridge/file-bridge.js";
|
|
3
|
+
export function getUtilityTools(bridgeOptions) {
|
|
4
|
+
return {
|
|
5
|
+
delete_project_item: {
|
|
6
|
+
description: "Delete a project item (clip, bin, etc.) from the project panel. This removes it from the project but does not affect timeline instances.",
|
|
7
|
+
parameters: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
item_id: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Node ID or name of the project item to delete",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
required: ["item_id"],
|
|
16
|
+
},
|
|
17
|
+
handler: async (args) => {
|
|
18
|
+
const script = buildToolScript(`
|
|
19
|
+
var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
|
|
20
|
+
if (!item) return __error("Item not found: ${escapeForExtendScript(args.item_id)}");
|
|
21
|
+
|
|
22
|
+
var name = item.name;
|
|
23
|
+
app.project.deleteProjectItem(item);
|
|
24
|
+
return __result({ deleted: true, name: name });
|
|
25
|
+
`);
|
|
26
|
+
return sendCommand(script, bridgeOptions);
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
delete_multiple_project_items: {
|
|
30
|
+
description: "Delete multiple project items at once from the project panel.",
|
|
31
|
+
parameters: {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
item_ids: {
|
|
35
|
+
type: "array",
|
|
36
|
+
description: "Array of node IDs or names of items to delete",
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ["item_ids"],
|
|
40
|
+
},
|
|
41
|
+
handler: async (args) => {
|
|
42
|
+
const idsJson = JSON.stringify(args.item_ids);
|
|
43
|
+
const script = buildToolScript(`
|
|
44
|
+
var ids = ${idsJson};
|
|
45
|
+
var deleted = 0;
|
|
46
|
+
var names = [];
|
|
47
|
+
for (var i = 0; i < ids.length; i++) {
|
|
48
|
+
var item = __findProjectItem(ids[i]);
|
|
49
|
+
if (item) {
|
|
50
|
+
names.push(item.name);
|
|
51
|
+
try { app.project.deleteProjectItem(item); deleted++; } catch(e) {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return __result({ deleted: deleted, total: ids.length, names: names });
|
|
55
|
+
`);
|
|
56
|
+
return sendCommand(script, bridgeOptions);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
rename_project_item: {
|
|
60
|
+
description: "Rename a project item in the project panel.",
|
|
61
|
+
parameters: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
item_id: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Node ID or name of the project item",
|
|
67
|
+
},
|
|
68
|
+
new_name: {
|
|
69
|
+
type: "string",
|
|
70
|
+
description: "New name for the item",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
required: ["item_id", "new_name"],
|
|
74
|
+
},
|
|
75
|
+
handler: async (args) => {
|
|
76
|
+
const script = buildToolScript(`
|
|
77
|
+
var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
|
|
78
|
+
if (!item) return __error("Item not found");
|
|
79
|
+
|
|
80
|
+
var oldName = item.name;
|
|
81
|
+
item.name = "${escapeForExtendScript(args.new_name)}";
|
|
82
|
+
return __result({ oldName: oldName, newName: item.name });
|
|
83
|
+
`);
|
|
84
|
+
return sendCommand(script, bridgeOptions);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
add_adjustment_layer: {
|
|
88
|
+
description: "Add an adjustment layer to the active sequence via QE DOM. The layer is added at the playhead position on the specified track.",
|
|
89
|
+
parameters: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
track_index: {
|
|
93
|
+
type: "number",
|
|
94
|
+
description: "Video track index (default: 0)",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
handler: async (args) => {
|
|
99
|
+
const track = args.track_index ?? 0;
|
|
100
|
+
const script = buildToolScript(`
|
|
101
|
+
var seq = app.project.activeSequence;
|
|
102
|
+
if (!seq) return __error("No active sequence");
|
|
103
|
+
|
|
104
|
+
app.enableQE();
|
|
105
|
+
try {
|
|
106
|
+
var qeSeq = qe.project.getActiveSequence();
|
|
107
|
+
qeSeq.addAdjustmentLayer(${track});
|
|
108
|
+
|
|
109
|
+
return __result({
|
|
110
|
+
added: true,
|
|
111
|
+
trackIndex: ${track}
|
|
112
|
+
});
|
|
113
|
+
} catch(e) {
|
|
114
|
+
return __error("Failed to add adjustment layer: " + e.message);
|
|
115
|
+
}
|
|
116
|
+
`);
|
|
117
|
+
return sendCommand(script, bridgeOptions);
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
freeze_frame: {
|
|
121
|
+
description: "Create a freeze frame from a clip at a specific time. Exports the frame and imports it back as a still image.",
|
|
122
|
+
parameters: {
|
|
123
|
+
type: "object",
|
|
124
|
+
properties: {
|
|
125
|
+
time_seconds: {
|
|
126
|
+
type: "number",
|
|
127
|
+
description: "Time in the sequence to freeze (in seconds). Uses playhead if omitted.",
|
|
128
|
+
},
|
|
129
|
+
output_path: {
|
|
130
|
+
type: "string",
|
|
131
|
+
description: "Full path for the exported frame (e.g., /path/to/freeze.png)",
|
|
132
|
+
},
|
|
133
|
+
duration_seconds: {
|
|
134
|
+
type: "number",
|
|
135
|
+
description: "Duration of the freeze frame on the timeline (default: 2)",
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
required: ["output_path"],
|
|
139
|
+
},
|
|
140
|
+
handler: async (args) => {
|
|
141
|
+
const script = buildToolScript(`
|
|
142
|
+
var seq = app.project.activeSequence;
|
|
143
|
+
if (!seq) return __error("No active sequence");
|
|
144
|
+
|
|
145
|
+
${args.time_seconds !== undefined
|
|
146
|
+
? `var timeTicks = __secondsToTicks(${args.time_seconds}).toString();`
|
|
147
|
+
: `var timeTicks = seq.getPlayerPosition().ticks;`}
|
|
148
|
+
|
|
149
|
+
// Export frame
|
|
150
|
+
seq.exportFramePNG(timeTicks, "${escapeForExtendScript(args.output_path)}");
|
|
151
|
+
|
|
152
|
+
// Import back
|
|
153
|
+
app.project.importFiles(["${escapeForExtendScript(args.output_path)}"], false, app.project.rootItem, false);
|
|
154
|
+
|
|
155
|
+
return __result({
|
|
156
|
+
exported: true,
|
|
157
|
+
path: "${escapeForExtendScript(args.output_path)}",
|
|
158
|
+
atSeconds: __ticksToSeconds(timeTicks),
|
|
159
|
+
note: "Frame exported and imported. Add to timeline with add_to_timeline."
|
|
160
|
+
});
|
|
161
|
+
`);
|
|
162
|
+
return sendCommand(script, bridgeOptions);
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
set_sequence_frame_rate: {
|
|
166
|
+
description: "Change the frame rate of the active sequence.",
|
|
167
|
+
parameters: {
|
|
168
|
+
type: "object",
|
|
169
|
+
properties: {
|
|
170
|
+
frame_rate: {
|
|
171
|
+
type: "number",
|
|
172
|
+
description: "New frame rate (e.g., 23.976, 24, 25, 29.97, 30, 50, 59.94, 60)",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
required: ["frame_rate"],
|
|
176
|
+
},
|
|
177
|
+
handler: async (args) => {
|
|
178
|
+
const script = buildToolScript(`
|
|
179
|
+
var seq = app.project.activeSequence;
|
|
180
|
+
if (!seq) return __error("No active sequence");
|
|
181
|
+
|
|
182
|
+
var settings = seq.getSettings();
|
|
183
|
+
if (!settings) return __error("Could not get sequence settings");
|
|
184
|
+
|
|
185
|
+
var fps = ${args.frame_rate};
|
|
186
|
+
if (!fps || fps <= 0) return __error("Invalid frame rate: " + fps);
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
var t = new Time();
|
|
190
|
+
t.ticks = Math.round(TICKS_PER_SECOND / fps).toString();
|
|
191
|
+
settings.videoFrameRate = t;
|
|
192
|
+
seq.setSettings(settings);
|
|
193
|
+
} catch(e) {
|
|
194
|
+
return __error("Failed to set frame rate: " + e.message);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return __result({ frameRate: fps, ticksPerFrame: Math.round(TICKS_PER_SECOND / fps), sequence: seq.name });
|
|
198
|
+
`);
|
|
199
|
+
return sendCommand(script, bridgeOptions);
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
set_sequence_resolution: {
|
|
203
|
+
description: "Change the resolution (frame size) of the active sequence.",
|
|
204
|
+
parameters: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
width: {
|
|
208
|
+
type: "number",
|
|
209
|
+
description: "Width in pixels",
|
|
210
|
+
},
|
|
211
|
+
height: {
|
|
212
|
+
type: "number",
|
|
213
|
+
description: "Height in pixels",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
required: ["width", "height"],
|
|
217
|
+
},
|
|
218
|
+
handler: async (args) => {
|
|
219
|
+
const script = buildToolScript(`
|
|
220
|
+
var seq = app.project.activeSequence;
|
|
221
|
+
if (!seq) return __error("No active sequence");
|
|
222
|
+
|
|
223
|
+
var settings = seq.getSettings();
|
|
224
|
+
if (!settings) return __error("Could not get sequence settings");
|
|
225
|
+
|
|
226
|
+
settings.videoFrameWidth = ${args.width};
|
|
227
|
+
settings.videoFrameHeight = ${args.height};
|
|
228
|
+
seq.setSettings(settings);
|
|
229
|
+
|
|
230
|
+
return __result({ width: ${args.width}, height: ${args.height}, sequence: seq.name });
|
|
231
|
+
`);
|
|
232
|
+
return sendCommand(script, bridgeOptions);
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
set_sequence_audio_settings: {
|
|
236
|
+
description: "Change audio settings of the active sequence (sample rate, channel type).",
|
|
237
|
+
parameters: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
sample_rate: {
|
|
241
|
+
type: "number",
|
|
242
|
+
description: "Audio sample rate (e.g., 44100, 48000, 96000)",
|
|
243
|
+
},
|
|
244
|
+
channel_type: {
|
|
245
|
+
type: "number",
|
|
246
|
+
description: "Channel type: 0=Mono, 1=Stereo, 2=5.1, 3=Multichannel",
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
handler: async (args) => {
|
|
251
|
+
const script = buildToolScript(`
|
|
252
|
+
var seq = app.project.activeSequence;
|
|
253
|
+
if (!seq) return __error("No active sequence");
|
|
254
|
+
|
|
255
|
+
var settings = seq.getSettings();
|
|
256
|
+
if (!settings) return __error("Could not get sequence settings");
|
|
257
|
+
|
|
258
|
+
${args.sample_rate !== undefined ? `settings.audioSampleRate = ${args.sample_rate};` : ""}
|
|
259
|
+
${args.channel_type !== undefined ? `settings.audioChannelType = ${args.channel_type};` : ""}
|
|
260
|
+
seq.setSettings(settings);
|
|
261
|
+
|
|
262
|
+
return __result({ sequence: seq.name, sampleRate: settings.audioSampleRate, channelType: settings.audioChannelType });
|
|
263
|
+
`);
|
|
264
|
+
return sendCommand(script, bridgeOptions);
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
set_sequence_pixel_aspect_ratio: {
|
|
268
|
+
description: "Change the pixel aspect ratio of the active sequence.",
|
|
269
|
+
parameters: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
ratio: {
|
|
273
|
+
type: "number",
|
|
274
|
+
description: "Pixel aspect ratio (1.0 for square pixels, 1.4222 for 16:9 DV, etc.)",
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
required: ["ratio"],
|
|
278
|
+
},
|
|
279
|
+
handler: async (args) => {
|
|
280
|
+
const script = buildToolScript(`
|
|
281
|
+
var seq = app.project.activeSequence;
|
|
282
|
+
if (!seq) return __error("No active sequence");
|
|
283
|
+
|
|
284
|
+
var settings = seq.getSettings();
|
|
285
|
+
if (!settings) return __error("Could not get sequence settings");
|
|
286
|
+
|
|
287
|
+
settings.videoPixelAspectRatio = ${args.ratio};
|
|
288
|
+
seq.setSettings(settings);
|
|
289
|
+
|
|
290
|
+
return __result({ ratio: ${args.ratio}, sequence: seq.name });
|
|
291
|
+
`);
|
|
292
|
+
return sendCommand(script, bridgeOptions);
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
set_sequence_field_type: {
|
|
296
|
+
description: "Set the field order of the active sequence.",
|
|
297
|
+
parameters: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
field_type: {
|
|
301
|
+
type: "number",
|
|
302
|
+
description: "0=No Fields (Progressive), 1=Upper Field First, 2=Lower Field First",
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
required: ["field_type"],
|
|
306
|
+
},
|
|
307
|
+
handler: async (args) => {
|
|
308
|
+
const script = buildToolScript(`
|
|
309
|
+
var seq = app.project.activeSequence;
|
|
310
|
+
if (!seq) return __error("No active sequence");
|
|
311
|
+
|
|
312
|
+
var settings = seq.getSettings();
|
|
313
|
+
if (!settings) return __error("Could not get sequence settings");
|
|
314
|
+
|
|
315
|
+
settings.videoFieldType = ${args.field_type};
|
|
316
|
+
seq.setSettings(settings);
|
|
317
|
+
|
|
318
|
+
return __result({ fieldType: ${args.field_type}, sequence: seq.name });
|
|
319
|
+
`);
|
|
320
|
+
return sendCommand(script, bridgeOptions);
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
get_all_project_paths: {
|
|
324
|
+
description: "Get all unique media file paths used in the project. Useful for asset management and archiving.",
|
|
325
|
+
parameters: {},
|
|
326
|
+
handler: async () => {
|
|
327
|
+
const script = buildToolScript(`
|
|
328
|
+
var paths = {};
|
|
329
|
+
function scan(bin) {
|
|
330
|
+
for (var i = 0; i < bin.children.numItems; i++) {
|
|
331
|
+
var item = bin.children[i];
|
|
332
|
+
try {
|
|
333
|
+
var mp = item.getMediaPath();
|
|
334
|
+
if (mp && !paths[mp]) {
|
|
335
|
+
paths[mp] = {
|
|
336
|
+
path: mp,
|
|
337
|
+
name: item.name,
|
|
338
|
+
nodeId: item.nodeId,
|
|
339
|
+
offline: false
|
|
340
|
+
};
|
|
341
|
+
try { paths[mp].offline = item.isOffline(); } catch(e) {}
|
|
342
|
+
}
|
|
343
|
+
} catch(e) {}
|
|
344
|
+
if (item.type === 2) scan(item);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
scan(app.project.rootItem);
|
|
348
|
+
|
|
349
|
+
var result = [];
|
|
350
|
+
for (var key in paths) {
|
|
351
|
+
if (paths.hasOwnProperty(key)) result.push(paths[key]);
|
|
352
|
+
}
|
|
353
|
+
return __result({ pathCount: result.length, paths: result });
|
|
354
|
+
`);
|
|
355
|
+
return sendCommand(script, bridgeOptions);
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
get_unused_media: {
|
|
359
|
+
description: "Find all project items that are NOT used in any sequence. Useful for cleaning up projects.",
|
|
360
|
+
parameters: {},
|
|
361
|
+
handler: async () => {
|
|
362
|
+
const script = buildToolScript(`
|
|
363
|
+
// First, collect all project item nodeIds used in any sequence
|
|
364
|
+
var usedIds = {};
|
|
365
|
+
for (var s = 0; s < app.project.sequences.numSequences; s++) {
|
|
366
|
+
var seq = app.project.sequences[s];
|
|
367
|
+
function scanTracks(tracks) {
|
|
368
|
+
for (var t = 0; t < tracks.numTracks; t++) {
|
|
369
|
+
for (var c = 0; c < tracks[t].clips.numItems; c++) {
|
|
370
|
+
try {
|
|
371
|
+
var src = tracks[t].clips[c].projectItem;
|
|
372
|
+
if (src) usedIds[src.nodeId] = true;
|
|
373
|
+
} catch(e) {}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
scanTracks(seq.videoTracks);
|
|
378
|
+
scanTracks(seq.audioTracks);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Then find items not in usedIds
|
|
382
|
+
var unused = [];
|
|
383
|
+
function findUnused(bin) {
|
|
384
|
+
for (var i = 0; i < bin.children.numItems; i++) {
|
|
385
|
+
var item = bin.children[i];
|
|
386
|
+
if (item.type === 1 || item.type === 4) { // clips and files
|
|
387
|
+
if (!usedIds[item.nodeId]) {
|
|
388
|
+
var entry = {
|
|
389
|
+
nodeId: item.nodeId,
|
|
390
|
+
name: item.name,
|
|
391
|
+
treePath: item.treePath
|
|
392
|
+
};
|
|
393
|
+
try { entry.mediaPath = item.getMediaPath(); } catch(e) {}
|
|
394
|
+
unused.push(entry);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (item.type === 2) findUnused(item);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
findUnused(app.project.rootItem);
|
|
401
|
+
|
|
402
|
+
return __result({ unusedCount: unused.length, items: unused });
|
|
403
|
+
`);
|
|
404
|
+
return sendCommand(script, bridgeOptions);
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
get_duplicate_media: {
|
|
408
|
+
description: "Find project items that reference the same source media file. Useful for consolidation.",
|
|
409
|
+
parameters: {},
|
|
410
|
+
handler: async () => {
|
|
411
|
+
const script = buildToolScript(`
|
|
412
|
+
var pathMap = {};
|
|
413
|
+
function scan(bin) {
|
|
414
|
+
for (var i = 0; i < bin.children.numItems; i++) {
|
|
415
|
+
var item = bin.children[i];
|
|
416
|
+
try {
|
|
417
|
+
var mp = item.getMediaPath();
|
|
418
|
+
if (mp) {
|
|
419
|
+
if (!pathMap[mp]) pathMap[mp] = [];
|
|
420
|
+
pathMap[mp].push({ nodeId: item.nodeId, name: item.name, treePath: item.treePath });
|
|
421
|
+
}
|
|
422
|
+
} catch(e) {}
|
|
423
|
+
if (item.type === 2) scan(item);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
scan(app.project.rootItem);
|
|
427
|
+
|
|
428
|
+
var duplicates = [];
|
|
429
|
+
for (var path in pathMap) {
|
|
430
|
+
if (pathMap.hasOwnProperty(path) && pathMap[path].length > 1) {
|
|
431
|
+
duplicates.push({ mediaPath: path, count: pathMap[path].length, items: pathMap[path] });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return __result({ duplicateGroupCount: duplicates.length, duplicates: duplicates });
|
|
436
|
+
`);
|
|
437
|
+
return sendCommand(script, bridgeOptions);
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
lift_selection: {
|
|
441
|
+
description: "Lift (remove without closing gap) the content between sequence in/out points or selected clips.",
|
|
442
|
+
parameters: {},
|
|
443
|
+
handler: async () => {
|
|
444
|
+
const script = buildToolScript(`
|
|
445
|
+
app.enableQE();
|
|
446
|
+
var seq = app.project.activeSequence;
|
|
447
|
+
if (!seq) return __error("No active sequence");
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
var qeSeq = qe.project.getActiveSequence();
|
|
451
|
+
qeSeq.lift();
|
|
452
|
+
return __result({ lifted: true });
|
|
453
|
+
} catch(e) {
|
|
454
|
+
return __error("Lift failed: " + e.message);
|
|
455
|
+
}
|
|
456
|
+
`);
|
|
457
|
+
return sendCommand(script, bridgeOptions);
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
extract_selection: {
|
|
461
|
+
description: "Extract (remove and close gap) the content between sequence in/out points.",
|
|
462
|
+
parameters: {},
|
|
463
|
+
handler: async () => {
|
|
464
|
+
const script = buildToolScript(`
|
|
465
|
+
app.enableQE();
|
|
466
|
+
var seq = app.project.activeSequence;
|
|
467
|
+
if (!seq) return __error("No active sequence");
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
var qeSeq = qe.project.getActiveSequence();
|
|
471
|
+
qeSeq.extract();
|
|
472
|
+
return __result({ extracted: true });
|
|
473
|
+
} catch(e) {
|
|
474
|
+
return __error("Extract failed: " + e.message);
|
|
475
|
+
}
|
|
476
|
+
`);
|
|
477
|
+
return sendCommand(script, bridgeOptions);
|
|
478
|
+
},
|
|
479
|
+
},
|
|
480
|
+
get_clip_links: {
|
|
481
|
+
description: "Get information about linked clips (audio/video linked together) for a given clip.",
|
|
482
|
+
parameters: {
|
|
483
|
+
type: "object",
|
|
484
|
+
properties: {
|
|
485
|
+
node_id: {
|
|
486
|
+
type: "string",
|
|
487
|
+
description: "Node ID of the clip",
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
required: ["node_id"],
|
|
491
|
+
},
|
|
492
|
+
handler: async (args) => {
|
|
493
|
+
const script = buildToolScript(`
|
|
494
|
+
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
495
|
+
if (!result) return __error("Clip not found");
|
|
496
|
+
|
|
497
|
+
var clip = result.clip;
|
|
498
|
+
var info = {
|
|
499
|
+
nodeId: clip.nodeId,
|
|
500
|
+
name: clip.name,
|
|
501
|
+
trackType: result.trackType,
|
|
502
|
+
trackIndex: result.trackIndex
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Find linked clips by matching projectItem and overlapping time
|
|
506
|
+
var linked = [];
|
|
507
|
+
var seq = app.project.activeSequence;
|
|
508
|
+
var clipStart = clip.start.ticks;
|
|
509
|
+
var clipEnd = clip.end.ticks;
|
|
510
|
+
var srcId = null;
|
|
511
|
+
try { srcId = clip.projectItem ? clip.projectItem.nodeId : null; } catch(e) {}
|
|
512
|
+
|
|
513
|
+
function findLinked(tracks, type) {
|
|
514
|
+
for (var t = 0; t < tracks.numTracks; t++) {
|
|
515
|
+
for (var c = 0; c < tracks[t].clips.numItems; c++) {
|
|
516
|
+
var other = tracks[t].clips[c];
|
|
517
|
+
if (other.nodeId === clip.nodeId) continue;
|
|
518
|
+
// Check same source and overlapping time
|
|
519
|
+
try {
|
|
520
|
+
var otherSrcId = other.projectItem ? other.projectItem.nodeId : null;
|
|
521
|
+
if (srcId && otherSrcId === srcId && other.start.ticks === clipStart) {
|
|
522
|
+
linked.push({
|
|
523
|
+
nodeId: other.nodeId,
|
|
524
|
+
name: other.name,
|
|
525
|
+
trackType: type,
|
|
526
|
+
trackIndex: t,
|
|
527
|
+
startSeconds: __ticksToSeconds(other.start.ticks),
|
|
528
|
+
endSeconds: __ticksToSeconds(other.end.ticks)
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
} catch(e) {}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
findLinked(seq.videoTracks, "video");
|
|
537
|
+
findLinked(seq.audioTracks, "audio");
|
|
538
|
+
|
|
539
|
+
info.linkedClips = linked;
|
|
540
|
+
info.linkedCount = linked.length;
|
|
541
|
+
|
|
542
|
+
return __result(info);
|
|
543
|
+
`);
|
|
544
|
+
return sendCommand(script, bridgeOptions);
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
get_sequence_markers_by_type: {
|
|
548
|
+
description: "Get all markers of a specific type (comment, chapter, web link, etc.) from a sequence.",
|
|
549
|
+
parameters: {
|
|
550
|
+
type: "object",
|
|
551
|
+
properties: {
|
|
552
|
+
marker_type: {
|
|
553
|
+
type: "string",
|
|
554
|
+
enum: ["Comment", "Chapter", "Segmentation", "WebLink", "FlashCuePoint"],
|
|
555
|
+
description: "Type of marker to filter",
|
|
556
|
+
},
|
|
557
|
+
sequence_id: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: "Sequence name or ID. Uses active sequence if omitted.",
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
required: ["marker_type"],
|
|
563
|
+
},
|
|
564
|
+
handler: async (args) => {
|
|
565
|
+
const seqLookup = args.sequence_id
|
|
566
|
+
? `var seq = __findSequence("${escapeForExtendScript(args.sequence_id)}"); if (!seq) return __error("Sequence not found");`
|
|
567
|
+
: `var seq = app.project.activeSequence; if (!seq) return __error("No active sequence");`;
|
|
568
|
+
const script = buildToolScript(`
|
|
569
|
+
${seqLookup}
|
|
570
|
+
|
|
571
|
+
var markers = [];
|
|
572
|
+
var m = seq.markers.getFirstMarker();
|
|
573
|
+
while (m) {
|
|
574
|
+
if (m.type === "${escapeForExtendScript(args.marker_type)}") {
|
|
575
|
+
markers.push({
|
|
576
|
+
name: m.name,
|
|
577
|
+
comments: m.comments,
|
|
578
|
+
startSeconds: __ticksToSeconds(m.start.ticks),
|
|
579
|
+
endSeconds: __ticksToSeconds(m.end.ticks),
|
|
580
|
+
type: m.type
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
m = seq.markers.getNextMarker(m);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return __result({ type: "${escapeForExtendScript(args.marker_type)}", count: markers.length, markers: markers });
|
|
587
|
+
`);
|
|
588
|
+
return sendCommand(script, bridgeOptions);
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
get_clip_markers: {
|
|
592
|
+
description: "Get all markers on a specific project item (source clip markers, not sequence markers).",
|
|
593
|
+
parameters: {
|
|
594
|
+
type: "object",
|
|
595
|
+
properties: {
|
|
596
|
+
item_id: {
|
|
597
|
+
type: "string",
|
|
598
|
+
description: "Node ID or name of the project item",
|
|
599
|
+
},
|
|
600
|
+
},
|
|
601
|
+
required: ["item_id"],
|
|
602
|
+
},
|
|
603
|
+
handler: async (args) => {
|
|
604
|
+
const script = buildToolScript(`
|
|
605
|
+
// Accept project item IDs/names AND timeline clip nodeIds.
|
|
606
|
+
var item = __resolveProjectItemOrTimelineClip("${escapeForExtendScript(args.item_id)}");
|
|
607
|
+
if (!item) return __error("Item not found in project tree or timeline: ${escapeForExtendScript(args.item_id)}");
|
|
608
|
+
|
|
609
|
+
var markers = [];
|
|
610
|
+
try {
|
|
611
|
+
var m = item.getMarkers().getFirstMarker();
|
|
612
|
+
while (m) {
|
|
613
|
+
var mi = {
|
|
614
|
+
name: m.name,
|
|
615
|
+
comments: m.comments,
|
|
616
|
+
startSeconds: __ticksToSeconds(m.start.ticks),
|
|
617
|
+
type: m.type
|
|
618
|
+
};
|
|
619
|
+
try { mi.endSeconds = __ticksToSeconds(m.end.ticks); } catch(e) {}
|
|
620
|
+
try { mi.colorIndex = m.getColorByIndex(); } catch(e) {}
|
|
621
|
+
markers.push(mi);
|
|
622
|
+
m = item.getMarkers().getNextMarker(m);
|
|
623
|
+
}
|
|
624
|
+
} catch(e) {}
|
|
625
|
+
|
|
626
|
+
return __result({ item: item.name, markerCount: markers.length, markers: markers });
|
|
627
|
+
`);
|
|
628
|
+
return sendCommand(script, bridgeOptions);
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
add_marker_to_project_item: {
|
|
632
|
+
description: "Add a marker to a project item (source clip marker).",
|
|
633
|
+
parameters: {
|
|
634
|
+
type: "object",
|
|
635
|
+
properties: {
|
|
636
|
+
item_id: {
|
|
637
|
+
type: "string",
|
|
638
|
+
description: "Node ID or name of the project item",
|
|
639
|
+
},
|
|
640
|
+
time_seconds: {
|
|
641
|
+
type: "number",
|
|
642
|
+
description: "Time in seconds for the marker",
|
|
643
|
+
},
|
|
644
|
+
name: {
|
|
645
|
+
type: "string",
|
|
646
|
+
description: "Marker name",
|
|
647
|
+
},
|
|
648
|
+
comments: {
|
|
649
|
+
type: "string",
|
|
650
|
+
description: "Marker comments",
|
|
651
|
+
},
|
|
652
|
+
duration_seconds: {
|
|
653
|
+
type: "number",
|
|
654
|
+
description: "Duration of the marker in seconds (0 for point marker)",
|
|
655
|
+
},
|
|
656
|
+
type: {
|
|
657
|
+
type: "string",
|
|
658
|
+
enum: ["Comment", "Chapter", "Segmentation", "WebLink"],
|
|
659
|
+
description: "Marker type (default: Comment)",
|
|
660
|
+
},
|
|
661
|
+
color_index: {
|
|
662
|
+
type: "number",
|
|
663
|
+
description: "Color label index (0-7)",
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
required: ["item_id", "time_seconds"],
|
|
667
|
+
},
|
|
668
|
+
handler: async (args) => {
|
|
669
|
+
const script = buildToolScript(`
|
|
670
|
+
var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
|
|
671
|
+
if (!item) return __error("Item not found");
|
|
672
|
+
|
|
673
|
+
var markers = item.getMarkers();
|
|
674
|
+
var marker = markers.createMarker(${args.time_seconds});
|
|
675
|
+
|
|
676
|
+
${args.name ? `marker.name = "${escapeForExtendScript(args.name)}";` : ""}
|
|
677
|
+
${args.comments ? `marker.comments = "${escapeForExtendScript(args.comments)}";` : ""}
|
|
678
|
+
${args.type ? `marker.type = "${escapeForExtendScript(args.type)}";` : ""}
|
|
679
|
+
${args.duration_seconds !== undefined ? `
|
|
680
|
+
var endTime = new Time();
|
|
681
|
+
endTime.seconds = ${args.time_seconds + args.duration_seconds};
|
|
682
|
+
marker.end = endTime;
|
|
683
|
+
` : ""}
|
|
684
|
+
${args.color_index !== undefined ? `marker.setColorByIndex(${args.color_index});` : ""}
|
|
685
|
+
|
|
686
|
+
return __result({ added: true, item: item.name, timeSeconds: ${args.time_seconds} });
|
|
687
|
+
`);
|
|
688
|
+
return sendCommand(script, bridgeOptions);
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
set_sequence_display_format: {
|
|
692
|
+
description: "Set the timecode display format for the active sequence.",
|
|
693
|
+
parameters: {
|
|
694
|
+
type: "object",
|
|
695
|
+
properties: {
|
|
696
|
+
video_display_format: {
|
|
697
|
+
type: "number",
|
|
698
|
+
description: "Video: 0=24 Timecode, 1=25 Timecode, 2=29.97 Drop-frame, 3=29.97 Non-drop-frame, 4=30 Timecode, 5=50 Timecode, 6=59.94 Drop-frame, 7=59.94 Non-drop-frame, 8=60 Timecode, 9=Frames, 10=Feet+Frames 16mm, 11=Feet+Frames 35mm",
|
|
699
|
+
},
|
|
700
|
+
audio_display_format: {
|
|
701
|
+
type: "number",
|
|
702
|
+
description: "Audio: 0=Audio Samples, 1=Milliseconds",
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
handler: async (args) => {
|
|
707
|
+
const script = buildToolScript(`
|
|
708
|
+
var seq = app.project.activeSequence;
|
|
709
|
+
if (!seq) return __error("No active sequence");
|
|
710
|
+
|
|
711
|
+
var settings = seq.getSettings();
|
|
712
|
+
if (!settings) return __error("Could not get sequence settings");
|
|
713
|
+
|
|
714
|
+
${args.video_display_format !== undefined ? `settings.videoDisplayFormat = ${args.video_display_format};` : ""}
|
|
715
|
+
${args.audio_display_format !== undefined ? `settings.audioDisplayFormat = ${args.audio_display_format};` : ""}
|
|
716
|
+
seq.setSettings(settings);
|
|
717
|
+
|
|
718
|
+
return __result({ sequence: seq.name, videoDisplayFormat: settings.videoDisplayFormat, audioDisplayFormat: settings.audioDisplayFormat });
|
|
719
|
+
`);
|
|
720
|
+
return sendCommand(script, bridgeOptions);
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
get_clip_at_playhead: {
|
|
724
|
+
description: "Get all clips at the current playhead position across all tracks.",
|
|
725
|
+
parameters: {
|
|
726
|
+
type: "object",
|
|
727
|
+
properties: {
|
|
728
|
+
track_type: {
|
|
729
|
+
type: "string",
|
|
730
|
+
enum: ["video", "audio", "both"],
|
|
731
|
+
description: "Track type to check (default: both)",
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
handler: async (args) => {
|
|
736
|
+
const trackType = args.track_type || "both";
|
|
737
|
+
const script = buildToolScript(`
|
|
738
|
+
var seq = app.project.activeSequence;
|
|
739
|
+
if (!seq) return __error("No active sequence");
|
|
740
|
+
|
|
741
|
+
var posTicks = parseFloat(seq.getPlayerPosition().ticks);
|
|
742
|
+
var clips = [];
|
|
743
|
+
|
|
744
|
+
function findAtPlayhead(tracks, type) {
|
|
745
|
+
for (var t = 0; t < tracks.numTracks; t++) {
|
|
746
|
+
for (var c = 0; c < tracks[t].clips.numItems; c++) {
|
|
747
|
+
var clip = tracks[t].clips[c];
|
|
748
|
+
var cs = parseFloat(clip.start.ticks);
|
|
749
|
+
var ce = parseFloat(clip.end.ticks);
|
|
750
|
+
if (cs <= posTicks && ce > posTicks) {
|
|
751
|
+
var ci = {
|
|
752
|
+
nodeId: clip.nodeId,
|
|
753
|
+
name: clip.name,
|
|
754
|
+
trackType: type,
|
|
755
|
+
trackIndex: t,
|
|
756
|
+
trackName: tracks[t].name,
|
|
757
|
+
clipIndex: c,
|
|
758
|
+
startSeconds: __ticksToSeconds(clip.start.ticks),
|
|
759
|
+
endSeconds: __ticksToSeconds(clip.end.ticks)
|
|
760
|
+
};
|
|
761
|
+
try { ci.enabled = !clip.isDisabled(); } catch(e) { ci.enabled = true; }
|
|
762
|
+
clips.push(ci);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if ("${trackType}" !== "audio") findAtPlayhead(seq.videoTracks, "video");
|
|
769
|
+
if ("${trackType}" !== "video") findAtPlayhead(seq.audioTracks, "audio");
|
|
770
|
+
|
|
771
|
+
return __result({
|
|
772
|
+
playheadSeconds: __ticksToSeconds(seq.getPlayerPosition().ticks),
|
|
773
|
+
clipCount: clips.length,
|
|
774
|
+
clips: clips
|
|
775
|
+
});
|
|
776
|
+
`);
|
|
777
|
+
return sendCommand(script, bridgeOptions);
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
get_next_edit_point: {
|
|
781
|
+
description: "Find the next or previous edit point (clip boundary) from the playhead position.",
|
|
782
|
+
parameters: {
|
|
783
|
+
type: "object",
|
|
784
|
+
properties: {
|
|
785
|
+
direction: {
|
|
786
|
+
type: "string",
|
|
787
|
+
enum: ["next", "previous"],
|
|
788
|
+
description: "Direction to search (default: next)",
|
|
789
|
+
},
|
|
790
|
+
track_type: {
|
|
791
|
+
type: "string",
|
|
792
|
+
enum: ["video", "audio", "both"],
|
|
793
|
+
description: "Track type to check (default: both)",
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
handler: async (args) => {
|
|
798
|
+
const direction = args.direction || "next";
|
|
799
|
+
const trackType = args.track_type || "both";
|
|
800
|
+
const script = buildToolScript(`
|
|
801
|
+
var seq = app.project.activeSequence;
|
|
802
|
+
if (!seq) return __error("No active sequence");
|
|
803
|
+
|
|
804
|
+
var posTicks = parseFloat(seq.getPlayerPosition().ticks);
|
|
805
|
+
var editPoints = [];
|
|
806
|
+
|
|
807
|
+
function collectPoints(tracks, type) {
|
|
808
|
+
for (var t = 0; t < tracks.numTracks; t++) {
|
|
809
|
+
for (var c = 0; c < tracks[t].clips.numItems; c++) {
|
|
810
|
+
var clip = tracks[t].clips[c];
|
|
811
|
+
editPoints.push(parseFloat(clip.start.ticks));
|
|
812
|
+
editPoints.push(parseFloat(clip.end.ticks));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if ("${trackType}" !== "audio") collectPoints(seq.videoTracks, "video");
|
|
818
|
+
if ("${trackType}" !== "video") collectPoints(seq.audioTracks, "audio");
|
|
819
|
+
|
|
820
|
+
// Sort and deduplicate
|
|
821
|
+
editPoints.sort(function(a, b) { return a - b; });
|
|
822
|
+
var unique = [];
|
|
823
|
+
for (var i = 0; i < editPoints.length; i++) {
|
|
824
|
+
if (unique.length === 0 || editPoints[i] !== unique[unique.length - 1]) {
|
|
825
|
+
unique.push(editPoints[i]);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
var found = null;
|
|
830
|
+
if ("${direction}" === "next") {
|
|
831
|
+
for (var i = 0; i < unique.length; i++) {
|
|
832
|
+
if (unique[i] > posTicks + 1) { found = unique[i]; break; }
|
|
833
|
+
}
|
|
834
|
+
} else {
|
|
835
|
+
for (var i = unique.length - 1; i >= 0; i--) {
|
|
836
|
+
if (unique[i] < posTicks - 1) { found = unique[i]; break; }
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (found === null) return __result({ found: false, direction: "${direction}" });
|
|
841
|
+
|
|
842
|
+
return __result({
|
|
843
|
+
found: true,
|
|
844
|
+
direction: "${direction}",
|
|
845
|
+
editPointSeconds: __ticksToSeconds("" + found),
|
|
846
|
+
playheadSeconds: __ticksToSeconds("" + posTicks)
|
|
847
|
+
});
|
|
848
|
+
`);
|
|
849
|
+
return sendCommand(script, bridgeOptions);
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
move_playhead_to_edit: {
|
|
853
|
+
description: "Move the playhead to the next or previous edit point.",
|
|
854
|
+
parameters: {
|
|
855
|
+
type: "object",
|
|
856
|
+
properties: {
|
|
857
|
+
direction: {
|
|
858
|
+
type: "string",
|
|
859
|
+
enum: ["next", "previous"],
|
|
860
|
+
description: "Direction (default: next)",
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
handler: async (args) => {
|
|
865
|
+
const direction = args.direction || "next";
|
|
866
|
+
const script = buildToolScript(`
|
|
867
|
+
var seq = app.project.activeSequence;
|
|
868
|
+
if (!seq) return __error("No active sequence");
|
|
869
|
+
|
|
870
|
+
var posTicks = parseFloat(seq.getPlayerPosition().ticks);
|
|
871
|
+
var editPoints = [];
|
|
872
|
+
|
|
873
|
+
function collectPoints(tracks) {
|
|
874
|
+
for (var t = 0; t < tracks.numTracks; t++) {
|
|
875
|
+
for (var c = 0; c < tracks[t].clips.numItems; c++) {
|
|
876
|
+
editPoints.push(parseFloat(tracks[t].clips[c].start.ticks));
|
|
877
|
+
editPoints.push(parseFloat(tracks[t].clips[c].end.ticks));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
collectPoints(seq.videoTracks);
|
|
883
|
+
collectPoints(seq.audioTracks);
|
|
884
|
+
|
|
885
|
+
editPoints.sort(function(a, b) { return a - b; });
|
|
886
|
+
var unique = [];
|
|
887
|
+
for (var i = 0; i < editPoints.length; i++) {
|
|
888
|
+
if (unique.length === 0 || editPoints[i] !== unique[unique.length - 1]) unique.push(editPoints[i]);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
var found = null;
|
|
892
|
+
if ("${direction}" === "next") {
|
|
893
|
+
for (var i = 0; i < unique.length; i++) {
|
|
894
|
+
if (unique[i] > posTicks + 1) { found = unique[i]; break; }
|
|
895
|
+
}
|
|
896
|
+
} else {
|
|
897
|
+
for (var i = unique.length - 1; i >= 0; i--) {
|
|
898
|
+
if (unique[i] < posTicks - 1) { found = unique[i]; break; }
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (found === null) return __error("No " + "${direction}" + " edit point found");
|
|
903
|
+
|
|
904
|
+
seq.setPlayerPosition("" + found);
|
|
905
|
+
|
|
906
|
+
return __result({ movedTo: __ticksToSeconds("" + found), direction: "${direction}" });
|
|
907
|
+
`);
|
|
908
|
+
return sendCommand(script, bridgeOptions);
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
set_project_scratch_disk: {
|
|
912
|
+
description: "Set the project's scratch disk paths for captured video, audio, and previews.",
|
|
913
|
+
parameters: {
|
|
914
|
+
type: "object",
|
|
915
|
+
properties: {
|
|
916
|
+
captured_video: {
|
|
917
|
+
type: "string",
|
|
918
|
+
description: "Path for captured video",
|
|
919
|
+
},
|
|
920
|
+
captured_audio: {
|
|
921
|
+
type: "string",
|
|
922
|
+
description: "Path for captured audio",
|
|
923
|
+
},
|
|
924
|
+
video_previews: {
|
|
925
|
+
type: "string",
|
|
926
|
+
description: "Path for video previews",
|
|
927
|
+
},
|
|
928
|
+
audio_previews: {
|
|
929
|
+
type: "string",
|
|
930
|
+
description: "Path for audio previews",
|
|
931
|
+
},
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
handler: async (args) => {
|
|
935
|
+
const script = buildToolScript(`
|
|
936
|
+
var project = app.project;
|
|
937
|
+
if (!project) return __error("No project open");
|
|
938
|
+
|
|
939
|
+
var set = {};
|
|
940
|
+
${args.captured_video ? `
|
|
941
|
+
try { project.setScratchDiskPath("${escapeForExtendScript(args.captured_video)}", 0); set.capturedVideo = "${escapeForExtendScript(args.captured_video)}"; } catch(e) {}` : ""}
|
|
942
|
+
${args.captured_audio ? `
|
|
943
|
+
try { project.setScratchDiskPath("${escapeForExtendScript(args.captured_audio)}", 1); set.capturedAudio = "${escapeForExtendScript(args.captured_audio)}"; } catch(e) {}` : ""}
|
|
944
|
+
${args.video_previews ? `
|
|
945
|
+
try { project.setScratchDiskPath("${escapeForExtendScript(args.video_previews)}", 2); set.videoPreviews = "${escapeForExtendScript(args.video_previews)}"; } catch(e) {}` : ""}
|
|
946
|
+
${args.audio_previews ? `
|
|
947
|
+
try { project.setScratchDiskPath("${escapeForExtendScript(args.audio_previews)}", 3); set.audioPreviews = "${escapeForExtendScript(args.audio_previews)}"; } catch(e) {}` : ""}
|
|
948
|
+
|
|
949
|
+
return __result(set);
|
|
950
|
+
`);
|
|
951
|
+
return sendCommand(script, bridgeOptions);
|
|
952
|
+
},
|
|
953
|
+
},
|
|
954
|
+
get_project_scratch_disks: {
|
|
955
|
+
description: "Get the current scratch disk paths for the project.",
|
|
956
|
+
parameters: {},
|
|
957
|
+
handler: async () => {
|
|
958
|
+
const script = buildToolScript(`
|
|
959
|
+
var project = app.project;
|
|
960
|
+
if (!project) return __error("No project open");
|
|
961
|
+
|
|
962
|
+
var disks = {};
|
|
963
|
+
try { disks.capturedVideo = project.getScratchDiskPath(0); } catch(e) {}
|
|
964
|
+
try { disks.capturedAudio = project.getScratchDiskPath(1); } catch(e) {}
|
|
965
|
+
try { disks.videoPreviews = project.getScratchDiskPath(2); } catch(e) {}
|
|
966
|
+
try { disks.audioPreviews = project.getScratchDiskPath(3); } catch(e) {}
|
|
967
|
+
|
|
968
|
+
return __result(disks);
|
|
969
|
+
`);
|
|
970
|
+
return sendCommand(script, bridgeOptions);
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
nest_clips: {
|
|
974
|
+
description: "Nest selected clips into a nested sequence. Select the clips first, then call this tool.",
|
|
975
|
+
parameters: {
|
|
976
|
+
type: "object",
|
|
977
|
+
properties: {
|
|
978
|
+
name: {
|
|
979
|
+
type: "string",
|
|
980
|
+
description: "Name for the nested sequence",
|
|
981
|
+
},
|
|
982
|
+
},
|
|
983
|
+
required: ["name"],
|
|
984
|
+
},
|
|
985
|
+
handler: async (args) => {
|
|
986
|
+
const script = buildToolScript(`
|
|
987
|
+
var seq = app.project.activeSequence;
|
|
988
|
+
if (!seq) return __error("No active sequence");
|
|
989
|
+
|
|
990
|
+
// Get selected clips
|
|
991
|
+
var selected = [];
|
|
992
|
+
function getSelected(tracks) {
|
|
993
|
+
for (var t = 0; t < tracks.numTracks; t++) {
|
|
994
|
+
for (var c = 0; c < tracks[t].clips.numItems; c++) {
|
|
995
|
+
if (tracks[t].clips[c].isSelected()) selected.push(tracks[t].clips[c]);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
getSelected(seq.videoTracks);
|
|
1000
|
+
getSelected(seq.audioTracks);
|
|
1001
|
+
|
|
1002
|
+
if (selected.length === 0) return __error("No clips selected. Select clips first.");
|
|
1003
|
+
|
|
1004
|
+
try {
|
|
1005
|
+
var sub = seq.createSubsequence(true);
|
|
1006
|
+
var appliedName = null;
|
|
1007
|
+
if (sub) {
|
|
1008
|
+
try { sub.name = "${escapeForExtendScript(args.name)}"; appliedName = sub.name; } catch(e) {}
|
|
1009
|
+
}
|
|
1010
|
+
var res = {
|
|
1011
|
+
nested: true,
|
|
1012
|
+
clipCount: selected.length,
|
|
1013
|
+
note: "createSubsequence nests the sequence in/out range, not the clip selection."
|
|
1014
|
+
};
|
|
1015
|
+
if (appliedName !== null) res.name = appliedName;
|
|
1016
|
+
return __result(res);
|
|
1017
|
+
} catch(e) {
|
|
1018
|
+
return __error("Nesting failed: " + e.message);
|
|
1019
|
+
}
|
|
1020
|
+
`);
|
|
1021
|
+
return sendCommand(script, bridgeOptions);
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
get_sequence_count: {
|
|
1025
|
+
description: "Get the total number of sequences in the project.",
|
|
1026
|
+
parameters: {},
|
|
1027
|
+
handler: async () => {
|
|
1028
|
+
const script = buildToolScript(`
|
|
1029
|
+
var project = app.project;
|
|
1030
|
+
if (!project) return __error("No project open");
|
|
1031
|
+
return __result({ count: project.sequences.numSequences });
|
|
1032
|
+
`);
|
|
1033
|
+
return sendCommand(script, bridgeOptions);
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
get_total_clip_count: {
|
|
1037
|
+
description: "Get the total number of clips across all tracks in the active sequence.",
|
|
1038
|
+
parameters: {},
|
|
1039
|
+
handler: async () => {
|
|
1040
|
+
const script = buildToolScript(`
|
|
1041
|
+
var seq = app.project.activeSequence;
|
|
1042
|
+
if (!seq) return __error("No active sequence");
|
|
1043
|
+
|
|
1044
|
+
var video = 0, audio = 0;
|
|
1045
|
+
for (var t = 0; t < seq.videoTracks.numTracks; t++) video += seq.videoTracks[t].clips.numItems;
|
|
1046
|
+
for (var t = 0; t < seq.audioTracks.numTracks; t++) audio += seq.audioTracks[t].clips.numItems;
|
|
1047
|
+
|
|
1048
|
+
return __result({ videoClips: video, audioClips: audio, total: video + audio });
|
|
1049
|
+
`);
|
|
1050
|
+
return sendCommand(script, bridgeOptions);
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
match_frame: {
|
|
1054
|
+
description: "Get source media info for the frame at the current playhead on a specific track. Useful for match frame operations.",
|
|
1055
|
+
parameters: {
|
|
1056
|
+
type: "object",
|
|
1057
|
+
properties: {
|
|
1058
|
+
track_type: {
|
|
1059
|
+
type: "string",
|
|
1060
|
+
enum: ["video", "audio"],
|
|
1061
|
+
description: "Track type (default: video)",
|
|
1062
|
+
},
|
|
1063
|
+
track_index: {
|
|
1064
|
+
type: "number",
|
|
1065
|
+
description: "Track index (default: 0)",
|
|
1066
|
+
},
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
handler: async (args) => {
|
|
1070
|
+
const trackType = args.track_type || "video";
|
|
1071
|
+
const trackIndex = args.track_index ?? 0;
|
|
1072
|
+
const script = buildToolScript(`
|
|
1073
|
+
var seq = app.project.activeSequence;
|
|
1074
|
+
if (!seq) return __error("No active sequence");
|
|
1075
|
+
|
|
1076
|
+
var tracks = ${trackType === "video" ? "seq.videoTracks" : "seq.audioTracks"};
|
|
1077
|
+
if (${trackIndex} >= tracks.numTracks) return __error("Track index out of range");
|
|
1078
|
+
|
|
1079
|
+
var posTicks = parseFloat(seq.getPlayerPosition().ticks);
|
|
1080
|
+
var track = tracks[${trackIndex}];
|
|
1081
|
+
var found = null;
|
|
1082
|
+
|
|
1083
|
+
for (var c = 0; c < track.clips.numItems; c++) {
|
|
1084
|
+
var clip = track.clips[c];
|
|
1085
|
+
if (parseFloat(clip.start.ticks) <= posTicks && parseFloat(clip.end.ticks) > posTicks) {
|
|
1086
|
+
found = clip;
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (!found) return __error("No clip at playhead on ${trackType} track ${trackIndex}");
|
|
1092
|
+
|
|
1093
|
+
var offsetTicks = posTicks - parseFloat(found.start.ticks) + parseFloat(found.inPoint.ticks);
|
|
1094
|
+
|
|
1095
|
+
var result = {
|
|
1096
|
+
clipName: found.name,
|
|
1097
|
+
clipNodeId: found.nodeId,
|
|
1098
|
+
timelineSeconds: __ticksToSeconds("" + posTicks),
|
|
1099
|
+
sourceSeconds: __ticksToSeconds("" + offsetTicks)
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
try {
|
|
1103
|
+
var src = found.projectItem;
|
|
1104
|
+
if (src) {
|
|
1105
|
+
result.sourceNodeId = src.nodeId;
|
|
1106
|
+
result.sourceName = src.name;
|
|
1107
|
+
try { result.sourceMediaPath = src.getMediaPath(); } catch(e) {}
|
|
1108
|
+
}
|
|
1109
|
+
} catch(e) {}
|
|
1110
|
+
|
|
1111
|
+
return __result(result);
|
|
1112
|
+
`);
|
|
1113
|
+
return sendCommand(script, bridgeOptions);
|
|
1114
|
+
},
|
|
1115
|
+
},
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
//# sourceMappingURL=utility.js.map
|