From 19d10e61a150534562fcc7c571885640f743c948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Henrique?= Date: Wed, 9 Sep 2026 19:45:15 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20sincronizado=20o=20estado=20de=20transc?= =?UTF-8?q?ri=C3=A7=C3=A3o,=20m=C3=A9tricas=20e=20diariza=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sincronizado o estado de transcrição, métricas e diarização entre SQLite, Scanner e Editar vídeo Resumo: - 6 arquivos alterados - 1 novos - 5 modificados - 0 removidos 5 files changed, 181 insertions(+), 12 deletions(-) Arquivos: - code/cep-plugin/index.html - code/cep-plugin/main.js - code/cep-plugin/styles.css - code/engine/persistencia/consultas.py - code/engine/testes/test_consultas_de_persistencia.py - code/engine/consultar_status_da_analise.py --- code/cep-plugin/index.html | 10 +- code/cep-plugin/main.js | 132 ++++++++++++++++-- code/cep-plugin/styles.css | 2 + code/engine/consultar_status_da_analise.py | 39 ++++++ code/engine/persistencia/consultas.py | 39 ++++++ .../testes/test_consultas_de_persistencia.py | 10 ++ 6 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 code/engine/consultar_status_da_analise.py diff --git a/code/cep-plugin/index.html b/code/cep-plugin/index.html index a4cea9d..274c20a 100755 --- a/code/cep-plugin/index.html +++ b/code/cep-plugin/index.html @@ -269,6 +269,11 @@

Escolha o que será lido no arquivo original. O Scanner usa os tempos da timeline, mas não exporta frames pelo Premiere.

+
+

Análises já realizadas

Banco de dados
+
Estado

Verificando o banco…

+

As análises concluídas ficam disponíveis para a tela Editar vídeo.

+

Tipo de análise

Primeiro escolha o fluxo
@@ -323,8 +328,8 @@
Faixas de áudio

Transcrição

—
-
-
+
Ainda não processado
+
Ainda não processado

Mede pitch, energia, velocidade de fala e pausas por trecho. Não classifica emoção e tem custo baixo comparado à transcrição.

Hugging Face não configurado

A lista inclui somente modelos encontrados nas pastas locais configuradas. O token do Hugging Face é usado apenas na diarização; nunca é salvo no perfil nem no relatório.

@@ -334,7 +339,6 @@

Perfil padrão pronto.

