primocode 9.9.0 → 10.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.
Files changed (63) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +100 -341
  3. package/lib/act.js +245 -23
  4. package/lib/browser.js +17 -0
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/config.js +10 -3
  8. package/lib/cursor.js +10 -1
  9. package/lib/entrada.js +4 -2
  10. package/lib/estudio/diretor.js +9 -1
  11. package/lib/estudio/ende.js +2 -0
  12. package/lib/estudio/estilo.js +51 -0
  13. package/lib/estudio/glifos.js +215 -0
  14. package/lib/estudio/index.js +63 -11
  15. package/lib/estudio/kit/base.jsx +285 -0
  16. package/lib/estudio/kit/index.jsx +11 -0
  17. package/lib/estudio/kit/marca.jsx +741 -0
  18. package/lib/estudio/kit/vox.jsx +357 -0
  19. package/lib/estudio/kit.js +72 -0
  20. package/lib/estudio/lancamento.js +413 -0
  21. package/lib/estudio/morfar.js +191 -0
  22. package/lib/estudio/motion.js +333 -0
  23. package/lib/estudio/plano.js +54 -7
  24. package/lib/estudio/remotion.js +167 -52
  25. package/lib/estudio/ritmo.js +77 -0
  26. package/lib/estudio/sons-leves.js +62 -0
  27. package/lib/estudio/vox.js +385 -0
  28. package/lib/fala.js +1 -94
  29. package/lib/pipeline.js +55 -56
  30. package/lib/repeticao.js +3 -1
  31. package/lib/skills.js +8 -2
  32. package/lib/snapshot.js +111 -0
  33. package/lib/tools.js +79 -20
  34. package/lib/ui.js +13 -6
  35. package/lib/video/analise.js +89 -0
  36. package/lib/video/cli.js +74 -0
  37. package/lib/video/delta.js +90 -0
  38. package/lib/video/edits.js +379 -0
  39. package/lib/video/filters.js +247 -0
  40. package/lib/video/ops.js +322 -0
  41. package/lib/video/projeto.js +133 -0
  42. package/lib/video/render.js +221 -0
  43. package/lib/video/texto.js +142 -0
  44. package/lib/video/timeline.js +310 -0
  45. package/lib/video/transcript.js +124 -0
  46. package/lib/voz.js +75 -223
  47. package/package.json +2 -3
  48. package/skills/motion-de-primeira/SKILL.md +21 -1
  49. package/studio/texto.py +208 -0
  50. package/voz/primo_voz/__main__.py +96 -236
  51. package/voz/primo_voz/transcrever.py +31 -1
  52. package/app/ansi.js +0 -55
  53. package/app/app.css +0 -500
  54. package/app/app.js +0 -1017
  55. package/app/icone.svg +0 -6
  56. package/app/index.html +0 -220
  57. package/app/primo-marca.js +0 -192
  58. package/lib/app.js +0 -615
  59. package/lib/icone.js +0 -263
  60. package/lib/instalar-app.js +0 -231
  61. package/lib/painel.js +0 -291
  62. package/voz/primo_voz/desperta.py +0 -195
  63. package/voz/primo_voz/orbe.py +0 -406
@@ -0,0 +1,385 @@
1
+ /**
2
+ * vox.js — o estilo VOX: explainer editorial, narração ilustrada.
3
+ *
4
+ * "O vídeo é uma narração ilustrada. Cada elemento existe porque uma
5
+ * palavra pediu — e entra no instante exato em que ela é dita."
6
+ * — guia MOTION VOX STYLE com Remotion
7
+ *
8
+ * Uma cena por parte, com CORTE SECO no beat. Cada cena é composta como
9
+ * revista, não como slide:
10
+ *
11
+ * · 30 a 40% do quadro é fundo puro, e a banda de cima nunca é invadida;
12
+ * · uma âncora grande (30 a 40% da largura), o resto três vezes menor;
13
+ * · UM acento, em no máximo três pontos; o resto é tinta sobre papel;
14
+ * · tudo imprime no papel: sombra de contato, tinta, grão e vinheta;
15
+ * · a câmera conta a frase: dolly do perto para o longe na abertura,
16
+ * empurrão no clímax, e acelera antes do corte;
17
+ * · parallax como consequência da câmera: quatro planos (0,12 · 0,40 ·
18
+ * 0,55 · 1,25), com o desfoque da lente em cada um.
19
+ *
20
+ * Cada tipo de parte vira um dos oito dispositivos tipográficos do kit
21
+ * (kit/vox.jsx):
22
+ *
23
+ * abertura cartão-espécime (termo curto) ou highlight na letra, no escuro
24
+ * titulo highlight na letra + source tag
25
+ * numero palavra ancorada gigante (o número) + o rótulo carimbando
26
+ * lista lista com travessão (o anterior apaga para cinza)
27
+ * passos idem, numerada 01 — 02 — 03
28
+ * contraste o "não" riscado à caneta, o "sim" carimba no acento
29
+ * citacao o documento lido em cascata, marca-texto e círculo
30
+ * imagem a foto na mesa, a pill branca e a linha-guia
31
+ * grafico as barras em tinta, a maior circulada
32
+ * cta texto-textura de fundo, a chamada carimbada e a pill
33
+ * logo a marca e o nome em espécime
34
+ * cores os cartões de cor sobre o papel
35
+ * produto o cartão do produto sobre a mesa
36
+ *
37
+ * Puro: roteiro, tempos da voz e arquivos entram; o plano sai.
38
+ */
39
+
40
+ 'use strict';
41
+
42
+ const { caber } = require('./composicao.js');
43
+ const { misturarCor, ehCor } = require('./morfar.js');
44
+ const { tempoDasPartes, gatilhos, palavrasNaCena, volumesDaTrilha, r2 } = require('./ritmo.js');
45
+
46
+ const FPS = 30;
47
+ const k = (t, v, e) => (e ? { t: r2(t), v, e } : { t: r2(t), v });
48
+ const luz = (hex) => {
49
+ const h = String(hex || '').replace('#', '');
50
+ const n = h.length === 3 ? h.split('').map((c) => c + c).join('') : h.slice(0, 6);
51
+ const [r, g, b] = [0, 2, 4].map((i) => parseInt(n.slice(i, i + 2), 16) || 0);
52
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
53
+ };
54
+ const itensDe = (p) => (p.itens || []).map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
55
+ const semAcento = (w) => String(w || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
56
+ const PEQUENAS = new Set(['a', 'o', 'as', 'os', 'um', 'uma', 'de', 'do', 'da', 'dos', 'das', 'e', 'em', 'no', 'na', 'nos', 'nas', 'que', 'se', 'por', 'pra', 'para', 'com', 'ao', 'the', 'of', 'to', 'and']);
57
+
58
+ /** A palavra-tese de uma frase: a pedida, ou a mais longa das últimas. */
59
+ function tese(frase, pedida) {
60
+ const ws = String(frase || '').split(/\s+/).filter(Boolean);
61
+ if (pedida && ws.some((w) => semAcento(w) === semAcento(pedida))) return pedida;
62
+ let melhor = '';
63
+ ws.forEach((w, i) => { const c = semAcento(w); if (!PEQUENAS.has(c) && c.length + i * 0.6 >= semAcento(melhor).length) melhor = w; });
64
+ return melhor.replace(/[.,;:!?]+$/, '');
65
+ }
66
+
67
+ /** Quebra um texto em linhas de até `n` caracteres, palavra por palavra. */
68
+ function quebrar(texto, n) {
69
+ const linhas = [];
70
+ let atual = '';
71
+ for (const w of String(texto || '').split(/\s+/).filter(Boolean)) {
72
+ if (atual && (atual + ' ' + w).length > n) { linhas.push(atual); atual = w; } else atual = atual ? atual + ' ' + w : w;
73
+ }
74
+ if (atual) linhas.push(atual);
75
+ return linhas;
76
+ }
77
+
78
+ /** "Termo — definição" (ou "termo: definição") vira {termo, def}. */
79
+ function termoDef(t) {
80
+ const m = String(t).match(/^(.{1,40}?)\s*(?:—|–|-|:)\s+(.+)$/);
81
+ return m ? { termo: m[1], def: m[2] } : { termo: String(t) };
82
+ }
83
+
84
+ /**
85
+ * @param {object} op
86
+ * @param {object} op.roteiro { marca, partes: [{tipo, titulo, sub, itens, valor, rotulo, sim, nao, autor, destaque, fonte, barras, link, src}] }
87
+ * @param {Array} op.tempos [{de, ate, palavras}] por parte (da narração)
88
+ * @param {object} op.imagens { indiceDaParte: {src, tipo} }
89
+ */
90
+ function montarVox({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null, imagens = {} }) {
91
+ const partes = (roteiro && roteiro.partes) || [];
92
+ if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
93
+ const W = largura, H = altura, deitado = W >= H;
94
+ const b = Math.min(W, H) / 1080;
95
+ const M = Math.round(Math.min(W, H) * 0.075);
96
+ const TOPO = Math.round(H * (deitado ? 0.17 : 0.2)); // a banda intocada
97
+
98
+ // ── a paleta: papel, tinta e UM acento ─────────────────────────────
99
+ const marca = roteiro.marca || {};
100
+ const cores = marca.cores || {};
101
+ const PAPEL = ehCor(cores.fundo) && luz(cores.fundo) > 0.78 ? cores.fundo : '#ece8df';
102
+ const TINTA = ehCor(cores.tinta) && luz(cores.tinta) < 0.3 ? cores.tinta : '#171614';
103
+ let ACENTO = ehCor(cores.destaque) ? cores.destaque : '#d23a2a';
104
+ for (let i = 0; i < 5 && luz(ACENTO) > 0.62; i++) ACENTO = misturarCor(ACENTO, '#000000', 0.2);
105
+ const CINZA = misturarCor(PAPEL, TINTA, 0.3);
106
+ const ESCURO = '#121110';
107
+ const F = {
108
+ titulo: (marca.fontes && marca.fontes.titulo) || 'Inter Tight',
109
+ texto: (marca.fontes && marca.fontes.texto) || 'Inter Tight',
110
+ mono: 'JetBrains Mono', serifa: 'Newsreader',
111
+ };
112
+ const base = { fonte: F.titulo, fonteMono: F.mono, cor: TINTA, corAcento: ACENTO };
113
+
114
+ // ── o tempo ────────────────────────────────────────────────────────
115
+ const leitura = (p) => 1.6 + 0.3 * String([p.titulo, p.sub, p.valor, p.rotulo, p.nao, p.sim, ...itensDe(p)].filter(Boolean).join(' ')).split(/\s+/).length;
116
+ const { ini, dur, total, temVoz } = tempoDasPartes(partes, tempos, { semVoz: leitura });
117
+ const efeitos = [];
118
+ const som = (t, nome, volume) => { if (sons[nome]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome], volume }); };
119
+
120
+ // ── as quatro profundidades, e o que mora em cada uma ──────────────
121
+ const CAMADAS = [{ p: 0.12, desfoque: 2.2 }, { p: 0.4, desfoque: 0.9 }, { p: 0.55, desfoque: 0, sujeito: true }, { p: 1.25, desfoque: 3.4 }];
122
+ const papel = (escuro) => (escuro
123
+ ? { tipo: 'forma', forma: 'retangulo', x: '-8%', y: '-8%', largura: '116%', altura: '116%', camada: 0,
124
+ fundo: { tipo: 'gradiente', radial: true, centro: '38% 45%', cores: ['#2a2723', ESCURO] } }
125
+ : { tipo: 'kit', peca: 'papel', cor: PAPEL, sobra: 1.16, camada: 0 });
126
+ /* A câmera conta a frase. Dolly do perto para o longe na abertura (3,4 →
127
+ 1,0), empurrão no clímax, e nos outros um avanço lento; nos últimos
128
+ quadros ela ACELERA — é o que vende o corte. Bezier longa, nunca mola. */
129
+ const camera = (d, modo, i) => {
130
+ const lado = i % 2 ? 1 : -1;
131
+ const fimLento = Math.max(0.4, d - 0.3);
132
+ if (modo === 'abertura') {
133
+ return { zoom: [k(0, 3.4), k(Math.min(1.8, d * 0.55), 1.0, 'camera'), k(fimLento, 1.03, 'camera'), k(d, 1.06, 'acelera')],
134
+ x: [k(0, 0), k(d, lado * 10 * b, 'camera')] };
135
+ }
136
+ const z1 = modo === 'climax' ? 1.14 : 1.045;
137
+ return { zoom: [k(0, 1.0), k(fimLento, z1, 'camera'), k(d, z1 + 0.035, 'acelera')],
138
+ x: [k(0, -lado * 14 * b), k(d, lado * 14 * b, 'camera')],
139
+ rotacao: [k(0, -lado * 0.3), k(d, lado * 0.3, 'camera')] };
140
+ };
141
+
142
+ /* O CORTE NO BEAT dentro da parte longa: passou de 5 s, a câmera pula de
143
+ enquadramento numa palavra do meio (dois quadros-chave a 1 quadro). */
144
+ const corteInterno = (cam, d, tempo, inicio) => {
145
+ if (d <= 5.2) return cam;
146
+ const ws = palavrasNaCena(tempo, inicio).filter((w) => w.t > d * 0.4 && w.t < d * 0.7);
147
+ const tc = ws.length ? ws[0].t : r2(d * 0.5);
148
+ const z = (cam.zoom || []).filter((q) => q.t < tc);
149
+ const zAntes = z.length ? z[z.length - 1].v : 1;
150
+ const zFim = cam.zoom[cam.zoom.length - 1].v;
151
+ cam.zoom = [...z, k(tc - 1 / FPS, zAntes), k(tc, zAntes + 0.09), k(d, zFim + 0.09, 'camera')];
152
+ return cam;
153
+ };
154
+
155
+ // ── as partes ──────────────────────────────────────────────────────
156
+ /* Em pé a âncora ocupa a largura toda, então ela mede um pouco menos: o
157
+ empurrão da câmera (até 8%) não pode levar a última letra para fora. */
158
+ const larguraAncora = deitado ? W * 0.64 : (W - 2 * M) * 0.9;
159
+ const cenas = partes.map((p, i) => {
160
+ const d = dur[i];
161
+ const t0 = ini[i];
162
+ const tp = tempos[i];
163
+ const g = (textos, op) => gatilhos(textos, tp, t0, d, op);
164
+ const esquerda = i % 2 === 0 || !deitado;
165
+ const xA = esquerda ? M : W - M - larguraAncora; // a âncora de um lado, o vazio do outro
166
+ const els = [];
167
+ let modo = 'normal', escuro = false, origem = null;
168
+ const titulo = p.titulo || p.texto || '';
169
+ const semMidia = p.tipo === 'imagem' && !(imagens[i] || p.src);
170
+ const tipo = semMidia ? 'titulo' : p.tipo;
171
+ const fonteTag = (texto, y, t, extra = {}) => els.push({ tipo: 'kit', peca: 'fonte', texto, x: xA, y: r2(y), t, tamanho: r2(24 * b), ...base, cor: misturarCor(TINTA, PAPEL, 0.35), ...extra });
172
+
173
+ switch (tipo) {
174
+ case 'abertura': {
175
+ escuro = true; modo = 'abertura';
176
+ const curto = titulo.replace(/\s+/g, '').length <= 12 && titulo.split(/\s+/).length <= 2;
177
+ if (curto) {
178
+ const n = titulo.length;
179
+ const tam = Math.min(170 * b, (W - 2 * M) / (n * 1.06));
180
+ const [tt] = g([titulo]);
181
+ els.push({ tipo: 'kit', peca: 'especime', termo: titulo, sub: p.sub || '', x: W / 2, y: H / 2, t: Math.min(0.35, tt), tamanho: r2(tam),
182
+ ...base, cor: '#f2eee6', corSub: '#9d968a' });
183
+ origem = [W / 2 - (n * tam * 1.02) / 2 + tam * 0.4, H / 2 - (p.sub ? tam * 0.34 : 0)];
184
+ } else {
185
+ const fit = caber(titulo, { largura: W - 2 * M, linhas: 3, max: 150 * b, min: 44 * b, fonte: F.titulo });
186
+ const alt = fit.linhas * fit.tamanho * 1.04;
187
+ els.push({ tipo: 'kit', peca: 'letra', texto: titulo, destaque: tese(titulo, p.destaque), x: M, y: r2(H * 0.55 - alt / 2), largura: W - 2 * M,
188
+ tamanho: fit.tamanho, t: 0.3, tDestaque: g([tese(titulo, p.destaque)])[0], ...base, cor: '#f2eee6' });
189
+ if (p.sub) fonteTag(p.sub, H * 0.55 + alt / 2 + 40 * b, 1.0, { cor: '#9d968a' });
190
+ origem = [M + 40 * b, H * 0.55];
191
+ }
192
+ som(t0 + 0.3, 'toque', 0.3);
193
+ break;
194
+ }
195
+ case 'numero': {
196
+ modo = 'climax';
197
+ const v = String(p.valor || titulo);
198
+ const tamN = caber(v, { largura: larguraAncora * 0.9, linhas: 1, max: 400 * b, min: 110 * b, fonte: F.titulo }).tamanho;
199
+ const rot = p.rotulo || (p.valor ? titulo : '');
200
+ const [tv, tr] = g([v, rot || v]);
201
+ const yN = H * 0.62 - tamN;
202
+ els.push({ tipo: 'kit', peca: 'palavra', palavras: [{ texto: v, t: tv, acento: true, peso: 800 }], x: xA, y: r2(yN), tamanho: tamN, ...base, sublinhar: false });
203
+ let yFim = yN + tamN * 1.02;
204
+ if (rot) {
205
+ const fr = caber(rot, { largura: larguraAncora, linhas: 2, max: 76 * b, min: 34 * b, fonte: F.texto });
206
+ const tamR = fr.tamanho;
207
+ yFim += fr.linhas * tamR * 1.08;
208
+ els.push({ tipo: 'kit', peca: 'palavra', x: xA, y: r2(yN + tamN * 1.02), largura: larguraAncora, tamanho: tamR, ...base, sublinhar: false,
209
+ palavras: rot.split(/\s+/).map((w, j) => ({ texto: w, t: r2(tr + j * 4 / FPS), peso: PEQUENAS.has(semAcento(w)) ? 300 : 500 })) });
210
+ }
211
+ // A fonte vem DEPOIS do rótulo, na altura que ele de fato ocupa (em pé ele quebra em duas linhas).
212
+ if (p.sub || p.fonte) fonteTag(p.fonte || p.sub, yFim + 36 * b, Math.min(d - 0.8, tr + 0.6));
213
+ origem = [xA + tamN, yN + tamN / 2];
214
+ som(t0 + tv, 'toque', 0.35);
215
+ break;
216
+ }
217
+ case 'lista': case 'passos': case 'contraste': {
218
+ let itens;
219
+ if (tipo === 'contraste') itens = [{ termo: p.nao || '—', riscar: true }, { termo: p.sim || titulo }];
220
+ else itens = itensDe(p).slice(0, 5).map((x, j) => (tipo === 'passos' ? { termo: String(j + 1).padStart(2, '0'), def: x } : termoDef(x)));
221
+ const maior = itens.reduce((a, x) => ((x.termo + (x.def ? ' — ' + x.def : '')).length > a.length ? x.termo + (x.def ? ' — ' + x.def : '') : a), '');
222
+ // Uma linha por item quando cabe grande; senão, duas.
223
+ const umaLinha = caber(maior, { largura: larguraAncora * (deitado ? 1.05 : 1), linhas: 1, max: (tipo === 'contraste' ? 170 : itens.length > 3 ? 80 : 96) * b, min: 30 * b, fonte: F.titulo });
224
+ const tam = umaLinha.tamanho >= 58 * b || tipo === 'contraste' ? umaLinha.tamanho
225
+ : caber(maior, { largura: larguraAncora * (deitado ? 1.05 : 1), linhas: 2, max: 80 * b, min: 30 * b, fonte: F.titulo }).tamanho;
226
+ const ts = g(itens.map((x) => (tipo === 'passos' ? x.def : x.termo)), { t0: titulo && tipo !== 'contraste' ? 0.6 : 0.3 });
227
+ itens.forEach((x, j) => { x.t = ts[j]; });
228
+ const linhasEst = itens.reduce((n, x) => n + Math.ceil(((x.termo + (x.def ? ' — ' + x.def : '')).length * tam * 0.56) / larguraAncora), 0);
229
+ const alt = linhasEst * tam * 1.08 + itens.length * tam * 0.34;
230
+ const y = Math.max(TOPO + 60 * b, H * 0.58 - alt / 2);
231
+ if (titulo && tipo !== 'contraste') fonteTag(titulo, y - 70 * b, 0.15);
232
+ els.push({ tipo: 'kit', peca: 'lista', itens, x: xA, y: r2(y), largura: larguraAncora, tamanho: tam, ...base, corVelha: CINZA });
233
+ origem = [xA + 60 * b, y + alt / 2];
234
+ itens.forEach((x) => som(t0 + x.t, 'toque', 0.22));
235
+ if (tipo === 'contraste') som(t0 + itens[0].t + 0.35, 'sopro', 0.18);
236
+ break;
237
+ }
238
+ case 'citacao': {
239
+ const frase = `“${titulo}”`;
240
+ const porLinha = deitado ? 30 : 22;
241
+ const linhas = quebrar(frase, porLinha).slice(0, 6);
242
+ const alvo = semAcento(tese(titulo, p.destaque));
243
+ const marca = Math.max(0, linhas.findIndex((l) => l.split(/\s+/).some((w) => semAcento(w) === alvo)));
244
+ const docW = deitado ? W * 0.56 : W - 2 * M;
245
+ const tam = Math.min(64 * b, docW / (porLinha * 0.5 + 2.4));
246
+ const docH = linhas.length * tam * 1.36 + tam * 3.4;
247
+ const x = esquerda ? M + 20 * b : W - M - docW - 20 * b;
248
+ const y = Math.max(TOPO, H * 0.52 - docH / 2);
249
+ const [tm] = g([linhas[marca]]);
250
+ els.push({ tipo: 'kit', peca: 'marcatexto', linhas, marca, tMarca: Math.max(0.9, tm), x: r2(x), y: r2(y), largura: r2(docW), tamanho: r2(tam), t: 0.25,
251
+ titulo: p.fonte || 'citação', fonte: F.serifa, fonteMono: F.mono, corAcento: ACENTO, fundo: misturarCor(PAPEL, '#ffffff', 0.55), rotacao: esquerda ? -0.7 : 0.6 });
252
+ if (p.autor) els.push({ tipo: 'kit', peca: 'fonte', texto: '— ' + p.autor, x: r2(esquerda ? x + docW + 50 * b : x - 50 * b), y: r2(y + docH - 40 * b),
253
+ alinhamento: esquerda ? 'left' : 'right', t: Math.min(d - 0.8, Math.max(1.4, tm + 0.8)), tamanho: r2(26 * b), ...base, cor: misturarCor(TINTA, PAPEL, 0.3) });
254
+ origem = [x + docW / 2, y + (marca + 2) * tam * 1.36];
255
+ som(t0 + Math.max(0.9, tm), 'sopro', 0.15);
256
+ break;
257
+ }
258
+ case 'imagem': {
259
+ const m = imagens[i] || { src: p.src, tipo: 'imagem' };
260
+ const iw = deitado ? W * 0.5 : W - 2 * M, ih = iw * (deitado ? 0.62 : 0.75);
261
+ const ix = deitado ? (esquerda ? W - M - iw : M) : M, iy = deitado ? Math.max(TOPO, H * 0.52 - ih / 2) : TOPO + 40 * b;
262
+ els.push({ tipo: m.tipo === 'video' ? 'video' : 'imagem', src: m.src, ...(m.tipo === 'video' ? { volume: 0 } : {}), x: r2(ix), y: r2(iy), largura: r2(iw), altura: r2(ih),
263
+ ajuste: 'cover', raio: 4, sombra: '0 1px 1px rgba(20,16,10,0.25), 0 20px 40px -14px rgba(20,16,10,0.45)',
264
+ anima: { rotacao: [k(0, esquerda ? 0.8 : -0.8)], escala: [k(0, 1.0), k(d, 1.025, 'camera')] } });
265
+ if (titulo) {
266
+ const tamP = Math.min(50 * b, (deitado ? W * 0.36 : W - 2 * M) / (titulo.length * 0.72));
267
+ const px = deitado ? (esquerda ? ix - 40 * b - titulo.length * tamP * 0.36 : ix + iw + 40 * b + titulo.length * tamP * 0.36) : W / 2;
268
+ const py = deitado ? iy + ih * 0.78 : iy + ih + 150 * b;
269
+ const pxOk = Math.min(W - M - titulo.length * tamP * 0.4, Math.max(M + titulo.length * tamP * 0.4, px));
270
+ els.push({ tipo: 'kit', peca: 'pilula', texto: titulo, sub: p.sub || '', x: r2(pxOk), y: r2(py), alvo: { x: r2(ix + iw * (esquerda ? 0.3 : 0.7)), y: r2(iy + ih * 0.42) },
271
+ t: g([titulo])[0], tamanho: r2(tamP), ...base, cor: '#ffffff', corTexto: TINTA, corLinha: TINTA });
272
+ }
273
+ if (p.fonte) els.push({ tipo: 'kit', peca: 'fonte', texto: p.fonte, x: r2(ix), y: r2(iy + ih + 24 * b), t: 0.8, tamanho: r2(18 * b), ...base, cor: misturarCor(TINTA, PAPEL, 0.35) });
274
+ origem = [ix + iw / 2, iy + ih / 2];
275
+ break;
276
+ }
277
+ case 'grafico': {
278
+ const barras = (p.barras || itensDe(p).map((x, j) => ({ rotulo: x, valor: itensDe(p).length - j }))).slice(0, 6);
279
+ const gw = deitado ? W * 0.6 : W - 2 * M;
280
+ const tamB = Math.min(44 * b, (H * 0.46) / (barras.length * 2.2));
281
+ const altB = barras.length * tamB * 1.5 * 1.45;
282
+ const y = Math.max(TOPO + 130 * b, H * 0.6 - altB / 2);
283
+ if (titulo) {
284
+ const tt = caber(titulo, { largura: gw, linhas: 2, max: 96 * b, min: 34 * b, fonte: F.titulo }).tamanho;
285
+ els.push({ tipo: 'kit', peca: 'letra', texto: titulo, destaque: tese(titulo, p.destaque), x: xA, y: r2(y - tt * 1.1 * Math.min(2, Math.ceil(titulo.length * tt * 0.56 / gw)) - 40 * b),
286
+ largura: r2(gw), tamanho: tt, t: 0.2, ...base });
287
+ }
288
+ els.push({ tipo: 'kit', peca: 'barras', barras, x: xA, y: r2(y), largura: r2(gw), tamanho: r2(tamB), t: g([String(barras[0].rotulo)])[0] + 0.1, ...base });
289
+ if (p.fonte) fonteTag(p.fonte, y + altB + 20 * b, 1.6);
290
+ origem = [xA + gw / 2, y + altB / 2];
291
+ break;
292
+ }
293
+ case 'cta': {
294
+ modo = 'climax';
295
+ const fundo = p.link || marca.nome || titulo;
296
+ els.push({ tipo: 'kit', peca: 'textura', texto: fundo, largura: W * 1.16, altura: H * 1.16, x: -W * 0.08, y: -H * 0.08, tamanho: r2((deitado ? 96 : 84) * b),
297
+ fonte: F.titulo, cor: TINTA, t: 0.05, camada: 1, velocidade: 30 });
298
+ const fit = caber(titulo, { largura: W - 2 * M, linhas: 2, max: 150 * b, min: 44 * b, fonte: F.titulo });
299
+ const alt = fit.linhas * fit.tamanho * 1.02;
300
+ const ts = g(titulo.split(/\s+/).slice(0, 1));
301
+ const alvo = semAcento(tese(titulo, p.destaque));
302
+ els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: r2(H / 2 - alt / 2 - 50 * b), largura: '100%', altura: r2(alt + 100 * b), cor: PAPEL, opacidade: 0.94, camada: 1 });
303
+ els.push({ tipo: 'kit', peca: 'palavra', x: W / 2, y: r2(H / 2 - alt / 2), largura: W - 2 * M, tamanho: fit.tamanho, alinhamento: 'center', ...base,
304
+ palavras: titulo.split(/\s+/).map((w, j) => ({ texto: w, t: r2(ts[0] + j * 5 / FPS), acento: semAcento(w) === alvo, peso: PEQUENAS.has(semAcento(w)) ? 400 : 700 })) });
305
+ if (p.link) els.push({ tipo: 'kit', peca: 'pilula', texto: p.link, x: W / 2, y: r2(H / 2 + alt / 2 + 110 * b), alvo: { x: W / 2, y: r2(H / 2 + alt / 2 + 20 * b) },
306
+ t: Math.min(d - 0.9, ts[0] + 0.7), tamanho: r2(34 * b), ...base, cor: '#ffffff', corTexto: TINTA, corLinha: TINTA });
307
+ origem = [W / 2, H / 2];
308
+ som(t0 + ts[0], 'toque', 0.35);
309
+ break;
310
+ }
311
+ case 'logo': {
312
+ escuro = true; modo = 'abertura';
313
+ const nome = p.titulo || marca.nome || '';
314
+ const tam = Math.min(130 * b, (W - 2 * M) / (Math.max(4, nome.length) * 1.06));
315
+ if (marca.logo) els.push({ tipo: 'imagem', src: marca.logo, x: W / 2, y: r2(H / 2 - tam * 1.5), largura: r2(tam * 1.4), altura: r2(tam * 1.4), ancora: 'centro', ajuste: 'contain', de: 0.1 });
316
+ els.push({ tipo: 'kit', peca: 'especime', termo: nome, sub: p.sub || '', x: W / 2, y: r2(H / 2 + (marca.logo ? tam * 0.4 : 0)), t: 0.4, tamanho: r2(tam), ...base, cor: '#f2eee6', corSub: '#9d968a' });
317
+ origem = [W / 2, H / 2];
318
+ break;
319
+ }
320
+ case 'cores': {
321
+ const lista = (p.cores || [cores.fundo, cores.destaque, cores.tinta, cores.apoio].filter(ehCor).map((hex, j) => ({ nome: ['Fundo', 'Destaque', 'Tinta', 'Apoio'][j], hex })))
322
+ .slice(0, 5).map((c) => (typeof c === 'string' ? { nome: c, hex: c } : c));
323
+ if (titulo) fonteTag(titulo, TOPO, 0.2);
324
+ els.push({ tipo: 'kit', peca: 'cores', cores: lista.length ? lista : [{ nome: 'Tinta', hex: TINTA }, { nome: 'Papel', hex: PAPEL }, { nome: 'Acento', hex: ACENTO }],
325
+ x: W / 2, y: r2(H * 0.56), largura: r2((deitado ? 300 : 280) * b), raioX: r2(deitado ? W * 0.3 : W * 0.28), raioY: r2(deitado ? H * 0.2 : H * 0.18), t: 0.3, ...base });
326
+ origem = [W / 2, H * 0.56];
327
+ break;
328
+ }
329
+ case 'produto': {
330
+ const pw = deitado ? W * 0.62 : W - 2 * M;
331
+ if (titulo) fonteTag(titulo, TOPO - 20 * b, 0.1, { x: r2((W - pw) / 2) });
332
+ els.push({ tipo: 'kit', peca: 'painel', titulo: marca.nome || titulo, texto: p.texto || p.sub || titulo, destaque: p.destaque || tese(p.texto || p.sub || titulo), selo: p.selo,
333
+ barras: p.barras, x: r2((W - pw) / 2), y: r2(Math.max(TOPO, H * 0.5 - pw * 0.22)), largura: r2(pw), t: 0.2, ...base, fonte: F.texto });
334
+ origem = [W / 2, H / 2];
335
+ break;
336
+ }
337
+ default: { // titulo e o que mais vier
338
+ const fit = caber(titulo, { largura: larguraAncora, linhas: 3, max: 160 * b, min: 44 * b, fonte: F.titulo });
339
+ const alt = fit.linhas * fit.tamanho * 1.04;
340
+ const y = Math.max(TOPO + 20 * b, H * 0.7 - alt);
341
+ const alvo = tese(titulo, p.destaque);
342
+ const [tA] = g([alvo]);
343
+ /* Do lado direito o texto encosta na margem direita: alinhado à
344
+ esquerda numa caixa larga, ele flutuaria no meio e a câmera o
345
+ empurraria para fora do quadro. */
346
+ const dir = !esquerda;
347
+ els.push({ tipo: 'kit', peca: 'letra', texto: titulo, destaque: alvo, x: r2(dir ? W - M : xA), y: r2(y), largura: r2(larguraAncora), tamanho: fit.tamanho, t: 0.12,
348
+ tDestaque: Math.max(0.5, tA), alinhamento: dir ? 'right' : 'left', ...base });
349
+ if (p.sub) fonteTag(p.sub, y + alt + 36 * b, Math.min(d - 0.8, Math.max(0.9, tA + 0.5)), dir ? { x: r2(W - M), alinhamento: 'right' } : {});
350
+ // A câmera ancora na margem do texto: o empurrão cresce para dentro, nunca para fora do quadro.
351
+ origem = [dir ? W - M : xA, y + alt / 2];
352
+ som(t0 + Math.max(0.5, tA), 'sopro', 0.14);
353
+ }
354
+ }
355
+
356
+ const cam = corteInterno(camera(d, modo, i), d, tp, t0);
357
+ return {
358
+ duracao: d,
359
+ fundo: escuro ? ESCURO : PAPEL,
360
+ escuro,
361
+ secao: String(i + 1).padStart(2, '0') + ' — ' + (p.tipo || 'parte'),
362
+ camadas: CAMADAS,
363
+ camera: cam,
364
+ cameraOrigem: origem ? `${r2(origem[0])}px ${r2(origem[1])}px` : 'center center',
365
+ transicao: 'corte',
366
+ elementos: [papel(escuro), ...els],
367
+ };
368
+ });
369
+
370
+ const plano = {
371
+ formato: { largura: W, altura: H, fps: FPS },
372
+ fundoGeral: PAPEL,
373
+ estilo: 'vox',
374
+ acabamento: { grao: 0.06, vinheta: 0.22, corVinheta: '40,30,18' },
375
+ cenas,
376
+ audio: {
377
+ ...(narracao ? { narracao } : {}),
378
+ ...(trilha ? { trilha: { src: trilha, volumes: volumesDaTrilha(temVoz ? tempos : [], total), loop: true } } : {}),
379
+ efeitos,
380
+ },
381
+ };
382
+ return { ok: true, plano, resumo: { cenas: cenas.length, segundos: total, partes: partes.length, fps: FPS, estilo: 'vox' } };
383
+ }
384
+
385
+ module.exports = { montarVox, tese, quebrar, termoDef };
package/lib/fala.js CHANGED
@@ -41,91 +41,6 @@ const { EventEmitter } = require('events');
41
41
  const { c, actionHeader, friendlyError } = require('./ui');
