Files
jhonny-editor/code/engine/scanner/configuracao_visual.py
T
João Henrique c1b544f4b5 feat: reorganizado o fluxo de edição para validar a análise do Sca
- reorganizado o fluxo de edição para validar a análise do Scanner, selecionar o tipo de vídeo e enviar suas instruções no JSON.
- banco de análises: métricas de fala viraram colunas, busca lexical FTS5, enunciados com embeddings, views achatadas de leitura (fala/palavra/linha do tempo/fala com visual), ingestor do pipeline de voz e gravação idempotente de evidências visuais e cenas.
- retakes passam a ser gravados no banco de análises (SQLite) em vez de JSON por caso, com status de revisão persistido.
- planos de edição e suas aplicações passam a ser registrados no banco, em vez de se perderem no arquivo temporário.
- comando de limpeza das evidências visuais e cenas duplicadas por execuções antigas do Scanner.
- análise visual: OpenCV (rostos) e PySceneDetect passam a entrar no detector local por padrão; novo adapter InsightFace gera assinatura facial (embedding) para reconhecer a mesma pessoa entre tomadas, gravada como evidência visual no banco.
- corrigido: métricas de fala (energia, pitch, velocidade) agora gravam nas colunas dedicadas, não só no JSON; a view fala+visual passa a casar por vídeo e tempo, já que o mesmo arquivo entra na timeline como clipes distintos de vídeo e áudio; views são recriadas a cada abertura do banco para uma correção de consulta chegar a bancos já existentes.
- reordenadas as abas do painel CEP para Scanner, Refinar e Editar vídeo

Resumo:
- 24 arquivos alterados
- 9 novos
- 15 modificados
- 0 removidos

 15 files changed, 872 insertions(+), 29 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/engine/aplicar_plano_de_edicao.py
  - code/engine/integracoes/visual/README.md
  - code/engine/integracoes/visual/__init__.py
  - code/engine/integracoes/visual/analisadores.py
  - code/engine/persistencia/__init__.py
  - code/engine/persistencia/esquema.py
  - code/engine/persistencia/repositorio_de_analises_sqlite.py
  - code/engine/persistencia/repositorio_de_retakes_sqlite.py
  - code/engine/requirements-visual.txt
  - code/engine/scanner/configuracao_visual.py
  - code/engine/scanner/visual.py
  - code/engine/testes/test_analise_visual_local.py
  - .jhonny/analises.db.pos-scanner-084841
  - code/engine/integracoes/embeddings/
  - code/engine/limpar_duplicatas.py
  - code/engine/persistencia/busca_de_conteudo.py
  - code/engine/persistencia/enunciados.py
  - code/engine/persistencia/ingestao_de_voz.py
  - code/engine/persistencia/limpeza.py
  - code/engine/persistencia/repositorio_de_planos.py
  - code/engine/testes/test_busca_de_conteudo.py
2026-09-10 08:58:22 -04:00

132 lines
6.4 KiB
Python

"""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
# Assinatura facial (InsightFace) — desligada por padrão. Nem o OpenCV nem
# o Apple Vision reconhecem que o mesmo rosto aparece em clipes diferentes;
# esse recurso existe só para isso, então fica fora do custo padrão.
identidade_facial: bool = False
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)),
identidade_facial=bool(dados.get("identidade_facial", False)),
)
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,
"identidade_facial": self.identidade_facial,
}