premiere-pro-mcp 1.23.0 → 1.24.0

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 CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.24.0] - 2026-10-11
10
+
11
+ ### Added
12
+
13
+ - `get_caption_style_guidance` accepts an optional, bounded `custom_style` (font family/weight, font size 8-400 px, `#RRGGBB` fill, up to three strokes with width and outer/inner/center placement, drop shadow, one of nine zones, X/Y offsets, Track Style name) and returns that spec with numbered Premiere UI steps carrying the exact values. Each extra stroke is added with + next to Stroke, and the look is saved via Track Style > + Create style. Preset steps use the same workflow. Results add `track_style` (whole-track scope, reuse from the Track Style dropdown in other sequences, project item in the Project panel), `limitations`, `warnings` (for example, layered outer strokes that do not grow wider) and `applied: false`. It is still guidance only: Premiere exposes no caption style API, so nothing is applied. Verify the look with `export_frame` or `export_sequence_review_frames`. Calls that pass only `preset` keep their `style` fields.
14
+ - The platform capability report lists `captionTrackStyle` under the CEP `unsupported` map, so clients see that caption track styling cannot be automated and are pointed to `get_caption_style_guidance`.
15
+
16
+ ### Fixed
17
+
18
+ - `__setItemMarks` now writes the same quarter-grid-biased seconds when applying a temporary overwrite range. Premiere 26.5.2 can floor exact tick-boundary seconds one media frame early, so `unnest_sequence` (16-tick default tolerance) refused a valid nest, or `replace_clip` placed a clip one frame short after the original was already removed. `replace_clip` also pulls a placed end that overshoots the original span by less than one media frame back onto that span.
19
+
20
+ - `__itemMarksForRestore` now applies that same write bias when it returns DOM marks (no private metadata, or DOM already matching the private range). Those paths previously handed exact tick-boundary seconds to restore, so a normal clip could shrink one media frame after replace/unnest on 26.5.2.
21
+
9
22
  ## [1.23.0] - 2026-10-11
10
23
 
11
24
  ### Added
@@ -25,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
25
38
 
26
39
  - `unnest_sequence` now refuses, before removing the nested clip, any inner item that also carries the other media type. Premiere 26.5.2's `Track.overwriteClip` places that media on the matching track and overwrites the clips there. Same guard as `replace_clip`. Unnest a nest of video-only or audio-only items, or use Premiere's Unnest command.
27
40
  - Shared project-item mark restore after `replace_clip`, `unnest_sequence`, and other `Track.overwriteClip` helpers now writes the quarter-frame-biased seconds from `__itemMarksForRestore`. Writing the exact tick boundary as seconds can floor one media frame early on Premiere 26.5.2 (a soft-subclip Out of `00:00:29:22` stored as `00:00:29:21`), shrinking the source item. `duplicate_clip` and `set_item_in_out` already used those seconds.
41
+
28
42
  - `export_sequence` without `preset_path` no longer exports at proxy resolution when Media Encoder is not installed. Preset discovery only searched Premiere's `Settings/IngestPresets`, so the default became `IngestPresets/Proxy/00_1024x540 H.264.epr` and a 1080x1920 sequence was exported at 1024x540 while reporting `verified: true` (Premiere 24.0, macOS). Preset discovery now also searches Premiere's own `MediaIO/systempresets` when no Media Encoder presets exist, and the default never comes from `IngestPresets`; if no other H.264 preset is found, the tool asks for `preset_path`.
29
43
 
30
44
  ### Compatibility and evidence
package/README.md CHANGED
@@ -129,13 +129,13 @@ The current source exposes 434 core tools for supported workflow steps spanning
129
129
 
130
130
  <a id="latest-release"></a>
131
131
 
132
- ### Latest release: 1.22.0
132
+ ### Latest release: 1.24.0
133
133
 
