feat: criado repositório jhonny-editor no Gitea

criado repositório jhonny-editor no Gitea
adicionado script admin/deploy.command com commit automático
atualizado admin/DEV-NOTES.md com template limpo

Resumo:
- 32 arquivos alterados
- 16 novos
- 15 modificados
- 0 removidos

 16 files changed, 638 insertions(+), 61 deletions(-)

Arquivos:
  - code/engine/skills/boas-praticas-oo.md -> .agents/skills/boas-praticas-oo/SKILL.md
  - AGENTS.md
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/cep-plugin/styles.css
  - code/engine/integracoes/visual/apple_vision.py
  - code/engine/integracoes/visual/apple_vision_runner.swift
  - code/engine/integracoes/visual/extrator_ffmpeg.py
  - code/engine/scanner/__init__.py
  - code/engine/scanner/configuracao_visual.py
  - code/engine/scanner/coordenacao/__init__.py
  - code/engine/scanner/retakes/adaptador_de_falas.py
  - code/engine/scanner/retakes/agrupador_de_takes.py
  - code/engine/scanner/retakes/detector_de_reinicios.py
  - code/engine/scanner/retakes/modelos_de_retakes.py
  - code/engine/scanner/visual.py
  - .jhonny/
  - .retakes/
  - CODING_STANDARDS.md
  - code/engine/executar_retakes.py
  - code/engine/persistencia/
  - code/engine/scanner/retakes/__init__.py
  - code/engine/scanner/retakes/carregador_de_artefatos.py
  - code/engine/scanner/retakes/classificador_de_retakes.py
  - code/engine/scanner/retakes/coordenador_de_retakes.py
  - code/engine/scanner/retakes/deteccao_de_retakes.py
  - code/engine/scanner/retakes/gerador_de_evidencias.py
  - code/engine/scanner/retakes/repositorio_de_retakes.py
  - code/engine/testes/test_consultas_de_persistencia.py
  - code/engine/testes/test_deteccao_de_retakes.py
  - code/engine/testes/test_persistencia_sqlite.py
  - code/relatorios/audio/arquivos/92f97877259a86a8095b1eecafdefe357212a12247d04b78df60e0c0ae38b2ea/
