From e73b0941872ce60844a185c724f4ab453ca3558d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Henrique?= Date: Thu, 10 Sep 2026 18:07:16 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20criado=20reposit=C3=B3rio=20jhonny-edit?= =?UTF-8?q?or=20no=20Gitea?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit criado repositório jhonny-editor no Gitea adicionado script admin/deploy.command com commit automático atualizado admin/DEV-NOTES.md com template limpo Resumo: - 4 arquivos alterados - 0 novos - 4 modificados - 0 removidos 4 files changed, 101 insertions(+), 4 deletions(-) Arquivos: - code/engine/editor/aplicador_de_plano_de_edicao.py - code/engine/editor/escrita/escrita_no_editor.py - code/engine/testes/duplos_de_premiere.py - code/engine/testes/test_aplicador_de_plano_de_edicao.py --- .../editor/aplicador_de_plano_de_edicao.py | 13 ++++- .../editor/escrita/escrita_no_editor.py | 16 ++++++- code/engine/testes/duplos_de_premiere.py | 28 ++++++++++- .../test_aplicador_de_plano_de_edicao.py | 48 +++++++++++++++++++ 4 files changed, 101 insertions(+), 4 deletions(-) diff --git a/code/engine/editor/aplicador_de_plano_de_edicao.py b/code/engine/editor/aplicador_de_plano_de_edicao.py index 362bca3..b0e7903 100644 --- a/code/engine/editor/aplicador_de_plano_de_edicao.py +++ b/code/engine/editor/aplicador_de_plano_de_edicao.py @@ -215,8 +215,19 @@ class AplicadorDePlanoDeEdicao: ) pontos_de_corte = self._calcular_pontos_de_corte(faixas_afetadas, acao, arquivo_de_origem) + razor_bem_sucedido = True for ponto in pontos_de_corte: - self.escrita.dividir_todas_as_faixas(ponto) + try: + self.escrita.dividir_todas_as_faixas(ponto) + except ErroDeEdicao: + razor_bem_sucedido = False + break + + if not razor_bem_sucedido: + raise ErroDeMapeamentoDeTempo( + f"O razor falhou ao cortar as faixas em {acao.inicio:.3f}-{acao.fim:.3f}s " + f"(Premiere 26.x QE no-op); nada foi removido." + ) faixas = self._timeline_atual().faixas for faixa_id in {f.identificador for f in faixas_afetadas}: diff --git a/code/engine/editor/escrita/escrita_no_editor.py b/code/engine/editor/escrita/escrita_no_editor.py index ac5366c..c1e1f03 100644 --- a/code/engine/editor/escrita/escrita_no_editor.py +++ b/code/engine/editor/escrita/escrita_no_editor.py @@ -85,9 +85,16 @@ class EscritaNoEditor: Usa ``razor_all_tracks`` para garantir que vídeo e áudio sejam divididos no mesmo ponto de tempo, evitando drift entre chamadas separadas de ``split_clip``. + + Levanta :class:`ErroDeEscritaNoEditor` se o razor reportar uma + falha parcial (algumas faixas cortadas, outras não) — o Premiere + 26.x tem um bug QE que faz o razor falhar silenciosamente em + algumas instalações. Uma falha parcial deixa a timeline num estado + inconsistente que corrompe operações subsequentes (remoção, zoom, + marcador), então a operação é recusada inteiramente. """ try: - self.cliente_mcp.chamar( + resultado = self.cliente_mcp.chamar( "razor_all_tracks", {"time_seconds": instante_da_timeline, "track_type": "both"}, ) @@ -95,6 +102,13 @@ class EscritaNoEditor: raise ErroDeEscritaNoEditor( f"Não foi possível cortar todas as faixas em {instante_da_timeline:.3f}s: {erro}" ) from erro + elegiveis = int(resultado.get("eligibleTracks", 0)) + cortados = int(resultado.get("razored", 0)) + if elegiveis > 0 and cortados < elegiveis: + raise ErroDeEscritaNoEditor( + f"O razor cortou apenas {cortados} de {elegiveis} faixa(s) elegíveis " + f"em {instante_da_timeline:.3f}s (Premiere 26.x QE no-op)" + ) def remover_trecho(self, identificador_do_clipe: str) -> None: """Remove o clipe da timeline fechando o espaço que ele ocupava. diff --git a/code/engine/testes/duplos_de_premiere.py b/code/engine/testes/duplos_de_premiere.py index 836b33b..6683fac 100644 --- a/code/engine/testes/duplos_de_premiere.py +++ b/code/engine/testes/duplos_de_premiere.py @@ -49,6 +49,7 @@ class ClienteMCPFalso(ClienteMCP): sem_origem_no_audio: bool = False, clipe_unico: bool = False, desvio_do_split: float = 0.0, + simular_falha_razor: bool = False, ) -> None: """``sem_origem_no_audio`` reproduz o bug real do bridge: relatar clipes de áudio sem ``inPoint``/``outPoint`` na resposta de ``get_active_sequence``. @@ -59,9 +60,14 @@ class ClienteMCPFalso(ClienteMCP): ``desvio_do_split`` reproduz o drift real do ``split_clip``: em vez de cortar exatamente no instante pedido, corta em - ``instante + desvio_do_split``.""" + ``instante + desvio_do_split``. + + ``simular_falha_razor`` reproduz o bug QE do Premiere 26.x: o + ``razor_all_tracks`` reporta clipes elegíveis mas não corta nenhum + (``razored=0, eligibleTracks=N``).""" self.sem_origem_no_audio = sem_origem_no_audio self.desvio_do_split = desvio_do_split + self.simular_falha_razor = simular_falha_razor self.chamadas: list[tuple[str, dict[str, Any]]] = [] self._proximo_id = 0 self._historico: list[dict[str, list[_ClipeFalso]]] = [] @@ -230,9 +236,27 @@ class ClienteMCPFalso(ClienteMCP): Reproduz o ``razor_all_tracks`` do bridge real: corta todas as faixas de vídeo e/ou áudio no mesmo ponto de tempo, garantindo sincronização entre elas. + + Quando ``simular_falha_razor`` está ativo, reporta clipes elegíveis + mas não corta nenhum, reproduzindo o bug QE do Premiere 26.x. """ instante_pedido = float(argumentos["time_seconds"]) track_type = argumentos.get("track_type", "both") + + elegiveis = 0 + for identificador_da_faixa, faixa in self._faixas.items(): + if track_type == "video" and not identificador_da_faixa.startswith("video_"): + continue + if track_type == "audio" and not identificador_da_faixa.startswith("audio_"): + continue + for clipe in faixa: + if clipe.inicio_na_timeline < instante_pedido < clipe.fim_na_timeline: + elegiveis += 1 + break + + if self.simular_falha_razor: + return {"razored": 0, "eligibleTracks": elegiveis, "verified": False} + cortados = 0 self._salvar_estado() for identificador_da_faixa, faixa in self._faixas.items(): @@ -262,7 +286,7 @@ class ClienteMCPFalso(ClienteMCP): faixa[posicao:posicao + 1] = [parte_esquerda, parte_direita] cortados += 1 break - return {"razored": cortados, "verified": True} + return {"razored": cortados, "eligibleTracks": elegiveis, "verified": True} 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.""" 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 778dbfd..cf64b2b 100644 --- a/code/engine/testes/test_aplicador_de_plano_de_edicao.py +++ b/code/engine/testes/test_aplicador_de_plano_de_edicao.py @@ -317,6 +317,54 @@ class TesteAplicadorDePlanoDeEdicao(unittest.TestCase): self.assertLess(nomes.index("remove_linked_from_timeline"), nomes.index("duplicate_linked_range_to_start")) self.assertLess(nomes.index("duplicate_linked_range_to_start"), nomes.index("set_clip_properties")) + def test_corte_falha_limpo_quando_razor_nao_corta_nenhuma_faixa(self): + """Quando o razor_all_tracks falha (QE no-op do Premiere 26.x), + o corte deve falhar sem corromper a timeline para operações + subsequentes — nenhuma remoção é tentada e marcadores/zooms + continuam funcionando.""" + cliente = ClienteMCPFalso(simular_falha_razor=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"), + AcaoDeEdicao(TipoDeAcao.MARCADOR, inicio=90.0, fim=90.5, motivo="ok", parametros={"content": "X"}), + )) + resultado = aplicador.aplicar(plano) + self.assertFalse(resultado.todas_bem_sucedidas) + self.assertEqual(len(resultado.falhas), 1) + self.assertIn("razor", resultado.falhas[0].detalhe.lower()) + self.assertTrue(any(c[0] == "add_marker" for c in cliente.chamadas)) + self.assertEqual(cliente.chamadas_de_remocao, []) + + def test_gancho_falha_limpo_quando_razor_nao_corta_nenhuma_faixa(self): + """Quando o razor_all_tracks falha ao isolar o trecho do gancho, + a operação deve falhar sem duplicar nada na timeline.""" + cliente = ClienteMCPFalso(clipe_unico=True, simular_falha_razor=True) + aplicador = AplicadorDePlanoDeEdicao( + acesso_ao_editor=AcessoAoEditor(AcessoATimeline(cliente)), + conversor_de_timeline=ConversorDeTimeline(), + escrita=EscritaNoEditor(cliente), + mapeador=MapeadorDeTempoDeOrigemParaTimeline(), + ) + plano = PlanoDeEdicao("0E6A8290.mp4", ( + AcaoDeEdicao( + TipoDeAcao.GANCHO, + inicio=80.0, + fim=90.0, + motivo="gancho de abertura", + parametros={"target": 0, "keep_original": True}, + ), + )) + resultado = aplicador.aplicar(plano) + self.assertFalse(resultado.todas_bem_sucedidas) + self.assertEqual(len(resultado.falhas), 1) + duplicacoes = [c for c in cliente.chamadas if c[0] == "duplicate_linked_range_to_start"] + self.assertEqual(len(duplicacoes), 0) + if __name__ == "__main__": unittest.main()