feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
+88
@@ -0,0 +1,88 @@
|
||||
# UXP CCX distribution
|
||||
|
||||
The UXP bridge is packaged as a `.ccx` candidate for direct installation
|
||||
through Adobe Creative Cloud Desktop. It targets Premiere Pro 25.6+ only. The
|
||||
CI packer creates a deterministic ZIP-based CCX structure without requiring
|
||||
UXP Developer Tool, but Adobe's own tooling and a real Premiere installation
|
||||
remain the authoritative installation check.
|
||||
|
||||
## Build channels
|
||||
|
||||
Validate the source manifest and build the normal direct-distribution package:
|
||||
|
||||
```sh
|
||||
node scripts/validate-distribution.mjs --uxp
|
||||
node scripts/build-uxp-ccx.mjs
|
||||
```
|
||||
|
||||
This writes:
|
||||
|
||||
```text
|
||||
artifacts/premiere-pro-mcp-uxp-<version>-direct.ccx
|
||||
```
|
||||
|
||||
The builder uses a fixed ZIP timestamp, lexical file order, and stored entries,
|
||||
then reads the generated central directory and checks every entry and CRC. The
|
||||
result is reproducible for identical source bytes. It also rejects symlinks,
|
||||
requires the UXP v5 Premiere manifest, checks the version against
|
||||
`package.json`. Adobe's Premiere 26.3 runtime requires the compatible `domains: "all"`
|
||||
manifest form for the WebSocket connection; `workspace.cjs` remains the enforced authority and
|
||||
accepts only `ws://127.0.0.1:<port>/uxp` or `ws://localhost:<port>/uxp`.
|
||||
|
||||
For an Adobe Creative Cloud Marketplace submission, Adobe Developer
|
||||
Distribution must first create the Marketplace plugin ID. Build a separate
|
||||
package without changing the checked-in direct manifest:
|
||||
|
||||
```sh
|
||||
UXP_DISTRIBUTION_CHANNEL=marketplace \
|
||||
UXP_MARKETPLACE_PLUGIN_ID='<Adobe-issued ID>' \
|
||||
node scripts/build-uxp-ccx.mjs
|
||||
```
|
||||
|
||||
In PowerShell:
|
||||
|
||||
```powershell
|
||||
$env:UXP_DISTRIBUTION_CHANNEL = 'marketplace'
|
||||
$env:UXP_MARKETPLACE_PLUGIN_ID = '<Adobe-issued ID>'
|
||||
node scripts/build-uxp-ccx.mjs
|
||||
```
|
||||
|
||||
That produces `artifacts/premiere-pro-mcp-uxp-<version>-marketplace.ccx`. The
|
||||
script refuses a Marketplace build without an Adobe-issued ID or if that ID is
|
||||
the direct-distribution ID. This preserves Adobe's required split between
|
||||
direct and Marketplace identities when both channels are used.
|
||||
|
||||
## CI and approval boundaries
|
||||
|
||||
`.github/workflows/uxp-package.yml` builds and uploads the direct CCX on every
|
||||
published release. It can also be run manually for a Marketplace candidate
|
||||
after the owner supplies the Adobe-issued ID. The workflow only produces
|
||||
artifacts; it does not create a Developer Distribution listing, submit an
|
||||
artifact, make a Marketplace listing public, or claim Adobe approval.
|
||||
|
||||
Adobe recommends packaging through UXP Developer Tool for normal release work.
|
||||
This repository's deterministic packer is intended for CI artifact creation
|
||||
because a CCX is a ZIP archive, but it does not replace these required release
|
||||
gates:
|
||||
|
||||
1. Install the resulting CCX through Creative Cloud Desktop on Windows, Intel
|
||||
macOS, and Apple Silicon as applicable.
|
||||
2. Load the panel in a real Premiere Pro 25.6+ host and run the documented
|
||||
read-only connection check.
|
||||
3. For Marketplace distribution, use the exact ID from Adobe Developer
|
||||
Distribution, complete listing metadata and reviewer instructions, and wait
|
||||
for Adobe review and approval.
|
||||
|
||||
Adobe states that ordinary UXP CCX packages do not use the CEP-style digital
|
||||
signature/timestamp process. Direct installs may still show Creative Cloud's
|
||||
trust warning, so distribute the artifact only from a source users can verify.
|
||||
See Adobe's [packaging guide](https://developer.adobe.com/premiere-pro/uxp/plugins/distribution/package/)
|
||||
and [Marketplace guide](https://developer.adobe.com/premiere-pro/uxp/plugins/distribution/adobe-marketplace/).
|
||||
|
||||
## Capability boundary
|
||||
|
||||
The CCX package being well-formed does not establish live-host capability
|
||||
parity. UXP is the supported path for the operations its runtime capability
|
||||
probe advertises. CEP remains the explicit compatibility path for older hosts
|
||||
and unsupported UXP operations; a failed UXP mutation must never be silently
|
||||
retried through CEP.
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
# MCP for Adobe Premiere Pro UXP bridge
|
||||
|
||||
Production-oriented UXP transport for Premiere Pro 25.6+. Set `PREMIERE_UXP_TOKEN` to a secret of at least 16 characters before starting the MCP server. Side-load `manifest.json` with UXP Developer Tool during development, open **Window → UXP Plugins → MCP for Adobe Premiere Pro**, enter the same secret in **Bridge token**, then connect to the MCP-side WebSocket endpoint (default `ws://127.0.0.1:7777/uxp`). For end-user direct distribution, use the `.ccx` workflow in [DISTRIBUTION.md](./DISTRIBUTION.md) instead of asking users to load a development folder.
|
||||
|
||||
Before using a path-based command, choose one **Approved workspace** folder in the
|
||||
panel. The manifest uses requested filesystem access, and the panel restores only
|
||||
Adobe's opaque persistent folder token. Proxy, relink, preset, interchange, AAF,
|
||||
frame-export, and Source Monitor file paths must remain inside that folder. The
|
||||
token and native root path are never sent to the MCP server.
|
||||
|
||||
The bridge sends a versioned `hello`, subscribes to Premiere's documented global project and sequence events, emits `premiere.state.changed` notifications, and accepts only commands that its runtime capability probe declares supported. A five-second deduplicated poll remains as a fallback for state such as playhead movement that has no matching documented event.
|
||||
|
||||
It also exposes documented Premiere 25.6+ video-transition and transcript workflows:
|
||||
|
||||
- `timeline.selection.lift` removes the current timeline selection without ripple through one undoable transaction; the committed transaction is not a timeline readback
|
||||
- `transition.video.list`, `transition.video.add`, and `transition.video.remove`
|
||||
- `transcript.export`, `transcript.search`, `transcript.has`, and guarded undoable `transcript.import`
|
||||
- read-only `captions.inspect`
|
||||
|
||||
Transition names must come from `transition.video.list`. Transcript inspection targets can be selected in the Project panel or addressed by project-item ID/name. Transcript import is stricter: it requires an exact project GUID and project-item ID plus a current transcript SHA-256 (or an explicit absent-transcript precondition), confirmation, and an operation ID; it never uses selection or a name to mutate. Caption creation, text/timing mutation, and deletion remain explicitly unsupported because Premiere does not document those UXP APIs.
|
||||
|
||||
The MCP adapter exposes `transcript.export` as `get_clip_transcript_uxp` and adds a
|
||||
SHA-256 revision to the result. `search_clip_transcript_uxp` maps to the existing
|
||||
read-only search command. `preview_transcript_edit_uxp` re-exports the transcript,
|
||||
rejects stale revisions, validates and merges selected source-time deletion ranges,
|
||||
and returns a confirmation token. It never changes the transcript or timeline;
|
||||
automatic transcript-to-timeline application remains a live-host validation gate.
|
||||
`import_transcript_uxp` is the guarded MCP facade for `transcript.import`: it
|
||||
caps the replacement JSON at 24 KiB UTF-8, serializes import requests per source
|
||||
clip, commits one native transaction, and only calls the result verified after a
|
||||
bounded native export has the exact requested SHA-256. A failed post-commit
|
||||
readback remains `committed_unverified`; static tests do not prove host acceptance,
|
||||
persistence, Undo, or licensed-host behavior.
|
||||
|
||||
## Premiere 26.3 commands
|
||||
|
||||
The following commands use APIs Adobe introduced in Premiere 26.3. They are
|
||||
available only when the connected host advertises them through `capabilities.get`:
|
||||
|
||||
- `track.rename` — rename one `video`, `audio`, or `caption` track using an
|
||||
undoable `createSetNameAction` transaction. The post-transaction read-back is
|
||||
the verification evidence.
|
||||
- `subclip.create` — create a subclip from a clip project item with non-negative,
|
||||
ordered in/out points, hard-boundary selection, and optional audio/video
|
||||
inclusion. It is an undoable action and must return the created item identity.
|
||||
- `marker.list` — return sequence or resolved project-item markers, including the
|
||||
stable marker `guid` introduced in 26.3. It is read-only; documented web links
|
||||
and raw RGBA color components are opt-in, and raw color values make no
|
||||
color-profile or rendered-appearance claim.
|
||||
- `sourceMonitor.position.set` — set the Source Monitor using a `TickTime` and
|
||||
read the position back. It changes monitor state, not project edit history.
|
||||
- `transcript.has` — report whether a resolved `ClipProjectItem` has a
|
||||
transcript. It is read-only and does not start Speech-to-Text.
|
||||
- `transcript.import` — replace one exact clip transcript through a revision-locked,
|
||||
explicitly confirmed 26.3+ transaction with bounded export readback.
|
||||
- `interchange.aaf.export` — export the active sequence through
|
||||
`ProjectConverter.exportAAF` with a bounded, documented `AAFExportOptions`
|
||||
schema. Premiere's boolean result is recorded, but output-file inspection must
|
||||
still be completed by the host gate.
|
||||
|
||||
These map to MCP tools `rename_track_uxp`, `create_subclip_uxp`,
|
||||
`list_markers_uxp`, `set_source_monitor_position_uxp`, `has_transcript_uxp`, and
|
||||
`import_transcript_uxp`, and `export_aaf_uxp`. See [the 26.3 coverage matrix](../docs/adobe-uxp-26.3-coverage.md)
|
||||
for command arguments, support states, and primary Adobe references.
|
||||
|
||||
## Stable workflow commands
|
||||
|
||||
The panel also exposes runtime-probed commands for native audio/video component
|
||||
chains, deterministic timeline selection, compound effect batches over the current
|
||||
timeline selection, scene-edit
|
||||
detection, proxy and ingest state, guarded offline relink, transactional project/XMP
|
||||
metadata, footage interpretation and LUTs, Source Monitor audition, and
|
||||
project/Production scratch-disk preflight. They map to eleven consolidated MCP tools;
|
||||
see [the stable workflow matrix](../docs/uxp-stable-workflows.md) for exact commands,
|
||||
arguments, confirmation requirements, and the pending live-host gate.
|
||||
|
||||
A second stable expansion adds Project-view selection, native marker CRUD, bin
|
||||
organization, sequence settings profiles, workspace-gated imports, typed parameter,
|
||||
keyframe, and animation-mode automation, track-item transformations, documented SequenceEditor
|
||||
edits, sequence lifecycle management, and AME encoding. These map to another ten
|
||||
consolidated MCP tools and 45 capability-probed panel commands. See [the next-ten
|
||||
workflow matrix](../docs/uxp-next-ten-workflows.md). Automated contract evidence is
|
||||
complete; live Premiere verification remains pending.
|
||||
|
||||
The third-wave command surface adds bounded event and AME receipts, readiness waits,
|
||||
safe multi-project sessions, a growing-media pause lease, workflow checkpoints,
|
||||
media-health maintenance, caption-aware track state, and transactional source
|
||||
trim/framing. See [the third-wave workflow matrix](../docs/third-wave-uxp-workflows.md)
|
||||
for the verification boundary of every operation.
|
||||
|
||||
`hybrid-benchmark.cjs` is a dormant developer benchmark. The production manifest
|
||||
does not request `enableAddon`, declare an addon, or ship a native binary. A future
|
||||
native-acceleration change must first pass the documented Windows x64, macOS x64,
|
||||
and macOS arm64 Release evidence gate; see
|
||||
[the hybrid benchmark procedure](../docs/uxp-hybrid-benchmark.md).
|
||||
|
||||
`frame.export` uses Adobe's supported `Exporter.exportSequenceFrame()`. The public request and returned path include one `.png` extension; the panel passes the bare stem to Premiere because its exporter appends the extension. Premiere must confirm the export request before a path is reported. A licensed-host file check remains required to prove the artifact exists on disk. Example:
|
||||
|
||||
```json
|
||||
{"type":"command","requestId":"42","command":"frame.export","args":{"outputDirectory":"C:/temp","filename":"frame.png"}}
|
||||
```
|
||||
|
||||
## Operation lifecycle and guarantees
|
||||
|
||||
Commands emit correlated lifecycle events:
|
||||
|
||||
- `premiere.operation.started`
|
||||
- `premiere.operation.progress`
|
||||
- `premiere.operation.completed`
|
||||
- `premiere.operation.failed`
|
||||
- `premiere.operation.cancelled`
|
||||
|
||||
Each result includes structured operation metadata covering project mutation, verification evidence, undo, transaction, and cancellation boundaries. These fields describe evidence, not intent:
|
||||
|
||||
- Frame export is not a project mutation, is not undoable, and verifies success by checking the output file.
|
||||
- `operation.cancel` is cooperative during preflight. Once a Premiere host call begins, the bridge reports `host_call_not_cancellable` and does not claim interruption.
|
||||
- Action-based commands use `project.lockedAccess()` to create the action and
|
||||
`project.executeTransaction()` to consume it without asynchronous work or an
|
||||
escaped action object. Only those commands may claim a Premiere undo-history or
|
||||
transaction boundary.
|
||||
- The bridge does not claim atomic rollback. Failed operations must be inspected using their verification metadata before retrying.
|
||||
|
||||
Example cancellation request:
|
||||
|
||||
```json
|
||||
{"type":"command","requestId":"cancel-42","command":"operation.cancel","args":{"requestId":"42"}}
|
||||
```
|
||||
|
||||
## CEP fallback contract
|
||||
|
||||
Capability discovery is authoritative per host session. Route a command to CEP only when the UXP capability is absent or explicitly `supported: false`. Never replay a failed UXP mutation through CEP automatically: the first attempt may have partially succeeded. CEP/QE results must identify `backend: "cep"`, and callers should treat QE-only behavior as compatibility mode rather than equivalent proof. UXP remains authoritative for supported frame export.
|
||||
|
||||
Loopback WebSocket support must still be verified on each supported OS/Premiere combination. Until that evidence exists, keep the existing file bridge available as the transport fallback; transport fallback does not change command-backend selection.
|
||||
|
||||
The event and transaction boundaries follow Adobe's documented [EventManager](https://developer.adobe.com/premiere-pro/uxp/ppro-reference/classes/eventmanager), [Project.executeTransaction](https://developer.adobe.com/premiere-pro/uxp/ppro-reference/classes/project), and [CompoundAction](https://developer.adobe.com/premiere-pro/uxp/ppro-reference/classes/compoundaction) APIs.
|
||||
|
||||
Adobe's published [`@adobe/premierepro`](https://www.npmjs.com/package/@adobe/premierepro)
|
||||
26.3.0 package is the stable declaration baseline for this bridge. The `beta` tag
|
||||
(currently a 26.5 prerelease) is deliberately excluded from capability claims and
|
||||
release support until Adobe ships a stable host API and this panel passes the same
|
||||
live-host gate. The official [Premiere UXP ESLint rules](https://developer.adobe.com/premiere-pro/uxp/resources/fundamentals/eslint-support/)
|
||||
are the intended static guard against lock, transaction, async-callback, and
|
||||
action-lifetime violations; static lint and unit tests are not Premiere-host proof.
|
||||
|
||||
## MCP-side transport
|
||||
|
||||
The server listener binds only to `127.0.0.1`, requires the token during the WebSocket upgrade, limits messages to 1 MiB, and requires a versioned UXP `hello` before accepting results. Requests are correlated by random IDs and fail on timeout or disconnect.
|
||||
|
||||
When `PREMIERE_UXP_TOKEN` is configured, the MCP server registers only commands implemented by this panel. These tools never silently replay a failed UXP request through CEP. `PREMIERE_UXP_PORT` changes the loopback port when `7777` is unavailable. The server adapter accepts protocol versions 1 and 2; this integrated panel emits protocol version 2.
|
||||
+2382
File diff suppressed because it is too large
Load Diff
+344
@@ -0,0 +1,344 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpCloneWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// This is intentionally an append-only clone. SequenceEditor can clone at
|
||||
// arbitrary offsets and tracks, but a coordinate-bound append lets the
|
||||
// bridge prove both the original and exactly one new item without scanning
|
||||
// or overwriting unrelated timeline content.
|
||||
function createCloneWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const fallbackTails = new Map();
|
||||
const locks = deps.locks && typeof deps.locks.withTrackMutationLock === "function"
|
||||
? deps.locks
|
||||
: { withTrackMutationLock: localLock };
|
||||
const definitions = {
|
||||
"trackItem.clone.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTrackItemClone,
|
||||
handler: inspectTrackItemClone
|
||||
},
|
||||
"trackItem.clone": {
|
||||
destructive: true,
|
||||
undoable: true,
|
||||
idempotent: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTrackItemClone,
|
||||
handler: cloneTrackItem
|
||||
}
|
||||
};
|
||||
|
||||
function canUseTrackItemClone() {
|
||||
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.SequenceEditor && typeof ppro.SequenceEditor.getEditor === "function" &&
|
||||
ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function");
|
||||
}
|
||||
|
||||
async function inspectTrackItemClone(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
|
||||
const context = await activeTarget(args, false);
|
||||
const snapshot = await cloneSnapshot(context);
|
||||
assertAppendSupported(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async function cloneTrackItem(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSnapshot", "confirmDuplicate", "operationId"]);
|
||||
requireConfirmation(args.confirmDuplicate);
|
||||
requireOperationId(args.operationId);
|
||||
const target = targetCoordinates(args);
|
||||
const expected = requiredSnapshot(args.expectedSnapshot);
|
||||
assertExpectedTarget(expected, target);
|
||||
const initial = await activeTarget(target, true);
|
||||
if (initial.projectGuid !== expected.projectGuid || initial.sequenceId !== expected.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence no longer matches the reviewed duplicate snapshot");
|
||||
}
|
||||
const key = initial.projectGuid + "\u0000" + initial.sequenceId + "\u0000" + target.mediaType + "\u0000" + target.trackIndex;
|
||||
return locks.withTrackMutationLock(key, async function () {
|
||||
// Read only after entering the same track tail used by the guarded
|
||||
// slip and slide workflows. A distinct operation ID cannot pass an
|
||||
// earlier inspected coordinate into action creation.
|
||||
const context = await activeTarget(target, true);
|
||||
if (context.projectGuid !== expected.projectGuid || context.sequenceId !== expected.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence changed before the duplicate transaction");
|
||||
}
|
||||
const before = await cloneSnapshot(context);
|
||||
if (!sameSnapshot(before, expected)) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed source item or append boundary changed since inspection");
|
||||
}
|
||||
assertAppendSupported(before);
|
||||
let committed = false;
|
||||
context.project.lockedAccess(function () {
|
||||
// The complete asynchronous snapshot is immediately followed by
|
||||
// this synchronous check/action construction boundary. This avoids
|
||||
// creating an Action from a stale active project or sequence.
|
||||
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed project or sequence changed before duplicate action creation");
|
||||
}
|
||||
const editor = ppro.SequenceEditor.getEditor(context.sequence);
|
||||
if (!editor || typeof editor.createCloneTrackItemAction !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose SequenceEditor clone actions");
|
||||
}
|
||||
const action = editor.createCloneTrackItemAction(
|
||||
context.item,
|
||||
ppro.TickTime.createWithSeconds(before.source.durationSeconds),
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
true
|
||||
);
|
||||
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the duplicate action");
|
||||
committed = context.project.executeTransaction(function (compoundAction) {
|
||||
if (compoundAction.addAction(action) === false) {
|
||||
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the duplicate action");
|
||||
}
|
||||
}, "Duplicate timeline item after source");
|
||||
});
|
||||
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the duplicate transaction");
|
||||
|
||||
// Re-resolve by the original coordinate plus the one deterministic
|
||||
// append coordinate. Do not walk or read unrelated item fields after
|
||||
// a committed action.
|
||||
const afterContext = await activeTarget(target, true);
|
||||
if (afterContext.projectGuid !== before.projectGuid || afterContext.sequenceId !== before.sequenceId) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere changed the active project or sequence during the committed duplicate");
|
||||
}
|
||||
const after = await cloneReadback(afterContext, before);
|
||||
if (!sameCloneResult(before, after)) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested append-only duplicate");
|
||||
}
|
||||
return {
|
||||
duplicated: true,
|
||||
before,
|
||||
after,
|
||||
outcome: "verified",
|
||||
verificationBoundary: "source_and_appended_track_item_readback",
|
||||
undoLabel: "Duplicate timeline item after source"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function activeTarget(args, requireTransaction) {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
|
||||
}
|
||||
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
||||
const target = targetCoordinates(args), resolved = await trackItemsAt(sequence, target);
|
||||
const item = resolved.items[target.clipIndex];
|
||||
if (!item) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
|
||||
return {
|
||||
project,
|
||||
sequence,
|
||||
projectGuid: requiredGuid(project.guid, "active project GUID"),
|
||||
sequenceId: requiredGuid(sequence.guid, "active sequence GUID"),
|
||||
...target,
|
||||
item,
|
||||
items: resolved.items
|
||||
};
|
||||
}
|
||||
|
||||
async function trackItemsAt(sequence, target) {
|
||||
const title = target.mediaType === "video" ? "Video" : "Audio";
|
||||
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
|
||||
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
|
||||
}
|
||||
const count = Number(await sequence[countMethod]());
|
||||
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
|
||||
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
|
||||
const track = await sequence[trackMethod](target.trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
|
||||
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
|
||||
}
|
||||
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
|
||||
if (!items.length) throw commandError("UXP_TARGET_NOT_FOUND", "The target track has no clip items");
|
||||
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
|
||||
return { items };
|
||||
}
|
||||
|
||||
async function cloneSnapshot(context) {
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
sequenceId: context.sequenceId,
|
||||
mediaType: context.mediaType,
|
||||
trackIndex: context.trackIndex,
|
||||
clipIndex: context.clipIndex,
|
||||
trackItemCount: context.items.length,
|
||||
source: await itemSnapshot(context.item, "source")
|
||||
};
|
||||
}
|
||||
|
||||
async function cloneReadback(context, before) {
|
||||
const items = context.items;
|
||||
const source = items[before.clipIndex], duplicate = items[before.clipIndex + 1];
|
||||
if (!source || !duplicate) throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not expose both source and appended duplicate coordinates");
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
sequenceId: context.sequenceId,
|
||||
mediaType: context.mediaType,
|
||||
trackIndex: context.trackIndex,
|
||||
sourceClipIndex: before.clipIndex,
|
||||
duplicateClipIndex: before.clipIndex + 1,
|
||||
trackItemCount: items.length,
|
||||
source: await itemSnapshot(source, "source readback"),
|
||||
duplicate: await itemSnapshot(duplicate, "duplicate readback")
|
||||
};
|
||||
}
|
||||
|
||||
async function itemSnapshot(item, label) {
|
||||
const sourceItem = await requiredMethod(item, "getProjectItem")();
|
||||
const snapshot = {
|
||||
projectItemId: requiredIdentifier(await requiredMethod(sourceItem, "getId")(), label + " source project-item ID"),
|
||||
startSeconds: tickSeconds(await requiredMethod(item, "getStartTime")()),
|
||||
endSeconds: tickSeconds(await requiredMethod(item, "getEndTime")()),
|
||||
inSeconds: tickSeconds(await requiredMethod(item, "getInPoint")()),
|
||||
outSeconds: tickSeconds(await requiredMethod(item, "getOutPoint")()),
|
||||
durationSeconds: tickSeconds(await requiredMethod(item, "getDuration")()),
|
||||
speed: Number(await requiredMethod(item, "getSpeed")()),
|
||||
reversed: Boolean(await requiredMethod(item, "isSpeedReversed")())
|
||||
};
|
||||
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds"]) {
|
||||
if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0 || snapshot[key] > 86400) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " " + key);
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(snapshot.speed) || snapshot.speed < 0 || snapshot.speed > 100) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " speed");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function assertAppendSupported(snapshot) {
|
||||
if (snapshot.clipIndex !== snapshot.trackItemCount - 1) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "Duplicate only appends a copy after the final clip item on the requested track");
|
||||
}
|
||||
const source = snapshot.source;
|
||||
if (source.durationSeconds <= 0 || !numbersEqual(source.endSeconds - source.startSeconds, source.durationSeconds) ||
|
||||
source.endSeconds + source.durationSeconds > 86400) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "The source item cannot be safely appended within the documented timeline bounds");
|
||||
}
|
||||
}
|
||||
|
||||
function sameCloneResult(before, after) {
|
||||
const source = before.source, duplicate = after.duplicate;
|
||||
return after.projectGuid === before.projectGuid && after.sequenceId === before.sequenceId &&
|
||||
after.mediaType === before.mediaType && after.trackIndex === before.trackIndex &&
|
||||
after.sourceClipIndex === before.clipIndex && after.duplicateClipIndex === before.clipIndex + 1 &&
|
||||
after.trackItemCount === before.trackItemCount + 1 && sameItem(after.source, source) &&
|
||||
duplicate.projectItemId === source.projectItemId &&
|
||||
numbersEqual(duplicate.startSeconds, source.endSeconds) &&
|
||||
numbersEqual(duplicate.endSeconds, source.endSeconds + source.durationSeconds) &&
|
||||
numbersEqual(duplicate.durationSeconds, source.durationSeconds) &&
|
||||
numbersEqual(duplicate.inSeconds, source.inSeconds) && numbersEqual(duplicate.outSeconds, source.outSeconds) &&
|
||||
numbersEqual(duplicate.speed, source.speed) && duplicate.reversed === source.reversed;
|
||||
}
|
||||
|
||||
function sameSnapshot(left, right) {
|
||||
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId &&
|
||||
left.mediaType === right.mediaType && left.trackIndex === right.trackIndex &&
|
||||
left.clipIndex === right.clipIndex && left.trackItemCount === right.trackItemCount &&
|
||||
sameItem(left.source, right.source);
|
||||
}
|
||||
|
||||
function sameItem(left, right) {
|
||||
return left.projectItemId === right.projectItemId && numbersEqual(left.startSeconds, right.startSeconds) &&
|
||||
numbersEqual(left.endSeconds, right.endSeconds) && numbersEqual(left.inSeconds, right.inSeconds) &&
|
||||
numbersEqual(left.outSeconds, right.outSeconds) && numbersEqual(left.durationSeconds, right.durationSeconds) &&
|
||||
numbersEqual(left.speed, right.speed) && left.reversed === right.reversed;
|
||||
}
|
||||
|
||||
function targetCoordinates(args) {
|
||||
return {
|
||||
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
|
||||
trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"),
|
||||
clipIndex: nonNegativeInt(args.clipIndex, "clipIndex")
|
||||
};
|
||||
}
|
||||
|
||||
function requiredSnapshot(value) {
|
||||
assertObject(value, "expectedSnapshot");
|
||||
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "trackItemCount", "source"];
|
||||
assertOnlyKeys(value, allowed);
|
||||
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + key + " is required");
|
||||
return {
|
||||
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
|
||||
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
|
||||
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]),
|
||||
trackIndex: nonNegativeInt(value.trackIndex, "expectedSnapshot.trackIndex"),
|
||||
clipIndex: nonNegativeInt(value.clipIndex, "expectedSnapshot.clipIndex"),
|
||||
trackItemCount: positiveInt(value.trackItemCount, "expectedSnapshot.trackItemCount"),
|
||||
source: requiredItemSnapshot(value.source, "expectedSnapshot.source")
|
||||
};
|
||||
}
|
||||
|
||||
function requiredItemSnapshot(value, name) {
|
||||
assertObject(value, name);
|
||||
const allowed = ["projectItemId", "startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds", "speed", "reversed"];
|
||||
assertOnlyKeys(value, allowed);
|
||||
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", name + "." + key + " is required");
|
||||
return {
|
||||
projectItemId: requiredIdentifier(value.projectItemId, name + ".projectItemId"),
|
||||
startSeconds: boundedSeconds(value.startSeconds, name + ".startSeconds"),
|
||||
endSeconds: boundedSeconds(value.endSeconds, name + ".endSeconds"),
|
||||
inSeconds: boundedSeconds(value.inSeconds, name + ".inSeconds"),
|
||||
outSeconds: boundedSeconds(value.outSeconds, name + ".outSeconds"),
|
||||
durationSeconds: boundedSeconds(value.durationSeconds, name + ".durationSeconds"),
|
||||
speed: boundedSpeed(value.speed, name + ".speed"),
|
||||
reversed: requiredBoolean(value.reversed, name + ".reversed")
|
||||
};
|
||||
}
|
||||
|
||||
function assertExpectedTarget(expected, target) {
|
||||
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot target coordinates must match mediaType, trackIndex, and clipIndex");
|
||||
}
|
||||
}
|
||||
|
||||
function localLock(key, operation) {
|
||||
const previous = fallbackTails.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const gate = new Promise(function (resolve) { release = resolve; });
|
||||
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
|
||||
fallbackTails.set(key, tail);
|
||||
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
|
||||
release();
|
||||
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
function requiredMethod(target, name) { if (!target || typeof target[name] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The required item does not expose " + name); return target[name].bind(target); }
|
||||
function assertObject(value, name) { if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an object"); }
|
||||
function assertOnlyKeys(value, allowed) { assertObject(value, "arguments"); for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw commandError("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key); }
|
||||
function enumValue(value, name, allowed) { if (allowed.indexOf(value) === -1) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; }
|
||||
function nonNegativeInt(value, name) { if (!Number.isSafeInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 to 511"); return value; }
|
||||
function positiveInt(value, name) { if (!Number.isSafeInteger(value) || value < 1 || value > 512) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 1 to 512"); return value; }
|
||||
function boundedSeconds(value, name) { const seconds = Number(value); if (!Number.isFinite(seconds) || seconds < 0 || seconds > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400"); return seconds; }
|
||||
function boundedSpeed(value, name) { const speed = Number(value); if (!Number.isFinite(speed) || speed < 0 || speed > 100) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 100"); return speed; }
|
||||
function requiredBoolean(value, name) { if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean"); return value; }
|
||||
function requireConfirmation(value) { if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "trackItem.clone requires confirmDuplicate: true"); }
|
||||
function requireOperationId(value) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters"); }
|
||||
function requiredGuid(value, name) { const guid = guidString(value); if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return guid; }
|
||||
function requiredIdentifier(value, name) { const id = guidString(value); if (!id || id.length > 512 || id.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return id; }
|
||||
function guidString(value) { if (value == null) return ""; try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; } }
|
||||
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
|
||||
function numbersEqual(left, right) { return Number.isFinite(Number(left)) && Number.isFinite(Number(right)) && Math.abs(Number(left) - Number(right)) < 0.000001; }
|
||||
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
return { createCloneWorkflowDefinitions };
|
||||
});
|
||||
+1727
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpDialogueWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
function createDialogueWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
return { "dialogue.deriveSequence": { destructive: true, undoable: false, idempotent: true, targetCapabilityProbe: true, minHostVersion: "26.3.0", probe: canDerive, handler: derive } };
|
||||
|
||||
function canDerive() { return !!(ppro.Project && ppro.Project.getActiveProject && ppro.SequenceEditor && ppro.SequenceEditor.getEditor && ppro.TickTime && ppro.TickTime.createWithSeconds && ppro.ClipProjectItem && ppro.ClipProjectItem.cast); }
|
||||
async function derive(args) {
|
||||
only(args, ["plan", "operationId"]); text(args.operationId, "operationId", 128, /^[A-Za-z0-9._:-]+$/);
|
||||
const plan = validate(args.plan), project = await ppro.Project.getActiveProject();
|
||||
if (!project) fail("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
if (!project.lockedAccess || !project.executeTransaction || !project.createSequenceFromMedia) fail("UXP_COMMAND_UNAVAILABLE", "Required sequence and Action APIs are unavailable");
|
||||
if (await id(project) !== plan.project_guid) fail("UXP_STALE_PROJECT", "The active project changed after preview");
|
||||
const items = new Map();
|
||||
for (const segment of plan.segments) items.set(segment.source_project_item_id, await find(project, segment.source_project_item_id));
|
||||
if (plan.master_audio_project_item_id) items.set(plan.master_audio_project_item_id, await find(project, plan.master_audio_project_item_id));
|
||||
for (const [itemId, item] of items) { const clip = asClip(item, itemId); if (clip.isMulticamClip && await clip.isMulticamClip()) fail("UXP_UNSUPPORTED_MULTICAM", "Native multicam sources are unsupported; use reviewed ordinary sources"); if (!clip.createSubClipAction) fail("UXP_COMMAND_UNAVAILABLE", "Source cannot create subclips: " + itemId); }
|
||||
const sequencesBefore = await sequences(project);
|
||||
if (sequencesBefore.some(function (entry) { return entry.name === plan.sequence_name; })) fail("UXP_NAME_CONFLICT", "The reviewed sequence name already exists");
|
||||
const target = plan.target_bin_id ? asFolder(await find(project, plan.target_bin_id), "target_bin_id") : undefined;
|
||||
const parents = new Map(), beforeIds = new Set();
|
||||
for (const [itemId, item] of items) {
|
||||
const parent = asFolder(await asClip(item, itemId).getParentBin(), "source parent");
|
||||
const parentId = await id(parent); parents.set(parentId || itemId, parent);
|
||||
}
|
||||
for (const parent of parents.values()) for (const item of Array.from(await parent.getItems() || [])) beforeIds.add(await id(item));
|
||||
const prefix = plan.sequence_name + " Segment"; let attempted = false;
|
||||
try {
|
||||
project.lockedAccess(function () {
|
||||
const actions = [];
|
||||
plan.segments.forEach(function (segment, index) {
|
||||
actions.push(asClip(items.get(segment.source_project_item_id), "source").createSubClipAction(prefix + " V" + (index + 1), tick(segment.source_start_seconds), tick(segment.source_end_seconds), true, plan.mode === "podcast" ? { takeVideo: true, takeAudio: false } : { takeVideo: true, takeAudio: true }));
|
||||
if (plan.mode === "podcast") actions.push(asClip(items.get(plan.master_audio_project_item_id), "master audio").createSubClipAction(prefix + " A" + (index + 1), tick(segment.master_audio_start_seconds), tick(segment.master_audio_end_seconds), true, { takeVideo: false, takeAudio: true }));
|
||||
});
|
||||
attempted = true; commit(project, "Create reviewed dialogue subclips", actions);
|
||||
});
|
||||
} catch (error) { if (!attempted) throw error; return partial("dialogue_subclip_transaction_receipt", [], null, []); }
|
||||
const added = []; for (const parent of parents.values()) for (const item of Array.from(await parent.getItems() || [])) if (!beforeIds.has(await id(item))) added.push(item);
|
||||
const videos = [], audios = [];
|
||||
for (let i = 0; i < plan.segments.length; i++) { videos.push(added.find(function (x) { return String(x.name || "") === prefix + " V" + (i + 1); })); if (plan.mode === "podcast") audios.push(added.find(function (x) { return String(x.name || "") === prefix + " A" + (i + 1); })); }
|
||||
if (videos.some(function (x) { return !x; }) || audios.some(function (x) { return !x; })) return partial("dialogue_subclip_identity_readback", added, null, []);
|
||||
let sequence; try { sequence = await project.createSequenceFromMedia(plan.sequence_name, [videos[0]], target); } catch (_) { sequence = await addedSequence(project, sequencesBefore); }
|
||||
if (!sequence) return partial("dialogue_sequence_host_return", added, null, []);
|
||||
const inserted = [await id(videos[0])];
|
||||
try {
|
||||
const editor = ppro.SequenceEditor.getEditor(sequence); if (!editor || !editor.createInsertProjectItemAction) throw new Error("SequenceEditor unavailable");
|
||||
let offset = 0;
|
||||
for (let i = 0; i < plan.segments.length; i++) {
|
||||
if (i) { project.lockedAccess(function () { commit(project, "Insert reviewed dialogue video", [editor.createInsertProjectItemAction(videos[i], tick(offset), 0, 0, false)]); }); inserted.push(await id(videos[i])); }
|
||||
if (plan.mode === "podcast") { project.lockedAccess(function () { commit(project, "Insert reviewed dialogue audio", [editor.createInsertProjectItemAction(audios[i], tick(offset), 0, 0, false)]); }); inserted.push(await id(audios[i])); }
|
||||
offset += plan.segments[i].source_end_seconds - plan.segments[i].source_start_seconds;
|
||||
}
|
||||
} catch (_) { return partial("dialogue_sequence_partial_insert_receipt", added, sequence, inserted); }
|
||||
return receipt({ created: true, partial: false, sequence: await snap(sequence), createdSubclips: await snaps(videos.concat(audios)), insertedProjectItemIds: inserted, mode: plan.mode, outputDurationSeconds: plan.output_duration_seconds, originalSourcesChanged: false, renderVerified: false }, "dialogue_sequence_structure_unverified");
|
||||
}
|
||||
function validate(value) {
|
||||
only(value, ["schema_version", "project_guid", "mode", "sequence_name", "target_bin_id", "master_audio_project_item_id", "segments", "output_duration_seconds", "original_sources_unchanged", "render_verified"]);
|
||||
if (value.schema_version !== 1 || ["talking_head", "podcast"].indexOf(value.mode) < 0) fail("UXP_INVALID_ARGUMENT", "Unsupported dialogue plan");
|
||||
text(value.project_guid, "project_guid", 512); text(value.sequence_name, "sequence_name", 255);
|
||||
if (!Array.isArray(value.segments) || !value.segments.length || value.segments.length > 64) fail("UXP_INVALID_ARGUMENT", "segments must contain 1-64 entries");
|
||||
if (value.original_sources_unchanged !== true || value.render_verified !== false) fail("UXP_INVALID_ARGUMENT", "Original-source and render boundaries are invalid");
|
||||
if (value.mode === "podcast") text(value.master_audio_project_item_id, "master_audio_project_item_id", 512);
|
||||
value.segments.forEach(function (s, i) { only(s, ["id", "source_project_item_id", "transcript_revision", "source_start_seconds", "source_end_seconds", "speaker_label", "master_audio_start_seconds", "master_audio_end_seconds"]); text(s.id, "id", 128); text(s.source_project_item_id, "source_project_item_id", 512); const a = secs(s.source_start_seconds), b = secs(s.source_end_seconds); if (b <= a) fail("UXP_INVALID_ARGUMENT", "Segment " + i + " has no duration"); if (value.mode === "podcast") { const c = secs(s.master_audio_start_seconds), d = secs(s.master_audio_end_seconds); if (d <= c || Math.abs((d-c)-(b-a)) > .001) fail("UXP_INVALID_ARGUMENT", "Podcast audio mismatch at segment " + i); } });
|
||||
return value;
|
||||
}
|
||||
async function find(project, wanted) { const queue = [await project.getRootItem()]; let count = 0; while (queue.length) { const item = queue.shift(); if (++count > 4096) fail("UXP_PROJECT_TOO_LARGE", "Project search exceeded 4096 items"); if (await id(item) === wanted) return item; if (item && item.getItems) queue.push.apply(queue, Array.from(await item.getItems() || [])); } fail("UXP_PROJECT_ITEM_NOT_FOUND", "Project item not found: " + wanted); }
|
||||
async function sequences(project) { const out = []; for (const item of Array.from(await project.getSequences() || [])) out.push(await snap(item)); return out; }
|
||||
async function addedSequence(project, before) { const known = new Set(before.map(function (x) { return x.id; })); for (const item of Array.from(await project.getSequences() || [])) if (!known.has(await id(item))) return item; return null; }
|
||||
function commit(project, label, actions) {
|
||||
// Callers construct actions and invoke this helper inside lockedAccess.
|
||||
// eslint-disable-next-line @adobe/premierepro/prefer-locked-access-wrapper
|
||||
const ok = project.executeTransaction(function (compound) { actions.forEach(function (action) { if (!action || compound.addAction(action) === false) fail("UXP_ACTION_REJECTED", "Premiere rejected " + label); }); }, label);
|
||||
if (!ok) fail("UXP_TRANSACTION_FAILED", "Premiere did not commit " + label);
|
||||
}
|
||||
function asClip(item, name) { try { const cast = ppro.ClipProjectItem.cast(item); if (cast) return cast; } catch (_) {} fail("UXP_INVALID_PROJECT_ITEM", name + " is not a clip"); }
|
||||
function asFolder(item, name) { if (ppro.FolderItem && ppro.FolderItem.cast) try { const cast = ppro.FolderItem.cast(item); if (cast) return cast; } catch (_) {} if (item && item.getItems) return item; fail("UXP_INVALID_PROJECT_ITEM", name + " is not a folder"); }
|
||||
function tick(value) { return ppro.TickTime.createWithSeconds(secs(value)); }
|
||||
async function id(item) { if (!item) return ""; if (item.getId) return String(await item.getId()); if (item.getGuid) return String(await item.getGuid()); return ""; }
|
||||
async function snap(item) { return item ? { id: await id(item), name: String(item.name || "") } : null; }
|
||||
async function snaps(items) { const out = []; for (const item of items) out.push(await snap(item)); return out; }
|
||||
function partial(boundary, items, sequence, inserted) { return Promise.all([snaps(items), snap(sequence)]).then(function (x) { return receipt({ created: !!sequence, partial: true, createdSubclips: x[0], sequence: x[1], insertedProjectItemIds: inserted, originalSourcesChanged: false, renderVerified: false }, boundary); }); }
|
||||
function receipt(values, boundary) { return Object.assign({ outcome: "committed_unverified", verified: false, verificationBoundary: boundary }, values); }
|
||||
function only(value, keys) { if (!value || typeof value !== "object" || Array.isArray(value)) fail("UXP_INVALID_ARGUMENT", "Arguments must be an object"); Object.keys(value).forEach(function (key) { if (keys.indexOf(key) < 0) fail("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key); }); }
|
||||
function text(value, name, max, pattern) { if (typeof value !== "string" || !value || value.length > max || (pattern && !pattern.test(value))) fail("UXP_INVALID_ARGUMENT", name + " is invalid"); return value; }
|
||||
function secs(value) { if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 86400) fail("UXP_INVALID_ARGUMENT", "Time must be 0-86400 seconds"); return value; }
|
||||
function fail(code, message) { const error = new Error(message); error.code = code; throw error; }
|
||||
}
|
||||
return { createDialogueWorkflowDefinitions: createDialogueWorkflowDefinitions };
|
||||
});
|
||||
+229
@@ -0,0 +1,229 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpEffectParameterCatalogWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// A caller can already inspect a known parameter through the automation
|
||||
// workflow. This catalog fills the discovery gap without returning parameter
|
||||
// values, which may be arbitrary PointF/Color objects or user content.
|
||||
function createEffectParameterCatalogWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const MAX_TRACK_ITEMS = 512;
|
||||
const MAX_COMPONENTS = 512;
|
||||
const MAX_PARAMETERS = 64;
|
||||
const definitions = {
|
||||
"parameters.catalog.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canInspectParameterCatalog,
|
||||
handler: inspectParameterCatalog
|
||||
}
|
||||
};
|
||||
|
||||
function canInspectParameterCatalog() {
|
||||
return !!(ppro && ppro.Project && typeof ppro.Project.getActiveProject === "function");
|
||||
}
|
||||
|
||||
async function inspectParameterCatalog(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "componentIndex", "expectedSequenceGuid", "expectedComponentId"]);
|
||||
const input = coordinateInput(args);
|
||||
const first = await catalogSnapshot(input);
|
||||
assertExpected(input.expectedSequenceGuid, first.sequenceGuid, "UXP_STALE_PARAMETER_CATALOG", "Active sequence GUID");
|
||||
assertExpected(input.expectedComponentId, first.component.id, "UXP_STALE_PARAMETER_CATALOG", "Component identity");
|
||||
const second = await catalogSnapshot(input);
|
||||
if (!sameSnapshot(first, second)) {
|
||||
throw commandError("UXP_STALE_PARAMETER_CATALOG", "The project, active sequence, component, or parameter descriptors changed while being inspected; retry the catalog inspection");
|
||||
}
|
||||
return {
|
||||
projectGuid: first.projectGuid,
|
||||
sequenceGuid: first.sequenceGuid,
|
||||
mediaType: input.mediaType,
|
||||
trackIndex: input.trackIndex,
|
||||
clipIndex: input.clipIndex,
|
||||
componentIndex: input.componentIndex,
|
||||
component: first.component,
|
||||
parameterCount: first.parameters.length,
|
||||
parameters: first.parameters,
|
||||
verificationBoundary: "bounded_effect_parameter_catalog_double_readback"
|
||||
};
|
||||
}
|
||||
|
||||
function coordinateInput(args) {
|
||||
return {
|
||||
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
|
||||
trackIndex: boundedIndex(args.trackIndex, "trackIndex"),
|
||||
clipIndex: boundedIndex(args.clipIndex, "clipIndex"),
|
||||
componentIndex: boundedIndex(args.componentIndex, "componentIndex"),
|
||||
expectedSequenceGuid: args.expectedSequenceGuid == null ? null : requestedGuid(args.expectedSequenceGuid, "expectedSequenceGuid"),
|
||||
expectedComponentId: args.expectedComponentId == null ? null : boundedText(args.expectedComponentId, "expectedComponentId", 512, false)
|
||||
};
|
||||
}
|
||||
|
||||
async function catalogSnapshot(input) {
|
||||
const project = await activeProject();
|
||||
const projectGuid = requiredGuid(project.guid, "active project GUID");
|
||||
if (typeof project.getActiveSequence !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose the active sequence for parameter catalog inspection");
|
||||
}
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
||||
const sequenceGuid = requiredGuid(sequence.guid, "active sequence GUID");
|
||||
const item = await trackItemAt(sequence, input.mediaType, input.trackIndex, input.clipIndex);
|
||||
const component = await componentAt(item, input.componentIndex);
|
||||
const componentSnapshot = await componentDescriptor(component);
|
||||
const parameterCount = boundedCount(component.getParamCount && component.getParamCount(), "component parameter count", MAX_PARAMETERS);
|
||||
if (typeof component.getParam !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose component parameter lookup");
|
||||
}
|
||||
const parameters = [];
|
||||
for (let index = 0; index < parameterCount; index += 1) {
|
||||
parameters.push(await parameterDescriptor(component.getParam(index), index));
|
||||
}
|
||||
return { projectGuid, sequenceGuid, component: componentSnapshot, parameters };
|
||||
}
|
||||
|
||||
async function activeProject() {
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
return project;
|
||||
}
|
||||
|
||||
async function trackItemAt(sequence, mediaType, trackIndex, clipIndex) {
|
||||
const countMethod = mediaType === "video" ? "getVideoTrackCount" : "getAudioTrackCount";
|
||||
const trackMethod = mediaType === "video" ? "getVideoTrack" : "getAudioTrack";
|
||||
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented " + mediaType + " track access");
|
||||
}
|
||||
const count = boundedCount(await sequence[countMethod](), mediaType + " track count", MAX_TRACK_ITEMS);
|
||||
if (trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", "trackIndex is out of range");
|
||||
const track = await sequence[trackMethod](trackIndex);
|
||||
const itemType = ppro.Constants && ppro.Constants.TrackItemType;
|
||||
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented clip-item access on the requested track");
|
||||
}
|
||||
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
|
||||
if (items.length > MAX_TRACK_ITEMS) {
|
||||
throw commandError("UXP_TARGET_TOO_LARGE", "The requested track has more than " + MAX_TRACK_ITEMS + " clip items");
|
||||
}
|
||||
if (!items[clipIndex]) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
|
||||
return items[clipIndex];
|
||||
}
|
||||
|
||||
async function componentAt(item, componentIndex) {
|
||||
if (!item || typeof item.getComponentChain !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "The requested clip does not expose a documented component chain");
|
||||
}
|
||||
const chain = await item.getComponentChain();
|
||||
if (!chain || typeof chain.getComponentCount !== "function" || typeof chain.getComponentAtIndex !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not return a documented component chain");
|
||||
}
|
||||
const count = boundedCount(chain.getComponentCount(), "component count", MAX_COMPONENTS);
|
||||
if (componentIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", "componentIndex is out of range");
|
||||
const component = chain.getComponentAtIndex(componentIndex);
|
||||
if (!component) throw commandError("UXP_TARGET_NOT_FOUND", "Premiere did not return the requested component");
|
||||
return component;
|
||||
}
|
||||
|
||||
async function componentDescriptor(component) {
|
||||
if (typeof component.getMatchName !== "function" || typeof component.getDisplayName !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented component identity getters");
|
||||
}
|
||||
const matchName = boundedText(await component.getMatchName(), "component match name", 512, true);
|
||||
const displayName = boundedText(await component.getDisplayName(), "component display name", 512, true);
|
||||
const id = matchName || displayName;
|
||||
if (!id) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned a component without a match or display name");
|
||||
return { id, matchName, displayName };
|
||||
}
|
||||
|
||||
async function parameterDescriptor(parameter, index) {
|
||||
if (!parameter || typeof parameter.displayName !== "string" || typeof parameter.areKeyframesSupported !== "function" || typeof parameter.isTimeVarying !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not expose documented parameter descriptor access");
|
||||
}
|
||||
const displayName = boundedText(parameter.displayName, "parameter display name", 512, true);
|
||||
const keyframesSupported = await parameter.areKeyframesSupported();
|
||||
const timeVarying = parameter.isTimeVarying();
|
||||
if (typeof keyframesSupported !== "boolean" || typeof timeVarying !== "boolean") {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid parameter animation descriptor");
|
||||
}
|
||||
return { index, displayName, keyframesSupported, timeVarying };
|
||||
}
|
||||
|
||||
function sameSnapshot(left, right) {
|
||||
if (!left || !right || left.projectGuid !== right.projectGuid || left.sequenceGuid !== right.sequenceGuid ||
|
||||
left.component.id !== right.component.id || left.component.matchName !== right.component.matchName ||
|
||||
left.component.displayName !== right.component.displayName || left.parameters.length !== right.parameters.length) return false;
|
||||
return left.parameters.every(function (parameter, index) {
|
||||
const other = right.parameters[index];
|
||||
return other && parameter.index === other.index && parameter.displayName === other.displayName &&
|
||||
parameter.keyframesSupported === other.keyframesSupported && parameter.timeVarying === other.timeVarying;
|
||||
});
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "arguments must be an object");
|
||||
const unknown = Object.keys(value).find(function (key) { return !allowed.includes(key); });
|
||||
if (unknown) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown);
|
||||
}
|
||||
|
||||
function enumValue(value, name, values) {
|
||||
if (typeof value !== "string" || !values.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be " + values.join(" or "));
|
||||
return value;
|
||||
}
|
||||
|
||||
function boundedIndex(value, name) {
|
||||
if (!Number.isInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 through 511");
|
||||
return value;
|
||||
}
|
||||
|
||||
function boundedCount(value, name, maximum) {
|
||||
const count = Number(value);
|
||||
if (!Number.isInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + name);
|
||||
if (count > maximum) throw commandError("UXP_TARGET_TOO_LARGE", "The " + name + " exceeds the " + maximum + " entry limit");
|
||||
return count;
|
||||
}
|
||||
|
||||
function requiredGuid(value, name) {
|
||||
const guid = boundedGuid(value, name);
|
||||
if (!guid) throw commandError("UXP_VERIFICATION_FAILED", name + " must be a bounded non-empty GUID");
|
||||
return guid;
|
||||
}
|
||||
|
||||
function boundedGuid(value, name) {
|
||||
let text = "";
|
||||
try { text = String(value && typeof value.toString === "function" ? value.toString() : value || ""); } catch (_) {}
|
||||
if (!text || text.length > 512) throw commandError("UXP_VERIFICATION_FAILED", name + " must be a bounded non-empty GUID");
|
||||
return text;
|
||||
}
|
||||
|
||||
function requestedGuid(value, name) {
|
||||
let text = "";
|
||||
try { text = String(value && typeof value.toString === "function" ? value.toString() : value || ""); } catch (_) {}
|
||||
if (!text || text.length > 512) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a bounded non-empty GUID");
|
||||
return text;
|
||||
}
|
||||
|
||||
function boundedText(value, name, maximum, allowEmpty) {
|
||||
if (typeof value !== "string" || value.length > maximum || (!allowEmpty && !value)) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", name + " must be a bounded " + (allowEmpty ? "string" : "non-empty string"));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function assertExpected(expected, actual, code, name) {
|
||||
if (expected != null && expected !== actual) throw commandError(code, name + " differs from the inspected value; retry the catalog inspection");
|
||||
}
|
||||
|
||||
function commandError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
return { createEffectParameterCatalogWorkflowDefinitions };
|
||||
});
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpEvents = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
const DEFAULT_CAPACITY = 512;
|
||||
const MAX_CAPACITY = 2048;
|
||||
const MAX_WAIT_MS = 60000;
|
||||
const MAX_RESULTS = 256;
|
||||
const MAX_ENCODE_JOBS = 64;
|
||||
const SAFE_DETAIL_KEYS = new Set([
|
||||
"state", "progress", "phase", "trackIndex", "mediaType", "attributed", "jobId"
|
||||
]);
|
||||
|
||||
// Snap events are notifications only. Keep their host constants and the
|
||||
// public journal names in one small, testable mapping so neither event
|
||||
// payloads nor any unrecognized host event can cross the bridge.
|
||||
function createTimelineSnapEventDefinitions(snapEvent) {
|
||||
if (!snapEvent || typeof snapEvent !== "object") return [];
|
||||
return [
|
||||
["timeline.snap.keyframe", snapEvent.EVENT_SNAP_TO_KEYFRAME],
|
||||
["timeline.snap.trackItem", snapEvent.EVENT_SNAP_TO_TRACKITEM],
|
||||
["timeline.snap.guides", snapEvent.EVENT_SNAP_TO_GUIDES],
|
||||
["timeline.snap.razor.playhead", snapEvent.EVENT_SNAP_RAZOR_TO_PLAYHEAD],
|
||||
["timeline.snap.razor.marker", snapEvent.EVENT_SNAP_RAZOR_TO_MARKER],
|
||||
["timeline.snap.playhead.trackItemEdge", snapEvent.EVENT_SNAP_PLAYHEAD_TO_TRACKITEM_EDGE]
|
||||
].filter(function (entry) {
|
||||
return typeof entry[1] === "string" && entry[1].length > 0;
|
||||
}).map(function (entry) {
|
||||
return {
|
||||
category: "timeline",
|
||||
name: entry[0],
|
||||
eventName: entry[1],
|
||||
stateInvalidating: false,
|
||||
coalesceKey: null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// These two root OperationCompleteEvent constants are not completion
|
||||
// attestations: one reports a clip's extend limit and the other reports an
|
||||
// effect drag-over. Keep them passive and redact the native event object.
|
||||
// Drag-over may be noisy, so retain only the most recent consecutive receipt.
|
||||
function createOperationBoundaryEventDefinitions(operationCompleteEvent) {
|
||||
if (!operationCompleteEvent || typeof operationCompleteEvent !== "object") return [];
|
||||
return [
|
||||
["operation.clip.extend.reached", operationCompleteEvent.EVENT_CLIP_EXTEND_REACHED, null],
|
||||
["operation.effect.drag.over", operationCompleteEvent.EVENT_EFFECT_DRAG_OVER, "operation.effect.drag.over"]
|
||||
].filter(function (entry) {
|
||||
return typeof entry[1] === "string" && entry[1].length > 0;
|
||||
}).map(function (entry) {
|
||||
return {
|
||||
category: "operation",
|
||||
name: entry[0],
|
||||
eventName: entry[1],
|
||||
stateInvalidating: false,
|
||||
coalesceKey: entry[2]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function createEventJournal(options) {
|
||||
const value = options || {};
|
||||
const capacity = boundedInteger(value.capacity == null ? DEFAULT_CAPACITY : value.capacity, "capacity", 16, MAX_CAPACITY);
|
||||
const now = typeof value.now === "function" ? value.now : function () { return Date.now(); };
|
||||
const setTimer = typeof value.setTimer === "function" ? value.setTimer : setTimeout;
|
||||
const clearTimer = typeof value.clearTimer === "function" ? value.clearTimer : clearTimeout;
|
||||
const entries = [];
|
||||
const waiters = new Set();
|
||||
const jobs = new Map();
|
||||
const jobWaiters = new Set();
|
||||
let revision = 0;
|
||||
let dropped = 0;
|
||||
let evictedThroughRevision = 0;
|
||||
let jobSequence = 0;
|
||||
let unattributedEncoderEvents = 0;
|
||||
let closed = false;
|
||||
|
||||
function append(input) {
|
||||
if (closed) return null;
|
||||
const event = normalizeEvent(input, ++revision, now());
|
||||
const previous = entries.length ? entries[entries.length - 1] : null;
|
||||
if (event.coalesceKey && previous && previous.coalesceKey === event.coalesceKey) {
|
||||
event.coalesced = (previous.coalesced || 0) + 1;
|
||||
entries[entries.length - 1] = event;
|
||||
} else {
|
||||
entries.push(event);
|
||||
}
|
||||
while (entries.length > capacity) {
|
||||
const evicted = entries.shift();
|
||||
if (evicted) evictedThroughRevision = Math.max(evictedThroughRevision, evicted.revision);
|
||||
dropped += 1;
|
||||
}
|
||||
settleWaiters();
|
||||
return publicEvent(event);
|
||||
}
|
||||
|
||||
function recordHostEvent(input) {
|
||||
const value = Object.assign({}, input, { detail: Object.assign({}, input && input.detail) });
|
||||
let attributedJob = null;
|
||||
if (value.category === "encoder") {
|
||||
const candidates = Array.from(jobs.values()).filter(function (job) { return !job.terminal; });
|
||||
if (candidates.length === 1) {
|
||||
attributedJob = candidates[0];
|
||||
value.detail.attributed = true;
|
||||
value.detail.jobId = attributedJob.jobId;
|
||||
} else {
|
||||
value.detail.attributed = false;
|
||||
unattributedEncoderEvents += 1;
|
||||
}
|
||||
}
|
||||
const receipt = append(value);
|
||||
if (attributedJob && receipt) applyEncoderReceipt(attributedJob, receipt);
|
||||
return receipt;
|
||||
}
|
||||
|
||||
function list(input) {
|
||||
const query = normalizeQuery(input);
|
||||
const oldestRevision = entries.length ? entries[0].revision : revision + 1;
|
||||
const overflow = query.afterRevision > 0 && query.afterRevision < evictedThroughRevision;
|
||||
const matching = entries.filter(function (event) {
|
||||
return event.revision > query.afterRevision && matches(event, query);
|
||||
}).slice(0, query.limit).map(publicEvent);
|
||||
return {
|
||||
latestRevision: revision,
|
||||
oldestRevision,
|
||||
dropped,
|
||||
overflow,
|
||||
timedOut: false,
|
||||
events: matching
|
||||
};
|
||||
}
|
||||
|
||||
function wait(input) {
|
||||
const query = normalizeQuery(input);
|
||||
const immediate = list(query);
|
||||
if (immediate.events.length || immediate.overflow || query.timeoutMs === 0 || closed) {
|
||||
return Promise.resolve(Object.assign(immediate, { closed }));
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
const waiter = { query, resolve, timer: null };
|
||||
waiter.timer = setTimer(function () {
|
||||
waiters.delete(waiter);
|
||||
resolve(Object.assign(list(query), { timedOut: true, closed }));
|
||||
}, query.timeoutMs);
|
||||
waiters.add(waiter);
|
||||
});
|
||||
}
|
||||
|
||||
function beginEncodeJob(input) {
|
||||
if (closed) throw trackerError("UXP_ENCODE_TRACKER_CLOSED", "Encode receipt tracking is closed");
|
||||
const value = input || {};
|
||||
const operationId = value.operationId == null ? null : boundedToken(value.operationId, "operationId");
|
||||
const jobId = operationId || ("encode-" + now() + "-" + (++jobSequence));
|
||||
if (jobs.has(jobId)) return publicJob(jobs.get(jobId));
|
||||
pruneJobs();
|
||||
if (jobs.size >= MAX_ENCODE_JOBS) throw trackerError("UXP_ENCODE_TRACKER_FULL", "Encode receipt tracker is full");
|
||||
const timestamp = new Date(now()).toISOString();
|
||||
const job = {
|
||||
jobId,
|
||||
operationId,
|
||||
kind: boundedToken(value.kind || "unknown", "kind"),
|
||||
state: "submitting",
|
||||
progress: null,
|
||||
hostEvent: null,
|
||||
eventRevision: null,
|
||||
terminal: false,
|
||||
terminalReason: null,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
jobs.set(jobId, job);
|
||||
return publicJob(job);
|
||||
}
|
||||
|
||||
function markEncodeAccepted(jobId) {
|
||||
const job = requiredJob(jobId);
|
||||
if (job.state === "submitting") updateJob(job, { state: "accepted" });
|
||||
return publicJob(job);
|
||||
}
|
||||
|
||||
function markEncodeRejected(jobId, reason) {
|
||||
const job = requiredJob(jobId);
|
||||
updateJob(job, {
|
||||
state: "failed",
|
||||
terminal: true,
|
||||
terminalReason: boundedToken(reason || "host_rejected", "reason")
|
||||
});
|
||||
return publicJob(job);
|
||||
}
|
||||
|
||||
function listEncodeJobs(input) {
|
||||
const value = input || {};
|
||||
const jobId = value.jobId == null ? null : boundedToken(value.jobId, "jobId");
|
||||
const limit = boundedInteger(value.limit == null ? 32 : value.limit, "limit", 1, MAX_ENCODE_JOBS);
|
||||
const values = jobId ? [requiredJob(jobId)] : Array.from(jobs.values()).slice(-limit).reverse();
|
||||
return {
|
||||
jobs: values.map(publicJob),
|
||||
count: values.length,
|
||||
unattributedEncoderEvents,
|
||||
correlation: "single-active-job-only"
|
||||
};
|
||||
}
|
||||
|
||||
function waitForEncodeJob(input) {
|
||||
const value = input || {};
|
||||
const job = requiredJob(value.jobId);
|
||||
const timeoutMs = boundedInteger(value.timeoutMs == null ? 0 : value.timeoutMs, "timeoutMs", 0, MAX_WAIT_MS);
|
||||
if (job.terminal || timeoutMs === 0 || closed) {
|
||||
return Promise.resolve({ job: publicJob(job), timedOut: false, closed });
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
const waiter = { jobId: job.jobId, resolve, timer: null };
|
||||
waiter.timer = setTimer(function () {
|
||||
jobWaiters.delete(waiter);
|
||||
resolve({ job: publicJob(requiredJob(waiter.jobId)), timedOut: true, closed });
|
||||
}, timeoutMs);
|
||||
jobWaiters.add(waiter);
|
||||
});
|
||||
}
|
||||
|
||||
function settleWaiters() {
|
||||
for (const waiter of Array.from(waiters)) {
|
||||
const result = list(waiter.query);
|
||||
if (!result.events.length && !result.overflow) continue;
|
||||
waiters.delete(waiter);
|
||||
if (waiter.timer) clearTimer(waiter.timer);
|
||||
waiter.resolve(Object.assign(result, { closed: false }));
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
for (const waiter of Array.from(waiters)) {
|
||||
waiters.delete(waiter);
|
||||
if (waiter.timer) clearTimer(waiter.timer);
|
||||
waiter.resolve(Object.assign(list(waiter.query), { closed: true }));
|
||||
}
|
||||
for (const waiter of Array.from(jobWaiters)) {
|
||||
jobWaiters.delete(waiter);
|
||||
if (waiter.timer) clearTimer(waiter.timer);
|
||||
waiter.resolve({ job: publicJob(requiredJob(waiter.jobId)), timedOut: false, closed: true });
|
||||
}
|
||||
}
|
||||
|
||||
function status() {
|
||||
return {
|
||||
capacity,
|
||||
size: entries.length,
|
||||
latestRevision: revision,
|
||||
oldestRevision: entries.length ? entries[0].revision : revision + 1,
|
||||
dropped,
|
||||
encodeJobs: jobs.size,
|
||||
unattributedEncoderEvents,
|
||||
closed
|
||||
};
|
||||
}
|
||||
|
||||
function applyEncoderReceipt(job, receipt) {
|
||||
const states = {
|
||||
"encoder.queued": { state: "queued" },
|
||||
"encoder.progress": { state: "rendering", progress: receipt.detail.progress == null ? job.progress : receipt.detail.progress },
|
||||
"encoder.complete": { state: "completed", progress: 1, terminal: true, terminalReason: "completed" },
|
||||
"encoder.error": { state: "failed", terminal: true, terminalReason: "encoder_error" },
|
||||
"encoder.cancelled": { state: "cancelled", terminal: true, terminalReason: "cancelled" }
|
||||
};
|
||||
updateJob(job, Object.assign({ hostEvent: receipt.name, eventRevision: receipt.revision }, states[receipt.name] || {}));
|
||||
}
|
||||
|
||||
function updateJob(job, values) {
|
||||
Object.assign(job, values, { updatedAt: new Date(now()).toISOString() });
|
||||
if (!job.terminal) return;
|
||||
for (const waiter of Array.from(jobWaiters)) {
|
||||
if (waiter.jobId !== job.jobId) continue;
|
||||
jobWaiters.delete(waiter);
|
||||
if (waiter.timer) clearTimer(waiter.timer);
|
||||
waiter.resolve({ job: publicJob(job), timedOut: false, closed });
|
||||
}
|
||||
}
|
||||
|
||||
function requiredJob(jobId) {
|
||||
const id = boundedToken(jobId, "jobId");
|
||||
const job = jobs.get(id);
|
||||
if (!job) throw trackerError("UXP_ENCODE_JOB_NOT_FOUND", "Encode job receipt was not found");
|
||||
return job;
|
||||
}
|
||||
|
||||
function pruneJobs() {
|
||||
if (jobs.size < MAX_ENCODE_JOBS) return;
|
||||
const terminal = Array.from(jobs.values()).filter(function (job) { return job.terminal; });
|
||||
if (terminal.length) jobs.delete(terminal[0].jobId);
|
||||
}
|
||||
|
||||
return {
|
||||
append, recordHostEvent, list, wait, close, status,
|
||||
beginEncodeJob, markEncodeAccepted, markEncodeRejected, listEncodeJobs, waitForEncodeJob
|
||||
};
|
||||
}
|
||||
|
||||
function publicJob(job) {
|
||||
return {
|
||||
jobId: job.jobId,
|
||||
operationId: job.operationId,
|
||||
kind: job.kind,
|
||||
state: job.state,
|
||||
progress: job.progress,
|
||||
hostEvent: job.hostEvent,
|
||||
eventRevision: job.eventRevision,
|
||||
terminal: job.terminal,
|
||||
terminalReason: job.terminalReason,
|
||||
createdAt: job.createdAt,
|
||||
updatedAt: job.updatedAt,
|
||||
verificationBoundary: job.terminal ? "encoder_terminal_event_only" : "encoder_host_acceptance_or_event"
|
||||
};
|
||||
}
|
||||
|
||||
function trackerError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
function normalizeEvent(input, revision, timestamp) {
|
||||
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("event must be an object");
|
||||
const category = boundedToken(input.category, "category");
|
||||
const name = boundedToken(input.name, "name");
|
||||
const coalesceKey = input.coalesceKey == null ? null : boundedToken(input.coalesceKey, "coalesceKey");
|
||||
return {
|
||||
revision,
|
||||
category,
|
||||
name,
|
||||
receivedAt: new Date(timestamp).toISOString(),
|
||||
detail: safeDetail(input.detail),
|
||||
coalesceKey,
|
||||
coalesced: 0
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeQuery(input) {
|
||||
const value = input || {};
|
||||
if (typeof value !== "object" || Array.isArray(value)) throw new Error("event query must be an object");
|
||||
return {
|
||||
afterRevision: boundedInteger(value.afterRevision == null ? 0 : value.afterRevision, "afterRevision", 0, Number.MAX_SAFE_INTEGER),
|
||||
categories: tokenList(value.categories, "categories"),
|
||||
eventNames: tokenList(value.eventNames, "eventNames"),
|
||||
limit: boundedInteger(value.limit == null ? 100 : value.limit, "limit", 1, MAX_RESULTS),
|
||||
timeoutMs: boundedInteger(value.timeoutMs == null ? 0 : value.timeoutMs, "timeoutMs", 0, MAX_WAIT_MS)
|
||||
};
|
||||
}
|
||||
|
||||
function matches(event, query) {
|
||||
return (!query.categories.length || query.categories.indexOf(event.category) !== -1)
|
||||
&& (!query.eventNames.length || query.eventNames.indexOf(event.name) !== -1);
|
||||
}
|
||||
|
||||
function safeDetail(value) {
|
||||
if (value == null) return {};
|
||||
if (typeof value !== "object" || Array.isArray(value)) throw new Error("event detail must be an object");
|
||||
const result = {};
|
||||
for (const key of Object.keys(value)) {
|
||||
if (!SAFE_DETAIL_KEYS.has(key)) continue;
|
||||
const item = value[key];
|
||||
if (typeof item === "boolean" || (typeof item === "number" && Number.isFinite(item))) result[key] = item;
|
||||
else if (typeof item === "string") result[key] = item.slice(0, 128);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function tokenList(value, name) {
|
||||
if (value == null) return [];
|
||||
if (!Array.isArray(value) || value.length > 32) throw new Error(name + " must be an array of at most 32 tokens");
|
||||
return value.map(function (item) { return boundedToken(item, name); });
|
||||
}
|
||||
|
||||
function boundedToken(value, name) {
|
||||
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
|
||||
throw new Error(name + " must be a 1-128 character token");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function boundedInteger(value, name, minimum, maximum) {
|
||||
const number = Number(value);
|
||||
if (!Number.isInteger(number) || number < minimum || number > maximum) {
|
||||
throw new Error(name + " must be an integer between " + minimum + " and " + maximum);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
function publicEvent(event) {
|
||||
return {
|
||||
revision: event.revision,
|
||||
category: event.category,
|
||||
name: event.name,
|
||||
receivedAt: event.receivedAt,
|
||||
detail: Object.assign({}, event.detail),
|
||||
coalesced: event.coalesced || 0
|
||||
};
|
||||
}
|
||||
|
||||
return { createEventJournal, createTimelineSnapEventDefinitions, createOperationBoundaryEventDefinitions };
|
||||
});
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpHybridBenchmark = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
const WORKLOAD_ID = "weighted-energy-v1";
|
||||
const DEFAULT_ADDON = "premiere-mcp-benchmark.uxpaddon";
|
||||
|
||||
function run(options) {
|
||||
const input = options || {};
|
||||
assertOnlyKeys(input, [
|
||||
"sampleCount", "warmupCount", "iterations", "inputLength", "seed",
|
||||
"addonName", "loadAddon", "requireAddon", "now", "readMemory"
|
||||
]);
|
||||
const sampleCount = boundedInteger(input.sampleCount == null ? 30 : input.sampleCount, "sampleCount", 5, 200);
|
||||
const warmupCount = boundedInteger(input.warmupCount == null ? 3 : input.warmupCount, "warmupCount", 0, 20);
|
||||
const iterations = boundedInteger(input.iterations == null ? 4 : input.iterations, "iterations", 1, 50);
|
||||
const inputLength = boundedInteger(input.inputLength == null ? 131072 : input.inputLength, "inputLength", 1024, 1048576);
|
||||
const seed = boundedInteger(input.seed == null ? 1337 : input.seed, "seed", 1, 2147483646);
|
||||
const now = typeof input.now === "function" ? input.now : monotonicNow;
|
||||
const readMemory = typeof input.readMemory === "function" ? input.readMemory : defaultMemoryReader;
|
||||
const data = deterministicInput(inputLength, seed);
|
||||
const js = measure(function () { return weightedEnergy(data, iterations); }, sampleCount, warmupCount, now, readMemory);
|
||||
const addon = loadAddon(input);
|
||||
let native = null, checksumMatch = null;
|
||||
if (addon.loaded) {
|
||||
if (!addon.value || typeof addon.value.runBenchmarkKernel !== "function") {
|
||||
throw benchmarkError("UXP_HYBRID_CONTRACT_MISMATCH", "The addon must export runBenchmarkKernel(Float64Array, iterations)");
|
||||
}
|
||||
native = measure(function () {
|
||||
const value = Number(addon.value.runBenchmarkKernel(data, iterations));
|
||||
if (!Number.isFinite(value)) throw benchmarkError("UXP_HYBRID_INVALID_RESULT", "The native benchmark returned a non-finite checksum");
|
||||
return value;
|
||||
}, sampleCount, warmupCount, now, readMemory);
|
||||
checksumMatch = sameChecksum(js.checksum, native.checksum);
|
||||
}
|
||||
if (input.requireAddon === true && !addon.loaded) {
|
||||
throw benchmarkError("UXP_HYBRID_ADDON_REQUIRED", addon.error || "The benchmark addon was not available");
|
||||
}
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
workloadId: WORKLOAD_ID,
|
||||
configuration: { sampleCount, warmupCount, iterations, inputLength, seed },
|
||||
addon: { name: addon.name, loaded: addon.loaded, error: addon.error },
|
||||
javascript: js,
|
||||
native,
|
||||
checksumMatch,
|
||||
promotionEligible: false,
|
||||
promotionBoundary: "Run the cross-platform evidence verifier; this local result alone cannot enable a hybrid addon."
|
||||
};
|
||||
}
|
||||
|
||||
function weightedEnergy(values, iterations) {
|
||||
let checksum = 0;
|
||||
for (let pass = 0; pass < iterations; pass += 1) {
|
||||
let energy = 0, crossings = 0, previous = values[0];
|
||||
for (let i = 0; i < values.length; i += 1) {
|
||||
const sample = values[i];
|
||||
energy += sample * sample * ((i & 15) + 1);
|
||||
if ((sample < 0) !== (previous < 0)) crossings += 1;
|
||||
previous = sample;
|
||||
}
|
||||
checksum += energy / values.length + crossings * 0.000001 + pass * 0.000000001;
|
||||
}
|
||||
return checksum;
|
||||
}
|
||||
|
||||
function deterministicInput(length, seed) {
|
||||
const values = new Float64Array(length);
|
||||
let state = seed;
|
||||
for (let i = 0; i < length; i += 1) {
|
||||
state = state * 16807 % 2147483647;
|
||||
values[i] = state / 1073741823.5 - 1;
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function measure(workload, sampleCount, warmupCount, now, readMemory) {
|
||||
for (let i = 0; i < warmupCount; i += 1) workload();
|
||||
const beforeBytes = safeMemory(readMemory), samples = [];
|
||||
let checksum = null;
|
||||
for (let i = 0; i < sampleCount; i += 1) {
|
||||
const startedAt = Number(now());
|
||||
checksum = Number(workload());
|
||||
const finishedAt = Number(now());
|
||||
const elapsed = finishedAt - startedAt;
|
||||
if (!Number.isFinite(elapsed) || elapsed < 0) throw benchmarkError("UXP_HYBRID_INVALID_CLOCK", "Benchmark clock must be monotonic");
|
||||
samples.push(elapsed);
|
||||
}
|
||||
const afterBytes = safeMemory(readMemory);
|
||||
return {
|
||||
sampleCount,
|
||||
p50Ms: percentile(samples, 0.5),
|
||||
p95Ms: percentile(samples, 0.95),
|
||||
minMs: Math.min.apply(Math, samples),
|
||||
maxMs: Math.max.apply(Math, samples),
|
||||
checksum,
|
||||
memory: {
|
||||
beforeBytes,
|
||||
afterBytes,
|
||||
deltaBytes: beforeBytes == null || afterBytes == null ? null : afterBytes - beforeBytes,
|
||||
boundary: "runtime_heap_snapshot_if_available"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function loadAddon(options) {
|
||||
const name = typeof options.addonName === "string" && options.addonName
|
||||
? options.addonName
|
||||
: DEFAULT_ADDON;
|
||||
if (!/^[A-Za-z0-9._-]{1,128}\.uxpaddon$/.test(name)) {
|
||||
return { name, loaded: false, value: null, error: "addonName must be a simple .uxpaddon filename" };
|
||||
}
|
||||
const loader = typeof options.loadAddon === "function" ? options.loadAddon : defaultAddonLoader;
|
||||
try { return { name, loaded: true, value: loader(name), error: null }; }
|
||||
catch (error) { return { name, loaded: false, value: null, error: error && error.message || String(error) }; }
|
||||
}
|
||||
|
||||
function defaultAddonLoader(name) {
|
||||
if (typeof require !== "function") throw new Error("CommonJS addon loading is unavailable");
|
||||
return require(name);
|
||||
}
|
||||
|
||||
function monotonicNow() {
|
||||
if (typeof performance !== "undefined" && typeof performance.now === "function") return performance.now();
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
function defaultMemoryReader() {
|
||||
if (typeof performance !== "undefined" && performance.memory && Number.isFinite(performance.memory.usedJSHeapSize)) {
|
||||
return performance.memory.usedJSHeapSize;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function safeMemory(reader) {
|
||||
try {
|
||||
const value = reader();
|
||||
return Number.isFinite(value) && value >= 0 ? Number(value) : null;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
|
||||
function percentile(values, quantile) {
|
||||
const sorted = values.slice().sort(function (left, right) { return left - right; });
|
||||
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * quantile) - 1));
|
||||
return sorted[index];
|
||||
}
|
||||
|
||||
function sameChecksum(left, right) {
|
||||
return Number.isFinite(left) && Number.isFinite(right) &&
|
||||
Math.abs(left - right) <= Math.max(1e-9, Math.abs(left) * 1e-10);
|
||||
}
|
||||
|
||||
function boundedInteger(value, name, minimum, maximum) {
|
||||
const number = Number(value);
|
||||
if (!Number.isInteger(number) || number < minimum || number > maximum) {
|
||||
throw benchmarkError("UXP_HYBRID_INVALID_ARGUMENT", name + " must be an integer between " + minimum + " and " + maximum);
|
||||
}
|
||||
return number;
|
||||
}
|
||||
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw benchmarkError("UXP_HYBRID_INVALID_ARGUMENT", "options must be an object");
|
||||
for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw benchmarkError("UXP_HYBRID_INVALID_ARGUMENT", "Unexpected option: " + key);
|
||||
}
|
||||
|
||||
function benchmarkError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
return { WORKLOAD_ID, run, weightedEnergy, deterministicInput, percentile, sameChecksum };
|
||||
});
|
||||
+582
@@ -0,0 +1,582 @@
|
||||
"use strict";
|
||||
const { entrypoints, host, storage } = require("uxp");
|
||||
const ppro = require("premierepro");
|
||||
const Protocol = globalThis.PremiereMcpProtocol;
|
||||
const TranscriptSupport = globalThis.PremiereMcpTranscript;
|
||||
const TranscriptImport = globalThis.PremiereMcpTranscriptImport || (typeof require === "function" ? require("./transcript-import.cjs") : null);
|
||||
const WorkspaceSupport = globalThis.PremiereMcpWorkspace;
|
||||
const EventSupport = globalThis.PremiereMcpEvents;
|
||||
const Commands = globalThis.PremiereMcpCommands;
|
||||
const workspaceBroker = WorkspaceSupport.createWorkspaceBroker({ fs: storage && storage.localFileSystem });
|
||||
const eventJournal = EventSupport.createEventJournal({ capacity: 512 });
|
||||
const transcriptImportRuntime = TranscriptImport && typeof TranscriptImport.createTranscriptImportRuntime === "function"
|
||||
? TranscriptImport.createTranscriptImportRuntime({ ppro, TranscriptSupport, Protocol })
|
||||
: null;
|
||||
const commandRegistry = Commands.createCommandRegistry({
|
||||
ppro,
|
||||
Protocol,
|
||||
workspace: workspaceBroker,
|
||||
events: eventJournal,
|
||||
storage: typeof globalThis !== "undefined" ? globalThis.localStorage : null,
|
||||
transcriptImportHandler: transcriptImportRuntime && transcriptImportRuntime.importTranscript,
|
||||
transcriptImportProbe: transcriptImportRuntime && transcriptImportRuntime.canImportTranscript
|
||||
});
|
||||
let socket = null;
|
||||
let reconnectTimer = null;
|
||||
let lastState = "";
|
||||
let stateRevision = 0;
|
||||
let fallbackPollTimer = null;
|
||||
const operationTracker = Protocol.createOperationTracker();
|
||||
const eventSubscriptions = [];
|
||||
const targetEventSubscriptions = [];
|
||||
let targetSubscriptionGeneration = 0;
|
||||
|
||||
entrypoints.setup({
|
||||
panels: {
|
||||
mcpBridgePanel: {
|
||||
create() { subscribeHostEvents(); },
|
||||
show() { publishState("panel.show"); },
|
||||
destroy() { unsubscribeHostEvents(); stopFallbackPolling(); eventJournal.close(); void commandRegistry.dispose(); disconnect(); }
|
||||
}
|
||||
}
|
||||
});
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
document.getElementById("connect").addEventListener("click", connect);
|
||||
document.getElementById("refresh").addEventListener("click", () => publishState("manual"));
|
||||
document.getElementById("choose-workspace").addEventListener("click", chooseWorkspace);
|
||||
document.getElementById("revoke-workspace").addEventListener("click", revokeWorkspace);
|
||||
try { await workspaceBroker.initialize(); } catch (error) { setStatus(error.message || String(error)); }
|
||||
try { await commandRegistry.initialize(); } catch (error) { setStatus(error.message || String(error)); }
|
||||
renderWorkspaceStatus();
|
||||
subscribeHostEvents();
|
||||
connect();
|
||||
startFallbackPolling();
|
||||
});
|
||||
|
||||
async function capabilities() {
|
||||
const value = await commandRegistry.capabilities();
|
||||
value.commands["capabilities.get"].cancellable = false;
|
||||
value.commands["state.get"].cancellable = false;
|
||||
value.commands["operation.cancel"] = { supported: true, readOnly: false, scope: "preflight only" };
|
||||
if (value.commands["frame.export"]) Object.assign(value.commands["frame.export"], {
|
||||
cancellable: "preflight only", verification: "exporter return value", undoable: false, atomic: false
|
||||
});
|
||||
const supportedHost = TranscriptSupport.versionAtLeast(host && host.version, "25.6.0");
|
||||
const transcriptImportHost = TranscriptSupport.versionAtLeast(host && host.version, "26.3.0");
|
||||
const transcriptExportApi = supportedHost && !!(ppro.Transcript && ppro.Transcript.exportToJSON && ppro.Transcript.importFromJSON);
|
||||
const transcriptNativeHasApi = supportedHost && !!(ppro.Transcript && typeof ppro.Transcript.hasTranscript === "function");
|
||||
const transcriptHasApi = transcriptNativeHasApi || !!(supportedHost && ppro.Transcript && typeof ppro.Transcript.exportToJSON === "function");
|
||||
Object.assign(value.commands, {
|
||||
"transcript.export": { supported: transcriptExportApi, readOnly: true, minVersion: "25.6.0" },
|
||||
"transcript.search": { supported: transcriptExportApi, readOnly: true, minVersion: "25.6.0" },
|
||||
"transcript.has": {
|
||||
supported: transcriptHasApi, readOnly: true, minVersion: transcriptNativeHasApi ? "26.3.0" : "25.6.0",
|
||||
nativeCheckMinVersion: "26.3.0", nativeCheck: transcriptNativeHasApi,
|
||||
fallback: transcriptNativeHasApi ? null : transcriptHasApi ? "export-probe" : null,
|
||||
fallbackMinVersion: transcriptHasApi ? "25.6.0" : null
|
||||
},
|
||||
"transcript.import": {
|
||||
supported: transcriptImportHost && !!transcriptImportRuntime && transcriptImportRuntime.canImportTranscript(),
|
||||
readOnly: false,
|
||||
destructive: true,
|
||||
undoable: true,
|
||||
idempotent: true,
|
||||
minVersion: "26.3.0",
|
||||
confirmationRequired: true,
|
||||
verification: "bounded exact transcript-export SHA-256 readback"
|
||||
},
|
||||
"captions.inspect": { supported: supportedHost, readOnly: true, minVersion: "25.6.0" },
|
||||
"captions.create": { supported: false, reason: "No documented Premiere UXP caption creation API." },
|
||||
"captions.update": { supported: false, reason: "No documented Premiere UXP caption text/timing mutation API." },
|
||||
"captions.delete": { supported: false, reason: "No documented Premiere UXP caption deletion API." }
|
||||
});
|
||||
value.hostVersion = host && host.version || null;
|
||||
Object.assign(value, {
|
||||
events: {
|
||||
host: supportedHostEvents().map((event) => event.name),
|
||||
stateNotifications: true,
|
||||
journal: eventJournal.status(),
|
||||
fallbackPolling: { enabled: true, intervalMs: 5000 },
|
||||
operationLifecycle: ["started", "progress", "completed", "failed", "cancelled"]
|
||||
},
|
||||
operationSemantics: {
|
||||
mutations: "Only action-based commands executed by Project.executeTransaction may claim an undo boundary.",
|
||||
rollback: "No atomic rollback is claimed. Callers must inspect result verification metadata.",
|
||||
cancellation: "Cooperative before a non-cancellable Premiere host call; no interruption is claimed after that boundary."
|
||||
},
|
||||
fallback: { backend: "cep", reason: "Use CEP/QE only when a command is absent or reports unsupported; never silently retry a failed UXP mutation." }
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
function castClipProjectItem(item) {
|
||||
try {
|
||||
const clip = ppro.ClipProjectItem.cast(item);
|
||||
if (clip) return clip;
|
||||
} catch (_) {}
|
||||
throw new Error("The resolved project item is not a media clip");
|
||||
}
|
||||
|
||||
async function selectedClipProjectItem(project) {
|
||||
if (!ppro.ProjectUtils || typeof ppro.ProjectUtils.getSelection !== "function") {
|
||||
throw new Error("Project panel selection is unavailable; pass projectItemId or projectItemName");
|
||||
}
|
||||
const selection = await ppro.ProjectUtils.getSelection(project);
|
||||
const items = selection && await selection.getItems();
|
||||
if (!items || items.length !== 1) throw new Error("Select exactly one media project item, or pass projectItemId/projectItemName");
|
||||
return castClipProjectItem(items[0]);
|
||||
}
|
||||
|
||||
async function findProjectItem(project, args) {
|
||||
const wantedId = args && args.projectItemId != null ? String(args.projectItemId) : "";
|
||||
const wantedName = args && args.projectItemName != null ? String(args.projectItemName) : "";
|
||||
if (!wantedId && !wantedName) return selectedClipProjectItem(project);
|
||||
const queue = [await project.getRootItem()];
|
||||
while (queue.length) {
|
||||
const folder = queue.shift();
|
||||
const children = await folder.getItems();
|
||||
for (let i = 0; i < children.length; i += 1) {
|
||||
const item = children[i];
|
||||
const itemId = typeof item.getId === "function" ? String(item.getId()) : "";
|
||||
const candidate = TranscriptSupport.matchingClipCandidate(
|
||||
item, itemId, wantedId, wantedName, castClipProjectItem
|
||||
);
|
||||
if (candidate.clip) return candidate.clip;
|
||||
try {
|
||||
const childFolder = ppro.FolderItem.cast(item);
|
||||
if (childFolder) queue.push(childFolder);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
throw new Error("Project item not found");
|
||||
}
|
||||
|
||||
async function transcriptContext(args) {
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw new Error("No active project");
|
||||
if (!ppro.Transcript || typeof ppro.Transcript.exportToJSON !== "function") throw new Error("Transcript APIs require Premiere Pro 25.6 or newer");
|
||||
const clip = await findProjectItem(project, args || {});
|
||||
const projectItem = ppro.ProjectItem.cast(clip);
|
||||
return {
|
||||
project,
|
||||
projectGuid: String(project.guid),
|
||||
clip,
|
||||
projectItemId: String(await projectItem.getId()),
|
||||
projectItemName: clip.name
|
||||
};
|
||||
}
|
||||
|
||||
async function exportTranscript(args) {
|
||||
const context = await transcriptContext(args);
|
||||
const json = await ppro.Transcript.exportToJSON(context.clip);
|
||||
if (typeof json !== "string" || !json) throw new Error("The selected clip has no transcript");
|
||||
TranscriptSupport.parseTranscriptJSON(json);
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
projectItemId: context.projectItemId,
|
||||
projectItemName: context.projectItemName,
|
||||
json
|
||||
};
|
||||
}
|
||||
|
||||
async function searchTranscript(args) {
|
||||
const exported = await exportTranscript(args);
|
||||
const result = TranscriptSupport.searchTranscriptJSON(exported.json, args.query, {
|
||||
caseSensitive: args.caseSensitive, maxResults: args.maxResults
|
||||
});
|
||||
return Object.assign({ projectItemId: exported.projectItemId, projectItemName: exported.projectItemName }, result);
|
||||
}
|
||||
|
||||
async function hasTranscript(args) {
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw new Error("No active project");
|
||||
if (!ppro.Transcript) throw new Error("Transcript APIs require Premiere Pro 25.6 or newer");
|
||||
const clip = await findProjectItem(project, args || {});
|
||||
const projectItem = ppro.ProjectItem.cast(clip);
|
||||
const context = {
|
||||
projectGuid: String(project.guid),
|
||||
projectItemId: String(await projectItem.getId()),
|
||||
projectItemName: clip.name,
|
||||
clip
|
||||
};
|
||||
if (typeof ppro.Transcript.hasTranscript === "function") {
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
projectItemId: context.projectItemId,
|
||||
projectItemName: context.projectItemName,
|
||||
hasTranscript: !!await ppro.Transcript.hasTranscript(context.clip),
|
||||
method: "native"
|
||||
};
|
||||
}
|
||||
if (typeof ppro.Transcript.exportToJSON !== "function") throw new Error("This Premiere build cannot check transcripts");
|
||||
const present = await TranscriptSupport.probeTranscriptExport(function () {
|
||||
return ppro.Transcript.exportToJSON(context.clip);
|
||||
});
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
projectItemId: context.projectItemId,
|
||||
projectItemName: context.projectItemName,
|
||||
hasTranscript: present,
|
||||
method: "export-probe"
|
||||
};
|
||||
}
|
||||
|
||||
async function inspectCaptions() {
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw new Error("No active project");
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) throw new Error("No active sequence");
|
||||
const count = await sequence.getCaptionTrackCount();
|
||||
const tracks = [];
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const track = await sequence.getCaptionTrack(i);
|
||||
const items = await track.getTrackItems(ppro.Constants.TrackItemType.CLIP, false);
|
||||
tracks.push({ id: track.id, index: await track.getIndex(), name: track.name, muted: await track.isMuted(), itemCount: items ? items.length : 0 });
|
||||
}
|
||||
return { sequenceId: String(sequence.guid), sequenceName: sequence.name, trackCount: count, tracks };
|
||||
}
|
||||
|
||||
async function stateSnapshot() {
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
const sequence = project && await project.getActiveSequence();
|
||||
const position = sequence && await sequence.getPlayerPosition();
|
||||
return {
|
||||
revision: stateRevision,
|
||||
projectOpen: !!project,
|
||||
project: project ? { id: project.guid || null, name: project.name || null, path: project.path || null } : null,
|
||||
sequenceOpen: !!sequence,
|
||||
sequence: sequence ? { id: sequence.guid || null, name: sequence.name || null } : null,
|
||||
playheadSeconds: position ? position.seconds : null
|
||||
};
|
||||
}
|
||||
|
||||
async function exportFrame(args, operation) {
|
||||
assertNotCancelled(operation);
|
||||
publishOperation("progress", operation, { phase: "preflight", progress: 0.1 });
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw new Error("No active project");
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) throw new Error("No active sequence");
|
||||
if (!args.outputDirectory) throw new Error("outputDirectory is required");
|
||||
const outputDirectory = await workspaceBroker.assertPathAllowed(args.outputDirectory, { label: "outputDirectory", kind: "directory" });
|
||||
const filename = Protocol.safeFilename(args.filename);
|
||||
const exporterFilename = Protocol.exporterFrameName(filename);
|
||||
const position = args.seconds == null ? await sequence.getPlayerPosition() : await tickTime(args.seconds);
|
||||
const size = await sequence.getFrameSize();
|
||||
const width = positiveInt(args.width, size.width), height = positiveInt(args.height, size.height);
|
||||
assertNotCancelled(operation);
|
||||
operation.phase = "host_call";
|
||||
publishOperation("progress", operation, { phase: "host_call", progress: 0.4, cancellable: false });
|
||||
const returned = await ppro.Exporter.exportSequenceFrame(sequence, position, exporterFilename, outputDirectory, width, height);
|
||||
if (returned !== true) throw new Error("Premiere did not confirm frame export; no output path is reported");
|
||||
const path = Protocol.joinPath(outputDirectory, filename);
|
||||
return {
|
||||
path, width, height, seconds: position.seconds, exporterResult: returned,
|
||||
operation: Protocol.operationSemantics({
|
||||
mutatesProject: false,
|
||||
verificationStatus: "not_verified",
|
||||
verificationBoundary: "exporter_return_value",
|
||||
verificationEvidence: [{ type: "host_return", value: returned }],
|
||||
cancellationSupported: true
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
async function tickTime(seconds) {
|
||||
if (ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function") return ppro.TickTime.createWithSeconds(Number(seconds));
|
||||
throw new Error("This Premiere build cannot create TickTime; omit seconds to capture the playhead");
|
||||
}
|
||||
function positiveInt(value, fallback) { const n = Number(value == null ? fallback : value); if (!Number.isFinite(n) || n <= 0) throw new Error("frame dimensions must be positive"); return Math.round(n); }
|
||||
|
||||
async function dispatch(raw) {
|
||||
let cmd;
|
||||
let operation;
|
||||
try {
|
||||
cmd = Protocol.parseCommand(raw);
|
||||
if (cmd.command === "operation.cancel") {
|
||||
const result = operationTracker.requestCancel(cmd.args.requestId);
|
||||
send(Protocol.envelope("result", { ok: true, result }, cmd.requestId));
|
||||
return;
|
||||
}
|
||||
operation = operationTracker.begin(cmd.requestId, cmd.command);
|
||||
publishOperation("started", operation, { phase: "preflight", progress: 0 });
|
||||
let result;
|
||||
if (cmd.command === "capabilities.get") result = await capabilities();
|
||||
else if (cmd.command === "state.get") result = await stateSnapshot();
|
||||
else if (cmd.command === "frame.export") result = await exportFrame(cmd.args, operation);
|
||||
else if (cmd.command === "transcript.export") result = await exportTranscript(cmd.args);
|
||||
else if (cmd.command === "transcript.search") result = await searchTranscript(cmd.args);
|
||||
else if (cmd.command === "transcript.has") result = await hasTranscript(cmd.args);
|
||||
else if (cmd.command === "captions.inspect") result = await inspectCaptions();
|
||||
else {
|
||||
assertNotCancelled(operation);
|
||||
operation.phase = "host_call";
|
||||
result = await commandRegistry.dispatch(cmd.command, cmd.args);
|
||||
}
|
||||
const response = Protocol.envelope("result", {
|
||||
ok: true,
|
||||
result,
|
||||
operation: result && result.operation
|
||||
? result.operation
|
||||
: Protocol.operationSemantics(
|
||||
cmd.command.indexOf("transition.video.") === 0 && cmd.command !== "transition.video.list"
|
||||
? {
|
||||
mutatesProject: true,
|
||||
verificationStatus: "verified",
|
||||
verificationBoundary: "project_executeTransaction_return",
|
||||
verificationEvidence: [{ type: "transaction", accepted: true }],
|
||||
undoSupported: true,
|
||||
undoLabel: cmd.command === "transition.video.add"
|
||||
? "Add video transition"
|
||||
: "Remove video transition",
|
||||
transactionActionGroup: true,
|
||||
cancellationSupported: true
|
||||
}
|
||||
: {
|
||||
mutatesProject: false,
|
||||
verificationStatus: "verified",
|
||||
verificationBoundary: "host_snapshot"
|
||||
}
|
||||
)
|
||||
}, cmd.requestId);
|
||||
// Validate the exact response before emitting a terminal success event. If
|
||||
// serialization rejects an oversized result, the catch path emits only
|
||||
// `failed`, never both `completed` and `failed` for one operation.
|
||||
Protocol.serializeEnvelope(response);
|
||||
publishOperation("completed", operation, { phase: "complete", progress: 1 });
|
||||
send(response);
|
||||
} catch (error) {
|
||||
const cancelled = error && error.code === "UXP_OPERATION_CANCELLED";
|
||||
const errorCode = cancelled
|
||||
? "UXP_OPERATION_CANCELLED"
|
||||
: error && typeof error.code === "string" && /^UXP_[A-Z0-9_]+$/.test(error.code)
|
||||
? error.code
|
||||
: "UXP_COMMAND_FAILED";
|
||||
if (operation) publishOperation(cancelled ? "cancelled" : "failed", operation, {
|
||||
phase: operation.phase, progress: null, error: error.message || String(error)
|
||||
});
|
||||
send(Protocol.envelope("result", {
|
||||
ok: false,
|
||||
error: {
|
||||
code: errorCode,
|
||||
message: error.message || String(error),
|
||||
operation: Protocol.operationSemantics({ cancellationSupported: true })
|
||||
}
|
||||
}, cmd && cmd.requestId));
|
||||
} finally {
|
||||
if (operation) operationTracker.finish(operation);
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
if (socket) try { socket.onclose = null; socket.close(); } catch (_) {}
|
||||
const configuredUrl = document.getElementById("bridge-url").value;
|
||||
const token = document.getElementById("bridge-token").value;
|
||||
let url;
|
||||
try {
|
||||
url = WorkspaceSupport.validateLoopbackBridgeUrl(configuredUrl);
|
||||
if (token) url.searchParams.set("token", token);
|
||||
url = url.toString();
|
||||
} catch (_) {
|
||||
return scheduleReconnect("Invalid bridge URL");
|
||||
}
|
||||
setStatus("Connecting to " + url.origin + url.pathname);
|
||||
try { socket = new WebSocket(url); } catch (e) { return scheduleReconnect(e.message); }
|
||||
socket.onopen = async () => { setStatus("Connected"); send(Protocol.envelope("hello", await capabilities())); publishState("connected"); };
|
||||
socket.onmessage = (event) => dispatch(event.data);
|
||||
socket.onerror = () => setStatus("Bridge connection error");
|
||||
socket.onclose = () => { void commandRegistry.dispose(); scheduleReconnect("Disconnected"); };
|
||||
}
|
||||
function scheduleReconnect(message) { setStatus(message + "; retrying in 2s"); reconnectTimer = setTimeout(connect, 2000); }
|
||||
function send(value) { if (socket && socket.readyState === WebSocket.OPEN) socket.send(Protocol.serializeEnvelope(value)); }
|
||||
function disconnect() {
|
||||
if (reconnectTimer) clearTimeout(reconnectTimer);
|
||||
reconnectTimer = null;
|
||||
if (socket) try { socket.onclose = null; socket.close(); } catch (_) {}
|
||||
socket = null;
|
||||
}
|
||||
function publishOperation(name, operation, detail) {
|
||||
send(Protocol.operationEvent(name, operation, detail));
|
||||
}
|
||||
function assertNotCancelled(operation) {
|
||||
if (!operation || !operation.cancelRequested) return;
|
||||
const error = new Error("Operation cancelled before the Premiere host call");
|
||||
error.code = "UXP_OPERATION_CANCELLED";
|
||||
throw error;
|
||||
}
|
||||
function supportedHostEvents() {
|
||||
const constants = ppro.Constants || {};
|
||||
const project = constants.ProjectEvent || {};
|
||||
const sequence = constants.SequenceEvent || {};
|
||||
const operation = constants.OperationCompleteEvent || {};
|
||||
const encoder = ppro.EncoderManager || {};
|
||||
const snapEvents = typeof EventSupport.createTimelineSnapEventDefinitions === "function"
|
||||
? EventSupport.createTimelineSnapEventDefinitions(ppro.SnapEvent)
|
||||
: [];
|
||||
const operationBoundaryEvents = typeof EventSupport.createOperationBoundaryEventDefinitions === "function"
|
||||
? EventSupport.createOperationBoundaryEventDefinitions(ppro.OperationCompleteEvent)
|
||||
: [];
|
||||
return [
|
||||
hostEvent("project", "project.opened", project.OPENED, true),
|
||||
hostEvent("project", "project.closed", project.CLOSED, true),
|
||||
hostEvent("project", "project.dirty", project.DIRTY, true),
|
||||
hostEvent("project", "project.activated", project.ACTIVATED, true),
|
||||
hostEvent("project", "project.selection.changed", project.PROJECT_ITEM_SELECTION_CHANGED, true),
|
||||
hostEvent("sequence", "sequence.activated", sequence.ACTIVATED, true),
|
||||
hostEvent("sequence", "sequence.closed", sequence.CLOSED, true),
|
||||
hostEvent("sequence", "sequence.selection.changed", sequence.SELECTION_CHANGED, true),
|
||||
hostEvent("operation", "operation.import.complete", operation.IMPORT_MEDIA_COMPLETE || operation.EVENT_IMPORT_MEDIA_COMPLETE, false),
|
||||
hostEvent("operation", "operation.export.complete", operation.EXPORT_MEDIA_COMPLETE || operation.EVENT_EXPORT_MEDIA_COMPLETE, false),
|
||||
hostEvent("operation", "operation.effect.drop.complete", operation.EFFECT_DROP_COMPLETE || operation.EVENT_EFFECT_DROP_COMPLETE, false),
|
||||
hostEvent("operation", "operation.generative.extend.complete", operation.GENERATIVE_EXTEND_COMPLETE || operation.EVENT_GENERATIVE_EXTEND_COMPLETE, false),
|
||||
hostEvent("encoder", "encoder.queued", encoder.EVENT_RENDER_QUEUE, false),
|
||||
hostEvent("encoder", "encoder.progress", encoder.EVENT_RENDER_PROGRESS, false, "encoder.progress"),
|
||||
hostEvent("encoder", "encoder.complete", encoder.EVENT_RENDER_COMPLETE, false),
|
||||
hostEvent("encoder", "encoder.error", encoder.EVENT_RENDER_ERROR, false),
|
||||
hostEvent("encoder", "encoder.cancelled", encoder.EVENT_RENDER_CANCEL, false)
|
||||
].concat(snapEvents, operationBoundaryEvents).filter((event) => event.eventName !== undefined && event.eventName !== null);
|
||||
}
|
||||
function hostEvent(category, name, eventName, stateInvalidating, coalesceKey) {
|
||||
return { category, name, eventName, stateInvalidating, coalesceKey: coalesceKey || null };
|
||||
}
|
||||
function subscribeHostEvents() {
|
||||
if (!ppro.EventManager) return;
|
||||
if (!eventSubscriptions.length) {
|
||||
supportedHostEvents().forEach((definition) => {
|
||||
const handler = (event) => recordHostEvent(definition, event);
|
||||
ppro.EventManager.addGlobalEventListener(definition.eventName, handler, false);
|
||||
eventSubscriptions.push({ eventName: definition.eventName, handler });
|
||||
});
|
||||
}
|
||||
void rebindTrackEvents();
|
||||
}
|
||||
function unsubscribeHostEvents() {
|
||||
if (!ppro.EventManager) return;
|
||||
unsubscribeTrackEvents();
|
||||
eventSubscriptions.splice(0).forEach(({ eventName, handler }) => {
|
||||
try { ppro.EventManager.removeGlobalEventListener(eventName, handler, false); } catch (_) {}
|
||||
});
|
||||
}
|
||||
function recordHostEvent(definition, event) {
|
||||
const detail = {};
|
||||
if (event && typeof event === "object") {
|
||||
if (Number.isFinite(Number(event.state))) detail.state = Number(event.state);
|
||||
if (Number.isFinite(Number(event.progress))) detail.progress = Number(event.progress);
|
||||
}
|
||||
const receipt = eventJournal.recordHostEvent({
|
||||
category: definition.category,
|
||||
name: definition.name,
|
||||
detail,
|
||||
coalesceKey: definition.coalesceKey
|
||||
});
|
||||
if (receipt) send(Protocol.envelope("event", { name: "premiere.host.event", receipt }));
|
||||
if (definition.stateInvalidating) publishState("host-event", definition.name);
|
||||
if (definition.name === "project.opened" || definition.name === "project.closed" ||
|
||||
definition.name === "project.activated" || definition.name === "sequence.activated" ||
|
||||
definition.name === "sequence.closed") void rebindTrackEvents();
|
||||
}
|
||||
async function rebindTrackEvents() {
|
||||
if (!ppro.EventManager || typeof ppro.EventManager.addEventListener !== "function") return;
|
||||
const generation = ++targetSubscriptionGeneration;
|
||||
unsubscribeTrackEvents(false);
|
||||
try {
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
const sequence = project && await project.getActiveSequence();
|
||||
if (!sequence || generation !== targetSubscriptionGeneration) return;
|
||||
const definitions = [
|
||||
{ mediaType: "video", countMethod: "getVideoTrackCount", getMethod: "getVideoTrack", api: ppro.VideoTrack },
|
||||
{ mediaType: "audio", countMethod: "getAudioTrackCount", getMethod: "getAudioTrack", api: ppro.AudioTrack }
|
||||
];
|
||||
for (let d = 0; d < definitions.length; d += 1) {
|
||||
const definition = definitions[d];
|
||||
if (typeof sequence[definition.countMethod] !== "function" || typeof sequence[definition.getMethod] !== "function") continue;
|
||||
const count = Math.min(256, Math.max(0, Number(await sequence[definition.countMethod]()) || 0));
|
||||
for (let index = 0; index < count; index += 1) {
|
||||
if (generation !== targetSubscriptionGeneration) return;
|
||||
const track = await sequence[definition.getMethod](index);
|
||||
if (generation !== targetSubscriptionGeneration) return;
|
||||
if (!track) continue;
|
||||
const eventKinds = [
|
||||
["changed", definition.api && definition.api.EVENT_TRACK_CHANGED],
|
||||
["info_changed", definition.api && definition.api.EVENT_TRACK_INFO_CHANGED],
|
||||
["lock_changed", definition.api && definition.api.EVENT_TRACK_LOCK_CHANGED]
|
||||
];
|
||||
for (let e = 0; e < eventKinds.length; e += 1) {
|
||||
const kind = eventKinds[e][0], eventName = eventKinds[e][1];
|
||||
if (eventName == null) continue;
|
||||
const handler = () => recordTrackEvent(definition.mediaType, index, kind);
|
||||
ppro.EventManager.addEventListener(track, eventName, handler, false);
|
||||
targetEventSubscriptions.push({ target: track, eventName, handler });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
function unsubscribeTrackEvents(incrementGeneration = true) {
|
||||
if (incrementGeneration) targetSubscriptionGeneration += 1;
|
||||
targetEventSubscriptions.splice(0).forEach(({ target, eventName, handler }) => {
|
||||
try { ppro.EventManager.removeEventListener(target, eventName, handler); } catch (_) {}
|
||||
});
|
||||
}
|
||||
function recordTrackEvent(mediaType, trackIndex, eventKind) {
|
||||
const receipt = eventJournal.recordHostEvent({
|
||||
category: "track",
|
||||
name: "track." + mediaType + "." + eventKind,
|
||||
detail: { mediaType, trackIndex, eventKind }
|
||||
});
|
||||
if (receipt) send(Protocol.envelope("event", { name: "premiere.host.event", receipt }));
|
||||
}
|
||||
function startFallbackPolling() {
|
||||
if (!fallbackPollTimer) fallbackPollTimer = setInterval(() => publishState("fallback-poll"), 5000);
|
||||
}
|
||||
function stopFallbackPolling() {
|
||||
if (fallbackPollTimer) clearInterval(fallbackPollTimer);
|
||||
fallbackPollTimer = null;
|
||||
}
|
||||
async function publishState(reason, hostEvent) {
|
||||
try {
|
||||
stateRevision += 1;
|
||||
const state = await stateSnapshot();
|
||||
const comparable = Object.assign({}, state, { revision: 0 });
|
||||
const encoded = JSON.stringify(comparable);
|
||||
if (reason !== "fallback-poll" || encoded !== lastState) {
|
||||
lastState = encoded;
|
||||
send(Protocol.envelope("event", {
|
||||
name: "premiere.state.changed",
|
||||
reason,
|
||||
hostEvent: hostEvent || null,
|
||||
state
|
||||
}));
|
||||
}
|
||||
} catch (e) { setStatus(e.message); }
|
||||
}
|
||||
function setStatus(value) { const el = document.getElementById("status"); if (el) el.textContent = value; }
|
||||
|
||||
async function chooseWorkspace() {
|
||||
try {
|
||||
setStatus("Choose the folder that contains approved media, presets, and exports");
|
||||
await workspaceBroker.requestRoot();
|
||||
renderWorkspaceStatus();
|
||||
setStatus("Workspace access granted");
|
||||
} catch (error) {
|
||||
setStatus(error.message || String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function revokeWorkspace() {
|
||||
try {
|
||||
await workspaceBroker.revoke();
|
||||
renderWorkspaceStatus();
|
||||
setStatus("Workspace access revoked");
|
||||
} catch (error) {
|
||||
setStatus(error.message || String(error));
|
||||
}
|
||||
}
|
||||
|
||||
function renderWorkspaceStatus() {
|
||||
const element = document.getElementById("workspace-status"), value = workspaceBroker.status();
|
||||
if (element) element.textContent = value.configured
|
||||
? "Approved folder: " + value.rootName + "\nPersistent access: yes\nNative path: redacted\nCanonical path validation: " + value.canonicalPathValidation
|
||||
: "No approved workspace folder";
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; connect-src ws://127.0.0.1:* ws://localhost:*; object-src 'none'; base-uri 'none'">
|
||||
<link rel="stylesheet" href="styles.css"></head><body>
|
||||
<h3>MCP for Adobe Premiere Pro</h3>
|
||||
<label for="bridge-url">Bridge URL</label><input id="bridge-url" value="ws://127.0.0.1:7777/uxp" spellcheck="false" autocomplete="off">
|
||||
<label for="bridge-token">Bridge token</label><input id="bridge-token" type="password" autocomplete="off">
|
||||
<button id="connect" aria-controls="status">Connect</button><button id="refresh" aria-controls="status">Refresh state</button>
|
||||
<h4>Approved workspace</h4>
|
||||
<p class="help">File-based UXP workflows are limited to one folder you choose. The bridge never returns its native path.</p>
|
||||
<button id="choose-workspace" aria-controls="workspace-status">Choose folder</button><button id="revoke-workspace" aria-controls="workspace-status">Revoke access</button>
|
||||
<pre id="workspace-status" role="status" aria-live="polite" aria-atomic="true">Loading workspace access…</pre>
|
||||
<pre id="status" role="status" aria-live="polite" aria-atomic="true" tabindex="0">Starting…</pre>
|
||||
<script src="protocol.cjs"></script><script src="transcript.cjs"></script><script src="transcript-import.cjs"></script><script src="workspace.cjs"></script><script src="events.cjs"></script><script src="hybrid-benchmark.cjs"></script><script src="workflows.cjs"></script><script src="project-item-color-label-locks.cjs"></script><script src="advanced-workflows.cjs"></script><script src="dialogue-workflows.cjs"></script><script src="timeline-source-label-workflows.cjs"></script><script src="sequence-preview-frame-workflows.cjs"></script><script src="source-media-provenance-workflows.cjs"></script><script src="source-proxy-workflows.cjs"></script><script src="tick-time-arithmetic-workflows.cjs"></script><script src="object-mask-audit-workflows.cjs"></script><script src="unique-identity-workflows.cjs"></script><script src="effect-parameter-catalog-workflows.cjs"></script><script src="track-item-mutation-locks.cjs"></script><script src="slip-workflows.cjs"></script><script src="slide-workflows.cjs"></script><script src="clone-workflows.cjs"></script><script src="ripple-delete-workflows.cjs"></script><script src="next-workflows.cjs"></script><script src="commands.cjs"></script><script src="index.cjs"></script>
|
||||
</body></html>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"manifestVersion": 5,
|
||||
"id": "com.ppmcp.premiere.uxp",
|
||||
"name": "MCP for Adobe Premiere Pro",
|
||||
"version": "1.14.9",
|
||||
"main": "index.html",
|
||||
"host": { "app": "premierepro", "minVersion": "25.6.0" },
|
||||
"entrypoints": [
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "mcpBridgePanel",
|
||||
"label": { "default": "MCP for Adobe Premiere Pro" },
|
||||
"minimumSize": { "width": 320, "height": 240 },
|
||||
"preferredDockedSize": { "width": 380, "height": 520 },
|
||||
"preferredFloatingSize": { "width": 420, "height": 560 }
|
||||
}
|
||||
],
|
||||
"requiredPermissions": {
|
||||
"localFileSystem": "request",
|
||||
"network": { "domains": "all" }
|
||||
}
|
||||
}
|
||||
+1905
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,173 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpObjectMaskAuditWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// ObjectMaskUtils intentionally exposes only a yes/no answer. This audit
|
||||
// makes that answer useful for bounded review without inventing a mask
|
||||
// count, location, quality, editability, tracking state, or render result.
|
||||
function createObjectMaskAuditWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const MAX_SEQUENCES = 64;
|
||||
const definitions = {
|
||||
"objectMask.audit": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "26.3.0",
|
||||
probe: canAuditObjectMasks,
|
||||
handler: auditObjectMasks
|
||||
}
|
||||
};
|
||||
|
||||
function canAuditObjectMasks() {
|
||||
return !!(ppro && ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.ObjectMaskUtils && typeof ppro.ObjectMaskUtils.hasObjectMask === "function");
|
||||
}
|
||||
|
||||
async function auditObjectMasks(args) {
|
||||
assertOnlyKeys(args, ["expectedProjectGuid", "sequenceIds"]);
|
||||
const requestedIds = requestedSequenceIds(args.sequenceIds);
|
||||
const project = await activeProject();
|
||||
const projectGuid = requiredGuid(project.guid, "active project GUID");
|
||||
if (args.expectedProjectGuid != null && requiredGuid(args.expectedProjectGuid, "expectedProjectGuid") !== projectGuid) {
|
||||
throw commandError("UXP_STALE_OBJECT_MASK_AUDIT", "The active project differs from expectedProjectGuid; inspect the current object-mask state again");
|
||||
}
|
||||
|
||||
const projectHasObjectMask = objectMaskValue(project);
|
||||
const first = await targetSequences(project, requestedIds);
|
||||
const firstResults = first.map(function (entry) {
|
||||
return { id: entry.id, name: entry.name, hasObjectMask: objectMaskValue(entry.sequence) };
|
||||
});
|
||||
|
||||
const activeAfter = await activeProject();
|
||||
if (requiredGuid(activeAfter.guid, "active project GUID after audit") !== projectGuid) {
|
||||
throw commandError("UXP_STALE_OBJECT_MASK_AUDIT", "The active project changed while object masks were being audited");
|
||||
}
|
||||
const second = await targetSequences(activeAfter, requestedIds);
|
||||
const secondResults = second.map(function (entry) {
|
||||
return { id: entry.id, name: entry.name, hasObjectMask: objectMaskValue(entry.sequence) };
|
||||
});
|
||||
const projectHasObjectMaskAfter = objectMaskValue(activeAfter);
|
||||
if (projectHasObjectMaskAfter !== projectHasObjectMask || !sameAudit(firstResults, secondResults)) {
|
||||
throw commandError("UXP_STALE_OBJECT_MASK_AUDIT", "The project, sequence identities, or object-mask results changed during the audit; retry");
|
||||
}
|
||||
|
||||
const maskedSequenceCount = firstResults.filter(function (entry) { return entry.hasObjectMask; }).length;
|
||||
return {
|
||||
projectGuid,
|
||||
scope: requestedIds ? "explicit_sequences" : "all_project_sequences",
|
||||
projectHasObjectMask,
|
||||
sequenceCount: firstResults.length,
|
||||
maskedSequenceCount,
|
||||
sequences: firstResults,
|
||||
verificationBoundary: "bounded_project_and_sequence_object_mask_double_readback"
|
||||
};
|
||||
}
|
||||
|
||||
async function activeProject() {
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
return project;
|
||||
}
|
||||
|
||||
function requestedSequenceIds(value) {
|
||||
if (value == null) return null;
|
||||
if (!Array.isArray(value) || !value.length || value.length > MAX_SEQUENCES) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "sequenceIds must contain 1-" + MAX_SEQUENCES + " exact sequence GUIDs");
|
||||
}
|
||||
const ids = value.map(function (entry, index) {
|
||||
return requiredGuid(entry, "sequenceIds[" + index + "]");
|
||||
});
|
||||
if (new Set(ids).size !== ids.length) throw commandError("UXP_INVALID_ARGUMENT", "sequenceIds must not contain duplicates");
|
||||
return ids.sort();
|
||||
}
|
||||
|
||||
async function targetSequences(project, requestedIds) {
|
||||
const values = requestedIds ? await sequencesById(project, requestedIds) : await allSequences(project);
|
||||
const seen = new Set(), snapshots = [];
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
const sequence = values[index];
|
||||
const id = requiredGuid(sequence && sequence.guid, "sequence GUID");
|
||||
if (seen.has(id)) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned duplicate sequence GUIDs during object-mask audit");
|
||||
seen.add(id);
|
||||
snapshots.push({ sequence, id, name: boundedName(sequence && sequence.name) });
|
||||
}
|
||||
snapshots.sort(function (left, right) { return left.id.localeCompare(right.id); });
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
async function allSequences(project) {
|
||||
if (typeof project.getSequences !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented project sequence enumeration");
|
||||
}
|
||||
const values = Array.from(await project.getSequences() || []);
|
||||
if (values.length > MAX_SEQUENCES) {
|
||||
throw commandError("UXP_PROJECT_TOO_LARGE", "Object-mask audit is capped at " + MAX_SEQUENCES + " sequences; pass at most " + MAX_SEQUENCES + " exact sequenceIds instead");
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
async function sequencesById(project, ids) {
|
||||
if (!ppro.Guid || typeof ppro.Guid.fromString !== "function" || typeof project.getSequence !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented exact sequence GUID lookup");
|
||||
}
|
||||
const values = [];
|
||||
for (let index = 0; index < ids.length; index += 1) {
|
||||
let sequence;
|
||||
try { sequence = await project.getSequence(ppro.Guid.fromString(ids[index])); } catch (_) { sequence = null; }
|
||||
if (!sequence || requiredGuid(sequence.guid, "resolved sequence GUID") !== ids[index]) {
|
||||
throw commandError("UXP_STALE_OBJECT_MASK_AUDIT", "A requested sequence no longer resolves to its exact GUID; retry the audit");
|
||||
}
|
||||
values.push(sequence);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
function objectMaskValue(target) {
|
||||
let value;
|
||||
try { value = ppro.ObjectMaskUtils.hasObjectMask(target); } catch (error) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere could not report object-mask presence: " + (error && error.message || String(error)));
|
||||
}
|
||||
if (typeof value !== "boolean") throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned a non-boolean object-mask result");
|
||||
return value;
|
||||
}
|
||||
|
||||
function sameAudit(left, right) {
|
||||
return left.length === right.length && left.every(function (entry, index) {
|
||||
const other = right[index];
|
||||
return other && entry.id === other.id && entry.name === other.name && entry.hasObjectMask === other.hasObjectMask;
|
||||
});
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "arguments must be an object");
|
||||
const unknown = Object.keys(value).find(function (key) { return !allowed.includes(key); });
|
||||
if (unknown) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown);
|
||||
}
|
||||
|
||||
function requiredGuid(value, name) {
|
||||
let text = "";
|
||||
try { text = String(value && typeof value.toString === "function" ? value.toString() : value || ""); } catch (_) {}
|
||||
if (!text || text.length > 512) throw commandError("UXP_VERIFICATION_FAILED", name + " must be a bounded non-empty GUID");
|
||||
return text;
|
||||
}
|
||||
|
||||
function boundedName(value) {
|
||||
const name = String(value == null ? "" : value);
|
||||
if (name.length > 255) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an overlong sequence name");
|
||||
return name;
|
||||
}
|
||||
|
||||
function commandError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
return { createObjectMaskAuditWorkflowDefinitions };
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpProjectItemColorLabelLocks = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// A source item's color label is project-global: the same item can appear on
|
||||
// multiple timeline tracks or sequences. Keep every bridge color-label
|
||||
// action for that item behind one tail so a second operation cannot build an
|
||||
// action from a label snapshot that the first operation has already changed.
|
||||
function createProjectItemColorLabelLocks() {
|
||||
const tails = new Map();
|
||||
|
||||
function withProjectItemColorLabelLock(key, operation) {
|
||||
const previous = tails.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const gate = new Promise(function (resolve) { release = resolve; });
|
||||
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
|
||||
tails.set(key, tail);
|
||||
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
|
||||
release();
|
||||
if (tails.get(key) === tail) tails.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
return { withProjectItemColorLabelLock };
|
||||
}
|
||||
|
||||
return { createProjectItemColorLabelLocks };
|
||||
});
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpProtocol = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
const PROTOCOL_VERSION = 2;
|
||||
const MAX_COMMAND_BYTES = 64 * 1024;
|
||||
const MAX_RESULT_BYTES = 1024 * 1024;
|
||||
const COMMAND_NAME = /^[a-z][A-Za-z0-9]*(?:\.[a-z][A-Za-z0-9]*)+$/;
|
||||
function envelope(type, payload, requestId) {
|
||||
const value = { protocolVersion: PROTOCOL_VERSION, type, payload: payload || {}, sentAt: new Date().toISOString() };
|
||||
if (requestId) value.requestId = requestId;
|
||||
return value;
|
||||
}
|
||||
function utf8ByteLength(value) {
|
||||
const text = String(value);
|
||||
let bytes = 0;
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const code = text.charCodeAt(index);
|
||||
if (code < 0x80) bytes += 1;
|
||||
else if (code < 0x800) bytes += 2;
|
||||
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length && text.charCodeAt(index + 1) >= 0xdc00 && text.charCodeAt(index + 1) <= 0xdfff) {
|
||||
bytes += 4;
|
||||
index += 1;
|
||||
} else bytes += 3;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
function resultTooLarge() {
|
||||
const error = new Error("UXP bridge result exceeds 1 MiB after UTF-8 serialization");
|
||||
error.code = "UXP_RESULT_TOO_LARGE";
|
||||
return error;
|
||||
}
|
||||
function serializeEnvelope(value) {
|
||||
const encoded = JSON.stringify(value);
|
||||
if (utf8ByteLength(encoded) > MAX_RESULT_BYTES) throw resultTooLarge();
|
||||
return encoded;
|
||||
}
|
||||
function assertResultSize(result) {
|
||||
// Use the longest legal request id and the same fallback operation payload
|
||||
// dispatch adds to read-only command results, so this is conservative for
|
||||
// metadata snapshots before the exact final envelope is serialized.
|
||||
serializeEnvelope(envelope("result", {
|
||||
ok: true,
|
||||
result,
|
||||
operation: operationSemantics({
|
||||
mutatesProject: false,
|
||||
verificationStatus: "verified",
|
||||
verificationBoundary: "host_snapshot"
|
||||
})
|
||||
}, "x".repeat(128)));
|
||||
return result;
|
||||
}
|
||||
function parseCommand(raw) {
|
||||
if (typeof raw === "string" && utf8ByteLength(raw) > MAX_COMMAND_BYTES) throw new Error("UXP bridge command exceeds 64 KiB");
|
||||
const value = typeof raw === "string" ? JSON.parse(raw) : raw;
|
||||
if (!isPlainObject(value) || value.type !== "command" || typeof value.command !== "string" || !COMMAND_NAME.test(value.command)) throw new Error("Invalid UXP bridge command");
|
||||
if (value.protocolVersion != null && value.protocolVersion !== PROTOCOL_VERSION) throw new Error("Unsupported UXP protocol version: " + value.protocolVersion);
|
||||
if (value.requestId != null && (typeof value.requestId !== "string" || value.requestId.length < 1 || value.requestId.length > 128)) throw new Error("requestId must be a non-empty string of at most 128 characters");
|
||||
if (value.args != null && !isPlainObject(value.args)) throw new Error("command args must be an object");
|
||||
return { requestId: value.requestId || null, command: value.command, args: value.args || {} };
|
||||
}
|
||||
function isPlainObject(value) { return !!value && typeof value === "object" && !Array.isArray(value); }
|
||||
function operationEvent(name, operation, detail) {
|
||||
return envelope("event", {
|
||||
name: "premiere.operation." + name,
|
||||
operation: Object.assign({
|
||||
requestId: operation.requestId,
|
||||
command: operation.command
|
||||
}, detail || {})
|
||||
}, operation.requestId);
|
||||
}
|
||||
function operationSemantics(options) {
|
||||
const value = options || {};
|
||||
return {
|
||||
mutatesProject: value.mutatesProject === true,
|
||||
verification: {
|
||||
status: value.verificationStatus || "not_verified",
|
||||
boundary: value.verificationBoundary || "command_return_value",
|
||||
evidence: value.verificationEvidence || []
|
||||
},
|
||||
undo: {
|
||||
supported: value.undoSupported === true,
|
||||
boundary: value.undoSupported === true ? "premiere_undo_history" : "not_undoable",
|
||||
label: value.undoLabel || null
|
||||
},
|
||||
transaction: {
|
||||
actionGroup: value.transactionActionGroup === true,
|
||||
boundary: value.transactionActionGroup === true ? "project_executeTransaction" : "single_host_call",
|
||||
atomicRollback: false
|
||||
},
|
||||
cancellation: {
|
||||
supported: value.cancellationSupported === true,
|
||||
boundary: value.cancellationBoundary || "before_non_cancellable_host_call"
|
||||
}
|
||||
};
|
||||
}
|
||||
function createOperationTracker() {
|
||||
const operations = new Map();
|
||||
return {
|
||||
begin(requestId, command) {
|
||||
const operation = {
|
||||
requestId: requestId || null,
|
||||
command,
|
||||
phase: "preflight",
|
||||
cancelRequested: false,
|
||||
complete: false
|
||||
};
|
||||
if (requestId) operations.set(requestId, operation);
|
||||
return operation;
|
||||
},
|
||||
get(requestId) { return requestId ? operations.get(requestId) || null : null; },
|
||||
requestCancel(requestId) {
|
||||
const operation = this.get(requestId);
|
||||
if (!operation || operation.complete) return { accepted: false, reason: "operation_not_active" };
|
||||
if (operation.phase !== "preflight") return { accepted: false, reason: "host_call_not_cancellable" };
|
||||
operation.cancelRequested = true;
|
||||
return { accepted: true, reason: "cancellation_requested" };
|
||||
},
|
||||
finish(operation) {
|
||||
operation.complete = true;
|
||||
if (operation.requestId) operations.delete(operation.requestId);
|
||||
}
|
||||
};
|
||||
}
|
||||
function safeFilename(value) {
|
||||
const name = String(value || "mcp-frame.png");
|
||||
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*\.png$/i.test(name)) throw new Error("filename must be a simple .png name");
|
||||
return name;
|
||||
}
|
||||
// Premiere's frame exporter selects PNG from the name and appends that extension
|
||||
// itself. Keep the public, fully qualified filename for reporting, but pass the
|
||||
// bare stem to the host so `frame.png` does not become `frame.png.png`.
|
||||
function exporterFrameName(value) { return safeFilename(value).replace(/\.png$/i, ""); }
|
||||
function joinPath(dir, name) { return /[\\\/]$/.test(dir) ? dir + name : dir + "/" + name; }
|
||||
return {
|
||||
PROTOCOL_VERSION,
|
||||
MAX_COMMAND_BYTES,
|
||||
MAX_RESULT_BYTES,
|
||||
envelope,
|
||||
utf8ByteLength,
|
||||
serializeEnvelope,
|
||||
assertResultSize,
|
||||
parseCommand,
|
||||
operationEvent,
|
||||
operationSemantics,
|
||||
createOperationTracker,
|
||||
safeFilename,
|
||||
exporterFrameName,
|
||||
joinPath
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,360 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpRippleDeleteWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// A coordinate-bound ripple delete is deliberately narrower than the legacy
|
||||
// QE command: it removes exactly one item only when its immediate successor
|
||||
// is contiguous, so post-readback can prove the closed cut without reading
|
||||
// unrelated items on the track.
|
||||
function createRippleDeleteWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const fallbackTails = new Map();
|
||||
const locks = deps.locks && typeof deps.locks.withTrackMutationLock === "function"
|
||||
? deps.locks
|
||||
: { withTrackMutationLock: localLock };
|
||||
const definitions = {
|
||||
"trackItem.rippleDelete.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseRippleDelete,
|
||||
handler: inspectRippleDelete
|
||||
},
|
||||
"trackItem.rippleDelete": {
|
||||
destructive: true,
|
||||
undoable: true,
|
||||
idempotent: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseRippleDelete,
|
||||
handler: rippleDelete
|
||||
}
|
||||
};
|
||||
|
||||
function canUseRippleDelete() {
|
||||
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.SequenceEditor && typeof ppro.SequenceEditor.getEditor === "function" &&
|
||||
ppro.TrackItemSelection && typeof ppro.TrackItemSelection.createEmptySelection === "function" &&
|
||||
ppro.Constants && ppro.Constants.MediaType);
|
||||
}
|
||||
|
||||
async function inspectRippleDelete(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
|
||||
const context = await activeTarget(args, false);
|
||||
const snapshot = await rippleSnapshot(context);
|
||||
assertRippleSupported(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async function rippleDelete(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSnapshot", "confirmRippleDelete", "operationId"]);
|
||||
requireConfirmation(args.confirmRippleDelete);
|
||||
requireOperationId(args.operationId);
|
||||
const target = targetCoordinates(args), expected = requiredSnapshot(args.expectedSnapshot);
|
||||
assertExpectedTarget(expected, target);
|
||||
const initial = await activeTarget(target, true);
|
||||
if (initial.projectGuid !== expected.projectGuid || initial.sequenceId !== expected.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence no longer matches the reviewed ripple-delete snapshot");
|
||||
}
|
||||
const key = initial.projectGuid + "\u0000" + initial.sequenceId + "\u0000" + target.mediaType + "\u0000" + target.trackIndex;
|
||||
return locks.withTrackMutationLock(key, async function () {
|
||||
// This full preflight occurs after entering the same per-track tail as
|
||||
// slips, slides, and append duplicates, so a distinct operation ID
|
||||
// cannot construct a remove action from an obsolete timeline state.
|
||||
let context;
|
||||
try {
|
||||
context = await activeTarget(target, true);
|
||||
} catch (error) {
|
||||
if (error && error.code === "UXP_TARGET_NOT_FOUND") {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed target no longer exists before the ripple-delete transaction");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (context.projectGuid !== expected.projectGuid || context.sequenceId !== expected.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence changed before the ripple-delete transaction");
|
||||
}
|
||||
let before;
|
||||
try {
|
||||
before = await rippleSnapshot(context);
|
||||
} catch (error) {
|
||||
if (error && error.code === "UXP_TARGET_UNSUPPORTED") {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed contiguous successor changed before the ripple-delete transaction");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
if (!sameSnapshot(before, expected)) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed target or contiguous successor changed since inspection");
|
||||
}
|
||||
assertRippleSupported(before);
|
||||
let committed = false;
|
||||
context.project.lockedAccess(function () {
|
||||
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed project or sequence changed before ripple-delete action creation");
|
||||
}
|
||||
const editor = ppro.SequenceEditor.getEditor(context.sequence);
|
||||
const mediaType = ppro.Constants.MediaType[context.mediaType.toUpperCase()];
|
||||
if (!editor || typeof editor.createRemoveItemsAction !== "function" || mediaType == null) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose documented ripple-delete actions");
|
||||
}
|
||||
const selection = createSingleItemSelection(context.item);
|
||||
const action = editor.createRemoveItemsAction(selection, true, mediaType, false);
|
||||
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the ripple-delete action");
|
||||
committed = context.project.executeTransaction(function (compoundAction) {
|
||||
if (compoundAction.addAction(action) === false) {
|
||||
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the ripple-delete action");
|
||||
}
|
||||
}, "Ripple delete timeline item");
|
||||
});
|
||||
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the ripple-delete transaction");
|
||||
|
||||
// The successor now occupies the removed target's coordinate. No
|
||||
// unrelated item field is read after the host transaction commits.
|
||||
const afterContext = await activeTarget(target, true);
|
||||
if (afterContext.projectGuid !== before.projectGuid || afterContext.sequenceId !== before.sequenceId) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere changed the active project or sequence during the committed ripple delete");
|
||||
}
|
||||
const after = await rippleReadback(afterContext, before);
|
||||
if (!sameRippleResult(before, after)) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested contiguous ripple-delete result");
|
||||
}
|
||||
return {
|
||||
rippleDeleted: true,
|
||||
before,
|
||||
after,
|
||||
outcome: "verified",
|
||||
verificationBoundary: "contiguous_successor_track_item_readback",
|
||||
undoLabel: "Ripple delete timeline item"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function activeTarget(args, requireTransaction) {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
|
||||
}
|
||||
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
||||
const target = targetCoordinates(args), resolved = await trackItemsAt(sequence, target);
|
||||
const item = resolved.items[target.clipIndex];
|
||||
if (!item) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
|
||||
return {
|
||||
project,
|
||||
sequence,
|
||||
projectGuid: requiredGuid(project.guid, "active project GUID"),
|
||||
sequenceId: requiredGuid(sequence.guid, "active sequence GUID"),
|
||||
...target,
|
||||
item,
|
||||
items: resolved.items
|
||||
};
|
||||
}
|
||||
|
||||
async function trackItemsAt(sequence, target) {
|
||||
const title = target.mediaType === "video" ? "Video" : "Audio";
|
||||
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
|
||||
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
|
||||
}
|
||||
const count = Number(await sequence[countMethod]());
|
||||
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
|
||||
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
|
||||
const track = await sequence[trackMethod](target.trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
|
||||
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
|
||||
}
|
||||
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
|
||||
if (!items.length) throw commandError("UXP_TARGET_NOT_FOUND", "The target track has no clip items");
|
||||
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
|
||||
return { items };
|
||||
}
|
||||
|
||||
async function rippleSnapshot(context) {
|
||||
const following = context.items[context.clipIndex + 1];
|
||||
if (!following) throw commandError("UXP_TARGET_UNSUPPORTED", "Ripple delete requires one immediate following clip item on the same track");
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
sequenceId: context.sequenceId,
|
||||
mediaType: context.mediaType,
|
||||
trackIndex: context.trackIndex,
|
||||
clipIndex: context.clipIndex,
|
||||
trackItemCount: context.items.length,
|
||||
target: await itemSnapshot(context.item, "target"),
|
||||
following: await itemSnapshot(following, "following")
|
||||
};
|
||||
}
|
||||
|
||||
async function rippleReadback(context, before) {
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
sequenceId: context.sequenceId,
|
||||
mediaType: context.mediaType,
|
||||
trackIndex: context.trackIndex,
|
||||
successorClipIndex: before.clipIndex,
|
||||
trackItemCount: context.items.length,
|
||||
successor: await itemSnapshot(context.item, "successor readback")
|
||||
};
|
||||
}
|
||||
|
||||
async function itemSnapshot(item, label) {
|
||||
const sourceItem = await requiredMethod(item, "getProjectItem")();
|
||||
const snapshot = {
|
||||
projectItemId: requiredIdentifier(await requiredMethod(sourceItem, "getId")(), label + " source project-item ID"),
|
||||
startSeconds: tickSeconds(await requiredMethod(item, "getStartTime")()),
|
||||
endSeconds: tickSeconds(await requiredMethod(item, "getEndTime")()),
|
||||
inSeconds: tickSeconds(await requiredMethod(item, "getInPoint")()),
|
||||
outSeconds: tickSeconds(await requiredMethod(item, "getOutPoint")()),
|
||||
durationSeconds: tickSeconds(await requiredMethod(item, "getDuration")()),
|
||||
speed: Number(await requiredMethod(item, "getSpeed")()),
|
||||
reversed: Boolean(await requiredMethod(item, "isSpeedReversed")())
|
||||
};
|
||||
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds"]) {
|
||||
if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0 || snapshot[key] > 86400) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " " + key);
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(snapshot.speed) || snapshot.speed < 0 || snapshot.speed > 100) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " speed");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function assertRippleSupported(snapshot) {
|
||||
if (snapshot.clipIndex >= snapshot.trackItemCount - 1) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "Ripple delete requires a following clip item to close the requested cut");
|
||||
}
|
||||
const target = snapshot.target, following = snapshot.following;
|
||||
if (target.endSeconds <= target.startSeconds || following.endSeconds <= following.startSeconds ||
|
||||
target.durationSeconds <= 0 || following.durationSeconds <= 0 ||
|
||||
!numbersEqual(target.endSeconds, following.startSeconds)) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "Ripple delete supports only positive-duration items with one contiguous same-track successor");
|
||||
}
|
||||
}
|
||||
|
||||
function sameRippleResult(before, after) {
|
||||
const targetTimelineDuration = before.target.endSeconds - before.target.startSeconds;
|
||||
const following = before.following, successor = after.successor;
|
||||
return after.projectGuid === before.projectGuid && after.sequenceId === before.sequenceId &&
|
||||
after.mediaType === before.mediaType && after.trackIndex === before.trackIndex &&
|
||||
after.successorClipIndex === before.clipIndex && after.trackItemCount === before.trackItemCount - 1 &&
|
||||
successor.projectItemId === following.projectItemId &&
|
||||
numbersEqual(successor.startSeconds, following.startSeconds - targetTimelineDuration) &&
|
||||
numbersEqual(successor.endSeconds, following.endSeconds - targetTimelineDuration) &&
|
||||
numbersEqual(successor.inSeconds, following.inSeconds) && numbersEqual(successor.outSeconds, following.outSeconds) &&
|
||||
numbersEqual(successor.durationSeconds, following.durationSeconds) &&
|
||||
numbersEqual(successor.speed, following.speed) && successor.reversed === following.reversed;
|
||||
}
|
||||
|
||||
function sameSnapshot(left, right) {
|
||||
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId &&
|
||||
left.mediaType === right.mediaType && left.trackIndex === right.trackIndex &&
|
||||
left.clipIndex === right.clipIndex && left.trackItemCount === right.trackItemCount &&
|
||||
sameItem(left.target, right.target) && sameItem(left.following, right.following);
|
||||
}
|
||||
|
||||
function sameItem(left, right) {
|
||||
return left.projectItemId === right.projectItemId && numbersEqual(left.startSeconds, right.startSeconds) &&
|
||||
numbersEqual(left.endSeconds, right.endSeconds) && numbersEqual(left.inSeconds, right.inSeconds) &&
|
||||
numbersEqual(left.outSeconds, right.outSeconds) && numbersEqual(left.durationSeconds, right.durationSeconds) &&
|
||||
numbersEqual(left.speed, right.speed) && left.reversed === right.reversed;
|
||||
}
|
||||
|
||||
function createSingleItemSelection(item) {
|
||||
let selection = null;
|
||||
const created = ppro.TrackItemSelection.createEmptySelection(function (value) { selection = value; });
|
||||
if (created !== true || !selection || typeof selection.addItem !== "function" || selection.addItem(item, false) !== true) {
|
||||
throw commandError("UXP_SELECTION_REJECTED", "Premiere rejected the bounded ripple-delete selection");
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
function targetCoordinates(args) {
|
||||
return {
|
||||
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
|
||||
trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"),
|
||||
clipIndex: nonNegativeInt(args.clipIndex, "clipIndex")
|
||||
};
|
||||
}
|
||||
|
||||
function requiredSnapshot(value) {
|
||||
assertObject(value, "expectedSnapshot");
|
||||
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "trackItemCount", "target", "following"];
|
||||
assertOnlyKeys(value, allowed);
|
||||
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + key + " is required");
|
||||
return {
|
||||
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
|
||||
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
|
||||
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]),
|
||||
trackIndex: nonNegativeInt(value.trackIndex, "expectedSnapshot.trackIndex"),
|
||||
clipIndex: nonNegativeInt(value.clipIndex, "expectedSnapshot.clipIndex"),
|
||||
trackItemCount: positiveInt(value.trackItemCount, "expectedSnapshot.trackItemCount"),
|
||||
target: requiredItemSnapshot(value.target, "expectedSnapshot.target"),
|
||||
following: requiredItemSnapshot(value.following, "expectedSnapshot.following")
|
||||
};
|
||||
}
|
||||
|
||||
function requiredItemSnapshot(value, name) {
|
||||
assertObject(value, name);
|
||||
const allowed = ["projectItemId", "startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds", "speed", "reversed"];
|
||||
assertOnlyKeys(value, allowed);
|
||||
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", name + "." + key + " is required");
|
||||
return {
|
||||
projectItemId: requiredIdentifier(value.projectItemId, name + ".projectItemId"),
|
||||
startSeconds: boundedSeconds(value.startSeconds, name + ".startSeconds"),
|
||||
endSeconds: boundedSeconds(value.endSeconds, name + ".endSeconds"),
|
||||
inSeconds: boundedSeconds(value.inSeconds, name + ".inSeconds"),
|
||||
outSeconds: boundedSeconds(value.outSeconds, name + ".outSeconds"),
|
||||
durationSeconds: boundedSeconds(value.durationSeconds, name + ".durationSeconds"),
|
||||
speed: boundedSpeed(value.speed, name + ".speed"),
|
||||
reversed: requiredBoolean(value.reversed, name + ".reversed")
|
||||
};
|
||||
}
|
||||
|
||||
function assertExpectedTarget(expected, target) {
|
||||
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot target coordinates must match mediaType, trackIndex, and clipIndex");
|
||||
}
|
||||
}
|
||||
|
||||
function localLock(key, operation) {
|
||||
const previous = fallbackTails.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const gate = new Promise(function (resolve) { release = resolve; });
|
||||
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
|
||||
fallbackTails.set(key, tail);
|
||||
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
|
||||
release();
|
||||
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
function requiredMethod(target, name) { if (!target || typeof target[name] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The required item does not expose " + name); return target[name].bind(target); }
|
||||
function assertObject(value, name) { if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an object"); }
|
||||
function assertOnlyKeys(value, allowed) { assertObject(value, "arguments"); for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw commandError("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key); }
|
||||
function enumValue(value, name, allowed) { if (allowed.indexOf(value) === -1) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; }
|
||||
function nonNegativeInt(value, name) { if (!Number.isSafeInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 to 511"); return value; }
|
||||
function positiveInt(value, name) { if (!Number.isSafeInteger(value) || value < 1 || value > 512) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 1 to 512"); return value; }
|
||||
function boundedSeconds(value, name) { const seconds = Number(value); if (!Number.isFinite(seconds) || seconds < 0 || seconds > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400"); return seconds; }
|
||||
function boundedSpeed(value, name) { const speed = Number(value); if (!Number.isFinite(speed) || speed < 0 || speed > 100) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 100"); return speed; }
|
||||
function requiredBoolean(value, name) { if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean"); return value; }
|
||||
function requireConfirmation(value) { if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "trackItem.rippleDelete requires confirmRippleDelete: true"); }
|
||||
function requireOperationId(value) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters"); }
|
||||
function requiredGuid(value, name) { const guid = guidString(value); if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return guid; }
|
||||
function requiredIdentifier(value, name) { const id = guidString(value); if (!id || id.length > 512 || id.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return id; }
|
||||
function guidString(value) { if (value == null) return ""; try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; } }
|
||||
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
|
||||
function numbersEqual(left, right) { return Number.isFinite(Number(left)) && Number.isFinite(Number(right)) && Math.abs(Number(left) - Number(right)) < 0.000001; }
|
||||
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
return { createRippleDeleteWorkflowDefinitions };
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpSequencePreviewFrameWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// This workflow intentionally owns its per-sequence lock instead of extending
|
||||
// the broad sequence-settings profile. A reviewed preview rectangle must not
|
||||
// be silently applied after another invocation has changed the same sequence.
|
||||
function createSequencePreviewFrameWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const tails = new Map();
|
||||
const definitions = {
|
||||
"sequence.previewFrame.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "26.3.0",
|
||||
probe: canUseSequencePreviewFrame,
|
||||
handler: inspectSequencePreviewFrame
|
||||
},
|
||||
"sequence.previewFrame.update": {
|
||||
destructive: true,
|
||||
undoable: true,
|
||||
idempotent: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "26.3.0",
|
||||
probe: canUseSequencePreviewFrame,
|
||||
handler: updateSequencePreviewFrame
|
||||
}
|
||||
};
|
||||
|
||||
function canUseSequencePreviewFrame() {
|
||||
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" && typeof ppro.RectF === "function");
|
||||
}
|
||||
|
||||
async function inspectSequencePreviewFrame(args) {
|
||||
assertOnlyKeys(args, ["sequenceId"]);
|
||||
const sequenceId = requiredGuid(args.sequenceId, "sequenceId");
|
||||
const first = await targetSequence(sequenceId, false);
|
||||
const before = await previewFrameSnapshot(first);
|
||||
const second = await targetSequence(sequenceId, false);
|
||||
const after = await previewFrameSnapshot(second);
|
||||
if (!sameSnapshot(before, after)) {
|
||||
throw commandError("UXP_STALE_SEQUENCE_PREVIEW_FRAME", "The reviewed project, sequence, or preview frame changed while it was being inspected");
|
||||
}
|
||||
return after;
|
||||
}
|
||||
|
||||
async function updateSequencePreviewFrame(args) {
|
||||
assertOnlyKeys(args, ["sequenceId", "previewWidth", "previewHeight", "expectedSnapshot", "confirmSetPreviewFrame", "operationId"]);
|
||||
const sequenceId = requiredGuid(args.sequenceId, "sequenceId");
|
||||
const requested = previewRect(args.previewWidth, args.previewHeight, "requested preview frame");
|
||||
const expected = requiredSnapshot(args.expectedSnapshot);
|
||||
requireConfirmation(args.confirmSetPreviewFrame);
|
||||
requireOperationId(args.operationId);
|
||||
if (expected.sequenceId !== sequenceId) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot.sequenceId must exactly match sequenceId");
|
||||
}
|
||||
// The complete reviewed owner identity is enough to join the tail before
|
||||
// *any* live preflight getter. If the project has changed, the guarded
|
||||
// snapshot inside the tail fails closed instead of selecting a new owner.
|
||||
const lockKey = expected.projectGuid + "\u0000" + expected.sequenceId;
|
||||
return withSequenceLock(lockKey, async function () {
|
||||
// Re-resolve only after joining the owner-specific tail. A distinct
|
||||
// operation ID cannot bypass the reviewed snapshot between preflight
|
||||
// and action construction inside this bridge process.
|
||||
let context;
|
||||
try {
|
||||
context = await targetSequence(sequenceId, true);
|
||||
} catch (error) {
|
||||
if (error && error.code === "UXP_TARGET_NOT_FOUND") {
|
||||
throw commandError("UXP_STALE_SEQUENCE_PREVIEW_FRAME", "The reviewed sequence no longer resolves before preview-frame action creation");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const before = await previewFrameSnapshot(context);
|
||||
if (!sameSnapshot(before, expected)) {
|
||||
throw commandError("UXP_STALE_SEQUENCE_PREVIEW_FRAME", "The reviewed preview frame changed before action creation");
|
||||
}
|
||||
const settings = await requiredMethod(context.sequence, "getSettings")();
|
||||
if (!settings || typeof settings.setPreviewFrameRect !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose SequenceSettings.setPreviewFrameRect for this sequence");
|
||||
}
|
||||
const nativeRect = nativeRectFor(requested);
|
||||
let accepted;
|
||||
try {
|
||||
accepted = await settings.setPreviewFrameRect(nativeRect);
|
||||
} catch (error) {
|
||||
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the requested preview frame rectangle: " + messageOf(error));
|
||||
}
|
||||
if (accepted !== true) throw commandError("UXP_ACTION_REJECTED", "Premiere did not accept the requested preview frame rectangle");
|
||||
|
||||
let committed = false;
|
||||
context.project.lockedAccess(function () {
|
||||
if (requiredGuid(context.project.guid, "active project GUID") !== before.projectGuid ||
|
||||
requiredGuid(context.sequence.guid, "target sequence GUID") !== before.sequenceId) {
|
||||
throw commandError("UXP_STALE_SEQUENCE_PREVIEW_FRAME", "The active project or reviewed sequence changed before preview-frame action creation");
|
||||
}
|
||||
if (typeof context.sequence.createSetSettingsAction !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose Sequence.createSetSettingsAction for this sequence");
|
||||
}
|
||||
const action = context.sequence.createSetSettingsAction(settings);
|
||||
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create a preview-frame settings action");
|
||||
committed = context.project.executeTransaction(function (compoundAction) {
|
||||
if (!compoundAction || typeof compoundAction.addAction !== "function" || compoundAction.addAction(action) === false) {
|
||||
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the preview-frame settings action");
|
||||
}
|
||||
}, "Set sequence preview frame");
|
||||
});
|
||||
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the preview-frame settings transaction");
|
||||
|
||||
const after = await previewFrameSnapshot(await targetSequence(sequenceId, true));
|
||||
if (after.projectGuid !== before.projectGuid || after.sequenceId !== before.sequenceId ||
|
||||
after.previewWidth !== requested.width || after.previewHeight !== requested.height) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested preview frame after the transaction");
|
||||
}
|
||||
return {
|
||||
previewFrameUpdated: true,
|
||||
before,
|
||||
after,
|
||||
outcome: "verified",
|
||||
verificationBoundary: "sequence_preview_frame_readback",
|
||||
undoLabel: "Set sequence preview frame"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function targetSequence(sequenceId, requireTransaction) {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
|
||||
}
|
||||
const projectGuid = requiredGuid(project.guid, "active project GUID");
|
||||
const sequences = await requiredMethod(project, "getSequences")();
|
||||
if (!Array.isArray(sequences)) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid sequence collection");
|
||||
if (sequences.length > 1024) throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 1024 sequences");
|
||||
const sequence = sequences.find(function (candidate) {
|
||||
return candidate && guidString(candidate.guid) === sequenceId;
|
||||
});
|
||||
if (!sequence) throw commandError("UXP_TARGET_NOT_FOUND", "sequenceId does not identify a sequence in the active project");
|
||||
return { project, projectGuid, sequence, sequenceId: requiredGuid(sequence.guid, "target sequence GUID") };
|
||||
}
|
||||
|
||||
async function previewFrameSnapshot(context) {
|
||||
const settings = await requiredMethod(context.sequence, "getSettings")();
|
||||
if (!settings || typeof settings.getPreviewFrameRect !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose SequenceSettings.getPreviewFrameRect for this sequence");
|
||||
}
|
||||
const rect = await settings.getPreviewFrameRect();
|
||||
const dimensions = previewRect(rect && rect.width, rect && rect.height, "native preview frame");
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
sequenceId: context.sequenceId,
|
||||
previewWidth: dimensions.width,
|
||||
previewHeight: dimensions.height
|
||||
};
|
||||
}
|
||||
|
||||
function nativeRectFor(requested) {
|
||||
let rect;
|
||||
try { rect = new ppro.RectF(); } catch (error) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere could not construct a documented RectF: " + messageOf(error));
|
||||
}
|
||||
if (!rect || typeof rect !== "object") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not construct a documented RectF");
|
||||
try {
|
||||
rect.width = requested.width;
|
||||
rect.height = requested.height;
|
||||
} catch (error) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere rejected the requested RectF dimensions: " + messageOf(error));
|
||||
}
|
||||
if (Number(rect.width) !== requested.width || Number(rect.height) !== requested.height) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere did not retain the requested RectF dimensions");
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
|
||||
function withSequenceLock(key, operation) {
|
||||
const previous = tails.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const gate = new Promise(function (resolve) { release = resolve; });
|
||||
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
|
||||
tails.set(key, tail);
|
||||
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
|
||||
release();
|
||||
if (tails.get(key) === tail) tails.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function assertObject(value, name) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", (name || "args") + " must be an object");
|
||||
}
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
assertObject(value, "args");
|
||||
const unknown = Object.keys(value).filter(function (key) { return !allowed.includes(key); });
|
||||
if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]);
|
||||
}
|
||||
function requiredMethod(value, method) {
|
||||
if (!value || typeof value[method] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose " + method + " for this target");
|
||||
return value[method].bind(value);
|
||||
}
|
||||
function requiredSnapshot(value) {
|
||||
assertObject(value, "expectedSnapshot");
|
||||
const keys = ["projectGuid", "sequenceId", "previewWidth", "previewHeight"];
|
||||
assertOnlyKeys(value, keys);
|
||||
for (let index = 0; index < keys.length; index += 1) {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, keys[index])) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + keys[index] + " is required");
|
||||
}
|
||||
}
|
||||
const dimensions = previewRect(value.previewWidth, value.previewHeight, "expectedSnapshot preview frame");
|
||||
return {
|
||||
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
|
||||
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
|
||||
previewWidth: dimensions.width,
|
||||
previewHeight: dimensions.height
|
||||
};
|
||||
}
|
||||
function previewRect(width, height, name) {
|
||||
return {
|
||||
width: boundedInt(width, name + ".width", 16, 10240),
|
||||
height: boundedInt(height, name + ".height", 16, 8192)
|
||||
};
|
||||
}
|
||||
function boundedInt(value, name, minimum, maximum) {
|
||||
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from " + minimum + " to " + maximum);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function requireConfirmation(value) {
|
||||
if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "sequence.previewFrame.update requires confirmSetPreviewFrame: true after reviewing the complete snapshot");
|
||||
}
|
||||
function requireOperationId(value) {
|
||||
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters");
|
||||
}
|
||||
function sameSnapshot(left, right) {
|
||||
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId &&
|
||||
left.previewWidth === right.previewWidth && left.previewHeight === right.previewHeight;
|
||||
}
|
||||
function requiredGuid(value, name) {
|
||||
const guid = guidString(value);
|
||||
if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
|
||||
return guid;
|
||||
}
|
||||
function guidString(value) {
|
||||
if (value == null) return "";
|
||||
try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; }
|
||||
}
|
||||
function messageOf(error) { return error && error.message ? String(error.message) : String(error || "unknown error"); }
|
||||
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
|
||||
|
||||
return { createSequencePreviewFrameWorkflowDefinitions };
|
||||
});
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpSlideWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// A slide is a narrow three-item trim composition: it moves the center item
|
||||
// without changing its source range, then retimes the immediate neighbours
|
||||
// to retain both adjacent cuts. It intentionally does not ripple, relink, or
|
||||
// infer source handles outside the exact readback below.
|
||||
function createSlideWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const fallbackTails = new Map();
|
||||
const locks = deps.locks && typeof deps.locks.withTrackMutationLock === "function"
|
||||
? deps.locks
|
||||
: { withTrackMutationLock: localLock };
|
||||
const definitions = {
|
||||
"trackItem.slide.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTrackItemSlide,
|
||||
handler: inspectTrackItemSlide
|
||||
},
|
||||
"trackItem.slide": {
|
||||
destructive: true,
|
||||
undoable: true,
|
||||
idempotent: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTrackItemSlide,
|
||||
handler: slideTrackItem
|
||||
}
|
||||
};
|
||||
|
||||
function canUseTrackItemSlide() {
|
||||
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function");
|
||||
}
|
||||
|
||||
async function inspectTrackItemSlide(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
|
||||
return slideSnapshot(await activeTriplet(args, false));
|
||||
}
|
||||
|
||||
async function slideTrackItem(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSnapshot", "slideBySeconds", "confirmSlide", "operationId"]);
|
||||
requireConfirmation(args.confirmSlide);
|
||||
requireOperationId(args.operationId);
|
||||
const target = targetCoordinates(args);
|
||||
const expected = requiredSnapshot(args.expectedSnapshot);
|
||||
const offset = signedOffset(args.slideBySeconds);
|
||||
assertExpectedTarget(expected, target);
|
||||
const initial = await activeTriplet(target, true);
|
||||
if (initial.projectGuid !== expected.projectGuid || initial.sequenceId !== expected.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence no longer matches the reviewed slide snapshot");
|
||||
}
|
||||
const key = initial.projectGuid + "\u0000" + initial.sequenceId + "\u0000" + target.mediaType + "\u0000" + target.trackIndex;
|
||||
return locks.withTrackMutationLock(key, async function () {
|
||||
// Read the complete affected triplet only after entering the shared
|
||||
// track tail. A different operation ID cannot reuse an old snapshot
|
||||
// after a preceding slide or source-only slip has committed.
|
||||
const context = await activeTriplet(target, true);
|
||||
if (context.projectGuid !== expected.projectGuid || context.sequenceId !== expected.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence changed before the slide transaction");
|
||||
}
|
||||
const before = await slideSnapshot(context);
|
||||
if (!sameSnapshot(before, expected)) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The target or either immediate neighbour changed since the reviewed slide snapshot");
|
||||
}
|
||||
assertSlideSupported(before);
|
||||
const desired = desiredSlide(before, offset);
|
||||
let committed = false;
|
||||
context.project.lockedAccess(function () {
|
||||
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed project or sequence changed before slide action creation");
|
||||
}
|
||||
const actions = [
|
||||
createAction(context.target, "createMoveAction", offset, "center move"),
|
||||
createAction(context.previous, "createSetEndAction", desired.previous.endSeconds, "previous timeline end"),
|
||||
createAction(context.previous, "createSetOutPointAction", desired.previous.outSeconds, "previous source out"),
|
||||
createAction(context.following, "createSetStartAction", desired.following.startSeconds, "following timeline start"),
|
||||
createAction(context.following, "createSetInPointAction", desired.following.inSeconds, "following source in")
|
||||
];
|
||||
committed = context.project.executeTransaction(function (compoundAction) {
|
||||
for (const action of actions) {
|
||||
if (compoundAction.addAction(action) === false) {
|
||||
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected a slide action");
|
||||
}
|
||||
}
|
||||
}, "Slide timeline item");
|
||||
});
|
||||
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the slide transaction");
|
||||
|
||||
// Re-resolve all coordinates. A failed postcondition can follow a
|
||||
// committed host transaction, so callers must inspect before another
|
||||
// mutation if this verification throws.
|
||||
const afterContext = await activeTriplet(target, true);
|
||||
if (afterContext.projectGuid !== before.projectGuid || afterContext.sequenceId !== before.sequenceId) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere changed the active project or sequence during the committed slide");
|
||||
}
|
||||
const after = await slideSnapshot(afterContext);
|
||||
if (!sameSlideResult(before, after, desired)) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested contiguous three-item slide");
|
||||
}
|
||||
return {
|
||||
slid: true,
|
||||
before,
|
||||
after,
|
||||
slideBySeconds: offset,
|
||||
outcome: "verified",
|
||||
verificationBoundary: "three_track_item_source_and_timeline_readback",
|
||||
undoLabel: "Slide timeline item"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function activeTriplet(args, requireTransaction) {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
|
||||
}
|
||||
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
||||
const target = targetCoordinates(args), projectGuid = requiredGuid(project.guid, "active project GUID"), sequenceId = requiredGuid(sequence.guid, "active sequence GUID");
|
||||
const items = await clipItemsAt(sequence, target);
|
||||
if (target.clipIndex < 1 || target.clipIndex + 1 >= items.length) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "A slide requires an immediate previous and following clip on the same track");
|
||||
}
|
||||
return {
|
||||
project, sequence, projectGuid, sequenceId, ...target,
|
||||
previous: items[target.clipIndex - 1], target: items[target.clipIndex], following: items[target.clipIndex + 1]
|
||||
};
|
||||
}
|
||||
|
||||
async function clipItemsAt(sequence, target) {
|
||||
const title = target.mediaType === "video" ? "Video" : "Audio";
|
||||
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
|
||||
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
|
||||
}
|
||||
const count = Number(await sequence[countMethod]());
|
||||
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
|
||||
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
|
||||
const track = await sequence[trackMethod](target.trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
|
||||
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
|
||||
}
|
||||
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
|
||||
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
|
||||
return items;
|
||||
}
|
||||
|
||||
async function slideSnapshot(context) {
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
sequenceId: context.sequenceId,
|
||||
mediaType: context.mediaType,
|
||||
trackIndex: context.trackIndex,
|
||||
clipIndex: context.clipIndex,
|
||||
previous: await itemSnapshot(context.previous, "previous"),
|
||||
target: await itemSnapshot(context.target, "target"),
|
||||
following: await itemSnapshot(context.following, "following")
|
||||
};
|
||||
}
|
||||
|
||||
async function itemSnapshot(item, label) {
|
||||
const snapshot = {
|
||||
startSeconds: tickSeconds(await requiredMethod(item, "getStartTime")()),
|
||||
endSeconds: tickSeconds(await requiredMethod(item, "getEndTime")()),
|
||||
inSeconds: tickSeconds(await requiredMethod(item, "getInPoint")()),
|
||||
outSeconds: tickSeconds(await requiredMethod(item, "getOutPoint")()),
|
||||
durationSeconds: tickSeconds(await requiredMethod(item, "getDuration")()),
|
||||
speed: Number(await requiredMethod(item, "getSpeed")()),
|
||||
reversed: Boolean(await requiredMethod(item, "isSpeedReversed")())
|
||||
};
|
||||
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds"]) {
|
||||
if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0 || snapshot[key] > 86400) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " " + key);
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(snapshot.speed) || snapshot.speed < 0 || snapshot.speed > 100) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + label + " speed");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function assertSlideSupported(snapshot) {
|
||||
for (const label of ["previous", "target", "following"]) {
|
||||
const item = snapshot[label];
|
||||
if (!numbersEqual(item.speed, 1) || item.reversed || !numbersEqual(item.endSeconds - item.startSeconds, item.durationSeconds) ||
|
||||
!numbersEqual(item.outSeconds - item.inSeconds, item.durationSeconds) || item.durationSeconds <= 0) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "Slide only supports contiguous forward 1x items with matching source and timeline durations");
|
||||
}
|
||||
}
|
||||
if (!numbersEqual(snapshot.previous.endSeconds, snapshot.target.startSeconds) || !numbersEqual(snapshot.target.endSeconds, snapshot.following.startSeconds)) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "Slide requires no gap or overlap at either adjacent cut");
|
||||
}
|
||||
}
|
||||
|
||||
function desiredSlide(before, offset) {
|
||||
const previous = { ...before.previous, endSeconds: before.previous.endSeconds + offset, outSeconds: before.previous.outSeconds + offset, durationSeconds: before.previous.durationSeconds + offset };
|
||||
const target = { ...before.target, startSeconds: before.target.startSeconds + offset, endSeconds: before.target.endSeconds + offset };
|
||||
const following = { ...before.following, startSeconds: before.following.startSeconds + offset, inSeconds: before.following.inSeconds + offset, durationSeconds: before.following.durationSeconds - offset };
|
||||
for (const item of [previous, target, following]) {
|
||||
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds"]) {
|
||||
if (!Number.isFinite(item[key]) || item[key] < 0 || item[key] > 86400) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "The requested slide exceeds documented timeline or source-time bounds");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previous.endSeconds <= previous.startSeconds || previous.outSeconds <= previous.inSeconds ||
|
||||
following.endSeconds <= following.startSeconds || following.outSeconds <= following.inSeconds) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "The requested slide would create a zero- or negative-duration neighbour");
|
||||
}
|
||||
return { previous, target, following };
|
||||
}
|
||||
|
||||
function sameSlideResult(before, after, desired) {
|
||||
return sameIdentity(before, after) &&
|
||||
sameItem(after.previous, desired.previous) && sameItem(after.target, desired.target) && sameItem(after.following, desired.following) &&
|
||||
numbersEqual(after.previous.endSeconds, after.target.startSeconds) && numbersEqual(after.target.endSeconds, after.following.startSeconds) &&
|
||||
numbersEqual(after.previous.endSeconds - after.previous.startSeconds, after.previous.durationSeconds) &&
|
||||
numbersEqual(after.target.endSeconds - after.target.startSeconds, after.target.durationSeconds) &&
|
||||
numbersEqual(after.following.endSeconds - after.following.startSeconds, after.following.durationSeconds) &&
|
||||
numbersEqual(after.previous.outSeconds - after.previous.inSeconds, after.previous.durationSeconds) &&
|
||||
numbersEqual(after.target.outSeconds - after.target.inSeconds, after.target.durationSeconds) &&
|
||||
numbersEqual(after.following.outSeconds - after.following.inSeconds, after.following.durationSeconds);
|
||||
}
|
||||
|
||||
function sameSnapshot(left, right) {
|
||||
return sameIdentity(left, right) && sameItem(left.previous, right.previous) && sameItem(left.target, right.target) && sameItem(left.following, right.following);
|
||||
}
|
||||
|
||||
function sameIdentity(left, right) {
|
||||
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId && left.mediaType === right.mediaType &&
|
||||
left.trackIndex === right.trackIndex && left.clipIndex === right.clipIndex;
|
||||
}
|
||||
|
||||
function sameItem(left, right) {
|
||||
return numbersEqual(left.startSeconds, right.startSeconds) && numbersEqual(left.endSeconds, right.endSeconds) &&
|
||||
numbersEqual(left.inSeconds, right.inSeconds) && numbersEqual(left.outSeconds, right.outSeconds) &&
|
||||
numbersEqual(left.durationSeconds, right.durationSeconds) && numbersEqual(left.speed, right.speed) && left.reversed === right.reversed;
|
||||
}
|
||||
|
||||
function createAction(item, method, seconds, label) {
|
||||
const action = requiredMethod(item, method)(ppro.TickTime.createWithSeconds(seconds));
|
||||
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the " + label + " action");
|
||||
return action;
|
||||
}
|
||||
|
||||
function targetCoordinates(args) {
|
||||
return { mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]), trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"), clipIndex: nonNegativeInt(args.clipIndex, "clipIndex") };
|
||||
}
|
||||
|
||||
function requiredSnapshot(value) {
|
||||
assertObject(value, "expectedSnapshot");
|
||||
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "previous", "target", "following"];
|
||||
assertOnlyKeys(value, allowed);
|
||||
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + key + " is required");
|
||||
return {
|
||||
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"), sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
|
||||
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]), trackIndex: nonNegativeInt(value.trackIndex, "expectedSnapshot.trackIndex"),
|
||||
clipIndex: nonNegativeInt(value.clipIndex, "expectedSnapshot.clipIndex"),
|
||||
previous: requiredItemSnapshot(value.previous, "expectedSnapshot.previous"), target: requiredItemSnapshot(value.target, "expectedSnapshot.target"), following: requiredItemSnapshot(value.following, "expectedSnapshot.following")
|
||||
};
|
||||
}
|
||||
|
||||
function requiredItemSnapshot(value, name) {
|
||||
assertObject(value, name);
|
||||
const allowed = ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds", "speed", "reversed"];
|
||||
assertOnlyKeys(value, allowed);
|
||||
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", name + "." + key + " is required");
|
||||
return {
|
||||
startSeconds: boundedSeconds(value.startSeconds, name + ".startSeconds"), endSeconds: boundedSeconds(value.endSeconds, name + ".endSeconds"),
|
||||
inSeconds: boundedSeconds(value.inSeconds, name + ".inSeconds"), outSeconds: boundedSeconds(value.outSeconds, name + ".outSeconds"),
|
||||
durationSeconds: boundedSeconds(value.durationSeconds, name + ".durationSeconds"), speed: boundedSpeed(value.speed, name + ".speed"), reversed: requiredBoolean(value.reversed, name + ".reversed")
|
||||
};
|
||||
}
|
||||
|
||||
function assertExpectedTarget(expected, target) {
|
||||
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot target coordinates must match mediaType, trackIndex, and clipIndex");
|
||||
}
|
||||
}
|
||||
|
||||
function signedOffset(value) {
|
||||
const offset = Number(value);
|
||||
if (!Number.isFinite(offset) || offset < -60 || offset > 60 || numbersEqual(offset, 0)) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "slideBySeconds must be a non-zero number from -60 to 60");
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
function localLock(key, operation) {
|
||||
const previous = fallbackTails.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const gate = new Promise(function (resolve) { release = resolve; });
|
||||
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
|
||||
fallbackTails.set(key, tail);
|
||||
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
|
||||
release();
|
||||
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
function requiredMethod(target, name) { if (!target || typeof target[name] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The timeline item does not expose " + name); return target[name].bind(target); }
|
||||
function assertObject(value, name) { if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an object"); }
|
||||
function assertOnlyKeys(value, allowed) { assertObject(value, "arguments"); for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw commandError("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key); }
|
||||
function enumValue(value, name, allowed) { if (allowed.indexOf(value) === -1) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", ")); return value; }
|
||||
function nonNegativeInt(value, name) { if (!Number.isSafeInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 to 511"); return value; }
|
||||
function boundedSeconds(value, name) { const seconds = Number(value); if (!Number.isFinite(seconds) || seconds < 0 || seconds > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400"); return seconds; }
|
||||
function boundedSpeed(value, name) { const speed = Number(value); if (!Number.isFinite(speed) || speed < 0 || speed > 100) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 100"); return speed; }
|
||||
function requiredBoolean(value, name) { if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean"); return value; }
|
||||
function requireConfirmation(value) { if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "trackItem.slide requires confirmSlide: true"); }
|
||||
function requireOperationId(value) { if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters"); }
|
||||
function requiredGuid(value, name) { const guid = guidString(value); if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid"); return guid; }
|
||||
function guidString(value) { if (value == null) return ""; try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; } }
|
||||
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
|
||||
function numbersEqual(left, right) { return Number.isFinite(Number(left)) && Number.isFinite(Number(right)) && Math.abs(Number(left) - Number(right)) < 0.000001; }
|
||||
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
return { createSlideWorkflowDefinitions };
|
||||
});
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpSlipWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// A slip is deliberately a separate, narrow operation rather than a
|
||||
// convenience alias for trackItem.update: it preserves timeline timing,
|
||||
// requires the complete reviewed state, and serializes its whole guarded
|
||||
// preflight/transaction/readback boundary per target.
|
||||
function createSlipWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const fallbackTails = new Map();
|
||||
const locks = deps.locks && typeof deps.locks.withTrackMutationLock === "function"
|
||||
? deps.locks
|
||||
: { withTrackMutationLock: localLock };
|
||||
const definitions = {
|
||||
"trackItem.slip.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTrackItemSlip,
|
||||
handler: inspectTrackItemSlip
|
||||
},
|
||||
"trackItem.slip": {
|
||||
destructive: true,
|
||||
undoable: true,
|
||||
idempotent: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTrackItemSlip,
|
||||
handler: slipTrackItem
|
||||
}
|
||||
};
|
||||
|
||||
function canUseTrackItemSlip() {
|
||||
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.TickTime && typeof ppro.TickTime.createWithSeconds === "function");
|
||||
}
|
||||
|
||||
async function inspectTrackItemSlip(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
|
||||
const context = await activeTarget(args, false);
|
||||
return await slipSnapshot(context);
|
||||
}
|
||||
|
||||
async function slipTrackItem(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "expectedSnapshot", "slipBySeconds", "confirmSlip", "operationId"]);
|
||||
requireConfirmation(args.confirmSlip);
|
||||
requireOperationId(args.operationId);
|
||||
const target = targetCoordinates(args);
|
||||
const expected = requiredSnapshot(args.expectedSnapshot);
|
||||
const requestedOffset = signedOffset(args.slipBySeconds);
|
||||
assertExpectedTarget(expected, target);
|
||||
const initial = await activeTarget(target, true);
|
||||
if (expected.projectGuid !== initial.projectGuid || expected.sequenceId !== initial.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence no longer matches the reviewed slip snapshot");
|
||||
}
|
||||
// Slides can trim either immediate neighbour, so slips and slides share
|
||||
// a track-level lock rather than allowing different command families to
|
||||
// pass one another with independently reviewed snapshots.
|
||||
const key = initial.projectGuid + "\u0000" + initial.sequenceId + "\u0000" + target.mediaType + "\u0000" + target.trackIndex;
|
||||
return locks.withTrackMutationLock(key, async function () {
|
||||
// Resolve the active target only after entering the per-item tail so
|
||||
// a different operation ID cannot reuse a snapshot taken before a
|
||||
// prior slip completed.
|
||||
const context = await activeTarget(target, true);
|
||||
if (context.projectGuid !== expected.projectGuid || context.sequenceId !== expected.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The active project or sequence changed before the slip transaction");
|
||||
}
|
||||
const before = await slipSnapshot(context);
|
||||
if (!sameSnapshot(before, expected)) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The timeline item changed since the reviewed slip snapshot");
|
||||
}
|
||||
assertSlipSupported(before);
|
||||
const desired = desiredSlip(before, requestedOffset);
|
||||
// The complete asynchronous snapshot is captured immediately before
|
||||
// this synchronous action-creation boundary. The keyed tail prevents
|
||||
// another MCP slip from interleaving between this check and readback.
|
||||
let committed = false;
|
||||
context.project.lockedAccess(function () {
|
||||
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
|
||||
throw commandError("UXP_STALE_TRACK_ITEM", "The reviewed project or sequence changed before slip action creation");
|
||||
}
|
||||
const inAction = createPointAction(context.item, "createSetInPointAction", desired.inSeconds, "source in point");
|
||||
const outAction = createPointAction(context.item, "createSetOutPointAction", desired.outSeconds, "source out point");
|
||||
committed = context.project.executeTransaction(function (compoundAction) {
|
||||
if (compoundAction.addAction(inAction) === false || compoundAction.addAction(outAction) === false) {
|
||||
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected a slip action");
|
||||
}
|
||||
}, "Slip timeline item source");
|
||||
});
|
||||
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the slip transaction");
|
||||
|
||||
// Resolve by coordinate again rather than trusting the retained object.
|
||||
// An action may have committed even when this verification fails, so
|
||||
// callers must inspect before issuing another edit after that error.
|
||||
const afterContext = await activeTarget(target, true);
|
||||
if (afterContext.projectGuid !== before.projectGuid || afterContext.sequenceId !== before.sequenceId) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere changed the active project or sequence during the committed slip");
|
||||
}
|
||||
const after = await slipSnapshot(afterContext);
|
||||
if (!sameSlipResult(before, after, desired)) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested source-only slip");
|
||||
}
|
||||
return {
|
||||
slipped: true,
|
||||
before,
|
||||
after,
|
||||
slipBySeconds: requestedOffset,
|
||||
outcome: "verified",
|
||||
verificationBoundary: "track_item_source_and_timeline_readback",
|
||||
undoLabel: "Slip timeline item source"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function activeTarget(args, requireTransaction) {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
|
||||
}
|
||||
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
||||
const projectGuid = requiredGuid(project.guid, "active project GUID"), sequenceId = requiredGuid(sequence.guid, "active sequence GUID");
|
||||
const target = targetCoordinates(args), item = await trackItemAt(sequence, target);
|
||||
return { project, sequence, item, projectGuid, sequenceId, ...target };
|
||||
}
|
||||
|
||||
async function trackItemAt(sequence, target) {
|
||||
const title = target.mediaType === "video" ? "Video" : "Audio";
|
||||
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
|
||||
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
|
||||
}
|
||||
const count = Number(await sequence[countMethod]());
|
||||
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
|
||||
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
|
||||
const track = await sequence[trackMethod](target.trackIndex);
|
||||
const itemType = ppro.Constants && ppro.Constants.TrackItemType;
|
||||
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
|
||||
}
|
||||
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
|
||||
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
|
||||
const item = items[target.clipIndex];
|
||||
if (!item) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
|
||||
return item;
|
||||
}
|
||||
|
||||
async function slipSnapshot(context) {
|
||||
const snapshot = {
|
||||
projectGuid: context.projectGuid,
|
||||
sequenceId: context.sequenceId,
|
||||
mediaType: context.mediaType,
|
||||
trackIndex: context.trackIndex,
|
||||
clipIndex: context.clipIndex,
|
||||
startSeconds: tickSeconds(await requiredMethod(context.item, "getStartTime")()),
|
||||
endSeconds: tickSeconds(await requiredMethod(context.item, "getEndTime")()),
|
||||
inSeconds: tickSeconds(await requiredMethod(context.item, "getInPoint")()),
|
||||
outSeconds: tickSeconds(await requiredMethod(context.item, "getOutPoint")()),
|
||||
durationSeconds: tickSeconds(await requiredMethod(context.item, "getDuration")()),
|
||||
speed: Number(await requiredMethod(context.item, "getSpeed")()),
|
||||
reversed: Boolean(await requiredMethod(context.item, "isSpeedReversed")())
|
||||
};
|
||||
for (const key of ["startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds"]) {
|
||||
if (!Number.isFinite(snapshot[key]) || snapshot[key] < 0 || snapshot[key] > 86400) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + key + " for the timeline item");
|
||||
}
|
||||
}
|
||||
if (!Number.isFinite(snapshot.speed) || snapshot.speed < 0 || snapshot.speed > 100) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid timeline-item speed");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function desiredSlip(before, offset) {
|
||||
const sourceDuration = before.outSeconds - before.inSeconds;
|
||||
if (!numbersEqual(before.endSeconds - before.startSeconds, before.durationSeconds) ||
|
||||
!numbersEqual(sourceDuration, before.durationSeconds) || sourceDuration <= 0) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "Slip requires an unchanged forward 1x timeline and source duration");
|
||||
}
|
||||
const inSeconds = before.inSeconds + offset, outSeconds = before.outSeconds + offset;
|
||||
if (inSeconds < 0 || outSeconds > 86400 || outSeconds <= inSeconds) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "The requested slip exceeds the documented source-time bounds");
|
||||
}
|
||||
return { inSeconds, outSeconds, sourceDuration };
|
||||
}
|
||||
|
||||
function assertSlipSupported(snapshot) {
|
||||
if (!numbersEqual(snapshot.speed, 1) || snapshot.reversed) {
|
||||
throw commandError("UXP_TARGET_UNSUPPORTED", "Slip only supports forward 1x track items");
|
||||
}
|
||||
}
|
||||
|
||||
function sameSlipResult(before, after, desired) {
|
||||
return sameSnapshotIdentity(before, after) &&
|
||||
numbersEqual(after.startSeconds, before.startSeconds) &&
|
||||
numbersEqual(after.endSeconds, before.endSeconds) &&
|
||||
numbersEqual(after.durationSeconds, before.durationSeconds) &&
|
||||
numbersEqual(after.speed, before.speed) && after.reversed === before.reversed &&
|
||||
numbersEqual(after.inSeconds, desired.inSeconds) && numbersEqual(after.outSeconds, desired.outSeconds) &&
|
||||
numbersEqual(after.outSeconds - after.inSeconds, desired.sourceDuration);
|
||||
}
|
||||
|
||||
function sameSnapshot(left, right) {
|
||||
return sameSnapshotIdentity(left, right) &&
|
||||
numbersEqual(left.startSeconds, right.startSeconds) &&
|
||||
numbersEqual(left.endSeconds, right.endSeconds) &&
|
||||
numbersEqual(left.inSeconds, right.inSeconds) &&
|
||||
numbersEqual(left.outSeconds, right.outSeconds) &&
|
||||
numbersEqual(left.durationSeconds, right.durationSeconds) &&
|
||||
numbersEqual(left.speed, right.speed) && left.reversed === right.reversed;
|
||||
}
|
||||
|
||||
function sameSnapshotIdentity(left, right) {
|
||||
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId &&
|
||||
left.mediaType === right.mediaType && left.trackIndex === right.trackIndex && left.clipIndex === right.clipIndex;
|
||||
}
|
||||
|
||||
function localLock(key, operation) {
|
||||
const previous = fallbackTails.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const gate = new Promise(function (resolve) { release = resolve; });
|
||||
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
|
||||
fallbackTails.set(key, tail);
|
||||
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
|
||||
release();
|
||||
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
function createPointAction(item, method, seconds, label) {
|
||||
const creator = requiredMethod(item, method);
|
||||
const action = creator(ppro.TickTime.createWithSeconds(seconds));
|
||||
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the " + label + " action");
|
||||
return action;
|
||||
}
|
||||
|
||||
function targetCoordinates(args) {
|
||||
return {
|
||||
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
|
||||
trackIndex: nonNegativeInt(args.trackIndex, "trackIndex"),
|
||||
clipIndex: nonNegativeInt(args.clipIndex, "clipIndex")
|
||||
};
|
||||
}
|
||||
|
||||
function requiredSnapshot(value) {
|
||||
assertObject(value, "expectedSnapshot");
|
||||
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "startSeconds", "endSeconds", "inSeconds", "outSeconds", "durationSeconds", "speed", "reversed"];
|
||||
assertOnlyKeys(value, allowed);
|
||||
for (const key of allowed) if (!(key in value)) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + key + " is required");
|
||||
return {
|
||||
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
|
||||
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
|
||||
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]),
|
||||
trackIndex: nonNegativeInt(value.trackIndex, "expectedSnapshot.trackIndex"),
|
||||
clipIndex: nonNegativeInt(value.clipIndex, "expectedSnapshot.clipIndex"),
|
||||
startSeconds: boundedSeconds(value.startSeconds, "expectedSnapshot.startSeconds"),
|
||||
endSeconds: boundedSeconds(value.endSeconds, "expectedSnapshot.endSeconds"),
|
||||
inSeconds: boundedSeconds(value.inSeconds, "expectedSnapshot.inSeconds"),
|
||||
outSeconds: boundedSeconds(value.outSeconds, "expectedSnapshot.outSeconds"),
|
||||
durationSeconds: boundedSeconds(value.durationSeconds, "expectedSnapshot.durationSeconds"),
|
||||
speed: boundedSpeed(value.speed, "expectedSnapshot.speed"),
|
||||
reversed: requiredBoolean(value.reversed, "expectedSnapshot.reversed")
|
||||
};
|
||||
}
|
||||
|
||||
function assertExpectedTarget(expected, target) {
|
||||
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot target coordinates must match mediaType, trackIndex, and clipIndex");
|
||||
}
|
||||
}
|
||||
|
||||
function signedOffset(value) {
|
||||
const offset = Number(value);
|
||||
if (!Number.isFinite(offset) || offset < -60 || offset > 60 || numbersEqual(offset, 0)) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "slipBySeconds must be a non-zero number from -60 to 60");
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
function requiredMethod(target, name) {
|
||||
if (!target || typeof target[name] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "The timeline item does not expose " + name);
|
||||
return target[name].bind(target);
|
||||
}
|
||||
|
||||
function requiredGuid(value, name) {
|
||||
const guid = guidString(value);
|
||||
if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
|
||||
return guid;
|
||||
}
|
||||
|
||||
function guidString(value) {
|
||||
if (value == null) return "";
|
||||
try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; }
|
||||
}
|
||||
|
||||
function tickSeconds(value) {
|
||||
const seconds = value && Number(value.seconds);
|
||||
return Number.isFinite(seconds) ? seconds : null;
|
||||
}
|
||||
|
||||
function numbersEqual(left, right) {
|
||||
return Number.isFinite(Number(left)) && Number.isFinite(Number(right)) && Math.abs(Number(left) - Number(right)) < 0.000001;
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function assertObject(value, name) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an object");
|
||||
}
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
assertObject(value, "arguments");
|
||||
for (const key of Object.keys(value)) if (allowed.indexOf(key) === -1) throw commandError("UXP_INVALID_ARGUMENT", "Unexpected argument: " + key);
|
||||
}
|
||||
function enumValue(value, name, allowed) {
|
||||
if (allowed.indexOf(value) === -1) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", "));
|
||||
return value;
|
||||
}
|
||||
function nonNegativeInt(value, name) {
|
||||
if (!Number.isSafeInteger(value) || value < 0 || value > 511) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from 0 to 511");
|
||||
return value;
|
||||
}
|
||||
function boundedSeconds(value, name) {
|
||||
const seconds = Number(value);
|
||||
if (!Number.isFinite(seconds) || seconds < 0 || seconds > 86400) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400");
|
||||
return seconds;
|
||||
}
|
||||
function boundedSpeed(value, name) {
|
||||
const speed = Number(value);
|
||||
if (!Number.isFinite(speed) || speed < 0 || speed > 100) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 100");
|
||||
return speed;
|
||||
}
|
||||
function requiredBoolean(value, name) {
|
||||
if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean");
|
||||
return value;
|
||||
}
|
||||
function requireConfirmation(value) {
|
||||
if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "confirmSlip must be true after reviewing the complete slip snapshot");
|
||||
}
|
||||
function requireOperationId(value) {
|
||||
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be a 1-128 character operation token");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function commandError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
return { createSlipWorkflowDefinitions };
|
||||
});
|
||||
@@ -0,0 +1,204 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpSourceMediaProvenanceWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// This command intentionally resolves only the explicitly requested item
|
||||
// twice. It must not invoke media-path getters while traversing unrelated
|
||||
// project items because those values are sensitive and are outside the
|
||||
// caller's bounded target.
|
||||
function createSourceMediaProvenanceWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
return {
|
||||
"source.provenance.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "26.3.0",
|
||||
probe: canInspectSourceProvenance,
|
||||
handler: inspectSourceProvenance
|
||||
}
|
||||
};
|
||||
|
||||
function canInspectSourceProvenance() {
|
||||
return !!(
|
||||
ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.FolderItem && typeof ppro.FolderItem.cast === "function" &&
|
||||
ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function"
|
||||
);
|
||||
}
|
||||
|
||||
async function inspectSourceProvenance(args) {
|
||||
const request = requestFrom(args);
|
||||
const first = await sourceProvenanceSnapshot(request);
|
||||
const second = await sourceProvenanceSnapshot(request);
|
||||
if (!sameSnapshot(first, second)) {
|
||||
throw commandError(
|
||||
"UXP_STALE_SOURCE_PROVENANCE",
|
||||
"The active project, requested project item, or requested provenance path changed while it was being inspected"
|
||||
);
|
||||
}
|
||||
return second;
|
||||
}
|
||||
|
||||
function requestFrom(args) {
|
||||
assertOnlyKeys(args, ["projectItemId", "includeMediaFilePath", "includeOriginatingProjectPath"]);
|
||||
const includeMediaFilePath = optionalBoolean(args.includeMediaFilePath, "includeMediaFilePath");
|
||||
const includeOriginatingProjectPath = optionalBoolean(args.includeOriginatingProjectPath, "includeOriginatingProjectPath");
|
||||
if (!includeMediaFilePath && !includeOriginatingProjectPath) {
|
||||
throw commandError(
|
||||
"UXP_PATH_DISCLOSURE_REQUIRED",
|
||||
"Set includeMediaFilePath or includeOriginatingProjectPath to true before a native path is read"
|
||||
);
|
||||
}
|
||||
return {
|
||||
projectItemId: requiredText(args.projectItemId, "projectItemId", 512),
|
||||
includeMediaFilePath,
|
||||
includeOriginatingProjectPath
|
||||
};
|
||||
}
|
||||
|
||||
async function sourceProvenanceSnapshot(request) {
|
||||
const project = await activeProject();
|
||||
const projectGuid = requiredGuid(project.guid, "active project GUID");
|
||||
const rootItem = await requiredMethod(project, "getRootItem")();
|
||||
const projectItem = await findProjectItem(rootItem, request.projectItemId);
|
||||
if (!projectItem) {
|
||||
throw commandError("UXP_TARGET_NOT_FOUND", "projectItemId does not identify an item in the active project");
|
||||
}
|
||||
const resolvedId = requiredText(requiredMethod(projectItem, "getId")(), "resolved project item ID", 512);
|
||||
if (resolvedId !== request.projectItemId) {
|
||||
throw commandError("UXP_STALE_SOURCE_PROVENANCE", "The resolved project item ID changed during provenance inspection");
|
||||
}
|
||||
const clipProjectItem = castClipProjectItem(projectItem);
|
||||
const readbackId = requiredText(requiredMethod(projectItem, "getId")(), "resolved project item ID", 512);
|
||||
if (readbackId !== request.projectItemId) {
|
||||
throw commandError("UXP_STALE_SOURCE_PROVENANCE", "The resolved project item changed before provenance getters ran");
|
||||
}
|
||||
const snapshot = { projectGuid, projectItemId: readbackId };
|
||||
if (request.includeMediaFilePath) {
|
||||
snapshot.mediaFilePath = boundedPath(await requiredMethod(clipProjectItem, "getMediaFilePath")(), "media file path");
|
||||
}
|
||||
if (request.includeOriginatingProjectPath) {
|
||||
snapshot.originatingProjectPath = boundedPath(await requiredMethod(clipProjectItem, "getOriginatingProjectPath")(), "originating project path");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async function activeProject() {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
return project;
|
||||
}
|
||||
|
||||
async function findProjectItem(rootItem, expectedId) {
|
||||
if (!rootItem || typeof rootItem !== "object") {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid project root item");
|
||||
}
|
||||
const pending = [rootItem];
|
||||
const visited = new Set();
|
||||
while (pending.length) {
|
||||
const candidate = pending.shift();
|
||||
const candidateId = requiredText(requiredMethod(candidate, "getId")(), "project item ID", 512);
|
||||
if (visited.has(candidateId)) continue;
|
||||
visited.add(candidateId);
|
||||
if (visited.size > 4096) {
|
||||
throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 4096 reachable project items");
|
||||
}
|
||||
if (candidateId === expectedId) return candidate;
|
||||
const folder = castFolderItem(candidate);
|
||||
if (!folder) continue;
|
||||
const children = await requiredMethod(folder, "getItems")();
|
||||
if (!Array.isArray(children)) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid project folder collection");
|
||||
}
|
||||
if (children.length > 4096 || pending.length + children.length + visited.size > 4096) {
|
||||
throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 4096 reachable project items");
|
||||
}
|
||||
for (let index = 0; index < children.length; index += 1) pending.push(children[index]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function castFolderItem(projectItem) {
|
||||
if (!ppro.FolderItem || typeof ppro.FolderItem.cast !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere FolderItem APIs are unavailable");
|
||||
}
|
||||
try {
|
||||
return ppro.FolderItem.cast(projectItem) || null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function castClipProjectItem(projectItem) {
|
||||
if (!ppro.ClipProjectItem || typeof ppro.ClipProjectItem.cast !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere ClipProjectItem APIs are unavailable");
|
||||
}
|
||||
try {
|
||||
const clip = ppro.ClipProjectItem.cast(projectItem);
|
||||
if (clip) return clip;
|
||||
} catch (_) {
|
||||
// A non-clip project item cannot expose the two documented provenance
|
||||
// getters. Preserve that distinction instead of treating it as absent.
|
||||
}
|
||||
throw commandError("UXP_TARGET_NOT_MEDIA", "projectItemId does not identify a media-backed ClipProjectItem");
|
||||
}
|
||||
}
|
||||
|
||||
function assertObject(value, name) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", (name || "args") + " must be an object");
|
||||
}
|
||||
}
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
assertObject(value, "args");
|
||||
const unknown = Object.keys(value).filter(function (key) { return !allowed.includes(key); });
|
||||
if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]);
|
||||
}
|
||||
function optionalBoolean(value, name) {
|
||||
if (value === undefined) return false;
|
||||
if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean");
|
||||
return value;
|
||||
}
|
||||
function requiredMethod(value, method) {
|
||||
if (!value || typeof value[method] !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose " + method + " for this target");
|
||||
}
|
||||
return value[method].bind(value);
|
||||
}
|
||||
function requiredText(value, name, maximum) {
|
||||
if (typeof value !== "string" || !value.length || value.length > maximum || value.indexOf("\u0000") !== -1) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function requiredGuid(value, name) {
|
||||
let guid = "";
|
||||
try { guid = value == null ? "" : typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { guid = ""; }
|
||||
return requiredText(guid, name, 128);
|
||||
}
|
||||
function boundedPath(value, name) {
|
||||
if (typeof value !== "string" || value.length > 4096 || value.indexOf("\u0000") !== -1) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + name);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function sameSnapshot(left, right) {
|
||||
return left.projectGuid === right.projectGuid &&
|
||||
left.projectItemId === right.projectItemId &&
|
||||
left.mediaFilePath === right.mediaFilePath &&
|
||||
left.originatingProjectPath === right.originatingProjectPath;
|
||||
}
|
||||
function commandError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
return { createSourceMediaProvenanceWorkflowDefinitions };
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpSourceProxyWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// Project-item traversal intentionally reads only identifiers and folder
|
||||
// membership. Native proxy getters run only on the requested item, and the
|
||||
// optional proxy path getter runs only when the caller explicitly asks for it.
|
||||
function createSourceProxyWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
return {
|
||||
"source.proxy.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "26.3.0",
|
||||
probe: canInspectSourceProxy,
|
||||
handler: inspectSourceProxy
|
||||
}
|
||||
};
|
||||
|
||||
function canInspectSourceProxy() {
|
||||
return !!(
|
||||
ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.FolderItem && typeof ppro.FolderItem.cast === "function" &&
|
||||
ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function"
|
||||
);
|
||||
}
|
||||
|
||||
async function inspectSourceProxy(args) {
|
||||
const request = requestFrom(args);
|
||||
const first = await sourceProxySnapshot(request);
|
||||
const second = await sourceProxySnapshot(request);
|
||||
if (!sameSnapshot(first, second)) {
|
||||
throw commandError(
|
||||
"UXP_STALE_SOURCE_PROXY",
|
||||
"The active project, requested project item, or requested proxy state changed while it was being inspected"
|
||||
);
|
||||
}
|
||||
return second;
|
||||
}
|
||||
|
||||
function requestFrom(args) {
|
||||
assertOnlyKeys(args, ["projectItemId", "includeProxyPath"]);
|
||||
return {
|
||||
projectItemId: requiredText(args.projectItemId, "projectItemId", 512),
|
||||
includeProxyPath: optionalBoolean(args.includeProxyPath, "includeProxyPath")
|
||||
};
|
||||
}
|
||||
|
||||
async function sourceProxySnapshot(request) {
|
||||
const project = await activeProject();
|
||||
const projectGuid = requiredGuid(project.guid, "active project GUID");
|
||||
const rootItem = await requiredMethod(project, "getRootItem")();
|
||||
const projectItem = await findProjectItem(rootItem, request.projectItemId);
|
||||
if (!projectItem) {
|
||||
throw commandError("UXP_TARGET_NOT_FOUND", "projectItemId does not identify an item in the active project");
|
||||
}
|
||||
const resolvedId = requiredText(requiredMethod(projectItem, "getId")(), "resolved project item ID", 512);
|
||||
if (resolvedId !== request.projectItemId) {
|
||||
throw commandError("UXP_STALE_SOURCE_PROXY", "The resolved project item ID changed during proxy inspection");
|
||||
}
|
||||
const clipProjectItem = castClipProjectItem(projectItem);
|
||||
const readbackId = requiredText(requiredMethod(projectItem, "getId")(), "resolved project item ID", 512);
|
||||
if (readbackId !== request.projectItemId) {
|
||||
throw commandError("UXP_STALE_SOURCE_PROXY", "The resolved project item changed before proxy getters ran");
|
||||
}
|
||||
const snapshot = {
|
||||
projectGuid,
|
||||
projectItemId: readbackId,
|
||||
canChangeMediaPath: requiredBoolean(await requiredMethod(clipProjectItem, "canChangeMediaPath")(), "canChangeMediaPath"),
|
||||
isOffline: requiredBoolean(await requiredMethod(clipProjectItem, "isOffline")(), "isOffline"),
|
||||
canProxy: requiredBoolean(await requiredMethod(clipProjectItem, "canProxy")(), "canProxy"),
|
||||
hasProxy: requiredBoolean(await requiredMethod(clipProjectItem, "hasProxy")(), "hasProxy")
|
||||
};
|
||||
if (request.includeProxyPath && snapshot.hasProxy) {
|
||||
snapshot.proxyPath = boundedPath(await requiredMethod(clipProjectItem, "getProxyPath")(), "proxy path");
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
async function activeProject() {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
return project;
|
||||
}
|
||||
|
||||
async function findProjectItem(rootItem, expectedId) {
|
||||
if (!rootItem || typeof rootItem !== "object") {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid project root item");
|
||||
}
|
||||
const pending = [rootItem];
|
||||
const visited = new Set();
|
||||
while (pending.length) {
|
||||
const candidate = pending.shift();
|
||||
const candidateId = requiredText(requiredMethod(candidate, "getId")(), "project item ID", 512);
|
||||
if (visited.has(candidateId)) continue;
|
||||
visited.add(candidateId);
|
||||
if (visited.size > 4096) {
|
||||
throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 4096 reachable project items");
|
||||
}
|
||||
if (candidateId === expectedId) return candidate;
|
||||
const folder = castFolderItem(candidate);
|
||||
if (!folder) continue;
|
||||
const children = await requiredMethod(folder, "getItems")();
|
||||
if (!Array.isArray(children)) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid project folder collection");
|
||||
}
|
||||
if (children.length > 4096 || pending.length + children.length + visited.size > 4096) {
|
||||
throw commandError("UXP_TARGET_TOO_LARGE", "The active project has more than 4096 reachable project items");
|
||||
}
|
||||
for (let index = 0; index < children.length; index += 1) pending.push(children[index]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function castFolderItem(projectItem) {
|
||||
if (!ppro.FolderItem || typeof ppro.FolderItem.cast !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere FolderItem APIs are unavailable");
|
||||
}
|
||||
try {
|
||||
return ppro.FolderItem.cast(projectItem) || null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function castClipProjectItem(projectItem) {
|
||||
if (!ppro.ClipProjectItem || typeof ppro.ClipProjectItem.cast !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere ClipProjectItem APIs are unavailable");
|
||||
}
|
||||
try {
|
||||
const clip = ppro.ClipProjectItem.cast(projectItem);
|
||||
if (clip) return clip;
|
||||
} catch (_) {
|
||||
// A non-clip Project item does not expose the documented proxy getters.
|
||||
}
|
||||
throw commandError("UXP_TARGET_NOT_MEDIA", "projectItemId does not identify a media-backed ClipProjectItem");
|
||||
}
|
||||
}
|
||||
|
||||
function assertObject(value, name) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", (name || "args") + " must be an object");
|
||||
}
|
||||
}
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
assertObject(value, "args");
|
||||
const unknown = Object.keys(value).filter(function (key) { return !allowed.includes(key); });
|
||||
if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]);
|
||||
}
|
||||
function optionalBoolean(value, name) {
|
||||
if (value === undefined) return false;
|
||||
if (typeof value !== "boolean") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a boolean");
|
||||
return value;
|
||||
}
|
||||
function requiredMethod(value, method) {
|
||||
if (!value || typeof value[method] !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose " + method + " for this target");
|
||||
}
|
||||
return value[method].bind(value);
|
||||
}
|
||||
function requiredText(value, name, maximum) {
|
||||
if (typeof value !== "string" || !value.length || value.length > maximum || value.indexOf("\u0000") !== -1) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function requiredGuid(value, name) {
|
||||
let guid = "";
|
||||
try { guid = value == null ? "" : typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { guid = ""; }
|
||||
return requiredText(guid, name, 128);
|
||||
}
|
||||
function requiredBoolean(value, name) {
|
||||
if (typeof value !== "boolean") {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + name + " value");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function boundedPath(value, name) {
|
||||
if (typeof value !== "string" || !value.length || value.length > 4096 || value.indexOf("\u0000") !== -1) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + name);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function sameSnapshot(left, right) {
|
||||
return left.projectGuid === right.projectGuid &&
|
||||
left.projectItemId === right.projectItemId &&
|
||||
left.canChangeMediaPath === right.canChangeMediaPath &&
|
||||
left.isOffline === right.isOffline &&
|
||||
left.canProxy === right.canProxy &&
|
||||
left.hasProxy === right.hasProxy &&
|
||||
left.proxyPath === right.proxyPath;
|
||||
}
|
||||
function commandError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
return { createSourceProxyWorkflowDefinitions };
|
||||
});
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
body {
|
||||
color: #ddd;
|
||||
background: #242424;
|
||||
font: 12px system-ui;
|
||||
margin: 14px;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
margin: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
button {
|
||||
color: white;
|
||||
background: #5b5bd6;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
background: #181818;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.help {
|
||||
color: #bbb;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
input:focus-visible {
|
||||
outline: 2px solid #c7b8ff;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
button,
|
||||
input,
|
||||
pre {
|
||||
border: 1px solid CanvasText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpTickTimeArithmeticWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// Keep this utility intentionally separate from frame alignment. It accepts
|
||||
// already-quantized Premiere ticks and returns only TickTime's native value
|
||||
// readback; it does not create a FrameRate, accept seconds, or inspect a
|
||||
// sequence, track, clip, or project.
|
||||
function createTickTimeArithmeticWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const definitions = {
|
||||
"time.tickArithmetic.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTickArithmetic,
|
||||
handler: inspectTickArithmetic
|
||||
}
|
||||
};
|
||||
|
||||
function canUseTickArithmetic() {
|
||||
return !!(ppro && ppro.TickTime && typeof ppro.TickTime.createWithTicks === "function");
|
||||
}
|
||||
|
||||
async function inspectTickArithmetic(args) {
|
||||
assertOnlyKeys(args, ["operation", "baseTicks", "operandTicks", "factor"]);
|
||||
const operation = enumValue(args.operation, "operation", ["add", "subtract", "multiply", "divide"]);
|
||||
const baseTicks = canonicalTicks(args.baseTicks, "baseTicks");
|
||||
const base = createTickTime(baseTicks, "baseTicks");
|
||||
let operand = null, result;
|
||||
if (operation === "add" || operation === "subtract") {
|
||||
if (args.factor !== undefined) throw commandError("UXP_INVALID_ARGUMENT", "factor is only allowed for multiply or divide");
|
||||
operand = createTickTime(canonicalTicks(args.operandTicks, "operandTicks"), "operandTicks");
|
||||
const method = operation === "add" ? "add" : "subtract";
|
||||
if (typeof base.value[method] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose TickTime." + method);
|
||||
result = base.value[method](operand.value);
|
||||
} else {
|
||||
if (args.operandTicks !== undefined) throw commandError("UXP_INVALID_ARGUMENT", "operandTicks is only allowed for add or subtract");
|
||||
const factor = boundedFactor(args.factor, operation === "divide" ? "divisor" : "factor");
|
||||
const method = operation === "multiply" ? "multiply" : "divide";
|
||||
if (typeof base.value[method] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose TickTime." + method);
|
||||
result = base.value[method](factor);
|
||||
operand = { factor };
|
||||
}
|
||||
return {
|
||||
operation,
|
||||
base: readTickTime(base.value, "base"),
|
||||
...(operation === "add" || operation === "subtract" ? { operand: readTickTime(operand.value, "operand") } : { factor: operand.factor }),
|
||||
result: readTickTime(result, "result"),
|
||||
verificationBoundary: "native_tick_time_value_readback",
|
||||
limitations: ["This is pure native TickTime arithmetic over caller-supplied ticks; it does not align frames, infer timecode, inspect Premiere project state, or prove a licensed host."]
|
||||
};
|
||||
}
|
||||
|
||||
function createTickTime(ticks, name) {
|
||||
let value;
|
||||
try { value = ppro.TickTime.createWithTicks(ticks); } catch (_) { value = null; }
|
||||
if (!value) throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere rejected " + name + " while constructing TickTime");
|
||||
return { value };
|
||||
}
|
||||
|
||||
function readTickTime(value, name) {
|
||||
if (!value || typeof value !== "object") throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned no TickTime for " + name);
|
||||
const ticks = canonicalTicks(value.ticks, name + ".ticks");
|
||||
const seconds = Number(value.seconds);
|
||||
if (!Number.isFinite(seconds) || Math.abs(seconds) > 1_000_000_000) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned invalid " + name + ".seconds");
|
||||
}
|
||||
return { ticks, seconds };
|
||||
}
|
||||
|
||||
function canonicalTicks(value, name) {
|
||||
if (typeof value !== "string" || !/^(?:0|-?[1-9][0-9]{0,17})$/.test(value)) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a canonical signed tick integer with at most 18 digits");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function boundedFactor(value, name) {
|
||||
if (!Number.isInteger(value) || value < -1000000 || value > 1000000 || value === 0) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-zero integer from -1000000 through 1000000");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function enumValue(value, name, allowed) {
|
||||
if (!allowed.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", "));
|
||||
return value;
|
||||
}
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "arguments must be an object");
|
||||
const unknown = Object.keys(value).find(function (key) { return !allowed.includes(key); });
|
||||
if (unknown) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown);
|
||||
}
|
||||
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
return { createTickTimeArithmeticWorkflowDefinitions };
|
||||
});
|
||||
@@ -0,0 +1,306 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpTimelineSourceLabelWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// This is deliberately a source-project-item label action resolved from one
|
||||
// active timeline coordinate. It does not claim to label a timeline-only
|
||||
// instance: Premiere's documented color-label action belongs to the source
|
||||
// ClipProjectItem, so other uses of that source can reflect the change.
|
||||
function createTimelineSourceLabelWorkflowDefinitions(deps) {
|
||||
const ppro = deps.ppro;
|
||||
const fallbackTails = new Map();
|
||||
const locks = deps.colorLabelLocks && typeof deps.colorLabelLocks.withProjectItemColorLabelLock === "function"
|
||||
? deps.colorLabelLocks
|
||||
: { withProjectItemColorLabelLock: localLock };
|
||||
const definitions = {
|
||||
"timeline.sourceLabel.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTimelineSourceLabels,
|
||||
handler: inspectTimelineSourceLabel
|
||||
},
|
||||
"timeline.sourceLabel.update": {
|
||||
destructive: true,
|
||||
undoable: true,
|
||||
idempotent: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canUseTimelineSourceLabels,
|
||||
handler: updateTimelineSourceLabel
|
||||
}
|
||||
};
|
||||
|
||||
function canUseTimelineSourceLabels() {
|
||||
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.Constants && ppro.Constants.TrackItemType && ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function");
|
||||
}
|
||||
|
||||
async function inspectTimelineSourceLabel(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex"]);
|
||||
return sourceLabelSnapshot(await activeTarget(args, false));
|
||||
}
|
||||
|
||||
async function updateTimelineSourceLabel(args) {
|
||||
assertOnlyKeys(args, ["mediaType", "trackIndex", "clipIndex", "colorIndex", "expectedSnapshot", "confirmSetLabel", "operationId"]);
|
||||
requireConfirmation(args.confirmSetLabel);
|
||||
requireOperationId(args.operationId);
|
||||
const target = targetCoordinates(args), colorIndex = labelColorIndex(args.colorIndex);
|
||||
const expected = requiredSnapshot(args.expectedSnapshot);
|
||||
assertExpectedTarget(expected, target);
|
||||
const initial = await activeTarget(target, true);
|
||||
const initialSnapshot = await sourceLabelSnapshot(initial);
|
||||
if (initialSnapshot.projectGuid !== expected.projectGuid || initialSnapshot.sequenceId !== expected.sequenceId ||
|
||||
initialSnapshot.sourceProjectItemId !== expected.sourceProjectItemId) {
|
||||
throw commandError("UXP_STALE_SOURCE_LABEL", "The active project, sequence, or timeline source item no longer matches the reviewed label snapshot");
|
||||
}
|
||||
const key = initialSnapshot.projectGuid + "\u0000" + initialSnapshot.sourceProjectItemId;
|
||||
return locks.withProjectItemColorLabelLock(key, async function () {
|
||||
// Re-resolve after joining the source-global tail. This catches both a
|
||||
// competing coordinate update and the generic project-bin color tool,
|
||||
// which uses the same lock for the same ClipProjectItem.
|
||||
let context;
|
||||
try {
|
||||
context = await activeTarget(target, true);
|
||||
} catch (error) {
|
||||
if (error && error.code === "UXP_TARGET_NOT_FOUND") {
|
||||
throw commandError("UXP_STALE_SOURCE_LABEL", "The reviewed timeline coordinate no longer resolves before color-label action creation");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const before = await sourceLabelSnapshot(context);
|
||||
if (!sameSnapshot(before, expected)) {
|
||||
throw commandError("UXP_STALE_SOURCE_LABEL", "The reviewed timeline source label changed since inspection");
|
||||
}
|
||||
let committed = false;
|
||||
context.project.lockedAccess(function () {
|
||||
if (guidString(context.project.guid) !== before.projectGuid || guidString(context.sequence.guid) !== before.sequenceId) {
|
||||
throw commandError("UXP_STALE_SOURCE_LABEL", "The active project or sequence changed before color-label action creation");
|
||||
}
|
||||
if (typeof context.source.createSetColorLabelAction !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "This timeline source item cannot create a documented color-label action");
|
||||
}
|
||||
const action = context.source.createSetColorLabelAction(colorIndex);
|
||||
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create the source color-label action");
|
||||
committed = context.project.executeTransaction(function (compoundAction) {
|
||||
if (!compoundAction || typeof compoundAction.addAction !== "function" || compoundAction.addAction(action) === false) {
|
||||
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the source color-label action");
|
||||
}
|
||||
}, "Set timeline source label");
|
||||
});
|
||||
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the source color-label transaction");
|
||||
|
||||
const after = await sourceLabelSnapshot(await activeTarget(target, true));
|
||||
if (!sameTargetAfterUpdate(before, after, colorIndex)) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere did not retain the requested source color label at the reviewed timeline coordinate");
|
||||
}
|
||||
return {
|
||||
sourceLabelUpdated: true,
|
||||
before,
|
||||
after,
|
||||
outcome: "verified",
|
||||
verificationBoundary: "timeline_coordinate_source_color_label_readback",
|
||||
undoLabel: "Set timeline source label"
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function activeTarget(args, requireTransaction) {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere project APIs are unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw commandError("UXP_NO_ACTIVE_PROJECT", "No active project");
|
||||
if (requireTransaction && (typeof project.lockedAccess !== "function" || typeof project.executeTransaction !== "function")) {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "This Premiere build does not expose locked undoable transactions");
|
||||
}
|
||||
if (typeof project.getActiveSequence !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere cannot resolve the active sequence");
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) throw commandError("UXP_NO_ACTIVE_SEQUENCE", "No active sequence");
|
||||
const target = targetCoordinates(args), items = await clipItemsAt(sequence, target);
|
||||
const item = items[target.clipIndex];
|
||||
if (!item) throw commandError("UXP_TARGET_NOT_FOUND", "clipIndex is out of range");
|
||||
const sourceItem = await requiredMethod(item, "getProjectItem")();
|
||||
let source;
|
||||
try { source = ppro.ClipProjectItem.cast(sourceItem); } catch (_) { source = null; }
|
||||
if (!source) throw commandError("UXP_TARGET_UNSUPPORTED", "The resolved timeline item has no ClipProjectItem color-label surface");
|
||||
const sourceProjectItemId = requiredIdentifier(await requiredMethod(source, "getId")(), "source project-item ID");
|
||||
return {
|
||||
project,
|
||||
sequence,
|
||||
projectGuid: requiredGuid(project.guid, "active project GUID"),
|
||||
sequenceId: requiredGuid(sequence.guid, "active sequence GUID"),
|
||||
source,
|
||||
sourceProjectItemId,
|
||||
trackItem: item,
|
||||
trackItemCount: items.length,
|
||||
...target
|
||||
};
|
||||
}
|
||||
|
||||
async function clipItemsAt(sequence, target) {
|
||||
const title = target.mediaType === "video" ? "Video" : "Audio";
|
||||
const countMethod = "get" + title + "TrackCount", trackMethod = "get" + title + "Track";
|
||||
if (typeof sequence[countMethod] !== "function" || typeof sequence[trackMethod] !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", target.mediaType + " track APIs are unavailable");
|
||||
}
|
||||
const count = Number(await sequence[countMethod]());
|
||||
if (!Number.isSafeInteger(count) || count < 0) throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid " + target.mediaType + " track count");
|
||||
if (target.trackIndex >= count) throw commandError("UXP_TARGET_NOT_FOUND", target.mediaType + " trackIndex is out of range");
|
||||
const track = await sequence[trackMethod](target.trackIndex), itemType = ppro.Constants && ppro.Constants.TrackItemType;
|
||||
if (!track || !itemType || itemType.CLIP == null || typeof track.getTrackItems !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Clip track-item APIs are unavailable");
|
||||
}
|
||||
const items = Array.from(await track.getTrackItems(itemType.CLIP, false) || []);
|
||||
if (!items.length) throw commandError("UXP_TARGET_NOT_FOUND", "The target track has no clip items");
|
||||
if (items.length > 512) throw commandError("UXP_TARGET_TOO_LARGE", "The target track has more than 512 clip items");
|
||||
return items;
|
||||
}
|
||||
|
||||
async function sourceLabelSnapshot(context) {
|
||||
const colorLabelIndex = Number(await requiredMethod(context.source, "getColorLabelIndex")());
|
||||
if (!Number.isSafeInteger(colorLabelIndex) || colorLabelIndex < 0 || colorLabelIndex > 15) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an invalid source color-label index");
|
||||
}
|
||||
const startSeconds = tickSeconds(await requiredMethod(context.trackItem, "getStartTime")());
|
||||
const endSeconds = tickSeconds(await requiredMethod(context.trackItem, "getEndTime")());
|
||||
if (!validSeconds(startSeconds) || !validSeconds(endSeconds) || endSeconds < startSeconds) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned invalid timeline coordinates for the source-label target");
|
||||
}
|
||||
return {
|
||||
projectGuid: context.projectGuid,
|
||||
sequenceId: context.sequenceId,
|
||||
mediaType: context.mediaType,
|
||||
trackIndex: context.trackIndex,
|
||||
clipIndex: context.clipIndex,
|
||||
trackItemCount: context.trackItemCount,
|
||||
sourceProjectItemId: context.sourceProjectItemId,
|
||||
sourceColorLabelIndex: colorLabelIndex,
|
||||
startSeconds,
|
||||
endSeconds
|
||||
};
|
||||
}
|
||||
|
||||
function targetCoordinates(args) {
|
||||
return {
|
||||
mediaType: enumValue(args.mediaType, "mediaType", ["video", "audio"]),
|
||||
trackIndex: boundedInt(args.trackIndex, "trackIndex", 0, 511),
|
||||
clipIndex: boundedInt(args.clipIndex, "clipIndex", 0, 511)
|
||||
};
|
||||
}
|
||||
|
||||
function requiredSnapshot(value) {
|
||||
assertObject(value, "expectedSnapshot");
|
||||
const allowed = ["projectGuid", "sequenceId", "mediaType", "trackIndex", "clipIndex", "trackItemCount", "sourceProjectItemId", "sourceColorLabelIndex", "startSeconds", "endSeconds"];
|
||||
assertOnlyKeys(value, allowed);
|
||||
for (let index = 0; index < allowed.length; index += 1) {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, allowed[index])) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot." + allowed[index] + " is required");
|
||||
}
|
||||
}
|
||||
const snapshot = {
|
||||
projectGuid: requiredGuid(value.projectGuid, "expectedSnapshot.projectGuid"),
|
||||
sequenceId: requiredGuid(value.sequenceId, "expectedSnapshot.sequenceId"),
|
||||
mediaType: enumValue(value.mediaType, "expectedSnapshot.mediaType", ["video", "audio"]),
|
||||
trackIndex: boundedInt(value.trackIndex, "expectedSnapshot.trackIndex", 0, 511),
|
||||
clipIndex: boundedInt(value.clipIndex, "expectedSnapshot.clipIndex", 0, 511),
|
||||
trackItemCount: boundedInt(value.trackItemCount, "expectedSnapshot.trackItemCount", 1, 512),
|
||||
sourceProjectItemId: requiredIdentifier(value.sourceProjectItemId, "expectedSnapshot.sourceProjectItemId"),
|
||||
sourceColorLabelIndex: labelColorIndex(value.sourceColorLabelIndex),
|
||||
startSeconds: boundedSeconds(value.startSeconds, "expectedSnapshot.startSeconds"),
|
||||
endSeconds: boundedSeconds(value.endSeconds, "expectedSnapshot.endSeconds")
|
||||
};
|
||||
if (snapshot.endSeconds < snapshot.startSeconds) throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot.endSeconds must not precede startSeconds");
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
function assertExpectedTarget(expected, target) {
|
||||
if (expected.mediaType !== target.mediaType || expected.trackIndex !== target.trackIndex || expected.clipIndex !== target.clipIndex) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedSnapshot coordinates must exactly match the requested timeline target");
|
||||
}
|
||||
}
|
||||
|
||||
function sameSnapshot(left, right) {
|
||||
return left.projectGuid === right.projectGuid && left.sequenceId === right.sequenceId && left.mediaType === right.mediaType &&
|
||||
left.trackIndex === right.trackIndex && left.clipIndex === right.clipIndex && left.trackItemCount === right.trackItemCount &&
|
||||
left.sourceProjectItemId === right.sourceProjectItemId && left.sourceColorLabelIndex === right.sourceColorLabelIndex &&
|
||||
sameNumber(left.startSeconds, right.startSeconds) && sameNumber(left.endSeconds, right.endSeconds);
|
||||
}
|
||||
|
||||
function sameTargetAfterUpdate(before, after, colorIndex) {
|
||||
return before.projectGuid === after.projectGuid && before.sequenceId === after.sequenceId && before.mediaType === after.mediaType &&
|
||||
before.trackIndex === after.trackIndex && before.clipIndex === after.clipIndex && before.trackItemCount === after.trackItemCount &&
|
||||
before.sourceProjectItemId === after.sourceProjectItemId && after.sourceColorLabelIndex === colorIndex &&
|
||||
sameNumber(before.startSeconds, after.startSeconds) && sameNumber(before.endSeconds, after.endSeconds);
|
||||
}
|
||||
|
||||
function localLock(key, operation) {
|
||||
const previous = fallbackTails.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const gate = new Promise(function (resolve) { release = resolve; });
|
||||
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
|
||||
fallbackTails.set(key, tail);
|
||||
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
|
||||
release();
|
||||
if (fallbackTails.get(key) === tail) fallbackTails.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function assertObject(value, name) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", (name || "args") + " must be an object");
|
||||
}
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
const unknown = Object.keys(value).filter(function (key) { return !allowed.includes(key); });
|
||||
if (unknown.length) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown[0]);
|
||||
}
|
||||
function requiredMethod(value, method) {
|
||||
if (!value || typeof value[method] !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere does not expose " + method + " for this target");
|
||||
return value[method].bind(value);
|
||||
}
|
||||
function enumValue(value, name, allowed) {
|
||||
if (!allowed.includes(value)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be one of " + allowed.join(", "));
|
||||
return value;
|
||||
}
|
||||
function boundedInt(value, name, minimum, maximum) {
|
||||
if (!Number.isInteger(value) || value < minimum || value > maximum) throw commandError("UXP_INVALID_ARGUMENT", name + " must be an integer from " + minimum + " to " + maximum);
|
||||
return value;
|
||||
}
|
||||
function labelColorIndex(value) { return boundedInt(value, "colorIndex", 0, 15); }
|
||||
function boundedSeconds(value, name) {
|
||||
const seconds = Number(value);
|
||||
if (!validSeconds(seconds)) throw commandError("UXP_INVALID_ARGUMENT", name + " must be a number from 0 to 86400");
|
||||
return seconds;
|
||||
}
|
||||
function validSeconds(value) { return Number.isFinite(value) && value >= 0 && value <= 86400; }
|
||||
function sameNumber(left, right) { return Math.abs(Number(left) - Number(right)) <= 0.000001; }
|
||||
function requireConfirmation(value) {
|
||||
if (value !== true) throw commandError("UXP_CONFIRMATION_REQUIRED", "timeline.sourceLabel.update requires confirmSetLabel: true after reviewing the complete snapshot");
|
||||
}
|
||||
function requireOperationId(value) {
|
||||
if (typeof value !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(value)) throw commandError("UXP_INVALID_ARGUMENT", "operationId is required and must be 1-128 safe characters");
|
||||
}
|
||||
function requiredGuid(value, name) {
|
||||
const guid = guidString(value);
|
||||
if (!guid || guid.length > 128 || guid.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
|
||||
return guid;
|
||||
}
|
||||
function requiredIdentifier(value, name) {
|
||||
const id = guidString(value);
|
||||
if (!id || id.length > 512 || id.indexOf("\u0000") !== -1) throw commandError("UXP_VERIFICATION_FAILED", name + " is unavailable or invalid");
|
||||
return id;
|
||||
}
|
||||
function guidString(value) {
|
||||
if (value == null) return "";
|
||||
try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; }
|
||||
}
|
||||
function tickSeconds(value) { const seconds = value && Number(value.seconds); return Number.isFinite(seconds) ? seconds : null; }
|
||||
function commandError(code, message) { const error = new Error(message); error.code = code; return error; }
|
||||
|
||||
return { createTimelineSourceLabelWorkflowDefinitions };
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpTrackItemMutationLocks = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
// A slide modifies the target plus both immediate neighbours. Use the same
|
||||
// track-level tail for slips and slides so an operation on one of those
|
||||
// neighbours cannot bypass the reviewed snapshot while a slide is pending.
|
||||
function createTrackItemMutationLocks() {
|
||||
const tails = new Map();
|
||||
|
||||
function withTrackMutationLock(key, operation) {
|
||||
const previous = tails.get(key) || Promise.resolve();
|
||||
let release;
|
||||
const gate = new Promise(function (resolve) { release = resolve; });
|
||||
const tail = previous.catch(function () { return undefined; }).then(function () { return gate; });
|
||||
tails.set(key, tail);
|
||||
return previous.catch(function () { return undefined; }).then(operation).finally(function () {
|
||||
release();
|
||||
if (tails.get(key) === tail) tails.delete(key);
|
||||
});
|
||||
}
|
||||
|
||||
return { withTrackMutationLock };
|
||||
}
|
||||
|
||||
return { createTrackItemMutationLocks };
|
||||
});
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpTranscriptImport = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
const MAX_IMPORT_BYTES = 24 * 1024;
|
||||
const MAX_SNAPSHOT_BYTES = 1024 * 1024;
|
||||
const MAX_PROJECT_ITEMS = 512;
|
||||
const MAX_PROJECT_DEPTH = 16;
|
||||
|
||||
function createTranscriptImportRuntime(deps) {
|
||||
const ppro = deps.ppro, transcript = deps.TranscriptSupport, Protocol = deps.Protocol;
|
||||
const importTails = new Map();
|
||||
|
||||
function commandError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
function assertObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) throw commandError("UXP_INVALID_ARGUMENT", "args must be an object");
|
||||
}
|
||||
|
||||
function assertOnlyKeys(value, allowed) {
|
||||
const unknown = Object.keys(value).find(function (key) { return !allowed.includes(key); });
|
||||
if (unknown) throw commandError("UXP_INVALID_ARGUMENT", "Unknown argument: " + unknown);
|
||||
}
|
||||
|
||||
function requiredString(value, name, maximum) {
|
||||
if (typeof value !== "string" || !value.trim() || value.length > maximum) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", name + " must be a non-empty string of at most " + maximum + " characters");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function exactGuid(value, name) {
|
||||
const guid = requiredString(value, name, 512);
|
||||
if (guid === "[object Object]") throw commandError("UXP_INVALID_ARGUMENT", name + " must be a readable GUID string");
|
||||
return guid;
|
||||
}
|
||||
|
||||
function expectedRevision(value) {
|
||||
if (value === null) return null;
|
||||
if (typeof value !== "string" || !/^sha256:[a-f0-9]{64}$/.test(value)) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "expectedTranscriptRevision must be a sha256 revision or null");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function validateImportArgs(args) {
|
||||
assertObject(args);
|
||||
assertOnlyKeys(args, ["projectItemId", "expectedProjectGuid", "expectedTranscriptRevision", "json", "confirmDestructive", "operationId"]);
|
||||
if (args.confirmDestructive !== true) {
|
||||
throw commandError("UXP_CONFIRMATION_REQUIRED", "confirmDestructive: true is required to replace a source transcript");
|
||||
}
|
||||
if (typeof args.operationId !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(args.operationId)) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "operationId must be 1-128 safe characters");
|
||||
}
|
||||
const json = requiredString(args.json, "json", MAX_IMPORT_BYTES);
|
||||
if (transcript.utf8ByteLength(json) > MAX_IMPORT_BYTES) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", "json exceeds the 24 KiB transcript-import bridge limit");
|
||||
}
|
||||
try { transcript.parseTranscriptJSON(json); } catch (error) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", error && error.message ? error.message : "json is not valid transcript JSON");
|
||||
}
|
||||
return {
|
||||
projectItemId: requiredString(args.projectItemId, "projectItemId", 512),
|
||||
expectedProjectGuid: exactGuid(args.expectedProjectGuid, "expectedProjectGuid"),
|
||||
expectedTranscriptRevision: expectedRevision(args.expectedTranscriptRevision),
|
||||
json,
|
||||
};
|
||||
}
|
||||
|
||||
function guidString(value) {
|
||||
if (value == null) return "";
|
||||
try { return typeof value.toString === "function" ? String(value.toString()) : String(value); } catch (_) { return ""; }
|
||||
}
|
||||
|
||||
async function projectItemId(item) {
|
||||
if (!item || typeof item.getId !== "function") throw commandError("UXP_COMMAND_UNAVAILABLE", "Project-item identity access is unavailable");
|
||||
const value = await item.getId();
|
||||
if (value == null || String(value) === "") throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an unreadable project-item ID");
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function folderValue(item) {
|
||||
if (!item || typeof item.getItems !== "function") return null;
|
||||
if (!ppro.FolderItem || typeof ppro.FolderItem.cast !== "function") return item;
|
||||
try { return ppro.FolderItem.cast(item) || null; } catch (_) { return null; }
|
||||
}
|
||||
|
||||
function clipValue(item) {
|
||||
if (!ppro.ClipProjectItem || typeof ppro.ClipProjectItem.cast !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "ClipProjectItem casting is unavailable");
|
||||
}
|
||||
try { return ppro.ClipProjectItem.cast(item) || null; } catch (_) { return null; }
|
||||
}
|
||||
|
||||
async function resolveTarget(input) {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Active-project access is unavailable");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project || typeof project.getRootItem !== "function") throw commandError("UXP_NO_ACTIVE_PROJECT", "No readable active project");
|
||||
const projectGuid = guidString(project.guid);
|
||||
if (!projectGuid || projectGuid === "[object Object]") throw commandError("UXP_VERIFICATION_FAILED", "Premiere returned an unreadable active-project GUID");
|
||||
if (projectGuid !== input.expectedProjectGuid) throw commandError("UXP_STALE_TRANSCRIPT", "The active project no longer matches expectedProjectGuid");
|
||||
const root = await project.getRootItem();
|
||||
const queue = [{ item: root, depth: 0 }];
|
||||
let inspected = 0;
|
||||
while (queue.length) {
|
||||
const current = queue.shift(), folder = folderValue(current.item);
|
||||
if (!folder) continue;
|
||||
const children = Array.from(await folder.getItems() || []);
|
||||
for (let index = 0; index < children.length; index += 1) {
|
||||
inspected += 1;
|
||||
if (inspected > MAX_PROJECT_ITEMS) {
|
||||
throw commandError("UXP_PROJECT_TOO_LARGE", "Transcript target lookup exceeds " + MAX_PROJECT_ITEMS + " project items");
|
||||
}
|
||||
const child = children[index], childId = await projectItemId(child);
|
||||
if (childId === input.projectItemId) {
|
||||
const clip = clipValue(child);
|
||||
if (!clip) throw commandError("UXP_TARGET_NOT_FOUND", "The requested project item is not a media clip");
|
||||
return { project, projectGuid, clip, projectItemId: childId };
|
||||
}
|
||||
const childFolder = folderValue(child);
|
||||
if (childFolder) {
|
||||
if (current.depth >= MAX_PROJECT_DEPTH) {
|
||||
throw commandError("UXP_PROJECT_TOO_LARGE", "Transcript target lookup exceeds project-folder depth " + MAX_PROJECT_DEPTH);
|
||||
}
|
||||
queue.push({ item: childFolder, depth: current.depth + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
throw commandError("UXP_TARGET_NOT_FOUND", "projectItemId was not found in the active project");
|
||||
}
|
||||
|
||||
async function transcriptSnapshot(clip) {
|
||||
let hasTranscript = ppro.Transcript.hasTranscript(clip);
|
||||
if (hasTranscript && typeof hasTranscript.then === "function") hasTranscript = await hasTranscript;
|
||||
if (!hasTranscript) return { hasTranscript: false, transcriptRevision: null };
|
||||
const json = await ppro.Transcript.exportToJSON(clip);
|
||||
if (typeof json !== "string" || !json) throw commandError("UXP_VERIFICATION_FAILED", "Premiere reports a transcript but did not export readable JSON");
|
||||
if (transcript.utf8ByteLength(json) > MAX_SNAPSHOT_BYTES) {
|
||||
throw commandError("UXP_RESULT_TOO_LARGE", "Transcript readback exceeds the 1 MiB bounded snapshot limit");
|
||||
}
|
||||
try { transcript.parseTranscriptJSON(json); } catch (error) {
|
||||
throw commandError("UXP_VERIFICATION_FAILED", error && error.message ? error.message : "Premiere returned invalid transcript JSON");
|
||||
}
|
||||
return { hasTranscript: true, transcriptRevision: transcript.transcriptRevision(json) };
|
||||
}
|
||||
|
||||
function requireExpectedSnapshot(snapshot, expected) {
|
||||
if (!snapshot.hasTranscript && expected === null) return;
|
||||
if (snapshot.hasTranscript && snapshot.transcriptRevision === expected) return;
|
||||
throw commandError("UXP_STALE_TRANSCRIPT", "The source transcript no longer matches expectedTranscriptRevision");
|
||||
}
|
||||
|
||||
function serialize(key, work) {
|
||||
const previous = importTails.get(key) || Promise.resolve();
|
||||
const next = previous.catch(function () { return undefined; }).then(work);
|
||||
importTails.set(key, next);
|
||||
return next.finally(function () { if (importTails.get(key) === next) importTails.delete(key); });
|
||||
}
|
||||
|
||||
function operation(verified, boundary, evidence) {
|
||||
if (!Protocol || typeof Protocol.operationSemantics !== "function") return undefined;
|
||||
return Protocol.operationSemantics({
|
||||
mutatesProject: true,
|
||||
verificationStatus: verified ? "verified" : "committed_unverified",
|
||||
verificationBoundary: boundary,
|
||||
verificationEvidence: evidence,
|
||||
undoSupported: true,
|
||||
undoLabel: "Import transcript",
|
||||
transactionActionGroup: true,
|
||||
cancellationSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
async function importTranscript(args) {
|
||||
const input = validateImportArgs(args);
|
||||
return serialize(input.expectedProjectGuid + ":" + input.projectItemId, async function () {
|
||||
const beforeTarget = await resolveTarget(input), before = await transcriptSnapshot(beforeTarget.clip);
|
||||
requireExpectedSnapshot(before, input.expectedTranscriptRevision);
|
||||
|
||||
// Resolve and snapshot a second time immediately before action creation.
|
||||
// The per-owner queue prevents a different operation ID in this panel
|
||||
// from passing the same stale preflight concurrently.
|
||||
const actionTarget = await resolveTarget(input), actionSnapshot = await transcriptSnapshot(actionTarget.clip);
|
||||
requireExpectedSnapshot(actionSnapshot, input.expectedTranscriptRevision);
|
||||
let textSegments;
|
||||
try { textSegments = ppro.Transcript.importFromJSON(input.json); } catch (error) {
|
||||
throw commandError("UXP_INVALID_ARGUMENT", error && error.message ? error.message : "Premiere rejected transcript JSON");
|
||||
}
|
||||
if (!textSegments) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create transcript text segments");
|
||||
if (typeof actionTarget.project.lockedAccess !== "function" || typeof actionTarget.project.executeTransaction !== "function") {
|
||||
throw commandError("UXP_COMMAND_UNAVAILABLE", "Premiere transaction APIs are unavailable for transcript import");
|
||||
}
|
||||
let committed = false;
|
||||
actionTarget.project.lockedAccess(function () {
|
||||
const action = ppro.Transcript.createImportTextSegmentsAction(textSegments, actionTarget.clip);
|
||||
if (!action) throw commandError("UXP_ACTION_REJECTED", "Premiere did not create a transcript import action");
|
||||
committed = actionTarget.project.executeTransaction(function (compoundAction) {
|
||||
if (!compoundAction || typeof compoundAction.addAction !== "function" || compoundAction.addAction(action) === false) {
|
||||
throw commandError("UXP_ACTION_REJECTED", "Premiere rejected the transcript import action");
|
||||
}
|
||||
}, "Import transcript");
|
||||
});
|
||||
if (!committed) throw commandError("UXP_TRANSACTION_FAILED", "Premiere did not commit the transcript import transaction");
|
||||
|
||||
const requestedRevision = transcript.transcriptRevision(input.json);
|
||||
try {
|
||||
const afterTarget = await resolveTarget(input), after = await transcriptSnapshot(afterTarget.clip);
|
||||
const verified = after.hasTranscript && after.transcriptRevision === requestedRevision;
|
||||
return {
|
||||
committed: true,
|
||||
verified,
|
||||
projectGuid: actionTarget.projectGuid,
|
||||
projectItemId: actionTarget.projectItemId,
|
||||
before,
|
||||
requestedTranscriptRevision: requestedRevision,
|
||||
after,
|
||||
outcome: verified ? "verified" : "committed_unverified",
|
||||
verificationBoundary: verified ? "transcript_export_exact_readback" : "transcript_transaction_commit_with_readback_mismatch",
|
||||
undoLabel: "Import transcript",
|
||||
operation: operation(
|
||||
verified,
|
||||
verified ? "transcript_export_exact_readback" : "transcript_transaction_commit_with_readback_mismatch",
|
||||
verified
|
||||
? [{ type: "transcript_export_sha256", expected: requestedRevision, actual: after.transcriptRevision }]
|
||||
: [{ type: "transcript_export_sha256", expected: requestedRevision, actual: after.transcriptRevision }]
|
||||
)
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
committed: true,
|
||||
verified: false,
|
||||
projectGuid: actionTarget.projectGuid,
|
||||
projectItemId: actionTarget.projectItemId,
|
||||
before,
|
||||
requestedTranscriptRevision: requestedRevision,
|
||||
after: null,
|
||||
outcome: "committed_unverified",
|
||||
verificationBoundary: "transcript_transaction_commit_with_readback_unavailable",
|
||||
readbackError: error && error.message ? error.message : String(error),
|
||||
undoLabel: "Import transcript",
|
||||
operation: operation(false, "transcript_transaction_commit_with_readback_unavailable", [{
|
||||
type: "readback_error", message: error && error.message ? error.message : String(error)
|
||||
}])
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function canImportTranscript() {
|
||||
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" && ppro.Transcript
|
||||
&& typeof ppro.Transcript.hasTranscript === "function" && typeof ppro.Transcript.exportToJSON === "function"
|
||||
&& typeof ppro.Transcript.importFromJSON === "function" && typeof ppro.Transcript.createImportTextSegmentsAction === "function"
|
||||
&& ppro.ClipProjectItem && typeof ppro.ClipProjectItem.cast === "function");
|
||||
}
|
||||
|
||||
return { canImportTranscript, importTranscript, constants: { MAX_IMPORT_BYTES, MAX_SNAPSHOT_BYTES, MAX_PROJECT_ITEMS, MAX_PROJECT_DEPTH } };
|
||||
}
|
||||
|
||||
return { createTranscriptImportRuntime };
|
||||
});
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpTranscript = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
const MAX_TRANSCRIPT_JSON_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
function utf8ByteLength(value) {
|
||||
let bytes = 0;
|
||||
const text = String(value);
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
const code = text.charCodeAt(index);
|
||||
if (code < 0x80) bytes += 1;
|
||||
else if (code < 0x800) bytes += 2;
|
||||
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < text.length && text.charCodeAt(index + 1) >= 0xdc00 && text.charCodeAt(index + 1) <= 0xdfff) {
|
||||
bytes += 4;
|
||||
index += 1;
|
||||
} else bytes += 3;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// This stays local to the panel instead of assuming Web Crypto is exposed by
|
||||
// every supported Premiere UXP runtime. It uses the same UTF-8 replacement
|
||||
// behavior as Node's sha256 revision returned by get_clip_transcript_uxp.
|
||||
function utf8Bytes(value) {
|
||||
const text = String(value), bytes = [];
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
let code = text.charCodeAt(index);
|
||||
if (code >= 0xd800 && code <= 0xdbff) {
|
||||
const next = text.charCodeAt(index + 1);
|
||||
if (next >= 0xdc00 && next <= 0xdfff) {
|
||||
code = 0x10000 + ((code - 0xd800) << 10) + next - 0xdc00;
|
||||
index += 1;
|
||||
} else code = 0xfffd;
|
||||
} else if (code >= 0xdc00 && code <= 0xdfff) code = 0xfffd;
|
||||
if (code < 0x80) bytes.push(code);
|
||||
else if (code < 0x800) bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
||||
else if (code < 0x10000) bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
||||
else bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function rightRotate(value, bits) { return (value >>> bits) | (value << (32 - bits)); }
|
||||
|
||||
function sha256Hex(value) {
|
||||
const bytes = utf8Bytes(value), bitLength = bytes.length * 8;
|
||||
bytes.push(0x80);
|
||||
while ((bytes.length % 64) !== 56) bytes.push(0);
|
||||
const high = Math.floor(bitLength / 0x100000000), low = bitLength >>> 0;
|
||||
bytes.push((high >>> 24) & 0xff, (high >>> 16) & 0xff, (high >>> 8) & 0xff, high & 0xff);
|
||||
bytes.push((low >>> 24) & 0xff, (low >>> 16) & 0xff, (low >>> 8) & 0xff, low & 0xff);
|
||||
const words = [
|
||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
||||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
||||
];
|
||||
const constants = [
|
||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
||||
];
|
||||
for (let offset = 0; offset < bytes.length; offset += 64) {
|
||||
const schedule = new Array(64);
|
||||
for (let index = 0; index < 16; index += 1) {
|
||||
const position = offset + index * 4;
|
||||
schedule[index] = ((bytes[position] << 24) | (bytes[position + 1] << 16) | (bytes[position + 2] << 8) | bytes[position + 3]) >>> 0;
|
||||
}
|
||||
for (let index = 16; index < 64; index += 1) {
|
||||
const previous = schedule[index - 15], earlier = schedule[index - 2];
|
||||
const sigma0 = rightRotate(previous, 7) ^ rightRotate(previous, 18) ^ (previous >>> 3);
|
||||
const sigma1 = rightRotate(earlier, 17) ^ rightRotate(earlier, 19) ^ (earlier >>> 10);
|
||||
schedule[index] = (schedule[index - 16] + sigma0 + schedule[index - 7] + sigma1) >>> 0;
|
||||
}
|
||||
let a = words[0], b = words[1], c = words[2], d = words[3], e = words[4], f = words[5], g = words[6], h = words[7];
|
||||
for (let index = 0; index < 64; index += 1) {
|
||||
const sigma1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
|
||||
const choice = (e & f) ^ (~e & g);
|
||||
const first = (h + sigma1 + choice + constants[index] + schedule[index]) >>> 0;
|
||||
const sigma0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
|
||||
const majority = (a & b) ^ (a & c) ^ (b & c);
|
||||
const second = (sigma0 + majority) >>> 0;
|
||||
h = g; g = f; f = e; e = (d + first) >>> 0; d = c; c = b; b = a; a = (first + second) >>> 0;
|
||||
}
|
||||
words[0] = (words[0] + a) >>> 0; words[1] = (words[1] + b) >>> 0;
|
||||
words[2] = (words[2] + c) >>> 0; words[3] = (words[3] + d) >>> 0;
|
||||
words[4] = (words[4] + e) >>> 0; words[5] = (words[5] + f) >>> 0;
|
||||
words[6] = (words[6] + g) >>> 0; words[7] = (words[7] + h) >>> 0;
|
||||
}
|
||||
return words.map(function (word) { return (word >>> 0).toString(16).padStart(8, "0"); }).join("");
|
||||
}
|
||||
|
||||
function transcriptRevision(raw) { return "sha256:" + sha256Hex(raw); }
|
||||
|
||||
function parseTranscriptJSON(raw) {
|
||||
if (typeof raw !== "string" || raw.length === 0) throw new Error("transcript JSON must be a non-empty string");
|
||||
if (utf8ByteLength(raw) > MAX_TRANSCRIPT_JSON_BYTES) throw new Error("transcript JSON exceeds the 5 MB command limit");
|
||||
try { return JSON.parse(raw); } catch (error) { throw new Error("transcript JSON is invalid: " + error.message); }
|
||||
}
|
||||
|
||||
function searchTranscriptJSON(raw, query, options) {
|
||||
const document = parseTranscriptJSON(raw);
|
||||
const term = typeof query === "string" ? query.trim() : "";
|
||||
if (!term) throw new Error("query must be a non-empty string");
|
||||
const settings = options || {};
|
||||
const caseSensitive = settings.caseSensitive === true;
|
||||
const requestedLimit = Number(settings.maxResults == null ? 50 : settings.maxResults);
|
||||
if (!Number.isInteger(requestedLimit) || requestedLimit < 1 || requestedLimit > 500) {
|
||||
throw new Error("maxResults must be an integer between 1 and 500");
|
||||
}
|
||||
const needle = caseSensitive ? term : term.toLocaleLowerCase();
|
||||
const matches = [];
|
||||
const collectionLimit = requestedLimit + 1;
|
||||
function visit(value, path) {
|
||||
if (matches.length >= collectionLimit) return;
|
||||
if (typeof value === "string") {
|
||||
const haystack = caseSensitive ? value : value.toLocaleLowerCase();
|
||||
let from = 0;
|
||||
while (matches.length < collectionLimit) {
|
||||
const index = haystack.indexOf(needle, from);
|
||||
if (index < 0) break;
|
||||
const contextStart = Math.max(0, index - 80);
|
||||
const contextEnd = Math.min(value.length, index + term.length + 80);
|
||||
matches.push({ path, index, text: value, context: value.slice(contextStart, contextEnd) });
|
||||
from = index + Math.max(needle.length, 1);
|
||||
}
|
||||
} else if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length && matches.length < collectionLimit; i += 1) visit(value[i], path + "[" + i + "]");
|
||||
} else if (value && typeof value === "object") {
|
||||
const keys = Object.keys(value);
|
||||
for (let i = 0; i < keys.length && matches.length < collectionLimit; i += 1) {
|
||||
const key = keys[i];
|
||||
visit(value[key], path ? path + "." + key : key);
|
||||
}
|
||||
}
|
||||
}
|
||||
visit(document, "$");
|
||||
return { query: term, caseSensitive, matches: matches.slice(0, requestedLimit), limited: matches.length > requestedLimit };
|
||||
}
|
||||
|
||||
function versionAtLeast(version, minimum) {
|
||||
const current = String(version || "").split(".").map(Number);
|
||||
const required = String(minimum).split(".").map(Number);
|
||||
for (let i = 0; i < Math.max(current.length, required.length); i += 1) {
|
||||
const left = Number.isFinite(current[i]) ? current[i] : 0;
|
||||
const right = Number.isFinite(required[i]) ? required[i] : 0;
|
||||
if (left !== right) return left > right;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchingClipCandidate(item, itemId, wantedId, wantedName, cast) {
|
||||
const idMatch = !!wantedId && itemId === wantedId;
|
||||
const nameMatch = !wantedId && !!wantedName && item && item.name === wantedName;
|
||||
if (!idMatch && !nameMatch) return { matched: false, clip: null };
|
||||
try {
|
||||
return { matched: true, clip: cast(item) };
|
||||
} catch (error) {
|
||||
if (idMatch) throw error;
|
||||
return { matched: true, clip: null };
|
||||
}
|
||||
}
|
||||
|
||||
async function probeTranscriptExport(exportTranscript) {
|
||||
const json = await exportTranscript();
|
||||
return typeof json === "string" && json.length > 0;
|
||||
}
|
||||
|
||||
return {
|
||||
MAX_TRANSCRIPT_JSON_BYTES,
|
||||
utf8ByteLength,
|
||||
parseTranscriptJSON,
|
||||
searchTranscriptJSON,
|
||||
transcriptRevision,
|
||||
versionAtLeast,
|
||||
matchingClipCandidate,
|
||||
probeTranscriptExport
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,201 @@
|
||||
(function attachUniqueIdentityWorkflows(root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = api;
|
||||
root.PremiereMcpUniqueIdentityWorkflows = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function createUniqueIdentityWorkflows() {
|
||||
"use strict";
|
||||
|
||||
const MAX_PROJECT_ITEMS = 512;
|
||||
const MAX_TOKEN_LENGTH = 512;
|
||||
|
||||
function createUniqueIdentityWorkflowDefinitions(deps) {
|
||||
const ppro = deps && deps.ppro;
|
||||
if (!ppro) throw new Error("createUniqueIdentityWorkflowDefinitions requires ppro");
|
||||
const definitions = {
|
||||
"object.uniqueIdentity.inspect": {
|
||||
readOnly: true,
|
||||
targetCapabilityProbe: true,
|
||||
minHostVersion: "25.6.0",
|
||||
probe: canInspectUniqueIdentity,
|
||||
handler: inspectUniqueIdentity
|
||||
}
|
||||
};
|
||||
|
||||
function canInspectUniqueIdentity() {
|
||||
return !!(ppro.Project && typeof ppro.Project.getActiveProject === "function" &&
|
||||
ppro.UniqueSerializeable && typeof ppro.UniqueSerializeable.cast === "function");
|
||||
}
|
||||
|
||||
async function inspectUniqueIdentity(args) {
|
||||
const input = normalizeInput(args);
|
||||
const firstSnapshot = await readSnapshot(ppro, input);
|
||||
if (input.expectedProjectGuid && firstSnapshot.projectGuid !== input.expectedProjectGuid) {
|
||||
throw createError("UXP_STALE_UNIQUE_IDENTITY", "The active project changed before unique identity inspection.");
|
||||
}
|
||||
if (input.expectedUniqueId && firstSnapshot.target.uniqueId !== input.expectedUniqueId) {
|
||||
throw createError("UXP_STALE_UNIQUE_IDENTITY", "The requested target no longer has the expected unique identity.");
|
||||
}
|
||||
const secondSnapshot = await readSnapshot(ppro, input);
|
||||
if (!snapshotsMatch(firstSnapshot, secondSnapshot)) {
|
||||
throw createError("UXP_STALE_UNIQUE_IDENTITY", "The requested target changed during unique identity inspection.");
|
||||
}
|
||||
return Object.assign({}, secondSnapshot, {
|
||||
verificationBoundary: "bounded_unique_serializable_double_readback"
|
||||
});
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function normalizeInput(args) {
|
||||
const value = args || {};
|
||||
if (!isPlainObject(value)) throw createError("UXP_INVALID_ARGUMENT", "Expected an object argument.");
|
||||
const allowedKeys = ["projectItemId", "sequenceGuid", "expectedProjectGuid", "expectedUniqueId"];
|
||||
Object.keys(value).forEach(function rejectUnknownKey(key) {
|
||||
if (allowedKeys.indexOf(key) === -1) throw createError("UXP_INVALID_ARGUMENT", "Unsupported argument: " + key + ".");
|
||||
});
|
||||
const projectItemId = optionalToken(value.projectItemId, "projectItemId");
|
||||
const sequenceGuid = optionalToken(value.sequenceGuid, "sequenceGuid");
|
||||
if ((projectItemId ? 1 : 0) + (sequenceGuid ? 1 : 0) !== 1) {
|
||||
throw createError("UXP_INVALID_ARGUMENT", "Provide exactly one of projectItemId or sequenceGuid.");
|
||||
}
|
||||
return {
|
||||
projectItemId: projectItemId,
|
||||
sequenceGuid: sequenceGuid,
|
||||
expectedProjectGuid: optionalToken(value.expectedProjectGuid, "expectedProjectGuid"),
|
||||
expectedUniqueId: optionalToken(value.expectedUniqueId, "expectedUniqueId")
|
||||
};
|
||||
}
|
||||
|
||||
async function readSnapshot(ppro, input) {
|
||||
const project = await activeProject(ppro);
|
||||
const projectGuid = await requiredGuid(project.guid, "active project GUID");
|
||||
const target = input.sequenceGuid
|
||||
? await sequenceTarget(ppro, project, input.sequenceGuid)
|
||||
: await projectItemTarget(ppro, project, input.projectItemId);
|
||||
return { projectGuid: projectGuid, target: target };
|
||||
}
|
||||
|
||||
async function activeProject(ppro) {
|
||||
if (!ppro.Project || typeof ppro.Project.getActiveProject !== "function") {
|
||||
throw createError("UXP_COMMAND_UNAVAILABLE", "Project.getActiveProject is unavailable.");
|
||||
}
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) throw createError("UXP_NO_ACTIVE_PROJECT", "No active project is available.");
|
||||
return project;
|
||||
}
|
||||
|
||||
async function sequenceTarget(ppro, project, requestedGuid) {
|
||||
if (!ppro.Guid || typeof ppro.Guid.fromString !== "function" || typeof project.getSequence !== "function") {
|
||||
throw createError("UXP_COMMAND_UNAVAILABLE", "Sequence lookup is unavailable.");
|
||||
}
|
||||
let sequence;
|
||||
try {
|
||||
sequence = await project.getSequence(ppro.Guid.fromString(requestedGuid));
|
||||
} catch (error) {
|
||||
throw createError("UXP_TARGET_NOT_FOUND", "The requested sequence could not be resolved.");
|
||||
}
|
||||
if (!sequence) throw createError("UXP_TARGET_NOT_FOUND", "The requested sequence could not be resolved.");
|
||||
const sequenceGuid = await requiredGuid(sequence.guid, "resolved sequence GUID");
|
||||
if (sequenceGuid !== requestedGuid) {
|
||||
throw createError("UXP_TARGET_NOT_FOUND", "The requested sequence could not be resolved.");
|
||||
}
|
||||
return {
|
||||
kind: "sequence",
|
||||
sequenceGuid: sequenceGuid,
|
||||
uniqueId: await uniqueIdFor(ppro, sequence)
|
||||
};
|
||||
}
|
||||
|
||||
async function projectItemTarget(ppro, project, requestedProjectItemId) {
|
||||
if (typeof project.getRootItem !== "function") {
|
||||
throw createError("UXP_COMMAND_UNAVAILABLE", "Project.getRootItem is unavailable.");
|
||||
}
|
||||
const rootItem = await project.getRootItem();
|
||||
if (!rootItem) throw createError("UXP_TARGET_NOT_FOUND", "The active project has no root item.");
|
||||
const item = await findProjectItem(rootItem, requestedProjectItemId);
|
||||
if (!item) throw createError("UXP_TARGET_NOT_FOUND", "The requested project item could not be resolved.");
|
||||
return {
|
||||
kind: "project_item",
|
||||
projectItemId: requestedProjectItemId,
|
||||
uniqueId: await uniqueIdFor(ppro, item)
|
||||
};
|
||||
}
|
||||
|
||||
async function findProjectItem(rootItem, requestedProjectItemId) {
|
||||
const queue = [rootItem];
|
||||
let visited = 0;
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift();
|
||||
visited += 1;
|
||||
if (visited > MAX_PROJECT_ITEMS) {
|
||||
throw createError("UXP_PROJECT_TOO_LARGE", "Project item lookup exceeded the " + MAX_PROJECT_ITEMS + " item limit.");
|
||||
}
|
||||
if (!current || typeof current.getId !== "function") {
|
||||
throw createError("UXP_COMMAND_UNAVAILABLE", "Project item ID lookup is unavailable.");
|
||||
}
|
||||
if (requiredToken(await current.getId(), "project item ID") === requestedProjectItemId) return current;
|
||||
if (typeof current.getItems === "function") {
|
||||
const children = await current.getItems();
|
||||
if (!Array.isArray(children)) {
|
||||
throw createError("UXP_VERIFICATION_FAILED", "Project item children were not returned as an array.");
|
||||
}
|
||||
Array.prototype.push.apply(queue, children);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function uniqueIdFor(ppro, target) {
|
||||
if (!ppro.UniqueSerializeable || typeof ppro.UniqueSerializeable.cast !== "function") {
|
||||
throw createError("UXP_COMMAND_UNAVAILABLE", "UniqueSerializeable.cast is unavailable.");
|
||||
}
|
||||
let serializable;
|
||||
try {
|
||||
serializable = ppro.UniqueSerializeable.cast(target);
|
||||
} catch (error) {
|
||||
throw createError("UXP_COMMAND_UNAVAILABLE", "The requested target cannot be serialized uniquely.");
|
||||
}
|
||||
if (!serializable || typeof serializable.getUniqueID !== "function") {
|
||||
throw createError("UXP_COMMAND_UNAVAILABLE", "UniqueSerializeable.getUniqueID is unavailable.");
|
||||
}
|
||||
return requiredGuid(await serializable.getUniqueID(), "unique identity");
|
||||
}
|
||||
|
||||
async function requiredGuid(value, label) {
|
||||
if (!value || typeof value.toString !== "function") {
|
||||
throw createError("UXP_VERIFICATION_FAILED", "Expected a " + label + ".");
|
||||
}
|
||||
return requiredToken(await value.toString(), label);
|
||||
}
|
||||
|
||||
function snapshotsMatch(first, second) {
|
||||
if (first.projectGuid !== second.projectGuid || first.target.kind !== second.target.kind || first.target.uniqueId !== second.target.uniqueId) return false;
|
||||
return first.target.kind === "sequence"
|
||||
? first.target.sequenceGuid === second.target.sequenceGuid
|
||||
: first.target.projectItemId === second.target.projectItemId;
|
||||
}
|
||||
|
||||
function optionalToken(value, label) {
|
||||
return value === undefined || value === null ? undefined : requiredToken(value, label);
|
||||
}
|
||||
|
||||
function requiredToken(value, label) {
|
||||
if (typeof value !== "string" || value.length === 0 || value.length > MAX_TOKEN_LENGTH) {
|
||||
throw createError("UXP_INVALID_ARGUMENT", "Expected " + label + " to be a non-empty string up to " + MAX_TOKEN_LENGTH + " characters.");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function createError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
return { createUniqueIdentityWorkflowDefinitions: createUniqueIdentityWorkflowDefinitions };
|
||||
});
|
||||
+1669
File diff suppressed because it is too large
Load Diff
+220
@@ -0,0 +1,220 @@
|
||||
(function (root, factory) {
|
||||
const api = factory();
|
||||
if (typeof module === "object" && module.exports) module.exports = api;
|
||||
root.PremiereMcpWorkspace = api;
|
||||
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
|
||||
"use strict";
|
||||
|
||||
const CONFIG_FILE = "workspace-access.json";
|
||||
|
||||
function workspaceError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
function parseAbsolutePath(value, label) {
|
||||
if (typeof value !== "string" || !value.trim() || value.length > 4096 || value.indexOf("\0") !== -1) {
|
||||
throw workspaceError("UXP_INVALID_ARGUMENT", label + " must be a non-empty absolute path of at most 4096 characters");
|
||||
}
|
||||
// Trimming is only valid for the blank-value check above. Leading and
|
||||
// trailing spaces are legal POSIX filename characters and must survive
|
||||
// normalization unchanged.
|
||||
const original = value;
|
||||
const windowsInput = /^[A-Za-z]:[\\/]/.test(original) || /^\\\\/.test(original);
|
||||
const slashed = windowsInput ? original.replace(/\\/g, "/") : original;
|
||||
const drive = /^([A-Za-z]):\/(.*)$/.exec(slashed);
|
||||
const unc = windowsInput && /^\/\/([^/]+)\/([^/]+)(?:\/(.*))?$/.exec(slashed);
|
||||
const posix = !drive && !unc && slashed.charAt(0) === "/";
|
||||
if (!drive && !unc && !posix) {
|
||||
throw workspaceError("UXP_INVALID_ARGUMENT", label + " must be absolute");
|
||||
}
|
||||
const prefix = drive ? drive[1].toUpperCase() + ":" : unc ? "//" + unc[1] + "/" + unc[2] : "";
|
||||
const remainder = drive ? drive[2] : unc ? (unc[3] || "") : slashed.slice(1);
|
||||
const parts = [];
|
||||
for (const part of remainder.split("/")) {
|
||||
if (!part || part === ".") continue;
|
||||
if (part === "..") {
|
||||
if (!parts.length) throw workspaceError("UXP_PATH_OUTSIDE_WORKSPACE", label + " escapes its filesystem root");
|
||||
parts.pop();
|
||||
continue;
|
||||
}
|
||||
if ((drive || unc) && (/[. ]$/.test(part) || part.indexOf(":") !== -1 || /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(part))) {
|
||||
throw workspaceError("UXP_INVALID_ARGUMENT", label + " contains a Windows-ambiguous path segment");
|
||||
}
|
||||
parts.push(part);
|
||||
}
|
||||
const normalized = prefix + "/" + parts.join("/");
|
||||
return {
|
||||
normalized: normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized,
|
||||
comparison: (drive || unc ? normalized.toLowerCase() : normalized),
|
||||
kind: drive ? "windows" : unc ? "unc" : "posix",
|
||||
depth: parts.length
|
||||
};
|
||||
}
|
||||
|
||||
function isContained(rootPath, candidatePath, allowRoot) {
|
||||
const root = parseAbsolutePath(rootPath, "workspace root");
|
||||
const candidate = parseAbsolutePath(candidatePath, "path");
|
||||
if (root.kind !== candidate.kind) return false;
|
||||
if (candidate.comparison === root.comparison) return !!allowRoot;
|
||||
return candidate.comparison.indexOf(root.comparison + "/") === 0;
|
||||
}
|
||||
|
||||
function validateLoopbackBridgeUrl(value) {
|
||||
let url;
|
||||
try { url = new URL(value); } catch (_) {
|
||||
throw workspaceError("UXP_INVALID_BRIDGE_URL", "Bridge URL is invalid");
|
||||
}
|
||||
if (url.protocol !== "ws:" || (url.hostname !== "127.0.0.1" && url.hostname !== "localhost") ||
|
||||
url.pathname !== "/uxp" || url.username || url.password || url.hash) {
|
||||
throw workspaceError("UXP_INVALID_BRIDGE_URL", "Bridge URL must be ws://127.0.0.1:<port>/uxp or ws://localhost:<port>/uxp");
|
||||
}
|
||||
url.search = "";
|
||||
return url;
|
||||
}
|
||||
|
||||
function createWorkspaceBroker(deps) {
|
||||
const fs = deps && deps.fs;
|
||||
const resolveCanonicalPath = deps && deps.resolveCanonicalPath;
|
||||
let rootEntry = null;
|
||||
let persistentToken = null;
|
||||
let initialized = false;
|
||||
|
||||
function nativePathFor(entry) {
|
||||
if (entry && typeof entry.nativePath === "string" && entry.nativePath) return entry.nativePath;
|
||||
if (entry && fs && typeof fs.getNativePath === "function") return fs.getNativePath(entry);
|
||||
return "";
|
||||
}
|
||||
|
||||
async function dataFolder() {
|
||||
if (!fs || typeof fs.getDataFolder !== "function") {
|
||||
throw workspaceError("UXP_WORKSPACE_UNAVAILABLE", "UXP persistent storage is unavailable");
|
||||
}
|
||||
return fs.getDataFolder();
|
||||
}
|
||||
|
||||
async function readConfiguration() {
|
||||
try {
|
||||
const folder = await dataFolder();
|
||||
const file = await folder.getEntry(CONFIG_FILE);
|
||||
const parsed = JSON.parse(await file.read());
|
||||
if (!parsed || parsed.schemaVersion !== 1 || typeof parsed.persistentToken !== "string") return null;
|
||||
return parsed;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeConfiguration(token) {
|
||||
const folder = await dataFolder();
|
||||
const file = await folder.createFile(CONFIG_FILE, { overwrite: true });
|
||||
await file.write(JSON.stringify({ schemaVersion: 1, persistentToken: token }));
|
||||
}
|
||||
|
||||
async function deleteConfiguration() {
|
||||
try {
|
||||
const folder = await dataFolder();
|
||||
const file = await folder.getEntry(CONFIG_FILE);
|
||||
if (file && typeof file.delete === "function") await file.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
if (initialized) return status();
|
||||
initialized = true;
|
||||
const stored = await readConfiguration();
|
||||
if (!stored || !fs || typeof fs.getEntryForPersistentToken !== "function") return status();
|
||||
try {
|
||||
const entry = await fs.getEntryForPersistentToken(stored.persistentToken);
|
||||
if (entry && entry.isFolder && nativePathFor(entry)) {
|
||||
rootEntry = entry;
|
||||
persistentToken = stored.persistentToken;
|
||||
}
|
||||
} catch (_) {
|
||||
await deleteConfiguration();
|
||||
}
|
||||
return status();
|
||||
}
|
||||
|
||||
async function requestRoot() {
|
||||
if (!fs || typeof fs.getFolder !== "function" || typeof fs.createPersistentToken !== "function") {
|
||||
throw workspaceError("UXP_WORKSPACE_UNAVAILABLE", "This UXP runtime cannot request persistent folder access");
|
||||
}
|
||||
const entry = await fs.getFolder();
|
||||
const nativePath = nativePathFor(entry);
|
||||
if (!entry || !entry.isFolder || !nativePath) {
|
||||
throw workspaceError("UXP_WORKSPACE_NOT_SELECTED", "No workspace folder was selected");
|
||||
}
|
||||
if (parseAbsolutePath(nativePath, "workspace root").depth < 1) {
|
||||
throw workspaceError("UXP_WORKSPACE_TOO_BROAD", "Choose a project subfolder instead of a filesystem or share root");
|
||||
}
|
||||
const token = await fs.createPersistentToken(entry);
|
||||
if (typeof token !== "string" || !token) {
|
||||
throw workspaceError("UXP_WORKSPACE_UNAVAILABLE", "Premiere did not return a persistent workspace token");
|
||||
}
|
||||
await writeConfiguration(token);
|
||||
rootEntry = entry;
|
||||
persistentToken = token;
|
||||
initialized = true;
|
||||
return status();
|
||||
}
|
||||
|
||||
async function revoke() {
|
||||
rootEntry = null;
|
||||
persistentToken = null;
|
||||
initialized = true;
|
||||
await deleteConfiguration();
|
||||
return status();
|
||||
}
|
||||
|
||||
function status() {
|
||||
return {
|
||||
configured: !!rootEntry,
|
||||
accessMode: "request",
|
||||
rootName: rootEntry && typeof rootEntry.name === "string" ? rootEntry.name : null,
|
||||
persistent: !!persistentToken,
|
||||
pathDisclosure: "redacted",
|
||||
canonicalPathValidation: typeof resolveCanonicalPath === "function" ? "available" : "unavailable"
|
||||
};
|
||||
}
|
||||
|
||||
async function assertPathAllowed(value, options) {
|
||||
const label = options && options.label || "path";
|
||||
const kind = options && options.kind || "file";
|
||||
const rootPath = nativePathFor(rootEntry);
|
||||
if (!rootEntry || !rootPath) {
|
||||
throw workspaceError("UXP_WORKSPACE_REQUIRED", "Choose an approved workspace folder in the MCP for Adobe Premiere Pro panel before using " + label);
|
||||
}
|
||||
const candidate = parseAbsolutePath(value, label);
|
||||
if (!isContained(rootPath, candidate.normalized, kind === "directory")) {
|
||||
throw workspaceError("UXP_PATH_OUTSIDE_WORKSPACE", label + " must stay inside the approved workspace folder");
|
||||
}
|
||||
// A lexical prefix check cannot detect symlinks, Windows junctions, or
|
||||
// other reparse points. Adobe's request-scoped UXP filesystem API does
|
||||
// not expose a documented realpath/link-inspection primitive, so raw
|
||||
// native paths must fail closed unless the embedding host supplies one.
|
||||
if (typeof resolveCanonicalPath !== "function") {
|
||||
throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host cannot prove that " + label + " stays inside the approved workspace after resolving filesystem links");
|
||||
}
|
||||
let canonicalRootValue, canonicalCandidateValue;
|
||||
try {
|
||||
canonicalRootValue = await resolveCanonicalPath(rootPath, { label: "workspace root", kind: "directory" });
|
||||
canonicalCandidateValue = await resolveCanonicalPath(candidate.normalized, { label, kind });
|
||||
} catch (error) {
|
||||
if (error && /^UXP_[A-Z0-9_]+$/.test(error.code || "")) throw error;
|
||||
throw workspaceError("UXP_CANONICAL_PATH_UNAVAILABLE", "This UXP host could not resolve " + label + " to a canonical filesystem path");
|
||||
}
|
||||
const canonicalRoot = parseAbsolutePath(canonicalRootValue, "workspace root");
|
||||
const canonicalCandidate = parseAbsolutePath(canonicalCandidateValue, label);
|
||||
if (!isContained(canonicalRoot.normalized, canonicalCandidate.normalized, kind === "directory")) {
|
||||
throw workspaceError("UXP_PATH_OUTSIDE_WORKSPACE", label + " resolves outside the approved workspace folder");
|
||||
}
|
||||
return canonicalCandidate.normalized;
|
||||
}
|
||||
|
||||
return { initialize, requestRoot, revoke, status, assertPathAllowed };
|
||||
}
|
||||
|
||||
return { createWorkspaceBroker, parseAbsolutePath, isContained, validateLoopbackBridgeUrl, workspaceError };
|
||||
});
|
||||
Reference in New Issue
Block a user