criado repositório jhonny-editor no Gitea adicionado script admin/deploy.command com commit automático atualizado admin/DEV-NOTES.md com template limpo Resumo: - 32 arquivos alterados - 16 novos - 15 modificados - 0 removidos 16 files changed, 638 insertions(+), 61 deletions(-) Arquivos: - code/engine/skills/boas-praticas-oo.md -> .agents/skills/boas-praticas-oo/SKILL.md - AGENTS.md - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/integracoes/visual/apple_vision.py - code/engine/integracoes/visual/apple_vision_runner.swift - code/engine/integracoes/visual/extrator_ffmpeg.py - code/engine/scanner/__init__.py - code/engine/scanner/configuracao_visual.py - code/engine/scanner/coordenacao/__init__.py - code/engine/scanner/retakes/adaptador_de_falas.py - code/engine/scanner/retakes/agrupador_de_takes.py - code/engine/scanner/retakes/detector_de_reinicios.py - code/engine/scanner/retakes/modelos_de_retakes.py - code/engine/scanner/visual.py - .jhonny/ - .retakes/ - CODING_STANDARDS.md - code/engine/executar_retakes.py - code/engine/persistencia/ - code/engine/scanner/retakes/__init__.py - code/engine/scanner/retakes/carregador_de_artefatos.py - code/engine/scanner/retakes/classificador_de_retakes.py - code/engine/scanner/retakes/coordenador_de_retakes.py - code/engine/scanner/retakes/deteccao_de_retakes.py - code/engine/scanner/retakes/gerador_de_evidencias.py - code/engine/scanner/retakes/repositorio_de_retakes.py - code/engine/testes/test_consultas_de_persistencia.py - code/engine/testes/test_deteccao_de_retakes.py - code/engine/testes/test_persistencia_sqlite.py - code/relatorios/audio/arquivos/92f97877259a86a8095b1eecafdefe357212a12247d04b78df60e0c0ae38b2ea/
204 lines
9.5 KiB
Python
204 lines
9.5 KiB
Python
"""Adapter Python para o runner Swift que concentra Vision e Apple Intelligence."""
|
|
|
|
import atexit
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import queue
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
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 _TrabalhadorPersistente:
|
|
"""Um processo do runner Swift vivo entre chamadas, evitando recarregar Vision/Foundation Models a cada frame."""
|
|
|
|
def __init__(self, executavel: Path) -> None:
|
|
self.processo = subprocess.Popen([str(executavel)], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, text=True, bufsize=1)
|
|
|
|
def executar(self, carga: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
if self.processo.poll() is not None or not self.processo.stdin or not self.processo.stdout:
|
|
raise RuntimeError("Runner Apple Vision não está mais em execução.")
|
|
try:
|
|
self.processo.stdin.write(json.dumps(carga) + "\n")
|
|
self.processo.stdin.flush()
|
|
except (BrokenPipeError, ValueError) as exc:
|
|
raise RuntimeError("Runner Apple Vision não aceitou o pedido.") from exc
|
|
|
|
resposta: dict[str, str] = {}
|
|
falha: list[BaseException] = []
|
|
|
|
def ler() -> None:
|
|
try:
|
|
resposta["linha"] = self.processo.stdout.readline() # type: ignore[union-attr]
|
|
except BaseException as exc: # noqa: BLE001 - repassado à thread principal
|
|
falha.append(exc)
|
|
|
|
leitor = threading.Thread(target=ler, daemon=True)
|
|
leitor.start()
|
|
leitor.join(timeout)
|
|
if leitor.is_alive():
|
|
self.encerrar()
|
|
raise TimeoutError(f"Runner Apple Vision excedeu {timeout}s.")
|
|
if falha:
|
|
raise falha[0]
|
|
linha = resposta.get("linha", "")
|
|
if not linha:
|
|
detalhe = (self.processo.stderr.read() if self.processo.stderr else "") or "runner encerrou sem resposta"
|
|
raise RuntimeError(f"Runner Apple Vision falhou: {detalhe.strip()}")
|
|
return json.loads(linha)
|
|
|
|
def encerrar(self) -> None:
|
|
if self.processo.poll() is None:
|
|
self.processo.kill()
|
|
try:
|
|
self.processo.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:
|
|
pass
|
|
|
|
|
|
class ExecutorDoRunnerApple:
|
|
"""Compila o runner Swift quando necessário e mantém um pool de processos persistentes."""
|
|
|
|
def __init__(self, fonte: str | Path | None = None, compilador: str = "swiftc",
|
|
diretorio_de_build: str | Path | None = None, trabalhadores: int = 4) -> 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")
|
|
self.trabalhadores = max(1, trabalhadores)
|
|
self._pool: "queue.Queue[_TrabalhadorPersistente]" | None = None
|
|
self._trava = threading.Lock()
|
|
self._todos: list[_TrabalhadorPersistente] = []
|
|
atexit.register(self.encerrar)
|
|
|
|
def executar(self, carga: dict[str, Any], timeout: float) -> dict[str, Any]:
|
|
executavel = self._garantir_compilado()
|
|
pool = self._garantir_pool(executavel)
|
|
trabalhador = pool.get()
|
|
try:
|
|
return trabalhador.executar(carga, timeout)
|
|
except Exception:
|
|
trabalhador.encerrar()
|
|
with self._trava:
|
|
self._todos.remove(trabalhador)
|
|
trabalhador = _TrabalhadorPersistente(executavel)
|
|
self._todos.append(trabalhador)
|
|
raise
|
|
finally:
|
|
pool.put(trabalhador)
|
|
|
|
def encerrar(self) -> None:
|
|
with self._trava:
|
|
for trabalhador in self._todos:
|
|
trabalhador.encerrar()
|
|
self._todos.clear()
|
|
self._pool = None
|
|
|
|
def _garantir_pool(self, executavel: Path) -> "queue.Queue[_TrabalhadorPersistente]":
|
|
with self._trava:
|
|
if self._pool is None:
|
|
self._pool = queue.Queue()
|
|
for _ in range(self.trabalhadores):
|
|
trabalhador = _TrabalhadorPersistente(executavel)
|
|
self._todos.append(trabalhador)
|
|
self._pool.put(trabalhador)
|
|
return self._pool
|
|
|
|
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
|