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
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
  };
@@ -38,6 +38,8 @@ const CLIMAS = {
38
38
  viral: ['upbeat', 'beat', 'energetic', 'pop', 'electronic'],
39
39
  impacto: ['epic', 'drums', 'trailer', 'energetic', 'beat'],
40
40
  neutro: ['background', 'corporate', 'chilled', 'calm', 'ambient'],
41
+ // Produto e tecnologia: pulso de sintetizador, limpo, com impulso.
42
+ tecnologia: ['arpeggiator', 'analog synths', 'clockwork', 'rhythm', 'commercial', 'clean mix', 'crisp'],
41
43
  };
42
44
 
43
45
  const limpar = (s) => String(s || '').replace(/<[^>]*>/g, '').replace(/&amp;/g, '&').replace(/&#0?39;/g, "'").replace(/\s+/g, ' ').trim();
@@ -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 };
@@ -45,6 +45,13 @@ const composicao = require('./composicao.js');
45
45
  const acervoSite = require('./acervo-site.js');
46
46
  const impacto = require('./impacto.js');
47
47
  const cinema = require('./cinema.js');
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');
53
+ const sonsLeves = require('./sons-leves.js');
54
+ const ende = require('./ende.js');
48
55
 
49
56
  const PY = path.join(__dirname, '..', '..', 'studio');
50
57
 
@@ -219,7 +226,8 @@ function roteiroValido(r) {
219
226
  const partes = Array.isArray(x) ? x : x && x.partes;
220
227
  if (!Array.isArray(partes) || partes.length < 2) return null;
221
228
  const CINEMA = ['frase', 'montagem', 'numeros', 'escada', 'dia', 'depoimento', 'climax', 'final'];
222
- 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;
223
231
  const obj = Array.isArray(x) ? { partes } : x;
224
232
  return partes.some((pt) => CINEMA.includes(pt.tipo)) && !obj.estilo ? { ...obj, estilo: 'cinema' } : obj;
225
233
  }
@@ -242,6 +250,7 @@ async function garantirAcervo(p) {
242
250
  /* O ROTEIRISTA do servidor: quando o modelo não manda um roteiro que preste
243
251
  (nenhum, ou texto corrido), o pedido e os fatos da marca viram o roteiro
244
252
  numa chamada só, com uma tarefa só. */
253
+
245
254
  async function roteiroDoServidor(p, args) {
246
255
  const cfg = require('../config').loadConfig();
247
256
  const api = require('../api');
@@ -254,7 +263,8 @@ async function roteiroDoServidor(p, args) {
254
263
  fotos: (acervo.fotos || []).length ? `${acervo.fotos.length} fotos reais` : '',
255
264
  site: acervo.site ? new URL(acervo.site).hostname.replace(/^www\./, '') : '',
256
265
  rascunho: typeof args.roteiro === 'string' ? args.roteiro : '',
257
- estilo: args.estilo || (/an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i.test(pedido) ? 'impacto' : undefined),
266
+ // Pedido de motion nunca vira impacto, nem sendo anúncio.
267
+ estilo: args.estilo || (escolherEstilo({ pedido }) === 'impacto' ? 'impacto' : undefined),
258
268
  }, cfg.token);
259
269
  if (!r || !r.ok) return { ok: false, error: (r && r.erro) || 'o roteirista não respondeu.' };
260
270
  return { ok: true, roteiro: { ...r.roteiro, marca: (args.roteiro && args.roteiro.marca) || p.marca || undefined } };
@@ -916,11 +926,15 @@ async function prepararRoteiro(p, args) {
916
926
  if (roteiro.marca.logo && !existe(roteiro.marca.logo)) roteiro.marca.logo = (p.marca && existe(p.marca.logo)) ? p.marca.logo : undefined;
917
927
  } else if (p.marca) roteiro.marca = p.marca;
918
928
  const partes = roteiro.partes || [];
919
- /* O ESTILO: "impacto" (anúncio, corte a cada batida) ou "limpo" (uma cena
920
- por parte). Pedido de anúncio, reels, divulgação ou venda já vem em
921
- impacto — é o que se espera de uma peça dessas em 2026. */
922
- const estilo = args.estilo || roteiro.estilo
923
- || (/an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i.test(`${p.pedido || ''} ${args.pedido || ''}`) ? 'impacto' : 'limpo');
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. */
934
+ const pedidoTxt = `${p.pedido || ''} ${args.pedido || ''}`;
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);
924
938
  const modo = (args.modo || roteiro.modo || (p.formato === 'slides' || p.formato === 'arte' ? 'slides' : 'video'));
925
939
  const [largura, altura] = PROPORCOES[roteiro.proporcao || args.proporcao] || (modo === 'slides' ? PROPORCOES['16:9'] : PROPORCOES['16:9']);
926
940
 
@@ -1131,8 +1145,17 @@ async function prepararRoteiro(p, args) {
1131
1145
  tempos = v.partes;
1132
1146
  narracao = path.relative(p.pasta, mp3).replace(/\\/g, '/');
1133
1147
  projeto.anotar(p.id, 'narracao', `${v.segundos}s em ${partes.length} partes, voz ${v.voz}`);
1134
- sons = await edicao.prepararSons(p.pasta, video);
1135
- try {
1148
+ // O motion usa os efeitos leves; os estilos de corte, os cheios.
1149
+ sons = ehMotion ? sonsLeves.preparar(p.pasta) : await edicao.prepararSons(p.pasta, video);
1150
+ // A trilha do motion: ende.app (CC BY 4.0, sem Content ID), clima tecnologia.
1151
+ if (ehMotion) {
1152
+ try {
1153
+ const segundos = tempos.length ? tempos[tempos.length - 1].ate + 2 : 20;
1154
+ const m = await ende.provedor({ pasta: path.join(p.pasta, 'midia') }).gerar({ clima: roteiro.clima || 'tecnologia', segundos });
1155
+ if (m) { trilha = path.relative(p.pasta, m.src).replace(/\\/g, '/'); projeto.anotar(p.id, 'trilha', m.credito); }
1156
+ } catch { /* cai na busca de sempre */ }
1157
+ }
1158
+ if (!trilha) try {
1136
1159
  const b = await assets.procurarMusica(roteiro.musica || args.trilha || (estilo === 'impacto' ? 'epic energetic' : 'inspiring corporate'), { quantas: 6, duracaoMin: 45 });
1137
1160
  for (const item of (b.ok ? b.sons : [])) {
1138
1161
  const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha' });
@@ -1156,7 +1179,36 @@ async function prepararRoteiro(p, args) {
1156
1179
  if (r.ok) projeto.anotar(p.id, 'plano', `estilo impacto: ${r.resumo.cenas} batidas`);
1157
1180
  return r;
1158
1181
  }
1159
- return composicao.comporRoteiro({ roteiro, modo, largura, altura, tempos, narracao, sons, trilha, imagens, iconesOk, fundos });
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') {
1201
+ const r = motion.montarMotion({ roteiro, largura, altura, tempos, narracao, sons, trilha, imagens });
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`);
1203
+ return r;
1204
+ }
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 });
1160
1212
  }
1161
1213
 
1162
1214
  /* Sem o tempo de cada palavra, cada trecho é repartido pelas palavras dele,
@@ -1435,5 +1487,5 @@ async function ferramentaSite(args) {
1435
1487
  module.exports = {
1436
1488
  ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
1437
1489
  ferramentaVideo, ferramentaRender, ferramentaSite,
1438
- projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes,
1490
+ projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro,
1439
1491
  };