primocode 10.0.0 → 10.0.9

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 (55) hide show
  1. package/README.md +2 -2
  2. package/bin/primocode.js +69 -1
  3. package/lib/act.js +118 -19
  4. package/lib/browser.js +89 -5
  5. package/lib/catalogo.js +1 -1
  6. package/lib/claude-engine.js +38 -17
  7. package/lib/codex-engine.js +27 -9
  8. package/lib/entrada.js +73 -9
  9. package/lib/estudio/cinema.js +27 -9
  10. package/lib/estudio/diretor.js +13 -1
  11. package/lib/estudio/ende.js +16 -5
  12. package/lib/estudio/estilo.js +23 -8
  13. package/lib/estudio/gerativo.js +1453 -0
  14. package/lib/estudio/hibrido.js +165 -0
  15. package/lib/estudio/index.js +244 -36
  16. package/lib/estudio/kit/gerativo.jsx +980 -0
  17. package/lib/estudio/kit/index.jsx +3 -1
  18. package/lib/estudio/kit/tres.jsx +308 -0
  19. package/lib/estudio/kit.js +7 -1
  20. package/lib/estudio/lancamento.js +28 -0
  21. package/lib/estudio/referencias.js +105 -0
  22. package/lib/estudio/remotion.js +53 -9
  23. package/lib/estudio/ritmo.js +2 -2
  24. package/lib/estudio/sons-leves.js +38 -1
  25. package/lib/estudio/variedade.js +105 -0
  26. package/lib/estudio/vox.js +37 -8
  27. package/lib/mcp.js +156 -0
  28. package/lib/personas.js +67 -0
  29. package/lib/skills.js +36 -0
  30. package/lib/tools.js +22 -3
  31. package/lib/ui.js +41 -6
  32. package/lib/video/auto.js +329 -0
  33. package/lib/video/camadas.js +207 -0
  34. package/lib/video/cli.js +1 -1
  35. package/lib/video/filters.js +39 -6
  36. package/lib/video/fx3d/Camadas.jsx +64 -0
  37. package/lib/video/fx3d/Objetos.jsx +128 -0
  38. package/lib/video/fx3d/Root.jsx +18 -0
  39. package/lib/video/fx3d/modelos.jsx +304 -0
  40. package/lib/video/fx3d.js +108 -0
  41. package/lib/video/gerados.js +119 -0
  42. package/lib/video/objetos.js +111 -0
  43. package/lib/video/ops-fx.js +453 -0
  44. package/lib/video/ops.js +30 -5
  45. package/lib/video/paralelo.js +50 -0
  46. package/lib/video/pedido.js +191 -0
  47. package/lib/video/rastrear.js +95 -0
  48. package/lib/video/rastro.js +276 -0
  49. package/lib/video/render.js +67 -14
  50. package/lib/video/texto.js +6 -4
  51. package/lib/video/timeline.js +9 -1
  52. package/package.json +2 -2
  53. package/skills/motion-de-primeira/SKILL.md +5 -4
  54. package/studio/narrar.py +10 -7
  55. package/studio/rastrear.py +399 -0
@@ -6,6 +6,8 @@
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';
9
11
 
10
- export const KIT = { ...VOX, ...MARCA, papel: Papel };
12
+ export const KIT = { ...VOX, ...MARCA, ...TRES, ...GERATIVO, papel: Papel };
11
13
  export { Filtros, Acabamento, Hud, CURVAS_KIT, bolhaCaminho } from './base';
