feat: destacado no deploy o resumo que será gravado na mensagem do
- destacado no deploy o resumo que será gravado na mensagem do commit Resumo: - 8 arquivos alterados - 1 novos - 7 modificados - 0 removidos 7 files changed, 143 insertions(+), 11 deletions(-) Arquivos: - .jhonny/analises.db - admin/deploy.command - code/engine/executar_scanner.py - code/engine/integracoes/midia/__init__.py - code/engine/integracoes/visual/apple_vision.py - code/engine/scanner/configuracao_visual.py - code/engine/scanner/visual.py - code/engine/integracoes/midia/compactador_de_proxy.py
This commit is contained in:
@@ -15,13 +15,41 @@ CAMINHO_DO_CODIGO = Path(__file__).resolve().parent.parent
|
||||
if str(CAMINHO_DO_CODIGO) not in sys.path:
|
||||
sys.path.insert(0, str(CAMINHO_DO_CODIGO))
|
||||
|
||||
|
||||
def _encontrar_caminho_da_midia() -> str:
|
||||
"""Localiza a pasta com ``ffmpeg``/``ffprobe`` para o PATH dos subprocessos.
|
||||
|
||||
O Premiere é aberto pelo Finder/LaunchServices (sem shell de login), então o
|
||||
PATH herdado pelo painel não costuma trazer ``/opt/homebrew/bin``. Essa
|
||||
função devolve o primeiro caminho conhecido onde essas ferramentas existem,
|
||||
ou string vazia quando nenhum é encontrado.
|
||||
"""
|
||||
for candidato in ("/opt/homebrew/bin", "/usr/local/bin"):
|
||||
if Path(candidato, "ffprobe").is_file() or Path(candidato, "ffmpeg").is_file():
|
||||
return candidato
|
||||
return ""
|
||||
|
||||
|
||||
# O mesmo ajuste de PATH já usado em ``admin/deploy.command`` e
|
||||
# ``admin/install.command``: sem ele, a extração de frames e a transcrição
|
||||
# falham com ``No such file or directory: 'ffprobe'``.
|
||||
CAMINHO_DA_MIDIA = _encontrar_caminho_da_midia()
|
||||
if CAMINHO_DA_MIDIA:
|
||||
os.environ["PATH"] = CAMINHO_DA_MIDIA + os.pathsep + os.environ.get("PATH", "")
|
||||
|
||||
from engine.integracoes.premiere.conversores import ConversorDeTimeline
|
||||
from engine.scanner import ConfiguracaoVisualDoScanner, criar_detector_apple_vision, gerar_relatorio_visual
|
||||
from engine.scanner.transcricao_da_timeline import TranscricaoDaTimeline
|
||||
from engine.persistencia import RepositorioDeAnalisesSQLite, RepositorioDeTimelineSQLite
|
||||
|
||||
|
||||
MODELO_DIARIZACAO_PADRAO = "pyannote/speaker-diarization-community-1"
|
||||
|
||||
# O padrão do banco de análises é ``.jhonny/analises.db`` relativo ao cwd, mas o
|
||||
# painel roda o scanner com cwd ``code``. Fixamos o caminho absoluto na raiz do
|
||||
# projeto para garantir que a gravação sempre caia no mesmo banco.
|
||||
CAMINHO_DO_BANCO = CAMINHO_DO_CODIGO.parent / ".jhonny" / "analises.db"
|
||||
|
||||
|
||||
def nome_seguro(nome: str) -> str:
|
||||
return re.sub(r"[^A-Za-z0-9._-]+", "-", nome.strip()).strip(".-") or "timeline"
|
||||
@@ -41,6 +69,13 @@ def executar(entrada: Path, saida: Path) -> Path:
|
||||
print(json.dumps({"evento": "progresso", "etapa": etapa, "percentual": round(percentual, 1),
|
||||
"clipe": clipe}, ensure_ascii=False), flush=True)
|
||||
|
||||
# Persistência: timeline/estrutura + análise visual + transcrição vão para o
|
||||
# banco SQLite do projeto (mesmo dos relatórios gerados).
|
||||
repositorio_de_timeline = RepositorioDeTimelineSQLite(CAMINHO_DO_BANCO)
|
||||
repositorio_de_analises = RepositorioDeAnalisesSQLite(CAMINHO_DO_BANCO)
|
||||
emitir("Persistindo timeline", 0)
|
||||
repositorio_de_timeline.registrar_timeline(timeline)
|
||||
|
||||
resultados = []
|
||||
for clipe in clipes:
|
||||
peso = max(1, math.ceil((clipe.intervalo_na_origem or clipe.intervalo_na_timeline).duracao /
|
||||
@@ -60,6 +95,11 @@ def executar(entrada: Path, saida: Path) -> Path:
|
||||
arquivo = Path(tempfile.gettempdir()) / f"scanner-{clipe.identificador}.json"
|
||||
gerar_relatorio_visual(clipe, resultado, arquivo, configuracao.intervalo_em_segundos)
|
||||
resultados.append(json.loads(arquivo.read_text(encoding="utf-8")))
|
||||
# Grava no banco os fatos visuais e as cenas detectadas deste clipe.
|
||||
repositorio_de_analises.registrar_evidencias_visuais(
|
||||
timeline.identificador, clipe.identificador, resultado.evidencias)
|
||||
repositorio_de_analises.registrar_cenas(
|
||||
timeline.identificador, resultado.cenas, clipe.identificador)
|
||||
except Exception as erro:
|
||||
resultados.append({"clipe": {"identificador": clipe.identificador, "nome": clipe.nome,
|
||||
"arquivo_original": clipe.arquivo}, "erro": str(erro)})
|
||||
@@ -111,6 +151,8 @@ def executar(entrada: Path, saida: Path) -> Path:
|
||||
transcricoes = TranscricaoDaTimeline(provider, diretoria_de_trabalho=pasta / ".cache-audio",
|
||||
diarizador=diarizador).executar(
|
||||
Timeline(timeline.identificador, timeline.nome, faixas=faixas_de_audio))
|
||||
# Persiste a transcrição (segmentos + falas) no banco de análises.
|
||||
repositorio_de_analises.registrar_transcricoes(timeline.identificador, transcricoes)
|
||||
for transcricao in transcricoes:
|
||||
transcricoes_por_clipe[transcricao.identificador_do_clipe] = [
|
||||
{"inicio": item.inicio, "fim": item.fim, "texto": item.texto,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
from .compactador_de_proxy import (CompactadorDeProxy, ExtratorDeQuadrosDoProxy,
|
||||
RESOLUCAO_PADRAO_DO_PROXY)
|
||||
from .extracao_de_audio import ExtracaoDeAudio, ParteDeAudio
|
||||
from .extracao_de_metadados import ExtracaoDeMetadados, MetadadosDoArquivo
|
||||
|
||||
__all__ = ["ExtracaoDeAudio", "ExtracaoDeMetadados", "MetadadosDoArquivo", "ParteDeAudio"]
|
||||
__all__ = ["CompactadorDeProxy", "ExtracaoDeAudio", "ExtracaoDeMetadados",
|
||||
"ExtratorDeQuadrosDoProxy", "MetadadosDoArquivo", "ParteDeAudio",
|
||||
"RESOLUCAO_PADRAO_DO_PROXY"]
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Transcodifica vídeos de origem para um proxy leve usado na leitura visual e de cenas.
|
||||
|
||||
O gargalo de ler frames direto dos arquivos de origem (ex.: MOV com vários GB)
|
||||
está em buscar com ``-ss`` em arquivos pesados e não indexados. Este módulo
|
||||
produz, uma única vez por arquivo e por resolução, uma cópia leve (proxy) em
|
||||
H.264 com keyframes frequentes e ``+faststart``, sem áudio. Toda a análise
|
||||
visual e detecção de cena passa a ler desse proxy, que é pequeno, tem seek
|
||||
rápido e leitura sequencial barata — enquanto o arquivo original fica intocado
|
||||
para áudio/transcrição.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
from ...dominio.entidades.modelos import Clipe, IntervaloDeTempo
|
||||
from ..visual.contratos import ExtratorDeQuadros
|
||||
|
||||
# Resolução padrão do proxy: Full HD (1080) — mantém a qualidade de leitura
|
||||
# visual (faces/OCR/pose) num arquivo bem mais leve e rápido de processar do
|
||||
# que o original em 4K. O formato H.264/MP4 +faststart com keyframes frequentes
|
||||
# é o que torna o seek barato.
|
||||
RESOLUCAO_PADRAO_DO_PROXY = 1080
|
||||
|
||||
# Prazo máximo para transcodificar um único arquivo (protetor contra falhas).
|
||||
LIMITE_DE_TEMPO_DA_TRANSCODIFICACAO = 1800 # 30 min
|
||||
|
||||
|
||||
class CompactadorDeProxy:
|
||||
"""Transcodifica arquivos de vídeo para um proxy leve e o reaproveita."""
|
||||
|
||||
def __init__(self, ffmpeg: str = "ffmpeg", ffprobe: str = "ffprobe",
|
||||
diretorio_de_cache: str | Path | None = None,
|
||||
largura_maxima: int = RESOLUCAO_PADRAO_DO_PROXY) -> None:
|
||||
self.ffmpeg = ffmpeg
|
||||
self.ffprobe = ffprobe
|
||||
self.largura_maxima = int(largura_maxima)
|
||||
self.diretorio_de_cache = (
|
||||
Path(diretorio_de_cache) if diretorio_de_cache
|
||||
else Path.home() / ".cache" / "premiere-mcp" / "proxies"
|
||||
)
|
||||
self.diretorio_de_cache.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def obter(self, arquivo: str | Path,
|
||||
largura_maxima: int | None = None) -> Path:
|
||||
"""Devolve o caminho do proxy do arquivo, gerando-o quando necessário.
|
||||
|
||||
O proxy é idempotente: se ele já existir e o arquivo de origem não tiver
|
||||
mudado desde a última geração, devolve o caminho sem re-transcodificar.
|
||||
"""
|
||||
largura = int(largura_maxima or self.largura_maxima)
|
||||
origem = Path(arquivo).expanduser().resolve(strict=False)
|
||||
if not origem.is_file():
|
||||
raise FileNotFoundError(f"Arquivo de origem não encontrado: {origem}")
|
||||
|
||||
proxy = self._caminho_do_proxy(origem, largura)
|
||||
if proxy.is_file() and self._proxies_sao_validos(origem, proxy):
|
||||
return proxy
|
||||
|
||||
self._transcodificar(origem, proxy, largura)
|
||||
return proxy
|
||||
|
||||
def obter_por_clipe(self, clipe,
|
||||
largura_maxima: int | None = None) -> Path | None:
|
||||
"""Devolve o proxy do arquivo do clipe, ou None quando não houver arquivo.
|
||||
|
||||
Se o clipe for offline ou não tiver arquivo, devolve None (sem proxy).
|
||||
"""
|
||||
if not getattr(clipe, "arquivo", None) or getattr(clipe, "offline", False):
|
||||
return None
|
||||
try:
|
||||
return self.obter(clipe.arquivo, largura_maxima=largura_maxima)
|
||||
except (FileNotFoundError, RuntimeError):
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------ internos
|
||||
|
||||
def _caminho_do_proxy(self, origem: Path, largura_maxima: int) -> Path:
|
||||
"""Monta o nome estável do proxy a partir do caminho e da largura."""
|
||||
resumo = hashlib.sha1(str(origem).encode("utf-8")).hexdigest()[:12]
|
||||
return self.diretorio_de_cache / f"{origem.stem}-{resumo}-{largura_maxima}p.mp4"
|
||||
|
||||
@staticmethod
|
||||
def _alteracao_do_arquivo(caminho: Path) -> float:
|
||||
"""Última alteração do arquivo, útil para detectar que a origem mudou."""
|
||||
try:
|
||||
return caminho.stat().st_mtime_ns
|
||||
except OSError:
|
||||
return 0.0
|
||||
|
||||
def _proxies_sao_validos(self, origem: Path, proxy: Path) -> bool:
|
||||
"""O proxy é válido só se nasceu depois do arquivo de origem mudar."""
|
||||
return proxy.stat().st_mtime_ns >= self._alteracao_do_arquivo(origem)
|
||||
|
||||
def _transcodificar(self, origem: Path, proxy: Path, largura_maxima: int) -> None:
|
||||
"""Transcodifica o arquivo de origem para o proxy H.264 de leitura visual."""
|
||||
comando = [
|
||||
self.ffmpeg, "-y", "-hide_banner", "-loglevel", "error",
|
||||
"-i", str(origem),
|
||||
"-map", "0:v:0",
|
||||
"-vf", (
|
||||
f"scale=w={largura_maxima}:h={largura_maxima}:"
|
||||
"force_original_aspect_ratio=decrease"
|
||||
),
|
||||
"-c:v", "libx264", "-preset", "veryfast", "-crf", "28",
|
||||
"-g", "30", "-keyint_min", "30", "-sc_threshold", "0",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-an",
|
||||
"-movflags", "+faststart",
|
||||
str(proxy),
|
||||
]
|
||||
try:
|
||||
processo = subprocess.run(
|
||||
comando, capture_output=True, text=True,
|
||||
timeout=LIMITE_DE_TEMPO_DA_TRANSCODIFICACAO,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise RuntimeError(
|
||||
f"Transcodificação do proxy excedeu o tempo: {origem}"
|
||||
) from exc
|
||||
if processo.returncode != 0 or not proxy.is_file():
|
||||
detalhe = (processo.stderr or processo.stdout or "").strip()
|
||||
raise RuntimeError(
|
||||
f"Não foi possível gerar o proxy de {origem}: {detalhe}"
|
||||
)
|
||||
|
||||
|
||||
def _clipe_com_arquivo(clipe: Clipe, caminho: Path) -> Clipe:
|
||||
"""Devolve uma cópia do clipe com o ``arquivo`` substituído pelo caminho dado.
|
||||
|
||||
Mantém a identidade, os intervalos e os metadados; só troca o arquivo lido.
|
||||
"""
|
||||
origem = clipe.intervalo_na_origem
|
||||
return Clipe(
|
||||
identificador=clipe.identificador,
|
||||
nome=clipe.nome,
|
||||
intervalo_na_timeline=clipe.intervalo_na_timeline,
|
||||
intervalo_na_origem=IntervaloDeTempo(origem.inicio, origem.fim) if origem else None,
|
||||
arquivo=str(caminho),
|
||||
identificador_da_faixa=clipe.identificador_da_faixa,
|
||||
offline=clipe.offline,
|
||||
metadados=clipe.metadados,
|
||||
)
|
||||
|
||||
|
||||
class ExtratorDeQuadrosDoProxy:
|
||||
"""Extrai frames de um clipe a partir do seu proxy, sem duplicar lógica.
|
||||
|
||||
Delega todo o trabalho ao ``ExtratorDeQuadrosFFmpeg`` informado, apontando-o
|
||||
para o proxy. Os timestamps continuam relativos ao tempo do arquivo original,
|
||||
pois o proxy preserva a mesma cronologia.
|
||||
"""
|
||||
|
||||
def __init__(self, extrator: ExtratorDeQuadros, compactador: CompactadorDeProxy,
|
||||
largura_maxima: int | None = None) -> None:
|
||||
self.extrator = extrator
|
||||
self.compactador = compactador
|
||||
self.largura_maxima = largura_maxima
|
||||
|
||||
def extrair(self, clipe: Clipe) -> list:
|
||||
"""Extrai frames usando o proxy; usa o arquivo original quando não há proxy."""
|
||||
proxy = (
|
||||
self.compactador.obter_por_clipe(clipe)
|
||||
if self.largura_maxima is None
|
||||
else self.compactador.obter_por_clipe(clipe, largura_maxima=self.largura_maxima)
|
||||
)
|
||||
alvo = clipe if proxy is None else _clipe_com_arquivo(clipe, proxy)
|
||||
return self.extrator.extrair(alvo)
|
||||
@@ -143,17 +143,25 @@ class AnalisadorAppleVisionNativo(AnalisadorDeFrame):
|
||||
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:
|
||||
timeout_em_segundos: float = 90.0,
|
||||
maximo_de_frames_interpretados: int | None = None) -> 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
|
||||
# Limita quantos frames por clipe fazem a interpretação editorial (Apple
|
||||
# Intelligence / Foundation Models), que é a parte mais cara. Os demais
|
||||
# frames continuam com a análise visual completa. None = interpreta todos.
|
||||
self.maximo_de_frames_interpretados = maximo_de_frames_interpretados
|
||||
|
||||
def analisar(self, quadro: QuadroDeVideo) -> list[EvidenciaVisual]:
|
||||
def analisar(self, quadro: QuadroDeVideo, interpretar: bool | None = None) -> list[EvidenciaVisual]:
|
||||
if quadro.caminho is None:
|
||||
raise ValueError("Apple Vision requer que o frame tenha caminho persistido em disco.")
|
||||
if self._indices_para_interpretar and quadro.indice not in self._indices_para_interpretar:
|
||||
interpretar = False
|
||||
deve_interpretar = interpretar if interpretar is not None else self.interpretar_com_apple_intelligence
|
||||
dados = self.executor.executar({"frame": str(quadro.caminho), "recursos": list(self.recursos),
|
||||
"resumir": self.interpretar_com_apple_intelligence},
|
||||
"resumir": deve_interpretar},
|
||||
self.timeout_em_segundos)
|
||||
evidencias = [self._converter(item, quadro) for item in dados.get("evidencias", [])]
|
||||
avisos = dados.get("avisos", [])
|
||||
@@ -162,6 +170,27 @@ class AnalisadorAppleVisionNativo(AnalisadorDeFrame):
|
||||
{"avisos": avisos, "recursos_solicitados": list(self.recursos)}, provider=self.nome))
|
||||
return evidencias
|
||||
|
||||
def preparar_interpretacao_por_clipe(self, quantidade_de_frames: int) -> None:
|
||||
"""Marca os índices de frames que receberão a interpretação editorial.
|
||||
|
||||
Distribui a interpretação ao longo do clipe (início, pontos médios e fim)
|
||||
em vez de aplicar a todos os frames, mantendo a cobertura de cena com
|
||||
custo muito menor. Quando não há limite (None), interpreta todos.
|
||||
"""
|
||||
if self.maximo_de_frames_interpretados is None or quantidade_de_frames <= 0:
|
||||
self._indices_para_interpretar = set(range(quantidade_de_frames))
|
||||
return
|
||||
quantidade = min(quantidade_de_frames, max(1, self.maximo_de_frames_interpretados))
|
||||
indices = set()
|
||||
if quantidade_de_frames == 1:
|
||||
indices.add(0)
|
||||
else:
|
||||
passo = max(1, quantidade_de_frames // quantidade)
|
||||
indices = set(range(0, quantidade_de_frames, passo)) | {quantidade_de_frames - 1}
|
||||
self._indices_para_interpretar = {i for i in indices if i < quantidade_de_frames}
|
||||
|
||||
_indices_para_interpretar: set[int] = set()
|
||||
|
||||
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,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
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",
|
||||
@@ -21,6 +23,13 @@ SENSIBILIDADES_VISUAIS: dict[str, float] = {
|
||||
}
|
||||
|
||||
|
||||
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."""
|
||||
@@ -38,6 +47,13 @@ class ConfiguracaoVisualDoScanner:
|
||||
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 not 0.04 <= self.intervalo_em_segundos <= 60:
|
||||
@@ -75,6 +91,9 @@ class ConfiguracaoVisualDoScanner:
|
||||
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]:
|
||||
@@ -92,4 +111,7 @@ class ConfiguracaoVisualDoScanner:
|
||||
"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,
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class DetectorDeCenas:
|
||||
|
||||
def detectar(self, clipe: Clipe) -> ResultadoVisualDoClipe:
|
||||
quadros = self.extrator.extrair(clipe)
|
||||
self._preparar_analisadores(quadros)
|
||||
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)
|
||||
@@ -51,6 +52,19 @@ class DetectorDeCenas:
|
||||
) for cena in cenas]
|
||||
return ResultadoVisualDoClipe(clipe.identificador, evidencias, cenas, cenas_visuais)
|
||||
|
||||
def _preparar_analisadores(self, quadros: list[QuadroDeVideo]) -> None:
|
||||
"""Prepara analisadores que precisam saber a quantidade de frames do clipe.
|
||||
|
||||
Caso especial do `AnalisadorAppleVisionNativo`: ele distribui a
|
||||
interpretação editorial (Apple Intelligence / Foundation Models) em um
|
||||
subconjunto dos frames para manter a cobertura de cena com custo menor.
|
||||
"""
|
||||
quantidade_de_quadros = len(quadros)
|
||||
for analisador in self.analisadores_de_frame:
|
||||
preparar = getattr(analisador, "preparar_interpretacao_por_clipe", None)
|
||||
if callable(preparar):
|
||||
preparar(quantidade_de_quadros)
|
||||
|
||||
def _analisar_quadros(self, quadros: list[QuadroDeVideo], progresso: list[int] | None = None,
|
||||
total: int = 0) -> list[EvidenciaVisual]:
|
||||
if not self.analisadores_de_frame or not quadros:
|
||||
@@ -205,18 +219,35 @@ def criar_detector_apple_vision(
|
||||
from ..integracoes.visual import (AnalisadorAppleVisionNativo,
|
||||
AnalisadorSequenciaAppleVisionNativo,
|
||||
ExtratorDeQuadrosFFmpeg)
|
||||
from ..integracoes.midia import CompactadorDeProxy, ExtratorDeQuadrosDoProxy
|
||||
|
||||
perfil = configuracao or ConfiguracaoVisualDoScanner(intervalo_em_segundos=intervalo_em_segundos)
|
||||
|
||||
# Quantidade fixa de trabalhadores para extração e análise de frames.
|
||||
# Em volumes externos (ex.: /Volumes/Merongo), mais threads do que isso
|
||||
# satura a E/S e trava os processos de mídia a ~0% de CPU.
|
||||
QUANTIDADE_DE_TRABALHADORES = 4
|
||||
|
||||
extrator = ExtratorDeQuadrosFFmpeg(intervalo_em_segundos=perfil.intervalo_em_segundos,
|
||||
diretorio_de_cache=diretorio_de_cache or ".frames",
|
||||
resolucao_maxima=perfil.resolucao_maxima,
|
||||
trabalhadores_em_paralelo=QUANTIDADE_DE_TRABALHADORES)
|
||||
# Leitura via proxy compactado: para arquivos pesados, evita buscar frame
|
||||
# por frame no arquivo original (um -ss por frame num MOV de vários GB).
|
||||
if perfil.usar_proxy:
|
||||
extrator = ExtratorDeQuadrosDoProxy(
|
||||
extrator, CompactadorDeProxy(), largura_maxima=perfil.resolucao_do_proxy
|
||||
)
|
||||
|
||||
return DetectorDeCenas(
|
||||
ExtratorDeQuadrosFFmpeg(intervalo_em_segundos=perfil.intervalo_em_segundos,
|
||||
diretorio_de_cache=diretorio_de_cache or ".frames",
|
||||
resolucao_maxima=perfil.resolucao_maxima),
|
||||
extrator,
|
||||
analisadores_de_frame=[AnalisadorAppleVisionNativo(
|
||||
recursos=tuple(sorted(perfil.recursos_vision)),
|
||||
interpretar_com_apple_intelligence=perfil.interpretar_cena,
|
||||
maximo_de_frames_interpretados=perfil.maximo_de_frames_interpretados,
|
||||
)],
|
||||
analisadores_de_sequencia=[AnalisadorSequenciaAppleVisionNativo()]
|
||||
if perfil.analisar_continuidade else [],
|
||||
detectores_de_intervalo=_detectores_de_cena_do_perfil(perfil),
|
||||
trabalhadores_em_paralelo=QUANTIDADE_DE_TRABALHADORES,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user