Files
jhonny-editor/code/src/tools/spot-workflows.ts
T
João Henrique b541f502ba feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
2026-09-08 09:59:31 -04:00

602 lines
31 KiB
TypeScript
Executable File

import { createHash } from "node:crypto";
import { existsSync } from "node:fs";
import path from "node:path";
import { BridgeOptions, sendCommand } from "../bridge/file-bridge.js";
import { buildToolScript, escapeForExtendScript } from "../bridge/script-builder.js";
import {
AuditSink,
CapabilityConfig,
createOperationId,
emitAudit,
requireCapability,
resolveCapabilities,
stderrAuditSink,
} from "../security/index.js";
type WorkflowKind = "motion_graphics_demo" | "product_spot" | "brand_spot";
type MotionStyle = "none" | "push_in" | "pull_out" | "alternate" | "demo";
interface TransitionSpec {
name: string;
duration_seconds: number;
}
interface MogrtSpec {
path: string;
approved_workspace_path: string;
title_track_index: number;
title_start_seconds: number;
}
export interface SpotWorkflowPlan {
schema_version: 1;
workflow: WorkflowKind;
sequence_id: string;
asset_item_ids: string[];
clip_duration_seconds: number;
video_track_index: number;
audio_track_index: number;
motion_style: MotionStyle;
transition?: TransitionSpec;
mogrt?: MogrtSpec;
}
export interface SpotWorkflowDependencies {
capabilities?: CapabilityConfig;
auditSink?: AuditSink;
operationIdFactory?: () => string;
fileExists?: (value: string) => boolean;
}
const MAX_ASSETS = 12;
const MAX_TEXT = 512;
const MAX_PATH = 4096;
function text(value: unknown, field: string, maxLength = MAX_TEXT): string {
if (typeof value !== "string" || !value.trim()) throw new Error(`${field} is required`);
const normalized = value.trim();
if (normalized.length > maxLength) throw new Error(`${field} must be at most ${maxLength} characters`);
return normalized;
}
function optionalText(value: unknown, field: string, maxLength = MAX_TEXT): string | undefined {
return value === undefined ? undefined : text(value, field, maxLength);
}
function positiveNumber(value: unknown, field: string, fallback: number, maximum: number): number {
if (value === undefined) return fallback;
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0 || value > maximum) {
throw new Error(`${field} must be a finite number greater than 0 and at most ${maximum}`);
}
return value;
}
function nonNegativeNumber(value: unknown, field: string, fallback: number, maximum: number): number {
if (value === undefined) return fallback;
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > maximum) {
throw new Error(`${field} must be a finite number between 0 and ${maximum}`);
}
return value;
}
function trackIndex(value: unknown, field: string, fallback: number): number {
if (value === undefined) return fallback;
if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > 32) {
throw new Error(`${field} must be an integer between 0 and 32`);
}
return value;
}
function assetItemIds(value: unknown): string[] {
if (!Array.isArray(value) || value.length === 0 || value.length > MAX_ASSETS) {
throw new Error(`asset_item_ids must contain between 1 and ${MAX_ASSETS} project item IDs`);
}
return value.map((item, index) => text(item, `asset_item_ids[${index}]`, 256));
}
function motionStyle(value: unknown, fallback: MotionStyle): MotionStyle {
if (value === undefined) return fallback;
if (value === "none" || value === "push_in" || value === "pull_out" || value === "alternate") return value;
throw new Error("motion_style must be one of none, push_in, pull_out, or alternate");
}
function transition(value: unknown, duration: unknown, fallbackName: string, fallbackDuration: number): TransitionSpec | undefined {
const name = optionalText(value, "transition_name", 128) ?? fallbackName;
if (name.toLowerCase() === "none") return undefined;
return {
name,
duration_seconds: positiveNumber(duration, "transition_duration_seconds", fallbackDuration, 10),
};
}
function isAbsolutePortable(value: string): boolean {
return path.posix.isAbsolute(value) || path.win32.isAbsolute(value);
}
function isWithinApprovedWorkspace(approvedWorkspacePath: string, candidatePath: string): boolean {
// Plans can be created on Windows and later inspected on macOS (or vice versa),
// so do not let the runner's host OS reinterpret a Windows absolute path as a
// relative POSIX path. Mixed path families are not contained by definition.
const windowsPaths = path.win32.isAbsolute(approvedWorkspacePath) && path.win32.isAbsolute(candidatePath);
const posixPaths = path.posix.isAbsolute(approvedWorkspacePath) && path.posix.isAbsolute(candidatePath);
if (!windowsPaths && !posixPaths) return false;
const pathApi = windowsPaths ? path.win32 : path.posix;
const root = pathApi.resolve(approvedWorkspacePath);
const candidate = pathApi.resolve(candidatePath);
const relative = pathApi.relative(root, candidate);
return relative !== "" && relative !== ".." && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative);
}
function mogrt(value: Record<string, unknown>, videoTrackIndex: number): MogrtSpec | undefined {
const mogrtPath = optionalText(value.mogrt_path, "mogrt_path", MAX_PATH);
if (!mogrtPath) return undefined;
if (!/\.mogrt$/i.test(mogrtPath)) throw new Error("mogrt_path must point to a .mogrt file");
const approvedWorkspacePath = text(value.approved_workspace_path, "approved_workspace_path", MAX_PATH);
if (!isAbsolutePortable(mogrtPath) || !isAbsolutePortable(approvedWorkspacePath)) {
throw new Error("mogrt_path and approved_workspace_path must be absolute paths");
}
if (!isWithinApprovedWorkspace(approvedWorkspacePath, mogrtPath)) {
throw new Error("mogrt_path must be within approved_workspace_path");
}
const titleTrackIndex = trackIndex(value.title_track_index, "title_track_index", 1);
if (titleTrackIndex === videoTrackIndex) {
throw new Error("title_track_index must differ from video_track_index so the branding overlay remains separate from the edit");
}
return {
path: mogrtPath,
approved_workspace_path: approvedWorkspacePath,
title_track_index: titleTrackIndex,
title_start_seconds: nonNegativeNumber(value.title_start_seconds, "title_start_seconds", 0.4, 36000),
};
}
function rawObject(value: unknown, field: string): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${field} must be an object`);
return value as Record<string, unknown>;
}
function assertOnlyKeys(value: Record<string, unknown>, keys: readonly string[], field: string): void {
for (const key of Object.keys(value)) {
if (!keys.includes(key)) throw new Error(`${field} contains unsupported field: ${key}`);
}
}
function buildPlan(workflow: WorkflowKind, value: unknown): SpotWorkflowPlan {
const input = rawObject(value, "arguments");
const baseKeys = [
"sequence_id", "asset_item_ids", "clip_duration_seconds", "video_track_index", "audio_track_index",
"motion_style", "transition_name", "transition_duration_seconds",
];
const brandKeys = [...baseKeys, "mogrt_path", "approved_workspace_path", "title_track_index", "title_start_seconds"];
assertOnlyKeys(input, workflow === "brand_spot" ? brandKeys : baseKeys, "arguments");
const videoTrackIndex = trackIndex(input.video_track_index, "video_track_index", 0);
const plan: SpotWorkflowPlan = {
schema_version: 1,
workflow,
sequence_id: text(input.sequence_id, "sequence_id", 256),
asset_item_ids: assetItemIds(input.asset_item_ids),
clip_duration_seconds: positiveNumber(input.clip_duration_seconds, "clip_duration_seconds", workflow === "motion_graphics_demo" ? 5 : 4, 300),
video_track_index: videoTrackIndex,
audio_track_index: trackIndex(input.audio_track_index, "audio_track_index", 0),
motion_style: workflow === "motion_graphics_demo" ? "demo" : motionStyle(input.motion_style, "alternate"),
transition: transition(input.transition_name, input.transition_duration_seconds, "Cross Dissolve", workflow === "motion_graphics_demo" ? 0.75 : 0.5),
};
if (workflow === "brand_spot") {
const overlay = mogrt(input, videoTrackIndex);
if (overlay) plan.mogrt = overlay;
}
return plan;
}
/** Reconstruct a bounded, canonical plan before deriving its confirmation token. */
export function validateSpotWorkflowPlan(value: unknown): SpotWorkflowPlan {
const raw = rawObject(value, "plan");
assertOnlyKeys(raw, [
"schema_version", "workflow", "sequence_id", "asset_item_ids", "clip_duration_seconds", "video_track_index",
"audio_track_index", "motion_style", "transition", "mogrt",
], "plan");
if (raw.schema_version !== 1) throw new Error("plan.schema_version must be 1");
if (raw.workflow !== "motion_graphics_demo" && raw.workflow !== "product_spot" && raw.workflow !== "brand_spot") {
throw new Error("plan.workflow is unsupported");
}
const workflow = raw.workflow;
const input: Record<string, unknown> = {
sequence_id: raw.sequence_id,
asset_item_ids: raw.asset_item_ids,
clip_duration_seconds: raw.clip_duration_seconds,
video_track_index: raw.video_track_index,
audio_track_index: raw.audio_track_index,
};
if (workflow !== "motion_graphics_demo") input.motion_style = raw.motion_style;
else if (raw.motion_style !== "demo") throw new Error("motion_graphics_demo plans must use demo motion_style");
if (raw.transition !== undefined) {
const rawTransition = rawObject(raw.transition, "plan.transition");
assertOnlyKeys(rawTransition, ["name", "duration_seconds"], "plan.transition");
input.transition_name = rawTransition.name;
input.transition_duration_seconds = rawTransition.duration_seconds;
} else {
input.transition_name = "none";
}
if (raw.mogrt !== undefined) {
if (workflow !== "brand_spot") throw new Error("only brand_spot plans may include mogrt");
const rawMogrt = rawObject(raw.mogrt, "plan.mogrt");
assertOnlyKeys(rawMogrt, ["path", "approved_workspace_path", "title_track_index", "title_start_seconds"], "plan.mogrt");
input.mogrt_path = rawMogrt.path;
input.approved_workspace_path = rawMogrt.approved_workspace_path;
input.title_track_index = rawMogrt.title_track_index;
input.title_start_seconds = rawMogrt.title_start_seconds;
}
return buildPlan(workflow, input);
}
export function spotWorkflowConfirmationToken(plan: SpotWorkflowPlan): string {
return createHash("sha256").update(JSON.stringify(plan)).digest("hex");
}
function changes(plan: SpotWorkflowPlan) {
const placementChanges = plan.asset_item_ids.map((itemId, index) => ({
type: "insert_clip" as const,
project_item_id: itemId,
start_seconds: index * plan.clip_duration_seconds,
video_track_index: plan.video_track_index,
audio_track_index: plan.audio_track_index,
}));
return {
target: { sequence_id: plan.sequence_id, requires_empty_target_tracks: true },
placements: placementChanges,
transition: plan.transition
? { ...plan.transition, applies_only_to_observed_adjacent_cuts: true }
: { skipped: true, reason: "No transition was requested" },
motion: plan.motion_style === "none"
? { skipped: true }
: { style: plan.motion_style, parameter_readback_only: true },
mogrt: plan.mogrt
? { title_track_index: plan.mogrt.title_track_index, start_seconds: plan.mogrt.title_start_seconds, workspace_contained: true }
: { skipped: true },
};
}
function quotedArray(values: readonly string[]): string {
return `[${values.map((value) => `"${escapeForExtendScript(value)}"`).join(",")}]`;
}
function buildApplyScript(plan: SpotWorkflowPlan): string {
const sequenceId = escapeForExtendScript(plan.sequence_id);
const itemIds = quotedArray(plan.asset_item_ids);
const transitionName = plan.transition ? `"${escapeForExtendScript(plan.transition.name)}"` : "null";
const transitionDuration = plan.transition?.duration_seconds ?? 0;
const mogrtScript = plan.mogrt
? `
var titleTrack = seq.videoTracks[${plan.mogrt.title_track_index}];
if (!titleTrack) return __error("Title video track index ${plan.mogrt.title_track_index} is out of range");
if (titleTrack.clips.numItems !== 0) return __error("Title video track must be empty before applying this brand-spot plan");
`
: "";
const mogrtApplyScript = plan.mogrt
? `
var mogrtCountBefore = titleTrack.clips.numItems;
var mogrtImportResult = null;
try {
mogrtImportResult = seq.importMGT("${escapeForExtendScript(plan.mogrt.path)}", __secondsToTicks(${plan.mogrt.title_start_seconds}).toString(), ${plan.mogrt.title_track_index}, ${plan.audio_track_index});
} catch (mogrtError) {
mogrtVerification = { applied: false, verified: false, error: String(mogrtError) };
}
if (!mogrtVerification) {
var mogrtCountAfter = titleTrack.clips.numItems;
mogrtVerification = mogrtCountAfter > mogrtCountBefore
? { applied: true, verified: true, insertedTrackItems: mogrtCountAfter - mogrtCountBefore }
: { applied: false, verified: false, error: "Premiere returned from importMGT without adding a title-track item" };
}
`
: 'var mogrtVerification = { skipped: true, reason: "No MOGRT was requested" };';
return buildToolScript(`
var seq = __findSequence("${sequenceId}");
if (!seq) return __error("Target sequence was not found");
if (!app.project.activeSequence || String(app.project.activeSequence.sequenceID) !== String(seq.sequenceID)) {
return __error("Target sequence must be active before applying a spot workflow plan; activate it and preview again");
}
var videoTrack = seq.videoTracks[${plan.video_track_index}];
if (!videoTrack) return __error("Video track index ${plan.video_track_index} is out of range");
var audioTrack = seq.audioTracks[${plan.audio_track_index}];
if (!audioTrack) return __error("Audio track index ${plan.audio_track_index} is out of range");
if (videoTrack.clips.numItems !== 0 || audioTrack.clips.numItems !== 0) {
return __error("Target video and audio tracks must be empty before applying a spot workflow plan");
}
${mogrtScript}
var requestedItemIds = ${itemIds};
var requestedItems = [];
for (var requestIndex = 0; requestIndex < requestedItemIds.length; requestIndex++) {
var requestedItem = __findProjectItem(requestedItemIds[requestIndex]);
if (!requestedItem || String(requestedItem.nodeId) !== String(requestedItemIds[requestIndex])) {
return __error("Project item ID was not found exactly: " + requestedItemIds[requestIndex]);
}
requestedItems.push(requestedItem);
}
var transitionName = ${transitionName};
var transitionDuration = ${transitionDuration};
var transitionReady = false;
var qeTrack = null;
var transitionQE = null;
var transitionPreflight = { requested: !!transitionName, ready: false };
if (transitionName) {
try {
app.enableQE();
var qeSequence = qe.project.getActiveSequence();
qeTrack = qeSequence ? qeSequence.getVideoTrackAt(${plan.video_track_index}) : null;
if (qeTrack && qe.project.getVideoTransitionByName) {
transitionQE = qe.project.getVideoTransitionByName(transitionName);
}
transitionReady = !!qeTrack && !!transitionQE;
transitionPreflight = transitionReady
? { requested: true, ready: true }
: { requested: true, ready: false, reason: "The active Premiere build did not expose the requested transition or QE video track" };
} catch (transitionProbeError) {
transitionPreflight = { requested: true, ready: false, reason: String(transitionProbeError) };
}
}
var frameTicks = parseFloat(seq.timebase);
if (!frameTicks || isNaN(frameTicks)) frameTicks = TICKS_PER_SECOND / 24;
var frameTolerance = __ticksToSeconds(frameTicks) + 0.000001;
var placed = [];
var usedNodeIds = {};
function findPlacedClip(track, itemId, expectedStart) {
for (var clipIndex = 0; clipIndex < track.clips.numItems; clipIndex++) {
var candidate = track.clips[clipIndex];
if (!candidate.projectItem || String(candidate.projectItem.nodeId) !== String(itemId) || usedNodeIds[String(candidate.nodeId)]) continue;
if (Math.abs(__ticksToSeconds(candidate.start.ticks) - expectedStart) <= frameTolerance) return candidate;
}
return null;
}
function trimPlacedClip(clip, targetEnd) {
var requestedEnd = new Time(); requestedEnd.ticks = __secondsToTicks(targetEnd).toString();
clip.end = requestedEnd;
return Math.abs(__ticksToSeconds(clip.end.ticks) - targetEnd) <= frameTolerance;
}
for (var placementIndex = 0; placementIndex < requestedItems.length; placementIndex++) {
var targetStart = placementIndex * ${plan.clip_duration_seconds};
var targetEnd = targetStart + ${plan.clip_duration_seconds};
var audioCountBefore = audioTrack.clips.numItems;
seq.insertClip(requestedItems[placementIndex], __secondsToTicks(targetStart).toString(), ${plan.video_track_index}, ${plan.audio_track_index});
var placedClip = findPlacedClip(videoTrack, requestedItemIds[placementIndex], targetStart);
if (!placedClip) return __error("Premiere did not add the requested video item at the planned frame; the assembly is not reported as verified");
if (!trimPlacedClip(placedClip, targetEnd)) return __error("Premiere did not trim the placed video item to the previewed duration; the assembly is not reported as verified");
var audioCountAfter = audioTrack.clips.numItems;
if (audioCountAfter > audioCountBefore) {
if (audioCountAfter !== audioCountBefore + 1) return __error("Premiere added an unexpected number of audio items; the assembly is not reported as verified");
var placedAudio = findPlacedClip(audioTrack, requestedItemIds[placementIndex], targetStart);
if (!placedAudio || !trimPlacedClip(placedAudio, targetEnd)) return __error("Premiere did not identify and trim the placed audio item to the previewed duration; the assembly is not reported as verified");
}
usedNodeIds[String(placedClip.nodeId)] = true;
placed.push({
nodeId: String(placedClip.nodeId),
projectItemId: requestedItemIds[placementIndex],
startSeconds: __ticksToSeconds(placedClip.start.ticks),
endSeconds: __ticksToSeconds(placedClip.end.ticks),
requestedDurationSeconds: ${plan.clip_duration_seconds},
verified: true
});
}
function scaleRange(style, index) {
if (style === "push_in") return { from: 100, to: 108 };
if (style === "pull_out") return { from: 108, to: 100 };
if (style === "demo") {
if (index === 1) return { from: 112, to: 100 };
return { from: 100, to: index === 2 ? 106 : 108 };
}
return index % 2 === 1 ? { from: 110, to: 100 } : { from: 100, to: 108 };
}
function addScaleMotion(clip, index) {
if ("${plan.motion_style}" === "none") return { skipped: true };
var motionComponent = null;
for (var componentIndex = 0; componentIndex < clip.components.numItems; componentIndex++) {
var component = clip.components[componentIndex];
if (component.displayName === "Motion" || component.matchName === "AE.ADBE Motion") { motionComponent = component; break; }
}
if (!motionComponent) return { applied: false, verified: false, reason: "Motion component was not available on the placed clip" };
var scaleProperty = null;
for (var propertyIndex = 0; propertyIndex < motionComponent.properties.numItems; propertyIndex++) {
var property = motionComponent.properties[propertyIndex];
if (property.displayName === "Scale") { scaleProperty = property; break; }
}
if (!scaleProperty) return { applied: false, verified: false, reason: "Motion Scale property was not available on the placed clip" };
var start = __ticksToSeconds(clip.start.ticks);
var end = __ticksToSeconds(clip.end.ticks) - 0.1;
if (end <= start) return { applied: false, verified: false, reason: "Placed clip is too short for the requested scale motion" };
var range = scaleRange("${plan.motion_style}", index);
try {
if (!scaleProperty.isTimeVarying()) scaleProperty.setTimeVarying(true);
var startTime = new Time(); startTime.ticks = __secondsToTicks(start).toString();
var endTime = new Time(); endTime.ticks = __secondsToTicks(end).toString();
scaleProperty.addKey(startTime); scaleProperty.setValueAtKey(startTime, range.from, true);
scaleProperty.addKey(endTime); scaleProperty.setValueAtKey(endTime, range.to, true);
var readStart = scaleProperty.getValueAtKey(startTime);
var readEnd = scaleProperty.getValueAtKey(endTime);
if (typeof readStart === "number" && Math.abs(readStart - range.from) > 0.0001) throw new Error("start keyframe readback differed");
if (typeof readEnd === "number" && Math.abs(readEnd - range.to) > 0.0001) throw new Error("end keyframe readback differed");
return { applied: true, verified: true, startSeconds: start, endSeconds: end, from: range.from, to: range.to };
} catch (motionError) {
return { applied: false, verified: false, reason: String(motionError) };
}
}
var motionResults = [];
for (var motionIndex = 0; motionIndex < placed.length; motionIndex++) {
var verifiedClip = __findClip(placed[motionIndex].nodeId);
motionResults.push(verifiedClip ? addScaleMotion(verifiedClip.clip, motionIndex) : { applied: false, verified: false, reason: "Placed clip could not be re-resolved for motion" });
}
var transitionResults = [];
for (var cutIndex = 0; cutIndex < placed.length - 1; cutIndex++) {
if (!transitionName) { transitionResults.push({ skipped: true, reason: "No transition was requested" }); continue; }
if (!transitionReady) { transitionResults.push({ skipped: true, reason: transitionPreflight.reason || "Transition API unavailable" }); continue; }
if (Math.abs(placed[cutIndex].endSeconds - placed[cutIndex + 1].startSeconds) > frameTolerance) {
transitionResults.push({ skipped: true, reason: "Placed clips were not adjacent, so no transition was added" }); continue;
}
var transitionCountBefore = videoTrack.transitions.numItems;
try {
var placedInfo = __findClip(placed[cutIndex + 1].nodeId);
var qeClip = placedInfo ? __findQeClipByDomClip(qeTrack, placedInfo.clip) : null;
if (!qeClip || typeof qeClip.addTransition !== "function") {
transitionResults.push({ applied: false, verified: false, reason: "The target QE clip did not expose addTransition" });
continue;
}
var durationFrames = Math.max(1, Math.round(__secondsToTicks(transitionDuration) / frameTicks));
qeClip.addTransition(transitionQE, true, String(durationFrames), "0", 0.5, false, true);
var transitionCountAfter = videoTrack.transitions.numItems;
var expectedCutTicks = __secondsToTicks(placed[cutIndex + 1].startSeconds);
var foundAtExpectedCut = false;
for (var transitionIndex = 0; transitionIndex < videoTrack.transitions.numItems; transitionIndex++) {
var transitionReadback = videoTrack.transitions[transitionIndex];
var transitionStartTicks = parseFloat(transitionReadback.start.ticks);
var transitionEndTicks = parseFloat(transitionReadback.end.ticks);
if (!isNaN(transitionStartTicks) && !isNaN(transitionEndTicks) && Math.abs(((transitionStartTicks + transitionEndTicks) / 2) - expectedCutTicks) <= frameTicks / 2) { foundAtExpectedCut = true; break; }
}
transitionResults.push(transitionCountAfter > transitionCountBefore && foundAtExpectedCut
? { applied: true, verified: true, atSeconds: placed[cutIndex].endSeconds }
: { applied: false, verified: false, reason: transitionCountAfter > transitionCountBefore ? "Premiere added a transition, but not at the requested cut" : "Premiere did not add a transition to the track" });
} catch (transitionError) {
transitionResults.push({ applied: false, verified: false, reason: String(transitionError) });
}
}
${mogrtApplyScript}
return __result({
applied: true,
workflow: "${plan.workflow}",
targetSequenceId: String(seq.sequenceID),
placements: placed,
transitions: transitionResults,
transitionPreflight: transitionPreflight,
motion: motionResults,
mogrt: mogrtVerification,
renderVerified: false,
verificationScope: "Placement, transition-count, MOGRT track-count, and Motion keyframe readback only. Inspect the resulting sequence and verify playback or an exported frame before delivery."
});
`);
}
function previewResult(plan: SpotWorkflowPlan, operationId: string) {
return {
success: true,
data: {
operationId,
applied: false,
plan,
changes: changes(plan),
confirmationToken: spotWorkflowConfirmationToken(plan),
verificationScope: "This is a local plan preview. It does not inspect Premiere, prove assets exist, or make project changes.",
},
};
}
const sharedProperties = {
sequence_id: { type: "string", description: "Exact ID of an existing empty destination sequence. It must be active at apply time." },
asset_item_ids: { type: "array", description: "Exact existing project-item node IDs in playback order; external media paths are deliberately not imported by this workflow.", items: { type: "string" } },
clip_duration_seconds: { type: "number", description: "Planned spacing between asset starts; defaults to 5 seconds for a motion demo and 4 seconds for spots." },
video_track_index: { type: "number", description: "Empty destination video track index, default 0." },
audio_track_index: { type: "number", description: "Empty destination audio track index, default 0." },
motion_style: { type: "string", enum: ["none", "push_in", "pull_out", "alternate"], description: "Scale-motion style for product and brand spots; defaults to alternate." },
transition_name: { type: "string", description: "Transition name; defaults to Cross Dissolve. Use none to disable transitions." },
transition_duration_seconds: { type: "number", description: "Transition duration, default 0.75 seconds for demos and 0.5 seconds for spots." },
};
const planParameter = { type: "object", description: "Exact plan returned by a preview_*_spot tool" };
export function getSpotWorkflowTools(bridgeOptions: BridgeOptions, dependencies: SpotWorkflowDependencies = {}) {
const capabilities = dependencies.capabilities ?? resolveCapabilities();
const auditSink = dependencies.auditSink ?? stderrAuditSink;
const nextId = dependencies.operationIdFactory ?? createOperationId;
const fileExists = dependencies.fileExists ?? existsSync;
const preview = (workflow: WorkflowKind) => async (args: Record<string, unknown>) => {
const operationId = nextId();
requireCapability(capabilities, "inspect", operationId);
return previewResult(buildPlan(workflow, args), operationId);
};
return {
preview_motion_graphics_demo: {
description: "Preview a contained motion-graphics demo assembly from existing project items. It never creates demo assets, imports files, creates a sequence, or changes Premiere; apply requires an exact confirmation token.",
parameters: {
type: "object" as const,
additionalProperties: false,
properties: {
sequence_id: sharedProperties.sequence_id,
asset_item_ids: sharedProperties.asset_item_ids,
clip_duration_seconds: sharedProperties.clip_duration_seconds,
video_track_index: sharedProperties.video_track_index,
audio_track_index: sharedProperties.audio_track_index,
transition_name: sharedProperties.transition_name,
transition_duration_seconds: sharedProperties.transition_duration_seconds,
},
required: ["sequence_id", "asset_item_ids"],
},
handler: preview("motion_graphics_demo"),
},
preview_product_spot: {
description: "Preview a product-spot assembly from existing project items. The eventual apply is limited to explicit empty tracks, revalidates item IDs, and reports host readback without claiming visual delivery verification.",
parameters: {
type: "object" as const,
additionalProperties: false,
properties: sharedProperties,
required: ["sequence_id", "asset_item_ids"],
},
handler: preview("product_spot"),
},
preview_brand_spot: {
description: "Preview a brand-spot assembly from existing project items with an optional workspace-contained MOGRT overlay. Preview is local-only; it does not read or import the MOGRT file.",
parameters: {
type: "object" as const,
additionalProperties: false,
properties: {
...sharedProperties,
mogrt_path: { type: "string", description: "Optional absolute .mogrt path, which must be inside approved_workspace_path." },
approved_workspace_path: { type: "string", description: "Operator-approved root containing mogrt_path; required whenever mogrt_path is supplied." },
title_track_index: { type: "number", description: "Empty title-overlay video track index, default 1 and distinct from video_track_index." },
title_start_seconds: { type: "number", description: "MOGRT start time in seconds, default 0.4." },
},
required: ["sequence_id", "asset_item_ids"],
},
handler: preview("brand_spot"),
},
apply_spot_workflow_plan: {
description: "Apply one exact previewed motion-demo, product-spot, or brand-spot plan. Requires edit authority, requires filesystem authority for a MOGRT, and only targets empty explicitly named tracks. Host readback is not playback or render verification.",
parameters: {
type: "object" as const,
additionalProperties: false,
properties: {
plan: planParameter,
confirmation_token: { type: "string", description: "Exact token returned by the corresponding preview tool." },
},
required: ["plan", "confirmation_token"],
},
handler: async (args: { plan: unknown; confirmation_token: string }) => {
const operationId = nextId();
try {
requireCapability(capabilities, "edit", operationId);
const plan = validateSpotWorkflowPlan(args.plan);
if (args.confirmation_token !== spotWorkflowConfirmationToken(plan)) {
throw new Error("Confirmation token does not match this spot workflow plan; preview it again");
}
if (plan.mogrt) {
requireCapability(capabilities, "filesystem", operationId);
if (!fileExists(plan.mogrt.path)) {
return { success: false, error: "The previewed MOGRT file no longer exists; preview the plan again after correcting it" };
}
}
emitAudit(auditSink, { operationId, action: "apply_spot_workflow_plan", outcome: "started", details: { workflow: plan.workflow, placements: plan.asset_item_ids.length, hasMogrt: !!plan.mogrt } });
const result = await sendCommand(buildApplyScript(plan), bridgeOptions);
emitAudit(auditSink, { operationId, action: "apply_spot_workflow_plan", outcome: result.success ? "succeeded" : "failed" });
return result.success
? { ...result, data: { ...(result.data as object), operationId } }
: { ...result, error: `${result.error ?? "Spot workflow plan failed"} (operation ${operationId})` };
} catch (error) {
emitAudit(auditSink, { operationId, action: "apply_spot_workflow_plan", outcome: error instanceof Error && error.name === "CapabilityDeniedError" ? "denied" : "failed" });
throw error;
}
},
},
};
}