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:
@@ -1,7 +1,8 @@
|
||||
from .cliente_mcp import ClienteMCP, ClienteMCPPorStdio
|
||||
from .cliente_mcp_com_pausa import ClienteMCPComPausa
|
||||
from .erros_mcp import ErroMCP, ErroDeConexaoMCP, ErroDeRespostaMCP, ErroDeFerramentaMCP
|
||||
|
||||
__all__ = ["ClienteMCP", "ClienteMCPPorStdio", "ErroMCP", "ErroDeConexaoMCP", "ErroDeRespostaMCP", "ErroDeFerramentaMCP"]
|
||||
__all__ = ["ClienteMCP", "ClienteMCPPorStdio", "ClienteMCPComPausa", "ErroMCP", "ErroDeConexaoMCP", "ErroDeRespostaMCP", "ErroDeFerramentaMCP"]
|
||||
from .sessao_mcp import SessaoMCP
|
||||
|
||||
__all__ = ["SessaoMCP"]
|
||||
|
||||
@@ -89,7 +89,28 @@ class ClienteMCPPorStdio(ClienteMCP):
|
||||
resposta = self._enviar("tools/call", {"name": nome_da_ferramenta, "arguments": argumentos or {}})
|
||||
if resposta.get("error"):
|
||||
raise ErroDeFerramentaMCP(f"Erro ao executar {nome_da_ferramenta!r} no MCP: {resposta['error']}")
|
||||
return resposta.get("result", {})
|
||||
resultado = resposta.get("result", {})
|
||||
# Uma ferramenta que falha não devolve erro no nível do JSON-RPC: o
|
||||
# servidor responde "com sucesso" e marca `isError` no resultado. Sem
|
||||
# olhar esse campo, uma escrita recusada pelo Premiere passaria por
|
||||
# bem-sucedida — foi assim que uma chamada a uma ferramenta inexistente
|
||||
# ficou invisível até o efeito aparecer na timeline.
|
||||
if resultado.get("isError"):
|
||||
raise ErroDeFerramentaMCP(
|
||||
f"A ferramenta {nome_da_ferramenta!r} falhou: {self._mensagem_de_erro(resultado)}"
|
||||
)
|
||||
return resultado
|
||||
|
||||
@staticmethod
|
||||
def _mensagem_de_erro(resultado: dict[str, Any]) -> str:
|
||||
"""Extrai a mensagem de erro que o servidor MCP devolveu no resultado."""
|
||||
conteudo_estruturado = resultado.get("structuredContent")
|
||||
if isinstance(conteudo_estruturado, dict) and conteudo_estruturado.get("error"):
|
||||
return str(conteudo_estruturado["error"])
|
||||
for bloco in resultado.get("content", []):
|
||||
if isinstance(bloco, dict) and bloco.get("type") == "text":
|
||||
return str(bloco.get("text", ""))
|
||||
return "sem detalhes"
|
||||
|
||||
def _enviar(self, metodo: str, parametros: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Envia uma requisição JSON-RPC pela stdin do processo e lê a resposta da stdout.
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user