from collections.abc import Callable from dataclasses import dataclass, field from ..dominio import Clipe from ..integracoes.visual.contratos import (AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros, DetectorDeIntervalosDeCena, ExtratorDeQuadros) from ..integracoes.visual.modelos import QuadroDeVideo from .modelos import Cena, CenaVisual, EvidenciaVisual @dataclass(frozen=True) class ResultadoVisualDoClipe: identificador_do_clipe: str evidencias: list[EvidenciaVisual] = field(default_factory=list) cenas: list[Cena] = field(default_factory=list) cenas_visuais: list[CenaVisual] = field(default_factory=list) class DetectorDeCenas: """Módulo profundo que orquestra frames, analisadores e cenas de um clipe. A interface não expõe OpenCV, ONNX, MediaPipe, PySceneDetect ou Vision. Cada adapter pode ser trocado sem alterar chamadores nem resultados do domínio. """ def __init__(self, extrator: ExtratorDeQuadros, analisadores_de_frame: list[AnalisadorDeFrame] | None = None, detectores_de_intervalo: list[DetectorDeIntervalosDeCena] | None = None, analisadores_de_sequencia: list[AnalisadorDeSequenciaDeQuadros] | None = None, ao_progresso: Callable[[int, int], None] | None = None) -> None: self.extrator = extrator self.analisadores_de_frame = analisadores_de_frame or [] self.analisadores_de_sequencia = analisadores_de_sequencia or [] self.detectores_de_intervalo = detectores_de_intervalo or [] self.ao_progresso = ao_progresso def detectar(self, clipe: Clipe) -> ResultadoVisualDoClipe: quadros = self.extrator.extrair(clipe) total = len(quadros) * len(self.analisadores_de_frame) + len(self.analisadores_de_sequencia) progresso = [0] evidencias = self._analisar_quadros(quadros, progresso, total) + self._analisar_sequencia(quadros, progresso, total) cenas = self._detectar_intervalos(clipe, quadros) cenas_visuais = [CenaVisual( clipe.identificador, cena.inicio, cena.fim, tuple(item for item in evidencias if item.inicio <= cena.fim and item.fim >= cena.inicio), cena.referencias, ) for cena in cenas] return ResultadoVisualDoClipe(clipe.identificador, evidencias, cenas, cenas_visuais) def _analisar_quadros(self, quadros: list[QuadroDeVideo], progresso: list[int] | None = None, total: int = 0) -> list[EvidenciaVisual]: resultado: list[EvidenciaVisual] = [] for quadro in quadros: for analisador in self.analisadores_de_frame: resultado.extend(analisador.analisar(quadro)) self._avancar_progresso(progresso, total) return resultado def _analisar_sequencia(self, quadros: list[QuadroDeVideo], progresso: list[int] | None = None, total: int = 0) -> list[EvidenciaVisual]: resultado: list[EvidenciaVisual] = [] for analisador in self.analisadores_de_sequencia: resultado.extend(analisador.analisar(quadros)) self._avancar_progresso(progresso, total) return resultado def _avancar_progresso(self, progresso: list[int] | None, total: int) -> None: if progresso is None or total <= 0: return progresso[0] += 1 if self.ao_progresso: self.ao_progresso(progresso[0], total) def _detectar_intervalos(self, clipe: Clipe, quadros: list[QuadroDeVideo]) -> list[Cena]: cenas: list[Cena] = [] for detector in self.detectores_de_intervalo: cenas.extend(detector.detectar(clipe, quadros)) cenas = self._consolidar_cenas(cenas) if not cenas and quadros: cenas = [Cena(quadros[0].timestamp, quadros[-1].timestamp)] return cenas @staticmethod def _consolidar_cenas(cenas: list[Cena]) -> list[Cena]: resultado: list[Cena] = [] for cena in sorted(cenas, key=lambda item: (item.inicio, item.fim)): if resultado and cena.inicio <= resultado[-1].fim: anterior = resultado[-1] resultado[-1] = Cena(anterior.inicio, max(anterior.fim, cena.fim), anterior.confianca or cena.confianca, anterior.referencias + cena.referencias) else: resultado.append(cena) return resultado class AnaliseVisualDaTimeline: """Etapa do Scanner que guarda evidências e cenas por clipe no contexto.""" nome = "analise_visual_local" def __init__(self, detector: DetectorDeCenas, continuar_em_falha: bool = True) -> None: self.detector = detector self.continuar_em_falha = continuar_em_falha def executar(self, contexto): if contexto.timeline is None: return contexto for faixa in contexto.timeline.faixas: for clipe in faixa.clipes: if clipe.offline or not clipe.arquivo: continue try: resultado = self.detector.detectar(clipe) except Exception as exc: if not self.continuar_em_falha: raise contexto.avisos.append( f"analise_visual_indisponivel: clipe {clipe.identificador}: {exc}" ) contexto.caracteristicas_visuais[clipe.identificador] = { "evidencias": [], "cenas": [], "cenas_visuais": [], "disponivel": False, } continue contexto.caracteristicas_visuais[clipe.identificador] = { "evidencias": resultado.evidencias, "cenas": resultado.cenas, "cenas_visuais": resultado.cenas_visuais, "disponivel": True, } contexto.cenas.extend(resultado.cenas) contexto.cenas_visuais.extend(resultado.cenas_visuais) contexto.cenas = DetectorDeCenas._consolidar_cenas(contexto.cenas) return contexto def criar_detector_visual_local( diretorio_de_cache: str | None = ".frames", intervalo_em_segundos: float = 1.0, ) -> DetectorDeCenas: """Monta o detector local padrão com uma interface curta para o Scanner.""" from ..integracoes.visual import ( AnalisadorDeComposicaoOpenCV, AnalisadorDeContinuidadeOpenCV, AnalisadorDeQualidadeOpenCV, ExtratorDeQuadrosOpenCV, ) return DetectorDeCenas( ExtratorDeQuadrosOpenCV( intervalo_em_segundos=intervalo_em_segundos, diretorio_de_cache=diretorio_de_cache, ), analisadores_de_frame=[ AnalisadorDeQualidadeOpenCV(), AnalisadorDeComposicaoOpenCV(), ], analisadores_de_sequencia=[AnalisadorDeContinuidadeOpenCV()], ) def criar_detector_apple_vision( diretorio_de_cache: str | None = ".frames", intervalo_em_segundos: float = 1.0, configuracao=None, ) -> DetectorDeCenas: """Monta a análise local de cena baseada em Vision e arquivos de origem.""" from .configuracao_visual import ConfiguracaoVisualDoScanner from ..integracoes.visual import (AnalisadorAppleVisionNativo, AnalisadorSequenciaAppleVisionNativo, ExtratorDeQuadrosFFmpeg) perfil = configuracao or ConfiguracaoVisualDoScanner(intervalo_em_segundos=intervalo_em_segundos) return DetectorDeCenas( ExtratorDeQuadrosFFmpeg(intervalo_em_segundos=perfil.intervalo_em_segundos, diretorio_de_cache=diretorio_de_cache or ".frames"), analisadores_de_frame=[AnalisadorAppleVisionNativo( recursos=tuple(sorted(perfil.recursos_vision)), interpretar_com_apple_intelligence=perfil.interpretar_cena, )], analisadores_de_sequencia=[AnalisadorSequenciaAppleVisionNativo()] if perfil.analisar_continuidade else [], )