This commit is contained in:
João Henrique
2026-09-08 17:13:19 -04:00
parent b9bf3b2863
commit ee181360c8
40 changed files with 3674 additions and 61 deletions
+78 -3
View File
@@ -28,6 +28,9 @@
<button class="tab-button" id="tabBtnScanner" role="tab" aria-selected="false" aria-controls="tabScanner" onclick="switchTab('scanner')" type="button">
<span class="tab-icon" aria-hidden="true">◉</span>Scanner
</button>
<button class="tab-button" id="tabBtnRetakes" role="tab" aria-selected="false" aria-controls="tabRetakes" onclick="switchTab('retakes')" type="button">
<span class="tab-icon" aria-hidden="true">↻</span>Retakes
</button>
<button class="tab-button" id="tabBtnBridge" role="tab" aria-selected="false" aria-controls="tabBridge" onclick="switchTab('bridge')" type="button">
<span class="tab-icon" aria-hidden="true">⇄</span>Conexão
</button>
@@ -226,9 +229,19 @@
<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>
<label class="field-label" for="scannerInterval">Frames de vídeo</label>
<select id="scannerInterval" class="select-field"><option value="0.2">Precisa — 5 frames por segundo</option><option value="1" selected>Equilibrada — 1 frame por segundo</option><option value="5">Econômica — 1 frame a cada 5 segundos</option></select>
<p class="field-help">Use a amostragem precisa somente no trecho que será reenquadrado.</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()">
<span class="range-field-value" id="scannerIntervalValue">1 s/quadro</span>
</div>
<p class="field-help">Quanto menor, mais frames e mais detalhado (e mais lento). Valores maiores aceleram a análise, principalmente em clipes curtos.</p>
<label class="field-label" for="scannerResolution">Resolução da análise</label>
<select id="scannerResolution" class="select-field">
<option value="0">Original — sem redimensionar</option>
<option value="1080" selected>1080p — recomendado</option>
<option value="720">720p — mais rápido</option>
</select>
<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>
@@ -258,6 +271,68 @@
<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>
</div>
<!-- ============================== RETAKES ============================== -->
<div class="tab-panel" id="tabRetakes" role="tabpanel" aria-labelledby="tabBtnRetakes" hidden>
<p class="tab-intro">Identifica repetições e tentativas alternativas usando a transcrição do Scanner. O painel nunca corta o vídeo: você revisa e decide.</p>
<!-- 1. Arquivo ou conjunto analisado -->
<section class="card">
<div class="card-head"><h2>Arquivo analisado</h2><span class="card-kicker" id="retakesStatusGeral">—</span></div>
<div class="field-grid">
<div><label class="field-label" for="retakesCaso">Caso do Scanner</label>
<select id="retakesCaso" class="select-field" onchange="retakesCarregarCaso()"></select></div>
<div><button class="button button-ghost" onclick="retakesListarCasos()" type="button">Atualizar lista</button></div>
</div>
<p class="field-help" id="retakesInfoCaso">Selecione um caso já processado pelo Scanner.</p>
</section>
<!-- 2/3. Configurações e critérios -->
<div class="scanner-options-grid">
<section class="card"><div class="card-head"><h2>Critérios</h2></div>
<label class="field-label" for="retakesCriterio">Critério de confirmação</label>
<select id="retakesCriterio" class="select-field">
<option value="conservador">Conservador — só evidências fortes</option>
<option value="equilibrado" selected>Equilibrado — prováveis e fortes</option>
<option value="sensivel">Sensível — também possíveis</option>
</select>
<label class="checkbox-field"><input id="retakesReaproveitar" type="checkbox" checked><span>Reaproveitar análise existente</span></label>
<p class="field-help">A comparação textual, os reinícios de fala e as pausas já fazem parte da análise da engine.</p>
</section>
<section class="card"><div class="card-head"><h2>Análise visual auxiliar</h2></div>
<label class="checkbox-field"><input id="retakesUsarVisual" type="checkbox"><span>Usar cortes visuais como evidência</span></label>
<p class="field-help">Reforça a confiança, mas nunca decide sozinha. Reutiliza as cenas já analisadas.</p>
<div class="button-row">
<button class="button button-primary" id="btnRetakesAnalisar" onclick="retakesAnalisar()" type="button">Analisar retakes</button>
</div>
</section>
</div>
<!-- 4. Progresso -->
<div class="callout callout-info" id="retakesProgressoCard" hidden>
<strong id="retakesProgressoEtapa">Analisando retakes…</strong>
<p id="retakesProgressoDetalhe">—</p>
</div>
<!-- 5/6. Resultados e revisão -->
<section class="card">
<div class="card-head"><h2>Resultados</h2><span class="card-kicker" id="retakesContagem">—</span></div>
<label class="field-label" for="retakesFiltro">Filtrar</label>
<select id="retakesFiltro" class="select-field" onchange="retakesRenderResultados()">
<option value="todos" selected>Todos</option>
<option value="fortes">Fortes</option>
<option value="possiveis">Possíveis e prováveis</option>
<option value="pendente_de_revisao">Pendentes de revisão</option>
<option value="confirmado">Confirmados</option>
<option value="nao_e_retake">Rejeitados</option>
<option value="ignorado">Ignorados</option>
</select>
<div id="retakesResultados" role="list" aria-label="Grupos de retakes"></div>
<p class="field-help" id="retakesSemResultados" hidden>Nenhum retake identificado com os critérios atuais.</p>
</section>
<div id="retakesLog" role="log" aria-live="polite" aria-relevant="additions" tabindex="0" aria-label="Log de retakes"></div>
</div>
<!-- ============================== CONEXÃO ============================== -->
<div class="tab-panel" id="tabBridge" role="tabpanel" aria-labelledby="tabBtnBridge" hidden>
+197 -3
View File
@@ -149,6 +149,7 @@ function switchTab(name) {
var tabs = [
{ key: "silence", panel: "tabSilence", btn: "tabBtnSilence" },
{ key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" },
{ key: "retakes", panel: "tabRetakes", btn: "tabBtnRetakes" },
{ key: "bridge", panel: "tabBridge", btn: "tabBtnBridge" },
{ key: "models", panel: "tabModels", btn: "tabBtnModels" },
{ key: "settings", panel: "tabSettings", btn: "tabBtnSettings" },
@@ -166,6 +167,190 @@ function switchTab(name) {
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); }
if (name === "settings") renderEditorPersonalities();
if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); }
if (name === "retakes") retakesAoAbrir();
}
// ---- Retakes ---------------------------------------------------------------
// A aba apenas controla o processo e mostra resultados; a análise inteira
// acontece na engine (scanner/retakes), chamada via executar_retakes.py.
var RETAKES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_retakes.py";
var retakesCasoAtual = null;
var retakesGrupos = [];
var retakesProcesso = null;
function retakesRaiz() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); }
function retakesAoAbrir() { retakesListarCasos(); }
function retakesListarCasos() {
var seletor = document.getElementById("retakesCaso");
var anterior = seletor.value;
seletor.innerHTML = "";
var raiz = retakesRaiz();
var casos = [];
try { casos = fs.readdirSync(raiz).filter(function (item) { return fs.statSync(path.join(raiz, item)).isDirectory(); }).sort(); } catch (_) {}
if (!casos.length) { seletor.appendChild(new Option("Nenhum caso encontrado", "")); retakesCasoAtual = null; retakesInfo("Execute o Scanner primeiro para gerar transcrições."); return; }
casos.forEach(function (caso) { seletor.appendChild(new Option(caso, caso)); });
seletor.value = anterior && casos.indexOf(anterior) >= 0 ? anterior : casos[0];
retakesCarregarCaso();
}
function retakesInfo(mensagem) { document.getElementById("retakesInfoCaso").textContent = mensagem; }
function retakesArtefatosDeAudio(caso) {
var pasta = path.join(retakesRaiz(), caso);
try { return fs.readdirSync(pasta).filter(function (item) { return /-audio-faixa-\d+\.json$/.test(item); }).map(function (item) { return path.join(pasta, item); }).sort(); } catch (_) { return []; }
}
function retakesCarregarCaso() {
var caso = document.getElementById("retakesCaso").value;
retakesCasoAtual = caso || null;
retakesGrupos = [];
retakesRenderResultados();
if (!caso) { retakesInfo("Nenhum caso disponível."); return; }
var artefatos = retakesArtefatosDeAudio(caso);
var jaTem = fs.existsSync(path.join(retakesRaiz(), caso, "retakes"));
retakesInfo("Caso: " + caso + " · " + artefatos.length + " faixa(s) de áudio com transcrição" + (jaTem ? " · já analisado" : " · não analisado"));
document.getElementById("retakesStatusGeral").textContent = jaTem ? "Analisado" : "Não analisado";
if (jaTem) retakesCarregarResultadosSalvos();
}
function retakesCarregarResultadosSalvos() {
var arquivo = path.join(retakesRaiz(), retakesCasoAtual, "retakes", "retakes-" + retakesCasoAtual + ".json");
try {
var dados = JSON.parse(fs.readFileSync(arquivo, "utf-8"));
retakesGrupos = dados.grupos_de_retakes || [];
retakesRenderResultados();
} catch (erro) { showToast("err", "Não foi possível ler os resultados: " + erro.message); }
}
function retakesFormatarTempo(segundos) {
if (typeof segundos !== "number" || isNaN(segundos)) return "—";
var total = Math.max(0, Math.round(segundos));
var m = Math.floor(total / 60), s = total % 60;
return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
}
function retakesRotuloDoGrupo(grupo) {
var tipo = { retake_confirmado: "Retake forte", possivel_retake: "Possível retake", repeticao_parcial: "Repetição parcial", repeticao_de_frase: "Repetição de frase", repeticao_de_inicio: "Repetição de início", continuidade_de_fala: "Continuidade de fala", sem_indicio_de_retake: "Sem indício" }[grupo.tipo] || grupo.tipo;
var status = grupo.status_de_revisao === "confirmado" ? " · confirmado" : grupo.status_de_revisao === "nao_e_retake" ? " · rejeitado" : grupo.status_de_revisao === "ignorado" ? " · ignorado" : "";
return tipo + " · " + Math.round(grupo.confianca * 100) + "% de confiança" + status;
}
function retakesPassaNoFiltro(grupo) {
var filtro = document.getElementById("retakesFiltro").value;
if (filtro === "todos") return true;
if (filtro === "fortes") return grupo.tipo === "retake_confirmado";
if (filtro === "possiveis") return grupo.tipo !== "retake_confirmado" && grupo.tipo !== "sem_indicio_de_retake";
return grupo.status_de_revisao === filtro;
}
function retakesRenderResultados() {
var alvo = document.getElementById("retakesResultados");
alvo.innerHTML = "";
var visiveis = retakesGrupos.filter(retakesPassaNoFiltro);
document.getElementById("retakesContagem").textContent = retakesGrupos.length ? retakesGrupos.length + " grupo(s)" : "—";
document.getElementById("retakesSemResultados").hidden = visiveis.length > 0;
visiveis.forEach(function (grupo) { alvo.appendChild(retakesCartaoDoGrupo(grupo)); });
}
function retakesCartaoDoGrupo(grupo) {
var cartao = document.createElement("div");
cartao.className = "card"; cartao.setAttribute("role", "listitem");
var titulo = document.createElement("strong"); titulo.textContent = retakesRotuloDoGrupo(grupo);
cartao.appendChild(titulo);
grupo.tomadas.forEach(function (tomada, indice) {
var linha = document.createElement("p");
linha.textContent = "Tentativa " + (indice + 1) + ": " + retakesFormatarTempo(tomada.inicio) + " – " + retakesFormatarTempo(tomada.fim) + " · " + (tomada.texto || "");
linha.style.margin = "2px 0"; cartao.appendChild(linha);
});
if (grupo.evidencias && grupo.evidencias.length) {
var motivos = document.createElement("p");
motivos.textContent = "Evidências: " + grupo.evidencias.map(function (e) { return e.descricao || e.tipo; }).join("; ");
motivos.className = "field-help"; cartao.appendChild(motivos);
}
var acoes = document.createElement("div"); acoes.className = "button-row";
[["confirmar", "Confirmar"], ["rejeitar", "Não é retake"], ["manter", "Manter p/ revisão"], ["ignorar", "Ignorar"]].forEach(function (par) {
var botao = document.createElement("button");
botao.className = "button button-ghost"; botao.type = "button"; botao.textContent = par[1];
botao.onclick = function () { retakesRevisar(grupo.id, par[0]); };
acoes.appendChild(botao);
});
cartao.appendChild(acoes);
return cartao;
}
function retakesAnalisar() {
if (!retakesCasoAtual) { showToast("err", "Selecione um caso do Scanner."); return; }
var artefatos = retakesArtefatosDeAudio(retakesCasoAtual);
if (!artefatos.length) { showToast("err", "O caso não possui transcrição. Execute o Scanner com transcrição primeiro."); return; }
if (retakesProcesso) { showToast("err", "Já existe uma análise em andamento."); return; }
if (!document.getElementById("retakesReaproveitar").checked) {
var pastaRetakes = path.join(retakesRaiz(), retakesCasoAtual, "retakes");
try { if (fs.existsSync(pastaRetakes)) fs.rmSync(pastaRetakes, { recursive: true }); } catch (_) {}
}
var pedido = { acao: "analisar", video_id: retakesCasoAtual, artefatos_de_audio: artefatos,
configuracao: { criterio: document.getElementById("retakesCriterio").value,
usar_analise_visual: document.getElementById("retakesUsarVisual").checked } };
var entrada = path.join(os.tmpdir(), "pedido-retakes-" + Date.now() + ".json");
fs.writeFileSync(entrada, JSON.stringify(pedido, null, 2), "utf-8");
setBusy("btnRetakesAnalisar", true);
taskStart("Retakes", "Preparando análise", null);
document.getElementById("retakesProgressoCard").hidden = false;
document.getElementById("retakesProgressoEtapa").textContent = "Analisando retakes…";
var child = childProcess.spawn("python3", [RETAKES_ENGINE_SCRIPT, entrada, path.join(retakesRaiz(), retakesCasoAtual)],
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine", stdio: ["ignore", "pipe", "pipe"] });
retakesProcesso = child;
var resto = "";
child.stdout.on("data", function (pedaco) {
resto += pedaco.toString(); var linhas = resto.split("\n"); resto = linhas.pop();
linhas.forEach(function (linha) { var evento; try { evento = JSON.parse(linha); } catch (_) { return; } retakesProcessarEvento(evento); });
});
child.stderr.on("data", function (pedaco) { if (pedaco.toString().trim()) retakesLog(pedaco.toString().trim()); });
child.on("error", function (erro) { retakesProcesso = null; setBusy("btnRetakesAnalisar", false); taskEnd(false, "Engine indisponível"); showToast("err", erro.message); });
child.on("close", function (codigo) {
retakesProcesso = null; setBusy("btnRetakesAnalisar", false);
document.getElementById("retakesProgressoCard").hidden = true;
document.getElementById("retakesStatusGeral").textContent = "Analisado";
if (codigo !== 0) taskEnd(false, "Análise falhou");
});
}
function retakesProcessarEvento(evento) {
if (evento.evento === "progresso") {
taskUpdate({ stage: evento.etapa, pct: evento.percentual });
document.getElementById("retakesProgressoEtapa").textContent = evento.etapa;
document.getElementById("retakesProgressoDetalhe").textContent = evento.percentual + "% concluído";
} else if (evento.evento === "erro") {
showToast("err", evento.mensagem); retakesLog("Erro: " + evento.mensagem);
} else if (evento.evento === "concluido") {
retakesGrupos = (evento.resultado && evento.resultado.grupos) || [];
retakesRenderResultados();
taskEnd(true, retakesGrupos.length + " grupo(s) de retakes");
showToast("ok", "Análise concluída: " + retakesGrupos.length + " grupo(s) encontrado(s).");
}
}
function retakesRevisar(grupoId, acao) {
if (!retakesCasoAtual) return;
var pedido = { acao: "revisar", video_id: retakesCasoAtual, revisao: { grupo_id: grupoId, acao: acao } };
var entrada = path.join(os.tmpdir(), "revisao-retakes-" + Date.now() + ".json");
fs.writeFileSync(entrada, JSON.stringify(pedido), "utf-8");
var child = childProcess.spawn("python3", [RETAKES_ENGINE_SCRIPT, entrada, path.join(retakesRaiz(), retakesCasoAtual)],
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine", stdio: ["ignore", "pipe", "pipe"] });
child.on("close", function (codigo) {
if (codigo !== 0) { showToast("err", "Não foi possível registrar a revisão."); return; }
var novos = { confirmar: "confirmado", rejeitar: "nao_e_retake", manter: "pendente_de_revisao", ignorar: "ignorado" };
retakesGrupos = retakesGrupos.map(function (g) { return g.id === grupoId ? Object.assign({}, g, { status_de_revisao: novos[acao] }) : g; });
retakesRenderResultados();
showToast("ok", "Revisão registrada.");
});
}
function retakesLog(mensagem) {
var alvo = document.getElementById("retakesLog");
var linha = document.createElement("p"); linha.textContent = mensagem;
alvo.appendChild(linha);
}
// ---- Perfil do Scanner -----------------------------------------------------
@@ -191,23 +376,31 @@ function scannerRenderTracks(kind, tracks, selected) {
tracks.forEach(function (track) { var label = document.createElement("label"); label.className = "checkbox-field"; var input = document.createElement("input"); input.type = "checkbox"; input.setAttribute("data-scanner-track", kind); input.value = track.index; input.checked = selected.indexOf(track.index) >= 0; label.appendChild(input); label.appendChild(document.createTextNode(" " + track.name + " · " + track.numClips + " clipe(s)")); target.appendChild(label); });
}
function scannerUpdateIntervalLabel() {
var valor = Number(document.getElementById("scannerInterval").value);
var texto = valor === 1 ? "1 s/quadro" : valor.toFixed(1).replace(/\.0$/, "") + " s/quadro";
document.getElementById("scannerIntervalValue").textContent = texto;
}
function scannerProfileFromScreen() {
var enabled = Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]:checked")).map(function (item) { return item.getAttribute("data-scanner-resource"); });
function include(source, extras) { if (enabled.indexOf(source) >= 0) extras.forEach(function (item) { if (enabled.indexOf(item) < 0) enabled.push(item); }); }
include("faces", ["qualidade_facial"]); include("pose", ["maos"]); include("ocr", ["codigos"]); include("estetica", ["horizonte", "retangulos", "contornos"]);
var modelo = document.getElementById("scannerTranscribeModel").value;
var opcao = document.getElementById("scannerTranscribeModel").selectedOptions[0];
return { versao: 1, intervalo_em_segundos: Number(document.getElementById("scannerInterval").value), faixas_de_video: scannerSelectedTracks("video"), faixas_de_audio: scannerSelectedTracks("audio"), recursos_vision: SCANNER_ALL_RESOURCES.filter(function (item) { return enabled.indexOf(item) >= 0; }), detectar_cenas: document.getElementById("scannerScenes").checked, analisar_continuidade: document.getElementById("scannerContinuity").checked, preparar_reenquadramento: document.getElementById("scannerReframe").checked, interpretar_cena: document.getElementById("scannerInterpret").checked, transcricao: { modelo: modelo, caminho_modelo: opcao && opcao.dataset.path || "", idioma: document.getElementById("scannerTranscribeLanguage").value, diarizacao: document.getElementById("scannerDiarization").checked, usar_hugging_face: !!loadHfToken() } };
return { versao: 1, intervalo_em_segundos: Number(document.getElementById("scannerInterval").value), resolucao_maxima: Number(document.getElementById("scannerResolution").value), faixas_de_video: scannerSelectedTracks("video"), faixas_de_audio: scannerSelectedTracks("audio"), recursos_vision: SCANNER_ALL_RESOURCES.filter(function (item) { return enabled.indexOf(item) >= 0; }), detectar_cenas: document.getElementById("scannerScenes").checked, analisar_continuidade: document.getElementById("scannerContinuity").checked, preparar_reenquadramento: document.getElementById("scannerReframe").checked, interpretar_cena: document.getElementById("scannerInterpret").checked, transcricao: { modelo: modelo, caminho_modelo: opcao && opcao.dataset.path || "", idioma: document.getElementById("scannerTranscribeLanguage").value, diarizacao: document.getElementById("scannerDiarization").checked, usar_hugging_face: !!loadHfToken() } };
}
function saveScannerProfile() {
try { var profile = scannerProfileFromScreen(); localStorage.setItem(SCANNER_PROFILE_KEY, JSON.stringify(profile)); document.getElementById("scannerProfileStatus").textContent = "Perfil salvo: " + profile.intervalo_em_segundos + " s por frame; " + profile.faixas_de_video.length + " faixa(s) de vídeo."; showToast("ok", "Perfil do Scanner salvo."); } catch (error) { showToast("err", error.message || String(error)); }
try { var profile = scannerProfileFromScreen(); localStorage.setItem(SCANNER_PROFILE_KEY, JSON.stringify(profile)); document.getElementById("scannerProfileStatus").textContent = "Perfil salvo: " + profile.intervalo_em_segundos + " s por quadro; " + profile.faixas_de_video.length + " faixa(s) de vídeo."; showToast("ok", "Perfil do Scanner salvo."); } catch (error) { showToast("err", error.message || String(error)); }
}
function loadScannerProfile() {
var saved; try { saved = JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY) || "null"); } catch (_) { saved = null; }
if (!saved) return;
if (!saved) { scannerUpdateIntervalLabel(); return; }
document.getElementById("scannerInterval").value = String(saved.intervalo_em_segundos || 1);
scannerUpdateIntervalLabel();
document.getElementById("scannerResolution").value = String(saved.resolucao_maxima != null ? saved.resolucao_maxima : 1080);
if (scannerTimeline) { scannerRenderTracks("video", scannerTimeline.videoTracks, saved.faixas_de_video || []); scannerRenderTracks("audio", scannerTimeline.audioTracks, saved.faixas_de_audio || []); }
var resources = saved.recursos_vision || SCANNER_ALL_RESOURCES;
Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]")).forEach(function (item) { item.checked = resources.indexOf(item.getAttribute("data-scanner-resource")) >= 0; });
@@ -368,6 +561,7 @@ var fs = nodeRequire("fs");
var path = nodeRequire("path");
var os = nodeRequire("os");
var https = nodeRequire("https");
var childProcess = nodeRequire("child_process");
function defaultBridgeDirectory() {
try {
var nodeProcess = nodeRequire("process");
+3
View File
@@ -339,6 +339,9 @@ h1, h2, h3 { margin: 0; }
transition: border-color .15s, box-shadow .15s;
}
.select-field { height: 32px; padding: 0 8px; font-size: 10.5px; }
.range-field-row { display: flex; align-items: center; gap: 10px; }
.range-field-row input[type="range"] { flex: 1 1 auto; accent-color: var(--violet); height: 20px; }
.range-field-value { flex: 0 0 auto; min-width: 84px; text-align: right; font: 10.5px var(--font-mono); color: var(--text-secondary); }
.path-field input, .file-field input { height: 34px; padding: 0 10px; font: 10px var(--font-mono); color: var(--text-secondary); user-select: text; }
.path-field > svg + input { padding-left: 32px; }
textarea { padding: 8px 10px; font: 10px var(--font-mono); color: var(--text-secondary); resize: vertical; user-select: text; }