diff --git a/code/cep-plugin/index.html b/code/cep-plugin/index.html index 274c20a..70cce17 100755 --- a/code/cep-plugin/index.html +++ b/code/cep-plugin/index.html @@ -28,6 +28,9 @@ + @@ -229,6 +232,10 @@
Confirme quem são os falantes antes de editar. O áudio reproduz somente os trechos identificados de cada pessoa.
+Carregue a análise mais recente do Scanner para consultar os falantes detectados.
+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.
+A análise carregada não possui diarização. Execute a diarização no Scanner para liberar esta etapa.
Identifica repetições e tentativas alternativas usando a transcrição do Scanner. O painel nunca corta o vídeo: você revisa e decide.
diff --git a/code/cep-plugin/main.js b/code/cep-plugin/main.js index e71abe4..7b6fd3b 100755 --- a/code/cep-plugin/main.js +++ b/code/cep-plugin/main.js @@ -245,6 +245,7 @@ function switchTab(name) { var tabs = [ { key: "silence", panel: "tabSilence", btn: "tabBtnSilence" }, { key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" }, + { key: "refinar", panel: "tabRefinar", btn: "tabBtnRefinar" }, { key: "retakes", panel: "tabRetakes", btn: "tabBtnRetakes" }, { key: "analises", panel: "tabAnalises", btn: "tabBtnAnalises" }, { key: "tipos-video", panel: "tabTiposVideo", btn: "tabBtnTiposVideo" }, @@ -267,6 +268,7 @@ function switchTab(name) { if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); } if (name === "settings") renderEditorPersonalities(); if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); scannerListarAnalisesExistentes(); } + if (name === "refinar") refinarAoAbrir(); if (name === "retakes") retakesAoAbrir(); if (name === "analises") analisesAoAbrir(); if (name === "tipos-video") tiposVideoAoAbrir(); @@ -771,7 +773,8 @@ function tipoVideoCopiarJson() { } catch (erro) { showToast("err", erro.message); } } -function copiarTextoParaAreaDeTransferencia(texto) { +function copiarTextoParaAreaDeTransferencia(texto, mensagem) { + mensagem = mensagem || "Conteúdo copiado para a área de transferência."; function copiarComSelecaoDoPainel() { var area = document.createElement("textarea"); area.value = texto; @@ -785,7 +788,7 @@ function copiarTextoParaAreaDeTransferencia(texto) { var copiado = false; try { copiado = document.execCommand("copy"); } catch (_) { copiado = false; } document.body.removeChild(area); - if (copiado) showToast("ok", "JSON resumido copiado para a área de transferência."); + if (copiado) showToast("ok", mensagem); else showToast("err", "Não foi possível copiar o JSON. Selecione e copie manualmente."); } @@ -795,23 +798,23 @@ function copiarTextoParaAreaDeTransferencia(texto) { try { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(texto).then(function () { - showToast("ok", "JSON resumido copiado para a área de transferência."); + showToast("ok", mensagem); }).catch(copiarComSelecaoDoPainel); return; } } catch (_) {} copiarComSelecaoDoPainel(); - }); + }, mensagem); } -function copiarComPbcopy(texto, fallback) { +function copiarComPbcopy(texto, fallback, mensagem) { try { var copia = childProcess.spawn("pbcopy"); var finalizado = false; function concluir(copiado) { if (finalizado) return; finalizado = true; - if (copiado) showToast("ok", "JSON resumido copiado para a área de transferência."); + if (copiado) showToast("ok", mensagem || "Conteúdo copiado para a área de transferência."); else fallback(); } copia.once("error", function () { concluir(false); }); @@ -912,6 +915,26 @@ function analisesCopiar() { } catch (error) { showToast("err", error.message); } } +function abrirPastaDoJsonGerado() { + if (!silenceState.jsonOutputPath) return; + try { + childProcess.spawn("open", [path.dirname(silenceState.jsonOutputPath)], { detached: true, stdio: "ignore" }).unref(); + } catch (erro) { + showToast("err", "Não foi possível abrir a pasta no Finder: " + erro.message); + } +} + +function copiarCaminhoDoJsonGerado() { + if (!silenceState.jsonOutputPath) return; + copiarComPbcopy( + silenceState.jsonOutputPath, + function () { + copiarTextoParaAreaDeTransferencia(silenceState.jsonOutputPath, "Caminho do JSON copiado para a área de transferência."); + }, + "Caminho do JSON copiado para a área de transferência." + ); +} + // ---- Retakes --------------------------------------------------------------- // A aba apenas controla o processo e mostra resultados; a análise inteira // acontece na engine (scanner/retakes), chamada via executar_retakes.py. @@ -2169,9 +2192,11 @@ function silenceLoadScannerAnalysis() { var status = document.getElementById("scannerAnalysisStatus"); var info = document.getElementById("scannerAnalysisInfo"); setBusy("btnLoadScannerAnalysis", true); + setBusy("btnRefinarLoad", true); var caminhoDoRelatorio = silenceLocalizarUltimoRelatorioDoScanner(); if (!caminhoDoRelatorio) { setBusy("btnLoadScannerAnalysis", false); + setBusy("btnRefinarLoad", false); status.textContent = "Não encontrada"; info.textContent = "Execute o Scanner primeiro para criar uma análise reutilizável."; showToast("err", info.textContent); @@ -2199,7 +2224,8 @@ function silenceLoadScannerAnalysis() { var turnos = transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) { return { speaker: segmento.falante, start: segmento.start, end: segmento.end }; }); - if (falantes.length) renderSpeakerConfiguration(falantes, turnos); + renderSpeakerConfiguration(falantes, turnos); + atualizarTelaRefinar(falantes, audioDoBanco); status.textContent = "Carregada"; info.textContent = silenceState.sequenceName + " · " + quantidade + " trecho(s) · " + (audioDoBanco.metricas_de_voz_disponiveis ? "métricas disponíveis" : "métricas não realizadas") + " · " + @@ -2224,6 +2250,36 @@ function silenceLoadScannerAnalysis() { showToast("err", info.textContent); } setBusy("btnLoadScannerAnalysis", false); + setBusy("btnRefinarLoad", false); +} + +function refinarAoAbrir() { + var info = document.getElementById("refinarInfo"); + if (!info || !silenceState.sequenceId) return; + info.textContent = silenceState.sequenceName + " · " + (silenceState.speakers.length + ? silenceState.speakers.length + " falante(s) carregado(s)." + : "Nenhum falante carregado."); +} + +function refinarCarregarAnalise() { + silenceLoadScannerAnalysis(); +} + +function atualizarTelaRefinar(falantes, audioDoBanco) { + var status = document.getElementById("refinarStatus"); + var info = document.getElementById("refinarInfo"); + var vazio = document.getElementById("refinarSpeakerEmpty"); + var count = document.getElementById("refinarSpeakerCount"); + var save = document.getElementById("btnRefinarSave"); + if (!status || !info || !vazio || !count) return; + var quantidade = Array.isArray(falantes) ? falantes.length : 0; + status.textContent = quantidade ? "Pronto" : "Sem diarização"; + info.textContent = silenceState.sequenceName + " · " + (audioDoBanco && audioDoBanco.diarizacao_disponivel + ? quantidade + " falante(s) detectado(s)." + : "A transcrição está disponível, mas a diarização não foi realizada."); + count.textContent = quantidade ? quantidade + (quantidade === 1 ? " falante detectado" : " falantes detectados") : "Nenhum falante carregado"; + vazio.hidden = quantidade > 0; + if (save) save.disabled = quantidade === 0; } var speakerAudioPlayer = null; @@ -2490,6 +2546,7 @@ function silenceSelectSequence() { setStep(2, "ready", "Sua vez"); document.getElementById("btnTranscribe").disabled = false; document.getElementById("silenceJsonCard").hidden = true; + document.getElementById("jsonFileActions").hidden = true; document.getElementById("silencePlanInfo").textContent = ""; silenceLog("Timeline escolhida: " + result.sequenceName + " — " + result.mediaPath); renderCachedTranscripts(); @@ -2748,6 +2805,7 @@ function silenceDiarizeSpeakers(onFinished) { if (parsed && Array.isArray(parsed.speakers)) { silenceState.speakerTurns = Array.isArray(parsed.turns) ? parsed.turns : []; renderSpeakerConfiguration(parsed.speakers, silenceState.speakerTurns); + atualizarTelaRefinar(parsed.speakers, { diarizacao_disponivel: true }); } var msg = parsed ? "Foram detectados " + parsed.num_speakers + (parsed.num_speakers === 1 ? " falante" : " falantes") + ": " + parsed.speakers.join(", ") + "." @@ -2767,20 +2825,45 @@ function speakerNameDefault(speakerId) { return speakerId; } -function renderSpeakerConfiguration(speakerIds, speakerTurns) { - var config = document.getElementById("silenceSpeakerConfig"); - var list = document.getElementById("silenceSpeakerList"); - var count = document.getElementById("silenceSpeakerCount"); +function carregarNomesDeFalantes() { + if (!silenceState.sequenceId) return {}; + try { + var salvo = JSON.parse(localStorage.getItem("mcp_falantes_" + silenceState.sequenceId) || "{}"); + return salvo && typeof salvo === "object" ? salvo : {}; + } catch (_) { return {}; } +} + +function salvarNomesDeFalantes() { + if (!silenceState.sequenceId) return; + var salvo = {}; + silenceState.speakers.forEach(function (speaker) { + salvo[speaker.id] = { name: speaker.name, active: speaker.active !== false }; + }); + try { localStorage.setItem("mcp_falantes_" + silenceState.sequenceId, JSON.stringify(salvo)); } catch (_) {} +} + +function refinarSalvarFalantes() { + salvarNomesDeFalantes(); + showToast("ok", "Identificação dos falantes salva."); +} + +function renderSpeakerConfiguration(speakerIds, speakerTurns, alvo) { + alvo = alvo || {}; + var config = document.getElementById(alvo.config || "silenceSpeakerConfig"); + var list = document.getElementById(alvo.list || "silenceSpeakerList"); + var count = document.getElementById(alvo.count || "silenceSpeakerCount"); if (!config || !list || !count) return; silenceState.speakerTurns = Array.isArray(speakerTurns) ? speakerTurns : silenceState.speakerTurns; var previous = {}; silenceState.speakers.forEach(function (speaker) { previous[speaker.id] = speaker; }); + var salvo = carregarNomesDeFalantes(); silenceState.speakers = speakerIds.map(function (id) { var old = previous[id]; + var salvoSpeaker = salvo[id] || {}; return { id: id, - name: old && old.name ? old.name : speakerNameDefault(id), - active: !old || old.active !== false, + name: old && old.name ? old.name : (salvoSpeaker.name || speakerNameDefault(id)), + active: old ? old.active !== false : salvoSpeaker.active !== false, }; }); count.textContent = silenceState.speakers.length + @@ -2806,13 +2889,19 @@ function renderSpeakerConfiguration(speakerIds, speakerTurns) { input.placeholder = "Nome do falante"; input.maxLength = 120; input.setAttribute("aria-label", "Nome de " + speaker.id); - input.addEventListener("input", function () { silenceState.speakers[index].name = input.value.trim() || speaker.id; }); + input.addEventListener("input", function () { + silenceState.speakers[index].name = input.value.trim() || speaker.id; + salvarNomesDeFalantes(); + }); 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; }); + active.addEventListener("change", function () { + silenceState.speakers[index].active = active.checked; + salvarNomesDeFalantes(); + }); var activeText = document.createElement("span"); activeText.textContent = "Incluir no vídeo"; activeLabel.appendChild(active); @@ -2824,6 +2913,12 @@ function renderSpeakerConfiguration(speakerIds, speakerTurns) { list.appendChild(row); }); config.hidden = silenceState.speakers.length === 0; + var refineConfig = document.getElementById("refinarSpeakerConfig"); + if (refineConfig && config.id !== refineConfig.id) { + renderSpeakerConfiguration(speakerIds, speakerTurns, { + config: "refinarSpeakerConfig", list: "refinarSpeakerList", count: "refinarSpeakerCount" + }); + } } function speakerAudioFileUrl(filePath) { @@ -3012,6 +3107,7 @@ function silenceGenerateJson() { pathCell.textContent = out; pathCell.title = out; document.getElementById("silenceJsonCard").hidden = false; + document.getElementById("jsonFileActions").hidden = false; document.getElementById("silencePlanInfo").textContent = "Envie este arquivo a um agente de IA externo. Ele deve devolver o plano de edição em JSON para o passo 5."; diff --git a/code/cep-plugin/styles.css b/code/cep-plugin/styles.css index 82b3b3f..0e1b115 100755 --- a/code/cep-plugin/styles.css +++ b/code/cep-plugin/styles.css @@ -417,6 +417,8 @@ textarea { padding: 8px 10px; font: 10px var(--font-mono); color: var(--text-sec .inline-form > * + * { margin-top: 8px; } .button-row { display: flex; gap: 8px; } .button-row .button { flex: 1; } +.json-file-actions { margin-top: 8px; } +.json-file-actions .button { min-width: 0; } /* -------------------------- Catálogo editorial ------------------------- */ .video-type-list { display: grid; gap: 6px; } @@ -479,6 +481,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; } +.refinar-actions { display: flex; gap: 8px; flex-wrap: wrap; } +.refinar-actions .button { flex: 1 1 180px; } +#refinarSpeakerConfig .button { margin-top: 10px; } /* ------------------------------- Callouts ------------------------------- */ .callout { padding: 10px 11px; border: 1px solid var(--border); border-left-width: 3px; border-radius: 6px; background: var(--surface-deep); }