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
@@ -0,0 +1,72 @@
"""Espaçamento das chamadas ao bridge do Premiere.
As ferramentas de edição do Premiere são round-trips síncronos de QE
DOM/ExtendScript no processo principal do aplicativo. Uma rajada dessas
chamadas sem folga entre elas trava a interface do Premiere — observado na
prática ao aplicar um plano com 17 cortes seguidos de zooms e marcadores.
O script ``scripts/apply-editorial-actions.mjs``, que aplica planos por este
mesmo bridge sem travar, resolve isso com uma pausa fixa entre chamadas;
este módulo traz a mesma folga para o caminho Python.
"""
from __future__ import annotations
import time
from typing import Any, Callable
from .cliente_mcp import ClienteMCP
#: Folga adotada entre chamadas consecutivas, igual à do apply-editorial-actions.mjs
#: (``BRIDGE_DELAY_MS = 120``), que é o valor validado numa aplicação real.
PAUSA_PADRAO_ENTRE_CHAMADAS_EM_SEGUNDOS = 0.12
class ClienteMCPComPausa(ClienteMCP):
"""Envolve outro :class:`ClienteMCP` garantindo uma folga entre chamadas.
Decora um cliente já pronto em vez de espalhar esperas pelas classes de
domínio: assim a folga vale tanto para as leituras
(``get_active_sequence``) quanto para as escritas (``split_clip``,
``ripple_delete``), que é o que de fato ocupa o processo principal do
Premiere, e as classes de edição continuam sem saber que existe um
bridge lento do outro lado.
A pausa só entra *entre* chamadas — a primeira não espera — e vale
também depois de uma chamada que falhou, porque uma chamada recusada
ocupou o bridge do mesmo jeito.
"""
def __init__(
self,
cliente_mcp: ClienteMCP,
pausa_em_segundos: float = PAUSA_PADRAO_ENTRE_CHAMADAS_EM_SEGUNDOS,
esperar: Callable[[float], None] = time.sleep,
) -> None:
"""``esperar`` é injetável para que os testes não durmam de verdade."""
if pausa_em_segundos < 0:
raise ValueError(
f"A pausa entre chamadas não pode ser negativa (recebido: {pausa_em_segundos})."
)
self.cliente_mcp = cliente_mcp
self.pausa_em_segundos = pausa_em_segundos
self.esperar = esperar
self._ja_chamou = False
def conectar(self) -> None:
"""Delega ao cliente interno; conectar não é um round-trip de edição."""
self.cliente_mcp.conectar()
def desconectar(self) -> None:
"""Delega ao cliente interno; desconectar não é um round-trip de edição."""
self.cliente_mcp.desconectar()
def esta_conectado(self) -> bool:
"""Estado de conexão do cliente interno."""
return self.cliente_mcp.esta_conectado()
def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]:
"""Espera a folga devida (menos na primeira chamada) e delega ao cliente interno."""
if self._ja_chamou and self.pausa_em_segundos > 0:
self.esperar(self.pausa_em_segundos)
self._ja_chamou = True
return self.cliente_mcp.chamar(nome_da_ferramenta, argumentos)