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:
Binary file not shown.
+33
-3
@@ -944,6 +944,7 @@ var RETAKES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/co
|
|||||||
// (Python 3.14, com faster_whisper) garante que Scanner e Retakes carreguem bem.
|
// (Python 3.14, com faster_whisper) garante que Scanner e Retakes carreguem bem.
|
||||||
var SCANNER_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python";
|
var SCANNER_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python";
|
||||||
var ANALISE_STATUS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_status_da_analise.py";
|
var ANALISE_STATUS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_status_da_analise.py";
|
||||||
|
var ANALISE_PREVIEW_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_preview_de_falantes.py";
|
||||||
var retakesCasoAtual = null;
|
var retakesCasoAtual = null;
|
||||||
var retakesGrupos = [];
|
var retakesGrupos = [];
|
||||||
var retakesProcesso = null;
|
var retakesProcesso = null;
|
||||||
@@ -2231,6 +2232,20 @@ function silenceConsultarStatusNoBanco(videoId) {
|
|||||||
} catch (_) { return null; }
|
} catch (_) { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function silenceConsultarPreviewDeFalantes(videoId) {
|
||||||
|
if (!videoId || !childProcess.spawnSync) return null;
|
||||||
|
try {
|
||||||
|
var resposta = childProcess.spawnSync(
|
||||||
|
SCANNER_PYTHON_BIN,
|
||||||
|
[ANALISE_PREVIEW_SCRIPT, videoId, "--banco", "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db"],
|
||||||
|
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", encoding: "utf-8", maxBuffer: 4 * 1024 * 1024 }
|
||||||
|
);
|
||||||
|
if (resposta.status !== 0) return null;
|
||||||
|
var dados = JSON.parse(String(resposta.stdout || "{}"));
|
||||||
|
return Array.isArray(dados.intervalos) ? dados.intervalos : null;
|
||||||
|
} catch (_) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
function silenceLocalizarAudioParaFalantes(pasta) {
|
function silenceLocalizarAudioParaFalantes(pasta) {
|
||||||
var encontrados = [];
|
var encontrados = [];
|
||||||
function visitar(caminho) {
|
function visitar(caminho) {
|
||||||
@@ -2307,6 +2322,20 @@ function silenceLoadScannerAnalysis() {
|
|||||||
var turnos = transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) {
|
var turnos = transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) {
|
||||||
return { speaker: segmento.falante, start: segmento.start, end: segmento.end };
|
return { speaker: segmento.falante, start: segmento.start, end: segmento.end };
|
||||||
});
|
});
|
||||||
|
var intervalosDoBanco = silenceConsultarPreviewDeFalantes(silenceState.sequenceId);
|
||||||
|
if (intervalosDoBanco && intervalosDoBanco.length) {
|
||||||
|
turnos = intervalosDoBanco.map(function (intervalo) {
|
||||||
|
return {
|
||||||
|
speaker: intervalo.falante,
|
||||||
|
start: intervalo.inicio,
|
||||||
|
end: intervalo.fim,
|
||||||
|
timelineStart: intervalo.inicio_na_timeline,
|
||||||
|
timelineEnd: intervalo.fim_na_timeline,
|
||||||
|
mediaPath: intervalo.arquivo,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
silenceState.mediaPath = intervalosDoBanco[0].arquivo || silenceState.mediaPath;
|
||||||
|
}
|
||||||
renderSpeakerConfiguration(falantes, turnos);
|
renderSpeakerConfiguration(falantes, turnos);
|
||||||
atualizarTelaRefinar(falantes, audioDoBanco);
|
atualizarTelaRefinar(falantes, audioDoBanco);
|
||||||
status.textContent = "Carregada";
|
status.textContent = "Carregada";
|
||||||
@@ -3089,9 +3118,6 @@ function toggleSpeakerVideo(speakerId, button) {
|
|||||||
speakerVideoPlayer = video;
|
speakerVideoPlayer = video;
|
||||||
preview.hidden = false;
|
preview.hidden = false;
|
||||||
if (info) info.textContent = "Reproduzindo os trechos de " + speakerId + ".";
|
if (info) info.textContent = "Reproduzindo os trechos de " + speakerId + ".";
|
||||||
video.src = speakerMediaFileUrl(silenceState.mediaPath);
|
|
||||||
button.textContent = "■ vídeo";
|
|
||||||
button.classList.add("is-playing");
|
|
||||||
video.onloadedmetadata = playCurrentSpeakerVideoRange;
|
video.onloadedmetadata = playCurrentSpeakerVideoRange;
|
||||||
video.ontimeupdate = advanceSpeakerVideoRange;
|
video.ontimeupdate = advanceSpeakerVideoRange;
|
||||||
video.onended = advanceSpeakerVideoRange;
|
video.onended = advanceSpeakerVideoRange;
|
||||||
@@ -3099,6 +3125,10 @@ function toggleSpeakerVideo(speakerId, button) {
|
|||||||
stopSpeakerAudio();
|
stopSpeakerAudio();
|
||||||
showToast("err", "Não foi possível reproduzir o vídeo deste falante.");
|
showToast("err", "Não foi possível reproduzir o vídeo deste falante.");
|
||||||
};
|
};
|
||||||
|
button.textContent = "■ vídeo";
|
||||||
|
button.classList.add("is-playing");
|
||||||
|
video.src = speakerMediaFileUrl(silenceState.mediaPath);
|
||||||
|
video.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
function playCurrentSpeakerVideoRange() {
|
function playCurrentSpeakerVideoRange() {
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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(
|
def listar_falas_no_intervalo(
|
||||||
self,
|
self,
|
||||||
video_id: str,
|
video_id: str,
|
||||||
|
|||||||
@@ -105,6 +105,35 @@ class TesteConsultasDeAnalises(unittest.TestCase):
|
|||||||
self.assertFalse(status["audio"]["metricas_de_voz_disponiveis"])
|
self.assertFalse(status["audio"]["metricas_de_voz_disponiveis"])
|
||||||
self.assertEqual(status["audio"]["falantes"], ["locutor"])
|
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 ------------------------------------------------------
|
# --- listar_falas_no_intervalo ------------------------------------------------------
|
||||||
|
|
||||||
def test_listar_falas_no_intervalo_filtra_por_tempo_e_omite_palavras_por_padrao(self) -> None:
|
def test_listar_falas_no_intervalo_filtra_por_tempo_e_omite_palavras_por_padrao(self) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user