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.
-
Este Scanner já foi processado
Se continuar, a transcrição e as análises serão executadas novamente. Quer reprocessar o áudio?
Relatório 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: