"""Adapter Python para o runner Swift que concentra Vision e Apple Intelligence.""" import json import os from pathlib import Path import subprocess import tempfile from typing import Any, Callable from ...scanner.modelos import EvidenciaVisual from .contratos import AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros from .modelos import QuadroDeVideo RECURSOS_PADRAO = ( "faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "categorias", "estetica", "codigos", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas", ) class ExecutorDoRunnerApple: """Compila o runner Swift quando necessário e o executa em processo isolado.""" def __init__(self, fonte: str | Path | None = None, compilador: str = "swiftc", diretorio_de_build: str | Path | None = None) -> None: self.fonte = Path(fonte) if fonte else Path(__file__).with_name("apple_vision_runner.swift") self.compilador = compilador self.diretorio_de_build = Path(diretorio_de_build) if diretorio_de_build else ( Path(tempfile.gettempdir()) / "jhonny-apple-vision") def executar(self, carga: dict[str, Any], timeout: float) -> dict[str, Any]: executavel = self._garantir_compilado() processo = subprocess.run([str(executavel)], input=json.dumps(carga), capture_output=True, text=True, timeout=timeout) if processo.returncode != 0: detalhe = processo.stderr.strip() or "runner Apple terminou sem diagnóstico" raise RuntimeError(f"Runner Apple Vision falhou: {detalhe}") try: return json.loads(processo.stdout) except json.JSONDecodeError as exc: raise RuntimeError(f"Runner Apple Vision devolveu JSON inválido: {processo.stdout!r}") from exc def _garantir_compilado(self) -> Path: if not self.fonte.is_file(): raise FileNotFoundError(f"Fonte do runner Apple não encontrada: {self.fonte}") self.diretorio_de_build.mkdir(parents=True, exist_ok=True) executavel = self.diretorio_de_build / "apple-vision-runner" if not executavel.exists() or executavel.stat().st_mtime < self.fonte.stat().st_mtime: ambiente = os.environ | {"CLANG_MODULE_CACHE_PATH": str(self.diretorio_de_build / "module-cache")} processo = subprocess.run([self.compilador, "-parse-as-library", str(self.fonte), "-o", str(executavel)], capture_output=True, text=True, timeout=120, env=ambiente) if processo.returncode != 0: raise RuntimeError(f"Não foi possível compilar runner Apple Vision: {processo.stderr.strip()}") return executavel class AnalisadorAppleVisionNativo(AnalisadorDeFrame): """Módulo profundo: pede fatos visuais nativos e opcionalmente interpretação local. A interface recebe somente um `QuadroDeVideo`; Vision, Foundation Models, Swift, compilação e erros nativos permanecem atrás deste adapter. """ nome = "apple_vision" def __init__(self, recursos: tuple[str, ...] = RECURSOS_PADRAO, interpretar_com_apple_intelligence: bool = True, executor: ExecutorDoRunnerApple | None = None, timeout_em_segundos: float = 90.0) -> None: self.recursos = recursos self.interpretar_com_apple_intelligence = interpretar_com_apple_intelligence self.executor = executor or ExecutorDoRunnerApple() self.timeout_em_segundos = timeout_em_segundos def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]: if quadro.caminho is None: raise ValueError("Apple Vision requer que o frame tenha caminho persistido em disco.") dados = self.executor.executar({"frame": str(quadro.caminho), "recursos": list(self.recursos), "resumir": self.interpretar_com_apple_intelligence}, self.timeout_em_segundos) evidencias = [self._converter(item, quadro) for item in dados.get("evidencias", [])] avisos = dados.get("avisos", []) if avisos: evidencias.append(EvidenciaVisual("diagnostico_apple_vision", quadro.timestamp, quadro.timestamp, {"avisos": avisos, "recursos_solicitados": list(self.recursos)}, provider=self.nome)) return evidencias def _converter(self, item: dict[str, Any], quadro: QuadroDeVideo) -> EvidenciaVisual: return EvidenciaVisual(item["tipo"], quadro.timestamp, quadro.timestamp, item["valor"], confianca=item.get("confianca"), provider=self.nome, modelo=item.get("modelo")) class AnalisadorSequenciaAppleVisionNativo(AnalisadorDeSequenciaDeQuadros): """Compara frames com feature prints nativos sem expor o protocolo Swift.""" nome = "apple_vision_sequencia" def __init__(self, executor: ExecutorDoRunnerApple | None = None, timeout_em_segundos: float = 90.0) -> None: self.executor = executor or ExecutorDoRunnerApple() self.timeout_em_segundos = timeout_em_segundos def analisar(self, quadros: list[QuadroDeVideo]) -> list[EvidenciaVisual]: if len(quadros) < 2: return [] if any(quadro.caminho is None for quadro in quadros): raise ValueError("Apple Vision de sequência requer frames persistidos em disco.") dados = self.executor.executar({"frames": [str(quadro.caminho) for quadro in quadros], "recursos": [], "resumir": False}, self.timeout_em_segundos) evidencias: list[EvidenciaVisual] = [] for item in dados.get("evidencias", []): valor = dict(item["valor"]) inicio = int(valor.pop("frame_inicial", 0)) fim = int(valor.pop("frame_final", inicio + 1)) if inicio < 0 or fim >= len(quadros) or fim < inicio: raise RuntimeError("Runner Apple Vision devolveu índices temporais inválidos.") evidencias.append(EvidenciaVisual(item["tipo"], quadros[inicio].timestamp, quadros[fim].timestamp, valor, confianca=item.get("confianca"), provider=self.nome, modelo=item.get("modelo"))) avisos = dados.get("avisos", []) if avisos: evidencias.append(EvidenciaVisual("diagnostico_apple_vision", quadros[0].timestamp, quadros[-1].timestamp, {"avisos": avisos}, provider=self.nome)) return evidencias