primocode 9.10.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 (89) hide show
  1. package/README.md +30 -6
  2. package/bin/primocode.js +169 -342
  3. package/lib/act.js +360 -39
  4. package/lib/browser.js +105 -4
  5. package/lib/casar.js +108 -0
  6. package/lib/catalogo.js +6 -2
  7. package/lib/claude-engine.js +38 -17
  8. package/lib/codex-engine.js +27 -9
  9. package/lib/config.js +10 -3
  10. package/lib/cursor.js +10 -1
  11. package/lib/entrada.js +77 -11
  12. package/lib/estudio/cinema.js +27 -9
  13. package/lib/estudio/diretor.js +21 -1
  14. package/lib/estudio/ende.js +16 -5
  15. package/lib/estudio/estilo.js +66 -0
  16. package/lib/estudio/gerativo.js +1453 -0
  17. package/lib/estudio/glifos.js +215 -0
  18. package/lib/estudio/hibrido.js +165 -0
  19. package/lib/estudio/index.js +284 -53
  20. package/lib/estudio/kit/base.jsx +285 -0
  21. package/lib/estudio/kit/gerativo.jsx +980 -0
  22. package/lib/estudio/kit/index.jsx +13 -0
  23. package/lib/estudio/kit/marca.jsx +741 -0
  24. package/lib/estudio/kit/tres.jsx +308 -0
  25. package/lib/estudio/kit/vox.jsx +357 -0
  26. package/lib/estudio/kit.js +78 -0
  27. package/lib/estudio/lancamento.js +441 -0
  28. package/lib/estudio/motion.js +2 -5
  29. package/lib/estudio/plano.js +28 -3
  30. package/lib/estudio/referencias.js +105 -0
  31. package/lib/estudio/remotion.js +139 -51
  32. package/lib/estudio/ritmo.js +77 -0
  33. package/lib/estudio/sons-leves.js +38 -1
  34. package/lib/estudio/variedade.js +105 -0
  35. package/lib/estudio/vox.js +414 -0
  36. package/lib/fala.js +1 -94
  37. package/lib/mcp.js +156 -0
  38. package/lib/personas.js +67 -0
  39. package/lib/pipeline.js +55 -56
  40. package/lib/repeticao.js +3 -1
  41. package/lib/skills.js +44 -2
  42. package/lib/snapshot.js +111 -0
  43. package/lib/tools.js +100 -22
  44. package/lib/ui.js +50 -8
  45. package/lib/video/analise.js +89 -0
  46. package/lib/video/auto.js +329 -0
  47. package/lib/video/camadas.js +207 -0
  48. package/lib/video/cli.js +74 -0
  49. package/lib/video/delta.js +90 -0
  50. package/lib/video/edits.js +379 -0
  51. package/lib/video/filters.js +280 -0
  52. package/lib/video/fx3d/Camadas.jsx +64 -0
  53. package/lib/video/fx3d/Objetos.jsx +128 -0
  54. package/lib/video/fx3d/Root.jsx +18 -0
  55. package/lib/video/fx3d/modelos.jsx +304 -0
  56. package/lib/video/fx3d.js +108 -0
  57. package/lib/video/gerados.js +119 -0
  58. package/lib/video/objetos.js +111 -0
  59. package/lib/video/ops-fx.js +453 -0
  60. package/lib/video/ops.js +347 -0
  61. package/lib/video/paralelo.js +50 -0
  62. package/lib/video/pedido.js +191 -0
  63. package/lib/video/projeto.js +133 -0
  64. package/lib/video/rastrear.js +95 -0
  65. package/lib/video/rastro.js +276 -0
  66. package/lib/video/render.js +274 -0
  67. package/lib/video/texto.js +144 -0
  68. package/lib/video/timeline.js +318 -0
  69. package/lib/video/transcript.js +124 -0
  70. package/lib/voz.js +75 -223
  71. package/package.json +2 -3
  72. package/skills/motion-de-primeira/SKILL.md +5 -5
  73. package/studio/narrar.py +10 -7
  74. package/studio/rastrear.py +399 -0
  75. package/studio/texto.py +208 -0
  76. package/voz/primo_voz/__main__.py +96 -236
  77. package/voz/primo_voz/transcrever.py +31 -1
  78. package/app/ansi.js +0 -55
  79. package/app/app.css +0 -500
  80. package/app/app.js +0 -1017
  81. package/app/icone.svg +0 -6
  82. package/app/index.html +0 -220
  83. package/app/primo-marca.js +0 -192
  84. package/lib/app.js +0 -615
  85. package/lib/icone.js +0 -263
  86. package/lib/instalar-app.js +0 -231
  87. package/lib/painel.js +0 -291
  88. package/voz/primo_voz/desperta.py +0 -195
  89. package/voz/primo_voz/orbe.py +0 -406
