primocode 10.0.0 → 10.1.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 (73) hide show
  1. package/README.md +4 -4
  2. package/bin/primocode.js +88 -3
  3. package/lib/act.js +122 -20
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +6 -3
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/config.js +10 -2
  9. package/lib/entrada.js +73 -9
  10. package/lib/estudio/assets.js +12 -6
  11. package/lib/estudio/biblioteca.js +174 -0
  12. package/lib/estudio/cinema.js +63 -25
  13. package/lib/estudio/composicao.js +2 -1
  14. package/lib/estudio/diretor.js +13 -1
  15. package/lib/estudio/edicao.js +2 -1
  16. package/lib/estudio/ende.js +16 -5
  17. package/lib/estudio/estilo.js +41 -10
  18. package/lib/estudio/fluido.js +283 -0
  19. package/lib/estudio/gerativo.js +1471 -0
  20. package/lib/estudio/hibrido.js +169 -0
  21. package/lib/estudio/impacto.js +66 -31
  22. package/lib/estudio/index.js +366 -47
  23. package/lib/estudio/kit/fluido.jsx +493 -0
  24. package/lib/estudio/kit/gerativo.jsx +990 -0
  25. package/lib/estudio/kit/index.jsx +4 -1
  26. package/lib/estudio/kit/tres.jsx +375 -0
  27. package/lib/estudio/kit.js +9 -1
  28. package/lib/estudio/kokoro.js +111 -0
  29. package/lib/estudio/lancamento.js +27 -0
  30. package/lib/estudio/legibilidade.js +20 -0
  31. package/lib/estudio/referencias.js +105 -0
  32. package/lib/estudio/remotion.js +103 -14
  33. package/lib/estudio/ritmo.js +2 -2
  34. package/lib/estudio/sons-leves.js +38 -1
  35. package/lib/estudio/variacao3d.js +46 -0
  36. package/lib/estudio/variedade.js +183 -0
  37. package/lib/estudio/vox.js +37 -8
  38. package/lib/mcp.js +156 -0
  39. package/lib/personas.js +67 -0
  40. package/lib/portao-skill.js +150 -0
  41. package/lib/skills.js +51 -1
  42. package/lib/tools.js +53 -8
  43. package/lib/ui.js +41 -6
  44. package/lib/video/auto.js +329 -0
  45. package/lib/video/camadas.js +207 -0
  46. package/lib/video/cli.js +1 -1
  47. package/lib/video/filters.js +39 -6
  48. package/lib/video/fx3d/Camadas.jsx +64 -0
  49. package/lib/video/fx3d/Objetos.jsx +128 -0
  50. package/lib/video/fx3d/Root.jsx +18 -0
  51. package/lib/video/fx3d/modelos.jsx +304 -0
  52. package/lib/video/fx3d.js +108 -0
  53. package/lib/video/gerados.js +119 -0
  54. package/lib/video/objetos.js +111 -0
  55. package/lib/video/ops-fx.js +453 -0
  56. package/lib/video/ops.js +30 -5
  57. package/lib/video/paralelo.js +50 -0
  58. package/lib/video/pedido.js +191 -0
  59. package/lib/video/rastrear.js +95 -0
  60. package/lib/video/rastro.js +276 -0
  61. package/lib/video/render.js +67 -14
  62. package/lib/video/texto.js +6 -4
  63. package/lib/video/timeline.js +9 -1
  64. package/package.json +2 -2
  65. package/skills/escrever-em-portugues/SKILL.md +3 -0
  66. package/skills/interface-revisada/SKILL.md +7 -1
  67. package/skills/motion-de-primeira/SKILL.md +5 -4
  68. package/skills/projeto-no-github/SKILL.md +6 -0
  69. package/skills/publicar-na-internet/SKILL.md +3 -0
  70. package/skills/site-com-motion/SKILL.md +2 -0
  71. package/studio/narrar.py +10 -7
  72. package/studio/rastrear.py +399 -0
  73. package/studio/voz_kokoro.py +98 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * fluido.js — o estilo FLUIDO: uma ideia por batida, muito respiro, e nada
3
+ * de corte. O elemento de uma batida VIRA o da próxima.
4
+ *
5
+ * "Uma ideia por quadro, muito espaço vazio. Texto pequeno, caixa normal,
6
+ * sans limpa. Movimento contínuo: um elemento se transforma no próximo."
7
+ * — a referência de motion do Joel (05/10), os vídeos bons.
8
+ *
9
+ * Existe UMA forma que vive o vídeo inteiro (o "portador"): é o ponto da
10
+ * abertura, a pílula do título, o anel do número, a primeira pílula da lista,
11
+ * a linha que risca o "não", o cartão da citação e o primeiro círculo da
12
+ * marca no fim. Entre uma batida e outra ela muda de lugar, de tamanho, de
13
+ * raio e de preenchimento — o olho segue a forma, e o corte some.
14
+ *
15
+ * O texto entra palavra por palavra (stagger curto, ease-out forte, sem
16
+ * linear) e cada frase tem UM acento: uma palavra em serifa itálica na cor de
17
+ * acento. Fundo em gradiente grande e macio, de uma paleta curta (no máximo
18
+ * dois acentos e os neutros), clara ou escura.
19
+ *
20
+ * Proibido aqui, de propósito (é a lista do "cara de IA"): HUD, cantoneira,
21
+ * timecode, caixa na palavra-chave, neon, partícula e CAIXA ALTA.
22
+ *
23
+ * Puro: devolve a partitura (os testes olham) e o plano com UMA peça do kit
24
+ * (kit/fluido.jsx), que desenha a partitura quadro a quadro.
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ const { volumesDaTrilha } = require('./ritmo.js');
30
+
31
+ const FPS = 30;
32
+ const r2 = (n) => Math.round(n * 100) / 100;
33
+ const POR_PALAVRA = 0.3, MINIMO = 0.8; // o tempo de leitura: 0,3 s por palavra, nunca menos de 0,8 s
34
+ const STAGGER = 0.07; // entre uma palavra e a próxima (a que a voz não diz)
35
+ const ANTES = 0.12; // a batida troca isto antes da primeira palavra da frase
36
+
37
+ /* As paletas: um fundo em três tons (o gradiente), a tinta, a tinta suave e
38
+ NO MÁXIMO dois acentos (o da palavra e o brilho difuso). */
39
+ const PALETAS = {
40
+ noite: { escuro: true, fundo: ['#020615', '#061a5c', '#1240c9'], tinta: '#eef2ff', suave: '#9fb0d9', acentos: ['#8fb8ff', '#2f6bff'] },
41
+ breu: { escuro: true, fundo: ['#030304', '#0b0a12', '#2a2156'], tinta: '#f4f4f6', suave: '#8d8a9c', acentos: ['#b4a6ff', '#5b45ff'] },
42
+ lavanda: { escuro: false, fundo: ['#f5f4ff', '#e4e1ff', '#b9b2ff'], tinta: '#17153a', suave: '#6e6a92', acentos: ['#4a3ce0', '#8f86ff'] },
43
+ creme: { escuro: false, fundo: ['#fbf8f2', '#f2e8d8', '#e7c9a5'], tinta: '#1e1a15', suave: '#857a6c', acentos: ['#c2502a', '#f0a36b'] },
44
+ };
45
+ const ESCURAS = ['noite', 'breu'], CLARAS = ['lavanda', 'creme'];
46
+
47
+ const hash = (s) => { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; };
48
+
49
+ /** A paleta: a pedida pelo nome; senão o tema (claro/escuro); senão a semente. */
50
+ function paletaDe(roteiro, semente) {
51
+ const pedida = String(roteiro.paleta || '').toLowerCase();
52
+ const tema = String(roteiro.tema || '').toLowerCase();
53
+ const nome = PALETAS[pedida] ? pedida
54
+ : /claro|light/.test(tema) ? CLARAS[semente % 2]
55
+ : /escuro|dark/.test(tema) ? ESCURAS[semente % 2]
56
+ : Object.keys(PALETAS)[semente % 4];
57
+ const p = { nome, ...PALETAS[nome], acentos: [...PALETAS[nome].acentos] };
58
+ // A cor da marca vira o acento da palavra (e só ela: a paleta não cresce).
59
+ const dest = roteiro.marca && roteiro.marca.cores && roteiro.marca.cores.destaque;
60
+ if (/^#[0-9a-f]{6}$/i.test(dest || '')) p.acentos[0] = dest;
61
+ return p;
62
+ }
63
+
64
+ /* Caixa alta não entra: frase GRITADA vira caixa normal (sigla curta fica). */
65
+ function caixaNormal(t) {
66
+ const s = String(t || '').trim();
67
+ const letras = s.replace(/[^A-Za-zÀ-ÿ]/g, '');
68
+ if (letras.length < 4 || letras !== letras.toUpperCase()) return s;
69
+ const m = s.toLowerCase();
70
+ return m.charAt(0).toUpperCase() + m.slice(1);
71
+ }
72
+
73
+ const VAZIAS = new Set('a o as os um uma de da do das dos e é em no na nos nas que com por para pra se seu sua você ao à mais muito isso este esta não sim'.split(' '));
74
+ const limpa = (w) => w.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]/g, '');
75
+
76
+ /** A frase em palavras, com UM acento: o destaque pedido, ou a palavra de mais peso. */
77
+ function frase(texto, { destaque = '', semAcento = false } = {}) {
78
+ const ws = caixaNormal(texto).split(/\s+/).filter(Boolean);
79
+ let ac = -1;
80
+ if (!semAcento && ws.length > 1) {
81
+ const d = limpa(String(destaque).split(/\s+/)[0] || '');
82
+ if (d) ac = ws.findIndex((w) => limpa(w) === d);
83
+ if (ac < 0) {
84
+ // A palavra mais longa que não é vazia; no empate, a mais tarde (a frase termina nela).
85
+ let melhor = 0;
86
+ ws.forEach((w, i) => { const n = limpa(w).length; if (!VAZIAS.has(limpa(w)) && n >= 4 && n >= melhor) { melhor = n; ac = i; } });
87
+ }
88
+ }
89
+ return ws.map((t, i) => ({ t, acento: i === ac }));
90
+ }
91
+
92
+ const numeroDe = (v) => {
93
+ const m = String(v == null ? '' : v).match(/^(\D*)([\d.,]+)(.*)$/);
94
+ if (!m) return null;
95
+ const n = Number(m[2].replace(/\./g, '').replace(',', '.'));
96
+ return Number.isFinite(n) ? { antes: m[1], ate: n, depois: m[3], casas: /,\d/.test(m[2]) ? 1 : 0 } : null;
97
+ };
98
+ const itensDe = (p) => (p.itens || []).map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
99
+ const leitura = (n) => Math.max(MINIMO, POR_PALAVRA * n);
100
+
101
+ /* A forma do portador em cada tipo de parte (kit/fluido.jsx dá o tamanho). */
102
+ const FORMA = { abertura: 'ponto', titulo: 'pilula', numero: 'anel', lista: 'pilula', passos: 'ponto', contraste: 'linha', citacao: 'cartao', cta: 'marca' };
103
+ const TIPO = { logo: 'abertura', imagem: 'titulo', produto: 'titulo', grafico: 'lista', cores: 'lista' };
104
+
105
+ /**
106
+ * A partitura: as batidas, cada uma com as frases (e quando cada uma entra),
107
+ * a forma do portador e a transição. Sem áudio nem plano — é o que os testes olham.
108
+ */
109
+ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
110
+ const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object');
111
+ const semente = Number.isFinite(roteiro.semente) ? roteiro.semente : hash(JSON.stringify(partes.map((p) => [p.tipo, p.titulo, p.valor])));
112
+ const paleta = paletaDe(roteiro, semente);
113
+ const marca = roteiro.marca || {};
114
+ const temVoz = tempos.length === partes.length && tempos.every((t) => t && t.ate > t.de);
115
+ /* O comando do terminal: o link da cta, se for comando; senão o nome da marca. */
116
+ const link = (partes.find((p) => p.tipo === 'cta') || {}).link || '';
117
+ const cmd = /^(npm|npx|pnpm|yarn|pip|brew|curl|git|bun)\b/.test(link) ? link : String(marca.nome || 'app').toLowerCase().replace(/\s+/g, '-');
118
+ let comCartao = 0;
119
+
120
+ const batidas = partes.map((p, i) => {
121
+ const tipo = TIPO[p.tipo] || (FORMA[p.tipo] ? p.tipo : 'titulo');
122
+ const titulo = p.titulo || p.texto || '';
123
+ const fr = [];
124
+ const poe = (papel, texto, o = {}) => { if (String(texto || '').trim()) fr.push({ id: `${i}-${fr.length}`, papel, palavras: frase(texto, o) }); };
125
+ const b = { i, tipo, forma: FORMA[tipo], transicao: p.corte === true && i > 0 ? 'corte' : 'morfa', frases: fr, lado: 'centro' };
126
+ /* O CARTÃO DE PRODUTO que flutua ao lado (terminal, chat, painel), feito
127
+ de forma e texto — e o texto vai para o outro lado. `ui: false` tira. */
128
+ const ui = p.ui === false ? null : typeof p.ui === 'string' ? p.ui
129
+ : tipo === 'titulo' || tipo === 'abertura' ? ['terminal', 'chat'][comCartao % 2] : tipo === 'numero' ? 'painel' : null;
130
+ if (ui) {
131
+ b.lado = comCartao++ % 2 ? 'dir' : 'esq';
132
+ b.ui = ui === 'terminal' ? { tipo: ui, linhas: ['$ ' + cmd, '✓ ' + caixaNormal(p.sub || titulo)] }
133
+ : ui === 'chat' ? { tipo: ui, pergunta: caixaNormal(titulo) }
134
+ : { tipo: 'painel', rotulo: caixaNormal(p.rotulo || titulo), barras: [0, 1, 2, 3, 4, 5, 6].map((k) => r2(0.35 + ((semente >>> (k * 3)) % 7) / 10)) };
135
+ }
136
+ switch (tipo) {
137
+ case 'numero': {
138
+ b.numero = numeroDe(p.valor);
139
+ poe('numero', p.valor, { semAcento: true });
140
+ poe('rotulo', p.rotulo || titulo, { destaque: p.destaque });
141
+ break;
142
+ }
143
+ case 'lista': case 'passos':
144
+ poe('principal', titulo, { destaque: p.destaque });
145
+ itensDe(p).slice(0, tipo === 'passos' ? 6 : 4).forEach((it) => poe('item', it, { semAcento: true }));
146
+ break;
147
+ case 'contraste':
148
+ poe('nao', p.nao, { semAcento: true });
149
+ poe('sim', p.sim || titulo, { destaque: p.destaque });
150
+ break;
151
+ case 'citacao':
152
+ poe('principal', titulo, { destaque: p.destaque });
153
+ poe('autor', p.autor ? `— ${p.autor}` : '', { semAcento: true });
154
+ break;
155
+ case 'cta':
156
+ b.marca = { nome: caixaNormal(marca.nome || titulo) };
157
+ poe('marca', b.marca.nome, { semAcento: true });
158
+ poe('principal', marca.nome ? titulo : '', { destaque: p.destaque });
159
+ poe('sub', p.link || p.sub, { semAcento: true });
160
+ break;
161
+ default:
162
+ poe('principal', titulo, { destaque: p.destaque });
163
+ poe('sub', p.sub, { semAcento: true });
164
+ }
165
+ return b;
166
+ });
167
+
168
+ /* ── o tempo: a VOZ manda. A batida troca na fronteira da frase (um
169
+ pouco antes da primeira palavra da parte) e cada palavra na tela
170
+ aparece no instante em que é dita (os tempos por palavra da narração).
171
+ A palavra que a voz não diz entra em cascata curta. Se o texto pede
172
+ mais leitura do que a frase deu, a batida estica e a próxima volta a
173
+ casar com a voz na primeira folga; o atraso medido vai na partitura. */
174
+ let atraso = 0, sincronia = 0;
175
+ batidas.forEach((b, i) => {
176
+ const tp = temVoz ? tempos[i] : null;
177
+ const deVoz = tp ? (i === 0 ? 0 : Math.max(0, tp.de - ANTES)) : null;
178
+ b.de = r2(i === 0 ? 0 : Math.max(batidas[i - 1].ate, deVoz == null ? 0 : deVoz));
179
+ if (deVoz != null) atraso = Math.max(atraso, r2(b.de - deVoz));
180
+ const ditas = ((tp && tp.palavras) || []).map((w) => ({ k: limpa(w.texto), de: w.de }));
181
+ let cursor = 0, livre = b.de + (b.tipo === 'cta' ? 0.55 : 0.12);
182
+ const fimVoz = tp ? (i < batidas.length - 1 ? tempos[i + 1].de - ANTES : tp.ate + 0.2) : 0;
183
+ for (const f of b.frases) {
184
+ /* Casa as palavras da tela com as faladas, na ordem. Tenta começar de
185
+ cada palavra (a frase da tela pode vir em outra ordem que a falada)
186
+ e fica com o casamento que acha mais palavras. */
187
+ const casa = (k, d) => d === k || (k.length >= 4 && d.length >= 4 && d.slice(0, 5) === k.slice(0, 5));
188
+ let ts = f.palavras.map(() => null), fim = cursor;
189
+ for (let ini = 0; ini < f.palavras.length; ini++) {
190
+ let c = cursor;
191
+ const tt = f.palavras.map((w, j) => {
192
+ const k = limpa(w.t);
193
+ if (j < ini || !k) return null;
194
+ for (let q = c; q < Math.min(ditas.length, c + 12); q++) if (casa(k, ditas[q].k)) { c = q + 1; return ditas[q].de; }
195
+ return null;
196
+ });
197
+ if (tt.filter((x) => x != null).length > ts.filter((x) => x != null).length) { ts = tt; fim = c; }
198
+ }
199
+ cursor = fim;
200
+ f.falada = ts.filter((x) => x != null).length >= f.palavras.length / 2;
201
+ const ach = ts.findIndex((x) => x != null);
202
+ /* O que a voz não diz entra cedo o bastante para ser lido antes de a
203
+ frase acabar (e nunca antes da batida). */
204
+ let t = ach >= 0 ? ts[ach] - ach * STAGGER
205
+ : tp ? Math.max(b.de + 0.25, Math.min(livre, fimVoz - leitura(f.palavras.length) - 0.05)) : livre;
206
+ f.palavras.forEach((w, j) => {
207
+ const falada = ts[j];
208
+ t = Math.max(b.de + 0.06, falada != null ? Math.max(t, falada - 0.05) : t);
209
+ w.em = r2(t);
210
+ if (falada != null) sincronia = Math.max(sincronia, r2(w.em - falada));
211
+ t += STAGGER;
212
+ });
213
+ f.de = f.palavras[0].em;
214
+ // O que não é dito (rótulo, item, link) entra logo depois do anterior.
215
+ livre = Math.max(livre, f.papel === 'item' && b.tipo === 'passos' ? f.de + leitura(f.palavras.length) * 0.8 : f.palavras[f.palavras.length - 1].em + 0.18);
216
+ }
217
+ /* O texto que a voz DIZ é lido junto com ela: fica até depois da última
218
+ palavra dita (e nunca menos de 0,8 s). O que a voz não diz pede 0,3 s por palavra. */
219
+ const precisa = Math.max(...b.frases.map((f) => f.de + (f.falada
220
+ ? Math.max(MINIMO, f.palavras[f.palavras.length - 1].em - f.de + 0.3) : leitura(f.palavras.length))), b.de + 0.9);
221
+ b.ate = r2(Math.max(precisa, fimVoz));
222
+ });
223
+ // A marca do fim segura um instante; o resto não espera.
224
+ const ult = batidas[batidas.length - 1];
225
+ ult.ate = r2(ult.ate + (ult.tipo === 'cta' ? 1.0 : 0.5));
226
+
227
+ return {
228
+ versao: 2, semente, fps: FPS, atraso, sincronia, largura, altura, duracao: ult.ate, paleta,
229
+ fontes: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono' },
230
+ logo: marca.logo || null,
231
+ batidas,
232
+ };
233
+ }
234
+
235
+ /**
236
+ * @param {object} op
237
+ * @param {object} op.roteiro { marca, paleta?: noite|breu|lavanda|creme, tema?: claro|escuro, partes }
238
+ * @param {Array} op.tempos [{de, ate, palavras}] por parte (da narração)
239
+ * @param {object} op.sons os efeitos leves (sons-leves.js)
240
+ */
241
+ function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null }) {
242
+ const partes = ((roteiro && roteiro.partes) || []).filter((p) => p && typeof p === 'object');
243
+ if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
244
+ const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos });
245
+ const DUR = partitura.duracao;
246
+ /* O SOM segue o movimento (sons-leves.js, tudo sintetizado): whoosh em
247
+ cada transformação, pop quando pílula ou cartão assenta, clique na roda e
248
+ no terminal, um bolha discreto na palavra de acento, o riser que leva à
249
+ marca e o impacto quando ela trava. Baixo: a voz vem primeiro. */
250
+ const efeitos = [];
251
+ const som = (t, nome, volume) => { if (sons[nome]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome], volume }); };
252
+ for (const b of partitura.batidas) {
253
+ if (b.i > 0 && b.tipo !== 'cta') som(b.de - 0.18, 'whoosh', 0.2);
254
+ if (b.ui) { som(b.de + 0.6, 'pop', 0.16); if (b.ui.tipo === 'terminal') b.ui.linhas.forEach((_, k) => som(b.de + 0.75 + k * 0.5, 'clique', 0.1)); }
255
+ for (const f of b.frases) {
256
+ if (f.papel === 'item') som(f.de - 0.06, b.tipo === 'passos' ? 'clique' : 'pop', 0.15);
257
+ const ac = f.palavras.find((w) => w.acento);
258
+ if (ac && f.papel !== 'item') som(ac.em, 'bolha', 0.09);
259
+ }
260
+ if (b.tipo === 'citacao') som(b.de + 0.3, 'pop', 0.14);
261
+ if (b.tipo === 'cta') { som(b.de - 0.75, 'riser', 0.2); som(b.de + 0.45, 'impacto', 0.26); som(b.de + 0.5, 'brilho', 0.16); }
262
+ }
263
+ const volumes = volumesDaTrilha(tempos.length === partes.length ? tempos : [], DUR, { alto: 0.55, baixo: 0.22 });
264
+ const P = partitura.paleta;
265
+ const plano = {
266
+ formato: { largura, altura, fps: FPS },
267
+ fundoGeral: P.fundo[0],
268
+ fontesVariaveis: { Inter: 'wght@300..700', 'Instrument Serif': 'ital@0;1', 'JetBrains Mono': 'wght@400;500' },
269
+ cenas: [{
270
+ duracao: DUR,
271
+ fundo: P.fundo[0],
272
+ elementos: [{ tipo: 'kit', peca: 'fluido', partitura, fonte: partitura.fontes.texto, fonteSerifa: partitura.fontes.serifa, fonteMono: partitura.fontes.mono }],
273
+ }],
274
+ audio: {
275
+ ...(narracao ? { narracao } : {}),
276
+ ...(trilha ? { trilha: { src: trilha, volumes, loop: true } } : {}),
277
+ efeitos,
278
+ },
279
+ };
280
+ return { ok: true, plano, partitura, resumo: { cenas: 1, batidas: partitura.batidas.length, segundos: DUR, fps: FPS, paleta: P.nome, atraso: partitura.atraso, sincronia: partitura.sincronia } };
281
+ }
282
+
283
+ module.exports = { montarFluido, partituraDe, caixaNormal, PALETAS, POR_PALAVRA, MINIMO };