Files
jhonny-editor/code/src/tools/track-targeting.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

1689 lines
62 KiB
TypeScript
Executable File

import {
buildToolScript,
escapeForExtendScript,
} from "../bridge/script-builder.js";
import { sendCommand, BridgeOptions } from "../bridge/file-bridge.js";
/**
* Premiere's audio `Volume > Level` property is NOT in decibels. It is a
* normalised 0..1 value where 1.0 is the maximum boost of +15 dB, so unity
* (0 dB) sits at 10^(-15/20) = 0.17782794.
*
* Passing decibels straight to setValue() silently clamps: any negative dB
* becomes 0 (total silence) and any positive dB becomes 1.0 (+15 dB). Both
* fail without an error, which makes the mistake very hard to spot - the
* timeline just plays wrong.
*/
export const PREMIERE_MAX_LEVEL_DB = 15;
export function dbToPremiereLevel(db: number): number {
if (!Number.isFinite(db)) return 0;
const clamped = Math.min(db, PREMIERE_MAX_LEVEL_DB);
return Math.pow(10, (clamped - PREMIERE_MAX_LEVEL_DB) / 20);
}
export function premiereLevelToDb(level: number): number | null {
if (!Number.isFinite(level) || level <= 0) return null; // silence
return 20 * Math.log10(level) + PREMIERE_MAX_LEVEL_DB;
}
export function getTrackTargetingTools(bridgeOptions: BridgeOptions) {
return {
set_target_track: {
description:
"Set a track as targeted (active for insert/overwrite edits). Only one video and one audio track can be targeted at a time.",
parameters: {
type: "object" as const,
properties: {
track_type: {
type: "string",
enum: ["video", "audio"],
description: "Track type",
},
track_index: {
type: "number",
description: "Track index (0-based)",
},
targeted: {
type: "boolean",
description:
"Whether to target (true) or untarget (false) the track",
},
},
required: ["track_type", "track_index", "targeted"],
},
handler: async (args: {
track_type: string;
track_index: number;
targeted: boolean;
}) => {
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var tracks = ${args.track_type === "video" ? "seq.videoTracks" : "seq.audioTracks"};
if (${args.track_index} >= tracks.numTracks) return __error("Track index out of range");
var track = tracks[${args.track_index}];
track.setTargeted(${args.targeted}, ${args.track_type === "video"});
return __result({
trackType: "${args.track_type}",
trackIndex: ${args.track_index},
trackName: track.name,
targeted: ${args.targeted}
});
`);
return sendCommand(script, bridgeOptions);
},
},
get_target_tracks: {
description: "Get which tracks are currently targeted for editing.",
parameters: {},
handler: async () => {
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var targets = { video: [], audio: [] };
for (var t = 0; t < seq.videoTracks.numTracks; t++) {
var track = seq.videoTracks[t];
try {
if (track.isTargeted()) {
targets.video.push({ index: t, name: track.name });
}
} catch(e) {}
}
for (var t = 0; t < seq.audioTracks.numTracks; t++) {
var track = seq.audioTracks[t];
try {
if (track.isTargeted()) {
targets.audio.push({ index: t, name: track.name });
}
} catch(e) {}
}
return __result(targets);
`);
return sendCommand(script, bridgeOptions);
},
},
set_all_tracks_targeted: {
description:
"Set all tracks targeted or untargeted. Useful before insert/overwrite edits.",
parameters: {
type: "object" as const,
properties: {
targeted: {
type: "boolean",
description:
"Whether to target (true) or untarget (false) all tracks",
},
track_type: {
type: "string",
enum: ["video", "audio", "both"],
description: "Which track type(s) to affect (default: both)",
},
},
required: ["targeted"],
},
handler: async (args: { targeted: boolean; track_type?: string }) => {
const trackType = args.track_type || "both";
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var count = 0;
if ("${trackType}" !== "audio") {
for (var t = 0; t < seq.videoTracks.numTracks; t++) {
try { seq.videoTracks[t].setTargeted(${args.targeted}, true); count++; } catch(e) {}
}
}
if ("${trackType}" !== "video") {
for (var t = 0; t < seq.audioTracks.numTracks; t++) {
try { seq.audioTracks[t].setTargeted(${args.targeted}, false); count++; } catch(e) {}
}
}
return __result({ tracksAffected: count, targeted: ${args.targeted} });
`);
return sendCommand(script, bridgeOptions);
},
},
rename_track: {
description: "Rename a video or audio track.",
parameters: {
type: "object" as const,
properties: {
track_type: {
type: "string",
enum: ["video", "audio"],
description: "Track type",
},
track_index: {
type: "number",
description: "Track index (0-based)",
},
name: {
type: "string",
description: "New track name",
},
},
required: ["track_type", "track_index", "name"],
},
handler: async (args: {
track_type: string;
track_index: number;
name: string;
}) => {
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var tracks = ${args.track_type === "video" ? "seq.videoTracks" : "seq.audioTracks"};
if (${args.track_index} >= tracks.numTracks) return __error("Track index out of range");
var track = tracks[${args.track_index}];
var oldName = track.name;
track.name = "${escapeForExtendScript(args.name)}";
return __result({ oldName: oldName, newName: track.name, trackType: "${args.track_type}", trackIndex: ${args.track_index} });
`);
return sendCommand(script, bridgeOptions);
},
},
get_track_info: {
description:
"Get detailed information about a specific track: name, clip count, muted, locked, targeted, and list of all clips.",
parameters: {
type: "object" as const,
properties: {
track_type: {
type: "string",
enum: ["video", "audio"],
description: "Track type",
},
track_index: {
type: "number",
description: "Track index (0-based)",
},
},
required: ["track_type", "track_index"],
},
handler: async (args: { track_type: string; track_index: number }) => {
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var tracks = ${args.track_type === "video" ? "seq.videoTracks" : "seq.audioTracks"};
if (${args.track_index} >= tracks.numTracks) return __error("Track index out of range");
var track = tracks[${args.track_index}];
var info = {
name: track.name,
trackType: "${args.track_type}",
trackIndex: ${args.track_index},
clipCount: track.clips.numItems,
isMuted: track.isMuted(),
isLocked: track.isLocked()
};
try { info.isTargeted = track.isTargeted(); } catch(e) {}
info.clips = [];
for (var c = 0; c < track.clips.numItems; c++) {
var clip = track.clips[c];
var ci = {
index: c,
nodeId: clip.nodeId,
name: clip.name,
startSeconds: __ticksToSeconds(clip.start.ticks),
endSeconds: __ticksToSeconds(clip.end.ticks),
durationSeconds: __ticksToSeconds(clip.duration.ticks)
};
try { ci.enabled = !clip.isDisabled(); } catch(e) { ci.enabled = true; }
try { ci.speed = clip.getSpeed(); } catch(e) {}
info.clips.push(ci);
}
info.transitions = [];
try {
for (var t = 0; t < track.transitions.numItems; t++) {
info.transitions.push({
index: t,
startSeconds: __ticksToSeconds(track.transitions[t].start.ticks),
endSeconds: __ticksToSeconds(track.transitions[t].end.ticks)
});
}
} catch(e) {}
return __result(info);
`);
return sendCommand(script, bridgeOptions);
},
},
razor_all_tracks: {
description:
"Razor (split) all clips at the playhead position across all tracks, or at a specific time.",
parameters: {
type: "object" as const,
properties: {
time_seconds: {
type: "number",
description:
"Time to razor at in seconds (uses playhead position if omitted)",
},
track_type: {
type: "string",
enum: ["video", "audio", "both"],
description: "Which track types to razor (default: both)",
},
},
},
handler: async (args: { time_seconds?: number; track_type?: string }) => {
const trackType = args.track_type || "both";
const script = buildToolScript(`
app.enableQE();
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var qeSeq = qe.project.getActiveSequence();
${
args.time_seconds !== undefined
? `var ticks = __secondsToTicks(${args.time_seconds}).toString();`
: `var ticks = seq.getPlayerPosition().ticks;`
}
// A razor only has to produce a new clip on tracks where some clip
// strictly spans the cut point. Tracks that are empty there are a
// legitimate no-op, so they must not count as failures.
function __spansPoint(domTrack, tickValue) {
var v = parseFloat(tickValue);
for (var c = 0; c < domTrack.clips.numItems; c++) {
var s = parseFloat(domTrack.clips[c].start.ticks);
var e = parseFloat(domTrack.clips[c].end.ticks);
if (v > s && v < e) return true;
}
return false;
}
var razored = 0;
var eligible = 0;
var failures = [];
// QE razor() expects a timecode string, not ticks. See timeline.ts.
var __razorFrameTicks = seq && seq.timebase ? parseFloat(seq.timebase) : NaN;
if (!__razorFrameTicks || isNaN(__razorFrameTicks)) __razorFrameTicks = 254016000000 / 24;
var __razorFps = Math.round(254016000000 / __razorFrameTicks);
if (!__razorFps || !isFinite(__razorFps) || __razorFps < 1) __razorFps = 30;
var __razorFrames = Math.round(parseFloat(ticks) / __razorFrameTicks);
function __pad2(n) { return n < 10 ? "0" + n : "" + n; }
var __razorTc = __pad2(Math.floor(__razorFrames / (__razorFps * 3600))) + ":" +
__pad2(Math.floor((__razorFrames % (__razorFps * 3600)) / (__razorFps * 60))) + ":" +
__pad2(Math.floor((__razorFrames % (__razorFps * 60)) / __razorFps)) + ":" +
__pad2(__razorFrames % __razorFps);
if ("${trackType}" !== "audio") {
for (var t = 0; t < seq.videoTracks.numTracks; t++) {
var domTrack = seq.videoTracks[t];
var wasEligible = __spansPoint(domTrack, ticks);
if (wasEligible) eligible++;
var before = domTrack.clips.numItems;
try {
qeSeq.getVideoTrackAt(t).razor(__razorTc);
} catch(e) {
if (wasEligible) failures.push("V" + (t + 1) + ": " + e.toString());
continue;
}
if (domTrack.clips.numItems > before) razored++;
}
}
if ("${trackType}" !== "video") {
for (var t = 0; t < seq.audioTracks.numTracks; t++) {
var domTrack = seq.audioTracks[t];
var wasEligible = __spansPoint(domTrack, ticks);
if (wasEligible) eligible++;
var before = domTrack.clips.numItems;
try {
qeSeq.getAudioTrackAt(t).razor(__razorTc);
} catch(e) {
if (wasEligible) failures.push("A" + (t + 1) + ": " + e.toString());
continue;
}
if (domTrack.clips.numItems > before) razored++;
}
}
// Previously this counted attempts, so it reported one "razor" per
// track even when every call silently did nothing.
if (eligible > 0 && razored < eligible) {
return __error("Premiere razored only " + razored + " of " + eligible + " eligible track(s)" + (failures.length ? " (" + failures.join("; ") + ")" : "") + ". The operation was only partially applied, so it is not reported as verified. Structural QE edits are known to no-op on some Premiere Pro 26.x installations (confirmed on 26.2.2).");
}
return __result({
razored: razored,
eligibleTracks: eligible,
verified: true,
failures: failures,
atSeconds: __ticksToSeconds(ticks)
});
`);
return sendCommand(script, bridgeOptions);
},
},
set_clip_start_time: {
description:
"Set the start time (timecode offset) of a project item. This shifts where timecode begins for the source media.",
parameters: {
type: "object" as const,
properties: {
item_id: {
type: "string",
description: "Node ID or name of the project item",
},
start_seconds: {
type: "number",
description: "New start time in seconds",
},
},
required: ["item_id", "start_seconds"],
},
handler: async (args: { item_id: string; start_seconds: number }) => {
const script = buildToolScript(`
var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
if (!item) return __error("Item not found");
var t = new Time();
t.seconds = ${args.start_seconds};
item.setStartTime(t.ticks);
return __result({ item: item.name, startSeconds: ${args.start_seconds} });
`);
return sendCommand(script, bridgeOptions);
},
},
clear_item_in_out: {
description:
"Clear in and/or out points on a project item (reset to full duration).",
parameters: {
type: "object" as const,
properties: {
item_id: {
type: "string",
description: "Node ID or name of the project item",
},
clear_in: {
type: "boolean",
description: "Clear the in point (default: true)",
},
clear_out: {
type: "boolean",
description: "Clear the out point (default: true)",
},
},
required: ["item_id"],
},
handler: async (args: {
item_id: string;
clear_in?: boolean;
clear_out?: boolean;
}) => {
const clearIn = args.clear_in !== false;
const clearOut = args.clear_out !== false;
const script = buildToolScript(`
var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
if (!item) return __error("Item not found");
${clearIn ? `item.clearInPoint();` : ""}
${clearOut ? `item.clearOutPoint();` : ""}
return __result({ item: item.name, clearedIn: ${clearIn}, clearedOut: ${clearOut} });
`);
return sendCommand(script, bridgeOptions);
},
},
set_item_in_out: {
description:
"Set in and/or out points on a project item in the project panel (marks source range for editing).",
parameters: {
type: "object" as const,
properties: {
item_id: {
type: "string",
description: "Node ID or name of the project item",
},
in_seconds: {
type: "number",
description: "In point in seconds",
},
out_seconds: {
type: "number",
description: "Out point in seconds",
},
media_type: {
type: "number",
description: "Media type: 1=video, 2=audio, 4=all (default: 4)",
},
},
required: ["item_id"],
},
handler: async (args: {
item_id: string;
in_seconds?: number;
out_seconds?: number;
media_type?: number;
}) => {
if (args.in_seconds === undefined && args.out_seconds === undefined) {
return { success: false, error: "Provide in_seconds, out_seconds, or both." };
}
if ((args.in_seconds !== undefined && (!Number.isFinite(args.in_seconds) || args.in_seconds < 0))
|| (args.out_seconds !== undefined && (!Number.isFinite(args.out_seconds) || args.out_seconds < 0))) {
return { success: false, error: "in_seconds and out_seconds must be finite, non-negative numbers." };
}
const mediaType = args.media_type ?? 4;
const script = buildToolScript(`
var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
if (!item) return __error("Item not found");
${
args.in_seconds !== undefined
? `
var inTime = new Time();
inTime.seconds = ${args.in_seconds};
item.setInPoint(inTime.ticks, ${mediaType});
var observedIn = item.getInPoint(${mediaType});
if (!observedIn || String(observedIn.ticks) !== String(inTime.ticks)) {
return __error("Premiere did not apply the requested project-item in point.");
}
`
: ""
}
${
args.out_seconds !== undefined
? `
var outTime = new Time();
outTime.seconds = ${args.out_seconds};
item.setOutPoint(outTime.ticks, ${mediaType});
var observedOut = item.getOutPoint(${mediaType});
if (!observedOut || String(observedOut.ticks) !== String(outTime.ticks)) {
return __error("Premiere did not apply the requested project-item out point.");
}
`
: ""
}
return __result({
item: item.name,
inSet: ${args.in_seconds !== undefined},
outSet: ${args.out_seconds !== undefined},
verified: true
});
`);
return sendCommand(script, bridgeOptions);
},
},
import_image_sequence: {
description: "Import a numbered image sequence as a single video clip.",
parameters: {
type: "object" as const,
properties: {
first_file_path: {
type: "string",
description:
"Full path to the first image in the sequence (e.g., /path/to/frame_001.png)",
},
target_bin: {
type: "string",
description:
"Target bin name to import into (optional, imports to root if omitted)",
},
},
required: ["first_file_path"],
},
handler: async (args: {
first_file_path: string;
target_bin?: string;
}) => {
const script = buildToolScript(`
var sourceFile = new File("${escapeForExtendScript(args.first_file_path)}");
if (!sourceFile.exists) return __error("The first image file does not exist: " + sourceFile.fsName);
var targetBin = app.project.rootItem;
${
args.target_bin
? `
var found = __findProjectItem("${escapeForExtendScript(args.target_bin)}");
if (found && found.type === 2) targetBin = found;
`
: ""
}
var beforeCount = targetBin.children.numItems;
var imported = app.project.importFiles([sourceFile.fsName], true, targetBin, true);
if (!imported) return __error("Premiere did not import the numbered image sequence.");
var importedItem = null;
for (var i = 0; i < targetBin.children.numItems; i++) {
var candidate = targetBin.children[i];
try {
if (candidate.getMediaPath && String(candidate.getMediaPath()) === String(sourceFile.fsName)) {
importedItem = candidate;
break;
}
} catch (e) {}
}
if (!importedItem && targetBin.children.numItems <= beforeCount) {
return __error("Premiere accepted the image-sequence import but no project item was added.");
}
return __result({
imported: true,
file: sourceFile.fsName,
asImageSequence: true,
verified: true,
projectItem: importedItem ? { name: importedItem.name, nodeId: importedItem.nodeId } : null
});
`);
return sendCommand(script, bridgeOptions);
},
},
set_clip_position: {
description:
"Set the Position property on a video clip's Motion effect. Values are in pixels.",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the clip",
},
x: {
type: "number",
description: "X position in pixels",
},
y: {
type: "number",
description: "Y position in pixels",
},
},
required: ["node_id", "x", "y"],
},
handler: async (args: { node_id: string; x: number; y: number }) => {
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var set = false;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Motion") {
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
if (clip.components[i].properties[p].displayName === "Position") {
clip.components[i].properties[p].setValue([${args.x}, ${args.y}], true);
set = true;
break;
}
}
break;
}
}
if (!set) return __error("Could not set position");
return __result({ x: ${args.x}, y: ${args.y}, clip: clip.name });
`);
return sendCommand(script, bridgeOptions);
},
},
set_clip_scale: {
description: "Set the Scale property on a video clip's Motion effect.",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the clip",
},
scale: {
type: "number",
description:
"Scale value (100 = original size, 200 = 2x, 50 = half)",
},
},
required: ["node_id", "scale"],
},
handler: async (args: { node_id: string; scale: number }) => {
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var set = false;
for (var i = 0; i < clip.components.numItems; i++) {
var comp = clip.components[i];
// matchName do COMPONENTE é estável entre idiomas ("AE.ADBE Motion");
// displayName varia com o idioma da UI (ex.: "Movimento" em pt-BR).
var isMotion = comp.matchName === "AE.ADBE Motion" || comp.displayName === "Motion";
if (isMotion) {
// As propriedades individuais (ComponentParam) NÃO expõem matchName de
// forma confiável nesta API — vem undefined mesmo quando o componente
// tem matchName válido (confirmado via get_full_clip_info). O efeito
// Motion embutido do Premiere, porém, sempre lista Scale no índice 1
// (0=Position, 1=Scale, 2=Scale Width, 3=Uniform Scale, 4=Rotation, ...)
// em qualquer idioma ou versão — é um efeito interno, não reordenável
// pelo usuário — então usamos a posição em vez do nome.
var scaleProp = comp.properties[1];
if (scaleProp) {
scaleProp.setValue(${args.scale}, true);
set = true;
}
break;
}
}
if (!set) return __error("Could not set scale");
return __result({ scale: ${args.scale}, clip: clip.name });
`);
return sendCommand(script, bridgeOptions);
},
},
set_clip_rotation: {
description: "Set the Rotation property on a video clip's Motion effect.",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the clip",
},
degrees: {
type: "number",
description:
"Rotation in degrees (0-360, can exceed for multiple rotations)",
},
},
required: ["node_id", "degrees"],
},
handler: async (args: { node_id: string; degrees: number }) => {
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var set = false;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Motion") {
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
if (clip.components[i].properties[p].displayName === "Rotation") {
clip.components[i].properties[p].setValue(${args.degrees}, true);
set = true;
break;
}
}
break;
}
}
if (!set) return __error("Could not set rotation");
return __result({ degrees: ${args.degrees}, clip: clip.name });
`);
return sendCommand(script, bridgeOptions);
},
},
set_clip_anchor_point: {
description:
"Set the Anchor Point property on a video clip's Motion effect.",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the clip",
},
x: {
type: "number",
description: "Anchor point X in pixels",
},
y: {
type: "number",
description: "Anchor point Y in pixels",
},
},
required: ["node_id", "x", "y"],
},
handler: async (args: { node_id: string; x: number; y: number }) => {
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var set = false;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Motion") {
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
if (clip.components[i].properties[p].displayName === "Anchor Point") {
clip.components[i].properties[p].setValue([${args.x}, ${args.y}], true);
set = true;
break;
}
}
break;
}
}
if (!set) return __error("Could not set anchor point");
return __result({ x: ${args.x}, y: ${args.y}, clip: clip.name });
`);
return sendCommand(script, bridgeOptions);
},
},
set_clip_opacity: {
description: "Set the opacity of a video clip (0-100).",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the clip",
},
opacity: {
type: "number",
description: "Opacity value (0 = transparent, 100 = fully opaque)",
},
},
required: ["node_id", "opacity"],
},
handler: async (args: { node_id: string; opacity: number }) => {
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var set = false;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Opacity") {
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
if (clip.components[i].properties[p].displayName === "Opacity") {
clip.components[i].properties[p].setValue(${args.opacity}, true);
set = true;
break;
}
}
break;
}
}
if (!set) return __error("Could not set opacity");
return __result({ opacity: ${args.opacity}, clip: clip.name });
`);
return sendCommand(script, bridgeOptions);
},
},
set_clip_volume: {
description: "Set an audio clip's Volume > Level in dB. Does not read or change Essential Sound Amplify automation.",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the audio clip",
},
volume_db: {
type: "number",
description:
"Volume in dB (0 = unity, negative = quieter, positive = louder)",
},
},
required: ["node_id", "volume_db"],
},
handler: async (args: { node_id: string; volume_db: number }) => {
const level = dbToPremiereLevel(args.volume_db);
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var set = false;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Volume") {
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
if (clip.components[i].properties[p].displayName === "Level") {
// normalised 0..1, NOT dB - see dbToPremiereLevel()
clip.components[i].properties[p].setValue(${level}, true);
set = true;
break;
}
}
break;
}
}
if (!set) return __error("Could not set volume - is this an audio clip?");
return __result({ volumeDb: ${args.volume_db}, level: ${level}, clip: clip.name });
`);
return sendCommand(script, bridgeOptions);
},
},
get_clip_volume: {
description:
"Read an audio clip's Volume > Level in dB. Use this to verify a level actually applied - setValue() clamps silently. Does not report Essential Sound Amplify automation.",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the audio 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 level = null;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Volume") {
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
if (clip.components[i].properties[p].displayName === "Level") {
level = clip.components[i].properties[p].getValue();
break;
}
}
break;
}
}
if (level === null) return __error("No volume component - is this an audio clip?");
var db = (level > 0) ? (20 * (Math.log(level) / Math.LN10) + ${PREMIERE_MAX_LEVEL_DB}) : null;
return __result({ clip: clip.name, level: level, volumeDb: db });
`);
return sendCommand(script, bridgeOptions);
},
},
set_clips_volume: {
description:
"Set the volume (in dB) on every audio clip of a track, or on a list of clip indices. One round trip instead of one call per clip - essential for sequences with dozens of clips.",
parameters: {
type: "object" as const,
properties: {
track_index: {
type: "number",
description: "Audio track index (0-based)",
},
volume_db: {
type: "number",
description:
"Volume in dB applied to every selected clip (0 = unity, max +15)",
},
clip_indices: {
type: "array",
items: { type: "number" },
description:
"Optional clip indices on that track. Omit to apply to all clips.",
},
},
required: ["track_index", "volume_db"],
},
handler: async (args: {
track_index: number;
volume_db: number;
clip_indices?: number[];
}) => {
const level = dbToPremiereLevel(args.volume_db);
const only = Array.isArray(args.clip_indices)
? JSON.stringify(args.clip_indices)
: "null";
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
if (${args.track_index} >= seq.audioTracks.numTracks)
return __error("Track index out of range");
var track = seq.audioTracks[${args.track_index}];
var only = ${only};
var wanted = {};
if (only) { for (var w = 0; w < only.length; w++) wanted[only[w]] = true; }
var applied = 0, skipped = 0;
for (var c = 0; c < track.clips.numItems; c++) {
if (only && !wanted[c]) continue;
var clip = track.clips[c], set = false;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName !== "Volume") continue;
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
if (clip.components[i].properties[p].displayName === "Level") {
clip.components[i].properties[p].setValue(${level}, true);
set = true;
break;
}
}
break;
}
if (set) applied++; else skipped++;
}
return __result({
trackIndex: ${args.track_index},
volumeDb: ${args.volume_db},
level: ${level},
applied: applied,
skipped: skipped
});
`);
return sendCommand(script, bridgeOptions);
},
},
set_clip_pan: {
description: "Set and read back the pan (left/right balance) on an audio clip, including Channel Volume layouts.",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the audio clip",
},
pan: {
type: "number",
description:
"Pan value (-100 = full left, 0 = center, 100 = full right)",
},
},
required: ["node_id", "pan"],
},
handler: async (args: { node_id: string; pan: number }) => {
if (!Number.isFinite(args.pan) || args.pan < -100 || args.pan > 100) {
return { success: false, error: "pan must be a finite value from -100 through 100" };
}
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
if (result.trackType !== "audio") return __error("Clip is not on an audio track");
var clip = result.clip;
var panProperty = null;
for (var i = 0; i < clip.components.numItems; i++) {
var component = clip.components[i];
for (var p = 0; p < component.properties.numItems; p++) {
var property = component.properties[p];
if (property.displayName === "Balance" || property.displayName === "Pan") {
panProperty = property;
break;
}
}
if (panProperty) break;
}
if (!panProperty) return __error("Audio clip does not expose a writable Balance or Pan property (checked Panner, Channel Volume, and other audio components).");
var writeResult = panProperty.setValue(${args.pan}, true);
var appliedPan = Number(panProperty.getValue());
if (isNaN(appliedPan) || Math.abs(appliedPan - ${args.pan}) > 0.0001) {
return __error("Premiere did not apply the requested pan: expected ${args.pan}, got " + appliedPan);
}
return __result({ pan: appliedPan, verified: true, writeResult: writeResult, clip: clip.name });
`);
return sendCommand(script, bridgeOptions);
},
},
batch_rename_clips: {
description:
"Rename multiple clips on the timeline using a pattern. Supports sequential numbering.",
parameters: {
type: "object" as const,
properties: {
pattern: {
type: "string",
description:
"Name pattern. Use {n} for a sequential number, ## for a zero-padded two-digit sequence number, and {name} for the original name (e.g., 'Scene_##', '{name}_v2')",
},
track_type: {
type: "string",
enum: ["video", "audio"],
description: "Track type to rename clips on",
},
track_index: {
type: "number",
description: "Track index (0-based)",
},
selected_only: {
type: "boolean",
description:
"Only rename selected clips (default: false, renames all on track)",
},
start_number: {
type: "number",
description: "Starting number for {n} placeholder (default: 1)",
},
},
required: ["pattern", "track_type", "track_index"],
},
handler: async (args: {
pattern: string;
track_type: string;
track_index: number;
selected_only?: boolean;
start_number?: number;
}) => {
const startNum = args.start_number ?? 1;
if (!Number.isInteger(startNum) || startNum < 1 || startNum > 1_000_000) {
return { success: false, error: "start_number must be an integer from 1 through 1000000" };
}
const script = buildToolScript(`
app.enableQE();
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var tracks = ${args.track_type === "video" ? "seq.videoTracks" : "seq.audioTracks"};
if (${args.track_index} >= tracks.numTracks) return __error("Track index out of range");
var qeSeq = qe.project.getActiveSequence();
var qeTrack = ${
args.track_type === "video"
? `qeSeq.getVideoTrackAt(${args.track_index})`
: `qeSeq.getAudioTrackAt(${args.track_index})`
};
var track = tracks[${args.track_index}];
var renamed = 0;
var num = ${startNum};
var pattern = "${escapeForExtendScript(args.pattern)}";
for (var c = 0; c < track.clips.numItems; c++) {
var clip = track.clips[c];
${args.selected_only ? `if (!clip.isSelected()) continue;` : ""}
var sequenceNumber = "" + num;
var paddedSequenceNumber = sequenceNumber;
while (paddedSequenceNumber.length < 2) paddedSequenceNumber = "0" + paddedSequenceNumber;
var newName = pattern.split("{n}").join(sequenceNumber).split("##").join(paddedSequenceNumber).split("{name}").join(clip.name);
try {
var qeClip = qeTrack.getItemAt(c);
qeClip.setName(newName);
renamed++;
} catch(e) {}
num++;
}
return __result({ renamed: renamed, pattern: pattern });
`);
return sendCommand(script, bridgeOptions);
},
},
batch_enable_disable: {
description:
"Enable or disable multiple clips at once (selected, track, or all).",
parameters: {
type: "object" as const,
properties: {
enabled: {
type: "boolean",
description: "true to enable, false to disable",
},
target: {
type: "string",
enum: ["selected", "track", "all"],
description: "Which clips to affect",
},
track_type: {
type: "string",
enum: ["video", "audio"],
description: "Track type (required when target is 'track')",
},
track_index: {
type: "number",
description: "Track index (required when target is 'track')",
},
},
required: ["enabled", "target"],
},
handler: async (args: {
enabled: boolean;
target: string;
track_type?: string;
track_index?: number;
}) => {
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var count = 0;
var disabled = ${!args.enabled};
var candidates = 0;
var failures = [];
function setOnTrack(track) {
for (var c = 0; c < track.clips.numItems; c++) {
try {
if ("${args.target}" === "selected" && !track.clips[c].isSelected()) continue;
candidates++;
track.clips[c].disabled = disabled;
if (Boolean(track.clips[c].disabled) !== disabled) {
failures.push(track.clips[c].name || ("clip #" + c));
continue;
}
count++;
} catch(e) { failures.push("clip #" + c + ": " + e.toString()); }
}
}
if ("${args.target}" === "track") {
var tracks = ${(args.track_type || "video") === "video" ? "seq.videoTracks" : "seq.audioTracks"};
if (${args.track_index ?? 0} >= tracks.numTracks) return __error("Track index out of range");
setOnTrack(tracks[${args.track_index ?? 0}]);
} else {
for (var t = 0; t < seq.videoTracks.numTracks; t++) setOnTrack(seq.videoTracks[t]);
for (var t = 0; t < seq.audioTracks.numTracks; t++) setOnTrack(seq.audioTracks[t]);
}
if (candidates === 0) return __error("No clips matched target '${args.target}'. Select clips first or choose target 'track' or 'all'.");
if (failures.length) return __error("Premiere did not apply the requested enabled state to " + failures.length + " clip(s): " + failures.join("; "));
return __result({ affected: count, enabled: ${args.enabled}, verified: true });
`);
return sendCommand(script, bridgeOptions);
},
},
remove_selected_clips: {
description: "Remove all currently selected clips from the timeline.",
parameters: {
type: "object" as const,
properties: {
ripple: {
type: "boolean",
description:
"If true, close the gap after removing (ripple delete). Default: false",
},
},
},
handler: async (args: { ripple?: boolean }) => {
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var removed = 0;
var toRemove = [];
function collect(tracks) {
for (var t = 0; t < tracks.numTracks; t++) {
for (var c = tracks[t].clips.numItems - 1; c >= 0; c--) {
if (tracks[t].clips[c].isSelected()) {
toRemove.push(tracks[t].clips[c]);
}
}
}
}
collect(seq.videoTracks);
collect(seq.audioTracks);
for (var i = 0; i < toRemove.length; i++) {
try {
toRemove[i].remove(${args.ripple ? "true" : "false"}, true);
removed++;
} catch(e) {}
}
return __result({ removed: removed, ripple: ${args.ripple ? "true" : "false"} });
`);
return sendCommand(script, bridgeOptions);
},
},
clear_sequence_in_out: {
description: "Clear the in and/or out points on the active sequence.",
parameters: {
type: "object" as const,
properties: {
clear_in: {
type: "boolean",
description: "Clear in point (default: true)",
},
clear_out: {
type: "boolean",
description: "Clear out point (default: true)",
},
},
},
handler: async (args: { clear_in?: boolean; clear_out?: boolean }) => {
const clearIn = args.clear_in !== false;
const clearOut = args.clear_out !== false;
const script = buildToolScript(`
var seq = app.project.activeSequence;
if (!seq) return __error("No active sequence");
var zeroSeconds = __ticksToSeconds(seq.zeroPoint);
var endSeconds = __ticksToSeconds(seq.end);
${clearIn ? `seq.setInPoint(zeroSeconds);` : ""}
${clearOut ? `seq.setOutPoint(endSeconds);` : ""}
${clearIn ? `if (Math.abs(Number(seq.getInPoint()) - zeroSeconds) > 0.000001) return __error("Premiere did not clear the sequence in point.");` : ""}
${clearOut ? `if (Math.abs(Number(seq.getOutPoint()) - endSeconds) > 0.000001) return __error("Premiere did not clear the sequence out point.");` : ""}
return __result({ clearedIn: ${clearIn}, clearedOut: ${clearOut}, verified: true });
`);
return sendCommand(script, bridgeOptions);
},
},
get_encoder_presets: {
description:
"List available Adobe Media Encoder export presets, with the .epr path of each so it can be " +
"passed to export_sequence or encode_project_item. Presets are discovered by scanning the .epr " +
"files Adobe ships on disk (Premiere's ExtendScript API exposes no preset enumeration).",
parameters: {
type: "object" as const,
properties: {
format: {
type: "string",
description:
"Filter to presets whose name or format bucket matches this (e.g. 'H.264', 'ProRes', 'Proxy'). Omit to list all.",
},
},
},
handler: async (args: { format?: string }) => {
const script = buildToolScript(`
var presets = __collectAllPresets();
if (!presets.length) {
return __error("No .epr presets found. Is Adobe Media Encoder installed alongside Premiere Pro?");
}
${
args.format
? `var needle = __presetSearchText("${escapeForExtendScript(args.format)}");
var filtered = [];
for (var i = 0; i < presets.length; i++) {
var p = presets[i];
if (__presetSearchText(p.name).indexOf(needle) !== -1 || __presetSearchText(p.format).indexOf(needle) !== -1) {
filtered.push(p);
}
}
presets = filtered;`
: ""
}
return __result({ count: presets.length, presets: presets });
`);
return sendCommand(script, bridgeOptions);
},
},
get_qe_clip_info: {
description:
"Get QE DOM information about a clip, including properties not available through the standard API.",
parameters: {
type: "object" as const,
properties: {
track_type: {
type: "string",
enum: ["video", "audio"],
description: "Track type",
},
track_index: {
type: "number",
description: "Track index (0-based)",
},
clip_index: {
type: "number",
description: "Clip index on the track (0-based)",
},
},
required: ["track_type", "track_index", "clip_index"],
},
handler: async (args: {
track_type: string;
track_index: number;
clip_index: number;
}) => {
const script = buildToolScript(`
app.enableQE();
var qeSeq = qe.project.getActiveSequence();
if (!qeSeq) return __error("No active sequence");
var qeTrack = ${
args.track_type === "video"
? `qeSeq.getVideoTrackAt(${args.track_index})`
: `qeSeq.getAudioTrackAt(${args.track_index})`
};
if (!qeTrack) return __error("Track not found");
var qeClip = qeTrack.getItemAt(${args.clip_index});
if (!qeClip) return __error("Clip not found at index ${args.clip_index}");
var info = { trackType: "${args.track_type}", trackIndex: ${args.track_index}, clipIndex: ${args.clip_index} };
// Try to read all QE clip properties
var props = ["name", "type", "mediaType", "duration", "start", "end", "inPoint", "outPoint",
"speed", "audioChannelType", "numAudioChannels"];
for (var i = 0; i < props.length; i++) {
try { info[props[i]] = qeClip[props[i]]; } catch(e) {}
}
// Enumerate any additional properties
var extra = [];
try {
for (var key in qeClip) {
if (typeof qeClip[key] !== "function") {
extra.push(key);
}
}
} catch(e) {}
info.availableProperties = extra;
// Enumerate methods
var methods = [];
try {
for (var key in qeClip) {
if (typeof qeClip[key] === "function") {
methods.push(key);
}
}
} catch(e) {}
info.availableMethods = methods;
return __result(info);
`);
return sendCommand(script, bridgeOptions);
},
},
redo: {
description: "Redo the last undone action in Premiere Pro.",
parameters: {},
handler: async () => {
const script = buildToolScript(`
app.enableQE();
try {
qe.project.redo();
return __result({ redone: true });
} catch(e) {
return __error("Redo failed: " + e.message);
}
`);
return sendCommand(script, bridgeOptions);
},
},
multiple_undo: {
description: "Unavailable: Premiere exposes no supported, observable undo-stack API for multiple scripted undo steps.",
parameters: {
type: "object" as const,
properties: {
count: {
type: "number",
description: "Number of undo steps (default: 1)",
},
},
},
handler: async (args: { count?: number }) => {
const count = args.count ?? 1;
if (!Number.isInteger(count) || count < 1 || count > 100) {
return { success: false, error: "count must be an integer from 1 through 100" };
}
return {
success: false,
error: "multiple_undo is unavailable because Premiere exposes no supported undo-stack API that can verify how many actions were undone. No mutation was attempted.",
};
},
},
set_poster_frame: {
description:
"Set the poster frame (thumbnail) for a project item at a specific time.",
parameters: {
type: "object" as const,
properties: {
item_id: {
type: "string",
description: "Node ID or name of the project item",
},
time_seconds: {
type: "number",
description: "Time in seconds for the poster frame",
},
},
required: ["item_id", "time_seconds"],
},
handler: async (args: { item_id: string; time_seconds: number }) => {
const script = buildToolScript(`
var item = __findProjectItem("${escapeForExtendScript(args.item_id)}");
if (!item) return __error("Item not found");
try {
var t = new Time();
t.seconds = ${args.time_seconds};
item.setOverrideFrameRate(0); // Trigger internal update
// Use project metadata to mark poster frame
return __result({ item: item.name, timeSeconds: ${args.time_seconds}, note: "Poster frame set attempt - may require UI interaction" });
} catch(e) {
return __error("Failed to set poster frame: " + e.message);
}
`);
return sendCommand(script, bridgeOptions);
},
},
get_version_info: {
description: "Get Premiere Pro version and build information.",
parameters: {},
handler: async () => {
const script = buildToolScript(`
var info = {
version: app.version,
buildNumber: app.build
};
try { info.isDocumentOpen = app.isDocumentOpen(); } catch(e) {}
try { info.path = app.path; } catch(e) {}
return __result(info);
`);
return sendCommand(script, bridgeOptions);
},
},
move_items_to_bin: {
description: "Move multiple project items to a target bin at once.",
parameters: {
type: "object" as const,
properties: {
item_ids: {
type: "array",
items: { type: "string" },
description: "Array of node IDs or names of items to move",
},
target_bin: {
type: "string",
description: "Name or node ID of the target bin",
},
},
required: ["item_ids", "target_bin"],
},
handler: async (args: { item_ids: string[]; target_bin: string }) => {
const idsJson = JSON.stringify(args.item_ids);
const script = buildToolScript(`
var targetBin = __findProjectItem("${escapeForExtendScript(args.target_bin)}");
if (!targetBin || targetBin.type !== 2) return __error("Target bin not found: ${escapeForExtendScript(args.target_bin)}");
var ids = ${idsJson};
var moved = 0;
for (var i = 0; i < ids.length; i++) {
var item = __findProjectItem(ids[i]);
if (item) {
try {
item.moveBin(targetBin);
moved++;
} catch(e) {}
}
}
return __result({ moved: moved, total: ids.length, targetBin: targetBin.name });
`);
return sendCommand(script, bridgeOptions);
},
},
set_anti_alias_quality: {
description:
"Set the anti-alias quality on a clip's Motion effect (useful for scaled/rotated clips).",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the clip",
},
enabled: {
type: "boolean",
description: "Enable (true) or disable (false) anti-aliasing",
},
},
required: ["node_id", "enabled"],
},
handler: async (args: { node_id: string; enabled: boolean }) => {
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var antiFlicker = null;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Motion") {
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
var pName = clip.components[i].properties[p].displayName;
if (pName === "Anti-flicker Filter") {
antiFlicker = clip.components[i].properties[p];
break;
}
}
break;
}
}
if (!antiFlicker) return __error("The clip Motion component does not expose an Anti-flicker Filter parameter.");
var requestedValue = ${args.enabled ? 1 : 0};
antiFlicker.setValue(requestedValue, 1);
var observedValue = Number(antiFlicker.getValue());
if (Math.abs(observedValue - requestedValue) > 0.000001) {
return __error("Premiere did not apply the requested Anti-flicker Filter value.");
}
return __result({
clip: clip.name,
antiAlias: ${args.enabled},
antiFlickerValue: observedValue,
verified: true
});
`);
return sendCommand(script, bridgeOptions);
},
},
set_uniform_scale: {
description:
"Toggle uniform scale on a clip's Motion effect. When enabled, Scale Width and Scale Height are linked.",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the clip",
},
uniform: {
type: "boolean",
description:
"true for uniform (linked), false for non-uniform (independent width/height)",
},
},
required: ["node_id", "uniform"],
},
handler: async (args: { node_id: string; uniform: boolean }) => {
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var set = false;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Motion") {
for (var p = 0; p < clip.components[i].properties.numItems; p++) {
if (clip.components[i].properties[p].displayName === "Uniform Scale") {
clip.components[i].properties[p].setValue(${args.uniform}, true);
set = true;
break;
}
}
break;
}
}
if (!set) return __error("Uniform Scale property not found");
return __result({ clip: clip.name, uniformScale: ${args.uniform} });
`);
return sendCommand(script, bridgeOptions);
},
},
set_scale_width_height: {
description:
"Set independent Scale Width and Scale Height on a clip (requires Uniform Scale to be OFF).",
parameters: {
type: "object" as const,
properties: {
node_id: {
type: "string",
description: "Node ID of the clip",
},
scale_width: {
type: "number",
description: "Scale width percentage",
},
scale_height: {
type: "number",
description: "Scale height percentage",
},
},
required: ["node_id", "scale_width", "scale_height"],
},
handler: async (args: {
node_id: string;
scale_width: number;
scale_height: number;
}) => {
const script = buildToolScript(`
var result = __findClip("${escapeForExtendScript(args.node_id)}");
if (!result) return __error("Clip not found");
var clip = result.clip;
var motion = null;
for (var i = 0; i < clip.components.numItems; i++) {
if (clip.components[i].displayName === "Motion") { motion = clip.components[i]; break; }
}
if (!motion) return __error("Motion component not found");
// Disable uniform scale first
for (var p = 0; p < motion.properties.numItems; p++) {
if (motion.properties[p].displayName === "Uniform Scale") {
motion.properties[p].setValue(false, true);
break;
}
}
var setW = false, setH = false;
for (var p = 0; p < motion.properties.numItems; p++) {
var pName = motion.properties[p].displayName;
if (pName === "Scale Width") { motion.properties[p].setValue(${args.scale_width}, true); setW = true; }
if (pName === "Scale Height") { motion.properties[p].setValue(${args.scale_height}, true); setH = true; }
}
return __result({ clip: clip.name, scaleWidth: ${args.scale_width}, scaleHeight: ${args.scale_height}, widthSet: setW, heightSet: setH });
`);
return sendCommand(script, bridgeOptions);
},
},
};
}