42
42
  const markdown = require('./markdown.js');
43
43
 
44
- /* ── O CANAL ESTRUTURADO ─────────────────────────────────────────────────
45
- * A janela do app (lib/app.js) mostra a conversa como CHAT: a fala do Primo
46
- * em texto e cada ação dele numa linha cinza ("Leu index.html", "Rodou npm
47
- * test ✓"). Adivinhar isso relendo a saída colorida do terminal seria frágil
48
- * — um rótulo que muda aqui quebraria a janela calado. Então a MESMA função
49
- * que desenha no terminal também avisa, por este canal, o que desenhou.
50
- *
51
- * Sem ouvinte (terminal puro), `avisar` não faz nada: nenhum custo para quem
52
- * não abriu a janela. */
53
- const eventos = new EventEmitter();
54
- eventos.setMaxListeners(20);
55
- function avisar(tipo, dados) {
56
- if (!eventos.listenerCount('evento')) return;
57
- try { eventos.emit('evento', Object.assign({ tipo }, dados || {})); } catch { /* a janela nunca derruba o agente */ }
58
- }
59
-
60
- /* O que a linha de ação da janela diz, por ferramenta. Verbo no passado
61
- porque a linha fica na conversa depois que a ação acabou — "Lendo" viraria
62
- mentira um segundo depois. O ícone é uma família, não um desenho por tool:
63
- a página tem dez ícones, não setenta. */
64
- const VERBOS = {
65
- read_file: ['Leu', 'ler'], write_file: ['Escreveu', 'escrever'], edit_file: ['Editou', 'editar'],
66
- multi_edit: ['Editou', 'editar'], list_dir: ['Listou', 'buscar'], glob: ['Procurou', 'buscar'],
67
- grep: ['Buscou', 'buscar'], run_shell: ['Rodou', 'rodar'], run_in_new_terminal: ['Abriu um terminal com', 'rodar'],
68
- open_in_browser: ['Abriu', 'web'], spawn_agent: ['Chamou um agente', 'agente'], write_todos: ['Planejou', 'plano'],
69
- finish: ['Concluiu', 'plano'], pesquisar_referencia: ['Pesquisou', 'web'], remember_general: ['Guardou na memória', 'plano'],
70
- planejar_producao: ['Planejou a produção', 'agente'], construir_app: ['Construiu', 'agente'],
71
- };
72
-
73
- /** Verbo, alvo e ícone de uma ação — só o que cabe numa linha, nunca o
74
- * conteúdo inteiro de um write_file. */
75
- function resumoDaAcao(name, args) {
76
- const a = args || {};
77
- let [verbo, icone] = VERBOS[name] || [];
78
- if (!verbo) {
79
- if (/^browser_/.test(name)) { verbo = 'Navegador · ' + name.slice(8); icone = 'web'; }
80
- else if (/^desktop_/.test(name)) { verbo = 'Computador · ' + name.slice(8).replace(/_/g, ' '); icone = 'computador'; }
81
- else if (/^estudio_/.test(name)) { verbo = 'Estúdio · ' + name.slice(8); icone = 'agente'; }
82
- else { verbo = name; icone = 'outro'; }
83
- }
84
- let alvo = a.path || a.command || a.pattern || a.consulta || a.url || a.target || a.app || a.selector
85
- || a.titulo || a.description || a.arquivo || (a.todos ? `${a.todos.length} tarefas` : '') || '';
86
- if (typeof alvo !== 'string') alvo = '';
87
- alvo = alvo.replace(/\s+/g, ' ').trim();
88
- if (alvo.length > 120) alvo = alvo.slice(0, 119) + '…';
89
- return { nome: name, verbo, alvo, icone };
90
- }
91
-
92
- /** O resultado de uma ação, resumido para a linha da janela: ok ou não, o
93
- * "+12 −3" das edições e até 40 linhas de detalhe para quem expandir. */
94
- function resumoDoResultado(name, r) {
95
- if (!r || typeof r !== 'object') return { nome: name, ok: true };
96
- if (r.ok === false) {
97
- return { nome: name, ok: false, resumo: String(r.error || `exit ${r.code ?? 'erro'}`).replace(/\s+/g, ' ').trim().slice(0, 300) };
98
- }
99
- const fora = { nome: name, ok: true };
100
- if (typeof r._antes === 'string' && typeof r._depois === 'string') {
101
- try {
102
- const diff = require('./diff.js');
103
- const n = r._antes.split('\n').length + r._depois.split('\n').length;
104
- if (n <= diff.MAX_LINHAS_ARQUIVO * 2) {
105
- const ops = diff.operacoes(r._antes, r._depois);
106
- Object.assign(fora, diff.contar(ops));
107
- fora.detalhe = ops.filter((o) => o.tipo !== ' ').slice(0, 40).map((o) => o.tipo + ' ' + o.texto).join('\n');
108
- }
109
- } catch { /* sem diff: a linha sai sem os números */ }
110
- }
111
- if (name === 'write_file') fora.criado = !!r.created;
112
- if (name === 'run_shell' || name === 'run_in_new_terminal') {
113
- const saida = String(r.stdout || '').trim() + (r.stderr ? '\n' + String(r.stderr).trim() : '');
114
- fora.detalhe = saida.split('\n').slice(-40).join('\n').trim();
115
- fora.codigo = r.code ?? 0;
116
- }
117
- const plural = (n, um, varios) => `${n} ${n === 1 ? um : varios}`;
118
- if (name === 'read_file') fora.resumo = plural(r.linhas || 0, 'linha', 'linhas');
119
- if (name === 'grep') fora.resumo = plural(r.total || 0, 'ocorrência', 'ocorrências');
120
- if (name === 'glob') fora.resumo = plural(r.total || 0, 'arquivo', 'arquivos');
121
- if (name === 'list_dir' && r.tree) fora.detalhe = String(r.tree).split('\n').slice(0, 40).join('\n');
122
- if (name === 'write_todos' && Array.isArray(r.todos)) {
123
- fora.detalhe = r.todos.map((t) => (t.status === 'completed' ? '✓ ' : '○ ') + t.content).join('\n');
124
- fora.resumo = `${r.feitos}/${r.total}`;
125
- }
126
- return fora;
127
- }
128
-
129
44
  /* Até este tamanho, o texto que o modelo escreve antes de chamar uma tool é
130
45
  guardado e sai de uma vez, como pensamento. Passou disso sem tool à vista,
131
46
  é resposta — e volta a correr na tela conforme chega. Uma frase de
@@ -144,7 +59,6 @@ function pensamento(texto, profundidade) {
144
59
  if (!ls.length || !ls[0]) return '';
145
60
  const p = ident(profundidade);
146
61
  const saida = ls.map((l, i) => `${p} ${i === 0 ? c.dim('✦') : ' '} ${c.muted(l)}`).join('\n');
147
- avisar('pensamento', { texto: ls.join('\n'), prof: profundidade || 0 });
148
62
  console.log(saida);
149
63
  return saida;
150
64
  }
@@ -152,7 +66,6 @@ function pensamento(texto, profundidade) {
152
66
  /** A ação (tool) que ele decidiu fazer. */
