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.
- package/README.md +2 -2
- package/bin/primocode.js +69 -1
- package/lib/act.js +118 -19
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +1 -1
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/entrada.js +73 -9
- package/lib/estudio/cinema.js +27 -9
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +23 -8
- package/lib/estudio/gerativo.js +1453 -0
- package/lib/estudio/hibrido.js +165 -0
- package/lib/estudio/index.js +244 -36
- package/lib/estudio/kit/gerativo.jsx +980 -0
- package/lib/estudio/kit/index.jsx +3 -1
- package/lib/estudio/kit/tres.jsx +308 -0
- package/lib/estudio/kit.js +7 -1
- package/lib/estudio/lancamento.js +28 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +53 -9
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variedade.js +105 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/skills.js +36 -0
- package/lib/tools.js +22 -3
- package/lib/ui.js +41 -6
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +1 -1
- package/lib/video/filters.js +39 -6
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +30 -5
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +67 -14
- package/lib/video/texto.js +6 -4
- package/lib/video/timeline.js +9 -1
- package/package.json +2 -2
- package/skills/motion-de-primeira/SKILL.md +5 -4
- package/studio/narrar.py +10 -7
- 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 };
|
package/lib/estudio/kit.js
CHANGED
|
@@ -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 };
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -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, '+')
|
|
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
|
-
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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 };
|
package/lib/estudio/ritmo.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|