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

 22 files changed, 658 insertions(+), 568 deletions(-)

Arquivos:
  - AGENTS.md
  - admin/DEV-NOTES.md
  - admin/OpenCut.command
  - admin/commit.command
  - admin/deploy.command
  - admin/update.command
  - code/cep-plugin/index.html
  - code/cep-plugin/main.js
  - code/cep-plugin/styles.css
  - code/engine/ARQUITETURA.md
  - code/engine/arquitetura/README.md
  - code/engine/gerar_relatorio_timeline.py
  - code/engine/integracoes/apple_speech/apple_speech_transcriber.swift
  - code/engine/integracoes/apple_speech/provider_de_transcricao_apple.py
  - code/engine/integracoes/midia/__init__.py
  - code/engine/integracoes/whisper/provider_de_transcricao_local.py
  - code/engine/scanner/__init__.py
  - code/engine/scanner/coordenacao/__init__.py
  - code/engine/scanner/descoberta/__init__.py
  - code/engine/scanner/modelos.py
  - code/engine/scanner/transcricao_da_timeline.py
  - code/src/tools/discovery.ts
  - :memory:.ses
  - admin/Jhonny.command
  - admin/inativos/OpenCut.command
  - admin/inativos/update.command
  - code/docs/glossario-analise-emocional.md
  - code/docs/levantamento-apple-vision-e-apple-intelligence.md
  - code/docs/levantamento-ferramentas-analise-visual-local.md
  - code/engine/arquitetura/biblioteca-inteligente-de-videos.md
  - code/engine/executar_scanner.py
  - code/engine/integracoes/huggingface/
  - code/engine/integracoes/midia/extracao_de_metadados.py
  - code/engine/integracoes/visual/
  - code/engine/requirements-visual.txt
  - code/engine/scanner/configuracao_visual.py
  - code/engine/scanner/descoberta/descoberta_de_arquivos.py
  - code/engine/scanner/metadados.py
  - code/engine/scanner/relatorio_visual.py
  - code/engine/scanner/retakes/
  - code/engine/scanner/visual.py
  - code/engine/testes/test_analise_visual_local.py
  - code/engine/testes/test_arquivos_e_metadados.py
  - code/engine/testes/test_provider_de_transcricao_apple.py
  - code/relatorios/analise-brools/
  - code/relatorios/analise-visual/
  - code/relatorios/audio/arquivos/
  - code/relatorios/transcricao-timeline.md
2026-09-08 16:13:27 -04:00

2253 lines
90 KiB
JavaScript
Executable File

/* 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
// fixo no topo com etapa atual, barra e tempo decorrido.
var taskState = { active: false, startedAt: 0, timer: null, clearTimer: null, mirror: null };
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 taskStart(title, stage, mirror) {
if (taskState.clearTimer) { clearTimeout(taskState.clearTimer); taskState.clearTimer = null; }
taskState.active = true;
taskState.startedAt = Date.now();
taskState.mirror = mirror || null;
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%";
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) {
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%" : "";
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 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: "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(); }
}
// ---- 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 scannerProfileFromScreen() {
var enabled = Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]:checked")).map(function (item) { return item.getAttribute("data-scanner-resource"); });
function include(source, extras) { if (enabled.indexOf(source) >= 0) extras.forEach(function (item) { if (enabled.indexOf(item) < 0) enabled.push(item); }); }
include("faces", ["qualidade_facial"]); include("pose", ["maos"]); include("ocr", ["codigos"]); include("estetica", ["horizonte", "retangulos", "contornos"]);
var modelo = document.getElementById("scannerTranscribeModel").value;
var opcao = document.getElementById("scannerTranscribeModel").selectedOptions[0];
return { versao: 1, intervalo_em_segundos: Number(document.getElementById("scannerInterval").value), faixas_de_video: scannerSelectedTracks("video"), faixas_de_audio: scannerSelectedTracks("audio"), recursos_vision: SCANNER_ALL_RESOURCES.filter(function (item) { return enabled.indexOf(item) >= 0; }), detectar_cenas: document.getElementById("scannerScenes").checked, analisar_continuidade: document.getElementById("scannerContinuity").checked, preparar_reenquadramento: document.getElementById("scannerReframe").checked, interpretar_cena: document.getElementById("scannerInterpret").checked, transcricao: { modelo: modelo, caminho_modelo: opcao && opcao.dataset.path || "", idioma: document.getElementById("scannerTranscribeLanguage").value, diarizacao: document.getElementById("scannerDiarization").checked, usar_hugging_face: !!loadHfToken() } };
}
function saveScannerProfile() {
try { var profile = scannerProfileFromScreen(); localStorage.setItem(SCANNER_PROFILE_KEY, JSON.stringify(profile)); document.getElementById("scannerProfileStatus").textContent = "Perfil salvo: " + profile.intervalo_em_segundos + " s por frame; " + profile.faixas_de_video.length + " faixa(s) de vídeo."; showToast("ok", "Perfil do Scanner salvo."); } catch (error) { showToast("err", error.message || String(error)); }
}
function loadScannerProfile() {
var saved; try { saved = JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY) || "null"); } catch (_) { saved = null; }
if (!saved) return;
document.getElementById("scannerInterval").value = String(saved.intervalo_em_segundos || 1);
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");showToast("err",error.message);});
child.on("close",function(code){setBusy("btnScannerProcess",false);if(code!==0||!fs.existsSync(scannerReportPath)){taskEnd(false,"Scanner falhou");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");
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 = "";
// 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) { 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, 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) {
setBusy("btnTranscribe", false);
if (code !== 0) {
setStep(2, "error", "Falhou");
taskEnd(false, "A transcrição falhou (código " + code + "). Veja os detalhes técnicos.");
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.");
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 + ").");
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 + ").");
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) {
setBusy("btnApplyEditorialActions", false);
if (code !== 0) {
setStep(5, "error", "Falhou");
taskEnd(false, "Falha ao aplicar o plano (código " + code + ").");
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) {
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 + ").");
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);
})();