Files
jhonny-editor/bm/premiere-pro-mcp-main/src/tools/advanced.ts
T
João Henrique b541f502ba feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
2026-09-08 09:59:31 -04:00

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);
},
},
};
}