primocode 11.1.0 → 11.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +16 -2
  2. package/bin/primocode-rpc.js +100 -0
  3. package/lib/agentes-pessoais.js +120 -0
  4. package/lib/app-server/cli.js +64 -0
  5. package/lib/app-server/enderecos.js +43 -0
  6. package/lib/app-server/index.js +3 -0
  7. package/lib/app-server/pareamento.js +61 -0
  8. package/lib/app-server/protocolo.js +86 -0
  9. package/lib/app-server/servidor.js +805 -0
  10. package/lib/app-server/websocket.js +87 -0
  11. package/lib/avisos.js +65 -0
  12. package/lib/cli/sessao.js +202 -17
  13. package/lib/cli-nova.js +237 -0
  14. package/lib/config/config.js +22 -2
  15. package/lib/diff.js +12 -2
  16. package/lib/motor/aprovacao.js +114 -0
  17. package/lib/motor/eventos.js +77 -0
  18. package/lib/motor/index.js +8 -0
  19. package/lib/motor/tela-terminal.js +64 -0
  20. package/lib/motor/transcricao.js +57 -0
  21. package/lib/motor/turno.js +43 -0
  22. package/lib/motor/voz.js +65 -0
  23. package/lib/piloto.js +39 -0
  24. package/lib/plugin/entregaveis/nucleo.js +2 -2
  25. package/lib/plugin/estudio/criar.js +113 -0
  26. package/lib/plugin/estudio/gosto.js +3 -0
  27. package/lib/plugin/estudio/historico.js +60 -0
  28. package/lib/plugin/estudio/index.js +14 -1
  29. package/lib/plugin/estudio/kits/tela.html +3 -1
  30. package/lib/plugin/estudio/miniatura.js +56 -0
  31. package/lib/plugin/video/auto.js +1 -1
  32. package/lib/plugin/video/catalogo.js +20 -0
  33. package/lib/plugin/video/mutar.js +28 -0
  34. package/lib/plugin/video/ops-fx.js +6 -6
  35. package/lib/plugin/video/ops.js +2 -13
  36. package/lib/plugin/video/pedido.js +1 -1
  37. package/lib/plugin/video/zoom.js +1 -1
  38. package/lib/projetos-meta.js +107 -0
  39. package/lib/qr.js +209 -0
  40. package/lib/session/runner/act.js +127 -68
  41. package/lib/skill/catalogo.js +3 -1
  42. package/lib/tool/tools.js +7 -0
  43. package/lib/tui/entrada.js +143 -18
  44. package/lib/tui/tela.js +312 -49
  45. package/lib/tui/ui.js +78 -4
  46. package/package.json +10 -2
  47. package/voz/primo_voz/transcrever.py +12 -0
