feat: criado protótipo local de montagem incremental com gancho e

- criado protótipo local de montagem incremental com gancho e trechos preservados
- adicionado checkbox no painel para executar planos pelo modo de montagem incremental

Resumo:
- 8 arquivos alterados
- 4 novos
- 4 modificados
- 0 removidos

 4 files changed, 230 insertions(+)

Arquivos:
  - .jhonny/analises.db
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/src/tools/timeline.ts
  - code/engine/aplicar_montagem_incremental_prototipo.py
  - code/engine/editor/pedido_montagem_incremental_exemplo.json
  - code/engine/editor/plano_montagem_incremental_6.json
  - code/engine/editor/prototipo_montagem_incremental.py
This commit is contained in:
João Henrique
2026-09-10 19:21:07 -04:00
parent 024b004938
commit 59f1358f18
8 changed files with 643 additions and 0 deletions
+178
View File
@@ -3,6 +3,184 @@ import { sendCommand, BridgeOptions } from "../bridge/file-bridge.js";
export function getTimelineTools(bridgeOptions: BridgeOptions) {
return {
assemble_incremental_edit_prototype: {
description:
"PROTOTYPE: clone a source sequence, clear only the clone, and assemble one project item's A/V source ranges in order. The source sequence is not edited.",
parameters: {
type: "object" as const,
additionalProperties: false,
properties: {
source_sequence_id: {
type: "string",
minLength: 1,
description: "Source sequence name or ID to clone",
},
source_item_id: {
type: "string",
minLength: 1,
description: "Project item name or node ID used by every source range",
},
target_name: {
type: "string",
minLength: 1,
maxLength: 255,
description: "Name for the cloned destination sequence",
},
ranges: {
type: "array",
minItems: 1,
maxItems: 64,
items: {
type: "object",
additionalProperties: false,
properties: {
in_seconds: { type: "number", minimum: 0 },
out_seconds: { type: "number", minimum: 0 },
destination_seconds: { type: "number", minimum: 0 },
label: { type: "string", maxLength: 255 },
},
required: ["in_seconds", "out_seconds", "destination_seconds"],
},
},
confirm_prototype: {
type: "boolean",
description: "Must be true because this creates and mutates a Premiere sequence",
},
},
required: ["source_sequence_id", "source_item_id", "target_name", "ranges", "confirm_prototype"],
},
handler: async (args: {
source_sequence_id: string;
source_item_id: string;
target_name: string;
ranges: Array<{
in_seconds: number;
out_seconds: number;
destination_seconds: number;
label?: string;
}>;
confirm_prototype: boolean;
}) => {
if (args.confirm_prototype !== true) {
return { success: false, error: "confirm_prototype must be true for the experimental sequence assembly." };
}
if (!Array.isArray(args.ranges) || args.ranges.length === 0 || args.ranges.length > 64) {
return { success: false, error: "ranges must contain between 1 and 64 source ranges." };
}
for (const [index, range] of args.ranges.entries()) {
if (!Number.isFinite(range.in_seconds) || !Number.isFinite(range.out_seconds) || !Number.isFinite(range.destination_seconds) ||
range.in_seconds < 0 || range.out_seconds <= range.in_seconds || range.destination_seconds < 0) {
return { success: false, error: `ranges[${index}] has invalid source or destination timing.` };
}
}
const sourceSequenceId = escapeForExtendScript(args.source_sequence_id);
const sourceItemId = escapeForExtendScript(args.source_item_id);
const targetName = escapeForExtendScript(args.target_name);
const ranges = args.ranges.map((range) => ({
inSeconds: range.in_seconds,
outSeconds: range.out_seconds,
destinationSeconds: range.destination_seconds,
label: range.label || "",
}));
const rangeJson = JSON.stringify(ranges);
const script = buildToolScript(`
var sourceSequence = __findSequence("${sourceSequenceId}");
if (!sourceSequence) return __error("Source sequence not found; no prototype sequence was created.");
var item = __findProjectItem("${sourceItemId}");
if (!item) return __error("Source project item not found; no prototype sequence was created.");
var beforeIds = {};
var sequenceIndex;
for (sequenceIndex = 0; sequenceIndex < app.project.sequences.numSequences; sequenceIndex++) {
beforeIds[String(app.project.sequences[sequenceIndex].sequenceID)] = true;
}
sourceSequence.clone();
var destinationSequence = null;
for (sequenceIndex = 0; sequenceIndex < app.project.sequences.numSequences; sequenceIndex++) {
var candidate = app.project.sequences[sequenceIndex];
if (!beforeIds[String(candidate.sequenceID)]) {
destinationSequence = candidate;
break;
}
}
if (!destinationSequence) return __error("Premiere did not expose the cloned destination sequence.");
destinationSequence.name = "${targetName}";
var trackGroup;
var trackIndex;
var clipIndex;
var trackGroups = [destinationSequence.videoTracks, destinationSequence.audioTracks];
for (trackGroup = 0; trackGroup < trackGroups.length; trackGroup++) {
for (trackIndex = 0; trackIndex < trackGroups[trackGroup].numTracks; trackIndex++) {
var track = trackGroups[trackGroup][trackIndex];
for (clipIndex = track.clips.numItems - 1; clipIndex >= 0; clipIndex--) {
track.clips[clipIndex].remove(false, false);
}
if (track.clips.numItems !== 0) return __error("Could not clear the cloned destination sequence.");
}
}
if (!destinationSequence.videoTracks.numTracks || !destinationSequence.audioTracks.numTracks) {
return __error("The cloned destination sequence must have at least one video and one audio track.");
}
var ranges = ${rangeJson};
var oldIn = item.getInPoint(4);
var oldOut = item.getOutPoint(4);
var oldInTicks = oldIn ? oldIn.ticks : null;
var oldOutTicks = oldOut ? oldOut.ticks : null;
var placements = [];
var insertionError = null;
try {
for (var rangeIndex = 0; rangeIndex < ranges.length; rangeIndex++) {
var range = ranges[rangeIndex];
if (range.outSeconds <= range.inSeconds) return __error("A prototype source range has non-positive duration.");
item.setInPoint(__secondsToTicks(range.inSeconds).toString(), 4);
item.setOutPoint(__secondsToTicks(range.outSeconds).toString(), 4);
destinationSequence.insertClip(item, __secondsToTicks(range.destinationSeconds).toString(), 0, 0);
placements.push({
label: range.label,
sourceInSeconds: range.inSeconds,
sourceOutSeconds: range.outSeconds,
destinationSeconds: range.destinationSeconds,
durationSeconds: range.outSeconds - range.inSeconds
});
}
} catch (insertError) {
insertionError = insertError.toString();
} finally {
try {
if (oldInTicks !== null) item.setInPoint(String(oldInTicks), 4); else item.clearInPoint();
if (oldOutTicks !== null) item.setOutPoint(String(oldOutTicks), 4); else item.clearOutPoint();
} catch (restoreError) {
return __error("The source item's in/out could not be restored: " + restoreError.toString());
}
}
if (insertionError !== null) return __error("Premiere rejected a prototype source-range insertion: " + insertionError);
var videoCount = destinationSequence.videoTracks[0].clips.numItems;
var audioCount = destinationSequence.audioTracks[0].clips.numItems;
if (videoCount !== ranges.length || audioCount !== ranges.length) {
return __error("The prototype destination did not receive exactly one video and one audio clip per range.");
}
return __result({
prototype: true,
verified: true,
sourceSequenceId: String(sourceSequence.sequenceID),
destinationSequenceId: String(destinationSequence.sequenceID),
destinationSequenceName: destinationSequence.name,
videoClipCount: videoCount,
audioClipCount: audioCount,
placements: placements,
note: "The source sequence was not edited. Review the cloned sequence manually before keeping it."
});
`);
return sendCommand(script, bridgeOptions);
},
},
add_to_timeline: {
description:
"Insert a project item at a timeline position and verify Premiere added no unexpected same-track fragments.",