"""Verificação estrutural da timeline depois da aplicação de um plano. Compara a cobertura de origem que deveria sobreviver aos cortes com a cobertura realmente lida no Premiere depois da mutação. A verificação não avalia pixels, áudio ou qualidade editorial; ela detecta sobras, lacunas e desalinhamentos estruturais em cada tipo de faixa. """ from __future__ import annotations from dataclasses import dataclass from ..dominio import Clipe, Faixa, IntervaloDeTempo, Timeline from .modelos import PlanoDeEdicao, TipoDeAcao _TOLERANCIA_EM_SEGUNDOS = 0.05 @dataclass(frozen=True) class ResultadoDaVerificacao: """Resultado da comparação entre a timeline esperada e a timeline lida.""" aprovada: bool divergencias: tuple[str, ...] = () avisos: tuple[str, ...] = () @property def mensagem(self) -> str: """Resume a verificação em uma mensagem adequada para o histórico.""" if self.aprovada: mensagem = "verificação pós-aplicação aprovada" else: mensagem = "verificação pós-aplicação reprovada: " + " | ".join(self.divergencias) if self.avisos: mensagem += "; avisos: " + " | ".join(self.avisos) return mensagem class VerificadorDaAplicacao: """Confere se a cobertura de mídia depois da aplicação bate com o plano.""" def __init__(self, tolerancia_em_segundos: float = _TOLERANCIA_EM_SEGUNDOS) -> None: """Configura a tolerância de arredondamento do Premiere.""" if tolerancia_em_segundos < 0: raise ValueError("A tolerância da verificação não pode ser negativa.") self._tolerancia = tolerancia_em_segundos def verificar( self, timeline_antes: Timeline, timeline_depois: Timeline, plano: PlanoDeEdicao, ) -> ResultadoDaVerificacao: """Compara os intervalos de origem sobreviventes antes e depois da aplicação.""" tipos_de_faixa = {faixa.tipo for faixa in timeline_antes.faixas} divergencias: list[str] = [] avisos: list[str] = [] cortes = tuple( acao for acao in plano.acoes if acao.tipo is TipoDeAcao.CORTE ) for tipo_de_faixa in sorted(tipos_de_faixa): esperados = self._intervalos_esperados( timeline_antes, tipo_de_faixa, plano.arquivo_de_origem, cortes ) encontrados = self._intervalos_da_timeline( timeline_depois, tipo_de_faixa, plano.arquivo_de_origem ) if not self._intervalos_equivalentes(esperados, encontrados): divergencias.append( f"faixa {tipo_de_faixa}: esperado {self._formatar_intervalos(esperados)}, " f"encontrado {self._formatar_intervalos(encontrados)}" ) avisos.extend(self._avisar_material_posterior_à_ultima_acao( timeline_depois, plano, tipo_de_faixa )) return ResultadoDaVerificacao(not divergencias, tuple(divergencias), tuple(avisos)) def _avisar_material_posterior_à_ultima_acao( self, timeline: Timeline, plano: PlanoDeEdicao, tipo_de_faixa: str ) -> list[str]: """Sinaliza material depois da última ação para revisão editorial.""" if not plano.acoes: return [] ultima_acao = max(plano.acoes, key=lambda acao: acao.fim) if ultima_acao.tipo is not TipoDeAcao.CORTE: return [] sobras = [ clipe.intervalo_na_origem.fim for faixa in timeline.faixas if faixa.tipo == tipo_de_faixa for clipe in self._clipes_do_arquivo(faixa, plano.arquivo_de_origem) if clipe.intervalo_na_origem.inicio >= ultima_acao.fim - self._tolerancia and clipe.intervalo_na_origem.fim > ultima_acao.fim + self._tolerancia ] if not sobras: return [] return [ f"faixa {tipo_de_faixa}: há material após a última ação, " f"a partir de {ultima_acao.fim:.2f}s; conferir possível sobra" ] def _intervalos_esperados( self, timeline: Timeline, tipo_de_faixa: str, arquivo_de_origem: str, cortes: tuple, ) -> tuple[tuple[float, float], ...]: intervalos: list[tuple[float, float]] = [] for faixa in timeline.faixas: if faixa.tipo != tipo_de_faixa: continue for clipe in self._clipes_do_arquivo(faixa, arquivo_de_origem): intervalos.extend(self._subtrair_cortes(clipe.intervalo_na_origem, cortes)) return self._normalizar_intervalos(intervalos) def _intervalos_da_timeline( self, timeline: Timeline, tipo_de_faixa: str, arquivo_de_origem: str ) -> tuple[tuple[float, float], ...]: intervalos = [ (clipe.intervalo_na_origem.inicio, clipe.intervalo_na_origem.fim) for faixa in timeline.faixas if faixa.tipo == tipo_de_faixa for clipe in self._clipes_do_arquivo(faixa, arquivo_de_origem) ] return self._normalizar_intervalos(intervalos) def _subtrair_cortes(self, intervalo: IntervaloDeTempo, cortes: tuple) -> list[tuple[float, float]]: partes = [(intervalo.inicio, intervalo.fim)] for corte in cortes: novas_partes: list[tuple[float, float]] = [] for inicio, fim in partes: if corte.fim <= inicio or corte.inicio >= fim: novas_partes.append((inicio, fim)) continue if inicio < corte.inicio: novas_partes.append((inicio, min(fim, corte.inicio))) if corte.fim < fim: novas_partes.append((max(inicio, corte.fim), fim)) partes = novas_partes return partes def _intervalos_equivalentes( self, esperados: tuple[tuple[float, float], ...], encontrados: tuple[tuple[float, float], ...] ) -> bool: if len(esperados) != len(encontrados): return False return all( abs(inicio_esperado - inicio_encontrado) <= self._tolerancia and abs(fim_esperado - fim_encontrado) <= self._tolerancia for (inicio_esperado, fim_esperado), (inicio_encontrado, fim_encontrado) in zip(esperados, encontrados) ) def _clipes_do_arquivo(self, faixa: Faixa, arquivo_de_origem: str) -> list[Clipe]: nome_alvo = arquivo_de_origem.rsplit("/", 1)[-1].lower() return [ clipe for clipe in faixa.clipes if clipe.intervalo_na_origem is not None and (clipe.nome or "").rsplit("/", 1)[-1].lower() == nome_alvo ] def _normalizar_intervalos( self, intervalos: list[tuple[float, float]] ) -> tuple[tuple[float, float], ...]: ordenados = sorted(intervalos) unidos: list[list[float]] = [] for inicio, fim in ordenados: if fim <= inicio + self._tolerancia: continue if unidos and inicio <= unidos[-1][1] + self._tolerancia: unidos[-1][1] = max(unidos[-1][1], fim) else: unidos.append([inicio, fim]) return tuple((inicio, fim) for inicio, fim in unidos) @staticmethod def _formatar_intervalos(intervalos: tuple[tuple[float, float], ...]) -> str: """Formata intervalos para mensagens curtas e conferíveis.""" if not intervalos: return "[]" return "[" + ", ".join(f"{inicio:.2f}-{fim:.2f}s" for inicio, fim in intervalos) + "]"