"""Perfil declarativo que liga a configuração do painel à leitura visual.""" from dataclasses import dataclass from typing import Any from ..integracoes.midia import RESOLUCAO_PADRAO_DO_PROXY RECURSOS_VISION = frozenset(( "faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "categorias", "estetica", "codigos", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas", )) # Sensibilidades aceitas para a análise visual auxiliar e o limiar do # detector de cortes (PySceneDetect). Valores MENORES de limiar são mais # sensíveis — detectam mais cortes. O default segue o PySceneDetect (27). SENSIBILIDADES_VISUAIS: dict[str, float] = { "muito_baixa": 45.0, "baixa": 35.0, "media": 27.0, "alta": 20.0, "muito_alta": 14.0, } def _inteiro_ou_nulo(valor: Any) -> int | None: """Converte um valor de perfil em int, preservando None.""" if valor is None or valor == "": return None return int(valor) @dataclass(frozen=True) class ConfiguracaoVisualDoScanner: """Interface curta para uma execução de leitura visual da timeline.""" intervalo_em_segundos: float = 1.0 resolucao_maxima: int = 1080 faixas_de_video: tuple[int, ...] = () faixas_de_audio: tuple[int, ...] = () modo_de_analise: str = "ambos" metricas_de_fala: bool = False recursos_vision: frozenset[str] = RECURSOS_VISION detectar_cenas: bool = True analisar_continuidade: bool = True preparar_reenquadramento: bool = False interpretar_cena: bool = True # Análise visual auxiliar (PySceneDetect) — desligada por padrão. usar_analise_visual: bool = False detectar_cortes: bool = True sensibilidade: str = "media" # Leitura via proxy compactado. Para arquivos de origem pesados (MOV de # vários GB), ler os frames de uma cópia leve é muito mais rápido. usar_proxy: bool = True resolucao_do_proxy: int = RESOLUCAO_PADRAO_DO_PROXY # Limita quantos frames por clipe recebem a interpretação editorial da Apple # Intelligence (Foundation Models) — a etapa mais cara. None interpreta todos. maximo_de_frames_interpretados: int | None = None def __post_init__(self) -> None: if self.modo_de_analise not in {"som", "imagem", "ambos"}: raise ValueError("modo_de_analise deve ser som, imagem ou ambos.") if not 0.04 <= self.intervalo_em_segundos <= 60: raise ValueError("intervalo_em_segundos deve estar entre 0,04 e 60.") if self.resolucao_maxima != 0 and not 240 <= self.resolucao_maxima <= 8640: raise ValueError("resolucao_maxima deve ser 0 (original) ou estar entre 240 e 8640.") desconhecidos = self.recursos_vision - RECURSOS_VISION if desconhecidos: raise ValueError(f"Recursos Vision desconhecidos: {', '.join(sorted(desconhecidos))}") if any(indice < 0 for indice in self.faixas_de_video + self.faixas_de_audio): raise ValueError("Índices de faixa não podem ser negativos.") if self.sensibilidade not in SENSIBILIDADES_VISUAIS: raise ValueError( f"Sensibilidade inválida: {self.sensibilidade}. " f"Use um de {', '.join(SENSIBILIDADES_VISUAIS)}.") @property def limiar_de_corte(self) -> float: """Limiar do ContentDetector para a sensibilidade configurada.""" return SENSIBILIDADES_VISUAIS[self.sensibilidade] @classmethod def de_dict(cls, dados: dict[str, Any]) -> "ConfiguracaoVisualDoScanner": """Lê o mesmo JSON produzido pelo painel, sem vazar detalhes de adapters.""" return cls( intervalo_em_segundos=float(dados.get("intervalo_em_segundos", 1)), resolucao_maxima=int(dados.get("resolucao_maxima", 1080)), faixas_de_video=tuple(sorted(set(int(item) for item in dados.get("faixas_de_video", ())))), faixas_de_audio=tuple(sorted(set(int(item) for item in dados.get("faixas_de_audio", ())))), modo_de_analise=str(dados.get("modo_de_analise", "ambos")), metricas_de_fala=bool(dados.get("metricas_de_fala", False)), recursos_vision=frozenset(dados.get("recursos_vision", RECURSOS_VISION)), detectar_cenas=bool(dados.get("detectar_cenas", True)), analisar_continuidade=bool(dados.get("analisar_continuidade", True)), preparar_reenquadramento=bool(dados.get("preparar_reenquadramento", False)), interpretar_cena=bool(dados.get("interpretar_cena", True)), usar_analise_visual=bool(dados.get("usar_analise_visual", False)), detectar_cortes=bool(dados.get("detectar_cortes", True)), sensibilidade=str(dados.get("sensibilidade", "media")), usar_proxy=bool(dados.get("usar_proxy", True)), resolucao_do_proxy=int(dados.get("resolucao_do_proxy", RESOLUCAO_PADRAO_DO_PROXY)), maximo_de_frames_interpretados=_inteiro_ou_nulo(dados.get("maximo_de_frames_interpretados", None)), ) def para_dict(self) -> dict[str, Any]: return { "versao": 1, "intervalo_em_segundos": self.intervalo_em_segundos, "resolucao_maxima": self.resolucao_maxima, "faixas_de_video": list(self.faixas_de_video), "faixas_de_audio": list(self.faixas_de_audio), "modo_de_analise": self.modo_de_analise, "metricas_de_fala": self.metricas_de_fala, "recursos_vision": sorted(self.recursos_vision), "detectar_cenas": self.detectar_cenas, "analisar_continuidade": self.analisar_continuidade, "preparar_reenquadramento": self.preparar_reenquadramento, "interpretar_cena": self.interpretar_cena, "usar_analise_visual": self.usar_analise_visual, "detectar_cortes": self.detectar_cortes, "sensibilidade": self.sensibilidade, "usar_proxy": self.usar_proxy, "resolucao_do_proxy": self.resolucao_do_proxy, "maximo_de_frames_interpretados": self.maximo_de_frames_interpretados, }