@@ -0,0 +1,308 @@
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",
8
+ * "cor":"#1b1c20","corAcento":"#ff5a1f","t":0,"giro":0.35,
9
+ * "x":0,"y":0,"largura":1920,"altura":1080}
10
+ *
11
+ * O movimento: entra pequeno e girado e assenta com mola (0,9 s), depois gira
12
+ * devagar e flutua — produto de vitrine, nunca pião. A luz de recorte vem na
13
+ * cor de acento: é ela que desenha a silhueta no fundo escuro. */
14
+ import React, { useMemo } from 'react';
15
+ import { ThreeCanvas } from '@remotion/three';
16
+ import * as THREE from 'three';
17
+ import { useThree } from '@react-three/fiber';
18
+ import { RoomEnvironment } from 'three/examples/jsm/environments/RoomEnvironment.js';
19
+
20
+ /* A LUZ DE ESTÚDIO: sem um ambiente, material brilhante ou metálico não tem o
21
+ que refletir e sai preto ("a engrenagem parecia uma mancha", 30/09). A sala
22
+ do three (RoomEnvironment, local, sem rede) vira o reflexo de tudo. */
23
+ function Ambiente({ forca = 1 }) {
24
+ const { gl, scene } = useThree();
25
+ useMemo(() => {
26
+ const pm = new THREE.PMREMGenerator(gl);
27
+ scene.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
28
+ scene.environmentIntensity = forca;
29
+ pm.dispose();
30
+ }, [gl, scene]);
31
+ return null;
32
+ }
33
+
34
+ const saida = (x) => 1 - Math.pow(1 - Math.max(0, Math.min(1, x)), 3);
35
+
36
+ function Material({ cor, brilho = 0.35, metal = 0.25, verniz = 0.8, emissivo }) {
37
+ return <meshPhysicalMaterial color={cor} roughness={brilho} metalness={metal} clearcoat={verniz} clearcoatRoughness={0.25}
38
+ {...(emissivo ? { emissive: emissivo, emissiveIntensity: 1.2 } : {})} />;
39
+ }
40
+
41
+ // O fone: o arco, os deslizadores, as conchas com o anel de acento, as almofadas e o logo aceso.
42
+ function Fone({ cor, acento }) {
43
+ const R = 1.05;
44
+ const concha = (lado) => (
45
+ <group position={[lado * R, -0.12, 0]}>
46
+ <mesh rotation={[0, 0, Math.PI / 2]}>
47
+ <cylinderGeometry args={[0.44, 0.46, 0.26, 64]} />
48
+ <Material cor={cor} brilho={0.3} metal={0.35} />
49
+ </mesh>
50
+ <mesh position={[lado * 0.135, 0, 0]} rotation={[0, Math.PI / 2, 0]}>
51
+ <torusGeometry args={[0.43, 0.035, 24, 96]} />
52
+ <Material cor={acento} brilho={0.18} metal={0.9} verniz={1} />
53
+ </mesh>
54
+ <mesh position={[lado * 0.14, 0, 0]} rotation={[0, Math.PI / 2, 0]}>
55
+ <circleGeometry args={[0.16, 48]} />
56
+ <Material cor={acento} brilho={0.2} metal={0.2} emissivo={acento} />
57
+ </mesh>
58
+ <mesh position={[-lado * 0.16, 0, 0]} rotation={[0, Math.PI / 2, 0]}>
59
+ <torusGeometry args={[0.3, 0.11, 32, 96]} />
60
+ <Material cor="#141416" brilho={0.85} metal={0} verniz={0} />
61
+ </mesh>
62
+ </group>
63
+ );
64
+ return (
65
+ <group>
66
+ <mesh>
67
+ <torusGeometry args={[R, 0.075, 32, 160, Math.PI]} />
68
+ <Material cor={cor} brilho={0.28} metal={0.4} />
69
+ </mesh>
70
+ <mesh position={[0, -0.06, 0]} scale={[1, 1, 1.6]}>
71
+ <torusGeometry args={[R - 0.07, 0.05, 24, 140, Math.PI]} />
72
+ <Material cor="#141416" brilho={0.9} metal={0} verniz={0} />
73
+ </mesh>
74
+ {[-1, 1].map((lado) => (
75
+ <mesh key={lado} position={[lado * R, 0.1, 0]}>
76
+ <boxGeometry args={[0.07, 0.34, 0.12]} />
77
+ <Material cor={acento} brilho={0.15} metal={0.95} verniz={1} />
78
+ </mesh>
79
+ ))}
80
+ {concha(-1)}
81
+ {concha(1)}
82
+ </group>
83
+ );
84
+ }
85
+
86
+ function Celular({ cor, acento }) {
87
+ return (
88
+ <group rotation={[0, 0, 0.05]}>
89
+ <mesh><boxGeometry args={[1.0, 2.05, 0.1]} /><Material cor={cor} brilho={0.22} metal={0.7} /></mesh>
90
+ <mesh position={[0, 0, 0.052]}><planeGeometry args={[0.92, 1.96]} /><Material cor="#0b0c10" brilho={0.1} metal={0} emissivo="#0d1830" /></mesh>
91
+ <mesh position={[0, 0.3, 0.054]}><circleGeometry args={[0.22, 48]} /><Material cor={acento} emissivo={acento} /></mesh>
92
+ <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>
93
+ </group>
94
+ );
95
+ }
96
+
97
+ function Lata({ cor, acento }) {
98
+ return (
99
+ <group>
100
+ <mesh><cylinderGeometry args={[0.55, 0.55, 1.9, 96]} /><Material cor={cor} brilho={0.2} metal={0.85} /></mesh>
101
+ <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>
102
+ <mesh position={[0, 0.97, 0]}><cylinderGeometry args={[0.5, 0.55, 0.06, 96]} /><Material cor="#c9ccd2" brilho={0.25} metal={1} /></mesh>
103
+ </group>
104
+ );
105
+ }
106
+
107
+ function Caixa({ cor, acento }) {
108
+ return (
109
+ <group>
110
+ <mesh><boxGeometry args={[1.5, 1.5, 1.5]} /><Material cor={cor} brilho={0.3} metal={0.3} /></mesh>
111
+ <mesh position={[0, 0, 0.752]}><planeGeometry args={[1.0, 0.18]} /><Material cor={acento} emissivo={acento} /></mesh>
112
+ </group>
113
+ );
114
+ }
115
+
116
+ const MODELOS = { fone: Fone, headset: Fone, celular: Celular, phone: Celular, lata: Lata, garrafa: Lata, caixa: Caixa };
117
+
118
+ export function Produto3D({ el, t }) {
119
+ const W = el.largura || 1920, H = el.altura || 1080;
120
+ const t0 = el.t || 0;
121
+ const tl = Math.max(0, t - t0);
122
+ const k = saida(tl / 0.9);
123
+ const Modelo = MODELOS[el.modelo] || Caixa;
124
+ const cor = el.cor || '#1b1c20';
125
+ const acento = el.corAcento || '#ff5a1f';
126
+ const giro = (el.giro0 ?? -0.9) + (1 - k) * 1.4 + tl * (el.giro ?? 0.35);
127
+ const escala = (el.escala || 1) * (0.72 + 0.28 * k);
128
+ const flutua = Math.sin(tl * 1.3) * 0.05;
129
+ return (
130
+ <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 }}>
131
+ <ThreeCanvas width={W} height={H} camera={{ position: [0, 0.35, 4.4], fov: el.fov || 32 }} gl={{ antialias: true, preserveDrawingBuffer: true }}>
132
+ <Ambiente forca={0.8} />
133
+ <hemisphereLight args={['#ffffff', '#202020', 0.5]} />
134
+ <ambientLight intensity={0.35} />
135
+ <directionalLight position={[3, 4, 5]} intensity={3.2} />
136
+ <directionalLight position={[-2, 3, 4]} intensity={1.1} />
137
+ <directionalLight position={[-4, 1.5, -3]} intensity={3.2} color={acento} />
138
+ <pointLight position={[0, -2, 3]} intensity={0.8} />
139
+ <group position={[0, flutua - 0.05, 0]} rotation={[0.18 + (1 - k) * 0.3, giro, 0]} scale={[escala, escala, escala]}>
140
+ <Modelo cor={cor} acento={acento} />
141
+ </group>
142
+ {/* O chão. O gerativo dispensa (semChao): o disco virava uma faixa escura atrás do texto. */}
143
+ {!el.semChao && (
144
+ <mesh position={[0, -1.25, 0]} rotation={[-Math.PI / 2, 0, 0]}>
145
+ <circleGeometry args={[2.4, 64]} />
146
+ <meshStandardMaterial color="#0a0a0c" roughness={0.35} metalness={0.6} transparent opacity={0.85} />
147
+ </mesh>
148
+ )}
149
+ </ThreeCanvas>
150
+ </div>
151
+ );
152
+ }
153
+
154
+ /* ── OS ÍCONES 3D ─────────────────────────────────────────────────────────
155
+ * "A gente não tem ícone 3D" (30/09). Nada de emoji-3D de banco: cada ícone
156
+ * é geometria daqui — forma 2D extrudada com chanfro (raio, coração,
157
+ * estrela, escudo, balão, play, check, seta, engrenagem) ou primitivas
158
+ * compostas (bateria, cadeado, relógio, barras, fone, celular, sol, onda) —
159
+ * no material e na paleta do vídeo (argila, verniz ou metal), com a luz de
160
+ * recorte na cor de acento.
161
+ *
162
+ * {"tipo":"kit","peca":"icone3d","icone":"raio","x":0,"y":0,"tamanho":360,
163
+ * "material":"argila","cor":"#efe8da","cor2":"#7a1f1f","luz":"#7a1f1f","t":0} */
164
+ function forma(pts, curvas) {
165
+ const sh = new THREE.Shape();
166
+ pts.forEach(([x, y], i) => (i ? sh.lineTo(x, y) : sh.moveTo(x, y)));
167
+ if (curvas) curvas(sh);
168
+ sh.closePath();
169
+ return sh;
170
+ }
171
+ const FORMAS = {
172
+ 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]]),
173
+ 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]; })),
174
+ play: () => forma([[-0.55, 0.85], [0.85, 0], [-0.55, -0.85]]),
175
+ 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]]),
176
+ 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]]),
177
+ coracao: () => {
178
+ const sh = new THREE.Shape();
179
+ sh.moveTo(0, -0.9);
180
+ sh.bezierCurveTo(-0.2, -0.65, -1, -0.2, -1, 0.3);
181
+ sh.bezierCurveTo(-1, 0.75, -0.6, 0.95, -0.35, 0.95);
182
+ sh.bezierCurveTo(-0.15, 0.95, 0, 0.8, 0, 0.62);
183
+ sh.bezierCurveTo(0, 0.8, 0.15, 0.95, 0.35, 0.95);
184
+ sh.bezierCurveTo(0.6, 0.95, 1, 0.75, 1, 0.3);
185
+ sh.bezierCurveTo(1, -0.2, 0.2, -0.65, 0, -0.9);
186
+ return sh;
187
+ },
188
+ escudo: () => {
189
+ const sh = new THREE.Shape();
190
+ sh.moveTo(0, 1); sh.lineTo(0.85, 0.7); sh.lineTo(0.8, 0);
191
+ sh.bezierCurveTo(0.75, -0.55, 0.35, -0.85, 0, -1);
192
+ sh.bezierCurveTo(-0.35, -0.85, -0.75, -0.55, -0.8, 0);
193
+ sh.lineTo(-0.85, 0.7); sh.closePath();
194
+ return sh;
195
+ },
196
+ balao: () => {
197
+ const sh = new THREE.Shape();
198
+ const r = 0.3, x0 = -0.95, x1 = 0.95, y0 = -0.45, y1 = 0.8;
199
+ sh.moveTo(x0 + r, y1); sh.lineTo(x1 - r, y1); sh.quadraticCurveTo(x1, y1, x1, y1 - r); sh.lineTo(x1, y0 + r);
200
+ sh.quadraticCurveTo(x1, y0, x1 - r, y0); sh.lineTo(-0.15, y0); sh.lineTo(-0.55, -0.95); sh.lineTo(-0.5, y0);
201
+ sh.lineTo(x0 + r, y0); sh.quadraticCurveTo(x0, y0, x0, y0 + r); sh.lineTo(x0, y1 - r); sh.quadraticCurveTo(x0, y1, x0 + r, y1);
202
+ return sh;
203
+ },
204
+ engrenagem: () => {
205
+ const n = 8, pts = [];
206
+ 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]); }
207
+ const sh = forma(pts);
208
+ const furo = new THREE.Path(); furo.absarc(0, 0, 0.34, 0, Math.PI * 2, true); sh.holes.push(furo);
209
+ return sh;
210
+ },
211
+ };
212
+
213
+ function Mat({ tipo, cor }) {
214
+ // Metal escovado, bala (verniz) ou argila — os três refletem a sala (Ambiente).
215
+ if (tipo === 'metal') return <meshPhysicalMaterial color={cor} roughness={0.32} metalness={0.85} clearcoat={0.4} envMapIntensity={1.2} />;
216
+ if (tipo === 'verniz') return <meshPhysicalMaterial color={cor} roughness={0.18} metalness={0} clearcoat={1} clearcoatRoughness={0.08} sheen={0.3} />;
217
+ return <meshPhysicalMaterial color={cor} roughness={0.72} metalness={0} clearcoat={0.15} />; // argila: fosca, macia
218
+ }
219
+
220
+ function Extrudado({ nome, m, cor }) {
221
+ const geo = useMemo(() => new THREE.ExtrudeGeometry(FORMAS[nome](), { depth: 0.42, bevelEnabled: true, bevelThickness: 0.12, bevelSize: 0.08, bevelSegments: 4, curveSegments: 18 }), [nome]);
222
+ return <mesh geometry={geo} position={[0, 0, -0.27]}><Mat tipo={m} cor={cor} /></mesh>;
223
+ }
224
+
225
+ function Composto({ nome, m, cor, cor2, tl }) {
226
+ const caixa = (pos, tam, c = cor, rot) => <mesh position={pos} rotation={rot}><boxGeometry args={tam} /><Mat tipo={m} cor={c} /></mesh>;
227
+ switch (nome) {
228
+ case 'bateria': return (
229
+ <group>
230
+ {caixa([0, 0, 0], [1.7, 0.95, 0.5])}
231
+ {caixa([0.95, 0, 0], [0.16, 0.4, 0.3])}
232
+ {[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))}
233
+ </group>
234
+ );
235
+ case 'cadeado': return (
236
+ <group>
237
+ {caixa([0, -0.3, 0], [1.3, 1.0, 0.5])}
238
+ <mesh position={[0, 0.25, 0]}><torusGeometry args={[0.42, 0.11, 20, 48, Math.PI]} /><Mat tipo={m} cor={cor2} /></mesh>
239
+ <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>
240
+ </group>
241
+ );
242
+ case 'relogio': return (
243
+ <group>
244
+ <mesh rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[1, 1, 0.32, 64]} /><Mat tipo={m} cor={cor} /></mesh>
245
+ <mesh position={[0, 0, 0.17]}><torusGeometry args={[0.98, 0.07, 16, 64]} /><Mat tipo={m} cor={cor2} /></mesh>
246
+ <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>
247
+ <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>
248
+ </group>
249
+ );
250
+ case 'barras': return (
251
+ <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],
252
+ [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>
253
+ );
254
+ case 'fone': return (
255
+ <group>
256
+ <mesh><torusGeometry args={[0.85, 0.11, 20, 64, Math.PI]} /><Mat tipo={m} cor={cor} /></mesh>
257
+ {[-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>)}
258
+ </group>
259
+ );
260
+ case 'celular': return (
261
+ <group>
262
+ {caixa([0, 0, 0], [1.05, 1.9, 0.16])}
263
+ {caixa([0, 0.02, 0.085], [0.9, 1.65, 0.02], cor2)}
264
+ </group>
265
+ );
266
+ case 'sol': return (
267
+ <group rotation={[0, 0, tl * 0.5]}>
268
+ <mesh><sphereGeometry args={[0.52, 48, 32]} /><Mat tipo={m} cor={cor2} /></mesh>
269
+ {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]}>
270
+ <coneGeometry args={[0.13, 0.36, 20]} /><Mat tipo={m} cor={cor} /></mesh>)}
271
+ </group>
272
+ );
273
+ case 'onda': return (
274
+ <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>
275
+ );
276
+ default: return null;
277
+ }
278
+ }
279
+
280
+ export const ICONES = [...Object.keys(FORMAS), 'bateria', 'cadeado', 'relogio', 'barras', 'fone', 'celular', 'sol', 'onda'];
281
+
282
+ export function Icone3D({ el, t }) {
283
+ const tam = Math.round(el.tamanho || 360);
284
+ const tl = Math.max(0, t - (el.t || 0));
285
+ // Entra com mola que passa um pouco (o ícone tem peso) e gira até de frente.
286
+ const k = 1 - Math.exp(-7 * tl) * Math.cos(9 * tl);
287
+ const giro = (1 - saida(tl / 0.8)) * -1.6 + Math.sin(tl * 1.1) * 0.35;
288
+ const m = el.material || 'argila';
289
+ return (
290
+ <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 }}>
291
+ {/* A sombra de contato: o objeto pousa, não flutua solto. */}
292
+ <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) + ')' }} />
293
+ <ThreeCanvas width={tam} height={tam} camera={{ position: [0, 0, 5.2], fov: 30 }} gl={{ antialias: true, preserveDrawingBuffer: true, alpha: true }}>
294
+ <Ambiente forca={1.1} />
295
+ <hemisphereLight args={['#ffffff', '#3a3a3a', 0.6]} />
296
+ <ambientLight intensity={0.25} />
297
+ <directionalLight position={[2.5, 3, 4]} intensity={2.6} />
298
+ <directionalLight position={[-3, -1, -2]} intensity={2.4} color={el.luz || '#ffffff'} />
299
+ <pointLight position={[0, 0, 3]} intensity={0.5} />
300
+ <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]}>
301
+ {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} />}
302
+ </group>
303
+ </ThreeCanvas>
304
+ </div>
305
+ );
306
+ }
307
+
308
+ export const TRES = { produto3d: Produto3D, icone3d: Icone3D };
@@ -49,9 +49,15 @@ 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'] },
52
58
  };
