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
@@ -6,6 +6,9 @@
6
6
  import { VOX } from './vox';
7
7
  import { MARCA } from './marca';
8
8
  import { Papel } from './base';
9
+ import { TRES } from './tres';
10
+ import { GERATIVO } from './gerativo';
11
+ import { FLUIDO } from './fluido';
9
12
 
10
- export const KIT = { ...VOX, ...MARCA, papel: Papel };
13
+ export const KIT = { ...VOX, ...MARCA, ...TRES, ...GERATIVO, ...FLUIDO, papel: Papel };
11
14
  export { Filtros, Acabamento, Hud, CURVAS_KIT, bolhaCaminho } from './base';
@@ -0,0 +1,375 @@
1
+ /* kit/tres.jsx — o PRODUTO EM 3D, feito de geometria, sem arquivo de fora.
2
+ *
3
+ * "Crie um vídeo para anunciar um headset — o vídeo deve ter um 3D do fone"
4
+ * (30/09), com "sem referências": nada de foto, então o produto é modelado
5
+ * aqui, com as peças do three.js, e iluminado como em estúdio.
6
+ *
7
+ * {"tipo":"kit","peca":"produto3d","modelo":"fone"|"celular"|"lata"|"caixa"|"notebook"|"garrafa"|"relogio",
8
+ * "cor":"#1b1c20","corAcento":"#ff5a1f","t":0,"giro":0.35,
9
+ * "acabamento":"metal","camera":"alto","luz":"recorte","ritmo":"pendulo", (variacao3d.js)
10
+ * "x":0,"y":0,"largura":1920,"altura":1080}
11
+ *
12
+ * O movimento: entra pequeno e girado e assenta com mola (0,9 s), depois gira
13
+ * devagar e flutua — produto de vitrine, nunca pião. A luz de recorte vem na
14
+ * cor de acento: é ela que desenha a silhueta no fundo escuro. */
15
+ import React, { useMemo } from 'react';
16
+ import { ThreeCanvas } from '@remotion/three';
17
+ import * as THREE from 'three';
18
+ import { useThree } from '@react-three/fiber';
19
+ import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
20
+
21
+ /* A LUZ DE ESTÚDIO: sem um ambiente, material brilhante ou metálico não tem o
22
+ que refletir e sai preto ("a engrenagem parecia uma mancha", 30/09). A sala
23
+ do three (RoomEnvironment, local, sem rede) vira o reflexo de tudo. */
24
+ function Ambiente({ forca = 1 }) {
25
+ const { gl, scene } = useThree();
26
+ useMemo(() => {
27
+ const pm = new THREE.PMREMGenerator(gl);
28
+ scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
29
+ scene.environmentIntensity = forca;
30
+ pm.dispose();
31
+ }, [gl, scene]);
32
+ return null;
33
+ }
34
+
35
+ const saida = (x) => 1 - Math.pow(1 - Math.max(0, Math.min(1, x)), 3);
36
+
37
+ function Material({ cor, brilho = 0.35, metal = 0.25, verniz = 0.8, iris = 0, emissivo }) {
38
+ return <meshPhysicalMaterial color={cor} roughness={brilho} metalness={metal} clearcoat={verniz} clearcoatRoughness={0.25} iridescence={iris}
39
+ {...(emissivo ? { emissive: emissivo, emissiveIntensity: 1.2 } : {})} />;
40
+ }
41
+
42
+ // O fone: o arco, os deslizadores, as conchas com o anel de acento, as almofadas e o logo aceso.
43
+ /* AS VARIAÇÕES (variacao3d.js sorteia os nomes): o acabamento do corpo, o
44
+ enquadramento, a luz e o ritmo. Sem nome, o de sempre. */
45
+ const ACABAMENTOS = {
46
+ verniz: { brilho: 0.3, metal: 0.3, verniz: 0.8 }, metal: { brilho: 0.18, metal: 0.92, verniz: 0.3 },
47
+ fosco: { brilho: 0.78, metal: 0, verniz: 0 }, perola: { brilho: 0.25, metal: 0.1, verniz: 1, iris: 1 },
48
+ acetinado: { brilho: 0.5, metal: 0.45, verniz: 0.4 },
49
+ };
50
+ const CAMERAS = {
51
+ frente: {}, alto: { y: 1.5, inclina: 0.12 }, baixo: { y: -0.6, inclina: -0.1 },
52
+ perfil: { giro0: -1.7, roll: 0.07 }, perto: { fov: 25 },
53
+ };
54
+ // [chave, preenchimento, recorte na cor de acento, ambiente]
55
+ const LUZES = { estudio: [3.2, 1.1, 3.2, 0.35], recorte: [1.6, 0.4, 5.5, 0.15], suave: [2.2, 2.0, 1.4, 0.7], lateral: [4.2, 0.2, 2.6, 0.2] };
56
+ const RITMOS = { lento: 0.18, medio: 0.35, rapido: 0.6 };
57
+
58
+ function Fone({ cor, acento, fin }) {
59
+ const R = 1.05;
60
+ const concha = (lado) => (
61
+ <group position={[lado * R, -0.12, 0]}>
62
+ <mesh rotation={[0, 0, Math.PI / 2]}>
63
+ <cylinderGeometry args={[0.44, 0.46, 0.26, 64]} />
64
+ <Material cor={cor} brilho={0.3} metal={0.35} {...fin} />
65
+ </mesh>
66
+ <mesh position={[lado * 0.135, 0, 0]} rotation={[0, Math.PI / 2, 0]}>
67
+ <torusGeometry args={[0.43, 0.035, 24, 96]} />
68
+ <Material cor={acento} brilho={0.18} metal={0.9} verniz={1} />
69
+ </mesh>
70
+ <mesh position={[lado * 0.14, 0, 0]} rotation={[0, Math.PI / 2, 0]}>
71
+ <circleGeometry args={[0.16, 48]} />
72
+ <Material cor={acento} brilho={0.2} metal={0.2} emissivo={acento} />
73
+ </mesh>
74
+ <mesh position={[-lado * 0.16, 0, 0]} rotation={[0, Math.PI / 2, 0]}>
75
+ <torusGeometry args={[0.3, 0.11, 32, 96]} />
76
+ <Material cor="#141416" brilho={0.85} metal={0} verniz={0} />
77
+ </mesh>
78
+ </group>
79
+ );
80
+ return (
81
+ <group>
82
+ <mesh>
83
+ <torusGeometry args={[R, 0.075, 32, 160, Math.PI]} />
84
+ <Material cor={cor} brilho={0.28} metal={0.4} {...fin} />
85
+ </mesh>
86
+ <mesh position={[0, -0.06, 0]} scale={[1, 1, 1.6]}>
87
+ <torusGeometry args={[R - 0.07, 0.05, 24, 140, Math.PI]} />
88
+ <Material cor="#141416" brilho={0.9} metal={0} verniz={0} />
89
+ </mesh>
90
+ {[-1, 1].map((lado) => (
91
+ <mesh key={lado} position={[lado * R, 0.1, 0]}>
92
+ <boxGeometry args={[0.07, 0.34, 0.12]} />
93
+ <Material cor={acento} brilho={0.15} metal={0.95} verniz={1} />
94
+ </mesh>
95
+ ))}
96
+ {concha(-1)}
97
+ {concha(1)}
98
+ </group>
99
+ );
100
+ }
101
+
102
+ function Celular({ cor, acento, fin }) {
103
+ return (
104
+ <group rotation={[0, 0, 0.05]}>
105
+ <mesh><boxGeometry args={[1.0, 2.05, 0.1]} /><Material cor={cor} brilho={0.22} metal={0.7} {...fin} /></mesh>
106
+ <mesh position={[0, 0, 0.052]}><planeGeometry args={[0.92, 1.96]} /><Material cor="#0b0c10" brilho={0.1} metal={0} emissivo="#0d1830" /></mesh>
107
+ <mesh position={[0, 0.3, 0.054]}><circleGeometry args={[0.22, 48]} /><Material cor={acento} emissivo={acento} /></mesh>
108
+ <mesh position={[0.26, 0.78, -0.07]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.14, 0.14, 0.05, 48]} /><Material cor="#222" brilho={0.1} metal={0.9} /></mesh>
109
+ </group>
110
+ );
111
+ }
112
+
113
+ function Lata({ cor, acento, fin }) {
114
+ return (
115
+ <group>
116
+ <mesh><cylinderGeometry args={[0.55, 0.55, 1.9, 96]} /><Material cor={cor} brilho={0.2} metal={0.85} {...fin} /></mesh>
117
+ <mesh position={[0, 0.2, 0]}><cylinderGeometry args={[0.556, 0.556, 0.5, 96, 1, true]} /><Material cor={acento} brilho={0.3} metal={0.3} /></mesh>
118
+ <mesh position={[0, 0.97, 0]}><cylinderGeometry args={[0.5, 0.55, 0.06, 96]} /><Material cor="#c9ccd2" brilho={0.25} metal={1} /></mesh>
119
+ </group>
120
+ );
121
+ }
122
+
123
+ function Caixa({ cor, acento, fin }) {
124
+ return (
125
+ <group>
126
+ <mesh><boxGeometry args={[1.5, 1.5, 1.5]} /><Material cor={cor} brilho={0.3} metal={0.3} {...fin} /></mesh>
127
+ <mesh position={[0, 0, 0.752]}><planeGeometry args={[1.0, 0.18]} /><Material cor={acento} emissivo={acento} /></mesh>
128
+ </group>
129
+ );
130
+ }
131
+
132
+ // O notebook: a base, o teclado escuro, a tampa aberta a ~105° e a tela acesa.
133
+ function Notebook({ cor, acento, fin }) {
134
+ return (
135
+ <group position={[0, -0.3, 0.1]} rotation={[0.1, 0, 0]} scale={[0.7, 0.7, 0.7]}>{/* a 1,0 a tela saía cortada no topo (05/10) */}
136
+ <mesh><boxGeometry args={[2.2, 0.08, 1.45]} /><Material cor={cor} brilho={0.25} metal={0.75} {...fin} /></mesh>
137
+ <mesh position={[0, 0.042, -0.12]}><boxGeometry args={[1.9, 0.01, 0.75]} /><Material cor="#141416" brilho={0.8} metal={0} verniz={0} /></mesh>
138
+ <mesh position={[0, 0.042, 0.45]}><boxGeometry args={[0.7, 0.01, 0.38]} /><Material cor="#26272c" brilho={0.4} metal={0.2} /></mesh>
139
+ <group position={[0, 0.04, -0.72]} rotation={[-0.26, 0, 0]}>
140
+ <mesh position={[0, 0.72, 0]}><boxGeometry args={[2.2, 1.44, 0.06]} /><Material cor={cor} brilho={0.25} metal={0.75} {...fin} /></mesh>
141
+ <mesh position={[0, 0.74, 0.032]}><planeGeometry args={[2.0, 1.25]} /><Material cor="#0b0c10" brilho={0.1} metal={0} emissivo="#0d1830" /></mesh>
142
+ <mesh position={[0, 0.74, 0.034]}><circleGeometry args={[0.2, 48]} /><Material cor={acento} emissivo={acento} /></mesh>
143
+ </group>
144
+ </group>
145
+ );
146
+ }
147
+
148
+ // A garrafa: o corpo, o ombro, o gargalo, a tampa e o rótulo na cor de acento.
149
+ function Garrafa({ cor, acento, fin }) {
150
+ return (
151
+ <group position={[0, -0.15, 0]}>
152
+ <mesh><cylinderGeometry args={[0.5, 0.5, 1.5, 96]} /><Material cor={cor} brilho={0.15} metal={0.6} {...fin} /></mesh>
153
+ <mesh position={[0, 0.95, 0]}><cylinderGeometry args={[0.2, 0.5, 0.4, 96]} /><Material cor={cor} brilho={0.15} metal={0.6} {...fin} /></mesh>
154
+ <mesh position={[0, 1.3, 0]}><cylinderGeometry args={[0.2, 0.2, 0.3, 64]} /><Material cor={cor} brilho={0.15} metal={0.6} {...fin} /></mesh>
155
+ <mesh position={[0, 1.52, 0]}><cylinderGeometry args={[0.23, 0.23, 0.18, 64]} /><Material cor={acento} brilho={0.3} metal={0.8} /></mesh>
156
+ <mesh position={[0, 0.05, 0]}><cylinderGeometry args={[0.506, 0.506, 0.55, 96, 1, true]} /><Material cor={acento} brilho={0.5} metal={0.1} verniz={0.3} /></mesh>
157
+ </group>
158
+ );
159
+ }
160
+
161
+ // O relógio: a caixa redonda, o aro de acento, o mostrador aceso e a pulseira.
162
+ function Relogio({ cor, acento, fin }) {
163
+ return (
164
+ <group>
165
+ <mesh rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.62, 0.62, 0.22, 96]} /><Material cor={cor} brilho={0.2} metal={0.85} {...fin} /></mesh>
166
+ <mesh position={[0, 0, 0.11]}><torusGeometry args={[0.6, 0.045, 24, 96]} /><Material cor={acento} brilho={0.15} metal={0.95} verniz={1} /></mesh>
167
+ <mesh position={[0, 0, 0.112]}><circleGeometry args={[0.55, 64]} /><Material cor="#0b0c10" brilho={0.1} metal={0} emissivo="#0d1830" /></mesh>
168
+ <mesh position={[0, 0.16, 0.116]}><boxGeometry args={[0.04, 0.32, 0.01]} /><Material cor={acento} emissivo={acento} /></mesh>
169
+ <mesh position={[0.1, 0, 0.118]} rotation={[0, 0, -Math.PI / 2]}><boxGeometry args={[0.05, 0.22, 0.01]} /><Material cor="#f4f4f6" emissivo="#9aa0aa" /></mesh>
170
+ <mesh position={[0.66, 0, 0]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.06, 0.06, 0.14, 24]} /><Material cor={cor} brilho={0.2} metal={0.85} {...fin} /></mesh>
171
+ {[-1, 1].map((l) => <mesh key={l} position={[0, l * 1.0, -0.02]}><boxGeometry args={[0.6, 0.85, 0.08]} /><Material cor="#1a1a1d" brilho={0.7} metal={0} verniz={0.2} /></mesh>)}
172
+ </group>
173
+ );
174
+ }
175
+
176
+ const MODELOS = { fone: Fone, headset: Fone, celular: Celular, phone: Celular, lata: Lata, garrafa: Garrafa, caixa: Caixa,
177
+ notebook: Notebook, laptop: Notebook, relogio: Relogio, watch: Relogio };
178
+ export const MODELOS_3D = Object.keys(MODELOS);
179
+
180
+ export function Produto3D({ el, t }) {
181
+ const W = el.largura || 1920, H = el.altura || 1080;
182
+ const t0 = el.t || 0;
183
+ const tl = Math.max(0, t - t0);
184
+ const k = saida(tl / 0.9);
185
+ const Modelo = MODELOS[el.modelo] || Caixa;
186
+ const cor = el.cor || '#1b1c20';
187
+ const acento = el.corAcento || '#ff5a1f';
188
+ const cam = CAMERAS[el.camera] || {};
189
+ const luz = LUZES[el.luz] || LUZES.estudio;
190
+ const fin = ACABAMENTOS[el.acabamento];
191
+ // O ritmo: o giro contínuo em três velocidades, ou o pêndulo, que vai e volta mostrando a frente.
192
+ const anda = el.ritmo === 'pendulo' ? Math.sin(tl * 0.9) * 0.7 : tl * (RITMOS[el.ritmo] ?? el.giro ?? 0.35);
193
+ const giro = (el.giro0 ?? cam.giro0 ?? -0.9) + (1 - k) * 1.4 + anda;
194
+ const escala = (el.escala || 1) * (0.72 + 0.28 * k);
195
+ const flutua = Math.sin(tl * 1.3) * 0.05;
196
+ return (
197
+ <div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, opacity: t >= t0 ? Math.min(1, tl / 0.25) : 0 }}>
198
+ <ThreeCanvas width={W} height={H} camera={{ position: [0, cam.y ?? 0.35, 4.4], fov: el.fov || cam.fov || 32 }} gl={{ antialias: true, preserveDrawingBuffer: true }}>
199
+ <Ambiente forca={0.8} />
200
+ <hemisphereLight args={['#ffffff', '#202020', 0.5]} />
201
+ <ambientLight intensity={luz[3]} />
202
+ <directionalLight position={el.luz === 'lateral' ? [5, 1.5, 1.5] : [3, 4, 5]} intensity={luz[0]} />
203
+ <directionalLight position={[-2, 3, 4]} intensity={luz[1]} />
204
+ <directionalLight position={[-4, 1.5, -3]} intensity={luz[2]} color={acento} />
205
+ <pointLight position={[0, -2, 3]} intensity={0.8} />
206
+ <group position={[0, flutua - 0.05, 0]} rotation={[0.18 + (cam.inclina || 0) + (1 - k) * 0.3, giro, cam.roll || 0]} scale={[escala, escala, escala]}>
207
+ <Modelo cor={cor} acento={acento} fin={fin} />
208
+ </group>
209
+ {/* O chão. O gerativo dispensa (semChao): o disco virava uma faixa escura atrás do texto. */}
210
+ {!el.semChao && (
211
+ <mesh position={[0, -1.25, 0]} rotation={[-Math.PI / 2, 0, 0]}>
212
+ <circleGeometry args={[2.4, 64]} />
213
+ <meshStandardMaterial color="#0a0a0c" roughness={0.35} metalness={0.6} transparent opacity={0.85} />
214
+ </mesh>
215
+ )}
216
+ </ThreeCanvas>
217
+ </div>
218
+ );
219
+ }
220
+
221
+ /* ── OS ÍCONES 3D ─────────────────────────────────────────────────────────
222
+ * "A gente não tem ícone 3D" (30/09). Nada de emoji-3D de banco: cada ícone
223
+ * é geometria daqui — forma 2D extrudada com chanfro (raio, coração,
224
+ * estrela, escudo, balão, play, check, seta, engrenagem) ou primitivas
225
+ * compostas (bateria, cadeado, relógio, barras, fone, celular, sol, onda) —
226
+ * no material e na paleta do vídeo (argila, verniz ou metal), com a luz de
227
+ * recorte na cor de acento.
228
+ *
229
+ * {"tipo":"kit","peca":"icone3d","icone":"raio","x":0,"y":0,"tamanho":360,
230
+ * "material":"argila","cor":"#efe8da","cor2":"#7a1f1f","luz":"#7a1f1f","t":0} */
231
+ function forma(pts, curvas) {
232
+ const sh = new THREE.Shape();
233
+ pts.forEach(([x, y], i) => (i ? sh.lineTo(x, y) : sh.moveTo(x, y)));
234
+ if (curvas) curvas(sh);
235
+ sh.closePath();
236
+ return sh;
237
+ }
238
+ const FORMAS = {
239
+ raio: () => forma([[0.15, 1], [-0.55, -0.1], [-0.05, -0.1], [-0.25, -1], [0.55, 0.15], [0.05, 0.15], [0.35, 1]]),
240
+ estrela: () => forma(Array.from({ length: 10 }, (_, i) => { const a = Math.PI / 2 + (i * Math.PI) / 5, r = i % 2 ? 0.42 : 1; return [Math.cos(a) * r, Math.sin(a) * r]; })),
241
+ play: () => forma([[-0.55, 0.85], [0.85, 0], [-0.55, -0.85]]),
242
+ check: () => forma([[-0.95, 0.05], [-0.6, 0.4], [-0.25, 0.05], [0.6, 0.9], [0.95, 0.55], [-0.25, -0.65]]),
243
+ seta: () => forma([[-0.9, 0.22], [0.2, 0.22], [0.2, 0.62], [0.95, 0], [0.2, -0.62], [0.2, -0.22], [-0.9, -0.22]]),
244
+ coracao: () => {
245
+ const sh = new THREE.Shape();
246
+ sh.moveTo(0, -0.9);
247
+ sh.bezierCurveTo(-0.2, -0.65, -1, -0.2, -1, 0.3);
248
+ sh.bezierCurveTo(-1, 0.75, -0.6, 0.95, -0.35, 0.95);
249
+ sh.bezierCurveTo(-0.15, 0.95, 0, 0.8, 0, 0.62);
250
+ sh.bezierCurveTo(0, 0.8, 0.15, 0.95, 0.35, 0.95);
251
+ sh.bezierCurveTo(0.6, 0.95, 1, 0.75, 1, 0.3);
252
+ sh.bezierCurveTo(1, -0.2, 0.2, -0.65, 0, -0.9);
253
+ return sh;
254
+ },
255
+ escudo: () => {
256
+ const sh = new THREE.Shape();
257
+ sh.moveTo(0, 1); sh.lineTo(0.85, 0.7); sh.lineTo(0.8, 0);
258
+ sh.bezierCurveTo(0.75, -0.55, 0.35, -0.85, 0, -1);
259
+ sh.bezierCurveTo(-0.35, -0.85, -0.75, -0.55, -0.8, 0);
260
+ sh.lineTo(-0.85, 0.7); sh.closePath();
261
+ return sh;
262
+ },
263
+ balao: () => {
264
+ const sh = new THREE.Shape();
265
+ const r = 0.3, x0 = -0.95, x1 = 0.95, y0 = -0.45, y1 = 0.8;
266
+ sh.moveTo(x0 + r, y1); sh.lineTo(x1 - r, y1); sh.quadraticCurveTo(x1, y1, x1, y1 - r); sh.lineTo(x1, y0 + r);
267
+ sh.quadraticCurveTo(x1, y0, x1 - r, y0); sh.lineTo(-0.15, y0); sh.lineTo(-0.55, -0.95); sh.lineTo(-0.5, y0);
268
+ sh.lineTo(x0 + r, y0); sh.quadraticCurveTo(x0, y0, x0, y0 + r); sh.lineTo(x0, y1 - r); sh.quadraticCurveTo(x0, y1, x0 + r, y1);
269
+ return sh;
270
+ },
271
+ engrenagem: () => {
272
+ const n = 8, pts = [];
273
+ for (let i = 0; i < n * 4; i++) { const a = (i / (n * 4)) * Math.PI * 2, r = i % 4 < 2 ? 1 : 0.74; pts.push([Math.cos(a) * r, Math.sin(a) * r]); }
274
+ const sh = forma(pts);
275
+ const furo = new THREE.Path(); furo.absarc(0, 0, 0.34, 0, Math.PI * 2, true); sh.holes.push(furo);
276
+ return sh;
277
+ },
278
+ };
279
+
280
+ function Mat({ tipo, cor }) {
281
+ // Metal escovado, bala (verniz) ou argila — os três refletem a sala (Ambiente).
282
+ if (tipo === 'metal') return <meshPhysicalMaterial color={cor} roughness={0.32} metalness={0.85} clearcoat={0.4} envMapIntensity={1.2} />;
283
+ if (tipo === 'verniz') return <meshPhysicalMaterial color={cor} roughness={0.18} metalness={0} clearcoat={1} clearcoatRoughness={0.08} sheen={0.3} />;
284
+ return <meshPhysicalMaterial color={cor} roughness={0.72} metalness={0} clearcoat={0.15} />; // argila: fosca, macia
285
+ }
286
+
287
+ function Extrudado({ nome, m, cor }) {
288
+ const geo = useMemo(() => new THREE.ExtrudeGeometry(FORMAS[nome](), { depth: 0.42, bevelEnabled: true, bevelThickness: 0.12, bevelSize: 0.08, bevelSegments: 4, curveSegments: 18 }), [nome]);
289
+ return <mesh geometry={geo} position={[0, 0, -0.27]}><Mat tipo={m} cor={cor} /></mesh>;
290
+ }
291
+
292
+ function Composto({ nome, m, cor, cor2, tl }) {
293
+ const caixa = (pos, tam, c = cor, rot) => <mesh position={pos} rotation={rot}><boxGeometry args={tam} /><Mat tipo={m} cor={c} /></mesh>;
294
+ switch (nome) {
295
+ case 'bateria': return (
296
+ <group>
297
+ {caixa([0, 0, 0], [1.7, 0.95, 0.5])}
298
+ {caixa([0.95, 0, 0], [0.16, 0.4, 0.3])}
299
+ {[0, 1, 2].map((i) => caixa([-0.52 + i * 0.52, 0, 0.27], [0.36, 0.62, 0.08], i <= Math.floor(tl * 4) % 3 + 0.5 ? cor2 : cor))}
300
+ </group>
301
+ );
302
+ case 'cadeado': return (
303
+ <group>
304
+ {caixa([0, -0.3, 0], [1.3, 1.0, 0.5])}
305
+ <mesh position={[0, 0.25, 0]}><torusGeometry args={[0.42, 0.11, 20, 48, Math.PI]} /><Mat tipo={m} cor={cor2} /></mesh>
306
+ <mesh position={[0, -0.3, 0.27]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.1, 0.1, 0.06, 24]} /><Mat tipo={m} cor={cor2} /></mesh>
307
+ </group>
308
+ );
309
+ case 'relogio': return (
310
+ <group>
311
+ <mesh rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[1, 1, 0.32, 64]} /><Mat tipo={m} cor={cor} /></mesh>
312
+ <mesh position={[0, 0, 0.17]}><torusGeometry args={[0.98, 0.07, 16, 64]} /><Mat tipo={m} cor={cor2} /></mesh>
313
+ <group position={[0, 0, 0.2]} rotation={[0, 0, -tl * 2.2]}>{caixa([0, 0.33, 0], [0.09, 0.66, 0.05], cor2)}</group>
314
+ <group position={[0, 0, 0.22]} rotation={[0, 0, -tl * 0.4 - 1]}>{caixa([0, 0.22, 0], [0.12, 0.44, 0.05], cor2)}</group>
315
+ </group>
316
+ );
317
+ case 'barras': return (
318
+ <group>{[0.55, 0.95, 1.5].map((h, i) => caixa([-0.6 + i * 0.6, -0.75 + (h * Math.min(1, tl * 2.4 - i * 0.25 > 0 ? Math.min(1, tl * 2.4 - i * 0.25) : 0.05)) / 2, 0],
319
+ [0.42, Math.max(0.05, h * Math.min(1, Math.max(0.05, tl * 2.4 - i * 0.25))), 0.42], i === 2 ? cor2 : cor))}</group>
320
+ );
321
+ case 'fone': return (
322
+ <group>
323
+ <mesh><torusGeometry args={[0.85, 0.11, 20, 64, Math.PI]} /><Mat tipo={m} cor={cor} /></mesh>
324
+ {[-1, 1].map((l) => <mesh key={l} position={[l * 0.85, -0.25, 0]} rotation={[0, 0, Math.PI / 2]}><cylinderGeometry args={[0.36, 0.36, 0.3, 40]} /><Mat tipo={m} cor={cor2} /></mesh>)}
325
+ </group>
326
+ );
327
+ case 'celular': return (
328
+ <group>
329
+ {caixa([0, 0, 0], [1.05, 1.9, 0.16])}
330
+ {caixa([0, 0.02, 0.085], [0.9, 1.65, 0.02], cor2)}
331
+ </group>
332
+ );
333
+ case 'sol': return (
334
+ <group rotation={[0, 0, tl * 0.5]}>
335
+ <mesh><sphereGeometry args={[0.52, 48, 32]} /><Mat tipo={m} cor={cor2} /></mesh>
336
+ {Array.from({ length: 8 }, (_, i) => <mesh key={i} position={[Math.cos(i * Math.PI / 4) * 0.85, Math.sin(i * Math.PI / 4) * 0.85, 0]} rotation={[0, 0, i * Math.PI / 4 - Math.PI / 2]}>
337
+ <coneGeometry args={[0.13, 0.36, 20]} /><Mat tipo={m} cor={cor} /></mesh>)}
338
+ </group>
339
+ );
340
+ case 'onda': return (
341
+ <group>{[0.5, 1.1, 1.7, 1.1, 0.5].map((h, i) => { const k = 0.55 + 0.45 * Math.abs(Math.sin(tl * 5 + i * 1.1)); return caixa([-0.9 + i * 0.45, 0, 0], [0.26, h * k, 0.26], i === 2 ? cor2 : cor); })}</group>
342
+ );
343
+ default: return null;
344
+ }
345
+ }
346
+
347
+ export const ICONES = [...Object.keys(FORMAS), 'bateria', 'cadeado', 'relogio', 'barras', 'fone', 'celular', 'sol', 'onda'];
348
+
349
+ export function Icone3D({ el, t }) {
350
+ const tam = Math.round(el.tamanho || 360);
351
+ const tl = Math.max(0, t - (el.t || 0));
352
+ // Entra com mola que passa um pouco (o ícone tem peso) e gira até de frente.
353
+ const k = 1 - Math.exp(-7 * tl) * Math.cos(9 * tl);
354
+ const giro = (1 - saida(tl / 0.8)) * -1.6 + Math.sin(tl * 1.1) * 0.35;
355
+ const m = el.material || 'argila';
356
+ return (
357
+ <div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: tam, height: tam, opacity: t >= (el.t || 0) ? Math.min(1, tl / 0.12) : 0 }}>
358
+ {/* A sombra de contato: o objeto pousa, não flutua solto. */}
359
+ <div style={{ position: 'absolute', left: '18%', width: '64%', top: '84%', height: '10%', borderRadius: '50%', background: 'radial-gradient(ellipse, rgba(0,0,0,0.42) 0%, transparent 70%)', transform: 'scale(' + (0.8 + 0.2 * k).toFixed(3) + ')' }} />
360
+ <ThreeCanvas width={tam} height={tam} camera={{ position: [0, 0, 5.2], fov: 30 }} gl={{ antialias: true, preserveDrawingBuffer: true, alpha: true }}>
361
+ <Ambiente forca={1.1} />
362
+ <hemisphereLight args={['#ffffff', '#3a3a3a', 0.6]} />
363
+ <ambientLight intensity={0.25} />
364
+ <directionalLight position={[2.5, 3, 4]} intensity={2.6} />
365
+ <directionalLight position={[-3, -1, -2]} intensity={2.4} color={el.luz || '#ffffff'} />
366
+ <pointLight position={[0, 0, 3]} intensity={0.5} />
367
+ <group rotation={[0.25 + Math.sin(tl * 0.8) * 0.08, giro, Math.sin(tl * 0.6) * 0.06]} scale={[k, k, k]} position={[0, Math.sin(tl * 1.4) * 0.06, 0]}>
368
+ {FORMAS[el.icone] ? <Extrudado nome={el.icone} m={m} cor={el.cor || '#eeeeee'} /> : <Composto nome={el.icone} m={m} cor={el.cor || '#eeeeee'} cor2={el.cor2 || el.cor || '#ff5a1f'} tl={tl} />}
369
+ </group>
370
+ </ThreeCanvas>
371
+ </div>
372
+ );
373
+ }
374
+
375
+ export const TRES = { produto3d: Produto3D, icone3d: Icone3D };
@@ -49,9 +49,17 @@ const PECAS = {
49
49
  esfera: {},
50
50
  grade: {},
51
51
  site: {},
52
+ // O produto em 3D (kit/tres.jsx): fone, celular, lata ou caixa, de geometria.
53
+ produto3d: { precisa: ['modelo'] },
54
+ // Os ícones 3D de geometria (kit/tres.jsx): raio, coração, bateria, relógio, fone…
55
+ icone3d: { precisa: ['icone'] },
56
+ // O motion gerativo (kit/gerativo.jsx): a partitura inteira, numa peça só (gerativo.js).
57
+ gerativo: { precisa: ['partitura'] },
58
+ // O estilo fluido (kit/fluido.jsx): a forma que vira a próxima, numa peça só (fluido.js).
59
+ fluido: { precisa: ['partitura'] },
52
60
  };
