diff --git a/code/cep-plugin/index.html b/code/cep-plugin/index.html
index 0b3ce97..e064e4a 100755
--- a/code/cep-plugin/index.html
+++ b/code/cep-plugin/index.html
@@ -169,6 +169,14 @@
+
Métricas de voz
diff --git a/code/cep-plugin/main.js b/code/cep-plugin/main.js
index 21df6eb..87884f6 100755
--- a/code/cep-plugin/main.js
+++ b/code/cep-plugin/main.js
@@ -1331,6 +1331,7 @@ var silenceState = {
transcribeModel: null,
cachedTranscriptPath: null,
jsonOutputPath: null,
+ speakers: [],
};
// ---- Central transcript cache ----
@@ -1485,6 +1486,9 @@ function silenceDetectClip() {
silenceState.outputDir = path.join(SILENCE_OUTPUT_ROOT, silenceState.fileBaseName);
silenceState.transcriptPath = null;
silenceState.jsonOutputPath = null;
+ silenceState.speakers = [];
+ var speakerConfig = document.getElementById("silenceSpeakerConfig");
+ if (speakerConfig) speakerConfig.hidden = true;
document.getElementById("clipSequenceName").textContent = result.sequenceName;
document.getElementById("clipFileName").textContent = path.basename(result.mediaPath);
@@ -1554,6 +1558,7 @@ function silenceUseCachedTranscript() {
silenceState.transcriptPath = entry.transcriptPath;
silenceState.transcribeModel = entry.model;
silenceState.cachedTranscriptPath = entry.transcriptPath;
+ silenceLoadSpeakerConfiguration();
document.getElementById("silenceCacheInfo").textContent =
"Usando a transcrição salva em " + new Date(entry.savedAt).toLocaleString() + ".";
unlockAfterTranscript();
@@ -1676,6 +1681,9 @@ function silenceTranscribe() {
}
silenceState.transcriptPath = out;
silenceState.transcribeModel = model;
+ silenceState.speakers = [];
+ var speakerConfig = document.getElementById("silenceSpeakerConfig");
+ if (speakerConfig) speakerConfig.hidden = true;
var resumo = parsed.segments + " trechos · " + Math.round(parsed.duration) + "s de áudio";
silenceLog("Transcrição concluída: " + resumo, "ok");
document.getElementById("silenceCacheInfo").textContent = "Transcrição pronta — " + resumo + ".";
@@ -1734,6 +1742,7 @@ function silenceDiarizeSpeakers() {
}
var parsed = null;
try { parsed = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")).diarization; } catch (e) {}
+ if (parsed && Array.isArray(parsed.speakers)) renderSpeakerConfiguration(parsed.speakers);
var msg = parsed ? parsed.num_speakers + " locutor(es): " + parsed.speakers.join(", ") : "Locutores detectados.";
silenceLog(msg, "ok");
info.textContent = msg;
@@ -1745,6 +1754,88 @@ function silenceDiarizeSpeakers() {
);
}
+function speakerNameDefault(speakerId) {
+ return speakerId;
+}
+
+function renderSpeakerConfiguration(speakerIds) {
+ var config = document.getElementById("silenceSpeakerConfig");
+ var list = document.getElementById("silenceSpeakerList");
+ var count = document.getElementById("silenceSpeakerCount");
+ if (!config || !list || !count) return;
+ var previous = {};
+ silenceState.speakers.forEach(function (speaker) { previous[speaker.id] = speaker; });
+ silenceState.speakers = speakerIds.map(function (id) {
+ var old = previous[id];
+ return {
+ id: id,
+ name: old && old.name ? old.name : speakerNameDefault(id),
+ active: !old || old.active !== false,
+ };
+ });
+ count.textContent = silenceState.speakers.length + (silenceState.speakers.length === 1 ? " identificado" : " identificados");
+ list.innerHTML = "";
+ silenceState.speakers.forEach(function (speaker, index) {
+ var row = document.createElement("div");
+ row.className = "speaker-row";
+ var label = document.createElement("label");
+ label.className = "field-label";
+ label.textContent = speaker.id;
+ var input = document.createElement("input");
+ input.type = "text";
+ input.className = "select-field speaker-name-input";
+ input.value = speaker.name;
+ input.maxLength = 120;
+ input.setAttribute("aria-label", "Nome de " + speaker.id);
+ input.addEventListener("input", function () { silenceState.speakers[index].name = input.value.trim() || speaker.id; });
+ var activeLabel = document.createElement("label");
+ activeLabel.className = "checkbox-field speaker-active-field";
+ var active = document.createElement("input");
+ active.type = "checkbox";
+ active.checked = speaker.active;
+ active.addEventListener("change", function () { silenceState.speakers[index].active = active.checked; });
+ var activeText = document.createElement("span");
+ activeText.textContent = "Incluir no vídeo";
+ activeLabel.appendChild(active);
+ activeLabel.appendChild(activeText);
+ row.appendChild(label);
+ row.appendChild(input);
+ row.appendChild(activeLabel);
+ list.appendChild(row);
+ });
+ config.hidden = silenceState.speakers.length === 0;
+}
+
+function silenceLoadSpeakerConfiguration() {
+ if (!silenceState.transcriptPath) return;
+ try {
+ var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
+ var diarization = transcript.diarization;
+ if (diarization && Array.isArray(diarization.speakers)) renderSpeakerConfiguration(diarization.speakers);
+ } catch (e) {}
+}
+
+function speakerConfigurationForJson() {
+ var speakers = silenceState.speakers.map(function (speaker) {
+ var active = speaker.active !== false;
+ return {
+ id: speaker.id,
+ name: speaker.name || speaker.id,
+ status: active ? "active" : "inactive",
+ include_in_video: active,
+ editorial_instruction: active
+ ? "Manter as falas deste falante, salvo decisão específica da IA."
+ : "Excluir todas as falas deste falante do vídeo final.",
+ };
+ });
+ return {
+ total_detected: speakers.length,
+ speakers: speakers,
+ inactive_speakers: speakers.filter(function (speaker) { return !speaker.include_in_video; }).map(function (speaker) { return speaker.id; }),
+ instruction: "Falantes inativos devem ter todas as suas falas removidas do vídeo final; não confundir nome com identidade confirmada.",
+ };
+}
+
// Depois de mutar silenceState.transcriptPath em lugar (diarização, métricas de
// voz), propaga a mudança pra cópia central de cache, se forem arquivos diferentes.
function syncTranscriptToCache() {
@@ -1818,6 +1909,7 @@ function silenceGenerateJson() {
transcript.settings = Object.assign({}, transcript.settings, {
silence_removal: { enabled: silenceEnabled, min_silence_seconds: minSilence },
});
+ transcript.speaker_configuration = speakerConfigurationForJson();
ensureDir(silenceState.outputDir);
var out = path.join(silenceState.outputDir, "dados-para-ia.json");
diff --git a/code/cep-plugin/styles.css b/code/cep-plugin/styles.css
index 7812d2c..c8b6c86 100755
--- a/code/cep-plugin/styles.css
+++ b/code/cep-plugin/styles.css
@@ -400,6 +400,14 @@ textarea { padding: 8px 10px; font: 10px var(--font-mono); color: var(--text-sec
.mini-action strong { display: block; font-size: 10.5px; font-weight: 600; }
.mini-action span { display: block; margin-top: 3px; color: var(--text-muted); font-size: 9.2px; line-height: 1.4; }
.mini-action .button { width: auto; flex: 0 0 auto; padding: 0 12px; height: 28px; }
+.speaker-config { margin: 8px 0; padding: 10px; border: 1px solid var(--border); border-radius: 6px; background: var(--surface-deep); }
+.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 .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 .field-label { margin: 0; }
+.speaker-name-input { width: 100%; }
+.speaker-active-field { margin: 0; padding: 6px 8px; white-space: nowrap; }
/* ------------------------------- Callouts ------------------------------- */
.callout { padding: 10px 11px; border: 1px solid var(--border); border-left-width: 3px; border-radius: 6px; background: var(--surface-deep); }
@@ -622,4 +630,3 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
.analise-fala strong { display: inline-block; min-width: 34px; color: var(--text-muted); font-weight: 600; text-transform: uppercase; }
.analise-fala.analise-vazia { color: var(--text-muted); font-style: italic; }
.analise-evidencias { margin: 6px 0 0; font-size: 10px; color: var(--text-muted); }
-