134
- The published v1.22.0 npm artifact contains **393 core tools**, 391 in its default profile,
135
- and 487 with a compatible UXP connection. The development catalog above can include
134
+ The published v1.24.0 npm artifact contains **434 core tools**, 432 in its default profile,
135
+ and 528 with a compatible UXP connection. The development catalog above can include
136
136
  unreleased work. See the [versioned facts and package provenance](https://premiere-pro-mcp.com/facts/).
137
137
 
138
- GitHub releases use `v<SemVer>` for both title and tag, such as `v1.22.0`.
138
+ GitHub releases use `v<SemVer>` for both title and tag, such as `v1.24.0`.
139
139
  See [release naming and version conventions](docs/release-conventions.md).
140
140
 
141
141
  ### Try a bounded workflow
@@ -154,6 +154,12 @@ if the connection is unavailable.
154
154
 
155
155
  ### Release highlights
156
156
 
157
+ - **1.24.0 mark-write fixes:** temporary overwrite marks and restored project-item marks are written a quarter media frame past the tick boundary, so Premiere 26.5.2 no longer floors them one frame early in `unnest_sequence`, `replace_clip` and `duplicate_clip`.
158
+ - **1.24.0 caption style guidance:** `get_caption_style_guidance` accepts a bounded `custom_style` and returns Properties panel and Track Style steps; it is guidance only and applies nothing.
159
+ - **1.23.0 bounded workflows:** 41 added core tools and five guided routes cover exact timeline planning, editorial review, interchange/turnover and guarded delivery. The [52-item implementation ledger](docs/filmcraft-improvements.md) records each supported subset and host limit.
160
+ - **1.23.0 execution and recovery:** cooperating processes serialize protected edits, retain recovery for uncertain outcomes and report cancellation/partial work explicitly. UXP writes require the matching updated panel; older unversioned panels remain read-only.
161
+ - **1.23.0 artifact evidence:** review and delivery checks pin source checksums, inspect actual decoded frames/audio and preserve unknown native-job completion. Local decoder checks do not establish Premiere playback or native round-trip compatibility.
162
+ - **1.23.0 import fixes:** folder imports verify actual project-item readback; caption imports validate formats and counts and preserve mutation readback. Export preset discovery avoids proxy ingest presets, and unnesting refuses mixed-media overwrite hazards.
157
163
  - **1.22.0 readback receipts:** effect writes, stabilization, smart bins, proxies, folder imports, sequence creation, marker deletion and clip selection report what Premiere applied as `verified`, `committed_unverified` or `failed`; `create_subclip`, `add_title` and `update_marker` report the stored values; `set_sequence_frame_rate` lists clips Premiere re-snapped.
158
164
  - **1.22.0 display codes and proxies:** `set_sequence_display_format` writes Premiere's real display codes (video 100-113, audio 200-201) and maps the older 0-11 and 0-1 inputs onto them; `manage_proxies` toggle can set a known proxy display state; the Windows ACL check waits up to 30 seconds for PowerShell.
159
165
  - **1.21.0 Claude Desktop startup fix:** the extension starts when optional settings are blank, and Desktop's discover-only probe no longer holds the UXP port, so the real server keeps its UXP tools.
@@ -195,7 +201,7 @@ if the connection is unavailable.
195
201
  local Premiere processes. See the generated [supported action catalog](docs/supported-actions.md)
196
202
  for individual capability and verification contracts.
197
203
 
198
- See the [v1.22.0 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.22.0)
204
+ See the [v1.24.0 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.24.0)
199
205
  for complete details. Live installation in Premiere Pro still requires host verification.
200
206
 
201
207
  ### Current MCP protocol support
@@ -253,7 +259,7 @@ their bins, media rules, and organization rules before a facility uses one.
253
259
  > ### Install the published package (verify the name)
254
260
  >
255
261
  > ```bash
256
- > npm i -g premiere-pro-mcp@1.22.0
262
+ > npm i -g premiere-pro-mcp@1.24.0
257
263
  > ```
258
264
  >
259
265
  > 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:
@@ -261,7 +267,7 @@ their bins, media rules, and organization rules before a facility uses one.
261
267
  > | Check | Expected |
262
268
  > | --- | --- |
263
269
  > | Package name | `premiere-pro-mcp` (not `adobe-premiere-pro-mcp`) |
264
- > | Version | `1.22.0` |
270
+ > | Version | `1.24.0` |
265
271
  > | Homepage / repo | https://premiere-pro-mcp.com/ · https://github.com/leancoderkavy/premiere-pro-mcp |
266
272
  >
267
273
  > ```bash
@@ -274,9 +280,9 @@ their bins, media rules, and organization rules before a facility uses one.
274
280
 
275
281
  ### Easiest supported path: Claude Desktop
276
282
 
277
- 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.22.0/premiere-pro-mcp-1.22.0.mcpb).
283
+ 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.24.0/premiere-pro-mcp-1.24.0.mcpb).
278
284
  2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
279
- 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.22.0/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.
285
+ 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.24.0/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.
280
286
  4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
281
287
  5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
282
288
 
@@ -580,11 +586,11 @@ From a clone of this repository:
580
586
  ```bash
581
587
  codex plugin marketplace add .
582
588
  codex plugin add premiere-pro@premiere-pro-mcp
583
- npx -y premiere-pro-mcp@1.22.0 --install-cep
589
+ npx -y premiere-pro-mcp@1.24.0 --install-cep
584
590
  ```
585
591
 
586
592
  Restart Premiere Pro and start a new Codex session after installation. The plugin
587
- launches `premiere-pro-mcp@1.22.0` through `npx`; the separate CEP installation is
593
+ launches `premiere-pro-mcp@1.24.0` through `npx`; the separate CEP installation is
588
594
  required because the MCP server communicates with the running Premiere host through
589
595
  the local bridge.
590
596
 
@@ -622,7 +628,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
622
628
  Then install the Premiere bridge and start a new Claude Code session:
623
629
 
624
630
  ```bash
625
- npx -y premiere-pro-mcp@1.22.0 --install-cep
631
+ npx -y premiere-pro-mcp@1.24.0 --install-cep
626
632
  ```
627
633
 
628
634
  The Claude Code package lives in
@@ -1,8 +1,8 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.23.0" ExtensionBundleName="MCP for Adobe After Effects">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.24.0" ExtensionBundleName="MCP for Adobe After Effects">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.23.0"/>
5
- <Extension Id="com.mcp.aftereffects.bridge.headless" Version="1.23.0"/>
4
+ <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.24.0"/>
5
+ <Extension Id="com.mcp.aftereffects.bridge.headless" Version="1.24.0"/>
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <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.23.0" ExtensionBundleName="MCP for Adobe Premiere Pro">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.24.0" ExtensionBundleName="MCP for Adobe Premiere Pro">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.premiere.bridge.panel" Version="1.23.0"/>
5
- <Extension Id="com.mcp.premiere.bridge.headless" Version="1.23.0"/>
4
+ <Extension Id="com.mcp.premiere.bridge.panel" Version="1.24.0"/>
5
+ <Extension Id="com.mcp.premiere.bridge.headless" Version="1.24.0"/>
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <HostList>
@@ -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.23.0</strong>
84
+ <strong id="updateTitle">Version 1.24.0</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>
@@ -7,7 +7,7 @@
7
7
  })(this, function () {
8
8
  "use strict";
9
9
 
10
- var CURRENT_VERSION = "1.23.0";
10
+ var CURRENT_VERSION = "1.24.0";
11
11
  var PACKAGE_NAME = "premiere-pro-mcp";
12
12
  var LATEST_PACKAGE_API = "https://registry.npmjs.org/" + PACKAGE_NAME;
13
13
  var LATEST_RELEASE_API =
@@ -103,6 +103,101 @@ type PlatformProfile = {
103
103
  /** Approximate platform UI occlusion zones as normalized rects (x, y top-left). Verify against current platform overlay guides. */
104
104
  export declare const PLATFORM_SAFE_ZONES: Record<SafeZonePlatform, PlatformProfile>;
105
105
  export declare function describeCaptionStyle(preset: CaptionStylePreset): CaptionStyleDescriptor;
106
+ export declare const CAPTION_ZONES: readonly ["top_left", "top_center", "top_right", "middle_left", "middle_center", "middle_right", "bottom_left", "bottom_center", "bottom_right"];
107
+ export type CaptionZone = (typeof CAPTION_ZONES)[number];
108
+ export declare const CAPTION_STROKE_PLACEMENTS: readonly ["outer", "inner", "center"];
109
+ export type CaptionStrokePlacement = (typeof CAPTION_STROKE_PLACEMENTS)[number];
110
+ export declare const MAX_CAPTION_STROKES = 3;
111
+ export declare const CUSTOM_CAPTION_STYLE_LIMITS: {
112
+ readonly font_family_max_length: 128;
113
+ readonly font_weight_max_length: 64;
114
+ readonly track_style_name_max_length: 80;
115
+ readonly font_size_px: {
116
+ readonly min: 8;
117
+ readonly max: 400;
118
+ };
119
+ readonly stroke_width_px: {
120
+ readonly min: 0;
121
+ readonly max: 100;
122
+ };
123
+ readonly offset_px: {
124
+ readonly min: -2000;
125
+ readonly max: 2000;
126
+ };
127
+ readonly shadow: {
128
+ readonly opacity: {
129
+ readonly min: 0;
130
+ readonly max: 100;
131
+ };
132
+ readonly angle: {
133
+ readonly min: 0;
134
+ readonly max: 360;
135
+ };
136
+ readonly distance: {
137
+ readonly min: 0;
138
+ readonly max: 200;
139
+ };
140
+ readonly size: {
141
+ readonly min: 0;
142
+ readonly max: 100;
143
+ };
144
+ readonly blur: {
145
+ readonly min: 0;
146
+ readonly max: 100;
147
+ };
148
+ };
149
+ };
150
+ export declare const CAPTION_SHADOW_DEFAULTS: {
151
+ readonly color: "#000000";
152
+ readonly opacity: 75;
153
+ readonly angle: 135;
154
+ readonly distance: 7;
155
+ readonly size: 0;
156
+ readonly blur: 40;
157
+ };
158
+ export declare const DEFAULT_TRACK_STYLE_NAME = "Caption Track Style";
159
+ export type CaptionStroke = {
160
+ color: string;
161
+ width_px: number;
162
+ placement: CaptionStrokePlacement;
163
+ };
164
+ export type CaptionShadow = {
165
+ color: string;
166
+ opacity: number;
167
+ angle: number;
168
+ distance: number;
169
+ size: number;
170
+ blur: number;
171
+ };
172
+ export type CustomCaptionStyle = {
173
+ font_family?: string;
174
+ font_weight?: string;
175
+ font_size_px?: number;
176
+ fill_color?: string;
177
+ strokes?: CaptionStroke[];
178
+ shadow?: CaptionShadow;
179
+ zone?: CaptionZone;
180
+ offset_x_px?: number;
181
+ offset_y_px?: number;
182
+ track_style_name?: string;
183
+ };
184
+ export declare function validateCustomCaptionStyle(value: unknown): CustomCaptionStyle;
185
+ /** Warnings about a custom style that Premiere will accept but that may not render as intended. */
186
+ export declare function customCaptionStyleWarnings(style: CustomCaptionStyle): string[];
187
+ /** Premiere 2024+ Properties-panel / Track Style steps for a custom caption style. */
188
+ export declare function buildCustomCaptionStyleSteps(style: CustomCaptionStyle, preset?: CaptionStylePreset): string[];
189
+ /** Premiere 2024+ Properties-panel / Track Style steps for a built-in preset. */
190
+ export declare function buildPresetCaptionStyleSteps(style: CaptionStyleDescriptor): string[];
191
+ export declare const CAPTION_TRACK_STYLE_GUIDANCE: {
192
+ readonly scope: "A Track Style applies to every caption on the caption track. Style one caption, save it with Track Style > + Create style, and the whole track updates; no multi-select is needed.";
193
+ readonly reusable: "A saved Track Style can be applied to the caption track of any other sequence in the project from the Track Style dropdown.";
194
+ readonly project_item: "Each Track Style is stored as a project item in the Project panel; keep it in a shared bin so it can be found and reused.";
195
+ readonly limitations: readonly ["Premiere Pro exposes no documented ExtendScript, QE DOM, or UXP API that creates, edits, or applies caption styles, so these steps must be performed in the Premiere UI.", "These steps target Premiere Pro 2024 and later. Earlier versions lack the Track Style workflow described here and were not checked; panel labels can also differ between versions and UI languages.", "Verify the rendered result with export_frame or export_sequence_review_frames; this tool cannot read the applied style back."];
196
+ };
197
+ export declare function captionSafeZoneRecommendation(): Record<SafeZonePlatform, {
198
+ x: number;
199
+ y: number;
200
+ }>;
106
201
  export declare function formatCaptionTimestamp(ms: number, format: CaptionFormat): string;
107
202
  /** Greedy word wrap that never splits a word; a single word longer than the width occupies its own line. */
108
203
  export declare function wrapCaptionWords(words: readonly string[], maxChars: number): string[][];
@@ -154,6 +154,219 @@ export function describeCaptionStyle(preset) {
154
154
  note: "Documentation only. SRT/VTT artifacts carry timing and text; apply visual styling in Premiere's caption track style after import.",
155
155
  };
156
156
  }
157
+ // ---------------------------------------------------------------------------
158
+ // Custom caption track style (documentation only)
159
+ // ---------------------------------------------------------------------------
160
+ export const CAPTION_ZONES = ["top_left", "top_center", "top_right", "middle_left", "middle_center", "middle_right", "bottom_left", "bottom_center", "bottom_right"];
161
+ export const CAPTION_STROKE_PLACEMENTS = ["outer", "inner", "center"];
162
+ export const MAX_CAPTION_STROKES = 3;
163
+ export const CUSTOM_CAPTION_STYLE_LIMITS = {
164
+ font_family_max_length: 128,
165
+ font_weight_max_length: 64,
166
+ track_style_name_max_length: 80,
167
+ font_size_px: { min: 8, max: 400 },
168
+ stroke_width_px: { min: 0, max: 100 },
169
+ offset_px: { min: -2000, max: 2000 },
170
+ shadow: {
171
+ opacity: { min: 0, max: 100 },
172
+ angle: { min: 0, max: 360 },
173
+ distance: { min: 0, max: 200 },
174
+ size: { min: 0, max: 100 },
175
+ blur: { min: 0, max: 100 },
176
+ },
177
+ };
178
+ export const CAPTION_SHADOW_DEFAULTS = { color: "#000000", opacity: 75, angle: 135, distance: 7, size: 0, blur: 40 };
179
+ export const DEFAULT_TRACK_STYLE_NAME = "Caption Track Style";
180
+ const CUSTOM_STYLE_KEYS = ["font_family", "font_weight", "font_size_px", "fill_color", "strokes", "shadow", "zone", "offset_x_px", "offset_y_px", "track_style_name"];
181
+ const STROKE_KEYS = ["color", "width_px", "placement"];
182
+ const SHADOW_KEYS = ["color", "opacity", "angle", "distance", "size", "blur"];
183
+ const HEX_COLOR = /^#[0-9A-Fa-f]{6}$/;
184
+ function plainObject(value, label, allowed) {
185
+ if (!value || typeof value !== "object" || Array.isArray(value))
186
+ fail(`${label} must be an object`);
187
+ const record = value;
188
+ const unknown = Object.keys(record).find((key) => !allowed.includes(key));
189
+ if (unknown)
190
+ fail(`${label} has an unknown field: ${unknown}`);
191
+ return record;
192
+ }
193
+ function boundedText(value, label, maxLength) {
194
+ if (typeof value !== "string" || !value.trim() || value.length > maxLength)
195
+ fail(`${label} must be a non-empty string of at most ${maxLength} characters`);
196
+ return value.trim();
197
+ }
198
+ function hexColor(value, label) {
199
+ if (typeof value !== "string" || !HEX_COLOR.test(value))
200
+ fail(`${label} must be a hex colour in the form #RRGGBB`);
201
+ return value.toUpperCase();
202
+ }
203
+ function rangedNumber(value, label, range, integer = false) {
204
+ if (typeof value !== "number" || !Number.isFinite(value))
205
+ fail(`${label} must be a finite number`);
206
+ if (integer && !Number.isInteger(value))
207
+ fail(`${label} must be an integer`);
208
+ if (value < range.min || value > range.max)
209
+ fail(`${label} must be between ${range.min} and ${range.max}`);
210
+ return value;
211
+ }
212
+ export function validateCustomCaptionStyle(value) {
213
+ const raw = plainObject(value, "custom_style", CUSTOM_STYLE_KEYS);
214
+ if (!Object.keys(raw).length)
215
+ fail("custom_style must set at least one field");
216
+ const limits = CUSTOM_CAPTION_STYLE_LIMITS;
217
+ const style = {};
218
+ if (raw.font_family !== undefined)
219
+ style.font_family = boundedText(raw.font_family, "custom_style.font_family", limits.font_family_max_length);
220
+ if (raw.font_weight !== undefined)
221
+ style.font_weight = boundedText(raw.font_weight, "custom_style.font_weight", limits.font_weight_max_length);
222
+ if (raw.font_size_px !== undefined)
223
+ style.font_size_px = rangedNumber(raw.font_size_px, "custom_style.font_size_px", limits.font_size_px, true);
224
+ if (raw.fill_color !== undefined)
225
+ style.fill_color = hexColor(raw.fill_color, "custom_style.fill_color");
226
+ if (raw.strokes !== undefined) {
227
+ if (!Array.isArray(raw.strokes) || raw.strokes.length > MAX_CAPTION_STROKES)
228
+ fail(`custom_style.strokes must be an array of at most ${MAX_CAPTION_STROKES} strokes`);
229
+ style.strokes = raw.strokes.map((item, index) => {
230
+ const label = `custom_style.strokes[${index}]`;
231
+ const stroke = plainObject(item, label, STROKE_KEYS);
232
+ const placement = stroke.placement === undefined ? "outer" : stroke.placement;
233
+ if (typeof placement !== "string" || !CAPTION_STROKE_PLACEMENTS.includes(placement))
234
+ fail(`${label}.placement must be one of: ${CAPTION_STROKE_PLACEMENTS.join(", ")}`);
235
+ return {
236
+ color: hexColor(stroke.color, `${label}.color`),
237
+ width_px: rangedNumber(stroke.width_px, `${label}.width_px`, limits.stroke_width_px),
238
+ placement: placement,
239
+ };
240
+ });
241
+ }
242
+ if (raw.shadow !== undefined) {
243
+ const shadow = plainObject(raw.shadow, "custom_style.shadow", SHADOW_KEYS);
244
+ const numberOr = (key) => shadow[key] === undefined ? CAPTION_SHADOW_DEFAULTS[key] : rangedNumber(shadow[key], `custom_style.shadow.${key}`, limits.shadow[key]);
245
+ style.shadow = {
246
+ color: shadow.color === undefined ? CAPTION_SHADOW_DEFAULTS.color : hexColor(shadow.color, "custom_style.shadow.color"),
247
+ opacity: numberOr("opacity"),
248
+ angle: numberOr("angle"),
249
+ distance: numberOr("distance"),
250
+ size: numberOr("size"),
251
+ blur: numberOr("blur"),
252
+ };
253
+ }
254
+ if (raw.zone !== undefined) {
255
+ if (typeof raw.zone !== "string" || !CAPTION_ZONES.includes(raw.zone))
256
+ fail(`custom_style.zone must be one of: ${CAPTION_ZONES.join(", ")}`);
257
+ style.zone = raw.zone;
258
+ }
259
+ if (raw.offset_x_px !== undefined)
260
+ style.offset_x_px = rangedNumber(raw.offset_x_px, "custom_style.offset_x_px", limits.offset_px, true);
261
+ if (raw.offset_y_px !== undefined)
262
+ style.offset_y_px = rangedNumber(raw.offset_y_px, "custom_style.offset_y_px", limits.offset_px, true);
263
+ if (raw.track_style_name !== undefined)
264
+ style.track_style_name = boundedText(raw.track_style_name, "custom_style.track_style_name", limits.track_style_name_max_length);
265
+ return style;
266
+ }
267
+ const ordinal = (index) => ["first", "second", "third"][index] ?? `#${index + 1}`;
268
+ const zoneLabel = (zone) => zone.replace("_", "-");
269
+ /** Warnings about a custom style that Premiere will accept but that may not render as intended. */
270
+ export function customCaptionStyleWarnings(style) {
271
+ const warnings = [];
272
+ const outer = (style.strokes ?? []).filter((stroke) => stroke.placement === "outer");
273
+ for (let index = 1; index < outer.length; index += 1) {
274
+ if (outer[index].width_px <= outer[index - 1].width_px)
275
+ warnings.push(`outer stroke ${index + 1} (${outer[index].width_px} px) is not wider than outer stroke ${index} (${outer[index - 1].width_px} px), so it may be hidden behind it; layered outer strokes need increasing widths`);
276
+ }
277
+ if (style.shadow && style.shadow.opacity === 0)
278
+ warnings.push("shadow opacity is 0, so the shadow will not be visible");
279
+ if (style.strokes?.some((stroke) => stroke.width_px === 0))
280
+ warnings.push("a stroke with width 0 will not be visible");
281
+ return warnings;
282
+ }
283
+ const TRACK_STYLE_SELECT_STEPS = [
284
+ "Select one caption on the caption track in the timeline. One caption is enough: a Track Style applies to every caption on the track, so no multi-select is needed.",
285
+ "Open the Properties panel (Window > Properties). If your Premiere version has no Properties panel, use Window > Essential Graphics > Edit, which shows the same caption sections.",
286
+ ];
287
+ function trackStyleSaveSteps(name) {
288
+ return [
289
+ `Track Style: open the Track Style dropdown in the Properties panel, choose "+ Create style", and name it "${name}". The style now applies to the whole caption track.`,
290
+ "Scrub through the track and confirm that the other captions show the new style.",
291
+ `Reuse: select a caption on the caption track in another sequence and pick "${name}" from the Track Style dropdown.`,
292
+ `The style is a project item in the Project panel. Move "${name}" into a shared bin (for example "Caption Styles") so other sequences and projects can reuse it.`,
293
+ "Verify the rendered look with export_frame or export_sequence_review_frames.",
294
+ ];
295
+ }
296
+ const numbered = (steps) => steps.map((step, index) => `${index + 1}. ${step}`);
297
+ /** Premiere 2024+ Properties-panel / Track Style steps for a custom caption style. */
298
+ export function buildCustomCaptionStyleSteps(style, preset) {
299
+ const fallback = preset ? describeCaptionStyle(preset) : undefined;
300
+ const steps = [...TRACK_STYLE_SELECT_STEPS];
301
+ const text = [];
302
+ const family = style.font_family ?? fallback?.font_family_suggestion;
303
+ const weight = style.font_weight ?? fallback?.font_weight;
304
+ if (family)
305
+ text.push(`font family "${family}"`);
306
+ if (weight)
307
+ text.push(`font weight/style "${weight}"`);
308
+ if (style.font_size_px !== undefined)
309
+ text.push(`font size ${style.font_size_px}`);
310
+ else if (fallback)
311
+ text.push(`font size about ${fallback.font_size_percent_of_height}% of the frame height`);
312
+ if (text.length)
313
+ steps.push(`Text: set ${text.join(", ")}.`);
314
+ if (style.fill_color)
315
+ steps.push(`Appearance > Fill: turn Fill on and set the colour to ${style.fill_color}.`);
316
+ if (style.strokes) {
317
+ if (!style.strokes.length)
318
+ steps.push("Appearance > Stroke: turn Stroke off.");
319
+ style.strokes.forEach((stroke, index) => {
320
+ const values = `colour ${stroke.color}, width ${stroke.width_px}, ${stroke.placement} stroke`;
321
+ steps.push(index === 0
322
+ ? `Appearance > Stroke: turn Stroke on and set the ${ordinal(index)} stroke to ${values}.`
323
+ : `Appearance > Stroke: press + next to Stroke to add stroke ${index + 1}, then set it to ${values}.`);
324
+ });
325
+ }
326
+ else if (fallback) {
327
+ steps.push(`Appearance > Stroke: ${fallback.stroke ? "turn Stroke on" : "leave Stroke off"} (preset ${fallback.preset}).`);
328
+ }
329
+ if (style.shadow) {
330
+ const shadow = style.shadow;
331
+ steps.push(`Appearance > Shadow: turn Shadow on and set colour ${shadow.color}, Opacity ${shadow.opacity}%, Angle ${shadow.angle}°, Distance ${shadow.distance}, Size ${shadow.size}, Blur ${shadow.blur}.`);
332
+ }
333
+ if (style.zone)
334
+ steps.push(`Align and Transform: choose the ${zoneLabel(style.zone)} cell in the caption region (zone) grid.`);
335
+ else if (fallback)
336
+ steps.push(`Align and Transform: place the caption centre near x=${fallback.position.x * 100}%, y=${fallback.position.y * 100}% of the frame (preset ${fallback.preset}).`);
337
+ if (style.offset_x_px !== undefined || style.offset_y_px !== undefined) {
338
+ steps.push(`Align and Transform: set the position offset to X ${style.offset_x_px ?? 0} and Y ${style.offset_y_px ?? 0} (pixels relative to the chosen zone; negative Y moves the caption up).`);
339
+ }
340
+ steps.push(...trackStyleSaveSteps(style.track_style_name ?? DEFAULT_TRACK_STYLE_NAME));
341
+ return numbered(steps);
342
+ }
343
+ /** Premiere 2024+ Properties-panel / Track Style steps for a built-in preset. */
344
+ export function buildPresetCaptionStyleSteps(style) {
345
+ const steps = [
346
+ ...TRACK_STYLE_SELECT_STEPS,
347
+ `Text: set font family "${style.font_family_suggestion}", font weight ${style.font_weight}, and a font size of about ${style.font_size_percent_of_height}% of the frame height.`,
348
+ `Appearance > Stroke: ${style.stroke ? "turn Stroke on (press + next to Stroke for a second, wider outer stroke if you want a layered outline)" : "leave Stroke off"}.`,
349
+ `Appearance > Background: ${style.background ? "turn Background on and lower its opacity so the video stays visible" : "leave Background off"}.`,
350
+ `Align and Transform: centre-align the text and place the caption centre near x=${style.position.x * 100}%, y=${style.position.y * 100}% of the frame (normalized, centre-anchored).`,
351
+ ...(style.word_highlight ? ["Word highlight: enable word highlighting for a karaoke-style progressive reveal where your Premiere version offers it."] : []),
352
+ ...(style.uppercase_recommended ? ["Text: uppercase is recommended for this preset (use the All Caps option or the build_caption_artifact uppercase flag)."] : []),
353
+ ...trackStyleSaveSteps(`${style.preset} captions`),
354
+ ];
355
+ return numbered(steps);
356
+ }
357
+ export const CAPTION_TRACK_STYLE_GUIDANCE = {
358
+ scope: "A Track Style applies to every caption on the caption track. Style one caption, save it with Track Style > + Create style, and the whole track updates; no multi-select is needed.",
359
+ reusable: "A saved Track Style can be applied to the caption track of any other sequence in the project from the Track Style dropdown.",
360
+ project_item: "Each Track Style is stored as a project item in the Project panel; keep it in a shared bin so it can be found and reused.",
361
+ limitations: [
362
+ "Premiere Pro exposes no documented ExtendScript, QE DOM, or UXP API that creates, edits, or applies caption styles, so these steps must be performed in the Premiere UI.",
363
+ "These steps target Premiere Pro 2024 and later. Earlier versions lack the Track Style workflow described here and were not checked; panel labels can also differ between versions and UI languages.",
364
+ "Verify the rendered result with export_frame or export_sequence_review_frames; this tool cannot read the applied style back.",
365
+ ],
366
+ };
367
+ export function captionSafeZoneRecommendation() {
368
+ return safeZoneRecommendation();
369
+ }
157
370
  const toMs = (seconds) => Math.round(seconds * 1000);
