diff --git a/.jhonny/analises.db b/.jhonny/analises.db
index 1574930..90ed321 100644
Binary files a/.jhonny/analises.db and b/.jhonny/analises.db differ
diff --git a/.jhonny/analises.db-shm b/.jhonny/analises.db-shm
new file mode 100644
index 0000000..fe9ac28
Binary files /dev/null and b/.jhonny/analises.db-shm differ
diff --git a/.jhonny/analises.db-wal b/.jhonny/analises.db-wal
new file mode 100644
index 0000000..e69de29
diff --git a/code/cep-plugin/index.html b/code/cep-plugin/index.html
index 9ba3c91..22897d6 100755
--- a/code/cep-plugin/index.html
+++ b/code/cep-plugin/index.html
@@ -360,20 +360,24 @@
-
Confirme quem são os falantes antes de editar. O áudio reproduz somente os trechos identificados de cada pessoa.
+
Confirme quem são os falantes antes de editar. O preview reproduz somente os trechos identificados de cada pessoa.
Falantes da análise Aguardando análise
Carregue a análise mais recente do Scanner para consultar os falantes detectados.
Carregar análise do Scanner
- Parar áudio
+ Parar preview
Identificar falantes Nenhum falante carregado
- 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.
+ 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.
Nenhum falante identificado A análise carregada não possui diarização. Execute a diarização no Scanner para liberar esta etapa.
+
+
+
Reproduzindo os trechos do falante selecionado.
+
Salvar identificação
diff --git a/code/cep-plugin/main.js b/code/cep-plugin/main.js
index dec02ea..c75a650 100755
--- a/code/cep-plugin/main.js
+++ b/code/cep-plugin/main.js
@@ -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);
diff --git a/code/cep-plugin/styles.css b/code/cep-plugin/styles.css
index ccc81b0..3e7d43c 100755
--- a/code/cep-plugin/styles.css
+++ b/code/cep-plugin/styles.css
@@ -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; }