@@ -0,0 +1,304 @@
1
+ /* fx3d/modelos.jsx — os objetos 3D que flutuam na mão, feitos de geometria.
2
+ *
3
+ * Nenhum arquivo de marca é baixado: as duas "logos" são DESENHADAS aqui,
4
+ * como formas genéricas que lembram o tema — um nó hexagonal de seis elos
5
+ * vazados (o "assistente verde") e um sol de raios irregulares (o
6
+ * "assistente coral") — extrudadas com chanfro sobre uma placa arredondada,
7
+ * como ícone de app. O cérebro é procedural: dois hemisférios de esfera
8
+ * deformada, com os sulcos feitos de ruído.
9
+ *
10
+ * Todo modelo cabe numa caixa de ~1 unidade de lado, centrado na origem. */
11
+ import React, { useMemo } from 'react';
12
+ import * as THREE from 'three';
13
+
14
+ function placa(lado = 1.15, raio = 0.28) {
15
+ const s = new THREE.Shape();
16
+ const m = lado / 2;
17
+ s.moveTo(-m + raio, -m);
18
+ s.lineTo(m - raio, -m); s.quadraticCurveTo(m, -m, m, -m + raio);
19
+ s.lineTo(m, m - raio); s.quadraticCurveTo(m, m, m - raio, m);
20
+ s.lineTo(-m + raio, m); s.quadraticCurveTo(-m, m, -m, m - raio);
21
+ s.lineTo(-m, -m + raio); s.quadraticCurveTo(-m, -m, -m + raio, -m);
22
+ return s;
23
+ }
24
+
25
+ const EXT = (prof, chanfro = 0.035) => ({ depth: prof, bevelEnabled: true, bevelThickness: chanfro, bevelSize: chanfro, bevelSegments: 6, curveSegments: 36 });
26
+
27
+ function Placa({ cor }) {
28
+ const g = useMemo(() => { const x = new THREE.ExtrudeGeometry(placa(), EXT(0.16, 0.05)); x.translate(0, 0, -0.2); return x; }, []);
29
+ // Fosca o bastante para a cor aparecer: muito verniz e o reflexo da sala lava a placa para branco.
30
+ return (
31
+ <mesh geometry={g}>
32
+ <meshPhysicalMaterial color={cor} roughness={0.5} metalness={0} clearcoat={0.6} clearcoatRoughness={0.35} />
33
+ </mesh>
34
+ );
35
+ }
36
+
37
+ /* Um elo: a cápsula VAZADA (contorno grosso). Seis delas, giradas de 60° em
38
+ volta do centro e inclinadas, formam o nó hexagonal. */
39
+ function elo(comp = 0.62, larg = 0.24, esp = 0.065) {
40
+ const capsula = (c, l) => {
41
+ const s = new THREE.Path();
42
+ const r = l / 2, m = c / 2 - r;
43
+ s.moveTo(-m, -r); s.lineTo(m, -r);
44
+ s.absarc(m, 0, r, -Math.PI / 2, Math.PI / 2, false);
45
+ s.lineTo(-m, r);
46
+ s.absarc(-m, 0, r, Math.PI / 2, (3 * Math.PI) / 2, false);
47
+ return s;
48
+ };
49
+ const fora = new THREE.Shape(capsula(comp, larg).getPoints(48));
50
+ fora.holes.push(new THREE.Path(capsula(comp - 2 * esp, larg - 2 * esp).getPoints(48).reverse()));
51
+ return fora;
52
+ }
53
+
54
+ function NoHexagonal() {
55
+ const g = useMemo(() => new THREE.ExtrudeGeometry(elo(), EXT(0.07, 0.018)), []);
56
+ return (
57
+ <group scale={[0.92, 0.92, 0.92]}>
58
+ {Array.from({ length: 6 }, (_, k) => {
59
+ const a = (k * Math.PI) / 3;
60
+ return (
61
+ <group key={k} rotation={[0, 0, a]}>
62
+ <mesh geometry={g} position={[0.04, 0.2, 0.02 + (k % 2) * 0.012]} rotation={[0, 0, Math.PI / 3.2]}>
63
+ <meshPhysicalMaterial color="#ffffff" roughness={0.18} metalness={0.1} clearcoat={1} clearcoatRoughness={0.08} />
64
+ </mesh>
65
+ </group>
66
+ );
67
+ })}
68
+ </group>
69
+ );
70
+ }
71
+
72
+ /* O sol de raios: onze raios de comprimento e largura um pouco diferentes
73
+ (a irregularidade é o que faz parecer desenhado à mão, não engrenagem). */
74
+ function raio(comp, base, ponta) {
75
+ const s = new THREE.Shape();
76
+ s.moveTo(-base / 2, 0);
77
+ s.lineTo(-ponta / 2, comp - ponta / 2);
78
+ s.absarc(0, comp - ponta / 2, ponta / 2, Math.PI, 0, true);
79
+ s.lineTo(base / 2, 0);
80
+ s.lineTo(-base / 2, 0);
81
+ return s;
82
+ }
83
+
84
+ function Sol({ cor }) {
85
+ const raios = useMemo(() => {
86
+ const comps = [0.47, 0.42, 0.5, 0.44, 0.48, 0.4, 0.49, 0.43, 0.46, 0.41, 0.5];
87
+ return comps.map((c, k) => ({ g: new THREE.ExtrudeGeometry(raio(c, 0.07, 0.1 + (k % 3) * 0.012), EXT(0.08, 0.02)), a: (k / comps.length) * Math.PI * 2 + 0.12 * Math.sin(k * 2.3) }));
88
+ }, []);
89
+ return (
90
+ <group position={[0, 0, 0.02]}>
91
+ {raios.map((r, k) => (
92
+ <mesh key={k} geometry={r.g} rotation={[0, 0, r.a]}>
93
+ <meshPhysicalMaterial color={cor} roughness={0.35} metalness={0.05} clearcoat={0.8} clearcoatRoughness={0.2} />
94
+ </mesh>
95
+ ))}
96
+ <mesh position={[0, 0, 0.05]}><cylinderGeometry args={[0.09, 0.09, 0.1, 48]} /><meshPhysicalMaterial color={cor} roughness={0.35} clearcoat={0.8} /></mesh>
97
+ </group>
98
+ );
99
+ }
100
+
101
+ /* ── o cérebro ────────────────────────────────────────────────────────────
102
+ * Esfera → elipsoide → giros. O giro é RUÍDO DE CRISTA (1 − |simplex|, com o
103
+ * espaço torcido por outro ruído): dá as linhas sinuosas, contínuas e de
104
+ * largura parecida que o córtex tem — seno sobre seno dava papel amassado.
105
+ * O lado de dentro de cada hemisfério é achatado e a fenda fica no meio. */
106
+ const GRAD = [[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0], [1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1], [0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1]];
107
+ const PERM = (() => {
108
+ // Permutação fixa (semente constante): o cérebro sai igual em todo render.
109
+ const p = Array.from({ length: 256 }, (_, i) => i);
110
+ let x = 1234567;
111
+ for (let i = 255; i > 0; i--) { x = (x * 1103515245 + 12345) & 0x7fffffff; const j = x % (i + 1); [p[i], p[j]] = [p[j], p[i]]; }
112
+ return [...p, ...p];
113
+ })();
114
+ function simplex(xin, yin, zin) {
115
+ const F3 = 1 / 3, G3 = 1 / 6;
116
+ const s = (xin + yin + zin) * F3;
117
+ const i = Math.floor(xin + s), j = Math.floor(yin + s), k = Math.floor(zin + s);
118
+ const t = (i + j + k) * G3;
119
+ const x0 = xin - (i - t), y0 = yin - (j - t), z0 = zin - (k - t);
120
+ let i1, j1, k1, i2, j2, k2;
121
+ if (x0 >= y0) {
122
+ if (y0 >= z0) { i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 1; k2 = 0; } else if (x0 >= z0) { i1 = 1; j1 = 0; k1 = 0; i2 = 1; j2 = 0; k2 = 1; } else { i1 = 0; j1 = 0; k1 = 1; i2 = 1; j2 = 0; k2 = 1; }
123
+ } else if (y0 < z0) { i1 = 0; j1 = 0; k1 = 1; i2 = 0; j2 = 1; k2 = 1; } else if (x0 < z0) { i1 = 0; j1 = 1; k1 = 0; i2 = 0; j2 = 1; k2 = 1; } else { i1 = 0; j1 = 1; k1 = 0; i2 = 1; j2 = 1; k2 = 0; }
124
+ const cantos = [[x0, y0, z0, 0, 0, 0], [x0 - i1 + G3, y0 - j1 + G3, z0 - k1 + G3, i1, j1, k1],
125
+ [x0 - i2 + 2 * G3, y0 - j2 + 2 * G3, z0 - k2 + 2 * G3, i2, j2, k2], [x0 - 1 + 3 * G3, y0 - 1 + 3 * G3, z0 - 1 + 3 * G3, 1, 1, 1]];
126
+ let n = 0;
127
+ const ii = i & 255, jj = j & 255, kk = k & 255;
128
+ for (const [x, y, z, a, b, c] of cantos) {
129
+ let tt = 0.6 - x * x - y * y - z * z;
130
+ if (tt < 0) continue;
131
+ const g = GRAD[PERM[ii + a + PERM[jj + b + PERM[kk + c]]] % 12];
132
+ tt *= tt;
133
+ n += tt * tt * (g[0] * x + g[1] * y + g[2] * z);
134
+ }
135
+ return 32 * n;
136
+ }
137
+ /* 0 no fundo do sulco, 1 no alto do giro. O sulco é onde o ruído cruza o
138
+ zero — curvas fechadas e sinuosas; o giro é o resto, largo e redondo. */
139
+ const liso = (a, b, x) => { const t = Math.max(0, Math.min(1, (x - a) / (b - a))); return t * t * (3 - 2 * t); };
140
+ function sulcos(x, y, z) {
141
+ const w = 0.25 * simplex(x * 1.3 + 7, y * 1.3, z * 1.3);
142
+ const n1 = simplex(x * 2.6 + w, y * 2.6 - w, z * 2.6 + w);
143
+ const n2 = simplex(x * 5.2 - w + 3, y * 5.2 + w, z * 5.2);
144
+ return Math.min(liso(0.0, 0.3, Math.abs(n1)), 0.35 + 0.65 * liso(0.0, 0.22, Math.abs(n2)));
145
+ }
146
+
147
+ // Cor de vértice é linear no three: os valores parecem escuros, e saem rosados na tela.
148
+ const ROSA_FUNDO = new THREE.Color('#3a0816');
149
+ const ROSA_TOPO = new THREE.Color('#d65a7c');
150
+
151
+ /* Cor por vértice: o fundo do sulco escurece (a "sombra" que a luz sozinha
152
+ não faz numa malha tão fina) e a crista do giro clareia. */
153
+ function pintar(g, alturas) {
154
+ const cores = new Float32Array(alturas.length * 3);
155
+ const c = new THREE.Color();
156
+ for (let i = 0; i < alturas.length; i++) {
157
+ c.copy(ROSA_FUNDO).lerp(ROSA_TOPO, 0.25 + 0.75 * alturas[i]);
158
+ cores.set([c.r, c.g, c.b], i * 3);
159
+ }
160
+ g.setAttribute('color', new THREE.BufferAttribute(cores, 3));
161
+ }
162
+
163
+ function hemisferio(lado) {
164
+ const g = new THREE.SphereGeometry(1, 220, 170);
165
+ const p = g.attributes.position;
166
+ const v = new THREE.Vector3();
167
+ const alturas = new Float32Array(p.count);
168
+ for (let i = 0; i < p.count; i++) {
169
+ v.fromBufferAttribute(p, i);
170
+ const s = sulcos(v.x + lado * 0.3, v.y, v.z);
171
+ alturas[i] = s;
172
+ v.multiplyScalar(0.93 + 0.07 * s);
173
+ v.set(v.x * 0.46, v.y * 0.44, v.z * 0.74);
174
+ if (v.x * lado < 0) v.x *= 0.3; // achata o lado de dentro (contínuo: sem degrau)
175
+ if (v.y < 0) v.y *= 0.72; // a base mais plana
176
+ p.setXYZ(i, v.x + lado * 0.16, v.y, v.z);
177
+ }
178
+ g.computeVertexNormals();
179
+ pintar(g, alturas);
180
+ return g;
181
+ }
182
+
183
+ function Cerebro({ cor }) {
184
+ const [e, d] = useMemo(() => [hemisferio(-1), hemisferio(1)], []);
185
+ const mat = <meshPhysicalMaterial vertexColors color={cor || '#ffffff'} roughness={0.5} metalness={0} clearcoat={0.35} clearcoatRoughness={0.4} sheen={0.3} sheenColor="#ffc2d2" />;
186
+ /* Visto de cima e de frente (o topo inclinado para a câmera): é assim que
187
+ se reconhece um cérebro — os dois hemisférios e a fenda no meio. */
188
+ return (
189
+ <group rotation={[0.95, 0, 0]} scale={[1.05, 1.05, 1.05]}>
190
+ <mesh geometry={e}>{mat}</mesh>
191
+ <mesh geometry={d}>{mat}</mesh>
192
+ </group>
193
+ );
194
+ }
195
+
196
+ /* Texto curto numa placa: desenhado num canvas (sem fonte 3D para baixar). */
197
+ function PlacaTexto({ texto = '', cor = '#111827' }) {
198
+ const tex = useMemo(() => {
199
+ const cv = document.createElement('canvas');
200
+ cv.width = 512; cv.height = 512;
201
+ const ctx = cv.getContext('2d');
202
+ ctx.fillStyle = '#ffffff';
203
+ const t = String(texto).slice(0, 14).toUpperCase();
204
+ let tam = 200;
205
+ ctx.font = `900 ${tam}px system-ui, -apple-system, Helvetica, Arial`;
206
+ while (ctx.measureText(t).width > 440 && tam > 40) { tam -= 8; ctx.font = `900 ${tam}px system-ui, -apple-system, Helvetica, Arial`; }
207
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
208
+ ctx.fillText(t, 256, 270);
209
+ const x = new THREE.CanvasTexture(cv);
210
+ x.colorSpace = THREE.SRGBColorSpace;
211
+ return x;
212
+ }, [texto]);
213
+ return (
214
+ <group>
215
+ <Placa cor={cor} />
216
+ <mesh position={[0, 0, 0.05]}><planeGeometry args={[1.0, 1.0]} /><meshBasicMaterial map={tex} transparent toneMapped={false} /></mesh>
217
+ </group>
218
+ );
219
+ }
220
+
221
+ /* O navegador: o anel de três arcos (vermelho, amarelo, verde) em volta da
222
+ pupila azul, extrudado — lembra o tema sem copiar o arquivo da marca. */
223
+ function Navegador() {
224
+ const arcos = useMemo(() => [0, 1, 2].map((k) => {
225
+ const s = new THREE.Shape();
226
+ const a0 = (k * 2 * Math.PI) / 3 + Math.PI / 2;
227
+ const a1 = a0 + (2 * Math.PI) / 3 - 0.06;
228
+ s.absarc(0, 0, 0.44, a0, a1, false);
229
+ s.absarc(0, 0, 0.2, a1, a0, true);
230
+ return new THREE.ExtrudeGeometry(s, EXT(0.09, 0.02));
231
+ }), []);
232
+ const cores = ['#db4437', '#f4b400', '#0f9d58'];
233
+ return (
234
+ <group position={[0, 0, -0.02]}>
235
+ {arcos.map((g, k) => (<mesh key={k} geometry={g}><meshPhysicalMaterial color={cores[k]} roughness={0.45} clearcoat={0.5} /></mesh>))}
236
+ <mesh position={[0, 0, 0.06]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.15, 0.15, 0.12, 48]} /><meshPhysicalMaterial color="#ffffff" roughness={0.4} /></mesh>
237
+ <mesh position={[0, 0, 0.1]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.11, 0.11, 0.12, 48]} /><meshPhysicalMaterial color="#4285f4" roughness={0.35} clearcoat={0.6} /></mesh>
238
+ </group>
239
+ );
240
+ }
241
+
242
+ /* A pasta na nuvem: o triângulo de três faixas (verde, azul, amarelo). */
243
+ function Triangulo() {
244
+ const faixas = useMemo(() => {
245
+ const L = 0.5, h = L * Math.sqrt(3);
246
+ const v = [[0, h * 0.62], [-L, -h * 0.38], [L, -h * 0.38]];
247
+ return [0, 1, 2].map((k) => {
248
+ const a = v[k], b = v[(k + 1) % 3];
249
+ const nx = (b[1] - a[1]), ny = -(b[0] - a[0]);
250
+ const n = Math.hypot(nx, ny);
251
+ const e = 0.2;
252
+ const s = new THREE.Shape();
253
+ s.moveTo(a[0], a[1]); s.lineTo(b[0], b[1]);
254
+ s.lineTo(b[0] - (nx / n) * e * 0.9 + (a[0] - b[0]) * 0.18, b[1] - (ny / n) * e * 0.9 + (a[1] - b[1]) * 0.18);
255
+ s.lineTo(a[0] - (nx / n) * e * 0.9 + (b[0] - a[0]) * 0.18, a[1] - (ny / n) * e * 0.9 + (b[1] - a[1]) * 0.18);
256
+ s.closePath();
257
+ return new THREE.ExtrudeGeometry(s, EXT(0.09, 0.02));
258
+ });
259
+ }, []);
260
+ const cores = ['#0f9d58', '#4285f4', '#f4b400'];
261
+ return (
262
+ <group position={[0, 0.02, -0.02]} scale={[0.9, 0.9, 0.9]}>
263
+ {faixas.map((g, k) => (<mesh key={k} geometry={g}><meshPhysicalMaterial color={cores[k]} roughness={0.45} clearcoat={0.5} side={THREE.DoubleSide} /></mesh>))}
264
+ </group>
265
+ );
266
+ }
267
+
268
+ /* O foguete: corpo em revolução (o perfil girado), janela, três aletas e a
269
+ chama que pulsa — de geometria, como o resto. */
270
+ function Foguete() {
271
+ const corpo = useMemo(() => {
272
+ const p = [[0, -0.42], [0.15, -0.4], [0.17, -0.1], [0.16, 0.12], [0.12, 0.28], [0.06, 0.4], [0, 0.46]].map(([x, y]) => new THREE.Vector2(x, y));
273
+ return new THREE.LatheGeometry(p, 48);
274
+ }, []);
275
+ const aleta = useMemo(() => {
276
+ const s = new THREE.Shape();
277
+ s.moveTo(0, 0); s.lineTo(0.16, -0.12); s.lineTo(0.16, -0.26); s.lineTo(0, -0.18); s.closePath();
278
+ return new THREE.ExtrudeGeometry(s, EXT(0.03, 0.01));
279
+ }, []);
280
+ return (
281
+ <group rotation={[0, 0, -0.35]} scale={[1.1, 1.1, 1.1]}>
282
+ <mesh geometry={corpo}><meshPhysicalMaterial color="#f3f4f6" roughness={0.3} metalness={0.2} clearcoat={0.8} /></mesh>
283
+ <mesh position={[0, 0.34, 0]}><sphereGeometry args={[0.075, 32, 32]} /><meshPhysicalMaterial color="#e5484d" roughness={0.35} clearcoat={0.6} /></mesh>
284
+ <mesh position={[0, 0.12, 0.15]}><sphereGeometry args={[0.055, 32, 32]} /><meshPhysicalMaterial color="#3b82f6" roughness={0.1} metalness={0.3} clearcoat={1} /></mesh>
285
+ {[0, 1, 2].map((k) => (
286
+ <group key={k} rotation={[0, (k * 2 * Math.PI) / 3, 0]}>
287
+ <mesh geometry={aleta} position={[0.14, -0.14, -0.015]}><meshPhysicalMaterial color="#e5484d" roughness={0.4} clearcoat={0.5} /></mesh>
288
+ </group>
289
+ ))}
290
+ <mesh position={[0, -0.52, 0]} rotation={[Math.PI, 0, 0]}><coneGeometry args={[0.1, 0.24, 32]} /><meshBasicMaterial color="#ffb020" toneMapped={false} /></mesh>
291
+ <mesh position={[0, -0.49, 0]} rotation={[Math.PI, 0, 0]}><coneGeometry args={[0.06, 0.14, 32]} /><meshBasicMaterial color="#fff3c4" toneMapped={false} /></mesh>
292
+ </group>
293
+ );
294
+ }
295
+
296
+ export function Modelo({ modelo, cor, texto }) {
297
+ if (modelo === 'foguete') return <Foguete />;
298
+ if (modelo === 'chrome') return (<group><Placa cor={cor || '#ffffff'} /><Navegador /></group>);
299
+ if (modelo === 'drive') return (<group><Placa cor={cor || '#ffffff'} /><Triangulo /></group>);
300
+ if (modelo === 'chatgpt') return (<group><Placa cor={cor || '#0b8a6a'} /><NoHexagonal /></group>);
301
+ if (modelo === 'claude') return (<group><Placa cor={cor || '#f4efe6'} /><Sol cor="#cf5f3c" /></group>);
302
+ if (modelo === 'cerebro') return <Cerebro cor={cor} />;
303
+ return <PlacaTexto texto={texto || modelo} cor={cor || '#111827'} />;
304
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * fx3d.js — as mídias GERADAS do editor: o momento 3D das camadas e a
3
+ * camada dos objetos que seguem as mãos. Cada uma é um asset da timeline
4
+ * (`asset.gerado`), renderizado no Remotion na primeira vez que o export ou
5
+ * o inspect precisa dele, e guardado pelo hash da receita.
6
+ *
7
+ * ── POR QUE O 3D SAI COMO UMA CAMADA, E O RESTO CONTINUA NO ffmpeg ───────
8
+ * O editor inteiro é um grafo de ffmpeg (filters.js): corte, ripple, cor,
9
+ * legenda. Levar tudo para o Remotion obrigaria a reescrever o editor e
10
+ * deixaria cada export lento como um render de navegador. O contrário é
11
+ * o robusto: só o que PRECISA de GPU (o three.js, a perspectiva das
12
+ * camadas) passa pelo Remotion, numa camada com canal alfa (ProRes 4444 —
13
+ * o ffmpeg lê o alfa dele sem decodificador especial, ao contrário do VP9),
14
+ * e entra no grafo como um clipe qualquer. O clipe "espelha" a fala na
15
+ * timeline, então um corte de silêncio corta a camada no mesmo quadro.
16
+ *
17
+ * A receita fica na timeline (pequena); as trilhas por quadro saem do
18
+ * rastro na hora do render — mudar a receita muda o hash e refaz só ela.
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const fs = require('fs');
24
+ const os = require('os');
25
+ const path = require('path');
26
+ const crypto = require('crypto');
27
+ const { spawn } = require('child_process');
28
+ const RM = require('../estudio/remotion.js');
29
+
30
+ const FONTES = path.join(__dirname, 'fx3d');
31
+ const ARQUIVOS = ['Root.jsx', 'Camadas.jsx', 'Objetos.jsx', 'modelos.jsx'];
32
+
33
+ const CONFIG = `import { Config } from '@remotion/cli/config';
34
+ // O three.js precisa de WebGL de verdade no Chromium sem tela.
35
+ Config.setChromiumOpenGlRenderer('angle');
36
+ `;
37
+
38
+ function trabalhadores() { return Math.max(1, (os.cpus() || []).length - 1); }
39
+
40
+ /** A versão das fontes JSX: mudou o desenho, muda o hash, e o render refaz. */
41
+ let _versao = null;
42
+ function versao() {
43
+ if (_versao) return _versao;
44
+ return (_versao = hash(ARQUIVOS.map((a) => fs.readFileSync(path.join(FONTES, a), 'utf8')).join('\n')));
45
+ }
46
+
47
+ function hash(obj) { return crypto.createHash('sha1').update(JSON.stringify(obj)).digest('hex').slice(0, 12); }
48
+
49
+ function escrever(arq, txt) {
50
+ fs.mkdirSync(path.dirname(arq), { recursive: true });
51
+ if (!fs.existsSync(arq) || fs.readFileSync(arq, 'utf8') !== txt) fs.writeFileSync(arq, txt);
52
+ }
53
+
54
+ function rodar(cmd, args, { cwd, minutos = 30 } = {}) {
55
+ return new Promise((resolve) => {
56
+ const p = spawn(cmd, args, { cwd, shell: process.platform === 'win32' });
57
+ let erro = '';
58
+ p.stderr.on('data', (d) => { erro = (erro + d.toString()).slice(-3000); });
59
+ p.stdout.on('data', (d) => { erro = (erro + d.toString()).slice(-3000); });
60
+ const rel = setTimeout(() => { try { p.kill(); } catch {} }, minutos * 60000);
61
+ p.on('error', (e) => { clearTimeout(rel); resolve({ ok: false, error: e.message }); });
62
+ p.on('close', (code) => { clearTimeout(rel); resolve(code === 0 ? { ok: true } : { ok: false, error: `${cmd} saiu com ${code}: ${erro.slice(-700)}` }); });
63
+ });
64
+ }
65
+
66
+ /** O projeto Remotion do editor, em <projeto>/cache/fx3d (um por projeto de vídeo). */
67
+ async function projeto(pasta) {
68
+ const raiz = path.join(pasta, 'cache', 'fx3d');
69
+ escrever(path.join(raiz, 'package.json'), RM.pkgJson('fx3d'));
70
+ escrever(path.join(raiz, 'remotion.config.ts'), CONFIG);
71
+ escrever(path.join(raiz, 'tsconfig.json'), RM.TSCONFIG);
72
+ for (const a of ARQUIVOS) escrever(path.join(raiz, 'src', a), fs.readFileSync(path.join(FONTES, a), 'utf8'));
73
+ fs.mkdirSync(path.join(raiz, 'public'), { recursive: true });
74
+ if (fs.existsSync(path.join(raiz, 'node_modules'))) return { ok: true, raiz };
75
+ const comp = RM.baseCompartilhada(raiz);
76
+ if (comp && !comp.pronta) {
77
+ // A mesma base do Estúdio (~/.primocode/studio/remotion-base): instala uma vez.
78
+ fs.mkdirSync(comp.base, { recursive: true });
79
+ fs.copyFileSync(path.join(raiz, 'package.json'), path.join(comp.base, 'package.json'));
80
+ const r = await rodar('npm', ['install', '--no-audit', '--no-fund'], { cwd: comp.base, minutos: 20 });
81
+ if (!r.ok) return r;
82
+ fs.writeFileSync(path.join(comp.base, 'deps.json'), comp.deps);
83
+ }
84
+ if (comp) {
85
+ try { fs.symlinkSync(path.join(comp.base, 'node_modules'), path.join(raiz, 'node_modules'), 'dir'); return { ok: true, raiz }; } catch { /* instala aqui */ }
86
+ }
87
+ const r = await rodar('npm', ['install', '--no-audit', '--no-fund'], { cwd: raiz, minutos: 20 });
88
+ return r.ok ? { ok: true, raiz } : r;
89
+ }
90
+
91
+ /** Renderiza uma composição. `alfa`: ProRes 4444 com transparência. */
92
+ async function renderizar(raiz, comp, props, saida, { alfa = false } = {}) {
93
+ const arqProps = path.join(raiz, `props-${hash(props)}.json`);
94
+ fs.writeFileSync(arqProps, JSON.stringify(props));
95
+ const tmp = saida.replace(/(\.\w+)$/, `.tmp$1`);
96
+ fs.mkdirSync(path.dirname(saida), { recursive: true });
97
+ const formato = alfa
98
+ ? ['--codec=prores', '--prores-profile=4444', '--pixel-format=yuva444p10le', '--image-format=png']
99
+ : ['--codec=h264', '--crf=14', '--image-format=jpeg', '--jpeg-quality=95'];
100
+ const r = await rodar('npx', ['remotion', 'render', 'src/Root.jsx', comp, tmp, `--props=${arqProps}`,
101
+ `--concurrency=${trabalhadores()}`, ...formato, '--log=error', '--timeout=120000'], { cwd: raiz, minutos: 40 });
102
+ try { fs.unlinkSync(arqProps); } catch {}
103
+ if (!r.ok) return r;
104
+ fs.renameSync(tmp, saida);
105
+ return { ok: true, arquivo: saida };
106
+ }
107
+
108
+ module.exports = { projeto, renderizar, hash, versao, trabalhadores, rodar, ARQUIVOS };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * gerados.js — na hora do export (ou do inspect), o que a timeline PEDE e
3
+ * ainda não existe em disco: as camadas 3D renderizadas no Remotion, e a
4
+ * posição de cada legenda que segue a cabeça.
5
+ *
6
+ * Uma operação de efeito só escreve a receita (rápido, com undo). O render
7
+ * pesado acontece uma vez, aqui, e fica no disco pelo hash da receita:
8
+ * desfazer e refazer não renderiza de novo.
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const T = require('./timeline.js');
16
+ const C = require('./camadas.js');
17
+ const R = require('./rastro.js');
18
+ const RT = require('./rastrear.js');
19
+ const OB = require('./objetos.js');
20
+ const FX = require('./fx3d.js');
21
+ const video = require('../estudio/video.js');
22
+
23
+ function propsObjetos(tl, a, rastro) {
24
+ const baseAsset = tl.assets[a.gerado.base];
25
+ const clip = C.clipesBase(tl, baseAsset.id)[0];
26
+ const tela = clip ? (x, y) => R.origemParaTela(tl, clip, baseAsset, x, y) : (x, y) => ({ x, y });
27
+ const p = OB.plano({ receita: a.gerado, trilhas: R.trilhasDasMaos(rastro), rostos: (rastro.quadros || []).map((q) => (q && q.f) || null), rfps: rastro.fps, fps: tl.fps, tela, asp: tl.width / tl.height });
28
+ return { ...p, fps: tl.fps, w: tl.width, h: tl.height };
29
+ }
30
+
31
+ /** Os quadros da pessoa recortada (PNG com alfa) e a placa do fundo, no public/ do projeto Remotion. */
32
+ async function quadrosDasCamadas(raiz, tl, a, arqs) {
33
+ const g = a.gerado;
34
+ const baseAsset = tl.assets[g.base];
35
+ const dir = path.join(raiz, 'public', a.id + '-' + FX.hash(g));
36
+ fs.rmSync(dir, { recursive: true, force: true });
37
+ fs.mkdirSync(dir, { recursive: true });
38
+ const n = Math.max(1, Math.round((g.ate - g.de) * tl.fps));
39
+ const W = tl.width;
40
+ const H = tl.height;
41
+ const encaixe = `scale=${W}:${H}:force_original_aspect_ratio=increase,crop=${W}:${H}`;
42
+ const ss = Math.max(0, g.de - 0.5 / (baseAsset.fps || tl.fps)).toFixed(6);
43
+ const r = await video.rodar(video.ffmpeg(), ['-hide_banner', '-loglevel', 'error', '-y',
44
+ '-ss', ss, '-i', baseAsset.path, '-ss', ss, '-i', arqs.matte,
45
+ '-filter_complex', `[0:v]fps=${tl.fps},${encaixe}[v];[1:v]format=gray,fps=${tl.fps},${encaixe}[m];[v][m]alphamerge[p]`,
46
+ '-map', '[p]', '-frames:v', String(n), '-compression_level', '1', path.join(dir, 'p%04d.png')]);
47
+ if (!r.ok) return { ok: false, error: `não extraí os quadros das camadas: ${r.erro.slice(-300)}` };
48
+ const r2 = await video.rodar(video.ffmpeg(), ['-hide_banner', '-loglevel', 'error', '-y', '-i', arqs.fundo, '-vf', encaixe, path.join(dir, 'fundo.png')]);
49
+ if (!r2.ok) return { ok: false, error: 'não preparei a placa do fundo' };
50
+ return { ok: true, dir, rel: path.basename(dir), n };
51
+ }
52
+
53
+ /** Renderiza o que falta. Devolve {ok, feitos, tookS} ou o erro. */
54
+ async function garantir(pasta, tl, { aoFalar = () => {} } = {}) {
55
+ const falta = Object.values(tl.assets).filter((a) => a.gerado && !fs.existsSync(a.path));
56
+ if (!falta.length) return { ok: true, feitos: 0 };
57
+ const inicio = Date.now();
58
+ const pj = await FX.projeto(pasta);
59
+ if (!pj.ok) return { ok: false, error: `o Remotion do editor não subiu: ${pj.error}` };
60
+ for (const a of falta) {
61
+ const baseAsset = tl.assets[a.gerado.base];
62
+ if (!baseAsset) return { ok: false, error: `a camada ${a.id} aponta para a mídia ${a.gerado.base}, que não existe` };
63
+ const rs = await RT.garantir(pasta, baseAsset);
64
+ if (!rs.ok) return rs;
65
+ aoFalar(`renderizando ${a.nome} (Remotion, ${FX.trabalhadores()} trabalhadores)`);
66
+ let r;
67
+ if (a.gerado.tipo === 'objetos') {
68
+ r = await FX.renderizar(pj.raiz, 'Objetos', propsObjetos(tl, a, rs.rastro), a.path, { alfa: true });
69
+ } else if (a.gerado.tipo === 'camadas') {
70
+ const q = await quadrosDasCamadas(pj.raiz, tl, a, rs.arquivos);
71
+ if (!q.ok) return q;
72
+ r = await FX.renderizar(pj.raiz, 'Camadas', { n: q.n, fps: tl.fps, w: tl.width, h: tl.height, dir: q.rel, fundo: `${q.rel}/fundo.png`, rotulos: a.gerado.rotulos || null }, a.path);
73
+ fs.rmSync(q.dir, { recursive: true, force: true });
74
+ } else r = { ok: false, error: `tipo gerado "${a.gerado.tipo}"` };
75
+ if (!r.ok) return { ok: false, error: `o render 3D falhou (${a.nome}): ${r.error}` };
76
+ }
77
+ return { ok: true, feitos: falta.length, tookS: Math.round((Date.now() - inicio) / 100) / 10 };
78
+ }
79
+
80
+ // ── a legenda que segue a cabeça ─────────────────────────────────────────
81
+
82
+ const Q = (v) => Math.round(v / 0.004) * 0.004; // passo de ~8 px: menos estados de PNG, sem tremer
83
+
84
+ /**
85
+ * Uma função (clipe, quadroDaTimeline) → {cx, cy} | null para as legendas
86
+ * com `text.follow`, ou null se nenhuma segue. `avisos` recebe o que faltou.
87
+ */
88
+ function posicoesDeLegenda(pasta, tl, avisos = []) {
89
+ const seguem = T.todosOsClipes(tl).some((x) => x.clip.text && x.clip.text.follow);
90
+ if (!seguem) return null;
91
+ const cabecas = {};
92
+ const cabecaDe = (assetId) => {
93
+ if (assetId in cabecas) return cabecas[assetId];
94
+ const a = tl.assets[assetId];
95
+ const arq = a && RT.arquivos(pasta, a).rastro;
96
+ if (!arq || !fs.existsSync(arq)) { avisos.push(`legenda na cabeça: a mídia ${assetId} não foi rastreada (rode track)`); return (cabecas[assetId] = null); }
97
+ const rastro = RT.lerRastro(arq);
98
+ return (cabecas[assetId] = { rastro, trilha: R.trilhaDaCabeca(rastro) });
99
+ };
100
+ return (c, f) => {
101
+ const fw = c.text && c.text.follow;
102
+ if (!fw) return null;
103
+ const cab = cabecaDe(fw.asset);
104
+ if (!cab) return null;
105
+ const clip = C.clipeNoQuadro(tl, fw.asset, f);
106
+ if (!clip) return null;
107
+ const seg = C.quadroParaOrigem(tl, fw.asset, f);
108
+ const i = Math.max(0, Math.min(cab.trilha.length - 1, Math.round(seg * cab.rastro.fps)));
109
+ const h = cab.trilha[i];
110
+ if (!h) return null;
111
+ const asset = tl.assets[fw.asset];
112
+ const topo = R.origemParaTela(tl, clip, asset, h.cx, h.top);
113
+ const tam = (c.text.style && c.text.style.size) || 0.048;
114
+ const p = R.acimaDaCabeca({ cx: topo.x, top: topo.y }, { altura: tam * 1.45, largura: Math.min(c.transform.w, 0.9), margem: fw.margem ?? 0.02 });
115
+ return p && { cx: Q(p.cx), cy: Q(p.cy) };
116
+ };
117
+ }
118
+
119
+ module.exports = { garantir, propsObjetos, posicoesDeLegenda };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * objetos.js — a receita dos objetos na mão vira, quadro a quadro, ONDE
3
+ * cada um está (as props do fx3d/Objetos.jsx). Conta pura: entra a receita
4
+ * (segundos da origem) e as trilhas das mãos, sai posição e tamanho.
5
+ *
6
+ * · o objeto flutua ACIMA da palma (um tanto do tamanho da palma), e não
7
+ * em cima dela: a mão aberta segura o objeto no ar;
8
+ * · o TAMANHO é fixo durante a vida do objeto (a mediana da palma): a
9
+ * palma medida pulsa com o gesto, e um logo que incha e murcha a cada
10
+ * quadro parece defeito, não perspectiva;
11
+ * · quadro sem mão segura a última posição — o objeto não some porque o
12
+ * detector piscou;
13
+ * · nunca sai da tela: quem fala para a câmera tem a mão na borda de
14
+ * baixo, e o objeto "acima da palma" ficaria cortado.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const r4 = (n) => Math.round(n * 10000) / 10000;
20
+
21
+ function mediana(v) {
22
+ const s = v.filter((x) => Number.isFinite(x)).sort((a, b) => a - b);
23
+ return s.length ? s[Math.floor(s.length / 2)] : null;
24
+ }
25
+
26
+ /** A trilha de uma mão no intervalo [i0, i1) de quadros da ORIGEM, segurando o último valor. */
27
+ function segurar(trilha, i0, i1) {
28
+ const out = [];
29
+ let ult = null;
30
+ for (let i = i0; i < i1; i++) { const h = trilha[i] || null; if (h) ult = h; out.push(ult); }
31
+ // O começo sem mão herda a primeira que aparecer.
32
+ const primeira = out.find(Boolean) || null;
33
+ return out.map((h) => h || primeira);
34
+ }
35
+
36
+ /**
37
+ * {receita, trilhas:{left,right}, rfps (fps do rastro), fps (do projeto),
38
+ * tela(x,y) → {x,y} (origem → quadro do projeto), asp (largura/altura do projeto)}
39
+ * → {n, objetos:[{modelo,cor,texto,f0,f1,trilha:[[x,y,s]|0],funde}], fusao?}
40
+ */
41
+ function plano({ receita, trilhas, rfps, fps, tela = (x, y) => ({ x, y }), asp = 9 / 16, rostos = null }) {
42
+ const n = Math.max(1, Math.round((receita.ate - receita.de) * fps));
43
+ const iDe = (seg) => Math.round((seg - receita.de) * fps); // segundo da origem → quadro da camada
44
+ const iOrig = (f) => Math.round((receita.de + f / fps) * rfps); // quadro da camada → quadro do rastro
45
+ const posicao = (h, tam) => {
46
+ const p = tela(h.x, h.y - 1.2 * h.s);
47
+ // A placa do objeto tem ~1,2× o "tamanho" de largura (girando, um pouco mais).
48
+ const mx = (1.25 * tam / asp) / 2 + 0.015;
49
+ const my = 1.25 * tam / 2 + 0.015;
50
+ return [r4(Math.max(mx, Math.min(1 - mx, p.x))), r4(Math.max(my, Math.min(0.93 - my, p.y))), r4(tam)];
51
+ };
52
+ const fusao = receita.fusao || null;
53
+ const fj = fusao && fusao.junta !== null && fusao.junta !== undefined ? iDe(fusao.junta) : null;
54
+ const fm = fj !== null ? fj + Math.round(0.4 * fps) : null;
55
+ const objetos = [];
56
+ let tamBase = null;
57
+ for (const o of receita.objetos || []) {
58
+ const trilha = trilhas[o.mao] || trilhas.right;
59
+ const f0 = Math.max(0, iDe(o.de));
60
+ let f1 = Math.min(n, iDe(o.ate));
61
+ const funde = fj !== null && f0 < fj && f1 > fj;
62
+ if (funde) f1 = Math.min(n, fm);
63
+ if (f1 <= f0) continue;
64
+ const brutos = segurar(trilha, iOrig(f0), iOrig(f1) + 1);
65
+ const tam = Math.max(0.085, Math.min(0.2, 1.45 * (mediana(brutos.map((h) => h && h.s)) || 0.08)));
66
+ tamBase = tamBase || tam;
67
+ const lista = [];
68
+ for (let f = f0; f < f1; f++) {
69
+ const h = brutos[iOrig(f) - iOrig(f0)] || brutos[brutos.length - 1];
70
+ lista.push(h ? posicao(h, tam) : 0);
71
+ }
72
+ if (!lista.some(Boolean)) continue;
73
+ objetos.push({ modelo: o.modelo, ...(o.cor ? { cor: o.cor } : {}), ...(o.texto ? { texto: o.texto } : {}), f0, f1, trilha: lista, ...(funde ? { funde: true } : {}) });
74
+ }
75
+ const out = { n, objetos };
76
+ const fundidos = objetos.filter((o) => o.funde);
77
+ if (fusao && fj !== null && fundidos.length) {
78
+ // O meio das duas mãos (ou a mão que houver), um pouco acima: onde a fusão acontece e o cérebro nasce.
79
+ const ate = Math.min(n, fusao.depoisAte !== undefined ? iDe(fusao.depoisAte) : n);
80
+ const L = segurar(trilhas.left, iOrig(fj), iOrig(ate) + 1);
81
+ const R = segurar(trilhas.right, iOrig(fj), iOrig(ate) + 1);
82
+ const tam = Math.min(0.26, (tamBase || 0.12) * 1.6);
83
+ const meio = [];
84
+ for (let f = fj; f < ate; f++) {
85
+ const k = iOrig(f) - iOrig(fj);
86
+ const a = L[k];
87
+ const b = R[k];
88
+ const h = a && b ? { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2, s: (a.s + b.s) / 2 } : a || b;
89
+ let q = h ? posicao({ ...h, y: h.y - 0.25 * h.s }, tam) : 0;
90
+ /* As mãos abertas ficam na altura do rosto (o gesto de "nasceu"), e o
91
+ meio delas é a cara da pessoa: o cérebro desce para logo abaixo do
92
+ queixo. Visto no primeiro vídeo de verdade (01/10). */
93
+ const fc = q && rostos && rostos[iOrig(f)];
94
+ if (fc) {
95
+ const a0 = tela(fc[0], fc[1]);
96
+ const a1 = tela(fc[2], fc[3]);
97
+ const cobre = q[0] + tam / asp / 2 > a0.x && q[0] - tam / asp / 2 < a1.x && q[1] - tam / 2 < a1.y && q[1] + tam / 2 > a0.y;
98
+ if (cobre) q = [q[0], r4(Math.min(0.93 - 1.25 * tam / 2 - 0.015, a1.y + 0.62 * tam)), q[2]];
99
+ }
100
+ meio.push(q);
101
+ }
102
+ out.fusao = { fj, fm, trilha: meio };
103
+ if (fusao.depois) {
104
+ const fa = fusao.abre !== null && fusao.abre !== undefined ? Math.max(fm, iDe(fusao.abre)) : fm + Math.round(0.3 * fps);
105
+ if (fa < ate) objetos.push({ modelo: fusao.depois, f0: fa, f1: ate, trilha: meio.slice(fa - fj) });
106
+ }
107
+ }
108
+ return out;
109
+ }
110
+
111
+ module.exports = { plano, segurar, mediana };