158
371
  const msToSeconds = (ms) => Number((ms / 1000).toFixed(3));
159
372
  function pad(value, width) {
@@ -118,6 +118,33 @@ function __isTrackLocked(track) {
118
118
  return false;
119
119
  }
120
120
 
121
+ // Seconds used to put marks back. Exact tick-boundary seconds can floor one
122
+ // media frame early on 26.5.2; write a quarter grid past the boundary.
123
+ function __restoreSecondsFromTicks(ticks, gridTicks) {
124
+ var grid = parseFloat(gridTicks);
125
+ if (!isFinite(grid) || !(grid > 0)) grid = TICKS_PER_SECOND / 24;
126
+ return (parseFloat(ticks) + grid / 4) / TICKS_PER_SECOND;
127
+ }
128
+
129
+ function __restoreGridFromItem(item, mediaType) {
130
+ if (mediaType === 2) return TICKS_PER_SECOND / 48000;
131
+ try {
132
+ var fps = parseFloat(item.getFootageInterpretation().frameRate);
133
+ if (fps > 0) return TICKS_PER_SECOND / fps;
134
+ } catch (eGrid) {}
135
+ return TICKS_PER_SECOND / 24;
136
+ }
137
+
138
+ function __biasedDomMarks(marks, gridTicks) {
139
+ if (!marks) return marks;
140
+ return {
141
+ inTicks: marks.inTicks,
142
+ outTicks: marks.outTicks,
143
+ inSeconds: __restoreSecondsFromTicks(marks.inTicks, gridTicks),
144
+ outSeconds: __restoreSecondsFromTicks(marks.outTicks, gridTicks)
145
+ };
146
+ }
147
+
121
148
  // Read the private source range before temporarily changing project-item marks.
122
149
  function __itemMarksForRestore(item, mediaType) {
123
150
  function domMarks() {
@@ -131,7 +158,7 @@ function __itemMarksForRestore(item, mediaType) {
131
158
  }
132
159
  var dom = domMarks(), xml = "";
133
160
  try { if (typeof item.getProjectMetadata === "function") xml = String(item.getProjectMetadata() || ""); } catch (metadataMarkError) { return null; }
134
- if (!xml) return dom;
161
+ if (!xml) return __biasedDomMarks(dom, __restoreGridFromItem(item, mediaType));
135
162
  if (xml.length > 1000000) return null;
136
163
  function field(name, text) {
137
164
  var escaped = name.replace(/\\./g, "\\\\.");
@@ -192,7 +219,7 @@ function __itemMarksForRestore(item, mediaType) {
192
219
  }
193
220
  if (inBlock === null && outBlock === null) {
194
221
  if (xml.indexOf("Column.Intrinsic." + kind + "InPoint") >= 0 || xml.indexOf("Column.Intrinsic." + kind + "OutPoint") >= 0) return null;
195
- return dom;
222
+ return __biasedDomMarks(dom, __restoreGridFromItem(item, mediaType));
196
223
  }
197
224
  var left = point(inBlock, kind === "Audio"), right = point(outBlock, kind === "Audio");
198
225
  // The private VideoOutPoint names the last frame (inclusive): a subclip made
@@ -200,7 +227,12 @@ function __itemMarksForRestore(item, mediaType) {
200
227
  if (right && kind !== "Audio") right.ticks += right.frameTicks;
201
228
  if (!left || !right || !isFinite(left.ticks) || !isFinite(right.ticks) || left.ticks < 0 || right.ticks > 9007199254740991 || right.ticks <= left.ticks || Math.abs(left.frameTicks - right.frameTicks) > 1) return null;
202
229
  var tolerance = Math.max(left.frameTicks, right.frameTicks);
203
- if (dom && Math.abs(Number(dom.inTicks) - left.ticks) <= tolerance && Math.abs(Number(dom.outTicks) - right.ticks) <= tolerance) return dom;
230
+ // DOM already matches the private range. Still bias the write seconds: 26.5.2
231
+ // floors exact tick-boundary seconds one media frame early, which shrinks a
232
+ // normal clip after replace/unnest the same way it shrinks a soft subclip.
233
+ if (dom && Math.abs(Number(dom.inTicks) - left.ticks) <= tolerance && Math.abs(Number(dom.outTicks) - right.ticks) <= tolerance) {
234
+ return __biasedDomMarks(dom, tolerance);
235
+ }
204
236
  // Premiere floors a written mark to the media's frame (or sample) grid, so a
205
237
  // value exactly on the boundary can land one frame early (live 26.5.2: a
206
238
  // restored Out of 00:00:29:22 read back 00:00:29:21). Write a quarter frame
@@ -215,18 +247,43 @@ function __itemMarksForRestore(item, mediaType) {
215
247
  // empty first; overwrite never shifts neighbours but would replace them.
216
248
  // Returns { ok, attempted, marksRestored, error }. attempted=true means the
217
249
  // overwrite call ran (or threw), so the timeline may have changed.
250
+ // Media/sample grid for a project-item mark write. Video uses footage fps;
251
+ // audio uses 48 kHz when the host does not expose a rate. Fallback 24 fps is
252
+ // smaller than one frame at 23.976–60 and still larger than float ulp.
253
+ function __mediaGridTicks(item, mediaType) {
254
+ if (mediaType !== 2) {
255
+ try {
256
+ var fps = parseFloat(item.getFootageInterpretation().frameRate);
257
+ if (fps > 0) return TICKS_PER_SECOND / fps;
258
+ } catch (eInterp) {}
259
+ return TICKS_PER_SECOND / 24;
260
+ }
261
+ return TICKS_PER_SECOND / 48000;
262
+ }
263
+
264
+ function __biasedMarkSeconds(ticks, gridTicks) {
265
+ var grid = parseFloat(gridTicks);
266
+ if (!isFinite(grid) || !(grid > 0)) grid = TICKS_PER_SECOND / 24;
267
+ return (parseFloat(ticks) + grid / 4) / TICKS_PER_SECOND;
268
+ }
269
+
218
270
  // Set a project item's In/Out marks, ordering the writes so the In never
219
271
  // passes the current Out, and report whether both read back within tolerance
220
- // ticks (default: exact).
272
+ // ticks (default: exact). Write a quarter grid past the tick boundary: the
273
+ // same 26.5.2 floor that shrinks restored soft-subclip marks also shortens
274
+ // the temporary overwrite range (unnest/replace) when exact seconds are used.
221
275
  function __setItemMarks(item, wantIn, wantOut, mediaType, tolerance) {
222
276
  var tol = tolerance > __TICK_MATCH_TOL ? tolerance : __TICK_MATCH_TOL;
277
+ var grid = __mediaGridTicks(item, mediaType);
278
+ var inSeconds = __biasedMarkSeconds(wantIn, grid);
279
+ var outSeconds = __biasedMarkSeconds(wantOut, grid);
223
280
  var currentOut = parseFloat(item.getOutPoint(mediaType).ticks);
224
281
  if (parseFloat(wantIn) >= currentOut) {
225
- item.setOutPoint(__ticksToSeconds(wantOut), mediaType);
226
- item.setInPoint(__ticksToSeconds(wantIn), mediaType);
282
+ item.setOutPoint(outSeconds, mediaType);
283
+ item.setInPoint(inSeconds, mediaType);
227
284
  } else {
228
- item.setInPoint(__ticksToSeconds(wantIn), mediaType);
229
- item.setOutPoint(__ticksToSeconds(wantOut), mediaType);
285
+ item.setInPoint(inSeconds, mediaType);
286
+ item.setOutPoint(outSeconds, mediaType);
230
287
  }
231
288
  return Math.abs(parseFloat(item.getInPoint(mediaType).ticks) - parseFloat(wantIn)) <= tol &&
232
289
  Math.abs(parseFloat(item.getOutPoint(mediaType).ticks) - parseFloat(wantOut)) <= tol;
@@ -140,6 +140,7 @@ export declare function buildPlatformCapabilityReport(capabilities: CapabilityCo
140
140
  cloudPublish: string;
141
141
  contentCredentials: string;
142
142
  renderAndReplace: string;
143
+ captionTrackStyle: string;
143
144
  };
144
145
  };
145
146
  };
@@ -229,6 +229,7 @@ export function buildPlatformCapabilityReport(capabilities, platform = process.p
229
229
  cloudPublish: "No documented Premiere automation API for publishing to cloud destinations.",
230
230
  contentCredentials: "No documented Premiere automation API for configuring Content Credentials.",
231
231
  renderAndReplace: "No documented Premiere ExtendScript or UXP Render and Replace API.",
232
+ captionTrackStyle: "No documented Premiere ExtendScript, QE DOM, or UXP API creates, edits, or applies caption track styles; call get_caption_style_guidance for Properties-panel and Track Style steps to apply in the Premiere UI.",
232
233
  },
233
234
  },
234
235
  },
@@ -1,3 +1,4 @@
1
+ import { type CaptionStylePreset } from "../ai/caption-authoring.js";
1
2
  export declare const MAX_INLINE_ARTIFACT_BYTES: number;
2
3
  /**
3
4
  * Resolves `output_path` to a writable location contained inside the approved
@@ -343,22 +344,178 @@ export declare function getCaptionAuthoringTools(): {
343
344
  enum: ("podcast" | "clean" | "bold_pop" | "karaoke" | "lecture")[];
344
345
  description: string;
345
346
  };
347
+ custom_style: {
348
+ readonly type: "object";
349
+ readonly additionalProperties: false;
350
+ readonly description: "Optional caption look to translate into concrete Premiere UI steps. Every field is optional; at least one is required. Values are documentation only and are never applied.";
351
+ readonly properties: {
352
+ readonly font_family: {
353
+ readonly type: "string";
354
+ readonly minLength: 1;
355
+ readonly maxLength: 128;
356
+ readonly description: "Font family name as shown in Premiere, e.g. Source Han Sans TW.";
357
+ };
358
+ readonly font_weight: {
359
+ readonly type: "string";
360
+ readonly minLength: 1;
361
+ readonly maxLength: 64;
362
+ readonly description: "Font weight or style name, e.g. Heavy or Bold.";
363
+ };
364
+ readonly font_size_px: {
365
+ readonly type: "integer";
366
+ readonly minimum: 8;
367
+ readonly maximum: 400;
368
+ readonly description: "Font size in Premiere's caption units (pixels).";
369
+ };
370
+ readonly fill_color: {
371
+ readonly type: "string";
372
+ readonly pattern: "^#[0-9A-Fa-f]{6}$";
373
+ readonly description: "Text fill colour as #RRGGBB.";
374
+ };
375
+ readonly strokes: {
376
+ readonly type: "array";
377
+ readonly maxItems: 3;
378
+ readonly description: "Up to three strokes in Premiere's Stroke list order; each stroke after the first is added with + next to Stroke. Layered outer strokes need increasing widths.";
379
+ readonly items: {
380
+ readonly type: "object";
381
+ readonly additionalProperties: false;
382
+ readonly description: "One stroke.";
383
+ readonly properties: {
384
+ readonly color: {
385
+ readonly type: "string";
386
+ readonly pattern: "^#[0-9A-Fa-f]{6}$";
387
+ readonly description: "Stroke colour as #RRGGBB.";
388
+ };
389
+ readonly width_px: {
390
+ readonly type: "number";
391
+ readonly minimum: 0;
392
+ readonly maximum: 100;
393
+ readonly description: "Stroke width.";
394
+ };
395
+ readonly placement: {
396
+ readonly type: "string";
397
+ readonly enum: readonly ["outer", "inner", "center"];
398
+ readonly description: "Stroke placement (default outer).";
399
+ };
400
+ };
401
+ readonly required: readonly ["color", "width_px"];
402
+ };
403
+ };
404
+ readonly shadow: {
405
+ readonly type: "object";
406
+ readonly additionalProperties: false;
407
+ readonly description: "Drop shadow; omitted fields use the listed defaults.";
408
+ readonly properties: {
409
+ readonly color: {
410
+ readonly type: "string";
411
+ readonly pattern: "^#[0-9A-Fa-f]{6}$";
412
+ readonly description: "Shadow colour as #RRGGBB (default #000000).";
413
+ };
414
+ readonly opacity: {
415
+ type: string;
416
+ minimum: 0;
417
+ maximum: 100 | 200 | 360;
418
+ description: string;
419
+ };
420
+ readonly angle: {
421
+ type: string;
422
+ minimum: 0;
423
+ maximum: 100 | 200 | 360;
424
+ description: string;
425
+ };
426
+ readonly distance: {
427
+ type: string;
428
+ minimum: 0;
429
+ maximum: 100 | 200 | 360;
430
+ description: string;
431
+ };
432
+ readonly size: {
433
+ type: string;
434
+ minimum: 0;
435
+ maximum: 100 | 200 | 360;
436
+ description: string;
437
+ };
438
+ readonly blur: {
439
+ type: string;
440
+ minimum: 0;
441
+ maximum: 100 | 200 | 360;
442
+ description: string;
443
+ };
444
+ };
445
+ };
446
+ readonly zone: {
447
+ readonly type: "string";
448
+ readonly enum: readonly ["top_left", "top_center", "top_right", "middle_left", "middle_center", "middle_right", "bottom_left", "bottom_center", "bottom_right"];
449
+ readonly description: "Caption region in Premiere's Align and Transform zone grid.";
450
+ };
451
+ readonly offset_x_px: {
452
+ readonly type: "integer";
453
+ readonly minimum: -2000;
454
+ readonly maximum: 2000;
455
+ readonly description: "Horizontal position offset from the zone in pixels.";
456
+ };
457
+ readonly offset_y_px: {
458
+ readonly type: "integer";
459
+ readonly minimum: -2000;
460
+ readonly maximum: 2000;
461
+ readonly description: "Vertical position offset from the zone in pixels; negative moves up.";
462
+ };
463
+ readonly track_style_name: {
464
+ readonly type: "string";
465
+ readonly minLength: 1;
466
+ readonly maxLength: 80;
467
+ readonly description: "Name for the saved Track Style (default Caption Track Style).";
468
+ };
469
+ };
470
+ };
346
471
  };
347
472
  };
348
- handler: (args: {
349
- preset?: string;
350
- }) => Promise<{
473
+ handler: (args: Record<string, unknown>) => Promise<{
351
474
  success: boolean;
352
475
  data: {
353
- style: import("../ai/caption-authoring.js").CaptionStyleDescriptor;
476
+ style: import("../ai/caption-authoring.js").CaptionStyleDescriptor | {
477
+ custom_style: import("../ai/caption-authoring.js").CustomCaptionStyle;
478
+ source: string;
479
+ preset: CaptionStylePreset;
480
+ font_family_suggestion: string;
481
+ font_weight: string;
482
+ font_size_percent_of_height: number;
483
+ position: {
484
+ x: number;
485
+ y: number;
486
+ };
487
+ alignment: "center";
488
+ stroke: boolean;
489
+ background: boolean;
490
+ word_highlight: boolean;
491
+ uppercase_recommended: boolean;
492
+ safe_zone_recommendation: Record<import("../ai/caption-authoring.js").SafeZonePlatform, {
493
+ x: number;
494
+ y: number;
495
+ }>;
496
+ note: string;
497
+ } | {
498
+ custom_style: import("../ai/caption-authoring.js").CustomCaptionStyle;
499
+ source: string;
500
+ preset: null;
501
+ };
354
502
  workflow: string[];
355
503
  premiere_ui_steps: string[];
504
+ track_style: {
505
+ name: string;
506
+ scope: "A Track Style applies to every caption on the caption track. Style one caption, save it with Track Style > + Create style, and the whole track updates; no multi-select is needed.";
507
+ reusable: "A saved Track Style can be applied to the caption track of any other sequence in the project from the Track Style dropdown.";
508
+ project_item: "Each Track Style is stored as a project item in the Project panel; keep it in a shared bin so it can be found and reused.";
509
+ };
510
+ limitations: ("Premiere Pro exposes no documented ExtendScript, QE DOM, or UXP API that creates, edits, or applies caption styles, so these steps must be performed in the Premiere UI." | "These steps target Premiere Pro 2024 and later. Earlier versions lack the Track Style workflow described here and were not checked; panel labels can also differ between versions and UI languages." | "Verify the rendered result with export_frame or export_sequence_review_frames; this tool cannot read the applied style back.")[];
511
+ warnings: string[];
512
+ applied: boolean;
356
513
  mutation_refused: string;
357
514
  safe_zones: Record<"x" | "tiktok" | "instagram_reels" | "youtube_shorts" | "instagram_feed" | "youtube" | "linkedin", {
358
515
  x: number;
359
516
  y: number;
360
517
  }>;
361
- routes: ("read_sequence_captions" | "transform_track_item_uxp" | "export_sequence_review_frames" | "create_caption_track" | "set_clip_position" | "inspect_caption_tracks_uxp")[];
518
+ routes: ("read_sequence_captions" | "transform_track_item_uxp" | "export_frame" | "export_sequence_review_frames" | "create_caption_track" | "set_clip_position" | "inspect_caption_tracks_uxp")[];
362
519
  };
363
520
  error?: undefined;
364
521
  } | {
@@ -1,10 +1,63 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { existsSync, realpathSync, statSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
- import { buildCaptionArtifact, CAPTION_AUTHORING_ROUTES, CAPTION_STYLE_PRESETS, checkCaptionSafeZone, describeCaptionStyle, MAX_EMPHASIS_WORDS, MAX_FILLER_TOKENS, MAX_FRAME_DIMENSION, MAX_SAFE_ZONE_ELEMENTS, MIN_FRAME_DIMENSION, SAFE_ZONE_ELEMENT_KINDS, SAFE_ZONE_PLATFORMS, SAFE_ZONE_ROUTES, } from "../ai/caption-authoring.js";
4
+ import { buildCaptionArtifact, CAPTION_AUTHORING_ROUTES, CAPTION_SHADOW_DEFAULTS, CAPTION_STROKE_PLACEMENTS, CAPTION_STYLE_PRESETS, CAPTION_TRACK_STYLE_GUIDANCE, CAPTION_ZONES, buildCustomCaptionStyleSteps, buildPresetCaptionStyleSteps, captionSafeZoneRecommendation, checkCaptionSafeZone, CUSTOM_CAPTION_STYLE_LIMITS, customCaptionStyleWarnings, DEFAULT_TRACK_STYLE_NAME, describeCaptionStyle, MAX_CAPTION_STROKES, validateCustomCaptionStyle, MAX_EMPHASIS_WORDS, MAX_FILLER_TOKENS, MAX_FRAME_DIMENSION, MAX_SAFE_ZONE_ELEMENTS, MIN_FRAME_DIMENSION, SAFE_ZONE_ELEMENT_KINDS, SAFE_ZONE_PLATFORMS, SAFE_ZONE_ROUTES, } from "../ai/caption-authoring.js";
5
5
  import { WORD_TIMELINE_PARAMETER } from "../ai/word-timeline.js";
6
6
  export const MAX_INLINE_ARTIFACT_BYTES = 512 * 1024;
7
7
  const MAX_PATH_LENGTH = 4096;
8
+ const CAPTION_STYLE_VERIFY_ROUTES = ["export_frame", "export_sequence_review_frames"];
9
+ const HEX_COLOR_PATTERN = "^#[0-9A-Fa-f]{6}$";
10
+ const LIMITS = CUSTOM_CAPTION_STYLE_LIMITS;
11
+ const shadowNumber = (key, label) => ({
12
+ type: "number",
13
+ minimum: LIMITS.shadow[key].min,
14
+ maximum: LIMITS.shadow[key].max,
15
+ description: `${label} (default ${CAPTION_SHADOW_DEFAULTS[key]}).`,
16
+ });
17
+ const CUSTOM_STYLE_PARAMETER = {
18
+ type: "object",
19
+ additionalProperties: false,
20
+ description: "Optional caption look to translate into concrete Premiere UI steps. Every field is optional; at least one is required. Values are documentation only and are never applied.",
21
+ properties: {
22
+ font_family: { type: "string", minLength: 1, maxLength: LIMITS.font_family_max_length, description: "Font family name as shown in Premiere, e.g. Source Han Sans TW." },
23
+ font_weight: { type: "string", minLength: 1, maxLength: LIMITS.font_weight_max_length, description: "Font weight or style name, e.g. Heavy or Bold." },
24
+ font_size_px: { type: "integer", minimum: LIMITS.font_size_px.min, maximum: LIMITS.font_size_px.max, description: "Font size in Premiere's caption units (pixels)." },
25
+ fill_color: { type: "string", pattern: HEX_COLOR_PATTERN, description: "Text fill colour as #RRGGBB." },
26
+ strokes: {
27
+ type: "array",
28
+ maxItems: MAX_CAPTION_STROKES,
29
+ description: "Up to three strokes in Premiere's Stroke list order; each stroke after the first is added with + next to Stroke. Layered outer strokes need increasing widths.",
30
+ items: {
31
+ type: "object",
32
+ additionalProperties: false,
33
+ description: "One stroke.",
34
+ properties: {
35
+ color: { type: "string", pattern: HEX_COLOR_PATTERN, description: "Stroke colour as #RRGGBB." },
36
+ width_px: { type: "number", minimum: LIMITS.stroke_width_px.min, maximum: LIMITS.stroke_width_px.max, description: "Stroke width." },
37
+ placement: { type: "string", enum: [...CAPTION_STROKE_PLACEMENTS], description: "Stroke placement (default outer)." },
38
+ },
39
+ required: ["color", "width_px"],
40
+ },
41
+ },
42
+ shadow: {
43
+ type: "object",
44
+ additionalProperties: false,
45
+ description: "Drop shadow; omitted fields use the listed defaults.",
46
+ properties: {
47
+ color: { type: "string", pattern: HEX_COLOR_PATTERN, description: `Shadow colour as #RRGGBB (default ${CAPTION_SHADOW_DEFAULTS.color}).` },
48
+ opacity: shadowNumber("opacity", "Shadow opacity percent"),
49
+ angle: shadowNumber("angle", "Shadow angle in degrees"),
50
+ distance: shadowNumber("distance", "Shadow distance"),
51
+ size: shadowNumber("size", "Shadow size"),
52
+ blur: shadowNumber("blur", "Shadow blur"),
53
+ },
54
+ },
55
+ zone: { type: "string", enum: [...CAPTION_ZONES], description: "Caption region in Premiere's Align and Transform zone grid." },
56
+ offset_x_px: { type: "integer", minimum: LIMITS.offset_px.min, maximum: LIMITS.offset_px.max, description: "Horizontal position offset from the zone in pixels." },
57
+ offset_y_px: { type: "integer", minimum: LIMITS.offset_px.min, maximum: LIMITS.offset_px.max, description: "Vertical position offset from the zone in pixels; negative moves up." },
58
+ track_style_name: { type: "string", minLength: 1, maxLength: LIMITS.track_style_name_max_length, description: `Name for the saved Track Style (default ${DEFAULT_TRACK_STYLE_NAME}).` },
59
+ },
60
+ };
8
61
  function absolutePath(value, label) {
9
62
  if (typeof value !== "string" || !value.trim() || value.length > MAX_PATH_LENGTH)
10
63
  throw new Error(`${label} must be a non-empty string of at most ${MAX_PATH_LENGTH} characters`);
@@ -195,22 +248,30 @@ export function getCaptionAuthoringTools() {
195
248
  },
196
249
  },
197
250
  get_caption_style_guidance: {
198
- description: "Returns caption style presets (clean, bold_pop, karaoke, podcast, lecture) with font, position, and styling recommendations, plus Premiere UI steps for applying styles. Read-only guidance; NEVER modifies caption tracks. Use build_caption_artifact for SRT/VTT timing, then import via create_caption_track and apply styles manually in Premiere's UI.",
251
+ description: "Read-only caption style guidance: a preset (clean, bold_pop, karaoke, podcast, lecture) and/or a custom_style spec (font, size, fill, up to 3 strokes, shadow, zone, offsets) turned into Premiere 2024+ Properties-panel steps that save a reusable whole-track Track Style. NEVER modifies caption tracks; Premiere has no caption style API.",
199
252
  parameters: {
200
253
  type: "object",
201
254
  additionalProperties: false,
202
255
  required: [],
203
256
  properties: {
204
- preset: { type: "string", enum: [...CAPTION_STYLE_PRESETS], description: "Style preset to describe (default clean)." },
257
+ preset: { type: "string", enum: [...CAPTION_STYLE_PRESETS], description: "Style preset to describe (default clean when custom_style is omitted)." },
258
+ custom_style: CUSTOM_STYLE_PARAMETER,
205
259
  },
206
260
  },
207
261
  handler: async (args) => {
208
262
  try {
209
- const preset = (args.preset ?? "clean");
210
- if (!CAPTION_STYLE_PRESETS.includes(preset)) {
263
+ if (!args || typeof args !== "object" || Array.isArray(args))
264
+ throw new Error("arguments must be an object");
265
+ if (args.preset !== undefined && (typeof args.preset !== "string" || !CAPTION_STYLE_PRESETS.includes(args.preset))) {
211
266
  throw new Error(`preset must be one of: ${CAPTION_STYLE_PRESETS.join(", ")}`);
212
267
  }
213
- const style = describeCaptionStyle(preset);
268
+ const custom = args.custom_style === undefined ? undefined : validateCustomCaptionStyle(args.custom_style);
269
+ const preset = (args.preset ?? (custom ? undefined : "clean"));
270
+ const presetStyle = preset ? describeCaptionStyle(preset) : undefined;
271
+ const style = custom
272
+ ? { ...(presetStyle ?? { preset: null }), custom_style: custom, source: presetStyle ? "preset_with_custom_style" : "custom_style" }
273
+ : presetStyle;
274
+ const premiereUiSteps = custom ? buildCustomCaptionStyleSteps(custom, preset) : buildPresetCaptionStyleSteps(presetStyle);
214
275
  return {
215
276
  success: true,
216
277
  data: {
@@ -220,29 +281,21 @@ export function getCaptionAuthoringTools() {
220
281
  "Step 2: Import the caption file into Premiere project",
221
282
  "Step 3: Call create_caption_track with action:import and the imported item_id",
222
283
  "Step 4: Inspect the track with read_sequence_captions or inspect_caption_tracks_uxp",
223
- "Step 5: Apply font/color/position/background styles manually in Premiere UI",
284
+ "Step 5: Style one caption in Premiere's Properties panel and save it as a Track Style (premiere_ui_steps); it applies to the whole caption track",
224
285
  ],
225
- premiere_ui_steps: [
226
- `1. Select the caption track in the timeline`,
227
- `2. Open Essential Graphics panel (Window > Essential Graphics)`,
228
- `3. In the Edit tab, adjust Text properties:`,
229
- ` - Font Family: ${style.font_family_suggestion}`,
230
- ` - Font Weight: ${style.font_weight}`,
231
- ` - Font Size: approximately ${style.font_size_percent_of_height}% of frame height`,
232
- ` - Position: x=${style.position.x * 100}%, y=${style.position.y * 100}% (normalized, center-anchored)`,
233
- ` - Alignment: ${style.alignment}`,
234
- ` - Stroke: ${style.stroke ? "enabled" : "disabled"}`,
235
- ` - Background: ${style.background ? "enabled with opacity" : "disabled"}`,
236
- ...(style.word_highlight
237
- ? [" - Word Highlight: enabled for karaoke-style progressive reveal"]
238
- : []),
239
- ...(style.uppercase_recommended
240
- ? [" - Transform: uppercase recommended for this preset"]
241
- : []),
242
- ],
243
- mutation_refused: "This tool provides guidance only. MCP for Adobe Premiere Pro does not expose a CaptionTrack style mutation API because Premiere does not expose one through documented ExtendScript, QE DOM, or UXP surfaces. Apply caption styles manually in the Essential Graphics panel.",
244
- safe_zones: style.safe_zone_recommendation,
245
- routes: [...CAPTION_AUTHORING_ROUTES, ...SAFE_ZONE_ROUTES],
286
+ premiere_ui_steps: premiereUiSteps,
287
+ track_style: {
288
+ name: custom?.track_style_name ?? (custom ? DEFAULT_TRACK_STYLE_NAME : `${preset} captions`),
289
+ scope: CAPTION_TRACK_STYLE_GUIDANCE.scope,
290
+ reusable: CAPTION_TRACK_STYLE_GUIDANCE.reusable,
291
+ project_item: CAPTION_TRACK_STYLE_GUIDANCE.project_item,
292
+ },
293
+ limitations: [...CAPTION_TRACK_STYLE_GUIDANCE.limitations],
294
+ warnings: custom ? customCaptionStyleWarnings(custom) : [],
295
+ applied: false,
296
+ mutation_refused: "This tool provides guidance only. MCP for Adobe Premiere Pro does not expose a CaptionTrack style mutation API because Premiere does not expose one through documented ExtendScript, QE DOM, or UXP surfaces. Apply caption styles manually in Premiere's Properties panel (or Essential Graphics) and save them as a Track Style.",
297
+ safe_zones: presetStyle?.safe_zone_recommendation ?? captionSafeZoneRecommendation(),
298
+ routes: [...CAPTION_AUTHORING_ROUTES, ...SAFE_ZONE_ROUTES, ...CAPTION_STYLE_VERIFY_ROUTES],
246
299
  },
247
300
  };
248
301
  }
@@ -337,6 +337,7 @@ export declare function getHealthTools(bridgeOptions: BridgeOptions, capabilitie
337
337
  cloudPublish: string;
338
338
  contentCredentials: string;
339
339
  renderAndReplace: string;
340
+ captionTrackStyle: string;
340
341
  };
341
342
  };
342
343
  };
@@ -1654,13 +1654,15 @@ export function getTimelineTools(bridgeOptions, dependencies = {}) {
1654
1654
  if (Math.abs(parseFloat(cand.start.ticks) - oldStart) > __TICK_MATCH_TOL) continue;
1655
1655
  replacement = cand;
1656
1656
  }
1657
- // The item's Out mark can land up to one media frame early, and
1658
- // overwriteClip can place a clip a frame short (live 26.5.2). Extend
1659
- // the end into the empty remainder of the original span.
1657
+ // The item's Out mark can land up to one media frame off the 25 fps
1658
+ // span (early without write bias, or one media frame long when the
1659
+ // closer media frame is past the tick boundary). Pull the placed end
1660
+ // back onto the original span when the drift is within one media
1661
+ // frame plus one sequence frame.
1660
1662
  var endCorrected = false;
1661
1663
  if (replacement) {
1662
1664
  var endGap = oldEnd - parseFloat(replacement.end.ticks);
1663
- if (endGap > __TICK_MATCH_TOL && endGap <= markTolerance + __sequenceFrameTicks(seq)) {
1665
+ if (Math.abs(endGap) > __TICK_MATCH_TOL && Math.abs(endGap) <= markTolerance + __sequenceFrameTicks(seq)) {
1664
1666
  var endTime = new Time();
1665
1667
  endTime.ticks = String(oldEnd);
1666
1668
  try { replacement.end = endTime; endCorrected = true; } catch (eEnd) {}
@@ -165,7 +165,7 @@ operation” when the tool has no enum-based mode.
165
165
  | `get_bin_contents` | Default profile | Single operation | Get detailed contents of a specific bin (folder) including all nested items, media paths, offline status, color labels, and metadata. Searches by bin name or node ID. |
166
166
  | `get_bridge_telemetry` | Default profile | Single operation | Inspect privacy-preserving aggregate bridge health: pending command/response counts, busy operations, queue age, and CEP heartbeat state without returning project or personal data. |
167
167
  | `get_capabilities` | Default profile | Single operation | Discover Premiere operations and report backend coverage, authority, and verification requirements. Use tool_query with task keywords (for example transcript, captions, or review frames) for ranked, bounded matches available in this session. Use tool_names for exact lookups or tool_offset/tool_limit for paging. Discovery never grants authority or proves a live host is ready. |
168
- | `get_caption_style_guidance` | Default profile | `preset`: `clean`, `bold_pop`, `karaoke`, `podcast`, `lecture` | Returns caption style presets (clean, bold_pop, karaoke, podcast, lecture) with font, position, and styling recommendations, plus Premiere UI steps for applying styles. Read-only guidance; NEVER modifies caption tracks. Use build_caption_artifact for SRT/VTT timing, then import via create_caption_track and apply styles manually in Premiere's UI. |
168
+ | `get_caption_style_guidance` | Default profile | `preset`: `clean`, `bold_pop`, `karaoke`, `podcast`, `lecture` | Read-only caption style guidance: a preset (clean, bold_pop, karaoke, podcast, lecture) and/or a custom_style spec (font, size, fill, up to 3 strokes, shadow, zone, offsets) turned into Premiere 2024+ Properties-panel steps that save a reusable whole-track Track Style. NEVER modifies caption tracks; Premiere has no caption style API. |
169
169
  | `get_clip_adjustment_layer` | Default profile | Single operation | Check if a clip is an adjustment layer |
170
170
  | `get_clip_at_playhead` | Default profile | `track_type`: `video`, `audio`, `both` | Get all clips at the current playhead position across all tracks. |
171
171
  | `get_clip_at_position` | Default profile | `track_type`: `video`, `audio` | Get the clip at a specific time position on a track |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "premiere-pro-mcp",
3
3
  "mcpName": "io.github.leancoderkavy/premiere-pro",
4
- "version": "1.23.0",
4
+ "version": "1.24.0",
5
5
  "description": "MCP server for Adobe Premiere Pro with 434 core AI video editing tools, a guarded After Effects MOGRT studio, and a capability-aware UXP bridge for supported Premiere workflows.",
6
6
  "main": "dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  "name": "MCP for Adobe Premiere Pro",
11
11
  "mcpName": "io.github.leancoderkavy/premiere-pro",
12
12
  "npmPackage": "premiere-pro-mcp",
13
- "version": "1.23.0",
13
+ "version": "1.24.0",
14
14
  "repository": "https://github.com/leancoderkavy/premiere-pro-mcp",
15
15
  "homepage": "https://premiere-pro-mcp.com/",
16
16
  "license": "MIT",
@@ -2,7 +2,7 @@
2
2
  "manifestVersion": 5,
3
3
  "id": "com.ppmcp.premiere.uxp",
4
4
  "name": "MCP for Adobe Premiere Pro",
5
- "version": "1.23.0",
5
+ "version": "1.24.0",
6
6
  "main": "index.html",
7
7
  "host": { "app": "premierepro", "minVersion": "25.6.0" },
8
8
  "entrypoints": [