feat: Alinhado o aplicador Python de plano de edição (aplicador_de

- Alinhado o aplicador Python de plano de edição (aplicador_de_plano_de_edicao.py) com o script apply-editorial-actions.mjs de referência: no-op de split restrito aos padrões reais do bridge, pausa entre chamadas ao MCP, tolerância de borda de corte usa a posição real em vez de rejeitar desvio; adicionado backup automático da sequência (duplicate_sequence) e documentado o comando python code/engine/aplicar_plano_de_edicao.py como via alternativa ao painel CEP.

Resumo:
- 18 arquivos alterados
- 5 novos
- 13 modificados
- 0 removidos

 13 files changed, 704 insertions(+), 133 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - CLAUDE.md
  - code/engine/aplicar_plano_de_edicao.py
  - code/engine/editor/__init__.py
  - code/engine/editor/aplicador_de_plano_de_edicao.py
  - code/engine/editor/escrita/escrita_no_editor.py
  - code/engine/integracoes/premiere/__init__.py
  - code/engine/integracoes/premiere/cliente_mcp.py
  - code/engine/testes/duplos_de_premiere.py
  - code/engine/testes/test_aplicador_de_plano_de_edicao.py
  - code/scripts/apply-editorial-actions.mjs
  - code/src/tools/discovery.ts
  - code/src/tools/timeline.ts
  - code/engine/editor/backup_de_sequencia.py
  - code/engine/integracoes/premiere/cliente_mcp_com_pausa.py
  - code/engine/testes/test_backup_de_sequencia.py
  - code/engine/testes/test_cliente_mcp_com_pausa.py
  - code/engine/testes/test_escrita_no_editor.py
This commit is contained in:
João Henrique
2026-09-09 09:31:29 -04:00
parent 4308c72441
commit 69e931eb45
18 changed files with 1040 additions and 133 deletions
+68 -8
View File
@@ -11,8 +11,12 @@
* "coordenada pós-corte" (ver adjustTimeForCuts) antes de procurar o clipe.
*
* Limitações conhecidas:
* - "zoom" aplica um scale estático (set_clip_scale) no clipe do intervalo, sem rampa
* de entrada/saída. Suficiente para um punch-in simples; não é um Ken Burns animado.
* - "zoom" cria uma camada de ajuste (adjustment layer) numa track de vídeo dedicada,
* posicionada e recortada para durar exatamente o intervalo da action, com o Scale
* do próprio Motion da camada de ajuste ajustado — não altera o clipe original.
* É um scale estático (sem rampa de entrada/saída); suficiente para um punch-in
* simples, não é um Ken Burns animado. A track de ajuste é criada uma vez (no
* topo da pilha de vídeo) e reaproveitada por todas as actions "zoom" do plano.
* - "text" não tem API de scripting suportada no Premiere para criar clipe de texto
* a partir de string crua (add_text_overlay do MCP já retorna erro por design).
* Em vez de falhar o plano inteiro, cada action "text" vira um marker de sequência
@@ -28,6 +32,9 @@ import { getDiscoveryTools } from "../dist/tools/discovery.js";
import { getProjectTools } from "../dist/tools/project.js";
import { getMarkerTools } from "../dist/tools/markers.js";
import { getTrackTargetingTools } from "../dist/tools/track-targeting.js";
import { getTrackTools } from "../dist/tools/tracks.js";
import { getPlayheadTools } from "../dist/tools/playhead.js";
import { getUtilityTools } from "../dist/tools/utility.js";
// Progresso para a barra do painel CEP: linhas @@PROGRESS em stderr, que o painel
// intercepta em vez de despejar no log. Fases A/B/C viram uma barra única.
@@ -121,6 +128,9 @@ async function main() {
const project = getProjectTools(bridgeOptions);
const markers = getMarkerTools(bridgeOptions);
const trackTargeting = getTrackTargetingTools(bridgeOptions);
const tracks = getTrackTools(bridgeOptions);
const playhead = getPlayheadTools(bridgeOptions);
const utility = getUtilityTools(bridgeOptions);
console.error(
`[apply-editorial-actions] sequência="${args.sequence}" cuts=${cuts.length} outras=${others.length} dryRun=${args.dryRun}`
@@ -236,6 +246,9 @@ async function main() {
reportProgress("Aplicando zooms, textos e marcadores", phaseCBase, `0 de ${others.length} ações`);
let applied = 0;
const warnings = [];
// Criada sob demanda, na primeira action "zoom" do plano — planos sem zoom não
// ganham uma track extra à toa.
let adjustmentTrackIndex = null;
for (const action of others) {
const adjStart = adjustTimeForCuts(action.start, sortedCutsForAdjust);
const adjEnd = adjustTimeForCuts(action.end, sortedCutsForAdjust);
@@ -258,13 +271,60 @@ async function main() {
warnings.push(`zoom @${action.start} sem params.scale numérico — ignorado.`);
continue;
}
const videoClip = unwrap(
await discovery.get_clip_at_position.handler({ time_seconds: mid, track_index: args.videoTrack, track_type: "video" }),
`get_clip_at_position video @${mid}`
);
const duration = adjEnd - adjStart;
if (duration <= 0) {
warnings.push(`zoom @${action.start} com duração inválida (${duration}s) — ignorado.`);
continue;
}
// A camada de ajuste é criada uma vez, no topo da pilha de vídeo, e
// reaproveitada por todas as actions "zoom" — não altera o clipe
// original, então cortes/re-splits feitos na Fase A/B não a afetam.
if (adjustmentTrackIndex === null) {
const trackResult = unwrap(
await tracks.add_track.handler({ track_type: "video", count: 1 }),
"add_track (camada de ajuste para zoom)"
);
adjustmentTrackIndex = trackResult.totalTracks - 1;
await sleep(BRIDGE_DELAY_MS);
}
unwrap(
await trackTargeting.set_clip_scale.handler({ node_id: videoClip.nodeId, scale: scale * 100 }),
`set_clip_scale ${videoClip.nodeId}`
await playhead.set_playhead_position.handler({ time_seconds: adjStart }),
`set_playhead_position @${adjStart}`
);
await sleep(BRIDGE_DELAY_MS);
unwrap(
await utility.add_adjustment_layer.handler({ track_index: adjustmentTrackIndex }),
`add_adjustment_layer @${adjStart}`
);
await sleep(BRIDGE_DELAY_MS);
const adjClip = unwrap(
await discovery.get_clip_at_position.handler({
time_seconds: adjStart + 0.01,
track_index: adjustmentTrackIndex,
track_type: "video",
}),
`get_clip_at_position (camada de ajuste) @${adjStart}`
);
unwrap(
await timeline.trim_clip.handler({ node_id: adjClip.nodeId, new_out_seconds: duration }),
`trim_clip (camada de ajuste) @${adjStart}`
);
await sleep(BRIDGE_DELAY_MS);
unwrap(
await timeline.move_clip.handler({ node_id: adjClip.nodeId, new_start_seconds: adjStart }),
`move_clip (camada de ajuste) @${adjStart}`
);
await sleep(BRIDGE_DELAY_MS);
unwrap(
await trackTargeting.set_clip_scale.handler({ node_id: adjClip.nodeId, scale: scale * 100 }),
`set_clip_scale (camada de ajuste) ${adjClip.nodeId}`
);
} else if (action.kind === "text") {
// Sem API de scripting suportada para clipe de texto a partir de string crua