From b0b8027c7e73b5641dafc51c308a3e119c7aed68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Henrique?= Date: Thu, 10 Sep 2026 09:27:44 -0400 Subject: [PATCH] feat: aprimorado o preview de falantes na tela Refinar para reprod MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .jhonny/analises.db | Bin 1069056 -> 1069056 bytes code/cep-plugin/main.js | 36 +++++++- code/engine/consultar_preview_de_falantes.py | 37 ++++++++ code/engine/persistencia/consultas.py | 80 ++++++++++++++++++ .../testes/test_consultas_de_persistencia.py | 29 +++++++ 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 code/engine/consultar_preview_de_falantes.py diff --git a/.jhonny/analises.db b/.jhonny/analises.db index 90ed3219c2a4d1954e91ac5fbb1c5efc2f3f596c..352944436e54927e0ed438b76f22d508f7ba5bec 100644 GIT binary patch delta 83 zcmWN=ISqh76h+ZL?E6wRA%Wuc;9yMP1~HJFbA>otwu%%x`b{@riC g=f(LG;Y1{2k%&}eA{T`yMI~y{h*or>e;C5{0e4py1poj5 delta 83 zcmWN=ITe5~6h*-w_I;_bkPZb=fQ4%S4^X@w9K-}}5CiAT95Ea*T+8guTyAFX-~IO6 fug;$cClZm0Oyr^vrKm(L8qtbQ^kNX>!xXm 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() diff --git a/code/engine/persistencia/consultas.py b/code/engine/persistencia/consultas.py index d59d195..95beb9e 100644 --- a/code/engine/persistencia/consultas.py +++ b/code/engine/persistencia/consultas.py @@ -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, diff --git a/code/engine/testes/test_consultas_de_persistencia.py b/code/engine/testes/test_consultas_de_persistencia.py index 27af880..f68fbf1 100644 --- a/code/engine/testes/test_consultas_de_persistencia.py +++ b/code/engine/testes/test_consultas_de_persistencia.py @@ -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: