primocode 10.19.0 → 11.0.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.
- package/LICENSES/opencode-MIT.txt +24 -0
- package/LICENSES/pptxgenjs-MIT.txt +31 -0
- package/README.md +16 -3
- package/agentes/acessibilidade.md +10 -0
- package/agentes/ambiente.md +10 -0
- package/agentes/api.md +10 -0
- package/agentes/backend.md +10 -0
- package/agentes/copy.md +10 -0
- package/agentes/css-layout.md +10 -0
- package/agentes/dados.md +10 -0
- package/agentes/depuracao.md +10 -0
- package/agentes/design.md +10 -0
- package/agentes/docs.md +10 -0
- package/agentes/frontend.md +10 -0
- package/agentes/geral/capacidades/arquivos.json +132 -0
- package/agentes/geral/capacidades/codigo.json +132 -0
- package/agentes/geral/capacidades/dados.json +126 -0
- package/agentes/geral/capacidades/depuracao.json +126 -0
- package/agentes/geral/capacidades/escrita.json +126 -0
- package/agentes/geral/capacidades/git.json +126 -0
- package/agentes/geral/capacidades/pesquisa.json +126 -0
- package/agentes/geral/capacidades/seguranca.json +132 -0
- package/agentes/geral/capacidades/shell.json +132 -0
- package/agentes/geral/capacidades/web.json +126 -0
- package/agentes/git-pr.md +10 -0
- package/agentes/indice.md +86 -0
- package/agentes/nextjs.md +10 -0
- package/agentes/node.md +10 -0
- package/agentes/performance.md +10 -0
- package/agentes/pesquisa.md +10 -0
- package/agentes/python.md +10 -0
- package/agentes/react.md +10 -0
- package/agentes/refatoracao.md +10 -0
- package/agentes/revisor.md +10 -0
- package/agentes/seguranca.md +10 -0
- package/agentes/sql.md +10 -0
- package/agentes/testes.md +10 -0
- package/agentes/typescript.md +10 -0
- package/agentes/ux.md +10 -0
- package/bin/primocode.js +65 -8
- package/editor/helpers/camera_viva.py +28 -2
- package/lib/act.js +79 -10
- package/lib/agentes/index.js +146 -0
- package/lib/agentes/nucleo.js +18 -0
- package/lib/catalogo.js +12 -2
- package/lib/cli/review.js +64 -0
- package/lib/cli/subcomandos.js +34 -0
- package/lib/cli/tentativas.js +107 -0
- package/lib/cli/util.js +51 -0
- package/lib/comandos-extra.js +4 -2
- package/lib/editor-pro-maquina.js +165 -0
- package/lib/editor-pro.js +10 -7
- package/lib/entregaveis/cli.js +26 -0
- package/lib/entregaveis/especialistas.js +50 -0
- package/lib/entregaveis/nucleo.js +111 -0
- package/lib/entregaveis/slides.js +221 -0
- package/lib/estudio/materiais.js +172 -0
- package/lib/fundo.js +34 -1
- package/lib/kits.js +41 -2
- package/lib/nomes.js +6 -0
- package/lib/placar.js +173 -0
- package/lib/plugin/autoteste.js +99 -0
- package/lib/plugin/edicao-visual.js +133 -0
- package/lib/plugin/pesquisar.js +92 -0
- package/lib/plugin/preview-cli.js +38 -0
- package/lib/plugin/preview.js +89 -0
- package/lib/plugin/tunel.js +27 -0
- package/lib/review.js +134 -0
- package/lib/session/compactacao.js +124 -0
- package/lib/statusline.js +52 -0
- package/lib/tool/diagnostico.js +100 -0
- package/lib/tool/perguntar.js +60 -0
- package/lib/tool/reparar-entrada.js +66 -0
- package/lib/tool/web.js +102 -0
- package/lib/tools.js +20 -1
- package/lib/ui.js +1 -1
- package/materiais/saas-linear/base.ts +84 -0
- package/materiais/saas-linear/camera/Camera.tsx +38 -0
- package/materiais/saas-linear/fontes/OFL-Inter.txt +93 -0
- package/materiais/saas-linear/fontes/OFL-Newsreader.txt +93 -0
- package/materiais/saas-linear/fontes/inter-400.woff2 +0 -0
- package/materiais/saas-linear/fontes/inter-600.woff2 +0 -0
- package/materiais/saas-linear/fontes/newsreader-400.woff2 +0 -0
- package/materiais/saas-linear/formas/Agenda.tsx +46 -0
- package/materiais/saas-linear/formas/CaixaPrompt.tsx +41 -0
- package/materiais/saas-linear/formas/CartoesIntegracao.tsx +57 -0
- package/materiais/saas-linear/formas/Cursor.tsx +40 -0
- package/materiais/saas-linear/formas/Menu.tsx +46 -0
- package/materiais/saas-linear/formas/Monitor.tsx +28 -0
- package/materiais/saas-linear/paleta.json +18 -0
- package/materiais/saas-linear/pecas.json +590 -0
- package/materiais/saas-linear/regras.md +40 -0
- package/materiais/saas-linear/sons/Som.tsx +39 -0
- package/materiais/saas-linear/sons/assinatura.mp3 +0 -0
- package/materiais/saas-linear/sons/clique.mp3 +0 -0
- package/materiais/saas-linear/sons/impacto.mp3 +0 -0
- package/materiais/saas-linear/sons/pop-cartao.mp3 +0 -0
- package/materiais/saas-linear/sons/pop-palavra.mp3 +0 -0
- package/materiais/saas-linear/sons/subida.mp3 +0 -0
- package/materiais/saas-linear/sons/swoosh.mp3 +0 -0
- package/materiais/saas-linear/sons/trilha.mp3 +0 -0
- package/materiais/saas-linear/sons/whoosh-entra.mp3 +0 -0
- package/materiais/saas-linear/sons/whoosh-sai.mp3 +0 -0
- package/materiais/saas-linear/texturas/Fundo.tsx +18 -0
- package/materiais/saas-linear/texturas/Particulas.tsx +22 -0
- package/materiais/saas-linear/tipografia/FraseSerifa.tsx +25 -0
- package/materiais/saas-linear/tipografia/Marca.tsx +20 -0
- package/materiais/saas-linear/tipografia/MarcaFinal.tsx +22 -0
- package/materiais/saas-linear/tipografia/Saudacao.tsx +27 -0
- package/materiais/saas-linear/transicoes/Cena.tsx +38 -0
- package/materiais/saas-linear/transicoes/CorteImpacto.tsx +16 -0
- package/package.json +3 -1
- package/skills/deck-conferido/SKILL.md +29 -0
- package/vendor/pptxgenjs/carregar.js +12 -0
- package/vendor/pptxgenjs/jszip.min.js +13 -0
- package/vendor/pptxgenjs/pptxgen.cjs.js +7370 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* nucleo.js — a parte que slides, cartaz e documento dividem.
|
|
3
|
+
*
|
|
4
|
+
* brief → direção de arte → conferência numérica → exportação
|
|
5
|
+
*
|
|
6
|
+
* Nada aqui sabe de "slide": só cor, texto, contagem e o corredor de checagem.
|
|
7
|
+
* Cada tipo de entregável (slides.js, depois cartaz.js e documento.js) entrega
|
|
8
|
+
* ao `conferir` uma lista de REGRAS, e recebe de volta { nota, falhas }.
|
|
9
|
+
* Sem modelo, sem rede: tudo é número que dá para testar.
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
/** Palavras de um texto (hífen e apóstrofo ficam dentro da palavra). */
|
|
14
|
+
function palavras(t) {
|
|
15
|
+
return (String(t || '').match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) || []).length;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hex(h) {
|
|
19
|
+
const m = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i.exec(String(h || '').trim());
|
|
20
|
+
if (!m) return null;
|
|
21
|
+
let s = m[1];
|
|
22
|
+
if (s.length === 3) s = s.split('').map((c) => c + c).join('');
|
|
23
|
+
return [0, 2, 4].map((i) => parseInt(s.slice(i, i + 2), 16));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Luminância relativa WCAG 2.x. */
|
|
27
|
+
function luminancia(h) {
|
|
28
|
+
const rgb = hex(h);
|
|
29
|
+
if (!rgb) return null;
|
|
30
|
+
const [r, g, b] = rgb.map((v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; });
|
|
31
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Razão de contraste WCAG (1 a 21). null se uma das cores não for hex. */
|
|
35
|
+
function contraste(a, b) {
|
|
36
|
+
const la = luminancia(a); const lb = luminancia(b);
|
|
37
|
+
if (la == null || lb == null) return null;
|
|
38
|
+
const [claro, escuro] = la >= lb ? [la, lb] : [lb, la];
|
|
39
|
+
return (claro + 0.05) / (escuro + 0.05);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** O brief mínimo: sem tema e sem público não há o que desenhar. Devolve { ok, faltam }. */
|
|
43
|
+
function conferirBrief(brief) {
|
|
44
|
+
const b = brief || {};
|
|
45
|
+
const faltam = ['tema', 'publico', 'objetivo'].filter((k) => !String(b[k] || '').trim());
|
|
46
|
+
return { ok: faltam.length === 0, faltam };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Direção de arte escolhida pela IA a partir de material solto, não de modelo pronto.
|
|
51
|
+
* A direção válida tem: conceito (a ideia visual, ligada ao assunto), paleta com
|
|
52
|
+
* fundo/texto/destaque, e duas fontes. Aqui só se confere; quem escolhe é o modelo.
|
|
53
|
+
*/
|
|
54
|
+
function conferirDirecao(d) {
|
|
55
|
+
const falhas = [];
|
|
56
|
+
const x = d || {};
|
|
57
|
+
if (palavras(x.conceito) < 6) falhas.push({ regra: 'direcao-conceito', msg: 'conceito visual com menos de 6 palavras: diga por que este visual é DESTE assunto' });
|
|
58
|
+
const p = x.paleta || {};
|
|
59
|
+
for (const k of ['fundo', 'texto', 'destaque']) if (!hex(p[k])) falhas.push({ regra: 'direcao-paleta', msg: `paleta.${k} não é cor hex` });
|
|
60
|
+
if (!x.fonteTitulo || !x.fonteCorpo) falhas.push({ regra: 'direcao-fontes', msg: 'faltam fonteTitulo e fonteCorpo' });
|
|
61
|
+
const c = contraste(p.texto, p.fundo);
|
|
62
|
+
if (c != null && c < 4.5) falhas.push({ regra: 'contraste', msg: `texto sobre fundo: contraste ${c.toFixed(2)} < 4.5` });
|
|
63
|
+
const cd = contraste(p.destaque, p.fundo);
|
|
64
|
+
if (cd != null && cd < 3) falhas.push({ regra: 'contraste-destaque', msg: `destaque sobre fundo: contraste ${cd.toFixed(2)} < 3 (texto grande)` });
|
|
65
|
+
return falhas;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* O corredor de checagem. `regras` é uma lista de { id, peso, testa(alvo) → falhas[] }.
|
|
70
|
+
* Nota = 100 menos o peso de cada regra que falhou; passa quando nenhuma regra de
|
|
71
|
+
* peso 'bloqueia' falha e a nota é >= minimo.
|
|
72
|
+
*/
|
|
73
|
+
function conferir(alvo, regras, { minimo = 80 } = {}) {
|
|
74
|
+
const falhas = [];
|
|
75
|
+
let nota = 100;
|
|
76
|
+
for (const r of regras) {
|
|
77
|
+
const f = r.testa(alvo) || [];
|
|
78
|
+
if (f.length) {
|
|
79
|
+
nota -= r.peso;
|
|
80
|
+
for (const item of f) falhas.push({ regra: r.id, ...item });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
nota = Math.max(0, nota);
|
|
84
|
+
const bloqueia = regras.some((r) => r.bloqueia && falhas.some((f) => f.regra === r.id));
|
|
85
|
+
return { nota, falhas, passou: !bloqueia && nota >= minimo };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Slug de arquivo: afirmação curta vira nome seguro. */
|
|
89
|
+
function slug(t) {
|
|
90
|
+
return String(t || 'entrega').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()
|
|
91
|
+
.replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'entrega';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** O caminho do Chrome/Chromium para imprimir PDF, ou null. */
|
|
95
|
+
function acharNavegador(env = process.env, existe = require('fs').existsSync) {
|
|
96
|
+
const lista = [env.PRIMO_CHROME, '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
97
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium', '/usr/bin/google-chrome', '/usr/bin/chromium',
|
|
98
|
+
'/usr/bin/chromium-browser', 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'];
|
|
99
|
+
return lista.find((c) => c && existe(c)) || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** HTML → PDF com o Chrome sem cabeça. Devolve { ok, erro }. */
|
|
103
|
+
function htmlParaPdf(htmlPath, pdfPath, { navegador = acharNavegador() } = {}) {
|
|
104
|
+
if (!navegador) return { ok: false, erro: 'Chrome/Chromium não encontrado (defina PRIMO_CHROME)' };
|
|
105
|
+
const { spawnSync } = require('child_process');
|
|
106
|
+
const r = spawnSync(navegador, ['--headless=new', '--disable-gpu', '--no-pdf-header-footer',
|
|
107
|
+
`--print-to-pdf=${pdfPath}`, 'file://' + htmlPath], { encoding: 'utf8', timeout: 90000 });
|
|
108
|
+
return require('fs').existsSync(pdfPath) ? { ok: true } : { ok: false, erro: (r.stderr || '').slice(-300) || 'PDF não saiu' };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { palavras, hex, luminancia, contraste, conferirBrief, conferirDirecao, conferir, slug, acharNavegador, htmlParaPdf };
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* slides.js — o deck de ponta a ponta: plano → conferência numérica → .pptx + PDF.
|
|
3
|
+
*
|
|
4
|
+
* O MODELO escreve o plano (JSON): brief, direção de arte e os slides. Este
|
|
5
|
+
* arquivo NÃO escolhe cor nem texto. Ele (1) confere o plano com números,
|
|
6
|
+
* (2) desenha o mesmo plano em duas saídas: .pptx editável (pptxgenjs, MIT)
|
|
7
|
+
* e PDF (HTML → Chrome sem cabeça).
|
|
8
|
+
*
|
|
9
|
+
* Os layouts abaixo são GEOMETRIA (onde cabe o quê), não estilo: cor, fonte e
|
|
10
|
+
* conceito vêm sempre da direção que a IA escolheu para aquele assunto.
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const nucleo = require('./nucleo');
|
|
17
|
+
|
|
18
|
+
const LARGURA = 13.333; const ALTURA = 7.5; // polegadas, 16:9
|
|
19
|
+
const PX = 144; // 13.333 pol * 144 = 1920 px
|
|
20
|
+
|
|
21
|
+
/** Geometria por layout: caixas em polegadas e tamanho da letra em pt. */
|
|
22
|
+
const LAYOUTS = {
|
|
23
|
+
capa: { titulo: { x: 0.8, y: 1.5, w: 11.7, h: 3.6, pt: 80 }, sub: { x: 0.8, y: 5.3, w: 11.7, h: 1.2, pt: 28 } },
|
|
24
|
+
gigante: { titulo: { x: 0.8, y: 0.9, w: 11.7, h: 3.6, pt: 66 }, corpo: { x: 0.8, y: 4.7, w: 11.7, h: 2.2, pt: 28 } },
|
|
25
|
+
numero: { numero: { x: 0.6, y: 1.0, w: 6.4, h: 4.6, pt: 190 }, titulo: { x: 7.2, y: 1.6, w: 5.5, h: 2.6, pt: 40 }, corpo: { x: 7.2, y: 4.3, w: 5.5, h: 2.4, pt: 26 } },
|
|
26
|
+
colunas: { titulo: { x: 0.8, y: 0.6, w: 11.7, h: 1.4, pt: 40 }, col1: { x: 0.8, y: 2.3, w: 5.6, h: 4.2, pt: 26 }, col2: { x: 6.9, y: 2.3, w: 5.6, h: 4.2, pt: 26 } },
|
|
27
|
+
citacao: { citacao: { x: 1.3, y: 1.4, w: 10.7, h: 3.9, pt: 44 }, autor: { x: 1.3, y: 5.6, w: 10.7, h: 0.8, pt: 24 } },
|
|
28
|
+
lista: { titulo: { x: 0.8, y: 0.6, w: 11.7, h: 1.4, pt: 44 }, corpo: { x: 0.8, y: 2.3, w: 11.7, h: 4.4, pt: 28 } },
|
|
29
|
+
divisor: { titulo: { x: 0.8, y: 2.4, w: 11.7, h: 2.4, pt: 60 } },
|
|
30
|
+
fechamento: { titulo: { x: 0.8, y: 1.2, w: 11.7, h: 3.0, pt: 64 }, acao: { x: 0.8, y: 4.8, w: 11.7, h: 1.4, pt: 34 } },
|
|
31
|
+
};
|
|
32
|
+
const NOMES_LAYOUT = Object.keys(LAYOUTS);
|
|
33
|
+
|
|
34
|
+
/** Cabe o texto na caixa? Estimativa conservadora (0,52 em por letra, entrelinha 1,2). */
|
|
35
|
+
function cabe(texto, pt, wIn, hIn) {
|
|
36
|
+
const t = String(texto || '');
|
|
37
|
+
if (!t) return true;
|
|
38
|
+
const porLinha = Math.max(1, Math.floor((wIn * 72) / (pt * 0.52)));
|
|
39
|
+
let linhas = 0;
|
|
40
|
+
for (const par of t.split('\n')) linhas += Math.max(1, Math.ceil(par.length / porLinha));
|
|
41
|
+
return linhas * pt * 1.2 / 72 <= hIn;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function itens(s) { return Array.isArray(s.corpo) ? s.corpo : (s.corpo ? [String(s.corpo)] : []); }
|
|
45
|
+
|
|
46
|
+
function textoDoSlide(s) {
|
|
47
|
+
return [s.titulo, s.subtitulo, s.numero, s.legenda, s.citacao, s.autor, s.acao, ...itens(s)].filter(Boolean).join(' ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const REGRAS = [
|
|
51
|
+
{ id: 'tamanho-do-deck', peso: 10, testa: (p) => (p.slides.length < 5 || p.slides.length > 20) ? [{ msg: `${p.slides.length} slides: o deck tem de ter entre 5 e 20` }] : [] },
|
|
52
|
+
{ id: 'layout-valido', peso: 10, bloqueia: true, testa: (p) => p.slides.flatMap((s, i) => NOMES_LAYOUT.includes(s.layout) ? [] : [{ slide: i + 1, msg: `layout "${s.layout}" não existe (${NOMES_LAYOUT.join(', ')})` }]) },
|
|
53
|
+
{ id: 'palavras-por-slide', peso: 15, testa: (p) => p.slides.flatMap((s, i) => {
|
|
54
|
+
const max = ['capa', 'divisor', 'fechamento', 'numero', 'citacao'].includes(s.layout) ? 30 : 40;
|
|
55
|
+
const n = nucleo.palavras(textoDoSlide(s));
|
|
56
|
+
return n > max ? [{ slide: i + 1, msg: `${n} palavras (máx ${max}): corte ou divida em dois slides` }] : [];
|
|
57
|
+
}) },
|
|
58
|
+
{ id: 'titulo-afirmacao', peso: 10, testa: (p) => p.slides.flatMap((s, i) => {
|
|
59
|
+
if (s.layout === 'citacao' || s.layout === 'divisor' || s.layout === 'numero') return [];
|
|
60
|
+
const n = nucleo.palavras(s.titulo);
|
|
61
|
+
return n < 3 ? [{ slide: i + 1, msg: `título com ${n} palavra(s): escreva a afirmação (3 a 12 palavras), não o tema` }]
|
|
62
|
+
: n > 12 ? [{ slide: i + 1, msg: `título com ${n} palavras (máx 12)` }] : [];
|
|
63
|
+
}) },
|
|
64
|
+
{ id: 'uma-ideia', peso: 10, testa: (p) => p.slides.flatMap((s, i) => {
|
|
65
|
+
const it = itens(s); const out = [];
|
|
66
|
+
if (it.length > 4) out.push({ slide: i + 1, msg: `${it.length} itens (máx 4): uma ideia por slide` });
|
|
67
|
+
it.forEach((x) => { if (nucleo.palavras(x) > 14) out.push({ slide: i + 1, msg: `item com ${nucleo.palavras(x)} palavras (máx 14): "${String(x).slice(0, 30)}…"` }); });
|
|
68
|
+
return out;
|
|
69
|
+
}) },
|
|
70
|
+
{ id: 'layout-repetido', peso: 15, testa: (p) => p.slides.flatMap((s, i) => (i && s.layout === p.slides[i - 1].layout) ? [{ slide: i + 1, msg: `layout "${s.layout}" igual ao slide anterior` }] : []) },
|
|
71
|
+
{ id: 'variedade', peso: 10, testa: (p) => {
|
|
72
|
+
const n = p.slides.length; const dist = new Set(p.slides.map((s) => s.layout)).size; const meta = Math.min(4, Math.ceil(n / 2));
|
|
73
|
+
return n >= 6 && dist < meta ? [{ msg: `${dist} layouts diferentes em ${n} slides (mínimo ${meta})` }] : [];
|
|
74
|
+
} },
|
|
75
|
+
{ id: 'abertura-e-fim', peso: 10, testa: (p) => {
|
|
76
|
+
const f = []; const a = p.slides[0]; const z = p.slides[p.slides.length - 1];
|
|
77
|
+
if (!a || a.layout !== 'capa') f.push({ slide: 1, msg: 'o primeiro slide tem de ser a capa' });
|
|
78
|
+
if (!z || z.layout !== 'fechamento') f.push({ slide: p.slides.length, msg: 'o último slide tem de ser o fechamento' });
|
|
79
|
+
else if (nucleo.palavras(z.acao) < 2) f.push({ slide: p.slides.length, msg: 'o fechamento precisa de uma ação (campo "acao"): o que o público faz agora' });
|
|
80
|
+
return f;
|
|
81
|
+
} },
|
|
82
|
+
{ id: 'notas-do-orador', peso: 5, testa: (p) => p.slides.flatMap((s, i) => (s.layout === 'divisor' || nucleo.palavras(s.nota) >= 8) ? [] : [{ slide: i + 1, msg: 'sem nota do orador (mín 8 palavras): o texto do slide é o que se lê, a fala vai na nota' }]) },
|
|
83
|
+
{ id: 'tamanho-da-letra', peso: 10, bloqueia: true, testa: (p) => {
|
|
84
|
+
const t = (p.direcao || {}).tamanhoCorpo || 28;
|
|
85
|
+
return t < 24 ? [{ msg: `corpo com ${t}pt (mín 24pt): lê-se de longe` }] : [];
|
|
86
|
+
} },
|
|
87
|
+
{ id: 'encaixe', peso: 10, bloqueia: true, testa: (p) => p.slides.flatMap((s, i) => {
|
|
88
|
+
const g = LAYOUTS[s.layout]; if (!g) return [];
|
|
89
|
+
const mapa = { titulo: s.titulo, sub: s.subtitulo, numero: s.numero, citacao: s.citacao, autor: s.autor, acao: s.acao,
|
|
90
|
+
corpo: itens(s).join('\n'), col1: itens(s).filter((_, k) => k % 2 === 0).join('\n'), col2: itens(s).filter((_, k) => k % 2 === 1).join('\n') };
|
|
91
|
+
return Object.keys(g).filter((k) => mapa[k] && !cabe(mapa[k], g[k].pt, g[k].w, g[k].h))
|
|
92
|
+
.map((k) => ({ slide: i + 1, msg: `${k} não cabe na caixa (${g[k].pt}pt em ${g[k].w}x${g[k].h} pol): encurte` }));
|
|
93
|
+
}) },
|
|
94
|
+
{ id: 'visual-com-motivo', peso: 10, testa: (p) => p.slides.flatMap((s, i) => (s.visual && nucleo.palavras(s.visual.motivo) < 4)
|
|
95
|
+
? [{ slide: i + 1, msg: 'visual sem "motivo": todo elemento tem de ter a ver com o conteúdo do slide' }] : []) },
|
|
96
|
+
{ id: 'direcao-de-arte', peso: 15, bloqueia: true, testa: (p) => nucleo.conferirDirecao(p.direcao).map((f) => ({ msg: f.msg })) },
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/** Confere o plano. Devolve { nota, falhas, passou }. Plano malformado reprova sem estourar. */
|
|
100
|
+
function conferirDeck(plano, opcoes) {
|
|
101
|
+
if (!plano || !Array.isArray(plano.slides) || !plano.slides.length) {
|
|
102
|
+
return { nota: 0, passou: false, falhas: [{ regra: 'plano', msg: 'plano sem slides' }] };
|
|
103
|
+
}
|
|
104
|
+
const brief = nucleo.conferirBrief(plano.brief);
|
|
105
|
+
const r = nucleo.conferir(plano, REGRAS, opcoes);
|
|
106
|
+
if (!brief.ok) { r.falhas.unshift({ regra: 'brief', msg: `brief sem: ${brief.faltam.join(', ')}` }); r.nota = Math.max(0, r.nota - 10); r.passou = false; }
|
|
107
|
+
return r;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Resumo para o agente ler e consertar: uma linha por falha. */
|
|
111
|
+
function relatorio(r) {
|
|
112
|
+
const linhas = r.falhas.map((f) => `- [${f.regra}]${f.slide ? ` slide ${f.slide}:` : ''} ${f.msg}`);
|
|
113
|
+
return `nota ${r.nota}/100 — ${r.passou ? 'PASSOU' : 'REPROVADO'}${linhas.length ? '\n' + linhas.join('\n') : ''}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ───────────────────────── .pptx editável ─────────────────────────
|
|
117
|
+
|
|
118
|
+
const semHash = (c) => String(c).replace('#', '').toUpperCase();
|
|
119
|
+
|
|
120
|
+
async function gerarPptx(plano, destino) {
|
|
121
|
+
const PptxGenJS = require('../../vendor/pptxgenjs/carregar.js');
|
|
122
|
+
const pptx = new PptxGenJS();
|
|
123
|
+
pptx.defineLayout({ name: 'W169', width: LARGURA, height: ALTURA });
|
|
124
|
+
pptx.layout = 'W169';
|
|
125
|
+
pptx.title = (plano.brief || {}).tema || 'Apresentação';
|
|
126
|
+
pptx.author = 'PrimoCode';
|
|
127
|
+
const d = plano.direcao; const c = d.paleta;
|
|
128
|
+
const fundo = semHash(c.fundo); const texto = semHash(c.texto); const dest = semHash(c.destaque);
|
|
129
|
+
const suave = semHash(c.suave || c.texto);
|
|
130
|
+
const T = (s, caixa, extra) => s.addText(extra.text, { x: caixa.x, y: caixa.y, w: caixa.w, h: caixa.h, fontSize: caixa.pt,
|
|
131
|
+
fontFace: extra.face || d.fonteCorpo, color: extra.color || texto, bold: !!extra.bold, italic: !!extra.italic,
|
|
132
|
+
valign: extra.valign || 'top', margin: 0, fit: 'none', align: extra.align || 'left', lineSpacingMultiple: extra.ls || 1 });
|
|
133
|
+
plano.slides.forEach((sl) => {
|
|
134
|
+
const s = pptx.addSlide();
|
|
135
|
+
s.background = { color: fundo };
|
|
136
|
+
const g = LAYOUTS[sl.layout];
|
|
137
|
+
// Cada layout carrega UM gesto visual feito só de forma + cor da direção.
|
|
138
|
+
if (sl.layout === 'capa') s.addShape('rect', { x: 0, y: 0, w: 0.35, h: ALTURA, fill: { color: dest }, line: { color: dest, width: 0 } });
|
|
139
|
+
if (sl.layout === 'gigante') s.addShape('rect', { x: 0.8, y: 0.5, w: 1.6, h: 0.12, fill: { color: dest }, line: { color: dest, width: 0 } });
|
|
140
|
+
if (sl.layout === 'numero') s.addShape('rect', { x: 7.0, y: 1.2, w: 0.06, h: 5.2, fill: { color: dest }, line: { color: dest, width: 0 } });
|
|
141
|
+
if (sl.layout === 'divisor') s.addShape('rect', { x: 0, y: 0, w: LARGURA, h: ALTURA, fill: { color: dest }, line: { color: dest, width: 0 } });
|
|
142
|
+
if (sl.layout === 'fechamento') s.addShape('rect', { x: 0.8, y: 4.55, w: 3, h: 0.1, fill: { color: dest }, line: { color: dest, width: 0 } });
|
|
143
|
+
if (sl.visual && sl.visual.imagem && fs.existsSync(sl.visual.imagem)) {
|
|
144
|
+
s.addImage({ path: sl.visual.imagem, x: 9.0, y: 4.6, w: 3.8, h: 2.6, sizing: { type: 'cover', w: 3.8, h: 2.6 } });
|
|
145
|
+
}
|
|
146
|
+
const sobreDestaque = sl.layout === 'divisor' ? { color: fundo } : {};
|
|
147
|
+
if (sl.layout === 'capa') { T(s, g.titulo, { text: sl.titulo, face: d.fonteTitulo, bold: true, color: texto, valign: 'bottom', ls: 0.9 }); if (sl.subtitulo) T(s, g.sub, { text: sl.subtitulo, color: dest }); }
|
|
148
|
+
else if (sl.layout === 'gigante') { T(s, g.titulo, { text: sl.titulo, face: d.fonteTitulo, bold: true, color: dest, ls: 0.9 }); if (itens(sl).length) T(s, g.corpo, { text: itens(sl).join('\n') }); }
|
|
149
|
+
else if (sl.layout === 'numero') { T(s, g.numero, { text: sl.numero, face: d.fonteTitulo, bold: true, color: dest, valign: 'middle' }); T(s, g.titulo, { text: sl.titulo, face: d.fonteTitulo, bold: true }); if (sl.legenda || itens(sl).length) T(s, g.corpo, { text: sl.legenda || itens(sl).join('\n'), color: suave }); }
|
|
150
|
+
else if (sl.layout === 'colunas') { T(s, g.titulo, { text: sl.titulo, face: d.fonteTitulo, bold: true }); T(s, g.col1, { text: itens(sl).filter((_, k) => k % 2 === 0).join('\n') }); T(s, g.col2, { text: itens(sl).filter((_, k) => k % 2 === 1).join('\n') }); }
|
|
151
|
+
else if (sl.layout === 'citacao') { T(s, g.citacao, { text: '“' + sl.citacao + '”', face: d.fonteTitulo, italic: true, color: dest, valign: 'middle' }); if (sl.autor) T(s, g.autor, { text: '— ' + sl.autor, color: suave }); }
|
|
152
|
+
else if (sl.layout === 'lista') { T(s, g.titulo, { text: sl.titulo, face: d.fonteTitulo, bold: true }); s.addText(itens(sl).map((t) => ({ text: t, options: { bullet: { code: '25A0', color: dest }, breakLine: true, paraSpaceAfter: 10 } })), { x: g.corpo.x, y: g.corpo.y, w: g.corpo.w, h: g.corpo.h, fontSize: g.corpo.pt, fontFace: d.fonteCorpo, color: texto, margin: 0, valign: 'top', fit: 'none' }); }
|
|
153
|
+
else if (sl.layout === 'divisor') T(s, g.titulo, { text: sl.titulo, face: d.fonteTitulo, bold: true, color: sobreDestaque.color, valign: 'middle' });
|
|
154
|
+
else if (sl.layout === 'fechamento') { T(s, g.titulo, { text: sl.titulo, face: d.fonteTitulo, bold: true, valign: 'bottom', ls: 0.9 }); T(s, g.acao, { text: sl.acao, color: dest, bold: true }); }
|
|
155
|
+
if (sl.nota) s.addNotes(sl.nota);
|
|
156
|
+
});
|
|
157
|
+
await pptx.writeFile({ fileName: destino });
|
|
158
|
+
return destino;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ───────────────────────── HTML → PDF ─────────────────────────
|
|
162
|
+
|
|
163
|
+
const esc = (t) => String(t == null ? '' : t).replace(/[&<>"]/g, (m) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[m]));
|
|
164
|
+
const px = (pol) => Math.round(pol * PX) + 'px';
|
|
165
|
+
const pt2px = (pt) => Math.round(pt * PX / 72) + 'px';
|
|
166
|
+
|
|
167
|
+
function gerarHtml(plano) {
|
|
168
|
+
const d = plano.direcao; const c = d.paleta;
|
|
169
|
+
const dest2 = c.destaque2 || c.destaque;
|
|
170
|
+
const caixa = (g, extra = '') => `position:absolute;left:${px(g.x)};top:${px(g.y)};width:${px(g.w)};height:${px(g.h)};font-size:${pt2px(g.pt)};${extra}`;
|
|
171
|
+
const grad = `color:${c.destaque};`; // o degradê vai no <span> interno (ver gradiente)
|
|
172
|
+
// background-clip:text vira retângulo cheio no print-to-pdf do Chrome (visto em 29/10): título gigante sai em cor sólida; degradê de texto só via SVG (ver entregaveis-proximos.md).
|
|
173
|
+
const gradiente = `color:${c.destaque}`;
|
|
174
|
+
const fam = (f) => `'${esc(f)}',system-ui,sans-serif`;
|
|
175
|
+
const secoes = plano.slides.map((s) => {
|
|
176
|
+
const g = LAYOUTS[s.layout]; let h = '';
|
|
177
|
+
const t = (k, txt, extra = '') => {
|
|
178
|
+
if (!txt) return '';
|
|
179
|
+
const miolo = esc(txt).replace(/\n/g, '<br>');
|
|
180
|
+
return `<div style="${caixa(g[k], extra)}">${extra.includes(grad) ? `<span style="${gradiente}">${miolo}</span>` : miolo}</div>`;
|
|
181
|
+
};
|
|
182
|
+
if (s.layout === 'capa') h = `<i style="position:absolute;left:0;top:0;width:${px(0.35)};height:100%;background:${c.destaque}"></i>` + t('titulo', s.titulo, `font-family:${fam(d.fonteTitulo)};font-weight:800;line-height:.92;display:flex;align-items:flex-end;${grad}`) + t('sub', s.subtitulo, `color:${c.destaque}`);
|
|
183
|
+
else if (s.layout === 'gigante') h = `<i style="position:absolute;left:${px(0.8)};top:${px(0.5)};width:${px(1.6)};height:${px(0.12)};background:${c.destaque}"></i>` + t('titulo', s.titulo, `font-family:${fam(d.fonteTitulo)};font-weight:800;line-height:.92;${grad}`) + t('corpo', itens(s).join('\n'));
|
|
184
|
+
else if (s.layout === 'numero') h = `<i style="position:absolute;left:${px(7.0)};top:${px(1.2)};width:${px(0.06)};height:${px(5.2)};background:${c.destaque}"></i>` + t('numero', s.numero, `font-family:${fam(d.fonteTitulo)};font-weight:800;line-height:1;display:flex;align-items:center;${grad}`) + t('titulo', s.titulo, `font-family:${fam(d.fonteTitulo)};font-weight:700`) + t('corpo', s.legenda || itens(s).join('\n'), `color:${c.suave || c.texto}`);
|
|
185
|
+
else if (s.layout === 'colunas') h = t('titulo', s.titulo, `font-family:${fam(d.fonteTitulo)};font-weight:700`) + t('col1', itens(s).filter((_, k) => k % 2 === 0).join('\n')) + t('col2', itens(s).filter((_, k) => k % 2 === 1).join('\n'));
|
|
186
|
+
else if (s.layout === 'citacao') h = t('citacao', '“' + s.citacao + '”', `font-family:${fam(d.fonteTitulo)};font-style:italic;display:flex;align-items:center;color:${c.destaque}`) + t('autor', s.autor && '— ' + s.autor, `color:${c.suave || c.texto}`);
|
|
187
|
+
else if (s.layout === 'lista') h = t('titulo', s.titulo, `font-family:${fam(d.fonteTitulo)};font-weight:700`) + `<ul style="${caixa(LAYOUTS.lista.corpo, 'margin:0;padding:0;list-style:none')}">${itens(s).map((x) => `<li style="margin-bottom:${px(0.14)};padding-left:${px(0.5)};position:relative"><b style="position:absolute;left:0;color:${c.destaque}">■</b>${esc(x)}</li>`).join('')}</ul>`;
|
|
188
|
+
else if (s.layout === 'divisor') h = t('titulo', s.titulo, `font-family:${fam(d.fonteTitulo)};font-weight:800;display:flex;align-items:center;color:${c.fundo}`);
|
|
189
|
+
else if (s.layout === 'fechamento') h = `<i style="position:absolute;left:${px(0.8)};top:${px(4.55)};width:${px(3)};height:${px(0.1)};background:${c.destaque}"></i>` + t('titulo', s.titulo, `font-family:${fam(d.fonteTitulo)};font-weight:800;line-height:.92;display:flex;align-items:flex-end;${grad}`) + t('acao', s.acao, `color:${c.destaque};font-weight:700`);
|
|
190
|
+
if (s.visual && s.visual.imagem && fs.existsSync(s.visual.imagem)) {
|
|
191
|
+
const b64 = fs.readFileSync(s.visual.imagem).toString('base64');
|
|
192
|
+
const ext = path.extname(s.visual.imagem).slice(1).toLowerCase().replace('jpg', 'jpeg');
|
|
193
|
+
h += `<img alt="" src="data:image/${ext};base64,${b64}" style="position:absolute;left:${px(9)};top:${px(4.6)};width:${px(3.8)};height:${px(2.6)};object-fit:cover">`;
|
|
194
|
+
}
|
|
195
|
+
const fundo = s.layout === 'divisor' ? c.destaque : c.fundo;
|
|
196
|
+
return `<section style="background:${fundo};color:${c.texto};font-family:${fam(d.fonteCorpo)}">${h}</section>`;
|
|
197
|
+
}).join('\n');
|
|
198
|
+
return `<!doctype html><html lang="pt-BR"><head><meta charset="utf-8"><title>${esc((plano.brief || {}).tema || 'Apresentação')}</title>
|
|
199
|
+
<style>@page{size:1920px 1080px;margin:0}*{box-sizing:border-box}html,body{margin:0;background:${c.fundo}}
|
|
200
|
+
section{position:relative;width:1920px;height:1080px;overflow:hidden;page-break-after:always;break-after:page;-webkit-print-color-adjust:exact;print-color-adjust:exact}
|
|
201
|
+
section>div{line-height:1.2}</style></head><body>\n${secoes}\n</body></html>`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Exporta o deck: confere, escreve .pptx, .html e (se houver Chrome) .pdf.
|
|
206
|
+
* Plano reprovado não exporta, a menos que { forcar: true }.
|
|
207
|
+
*/
|
|
208
|
+
async function exportarDeck(plano, pasta, { forcar = false, pdf = true, nome } = {}) {
|
|
209
|
+
const resultado = conferirDeck(plano);
|
|
210
|
+
if (!resultado.passou && !forcar) return { exportado: false, conferencia: resultado, arquivos: [] };
|
|
211
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
212
|
+
const base = path.join(pasta, nome || nucleo.slug((plano.brief || {}).tema || plano.titulo));
|
|
213
|
+
const arquivos = [];
|
|
214
|
+
await gerarPptx(plano, base + '.pptx'); arquivos.push(base + '.pptx');
|
|
215
|
+
fs.writeFileSync(base + '.html', gerarHtml(plano)); arquivos.push(base + '.html');
|
|
216
|
+
let erroPdf = null;
|
|
217
|
+
if (pdf) { const r = nucleo.htmlParaPdf(base + '.html', base + '.pdf'); if (r.ok) arquivos.push(base + '.pdf'); else erroPdf = r.erro; }
|
|
218
|
+
return { exportado: true, conferencia: resultado, arquivos, erroPdf };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { LAYOUTS, NOMES_LAYOUT, cabe, conferirDeck, relatorio, gerarPptx, gerarHtml, exportarDeck };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* materiais.js — peças soltas por estilo. A IA monta o vídeo DO ZERO.
|
|
3
|
+
*
|
|
4
|
+
* Um estilo migrado tem `materiais/<estilo>/` com só peças (formas, texturas, transições, tipografia, câmera, sons),
|
|
5
|
+
* a paleta, as regras e o índice `pecas.json`. Não existe Montagem, receita nem plano pronto: a IA lista as peças,
|
|
6
|
+
* lê as que quer, escreve a cena nova (TSX) e o conferir cobra a densidade mínima.
|
|
7
|
+
*
|
|
8
|
+
* primocode kit pecas <estilo> as peças (nome · o que é · params · duração)
|
|
9
|
+
* primocode kit pecas <estilo> ver <peça> o código da peça (ou os dados do áudio)
|
|
10
|
+
* primocode kit pecas <estilo> regras as regras e a densidade mínima
|
|
11
|
+
* primocode kit pecas <estilo> escrever <nome> --arquivo <cena.tsx> grava a cena em motion/ e confere
|
|
12
|
+
* primocode kit pecas <estilo> conferir <cena.tsx>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const RAIZ = path.join(__dirname, '..', '..', 'materiais');
|
|
21
|
+
const CATEGORIAS = ['formas', 'texturas', 'transicoes', 'tipografia', 'camera', 'sons', 'paleta'];
|
|
22
|
+
/** Nada disto pode existir como peça, nem ser lido nem importado no caminho da IA. */
|
|
23
|
+
const PROIBIDO = /(^|[\\/'"._-])(Montagem|receita|plano)(\.(tsx?|jsx?|json))?(['"]|$)|Montagem\.tsx|receita\.json|plano\.json/;
|
|
24
|
+
const MINIMO = { camadas: 4, pecasDiferentes: 6, chavesCamera: 4, efeitosPorCorte: 1 };
|
|
25
|
+
|
|
26
|
+
const pastaDe = (estilo, raiz = RAIZ) => path.join(raiz, String(estilo || '').replace(/[^a-z0-9-]/gi, ''));
|
|
27
|
+
|
|
28
|
+
/** O estilo foi migrado pra peças soltas? */
|
|
29
|
+
function migrado(estilo, raiz = RAIZ) { return !!estilo && fs.existsSync(path.join(pastaDe(estilo, raiz), 'pecas.json')); }
|
|
30
|
+
function estilos(raiz = RAIZ) {
|
|
31
|
+
try { return fs.readdirSync(raiz, { withFileTypes: true }).filter((d) => d.isDirectory() && migrado(d.name, raiz)).map((d) => d.name); } catch { return []; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ler(estilo, raiz = RAIZ) {
|
|
35
|
+
try { return JSON.parse(fs.readFileSync(path.join(pastaDe(estilo, raiz), 'pecas.json'), 'utf8')); } catch { return null; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Confere o índice: campos, arquivos que existem, nenhuma peça que seja cena/vídeo pronto. Devolve a lista de erros. */
|
|
39
|
+
function validar(estilo, raiz = RAIZ) {
|
|
40
|
+
const erros = [];
|
|
41
|
+
const idx = ler(estilo, raiz);
|
|
42
|
+
if (!idx) return [`materiais/${estilo}/pecas.json ausente ou inválido`];
|
|
43
|
+
const base = pastaDe(estilo, raiz);
|
|
44
|
+
if (!Array.isArray(idx.pecas) || !idx.pecas.length) return ['pecas.json sem peças'];
|
|
45
|
+
const nomes = new Set();
|
|
46
|
+
for (const p of idx.pecas) {
|
|
47
|
+
const q = `peça "${p && p.nome}"`;
|
|
48
|
+
if (!p || !p.nome || !p.categoria || !p.arquivo || !p.descricao) { erros.push(`${q}: faltam nome, categoria, arquivo ou descricao`); continue; }
|
|
49
|
+
if (nomes.has(p.nome)) erros.push(`${q}: nome repetido`);
|
|
50
|
+
nomes.add(p.nome);
|
|
51
|
+
if (!CATEGORIAS.includes(p.categoria)) erros.push(`${q}: categoria "${p.categoria}" fora de ${CATEGORIAS.join(', ')}`);
|
|
52
|
+
if (!Array.isArray(p.params)) erros.push(`${q}: params precisa ser lista`);
|
|
53
|
+
if (p.duracao != null && !(Array.isArray(p.duracao) && p.duracao.length === 2 && p.duracao[0] <= p.duracao[1])) erros.push(`${q}: duracao é null ou [min, max]`);
|
|
54
|
+
if (!Array.isArray(p.tags)) erros.push(`${q}: tags precisa ser lista`);
|
|
55
|
+
if (/\n/.test(p.descricao) || p.descricao.length > 200) erros.push(`${q}: a descrição é de uma linha (até 200 caracteres)`);
|
|
56
|
+
if (PROIBIDO.test(p.arquivo) || PROIBIDO.test(p.nome)) erros.push(`${q}: peça não pode ser Montagem, receita ou plano`);
|
|
57
|
+
const arq = path.join(base, p.arquivo);
|
|
58
|
+
if (!arq.startsWith(base + path.sep)) erros.push(`${q}: arquivo fora da pasta do estilo`);
|
|
59
|
+
else if (!fs.existsSync(arq)) erros.push(`${q}: arquivo ${p.arquivo} não existe`);
|
|
60
|
+
else if (/\.tsx?$/.test(arq) && fs.readFileSync(arq, 'utf8').split('\n').length > 140) erros.push(`${q}: arquivo grande demais pra ser uma peça (mais de 140 linhas)`);
|
|
61
|
+
}
|
|
62
|
+
for (const f of ['regras.md', 'paleta.json']) if (!fs.existsSync(path.join(base, f))) erros.push(`falta ${f}`);
|
|
63
|
+
return erros;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const linha = (p) => `- ${p.nome} [${p.categoria}] ${p.descricao} | params: ${(p.params || []).join(', ') || '-'} | duração: ${p.duracao ? p.duracao.join('-') + ' quadros' : 'contínua'} | tags: ${(p.tags || []).join(', ')}`;
|
|
67
|
+
|
|
68
|
+
function listar(estilo, raiz = RAIZ) {
|
|
69
|
+
const idx = ler(estilo, raiz);
|
|
70
|
+
if (!idx) return { ok: false, texto: `O estilo "${estilo}" não tem peças soltas. Migrados: ${estilos(raiz).join(', ') || 'nenhum'}.` };
|
|
71
|
+
const porCat = {};
|
|
72
|
+
for (const p of idx.pecas) (porCat[p.categoria] = porCat[p.categoria] || []).push(p);
|
|
73
|
+
const out = [`# Peças de ${idx.estilo} (${idx.pecas.length}): só peças soltas, nenhuma cena pronta.`,
|
|
74
|
+
`Leia as regras (primocode kit pecas ${estilo} regras), leia as peças que vai usar (… ver <peça>), escreva a cena NOVA e grave (… escrever <nome> --arquivo <cena.tsx>).`, ''];
|
|
75
|
+
for (const c of CATEGORIAS) if (porCat[c]) out.push(`## ${c}`, ...porCat[c].map(linha), '');
|
|
76
|
+
return { ok: true, texto: out.join('\n').trim() };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function lerPeca(estilo, nome, raiz = RAIZ) {
|
|
80
|
+
const idx = ler(estilo, raiz);
|
|
81
|
+
const p = idx && idx.pecas.find((x) => x.nome.toLowerCase() === String(nome || '').toLowerCase());
|
|
82
|
+
if (!p) return { ok: false, texto: `Peça "${nome}" não existe em ${estilo}. Veja: primocode kit pecas ${estilo}` };
|
|
83
|
+
const arq = path.join(pastaDe(estilo, raiz), p.arquivo);
|
|
84
|
+
const cab = linha(p);
|
|
85
|
+
if (/\.(mp3|wav)$/.test(arq)) return { ok: true, texto: `${cab}\narquivo de áudio: ${p.arquivo} (toque com <Efeito som="${p.nome}" em={quadro}/> ou <Trilha/>; volume sugerido ${p.volumeSugerido ?? '-'})`, peca: p };
|
|
86
|
+
return { ok: true, texto: `${cab}\nimport: ${p.arquivo.replace(/\.tsx?$/, '')}\n\n${fs.readFileSync(arq, 'utf8')}`, peca: p };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function regras(estilo, raiz = RAIZ) {
|
|
90
|
+
try { return { ok: true, texto: fs.readFileSync(path.join(pastaDe(estilo, raiz), 'regras.md'), 'utf8') }; } catch { return { ok: false, texto: `Sem regras para ${estilo}.` }; }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** O texto que entra no prompt da IA pra um estilo migrado (no lugar do `kit ver`). */
|
|
94
|
+
function promptDoEstilo(estilo, raiz = RAIZ) {
|
|
95
|
+
const l = listar(estilo, raiz);
|
|
96
|
+
if (!l.ok) return l.texto;
|
|
97
|
+
return [`ESTILO ${estilo}: montagem a partir de peças soltas. Você COMPÕE a cena do zero; não há vídeo, receita nem plano pronto pra copiar.`,
|
|
98
|
+
'Passos: 1) leia o pedido e o roteiro; 2) escreva o plano da cena (linha do tempo em quadros, camadas, chaves de câmera, cues de som); 3) leia as peças que vai usar;',
|
|
99
|
+
'4) escreva UM arquivo TSX com a composição nova: import só do remotion e das peças, no caminho `./pecas/<categoria>/<Peça>` (ex.: `import {Agenda} from "./pecas/formas/Agenda"`, `import {e4} from "./pecas/base"`); exporte `Video` (componente) e `DURACAO` (quadros); 5) grave com `escrever`, que confere a densidade; 6) conserte o que o conferir apontar; 7) renderize.',
|
|
100
|
+
'', l.texto, '', '## regras', regras(estilo, raiz).texto].join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const SEM_COMENTARIO = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Conferir uma cena escrita pela IA (estático, sem render). Cobra: só importa peças do estilo, nada de Montagem/receita/plano,
|
|
107
|
+
* camadas, peças diferentes, chaves de câmera, SFX em todo corte, trilha. Devolve {ok, erros, medidas}.
|
|
108
|
+
*/
|
|
109
|
+
function conferirCena(estilo, codigo, raiz = RAIZ) {
|
|
110
|
+
const idx = ler(estilo, raiz);
|
|
111
|
+
const erros = [];
|
|
112
|
+
if (!idx) return { ok: false, erros: [`estilo ${estilo} sem peças`], medidas: {} };
|
|
113
|
+
const src = SEM_COMENTARIO(String(codigo || ''));
|
|
114
|
+
if (PROIBIDO.test(src)) erros.push('a cena não pode usar Montagem, receita nem plano: componha as peças direto');
|
|
115
|
+
const nomes = new Set(idx.pecas.map((p) => p.nome));
|
|
116
|
+
const usadas = new Set();
|
|
117
|
+
for (const m of src.matchAll(/import\s+(?:type\s+)?(?:\{([^}]*)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g)) {
|
|
118
|
+
const de = m[3];
|
|
119
|
+
if (/^(react|remotion)(\/|$)/.test(de)) continue;
|
|
120
|
+
if (!/(^|\/)(formas|texturas|transicoes|tipografia|camera|sons)\/|(^|\/)base$|(^|\/)paleta/.test(de) && !/\.(json|mp3|svg|png|jpg)$/.test(de)) { erros.push(`import fora das peças: "${de}"`); continue; }
|
|
121
|
+
for (const n of (m[1] || m[2] || '').split(',').map((x) => x.trim().split(/\s+as\s+/)[0]).filter(Boolean)) if (nomes.has(n)) usadas.add(n);
|
|
122
|
+
}
|
|
123
|
+
const cenas = (src.match(/<Cena\b/g) || []).length;
|
|
124
|
+
const cortes = Math.max(0, cenas - 1);
|
|
125
|
+
const efeitos = (src.match(/<Efeito\b/g) || []).length;
|
|
126
|
+
const chaves = (src.match(/\{\s*em\s*:/g) || []).length;
|
|
127
|
+
const camadasPorCena = cenas ? Math.round(([...usadas].filter((n) => !['Cena', 'Camera', 'Som'].includes(n)).length)) : 0;
|
|
128
|
+
const medidas = { cenas, cortes, efeitos, chavesCamera: chaves, pecasDiferentes: usadas.size, camadas: camadasPorCena };
|
|
129
|
+
if (!/<Camera\b/.test(src)) erros.push('falta <Camera> (a câmera nunca para)');
|
|
130
|
+
if (!/<Trilha\b/.test(src)) erros.push('falta <Trilha> (trilha forte sempre)');
|
|
131
|
+
if (cenas < 2) erros.push('use ao menos 2 <Cena> (um vídeo é uma sequência de cenas)');
|
|
132
|
+
if (usadas.size < MINIMO.pecasDiferentes) erros.push(`só ${usadas.size} peças diferentes; mínimo ${MINIMO.pecasDiferentes}`);
|
|
133
|
+
if (camadasPorCena < MINIMO.camadas) erros.push(`só ${camadasPorCena} tipos de camada; mínimo ${MINIMO.camadas} (fundo, partículas, herói, apoio)`);
|
|
134
|
+
if (chaves < MINIMO.chavesCamera) erros.push(`só ${chaves} chaves de câmera; mínimo ${MINIMO.chavesCamera}`);
|
|
135
|
+
if (efeitos < cortes * MINIMO.efeitosPorCorte) erros.push(`${efeitos} <Efeito> para ${cortes} cortes: SFX em todo corte (alvo 2 por corte)`);
|
|
136
|
+
return { ok: !erros.length, erros, medidas };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Grava a cena em <pasta>/motion/<nome>.tsx (com as peças ao lado, em motion/pecas/) e confere. */
|
|
140
|
+
function escreverCena(estilo, pasta, nome, codigo, raiz = RAIZ) {
|
|
141
|
+
const slug = String(nome || '').toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
142
|
+
if (!slug) return { ok: false, texto: 'Dê um nome à cena: … escrever <nome> --arquivo <cena.tsx>' };
|
|
143
|
+
const c = conferirCena(estilo, codigo, raiz);
|
|
144
|
+
const proibido = c.erros.some((e) => /Montagem|receita|plano|import fora/.test(e));
|
|
145
|
+
if (proibido) return { ok: false, texto: `Cena recusada:\n- ${c.erros.join('\n- ')}`, ...c };
|
|
146
|
+
const dir = path.join(pasta, 'motion');
|
|
147
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
148
|
+
const arq = path.join(dir, `${slug}.tsx`);
|
|
149
|
+
fs.writeFileSync(arq, codigo);
|
|
150
|
+
const texto = c.ok ? `Cena gravada em ${arq}. Conferida: ${JSON.stringify(c.medidas)}.`
|
|
151
|
+
: `Cena gravada em ${arq}, mas NÃO passou no conferir; conserte e grave de novo:\n- ${c.erros.join('\n- ')}`;
|
|
152
|
+
return { ok: true, passou: c.ok, texto, arquivo: arq, ...c };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Copia as peças do estilo pra <dir>/pecas e escreve a entrada do render que registra a cena da IA. */
|
|
156
|
+
function prepararRender(estilo, dir, arqCena, { duracao, raiz = RAIZ } = {}) {
|
|
157
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
158
|
+
const alvo = path.join(dir, 'pecas');
|
|
159
|
+
fs.rmSync(alvo, { recursive: true, force: true });
|
|
160
|
+
fs.cpSync(pastaDe(estilo, raiz), alvo, { recursive: true });
|
|
161
|
+
fs.copyFileSync(arqCena, path.join(dir, 'Cena-ia.tsx'));
|
|
162
|
+
fs.writeFileSync(path.join(dir, 'entrada.tsx'), `// gerado por lib/estudio/materiais.js
|
|
163
|
+
import React from 'react';
|
|
164
|
+
import {Composition, registerRoot} from 'remotion';
|
|
165
|
+
import {Video, DURACAO} from './Cena-ia';
|
|
166
|
+
const Raiz: React.FC = () => <Composition id="Video" component={Video as React.FC} durationInFrames={${duracao || 'DURACAO'}} fps={30} width={1080} height={1920} />;
|
|
167
|
+
registerRoot(Raiz);
|
|
168
|
+
`);
|
|
169
|
+
return { dir, entrada: path.join(dir, 'entrada.tsx') };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { RAIZ, CATEGORIAS, MINIMO, PROIBIDO, migrado, estilos, ler, validar, listar, lerPeca, regras, promptDoEstilo, conferirCena, escreverCena, prepararRender };
|
package/lib/fundo.js
CHANGED
|
@@ -64,6 +64,38 @@ function anexar(t, pedaco) {
|
|
|
64
64
|
if (t.log) { try { fs.writeSync(t.log, pedaco); } catch { /* disco cheio: o buffer ainda serve */ } }
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* ── AVISO DE FIM ─────────────────────────────────────────────────────────
|
|
68
|
+
* Quando uma tarefa acaba SOZINHA (não foi o agente que mandou parar), o aviso entra numa fila. O loop do agente
|
|
69
|
+
* esvazia a fila a cada volta (pegarAvisos) e põe o texto na conversa; a tela mostra uma linha; no macOS sai
|
|
70
|
+
* também uma notificação do sistema. PRIMOCODE_SEM_NOTIFICACAO=1 desliga só a do sistema. */
|
|
71
|
+
const avisos = [];
|
|
72
|
+
const ouvintes = new Set();
|
|
73
|
+
function aoTerminar(fn) { ouvintes.add(fn); return () => ouvintes.delete(fn); }
|
|
74
|
+
function pegarAvisos() { return avisos.splice(0, avisos.length); }
|
|
75
|
+
|
|
76
|
+
function notificarSistema(titulo, texto) {
|
|
77
|
+
if (process.platform !== 'darwin' || process.env.PRIMOCODE_SEM_NOTIFICACAO === '1') return false;
|
|
78
|
+
try {
|
|
79
|
+
const p = spawn('osascript', ['-e', 'on run argv\ndisplay notification (item 2 of argv) with title (item 1 of argv)\nend run', String(titulo).slice(0, 80), String(texto).slice(0, 200)],
|
|
80
|
+
{ stdio: 'ignore', detached: true });
|
|
81
|
+
p.on('error', () => {}); p.unref();
|
|
82
|
+
return true;
|
|
83
|
+
} catch { return false; }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function avisarFim(t) {
|
|
87
|
+
if (t.pedidoParar) return;
|
|
88
|
+
const cauda = t.buffer.split('\n').filter(Boolean).slice(-6).join('\n').slice(-600);
|
|
89
|
+
const ok = t.codigo === 0;
|
|
90
|
+
const curto = t.comando.length > 60 ? t.comando.slice(0, 57) + '…' : t.comando;
|
|
91
|
+
const texto = `[aviso do sistema] A tarefa em segundo plano ${t.id} (${curto}) terminou ${ok ? 'com sucesso' : `com erro (código ${t.codigo ?? t.sinal})`}.`
|
|
92
|
+
+ (cauda ? `\nFim da saída:\n${cauda}` : '') + `\nLeia tudo com background_output id:"${t.id}" se precisar.`;
|
|
93
|
+
const aviso = { id: t.id, comando: t.comando, ok, codigo: t.codigo, texto };
|
|
94
|
+
avisos.push(aviso);
|
|
95
|
+
for (const f of ouvintes) { try { f(aviso); } catch { /* ouvinte não derruba */ } }
|
|
96
|
+
notificarSistema(ok ? 'PrimoCode: tarefa terminou' : 'PrimoCode: tarefa falhou', `${curto}${ok ? '' : ` (código ${t.codigo})`}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
67
99
|
function iniciar(comando, { projectDir, env } = {}) {
|
|
68
100
|
const cmd = String(comando || '').trim();
|
|
69
101
|
if (!cmd) return { ok: false, error: 'Comando vazio.' };
|
|
@@ -101,6 +133,7 @@ function iniciar(comando, { projectDir, env } = {}) {
|
|
|
101
133
|
// 'close' (e não 'exit'): só dispara depois de a saída ser toda lida.
|
|
102
134
|
filho.on('close', (codigo, sinal) => {
|
|
103
135
|
t.rodando = false; t.codigo = codigo; t.sinal = sinal || null; t.terminou = Date.now(); fechaLog();
|
|
136
|
+
avisarFim(t);
|
|
104
137
|
});
|
|
105
138
|
tarefas.set(id, t);
|
|
106
139
|
ligarSinais();
|
|
@@ -196,4 +229,4 @@ function linhasRodape(lista, c) {
|
|
|
196
229
|
return linhas;
|
|
197
230
|
}
|
|
198
231
|
|
|
199
|
-
module.exports = { iniciar, saida, parar, pararTodas, listar, endereco, linhasRodape, TETO_BUFFER, TETO_LEITURA };
|
|
232
|
+
module.exports = { aoTerminar, pegarAvisos, avisarFim, iniciar, saida, parar, pararTodas, listar, endereco, linhasRodape, TETO_BUFFER, TETO_LEITURA };
|