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:
@@ -0,0 +1,21 @@
|
||||
"""Providers de embeddings usados pela busca semântica de conteúdo."""
|
||||
|
||||
from .provider_ollama import (
|
||||
DIMENSOES_PADRAO,
|
||||
ErroDeEmbedding,
|
||||
MODELO_PADRAO,
|
||||
ProviderDeEmbeddings,
|
||||
ProviderDeEmbeddingsOllama,
|
||||
desempacotar,
|
||||
empacotar,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DIMENSOES_PADRAO",
|
||||
"ErroDeEmbedding",
|
||||
"MODELO_PADRAO",
|
||||
"ProviderDeEmbeddings",
|
||||
"ProviderDeEmbeddingsOllama",
|
||||
"desempacotar",
|
||||
"empacotar",
|
||||
]
|
||||
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
Geração de embeddings por um Ollama local.
|
||||
|
||||
O projeto já roda um Ollama nesta máquina com o ``nomic-embed-text`` — o mesmo
|
||||
modelo que o RAG de código usa. Reaproveitá-lo mantém a análise de conteúdo
|
||||
local: nenhum trecho de transcrição de cliente sai da máquina, e não há custo
|
||||
por chamada.
|
||||
|
||||
O ``nomic-embed-text`` exige prefixos distintos para o que é indexado e para o
|
||||
que é consultado. Sem eles a similaridade cai de forma silenciosa, então os
|
||||
prefixos são aplicados aqui e não ficam a cargo de quem chama.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import struct
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import Protocol, Sequence
|
||||
|
||||
URL_PADRAO = "http://localhost:11434"
|
||||
MODELO_PADRAO = "nomic-embed-text"
|
||||
DIMENSOES_PADRAO = 768
|
||||
PREFIXO_DE_DOCUMENTO = "search_document: "
|
||||
PREFIXO_DE_CONSULTA = "search_query: "
|
||||
|
||||
|
||||
class ErroDeEmbedding(RuntimeError):
|
||||
"""Falha ao gerar um embedding — serviço fora do ar, modelo ausente ou resposta inválida."""
|
||||
|
||||
|
||||
class ProviderDeEmbeddings(Protocol):
|
||||
"""Contrato de quem transforma texto em vetor."""
|
||||
|
||||
@property
|
||||
def modelo(self) -> str:
|
||||
"""Nome do modelo que gerou os vetores."""
|
||||
...
|
||||
|
||||
@property
|
||||
def dimensoes(self) -> int:
|
||||
"""Quantidade de dimensões dos vetores gerados."""
|
||||
...
|
||||
|
||||
def gerar_para_documento(self, texto: str) -> tuple[float, ...]:
|
||||
"""Gera o vetor de um texto que será indexado."""
|
||||
...
|
||||
|
||||
def gerar_para_consulta(self, texto: str) -> tuple[float, ...]:
|
||||
"""Gera o vetor de um texto que está sendo buscado."""
|
||||
...
|
||||
|
||||
|
||||
class ProviderDeEmbeddingsOllama:
|
||||
"""
|
||||
Gera embeddings chamando um Ollama local por HTTP.
|
||||
|
||||
Atributos:
|
||||
modelo: Nome do modelo de embeddings usado.
|
||||
dimensoes: Dimensões esperadas nos vetores devolvidos.
|
||||
url: Endereço base do serviço Ollama.
|
||||
tempo_limite: Segundos a esperar por cada resposta.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
modelo: str = MODELO_PADRAO,
|
||||
dimensoes: int = DIMENSOES_PADRAO,
|
||||
url: str = URL_PADRAO,
|
||||
tempo_limite: float = 60.0,
|
||||
) -> None:
|
||||
"""
|
||||
Inicializa o provider com o modelo e o endereço do serviço.
|
||||
|
||||
Parâmetros:
|
||||
modelo: Nome do modelo de embeddings no Ollama.
|
||||
dimensoes: Dimensões esperadas — vetores de outro tamanho são
|
||||
recusados, para não misturar modelos no mesmo índice.
|
||||
url: Endereço base do Ollama.
|
||||
tempo_limite: Segundos a esperar por cada resposta.
|
||||
"""
|
||||
self._modelo = modelo
|
||||
self._dimensoes = dimensoes
|
||||
self.url = url.rstrip("/")
|
||||
self.tempo_limite = tempo_limite
|
||||
|
||||
@property
|
||||
def modelo(self) -> str:
|
||||
"""Nome do modelo que gera os vetores."""
|
||||
return self._modelo
|
||||
|
||||
@property
|
||||
def dimensoes(self) -> int:
|
||||
"""Quantidade de dimensões dos vetores gerados."""
|
||||
return self._dimensoes
|
||||
|
||||
def gerar_para_documento(self, texto: str) -> tuple[float, ...]:
|
||||
"""
|
||||
Gera o vetor de um enunciado que será indexado.
|
||||
|
||||
Parâmetros:
|
||||
texto: Texto do enunciado.
|
||||
|
||||
Retorna:
|
||||
O vetor normalizado do texto.
|
||||
|
||||
Pode gerar:
|
||||
ErroDeEmbedding: quando o Ollama não responde ou devolve um vetor
|
||||
com dimensões diferentes das esperadas.
|
||||
"""
|
||||
return self._gerar(PREFIXO_DE_DOCUMENTO + texto)
|
||||
|
||||
def gerar_para_consulta(self, texto: str) -> tuple[float, ...]:
|
||||
"""
|
||||
Gera o vetor de uma consulta feita pelo usuário.
|
||||
|
||||
Parâmetros:
|
||||
texto: Texto da consulta.
|
||||
|
||||
Retorna:
|
||||
O vetor normalizado da consulta.
|
||||
|
||||
Pode gerar:
|
||||
ErroDeEmbedding: quando o Ollama não responde ou devolve um vetor
|
||||
com dimensões diferentes das esperadas.
|
||||
"""
|
||||
return self._gerar(PREFIXO_DE_CONSULTA + texto)
|
||||
|
||||
def _gerar(self, texto: str) -> tuple[float, ...]:
|
||||
"""Chama o Ollama e valida o vetor devolvido."""
|
||||
corpo = json.dumps({"model": self._modelo, "prompt": texto}).encode("utf-8")
|
||||
requisicao = urllib.request.Request(
|
||||
f"{self.url}/api/embeddings", data=corpo,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(requisicao, timeout=self.tempo_limite) as resposta:
|
||||
dados = json.loads(resposta.read().decode("utf-8"))
|
||||
except urllib.error.URLError as erro:
|
||||
raise ErroDeEmbedding(
|
||||
f"Não foi possível falar com o Ollama em {self.url}: {erro}"
|
||||
) from erro
|
||||
except json.JSONDecodeError as erro:
|
||||
raise ErroDeEmbedding(f"Resposta do Ollama não é JSON válido: {erro}") from erro
|
||||
|
||||
vetor = dados.get("embedding")
|
||||
if not isinstance(vetor, list) or not vetor:
|
||||
raise ErroDeEmbedding(f"O Ollama não devolveu embedding para o modelo {self._modelo}.")
|
||||
if len(vetor) != self._dimensoes:
|
||||
raise ErroDeEmbedding(
|
||||
f"Embedding com {len(vetor)} dimensões, esperado {self._dimensoes}."
|
||||
)
|
||||
return tuple(float(valor) for valor in vetor)
|
||||
|
||||
|
||||
def empacotar(vetor: Sequence[float]) -> bytes:
|
||||
"""
|
||||
Serializa um vetor para o BLOB gravado no banco.
|
||||
|
||||
Parâmetros:
|
||||
vetor: Valores do vetor.
|
||||
|
||||
Retorna:
|
||||
Os valores como float de 32 bits em sequência.
|
||||
"""
|
||||
return struct.pack(f"<{len(vetor)}f", *vetor)
|
||||
|
||||
|
||||
def desempacotar(dados: bytes) -> tuple[float, ...]:
|
||||
"""
|
||||
Reconstrói um vetor a partir do BLOB lido do banco.
|
||||
|
||||
Parâmetros:
|
||||
dados: Bytes gravados por ``empacotar``.
|
||||
|
||||
Retorna:
|
||||
Os valores do vetor.
|
||||
"""
|
||||
return struct.unpack(f"<{len(dados) // 4}f", dados)
|
||||
Reference in New Issue
Block a user