primocode 9.10.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 (60) 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/estilo.js +51 -0
  12. package/lib/estudio/glifos.js +215 -0
  13. package/lib/estudio/index.js +41 -18
  14. package/lib/estudio/kit/base.jsx +285 -0
  15. package/lib/estudio/kit/index.jsx +11 -0
  16. package/lib/estudio/kit/marca.jsx +741 -0
  17. package/lib/estudio/kit/vox.jsx +357 -0
  18. package/lib/estudio/kit.js +72 -0
  19. package/lib/estudio/lancamento.js +413 -0
  20. package/lib/estudio/motion.js +2 -5
  21. package/lib/estudio/plano.js +28 -3
  22. package/lib/estudio/remotion.js +87 -43
  23. package/lib/estudio/ritmo.js +77 -0
  24. package/lib/estudio/vox.js +385 -0
  25. package/lib/fala.js +1 -94
  26. package/lib/pipeline.js +55 -56
  27. package/lib/repeticao.js +3 -1
  28. package/lib/skills.js +8 -2
  29. package/lib/snapshot.js +111 -0
  30. package/lib/tools.js +79 -20
  31. package/lib/ui.js +13 -6
  32. package/lib/video/analise.js +89 -0
  33. package/lib/video/cli.js +74 -0
  34. package/lib/video/delta.js +90 -0
  35. package/lib/video/edits.js +379 -0
  36. package/lib/video/filters.js +247 -0
  37. package/lib/video/ops.js +322 -0
  38. package/lib/video/projeto.js +133 -0
  39. package/lib/video/render.js +221 -0
  40. package/lib/video/texto.js +142 -0
  41. package/lib/video/timeline.js +310 -0
  42. package/lib/video/transcript.js +124 -0
  43. package/lib/voz.js +75 -223
  44. package/package.json +2 -3
  45. package/skills/motion-de-primeira/SKILL.md +3 -4
  46. package/studio/texto.py +208 -0
  47. package/voz/primo_voz/__main__.py +96 -236
  48. package/voz/primo_voz/transcrever.py +31 -1
  49. package/app/ansi.js +0 -55
  50. package/app/app.css +0 -500
  51. package/app/app.js +0 -1017
  52. package/app/icone.svg +0 -6
  53. package/app/index.html +0 -220
  54. package/app/primo-marca.js +0 -192
  55. package/lib/app.js +0 -615
  56. package/lib/icone.js +0 -263
  57. package/lib/instalar-app.js +0 -231
  58. package/lib/painel.js +0 -291
  59. package/voz/primo_voz/desperta.py +0 -195
  60. package/voz/primo_voz/orbe.py +0 -406
package/lib/cursor.js CHANGED
@@ -95,7 +95,16 @@ function subir() {
95
95
  };
96
96
  proc.on('exit', morreu);
97
97
  proc.on('error', morreu);
98
- setTimeout(() => { if (!subiu) { semCursor = 'o cursor demorou demais para abrir'; resolve(false); } }, 20000);
98
+ // Demorou demais: desiste E mata — senão o Python ficava órfão, e o
99
+ // `pronto` guardado como false deixava o cursor fora a sessão toda.
100
+ const teto = setTimeout(() => {
101
+ if (subiu) return;
102
+ semCursor = 'o cursor demorou demais para abrir';
103
+ try { proc && proc.kill(); } catch { /* já morreu */ }
104
+ pronto = null;
105
+ resolve(false);
106
+ }, 20000);
107
+ if (teto.unref) teto.unref();
99
108
  // O cursor não pode segurar o PrimoCode aberto na saída.
100
109
  proc.unref();
101
110
  proc.stdout.unref && proc.stdout.unref();
