/* 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: "projeto", panel: "tabProjeto", btn: "tabBtnProjeto" }, { key: "scanner", panel: "tabScanner", btn: "tabBtnScanner" }, { key: "trilhas", panel: "tabTrilhas", btn: "tabBtnTrilhas" }, { key: "refinar", panel: "tabRefinar", btn: "tabBtnRefinar" }, { key: "silence", panel: "tabSilence", btn: "tabBtnSilence" }, { key: "retakes", panel: "tabRetakes", btn: "tabBtnRetakes" }, { key: "analises", panel: "tabAnalises", btn: "tabBtnAnalises" }, { key: "tipos-video", panel: "tabTiposVideo", btn: "tabBtnTiposVideo" }, { key: "bridge", panel: "tabBridge", btn: "tabBtnBridge" }, { key: "models", panel: "tabModels", btn: "tabBtnModels" }, { key: "settings", panel: "tabSettings", btn: "tabBtnSettings" }, { key: "testar-acoes", panel: "tabTestarAcoes", btn: "tabBtnTestarAcoes" }, { key: "legendas", panel: "tabLegendas", btn: "tabBtnLegendas" }, ]; tabs.forEach(function (t) { var active = t.key === name; var panel = document.getElementById(t.panel); var btn = document.getElementById(t.btn); if (!panel || !btn) return; panel.hidden = !active; btn.classList.toggle("active", active); btn.setAttribute("aria-selected", String(active)); }); if (name === "models") renderModelList(); if (name === "trilhas") trilhasAoAbrir(); if (name === "projeto") projetoListarSequencias(); if (name === "silence") { syncTranscribeModelSelect(); syncLanguageInfo(); renderizarTiposDeVideoParaEdicao(); } if (name === "settings") renderEditorPersonalities(); if (name === "scanner") { renderScannerTranscriptionOptions(); loadScannerProfile(); renderScannerHuggingFaceStatus(); scannerListarAnalisesExistentes(); } if (name === "refinar") refinarAoAbrir(); if (name === "retakes") retakesAoAbrir(); if (name === "analises") analisesAoAbrir(); if (name === "tipos-video") tiposVideoAoAbrir(); } var LEGENDAS_CONFIGURACAO_CHAVE = "schedule_light_configuracao_legendas"; function configuracaoDeLegendasPadrao() { return { habilitadas: false, estilo: "word_top_down", faixa_video: 3, max_palavras: 5, max_caracteres: 32, mogrt_path: "" }; } function carregarConfiguracaoDeLegendas() { var configuracao = configuracaoDeLegendasPadrao(); try { var salva = JSON.parse(localStorage.getItem(LEGENDAS_CONFIGURACAO_CHAVE) || "null"); if (salva && typeof salva === "object") configuracao = Object.assign(configuracao, salva); } catch (e) {} return configuracao; } function restaurarConfiguracaoDeLegendas() { var configuracao = carregarConfiguracaoDeLegendas(); document.getElementById("legendasHabilitadas").checked = configuracao.habilitadas; document.getElementById("legendasEstilo").value = configuracao.estilo; document.getElementById("legendasFaixa").value = configuracao.faixa_video; document.getElementById("legendasMaxPalavras").value = configuracao.max_palavras; document.getElementById("legendasMaxCaracteres").value = configuracao.max_caracteres; document.getElementById("legendasMogrtPath").value = configuracao.mogrt_path; } function salvarConfiguracaoDeLegendas() { var configuracao = { habilitadas: !!document.getElementById("legendasHabilitadas").checked, estilo: document.getElementById("legendasEstilo").value, faixa_video: Number(document.getElementById("legendasFaixa").value), max_palavras: Number(document.getElementById("legendasMaxPalavras").value), max_caracteres: Number(document.getElementById("legendasMaxCaracteres").value), mogrt_path: document.getElementById("legendasMogrtPath").value.trim(), }; try { localStorage.setItem(LEGENDAS_CONFIGURACAO_CHAVE, JSON.stringify(configuracao)); } catch (e) {} document.getElementById("legendasStatus").textContent = "Configuração de legendas salva neste computador."; } var legendasGeradas = []; function legendaNumeroParaTempo(segundos) { var horas = Math.floor(segundos / 3600); var minutos = Math.floor((segundos % 3600) / 60); var segundosInteiros = Math.floor(segundos % 60); var milissegundos = Math.round((segundos - Math.floor(segundos)) * 1000); if (milissegundos >= 1000) { segundosInteiros++; milissegundos = 0; } return [horas, minutos, segundosInteiros].map(function (n) { return String(n).padStart(2, "0"); }).join(":") + "," + String(milissegundos).padStart(3, "0"); } function legendasExtrairPalavras(transcricao) { var segmentos = transcricao.segmentos || transcricao.segments || []; var palavras = []; segmentos.forEach(function (segmento) { var itens = segmento.palavras || segmento.words || []; itens.forEach(function (palavra) { var texto = String(palavra.texto || palavra.word || "").trim(); var inicio = Number(palavra.inicio !== undefined ? palavra.inicio : palavra.start); var fim = Number(palavra.fim !== undefined ? palavra.fim : palavra.end); if (texto && isFinite(inicio) && isFinite(fim) && fim > inicio) palavras.push({ texto: texto, inicio: inicio, fim: fim }); }); }); return palavras.sort(function (a, b) { return a.inicio - b.inicio; }); } function legendasIntervaloCortado(palavra, cortes) { return cortes.some(function (corte) { return palavra.inicio < corte.end && palavra.fim > corte.start; }); } function legendasGerarBlocos(palavras, cortes, configuracao) { var mantidas = palavras.filter(function (palavra) { return !legendasIntervaloCortado(palavra, cortes); }); var blocos = [], bloco = [], caracteres = 0; function adicionar() { if (!bloco.length) return; var primeiro = bloco[0], ultimo = bloco[bloco.length - 1]; // Para o primeiro corte, todos os cortes anteriores são considerados. var inicio = primeiro.inicio - cortes.reduce(function (total, corte) { return total + (corte.end <= primeiro.inicio ? corte.end - corte.start : 0); }, 0); var fim = ultimo.fim - cortes.reduce(function (total, corte) { return total + (corte.end <= ultimo.fim ? corte.end - corte.start : 0); }, 0); blocos.push({ texto: bloco.map(function (item) { return item.texto; }).join(" "), inicio: inicio, fim: fim }); bloco = []; caracteres = 0; } mantidas.forEach(function (palavra) { var espaco = bloco.length ? 1 : 0; if (bloco.length && (bloco.length >= configuracao.max_palavras || caracteres + espaco + palavra.texto.length > configuracao.max_caracteres)) adicionar(); bloco.push(palavra); caracteres += (bloco.length > 1 ? 1 : 0) + palavra.texto.length; }); adicionar(); return blocos; } function legendasRenderizar() { var lista = document.getElementById("legendasLista"); document.getElementById("legendasContagem").textContent = "(" + legendasGeradas.length + ")"; lista.textContent = ""; if (!legendasGeradas.length) { lista.innerHTML = '

Nenhuma legenda carregada.

'; return; } legendasGeradas.forEach(function (legenda, indice) { var linha = document.createElement("div"); linha.className = "legenda-row"; linha.setAttribute("role", "listitem"); var cabecalho = document.createElement("div"); cabecalho.className = "legenda-timecode"; cabecalho.textContent = (indice + 1) + " " + legendaNumeroParaTempo(legenda.inicio) + " → " + legendaNumeroParaTempo(legenda.fim); var campo = document.createElement("input"); campo.className = "text-field"; campo.value = legenda.texto; campo.setAttribute("aria-label", "Texto da legenda " + (indice + 1)); campo.oninput = function () { legendasGeradas[indice].texto = campo.value; }; var excluir = document.createElement("button"); excluir.className = "button button-ghost"; excluir.type = "button"; excluir.textContent = "×"; excluir.title = "Excluir legenda"; excluir.onclick = function () { legendasGeradas.splice(indice, 1); legendasRenderizar(); }; linha.appendChild(cabecalho); linha.appendChild(campo); linha.appendChild(excluir); lista.appendChild(linha); }); } function gerarLegendasDaTranscricao() { var caminhoDaTranscricao = obterCaminhoDaTranscricaoParaLegendas(); if (!caminhoDaTranscricao) { document.getElementById("legendasStatus").textContent = "Escolha uma timeline na aba Editar vídeo e carregue uma transcrição salva ou execute a transcrição."; return; } try { var transcricao = JSON.parse(fs.readFileSync(caminhoDaTranscricao, "utf-8")); var plano = JSON.parse(document.getElementById("testarAcoesJson").value || "{}"); var cortes = (plano.actions || []).filter(function (acao) { return acao.kind === "cut"; }).map(function (acao) { return { start: Number(acao.start), end: Number(acao.end) }; }); var configuracao = carregarConfiguracaoDeLegendas(); legendasGeradas = legendasGerarBlocos(legendasExtrairPalavras(transcricao), cortes, configuracao); legendasRenderizar(); document.getElementById("legendasStatus").textContent = legendasGeradas.length + " legenda(s) gerada(s) para revisão."; } catch (erro) { document.getElementById("legendasStatus").textContent = "Não foi possível gerar: " + erro.message; } } function obterCaminhoDaTranscricaoParaLegendas() { if (silenceState.transcriptPath && fs.existsSync(silenceState.transcriptPath)) return silenceState.transcriptPath; if (!silenceState.mediaPath) return null; var entradas = findCachedTranscripts(silenceState.mediaPath).sort(function (a, b) { return new Date(b.savedAt) - new Date(a.savedAt); }); if (!entradas.length) return null; silenceState.transcriptPath = entradas[0].transcriptPath; silenceState.cachedTranscriptPath = entradas[0].transcriptPath; return silenceState.transcriptPath; } function exportarLegendasSrt() { if (!legendasGeradas.length) { document.getElementById("legendasStatus").textContent = "Gere as legendas antes de exportar."; return; } var destino = path.join(silenceState.outputDir || os.tmpdir(), "legendas-geradas.srt"); var conteudo = legendasGeradas.map(function (legenda, indice) { return (indice + 1) + "\n" + legendaNumeroParaTempo(legenda.inicio) + " --> " + legendaNumeroParaTempo(legenda.fim) + "\n" + legenda.texto + "\n"; }).join("\n"); fs.writeFileSync(destino, conteudo, "utf-8"); document.getElementById("legendasStatus").textContent = "SRT exportado em " + destino; } function inserirLegendasNaTimeline() { document.getElementById("legendasStatus").textContent = "A lista está pronta. A inserção automática dos MOGRTs será conectada na próxima etapa, após validar o estilo escolhido."; } // ---- Teste isolado de ações pela engine Python ---------------------------- var TESTAR_ACOES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/aplicar_plano_de_edicao.py"; function testarAcoesCarregarExemplo() { var exemplo = { schema_version: "1.0", source: "video.mp4", actions: [{ kind: "cut", start: 0.0, end: 1.0, reason: "Teste de corte inicial pela engine Python." }] }; document.getElementById("testarAcoesJson").value = JSON.stringify(exemplo, null, 2); testarAcoesAtualizarStatus("Exemplo carregado. Ajuste o source e os tempos antes de executar.", ""); } function testarAcoesLog(mensagem, classe) { var alvo = document.getElementById("testarAcoesLog"); if (!alvo) return; var linha = document.createElement("p"); linha.textContent = mensagem; if (classe) linha.className = classe; alvo.appendChild(linha); alvo.scrollTop = alvo.scrollHeight; } function testarAcoesAtualizarStatus(mensagem, classe) { var status = document.getElementById("testarAcoesStatus"); if (status) { status.textContent = mensagem; status.className = "field-help " + (classe || ""); } } function testarAcoesExecutar() { var campo = document.getElementById("testarAcoesJson"); var texto = campo.value.trim(); var plano; if (!texto) { testarAcoesAtualizarStatus("Cole um plano JSON antes de executar.", "err"); return; } try { plano = JSON.parse(texto); } catch (erro) { testarAcoesAtualizarStatus("JSON inválido: " + erro.message, "err"); testarAcoesLog("JSON inválido: " + erro.message, "err"); return; } if (!plano || typeof plano !== "object" || !plano.source || !Array.isArray(plano.actions) || !plano.actions.length) { testarAcoesAtualizarStatus("O plano precisa ter source e uma lista não vazia de actions.", "err"); return; } var tipos = plano.actions.map(function (acao) { return acao && acao.kind; }); if (tipos.some(function (tipo) { return tipo !== "cut"; })) { testarAcoesAtualizarStatus("Este primeiro teste aceita somente actions kind: cut.", "err"); return; } var entrada = path.join(os.tmpdir(), "teste-acoes-" + Date.now() + ".json"); try { fs.writeFileSync(entrada, JSON.stringify(plano, null, 2), "utf-8"); } catch (erroEscrita) { testarAcoesAtualizarStatus("Não foi possível criar o plano temporário: " + erroEscrita.message, "err"); return; } var botao = document.getElementById("btnTestarAcoesExecutar"); if (botao) botao.disabled = true; testarAcoesAtualizarStatus("A engine está conectando ao Premiere e aplicando o corte…", ""); testarAcoesLog("Plano temporário criado: " + entrada); taskStart("Teste de ação", "Executando pela engine Python"); runStreamed( SCANNER_PYTHON_BIN, [TESTAR_ACOES_ENGINE_SCRIPT, entrada], function (linha) { testarAcoesLog(linha); }, function (codigo, ultimoErro, ultimaSaida) { if (botao) botao.disabled = false; if (codigo !== 0) { testarAcoesAtualizarStatus("Falha na execução. Consulte os detalhes técnicos.", "err"); testarAcoesLog(ultimoErro || "A engine terminou com código " + codigo + ".", "err"); taskEnd(false, "Teste de ação falhou", ultimoErro || null); return; } testarAcoesAtualizarStatus("Corte executado pela engine Python. Verifique a timeline no Premiere.", "ok"); testarAcoesLog("Engine concluída com sucesso.", "ok"); taskEnd(true, "Teste de corte concluído"); }, { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine" } ); } // ---- Catálogo de tipos de vídeo ------------------------------------------- // A primeira versão usa o armazenamento local do painel. O documento salvo já // tem formato estável e pode ser migrado para o Schedule Light sem alterar o // contrato editorial enviado para a IA. function tiposVideoLer() { try { var salvo = JSON.parse(localStorage.getItem(TIPOS_VIDEO_CHAVE) || "null"); if (Array.isArray(salvo) && salvo.length) return salvo.map(tipoVideoCompletar); var iniciais = tiposVideoPadrao.slice().map(tipoVideoCompletar); tiposVideoSalvarNoCatalogo(iniciais); return iniciais; } catch (_) { return tiposVideoPadrao.slice().map(tipoVideoCompletar); } } function tiposVideoSalvarNoCatalogo(lista) { localStorage.setItem(TIPOS_VIDEO_CHAVE, JSON.stringify(lista)); } function tiposVideoAoAbrir() { tiposVideoRenderizarLista(); if (!tipoVideoAtualId) tipoVideoNovo(); } function tiposVideoRenderizarLista() { var lista = document.getElementById("tiposVideoLista"); if (!lista) return; lista.innerHTML = ""; tiposVideoLer().forEach(function (tipo) { var item = document.createElement("div"); item.className = "video-type-row" + (tipo.id === tipoVideoAtualId ? " is-selected" : ""); item.setAttribute("role", "listitem"); item.innerHTML = '
'; item.querySelector("strong").textContent = tipo.nome; item.querySelector("span").textContent = tipo.descricao; item.querySelector(".video-type-meta").textContent = tipo.duracao_maxima_segundos + " s · " + tipo.proporcao + " · " + tipo.canais; item.querySelector(".video-type-status").textContent = tipo.ativo === false ? "Inativo" : "Ativo"; item.querySelector(".video-type-status").setAttribute("data-state", tipo.ativo === false ? "inactive" : "active"); item.querySelector(".video-type-edit").onclick = function () { tipoVideoEditar(tipo.id); }; item.querySelector(".video-type-duplicate").onclick = function () { tipoVideoDuplicar(tipo.id); }; item.querySelector(".video-type-toggle").textContent = tipo.ativo === false ? "Ativar" : "Inativar"; item.querySelector(".video-type-toggle").onclick = function () { tipoVideoAlternarAtivo(tipo.id); }; item.querySelector(".video-type-copy").onclick = function () { tipoVideoEditar(tipo.id); }; lista.appendChild(item); }); } function tipoVideoNovo() { tipoVideoAtualId = null; tipoVideoPreencher(tipoVideoCompletar({ id: "", nome: "", duracao_maxima_segundos: 60, descricao: "", canais: "", proporcao: "9:16", ritmo: "equilibrado", emocao: "", storytelling: true, estrutura: "", cortes: "", broll: "", texto: "", som: "", diretriz: "" })); document.getElementById("tipoVideoEditorTitulo").textContent = "Novo tipo de vídeo"; document.getElementById("tipoVideoEditorStatus").textContent = "Rascunho"; document.getElementById("tipoVideoEditorCard").hidden = false; tiposVideoRenderizarLista(); } function tipoVideoEditar(id) { var tipo = tiposVideoLer().filter(function (item) { return item.id === id; })[0]; if (!tipo) return; tipoVideoAtualId = id; tipoVideoPreencher(tipo); document.getElementById("tipoVideoEditorTitulo").textContent = "Editar tipo de vídeo"; document.getElementById("tipoVideoEditorStatus").textContent = "Cadastrado"; document.getElementById("tipoVideoEditorCard").hidden = false; tiposVideoRenderizarLista(); } function tipoVideoPreencher(tipo) { tipo = tipoVideoCompletar(tipo); ["nome", "descricao", "canais", "proporcao", "ritmo", "emocao", "estrutura", "cortes", "broll", "texto", "som", "diretriz", "categoria", "objetivo", "objetivo_secundario", "publico", "mensagem", "tipo_narrativa", "foco_narrativo", "tom", "construcao_emocao", "restricoes", "qualidade", "prioridade_voz"].forEach(function (campo) { var elemento = document.getElementById("tipoVideo" + campo.charAt(0).toUpperCase() + campo.slice(1)); if (elemento) elemento.value = tipo[campo] || ""; }); document.getElementById("tipoVideoDuracao").value = tipo.duracao_maxima_segundos || ""; document.getElementById("tipoVideoDuracaoMinima").value = tipo.duracao_minima || ""; document.getElementById("tipoVideoStorytelling").checked = !!tipo.storytelling; document.getElementById("tipoVideoAtivo").checked = tipo.ativo !== false; document.getElementById("tipoVideoIntensidade").value = tipo.intensidade || 3; document.getElementById("tipoVideoIntensidadeMusica").value = tipo.intensidade_musica || 0; document.getElementById("tipoVideoUsaBroll").checked = !!tipo.usa_broll; document.getElementById("tipoVideoUsaLegendas").checked = !!tipo.usa_legendas; document.getElementById("tipoVideoUsaTextos").checked = !!tipo.usa_textos; document.getElementById("tipoVideoUsaGraficos").checked = !!tipo.usa_graficos; document.getElementById("tipoVideoUsaZoom").checked = !!tipo.usa_zoom; document.getElementById("tipoVideoUsaMusica").checked = !!tipo.usa_musica; tipoVideoRenderizarPrioridades(tipo.prioridades); tipoVideoAtualizarIntensidade(); tipoVideoAtualizarIntensidadeMusica(); } function tipoVideoRenderizarPrioridades(prioridades) { var lista = document.getElementById("tipoVideoPrioridades"); lista.innerHTML = ""; (prioridades || []).slice(0, 5).forEach(function (valor, indice) { var linha = document.createElement("div"); linha.className = "priority-row"; linha.innerHTML = '' + (indice + 1) + ''; linha.querySelector("input").value = valor; lista.appendChild(linha); }); } function tipoVideoPrioridadesDoFormulario() { return Array.prototype.slice.call(document.querySelectorAll("#tipoVideoPrioridades .priority-input")).map(function (input) { return input.value.trim(); }).filter(Boolean); } function tipoVideoAtualizarIntensidade() { var valor = document.getElementById("tipoVideoIntensidade"); var saida = document.getElementById("tipoVideoIntensidadeValor"); if (valor && saida) saida.textContent = valor.value + " / 5"; } function tipoVideoAtualizarIntensidadeMusica() { var valor = document.getElementById("tipoVideoIntensidadeMusica"); var saida = document.getElementById("tipoVideoIntensidadeMusicaValor"); if (valor && saida) saida.textContent = valor.value + "%"; } function tipoVideoDoFormulario() { var nome = document.getElementById("tipoVideoNome").value.trim(); if (!nome) throw new Error("Informe o nome do tipo de vídeo."); var base = tipoVideoAtualId ? tiposVideoLer().filter(function (item) { return item.id === tipoVideoAtualId; })[0] : null; return { id: tipoVideoAtualId || (nome.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + "-" + Date.now()), nome: nome, descricao: document.getElementById("tipoVideoDescricao").value.trim(), ativo: document.getElementById("tipoVideoAtivo").checked, categoria: document.getElementById("tipoVideoCategoria").value, objetivo: document.getElementById("tipoVideoObjetivo").value.trim(), objetivo_secundario: document.getElementById("tipoVideoObjetivoSecundario").value.trim(), publico: document.getElementById("tipoVideoPublico").value.trim(), mensagem: document.getElementById("tipoVideoMensagem").value.trim(), tipo_narrativa: document.getElementById("tipoVideoTipoNarrativa").value, foco_narrativo: document.getElementById("tipoVideoFocoNarrativo").value.trim(), intensidade: Number(document.getElementById("tipoVideoIntensidade").value) || 3, tom: document.getElementById("tipoVideoTom").value, construcao_emocao: document.getElementById("tipoVideoConstrucaoEmocao").value.trim(), duracao_maxima_segundos: Number(document.getElementById("tipoVideoDuracao").value) || 0, duracao_minima: Number(document.getElementById("tipoVideoDuracaoMinima").value) || 0, canais: document.getElementById("tipoVideoCanais").value.trim(), proporcao: document.getElementById("tipoVideoProporcao").value, ritmo: document.getElementById("tipoVideoRitmo").value, emocao: document.getElementById("tipoVideoEmocao").value.trim(), storytelling: document.getElementById("tipoVideoStorytelling").checked, estrutura: document.getElementById("tipoVideoEstrutura").value.trim(), cortes: document.getElementById("tipoVideoCortes").value.trim(), broll: document.getElementById("tipoVideoBroll").value.trim(), texto: document.getElementById("tipoVideoTexto").value.trim(), som: document.getElementById("tipoVideoSom").value.trim(), diretriz: document.getElementById("tipoVideoDiretriz").value.trim(), prioridades: tipoVideoPrioridadesDoFormulario(), usa_broll: document.getElementById("tipoVideoUsaBroll").checked, usa_legendas: document.getElementById("tipoVideoUsaLegendas").checked, usa_textos: document.getElementById("tipoVideoUsaTextos").checked, usa_graficos: document.getElementById("tipoVideoUsaGraficos").checked, usa_zoom: document.getElementById("tipoVideoUsaZoom").checked, usa_musica: document.getElementById("tipoVideoUsaMusica").checked, intensidade_musica: Number(document.getElementById("tipoVideoIntensidadeMusica").value) || 0, prioridade_voz: document.getElementById("tipoVideoPrioridadeVoz").value, restricoes: document.getElementById("tipoVideoRestricoes").value.trim(), qualidade: document.getElementById("tipoVideoQualidade").value.trim(), criado_em: base && base.criado_em ? base.criado_em : new Date().toISOString(), atualizado_em: new Date().toISOString() }; } function tipoVideoSalvar() { try { var item = tipoVideoDoFormulario(); var lista = tiposVideoLer(); var indice = lista.findIndex(function (tipo) { return tipo.id === item.id; }); if (indice >= 0) lista[indice] = item; else lista.push(item); tiposVideoSalvarNoCatalogo(lista); tipoVideoAtualId = item.id; document.getElementById("tipoVideoEditorStatus").textContent = "Salvo"; document.getElementById("tiposVideoStatus").textContent = "Catálogo salvo localmente · " + lista.length + " tipo(s) cadastrado(s)."; tiposVideoRenderizarLista(); showToast("ok", "Tipo de vídeo salvo no catálogo."); } catch (erro) { showToast("err", erro.message); } } function tipoVideoDuplicar(id) { var origem = tiposVideoLer().filter(function (item) { return item.id === id; })[0]; if (!origem) return; var copia = JSON.parse(JSON.stringify(origem)); copia.id = origem.id + "-copia-" + Date.now(); copia.nome = origem.nome + " (cópia)"; copia.ativo = true; copia.criado_em = new Date().toISOString(); copia.atualizado_em = copia.criado_em; var lista = tiposVideoLer(); lista.push(copia); tiposVideoSalvarNoCatalogo(lista); tipoVideoAtualId = copia.id; tipoVideoPreencher(copia); document.getElementById("tipoVideoEditorTitulo").textContent = "Editar tipo de vídeo"; document.getElementById("tipoVideoEditorStatus").textContent = "Cópia criada"; document.getElementById("tipoVideoEditorCard").hidden = false; document.getElementById("tiposVideoStatus").textContent = "Perfil duplicado. Revise os campos e salve a cópia."; tiposVideoRenderizarLista(); showToast("ok", "Perfil duplicado. Você pode ajustar a cópia antes de salvar."); } function tipoVideoAlternarAtivo(id) { var lista = tiposVideoLer(); var item = lista.filter(function (perfil) { return perfil.id === id; })[0]; if (!item) return; item.ativo = item.ativo === false; item.atualizado_em = new Date().toISOString(); tiposVideoSalvarNoCatalogo(lista); if (tipoVideoAtualId === id) { document.getElementById("tipoVideoAtivo").checked = item.ativo; document.getElementById("tipoVideoEditorStatus").textContent = item.ativo ? "Ativo" : "Inativo"; } tiposVideoRenderizarLista(); showToast("ok", item.ativo ? "Perfil ativado." : "Perfil inativado."); } function tipoVideoCancelar() { tipoVideoAtualId = null; document.getElementById("tipoVideoEditorCard").hidden = true; tiposVideoRenderizarLista(); } function tipoVideoCopiarJson() { try { var tipo = tipoVideoDoFormulario(); var resumo = { perfil_de_video: tipo.nome, objetivo: { principal: tipo.objetivo, secundario: tipo.objetivo_secundario, publico: tipo.publico, mensagem: tipo.mensagem }, narrativa: { tipo: tipo.tipo_narrativa, foco: tipo.foco_narrativo, storytelling: tipo.storytelling, estrutura: tipo.estrutura }, emocao: { objetivo: tipo.emocao, intensidade: tipo.intensidade, tom: tipo.tom, construcao: tipo.construcao_emocao }, formato: { canais: tipo.canais, proporcao: tipo.proporcao, duracao_minima_segundos: tipo.duracao_minima, duracao_maxima_segundos: tipo.duracao_maxima_segundos, ritmo: tipo.ritmo }, prioridades: tipo.prioridades, elementos_de_edicao: { broll: tipo.usa_broll, legendas: tipo.usa_legendas, textos: tipo.usa_textos, graficos: tipo.usa_graficos, zoom: tipo.usa_zoom, cortes: tipo.cortes, orientacao_de_broll: tipo.broll, textos_e_legendas: tipo.texto }, audio: { musica: tipo.usa_musica, intensidade_da_musica: tipo.intensidade_musica, prioridade_da_voz: tipo.prioridade_voz, tratamento: tipo.som }, regras_de_edicao: tipo.diretriz, restricoes: tipo.restricoes, criterios_de_qualidade: tipo.qualidade }; copiarTextoParaAreaDeTransferencia(JSON.stringify(resumo, null, 2)); } catch (erro) { showToast("err", erro.message); } } function copiarTextoParaAreaDeTransferencia(texto, mensagem) { mensagem = mensagem || "Conteúdo copiado para a área de transferência."; function copiarComSelecaoDoPainel() { var area = document.createElement("textarea"); area.value = texto; area.setAttribute("readonly", "readonly"); area.style.position = "fixed"; area.style.left = "-9999px"; area.style.top = "0"; document.body.appendChild(area); area.focus(); area.select(); var copiado = false; try { copiado = document.execCommand("copy"); } catch (_) { copiado = false; } document.body.removeChild(area); if (copiado) showToast("ok", mensagem); else showToast("err", "Não foi possível copiar o JSON. Selecione e copie manualmente."); } // A API nativa preserva o texto Unicode no clipboard do CEP/Chromium. // Use pbcopy apenas quando a API não existir ou falhar. try { if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(texto).then(function () { showToast("ok", mensagem); }).catch(function () { copiarComPbcopy(texto, copiarComSelecaoDoPainel, mensagem); }); return; } } catch (_) {} copiarComPbcopy(texto, copiarComSelecaoDoPainel, mensagem); } function copiarComPbcopy(texto, fallback, mensagem) { try { var copia = childProcess.spawn("pbcopy"); var finalizado = false; function concluir(copiado) { if (finalizado) return; finalizado = true; if (copiado) showToast("ok", mensagem || "Conteúdo copiado para a área de transferência."); else fallback(); } copia.once("error", function () { concluir(false); }); copia.once("close", function (codigo) { concluir(codigo === 0); }); copia.stdin.write(texto, "utf8"); copia.stdin.end(); } catch (_) { fallback(); } } // ---- Análises --------------------------------------------------------------- // Aba que lê o banco de análises (.jhonny/analises.db) já produzido pelo // Scanner e mostra/copiar os dois J-SOM: som (transcrição por cena) e // imagem (evidências visuais). A leitura acontece na engine, via script // dedicado; a aba nunca escreve no banco. var ANALISES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/gerar_relatorio_de_analises.py"; var ANALISES_DB_PATH = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db"; var analisesRelatorioPath = null; function analisesAoAbrir() { document.getElementById("analisesCaminho").textContent = ANALISES_DB_PATH; } function analisesFormatarTempo(segundos) { var s = Number(segundos) || 0; var min = Math.floor(s / 60), resto = (s - min * 60).toFixed(1); return min + ":" + (resto < 10 ? "0" : "") + resto; } function analisesCarregar() { var stamp = new Date().toISOString().replace(/[:.]/g, "-"); analisesRelatorioPath = path.join(os.tmpdir(), "jhonny-analises-" + stamp + ".json"); setBusy("btnAnalisesCarregar", true); taskStart("Análises", "Lendo o banco de análises", null); var child = childProcess.spawn(SCANNER_PYTHON_BIN, [ANALISES_ENGINE_SCRIPT, ANALISES_DB_PATH, analisesRelatorioPath], { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", stdio: ["ignore", "pipe", "pipe"] }); var stderr = "", stdout = ""; child.stdout.on("data", function (chunk) { stdout += chunk.toString(); }); child.stderr.on("data", function (chunk) { stderr += chunk.toString(); }); child.on("error", function (error) { setBusy("btnAnalisesCarregar", false); taskEnd(false, "Análises indisponíveis", error.stack || error.message); }); child.on("close", function (code) { setBusy("btnAnalisesCarregar", false); if (code !== 0 || !fs.existsSync(analisesRelatorioPath)) { taskEnd(false, "Leitura do banco falhou", stderr || stdout || "Não foi possível gerar o relatório."); return; } taskEnd(true, "Relatório carregado do banco"); analisesRender(JSON.parse(fs.readFileSync(analisesRelatorioPath, "utf-8"))); }); } function analisesRender(relatorio) { var video = relatorio.video || {}; var cenas = relatorio.cenas || []; var falas = cenas.reduce(function (n, c) { return n + (c.falas || []).length; }, 0); var evidencias = cenas.reduce(function (n, c) { var resumo = c.resumo_de_evidencias || {}; return n + Object.keys(resumo).reduce(function (m, k) { return m + resumo[k]; }, 0); }, 0); document.getElementById("analisesStatus").textContent = "Carregado"; document.getElementById("analisesVideoNome").textContent = video.nome || "(sem nome)"; document.getElementById("analisesResumo").textContent = cenas.length + " cenas · " + falas + " falas · " + evidencias + " evidências visuais"; document.getElementById("analisesResumoCard").hidden = false; document.getElementById("btnAnalisesAbrir").disabled = false; document.getElementById("btnAnalisesCopiar").disabled = false; var lista = document.getElementById("analisesCenas"); lista.innerHTML = ""; cenas.forEach(function (cena) { var bloco = document.createElement("div"); bloco.className = "analise-cena"; bloco.setAttribute("role", "listitem"); var titulo = cena.cena_registrada ? "" : " (sem cena gravada)"; var tipos = Object.keys(cena.resumo_de_evidencias || {}) .sort().map(function (k) { return k + " " + cena.resumo_de_evidencias[k]; }).join(", ") || "—"; var falasHtml = (cena.falas || []).map(function (fala) { return "

" + (fala.emocao || "—") + " " + analisesFormatarTempo(fala.inicio) + "–" + analisesFormatarTempo(fala.fim) + " · " + String(fala.texto || "").replace(/&/g, "&").replace(/"; }).join("") || "

Sem fala neste clipe.

"; bloco.innerHTML = "
" + cena.clipe_id + titulo + "" + "" + analisesFormatarTempo(cena.inicio) + "–" + analisesFormatarTempo(cena.fim) + "
" + falasHtml + "

Imagem: " + tipos + "

"; lista.appendChild(bloco); }); } function analisesAbrirJson() { if (!analisesRelatorioPath) return; childProcess.spawn("open", [analisesRelatorioPath], { detached: true, stdio: "ignore" }).unref(); } function analisesCopiar() { if (!analisesRelatorioPath) return; try { var copy = childProcess.spawn("pbcopy"); copy.stdin.end(fs.readFileSync(analisesRelatorioPath, "utf-8")); showToast("ok", "J-SOM de som e imagem copiados (JSON completo)."); } catch (error) { showToast("err", error.message); } } function abrirPastaDoJsonGerado() { if (!silenceState.jsonOutputPath) return; try { childProcess.spawn("open", [path.dirname(silenceState.jsonOutputPath)], { detached: true, stdio: "ignore" }).unref(); } catch (erro) { showToast("err", "Não foi possível abrir a pasta no Finder: " + erro.message); } } function copiarCaminhoDoJsonGerado() { if (!silenceState.jsonOutputPath) return; copiarComPbcopy( silenceState.jsonOutputPath, function () { copiarTextoParaAreaDeTransferencia(silenceState.jsonOutputPath, "Caminho do JSON copiado para a área de transferência."); }, "Caminho do JSON copiado para a área de transferência." ); } // ---- Retakes --------------------------------------------------------------- // A aba apenas controla o processo e mostra resultados; a análise inteira // acontece na engine (scanner/retakes), chamada via executar_retakes.py. var RETAKES_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/executar_retakes.py"; // A engine usa tipos do Python 3.10+ (ex.: `X | None`, `dict[str, ...]`), que // o `/usr/bin/python3` do macOS (3.9) não aceita. Usar o mesmo venv do projeto // (Python 3.14, com faster_whisper) garante que Scanner e Retakes carreguem bem. var SCANNER_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python"; var ANALISE_STATUS_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_status_da_analise.py"; var ANALISE_PREVIEW_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_preview_de_falantes.py"; var REGISTRAR_EDICAO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/registrar_edicao.py"; var CARREGAR_PLANO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/carregar_plano_de_edicao.py"; var REGISTRAR_PROJETO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/registrar_projeto_de_edicao.py"; var CONSULTAR_EDICAO_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/consultar_edicao_concluida.py"; var planoEditorialIdCarregado = null; var retakesCasoAtual = null; var retakesGrupos = []; var retakesProcesso = null; function retakesRaiz() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); } function retakesAoAbrir() { retakesListarCasos(); } function retakesListarCasos() { var seletor = document.getElementById("retakesCaso"); var anterior = seletor.value; seletor.innerHTML = ""; var raiz = retakesRaiz(); var casos = []; try { casos = fs.readdirSync(raiz).filter(function (item) { return fs.statSync(path.join(raiz, item)).isDirectory(); }).sort(); } catch (_) {} if (!casos.length) { seletor.appendChild(new Option("Nenhum caso encontrado", "")); retakesCasoAtual = null; retakesInfo("Execute o Scanner primeiro para gerar transcrições."); return; } casos.forEach(function (caso) { seletor.appendChild(new Option(caso, caso)); }); seletor.value = anterior && casos.indexOf(anterior) >= 0 ? anterior : casos[0]; retakesCarregarCaso(); } function retakesInfo(mensagem) { document.getElementById("retakesInfoCaso").textContent = mensagem; } function retakesArtefatosDeAudio(caso) { var pasta = path.join(retakesRaiz(), caso); try { return fs.readdirSync(pasta).filter(function (item) { return /-audio-faixa-\d+\.json$/.test(item); }).map(function (item) { return path.join(pasta, item); }).sort(); } catch (_) { return []; } } function retakesCarregarCaso() { var caso = document.getElementById("retakesCaso").value; retakesCasoAtual = caso || null; retakesGrupos = []; retakesRenderResultados(); if (!caso) { retakesInfo("Nenhum caso disponível."); return; } var artefatos = retakesArtefatosDeAudio(caso); var jaTem = fs.existsSync(path.join(retakesRaiz(), caso, "retakes")); retakesInfo("Caso: " + caso + " · " + artefatos.length + " faixa(s) de áudio com transcrição" + (jaTem ? " · já analisado" : " · não analisado")); document.getElementById("retakesStatusGeral").textContent = jaTem ? "Analisado" : "Não analisado"; if (jaTem) retakesCarregarResultadosSalvos(); } function retakesCarregarResultadosSalvos() { var arquivo = path.join(retakesRaiz(), retakesCasoAtual, "retakes", "retakes-" + retakesCasoAtual + ".json"); try { var dados = JSON.parse(fs.readFileSync(arquivo, "utf-8")); retakesGrupos = dados.grupos_de_retakes || []; retakesRenderResultados(); } catch (erro) { showToast("err", "Não foi possível ler os resultados: " + erro.message); } } function retakesFormatarTempo(segundos) { if (typeof segundos !== "number" || isNaN(segundos)) return "—"; var total = Math.max(0, Math.round(segundos)); var m = Math.floor(total / 60), s = total % 60; return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0"); } function retakesRotuloDoGrupo(grupo) { var tipo = { retake_confirmado: "Retake forte", possivel_retake: "Possível retake", repeticao_parcial: "Repetição parcial", repeticao_de_frase: "Repetição de frase", repeticao_de_inicio: "Repetição de início", continuidade_de_fala: "Continuidade de fala", sem_indicio_de_retake: "Sem indício" }[grupo.tipo] || grupo.tipo; var status = grupo.status_de_revisao === "confirmado" ? " · confirmado" : grupo.status_de_revisao === "nao_e_retake" ? " · rejeitado" : grupo.status_de_revisao === "ignorado" ? " · ignorado" : ""; return tipo + " · " + Math.round(grupo.confianca * 100) + "% de confiança" + status; } function retakesPassaNoFiltro(grupo) { var filtro = document.getElementById("retakesFiltro").value; if (filtro === "todos") return true; if (filtro === "fortes") return grupo.tipo === "retake_confirmado"; if (filtro === "possiveis") return grupo.tipo !== "retake_confirmado" && grupo.tipo !== "sem_indicio_de_retake"; return grupo.status_de_revisao === filtro; } function retakesRenderResultados() { var alvo = document.getElementById("retakesResultados"); alvo.innerHTML = ""; var visiveis = retakesGrupos.filter(retakesPassaNoFiltro); document.getElementById("retakesContagem").textContent = retakesGrupos.length ? retakesGrupos.length + " grupo(s)" : "—"; document.getElementById("retakesSemResultados").hidden = visiveis.length > 0; visiveis.forEach(function (grupo) { alvo.appendChild(retakesCartaoDoGrupo(grupo)); }); } function retakesCartaoDoGrupo(grupo) { var cartao = document.createElement("div"); cartao.className = "card"; cartao.setAttribute("role", "listitem"); var titulo = document.createElement("strong"); titulo.textContent = retakesRotuloDoGrupo(grupo); cartao.appendChild(titulo); grupo.tomadas.forEach(function (tomada, indice) { var linha = document.createElement("p"); linha.textContent = "Tentativa " + (indice + 1) + ": " + retakesFormatarTempo(tomada.inicio) + " – " + retakesFormatarTempo(tomada.fim) + " · " + (tomada.texto || ""); linha.style.margin = "2px 0"; cartao.appendChild(linha); }); if (grupo.evidencias && grupo.evidencias.length) { var motivos = document.createElement("p"); motivos.textContent = "Evidências: " + grupo.evidencias.map(function (e) { return e.descricao || e.tipo; }).join("; "); motivos.className = "field-help"; cartao.appendChild(motivos); } var acoes = document.createElement("div"); acoes.className = "button-row"; [["confirmar", "Confirmar"], ["rejeitar", "Não é retake"], ["manter", "Manter p/ revisão"], ["ignorar", "Ignorar"]].forEach(function (par) { var botao = document.createElement("button"); botao.className = "button button-ghost"; botao.type = "button"; botao.textContent = par[1]; botao.onclick = function () { retakesRevisar(grupo.id, par[0]); }; acoes.appendChild(botao); }); cartao.appendChild(acoes); return cartao; } function retakesAnalisar() { if (!retakesCasoAtual) { showToast("err", "Selecione um caso do Scanner."); return; } var artefatos = retakesArtefatosDeAudio(retakesCasoAtual); if (!artefatos.length) { showToast("err", "O caso não possui transcrição. Execute o Scanner com transcrição primeiro."); return; } if (retakesProcesso) { showToast("err", "Já existe uma análise em andamento."); return; } if (!document.getElementById("retakesReaproveitar").checked) { var pastaRetakes = path.join(retakesRaiz(), retakesCasoAtual, "retakes"); try { if (fs.existsSync(pastaRetakes)) fs.rmSync(pastaRetakes, { recursive: true }); } catch (_) {} } var pedido = { acao: "analisar", video_id: retakesCasoAtual, artefatos_de_audio: artefatos, configuracao: { criterio: document.getElementById("retakesCriterio").value, usar_analise_visual: document.getElementById("retakesUsarVisual").checked } }; var entrada = path.join(os.tmpdir(), "pedido-retakes-" + Date.now() + ".json"); fs.writeFileSync(entrada, JSON.stringify(pedido, null, 2), "utf-8"); setBusy("btnRetakesAnalisar", true); taskStart("Retakes", "Preparando análise", null); document.getElementById("retakesProgressoCard").hidden = false; document.getElementById("retakesProgressoEtapa").textContent = "Analisando retakes…"; var child = childProcess.spawn(SCANNER_PYTHON_BIN, [RETAKES_ENGINE_SCRIPT, entrada, path.join(retakesRaiz(), retakesCasoAtual)], { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine", stdio: ["ignore", "pipe", "pipe"] }); 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"; var TRILHAS_ENGINE_SCRIPT = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/engine/analisar_trilhas.py"; var TRILHAS_BANCO_PATH = ""; var TRILHAS_MODELOS_PATH = ""; var TRILHAS_CONFIG_KEY = "mcp_trilhas_config_v1"; // `path` e `os` são carregados mais abaixo, junto com o runtime Node do CEP. // Calcular isso aqui interrompia todo o script antes da inicialização do painel. function scannerOutputRoot() { return path.join(os.homedir(), ".premiere-mcp", "scanner"); } function scannerNomeSeguro(nome) { return String(nome || "timeline").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "timeline"; } function scannerCarregarRelatorioExistente() { scannerRelatorioExistente = null; if (!scannerTimeline || !scannerTimeline.name) return; var caso = scannerNomeSeguro(scannerTimeline.name); var caminho = path.join(scannerOutputRoot(), caso, caso + "-resultado.json"); try { if (!fs.existsSync(caminho)) return; var relatorio = JSON.parse(fs.readFileSync(caminho, "utf-8")); if (relatorio.artefatos && relatorio.artefatos.audio && relatorio.artefatos.audio.length) { scannerRelatorioExistente = caminho; scannerReportPath = caminho; } } catch (_) { scannerRelatorioExistente = null; } } function scannerListarAnalisesExistentes() { var seletor = document.getElementById("scannerExistingAnalysis"); var status = document.getElementById("scannerExistingAnalysisStatus"); var info = document.getElementById("scannerExistingAnalysisInfo"); if (!seletor) return; scannerAnalisesExistentes = []; try { fs.readdirSync(scannerOutputRoot()).forEach(function (nomeDaPasta) { var caminho = path.join(scannerOutputRoot(), nomeDaPasta, nomeDaPasta + "-resultado.json"); if (!fs.existsSync(caminho)) return; try { var dados = JSON.parse(fs.readFileSync(caminho, "utf-8")); var estado = silenceConsultarStatusNoBanco((dados.sequencia || {}).id); if (estado) scannerAnalisesExistentes.push({ caminho: caminho, dados: dados, estado: estado }); } catch (_) {} }); } catch (_) {} scannerAnalisesExistentes.sort(function (a, b) { try { return fs.statSync(b.caminho).mtimeMs - fs.statSync(a.caminho).mtimeMs; } catch (_) { return 0; } }); seletor.innerHTML = ""; if (!scannerAnalisesExistentes.length) { seletor.innerHTML = ""; status.textContent = "Nenhuma análise"; info.textContent = "Execute o Scanner para criar a primeira análise persistida."; return; } scannerAnalisesExistentes.forEach(function (analise, indice) { var audio = analise.estado.audio || {}; var option = document.createElement("option"); option.value = String(indice); option.textContent = (analise.dados.sequencia || {}).nome + " · " + audio.total_falas + " falas"; seletor.appendChild(option); }); seletor.selectedIndex = 0; scannerSelecionarAnaliseExistente(); } function scannerSelecionarAnaliseExistente() { var seletor = document.getElementById("scannerExistingAnalysis"); var analise = scannerAnalisesExistentes[Number(seletor && seletor.value)]; var status = document.getElementById("scannerExistingAnalysisStatus"); var info = document.getElementById("scannerExistingAnalysisInfo"); if (!analise) return; scannerRelatorioExistente = analise.caminho; scannerReportPath = analise.caminho; var audio = analise.estado.audio || {}; status.textContent = (audio.metricas_de_voz_disponiveis ? "Métricas prontas" : "Sem métricas") + " · " + (audio.diarizacao_disponivel ? "Diarização pronta" : "Sem diarização"); info.textContent = "Transcrição: " + (audio.transcricao_disponivel ? "pronta" : "não encontrada") + " · " + audio.total_falas + " falas · " + (audio.falantes || []).length + " falante(s)."; scannerAtualizarComplementos(); } function scannerAtualizarComplementos() { var relatorio = scannerRelatorioExistente; var arquivoDiarizacao = document.getElementById("btnScannerDiarization"); var arquivoMetricas = document.getElementById("btnScannerVoiceFeatures"); var statusDiarizacao = document.getElementById("scannerDiarizationStatus"); var statusMetricas = document.getElementById("scannerVoiceFeaturesStatus"); var botaoProcessar = document.getElementById("btnScannerProcess"); if (!relatorio) { if (arquivoDiarizacao) arquivoDiarizacao.disabled = true; if (arquivoMetricas) arquivoMetricas.disabled = true; if (statusDiarizacao) statusDiarizacao.textContent = "Ainda não processado"; if (statusMetricas) statusMetricas.textContent = "Ainda não processado"; return; } var dados = {}; try { dados = JSON.parse(fs.readFileSync(relatorio, "utf-8")); } catch (_) {} var transcricao = dados.perfil && dados.perfil.transcricao || {}; var audio = dados.artefatos && dados.artefatos.audio || []; var metricas = !!(dados.perfil && dados.perfil.metricas_de_fala) || !!transcricao.metricas_de_fala; var diarizacao = !!transcricao.diarizacao; if (arquivoDiarizacao) arquivoDiarizacao.disabled = true; if (arquivoMetricas) arquivoMetricas.disabled = true; if (statusDiarizacao) { statusDiarizacao.textContent = diarizacao ? "Disponível" : "Não realizada"; statusDiarizacao.dataset.state = diarizacao ? "done" : "pending"; } if (statusMetricas) { statusMetricas.textContent = metricas ? "Disponíveis" : "Não realizadas"; statusMetricas.dataset.state = metricas ? "done" : "pending"; } if (botaoProcessar) { botaoProcessar.disabled = true; botaoProcessar.textContent = "Já processado"; } } function scannerSelectedTracks(kind) { return Array.prototype.slice.call(document.querySelectorAll("[data-scanner-track='" + kind + "']:checked")).map(function (item) { return Number(item.value); }).sort(function (a, b) { return a - b; }); } function scannerRenderTracks(kind, tracks, selected) { if (kind === "audio" && !selected.length) selected = tracks.map(function (track) { return track.index; }); var target = document.getElementById(kind === "video" ? "scannerVideoTracks" : "scannerAudioTracks"); target.innerHTML = ""; if (!tracks.length) { target.textContent = "Nenhuma faixa encontrada."; return; } tracks.forEach(function (track) { var label = document.createElement("label"); label.className = "checkbox-field"; var input = document.createElement("input"); input.type = "checkbox"; input.setAttribute("data-scanner-track", kind); input.value = track.index; input.checked = selected.indexOf(track.index) >= 0; label.appendChild(input); label.appendChild(document.createTextNode(" " + track.name + " · " + track.numClips + " clipe(s)")); target.appendChild(label); }); } function scannerUpdateIntervalLabel() { var valor = Number(document.getElementById("scannerInterval").value); var texto = valor === 1 ? "1 s/quadro" : valor.toFixed(1).replace(/\.0$/, "") + " s/quadro"; document.getElementById("scannerIntervalValue").textContent = texto; } function scannerModoAtual() { var selecionado = document.querySelector('input[name="scannerMode"]:checked'); return selecionado ? selecionado.value : "som"; } function scannerAtualizarModo() { var modo = scannerModoAtual(); var imagem = document.getElementById("scannerImageOptions"); var som = document.getElementById("scannerAudioOptions"); var ajuda = document.getElementById("scannerModeHelp"); var botao = document.getElementById("btnScannerProcess"); if (imagem) imagem.hidden = modo === "som"; if (som) som.hidden = modo === "imagem"; if (ajuda) ajuda.textContent = modo === "som" ? "O Scanner vai ignorar completamente a análise de frames e processar apenas as faixas de áudio." : modo === "imagem" ? "O Scanner vai ignorar a transcrição e processar apenas frames, cenas e evidências visuais." : "O Scanner vai executar os dois fluxos e manter os resultados separados no relatório."; if (botao) botao.textContent = modo === "som" ? "Processar áudio" : modo === "imagem" ? "Analisar imagem" : "Processar áudio + imagem"; } function scannerProfileFromScreen() { var enabled = Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]:checked")).map(function (item) { return item.getAttribute("data-scanner-resource"); }); function include(source, extras) { if (enabled.indexOf(source) >= 0) extras.forEach(function (item) { if (enabled.indexOf(item) < 0) enabled.push(item); }); } include("faces", ["qualidade_facial"]); include("pose", ["maos"]); include("ocr", ["codigos"]); include("estetica", ["horizonte", "retangulos", "contornos"]); var modelo = document.getElementById("scannerTranscribeModel").value; var opcao = document.getElementById("scannerTranscribeModel").selectedOptions[0]; return { versao: 1, modo_de_analise: scannerModoAtual(), metricas_de_fala: document.getElementById("scannerVoiceFeatures").checked, intervalo_em_segundos: Number(document.getElementById("scannerInterval").value), resolucao_maxima: Number(document.getElementById("scannerResolution").value), faixas_de_video: scannerSelectedTracks("video"), faixas_de_audio: scannerSelectedTracks("audio"), recursos_vision: SCANNER_ALL_RESOURCES.filter(function (item) { return enabled.indexOf(item) >= 0; }), detectar_cenas: document.getElementById("scannerScenes").checked, analisar_continuidade: document.getElementById("scannerContinuity").checked, preparar_reenquadramento: document.getElementById("scannerReframe").checked, interpretar_cena: document.getElementById("scannerInterpret").checked, transcricao: { modelo: modelo, caminho_modelo: opcao && opcao.dataset.path || "", idioma: document.getElementById("scannerTranscribeLanguage").value, diarizacao: document.getElementById("scannerDiarization").checked, usar_hugging_face: !!loadHfToken() } }; } function saveScannerProfile() { try { var profile = scannerProfileFromScreen(); localStorage.setItem(SCANNER_PROFILE_KEY, JSON.stringify(profile)); document.getElementById("scannerProfileStatus").textContent = "Perfil salvo: " + profile.intervalo_em_segundos + " s por quadro; " + profile.faixas_de_video.length + " faixa(s) de vídeo."; showToast("ok", "Perfil do Scanner salvo."); } catch (error) { showToast("err", error.message || String(error)); } } function loadScannerProfile() { var saved; try { saved = JSON.parse(localStorage.getItem(SCANNER_PROFILE_KEY) || "null"); } catch (_) { saved = null; } if (!saved) { scannerUpdateIntervalLabel(); scannerAtualizarModo(); return; } var modo = saved.modo_de_analise || "som"; var seletorDeModo = document.querySelector('input[name="scannerMode"][value="' + modo + '"]'); if (seletorDeModo) seletorDeModo.checked = true; document.getElementById("scannerInterval").value = String(saved.intervalo_em_segundos || 1); scannerUpdateIntervalLabel(); document.getElementById("scannerResolution").value = String(saved.resolucao_maxima != null ? saved.resolucao_maxima : 1080); if (scannerTimeline) { scannerRenderTracks("video", scannerTimeline.videoTracks, saved.faixas_de_video || []); scannerRenderTracks("audio", scannerTimeline.audioTracks, saved.faixas_de_audio || []); } var resources = saved.recursos_vision || SCANNER_ALL_RESOURCES; Array.prototype.slice.call(document.querySelectorAll("[data-scanner-resource]")).forEach(function (item) { item.checked = resources.indexOf(item.getAttribute("data-scanner-resource")) >= 0; }); document.getElementById("scannerScenes").checked = saved.detectar_cenas !== false; document.getElementById("scannerContinuity").checked = saved.analisar_continuidade !== false; document.getElementById("scannerReframe").checked = !!saved.preparar_reenquadramento; document.getElementById("scannerInterpret").checked = saved.interpretar_cena !== false; document.getElementById("scannerVoiceFeatures").checked = !!saved.metricas_de_fala; if (saved.transcricao) { document.getElementById("scannerTranscribeModel").value = saved.transcricao.modelo || ""; document.getElementById("scannerTranscribeLanguage").value = saved.transcricao.idioma || loadLanguage(); document.getElementById("scannerDiarization").checked = !!saved.transcricao.diarizacao && !!loadHfToken(); } scannerAtualizarModo(); } function renderScannerTranscriptionOptions() { var modelos=document.getElementById("scannerTranscribeModel"), idiomas=document.getElementById("scannerTranscribeLanguage"); if (!modelos || !idiomas) return; var atual=modelos.value; modelos.innerHTML=""; var disponiveis=discoverPortugueseModelsOnMerongo(); if (!disponiveis.length) { var vazio=document.createElement("option"); vazio.value=""; vazio.textContent="Nenhum modelo com português encontrado no Merongo"; modelos.appendChild(vazio); } else { disponiveis.forEach(function(item){var option=document.createElement("option");option.value=item.nome;option.dataset.path=item.caminho;option.textContent=item.nome+" · "+formatMB(item.tamanho);modelos.appendChild(option);}); if (atual) modelos.value=atual; } idiomas.innerHTML=""; LANGUAGE_CATALOG.forEach(function(item){var option=document.createElement("option");option.value=item.value;option.textContent=item.label;idiomas.appendChild(option);}); idiomas.value=loadLanguage(); document.getElementById("scannerModelCount").textContent=disponiveis.length+" instalado(s)"; renderScannerHuggingFaceStatus(); } function renderScannerHuggingFaceStatus() { var status = document.getElementById("scannerHfStatus"); var text = document.getElementById("scannerHfStatusText"); var diarization = document.getElementById("scannerDiarization"); if (!status || !text || !diarization) return; var available = !!loadHfToken(); status.setAttribute("data-state", available ? "valid" : "unset"); text.textContent = available ? "Hugging Face disponível para identificar locutores" : "Configure o token do Hugging Face para identificar locutores"; diarization.disabled = !available; if (!available) diarization.checked = false; } var MERONGO_MODEL_ROOTS = ["/Volumes/Merongo/SISTEMAS/WHISPERX/whisper/models", "/Volumes/Merongo/SISTEMAS/MODELOSIA/hf-cache", "/Volumes/Merongo/Applications/WhisperX/huggingface/hub", "/Volumes/Merongo/Applications/Trasncritor", "/Volumes/Merongo/SISTEMAS/AUX"]; function discoverPortugueseModelsOnMerongo() { var encontrados = {}, roots = [effectiveModelsPath()].concat(MERONGO_MODEL_ROOTS); function walk(dir, profundidade) { if (profundidade > 5) return; var entries; try { entries=fs.readdirSync(dir,{withFileTypes:true}); } catch (_) { return; } entries.forEach(function(entry) { if (!entry.isDirectory() || entry.name === ".locks" || entry.name === ".git") return; var full=path.join(dir,entry.name); if (fs.existsSync(path.join(full,"model.bin")) && modelSupportsPortuguese(full)) { var nome=modelNameFromPath(full); if (!encontrados[nome]) encontrados[nome]={nome:nome,caminho:full,tamanho:folderSizeMB(full)}; return; } walk(full,profundidade+1); }); } roots.filter(function(root,index){return root && roots.indexOf(root)===index;}).forEach(function(root){walk(root,0);}); return Object.keys(encontrados).sort().map(function(nome){return encontrados[nome];}); } function modelSupportsPortuguese(folder) { try { var tokenizer=path.join(folder,"tokenizer.json"); if (!fs.existsSync(tokenizer)) return false; return fs.readFileSync(tokenizer,"utf-8").indexOf("<|pt|>") >= 0; } catch (_) { return false; } } function modelNameFromPath(folder) { var value=String(folder).replace(/.*faster-whisper-/i,"").replace(/[\\/].*$/,""); return value || path.basename(folder); } function scannerDetectTracks() { setBusy("btnScannerDetectTracks", true); document.getElementById("scannerTracksHelp").textContent = "Lendo as faixas da sequência ativa…"; cs.evalScript("(function(){try{var s=app.project.activeSequence;if(!s)return JSON.stringify({ok:false,error:'Nenhuma sequência ativa.'});function tracks(c,a){for(var i=0;i--enable-nodejs, then fully quit and reopen Premiere Pro." ); } var fs = nodeRequire("fs"); var path = nodeRequire("path"); var os = nodeRequire("os"); var https = nodeRequire("https"); var childProcess = nodeRequire("child_process"); TRILHAS_BANCO_PATH = path.join(os.homedir(), ".premiere-mcp", "trilhas.db"); TRILHAS_MODELOS_PATH = path.join(os.homedir(), ".premiere-mcp", "modelos-essentia"); function defaultBridgeDirectory() { try { var nodeProcess = nodeRequire("process"); var configured = nodeProcess && nodeProcess.env && nodeProcess.env.PREMIERE_TEMP_DIR; if (typeof configured === "string" && configured.trim()) return configured.trim(); } catch (e) { // The panel still has a safe OS temporary-directory fallback. } return path.join(os.tmpdir(), "premiere-mcp-bridge"); } tempDir = defaultBridgeDirectory(); var latestUpdate = null; var UPDATE_STATUS_STORAGE_KEY = "mcp_bridge_desktop_update_status_path"; var MAX_UPDATE_RESPONSE_BYTES = 64 * 1024; function getPerUserGlobalInstall() { try { var nodeProcess = nodeRequire("process"); var appData = nodeProcess && nodeProcess.env && nodeProcess.env.APPDATA; if (typeof appData !== "string" || !appData.trim()) return null; var npmDirectory = path.resolve(appData, "npm"); var commandPath = path.resolve(npmDirectory, "premiere-pro-mcp.cmd"); var packagePath = path.resolve(npmDirectory, "node_modules", "premiere-pro-mcp", "package.json"); var relative = path.relative(npmDirectory, commandPath); var packageRelative = path.relative(npmDirectory, packagePath); if ( !relative || !packageRelative || relative.indexOf(".." + path.sep) === 0 || packageRelative.indexOf(".." + path.sep) === 0 || path.isAbsolute(relative) || path.isAbsolute(packageRelative) || !fs.existsSync(commandPath) || !fs.existsSync(packagePath) ) return null; var packageMetadata = JSON.parse(fs.readFileSync(packagePath, "utf-8")); var serverVersion = MCPBridgeUpdater.normalizeVersion(packageMetadata && packageMetadata.version); if (!serverVersion) return null; return { commandPath: commandPath, serverVersion: serverVersion }; } catch (e) { return null; } } function getPerUserGlobalCommand() { var install = getPerUserGlobalInstall(); return install ? install.commandPath : null; } function saveUpdateStatusPath(statusPath) { try { localStorage.setItem(UPDATE_STATUS_STORAGE_KEY, statusPath); } catch (e) {} } function readScheduledUpdateStatus() { var statusPath = ""; try { statusPath = localStorage.getItem(UPDATE_STATUS_STORAGE_KEY) || ""; } catch (e) { return null; } if (!statusPath || !path.isAbsolute(statusPath) || !fs.existsSync(statusPath)) return null; try { var status = JSON.parse(fs.readFileSync(statusPath, "utf-8")); var validStates = ["waiting_for_premiere", "updating", "complete", "failed"]; if ( !status || status.schemaVersion !== "premiere-pro-mcp.desktop-update.v1" || validStates.indexOf(status.state) === -1 ) return null; return status; } catch (e) { return null; } } function ensureDir(dir) { try { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } } catch (e) { log("Error creating dir: " + e.message, "err"); } } function listCommandFiles() { try { if (!fs.existsSync(tempDir)) return []; var files = fs.readdirSync(tempDir); return files .filter(function (f) { return f.indexOf("cmd_") === 0 && f.slice(-4) === ".jsx"; }) .sort(); // process in order } catch (e) { return []; } } function readFile(filePath) { try { return fs.readFileSync(filePath, "utf-8"); } catch (e) { return null; } } function writeFile(filePath, content) { try { fs.writeFileSync(filePath, content, "utf-8"); return true; } catch (e) { log("Error writing " + filePath + ": " + e.message, "err"); return false; } } // Publish responses atomically so the MCP process never sees a partially-written // JSON file. The staging suffix is not a response filename the server will read. function writeResponseFile(filePath, content) { var stagedPath = filePath + ".staged"; try { fs.writeFileSync(stagedPath, content, "utf-8"); fs.renameSync(stagedPath, filePath); return true; } catch (e) { deleteFile(stagedPath); log("Error publishing " + filePath + ": " + e.message, "err"); return false; } } function deleteFile(filePath) { try { if (fs.existsSync(filePath)) fs.unlinkSync(filePath); } catch (e) {} } // The heartbeat carries only protocol state. It is published by rename so a // server never observes partial JSON, and an older server can ignore it. function writeBridgeHeartbeat() { if (!tempDir) return; var heartbeatPath = path.join(tempDir, "bridge-heartbeat.json"); var stagedPath = heartbeatPath + "." + ENGINE_ID + ".staged"; try { fs.writeFileSync(stagedPath, JSON.stringify({ protocolVersion: 1, state: bridgeRunning ? "running" : "waiting" }), "utf-8"); fs.renameSync(stagedPath, heartbeatPath); } catch (e) { deleteFile(stagedPath); } } function startBridgeHeartbeat() { if (heartbeatInterval) clearInterval(heartbeatInterval); writeBridgeHeartbeat(); heartbeatInterval = setInterval(writeBridgeHeartbeat, HEARTBEAT_MS); } function stopBridgeHeartbeat() { if (heartbeatInterval) clearInterval(heartbeatInterval); heartbeatInterval = null; // Keep the last heartbeat in place. Its age lets newer servers diagnose a // stopped connector, while concurrent visible/headless panels stay isolated. } // ---- Script Execution ---- function executeScript(script, callback) { // Script is already wrapped in an IIFE by the MCP server's buildScript(), // so we pass it directly to avoid double-wrapping. cs.evalScript(script, function (result) { callback(result); }); } // ---- Command Processing ---- function processCommands() { if (commandInFlight) return; var cmdFiles = listCommandFiles(); // Premiere's scripting engine is stateful. Starting every discovered command // at once lets overlapping edits race each other and overload the host. The // atomic claim below still prevents duplicate work across the visible and // headless panels, while this panel dispatches strictly one command at a time. if (cmdFiles.length > 0) processOneCommand(cmdFiles[0]); } // Both the visible panel and the headless auto-start instance run this file. // A rename is atomic on the same volume, so whichever engine renames first owns // the command; the loser's rename throws and it skips the file. var ENGINE_ID = Math.random().toString(36).slice(2, 8); var commandInFlight = false; function processOneCommand(cmdFileName) { var cmdFilePath = path.join(tempDir, cmdFileName); var claimPath = cmdFilePath + "." + ENGINE_ID + ".claimed"; try { fs.renameSync(cmdFilePath, claimPath); } catch (e) { return; // another engine claimed this command } var script = readFile(claimPath); deleteFile(claimPath); if (!script) { log("Failed to read: " + cmdFileName, "err"); return; } commandInFlight = true; // Derive response filename: cmd_12345.jsx -> res_12345.json var id = cmdFileName.replace("cmd_", "").replace(".jsx", ""); var resFilePath = path.join(tempDir, "res_" + id + ".json"); log("Executing: " + cmdFileName + " (" + script.length + " chars)", "cmd"); // While evalScript is in flight, heartbeat a busy file so the MCP server can // tell "script still running (modal dialog?)" apart from "plugin not running". // Only starts after 2s, so fast commands never touch the extra file. var busyFilePath = path.join(tempDir, "busy_" + id + ".json"); var startedAt = new Date().getTime(); var busyTimer = setInterval(function () { writeFile(busyFilePath, '{"id":"' + id + '","elapsedMs":' + (new Date().getTime() - startedAt) + "}"); }, 2000); executeScript(script, function (result) { clearInterval(busyTimer); deleteFile(busyFilePath); commandCount++; document.getElementById("cmdCount").textContent = commandCount; var response; try { // ExtendScript returns a string; try to parse it as JSON if (result && result !== "undefined" && result !== "null") { // Check if it's already valid JSON var parsed = JSON.parse(result); response = JSON.stringify(parsed); log("Result: OK", "ok"); } else { // An empty result means evalScript gave us nothing back. That is a bridge // failure, not a successful command with no data — reporting it as "OK" is // what made this so hard to diagnose. Say so. response = JSON.stringify({ success: false, error: "The bridge received an empty result from evalScript (got " + (typeof result) + "). The script may not have run. If every command does this, the CEP panel is stale — " + "close and reopen it (a reload is not enough), or reinstall the extension.", }); log("Result: EMPTY — evalScript returned nothing (see response file)", "err"); } } catch (e) { // If result isn't JSON, wrap it if (result && result.indexOf("Error") === 0) { response = JSON.stringify({ success: false, error: result }); log("Result: " + result, "err"); } else { response = JSON.stringify({ success: true, data: result }); log("Result: OK (raw)", "ok"); } } writeResponseFile(resFilePath, response); commandInFlight = false; // Continue without waiting for the next poll interval, preserving FIFO // ordering while minimizing queue handoff latency. if (bridgeRunning) processCommands(); }); } // ---- Bridge Control ---- function startBridge() { tempDir = document.getElementById("tempDir").value.trim(); if (!tempDir) { log("Please set a temp directory", "err"); document.getElementById("tempDir").focus(); return; } ensureDir(tempDir); bridgeRunning = true; startBridgeHeartbeat(); setStatus("waiting", "Connector running"); log("Connector started and ready for safe checks.", "ok"); document.getElementById("btnStart").disabled = true; document.getElementById("btnStop").disabled = false; refreshConnectionCenter(); pollInterval = setInterval(function () { if (bridgeRunning) processCommands(); }, POLL_MS); } function stopBridge() { bridgeRunning = false; writeBridgeHeartbeat(); stopBridgeHeartbeat(); if (pollInterval) clearInterval(pollInterval); pollInterval = null; setStatus("", "Stopped"); refreshConnectionCenter(); log("Bridge stopped"); document.getElementById("btnStart").disabled = false; document.getElementById("btnStop").disabled = true; } function saveTempDir() { tempDir = document.getElementById("tempDir").value.trim(); log("Temp directory saved: " + tempDir); showToast("ok", "Pasta da ponte salva."); // Persist via localStorage try { localStorage.setItem("mcp_bridge_temp_dir", tempDir); } catch (e) {} } // ---- Connector Updates ---- function setUpdateUI(title, detail, buttonText, disabled) { document.getElementById("updateTitle").textContent = title; document.getElementById("updateDetail").textContent = detail; var button = document.getElementById("btnUpdate"); button.textContent = buttonText; button.disabled = !!disabled; } function updateInstructionUrl() { return MCPBridgeUpdater.RELEASES_URL; } function openTrustedUpdateInstructions() { var url = updateInstructionUrl(); if (!MCPBridgeUpdater.isTrustedDownloadUrl(url)) { showUpdateCheckError("The update instructions link was not trusted."); return; } try { var childProcess = nodeRequire("child_process"); var command = os.platform() === "win32" ? ["cmd.exe", ["/d", "/s", "/c", "start", "", url]] : ["open", [url]]; var child = childProcess.spawn(command[0], command[1], { detached: true, stdio: "ignore", }); child.unref(); } catch (e) { showUpdateCheckError("Could not open the update instructions. Try again."); } } function restoreScheduledUpdateStatus() { var status = readScheduledUpdateStatus(); if (!status) return false; if (status.state === "complete") { setUpdateUI( "Update complete", "Restart your MCP client, then use Verify Premiere connection before editing.", "Check again", false ); return true; } if (status.state === "failed") { setUpdateUI( "Update needs attention", "Nothing was changed in your projects. Check the update command or retry after Premiere closes.", "Check again", false ); return true; } setUpdateUI( "Update scheduled", status.state === "updating" ? "The global MCP server and connector are being updated. Keep Premiere closed." : "Quit Premiere Pro. The updater will begin after it fully closes.", "Scheduled", true ); return true; } function checkForUpdates() { latestUpdate = null; var globalInstall = os.platform() === "win32" ? getPerUserGlobalInstall() : null; var responseTooLarge = false; setUpdateUI( "Version " + MCPBridgeUpdater.CURRENT_VERSION, "Checking for updates…", "Checking…", true ); var request = https.get( MCPBridgeUpdater.LATEST_PACKAGE_API, { headers: { Accept: "application/vnd.npm.install-v1+json", "User-Agent": "premiere-pro-mcp-connector/" + MCPBridgeUpdater.CURRENT_VERSION, }, }, function (response) { var body = ""; response.setEncoding("utf8"); response.on("data", function (chunk) { if (body.length + chunk.length > MAX_UPDATE_RESPONSE_BYTES) { responseTooLarge = true; request.destroy(new Error("npm registry update record was unexpectedly large.")); return; } body += chunk; }); response.on("end", function () { if (responseTooLarge) return; if (response.statusCode !== 200) { showUpdateCheckError("Could not check npm (HTTP " + response.statusCode + ")."); return; } try { var update = MCPBridgeUpdater.updateStateFromPackageRecord( MCPBridgeUpdater.CURRENT_VERSION, JSON.parse(body) ); var serverUpdateAvailable = Boolean( globalInstall && MCPBridgeUpdater.compareVersions(update.latestVersion, globalInstall.serverVersion) > 0 ); var needsUpdate = update.updateAvailable || serverUpdateAvailable; if (needsUpdate) { latestUpdate = { version: update.latestVersion, }; if (os.platform() === "win32" && globalInstall) { var versionSummary = "Server " + globalInstall.serverVersion + ", connector " + MCPBridgeUpdater.CURRENT_VERSION + ". "; setUpdateUI( "Version " + update.latestVersion + " is available", versionSummary + "Update both together after you close Premiere.", "Update after quit", false ); } else if (os.platform() === "win32") { setUpdateUI( "Version " + update.latestVersion + " is available", "A global npm install was not found. This panel will not modify a source checkout.", "Open instructions", false ); } else { setUpdateUI( "Version " + update.latestVersion + " is available", "Open the matching release, then update your local server using the documented install path.", "Open instructions", false ); } } else { var currentDetail = globalInstall ? "Server " + globalInstall.serverVersion + " and connector " + MCPBridgeUpdater.CURRENT_VERSION + " are current." : "Your connector release is current. This check does not alter your projects or MCP client configuration."; setUpdateUI( "Version " + MCPBridgeUpdater.CURRENT_VERSION, currentDetail, "Check again", false ); } } catch (e) { showUpdateCheckError("npm returned an unreadable package record."); } }); } ); request.setTimeout(10000, function () { request.destroy(new Error("Update check timed out")); }); request.on("error", function () { showUpdateCheckError( responseTooLarge ? "npm returned an unexpectedly large package record." : "Unable to check while offline." ); }); } function showUpdateCheckError(message) { setUpdateUI( "Version " + MCPBridgeUpdater.CURRENT_VERSION, message, "Check again", false ); } function handleUpdateClick() { if (!latestUpdate) { checkForUpdates(); return; } if (os.platform() !== "win32") { openTrustedUpdateInstructions(); return; } var cliPath = getPerUserGlobalCommand(); if (!cliPath) { openTrustedUpdateInstructions(); return; } var confirmation = "Update Premiere MCP to " + latestUpdate.version + " after Premiere Pro fully closes?\n\n" + "This updates only the per-user global MCP server and its connector. " + "It does not change your projects or MCP client configuration, and it will not force Premiere to close."; if (typeof window.confirm === "function" && !window.confirm(confirmation)) return; try { var childProcess = nodeRequire("child_process"); var nodeCrypto = nodeRequire("crypto"); var scheduled = MCPBridgeUpdater.scheduleWindowsGlobalUpdate({ cliPath: cliPath, runtime: { fs: fs, path: path, os: os, childProcess: childProcess, crypto: nodeCrypto, }, }); saveUpdateStatusPath(scheduled.statusPath); setUpdateUI( "Update scheduled", "Quit Premiere Pro. The updater will refresh the global server and connector after it fully closes.", "Scheduled", true ); } catch (e) { showUpdateCheckError("Could not schedule the local update. No files were changed."); } } // ---- Silence Cut tab ---- // Fixed local machine paths for the Whisper pipeline (this rig's setup). var SILENCE_PYTHON_BIN = "/Volumes/Merongo/SISTEMAS/venvs/whisperx-transcricao/bin/python"; var SILENCE_TRANSCRIBE_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/transcribe_cli.py"; var DEFAULT_MODELS_PATH = "/Volumes/Merongo/SISTEMAS/WHISPERX/whisper/models"; var SILENCE_OUTPUT_ROOT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/output"; var SILENCE_DOWNLOAD_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/download_model_cli.py"; var SILENCE_DIARIZE_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/diarize_cli.py"; var VOICE_FEATURES_SCRIPT = "/Volumes/Merongo/SISTEMAS/WHISPERX/transcricao/voice_features_cli.py"; var EDITORIAL_ACTIONS_APPLY_SCRIPT_FALLBACK = "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code/scripts/apply-editorial-actions.mjs"; // Catálogo dos modelos compatíveis com faster-whisper e aceitos pelo pipeline // local. As variantes destilada e turbo também são suportadas pelo carregador, // embora tenham organizações diferentes no Hugging Face. var MODEL_CATALOG = [ { value: "tiny", label: "Tiny", approxMB: 75 }, { value: "base", label: "Base", approxMB: 145 }, { value: "small", label: "Small", approxMB: 483 }, { value: "medium", label: "Medium", approxMB: 1500 }, { value: "large-v1", label: "Large v1", approxMB: 3100 }, { value: "large-v2", label: "Large v2", approxMB: 3100 }, { value: "large-v3", label: "Large v3", approxMB: 3100 }, { value: "large-v3-turbo", label: "Large v3 Turbo", approxMB: 1620 }, { value: "distil-large-v3", label: "Distil Large v3", approxMB: 1500 }, ]; var DEFAULT_MODEL = "small"; function formatMB(mb) { if (mb >= 1000) return (mb / 1000).toFixed(1).replace(/\.0$/, "") + " GB"; return Math.round(mb) + " MB"; } function folderSizeMB(folder) { var total = 0; function walk(dir) { var entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (e) { return; } for (var i = 0; i < entries.length; i++) { var full = path.join(dir, entries[i].name); if (entries[i].isSymbolicLink()) continue; if (entries[i].isDirectory()) walk(full); else { try { total += fs.statSync(full).size; } catch (e) {} } } } walk(folder); return total / (1024 * 1024); } var LANGUAGE_CATALOG = [ { value: "pt", label: "Português" }, { value: "en", label: "English" }, { value: "es", label: "Español" }, ]; var DEFAULT_LANGUAGE = "pt"; var silenceState = { sequenceId: null, sequenceName: null, mediaPath: null, audioCachePath: null, fileBaseName: null, outputDir: null, transcriptPath: null, transcribeModel: null, cachedTranscriptPath: null, jsonOutputPath: null, tipoVideoId: null, tipoVideo: null, speakers: [], speakerTurns: [], }; function atualizarCheckDaAnalise(id, disponivel, textoDisponibilidade) { var item = document.getElementById(id); if (!item) return; item.setAttribute("data-state", disponivel ? "done" : "missing"); var estado = item.querySelector("em"); if (estado) estado.textContent = textoDisponibilidade || (disponivel ? "Carregado" : "Não disponível"); } function renderizarTiposDeVideoParaEdicao() { var seletor = document.getElementById("silenceTipoVideoSelect"); if (!seletor) return; var selecionado = silenceState.tipoVideoId || ""; seletor.innerHTML = ''; tiposVideoLer().filter(function (tipo) { return tipo.ativo !== false; }).forEach(function (tipo) { var opcao = document.createElement("option"); opcao.value = tipo.id; opcao.textContent = tipo.nome; opcao.selected = tipo.id === selecionado; seletor.appendChild(opcao); }); seletor.disabled = !silenceState.sequenceId; } function silenceSelecionarTipoVideo() { var seletor = document.getElementById("silenceTipoVideoSelect"); var tipo = tiposVideoLer().filter(function (item) { return item.id === seletor.value; })[0] || null; silenceState.tipoVideoId = tipo ? tipo.id : null; silenceState.tipoVideo = tipo; if (!tipo) { document.getElementById("silenceTipoVideoInfo").textContent = "Selecione um tipo cadastrado para continuar."; setStep(1, "ready", "Selecione um tipo"); setStep(2, "locked", "Aguardando plano"); document.getElementById("btnBuildPlan").disabled = true; return; } document.getElementById("silenceTipoVideoInfo").textContent = tipo.nome + " · instruções serão incluídas no JSON para a IA."; setStep(1, "done", "Selecionado"); setStep(2, "ready", "Pronto para concluir"); document.getElementById("btnBuildPlan").disabled = false; silenceLog("Tipo de vídeo selecionado: " + tipo.nome + ".", "ok"); } function tipoVideoResumoParaJson(tipo) { if (!tipo) return null; return { id: tipo.id, nome: tipo.nome, categoria: tipo.categoria, objetivo: { principal: tipo.objetivo, secundario: tipo.objetivo_secundario, publico: tipo.publico, mensagem: tipo.mensagem }, narrativa: { tipo: tipo.tipo_narrativa, foco: tipo.foco_narrativo, storytelling: tipo.storytelling, estrutura: tipo.estrutura }, emocao: { objetivo: tipo.emocao, intensidade: tipo.intensidade, tom: tipo.tom, construcao: tipo.construcao_emocao }, formato: { canais: tipo.canais, proporcao: tipo.proporcao, duracao_minima_segundos: tipo.duracao_minima, duracao_maxima_segundos: tipo.duracao_maxima_segundos, ritmo: tipo.ritmo }, prioridades: tipo.prioridades, elementos_de_edicao: { broll: tipo.usa_broll, legendas: tipo.usa_legendas, textos: tipo.usa_textos, graficos: tipo.usa_graficos, zoom: tipo.usa_zoom, cortes: tipo.cortes, orientacao_de_broll: tipo.broll, textos_e_legendas: tipo.texto }, audio: { musica: tipo.usa_musica, intensidade_da_musica: tipo.intensidade_musica, prioridade_da_voz: tipo.prioridade_voz, tratamento: tipo.som }, regras_de_edicao: tipo.diretriz, restricoes: tipo.restricoes, criterios_de_qualidade: tipo.qualidade }; } function silenceLocalizarUltimoRelatorioDoScanner() { var raiz = scannerOutputRoot(); var candidatos = []; try { fs.readdirSync(raiz).forEach(function (nomeDaPasta) { var pasta = path.join(raiz, nomeDaPasta); var relatorio = path.join(pasta, nomeDaPasta + "-resultado.json"); if (fs.existsSync(relatorio)) candidatos.push(relatorio); }); } catch (_) {} candidatos.sort(function (a, b) { try { return fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs; } catch (_) { return 0; } }); return candidatos[0] || null; } function silenceConverterRelatorioDoScannerParaTranscricao(caminhoDoRelatorio, dadosDoRelatorio) { var segmentos = []; (dadosDoRelatorio.artefatos && dadosDoRelatorio.artefatos.audio || []).forEach(function (nomeDoArquivo) { var caminhoDoAudio = path.join(path.dirname(caminhoDoRelatorio), nomeDoArquivo); if (!fs.existsSync(caminhoDoAudio)) return; var dadosDoAudio; try { dadosDoAudio = JSON.parse(fs.readFileSync(caminhoDoAudio, "utf-8")); } catch (_) { return; } (dadosDoAudio.segmentos_por_clipe || []).forEach(function (grupo) { (grupo.segmentos || []).forEach(function (segmento) { segmentos.push({ start: Number(segmento.inicio), end: Number(segmento.fim), inicio: Number(segmento.inicio), fim: Number(segmento.fim), text: segmento.texto || "", texto: segmento.texto || "", confidence: segmento.confianca, confianca: segmento.confianca, speaker: segmento.falante, falante: segmento.falante, caracteristicas_acusticas: segmento.caracteristicas_acusticas || {}, words: segmento.palavras || [], palavras: segmento.palavras || [], }); }); }); }); segmentos.sort(function (a, b) { return a.start - b.start; }); return { version: 1, segments: segmentos, origem: "scanner" }; } function silenceConsultarStatusNoBanco(videoId) { if (!videoId || !childProcess.spawnSync) return null; try { var resposta = childProcess.spawnSync( SCANNER_PYTHON_BIN, [ANALISE_STATUS_SCRIPT, videoId, "--banco", "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db"], { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", encoding: "utf-8", maxBuffer: 1024 * 1024 } ); if (resposta.status !== 0) return null; return JSON.parse(String(resposta.stdout || "{}")); } catch (_) { return null; } } function silenceConsultarPreviewDeFalantes(videoId) { if (!videoId || !childProcess.spawnSync) return null; try { var resposta = childProcess.spawnSync( SCANNER_PYTHON_BIN, [ANALISE_PREVIEW_SCRIPT, videoId, "--banco", "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/.jhonny/analises.db"], { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", encoding: "utf-8", maxBuffer: 4 * 1024 * 1024 } ); if (resposta.status !== 0) return null; var dados = JSON.parse(String(resposta.stdout || "{}")); return Array.isArray(dados.intervalos) ? dados.intervalos : null; } catch (_) { return null; } } function silenceLocalizarAudioParaFalantes(pasta, dadosDoRelatorio) { var encontrados = []; var nomesDeMidia = []; function adicionarNomeDeMidia(nome) { if (!nome) return; var base = path.parse(String(nome)).name; if (base && nomesDeMidia.indexOf(base) < 0) nomesDeMidia.push(base); } adicionarNomeDeMidia(silenceState.mediaPath); var artefatosDeAudio = dadosDoRelatorio && dadosDoRelatorio.artefatos && dadosDoRelatorio.artefatos.audio || []; artefatosDeAudio.forEach(function (nomeDoArtefato) { try { var caminhoDoArtefato = path.join(pasta, nomeDoArtefato); var dadosDoAudio = JSON.parse(fs.readFileSync(caminhoDoArtefato, "utf-8")); (dadosDoAudio.faixa && dadosDoAudio.faixa.clipes || []).forEach(function (clipe) { adicionarNomeDeMidia(clipe.nome || clipe.arquivo || clipe.sourceFile); }); } catch (_) {} }); function visitar(caminho) { if (encontrados.length) return; try { fs.readdirSync(caminho).forEach(function (nome) { if (encontrados.length) return; var item = path.join(caminho, nome); var estatisticas = fs.statSync(item); if (estatisticas.isDirectory()) visitar(item); else if (/\.wav$/i.test(nome)) encontrados.push(item); }); } catch (_) {} } visitar(pasta); if (encontrados.length) return encontrados[0]; for (var indice = 0; indice < nomesDeMidia.length; indice++) { var pastaDaTranscricao = path.join(SILENCE_OUTPUT_ROOT, nomesDeMidia[indice]); var caminhoDoAudio = path.join(pastaDaTranscricao, nomesDeMidia[indice] + ".wav"); if (fs.existsSync(caminhoDoAudio)) return caminhoDoAudio; } return null; } function silenceLocalizarMidiaNoRelatorio(caminhoDoRelatorio, dadosDoRelatorio) { var arquivosDeAudio = dadosDoRelatorio.artefatos && dadosDoRelatorio.artefatos.audio || []; for (var indice = 0; indice < arquivosDeAudio.length; indice++) { try { var dadosDoAudio = JSON.parse(fs.readFileSync(path.join(path.dirname(caminhoDoRelatorio), arquivosDeAudio[indice]), "utf-8")); var clipes = dadosDoAudio.faixa && dadosDoAudio.faixa.clipes || []; for (var indiceDoClipe = 0; indiceDoClipe < clipes.length; indiceDoClipe++) { var arquivoDoClipe = clipes[indiceDoClipe].sourceFile || clipes[indiceDoClipe].arquivo || clipes[indiceDoClipe].arquivo_original; if (arquivoDoClipe) return arquivoDoClipe; } } catch (_) {} } return null; } function silenceLoadScannerAnalysis() { var status = document.getElementById("scannerAnalysisStatus"); var info = document.getElementById("scannerAnalysisInfo"); setBusy("btnLoadScannerAnalysis", true); setBusy("btnRefinarLoad", true); var caminhoDoRelatorio = silenceLocalizarUltimoRelatorioDoScanner(); if (!caminhoDoRelatorio) { setBusy("btnLoadScannerAnalysis", false); setBusy("btnRefinarLoad", false); status.textContent = "Não encontrada"; info.textContent = "Execute o Scanner primeiro para criar uma análise reutilizável."; showToast("err", info.textContent); return; } try { var dadosDoRelatorio = JSON.parse(fs.readFileSync(caminhoDoRelatorio, "utf-8")); var sequencia = dadosDoRelatorio.sequencia || {}; silenceState.sequenceId = sequencia.id || null; silenceState.sequenceName = sequencia.nome || path.basename(caminhoDoRelatorio); silenceState.mediaPath = silenceLocalizarMidiaNoRelatorio(caminhoDoRelatorio, dadosDoRelatorio); silenceState.fileBaseName = path.basename(path.dirname(caminhoDoRelatorio)); silenceState.outputDir = path.dirname(caminhoDoRelatorio); silenceState.audioCachePath = silenceLocalizarAudioParaFalantes(silenceState.outputDir, dadosDoRelatorio); silenceState.transcriptPath = path.join(silenceState.outputDir, "transcricao-do-scanner.json"); silenceState.cachedTranscriptPath = null; silenceState.tipoVideoId = null; silenceState.tipoVideo = null; fs.writeFileSync(silenceState.transcriptPath, JSON.stringify( silenceConverterRelatorioDoScannerParaTranscricao(caminhoDoRelatorio, dadosDoRelatorio), null, 2), "utf-8"); var transcricao = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")); var quantidade = transcricao.segments.length; var statusDoBanco = silenceConsultarStatusNoBanco(silenceState.sequenceId); if (!statusDoBanco || !statusDoBanco.audio) throw new Error("A análise não foi encontrada no banco de dados."); var audioDoBanco = statusDoBanco.audio; var falantes = audioDoBanco.falantes || []; var temTranscricao = quantidade > 0; var temDiarizacao = !!audioDoBanco.diarizacao_disponivel || falantes.length > 0; var temMetricasDeVoz = !!audioDoBanco.metricas_de_voz_disponiveis; atualizarCheckDaAnalise("scannerCheckTranscription", temTranscricao, temTranscricao ? "Carregado" : "Não encontrada"); atualizarCheckDaAnalise("scannerCheckDiarization", temDiarizacao, temDiarizacao ? "Carregado" : "Não realizada"); atualizarCheckDaAnalise("scannerCheckVoiceFeatures", temMetricasDeVoz, temMetricasDeVoz ? "Carregado" : "Não realizada"); var intervalosDePreview = silenceConsultarPreviewDeFalantes(silenceState.sequenceId); var turnos = intervalosDePreview && intervalosDePreview.length ? intervalosDePreview.filter(function (intervalo) { return intervalo.falante && Number(intervalo.fim) > Number(intervalo.inicio); }).map(function (intervalo) { return { speaker: intervalo.falante, start: Number(intervalo.inicio), end: Number(intervalo.fim), }; }) : transcricao.segments.filter(function (segmento) { return segmento.falante; }).map(function (segmento) { return { speaker: segmento.falante, start: Number(segmento.start), end: Number(segmento.end), }; }); renderSpeakerConfiguration(falantes, turnos, { config: "refinarSpeakerConfig", list: "refinarSpeakerList", count: "refinarSpeakerCount", }); atualizarTelaRefinar(falantes, audioDoBanco); status.textContent = "Carregada"; info.textContent = silenceState.sequenceName + " · " + quantidade + " trecho(s) · " + (audioDoBanco.metricas_de_voz_disponiveis ? "métricas disponíveis" : "métricas não realizadas") + " · " + (audioDoBanco.diarizacao_disponivel ? falantes.length + " falante(s)" : "diarização não realizada"); setStep(2, "locked", "Aguardando plano"); setStep(1, "ready", "Tipo no Projeto"); setStep(3, "locked", "Aguardando plano"); renderizarTiposDeVideoParaEdicao(); document.getElementById("btnBuildPlan").disabled = true; // A análise carregada já identifica a sequência; reutiliza o plano salvo // dessa mesma sequência sem exigir uma nova seleção manual. carregarPlanoEditorialSalvo(); // btnDiarize/btnVoiceFeatures/silenceDiarizeInfo/silenceVoiceFeaturesInfo ficavam no // step3 (legado, comentado em 2026-09-09 no index.html) — checagem defensiva para não // quebrar este fluxo (ativo) se os elementos não existirem mais no DOM. var elBtnDiarize = document.getElementById("btnDiarize"); if (elBtnDiarize) elBtnDiarize.disabled = false; var elBtnVoiceFeatures = document.getElementById("btnVoiceFeatures"); if (elBtnVoiceFeatures) elBtnVoiceFeatures.disabled = false; var elDiarizeInfo = document.getElementById("silenceDiarizeInfo"); if (elDiarizeInfo) elDiarizeInfo.textContent = temDiarizacao ? falantes.length + " falante(s) disponíveis no banco de dados." : "Diarização não realizada nesta análise do Scanner. Use Refazer se necessário."; var elVoiceFeaturesInfo = document.getElementById("silenceVoiceFeaturesInfo"); if (elVoiceFeaturesInfo) elVoiceFeaturesInfo.textContent = temMetricasDeVoz ? (audioDoBanco.falas_com_metricas || 0) + " trecho(s) com métricas de voz no banco." : "Métricas de voz não realizadas nesta análise do Scanner. Use Refazer se necessário."; silenceLog("Análise do Scanner carregada: " + quantidade + " trecho(s).", "ok"); showToast("ok", "Análise do Scanner carregada."); } catch (erro) { status.textContent = "Erro"; info.textContent = "Não foi possível ler a análise: " + erro.message; showToast("err", info.textContent); } setBusy("btnLoadScannerAnalysis", false); setBusy("btnRefinarLoad", false); } function refinarAoAbrir() { var info = document.getElementById("refinarInfo"); if (!info) return; if (!silenceState.sequenceId || !silenceState.transcriptPath) { silenceLoadScannerAnalysis(); return; } info.textContent = silenceState.sequenceName + " · " + (silenceState.speakers.length ? silenceState.speakers.length + " falante(s) carregado(s)." : "Nenhum falante carregado."); var botao = document.getElementById("btnRefinarLoad"); if (botao) botao.textContent = "Recarregar análise do Scanner"; } function refinarCarregarAnalise() { silenceLoadScannerAnalysis(); } function atualizarTelaRefinar(falantes, audioDoBanco) { var status = document.getElementById("refinarStatus"); var info = document.getElementById("refinarInfo"); var vazio = document.getElementById("refinarSpeakerEmpty"); var count = document.getElementById("refinarSpeakerCount"); var save = document.getElementById("btnRefinarSave"); if (!status || !info || !vazio || !count) return; var quantidade = Array.isArray(falantes) ? falantes.length : 0; status.textContent = quantidade ? "Pronto" : "Sem diarização"; info.textContent = silenceState.sequenceName + " · " + (audioDoBanco && audioDoBanco.diarizacao_disponivel ? quantidade + " falante(s) detectado(s)." : "A transcrição está disponível, mas a diarização não foi realizada."); count.textContent = quantidade ? quantidade + (quantidade === 1 ? " falante detectado" : " falantes detectados") : "Nenhum falante carregado"; vazio.hidden = quantidade > 0; if (save) save.disabled = quantidade === 0; } var speakerAudioPlayer = null; var speakerAudioQueue = null; // ---- Central transcript cache ---- // Evita rodar o Whisper de novo para um vídeo já transcrito antes: indexa // transcrições por caminho absoluto + tamanho + data de modificação do // arquivo de mídia, para invalidar sozinho se o arquivo original mudar. var TRANSCRIPT_CACHE_DIR = path.join(os.homedir(), ".premiere-mcp", "transcricoes"); var TRANSCRIPT_INDEX_FILE = path.join(TRANSCRIPT_CACHE_DIR, "index.json"); function loadTranscriptIndex() { try { if (!fs.existsSync(TRANSCRIPT_INDEX_FILE)) return {}; var parsed = JSON.parse(fs.readFileSync(TRANSCRIPT_INDEX_FILE, "utf-8")); return (parsed && typeof parsed === "object") ? parsed : {}; } catch (e) { return {}; } } function saveTranscriptIndex(index) { try { ensureDir(TRANSCRIPT_CACHE_DIR); fs.writeFileSync(TRANSCRIPT_INDEX_FILE, JSON.stringify(index, null, 2)); return true; } catch (e) { return false; } } function transcriptCacheKey(mediaPath) { var nodeCrypto = nodeRequire("crypto"); return nodeCrypto.createHash("sha1").update(String(mediaPath)).digest("hex"); } // Uma entrada de índice é sempre uma lista (várias transcrições podem existir // para o mesmo vídeo: modelos diferentes, com/sem diarização, refeitas etc.). function entryListFor(index, key) { var entry = index[key]; if (!entry) return []; return Array.isArray(entry) ? entry : [entry]; // compat com formato antigo (objeto único) } function findCachedTranscripts(mediaPath) { try { var stat = fs.statSync(mediaPath); var index = loadTranscriptIndex(); var key = transcriptCacheKey(mediaPath); var list = entryListFor(index, key); return list.filter(function (entry) { return entry.size === stat.size && entry.mtimeMs === stat.mtimeMs && fs.existsSync(entry.transcriptPath); }); } catch (e) { return []; } } function transcriptCacheFileName(mediaPath, model, when) { var base = slugify(path.basename(mediaPath)); var pad = function (n) { return String(n).padStart(2, "0"); }; var dateStr = when.getFullYear() + pad(when.getMonth() + 1) + pad(when.getDate()); var timeStr = pad(when.getHours()) + pad(when.getMinutes()) + pad(when.getSeconds()); return base + "-" + dateStr + "-" + timeStr + "-" + slugify(model || "modelo") + ".json"; } function saveTranscriptToCache(mediaPath, transcriptPath, model) { try { var stat = fs.statSync(mediaPath); ensureDir(TRANSCRIPT_CACHE_DIR); var key = transcriptCacheKey(mediaPath); var index = loadTranscriptIndex(); var list = entryListFor(index, key); var when = new Date(); var fileName = transcriptCacheFileName(mediaPath, model, when); var cachedPath = path.join(TRANSCRIPT_CACHE_DIR, fileName); fs.copyFileSync(transcriptPath, cachedPath); list.push({ mediaPath: mediaPath, size: stat.size, mtimeMs: stat.mtimeMs, model: model || null, transcriptPath: cachedPath, savedAt: when.toISOString(), }); index[key] = list; saveTranscriptIndex(index); return cachedPath; } catch (e) { return null; } } function silenceLog(msg, cls) { var el = document.getElementById("silenceLog"); var entry = document.createElement("div"); entry.className = "log-entry " + (cls || ""); var ts = new Date().toLocaleTimeString(); entry.textContent = "[" + ts + "] " + msg; el.appendChild(entry); el.scrollTop = el.scrollHeight; while (el.children.length > 100) el.removeChild(el.firstChild); } function slugify(name) { return String(name || "video") .toLowerCase() .replace(/\.[^/.]+$/, "") .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, "") || "video"; } function formatSequenceDuration(seconds) { var total = Math.max(0, Math.round(Number(seconds) || 0)); var minutes = Math.floor(total / 60); var remainder = total % 60; return minutes + ":" + String(remainder).padStart(2, "0"); } function silenceListSequences() { setBusy("btnDetectClip", true); setStep(1, "running", "Carregando…"); document.getElementById("silenceClipInfo").textContent = "Lendo as timelines do projeto…"; cs.evalScript( "(function(){" + "try{" + "if(!app.project) return JSON.stringify({ok:false,error:'Nenhum projeto aberto.'});" + "var sequences=[];" + "var colecao=null;try{colecao=app.project.sequences;}catch(ignorarColecao){colecao=null;}" + "var quantidade=colecao&&typeof colecao.numSequences!=='undefined'?Number(colecao.numSequences):0;" + "for(var i=0;i= 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 span").textContent = estado === "concluida" ? "Analisada" : estado === "erro" ? "Erro: " + (evento.erro || "falha") : "Analisando…"; var faixa = item.querySelector(".progress-track"); var preenchimento = item.querySelector(".progress-fill"); faixa.setAttribute("data-mode", estado === "analisando" ? "indeterminate" : "normal"); preenchimento.style.width = estado === "concluida" ? "100%" : estado === "erro" ? "100%" : "0%"; } function trilhasAnalisar() { var pasta = document.getElementById("trilhasPasta").value.trim(); if (!pasta || !fs.existsSync(pasta) || !fs.statSync(pasta).isDirectory()) { showToast("err", "Selecione uma pasta existente."); return; } trilhasSalvarConfiguracao(pasta); var argumentos = [TRILHAS_ENGINE_SCRIPT, pasta, "--banco", TRILHAS_BANCO_PATH]; argumentos.push("--modelos", TRILHAS_MODELOS_PATH); setBusy("btnTrilhasAnalisar", true); taskStart("Analisando trilhas musicais", "Descobrindo arquivos", function (evento) { trilhasAtualizarEvento(evento); }, function () { if (trilhasProcesso) trilhasProcesso.kill("SIGTERM"); }); trilhasProcesso = runStreamed(SCANNER_PYTHON_BIN, argumentos, function () {}, function (codigo, erro) { trilhasProcesso = null; setBusy("btnTrilhasAnalisar", false); trilhasAtualizarEstado(); if (codigo !== 0) { taskEnd(false, "Falha na análise das trilhas", erro || "O processamento não foi concluído."); return; } taskEnd(true, "Análise das trilhas concluída"); }, { cwd: "/Volumes/Merongo/SISTEMAS/GENIAL SISTEMAS/Jhonny/code", stdio: ["ignore", "pipe", "pipe"] }); } var trilhasProcesso = null; // LEGADO comentado em 2026-09-09: transcrição/diarização feitas direto nesta aba, // duplicando o que a aba Scanner já faz e persiste no banco (.jhonny/analises.db). // Confirmado por teste real que o Scanner cobre o mesmo fluxo de ponta a ponta. // Mantido comentado (não removido) a pedido do usuário, para referência futura. /* function silenceTranscribe() { if (!silenceState.mediaPath) return; document.getElementById("silenceCacheInfo").textContent = ""; var model = document.getElementById("silenceModel").value; setBusy("btnTranscribe", true); setStep(2, "running", "Transcrevendo…"); ensureDir(silenceState.outputDir); var out = path.join(silenceState.outputDir, "transcricao.json"); silenceLog("Transcrevendo com modelo " + model + "… isso pode levar alguns minutos."); taskStart("Transcrevendo o áudio", "Preparando o modelo " + model); var lastLine = ""; runStreamed( SILENCE_PYTHON_BIN, [ SILENCE_TRANSCRIBE_SCRIPT, "--video", silenceState.mediaPath, "--out", out, "--model", model, "--language", loadLanguage(), "--models-path", effectiveModelsPath(), "--cache-dir", silenceState.outputDir, ], function (line) { lastLine = line; silenceLog(line); }, function (code, lastErr) { setBusy("btnTranscribe", false); if (code !== 0) { setStep(2, "error", "Falhou"); taskEnd(false, "A transcrição falhou (código " + code + "). Veja os detalhes técnicos.", lastErr || lastLine); silenceLog("Falha na transcrição (código " + code + ").", "err"); return; } var parsed = null; try { parsed = JSON.parse(lastLine); } catch (e) {} if (!parsed || parsed.ok === false) { setStep(2, "error", "Falhou"); taskEnd(false, "A transcrição terminou com uma resposta inválida.", lastLine); silenceLog("Transcrição terminou mas resposta é inválida: " + lastLine, "err"); return; } silenceState.transcriptPath = out; silenceState.transcribeModel = model; silenceState.speakers = []; silenceState.speakerTurns = []; var speakerConfig = document.getElementById("silenceSpeakerConfig"); if (speakerConfig) speakerConfig.hidden = true; var resumo = parsed.segments + " trechos · " + Math.round(parsed.duration) + "s de áudio"; silenceLog("Transcrição concluída: " + resumo, "ok"); document.getElementById("silenceCacheInfo").textContent = "Transcrição pronta — " + resumo + "."; applyEditorPersonalityToTranscript(out); silenceState.cachedTranscriptPath = saveTranscriptToCache(silenceState.mediaPath, out, model); if (silenceState.cachedTranscriptPath) { silenceLog("Transcrição salva na pasta central para reaproveitar depois.", "ok"); } taskEnd(true, "Transcrição pronta: " + resumo); unlockAfterTranscript(); silenceRunAutomaticEnrichments(); }, hfSpawnOptions() ); } function silenceRunAutomaticEnrichments() { var shouldDiarize = document.getElementById("silenceDiarize").checked && !!loadHfToken(); var shouldMeasureVoice = document.getElementById("silenceVoiceFeaturesAuto").checked; if (shouldDiarize) { silenceDiarizeSpeakers(function () { if (shouldMeasureVoice) silenceComputeVoiceFeatures(); }); } else if (shouldMeasureVoice) { silenceComputeVoiceFeatures(); } } function silenceDiarizeSpeakers(onFinished) { if (!silenceState.transcriptPath) return; if (!silenceState.mediaPath) { silenceLog("Detecte o clipe/sequência primeiro (passo 1) para saber qual vídeo diarizar.", "err"); showToast("err", "Detecte o vídeo no passo 1 primeiro."); return; } if (!loadHfToken()) { silenceLog("Diarização pulada: configure um HF_TOKEN na aba Ajustes.", "err"); showToast("err", "Configure o token do Hugging Face na aba Ajustes."); return; } var info = document.getElementById("silenceDiarizeInfo"); setBusy("btnDiarize", true); setStep(3, "running", "Detectando…"); info.textContent = "Detectando locutores…"; silenceLog("Detectando locutores…"); taskStart("Detectando quem fala", "Preparando"); runStreamed( SILENCE_PYTHON_BIN, [ SILENCE_DIARIZE_SCRIPT, "--video", silenceState.mediaPath, "--transcript", silenceState.transcriptPath, "--out", silenceState.transcriptPath, "--models-path", effectiveModelsPath(), "--cache-dir", silenceState.outputDir, ], function (line) { silenceLog(line); }, function (code, lastErr) { setBusy("btnDiarize", false); if (code !== 0) { setStep(3, "ready", "Opcional"); info.textContent = "Falhou — veja os detalhes técnicos."; taskEnd(false, "A detecção de locutores falhou (código " + code + ").", lastErr); silenceLog("Falha na diarização (código " + code + (lastErr ? ": " + lastErr : "") + ").", "err"); return; } var parsed = null; try { parsed = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")).diarization; } catch (e) {} if (parsed && Array.isArray(parsed.speakers)) { silenceState.speakerTurns = Array.isArray(parsed.turns) ? parsed.turns : []; renderSpeakerConfiguration(parsed.speakers, silenceState.speakerTurns); atualizarTelaRefinar(parsed.speakers, { diarizacao_disponivel: true }); } var msg = parsed ? "Foram detectados " + parsed.num_speakers + (parsed.num_speakers === 1 ? " falante" : " falantes") + ": " + parsed.speakers.join(", ") + "." : "Falantes detectados."; silenceLog(msg, "ok"); info.textContent = msg; atualizarCheckDaAnalise("scannerCheckDiarization", true, "Carregado"); setStep(3, "done", "Enriquecido"); taskEnd(true, msg); syncTranscriptToCache(); if (typeof onFinished === "function") onFinished(); }, hfSpawnOptions() ); } */ function speakerNameDefault(speakerId) { return speakerId; } function carregarNomesDeFalantes() { if (!silenceState.sequenceId) return {}; try { var salvo = JSON.parse(localStorage.getItem("mcp_falantes_" + silenceState.sequenceId) || "{}"); return salvo && typeof salvo === "object" ? salvo : {}; } catch (_) { return {}; } } function salvarNomesDeFalantes() { if (!silenceState.sequenceId) return; var salvo = {}; silenceState.speakers.forEach(function (speaker) { salvo[speaker.id] = { name: speaker.name, active: speaker.active !== false }; }); try { localStorage.setItem("mcp_falantes_" + silenceState.sequenceId, JSON.stringify(salvo)); } catch (_) {} } function refinarSalvarFalantes() { salvarNomesDeFalantes(); showToast("ok", "Identificação dos falantes salva."); } function renderSpeakerConfiguration(speakerIds, speakerTurns, alvo) { alvo = alvo || {}; var config = document.getElementById(alvo.config || "silenceSpeakerConfig"); var list = document.getElementById(alvo.list || "silenceSpeakerList"); var count = document.getElementById(alvo.count || "silenceSpeakerCount"); if (!config || !list || !count) return; silenceState.speakerTurns = Array.isArray(speakerTurns) ? speakerTurns : silenceState.speakerTurns; var previous = {}; silenceState.speakers.forEach(function (speaker) { previous[speaker.id] = speaker; }); var salvo = carregarNomesDeFalantes(); silenceState.speakers = speakerIds.map(function (id) { var old = previous[id]; var salvoSpeaker = salvo[id] || {}; return { id: id, name: old && old.name ? old.name : (salvoSpeaker.name || speakerNameDefault(id)), active: old ? old.active !== false : salvoSpeaker.active !== false, }; }); count.textContent = silenceState.speakers.length + (silenceState.speakers.length === 1 ? " falante detectado" : " falantes detectados"); list.innerHTML = ""; silenceState.speakers.forEach(function (speaker, index) { var row = document.createElement("div"); row.className = "speaker-row"; var label = document.createElement("label"); label.className = "field-label"; label.textContent = speaker.id; var play = document.createElement("button"); play.type = "button"; play.className = "button speaker-play-button"; play.textContent = "▶ áudio"; play.title = "Ouvir trechos de " + speaker.id; play.setAttribute("aria-label", "Ouvir trechos de " + speaker.id); play.addEventListener("click", function () { toggleSpeakerAudio(speaker.id, play); }); var input = document.createElement("input"); input.type = "text"; input.className = "select-field speaker-name-input"; input.value = speaker.name; input.placeholder = "Nome do falante"; input.maxLength = 120; input.setAttribute("aria-label", "Nome de " + speaker.id); input.addEventListener("input", function () { silenceState.speakers[index].name = input.value.trim() || speaker.id; salvarNomesDeFalantes(); }); var activeLabel = document.createElement("label"); activeLabel.className = "checkbox-field speaker-active-field"; var active = document.createElement("input"); active.type = "checkbox"; active.checked = speaker.active; active.addEventListener("change", function () { silenceState.speakers[index].active = active.checked; salvarNomesDeFalantes(); }); var activeText = document.createElement("span"); activeText.textContent = "Incluir no vídeo"; activeLabel.appendChild(active); activeLabel.appendChild(activeText); row.appendChild(label); row.appendChild(play); row.appendChild(input); row.appendChild(activeLabel); list.appendChild(row); }); config.hidden = silenceState.speakers.length === 0; var refineConfig = document.getElementById("refinarSpeakerConfig"); if (refineConfig && config.id !== refineConfig.id) { renderSpeakerConfiguration(speakerIds, speakerTurns, { config: "refinarSpeakerConfig", list: "refinarSpeakerList", count: "refinarSpeakerCount" }); } } function speakerMediaFileUrl(filePath) { return encodeURI("file://" + String(filePath || "")); } function stopSpeakerAudio() { if (speakerAudioPlayer) { speakerAudioPlayer.pause(); speakerAudioPlayer.removeAttribute("src"); speakerAudioPlayer.load(); } speakerAudioPlayer = null; speakerAudioQueue = null; document.querySelectorAll(".speaker-play-button").forEach(function (button) { button.textContent = "▶ áudio"; button.classList.remove("is-playing"); }); } function toggleSpeakerAudio(speakerId, button) { if (speakerAudioPlayer && speakerAudioQueue && speakerAudioQueue.speakerId === speakerId) { stopSpeakerAudio(); return; } stopSpeakerAudio(); var ranges = silenceState.speakerTurns.filter(function (turn) { return turn.speaker === speakerId && Number(turn.end) > Number(turn.start); }).sort(function (a, b) { return Number(a.start) - Number(b.start); }); if (!ranges.length) { showToast("err", "Não há intervalos de áudio disponíveis para " + speakerId + "."); return; } if (!silenceState.audioCachePath || !fs.existsSync(silenceState.audioCachePath)) { showToast("err", "Arquivo de áudio do cache não encontrado. Reprocesse a transcrição."); return; } speakerAudioQueue = { speakerId: speakerId, ranges: ranges, index: 0, button: button }; speakerAudioPlayer = new Audio(speakerMediaFileUrl(silenceState.audioCachePath)); button.textContent = "■"; button.classList.add("is-playing"); speakerAudioPlayer.addEventListener("loadedmetadata", playCurrentSpeakerRange); speakerAudioPlayer.addEventListener("timeupdate", advanceSpeakerAudioRange); speakerAudioPlayer.addEventListener("ended", advanceSpeakerAudioRange); speakerAudioPlayer.addEventListener("error", function () { stopSpeakerAudio(); showToast("err", "Não foi possível reproduzir o áudio deste falante."); }); } function playCurrentSpeakerRange() { if (!speakerAudioPlayer || !speakerAudioQueue) return; var range = speakerAudioQueue.ranges[speakerAudioQueue.index]; if (!range) { stopSpeakerAudio(); return; } speakerAudioPlayer.currentTime = Math.max(0, Number(range.start) - 0.15); var playPromise = speakerAudioPlayer.play(); if (playPromise && playPromise.catch) playPromise.catch(function () { stopSpeakerAudio(); }); } function advanceSpeakerAudioRange() { if (!speakerAudioPlayer || !speakerAudioQueue) return; var range = speakerAudioQueue.ranges[speakerAudioQueue.index]; if (!range || speakerAudioPlayer.currentTime < Number(range.end)) return; speakerAudioQueue.index += 1; if (speakerAudioQueue.index >= speakerAudioQueue.ranges.length) { stopSpeakerAudio(); return; } playCurrentSpeakerRange(); } function silenceLoadSpeakerConfiguration() { if (!silenceState.transcriptPath) return; try { var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")); var diarization = transcript.diarization; if (diarization && Array.isArray(diarization.speakers)) renderSpeakerConfiguration(diarization.speakers, diarization.turns); } catch (e) {} } function speakerConfigurationForJson() { var speakers = silenceState.speakers.map(function (speaker) { var active = speaker.active !== false; return { id: speaker.id, name: speaker.name || speaker.id, status: active ? "active" : "inactive", include_in_video: active, editorial_instruction: active ? "Manter as falas deste falante, salvo decisão específica da IA." : "Excluir todas as falas deste falante do vídeo final.", }; }); return { total_detected: speakers.length, speakers: speakers, inactive_speakers: speakers.filter(function (speaker) { return !speaker.include_in_video; }).map(function (speaker) { return speaker.id; }), instruction: "Falantes inativos devem ter todas as suas falas removidas do vídeo final; não confundir nome com identidade confirmada.", }; } // Depois de mutar silenceState.transcriptPath em lugar (diarização, métricas de // voz), propaga a mudança pra cópia central de cache, se forem arquivos diferentes. function syncTranscriptToCache() { if (silenceState.cachedTranscriptPath && silenceState.cachedTranscriptPath !== silenceState.transcriptPath) { try { fs.copyFileSync(silenceState.transcriptPath, silenceState.cachedTranscriptPath); } catch (e) {} } } // LEGADO comentado em 2026-09-09: calculava métricas de voz direto nesta aba, // duplicando o que a aba Scanner já faz e persiste no banco (.jhonny/analises.db). // Mantido comentado (não removido) a pedido do usuário, para referência futura. /* function silenceComputeVoiceFeatures() { if (!silenceState.transcriptPath) return; if (!silenceState.mediaPath) { silenceLog("Detecte o clipe/sequência primeiro (passo 1) para saber qual vídeo analisar.", "err"); showToast("err", "Detecte o vídeo no passo 1 primeiro."); return; } var info = document.getElementById("silenceVoiceFeaturesInfo"); setBusy("btnVoiceFeatures", true); setStep(3, "running", "Calculando…"); info.textContent = "Calculando…"; silenceLog("Calculando pitch/energia/velocidade de fala/pausas…"); taskStart("Medindo a voz", "Lendo a transcrição"); runStreamed( SILENCE_PYTHON_BIN, [ VOICE_FEATURES_SCRIPT, "--video", silenceState.mediaPath, "--transcript", silenceState.transcriptPath, "--out", silenceState.transcriptPath, "--cache-dir", silenceState.outputDir, ], function (line) { silenceLog(line); }, function (code, lastErr) { setBusy("btnVoiceFeatures", false); if (code !== 0) { setStep(3, "ready", "Opcional"); info.textContent = "Falhou — veja os detalhes técnicos."; taskEnd(false, "O cálculo das métricas de voz falhou (código " + code + ").", lastErr); silenceLog("Falha ao calcular métricas de voz (código " + code + (lastErr ? ": " + lastErr : "") + ").", "err"); return; } var msg = "Pitch, energia, velocidade e pausas calculados por trecho."; silenceLog(msg, "ok"); info.textContent = msg; atualizarCheckDaAnalise("scannerCheckVoiceFeatures", true, "Carregado"); setStep(3, "done", "Enriquecido"); taskEnd(true, msg); syncTranscriptToCache(); } ); } */ // Registra a decisão editorial sem duplicar a análise normalizada do Scanner. // O JSON continua disponível para o fluxo da IA, mas não é pré-requisito para // concluir a edição no painel. function silenceConcluirEdicao() { if (!silenceState.sequenceId || !silenceState.sequenceName) { showToast("err", "Carregue uma análise do Scanner antes de concluir."); return; } if (!silenceState.tipoVideo) { showToast("err", "Selecione o tipo de vídeo antes de concluir a edição."); setStep(1, "ready", "Selecione um tipo"); return; } var pedido = { video_id: String(silenceState.sequenceId), sequencia: silenceState.sequenceName, origem: silenceState.mediaPath, tipo_de_video: silenceState.tipoVideo.id, configuracao: { nome_do_tipo: silenceState.tipoVideo.nome, instrucoes_do_tipo: tipoVideoResumoParaJson(silenceState.tipoVideo), transcricao: { caminho: silenceState.transcriptPath, origem: "Scanner", }, }, }; var caminhoDoPedido = path.join(os.tmpdir(), "pedido-concluir-edicao-" + Date.now() + ".json"); try { fs.writeFileSync(caminhoDoPedido, JSON.stringify(pedido, null, 2), "utf-8"); } catch (erroEscrita) { showToast("err", "Não foi possível preparar a conclusão: " + erroEscrita.message); return; } setBusy("btnBuildPlan", true); setStep(2, "running", "Salvando…"); taskStart("Concluindo a edição", "Salvando a configuração no banco"); silenceLog("Salvando o tipo de vídeo e as configurações editoriais no banco…"); runStreamed( SCANNER_PYTHON_BIN, [REGISTRAR_EDICAO_SCRIPT, caminhoDoPedido, "--banco", ANALISES_DB_PATH], function (linha) { silenceLog(linha); }, function (codigo, ultimoErro, ultimaSaida) { setBusy("btnBuildPlan", false); if (codigo !== 0) { setStep(2, "error", "Falhou"); taskEnd(false, "Não foi possível concluir a edição.", ultimoErro); silenceLog("Falha ao salvar a configuração editorial.", "err"); showToast("err", ultimoErro || "Não foi possível salvar a edição no banco."); return; } setStep(2, "done", "Edição concluída"); setStep(3, "locked", "Aguardando plano da IA"); taskEnd(true, "Edição concluída e salva no banco."); silenceLog("Edição concluída e salva no banco de análises.", "ok"); showToast("ok", "Edição concluída e salva no banco."); } ); } // Busca o plano já registrado no SQLite; não cria JSON novo nem duplica plano. function carregarPlanoEditorialSalvo() { if (!silenceState.sequenceId) { showToast("err", "Detecte a sequência antes de carregar o plano."); return; } setBusy("btnBuildPlan", true); silenceLog("Lendo o plano editorial diretamente do banco…"); runStreamed( SCANNER_PYTHON_BIN, [CARREGAR_PLANO_SCRIPT, "--banco", ANALISES_DB_PATH, "--video-id", String(silenceState.sequenceId)], function (linha) { silenceLog(linha); }, function (codigo, ultimoErro, ultimaSaida) { setBusy("btnBuildPlan", false); if (codigo !== 0) { silenceLog("Não foi possível carregar o plano salvo.", "err"); showToast("err", ultimoErro || "Nenhum plano salvo encontrado para este vídeo."); return; } try { var resposta = JSON.parse(ultimaSaida || "{}"); if (!resposta.ok || !resposta.plano) throw new Error("Resposta inválida do banco."); // O plano histórico pode usar o nome da sequência como origem. // A engine localiza os clipes pela mídia real descoberta no Premiere. if (silenceState.mediaPath) resposta.plano.source = silenceState.mediaPath; planoEditorialIdCarregado = resposta.plano.plano_id; document.getElementById("editorialPlanJson").value = JSON.stringify(resposta.plano, null, 2); document.getElementById("planoResumoEdicao").textContent = "Plano " + planoEditorialIdCarregado + " encontrado · " + resposta.plano.actions.length + " ação(ões) · aguardando aprovação."; document.getElementById("editorialSavedPlanStatus").textContent = "Plano " + planoEditorialIdCarregado + " carregado do banco."; document.getElementById("editorialPlanJsonStatus").textContent = "Plano salvo no SQLite; pronto para aprovação e aplicação."; document.getElementById("btnApplyEditorialActions").disabled = false; setStep(2, "done", "Plano carregado"); setStep(3, "ready", "Pronto para aplicar"); silenceLog("Plano " + planoEditorialIdCarregado + " carregado diretamente do SQLite.", "ok"); showToast("ok", "Plano salvo carregado."); } catch (erro) { showToast("err", erro.message); } } ); } // Não gera decisões de corte: só empacota transcrição + tempos + locutores + // métricas de voz + configurações de edição num JSON para um agente de IA // externo analisar. Quem decide os cortes é a IA, não este painel. function silenceGenerateJson() { if (!silenceState.transcriptPath) return; planoEditorialIdCarregado = null; if (!silenceState.tipoVideo) { showToast("err", "Selecione o tipo de vídeo antes de gerar o JSON."); setStep(1, "ready", "Selecione um tipo"); return; } setBusy("btnBuildPlan", true); setStep(2, "running", "Gerando…"); silenceLog("Gerando arquivo JSON para a IA…"); try { var transcript = JSON.parse(fs.readFileSync(silenceState.transcriptPath, "utf-8")); transcript.sequence = { name: silenceState.sequenceName, media_path: silenceState.mediaPath, }; transcript.settings = Object.assign({}, transcript.settings, { legendas: carregarConfiguracaoDeLegendas(), }); transcript.speaker_configuration = speakerConfigurationForJson(); transcript.tipo_de_video = { id: silenceState.tipoVideo.id, nome: silenceState.tipoVideo.nome, instrucoes: tipoVideoResumoParaJson(silenceState.tipoVideo), }; ensureDir(silenceState.outputDir); var out = path.join(silenceState.outputDir, "dados-para-ia.json"); fs.writeFileSync(out, JSON.stringify(transcript, null, 2), "utf-8"); silenceState.jsonOutputPath = out; var segCount = Array.isArray(transcript.segments) ? transcript.segments.length : 0; document.getElementById("jsonSegmentCount").textContent = segCount; var pathCell = document.getElementById("jsonOutputPath"); pathCell.textContent = out; pathCell.title = out; document.getElementById("silenceJsonCard").hidden = false; document.getElementById("jsonFileActions").hidden = false; document.getElementById("silencePlanInfo").textContent = "Envie este arquivo a um agente de IA externo. Ele deve devolver o plano de edição em JSON para o passo 5."; setStep(2, "done", "JSON pronto"); setStep(3, "ready", "Sua vez"); document.getElementById("btnApplyEditorialActions").disabled = false; showToast("ok", "JSON gerado: " + path.basename(out)); silenceLog("JSON gerado em " + out + " (" + segCount + " trechos).", "ok"); } catch (e) { setStep(2, "error", "Falhou"); showToast("err", "Falha ao gerar o JSON: " + e.message); silenceLog("Falha ao gerar o JSON: " + e.message, "err"); } setBusy("btnBuildPlan", false); } // O script de cortes importa ../dist/tools/*.js, então precisa rodar de dentro // do repositório — não da cópia instalada em ~/Library/.../CEP/extensions. // Ordem: repoRoot gravado pelo install-cep.sh > relativo ao painel (modo symlink) > fallback fixo. function resolveScript(scriptFileName, fallbackPath) { var candidates = []; var repoRoot = String(loadConfig().repoRoot || "").trim(); if (repoRoot) candidates.push(path.join(repoRoot, "scripts", scriptFileName)); try { var extensionRoot = toFsPath(cs.getSystemPath("extension")); candidates.push(path.join(extensionRoot, "..", "scripts", scriptFileName)); } catch (e) {} candidates.push(fallbackPath); for (var i = 0; i < candidates.length; i++) { try { if (fs.existsSync(candidates[i])) return candidates[i]; } catch (e) {} } return null; } function resolveEditorialActionsScript() { return resolveScript("apply-editorial-actions.mjs", EDITORIAL_ACTIONS_APPLY_SCRIPT_FALLBACK); } // ---- Aplicar o plano de edição devolvido pela IA (cut/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(3, "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(3, "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].concat( planoEditorialIdCarregado === null ? [] : ["--plano-id", String(planoEditorialIdCarregado), "--banco", ANALISES_DB_PATH] ), function (line) { silenceLog(line); }, function (code, lastErr, lastOutput) { processoAplicacao = null; setBusy("btnApplyEditorialActions", false); if (code !== 0) { var detalheDaAplicacao = ""; try { var resultadoDaAplicacao = JSON.parse(lastOutput || "{}"); var falhasDaAplicacao = (resultadoDaAplicacao.acoes || []).filter(function (acao) { return acao.sucesso === false; }); if (falhasDaAplicacao.length) detalheDaAplicacao = falhasDaAplicacao.map(function (acao) { return acao.detalhe; }).join(" | "); } catch (_) {} setStep(3, "error", "Falhou"); var mensagemDaFalha = detalheDaAplicacao || lastErr || "A engine não retornou detalhes."; taskEnd(false, "Falha ao aplicar o plano (código " + code + ").", mensagemDaFalha); silenceLog("Falha ao aplicar o plano (código " + code + "): " + mensagemDaFalha, "err"); return; } setStep(3, "done", "Aplicado"); taskEnd(true, numActions + " ação(ões) aplicadas na timeline!"); silenceLog("Plano de edição aplicado com sucesso!", "ok"); } ); } ); } // Abre o seletor de arquivos nativo do CEP em vez de exigir que a pessoa cole // um caminho absoluto à mão. function pickEditorialPlanFile() { var input = document.getElementById("editorialPlanPath"); try { var api = window.cep && window.cep.fs; if (!api || typeof api.showOpenDialog !== "function") { input.focus(); showToast("err", "Seletor de arquivos indisponível aqui — cole o caminho do JSON."); return; } var result = api.showOpenDialog(false, false, "Escolha o plano de edição (JSON)", "", ["json"]); var chosen = result && result.data && result.data.length ? result.data[0] : ""; if (chosen) input.value = toFsPath(chosen); } catch (e) { input.focus(); showToast("err", "Não foi possível abrir o seletor: " + e.message); } } // ---- Settings / secrets storage ---- var CONFIG_DIR = path.join(os.homedir(), ".premiere-mcp"); var CONFIG_FILE = path.join(CONFIG_DIR, "config.json"); function loadConfig() { try { if (!fs.existsSync(CONFIG_FILE)) return {}; var parsed = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8")); return (parsed && typeof parsed === "object") ? parsed : {}; } catch (e) { return {}; } } function saveConfig(patch) { try { ensureDir(CONFIG_DIR); var next = Object.assign({}, loadConfig(), patch); fs.writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), { mode: 0o600 }); try { fs.chmodSync(CONFIG_FILE, 0o600); } catch (e) {} return true; } catch (e) { log("Error saving config: " + e.message, "err"); return false; } } // ---- Configuração do Editor: personalidades de edição ---- // Fase 1: nome + texto livre descrevendo o comportamento esperado. A // personalidade marcada como ativa vai embutida no JSON de transcrição // (settings.editor_personality), pra eu já saber o estilo esperado quando // você me trouxer o arquivo. Fases futuras podem trocar/complementar o texto // livre por parâmetros estruturados obrigatórios. function loadEditorPersonalities() { var list = loadConfig().editorPersonalities; return Array.isArray(list) ? list : []; } function loadActiveEditorPersonalityId() { return String(loadConfig().activeEditorPersonalityId || "").trim(); } function getActiveEditorPersonality() { var id = loadActiveEditorPersonalityId(); if (!id) return null; var list = loadEditorPersonalities(); for (var i = 0; i < list.length; i++) { if (list[i].id === id) return list[i]; } return null; } function setActiveEditorPersonality(id) { saveConfig({ activeEditorPersonalityId: id }); showToast("ok", "Personalidade ativa atualizada."); renderEditorPersonalities(); } // Embute a personalidade de edição ativa (nome + texto livre) como // `settings.editor_personality` dentro do JSON de transcrição recém-gerado, // pra ir junto quando o arquivo for trazido pra análise. function applyEditorPersonalityToTranscript(transcriptPath) { var personality = getActiveEditorPersonality(); if (!personality) return; try { var transcript = JSON.parse(fs.readFileSync(transcriptPath, "utf-8")); transcript.settings = Object.assign({}, transcript.settings, { editor_personality: { name: personality.name, text: personality.text }, }); fs.writeFileSync(transcriptPath, JSON.stringify(transcript, null, 2), "utf-8"); silenceLog('Personalidade de edição embutida no JSON: "' + personality.name + '".', "ok"); } catch (e) { silenceLog("Não foi possível embutir a personalidade de edição: " + e.message, "err"); } } var editingPersonalityId = null; function startNewEditorPersonality() { editingPersonalityId = null; document.getElementById("editorPersonalityName").value = ""; document.getElementById("editorPersonalityText").value = ""; document.getElementById("editorPersonalityForm").hidden = false; } function editEditorPersonality(id) { var list = loadEditorPersonalities(); var entry = list.filter(function (p) { return p.id === id; })[0]; if (!entry) return; editingPersonalityId = id; document.getElementById("editorPersonalityName").value = entry.name; document.getElementById("editorPersonalityText").value = entry.text; document.getElementById("editorPersonalityForm").hidden = false; } function cancelEditorPersonalityForm() { editingPersonalityId = null; document.getElementById("editorPersonalityForm").hidden = true; } function saveEditorPersonalityForm() { var name = document.getElementById("editorPersonalityName").value.trim(); var text = document.getElementById("editorPersonalityText").value.trim(); if (!name || !text) { modelsLog("Preencha nome e descrição da personalidade.", "err"); return; } var list = loadEditorPersonalities(); if (editingPersonalityId) { list = list.map(function (p) { return p.id === editingPersonalityId ? { id: p.id, name: name, text: text } : p; }); } else { var id = "personality-" + Date.now().toString(36); list.push({ id: id, name: name, text: text }); if (!loadActiveEditorPersonalityId()) saveConfig({ activeEditorPersonalityId: id }); } saveConfig({ editorPersonalities: list }); cancelEditorPersonalityForm(); renderEditorPersonalities(); modelsLog("Personalidade de edição salva: " + name, "ok"); } function deleteEditorPersonality(id) { var list = loadEditorPersonalities(); var entry = list.filter(function (p) { return p.id === id; })[0]; if (!entry) return; if (typeof window.confirm === "function" && !window.confirm('Excluir a personalidade "' + entry.name + '"?')) return; list = list.filter(function (p) { return p.id !== id; }); var patch = { editorPersonalities: list }; if (loadActiveEditorPersonalityId() === id) patch.activeEditorPersonalityId = ""; saveConfig(patch); renderEditorPersonalities(); } function renderEditorPersonalities() { var container = document.getElementById("editorPersonalityList"); if (!container) return; container.innerHTML = ""; var activeId = loadActiveEditorPersonalityId(); var list = loadEditorPersonalities(); if (!list.length) { var empty = document.createElement("p"); empty.className = "field-help"; empty.textContent = "Nenhuma personalidade cadastrada ainda. Crie uma para orientar o corte por IA."; container.appendChild(empty); return; } list.forEach(function (entry) { var isActive = entry.id === activeId; var row = document.createElement("div"); row.className = "personality-row"; row.setAttribute("data-active", String(isActive)); var main = document.createElement("div"); main.className = "model-main"; var name = document.createElement("span"); name.className = "model-name"; name.textContent = entry.name; main.appendChild(name); var meta = document.createElement("div"); meta.className = "model-meta"; var snippet = document.createElement("span"); snippet.className = "model-badge"; snippet.textContent = entry.text.length > 60 ? entry.text.slice(0, 60) + "…" : entry.text; snippet.title = entry.text; meta.appendChild(snippet); if (isActive) { var badge = document.createElement("span"); badge.className = "model-badge model-badge-star"; badge.textContent = "★ Ativa"; meta.appendChild(badge); } main.appendChild(meta); row.appendChild(main); var actions = document.createElement("div"); actions.className = "model-actions"; var activeBtn = document.createElement("button"); activeBtn.type = "button"; if (isActive) { activeBtn.className = "button button-primary model-btn-active"; activeBtn.textContent = "Ativa"; activeBtn.disabled = true; } else { activeBtn.className = "button button-ghost"; activeBtn.textContent = "Usar esta"; activeBtn.onclick = function () { setActiveEditorPersonality(entry.id); }; } actions.appendChild(activeBtn); var editBtn = document.createElement("button"); editBtn.type = "button"; editBtn.className = "link-button"; editBtn.textContent = "Editar"; editBtn.onclick = function () { editEditorPersonality(entry.id); }; actions.appendChild(editBtn); var deleteBtn = document.createElement("button"); deleteBtn.type = "button"; deleteBtn.className = "link-button"; deleteBtn.textContent = "Excluir"; deleteBtn.onclick = function () { deleteEditorPersonality(entry.id); }; actions.appendChild(deleteBtn); row.appendChild(actions); container.appendChild(row); }); } function loadHfToken() { return String(loadConfig().hfToken || "").trim(); } function hfSpawnOptions() { var token = loadHfToken(); if (!token) return undefined; var nodeProcess = nodeRequire("process"); return { env: Object.assign({}, nodeProcess.env, { HF_TOKEN: token }) }; } function loadModelsPath() { return String(loadConfig().modelsPath || "").trim(); } function effectiveModelsPath() { return loadModelsPath() || DEFAULT_MODELS_PATH; } function saveModelsPath() { var value = document.getElementById("modelsPath").value.trim(); if (!saveConfig({ modelsPath: value })) return; modelsLog("Pasta de modelos salva: " + effectiveModelsPath()); showToast("ok", "Pasta de modelos salva."); renderModelList(); } function loadLanguage() { return String(loadConfig().language || "").trim() || DEFAULT_LANGUAGE; } function saveLanguage() { var value = document.getElementById("transcribeLanguage").value; saveConfig({ language: value }); syncLanguageInfo(); showToast("ok", "Idioma da transcrição salvo."); modelsLog("Idioma de transcrição salvo: " + value); } function loadDefaultModel() { return String(loadConfig().defaultModel || "").trim() || DEFAULT_MODEL; } function setDefaultModel(model) { saveConfig({ defaultModel: model }); modelsLog("Modelo padrão definido: " + model, "ok"); showToast("ok", "Modelo padrão: " + model + "."); renderModelList(); syncTranscribeModelSelect(); } // Mantém o seletor de modelo da aba de Transcrição em sincronia com o catálogo // e com o modelo padrão escolhido na aba Modelos Locais. function syncTranscribeModelSelect() { var select = document.getElementById("silenceModel"); if (!select) return; var current = select.value; select.innerHTML = ""; var modelosDisponiveis = discoverPortugueseModelsOnMerongo(); modelosDisponiveis.forEach(function (modelo) { var opt = document.createElement("option"); opt.value = modelo.nome; opt.textContent = modelo.nome; select.appendChild(opt); }); if (!modelosDisponiveis.length) { var vazio = document.createElement("option"); vazio.value = ""; vazio.textContent = "Nenhum modelo instalado"; select.appendChild(vazio); return; } var nomes = modelosDisponiveis.map(function (modelo) { return modelo.nome; }); var desejado = nomes.indexOf(current) >= 0 ? current : loadDefaultModel(); select.value = nomes.indexOf(desejado) >= 0 ? desejado : nomes[0]; } // ---- Settings tab: HF token ---- function saveHfToken() { var token = document.getElementById("hfToken").value.trim(); if (!saveConfig({ hfToken: token })) return; log("Hugging Face token salvo"); showToast("ok", token ? "Token salvo." : "Token removido."); setHfTokenStatus(token ? "unknown" : "unset", token ? "Salvo — clique em Validar" : "Não configurado"); } function toggleHfTokenVisibility() { var input = document.getElementById("hfToken"); input.type = input.type === "password" ? "text" : "password"; } function loadGroqApiKey() { return String(loadConfig().groqApiKey || "").trim(); } function saveGroqApiKey() { var key = document.getElementById("groqApiKey").value.trim(); if (!saveConfig({ groqApiKey: key })) return; log("Chave Groq salva"); showToast("ok", key ? "API Groq salva." : "API Groq removida."); var status = document.getElementById("groqApiKeyStatus"); if (status) status.setAttribute("data-state", key ? "unknown" : "unset"); document.getElementById("groqApiKeyStatusText").textContent = key ? "Salva" : "Não configurada"; } function toggleGroqApiKeyVisibility() { var input = document.getElementById("groqApiKey"); input.type = input.type === "password" ? "text" : "password"; } function setHfTokenStatus(state, text) { var el = document.getElementById("hfTokenStatus"); if (!el) return; el.setAttribute("data-state", state); document.getElementById("hfTokenStatusText").textContent = text; } function validateHfToken() { var token = document.getElementById("hfToken").value.trim() || loadHfToken(); if (!token) { setHfTokenStatus("unset", "Não configurado"); return; } setHfTokenStatus("checking", "Validando…"); var req = https.get( "https://huggingface.co/api/whoami-v2", { headers: { Authorization: "Bearer " + token, "User-Agent": "premiere-pro-mcp-connector" } }, function (response) { var body = ""; response.setEncoding("utf8"); response.on("data", function (chunk) { body += chunk; }); response.on("end", function () { if (response.statusCode === 200) { var name = ""; try { name = JSON.parse(body).name || ""; } catch (e) {} setHfTokenStatus("valid", "Token válido" + (name ? " (" + name + ")" : "")); } else { setHfTokenStatus("invalid", "Token inválido (HTTP " + response.statusCode + ")"); } }); } ); req.on("error", function (err) { setHfTokenStatus("invalid", "Erro ao validar: " + err.message); }); req.setTimeout(8000, function () { req.destroy(); setHfTokenStatus("invalid", "Tempo esgotado ao validar"); }); } // ---- Models tab ---- // Localiza a pasta de cache HF (formato "models--Systran--faster-whisper-") // que contém um model.bin de fato baixado para o modelo pedido. function findModelFolder(model) { try { var modelsPath = effectiveModelsPath(); if (!fs.existsSync(modelsPath)) return null; var entries = fs.readdirSync(modelsPath); for (var i = 0; i < entries.length; i++) { // ex: "models--Systran--faster-whisper-small" for model "small" if (entries[i].indexOf("models--") !== 0) continue; if (entries[i].toLowerCase().indexOf(("faster-whisper-" + model).toLowerCase()) === -1) continue; var fullDir = path.join(modelsPath, entries[i]); var snapshotsDir = path.join(fullDir, "snapshots"); if (!fs.existsSync(snapshotsDir)) continue; var snaps = fs.readdirSync(snapshotsDir); for (var j = 0; j < snaps.length; j++) { if (fs.existsSync(path.join(snapshotsDir, snaps[j], "model.bin"))) return fullDir; } } return null; } catch (e) { return null; } } function isModelDownloaded(model) { return !!findModelFolder(model); } function deleteModel(model) { var folder = findModelFolder(model); if (!folder) return; var confirmMsg = "Excluir o modelo \"" + model + "\" baixado (" + folder + ")? Isso libera espaço em disco; será baixado de novo se você precisar dele depois."; if (typeof window.confirm === "function" && !window.confirm(confirmMsg)) return; try { fs.rmSync(folder, { recursive: true, force: true }); modelsLog("Modelo " + model + " excluído.", "ok"); showToast("ok", "Modelo " + model + " excluído."); } catch (e) { modelsLog("Falha ao excluir " + model + ": " + e.message, "err"); } renderModelList(); } function modelsLog(msg, cls) { var el = document.getElementById("modelsLog"); if (!el) return; var entry = document.createElement("div"); entry.className = "log-entry " + (cls || ""); var ts = new Date().toLocaleTimeString(); entry.textContent = "[" + ts + "] " + msg; el.appendChild(entry); el.scrollTop = el.scrollHeight; while (el.children.length > 100) el.removeChild(el.firstChild); } function renderLanguageOptions() { var select = document.getElementById("transcribeLanguage"); if (!select) return; select.innerHTML = ""; LANGUAGE_CATALOG.forEach(function (lang) { var opt = document.createElement("option"); opt.value = lang.value; opt.textContent = lang.label; select.appendChild(opt); }); } function renderModelList() { var list = document.getElementById("modelList"); if (!list) return; list.innerHTML = ""; var defaultModel = loadDefaultModel(); MODEL_CATALOG.forEach(function (entry) { var model = entry.value; var folder = findModelFolder(model); var downloaded = !!folder; var isDefault = model === defaultModel; var row = document.createElement("div"); row.className = "model-row"; row.id = "modelRow-" + model; var top = document.createElement("div"); top.className = "model-row-top"; var main = document.createElement("div"); main.className = "model-main"; var name = document.createElement("span"); name.className = "model-name"; name.textContent = entry.label; main.appendChild(name); var meta = document.createElement("div"); meta.className = "model-meta"; var sizeBadge = document.createElement("span"); sizeBadge.className = "model-badge model-badge-size"; sizeBadge.textContent = "≈ " + formatMB(downloaded ? folderSizeMB(folder) : entry.approxMB) + (downloaded ? "" : " (aprox.)"); meta.appendChild(sizeBadge); var stateBadge = document.createElement("span"); stateBadge.className = "model-badge" + (downloaded ? " model-badge-ready" : ""); stateBadge.textContent = downloaded ? "Baixado" : "Não baixado"; meta.appendChild(stateBadge); if (isDefault) { var defaultBadge = document.createElement("span"); defaultBadge.className = "model-badge model-badge-star"; defaultBadge.textContent = "★ Padrão"; meta.appendChild(defaultBadge); } main.appendChild(meta); top.appendChild(main); var actions = document.createElement("div"); actions.className = "model-actions"; var primaryBtn = document.createElement("button"); primaryBtn.type = "button"; if (isDefault) { primaryBtn.className = "button button-primary model-btn-active"; primaryBtn.textContent = "Ativo"; primaryBtn.disabled = true; } else if (downloaded) { primaryBtn.className = "button button-ghost"; primaryBtn.textContent = "Usar como padrão"; primaryBtn.onclick = function () { setDefaultModel(model); }; } else { primaryBtn.className = "button button-primary"; primaryBtn.textContent = "Baixar"; primaryBtn.onclick = function () { downloadModel(model, primaryBtn, entry.approxMB); }; } actions.appendChild(primaryBtn); if (downloaded && !isDefault) { var deleteBtn = document.createElement("button"); deleteBtn.type = "button"; deleteBtn.className = "link-button"; deleteBtn.textContent = "Excluir"; deleteBtn.onclick = function () { deleteModel(model); }; actions.appendChild(deleteBtn); } top.appendChild(actions); row.appendChild(top); // Barra de progresso do download, na própria linha do modelo. var progress = document.createElement("div"); progress.className = "model-progress"; progress.id = "modelProgress-" + model; progress.hidden = true; progress.innerHTML = '
' + ''; row.appendChild(progress); list.appendChild(row); }); } function downloadModel(model, btn, approxMB) { btn.disabled = true; btn.classList.add("is-busy"); modelsLog("Baixando modelo " + model + "… isso pode levar alguns minutos."); var box = document.getElementById("modelProgress-" + model); var track = box ? box.querySelector(".progress-track") : null; var fill = box ? box.querySelector(".progress-fill") : null; var label = box ? box.querySelector(".model-progress-label") : null; if (box) box.hidden = false; function mirror(info) { if (!box) return; if (typeof info.pct === "number" && isFinite(info.pct)) { track.setAttribute("data-mode", "determinate"); fill.style.width = Math.max(0, Math.min(100, info.pct)) + "%"; } else { track.setAttribute("data-mode", "indeterminate"); } label.textContent = info.detail ? info.stage + " · " + info.detail : info.stage || ""; } taskStart("Baixando o modelo " + model, "Preparando o download", mirror); runStreamed( SILENCE_PYTHON_BIN, [ SILENCE_DOWNLOAD_SCRIPT, "--model", model, "--models-path", effectiveModelsPath(), "--approx-mb", String(approxMB || 0), ], function (line) { modelsLog(line); }, function (code, lastErr) { btn.classList.remove("is-busy"); if (code !== 0) { if (box) box.hidden = true; modelsLog("Falha ao baixar " + model + " (código " + code + ").", "err"); taskEnd(false, "Falha ao baixar o modelo " + model + " (código " + code + ").", lastErr); btn.disabled = false; return; } modelsLog("Modelo " + model + " baixado com sucesso.", "ok"); taskEnd(true, "Modelo " + model + " pronto para uso."); renderModelList(); }, hfSpawnOptions() ); } // ---- Init ---- (function init() { // A identificação de locutores pertence ao Scanner; abrir nele evita que o // painel pareça travado na etapa antiga de edição de vídeo. switchTab("projeto"); // Set the default temp dir in the input field document.getElementById("tempDir").value = tempDir; // Restore saved temp dir try { var saved = localStorage.getItem("mcp_bridge_temp_dir"); if (saved) { tempDir = saved; document.getElementById("tempDir").value = tempDir; } } catch (e) {} // Restore saved HF token (masked) and show its status var savedToken = loadHfToken(); if (savedToken) { document.getElementById("hfToken").value = savedToken; setHfTokenStatus("unknown", "Salvo — clique em Validar para confirmar"); } else { setHfTokenStatus("unset", "Não configurado"); } var savedGroqApiKey = loadGroqApiKey(); document.getElementById("groqApiKey").value = savedGroqApiKey; if (savedGroqApiKey) { document.getElementById("groqApiKeyStatus").setAttribute("data-state", "unknown"); document.getElementById("groqApiKeyStatusText").textContent = "Salva"; } document.getElementById("modelsPath").value = effectiveModelsPath(); renderLanguageOptions(); document.getElementById("transcribeLanguage").value = loadLanguage(); renderModelList(); syncTranscribeModelSelect(); syncLanguageInfo(); renderEditorPersonalities(); restaurarConfiguracaoDeLegendas(); renderizarTiposDeVideoParaEdicao(); // A aba Editar vídeo começa com a análise já produzida pelo Scanner. setStep(1, "locked", "Scanner"); setStep(2, "locked", "Scanner"); setStep(3, "locked", "Aguardando análise"); setStep(2, "locked", "Aguardando análise"); setStep(3, "locked", "Aguardando análise"); log("MCP for Adobe Premiere Pro CEP connector loaded"); setStatus("waiting", "Aguardando — a ponte está iniciando"); // Always auto-start. The headless instance (StartOn ApplicationActivate) has no // one to click Start, and macOS periodically purges the temp dir — so create it // rather than gating auto-start on its existence. ensureDir(tempDir); startBridgeHeartbeat(); log("Auto-starting bridge..."); setTimeout(startBridge, 500); if (!restoreScheduledUpdateStatus()) setTimeout(checkForUpdates, 1200); })();