53
61
 
54
- const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'index.jsx'];
62
+ const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'fluido.jsx', 'index.jsx'];
55
63
 
56
64
  /** Os arquivos do kit, para o projeto Remotion: [{nome: 'kit/base.jsx', conteudo}]. */
57
65
  function arquivos() {
@@ -0,0 +1,111 @@
1
+ /* A VOZ LOCAL (Kokoro-82M): roda na CPU desta máquina, sem cota e sem
2
+ servidor. Entra quando o Gemini falha (cota, voz cortada) ou quando o
3
+ roteiro pede `voz: 'kokoro'` (ou PRIMOCODE_VOZ=kokoro). Sem `uv` ou sem
4
+ espeak-ng ela diz que não está disponível e a corrente segue — nunca
5
+ derruba a narração.
6
+
7
+ Provado nesta máquina em 05/10: 11 s de áudio em 3,5 s (+4,3 s de carga
8
+ do modelo), sem o corte no fim que o Gemini às vezes deixa. */
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { spawn, execFileSync } = require('child_process');
12
+
13
+ const SCRIPT = path.join(__dirname, '..', '..', 'studio', 'voz_kokoro.py');
14
+ // Sem `transformers>=4.45` o uv escolhe a 4.12 e o tokenizers não compila (05/10).
15
+ const DEPS = ['kokoro>=0.9.4', 'transformers>=4.45', 'soundfile', 'misaki[en]'];
16
+ // As vozes do Studio viram as do Kokoro em português: pf_dora (feminina), pm_alex (masculina).
17
+ const VOZES = { thalita: 'pf_dora', francisca: 'pf_dora', feminina: 'pf_dora', antonio: 'pm_alex', masculina: 'pm_alex' };
18
+
19
+ /** O roteiro (ou quem usa) pediu a voz local? */
20
+ function querKokoro(voz, env = process.env) {
21
+ return String(voz || '').toLowerCase() === 'kokoro' || String(env.PRIMOCODE_VOZ || '').toLowerCase() === 'kokoro';
22
+ }
23
+
24
+ /** "PrimoCode" sai "Primo Kodi" no Kokoro; escrito assim, sai certo. */
25
+ function pronuncia(texto) {
26
+ return String(texto || '').replace(/primo\s*code/gi, 'Primo Côude');
27
+ }
28
+
29
+ function vozKokoro(voz) {
30
+ const v = String(voz || '').toLowerCase();
31
+ if (/^p[fm]_/.test(v)) return v;
32
+ return VOZES[v] || 'pm_alex';
33
+ }
34
+
35
+ /** Onde está o espeak-ng (o Kokoro precisa dele para o português). */
36
+ function acharEspeak({ existe = fs.existsSync, brew = () => execFileSync('brew', ['--prefix', 'espeak-ng'], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000 }).toString().trim() } = {}) {
37
+ const bases = [];
38
+ try { bases.push(brew()); } catch { /* sem brew */ }
39
+ bases.push('/opt/homebrew/opt/espeak-ng', '/usr/local/opt/espeak-ng', '/usr');
40
+ for (const b of bases) {
41
+ if (!b) continue;
42
+ const dados = path.join(b, 'share', 'espeak-ng-data');
43
+ const lib = ['lib/libespeak-ng.dylib', 'lib/libespeak-ng.so.1', 'lib/x86_64-linux-gnu/libespeak-ng.so.1', 'lib/aarch64-linux-gnu/libespeak-ng.so.1']
44
+ .map((l) => path.join(b, l)).find(existe);
45
+ if (existe(dados) && lib) return { dados, lib };
46
+ }
47
+ return null;
48
+ }
49
+
50
+ /** O comando inteiro (uv + deps fixadas + env do espeak), ou null se falta peça. */
51
+ function comando({ uv, espeak, arquivo, saida, voz, env = process.env }) {
52
+ if (!uv || !espeak) return null;
53
+ const args = ['run', '--quiet', '--python', '3.11'];
54
+ for (const d of DEPS) args.push('--with', d);
55
+ args.push('python', SCRIPT, 'roteiro', '--arquivo', arquivo, '--saida', saida, '--voz', vozKokoro(voz));
56
+ return { cmd: uv, args, env: { ...env, ESPEAK_DATA_PATH: espeak.dados, PHONEMIZER_ESPEAK_LIBRARY: espeak.lib } };
57
+ }
58
+
59
+ function rodar({ cmd, args, env }, timeout) {
60
+ return new Promise((resolve) => {
61
+ const p = spawn(cmd, args, { env });
62
+ let saida = '', erro = '';
63
+ p.stdout.on('data', (d) => { saida += d.toString(); });
64
+ p.stderr.on('data', (d) => { erro += d.toString(); });
65
+ const relogio = setTimeout(() => { try { p.kill(); } catch {} }, timeout);
66
+ p.on('close', () => {
67
+ clearTimeout(relogio);
68
+ try { resolve(JSON.parse(saida.trim().split('\n').pop())); }
69
+ catch { resolve({ ok: false, error: (erro || saida).slice(-400).trim() || 'o kokoro não respondeu' }); }
70
+ });
71
+ p.on('error', (e) => { clearTimeout(relogio); resolve({ ok: false, error: e.message }); });
72
+ });
73
+ }
74
+
75
+ /** Narra a lista [{texto}] em mp3 com o Kokoro. Devolve o mesmo formato do narrar.py. */
76
+ async function narrar(lista, mp3, { voz, uv, espeak = acharEspeak(), executar = rodar, timeout = 15 * 60000 } = {}) {
77
+ const arquivo = mp3.replace(/\.mp3$/, '') + '.kokoro.json';
78
+ const c = comando({ uv, espeak, arquivo, saida: mp3, voz });
79
+ if (!c) return { ok: false, indisponivel: true, error: !uv ? 'kokoro: falta o uv' : 'kokoro: falta o espeak-ng (brew install espeak-ng)' };
80
+ try {
81
+ fs.writeFileSync(arquivo, JSON.stringify(lista.map((x) => ({ ...x, texto: pronuncia(x.texto) }))));
82
+ return await executar(c, timeout);
83
+ } catch (e) {
84
+ return { ok: false, error: e.message };
85
+ } finally {
86
+ try { fs.unlinkSync(arquivo); } catch { /* fica */ }
87
+ }
88
+ }
89
+
90
+ /** A ordem das vozes: pedida → Kokoro e depois o Edge; senão Gemini → Kokoro → Edge.
91
+ `motorVoz: 'edge'` no roteiro pula as outras. */
92
+ function ordem(voz, env = process.env, { soEdge = false } = {}) {
93
+ if (soEdge) return ['edge'];
94
+ return querKokoro(voz, env) ? ['kokoro', 'edge'] : ['gemini', 'kokoro', 'edge'];
95
+ }
96
+
97
+ /** Roda a corrente: a primeira voz que der certo ganha; a falha de uma nunca derruba. */
98
+ async function corrente(passos, provedores) {
99
+ let ultimo = { ok: false, error: 'nenhuma voz disponível' };
100
+ for (const nome of passos) {
101
+ const f = provedores[nome];
102
+ if (!f) continue;
103
+ let r;
104
+ try { r = await f(); } catch (e) { r = { ok: false, error: e.message }; }
105
+ if (r && r.ok) return r;
106
+ if (r) ultimo = r;
107
+ }
108
+ return ultimo;
109
+ }
110
+
111
+ module.exports = { querKokoro, pronuncia, vozKokoro, acharEspeak, comando, narrar, ordem, corrente, DEPS, SCRIPT };
@@ -29,6 +29,7 @@ const { caber } = require('./composicao.js');
29
29
  const { misturarCor, ehCor } = require('./morfar.js');
30
30
  const { tempoDasPartes, gatilhos, volumesDaTrilha, r2 } = require('./ritmo.js');
31
31
  const { tese, termoDef } = require('./vox.js');
32
+ const { modeloDoPedido } = require('./variacao3d.js');
32
33
 
33
34
  const FPS = 30;
34
35
  const k = (t, v, e) => (e ? { t: r2(t), v, e } : { t: r2(t), v });
@@ -326,7 +327,27 @@ function montarLancamento({ roteiro, variante = 'lancamento', largura = 1920, al
326
327
  };
327
328
 
328
329
  // ── as peças que qualquer parte pode pedir pelo nome ("visual") ────
330
+ /* Qual modelo 3D: o que a parte ou o roteiro disser, ou o que o texto
331
+ deixa claro. Sem pista, a caixa — um produto genérico que ainda gira. */
332
+ const modeloDe = modeloDoPedido;
329
333
  const VISUAL = {
334
+ /* O PRODUTO EM 3D (kit/tres.jsx): modelado e iluminado aqui, sem foto
335
+ de fora — girando devagar no centro, o nome embaixo. */
336
+ '3d'(p, d, tp, t0) {
337
+ const texto = `${p.titulo || ''} ${p.texto || ''} ${nome}`;
338
+ const modelo = p.modelo || roteiro.modelo3d || modeloDe(texto);
339
+ const f = fit(p.titulo || nome, W - 2 * M, 1, deitado ? 84 : 96);
340
+ const yT = deitado ? H * 0.8 : H * 0.76;
341
+ const els = [
342
+ { tipo: 'kit', peca: 'produto3d', modelo, cor: p.cor3d || roteiro.cor3d || '#1b1c20', corAcento: ACENTO, t: 0, giro: 0.4, ...(roteiro.variacao3d || {}),
343
+ x: 0, y: r2(deitado ? -H * 0.02 : -H * 0.08), largura: W, altura: H, escala: deitado ? 0.8 : 1.15 }, // deitado a 1,0 o arco saía cortado no topo
344
+ { tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.titulo || nome, x: W / 2, y: r2(yT), largura: W - 2 * M, alinhamento: 'center',
345
+ tamanho: f.tamanho, peso: 800, cor: '#f4f4f6', t: Math.min(0.9, gatilhos([p.titulo || nome], tp, t0, d)[0] || 0.6), ...base },
346
+ ];
347
+ if (p.texto) els.push({ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.texto, x: W / 2, y: r2(yT + f.tamanho * 1.25), largura: W - 2 * M, alinhamento: 'center',
348
+ tamanho: r2(fit(p.texto, W - 2 * M, 2, 40).tamanho), peso: 500, cor: '#b8bcc8', t: 1.1, ...base });
349
+ return [cena(d, ESCURO, true, els, { zoom: 1.06 })];
350
+ },
330
351
  esfera(p, d, tp, t0) {
331
352
  const f = fit(p.titulo || '', W - 2 * M, 1, 64);
332
353
  return [cena(d, ESCURO, true, [
@@ -375,6 +396,12 @@ function montarLancamento({ roteiro, variante = 'lancamento', largura = 1920, al
375
396
 
376
397
  // ── montar ─────────────────────────────────────────────────────────
377
398
  const cenas = [];
399
+ /* Com modelo 3D no roteiro (ou "3d" pedido), a parte de produto — ou a
400
+ abertura, se não houver produto — ganha o produto girando. */
401
+ if (roteiro.modelo3d) {
402
+ const alvo = partes.find((p) => p.tipo === 'produto' && !p.visual) || partes.find((p) => p.tipo === 'abertura' && !p.visual);
403
+ if (alvo) alvo.visual = '3d';
404
+ }
378
405
  partes.forEach((p, i) => {
379
406
  const fazer = (p.visual && VISUAL[p.visual]) || CENAS[p.tipo] || CENAS.titulo;
380
407
  const feitas = fazer(p, dur[i], tempos[i], ini[i], i);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * legibilidade.js — a lei da legibilidade do /brag, em número (comum/desmontados/
3
+ * latent-spaces-brag.md): rótulo curto fica PARADO ~0,8 s; frase, 0,3 s por
4
+ * palavra; "fast-in, then hold — never fast-in, then gone".
5
+ *
6
+ * "Parado" é o tempo entre o fim da entrada e o começo da saída. Quando a cena
7
+ * é curta, quem encolhe é a animação (entrada e saída), nunca a leitura.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const nPalavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
13
+
14
+ /** Quanto tempo o texto precisa ficar parado na tela: max(0,8 s; 0,3 s por palavra). */
15
+ const minimoLeitura = (t) => Math.max(0.8, 0.3 * nPalavras(t));
16
+
17
+ /** O que sobra para a animação depois da leitura, entre um piso e o tempo cheio. */
18
+ const sobra = (vida, leitura, piso, cheio) => Math.min(cheio, Math.max(piso, vida - leitura));
19
+
20
+ module.exports = { minimoLeitura, nPalavras, sobra };