package/lib/entrada.js CHANGED
@@ -143,7 +143,8 @@ function montar(e) {
143
143
  if (i === linhas.length - 1 && e.fantasma && cursor === String(e.texto).length) {
144
144
  conteudo += c.dim(e.fantasma);
145
145
  }
146
- if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
146
+ if (i === 0 && vazio && e.ocupado) conteudo = c.dim('mande um recado — ele entra na tarefa depois do passo atual');
147
+ else if (i === 0 && vazio && e.vazio) conteudo = c.dim(e.vazio);
147
148
  // Sem `encher`: sem barra à direita não há nada para alinhar, e o
148
149
  // espaço sobrando só apagaria o que estivesse embaixo.
149
150
  out.push(' ' + marca + conteudo);
@@ -315,7 +316,8 @@ class Entrada extends EventEmitter {
315
316
  suspender() { this.suspenso = true; this._apagar(); }
316
317
  retomar() { this.suspenso = false; if (this.ativo && !this.fechado) this._desenhar(); }
317
318
 
318
- /** O agente está trabalhando: a caixa esmaece e o Enter não manda nada. */
319
+ /** O agente está trabalhando: a caixa esmaece, e o Enter manda um RECADO
320
+ * que entra na tarefa em curso (a caixa de entrada de lib/act.js). */
319
321
  trabalhando(sim) {
320
322
  if (this.ocupado === !!sim) return;
321
323
  this.ocupado = !!sim;
@@ -62,6 +62,14 @@ function precisaDeProducao(texto) {
62
62
  return { producao: false };
63
63
  }
64
64
 
65
+ /* Editar a GRAVAÇÃO da pessoa não é produzir vídeo do zero: aí o caminho é o
66
+ editor por linha do tempo (`primocode video`, lib/video/), não o roteiro. */
67
+ const PEDE_EDICAO = /\b(edit[ae]r?|edi[cç][aã]o|cort[ae]r?|legend[ae]r?|tir[ae]r?\s+(o|os)\s+sil[eê]ncios?)\b[\s\S]{0,80}\b(grava[cç][aã]o|meu\s+v[ií]deo|esse\s+v[ií]deo|este\s+v[ií]deo|[\w-]+\.(mp4|mov|mkv|webm|m4v))\b|\b[\w-]+\.(mp4|mov|mkv|webm|m4v)\b[\s\S]{0,80}\b(edit|cort|legend|sil[eê]nci)/i;
68
+
69
+ function precisaDeEdicao(texto) {
70
+ return PEDE_EDICAO.test(String(texto || ''));
71
+ }
72
+
65
73
  // ── 2. as decisões que não são do modelo ─────────────────────────────────
66
74
 
67
75
  /**
@@ -293,7 +301,7 @@ function pauta(plano) {
293
301
  }
294
302
 
295
303
  module.exports = {
296
- precisaDeProducao, vozPara, ritmoPara,
304
+ precisaDeProducao, precisaDeEdicao, vozPara, ritmoPara,
297
305
  normalizarPlano, extrairJSON, planejar, pauta,
298
306
  DIRETOR, REVISAO,
299
307
  };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * estilo.js — qual estilo de vídeo um roteiro vira.
3
+ *
4
+ * motion o padrão: um plano contínuo em 3D, o protagonista que se deforma (motion.js)
5
+ * vox explainer editorial: tinta sobre papel, os oito dispositivos VOX (vox.js)
6
+ * marca brand reveal: o brilho, o vetor, a marca cromada, o padrão (lancamento.js)
7
+ * lancamento filme de lançamento de produto: cartões, painel, calendário, mural (lancamento.js)
8
+ * impacto anúncio: corte a cada batida (impacto.js)
9
+ * cinema filme de marca premium com fotos reais (cinema.js)
10
+ * composicao a composição antiga, uma cena por parte (composicao.js)
11
+ *
12
+ * Quem manda, em ordem: o estilo que o AGENTE pediu; o do roteiro (a menos
13
+ * que o pedido diga motion); o que o pedido descreve; e o motion.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const ESTILOS = ['motion', 'vox', 'marca', 'lancamento', 'impacto', 'cinema', 'composicao'];
19
+ // Nomes que o modelo usa para os mesmos estilos.
20
+ const APELIDOS = { limpo: 'motion', explainer: 'vox', editorial: 'vox', brand: 'marca', branding: 'marca', 'brand-reveal': 'marca', launch: 'lancamento', 'lançamento': 'lancamento', produto: 'lancamento' };
21
+
22
+ const PEDE_MOTION = /\bmotion\b|anima[çc][ãa]o|animad[oa]/i;
23
+ const PEDE_VOX = /\bvox\b|explica|explainer|document[aá]rio|editorial|hist[oó]ria d[aoe]s?\b/i;
24
+ const PEDE_MARCA = /\bbrand|identidade visual|revela[çc][ãa]o d[ae] marca|\blogo(tipo|marca)?\b|vinheta de marca/i;
25
+ const PEDE_LANCAMENTO = /lan[çc]amento|\blaunch|\bsaas\b|produto novo|apresenta(ndo|r|ção d[oe]) (o |a )?(app|produto|plataforma|ferramenta)/i;
26
+ const PEDE_IMPACTO = /an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i;
27
+
28
+ const normal = (e) => {
29
+ const x = String(e || '').trim().toLowerCase();
30
+ if (!x) return null;
31
+ const n = APELIDOS[x] || x;
32
+ return ESTILOS.includes(n) ? n : null;
33
+ };
34
+
35
+ function escolherEstilo({ pedido = '', agente = null, roteiro = null } = {}) {
36
+ const pedidoAgente = normal(agente);
37
+ if (pedidoAgente) return pedidoAgente;
38
+ const doRoteiro = normal(roteiro);
39
+ if (doRoteiro && !(doRoteiro !== 'motion' && PEDE_MOTION.test(pedido) && !PEDE_VOX.test(pedido))) return doRoteiro;
40
+ if (PEDE_VOX.test(pedido)) return 'vox';
41
+ if (PEDE_MARCA.test(pedido)) return 'marca';
42
+ if (PEDE_LANCAMENTO.test(pedido)) return 'lancamento';
43
+ if (PEDE_MOTION.test(pedido)) return 'motion';
44
+ if (PEDE_IMPACTO.test(pedido)) return 'impacto';
45
+ return 'motion';
46
+ }
47
+
48
+ /** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
49
+ const LEVES = new Set(['motion', 'vox', 'marca', 'lancamento']);
50
+
51
+ module.exports = { escolherEstilo, ESTILOS, LEVES, PEDE_MOTION };
@@ -0,0 +1,215 @@
1
+ /**
2
+ * glifos.js — as letras de uma palavra como VETOR de verdade: o contorno, as
3
+ * âncoras e as alças, lidos da própria fonte TrueType.
4
+ *
5
+ * A cena do brand reveal que mais vende "feito por designer" é a palavra
6
+ * enorme em contorno, com os quadradinhos das âncoras, as alças de bezier e o
7
+ * cursor passeando. Isso só convence com os pontos REAIS da letra — pontos
8
+ * inventados em cima de um <text> denunciam na hora. Então o PrimoCode lê a
9
+ * fonte: a tabela glyf de um .ttf guarda cada contorno como pontos na curva
10
+ * (âncoras) e fora dela (controles de quadrática, as alças).
11
+ *
12
+ * Zero dependência: um leitor de TrueType de ~150 linhas (cmap formatos 4 e
13
+ * 12, loca, glyf simples e composto, hmtx). A fonte vem do Google Fonts
14
+ * (a API antiga devolve .ttf) e fica em cache; sem rede, `palavra()` devolve
15
+ * null e a peça "vetor" desenha o contorno do próprio texto, sem alças.
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const os = require('os');
22
+ const path = require('path');
23
+
24
+ // ── o leitor ─────────────────────────────────────────────────────────────
25
+ function tabelas(buf) {
26
+ const n = buf.readUInt16BE(4);
27
+ const t = {};
28
+ for (let i = 0; i < n; i++) {
29
+ const o = 12 + i * 16;
30
+ t[buf.toString('latin1', o, o + 4)] = { off: buf.readUInt32BE(o + 8), len: buf.readUInt32BE(o + 12) };
31
+ }
32
+ return t;
33
+ }
34
+
35
+ /** Caractere → índice do glifo (cmap 3/10 formato 12, ou 3/1 e 0/x formato 4). */
36
+ function mapaDeCaracteres(buf, cmap) {
37
+ const n = buf.readUInt16BE(cmap.off + 2);
38
+ const subs = [];
39
+ for (let i = 0; i < n; i++) {
40
+ const o = cmap.off + 4 + i * 8;
41
+ subs.push({ plat: buf.readUInt16BE(o), enc: buf.readUInt16BE(o + 2), off: cmap.off + buf.readUInt32BE(o + 4) });
42
+ }
43
+ subs.sort((a, b) => buf.readUInt16BE(b.off) - buf.readUInt16BE(a.off)); // o formato 12 primeiro
44
+ for (const s of subs) {
45
+ const fmt = buf.readUInt16BE(s.off);
46
+ if (fmt === 12) {
47
+ const grupos = buf.readUInt32BE(s.off + 12);
48
+ return (cp) => {
49
+ for (let g = 0; g < grupos; g++) {
50
+ const o = s.off + 16 + g * 12;
51
+ const ini = buf.readUInt32BE(o), fim = buf.readUInt32BE(o + 4);
52
+ if (cp >= ini && cp <= fim) return buf.readUInt32BE(o + 8) + (cp - ini);
53
+ }
54
+ return 0;
55
+ };
56
+ }
57
+ if (fmt === 4) {
58
+ const segs = buf.readUInt16BE(s.off + 6) / 2;
59
+ const fins = s.off + 14, inis = fins + segs * 2 + 2, deltas = inis + segs * 2, ranges = deltas + segs * 2;
60
+ return (cp) => {
61
+ for (let i = 0; i < segs; i++) {
62
+ const fim = buf.readUInt16BE(fins + i * 2);
63
+ if (cp > fim) continue;
64
+ const ini = buf.readUInt16BE(inis + i * 2);
65
+ if (cp < ini) return 0;
66
+ const delta = buf.readInt16BE(deltas + i * 2), ro = buf.readUInt16BE(ranges + i * 2);
67
+ if (!ro) return (cp + delta) & 0xffff;
68
+ const g = buf.readUInt16BE(ranges + i * 2 + ro + (cp - ini) * 2);
69
+ return g ? (g + delta) & 0xffff : 0;
70
+ }
71
+ return 0;
72
+ };
73
+ }
74
+ }
75
+ return () => 0;
76
+ }
77
+
78
+ /** Os contornos de um glifo: [[{x, y, on}]], já com os componentes do composto. */
79
+ function contornos(f, gid, profundidade = 0) {
80
+ const a = f.loca(gid), b = f.loca(gid + 1);
81
+ if (b <= a || profundidade > 4) return [];
82
+ const buf = f.buf, o = f.glyf + a;
83
+ const nc = buf.readInt16BE(o);
84
+ if (nc < 0) {
85
+ // Composto: cada componente é outro glifo, deslocado (e às vezes escalado).
86
+ const saida = [];
87
+ let p = o + 10, flags;
88
+ do {
89
+ flags = buf.readUInt16BE(p); const g = buf.readUInt16BE(p + 2); p += 4;
90
+ let dx, dy;
91
+ if (flags & 1) { dx = buf.readInt16BE(p); dy = buf.readInt16BE(p + 2); p += 4; } else { dx = buf.readInt8(p); dy = buf.readInt8(p + 1); p += 2; }
92
+ let sx = 1, sy = 1;
93
+ if (flags & 8) { sx = sy = buf.readInt16BE(p) / 16384; p += 2; }
94
+ else if (flags & 0x40) { sx = buf.readInt16BE(p) / 16384; sy = buf.readInt16BE(p + 2) / 16384; p += 4; }
95
+ else if (flags & 0x80) { sx = buf.readInt16BE(p) / 16384; sy = buf.readInt16BE(p + 6) / 16384; p += 8; }
96
+ if (!(flags & 2)) { dx = 0; dy = 0; } // pontos casados em vez de deslocamento: raro em fonte do Google
97
+ for (const c of contornos(f, g, profundidade + 1)) saida.push(c.map((q) => ({ x: q.x * sx + dx, y: q.y * sy + dy, on: q.on })));
98
+ } while (flags & 0x20);
99
+ return saida;
100
+ }
101
+ const fins = [];
102
+ for (let i = 0; i < nc; i++) fins.push(buf.readUInt16BE(o + 10 + i * 2));
103
+ const n = nc ? fins[nc - 1] + 1 : 0;
104
+ let p = o + 10 + nc * 2;
105
+ p += 2 + buf.readUInt16BE(p); // pula as instruções
106
+ const fl = [];
107
+ while (fl.length < n) {
108
+ const f0 = buf.readUInt8(p++); fl.push(f0);
109
+ if (f0 & 8) { let r = buf.readUInt8(p++); while (r-- > 0) fl.push(f0); }
110
+ }
111
+ const coord = (curto, igual) => {
112
+ const v = []; let atual = 0;
113
+ for (let i = 0; i < n; i++) {
114
+ const q = fl[i];
115
+ if (q & curto) { const d = buf.readUInt8(p++); atual += q & igual ? d : -d; }
116
+ else if (!(q & igual)) { atual += buf.readInt16BE(p); p += 2; }
117
+ v.push(atual);
118
+ }
119
+ return v;
120
+ };
121
+ const xs = coord(2, 16), ys = coord(4, 32);
122
+ const saida = [];
123
+ let ini = 0;
124
+ for (const fim of fins) {
125
+ const c = [];
126
+ for (let i = ini; i <= fim; i++) c.push({ x: xs[i], y: ys[i], on: Boolean(fl[i] & 1) });
127
+ saida.push(c);
128
+ ini = fim + 1;
129
+ }
130
+ return saida;
131
+ }
132
+
133
+ function abrirFonte(buf) {
134
+ const t = tabelas(buf);
135
+ if (!t.glyf || !t.loca || !t.head || !t.cmap || !t.hmtx || !t.hhea) throw new Error('não é uma fonte TrueType com contornos (glyf)');
136
+ const longo = buf.readInt16BE(t.head.off + 50) === 1;
137
+ const nMetr = buf.readUInt16BE(t.hhea.off + 34);
138
+ return {
139
+ buf, glyf: t.glyf.off,
140
+ porEm: buf.readUInt16BE(t.head.off + 18),
141
+ loca: (i) => (longo ? buf.readUInt32BE(t.loca.off + i * 4) : buf.readUInt16BE(t.loca.off + i * 2) * 2),
142
+ avanco: (g) => buf.readUInt16BE(t.hmtx.off + Math.min(g, nMetr - 1) * 4),
143
+ glifo: mapaDeCaracteres(buf, t.cmap),
144
+ };
145
+ }
146
+
147
+ const r1 = (n) => Math.round(n * 10) / 10;
148
+
149
+ /**
150
+ * A palavra desenhada: {d, pontos: [[x, y, naCurva, contorno]], caixa: [x, y, w, h]}.
151
+ * Coordenadas em unidades da fonte, com y para baixo (como no SVG).
152
+ */
153
+ function desenhar(buf, texto, { espacamento = 0 } = {}) {
154
+ const f = abrirFonte(buf);
155
+ let x = 0, nC = 0;
156
+ const d = [], pontos = [];
157
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
158
+ for (const ch of String(texto)) {
159
+ const g = f.glifo(ch.codePointAt(0));
160
+ for (const c of contornos(f, g)) {
161
+ if (c.length < 2) continue;
162
+ const P = c.map((q) => ({ x: r1(q.x + x), y: r1(-q.y), on: q.on }));
163
+ for (const q of P) {
164
+ pontos.push([q.x, q.y, q.on ? 1 : 0, nC]);
165
+ if (q.x < minX) minX = q.x; if (q.x > maxX) maxX = q.x; if (q.y < minY) minY = q.y; if (q.y > maxY) maxY = q.y;
166
+ }
167
+ // Quadrática com pontos implícitos: entre dois controles seguidos mora uma âncora no meio.
168
+ let k0 = P.findIndex((q) => q.on);
169
+ let inicio = k0 >= 0 ? P[k0] : { x: r1((P[0].x + P[1].x) / 2), y: r1((P[0].y + P[1].y) / 2), on: true };
170
+ if (k0 < 0) k0 = 0;
171
+ const seg = [`M ${inicio.x} ${inicio.y}`];
172
+ let ctrl = null;
173
+ for (let j = 1; j <= P.length; j++) {
174
+ const q = P[(k0 + j) % P.length];
175
+ if (q.on) { seg.push(ctrl ? `Q ${ctrl.x} ${ctrl.y} ${q.x} ${q.y}` : `L ${q.x} ${q.y}`); ctrl = null; }
176
+ else if (ctrl) { const m = { x: r1((ctrl.x + q.x) / 2), y: r1((ctrl.y + q.y) / 2) }; seg.push(`Q ${ctrl.x} ${ctrl.y} ${m.x} ${m.y}`); ctrl = q; }
177
+ else ctrl = q;
178
+ }
179
+ if (ctrl) seg.push(`Q ${ctrl.x} ${ctrl.y} ${inicio.x} ${inicio.y}`);
180
+ d.push(seg.join(' ') + ' Z');
181
+ nC++;
182
+ }
183
+ x += f.avanco(g) + espacamento * f.porEm;
184
+ }
185
+ if (!pontos.length) return null;
186
+ return { d: d.join(' '), pontos, caixa: [r1(minX), r1(minY), r1(maxX - minX), r1(maxY - minY)], porEm: f.porEm };
187
+ }
188
+
189
+ // ── a fonte, do Google Fonts para o cache ────────────────────────────────
190
+ const CACHE = path.join(os.homedir(), '.primocode', 'studio', 'cache-fontes');
191
+
192
+ async function baixarFonte(familia, peso = 700) {
193
+ const nome = String(familia).replace(/[^a-z0-9]+/gi, '-').toLowerCase() + '-' + peso + '.ttf';
194
+ const arq = path.join(CACHE, nome);
195
+ if (fs.existsSync(arq)) return arq;
196
+ // A API antiga (css, não css2) devolve .ttf; a nova devolve woff2, que não tem a glyf crua.
197
+ const css = await (await fetch('https://fonts.googleapis.com/css?family=' + encodeURIComponent(familia) + ':' + peso, { signal: AbortSignal.timeout(10000) })).text();
198
+ const url = (css.match(/url\((https:[^)]+\.ttf)\)/) || [])[1];
199
+ if (!url) return null;
200
+ const bin = Buffer.from(await (await fetch(url, { signal: AbortSignal.timeout(15000) })).arrayBuffer());
201
+ fs.mkdirSync(CACHE, { recursive: true });
202
+ fs.writeFileSync(arq, bin);
203
+ return arq;
204
+ }
205
+
206
+ /** A palavra em vetor na fonte da marca, ou null (sem rede, fonte sem glyf, palavra vazia). */
207
+ async function palavra(texto, { fonte = 'Plus Jakarta Sans', peso = 700 } = {}) {
208
+ if (!String(texto || '').trim()) return null;
209
+ try {
210
+ const arq = await baixarFonte(fonte, peso);
211
+ return arq ? desenhar(fs.readFileSync(arq), texto) : null;
212
+ } catch { return null; }
213
+ }
214
+
215
+ module.exports = { palavra, desenhar, abrirFonte, baixarFonte, CACHE };
@@ -46,6 +46,10 @@ const acervoSite = require('./acervo-site.js');
46
46
  const impacto = require('./impacto.js');
47
47
  const cinema = require('./cinema.js');
48
48
  const motion = require('./motion.js');
49
+ const vox = require('./vox.js');
50
+ const lancamento = require('./lancamento.js');
51
+ const glifos = require('./glifos.js');
52
+ const { escolherEstilo, LEVES } = require('./estilo.js');
49
53
  const sonsLeves = require('./sons-leves.js');
50
54
  const ende = require('./ende.js');
51
55
 
@@ -222,7 +226,8 @@ function roteiroValido(r) {
222
226
  const partes = Array.isArray(x) ? x : x && x.partes;
223
227
  if (!Array.isArray(partes) || partes.length < 2) return null;
224
228
  const CINEMA = ['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'];
225
- if (!partes.every((pt) => pt && (composicao.TIPOS.includes(pt.tipo) || CINEMA.includes(pt.tipo)))) return null;
229
+ const NOVOS = ['logo', 'cores', 'produto']; // vox.js e lancamento.js
230
+ if (!partes.every((pt) => pt && (composicao.TIPOS.includes(pt.tipo) || CINEMA.includes(pt.tipo) || NOVOS.includes(pt.tipo)))) return null;
226
231
  const obj = Array.isArray(x) ? { partes } : x;
227
232
  return partes.some((pt) => CINEMA.includes(pt.tipo)) && !obj.estilo ? { ...obj, estilo: 'cinema' } : obj;
228
233
  }
@@ -245,7 +250,6 @@ async function garantirAcervo(p) {
245
250
  /* O ROTEIRISTA do servidor: quando o modelo não manda um roteiro que preste
246
251
  (nenhum, ou texto corrido), o pedido e os fatos da marca viram o roteiro
247
252
  numa chamada só, com uma tarefa só. */
248
- const PEDE_MOTION = /\bmotion\b|anima[çc][ãa]o|animad[oa]/i;
249
253
 
250
254
  async function roteiroDoServidor(p, args) {
251
255
  const cfg = require('../config').loadConfig();
@@ -260,7 +264,7 @@ async function roteiroDoServidor(p, args) {
260
264
  site: acervo.site ? new URL(acervo.site).hostname.replace(/^www\./, '') : '',
261
265
  rascunho: typeof args.roteiro === 'string' ? args.roteiro : '',
262
266
  // Pedido de motion nunca vira impacto, nem sendo anúncio.
263
- estilo: args.estilo || (!PEDE_MOTION.test(pedido) && /an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i.test(pedido) ? 'impacto' : undefined),
267
+ estilo: args.estilo || (escolherEstilo({ pedido }) === 'impacto' ? 'impacto' : undefined),
264
268
  }, cfg.token);
265
269
  if (!r || !r.ok) return { ok: false, error: (r && r.erro) || 'o roteirista não respondeu.' };
266
270
  return { ok: true, roteiro: { ...r.roteiro, marca: (args.roteiro && args.roteiro.marca) || p.marca || undefined } };
@@ -922,20 +926,15 @@ async function prepararRoteiro(p, args) {
922
926
  if (roteiro.marca.logo && !existe(roteiro.marca.logo)) roteiro.marca.logo = (p.marca && existe(p.marca.logo)) ? p.marca.logo : undefined;
923
927
  } else if (p.marca) roteiro.marca = p.marca;
924
928
  const partes = roteiro.partes || [];
925
- /* O ESTILO. O padrão é MOTION (motion.js): um plano contínuo em 3D, com
926
- um protagonista que se deforma de parte em parte — "isso sim é motion"
927
- (Joel, 28/09). "limpo" era o nome antigo do padrão e vira motion
928
- também. Anúncio, reels e venda vão para o "impacto" (corte a cada
929
- batida), a menos que o pedido diga motion ou animação. A composição
930
- antiga, uma cena por parte, só com estilo: "composicao". */
929
+ /* O ESTILO (estilo.js). O padrão é MOTION (motion.js): um plano contínuo
930
+ em 3D — "isso sim é motion" (Joel, 28/09). Explicação vira VOX
931
+ (editorial, tinta sobre papel), marca vira MARCA (brand reveal),
932
+ lançamento de produto vira LANCAMENTO, anúncio e reels vão para o
933
+ IMPACTO. O que o agente pediu vale mais que tudo. */
931
934
  const pedidoTxt = `${p.pedido || ''} ${args.pedido || ''}`;
932
- // O estilo que o AGENTE pediu vale; o que o roteirista pôs no roteiro não
933
- // passa por cima de um pedido de motion.
934
- const pedido0 = args.estilo || (PEDE_MOTION.test(pedidoTxt) ? null : roteiro.estilo);
935
- const estilo = pedido0 === 'limpo' ? 'motion' : (pedido0
936
- || (PEDE_MOTION.test(pedidoTxt) ? 'motion'
937
- : /an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i.test(pedidoTxt) ? 'impacto' : 'motion'));
938
- const ehMotion = estilo === 'motion';
935
+ const estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo });
936
+ // Os estilos de motion usam os efeitos leves e a trilha do ende.app; os de corte, os cheios.
937
+ const ehMotion = LEVES.has(estilo);
939
938
  const modo = (args.modo || roteiro.modo || (p.formato === 'slides' || p.formato === 'arte' ? 'slides' : 'video'));
940
939
  const [largura, altura] = PROPORCOES[roteiro.proporcao || args.proporcao] || (modo === 'slides' ? PROPORCOES['16:9'] : PROPORCOES['16:9']);
941
940
 
@@ -1180,12 +1179,36 @@ async function prepararRoteiro(p, args) {
1180
1179
  if (r.ok) projeto.anotar(p.id, 'plano', `estilo impacto: ${r.resumo.cenas} batidas`);
1181
1180
  return r;
1182
1181
  }
1183
- if (ehMotion && modo === 'video') {
1182
+ if (estilo === 'vox' && modo === 'video') {
1183
+ const r = vox.montarVox({ roteiro, largura, altura, tempos, narracao, sons, trilha, imagens });
1184
+ if (r.ok) projeto.anotar(p.id, 'plano', `estilo vox: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s`);
1185
+ return r;
1186
+ }
1187
+ if ((estilo === 'marca' || estilo === 'lancamento') && modo === 'video') {
1188
+ /* As letras em vetor (a peça "vetor") pedem os pontos de verdade da
1189
+ fonte: glifos.js lê o .ttf. Sem rede, o vetor desenha o contorno do
1190
+ próprio texto, sem as alças. */
1191
+ const m = roteiro.marca || {};
1192
+ const nome = m.nome || (partes.find((pt) => pt.tipo === 'logo' || pt.tipo === 'abertura') || {}).titulo || '';
1193
+ const precisa = partes.some((pt) => pt.tipo === 'logo' || pt.visual === 'vetor');
1194
+ const g = precisa ? await glifos.palavra((partes.find((pt) => pt.tipo === 'logo') || {}).titulo || nome,
1195
+ { fonte: (m.fontes && m.fontes.titulo) || (estilo === 'marca' ? 'Plus Jakarta Sans' : 'Inter Tight') }) : null;
1196
+ const r = lancamento.montarLancamento({ roteiro, variante: estilo, largura, altura, tempos, narracao, sons, trilha, imagens, glifos: g });
1197
+ if (r.ok) projeto.anotar(p.id, 'plano', `estilo ${estilo}: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s${precisa ? (g ? ', vetor com os pontos da fonte' : ', vetor sem os pontos (sem a fonte)') : ''}`);
1198
+ return r;
1199
+ }
1200
+ if (estilo === 'motion' && modo === 'video') {
1184
1201
  const r = motion.montarMotion({ roteiro, largura, altura, tempos, narracao, sons, trilha, imagens });
1185
1202
  if (r.ok) projeto.anotar(p.id, 'plano', `estilo motion: ${r.resumo.segundos}s contínuos, ${r.resumo.partes} partes, ${r.resumo.fps} fps`);
1186
1203
  return r;
1187
1204
  }
1188
- return composicao.comporRoteiro({ roteiro, modo, largura, altura, tempos, narracao, sons, trilha, imagens, iconesOk, fundos });
1205
+ /* A composição (e os slides) só conhece os tipos antigos: as partes novas
1206
+ viram a mais parecida delas, e o roteiro do vídeo serve à página. */
1207
+ const PARECIDA = { logo: 'abertura', cores: 'lista', produto: 'titulo' };
1208
+ const paraComposicao = { ...roteiro, partes: partes.map((pt) => (PARECIDA[pt.tipo]
1209
+ ? { ...pt, tipo: PARECIDA[pt.tipo], itens: pt.itens || (pt.cores || []).map((c) => (typeof c === 'string' ? c : c.nome)) }
1210
+ : pt)) };
1211
+ return composicao.comporRoteiro({ roteiro: paraComposicao, modo, largura, altura, tempos, narracao, sons, trilha, imagens, iconesOk, fundos });
1189
1212
  }
1190
1213
 
1191
1214
  /* Sem o tempo de cada palavra, cada trecho é repartido pelas palavras dele,