@@ -0,0 +1,43 @@
1
+ // Um turno sem tela: chama o runAct e emite o ciclo do turno.
2
+ 'use strict';
3
+
4
+ const { EVENTOS, novoId } = require('./eventos');
5
+
6
+ /* `iniciarTurno({ texto, ...opts, emitir })`. Sem `emitir`, usa a tela de terminal.
7
+ `opts` são os mesmos do runAct (server, model, token, history, projectDir…). */
8
+ async function iniciarTurno({ texto, emitir, portao, ...opts }) {
9
+ const act = require('../session/runner/act');
10
+ const saida = emitir || require('./tela-terminal').criar();
11
+ const id = novoId();
12
+ const t0 = Date.now();
13
+ saida({ evento: EVENTOS.TURNO_COMECOU, id, tipo: 'turno', estado: 'rodando', texto });
14
+ let r;
15
+ try {
16
+ // Com `portao` (lib/motor/aprovacao.js), aprovações e perguntas viram eventos.
17
+ const viaPortao = portao ? { autorizar: portao.autorizar, mudarPasta: opts.mudarPasta || portao.mudarPasta } : {};
18
+ r = await act.runAct(texto, { ...opts, ...viaPortao, emitir: saida });
19
+ } catch (erro) {
20
+ if (portao) portao.cancelar();
21
+ saida({ evento: EVENTOS.TURNO_INTERROMPIDO, id, tipo: 'turno', estado: 'erro', duracao: Date.now() - t0, erro });
22
+ throw erro;
23
+ }
24
+ const duracao = Date.now() - t0;
25
+ if (portao) portao.cancelar();
26
+ // Contexto da conversa para a status line (régua do /compact, lib/memoria-conversa.js).
27
+ try {
28
+ const memoria = require('../session/memoria-conversa');
29
+ const hist = (opts.history || []).concat(r && r.userMessage ? [r.userMessage] : [], r && r.assistantMessage ? [r.assistantMessage] : []);
30
+ saida({ evento: EVENTOS.USO, id: novoId(), tipo: 'uso', estado: 'ok', contexto: memoria.usoDoContexto(memoria.nova(), hist) });
31
+ } catch { /* sem medida, a status line fica sem o número */ }
32
+ if (act.interrompido()) {
33
+ saida({ evento: EVENTOS.TURNO_INTERROMPIDO, id, tipo: 'turno', estado: 'interrompido', duracao, resultado: r });
34
+ } else {
35
+ saida({ evento: EVENTOS.TURNO_TERMINOU, id, tipo: 'turno', estado: 'ok', duracao, resultado: r });
36
+ }
37
+ return r;
38
+ }
39
+
40
+ const interromper = (...a) => require('../session/runner/act').interromper(...a);
41
+ const mandarDuranteATarefa = (...a) => require('../session/runner/act').mandarDuranteATarefa(...a);
42
+
43
+ module.exports = { iniciarTurno, interromper, mandarDuranteATarefa };
@@ -0,0 +1,65 @@
1
+ // Voz do app (docs/estrutura.md 6.9): interface com provedor trocável.
2
+ // DECISÃO FINAL DO PROVEDOR PENDENTE com o dono. Aqui só o mínimo que não trava a escolha:
3
+ // - fala (TTS): provedor "kokoro" (voz local do Estúdio, grátis, gera arquivo de áudio; este módulo NUNCA toca som);
4
+ // - escuta (STT): fica no renderer (Web Speech API do navegador); o motor só informa que é do navegador.
5
+ // Provedor = { nome, disponivel() -> {ok, motivo?}, falar(texto, arquivo) -> Promise<{ok, error?}> }.
6
+ 'use strict';
7
+
8
+ const fs = require('fs');
9
+ const os = require('os');
10
+ const path = require('path');
11
+
12
+ const TETO_TEXTO = 600;
13
+
14
+ function provedorKokoro({ uv, voz, executar } = {}) {
15
+ const K = require('../plugin/estudio/kokoro');
16
+ const achar = () => uv || (() => { try { return require('child_process').execFileSync('which', ['uv'], { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim() || null; } catch { return null; } })();
17
+ return {
18
+ nome: 'kokoro',
19
+ disponivel() {
20
+ if (!achar()) return { ok: false, motivo: 'falta o uv (voz local)' };
21
+ if (!K.acharEspeak()) return { ok: false, motivo: 'falta o espeak-ng (brew install espeak-ng)' };
22
+ return { ok: true };
23
+ },
24
+ async falar(texto, arquivo) {
25
+ const r = await K.narrar([{ texto }], arquivo, { voz, uv: achar(), ...(executar ? { executar } : {}) });
26
+ return r && r.ok ? { ok: true } : { ok: false, error: (r && r.error) || 'a voz não respondeu' };
27
+ },
28
+ };
29
+ }
30
+
31
+ const PROVEDORES = { kokoro: (o) => provedorKokoro(o) };
32
+
33
+ function criarVoz({ provedor, pasta, escuta = 'navegador' } = {}) {
34
+ const prov = provedor || PROVEDORES.kokoro();
35
+ const saida = pasta || path.join(os.tmpdir(), 'primocode-voz');
36
+ let ativa = false;
37
+ let n = 0;
38
+
39
+ const estado = () => {
40
+ const d = prov.disponivel();
41
+ return {
42
+ ativa, provedor: prov.nome, fala: { disponivel: !!d.ok, motivo: d.ok ? undefined : d.motivo },
43
+ escuta, decisaoPendente: true,
44
+ };
45
+ };
46
+
47
+ return {
48
+ estado,
49
+ /** Liga o modo voz; com `texto`, gera o arquivo de áudio da fala (não toca). */
50
+ async iniciar({ texto } = {}) {
51
+ ativa = true;
52
+ const e = estado();
53
+ const t = typeof texto === 'string' ? texto.trim().slice(0, TETO_TEXTO) : '';
54
+ if (!t) return e;
55
+ if (!e.fala.disponivel) return { ...e, fala: { ...e.fala, gerou: false } };
56
+ fs.mkdirSync(saida, { recursive: true });
57
+ const arquivo = path.join(saida, `fala-${Date.now()}-${++n}.mp3`);
58
+ const r = await prov.falar(t, arquivo);
59
+ return r.ok ? { ...e, arquivo } : { ...e, fala: { ...e.fala, gerou: false, motivo: r.error } };
60
+ },
61
+ parar() { ativa = false; return estado(); },
62
+ };
63
+ }
64
+
65
+ module.exports = { criarVoz, provedorKokoro, PROVEDORES, TETO_TEXTO };
package/lib/piloto.js ADDED
@@ -0,0 +1,39 @@
1
+ // Piloto automático (docs/estrutura.md, seção 9): os agentes ligados procuram o que fazer e fazem.
2
+ // Este módulo só decide QUEM roda e QUANDO parar; quem executa é injetado (o app-server passa o motor).
3
+ // Node puro. Um ciclo roda os agentes ligados um de cada vez e para no freio da cota.
4
+ 'use strict';
5
+
6
+ /* O que o agente recebe quando ninguém pediu nada: procurar trabalho no seu terreno. */
7
+ function pedidoDoPiloto(agente, { grupo = false } = {}) {
8
+ return `[piloto automático] Você é @${agente.nome}. Procure UMA coisa útil para fazer no seu terreno `
9
+ + `(${agente.onde && agente.onde !== 'fora' ? `projeto ${agente.onde}` : 'fora de projetos'}) e faça. `
10
+ + 'Tudo que escrever arquivo vai para a pessoa aprovar antes de valer. Se não houver nada a fazer, diga isso em uma frase.'
11
+ + (grupo ? ' Se precisar de outro agente ligado, chame com spawn_agent (profundidade 1).' : '');
12
+ }
13
+
14
+ /* Freio padrão: a cota do plano (lib/limites.js). Sem rede vale o modo local generoso. */
15
+ async function freioDaCota() {
16
+ try {
17
+ const c = await require('./config/limites').situacao();
18
+ if (c && (c.ok === false || c.liberado === false || c.restam === 0)) return { parar: true, motivo: 'a cota do plano acabou' };
19
+ } catch { /* sem leitura, não barra */ }
20
+ return { parar: false };
21
+ }
22
+
23
+ /* `ciclo({ agentes, executar, freio, ocupado, grupo })` → { executados:[{agente,ok,resumo}], parou? }.
24
+ `executar(agente, pedido)` devolve o resumo (texto) ou lança. */
25
+ async function ciclo({ agentes = [], executar, freio = freioDaCota, ocupado = () => false, grupo = false } = {}) {
26
+ const ligados = agentes.filter((a) => a.ligado);
27
+ const r = { executados: [], parou: null };
28
+ if (!ligados.length) { r.parou = 'nenhum agente ligado'; return r; }
29
+ for (const a of ligados) {
30
+ if (ocupado()) { r.parou = 'a pessoa está trabalhando'; break; }
31
+ const f = await freio();
32
+ if (f && f.parar) { r.parou = f.motivo || 'freio'; break; }
33
+ try { r.executados.push({ agente: a.nome, ok: true, resumo: String(await executar(a, pedidoDoPiloto(a, { grupo })) || '').slice(0, 400) }); }
34
+ catch (e) { r.executados.push({ agente: a.nome, ok: false, resumo: (e && e.message) || 'falhou' }); }
35
+ }
36
+ return r;
37
+ }
38
+
39
+ module.exports = { ciclo, pedidoDoPiloto, freioDaCota };
@@ -103,7 +103,7 @@ function acharNavegador(env = process.env, existe = require('fs').existsSync) {
103
103
  function htmlParaPdf(htmlPath, pdfPath, { navegador = acharNavegador() } = {}) {
104
104
  if (!navegador) return { ok: false, erro: 'Chrome/Chromium não encontrado (defina PRIMO_CHROME)' };
105
105
  const { spawnSync } = require('child_process');
106
- const r = spawnSync(navegador, ['--headless=new', '--disable-gpu', '--no-pdf-header-footer',
106
+ const r = spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--no-pdf-header-footer',
107
107
  `--print-to-pdf=${pdfPath}`, 'file://' + htmlPath], { encoding: 'utf8', timeout: 90000 });
108
108
  return require('fs').existsSync(pdfPath) ? { ok: true } : { ok: false, erro: (r.stderr || '').slice(-300) || 'PDF não saiu' };
109
109
  }
@@ -180,7 +180,7 @@ function htmlParaPngs(htmlPath, baseSemExt, n, w, h, { escala = 1, navegador = a
180
180
  const tmp = `${baseSemExt}.tmp-${i}.html`;
181
181
  const alvo = `${baseSemExt}-${String(i).padStart(2, '0')}.png`;
182
182
  fs.writeFileSync(tmp, html.replace('</head>', `<style>section:not(:nth-of-type(${i})){display:none!important}html,body{height:${h}px;overflow:hidden}</style></head>`));
183
- spawnSync(navegador, ['--headless=new', '--disable-gpu', '--hide-scrollbars', `--force-device-scale-factor=${escala}`,
183
+ spawnSync(navegador, ['--headless=new', '--use-mock-keychain', '--password-store=basic', '--disable-gpu', '--hide-scrollbars', `--force-device-scale-factor=${escala}`,
184
184
  `--window-size=${w},${h}`, `--screenshot=${alvo}`, 'file://' + tmp], { encoding: 'utf8', timeout: 90000 });
185
185
  try { fs.unlinkSync(tmp); } catch {}
186
186
  if (fs.existsSync(alvo)) arquivos.push(alvo); else erro = `PNG ${i} não saiu`;
@@ -0,0 +1,113 @@
1
+ // estudio_criar: a porta de entrada simples do Estúdio (docs/estrutura.md, seção 8).
2
+ // Quatro parâmetros fixos (tipo, briefing, duracao, formato); por dentro escolhe a receita,
3
+ // abre o projeto e chama as ferramentas que já existem (estudio_plano, estudio_motion,
4
+ // estudio_render). Não reescreve nada do Estúdio. As dependências entram por parâmetro,
5
+ // para o teste rodar sem rede e sem render.
6
+ 'use strict';
7
+
8
+ const path = require('path');
9
+
10
+ /* tipo → receita. `via`: 'roteiro' (plano pelo roteirista + render) ou 'motion' (a máquina de motion). */
11
+ const TIPOS = Object.freeze({
12
+ slides: { rotulo: 'Slides', via: 'roteiro', formato: '16:9', projeto: 'slides' },
13
+ video: { rotulo: 'Vídeo curto', via: 'roteiro', formato: '9:16', projeto: 'video', duracao: [15, 60, 30] },
14
+ motion: { rotulo: 'Motion', via: 'motion', formato: '9:16', duracao: [6, 60, 15] },
15
+ anuncio: { rotulo: 'Anúncio', via: 'motion', formato: '9:16', duracao: [10, 30, 15], prefixo: 'Anúncio para chamar pra comprar: ' },
16
+ abertura: { rotulo: 'Abertura de canal', via: 'motion', formato: '16:9', duracao: [5, 5, 5], prefixo: 'Abertura de canal com a marca: ' },
17
+ });
18
+ const FORMATOS = ['9:16', '16:9', '1:1'];
19
+
20
+ const limitar = (n, [min, max, padrao]) => { n = Number(n); return Number.isFinite(n) && n > 0 ? Math.min(max, Math.max(min, Math.round(n))) : padrao; };
21
+
22
+ /** Confere os parâmetros. Devolve { ok, tipo, briefing, duracao, formato } ou { ok:false, error }. */
23
+ function normalizar(args = {}) {
24
+ const tipo = String(args.tipo || '').toLowerCase().trim();
25
+ const receita = TIPOS[tipo];
26
+ if (!receita) return { ok: false, error: `tipo "${args.tipo || ''}" não existe. Use um de: ${Object.keys(TIPOS).join(', ')}.` };
27
+ const briefing = String(args.briefing || '').replace(/\s+/g, ' ').trim();
28
+ if (briefing.length < 3) return { ok: false, error: 'falta o briefing: diga em uma frase o que criar.' };
29
+ const formato = FORMATOS.includes(String(args.formato)) ? String(args.formato) : receita.formato;
30
+ const duracao = receita.duracao ? limitar(args.duracao, receita.duracao) : undefined;
31
+ return { ok: true, tipo, receita, briefing: briefing.slice(0, 600), formato, duracao };
32
+ }
33
+
34
+ /** Os arquivos entregues, com o tipo que a tela sabe abrir. */
35
+ function arquivosDe(r) {
36
+ const lista = [];
37
+ const por = (caminho, extra) => { if (caminho) lista.push({ nome: path.basename(caminho), caminho, tipo: tipoDoArquivo(caminho), ...extra }); };
38
+ por(r.arquivo || r.video);
39
+ if (r.entrega && r.entrega.pasta && Array.isArray(r.entrega.arquivos)) {
40
+ for (const n of r.entrega.arquivos) { const c = path.join(r.entrega.pasta, n); if (!lista.some((x) => x.caminho === c)) por(c); }
41
+ }
42
+ return lista;
43
+ }
44
+ function tipoDoArquivo(c) {
45
+ const e = path.extname(c).toLowerCase();
46
+ if (e === '.mp4' || e === '.webm' || e === '.mov') return 'video';
47
+ if (e === '.pdf') return 'pdf';
48
+ if (['.png', '.jpg', '.jpeg', '.webp', '.gif'].includes(e)) return 'imagem';
49
+ if (e === '.html') return 'pagina';
50
+ return 'arquivo';
51
+ }
52
+
53
+ /**
54
+ * @param {object} args { tipo, briefing, duracao?, formato? }
55
+ * @param {object} deps { projeto, plano(args), motion(args), render(args), progresso(pct, etapa) }
56
+ */
57
+ async function criar(args, deps) {
58
+ const n = normalizar(args);
59
+ if (!n.ok) return n;
60
+ const { tipo, receita, briefing, formato, duracao } = n;
61
+ const progresso = deps.progresso || (() => {});
62
+ const falha = (error, extra) => ({ ok: false, error, ...extra });
63
+
64
+ progresso(5, 'Abrindo o projeto');
65
+ const pedido = `${receita.prefixo || ''}${briefing}`;
66
+ const titulo = briefing.slice(0, 60);
67
+ const proj = deps.projeto.criar({ titulo, pedido: `${pedido} (${receita.rotulo}, ${formato}${duracao ? `, ${duracao} s` : ''})`, formato: receita.projeto || 'video' });
68
+ if (!proj || proj.ok === false) return falha((proj && proj.error) || 'não consegui abrir o projeto.');
69
+ const id = proj.id;
70
+ const base = { tipo, id, pasta: proj.pasta, formato, duracao, titulo };
71
+
72
+ if (receita.via === 'motion') {
73
+ progresso(15, 'Montando as cenas de motion');
74
+ const m = await deps.motion({ id, assunto: pedido, formato, duracao });
75
+ if (!m || m.ok === false) return falha((m && m.error) || 'o motion não saiu.', base);
76
+ progresso(100, 'Pronto');
77
+ return { ok: true, ...base, duracao: m.duracao || duracao, arquivo: m.video, arquivos: arquivosDe(m), resumo: `${receita.rotulo} de ${m.duracao || duracao} s.` };
78
+ }
79
+
80
+ progresso(15, 'Escrevendo o roteiro');
81
+ const pl = await deps.plano({ id, pedido: `${pedido}. Formato ${formato}.`, segundos: duracao });
82
+ if (!pl || pl.ok === false) return falha((pl && pl.error) || 'não consegui escrever o plano.', base);
83
+ progresso(40, tipo === 'slides' ? 'Desenhando os slides' : 'Montando o vídeo');
84
+ const r = await deps.render({ id });
85
+ if (!r || r.ok === false) return falha((r && r.error) || 'o render não saiu.', { ...base, problemas: r && r.problemas });
86
+ progresso(100, 'Pronto');
87
+ return {
88
+ ok: true, ...base, arquivo: r.arquivo, arquivos: arquivosDe(r), paginas: r.paginas,
89
+ duracao: r.segundos || duracao,
90
+ resumo: r.paginas ? `${r.paginas} slides em PDF.` : `${receita.rotulo} de ${r.segundos || duracao} s.`,
91
+ };
92
+ }
93
+
94
+ module.exports = { criar, normalizar, TIPOS, FORMATOS, tipoDoArquivo, arquivosDe };
95
+
96
+ /* Os "quadrados" do Studio no app viram `tipo` no turn/start. O motor traduz o tipo em instrução:
97
+ o modelo escolhe o caminho (a IA decide); aqui só o cardápio do tipo. Carrossel não tem receita no Estúdio
98
+ (conferido na fase 7), por isso não há quadrado para ele. */
99
+ const QUADRADOS = Object.freeze({
100
+ slides: 'Tipo escolhido na tela: Apresentação de slides. Use estudio_criar com tipo "slides" e o briefing do pedido (a skill slides-que-prendem ajuda no conteúdo).',
101
+ video: 'Tipo escolhido na tela: Vídeo curto vertical. Use estudio_criar com tipo "video" e o briefing do pedido.',
102
+ motion: 'Tipo escolhido na tela: Motion. Use estudio_criar com tipo "motion" e o briefing do pedido.',
103
+ anuncio: 'Tipo escolhido na tela: Anúncio de 15 s. Use estudio_criar com tipo "anuncio" e o briefing do pedido.',
104
+ abertura: 'Tipo escolhido na tela: Abertura de canal de 5 s. Use estudio_criar com tipo "abertura" e o briefing do pedido.',
105
+ pagina: 'Tipo escolhido na tela: Página ou app. Construa com write_file na pasta do projeto, confira abrindo no navegador e entregue o arquivo principal. Não use o Estúdio.',
106
+ bug: 'Tipo escolhido na tela: Corrigir um bug. Ache a causa lendo o código do projeto (read_file, grep), corrija com edit_file e crie um teste que pegue o caso; rode os testes e conte o que mudou. Não use o Estúdio.',
107
+ pr: 'Tipo escolhido na tela: Revisar um PR. Leia o diff do PR (o número ou o link que a pessoa der; gh ou git diff), aponte os riscos do mais grave ao menos grave e sugira a correção. Não use o Estúdio.',
108
+ app: 'Tipo escolhido na tela: Página ou app. Construa com write_file na pasta do projeto, confira abrindo no navegador e entregue o arquivo principal. Não use o Estúdio.',
109
+ });
110
+ const instrucaoDoTipo = (t) => QUADRADOS[String(t || '').toLowerCase()] || (t ? `Tipo de trabalho escolhido na tela: ${String(t).slice(0, 40)}` : '');
111
+
112
+ module.exports.QUADRADOS = QUADRADOS;
113
+ module.exports.instrucaoDoTipo = instrucaoDoTipo;
@@ -519,6 +519,9 @@ function abrirTela({ home, abrir, timeoutMs = 10 * 60 * 1000, porta = 0, inicio
519
519
  const token = crypto.randomBytes(16).toString('hex');
520
520
  let html;
521
521
  try { html = fs.readFileSync(path.join(__dirname, 'gosto', 'tela.html'), 'utf8'); } catch { return resolve({ ok: false, motivo: 'sem_tela' }); }
522
+ // No app do PrimoCode a tela abre num quadro (iframe) e o "Finalizar" (window.close) não faz nada ali: avisa a janela de fora
523
+ // para fechar o quadro. Entra na hora de servir, porque o gosto/tela.html é guardado sem mudança (test/kits-tela.test.js confere o sha1).
524
+ html = html.replace('</body>', "<script>document.addEventListener('click',function(e){var b=e.target.closest&&e.target.closest('#finalizar');if(b&&window.parent!==window)window.parent.postMessage('primocode:tela-fim','*');},true);</script></body>");
522
525
  // os vídeos que já estão no gosto: quem abre o /referencias de novo vê o que já trouxe (só o nome)
523
526
  const jaTem = ((ler(home) || {}).referencias || []).map((r) => String(r.nome || 'vídeo').slice(0, 80));
524
527
  const dados = JSON.stringify({ ESTILOS, GOSTEI, NAO_QUERO, INICIO: inicio === 'referencias' ? 'referencias' : 'carrossel', JA_TEM: jaTem }).replace(/</g, '\\u003c');
@@ -0,0 +1,60 @@
1
+ // Histórico do Studio no app (studio/history): o que o Estúdio já gerou, com miniatura.
2
+ // Lê as pastas de projeto (lib/estudio/projeto.js) sem mudar nada. Node puro.
3
+ 'use strict';
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { tipoDoArquivo } = require('./criar');
8
+ const Miniatura = require('./miniatura');
9
+
10
+ const PULAR = new Set(['node_modules', 'remotion', 'midia', 'entrada', 'versoes', '.git']);
11
+ const SAIDAS = new Set(['video', 'pdf', 'imagem']);
12
+
13
+ /* Procura as saídas em até 2 níveis; imagem solta (quadro de conferência) serve de miniatura, não de entrega. */
14
+ function varrer(pasta, nivel = 0, achados = []) {
15
+ let itens = [];
16
+ try { itens = fs.readdirSync(pasta, { withFileTypes: true }); } catch { return achados; }
17
+ for (const d of itens) {
18
+ const c = path.join(pasta, d.name);
19
+ if (d.isDirectory()) { if (nivel < 2 && !PULAR.has(d.name) && !d.name.startsWith('.')) varrer(c, nivel + 1, achados); continue; }
20
+ const tipo = tipoDoArquivo(c);
21
+ if (!SAIDAS.has(tipo)) continue;
22
+ let st; try { st = fs.statSync(c); } catch { continue; }
23
+ achados.push({ nome: d.name, caminho: c, tipo, bytes: st.size, quando: st.mtimeMs, dentro: path.relative(pasta, c) });
24
+ }
25
+ return achados;
26
+ }
27
+
28
+ /** { itens, total }. `busca` filtra por título e pedido (sem acento); `limite` padrão 60. */
29
+ function historico({ busca = '', limite = 60, projeto, miniaturaDeVideo } = {}) {
30
+ // Vídeo na grade vira um JPG (cache ao lado do vídeo). Até 4 saem na hora; o resto sai em segundo plano e aparece na próxima abertura.
31
+ const quadro = miniaturaDeVideo || Miniatura.gerar;
32
+ let gerados = 0;
33
+ const P = projeto || require('./projeto');
34
+ const sem = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
35
+ const q = sem(busca).trim();
36
+ const itens = [];
37
+ for (const p of P.listar({ limite: 400 })) {
38
+ if (q && !sem(`${p.titulo} ${p.pedido}`).includes(q)) continue;
39
+ const todos = varrer(p.pasta).sort((a, b) => b.quando - a.quando);
40
+ const entregas = todos.filter((a) => a.tipo !== 'imagem' || /^saida\b/.test(a.dentro) || !todos.some((x) => x.tipo !== 'imagem'));
41
+ const principal = entregas.find((a) => a.tipo === 'video') || entregas.find((a) => a.tipo === 'pdf') || entregas[0] || null;
42
+ const imagem = todos.find((a) => a.tipo === 'imagem');
43
+ let miniatura = null;
44
+ if (principal && principal.tipo === 'imagem') miniatura = { tipo: 'imagem', caminho: principal.caminho };
45
+ else if (principal && principal.tipo === 'video') {
46
+ let jpg = Miniatura.emCache(principal.caminho);
47
+ if (!jpg && gerados < 4) { gerados++; jpg = quadro(principal.caminho); }
48
+ else if (!jpg && !miniaturaDeVideo) Miniatura.gerarDepois(principal.caminho);
49
+ miniatura = jpg ? { tipo: 'imagem', caminho: jpg, deVideo: true } : (imagem ? { tipo: 'imagem', caminho: imagem.caminho } : { tipo: 'video-sem-quadro', caminho: principal.caminho });
50
+ } else if (imagem) miniatura = { tipo: 'imagem', caminho: imagem.caminho };
51
+ itens.push({
52
+ id: p.id, titulo: p.titulo, formato: p.formato || 'video', criado: p.criado, mudado: p.mudado || p.criado,
53
+ pasta: p.pasta, arquivos: entregas.map(({ dentro, ...a }) => a), principal: principal ? principal.caminho : null, miniatura,
54
+ pronto: entregas.length > 0,
55
+ });
56
+ }
57
+ return { itens: itens.slice(0, Math.max(1, Number(limite) || 60)), total: itens.length };
58
+ }
59
+
60
+ module.exports = { historico, varrer };
@@ -748,6 +748,19 @@ async function renderMotion(args, p, receita) {
748
748
  return { ...r, ...(gostoResumo ? { gosto: gostoResumo } : {}), nota: 'Abra os quadros (png em ae/) para conferir. Se algo ficou torto, mude o JSON e rode de novo.' };
749
749
  }
750
750
 
751
+ // ── estudio_criar ────────────────────────────────────────────────────────
752
+
753
+ /** A porta simples (criar.js): escolhe a receita e chama as ferramentas de cima. `ctx.progresso(pct, etapa)` alimenta o item da tela. */
754
+ async function ferramentaCriar(args, ctx = {}) {
755
+ return require('./criar.js').criar(args, {
756
+ projeto,
757
+ plano: (a) => ferramentaPlano({ ...a, __pasta: ctx.projectDir }),
758
+ motion: (a) => ferramentaMotion(a),
759
+ render: (a) => ferramentaRender({ ...a, __pasta: ctx.projectDir }, ctx),
760
+ progresso: ctx.progresso,
761
+ });
762
+ }
763
+
751
764
  // ── estudio_editar ───────────────────────────────────────────────────────
752
765
 
753
766
  /** O editor por comando: reabre o plano da máquina de motion, aplica as mudanças e guarda a versão (editor.js). */
@@ -2706,7 +2719,7 @@ async function ferramentaSite(args) {
2706
2719
 
2707
2720
  module.exports = { proporcaoDoPedido,
2708
2721
  vazamentoNoComeco, variarIcones, objetosDoRoteiro,
2709
- ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaAE, ferramentaMotion, ferramentaEditar, ferramentaNarrar,
2722
+ ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaAE, ferramentaMotion, ferramentaEditar, ferramentaNarrar, ferramentaCriar,
2710
2723
  ferramentaVideo, ferramentaRender, ferramentaSite,
2711
2724
  projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
2712
2725
  };
@@ -380,9 +380,11 @@ function mandar(rota, corpo) {
380
380
  }
381
381
  function erro(msg) { const e = $('#erro'); e.hidden = !msg; e.textContent = msg || ''; }
382
382
 
383
+ /* No app do PrimoCode a tela abre dentro de um quadro (iframe): avisa a janela de fora que acabou, pra ela fechar o quadro. */
384
+ function avisarApp() { try { if (window.parent !== window) setTimeout(() => window.parent.postMessage('primocode:tela-fim', '*'), 1200); } catch { /* aberta solta no navegador */ } }
383
385
  function mostrar(n) {
384
386
  [1, 4].forEach((i) => { $('#passo' + i).hidden = i !== n; });
385
- if (n === 4) { $('#titulo').textContent = 'Pronto. O PrimoCode já sabe o estilo.'; $('#sub').hidden = true; }
387
+ if (n === 4) { $('#titulo').textContent = 'Pronto. O PrimoCode já sabe o estilo.'; $('#sub').hidden = true; avisarApp(); }
386
388
  window.scrollTo({ top: 0, behavior: parar ? 'auto' : 'smooth' });
387
389
  }
388
390
 
@@ -0,0 +1,56 @@
1
+ // Miniatura de vídeo para a grade do Studio: um quadro em JPG, gerado pelo ffmpeg e guardado em cache ao lado
2
+ // do vídeo (<pasta do vídeo>/.miniaturas/<nome>.jpg). A tela mostra <img>, nunca <video> na grade: vídeo em formato
3
+ // que o Chromium não decodifica loga "Unsupported pixel format" só de abrir. Node puro, só `ffmpeg` do sistema.
4
+ 'use strict';
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { spawn, spawnSync } = require('child_process');
9
+
10
+ const PASTA = '.miniaturas';
11
+ const LARGURA = 480;
12
+ const emAndamento = new Set();
13
+
14
+ const destinoDe = (video) => path.join(path.dirname(video), PASTA, `${path.basename(video)}.jpg`);
15
+ const argsDe = (video, saida) => ['-v', 'error', '-ss', '0.5', '-i', video, '-frames:v', '1', '-vf', `scale=${LARGURA}:-2`, '-q:v', '4', '-y', saida];
16
+
17
+ /* O cache vale enquanto o JPG existe, tem conteúdo e não é mais velho que o vídeo. */
18
+ function emCache(video) {
19
+ const jpg = destinoDe(video);
20
+ try {
21
+ const j = fs.statSync(jpg); const v = fs.statSync(video);
22
+ return j.size > 0 && j.mtimeMs >= v.mtimeMs ? jpg : null;
23
+ } catch { return null; }
24
+ }
25
+
26
+ function ffmpegExiste(ffmpeg = 'ffmpeg') {
27
+ try { return spawnSync(ffmpeg, ['-version'], { stdio: 'ignore', timeout: 4000 }).status === 0; } catch { return false; }
28
+ }
29
+
30
+ /* Gera agora (bloqueia até uns segundos). Devolve o caminho do JPG, ou null se não deu. */
31
+ function gerar(video, { ffmpeg = 'ffmpeg', espera = 8000 } = {}) {
32
+ const hit = emCache(video);
33
+ if (hit) return hit;
34
+ const jpg = destinoDe(video);
35
+ try { fs.mkdirSync(path.dirname(jpg), { recursive: true }); } catch { return null; }
36
+ try { spawnSync(ffmpeg, argsDe(video, jpg), { stdio: 'ignore', timeout: espera }); } catch { /* sem ffmpeg */ }
37
+ const ok = emCache(video);
38
+ if (!ok) { try { fs.rmSync(jpg, { force: true }); } catch { /* nada */ } }
39
+ return ok;
40
+ }
41
+
42
+ /* Gera sem bloquear (a próxima abertura do histórico já acha o JPG). Um por vídeo ao mesmo tempo. */
43
+ function gerarDepois(video, { ffmpeg = 'ffmpeg' } = {}) {
44
+ if (emAndamento.has(video) || emCache(video)) return;
45
+ const jpg = destinoDe(video);
46
+ try { fs.mkdirSync(path.dirname(jpg), { recursive: true }); } catch { return; }
47
+ emAndamento.add(video);
48
+ let p;
49
+ try { p = spawn(ffmpeg, argsDe(video, jpg), { stdio: 'ignore' }); } catch { emAndamento.delete(video); return; }
50
+ const fim = () => { emAndamento.delete(video); if (!emCache(video)) { try { fs.rmSync(jpg, { force: true }); } catch { /* nada */ } } };
51
+ p.on('error', fim); p.on('close', fim);
52
+ const t = setTimeout(() => { try { p.kill('SIGKILL'); } catch { /* já saiu */ } }, 20000);
53
+ if (t.unref) t.unref();
54
+ }
55
+
56
+ module.exports = { gerar, gerarDepois, emCache, destinoDe, ffmpegExiste, PASTA };
@@ -301,7 +301,7 @@ const OPS = {
301
301
  const ver = plano.passos.map((p) => ({ op: p.op, motivo: p.motivo, args: p.args }));
302
302
  if (a.dryRun) return { ok: true, plano: ver, legenda: plano.legenda, segundos: 'da gravação' };
303
303
 
304
- const { OPS: TODAS } = require('./ops.js');
304
+ const TODAS = require('./catalogo.js').catalogo();
305
305
  const feitos = [];
306
306
  const roda = async (op, args, motivo) => {
307
307
  const res = await TODAS[op].run(ctx, args);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * catalogo.js — o catálogo completo das operações. ops.js registra aqui ao
3
+ * carregar; auto.js e pedido.js leem daqui para encadear operações sem
4
+ * importar ops.js (que os importa: isso fechava um ciclo).
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ let todas = null;
10
+
11
+ function registrar(ops) {
12
+ todas = ops;
13
+ }
14
+
15
+ function catalogo() {
16
+ if (!todas) throw new Error('catálogo de operações vazio: carregue lib/video/ops.js antes');
17
+ return todas;
18
+ }
19
+
20
+ module.exports = { registrar, catalogo };
@@ -0,0 +1,28 @@
1
+ /**
2
+ * mutar.js — o caminho de toda operação que MUDA o projeto: foto de antes,
3
+ * edição, validação, gravação, delta. Se qualquer passo falha, nada é gravado.
4
+ *
5
+ * Mora fora de ops.js para os módulos de operação (ops-fx, zoom) usarem sem
6
+ * importar o catálogo que os importa.
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const T = require('./timeline.js');
12
+ const { diff } = require('./delta.js');
13
+ const P = require('./projeto.js');
14
+
15
+ async function mutar(ctx, op, fn) {
16
+ const antes = T.clone(ctx.tl);
17
+ const extra = (await fn(ctx.tl)) || {};
18
+ T.ordenar(ctx.tl);
19
+ const probs = T.problemas(ctx.tl);
20
+ if (probs.length) throw T.erro(`edição recusada (nada mudou): ${probs.slice(0, 4).join('; ')}`);
21
+ P.fotografar(ctx.p, antes, op);
22
+ P.salvar(ctx.p, ctx.tl);
23
+ const d = diff(antes, ctx.tl, extra);
24
+ P.anotar(ctx.p, op, { dur: d.dur, added: (d.added || []).length, removed: (d.removed || []).length });
25
+ return d;
26
+ }
27
+
28
+ module.exports = { mutar };
@@ -215,7 +215,7 @@ const OPS = {
215
215
  ate = Math.min(asset.seconds, ate);
216
216
  if (!(ate > de + 0.5)) throw T.erro('o momento 3D precisa de pelo menos meio segundo');
217
217
  }
218
- return require('./ops.js').mutar(ctx, 'separate_layers', (tl) => {
218
+ return require('./mutar.js').mutar(ctx, 'separate_layers', (tl) => {
219
219
  const pessoa = camadaDaPessoa(tl, asset, rs);
220
220
  const out = { layers: { pessoa: pessoa.length, fundo: 'a gravação embaixo' } };
221
221
  if (a.show3d !== undefined) { C.limparPapel(tl, 'camadas3d', asset.id); C.limparGerados(tl); }
@@ -261,7 +261,7 @@ const OPS = {
261
261
  const seg = a.at === undefined ? await momento(ctx, asset, 'snap').catch(() => 0) : await momento(ctx, asset, a.at);
262
262
  const medido = await r().medirAsset(ctx.tl, arquivo);
263
263
  const ganho = ganhoDeCor(await corMedia(rs.arquivos.fundo), await corMedia(arquivo));
264
- return require('./ops.js').mutar(ctx, 'replace_background', (tl) => {
264
+ return require('./mutar.js').mutar(ctx, 'replace_background', (tl) => {
265
265
  if (!temPessoa(tl, asset.id)) camadaDaPessoa(tl, asset, rs);
266
266
  const F = quadro({ tl }, asset, seg) ?? 0;
267
267
  const fim = a.until !== undefined ? Math.round(Number(a.until)) : T.duracao(tl);
@@ -334,7 +334,7 @@ const OPS = {
334
334
  if (de === null) de = t[0];
335
335
  const ate = a.to !== undefined ? await momento(ctx, asset, a.to) : Math.min(asset.seconds, Math.max(t[1] + 0.3, de + 1.5));
336
336
  if (!(ate > de)) throw T.erro('"to" antes de "from"');
337
- return require('./ops.js').mutar(ctx, 'attach_3d', (tl) => {
337
+ return require('./mutar.js').mutar(ctx, 'attach_3d', (tl) => {
338
338
  const f = fxDe(tl, asset.id);
339
339
  const o = { id: `o${f.objetos.length + 1}`, modelo, mao, de: r2(de * 1000) / 1000, ate: r2(ate * 1000) / 1000 };
340
340
  if (a.color) o.cor = C.cor(a.color);
@@ -365,7 +365,7 @@ const OPS = {
365
365
  }
366
366
  const ateFusao = a.until !== undefined ? await momento(ctx, asset, a.until) : null;
367
367
  const depois = a.then === 'none' || a.then === false ? null : C.modeloDe(a.then || 'brain') || 'cerebro';
368
- return require('./ops.js').mutar(ctx, 'merge_on_join', (tl) => {
368
+ return require('./mutar.js').mutar(ctx, 'merge_on_join', (tl) => {
369
369
  const f = fxDe(tl, asset.id);
370
370
  // Os objetos vivos na junção ficam até ela (e se fundem).
371
371
  for (const o of f.objetos) if (o.de < junta && o.ate < junta + 0.5) o.ate = r2((junta + 0.6) * 1000) / 1000;
@@ -400,7 +400,7 @@ const OPS = {
400
400
  if (i1 > i0) ranges.push([T.origemParaQuadro(ctx.tl, c, i0), T.origemParaQuadro(ctx.tl, c, i1)]);
401
401
  }
402
402
  if (!ranges.length) return { ok: false, error: 'esse silêncio já foi cortado' };
403
- return require('./ops.js').mutar(ctx, 'cut_silence_at', (tl) => ({ silence: { de: r2(de), ate: r2(ate) }, ...E.apagarTrechos(tl, ranges) }));
403
+ return require('./mutar.js').mutar(ctx, 'cut_silence_at', (tl) => ({ silence: { de: r2(de), ate: r2(ate) }, ...E.apagarTrechos(tl, ranges) }));
404
404
  },
405
405
  },
406
406
  style_captions: {
@@ -423,7 +423,7 @@ const OPS = {
423
423
  const CY = { bottom: 0.78, baixo: 0.78, top: 0.14, topo: 0.14, center: 0.5, centro: 0.5, middle: 0.5 };
424
424
  if (pos && !seguir && CY[pos] === undefined) throw T.erro(`position "${a.position}" (use above_head, bottom, top ou center)`);
425
425
  const corPedida = a.color !== undefined ? C.cor(a.color) : undefined;
426
- return require('./ops.js').mutar(ctx, 'style_captions', (t) => {
426
+ return require('./mutar.js').mutar(ctx, 'style_captions', (t) => {
427
427
  for (const ref of alvo) {
428
428
  const c = T.achar(t, ref.id).clip;
429
429
  const st = { ...(c.text.style || {}) };
@@ -17,6 +17,7 @@ const T = require('./timeline.js');
17
17
  const E = require('./edits.js');
18
18
  const F = require('./filters.js');
19
19
  const { diff } = require('./delta.js');
20
+ const { mutar } = require('./mutar.js');
20
21
  const P = require('./projeto.js');
21
22
  const TR = require('./transcript.js');
22
23
  const A = require('./analise.js');
@@ -35,19 +36,6 @@ function alvos(tl, args, { tipos = ['video', 'image'] } = {}) {
35
36
  return refs.map((ref) => T.achar(tl, ref).clip.id);
36
37
  }
37
38
 
38
- async function mutar(ctx, op, fn) {
39
- const antes = T.clone(ctx.tl);
40
- const extra = (await fn(ctx.tl)) || {};
41
- T.ordenar(ctx.tl);
42
- const probs = T.problemas(ctx.tl);
43
- if (probs.length) throw T.erro(`edição recusada (nada mudou): ${probs.slice(0, 4).join('; ')}`);
44
- P.fotografar(ctx.p, antes, op);
45
- P.salvar(ctx.p, ctx.tl);
46
- const d = diff(antes, ctx.tl, extra);
47
- P.anotar(ctx.p, op, { dur: d.dur, added: (d.added || []).length, removed: (d.removed || []).length });
48
- return d;
49
- }
50
-
51
39
  function precisaProjeto(ctx) {
52
40
  if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."} (cria o projeto) ou new');
53
41
  }
@@ -311,6 +299,7 @@ const PED = require('./pedido.js');
311
299
  const AUTO = require('./auto.js');
312
300
  const ZOOM = require('./zoom.js'); // o zoom que segue o cursor (gravação de tela): fica na ajuda principal
313
301
  Object.assign(OPS, FXO.OPS, PED.OPS, AUTO.OPS, ZOOM.OPS);
302
+ require('./catalogo.js').registrar(OPS); // auto e pedido encadeiam operações por aqui
314
303
 
315
304
  const ALIAS = { timeline: 'get_timeline', transcript: 'get_transcript', captions: 'add_captions', texts: 'add_texts', render: 'export', pedido: 'do', analyze: 'track' };
316
305
 
@@ -152,7 +152,7 @@ const OPS = {
152
152
  const resumo = plano.passos.map((p) => ({ op: p.op, args: p.args }));
153
153
  if (a.dryRun) return { ok: true, plano: resumo, ...(plano.avisos.length ? { avisos: plano.avisos } : {}) };
154
154
  if (!tl) throw T.erro('nenhum projeto aberto: import {"path":"..."} primeiro');
155
- const { OPS: TODAS } = require('./ops.js');
155
+ const TODAS = require('./catalogo.js').catalogo();
156
156
  // O que é lento e não depende de nada roda JUNTO antes do primeiro passo.
157
157
  const C2 = require('./camadas.js');
158
158
  const asset = C2.base(tl);
@@ -231,7 +231,7 @@ const OPS = {
231
231
  async run(ctx, a) {
232
232
  const T = require('./timeline.js');
233
233
  const E = require('./edits.js');
234
- const { mutar } = require('./ops.js');
234
+ const { mutar } = require('./mutar.js');
235
235
  if (!ctx.tl) throw T.erro('nenhum projeto aberto: comece com import {"path":"..."}');
236
236
  const primeiro = T.todosOsClipes(ctx.tl).find((x) => x.clip.type === 'video');
237
237
  if (!a.id && !primeiro) throw T.erro('não há clipe de vídeo');