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:
@@ -0,0 +1,227 @@
|
||||
"""Validação semântica das bordas de cortes antes de alterar a timeline."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from engine.scanner.modelos import PalavraDeTranscricao, SegmentoDeTranscricao
|
||||
|
||||
from .erros import ErroDePlanoInvalido
|
||||
from .modelos import AcaoDeEdicao, PlanoDeEdicao, TipoDeAcao
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProblemaSemanticoDoPlano:
|
||||
"""Descreve uma borda insegura e a ação em que ela foi encontrada."""
|
||||
|
||||
codigo: str
|
||||
mensagem: str
|
||||
indice_da_acao: int
|
||||
instante: float
|
||||
|
||||
|
||||
class ValidadorSemanticoDePlano:
|
||||
"""Protege palavras, frases completas e respiros nas emendas de cortes."""
|
||||
|
||||
_TOLERANCIA_EM_SEGUNDOS = 0.01
|
||||
_PONTUACAO_DE_FECHAMENTO = (".", "!", "?", "…")
|
||||
|
||||
def __init__(self, margem_minima_em_segundos: float = 0.3) -> None:
|
||||
"""Configura a folga acústica mínima exigida junto à fala mantida."""
|
||||
if margem_minima_em_segundos < 0:
|
||||
raise ValueError("A margem mínima não pode ser negativa.")
|
||||
self._margem_minima = margem_minima_em_segundos
|
||||
|
||||
def validar(
|
||||
self,
|
||||
plano: PlanoDeEdicao,
|
||||
falas: tuple[SegmentoDeTranscricao, ...],
|
||||
) -> tuple[ProblemaSemanticoDoPlano, ...]:
|
||||
"""Devolve todos os problemas semânticos encontrados nas ações de corte."""
|
||||
cortes = tuple(
|
||||
(indice, acao)
|
||||
for indice, acao in enumerate(plano.acoes)
|
||||
if acao.tipo is TipoDeAcao.CORTE
|
||||
)
|
||||
palavras = tuple(
|
||||
sorted(
|
||||
(palavra for fala in falas for palavra in fala.palavras),
|
||||
key=lambda palavra: (palavra.inicio, palavra.fim),
|
||||
)
|
||||
)
|
||||
problemas: list[ProblemaSemanticoDoPlano] = []
|
||||
for indice, corte in cortes:
|
||||
problemas.extend(self._validar_palavras_interceptadas(indice, corte, palavras))
|
||||
problemas.extend(self._validar_borda_inicial(indice, corte, cortes, palavras))
|
||||
problemas.extend(self._validar_borda_final(indice, corte, cortes, falas, palavras))
|
||||
return tuple(problemas)
|
||||
|
||||
def garantir_valido(
|
||||
self,
|
||||
plano: PlanoDeEdicao,
|
||||
falas: tuple[SegmentoDeTranscricao, ...],
|
||||
) -> None:
|
||||
"""Levanta erro explicativo quando qualquer corte ameaça a fala mantida."""
|
||||
problemas = self.validar(plano, falas)
|
||||
if not problemas:
|
||||
return
|
||||
detalhes = " | ".join(problema.mensagem for problema in problemas)
|
||||
raise ErroDePlanoInvalido(
|
||||
"Plano bloqueado pela validação de integridade da fala: " + detalhes
|
||||
)
|
||||
|
||||
def _validar_palavras_interceptadas(
|
||||
self,
|
||||
indice: int,
|
||||
corte: AcaoDeEdicao,
|
||||
palavras: tuple[PalavraDeTranscricao, ...],
|
||||
) -> list[ProblemaSemanticoDoPlano]:
|
||||
problemas = []
|
||||
for borda, instante in (("início", corte.inicio), ("fim", corte.fim)):
|
||||
palavra = next(
|
||||
(
|
||||
item
|
||||
for item in palavras
|
||||
if item.inicio + self._TOLERANCIA_EM_SEGUNDOS
|
||||
< instante
|
||||
< item.fim - self._TOLERANCIA_EM_SEGUNDOS
|
||||
),
|
||||
None,
|
||||
)
|
||||
if palavra is not None:
|
||||
problemas.append(ProblemaSemanticoDoPlano(
|
||||
codigo="palavra_interceptada",
|
||||
mensagem=(
|
||||
f"ação {indice + 1}: o {borda} em {instante:.2f}s corta "
|
||||
f'a palavra "{palavra.texto}"'
|
||||
),
|
||||
indice_da_acao=indice,
|
||||
instante=instante,
|
||||
))
|
||||
return problemas
|
||||
|
||||
def _validar_borda_inicial(
|
||||
self,
|
||||
indice: int,
|
||||
corte: AcaoDeEdicao,
|
||||
cortes: tuple[tuple[int, AcaoDeEdicao], ...],
|
||||
palavras: tuple[PalavraDeTranscricao, ...],
|
||||
) -> list[ProblemaSemanticoDoPlano]:
|
||||
palavra_anterior = self._ultima_palavra_mantida_antes(corte.inicio, cortes, palavras)
|
||||
if palavra_anterior is None:
|
||||
return []
|
||||
problemas = []
|
||||
margem = corte.inicio - palavra_anterior.fim
|
||||
if margem < self._margem_minima - self._TOLERANCIA_EM_SEGUNDOS:
|
||||
problemas.append(ProblemaSemanticoDoPlano(
|
||||
codigo="margem_antes_do_corte_insuficiente",
|
||||
mensagem=(
|
||||
f"ação {indice + 1}: deixe ao menos {self._margem_minima:.2f}s após "
|
||||
f'a palavra mantida "{palavra_anterior.texto}" antes de iniciar o corte'
|
||||
),
|
||||
indice_da_acao=indice,
|
||||
instante=corte.inicio,
|
||||
))
|
||||
if not palavra_anterior.texto.rstrip().endswith(self._PONTUACAO_DE_FECHAMENTO):
|
||||
problemas.append(ProblemaSemanticoDoPlano(
|
||||
codigo="frase_interrompida_antes_do_corte",
|
||||
mensagem=(
|
||||
f"ação {indice + 1}: a fala mantida termina em "
|
||||
f'"{palavra_anterior.texto}" sem concluir a frase'
|
||||
),
|
||||
indice_da_acao=indice,
|
||||
instante=corte.inicio,
|
||||
))
|
||||
return problemas
|
||||
|
||||
def _validar_borda_final(
|
||||
self,
|
||||
indice: int,
|
||||
corte: AcaoDeEdicao,
|
||||
cortes: tuple[tuple[int, AcaoDeEdicao], ...],
|
||||
falas: tuple[SegmentoDeTranscricao, ...],
|
||||
palavras: tuple[PalavraDeTranscricao, ...],
|
||||
) -> list[ProblemaSemanticoDoPlano]:
|
||||
proxima_palavra = self._primeira_palavra_mantida_depois(corte.fim, cortes, palavras)
|
||||
if proxima_palavra is None:
|
||||
return []
|
||||
problemas = []
|
||||
margem = proxima_palavra.inicio - corte.fim
|
||||
if margem < self._margem_minima - self._TOLERANCIA_EM_SEGUNDOS:
|
||||
problemas.append(ProblemaSemanticoDoPlano(
|
||||
codigo="margem_depois_do_corte_insuficiente",
|
||||
mensagem=(
|
||||
f"ação {indice + 1}: termine o corte ao menos {self._margem_minima:.2f}s "
|
||||
f'antes da palavra mantida "{proxima_palavra.texto}"'
|
||||
),
|
||||
indice_da_acao=indice,
|
||||
instante=corte.fim,
|
||||
))
|
||||
fala = self._fala_da_palavra(proxima_palavra, falas)
|
||||
comeca_no_meio_do_segmento = (
|
||||
fala is not None
|
||||
and proxima_palavra.inicio > fala.inicio + self._TOLERANCIA_EM_SEGUNDOS
|
||||
)
|
||||
primeira_letra = next((letra for letra in proxima_palavra.texto if letra.isalpha()), "")
|
||||
comeca_em_minuscula = bool(primeira_letra and primeira_letra.islower())
|
||||
if comeca_no_meio_do_segmento or comeca_em_minuscula:
|
||||
problemas.append(ProblemaSemanticoDoPlano(
|
||||
codigo="retomada_no_meio_da_frase",
|
||||
mensagem=(
|
||||
f"ação {indice + 1}: o trecho mantido retoma em "
|
||||
f'"{proxima_palavra.texto}" ({proxima_palavra.inicio:.2f}s), no meio da frase'
|
||||
),
|
||||
indice_da_acao=indice,
|
||||
instante=corte.fim,
|
||||
))
|
||||
return problemas
|
||||
|
||||
@staticmethod
|
||||
def _fala_da_palavra(
|
||||
palavra: PalavraDeTranscricao,
|
||||
falas: tuple[SegmentoDeTranscricao, ...],
|
||||
) -> SegmentoDeTranscricao | None:
|
||||
return next(
|
||||
(
|
||||
fala
|
||||
for fala in falas
|
||||
if fala.inicio - 0.01 <= palavra.inicio <= fala.fim + 0.01
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _palavra_esta_mantida(
|
||||
palavra: PalavraDeTranscricao,
|
||||
cortes: tuple[tuple[int, AcaoDeEdicao], ...],
|
||||
) -> bool:
|
||||
centro = (palavra.inicio + palavra.fim) / 2
|
||||
return not any(corte.inicio <= centro <= corte.fim for _, corte in cortes)
|
||||
|
||||
def _ultima_palavra_mantida_antes(
|
||||
self,
|
||||
instante: float,
|
||||
cortes: tuple[tuple[int, AcaoDeEdicao], ...],
|
||||
palavras: tuple[PalavraDeTranscricao, ...],
|
||||
) -> PalavraDeTranscricao | None:
|
||||
candidatas = (
|
||||
palavra
|
||||
for palavra in palavras
|
||||
if palavra.fim <= instante + self._TOLERANCIA_EM_SEGUNDOS
|
||||
and self._palavra_esta_mantida(palavra, cortes)
|
||||
)
|
||||
return max(candidatas, key=lambda palavra: palavra.fim, default=None)
|
||||
|
||||
def _primeira_palavra_mantida_depois(
|
||||
self,
|
||||
instante: float,
|
||||
cortes: tuple[tuple[int, AcaoDeEdicao], ...],
|
||||
palavras: tuple[PalavraDeTranscricao, ...],
|
||||
) -> PalavraDeTranscricao | None:
|
||||
candidatas = (
|
||||
palavra
|
||||
for palavra in palavras
|
||||
if palavra.inicio >= instante - self._TOLERANCIA_EM_SEGUNDOS
|
||||
and self._palavra_esta_mantida(palavra, cortes)
|
||||
)
|
||||
return min(candidatas, key=lambda palavra: palavra.inicio, default=None)
|
||||
Reference in New Issue
Block a user