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); } -