- reorganizado o fluxo de edição para validar a análise do Scanner, selecionar o tipo de vídeo e enviar suas instruções no JSON. - banco de análises: métricas de fala viraram colunas, busca lexical FTS5, enunciados com embeddings, views achatadas de leitura (fala/palavra/linha do tempo/fala com visual), ingestor do pipeline de voz e gravação idempotente de evidências visuais e cenas. - retakes passam a ser gravados no banco de análises (SQLite) em vez de JSON por caso, com status de revisão persistido. - planos de edição e suas aplicações passam a ser registrados no banco, em vez de se perderem no arquivo temporário. - comando de limpeza das evidências visuais e cenas duplicadas por execuções antigas do Scanner. - análise visual: OpenCV (rostos) e PySceneDetect passam a entrar no detector local por padrão; novo adapter InsightFace gera assinatura facial (embedding) para reconhecer a mesma pessoa entre tomadas, gravada como evidência visual no banco. - corrigido: métricas de fala (energia, pitch, velocidade) agora gravam nas colunas dedicadas, não só no JSON; a view fala+visual passa a casar por vídeo e tempo, já que o mesmo arquivo entra na timeline como clipes distintos de vídeo e áudio; views são recriadas a cada abertura do banco para uma correção de consulta chegar a bancos já existentes. - reordenadas as abas do painel CEP para Scanner, Refinar e Editar vídeo Resumo: - 24 arquivos alterados - 9 novos - 15 modificados - 0 removidos 15 files changed, 872 insertions(+), 29 deletions(-) Arquivos: - .jhonny/analises.db - code/cep-plugin/index.html - code/cep-plugin/main.js - code/engine/aplicar_plano_de_edicao.py - code/engine/integracoes/visual/README.md - code/engine/integracoes/visual/__init__.py - code/engine/integracoes/visual/analisadores.py - code/engine/persistencia/__init__.py - code/engine/persistencia/esquema.py - code/engine/persistencia/repositorio_de_analises_sqlite.py - code/engine/persistencia/repositorio_de_retakes_sqlite.py - code/engine/requirements-visual.txt - code/engine/scanner/configuracao_visual.py - code/engine/scanner/visual.py - code/engine/testes/test_analise_visual_local.py - .jhonny/analises.db.pos-scanner-084841 - code/engine/integracoes/embeddings/ - code/engine/limpar_duplicatas.py - code/engine/persistencia/busca_de_conteudo.py - code/engine/persistencia/enunciados.py - code/engine/persistencia/ingestao_de_voz.py - code/engine/persistencia/limpeza.py - code/engine/persistencia/repositorio_de_planos.py - code/engine/testes/test_busca_de_conteudo.py
4022 lines
188 KiB
JavaScript
Executable File
4022 lines
188 KiB
JavaScript
Executable File
/* MCP for Adobe Premiere Pro - CEP Plugin Main Script
|
||
* Polls a temp directory for command files (.jsx), executes them
|
||
* in Premiere Pro's ExtendScript engine, and writes results back. */
|
||
|
||
var cs = new CSInterface();
|
||
var bridgeRunning = false;
|
||
var pollInterval = null;
|
||
var commandCount = 0;
|
||
var tempDir = "";
|
||
var POLL_MS = 200;
|
||
var HEARTBEAT_MS = 1000;
|
||
var heartbeatInterval = null;
|
||
|
||
// ---- Logging ----
|
||
function log(msg, cls) {
|
||
var el = document.getElementById("log");
|
||
var entry = document.createElement("div");
|
||
entry.className = "log-entry " + (cls || "");
|
||
var ts = new Date().toLocaleTimeString();
|
||
entry.textContent = "[" + ts + "] " + msg;
|
||
el.appendChild(entry);
|
||
el.scrollTop = el.scrollHeight;
|
||
// Keep max 100 entries
|
||
while (el.children.length > 100) el.removeChild(el.firstChild);
|
||
}
|
||
|
||
// ---- Painel de progresso (task dock) ----
|
||
// Antes, tudo que rodava só aparecia como linhas no log lá embaixo e a pessoa
|
||
// não sabia o que estava acontecendo. Agora todo processo longo abre um card
|
||
// reposicionado na aba de origem com etapa atual, barra e tempo decorrido.
|
||
var taskState = { active: false, startedAt: 0, timer: null, clearTimer: null, mirror: null, cancel: null, canceled: false, originTab: "silence", originPanelId: "tabSilence", errorText: "" };
|
||
|
||
var TIPOS_VIDEO_CHAVE = "schedule_light_tipos_video";
|
||
var tipoVideoAtualId = null;
|
||
var tiposVideoPadrao = [
|
||
{ id: "midia-social", nome: "Conteúdo para mídia social", duracao_maxima_segundos: 60, descricao: "Vídeo curto para interromper a rolagem, comunicar uma ideia e estimular uma ação.", canais: "Instagram Reels, TikTok, Shorts", proporcao: "9:16", ritmo: "agil", emocao: "Curiosidade e energia", storytelling: true, estrutura: "Gancho nos primeiros 2 segundos → desenvolvimento em blocos curtos → CTA direto.", cortes: "Cortes rápidos, remover pausas e repetições; preservar frases de impacto. Usar jump cuts com moderação.", broll: "Inserir imagens de apoio a cada mudança de ideia, preferindo planos que reforcem a fala.", texto: "Legendas sempre; destacar palavras-chave na tela e usar título curto. CTA visual no final.", som: "Voz sempre inteligível e à frente da trilha; música energética em volume baixo.", diretriz: "Priorize retenção e clareza. Cada corte deve manter o ritmo e reforçar uma única ideia por vez." },
|
||
{ id: "depoimento", nome: "Depoimento de cliente", duracao_maxima_segundos: 120, descricao: "Relato autêntico que gera identificação, confiança e prova social.", canais: "Site, redes sociais, apresentação comercial", proporcao: "adaptavel", ritmo: "equilibrado", emocao: "Empatia e confiança", storytelling: true, estrutura: "Problema vivido → experiência com a solução → transformação → recomendação.", cortes: "Cortar hesitações e desvios sem deixar a fala artificial; manter reações, pausas emocionais e frases espontâneas.", broll: "Cobrir cortes com cenas do cliente usando o produto, ambiente real ou detalhes que comprovem o relato.", texto: "Nome e contexto do depoente; destacar a transformação em frases curtas; legendas completas.", som: "Voz natural, limpa e próxima; trilha emocional discreta, crescendo apenas na transformação.", diretriz: "Proteja a autenticidade do depoimento. A emoção deve vir da fala e das evidências, não de efeitos excessivos." },
|
||
{ id: "entrevista", nome: "Entrevista", duracao_maxima_segundos: 600, descricao: "Conversa estruturada para extrair conhecimento, opinião ou história de uma pessoa.", canais: "YouTube, podcast em vídeo, cortes sociais", proporcao: "16:9", ritmo: "equilibrado", emocao: "Interesse e credibilidade", storytelling: true, estrutura: "Apresentação → perguntas essenciais → aprofundamento → síntese e encerramento.", cortes: "Remover redundâncias e silêncios longos; preservar respostas completas, escuta, reação e contexto da pergunta.", broll: "Usar planos de reação, detalhes do ambiente e imagens relacionadas ao tema para quebrar a monotonia.", texto: "Identificação do entrevistado, perguntas resumidas quando necessário, capítulos ou palavras-chave.", som: "Diálogo consistente entre os canais; reduzir ruído e nivelar vozes sem tirar naturalidade.", diretriz: "Mantenha a linha de raciocínio da conversa. Não cortar uma resposta de modo que altere o sentido ou a intenção do entrevistado." },
|
||
{ id: "institucional", nome: "Vídeo institucional", duracao_maxima_segundos: 180, descricao: "Apresentação da empresa, marca, cultura ou solução com percepção de valor.", canais: "Site, eventos, apresentações, LinkedIn", proporcao: "16:9", ritmo: "progressivo", emocao: "Segurança e inspiração", storytelling: true, estrutura: "Contexto ou propósito → pessoas e operação → diferenciais → prova → convite para conhecer.", cortes: "Ritmo elegante; evitar excesso de cortes. Alternar entrevistas, planos de ambiente e detalhes de operação.", broll: "B-roll planejado para mostrar pessoas, processos, produto e escala; toda imagem deve provar uma mensagem.", texto: "Títulos limpos, dados essenciais e assinatura da marca; evitar excesso de texto na tela.", som: "Trilha cinematográfica ou institucional discreta; voz firme, clara e com pausas bem preservadas.", diretriz: "Construa confiança e percepção de valor. Use consistência visual e sonora; efeitos só quando ajudarem a mensagem da marca." },
|
||
{ id: "educativo-storytelling", nome: "Educativo com storytelling", duracao_maxima_segundos: 240, descricao: "Explicação didática que ensina uma ideia por meio de uma história, caso ou problema concreto.", canais: "YouTube, curso, LinkedIn, redes sociais", proporcao: "adaptavel", ritmo: "progressivo", emocao: "Curiosidade e clareza", storytelling: true, estrutura: "Problema ou pergunta → história/exemplo → explicação → aplicação prática → resumo.", cortes: "Cortar repetições, mas manter a sequência pedagógica; inserir pausas após conceitos importantes.", broll: "Usar diagramas, tela, exemplos e imagens que tornem o conceito visível; sincronizar apoio com a explicação.", texto: "Título do conceito, palavras-chave e resumo final; legendas e marcadores de etapas.", som: "Voz em primeiro plano; trilha quase imperceptível; pequenos efeitos apenas para sinalizar transições.", diretriz: "Ensine uma coisa por vez. O espectador deve entender o conceito sem depender apenas de texto ou de efeitos visuais." }
|
||
];
|
||
|
||
function tipoVideoCompletar(tipo) {
|
||
var configuracoes = {
|
||
"midia-social": { categoria: "Comercial", objetivo: "Gerar retenção e ação", objetivo_secundario: "Comunicar uma ideia rapidamente", publico: "Público das redes sociais", mensagem: "Uma ideia clara por vídeo.", tipo_narrativa: "Problema → solução → benefício", foco_narrativo: "Gancho e CTA", intensidade: 4, tom: "Positivo", construcao_emocao: "Ritmo ágil, cortes de impacto, legendas e trilha energética.", duracao_minima: 15, prioridades: ["Retenção", "Clareza", "Mensagem", "Ritmo", "Estética"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: false, usa_zoom: true, usa_musica: true, intensidade_musica: 25, prioridade_voz: "Muito alta", restricoes: "Evitar excesso de informação e cortes sem função.", qualidade: "A abertura prende atenção e a mensagem é compreendida sem áudio." },
|
||
"depoimento": { categoria: "Depoimento", objetivo: "Gerar credibilidade e confiança", objetivo_secundario: "Criar identificação emocional", publico: "Clientes em potencial", mensagem: "A experiência real comprova o valor da solução.", tipo_narrativa: "História pessoal", foco_narrativo: "Problema → experiência → transformação → recomendação", intensidade: 3, tom: "Íntimo", construcao_emocao: "Preservar pausas naturais, reações e imagens reais; usar trilha discreta.", duracao_minima: 30, prioridades: ["Autenticidade", "Clareza", "Conexão emocional", "Ritmo", "Estética"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: false, usa_zoom: false, usa_musica: true, intensidade_musica: 20, prioridade_voz: "Muito alta", restricoes: "Não alterar o sentido das frases nem deixar o depoimento artificial.", qualidade: "A pessoa parece verdadeira e o resultado do relato fica evidente." },
|
||
"entrevista": { categoria: "Entrevista", objetivo: "Transmitir conhecimento e autoridade", objetivo_secundario: "Organizar a conversa por temas", publico: "Pessoas interessadas no assunto", mensagem: "O conteúdo da conversa é compreendido com contexto.", tipo_narrativa: "Pergunta e resposta", foco_narrativo: "Pergunta → resposta → aprofundamento", intensidade: 2, tom: "Informativo", construcao_emocao: "Reações, escuta e pausas que preservem a naturalidade da conversa.", duracao_minima: 60, prioridades: ["Clareza", "Conteúdo", "Contexto", "Ritmo", "Estética"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: false, usa_zoom: true, usa_musica: false, intensidade_musica: 10, prioridade_voz: "Muito alta", restricoes: "Não cortar frases de modo que prejudique o raciocínio do entrevistado.", qualidade: "As respostas mantêm contexto, lógica e informação relevante." },
|
||
"institucional": { categoria: "Institucional", objetivo: "Apresentar valor e confiança na marca", objetivo_secundario: "Mostrar pessoas, processo e diferenciais", publico: "Clientes, parceiros e equipe", mensagem: "A marca entrega valor de forma consistente.", tipo_narrativa: "Jornada / transformação", foco_narrativo: "Propósito → pessoas → operação → prova", intensidade: 4, tom: "Inspirador", construcao_emocao: "Trilha progressiva, imagens de pessoas e planos de operação.", duracao_minima: 45, prioridades: ["Confiança", "Mensagem", "Narrativa", "Estética", "Ritmo"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: true, usa_zoom: false, usa_musica: true, intensidade_musica: 35, prioridade_voz: "Alta", restricoes: "Evitar efeitos decorativos que não reforcem a mensagem da marca.", qualidade: "O vídeo transmite profissionalismo, clareza e percepção de valor." },
|
||
"educativo-storytelling": { categoria: "Educativo", objetivo: "Ensinar um assunto com clareza", objetivo_secundario: "Aumentar retenção e aplicação prática", publico: "Pessoas que precisam aprender o assunto", mensagem: "O espectador consegue entender e aplicar o conceito.", tipo_narrativa: "Explicação passo a passo", foco_narrativo: "Problema → exemplo → explicação → aplicação", intensidade: 2, tom: "Informativo", construcao_emocao: "Curiosidade inicial, exemplos visuais e pausas após conceitos importantes.", duracao_minima: 60, prioridades: ["Clareza", "Organização", "Didática", "Retenção", "Estética"], usa_broll: true, usa_legendas: true, usa_textos: true, usa_graficos: true, usa_zoom: true, usa_musica: false, intensidade_musica: 5, prioridade_voz: "Muito alta", restricoes: "Não cortar exemplos ou transições que prejudiquem a explicação.", qualidade: "O conteúdo fica fácil de acompanhar, revisar e aplicar." }
|
||
}[tipo.id] || {};
|
||
var completo = Object.assign({ ativo: true, categoria: "Personalizado", objetivo: "", objetivo_secundario: "", publico: "", mensagem: "", tipo_narrativa: "História pessoal", foco_narrativo: "", intensidade: 3, tom: "Positivo", construcao_emocao: "", duracao_minima: 0, prioridades: ["Clareza", "Mensagem", "Ritmo", "Conexão emocional", "Estética"], usa_broll: false, usa_legendas: true, usa_textos: false, usa_graficos: false, usa_zoom: false, usa_musica: false, intensidade_musica: 20, prioridade_voz: "Alta", restricoes: "", qualidade: "" }, configuracoes, tipo);
|
||
return completo;
|
||
}
|
||
|
||
function taskEl(id) { return document.getElementById(id); }
|
||
|
||
function formatClock(ms) {
|
||
var total = Math.max(0, Math.round(ms / 1000));
|
||
var mm = String(Math.floor(total / 60)).padStart(2, "0");
|
||
var ss = String(total % 60).padStart(2, "0");
|
||
return mm + ":" + ss;
|
||
}
|
||
|
||
function activeTab() {
|
||
var panel = document.querySelector(".tab-panel:not([hidden])");
|
||
return panel ? panel.id.replace(/^tab/, "").toLowerCase() : "silence";
|
||
}
|
||
|
||
function taskMoveToOriginTab() {
|
||
var dock = taskEl("taskDock");
|
||
var nome = taskState.originTab;
|
||
var panel = document.getElementById("tab" + nome.charAt(0).toUpperCase() + nome.slice(1));
|
||
if (!dock || !panel) return;
|
||
panel.insertBefore(dock, panel.firstChild);
|
||
taskState.originPanelId = panel.id;
|
||
}
|
||
|
||
function taskStart(title, stage, mirror, cancel) {
|
||
if (taskState.clearTimer) { clearTimeout(taskState.clearTimer); taskState.clearTimer = null; }
|
||
taskState.active = true;
|
||
taskState.startedAt = Date.now();
|
||
taskState.mirror = mirror || null;
|
||
taskState.cancel = typeof cancel === "function" ? cancel : null;
|
||
taskState.canceled = false;
|
||
taskState.originTab = activeTab();
|
||
taskState.errorText = "";
|
||
taskMoveToOriginTab();
|
||
|
||
var dock = taskEl("taskDock");
|
||
dock.removeAttribute("data-result");
|
||
dock.hidden = false;
|
||
taskEl("taskTitle").textContent = title;
|
||
taskEl("taskStage").textContent = stage || "Preparando…";
|
||
taskEl("taskPct").textContent = "";
|
||
taskEl("taskElapsed").textContent = "00:00";
|
||
taskEl("taskTrack").setAttribute("data-mode", "indeterminate");
|
||
taskEl("taskFill").style.width = "0%";
|
||
taskEl("taskCopy").hidden = true;
|
||
taskEl("taskErrorDetails").hidden = true;
|
||
taskEl("taskErrorDetails").textContent = "";
|
||
taskEl("taskCancel").hidden = !taskState.cancel;
|
||
taskEl("taskCancel").disabled = false;
|
||
taskEl("taskCancel").textContent = "Cancelar";
|
||
hideToast();
|
||
|
||
if (taskState.timer) clearInterval(taskState.timer);
|
||
taskState.timer = setInterval(function () {
|
||
taskEl("taskElapsed").textContent = formatClock(Date.now() - taskState.startedAt);
|
||
}, 1000);
|
||
}
|
||
|
||
// info: { stage, pct, detail }
|
||
function taskUpdate(info) {
|
||
if (!taskState.active) return;
|
||
if (taskState.mirror) { try { taskState.mirror(info); } catch (e) {} }
|
||
var stage = info.stage || "";
|
||
var detail = info.detail || "";
|
||
taskEl("taskStage").textContent = detail ? stage + " · " + detail : stage;
|
||
|
||
if (typeof info.pct === "number" && isFinite(info.pct)) {
|
||
taskEl("taskTrack").setAttribute("data-mode", "determinate");
|
||
taskEl("taskFill").style.width = Math.max(0, Math.min(100, info.pct)) + "%";
|
||
taskEl("taskPct").textContent = Math.round(info.pct) + "%";
|
||
} else {
|
||
taskEl("taskTrack").setAttribute("data-mode", "indeterminate");
|
||
taskEl("taskPct").textContent = "";
|
||
}
|
||
}
|
||
|
||
function taskEnd(ok, message, technicalDetails) {
|
||
if (taskState.timer) { clearInterval(taskState.timer); taskState.timer = null; }
|
||
taskState.active = false;
|
||
|
||
var dock = taskEl("taskDock");
|
||
var cancelado = taskState.canceled;
|
||
dock.setAttribute("data-result", cancelado ? "cancelled" : (ok ? "ok" : "err"));
|
||
taskEl("taskTrack").setAttribute("data-mode", "determinate");
|
||
taskEl("taskFill").style.width = "100%";
|
||
taskEl("taskStage").textContent = message || (cancelado ? "Cancelado" : (ok ? "Concluído" : "Falhou"));
|
||
taskEl("taskPct").textContent = ok ? "100%" : "";
|
||
taskEl("taskCancel").hidden = true;
|
||
taskEl("taskCancel").disabled = false;
|
||
if (!ok && !cancelado) {
|
||
taskState.errorText = String(technicalDetails || message || "Falha sem detalhes técnicos.");
|
||
taskEl("taskCopy").hidden = false;
|
||
taskEl("taskErrorDetails").textContent = taskState.errorText;
|
||
taskEl("taskErrorDetails").hidden = false;
|
||
if (activeTab() !== taskState.originTab) switchTab(taskState.originTab);
|
||
}
|
||
|
||
taskState.mirror = null;
|
||
taskState.cancel = null;
|
||
// O próprio card já carrega o desfecho e fica na tela por alguns segundos —
|
||
// repetir a mesma frase no toast só empilhava dois avisos idênticos.
|
||
taskState.clearTimer = setTimeout(function () {
|
||
if (!taskState.active) taskEl("taskDock").hidden = true;
|
||
}, ok ? 4000 : 9000);
|
||
}
|
||
|
||
function taskCancel() {
|
||
if (!taskState.active || typeof taskState.cancel !== "function") return;
|
||
taskState.canceled = true;
|
||
taskEl("taskCancel").disabled = true;
|
||
taskEl("taskCancel").textContent = "Cancelando…";
|
||
taskEl("taskStage").textContent = "Cancelando…";
|
||
try { taskState.cancel(); } catch (erro) { taskEnd(false, "Falha ao cancelar", erro.message); }
|
||
}
|
||
|
||
function taskCopyError() {
|
||
var texto = taskState.errorText;
|
||
if (!texto) return;
|
||
try {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(texto).then(function () { showToast("ok", "Erro técnico copiado."); }).catch(function () { taskCopyErrorFallback(texto); });
|
||
return;
|
||
}
|
||
} catch (_) {}
|
||
taskCopyErrorFallback(texto);
|
||
}
|
||
|
||
function taskCopyErrorFallback(texto) {
|
||
try {
|
||
var copia = childProcess.spawn("pbcopy");
|
||
copia.stdin.end(texto);
|
||
showToast("ok", "Erro técnico copiado.");
|
||
return;
|
||
} catch (_) {}
|
||
var area = document.createElement("textarea");
|
||
area.value = texto;
|
||
area.style.position = "fixed";
|
||
area.style.opacity = "0";
|
||
document.body.appendChild(area);
|
||
area.select();
|
||
try { document.execCommand("copy"); showToast("ok", "Erro técnico copiado."); } catch (erro) { showToast("err", "Não foi possível copiar o erro: " + erro.message); }
|
||
document.body.removeChild(area);
|
||
}
|
||
|
||
function showToast(kind, text) {
|
||
var el = document.getElementById("toast");
|
||
if (!el) return;
|
||
el.setAttribute("data-kind", kind);
|
||
document.getElementById("toastIcon").textContent = kind === "ok" ? "✓" : "!";
|
||
document.getElementById("toastText").textContent = text;
|
||
el.hidden = false;
|
||
}
|
||
|
||
function hideToast() {
|
||
var el = document.getElementById("toast");
|
||
if (el) el.hidden = true;
|
||
}
|
||
|
||
// Botão com spinner embutido: o usuário vê que aquele botão específico disparou
|
||
// o processo, sem precisar caçar no log.
|
||
function setBusy(id, busy) {
|
||
var btn = document.getElementById(id);
|
||
if (!btn) return;
|
||
btn.classList.toggle("is-busy", !!busy);
|
||
btn.disabled = !!busy;
|
||
}
|
||
|
||
// ---- Estados dos passos da aba "Editar vídeo" ----
|
||
// locked = ainda não dá pra usar | ready = pode usar | running | done | error
|
||
function setStep(n, state, chipText) {
|
||
var step = document.getElementById("step" + n);
|
||
var chip = document.getElementById("step" + n + "Chip");
|
||
if (!step) return;
|
||
step.setAttribute("data-state", state);
|
||
if (chip && chipText) chip.textContent = chipText;
|
||
else if (chip) {
|
||
chip.textContent =
|
||
state === "locked" ? "Bloqueado" :
|
||
state === "ready" ? "Pronto" :
|
||
state === "running" ? "Rodando…" :
|
||
state === "done" ? "Concluído" : "Erro";
|
||
}
|
||
}
|
||
|
||
function stepState(n) {
|
||
var step = document.getElementById("step" + n);
|
||
return step ? step.getAttribute("data-state") : "locked";
|
||
}
|
||
|
||
// ---- Tabs ----
|
||
function switchTab(name) {
|
||
var tabs = [
|
||
{ key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" },
|
||
{ key: "refinar", panel: "tabRefinar", btn: "tabBtnRefinar" },
|
||
{ key: "silence", panel: "tabSilence", btn: "tabBtnSilence" },
|
||
{ key: "retakes", panel: "tabRetakes", btn: "tabBtnRetakes" },
|
||
{ key: "analises", panel: "tabAnalises", btn: "tabBtnAnalises" },
|
||
{ key: "tipos-video", panel: "tabTiposVideo", btn: "tabBtnTiposVideo" },
|
||
{ key: "bridge", panel: "tabBridge", btn: "tabBtnBridge" },
|
||
{ key: "models", panel: "tabModels", btn: "tabBtnModels" },
|
||
{ key: "settings", panel: "tabSettings", btn: "tabBtnSettings" },
|
||
{ key: "testar-acoes", panel: "tabTestarAcoes", btn: "tabBtnTestarAcoes" },
|
||
{ key: "legendas", panel: "tabLegendas", btn: "tabBtnLegendas" },
|
||
];
|
||
tabs.forEach(function (t) {
|
||
var active = t.key === name;
|
||
var panel = document.getElementById(t.panel);
|
||
var btn = document.getElementById(t.btn);
|
||
if (!panel || !btn) return;
|
||
panel.hidden = !active;
|
||
btn.classList.toggle("active", active);
|
||
btn.setAttribute("aria-selected", String(active));
|
||
});
|
||
if (name === "models") renderModelList();
|
||
if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); renderizarTiposDeVideoParaEdicao(); }
|
||
if (name === "settings") renderEditorPersonalities();
|
||
if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); scannerListarAnalisesExistentes(); }
|
||
if (name === "refinar") refinarAoAbrir();
|
||
if (name === "retakes") retakesAoAbrir();
|
||
if (name === "analises") analisesAoAbrir();
|
||
if (name === "tipos-video") tiposVideoAoAbrir();
|
||
}
|
||
|
||
var LEGENDAS_CONFIGURACAO_CHAVE = "schedule_light_configuracao_legendas";
|
||
|
||
function configuracaoDeLegendasPadrao() {
|
||
return { habilitadas: false, estilo: "word_top_down", faixa_video: 3, max_palavras: 5, max_caracteres: 32, mogrt_path: "" };
|
||
}
|
||
|
||
function carregarConfiguracaoDeLegendas() {
|
||
var configuracao = configuracaoDeLegendasPadrao();
|
||
try {
|
||
var salva = JSON.parse(localStorage.getItem(LEGENDAS_CONFIGURACAO_CHAVE) || "null");
|
||
if (salva && typeof salva === "object") configuracao = Object.assign(configuracao, salva);
|
||
} catch (e) {}
|
||
return configuracao;
|
||
}
|
||
|
||
function restaurarConfiguracaoDeLegendas() {
|
||
var configuracao = carregarConfiguracaoDeLegendas();
|
||
document.getElementById("legendasHabilitadas").checked = configuracao.habilitadas;
|
||
document.getElementById("legendasEstilo").value = configuracao.estilo;
|
||
document.getElementById("legendasFaixa").value = configuracao.faixa_video;
|
||
document.getElementById("legendasMaxPalavras").value = configuracao.max_palavras;
|
||
document.getElementById("legendasMaxCaracteres").value = configuracao.max_caracteres;
|
||
document.getElementById("legendasMogrtPath").value = configuracao.mogrt_path;
|
||
}
|
||
|
||
function salvarConfiguracaoDeLegendas() {
|
||
var configuracao = {
|
||
habilitadas: !!document.getElementById("legendasHabilitadas").checked,
|
||
estilo: document.getElementById("legendasEstilo").value,
|
||
faixa_video: Number(document.getElementById("legendasFaixa").value),
|
||
max_palavras: Number(document.getElementById("legendasMaxPalavras").value),
|
||
max_caracteres: Number(document.getElementById("legendasMaxCaracteres").value),
|
||
mogrt_path: document.getElementById("legendasMogrtPath").value.trim(),
|
||
};
|
||
try { localStorage.setItem(LEGENDAS_CONFIGURACAO_CHAVE, JSON.stringify(configuracao)); } catch (e) {}
|
||
document.getElementById("legendasStatus").textContent = "Configuração de legendas salva neste computador.";
|
||
}
|
||
|
||
var legendasGeradas = [];
|
||
|
||
function legendaNumeroParaTempo(segundos) {
|
||
var horas = Math.floor(segundos / 3600);
|
||
var minutos = Math.floor((segundos % 3600) / 60);
|
||
var segundosInteiros = Math.floor(segundos % 60);
|
||
var milissegundos = Math.round((segundos - Math.floor(segundos)) * 1000);
|
||
if (milissegundos >= 1000) { segundosInteiros++; milissegundos = 0; }
|
||
return [horas, minutos, segundosInteiros].map(function (n) { return String(n).padStart(2, "0"); }).join(":") + "," + String(milissegundos).padStart(3, "0");
|
||
}
|
||
|
||
function legendasExtrairPalavras(transcricao) {
|
||
var segmentos = transcricao.segmentos || transcricao.segments || [];
|
||
var palavras = [];
|
||
segmentos.forEach(function (segmento) {
|
||
var itens = segmento.palavras || segmento.words || [];
|
||
itens.forEach(function (palavra) {
|
||
var texto = String(palavra.texto || palavra.word || "").trim();
|
||
var inicio = Number(palavra.inicio !== undefined ? palavra.inicio : palavra.start);
|
||
var fim = Number(palavra.fim !== undefined ? palavra.fim : palavra.end);
|
||
if (texto && isFinite(inicio) && isFinite(fim) && fim > inicio) palavras.push({ texto: texto, inicio: inicio, fim: fim });
|
||
});
|
||
});
|
||
return palavras.sort(function (a, b) { return a.inicio - b.inicio; });
|
||
}
|
||
|
||
function legendasIntervaloCortado(palavra, cortes) {
|
||
return cortes.some(function (corte) { return palavra.inicio < corte.end && palavra.fim > corte.start; });
|
||
}
|
||
|
||
function legendasGerarBlocos(palavras, cortes, configuracao) {
|
||
var mantidas = palavras.filter(function (palavra) { return !legendasIntervaloCortado(palavra, cortes); });
|
||
var blocos = [], bloco = [], caracteres = 0;
|
||
function adicionar() {
|
||
if (!bloco.length) return;
|
||
var primeiro = bloco[0], ultimo = bloco[bloco.length - 1];
|
||
// Para o primeiro corte, todos os cortes anteriores são considerados.
|
||
var inicio = primeiro.inicio - cortes.reduce(function (total, corte) { return total + (corte.end <= primeiro.inicio ? corte.end - corte.start : 0); }, 0);
|
||
var fim = ultimo.fim - cortes.reduce(function (total, corte) { return total + (corte.end <= ultimo.fim ? corte.end - corte.start : 0); }, 0);
|
||
blocos.push({ texto: bloco.map(function (item) { return item.texto; }).join(" "), inicio: inicio, fim: fim });
|
||
bloco = []; caracteres = 0;
|
||
}
|
||
mantidas.forEach(function (palavra) {
|
||
var espaco = bloco.length ? 1 : 0;
|
||
if (bloco.length && (bloco.length >= configuracao.max_palavras || caracteres + espaco + palavra.texto.length > configuracao.max_caracteres)) adicionar();
|
||
bloco.push(palavra); caracteres += (bloco.length > 1 ? 1 : 0) + palavra.texto.length;
|
||
});
|
||
adicionar();
|
||
return blocos;
|
||
}
|
||
|
||
function legendasRenderizar() {
|
||
var lista = document.getElementById("legendasLista");
|
||
document.getElementById("legendasContagem").textContent = "(" + legendasGeradas.length + ")";
|
||
lista.textContent = "";
|
||
if (!legendasGeradas.length) { lista.innerHTML = '<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";
|
||
|
||
function testarAcoesCarregarExemplo() {
|
||
var exemplo = {
|
||
schema_version: "1.0",
|
||
source: "video.mp4",
|
||
actions: [{ kind: "cut", start: 0.0, end: 1.0, reason: "Teste de corte inicial pela engine Python." }]
|
||
};
|
||
document.getElementById("testarAcoesJson").value = JSON.stringify(exemplo, null, 2);
|
||
testarAcoesAtualizarStatus("Exemplo carregado. Ajuste o source e os tempos antes de executar.", "");
|
||
}
|
||
|
||
function testarAcoesLog(mensagem, classe) {
|
||
var alvo = document.getElementById("testarAcoesLog");
|
||
if (!alvo) return;
|
||
var linha = document.createElement("p");
|
||
linha.textContent = mensagem;
|
||
if (classe) linha.className = classe;
|
||
alvo.appendChild(linha);
|
||
alvo.scrollTop = alvo.scrollHeight;
|
||
}
|
||
|
||
function testarAcoesAtualizarStatus(mensagem, classe) {
|
||
var status = document.getElementById("testarAcoesStatus");
|
||
if (status) { status.textContent = mensagem; status.className = "field-help " + (classe || ""); }
|
||
}
|
||
|
||
function testarAcoesExecutar() {
|
||
var campo = document.getElementById("testarAcoesJson");
|
||
var texto = campo.value.trim();
|
||
var plano;
|
||
if (!texto) { testarAcoesAtualizarStatus("Cole um plano JSON antes de executar.", "err"); return; }
|
||
try { plano = JSON.parse(texto); } catch (erro) {
|
||
testarAcoesAtualizarStatus("JSON inválido: " + erro.message, "err");
|
||
testarAcoesLog("JSON inválido: " + erro.message, "err");
|
||
return;
|
||
}
|
||
if (!plano || typeof plano !== "object" || !plano.source || !Array.isArray(plano.actions) || !plano.actions.length) {
|
||
testarAcoesAtualizarStatus("O plano precisa ter source e uma lista não vazia de actions.", "err");
|
||
return;
|
||
}
|
||
var tipos = plano.actions.map(function (acao) { return acao && acao.kind; });
|
||
if (tipos.some(function (tipo) { return tipo !== "cut"; })) {
|
||
testarAcoesAtualizarStatus("Este primeiro teste aceita somente actions kind: cut.", "err");
|
||
return;
|
||
}
|
||
var entrada = path.join(os.tmpdir(), "teste-acoes-" + Date.now() + ".json");
|
||
try { fs.writeFileSync(entrada, JSON.stringify(plano, null, 2), "utf-8"); } catch (erroEscrita) {
|
||
testarAcoesAtualizarStatus("Não foi possível criar o plano temporário: " + erroEscrita.message, "err");
|
||
return;
|
||
}
|
||
var botao = document.getElementById("btnTestarAcoesExecutar");
|
||
if (botao) botao.disabled = true;
|
||
testarAcoesAtualizarStatus("A engine está conectando ao Premiere e aplicando o corte…", "");
|
||
testarAcoesLog("Plano temporário criado: " + entrada);
|
||
taskStart("Teste de ação", "Executando pela engine Python");
|
||
runStreamed(
|
||
SCANNER_PYTHON_BIN,
|
||
[TESTAR_ACOES_ENGINE_SCRIPT, entrada],
|
||
function (linha) { testarAcoesLog(linha); },
|
||
function (codigo, ultimoErro) {
|
||
if (botao) botao.disabled = false;
|
||
if (codigo !== 0) {
|
||
testarAcoesAtualizarStatus("Falha na execução. Consulte os detalhes técnicos.", "err");
|
||
testarAcoesLog(ultimoErro || "A engine terminou com código " + codigo + ".", "err");
|
||
taskEnd(false, "Teste de ação falhou", ultimoErro || null);
|
||
return;
|
||
}
|
||
testarAcoesAtualizarStatus("Corte executado pela engine Python. Verifique a timeline no Premiere.", "ok");
|
||
testarAcoesLog("Engine concluída com sucesso.", "ok");
|
||
taskEnd(true, "Teste de corte concluído");
|
||
},
|
||
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine" }
|
||
);
|
||
}
|
||
|
||
// ---- Catálogo de tipos de vídeo -------------------------------------------
|
||
// A primeira versão usa o armazenamento local do painel. O documento salvo já
|
||
// tem formato estável e pode ser migrado para o Schedule Light sem alterar o
|
||
// contrato editorial enviado para a IA.
|
||
function tiposVideoLer() {
|
||
try {
|
||
var salvo = JSON.parse(localStorage.getItem(TIPOS_VIDEO_CHAVE) || "null");
|
||
if (Array.isArray(salvo) && salvo.length) return salvo.map(tipoVideoCompletar);
|
||
var iniciais = tiposVideoPadrao.slice().map(tipoVideoCompletar);
|
||
tiposVideoSalvarNoCatalogo(iniciais);
|
||
return iniciais;
|
||
} catch (_) { return tiposVideoPadrao.slice().map(tipoVideoCompletar); }
|
||
}
|
||
|
||
function tiposVideoSalvarNoCatalogo(lista) {
|
||
localStorage.setItem(TIPOS_VIDEO_CHAVE, JSON.stringify(lista));
|
||
}
|
||
|
||
function tiposVideoAoAbrir() {
|
||
tiposVideoRenderizarLista();
|
||
if (!tipoVideoAtualId) tipoVideoNovo();
|
||
}
|
||
|
||
function tiposVideoRenderizarLista() {
|
||
var lista = document.getElementById("tiposVideoLista");
|
||
if (!lista) return;
|
||
lista.innerHTML = "";
|
||
tiposVideoLer().forEach(function (tipo) {
|
||
var item = document.createElement("div");
|
||
item.className = "video-type-row" + (tipo.id === tipoVideoAtualId ? " is-selected" : "");
|
||
item.setAttribute("role", "listitem");
|
||
item.innerHTML = '<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.");
|
||
}
|
||
|
||
// O CEP antigo pode corromper caracteres UTF-8 pela API de clipboard do Chromium.
|
||
// O pbcopy recebe bytes UTF-8 diretamente e evita essa conversão intermediária.
|
||
copiarComPbcopy(texto, function () {
|
||
try {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(texto).then(function () {
|
||
showToast("ok", mensagem);
|
||
}).catch(copiarComSelecaoDoPainel);
|
||
return;
|
||
}
|
||
} catch (_) {}
|
||
copiarComSelecaoDoPainel();
|
||
}, mensagem);
|
||
}
|
||
|
||
function copiarComPbcopy(texto, fallback, mensagem) {
|
||
try {
|
||
var copia = childProcess.spawn("pbcopy");
|
||
var finalizado = false;
|
||
function concluir(copiado) {
|
||
if (finalizado) return;
|
||
finalizado = true;
|
||
if (copiado) showToast("ok", mensagem || "Conteúdo copiado para a área de transferência.");
|
||
else fallback();
|
||
}
|
||
copia.once("error", function () { concluir(false); });
|
||
copia.once("close", function (codigo) { concluir(codigo === 0); });
|
||
copia.stdin.write(texto, "utf8");
|
||
copia.stdin.end();
|
||
} catch (_) { fallback(); }
|
||
}
|
||
|
||
// ---- Análises ---------------------------------------------------------------
|
||
// Aba que lê o banco de análises (.jhonny/analises.db) já produzido pelo
|
||
// Scanner e mostra/copiar os dois J-SOM: som (transcrição por cena) e
|
||
// imagem (evidências visuais). A leitura acontece na engine, via script
|
||
// dedicado; a aba nunca escreve no banco.
|
||
var ANALISES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/gerar_relatorio_de_analises.py";
|
||
var ANALISES_DB_PATH = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db";
|
||
var analisesRelatorioPath = null;
|
||
|
||
function analisesAoAbrir() {
|
||
document.getElementById("analisesCaminho").textContent = ANALISES_DB_PATH;
|
||
}
|
||
|
||
function analisesFormatarTempo(segundos) {
|
||
var s = Number(segundos) || 0;
|
||
var min = Math.floor(s / 60), resto = (s - min * 60).toFixed(1);
|
||
return min + ":" + (resto < 10 ? "0" : "") + resto;
|
||
}
|
||
|
||
function analisesCarregar() {
|
||
var stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||
analisesRelatorioPath = path.join(os.tmpdir(), "jhonny-analises-" + stamp + ".json");
|
||
setBusy("btnAnalisesCarregar", true); taskStart("Análises", "Lendo o banco de análises", null);
|
||
var child = childProcess.spawn(SCANNER_PYTHON_BIN, [ANALISES_ENGINE_SCRIPT, ANALISES_DB_PATH, analisesRelatorioPath],
|
||
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", stdio: ["ignore", "pipe", "pipe"] });
|
||
var stderr = "", stdout = "";
|
||
child.stdout.on("data", function (chunk) { stdout += chunk.toString(); });
|
||
child.stderr.on("data", function (chunk) { stderr += chunk.toString(); });
|
||
child.on("error", function (error) { setBusy("btnAnalisesCarregar", false); taskEnd(false, "Análises indisponíveis", error.stack || error.message); });
|
||
child.on("close", function (code) {
|
||
setBusy("btnAnalisesCarregar", false);
|
||
if (code !== 0 || !fs.existsSync(analisesRelatorioPath)) {
|
||
taskEnd(false, "Leitura do banco falhou", stderr || stdout || "Não foi possível gerar o relatório.");
|
||
return;
|
||
}
|
||
taskEnd(true, "Relatório carregado do banco");
|
||
analisesRender(JSON.parse(fs.readFileSync(analisesRelatorioPath, "utf-8")));
|
||
});
|
||
}
|
||
|
||
function analisesRender(relatorio) {
|
||
var video = relatorio.video || {};
|
||
var cenas = relatorio.cenas || [];
|
||
var falas = cenas.reduce(function (n, c) { return n + (c.falas || []).length; }, 0);
|
||
var evidencias = cenas.reduce(function (n, c) {
|
||
var resumo = c.resumo_de_evidencias || {};
|
||
return n + Object.keys(resumo).reduce(function (m, k) { return m + resumo[k]; }, 0);
|
||
}, 0);
|
||
document.getElementById("analisesStatus").textContent = "Carregado";
|
||
document.getElementById("analisesVideoNome").textContent = video.nome || "(sem nome)";
|
||
document.getElementById("analisesResumo").textContent =
|
||
cenas.length + " cenas · " + falas + " falas · " + evidencias + " evidências visuais";
|
||
document.getElementById("analisesResumoCard").hidden = false;
|
||
document.getElementById("btnAnalisesAbrir").disabled = false;
|
||
document.getElementById("btnAnalisesCopiar").disabled = false;
|
||
var lista = document.getElementById("analisesCenas");
|
||
lista.innerHTML = "";
|
||
cenas.forEach(function (cena) {
|
||
var bloco = document.createElement("div");
|
||
bloco.className = "analise-cena";
|
||
bloco.setAttribute("role", "listitem");
|
||
var titulo = cena.cena_registrada ? "" : " (sem cena gravada)";
|
||
var tipos = Object.keys(cena.resumo_de_evidencias || {})
|
||
.sort().map(function (k) { return k + " " + cena.resumo_de_evidencias[k]; }).join(", ") || "—";
|
||
var falasHtml = (cena.falas || []).map(function (fala) {
|
||
return "<p class=\"analise-fala\"><strong>" + (fala.emocao || "—") + "</strong> " +
|
||
analisesFormatarTempo(fala.inicio) + "–" + analisesFormatarTempo(fala.fim) + " · " +
|
||
String(fala.texto || "").replace(/&/g, "&").replace(/</g, "<") + "</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 retakesCasoAtual = null;
|
||
var retakesGrupos = [];
|
||
var retakesProcesso = null;
|
||
|
||
function retakesRaiz() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); }
|
||
|
||
function retakesAoAbrir() { retakesListarCasos(); }
|
||
|
||
function retakesListarCasos() {
|
||
var seletor = document.getElementById("retakesCaso");
|
||
var anterior = seletor.value;
|
||
seletor.innerHTML = "";
|
||
var raiz = retakesRaiz();
|
||
var casos = [];
|
||
try { casos = fs.readdirSync(raiz).filter(function (item) { return fs.statSync(path.join(raiz, item)).isDirectory(); }).sort(); } catch (_) {}
|
||
if (!casos.length) { seletor.appendChild(new Option("Nenhum caso encontrado", "")); retakesCasoAtual = null; retakesInfo("Execute o Scanner primeiro para gerar transcrições."); return; }
|
||
casos.forEach(function (caso) { seletor.appendChild(new Option(caso, caso)); });
|
||
seletor.value = anterior && casos.indexOf(anterior) >= 0 ? anterior : casos[0];
|
||
retakesCarregarCaso();
|
||
}
|
||
|
||
function retakesInfo(mensagem) { document.getElementById("retakesInfoCaso").textContent = mensagem; }
|
||
|
||
function retakesArtefatosDeAudio(caso) {
|
||
var pasta = path.join(retakesRaiz(), caso);
|
||
try { return fs.readdirSync(pasta).filter(function (item) { return /-audio-faixa-\d+\.json$/.test(item); }).map(function (item) { return path.join(pasta, item); }).sort(); } catch (_) { return []; }
|
||
}
|
||
|
||
function retakesCarregarCaso() {
|
||
var caso = document.getElementById("retakesCaso").value;
|
||
retakesCasoAtual = caso || null;
|
||
retakesGrupos = [];
|
||
retakesRenderResultados();
|
||
if (!caso) { retakesInfo("Nenhum caso disponível."); return; }
|
||
var artefatos = retakesArtefatosDeAudio(caso);
|
||
var jaTem = fs.existsSync(path.join(retakesRaiz(), caso, "retakes"));
|
||
retakesInfo("Caso: " + caso + " · " + artefatos.length + " faixa(s) de áudio com transcrição" + (jaTem ? " · já analisado" : " · não analisado"));
|
||
document.getElementById("retakesStatusGeral").textContent = jaTem ? "Analisado" : "Não analisado";
|
||
if (jaTem) retakesCarregarResultadosSalvos();
|
||
}
|
||
|
||
function retakesCarregarResultadosSalvos() {
|
||
var arquivo = path.join(retakesRaiz(), retakesCasoAtual, "retakes", "retakes-" + retakesCasoAtual + ".json");
|
||
try {
|
||
var dados = JSON.parse(fs.readFileSync(arquivo, "utf-8"));
|
||
retakesGrupos = dados.grupos_de_retakes || [];
|
||
retakesRenderResultados();
|
||
} catch (erro) { showToast("err", "Não foi possível ler os resultados: " + erro.message); }
|
||
}
|
||
|
||
function retakesFormatarTempo(segundos) {
|
||
if (typeof segundos !== "number" || isNaN(segundos)) return "—";
|
||
var total = Math.max(0, Math.round(segundos));
|
||
var m = Math.floor(total / 60), s = total % 60;
|
||
return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
|
||
}
|
||
|
||
function retakesRotuloDoGrupo(grupo) {
|
||
var tipo = { retake_confirmado: "Retake forte", possivel_retake: "Possível retake", repeticao_parcial: "Repetição parcial", repeticao_de_frase: "Repetição de frase", repeticao_de_inicio: "Repetição de início", continuidade_de_fala: "Continuidade de fala", sem_indicio_de_retake: "Sem indício" }[grupo.tipo] || grupo.tipo;
|
||
var status = grupo.status_de_revisao === "confirmado" ? " · confirmado" : grupo.status_de_revisao === "nao_e_retake" ? " · rejeitado" : grupo.status_de_revisao === "ignorado" ? " · ignorado" : "";
|
||
return tipo + " · " + Math.round(grupo.confianca * 100) + "% de confiança" + status;
|
||
}
|
||
|
||
function retakesPassaNoFiltro(grupo) {
|
||
var filtro = document.getElementById("retakesFiltro").value;
|
||
if (filtro === "todos") return true;
|
||
if (filtro === "fortes") return grupo.tipo === "retake_confirmado";
|
||
if (filtro === "possiveis") return grupo.tipo !== "retake_confirmado" && grupo.tipo !== "sem_indicio_de_retake";
|
||
return grupo.status_de_revisao === filtro;
|
||
}
|
||
|
||
function retakesRenderResultados() {
|
||
var alvo = document.getElementById("retakesResultados");
|
||
alvo.innerHTML = "";
|
||
var visiveis = retakesGrupos.filter(retakesPassaNoFiltro);
|
||
document.getElementById("retakesContagem").textContent = retakesGrupos.length ? retakesGrupos.length + " grupo(s)" : "—";
|
||
document.getElementById("retakesSemResultados").hidden = visiveis.length > 0;
|
||
visiveis.forEach(function (grupo) { alvo.appendChild(retakesCartaoDoGrupo(grupo)); });
|
||
}
|
||
|
||
function retakesCartaoDoGrupo(grupo) {
|
||
var cartao = document.createElement("div");
|
||
cartao.className = "card"; cartao.setAttribute("role", "listitem");
|
||
var titulo = document.createElement("strong"); titulo.textContent = retakesRotuloDoGrupo(grupo);
|
||
cartao.appendChild(titulo);
|
||
grupo.tomadas.forEach(function (tomada, indice) {
|
||
var linha = document.createElement("p");
|
||
linha.textContent = "Tentativa " + (indice + 1) + ": " + retakesFormatarTempo(tomada.inicio) + " – " + retakesFormatarTempo(tomada.fim) + " · " + (tomada.texto || "");
|
||
linha.style.margin = "2px 0"; cartao.appendChild(linha);
|
||
});
|
||
if (grupo.evidencias && grupo.evidencias.length) {
|
||
var motivos = document.createElement("p");
|
||
motivos.textContent = "Evidências: " + grupo.evidencias.map(function (e) { return e.descricao || e.tipo; }).join("; ");
|
||
motivos.className = "field-help"; cartao.appendChild(motivos);
|
||
}
|
||
var acoes = document.createElement("div"); acoes.className = "button-row";
|
||
[["confirmar", "Confirmar"], ["rejeitar", "Não é retake"], ["manter", "Manter p/ revisão"], ["ignorar", "Ignorar"]].forEach(function (par) {
|
||
var botao = document.createElement("button");
|
||
botao.className = "button button-ghost"; botao.type = "button"; botao.textContent = par[1];
|
||
botao.onclick = function () { retakesRevisar(grupo.id, par[0]); };
|
||
acoes.appendChild(botao);
|
||
});
|
||
cartao.appendChild(acoes);
|
||
return cartao;
|
||
}
|
||
|
||
function retakesAnalisar() {
|
||
if (!retakesCasoAtual) { showToast("err", "Selecione um caso do Scanner."); return; }
|
||
var artefatos = retakesArtefatosDeAudio(retakesCasoAtual);
|
||
if (!artefatos.length) { showToast("err", "O caso não possui transcrição. Execute o Scanner com transcrição primeiro."); return; }
|
||
if (retakesProcesso) { showToast("err", "Já existe uma análise em andamento."); return; }
|
||
if (!document.getElementById("retakesReaproveitar").checked) {
|
||
var pastaRetakes = path.join(retakesRaiz(), retakesCasoAtual, "retakes");
|
||
try { if (fs.existsSync(pastaRetakes)) fs.rmSync(pastaRetakes, { recursive: true }); } catch (_) {}
|
||
}
|
||
var pedido = { acao: "analisar", video_id: retakesCasoAtual, artefatos_de_audio: artefatos,
|
||
configuracao: { criterio: document.getElementById("retakesCriterio").value,
|
||
usar_analise_visual: document.getElementById("retakesUsarVisual").checked } };
|
||
var entrada = path.join(os.tmpdir(), "pedido-retakes-" + Date.now() + ".json");
|
||
fs.writeFileSync(entrada, JSON.stringify(pedido, null, 2), "utf-8");
|
||
setBusy("btnRetakesAnalisar", true);
|
||
taskStart("Retakes", "Preparando análise", null);
|
||
document.getElementById("retakesProgressoCard").hidden = false;
|
||
document.getElementById("retakesProgressoEtapa").textContent = "Analisando retakes…";
|
||
var child = childProcess.spawn(SCANNER_PYTHON_BIN, [RETAKES_ENGINE_SCRIPT, entrada, path.join(retakesRaiz(), retakesCasoAtual)],
|
||
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine", stdio: ["ignore", "pipe", "pipe"] });
|
||
retakesProcesso = child;
|
||
var resto = "";
|
||
var erroTecnico = "";
|
||
child.stdout.on("data", function (pedaco) {
|
||
resto += pedaco.toString(); var linhas = resto.split("\n"); resto = linhas.pop();
|
||
linhas.forEach(function (linha) { var evento; try { evento = JSON.parse(linha); } catch (_) { return; } retakesProcessarEvento(evento); });
|
||
});
|
||
child.stderr.on("data", function (pedaco) { erroTecnico += pedaco.toString(); if (pedaco.toString().trim()) retakesLog(pedaco.toString().trim()); });
|
||
child.on("error", function (erro) { retakesProcesso = null; setBusy("btnRetakesAnalisar", false); taskEnd(false, "Engine indisponível", erro.stack || erroTecnico || erro.message); showToast("err", erro.message); });
|
||
child.on("close", function (codigo) {
|
||
retakesProcesso = null; setBusy("btnRetakesAnalisar", false);
|
||
document.getElementById("retakesProgressoCard").hidden = true;
|
||
document.getElementById("retakesStatusGeral").textContent = "Analisado";
|
||
if (codigo !== 0) taskEnd(false, "Análise falhou", erroTecnico || "A engine terminou com código " + codigo + ".");
|
||
});
|
||
}
|
||
|
||
function retakesProcessarEvento(evento) {
|
||
if (evento.evento === "progresso") {
|
||
taskUpdate({ stage: evento.etapa, pct: evento.percentual });
|
||
document.getElementById("retakesProgressoEtapa").textContent = evento.etapa;
|
||
document.getElementById("retakesProgressoDetalhe").textContent = evento.percentual + "% concluído";
|
||
} else if (evento.evento === "erro") {
|
||
showToast("err", evento.mensagem); retakesLog("Erro: " + evento.mensagem);
|
||
} else if (evento.evento === "concluido") {
|
||
retakesGrupos = (evento.resultado && evento.resultado.grupos) || [];
|
||
retakesRenderResultados();
|
||
taskEnd(true, retakesGrupos.length + " grupo(s) de retakes");
|
||
showToast("ok", "Análise concluída: " + retakesGrupos.length + " grupo(s) encontrado(s).");
|
||
}
|
||
}
|
||
|
||
function retakesRevisar(grupoId, acao) {
|
||
if (!retakesCasoAtual) return;
|
||
var pedido = { acao: "revisar", video_id: retakesCasoAtual, revisao: { grupo_id: grupoId, acao: acao } };
|
||
var entrada = path.join(os.tmpdir(), "revisao-retakes-" + Date.now() + ".json");
|
||
fs.writeFileSync(entrada, JSON.stringify(pedido), "utf-8");
|
||
var child = childProcess.spawn(SCANNER_PYTHON_BIN, [RETAKES_ENGINE_SCRIPT, entrada, path.join(retakesRaiz(), retakesCasoAtual)],
|
||
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine", stdio: ["ignore", "pipe", "pipe"] });
|
||
child.on("close", function (codigo) {
|
||
if (codigo !== 0) { showToast("err", "Não foi possível registrar a revisão."); return; }
|
||
var novos = { confirmar: "confirmado", rejeitar: "nao_e_retake", manter: "pendente_de_revisao", ignorar: "ignorado" };
|
||
retakesGrupos = retakesGrupos.map(function (g) { return g.id === grupoId ? Object.assign({}, g, { status_de_revisao: novos[acao] }) : g; });
|
||
retakesRenderResultados();
|
||
showToast("ok", "Revisão registrada.");
|
||
});
|
||
}
|
||
|
||
function retakesLog(mensagem) {
|
||
var alvo = document.getElementById("retakesLog");
|
||
var linha = document.createElement("p"); linha.textContent = mensagem;
|
||
alvo.appendChild(linha);
|
||
}
|
||
|
||
// ---- Perfil do Scanner -----------------------------------------------------
|
||
// O painel e o Engine compartilham este contrato versionado. O perfil não
|
||
// contém caminhos de mídia; estes continuam sendo resolvidos pelo MCP.
|
||
var SCANNER_PROFILE_KEY = "mcp_scanner_profile_v1";
|
||
var SCANNER_ALL_RESOURCES = ["faces", "qualidade_facial", "pessoas", "pose", "maos", "ocr", "codigos", "categorias", "estetica", "horizonte", "retangulos", "contornos", "segmentacao_de_pessoas"];
|
||
|
||
var scannerTimeline = null;
|
||
var scannerReportPath = null;
|
||
var scannerRelatorioExistente = null;
|
||
var scannerReprocessamentoPendente = false;
|
||
var scannerAnalisesExistentes = [];
|
||
var scannerProcesso = null;
|
||
var scannerCancelSolicitado = false;
|
||
var SCANNER_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_scanner.py";
|
||
|
||
// `path` e `os` são carregados mais abaixo, junto com o runtime Node do CEP.
|
||
// Calcular isso aqui interrompia todo o script antes da inicialização do painel.
|
||
function scannerOutputRoot() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); }
|
||
function scannerNomeSeguro(nome) { return String(nome || "timeline").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "timeline"; }
|
||
|
||
function scannerCarregarRelatorioExistente() {
|
||
scannerRelatorioExistente = null;
|
||
if (!scannerTimeline || !scannerTimeline.name) return;
|
||
var caso = scannerNomeSeguro(scannerTimeline.name);
|
||
var caminho = path.join(scannerOutputRoot(), caso, caso + "-resultado.json");
|
||
try {
|
||
if (!fs.existsSync(caminho)) return;
|
||
var relatorio = JSON.parse(fs.readFileSync(caminho, "utf-8"));
|
||
if (relatorio.artefatos && relatorio.artefatos.audio && relatorio.artefatos.audio.length) { scannerRelatorioExistente = caminho; scannerReportPath = caminho; }
|
||
} catch (_) { scannerRelatorioExistente = null; }
|
||
}
|
||
|
||
function scannerListarAnalisesExistentes() {
|
||
var seletor = document.getElementById("scannerExistingAnalysis");
|
||
var status = document.getElementById("scannerExistingAnalysisStatus");
|
||
var info = document.getElementById("scannerExistingAnalysisInfo");
|
||
if (!seletor) return;
|
||
scannerAnalisesExistentes = [];
|
||
try {
|
||
fs.readdirSync(scannerOutputRoot()).forEach(function (nomeDaPasta) {
|
||
var caminho = path.join(scannerOutputRoot(), nomeDaPasta, nomeDaPasta + "-resultado.json");
|
||
if (!fs.existsSync(caminho)) return;
|
||
try {
|
||
var dados = JSON.parse(fs.readFileSync(caminho, "utf-8"));
|
||
var estado = silenceConsultarStatusNoBanco((dados.sequencia || {}).id);
|
||
if (estado) scannerAnalisesExistentes.push({ caminho: caminho, dados: dados, estado: estado });
|
||
} catch (_) {}
|
||
});
|
||
} catch (_) {}
|
||
scannerAnalisesExistentes.sort(function (a, b) {
|
||
try { return fs.statSync(b.caminho).mtimeMs - fs.statSync(a.caminho).mtimeMs; } catch (_) { return 0; }
|
||
});
|
||
seletor.innerHTML = "";
|
||
if (!scannerAnalisesExistentes.length) {
|
||
seletor.innerHTML = "<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"]};
|
||
var huggingFaceEnvironment=hfSpawnOptions(); if(huggingFaceEnvironment&&huggingFaceEnvironment.env) 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(); var lines=stdout.split("\n"); stdout=lines.pop();
|
||
lines.forEach(function(line) { try { var event=JSON.parse(line); if (event.evento === "progresso") taskUpdate({stage:event.etapa, detail:event.clipe || "", pct:event.percentual}); if (event.evento === "concluido" && event.arquivo) scannerReportPath=event.arquivo; } catch (_) {} });
|
||
});
|
||
child.stderr.on("data",function(chunk){stderr+=chunk.toString();});
|
||
child.on("error",function(error){setBusy("btnScannerProcess",false);taskEnd(false,"Scanner indisponível",error.stack||error.message);showToast("err",error.message);});
|
||
child.on("close",function(code){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"]};
|
||
var hf = hfSpawnOptions(); if (hf && hf.env) 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(); });
|
||
processo.stdout.on("data", function (bloco) { bloco.toString().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");
|
||
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");
|
||
document.getElementById("btnBuildPlan").disabled = true;
|
||
return;
|
||
}
|
||
document.getElementById("silenceTipoVideoInfo").textContent = tipo.nome + " · instruções serão incluídas no JSON para a IA.";
|
||
setStep(1, "done", "Selecionado");
|
||
document.getElementById("btnBuildPlan").disabled = false;
|
||
silenceLog("Tipo de vídeo selecionado: " + tipo.nome + ".", "ok");
|
||
}
|
||
|
||
function tipoVideoResumoParaJson(tipo) {
|
||
if (!tipo) return null;
|
||
return {
|
||
id: tipo.id,
|
||
nome: tipo.nome,
|
||
categoria: tipo.categoria,
|
||
objetivo: { principal: tipo.objetivo, secundario: tipo.objetivo_secundario, publico: tipo.publico, mensagem: tipo.mensagem },
|
||
narrativa: { tipo: tipo.tipo_narrativa, foco: tipo.foco_narrativo, storytelling: tipo.storytelling, estrutura: tipo.estrutura },
|
||
emocao: { objetivo: tipo.emocao, intensidade: tipo.intensidade, tom: tipo.tom, construcao: tipo.construcao_emocao },
|
||
formato: { canais: tipo.canais, proporcao: tipo.proporcao, duracao_minima_segundos: tipo.duracao_minima, duracao_maxima_segundos: tipo.duracao_maxima_segundos, ritmo: tipo.ritmo },
|
||
prioridades: tipo.prioridades,
|
||
elementos_de_edicao: { broll: tipo.usa_broll, legendas: tipo.usa_legendas, textos: tipo.usa_textos, graficos: tipo.usa_graficos, zoom: tipo.usa_zoom, cortes: tipo.cortes, orientacao_de_broll: tipo.broll, textos_e_legendas: tipo.texto },
|
||
audio: { musica: tipo.usa_musica, intensidade_da_musica: tipo.intensidade_musica, prioridade_da_voz: tipo.prioridade_voz, tratamento: tipo.som },
|
||
regras_de_edicao: tipo.diretriz,
|
||
restricoes: tipo.restricoes,
|
||
criterios_de_qualidade: tipo.qualidade
|
||
};
|
||
}
|
||
|
||
function silenceLocalizarUltimoRelatorioDoScanner() {
|
||
var raiz = scannerOutputRoot();
|
||
var candidatos = [];
|
||
try {
|
||
fs.readdirSync(raiz).forEach(function (nomeDaPasta) {
|
||
var pasta = path.join(raiz, nomeDaPasta);
|
||
var relatorio = path.join(pasta, nomeDaPasta + "-resultado.json");
|
||
if (fs.existsSync(relatorio)) candidatos.push(relatorio);
|
||
});
|
||
} catch (_) {}
|
||
candidatos.sort(function (a, b) {
|
||
try { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; } catch (_) { return 0; }
|
||
});
|
||
return candidatos[0] || null;
|
||
}
|
||
|
||
function silenceConverterRelatorioDoScannerParaTranscricao(caminhoDoRelatorio, dadosDoRelatorio) {
|
||
var segmentos = [];
|
||
(dadosDoRelatorio.artefatos && dadosDoRelatorio.artefatos.audio || []).forEach(function (nomeDoArquivo) {
|
||
var caminhoDoAudio = path.join(path.dirname(caminhoDoRelatorio), nomeDoArquivo);
|
||
if (!fs.existsSync(caminhoDoAudio)) return;
|
||
var dadosDoAudio;
|
||
try { dadosDoAudio = JSON.parse(fs.readFileSync(caminhoDoAudio, "utf-8")); } catch (_) { return; }
|
||
(dadosDoAudio.segmentos_por_clipe || []).forEach(function (grupo) {
|
||
(grupo.segmentos || []).forEach(function (segmento) {
|
||
segmentos.push({
|
||
start: Number(segmento.inicio), end: Number(segmento.fim),
|
||
inicio: Number(segmento.inicio), fim: Number(segmento.fim),
|
||
text: segmento.texto || "", texto: segmento.texto || "",
|
||
confidence: segmento.confianca, confianca: segmento.confianca,
|
||
speaker: segmento.falante, falante: segmento.falante,
|
||
caracteristicas_acusticas: segmento.caracteristicas_acusticas || {},
|
||
words: segmento.palavras || [], palavras: segmento.palavras || [],
|
||
});
|
||
});
|
||
});
|
||
});
|
||
segmentos.sort(function (a, b) { return a.start - b.start; });
|
||
return { version: 1, segments: segmentos, origem: "scanner" };
|
||
}
|
||
|
||
function silenceConsultarStatusNoBanco(videoId) {
|
||
if (!videoId || !childProcess.spawnSync) return null;
|
||
try {
|
||
var resposta = childProcess.spawnSync(
|
||
SCANNER_PYTHON_BIN,
|
||
[ANALISE_STATUS_SCRIPT, videoId, "--banco", "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db"],
|
||
{ cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", encoding: "utf-8", maxBuffer: 1024 * 1024 }
|
||
);
|
||
if (resposta.status !== 0) return null;
|
||
return JSON.parse(String(resposta.stdout || "{}"));
|
||
} catch (_) { return null; }
|
||
}
|
||
|
||
function silenceLocalizarAudioParaFalantes(pasta) {
|
||
var encontrados = [];
|
||
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);
|
||
return encontrados[0] || null;
|
||
}
|
||
|
||
function silenceLocalizarMidiaNoRelatorio(caminhoDoRelatorio, dadosDoRelatorio) {
|
||
var arquivosDeAudio = dadosDoRelatorio.artefatos && dadosDoRelatorio.artefatos.audio || [];
|
||
for (var indice = 0; indice < arquivosDeAudio.length; indice++) {
|
||
try {
|
||
var dadosDoAudio = JSON.parse(fs.readFileSync(path.join(path.dirname(caminhoDoRelatorio), arquivosDeAudio[indice]), "utf-8"));
|
||
var clipes = dadosDoAudio.faixa && dadosDoAudio.faixa.clipes || [];
|
||
for (var indiceDoClipe = 0; indiceDoClipe < clipes.length; indiceDoClipe++) {
|
||
if (clipes[indiceDoClipe].sourceFile) return clipes[indiceDoClipe].sourceFile;
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function silenceLoadScannerAnalysis() {
|
||
var status = document.getElementById("scannerAnalysisStatus");
|
||
var info = document.getElementById("scannerAnalysisInfo");
|
||
setBusy("btnLoadScannerAnalysis", true);
|
||
setBusy("btnRefinarLoad", true);
|
||
var caminhoDoRelatorio = silenceLocalizarUltimoRelatorioDoScanner();
|
||
if (!caminhoDoRelatorio) {
|
||
setBusy("btnLoadScannerAnalysis", false);
|
||
setBusy("btnRefinarLoad", false);
|
||
status.textContent = "Não encontrada";
|
||
info.textContent = "Execute o Scanner primeiro para criar uma análise reutilizável.";
|
||
showToast("err", info.textContent);
|
||
return;
|
||
}
|
||
try {
|
||
var dadosDoRelatorio = JSON.parse(fs.readFileSync(caminhoDoRelatorio, "utf-8"));
|
||
var sequencia = dadosDoRelatorio.sequencia || {};
|
||
silenceState.sequenceId = sequencia.id || null;
|
||
silenceState.sequenceName = sequencia.nome || path.basename(caminhoDoRelatorio);
|
||
silenceState.mediaPath = silenceLocalizarMidiaNoRelatorio(caminhoDoRelatorio, dadosDoRelatorio);
|
||
silenceState.fileBaseName = path.basename(path.dirname(caminhoDoRelatorio));
|
||
silenceState.outputDir = path.dirname(caminhoDoRelatorio);
|
||
silenceState.audioCachePath = silenceLocalizarAudioParaFalantes(silenceState.outputDir);
|
||
silenceState.transcriptPath = path.join(silenceState.outputDir, "transcricao-do-scanner.json");
|
||
silenceState.cachedTranscriptPath = null;
|
||
silenceState.tipoVideoId = null;
|
||
silenceState.tipoVideo = null;
|
||
fs.writeFileSync(silenceState.transcriptPath, JSON.stringify(
|
||
silenceConverterRelatorioDoScannerParaTranscricao(caminhoDoRelatorio, dadosDoRelatorio), null, 2), "utf-8");
|
||
var transcricao = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
|
||
var quantidade = transcricao.segments.length;
|
||
var statusDoBanco = silenceConsultarStatusNoBanco(silenceState.sequenceId);
|
||
if (!statusDoBanco || !statusDoBanco.audio) throw new Error("A análise não foi encontrada no banco de dados.");
|
||
var audioDoBanco = statusDoBanco.audio;
|
||
var falantes = audioDoBanco.falantes || [];
|
||
var temTranscricao = quantidade > 0;
|
||
var temDiarizacao = !!audioDoBanco.diarizacao_disponivel || falantes.length > 0;
|
||
var temMetricasDeVoz = !!audioDoBanco.metricas_de_voz_disponiveis;
|
||
atualizarCheckDaAnalise("scannerCheckTranscription", temTranscricao, temTranscricao ? "Carregado" : "Não encontrada");
|
||
atualizarCheckDaAnalise("scannerCheckDiarization", temDiarizacao, temDiarizacao ? "Carregado" : "Não realizada");
|
||
atualizarCheckDaAnalise("scannerCheckVoiceFeatures", temMetricasDeVoz, temMetricasDeVoz ? "Carregado" : "Não realizada");
|
||
var turnos = transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) {
|
||
return { speaker: segmento.falante, start: segmento.start, end: segmento.end };
|
||
});
|
||
renderSpeakerConfiguration(falantes, turnos);
|
||
atualizarTelaRefinar(falantes, audioDoBanco);
|
||
status.textContent = "Carregada";
|
||
info.textContent = silenceState.sequenceName + " · " + quantidade + " trecho(s) · " +
|
||
(audioDoBanco.metricas_de_voz_disponiveis ? "métricas disponíveis" : "métricas não realizadas") + " · " +
|
||
(audioDoBanco.diarizacao_disponivel ? falantes.length + " falante(s)" : "diarização não realizada");
|
||
setStep(3, "ready", "Pronto");
|
||
setStep(1, "ready", "Selecione um tipo");
|
||
setStep(4, "locked", "Selecione o tipo");
|
||
setStep(5, "locked", "Bloqueado");
|
||
renderizarTiposDeVideoParaEdicao();
|
||
document.getElementById("btnBuildPlan").disabled = true;
|
||
// btnDiarize/btnVoiceFeatures/silenceDiarizeInfo/silenceVoiceFeaturesInfo ficavam no
|
||
// step3 (legado, comentado em 2026-09-09 no index.html) — checagem defensiva para não
|
||
// quebrar este fluxo (ativo) se os elementos não existirem mais no DOM.
|
||
var elBtnDiarize = document.getElementById("btnDiarize");
|
||
if (elBtnDiarize) elBtnDiarize.disabled = false;
|
||
var elBtnVoiceFeatures = document.getElementById("btnVoiceFeatures");
|
||
if (elBtnVoiceFeatures) elBtnVoiceFeatures.disabled = false;
|
||
var elDiarizeInfo = document.getElementById("silenceDiarizeInfo");
|
||
if (elDiarizeInfo) elDiarizeInfo.textContent = temDiarizacao
|
||
? falantes.length + " falante(s) disponíveis no banco de dados."
|
||
: "Diarização não realizada nesta análise do Scanner. Use Refazer se necessário.";
|
||
var elVoiceFeaturesInfo = document.getElementById("silenceVoiceFeaturesInfo");
|
||
if (elVoiceFeaturesInfo) elVoiceFeaturesInfo.textContent = temMetricasDeVoz
|
||
? (audioDoBanco.falas_com_metricas || 0) + " trecho(s) com métricas de voz no banco."
|
||
: "Métricas de voz não realizadas nesta análise do Scanner. Use Refazer se necessário.";
|
||
silenceLog("Análise do Scanner carregada: " + quantidade + " trecho(s).", "ok");
|
||
showToast("ok", "Análise do Scanner carregada.");
|
||
} catch (erro) {
|
||
status.textContent = "Erro";
|
||
info.textContent = "Não foi possível ler a análise: " + erro.message;
|
||
showToast("err", info.textContent);
|
||
}
|
||
setBusy("btnLoadScannerAnalysis", false);
|
||
setBusy("btnRefinarLoad", false);
|
||
}
|
||
|
||
function refinarAoAbrir() {
|
||
var info = document.getElementById("refinarInfo");
|
||
if (!info || !silenceState.sequenceId) return;
|
||
info.textContent = silenceState.sequenceName + " · " + (silenceState.speakers.length
|
||
? silenceState.speakers.length + " falante(s) carregado(s)."
|
||
: "Nenhum falante carregado.");
|
||
}
|
||
|
||
function refinarCarregarAnalise() {
|
||
silenceLoadScannerAnalysis();
|
||
}
|
||
|
||
function atualizarTelaRefinar(falantes, audioDoBanco) {
|
||
var status = document.getElementById("refinarStatus");
|
||
var info = document.getElementById("refinarInfo");
|
||
var vazio = document.getElementById("refinarSpeakerEmpty");
|
||
var count = document.getElementById("refinarSpeakerCount");
|
||
var save = document.getElementById("btnRefinarSave");
|
||
if (!status || !info || !vazio || !count) return;
|
||
var quantidade = Array.isArray(falantes) ? falantes.length : 0;
|
||
status.textContent = quantidade ? "Pronto" : "Sem diarização";
|
||
info.textContent = silenceState.sequenceName + " · " + (audioDoBanco && audioDoBanco.diarizacao_disponivel
|
||
? quantidade + " falante(s) detectado(s)."
|
||
: "A transcrição está disponível, mas a diarização não foi realizada.");
|
||
count.textContent = quantidade ? quantidade + (quantidade === 1 ? " falante detectado" : " falantes detectados") : "Nenhum falante carregado";
|
||
vazio.hidden = quantidade > 0;
|
||
if (save) save.disabled = quantidade === 0;
|
||
}
|
||
|
||
var speakerAudioPlayer = null;
|
||
var speakerAudioQueue = null;
|
||
|
||
// ---- 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("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();
|
||
}
|
||
);
|
||
}
|
||
|
||
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) {
|
||
var buttons = {
|
||
2: ["btnTranscribe"],
|
||
3: ["btnDiarize", "btnVoiceFeatures"],
|
||
4: ["btnBuildPlan"],
|
||
5: ["btnApplyEditorialActions"],
|
||
};
|
||
for (var n = first; n <= 5; n++) {
|
||
setStep(n, "locked", "Bloqueado");
|
||
(buttons[n] || []).forEach(function (id) {
|
||
var btn = document.getElementById(id);
|
||
if (btn) btn.disabled = true;
|
||
});
|
||
}
|
||
}
|
||
|
||
function renderCachedTranscripts() {
|
||
var section = document.getElementById("silenceCachedSection");
|
||
var select = document.getElementById("silenceCachedSelect");
|
||
var entries = silenceState.mediaPath ? findCachedTranscripts(silenceState.mediaPath) : [];
|
||
if (!entries.length) {
|
||
section.hidden = true;
|
||
select.innerHTML = "";
|
||
return;
|
||
}
|
||
entries.sort(function (a, b) { return new Date(b.savedAt) - new Date(a.savedAt); });
|
||
select.innerHTML = "";
|
||
entries.forEach(function (entry, i) {
|
||
var opt = document.createElement("option");
|
||
opt.value = String(i);
|
||
opt.textContent = new Date(entry.savedAt).toLocaleString() + " — modelo " + (entry.model || "?");
|
||
select.appendChild(opt);
|
||
});
|
||
select.dataset.entries = JSON.stringify(entries);
|
||
section.hidden = false;
|
||
silenceLog(entries.length + " transcrição(ões) salva(s) encontrada(s) para este vídeo.", "ok");
|
||
}
|
||
|
||
function silenceUseCachedTranscript() {
|
||
var select = document.getElementById("silenceCachedSelect");
|
||
var entries = JSON.parse(select.dataset.entries || "[]");
|
||
var entry = entries[Number(select.value)];
|
||
if (!entry) return;
|
||
silenceState.transcriptPath = entry.transcriptPath;
|
||
silenceState.transcribeModel = entry.model;
|
||
silenceState.cachedTranscriptPath = entry.transcriptPath;
|
||
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(3, "ready", "Opcional");
|
||
setStep(4, "ready", "Sua vez");
|
||
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 child = childProcess.spawn(bin, args, options || {});
|
||
var buffer = "";
|
||
var ultimoErro = "";
|
||
// Linhas "@@PROGRESS {json}" são o canal de progresso dos scripts Python/Node:
|
||
// vão para a barra no topo em vez de virarem ruído no log.
|
||
function handleLine(line) {
|
||
var trimmed = line.trim();
|
||
if (trimmed.indexOf(PROGRESS_PREFIX) === 0) {
|
||
try {
|
||
taskUpdate(JSON.parse(trimmed.slice(PROGRESS_PREFIX.length)));
|
||
} catch (e) {}
|
||
return;
|
||
}
|
||
onLine(line);
|
||
}
|
||
function flushLines(chunk) {
|
||
buffer += chunk;
|
||
var parts = buffer.split("\n");
|
||
buffer = parts.pop();
|
||
for (var i = 0; i < parts.length; i++) {
|
||
if (parts[i].trim()) handleLine(parts[i]);
|
||
}
|
||
}
|
||
child.stdout.on("data", function (d) { flushLines(d.toString()); });
|
||
child.stderr.on("data", function (d) { ultimoErro = d.toString().trim() || ultimoErro; flushLines(d.toString()); });
|
||
child.on("error", function (err) { onDone(1, "spawn error: " + err.message); });
|
||
child.on("close", function (code) {
|
||
if (buffer.trim()) handleLine(buffer);
|
||
onDone(code, ultimoErro || null);
|
||
});
|
||
return child;
|
||
}
|
||
|
||
// 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 = "▶";
|
||
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 speakerAudioFileUrl(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 = "▶";
|
||
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(speakerAudioFileUrl(silenceState.audioCachePath));
|
||
button.textContent = "■";
|
||
button.classList.add("is-playing");
|
||
speakerAudioPlayer.addEventListener("loadedmetadata", playCurrentSpeakerRange);
|
||
speakerAudioPlayer.addEventListener("timeupdate", advanceSpeakerAudioRange);
|
||
speakerAudioPlayer.addEventListener("ended", advanceSpeakerAudioRange);
|
||
speakerAudioPlayer.addEventListener("error", function () {
|
||
stopSpeakerAudio();
|
||
showToast("err", "Não foi possível reproduzir o áudio deste falante.");
|
||
});
|
||
}
|
||
|
||
function playCurrentSpeakerRange() {
|
||
if (!speakerAudioPlayer || !speakerAudioQueue) return;
|
||
var range = speakerAudioQueue.ranges[speakerAudioQueue.index];
|
||
if (!range) { stopSpeakerAudio(); return; }
|
||
speakerAudioPlayer.currentTime = Math.max(0, Number(range.start) - 0.15);
|
||
var playPromise = speakerAudioPlayer.play();
|
||
if (playPromise && playPromise.catch) playPromise.catch(function () { stopSpeakerAudio(); });
|
||
}
|
||
|
||
function advanceSpeakerAudioRange() {
|
||
if (!speakerAudioPlayer || !speakerAudioQueue) return;
|
||
var range = speakerAudioQueue.ranges[speakerAudioQueue.index];
|
||
if (!range || speakerAudioPlayer.currentTime < Number(range.end)) return;
|
||
speakerAudioQueue.index += 1;
|
||
if (speakerAudioQueue.index >= speakerAudioQueue.ranges.length) {
|
||
stopSpeakerAudio();
|
||
return;
|
||
}
|
||
playCurrentSpeakerRange();
|
||
}
|
||
|
||
function silenceLoadSpeakerConfiguration() {
|
||
if (!silenceState.transcriptPath) return;
|
||
try {
|
||
var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
|
||
var diarization = transcript.diarization;
|
||
if (diarization && Array.isArray(diarization.speakers)) renderSpeakerConfiguration(diarization.speakers, diarization.turns);
|
||
} catch (e) {}
|
||
}
|
||
|
||
function speakerConfigurationForJson() {
|
||
var speakers = silenceState.speakers.map(function (speaker) {
|
||
var active = speaker.active !== false;
|
||
return {
|
||
id: speaker.id,
|
||
name: speaker.name || speaker.id,
|
||
status: active ? "active" : "inactive",
|
||
include_in_video: active,
|
||
editorial_instruction: active
|
||
? "Manter as falas deste falante, salvo decisão específica da IA."
|
||
: "Excluir todas as falas deste falante do vídeo final.",
|
||
};
|
||
});
|
||
return {
|
||
total_detected: speakers.length,
|
||
speakers: speakers,
|
||
inactive_speakers: speakers.filter(function (speaker) { return !speaker.include_in_video; }).map(function (speaker) { return speaker.id; }),
|
||
instruction: "Falantes inativos devem ter todas as suas falas removidas do vídeo final; não confundir nome com identidade confirmada.",
|
||
};
|
||
}
|
||
|
||
// Depois de mutar silenceState.transcriptPath em lugar (diarização, métricas de
|
||
// voz), propaga a mudança pra cópia central de cache, se forem arquivos diferentes.
|
||
function syncTranscriptToCache() {
|
||
if (silenceState.cachedTranscriptPath && silenceState.cachedTranscriptPath !== silenceState.transcriptPath) {
|
||
try {
|
||
fs.copyFileSync(silenceState.transcriptPath, silenceState.cachedTranscriptPath);
|
||
} catch (e) {}
|
||
}
|
||
}
|
||
|
||
// LEGADO comentado em 2026-09-09: calculava métricas de voz direto nesta aba,
|
||
// duplicando o que a aba Scanner já faz e persiste no banco (.jhonny/analises.db).
|
||
// Mantido comentado (não removido) a pedido do usuário, para referência futura.
|
||
/*
|
||
function silenceComputeVoiceFeatures() {
|
||
if (!silenceState.transcriptPath) return;
|
||
if (!silenceState.mediaPath) {
|
||
silenceLog("Detecte o clipe/sequência primeiro (passo 1) para saber qual vídeo analisar.", "err");
|
||
showToast("err", "Detecte o vídeo no passo 1 primeiro.");
|
||
return;
|
||
}
|
||
var info = document.getElementById("silenceVoiceFeaturesInfo");
|
||
setBusy("btnVoiceFeatures", true);
|
||
setStep(3, "running", "Calculando…");
|
||
info.textContent = "Calculando…";
|
||
silenceLog("Calculando pitch/energia/velocidade de fala/pausas…");
|
||
taskStart("Medindo a voz", "Lendo a transcrição");
|
||
runStreamed(
|
||
SILENCE_PYTHON_BIN,
|
||
[
|
||
VOICE_FEATURES_SCRIPT,
|
||
"--video", silenceState.mediaPath,
|
||
"--transcript", silenceState.transcriptPath,
|
||
"--out", silenceState.transcriptPath,
|
||
"--cache-dir", silenceState.outputDir,
|
||
],
|
||
function (line) { silenceLog(line); },
|
||
function (code, lastErr) {
|
||
setBusy("btnVoiceFeatures", false);
|
||
if (code !== 0) {
|
||
setStep(3, "ready", "Opcional");
|
||
info.textContent = "Falhou — veja os detalhes técnicos.";
|
||
taskEnd(false, "O cálculo das métricas de voz falhou (código " + code + ").", lastErr);
|
||
silenceLog("Falha ao calcular métricas de voz (código " + code + (lastErr ? ": " + lastErr : "") + ").", "err");
|
||
return;
|
||
}
|
||
var msg = "Pitch, energia, velocidade e pausas calculados por trecho.";
|
||
silenceLog(msg, "ok");
|
||
info.textContent = msg;
|
||
atualizarCheckDaAnalise("scannerCheckVoiceFeatures", true, "Carregado");
|
||
setStep(3, "done", "Enriquecido");
|
||
taskEnd(true, msg);
|
||
syncTranscriptToCache();
|
||
}
|
||
);
|
||
}
|
||
*/
|
||
|
||
// Não gera decisões de corte: só empacota transcrição + tempos + locutores +
|
||
// métricas de voz + configurações de edição num JSON para um agente de IA
|
||
// externo analisar. Quem decide os cortes é a IA, não este painel.
|
||
function silenceGenerateJson() {
|
||
if (!silenceState.transcriptPath) return;
|
||
if (!silenceState.tipoVideo) {
|
||
showToast("err", "Selecione o tipo de vídeo antes de gerar o JSON.");
|
||
setStep(1, "ready", "Selecione um tipo");
|
||
return;
|
||
}
|
||
setBusy("btnBuildPlan", true);
|
||
setStep(4, "running", "Gerando…");
|
||
silenceLog("Gerando arquivo JSON para a IA…");
|
||
try {
|
||
var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8"));
|
||
var silenceEnabled = document.getElementById("silenceRemovalEnabled").checked;
|
||
var minSilence = parseFloat(document.getElementById("silenceRemovalMinDuration").value);
|
||
if (!isFinite(minSilence) || minSilence < 0) minSilence = 0.8;
|
||
|
||
transcript.sequence = {
|
||
name: silenceState.sequenceName,
|
||
media_path: silenceState.mediaPath,
|
||
};
|
||
transcript.settings = Object.assign({}, transcript.settings, {
|
||
silence_removal: { enabled: silenceEnabled, min_silence_seconds: minSilence },
|
||
legendas: carregarConfiguracaoDeLegendas(),
|
||
});
|
||
transcript.speaker_configuration = speakerConfigurationForJson();
|
||
transcript.tipo_de_video = {
|
||
id: silenceState.tipoVideo.id,
|
||
nome: silenceState.tipoVideo.nome,
|
||
instrucoes: tipoVideoResumoParaJson(silenceState.tipoVideo),
|
||
};
|
||
|
||
ensureDir(silenceState.outputDir);
|
||
var out = path.join(silenceState.outputDir, "dados-para-ia.json");
|
||
fs.writeFileSync(out, JSON.stringify(transcript, null, 2), "utf-8");
|
||
silenceState.jsonOutputPath = out;
|
||
|
||
var segCount = Array.isArray(transcript.segments) ? transcript.segments.length : 0;
|
||
document.getElementById("jsonSegmentCount").textContent = segCount;
|
||
var pathCell = document.getElementById("jsonOutputPath");
|
||
pathCell.textContent = out;
|
||
pathCell.title = out;
|
||
document.getElementById("silenceJsonCard").hidden = false;
|
||
document.getElementById("jsonFileActions").hidden = false;
|
||
document.getElementById("silencePlanInfo").textContent =
|
||
"Envie este arquivo a um agente de IA externo. Ele deve devolver o plano de edição em JSON para o passo 5.";
|
||
|
||
setStep(4, "done", "JSON pronto");
|
||
setStep(5, "ready", "Sua vez");
|
||
document.getElementById("btnApplyEditorialActions").disabled = false;
|
||
showToast("ok", "JSON gerado: " + path.basename(out));
|
||
silenceLog("JSON gerado em " + out + " (" + segCount + " trechos).", "ok");
|
||
} catch (e) {
|
||
setStep(4, "error", "Falhou");
|
||
showToast("err", "Falha ao gerar o JSON: " + e.message);
|
||
silenceLog("Falha ao gerar o JSON: " + e.message, "err");
|
||
}
|
||
setBusy("btnBuildPlan", false);
|
||
}
|
||
|
||
// O script de cortes importa ../dist/tools/*.js, então precisa rodar de dentro
|
||
// do repositório — não da cópia instalada em ~/Library/.../CEP/extensions.
|
||
// Ordem: repoRoot gravado pelo install-cep.sh > relativo ao painel (modo symlink) > fallback fixo.
|
||
function resolveScript(scriptFileName, fallbackPath) {
|
||
var candidates = [];
|
||
var repoRoot = String(loadConfig().repoRoot || "").trim();
|
||
if (repoRoot) candidates.push(path.join(repoRoot, "scripts", scriptFileName));
|
||
try {
|
||
var extensionRoot = toFsPath(cs.getSystemPath("extension"));
|
||
candidates.push(path.join(extensionRoot, "..", "scripts", scriptFileName));
|
||
} catch (e) {}
|
||
candidates.push(fallbackPath);
|
||
for (var i = 0; i < candidates.length; i++) {
|
||
try { if (fs.existsSync(candidates[i])) return candidates[i]; } catch (e) {}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function resolveEditorialActionsScript() {
|
||
return resolveScript("apply-editorial-actions.mjs", EDITORIAL_ACTIONS_APPLY_SCRIPT_FALLBACK);
|
||
}
|
||
|
||
// ---- Aplicar o plano de edição devolvido pela IA (cut/zoom/text/marker) ----
|
||
function lerPlanoEditorialDaTela() {
|
||
var campoJson = document.getElementById("editorialPlanJson");
|
||
var campoCaminho = document.getElementById("editorialPlanPath");
|
||
var texto = String(campoJson && campoJson.value || "").trim();
|
||
var caminho = String(campoCaminho && campoCaminho.value || "").trim();
|
||
if (!texto && caminho) {
|
||
if (!fs.existsSync(caminho)) throw new Error("Arquivo do plano não encontrado.");
|
||
texto = fs.readFileSync(caminho, "utf-8");
|
||
}
|
||
if (!texto) throw new Error("Cole o JSON ou escolha um arquivo de plano.");
|
||
var plano;
|
||
try { plano = JSON.parse(texto); } catch (erro) { throw new Error("JSON inválido: " + erro.message); }
|
||
if (!plano || typeof plano !== "object" || !plano.source || !Array.isArray(plano.actions) || !plano.actions.length) {
|
||
throw new Error("O plano precisa ter source e uma lista não vazia de actions.");
|
||
}
|
||
plano.actions.forEach(function (acao, indice) {
|
||
if (!acao || !acao.kind || typeof acao.start !== "number" || typeof acao.end !== "number" || !acao.reason) {
|
||
throw new Error("actions[" + indice + "] precisa ter kind, start, end e reason.");
|
||
}
|
||
});
|
||
return plano;
|
||
}
|
||
|
||
function applyEditorialActions() {
|
||
if (!silenceState.sequenceName) {
|
||
silenceLog("Detecte o clipe/sequência primeiro (passo 1).", "err");
|
||
showToast("err", "Detecte o vídeo no passo 1 primeiro.");
|
||
return;
|
||
}
|
||
var plan;
|
||
try { plan = lerPlanoEditorialDaTela(); } catch (erroPlano) {
|
||
var statusPlano = document.getElementById("editorialPlanJsonStatus");
|
||
if (statusPlano) { statusPlano.textContent = erroPlano.message; statusPlano.className = "field-help err"; }
|
||
silenceLog(erroPlano.message, "err");
|
||
showToast("err", erroPlano.message);
|
||
return;
|
||
}
|
||
var numActions = plan.actions.length;
|
||
|
||
var summary =
|
||
numActions + " ação(ões) no plano (cortes, zooms, textos e/ou marcadores).\n\n" +
|
||
"Um backup da sequência atual será criado antes de aplicar. Confirmar?";
|
||
if (typeof window.confirm === "function" && !window.confirm(summary)) return;
|
||
|
||
setBusy("btnApplyEditorialActions", true);
|
||
setStep(5, "running", "Aplicando…");
|
||
taskStart("Aplicando o plano de edição", "Criando backup da sequência");
|
||
silenceLog("Criando backup da sequência…");
|
||
cs.evalScript(
|
||
"(function(){try{app.project.activeSequence.clone();return 'ok';}catch(e){return 'error:'+String(e);}}())",
|
||
function (raw) {
|
||
if (String(raw).indexOf("ok") !== 0) {
|
||
silenceLog("Não foi possível criar o backup automático (" + raw + "). Aplicando mesmo assim.", "err");
|
||
} else {
|
||
silenceLog("Backup criado.", "ok");
|
||
}
|
||
|
||
var caminhoDoPlano = path.join(silenceState.outputDir || os.tmpdir(), "plano-editorial-" + Date.now() + ".json");
|
||
try { fs.writeFileSync(caminhoDoPlano, JSON.stringify(plan, null, 2), "utf-8"); } catch (erroEscrita) {
|
||
setBusy("btnApplyEditorialActions", false);
|
||
setStep(5, "error", "Falhou");
|
||
taskEnd(false, "Não foi possível salvar o plano.", erroEscrita.message);
|
||
return;
|
||
}
|
||
var processoAplicacao = null;
|
||
silenceLog("Aplicando plano pela engine Python…");
|
||
taskStart("Aplicando o plano de edição", "Executando pela engine Python", null, function () {
|
||
if (processoAplicacao) processoAplicacao.kill("SIGTERM");
|
||
});
|
||
processoAplicacao = runStreamed(
|
||
SCANNER_PYTHON_BIN,
|
||
[TESTAR_ACOES_ENGINE_SCRIPT, caminhoDoPlano],
|
||
function (line) { silenceLog(line); },
|
||
function (code, lastErr) {
|
||
processoAplicacao = null;
|
||
setBusy("btnApplyEditorialActions", false);
|
||
if (code !== 0) {
|
||
setStep(5, "error", "Falhou");
|
||
taskEnd(false, "Falha ao aplicar o plano (código " + code + ").", lastErr);
|
||
silenceLog("Falha ao aplicar o plano (código " + code + ").", "err");
|
||
return;
|
||
}
|
||
setStep(5, "done", "Aplicado");
|
||
taskEnd(true, numActions + " ação(ões) aplicadas na timeline!");
|
||
silenceLog("Plano de edição aplicado com sucesso!", "ok");
|
||
}
|
||
);
|
||
}
|
||
);
|
||
}
|
||
|
||
// Abre o seletor de arquivos nativo do CEP em vez de exigir que a pessoa cole
|
||
// um caminho absoluto à mão.
|
||
function pickEditorialPlanFile() {
|
||
var input = document.getElementById("editorialPlanPath");
|
||
try {
|
||
var api = window.cep && window.cep.fs;
|
||
if (!api || typeof api.showOpenDialog !== "function") {
|
||
input.focus();
|
||
showToast("err", "Seletor de arquivos indisponível aqui — cole o caminho do JSON.");
|
||
return;
|
||
}
|
||
var result = api.showOpenDialog(false, false, "Escolha o plano de edição (JSON)", "", ["json"]);
|
||
var chosen = result && result.data && result.data.length ? result.data[0] : "";
|
||
if (chosen) input.value = toFsPath(chosen);
|
||
} catch (e) {
|
||
input.focus();
|
||
showToast("err", "Não foi possível abrir o seletor: " + e.message);
|
||
}
|
||
}
|
||
|
||
// ---- Settings / secrets storage ----
|
||
var CONFIG_DIR = path.join(os.homedir(), ".premiere-mcp");
|
||
var CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
||
|
||
function loadConfig() {
|
||
try {
|
||
if (!fs.existsSync(CONFIG_FILE)) return {};
|
||
var parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
||
return (parsed && typeof parsed === "object") ? parsed : {};
|
||
} catch (e) {
|
||
return {};
|
||
}
|
||
}
|
||
|
||
function saveConfig(patch) {
|
||
try {
|
||
ensureDir(CONFIG_DIR);
|
||
var next = Object.assign({}, loadConfig(), patch);
|
||
fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 0o600 });
|
||
try { fs.chmodSync(CONFIG_FILE, 0o600); } catch (e) {}
|
||
return true;
|
||
} catch (e) {
|
||
log("Error saving config: " + e.message, "err");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// ---- Configuração do Editor: personalidades de edição ----
|
||
// Fase 1: nome + texto livre descrevendo o comportamento esperado. A
|
||
// personalidade marcada como ativa vai embutida no JSON de transcrição
|
||
// (settings.editor_personality), pra eu já saber o estilo esperado quando
|
||
// você me trouxer o arquivo. Fases futuras podem trocar/complementar o texto
|
||
// livre por parâmetros estruturados obrigatórios.
|
||
function loadEditorPersonalities() {
|
||
var list = loadConfig().editorPersonalities;
|
||
return Array.isArray(list) ? list : [];
|
||
}
|
||
|
||
function loadActiveEditorPersonalityId() {
|
||
return String(loadConfig().activeEditorPersonalityId || "").trim();
|
||
}
|
||
|
||
function getActiveEditorPersonality() {
|
||
var id = loadActiveEditorPersonalityId();
|
||
if (!id) return null;
|
||
var list = loadEditorPersonalities();
|
||
for (var i = 0; i < list.length; i++) {
|
||
if (list[i].id === id) return list[i];
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function setActiveEditorPersonality(id) {
|
||
saveConfig({ activeEditorPersonalityId: id });
|
||
showToast("ok", "Personalidade ativa atualizada.");
|
||
renderEditorPersonalities();
|
||
}
|
||
|
||
// Embute a personalidade de edição ativa (nome + texto livre) como
|
||
// `settings.editor_personality` dentro do JSON de transcrição recém-gerado,
|
||
// pra ir junto quando o arquivo for trazido pra análise.
|
||
function applyEditorPersonalityToTranscript(transcriptPath) {
|
||
var personality = getActiveEditorPersonality();
|
||
if (!personality) return;
|
||
try {
|
||
var transcript = JSON.parse(fs.readFileSync(transcriptPath, "utf-8"));
|
||
transcript.settings = Object.assign({}, transcript.settings, {
|
||
editor_personality: { name: personality.name, text: personality.text },
|
||
});
|
||
fs.writeFileSync(transcriptPath, JSON.stringify(transcript, null, 2), "utf-8");
|
||
silenceLog('Personalidade de edição embutida no JSON: "' + personality.name + '".', "ok");
|
||
} catch (e) {
|
||
silenceLog("Não foi possível embutir a personalidade de edição: " + e.message, "err");
|
||
}
|
||
}
|
||
|
||
var editingPersonalityId = null;
|
||
|
||
function startNewEditorPersonality() {
|
||
editingPersonalityId = null;
|
||
document.getElementById("editorPersonalityName").value = "";
|
||
document.getElementById("editorPersonalityText").value = "";
|
||
document.getElementById("editorPersonalityForm").hidden = false;
|
||
}
|
||
|
||
function editEditorPersonality(id) {
|
||
var list = loadEditorPersonalities();
|
||
var entry = list.filter(function (p) { return p.id === id; })[0];
|
||
if (!entry) return;
|
||
editingPersonalityId = id;
|
||
document.getElementById("editorPersonalityName").value = entry.name;
|
||
document.getElementById("editorPersonalityText").value = entry.text;
|
||
document.getElementById("editorPersonalityForm").hidden = false;
|
||
}
|
||
|
||
function cancelEditorPersonalityForm() {
|
||
editingPersonalityId = null;
|
||
document.getElementById("editorPersonalityForm").hidden = true;
|
||
}
|
||
|
||
function saveEditorPersonalityForm() {
|
||
var name = document.getElementById("editorPersonalityName").value.trim();
|
||
var text = document.getElementById("editorPersonalityText").value.trim();
|
||
if (!name || !text) {
|
||
modelsLog("Preencha nome e descrição da personalidade.", "err");
|
||
return;
|
||
}
|
||
var list = loadEditorPersonalities();
|
||
if (editingPersonalityId) {
|
||
list = list.map(function (p) {
|
||
return p.id === editingPersonalityId ? { id: p.id, name: name, text: text } : p;
|
||
});
|
||
} else {
|
||
var id = "personality-" + Date.now().toString(36);
|
||
list.push({ id: id, name: name, text: text });
|
||
if (!loadActiveEditorPersonalityId()) saveConfig({ activeEditorPersonalityId: id });
|
||
}
|
||
saveConfig({ editorPersonalities: list });
|
||
cancelEditorPersonalityForm();
|
||
renderEditorPersonalities();
|
||
modelsLog("Personalidade de edição salva: " + name, "ok");
|
||
}
|
||
|
||
function deleteEditorPersonality(id) {
|
||
var list = loadEditorPersonalities();
|
||
var entry = list.filter(function (p) { return p.id === id; })[0];
|
||
if (!entry) return;
|
||
if (typeof window.confirm === "function" && !window.confirm('Excluir a personalidade "' + entry.name + '"?')) return;
|
||
list = list.filter(function (p) { return p.id !== id; });
|
||
var patch = { editorPersonalities: list };
|
||
if (loadActiveEditorPersonalityId() === id) patch.activeEditorPersonalityId = "";
|
||
saveConfig(patch);
|
||
renderEditorPersonalities();
|
||
}
|
||
|
||
function renderEditorPersonalities() {
|
||
var container = document.getElementById("editorPersonalityList");
|
||
if (!container) return;
|
||
container.innerHTML = "";
|
||
var activeId = loadActiveEditorPersonalityId();
|
||
var list = loadEditorPersonalities();
|
||
|
||
if (!list.length) {
|
||
var empty = document.createElement("p");
|
||
empty.className = "field-help";
|
||
empty.textContent = "Nenhuma personalidade cadastrada ainda. Crie uma para orientar o corte por IA.";
|
||
container.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
list.forEach(function (entry) {
|
||
var isActive = entry.id === activeId;
|
||
var row = document.createElement("div");
|
||
row.className = "personality-row";
|
||
row.setAttribute("data-active", String(isActive));
|
||
|
||
var main = document.createElement("div");
|
||
main.className = "model-main";
|
||
|
||
var name = document.createElement("span");
|
||
name.className = "model-name";
|
||
name.textContent = entry.name;
|
||
main.appendChild(name);
|
||
|
||
var meta = document.createElement("div");
|
||
meta.className = "model-meta";
|
||
var snippet = document.createElement("span");
|
||
snippet.className = "model-badge";
|
||
snippet.textContent = entry.text.length > 60 ? entry.text.slice(0, 60) + "…" : entry.text;
|
||
snippet.title = entry.text;
|
||
meta.appendChild(snippet);
|
||
if (isActive) {
|
||
var badge = document.createElement("span");
|
||
badge.className = "model-badge model-badge-star";
|
||
badge.textContent = "★ Ativa";
|
||
meta.appendChild(badge);
|
||
}
|
||
main.appendChild(meta);
|
||
row.appendChild(main);
|
||
|
||
var actions = document.createElement("div");
|
||
actions.className = "model-actions";
|
||
|
||
var activeBtn = document.createElement("button");
|
||
activeBtn.type = "button";
|
||
if (isActive) {
|
||
activeBtn.className = "button button-primary model-btn-active";
|
||
activeBtn.textContent = "Ativa";
|
||
activeBtn.disabled = true;
|
||
} else {
|
||
activeBtn.className = "button button-ghost";
|
||
activeBtn.textContent = "Usar esta";
|
||
activeBtn.onclick = function () { setActiveEditorPersonality(entry.id); };
|
||
}
|
||
actions.appendChild(activeBtn);
|
||
|
||
var editBtn = document.createElement("button");
|
||
editBtn.type = "button";
|
||
editBtn.className = "link-button";
|
||
editBtn.textContent = "Editar";
|
||
editBtn.onclick = function () { editEditorPersonality(entry.id); };
|
||
actions.appendChild(editBtn);
|
||
|
||
var deleteBtn = document.createElement("button");
|
||
deleteBtn.type = "button";
|
||
deleteBtn.className = "link-button";
|
||
deleteBtn.textContent = "Excluir";
|
||
deleteBtn.onclick = function () { deleteEditorPersonality(entry.id); };
|
||
actions.appendChild(deleteBtn);
|
||
|
||
row.appendChild(actions);
|
||
container.appendChild(row);
|
||
});
|
||
}
|
||
|
||
function loadHfToken() {
|
||
return String(loadConfig().hfToken || "").trim();
|
||
}
|
||
|
||
function hfSpawnOptions() {
|
||
var token = loadHfToken();
|
||
if (!token) return undefined;
|
||
var nodeProcess = nodeRequire("process");
|
||
return { env: Object.assign({}, nodeProcess.env, { HF_TOKEN: token }) };
|
||
}
|
||
|
||
function loadModelsPath() {
|
||
return String(loadConfig().modelsPath || "").trim();
|
||
}
|
||
|
||
function effectiveModelsPath() {
|
||
return loadModelsPath() || DEFAULT_MODELS_PATH;
|
||
}
|
||
|
||
function saveModelsPath() {
|
||
var value = document.getElementById("modelsPath").value.trim();
|
||
if (!saveConfig({ modelsPath: value })) return;
|
||
modelsLog("Pasta de modelos salva: " + effectiveModelsPath());
|
||
showToast("ok", "Pasta de modelos salva.");
|
||
renderModelList();
|
||
}
|
||
|
||
function loadLanguage() {
|
||
return String(loadConfig().language || "").trim() || DEFAULT_LANGUAGE;
|
||
}
|
||
|
||
function saveLanguage() {
|
||
var value = document.getElementById("transcribeLanguage").value;
|
||
saveConfig({ language: value });
|
||
syncLanguageInfo();
|
||
showToast("ok", "Idioma da transcrição salvo.");
|
||
modelsLog("Idioma de transcrição salvo: " + value);
|
||
}
|
||
|
||
function loadDefaultModel() {
|
||
return String(loadConfig().defaultModel || "").trim() || DEFAULT_MODEL;
|
||
}
|
||
|
||
function setDefaultModel(model) {
|
||
saveConfig({ defaultModel: model });
|
||
modelsLog("Modelo padrão definido: " + model, "ok");
|
||
showToast("ok", "Modelo padrão: " + model + ".");
|
||
renderModelList();
|
||
syncTranscribeModelSelect();
|
||
}
|
||
|
||
// Mantém o seletor de modelo da aba de Transcrição em sincronia com o catálogo
|
||
// e com o modelo padrão escolhido na aba Modelos Locais.
|
||
function syncTranscribeModelSelect() {
|
||
var select = document.getElementById("silenceModel");
|
||
if (!select) return;
|
||
var current = select.value;
|
||
select.innerHTML = "";
|
||
var modelosDisponiveis = discoverPortugueseModelsOnMerongo();
|
||
modelosDisponiveis.forEach(function (modelo) {
|
||
var opt = document.createElement("option");
|
||
opt.value = modelo.nome;
|
||
opt.textContent = modelo.nome;
|
||
select.appendChild(opt);
|
||
});
|
||
if (!modelosDisponiveis.length) {
|
||
var vazio = document.createElement("option");
|
||
vazio.value = "";
|
||
vazio.textContent = "Nenhum modelo instalado";
|
||
select.appendChild(vazio);
|
||
return;
|
||
}
|
||
var nomes = modelosDisponiveis.map(function (modelo) { return modelo.nome; });
|
||
var desejado = nomes.indexOf(current) >= 0 ? current : loadDefaultModel();
|
||
select.value = nomes.indexOf(desejado) >= 0 ? desejado : nomes[0];
|
||
}
|
||
|
||
// ---- Settings tab: HF token ----
|
||
function saveHfToken() {
|
||
var token = document.getElementById("hfToken").value.trim();
|
||
if (!saveConfig({ hfToken: token })) return;
|
||
log("Hugging Face token salvo");
|
||
showToast("ok", token ? "Token salvo." : "Token removido.");
|
||
setHfTokenStatus(token ? "unknown" : "unset", token ? "Salvo — clique em Validar" : "Não configurado");
|
||
}
|
||
|
||
function toggleHfTokenVisibility() {
|
||
var input = document.getElementById("hfToken");
|
||
input.type = input.type === "password" ? "text" : "password";
|
||
}
|
||
|
||
function loadGroqApiKey() {
|
||
return String(loadConfig().groqApiKey || "").trim();
|
||
}
|
||
|
||
function saveGroqApiKey() {
|
||
var key = document.getElementById("groqApiKey").value.trim();
|
||
if (!saveConfig({ groqApiKey: key })) return;
|
||
log("Chave Groq salva");
|
||
showToast("ok", key ? "API Groq salva." : "API Groq removida.");
|
||
var status = document.getElementById("groqApiKeyStatus");
|
||
if (status) status.setAttribute("data-state", key ? "unknown" : "unset");
|
||
document.getElementById("groqApiKeyStatusText").textContent = key ? "Salva" : "Não configurada";
|
||
}
|
||
|
||
function toggleGroqApiKeyVisibility() {
|
||
var input = document.getElementById("groqApiKey");
|
||
input.type = input.type === "password" ? "text" : "password";
|
||
}
|
||
|
||
function setHfTokenStatus(state, text) {
|
||
var el = document.getElementById("hfTokenStatus");
|
||
if (!el) return;
|
||
el.setAttribute("data-state", state);
|
||
document.getElementById("hfTokenStatusText").textContent = text;
|
||
}
|
||
|
||
function validateHfToken() {
|
||
var token = document.getElementById("hfToken").value.trim() || loadHfToken();
|
||
if (!token) {
|
||
setHfTokenStatus("unset", "Não configurado");
|
||
return;
|
||
}
|
||
setHfTokenStatus("checking", "Validando…");
|
||
var req = https.get(
|
||
"https://huggingface.co/api/whoami-v2",
|
||
{ headers: { Authorization: "Bearer " + token, "User-Agent": "premiere-pro-mcp-connector" } },
|
||
function (response) {
|
||
var body = "";
|
||
response.setEncoding("utf8");
|
||
response.on("data", function (chunk) { body += chunk; });
|
||
response.on("end", function () {
|
||
if (response.statusCode === 200) {
|
||
var name = "";
|
||
try { name = JSON.parse(body).name || ""; } catch (e) {}
|
||
setHfTokenStatus("valid", "Token válido" + (name ? " (" + name + ")" : ""));
|
||
} else {
|
||
setHfTokenStatus("invalid", "Token inválido (HTTP " + response.statusCode + ")");
|
||
}
|
||
});
|
||
}
|
||
);
|
||
req.on("error", function (err) {
|
||
setHfTokenStatus("invalid", "Erro ao validar: " + err.message);
|
||
});
|
||
req.setTimeout(8000, function () {
|
||
req.destroy();
|
||
setHfTokenStatus("invalid", "Tempo esgotado ao validar");
|
||
});
|
||
}
|
||
|
||
// ---- Models tab ----
|
||
// Localiza a pasta de cache HF (formato "models--Systran--faster-whisper-<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("scanner");
|
||
|
||
// Set the default temp dir in the input field
|
||
document.getElementById("tempDir").value = tempDir;
|
||
|
||
// Restore saved temp dir
|
||
try {
|
||
var saved = localStorage.getItem("mcp_bridge_temp_dir");
|
||
if (saved) {
|
||
tempDir = saved;
|
||
document.getElementById("tempDir").value = tempDir;
|
||
}
|
||
} catch (e) {}
|
||
|
||
// Restore saved HF token (masked) and show its status
|
||
var savedToken = loadHfToken();
|
||
if (savedToken) {
|
||
document.getElementById("hfToken").value = savedToken;
|
||
setHfTokenStatus("unknown", "Salvo — clique em Validar para confirmar");
|
||
} else {
|
||
setHfTokenStatus("unset", "Não configurado");
|
||
}
|
||
var savedGroqApiKey = loadGroqApiKey();
|
||
document.getElementById("groqApiKey").value = savedGroqApiKey;
|
||
if (savedGroqApiKey) {
|
||
document.getElementById("groqApiKeyStatus").setAttribute("data-state", "unknown");
|
||
document.getElementById("groqApiKeyStatusText").textContent = "Salva";
|
||
}
|
||
document.getElementById("modelsPath").value = effectiveModelsPath();
|
||
renderLanguageOptions();
|
||
document.getElementById("transcribeLanguage").value = loadLanguage();
|
||
renderModelList();
|
||
syncTranscribeModelSelect();
|
||
syncLanguageInfo();
|
||
renderEditorPersonalities();
|
||
restaurarConfiguracaoDeLegendas();
|
||
renderizarTiposDeVideoParaEdicao();
|
||
|
||
// A aba Editar vídeo começa com a análise já produzida pelo Scanner.
|
||
setStep(1, "locked", "Scanner");
|
||
setStep(2, "locked", "Scanner");
|
||
setStep(3, "locked", "Aguardando análise");
|
||
setStep(4, "locked", "Aguardando análise");
|
||
setStep(5, "locked", "Aguardando plano");
|
||
|
||
log("MCP for Adobe Premiere Pro CEP connector loaded");
|
||
setStatus("waiting", "Aguardando — a ponte está iniciando");
|
||
|
||
// Always auto-start. The headless instance (StartOn ApplicationActivate) has no
|
||
// one to click Start, and macOS periodically purges the temp dir — so create it
|
||
// rather than gating auto-start on its existence.
|
||
ensureDir(tempDir);
|
||
startBridgeHeartbeat();
|
||
log("Auto-starting bridge...");
|
||
setTimeout(startBridge, 500);
|
||
if (!restoreScheduledUpdateStatus()) setTimeout(checkForUpdates, 1200);
|
||
})();
|