primocode 10.0.9 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/primocode.js +19 -2
- package/lib/act.js +5 -2
- package/lib/catalogo.js +5 -2
- package/lib/config.js +10 -2
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +37 -17
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/estilo.js +24 -8
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +21 -3
- package/lib/estudio/hibrido.js +16 -12
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +134 -23
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +15 -5
- package/lib/estudio/kit/index.jsx +2 -1
- package/lib/estudio/kit/tres.jsx +88 -21
- package/lib/estudio/kit.js +3 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +3 -4
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/remotion.js +51 -6
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +80 -2
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +15 -1
- package/lib/tools.js +31 -5
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/voz_kokoro.py +98 -0
package/lib/estudio/kit/tres.jsx
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* (30/09), com "sem referências": nada de foto, então o produto é modelado
|
|
5
5
|
* aqui, com as peças do three.js, e iluminado como em estúdio.
|
|
6
6
|
*
|
|
7
|
-
* {"tipo":"kit","peca":"produto3d","modelo":"fone"|"celular"|"lata"|"caixa",
|
|
7
|
+
* {"tipo":"kit","peca":"produto3d","modelo":"fone"|"celular"|"lata"|"caixa"|"notebook"|"garrafa"|"relogio",
|
|
8
8
|
* "cor":"#1b1c20","corAcento":"#ff5a1f","t":0,"giro":0.35,
|
|
9
|
+
* "acabamento":"metal","camera":"alto","luz":"recorte","ritmo":"pendulo", (variacao3d.js)
|
|
9
10
|
* "x":0,"y":0,"largura":1920,"altura":1080}
|
|
10
11
|
*
|
|
11
12
|
* O movimento: entra pequeno e girado e assenta com mola (0,9 s), depois gira
|
|
@@ -33,19 +34,34 @@ function Ambiente({ forca = 1 }) {
|
|
|
33
34
|
|
|
34
35
|
const saida = (x) => 1 - Math.pow(1 - Math.max(0, Math.min(1, x)), 3);
|
|
35
36
|
|
|
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}
|
|
37
|
+
function Material({ cor, brilho = 0.35, metal = 0.25, verniz = 0.8, iris = 0, emissivo }) {
|
|
38
|
+
return <meshPhysicalMaterial color={cor} roughness={brilho} metalness={metal} clearcoat={verniz} clearcoatRoughness={0.25} iridescence={iris}
|
|
38
39
|
{...(emissivo ? { emissive: emissivo, emissiveIntensity: 1.2 } : {})} />;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
// O fone: o arco, os deslizadores, as conchas com o anel de acento, as almofadas e o logo aceso.
|
|
42
|
-
|
|
43
|
+
/* AS VARIAÇÕES (variacao3d.js sorteia os nomes): o acabamento do corpo, o
|
|
44
|
+
enquadramento, a luz e o ritmo. Sem nome, o de sempre. */
|
|
45
|
+
const ACABAMENTOS = {
|
|
46
|
+
verniz: { brilho: 0.3, metal: 0.3, verniz: 0.8 }, metal: { brilho: 0.18, metal: 0.92, verniz: 0.3 },
|
|
47
|
+
fosco: { brilho: 0.78, metal: 0, verniz: 0 }, perola: { brilho: 0.25, metal: 0.1, verniz: 1, iris: 1 },
|
|
48
|
+
acetinado: { brilho: 0.5, metal: 0.45, verniz: 0.4 },
|
|
49
|
+
};
|
|
50
|
+
const CAMERAS = {
|
|
51
|
+
frente: {}, alto: { y: 1.5, inclina: 0.12 }, baixo: { y: -0.6, inclina: -0.1 },
|
|
52
|
+
perfil: { giro0: -1.7, roll: 0.07 }, perto: { fov: 25 },
|
|
53
|
+
};
|
|
54
|
+
// [chave, preenchimento, recorte na cor de acento, ambiente]
|
|
55
|
+
const LUZES = { estudio: [3.2, 1.1, 3.2, 0.35], recorte: [1.6, 0.4, 5.5, 0.15], suave: [2.2, 2.0, 1.4, 0.7], lateral: [4.2, 0.2, 2.6, 0.2] };
|
|
56
|
+
const RITMOS = { lento: 0.18, medio: 0.35, rapido: 0.6 };
|
|
57
|
+
|
|
58
|
+
function Fone({ cor, acento, fin }) {
|
|
43
59
|
const R = 1.05;
|
|
44
60
|
const concha = (lado) => (
|
|
45
61
|
<group position={[lado * R, -0.12, 0]}>
|
|
46
62
|
<mesh rotation={[0, 0, Math.PI / 2]}>
|
|
47
63
|
<cylinderGeometry args={[0.44, 0.46, 0.26, 64]} />
|
|
48
|
-
<Material cor={cor} brilho={0.3} metal={0.35} />
|
|
64
|
+
<Material cor={cor} brilho={0.3} metal={0.35} {...fin} />
|
|
49
65
|
</mesh>
|
|
50
66
|
<mesh position={[lado * 0.135, 0, 0]} rotation={[0, Math.PI / 2, 0]}>
|
|
51
67
|
<torusGeometry args={[0.43, 0.035, 24, 96]} />
|
|
@@ -65,7 +81,7 @@ function Fone({ cor, acento }) {
|
|
|
65
81
|
<group>
|
|
66
82
|
<mesh>
|
|
67
83
|
<torusGeometry args={[R, 0.075, 32, 160, Math.PI]} />
|
|
68
|
-
<Material cor={cor} brilho={0.28} metal={0.4} />
|
|
84
|
+
<Material cor={cor} brilho={0.28} metal={0.4} {...fin} />
|
|
69
85
|
</mesh>
|
|
70
86
|
<mesh position={[0, -0.06, 0]} scale={[1, 1, 1.6]}>
|
|
71
87
|
<torusGeometry args={[R - 0.07, 0.05, 24, 140, Math.PI]} />
|
|
@@ -83,10 +99,10 @@ function Fone({ cor, acento }) {
|
|
|
83
99
|
);
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
function Celular({ cor, acento }) {
|
|
102
|
+
function Celular({ cor, acento, fin }) {
|
|
87
103
|
return (
|
|
88
104
|
<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>
|
|
105
|
+
<mesh><boxGeometry args={[1.0, 2.05, 0.1]} /><Material cor={cor} brilho={0.22} metal={0.7} {...fin} /></mesh>
|
|
90
106
|
<mesh position={[0, 0, 0.052]}><planeGeometry args={[0.92, 1.96]} /><Material cor="#0b0c10" brilho={0.1} metal={0} emissivo="#0d1830" /></mesh>
|
|
91
107
|
<mesh position={[0, 0.3, 0.054]}><circleGeometry args={[0.22, 48]} /><Material cor={acento} emissivo={acento} /></mesh>
|
|
92
108
|
<mesh position={[0.26, 0.78, -0.07]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.14, 0.14, 0.05, 48]} /><Material cor="#222" brilho={0.1} metal={0.9} /></mesh>
|
|
@@ -94,26 +110,72 @@ function Celular({ cor, acento }) {
|
|
|
94
110
|
);
|
|
95
111
|
}
|
|
96
112
|
|
|
97
|
-
function Lata({ cor, acento }) {
|
|
113
|
+
function Lata({ cor, acento, fin }) {
|
|
98
114
|
return (
|
|
99
115
|
<group>
|
|
100
|
-
<mesh><cylinderGeometry args={[0.55, 0.55, 1.9, 96]} /><Material cor={cor} brilho={0.2} metal={0.85} /></mesh>
|
|
116
|
+
<mesh><cylinderGeometry args={[0.55, 0.55, 1.9, 96]} /><Material cor={cor} brilho={0.2} metal={0.85} {...fin} /></mesh>
|
|
101
117
|
<mesh position={[0, 0.2, 0]}><cylinderGeometry args={[0.556, 0.556, 0.5, 96, 1, true]} /><Material cor={acento} brilho={0.3} metal={0.3} /></mesh>
|
|
102
118
|
<mesh position={[0, 0.97, 0]}><cylinderGeometry args={[0.5, 0.55, 0.06, 96]} /><Material cor="#c9ccd2" brilho={0.25} metal={1} /></mesh>
|
|
103
119
|
</group>
|
|
104
120
|
);
|
|
105
121
|
}
|
|
106
122
|
|
|
107
|
-
function Caixa({ cor, acento }) {
|
|
123
|
+
function Caixa({ cor, acento, fin }) {
|
|
108
124
|
return (
|
|
109
125
|
<group>
|
|
110
|
-
<mesh><boxGeometry args={[1.5, 1.5, 1.5]} /><Material cor={cor} brilho={0.3} metal={0.3} /></mesh>
|
|
126
|
+
<mesh><boxGeometry args={[1.5, 1.5, 1.5]} /><Material cor={cor} brilho={0.3} metal={0.3} {...fin} /></mesh>
|
|
111
127
|
<mesh position={[0, 0, 0.752]}><planeGeometry args={[1.0, 0.18]} /><Material cor={acento} emissivo={acento} /></mesh>
|
|
112
128
|
</group>
|
|
113
129
|
);
|
|
114
130
|
}
|
|
115
131
|
|
|
116
|
-
|
|
132
|
+
// O notebook: a base, o teclado escuro, a tampa aberta a ~105° e a tela acesa.
|
|
133
|
+
function Notebook({ cor, acento, fin }) {
|
|
134
|
+
return (
|
|
135
|
+
<group position={[0, -0.3, 0.1]} rotation={[0.1, 0, 0]} scale={[0.7, 0.7, 0.7]}>{/* a 1,0 a tela saía cortada no topo (05/10) */}
|
|
136
|
+
<mesh><boxGeometry args={[2.2, 0.08, 1.45]} /><Material cor={cor} brilho={0.25} metal={0.75} {...fin} /></mesh>
|
|
137
|
+
<mesh position={[0, 0.042, -0.12]}><boxGeometry args={[1.9, 0.01, 0.75]} /><Material cor="#141416" brilho={0.8} metal={0} verniz={0} /></mesh>
|
|
138
|
+
<mesh position={[0, 0.042, 0.45]}><boxGeometry args={[0.7, 0.01, 0.38]} /><Material cor="#26272c" brilho={0.4} metal={0.2} /></mesh>
|
|
139
|
+
<group position={[0, 0.04, -0.72]} rotation={[-0.26, 0, 0]}>
|
|
140
|
+
<mesh position={[0, 0.72, 0]}><boxGeometry args={[2.2, 1.44, 0.06]} /><Material cor={cor} brilho={0.25} metal={0.75} {...fin} /></mesh>
|
|
141
|
+
<mesh position={[0, 0.74, 0.032]}><planeGeometry args={[2.0, 1.25]} /><Material cor="#0b0c10" brilho={0.1} metal={0} emissivo="#0d1830" /></mesh>
|
|
142
|
+
<mesh position={[0, 0.74, 0.034]}><circleGeometry args={[0.2, 48]} /><Material cor={acento} emissivo={acento} /></mesh>
|
|
143
|
+
</group>
|
|
144
|
+
</group>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A garrafa: o corpo, o ombro, o gargalo, a tampa e o rótulo na cor de acento.
|
|
149
|
+
function Garrafa({ cor, acento, fin }) {
|
|
150
|
+
return (
|
|
151
|
+
<group position={[0, -0.15, 0]}>
|
|
152
|
+
<mesh><cylinderGeometry args={[0.5, 0.5, 1.5, 96]} /><Material cor={cor} brilho={0.15} metal={0.6} {...fin} /></mesh>
|
|
153
|
+
<mesh position={[0, 0.95, 0]}><cylinderGeometry args={[0.2, 0.5, 0.4, 96]} /><Material cor={cor} brilho={0.15} metal={0.6} {...fin} /></mesh>
|
|
154
|
+
<mesh position={[0, 1.3, 0]}><cylinderGeometry args={[0.2, 0.2, 0.3, 64]} /><Material cor={cor} brilho={0.15} metal={0.6} {...fin} /></mesh>
|
|
155
|
+
<mesh position={[0, 1.52, 0]}><cylinderGeometry args={[0.23, 0.23, 0.18, 64]} /><Material cor={acento} brilho={0.3} metal={0.8} /></mesh>
|
|
156
|
+
<mesh position={[0, 0.05, 0]}><cylinderGeometry args={[0.506, 0.506, 0.55, 96, 1, true]} /><Material cor={acento} brilho={0.5} metal={0.1} verniz={0.3} /></mesh>
|
|
157
|
+
</group>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// O relógio: a caixa redonda, o aro de acento, o mostrador aceso e a pulseira.
|
|
162
|
+
function Relogio({ cor, acento, fin }) {
|
|
163
|
+
return (
|
|
164
|
+
<group>
|
|
165
|
+
<mesh rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.62, 0.62, 0.22, 96]} /><Material cor={cor} brilho={0.2} metal={0.85} {...fin} /></mesh>
|
|
166
|
+
<mesh position={[0, 0, 0.11]}><torusGeometry args={[0.6, 0.045, 24, 96]} /><Material cor={acento} brilho={0.15} metal={0.95} verniz={1} /></mesh>
|
|
167
|
+
<mesh position={[0, 0, 0.112]}><circleGeometry args={[0.55, 64]} /><Material cor="#0b0c10" brilho={0.1} metal={0} emissivo="#0d1830" /></mesh>
|
|
168
|
+
<mesh position={[0, 0.16, 0.116]}><boxGeometry args={[0.04, 0.32, 0.01]} /><Material cor={acento} emissivo={acento} /></mesh>
|
|
169
|
+
<mesh position={[0.1, 0, 0.118]} rotation={[0, 0, -Math.PI / 2]}><boxGeometry args={[0.05, 0.22, 0.01]} /><Material cor="#f4f4f6" emissivo="#9aa0aa" /></mesh>
|
|
170
|
+
<mesh position={[0.66, 0, 0]} rotation={[Math.PI / 2, 0, 0]}><cylinderGeometry args={[0.06, 0.06, 0.14, 24]} /><Material cor={cor} brilho={0.2} metal={0.85} {...fin} /></mesh>
|
|
171
|
+
{[-1, 1].map((l) => <mesh key={l} position={[0, l * 1.0, -0.02]}><boxGeometry args={[0.6, 0.85, 0.08]} /><Material cor="#1a1a1d" brilho={0.7} metal={0} verniz={0.2} /></mesh>)}
|
|
172
|
+
</group>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const MODELOS = { fone: Fone, headset: Fone, celular: Celular, phone: Celular, lata: Lata, garrafa: Garrafa, caixa: Caixa,
|
|
177
|
+
notebook: Notebook, laptop: Notebook, relogio: Relogio, watch: Relogio };
|
|
178
|
+
export const MODELOS_3D = Object.keys(MODELOS);
|
|
117
179
|
|
|
118
180
|
export function Produto3D({ el, t }) {
|
|
119
181
|
const W = el.largura || 1920, H = el.altura || 1080;
|
|
@@ -123,21 +185,26 @@ export function Produto3D({ el, t }) {
|
|
|
123
185
|
const Modelo = MODELOS[el.modelo] || Caixa;
|
|
124
186
|
const cor = el.cor || '#1b1c20';
|
|
125
187
|
const acento = el.corAcento || '#ff5a1f';
|
|
126
|
-
const
|
|
188
|
+
const cam = CAMERAS[el.camera] || {};
|
|
189
|
+
const luz = LUZES[el.luz] || LUZES.estudio;
|
|
190
|
+
const fin = ACABAMENTOS[el.acabamento];
|
|
191
|
+
// O ritmo: o giro contínuo em três velocidades, ou o pêndulo, que vai e volta mostrando a frente.
|
|
192
|
+
const anda = el.ritmo === 'pendulo' ? Math.sin(tl * 0.9) * 0.7 : tl * (RITMOS[el.ritmo] ?? el.giro ?? 0.35);
|
|
193
|
+
const giro = (el.giro0 ?? cam.giro0 ?? -0.9) + (1 - k) * 1.4 + anda;
|
|
127
194
|
const escala = (el.escala || 1) * (0.72 + 0.28 * k);
|
|
128
195
|
const flutua = Math.sin(tl * 1.3) * 0.05;
|
|
129
196
|
return (
|
|
130
197
|
<div style={{ position: 'absolute', left: el.x || 0, top: el.y || 0, width: W, height: H, opacity: t >= t0 ? Math.min(1, tl / 0.25) : 0 }}>
|
|
131
|
-
<ThreeCanvas width={W} height={H} camera={{ position: [0, 0.35, 4.4], fov: el.fov || 32 }} gl={{ antialias: true, preserveDrawingBuffer: true }}>
|
|
198
|
+
<ThreeCanvas width={W} height={H} camera={{ position: [0, cam.y ?? 0.35, 4.4], fov: el.fov || cam.fov || 32 }} gl={{ antialias: true, preserveDrawingBuffer: true }}>
|
|
132
199
|
<Ambiente forca={0.8} />
|
|
133
200
|
<hemisphereLight args={['#ffffff', '#202020', 0.5]} />
|
|
134
|
-
<ambientLight intensity={
|
|
135
|
-
<directionalLight position={[3, 4, 5]} intensity={
|
|
136
|
-
<directionalLight position={[-2, 3, 4]} intensity={1
|
|
137
|
-
<directionalLight position={[-4, 1.5, -3]} intensity={
|
|
201
|
+
<ambientLight intensity={luz[3]} />
|
|
202
|
+
<directionalLight position={el.luz === 'lateral' ? [5, 1.5, 1.5] : [3, 4, 5]} intensity={luz[0]} />
|
|
203
|
+
<directionalLight position={[-2, 3, 4]} intensity={luz[1]} />
|
|
204
|
+
<directionalLight position={[-4, 1.5, -3]} intensity={luz[2]} color={acento} />
|
|
138
205
|
<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} />
|
|
206
|
+
<group position={[0, flutua - 0.05, 0]} rotation={[0.18 + (cam.inclina || 0) + (1 - k) * 0.3, giro, cam.roll || 0]} scale={[escala, escala, escala]}>
|
|
207
|
+
<Modelo cor={cor} acento={acento} fin={fin} />
|
|
141
208
|
</group>
|
|
142
209
|
{/* O chão. O gerativo dispensa (semChao): o disco virava uma faixa escura atrás do texto. */}
|
|
143
210
|
{!el.semChao && (
|
package/lib/estudio/kit.js
CHANGED
|
@@ -55,9 +55,11 @@ const PECAS = {
|
|
|
55
55
|
icone3d: { precisa: ['icone'] },
|
|
56
56
|
// O motion gerativo (kit/gerativo.jsx): a partitura inteira, numa peça só (gerativo.js).
|
|
57
57
|
gerativo: { precisa: ['partitura'] },
|
|
58
|
+
// O estilo fluido (kit/fluido.jsx): a forma que vira a próxima, numa peça só (fluido.js).
|
|
59
|
+
fluido: { precisa: ['partitura'] },
|
|
58
60
|
};
|
|
59
61
|
|
|
60
|
-
const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'index.jsx'];
|
|
62
|
+
const ARQUIVOS = ['base.jsx', 'vox.jsx', 'marca.jsx', 'tres.jsx', 'gerativo.jsx', 'fluido.jsx', 'index.jsx'];
|
|
61
63
|
|
|
62
64
|
/** Os arquivos do kit, para o projeto Remotion: [{nome: 'kit/base.jsx', conteudo}]. */
|
|
63
65
|
function arquivos() {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* A VOZ LOCAL (Kokoro-82M): roda na CPU desta máquina, sem cota e sem
|
|
2
|
+
servidor. Entra quando o Gemini falha (cota, voz cortada) ou quando o
|
|
3
|
+
roteiro pede `voz: 'kokoro'` (ou PRIMOCODE_VOZ=kokoro). Sem `uv` ou sem
|
|
4
|
+
espeak-ng ela diz que não está disponível e a corrente segue — nunca
|
|
5
|
+
derruba a narração.
|
|
6
|
+
|
|
7
|
+
Provado nesta máquina em 05/10: 11 s de áudio em 3,5 s (+4,3 s de carga
|
|
8
|
+
do modelo), sem o corte no fim que o Gemini às vezes deixa. */
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { spawn, execFileSync } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const SCRIPT = path.join(__dirname, '..', '..', 'studio', 'voz_kokoro.py');
|
|
14
|
+
// Sem `transformers>=4.45` o uv escolhe a 4.12 e o tokenizers não compila (05/10).
|
|
15
|
+
const DEPS = ['kokoro>=0.9.4', 'transformers>=4.45', 'soundfile', 'misaki[en]'];
|
|
16
|
+
// As vozes do Studio viram as do Kokoro em português: pf_dora (feminina), pm_alex (masculina).
|
|
17
|
+
const VOZES = { thalita: 'pf_dora', francisca: 'pf_dora', feminina: 'pf_dora', antonio: 'pm_alex', masculina: 'pm_alex' };
|
|
18
|
+
|
|
19
|
+
/** O roteiro (ou quem usa) pediu a voz local? */
|
|
20
|
+
function querKokoro(voz, env = process.env) {
|
|
21
|
+
return String(voz || '').toLowerCase() === 'kokoro' || String(env.PRIMOCODE_VOZ || '').toLowerCase() === 'kokoro';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** "PrimoCode" sai "Primo Kodi" no Kokoro; escrito assim, sai certo. */
|
|
25
|
+
function pronuncia(texto) {
|
|
26
|
+
return String(texto || '').replace(/primo\s*code/gi, 'Primo Côude');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function vozKokoro(voz) {
|
|
30
|
+
const v = String(voz || '').toLowerCase();
|
|
31
|
+
if (/^p[fm]_/.test(v)) return v;
|
|
32
|
+
return VOZES[v] || 'pm_alex';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Onde está o espeak-ng (o Kokoro precisa dele para o português). */
|
|
36
|
+
function acharEspeak({ existe = fs.existsSync, brew = () => execFileSync('brew', ['--prefix', 'espeak-ng'], { stdio: ['ignore', 'pipe', 'ignore'], timeout: 10000 }).toString().trim() } = {}) {
|
|
37
|
+
const bases = [];
|
|
38
|
+
try { bases.push(brew()); } catch { /* sem brew */ }
|
|
39
|
+
bases.push('/opt/homebrew/opt/espeak-ng', '/usr/local/opt/espeak-ng', '/usr');
|
|
40
|
+
for (const b of bases) {
|
|
41
|
+
if (!b) continue;
|
|
42
|
+
const dados = path.join(b, 'share', 'espeak-ng-data');
|
|
43
|
+
const lib = ['lib/libespeak-ng.dylib', 'lib/libespeak-ng.so.1', 'lib/x86_64-linux-gnu/libespeak-ng.so.1', 'lib/aarch64-linux-gnu/libespeak-ng.so.1']
|
|
44
|
+
.map((l) => path.join(b, l)).find(existe);
|
|
45
|
+
if (existe(dados) && lib) return { dados, lib };
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** O comando inteiro (uv + deps fixadas + env do espeak), ou null se falta peça. */
|
|
51
|
+
function comando({ uv, espeak, arquivo, saida, voz, env = process.env }) {
|
|
52
|
+
if (!uv || !espeak) return null;
|
|
53
|
+
const args = ['run', '--quiet', '--python', '3.11'];
|
|
54
|
+
for (const d of DEPS) args.push('--with', d);
|
|
55
|
+
args.push('python', SCRIPT, 'roteiro', '--arquivo', arquivo, '--saida', saida, '--voz', vozKokoro(voz));
|
|
56
|
+
return { cmd: uv, args, env: { ...env, ESPEAK_DATA_PATH: espeak.dados, PHONEMIZER_ESPEAK_LIBRARY: espeak.lib } };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rodar({ cmd, args, env }, timeout) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const p = spawn(cmd, args, { env });
|
|
62
|
+
let saida = '', erro = '';
|
|
63
|
+
p.stdout.on('data', (d) => { saida += d.toString(); });
|
|
64
|
+
p.stderr.on('data', (d) => { erro += d.toString(); });
|
|
65
|
+
const relogio = setTimeout(() => { try { p.kill(); } catch {} }, timeout);
|
|
66
|
+
p.on('close', () => {
|
|
67
|
+
clearTimeout(relogio);
|
|
68
|
+
try { resolve(JSON.parse(saida.trim().split('\n').pop())); }
|
|
69
|
+
catch { resolve({ ok: false, error: (erro || saida).slice(-400).trim() || 'o kokoro não respondeu' }); }
|
|
70
|
+
});
|
|
71
|
+
p.on('error', (e) => { clearTimeout(relogio); resolve({ ok: false, error: e.message }); });
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Narra a lista [{texto}] em mp3 com o Kokoro. Devolve o mesmo formato do narrar.py. */
|
|
76
|
+
async function narrar(lista, mp3, { voz, uv, espeak = acharEspeak(), executar = rodar, timeout = 15 * 60000 } = {}) {
|
|
77
|
+
const arquivo = mp3.replace(/\.mp3$/, '') + '.kokoro.json';
|
|
78
|
+
const c = comando({ uv, espeak, arquivo, saida: mp3, voz });
|
|
79
|
+
if (!c) return { ok: false, indisponivel: true, error: !uv ? 'kokoro: falta o uv' : 'kokoro: falta o espeak-ng (brew install espeak-ng)' };
|
|
80
|
+
try {
|
|
81
|
+
fs.writeFileSync(arquivo, JSON.stringify(lista.map((x) => ({ ...x, texto: pronuncia(x.texto) }))));
|
|
82
|
+
return await executar(c, timeout);
|
|
83
|
+
} catch (e) {
|
|
84
|
+
return { ok: false, error: e.message };
|
|
85
|
+
} finally {
|
|
86
|
+
try { fs.unlinkSync(arquivo); } catch { /* fica */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** A ordem das vozes: pedida → Kokoro e depois o Edge; senão Gemini → Kokoro → Edge.
|
|
91
|
+
`motorVoz: 'edge'` no roteiro pula as outras. */
|
|
92
|
+
function ordem(voz, env = process.env, { soEdge = false } = {}) {
|
|
93
|
+
if (soEdge) return ['edge'];
|
|
94
|
+
return querKokoro(voz, env) ? ['kokoro', 'edge'] : ['gemini', 'kokoro', 'edge'];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Roda a corrente: a primeira voz que der certo ganha; a falha de uma nunca derruba. */
|
|
98
|
+
async function corrente(passos, provedores) {
|
|
99
|
+
let ultimo = { ok: false, error: 'nenhuma voz disponível' };
|
|
100
|
+
for (const nome of passos) {
|
|
101
|
+
const f = provedores[nome];
|
|
102
|
+
if (!f) continue;
|
|
103
|
+
let r;
|
|
104
|
+
try { r = await f(); } catch (e) { r = { ok: false, error: e.message }; }
|
|
105
|
+
if (r && r.ok) return r;
|
|
106
|
+
if (r) ultimo = r;
|
|
107
|
+
}
|
|
108
|
+
return ultimo;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { querKokoro, pronuncia, vozKokoro, acharEspeak, comando, narrar, ordem, corrente, DEPS, SCRIPT };
|
|
@@ -29,6 +29,7 @@ const { caber } = require('./composicao.js');
|
|
|
29
29
|
const { misturarCor, ehCor } = require('./morfar.js');
|
|
30
30
|
const { tempoDasPartes, gatilhos, volumesDaTrilha, r2 } = require('./ritmo.js');
|
|
31
31
|
const { tese, termoDef } = require('./vox.js');
|
|
32
|
+
const { modeloDoPedido } = require('./variacao3d.js');
|
|
32
33
|
|
|
33
34
|
const FPS = 30;
|
|
34
35
|
const k = (t, v, e) => (e ? { t: r2(t), v, e } : { t: r2(t), v });
|
|
@@ -328,9 +329,7 @@ function montarLancamento({ roteiro, variante = 'lancamento', largura = 1920, al
|
|
|
328
329
|
// ── as peças que qualquer parte pode pedir pelo nome ("visual") ────
|
|
329
330
|
/* Qual modelo 3D: o que a parte ou o roteiro disser, ou o que o texto
|
|
330
331
|
deixa claro. Sem pista, a caixa — um produto genérico que ainda gira. */
|
|
331
|
-
const modeloDe =
|
|
332
|
-
: /celular|smartphone|iphone|phone|telefone/i.test(txt) ? 'celular'
|
|
333
|
-
: /lata|bebida|refri|cerveja|energ[eé]tico|garrafa/i.test(txt) ? 'lata' : 'caixa');
|
|
332
|
+
const modeloDe = modeloDoPedido;
|
|
334
333
|
const VISUAL = {
|
|
335
334
|
/* O PRODUTO EM 3D (kit/tres.jsx): modelado e iluminado aqui, sem foto
|
|
336
335
|
de fora — girando devagar no centro, o nome embaixo. */
|
|
@@ -340,7 +339,7 @@ function montarLancamento({ roteiro, variante = 'lancamento', largura = 1920, al
|
|
|
340
339
|
const f = fit(p.titulo || nome, W - 2 * M, 1, deitado ? 84 : 96);
|
|
341
340
|
const yT = deitado ? H * 0.8 : H * 0.76;
|
|
342
341
|
const els = [
|
|
343
|
-
{ tipo: 'kit', peca: 'produto3d', modelo, cor: p.cor3d || roteiro.cor3d || '#1b1c20', corAcento: ACENTO, t: 0, giro: 0.4,
|
|
342
|
+
{ tipo: 'kit', peca: 'produto3d', modelo, cor: p.cor3d || roteiro.cor3d || '#1b1c20', corAcento: ACENTO, t: 0, giro: 0.4, ...(roteiro.variacao3d || {}),
|
|
344
343
|
x: 0, y: r2(deitado ? -H * 0.02 : -H * 0.08), largura: W, altura: H, escala: deitado ? 0.8 : 1.15 }, // deitado a 1,0 o arco saía cortado no topo
|
|
345
344
|
{ tipo: 'kit', peca: 'manchete', modo: 'desfoque', texto: p.titulo || nome, x: W / 2, y: r2(yT), largura: W - 2 * M, alinhamento: 'center',
|
|
346
345
|
tamanho: f.tamanho, peso: 800, cor: '#f4f4f6', t: Math.min(0.9, gatilhos([p.titulo || nome], tp, t0, d)[0] || 0.6), ...base },
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* legibilidade.js — a lei da legibilidade do /brag, em número (comum/desmontados/
|
|
3
|
+
* latent-spaces-brag.md): rótulo curto fica PARADO ~0,8 s; frase, 0,3 s por
|
|
4
|
+
* palavra; "fast-in, then hold — never fast-in, then gone".
|
|
5
|
+
*
|
|
6
|
+
* "Parado" é o tempo entre o fim da entrada e o começo da saída. Quando a cena
|
|
7
|
+
* é curta, quem encolhe é a animação (entrada e saída), nunca a leitura.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const nPalavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
|
|
13
|
+
|
|
14
|
+
/** Quanto tempo o texto precisa ficar parado na tela: max(0,8 s; 0,3 s por palavra). */
|
|
15
|
+
const minimoLeitura = (t) => Math.max(0.8, 0.3 * nPalavras(t));
|
|
16
|
+
|
|
17
|
+
/** O que sobra para a animação depois da leitura, entre um piso e o tempo cheio. */
|
|
18
|
+
const sobra = (vida, leitura, piso, cheio) => Math.min(cheio, Math.max(piso, vida - leitura));
|
|
19
|
+
|
|
20
|
+
module.exports = { minimoLeitura, nPalavras, sobra };
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -279,14 +279,20 @@ function Elemento({ el, t, dados }) {
|
|
|
279
279
|
if (Array.isArray(el.revelar) && typeof el.texto === 'string') {
|
|
280
280
|
const palavras = el.texto.split(' ');
|
|
281
281
|
conteudo = palavras.map((w, i) => {
|
|
282
|
-
const
|
|
283
|
-
|
|
282
|
+
const marcada = !!(el.destaques && el.destaques.includes(i));
|
|
283
|
+
/* A PALAVRA DE DESTAQUE: com "fonteDestaque", o acento é a serifa em
|
|
284
|
+
itálico (na cor do texto), que chega um instante depois das outras.
|
|
285
|
+
Um acento só — nada de caixa de cor atrás (a "cara de IA", 05/10). */
|
|
286
|
+
const serifa = marcada && el.fonteDestaque;
|
|
287
|
+
const t0 = (el.revelar[Math.min(i, el.revelar.length - 1)] ?? 0) + (serifa ? 0.12 : 0);
|
|
288
|
+
const k = Math.min(1, Math.max(0, (tLocal - t0) / (serifa ? 0.45 : 0.28)));
|
|
284
289
|
const e = CURVAS.saida(k);
|
|
285
290
|
// O espaço fica FORA do span: é ali que a linha pode quebrar.
|
|
286
291
|
return (
|
|
287
292
|
<React.Fragment key={i}>
|
|
288
293
|
<span style={{ display: 'inline-block', opacity: e, transform: 'translateY(' + (1 - e) * 0.35 + 'em)',
|
|
289
|
-
color:
|
|
294
|
+
color: marcada && !serifa ? (el.corDestaque || el.cor) : undefined,
|
|
295
|
+
...(serifa ? { fontFamily: '"' + el.fonteDestaque + '", Georgia, serif', fontStyle: 'italic', fontWeight: 400, fontSize: '1.1em', letterSpacing: 0 } : {}) }}>{w}</span>
|
|
290
296
|
{i < palavras.length - 1 ? ' ' : null}
|
|
291
297
|
</React.Fragment>
|
|
292
298
|
);
|
|
@@ -465,12 +471,14 @@ function Elemento({ el, t, dados }) {
|
|
|
465
471
|
display: 'inline-block',
|
|
466
472
|
// A palavra forte é MAIOR na fonte, não na escala: escala não
|
|
467
473
|
// abre espaço, e ela cresceria por cima da vizinha.
|
|
468
|
-
fontSize: p.forte ? '1.22em' : undefined,
|
|
474
|
+
fontSize: p.forte ? (e.fonteForte ? '1.1em' : '1.22em') : undefined,
|
|
475
|
+
// Com "fonteForte", a palavra forte é a serifa em itálico, não o letreiro maior.
|
|
476
|
+
...(p.forte && e.fonteForte ? { fontFamily: '"' + e.fonteForte + '", Georgia, serif', fontStyle: 'italic', fontWeight: 400, textTransform: 'none' } : {}),
|
|
469
477
|
transform: 'scale(' + salto + ') translateY(' + (1 - entra) * 18 + 'px)',
|
|
470
478
|
opacity: entra,
|
|
471
479
|
color: cor,
|
|
472
480
|
textShadow: e.sombra || '0 6px 22px rgba(0,0,0,.6), 0 2px 0 rgba(0,0,0,.35)',
|
|
473
|
-
WebkitTextStroke: e.contorno || '2px rgba(0,0,0,.55)',
|
|
481
|
+
WebkitTextStroke: p.forte && e.fonteForte ? undefined : (e.contorno || '2px rgba(0,0,0,.55)'),
|
|
474
482
|
paintOrder: 'stroke fill',
|
|
475
483
|
background: falando && e.fundoAtivo ? e.fundoAtivo : undefined,
|
|
476
484
|
borderRadius: '0.18em', padding: falando && e.fundoAtivo ? '0 0.12em' : undefined,
|
|
@@ -1005,4 +1013,41 @@ function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
|
|
|
1005
1013
|
});
|
|
1006
1014
|
}
|
|
1007
1015
|
|
|
1008
|
-
|
|
1016
|
+
/* ── ABRIR PARA EDITAR ────────────────────────────────────────────────
|
|
1017
|
+
"Quando eu falar pra editar, edita no editor de vídeo open source." O
|
|
1018
|
+
projeto já é Remotion: editar é abrir o Remotion Studio na pasta dele. Ele
|
|
1019
|
+
sobe solto (o PrimoCode não espera) e abre o navegador sozinho. */
|
|
1020
|
+
function comandoStudio(raiz) {
|
|
1021
|
+
return { cmd: 'npx', args: ['remotion', 'studio'], cwd: raiz };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function abrirStudio({ raiz, spawnar = spawn } = {}) {
|
|
1025
|
+
if (!raiz || !fs.existsSync(path.join(raiz, 'package.json'))) {
|
|
1026
|
+
return { ok: false, error: 'não achei o projeto Remotion nesta pasta.' };
|
|
1027
|
+
}
|
|
1028
|
+
// O Studio precisa do Remotion instalado. A base compartilhada já vem do primeiro render.
|
|
1029
|
+
if (!fs.existsSync(path.join(raiz, 'node_modules'))) {
|
|
1030
|
+
const comp = process.platform === 'win32' ? null : baseCompartilhada(raiz);
|
|
1031
|
+
if (comp && comp.pronta) {
|
|
1032
|
+
try { fs.symlinkSync(path.join(comp.base, 'node_modules'), path.join(raiz, 'node_modules'), 'dir'); } catch { /* segue */ }
|
|
1033
|
+
}
|
|
1034
|
+
if (!fs.existsSync(path.join(raiz, 'node_modules'))) {
|
|
1035
|
+
return { ok: false, error: 'o Remotion ainda não está instalado para este projeto.',
|
|
1036
|
+
comoResolver: 'renderize uma vez com estudio_render (instala o Remotion), depois abra o editor.' };
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const c = comandoStudio(raiz);
|
|
1040
|
+
const win = process.platform === 'win32';
|
|
1041
|
+
try {
|
|
1042
|
+
const p = spawnar(c.cmd, win ? c.args.map(citarWin) : c.args,
|
|
1043
|
+
{ cwd: c.cwd, detached: true, stdio: 'ignore', shell: win });
|
|
1044
|
+
if (p && p.on) p.on('error', () => { /* sem npx: o caminho vai na resposta */ });
|
|
1045
|
+
if (p && p.unref) p.unref();
|
|
1046
|
+
} catch (e) {
|
|
1047
|
+
return { ok: false, error: e.message };
|
|
1048
|
+
}
|
|
1049
|
+
return { ok: true, raiz, comando: `cd "${raiz}" && npx remotion studio` };
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
module.exports = { montar, renderizar, fontesCSS, VERSAO_REMOTION, CENA_JSX, pkgJson, baseCompartilhada, TSCONFIG,
|
|
1053
|
+
comandoStudio, abrirStudio };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* variacao3d.js — o produto em 3D (kit/tres.jsx) diferente a cada vídeo.
|
|
3
|
+
*
|
|
4
|
+
* "várias opções de elementos 3D... vários estilos... que não sejam padrão
|
|
5
|
+
* da IA" (Joel, 05/10)
|
|
6
|
+
*
|
|
7
|
+
* Antes, todo fone saía igual: o mesmo material, a mesma câmera, a mesma luz
|
|
8
|
+
* e o mesmo giro. Agora cada vídeo sorteia o acabamento, o enquadramento, a
|
|
9
|
+
* luz e o ritmo — sem repetir os últimos (memória '3d' do variedade.js). Os
|
|
10
|
+
* nomes daqui são as chaves que o tres.jsx desenha.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const variedade = require('./variedade.js');
|
|
16
|
+
|
|
17
|
+
const VARIACOES = {
|
|
18
|
+
acabamento: ['verniz', 'metal', 'fosco', 'perola', 'acetinado'],
|
|
19
|
+
camera: ['frente', 'alto', 'baixo', 'perfil', 'perto'],
|
|
20
|
+
luz: ['estudio', 'recorte', 'suave', 'lateral'],
|
|
21
|
+
ritmo: ['lento', 'medio', 'rapido', 'pendulo'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Um 3D por vídeo: cada faceta sorteada sem repetir as últimas. */
|
|
25
|
+
function escolherVariacao3d(aleatorio = Math.random) {
|
|
26
|
+
const v = {};
|
|
27
|
+
for (const [faceta, opcoes] of Object.entries(VARIACOES)) v[faceta] = variedade.sortear(opcoes, '3d', { prefixo: faceta + ':', aleatorio });
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Qual modelo o pedido descreve. Sem pista, a caixa — um produto genérico que ainda gira. */
|
|
32
|
+
const MODELOS3D = [
|
|
33
|
+
['fone', /fone|headset|headphone|auricular/i],
|
|
34
|
+
['celular', /celular|smartphone|iphone|phone|telefone/i],
|
|
35
|
+
['notebook', /notebook|laptop|computador|macbook/i],
|
|
36
|
+
['relogio', /rel[oó]gio|smartwatch|\bwatch\b/i],
|
|
37
|
+
['garrafa', /garrafa|squeeze|bottle|garrafinha|vinho|perfume/i],
|
|
38
|
+
['lata', /lata|bebida|refri|cerveja|energ[eé]tico/i],
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function modeloDoPedido(texto) {
|
|
42
|
+
const achado = MODELOS3D.find(([, re]) => re.test(String(texto || '')));
|
|
43
|
+
return achado ? achado[0] : 'caixa';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { VARIACOES, MODELOS3D, escolherVariacao3d, modeloDoPedido };
|
package/lib/estudio/variedade.js
CHANGED
|
@@ -21,7 +21,7 @@ const os = require('os');
|
|
|
21
21
|
const path = require('path');
|
|
22
22
|
|
|
23
23
|
const PASTA = path.join(os.homedir(), '.primocode', 'studio');
|
|
24
|
-
const MEMORIA = { trilha: 16, sfx: 40 };
|
|
24
|
+
const MEMORIA = { trilha: 16, sfx: 40, broll: 80, icones: 4, estilo: 8, '3d': 24 };
|
|
25
25
|
|
|
26
26
|
function arquivo(tipo) { return path.join(PASTA, `${tipo}-usadas.json`); }
|
|
27
27
|
|
|
@@ -54,6 +54,22 @@ function ordenar(itens, tipo, aleatorio = Math.random) {
|
|
|
54
54
|
return [...embaralhar(validos.filter((x) => !ja.has(x.url)), aleatorio), ...validos.filter((x) => ja.has(x.url))];
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Uma das opções, sem repetir as últimas: as nunca usadas primeiro (sorteadas),
|
|
59
|
+
* depois a usada há mais tempo. Marca a escolhida na memória do tipo. Com
|
|
60
|
+
* `prefixo`, várias listas dividem o mesmo arquivo ('acabamento:fosco').
|
|
61
|
+
*/
|
|
62
|
+
function sortear(opcoes, tipo, { prefixo = '', aleatorio = Math.random } = {}) {
|
|
63
|
+
if (!opcoes || !opcoes.length) return null;
|
|
64
|
+
const ja = usadas(tipo);
|
|
65
|
+
const idade = (o) => ja.indexOf(prefixo + o); // 0 = a última usada
|
|
66
|
+
const novas = embaralhar(opcoes.filter((o) => idade(o) < 0), aleatorio);
|
|
67
|
+
const velhas = opcoes.filter((o) => idade(o) >= 0).sort((a, b) => idade(b) - idade(a));
|
|
68
|
+
const escolha = [...novas, ...velhas][0];
|
|
69
|
+
marcar(tipo, prefixo + escolha);
|
|
70
|
+
return escolha;
|
|
71
|
+
}
|
|
72
|
+
|
|
57
73
|
// ── a trilha: termos por clima ──────────────────────────────────────────
|
|
58
74
|
const CLIMAS = {
|
|
59
75
|
tecnologia: ['electronic instrumental', 'synthwave instrumental', 'future bass instrumental', 'downtempo electronic', 'minimal techno instrumental', 'chillwave instrumental'],
|
|
@@ -102,4 +118,66 @@ function termosSfx(nome, aleatorio = Math.random) {
|
|
|
102
118
|
return embaralhar(SFX[nome] || [nome], aleatorio).slice(0, 3);
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
|
|
121
|
+
// ── o b-roll: o mesmo pedido, de jeitos diferentes ───────────────────────
|
|
122
|
+
/* "não ficar só nas mesmas fontes, mesmos estilos" (Joel, 05/10): o mesmo
|
|
123
|
+
roteiro renderizado duas vezes trocou fonte, cor e música, mas o b-roll
|
|
124
|
+
veio IDÊNTICO — as mesmas mãos no teclado. O termo era o mesmo e o
|
|
125
|
+
primeiro resultado do Pexels também. Agora o termo ganha um jeito de
|
|
126
|
+
filmar sorteado, e o clipe já usado vai para o fim da fila. */
|
|
127
|
+
const JEITOS = ['cinematic', 'slow motion', 'close up', 'natural light', 'detail shot', 'wide shot', 'handheld'];
|
|
128
|
+
|
|
129
|
+
function termosBroll(termo, aleatorio = Math.random) {
|
|
130
|
+
const t = String(termo || '').replace(/\s+/g, ' ').trim();
|
|
131
|
+
if (!t) return [];
|
|
132
|
+
const jeitos = embaralhar(JEITOS.filter((j) => !t.toLowerCase().includes(j)), aleatorio).slice(0, 2);
|
|
133
|
+
return embaralhar([t, ...jeitos.map((j) => `${t} ${j}`)], aleatorio);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Busca com as variações do termo e devolve os itens na ordem de tentar:
|
|
138
|
+
* para no primeiro termo que trouxer algo nunca usado. `buscar(termo)`
|
|
139
|
+
* devolve a lista de itens ({url, …}) — é a busca de verdade, ou um stub.
|
|
140
|
+
*/
|
|
141
|
+
async function buscarSemRepetir(buscar, termo, tipo = 'broll', aleatorio = Math.random) {
|
|
142
|
+
const ja = new Set(usadas(tipo));
|
|
143
|
+
let reserva = [];
|
|
144
|
+
for (const t of termosBroll(termo, aleatorio)) {
|
|
145
|
+
let itens = [];
|
|
146
|
+
try { itens = ordenar(await buscar(t), tipo, aleatorio); } catch { continue; }
|
|
147
|
+
if (itens.length && !ja.has(itens[0].url)) return itens;
|
|
148
|
+
if (!reserva.length) reserva = itens;
|
|
149
|
+
}
|
|
150
|
+
return reserva;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── os ícones: a coleção muda de um vídeo para o outro ──────────────────
|
|
154
|
+
/* O ícone automático (edicao.iconeDe) é sempre "fluent-3d:<emoji>". Estas
|
|
155
|
+
coleções do Iconify usam os mesmos nomes de emoji, então dá para trocar a
|
|
156
|
+
coleção inteira do vídeo; o que não existir na coleção nova fica no 3D. */
|
|
157
|
+
const FAMILIAS_ICONE = ['fluent-3d', 'fluent-emoji-flat', 'fluent-emoji', 'noto', 'twemoji', 'openmoji'];
|
|
158
|
+
|
|
159
|
+
function colecaoIcones(aleatorio = Math.random) {
|
|
160
|
+
return ordenar(FAMILIAS_ICONE.map((url) => ({ url })), 'icones', aleatorio)[0].url;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function cadaIcone(plano, fn) {
|
|
164
|
+
for (const cena of (plano && plano.cenas) || []) for (const el of cena.elementos || []) if (el && el.tipo === 'icone' && el.nome) fn(el);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Os ícones automáticos do plano — os que o roteiro pediu pelo nome ficam como estão. */
|
|
168
|
+
function iconesTrocaveis(plano, explicitos = []) {
|
|
169
|
+
const fixos = new Set(explicitos);
|
|
170
|
+
const nomes = new Set();
|
|
171
|
+
cadaIcone(plano, (el) => { if (/^fluent-3d:/.test(el.nome) && !fixos.has(el.nome)) nomes.add(el.nome); });
|
|
172
|
+
return [...nomes];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Troca no plano cada nome que estiver em `troca` ({velho: novo}). Devolve quantos trocou. */
|
|
176
|
+
function trocarIcones(plano, troca = {}) {
|
|
177
|
+
let n = 0;
|
|
178
|
+
cadaIcone(plano, (el) => { if (troca[el.nome]) { el.nome = troca[el.nome]; n++; } });
|
|
179
|
+
return n;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { usadas, marcar, embaralhar, ordenar, sortear, climaDe, termosTrilha, termosSfx, termosBroll, buscarSemRepetir,
|
|
183
|
+
colecaoIcones, iconesTrocaveis, trocarIcones, CLIMAS, SFX, JEITOS, FAMILIAS_ICONE };
|