feat: removido o tamanho do modelo do seletor da tela Editar vídeo

- removido o tamanho do modelo do seletor da tela Editar vídeo
- acelerada a transcrição do Scanner com inferência Whisper em lote e cache de hashes
- garantido um resultado separado para cada clipe de áudio selecionado no Scanner
- adicionado cancelamento do processamento do Scanner pela barra de progresso
- exibido estado visual próprio para Scanner cancelado na barra de progresso
- reorganizada a tela Scanner em blocos separados de imagem e som

Resumo:
- 7 arquivos alterados
- 1 novos
- 6 modificados
- 0 removidos

 6 files changed, 92 insertions(+), 40 deletions(-)

Arquivos:
  - .jhonny/analises.db
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/cep-plugin/styles.css
  - code/engine/integracoes/whisper/provider_de_transcricao_local.py
  - code/engine/scanner/transcricao_da_timeline.py
  - code/engine/testes/test_provider_de_transcricao_local.py
This commit is contained in:
João Henrique
2026-09-09 16:57:29 -04:00
parent 245dcd8015
commit 53a8307f38
7 changed files with 154 additions and 40 deletions
+30 -19
View File
@@ -60,6 +60,7 @@
<span class="task-spinner" id="taskSpinner" aria-hidden="true"></span>
<strong class="task-title" id="taskTitle">Processando…</strong>
<span class="task-elapsed" id="taskElapsed">00:00</span>
<button type="button" class="task-cancel" id="taskCancel" onclick="taskCancel()" hidden>Cancelar</button>
<button type="button" class="task-copy" id="taskCopy" onclick="taskCopyError()" aria-label="Copiar erro técnico" title="Copiar erro técnico" hidden>⧉</button>
</div>
<div class="progress-track" id="taskTrack" data-mode="indeterminate">
@@ -259,8 +260,10 @@
<!-- ============================== SCANNER =============================== -->
<div class="tab-panel" id="tabScanner" role="tabpanel" aria-labelledby="tabBtnScanner" hidden>
<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">
<div class="card-head"><h2>Amostragem e faixas</h2></div>
<div class="scanner-media-grid">
<section class="card scanner-media-card">
<div class="card-head"><h2>Imagem</h2><span class="card-kicker">Vídeo e cenas</span></div>
<p class="field-help">Escolha as faixas e os recursos visuais que o Scanner deve analisar.</p>
<label class="field-label" for="scannerInterval">Segundos por quadro</label>
<div class="range-field-row">
<input id="scannerInterval" type="range" min="0.2" max="10" step="0.1" value="1" oninput="scannerUpdateIntervalLabel()">
@@ -276,32 +279,40 @@
<p class="field-help">Reduzir a resolução acelera a extração e a leitura Vision sem perder precisão no rosto, pose ou objetos.</p>
<button class="button button-ghost" id="btnScannerDetectTracks" onclick="scannerDetectTracks()" type="button">Detectar faixas da sequência</button>
<p class="field-help" id="scannerTracksHelp">Abra a sequência desejada e detecte as faixas para selecioná-las.</p>
<div class="scanner-track-grid"><div><span class="field-label">Faixas de vídeo</span><div class="scanner-track-list" id="scannerVideoTracks"></div></div><div><span class="field-label">Faixas de áudio</span><div class="scanner-track-list" id="scannerAudioTracks"></div></div></div>
<div class="scanner-track-list"><span class="field-label">Faixas de vídeo</span><div id="scannerVideoTracks"></div></div>
<div class="scanner-subsection"><div class="card-head"><h3>O que detectar</h3></div>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="faces" checked><span>Rostos e qualidade facial</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="pessoas" checked><span>Pessoas</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="pose" checked><span>Pose e mãos</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="ocr" checked><span>Texto e códigos</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="categorias" checked><span>Objetos e categorias</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="estetica" checked><span>Estética, horizonte e geometria</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="segmentacao_de_pessoas" checked><span>Segmentação de pessoas</span></label>
</div>
<div class="scanner-subsection"><div class="card-head"><h3>Leitura temporal</h3></div>
<label class="checkbox-field"><input id="scannerScenes" type="checkbox" checked><span>Agrupar cenas</span></label>
<label class="checkbox-field"><input id="scannerContinuity" type="checkbox" checked><span>Medir continuidade e cortes</span></label>
<label class="checkbox-field"><input id="scannerReframe" type="checkbox"><span>Guardar geometria para reenquadramento</span></label>
<label class="checkbox-field"><input id="scannerInterpret" type="checkbox" checked><span>Descrever a cena com Foundation Models</span></label>
</div>
</section>
<section class="card"><div class="card-head"><h2>Transcrição</h2><span class="card-kicker" id="scannerModelCount">—</span></div>
<section class="card scanner-media-card">
<div class="card-head"><h2>Som</h2><span class="card-kicker">Áudio e fala</span></div>
<p class="field-help">Escolha as faixas de áudio e configure a transcrição local do Whisper.</p>
<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>
<label class="checkbox-field"><input id="scannerDiarization" type="checkbox"><span>Identificar locutores após transcrever</span></label>
<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>
</div>
</section>
<div class="scanner-options-grid"><section class="card"><div class="card-head"><h2>O que detectar</h2></div>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="faces" checked><span>Rostos e qualidade facial</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="pessoas" checked><span>Pessoas</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="pose" checked><span>Pose e mãos</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="ocr" checked><span>Texto e códigos</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="categorias" checked><span>Objetos e categorias</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="estetica" checked><span>Estética, horizonte e geometria</span></label>
<label class="checkbox-field"><input type="checkbox" data-scanner-resource="segmentacao_de_pessoas" checked><span>Segmentação de pessoas</span></label>
</section>
<section class="card"><div class="card-head"><h2>Leitura temporal</h2></div>
<label class="checkbox-field"><input id="scannerScenes" type="checkbox" checked><span>Agrupar cenas</span></label>
<label class="checkbox-field"><input id="scannerContinuity" type="checkbox" checked><span>Medir continuidade e cortes</span></label>
<label class="checkbox-field"><input id="scannerReframe" type="checkbox"><span>Guardar geometria para reenquadramento</span></label>
<label class="checkbox-field"><input id="scannerInterpret" type="checkbox" checked><span>Descrever a cena com Foundation Models</span></label>
</div>
<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</button></div>
<p class="field-help" id="scannerProfileStatus">Perfil padrão pronto.</p>
<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>
</section>
</div>
<!-- ============================== RETAKES ============================== -->
<div class="tab-panel" id="tabRetakes" role="tabpanel" aria-labelledby="tabBtnRetakes" hidden>
+30 -8
View File
@@ -28,7 +28,7 @@ function log(msg, cls) {
// Antes, tudo que rodava só aparecia como linhas no log lá embaixo e a pessoa
// não sabia o que estava acontecendo. Agora todo processo longo abre um card
// reposicionado na aba de origem com etapa atual, barra e tempo decorrido.
var taskState = { active: false, startedAt: 0, timer: null, clearTimer: null, mirror: null, originTab: "silence", originPanelId: "tabSilence", errorText: "" };
var taskState = { active: false, startedAt: 0, timer: null, clearTimer: null, mirror: null, cancel: null, canceled: false, originTab: "silence", originPanelId: "tabSilence", errorText: "" };
var TIPOS_VIDEO_CHAVE = "schedule_light_tipos_video";
var tipoVideoAtualId = null;
@@ -75,11 +75,13 @@ function taskMoveToOriginTab() {
taskState.originPanelId = panel.id;
}
function taskStart(title, stage, mirror) {
function taskStart(title, stage, mirror, cancel) {
if (taskState.clearTimer) { clearTimeout(taskState.clearTimer); taskState.clearTimer = null; }
taskState.active = true;
taskState.startedAt = Date.now();
taskState.mirror = mirror || null;
taskState.cancel = typeof cancel === "function" ? cancel : null;
taskState.canceled = false;
taskState.originTab = activeTab();
taskState.errorText = "";
taskMoveToOriginTab();
@@ -96,6 +98,9 @@ function taskStart(title, stage, mirror) {
taskEl("taskCopy").hidden = true;
taskEl("taskErrorDetails").hidden = true;
taskEl("taskErrorDetails").textContent = "";
taskEl("taskCancel").hidden = !taskState.cancel;
taskEl("taskCancel").disabled = false;
taskEl("taskCancel").textContent = "Cancelar";
hideToast();
if (taskState.timer) clearInterval(taskState.timer);
@@ -127,12 +132,15 @@ function taskEnd(ok, message, technicalDetails) {
taskState.active = false;
var dock = taskEl("taskDock");
dock.setAttribute("data-result", ok ? "ok" : "err");
var cancelado = taskState.canceled;
dock.setAttribute("data-result", cancelado ? "cancelled" : (ok ? "ok" : "err"));
taskEl("taskTrack").setAttribute("data-mode", "determinate");
taskEl("taskFill").style.width = "100%";
taskEl("taskStage").textContent = message || (ok ? "Concluído" : "Falhou");
taskEl("taskStage").textContent = message || (cancelado ? "Cancelado" : (ok ? "Concluído" : "Falhou"));
taskEl("taskPct").textContent = ok ? "100%" : "";
if (!ok) {
taskEl("taskCancel").hidden = true;
taskEl("taskCancel").disabled = false;
if (!ok && !cancelado) {
taskState.errorText = String(technicalDetails || message || "Falha sem detalhes técnicos.");
taskEl("taskCopy").hidden = false;
taskEl("taskErrorDetails").textContent = taskState.errorText;
@@ -141,6 +149,7 @@ function taskEnd(ok, message, technicalDetails) {
}
taskState.mirror = null;
taskState.cancel = null;
// O próprio card já carrega o desfecho e fica na tela por alguns segundos —
// repetir a mesma frase no toast só empilhava dois avisos idênticos.
taskState.clearTimer = setTimeout(function () {
@@ -148,6 +157,15 @@ function taskEnd(ok, message, technicalDetails) {
}, ok ? 4000 : 9000);
}
function taskCancel() {
if (!taskState.active || typeof taskState.cancel !== "function") return;
taskState.canceled = true;
taskEl("taskCancel").disabled = true;
taskEl("taskCancel").textContent = "Cancelando…";
taskEl("taskStage").textContent = "Cancelando…";
try { taskState.cancel(); } catch (erro) { taskEnd(false, "Falha ao cancelar", erro.message); }
}
function taskCopyError() {
var texto = taskState.errorText;
if (!texto) return;
@@ -1090,6 +1108,8 @@ var SCANNER_ALL_RESOURCES = ["faces", "qualidade_facial", "pessoas", "pose", "ma
var scannerTimeline = null;
var scannerReportPath = null;
var scannerProcesso = null;
var scannerCancelSolicitado = false;
var SCANNER_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_scanner.py";
// `path` e `os` são carregados mais abaixo, junto com o runtime Node do CEP.
@@ -1180,10 +1200,12 @@ function scannerProcess() {
var input=path.join(outputRoot,"pedido-"+stamp+".json");
scannerReportPath=path.join(outputRoot,"relatorio-"+stamp+".json");
fs.writeFileSync(input,JSON.stringify({perfil:profile,timeline:scannerTimeline},null,2),"utf-8");
setBusy("btnScannerProcess",true); taskStart("Scanner", "Preparando análise", null);
setBusy("btnScannerProcess",true);
var spawnOptions={cwd:"/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code",stdio:["ignore","pipe","pipe"]};
var huggingFaceEnvironment=hfSpawnOptions(); if(huggingFaceEnvironment&&huggingFaceEnvironment.env) spawnOptions.env=huggingFaceEnvironment.env;
var child=childProcess.spawn(SCANNER_PYTHON_BIN,[SCANNER_ENGINE_SCRIPT,input,scannerReportPath],spawnOptions);
scannerProcesso=child; scannerCancelSolicitado=false;
taskStart("Scanner", "Preparando análise", null, function () { scannerCancelSolicitado=true; if (scannerProcesso) scannerProcesso.kill("SIGTERM"); });
var stderr="", stdout="";
child.stdout.on("data",function(chunk) {
stdout += chunk.toString(); var lines=stdout.split("\n"); stdout=lines.pop();
@@ -1191,7 +1213,7 @@ function scannerProcess() {
});
child.stderr.on("data",function(chunk){stderr+=chunk.toString();});
child.on("error",function(error){setBusy("btnScannerProcess",false);taskEnd(false,"Scanner indisponível",error.stack||error.message);showToast("err",error.message);});
child.on("close",function(code){setBusy("btnScannerProcess",false);if(code!==0||!fs.existsSync(scannerReportPath)){taskEnd(false,"Scanner falhou",stderr||"Não foi possível gerar o relatório.");showToast("err",stderr||"Não foi possível gerar o relatório.");return;}taskEnd(true,"Relatório JSON gerado");document.getElementById("scannerReportPath").textContent=scannerReportPath;document.getElementById("scannerReportCard").hidden=false;showToast("ok","Scanner concluído.");});
child.on("close",function(code){var foiCancelado=scannerCancelSolicitado;scannerProcesso=null;setBusy("btnScannerProcess",false);if(foiCancelado){taskEnd(false,"Scanner cancelado");return;}if(code!==0||!fs.existsSync(scannerReportPath)){taskEnd(false,"Scanner falhou",stderr||"Não foi possível gerar o relatório.");showToast("err",stderr||"Não foi possível gerar o relatório.");return;}taskEnd(true,"Relatório JSON gerado");document.getElementById("scannerReportPath").textContent=scannerReportPath;document.getElementById("scannerReportCard").hidden=false;showToast("ok","Scanner concluído.");});
}
function scannerOpenReport() { if (!scannerReportPath) return; childProcess.spawn("open",[scannerReportPath],{detached:true,stdio:"ignore"}).unref(); }
@@ -3118,7 +3140,7 @@ function syncTranscribeModelSelect() {
modelosDisponiveis.forEach(function (modelo) {
var opt = document.createElement("option");
opt.value = modelo.nome;
opt.textContent = modelo.nome + " · " + formatMB(modelo.tamanho);
opt.textContent = modelo.nome;
select.appendChild(opt);
});
if (!modelosDisponiveis.length) {
+11 -1
View File
@@ -156,6 +156,12 @@ h1, h2, h3 { margin: 0; }
.scanner-track-list .checkbox-field { margin: 2px 0; padding: 5px; border: 0; background: none; }
.scanner-options-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-items: start; }
.scanner-options-grid .card { margin: 0 0 10px; }
.scanner-media-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; align-items: start; }
.scanner-media-card { margin: 0 0 10px; }
.scanner-media-card h3 { margin: 0; font-size: 12px; }
.scanner-subsection { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
.scanner-subsection .card-head { margin-bottom: 7px; }
.scanner-execution-card { margin-top: 0; }
/* --------------------- Painel de progresso (task dock) ------------------- */
.task-dock {
@@ -178,6 +184,9 @@ h1, h2, h3 { margin: 0; }
.task-elapsed { flex: 0 0 auto; color: var(--text-muted); font: 10px var(--font-mono); }
.task-copy { flex: 0 0 auto; padding: 2px 5px; border: 1px solid var(--border); border-radius: 4px; background: transparent; color: var(--text-secondary); cursor: pointer; font-size: 12px; line-height: 1; }
.task-copy:hover { border-color: var(--red); color: var(--text); }
.task-cancel { flex: 0 0 auto; padding: 3px 8px; border: 1px solid rgba(255, 101, 101, .55); border-radius: 5px; background: transparent; color: #ffb0b0; cursor: pointer; font-size: 10px; }
.task-cancel:hover { border-color: var(--red); background: rgba(255, 101, 101, .12); color: var(--text); }
.task-cancel:disabled { cursor: wait; opacity: .65; }
.task-spinner {
width: 12px;
@@ -191,6 +200,7 @@ h1, h2, h3 { margin: 0; }
.task-dock[data-result] .task-spinner { animation: none; border: none; }
.task-dock[data-result="ok"] .task-spinner::before { content: "✓"; color: var(--green); font-size: 12px; font-weight: 700; }
.task-dock[data-result="err"] .task-spinner::before { content: "!"; color: var(--red); font-size: 12px; font-weight: 700; }
.task-dock[data-result="cancelled"] .task-spinner::before { content: "×"; color: var(--text-muted); font-size: 14px; font-weight: 700; }
@keyframes spin { to { transform: rotate(360deg); } }
.progress-track {
@@ -647,7 +657,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
.tab-button { font-size: 0; gap: 0; padding: 10px 2px; }
.tab-icon { font-size: 14px; }
.field-grid { grid-template-columns: minmax(0, 1fr); }
.scanner-track-grid, .scanner-options-grid { grid-template-columns: minmax(0, 1fr); }
.scanner-track-grid, .scanner-options-grid, .scanner-media-grid { grid-template-columns: minmax(0, 1fr); }
.status-panel { grid-template-columns: 38px minmax(0, 1fr); }
.command-stat { grid-column: 2; padding: 8px 0 0; text-align: left; }
.command-stat strong, .command-stat span { display: inline; }