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:
@@ -18,6 +18,13 @@ from ..scanner.transcricao_da_timeline import TranscricaoDoClipe
|
||||
from .conexao import abrir_banco
|
||||
|
||||
|
||||
def _numero_ou_nulo(valor: object) -> float | None:
|
||||
"""Converte um valor de métrica em float, ou None quando ausente/ilegível."""
|
||||
if isinstance(valor, bool) or not isinstance(valor, (int, float)):
|
||||
return None
|
||||
return float(valor)
|
||||
|
||||
|
||||
class RepositorioDeAnalisesSQLite:
|
||||
"""Grava metadados de arquivo, transcrição e evidências/cenas visuais."""
|
||||
|
||||
@@ -30,6 +37,17 @@ class RepositorioDeAnalisesSQLite:
|
||||
def registrar_metadados_de_arquivo(
|
||||
self, video_id: str, metadados: MetadadosDoArquivo, hash_do_conteudo: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Grava os metadados técnicos de um arquivo de mídia do vídeo.
|
||||
|
||||
Regravar o mesmo caminho atualiza a linha existente em vez de criar
|
||||
outra, então o método é seguro para reprocessamento.
|
||||
|
||||
Parâmetros:
|
||||
video_id: Identificador do vídeo dono do arquivo.
|
||||
metadados: Metadados técnicos extraídos da mídia.
|
||||
hash_do_conteudo: Hash do conteúdo do arquivo, quando calculado.
|
||||
"""
|
||||
with self.conexao:
|
||||
self._garantir_video(video_id)
|
||||
self.conexao.execute(
|
||||
@@ -59,6 +77,16 @@ class RepositorioDeAnalisesSQLite:
|
||||
def registrar_transcricoes(
|
||||
self, video_id: str, transcricoes: Iterable[TranscricaoDoClipe],
|
||||
) -> None:
|
||||
"""
|
||||
Acrescenta transcrições sem remover as já gravadas.
|
||||
|
||||
Para reprocessamento use ``substituir_transcricoes``: este método
|
||||
acumula, e chamá-lo duas vezes para o mesmo clipe duplica as falas.
|
||||
|
||||
Parâmetros:
|
||||
video_id: Identificador do vídeo dono das transcrições.
|
||||
transcricoes: Transcrições por clipe a gravar.
|
||||
"""
|
||||
with self.conexao:
|
||||
self._garantir_video(video_id)
|
||||
for transcricao in transcricoes:
|
||||
@@ -118,15 +146,31 @@ class RepositorioDeAnalisesSQLite:
|
||||
def _inserir_segmento(
|
||||
self, video_id: str, clipe_id: str, segmento: SegmentoDeTranscricao,
|
||||
) -> int:
|
||||
"""Grava uma fala e devolve o id gerado, para as palavras se ligarem a ela.
|
||||
|
||||
As métricas acústicas são gravadas duas vezes de propósito: em
|
||||
``caracteristicas_acusticas`` (JSON completo, para reconstrução fiel)
|
||||
e em colunas dedicadas (para filtrar e ordenar por elas em SQL sem
|
||||
precisar abrir o JSON).
|
||||
"""
|
||||
metricas = segmento.caracteristicas_acusticas or {}
|
||||
cursor = self.conexao.execute(
|
||||
"""INSERT INTO segmentos_de_transcricao
|
||||
(video_id, clipe_id, inicio, fim, texto, confianca, falante, voz_aparente,
|
||||
confianca_voz, emocao, confianca_emocao, caracteristicas_acusticas)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
confianca_voz, emocao, confianca_emocao, caracteristicas_acusticas,
|
||||
energia_rms, pitch_mediano_hz, pitch_desvio_hz, velocidade_de_fala_pps,
|
||||
maior_pausa_interna_s, intervalo_anterior_s)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
||||
(video_id, clipe_id, segmento.inicio, segmento.fim, segmento.texto,
|
||||
segmento.confianca, segmento.falante, segmento.voz_aparente,
|
||||
segmento.confianca_voz, segmento.emocao, segmento.confianca_emocao,
|
||||
json.dumps(segmento.caracteristicas_acusticas, ensure_ascii=False)),
|
||||
json.dumps(segmento.caracteristicas_acusticas, ensure_ascii=False),
|
||||
_numero_ou_nulo(metricas.get("energy_rms")),
|
||||
_numero_ou_nulo(metricas.get("pitch_hz_median")),
|
||||
_numero_ou_nulo(metricas.get("pitch_hz_std")),
|
||||
_numero_ou_nulo(metricas.get("speaking_rate_wps")),
|
||||
_numero_ou_nulo(metricas.get("longest_internal_pause_s")),
|
||||
_numero_ou_nulo(metricas.get("gap_before_s"))),
|
||||
)
|
||||
segmento_id = cursor.lastrowid
|
||||
for ordem, palavra in enumerate(segmento.palavras):
|
||||
@@ -140,6 +184,16 @@ class RepositorioDeAnalisesSQLite:
|
||||
return segmento_id
|
||||
|
||||
def carregar_transcricoes(self, video_id: str, clipe_id: str) -> list[SegmentoDeTranscricao]:
|
||||
"""
|
||||
Carrega as falas de um clipe, já com as palavras de cada uma.
|
||||
|
||||
Parâmetros:
|
||||
video_id: Identificador do vídeo dono das falas.
|
||||
clipe_id: Clipe cujas falas serão lidas.
|
||||
|
||||
Retorna:
|
||||
As falas do clipe ordenadas por início, com as palavras em ordem.
|
||||
"""
|
||||
segmentos: list[SegmentoDeTranscricao] = []
|
||||
for linha in self.conexao.execute(
|
||||
"""SELECT * FROM segmentos_de_transcricao
|
||||
@@ -166,9 +220,69 @@ class RepositorioDeAnalisesSQLite:
|
||||
))
|
||||
return segmentos
|
||||
|
||||
def substituir_evidencias_visuais(
|
||||
self, video_id: str, clipe_id: str, evidencias: Iterable[EvidenciaVisual],
|
||||
) -> int:
|
||||
"""
|
||||
Regrava as evidências visuais de um clipe, apagando as anteriores.
|
||||
|
||||
Torna o reprocessamento idempotente. ``registrar_evidencias_visuais``
|
||||
só acrescenta, então reanalisar o mesmo clipe com ele acumula cópias
|
||||
da mesma evidência e infla qualquer contagem feita sobre a tabela.
|
||||
|
||||
Parâmetros:
|
||||
video_id: Identificador do vídeo dono das evidências.
|
||||
clipe_id: Clipe cujas evidências serão substituídas.
|
||||
evidencias: Evidências a gravar.
|
||||
|
||||
Retorna:
|
||||
A quantidade de evidências gravadas.
|
||||
"""
|
||||
evidencias_materializadas = tuple(evidencias)
|
||||
with self.conexao:
|
||||
self._garantir_video(video_id)
|
||||
self.conexao.execute(
|
||||
"DELETE FROM evidencias_visuais WHERE video_id = ? AND clipe_id = ?",
|
||||
(video_id, clipe_id),
|
||||
)
|
||||
self.registrar_evidencias_visuais(video_id, clipe_id, evidencias_materializadas)
|
||||
return len(evidencias_materializadas)
|
||||
|
||||
def substituir_cenas(
|
||||
self, video_id: str, cenas: Iterable[Cena], clipe_id: str | None = None,
|
||||
) -> int:
|
||||
"""
|
||||
Regrava as cenas de um clipe, apagando as anteriores.
|
||||
|
||||
Parâmetros:
|
||||
video_id: Identificador do vídeo dono das cenas.
|
||||
cenas: Cenas a gravar.
|
||||
clipe_id: Clipe cujas cenas serão substituídas. Quando ``None``,
|
||||
substitui as cenas do vídeo que não pertencem a clipe algum.
|
||||
|
||||
Retorna:
|
||||
A quantidade de cenas gravadas.
|
||||
"""
|
||||
cenas_materializadas = tuple(cenas)
|
||||
with self.conexao:
|
||||
self._garantir_video(video_id)
|
||||
if clipe_id is None:
|
||||
self.conexao.execute(
|
||||
"DELETE FROM cenas WHERE video_id = ? AND clipe_id IS NULL", (video_id,))
|
||||
else:
|
||||
self.conexao.execute(
|
||||
"DELETE FROM cenas WHERE video_id = ? AND clipe_id = ?", (video_id, clipe_id))
|
||||
self.registrar_cenas(video_id, cenas_materializadas, clipe_id)
|
||||
return len(cenas_materializadas)
|
||||
|
||||
def registrar_evidencias_visuais(
|
||||
self, video_id: str, clipe_id: str, evidencias: Iterable[EvidenciaVisual],
|
||||
) -> None:
|
||||
"""Acrescenta evidências visuais sem remover as já gravadas.
|
||||
|
||||
Para reprocessamento use ``substituir_evidencias_visuais``: este método
|
||||
acumula, e chamá-lo duas vezes para o mesmo clipe duplica as linhas.
|
||||
"""
|
||||
with self.conexao:
|
||||
self._garantir_video(video_id)
|
||||
for evidencia in evidencias:
|
||||
@@ -184,6 +298,16 @@ class RepositorioDeAnalisesSQLite:
|
||||
def registrar_cenas(
|
||||
self, video_id: str, cenas: Iterable[Cena], clipe_id: str | None = None,
|
||||
) -> None:
|
||||
"""Acrescenta cenas sem remover as já gravadas.
|
||||
|
||||
Para reprocessamento use ``substituir_cenas``: este método acumula, e
|
||||
chamá-lo duas vezes para o mesmo clipe duplica as cenas.
|
||||
|
||||
Parâmetros:
|
||||
video_id: Identificador do vídeo dono das cenas.
|
||||
cenas: Cenas detectadas a gravar.
|
||||
clipe_id: Clipe a que as cenas pertencem, quando houver.
|
||||
"""
|
||||
with self.conexao:
|
||||
self._garantir_video(video_id)
|
||||
for cena in cenas:
|
||||
|
||||
Reference in New Issue
Block a user