feat: sincronizado o estado de transcrição, métricas e diarização

- 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
This commit is contained in:
João Henrique
2026-09-09 19:45:15 -04:00
parent c688d0eabf
commit 19d10e61a1
6 changed files with 220 additions and 12 deletions
+7 -3
View File
@@ -269,6 +269,11 @@
<!-- ============================== SCANNER =============================== -->
<div class="tab-panel" id="tabScanner" role="tabpanel" aria-labelledby="tabBtnScanner">
<p class="tab-intro">Escolha o que será lido no arquivo original. O Scanner usa os tempos da timeline, mas não exporta frames pelo Premiere.</p>
<section class="card scanner-existing-analysis">
<div class="card-head"><h2>Análises já realizadas</h2><span class="card-kicker">Banco de dados</span></div>
<div class="field-grid"><div><label class="field-label" for="scannerExistingAnalysis">Selecione uma análise salva</label><select id="scannerExistingAnalysis" class="select-field" onchange="scannerSelecionarAnaliseExistente()"><option value="">Nenhuma análise carregada</option></select></div><div><span class="field-label">Estado</span><p class="field-static" id="scannerExistingAnalysisStatus">Verificando o banco…</p></div></div>
<p class="field-help" id="scannerExistingAnalysisInfo">As análises concluídas ficam disponíveis para a tela Editar vídeo.</p>
</section>
<section class="card scanner-mode-card">
<div class="card-head"><h2>Tipo de análise</h2><span class="card-kicker">Primeiro escolha o fluxo</span></div>
<div class="scanner-mode-options" role="radiogroup" aria-label="Tipo de análise do Scanner">
@@ -323,8 +328,8 @@
<div class="scanner-track-list"><span class="field-label">Faixas de áudio</span><div id="scannerAudioTracks"></div></div>
<div class="scanner-subsection"><div class="card-head"><h3>Transcrição</h3><span class="card-kicker" id="scannerModelCount">—</span></div>
<div class="field-grid"><div><label class="field-label" for="scannerTranscribeModel">Modelo instalado</label><select id="scannerTranscribeModel" class="select-field"></select></div><div><label class="field-label" for="scannerTranscribeLanguage">Idioma</label><select id="scannerTranscribeLanguage" class="select-field"></select></div></div>
<div class="scanner-option-row"><label class="checkbox-field"><input id="scannerDiarization" type="checkbox"><span>Identificar locutores após transcrever</span></label><button class="button button-mini button-ghost" id="btnScannerDiarization" onclick="scannerProcessarComplemento('diarizacao')" type="button" disabled>Processar</button></div>
<div class="scanner-option-row"><label class="checkbox-field"><input id="scannerVoiceFeatures" type="checkbox"><span>Calcular métricas da fala</span></label><button class="button button-mini button-ghost" id="btnScannerVoiceFeatures" onclick="scannerProcessarComplemento('metricas_de_fala')" type="button" disabled>Processar</button></div>
<div class="scanner-option-row"><label class="checkbox-field"><input id="scannerDiarization" type="checkbox"><span>Identificar locutores após transcrever</span></label><span class="scanner-analysis-status" id="scannerDiarizationStatus">Ainda não processado</span></div>
<div class="scanner-option-row"><label class="checkbox-field"><input id="scannerVoiceFeatures" type="checkbox"><span>Calcular métricas da fala</span></label><span class="scanner-analysis-status" id="scannerVoiceFeaturesStatus">Ainda não processado</span></div>
<p class="field-help">Mede pitch, energia, velocidade de fala e pausas por trecho. Não classifica emoção e tem custo baixo comparado à transcrição.</p>
<div class="hf-token-status" id="scannerHfStatus" data-state="unset"><span class="check-dot" aria-hidden="true"></span><span id="scannerHfStatusText">Hugging Face não configurado</span><button class="link-button" type="button" onclick="switchTab('settings')">Configurar</button></div>
<p class="field-help">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.</p>
@@ -334,7 +339,6 @@
<section class="card scanner-execution-card">
<div class="button-row"><button class="button button-ghost" onclick="saveScannerProfile()" type="button">Salvar perfil</button><button class="button button-primary" id="btnScannerProcess" onclick="scannerProcess()" type="button" disabled>Processar áudio</button></div>
<p class="field-help" id="scannerProfileStatus">Perfil padrão pronto.</p>
<div class="callout callout-warning" id="scannerReprocessConfirm" hidden><strong>Este Scanner já foi processado</strong><p>Se continuar, a transcrição e as análises serão executadas novamente. Quer reprocessar o áudio?</p><div class="button-row"><button class="button button-primary" onclick="scannerConfirmarReprocessamento(true)" type="button">Reprocessar</button><button class="button button-ghost" onclick="scannerConfirmarReprocessamento(false)" type="button">Cancelar</button></div></div>
<div class="callout callout-info" id="scannerReportCard" hidden><strong>Relatório pronto</strong><p id="scannerReportPath"></p><div class="button-row"><button class="button button-ghost" onclick="scannerOpenReport()" type="button">Abrir JSON</button><button class="button button-ghost" onclick="scannerCopyReport()" type="button">Copiar conteúdo</button></div></div>
</section>
</div>
+123 -9
View File
@@ -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 = "<option value=''>Nenhuma análise carregada</option>";
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<c.numTracks;i++){var t=c[i],clips=[];for(var j=0;j<t.clips.numItems;j++){var x=t.clips[j],p=x.projectItem;clips.push({nodeId:x.nodeId,name:x.name,start:Number(x.start.seconds),end:Number(x.end.seconds),inPoint:Number(x.inPoint.seconds),outPoint:Number(x.outPoint.seconds),duration:Number(x.duration.seconds),sourceFile:p&&p.getMediaPath?p.getMediaPath():'',sourceOffline:p&&p.isOffline?p.isOffline():false});}a.push({index:i,name:t.name||('Faixa '+(i+1)),numClips:t.clips.numItems,clips:clips});}}var v=[],a=[];tracks(s.videoTracks,v);tracks(s.audioTracks,a);return JSON.stringify({ok:true,id:s.sequenceID,name:s.name,end:Number(s.end.seconds),videoTracks:v,audioTracks:a});}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())", function(raw) { setBusy("btnScannerDetectTracks", false); var data; try { data=JSON.parse(raw); } catch (_) { data={ok:false,error:"Resposta inválida do Premiere."}; } if (!data.ok) { document.getElementById("scannerTracksHelp").textContent=data.error; showToast("err",data.error); return; } scannerTimeline=data; scannerCarregarRelatorioExistente(); scannerAtualizarComplementos(); var saved; try { saved=JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY)||"null")||{}; } catch (_) { saved={}; } scannerRenderTracks("video",data.videoTracks,saved.faixas_de_video||data.videoTracks.map(function(t){return t.index;})); scannerRenderTracks("audio",data.audioTracks,saved.faixas_de_audio||[]); document.getElementById("scannerTracksHelp").textContent=data.name+": "+data.videoTracks.length+" faixa(s) de vídeo e "+data.audioTracks.length+" de áudio detectadas."; document.getElementById("btnScannerProcess").disabled=false; });
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<c.numTracks;i++){var t=c[i],clips=[];for(var j=0;j<t.clips.numItems;j++){var x=t.clips[j],p=x.projectItem;clips.push({nodeId:x.nodeId,name:x.name,start:Number(x.start.seconds),end:Number(x.end.seconds),inPoint:Number(x.inPoint.seconds),outPoint:Number(x.outPoint.seconds),duration:Number(x.duration.seconds),sourceFile:p&&p.getMediaPath?p.getMediaPath():'',sourceOffline:p&&p.isOffline?p.isOffline():false});}a.push({index:i,name:t.name||('Faixa '+(i+1)),numClips:t.clips.numItems,clips:clips});}}var v=[],a=[];tracks(s.videoTracks,v);tracks(s.audioTracks,a);return JSON.stringify({ok:true,id:s.sequenceID,name:s.name,end:Number(s.end.seconds),videoTracks:v,audioTracks:a});}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())", function(raw) { setBusy("btnScannerDetectTracks", false); var data; try { data=JSON.parse(raw); } catch (_) { data={ok:false,error:"Resposta inválida do Premiere."}; } if (!data.ok) { document.getElementById("scannerTracksHelp").textContent=data.error; showToast("err",data.error); return; } scannerTimeline=data; scannerCarregarRelatorioExistente(); scannerAtualizarComplementos(); var saved; try { saved=JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY)||"null")||{}; } catch (_) { saved={}; } scannerRenderTracks("video",data.videoTracks,saved.faixas_de_video||data.videoTracks.map(function(t){return t.index;})); scannerRenderTracks("audio",data.audioTracks,saved.faixas_de_audio||[]); document.getElementById("scannerTracksHelp").textContent=data.name+": "+data.videoTracks.length+" faixa(s) de vídeo e "+data.audioTracks.length+" de áudio detectadas."; if(!scannerRelatorioExistente){document.getElementById("btnScannerProcess").disabled=false;document.getElementById("btnScannerProcess").textContent="Processar áudio";} });
}
function scannerProcess() {
if (scannerRelatorioExistente) { scannerReprocessamentoPendente = true; document.getElementById("scannerReprocessConfirm").hidden = false; return; }
if (scannerRelatorioExistente) { showToast("ok", "Esta análise já está pronta. Abra Editar vídeo para utilizá-la."); return; }
scannerIniciarProcessamento();
}
@@ -2066,6 +2134,37 @@ function silenceConverterRelatorioDoScannerParaTranscricao(caminhoDoRelatorio, d
return { version: 1, segments: segmentos, origem: "scanner" };
}
function silenceConsultarStatusNoBanco(videoId) {
if (!videoId || !childProcess.spawnSync) return null;
try {
var resposta = childProcess.spawnSync(
SCANNER_PYTHON_BIN,
[ANALISE_STATUS_SCRIPT, videoId, "--banco", "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db"],
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", encoding: "utf-8", maxBuffer: 1024 * 1024 }
);
if (resposta.status !== 0) return null;
return JSON.parse(String(resposta.stdout || "{}"));
} catch (_) { return null; }
}
function silenceLocalizarAudioParaFalantes(pasta) {
var encontrados = [];
function visitar(caminho) {
if (encontrados.length) return;
try {
fs.readdirSync(caminho).forEach(function (nome) {
if (encontrados.length) return;
var item = path.join(caminho, nome);
var estatisticas = fs.statSync(item);
if (estatisticas.isDirectory()) visitar(item);
else if (/\.wav$/i.test(nome)) encontrados.push(item);
});
} catch (_) {}
}
visitar(pasta);
return encontrados[0] || null;
}
function silenceLoadScannerAnalysis() {
var status = document.getElementById("scannerAnalysisStatus");
var info = document.getElementById("scannerAnalysisInfo");
@@ -2086,22 +2185,37 @@ function silenceLoadScannerAnalysis() {
silenceState.mediaPath = null;
silenceState.fileBaseName = path.basename(path.dirname(caminhoDoRelatorio));
silenceState.outputDir = path.dirname(caminhoDoRelatorio);
silenceState.audioCachePath = silenceLocalizarAudioParaFalantes(silenceState.outputDir);
silenceState.transcriptPath = path.join(silenceState.outputDir, "transcricao-do-scanner.json");
silenceState.cachedTranscriptPath = null;
fs.writeFileSync(silenceState.transcriptPath, JSON.stringify(
silenceConverterRelatorioDoScannerParaTranscricao(caminhoDoRelatorio, dadosDoRelatorio), null, 2), "utf-8");
var transcricao = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
var quantidade = transcricao.segments.length;
var statusDoBanco = silenceConsultarStatusNoBanco(silenceState.sequenceId);
if (!statusDoBanco || !statusDoBanco.audio) throw new Error("A análise não foi encontrada no banco de dados.");
var audioDoBanco = statusDoBanco.audio;
var falantes = audioDoBanco.falantes || [];
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);
status.textContent = "Carregada";
info.textContent = silenceState.sequenceName + " · " + quantidade + " trecho(s) vindos do Scanner.";
info.textContent = silenceState.sequenceName + " · " + quantidade + " trecho(s) · " +
(audioDoBanco.metricas_de_voz_disponiveis ? "métricas disponíveis" : "métricas não realizadas") + " · " +
(audioDoBanco.diarizacao_disponivel ? falantes.length + " falante(s)" : "diarização não realizada");
setStep(3, "ready", "Pronto");
setStep(4, "ready", "Pronto");
setStep(5, "locked", "Bloqueado");
document.getElementById("btnBuildPlan").disabled = false;
document.getElementById("btnDiarize").disabled = true;
document.getElementById("btnVoiceFeatures").disabled = true;
document.getElementById("silenceDiarizeInfo").textContent = "Os falantes disponíveis vieram da análise do Scanner.";
document.getElementById("silenceVoiceFeaturesInfo").textContent = "As métricas disponíveis vieram da análise do Scanner.";
document.getElementById("silenceDiarizeInfo").textContent = audioDoBanco.diarizacao_disponivel
? falantes.length + " falante(s) disponíveis no banco de dados."
: "Diarização não realizada nesta análise do Scanner.";
document.getElementById("silenceVoiceFeaturesInfo").textContent = audioDoBanco.metricas_de_voz_disponiveis
? audioDoBanco.falas_com_metricas + " trecho(s) com métricas de voz no banco."
: "Métricas de voz não realizadas nesta análise do Scanner.";
silenceLog("Análise do Scanner carregada: " + quantidade + " trecho(s).", "ok");
showToast("ok", "Análise do Scanner carregada.");
} catch (erro) {
+2
View File
@@ -174,6 +174,8 @@ h1, h2, h3 { margin: 0; }
.scanner-option-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.scanner-option-row .checkbox-field { flex: 1; }
.button-mini { padding: 4px 8px; font-size: 10px; }
.scanner-analysis-status { flex: 0 0 auto; color: var(--text-muted); font-size: 11px; }
.scanner-analysis-status[data-state="done"] { color: var(--green, #8ed081); }
.editorial-plan-json { width: 100%; min-height: 150px; resize: vertical; box-sizing: border-box; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; line-height: 1.45; color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 9px; }
.editar-video-legado { display: none; }
.scanner-analysis-card { margin-bottom: 12px; }
@@ -0,0 +1,39 @@
"""Consulta o estado editorial persistido de uma análise do Scanner."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
import sys
CAMINHO_DO_CODIGO = Path(__file__).resolve().parent.parent
if str(CAMINHO_DO_CODIGO) not in sys.path:
sys.path.insert(0, str(CAMINHO_DO_CODIGO))
from engine.persistencia.consultas import ConsultasDeAnalises
def executar(caminho_do_banco: Path, video_id: str) -> 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()
+39
View File
@@ -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,
@@ -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: