- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
1255 lines
49 KiB
TypeScript
Executable File
1255 lines
49 KiB
TypeScript
Executable File
import {
|
|
buildToolScript,
|
|
escapeForExtendScript,
|
|
} from "../bridge/script-builder.js";
|
|
import { sendCommand, BridgeOptions } from "../bridge/file-bridge.js";
|
|
|
|
export function getAdvancedTools(bridgeOptions: BridgeOptions) {
|
|
return {
|
|
ripple_delete: {
|
|
description:
|
|
"Ripple delete a clip (removes clip and closes the gap). Uses QE DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip to ripple delete",
|
|
},
|
|
},
|
|
required: ["node_id"],
|
|
},
|
|
handler: async (args: { node_id: string }) => {
|
|
const script = buildToolScript(`
|
|
app.enableQE();
|
|
var qeSeq = qe.project.getActiveSequence();
|
|
if (!qeSeq) return __error("No active sequence (QE)");
|
|
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
|
|
|
|
var qeTrack = result.trackType === "video"
|
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
if (!qeTrack) return __error("QE track not found");
|
|
|
|
var qeClip = qeTrack.getItemAt(result.clipIndex);
|
|
if (!qeClip) return __error("QE clip not found");
|
|
|
|
var deletedNodeId = result.clip.nodeId;
|
|
var deletedName = result.clip.name;
|
|
qeClip.rippleDelete();
|
|
if (__findClip(deletedNodeId)) {
|
|
return __error("Premiere reported rippleDelete but the clip is still present. Structural QE edits are known to no-op on some Premiere Pro 26.3 installations; rebuild the keep-segments into a new sequence as a workaround.");
|
|
}
|
|
return __result({ rippleDeleted: true, verified: true, clipName: deletedName });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
roll_edit: {
|
|
description:
|
|
"Perform a verified roll edit at the outgoing cut of a clip using the public timeline DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
offset_seconds: {
|
|
type: "number",
|
|
description:
|
|
"Offset in seconds (positive = roll right, negative = roll left)",
|
|
},
|
|
},
|
|
required: ["node_id", "offset_seconds"],
|
|
},
|
|
handler: async (args: { node_id: string; offset_seconds: number }) => {
|
|
if (!Number.isFinite(args.offset_seconds) || args.offset_seconds === 0) {
|
|
return { success: false, error: "offset_seconds must be a finite, non-zero number" };
|
|
}
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
var track = result.trackType === "video"
|
|
? app.project.activeSequence.videoTracks[result.trackIndex]
|
|
: app.project.activeSequence.audioTracks[result.trackIndex];
|
|
var outgoing = track.clips[result.clipIndex + 1];
|
|
if (!outgoing) return __error("A roll edit requires the selected clip to have an outgoing adjacent clip on the same track.");
|
|
var beforeStart = String(result.clip.start.ticks);
|
|
var beforeEnd = String(result.clip.end.ticks);
|
|
if (String(outgoing.start.ticks) !== beforeEnd) return __error("A roll edit requires two contiguous clips with no gap at the outgoing cut.");
|
|
var newCutTicks = parseFloat(beforeEnd) + __secondsToTicks(${args.offset_seconds});
|
|
if (newCutTicks <= parseFloat(result.clip.start.ticks) || newCutTicks >= parseFloat(outgoing.end.ticks)) {
|
|
return __error("The requested roll offset would create a zero- or negative-duration clip.");
|
|
}
|
|
var newCut = new Time();
|
|
newCut.ticks = String(Math.round(newCutTicks));
|
|
result.clip.end = newCut;
|
|
outgoing.start = newCut;
|
|
var after = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!after) return __error("Clip could not be found after the roll edit");
|
|
if (String(after.clip.start.ticks) === beforeStart && String(after.clip.end.ticks) === beforeEnd) {
|
|
return __error("The roll edit returned without an observable timeline change; no successful edit is reported.");
|
|
}
|
|
if (String(after.clip.end.ticks) !== String(outgoing.start.ticks)) return __error("The roll edit left a gap or overlap at the edited cut.");
|
|
return __result({
|
|
rolled: true,
|
|
verified: true,
|
|
clipName: after.clip.name,
|
|
offsetSeconds: ${args.offset_seconds},
|
|
before: { startTicks: beforeStart, endTicks: beforeEnd },
|
|
after: { startTicks: String(after.clip.start.ticks), endTicks: String(after.clip.end.ticks) }
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
slide_edit: {
|
|
description:
|
|
"Perform a verified slide edit on a clip using adjacent clips from the public timeline DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
offset_seconds: {
|
|
type: "number",
|
|
description:
|
|
"Offset in seconds (positive = slide right, negative = slide left)",
|
|
},
|
|
},
|
|
required: ["node_id", "offset_seconds"],
|
|
},
|
|
handler: async (args: { node_id: string; offset_seconds: number }) => {
|
|
if (!Number.isFinite(args.offset_seconds) || args.offset_seconds === 0) {
|
|
return { success: false, error: "offset_seconds must be a finite, non-zero number" };
|
|
}
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
var track = result.trackType === "video"
|
|
? app.project.activeSequence.videoTracks[result.trackIndex]
|
|
: app.project.activeSequence.audioTracks[result.trackIndex];
|
|
var previous = track.clips[result.clipIndex - 1];
|
|
var following = track.clips[result.clipIndex + 1];
|
|
if (!previous || !following) return __error("A slide edit requires contiguous clips before and after the selected clip on the same track.");
|
|
var beforeStart = String(result.clip.start.ticks);
|
|
var beforeEnd = String(result.clip.end.ticks);
|
|
if (String(previous.end.ticks) !== beforeStart || String(following.start.ticks) !== beforeEnd) {
|
|
return __error("A slide edit requires no gaps at either adjacent cut.");
|
|
}
|
|
var deltaTicks = __secondsToTicks(${args.offset_seconds});
|
|
var newStartTicks = parseFloat(beforeStart) + deltaTicks;
|
|
var newEndTicks = parseFloat(beforeEnd) + deltaTicks;
|
|
if (newStartTicks <= parseFloat(previous.start.ticks) || newEndTicks >= parseFloat(following.end.ticks)) {
|
|
return __error("The requested slide offset would create a zero- or negative-duration adjacent clip.");
|
|
}
|
|
var newStart = new Time();
|
|
newStart.ticks = String(Math.round(newStartTicks));
|
|
var newEnd = new Time();
|
|
newEnd.ticks = String(Math.round(newEndTicks));
|
|
previous.end = newStart;
|
|
result.clip.start = newStart;
|
|
result.clip.end = newEnd;
|
|
following.start = newEnd;
|
|
var after = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!after) return __error("Clip could not be found after the slide edit");
|
|
if (String(after.clip.start.ticks) === beforeStart && String(after.clip.end.ticks) === beforeEnd) {
|
|
return __error("The slide edit returned without an observable timeline change; no successful edit is reported.");
|
|
}
|
|
if (String(previous.end.ticks) !== String(after.clip.start.ticks) || String(after.clip.end.ticks) !== String(following.start.ticks)) {
|
|
return __error("The slide edit left a gap or overlap at an adjacent cut.");
|
|
}
|
|
return __result({
|
|
slid: true,
|
|
verified: true,
|
|
clipName: after.clip.name,
|
|
offsetSeconds: ${args.offset_seconds},
|
|
before: { startTicks: beforeStart, endTicks: beforeEnd },
|
|
after: { startTicks: String(after.clip.start.ticks), endTicks: String(after.clip.end.ticks) }
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
slip_edit: {
|
|
description:
|
|
"Perform a verified slip edit on a clip using public source in/out properties.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
offset_seconds: {
|
|
type: "number",
|
|
description:
|
|
"Offset in seconds (positive = slip forward in source, negative = slip backward)",
|
|
},
|
|
},
|
|
required: ["node_id", "offset_seconds"],
|
|
},
|
|
handler: async (args: { node_id: string; offset_seconds: number }) => {
|
|
if (!Number.isFinite(args.offset_seconds) || args.offset_seconds === 0) {
|
|
return { success: false, error: "offset_seconds must be a finite, non-zero number" };
|
|
}
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
var beforeStart = String(result.clip.start.ticks);
|
|
var beforeEnd = String(result.clip.end.ticks);
|
|
var beforeIn = String(result.clip.inPoint.ticks);
|
|
var beforeOut = String(result.clip.outPoint.ticks);
|
|
var deltaTicks = __secondsToTicks(${args.offset_seconds});
|
|
var newInTicks = parseFloat(beforeIn) + deltaTicks;
|
|
var newOutTicks = parseFloat(beforeOut) + deltaTicks;
|
|
if (newInTicks < 0 || newOutTicks <= newInTicks) return __error("The requested slip offset would create an invalid source range.");
|
|
var newIn = new Time();
|
|
newIn.ticks = String(Math.round(newInTicks));
|
|
var newOut = new Time();
|
|
newOut.ticks = String(Math.round(newOutTicks));
|
|
result.clip.inPoint = newIn;
|
|
result.clip.outPoint = newOut;
|
|
var after = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!after) return __error("Clip could not be found after the slip edit");
|
|
if (String(after.clip.start.ticks) !== beforeStart || String(after.clip.end.ticks) !== beforeEnd) {
|
|
return __error("The slip edit changed the timeline placement instead of only source in/out points.");
|
|
}
|
|
if (String(after.clip.inPoint.ticks) === beforeIn && String(after.clip.outPoint.ticks) === beforeOut) {
|
|
return __error("The slip edit returned without an observable source in/out change; no successful edit is reported.");
|
|
}
|
|
return __result({
|
|
slipped: true,
|
|
verified: true,
|
|
clipName: after.clip.name,
|
|
offsetSeconds: ${args.offset_seconds},
|
|
before: { inTicks: beforeIn, outTicks: beforeOut },
|
|
after: { inTicks: String(after.clip.inPoint.ticks), outTicks: String(after.clip.outPoint.ticks) }
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
move_clip_to_track: {
|
|
description: "Move a clip to a different track. Uses QE DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
target_track_index: {
|
|
type: "number",
|
|
description: "Target track index (0-based)",
|
|
},
|
|
},
|
|
required: ["node_id", "target_track_index"],
|
|
},
|
|
handler: async (args: {
|
|
node_id: string;
|
|
target_track_index: number;
|
|
}) => {
|
|
const nodeId = escapeForExtendScript(args.node_id);
|
|
const script = buildToolScript(`
|
|
app.enableQE();
|
|
var qeSeq = qe.project.getActiveSequence();
|
|
if (!qeSeq) return __error("No active sequence (QE)");
|
|
|
|
var result = __findClip("${nodeId}");
|
|
if (!result) return __error("Clip not found: ${nodeId}");
|
|
|
|
var seq = app.project.activeSequence;
|
|
var targetTracks = result.trackType === "video" ? seq.videoTracks : seq.audioTracks;
|
|
if (${args.target_track_index} >= targetTracks.numTracks) {
|
|
return __error("Target track index ${args.target_track_index} is out of range: the sequence has " + targetTracks.numTracks + " " + result.trackType + " track(s).");
|
|
}
|
|
if (result.trackIndex === ${args.target_track_index}) {
|
|
return __result({ moved: false, verified: true, alreadyOnTrack: true, clipName: result.clip.name, trackIndex: result.trackIndex });
|
|
}
|
|
|
|
var qeTrack = result.trackType === "video"
|
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
if (!qeTrack) return __error("QE track not found");
|
|
|
|
// QE item indices count gaps ("Empty" items) alongside clips, so the
|
|
// DOM clip index does not map onto getItemAt -- on a track with a
|
|
// leading gap it returns the gap, and calling moveToTrack on that
|
|
// fails with a misleading parameter error. Match on start time.
|
|
var qeClip = null;
|
|
var wantStart = parseFloat(result.clip.start.ticks);
|
|
for (var qi = 0; qi < qeTrack.numItems; qi++) {
|
|
var cand = qeTrack.getItemAt(qi);
|
|
if (!cand || String(cand.type) !== "Clip") continue;
|
|
if (Math.abs(parseFloat(cand.start.ticks) - wantStart) < 1) { qeClip = cand; break; }
|
|
}
|
|
if (!qeClip) return __error("Could not locate the clip among the QE track's items; cannot change track.");
|
|
|
|
try {
|
|
qeClip.moveToTrack(${args.target_track_index});
|
|
} catch (moveErr) {
|
|
return __error("Could not move the clip to track ${args.target_track_index}: the QE moveToTrack API rejected the call (" + moveErr.toString() + "). It takes three parameters whose types are undocumented, and no combination tried was accepted on Premiere Pro 26.2.2. The clip was left untouched. Reconstructing the move with Track.overwriteClip would mint a new node ID and drop applied effects and keyframes, so it is not done automatically -- move the clip manually if you need it on another track.");
|
|
}
|
|
|
|
// QE reports nothing useful on success, so confirm against the DOM.
|
|
var after = __findClip("${nodeId}");
|
|
if (!after) return __error("Clip ${nodeId} could not be found after the track move; the timeline may be in an unexpected state.");
|
|
if (after.trackIndex !== ${args.target_track_index}) {
|
|
return __error("Premiere accepted the moveToTrack call but the clip is still on track " + after.trackIndex + " rather than ${args.target_track_index}. Structural QE edits are known to no-op on some Premiere Pro 26.x installations (confirmed on 26.2.2).");
|
|
}
|
|
|
|
return __result({
|
|
moved: true,
|
|
verified: true,
|
|
clipName: after.clip.name,
|
|
newTrackIndex: after.trackIndex
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
remove_all_effects: {
|
|
description: "Remove ALL effects from a clip. Uses QE DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
},
|
|
required: ["node_id"],
|
|
},
|
|
handler: async (args: { node_id: string }) => {
|
|
const script = buildToolScript(`
|
|
app.enableQE();
|
|
var qeSeq = qe.project.getActiveSequence();
|
|
if (!qeSeq) return __error("No active sequence (QE)");
|
|
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
var qeTrack = result.trackType === "video"
|
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
var qeClip = qeTrack.getItemAt(result.clipIndex);
|
|
if (!qeClip) return __error("QE clip not found");
|
|
|
|
qeClip.removeEffects();
|
|
return __result({ removed: true, clipName: result.clip.name });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
set_clip_speed_qe: {
|
|
description:
|
|
"Unavailable: Premiere does not expose a supported scripting API for changing a timeline clip's speed.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
speed_percent: {
|
|
type: "number",
|
|
description:
|
|
"Speed as percentage (100 = normal, 200 = 2x, 50 = half speed)",
|
|
},
|
|
reverse: {
|
|
type: "boolean",
|
|
description: "Reverse playback direction (default: false)",
|
|
},
|
|
},
|
|
required: ["node_id", "speed_percent"],
|
|
},
|
|
handler: async (args: {
|
|
node_id: string;
|
|
speed_percent: number;
|
|
reverse?: boolean;
|
|
}) => {
|
|
void args;
|
|
return {
|
|
success: false,
|
|
error:
|
|
"Changing a timeline clip's speed is not exposed by Premiere's supported ExtendScript or UXP APIs. No mutation was attempted. Use Premiere's Speed/Duration UI or pre-render retimed media before import.",
|
|
};
|
|
},
|
|
},
|
|
|
|
reverse_clip: {
|
|
description:
|
|
"Unavailable: Premiere does not expose a supported scripting API for reversing a timeline clip's playback direction.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
reverse: {
|
|
type: "boolean",
|
|
description: "True to reverse, false for normal (default: true)",
|
|
},
|
|
},
|
|
required: ["node_id"],
|
|
},
|
|
handler: async (args: { node_id: string; reverse?: boolean }) => {
|
|
void args;
|
|
return {
|
|
success: false,
|
|
error:
|
|
"Reversing a timeline clip is not exposed by Premiere's supported ExtendScript or UXP APIs. No mutation was attempted. Use Premiere's Speed/Duration UI or pre-render retimed media before import.",
|
|
};
|
|
},
|
|
},
|
|
|
|
set_frame_blend: {
|
|
description: "Enable or disable frame blending on a clip. Uses QE DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
enabled: {
|
|
type: "boolean",
|
|
description: "True to enable frame blending, false to disable",
|
|
},
|
|
},
|
|
required: ["node_id", "enabled"],
|
|
},
|
|
handler: async (args: { node_id: string; enabled: boolean }) => {
|
|
const script = buildToolScript(`
|
|
app.enableQE();
|
|
var qeSeq = qe.project.getActiveSequence();
|
|
if (!qeSeq) return __error("No active sequence (QE)");
|
|
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
var qeTrack = result.trackType === "video"
|
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
var qeClip = qeTrack.getItemAt(result.clipIndex);
|
|
if (!qeClip) return __error("QE clip not found");
|
|
|
|
qeClip.setFrameBlend(${args.enabled});
|
|
return __result({ frameBlend: ${args.enabled}, clipName: result.clip.name });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
set_time_interpolation: {
|
|
description:
|
|
"Set time interpolation type for a clip (Frame Sampling, Frame Blending, Optical Flow). Uses QE DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
interpolation_type: {
|
|
type: "number",
|
|
description:
|
|
"0 = Frame Sampling, 1 = Frame Blending, 2 = Optical Flow",
|
|
},
|
|
},
|
|
required: ["node_id", "interpolation_type"],
|
|
},
|
|
handler: async (args: {
|
|
node_id: string;
|
|
interpolation_type: number;
|
|
}) => {
|
|
const script = buildToolScript(`
|
|
app.enableQE();
|
|
var qeSeq = qe.project.getActiveSequence();
|
|
if (!qeSeq) return __error("No active sequence (QE)");
|
|
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
var qeTrack = result.trackType === "video"
|
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
var qeClip = qeTrack.getItemAt(result.clipIndex);
|
|
if (!qeClip) return __error("QE clip not found");
|
|
|
|
qeClip.setTimeInterpolationType(${args.interpolation_type});
|
|
var typeNames = ["Frame Sampling", "Frame Blending", "Optical Flow"];
|
|
return __result({
|
|
set: true,
|
|
clipName: result.clip.name,
|
|
interpolationType: typeNames[${args.interpolation_type}] || "Unknown"
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
rename_clip: {
|
|
description: "Rename a clip on the timeline. Uses QE DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
new_name: {
|
|
type: "string",
|
|
description: "New name for the clip",
|
|
},
|
|
},
|
|
required: ["node_id", "new_name"],
|
|
},
|
|
handler: async (args: { node_id: string; new_name: string }) => {
|
|
const script = buildToolScript(`
|
|
app.enableQE();
|
|
var qeSeq = qe.project.getActiveSequence();
|
|
if (!qeSeq) return __error("No active sequence (QE)");
|
|
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
var qeTrack = result.trackType === "video"
|
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
var qeClip = qeTrack.getItemAt(result.clipIndex);
|
|
if (!qeClip) return __error("QE clip not found");
|
|
|
|
var oldName = result.clip.name;
|
|
qeClip.setName("${escapeForExtendScript(args.new_name)}");
|
|
return __result({ renamed: true, oldName: oldName, newName: "${escapeForExtendScript(args.new_name)}" });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
get_clip_speed: {
|
|
description: "Get the playback speed and reverse state of a clip",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
},
|
|
required: ["node_id"],
|
|
},
|
|
handler: async (args: { node_id: string }) => {
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}");
|
|
|
|
var clip = result.clip;
|
|
var speed = 1;
|
|
var reversed = false;
|
|
try { speed = clip.getSpeed(); } catch(e) {}
|
|
try { reversed = clip.isSpeedReversed() == 1; } catch(e) {}
|
|
|
|
return __result({
|
|
clipName: clip.name,
|
|
speed: speed,
|
|
reversed: reversed
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
set_clip_selection: {
|
|
description: "Select or deselect a clip in the active sequence",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
selected: {
|
|
type: "boolean",
|
|
description: "True to select, false to deselect",
|
|
},
|
|
},
|
|
required: ["node_id", "selected"],
|
|
},
|
|
handler: async (args: { node_id: string; selected: boolean }) => {
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
result.clip.setSelected(${args.selected ? 1 : 0}, true);
|
|
return __result({ selected: ${args.selected}, clipName: result.clip.name });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
link_selection: {
|
|
description:
|
|
"Link the currently selected video and audio clips in the active sequence",
|
|
parameters: {},
|
|
handler: async () => {
|
|
const script = buildToolScript(`
|
|
var seq = app.project.activeSequence;
|
|
if (!seq) return __error("No active sequence");
|
|
seq.linkSelection();
|
|
return __result({ linked: true });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
unlink_selection: {
|
|
description:
|
|
"Unlink the currently selected video and audio clips in the active sequence",
|
|
parameters: {},
|
|
handler: async () => {
|
|
const script = buildToolScript(`
|
|
var seq = app.project.activeSequence;
|
|
if (!seq) return __error("No active sequence");
|
|
seq.unlinkSelection();
|
|
return __result({ unlinked: true });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
overwrite_clip: {
|
|
description:
|
|
"Overwrite a project item onto validated timeline tracks and verify a new source placement at the requested time",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
item_id: {
|
|
type: "string",
|
|
description: "Node ID or name of the project item to add",
|
|
},
|
|
start_seconds: {
|
|
type: "number",
|
|
description: "Start time in seconds on the timeline (default: 0)",
|
|
},
|
|
track_index: {
|
|
type: "number",
|
|
description: "Video track index (0-based, default: 0)",
|
|
},
|
|
audio_track_index: {
|
|
type: "number",
|
|
description: "Audio track index (0-based, default: 0)",
|
|
},
|
|
},
|
|
required: ["item_id"],
|
|
},
|
|
handler: async (args: {
|
|
item_id: string;
|
|
start_seconds?: number;
|
|
track_index?: number;
|
|
audio_track_index?: number;
|
|
}) => {
|
|
const startSeconds = args.start_seconds ?? 0;
|
|
const trackIndex = args.track_index ?? 0;
|
|
const audioTrackIndex = args.audio_track_index ?? 0;
|
|
if (!Number.isFinite(startSeconds) || startSeconds < 0) {
|
|
return { success: false, error: "start_seconds must be a non-negative finite number" };
|
|
}
|
|
if (!Number.isSafeInteger(trackIndex) || trackIndex < 0) {
|
|
return { success: false, error: "track_index must be a non-negative integer" };
|
|
}
|
|
if (!Number.isSafeInteger(audioTrackIndex) || audioTrackIndex < 0) {
|
|
return { success: false, error: "audio_track_index must be a non-negative integer" };
|
|
}
|
|
|
|
const script = buildToolScript(`
|
|
var seq = app.project.activeSequence;
|
|
if (!seq) return __error("No active sequence");
|
|
|
|
if (${trackIndex} >= seq.videoTracks.numTracks) {
|
|
return __error("Video track index ${trackIndex} is out of range: the sequence has " + seq.videoTracks.numTracks + " video track(s).");
|
|
}
|
|
if (${audioTrackIndex} >= seq.audioTracks.numTracks) {
|
|
return __error("Audio track index ${audioTrackIndex} is out of range: the sequence has " + seq.audioTracks.numTracks + " audio track(s).");
|
|
}
|
|
if (typeof seq.overwriteClip !== "function") {
|
|
return __error("Sequence.overwriteClip is unavailable on this Premiere build.");
|
|
}
|
|
|
|
var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
|
|
if (!item) return __error("Project item not found: ${escapeForExtendScript(args.item_id)}");
|
|
|
|
var startTicks = __secondsToTicks(${startSeconds}).toString();
|
|
var wantedItemId = String(item.nodeId);
|
|
var wantedStartTicks = parseFloat(startTicks);
|
|
var frameTicks = seq.timebase ? parseFloat(seq.timebase) : NaN;
|
|
if (!frameTicks || isNaN(frameTicks)) frameTicks = TICKS_PER_SECOND / 24;
|
|
|
|
function __isPlacedOn(track) {
|
|
for (var clipIndex = 0; clipIndex < track.clips.numItems; clipIndex++) {
|
|
var clip = track.clips[clipIndex];
|
|
var sourceId = "";
|
|
try { sourceId = clip.projectItem ? String(clip.projectItem.nodeId) : ""; } catch (sourceError) {}
|
|
if (sourceId !== wantedItemId) continue;
|
|
var actualStartTicks = NaN;
|
|
try { actualStartTicks = parseFloat(clip.start.ticks); } catch (startError) {}
|
|
if (!isNaN(actualStartTicks) && Math.abs(actualStartTicks - wantedStartTicks) <= frameTicks) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var videoWasPlaced = __isPlacedOn(seq.videoTracks[${trackIndex}]);
|
|
var audioWasPlaced = __isPlacedOn(seq.audioTracks[${audioTrackIndex}]);
|
|
try {
|
|
seq.overwriteClip(item, startTicks, ${trackIndex}, ${audioTrackIndex});
|
|
} catch (overwriteError) {
|
|
return __error("Sequence.overwriteClip failed: " + overwriteError.toString());
|
|
}
|
|
|
|
// The call can return without adding anything on Premiere 26.x. Read
|
|
// the target tracks back by source node ID and frame-snapped position
|
|
// instead of trusting the method's return value or a clip-count delta.
|
|
var videoPlaced = __isPlacedOn(seq.videoTracks[${trackIndex}]);
|
|
var audioPlaced = __isPlacedOn(seq.audioTracks[${audioTrackIndex}]);
|
|
if (!videoPlaced && !audioPlaced) {
|
|
return __error("overwrite_clip did not place project item " + item.name + " on either requested track at ${startSeconds}s. Premiere reported no verifiable timeline change.");
|
|
}
|
|
if ((videoPlaced && videoWasPlaced) && (audioPlaced && audioWasPlaced)) {
|
|
return __error("overwrite_clip produced no verifiable new placement: project item " + item.name + " was already present at ${startSeconds}s on the requested track(s).");
|
|
}
|
|
|
|
return __result({
|
|
overwritten: true,
|
|
verified: true,
|
|
item: item.name,
|
|
trackIndex: ${trackIndex},
|
|
audioTrackIndex: ${audioTrackIndex},
|
|
startSeconds: ${startSeconds},
|
|
placedOnVideoTrack: videoPlaced,
|
|
placedOnAudioTrack: audioPlaced
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
create_sequence_from_clips: {
|
|
description:
|
|
"Create a new sequence by automatically placing project items in order",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
name: {
|
|
type: "string",
|
|
description: "Name for the new sequence",
|
|
},
|
|
item_ids: {
|
|
type: "array",
|
|
items: { type: "string" },
|
|
description:
|
|
"Array of project item names or node IDs to include in order",
|
|
},
|
|
},
|
|
required: ["name", "item_ids"],
|
|
},
|
|
handler: async (args: { name: string; item_ids: string[] }) => {
|
|
const itemLookups = args.item_ids
|
|
.map(
|
|
(id, i) =>
|
|
`var item${i} = __findProjectItem("${escapeForExtendScript(id)}"); if (!item${i}) return __error("Item not found: ${escapeForExtendScript(id)}"); items.push(item${i});`,
|
|
)
|
|
.join("\n ");
|
|
|
|
const script = buildToolScript(`
|
|
var items = [];
|
|
${itemLookups}
|
|
|
|
var seq = app.project.createNewSequenceFromClips("${escapeForExtendScript(args.name)}", items);
|
|
if (!seq) return __error("Failed to create sequence from clips");
|
|
return __result({ created: true, name: seq.name, id: seq.sequenceID, clipCount: items.length });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
close_sequence: {
|
|
description: "Close a sequence tab in the timeline",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
sequence_id: {
|
|
type: "string",
|
|
description:
|
|
"Sequence name or ID. Uses active sequence if omitted.",
|
|
},
|
|
},
|
|
},
|
|
handler: async (args: { sequence_id?: string }) => {
|
|
const seqLookup = args.sequence_id
|
|
? `var seq = __findSequence("${escapeForExtendScript(args.sequence_id)}"); if (!seq) return __error("Sequence not found");`
|
|
: `var seq = app.project.activeSequence; if (!seq) return __error("No active sequence");`;
|
|
|
|
const script = buildToolScript(`
|
|
${seqLookup}
|
|
var name = seq.name;
|
|
var sequenceId = String(seq.sequenceID);
|
|
seq.close();
|
|
return __result({
|
|
timelineTabCloseRequested: true,
|
|
sequenceRetainedInProject: !!__findSequence(sequenceId),
|
|
name: name,
|
|
sequenceId: sequenceId,
|
|
note: "Closing a sequence closes its timeline tab; it does not delete the sequence from the project."
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
export_as_project: {
|
|
description:
|
|
"Export a sequence as a standalone Premiere Pro project file",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
sequence_id: {
|
|
type: "string",
|
|
description:
|
|
"Sequence name or ID. Uses active sequence if omitted.",
|
|
},
|
|
output_path: {
|
|
type: "string",
|
|
description: "Full path for the exported .prproj file",
|
|
},
|
|
},
|
|
required: ["output_path"],
|
|
},
|
|
handler: async (args: { sequence_id?: string; output_path: string }) => {
|
|
const seqLookup = args.sequence_id
|
|
? `var seq = __findSequence("${escapeForExtendScript(args.sequence_id)}"); if (!seq) return __error("Sequence not found");`
|
|
: `var seq = app.project.activeSequence; if (!seq) return __error("No active sequence");`;
|
|
|
|
const script = buildToolScript(`
|
|
${seqLookup}
|
|
seq.exportAsProject("${escapeForExtendScript(args.output_path)}");
|
|
return __result({ exported: true, path: "${escapeForExtendScript(args.output_path)}" });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
set_zero_point: {
|
|
description: "Set the starting timecode (zero point) of a sequence",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
sequence_id: {
|
|
type: "string",
|
|
description:
|
|
"Sequence name or ID. Uses active sequence if omitted.",
|
|
},
|
|
start_seconds: {
|
|
type: "number",
|
|
description: "Start time in seconds for the timecode origin",
|
|
},
|
|
},
|
|
required: ["start_seconds"],
|
|
},
|
|
handler: async (args: {
|
|
sequence_id?: string;
|
|
start_seconds: number;
|
|
}) => {
|
|
const seqLookup = args.sequence_id
|
|
? `var seq = __findSequence("${escapeForExtendScript(args.sequence_id)}"); if (!seq) return __error("Sequence not found");`
|
|
: `var seq = app.project.activeSequence; if (!seq) return __error("No active sequence");`;
|
|
|
|
const script = buildToolScript(`
|
|
${seqLookup}
|
|
var ticks = __secondsToTicks(${args.start_seconds}).toString();
|
|
seq.setZeroPoint(ticks);
|
|
return __result({ set: true, startSeconds: ${args.start_seconds} });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
scene_edit_detection: {
|
|
description:
|
|
"Perform scene edit detection on the selected clips in the active sequence. Defaults to creating markers rather than cutting.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
action: {
|
|
type: "string",
|
|
enum: ["CreateMarkers", "ApplyCuts"],
|
|
description: "Create markers (default) or apply cuts to the selected clips",
|
|
},
|
|
apply_cuts_to_linked_audio: {
|
|
type: "boolean",
|
|
description: "When applying cuts, also cut linked audio (default: false)",
|
|
},
|
|
sensitivity: {
|
|
type: "string",
|
|
enum: ["LowSensitivity", "MediumSensitivity", "HighSensitivity"],
|
|
description: "Scene-detection sensitivity (default: MediumSensitivity)",
|
|
},
|
|
},
|
|
},
|
|
handler: async (args: {
|
|
action?: "CreateMarkers" | "ApplyCuts";
|
|
apply_cuts_to_linked_audio?: boolean;
|
|
sensitivity?: "LowSensitivity" | "MediumSensitivity" | "HighSensitivity";
|
|
}) => {
|
|
const action = args.action ?? "CreateMarkers";
|
|
const applyCutsToLinkedAudio = args.apply_cuts_to_linked_audio === true;
|
|
const sensitivity = args.sensitivity ?? "MediumSensitivity";
|
|
const script = buildToolScript(`
|
|
var seq = app.project.activeSequence;
|
|
if (!seq) return __error("No active sequence");
|
|
var selected = seq.getSelection();
|
|
if (!selected || selected.length === 0) return __error("Select at least one clip before scene edit detection.");
|
|
var detected = seq.performSceneEditDetectionOnSelection(
|
|
"${action}",
|
|
${applyCutsToLinkedAudio},
|
|
"${sensitivity}"
|
|
);
|
|
if (!detected) return __error("Premiere did not complete scene edit detection for the selected clips.");
|
|
return __result({
|
|
sceneDetection: true,
|
|
selectedClipCount: selected.length,
|
|
action: "${action}",
|
|
applyCutsToLinkedAudio: ${applyCutsToLinkedAudio},
|
|
sensitivity: "${sensitivity}"
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
delete_preview_files: {
|
|
description:
|
|
"Delete all preview/render cache files for the project. Uses QE DOM.",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
media_type: {
|
|
type: "string",
|
|
description:
|
|
"Type of preview files to delete: 'video', 'audio', or 'all' (default: 'all')",
|
|
},
|
|
},
|
|
},
|
|
handler: async (args: { media_type?: string }) => {
|
|
const typeMap: Record<string, string> = {
|
|
video: '"228CDA18-3625-4d2d-951E-348879E4ED93"',
|
|
audio: '"80B8E3D5-6DCA-4195-AEFB-CB5F407AB009"',
|
|
all: '"FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"',
|
|
};
|
|
const mediaType = typeMap[args.media_type || "all"] || typeMap.all;
|
|
|
|
const script = buildToolScript(`
|
|
app.enableQE();
|
|
qe.project.deletePreviewFiles(${mediaType});
|
|
return __result({ deleted: true, mediaType: "${args.media_type || "all"}" });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
add_tracks: {
|
|
description:
|
|
"Add video and/or audio tracks through QE and verify the active sequence gained the exact requested counts",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
video_tracks: {
|
|
type: "number",
|
|
description: "Number of video tracks to add (default: 0)",
|
|
},
|
|
audio_tracks: {
|
|
type: "number",
|
|
description:
|
|
"Number of standard stereo audio tracks to add (default: 0)",
|
|
},
|
|
audio_mono_tracks: {
|
|
type: "number",
|
|
description: "Number of mono audio tracks to add (default: 0)",
|
|
},
|
|
audio_51_tracks: {
|
|
type: "number",
|
|
description: "Number of 5.1 audio tracks to add (default: 0)",
|
|
},
|
|
},
|
|
},
|
|
handler: async (args: {
|
|
video_tracks?: number;
|
|
audio_tracks?: number;
|
|
audio_mono_tracks?: number;
|
|
audio_51_tracks?: number;
|
|
}) => {
|
|
const v = args.video_tracks ?? 0;
|
|
const a = args.audio_tracks ?? 0;
|
|
const aMono = args.audio_mono_tracks ?? 0;
|
|
const a51 = args.audio_51_tracks ?? 0;
|
|
const requested = [
|
|
["video_tracks", v],
|
|
["audio_tracks", a],
|
|
["audio_mono_tracks", aMono],
|
|
["audio_51_tracks", a51],
|
|
] as const;
|
|
for (const [name, value] of requested) {
|
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
return { success: false, error: `${name} must be a non-negative integer` };
|
|
}
|
|
}
|
|
if (v + a + aMono + a51 === 0) {
|
|
return { success: false, error: "Request at least one video or audio track" };
|
|
}
|
|
|
|
const script = buildToolScript(`
|
|
var seq = app.project.activeSequence;
|
|
if (!seq) return __error("No active sequence");
|
|
var beforeVideo = seq.videoTracks.numTracks;
|
|
var beforeAudio = seq.audioTracks.numTracks;
|
|
var expectedVideo = beforeVideo + ${v};
|
|
var expectedAudio = beforeAudio + ${a + aMono + a51};
|
|
|
|
if (typeof app.enableQE !== "function") return __error("QE is unavailable on this Premiere build; cannot add tracks.");
|
|
app.enableQE();
|
|
if (typeof qe === "undefined" || !qe.project || typeof qe.project.getActiveSequence !== "function") {
|
|
return __error("QE active-sequence access is unavailable on this Premiere build; cannot add tracks.");
|
|
}
|
|
var qeSeq = qe.project.getActiveSequence();
|
|
if (!qeSeq) return __error("No active sequence (QE)");
|
|
if (typeof qeSeq.addTracks !== "function") return __error("QE addTracks is unavailable on this Premiere build.");
|
|
|
|
try {
|
|
qeSeq.addTracks(${v}, ${a}, ${aMono}, ${a51});
|
|
} catch (addTracksError) {
|
|
return __error("QE addTracks failed: " + addTracksError.toString());
|
|
}
|
|
|
|
var afterVideo = seq.videoTracks.numTracks;
|
|
var afterAudio = seq.audioTracks.numTracks;
|
|
if (afterVideo !== expectedVideo || afterAudio !== expectedAudio) {
|
|
return __error("QE addTracks did not add the requested tracks: video " + beforeVideo + " -> " + afterVideo + " (expected " + expectedVideo + "), audio " + beforeAudio + " -> " + afterAudio + " (expected " + expectedAudio + ").");
|
|
}
|
|
|
|
return __result({
|
|
added: true,
|
|
verified: true,
|
|
videoTracks: ${v},
|
|
audioTracks: ${a},
|
|
audioMonoTracks: ${aMono},
|
|
audio51Tracks: ${a51},
|
|
totalVideoTracks: afterVideo,
|
|
totalAudioTracks: afterAudio
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
set_color_value: {
|
|
description:
|
|
"Set a color value on an effect property (e.g., tint color, fill color)",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
component_name: {
|
|
type: "string",
|
|
description: "Name of the component/effect",
|
|
},
|
|
property_name: {
|
|
type: "string",
|
|
description: "Name of the color property",
|
|
},
|
|
alpha: {
|
|
type: "number",
|
|
description: "Alpha (0-255)",
|
|
},
|
|
red: {
|
|
type: "number",
|
|
description: "Red (0-255)",
|
|
},
|
|
green: {
|
|
type: "number",
|
|
description: "Green (0-255)",
|
|
},
|
|
blue: {
|
|
type: "number",
|
|
description: "Blue (0-255)",
|
|
},
|
|
},
|
|
required: [
|
|
"node_id",
|
|
"component_name",
|
|
"property_name",
|
|
"alpha",
|
|
"red",
|
|
"green",
|
|
"blue",
|
|
],
|
|
},
|
|
handler: async (args: {
|
|
node_id: string;
|
|
component_name: string;
|
|
property_name: string;
|
|
alpha: number;
|
|
red: number;
|
|
green: number;
|
|
blue: number;
|
|
}) => {
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
var clip = result.clip;
|
|
var components = clip.components;
|
|
var targetComp = null;
|
|
for (var i = 0; i < components.numItems; i++) {
|
|
if (components[i].displayName === "${escapeForExtendScript(args.component_name)}") {
|
|
targetComp = components[i];
|
|
break;
|
|
}
|
|
}
|
|
if (!targetComp) return __error("Component not found");
|
|
|
|
var targetProp = null;
|
|
for (var j = 0; j < targetComp.properties.numItems; j++) {
|
|
if (targetComp.properties[j].displayName === "${escapeForExtendScript(args.property_name)}") {
|
|
targetProp = targetComp.properties[j];
|
|
break;
|
|
}
|
|
}
|
|
if (!targetProp) return __error("Property not found");
|
|
|
|
targetProp.setColorValue(${args.alpha}, ${args.red}, ${args.green}, ${args.blue}, true);
|
|
return __result({
|
|
set: true,
|
|
color: { alpha: ${args.alpha}, red: ${args.red}, green: ${args.green}, blue: ${args.blue} }
|
|
});
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
get_clip_adjustment_layer: {
|
|
description: "Check if a clip is an adjustment layer",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
},
|
|
required: ["node_id"],
|
|
},
|
|
handler: async (args: { node_id: string }) => {
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
var clip = result.clip;
|
|
var isAdj = false;
|
|
try { isAdj = clip.isAdjustmentLayer(); } catch(e) {}
|
|
|
|
return __result({ clipName: clip.name, isAdjustmentLayer: isAdj });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
get_linked_items: {
|
|
description:
|
|
"Get all clips in the sequence that are linked to the same source as a given clip",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the clip",
|
|
},
|
|
},
|
|
required: ["node_id"],
|
|
},
|
|
handler: async (args: { node_id: string }) => {
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
var linked = result.clip.getLinkedItems();
|
|
var items = [];
|
|
if (linked) {
|
|
for (var i = 0; i < linked.numItems; i++) {
|
|
items.push({
|
|
name: linked[i].name,
|
|
nodeId: linked[i].nodeId,
|
|
startSeconds: __ticksToSeconds(linked[i].start.ticks)
|
|
});
|
|
}
|
|
}
|
|
|
|
return __result({ clipName: result.clip.name, linkedItems: items, count: items.length });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
|
|
get_mogrt_component: {
|
|
description:
|
|
"Get MOGRT (Motion Graphics Template) component parameters from a clip",
|
|
parameters: {
|
|
type: "object" as const,
|
|
properties: {
|
|
node_id: {
|
|
type: "string",
|
|
description: "Node ID of the MOGRT clip on the timeline",
|
|
},
|
|
},
|
|
required: ["node_id"],
|
|
},
|
|
handler: async (args: { node_id: string }) => {
|
|
const script = buildToolScript(`
|
|
var result = __findClip("${escapeForExtendScript(args.node_id)}");
|
|
if (!result) return __error("Clip not found");
|
|
|
|
var mgtComp = result.clip.getMGTComponent();
|
|
if (!mgtComp) return __error("Not a MOGRT clip or no MGT component found");
|
|
|
|
var params = [];
|
|
for (var i = 0; i < mgtComp.properties.numItems; i++) {
|
|
var p = mgtComp.properties[i];
|
|
params.push({
|
|
displayName: p.displayName,
|
|
value: p.getValue()
|
|
});
|
|
}
|
|
|
|
return __result({ clipName: result.clip.name, parameters: params });
|
|
`);
|
|
return sendCommand(script, bridgeOptions);
|
|
},
|
|
},
|
|
};
|
|
}
|