From 69e931eb45f192a82c850ebc9a0cb4403422d23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Henrique?= Date: Wed, 9 Sep 2026 09:31:29 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20Alinhado=20o=20aplicador=20Python=20de?= =?UTF-8?q?=20plano=20de=20edi=C3=A7=C3=A3o=20(aplicador=5Fde?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .jhonny/analises.db | Bin 864256 -> 1617920 bytes CLAUDE.md | 23 ++ code/engine/aplicar_plano_de_edicao.py | 30 ++- code/engine/editor/__init__.py | 2 + .../editor/aplicador_de_plano_de_edicao.py | 196 +++++++++++++++--- code/engine/editor/backup_de_sequencia.py | 34 +++ .../editor/escrita/escrita_no_editor.py | 56 ++++- code/engine/integracoes/premiere/__init__.py | 3 +- .../integracoes/premiere/cliente_mcp.py | 23 +- .../premiere/cliente_mcp_com_pausa.py | 72 +++++++ code/engine/testes/duplos_de_premiere.py | 114 +++++++--- .../test_aplicador_de_plano_de_edicao.py | 133 +++++++++++- .../engine/testes/test_backup_de_sequencia.py | 54 +++++ .../testes/test_cliente_mcp_com_pausa.py | 101 +++++++++ code/engine/testes/test_escrita_no_editor.py | 75 +++++++ code/scripts/apply-editorial-actions.mjs | 76 ++++++- code/src/tools/discovery.ts | 11 +- code/src/tools/timeline.ts | 170 +++++++++++---- 18 files changed, 1040 insertions(+), 133 deletions(-) create mode 100644 code/engine/editor/backup_de_sequencia.py create mode 100644 code/engine/integracoes/premiere/cliente_mcp_com_pausa.py create mode 100644 code/engine/testes/test_backup_de_sequencia.py create mode 100644 code/engine/testes/test_cliente_mcp_com_pausa.py create mode 100644 code/engine/testes/test_escrita_no_editor.py diff --git a/.jhonny/analises.db b/.jhonny/analises.db index 4acc632aa6406919a761cf3e539ce37fc718b047..aa650386acc9e61718d62122afdb9f0061044872 100644 GIT binary patch delta 220528 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�Ff9!G2I75!QIM(STu8JIgRl*sX90L9GN*8q2t0 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 delta 2032 zcmZ{ldsI}%9mnT4bLYPA9hM?eBw^7QZI9xzEQ%3S?8(y_qUYGORwxx&7F2{N9<42) zG1Qng(QfPTP+m6=dwN1DbawZczuG_MH^2M) z{_dUmeeOK!#x~TA`3}yY2101`kb4;zT}ACo2G)XDOW4(Sh)Knut_`-NmQY~V5##6M z@bs{|uZKx<$XYGv%75r59w|&bC!WOeD!R4k%o?$`$ZXO)G^~A2b?FW@N&c1p8~tcG**D9 z-4jtSv&-yZ7PQCho?vp(Oefn7vrX=%N~%+xo0x<7cslqLKk5{@2YB9I(@Qr9_i66> zEZ$2~g}abffoVa({ z9m|2`K-`j7nL=DIf{lVxP~qoGV6HPK0Pp|X#C8{A>x4nbEyR!=-13a^!`=!k8gY6n zq(@gP_&9nTDpAlZ@&vQT(pF9?{YryUqIl&gc|dNI{c@&^=pb#PrPN138j_l&fRrU+ zGE9!(tvCm3a1Gj^l9Z8!(MIqHdJnbZPv)}T%DQ7oww)0Ve$pgmvB%S|EyL=W9SKV21yIZf5>HWm=qHa8)Qwa zl=&F8N9<$vX8RFawXRw1)^pZ!%QA18r_AkUo@pDmj1HsPSYa^zef{V9F8y(Rn)U~+ zORLpZX*1N1)YsI#YQCDHe5`b%Gx#2U2XkegvPN;spUA!P0r{J9`or`ty?{63448y| zXn+#%qAB_cT~Fsp6VfFyI}~lALtcr)l=#;!X#+|P=H<`e|LC$jj`?|f0HO+y(-OFo zS&)RId?t9_!o+l5TA1YHchVq(|0oTLqsxVNOvL;6o>`FL)X#*Azyp)5?piUQNAX4^ST(aUwun)C)DGEO?OfZi zZdfO+ZU2+i*9@2bJN=BlTYplYuKh{-g<$n{?aS())v#Kpu2Peg50%%H=atn;s{D8P zb@@g4S$Q`7C+(v@rt9b&>8^BMdRh9mG&ha>i(DWFNgHLjOE5lh6w!9G85Ktbmd{&p`byoIR^D)13NcF;!Xy^P|&=d>ioRz`NbJkn@ z?9bE&=Sy6DnK<;Q`Z;#Kk_Pi23J~1&aY}cbit7Z3mGBUf+~gDTI@wRwl9_lM_kOX4 zz#g`b+MDdfwqjki+N>&T+5cHXxAoKdPW>@GLHoVdsnrNB$?AvdFV#Kj)9MW6Bf({_ zlCPx5AIshH^YSzD1N1M{p=tZ*YWkq`SE)yOL0W@f#NWWP;8QpU2jE#TuW{6SC@(pJ z!NZ>lY1z?(!#UZZ9R}yYuqABHY3)rK{NQgH&i77e6IcN9;LPoj&KVUbup_mq_ohnh zX7m(#71;qh%f{BQb;K&O7Fxs{F^|FuVCXh_72m|C@OGR>PLgfdCd-K>U6)S4E$D!1 zv)o){O2!rAnDm^qOfrp4Mz%r84RwjCDp&P)^%i}zzL>8Yky4U%SsT?_wJq8snxg(z z4XG9PY4RhLt{i^7&J6IJ7t9jp#2#}a=GQJVAJ5%qPCCQyFtHV`^fNE7c$?kht#1is zegV8P1$XAE7bUwW;lGLI3IUi~pm8k1CC0>{F)GS&Wvh~-X!_z97x`M6+$LAbx%UCdI(?6})2aw4x|ABy z+6Q>3k1gWGwXP<q2=dTQmzTHhbQSZm zT_V-LK7Nq*7sd~8Z;h*glU=R=A6X|JeFefad?!A@KbQ}h9BRdDgCYf^i;;+jU)}C1 u<*FDCqh5Yxw~&wS7GEg+ff##Whz-)n1>)zS0XCoa53@{+&YnDIw*D736>0te diff --git a/CLAUDE.md b/CLAUDE.md index 077c4c8..631cec9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,6 +69,29 @@ When working on this project, use these skills to: 2. **Design modules**: Use `/codebase-design` when creating new modules 3. **Model domain**: Use `/domain-modeling` to build shared vocabulary +## Aplicar um plano de edição sem o painel CEP + +O painel CEP (aba "Editar Vídeo", botão "Aplicar plano na timeline") chama +`code/scripts/apply-editorial-actions.mjs` — esse é o caminho de produção, +comprovado, e continua sendo o padrão. + +Existe uma segunda via em Python, para quando o painel não estiver aberto: + +```bash +python code/engine/aplicar_plano_de_edicao.py "/caminho/plano_de_acoes.json" +``` + +Pré-requisito: o Premiere aberto, com a sequência correta **ativa** (o +script nunca troca de sequência — opera sobre a que estiver ativa no +momento), e o bridge MCP (`dist/index.js`) disponível. Cria backup +automático da sequência (`duplicate_sequence`) antes de aplicar qualquer +corte, igual ao painel — se o backup falhar, avisa em stderr e segue +aplicando mesmo assim, sem bloquear. + +Ver [code/engine/editor/aplicador_de_plano_de_edicao.py](code/engine/editor/aplicador_de_plano_de_edicao.py) +para as diferenças deliberadas entre essa implementação e o `.mjs` do +painel (documentadas no docstring da classe `AplicadorDePlanoDeEdicao`). + ## Code Standards - Python in `code/engine/`: follow `.agents/skills/boas-praticas-oo/SKILL.md` and [CODING_STANDARDS.md](CODING_STANDARDS.md) — mandatory, not a suggestion diff --git a/code/engine/aplicar_plano_de_edicao.py b/code/engine/aplicar_plano_de_edicao.py index 6c0f38b..875de4e 100644 --- a/code/engine/aplicar_plano_de_edicao.py +++ b/code/engine/aplicar_plano_de_edicao.py @@ -25,8 +25,15 @@ CAMINHO_DO_CODIGO = Path(__file__).resolve().parent.parent if str(CAMINHO_DO_CODIGO) not in sys.path: sys.path.insert(0, str(CAMINHO_DO_CODIGO)) -from engine.editor import AplicadorDePlanoDeEdicao, EscritaNoEditor, LeitorDePlanoDeEdicao, MapeadorDeTempoDeOrigemParaTimeline +from engine.editor import ( + AplicadorDePlanoDeEdicao, + BackupDaSequencia, + EscritaNoEditor, + LeitorDePlanoDeEdicao, + MapeadorDeTempoDeOrigemParaTimeline, +) from engine.integracoes.premiere.cliente_mcp import ClienteMCPPorStdio +from engine.integracoes.premiere.cliente_mcp_com_pausa import ClienteMCPComPausa from engine.integracoes.premiere.conversores import ConversorDeTimeline from engine.integracoes.premiere.leitura import AcessoAoEditor, AcessoATimeline from engine.integracoes.premiere.sessao_mcp import SessaoMCP @@ -42,12 +49,29 @@ def executar(caminho_do_plano: Path) -> dict: """ plano = LeitorDePlanoDeEdicao().ler(caminho_do_plano) - cliente = ClienteMCPPorStdio(["node", str(CAMINHO_DO_SERVIDOR_MCP)]) + # A pausa é aplicada no cliente, e não dentro do aplicador, para valer + # igualmente nas leituras da timeline e nas escritas — são todas + # round-trips síncronos no processo principal do Premiere. + cliente = ClienteMCPComPausa(ClienteMCPPorStdio(["node", str(CAMINHO_DO_SERVIDOR_MCP)])) with SessaoMCP(cliente): acesso_ao_editor = AcessoAoEditor(AcessoATimeline(cliente)) + conversor_de_timeline = ConversorDeTimeline() + + # Backup antes de qualquer corte, espelhando o painel CEP + # (applyEditorialActions() em cep-plugin/main.js): sem undo por + # script nesta versão do Premiere, é a única forma de recuperar o + # estado anterior se algo sair errado. Uma falha aqui não impede a + # aplicação — só fica registrada em stderr, igual ao painel. + sequencia_ativa = conversor_de_timeline.converter(acesso_ao_editor.obter_timeline_ativa()) + backup_ok = BackupDaSequencia(cliente).criar_backup(sequencia_ativa.identificador) + print( + "Backup da sequência criado." if backup_ok else "Não foi possível criar o backup automático; aplicando mesmo assim.", + file=sys.stderr, + ) + aplicador = AplicadorDePlanoDeEdicao( acesso_ao_editor=acesso_ao_editor, - conversor_de_timeline=ConversorDeTimeline(), + conversor_de_timeline=conversor_de_timeline, escrita=EscritaNoEditor(cliente), mapeador=MapeadorDeTempoDeOrigemParaTimeline(), ) diff --git a/code/engine/editor/__init__.py b/code/engine/editor/__init__.py index c3780b8..c7d689a 100644 --- a/code/engine/editor/__init__.py +++ b/code/engine/editor/__init__.py @@ -12,6 +12,7 @@ não deu. """ from .aplicador_de_plano_de_edicao import AplicadorDePlanoDeEdicao, ResultadoDaAcao, ResultadoDaAplicacao +from .backup_de_sequencia import BackupDaSequencia from .erros import ErroDeEdicao, ErroDeEscritaNoEditor, ErroDeMapeamentoDeTempo, ErroDePlanoInvalido from .escrita import EscritaNoEditor from .leitura import LeitorDePlanoDeEdicao @@ -21,6 +22,7 @@ from .modelos import AcaoDeEdicao, PlanoDeEdicao, TipoDeAcao __all__ = [ "AcaoDeEdicao", "AplicadorDePlanoDeEdicao", + "BackupDaSequencia", "EscritaNoEditor", "ErroDeEdicao", "ErroDeEscritaNoEditor", diff --git a/code/engine/editor/aplicador_de_plano_de_edicao.py b/code/engine/editor/aplicador_de_plano_de_edicao.py index 619302f..2b4214e 100644 --- a/code/engine/editor/aplicador_de_plano_de_edicao.py +++ b/code/engine/editor/aplicador_de_plano_de_edicao.py @@ -19,6 +19,8 @@ from .mapeamento.mapeador_de_tempo import MapeadorDeTempoDeOrigemParaTimeline from .modelos import AcaoDeEdicao, PlanoDeEdicao, TipoDeAcao _TOLERANCIA_DE_BORDA_EM_SEGUNDOS = 0.05 +_TOLERANCIA_DE_DURACAO_EM_SEGUNDOS = 0.3 +_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS = 3.0 @dataclass(frozen=True) @@ -50,21 +52,44 @@ class ResultadoDaAplicacao: class AplicadorDePlanoDeEdicao: """Aplica um :class:`PlanoDeEdicao` inteiro na sequência ativa do Premiere. - Ordem de aplicação, para preservar a validade das posições calculadas: + Ordem de aplicação: - 1. Marcadores e zooms primeiro — não mudam a duração da timeline, então - a posição calculada para cada um continua válida até o fim. - 2. Cortes por último, do fim para o começo (maior início primeiro) — - cada corte fecha o espaço que ocupava (ripple), deslocando para a - esquerda tudo que vem depois dele. Processar do fim para o começo - garante que a posição de um corte ainda não aplicado nunca é afetada - pelos cortes já aplicados. + 1. Cortes primeiro, do fim para o começo (maior início primeiro) — cada + corte fecha o espaço que ocupava (ripple), deslocando para a esquerda + tudo que vem depois dele. Processar do fim para o começo garante que + a posição de um corte ainda não aplicado nunca é afetada pelos + cortes já aplicados. + 2. Marcadores e zooms por último, depois que a timeline já está no + formato final. Zoom (``set_clip_properties``) muda uma propriedade do + *clipe inteiro*, não de um trecho dele — se dois zooms caíssem em + instantes diferentes do mesmo clipe ainda não dividido pelos cortes, + o segundo sobrescreveria o primeiro no clipe inteiro, e a divisão + posterior faria os dois pedaços herdarem só o último valor. Cortar + primeiro garante que cada zoom já encontra um clipe próprio. A cada ação a timeline ativa é lida de novo no Premiere — nunca se reaproveita uma leitura anterior — porque a ação anterior pode ter - mudado a posição dos clipes. Uma ação que falha é registrada no - resultado e não interrompe as demais; quem chama decide o que fazer com - as falhas. + mudado a posição ou a divisão dos clipes. Uma ação que falha é + registrada no resultado e não interrompe as demais; quem chama decide o + que fazer com as falhas. + + Diferença deliberada do outro aplicador de plano do projeto + (``scripts/apply-editorial-actions.mjs``, chamado pelo painel CEP): + aquele script separa "Fase A" (todos os splits do plano inteiro, + coordenadas originais) de "Fase B" (todas as remoções, feitas depois), + porque só sabe fazer aritmética pura sobre os cortes pedidos — não relê + o estado do Premiere no meio do caminho. Esta classe faz split e + remoção corte a corte, relendo a faixa a cada passo + (:meth:`_clipes_do_arquivo` exige ``intervalo_na_origem`` em todo + clipe), porque isso permite achar cada clipe pelo tempo de *origem* + real reportado pelo Premiere em vez de confiar só em aritmética — mais + robusto quando o bridge relata ``inPoint``/``outPoint`` de cada clipe, + mas por isso mesmo falha alto (em vez de simplesmente pular a faixa) + quando essa garantia falta numa faixa (ver + :meth:`_clipes_do_arquivo` e o teste + ``test_corte_falha_em_vez_de_ignorar_faixa_sem_intervalo_de_origem``, + que comprova que um corte nessas condições não deixa vídeo cortado e + áudio intacto). """ def __init__( @@ -84,8 +109,8 @@ class AplicadorDePlanoDeEdicao: pontuais = [acao for acao in plano.acoes if acao.tipo is not TipoDeAcao.CORTE] cortes = sorted(plano.acoes_do_tipo(TipoDeAcao.CORTE), key=lambda acao: acao.inicio, reverse=True) - resultados = [self._aplicar_acao_pontual(acao, plano.arquivo_de_origem) for acao in pontuais] - resultados += [self._aplicar_corte(acao, plano.arquivo_de_origem) for acao in cortes] + resultados = [self._aplicar_corte(acao, plano.arquivo_de_origem) for acao in cortes] + resultados += [self._aplicar_acao_pontual(acao, plano.arquivo_de_origem) for acao in pontuais] return ResultadoDaAplicacao(tuple(resultados)) def _timeline_atual(self) -> Timeline: @@ -125,7 +150,7 @@ class AplicadorDePlanoDeEdicao: def _aplicar_zoom(self, acao: AcaoDeEdicao, arquivo_de_origem: str) -> int: fator_de_escala = float(acao.parametros.get("scale", 1.3)) faixa_de_video = self._faixa_de_video(self._timeline_atual()) - clipes = self.mapeador.clipes_do_arquivo(faixa_de_video, arquivo_de_origem) + clipes = self._clipes_do_arquivo(faixa_de_video, arquivo_de_origem) alvos = [clipe for clipe in clipes if self._sobrepoe(clipe, acao.inicio, acao.fim)] if not alvos: raise ErroDeMapeamentoDeTempo( @@ -136,10 +161,22 @@ class AplicadorDePlanoDeEdicao: return len(alvos) def _aplicar_corte(self, acao: AcaoDeEdicao, arquivo_de_origem: str) -> ResultadoDaAcao: - """Remove, em cada faixa afetada, todos os clipes contidos no intervalo do corte.""" + """Remove, em cada faixa afetada, todos os clipes contidos no intervalo do corte. + + Antes de mudar qualquer faixa, valida que todas as faixas com clipes + de ``arquivo_de_origem`` têm intervalo de origem conhecido. Sem essa + validação prévia, uma faixa problemática (ex.: áudio sem + ``inPoint``/``outPoint``) só falharia depois de outra faixa já ter + sido cortada — deixando vídeo e áudio dessincronizados mesmo com o + corte "reportado" como falho. + """ try: + faixas = self._timeline_atual().faixas + for faixa in faixas: + self._clipes_do_arquivo(faixa, arquivo_de_origem) + trechos_removidos = 0 - for faixa in self._timeline_atual().faixas: + for faixa in faixas: trechos_removidos += self._cortar_faixa(faixa, acao, arquivo_de_origem) if trechos_removidos == 0: raise ErroDeMapeamentoDeTempo( @@ -151,44 +188,104 @@ class AplicadorDePlanoDeEdicao: def _cortar_faixa(self, faixa: Faixa, acao: AcaoDeEdicao, arquivo_de_origem: str) -> int: """Corta o trecho de ``acao`` em uma única faixa e devolve quantos clipes foram removidos.""" - clipes = self.mapeador.clipes_do_arquivo(faixa, arquivo_de_origem) + clipes = self._clipes_do_arquivo(faixa, arquivo_de_origem) afetados = [clipe for clipe in clipes if self._sobrepoe(clipe, acao.inicio, acao.fim)] if not afetados: return 0 primeiro, ultimo = afetados[0], afetados[-1] - inicio_na_timeline = self._borda_de_entrada(primeiro, acao.inicio) - fim_na_timeline = self._borda_de_saida(ultimo, acao.fim) + inicio_pedido = self._borda_de_entrada(primeiro, acao.inicio) + fim_pedido = self._borda_de_saida(ultimo, acao.fim) - self.escrita.dividir_clipe_se_necessario(inicio_na_timeline, faixa.indice, faixa.tipo) - self.escrita.dividir_clipe_se_necessario(fim_na_timeline, faixa.indice, faixa.tipo) + inicio_real = self._dividir_e_conferir(inicio_pedido, faixa, arquivo_de_origem) + fim_real = self._dividir_e_conferir(fim_pedido, faixa, arquivo_de_origem) + + clipes_apos_a_divisao = self._clipes_do_arquivo(self._faixa_atualizada(faixa), arquivo_de_origem) - clipes_apos_a_divisao = self.mapeador.clipes_do_arquivo( - next(f for f in self._timeline_atual().faixas if f.identificador == faixa.identificador), - arquivo_de_origem, - ) alvo = [ clipe for clipe in clipes_apos_a_divisao - if inicio_na_timeline - _TOLERANCIA_DE_BORDA_EM_SEGUNDOS <= clipe.intervalo_na_timeline.inicio - and clipe.intervalo_na_timeline.fim <= fim_na_timeline + _TOLERANCIA_DE_BORDA_EM_SEGUNDOS + if inicio_real - _TOLERANCIA_DE_BORDA_EM_SEGUNDOS <= clipe.intervalo_na_timeline.inicio + and clipe.intervalo_na_timeline.fim <= fim_real + _TOLERANCIA_DE_BORDA_EM_SEGUNDOS ] duracao_removida = sum(clipe.intervalo_na_timeline.duracao for clipe in alvo) - duracao_esperada = fim_na_timeline - inicio_na_timeline - if abs(duracao_removida - duracao_esperada) > _TOLERANCIA_DE_BORDA_EM_SEGUNDOS: + # A duração esperada é a sobreposição real entre o intervalo da ação + # e os clipes afetados, não `acao.duracao` — se a ação começar ou + # terminar fora de qualquer clipe existente (ex.: pede corte a partir + # de um instante anterior ao início do material disponível), só a + # parte que realmente existe como filmagem deve ser contada. + duracao_esperada_pela_origem = sum( + self._sobreposicao_em_segundos(clipe, acao.inicio, acao.fim) for clipe in afetados + ) + if abs(duracao_removida - duracao_esperada_pela_origem) > _TOLERANCIA_DE_DURACAO_EM_SEGUNDOS: raise ErroDeMapeamentoDeTempo( - f"Na faixa {faixa.identificador!r}, os cortes em {inicio_na_timeline:.3f}s e " - f"{fim_na_timeline:.3f}s não isolaram o trecho esperado (esperado {duracao_esperada:.3f}s, " - f"encontrado {duracao_removida:.3f}s cobertos por clipe); nada foi removido para evitar " - "apagar o trecho errado." + f"Na faixa {faixa.identificador!r}, os cortes pedidos em {inicio_pedido:.3f}s e " + f"{fim_pedido:.3f}s (o Premiere criou as bordas em {inicio_real:.3f}s e {fim_real:.3f}s) " + f"não isolaram o trecho esperado (esperado {duracao_esperada_pela_origem:.3f}s pela duração " + f"de origem, encontrado {duracao_removida:.3f}s cobertos por clipe); nada foi removido para " + "evitar apagar o trecho errado." ) for clipe in alvo: self.escrita.remover_trecho(clipe.identificador) return len(alvo) + def _dividir_e_conferir(self, instante_pedido: float, faixa: Faixa, arquivo_de_origem: str) -> float: + """Pede o corte em ``instante_pedido`` e devolve a borda que o Premiere realmente criou. + + Corta **uma vez só** — nunca tenta de novo no mesmo ponto: uma + segunda tentativa deixaria dois cortes reais na timeline (o + "errado" e o corrigido), com uma fatia inútil entre eles, e o + Premiere não expõe undo por script para desfazer o primeiro + (``app.project.undo`` não existe nesta versão). + + O desvio entre o instante pedido e a borda criada não é comparado + contra um limite fixo aqui: o ``split_clip`` do bridge do Premiere + desvia alguns décimos de segundo do pedido, e esse desvio cresce + com o tempo absoluto (ver :meth:`_borda_realmente_criada`) — é + comportamento normal do bridge, não um bug a rejeitar. A única + checagem de sanidade é o raio de busca de + :meth:`_borda_realmente_criada` + (:data:`_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS`): além dele, não é + drift, é falha real, e essa função já levanta + :class:`ErroDeMapeamentoDeTempo` nesse caso. + """ + self.escrita.dividir_clipe_se_necessario(instante_pedido, faixa.indice, faixa.tipo) + clipes = self._clipes_do_arquivo(self._faixa_atualizada(faixa), arquivo_de_origem) + return self._borda_realmente_criada(clipes, instante_pedido) + + def _faixa_atualizada(self, faixa: Faixa) -> Faixa: + """Relê a timeline e devolve a versão atual da mesma faixa, pelo identificador.""" + return next(f for f in self._timeline_atual().faixas if f.identificador == faixa.identificador) + + def _borda_realmente_criada(self, clipes: list[Clipe], instante_pedido: float) -> float: + """Acha, entre as bordas de clipe existentes, a mais próxima do instante pedido a Premiere. + + O ``split_clip`` do bridge do Premiere pode cortar alguns décimos de + segundo antes ou depois do instante pedido (drift observado: cresce + com o tempo absoluto, compatível com uma conversão segundos↔ticks + que não usa a taxa de quadros real da sequência). As durações + relativas continuam corretas — só a posição absoluta desvia — por + isso a seleção do trecho a remover deve usar a borda que o Premiere + de fato criou, não o valor pedido. + + Levanta :class:`ErroDeMapeamentoDeTempo` se nenhuma borda estiver + dentro de :data:`_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS` do instante + pedido — nesse caso o corte realmente falhou, não é só drift. + """ + bordas = {clipe.intervalo_na_timeline.inicio for clipe in clipes} | { + clipe.intervalo_na_timeline.fim for clipe in clipes + } + borda_mais_proxima = min(bordas, key=lambda borda: abs(borda - instante_pedido), default=None) + if borda_mais_proxima is None or abs(borda_mais_proxima - instante_pedido) > _RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS: + raise ErroDeMapeamentoDeTempo( + f"Nenhuma borda de clipe foi encontrada perto de {instante_pedido:.3f}s " + f"(raio de busca: {_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS}s); o corte não foi aplicado." + ) + return borda_mais_proxima + def _instante_na_timeline(self, instante_de_origem: float, arquivo_de_origem: str) -> float: """Traduz um instante de origem usando a primeira faixa de vídeo do arquivo.""" faixa_de_video = self._faixa_de_video(self._timeline_atual()) - clipes = self.mapeador.clipes_do_arquivo(faixa_de_video, arquivo_de_origem) + clipes = self._clipes_do_arquivo(faixa_de_video, arquivo_de_origem) clipe = self.mapeador.localizar_clipe_no_instante(clipes, instante_de_origem) if clipe is None: raise ErroDeMapeamentoDeTempo( @@ -196,6 +293,29 @@ class AplicadorDePlanoDeEdicao: ) return self.mapeador.instante_na_timeline(clipe, instante_de_origem) + def _clipes_do_arquivo(self, faixa: Faixa, arquivo_de_origem: str) -> list[Clipe]: + """Clipes de ``arquivo_de_origem`` em ``faixa``, exigindo intervalo de origem conhecido em todos. + + Levanta :class:`ErroDeMapeamentoDeTempo` se algum clipe com esse nome + não tiver intervalo de origem — por exemplo, por uma lacuna na + ferramenta MCP que não reporta ``inPoint``/``outPoint`` para uma + faixa de áudio. Sem isso, o corte nessa faixa seria silenciosamente + ignorado (nenhum clipe "afetado" seria encontrado) em vez de avisar + que a faixa não pôde ser avaliada — foi exatamente essa lacuna que + deixou uma faixa de áudio inteira sem ser cortada numa aplicação + real deste plano. + """ + alvo = arquivo_de_origem.strip().lower() + nomeados = [clipe for clipe in faixa.clipes if (clipe.nome or "").strip().lower() == alvo] + sem_origem = [clipe for clipe in nomeados if clipe.intervalo_na_origem is None] + if sem_origem: + raise ErroDeMapeamentoDeTempo( + f"A faixa {faixa.identificador!r} tem {len(sem_origem)} clipe(s) de {arquivo_de_origem!r} " + "sem intervalo de origem reportado pelo Premiere; não é seguro decidir o que cortar, " + "mover ou marcar nessa faixa sem saber a que trecho de origem cada clipe corresponde." + ) + return sorted(nomeados, key=lambda clipe: clipe.intervalo_na_origem.inicio) + def _borda_de_entrada(self, clipe: Clipe, inicio_de_origem: float) -> float: """Posição de entrada do corte na timeline: a borda do clipe se o corte começa antes dele.""" if inicio_de_origem <= clipe.intervalo_na_origem.inicio: @@ -214,6 +334,14 @@ class AplicadorDePlanoDeEdicao: intervalo = clipe.intervalo_na_origem return intervalo is not None and intervalo.inicio < fim and inicio < intervalo.fim + @staticmethod + def _sobreposicao_em_segundos(clipe: Clipe, inicio: float, fim: float) -> float: + """Quantos segundos do intervalo de origem do clipe caem dentro de [inicio, fim].""" + intervalo = clipe.intervalo_na_origem + if intervalo is None: + return 0.0 + return max(0.0, min(intervalo.fim, fim) - max(intervalo.inicio, inicio)) + @staticmethod def _faixa_de_video(timeline: Timeline) -> Faixa: """A primeira faixa de vídeo da timeline; é onde marcadores e zooms são resolvidos.""" diff --git a/code/engine/editor/backup_de_sequencia.py b/code/engine/editor/backup_de_sequencia.py new file mode 100644 index 0000000..114a8e8 --- /dev/null +++ b/code/engine/editor/backup_de_sequencia.py @@ -0,0 +1,34 @@ +"""Backup automático da sequência antes de aplicar um plano de edição.""" + +from __future__ import annotations + +from ..integracoes.premiere.cliente_mcp import ClienteMCP +from ..integracoes.premiere.erros_mcp import ErroDeFerramentaMCP + + +class BackupDaSequencia: + """Duplica a sequência ativa do Premiere antes de uma edição destrutiva. + + Espelha o passo de segurança do painel CEP + (``app.project.activeSequence.clone()`` em ``applyEditorialActions()``, + ``cep-plugin/main.js``): sem undo por script nesta versão do Premiere, + duplicar a sequência antes de cortar é a única forma de recuperar o + estado anterior se algo sair errado. + """ + + def __init__(self, cliente_mcp: ClienteMCP) -> None: + self.cliente_mcp = cliente_mcp + + def criar_backup(self, identificador_da_sequencia: str) -> bool: + """Duplica a sequência. Devolve ``True`` se conseguiu, ``False`` se o Premiere recusou. + + Não levanta exceção numa recusa: o painel CEP também segue aplicando + o plano mesmo se o backup falhar (loga e avisa, não bloqueia) — a + falta de backup não deve impedir a edição que o usuário pediu, só + precisa ficar visível para quem chama decidir o que fazer. + """ + try: + self.cliente_mcp.chamar("duplicate_sequence", {"sequence_id": identificador_da_sequencia}) + return True + except ErroDeFerramentaMCP: + return False diff --git a/code/engine/editor/escrita/escrita_no_editor.py b/code/engine/editor/escrita/escrita_no_editor.py index fd39238..656048b 100644 --- a/code/engine/editor/escrita/escrita_no_editor.py +++ b/code/engine/editor/escrita/escrita_no_editor.py @@ -9,10 +9,33 @@ nomes de ferramenta ou ticks do Premiere. from __future__ import annotations +import re + from ...integracoes.premiere.cliente_mcp import ClienteMCP from ...integracoes.premiere.erros_mcp import ErroDeFerramentaMCP from ..erros import ErroDeEscritaNoEditor +# O bridge recusa a divisão de duas formas diferentes quando o ponto pedido +# JÁ é uma borda de clipe. Ambas são no-op, não falha: +# 1. nenhum clipe atravessa estritamente o instante ("strictly spans"); +# 2. o razor roda mas a contagem de clipes da faixa não muda, porque o ponto +# coincide por arredondamento de quadro com uma borda existente. +# Os mesmos dois padrões são reconhecidos por scripts/apply-editorial-actions.mjs, +# que aplica planos por este mesmo bridge. +_RECUSA_POR_NENHUM_CLIPE_ATRAVESSANDO = re.compile(r"strictly spans", re.IGNORECASE) +_RECUSA_COM_CONTAGEM_DE_CLIPES = re.compile( + r"changed the track clip count from (\d+) to (\d+)", re.IGNORECASE +) + + +def _e_divisao_ja_existente(mensagem_de_erro: str) -> bool: + """``True`` se a recusa do bridge significa "esse ponto já era uma borda".""" + if _RECUSA_POR_NENHUM_CLIPE_ATRAVESSANDO.search(mensagem_de_erro): + return True + contagem = _RECUSA_COM_CONTAGEM_DE_CLIPES.search(mensagem_de_erro) + return contagem is not None and contagem.group(1) == contagem.group(2) + + _CORES_POR_NOME = { "revisar": 3, # laranja: chama atenção sem parecer um erro (vermelho) "informar": 5, # branco: marcador neutro @@ -29,10 +52,17 @@ class EscritaNoEditor: """Corta, na timeline, o clipe que cobrir ``instante_da_timeline``. Devolve ``True`` se um corte novo foi criado e ``False`` quando o - instante já era uma borda de clipe (o Premiere recusa dividir onde - não há clipe atravessando o ponto) — esse segundo caso não é um erro, + instante já era uma borda de clipe — esse segundo caso não é um erro, é o resultado esperado quando o corte pedido coincide com um corte já - existente da análise de cenas. + existente da análise de cenas, ou quando um plano parcialmente + aplicado é reexecutado. + + Só as recusas reconhecidas por :func:`_e_divisao_ja_existente` contam + como no-op. Qualquer outra recusa vira + :class:`ErroDeEscritaNoEditor`: antes, um ``except`` largo tratava + toda :class:`ErroDeFerramentaMCP` como "já era borda", então uma + faixa bloqueada ou uma queda de bridge passava por corte + bem-sucedido e o trecho seguia inteiro na timeline, sem aviso. """ try: self.cliente_mcp.chamar( @@ -40,17 +70,29 @@ class EscritaNoEditor: {"time_seconds": instante_da_timeline, "track_index": indice_da_faixa, "track_type": tipo_da_faixa}, ) return True - except ErroDeFerramentaMCP: - return False + except ErroDeFerramentaMCP as erro: + if _e_divisao_ja_existente(str(erro)): + return False + raise ErroDeEscritaNoEditor( + f"Não foi possível dividir a faixa {tipo_da_faixa}_{indice_da_faixa} " + f"em {instante_da_timeline:.3f}s: {erro}" + ) from erro def remover_trecho(self, identificador_do_clipe: str) -> None: - """Remove o clipe da timeline, fechando o espaço deixado (ripple). + """Remove o clipe da timeline fechando o espaço que ele ocupava. + + Usa ``ripple_delete`` (QE ``rippleDelete``), não + ``remove_from_timeline``: numa sequência real com outras faixas + ocupadas, o ``remove_from_timeline`` com ``ripple`` apagou o clipe + mas deixou o buraco aberto, e um buraco no meio do corte é um corte + errado. O ``ripple_delete`` confere, do lado do Premiere, que o + clipe realmente sumiu. Levanta :class:`ErroDeEscritaNoEditor` se o Premiere recusar a remoção. """ try: - self.cliente_mcp.chamar("remove_from_timeline", {"node_id": identificador_do_clipe, "ripple": True}) + self.cliente_mcp.chamar("ripple_delete", {"node_id": identificador_do_clipe}) except ErroDeFerramentaMCP as erro: raise ErroDeEscritaNoEditor(f"Não foi possível remover o clipe {identificador_do_clipe!r}: {erro}") from erro diff --git a/code/engine/integracoes/premiere/__init__.py b/code/engine/integracoes/premiere/__init__.py index 72b4cff..24291eb 100644 --- a/code/engine/integracoes/premiere/__init__.py +++ b/code/engine/integracoes/premiere/__init__.py @@ -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"] diff --git a/code/engine/integracoes/premiere/cliente_mcp.py b/code/engine/integracoes/premiere/cliente_mcp.py index dac4b86..4b43941 100644 --- a/code/engine/integracoes/premiere/cliente_mcp.py +++ b/code/engine/integracoes/premiere/cliente_mcp.py @@ -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. diff --git a/code/engine/integracoes/premiere/cliente_mcp_com_pausa.py b/code/engine/integracoes/premiere/cliente_mcp_com_pausa.py new file mode 100644 index 0000000..9f67746 --- /dev/null +++ b/code/engine/integracoes/premiere/cliente_mcp_com_pausa.py @@ -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) diff --git a/code/engine/testes/duplos_de_premiere.py b/code/engine/testes/duplos_de_premiere.py index 62a9a1a..990ac0f 100644 --- a/code/engine/testes/duplos_de_premiere.py +++ b/code/engine/testes/duplos_de_premiere.py @@ -5,11 +5,12 @@ têm posição na timeline diferente da posição no arquivo de origem — o mes descompasso encontrado no projeto real entre ``inicio_na_timeline`` e ``inicio_na_origem`` do banco de análises. Implementa só as ferramentas MCP que o módulo ``engine.editor`` usa: ``get_active_sequence``, ``split_clip``, -``remove_from_timeline``, ``set_clip_properties`` e ``add_marker``. +``ripple_delete``, ``set_clip_properties`` e ``add_marker``. """ from __future__ import annotations +import copy from dataclasses import dataclass from typing import Any @@ -40,10 +41,31 @@ class ClienteMCPFalso(ClienteMCP): _ARQUIVO = "0E6A8290.mp4" - def __init__(self) -> None: + def __init__( + self, + sem_origem_no_audio: bool = False, + clipe_unico: bool = False, + desvio_do_split: float = 0.0, + ) -> None: + """``sem_origem_no_audio`` reproduz o bug real do bridge: relatar clipes de + áudio sem ``inPoint``/``outPoint`` na resposta de ``get_active_sequence``. + + ``clipe_unico`` reproduz a gravação "sem corte": um só clipe cobrindo + todo o arquivo, com origem e timeline idênticas — o cenário em que + dois zooms cairiam no mesmo clipe se rodassem antes dos cortes. + + ``desvio_do_split`` reproduz o drift real do ``split_clip``: em vez + de cortar exatamente no instante pedido, corta em + ``instante + desvio_do_split``.""" + self.sem_origem_no_audio = sem_origem_no_audio + self.desvio_do_split = desvio_do_split self.chamadas: list[tuple[str, dict[str, Any]]] = [] self._proximo_id = 0 + self._historico: list[dict[str, list[_ClipeFalso]]] = [] self._faixas: dict[str, list[_ClipeFalso]] = { + "video_0": [self._novo_clipe(self._ARQUIVO, 0.0, 200.0, 0.0, 200.0, "clipe_unico_video")], + "audio_0": [self._novo_clipe(self._ARQUIVO, 0.0, 200.0, 0.0, 200.0, "clipe_unico_audio")], + } if clipe_unico else { "video_0": [ self._novo_clipe(self._ARQUIVO, 0.0, 38.3, 2.3, 40.6, "clipe_a_video"), self._novo_clipe(self._ARQUIVO, 58.1, 66.7, 86.2, 94.8, "clipe_b_video"), @@ -56,8 +78,8 @@ class ClienteMCPFalso(ClienteMCP): @property def chamadas_de_remocao(self) -> list[dict[str, Any]]: - """Argumentos de cada chamada a ``remove_from_timeline``, na ordem em que ocorreram.""" - return [argumentos for nome, argumentos in self.chamadas if nome == "remove_from_timeline"] + """Argumentos de cada chamada a ``ripple_delete``, na ordem em que ocorreram.""" + return [argumentos for nome, argumentos in self.chamadas if nome == "ripple_delete"] def conectar(self) -> None: """Não há conexão real; existe só para satisfazer a interface de :class:`ClienteMCP`.""" @@ -76,9 +98,10 @@ class ClienteMCPFalso(ClienteMCP): despachantes = { "get_active_sequence": self._simular_get_active_sequence, "split_clip": self._simular_split_clip, - "remove_from_timeline": self._simular_remove_from_timeline, + "ripple_delete": self._simular_ripple_delete, "set_clip_properties": self._simular_set_clip_properties, "add_marker": self._simular_add_marker, + "undo": self._simular_undo, } simulador = despachantes.get(nome_da_ferramenta) if simulador is None: @@ -96,22 +119,23 @@ class ClienteMCPFalso(ClienteMCP): """Devolve a sequência atual no formato que :class:`ConversorDeTimeline` espera.""" del argumentos + def _clipe_como_dict(clipe: _ClipeFalso, tipo: str) -> dict[str, Any]: + base = { + "nodeId": clipe.identificador, + "name": clipe.nome, + "start": clipe.inicio_na_timeline, + "end": clipe.fim_na_timeline, + } + if tipo == "audio" and self.sem_origem_no_audio: + return base + return {**base, "inPoint": clipe.inicio_na_origem, "outPoint": clipe.fim_na_origem} + def _faixa_como_dict(identificador: str, tipo: str, indice: int) -> dict[str, Any]: return { "id": identificador, "name": identificador, "index": indice, - "clips": [ - { - "nodeId": clipe.identificador, - "name": clipe.nome, - "start": clipe.inicio_na_timeline, - "end": clipe.fim_na_timeline, - "inPoint": clipe.inicio_na_origem, - "outPoint": clipe.fim_na_origem, - } - for clipe in self._faixas[identificador] - ], + "clips": [_clipe_como_dict(clipe, tipo) for clipe in self._faixas[identificador]], } dados = { @@ -127,10 +151,31 @@ class ClienteMCPFalso(ClienteMCP): return {"structuredContent": {"data": dados}} def _simular_split_clip(self, argumentos: dict[str, Any]) -> dict[str, Any]: - """Divide, na faixa pedida, o clipe que cobrir estritamente o instante informado.""" + """Divide, na faixa pedida, o clipe que cobrir estritamente o instante informado. + + Aplica ``self.desvio_do_split`` ao ponto de corte, reproduzindo o + drift real do bridge — o corte pode sair do instante pedido. + + A recusa usa a mensagem em inglês do bridge real ("No clip strictly + spans ...") porque :class:`EscritaNoEditor` distingue no-op de erro + real pelo texto da recusa; um dublê com mensagem própria testaria um + caminho que não existe em produção. + """ identificador_da_faixa = self._identificador_da_faixa(argumentos["track_index"], argumentos["track_type"]) - instante = float(argumentos["time_seconds"]) + instante_pedido = float(argumentos["time_seconds"]) faixa = self._faixas[identificador_da_faixa] + clipe_no_instante_pedido = next( + (c for c in faixa if c.inicio_na_timeline < instante_pedido < c.fim_na_timeline), None + ) + if clipe_no_instante_pedido is None: + raise ErroDeFerramentaMCP( + f"No clip strictly spans {instante_pedido}s on track {identificador_da_faixa}." + ) + instante = min( + max(instante_pedido + self.desvio_do_split, clipe_no_instante_pedido.inicio_na_timeline + 0.001), + clipe_no_instante_pedido.fim_na_timeline - 0.001, + ) + self._salvar_estado() for posicao, clipe in enumerate(faixa): if clipe.inicio_na_timeline < instante < clipe.fim_na_timeline: deslocamento = instante - clipe.inicio_na_timeline @@ -148,22 +193,24 @@ class ClienteMCPFalso(ClienteMCP): ) faixa[posicao:posicao + 1] = [parte_esquerda, parte_direita] return {"split": True} - raise ErroDeFerramentaMCP(f"Nenhum clipe da faixa {identificador_da_faixa!r} cobre estritamente {instante}s.") + raise ErroDeFerramentaMCP( + f"No clip strictly spans {instante}s on track {identificador_da_faixa}." + ) - def _simular_remove_from_timeline(self, argumentos: dict[str, Any]) -> dict[str, Any]: - """Remove o clipe pedido e, com ``ripple``, fecha o espaço deixado na mesma faixa.""" + def _simular_ripple_delete(self, argumentos: dict[str, Any]) -> dict[str, Any]: + """Remove o clipe pedido e fecha o espaço deixado na mesma faixa.""" identificador_do_clipe = argumentos["node_id"] for identificador_da_faixa, clipes in self._faixas.items(): for posicao, clipe in enumerate(clipes): if clipe.identificador == identificador_do_clipe: + self._salvar_estado() duracao = clipe.fim_na_timeline - clipe.inicio_na_timeline del clipes[posicao] - if argumentos.get("ripple"): - for outro in clipes: - if outro.inicio_na_timeline >= clipe.fim_na_timeline: - outro.inicio_na_timeline -= duracao - outro.fim_na_timeline -= duracao - return {"removed": True} + for outro in clipes: + if outro.inicio_na_timeline >= clipe.fim_na_timeline: + outro.inicio_na_timeline -= duracao + outro.fim_na_timeline -= duracao + return {"rippleDeleted": True, "verified": True} raise ErroDeFerramentaMCP(f"Clipe não encontrado para remoção: {identificador_do_clipe!r}.") def _simular_set_clip_properties(self, argumentos: dict[str, Any]) -> dict[str, Any]: @@ -176,6 +223,19 @@ class ClienteMCPFalso(ClienteMCP): del argumentos return {"added": True} + def _salvar_estado(self) -> None: + """Guarda uma cópia do estado das faixas para uma futura chamada a ``undo``.""" + self._historico.append(copy.deepcopy(self._faixas)) + + def _simular_undo(self, argumentos: dict[str, Any]) -> dict[str, Any]: + """Restaura o estado das faixas anterior à última mutação salva.""" + contagem = int(argumentos.get("count", 1)) + for _ in range(contagem): + if not self._historico: + raise ErroDeFerramentaMCP("Nada para desfazer neste dublê de teste.") + self._faixas = self._historico.pop() + return {"undone": contagem} + @staticmethod def _identificador_da_faixa(indice: int, tipo: str) -> str: """Reconstrói o identificador de faixa (``video_0``/``audio_0``) a partir de índice e tipo.""" diff --git a/code/engine/testes/test_aplicador_de_plano_de_edicao.py b/code/engine/testes/test_aplicador_de_plano_de_edicao.py index a6e99b8..b5141db 100644 --- a/code/engine/testes/test_aplicador_de_plano_de_edicao.py +++ b/code/engine/testes/test_aplicador_de_plano_de_edicao.py @@ -44,7 +44,12 @@ class TesteAplicadorDePlanoDeEdicao(unittest.TestCase): {"clipe_a_video", "clipe_a_audio"}, ) - def test_marcador_e_zoom_sao_aplicados_antes_dos_cortes(self): + def test_marcador_e_zoom_sao_aplicados_depois_dos_cortes(self): + # Zoom muda uma propriedade do clipe inteiro (set_clip_properties), + # não de um trecho dele. Se o zoom rodasse antes do corte, cairia no + # clipe original ainda não dividido; dois zooms em pontos diferentes + # do mesmo clipe se sobrescreveriam, e a divisão posterior faria os + # pedaços herdarem só o último valor. Por isso cortes vêm primeiro. plano = PlanoDeEdicao("0E6A8290.mp4", ( AcaoDeEdicao(TipoDeAcao.CORTE, inicio=0.0, fim=40.6, motivo="bastidor"), AcaoDeEdicao(TipoDeAcao.MARCADOR, inicio=90.0, fim=90.5, motivo="conferir emenda", parametros={"content": "EMENDA"}), @@ -52,12 +57,12 @@ class TesteAplicadorDePlanoDeEdicao(unittest.TestCase): )) resultado = self.aplicador.aplicar(plano) self.assertTrue(resultado.todas_bem_sucedidas, msg=[r.detalhe for r in resultado.falhas]) - # marcador e zoom aconteceram antes do corte na sequência de chamadas + # corte aconteceu antes de marcador e zoom na sequência de chamadas indice_do_marcador = next(i for i, c in enumerate(self.cliente.chamadas) if c[0] == "add_marker") indice_do_zoom = next(i for i, c in enumerate(self.cliente.chamadas) if c[0] == "set_clip_properties") - indice_da_remocao = next(i for i, c in enumerate(self.cliente.chamadas) if c[0] == "remove_from_timeline") - self.assertLess(indice_do_marcador, indice_da_remocao) - self.assertLess(indice_do_zoom, indice_da_remocao) + indice_da_remocao = next(i for i, c in enumerate(self.cliente.chamadas) if c[0] == "ripple_delete") + self.assertLess(indice_da_remocao, indice_do_marcador) + self.assertLess(indice_da_remocao, indice_do_zoom) def test_dois_cortes_sao_aplicados_do_fim_para_o_comeco(self): plano = PlanoDeEdicao("0E6A8290.mp4", ( @@ -82,6 +87,124 @@ class TesteAplicadorDePlanoDeEdicao(unittest.TestCase): # a ação válida (marcador) ainda foi aplicada self.assertTrue(any(c[0] == "add_marker" for c in self.cliente.chamadas)) + def test_dois_zooms_no_mesmo_clipe_original_nao_se_sobrescrevem(self): + # Bug real: com um só clipe cobrindo o arquivo inteiro (gravação + # "sem corte"), dois zooms em trechos diferentes caíam no mesmo + # clipe ainda não dividido; o segundo sobrescrevia a escala do + # primeiro no clipe inteiro, e ambos os pedaços resultantes da + # divisão herdavam só o último valor. + cliente = ClienteMCPFalso(clipe_unico=True) + aplicador = AplicadorDePlanoDeEdicao( + acesso_ao_editor=AcessoAoEditor(AcessoATimeline(cliente)), + conversor_de_timeline=ConversorDeTimeline(), + escrita=EscritaNoEditor(cliente), + mapeador=MapeadorDeTempoDeOrigemParaTimeline(), + ) + plano = PlanoDeEdicao("0E6A8290.mp4", ( + AcaoDeEdicao(TipoDeAcao.CORTE, inicio=90.0, fim=100.0, motivo="bastidor no meio"), + AcaoDeEdicao(TipoDeAcao.ZOOM, inicio=10.0, fim=20.0, motivo="ênfase 1", parametros={"scale": 1.15}), + AcaoDeEdicao(TipoDeAcao.ZOOM, inicio=150.0, fim=160.0, motivo="ênfase 2", parametros={"scale": 1.30}), + )) + resultado = aplicador.aplicar(plano) + self.assertTrue(resultado.todas_bem_sucedidas, msg=[r.detalhe for r in resultado.falhas]) + chamadas_de_zoom = [c for c in cliente.chamadas if c[0] == "set_clip_properties"] + self.assertEqual(len(chamadas_de_zoom), 2) + node_ids_zoomados = {c[1]["node_id"] for c in chamadas_de_zoom} + # os dois zooms caíram em clipes diferentes, um por trecho + self.assertEqual(len(node_ids_zoomados), 2) + escalas = sorted(round(c[1]["scale"], 3) for c in chamadas_de_zoom) + self.assertEqual(escalas, [115.0, 130.0]) + + def test_corte_com_desvio_de_bridge_usa_a_borda_real_em_vez_de_falhar(self): + # O split_clip do bridge do Premiere corta alguns décimos de segundo + # antes ou depois do instante pedido, e o desvio cresce com o tempo + # absoluto (drift de conversão segundos↔ticks, não bug pontual) — é + # comportamento normal, não uma falha. _borda_realmente_criada já + # localiza onde o Premiere de fato cortou; o corte usa essa borda em + # vez de rejeitar o desvio. + cliente = ClienteMCPFalso(clipe_unico=True, desvio_do_split=-0.3) + aplicador = AplicadorDePlanoDeEdicao( + acesso_ao_editor=AcessoAoEditor(AcessoATimeline(cliente)), + conversor_de_timeline=ConversorDeTimeline(), + escrita=EscritaNoEditor(cliente), + mapeador=MapeadorDeTempoDeOrigemParaTimeline(), + ) + plano = PlanoDeEdicao("0E6A8290.mp4", ( + AcaoDeEdicao(TipoDeAcao.CORTE, inicio=50.0, fim=60.0, motivo="bastidor"), + )) + resultado = aplicador.aplicar(plano) + self.assertTrue(resultado.todas_bem_sucedidas, msg=[r.detalhe for r in resultado.falhas]) + self.assertEqual(len(cliente.chamadas_de_remocao), 2) # vídeo + áudio + # nenhuma segunda tentativa de corte no mesmo ponto pedido, por faixa + pedidos_por_faixa = [ + (c[1]["track_type"], c[1]["time_seconds"]) for c in cliente.chamadas if c[0] == "split_clip" + ] + self.assertEqual(len(pedidos_por_faixa), len(set(pedidos_por_faixa))) + + def test_corte_sem_nenhuma_borda_por_perto_ainda_falha(self): + # O desvio do bridge é tolerado, mas só dentro de um raio plausível + # (_RAIO_DE_BUSCA_DA_BORDA_EM_SEGUNDOS): além disso o corte realmente + # falhou, não é mais drift — precisa continuar falhando alto, sem + # tentar de novo no mesmo ponto (o Premiere não expõe undo por + # script, então uma segunda tentativa deixaria o corte errado + # comendo espaço na timeline, com uma fatia inútil ao lado). + cliente = ClienteMCPFalso(clipe_unico=True, desvio_do_split=-5.0) + aplicador = AplicadorDePlanoDeEdicao( + acesso_ao_editor=AcessoAoEditor(AcessoATimeline(cliente)), + conversor_de_timeline=ConversorDeTimeline(), + escrita=EscritaNoEditor(cliente), + mapeador=MapeadorDeTempoDeOrigemParaTimeline(), + ) + plano = PlanoDeEdicao("0E6A8290.mp4", ( + AcaoDeEdicao(TipoDeAcao.CORTE, inicio=50.0, fim=60.0, motivo="bastidor"), + )) + resultado = aplicador.aplicar(plano) + self.assertFalse(resultado.todas_bem_sucedidas) + self.assertIn("Nenhuma borda", resultado.falhas[0].detalhe) + # nada removido e nenhuma segunda tentativa de corte no mesmo ponto + self.assertEqual(cliente.chamadas_de_remocao, []) + instantes_pedidos = [c[1]["time_seconds"] for c in cliente.chamadas if c[0] == "split_clip"] + self.assertEqual(len(instantes_pedidos), len(set(instantes_pedidos))) + + def test_corte_dentro_da_tolerancia_de_um_quadro_e_aceito(self): + # O Premiere encaixa o corte no quadro mais próximo, então uma + # diferença de fração de quadro é normal e não pode falhar. + cliente = ClienteMCPFalso(clipe_unico=True, desvio_do_split=-0.01) + aplicador = AplicadorDePlanoDeEdicao( + acesso_ao_editor=AcessoAoEditor(AcessoATimeline(cliente)), + conversor_de_timeline=ConversorDeTimeline(), + escrita=EscritaNoEditor(cliente), + mapeador=MapeadorDeTempoDeOrigemParaTimeline(), + ) + plano = PlanoDeEdicao("0E6A8290.mp4", ( + AcaoDeEdicao(TipoDeAcao.CORTE, inicio=50.0, fim=60.0, motivo="bastidor"), + )) + resultado = aplicador.aplicar(plano) + self.assertTrue(resultado.todas_bem_sucedidas, msg=[r.detalhe for r in resultado.falhas]) + self.assertEqual(len(cliente.chamadas_de_remocao), 2) # vídeo + áudio + self.assertEqual(len(cliente._faixas["video_0"]), 2) # sem fatia sobrando + + def test_corte_falha_em_vez_de_ignorar_faixa_sem_intervalo_de_origem(self): + # Reproduz o bug real do bridge do Premiere: a faixa de áudio não + # reportava inPoint/outPoint, então o corte "afetava" 0 clipes ali e + # passava batido — a faixa de áudio inteira ficava sem ser cortada, + # dessincronizando o vídeo (já cortado) do áudio (intacto). + cliente = ClienteMCPFalso(sem_origem_no_audio=True) + aplicador = AplicadorDePlanoDeEdicao( + acesso_ao_editor=AcessoAoEditor(AcessoATimeline(cliente)), + conversor_de_timeline=ConversorDeTimeline(), + escrita=EscritaNoEditor(cliente), + mapeador=MapeadorDeTempoDeOrigemParaTimeline(), + ) + plano = PlanoDeEdicao("0E6A8290.mp4", ( + AcaoDeEdicao(TipoDeAcao.CORTE, inicio=0.0, fim=40.6, motivo="bastidor"), + )) + resultado = aplicador.aplicar(plano) + self.assertFalse(resultado.todas_bem_sucedidas) + self.assertIn("sem intervalo de origem", resultado.falhas[0].detalhe) + # o vídeo não pode ter sido cortado sozinho: nada foi removido em faixa nenhuma + self.assertEqual(cliente.chamadas_de_remocao, []) + def test_texto_e_convertido_em_marcador_por_falta_de_suporte_do_premiere(self): plano = PlanoDeEdicao("0E6A8290.mp4", ( AcaoDeEdicao(TipoDeAcao.TEXTO, inicio=90.0, fim=91.0, motivo="reforçar termo", parametros={"content": "MASTOPEXIA"}), diff --git a/code/engine/testes/test_backup_de_sequencia.py b/code/engine/testes/test_backup_de_sequencia.py new file mode 100644 index 0000000..d26c046 --- /dev/null +++ b/code/engine/testes/test_backup_de_sequencia.py @@ -0,0 +1,54 @@ +"""Testes do backup automático da sequência antes de aplicar um plano.""" + +import unittest +from typing import Any + +from engine.editor.backup_de_sequencia import BackupDaSequencia +from engine.integracoes.premiere.cliente_mcp import ClienteMCP +from engine.integracoes.premiere.erros_mcp import ErroDeFerramentaMCP + + +class _ClienteQueRegistra(ClienteMCP): + """Dublê que registra as chamadas e devolve sucesso ou o erro configurado.""" + + def __init__(self, erro_a_levantar: Exception | None = None) -> None: + self.chamadas: list[tuple[str, dict[str, Any]]] = [] + self.erro_a_levantar = erro_a_levantar + + def conectar(self) -> None: + """Não há conexão real neste dublê.""" + + def desconectar(self) -> None: + """Não há conexão real neste dublê.""" + + def esta_conectado(self) -> bool: + """Sempre conectado neste dublê.""" + return True + + def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]: + self.chamadas.append((nome_da_ferramenta, argumentos or {})) + if self.erro_a_levantar is not None: + raise self.erro_a_levantar + return {"success": True} + + +class TesteBackupDaSequencia(unittest.TestCase): + def test_duplica_a_sequencia_pelo_identificador(self): + cliente = _ClienteQueRegistra() + backup = BackupDaSequencia(cliente) + conseguiu = backup.criar_backup("sequencia_falsa") + self.assertTrue(conseguiu) + self.assertEqual(cliente.chamadas, [("duplicate_sequence", {"sequence_id": "sequencia_falsa"})]) + + def test_falha_no_backup_nao_levanta_excecao(self): + # Espelha o painel CEP: se o backup falhar, aplica o plano mesmo + # assim (loga e avisa, não bloqueia) — quem chama decide o que + # fazer com o retorno False, não uma exceção não tratada. + cliente = _ClienteQueRegistra(erro_a_levantar=ErroDeFerramentaMCP("recusado")) + backup = BackupDaSequencia(cliente) + conseguiu = backup.criar_backup("sequencia_falsa") + self.assertFalse(conseguiu) + + +if __name__ == "__main__": + unittest.main() diff --git a/code/engine/testes/test_cliente_mcp_com_pausa.py b/code/engine/testes/test_cliente_mcp_com_pausa.py new file mode 100644 index 0000000..10006d6 --- /dev/null +++ b/code/engine/testes/test_cliente_mcp_com_pausa.py @@ -0,0 +1,101 @@ +"""Testes do decorator que espaça as chamadas ao bridge do Premiere.""" + +import unittest +from typing import Any + +from engine.integracoes.premiere.cliente_mcp import ClienteMCP +from engine.integracoes.premiere.cliente_mcp_com_pausa import ClienteMCPComPausa +from engine.integracoes.premiere.erros_mcp import ErroDeFerramentaMCP + + +class _ClienteQueRegistra(ClienteMCP): + """Dublê que só registra as chamadas recebidas e devolve um resultado fixo.""" + + def __init__(self, erro_a_levantar: Exception | None = None) -> None: + self.chamadas: list[tuple[str, dict[str, Any]]] = [] + self.erro_a_levantar = erro_a_levantar + self.conectado = False + + def conectar(self) -> None: + """Marca o dublê como conectado.""" + self.conectado = True + + def desconectar(self) -> None: + """Marca o dublê como desconectado.""" + self.conectado = False + + def esta_conectado(self) -> bool: + """Estado de conexão registrado pelo dublê.""" + return self.conectado + + def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]: + """Registra a chamada e devolve um resultado fixo, ou levanta o erro configurado.""" + self.chamadas.append((nome_da_ferramenta, argumentos or {})) + if self.erro_a_levantar is not None: + raise self.erro_a_levantar + return {"ok": True} + + +class _EsperaFalsa: + """Substitui ``time.sleep`` registrando as pausas pedidas, sem esperar de verdade.""" + + def __init__(self) -> None: + self.pausas: list[float] = [] + + def __call__(self, segundos: float) -> None: + """Registra a pausa pedida.""" + self.pausas.append(segundos) + + +class TesteClienteMCPComPausa(unittest.TestCase): + """Garante o espaçamento entre chamadas e a transparência do decorator.""" + + def setUp(self): + self.interno = _ClienteQueRegistra() + self.espera = _EsperaFalsa() + self.cliente = ClienteMCPComPausa(self.interno, pausa_em_segundos=0.12, esperar=self.espera) + + def test_repassa_a_chamada_ao_cliente_interno(self): + resultado = self.cliente.chamar("split_clip", {"time_seconds": 10.0}) + self.assertEqual(resultado, {"ok": True}) + self.assertEqual(self.interno.chamadas, [("split_clip", {"time_seconds": 10.0})]) + + def test_nao_pausa_antes_da_primeira_chamada(self): + # A primeira chamada não precisa esperar: não há chamada anterior + # ocupando o processo principal do Premiere. + self.cliente.chamar("get_active_sequence") + self.assertEqual(self.espera.pausas, []) + + def test_pausa_entre_chamadas_consecutivas(self): + # Sem essa folga, uma rajada de round-trips síncronos de QE/ExtendScript + # trava a UI do Premiere — foi o que se viu com um plano de 17 cortes. + for _ in range(3): + self.cliente.chamar("split_clip", {"time_seconds": 1.0}) + self.assertEqual(self.espera.pausas, [0.12, 0.12]) + + def test_pausa_tambem_depois_de_uma_chamada_que_falhou(self): + # Uma chamada que falhou ocupou o bridge do mesmo jeito; emendar a + # próxima sem folga é justamente o que derruba a UI. + interno = _ClienteQueRegistra(erro_a_levantar=ErroDeFerramentaMCP("recusado")) + espera = _EsperaFalsa() + cliente = ClienteMCPComPausa(interno, pausa_em_segundos=0.12, esperar=espera) + for _ in range(2): + with self.assertRaises(ErroDeFerramentaMCP): + cliente.chamar("split_clip", {"time_seconds": 1.0}) + self.assertEqual(espera.pausas, [0.12]) + + def test_conexao_e_delegada_sem_pausa(self): + # Conectar/desconectar não são round-trips de edição; não precisam de folga. + self.cliente.conectar() + self.assertTrue(self.cliente.esta_conectado()) + self.cliente.desconectar() + self.assertFalse(self.cliente.esta_conectado()) + self.assertEqual(self.espera.pausas, []) + + def test_pausa_negativa_e_recusada(self): + with self.assertRaises(ValueError): + ClienteMCPComPausa(self.interno, pausa_em_segundos=-0.1) + + +if __name__ == "__main__": + unittest.main() diff --git a/code/engine/testes/test_escrita_no_editor.py b/code/engine/testes/test_escrita_no_editor.py new file mode 100644 index 0000000..5a8697a --- /dev/null +++ b/code/engine/testes/test_escrita_no_editor.py @@ -0,0 +1,75 @@ +"""Testes da escrita de mutações no Premiere, sem o app aberto. + +Cobrem a distinção — que já custou um corte silenciosamente perdido — entre +um ``split_clip`` que não fez nada porque o ponto já era borda de clipe (um +no-op legítimo) e um ``split_clip`` que falhou de verdade no bridge. +""" + +import unittest +from typing import Any + +from engine.editor.erros import ErroDeEscritaNoEditor +from engine.editor.escrita import EscritaNoEditor +from engine.integracoes.premiere.cliente_mcp import ClienteMCP +from engine.integracoes.premiere.erros_mcp import ErroDeFerramentaMCP + + +class _ClienteQueRecusa(ClienteMCP): + """Dublê que recusa toda chamada com a mensagem informada no construtor.""" + + def __init__(self, mensagem_de_erro: str) -> None: + self.mensagem_de_erro = mensagem_de_erro + self.chamadas: list[tuple[str, dict[str, Any]]] = [] + + def conectar(self) -> None: + """Não há conexão real neste dublê.""" + + def desconectar(self) -> None: + """Não há conexão real neste dublê.""" + + def esta_conectado(self) -> bool: + """Sempre conectado: este dublê não simula falha de conexão.""" + return True + + def chamar(self, nome_da_ferramenta: str, argumentos: dict[str, Any] | None = None) -> dict[str, Any]: + """Registra a chamada e recusa com :class:`ErroDeFerramentaMCP`.""" + self.chamadas.append((nome_da_ferramenta, argumentos or {})) + raise ErroDeFerramentaMCP(self.mensagem_de_erro) + + +class TesteDividirClipeSeNecessario(unittest.TestCase): + """Separa o no-op esperado do erro real de bridge.""" + + def _dividir(self, mensagem_de_erro: str) -> bool: + escrita = EscritaNoEditor(_ClienteQueRecusa(mensagem_de_erro)) + return escrita.dividir_clipe_se_necessario(10.0, 0, "video") + + def test_ponto_que_ja_era_borda_e_no_op_e_nao_erro(self): + # O Premiere recusa dividir onde nenhum clipe atravessa o instante: + # é o resultado esperado quando o corte pedido coincide com uma borda + # já existente (inclusive ao reexecutar um plano parcialmente aplicado). + self.assertFalse(self._dividir("No clip strictly spans 10.0s on track video_0.")) + + def test_contagem_de_clipes_inalterada_e_no_op_e_nao_erro(self): + # Segunda forma observada do mesmo no-op: o razor roda, mas o ponto já + # coincide por arredondamento de quadro com uma borda existente, então + # a contagem de clipes da faixa não muda. + self.assertFalse(self._dividir("razor changed the track clip count from 4 to 4, expected 5")) + + def test_erro_real_do_bridge_nao_e_confundido_com_no_op(self): + # Bug real: qualquer ErroDeFerramentaMCP era engolido como "já era + # borda", então uma falha de conexão ou uma faixa bloqueada passava + # por corte bem-sucedido e o trecho seguia na timeline sem aviso. + with self.assertRaises(ErroDeEscritaNoEditor) as capturado: + self._dividir("Track video_0 is locked.") + self.assertIn("locked", str(capturado.exception)) + + def test_contagem_de_clipes_que_mudou_errado_nao_e_no_op(self): + # A contagem mudou, mas não para o esperado: o razor fez algo, e algo + # diferente do pedido — isso é erro, não no-op. + with self.assertRaises(ErroDeEscritaNoEditor): + self._dividir("razor changed the track clip count from 4 to 7, expected 5") + + +if __name__ == "__main__": + unittest.main() diff --git a/code/scripts/apply-editorial-actions.mjs b/code/scripts/apply-editorial-actions.mjs index 8a590be..dc609d1 100644 --- a/code/scripts/apply-editorial-actions.mjs +++ b/code/scripts/apply-editorial-actions.mjs @@ -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 diff --git a/code/src/tools/discovery.ts b/code/src/tools/discovery.ts index c2cf602..faf5068 100755 --- a/code/src/tools/discovery.ts +++ b/code/src/tools/discovery.ts @@ -166,7 +166,16 @@ export function getDiscoveryTools(bridgeOptions: BridgeOptions) { name: clip.name, start: __ticksToSeconds(clip.start.ticks), end: __ticksToSeconds(clip.end.ticks), - duration: __ticksToSeconds(clip.duration.ticks) + inPoint: __ticksToSeconds(clip.inPoint.ticks), + outPoint: __ticksToSeconds(clip.outPoint.ticks), + duration: __ticksToSeconds(clip.duration.ticks), + sourceFile: clip.projectItem && clip.projectItem.getMediaPath + ? String(clip.projectItem.getMediaPath() || "") + : "", + sourceProjectItemId: clip.projectItem ? String(clip.projectItem.nodeId || "") : "", + sourceOffline: clip.projectItem && clip.projectItem.isOffline + ? !!clip.projectItem.isOffline() + : false }); } audioTracks.push({ diff --git a/code/src/tools/timeline.ts b/code/src/tools/timeline.ts index 338e750..ab654f2 100755 --- a/code/src/tools/timeline.ts +++ b/code/src/tools/timeline.ts @@ -418,7 +418,22 @@ export function getTimelineTools(bridgeOptions: BridgeOptions) { return __error("Refusing trim before mutation: " + beforeKeyframes.outside.length + " effect keyframe(s) would remain outside the visible clip. Use keyframe_policy: preserve only if retaining those keyframes is intentional, or adjust them explicitly with the keyframe tools."); } - ${args.new_in_seconds !== undefined ? `clip.inPoint = __secondsToTicks(${args.new_in_seconds}).toString();` : "clip.outPoint = __secondsToTicks(" + args.new_out_seconds + ").toString();"} + // Set the source point AND the corresponding timeline boundary + // together, computed from the same before-snapshot, instead of + // setting only the source point and trusting Premiere to move the + // timeline boundary on its own: that auto-derivation is exactly + // what silently no-op'd on the timeline side while still moving + // the source point on a real Premiere Pro 26.x install, leaving a + // clip whose visible duration no longer matched its source range. + ${args.new_in_seconds !== undefined ? ` + var expectedStartOnWrite = before.start + (${args.new_in_seconds} - before.inPoint); + clip.start = __secondsToTicks(expectedStartOnWrite).toString(); + clip.inPoint = __secondsToTicks(${args.new_in_seconds}).toString(); + ` : ` + var expectedEndOnWrite = before.end + (${args.new_out_seconds} - before.outPoint); + clip.end = __secondsToTicks(expectedEndOnWrite).toString(); + clip.outPoint = __secondsToTicks(${args.new_out_seconds}).toString(); + `} // Re-find the TrackItem after the write. Premiere can replace stale // DOM references during an edit, especially for audio clips. @@ -586,10 +601,50 @@ export function getTimelineTools(bridgeOptions: BridgeOptions) { if (!__razorFps || !isFinite(__razorFps) || __razorFps < 1) __razorFps = 30; var __razorFrames = Math.round(cutTicks / frameTicks); function __pad2(n) { return n < 10 ? "0" + n : "" + n; } - var __razorTc = __pad2(Math.floor(__razorFrames / (__razorFps * 3600))) + ":" + - __pad2(Math.floor((__razorFrames % (__razorFps * 3600)) / (__razorFps * 60))) + ":" + - __pad2(Math.floor((__razorFrames % (__razorFps * 60)) / __razorFps)) + ":" + - __pad2(__razorFrames % __razorFps); + + // A sequence whose display format is drop-frame timecode (videoDisplayFormat + // 102 for 29.97 or 106 for 59.94) numbers frames as SMPTE drop-frame: 2 frame + // NUMBERS are skipped at the start of every minute except every 10th. razor() + // parses its timecode string using that same numbering, so handing it a plain + // non-drop-frame HH:MM:SS:FF built from the raw frame count lands the cut at + // the wrong absolute time, worse the further into the sequence the cut is — + // exactly the growing drift this tool used to report as an unexplained no-op + // on some cuts. __razorFrames is the true physical frame count from ticks, so + // it must be converted to the DROP-FRAME-numbered timecode when that display + // format is active, not just formatted with a rounded fps. + var __isDropFrame = false; + try { + // getSettings() existe na sequência do DOM, não na do QE: ler pelo + // objeto errado lança, cai no catch e desliga o drop-frame em + // silêncio, mantendo o desvio que esta correção existe para tirar. + var __seqSettings = domSequence.getSettings ? domSequence.getSettings() : null; + var __displayFormat = __seqSettings ? __seqSettings.videoDisplayFormat : null; + __isDropFrame = __displayFormat === 102 || __displayFormat === 106; + } catch (__dfErr) { __isDropFrame = false; } + + var __razorTc; + if (__isDropFrame) { + var __dropPerMin = Math.round(__razorFps * 2 / 30); + var __framesPerMinuteNominal = __razorFps * 60; + var __framesPer10Minutes = __framesPerMinuteNominal * 10 - __dropPerMin * 9; + var __framesPerMinuteDropped = __framesPerMinuteNominal - __dropPerMin; + var __d = Math.floor(__razorFrames / __framesPer10Minutes); + var __m = __razorFrames % __framesPer10Minutes; + var __labelFrames = __razorFrames + __dropPerMin * 9 * __d; + if (__m > __dropPerMin) { + __labelFrames += __dropPerMin * Math.floor((__m - __dropPerMin) / __framesPerMinuteDropped); + } + var __dfFrames = __labelFrames % __razorFps; + var __dfSeconds = Math.floor(__labelFrames / __razorFps) % 60; + var __dfMinutes = Math.floor(__labelFrames / (__razorFps * 60)) % 60; + var __dfHours = Math.floor(__labelFrames / (__razorFps * 3600)) % 24; + __razorTc = __pad2(__dfHours) + ":" + __pad2(__dfMinutes) + ":" + __pad2(__dfSeconds) + ";" + __pad2(__dfFrames); + } else { + __razorTc = __pad2(Math.floor(__razorFrames / (__razorFps * 3600))) + ":" + + __pad2(Math.floor((__razorFrames % (__razorFps * 3600)) / (__razorFps * 60))) + ":" + + __pad2(Math.floor((__razorFrames % (__razorFps * 60)) / __razorFps)) + ":" + + __pad2(__razorFrames % __razorFps); + } try { track.razor(__razorTc); @@ -750,55 +805,78 @@ export function getTimelineTools(bridgeOptions: BridgeOptions) { const script = buildToolScript(` var result = __findClip("${escapeForExtendScript(args.node_id)}"); if (!result) return __error("Clip not found: ${escapeForExtendScript(args.node_id)}"); - + var clip = result.clip; var changes = {}; - + var missing = []; + + // Property lookup uses the fixed index of Premiere's built-in Motion + // and Opacity effects, not displayName: displayName is localized + // (e.g. "Escala"/"Posição"/"Rotação" in pt_BR Premiere), so a plain + // string match against the English name silently matches nothing + // and no-ops on non-English installs. Index 0 of Opacity is its + // only property; Motion is fixed at 0=Position, 1=Scale, 4=Rotation + // across locales — confirmed against a pt_BR install. + function __motionComponent() { + for (var i = 0; i < clip.components.numItems; i++) { + if (clip.components[i].matchName === "AE.ADBE Motion") return clip.components[i]; + } + return null; + } + function __opacityComponent() { + for (var i = 0; i < clip.components.numItems; i++) { + if (clip.components[i].matchName === "AE.ADBE Opacity") return clip.components[i]; + } + return null; + } + ${args.opacity !== undefined ? ` - // Set opacity via Motion component - for (var i = 0; i < clip.components.numItems; i++) { - var comp = clip.components[i]; - if (comp.matchName === "AE.ADBE Opacity" || comp.displayName === "Opacity") { - for (var p = 0; p < comp.properties.numItems; p++) { - if (comp.properties[p].displayName === "Opacity") { - comp.properties[p].setValue(${args.opacity}, true); - changes.opacity = ${args.opacity}; - } - } - } + var opacityComp = __opacityComponent(); + if (!opacityComp || opacityComp.properties.numItems < 1) { + missing.push("opacity: componente Opacity não encontrado no clipe"); + } else { + opacityComp.properties[0].setValue(${args.opacity}, true); + changes.opacity = ${args.opacity}; } ` : ""} - + ${args.scale !== undefined || args.position_x !== undefined || args.position_y !== undefined || args.rotation !== undefined ? ` - for (var i = 0; i < clip.components.numItems; i++) { - var comp = clip.components[i]; - if (comp.matchName === "AE.ADBE Motion" || comp.displayName === "Motion") { - for (var p = 0; p < comp.properties.numItems; p++) { - var prop = comp.properties[p]; - ${args.scale !== undefined ? ` - if (prop.displayName === "Scale") { - prop.setValue(${args.scale}, true); - changes.scale = ${args.scale}; - }` : ""} - ${args.position_x !== undefined || args.position_y !== undefined ? ` - if (prop.displayName === "Position") { - var posVal = prop.getValue(); - var px = posVal && typeof posVal === "object" && posVal.length >= 2 ? posVal[0] : 0; - var py = posVal && typeof posVal === "object" && posVal.length >= 2 ? posVal[1] : 0; - ${args.position_x !== undefined ? `px = ${args.position_x}; changes.position_x = ${args.position_x};` : ""} - ${args.position_y !== undefined ? `py = ${args.position_y}; changes.position_y = ${args.position_y};` : ""} - prop.setValue([px, py], true); - }` : ""} - ${args.rotation !== undefined ? ` - if (prop.displayName === "Rotation") { - prop.setValue(${args.rotation}, true); - changes.rotation = ${args.rotation}; - }` : ""} - } - } + var motionComp = __motionComponent(); + if (!motionComp) { + missing.push("componente Motion não encontrado no clipe"); + } else { + ${args.scale !== undefined ? ` + if (motionComp.properties.numItems < 2) { + missing.push("scale: propriedade Escala (índice 1) não encontrada"); + } else { + motionComp.properties[1].setValue(${args.scale}, true); + changes.scale = ${args.scale}; + }` : ""} + ${args.position_x !== undefined || args.position_y !== undefined ? ` + if (motionComp.properties.numItems < 1) { + missing.push("position: propriedade Posição (índice 0) não encontrada"); + } else { + var posProp = motionComp.properties[0]; + var posVal = posProp.getValue(); + var px = posVal && typeof posVal === "object" && posVal.length >= 2 ? posVal[0] : 0; + var py = posVal && typeof posVal === "object" && posVal.length >= 2 ? posVal[1] : 0; + ${args.position_x !== undefined ? `px = ${args.position_x}; changes.position_x = ${args.position_x};` : ""} + ${args.position_y !== undefined ? `py = ${args.position_y}; changes.position_y = ${args.position_y};` : ""} + posProp.setValue([px, py], true); + }` : ""} + ${args.rotation !== undefined ? ` + if (motionComp.properties.numItems < 5) { + missing.push("rotation: propriedade Rotação (índice 4) não encontrada"); + } else { + motionComp.properties[4].setValue(${args.rotation}, true); + changes.rotation = ${args.rotation}; + }` : ""} } ` : ""} - + + if (missing.length) { + return __error("Não foi possível aplicar todas as propriedades pedidas: " + missing.join("; ") + ". Nenhuma mudança parcial foi reportada como sucesso."); + } return __result({ updated: true, clipName: clip.name, changes: changes }); `); return sendCommand(script, bridgeOptions);