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
This commit is contained in:
João Henrique
2026-09-10 08:58:22 -04:00
parent 1156619937
commit c1b544f4b5
25 changed files with 2792 additions and 29 deletions
+105 -1
View File
@@ -4,6 +4,7 @@ import json
import tempfile
from engine.dominio import Clipe, Faixa, IntervaloDeTempo, Timeline
from engine.integracoes.visual.analisadores import AnalisadorDeRostosInsightFace
from engine.integracoes.visual.contratos import (AnalisadorDeFrame, AnalisadorDeSequenciaDeQuadros,
DetectorDeIntervalosDeCena,
ExtratorDeQuadros)
@@ -13,11 +14,34 @@ from engine.integracoes.visual.apple_vision import (AnalisadorAppleVisionNativo,
AnalisadorSequenciaAppleVisionNativo)
from engine.scanner.coordenacao import ContextoDeAnalise
from engine.scanner.modelos import Cena, EvidenciaVisual
from engine.scanner.visual import AnaliseVisualDaTimeline, DetectorDeCenas, ResultadoVisualDoClipe
from engine.scanner.visual import (AnaliseVisualDaTimeline, DetectorDeCenas, ResultadoVisualDoClipe,
criar_detector_apple_vision, criar_detector_visual_local)
from engine.scanner.relatorio_visual import gerar_relatorio_visual, gerar_resumo_visual_markdown
from engine.scanner.configuracao_visual import ConfiguracaoVisualDoScanner
class RostoSimulado:
"""Dublê de um rosto devolvido por `FaceAnalysis.get`."""
def __init__(self, bbox, det_score=0.9, embedding=None, idade=None, genero=None):
self.bbox = bbox
self.det_score = det_score
self.normed_embedding = embedding
self.age = idade
self.sex = genero
class AppInsightFaceSimulado:
"""Dublê do `FaceAnalysis`: devolve uma lista fixa de rostos."""
def __init__(self, rostos):
self._rostos = rostos
def get(self, imagem):
self.imagem_recebida = imagem
return self._rostos
class ExtratorSimulado(ExtratorDeQuadros):
def extrair(self, clipe):
return [
@@ -211,6 +235,86 @@ class TesteAnaliseVisualLocal(unittest.TestCase):
self.assertEqual(extrator._timestamps(inicio, fim), [2.3])
def test_insightface_converte_rosto_em_evidencias_de_rosto_e_identidade(self):
rosto = RostoSimulado(bbox=[100.0, 50.0, 300.0, 350.0], det_score=0.87,
embedding=[0.1, 0.2, 0.3], idade=28, genero="F")
analisador = AnalisadorDeRostosInsightFace(app=AppInsightFaceSimulado([rosto]))
quadro = QuadroDeVideo(1.5, 0, 1000, 1000, imagem="frame-bgr")
evidencias = analisador.analisar(quadro)
self.assertEqual([item.tipo for item in evidencias], ["rosto", "identidade_facial"])
caixa = evidencias[0].valor["bounding_box"]
self.assertAlmostEqual(caixa["x"], 0.1)
self.assertAlmostEqual(caixa["y"], 0.05)
self.assertAlmostEqual(caixa["largura"], 0.2)
self.assertAlmostEqual(caixa["altura"], 0.3)
self.assertEqual(evidencias[0].confianca, 0.87)
identidade = evidencias[1].valor
self.assertEqual(identidade["assinatura_facial"], [0.1, 0.2, 0.3])
self.assertEqual(identidade["dimensoes"], 3)
self.assertEqual(identidade["idade_aproximada"], 28)
self.assertEqual(identidade["genero_aparente"], "F")
def test_insightface_descarta_rosto_menor_que_o_minimo_relativo(self):
rosto_pequeno = RostoSimulado(bbox=[0.0, 0.0, 10.0, 10.0])
analisador = AnalisadorDeRostosInsightFace(app=AppInsightFaceSimulado([rosto_pequeno]))
quadro = QuadroDeVideo(0.0, 0, 1000, 1000, imagem="frame-bgr")
self.assertEqual(analisador.analisar(quadro), [])
def test_insightface_nao_gera_identidade_sem_embedding(self):
rosto_sem_embedding = RostoSimulado(bbox=[100.0, 50.0, 300.0, 350.0])
analisador = AnalisadorDeRostosInsightFace(app=AppInsightFaceSimulado([rosto_sem_embedding]))
quadro = QuadroDeVideo(0.0, 0, 1000, 1000, imagem="frame-bgr")
evidencias = analisador.analisar(quadro)
self.assertEqual([item.tipo for item in evidencias], ["rosto"])
def test_insightface_exige_imagem_decodificada_ou_caminho_persistido(self):
analisador = AnalisadorDeRostosInsightFace(app=AppInsightFaceSimulado([]))
quadro = QuadroDeVideo(0.0, 0, 1000, 1000, imagem=None, caminho=None)
with self.assertRaises(ValueError):
analisador.analisar(quadro)
def test_insightface_rejeita_tamanho_minimo_relativo_invalido(self):
with self.assertRaises(ValueError):
AnalisadorDeRostosInsightFace(tamanho_minimo_relativo=1.5)
def test_detector_local_inclui_rostos_opencv_quando_recurso_faces_ativo(self):
detector = criar_detector_visual_local()
nomes = [type(item).__name__ for item in detector.analisadores_de_frame]
self.assertIn("AnalisadorDeRostosOpenCV", nomes)
self.assertNotIn("AnalisadorDeRostosInsightFace", nomes)
def test_detector_local_inclui_insightface_quando_identidade_facial_ativa(self):
perfil = ConfiguracaoVisualDoScanner(identidade_facial=True)
detector = criar_detector_visual_local(configuracao=perfil)
nomes = [type(item).__name__ for item in detector.analisadores_de_frame]
self.assertIn("AnalisadorDeRostosInsightFace", nomes)
def test_detector_apple_vision_inclui_insightface_quando_identidade_facial_ativa(self):
perfil = ConfiguracaoVisualDoScanner(identidade_facial=True)
detector = criar_detector_apple_vision(configuracao=perfil)
nomes = [type(item).__name__ for item in detector.analisadores_de_frame]
self.assertIn("AnalisadorAppleVisionNativo", nomes)
self.assertIn("AnalisadorDeRostosInsightFace", nomes)
def test_detector_apple_vision_nao_inclui_insightface_por_padrao(self):
detector = criar_detector_apple_vision()
nomes = [type(item).__name__ for item in detector.analisadores_de_frame]
self.assertNotIn("AnalisadorDeRostosInsightFace", nomes)
if __name__ == "__main__":
unittest.main()