feat: criado monitoramento contínuo de trilhas com varredura recur
- criado monitoramento contínuo de trilhas com varredura recursiva e baixa prioridade - gerado e validado plano editorial revisável do depoimento da Erika sem aplicação na timeline - adicionada leitura e comparação estrutural da timeline após aplicar planos de edição - limitadas as classificações de trilhas às duas maiores probabilidades de cada categoria - limpo o catálogo musical para reiniciar as análises sem remover dados de vídeo Resumo: - 34 arquivos alterados - 6 novos - 28 modificados - 0 removidos 28 files changed, 833 insertions(+), 75 deletions(-) Arquivos: - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/engine/analisar_trilhas.py - code/engine/aplicar_plano_de_edicao.py - code/engine/consultar_detalhes_de_trilha.py - code/engine/editor/__init__.py - code/engine/editor/aplicador_de_plano_de_edicao.py - code/engine/editor/backup_de_sequencia.py - code/engine/editor/escrita/escrita_no_editor.py - code/engine/editor/modelos.py - code/engine/editor/validacao_semantica.py - code/engine/integracoes/audio/classificador_de_musica_essentia.py - code/engine/integracoes/audio/modelos_de_analise_musical.py - code/engine/integracoes/audio/provider_de_analise_musical_essentia.py - code/engine/persistencia/esquema.py - code/engine/persistencia/repositorio_de_trilhas.py - code/engine/testes/duplos_de_premiere.py - code/engine/testes/test_aplicador_de_plano_de_edicao.py - code/engine/testes/test_aplicar_plano_de_edicao.py - code/engine/testes/test_escrita_no_editor.py - code/engine/testes/test_leitor_de_plano_de_edicao.py - code/engine/testes/test_validador_semantico_de_plano.py - code/plugins/premiere-pro/skills/editar-por-voz/SKILL.md - code/plugins/premiere-pro/skills/editar-por-voz/criterios/08-formato-de-saida.md - code/plugins/premiere-pro/skills/editar-por-voz/criterios/10-revisao-humana.md - code/src/tools/timeline.ts - code/tests/tools/tool-modules.test.ts - .jhonny/planos/ - code/.jhonny/analises.db-shm - code/.jhonny/analises.db-wal - code/engine/editor/verificador_da_aplicacao.py - code/engine/testes/test_verificador_da_aplicacao.py - code/plugins/premiere-pro/skills/editar-por-voz/criterios/11-gancho.md
This commit is contained in:
@@ -194,6 +194,176 @@ export function getTimelineTools(bridgeOptions: BridgeOptions) {
|
||||
},
|
||||
},
|
||||
|
||||
duplicate_linked_range_to_start: {
|
||||
description:
|
||||
"Duplicate one isolated, normal-speed linked video/audio range at sequence start with a verified ripple insert while preserving the original occurrence.",
|
||||
parameters: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
node_ids: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
minItems: 2,
|
||||
maxItems: 2,
|
||||
description: "IDs of the isolated video and audio clips that form the range.",
|
||||
},
|
||||
},
|
||||
required: ["node_ids"],
|
||||
},
|
||||
handler: async (args: { node_ids: string[] }) => {
|
||||
if (!Array.isArray(args.node_ids) || args.node_ids.length !== 2 || args.node_ids.some((id) => typeof id !== "string" || !id)) {
|
||||
return { success: false, error: "node_ids must contain exactly two non-empty clip IDs." };
|
||||
}
|
||||
const ids = args.node_ids.map(escapeForExtendScript);
|
||||
const script = buildToolScript(`
|
||||
var first = __findClip("${ids[0]}");
|
||||
var second = __findClip("${ids[1]}");
|
||||
if (!first || !second) return __error("One of the requested clips was not found; nothing was duplicated.");
|
||||
if (first.trackType === second.trackType) return __error("The range must contain one video clip and one audio clip.");
|
||||
|
||||
var video = first.trackType === "video" ? first : second;
|
||||
var audio = first.trackType === "audio" ? first : second;
|
||||
var seq = app.project.activeSequence;
|
||||
if (!seq) return __error("No active sequence; nothing was duplicated.");
|
||||
var frameTicks = parseFloat(seq.timebase);
|
||||
if (!frameTicks || isNaN(frameTicks)) frameTicks = TICKS_PER_SECOND / 24;
|
||||
var beforeEndTicks = parseFloat(seq.end.ticks);
|
||||
var videoStart = parseFloat(video.clip.start.ticks);
|
||||
var audioStart = parseFloat(audio.clip.start.ticks);
|
||||
var videoEnd = parseFloat(video.clip.end.ticks);
|
||||
var audioEnd = parseFloat(audio.clip.end.ticks);
|
||||
if (Math.abs(videoStart - audioStart) > frameTicks || Math.abs(videoEnd - audioEnd) > frameTicks) {
|
||||
return __error("The video and audio clips do not occupy the same range; nothing was duplicated.");
|
||||
}
|
||||
if (videoStart <= frameTicks) return __error("The requested range is already at sequence start; nothing was duplicated.");
|
||||
if (!video.clip.projectItem || !audio.clip.projectItem || video.clip.projectItem.nodeId !== audio.clip.projectItem.nodeId) {
|
||||
return __error("The video and audio clips do not share one source project item; nothing was duplicated.");
|
||||
}
|
||||
var videoSpeed = 100;
|
||||
var audioSpeed = 100;
|
||||
var videoReversed = false;
|
||||
var audioReversed = false;
|
||||
try { videoSpeed = Number(video.clip.getSpeed()); } catch (e) {}
|
||||
try { audioSpeed = Number(audio.clip.getSpeed()); } catch (e) {}
|
||||
try { videoReversed = video.clip.isSpeedReversed() == 1; } catch (e) {}
|
||||
try { audioReversed = audio.clip.isSpeedReversed() == 1; } catch (e) {}
|
||||
if (Math.abs(videoSpeed - 100) > 0.001 || Math.abs(audioSpeed - 100) > 0.001 || videoReversed || audioReversed) {
|
||||
return __error("Retimed or reversed clips cannot be duplicated by this guarded operation.");
|
||||
}
|
||||
|
||||
var g;
|
||||
var t;
|
||||
var groups = [seq.videoTracks, seq.audioTracks];
|
||||
for (g = 0; g < groups.length; g++) {
|
||||
for (t = 0; t < groups[g].numTracks; t++) {
|
||||
var isTarget = (g === 0 && t === video.trackIndex) || (g === 1 && t === audio.trackIndex);
|
||||
if (!isTarget && groups[g][t].clips.numItems > 0) {
|
||||
return __error("The guarded hook only supports a simple sequence with content on one video and one audio track.");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (seq.videoTracks[video.trackIndex].clips.numItems < 1 || seq.audioTracks[audio.trackIndex].clips.numItems < 1) {
|
||||
return __error("The target tracks are empty; nothing was duplicated.");
|
||||
}
|
||||
if ((seq.videoTracks[video.trackIndex].isLocked && seq.videoTracks[video.trackIndex].isLocked()) ||
|
||||
(seq.audioTracks[audio.trackIndex].isLocked && seq.audioTracks[audio.trackIndex].isLocked())) {
|
||||
return __error("A target track is locked; nothing was duplicated.");
|
||||
}
|
||||
if (parseFloat(seq.videoTracks[video.trackIndex].clips[0].start.ticks) > frameTicks ||
|
||||
parseFloat(seq.audioTracks[audio.trackIndex].clips[0].start.ticks) > frameTicks) {
|
||||
return __error("The guarded hook requires both target tracks to begin at sequence time zero.");
|
||||
}
|
||||
|
||||
var item = video.clip.projectItem;
|
||||
var sourceInTicks = parseFloat(video.clip.inPoint.ticks);
|
||||
var sourceOutTicks = parseFloat(video.clip.outPoint.ticks);
|
||||
if (Math.abs(sourceInTicks - parseFloat(audio.clip.inPoint.ticks)) > frameTicks ||
|
||||
Math.abs(sourceOutTicks - parseFloat(audio.clip.outPoint.ticks)) > frameTicks) {
|
||||
return __error("The linked clips do not share matching source in/out points; nothing was duplicated.");
|
||||
}
|
||||
var durationTicks = videoEnd - videoStart;
|
||||
if (durationTicks <= frameTicks) return __error("The requested range is too short to duplicate safely.");
|
||||
|
||||
var oldIn = item.getInPoint(4);
|
||||
var oldOut = item.getOutPoint(4);
|
||||
var oldInTicks = oldIn ? oldIn.ticks : null;
|
||||
var oldOutTicks = oldOut ? oldOut.ticks : null;
|
||||
var insertionError = null;
|
||||
try {
|
||||
item.setInPoint(String(sourceInTicks), 4);
|
||||
item.setOutPoint(String(sourceOutTicks), 4);
|
||||
seq.insertClip(item, "0", video.trackIndex, audio.trackIndex);
|
||||
} 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 range could not be restored after insertion: " + restoreError.toString());
|
||||
}
|
||||
}
|
||||
if (insertionError !== null) return __error("Premiere could not insert the range at sequence start: " + insertionError);
|
||||
|
||||
var insertedVideo = null;
|
||||
var insertedAudio = null;
|
||||
var vc;
|
||||
var ac;
|
||||
for (vc = 0; vc < seq.videoTracks[video.trackIndex].clips.numItems; vc++) {
|
||||
var candidateVideo = seq.videoTracks[video.trackIndex].clips[vc];
|
||||
if (candidateVideo.nodeId !== "${ids[0]}" && candidateVideo.nodeId !== "${ids[1]}" &&
|
||||
candidateVideo.projectItem && candidateVideo.projectItem.nodeId === item.nodeId &&
|
||||
Math.abs(parseFloat(candidateVideo.start.ticks)) <= frameTicks) insertedVideo = candidateVideo;
|
||||
}
|
||||
for (ac = 0; ac < seq.audioTracks[audio.trackIndex].clips.numItems; ac++) {
|
||||
var candidateAudio = seq.audioTracks[audio.trackIndex].clips[ac];
|
||||
if (candidateAudio.nodeId !== "${ids[0]}" && candidateAudio.nodeId !== "${ids[1]}" &&
|
||||
candidateAudio.projectItem && candidateAudio.projectItem.nodeId === item.nodeId &&
|
||||
Math.abs(parseFloat(candidateAudio.start.ticks)) <= frameTicks) insertedAudio = candidateAudio;
|
||||
}
|
||||
if (!insertedVideo || !insertedAudio) {
|
||||
return __error("Premiere inserted content but the new linked range could not be verified at sequence start. Restore the automatic sequence backup.");
|
||||
}
|
||||
if (Math.abs((parseFloat(insertedVideo.end.ticks) - parseFloat(insertedVideo.start.ticks)) - durationTicks) > frameTicks ||
|
||||
Math.abs((parseFloat(insertedAudio.end.ticks) - parseFloat(insertedAudio.start.ticks)) - durationTicks) > frameTicks) {
|
||||
return __error("The inserted range has an unexpected duration. Restore the automatic sequence backup.");
|
||||
}
|
||||
var insertedVideoId = String(insertedVideo.nodeId);
|
||||
var insertedAudioId = String(insertedAudio.nodeId);
|
||||
|
||||
var preservedFirst = __findClip("${ids[0]}");
|
||||
var preservedSecond = __findClip("${ids[1]}");
|
||||
if (!preservedFirst || !preservedSecond ||
|
||||
Math.abs(parseFloat(preservedFirst.clip.start.ticks) - (videoStart + durationTicks)) > frameTicks ||
|
||||
Math.abs(parseFloat(preservedSecond.clip.start.ticks) - (audioStart + durationTicks)) > frameTicks) {
|
||||
return __error("The original range was not preserved after the ripple insert. Restore the automatic sequence backup.");
|
||||
}
|
||||
|
||||
var hookVideo = __findClip(insertedVideoId);
|
||||
var hookAudio = __findClip(insertedAudioId);
|
||||
if (!hookVideo || !hookAudio ||
|
||||
Math.abs(parseFloat(hookVideo.clip.start.ticks)) > frameTicks ||
|
||||
Math.abs(parseFloat(hookAudio.clip.start.ticks)) > frameTicks) {
|
||||
return __error("The duplicated hook was not preserved at sequence start. Restore the automatic sequence backup.");
|
||||
}
|
||||
var afterEndTicks = parseFloat(seq.end.ticks);
|
||||
if (Math.abs(afterEndTicks - (beforeEndTicks + durationTicks)) > frameTicks * 2) {
|
||||
return __error("The sequence duration did not grow by the hook duration. Restore the automatic sequence backup.");
|
||||
}
|
||||
return __result({
|
||||
duplicated: true,
|
||||
verified: true,
|
||||
targetSeconds: 0,
|
||||
durationSeconds: __ticksToSeconds(durationTicks),
|
||||
insertedNodeIds: [String(hookVideo.clip.nodeId), String(hookAudio.clip.nodeId)],
|
||||
originalNodeIdsPreserved: ["${ids[0]}", "${ids[1]}"],
|
||||
clipEffectsPreserved: false
|
||||
});
|
||||
`);
|
||||
return sendCommand(script, bridgeOptions);
|
||||
},
|
||||
},
|
||||
|
||||
move_clip: {
|
||||
description: "Move a clip to a new position on the timeline",
|
||||
parameters: {
|
||||
|
||||
Reference in New Issue
Block a user