feat: aprimorado o preview de falantes na tela Refinar para reprod

- aprimorado o preview de falantes na tela Refinar para reproduzir os trechos do vídeo
- corrigida a leitura dos timestamps do preview de falantes usando os clipes do banco

Resumo:
- 5 arquivos alterados
- 1 novos
- 4 modificados
- 0 removidos

 4 files changed, 142 insertions(+), 3 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - code/cep-plugin/main.js
  - code/engine/persistencia/consultas.py
  - code/engine/testes/test_consultas_de_persistencia.py
  - code/engine/consultar_preview_de_falantes.py
This commit is contained in:
João Henrique
2026-09-10 09:27:44 -04:00
parent 485c35c4e8
commit b0b8027c7e
5 changed files with 179 additions and 3 deletions
@@ -0,0 +1,37 @@
"""Consulta os intervalos de vídeo necessários ao preview dos falantes."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
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))
from engine.persistencia.consultas import ConsultasDeAnalises
def executar(caminho_do_banco: Path, video_id: str) -> dict:
"""Consulta e serializa os intervalos de preview de um vídeo."""
intervalos = ConsultasDeAnalises(caminho_do_banco).listar_intervalos_de_preview_dos_falantes(video_id)
return {"video_id": video_id, "intervalos": intervalos}
def principal() -> None:
"""Executa a consulta pela linha de comando."""
argumentos = argparse.ArgumentParser()
argumentos.add_argument("video_id")
argumentos.add_argument("--banco", type=Path, default=Path(".jhonny/analises.db"))
opcoes = argumentos.parse_args()
try:
print(json.dumps(executar(opcoes.banco, opcoes.video_id), ensure_ascii=False))
except (OSError, ValueError) as erro:
print(json.dumps({"erro": str(erro)}, ensure_ascii=False))
raise SystemExit(1) from erro
if __name__ == "__main__":
principal()
+80
View File
@@ -135,6 +135,86 @@ class ConsultasDeAnalises:
},
}
def listar_intervalos_de_preview_dos_falantes(self, video_id: str) -> list[dict]:
"""
Lista os intervalos dos falantes convertidos para o arquivo de vídeo.
Os segmentos de transcrição usam segundos da timeline e normalmente
pertencem a um clipe de áudio. A consulta encontra o clipe de vídeo
correspondente e calcula os segundos na origem usando os campos
``inicio_na_timeline`` e ``inicio_na_origem`` da tabela ``clipes``.
Parâmetros:
video_id: Identificador do vídeo analisado.
Retorna:
Lista ordenada de intervalos com falante, tempos da timeline,
tempos no arquivo de origem e caminho do vídeo.
Pode gerar:
ErroDeConsultaInvalida: quando ``video_id`` estiver vazio.
"""
self._validar_texto_obrigatorio("video_id", video_id)
linhas = self.conexao.execute(
"""SELECT s.clipe_id, s.inicio, s.fim, s.falante,
a.arquivo AS arquivo_do_audio,
v.id AS clipe_de_video, v.inicio_na_timeline,
v.fim_na_timeline, v.inicio_na_origem,
v.fim_na_origem, v.arquivo AS arquivo_do_video
FROM segmentos_de_transcricao AS s
LEFT JOIN clipes AS a
ON a.video_id = s.video_id AND a.id = s.clipe_id
LEFT JOIN clipes AS v
ON v.video_id = s.video_id
AND v.inicio_na_timeline < s.fim
AND v.fim_na_timeline > s.inicio
LEFT JOIN faixas AS f
ON f.video_id = v.video_id AND f.id = v.faixa_id
AND f.tipo = 'video'
WHERE s.video_id = ?
AND s.falante IS NOT NULL
AND TRIM(s.falante) <> ''
AND (f.id IS NOT NULL OR v.id IS NULL)
ORDER BY s.inicio, s.fim,
(v.arquivo = a.arquivo) DESC, s.id""",
(video_id,),
).fetchall()
resultado = []
vistos = set()
for linha in linhas:
chave = (linha["clipe_id"], linha["inicio"], linha["fim"], linha["falante"])
if chave in vistos:
continue
vistos.add(chave)
inicio_na_timeline = float(linha["inicio"])
fim_na_timeline = float(linha["fim"])
inicio_na_origem = linha["inicio_na_origem"]
fim_na_origem = linha["fim_na_origem"]
arquivo = linha["arquivo_do_video"] or linha["arquivo_do_audio"]
if inicio_na_origem is None:
inicio_no_arquivo = inicio_na_timeline
fim_no_arquivo = fim_na_timeline
else:
deslocamento = inicio_na_timeline - float(linha["inicio_na_timeline"])
inicio_no_arquivo = float(inicio_na_origem) + deslocamento
fim_no_arquivo = inicio_no_arquivo + (fim_na_timeline - inicio_na_timeline)
if fim_na_origem is not None:
fim_no_arquivo = min(fim_no_arquivo, float(fim_na_origem))
if not arquivo or fim_no_arquivo <= inicio_no_arquivo:
continue
resultado.append({
"clipe_id": linha["clipe_id"],
"falante": linha["falante"],
"inicio_na_timeline": inicio_na_timeline,
"fim_na_timeline": fim_na_timeline,
"inicio": max(0.0, inicio_no_arquivo),
"fim": max(0.0, fim_no_arquivo),
"arquivo": arquivo,
"clipe_de_video": linha["clipe_de_video"],
})
return resultado
def listar_falas_no_intervalo(
self,
video_id: str,
@@ -105,6 +105,35 @@ class TesteConsultasDeAnalises(unittest.TestCase):
self.assertFalse(status["audio"]["metricas_de_voz_disponiveis"])
self.assertEqual(status["audio"]["falantes"], ["locutor"])
def test_listar_intervalos_de_preview_converte_timeline_para_origem(self) -> None:
self.consultas.conexao.executescript(
"""INSERT INTO faixas (id, video_id, nome, tipo, indice)
VALUES ('audio-2', 'v1', 'A2', 'audio', 1);
INSERT INTO faixas (id, video_id, nome, tipo, indice)
VALUES ('video-2', 'v1', 'V2', 'video', 1);
INSERT INTO clipes
(id, video_id, faixa_id, nome, inicio_na_timeline,
fim_na_timeline, inicio_na_origem, fim_na_origem, arquivo)
VALUES ('audio-clip-2', 'v1', 'audio-2', 'A2', 30.0, 40.0,
100.0, 110.0, '/videos/entrevista.mp4');
INSERT INTO clipes
(id, video_id, faixa_id, nome, inicio_na_timeline,
fim_na_timeline, inicio_na_origem, fim_na_origem, arquivo)
VALUES ('video-clip-2', 'v1', 'video-2', 'V2', 30.0, 40.0,
100.0, 110.0, '/videos/entrevista.mp4');
INSERT INTO segmentos_de_transcricao
(video_id, clipe_id, inicio, fim, texto, falante)
VALUES ('v1', 'audio-clip-2', 32.5, 35.0, 'trecho', 'locutor-2');"""
)
intervalos = self.consultas.listar_intervalos_de_preview_dos_falantes(VIDEO_ID)
intervalo = next(item for item in intervalos if item["falante"] == "locutor-2")
self.assertEqual(intervalo["clipe_de_video"], "video-clip-2")
self.assertEqual(intervalo["inicio"], 102.5)
self.assertEqual(intervalo["fim"], 105.0)
self.assertEqual(intervalo["arquivo"], "/videos/entrevista.mp4")
# --- listar_falas_no_intervalo ------------------------------------------------------
def test_listar_falas_no_intervalo_filtra_por_tempo_e_omite_palavras_por_padrao(self) -> None: