Files
jhonny-editor/code/cep-plugin/main.js
T
João Henrique 0a01b16148 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:
- 3 arquivos alterados
- 0 novos
- 3 modificados
- 0 removidos

 3 files changed, 83 insertions(+), 21 deletions(-)

Arquivos:
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/cep-plugin/styles.css
2026-09-08 17:30:08 -04:00

2505 lines
103 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* MCP for Adobe Premiere Pro - CEP Plugin Main Script
* Polls a temp directory for command files (.jsx), executes them
* in Premiere Pro's ExtendScript engine, and writes results back. */
var cs = new CSInterface();
var bridgeRunning = false;
var pollInterval = null;
var commandCount = 0;
var tempDir = "";
var POLL_MS = 200;
var HEARTBEAT_MS = 1000;
var heartbeatInterval = null;
// ---- Logging ----
function log(msg, cls) {
var el = document.getElementById("log");
var entry = document.createElement("div");
entry.className = "log-entry " + (cls || "");
var ts = new Date().toLocaleTimeString();
entry.textContent = "[" + ts + "] " + msg;
el.appendChild(entry);
el.scrollTop = el.scrollHeight;
// Keep max 100 entries
while (el.children.length > 100) el.removeChild(el.firstChild);
}
// ---- Painel de progresso (task dock) ----
// 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: "" };
function taskEl(id) { return document.getElementById(id); }
function formatClock(ms) {
var total = Math.max(0, Math.round(ms / 1000));
var mm = String(Math.floor(total / 60)).padStart(2, "0");
var ss = String(total % 60).padStart(2, "0");
return mm + ":" + ss;
}
function activeTab() {
var panel = document.querySelector(".tab-panel:not([hidden])");
return panel ? panel.id.replace(/^tab/, "").toLowerCase() : "silence";
}
function taskMoveToOriginTab() {
var dock = taskEl("taskDock");
var nome = taskState.originTab;
var panel = document.getElementById("tab" + nome.charAt(0).toUpperCase() + nome.slice(1));
if (!dock || !panel) return;
panel.insertBefore(dock, panel.firstChild);
taskState.originPanelId = panel.id;
}
function taskStart(title, stage, mirror) {
if (taskState.clearTimer) { clearTimeout(taskState.clearTimer); taskState.clearTimer = null; }
taskState.active = true;
taskState.startedAt = Date.now();
taskState.mirror = mirror || null;
taskState.originTab = activeTab();
taskState.errorText = "";
taskMoveToOriginTab();
var dock = taskEl("taskDock");
dock.removeAttribute("data-result");
dock.hidden = false;
taskEl("taskTitle").textContent = title;
taskEl("taskStage").textContent = stage || "Preparando…";
taskEl("taskPct").textContent = "";
taskEl("taskElapsed").textContent = "00:00";
taskEl("taskTrack").setAttribute("data-mode", "indeterminate");
taskEl("taskFill").style.width = "0%";
taskEl("taskCopy").hidden = true;
taskEl("taskErrorDetails").hidden = true;
taskEl("taskErrorDetails").textContent = "";
hideToast();
if (taskState.timer) clearInterval(taskState.timer);
taskState.timer = setInterval(function () {
taskEl("taskElapsed").textContent = formatClock(Date.now() - taskState.startedAt);
}, 1000);
}
// info: { stage, pct, detail }
function taskUpdate(info) {
if (!taskState.active) return;
if (taskState.mirror) { try { taskState.mirror(info); } catch (e) {} }
var stage = info.stage || "";
var detail = info.detail || "";
taskEl("taskStage").textContent = detail ? stage + " · " + detail : stage;
if (typeof info.pct === "number" && isFinite(info.pct)) {
taskEl("taskTrack").setAttribute("data-mode", "determinate");
taskEl("taskFill").style.width = Math.max(0, Math.min(100, info.pct)) + "%";
taskEl("taskPct").textContent = Math.round(info.pct) + "%";
} else {
taskEl("taskTrack").setAttribute("data-mode", "indeterminate");
taskEl("taskPct").textContent = "";
}
}
function taskEnd(ok, message, technicalDetails) {
if (taskState.timer) { clearInterval(taskState.timer); taskState.timer = null; }
taskState.active = false;
var dock = taskEl("taskDock");
dock.setAttribute("data-result", ok ? "ok" : "err");
taskEl("taskTrack").setAttribute("data-mode", "determinate");
taskEl("taskFill").style.width = "100%";
taskEl("taskStage").textContent = message || (ok ? "Concluído" : "Falhou");
taskEl("taskPct").textContent = ok ? "100%" : "";
if (!ok) {
taskState.errorText = String(technicalDetails || message || "Falha sem detalhes técnicos.");
taskEl("taskCopy").hidden = false;
taskEl("taskErrorDetails").textContent = taskState.errorText;
taskEl("taskErrorDetails").hidden = false;
if (activeTab() !== taskState.originTab) switchTab(taskState.originTab);
}
taskState.mirror = 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 () {
if (!taskState.active) taskEl("taskDock").hidden = true;
}, ok ? 4000 : 9000);
}
function taskCopyError() {
var texto = taskState.errorText;
if (!texto) return;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(texto).then(function () { showToast("ok", "Erro técnico copiado."); }).catch(function () { taskCopyErrorFallback(texto); });
return;
}
} catch (_) {}
taskCopyErrorFallback(texto);
}
function taskCopyErrorFallback(texto) {
try {
var copia = childProcess.spawn("pbcopy");
copia.stdin.end(texto);
showToast("ok", "Erro técnico copiado.");
return;
} catch (_) {}
var area = document.createElement("textarea");
area.value = texto;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
try { document.execCommand("copy"); showToast("ok", "Erro técnico copiado."); } catch (erro) { showToast("err", "Não foi possível copiar o erro: " + erro.message); }
document.body.removeChild(area);
}
function showToast(kind, text) {
var el = document.getElementById("toast");
if (!el) return;
el.setAttribute("data-kind", kind);
document.getElementById("toastIcon").textContent = kind === "ok" ? "✓" : "!";
document.getElementById("toastText").textContent = text;
el.hidden = false;
}
function hideToast() {
var el = document.getElementById("toast");
if (el) el.hidden = true;
}
// Botão com spinner embutido: o usuário vê que aquele botão específico disparou
// o processo, sem precisar caçar no log.
function setBusy(id, busy) {
var btn = document.getElementById(id);
if (!btn) return;
btn.classList.toggle("is-busy", !!busy);
btn.disabled = !!busy;
}
// ---- Estados dos passos da aba "Editar vídeo" ----
// locked = ainda não dá pra usar | ready = pode usar | running | done | error
function setStep(n, state, chipText) {
var step = document.getElementById("step" + n);
var chip = document.getElementById("step" + n + "Chip");
if (!step) return;
step.setAttribute("data-state", state);
if (chip && chipText) chip.textContent = chipText;
else if (chip) {
chip.textContent =
state === "locked" ? "Bloqueado" :
state === "ready" ? "Pronto" :
state === "running" ? "Rodando…" :
state === "done" ? "Concluído" : "Erro";
}
}
function stepState(n) {
var step = document.getElementById("step" + n);
return step ? step.getAttribute("data-state") : "locked";
}
// ---- Tabs ----
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" },
];
tabs.forEach(function (t) {
var active = t.key === name;
var panel = document.getElementById(t.panel);
var btn = document.getElementById(t.btn);
if (!panel || !btn) return;
panel.hidden = !active;
btn.classList.toggle("active", active);
btn.setAttribute("aria-selected", String(active));
});
if (name === "models") renderModelList();
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 = "";
var erroTecnico = "";
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) { erroTecnico += pedaco.toString(); if (pedaco.toString().trim()) retakesLog(pedaco.toString().trim()); });
child.on("error", function (erro) { retakesProcesso = null; setBusy("btnRetakesAnalisar", false); taskEnd(false, "Engine indisponível", erro.stack || erroTecnico || erro.message); 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", erroTecnico || "A engine terminou com código " + codigo + ".");
});
}
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 -----------------------------------------------------
// O painel e o Engine compartilham este contrato versionado. O perfil não
// contém caminhos de mídia; estes continuam sendo resolvidos pelo MCP.
var SCANNER_PROFILE_KEY = "mcp_scanner_profile_v1";
var SCANNER_ALL_RESOURCES = ["faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "codigos", "categorias", "estetica", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas"];
var scannerTimeline = null;
var scannerReportPath = null;
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.
// Calcular isso aqui interrompia todo o script antes da inicialização do painel.
function scannerOutputRoot() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); }
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; }); }
function scannerRenderTracks(kind, tracks, selected) {
var target = document.getElementById(kind === "video" ? "scannerVideoTracks" : "scannerAudioTracks");
target.innerHTML = "";
if (!tracks.length) { target.textContent = "Nenhuma faixa encontrada."; return; }
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), 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 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) { 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; });
document.getElementById("scannerScenes").checked = saved.detectar_cenas !== false; document.getElementById("scannerContinuity").checked = saved.analisar_continuidade !== false; document.getElementById("scannerReframe").checked = !!saved.preparar_reenquadramento; document.getElementById("scannerInterpret").checked = saved.interpretar_cena !== false;
if (saved.transcricao) { document.getElementById("scannerTranscribeModel").value = saved.transcricao.modelo || ""; document.getElementById("scannerTranscribeLanguage").value = saved.transcricao.idioma || loadLanguage(); document.getElementById("scannerDiarization").checked = !!saved.transcricao.diarizacao && !!loadHfToken(); }
}
function renderScannerTranscriptionOptions() {
var modelos=document.getElementById("scannerTranscribeModel"), idiomas=document.getElementById("scannerTranscribeLanguage"); if (!modelos || !idiomas) return;
var atual=modelos.value; modelos.innerHTML=""; var disponiveis=discoverPortugueseModelsOnMerongo();
if (!disponiveis.length) { var vazio=document.createElement("option"); vazio.value=""; vazio.textContent="Nenhum modelo com português encontrado no Merongo"; modelos.appendChild(vazio); } else { disponiveis.forEach(function(item){var option=document.createElement("option");option.value=item.nome;option.dataset.path=item.caminho;option.textContent=item.nome+" · "+formatMB(item.tamanho);modelos.appendChild(option);}); if (atual) modelos.value=atual; }
idiomas.innerHTML=""; LANGUAGE_CATALOG.forEach(function(item){var option=document.createElement("option");option.value=item.value;option.textContent=item.label;idiomas.appendChild(option);}); idiomas.value=loadLanguage(); document.getElementById("scannerModelCount").textContent=disponiveis.length+" instalado(s)"; renderScannerHuggingFaceStatus();
}
function renderScannerHuggingFaceStatus() {
var status = document.getElementById("scannerHfStatus");
var text = document.getElementById("scannerHfStatusText");
var diarization = document.getElementById("scannerDiarization");
if (!status || !text || !diarization) return;
var available = !!loadHfToken();
status.setAttribute("data-state", available ? "valid" : "unset");
text.textContent = available ? "Hugging Face disponível para identificar locutores" : "Configure o token do Hugging Face para identificar locutores";
diarization.disabled = !available;
if (!available) diarization.checked = false;
}
var MERONGO_MODEL_ROOTS = ["/Volumes/Merongo/SISTEMAS/WHISPERX/whisper/models", "/Volumes/Merongo/SISTEMAS/MODELOSIA/hf-cache", "/Volumes/Merongo/Applications/WhisperX/huggingface/hub", "/Volumes/Merongo/Applications/Trasncritor", "/Volumes/Merongo/SISTEMAS/AUX"];
function discoverPortugueseModelsOnMerongo() {
var encontrados = {}, roots = [effectiveModelsPath()].concat(MERONGO_MODEL_ROOTS);
function walk(dir, profundidade) { if (profundidade > 5) return; var entries; try { entries=fs.readdirSync(dir,{withFileTypes:true}); } catch (_) { return; } entries.forEach(function(entry) { if (!entry.isDirectory() || entry.name === ".locks" || entry.name === ".git") return; var full=path.join(dir,entry.name); if (fs.existsSync(path.join(full,"model.bin")) && modelSupportsPortuguese(full)) { var nome=modelNameFromPath(full); if (!encontrados[nome]) encontrados[nome]={nome:nome,caminho:full,tamanho:folderSizeMB(full)}; return; } walk(full,profundidade+1); }); }
roots.filter(function(root,index){return root && roots.indexOf(root)===index;}).forEach(function(root){walk(root,0);});
return Object.keys(encontrados).sort().map(function(nome){return encontrados[nome];});
}
function modelSupportsPortuguese(folder) { try { var tokenizer=path.join(folder,"tokenizer.json"); if (!fs.existsSync(tokenizer)) return false; return fs.readFileSync(tokenizer,"utf-8").indexOf("<|pt|>") >= 0; } catch (_) { return false; } }
function modelNameFromPath(folder) { var value=String(folder).replace(/.*faster-whisper-/i,"").replace(/[\\/].*$/,""); return value || path.basename(folder); }
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; 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; });
}
function scannerProcess() {
if (!scannerTimeline) { showToast("err", "Primeiro detecte as faixas da sequência."); return; }
var profile; try { profile=scannerProfileFromScreen(); } catch (error) { showToast("err", error.message); return; }
if (!profile.faixas_de_video.length && !profile.faixas_de_audio.length) { showToast("err", "Selecione ao menos uma faixa."); return; }
saveScannerProfile(); var outputRoot=scannerOutputRoot(); ensureDir(outputRoot);
var stamp=new Date().toISOString().replace(/[:.]/g,"-");
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);
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("python3",[SCANNER_ENGINE_SCRIPT,input,scannerReportPath],spawnOptions);
var stderr="", stdout="";
child.stdout.on("data",function(chunk) {
stdout += chunk.toString(); var lines=stdout.split("\n"); stdout=lines.pop();
lines.forEach(function(line) { try { var event=JSON.parse(line); if (event.evento === "progresso") taskUpdate({stage:event.etapa, detail:event.clipe || "", pct:event.percentual}); if (event.evento === "concluido" && event.arquivo) scannerReportPath=event.arquivo; } catch (_) {} });
});
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.");});
}
function scannerOpenReport() { if (!scannerReportPath) return; childProcess.spawn("open",[scannerReportPath],{detached:true,stdio:"ignore"}).unref(); }
function scannerCopyReport() { if (!scannerReportPath) return; try { var copy=childProcess.spawn("pbcopy"); copy.stdin.end(fs.readFileSync(scannerReportPath,"utf-8")); showToast("ok","Conteúdo do relatório copiado."); } catch (error) { showToast("err",error.message); } }
// Mostra na aba de edição o idioma configurado, para não precisar ir até
// Modelos só para conferir com que idioma a transcrição vai rodar.
function syncLanguageInfo() {
var el = document.getElementById("silenceLanguageInfo");
if (!el) return;
var code = loadLanguage();
var match = LANGUAGE_CATALOG.filter(function (l) { return l.value === code; })[0];
el.textContent = match ? match.label : code;
}
// ---- Status ----
function setStatus(state, text) {
var dot = document.getElementById("statusDot");
dot.className = "status-dot " + state;
var statusText = document.getElementById("statusText");
statusText.textContent = text;
statusText.setAttribute("data-state", state || "stopped");
var detail = document.getElementById("statusDetail");
if (detail) {
if (state === "connected") detail.textContent = "Ligação com o Premiere Pro ativa";
else if (state === "waiting") detail.textContent = "Aguardando um assistente de IA se conectar";
else if (state === "error") detail.textContent = "A ponte precisa de atenção";
else detail.textContent = "Aguardando o Premiere Pro";
}
// Espelha no cabeçalho para o status ficar visível de qualquer aba.
var pill = document.getElementById("headerStatus");
if (pill) {
pill.setAttribute("data-state", state || "stopped");
document.getElementById("headerStatusText").textContent =
state === "connected" ? "Conectado" :
state === "waiting" ? "Aguardando" :
state === "error" ? "Erro" : "Parado";
pill.title = text;
}
}
function setConnectionCheck(id, state, detail) {
var el = document.getElementById(id);
if (!el) return;
el.setAttribute("data-state", state);
var text = el.getElementsByTagName("small")[0];
if (text) text.textContent = detail;
}
// This reads only boolean Premiere state. Do not put project names, paths, or
// media information in the panel: the MCP safe-check uses the same boundary.
function refreshConnectionCenter() {
if (!bridgeRunning) {
setConnectionCheck("checkConnector", "waiting", "Start the connector first");
setConnectionCheck("checkProject", "waiting", "Waiting for the connector");
setConnectionCheck("checkSequence", "waiting", "Waiting for the connector");
return;
}
setConnectionCheck("checkConnector", "ready", "Running in Premiere Pro");
setConnectionCheck("checkProject", "waiting", "Checking…");
setConnectionCheck("checkSequence", "waiting", "Checking…");
cs.evalScript(
'(function(){var p=app&&app.project;return "mcpstate:"+(p&&typeof p.name!=="undefined"?"1":"0")+","+(p&&p.activeSequence?"1":"0");}())',
function (raw) {
var match = /^mcpstate:([01]),([01])$/.exec(String(raw || ""));
if (!match) {
setConnectionCheck("checkProject", "needs-attention", "Could not read Premiere state");
setConnectionCheck("checkSequence", "needs-attention", "Could not read Premiere state");
return;
}
var projectOpen = match[1] === "1";
var sequenceOpen = match[2] === "1";
setConnectionCheck("checkProject", projectOpen ? "ready" : "needs-attention", projectOpen ? "Project open" : "Open a project in Premiere Pro");
setConnectionCheck("checkSequence", sequenceOpen ? "ready" : "needs-attention", sequenceOpen ? "Active sequence open" : "Open a sequence in Premiere Pro");
}
);
}
// ---- File I/O via Node.js (CEP has access to Node) ----
// --enable-nodejs puts `require` in the global scope on most hosts, but on some it
// lands on cep_node instead. Try both, and fail loudly rather than letting fs come
// back undefined and surface later as "Cannot read properties of undefined".
function nodeRequire(moduleName) {
if (typeof require !== "undefined") return require(moduleName);
var cepNode = typeof cep_node !== "undefined" ? cep_node : typeof window !== "undefined" ? window.cep_node : null;
if (cepNode && typeof cepNode.require === "function") return cepNode.require(moduleName);
throw new Error(
'Node.js is not available in this CEP panel, so "' + moduleName + '" could not be loaded. ' +
"Check that CSXS/manifest.xml has <Parameter>--enable-nodejs</Parameter>, then fully quit and reopen Premiere Pro."
);
}
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");
var configured = nodeProcess && nodeProcess.env && nodeProcess.env.PREMIERE_TEMP_DIR;
if (typeof configured === "string" && configured.trim()) return configured.trim();
} catch (e) {
// The panel still has a safe OS temporary-directory fallback.
}
return path.join(os.tmpdir(), "premiere-mcp-bridge");
}
tempDir = defaultBridgeDirectory();
var latestUpdate = null;
var UPDATE_STATUS_STORAGE_KEY = "mcp_bridge_desktop_update_status_path";
var MAX_UPDATE_RESPONSE_BYTES = 64 * 1024;
function getPerUserGlobalInstall() {
try {
var nodeProcess = nodeRequire("process");
var appData = nodeProcess && nodeProcess.env && nodeProcess.env.APPDATA;
if (typeof appData !== "string" || !appData.trim()) return null;
var npmDirectory = path.resolve(appData, "npm");
var commandPath = path.resolve(npmDirectory, "premiere-pro-mcp.cmd");
var packagePath = path.resolve(npmDirectory, "node_modules", "premiere-pro-mcp", "package.json");
var relative = path.relative(npmDirectory, commandPath);
var packageRelative = path.relative(npmDirectory, packagePath);
if (
!relative ||
!packageRelative ||
relative.indexOf(".." + path.sep) === 0 ||
packageRelative.indexOf(".." + path.sep) === 0 ||
path.isAbsolute(relative) ||
path.isAbsolute(packageRelative) ||
!fs.existsSync(commandPath) ||
!fs.existsSync(packagePath)
) return null;
var packageMetadata = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
var serverVersion = MCPBridgeUpdater.normalizeVersion(packageMetadata && packageMetadata.version);
if (!serverVersion) return null;
return { commandPath: commandPath, serverVersion: serverVersion };
} catch (e) {
return null;
}
}
function getPerUserGlobalCommand() {
var install = getPerUserGlobalInstall();
return install ? install.commandPath : null;
}
function saveUpdateStatusPath(statusPath) {
try {
localStorage.setItem(UPDATE_STATUS_STORAGE_KEY, statusPath);
} catch (e) {}
}
function readScheduledUpdateStatus() {
var statusPath = "";
try {
statusPath = localStorage.getItem(UPDATE_STATUS_STORAGE_KEY) || "";
} catch (e) {
return null;
}
if (!statusPath || !path.isAbsolute(statusPath) || !fs.existsSync(statusPath)) return null;
try {
var status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
var validStates = ["waiting_for_premiere", "updating", "complete", "failed"];
if (
!status ||
status.schemaVersion !== "premiere-pro-mcp.desktop-update.v1" ||
validStates.indexOf(status.state) === -1
) return null;
return status;
} catch (e) {
return null;
}
}
function ensureDir(dir) {
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
} catch (e) {
log("Error creating dir: " + e.message, "err");
}
}
function listCommandFiles() {
try {
if (!fs.existsSync(tempDir)) return [];
var files = fs.readdirSync(tempDir);
return files
.filter(function (f) { return f.indexOf("cmd_") === 0 && f.slice(-4) === ".jsx"; })
.sort(); // process in order
} catch (e) {
return [];
}
}
function readFile(filePath) {
try {
return fs.readFileSync(filePath, "utf-8");
} catch (e) {
return null;
}
}
function writeFile(filePath, content) {
try {
fs.writeFileSync(filePath, content, "utf-8");
return true;
} catch (e) {
log("Error writing " + filePath + ": " + e.message, "err");
return false;
}
}
// Publish responses atomically so the MCP process never sees a partially-written
// JSON file. The staging suffix is not a response filename the server will read.
function writeResponseFile(filePath, content) {
var stagedPath = filePath + ".staged";
try {
fs.writeFileSync(stagedPath, content, "utf-8");
fs.renameSync(stagedPath, filePath);
return true;
} catch (e) {
deleteFile(stagedPath);
log("Error publishing " + filePath + ": " + e.message, "err");
return false;
}
}
function deleteFile(filePath) {
try {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
} catch (e) {}
}
// The heartbeat carries only protocol state. It is published by rename so a
// server never observes partial JSON, and an older server can ignore it.
function writeBridgeHeartbeat() {
if (!tempDir) return;
var heartbeatPath = path.join(tempDir, "bridge-heartbeat.json");
var stagedPath = heartbeatPath + "." + ENGINE_ID + ".staged";
try {
fs.writeFileSync(stagedPath, JSON.stringify({
protocolVersion: 1,
state: bridgeRunning ? "running" : "waiting"
}), "utf-8");
fs.renameSync(stagedPath, heartbeatPath);
} catch (e) {
deleteFile(stagedPath);
}
}
function startBridgeHeartbeat() {
if (heartbeatInterval) clearInterval(heartbeatInterval);
writeBridgeHeartbeat();
heartbeatInterval = setInterval(writeBridgeHeartbeat, HEARTBEAT_MS);
}
function stopBridgeHeartbeat() {
if (heartbeatInterval) clearInterval(heartbeatInterval);
heartbeatInterval = null;
// Keep the last heartbeat in place. Its age lets newer servers diagnose a
// stopped connector, while concurrent visible/headless panels stay isolated.
}
// ---- Script Execution ----
function executeScript(script, callback) {
// Script is already wrapped in an IIFE by the MCP server's buildScript(),
// so we pass it directly to avoid double-wrapping.
cs.evalScript(script, function (result) {
callback(result);
});
}
// ---- Command Processing ----
function processCommands() {
if (commandInFlight) return;
var cmdFiles = listCommandFiles();
// Premiere's scripting engine is stateful. Starting every discovered command
// at once lets overlapping edits race each other and overload the host. The
// atomic claim below still prevents duplicate work across the visible and
// headless panels, while this panel dispatches strictly one command at a time.
if (cmdFiles.length > 0) processOneCommand(cmdFiles[0]);
}
// Both the visible panel and the headless auto-start instance run this file.
// A rename is atomic on the same volume, so whichever engine renames first owns
// the command; the loser's rename throws and it skips the file.
var ENGINE_ID = Math.random().toString(36).slice(2, 8);
var commandInFlight = false;
function processOneCommand(cmdFileName) {
var cmdFilePath = path.join(tempDir, cmdFileName);
var claimPath = cmdFilePath + "." + ENGINE_ID + ".claimed";
try {
fs.renameSync(cmdFilePath, claimPath);
} catch (e) {
return; // another engine claimed this command
}
var script = readFile(claimPath);
deleteFile(claimPath);
if (!script) {
log("Failed to read: " + cmdFileName, "err");
return;
}
commandInFlight = true;
// Derive response filename: cmd_12345.jsx -> res_12345.json
var id = cmdFileName.replace("cmd_", "").replace(".jsx", "");
var resFilePath = path.join(tempDir, "res_" + id + ".json");
log("Executing: " + cmdFileName + " (" + script.length + " chars)", "cmd");
// While evalScript is in flight, heartbeat a busy file so the MCP server can
// tell "script still running (modal dialog?)" apart from "plugin not running".
// Only starts after 2s, so fast commands never touch the extra file.
var busyFilePath = path.join(tempDir, "busy_" + id + ".json");
var startedAt = new Date().getTime();
var busyTimer = setInterval(function () {
writeFile(busyFilePath, '{"id":"' + id + '","elapsedMs":' + (new Date().getTime() - startedAt) + "}");
}, 2000);
executeScript(script, function (result) {
clearInterval(busyTimer);
deleteFile(busyFilePath);
commandCount++;
document.getElementById("cmdCount").textContent = commandCount;
var response;
try {
// ExtendScript returns a string; try to parse it as JSON
if (result && result !== "undefined" && result !== "null") {
// Check if it's already valid JSON
var parsed = JSON.parse(result);
response = JSON.stringify(parsed);
log("Result: OK", "ok");
} else {
// An empty result means evalScript gave us nothing back. That is a bridge
// failure, not a successful command with no data — reporting it as "OK" is
// what made this so hard to diagnose. Say so.
response = JSON.stringify({
success: false,
error:
"The bridge received an empty result from evalScript (got " +
(typeof result) +
"). The script may not have run. If every command does this, the CEP panel is stale — " +
"close and reopen it (a reload is not enough), or reinstall the extension.",
});
log("Result: EMPTY — evalScript returned nothing (see response file)", "err");
}
} catch (e) {
// If result isn't JSON, wrap it
if (result && result.indexOf("Error") === 0) {
response = JSON.stringify({ success: false, error: result });
log("Result: " + result, "err");
} else {
response = JSON.stringify({ success: true, data: result });
log("Result: OK (raw)", "ok");
}
}
writeResponseFile(resFilePath, response);
commandInFlight = false;
// Continue without waiting for the next poll interval, preserving FIFO
// ordering while minimizing queue handoff latency.
if (bridgeRunning) processCommands();
});
}
// ---- Bridge Control ----
function startBridge() {
tempDir = document.getElementById("tempDir").value.trim();
if (!tempDir) {
log("Please set a temp directory", "err");
document.getElementById("tempDir").focus();
return;
}
ensureDir(tempDir);
bridgeRunning = true;
startBridgeHeartbeat();
setStatus("waiting", "Connector running");
log("Connector started and ready for safe checks.", "ok");
document.getElementById("btnStart").disabled = true;
document.getElementById("btnStop").disabled = false;
refreshConnectionCenter();
pollInterval = setInterval(function () {
if (bridgeRunning) processCommands();
}, POLL_MS);
}
function stopBridge() {
bridgeRunning = false;
writeBridgeHeartbeat();
stopBridgeHeartbeat();
if (pollInterval) clearInterval(pollInterval);
pollInterval = null;
setStatus("", "Stopped");
refreshConnectionCenter();
log("Bridge stopped");
document.getElementById("btnStart").disabled = false;
document.getElementById("btnStop").disabled = true;
}
function saveTempDir() {
tempDir = document.getElementById("tempDir").value.trim();
log("Temp directory saved: " + tempDir);
showToast("ok", "Pasta da ponte salva.");
// Persist via localStorage
try {
localStorage.setItem("mcp_bridge_temp_dir", tempDir);
} catch (e) {}
}
// ---- Connector Updates ----
function setUpdateUI(title, detail, buttonText, disabled) {
document.getElementById("updateTitle").textContent = title;
document.getElementById("updateDetail").textContent = detail;
var button = document.getElementById("btnUpdate");
button.textContent = buttonText;
button.disabled = !!disabled;
}
function updateInstructionUrl() {
return MCPBridgeUpdater.RELEASES_URL;
}
function openTrustedUpdateInstructions() {
var url = updateInstructionUrl();
if (!MCPBridgeUpdater.isTrustedDownloadUrl(url)) {
showUpdateCheckError("The update instructions link was not trusted.");
return;
}
try {
var childProcess = nodeRequire("child_process");
var command =
os.platform() === "win32"
? ["cmd.exe", ["/d", "/s", "/c", "start", "", url]]
: ["open", [url]];
var child = childProcess.spawn(command[0], command[1], {
detached: true,
stdio: "ignore",
});
child.unref();
} catch (e) {
showUpdateCheckError("Could not open the update instructions. Try again.");
}
}
function restoreScheduledUpdateStatus() {
var status = readScheduledUpdateStatus();
if (!status) return false;
if (status.state === "complete") {
setUpdateUI(
"Update complete",
"Restart your MCP client, then use Verify Premiere connection before editing.",
"Check again",
false
);
return true;
}
if (status.state === "failed") {
setUpdateUI(
"Update needs attention",
"Nothing was changed in your projects. Check the update command or retry after Premiere closes.",
"Check again",
false
);
return true;
}
setUpdateUI(
"Update scheduled",
status.state === "updating"
? "The global MCP server and connector are being updated. Keep Premiere closed."
: "Quit Premiere Pro. The updater will begin after it fully closes.",
"Scheduled",
true
);
return true;
}
function checkForUpdates() {
latestUpdate = null;
var globalInstall = os.platform() === "win32" ? getPerUserGlobalInstall() : null;
var responseTooLarge = false;
setUpdateUI(
"Version " + MCPBridgeUpdater.CURRENT_VERSION,
"Checking for updates…",
"Checking…",
true
);
var request = https.get(
MCPBridgeUpdater.LATEST_PACKAGE_API,
{
headers: {
Accept: "application/vnd.npm.install-v1+json",
"User-Agent": "premiere-pro-mcp-connector/" + MCPBridgeUpdater.CURRENT_VERSION,
},
},
function (response) {
var body = "";
response.setEncoding("utf8");
response.on("data", function (chunk) {
if (body.length + chunk.length > MAX_UPDATE_RESPONSE_BYTES) {
responseTooLarge = true;
request.destroy(new Error("npm registry update record was unexpectedly large."));
return;
}
body += chunk;
});
response.on("end", function () {
if (responseTooLarge) return;
if (response.statusCode !== 200) {
showUpdateCheckError("Could not check npm (HTTP " + response.statusCode + ").");
return;
}
try {
var update = MCPBridgeUpdater.updateStateFromPackageRecord(
MCPBridgeUpdater.CURRENT_VERSION,
JSON.parse(body)
);
var serverUpdateAvailable = Boolean(
globalInstall &&
MCPBridgeUpdater.compareVersions(update.latestVersion, globalInstall.serverVersion) > 0
);
var needsUpdate = update.updateAvailable || serverUpdateAvailable;
if (needsUpdate) {
latestUpdate = {
version: update.latestVersion,
};
if (os.platform() === "win32" && globalInstall) {
var versionSummary =
"Server " + globalInstall.serverVersion + ", connector " + MCPBridgeUpdater.CURRENT_VERSION + ". ";
setUpdateUI(
"Version " + update.latestVersion + " is available",
versionSummary + "Update both together after you close Premiere.",
"Update after quit",
false
);
} else if (os.platform() === "win32") {
setUpdateUI(
"Version " + update.latestVersion + " is available",
"A global npm install was not found. This panel will not modify a source checkout.",
"Open instructions",
false
);
} else {
setUpdateUI(
"Version " + update.latestVersion + " is available",
"Open the matching release, then update your local server using the documented install path.",
"Open instructions",
false
);
}
} else {
var currentDetail = globalInstall
? "Server " + globalInstall.serverVersion + " and connector " + MCPBridgeUpdater.CURRENT_VERSION + " are current."
: "Your connector release is current. This check does not alter your projects or MCP client configuration.";
setUpdateUI(
"Version " + MCPBridgeUpdater.CURRENT_VERSION,
currentDetail,
"Check again",
false
);
}
} catch (e) {
showUpdateCheckError("npm returned an unreadable package record.");
}
});
}
);
request.setTimeout(10000, function () {
request.destroy(new Error("Update check timed out"));
});
request.on("error", function () {
showUpdateCheckError(
responseTooLarge ? "npm returned an unexpectedly large package record." : "Unable to check while offline."
);
});
}
function showUpdateCheckError(message) {
setUpdateUI(
"Version " + MCPBridgeUpdater.CURRENT_VERSION,
message,
"Check again",
false
);
}
function handleUpdateClick() {
if (!latestUpdate) {
checkForUpdates();
return;
}
if (os.platform() !== "win32") {
openTrustedUpdateInstructions();
return;
}
var cliPath = getPerUserGlobalCommand();
if (!cliPath) {
openTrustedUpdateInstructions();
return;
}
var confirmation =
"Update Premiere MCP to " + latestUpdate.version + " after Premiere Pro fully closes?\n\n" +
"This updates only the per-user global MCP server and its connector. " +
"It does not change your projects or MCP client configuration, and it will not force Premiere to close.";
if (typeof window.confirm === "function" && !window.confirm(confirmation)) return;
try {
var childProcess = nodeRequire("child_process");
var nodeCrypto = nodeRequire("crypto");
var scheduled = MCPBridgeUpdater.scheduleWindowsGlobalUpdate({
cliPath: cliPath,
runtime: {
fs: fs,
path: path,
os: os,
childProcess: childProcess,
crypto: nodeCrypto,
},
});
saveUpdateStatusPath(scheduled.statusPath);
setUpdateUI(
"Update scheduled",
"Quit Premiere Pro. The updater will refresh the global server and connector after it fully closes.",
"Scheduled",
true
);
} catch (e) {
showUpdateCheckError("Could not schedule the local update. No files were changed.");
}
}
// ---- Silence Cut tab ----
// Fixed local machine paths for the Whisper pipeline (this rig's setup).
var SILENCE_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python";
var SILENCE_TRANSCRIBE_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/transcribe_cli.py";
var DEFAULT_MODELS_PATH = "/Volumes/Merongo/SISTEMAS/WHISPERX/whisper/models";
var SILENCE_OUTPUT_ROOT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/output";
var SILENCE_DOWNLOAD_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/download_model_cli.py";
var SILENCE_DIARIZE_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/diarize_cli.py";
var VOICE_FEATURES_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/voice_features_cli.py";
var EDITORIAL_ACTIONS_APPLY_SCRIPT_FALLBACK = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/scripts/apply-editorial-actions.mjs";
// Catálogo dos tamanhos oficiais do faster-whisper hospedados pela Systran no
// Hugging Face (org consistente — outras variantes como distil-* e large-v3-turbo
// vivem em orgs diferentes com nomes de pasta inconsistentes, por isso ficam de fora
// por ora). Ordenado do mais rápido/impreciso ao mais lento/preciso.
var MODEL_CATALOG = [
{ value: "tiny", label: "Tiny", approxMB: 75 },
{ value: "base", label: "Base", approxMB: 145 },
{ value: "small", label: "Small", approxMB: 483 },
{ value: "medium", label: "Medium", approxMB: 1500 },
{ value: "large-v1", label: "Large v1", approxMB: 3100 },
{ value: "large-v2", label: "Large v2", approxMB: 3100 },
{ value: "large-v3", label: "Large v3", approxMB: 3100 },
];
var DEFAULT_MODEL = "small";
function formatMB(mb) {
if (mb >= 1000) return (mb / 1000).toFixed(1).replace(/\.0$/, "") + " GB";
return Math.round(mb) + " MB";
}
function folderSizeMB(folder) {
var total = 0;
function walk(dir) {
var entries;
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return; }
for (var i = 0; i < entries.length; i++) {
var full = path.join(dir, entries[i].name);
if (entries[i].isSymbolicLink()) continue;
if (entries[i].isDirectory()) walk(full);
else {
try { total += fs.statSync(full).size; } catch (e) {}
}
}
}
walk(folder);
return total / (1024 * 1024);
}
var LANGUAGE_CATALOG = [
{ value: "pt", label: "Português" },
{ value: "en", label: "English" },
{ value: "es", label: "Español" },
];
var DEFAULT_LANGUAGE = "pt";
var silenceState = {
sequenceName: null,
mediaPath: null,
fileBaseName: null,
outputDir: null,
transcriptPath: null,
transcribeModel: null,
cachedTranscriptPath: null,
jsonOutputPath: null,
};
// ---- Central transcript cache ----
// Evita rodar o Whisper de novo para um vídeo já transcrito antes: indexa
// transcrições por caminho absoluto + tamanho + data de modificação do
// arquivo de mídia, para invalidar sozinho se o arquivo original mudar.
var TRANSCRIPT_CACHE_DIR = path.join(os.homedir(), ".premiere-mcp", "transcricoes");
var TRANSCRIPT_INDEX_FILE = path.join(TRANSCRIPT_CACHE_DIR, "index.json");
function loadTranscriptIndex() {
try {
if (!fs.existsSync(TRANSCRIPT_INDEX_FILE)) return {};
var parsed = JSON.parse(fs.readFileSync(TRANSCRIPT_INDEX_FILE, "utf-8"));
return (parsed && typeof parsed === "object") ? parsed : {};
} catch (e) {
return {};
}
}
function saveTranscriptIndex(index) {
try {
ensureDir(TRANSCRIPT_CACHE_DIR);
fs.writeFileSync(TRANSCRIPT_INDEX_FILE, JSON.stringify(index, null, 2));
return true;
} catch (e) {
return false;
}
}
function transcriptCacheKey(mediaPath) {
var nodeCrypto = nodeRequire("crypto");
return nodeCrypto.createHash("sha1").update(String(mediaPath)).digest("hex");
}
// Uma entrada de índice é sempre uma lista (várias transcrições podem existir
// para o mesmo vídeo: modelos diferentes, com/sem diarização, refeitas etc.).
function entryListFor(index, key) {
var entry = index[key];
if (!entry) return [];
return Array.isArray(entry) ? entry : [entry]; // compat com formato antigo (objeto único)
}
function findCachedTranscripts(mediaPath) {
try {
var stat = fs.statSync(mediaPath);
var index = loadTranscriptIndex();
var key = transcriptCacheKey(mediaPath);
var list = entryListFor(index, key);
return list.filter(function (entry) {
return entry.size === stat.size && entry.mtimeMs === stat.mtimeMs && fs.existsSync(entry.transcriptPath);
});
} catch (e) {
return [];
}
}
function transcriptCacheFileName(mediaPath, model, when) {
var base = slugify(path.basename(mediaPath));
var pad = function (n) { return String(n).padStart(2, "0"); };
var dateStr = when.getFullYear() + pad(when.getMonth() + 1) + pad(when.getDate());
var timeStr = pad(when.getHours()) + pad(when.getMinutes()) + pad(when.getSeconds());
return base + "-" + dateStr + "-" + timeStr + "-" + slugify(model || "modelo") + ".json";
}
function saveTranscriptToCache(mediaPath, transcriptPath, model) {
try {
var stat = fs.statSync(mediaPath);
ensureDir(TRANSCRIPT_CACHE_DIR);
var key = transcriptCacheKey(mediaPath);
var index = loadTranscriptIndex();
var list = entryListFor(index, key);
var when = new Date();
var fileName = transcriptCacheFileName(mediaPath, model, when);
var cachedPath = path.join(TRANSCRIPT_CACHE_DIR, fileName);
fs.copyFileSync(transcriptPath, cachedPath);
list.push({
mediaPath: mediaPath,
size: stat.size,
mtimeMs: stat.mtimeMs,
model: model || null,
transcriptPath: cachedPath,
savedAt: when.toISOString(),
});
index[key] = list;
saveTranscriptIndex(index);
return cachedPath;
} catch (e) {
return null;
}
}
function silenceLog(msg, cls) {
var el = document.getElementById("silenceLog");
var entry = document.createElement("div");
entry.className = "log-entry " + (cls || "");
var ts = new Date().toLocaleTimeString();
entry.textContent = "[" + ts + "] " + msg;
el.appendChild(entry);
el.scrollTop = el.scrollHeight;
while (el.children.length > 100) el.removeChild(el.firstChild);
}
function slugify(name) {
return String(name || "video")
.toLowerCase()
.replace(/\.[^/.]+$/, "")
.replace(/[^a-z0-9]+/g, "-")
.replace(/(^-|-$)/g, "") || "video";
}
function silenceDetectClip() {
setBusy("btnDetectClip", true);
setStep(1, "running", "Detectando…");
document.getElementById("silenceClipInfo").textContent = "Lendo a sequência ativa no Premiere…";
cs.evalScript(
"(function(){" +
"try{" +
"var seq=app.project.activeSequence;" +
"if(!seq) return JSON.stringify({ok:false,error:'Nenhuma sequência ativa.'});" +
"var vt=seq.videoTracks[0];" +
"if(!vt||vt.clips.numItems===0) return JSON.stringify({ok:false,error:'Vídeo 1 está vazio na sequência ativa.'});" +
"var clip=vt.clips[0];" +
"var projItem=clip.projectItem;" +
"var mediaPath=projItem&&projItem.getMediaPath?projItem.getMediaPath():'';" +
"return JSON.stringify({ok:true,sequenceName:seq.name,mediaPath:mediaPath,clipName:clip.name});" +
"}catch(e){return JSON.stringify({ok:false,error:String(e)});}" +
"}())",
function (raw) {
setBusy("btnDetectClip", false);
var result;
try {
result = JSON.parse(raw);
} catch (e) {
result = { ok: false, error: "Resposta inesperada do Premiere." };
}
var info = document.getElementById("silenceClipInfo");
var card = document.getElementById("silenceClipCard");
if (!result.ok || !result.mediaPath) {
card.hidden = true;
setStep(1, "error", "Não encontrado");
info.textContent = (result.error || "Caminho de mídia vazio.") +
" Abra a sequência no Premiere e tente de novo.";
showToast("err", result.error || "Não foi possível ler a sequência ativa.");
lockStepsFrom(2);
return;
}
silenceState.sequenceName = result.sequenceName;
silenceState.mediaPath = result.mediaPath;
silenceState.fileBaseName = slugify(result.clipName || result.sequenceName);
silenceState.outputDir = path.join(SILENCE_OUTPUT_ROOT, silenceState.fileBaseName);
silenceState.transcriptPath = null;
silenceState.jsonOutputPath = null;
document.getElementById("clipSequenceName").textContent = result.sequenceName;
document.getElementById("clipFileName").textContent = path.basename(result.mediaPath);
var pathCell = document.getElementById("clipMediaPath");
pathCell.textContent = result.mediaPath;
pathCell.title = result.mediaPath;
card.hidden = false;
info.textContent = "";
setStep(1, "done", "Vídeo escolhido");
lockStepsFrom(2);
setStep(2, "ready", "Sua vez");
document.getElementById("btnTranscribe").disabled = false;
document.getElementById("silenceJsonCard").hidden = true;
document.getElementById("silencePlanInfo").textContent = "";
silenceLog("Clipe detectado: " + result.mediaPath);
renderCachedTranscripts();
}
);
}
// Volta os passos seguintes ao estado travado quando o vídeo (ou a transcrição)
// muda — evita aplicar um plano gerado para outro material.
function lockStepsFrom(first) {
var buttons = {
2: ["btnTranscribe"],
3: ["btnDiarize", "btnVoiceFeatures"],
4: ["btnBuildPlan"],
5: ["btnApplyEditorialActions"],
};
for (var n = first; n <= 5; n++) {
setStep(n, "locked", "Bloqueado");
(buttons[n] || []).forEach(function (id) {
var btn = document.getElementById(id);
if (btn) btn.disabled = true;
});
}
}
function renderCachedTranscripts() {
var section = document.getElementById("silenceCachedSection");
var select = document.getElementById("silenceCachedSelect");
var entries = silenceState.mediaPath ? findCachedTranscripts(silenceState.mediaPath) : [];
if (!entries.length) {
section.hidden = true;
select.innerHTML = "";
return;
}
entries.sort(function (a, b) { return new Date(b.savedAt) - new Date(a.savedAt); });
select.innerHTML = "";
entries.forEach(function (entry, i) {
var opt = document.createElement("option");
opt.value = String(i);
opt.textContent = new Date(entry.savedAt).toLocaleString() + " — modelo " + (entry.model || "?");
select.appendChild(opt);
});
select.dataset.entries = JSON.stringify(entries);
section.hidden = false;
silenceLog(entries.length + " transcrição(ões) salva(s) encontrada(s) para este vídeo.", "ok");
}
function silenceUseCachedTranscript() {
var select = document.getElementById("silenceCachedSelect");
var entries = JSON.parse(select.dataset.entries || "[]");
var entry = entries[Number(select.value)];
if (!entry) return;
silenceState.transcriptPath = entry.transcriptPath;
silenceState.transcribeModel = entry.model;
silenceState.cachedTranscriptPath = entry.transcriptPath;
document.getElementById("silenceCacheInfo").textContent =
"Usando a transcrição salva em " + new Date(entry.savedAt).toLocaleString() + ".";
unlockAfterTranscript();
showToast("ok", "Transcrição reaproveitada — pule para o passo 4.");
silenceLog("Transcrição salva selecionada — pronto para gerar o plano de cortes.", "ok");
}
// Passos que dependem de existir uma transcrição, seja nova ou reaproveitada.
function unlockAfterTranscript() {
setStep(2, "done", "Transcrito");
setStep(3, "ready", "Opcional");
setStep(4, "ready", "Sua vez");
document.getElementById("btnBuildPlan").disabled = false;
document.getElementById("btnDiarize").disabled = false;
document.getElementById("btnVoiceFeatures").disabled = false;
}
// Premiere is launched by Finder/LaunchServices, not a login shell, so the CEP
// panel's process PATH lacks Homebrew/nvm dirs where `node` actually lives.
// Resolve an absolute path the same way SILENCE_PYTHON_BIN does, instead of
// relying on spawn("node", ...) to find it via PATH.
// cs.getSystemPath() sometimes returns a "file:/..." URI (percent-encoded,
// occasionally missing a leading slash) instead of a plain fs path. Normalize
// it before handing it to path.join/fs/spawn.
function toFsPath(maybeUri) {
var s = String(maybeUri || "");
if (s.indexOf("file:") === 0) {
s = s.replace(/^file:\/{0,3}/, "/");
try { s = decodeURIComponent(s); } catch (e) {}
}
return s;
}
function resolveNodeBin() {
var candidates = os.platform() === "win32"
? ["node.exe"]
: ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node", "node"];
for (var i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
if (candidate.indexOf("/") !== 0 || fs.existsSync(candidate)) return candidate;
}
return "node";
}
var PROGRESS_PREFIX = "@@PROGRESS ";
function runStreamed(bin, args, onLine, onDone, options) {
var childProcess = nodeRequire("child_process");
var child = childProcess.spawn(bin, args, options || {});
var buffer = "";
var ultimoErro = "";
// Linhas "@@PROGRESS {json}" são o canal de progresso dos scripts Python/Node:
// vão para a barra no topo em vez de virarem ruído no log.
function handleLine(line) {
var trimmed = line.trim();
if (trimmed.indexOf(PROGRESS_PREFIX) === 0) {
try {
taskUpdate(JSON.parse(trimmed.slice(PROGRESS_PREFIX.length)));
} catch (e) {}
return;
}
onLine(line);
}
function flushLines(chunk) {
buffer += chunk;
var parts = buffer.split("\n");
buffer = parts.pop();
for (var i = 0; i < parts.length; i++) {
if (parts[i].trim()) handleLine(parts[i]);
}
}
child.stdout.on("data", function (d) { flushLines(d.toString()); });
child.stderr.on("data", function (d) { ultimoErro = d.toString().trim() || ultimoErro; flushLines(d.toString()); });
child.on("error", function (err) { onDone(1, "spawn error: " + err.message); });
child.on("close", function (code) {
if (buffer.trim()) handleLine(buffer);
onDone(code, ultimoErro || null);
});
}
function silenceTranscribe() {
if (!silenceState.mediaPath) return;
document.getElementById("silenceCacheInfo").textContent = "";
var model = document.getElementById("silenceModel").value;
setBusy("btnTranscribe", true);
setStep(2, "running", "Transcrevendo…");
ensureDir(silenceState.outputDir);
var out = path.join(silenceState.outputDir, "transcricao.json");
silenceLog("Transcrevendo com modelo " + model + "… isso pode levar alguns minutos.");
taskStart("Transcrevendo o áudio", "Preparando o modelo " + model);
var lastLine = "";
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_TRANSCRIBE_SCRIPT,
"--video", silenceState.mediaPath,
"--out", out,
"--model", model,
"--language", loadLanguage(),
"--models-path", effectiveModelsPath(),
"--cache-dir", silenceState.outputDir,
],
function (line) { lastLine = line; silenceLog(line); },
function (code, lastErr) {
setBusy("btnTranscribe", false);
if (code !== 0) {
setStep(2, "error", "Falhou");
taskEnd(false, "A transcrição falhou (código " + code + "). Veja os detalhes técnicos.", lastErr || lastLine);
silenceLog("Falha na transcrição (código " + code + ").", "err");
return;
}
var parsed = null;
try { parsed = JSON.parse(lastLine); } catch (e) {}
if (!parsed || parsed.ok === false) {
setStep(2, "error", "Falhou");
taskEnd(false, "A transcrição terminou com uma resposta inválida.", lastLine);
silenceLog("Transcrição terminou mas resposta é inválida: " + lastLine, "err");
return;
}
silenceState.transcriptPath = out;
silenceState.transcribeModel = model;
var resumo = parsed.segments + " trechos · " + Math.round(parsed.duration) + "s de áudio";
silenceLog("Transcrição concluída: " + resumo, "ok");
document.getElementById("silenceCacheInfo").textContent = "Transcrição pronta — " + resumo + ".";
applyEditorPersonalityToTranscript(out);
silenceState.cachedTranscriptPath = saveTranscriptToCache(silenceState.mediaPath, out, model);
if (silenceState.cachedTranscriptPath) {
silenceLog("Transcrição salva na pasta central para reaproveitar depois.", "ok");
}
taskEnd(true, "Transcrição pronta: " + resumo);
unlockAfterTranscript();
if (document.getElementById("silenceDiarize").checked) {
silenceDiarizeSpeakers();
}
},
hfSpawnOptions()
);
}
function silenceDiarizeSpeakers() {
if (!silenceState.transcriptPath) return;
if (!silenceState.mediaPath) {
silenceLog("Detecte o clipe/sequência primeiro (passo 1) para saber qual vídeo diarizar.", "err");
showToast("err", "Detecte o vídeo no passo 1 primeiro.");
return;
}
if (!loadHfToken()) {
silenceLog("Diarização pulada: configure um HF_TOKEN na aba Ajustes.", "err");
showToast("err", "Configure o token do Hugging Face na aba Ajustes.");
return;
}
var info = document.getElementById("silenceDiarizeInfo");
setBusy("btnDiarize", true);
setStep(3, "running", "Detectando…");
info.textContent = "Detectando locutores…";
silenceLog("Detectando locutores…");
taskStart("Detectando quem fala", "Preparando");
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_DIARIZE_SCRIPT,
"--video", silenceState.mediaPath,
"--transcript", silenceState.transcriptPath,
"--out", silenceState.transcriptPath,
"--models-path", effectiveModelsPath(),
"--cache-dir", silenceState.outputDir,
],
function (line) { silenceLog(line); },
function (code, lastErr) {
setBusy("btnDiarize", false);
if (code !== 0) {
setStep(3, "ready", "Opcional");
info.textContent = "Falhou — veja os detalhes técnicos.";
taskEnd(false, "A detecção de locutores falhou (código " + code + ").", lastErr);
silenceLog("Falha na diarização (código " + code + (lastErr ? ": " + lastErr : "") + ").", "err");
return;
}
var parsed = null;
try { parsed = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")).diarization; } catch (e) {}
var msg = parsed ? parsed.num_speakers + " locutor(es): " + parsed.speakers.join(", ") : "Locutores detectados.";
silenceLog(msg, "ok");
info.textContent = msg;
setStep(3, "done", "Enriquecido");
taskEnd(true, msg);
syncTranscriptToCache();
},
hfSpawnOptions()
);
}
// Depois de mutar silenceState.transcriptPath em lugar (diarização, métricas de
// voz), propaga a mudança pra cópia central de cache, se forem arquivos diferentes.
function syncTranscriptToCache() {
if (silenceState.cachedTranscriptPath && silenceState.cachedTranscriptPath !== silenceState.transcriptPath) {
try {
fs.copyFileSync(silenceState.transcriptPath, silenceState.cachedTranscriptPath);
} catch (e) {}
}
}
function silenceComputeVoiceFeatures() {
if (!silenceState.transcriptPath) return;
if (!silenceState.mediaPath) {
silenceLog("Detecte o clipe/sequência primeiro (passo 1) para saber qual vídeo analisar.", "err");
showToast("err", "Detecte o vídeo no passo 1 primeiro.");
return;
}
var info = document.getElementById("silenceVoiceFeaturesInfo");
setBusy("btnVoiceFeatures", true);
setStep(3, "running", "Calculando…");
info.textContent = "Calculando…";
silenceLog("Calculando pitch/energia/velocidade de fala/pausas…");
taskStart("Medindo a voz", "Lendo a transcrição");
runStreamed(
SILENCE_PYTHON_BIN,
[
VOICE_FEATURES_SCRIPT,
"--video", silenceState.mediaPath,
"--transcript", silenceState.transcriptPath,
"--out", silenceState.transcriptPath,
"--cache-dir", silenceState.outputDir,
],
function (line) { silenceLog(line); },
function (code, lastErr) {
setBusy("btnVoiceFeatures", false);
if (code !== 0) {
setStep(3, "ready", "Opcional");
info.textContent = "Falhou — veja os detalhes técnicos.";
taskEnd(false, "O cálculo das métricas de voz falhou (código " + code + ").", lastErr);
silenceLog("Falha ao calcular métricas de voz (código " + code + (lastErr ? ": " + lastErr : "") + ").", "err");
return;
}
var msg = "Pitch, energia, velocidade e pausas calculados por trecho.";
silenceLog(msg, "ok");
info.textContent = msg;
setStep(3, "done", "Enriquecido");
taskEnd(true, msg);
syncTranscriptToCache();
}
);
}
// Não gera decisões de corte: só empacota transcrição + tempos + locutores +
// métricas de voz + configurações de edição num JSON para um agente de IA
// externo analisar. Quem decide os cortes é a IA, não este painel.
function silenceGenerateJson() {
if (!silenceState.transcriptPath) return;
setBusy("btnBuildPlan", true);
setStep(4, "running", "Gerando…");
silenceLog("Gerando arquivo JSON para a IA…");
try {
var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
var silenceEnabled = document.getElementById("silenceRemovalEnabled").checked;
var minSilence = parseFloat(document.getElementById("silenceRemovalMinDuration").value);
if (!isFinite(minSilence) || minSilence < 0) minSilence = 0.8;
transcript.sequence = {
name: silenceState.sequenceName,
media_path: silenceState.mediaPath,
};
transcript.settings = Object.assign({}, transcript.settings, {
silence_removal: { enabled: silenceEnabled, min_silence_seconds: minSilence },
});
ensureDir(silenceState.outputDir);
var out = path.join(silenceState.outputDir, "dados-para-ia.json");
fs.writeFileSync(out, JSON.stringify(transcript, null, 2), "utf-8");
silenceState.jsonOutputPath = out;
var segCount = Array.isArray(transcript.segments) ? transcript.segments.length : 0;
document.getElementById("jsonSegmentCount").textContent = segCount;
var pathCell = document.getElementById("jsonOutputPath");
pathCell.textContent = out;
pathCell.title = out;
document.getElementById("silenceJsonCard").hidden = false;
document.getElementById("silencePlanInfo").textContent =
"Envie este arquivo a um agente de IA externo. Ele deve devolver o plano de edição em JSON para o passo 5.";
setStep(4, "done", "JSON pronto");
setStep(5, "ready", "Sua vez");
document.getElementById("btnApplyEditorialActions").disabled = false;
showToast("ok", "JSON gerado: " + path.basename(out));
silenceLog("JSON gerado em " + out + " (" + segCount + " trechos).", "ok");
} catch (e) {
setStep(4, "error", "Falhou");
showToast("err", "Falha ao gerar o JSON: " + e.message);
silenceLog("Falha ao gerar o JSON: " + e.message, "err");
}
setBusy("btnBuildPlan", false);
}
// O script de cortes importa ../dist/tools/*.js, então precisa rodar de dentro
// do repositório — não da cópia instalada em ~/Library/.../CEP/extensions.
// Ordem: repoRoot gravado pelo install-cep.sh > relativo ao painel (modo symlink) > fallback fixo.
function resolveScript(scriptFileName, fallbackPath) {
var candidates = [];
var repoRoot = String(loadConfig().repoRoot || "").trim();
if (repoRoot) candidates.push(path.join(repoRoot, "scripts", scriptFileName));
try {
var extensionRoot = toFsPath(cs.getSystemPath("extension"));
candidates.push(path.join(extensionRoot, "..", "scripts", scriptFileName));
} catch (e) {}
candidates.push(fallbackPath);
for (var i = 0; i < candidates.length; i++) {
try { if (fs.existsSync(candidates[i])) return candidates[i]; } catch (e) {}
}
return null;
}
function resolveEditorialActionsScript() {
return resolveScript("apply-editorial-actions.mjs", EDITORIAL_ACTIONS_APPLY_SCRIPT_FALLBACK);
}
// ---- Aplicar o plano de edição devolvido pela IA (cut/zoom/text/marker) ----
function applyEditorialActions() {
if (!silenceState.sequenceName) {
silenceLog("Detecte o clipe/sequência primeiro (passo 1).", "err");
showToast("err", "Detecte o vídeo no passo 1 primeiro.");
return;
}
var planPath = document.getElementById("editorialPlanPath").value.trim();
if (!planPath) {
silenceLog("Informe o caminho do arquivo do plano (JSON de actions).", "err");
showToast("err", "Escolha o arquivo do plano de edição.");
return;
}
if (!fs.existsSync(planPath)) {
silenceLog("Arquivo não encontrado: " + planPath, "err");
showToast("err", "Arquivo não encontrado: " + path.basename(planPath));
return;
}
var plan;
try {
plan = JSON.parse(fs.readFileSync(planPath, "utf-8"));
} catch (e) {
silenceLog("Não foi possível ler o JSON do plano: " + e.message, "err");
showToast("err", "O arquivo não é um JSON válido.");
return;
}
var numActions = Array.isArray(plan.actions) ? plan.actions.length : 0;
if (!numActions) {
silenceLog("O plano não tem nenhuma action.", "err");
showToast("err", "O plano não tem nenhuma ação.");
return;
}
var summary =
numActions + " ação(ões) no plano (cortes, zooms, textos e/ou marcadores).\n\n" +
"Um backup da sequência atual será criado antes de aplicar. Confirmar?";
if (typeof window.confirm === "function" && !window.confirm(summary)) return;
setBusy("btnApplyEditorialActions", true);
setStep(5, "running", "Aplicando…");
taskStart("Aplicando o plano de edição", "Criando backup da sequência");
silenceLog("Criando backup da sequência…");
cs.evalScript(
"(function(){try{app.project.activeSequence.clone();return 'ok';}catch(e){return 'error:'+String(e);}}())",
function (raw) {
if (String(raw).indexOf("ok") !== 0) {
silenceLog("Não foi possível criar o backup automático (" + raw + "). Aplicando mesmo assim.", "err");
} else {
silenceLog("Backup criado.", "ok");
}
var applyScript = resolveEditorialActionsScript();
if (!applyScript) {
setBusy("btnApplyEditorialActions", false);
setStep(5, "error", "Falhou");
taskEnd(false, "Script apply-editorial-actions.mjs não encontrado.");
silenceLog(
"Script apply-editorial-actions.mjs não encontrado. Rode scripts/install-cep.sh para gravar o repoRoot em " +
CONFIG_FILE + ".",
"err"
);
return;
}
var nodeBin = resolveNodeBin();
silenceLog("Aplicando plano de edição na timeline…");
runStreamed(
nodeBin,
[applyScript, "--plan", planPath, "--sequence", silenceState.sequenceName],
function (line) { silenceLog(line); },
function (code, lastErr) {
setBusy("btnApplyEditorialActions", false);
if (code !== 0) {
setStep(5, "error", "Falhou");
taskEnd(false, "Falha ao aplicar o plano (código " + code + ").", lastErr);
silenceLog("Falha ao aplicar o plano (código " + code + ").", "err");
return;
}
setStep(5, "done", "Aplicado");
taskEnd(true, numActions + " ação(ões) aplicadas na timeline!");
silenceLog("Plano de edição aplicado com sucesso!", "ok");
}
);
}
);
}
// Abre o seletor de arquivos nativo do CEP em vez de exigir que a pessoa cole
// um caminho absoluto à mão.
function pickEditorialPlanFile() {
var input = document.getElementById("editorialPlanPath");
try {
var api = window.cep && window.cep.fs;
if (!api || typeof api.showOpenDialog !== "function") {
input.focus();
showToast("err", "Seletor de arquivos indisponível aqui — cole o caminho do JSON.");
return;
}
var result = api.showOpenDialog(false, false, "Escolha o plano de edição (JSON)", "", ["json"]);
var chosen = result && result.data && result.data.length ? result.data[0] : "";
if (chosen) input.value = toFsPath(chosen);
} catch (e) {
input.focus();
showToast("err", "Não foi possível abrir o seletor: " + e.message);
}
}
// ---- Settings / secrets storage ----
var CONFIG_DIR = path.join(os.homedir(), ".premiere-mcp");
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
function loadConfig() {
try {
if (!fs.existsSync(CONFIG_FILE)) return {};
var parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
return (parsed && typeof parsed === "object") ? parsed : {};
} catch (e) {
return {};
}
}
function saveConfig(patch) {
try {
ensureDir(CONFIG_DIR);
var next = Object.assign({}, loadConfig(), patch);
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch (e) {}
return true;
} catch (e) {
log("Error saving config: " + e.message, "err");
return false;
}
}
// ---- Configuração do Editor: personalidades de edição ----
// Fase 1: nome + texto livre descrevendo o comportamento esperado. A
// personalidade marcada como ativa vai embutida no JSON de transcrição
// (settings.editor_personality), pra eu já saber o estilo esperado quando
// você me trouxer o arquivo. Fases futuras podem trocar/complementar o texto
// livre por parâmetros estruturados obrigatórios.
function loadEditorPersonalities() {
var list = loadConfig().editorPersonalities;
return Array.isArray(list) ? list : [];
}
function loadActiveEditorPersonalityId() {
return String(loadConfig().activeEditorPersonalityId || "").trim();
}
function getActiveEditorPersonality() {
var id = loadActiveEditorPersonalityId();
if (!id) return null;
var list = loadEditorPersonalities();
for (var i = 0; i < list.length; i++) {
if (list[i].id === id) return list[i];
}
return null;
}
function setActiveEditorPersonality(id) {
saveConfig({ activeEditorPersonalityId: id });
showToast("ok", "Personalidade ativa atualizada.");
renderEditorPersonalities();
}
// Embute a personalidade de edição ativa (nome + texto livre) como
// `settings.editor_personality` dentro do JSON de transcrição recém-gerado,
// pra ir junto quando o arquivo for trazido pra análise.
function applyEditorPersonalityToTranscript(transcriptPath) {
var personality = getActiveEditorPersonality();
if (!personality) return;
try {
var transcript = JSON.parse(fs.readFileSync(transcriptPath, "utf-8"));
transcript.settings = Object.assign({}, transcript.settings, {
editor_personality: { name: personality.name, text: personality.text },
});
fs.writeFileSync(transcriptPath, JSON.stringify(transcript, null, 2), "utf-8");
silenceLog('Personalidade de edição embutida no JSON: "' + personality.name + '".', "ok");
} catch (e) {
silenceLog("Não foi possível embutir a personalidade de edição: " + e.message, "err");
}
}
var editingPersonalityId = null;
function startNewEditorPersonality() {
editingPersonalityId = null;
document.getElementById("editorPersonalityName").value = "";
document.getElementById("editorPersonalityText").value = "";
document.getElementById("editorPersonalityForm").hidden = false;
}
function editEditorPersonality(id) {
var list = loadEditorPersonalities();
var entry = list.filter(function (p) { return p.id === id; })[0];
if (!entry) return;
editingPersonalityId = id;
document.getElementById("editorPersonalityName").value = entry.name;
document.getElementById("editorPersonalityText").value = entry.text;
document.getElementById("editorPersonalityForm").hidden = false;
}
function cancelEditorPersonalityForm() {
editingPersonalityId = null;
document.getElementById("editorPersonalityForm").hidden = true;
}
function saveEditorPersonalityForm() {
var name = document.getElementById("editorPersonalityName").value.trim();
var text = document.getElementById("editorPersonalityText").value.trim();
if (!name || !text) {
modelsLog("Preencha nome e descrição da personalidade.", "err");
return;
}
var list = loadEditorPersonalities();
if (editingPersonalityId) {
list = list.map(function (p) {
return p.id === editingPersonalityId ? { id: p.id, name: name, text: text } : p;
});
} else {
var id = "personality-" + Date.now().toString(36);
list.push({ id: id, name: name, text: text });
if (!loadActiveEditorPersonalityId()) saveConfig({ activeEditorPersonalityId: id });
}
saveConfig({ editorPersonalities: list });
cancelEditorPersonalityForm();
renderEditorPersonalities();
modelsLog("Personalidade de edição salva: " + name, "ok");
}
function deleteEditorPersonality(id) {
var list = loadEditorPersonalities();
var entry = list.filter(function (p) { return p.id === id; })[0];
if (!entry) return;
if (typeof window.confirm === "function" && !window.confirm('Excluir a personalidade "' + entry.name + '"?')) return;
list = list.filter(function (p) { return p.id !== id; });
var patch = { editorPersonalities: list };
if (loadActiveEditorPersonalityId() === id) patch.activeEditorPersonalityId = "";
saveConfig(patch);
renderEditorPersonalities();
}
function renderEditorPersonalities() {
var container = document.getElementById("editorPersonalityList");
if (!container) return;
container.innerHTML = "";
var activeId = loadActiveEditorPersonalityId();
var list = loadEditorPersonalities();
if (!list.length) {
var empty = document.createElement("p");
empty.className = "field-help";
empty.textContent = "Nenhuma personalidade cadastrada ainda. Crie uma para orientar o corte por IA.";
container.appendChild(empty);
return;
}
list.forEach(function (entry) {
var isActive = entry.id === activeId;
var row = document.createElement("div");
row.className = "personality-row";
row.setAttribute("data-active", String(isActive));
var main = document.createElement("div");
main.className = "model-main";
var name = document.createElement("span");
name.className = "model-name";
name.textContent = entry.name;
main.appendChild(name);
var meta = document.createElement("div");
meta.className = "model-meta";
var snippet = document.createElement("span");
snippet.className = "model-badge";
snippet.textContent = entry.text.length > 60 ? entry.text.slice(0, 60) + "…" : entry.text;
snippet.title = entry.text;
meta.appendChild(snippet);
if (isActive) {
var badge = document.createElement("span");
badge.className = "model-badge model-badge-star";
badge.textContent = "★ Ativa";
meta.appendChild(badge);
}
main.appendChild(meta);
row.appendChild(main);
var actions = document.createElement("div");
actions.className = "model-actions";
var activeBtn = document.createElement("button");
activeBtn.type = "button";
if (isActive) {
activeBtn.className = "button button-primary model-btn-active";
activeBtn.textContent = "Ativa";
activeBtn.disabled = true;
} else {
activeBtn.className = "button button-ghost";
activeBtn.textContent = "Usar esta";
activeBtn.onclick = function () { setActiveEditorPersonality(entry.id); };
}
actions.appendChild(activeBtn);
var editBtn = document.createElement("button");
editBtn.type = "button";
editBtn.className = "link-button";
editBtn.textContent = "Editar";
editBtn.onclick = function () { editEditorPersonality(entry.id); };
actions.appendChild(editBtn);
var deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "link-button";
deleteBtn.textContent = "Excluir";
deleteBtn.onclick = function () { deleteEditorPersonality(entry.id); };
actions.appendChild(deleteBtn);
row.appendChild(actions);
container.appendChild(row);
});
}
function loadHfToken() {
return String(loadConfig().hfToken || "").trim();
}
function hfSpawnOptions() {
var token = loadHfToken();
if (!token) return undefined;
var nodeProcess = nodeRequire("process");
return { env: Object.assign({}, nodeProcess.env, { HF_TOKEN: token }) };
}
function loadModelsPath() {
return String(loadConfig().modelsPath || "").trim();
}
function effectiveModelsPath() {
return loadModelsPath() || DEFAULT_MODELS_PATH;
}
function saveModelsPath() {
var value = document.getElementById("modelsPath").value.trim();
if (!saveConfig({ modelsPath: value })) return;
modelsLog("Pasta de modelos salva: " + effectiveModelsPath());
showToast("ok", "Pasta de modelos salva.");
renderModelList();
}
function loadLanguage() {
return String(loadConfig().language || "").trim() || DEFAULT_LANGUAGE;
}
function saveLanguage() {
var value = document.getElementById("transcribeLanguage").value;
saveConfig({ language: value });
syncLanguageInfo();
showToast("ok", "Idioma da transcrição salvo.");
modelsLog("Idioma de transcrição salvo: " + value);
}
function loadDefaultModel() {
return String(loadConfig().defaultModel || "").trim() || DEFAULT_MODEL;
}
function setDefaultModel(model) {
saveConfig({ defaultModel: model });
modelsLog("Modelo padrão definido: " + model, "ok");
showToast("ok", "Modelo padrão: " + model + ".");
renderModelList();
syncTranscribeModelSelect();
}
// Mantém o seletor de modelo da aba de Transcrição em sincronia com o catálogo
// e com o modelo padrão escolhido na aba Modelos Locais.
function syncTranscribeModelSelect() {
var select = document.getElementById("silenceModel");
if (!select) return;
var current = select.value;
select.innerHTML = "";
MODEL_CATALOG.forEach(function (m) {
var opt = document.createElement("option");
opt.value = m.value;
opt.textContent = m.label;
select.appendChild(opt);
});
var wanted = MODEL_CATALOG.some(function (m) { return m.value === current; }) ? current : loadDefaultModel();
select.value = wanted;
}
// ---- Settings tab: HF token ----
function saveHfToken() {
var token = document.getElementById("hfToken").value.trim();
if (!saveConfig({ hfToken: token })) return;
log("Hugging Face token salvo");
showToast("ok", token ? "Token salvo." : "Token removido.");
setHfTokenStatus(token ? "unknown" : "unset", token ? "Salvo — clique em Validar" : "Não configurado");
}
function toggleHfTokenVisibility() {
var input = document.getElementById("hfToken");
input.type = input.type === "password" ? "text" : "password";
}
function loadGroqApiKey() {
return String(loadConfig().groqApiKey || "").trim();
}
function saveGroqApiKey() {
var key = document.getElementById("groqApiKey").value.trim();
if (!saveConfig({ groqApiKey: key })) return;
log("Chave Groq salva");
showToast("ok", key ? "API Groq salva." : "API Groq removida.");
var status = document.getElementById("groqApiKeyStatus");
if (status) status.setAttribute("data-state", key ? "unknown" : "unset");
document.getElementById("groqApiKeyStatusText").textContent = key ? "Salva" : "Não configurada";
}
function toggleGroqApiKeyVisibility() {
var input = document.getElementById("groqApiKey");
input.type = input.type === "password" ? "text" : "password";
}
function setHfTokenStatus(state, text) {
var el = document.getElementById("hfTokenStatus");
if (!el) return;
el.setAttribute("data-state", state);
document.getElementById("hfTokenStatusText").textContent = text;
}
function validateHfToken() {
var token = document.getElementById("hfToken").value.trim() || loadHfToken();
if (!token) {
setHfTokenStatus("unset", "Não configurado");
return;
}
setHfTokenStatus("checking", "Validando…");
var req = https.get(
"https://huggingface.co/api/whoami-v2",
{ headers: { Authorization: "Bearer " + token, "User-Agent": "premiere-pro-mcp-connector" } },
function (response) {
var body = "";
response.setEncoding("utf8");
response.on("data", function (chunk) { body += chunk; });
response.on("end", function () {
if (response.statusCode === 200) {
var name = "";
try { name = JSON.parse(body).name || ""; } catch (e) {}
setHfTokenStatus("valid", "Token válido" + (name ? " (" + name + ")" : ""));
} else {
setHfTokenStatus("invalid", "Token inválido (HTTP " + response.statusCode + ")");
}
});
}
);
req.on("error", function (err) {
setHfTokenStatus("invalid", "Erro ao validar: " + err.message);
});
req.setTimeout(8000, function () {
req.destroy();
setHfTokenStatus("invalid", "Tempo esgotado ao validar");
});
}
// ---- Models tab ----
// Localiza a pasta de cache HF (formato "models--Systran--faster-whisper-<model>")
// que contém um model.bin de fato baixado para o modelo pedido.
function findModelFolder(model) {
try {
var modelsPath = effectiveModelsPath();
if (!fs.existsSync(modelsPath)) return null;
var entries = fs.readdirSync(modelsPath);
for (var i = 0; i < entries.length; i++) {
// ex: "models--Systran--faster-whisper-small" for model "small"
if (entries[i].indexOf("models--") !== 0) continue;
if (entries[i].toLowerCase().indexOf(("faster-whisper-" + model).toLowerCase()) === -1) continue;
var fullDir = path.join(modelsPath, entries[i]);
var snapshotsDir = path.join(fullDir, "snapshots");
if (!fs.existsSync(snapshotsDir)) continue;
var snaps = fs.readdirSync(snapshotsDir);
for (var j = 0; j < snaps.length; j++) {
if (fs.existsSync(path.join(snapshotsDir, snaps[j], "model.bin"))) return fullDir;
}
}
return null;
} catch (e) {
return null;
}
}
function isModelDownloaded(model) {
return !!findModelFolder(model);
}
function deleteModel(model) {
var folder = findModelFolder(model);
if (!folder) return;
var confirmMsg = "Excluir o modelo \"" + model + "\" baixado (" + folder + ")? Isso libera espaço em disco; será baixado de novo se você precisar dele depois.";
if (typeof window.confirm === "function" && !window.confirm(confirmMsg)) return;
try {
fs.rmSync(folder, { recursive: true, force: true });
modelsLog("Modelo " + model + " excluído.", "ok");
showToast("ok", "Modelo " + model + " excluído.");
} catch (e) {
modelsLog("Falha ao excluir " + model + ": " + e.message, "err");
}
renderModelList();
}
function modelsLog(msg, cls) {
var el = document.getElementById("modelsLog");
if (!el) return;
var entry = document.createElement("div");
entry.className = "log-entry " + (cls || "");
var ts = new Date().toLocaleTimeString();
entry.textContent = "[" + ts + "] " + msg;
el.appendChild(entry);
el.scrollTop = el.scrollHeight;
while (el.children.length > 100) el.removeChild(el.firstChild);
}
function renderLanguageOptions() {
var select = document.getElementById("transcribeLanguage");
if (!select) return;
select.innerHTML = "";
LANGUAGE_CATALOG.forEach(function (lang) {
var opt = document.createElement("option");
opt.value = lang.value;
opt.textContent = lang.label;
select.appendChild(opt);
});
}
function renderModelList() {
var list = document.getElementById("modelList");
if (!list) return;
list.innerHTML = "";
var defaultModel = loadDefaultModel();
MODEL_CATALOG.forEach(function (entry) {
var model = entry.value;
var folder = findModelFolder(model);
var downloaded = !!folder;
var isDefault = model === defaultModel;
var row = document.createElement("div");
row.className = "model-row";
row.id = "modelRow-" + model;
var top = document.createElement("div");
top.className = "model-row-top";
var main = document.createElement("div");
main.className = "model-main";
var name = document.createElement("span");
name.className = "model-name";
name.textContent = entry.label;
main.appendChild(name);
var meta = document.createElement("div");
meta.className = "model-meta";
var sizeBadge = document.createElement("span");
sizeBadge.className = "model-badge model-badge-size";
sizeBadge.textContent = "≈ " + formatMB(downloaded ? folderSizeMB(folder) : entry.approxMB) + (downloaded ? "" : " (aprox.)");
meta.appendChild(sizeBadge);
var stateBadge = document.createElement("span");
stateBadge.className = "model-badge" + (downloaded ? " model-badge-ready" : "");
stateBadge.textContent = downloaded ? "Baixado" : "Não baixado";
meta.appendChild(stateBadge);
if (isDefault) {
var defaultBadge = document.createElement("span");
defaultBadge.className = "model-badge model-badge-star";
defaultBadge.textContent = "★ Padrão";
meta.appendChild(defaultBadge);
}
main.appendChild(meta);
top.appendChild(main);
var actions = document.createElement("div");
actions.className = "model-actions";
var primaryBtn = document.createElement("button");
primaryBtn.type = "button";
if (isDefault) {
primaryBtn.className = "button button-primary model-btn-active";
primaryBtn.textContent = "Ativo";
primaryBtn.disabled = true;
} else if (downloaded) {
primaryBtn.className = "button button-ghost";
primaryBtn.textContent = "Usar como padrão";
primaryBtn.onclick = function () { setDefaultModel(model); };
} else {
primaryBtn.className = "button button-primary";
primaryBtn.textContent = "Baixar";
primaryBtn.onclick = function () { downloadModel(model, primaryBtn, entry.approxMB); };
}
actions.appendChild(primaryBtn);
if (downloaded && !isDefault) {
var deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "link-button";
deleteBtn.textContent = "Excluir";
deleteBtn.onclick = function () { deleteModel(model); };
actions.appendChild(deleteBtn);
}
top.appendChild(actions);
row.appendChild(top);
// Barra de progresso do download, na própria linha do modelo.
var progress = document.createElement("div");
progress.className = "model-progress";
progress.id = "modelProgress-" + model;
progress.hidden = true;
progress.innerHTML =
'<div class="progress-track" data-mode="indeterminate"><div class="progress-fill" style="width:0%"></div></div>' +
'<span class="model-progress-label"></span>';
row.appendChild(progress);
list.appendChild(row);
});
}
function downloadModel(model, btn, approxMB) {
btn.disabled = true;
btn.classList.add("is-busy");
modelsLog("Baixando modelo " + model + "… isso pode levar alguns minutos.");
var box = document.getElementById("modelProgress-" + model);
var track = box ? box.querySelector(".progress-track") : null;
var fill = box ? box.querySelector(".progress-fill") : null;
var label = box ? box.querySelector(".model-progress-label") : null;
if (box) box.hidden = false;
function mirror(info) {
if (!box) return;
if (typeof info.pct === "number" && isFinite(info.pct)) {
track.setAttribute("data-mode", "determinate");
fill.style.width = Math.max(0, Math.min(100, info.pct)) + "%";
} else {
track.setAttribute("data-mode", "indeterminate");
}
label.textContent = info.detail ? info.stage + " · " + info.detail : info.stage || "";
}
taskStart("Baixando o modelo " + model, "Preparando o download", mirror);
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_DOWNLOAD_SCRIPT,
"--model", model,
"--models-path", effectiveModelsPath(),
"--approx-mb", String(approxMB || 0),
],
function (line) { modelsLog(line); },
function (code, lastErr) {
btn.classList.remove("is-busy");
if (code !== 0) {
if (box) box.hidden = true;
modelsLog("Falha ao baixar " + model + " (código " + code + ").", "err");
taskEnd(false, "Falha ao baixar o modelo " + model + " (código " + code + ").", lastErr);
btn.disabled = false;
return;
}
modelsLog("Modelo " + model + " baixado com sucesso.", "ok");
taskEnd(true, "Modelo " + model + " pronto para uso.");
renderModelList();
},
hfSpawnOptions()
);
}
// ---- Init ----
(function init() {
// Set the default temp dir in the input field
document.getElementById("tempDir").value = tempDir;
// Restore saved temp dir
try {
var saved = localStorage.getItem("mcp_bridge_temp_dir");
if (saved) {
tempDir = saved;
document.getElementById("tempDir").value = tempDir;
}
} catch (e) {}
// Restore saved HF token (masked) and show its status
var savedToken = loadHfToken();
if (savedToken) {
document.getElementById("hfToken").value = savedToken;
setHfTokenStatus("unknown", "Salvo — clique em Validar para confirmar");
} else {
setHfTokenStatus("unset", "Não configurado");
}
var savedGroqApiKey = loadGroqApiKey();
document.getElementById("groqApiKey").value = savedGroqApiKey;
if (savedGroqApiKey) {
document.getElementById("groqApiKeyStatus").setAttribute("data-state", "unknown");
document.getElementById("groqApiKeyStatusText").textContent = "Salva";
}
document.getElementById("modelsPath").value = effectiveModelsPath();
renderLanguageOptions();
document.getElementById("transcribeLanguage").value = loadLanguage();
renderModelList();
syncTranscribeModelSelect();
syncLanguageInfo();
renderEditorPersonalities();
// Passo 1 é o único liberado até o vídeo ser detectado.
setStep(1, "ready", "Comece aqui");
lockStepsFrom(2);
log("MCP for Adobe Premiere Pro CEP connector loaded");
setStatus("waiting", "Aguardando — a ponte está iniciando");
// Always auto-start. The headless instance (StartOn ApplicationActivate) has no
// one to click Start, and macOS periodically purges the temp dir — so create it
// rather than gating auto-start on its existence.
ensureDir(tempDir);
startBridgeHeartbeat();
log("Auto-starting bridge...");
setTimeout(startBridge, 500);
if (!restoreScheduledUpdateStatus()) setTimeout(checkForUpdates, 1200);
})();