Files
João Henrique c22e240abe feat: preparado teste incremental do gancho com inserção de adjust
- preparado teste incremental do gancho com inserção de adjustment layer na faixa superior
- integrado o teste de adjustment layer ao checkbox da montagem incremental do painel

Resumo:
- 6 arquivos alterados
- 2 novos
- 4 modificados
- 0 removidos

 4 files changed, 80 insertions(+)

Arquivos:
  - .jhonny/analises.db
  - code/cep-plugin/main.js
  - code/engine/aplicar_montagem_incremental_prototipo.py
  - code/src/tools/timeline.ts
  - code/engine/editor/plano_teste_gancho_adjustment.json
  - code/engine/editor/plano_teste_zooms_plano_6.json
2026-09-10 19:37:09 -04:00

4591 lines
220 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* MCP for Adobe Premiere Pro - CEP Plugin Main Script
* Polls a temp directory for command files (.jsx), executes them
* in Premiere Pro's ExtendScript engine, and writes results back. */
var cs = new CSInterface();
var bridgeRunning = false;
var pollInterval = null;
var commandCount = 0;
var tempDir = "";
var POLL_MS = 200;
var HEARTBEAT_MS = 1000;
var heartbeatInterval = null;
// ---- Logging ----
function log(msg, cls) {
var el = document.getElementById("log");
var entry = document.createElement("div");
entry.className = "log-entry " + (cls || "");
var ts = new Date().toLocaleTimeString();
entry.textContent = "[" + ts + "] " + msg;
el.appendChild(entry);
el.scrollTop = el.scrollHeight;
// Keep max 100 entries
while (el.children.length > 100) el.removeChild(el.firstChild);
}
// ---- Painel de progresso (task dock) ----
// Antes, tudo que rodava só aparecia como linhas no log lá embaixo e a pessoa
// não sabia o que estava acontecendo. Agora todo processo longo abre um card
// reposicionado na aba de origem com etapa atual, barra e tempo decorrido.
var taskState = { active: false, startedAt: 0, timer: null, clearTimer: null, mirror: null, 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 {
var copia = childProcess.spawn("pbcopy");
copia.stdin.write(texto, "utf-8");
copia.stdin.end();
showToast("ok", "Erro técnico copiado.");
} catch (_) {
taskCopyErrorFallback(texto);
}
}
function taskCopyErrorFallback(texto) {
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: "projeto", panel: "tabProjeto", btn: "tabBtnProjeto" },
{ key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" },
{ key: "trilhas", panel: "tabTrilhas", btn: "tabBtnTrilhas" },
{ 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 === "trilhas") trilhasAoAbrir();
if (name === "projeto") projetoListarSequencias();
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); renderizarTiposDeVideoParaEdicao(); carregarListaDePlanosEditoriais(); }
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 = '<p class="empty-state">Nenhuma legenda carregada.</p>'; 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";
var MONTAGEM_INCREMENTAL_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/aplicar_montagem_incremental_prototipo.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, ultimaSaida) {
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 = '<div class="video-type-copy"><strong></strong><span></span></div><div class="video-type-meta"></div><span class="video-type-status"></span><div class="video-type-actions"><button class="button button-ghost video-type-edit" type="button">Editar</button><button class="button button-ghost video-type-duplicate" type="button">Duplicar</button><button class="button button-ghost video-type-toggle" type="button"></button></div>';
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 = '<span class="priority-number">' + (indice + 1) + '</span><input class="text-field priority-input" type="text" value="">';
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.");
}
// A API nativa preserva o texto Unicode no clipboard do CEP/Chromium.
// Use pbcopy apenas quando a API não existir ou falhar.
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(texto).then(function () {
showToast("ok", mensagem);
}).catch(function () {
copiarComPbcopy(texto, copiarComSelecaoDoPainel, mensagem);
});
return;
}
} catch (_) {}
copiarComPbcopy(texto, 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"], env: Object.assign({}, process.env, { PYTHONIOENCODING: "utf-8" }) });
var stderr = "", stdout = "";
child.stdout.on("data", function (chunk) { stdout += chunk.toString("utf-8"); });
child.stderr.on("data", function (chunk) { stderr += chunk.toString("utf-8"); });
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 "<p class=\"analise-fala\"><strong>" + (fala.emocao || "—") + "</strong> " +
analisesFormatarTempo(fala.inicio) + "–" + analisesFormatarTempo(fala.fim) + " · " +
String(fala.texto || "").replace(/&/g, "&amp;").replace(/</g, "&lt;") + "</p>";
}).join("") || "<p class=\"analise-fala analise-vazia\">Sem fala neste clipe.</p>";
bloco.innerHTML =
"<div class=\"analise-cena-cab\"><strong>" + cena.clipe_id + titulo + "</strong>" +
"<span>" + analisesFormatarTempo(cena.inicio) + "–" + analisesFormatarTempo(cena.fim) + "</span></div>" +
falasHtml + "<p class=\"analise-evidencias\">Imagem: " + tipos + "</p>";
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 REGISTRAR_EDICAO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/registrar_edicao.py";
var CARREGAR_PLANO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/carregar_plano_de_edicao.py";
var LISTAR_PLANOS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/listar_planos_de_edicao.py";
var planosEditoriaisListados = [];
var REGISTRAR_PROJETO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/registrar_projeto_de_edicao.py";
var CONSULTAR_EDICAO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_edicao_concluida.py";
var planoEditorialIdCarregado = null;
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"], env: Object.assign({}, process.env, { PYTHONIOENCODING: "utf-8" }) });
retakesProcesso = child;
var resto = "";
var erroTecnico = "";
child.stdout.on("data", function (pedaco) {
resto += pedaco.toString("utf-8"); 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) { var texto = pedaco.toString("utf-8"); erroTecnico += texto; if (texto.trim()) retakesLog(texto.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";
var TRILHAS_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/analisar_trilhas.py";
var TRILHAS_CONSULTAR_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_detalhes_de_trilha.py";
var TRILHAS_BANCO_PATH = "";
var TRILHAS_MODELOS_PATH = "";
var TRILHAS_CONFIG_KEY = "mcp_trilhas_config_v1";
// `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 = "<option value=''>Nenhuma análise carregada</option>";
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<c.numTracks;i++){var t=c[i],clips=[];for(var j=0;j<t.clips.numItems;j++){var x=t.clips[j],p=x.projectItem;clips.push({nodeId:x.nodeId,name:x.name,start:Number(x.start.seconds),end:Number(x.end.seconds),inPoint:Number(x.inPoint.seconds),outPoint:Number(x.outPoint.seconds),duration:Number(x.duration.seconds),sourceFile:p&&p.getMediaPath?p.getMediaPath():'',sourceOffline:p&&p.isOffline?p.isOffline():false});}a.push({index:i,name:t.name||('Faixa '+(i+1)),numClips:t.clips.numItems,clips:clips});}}var v=[],a=[];tracks(s.videoTracks,v);tracks(s.audioTracks,a);return JSON.stringify({ok:true,id:s.sequenceID,name:s.name,end:Number(s.end.seconds),videoTracks:v,audioTracks:a});}catch(e){return JSON.stringify({ok:false,error:String(e)});}}())", function(raw) { setBusy("btnScannerDetectTracks", false); var data; try { data=JSON.parse(raw); } catch (_) { data={ok:false,error:"Resposta inválida do Premiere."}; } if (!data.ok) { document.getElementById("scannerTracksHelp").textContent=data.error; showToast("err",data.error); return; } scannerTimeline=data; scannerCarregarRelatorioExistente(); scannerAtualizarComplementos(); var saved; try { saved=JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY)||"null")||{}; } catch (_) { saved={}; } scannerRenderTracks("video",data.videoTracks,saved.faixas_de_video||data.videoTracks.map(function(t){return t.index;})); scannerRenderTracks("audio",data.audioTracks,saved.faixas_de_audio||[]); document.getElementById("scannerTracksHelp").textContent=data.name+": "+data.videoTracks.length+" faixa(s) de vídeo e "+data.audioTracks.length+" de áudio detectadas."; if(!scannerRelatorioExistente){document.getElementById("btnScannerProcess").disabled=false;document.getElementById("btnScannerProcess").textContent="Processar áudio";} });
}
function scannerProcess() {
if (scannerRelatorioExistente) { showToast("ok", "Esta análise já está pronta. Abra Editar vídeo para utilizá-la."); return; }
scannerIniciarProcessamento();
}
function scannerConfirmarReprocessamento(confirmado) {
document.getElementById("scannerReprocessConfirm").hidden = true;
if (confirmado && scannerReprocessamentoPendente) scannerIniciarProcessamento();
scannerReprocessamentoPendente = false;
}
function scannerIniciarProcessamento() {
if (!scannerTimeline) { showToast("err", "Primeiro detecte as faixas da sequência."); return; }
var profile; try { profile=scannerProfileFromScreen(); } catch (error) { showToast("err", error.message); return; }
if (profile.modo_de_analise === "som" && !profile.faixas_de_audio.length) { showToast("err", "Selecione ao menos uma faixa de áudio."); return; }
if (profile.modo_de_analise === "imagem" && !profile.faixas_de_video.length) { showToast("err", "Selecione ao menos uma faixa de vídeo."); return; }
if (profile.modo_de_analise === "ambos" && (!profile.faixas_de_video.length || !profile.faixas_de_audio.length)) { showToast("err", "Selecione ao menos uma faixa de vídeo e uma de áudio."); return; }
saveScannerProfile(); var outputRoot=scannerOutputRoot(); ensureDir(outputRoot);
var stamp=new Date().toISOString().replace(/[:.]/g,"-");
var input=path.join(outputRoot,"pedido-"+stamp+".json");
scannerReportPath=path.join(outputRoot,"relatorio-"+stamp+".json");
fs.writeFileSync(input,JSON.stringify({perfil:profile,timeline:scannerTimeline},null,2),"utf-8");
setBusy("btnScannerProcess",true);
var spawnOptions={cwd:"/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code",stdio:["ignore","pipe","pipe"],env:Object.assign({},process.env,{PYTHONIOENCODING:"utf-8"})};
var huggingFaceEnvironment=hfSpawnOptions(); if(huggingFaceEnvironment&&huggingFaceEnvironment.env) spawnOptions.env=Object.assign({},spawnOptions.env,huggingFaceEnvironment.env);
var child=childProcess.spawn(SCANNER_PYTHON_BIN,[SCANNER_ENGINE_SCRIPT,input,scannerReportPath],spawnOptions);
scannerProcesso=child; scannerCancelSolicitado=false;
taskStart("Scanner", "Preparando análise", null, function () { scannerCancelSolicitado=true; if (scannerProcesso) scannerProcesso.kill("SIGTERM"); });
var stderr="", stdout="";
child.stdout.on("data",function(chunk) {
stdout += chunk.toString("utf-8"); var lines=stdout.split("\n"); stdout=lines.pop();
lines.forEach(function(line) { try { var event=JSON.parse(line); if (event.evento === "progresso") taskUpdate({stage:event.etapa, detail:event.clipe || "", pct:event.percentual}); if (event.evento === "concluido" && event.arquivo) scannerReportPath=event.arquivo; } catch (_) {} });
});
child.stderr.on("data",function(chunk){stderr+=chunk.toString("utf-8");});
child.on("error",function(error){setBusy("btnScannerProcess",false);taskEnd(false,"Scanner indisponível",error.stack||error.message);showToast("err",error.message);});
child.on("close",function(code){var foiCancelado=scannerCancelSolicitado;scannerProcesso=null;setBusy("btnScannerProcess",false);if(foiCancelado){taskEnd(false,"Scanner cancelado");return;}if(code!==0||!fs.existsSync(scannerReportPath)){taskEnd(false,"Scanner falhou",stderr||"Não foi possível gerar o relatório.");showToast("err",stderr||"Não foi possível gerar o relatório.");return;}scannerRelatorioExistente=scannerReportPath;scannerAtualizarComplementos();taskEnd(true,"Relatório JSON gerado");document.getElementById("scannerReportPath").textContent=scannerReportPath;document.getElementById("scannerReportCard").hidden=false;showToast("ok","Scanner concluído.");});
}
function scannerProcessarComplemento(acao) {
if (!scannerRelatorioExistente || !scannerTimeline) { showToast("err", "Processe o áudio uma vez antes de complementar este item."); return; }
var botao = document.getElementById(acao === "diarizacao" ? "btnScannerDiarization" : "btnScannerVoiceFeatures");
setBusy(botao.id, true);
var entrada = path.join(scannerOutputRoot(), "pedido-complemento-" + Date.now() + ".json");
var perfil; try { perfil = scannerProfileFromScreen(); } catch (error) { setBusy(botao.id, false); showToast("err", error.message); return; }
fs.writeFileSync(entrada, JSON.stringify({perfil: perfil, timeline: scannerTimeline}, null, 2), "utf-8");
var opcoes = {cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", stdio: ["ignore", "pipe", "pipe"], env: Object.assign({}, process.env, { PYTHONIOENCODING: "utf-8" })};
var hf = hfSpawnOptions(); if (hf && hf.env) opcoes.env = Object.assign({}, opcoes.env, hf.env);
var processo = null;
taskStart("Complementando a análise do Scanner", acao === "diarizacao" ? "Detectando locutores" : "Calculando métricas de voz", null, function () {
if (processo) processo.kill("SIGTERM");
});
processo = childProcess.spawn(SCANNER_PYTHON_BIN, [SCANNER_ENGINE_SCRIPT, entrada, scannerRelatorioExistente, "--complemento", acao], opcoes);
var erro = "";
processo.stderr.on("data", function (bloco) { erro += bloco.toString("utf-8"); });
processo.stdout.on("data", function (bloco) { bloco.toString("utf-8").split("\n").forEach(function (linha) { try { var evento = JSON.parse(linha); if (evento.evento === "progresso") taskUpdate({stage: evento.etapa, detail: evento.clipe || "", pct: evento.percentual}); } catch (_) {} }); });
processo.on("close", function (codigo) { processo = null; setBusy(botao.id, false); if (taskState.canceled) { taskEnd(false, "Complemento cancelado"); return; } if (codigo !== 0) { taskEnd(false, "Falha ao processar o complemento.", erro || "Não foi possível processar este complemento."); showToast("err", erro || "Não foi possível processar este complemento."); return; } scannerAtualizarComplementos(); document.getElementById("scannerReportPath").textContent = scannerRelatorioExistente; document.getElementById("scannerReportCard").hidden = false; taskEnd(true, "Complemento concluído"); showToast("ok", acao === "diarizacao" ? "Locutores processados sem refazer a transcrição." : "Métricas da fala processadas sem refazer a transcrição."); });
}
function scannerOpenReport() { if (!scannerReportPath) return; childProcess.spawn("open",[scannerReportPath],{detached:true,stdio:"ignore"}).unref(); }
function scannerCopyReport() { if (!scannerReportPath) return; try { var copy=childProcess.spawn("pbcopy"); copy.stdin.end(fs.readFileSync(scannerReportPath,"utf-8")); showToast("ok","Conteúdo do relatório copiado."); } catch (error) { showToast("err",error.message); } }
// Mostra na aba de edição o idioma configurado, para não precisar ir até
// Modelos só para conferir com que idioma a transcrição vai rodar.
function syncLanguageInfo() {
var el = document.getElementById("silenceLanguageInfo");
if (!el) return;
var code = loadLanguage();
var match = LANGUAGE_CATALOG.filter(function (l) { return l.value === code; })[0];
el.textContent = match ? match.label : code;
}
// ---- Status ----
function setStatus(state, text) {
var dot = document.getElementById("statusDot");
dot.className = "status-dot " + state;
var statusText = document.getElementById("statusText");
statusText.textContent = text;
statusText.setAttribute("data-state", state || "stopped");
var detail = document.getElementById("statusDetail");
if (detail) {
if (state === "connected") detail.textContent = "Ligação com o Premiere Pro ativa";
else if (state === "waiting") detail.textContent = "Aguardando um assistente de IA se conectar";
else if (state === "error") detail.textContent = "A ponte precisa de atenção";
else detail.textContent = "Aguardando o Premiere Pro";
}
// Espelha no cabeçalho para o status ficar visível de qualquer aba.
var pill = document.getElementById("headerStatus");
if (pill) {
pill.setAttribute("data-state", state || "stopped");
document.getElementById("headerStatusText").textContent =
state === "connected" ? "Conectado" :
state === "waiting" ? "Aguardando" :
state === "error" ? "Erro" : "Parado";
pill.title = text;
}
}
function setConnectionCheck(id, state, detail) {
var el = document.getElementById(id);
if (!el) return;
el.setAttribute("data-state", state);
var text = el.getElementsByTagName("small")[0];
if (text) text.textContent = detail;
}
// This reads only boolean Premiere state. Do not put project names, paths, or
// media information in the panel: the MCP safe-check uses the same boundary.
function refreshConnectionCenter() {
if (!bridgeRunning) {
setConnectionCheck("checkConnector", "waiting", "Start the connector first");
setConnectionCheck("checkProject", "waiting", "Waiting for the connector");
setConnectionCheck("checkSequence", "waiting", "Waiting for the connector");
return;
}
setConnectionCheck("checkConnector", "ready", "Running in Premiere Pro");
setConnectionCheck("checkProject", "waiting", "Checking…");
setConnectionCheck("checkSequence", "waiting", "Checking…");
cs.evalScript(
'(function(){var p=app&&app.project;return "mcpstate:"+(p&&typeof p.name!=="undefined"?"1":"0")+","+(p&&p.activeSequence?"1":"0");}())',
function (raw) {
var match = /^mcpstate:([01]),([01])$/.exec(String(raw || ""));
if (!match) {
setConnectionCheck("checkProject", "needs-attention", "Could not read Premiere state");
setConnectionCheck("checkSequence", "needs-attention", "Could not read Premiere state");
return;
}
var projectOpen = match[1] === "1";
var sequenceOpen = match[2] === "1";
setConnectionCheck("checkProject", projectOpen ? "ready" : "needs-attention", projectOpen ? "Project open" : "Open a project in Premiere Pro");
setConnectionCheck("checkSequence", sequenceOpen ? "ready" : "needs-attention", sequenceOpen ? "Active sequence open" : "Open a sequence in Premiere Pro");
}
);
}
// ---- File I/O via Node.js (CEP has access to Node) ----
// --enable-nodejs puts `require` in the global scope on most hosts, but on some it
// lands on cep_node instead. Try both, and fail loudly rather than letting fs come
// back undefined and surface later as "Cannot read properties of undefined".
function nodeRequire(moduleName) {
if (typeof require !== "undefined") return require(moduleName);
var cepNode = typeof cep_node !== "undefined" ? cep_node : typeof window !== "undefined" ? window.cep_node : null;
if (cepNode && typeof cepNode.require === "function") return cepNode.require(moduleName);
throw new Error(
'Node.js is not available in this CEP panel, so "' + moduleName + '" could not be loaded. ' +
"Check that CSXS/manifest.xml has <Parameter>--enable-nodejs</Parameter>, then fully quit and reopen Premiere Pro."
);
}
var fs = nodeRequire("fs");
var path = nodeRequire("path");
var os = nodeRequire("os");
var https = nodeRequire("https");
var childProcess = nodeRequire("child_process");
TRILHAS_BANCO_PATH = path.join(os.homedir(), ".premiere-mcp", "trilhas.db");
TRILHAS_MODELOS_PATH = path.join(os.homedir(), ".premiere-mcp", "modelos-essentia");
if (!fs.existsSync(TRILHAS_MODELOS_PATH) && fs.existsSync("/private/tmp/essentia-modelos")) TRILHAS_MODELOS_PATH = "/private/tmp/essentia-modelos";
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 = '<option value="">Selecione um tipo cadastrado</option>';
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");
setStep(2, "locked", "Aguardando plano");
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");
setStep(2, "ready", "Pronto para concluir");
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++) {
var arquivoDoClipe = clipes[indiceDoClipe].sourceFile || clipes[indiceDoClipe].arquivo || clipes[indiceDoClipe].arquivo_original;
if (arquivoDoClipe) return arquivoDoClipe;
}
} 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 intervalosDePreview = silenceConsultarPreviewDeFalantes(silenceState.sequenceId);
var turnos = intervalosDePreview && intervalosDePreview.length
? intervalosDePreview.filter(function (intervalo) {
return intervalo.falante && Number(intervalo.fim) > Number(intervalo.inicio);
}).map(function (intervalo) {
return {
speaker: intervalo.falante,
start: Number(intervalo.inicio),
end: Number(intervalo.fim),
};
})
: transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) {
return {
speaker: segmento.falante,
start: Number(segmento.start),
end: Number(segmento.end),
};
});
renderSpeakerConfiguration(falantes, turnos, {
config: "refinarSpeakerConfig",
list: "refinarSpeakerList",
count: "refinarSpeakerCount",
});
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(2, "locked", "Aguardando plano");
setStep(1, "ready", "Tipo no Projeto");
setStep(3, "locked", "Aguardando plano");
renderizarTiposDeVideoParaEdicao();
document.getElementById("btnBuildPlan").disabled = true;
// A análise carregada já identifica a sequência; reutiliza o plano salvo
// dessa mesma sequência sem exigir uma nova seleção manual.
carregarPlanoEditorialSalvo();
// 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) return;
if (!silenceState.sequenceId || !silenceState.transcriptPath) {
silenceLoadScannerAnalysis();
return;
}
info.textContent = silenceState.sequenceName + " · " + (silenceState.speakers.length
? silenceState.speakers.length + " falante(s) carregado(s)."
: "Nenhum falante carregado.");
var botao = document.getElementById("btnRefinarLoad");
if (botao) botao.textContent = "Recarregar análise do Scanner";
}
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;
// ---- 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<quantidade;i++){" +
"var seq=app.project.sequences[i];" +
"sequences.push({id:String(seq.sequenceID),name:String(seq.name||''),duration:seq.end&&typeof seq.end.seconds!=='undefined'?Number(seq.end.seconds):0," +
"videoTracks:seq.videoTracks.numTracks||0,audioTracks:seq.audioTracks.numTracks||0," +
"active:app.project.activeSequence&&seq.sequenceID===app.project.activeSequence.sequenceID});" +
"}" +
"if(!sequences.length&&app.project.activeSequence){var ativa=app.project.activeSequence;sequences.push({id:String(ativa.sequenceID),name:String(ativa.name||''),duration:ativa.end&&typeof ativa.end.seconds!=='undefined'?Number(ativa.end.seconds):0,videoTracks:ativa.videoTracks.numTracks||0,audioTracks:ativa.audioTracks.numTracks||0,active:true});}" +
"return JSON.stringify({ok:true,sequences:sequences});" +
"}catch(e){return JSON.stringify({ok:false,error:String(e)});}" +
"}())",
function (raw) {
setBusy("btnDetectClip", false);
var result;
try {
result = JSON.parse(raw);
} catch (e) {
result = { ok: false, error: "Resposta inesperada do Premiere." };
}
var picker = document.getElementById("silenceSequencePicker");
var select = document.getElementById("silenceSequenceSelect");
var info = document.getElementById("silenceClipInfo");
if (!result.ok) {
picker.hidden = true;
setStep(1, "error", "Não encontrado");
info.textContent = result.error || "Não foi possível listar as timelines.";
showToast("err", info.textContent);
lockStepsFrom(2);
return;
}
if (!result.sequences || result.sequences.length === 0) {
picker.hidden = true;
setStep(1, "error", "Sem timelines");
info.textContent = "O projeto não possui timelines.";
showToast("err", info.textContent);
lockStepsFrom(2);
return;
}
select.innerHTML = "";
result.sequences.forEach(function (sequence) {
var option = document.createElement("option");
option.value = sequence.id;
option.textContent = sequence.name + " · " + formatSequenceDuration(sequence.duration) +
" · V" + sequence.videoTracks + " / A" + sequence.audioTracks +
(sequence.active ? " · ativa" : "");
option.dataset.sequence = JSON.stringify(sequence);
select.appendChild(option);
});
var activeIndex = -1;
for (var sequenceIndex = 0; sequenceIndex < result.sequences.length; sequenceIndex++) {
if (result.sequences[sequenceIndex].active) {
activeIndex = sequenceIndex;
break;
}
}
select.selectedIndex = activeIndex >= 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<app.project.sequences.numSequences;i++){if(String(app.project.sequences[i].sequenceID)===sequenceId){seq=app.project.sequences[i];break;}}" +
"if(!seq) return JSON.stringify({ok:false,error:'Timeline não encontrada.'});" +
"var vt=seq.videoTracks[0];" +
"if(!vt||vt.clips.numItems===0) return JSON.stringify({ok:false,error:'Vídeo 1 está vazio nesta timeline.'});" +
"var clip=vt.clips[0];" +
"var projItem=clip.projectItem;" +
"var mediaPath=projItem&&projItem.getMediaPath?projItem.getMediaPath():'';" +
"return JSON.stringify({ok:true,sequenceId:String(seq.sequenceID),sequenceName:seq.name,mediaPath:mediaPath,clipName:clip.name});" +
"}catch(e){return JSON.stringify({ok:false,error:String(e)});}" +
"}())",
function (raw) {
setBusy("btnDetectClip", false);
var result;
try {
result = JSON.parse(raw);
} catch (e) {
result = { ok: false, error: "Resposta inesperada do Premiere." };
}
var info = document.getElementById("silenceClipInfo");
var card = document.getElementById("silenceClipCard");
if (!result.ok || !result.mediaPath) {
card.hidden = true;
setStep(1, "error", "Sem mídia");
info.textContent = result.error || "Caminho de mídia vazio.";
showToast("err", info.textContent);
lockStepsFrom(2);
return;
}
silenceState.sequenceId = result.sequenceId;
silenceState.sequenceName = result.sequenceName;
silenceState.mediaPath = result.mediaPath;
silenceState.fileBaseName = slugify(result.clipName || result.sequenceName);
silenceState.outputDir = path.join(SILENCE_OUTPUT_ROOT, silenceState.fileBaseName);
silenceState.audioCachePath = path.join(silenceState.outputDir, path.parse(result.mediaPath).name + ".wav");
silenceState.transcriptPath = null;
silenceState.jsonOutputPath = null;
silenceState.speakers = [];
silenceState.speakerTurns = [];
var speakerConfig = document.getElementById("silenceSpeakerConfig");
if (speakerConfig) speakerConfig.hidden = true;
document.getElementById("clipSequenceName").textContent = result.sequenceName;
document.getElementById("clipFileName").textContent = path.basename(result.mediaPath);
var pathCell = document.getElementById("clipMediaPath");
pathCell.textContent = result.mediaPath;
pathCell.title = result.mediaPath;
card.hidden = false;
info.textContent = "";
setStep(1, "done", "Timeline escolhida");
lockStepsFrom(2);
setStep(2, "ready", "Sua vez");
document.getElementById("btnBuildPlan").disabled = false;
setStep(3, "ready", "Carregar plano salvo");
document.getElementById("btnTranscribe").disabled = false;
document.getElementById("silenceJsonCard").hidden = true;
document.getElementById("jsonFileActions").hidden = true;
document.getElementById("silencePlanInfo").textContent = "";
silenceLog("Timeline escolhida: " + result.sequenceName + " — " + result.mediaPath);
renderCachedTranscripts();
carregarPlanoEditorialSalvo();
}
);
}
function silenceDetectClip() {
silenceListSequences();
}
// Volta os passos seguintes ao estado travado quando o vídeo (ou a transcrição)
// muda — evita aplicar um plano gerado para outro material.
function lockStepsFrom(first) {
if (first <= 3) planoEditorialIdCarregado = null;
var buttons = {
2: ["btnBuildPlan"],
3: ["btnApplyEditorialActions"],
};
for (var n = first; n <= 5; n++) {
setStep(n, "locked", "Bloqueado");
(buttons[n] || []).forEach(function (id) {
var btn = document.getElementById(id);
if (btn) btn.disabled = true;
});
}
}
function renderCachedTranscripts() {
var section = document.getElementById("silenceCachedSection");
var select = document.getElementById("silenceCachedSelect");
var entries = silenceState.mediaPath ? findCachedTranscripts(silenceState.mediaPath) : [];
if (!entries.length) {
section.hidden = true;
select.innerHTML = "";
return;
}
entries.sort(function (a, b) { return new Date(b.savedAt) - new Date(a.savedAt); });
select.innerHTML = "";
entries.forEach(function (entry, i) {
var opt = document.createElement("option");
opt.value = String(i);
opt.textContent = new Date(entry.savedAt).toLocaleString() + " — modelo " + (entry.model || "?");
select.appendChild(opt);
});
select.dataset.entries = JSON.stringify(entries);
section.hidden = false;
silenceLog(entries.length + " transcrição(ões) salva(s) encontrada(s) para este vídeo.", "ok");
}
function silenceUseCachedTranscript() {
var select = document.getElementById("silenceCachedSelect");
var entries = JSON.parse(select.dataset.entries || "[]");
var entry = entries[Number(select.value)];
if (!entry) return;
silenceState.transcriptPath = entry.transcriptPath;
silenceState.transcribeModel = entry.model;
silenceState.cachedTranscriptPath = entry.transcriptPath;
silenceLoadSpeakerConfiguration();
// silenceCacheInfo ficava no step2 (botão "Transcrever", legado comentado em 2026-09-09).
var elCacheInfo = document.getElementById("silenceCacheInfo");
if (elCacheInfo) elCacheInfo.textContent =
"Usando a transcrição salva em " + new Date(entry.savedAt).toLocaleString() + ".";
unlockAfterTranscript();
showToast("ok", "Transcrição reaproveitada — pule para o passo 4.");
silenceLog("Transcrição salva selecionada — pronto para gerar o plano de cortes.", "ok");
}
// Passos que dependem de existir uma transcrição, seja nova ou reaproveitada.
function unlockAfterTranscript() {
setStep(2, "done", "Transcrito");
setStep(2, "ready", "Pronto para concluir");
document.getElementById("btnBuildPlan").disabled = false;
// btnDiarize/btnVoiceFeatures ficavam no step3 (legado, comentado em 2026-09-09).
var elBtnDiarize = document.getElementById("btnDiarize");
if (elBtnDiarize) elBtnDiarize.disabled = false;
var elBtnVoiceFeatures = document.getElementById("btnVoiceFeatures");
if (elBtnVoiceFeatures) elBtnVoiceFeatures.disabled = false;
}
// Premiere is launched by Finder/LaunchServices, not a login shell, so the CEP
// panel's process PATH lacks Homebrew/nvm dirs where `node` actually lives.
// Resolve an absolute path the same way SILENCE_PYTHON_BIN does, instead of
// relying on spawn("node", ...) to find it via PATH.
// cs.getSystemPath() sometimes returns a "file:/..." URI (percent-encoded,
// occasionally missing a leading slash) instead of a plain fs path. Normalize
// it before handing it to path.join/fs/spawn.
function toFsPath(maybeUri) {
var s = String(maybeUri || "");
if (s.indexOf("file:") === 0) {
s = s.replace(/^file:\/{0,3}/, "/");
try { s = decodeURIComponent(s); } catch (e) {}
}
return s;
}
function resolveNodeBin() {
var candidates = os.platform() === "win32"
? ["node.exe"]
: ["/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node", "node"];
for (var i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
if (candidate.indexOf("/") !== 0 || fs.existsSync(candidate)) return candidate;
}
return "node";
}
var PROGRESS_PREFIX = "@@PROGRESS ";
function runStreamed(bin, args, onLine, onDone, options) {
var childProcess = nodeRequire("child_process");
var baseEnv = Object.assign({}, process.env || {}, { PYTHONIOENCODING: "utf-8" });
var finalEnv = Object.assign(baseEnv, options && options.env || {});
var mergedOptions = Object.assign({}, options || {}, { env: finalEnv });
var child = childProcess.spawn(bin, args, mergedOptions);
var buffer = "";
var ultimoErro = "";
var ultimaSaida = "";
// Linhas "@@PROGRESS {json}" são o canal de progresso dos scripts Python/Node:
// vão para a barra no topo em vez de virarem ruído no log.
function handleLine(line) {
var trimmed = line.trim();
if (trimmed.indexOf(PROGRESS_PREFIX) === 0) {
try {
taskUpdate(JSON.parse(trimmed.slice(PROGRESS_PREFIX.length)));
} catch (e) {}
return;
}
onLine(line);
}
function flushLines(chunk) {
buffer += chunk;
var parts = buffer.split("\n");
buffer = parts.pop();
for (var i = 0; i < parts.length; i++) {
if (parts[i].trim()) handleLine(parts[i]);
}
}
child.stdout.on("data", function (d) { ultimaSaida += d.toString("utf-8"); flushLines(d.toString("utf-8")); });
child.stderr.on("data", function (d) { ultimoErro = d.toString("utf-8").trim() || ultimoErro; flushLines(d.toString("utf-8")); });
child.on("error", function (err) { onDone(1, "spawn error: " + err.message); });
child.on("close", function (code) {
if (buffer.trim()) handleLine(buffer);
onDone(code, ultimoErro || null, ultimaSaida);
});
return child;
}
// ---- Catálogo de trilhas musicais -----------------------------------------
function trilhasCarregarConfiguracao() {
try { return JSON.parse(localStorage.getItem(TRILHAS_CONFIG_KEY) || "{}") || {}; } catch (_) { return {}; }
}
function trilhasSalvarConfiguracao(pasta) {
localStorage.setItem(TRILHAS_CONFIG_KEY, JSON.stringify({ pasta: pasta }));
}
function trilhasAoAbrir() {
var configuracao = trilhasCarregarConfiguracao();
var pasta = document.getElementById("trilhasPasta");
if (pasta && configuracao.pasta) pasta.value = configuracao.pasta;
trilhasAtualizarEstado();
if (pasta && pasta.value.trim() && fs.existsSync(pasta.value.trim())) trilhasAtualizarCatalogo(pasta.value.trim(), function () { trilhasIniciarWorker(pasta.value.trim()); });
}
function trilhasAtualizarCatalogo(pasta, aoFinalizar) {
var argumentos = [TRILHAS_ENGINE_SCRIPT, pasta, "--banco", TRILHAS_BANCO_PATH, "--listar"];
runStreamed(SCANNER_PYTHON_BIN, argumentos, function (linha) {
if (linha.indexOf("@@PROGRESS ") !== 0) return;
try { trilhasAtualizarEvento(JSON.parse(linha.slice(11))); } catch (_) {}
}, function () { if (typeof aoFinalizar === "function") aoFinalizar(); }, { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", stdio: ["ignore", "pipe", "pipe"] });
}
function trilhasListarArquivos(pasta) {
var extensoes = { ".wav": true, ".mp3": true, ".flac": true, ".m4a": true, ".aiff": true, ".ogg": true };
var arquivos = [];
function visitar(diretorio) {
fs.readdirSync(diretorio).forEach(function (nome) {
var caminho = path.join(diretorio, nome);
var estatisticas = fs.statSync(caminho);
if (estatisticas.isDirectory()) visitar(caminho);
else if (estatisticas.isFile() && extensoes[path.extname(nome).toLowerCase()]) arquivos.push(caminho);
});
}
visitar(pasta);
return arquivos.sort();
}
function trilhasPrepararLista(pasta) {
try {
var arquivos = trilhasListarArquivos(pasta);
trilhasRenderizarLista(arquivos.map(function (arquivo) { return path.relative(pasta, arquivo) || path.basename(arquivo); }));
document.getElementById("trilhasPastaStatus").textContent = arquivos.length
? arquivos.length + " arquivo(s) de áudio encontrado(s). Pronto para análise."
: "Nenhum arquivo de áudio compatível foi encontrado nesta pasta.";
} catch (erro) {
document.getElementById("trilhasPastaStatus").textContent = "Não foi possível ler a pasta selecionada.";
}
}
function trilhasSelecionarPasta() {
try {
var api = window.cep && window.cep.fs;
if (!api || typeof api.showOpenDialog !== "function") throw new Error("Seletor de pastas indisponível.");
var resultado = api.showOpenDialog(false, true, "Escolha a pasta de trilhas musicais", "", []);
var escolhida = resultado && resultado.data && resultado.data.length ? toFsPath(resultado.data[0]) : "";
if (!escolhida) return;
document.getElementById("trilhasPasta").value = escolhida;
trilhasSalvarConfiguracao(escolhida);
trilhasAtualizarEstado();
trilhasPrepararLista(escolhida);
trilhasIniciarWorker(escolhida);
} catch (erro) { showToast("err", erro.message); }
}
function trilhasAtualizarEstado() {
var pasta = document.getElementById("trilhasPasta");
var botao = document.getElementById("btnTrilhasAnalisar");
if (!pasta || !botao) return;
var valida = pasta.value.trim() && fs.existsSync(pasta.value.trim()) && fs.statSync(pasta.value.trim()).isDirectory();
botao.disabled = !valida || taskState.active;
document.getElementById("trilhasPastaStatus").textContent = valida ? "Pasta pronta para análise." : "Selecione uma pasta existente.";
if (valida && !taskState.active && !document.querySelector(".trilha-item")) trilhasPrepararLista(pasta.value.trim());
}
function trilhasRenderizarLista(nomes) {
var lista = document.getElementById("trilhasLista");
lista.innerHTML = "";
(nomes || []).forEach(function (registro, indice) {
var nome = typeof registro === "string" ? registro : registro.nome;
var status = typeof registro === "string" ? "aguardando" : (registro.status || "aguardando");
var item = document.createElement("div");
item.className = "trilha-item";
item.id = "trilha-item-" + indice;
item.setAttribute("role", "button");
item.setAttribute("tabindex", "0");
item.setAttribute("aria-expanded", "false");
item.setAttribute("data-caminho", (typeof registro === "string" ? path.join(document.getElementById("trilhasPasta").value.trim(), nome) : registro.caminho) || "");
item.setAttribute("data-state", status === "concluida" ? "concluida" : status === "erro" ? "erro" : "aguardando");
item.innerHTML = '<span class="trilha-check" aria-hidden="true"></span><div class="trilha-info"><strong></strong><span>Aguardando</span><div class="progress-track"><div class="progress-fill" style="width:0%"></div></div><div class="trilha-detalhes" hidden></div></div>';
item.querySelector("strong").textContent = nome;
item.querySelector(".trilha-info > span").textContent = status === "concluida" ? "Analisada" : status === "erro" ? "Erro na análise anterior" : "Aguardando análise";
if (status === "concluida") item.querySelector(".progress-fill").style.width = "100%";
item.onclick = function () { trilhasAlternarDetalhes(item); };
item.onkeydown = function (evento) { if (evento.key === "Enter" || evento.key === " ") { evento.preventDefault(); trilhasAlternarDetalhes(item); } };
lista.appendChild(item);
});
document.getElementById("trilhasContagem").textContent = (nomes || []).length + " trilha(s)";
document.getElementById("trilhasProgressoCard").hidden = !(nomes || []).length;
}
function trilhasAlternarDetalhes(item) {
var detalhes = item.querySelector(".trilha-detalhes");
var aberto = !detalhes.hidden;
detalhes.hidden = aberto;
item.setAttribute("aria-expanded", String(!aberto));
if (!aberto && !detalhes.getAttribute("data-carregado")) trilhasCarregarDetalhes(item, detalhes);
}
function trilhasCarregarDetalhes(item, detalhes) {
detalhes.removeAttribute("data-carregado");
detalhes.innerHTML = "Consultando análise salva…";
var resposta = childProcess.spawnSync(SCANNER_PYTHON_BIN, [TRILHAS_CONSULTAR_SCRIPT, item.getAttribute("data-caminho"), "--banco", TRILHAS_BANCO_PATH], { encoding: "utf8" });
if (resposta.error || resposta.status !== 0) { detalhes.textContent = "Não foi possível consultar os detalhes desta trilha."; return; }
try {
var dados = JSON.parse(resposta.stdout.trim());
if (!dados.encontrada) { detalhes.textContent = dados.mensagem || "Ainda não há análise salva."; return; }
var etiquetas = dados.etiquetas || [];
var generos = etiquetas.filter(function (etiqueta) { return etiqueta.tipo === "genero"; });
var humores = etiquetas.filter(function (etiqueta) { return etiqueta.tipo === "humor"; });
var temas = etiquetas.filter(function (etiqueta) { return etiqueta.tipo === "tema"; });
function formatar(valor, casas) { return valor === null || valor === undefined ? "—" : Number(valor).toFixed(casas || 2); }
function listaEtiquetas(itens) { return itens.length ? itens.map(function (item) { return item.etiqueta + " (" + formatar(Number(item.confianca) * 100, 1) + "%)"; }).join(" · ") : "Não classificado"; }
function listaTextos(itens) { return itens && itens.length ? itens.join(" · ") : "Não identificado"; }
detalhes.innerHTML = '<div class="trilha-detalhes-grid"><div><small>Duração</small><strong>' + formatar(dados.duracao_em_segundos, 1) + ' s</strong></div><div><small>BPM</small><strong>' + formatar(dados.batidas_por_minuto, 1) + '</strong></div><div><small>Tonalidade</small><strong>' + (dados.tonalidade || "—") + ' ' + (dados.modo || "") + '</strong></div><div><small>Intensidade</small><strong>' + formatar(dados.intensidade, 2) + '</strong></div><div><small>Dançabilidade</small><strong>' + formatar(dados.dancabilidade, 2) + '</strong></div></div><div class="trilha-etiquetas"><div><small>Estilos / gêneros</small><p>' + listaEtiquetas(generos) + '</p></div><div><small>Humores</small><p>' + listaEtiquetas(humores) + '</p></div><div><small>Temas</small><p>' + listaEtiquetas(temas) + '</p></div><div><small>Instrumentos</small><p>' + listaTextos(dados.instrumentos) + '</p></div><div><small>Etiquetas do áudio</small><p>' + listaTextos(dados.etiquetas_audio) + '</p></div></div><small class="trilha-fonte">Análise salva por ' + (dados.provedor || "Essentia") + (dados.modelo ? " · " + dados.modelo : "") + '</small>';
detalhes.setAttribute("data-carregado", "true");
} catch (_) { detalhes.textContent = "A resposta da análise está inválida."; }
}
function trilhasAtualizarEvento(evento) {
if (evento.evento === "catalogo_atualizado") { trilhasRenderizarLista(evento.trilhas || []); return; }
if (evento.evento === "lote_iniciado") { trilhasRenderizarLista(evento.trilhas || evento.nomes || []); return; }
if (!evento.nome) return;
var itens = Array.prototype.slice.call(document.querySelectorAll(".trilha-item"));
var item = itens.filter(function (elemento) { return (evento.caminho && elemento.getAttribute("data-caminho") === evento.caminho) || elemento.querySelector("strong").textContent === evento.nome; })[0];
if (!item) return;
var estado = evento.evento === "trilha_concluida" ? "concluida" : evento.evento === "trilha_com_erro" ? "erro" : "analisando";
item.setAttribute("data-state", estado);
item.querySelector(".trilha-info > span").textContent = estado === "concluida" ? "Analisada" : estado === "erro" ? "Erro: " + (evento.erro || "falha") : "Analisando…";
var faixa = item.querySelector(".progress-track");
var preenchimento = item.querySelector(".progress-fill");
faixa.setAttribute("data-mode", estado === "analisando" ? "indeterminate" : "normal");
preenchimento.style.width = estado === "concluida" ? "100%" : estado === "erro" ? "100%" : "0%";
}
function trilhasAnalisar() {
var pasta = document.getElementById("trilhasPasta").value.trim();
if (!pasta || !fs.existsSync(pasta) || !fs.statSync(pasta).isDirectory()) { showToast("err", "Selecione uma pasta existente."); return; }
trilhasIniciarWorker(pasta);
}
function trilhasIniciarWorker(pasta) {
if (trilhasProcesso || taskState.active) return;
trilhasSalvarConfiguracao(pasta);
var argumentos = [TRILHAS_ENGINE_SCRIPT, pasta, "--banco", TRILHAS_BANCO_PATH, "--modelos", TRILHAS_MODELOS_PATH, "--continuo", "--intervalo", "20"];
var ambiente = Object.assign({}, process.env, { OMP_NUM_THREADS: "1", OPENBLAS_NUM_THREADS: "1", MKL_NUM_THREADS: "1", TF_NUM_INTRAOP_THREADS: "1", TF_NUM_INTEROP_THREADS: "1" });
setBusy("btnTrilhasAnalisar", true);
taskStart("Catálogo de trilhas em segundo plano", "Monitorando a pasta", function (evento) { trilhasAtualizarEvento(evento); }, function () { if (trilhasProcesso) trilhasProcesso.kill("SIGTERM"); });
trilhasProcesso = runStreamed(SCANNER_PYTHON_BIN, argumentos, function () {}, function (codigo, erro) {
trilhasProcesso = null; setBusy("btnTrilhasAnalisar", false); trilhasAtualizarEstado();
if (taskState.canceled) { taskEnd(false, "Monitoramento interrompido"); return; }
if (codigo !== 0) { taskEnd(false, "Falha no monitoramento das trilhas", erro || "O processamento não foi concluído."); return; }
taskEnd(true, "Monitoramento das trilhas concluído");
}, { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", env: ambiente, stdio: ["ignore", "pipe", "pipe"] });
if (trilhasProcesso && trilhasProcesso.pid) childProcess.spawn("renice", ["-n", "15", "-p", String(trilhasProcesso.pid)], { stdio: "ignore" });
}
var trilhasProcesso = null;
window.addEventListener("beforeunload", function () {
if (trilhasProcesso) trilhasProcesso.kill("SIGTERM");
});
// LEGADO comentado em 2026-09-09: transcrição/diarização feitas direto nesta aba,
// duplicando o que a aba Scanner já faz e persiste no banco (.jhonny/analises.db).
// Confirmado por teste real que o Scanner cobre o mesmo fluxo de ponta a ponta.
// Mantido comentado (não removido) a pedido do usuário, para referência futura.
/*
function silenceTranscribe() {
if (!silenceState.mediaPath) return;
document.getElementById("silenceCacheInfo").textContent = "";
var model = document.getElementById("silenceModel").value;
setBusy("btnTranscribe", true);
setStep(2, "running", "Transcrevendo…");
ensureDir(silenceState.outputDir);
var out = path.join(silenceState.outputDir, "transcricao.json");
silenceLog("Transcrevendo com modelo " + model + "… isso pode levar alguns minutos.");
taskStart("Transcrevendo o áudio", "Preparando o modelo " + model);
var lastLine = "";
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_TRANSCRIBE_SCRIPT,
"--video", silenceState.mediaPath,
"--out", out,
"--model", model,
"--language", loadLanguage(),
"--models-path", effectiveModelsPath(),
"--cache-dir", silenceState.outputDir,
],
function (line) { lastLine = line; silenceLog(line); },
function (code, lastErr) {
setBusy("btnTranscribe", false);
if (code !== 0) {
setStep(2, "error", "Falhou");
taskEnd(false, "A transcrição falhou (código " + code + "). Veja os detalhes técnicos.", lastErr || lastLine);
silenceLog("Falha na transcrição (código " + code + ").", "err");
return;
}
var parsed = null;
try { parsed = JSON.parse(lastLine); } catch (e) {}
if (!parsed || parsed.ok === false) {
setStep(2, "error", "Falhou");
taskEnd(false, "A transcrição terminou com uma resposta inválida.", lastLine);
silenceLog("Transcrição terminou mas resposta é inválida: " + lastLine, "err");
return;
}
silenceState.transcriptPath = out;
silenceState.transcribeModel = model;
silenceState.speakers = [];
silenceState.speakerTurns = [];
var speakerConfig = document.getElementById("silenceSpeakerConfig");
if (speakerConfig) speakerConfig.hidden = true;
var resumo = parsed.segments + " trechos · " + Math.round(parsed.duration) + "s de áudio";
silenceLog("Transcrição concluída: " + resumo, "ok");
document.getElementById("silenceCacheInfo").textContent = "Transcrição pronta — " + resumo + ".";
applyEditorPersonalityToTranscript(out);
silenceState.cachedTranscriptPath = saveTranscriptToCache(silenceState.mediaPath, out, model);
if (silenceState.cachedTranscriptPath) {
silenceLog("Transcrição salva na pasta central para reaproveitar depois.", "ok");
}
taskEnd(true, "Transcrição pronta: " + resumo);
unlockAfterTranscript();
silenceRunAutomaticEnrichments();
},
hfSpawnOptions()
);
}
function silenceRunAutomaticEnrichments() {
var shouldDiarize = document.getElementById("silenceDiarize").checked && !!loadHfToken();
var shouldMeasureVoice = document.getElementById("silenceVoiceFeaturesAuto").checked;
if (shouldDiarize) {
silenceDiarizeSpeakers(function () {
if (shouldMeasureVoice) silenceComputeVoiceFeatures();
});
} else if (shouldMeasureVoice) {
silenceComputeVoiceFeatures();
}
}
function silenceDiarizeSpeakers(onFinished) {
if (!silenceState.transcriptPath) return;
if (!silenceState.mediaPath) {
silenceLog("Detecte o clipe/sequência primeiro (passo 1) para saber qual vídeo diarizar.", "err");
showToast("err", "Detecte o vídeo no passo 1 primeiro.");
return;
}
if (!loadHfToken()) {
silenceLog("Diarização pulada: configure um HF_TOKEN na aba Ajustes.", "err");
showToast("err", "Configure o token do Hugging Face na aba Ajustes.");
return;
}
var info = document.getElementById("silenceDiarizeInfo");
setBusy("btnDiarize", true);
setStep(3, "running", "Detectando…");
info.textContent = "Detectando locutores…";
silenceLog("Detectando locutores…");
taskStart("Detectando quem fala", "Preparando");
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_DIARIZE_SCRIPT,
"--video", silenceState.mediaPath,
"--transcript", silenceState.transcriptPath,
"--out", silenceState.transcriptPath,
"--models-path", effectiveModelsPath(),
"--cache-dir", silenceState.outputDir,
],
function (line) { silenceLog(line); },
function (code, lastErr) {
setBusy("btnDiarize", false);
if (code !== 0) {
setStep(3, "ready", "Opcional");
info.textContent = "Falhou — veja os detalhes técnicos.";
taskEnd(false, "A detecção de locutores falhou (código " + code + ").", lastErr);
silenceLog("Falha na diarização (código " + code + (lastErr ? ": " + lastErr : "") + ").", "err");
return;
}
var parsed = null;
try { parsed = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")).diarization; } catch (e) {}
if (parsed && Array.isArray(parsed.speakers)) {
silenceState.speakerTurns = Array.isArray(parsed.turns) ? parsed.turns : [];
renderSpeakerConfiguration(parsed.speakers, silenceState.speakerTurns);
atualizarTelaRefinar(parsed.speakers, { diarizacao_disponivel: true });
}
var msg = parsed
? "Foram detectados " + parsed.num_speakers + (parsed.num_speakers === 1 ? " falante" : " falantes") + ": " + parsed.speakers.join(", ") + "."
: "Falantes detectados.";
silenceLog(msg, "ok");
info.textContent = msg;
atualizarCheckDaAnalise("scannerCheckDiarization", true, "Carregado");
setStep(3, "done", "Enriquecido");
taskEnd(true, msg);
syncTranscriptToCache();
if (typeof onFinished === "function") onFinished();
},
hfSpawnOptions()
);
}
*/
function speakerNameDefault(speakerId) {
return speakerId;
}
function carregarNomesDeFalantes() {
if (!silenceState.sequenceId) return {};
try {
var salvo = JSON.parse(localStorage.getItem("mcp_falantes_" + silenceState.sequenceId) || "{}");
return salvo && typeof salvo === "object" ? salvo : {};
} catch (_) { return {}; }
}
function salvarNomesDeFalantes() {
if (!silenceState.sequenceId) return;
var salvo = {};
silenceState.speakers.forEach(function (speaker) {
salvo[speaker.id] = { name: speaker.name, active: speaker.active !== false };
});
try { localStorage.setItem("mcp_falantes_" + silenceState.sequenceId, JSON.stringify(salvo)); } catch (_) {}
}
function refinarSalvarFalantes() {
salvarNomesDeFalantes();
showToast("ok", "Identificação dos falantes salva.");
}
function renderSpeakerConfiguration(speakerIds, speakerTurns, alvo) {
alvo = alvo || {};
var config = document.getElementById(alvo.config || "silenceSpeakerConfig");
var list = document.getElementById(alvo.list || "silenceSpeakerList");
var count = document.getElementById(alvo.count || "silenceSpeakerCount");
if (!config || !list || !count) return;
silenceState.speakerTurns = Array.isArray(speakerTurns) ? speakerTurns : silenceState.speakerTurns;
var previous = {};
silenceState.speakers.forEach(function (speaker) { previous[speaker.id] = speaker; });
var salvo = carregarNomesDeFalantes();
silenceState.speakers = speakerIds.map(function (id) {
var old = previous[id];
var salvoSpeaker = salvo[id] || {};
return {
id: id,
name: old && old.name ? old.name : (salvoSpeaker.name || speakerNameDefault(id)),
active: old ? old.active !== false : salvoSpeaker.active !== false,
};
});
count.textContent = silenceState.speakers.length +
(silenceState.speakers.length === 1 ? " falante detectado" : " falantes detectados");
list.innerHTML = "";
silenceState.speakers.forEach(function (speaker, index) {
var row = document.createElement("div");
row.className = "speaker-row";
var label = document.createElement("label");
label.className = "field-label";
label.textContent = speaker.id;
var play = document.createElement("button");
play.type = "button";
play.className = "button speaker-play-button";
play.textContent = "▶ áudio";
play.title = "Ouvir trechos de " + speaker.id;
play.setAttribute("aria-label", "Ouvir trechos de " + speaker.id);
play.addEventListener("click", function () {
toggleSpeakerAudio(speaker.id, play);
});
var input = document.createElement("input");
input.type = "text";
input.className = "select-field speaker-name-input";
input.value = speaker.name;
input.placeholder = "Nome do falante";
input.maxLength = 120;
input.setAttribute("aria-label", "Nome de " + speaker.id);
input.addEventListener("input", function () {
silenceState.speakers[index].name = input.value.trim() || speaker.id;
salvarNomesDeFalantes();
});
var activeLabel = document.createElement("label");
activeLabel.className = "checkbox-field speaker-active-field";
var active = document.createElement("input");
active.type = "checkbox";
active.checked = speaker.active;
active.addEventListener("change", function () {
silenceState.speakers[index].active = active.checked;
salvarNomesDeFalantes();
});
var activeText = document.createElement("span");
activeText.textContent = "Incluir no vídeo";
activeLabel.appendChild(active);
activeLabel.appendChild(activeText);
row.appendChild(label);
row.appendChild(play);
row.appendChild(input);
row.appendChild(activeLabel);
list.appendChild(row);
});
config.hidden = silenceState.speakers.length === 0;
var refineConfig = document.getElementById("refinarSpeakerConfig");
if (refineConfig && config.id !== refineConfig.id) {
renderSpeakerConfiguration(speakerIds, speakerTurns, {
config: "refinarSpeakerConfig", list: "refinarSpeakerList", count: "refinarSpeakerCount"
});
}
}
function speakerMediaFileUrl(filePath) {
return encodeURI("file://" + String(filePath || ""));
}
function stopSpeakerAudio() {
if (speakerAudioPlayer) {
speakerAudioPlayer.pause();
speakerAudioPlayer.removeAttribute("src");
speakerAudioPlayer.load();
}
speakerAudioPlayer = null;
speakerAudioQueue = null;
document.querySelectorAll(".speaker-play-button").forEach(function (button) {
button.textContent = "▶ áudio";
button.classList.remove("is-playing");
});
}
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();
}
);
}
*/
// Registra a decisão editorial sem duplicar a análise normalizada do Scanner.
// O JSON continua disponível para o fluxo da IA, mas não é pré-requisito para
// concluir a edição no painel.
function silenceConcluirEdicao() {
if (!silenceState.sequenceId || !silenceState.sequenceName) {
showToast("err", "Carregue uma análise do Scanner antes de concluir.");
return;
}
if (!silenceState.tipoVideo) {
showToast("err", "Selecione o tipo de vídeo antes de concluir a edição.");
setStep(1, "ready", "Selecione um tipo");
return;
}
var pedido = {
video_id: String(silenceState.sequenceId),
sequencia: silenceState.sequenceName,
origem: silenceState.mediaPath,
tipo_de_video: silenceState.tipoVideo.id,
configuracao: {
nome_do_tipo: silenceState.tipoVideo.nome,
instrucoes_do_tipo: tipoVideoResumoParaJson(silenceState.tipoVideo),
transcricao: {
caminho: silenceState.transcriptPath,
origem: "Scanner",
},
},
};
var caminhoDoPedido = path.join(os.tmpdir(), "pedido-concluir-edicao-" + Date.now() + ".json");
try {
fs.writeFileSync(caminhoDoPedido, JSON.stringify(pedido, null, 2), "utf-8");
} catch (erroEscrita) {
showToast("err", "Não foi possível preparar a conclusão: " + erroEscrita.message);
return;
}
setBusy("btnBuildPlan", true);
setStep(2, "running", "Salvando…");
taskStart("Concluindo a edição", "Salvando a configuração no banco");
silenceLog("Salvando o tipo de vídeo e as configurações editoriais no banco…");
runStreamed(
SCANNER_PYTHON_BIN,
[REGISTRAR_EDICAO_SCRIPT, caminhoDoPedido, "--banco", ANALISES_DB_PATH],
function (linha) { silenceLog(linha); },
function (codigo, ultimoErro, ultimaSaida) {
setBusy("btnBuildPlan", false);
if (codigo !== 0) {
setStep(2, "error", "Falhou");
taskEnd(false, "Não foi possível concluir a edição.", ultimoErro);
silenceLog("Falha ao salvar a configuração editorial.", "err");
showToast("err", ultimoErro || "Não foi possível salvar a edição no banco.");
return;
}
setStep(2, "done", "Edição concluída");
setStep(3, "locked", "Aguardando plano da IA");
taskEnd(true, "Edição concluída e salva no banco.");
silenceLog("Edição concluída e salva no banco de análises.", "ok");
showToast("ok", "Edição concluída e salva no banco.");
}
);
}
// Busca o plano já registrado no SQLite; não cria JSON novo nem duplica plano.
function carregarPlanoEditorialSalvo() {
if (!silenceState.sequenceId) {
showToast("err", "Detecte a sequência antes de carregar o plano.");
return;
}
setBusy("btnBuildPlan", true);
silenceLog("Lendo o plano editorial diretamente do banco…");
runStreamed(
SCANNER_PYTHON_BIN,
[CARREGAR_PLANO_SCRIPT, "--banco", ANALISES_DB_PATH, "--video-id", String(silenceState.sequenceId)],
function (linha) { silenceLog(linha); },
function (codigo, ultimoErro, ultimaSaida) {
setBusy("btnBuildPlan", false);
if (codigo !== 0) {
silenceLog("Não foi possível carregar o plano salvo.", "err");
showToast("err", ultimoErro || "Nenhum plano salvo encontrado para este vídeo.");
return;
}
try {
var resposta = JSON.parse(ultimaSaida || "{}");
if (!resposta.ok || !resposta.plano) throw new Error("Resposta inválida do banco.");
// O plano histórico pode usar o nome da sequência como origem.
// A engine localiza os clipes pela mídia real descoberta no Premiere.
if (silenceState.mediaPath) resposta.plano.source = silenceState.mediaPath;
planoEditorialIdCarregado = resposta.plano.plano_id;
document.getElementById("editorialPlanJson").value = JSON.stringify(resposta.plano, null, 2);
document.getElementById("planoResumoEdicao").textContent = "Plano " + planoEditorialIdCarregado + " encontrado · " + resposta.plano.actions.length + " ação(ões) · aguardando aprovação.";
document.getElementById("editorialSavedPlanStatus").textContent = "Plano " + planoEditorialIdCarregado + " carregado do banco.";
document.getElementById("editorialPlanJsonStatus").textContent = "Plano salvo no SQLite; pronto para aprovação e aplicação.";
document.getElementById("btnApplyEditorialActions").disabled = false;
setStep(2, "done", "Plano carregado");
setStep(3, "ready", "Pronto para aplicar");
silenceLog("Plano " + planoEditorialIdCarregado + " carregado diretamente do SQLite.", "ok");
showToast("ok", "Plano salvo carregado.");
} catch (erro) {
showToast("err", erro.message);
}
}
);
}
// Popula o seletor da etapa 2 com os planos mais recentes de qualquer vídeo
// (não só o da sequência detectada) — cada opção mostra data/hora e a
// intenção que a skill de edição registrou, para escolher sem decorar o id.
function carregarListaDePlanosEditoriais() {
var seletor = document.getElementById("editorialPlanIdInput");
var detalhe = document.getElementById("editorialPlanIdDetalhe");
if (!seletor) return;
seletor.innerHTML = "";
seletor.appendChild(new Option("Carregando planos…", ""));
if (detalhe) detalhe.textContent = "";
runStreamed(
SCANNER_PYTHON_BIN,
[LISTAR_PLANOS_SCRIPT, "--banco", ANALISES_DB_PATH, "--limite", "50"],
function () {},
function (codigo, ultimoErro, ultimaSaida) {
seletor.innerHTML = "";
if (codigo !== 0) {
seletor.appendChild(new Option("Falha ao listar planos", ""));
silenceLog("Não foi possível listar os planos salvos.", "err");
return;
}
try {
var resposta = JSON.parse(ultimaSaida || "{}");
if (!resposta.ok) throw new Error(resposta.erro || "Resposta inválida do banco.");
planosEditoriaisListados = resposta.planos || [];
if (!planosEditoriaisListados.length) {
seletor.appendChild(new Option("Nenhum plano salvo ainda", ""));
return;
}
seletor.appendChild(new Option("Selecione um plano…", ""));
planosEditoriaisListados.forEach(function (plano) {
var quando = formatarDataHoraDoPlano(plano.criado_em);
var intencaoResumida = plano.intencao
? (plano.intencao.length > 70 ? plano.intencao.slice(0, 70) + "…" : plano.intencao)
: (plano.origem || "sem intenção registrada");
var rotulo = "#" + plano.id + " · " + quando + " · " + intencaoResumida;
seletor.appendChild(new Option(rotulo, String(plano.id)));
});
} catch (erro) {
seletor.innerHTML = "";
seletor.appendChild(new Option("Falha ao listar planos", ""));
silenceLog("Falha ao interpretar a lista de planos: " + erro.message, "err");
}
}
);
}
function formatarDataHoraDoPlano(criadoEm) {
if (!criadoEm) return "data desconhecida";
var data = new Date(criadoEm);
if (isNaN(data.getTime())) return criadoEm;
return data.toLocaleString("pt-BR", { day: "2-digit", month: "2-digit", hour: "2-digit", minute: "2-digit" });
}
// Ao trocar a seleção, mostra a intenção completa e demais metadados do
// plano escolhido — o rótulo do <option> já vem truncado.
document.addEventListener("DOMContentLoaded", function () {
var seletor = document.getElementById("editorialPlanIdInput");
if (!seletor) return;
seletor.addEventListener("change", function () {
var detalhe = document.getElementById("editorialPlanIdDetalhe");
if (!detalhe) return;
var plano = planosEditoriaisListados.filter(function (p) { return String(p.id) === seletor.value; })[0];
if (!plano) { detalhe.textContent = ""; return; }
detalhe.textContent = "Origem: " + (plano.origem || "—") + " · " + plano.total_de_acoes + " ação(ões) · " +
(plano.modelo_da_ia || "modelo não registrado") +
(plano.intencao ? " — " + plano.intencao : "");
});
});
// Busca um plano pelo id escolhido na lista, sem depender do video_id da
// sequência detectada. Existe para o caso em que o plano foi registrado para
// uma mídia que este banco nunca escaneou (outro projeto/pasta) —
// registrar_plano_de_edicao.py aceita gravar sem video_id quando não acha
// correspondência em edicoes_de_video, e sem video_id o carregamento
// automático por sequência (carregarPlanoEditorialSalvo) nunca encontraria
// esse plano.
function carregarPlanoEditorialPorId() {
var campoId = document.getElementById("editorialPlanIdInput");
var planoId = parseInt(campoId.value, 10);
if (!planoId || planoId <= 0) {
showToast("err", "Selecione um plano na lista antes de carregar.");
return;
}
setBusy("btnLoadPlanById", true);
silenceLog("Lendo o plano " + planoId + " diretamente do banco…");
runStreamed(
SCANNER_PYTHON_BIN,
[CARREGAR_PLANO_SCRIPT, "--banco", ANALISES_DB_PATH, "--plano-id", String(planoId)],
function (linha) { silenceLog(linha); },
function (codigo, ultimoErro, ultimaSaida) {
setBusy("btnLoadPlanById", false);
if (codigo !== 0) {
silenceLog("Não foi possível carregar o plano " + planoId + ".", "err");
showToast("err", ultimoErro || "Plano " + planoId + " não encontrado no banco.");
return;
}
try {
var resposta = JSON.parse(ultimaSaida || "{}");
if (!resposta.ok || !resposta.plano) throw new Error("Resposta inválida do banco.");
// Plano sem video_id (ou de outra sequência) não tem como resolver a
// mídia real pelo cadastro de clipes — usa a mídia detectada na
// sequência ativa só quando o painel já a conhece; senão mantém o
// "source" gravado no plano e deixa a engine (que lê a sequência
// ativa do Premiere no momento da aplicação) resolver.
if (silenceState.mediaPath) resposta.plano.source = silenceState.mediaPath;
planoEditorialIdCarregado = resposta.plano.plano_id;
document.getElementById("editorialPlanJson").value = JSON.stringify(resposta.plano, null, 2);
document.getElementById("planoResumoEdicao").textContent = "Plano " + planoEditorialIdCarregado + " encontrado · " + resposta.plano.actions.length + " ação(ões) · aguardando aprovação.";
document.getElementById("editorialSavedPlanStatus").textContent = "Plano " + planoEditorialIdCarregado + " carregado do banco (por ID).";
document.getElementById("editorialPlanJsonStatus").textContent = "Plano salvo no SQLite; pronto para aprovação e aplicação.";
document.getElementById("btnApplyEditorialActions").disabled = false;
setStep(2, "done", "Plano carregado");
setStep(3, "ready", "Pronto para aplicar");
silenceLog("Plano " + planoEditorialIdCarregado + " carregado diretamente do SQLite (por ID).", "ok");
showToast("ok", "Plano " + planoEditorialIdCarregado + " carregado.");
} catch (erro) {
showToast("err", erro.message);
}
}
);
}
// 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;
planoEditorialIdCarregado = null;
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(2, "running", "Gerando…");
silenceLog("Gerando arquivo JSON para a IA…");
try {
var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
transcript.sequence = {
name: silenceState.sequenceName,
media_path: silenceState.mediaPath,
};
transcript.settings = Object.assign({}, transcript.settings, {
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(2, "done", "JSON pronto");
setStep(3, "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(2, "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/hook/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 invalidarVinculoDoPlanoEditorial() {
planoEditorialIdCarregado = null;
var status = document.getElementById("editorialPlanJsonStatus");
if (status) {
status.textContent = "Plano alterado fora do banco. Registre e carregue a nova versão antes de aplicar cortes ou ganchos.";
status.className = "field-help err";
}
}
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;
if (document.getElementById("editorialIncrementalMode") && document.getElementById("editorialIncrementalMode").checked) {
aplicarMontagemIncremental(plan, numActions);
return;
}
var summary =
numActions + " ação(ões) no plano (cortes, ganchos, 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(3, "running", "Validando…");
taskStart("Aplicando o plano de edição", "Validando frases e bordas editoriais");
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(3, "error", "Falhou");
taskEnd(false, "Não foi possível salvar o plano.", erroEscrita.message);
return;
}
var processoAplicacao = null;
silenceLog("Validando o plano antes de alterar a timeline…");
taskStart("Aplicando o plano de edição", "Validando e executando pela engine Python", null, function () {
if (processoAplicacao) processoAplicacao.kill("SIGTERM");
});
processoAplicacao = runStreamed(
SCANNER_PYTHON_BIN,
[TESTAR_ACOES_ENGINE_SCRIPT, caminhoDoPlano, "--exigir-validacao-semantica"].concat(
planoEditorialIdCarregado === null ? [] : ["--plano-id", String(planoEditorialIdCarregado), "--banco", ANALISES_DB_PATH]
),
function (line) { silenceLog(line); },
function (code, lastErr, lastOutput) {
processoAplicacao = null;
setBusy("btnApplyEditorialActions", false);
if (code !== 0) {
var detalheDaAplicacao = "";
try {
var resultadoDaAplicacao = JSON.parse(lastOutput || "{}");
var falhasDaAplicacao = (resultadoDaAplicacao.acoes || []).filter(function (acao) { return acao.sucesso === false; });
if (falhasDaAplicacao.length) detalheDaAplicacao = falhasDaAplicacao.map(function (acao) { return acao.detalhe; }).join(" | ");
if (!detalheDaAplicacao && resultadoDaAplicacao.verificacao && resultadoDaAplicacao.verificacao.aprovada === false) {
detalheDaAplicacao = resultadoDaAplicacao.verificacao.mensagem ||
(resultadoDaAplicacao.verificacao.divergencias || []).join(" | ");
}
if (!detalheDaAplicacao && resultadoDaAplicacao.erro) detalheDaAplicacao = resultadoDaAplicacao.erro;
} catch (_) {}
setStep(3, "error", "Bloqueado");
var mensagemDaFalha = detalheDaAplicacao || lastErr || "A engine não retornou detalhes.";
taskEnd(false, "O plano não foi aplicado (código " + code + ").", mensagemDaFalha);
silenceLog("Plano não aplicado (código " + code + "): " + mensagemDaFalha, "err");
return;
}
setStep(3, "done", "Aplicado");
var verificacaoDaAplicacao = null;
try { verificacaoDaAplicacao = JSON.parse(lastOutput || "{}").verificacao || null; } catch (_) {}
var textoDaVerificacao = verificacaoDaAplicacao && verificacaoDaAplicacao.mensagem
? " Verificação: " + verificacaoDaAplicacao.mensagem
: "";
taskEnd(true, numActions + " ação(ões) aplicadas na timeline!" + textoDaVerificacao);
silenceLog("Plano de edição aplicado com sucesso!", "ok");
if (verificacaoDaAplicacao && verificacaoDaAplicacao.avisos && verificacaoDaAplicacao.avisos.length) {
silenceLog("Atenção na verificação pós-aplicação: " + verificacaoDaAplicacao.avisos.join(" | "), "err");
}
}
);
}
function aplicarMontagemIncremental(plan, numActions) {
// No modo incremental, o gancho também é materializado numa Adjustment
// Layer para validar a montagem diretamente pelo fluxo do painel.
plan = Object.assign({}, plan, { test_adjustment_layer: true });
var summary =
numActions + " ação(ões) serão convertidas em trechos preservados.\n\n" +
"Será criada uma nova sequência clonada; a sequência original não será cortada. Confirmar?";
if (typeof window.confirm === "function" && !window.confirm(summary)) return;
setBusy("btnApplyEditorialActions", true);
setStep(3, "running", "Montando cópia…");
taskStart("Montagem incremental (teste)", "Clonando a sequência e preparando os trechos");
var caminhoDoPlano = path.join(silenceState.outputDir || os.tmpdir(), "plano-incremental-" + Date.now() + ".json");
try { fs.writeFileSync(caminhoDoPlano, JSON.stringify(plan, null, 2), "utf-8"); } catch (erroEscrita) {
setBusy("btnApplyEditorialActions", false);
setStep(3, "error", "Falhou");
taskEnd(false, "Não foi possível salvar o plano incremental.", erroEscrita.message);
return;
}
var processoIncremental = null;
taskStart("Montagem incremental (teste)", "Executando na sequência clonada", null, function () {
if (processoIncremental) processoIncremental.kill("SIGTERM");
});
processoIncremental = runStreamed(
SCANNER_PYTHON_BIN,
[MONTAGEM_INCREMENTAL_ENGINE_SCRIPT, caminhoDoPlano],
function (line) { silenceLog(line); },
function (code, lastErr, lastOutput) {
processoIncremental = null;
setBusy("btnApplyEditorialActions", false);
if (code !== 0) {
setStep(3, "error", "Falhou");
var detalhe = lastErr || "A montagem incremental não retornou detalhes.";
try { detalhe = JSON.parse(lastOutput || "{}").erro || detalhe; } catch (_) {}
taskEnd(false, "A montagem incremental falhou.", detalhe);
silenceLog("Montagem incremental não aplicada: " + detalhe, "err");
return;
}
setStep(3, "done", "Cópia montada");
var resultado = null;
try { resultado = JSON.parse(lastOutput || "{}").resultado || null; } catch (_) {}
var nome = resultado && resultado.destinationSequenceName ? resultado.destinationSequenceName : "nova sequência";
taskEnd(true, "Montagem incremental concluída em " + nome + ".");
silenceLog("Montagem incremental concluída em " + nome + ".", "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);
invalidarVinculoDoPlanoEditorial();
}
} 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-<model>")
// que contém um model.bin de fato baixado para o modelo pedido.
function findModelFolder(model) {
try {
var modelsPath = effectiveModelsPath();
if (!fs.existsSync(modelsPath)) return null;
var entries = fs.readdirSync(modelsPath);
for (var i = 0; i < entries.length; i++) {
// ex: "models--Systran--faster-whisper-small" for model "small"
if (entries[i].indexOf("models--") !== 0) continue;
if (entries[i].toLowerCase().indexOf(("faster-whisper-" + model).toLowerCase()) === -1) continue;
var fullDir = path.join(modelsPath, entries[i]);
var snapshotsDir = path.join(fullDir, "snapshots");
if (!fs.existsSync(snapshotsDir)) continue;
var snaps = fs.readdirSync(snapshotsDir);
for (var j = 0; j < snaps.length; j++) {
if (fs.existsSync(path.join(snapshotsDir, snaps[j], "model.bin"))) return fullDir;
}
}
return null;
} catch (e) {
return null;
}
}
function isModelDownloaded(model) {
return !!findModelFolder(model);
}
function deleteModel(model) {
var folder = findModelFolder(model);
if (!folder) return;
var confirmMsg = "Excluir o modelo \"" + model + "\" baixado (" + folder + ")? Isso libera espaço em disco; será baixado de novo se você precisar dele depois.";
if (typeof window.confirm === "function" && !window.confirm(confirmMsg)) return;
try {
fs.rmSync(folder, { recursive: true, force: true });
modelsLog("Modelo " + model + " excluído.", "ok");
showToast("ok", "Modelo " + model + " excluído.");
} catch (e) {
modelsLog("Falha ao excluir " + model + ": " + e.message, "err");
}
renderModelList();
}
function modelsLog(msg, cls) {
var el = document.getElementById("modelsLog");
if (!el) return;
var entry = document.createElement("div");
entry.className = "log-entry " + (cls || "");
var ts = new Date().toLocaleTimeString();
entry.textContent = "[" + ts + "] " + msg;
el.appendChild(entry);
el.scrollTop = el.scrollHeight;
while (el.children.length > 100) el.removeChild(el.firstChild);
}
function renderLanguageOptions() {
var select = document.getElementById("transcribeLanguage");
if (!select) return;
select.innerHTML = "";
LANGUAGE_CATALOG.forEach(function (lang) {
var opt = document.createElement("option");
opt.value = lang.value;
opt.textContent = lang.label;
select.appendChild(opt);
});
}
function renderModelList() {
var list = document.getElementById("modelList");
if (!list) return;
list.innerHTML = "";
var defaultModel = loadDefaultModel();
MODEL_CATALOG.forEach(function (entry) {
var model = entry.value;
var folder = findModelFolder(model);
var downloaded = !!folder;
var isDefault = model === defaultModel;
var row = document.createElement("div");
row.className = "model-row";
row.id = "modelRow-" + model;
var top = document.createElement("div");
top.className = "model-row-top";
var main = document.createElement("div");
main.className = "model-main";
var name = document.createElement("span");
name.className = "model-name";
name.textContent = entry.label;
main.appendChild(name);
var meta = document.createElement("div");
meta.className = "model-meta";
var sizeBadge = document.createElement("span");
sizeBadge.className = "model-badge model-badge-size";
sizeBadge.textContent = "≈ " + formatMB(downloaded ? folderSizeMB(folder) : entry.approxMB) + (downloaded ? "" : " (aprox.)");
meta.appendChild(sizeBadge);
var stateBadge = document.createElement("span");
stateBadge.className = "model-badge" + (downloaded ? " model-badge-ready" : "");
stateBadge.textContent = downloaded ? "Baixado" : "Não baixado";
meta.appendChild(stateBadge);
if (isDefault) {
var defaultBadge = document.createElement("span");
defaultBadge.className = "model-badge model-badge-star";
defaultBadge.textContent = "★ Padrão";
meta.appendChild(defaultBadge);
}
main.appendChild(meta);
top.appendChild(main);
var actions = document.createElement("div");
actions.className = "model-actions";
var primaryBtn = document.createElement("button");
primaryBtn.type = "button";
if (isDefault) {
primaryBtn.className = "button button-primary model-btn-active";
primaryBtn.textContent = "Ativo";
primaryBtn.disabled = true;
} else if (downloaded) {
primaryBtn.className = "button button-ghost";
primaryBtn.textContent = "Usar como padrão";
primaryBtn.onclick = function () { setDefaultModel(model); };
} else {
primaryBtn.className = "button button-primary";
primaryBtn.textContent = "Baixar";
primaryBtn.onclick = function () { downloadModel(model, primaryBtn, entry.approxMB); };
}
actions.appendChild(primaryBtn);
if (downloaded && !isDefault) {
var deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "link-button";
deleteBtn.textContent = "Excluir";
deleteBtn.onclick = function () { deleteModel(model); };
actions.appendChild(deleteBtn);
}
top.appendChild(actions);
row.appendChild(top);
// Barra de progresso do download, na própria linha do modelo.
var progress = document.createElement("div");
progress.className = "model-progress";
progress.id = "modelProgress-" + model;
progress.hidden = true;
progress.innerHTML =
'<div class="progress-track" data-mode="indeterminate"><div class="progress-fill" style="width:0%"></div></div>' +
'<span class="model-progress-label"></span>';
row.appendChild(progress);
list.appendChild(row);
});
}
function downloadModel(model, btn, approxMB) {
btn.disabled = true;
btn.classList.add("is-busy");
modelsLog("Baixando modelo " + model + "… isso pode levar alguns minutos.");
var box = document.getElementById("modelProgress-" + model);
var track = box ? box.querySelector(".progress-track") : null;
var fill = box ? box.querySelector(".progress-fill") : null;
var label = box ? box.querySelector(".model-progress-label") : null;
if (box) box.hidden = false;
function mirror(info) {
if (!box) return;
if (typeof info.pct === "number" && isFinite(info.pct)) {
track.setAttribute("data-mode", "determinate");
fill.style.width = Math.max(0, Math.min(100, info.pct)) + "%";
} else {
track.setAttribute("data-mode", "indeterminate");
}
label.textContent = info.detail ? info.stage + " · " + info.detail : info.stage || "";
}
taskStart("Baixando o modelo " + model, "Preparando o download", mirror);
runStreamed(
SILENCE_PYTHON_BIN,
[
SILENCE_DOWNLOAD_SCRIPT,
"--model", model,
"--models-path", effectiveModelsPath(),
"--approx-mb", String(approxMB || 0),
],
function (line) { modelsLog(line); },
function (code, lastErr) {
btn.classList.remove("is-busy");
if (code !== 0) {
if (box) box.hidden = true;
modelsLog("Falha ao baixar " + model + " (código " + code + ").", "err");
taskEnd(false, "Falha ao baixar o modelo " + model + " (código " + code + ").", lastErr);
btn.disabled = false;
return;
}
modelsLog("Modelo " + model + " baixado com sucesso.", "ok");
taskEnd(true, "Modelo " + model + " pronto para uso.");
renderModelList();
},
hfSpawnOptions()
);
}
// ---- Init ----
(function init() {
// 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("projeto");
// 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();
var campoDoPlanoEditorial = document.getElementById("editorialPlanJson");
if (campoDoPlanoEditorial) {
campoDoPlanoEditorial.addEventListener("input", invalidarVinculoDoPlanoEditorial);
}
// 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(2, "locked", "Aguardando análise");
setStep(3, "locked", "Aguardando análise");
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);
})();