53
59
 
54
- const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'index.jsx'];
60
+ const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'index.jsx'];
55
61
 
56
62
  /** Os arquivos do kit, para o projeto Remotion: [{nome: 'kit/base.jsx', conteudo}]. */
57
63
  function arquivos() {
@@ -326,7 +326,29 @@ function montarLancamento({ roteiro, variante = 'lancamento', largura = 1920, al
326
326
  };
327
327
 
328
328
  // ── as peças que qualquer parte pode pedir pelo nome ("visual") ────
329
+ /* Qual modelo 3D: o que a parte ou o roteiro disser, ou o que o texto
330
+ deixa claro. Sem pista, a caixa — um produto genérico que ainda gira. */
331
+ const modeloDe = (txt) => (/fone|headset|headphone|auricular/i.test(txt) ? 'fone'
332
+ : /celular|smartphone|iphone|phone|telefone/i.test(txt) ? 'celular'
333
+ : /lata|bebida|refri|cerveja|energ[eé]tico|garrafa/i.test(txt) ? 'lata' : 'caixa');
329
334
  const VISUAL = {
335
+ /* O PRODUTO EM 3D (kit/tres.jsx): modelado e iluminado aqui, sem foto
336
+ de fora — girando devagar no centro, o nome embaixo. */
337
+ '3d'(p, d, tp, t0) {
338
+ const texto = `${p.titulo || ''} ${p.texto || ''} ${nome}`;
339
+ const modelo = p.modelo || roteiro.modelo3d || modeloDe(texto);
340
+ const f = fit(p.titulo || nome, W - 2 * M, 1, deitado ? 84 : 96);
341
+ const yT = deitado ? H * 0.8 : H * 0.76;
342
+ const els = [
343
+ { tipo: 'kit', peca: 'produto3d', modelo, cor: p.cor3d || roteiro.cor3d || '#1b1c20', corAcento: ACENTO, t: 0, giro: 0.4,
344
+ 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
345
+ { tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.titulo || nome, x: W / 2, y: r2(yT), largura: W - 2 * M, alinhamento: 'center',
346
+ tamanho: f.tamanho, peso: 800, cor: '#f4f4f6', t: Math.min(0.9, gatilhos([p.titulo || nome], tp, t0, d)[0] || 0.6), ...base },
347
+ ];
348
+ 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',
349
+ tamanho: r2(fit(p.texto, W - 2 * M, 2, 40).tamanho), peso: 500, cor: '#b8bcc8', t: 1.1, ...base });
350
+ return [cena(d, ESCURO, true, els, { zoom: 1.06 })];
351
+ },
330
352
  esfera(p, d, tp, t0) {
331
353
  const f = fit(p.titulo || '', W - 2 * M, 1, 64);
332
354
  return [cena(d, ESCURO, true, [
@@ -375,6 +397,12 @@ function montarLancamento({ roteiro, variante = 'lancamento', largura = 1920, al
375
397
 
376
398
  // ── montar ─────────────────────────────────────────────────────────
377
399
  const cenas = [];
400
+ /* Com modelo 3D no roteiro (ou "3d" pedido), a parte de produto — ou a
401
+ abertura, se não houver produto — ganha o produto girando. */
402
+ if (roteiro.modelo3d) {
403
+ const alvo = partes.find((p) => p.tipo === 'produto' && !p.visual) || partes.find((p) => p.tipo === 'abertura' && !p.visual);
404
+ if (alvo) alvo.visual = '3d';
405
+ }
378
406
  partes.forEach((p, i) => {
379
407
  const fazer = (p.visual && VISUAL[p.visual]) || CENAS[p.tipo] || CENAS.titulo;
380
408
  const feitas = fazer(p, dur[i], tempos[i], ini[i], i);
@@ -0,0 +1,105 @@
1
+ /**
2
+ * referencias.js — de onde o vídeo tira as referências, decidido pela
3
+ * PESSOA antes de começar.
4
+ *
5
+ * "A IA deve pedir: pegar referências da web com escopo aberto, pegar
6
+ * referências da web com escopo que você define (escopo fechado), ou sem
7
+ * referências — só motion." (29/09)
8
+ *
9
+ * Três modos:
10
+ * aberto pesquisa, site da marca e b-roll de banco à vontade (o híbrido)
11
+ * fechado só o que a pessoa disse: os sites e termos do escopo, nada além
12
+ * sem nenhuma foto nem vídeo de fora: motion puro (o kit), sem b-roll
13
+ *
14
+ * A escolha vale para o pedido (o terminal pergunta a cada vídeo novo) e é
15
+ * lida em dois lugares: a instrução que vai para o agente e o Estúdio, que
16
+ * não baixa b-roll no "sem" e só busca o escopo no "fechado".
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ let ATUAL = { modo: null, escopo: '' };
22
+
23
+ function definir(modo, escopo = '') {
24
+ ATUAL = { modo: ['aberto', 'fechado', 'sem'].includes(modo) ? modo : null, escopo: String(escopo || '').trim() };
25
+ return ATUAL;
26
+ }
27
+ function atual() { return ATUAL; }
28
+ function limpar() { ATUAL = { modo: null, escopo: '' }; }
29
+
30
+ /** Os termos do escopo fechado: "site:anthropic.com, claude ai, sonnet" → lista. */
31
+ function termos(escopo = ATUAL.escopo) {
32
+ return String(escopo || '').split(/[,;\n]+/).map((x) => x.trim()).filter(Boolean).slice(0, 8);
33
+ }
34
+
35
+ /** A resposta da pergunta ("1", "2", "aberto", "só motion"…) → modo. */
36
+ function entender(resposta) {
37
+ const r = String(resposta || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').trim();
38
+ if (/^1\b|aberto|livre|qualquer/.test(r)) return 'aberto';
39
+ if (/^2\b|fechad|escopo|defino|so (d|n)(e|o)ss/.test(r)) return 'fechado';
40
+ if (/^3\b|sem ref|nenhum|so motion|motion|sem foto|sem imagem/.test(r)) return 'sem';
41
+ return null;
42
+ }
43
+
44
+ /* O MODO DEDUZIDO DO PEDIDO. Quem diz no pedido não é perguntado:
45
+ "só motion", "sem referência", "sem foto" → sem
46
+ "só do site x.com", "usa apenas anthropic.com e …" → fechado (o escopo)
47
+ "web aberta", "escopo aberto", "pesquisa à vontade" → aberto
48
+ O resto volta com `explicito: false` e o terminal pergunta antes de
49
+ começar: "eu posso pedir um vídeo mas estar me referindo ao motion"
50
+ (Joel, 02/10) — chutar o aberto fazia vídeo com foto e b-roll quando a
51
+ pessoa queria só motion. */
52
+ function inferir(texto) {
53
+ const t = String(texto || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
54
+ if (/so (o )?motion|motion puro|sem refer|sem (foto|imagem|imagens|video de banco|pesquisa)|nao (pesquis|busqu)|so anima/.test(t)) return { modo: 'sem', escopo: '', explicito: true };
55
+ const dominios = String(texto || '').match(/\b[a-z0-9-]+(\.[a-z0-9-]+)+\.?[a-z]{2,}(\/[^\s,]*)?/gi) || [];
56
+ if (/\b(so|apenas|somente|unicamente)\b[^.]{0,30}\b(site|sites|link|links|fonte|fontes|pagina|de|do|da|em)\b/.test(t) && dominios.length) {
57
+ return { modo: 'fechado', escopo: dominios.join(', '), explicito: true };
58
+ }
59
+ if (/escopo aberto|web aberta|pesquis\w* (a vontade|na web|na internet)|busc\w* (imagens|fotos|videos) na (web|internet)|com (imagens|fotos) da (web|internet)/.test(t)) {
60
+ return { modo: 'aberto', escopo: '', explicito: true };
61
+ }
62
+ return { modo: 'aberto', escopo: '', explicito: false };
63
+ }
64
+
65
+ /** A instrução para o agente, conforme o modo. */
66
+ function instrucao({ modo, escopo } = ATUAL) {
67
+ if (modo === 'sem') {
68
+ return '[REFERÊNCIAS: NENHUMA] A pessoa quer SÓ MOTION: não pesquise (nada de pesquisar_referencia nem estudio_site), '
69
+ + 'não busque foto nem vídeo, não use estudio_midia de banco. Escreva o roteiro com o que o pedido já diz e deixe '
70
+ + 'o motion gerativo fazer o trabalho (roteiro.estilo "gerativo": a tipografia anima sozinha, sem template).';
71
+ }
72
+ if (modo === 'fechado') {
73
+ const t = termos(escopo);
74
+ return `[REFERÊNCIAS: ESCOPO FECHADO] Use SÓ estas fontes e termos: ${t.join(' · ') || escopo}. `
75
+ + 'Pesquise apenas dentro delas (em pesquisar_referencia, cite o site ou o termo no próprio texto da busca). '
76
+ + 'Nada de fato, foto ou vídeo de fora desse escopo; se faltar algo, diga no fim em uma linha. '
77
+ + `No roteiro, ponha roteiro.busca = ${JSON.stringify(t)} para o b-roll.`;
78
+ }
79
+ if (modo === 'aberto') {
80
+ return '[REFERÊNCIAS: ESCOPO ABERTO] Pesquise o assunto à vontade (pesquisar_referencia; estudio_site se for de uma marca) '
81
+ + 'e dê em cada parte do roteiro um "busca" em inglês com o que mostrar de fundo — gente, lugar, tela, objeto de verdade.';
82
+ }
83
+ return '';
84
+ }
85
+
86
+ /** Os domínios do escopo fechado: "só do site anthropic.com" → ['anthropic.com']. */
87
+ function dominios(escopo = ATUAL.escopo) {
88
+ return termos(escopo).map((t) => t.replace(/^site:/, '').replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0].toLowerCase())
89
+ .filter((t) => /^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(t));
90
+ }
91
+
92
+ /** O resultado da pesquisa sem o que está fora do escopo fechado (subdomínios valem: www-cdn.anthropic.com). */
93
+ function filtrar(r, { modo, escopo } = ATUAL) {
94
+ if (modo !== 'fechado' || !r || !Array.isArray(r.referencias)) return r;
95
+ const ds = dominios(escopo);
96
+ if (!ds.length) return r;
97
+ const dentro = (u) => { try { const h = new URL(u).hostname.toLowerCase(); return ds.some((d) => h === d || h.endsWith('.' + d)); } catch { return false; } };
98
+ const refs = r.referencias.filter((x) => dentro(x.url));
99
+ const fora = r.referencias.length - refs.length;
100
+ if (!refs.length) return { ok: false, consulta: r.consulta, error: `nada desta busca é de ${ds.join(', ')} (escopo fechado)`,
101
+ comoCorrigir: `Abra o site direto: browser_open https://${ds[0]} e browser_read. Não use fonte de fora.` };
102
+ return { ...r, fontes: refs.length, referencias: refs, ...(fora ? { foraDoEscopo: fora } : {}) };
103
+ }
104
+
105
+ module.exports = { definir, atual, limpar, termos, entender, inferir, instrucao, dominios, filtrar };
@@ -74,6 +74,10 @@ function pkgJson(nome) {
74
74
  remotion: VERSAO_REMOTION,
75
75
  react: '19.0.0',
76
76
  'react-dom': '19.0.0',
77
+ // O produto em 3D (kit/tres.jsx). Instalado uma vez na base compartilhada.
78
+ '@remotion/three': VERSAO_REMOTION,
79
+ three: '0.186.1',
80
+ '@react-three/fiber': '9.8.1',
77
81
  },
78
82
  }, null, 2) + '\n';
79
83
  }
@@ -877,9 +881,15 @@ function fontesCSS(plano) {
877
881
  for (const cena of plano.cenas || []) andar(cena.elementos || []);
878
882
  andar(plano.hud);
879
883
  if (!usadas.size) return '/* nenhuma fonte externa neste vídeo */\n';
884
+ /* A FONTE VARIÁVEL: o motion gerativo anima os eixos (wght, wdth, GRAD…)
885
+ e precisa do arquivo variável inteiro, pedido pela faixa de cada eixo
886
+ ("Roboto+Flex:opsz,slnt,wdth,wght,GRAD@8..144,…"). As outras seguem
887
+ com os pesos fixos. */
888
+ const variaveis = plano.fontesVariaveis || {};
880
889
  const familias = [...usadas]
881
890
  // O Google devolve o que a família tem e ignora o resto: pedir todos os pesos e o itálico não quebra.
882
- .map((f) => 'family=' + encodeURIComponent(f).replace(/%20/g, '+') + ':ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,600')
891
+ .map((f) => 'family=' + encodeURIComponent(f).replace(/%20/g, '+')
892
+ + (variaveis[f] ? ':' + variaveis[f] : ':ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,400;1,600'))
883
893
  .join('&');
884
894
  return `@import url('https://fonts.googleapis.com/css2?${familias}&display=block');\n`;
885
895
  }
@@ -904,15 +914,47 @@ function citarWin(a) {
904
914
  return '"' + t.replace(/"/g, '""') + '"';
905
915
  }
906
916
 
917
+ /* O REMOTION COMPARTILHADO. Cada projeto rodava o próprio `npm install`
918
+ (minutos, e ~300 MB por vídeo) — e um 404 passageiro do registro no meio
919
+ dele derrubava o vídeo inteiro (medido em 30/09: source-map-js 1.2.2).
920
+ Agora a instalação mora UMA vez em ~/.primocode/studio/remotion-base, com
921
+ as mesmas dependências, e o projeto aponta para ela. Dependência diferente
922
+ (versão nova do Remotion) reinstala a base. */
923
+ const os = require('os');
924
+ function baseCompartilhada(raiz) {
925
+ const base = path.join(os.homedir(), '.primocode', 'studio', 'remotion-base');
926
+ let deps = '';
927
+ try { const pj = JSON.parse(fs.readFileSync(path.join(raiz, 'package.json'), 'utf8')); deps = JSON.stringify({ d: pj.dependencies || {}, v: pj.devDependencies || {} }); } catch { return null; }
928
+ return { base, deps, pronta: (() => {
929
+ try { return fs.existsSync(path.join(base, 'node_modules', '.bin')) && fs.readFileSync(path.join(base, 'deps.json'), 'utf8') === deps; } catch { return false; }
930
+ })() };
931
+ }
932
+
907
933
  function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
908
934
  return new Promise((resolve) => {
909
- const temNodeModules = fs.existsSync(path.join(raiz, 'node_modules'));
910
- const passos = temNodeModules ? [] : [['npm', ['install', '--no-audit', '--no-fund']]];
911
- passos.push(['npx', ['remotion', 'render', 'Video', saida, '--log=warn']]);
912
-
913
- const correr = (i) => {
935
+ let temNodeModules = fs.existsSync(path.join(raiz, 'node_modules'));
936
+ const comp = temNodeModules || process.platform === 'win32' ? null : baseCompartilhada(raiz);
937
+ if (comp && comp.pronta) {
938
+ try { fs.symlinkSync(path.join(comp.base, 'node_modules'), path.join(raiz, 'node_modules'), 'dir'); temNodeModules = true; } catch { /* instala aqui mesmo */ }
939
+ }
940
+ const passos = [];
941
+ if (!temNodeModules && comp) {
942
+ // Instala na base (com uma segunda tentativa: o registro às vezes devolve 404 por instantes) e liga.
943
+ try { fs.mkdirSync(comp.base, { recursive: true }); fs.copyFileSync(path.join(raiz, 'package.json'), path.join(comp.base, 'package.json')); } catch { /* segue */ }
944
+ passos.push(['npm', ['install', '--no-audit', '--no-fund'], { cwd: comp.base, tentativas: 2, depois: () => {
945
+ fs.writeFileSync(path.join(comp.base, 'deps.json'), comp.deps);
946
+ try { fs.symlinkSync(path.join(comp.base, 'node_modules'), path.join(raiz, 'node_modules'), 'dir'); } catch { /* já existe */ }
947
+ } }]);
948
+ } else if (!temNodeModules) passos.push(['npm', ['install', '--no-audit', '--no-fund'], { tentativas: 2 }]);
949
+ // Vídeo de b-roll pesado (Pexels HD) levava mais de 30 s para o primeiro quadro.
950
+ /* VÁRIOS TRABALHADORES no render: o Remotion usa metade dos núcleos por
951
+ padrão; aqui vão quase todos (um fica para o terminal respirar). */
952
+ const nucleos = Math.max(2, (require('os').cpus() || []).length - 1);
953
+ passos.push(['npx', ['remotion', 'render', 'Video', saida, '--log=warn', '--timeout=120000', `--concurrency=${nucleos}`]]);
954
+
955
+ const correr = (i, tentativa = 1) => {
914
956
  if (i >= passos.length) return resolve({ ok: true, arquivo: saida });
915
- const [cmd, args] = passos[i];
957
+ const [cmd, args, op = {}] = passos[i];
916
958
  aoFalar(i === 0 && !temNodeModules
917
959
  ? 'instalando o Remotion nesta pasta (uma vez só, alguns minutos)'
918
960
  : 'renderizando quadro a quadro');
@@ -926,7 +968,7 @@ function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
926
968
  CVE-2024-27980 o Node recusa spawnar npm.cmd/npx.cmd sem shell.
927
969
  Então as aspas entram na mão, só onde o shell existe. */
928
970
  const citados = process.platform === 'win32' ? args.map(citarWin) : args;
929
- const p = spawn(cmd, citados, { cwd: raiz, shell: process.platform === 'win32' });
971
+ const p = spawn(cmd, citados, { cwd: op.cwd || raiz, shell: process.platform === 'win32' });
930
972
  let erro = '';
931
973
  p.stderr.on('data', (d) => { erro += d.toString().slice(0, 2000); });
932
974
  p.stdout.on('data', (d) => {
@@ -937,8 +979,10 @@ function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
937
979
  p.on('close', (code) => {
938
980
  clearTimeout(relogio);
939
981
  if (code !== 0) {
982
+ if (tentativa < (op.tentativas || 1)) { aoFalar('o npm tropeçou — tentando de novo'); return setTimeout(() => correr(i, tentativa + 1), 3000); }
940
983
  return resolve({ ok: false, error: `${cmd} saiu com ${code}: ${erro.slice(-600).trim()}` });
941
984
  }
985
+ try { if (op.depois) op.depois(); } catch { /* segue */ }
942
986
  correr(i + 1);
943
987
  });
944
988
  p.on('error', (e) => {
@@ -961,4 +1005,4 @@ function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
961
1005
  });
962
1006
  }
963
1007
 
964
- module.exports = { montar, renderizar, fontesCSS, VERSAO_REMOTION, CENA_JSX };
1008
+ module.exports = { montar, renderizar, fontesCSS, VERSAO_REMOTION, CENA_JSX, pkgJson, baseCompartilhada, TSCONFIG };
@@ -25,7 +25,7 @@ const r2 = (n) => Math.round(n * 100) / 100;
25
25
  *
26
26
  * @returns {{ini:number[], dur:number[], total:number, temVoz:boolean}}
27
27
  */
28
- function tempoDasPartes(partes, tempos = [], { semVoz = () => 2.6, antes = 0.12, cauda = 1.1, minimo = 1.1 } = {}) {
28
+ function tempoDasPartes(partes, tempos = [], { semVoz = () => 2.0, antes = 0.08, cauda = 0.6, minimo = 0.9 } = {}) {
29
29
  const temVoz = tempos.length === partes.length && tempos.every((t) => t && t.ate > t.de);
30
30
  const ini = [];
31
31
  if (temVoz) partes.forEach((_, i) => ini.push(i === 0 ? 0 : r2(Math.max(ini[i - 1] + minimo, tempos[i].de - antes))));
@@ -60,7 +60,7 @@ function palavrasNaCena(tempo, inicio) {
60
60
  }
61
61
 
62
62
  /** O volume da trilha no tempo: cheia no respiro, baixa quando a voz fala, some no fim. */
63
- function volumesDaTrilha(tempos, total, { alto = 0.22, baixo = 0.11 } = {}) {
63
+ function volumesDaTrilha(tempos, total, { alto = 0.4, baixo = 0.2 } = {}) {
64
64
  const k = (t, v) => ({ t: r2(t), v });
65
65
  const vs = [k(0, 0.05), k(0.4, alto)];
66
66
  // Um quadro-chave de cada vez, sempre depois do anterior: fala colada na outra não volta no tempo.