/* 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);
}
// ---- Tabs ----
function switchTab(name) {
var tabs = [
{ key: "bridge", panel: "tabBridge", btn: "tabBtnBridge" },
{ key: "silence", panel: "tabSilence", btn: "tabBtnSilence" },
{ key: "settings", panel: "tabSettings", btn: "tabBtnSettings" },
{ key: "models", panel: "tabModels", btn: "tabBtnModels" },
];
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();
if (name === "settings") renderEditorPersonalities();
}
// ---- 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 = "Premiere Pro link is active";
else if (state === "waiting") detail.textContent = "Ready for an AI assistant connection";
else if (state === "error") detail.textContent = "Bridge needs attention";
else detail.textContent = "Waiting for Premiere Pro";
}
}
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 --enable-nodejs, 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);
// 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 SILENCE_CUTTER_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/silence_cutter.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 SILENCE_APPLY_SCRIPT_FALLBACK = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/scripts/apply-silence-cuts.mjs";
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,
planPath: null,
plan: 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() {
document.getElementById("silenceClipInfo").textContent = "Detectando…";
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) {
var result;
try {
result = JSON.parse(raw);
} catch (e) {
result = { ok: false, error: "Resposta inesperada do Premiere." };
}
var info = document.getElementById("silenceClipInfo");
if (!result.ok || !result.mediaPath) {
info.textContent = "Erro: " + (result.error || "caminho de mídia vazio.");
document.getElementById("btnTranscribe").disabled = true;
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);
info.textContent =
'Sequência "' + result.sequenceName + '" — clipe: ' + result.mediaPath;
document.getElementById("btnTranscribe").disabled = false;
document.getElementById("btnDiarize").disabled = true;
document.getElementById("silenceDiarizeInfo").textContent = "";
document.getElementById("btnVoiceFeatures").disabled = true;
document.getElementById("silenceVoiceFeaturesInfo").textContent = "";
silenceLog('Clipe detectado: ' + result.mediaPath);
renderCachedTranscripts();
}
);
}
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 transcrição salva de " + new Date(entry.savedAt).toLocaleString() + ".";
document.getElementById("btnBuildPlan").disabled = false;
document.getElementById("btnDiarize").disabled = false;
document.getElementById("btnVoiceFeatures").disabled = false;
silenceLog("Transcrição salva selecionada — pronto para gerar o plano de cortes.", "ok");
}
// 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";
}
function runStreamed(bin, args, onLine, onDone, options) {
var childProcess = nodeRequire("child_process");
var child = childProcess.spawn(bin, args, options || {});
var buffer = "";
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()) onLine(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()) onLine(buffer);
onDone(code, null);
});
}
function silenceTranscribe() {
if (!silenceState.mediaPath) return;
document.getElementById("silenceCacheInfo").textContent = "";
var model = document.getElementById("silenceModel").value;
document.getElementById("btnTranscribe").disabled = true;
ensureDir(silenceState.outputDir);
var out = path.join(silenceState.outputDir, "transcricao.json");
silenceLog("Transcrevendo com modelo " + model + "… isso pode levar alguns minutos.");
var lastLine = "";
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_TRANSCRIBE_SCRIPT,
"--video", silenceState.mediaPath,
"--out", out,
"--model", model,
"--language", loadLanguage(),
"--models-path", effectiveModelsPath(),
],
function (line) { lastLine = line; silenceLog(line); },
function (code) {
document.getElementById("btnTranscribe").disabled = false;
if (code !== 0) {
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) {
silenceLog("Transcrição terminou mas resposta é inválida: " + lastLine, "err");
return;
}
silenceState.transcriptPath = out;
silenceState.transcribeModel = model;
silenceLog("Transcrição concluída: " + parsed.segments + " segmentos, " + Math.round(parsed.duration) + "s.", "ok");
applyEditorPersonalityToTranscript(out);
silenceState.cachedTranscriptPath = saveTranscriptToCache(silenceState.mediaPath, out, model);
if (silenceState.cachedTranscriptPath) {
silenceLog("Transcrição salva na pasta central para reaproveitar depois.", "ok");
}
document.getElementById("btnBuildPlan").disabled = false;
document.getElementById("btnDiarize").disabled = false;
document.getElementById("btnVoiceFeatures").disabled = false;
if (document.getElementById("silenceDiarize").checked) {
silenceDiarizeSpeakers();
}
},
hfSpawnOptions()
);
}
function silenceDiarizeSpeakers() {
if (!silenceState.transcriptPath) return;
if (!silenceState.mediaPath) {
silenceLog("Detecte o clipe/sequência primeiro (seção 1) para saber qual vídeo diarizar.", "err");
return;
}
if (!loadHfToken()) {
silenceLog("Diarização pulada: configure um HF_TOKEN na aba Configurações.", "err");
return;
}
var btn = document.getElementById("btnDiarize");
var info = document.getElementById("silenceDiarizeInfo");
if (btn) btn.disabled = true;
if (info) info.textContent = "";
silenceLog("Detectando locutores…");
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_DIARIZE_SCRIPT,
"--video", silenceState.mediaPath,
"--transcript", silenceState.transcriptPath,
"--out", silenceState.transcriptPath,
"--models-path", effectiveModelsPath(),
],
function (line) { silenceLog(line); },
function (code, lastErr) {
if (btn) btn.disabled = false;
if (code !== 0) {
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) detectado(s): " + parsed.speakers.join(", ") : "Locutores detectados.";
silenceLog(msg, "ok");
if (info) info.textContent = 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 (seção 1) para saber qual vídeo analisar.", "err");
return;
}
var btn = document.getElementById("btnVoiceFeatures");
var info = document.getElementById("silenceVoiceFeaturesInfo");
if (btn) btn.disabled = true;
if (info) info.textContent = "";
silenceLog("Calculando pitch/energia/velocidade de fala/pausas…");
runStreamed(
SILENCE_PYTHON_BIN,
[
VOICE_FEATURES_SCRIPT,
"--video", silenceState.mediaPath,
"--transcript", silenceState.transcriptPath,
"--out", silenceState.transcriptPath,
],
function (line) { silenceLog(line); },
function (code, lastErr) {
if (btn) btn.disabled = false;
if (code !== 0) {
silenceLog("Falha ao calcular métricas de voz (código " + code + (lastErr ? ": " + lastErr : "") + ").", "err");
return;
}
var msg = "Métricas de voz calculadas (pitch, energia, velocidade, pausas) para cada trecho.";
silenceLog(msg, "ok");
if (info) info.textContent = msg;
syncTranscriptToCache();
}
);
}
function silenceBuildPlan() {
if (!silenceState.transcriptPath) return;
document.getElementById("btnBuildPlan").disabled = true;
var out = path.join(silenceState.outputDir, "plano-cortes.json");
silenceLog("Gerando plano de cortes…");
var lastLine = "";
runStreamed(
SILENCE_PYTHON_BIN,
[SILENCE_CUTTER_SCRIPT, "--transcript", silenceState.transcriptPath, "--out", out, "--min-silence", "0.8", "--margin", "0.3"],
function (line) { lastLine = line; silenceLog(line); },
function (code) {
document.getElementById("btnBuildPlan").disabled = false;
if (code !== 0) {
silenceLog("Falha ao gerar o plano (código " + code + ").", "err");
return;
}
var parsed = null;
try { parsed = JSON.parse(lastLine); } catch (e) {}
if (!parsed || parsed.ok === false) {
silenceLog("Plano gerado mas resposta é inválida: " + lastLine, "err");
return;
}
silenceState.planPath = out;
silenceState.plan = parsed;
var info = document.getElementById("silencePlanInfo");
info.textContent =
parsed.num_cuts + " cortes de silêncio · remove " + parsed.total_cut_seconds.toFixed(1) +
"s · duração final ≈ " + parsed.resulting_duration_seconds.toFixed(1) + "s";
silenceLog("Plano pronto: " + info.textContent, "ok");
document.getElementById("btnApplyCuts").disabled = 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 resolveApplyScript() {
return resolveScript("apply-silence-cuts.mjs", SILENCE_APPLY_SCRIPT_FALLBACK);
}
function resolveEditorialActionsScript() {
return resolveScript("apply-editorial-actions.mjs", EDITORIAL_ACTIONS_APPLY_SCRIPT_FALLBACK);
}
function silenceApplyCuts() {
if (!silenceState.planPath || !silenceState.plan) return;
var summary =
silenceState.plan.num_cuts + " cortes, removendo " + silenceState.plan.total_cut_seconds.toFixed(1) +
"s (duração final ≈ " + silenceState.plan.resulting_duration_seconds.toFixed(1) + "s).\n\n" +
"Um backup da sequência atual será criado antes de aplicar. Confirmar?";
if (typeof window.confirm === "function" && !window.confirm(summary)) return;
document.getElementById("btnApplyCuts").disabled = true;
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 = resolveApplyScript();
if (!applyScript) {
document.getElementById("btnApplyCuts").disabled = false;
silenceLog(
"Script de cortes não encontrado. Rode scripts/install-cep.sh para gravar o repoRoot em " +
CONFIG_FILE + ".",
"err"
);
return;
}
var nodeBin = resolveNodeBin();
silenceLog("Aplicando cortes na timeline…");
runStreamed(
nodeBin,
[applyScript, "--plan", silenceState.planPath, "--sequence", silenceState.sequenceName],
function (line) { silenceLog(line); },
function (code) {
document.getElementById("btnApplyCuts").disabled = false;
if (code !== 0) {
silenceLog("Falha ao aplicar os cortes (código " + code + ").", "err");
return;
}
silenceLog("Cortes aplicados com sucesso!", "ok");
}
);
}
);
}
// ---- Importar plano de edição (skill selecao-trechos: cut/zoom/text/marker) ----
function applyEditorialActions() {
if (!silenceState.sequenceName) {
silenceLog("Detecte o clipe/sequência primeiro (seção 1).", "err");
return;
}
var planPath = document.getElementById("editorialPlanPath").value.trim();
if (!planPath) {
silenceLog("Informe o caminho do arquivo do plano (JSON de actions).", "err");
return;
}
if (!fs.existsSync(planPath)) {
silenceLog("Arquivo não encontrado: " + planPath, "err");
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");
return;
}
var numActions = Array.isArray(plan.actions) ? plan.actions.length : 0;
if (!numActions) {
silenceLog("O plano não tem nenhuma action.", "err");
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;
document.getElementById("btnApplyEditorialActions").disabled = true;
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) {
document.getElementById("btnApplyEditorialActions").disabled = false;
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) {
document.getElementById("btnApplyEditorialActions").disabled = false;
if (code !== 0) {
silenceLog("Falha ao aplicar o plano (código " + code + ").", "err");
return;
}
silenceLog("Plano de edição aplicado com sucesso!", "ok");
}
);
}
);
}
// ---- 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 });
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.";
container.appendChild(empty);
return;
}
list.forEach(function (entry) {
var isActive = entry.id === activeId;
var row = document.createElement("div");
row.className = "model-row";
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";
var text = entry.text.length > 60 ? entry.text.slice(0, 60) + "…" : entry.text;
snippet.textContent = 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";
activeBtn.textContent = "Usar esta";
activeBtn.onclick = function () { setActiveEditorPersonality(entry.id); };
}
actions.appendChild(activeBtn);
var editBtn = document.createElement("button");
editBtn.type = "button";
editBtn.className = "save-link";
editBtn.textContent = "Editar";
editBtn.onclick = function () { editEditorPersonality(entry.id); };
actions.appendChild(editBtn);
var deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "save-link";
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());
renderModelList();
}
function loadLanguage() {
return String(loadConfig().language || "").trim() || DEFAULT_LANGUAGE;
}
function saveLanguage() {
var value = document.getElementById("transcribeLanguage").value;
saveConfig({ language: value });
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");
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");
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 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-")
// 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");
} 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";
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 engineBadge = document.createElement("span");
engineBadge.className = "model-badge";
engineBadge.textContent = "faster-whisper · multilíngue";
meta.appendChild(engineBadge);
if (isDefault) {
var defaultBadge = document.createElement("span");
defaultBadge.className = "model-badge model-badge-star";
defaultBadge.textContent = "★ Padrão";
meta.appendChild(defaultBadge);
}
main.appendChild(meta);
row.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";
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); };
}
actions.appendChild(primaryBtn);
if (downloaded && !isDefault) {
var deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "save-link";
deleteBtn.textContent = "Excluir";
deleteBtn.onclick = function () { deleteModel(model); };
actions.appendChild(deleteBtn);
}
row.appendChild(actions);
list.appendChild(row);
});
}
function downloadModel(model, btn) {
btn.disabled = true;
modelsLog("Baixando modelo " + model + "… isso pode levar alguns minutos.");
runStreamed(
SILENCE_PYTHON_BIN,
[SILENCE_DOWNLOAD_SCRIPT, "--model", model, "--models-path", effectiveModelsPath()],
function (line) { modelsLog(line); },
function (code) {
if (code !== 0) {
modelsLog("Falha ao baixar " + model + " (código " + code + ").", "err");
btn.disabled = false;
return;
}
modelsLog("Modelo " + model + " baixado com sucesso.", "ok");
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");
}
document.getElementById("modelsPath").value = effectiveModelsPath();
renderLanguageOptions();
document.getElementById("transcribeLanguage").value = loadLanguage();
renderModelList();
syncTranscribeModelSelect();
renderEditorPersonalities();
log("MCP for Adobe Premiere Pro CEP connector loaded");
setStatus("waiting", "Ready — click Start Bridge");
// 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);
})();