-
diff --git a/code/cep-plugin/main.js b/code/cep-plugin/main.js index 23855ea..e71abe4 100755 --- a/code/cep-plugin/main.js +++ b/code/cep-plugin/main.js @@ -266,7 +266,7 @@ function switchTab(name) { if (name === "models") renderModelList(); if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); } if (name === "settings") renderEditorPersonalities(); - if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); } + if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); scannerListarAnalisesExistentes(); } if (name === "retakes") retakesAoAbrir(); if (name === "analises") analisesAoAbrir(); if (name === "tipos-video") tiposVideoAoAbrir(); @@ -920,6 +920,7 @@ var RETAKES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/co // o `/usr/bin/python3` do macOS (3.9) não aceita. Usar o mesmo venv do projeto // (Python 3.14, com faster_whisper) garante que Scanner e Retakes carreguem bem. var SCANNER_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python"; +var ANALISE_STATUS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_status_da_analise.py"; var retakesCasoAtual = null; var retakesGrupos = []; var retakesProcesso = null; @@ -1110,6 +1111,7 @@ var scannerTimeline = null; var scannerReportPath = null; var scannerRelatorioExistente = null; var scannerReprocessamentoPendente = false; +var scannerAnalisesExistentes = []; var scannerProcesso = null; var scannerCancelSolicitado = false; var SCANNER_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_scanner.py"; @@ -1131,19 +1133,85 @@ function scannerCarregarRelatorioExistente() { } catch (_) { scannerRelatorioExistente = null; } } +function scannerListarAnalisesExistentes() { + var seletor = document.getElementById("scannerExistingAnalysis"); + var status = document.getElementById("scannerExistingAnalysisStatus"); + var info = document.getElementById("scannerExistingAnalysisInfo"); + if (!seletor) return; + scannerAnalisesExistentes = []; + try { + fs.readdirSync(scannerOutputRoot()).forEach(function (nomeDaPasta) { + var caminho = path.join(scannerOutputRoot(), nomeDaPasta, nomeDaPasta + "-resultado.json"); + if (!fs.existsSync(caminho)) return; + try { + var dados = JSON.parse(fs.readFileSync(caminho, "utf-8")); + var estado = silenceConsultarStatusNoBanco((dados.sequencia || {}).id); + if (estado) scannerAnalisesExistentes.push({ caminho: caminho, dados: dados, estado: estado }); + } catch (_) {} + }); + } catch (_) {} + scannerAnalisesExistentes.sort(function (a, b) { + try { return fs.statSync(b.caminho).mtimeMs - fs.statSync(a.caminho).mtimeMs; } catch (_) { return 0; } + }); + seletor.innerHTML = ""; + if (!scannerAnalisesExistentes.length) { + seletor.innerHTML = ""; + status.textContent = "Nenhuma análise"; + info.textContent = "Execute o Scanner para criar a primeira análise persistida."; + return; + } + scannerAnalisesExistentes.forEach(function (analise, indice) { + var audio = analise.estado.audio || {}; + var option = document.createElement("option"); + option.value = String(indice); + option.textContent = (analise.dados.sequencia || {}).nome + " · " + audio.total_falas + " falas"; + seletor.appendChild(option); + }); + seletor.selectedIndex = 0; + scannerSelecionarAnaliseExistente(); +} + +function scannerSelecionarAnaliseExistente() { + var seletor = document.getElementById("scannerExistingAnalysis"); + var analise = scannerAnalisesExistentes[Number(seletor && seletor.value)]; + var status = document.getElementById("scannerExistingAnalysisStatus"); + var info = document.getElementById("scannerExistingAnalysisInfo"); + if (!analise) return; + scannerRelatorioExistente = analise.caminho; + scannerReportPath = analise.caminho; + var audio = analise.estado.audio || {}; + status.textContent = (audio.metricas_de_voz_disponiveis ? "Métricas prontas" : "Sem métricas") + + " · " + (audio.diarizacao_disponivel ? "Diarização pronta" : "Sem diarização"); + info.textContent = "Transcrição: " + (audio.transcricao_disponivel ? "pronta" : "não encontrada") + + " · " + audio.total_falas + " falas · " + (audio.falantes || []).length + " falante(s)."; + scannerAtualizarComplementos(); +} + function scannerAtualizarComplementos() { var relatorio = scannerRelatorioExistente; var arquivoDiarizacao = document.getElementById("btnScannerDiarization"); var arquivoMetricas = document.getElementById("btnScannerVoiceFeatures"); - if (!relatorio) { if (arquivoDiarizacao) arquivoDiarizacao.disabled = true; if (arquivoMetricas) arquivoMetricas.disabled = true; return; } + var statusDiarizacao = document.getElementById("scannerDiarizationStatus"); + var statusMetricas = document.getElementById("scannerVoiceFeaturesStatus"); + var botaoProcessar = document.getElementById("btnScannerProcess"); + if (!relatorio) { + if (arquivoDiarizacao) arquivoDiarizacao.disabled = true; + if (arquivoMetricas) arquivoMetricas.disabled = true; + if (statusDiarizacao) statusDiarizacao.textContent = "Ainda não processado"; + if (statusMetricas) statusMetricas.textContent = "Ainda não processado"; + return; + } var dados = {}; try { dados = JSON.parse(fs.readFileSync(relatorio, "utf-8")); } catch (_) {} var transcricao = dados.perfil && dados.perfil.transcricao || {}; var audio = dados.artefatos && dados.artefatos.audio || []; var metricas = !!(dados.perfil && dados.perfil.metricas_de_fala) || !!transcricao.metricas_de_fala; var diarizacao = !!transcricao.diarizacao; - if (arquivoDiarizacao) { arquivoDiarizacao.disabled = diarizacao || !loadHfToken(); arquivoDiarizacao.textContent = diarizacao ? "Concluído" : "Processar"; } - if (arquivoMetricas) { arquivoMetricas.disabled = metricas || !audio.length; arquivoMetricas.textContent = metricas ? "Concluído" : "Processar"; } + if (arquivoDiarizacao) arquivoDiarizacao.disabled = true; + if (arquivoMetricas) arquivoMetricas.disabled = true; + if (statusDiarizacao) { statusDiarizacao.textContent = diarizacao ? "Disponível" : "Não realizada"; statusDiarizacao.dataset.state = diarizacao ? "done" : "pending"; } + if (statusMetricas) { statusMetricas.textContent = metricas ? "Disponíveis" : "Não realizadas"; statusMetricas.dataset.state = metricas ? "done" : "pending"; } + if (botaoProcessar) { botaoProcessar.disabled = true; botaoProcessar.textContent = "Já processado"; } } function scannerSelectedTracks(kind) { return Array.prototype.slice.call(document.querySelectorAll("[data-scanner-track='" + kind + "']:checked")).map(function (item) { return Number(item.value); }).sort(function (a, b) { return a - b; }); } @@ -1245,11 +1313,11 @@ function modelNameFromPath(folder) { var value=String(folder).replace(/.*faster- function scannerDetectTracks() { setBusy("btnScannerDetectTracks", true); document.getElementById("scannerTracksHelp").textContent = "Lendo as faixas da sequência ativa…"; - cs.evalScript("(function(){try{var s=app.project.activeSequence;if(!s)return JSON.stringify({ok:false,error:'Nenhuma sequência ativa.'});function tracks(c,a){for(var i=0;i dict: + """Consulta o banco e devolve o estado da análise solicitada.""" + resultado = ConsultasDeAnalises(caminho_do_banco).consultar_status_da_analise(video_id) + if resultado is None: + raise ValueError(f"Vídeo não encontrado no banco: {video_id}") + return resultado + + +def principal() -> None: + """Executa a consulta pela linha de comando.""" + argumentos = argparse.ArgumentParser() + argumentos.add_argument("video_id") + argumentos.add_argument("--banco", type=Path, default=Path(".jhonny/analises.db")) + opcoes = argumentos.parse_args() + try: + print(json.dumps(executar(opcoes.banco, opcoes.video_id), ensure_ascii=False)) + except (OSError, ValueError) as erro: + print(json.dumps({"erro": str(erro)}, ensure_ascii=False)) + raise SystemExit(1) from erro + + +if __name__ == "__main__": + principal() diff --git a/code/engine/persistencia/consultas.py b/code/engine/persistencia/consultas.py index 5d55382..d59d195 100644 --- a/code/engine/persistencia/consultas.py +++ b/code/engine/persistencia/consultas.py @@ -96,6 +96,45 @@ class ConsultasDeAnalises: "total_cenas": contagens["cenas"], } + def consultar_status_da_analise(self, video_id: str) -> dict | None: + """Devolve o estado compacto das análises de áudio de um vídeo.""" + self._validar_texto_obrigatorio("video_id", video_id) + resumo = self.consultar_resumo_do_video(video_id) + if resumo is None: + return None + linha = self.conexao.execute( + """SELECT + COUNT(*) AS falas, + SUM(CASE WHEN falante IS NOT NULL AND TRIM(falante) <> '' THEN 1 ELSE 0 END) AS falas_com_falante, + SUM(CASE WHEN caracteristicas_acusticas IS NOT NULL + AND caracteristicas_acusticas <> '{}' + THEN 1 ELSE 0 END) AS falas_com_metricas + FROM segmentos_de_transcricao + WHERE video_id = ?""", + (video_id,), + ).fetchone() + falantes = [ + item["falante"] for item in self.conexao.execute( + """SELECT DISTINCT falante + FROM segmentos_de_transcricao + WHERE video_id = ? AND falante IS NOT NULL AND TRIM(falante) <> '' + ORDER BY falante""", + (video_id,), + ).fetchall() + ] + return { + **resumo, + "audio": { + "transcricao_disponivel": bool(linha["falas"]), + "total_falas": linha["falas"] or 0, + "falas_com_falante": linha["falas_com_falante"] or 0, + "falas_com_metricas": linha["falas_com_metricas"] or 0, + "diarizacao_disponivel": bool(linha["falas_com_falante"]), + "metricas_de_voz_disponiveis": bool(linha["falas_com_metricas"]), + "falantes": falantes, + }, + } + def listar_falas_no_intervalo( self, video_id: str, diff --git a/code/engine/testes/test_consultas_de_persistencia.py b/code/engine/testes/test_consultas_de_persistencia.py index 7e19aae..27af880 100644 --- a/code/engine/testes/test_consultas_de_persistencia.py +++ b/code/engine/testes/test_consultas_de_persistencia.py @@ -95,6 +95,16 @@ class TesteConsultasDeAnalises(unittest.TestCase): with self.assertRaises(ErroDeConsultaInvalida): self.consultas.consultar_resumo_do_video("") + def test_consultar_status_da_analise_expoe_falantes_e_metricas(self) -> None: + status = self.consultas.consultar_status_da_analise(VIDEO_ID) + + self.assertEqual(status["audio"]["total_falas"], 1) + self.assertEqual(status["audio"]["falas_com_falante"], 1) + self.assertEqual(status["audio"]["falas_com_metricas"], 0) + self.assertTrue(status["audio"]["diarizacao_disponivel"]) + self.assertFalse(status["audio"]["metricas_de_voz_disponiveis"]) + self.assertEqual(status["audio"]["falantes"], ["locutor"]) + # --- listar_falas_no_intervalo ------------------------------------------------------ def test_listar_falas_no_intervalo_filtra_por_tempo_e_omite_palavras_por_padrao(self) -> None: