- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
1689 lines
62 KiB
TypeScript
Executable File
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);
|
|
},
|
|
},
|
|
};
|
|
}
|