feat: adicionada reprodução dos trechos de áudio individuais de ca

- adicionada reprodução dos trechos de áudio individuais de cada falante

Resumo:
- 2 arquivos alterados
- 0 novos
- 2 modificados
- 0 removidos

 2 files changed, 95 insertions(+), 4 deletions(-)

Arquivos:
  - code/cep-plugin/main.js
  - code/cep-plugin/styles.css
This commit is contained in:
João Henrique
2026-09-09 10:54:00 -04:00
parent f02f4f3e60
commit d37143d733
2 changed files with 95 additions and 4 deletions
+92 -3
View File
@@ -1326,6 +1326,7 @@ var silenceState = {
sequenceId: null, sequenceId: null,
sequenceName: null, sequenceName: null,
mediaPath: null, mediaPath: null,
audioCachePath: null,
fileBaseName: null, fileBaseName: null,
outputDir: null, outputDir: null,
transcriptPath: null, transcriptPath: null,
@@ -1333,8 +1334,12 @@ var silenceState = {
cachedTranscriptPath: null, cachedTranscriptPath: null,
jsonOutputPath: null, jsonOutputPath: null,
speakers: [], speakers: [],
speakerTurns: [],
}; };
var speakerAudioPlayer = null;
var speakerAudioQueue = null;
// ---- Central transcript cache ---- // ---- Central transcript cache ----
// Evita rodar o Whisper de novo para um vídeo já transcrito antes: indexa // Evita rodar o Whisper de novo para um vídeo já transcrito antes: indexa
// transcrições por caminho absoluto + tamanho + data de modificação do // transcrições por caminho absoluto + tamanho + data de modificação do
@@ -1572,9 +1577,11 @@ function silenceSelectSequence() {
silenceState.mediaPath = result.mediaPath; silenceState.mediaPath = result.mediaPath;
silenceState.fileBaseName = slugify(result.clipName || result.sequenceName); silenceState.fileBaseName = slugify(result.clipName || result.sequenceName);
silenceState.outputDir = path.join(SILENCE_OUTPUT_ROOT, silenceState.fileBaseName); silenceState.outputDir = path.join(SILENCE_OUTPUT_ROOT, silenceState.fileBaseName);
silenceState.audioCachePath = path.join(silenceState.outputDir, path.parse(result.mediaPath).name + ".wav");
silenceState.transcriptPath = null; silenceState.transcriptPath = null;
silenceState.jsonOutputPath = null; silenceState.jsonOutputPath = null;
silenceState.speakers = []; silenceState.speakers = [];
silenceState.speakerTurns = [];
var speakerConfig = document.getElementById("silenceSpeakerConfig"); var speakerConfig = document.getElementById("silenceSpeakerConfig");
if (speakerConfig) speakerConfig.hidden = true; if (speakerConfig) speakerConfig.hidden = true;
@@ -1774,6 +1781,7 @@ function silenceTranscribe() {
silenceState.transcriptPath = out; silenceState.transcriptPath = out;
silenceState.transcribeModel = model; silenceState.transcribeModel = model;
silenceState.speakers = []; silenceState.speakers = [];
silenceState.speakerTurns = [];
var speakerConfig = document.getElementById("silenceSpeakerConfig"); var speakerConfig = document.getElementById("silenceSpeakerConfig");
if (speakerConfig) speakerConfig.hidden = true; if (speakerConfig) speakerConfig.hidden = true;
var resumo = parsed.segments + " trechos · " + Math.round(parsed.duration) + "s de áudio"; var resumo = parsed.segments + " trechos · " + Math.round(parsed.duration) + "s de áudio";
@@ -1844,7 +1852,10 @@ function silenceDiarizeSpeakers(onFinished) {
} }
var parsed = null; var parsed = null;
try { parsed = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")).diarization; } catch (e) {} try { parsed = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")).diarization; } catch (e) {}
if (parsed && Array.isArray(parsed.speakers)) renderSpeakerConfiguration(parsed.speakers); if (parsed && Array.isArray(parsed.speakers)) {
silenceState.speakerTurns = Array.isArray(parsed.turns) ? parsed.turns : [];
renderSpeakerConfiguration(parsed.speakers, silenceState.speakerTurns);
}
var msg = parsed var msg = parsed
? "Foram detectados " + parsed.num_speakers + (parsed.num_speakers === 1 ? " falante" : " falantes") + ": " + parsed.speakers.join(", ") + "." ? "Foram detectados " + parsed.num_speakers + (parsed.num_speakers === 1 ? " falante" : " falantes") + ": " + parsed.speakers.join(", ") + "."
: "Falantes detectados."; : "Falantes detectados.";
@@ -1863,11 +1874,12 @@ function speakerNameDefault(speakerId) {
return speakerId; return speakerId;
} }
function renderSpeakerConfiguration(speakerIds) { function renderSpeakerConfiguration(speakerIds, speakerTurns) {
var config = document.getElementById("silenceSpeakerConfig"); var config = document.getElementById("silenceSpeakerConfig");
var list = document.getElementById("silenceSpeakerList"); var list = document.getElementById("silenceSpeakerList");
var count = document.getElementById("silenceSpeakerCount"); var count = document.getElementById("silenceSpeakerCount");
if (!config || !list || !count) return; if (!config || !list || !count) return;
silenceState.speakerTurns = Array.isArray(speakerTurns) ? speakerTurns : silenceState.speakerTurns;
var previous = {}; var previous = {};
silenceState.speakers.forEach(function (speaker) { previous[speaker.id] = speaker; }); silenceState.speakers.forEach(function (speaker) { previous[speaker.id] = speaker; });
silenceState.speakers = speakerIds.map(function (id) { silenceState.speakers = speakerIds.map(function (id) {
@@ -1887,6 +1899,13 @@ function renderSpeakerConfiguration(speakerIds) {
var label = document.createElement("label"); var label = document.createElement("label");
label.className = "field-label"; label.className = "field-label";
label.textContent = speaker.id; label.textContent = speaker.id;
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); });
var input = document.createElement("input"); var input = document.createElement("input");
input.type = "text"; input.type = "text";
input.className = "select-field speaker-name-input"; input.className = "select-field speaker-name-input";
@@ -1906,6 +1925,7 @@ function renderSpeakerConfiguration(speakerIds) {
activeLabel.appendChild(active); activeLabel.appendChild(active);
activeLabel.appendChild(activeText); activeLabel.appendChild(activeText);
row.appendChild(label); row.appendChild(label);
row.appendChild(play);
row.appendChild(input); row.appendChild(input);
row.appendChild(activeLabel); row.appendChild(activeLabel);
list.appendChild(row); list.appendChild(row);
@@ -1913,12 +1933,81 @@ function renderSpeakerConfiguration(speakerIds) {
config.hidden = silenceState.speakers.length === 0; config.hidden = silenceState.speakers.length === 0;
} }
function speakerAudioFileUrl(filePath) {
return encodeURI("file://" + String(filePath || ""));
}
function stopSpeakerAudio() {
if (speakerAudioPlayer) {
speakerAudioPlayer.pause();
speakerAudioPlayer.removeAttribute("src");
speakerAudioPlayer.load();
}
speakerAudioPlayer = null;
speakerAudioQueue = null;
document.querySelectorAll(".speaker-play-button").forEach(function (button) {
button.textContent = "▶";
button.classList.remove("is-playing");
});
}
function toggleSpeakerAudio(speakerId, button) {
if (speakerAudioPlayer && speakerAudioQueue && speakerAudioQueue.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 áudio disponíveis para " + speakerId + ".");
return;
}
if (!silenceState.audioCachePath || !fs.existsSync(silenceState.audioCachePath)) {
showToast("err", "Arquivo de áudio do cache não encontrado. Reprocesse a transcrição.");
return;
}
speakerAudioQueue = { speakerId: speakerId, ranges: ranges, index: 0, button: button };
speakerAudioPlayer = new Audio(speakerAudioFileUrl(silenceState.audioCachePath));
button.textContent = "■";
button.classList.add("is-playing");
speakerAudioPlayer.addEventListener("loadedmetadata", playCurrentSpeakerRange);
speakerAudioPlayer.addEventListener("timeupdate", advanceSpeakerAudioRange);
speakerAudioPlayer.addEventListener("ended", advanceSpeakerAudioRange);
speakerAudioPlayer.addEventListener("error", function () {
stopSpeakerAudio();
showToast("err", "Não foi possível reproduzir o áudio deste falante.");
});
}
function playCurrentSpeakerRange() {
if (!speakerAudioPlayer || !speakerAudioQueue) return;
var range = speakerAudioQueue.ranges[speakerAudioQueue.index];
if (!range) { stopSpeakerAudio(); return; }
speakerAudioPlayer.currentTime = Math.max(0, Number(range.start) - 0.15);
var playPromise = speakerAudioPlayer.play();
if (playPromise && playPromise.catch) playPromise.catch(function () { stopSpeakerAudio(); });
}
function advanceSpeakerAudioRange() {
if (!speakerAudioPlayer || !speakerAudioQueue) return;
var range = speakerAudioQueue.ranges[speakerAudioQueue.index];
if (!range || speakerAudioPlayer.currentTime < Number(range.end)) return;
speakerAudioQueue.index += 1;
if (speakerAudioQueue.index >= speakerAudioQueue.ranges.length) {
stopSpeakerAudio();
return;
}
playCurrentSpeakerRange();
}
function silenceLoadSpeakerConfiguration() { function silenceLoadSpeakerConfiguration() {
if (!silenceState.transcriptPath) return; if (!silenceState.transcriptPath) return;
try { try {
var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")); var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
var diarization = transcript.diarization; var diarization = transcript.diarization;
if (diarization && Array.isArray(diarization.speakers)) renderSpeakerConfiguration(diarization.speakers); if (diarization && Array.isArray(diarization.speakers)) renderSpeakerConfiguration(diarization.speakers, diarization.turns);
} catch (e) {} } catch (e) {}
} }
+3 -1
View File
@@ -406,8 +406,10 @@ textarea { padding: 8px 10px; font: 10px var(--font-mono); color: var(--text-sec
.speaker-config-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 10.5px; } .speaker-config-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 10.5px; }
.speaker-config-head span { color: var(--text-muted); font-size: 9.2px; } .speaker-config-head span { color: var(--text-muted); font-size: 9.2px; }
.speaker-config .field-help { margin: 6px 0 8px; } .speaker-config .field-help { margin: 6px 0 8px; }
.speaker-row { display: grid; grid-template-columns: 92px minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 6px 0; border-top: 1px solid var(--border); } .speaker-row { display: grid; grid-template-columns: 92px auto minmax(0, 1fr) auto; align-items: center; gap: 8px; padding: 6px 0; border-top: 1px solid var(--border); }
.speaker-row .field-label { margin: 0; } .speaker-row .field-label { margin: 0; }
.speaker-play-button { min-width: 28px; padding: 4px 7px; }
.speaker-play-button.is-playing { color: var(--accent); }
.speaker-name-input { width: 100%; } .speaker-name-input { width: 100%; }
.speaker-active-field { margin: 0; padding: 6px 8px; white-space: nowrap; } .speaker-active-field { margin: 0; padding: 6px 8px; white-space: nowrap; }