feat: aprimorada a edição por voz com validação de frases, margens
- aprimorada a edição por voz com validação de frases, margens seguras e backup único antes da timeline. - adicionada consulta expansível dos detalhes de cada trilha diretamente do banco Resumo: - 23 arquivos alterados - 5 novos - 18 modificados - 0 removidos 18 files changed, 321 insertions(+), 216 deletions(-) Arquivos: - .gitignore - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/analisar_trilhas.py - code/engine/aplicar_plano_de_edicao.py - code/engine/editor/__init__.py - code/engine/editor/backup_de_sequencia.py - code/engine/persistencia/__init__.py - code/engine/testes/test_aplicar_plano_de_edicao.py - code/plugins/premiere-pro/skills/edit-video-by-voice/SKILL.md - code/plugins/premiere-pro/skills/editar-por-voz/SKILL.md - code/plugins/premiere-pro/skills/editar-por-voz/criterios/05-zoom.md - code/plugins/premiere-pro/skills/editar-por-voz/criterios/06-texto-corte-marcador.md - code/plugins/premiere-pro/skills/editar-por-voz/criterios/08-formato-de-saida.md - code/plugins/premiere-pro/skills/editar-por-voz/criterios/10-revisao-humana.md - code/plugins/premiere-pro/skills/transcript-to-edit-actions/SKILL.md - code/engine/consultar_detalhes_de_trilha.py - code/engine/editor/validacao_semantica.py - code/engine/persistencia/leitura_semantica_do_plano.py - code/engine/testes/test_leitura_semantica_do_plano.py - code/engine/testes/test_validador_semantico_de_plano.py
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
"""Testes da resolução do executável Node usada pelo aplicador de planos."""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from engine import aplicar_plano_de_edicao
|
||||
from engine.editor import ErroDePlanoInvalido
|
||||
|
||||
|
||||
class TesteResolucaoDoNode(unittest.TestCase):
|
||||
@@ -35,5 +38,50 @@ class TesteResolucaoDoNode(unittest.TestCase):
|
||||
aplicar_plano_de_edicao.resolver_caminho_do_node()
|
||||
|
||||
|
||||
class TestePreValidacaoDoPlano(unittest.TestCase):
|
||||
"""Garante que planos semanticamente perigosos não alcancem o Premiere."""
|
||||
|
||||
def test_interrompe_antes_de_resolver_node_e_criar_backup(self) -> None:
|
||||
"""A validação semântica ocorre antes de abrir a sessão do editor."""
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
caminho_do_plano = Path(pasta) / "plano.json"
|
||||
caminho_do_plano.write_text(
|
||||
'{"source":"video.mp4","actions":['
|
||||
'{"kind":"cut","start":1.0,"end":2.0,"reason":"teste"}]}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
with patch(
|
||||
"engine.aplicar_plano_de_edicao._validar_integridade_semantica",
|
||||
side_effect=ErroDePlanoInvalido("frase interrompida"),
|
||||
) as validar, patch(
|
||||
"engine.aplicar_plano_de_edicao.resolver_caminho_do_node"
|
||||
) as resolver_node:
|
||||
with self.assertRaisesRegex(ErroDePlanoInvalido, "frase interrompida"):
|
||||
aplicar_plano_de_edicao.executar(caminho_do_plano, plano_id=7)
|
||||
|
||||
validar.assert_called_once()
|
||||
resolver_node.assert_not_called()
|
||||
|
||||
def test_painel_rejeita_corte_sem_plano_associado_ao_banco(self) -> None:
|
||||
"""Não permite contornar a validação colando um corte sem transcrição vinculada."""
|
||||
with tempfile.TemporaryDirectory() as pasta:
|
||||
caminho_do_plano = Path(pasta) / "plano.json"
|
||||
caminho_do_plano.write_text(
|
||||
'{"source":"video.mp4","actions":['
|
||||
'{"kind":"cut","start":1.0,"end":2.0,"reason":"teste"}]}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
with patch(
|
||||
"engine.aplicar_plano_de_edicao.resolver_caminho_do_node"
|
||||
) as resolver_node:
|
||||
with self.assertRaisesRegex(ErroDePlanoInvalido, "Salve ou carregue"):
|
||||
aplicar_plano_de_edicao.executar(
|
||||
caminho_do_plano,
|
||||
exigir_validacao_semantica=True,
|
||||
)
|
||||
|
||||
resolver_node.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Testes da leitura de falas usada antes da aplicação de um plano."""
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from engine.editor import ErroDePlanoInvalido
|
||||
from engine.persistencia import LeitorDeTranscricaoDoPlano, abrir_banco
|
||||
|
||||
|
||||
class TesteLeitorDeTranscricaoDoPlano(unittest.TestCase):
|
||||
"""Valida vínculo do plano e conversão de timeline para mídia original."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Cria um banco isolado com um clipe deslocado na timeline."""
|
||||
self._pasta_temporaria = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self._pasta_temporaria.cleanup)
|
||||
caminho_do_banco = Path(self._pasta_temporaria.name) / "analises.db"
|
||||
self.conexao = abrir_banco(caminho_do_banco)
|
||||
self.addCleanup(self.conexao.close)
|
||||
with self.conexao:
|
||||
self.conexao.execute(
|
||||
"INSERT INTO videos (id, nome, duracao) VALUES ('video-1', 'Vídeo', 40.0)"
|
||||
)
|
||||
self.conexao.execute(
|
||||
"""INSERT INTO faixas (id, video_id, nome, tipo, indice)
|
||||
VALUES ('audio-1', 'video-1', 'A1', 'audio', 0)"""
|
||||
)
|
||||
self.conexao.execute(
|
||||
"""INSERT INTO clipes
|
||||
(id, video_id, faixa_id, nome, inicio_na_timeline, fim_na_timeline,
|
||||
inicio_na_origem, fim_na_origem, arquivo, offline, metadados)
|
||||
VALUES ('clipe-1', 'video-1', 'audio-1', 'Clipe', 10.0, 30.0,
|
||||
2.0, 22.0, '/video.mp4', 0, '{}')"""
|
||||
)
|
||||
segmento = self.conexao.execute(
|
||||
"""INSERT INTO segmentos_de_transcricao
|
||||
(video_id, clipe_id, inicio, fim, texto)
|
||||
VALUES ('video-1', 'clipe-1', 12.0, 14.0, 'Frase completa.')"""
|
||||
)
|
||||
self.conexao.execute(
|
||||
"""INSERT INTO palavras_de_transcricao
|
||||
(segmento_id, ordem, texto, inicio, fim)
|
||||
VALUES (?, 0, 'Frase', 12.0, 12.5)""",
|
||||
(segmento.lastrowid,),
|
||||
)
|
||||
self.conexao.execute(
|
||||
"""INSERT INTO planos_de_edicao (video_id, origem)
|
||||
VALUES ('video-1', '/video.mp4')"""
|
||||
)
|
||||
|
||||
def test_converte_tempos_para_a_origem_do_clipe(self) -> None:
|
||||
"""Subtrai a posição da timeline e soma o início na mídia original."""
|
||||
falas = LeitorDeTranscricaoDoPlano(self.conexao).carregar(1)
|
||||
|
||||
self.assertEqual(falas[0].inicio, 4.0)
|
||||
self.assertEqual(falas[0].fim, 6.0)
|
||||
self.assertEqual(falas[0].palavras[0].inicio, 4.0)
|
||||
self.assertEqual(falas[0].palavras[0].fim, 4.5)
|
||||
|
||||
def test_rejeita_plano_inexistente(self) -> None:
|
||||
"""Evita validar uma transcrição que não pertence ao plano solicitado."""
|
||||
with self.assertRaisesRegex(ErroDePlanoInvalido, "não existe"):
|
||||
LeitorDeTranscricaoDoPlano(self.conexao).carregar(999)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Testes de regressão da integridade semântica dos cortes editoriais."""
|
||||
|
||||
import unittest
|
||||
|
||||
from engine.editor import AcaoDeEdicao, PlanoDeEdicao, TipoDeAcao
|
||||
from engine.editor.validacao_semantica import ValidadorSemanticoDePlano
|
||||
from engine.scanner.modelos import PalavraDeTranscricao, SegmentoDeTranscricao
|
||||
|
||||
|
||||
class TesteValidadorSemanticoDePlano(unittest.TestCase):
|
||||
"""Impede cortes de palavra e retomadas no meio de uma frase."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
"""Cria o validador com a margem segura usada pela aplicação real."""
|
||||
self.validador = ValidadorSemanticoDePlano(margem_minima_em_segundos=0.3)
|
||||
|
||||
def test_rejeita_caso_real_que_resultou_em_trazer_uma_seguranca_cortado(self) -> None:
|
||||
"""Reproduz as duas bordas problemáticas do último clipe aplicado."""
|
||||
falas = (
|
||||
SegmentoDeTranscricao(
|
||||
inicio=906.15,
|
||||
fim=934.63,
|
||||
texto="Eu acho que hoje, além de uma técnica boa, é você...",
|
||||
palavras=(
|
||||
PalavraDeTranscricao("você...", 934.10, 934.63),
|
||||
),
|
||||
),
|
||||
SegmentoDeTranscricao(
|
||||
inicio=934.65,
|
||||
fim=962.60,
|
||||
texto=(
|
||||
"conseguir achar um profissional que te passe confiança. "
|
||||
"Então é trazer uma segurança."
|
||||
),
|
||||
palavras=(
|
||||
PalavraDeTranscricao("conseguir", 934.65, 935.20),
|
||||
PalavraDeTranscricao("Então", 959.05, 959.55),
|
||||
PalavraDeTranscricao("é", 959.55, 960.45),
|
||||
PalavraDeTranscricao("trazer", 960.45, 961.81),
|
||||
PalavraDeTranscricao("uma", 961.94, 962.20),
|
||||
PalavraDeTranscricao("segurança.", 962.20, 962.60),
|
||||
),
|
||||
),
|
||||
SegmentoDeTranscricao(
|
||||
inicio=963.10,
|
||||
fim=970.0,
|
||||
texto="Eu recomendaria esse profissional.",
|
||||
palavras=(PalavraDeTranscricao("Eu", 963.10, 963.30),),
|
||||
),
|
||||
)
|
||||
plano = PlanoDeEdicao(
|
||||
"0E6A8829.MP4",
|
||||
(
|
||||
AcaoDeEdicao(TipoDeAcao.CORTE, 875.50, 934.65, "Remove trecho anterior."),
|
||||
AcaoDeEdicao(TipoDeAcao.CORTE, 962.60, 1018.22, "Encerra após a conclusão."),
|
||||
),
|
||||
)
|
||||
|
||||
problemas = self.validador.validar(plano, falas)
|
||||
|
||||
codigos = {problema.codigo for problema in problemas}
|
||||
self.assertIn("retomada_no_meio_da_frase", codigos)
|
||||
self.assertIn("margem_antes_do_corte_insuficiente", codigos)
|
||||
|
||||
def test_aceita_frases_completas_com_respiro_nas_duas_bordas(self) -> None:
|
||||
"""Aceita a remoção posicionada no silêncio entre frases completas."""
|
||||
falas = (
|
||||
SegmentoDeTranscricao(
|
||||
inicio=0.0,
|
||||
fim=4.0,
|
||||
texto="Esta é uma frase completa.",
|
||||
palavras=(PalavraDeTranscricao("completa.", 3.4, 4.0),),
|
||||
),
|
||||
SegmentoDeTranscricao(
|
||||
inicio=8.0,
|
||||
fim=12.0,
|
||||
texto="Esta é a próxima frase.",
|
||||
palavras=(PalavraDeTranscricao("Esta", 8.0, 8.4),),
|
||||
),
|
||||
)
|
||||
plano = PlanoDeEdicao(
|
||||
"video.mp4",
|
||||
(AcaoDeEdicao(TipoDeAcao.CORTE, 4.30, 7.70, "Remove silêncio longo."),),
|
||||
)
|
||||
|
||||
self.assertEqual(self.validador.validar(plano, falas), ())
|
||||
|
||||
def test_rejeita_borda_que_intercepta_uma_palavra(self) -> None:
|
||||
"""Distingue palavra realmente cortada de simples ausência de margem."""
|
||||
falas = (
|
||||
SegmentoDeTranscricao(
|
||||
inicio=0.0,
|
||||
fim=3.0,
|
||||
texto="Vamos trazer segurança.",
|
||||
palavras=(PalavraDeTranscricao("segurança.", 2.0, 3.0),),
|
||||
),
|
||||
)
|
||||
plano = PlanoDeEdicao(
|
||||
"video.mp4",
|
||||
(AcaoDeEdicao(TipoDeAcao.CORTE, 2.5, 5.0, "Corte inválido."),),
|
||||
)
|
||||
|
||||
problemas = self.validador.validar(plano, falas)
|
||||
|
||||
self.assertIn("palavra_interceptada", {problema.codigo for problema in problemas})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user