premiere-pro-mcp 1.18.3 → 1.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/README.md +10 -10
- package/after-effects-cep-plugin/CSXS/manifest.xml +2 -2
- package/artifacts/MCPBridgeCEP.zxp +0 -0
- package/cep-plugin/CSXS/manifest.xml +3 -3
- package/cep-plugin/index.html +1 -1
- package/cep-plugin/updater.cjs +1 -1
- package/dist/bridge/script-builder.js +64 -0
- package/dist/bridge/uxp-websocket-bridge.d.ts +2 -0
- package/dist/bridge/uxp-websocket-bridge.js +15 -7
- package/dist/tools/advanced.js +101 -24
- package/dist/tools/clipboard.js +75 -15
- package/dist/tools/effects.js +23 -15
- package/dist/tools/media.js +27 -2
- package/dist/tools/playback.js +12 -1
- package/dist/tools/project.d.ts +19 -2
- package/dist/tools/project.js +59 -6
- package/dist/tools/sequence.js +181 -24
- package/dist/tools/timeline.js +90 -15
- package/dist/tools/track-targeting.js +22 -3
- package/dist/tools/utility.d.ts +50 -4
- package/dist/tools/utility.js +182 -44
- package/dist/tools/uxp-advanced-workflows.js +2 -2
- package/dist/tools/uxp.js +2 -2
- package/docs/supported-actions.md +12 -12
- package/package.json +1 -1
- package/public-product-manifest.json +1 -1
- package/uxp-plugin/advanced-workflows.cjs +90 -14
- package/uxp-plugin/commands.cjs +67 -2
- package/uxp-plugin/index.cjs +2 -30
- package/uxp-plugin/manifest.json +1 -1
- package/uxp-plugin/next-workflows.cjs +29 -6
- package/uxp-plugin/transcript-import.cjs +6 -0
- package/uxp-plugin/workflows.cjs +8 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,43 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.18.5] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `unnest_sequence` refuses before any change when the nest is trimmed, speed-changed or reversed, a target track is locked, or the target range already holds a clip. It now overwrites each nested clip's exact source range in place instead of inserting it, which pushed later clips down the track and ignored each clip's in and out. It reads every clip back afterwards and reports "The timeline changed … Use Undo" on any mismatch. Effects, keyframes, and transitions inside the nest are not carried over; the description says so. (#642)
|
|
14
|
+
- `move_clip_to_track` refuses before calling QE when either track is locked or the target range is occupied, finds the moved clip even when its node ID changes, and reports a copy left on the original track or a changed start, length, or in/out. It is now marked EXPERIMENTAL (QE) and rejects non-integer track numbers. (#642)
|
|
15
|
+
- `replace_clip` overwrites exactly the old clip's start and end instead of inserting the new item, which pushed later clips down the track and used the new item's full length. It verifies the span, that the old clip is gone, and that track clip counts are unchanged, and refuses on a locked track. The old clip's linked partner is left in place; the description says so. (#642)
|
|
16
|
+
- `export_frame_uxp` checks that the PNG exists after Premiere's exporter returns. It fails when no file was written, reports the real path when the host names the file differently (with or without the `.png` extension), and reports `committed_unverified` when UXP storage cannot check or the file already existed before the export. The panel's direct frame-export path now uses the same handler. (#642)
|
|
17
|
+
- `encode_media_uxp` results for jobs sent to Adobe Media Encoder carry `ameQueueStarted: "unknown"` and a note. Premiere 26.5.1 accepted UXP encodes into the Media Encoder queue without starting it. The tool does not start the queue through CEP; the note points to Media Encoder or `start_batch_encode`. (#642)
|
|
18
|
+
- `organize_project_items_uxp` remove verifies absence by walking the project tree instead of checking the Project panel selection first, which could still hold the removed item and turned a successful removal into `committed_unverified`. Remove now requires `project_item_id` and never falls back to the selection. (#642)
|
|
19
|
+
- `refresh_media` reads the interpreted frame rate before and after the refresh. When the refresh leaves an implausible rate (reported on stills as 29.97 becoming 2.75e-8), it restores the previous rate, reads it back, and reports `repaired: true`; if the restore does not stick, it fails with the manual fix. A real rate change from the source file is kept. (#642)
|
|
20
|
+
- `edit_timeline_uxp` MOGRT inserts (`insert_mogrt_path`, `insert_mogrt_library`) read the placement back instead of reporting the SequenceEditor return value. The result is `verified` when a returned item starts at the requested time, within one frame, on the requested video track. It fails with `UXP_VERIFICATION_FAILED` when nothing is there, and stays `committed_unverified` when the track cannot be read. Results include `sequenceId`, `requested`, and up to 16 `placements`. (#642)
|
|
21
|
+
- `set_sequence_pixel_aspect_ratio` compares the read-back ratio as a number, so a host that formats it as `1`, `1:1`, or `1.42222` no longer fails a correct update. A real mismatch reports the value Premiere read back. (#642)
|
|
22
|
+
- `play_source_monitor` fails with "No clip is loaded in the Source Monitor" instead of reporting a playback request that does nothing, names the loaded clip, and rejects a `speed` that is zero, not finite, or outside -64 to 64 before building a script. (#642)
|
|
23
|
+
- `create_bars_and_tone` takes an optional `bin_id`. The bin is resolved before anything is created, the new item is moved there, and its location is read back; the result is `committed_unverified` when the move cannot be confirmed. (#642)
|
|
24
|
+
- `get_unused_media` and `get_duplicate_media` page their output with `offset`, `limit` (1-500, default 100), and a case-insensitive `contains` name filter, and return `total`, `returned`, `truncated`, and `nextOffset`. Large projects no longer return unbounded lists. (#642)
|
|
25
|
+
- `get_duplicate_media` no longer groups different After Effects compositions imported from the same `.aep` or `.aepx` as duplicates. The same composition imported twice is still reported. (#642)
|
|
26
|
+
- `get_project_panel_metadata` caps the returned XML at `max_chars` (256-200000, default 20000) and reports `truncated` and `totalChars`. Truncated XML is marked as unsafe to pass back to `set_project_panel_metadata`. (#642)
|
|
27
|
+
|
|
28
|
+
### Verification scope
|
|
29
|
+
|
|
30
|
+
- Automated checks validate package behavior. These changes follow live reports from Premiere 26.5.1 but have not been re-verified on a licensed Premiere host. The `unnest_sequence` and `replace_clip` overwrite path (`Track.overwriteClip` with source In/Out marks) has only been exercised against test fakes; its readback reports any host difference as a failure.
|
|
31
|
+
|
|
32
|
+
## [1.18.4] - 2026-09-28
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- `import_transcript_uxp` refuses to import over an existing transcript (`UXP_TRANSCRIPT_OVERWRITE_REFUSED`). On Premiere 26.5.1 a failed import cleared an existing transcript that could not be restored. (#642)
|
|
37
|
+
- UXP commands larger than the panel's 64 KiB frame limit are refused by the server with `UXP_COMMAND_TOO_LARGE` instead of timing out after 30 seconds. (#642)
|
|
38
|
+
- `manage_metadata_uxp` update and field update fail with `UXP_METADATA_NOT_APPLIED` when Premiere commits the transaction but the metadata reads back unchanged, instead of reporting `updated: true`. (#642)
|
|
39
|
+
- UXP project Save As, create, and branch copies ask for `confirmOverwrite` only when the destination file exists. Premiere 26.5.1's `Project.isProject` reports true for any `.prproj` path. (#642)
|
|
40
|
+
- CEP tools that act on a timeline clip through the QE DOM no longer pass the DOM clip index to `qeTrack.getItemAt()`. QE counts gaps as track items, so on a track with a leading or intermediate gap these tools renamed, re-effected, or retimed the wrong clip. `rename_clip`, `remove_all_effects`, `set_frame_blend`, `set_time_interpolation`, `apply_effect`, `apply_audio_effect`, `color_correct`, `apply_lut`, `stabilize_clip`, `copy_effects_between_clips`, `batch_rename_clips`, and `get_qe_clip_info` now match the QE clip by timeline start and fail before any change when no clip matches. `copy_effects_between_clips` reports each effect as verified, committed_unverified, or failed from a readback of the target's components instead of silently dropping errors. `apply_effect` and `apply_audio_effect` also no longer emit a malformed error string that stopped the generated script from parsing. (#642)
|
|
41
|
+
|
|
42
|
+
### Verification scope
|
|
43
|
+
|
|
44
|
+
- Automated checks validate package behavior. These changes follow live reports from Premiere 26.5.1 but have not been re-verified on a licensed Premiere host. The QE clip lookup change has not been verified on a licensed Premiere host either.
|
|
45
|
+
|
|
9
46
|
## [1.18.3] - 2026-09-28
|
|
10
47
|
|
|
11
48
|
### Security
|
package/README.md
CHANGED
|
@@ -110,9 +110,9 @@ The current source exposes 384 core tools for supported workflow steps spanning
|
|
|
110
110
|
|
|
111
111
|
<a id="latest-release"></a>
|
|
112
112
|
|
|
113
|
-
### Latest release: 1.18.
|
|
113
|
+
### Latest release: 1.18.4
|
|
114
114
|
|
|
115
|
-
The published v1.18.
|
|
115
|
+
The published v1.18.4 npm artifact contains **384 core tools**, 382 in its default profile,
|
|
116
116
|
and 478 with a compatible UXP connection. The development catalog above can include
|
|
117
117
|
unreleased work. See the [versioned facts and package provenance](https://premiere-pro-mcp.com/facts/).
|
|
118
118
|
|
|
@@ -162,7 +162,7 @@ if the connection is unavailable.
|
|
|
162
162
|
local Premiere processes. See the generated [supported action catalog](docs/supported-actions.md)
|
|
163
163
|
for individual capability and verification contracts.
|
|
164
164
|
|
|
165
|
-
See the [v1.18.
|
|
165
|
+
See the [v1.18.4 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.18.4)
|
|
166
166
|
for complete details. Live installation in Premiere Pro still requires host verification.
|
|
167
167
|
|
|
168
168
|
### Current MCP protocol support
|
|
@@ -210,7 +210,7 @@ their bins, media rules, and organization rules before a facility uses one.
|
|
|
210
210
|
> ### Install the published package (verify the name)
|
|
211
211
|
>
|
|
212
212
|
> ```bash
|
|
213
|
-
> npm i -g premiere-pro-mcp@1.18.
|
|
213
|
+
> npm i -g premiere-pro-mcp@1.18.4
|
|
214
214
|
> ```
|
|
215
215
|
>
|
|
216
216
|
> This repository publishes only **`premiere-pro-mcp`**. A differently named package (`adobe-premiere-pro-mcp`) may also declare a `premiere-pro-mcp` executable. Before configuring a client, confirm:
|
|
@@ -218,7 +218,7 @@ their bins, media rules, and organization rules before a facility uses one.
|
|
|
218
218
|
> | Check | Expected |
|
|
219
219
|
> | --- | --- |
|
|
220
220
|
> | Package name | `premiere-pro-mcp` (not `adobe-premiere-pro-mcp`) |
|
|
221
|
-
> | Version | `1.18.
|
|
221
|
+
> | Version | `1.18.4` |
|
|
222
222
|
> | Homepage / repo | https://premiere-pro-mcp.com/ · https://github.com/leancoderkavy/premiere-pro-mcp |
|
|
223
223
|
>
|
|
224
224
|
> ```bash
|
|
@@ -231,9 +231,9 @@ their bins, media rules, and organization rules before a facility uses one.
|
|
|
231
231
|
|
|
232
232
|
### Easiest supported path: Claude Desktop
|
|
233
233
|
|
|
234
|
-
1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.
|
|
234
|
+
1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.4/premiere-pro-mcp-1.18.4.mcpb).
|
|
235
235
|
2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
|
|
236
|
-
3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.
|
|
236
|
+
3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.4/MCPBridgeCEP.zxp). Open it with your trusted ZXP installer. If your computer has no ZXP installer, use the npm connector installer in **Advanced setup** below.
|
|
237
237
|
4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
|
|
238
238
|
5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
|
|
239
239
|
|
|
@@ -533,11 +533,11 @@ From a clone of this repository:
|
|
|
533
533
|
```bash
|
|
534
534
|
codex plugin marketplace add .
|
|
535
535
|
codex plugin add premiere-pro@premiere-pro-mcp
|
|
536
|
-
npx -y premiere-pro-mcp@1.18.
|
|
536
|
+
npx -y premiere-pro-mcp@1.18.4 --install-cep
|
|
537
537
|
```
|
|
538
538
|
|
|
539
539
|
Restart Premiere Pro and start a new Codex session after installation. The plugin
|
|
540
|
-
launches `premiere-pro-mcp@1.18.
|
|
540
|
+
launches `premiere-pro-mcp@1.18.4` through `npx`; the separate CEP installation is
|
|
541
541
|
required because the MCP server communicates with the running Premiere host through
|
|
542
542
|
the local bridge.
|
|
543
543
|
|
|
@@ -575,7 +575,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
|
|
|
575
575
|
Then install the Premiere bridge and start a new Claude Code session:
|
|
576
576
|
|
|
577
577
|
```bash
|
|
578
|
-
npx -y premiere-pro-mcp@1.18.
|
|
578
|
+
npx -y premiere-pro-mcp@1.18.4 --install-cep
|
|
579
579
|
```
|
|
580
580
|
|
|
581
581
|
The Claude Code package lives in
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.18.
|
|
2
|
+
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.18.5" ExtensionBundleName="MCP for Adobe After Effects">
|
|
3
3
|
<ExtensionList>
|
|
4
|
-
<Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.18.
|
|
4
|
+
<Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.18.5"/>
|
|
5
5
|
</ExtensionList>
|
|
6
6
|
<ExecutionEnvironment>
|
|
7
7
|
<HostList>
|
|
Binary file
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.18.
|
|
2
|
+
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.18.5" ExtensionBundleName="MCP for Adobe Premiere Pro">
|
|
3
3
|
<ExtensionList>
|
|
4
|
-
<Extension Id="com.mcp.premiere.bridge.panel" Version="1.18.
|
|
5
|
-
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.
|
|
4
|
+
<Extension Id="com.mcp.premiere.bridge.panel" Version="1.18.5"/>
|
|
5
|
+
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.5"/>
|
|
6
6
|
</ExtensionList>
|
|
7
7
|
<ExecutionEnvironment>
|
|
8
8
|
<HostList>
|
package/cep-plugin/index.html
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
<section class="update-section" aria-live="polite">
|
|
82
82
|
<div class="update-copy">
|
|
83
83
|
<span class="section-label">MCP updates</span>
|
|
84
|
-
<strong id="updateTitle">Version 1.18.
|
|
84
|
+
<strong id="updateTitle">Version 1.18.5</strong>
|
|
85
85
|
<span id="updateDetail">Checking the global MCP server and connector release…</span>
|
|
86
86
|
</div>
|
|
87
87
|
<button id="btnUpdate" class="button button-update" onclick="handleUpdateClick()" type="button" aria-describedby="updateDetail" disabled>
|
package/cep-plugin/updater.cjs
CHANGED
|
@@ -72,6 +72,70 @@ function __writeClipSpan(item, startTicks, endTicks) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// Tick comparisons for values that round-trip through seconds (ProjectItem
|
|
76
|
+
// marks take seconds). Far below one frame at any frame rate.
|
|
77
|
+
var __TICK_MATCH_TOL = 16;
|
|
78
|
+
|
|
79
|
+
function __isTrackLocked(track) {
|
|
80
|
+
try {
|
|
81
|
+
if (track && typeof track.isLocked === "function") return !!track.isLocked();
|
|
82
|
+
} catch (eLocked) {}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Place exactly [inTicks, outTicks) of a project item on ONE track at
|
|
87
|
+
// startTicks without rippling. Track.overwriteClip places the item's In/Out
|
|
88
|
+
// range, so set those marks first and restore the item's own marks afterwards.
|
|
89
|
+
// mediaType: 1 = video, 2 = audio. Callers must prove the destination range is
|
|
90
|
+
// empty first; overwrite never shifts neighbours but would replace them.
|
|
91
|
+
// Returns { ok, attempted, marksRestored, error }. attempted=true means the
|
|
92
|
+
// overwrite call ran (or threw), so the timeline may have changed.
|
|
93
|
+
function __overwriteRangeOnTrack(track, item, startTicks, inTicks, outTicks, mediaType) {
|
|
94
|
+
if (!track || typeof track.overwriteClip !== "function") {
|
|
95
|
+
return { ok: false, attempted: false, marksRestored: true, error: "Track.overwriteClip is unavailable on this Premiere build" };
|
|
96
|
+
}
|
|
97
|
+
if (!item || typeof item.getInPoint !== "function" || typeof item.getOutPoint !== "function" ||
|
|
98
|
+
typeof item.setInPoint !== "function" || typeof item.setOutPoint !== "function") {
|
|
99
|
+
return { ok: false, attempted: false, marksRestored: true, error: "Project item In/Out marks cannot be set on this Premiere build" };
|
|
100
|
+
}
|
|
101
|
+
var originalIn = null;
|
|
102
|
+
var originalOut = null;
|
|
103
|
+
try {
|
|
104
|
+
originalIn = String(item.getInPoint(mediaType).ticks);
|
|
105
|
+
originalOut = String(item.getOutPoint(mediaType).ticks);
|
|
106
|
+
} catch (eMarks) {
|
|
107
|
+
return { ok: false, attempted: false, marksRestored: true, error: "Could not read the project item's In/Out marks" };
|
|
108
|
+
}
|
|
109
|
+
function setMarks(wantIn, wantOut) {
|
|
110
|
+
var currentOut = parseFloat(item.getOutPoint(mediaType).ticks);
|
|
111
|
+
if (parseFloat(wantIn) >= currentOut) {
|
|
112
|
+
item.setOutPoint(__ticksToSeconds(wantOut), mediaType);
|
|
113
|
+
item.setInPoint(__ticksToSeconds(wantIn), mediaType);
|
|
114
|
+
} else {
|
|
115
|
+
item.setInPoint(__ticksToSeconds(wantIn), mediaType);
|
|
116
|
+
item.setOutPoint(__ticksToSeconds(wantOut), mediaType);
|
|
117
|
+
}
|
|
118
|
+
return Math.abs(parseFloat(item.getInPoint(mediaType).ticks) - parseFloat(wantIn)) <= __TICK_MATCH_TOL &&
|
|
119
|
+
Math.abs(parseFloat(item.getOutPoint(mediaType).ticks) - parseFloat(wantOut)) <= __TICK_MATCH_TOL;
|
|
120
|
+
}
|
|
121
|
+
function restore() {
|
|
122
|
+
try { return setMarks(originalIn, originalOut); } catch (eRestore) { return false; }
|
|
123
|
+
}
|
|
124
|
+
var applied = false;
|
|
125
|
+
try { applied = setMarks(inTicks, outTicks); } catch (eSet) { applied = false; }
|
|
126
|
+
if (!applied) {
|
|
127
|
+
return { ok: false, attempted: false, marksRestored: restore(), error: "Premiere did not apply the source In/Out range to project item " + item.name };
|
|
128
|
+
}
|
|
129
|
+
var at = new Time();
|
|
130
|
+
at.ticks = String(startTicks);
|
|
131
|
+
try {
|
|
132
|
+
track.overwriteClip(item, at);
|
|
133
|
+
} catch (eOverwrite) {
|
|
134
|
+
return { ok: false, attempted: true, marksRestored: restore(), error: "Track.overwriteClip rejected project item " + item.name + ": " + eOverwrite.toString() };
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, attempted: true, marksRestored: restore(), error: "" };
|
|
137
|
+
}
|
|
138
|
+
|
|
75
139
|
function __ticksToTimecode(ticks, fps) {
|
|
76
140
|
var totalSeconds = __ticksToSeconds(ticks);
|
|
77
141
|
var hours = Math.floor(totalSeconds / 3600);
|
|
@@ -5,6 +5,8 @@ import { WebSocket, WebSocketServer } from "ws";
|
|
|
5
5
|
import { assertNoSymlinkedPaths, SymlinkPathError } from "../security/path-guard.js";
|
|
6
6
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
7
7
|
const SUPPORTED_PROTOCOLS = new Set([1, 2]);
|
|
8
|
+
/** Must match MAX_COMMAND_BYTES in uxp-plugin/protocol.cjs. */
|
|
9
|
+
export const MAX_UXP_COMMAND_BYTES = 64 * 1024;
|
|
8
10
|
export class UxpBridgeError extends Error {
|
|
9
11
|
code;
|
|
10
12
|
constructor(code, message) {
|
|
@@ -142,19 +144,25 @@ export class UxpWebSocketBridge extends EventEmitter {
|
|
|
142
144
|
}
|
|
143
145
|
const requestTimeoutMs = Math.max(this.options.requestTimeoutMs, minimumTimeoutMs);
|
|
144
146
|
const requestId = randomUUID();
|
|
147
|
+
const frame = JSON.stringify({
|
|
148
|
+
protocolVersion: hello.protocolVersion,
|
|
149
|
+
type: "command",
|
|
150
|
+
requestId,
|
|
151
|
+
command,
|
|
152
|
+
args,
|
|
153
|
+
});
|
|
154
|
+
// The panel drops frames over 64 KiB before it can read the requestId, so an
|
|
155
|
+
// oversized command would only surface as a timeout. Refuse it here instead.
|
|
156
|
+
if (Buffer.byteLength(frame, "utf8") > MAX_UXP_COMMAND_BYTES) {
|
|
157
|
+
throw new UxpBridgeError("UXP_COMMAND_TOO_LARGE", `UXP command '${command}' is larger than the panel's 64 KiB limit. Send less data (for example a narrower metadata update). No command was sent to Premiere.`);
|
|
158
|
+
}
|
|
145
159
|
return new Promise((resolve, reject) => {
|
|
146
160
|
const timer = setTimeout(() => {
|
|
147
161
|
this.pending.delete(requestId);
|
|
148
162
|
reject(new UxpBridgeError("UXP_TIMEOUT", `UXP command '${command}' timed out`));
|
|
149
163
|
}, requestTimeoutMs);
|
|
150
164
|
this.pending.set(requestId, { command, resolve, reject, timer });
|
|
151
|
-
socket.send(
|
|
152
|
-
protocolVersion: hello.protocolVersion,
|
|
153
|
-
type: "command",
|
|
154
|
-
requestId,
|
|
155
|
-
command,
|
|
156
|
-
args,
|
|
157
|
-
}), (error) => {
|
|
165
|
+
socket.send(frame, (error) => {
|
|
158
166
|
if (!error)
|
|
159
167
|
return;
|
|
160
168
|
const pending = this.pending.get(requestId);
|
package/dist/tools/advanced.js
CHANGED
|
@@ -515,7 +515,9 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
515
515
|
},
|
|
516
516
|
},
|
|
517
517
|
move_clip_to_track: {
|
|
518
|
-
description: "Move a clip to a different track.
|
|
518
|
+
description: "Move a clip to a different track of the same type, keeping its start, duration, and source in/out. EXPERIMENTAL: uses the undocumented QE DOM moveToTrack. " +
|
|
519
|
+
"Refuses without changing anything when the origin or destination track is locked or the destination range is occupied. " +
|
|
520
|
+
"Reads the timeline back: verified only when the clip is on the destination track with the same span and source range and is gone from the origin track; committed_unverified when the source range cannot be read; otherwise failure with Undo guidance.",
|
|
519
521
|
parameters: {
|
|
520
522
|
type: "object",
|
|
521
523
|
properties: {
|
|
@@ -531,6 +533,9 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
531
533
|
required: ["node_id", "target_track_index"],
|
|
532
534
|
},
|
|
533
535
|
handler: async (args) => {
|
|
536
|
+
if (!Number.isSafeInteger(args.target_track_index) || args.target_track_index < 0) {
|
|
537
|
+
return { success: false, error: "target_track_index must be a non-negative integer" };
|
|
538
|
+
}
|
|
534
539
|
const nodeId = escapeForExtendScript(args.node_id);
|
|
535
540
|
const script = buildToolScript(`
|
|
536
541
|
app.enableQE();
|
|
@@ -549,6 +554,47 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
549
554
|
return __result({ moved: false, verified: true, alreadyOnTrack: true, clipName: result.clip.name, trackIndex: result.trackIndex });
|
|
550
555
|
}
|
|
551
556
|
|
|
557
|
+
// Preflight: refuse before any QE call when either track is locked or
|
|
558
|
+
// the destination range holds another clip (QE would overwrite it).
|
|
559
|
+
var originTrack = targetTracks[result.trackIndex];
|
|
560
|
+
var destTrack = targetTracks[${args.target_track_index}];
|
|
561
|
+
if (__isTrackLocked(originTrack)) {
|
|
562
|
+
return __error("Move refused; nothing was changed. Origin " + result.trackType + " track " + result.trackIndex + " is locked.");
|
|
563
|
+
}
|
|
564
|
+
if (__isTrackLocked(destTrack)) {
|
|
565
|
+
return __error("Move refused; nothing was changed. Destination " + result.trackType + " track ${args.target_track_index} is locked.");
|
|
566
|
+
}
|
|
567
|
+
var moveStart = parseFloat(result.clip.start.ticks);
|
|
568
|
+
var moveEnd = parseFloat(result.clip.end.ticks);
|
|
569
|
+
for (var oi = 0; oi < destTrack.clips.numItems; oi++) {
|
|
570
|
+
var occupant = destTrack.clips[oi];
|
|
571
|
+
var occupantStart = parseFloat(occupant.start.ticks);
|
|
572
|
+
var occupantEnd = parseFloat(occupant.end.ticks);
|
|
573
|
+
if (occupantStart < moveEnd - __TICK_MATCH_TOL && occupantEnd > moveStart + __TICK_MATCH_TOL) {
|
|
574
|
+
return __error("Move refused; nothing was changed. The destination range " + __ticksToSeconds(moveStart) + "s-" + __ticksToSeconds(moveEnd) + "s on " + result.trackType + " track ${args.target_track_index} is already occupied by " + occupant.name + ", and moving there would overwrite it.");
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
var movedName = result.clip.name;
|
|
578
|
+
var movedSourceId = "";
|
|
579
|
+
try { movedSourceId = result.clip.projectItem ? String(result.clip.projectItem.nodeId) : ""; } catch (eMovedSource) {}
|
|
580
|
+
|
|
581
|
+
// Premiere can mint a new node ID when a clip changes track, so fall
|
|
582
|
+
// back to matching the same source item, name, and start on a track.
|
|
583
|
+
function findMovedOn(trackIndex) {
|
|
584
|
+
var track = targetTracks[trackIndex];
|
|
585
|
+
for (var fi = 0; fi < track.clips.numItems; fi++) {
|
|
586
|
+
var cand = track.clips[fi];
|
|
587
|
+
if (String(cand.nodeId) === "${nodeId}") return { clip: cand, trackIndex: trackIndex, trackType: result.trackType };
|
|
588
|
+
var candSource = "";
|
|
589
|
+
try { candSource = cand.projectItem ? String(cand.projectItem.nodeId) : ""; } catch (eCandSource) {}
|
|
590
|
+
if (movedSourceId && candSource === movedSourceId && cand.name === movedName &&
|
|
591
|
+
Math.abs(parseFloat(cand.start.ticks) - moveStart) <= __TICK_MATCH_TOL) {
|
|
592
|
+
return { clip: cand, trackIndex: trackIndex, trackType: result.trackType };
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
552
598
|
var qeTrack = result.trackType === "video"
|
|
553
599
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
554
600
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
@@ -586,41 +632,64 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
586
632
|
|
|
587
633
|
// QE reports nothing useful on success, so confirm against the DOM.
|
|
588
634
|
var after = __findClip("${nodeId}");
|
|
589
|
-
if (!after)
|
|
635
|
+
if (!after) after = findMovedOn(${args.target_track_index});
|
|
636
|
+
if (!after) return __error("The timeline changed: clip " + movedName + " could not be found on its original or destination track after the track move. Use Undo and check the timeline.");
|
|
590
637
|
if (after.trackIndex !== ${args.target_track_index}) {
|
|
638
|
+
if (findMovedOn(${args.target_track_index})) {
|
|
639
|
+
return __error("The timeline changed: Premiere placed a copy of " + movedName + " on track ${args.target_track_index} but the original is still on track " + after.trackIndex + ". Use Undo and move the clip in the Premiere UI.");
|
|
640
|
+
}
|
|
591
641
|
return __error("Premiere accepted the moveToTrack call but the clip is still on track " + after.trackIndex + " rather than ${args.target_track_index}. Structural QE edits are known to no-op on some Premiere Pro 26.x installations (confirmed on 26.2.2).");
|
|
592
642
|
}
|
|
643
|
+
if (findMovedOn(result.trackIndex)) {
|
|
644
|
+
return __error("The timeline changed: " + movedName + " now exists on both track " + result.trackIndex + " and track ${args.target_track_index}. Use Undo and move the clip in the Premiere UI.");
|
|
645
|
+
}
|
|
593
646
|
// moveToTrack can rewrite end independently of start (#550). Re-assert
|
|
594
647
|
// the original span before verifying, then fail closed if it did not hold.
|
|
595
648
|
if (String(after.clip.start.ticks) !== beforeMoveStartTicks || String(after.clip.end.ticks) !== beforeMoveEndTicks) {
|
|
596
649
|
try {
|
|
597
650
|
__writeClipSpan(after.clip, beforeMoveStartTicks, beforeMoveEndTicks);
|
|
598
651
|
} catch (spanErr) {
|
|
599
|
-
return __error("Premiere changed the clip's timeline range during the track move and it could not be restored (" + spanErr.toString() + "). Use Undo and retry in the Premiere UI.");
|
|
652
|
+
return __error("The timeline changed: Premiere changed the clip's timeline range during the track move and it could not be restored (" + spanErr.toString() + "). Use Undo and retry in the Premiere UI.");
|
|
600
653
|
}
|
|
601
|
-
|
|
602
|
-
|
|
654
|
+
var restored = __findClip("${nodeId}");
|
|
655
|
+
after = restored || findMovedOn(${args.target_track_index});
|
|
656
|
+
if (!after) return __error("The timeline changed: clip " + movedName + " could not be found after restoring its timeline range. Use Undo and check the timeline.");
|
|
603
657
|
}
|
|
604
658
|
var afterMoveStartTicks = String(after.clip.start.ticks);
|
|
605
659
|
var afterMoveEndTicks = String(after.clip.end.ticks);
|
|
606
660
|
if (parseFloat(afterMoveStartTicks) >= parseFloat(afterMoveEndTicks)) {
|
|
607
|
-
return __error("Premiere left clip
|
|
661
|
+
return __error("The timeline changed: Premiere left clip " + movedName + " with an inverted or empty timeline range after the track move. Use Undo and retry in the Premiere UI.");
|
|
662
|
+
}
|
|
663
|
+
if (Math.abs(parseFloat(afterMoveStartTicks) - parseFloat(beforeMoveStartTicks)) > 1) {
|
|
664
|
+
return __error("The timeline changed: Premiere moved the clip to " + __ticksToSeconds(afterMoveStartTicks) + "s instead of keeping its start at " + __ticksToSeconds(beforeMoveStartTicks) + "s. Use Undo and retry in the Premiere UI.");
|
|
608
665
|
}
|
|
609
666
|
if (Math.abs((parseFloat(afterMoveEndTicks) - parseFloat(afterMoveStartTicks)) - spanTicks) > 1) {
|
|
610
|
-
return __error("Premiere changed the clip duration during the track move. Use Undo and retry in the Premiere UI.");
|
|
667
|
+
return __error("The timeline changed: Premiere changed the clip duration during the track move. Use Undo and retry in the Premiere UI.");
|
|
611
668
|
}
|
|
612
|
-
|
|
613
|
-
|
|
669
|
+
var sourceReadable = true;
|
|
670
|
+
var afterInTicks = "";
|
|
671
|
+
var afterOutTicks = "";
|
|
672
|
+
try {
|
|
673
|
+
afterInTicks = String(after.clip.inPoint.ticks);
|
|
674
|
+
afterOutTicks = String(after.clip.outPoint.ticks);
|
|
675
|
+
} catch (eSourceRead) {
|
|
676
|
+
sourceReadable = false;
|
|
677
|
+
}
|
|
678
|
+
if (sourceReadable && (afterInTicks !== beforeMoveInTicks || afterOutTicks !== beforeMoveOutTicks)) {
|
|
679
|
+
return __error("The timeline changed: Premiere changed the clip's source in/out points during the track move. Use Undo and retry in the Premiere UI.");
|
|
614
680
|
}
|
|
615
681
|
|
|
616
|
-
|
|
682
|
+
var moveResult = {
|
|
617
683
|
moved: true,
|
|
618
|
-
verified:
|
|
684
|
+
verified: sourceReadable,
|
|
685
|
+
outcome: sourceReadable ? "verified" : "committed_unverified",
|
|
619
686
|
clipName: after.clip.name,
|
|
620
687
|
newTrackIndex: after.trackIndex,
|
|
621
688
|
startSeconds: __ticksToSeconds(afterMoveStartTicks),
|
|
622
689
|
endSeconds: __ticksToSeconds(afterMoveEndTicks)
|
|
623
|
-
}
|
|
690
|
+
};
|
|
691
|
+
if (!sourceReadable) moveResult.warning = "The clip is on the destination track with the same start and duration, but its source in/out points could not be read back.";
|
|
692
|
+
return __result(moveResult);
|
|
624
693
|
`);
|
|
625
694
|
return sendCommand(script, bridgeOptions);
|
|
626
695
|
},
|
|
@@ -649,9 +718,11 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
649
718
|
var qeTrack = result.trackType === "video"
|
|
650
719
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
651
720
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
721
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
722
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
723
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
724
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
725
|
+
|
|
655
726
|
qeClip.removeEffects();
|
|
656
727
|
return __result({ removed: true, clipName: result.clip.name });
|
|
657
728
|
`);
|
|
@@ -738,9 +809,11 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
738
809
|
var qeTrack = result.trackType === "video"
|
|
739
810
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
740
811
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
812
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
813
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
814
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
815
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
816
|
+
|
|
744
817
|
qeClip.setFrameBlend(${args.enabled});
|
|
745
818
|
return __result({ frameBlend: ${args.enabled}, clipName: result.clip.name });
|
|
746
819
|
`);
|
|
@@ -775,9 +848,11 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
775
848
|
var qeTrack = result.trackType === "video"
|
|
776
849
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
777
850
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
851
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
852
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
853
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
854
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
855
|
+
|
|
781
856
|
qeClip.setTimeInterpolationType(${args.interpolation_type});
|
|
782
857
|
var typeNames = ["Frame Sampling", "Frame Blending", "Optical Flow"];
|
|
783
858
|
return __result({
|
|
@@ -817,9 +892,11 @@ export function getAdvancedTools(bridgeOptions) {
|
|
|
817
892
|
var qeTrack = result.trackType === "video"
|
|
818
893
|
? qeSeq.getVideoTrackAt(result.trackIndex)
|
|
819
894
|
: qeSeq.getAudioTrackAt(result.trackIndex);
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
895
|
+
if (!qeTrack) return __error("QE track not found; nothing was changed.");
|
|
896
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
897
|
+
var qeClip = __findQeClipByDomClip(qeTrack, result.clip);
|
|
898
|
+
if (!qeClip) return __error("Could not match the QE clip for " + result.clip.name + " by timeline start; nothing was changed.");
|
|
899
|
+
|
|
823
900
|
var oldName = result.clip.name;
|
|
824
901
|
qeClip.setName("${escapeForExtendScript(args.new_name)}");
|
|
825
902
|
return __result({ renamed: true, oldName: oldName, newName: "${escapeForExtendScript(args.new_name)}" });
|
package/dist/tools/clipboard.js
CHANGED
|
@@ -469,17 +469,39 @@ export function getClipboardTools(bridgeOptions) {
|
|
|
469
469
|
|
|
470
470
|
var src = srcResult.clip;
|
|
471
471
|
var tgt = tgtResult.clip;
|
|
472
|
-
var copied =
|
|
472
|
+
var copied = [];
|
|
473
|
+
var unverified = [];
|
|
474
|
+
var failed = [];
|
|
473
475
|
var effectFilter = ${args.effect_name ? `"${escapeForExtendScript(args.effect_name)}"` : "null"};
|
|
474
476
|
var intrinsic = ["Motion", "Opacity", "Time Remapping", "Volume", "Channel Volume", "Panner"];
|
|
475
477
|
|
|
476
478
|
// Use QE to copy effects by name
|
|
477
479
|
var qeSeq = qe.project.getActiveSequence();
|
|
480
|
+
if (!qeSeq) return __error("No active sequence (QE); nothing was changed.");
|
|
478
481
|
var tgtTrackType = tgtResult.trackType;
|
|
479
|
-
var tgtTrack = tgtTrackType === "video"
|
|
480
|
-
? qeSeq.getVideoTrackAt(tgtResult.trackIndex)
|
|
482
|
+
var tgtTrack = tgtTrackType === "video"
|
|
483
|
+
? qeSeq.getVideoTrackAt(tgtResult.trackIndex)
|
|
481
484
|
: qeSeq.getAudioTrackAt(tgtResult.trackIndex);
|
|
482
|
-
|
|
485
|
+
if (!tgtTrack) return __error("QE target track not found; nothing was changed.");
|
|
486
|
+
// QE track items include gaps, so the DOM clip index is not a QE index.
|
|
487
|
+
var qeTgtClip = __findQeClipByDomClip(tgtTrack, tgt);
|
|
488
|
+
if (!qeTgtClip) return __error("Could not match the QE clip for target " + tgt.name + " by timeline start; nothing was changed.");
|
|
489
|
+
|
|
490
|
+
function countTargetComponents(wanted) {
|
|
491
|
+
var n = 0;
|
|
492
|
+
for (var ci = 0; ci < tgt.components.numItems; ci++) {
|
|
493
|
+
if (tgt.components[ci].displayName === wanted) n++;
|
|
494
|
+
}
|
|
495
|
+
return n;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function listNames(entries) {
|
|
499
|
+
var out = [];
|
|
500
|
+
for (var li = 0; li < entries.length; li++) {
|
|
501
|
+
out.push(typeof entries[li] === "string" ? entries[li] : entries[li].effect + " (" + entries[li].reason + ")");
|
|
502
|
+
}
|
|
503
|
+
return out.join(", ");
|
|
504
|
+
}
|
|
483
505
|
|
|
484
506
|
for (var i = 0; i < src.components.numItems; i++) {
|
|
485
507
|
var comp = src.components[i];
|
|
@@ -494,23 +516,61 @@ export function getClipboardTools(bridgeOptions) {
|
|
|
494
516
|
if (skip) continue;
|
|
495
517
|
}
|
|
496
518
|
|
|
497
|
-
// Apply effect via QE
|
|
519
|
+
// Apply effect via QE, then read the target component list back.
|
|
520
|
+
var beforeCount = null;
|
|
521
|
+
try { beforeCount = countTargetComponents(name); } catch (beforeErr) {}
|
|
522
|
+
var qeEffect = null;
|
|
498
523
|
try {
|
|
499
|
-
|
|
524
|
+
qeEffect = tgtTrackType === "video"
|
|
500
525
|
? qe.project.getVideoEffectByName(name)
|
|
501
526
|
: qe.project.getAudioEffectByName(name);
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
527
|
+
} catch (lookupErr) {
|
|
528
|
+
failed.push({ effect: name, reason: "QE effect lookup failed: " + lookupErr.toString() });
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (!qeEffect) {
|
|
532
|
+
failed.push({ effect: name, reason: "QE did not resolve a " + tgtTrackType + " effect with this name" });
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
if (tgtTrackType === "video") {
|
|
537
|
+
qeTgtClip.addVideoEffect(qeEffect);
|
|
538
|
+
} else {
|
|
539
|
+
qeTgtClip.addAudioEffect(qeEffect);
|
|
509
540
|
}
|
|
510
|
-
} catch(
|
|
541
|
+
} catch (addErr) {
|
|
542
|
+
failed.push({ effect: name, reason: "Premiere rejected the effect: " + addErr.toString() });
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
var afterCount = null;
|
|
546
|
+
try { afterCount = countTargetComponents(name); } catch (afterErr) {}
|
|
547
|
+
if (beforeCount !== null && afterCount !== null && afterCount > beforeCount) {
|
|
548
|
+
copied.push(name);
|
|
549
|
+
} else {
|
|
550
|
+
unverified.push({ effect: name, reason: "Premiere accepted the call but the target component list did not show a new " + name + " component" });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (failed.length > 0) {
|
|
555
|
+
var partial = copied.length + unverified.length > 0;
|
|
556
|
+
return __error((partial ? "copy_effects_between_clips was only partially applied" : "No effects were copied") +
|
|
557
|
+
" to " + tgt.name + ". Verified: [" + listNames(copied) + "]; committed_unverified: [" + listNames(unverified) +
|
|
558
|
+
"]; failed: [" + listNames(failed) + "]. Check Effect Controls before retrying.");
|
|
511
559
|
}
|
|
512
560
|
|
|
513
|
-
|
|
561
|
+
var status = copied.length + unverified.length === 0
|
|
562
|
+
? "unchanged"
|
|
563
|
+
: (unverified.length > 0 ? "committed_unverified" : "verified");
|
|
564
|
+
return __result({
|
|
565
|
+
status: status,
|
|
566
|
+
verified: status === "verified",
|
|
567
|
+
copiedEffects: copied.length,
|
|
568
|
+
copied: copied,
|
|
569
|
+
committedUnverified: unverified,
|
|
570
|
+
failed: failed,
|
|
571
|
+
source: src.name,
|
|
572
|
+
target: tgt.name
|
|
573
|
+
});
|
|
514
574
|
`);
|
|
515
575
|
return sendCommand(script, bridgeOptions);
|
|
516
576
|
},
|