153
67
  function acao(name, args, profundidade) {
154
68
  const saida = ident(profundidade) + actionHeader(name, args);
155
- avisar('acao', Object.assign(resumoDaAcao(name, args), { prof: profundidade || 0 }));
156
69
  console.log(saida);
157
70
  return saida;
158
71
  }
@@ -190,7 +103,6 @@ function resposta(texto) {
190
103
  const largura = Math.max(40, (process.stdout.columns || 80) - 4);
191
104
  const linhasMd = markdown.renderizar(texto, largura);
192
105
  const saida = '\n' + linhasMd.map((l, i) => (i === 0 ? `${c.brand('●')} ${l}` : ` ${l}`)).join('\n');
193
- avisar('resposta', { texto: String(texto) });
194
106
  console.log(saida);
195
107
  return saida;
196
108
  }
@@ -200,7 +112,6 @@ function resposta(texto) {
200
112
  function aviso(texto) {
201
113
  if (!String(texto || '').trim()) return '';
202
114
  const saida = '\n' + formatar(texto, c.warn('●'));
203
- avisar('aviso', { texto: String(texto) });
204
115
  console.log(saida);
205
116
  return saida;
206
117
  }
@@ -244,11 +155,9 @@ function fluxo() {
244
155
  escrever(delta) {
245
156
  const t = String(delta || '');
246
157
  if (!t) return;
247
- avisar('pedaco', { texto: t });
248
158
  sair(desenho.pedaco(t));
249
159
  },
250
160
  fechar() {
251
- avisar('fim', {});
252
161
  sair(desenho.fim());
253
162
  if (escreveu) process.stdout.write('\n');
254
163
  },
@@ -266,7 +175,6 @@ function paralelo(quantos, profundidade) {
266
175
  /** Um agente delegado começando: "◆ Agente·frontend a folha de estilo". */
267
176
  function agente(rotulo, descricao, profundidade) {
268
177
  const saida = `${ident(profundidade)} ${c.brand('◆')} ${c.bold(rotulo || 'Agente')}${descricao ? ' ' + c.muted(descricao) : ''}`;
269
- avisar('acao', { nome: 'agente', verbo: rotulo || 'Agente', alvo: String(descricao || '').slice(0, 120), icone: 'agente', prof: profundidade || 0 });
270
178
  console.log(saida);
271
179
  return saida;
272
180
  }
@@ -275,9 +183,8 @@ function agente(rotulo, descricao, profundidade) {
275
183
  function agenteVoltou(resumo, profundidade) {
276
184
  const texto = String(resumo || 'sem resultado').replace(/\s+/g, ' ').trim();
277
185
  const saida = resultado(profundidade) + c.muted(texto.length > 140 ? texto.slice(0, 139) + '…' : texto);
278
- avisar('resultado', { nome: 'agente', ok: true, detalhe: texto.slice(0, 2000) });
279
186
  console.log(saida);
280
187
  return saida;
281
188
  }
282
189
 
283
- module.exports = { eventos, avisar, resumoDaAcao, resumoDoResultado, LIMIAR_PENSAMENTO, pensamento, acao, resultado, recuo, resposta, aviso, problema, frase, fluxo, paralelo, agente, agenteVoltou };
190
+ module.exports = { LIMIAR_PENSAMENTO, pensamento, acao, resultado, recuo, resposta, aviso, problema, frase, fluxo, paralelo, agente, agenteVoltou };