/* MCP for Adobe Premiere Pro - CEP Plugin Main Script
* Polls a temp directory for command files (.jsx), executes them
* in Premiere Pro's ExtendScript engine, and writes results back. */
var cs = new CSInterface();
var bridgeRunning = false;
var pollInterval = null;
var commandCount = 0;
var tempDir = "";
var POLL_MS = 200;
var HEARTBEAT_MS = 1000;
var heartbeatInterval = null;
// ---- Logging ----
function log(msg, cls) {
var el = document.getElementById("log");
var entry = document.createElement("div");
entry.className = "log-entry " + (cls || "");
var ts = new Date().toLocaleTimeString();
entry.textContent = "[" + ts + "] " + msg;
el.appendChild(entry);
el.scrollTop = el.scrollHeight;
// Keep max 100 entries
while (el.children.length > 100) el.removeChild(el.firstChild);
}
// ---- Painel de progresso (task dock) ----
// Antes, tudo que rodava só aparecia como linhas no log lá embaixo e a pessoa
// não sabia o que estava acontecendo. Agora todo processo longo abre um card
// reposicionado na aba de origem com etapa atual, barra e tempo decorrido.
var taskState = { active: false, startedAt: 0, timer: null, clearTimer: null, mirror: null, cancel: null, canceled: false, originTab: "silence", originPanelId: "tabSilence", errorText: "" };
var TIPOS_VIDEO_CHAVE = "schedule_light_tipos_video";
var tipoVideoAtualId = null;
var tiposVideoPadrao = [
{ id: "midia-social", nome: "Conteúdo para mídia social", duracao_maxima_segundos: 60, descricao: "Vídeo curto para interromper a rolagem, comunicar uma ideia e estimular uma ação.", canais: "Instagram Reels, TikTok, Shorts", proporcao: "9:16", ritmo: "agil", emocao: "Curiosidade e energia", storytelling: true, estrutura: "Gancho nos primeiros 2 segundos → desenvolvimento em blocos curtos → CTA direto.", cortes: "Cortes rápidos, remover pausas e repetições; preservar frases de impacto. Usar jump cuts com moderação.", broll: "Inserir imagens de apoio a cada mudança de ideia, preferindo planos que reforcem a fala.", texto: "Legendas sempre; destacar palavras-chave na tela e usar título curto. CTA visual no final.", som: "Voz sempre inteligível e à frente da trilha; música energética em volume baixo.", diretriz: "Priorize retenção e clareza. Cada corte deve manter o ritmo e reforçar uma única ideia por vez." },
{ id: "depoimento", nome: "Depoimento de cliente", duracao_maxima_segundos: 120, descricao: "Relato autêntico que gera identificação, confiança e prova social.", canais: "Site, redes sociais, apresentação comercial", proporcao: "adaptavel", ritmo: "equilibrado", emocao: "Empatia e confiança", storytelling: true, estrutura: "Problema vivido → experiência com a solução → transformação → recomendação.", cortes: "Cortar hesitações e desvios sem deixar a fala artificial; manter reações, pausas emocionais e frases espontâneas.", broll: "Cobrir cortes com cenas do cliente usando o produto, ambiente real ou detalhes que comprovem o relato.", texto: "Nome e contexto do depoente; destacar a transformação em frases curtas; legendas completas.", som: "Voz natural, limpa e próxima; trilha emocional discreta, crescendo apenas na transformação.", diretriz: "Proteja a autenticidade do depoimento. A emoção deve vir da fala e das evidências, não de efeitos excessivos." },
{ id: "entrevista", nome: "Entrevista", duracao_maxima_segundos: 600, descricao: "Conversa estruturada para extrair conhecimento, opinião ou história de uma pessoa.", canais: "YouTube, podcast em vídeo, cortes sociais", proporcao: "16:9", ritmo: "equilibrado", emocao: "Interesse e credibilidade", storytelling: true, estrutura: "Apresentação → perguntas essenciais → aprofundamento → síntese e encerramento.", cortes: "Remover redundâncias e silêncios longos; preservar respostas completas, escuta, reação e contexto da pergunta.", broll: "Usar planos de reação, detalhes do ambiente e imagens relacionadas ao tema para quebrar a monotonia.", texto: "Identificação do entrevistado, perguntas resumidas quando necessário, capítulos ou palavras-chave.", som: "Diálogo consistente entre os canais; reduzir ruído e nivelar vozes sem tirar naturalidade.", diretriz: "Mantenha a linha de raciocínio da conversa. Não cortar uma resposta de modo que altere o sentido ou a intenção do entrevistado." },
{ id: "institucional", nome: "Vídeo institucional", duracao_maxima_segundos: 180, descricao: "Apresentação da empresa, marca, cultura ou solução com percepção de valor.", canais: "Site, eventos, apresentações, LinkedIn", proporcao: "16:9", ritmo: "progressivo", emocao: "Segurança e inspiração", storytelling: true, estrutura: "Contexto ou propósito → pessoas e operação → diferenciais → prova → convite para conhecer.", cortes: "Ritmo elegante; evitar excesso de cortes. Alternar entrevistas, planos de ambiente e detalhes de operação.", broll: "B-roll planejado para mostrar pessoas, processos, produto e escala; toda imagem deve provar uma mensagem.", texto: "Títulos limpos, dados essenciais e assinatura da marca; evitar excesso de texto na tela.", som: "Trilha cinematográfica ou institucional discreta; voz firme, clara e com pausas bem preservadas.", diretriz: "Construa confiança e percepção de valor. Use consistência visual e sonora; efeitos só quando ajudarem a mensagem da marca." },
{ id: "educativo-storytelling", nome: "Educativo com storytelling", duracao_maxima_segundos: 240, descricao: "Explicação didática que ensina uma ideia por meio de uma história, caso ou problema concreto.", canais: "YouTube, curso, LinkedIn, redes sociais", proporcao: "adaptavel", ritmo: "progressivo", emocao: "Curiosidade e clareza", storytelling: true, estrutura: "Problema ou pergunta → história/exemplo → explicação → aplicação prática → resumo.", cortes: "Cortar repetições, mas manter a sequência pedagógica; inserir pausas após conceitos importantes.", broll: "Usar diagramas, tela, exemplos e imagens que tornem o conceito visível; sincronizar apoio com a explicação.", texto: "Título do conceito, palavras-chave e resumo final; legendas e marcadores de etapas.", som: "Voz em primeiro plano; trilha quase imperceptível; pequenos efeitos apenas para sinalizar transições.", diretriz: "Ensine uma coisa por vez. O espectador deve entender o conceito sem depender apenas de texto ou de efeitos visuais." }
];
function tipoVideoCompletar(tipo) {
var configuracoes = {
"midia-social": { categoria: "Comercial", objetivo: "Gerar retenção e ação", objetivo_secundario: "Comunicar uma ideia rapidamente", publico: "Público das redes sociais", mensagem: "Uma ideia clara por vídeo.", tipo_narrativa: "Problema → solução → benefício", foco_narrativo: "Gancho e CTA", intensidade: 4, tom: "Positivo", construcao_emocao: "Ritmo ágil, cortes de impacto, legendas e trilha energética.", duracao_minima: 15, prioridades: ["Retenção", "Clareza", "Mensagem", "Ritmo", "Estética"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: false, usa_zoom: true, usa_musica: true, intensidade_musica: 25, prioridade_voz: "Muito alta", restricoes: "Evitar excesso de informação e cortes sem função.", qualidade: "A abertura prende atenção e a mensagem é compreendida sem áudio." },
"depoimento": { categoria: "Depoimento", objetivo: "Gerar credibilidade e confiança", objetivo_secundario: "Criar identificação emocional", publico: "Clientes em potencial", mensagem: "A experiência real comprova o valor da solução.", tipo_narrativa: "História pessoal", foco_narrativo: "Problema → experiência → transformação → recomendação", intensidade: 3, tom: "Íntimo", construcao_emocao: "Preservar pausas naturais, reações e imagens reais; usar trilha discreta.", duracao_minima: 30, prioridades: ["Autenticidade", "Clareza", "Conexão emocional", "Ritmo", "Estética"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: false, usa_zoom: false, usa_musica: true, intensidade_musica: 20, prioridade_voz: "Muito alta", restricoes: "Não alterar o sentido das frases nem deixar o depoimento artificial.", qualidade: "A pessoa parece verdadeira e o resultado do relato fica evidente." },
"entrevista": { categoria: "Entrevista", objetivo: "Transmitir conhecimento e autoridade", objetivo_secundario: "Organizar a conversa por temas", publico: "Pessoas interessadas no assunto", mensagem: "O conteúdo da conversa é compreendido com contexto.", tipo_narrativa: "Pergunta e resposta", foco_narrativo: "Pergunta → resposta → aprofundamento", intensidade: 2, tom: "Informativo", construcao_emocao: "Reações, escuta e pausas que preservem a naturalidade da conversa.", duracao_minima: 60, prioridades: ["Clareza", "Conteúdo", "Contexto", "Ritmo", "Estética"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: false, usa_zoom: true, usa_musica: false, intensidade_musica: 10, prioridade_voz: "Muito alta", restricoes: "Não cortar frases de modo que prejudique o raciocínio do entrevistado.", qualidade: "As respostas mantêm contexto, lógica e informação relevante." },
"institucional": { categoria: "Institucional", objetivo: "Apresentar valor e confiança na marca", objetivo_secundario: "Mostrar pessoas, processo e diferenciais", publico: "Clientes, parceiros e equipe", mensagem: "A marca entrega valor de forma consistente.", tipo_narrativa: "Jornada / transformação", foco_narrativo: "Propósito → pessoas → operação → prova", intensidade: 4, tom: "Inspirador", construcao_emocao: "Trilha progressiva, imagens de pessoas e planos de operação.", duracao_minima: 45, prioridades: ["Confiança", "Mensagem", "Narrativa", "Estética", "Ritmo"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: true, usa_zoom: false, usa_musica: true, intensidade_musica: 35, prioridade_voz: "Alta", restricoes: "Evitar efeitos decorativos que não reforcem a mensagem da marca.", qualidade: "O vídeo transmite profissionalismo, clareza e percepção de valor." },
"educativo-storytelling": { categoria: "Educativo", objetivo: "Ensinar um assunto com clareza", objetivo_secundario: "Aumentar retenção e aplicação prática", publico: "Pessoas que precisam aprender o assunto", mensagem: "O espectador consegue entender e aplicar o conceito.", tipo_narrativa: "Explicação passo a passo", foco_narrativo: "Problema → exemplo → explicação → aplicação", intensidade: 2, tom: "Informativo", construcao_emocao: "Curiosidade inicial, exemplos visuais e pausas após conceitos importantes.", duracao_minima: 60, prioridades: ["Clareza", "Organização", "Didática", "Retenção", "Estética"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: true, usa_zoom: true, usa_musica: false, intensidade_musica: 5, prioridade_voz: "Muito alta", restricoes: "Não cortar exemplos ou transições que prejudiquem a explicação.", qualidade: "O conteúdo fica fácil de acompanhar, revisar e aplicar." }
}[tipo.id] || {};
var completo = Object.assign({ ativo: true, categoria: "Personalizado", objetivo: "", objetivo_secundario: "", publico: "", mensagem: "", tipo_narrativa: "História pessoal", foco_narrativo: "", intensidade: 3, tom: "Positivo", construcao_emocao: "", duracao_minima: 0, prioridades: ["Clareza", "Mensagem", "Ritmo", "Conexão emocional", "Estética"], usa_broll: false, usa_legendas: true, usa_textos: false, usa_graficos: false, usa_zoom: false, usa_musica: false, intensidade_musica: 20, prioridade_voz: "Alta", restricoes: "", qualidade: "" }, configuracoes, tipo);
return completo;
}
function taskEl(id) { return document.getElementById(id); }
function formatClock(ms) {
var total = Math.max(0, Math.round(ms / 1000));
var mm = String(Math.floor(total / 60)).padStart(2, "0");
var ss = String(total % 60).padStart(2, "0");
return mm + ":" + ss;
}
function activeTab() {
var panel = document.querySelector(".tab-panel:not([hidden])");
return panel ? panel.id.replace(/^tab/, "").toLowerCase() : "silence";
}
function taskMoveToOriginTab() {
var dock = taskEl("taskDock");
var nome = taskState.originTab;
var panel = document.getElementById("tab" + nome.charAt(0).toUpperCase() + nome.slice(1));
if (!dock || !panel) return;
panel.insertBefore(dock, panel.firstChild);
taskState.originPanelId = panel.id;
}
function taskStart(title, stage, mirror, cancel) {
if (taskState.clearTimer) { clearTimeout(taskState.clearTimer); taskState.clearTimer = null; }
taskState.active = true;
taskState.startedAt = Date.now();
taskState.mirror = mirror || null;
taskState.cancel = typeof cancel === "function" ? cancel : null;
taskState.canceled = false;
taskState.originTab = activeTab();
taskState.errorText = "";
taskMoveToOriginTab();
var dock = taskEl("taskDock");
dock.removeAttribute("data-result");
dock.hidden = false;
taskEl("taskTitle").textContent = title;
taskEl("taskStage").textContent = stage || "Preparando…";
taskEl("taskPct").textContent = "";
taskEl("taskElapsed").textContent = "00:00";
taskEl("taskTrack").setAttribute("data-mode", "indeterminate");
taskEl("taskFill").style.width = "0%";
taskEl("taskCopy").hidden = true;
taskEl("taskErrorDetails").hidden = true;
taskEl("taskErrorDetails").textContent = "";
taskEl("taskCancel").hidden = !taskState.cancel;
taskEl("taskCancel").disabled = false;
taskEl("taskCancel").textContent = "Cancelar";
hideToast();
if (taskState.timer) clearInterval(taskState.timer);
taskState.timer = setInterval(function () {
taskEl("taskElapsed").textContent = formatClock(Date.now() - taskState.startedAt);
}, 1000);
}
// info: { stage, pct, detail }
function taskUpdate(info) {
if (!taskState.active) return;
if (taskState.mirror) { try { taskState.mirror(info); } catch (e) {} }
var stage = info.stage || "";
var detail = info.detail || "";
taskEl("taskStage").textContent = detail ? stage + " · " + detail : stage;
if (typeof info.pct === "number" && isFinite(info.pct)) {
taskEl("taskTrack").setAttribute("data-mode", "determinate");
taskEl("taskFill").style.width = Math.max(0, Math.min(100, info.pct)) + "%";
taskEl("taskPct").textContent = Math.round(info.pct) + "%";
} else {
taskEl("taskTrack").setAttribute("data-mode", "indeterminate");
taskEl("taskPct").textContent = "";
}
}
function taskEnd(ok, message, technicalDetails) {
if (taskState.timer) { clearInterval(taskState.timer); taskState.timer = null; }
taskState.active = false;
var dock = taskEl("taskDock");
var cancelado = taskState.canceled;
dock.setAttribute("data-result", cancelado ? "cancelled" : (ok ? "ok" : "err"));
taskEl("taskTrack").setAttribute("data-mode", "determinate");
taskEl("taskFill").style.width = "100%";
taskEl("taskStage").textContent = message || (cancelado ? "Cancelado" : (ok ? "Concluído" : "Falhou"));
taskEl("taskPct").textContent = ok ? "100%" : "";
taskEl("taskCancel").hidden = true;
taskEl("taskCancel").disabled = false;
if (!ok && !cancelado) {
taskState.errorText = String(technicalDetails || message || "Falha sem detalhes técnicos.");
taskEl("taskCopy").hidden = false;
taskEl("taskErrorDetails").textContent = taskState.errorText;
taskEl("taskErrorDetails").hidden = false;
if (activeTab() !== taskState.originTab) switchTab(taskState.originTab);
}
taskState.mirror = null;
taskState.cancel = null;
// O próprio card já carrega o desfecho e fica na tela por alguns segundos —
// repetir a mesma frase no toast só empilhava dois avisos idênticos.
taskState.clearTimer = setTimeout(function () {
if (!taskState.active) taskEl("taskDock").hidden = true;
}, ok ? 4000 : 9000);
}
function taskCancel() {
if (!taskState.active || typeof taskState.cancel !== "function") return;
taskState.canceled = true;
taskEl("taskCancel").disabled = true;
taskEl("taskCancel").textContent = "Cancelando…";
taskEl("taskStage").textContent = "Cancelando…";
try { taskState.cancel(); } catch (erro) { taskEnd(false, "Falha ao cancelar", erro.message); }
}
function taskCopyError() {
var texto = taskState.errorText;
if (!texto) return;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(texto).then(function () { showToast("ok", "Erro técnico copiado."); }).catch(function () { taskCopyErrorFallback(texto); });
return;
}
} catch (_) {}
taskCopyErrorFallback(texto);
}
function taskCopyErrorFallback(texto) {
try {
var copia = childProcess.spawn("pbcopy");
copia.stdin.end(texto);
showToast("ok", "Erro técnico copiado.");
return;
} catch (_) {}
var area = document.createElement("textarea");
area.value = texto;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
try { document.execCommand("copy"); showToast("ok", "Erro técnico copiado."); } catch (erro) { showToast("err", "Não foi possível copiar o erro: " + erro.message); }
document.body.removeChild(area);
}
function showToast(kind, text) {
var el = document.getElementById("toast");
if (!el) return;
el.setAttribute("data-kind", kind);
document.getElementById("toastIcon").textContent = kind === "ok" ? "✓" : "!";
document.getElementById("toastText").textContent = text;
el.hidden = false;
}
function hideToast() {
var el = document.getElementById("toast");
if (el) el.hidden = true;
}
// Botão com spinner embutido: o usuário vê que aquele botão específico disparou
// o processo, sem precisar caçar no log.
function setBusy(id, busy) {
var btn = document.getElementById(id);
if (!btn) return;
btn.classList.toggle("is-busy", !!busy);
btn.disabled = !!busy;
}
// ---- Estados dos passos da aba "Editar vídeo" ----
// locked = ainda não dá pra usar | ready = pode usar | running | done | error
function setStep(n, state, chipText) {
var step = document.getElementById("step" + n);
var chip = document.getElementById("step" + n + "Chip");
if (!step) return;
step.setAttribute("data-state", state);
if (chip && chipText) chip.textContent = chipText;
else if (chip) {
chip.textContent =
state === "locked" ? "Bloqueado" :
state === "ready" ? "Pronto" :
state === "running" ? "Rodando…" :
state === "done" ? "Concluído" : "Erro";
}
}
function stepState(n) {
var step = document.getElementById("step" + n);
return step ? step.getAttribute("data-state") : "locked";
}
// ---- Tabs ----
function switchTab(name) {
var tabs = [
{ key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" },
{ key: "refinar", panel: "tabRefinar", btn: "tabBtnRefinar" },
{ key: "silence", panel: "tabSilence", btn: "tabBtnSilence" },
{ key: "retakes", panel: "tabRetakes", btn: "tabBtnRetakes" },
{ key: "analises", panel: "tabAnalises", btn: "tabBtnAnalises" },
{ key: "tipos-video", panel: "tabTiposVideo", btn: "tabBtnTiposVideo" },
{ key: "bridge", panel: "tabBridge", btn: "tabBtnBridge" },
{ key: "models", panel: "tabModels", btn: "tabBtnModels" },
{ key: "settings", panel: "tabSettings", btn: "tabBtnSettings" },
{ key: "testar-acoes", panel: "tabTestarAcoes", btn: "tabBtnTestarAcoes" },
{ key: "legendas", panel: "tabLegendas", btn: "tabBtnLegendas" },
];
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(); renderizarTiposDeVideoParaEdicao(); }
if (name === "settings") renderEditorPersonalities();
if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); scannerListarAnalisesExistentes(); }
if (name === "refinar") refinarAoAbrir();
if (name === "retakes") retakesAoAbrir();
if (name === "analises") analisesAoAbrir();
if (name === "tipos-video") tiposVideoAoAbrir();
}
var LEGENDAS_CONFIGURACAO_CHAVE = "schedule_light_configuracao_legendas";
function configuracaoDeLegendasPadrao() {
return { habilitadas: false, estilo: "word_top_down", faixa_video: 3, max_palavras: 5, max_caracteres: 32, mogrt_path: "" };
}
function carregarConfiguracaoDeLegendas() {
var configuracao = configuracaoDeLegendasPadrao();
try {
var salva = JSON.parse(localStorage.getItem(LEGENDAS_CONFIGURACAO_CHAVE) || "null");
if (salva && typeof salva === "object") configuracao = Object.assign(configuracao, salva);
} catch (e) {}
return configuracao;
}
function restaurarConfiguracaoDeLegendas() {
var configuracao = carregarConfiguracaoDeLegendas();
document.getElementById("legendasHabilitadas").checked = configuracao.habilitadas;
document.getElementById("legendasEstilo").value = configuracao.estilo;
document.getElementById("legendasFaixa").value = configuracao.faixa_video;
document.getElementById("legendasMaxPalavras").value = configuracao.max_palavras;
document.getElementById("legendasMaxCaracteres").value = configuracao.max_caracteres;
document.getElementById("legendasMogrtPath").value = configuracao.mogrt_path;
}
function salvarConfiguracaoDeLegendas() {
var configuracao = {
habilitadas: !!document.getElementById("legendasHabilitadas").checked,
estilo: document.getElementById("legendasEstilo").value,
faixa_video: Number(document.getElementById("legendasFaixa").value),
max_palavras: Number(document.getElementById("legendasMaxPalavras").value),
max_caracteres: Number(document.getElementById("legendasMaxCaracteres").value),
mogrt_path: document.getElementById("legendasMogrtPath").value.trim(),
};
try { localStorage.setItem(LEGENDAS_CONFIGURACAO_CHAVE, JSON.stringify(configuracao)); } catch (e) {}
document.getElementById("legendasStatus").textContent = "Configuração de legendas salva neste computador.";
}
var legendasGeradas = [];
function legendaNumeroParaTempo(segundos) {
var horas = Math.floor(segundos / 3600);
var minutos = Math.floor((segundos % 3600) / 60);
var segundosInteiros = Math.floor(segundos % 60);
var milissegundos = Math.round((segundos - Math.floor(segundos)) * 1000);
if (milissegundos >= 1000) { segundosInteiros++; milissegundos = 0; }
return [horas, minutos, segundosInteiros].map(function (n) { return String(n).padStart(2, "0"); }).join(":") + "," + String(milissegundos).padStart(3, "0");
}
function legendasExtrairPalavras(transcricao) {
var segmentos = transcricao.segmentos || transcricao.segments || [];
var palavras = [];
segmentos.forEach(function (segmento) {
var itens = segmento.palavras || segmento.words || [];
itens.forEach(function (palavra) {
var texto = String(palavra.texto || palavra.word || "").trim();
var inicio = Number(palavra.inicio !== undefined ? palavra.inicio : palavra.start);
var fim = Number(palavra.fim !== undefined ? palavra.fim : palavra.end);
if (texto && isFinite(inicio) && isFinite(fim) && fim > inicio) palavras.push({ texto: texto, inicio: inicio, fim: fim });
});
});
return palavras.sort(function (a, b) { return a.inicio - b.inicio; });
}
function legendasIntervaloCortado(palavra, cortes) {
return cortes.some(function (corte) { return palavra.inicio < corte.end && palavra.fim > corte.start; });
}
function legendasGerarBlocos(palavras, cortes, configuracao) {
var mantidas = palavras.filter(function (palavra) { return !legendasIntervaloCortado(palavra, cortes); });
var blocos = [], bloco = [], caracteres = 0;
function adicionar() {
if (!bloco.length) return;
var primeiro = bloco[0], ultimo = bloco[bloco.length - 1];
// Para o primeiro corte, todos os cortes anteriores são considerados.
var inicio = primeiro.inicio - cortes.reduce(function (total, corte) { return total + (corte.end <= primeiro.inicio ? corte.end - corte.start : 0); }, 0);
var fim = ultimo.fim - cortes.reduce(function (total, corte) { return total + (corte.end <= ultimo.fim ? corte.end - corte.start : 0); }, 0);
blocos.push({ texto: bloco.map(function (item) { return item.texto; }).join(" "), inicio: inicio, fim: fim });
bloco = []; caracteres = 0;
}
mantidas.forEach(function (palavra) {
var espaco = bloco.length ? 1 : 0;
if (bloco.length && (bloco.length >= configuracao.max_palavras || caracteres + espaco + palavra.texto.length > configuracao.max_caracteres)) adicionar();
bloco.push(palavra); caracteres += (bloco.length > 1 ? 1 : 0) + palavra.texto.length;
});
adicionar();
return blocos;
}
function legendasRenderizar() {
var lista = document.getElementById("legendasLista");
document.getElementById("legendasContagem").textContent = "(" + legendasGeradas.length + ")";
lista.textContent = "";
if (!legendasGeradas.length) { lista.innerHTML = '
Nenhuma legenda carregada.
'; return; }
legendasGeradas.forEach(function (legenda, indice) {
var linha = document.createElement("div"); linha.className = "legenda-row"; linha.setAttribute("role", "listitem");
var cabecalho = document.createElement("div"); cabecalho.className = "legenda-timecode"; cabecalho.textContent = (indice + 1) + " " + legendaNumeroParaTempo(legenda.inicio) + " → " + legendaNumeroParaTempo(legenda.fim);
var campo = document.createElement("input"); campo.className = "text-field"; campo.value = legenda.texto; campo.setAttribute("aria-label", "Texto da legenda " + (indice + 1));
campo.oninput = function () { legendasGeradas[indice].texto = campo.value; };
var excluir = document.createElement("button"); excluir.className = "button button-ghost"; excluir.type = "button"; excluir.textContent = "×"; excluir.title = "Excluir legenda";
excluir.onclick = function () { legendasGeradas.splice(indice, 1); legendasRenderizar(); };
linha.appendChild(cabecalho); linha.appendChild(campo); linha.appendChild(excluir); lista.appendChild(linha);
});
}
function gerarLegendasDaTranscricao() {
var caminhoDaTranscricao = obterCaminhoDaTranscricaoParaLegendas();
if (!caminhoDaTranscricao) {
document.getElementById("legendasStatus").textContent = "Escolha uma timeline na aba Editar vídeo e carregue uma transcrição salva ou execute a transcrição.";
return;
}
try {
var transcricao = JSON.parse(fs.readFileSync(caminhoDaTranscricao, "utf-8"));
var plano = JSON.parse(document.getElementById("testarAcoesJson").value || "{}");
var cortes = (plano.actions || []).filter(function (acao) { return acao.kind === "cut"; }).map(function (acao) { return { start: Number(acao.start), end: Number(acao.end) }; });
var configuracao = carregarConfiguracaoDeLegendas();
legendasGeradas = legendasGerarBlocos(legendasExtrairPalavras(transcricao), cortes, configuracao);
legendasRenderizar();
document.getElementById("legendasStatus").textContent = legendasGeradas.length + " legenda(s) gerada(s) para revisão.";
} catch (erro) { document.getElementById("legendasStatus").textContent = "Não foi possível gerar: " + erro.message; }
}
function obterCaminhoDaTranscricaoParaLegendas() {
if (silenceState.transcriptPath && fs.existsSync(silenceState.transcriptPath)) return silenceState.transcriptPath;
if (!silenceState.mediaPath) return null;
var entradas = findCachedTranscripts(silenceState.mediaPath).sort(function (a, b) { return new Date(b.savedAt) - new Date(a.savedAt); });
if (!entradas.length) return null;
silenceState.transcriptPath = entradas[0].transcriptPath;
silenceState.cachedTranscriptPath = entradas[0].transcriptPath;
return silenceState.transcriptPath;
}
function exportarLegendasSrt() {
if (!legendasGeradas.length) { document.getElementById("legendasStatus").textContent = "Gere as legendas antes de exportar."; return; }
var destino = path.join(silenceState.outputDir || os.tmpdir(), "legendas-geradas.srt");
var conteudo = legendasGeradas.map(function (legenda, indice) { return (indice + 1) + "\n" + legendaNumeroParaTempo(legenda.inicio) + " --> " + legendaNumeroParaTempo(legenda.fim) + "\n" + legenda.texto + "\n"; }).join("\n");
fs.writeFileSync(destino, conteudo, "utf-8");
document.getElementById("legendasStatus").textContent = "SRT exportado em " + destino;
}
function inserirLegendasNaTimeline() {
document.getElementById("legendasStatus").textContent = "A lista está pronta. A inserção automática dos MOGRTs será conectada na próxima etapa, após validar o estilo escolhido.";
}
// ---- Teste isolado de ações pela engine Python ----------------------------
var TESTAR_ACOES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/aplicar_plano_de_edicao.py";
function testarAcoesCarregarExemplo() {
var exemplo = {
schema_version: "1.0",
source: "video.mp4",
actions: [{ kind: "cut", start: 0.0, end: 1.0, reason: "Teste de corte inicial pela engine Python." }]
};
document.getElementById("testarAcoesJson").value = JSON.stringify(exemplo, null, 2);
testarAcoesAtualizarStatus("Exemplo carregado. Ajuste o source e os tempos antes de executar.", "");
}
function testarAcoesLog(mensagem, classe) {
var alvo = document.getElementById("testarAcoesLog");
if (!alvo) return;
var linha = document.createElement("p");
linha.textContent = mensagem;
if (classe) linha.className = classe;
alvo.appendChild(linha);
alvo.scrollTop = alvo.scrollHeight;
}
function testarAcoesAtualizarStatus(mensagem, classe) {
var status = document.getElementById("testarAcoesStatus");
if (status) { status.textContent = mensagem; status.className = "field-help " + (classe || ""); }
}
function testarAcoesExecutar() {
var campo = document.getElementById("testarAcoesJson");
var texto = campo.value.trim();
var plano;
if (!texto) { testarAcoesAtualizarStatus("Cole um plano JSON antes de executar.", "err"); return; }
try { plano = JSON.parse(texto); } catch (erro) {
testarAcoesAtualizarStatus("JSON inválido: " + erro.message, "err");
testarAcoesLog("JSON inválido: " + erro.message, "err");
return;
}
if (!plano || typeof plano !== "object" || !plano.source || !Array.isArray(plano.actions) || !plano.actions.length) {
testarAcoesAtualizarStatus("O plano precisa ter source e uma lista não vazia de actions.", "err");
return;
}
var tipos = plano.actions.map(function (acao) { return acao && acao.kind; });
if (tipos.some(function (tipo) { return tipo !== "cut"; })) {
testarAcoesAtualizarStatus("Este primeiro teste aceita somente actions kind: cut.", "err");
return;
}
var entrada = path.join(os.tmpdir(), "teste-acoes-" + Date.now() + ".json");
try { fs.writeFileSync(entrada, JSON.stringify(plano, null, 2), "utf-8"); } catch (erroEscrita) {
testarAcoesAtualizarStatus("Não foi possível criar o plano temporário: " + erroEscrita.message, "err");
return;
}
var botao = document.getElementById("btnTestarAcoesExecutar");
if (botao) botao.disabled = true;
testarAcoesAtualizarStatus("A engine está conectando ao Premiere e aplicando o corte…", "");
testarAcoesLog("Plano temporário criado: " + entrada);
taskStart("Teste de ação", "Executando pela engine Python");
runStreamed(
SCANNER_PYTHON_BIN,
[TESTAR_ACOES_ENGINE_SCRIPT, entrada],
function (linha) { testarAcoesLog(linha); },
function (codigo, ultimoErro) {
if (botao) botao.disabled = false;
if (codigo !== 0) {
testarAcoesAtualizarStatus("Falha na execução. Consulte os detalhes técnicos.", "err");
testarAcoesLog(ultimoErro || "A engine terminou com código " + codigo + ".", "err");
taskEnd(false, "Teste de ação falhou", ultimoErro || null);
return;
}
testarAcoesAtualizarStatus("Corte executado pela engine Python. Verifique a timeline no Premiere.", "ok");
testarAcoesLog("Engine concluída com sucesso.", "ok");
taskEnd(true, "Teste de corte concluído");
},
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine" }
);
}
// ---- Catálogo de tipos de vídeo -------------------------------------------
// A primeira versão usa o armazenamento local do painel. O documento salvo já
// tem formato estável e pode ser migrado para o Schedule Light sem alterar o
// contrato editorial enviado para a IA.
function tiposVideoLer() {
try {
var salvo = JSON.parse(localStorage.getItem(TIPOS_VIDEO_CHAVE) || "null");
if (Array.isArray(salvo) && salvo.length) return salvo.map(tipoVideoCompletar);
var iniciais = tiposVideoPadrao.slice().map(tipoVideoCompletar);
tiposVideoSalvarNoCatalogo(iniciais);
return iniciais;
} catch (_) { return tiposVideoPadrao.slice().map(tipoVideoCompletar); }
}
function tiposVideoSalvarNoCatalogo(lista) {
localStorage.setItem(TIPOS_VIDEO_CHAVE, JSON.stringify(lista));
}
function tiposVideoAoAbrir() {
tiposVideoRenderizarLista();
if (!tipoVideoAtualId) tipoVideoNovo();
}
function tiposVideoRenderizarLista() {
var lista = document.getElementById("tiposVideoLista");
if (!lista) return;
lista.innerHTML = "";
tiposVideoLer().forEach(function (tipo) {
var item = document.createElement("div");
item.className = "video-type-row" + (tipo.id === tipoVideoAtualId ? " is-selected" : "");
item.setAttribute("role", "listitem");
item.innerHTML = '
';
item.querySelector("strong").textContent = tipo.nome;
item.querySelector("span").textContent = tipo.descricao;
item.querySelector(".video-type-meta").textContent = tipo.duracao_maxima_segundos + " s · " + tipo.proporcao + " · " + tipo.canais;
item.querySelector(".video-type-status").textContent = tipo.ativo === false ? "Inativo" : "Ativo";
item.querySelector(".video-type-status").setAttribute("data-state", tipo.ativo === false ? "inactive" : "active");
item.querySelector(".video-type-edit").onclick = function () { tipoVideoEditar(tipo.id); };
item.querySelector(".video-type-duplicate").onclick = function () { tipoVideoDuplicar(tipo.id); };
item.querySelector(".video-type-toggle").textContent = tipo.ativo === false ? "Ativar" : "Inativar";
item.querySelector(".video-type-toggle").onclick = function () { tipoVideoAlternarAtivo(tipo.id); };
item.querySelector(".video-type-copy").onclick = function () { tipoVideoEditar(tipo.id); };
lista.appendChild(item);
});
}
function tipoVideoNovo() {
tipoVideoAtualId = null;
tipoVideoPreencher(tipoVideoCompletar({ id: "", nome: "", duracao_maxima_segundos: 60, descricao: "", canais: "", proporcao: "9:16", ritmo: "equilibrado", emocao: "", storytelling: true, estrutura: "", cortes: "", broll: "", texto: "", som: "", diretriz: "" }));
document.getElementById("tipoVideoEditorTitulo").textContent = "Novo tipo de vídeo";
document.getElementById("tipoVideoEditorStatus").textContent = "Rascunho";
document.getElementById("tipoVideoEditorCard").hidden = false;
tiposVideoRenderizarLista();
}
function tipoVideoEditar(id) {
var tipo = tiposVideoLer().filter(function (item) { return item.id === id; })[0];
if (!tipo) return;
tipoVideoAtualId = id;
tipoVideoPreencher(tipo);
document.getElementById("tipoVideoEditorTitulo").textContent = "Editar tipo de vídeo";
document.getElementById("tipoVideoEditorStatus").textContent = "Cadastrado";
document.getElementById("tipoVideoEditorCard").hidden = false;
tiposVideoRenderizarLista();
}
function tipoVideoPreencher(tipo) {
tipo = tipoVideoCompletar(tipo);
["nome", "descricao", "canais", "proporcao", "ritmo", "emocao", "estrutura", "cortes", "broll", "texto", "som", "diretriz", "categoria", "objetivo", "objetivo_secundario", "publico", "mensagem", "tipo_narrativa", "foco_narrativo", "tom", "construcao_emocao", "restricoes", "qualidade", "prioridade_voz"].forEach(function (campo) {
var elemento = document.getElementById("tipoVideo" + campo.charAt(0).toUpperCase() + campo.slice(1));
if (elemento) elemento.value = tipo[campo] || "";
});
document.getElementById("tipoVideoDuracao").value = tipo.duracao_maxima_segundos || "";
document.getElementById("tipoVideoDuracaoMinima").value = tipo.duracao_minima || "";
document.getElementById("tipoVideoStorytelling").checked = !!tipo.storytelling;
document.getElementById("tipoVideoAtivo").checked = tipo.ativo !== false;
document.getElementById("tipoVideoIntensidade").value = tipo.intensidade || 3;
document.getElementById("tipoVideoIntensidadeMusica").value = tipo.intensidade_musica || 0;
document.getElementById("tipoVideoUsaBroll").checked = !!tipo.usa_broll;
document.getElementById("tipoVideoUsaLegendas").checked = !!tipo.usa_legendas;
document.getElementById("tipoVideoUsaTextos").checked = !!tipo.usa_textos;
document.getElementById("tipoVideoUsaGraficos").checked = !!tipo.usa_graficos;
document.getElementById("tipoVideoUsaZoom").checked = !!tipo.usa_zoom;
document.getElementById("tipoVideoUsaMusica").checked = !!tipo.usa_musica;
tipoVideoRenderizarPrioridades(tipo.prioridades);
tipoVideoAtualizarIntensidade();
tipoVideoAtualizarIntensidadeMusica();
}
function tipoVideoRenderizarPrioridades(prioridades) {
var lista = document.getElementById("tipoVideoPrioridades");
lista.innerHTML = "";
(prioridades || []).slice(0, 5).forEach(function (valor, indice) {
var linha = document.createElement("div");
linha.className = "priority-row";
linha.innerHTML = '' + (indice + 1) + '';
linha.querySelector("input").value = valor;
lista.appendChild(linha);
});
}
function tipoVideoPrioridadesDoFormulario() {
return Array.prototype.slice.call(document.querySelectorAll("#tipoVideoPrioridades .priority-input")).map(function (input) { return input.value.trim(); }).filter(Boolean);
}
function tipoVideoAtualizarIntensidade() {
var valor = document.getElementById("tipoVideoIntensidade");
var saida = document.getElementById("tipoVideoIntensidadeValor");
if (valor && saida) saida.textContent = valor.value + " / 5";
}
function tipoVideoAtualizarIntensidadeMusica() {
var valor = document.getElementById("tipoVideoIntensidadeMusica");
var saida = document.getElementById("tipoVideoIntensidadeMusicaValor");
if (valor && saida) saida.textContent = valor.value + "%";
}
function tipoVideoDoFormulario() {
var nome = document.getElementById("tipoVideoNome").value.trim();
if (!nome) throw new Error("Informe o nome do tipo de vídeo.");
var base = tipoVideoAtualId ? tiposVideoLer().filter(function (item) { return item.id === tipoVideoAtualId; })[0] : null;
return {
id: tipoVideoAtualId || (nome.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + "-" + Date.now()),
nome: nome,
descricao: document.getElementById("tipoVideoDescricao").value.trim(),
ativo: document.getElementById("tipoVideoAtivo").checked,
categoria: document.getElementById("tipoVideoCategoria").value,
objetivo: document.getElementById("tipoVideoObjetivo").value.trim(),
objetivo_secundario: document.getElementById("tipoVideoObjetivoSecundario").value.trim(),
publico: document.getElementById("tipoVideoPublico").value.trim(),
mensagem: document.getElementById("tipoVideoMensagem").value.trim(),
tipo_narrativa: document.getElementById("tipoVideoTipoNarrativa").value,
foco_narrativo: document.getElementById("tipoVideoFocoNarrativo").value.trim(),
intensidade: Number(document.getElementById("tipoVideoIntensidade").value) || 3,
tom: document.getElementById("tipoVideoTom").value,
construcao_emocao: document.getElementById("tipoVideoConstrucaoEmocao").value.trim(),
duracao_maxima_segundos: Number(document.getElementById("tipoVideoDuracao").value) || 0,
duracao_minima: Number(document.getElementById("tipoVideoDuracaoMinima").value) || 0,
canais: document.getElementById("tipoVideoCanais").value.trim(),
proporcao: document.getElementById("tipoVideoProporcao").value,
ritmo: document.getElementById("tipoVideoRitmo").value,
emocao: document.getElementById("tipoVideoEmocao").value.trim(),
storytelling: document.getElementById("tipoVideoStorytelling").checked,
estrutura: document.getElementById("tipoVideoEstrutura").value.trim(),
cortes: document.getElementById("tipoVideoCortes").value.trim(),
broll: document.getElementById("tipoVideoBroll").value.trim(),
texto: document.getElementById("tipoVideoTexto").value.trim(),
som: document.getElementById("tipoVideoSom").value.trim(),
diretriz: document.getElementById("tipoVideoDiretriz").value.trim(),
prioridades: tipoVideoPrioridadesDoFormulario(),
usa_broll: document.getElementById("tipoVideoUsaBroll").checked,
usa_legendas: document.getElementById("tipoVideoUsaLegendas").checked,
usa_textos: document.getElementById("tipoVideoUsaTextos").checked,
usa_graficos: document.getElementById("tipoVideoUsaGraficos").checked,
usa_zoom: document.getElementById("tipoVideoUsaZoom").checked,
usa_musica: document.getElementById("tipoVideoUsaMusica").checked,
intensidade_musica: Number(document.getElementById("tipoVideoIntensidadeMusica").value) || 0,
prioridade_voz: document.getElementById("tipoVideoPrioridadeVoz").value,
restricoes: document.getElementById("tipoVideoRestricoes").value.trim(),
qualidade: document.getElementById("tipoVideoQualidade").value.trim(),
criado_em: base && base.criado_em ? base.criado_em : new Date().toISOString(),
atualizado_em: new Date().toISOString()
};
}
function tipoVideoSalvar() {
try {
var item = tipoVideoDoFormulario();
var lista = tiposVideoLer();
var indice = lista.findIndex(function (tipo) { return tipo.id === item.id; });
if (indice >= 0) lista[indice] = item; else lista.push(item);
tiposVideoSalvarNoCatalogo(lista);
tipoVideoAtualId = item.id;
document.getElementById("tipoVideoEditorStatus").textContent = "Salvo";
document.getElementById("tiposVideoStatus").textContent = "Catálogo salvo localmente · " + lista.length + " tipo(s) cadastrado(s).";
tiposVideoRenderizarLista();
showToast("ok", "Tipo de vídeo salvo no catálogo.");
} catch (erro) { showToast("err", erro.message); }
}
function tipoVideoDuplicar(id) {
var origem = tiposVideoLer().filter(function (item) { return item.id === id; })[0];
if (!origem) return;
var copia = JSON.parse(JSON.stringify(origem));
copia.id = origem.id + "-copia-" + Date.now();
copia.nome = origem.nome + " (cópia)";
copia.ativo = true;
copia.criado_em = new Date().toISOString();
copia.atualizado_em = copia.criado_em;
var lista = tiposVideoLer();
lista.push(copia);
tiposVideoSalvarNoCatalogo(lista);
tipoVideoAtualId = copia.id;
tipoVideoPreencher(copia);
document.getElementById("tipoVideoEditorTitulo").textContent = "Editar tipo de vídeo";
document.getElementById("tipoVideoEditorStatus").textContent = "Cópia criada";
document.getElementById("tipoVideoEditorCard").hidden = false;
document.getElementById("tiposVideoStatus").textContent = "Perfil duplicado. Revise os campos e salve a cópia.";
tiposVideoRenderizarLista();
showToast("ok", "Perfil duplicado. Você pode ajustar a cópia antes de salvar.");
}
function tipoVideoAlternarAtivo(id) {
var lista = tiposVideoLer();
var item = lista.filter(function (perfil) { return perfil.id === id; })[0];
if (!item) return;
item.ativo = item.ativo === false;
item.atualizado_em = new Date().toISOString();
tiposVideoSalvarNoCatalogo(lista);
if (tipoVideoAtualId === id) {
document.getElementById("tipoVideoAtivo").checked = item.ativo;
document.getElementById("tipoVideoEditorStatus").textContent = item.ativo ? "Ativo" : "Inativo";
}
tiposVideoRenderizarLista();
showToast("ok", item.ativo ? "Perfil ativado." : "Perfil inativado.");
}
function tipoVideoCancelar() {
tipoVideoAtualId = null;
document.getElementById("tipoVideoEditorCard").hidden = true;
tiposVideoRenderizarLista();
}
function tipoVideoCopiarJson() {
try {
var tipo = tipoVideoDoFormulario();
var resumo = {
perfil_de_video: tipo.nome,
objetivo: {
principal: tipo.objetivo,
secundario: tipo.objetivo_secundario,
publico: tipo.publico,
mensagem: tipo.mensagem
},
narrativa: {
tipo: tipo.tipo_narrativa,
foco: tipo.foco_narrativo,
storytelling: tipo.storytelling,
estrutura: tipo.estrutura
},
emocao: {
objetivo: tipo.emocao,
intensidade: tipo.intensidade,
tom: tipo.tom,
construcao: tipo.construcao_emocao
},
formato: {
canais: tipo.canais,
proporcao: tipo.proporcao,
duracao_minima_segundos: tipo.duracao_minima,
duracao_maxima_segundos: tipo.duracao_maxima_segundos,
ritmo: tipo.ritmo
},
prioridades: tipo.prioridades,
elementos_de_edicao: {
broll: tipo.usa_broll,
legendas: tipo.usa_legendas,
textos: tipo.usa_textos,
graficos: tipo.usa_graficos,
zoom: tipo.usa_zoom,
cortes: tipo.cortes,
orientacao_de_broll: tipo.broll,
textos_e_legendas: tipo.texto
},
audio: {
musica: tipo.usa_musica,
intensidade_da_musica: tipo.intensidade_musica,
prioridade_da_voz: tipo.prioridade_voz,
tratamento: tipo.som
},
regras_de_edicao: tipo.diretriz,
restricoes: tipo.restricoes,
criterios_de_qualidade: tipo.qualidade
};
copiarTextoParaAreaDeTransferencia(JSON.stringify(resumo, null, 2));
} catch (erro) { showToast("err", erro.message); }
}
function copiarTextoParaAreaDeTransferencia(texto, mensagem) {
mensagem = mensagem || "Conteúdo copiado para a área de transferência.";
function copiarComSelecaoDoPainel() {
var area = document.createElement("textarea");
area.value = texto;
area.setAttribute("readonly", "readonly");
area.style.position = "fixed";
area.style.left = "-9999px";
area.style.top = "0";
document.body.appendChild(area);
area.focus();
area.select();
var copiado = false;
try { copiado = document.execCommand("copy"); } catch (_) { copiado = false; }
document.body.removeChild(area);
if (copiado) showToast("ok", mensagem);
else showToast("err", "Não foi possível copiar o JSON. Selecione e copie manualmente.");
}
// O CEP antigo pode corromper caracteres UTF-8 pela API de clipboard do Chromium.
// O pbcopy recebe bytes UTF-8 diretamente e evita essa conversão intermediária.
copiarComPbcopy(texto, function () {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(texto).then(function () {
showToast("ok", mensagem);
}).catch(copiarComSelecaoDoPainel);
return;
}
} catch (_) {}
copiarComSelecaoDoPainel();
}, mensagem);
}
function copiarComPbcopy(texto, fallback, mensagem) {
try {
var copia = childProcess.spawn("pbcopy");
var finalizado = false;
function concluir(copiado) {
if (finalizado) return;
finalizado = true;
if (copiado) showToast("ok", mensagem || "Conteúdo copiado para a área de transferência.");
else fallback();
}
copia.once("error", function () { concluir(false); });
copia.once("close", function (codigo) { concluir(codigo === 0); });
copia.stdin.write(texto, "utf8");
copia.stdin.end();
} catch (_) { fallback(); }
}
// ---- Análises ---------------------------------------------------------------
// Aba que lê o banco de análises (.jhonny/analises.db) já produzido pelo
// Scanner e mostra/copiar os dois J-SOM: som (transcrição por cena) e
// imagem (evidências visuais). A leitura acontece na engine, via script
// dedicado; a aba nunca escreve no banco.
var ANALISES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/gerar_relatorio_de_analises.py";
var ANALISES_DB_PATH = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db";
var analisesRelatorioPath = null;
function analisesAoAbrir() {
document.getElementById("analisesCaminho").textContent = ANALISES_DB_PATH;
}
function analisesFormatarTempo(segundos) {
var s = Number(segundos) || 0;
var min = Math.floor(s / 60), resto = (s - min * 60).toFixed(1);
return min + ":" + (resto < 10 ? "0" : "") + resto;
}
function analisesCarregar() {
var stamp = new Date().toISOString().replace(/[:.]/g, "-");
analisesRelatorioPath = path.join(os.tmpdir(), "jhonny-analises-" + stamp + ".json");
setBusy("btnAnalisesCarregar", true); taskStart("Análises", "Lendo o banco de análises", null);
var child = childProcess.spawn(SCANNER_PYTHON_BIN, [ANALISES_ENGINE_SCRIPT, ANALISES_DB_PATH, analisesRelatorioPath],
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", stdio: ["ignore", "pipe", "pipe"] });
var stderr = "", stdout = "";
child.stdout.on("data", function (chunk) { stdout += chunk.toString(); });
child.stderr.on("data", function (chunk) { stderr += chunk.toString(); });
child.on("error", function (error) { setBusy("btnAnalisesCarregar", false); taskEnd(false, "Análises indisponíveis", error.stack || error.message); });
child.on("close", function (code) {
setBusy("btnAnalisesCarregar", false);
if (code !== 0 || !fs.existsSync(analisesRelatorioPath)) {
taskEnd(false, "Leitura do banco falhou", stderr || stdout || "Não foi possível gerar o relatório.");
return;
}
taskEnd(true, "Relatório carregado do banco");
analisesRender(JSON.parse(fs.readFileSync(analisesRelatorioPath, "utf-8")));
});
}
function analisesRender(relatorio) {
var video = relatorio.video || {};
var cenas = relatorio.cenas || [];
var falas = cenas.reduce(function (n, c) { return n + (c.falas || []).length; }, 0);
var evidencias = cenas.reduce(function (n, c) {
var resumo = c.resumo_de_evidencias || {};
return n + Object.keys(resumo).reduce(function (m, k) { return m + resumo[k]; }, 0);
}, 0);
document.getElementById("analisesStatus").textContent = "Carregado";
document.getElementById("analisesVideoNome").textContent = video.nome || "(sem nome)";
document.getElementById("analisesResumo").textContent =
cenas.length + " cenas · " + falas + " falas · " + evidencias + " evidências visuais";
document.getElementById("analisesResumoCard").hidden = false;
document.getElementById("btnAnalisesAbrir").disabled = false;
document.getElementById("btnAnalisesCopiar").disabled = false;
var lista = document.getElementById("analisesCenas");
lista.innerHTML = "";
cenas.forEach(function (cena) {
var bloco = document.createElement("div");
bloco.className = "analise-cena";
bloco.setAttribute("role", "listitem");
var titulo = cena.cena_registrada ? "" : " (sem cena gravada)";
var tipos = Object.keys(cena.resumo_de_evidencias || {})
.sort().map(function (k) { return k + " " + cena.resumo_de_evidencias[k]; }).join(", ") || "—";
var falasHtml = (cena.falas || []).map(function (fala) {
return "" + (fala.emocao || "—") + " " +
analisesFormatarTempo(fala.inicio) + "–" + analisesFormatarTempo(fala.fim) + " · " +
String(fala.texto || "").replace(/&/g, "&").replace(/";
}).join("") || "
Sem fala neste clipe.
";
bloco.innerHTML =
"" + cena.clipe_id + titulo + "" +
"" + analisesFormatarTempo(cena.inicio) + "–" + analisesFormatarTempo(cena.fim) + "
" +
falasHtml + "Imagem: " + tipos + "
";
lista.appendChild(bloco);
});
}
function analisesAbrirJson() {
if (!analisesRelatorioPath) return;
childProcess.spawn("open", [analisesRelatorioPath], { detached: true, stdio: "ignore" }).unref();
}
function analisesCopiar() {
if (!analisesRelatorioPath) return;
try {
var copy = childProcess.spawn("pbcopy");
copy.stdin.end(fs.readFileSync(analisesRelatorioPath, "utf-8"));
showToast("ok", "J-SOM de som e imagem copiados (JSON completo).");
} catch (error) { showToast("err", error.message); }
}
function abrirPastaDoJsonGerado() {
if (!silenceState.jsonOutputPath) return;
try {
childProcess.spawn("open", [path.dirname(silenceState.jsonOutputPath)], { detached: true, stdio: "ignore" }).unref();
} catch (erro) {
showToast("err", "Não foi possível abrir a pasta no Finder: " + erro.message);
}
}
function copiarCaminhoDoJsonGerado() {
if (!silenceState.jsonOutputPath) return;
copiarComPbcopy(
silenceState.jsonOutputPath,
function () {
copiarTextoParaAreaDeTransferencia(silenceState.jsonOutputPath, "Caminho do JSON copiado para a área de transferência.");
},
"Caminho do JSON copiado para a área de transferência."
);
}
// ---- Retakes ---------------------------------------------------------------
// A aba apenas controla o processo e mostra resultados; a análise inteira
// acontece na engine (scanner/retakes), chamada via executar_retakes.py.
var RETAKES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_retakes.py";
// A engine usa tipos do Python 3.10+ (ex.: `X | None`, `dict[str, ...]`), que
// o `/usr/bin/python3` do macOS (3.9) não aceita. Usar o mesmo venv do projeto
// (Python 3.14, com faster_whisper) garante que Scanner e Retakes carreguem bem.
var SCANNER_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python";
var ANALISE_STATUS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_status_da_analise.py";
var ANALISE_PREVIEW_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_preview_de_falantes.py";
var retakesCasoAtual = null;
var retakesGrupos = [];
var retakesProcesso = null;
function retakesRaiz() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); }
function retakesAoAbrir() { retakesListarCasos(); }
function retakesListarCasos() {
var seletor = document.getElementById("retakesCaso");
var anterior = seletor.value;
seletor.innerHTML = "";
var raiz = retakesRaiz();
var casos = [];
try { casos = fs.readdirSync(raiz).filter(function (item) { return fs.statSync(path.join(raiz, item)).isDirectory(); }).sort(); } catch (_) {}
if (!casos.length) { seletor.appendChild(new Option("Nenhum caso encontrado", "")); retakesCasoAtual = null; retakesInfo("Execute o Scanner primeiro para gerar transcrições."); return; }
casos.forEach(function (caso) { seletor.appendChild(new Option(caso, caso)); });
seletor.value = anterior && casos.indexOf(anterior) >= 0 ? anterior : casos[0];
retakesCarregarCaso();
}
function retakesInfo(mensagem) { document.getElementById("retakesInfoCaso").textContent = mensagem; }
function retakesArtefatosDeAudio(caso) {
var pasta = path.join(retakesRaiz(), caso);
try { return fs.readdirSync(pasta).filter(function (item) { return /-audio-faixa-\d+\.json$/.test(item); }).map(function (item) { return path.join(pasta, item); }).sort(); } catch (_) { return []; }
}
function retakesCarregarCaso() {
var caso = document.getElementById("retakesCaso").value;
retakesCasoAtual = caso || null;
retakesGrupos = [];
retakesRenderResultados();
if (!caso) { retakesInfo("Nenhum caso disponível."); return; }
var artefatos = retakesArtefatosDeAudio(caso);
var jaTem = fs.existsSync(path.join(retakesRaiz(), caso, "retakes"));
retakesInfo("Caso: " + caso + " · " + artefatos.length + " faixa(s) de áudio com transcrição" + (jaTem ? " · já analisado" : " · não analisado"));
document.getElementById("retakesStatusGeral").textContent = jaTem ? "Analisado" : "Não analisado";
if (jaTem) retakesCarregarResultadosSalvos();
}
function retakesCarregarResultadosSalvos() {
var arquivo = path.join(retakesRaiz(), retakesCasoAtual, "retakes", "retakes-" + retakesCasoAtual + ".json");
try {
var dados = JSON.parse(fs.readFileSync(arquivo, "utf-8"));
retakesGrupos = dados.grupos_de_retakes || [];
retakesRenderResultados();
} catch (erro) { showToast("err", "Não foi possível ler os resultados: " + erro.message); }
}
function retakesFormatarTempo(segundos) {
if (typeof segundos !== "number" || isNaN(segundos)) return "—";
var total = Math.max(0, Math.round(segundos));
var m = Math.floor(total / 60), s = total % 60;
return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
}
function retakesRotuloDoGrupo(grupo) {
var tipo = { retake_confirmado: "Retake forte", possivel_retake: "Possível retake", repeticao_parcial: "Repetição parcial", repeticao_de_frase: "Repetição de frase", repeticao_de_inicio: "Repetição de início", continuidade_de_fala: "Continuidade de fala", sem_indicio_de_retake: "Sem indício" }[grupo.tipo] || grupo.tipo;
var status = grupo.status_de_revisao === "confirmado" ? " · confirmado" : grupo.status_de_revisao === "nao_e_retake" ? " · rejeitado" : grupo.status_de_revisao === "ignorado" ? " · ignorado" : "";
return tipo + " · " + Math.round(grupo.confianca * 100) + "% de confiança" + status;
}
function retakesPassaNoFiltro(grupo) {
var filtro = document.getElementById("retakesFiltro").value;
if (filtro === "todos") return true;
if (filtro === "fortes") return grupo.tipo === "retake_confirmado";
if (filtro === "possiveis") return grupo.tipo !== "retake_confirmado" && grupo.tipo !== "sem_indicio_de_retake";
return grupo.status_de_revisao === filtro;
}
function retakesRenderResultados() {
var alvo = document.getElementById("retakesResultados");
alvo.innerHTML = "";
var visiveis = retakesGrupos.filter(retakesPassaNoFiltro);
document.getElementById("retakesContagem").textContent = retakesGrupos.length ? retakesGrupos.length + " grupo(s)" : "—";
document.getElementById("retakesSemResultados").hidden = visiveis.length > 0;
visiveis.forEach(function (grupo) { alvo.appendChild(retakesCartaoDoGrupo(grupo)); });
}
function retakesCartaoDoGrupo(grupo) {
var cartao = document.createElement("div");
cartao.className = "card"; cartao.setAttribute("role", "listitem");
var titulo = document.createElement("strong"); titulo.textContent = retakesRotuloDoGrupo(grupo);
cartao.appendChild(titulo);
grupo.tomadas.forEach(function (tomada, indice) {
var linha = document.createElement("p");
linha.textContent = "Tentativa " + (indice + 1) + ": " + retakesFormatarTempo(tomada.inicio) + " – " + retakesFormatarTempo(tomada.fim) + " · " + (tomada.texto || "");
linha.style.margin = "2px 0"; cartao.appendChild(linha);
});
if (grupo.evidencias && grupo.evidencias.length) {
var motivos = document.createElement("p");
motivos.textContent = "Evidências: " + grupo.evidencias.map(function (e) { return e.descricao || e.tipo; }).join("; ");
motivos.className = "field-help"; cartao.appendChild(motivos);
}
var acoes = document.createElement("div"); acoes.className = "button-row";
[["confirmar", "Confirmar"], ["rejeitar", "Não é retake"], ["manter", "Manter p/ revisão"], ["ignorar", "Ignorar"]].forEach(function (par) {
var botao = document.createElement("button");
botao.className = "button button-ghost"; botao.type = "button"; botao.textContent = par[1];
botao.onclick = function () { retakesRevisar(grupo.id, par[0]); };
acoes.appendChild(botao);
});
cartao.appendChild(acoes);
return cartao;
}
function retakesAnalisar() {
if (!retakesCasoAtual) { showToast("err", "Selecione um caso do Scanner."); return; }
var artefatos = retakesArtefatosDeAudio(retakesCasoAtual);
if (!artefatos.length) { showToast("err", "O caso não possui transcrição. Execute o Scanner com transcrição primeiro."); return; }
if (retakesProcesso) { showToast("err", "Já existe uma análise em andamento."); return; }
if (!document.getElementById("retakesReaproveitar").checked) {
var pastaRetakes = path.join(retakesRaiz(), retakesCasoAtual, "retakes");
try { if (fs.existsSync(pastaRetakes)) fs.rmSync(pastaRetakes, { recursive: true }); } catch (_) {}
}
var pedido = { acao: "analisar", video_id: retakesCasoAtual, artefatos_de_audio: artefatos,
configuracao: { criterio: document.getElementById("retakesCriterio").value,
usar_analise_visual: document.getElementById("retakesUsarVisual").checked } };
var entrada = path.join(os.tmpdir(), "pedido-retakes-" + Date.now() + ".json");
fs.writeFileSync(entrada, JSON.stringify(pedido, null, 2), "utf-8");
setBusy("btnRetakesAnalisar", true);
taskStart("Retakes", "Preparando análise", null);
document.getElementById("retakesProgressoCard").hidden = false;
document.getElementById("retakesProgressoEtapa").textContent = "Analisando retakes…";
var child = childProcess.spawn(SCANNER_PYTHON_BIN, [RETAKES_ENGINE_SCRIPT, entrada, path.join(retakesRaiz(), retakesCasoAtual)],
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine", stdio: ["ignore", "pipe", "pipe"] });
retakesProcesso = child;
var resto = "";
var erroTecnico = "";
child.stdout.on("data", function (pedaco) {
resto += pedaco.toString(); var linhas = resto.split("\n"); resto = linhas.pop();
linhas.forEach(function (linha) { var evento; try { evento = JSON.parse(linha); } catch (_) { return; } retakesProcessarEvento(evento); });
});
child.stderr.on("data", function (pedaco) { erroTecnico += pedaco.toString(); if (pedaco.toString().trim()) retakesLog(pedaco.toString().trim()); });
child.on("error", function (erro) { retakesProcesso = null; setBusy("btnRetakesAnalisar", false); taskEnd(false, "Engine indisponível", erro.stack || erroTecnico || erro.message); showToast("err", erro.message); });
child.on("close", function (codigo) {
retakesProcesso = null; setBusy("btnRetakesAnalisar", false);
document.getElementById("retakesProgressoCard").hidden = true;
document.getElementById("retakesStatusGeral").textContent = "Analisado";
if (codigo !== 0) taskEnd(false, "Análise falhou", erroTecnico || "A engine terminou com código " + codigo + ".");
});
}
function retakesProcessarEvento(evento) {
if (evento.evento === "progresso") {
taskUpdate({ stage: evento.etapa, pct: evento.percentual });
document.getElementById("retakesProgressoEtapa").textContent = evento.etapa;
document.getElementById("retakesProgressoDetalhe").textContent = evento.percentual + "% concluído";
} else if (evento.evento === "erro") {
showToast("err", evento.mensagem); retakesLog("Erro: " + evento.mensagem);
} else if (evento.evento === "concluido") {
retakesGrupos = (evento.resultado && evento.resultado.grupos) || [];
retakesRenderResultados();
taskEnd(true, retakesGrupos.length + " grupo(s) de retakes");
showToast("ok", "Análise concluída: " + retakesGrupos.length + " grupo(s) encontrado(s).");
}
}
function retakesRevisar(grupoId, acao) {
if (!retakesCasoAtual) return;
var pedido = { acao: "revisar", video_id: retakesCasoAtual, revisao: { grupo_id: grupoId, acao: acao } };
var entrada = path.join(os.tmpdir(), "revisao-retakes-" + Date.now() + ".json");
fs.writeFileSync(entrada, JSON.stringify(pedido), "utf-8");
var child = childProcess.spawn(SCANNER_PYTHON_BIN, [RETAKES_ENGINE_SCRIPT, entrada, path.join(retakesRaiz(), retakesCasoAtual)],
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine", stdio: ["ignore", "pipe", "pipe"] });
child.on("close", function (codigo) {
if (codigo !== 0) { showToast("err", "Não foi possível registrar a revisão."); return; }
var novos = { confirmar: "confirmado", rejeitar: "nao_e_retake", manter: "pendente_de_revisao", ignorar: "ignorado" };
retakesGrupos = retakesGrupos.map(function (g) { return g.id === grupoId ? Object.assign({}, g, { status_de_revisao: novos[acao] }) : g; });
retakesRenderResultados();
showToast("ok", "Revisão registrada.");
});
}
function retakesLog(mensagem) {
var alvo = document.getElementById("retakesLog");
var linha = document.createElement("p"); linha.textContent = mensagem;
alvo.appendChild(linha);
}
// ---- Perfil do Scanner -----------------------------------------------------
// O painel e o Engine compartilham este contrato versionado. O perfil não
// contém caminhos de mídia; estes continuam sendo resolvidos pelo MCP.
var SCANNER_PROFILE_KEY = "mcp_scanner_profile_v1";
var SCANNER_ALL_RESOURCES = ["faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "codigos", "categorias", "estetica", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas"];
var scannerTimeline = null;
var scannerReportPath = null;
var scannerRelatorioExistente = null;
var scannerReprocessamentoPendente = false;
var scannerAnalisesExistentes = [];
var scannerProcesso = null;
var scannerCancelSolicitado = false;
var SCANNER_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_scanner.py";
// `path` e `os` são carregados mais abaixo, junto com o runtime Node do CEP.
// Calcular isso aqui interrompia todo o script antes da inicialização do painel.
function scannerOutputRoot() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); }
function scannerNomeSeguro(nome) { return String(nome || "timeline").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "timeline"; }
function scannerCarregarRelatorioExistente() {
scannerRelatorioExistente = null;
if (!scannerTimeline || !scannerTimeline.name) return;
var caso = scannerNomeSeguro(scannerTimeline.name);
var caminho = path.join(scannerOutputRoot(), caso, caso + "-resultado.json");
try {
if (!fs.existsSync(caminho)) return;
var relatorio = JSON.parse(fs.readFileSync(caminho, "utf-8"));
if (relatorio.artefatos && relatorio.artefatos.audio && relatorio.artefatos.audio.length) { scannerRelatorioExistente = caminho; scannerReportPath = caminho; }
} catch (_) { scannerRelatorioExistente = null; }
}
function scannerListarAnalisesExistentes() {
var seletor = document.getElementById("scannerExistingAnalysis");
var status = document.getElementById("scannerExistingAnalysisStatus");
var info = document.getElementById("scannerExistingAnalysisInfo");
if (!seletor) return;
scannerAnalisesExistentes = [];
try {
fs.readdirSync(scannerOutputRoot()).forEach(function (nomeDaPasta) {
var caminho = path.join(scannerOutputRoot(), nomeDaPasta, nomeDaPasta + "-resultado.json");
if (!fs.existsSync(caminho)) return;
try {
var dados = JSON.parse(fs.readFileSync(caminho, "utf-8"));
var estado = silenceConsultarStatusNoBanco((dados.sequencia || {}).id);
if (estado) scannerAnalisesExistentes.push({ caminho: caminho, dados: dados, estado: estado });
} catch (_) {}
});
} catch (_) {}
scannerAnalisesExistentes.sort(function (a, b) {
try { return fs.statSync(b.caminho).mtimeMs - fs.statSync(a.caminho).mtimeMs; } catch (_) { return 0; }
});
seletor.innerHTML = "";
if (!scannerAnalisesExistentes.length) {
seletor.innerHTML = "";
status.textContent = "Nenhuma análise";
info.textContent = "Execute o Scanner para criar a primeira análise persistida.";
return;
}
scannerAnalisesExistentes.forEach(function (analise, indice) {
var audio = analise.estado.audio || {};
var option = document.createElement("option");
option.value = String(indice);
option.textContent = (analise.dados.sequencia || {}).nome + " · " + audio.total_falas + " falas";
seletor.appendChild(option);
});
seletor.selectedIndex = 0;
scannerSelecionarAnaliseExistente();
}
function scannerSelecionarAnaliseExistente() {
var seletor = document.getElementById("scannerExistingAnalysis");
var analise = scannerAnalisesExistentes[Number(seletor && seletor.value)];
var status = document.getElementById("scannerExistingAnalysisStatus");
var info = document.getElementById("scannerExistingAnalysisInfo");
if (!analise) return;
scannerRelatorioExistente = analise.caminho;
scannerReportPath = analise.caminho;
var audio = analise.estado.audio || {};
status.textContent = (audio.metricas_de_voz_disponiveis ? "Métricas prontas" : "Sem métricas") +
" · " + (audio.diarizacao_disponivel ? "Diarização pronta" : "Sem diarização");
info.textContent = "Transcrição: " + (audio.transcricao_disponivel ? "pronta" : "não encontrada") +
" · " + audio.total_falas + " falas · " + (audio.falantes || []).length + " falante(s).";
scannerAtualizarComplementos();
}
function scannerAtualizarComplementos() {
var relatorio = scannerRelatorioExistente;
var arquivoDiarizacao = document.getElementById("btnScannerDiarization");
var arquivoMetricas = document.getElementById("btnScannerVoiceFeatures");
var statusDiarizacao = document.getElementById("scannerDiarizationStatus");
var statusMetricas = document.getElementById("scannerVoiceFeaturesStatus");
var botaoProcessar = document.getElementById("btnScannerProcess");
if (!relatorio) {
if (arquivoDiarizacao) arquivoDiarizacao.disabled = true;
if (arquivoMetricas) arquivoMetricas.disabled = true;
if (statusDiarizacao) statusDiarizacao.textContent = "Ainda não processado";
if (statusMetricas) statusMetricas.textContent = "Ainda não processado";
return;
}
var dados = {};
try { dados = JSON.parse(fs.readFileSync(relatorio, "utf-8")); } catch (_) {}
var transcricao = dados.perfil && dados.perfil.transcricao || {};
var audio = dados.artefatos && dados.artefatos.audio || [];
var metricas = !!(dados.perfil && dados.perfil.metricas_de_fala) || !!transcricao.metricas_de_fala;
var diarizacao = !!transcricao.diarizacao;
if (arquivoDiarizacao) arquivoDiarizacao.disabled = true;
if (arquivoMetricas) arquivoMetricas.disabled = true;
if (statusDiarizacao) { statusDiarizacao.textContent = diarizacao ? "Disponível" : "Não realizada"; statusDiarizacao.dataset.state = diarizacao ? "done" : "pending"; }
if (statusMetricas) { statusMetricas.textContent = metricas ? "Disponíveis" : "Não realizadas"; statusMetricas.dataset.state = metricas ? "done" : "pending"; }
if (botaoProcessar) { botaoProcessar.disabled = true; botaoProcessar.textContent = "Já processado"; }
}
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) {
if (kind === "audio" && !selected.length) selected = tracks.map(function (track) { return track.index; });
var target = document.getElementById(kind === "video" ? "scannerVideoTracks" : "scannerAudioTracks");
target.innerHTML = "";
if (!tracks.length) { target.textContent = "Nenhuma faixa encontrada."; return; }
tracks.forEach(function (track) { var label = document.createElement("label"); label.className = "checkbox-field"; var input = document.createElement("input"); input.type = "checkbox"; input.setAttribute("data-scanner-track", kind); input.value = track.index; input.checked = selected.indexOf(track.index) >= 0; label.appendChild(input); label.appendChild(document.createTextNode(" " + track.name + " · " + track.numClips + " clipe(s)")); target.appendChild(label); });
}
function scannerUpdateIntervalLabel() {
var valor = Number(document.getElementById("scannerInterval").value);
var texto = valor === 1 ? "1 s/quadro" : valor.toFixed(1).replace(/\.0$/, "") + " s/quadro";
document.getElementById("scannerIntervalValue").textContent = texto;
}
function scannerModoAtual() {
var selecionado = document.querySelector('input[name="scannerMode"]:checked');
return selecionado ? selecionado.value : "som";
}
function scannerAtualizarModo() {
var modo = scannerModoAtual();
var imagem = document.getElementById("scannerImageOptions");
var som = document.getElementById("scannerAudioOptions");
var ajuda = document.getElementById("scannerModeHelp");
var botao = document.getElementById("btnScannerProcess");
if (imagem) imagem.hidden = modo === "som";
if (som) som.hidden = modo === "imagem";
if (ajuda) ajuda.textContent = modo === "som"
? "O Scanner vai ignorar completamente a análise de frames e processar apenas as faixas de áudio."
: modo === "imagem"
? "O Scanner vai ignorar a transcrição e processar apenas frames, cenas e evidências visuais."
: "O Scanner vai executar os dois fluxos e manter os resultados separados no relatório.";
if (botao) botao.textContent = modo === "som" ? "Processar áudio" : modo === "imagem" ? "Analisar imagem" : "Processar áudio + imagem";
}
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, modo_de_analise: scannerModoAtual(), metricas_de_fala: document.getElementById("scannerVoiceFeatures").checked, intervalo_em_segundos: Number(document.getElementById("scannerInterval").value), resolucao_maxima: Number(document.getElementById("scannerResolution").value), faixas_de_video: scannerSelectedTracks("video"), faixas_de_audio: scannerSelectedTracks("audio"), recursos_vision: SCANNER_ALL_RESOURCES.filter(function (item) { return enabled.indexOf(item) >= 0; }), detectar_cenas: document.getElementById("scannerScenes").checked, analisar_continuidade: document.getElementById("scannerContinuity").checked, preparar_reenquadramento: document.getElementById("scannerReframe").checked, interpretar_cena: document.getElementById("scannerInterpret").checked, transcricao: { modelo: modelo, caminho_modelo: opcao && opcao.dataset.path || "", idioma: document.getElementById("scannerTranscribeLanguage").value, diarizacao: document.getElementById("scannerDiarization").checked, usar_hugging_face: !!loadHfToken() } };
}
function saveScannerProfile() {
try { var profile = scannerProfileFromScreen(); localStorage.setItem(SCANNER_PROFILE_KEY, JSON.stringify(profile)); document.getElementById("scannerProfileStatus").textContent = "Perfil salvo: " + profile.intervalo_em_segundos + " s por quadro; " + profile.faixas_de_video.length + " faixa(s) de vídeo."; showToast("ok", "Perfil do Scanner salvo."); } catch (error) { showToast("err", error.message || String(error)); }
}
function loadScannerProfile() {
var saved; try { saved = JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY) || "null"); } catch (_) { saved = null; }
if (!saved) { scannerUpdateIntervalLabel(); scannerAtualizarModo(); return; }
var modo = saved.modo_de_analise || "som";
var seletorDeModo = document.querySelector('input[name="scannerMode"][value="' + modo + '"]');
if (seletorDeModo) seletorDeModo.checked = true;
document.getElementById("scannerInterval").value = String(saved.intervalo_em_segundos || 1);
scannerUpdateIntervalLabel();
document.getElementById("scannerResolution").value = String(saved.resolucao_maxima != null ? saved.resolucao_maxima : 1080);
if (scannerTimeline) { scannerRenderTracks("video", scannerTimeline.videoTracks, saved.faixas_de_video || []); scannerRenderTracks("audio", scannerTimeline.audioTracks, saved.faixas_de_audio || []); }
var resources = saved.recursos_vision || SCANNER_ALL_RESOURCES;
Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]")).forEach(function (item) { item.checked = resources.indexOf(item.getAttribute("data-scanner-resource")) >= 0; });
document.getElementById("scannerScenes").checked = saved.detectar_cenas !== false; document.getElementById("scannerContinuity").checked = saved.analisar_continuidade !== false; document.getElementById("scannerReframe").checked = !!saved.preparar_reenquadramento; document.getElementById("scannerInterpret").checked = saved.interpretar_cena !== false;
document.getElementById("scannerVoiceFeatures").checked = !!saved.metricas_de_fala;
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(); }
scannerAtualizarModo();
}
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--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");
var childProcess = nodeRequire("child_process");
function defaultBridgeDirectory() {
try {
var nodeProcess = nodeRequire("process");
var configured = nodeProcess && nodeProcess.env && nodeProcess.env.PREMIERE_TEMP_DIR;
if (typeof configured === "string" && configured.trim()) return configured.trim();
} catch (e) {
// The panel still has a safe OS temporary-directory fallback.
}
return path.join(os.tmpdir(), "premiere-mcp-bridge");
}
tempDir = defaultBridgeDirectory();
var latestUpdate = null;
var UPDATE_STATUS_STORAGE_KEY = "mcp_bridge_desktop_update_status_path";
var MAX_UPDATE_RESPONSE_BYTES = 64 * 1024;
function getPerUserGlobalInstall() {
try {
var nodeProcess = nodeRequire("process");
var appData = nodeProcess && nodeProcess.env && nodeProcess.env.APPDATA;
if (typeof appData !== "string" || !appData.trim()) return null;
var npmDirectory = path.resolve(appData, "npm");
var commandPath = path.resolve(npmDirectory, "premiere-pro-mcp.cmd");
var packagePath = path.resolve(npmDirectory, "node_modules", "premiere-pro-mcp", "package.json");
var relative = path.relative(npmDirectory, commandPath);
var packageRelative = path.relative(npmDirectory, packagePath);
if (
!relative ||
!packageRelative ||
relative.indexOf(".." + path.sep) === 0 ||
packageRelative.indexOf(".." + path.sep) === 0 ||
path.isAbsolute(relative) ||
path.isAbsolute(packageRelative) ||
!fs.existsSync(commandPath) ||
!fs.existsSync(packagePath)
) return null;
var packageMetadata = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
var serverVersion = MCPBridgeUpdater.normalizeVersion(packageMetadata && packageMetadata.version);
if (!serverVersion) return null;
return { commandPath: commandPath, serverVersion: serverVersion };
} catch (e) {
return null;
}
}
function getPerUserGlobalCommand() {
var install = getPerUserGlobalInstall();
return install ? install.commandPath : null;
}
function saveUpdateStatusPath(statusPath) {
try {
localStorage.setItem(UPDATE_STATUS_STORAGE_KEY, statusPath);
} catch (e) {}
}
function readScheduledUpdateStatus() {
var statusPath = "";
try {
statusPath = localStorage.getItem(UPDATE_STATUS_STORAGE_KEY) || "";
} catch (e) {
return null;
}
if (!statusPath || !path.isAbsolute(statusPath) || !fs.existsSync(statusPath)) return null;
try {
var status = JSON.parse(fs.readFileSync(statusPath, "utf-8"));
var validStates = ["waiting_for_premiere", "updating", "complete", "failed"];
if (
!status ||
status.schemaVersion !== "premiere-pro-mcp.desktop-update.v1" ||
validStates.indexOf(status.state) === -1
) return null;
return status;
} catch (e) {
return null;
}
}
function ensureDir(dir) {
try {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
}
} catch (e) {
log("Error creating dir: " + e.message, "err");
}
}
function listCommandFiles() {
try {
if (!fs.existsSync(tempDir)) return [];
var files = fs.readdirSync(tempDir);
return files
.filter(function (f) { return f.indexOf("cmd_") === 0 && f.slice(-4) === ".jsx"; })
.sort(); // process in order
} catch (e) {
return [];
}
}
function readFile(filePath) {
try {
return fs.readFileSync(filePath, "utf-8");
} catch (e) {
return null;
}
}
function writeFile(filePath, content) {
try {
fs.writeFileSync(filePath, content, "utf-8");
return true;
} catch (e) {
log("Error writing " + filePath + ": " + e.message, "err");
return false;
}
}
// Publish responses atomically so the MCP process never sees a partially-written
// JSON file. The staging suffix is not a response filename the server will read.
function writeResponseFile(filePath, content) {
var stagedPath = filePath + ".staged";
try {
fs.writeFileSync(stagedPath, content, "utf-8");
fs.renameSync(stagedPath, filePath);
return true;
} catch (e) {
deleteFile(stagedPath);
log("Error publishing " + filePath + ": " + e.message, "err");
return false;
}
}
function deleteFile(filePath) {
try {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
} catch (e) {}
}
// The heartbeat carries only protocol state. It is published by rename so a
// server never observes partial JSON, and an older server can ignore it.
function writeBridgeHeartbeat() {
if (!tempDir) return;
var heartbeatPath = path.join(tempDir, "bridge-heartbeat.json");
var stagedPath = heartbeatPath + "." + ENGINE_ID + ".staged";
try {
fs.writeFileSync(stagedPath, JSON.stringify({
protocolVersion: 1,
state: bridgeRunning ? "running" : "waiting"
}), "utf-8");
fs.renameSync(stagedPath, heartbeatPath);
} catch (e) {
deleteFile(stagedPath);
}
}
function startBridgeHeartbeat() {
if (heartbeatInterval) clearInterval(heartbeatInterval);
writeBridgeHeartbeat();
heartbeatInterval = setInterval(writeBridgeHeartbeat, HEARTBEAT_MS);
}
function stopBridgeHeartbeat() {
if (heartbeatInterval) clearInterval(heartbeatInterval);
heartbeatInterval = null;
// Keep the last heartbeat in place. Its age lets newer servers diagnose a
// stopped connector, while concurrent visible/headless panels stay isolated.
}
// ---- Script Execution ----
function executeScript(script, callback) {
// Script is already wrapped in an IIFE by the MCP server's buildScript(),
// so we pass it directly to avoid double-wrapping.
cs.evalScript(script, function (result) {
callback(result);
});
}
// ---- Command Processing ----
function processCommands() {
if (commandInFlight) return;
var cmdFiles = listCommandFiles();
// Premiere's scripting engine is stateful. Starting every discovered command
// at once lets overlapping edits race each other and overload the host. The
// atomic claim below still prevents duplicate work across the visible and
// headless panels, while this panel dispatches strictly one command at a time.
if (cmdFiles.length > 0) processOneCommand(cmdFiles[0]);
}
// Both the visible panel and the headless auto-start instance run this file.
// A rename is atomic on the same volume, so whichever engine renames first owns
// the command; the loser's rename throws and it skips the file.
var ENGINE_ID = Math.random().toString(36).slice(2, 8);
var commandInFlight = false;
function processOneCommand(cmdFileName) {
var cmdFilePath = path.join(tempDir, cmdFileName);
var claimPath = cmdFilePath + "." + ENGINE_ID + ".claimed";
try {
fs.renameSync(cmdFilePath, claimPath);
} catch (e) {
return; // another engine claimed this command
}
var script = readFile(claimPath);
deleteFile(claimPath);
if (!script) {
log("Failed to read: " + cmdFileName, "err");
return;
}
commandInFlight = true;
// Derive response filename: cmd_12345.jsx -> res_12345.json
var id = cmdFileName.replace("cmd_", "").replace(".jsx", "");
var resFilePath = path.join(tempDir, "res_" + id + ".json");
log("Executing: " + cmdFileName + " (" + script.length + " chars)", "cmd");
// While evalScript is in flight, heartbeat a busy file so the MCP server can
// tell "script still running (modal dialog?)" apart from "plugin not running".
// Only starts after 2s, so fast commands never touch the extra file.
var busyFilePath = path.join(tempDir, "busy_" + id + ".json");
var startedAt = new Date().getTime();
var busyTimer = setInterval(function () {
writeFile(busyFilePath, '{"id":"' + id + '","elapsedMs":' + (new Date().getTime() - startedAt) + "}");
}, 2000);
executeScript(script, function (result) {
clearInterval(busyTimer);
deleteFile(busyFilePath);
commandCount++;
document.getElementById("cmdCount").textContent = commandCount;
var response;
try {
// ExtendScript returns a string; try to parse it as JSON
if (result && result !== "undefined" && result !== "null") {
// Check if it's already valid JSON
var parsed = JSON.parse(result);
response = JSON.stringify(parsed);
log("Result: OK", "ok");
} else {
// An empty result means evalScript gave us nothing back. That is a bridge
// failure, not a successful command with no data — reporting it as "OK" is
// what made this so hard to diagnose. Say so.
response = JSON.stringify({
success: false,
error:
"The bridge received an empty result from evalScript (got " +
(typeof result) +
"). The script may not have run. If every command does this, the CEP panel is stale — " +
"close and reopen it (a reload is not enough), or reinstall the extension.",
});
log("Result: EMPTY — evalScript returned nothing (see response file)", "err");
}
} catch (e) {
// If result isn't JSON, wrap it
if (result && result.indexOf("Error") === 0) {
response = JSON.stringify({ success: false, error: result });
log("Result: " + result, "err");
} else {
response = JSON.stringify({ success: true, data: result });
log("Result: OK (raw)", "ok");
}
}
writeResponseFile(resFilePath, response);
commandInFlight = false;
// Continue without waiting for the next poll interval, preserving FIFO
// ordering while minimizing queue handoff latency.
if (bridgeRunning) processCommands();
});
}
// ---- Bridge Control ----
function startBridge() {
tempDir = document.getElementById("tempDir").value.trim();
if (!tempDir) {
log("Please set a temp directory", "err");
document.getElementById("tempDir").focus();
return;
}
ensureDir(tempDir);
bridgeRunning = true;
startBridgeHeartbeat();
setStatus("waiting", "Connector running");
log("Connector started and ready for safe checks.", "ok");
document.getElementById("btnStart").disabled = true;
document.getElementById("btnStop").disabled = false;
refreshConnectionCenter();
pollInterval = setInterval(function () {
if (bridgeRunning) processCommands();
}, POLL_MS);
}
function stopBridge() {
bridgeRunning = false;
writeBridgeHeartbeat();
stopBridgeHeartbeat();
if (pollInterval) clearInterval(pollInterval);
pollInterval = null;
setStatus("", "Stopped");
refreshConnectionCenter();
log("Bridge stopped");
document.getElementById("btnStart").disabled = false;
document.getElementById("btnStop").disabled = true;
}
function saveTempDir() {
tempDir = document.getElementById("tempDir").value.trim();
log("Temp directory saved: " + tempDir);
showToast("ok", "Pasta da ponte salva.");
// Persist via localStorage
try {
localStorage.setItem("mcp_bridge_temp_dir", tempDir);
} catch (e) {}
}
// ---- Connector Updates ----
function setUpdateUI(title, detail, buttonText, disabled) {
document.getElementById("updateTitle").textContent = title;
document.getElementById("updateDetail").textContent = detail;
var button = document.getElementById("btnUpdate");
button.textContent = buttonText;
button.disabled = !!disabled;
}
function updateInstructionUrl() {
return MCPBridgeUpdater.RELEASES_URL;
}
function openTrustedUpdateInstructions() {
var url = updateInstructionUrl();
if (!MCPBridgeUpdater.isTrustedDownloadUrl(url)) {
showUpdateCheckError("The update instructions link was not trusted.");
return;
}
try {
var childProcess = nodeRequire("child_process");
var command =
os.platform() === "win32"
? ["cmd.exe", ["/d", "/s", "/c", "start", "", url]]
: ["open", [url]];
var child = childProcess.spawn(command[0], command[1], {
detached: true,
stdio: "ignore",
});
child.unref();
} catch (e) {
showUpdateCheckError("Could not open the update instructions. Try again.");
}
}
function restoreScheduledUpdateStatus() {
var status = readScheduledUpdateStatus();
if (!status) return false;
if (status.state === "complete") {
setUpdateUI(
"Update complete",
"Restart your MCP client, then use Verify Premiere connection before editing.",
"Check again",
false
);
return true;
}
if (status.state === "failed") {
setUpdateUI(
"Update needs attention",
"Nothing was changed in your projects. Check the update command or retry after Premiere closes.",
"Check again",
false
);
return true;
}
setUpdateUI(
"Update scheduled",
status.state === "updating"
? "The global MCP server and connector are being updated. Keep Premiere closed."
: "Quit Premiere Pro. The updater will begin after it fully closes.",
"Scheduled",
true
);
return true;
}
function checkForUpdates() {
latestUpdate = null;
var globalInstall = os.platform() === "win32" ? getPerUserGlobalInstall() : null;
var responseTooLarge = false;
setUpdateUI(
"Version " + MCPBridgeUpdater.CURRENT_VERSION,
"Checking for updates…",
"Checking…",
true
);
var request = https.get(
MCPBridgeUpdater.LATEST_PACKAGE_API,
{
headers: {
Accept: "application/vnd.npm.install-v1+json",
"User-Agent": "premiere-pro-mcp-connector/" + MCPBridgeUpdater.CURRENT_VERSION,
},
},
function (response) {
var body = "";
response.setEncoding("utf8");
response.on("data", function (chunk) {
if (body.length + chunk.length > MAX_UPDATE_RESPONSE_BYTES) {
responseTooLarge = true;
request.destroy(new Error("npm registry update record was unexpectedly large."));
return;
}
body += chunk;
});
response.on("end", function () {
if (responseTooLarge) return;
if (response.statusCode !== 200) {
showUpdateCheckError("Could not check npm (HTTP " + response.statusCode + ").");
return;
}
try {
var update = MCPBridgeUpdater.updateStateFromPackageRecord(
MCPBridgeUpdater.CURRENT_VERSION,
JSON.parse(body)
);
var serverUpdateAvailable = Boolean(
globalInstall &&
MCPBridgeUpdater.compareVersions(update.latestVersion, globalInstall.serverVersion) > 0
);
var needsUpdate = update.updateAvailable || serverUpdateAvailable;
if (needsUpdate) {
latestUpdate = {
version: update.latestVersion,
};
if (os.platform() === "win32" && globalInstall) {
var versionSummary =
"Server " + globalInstall.serverVersion + ", connector " + MCPBridgeUpdater.CURRENT_VERSION + ". ";
setUpdateUI(
"Version " + update.latestVersion + " is available",
versionSummary + "Update both together after you close Premiere.",
"Update after quit",
false
);
} else if (os.platform() === "win32") {
setUpdateUI(
"Version " + update.latestVersion + " is available",
"A global npm install was not found. This panel will not modify a source checkout.",
"Open instructions",
false
);
} else {
setUpdateUI(
"Version " + update.latestVersion + " is available",
"Open the matching release, then update your local server using the documented install path.",
"Open instructions",
false
);
}
} else {
var currentDetail = globalInstall
? "Server " + globalInstall.serverVersion + " and connector " + MCPBridgeUpdater.CURRENT_VERSION + " are current."
: "Your connector release is current. This check does not alter your projects or MCP client configuration.";
setUpdateUI(
"Version " + MCPBridgeUpdater.CURRENT_VERSION,
currentDetail,
"Check again",
false
);
}
} catch (e) {
showUpdateCheckError("npm returned an unreadable package record.");
}
});
}
);
request.setTimeout(10000, function () {
request.destroy(new Error("Update check timed out"));
});
request.on("error", function () {
showUpdateCheckError(
responseTooLarge ? "npm returned an unexpectedly large package record." : "Unable to check while offline."
);
});
}
function showUpdateCheckError(message) {
setUpdateUI(
"Version " + MCPBridgeUpdater.CURRENT_VERSION,
message,
"Check again",
false
);
}
function handleUpdateClick() {
if (!latestUpdate) {
checkForUpdates();
return;
}
if (os.platform() !== "win32") {
openTrustedUpdateInstructions();
return;
}
var cliPath = getPerUserGlobalCommand();
if (!cliPath) {
openTrustedUpdateInstructions();
return;
}
var confirmation =
"Update Premiere MCP to " + latestUpdate.version + " after Premiere Pro fully closes?\n\n" +
"This updates only the per-user global MCP server and its connector. " +
"It does not change your projects or MCP client configuration, and it will not force Premiere to close.";
if (typeof window.confirm === "function" && !window.confirm(confirmation)) return;
try {
var childProcess = nodeRequire("child_process");
var nodeCrypto = nodeRequire("crypto");
var scheduled = MCPBridgeUpdater.scheduleWindowsGlobalUpdate({
cliPath: cliPath,
runtime: {
fs: fs,
path: path,
os: os,
childProcess: childProcess,
crypto: nodeCrypto,
},
});
saveUpdateStatusPath(scheduled.statusPath);
setUpdateUI(
"Update scheduled",
"Quit Premiere Pro. The updater will refresh the global server and connector after it fully closes.",
"Scheduled",
true
);
} catch (e) {
showUpdateCheckError("Could not schedule the local update. No files were changed.");
}
}
// ---- Silence Cut tab ----
// Fixed local machine paths for the Whisper pipeline (this rig's setup).
var SILENCE_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python";
var SILENCE_TRANSCRIBE_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/transcribe_cli.py";
var DEFAULT_MODELS_PATH = "/Volumes/Merongo/SISTEMAS/WHISPERX/whisper/models";
var SILENCE_OUTPUT_ROOT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/output";
var SILENCE_DOWNLOAD_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/download_model_cli.py";
var SILENCE_DIARIZE_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/diarize_cli.py";
var VOICE_FEATURES_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/voice_features_cli.py";
var EDITORIAL_ACTIONS_APPLY_SCRIPT_FALLBACK = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/scripts/apply-editorial-actions.mjs";
// Catálogo dos modelos compatíveis com faster-whisper e aceitos pelo pipeline
// local. As variantes destilada e turbo também são suportadas pelo carregador,
// embora tenham organizações diferentes no Hugging Face.
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 },
{ value: "large-v3-turbo", label: "Large v3 Turbo", approxMB: 1620 },
{ value: "distil-large-v3", label: "Distil Large v3", approxMB: 1500 },
];
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 = {
sequenceId: null,
sequenceName: null,
mediaPath: null,
audioCachePath: null,
fileBaseName: null,
outputDir: null,
transcriptPath: null,
transcribeModel: null,
cachedTranscriptPath: null,
jsonOutputPath: null,
tipoVideoId: null,
tipoVideo: null,
speakers: [],
speakerTurns: [],
};
function atualizarCheckDaAnalise(id, disponivel, textoDisponibilidade) {
var item = document.getElementById(id);
if (!item) return;
item.setAttribute("data-state", disponivel ? "done" : "missing");
var estado = item.querySelector("em");
if (estado) estado.textContent = textoDisponibilidade || (disponivel ? "Carregado" : "Não disponível");
}
function renderizarTiposDeVideoParaEdicao() {
var seletor = document.getElementById("silenceTipoVideoSelect");
if (!seletor) return;
var selecionado = silenceState.tipoVideoId || "";
seletor.innerHTML = '';
tiposVideoLer().filter(function (tipo) { return tipo.ativo !== false; }).forEach(function (tipo) {
var opcao = document.createElement("option");
opcao.value = tipo.id;
opcao.textContent = tipo.nome;
opcao.selected = tipo.id === selecionado;
seletor.appendChild(opcao);
});
seletor.disabled = !silenceState.sequenceId;
}
function silenceSelecionarTipoVideo() {
var seletor = document.getElementById("silenceTipoVideoSelect");
var tipo = tiposVideoLer().filter(function (item) { return item.id === seletor.value; })[0] || null;
silenceState.tipoVideoId = tipo ? tipo.id : null;
silenceState.tipoVideo = tipo;
if (!tipo) {
document.getElementById("silenceTipoVideoInfo").textContent = "Selecione um tipo cadastrado para continuar.";
setStep(1, "ready", "Selecione um tipo");
document.getElementById("btnBuildPlan").disabled = true;
return;
}
document.getElementById("silenceTipoVideoInfo").textContent = tipo.nome + " · instruções serão incluídas no JSON para a IA.";
setStep(1, "done", "Selecionado");
document.getElementById("btnBuildPlan").disabled = false;
silenceLog("Tipo de vídeo selecionado: " + tipo.nome + ".", "ok");
}
function tipoVideoResumoParaJson(tipo) {
if (!tipo) return null;
return {
id: tipo.id,
nome: tipo.nome,
categoria: tipo.categoria,
objetivo: { principal: tipo.objetivo, secundario: tipo.objetivo_secundario, publico: tipo.publico, mensagem: tipo.mensagem },
narrativa: { tipo: tipo.tipo_narrativa, foco: tipo.foco_narrativo, storytelling: tipo.storytelling, estrutura: tipo.estrutura },
emocao: { objetivo: tipo.emocao, intensidade: tipo.intensidade, tom: tipo.tom, construcao: tipo.construcao_emocao },
formato: { canais: tipo.canais, proporcao: tipo.proporcao, duracao_minima_segundos: tipo.duracao_minima, duracao_maxima_segundos: tipo.duracao_maxima_segundos, ritmo: tipo.ritmo },
prioridades: tipo.prioridades,
elementos_de_edicao: { broll: tipo.usa_broll, legendas: tipo.usa_legendas, textos: tipo.usa_textos, graficos: tipo.usa_graficos, zoom: tipo.usa_zoom, cortes: tipo.cortes, orientacao_de_broll: tipo.broll, textos_e_legendas: tipo.texto },
audio: { musica: tipo.usa_musica, intensidade_da_musica: tipo.intensidade_musica, prioridade_da_voz: tipo.prioridade_voz, tratamento: tipo.som },
regras_de_edicao: tipo.diretriz,
restricoes: tipo.restricoes,
criterios_de_qualidade: tipo.qualidade
};
}
function silenceLocalizarUltimoRelatorioDoScanner() {
var raiz = scannerOutputRoot();
var candidatos = [];
try {
fs.readdirSync(raiz).forEach(function (nomeDaPasta) {
var pasta = path.join(raiz, nomeDaPasta);
var relatorio = path.join(pasta, nomeDaPasta + "-resultado.json");
if (fs.existsSync(relatorio)) candidatos.push(relatorio);
});
} catch (_) {}
candidatos.sort(function (a, b) {
try { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; } catch (_) { return 0; }
});
return candidatos[0] || null;
}
function silenceConverterRelatorioDoScannerParaTranscricao(caminhoDoRelatorio, dadosDoRelatorio) {
var segmentos = [];
(dadosDoRelatorio.artefatos && dadosDoRelatorio.artefatos.audio || []).forEach(function (nomeDoArquivo) {
var caminhoDoAudio = path.join(path.dirname(caminhoDoRelatorio), nomeDoArquivo);
if (!fs.existsSync(caminhoDoAudio)) return;
var dadosDoAudio;
try { dadosDoAudio = JSON.parse(fs.readFileSync(caminhoDoAudio, "utf-8")); } catch (_) { return; }
(dadosDoAudio.segmentos_por_clipe || []).forEach(function (grupo) {
(grupo.segmentos || []).forEach(function (segmento) {
segmentos.push({
start: Number(segmento.inicio), end: Number(segmento.fim),
inicio: Number(segmento.inicio), fim: Number(segmento.fim),
text: segmento.texto || "", texto: segmento.texto || "",
confidence: segmento.confianca, confianca: segmento.confianca,
speaker: segmento.falante, falante: segmento.falante,
caracteristicas_acusticas: segmento.caracteristicas_acusticas || {},
words: segmento.palavras || [], palavras: segmento.palavras || [],
});
});
});
});
segmentos.sort(function (a, b) { return a.start - b.start; });
return { version: 1, segments: segmentos, origem: "scanner" };
}
function silenceConsultarStatusNoBanco(videoId) {
if (!videoId || !childProcess.spawnSync) return null;
try {
var resposta = childProcess.spawnSync(
SCANNER_PYTHON_BIN,
[ANALISE_STATUS_SCRIPT, videoId, "--banco", "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db"],
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", encoding: "utf-8", maxBuffer: 1024 * 1024 }
);
if (resposta.status !== 0) return null;
return JSON.parse(String(resposta.stdout || "{}"));
} catch (_) { return null; }
}
function silenceConsultarPreviewDeFalantes(videoId) {
if (!videoId || !childProcess.spawnSync) return null;
try {
var resposta = childProcess.spawnSync(
SCANNER_PYTHON_BIN,
[ANALISE_PREVIEW_SCRIPT, videoId, "--banco", "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db"],
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", encoding: "utf-8", maxBuffer: 4 * 1024 * 1024 }
);
if (resposta.status !== 0) return null;
var dados = JSON.parse(String(resposta.stdout || "{}"));
return Array.isArray(dados.intervalos) ? dados.intervalos : null;
} catch (_) { return null; }
}
function silenceLocalizarAudioParaFalantes(pasta, dadosDoRelatorio) {
var encontrados = [];
var nomesDeMidia = [];
function adicionarNomeDeMidia(nome) {
if (!nome) return;
var base = path.parse(String(nome)).name;
if (base && nomesDeMidia.indexOf(base) < 0) nomesDeMidia.push(base);
}
adicionarNomeDeMidia(silenceState.mediaPath);
var artefatosDeAudio = dadosDoRelatorio && dadosDoRelatorio.artefatos && dadosDoRelatorio.artefatos.audio || [];
artefatosDeAudio.forEach(function (nomeDoArtefato) {
try {
var caminhoDoArtefato = path.join(pasta, nomeDoArtefato);
var dadosDoAudio = JSON.parse(fs.readFileSync(caminhoDoArtefato, "utf-8"));
(dadosDoAudio.faixa && dadosDoAudio.faixa.clipes || []).forEach(function (clipe) {
adicionarNomeDeMidia(clipe.nome || clipe.arquivo || clipe.sourceFile);
});
} catch (_) {}
});
function visitar(caminho) {
if (encontrados.length) return;
try {
fs.readdirSync(caminho).forEach(function (nome) {
if (encontrados.length) return;
var item = path.join(caminho, nome);
var estatisticas = fs.statSync(item);
if (estatisticas.isDirectory()) visitar(item);
else if (/\.wav$/i.test(nome)) encontrados.push(item);
});
} catch (_) {}
}
visitar(pasta);
if (encontrados.length) return encontrados[0];
for (var indice = 0; indice < nomesDeMidia.length; indice++) {
var pastaDaTranscricao = path.join(SILENCE_OUTPUT_ROOT, nomesDeMidia[indice]);
var caminhoDoAudio = path.join(pastaDaTranscricao, nomesDeMidia[indice] + ".wav");
if (fs.existsSync(caminhoDoAudio)) return caminhoDoAudio;
}
return null;
}
function silenceLocalizarMidiaNoRelatorio(caminhoDoRelatorio, dadosDoRelatorio) {
var arquivosDeAudio = dadosDoRelatorio.artefatos && dadosDoRelatorio.artefatos.audio || [];
for (var indice = 0; indice < arquivosDeAudio.length; indice++) {
try {
var dadosDoAudio = JSON.parse(fs.readFileSync(path.join(path.dirname(caminhoDoRelatorio), arquivosDeAudio[indice]), "utf-8"));
var clipes = dadosDoAudio.faixa && dadosDoAudio.faixa.clipes || [];
for (var indiceDoClipe = 0; indiceDoClipe < clipes.length; indiceDoClipe++) {
if (clipes[indiceDoClipe].sourceFile) return clipes[indiceDoClipe].sourceFile;
}
} catch (_) {}
}
return null;
}
function silenceLoadScannerAnalysis() {
var status = document.getElementById("scannerAnalysisStatus");
var info = document.getElementById("scannerAnalysisInfo");
setBusy("btnLoadScannerAnalysis", true);
setBusy("btnRefinarLoad", true);
var caminhoDoRelatorio = silenceLocalizarUltimoRelatorioDoScanner();
if (!caminhoDoRelatorio) {
setBusy("btnLoadScannerAnalysis", false);
setBusy("btnRefinarLoad", false);
status.textContent = "Não encontrada";
info.textContent = "Execute o Scanner primeiro para criar uma análise reutilizável.";
showToast("err", info.textContent);
return;
}
try {
var dadosDoRelatorio = JSON.parse(fs.readFileSync(caminhoDoRelatorio, "utf-8"));
var sequencia = dadosDoRelatorio.sequencia || {};
silenceState.sequenceId = sequencia.id || null;
silenceState.sequenceName = sequencia.nome || path.basename(caminhoDoRelatorio);
silenceState.mediaPath = silenceLocalizarMidiaNoRelatorio(caminhoDoRelatorio, dadosDoRelatorio);
silenceState.fileBaseName = path.basename(path.dirname(caminhoDoRelatorio));
silenceState.outputDir = path.dirname(caminhoDoRelatorio);
silenceState.audioCachePath = silenceLocalizarAudioParaFalantes(silenceState.outputDir, dadosDoRelatorio);
silenceState.transcriptPath = path.join(silenceState.outputDir, "transcricao-do-scanner.json");
silenceState.cachedTranscriptPath = null;
silenceState.tipoVideoId = null;
silenceState.tipoVideo = null;
fs.writeFileSync(silenceState.transcriptPath, JSON.stringify(
silenceConverterRelatorioDoScannerParaTranscricao(caminhoDoRelatorio, dadosDoRelatorio), null, 2), "utf-8");
var transcricao = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
var quantidade = transcricao.segments.length;
var statusDoBanco = silenceConsultarStatusNoBanco(silenceState.sequenceId);
if (!statusDoBanco || !statusDoBanco.audio) throw new Error("A análise não foi encontrada no banco de dados.");
var audioDoBanco = statusDoBanco.audio;
var falantes = audioDoBanco.falantes || [];
var temTranscricao = quantidade > 0;
var temDiarizacao = !!audioDoBanco.diarizacao_disponivel || falantes.length > 0;
var temMetricasDeVoz = !!audioDoBanco.metricas_de_voz_disponiveis;
atualizarCheckDaAnalise("scannerCheckTranscription", temTranscricao, temTranscricao ? "Carregado" : "Não encontrada");
atualizarCheckDaAnalise("scannerCheckDiarization", temDiarizacao, temDiarizacao ? "Carregado" : "Não realizada");
atualizarCheckDaAnalise("scannerCheckVoiceFeatures", temMetricasDeVoz, temMetricasDeVoz ? "Carregado" : "Não realizada");
var turnos = transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) {
return { speaker: segmento.falante, start: segmento.start, end: segmento.end };
});
renderSpeakerConfiguration(falantes, turnos);
atualizarTelaRefinar(falantes, audioDoBanco);
status.textContent = "Carregada";
info.textContent = silenceState.sequenceName + " · " + quantidade + " trecho(s) · " +
(audioDoBanco.metricas_de_voz_disponiveis ? "métricas disponíveis" : "métricas não realizadas") + " · " +
(audioDoBanco.diarizacao_disponivel ? falantes.length + " falante(s)" : "diarização não realizada");
setStep(3, "ready", "Pronto");
setStep(1, "ready", "Selecione um tipo");
setStep(4, "locked", "Selecione o tipo");
setStep(5, "locked", "Bloqueado");
renderizarTiposDeVideoParaEdicao();
document.getElementById("btnBuildPlan").disabled = true;
// btnDiarize/btnVoiceFeatures/silenceDiarizeInfo/silenceVoiceFeaturesInfo ficavam no
// step3 (legado, comentado em 2026-09-09 no index.html) — checagem defensiva para não
// quebrar este fluxo (ativo) se os elementos não existirem mais no DOM.
var elBtnDiarize = document.getElementById("btnDiarize");
if (elBtnDiarize) elBtnDiarize.disabled = false;
var elBtnVoiceFeatures = document.getElementById("btnVoiceFeatures");
if (elBtnVoiceFeatures) elBtnVoiceFeatures.disabled = false;
var elDiarizeInfo = document.getElementById("silenceDiarizeInfo");
if (elDiarizeInfo) elDiarizeInfo.textContent = temDiarizacao
? falantes.length + " falante(s) disponíveis no banco de dados."
: "Diarização não realizada nesta análise do Scanner. Use Refazer se necessário.";
var elVoiceFeaturesInfo = document.getElementById("silenceVoiceFeaturesInfo");
if (elVoiceFeaturesInfo) elVoiceFeaturesInfo.textContent = temMetricasDeVoz
? (audioDoBanco.falas_com_metricas || 0) + " trecho(s) com métricas de voz no banco."
: "Métricas de voz não realizadas nesta análise do Scanner. Use Refazer se necessário.";
silenceLog("Análise do Scanner carregada: " + quantidade + " trecho(s).", "ok");
showToast("ok", "Análise do Scanner carregada.");
} catch (erro) {
status.textContent = "Erro";
info.textContent = "Não foi possível ler a análise: " + erro.message;
showToast("err", info.textContent);
}
setBusy("btnLoadScannerAnalysis", false);
setBusy("btnRefinarLoad", false);
}
function refinarAoAbrir() {
var info = document.getElementById("refinarInfo");
if (!info || !silenceState.sequenceId) return;
info.textContent = silenceState.sequenceName + " · " + (silenceState.speakers.length
? silenceState.speakers.length + " falante(s) carregado(s)."
: "Nenhum falante carregado.");
}
function refinarCarregarAnalise() {
silenceLoadScannerAnalysis();
}
function atualizarTelaRefinar(falantes, audioDoBanco) {
var status = document.getElementById("refinarStatus");
var info = document.getElementById("refinarInfo");
var vazio = document.getElementById("refinarSpeakerEmpty");
var count = document.getElementById("refinarSpeakerCount");
var save = document.getElementById("btnRefinarSave");
if (!status || !info || !vazio || !count) return;
var quantidade = Array.isArray(falantes) ? falantes.length : 0;
status.textContent = quantidade ? "Pronto" : "Sem diarização";
info.textContent = silenceState.sequenceName + " · " + (audioDoBanco && audioDoBanco.diarizacao_disponivel
? quantidade + " falante(s) detectado(s)."
: "A transcrição está disponível, mas a diarização não foi realizada.");
count.textContent = quantidade ? quantidade + (quantidade === 1 ? " falante detectado" : " falantes detectados") : "Nenhum falante carregado";
vazio.hidden = quantidade > 0;
if (save) save.disabled = quantidade === 0;
}
var speakerAudioPlayer = null;
var speakerAudioQueue = null;
var speakerTimelineQueue = null;
var speakerTimelineTimer = null;
var speakerTimelineToken = 0;
// ---- 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 formatSequenceDuration(seconds) {
var total = Math.max(0, Math.round(Number(seconds) || 0));
var minutes = Math.floor(total / 60);
var remainder = total % 60;
return minutes + ":" + String(remainder).padStart(2, "0");
}
function silenceListSequences() {
setBusy("btnDetectClip", true);
setStep(1, "running", "Carregando…");
document.getElementById("silenceClipInfo").textContent = "Lendo as timelines do projeto…";
cs.evalScript(
"(function(){" +
"try{" +
"if(!app.project) return JSON.stringify({ok:false,error:'Nenhum projeto aberto.'});" +
"var sequences=[];" +
"var colecao=null;try{colecao=app.project.sequences;}catch(ignorarColecao){colecao=null;}" +
"var quantidade=colecao&&typeof colecao.numSequences!=='undefined'?Number(colecao.numSequences):0;" +
"for(var i=0;i= 0 ? activeIndex : 0;
picker.hidden = false;
document.getElementById("silenceSequenceListInfo").textContent =
result.sequences.length + " timeline(s) encontrada(s).";
silenceSelectSequence();
}
);
}
function silenceSelectSequence() {
var select = document.getElementById("silenceSequenceSelect");
var option = select && select.options[select.selectedIndex];
if (!option || !option.dataset.sequence) return;
var sequence = JSON.parse(option.dataset.sequence);
setBusy("btnDetectClip", true);
setStep(1, "running", "Lendo timeline…");
document.getElementById("silenceClipInfo").textContent = "Lendo o primeiro clipe da timeline escolhida…";
var sequenceId = JSON.stringify(String(sequence.id));
cs.evalScript(
"(function(){" +
"try{" +
"var sequenceId=" + sequenceId + ";" +
"var seq=null;" +
"for(var i=0;i Number(turn.start);
}).sort(function (a, b) { return Number(a.start) - Number(b.start); });
if (!ranges.length) {
showToast("err", "Não há intervalos de vídeo disponíveis para " + speakerId + ".");
return;
}
var preview = document.getElementById("refinarSpeakerPreview");
var info = document.getElementById("refinarSpeakerPreviewInfo");
if (!preview) {
showToast("err", "Preview de vídeo indisponível nesta versão do painel.");
return;
}
speakerTimelineQueue = {
speakerId: speakerId,
ranges: ranges,
index: 0,
button: button,
inicioPendente: false,
};
preview.hidden = false;
if (info) info.textContent = "Reproduzindo os trechos de " + speakerId + ".";
button.textContent = "■ timeline";
button.classList.add("is-playing");
iniciarPreviewNaTimeline(speakerTimelineToken);
}
function iniciarPreviewNaTimeline(token) {
if (!speakerTimelineQueue || token !== speakerTimelineToken) return;
var indiceDoTrecho = speakerTimelineQueue.index;
if (speakerTimelineQueue.inicioPendente) return;
var range = speakerTimelineQueue.ranges[indiceDoTrecho];
if (!range) { stopSpeakerAudio(); return; }
var inicio = Math.max(0, Number(range.start) - 0.15);
var inicioJson = JSON.stringify(inicio);
speakerTimelineQueue.inicioPendente = true;
cs.evalScript(
"(function(){try{" +
"var seq=app.project.activeSequence;" +
"if(!seq)return JSON.stringify({ok:false,error:'Nenhuma sequência ativa.'});" +
"if(String(seq.sequenceID)!==" + JSON.stringify(String(silenceState.sequenceId)) + ")return JSON.stringify({ok:false,error:'A sequência da análise não está ativa.'});" +
"seq.setPlayerPosition(String(Math.round(" + inicioJson + "*254016000000)));" +
"app.enableQE();qe.startPlayback();" +
"return JSON.stringify({ok:true});" +
"}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())",
function (raw) {
if (!speakerTimelineQueue || token !== speakerTimelineToken || speakerTimelineQueue.index !== indiceDoTrecho) return;
speakerTimelineQueue.inicioPendente = false;
var resultado;
try { resultado = JSON.parse(raw); } catch (_) { resultado = { ok: false, error: "Resposta inválida do Premiere." }; }
if (!resultado.ok) {
stopSpeakerAudio();
showToast("err", resultado.error || "Não foi possível iniciar o preview na timeline.");
return;
}
agendarFimDoTrechoNaTimeline(token, indiceDoTrecho, inicio, Number(range.end));
}
);
}
function agendarFimDoTrechoNaTimeline(token, indiceDoTrecho, inicio, fim) {
if (!speakerTimelineQueue || token !== speakerTimelineToken || speakerTimelineQueue.index !== indiceDoTrecho) return;
var duracaoEmMilissegundos = Math.max(250, (fim - inicio) * 1000);
speakerTimelineTimer = setTimeout(function () {
speakerTimelineTimer = null;
if (!speakerTimelineQueue || token !== speakerTimelineToken || speakerTimelineQueue.index !== indiceDoTrecho) return;
pausarEAvancarPreviewNaTimeline(token);
}, duracaoEmMilissegundos);
}
function pausarEAvancarPreviewNaTimeline(token) {
if (!speakerTimelineQueue || token !== speakerTimelineToken) return;
cs.evalScript(
"(function(){try{if(typeof qe!==\"undefined\"&&qe.stopPlayback)qe.stopPlayback();return JSON.stringify({ok:true});}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())",
function (raw) {
if (!speakerTimelineQueue || token !== speakerTimelineToken) return;
var resultado;
try { resultado = JSON.parse(raw); } catch (_) { resultado = { ok: false, error: "Resposta inválida do Premiere." }; }
if (!resultado.ok) {
stopSpeakerAudio();
showToast("err", resultado.error || "Não foi possível pausar o preview na timeline.");
return;
}
speakerTimelineQueue.index += 1;
if (speakerTimelineQueue.index >= speakerTimelineQueue.ranges.length) {
stopSpeakerAudio();
} else {
iniciarPreviewNaTimeline(token);
}
}
);
}
function toggleSpeakerAudio(speakerId, button) {
if (speakerAudioPlayer && speakerAudioQueue && speakerAudioQueue.speakerId === speakerId) {
stopSpeakerAudio();
return;
}
stopSpeakerAudio();
var ranges = silenceState.speakerTurns.filter(function (turn) {
return turn.speaker === speakerId && Number(turn.end) > Number(turn.start);
}).sort(function (a, b) { return Number(a.start) - Number(b.start); });
if (!ranges.length) {
showToast("err", "Não há intervalos de áudio disponíveis para " + speakerId + ".");
return;
}
if (!silenceState.audioCachePath || !fs.existsSync(silenceState.audioCachePath)) {
showToast("err", "Arquivo de áudio do cache não encontrado. Reprocesse a transcrição.");
return;
}
speakerAudioQueue = { speakerId: speakerId, ranges: ranges, index: 0, button: button };
speakerAudioPlayer = new Audio(speakerMediaFileUrl(silenceState.audioCachePath));
button.textContent = "■";
button.classList.add("is-playing");
speakerAudioPlayer.addEventListener("loadedmetadata", playCurrentSpeakerRange);
speakerAudioPlayer.addEventListener("timeupdate", advanceSpeakerAudioRange);
speakerAudioPlayer.addEventListener("ended", advanceSpeakerAudioRange);
speakerAudioPlayer.addEventListener("error", function () {
stopSpeakerAudio();
showToast("err", "Não foi possível reproduzir o áudio deste falante.");
});
}
function playCurrentSpeakerRange() {
if (!speakerAudioPlayer || !speakerAudioQueue) return;
var range = speakerAudioQueue.ranges[speakerAudioQueue.index];
if (!range) { stopSpeakerAudio(); return; }
speakerAudioPlayer.currentTime = Math.max(0, Number(range.start) - 0.15);
var playPromise = speakerAudioPlayer.play();
if (playPromise && playPromise.catch) playPromise.catch(function () { stopSpeakerAudio(); });
}
function advanceSpeakerAudioRange() {
if (!speakerAudioPlayer || !speakerAudioQueue) return;
var range = speakerAudioQueue.ranges[speakerAudioQueue.index];
if (!range || speakerAudioPlayer.currentTime < Number(range.end)) return;
speakerAudioQueue.index += 1;
if (speakerAudioQueue.index >= speakerAudioQueue.ranges.length) {
stopSpeakerAudio();
return;
}
playCurrentSpeakerRange();
}
function silenceLoadSpeakerConfiguration() {
if (!silenceState.transcriptPath) return;
try {
var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
var diarization = transcript.diarization;
if (diarization && Array.isArray(diarization.speakers)) renderSpeakerConfiguration(diarization.speakers, diarization.turns);
} catch (e) {}
}
function speakerConfigurationForJson() {
var speakers = silenceState.speakers.map(function (speaker) {
var active = speaker.active !== false;
return {
id: speaker.id,
name: speaker.name || speaker.id,
status: active ? "active" : "inactive",
include_in_video: active,
editorial_instruction: active
? "Manter as falas deste falante, salvo decisão específica da IA."
: "Excluir todas as falas deste falante do vídeo final.",
};
});
return {
total_detected: speakers.length,
speakers: speakers,
inactive_speakers: speakers.filter(function (speaker) { return !speaker.include_in_video; }).map(function (speaker) { return speaker.id; }),
instruction: "Falantes inativos devem ter todas as suas falas removidas do vídeo final; não confundir nome com identidade confirmada.",
};
}
// 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) {}
}
}
// LEGADO comentado em 2026-09-09: calculava métricas de voz direto nesta aba,
// duplicando o que a aba Scanner já faz e persiste no banco (.jhonny/analises.db).
// Mantido comentado (não removido) a pedido do usuário, para referência futura.
/*
function silenceComputeVoiceFeatures() {
if (!silenceState.transcriptPath) return;
if (!silenceState.mediaPath) {
silenceLog("Detecte o clipe/sequência primeiro (passo 1) para saber qual vídeo analisar.", "err");
showToast("err", "Detecte o vídeo no passo 1 primeiro.");
return;
}
var info = document.getElementById("silenceVoiceFeaturesInfo");
setBusy("btnVoiceFeatures", true);
setStep(3, "running", "Calculando…");
info.textContent = "Calculando…";
silenceLog("Calculando pitch/energia/velocidade de fala/pausas…");
taskStart("Medindo a voz", "Lendo a transcrição");
runStreamed(
SILENCE_PYTHON_BIN,
[
VOICE_FEATURES_SCRIPT,
"--video", silenceState.mediaPath,
"--transcript", silenceState.transcriptPath,
"--out", silenceState.transcriptPath,
"--cache-dir", silenceState.outputDir,
],
function (line) { silenceLog(line); },
function (code, lastErr) {
setBusy("btnVoiceFeatures", false);
if (code !== 0) {
setStep(3, "ready", "Opcional");
info.textContent = "Falhou — veja os detalhes técnicos.";
taskEnd(false, "O cálculo das métricas de voz falhou (código " + code + ").", lastErr);
silenceLog("Falha ao calcular métricas de voz (código " + code + (lastErr ? ": " + lastErr : "") + ").", "err");
return;
}
var msg = "Pitch, energia, velocidade e pausas calculados por trecho.";
silenceLog(msg, "ok");
info.textContent = msg;
atualizarCheckDaAnalise("scannerCheckVoiceFeatures", true, "Carregado");
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;
if (!silenceState.tipoVideo) {
showToast("err", "Selecione o tipo de vídeo antes de gerar o JSON.");
setStep(1, "ready", "Selecione um tipo");
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 },
legendas: carregarConfiguracaoDeLegendas(),
});
transcript.speaker_configuration = speakerConfigurationForJson();
transcript.tipo_de_video = {
id: silenceState.tipoVideo.id,
nome: silenceState.tipoVideo.nome,
instrucoes: tipoVideoResumoParaJson(silenceState.tipoVideo),
};
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("jsonFileActions").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 lerPlanoEditorialDaTela() {
var campoJson = document.getElementById("editorialPlanJson");
var campoCaminho = document.getElementById("editorialPlanPath");
var texto = String(campoJson && campoJson.value || "").trim();
var caminho = String(campoCaminho && campoCaminho.value || "").trim();
if (!texto && caminho) {
if (!fs.existsSync(caminho)) throw new Error("Arquivo do plano não encontrado.");
texto = fs.readFileSync(caminho, "utf-8");
}
if (!texto) throw new Error("Cole o JSON ou escolha um arquivo de plano.");
var plano;
try { plano = JSON.parse(texto); } catch (erro) { throw new Error("JSON inválido: " + erro.message); }
if (!plano || typeof plano !== "object" || !plano.source || !Array.isArray(plano.actions) || !plano.actions.length) {
throw new Error("O plano precisa ter source e uma lista não vazia de actions.");
}
plano.actions.forEach(function (acao, indice) {
if (!acao || !acao.kind || typeof acao.start !== "number" || typeof acao.end !== "number" || !acao.reason) {
throw new Error("actions[" + indice + "] precisa ter kind, start, end e reason.");
}
});
return plano;
}
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 plan;
try { plan = lerPlanoEditorialDaTela(); } catch (erroPlano) {
var statusPlano = document.getElementById("editorialPlanJsonStatus");
if (statusPlano) { statusPlano.textContent = erroPlano.message; statusPlano.className = "field-help err"; }
silenceLog(erroPlano.message, "err");
showToast("err", erroPlano.message);
return;
}
var numActions = plan.actions.length;
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 caminhoDoPlano = path.join(silenceState.outputDir || os.tmpdir(), "plano-editorial-" + Date.now() + ".json");
try { fs.writeFileSync(caminhoDoPlano, JSON.stringify(plan, null, 2), "utf-8"); } catch (erroEscrita) {
setBusy("btnApplyEditorialActions", false);
setStep(5, "error", "Falhou");
taskEnd(false, "Não foi possível salvar o plano.", erroEscrita.message);
return;
}
var processoAplicacao = null;
silenceLog("Aplicando plano pela engine Python…");
taskStart("Aplicando o plano de edição", "Executando pela engine Python", null, function () {
if (processoAplicacao) processoAplicacao.kill("SIGTERM");
});
processoAplicacao = runStreamed(
SCANNER_PYTHON_BIN,
[TESTAR_ACOES_ENGINE_SCRIPT, caminhoDoPlano],
function (line) { silenceLog(line); },
function (code, lastErr) {
processoAplicacao = null;
setBusy("btnApplyEditorialActions", false);
if (code !== 0) {
setStep(5, "error", "Falhou");
taskEnd(false, "Falha ao aplicar o plano (código " + code + ").", lastErr);
silenceLog("Falha ao aplicar o plano (código " + code + ").", "err");
return;
}
setStep(5, "done", "Aplicado");
taskEnd(true, numActions + " ação(ões) aplicadas na timeline!");
silenceLog("Plano de edição aplicado com sucesso!", "ok");
}
);
}
);
}
// Abre o seletor de arquivos nativo do CEP em vez de exigir que a pessoa cole
// um caminho absoluto à mão.
function pickEditorialPlanFile() {
var input = document.getElementById("editorialPlanPath");
try {
var api = window.cep && window.cep.fs;
if (!api || typeof api.showOpenDialog !== "function") {
input.focus();
showToast("err", "Seletor de arquivos indisponível aqui — cole o caminho do JSON.");
return;
}
var result = api.showOpenDialog(false, false, "Escolha o plano de edição (JSON)", "", ["json"]);
var chosen = result && result.data && result.data.length ? result.data[0] : "";
if (chosen) input.value = toFsPath(chosen);
} catch (e) {
input.focus();
showToast("err", "Não foi possível abrir o seletor: " + e.message);
}
}
// ---- Settings / secrets storage ----
var CONFIG_DIR = path.join(os.homedir(), ".premiere-mcp");
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
function loadConfig() {
try {
if (!fs.existsSync(CONFIG_FILE)) return {};
var parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
return (parsed && typeof parsed === "object") ? parsed : {};
} catch (e) {
return {};
}
}
function saveConfig(patch) {
try {
ensureDir(CONFIG_DIR);
var next = Object.assign({}, loadConfig(), patch);
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch (e) {}
return true;
} catch (e) {
log("Error saving config: " + e.message, "err");
return false;
}
}
// ---- Configuração do Editor: personalidades de edição ----
// Fase 1: nome + texto livre descrevendo o comportamento esperado. A
// personalidade marcada como ativa vai embutida no JSON de transcrição
// (settings.editor_personality), pra eu já saber o estilo esperado quando
// você me trouxer o arquivo. Fases futuras podem trocar/complementar o texto
// livre por parâmetros estruturados obrigatórios.
function loadEditorPersonalities() {
var list = loadConfig().editorPersonalities;
return Array.isArray(list) ? list : [];
}
function loadActiveEditorPersonalityId() {
return String(loadConfig().activeEditorPersonalityId || "").trim();
}
function getActiveEditorPersonality() {
var id = loadActiveEditorPersonalityId();
if (!id) return null;
var list = loadEditorPersonalities();
for (var i = 0; i < list.length; i++) {
if (list[i].id === id) return list[i];
}
return null;
}
function setActiveEditorPersonality(id) {
saveConfig({ activeEditorPersonalityId: id });
showToast("ok", "Personalidade ativa atualizada.");
renderEditorPersonalities();
}
// Embute a personalidade de edição ativa (nome + texto livre) como
// `settings.editor_personality` dentro do JSON de transcrição recém-gerado,
// pra ir junto quando o arquivo for trazido pra análise.
function applyEditorPersonalityToTranscript(transcriptPath) {
var personality = getActiveEditorPersonality();
if (!personality) return;
try {
var transcript = JSON.parse(fs.readFileSync(transcriptPath, "utf-8"));
transcript.settings = Object.assign({}, transcript.settings, {
editor_personality: { name: personality.name, text: personality.text },
});
fs.writeFileSync(transcriptPath, JSON.stringify(transcript, null, 2), "utf-8");
silenceLog('Personalidade de edição embutida no JSON: "' + personality.name + '".', "ok");
} catch (e) {
silenceLog("Não foi possível embutir a personalidade de edição: " + e.message, "err");
}
}
var editingPersonalityId = null;
function startNewEditorPersonality() {
editingPersonalityId = null;
document.getElementById("editorPersonalityName").value = "";
document.getElementById("editorPersonalityText").value = "";
document.getElementById("editorPersonalityForm").hidden = false;
}
function editEditorPersonality(id) {
var list = loadEditorPersonalities();
var entry = list.filter(function (p) { return p.id === id; })[0];
if (!entry) return;
editingPersonalityId = id;
document.getElementById("editorPersonalityName").value = entry.name;
document.getElementById("editorPersonalityText").value = entry.text;
document.getElementById("editorPersonalityForm").hidden = false;
}
function cancelEditorPersonalityForm() {
editingPersonalityId = null;
document.getElementById("editorPersonalityForm").hidden = true;
}
function saveEditorPersonalityForm() {
var name = document.getElementById("editorPersonalityName").value.trim();
var text = document.getElementById("editorPersonalityText").value.trim();
if (!name || !text) {
modelsLog("Preencha nome e descrição da personalidade.", "err");
return;
}
var list = loadEditorPersonalities();
if (editingPersonalityId) {
list = list.map(function (p) {
return p.id === editingPersonalityId ? { id: p.id, name: name, text: text } : p;
});
} else {
var id = "personality-" + Date.now().toString(36);
list.push({ id: id, name: name, text: text });
if (!loadActiveEditorPersonalityId()) saveConfig({ activeEditorPersonalityId: id });
}
saveConfig({ editorPersonalities: list });
cancelEditorPersonalityForm();
renderEditorPersonalities();
modelsLog("Personalidade de edição salva: " + name, "ok");
}
function deleteEditorPersonality(id) {
var list = loadEditorPersonalities();
var entry = list.filter(function (p) { return p.id === id; })[0];
if (!entry) return;
if (typeof window.confirm === "function" && !window.confirm('Excluir a personalidade "' + entry.name + '"?')) return;
list = list.filter(function (p) { return p.id !== id; });
var patch = { editorPersonalities: list };
if (loadActiveEditorPersonalityId() === id) patch.activeEditorPersonalityId = "";
saveConfig(patch);
renderEditorPersonalities();
}
function renderEditorPersonalities() {
var container = document.getElementById("editorPersonalityList");
if (!container) return;
container.innerHTML = "";
var activeId = loadActiveEditorPersonalityId();
var list = loadEditorPersonalities();
if (!list.length) {
var empty = document.createElement("p");
empty.className = "field-help";
empty.textContent = "Nenhuma personalidade cadastrada ainda. Crie uma para orientar o corte por IA.";
container.appendChild(empty);
return;
}
list.forEach(function (entry) {
var isActive = entry.id === activeId;
var row = document.createElement("div");
row.className = "personality-row";
row.setAttribute("data-active", String(isActive));
var main = document.createElement("div");
main.className = "model-main";
var name = document.createElement("span");
name.className = "model-name";
name.textContent = entry.name;
main.appendChild(name);
var meta = document.createElement("div");
meta.className = "model-meta";
var snippet = document.createElement("span");
snippet.className = "model-badge";
snippet.textContent = entry.text.length > 60 ? entry.text.slice(0, 60) + "…" : entry.text;
snippet.title = entry.text;
meta.appendChild(snippet);
if (isActive) {
var badge = document.createElement("span");
badge.className = "model-badge model-badge-star";
badge.textContent = "★ Ativa";
meta.appendChild(badge);
}
main.appendChild(meta);
row.appendChild(main);
var actions = document.createElement("div");
actions.className = "model-actions";
var activeBtn = document.createElement("button");
activeBtn.type = "button";
if (isActive) {
activeBtn.className = "button button-primary model-btn-active";
activeBtn.textContent = "Ativa";
activeBtn.disabled = true;
} else {
activeBtn.className = "button button-ghost";
activeBtn.textContent = "Usar esta";
activeBtn.onclick = function () { setActiveEditorPersonality(entry.id); };
}
actions.appendChild(activeBtn);
var editBtn = document.createElement("button");
editBtn.type = "button";
editBtn.className = "link-button";
editBtn.textContent = "Editar";
editBtn.onclick = function () { editEditorPersonality(entry.id); };
actions.appendChild(editBtn);
var deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "link-button";
deleteBtn.textContent = "Excluir";
deleteBtn.onclick = function () { deleteEditorPersonality(entry.id); };
actions.appendChild(deleteBtn);
row.appendChild(actions);
container.appendChild(row);
});
}
function loadHfToken() {
return String(loadConfig().hfToken || "").trim();
}
function hfSpawnOptions() {
var token = loadHfToken();
if (!token) return undefined;
var nodeProcess = nodeRequire("process");
return { env: Object.assign({}, nodeProcess.env, { HF_TOKEN: token }) };
}
function loadModelsPath() {
return String(loadConfig().modelsPath || "").trim();
}
function effectiveModelsPath() {
return loadModelsPath() || DEFAULT_MODELS_PATH;
}
function saveModelsPath() {
var value = document.getElementById("modelsPath").value.trim();
if (!saveConfig({ modelsPath: value })) return;
modelsLog("Pasta de modelos salva: " + effectiveModelsPath());
showToast("ok", "Pasta de modelos salva.");
renderModelList();
}
function loadLanguage() {
return String(loadConfig().language || "").trim() || DEFAULT_LANGUAGE;
}
function saveLanguage() {
var value = document.getElementById("transcribeLanguage").value;
saveConfig({ language: value });
syncLanguageInfo();
showToast("ok", "Idioma da transcrição salvo.");
modelsLog("Idioma de transcrição salvo: " + value);
}
function loadDefaultModel() {
return String(loadConfig().defaultModel || "").trim() || DEFAULT_MODEL;
}
function setDefaultModel(model) {
saveConfig({ defaultModel: model });
modelsLog("Modelo padrão definido: " + model, "ok");
showToast("ok", "Modelo padrão: " + model + ".");
renderModelList();
syncTranscribeModelSelect();
}
// Mantém o seletor de modelo da aba de Transcrição em sincronia com o catálogo
// e com o modelo padrão escolhido na aba Modelos Locais.
function syncTranscribeModelSelect() {
var select = document.getElementById("silenceModel");
if (!select) return;
var current = select.value;
select.innerHTML = "";
var modelosDisponiveis = discoverPortugueseModelsOnMerongo();
modelosDisponiveis.forEach(function (modelo) {
var opt = document.createElement("option");
opt.value = modelo.nome;
opt.textContent = modelo.nome;
select.appendChild(opt);
});
if (!modelosDisponiveis.length) {
var vazio = document.createElement("option");
vazio.value = "";
vazio.textContent = "Nenhum modelo instalado";
select.appendChild(vazio);
return;
}
var nomes = modelosDisponiveis.map(function (modelo) { return modelo.nome; });
var desejado = nomes.indexOf(current) >= 0 ? current : loadDefaultModel();
select.value = nomes.indexOf(desejado) >= 0 ? desejado : nomes[0];
}
// ---- 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-")
// 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 =
'' +
'';
row.appendChild(progress);
list.appendChild(row);
});
}
function downloadModel(model, btn, approxMB) {
btn.disabled = true;
btn.classList.add("is-busy");
modelsLog("Baixando modelo " + model + "… isso pode levar alguns minutos.");
var box = document.getElementById("modelProgress-" + model);
var track = box ? box.querySelector(".progress-track") : null;
var fill = box ? box.querySelector(".progress-fill") : null;
var label = box ? box.querySelector(".model-progress-label") : null;
if (box) box.hidden = false;
function mirror(info) {
if (!box) return;
if (typeof info.pct === "number" && isFinite(info.pct)) {
track.setAttribute("data-mode", "determinate");
fill.style.width = Math.max(0, Math.min(100, info.pct)) + "%";
} else {
track.setAttribute("data-mode", "indeterminate");
}
label.textContent = info.detail ? info.stage + " · " + info.detail : info.stage || "";
}
taskStart("Baixando o modelo " + model, "Preparando o download", mirror);
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_DOWNLOAD_SCRIPT,
"--model", model,
"--models-path", effectiveModelsPath(),
"--approx-mb", String(approxMB || 0),
],
function (line) { modelsLog(line); },
function (code, lastErr) {
btn.classList.remove("is-busy");
if (code !== 0) {
if (box) box.hidden = true;
modelsLog("Falha ao baixar " + model + " (código " + code + ").", "err");
taskEnd(false, "Falha ao baixar o modelo " + model + " (código " + code + ").", lastErr);
btn.disabled = false;
return;
}
modelsLog("Modelo " + model + " baixado com sucesso.", "ok");
taskEnd(true, "Modelo " + model + " pronto para uso.");
renderModelList();
},
hfSpawnOptions()
);
}
// ---- Init ----
(function init() {
// A identificação de locutores pertence ao Scanner; abrir nele evita que o
// painel pareça travado na etapa antiga de edição de vídeo.
switchTab("scanner");
// 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();
restaurarConfiguracaoDeLegendas();
renderizarTiposDeVideoParaEdicao();
// A aba Editar vídeo começa com a análise já produzida pelo Scanner.
setStep(1, "locked", "Scanner");
setStep(2, "locked", "Scanner");
setStep(3, "locked", "Aguardando análise");
setStep(4, "locked", "Aguardando análise");
setStep(5, "locked", "Aguardando plano");
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);
})();