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 Resumo: - 6 arquivos alterados - 2 novos - 4 modificados - 0 removidos 4 files changed, 97 insertions(+), 11 deletions(-) Arquivos: - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - .jhonny/analises.db-shm - .jhonny/analises.db-wal
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -360,20 +360,24 @@
|
||||
</div>
|
||||
<!-- =============================== REFINAR ============================== -->
|
||||
<div class="tab-panel" id="tabRefinar" role="tabpanel" aria-labelledby="tabBtnRefinar" hidden>
|
||||
<p class="tab-intro">Confirme quem são os falantes antes de editar. O áudio reproduz somente os trechos identificados de cada pessoa.</p>
|
||||
<p class="tab-intro">Confirme quem são os falantes antes de editar. O preview reproduz somente os trechos identificados de cada pessoa.</p>
|
||||
<section class="card scanner-analysis-card">
|
||||
<div class="card-head"><h2>Falantes da análise</h2><span class="step-chip" id="refinarStatus">Aguardando análise</span></div>
|
||||
<p class="field-help" id="refinarInfo">Carregue a análise mais recente do Scanner para consultar os falantes detectados.</p>
|
||||
<div class="refinar-actions">
|
||||
<button id="btnRefinarLoad" class="button button-primary" onclick="refinarCarregarAnalise()" type="button">Carregar análise do Scanner</button>
|
||||
<button id="btnRefinarStopAudio" class="button" onclick="stopSpeakerAudio()" type="button">Parar áudio</button>
|
||||
<button id="btnRefinarStopAudio" class="button" onclick="stopSpeakerAudio()" type="button">Parar preview</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="card" id="refinarSpeakerCard">
|
||||
<div class="card-head"><h2>Identificar falantes</h2><span class="card-kicker" id="refinarSpeakerCount">Nenhum falante carregado</span></div>
|
||||
<p class="field-help">Use o botão de reprodução para reconhecer a voz, informe um nome e escolha quem poderá aparecer no vídeo. A identificação será usada ao gerar o JSON de edição.</p>
|
||||
<p class="field-help">Use o botão de reprodução para conferir a imagem e a voz de cada falante, informe um nome e escolha quem poderá aparecer no vídeo. A identificação será usada ao gerar o JSON de edição.</p>
|
||||
<div class="callout callout-info" id="refinarSpeakerEmpty"><strong>Nenhum falante identificado</strong><p>A análise carregada não possui diarização. Execute a diarização no Scanner para liberar esta etapa.</p></div>
|
||||
<div class="speaker-config" id="refinarSpeakerConfig" hidden>
|
||||
<div class="speaker-video-preview" id="refinarSpeakerPreview" hidden>
|
||||
<video id="refinarSpeakerVideo" controls preload="metadata" playsinline></video>
|
||||
<p class="field-help" id="refinarSpeakerPreviewInfo">Reproduzindo os trechos do falante selecionado.</p>
|
||||
</div>
|
||||
<div id="refinarSpeakerList"></div>
|
||||
<button id="btnRefinarSave" class="button button-primary" onclick="refinarSalvarFalantes()" type="button">Salvar identificação</button>
|
||||
</div>
|
||||
|
||||
+87
-8
@@ -2376,6 +2376,8 @@ function atualizarTelaRefinar(falantes, audioDoBanco) {
|
||||
|
||||
var speakerAudioPlayer = null;
|
||||
var speakerAudioQueue = null;
|
||||
var speakerVideoPlayer = null;
|
||||
var speakerVideoQueue = null;
|
||||
|
||||
// ---- Central transcript cache ----
|
||||
// Evita rodar o Whisper de novo para um vídeo já transcrito antes: indexa
|
||||
@@ -2982,10 +2984,13 @@ function renderSpeakerConfiguration(speakerIds, speakerTurns, alvo) {
|
||||
var play = document.createElement("button");
|
||||
play.type = "button";
|
||||
play.className = "button speaker-play-button";
|
||||
play.textContent = "▶";
|
||||
play.title = "Ouvir trechos de " + speaker.id;
|
||||
play.setAttribute("aria-label", "Ouvir trechos de " + speaker.id);
|
||||
play.addEventListener("click", function () { toggleSpeakerAudio(speaker.id, play); });
|
||||
play.textContent = alvo.video ? "▶ vídeo" : "▶ áudio";
|
||||
play.title = (alvo.video ? "Ver" : "Ouvir") + " trechos de " + speaker.id;
|
||||
play.setAttribute("aria-label", (alvo.video ? "Ver" : "Ouvir") + " trechos de " + speaker.id);
|
||||
play.addEventListener("click", function () {
|
||||
if (alvo.video) toggleSpeakerVideo(speaker.id, play);
|
||||
else toggleSpeakerAudio(speaker.id, play);
|
||||
});
|
||||
var input = document.createElement("input");
|
||||
input.type = "text";
|
||||
input.className = "select-field speaker-name-input";
|
||||
@@ -3020,12 +3025,12 @@ function renderSpeakerConfiguration(speakerIds, speakerTurns, alvo) {
|
||||
var refineConfig = document.getElementById("refinarSpeakerConfig");
|
||||
if (refineConfig && config.id !== refineConfig.id) {
|
||||
renderSpeakerConfiguration(speakerIds, speakerTurns, {
|
||||
config: "refinarSpeakerConfig", list: "refinarSpeakerList", count: "refinarSpeakerCount"
|
||||
config: "refinarSpeakerConfig", list: "refinarSpeakerList", count: "refinarSpeakerCount", video: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function speakerAudioFileUrl(filePath) {
|
||||
function speakerMediaFileUrl(filePath) {
|
||||
return encodeURI("file://" + String(filePath || ""));
|
||||
}
|
||||
|
||||
@@ -3037,12 +3042,86 @@ function stopSpeakerAudio() {
|
||||
}
|
||||
speakerAudioPlayer = null;
|
||||
speakerAudioQueue = null;
|
||||
if (speakerVideoPlayer) {
|
||||
speakerVideoPlayer.pause();
|
||||
speakerVideoPlayer.onloadedmetadata = null;
|
||||
speakerVideoPlayer.ontimeupdate = null;
|
||||
speakerVideoPlayer.onended = null;
|
||||
speakerVideoPlayer.onerror = null;
|
||||
speakerVideoPlayer.removeAttribute("src");
|
||||
speakerVideoPlayer.load();
|
||||
}
|
||||
speakerVideoPlayer = null;
|
||||
speakerVideoQueue = null;
|
||||
var videoPreview = document.getElementById("refinarSpeakerPreview");
|
||||
if (videoPreview) videoPreview.hidden = true;
|
||||
document.querySelectorAll(".speaker-play-button").forEach(function (button) {
|
||||
button.textContent = "▶";
|
||||
button.textContent = button.closest("#refinarSpeakerList") ? "▶ vídeo" : "▶ áudio";
|
||||
button.classList.remove("is-playing");
|
||||
});
|
||||
}
|
||||
|
||||
function toggleSpeakerVideo(speakerId, button) {
|
||||
if (speakerVideoPlayer && speakerVideoQueue && speakerVideoQueue.speakerId === speakerId) {
|
||||
stopSpeakerAudio();
|
||||
return;
|
||||
}
|
||||
stopSpeakerAudio();
|
||||
var ranges = silenceState.speakerTurns.filter(function (turn) {
|
||||
return turn.speaker === speakerId && Number(turn.end) > Number(turn.start);
|
||||
}).sort(function (a, b) { return Number(a.start) - Number(b.start); });
|
||||
if (!ranges.length) {
|
||||
showToast("err", "Não há intervalos de vídeo disponíveis para " + speakerId + ".");
|
||||
return;
|
||||
}
|
||||
if (!silenceState.mediaPath || !fs.existsSync(silenceState.mediaPath)) {
|
||||
showToast("err", "Arquivo de vídeo da análise não encontrado.");
|
||||
return;
|
||||
}
|
||||
var video = document.getElementById("refinarSpeakerVideo");
|
||||
var preview = document.getElementById("refinarSpeakerPreview");
|
||||
var info = document.getElementById("refinarSpeakerPreviewInfo");
|
||||
if (!video || !preview) {
|
||||
showToast("err", "Preview de vídeo indisponível nesta versão do painel.");
|
||||
return;
|
||||
}
|
||||
speakerVideoQueue = { speakerId: speakerId, ranges: ranges, index: 0, button: button };
|
||||
speakerVideoPlayer = video;
|
||||
preview.hidden = false;
|
||||
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.ontimeupdate = advanceSpeakerVideoRange;
|
||||
video.onended = advanceSpeakerVideoRange;
|
||||
video.onerror = function () {
|
||||
stopSpeakerAudio();
|
||||
showToast("err", "Não foi possível reproduzir o vídeo deste falante.");
|
||||
};
|
||||
}
|
||||
|
||||
function playCurrentSpeakerVideoRange() {
|
||||
if (!speakerVideoPlayer || !speakerVideoQueue) return;
|
||||
var range = speakerVideoQueue.ranges[speakerVideoQueue.index];
|
||||
if (!range) { stopSpeakerAudio(); return; }
|
||||
speakerVideoPlayer.currentTime = Math.max(0, Number(range.start) - 0.15);
|
||||
var playPromise = speakerVideoPlayer.play();
|
||||
if (playPromise && playPromise.catch) playPromise.catch(function () { stopSpeakerAudio(); });
|
||||
}
|
||||
|
||||
function advanceSpeakerVideoRange() {
|
||||
if (!speakerVideoPlayer || !speakerVideoQueue) return;
|
||||
var range = speakerVideoQueue.ranges[speakerVideoQueue.index];
|
||||
if (!range || speakerVideoPlayer.currentTime < Number(range.end)) return;
|
||||
speakerVideoQueue.index += 1;
|
||||
if (speakerVideoQueue.index >= speakerVideoQueue.ranges.length) {
|
||||
stopSpeakerAudio();
|
||||
return;
|
||||
}
|
||||
playCurrentSpeakerVideoRange();
|
||||
}
|
||||
|
||||
function toggleSpeakerAudio(speakerId, button) {
|
||||
if (speakerAudioPlayer && speakerAudioQueue && speakerAudioQueue.speakerId === speakerId) {
|
||||
stopSpeakerAudio();
|
||||
@@ -3061,7 +3140,7 @@ function toggleSpeakerAudio(speakerId, button) {
|
||||
return;
|
||||
}
|
||||
speakerAudioQueue = { speakerId: speakerId, ranges: ranges, index: 0, button: button };
|
||||
speakerAudioPlayer = new Audio(speakerAudioFileUrl(silenceState.audioCachePath));
|
||||
speakerAudioPlayer = new Audio(speakerMediaFileUrl(silenceState.audioCachePath));
|
||||
button.textContent = "■";
|
||||
button.classList.add("is-playing");
|
||||
speakerAudioPlayer.addEventListener("loadedmetadata", playCurrentSpeakerRange);
|
||||
|
||||
@@ -491,6 +491,9 @@ textarea { padding: 8px 10px; font: 10px var(--font-mono); color: var(--text-sec
|
||||
.speaker-play-button.is-playing { color: var(--accent); }
|
||||
.speaker-name-input { width: 100%; }
|
||||
.speaker-active-field { margin: 0; padding: 6px 8px; white-space: nowrap; }
|
||||
.speaker-video-preview { margin: 0 0 10px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
|
||||
.speaker-video-preview video { display: block; width: 100%; max-height: 260px; border-radius: 5px; background: #000; }
|
||||
.speaker-video-preview .field-help { margin: 6px 0 0; }
|
||||
.refinar-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.refinar-actions .button { flex: 1 1 180px; }
|
||||
#refinarSpeakerConfig .button { margin-top: 10px; }
|
||||
|
||||
Reference in New Issue
Block a user