primocode 10.0.9 → 10.2.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 +4 -4
- package/bin/primocode.js +25 -28
- package/lib/act.js +5 -2
- package/lib/catalogo.js +7 -4
- package/lib/config.js +10 -2
- package/lib/estudio/assets.js +86 -6
- package/lib/estudio/batida.js +184 -0
- package/lib/estudio/biblioteca.js +214 -0
- package/lib/estudio/cinema.js +37 -17
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/decisao.js +106 -0
- package/lib/estudio/edicao.js +36 -17
- package/lib/estudio/estilo.js +35 -8
- package/lib/estudio/fluido.js +352 -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 +245 -42
- package/lib/estudio/kit/fluido.jsx +614 -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/referencias.js +36 -25
- package/lib/estudio/remotion.js +53 -6
- package/lib/estudio/revisor.js +117 -0
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +140 -8
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +15 -1
- package/lib/tools.js +31 -5
- package/lib/video/filters.js +6 -1
- package/lib/video/ops.js +4 -3
- package/lib/video/timeline.js +1 -1
- package/lib/video/zoom.js +275 -0
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +3 -5
- 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
|
@@ -658,8 +658,8 @@ function Fundo({ P, b, prevB, t, cam, frame, origem }) {
|
|
|
658
658
|
return <div key={'q' + i} style={{ position: 'absolute', left: x, top: y, width: q.r * 2 * sc, height: q.r * 2 * sc, borderRadius: '50%',
|
|
659
659
|
background: i % 5 === 0 ? b.cores.acento : b.cores.texto, opacity: (0.1 + q.z * 0.25), filter: q.z < 0.3 ? 'blur(1.5px)' : undefined }} />;
|
|
660
660
|
})}
|
|
661
|
-
<div style={{ position: 'absolute', inset: 0, ...textura, backgroundSize: passo + 'px ' + passo + 'px', backgroundPosition: px + 'px ' + py + 'px',
|
|
662
|
-
WebkitMaskImage: 'radial-gradient(ellipse at 50% 50%, #000 20%, transparent 78%)', maskImage: 'radial-gradient(ellipse at 50% 50%, #000 20%, transparent 78%)' }} />
|
|
661
|
+
{f.textura && <div style={{ position: 'absolute', inset: 0, ...textura, backgroundSize: passo + 'px ' + passo + 'px', backgroundPosition: px + 'px ' + py + 'px',
|
|
662
|
+
WebkitMaskImage: 'radial-gradient(ellipse at 50% 50%, #000 20%, transparent 78%)', maskImage: 'radial-gradient(ellipse at 50% 50%, #000 20%, transparent 78%)' }} />}
|
|
663
663
|
</div>
|
|
664
664
|
);
|
|
665
665
|
}
|
|
@@ -688,7 +688,7 @@ function Heroi({ b, t, P, slot }) {
|
|
|
688
688
|
const sem = h.semente;
|
|
689
689
|
const cor = h.cor, cor2 = h.cor2;
|
|
690
690
|
const { cx, cy, R, fraco } = slot;
|
|
691
|
-
const op = fraco ? 0.32 : 1;
|
|
691
|
+
const op = slot.op != null ? slot.op : fraco ? 0.32 : 1;
|
|
692
692
|
const els = [];
|
|
693
693
|
if (h.tipo === 'explosao') {
|
|
694
694
|
// A explosão radial: nasce de um ponto, os raios saem em mola e giram devagar; anéis de régua em volta.
|
|
@@ -857,11 +857,21 @@ export function Gerativo({ el, t, fps }) {
|
|
|
857
857
|
<Fundo P={P} b={b} prevB={bs[bi - 1]} t={t} cam={cam} frame={frame} origem={origem} />
|
|
858
858
|
<div style={{ position: 'absolute', inset: 0, perspective: Math.round(Math.max(W, H) * 0.85) + 'px', perspectiveOrigin: '50% 48%' }}>
|
|
859
859
|
<div style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, transformOrigin: '0 0', transform: mundo, transformStyle: 'preserve-3d' }}>
|
|
860
|
+
{/* O CAMPO (o padrão que cobre o quadro) mora ATRÁS de tudo — do produto 3D
|
|
861
|
+
e do texto — e calmo: desenhado por cima, brigava com os dois (05/10). */}
|
|
862
|
+
{noAr.filter((x) => x.heroi && x.heroi.modo === 'campo' && L[x.id]).map((x) => {
|
|
863
|
+
const bl = L[x.id].bloco, some = 1 - c01((t - x.ate) / 0.14);
|
|
864
|
+
return (
|
|
865
|
+
<div key={'campo' + x.id} style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, transform: 'translateZ(-200px)', opacity: some }}>
|
|
866
|
+
<Heroi b={x} t={t} P={P} slot={{ cx: (bl.x + bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R: Math.min(W, H) * 0.32, op: 0.18 }} />
|
|
867
|
+
</div>
|
|
868
|
+
);
|
|
869
|
+
})}
|
|
860
870
|
{produtos.map((q, i) => {
|
|
861
871
|
const sai = c01((t - q.ate) / 0.3), entra = ed(c01((t - q.de) / 0.6));
|
|
862
872
|
return (
|
|
863
873
|
<div key={'p' + i} style={{ position: 'absolute', left: 0, top: 0, width: W, height: H, opacity: 1 - sai, transform: 'scale(' + (0.94 + 0.06 * entra + 0.1 * sai) + ')', transformOrigin: (q.x + q.largura / 2) + 'px ' + (q.y + q.altura / 2) + 'px' }}>
|
|
864
|
-
<Produto3D el={{ x: q.x, y: q.y, largura: q.largura, altura: q.altura, modelo: q.modelo, cor: q.cor, corAcento: q.corAcento, t: q.de + 0.05, giro: 0.45, escala: q.escala || 1, semChao: true }} t={t} />
|
|
874
|
+
<Produto3D el={{ x: q.x, y: q.y, largura: q.largura, altura: q.altura, modelo: q.modelo, cor: q.cor, corAcento: q.corAcento, acabamento: q.acabamento, camera: q.camera, luz: q.luz, ritmo: q.ritmo, t: q.de + 0.05, giro: 0.45, escala: q.escala || 1, semChao: true }} t={t} />
|
|
865
875
|
</div>
|
|
866
876
|
);
|
|
867
877
|
})}
|
|
@@ -898,7 +908,7 @@ export function Gerativo({ el, t, fps }) {
|
|
|
898
908
|
{x.heroi && (() => {
|
|
899
909
|
// O lugar do herói: o vazio do bloco (como o ícone); sem vazio, atrás do texto e mais fraco.
|
|
900
910
|
const bl = lay.bloco, M = P.margem;
|
|
901
|
-
if (x.heroi.modo === 'campo') return
|
|
911
|
+
if (x.heroi.modo === 'campo') return null; // desenhado atrás, antes do produto
|
|
902
912
|
const R = Math.min(W, H) * (W >= H ? 0.2 : 0.17);
|
|
903
913
|
let slot = null;
|
|
904
914
|
if (W - M - bl.x1 >= R * 2.3) slot = { cx: bl.x1 + (W - M - bl.x1) / 2, cy: (bl.y + bl.y1) / 2, R };
|
|
@@ -8,6 +8,7 @@ import { MARCA } from './marca';
|
|
|
8
8
|
import { Papel } from './base';
|
|
9
9
|
import { TRES } from './tres';
|
|
10
10
|
import { GERATIVO } from './gerativo';
|
|
11
|
+
import { FLUIDO } from './fluido';
|
|
11
12
|
|
|
12
|
-
export const KIT = { ...VOX, ...MARCA, ...TRES, ...GERATIVO, papel: Papel };
|
|
13
|
+
export const KIT = { ...VOX, ...MARCA, ...TRES, ...GERATIVO, ...FLUIDO, papel: Papel };
|
|
13
14
|
export { Filtros, Acabamento, Hud, CURVAS_KIT, bolhaCaminho } from './base';
|
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 };
|
|
@@ -18,14 +18,17 @@
|
|
|
18
18
|
|
|
19
19
|
'use strict';
|
|
20
20
|
|
|
21
|
-
let ATUAL = { modo: null, escopo: '' };
|
|
21
|
+
let ATUAL = { modo: null, escopo: '', explicito: false };
|
|
22
22
|
|
|
23
|
-
function definir(modo, escopo = '') {
|
|
24
|
-
ATUAL = { modo: ['aberto', 'fechado', 'sem'].includes(modo) ? modo : null, escopo: String(escopo || '').trim() };
|
|
23
|
+
function definir(modo, escopo = '', explicito = true) {
|
|
24
|
+
ATUAL = { modo: ['aberto', 'fechado', 'sem'].includes(modo) ? modo : null, escopo: String(escopo || '').trim(), explicito: !!explicito };
|
|
25
25
|
return ATUAL;
|
|
26
26
|
}
|
|
27
27
|
function atual() { return ATUAL; }
|
|
28
|
-
function limpar() { ATUAL = { modo: null, escopo: '' }; }
|
|
28
|
+
function limpar() { ATUAL = { modo: null, escopo: '', explicito: false }; }
|
|
29
|
+
|
|
30
|
+
/** Sem foto nem vídeo de fora: o "sem" e também ninguém ter dito nada (o padrão, 07/10). */
|
|
31
|
+
function semImagem({ modo } = ATUAL) { return modo !== 'aberto' && modo !== 'fechado'; }
|
|
29
32
|
|
|
30
33
|
/** Os termos do escopo fechado: "site:anthropic.com, claude ai, sonnet" → lista. */
|
|
31
34
|
function termos(escopo = ATUAL.escopo) {
|
|
@@ -41,33 +44,40 @@ function entender(resposta) {
|
|
|
41
44
|
return null;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
/* O MODO DEDUZIDO DO PEDIDO.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
/* O MODO DEDUZIDO DO PEDIDO. O padrão é SEM IMAGEM: "por padrão, NÃO
|
|
48
|
+
usar imagens. Somente quando o usuário pedir explicitamente" (Joel, 07/10).
|
|
49
|
+
O menu 1/2/3 do terminal saiu; quem quer imagem diz no pedido:
|
|
50
|
+
"adicione imagens de apoio", "quero imagens durante o vídeo",
|
|
51
|
+
"use imagens reais", "web aberta" → aberto
|
|
52
|
+
"só do site x.com", "usa apenas anthropic.com e …" → fechado (o escopo)
|
|
53
|
+
"só motion", "sem referência", "sem foto" → sem (explícito)
|
|
54
|
+
o resto → sem (o padrão) */
|
|
55
|
+
const PEDE_IMAGEM = /(adicion|coloc|use|usa|usar|quero|bota|pon|insir|inclu|com)\w*\s+(\w+\s+){0,3}(imagens?|fotos?|b-?roll|videos? de (apoio|banco)|cenas reais)\b|(imagens?|fotos?) (de apoio|reais|durante|para ilustrar|pra ilustrar|da (web|internet))|escopo aberto|web aberta|pesquis\w* (a vontade|na web|na internet)|busc\w* (imagens|fotos|videos) na (web|internet)/;
|
|
56
|
+
const NEGA_IMAGEM = /so (o )?motion|motion puro|sem refer|sem (foto|imagem|imagens|video de banco|pesquisa)|nao (pesquis|busqu)|so anima|(nao|nada de) (quero |use |usar |coloc\w* )?(imagens?|fotos?)/;
|
|
57
|
+
|
|
52
58
|
function inferir(texto) {
|
|
53
|
-
const t = String(texto || '').toLowerCase().normalize('NFD').replace(/[
|
|
54
|
-
if (
|
|
59
|
+
const t = String(texto || '').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
60
|
+
if (NEGA_IMAGEM.test(t)) return { modo: 'sem', escopo: '', explicito: true };
|
|
55
61
|
const dominios = String(texto || '').match(/\b[a-z0-9-]+(\.[a-z0-9-]+)+\.?[a-z]{2,}(\/[^\s,]*)?/gi) || [];
|
|
56
62
|
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
63
|
return { modo: 'fechado', escopo: dominios.join(', '), explicito: true };
|
|
58
64
|
}
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
return { modo: 'aberto', escopo: '', explicito: false };
|
|
65
|
+
if (PEDE_IMAGEM.test(t)) return { modo: 'aberto', escopo: '', explicito: true };
|
|
66
|
+
return { modo: 'sem', escopo: '', explicito: false };
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
/** A instrução para o agente, conforme o modo. */
|
|
66
|
-
function instrucao({ modo, escopo } = ATUAL) {
|
|
67
|
-
|
|
70
|
+
function instrucao({ modo, escopo, explicito } = ATUAL) {
|
|
71
|
+
// O vídeo sem imagem é motion: o agente escolhe o estilo de motion que serve ao roteiro, não um fixo.
|
|
72
|
+
const motion = 'O vídeo é MOTION: tipografia, formas, ícones, animação e som. Escolha em roteiro.estilo o motion que serve a este '
|
|
73
|
+
+ 'roteiro (gerativo, fluido, impacto, vox, marca, lancamento) ou deixe vazio para o rodízio — não repita sempre o mesmo.';
|
|
74
|
+
if (modo === 'sem' && explicito) {
|
|
68
75
|
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
|
|
70
|
-
|
|
76
|
+
+ 'não busque foto nem vídeo, não use estudio_midia de banco. Escreva o roteiro com o que o pedido já diz. ' + motion;
|
|
77
|
+
}
|
|
78
|
+
if (modo === 'sem') {
|
|
79
|
+
return '[SEM IMAGEM — o padrão] A pessoa não pediu imagem: não busque foto nem vídeo, não use estudio_midia de banco '
|
|
80
|
+
+ 'nem "busca" nas partes. Pesquisar FATOS em texto (pesquisar_referencia) pode, se o assunto pedir. ' + motion;
|
|
71
81
|
}
|
|
72
82
|
if (modo === 'fechado') {
|
|
73
83
|
const t = termos(escopo);
|
|
@@ -77,8 +87,9 @@ function instrucao({ modo, escopo } = ATUAL) {
|
|
|
77
87
|
+ `No roteiro, ponha roteiro.busca = ${JSON.stringify(t)} para o b-roll.`;
|
|
78
88
|
}
|
|
79
89
|
if (modo === 'aberto') {
|
|
80
|
-
return '[REFERÊNCIAS: ESCOPO ABERTO] Pesquise o assunto à vontade (pesquisar_referencia; estudio_site se for de uma marca) '
|
|
81
|
-
+ '
|
|
90
|
+
return '[REFERÊNCIAS: ESCOPO ABERTO — a pessoa pediu imagens] Pesquise o assunto à vontade (pesquisar_referencia; estudio_site se for de uma marca). '
|
|
91
|
+
+ 'A imagem é um elemento da narrativa, não o vídeo inteiro: dê "busca" (em inglês: gente, lugar, tela, objeto de verdade) '
|
|
92
|
+
+ 'SÓ nas partes em que uma imagem real mostra o que o texto não mostra. Não troque de imagem a cada frase; o resto é motion.';
|
|
82
93
|
}
|
|
83
94
|
return '';
|
|
84
95
|
}
|
|
@@ -102,4 +113,4 @@ function filtrar(r, { modo, escopo } = ATUAL) {
|
|
|
102
113
|
return { ...r, fontes: refs.length, referencias: refs, ...(fora ? { foraDoEscopo: fora } : {}) };
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
module.exports = { definir, atual, limpar, termos, entender, inferir, instrucao, dominios, filtrar };
|
|
116
|
+
module.exports = { definir, atual, limpar, semImagem, termos, entender, inferir, instrucao, dominios, filtrar };
|
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,
|
|
@@ -711,6 +719,8 @@ export const Video = () => {
|
|
|
711
719
|
return vs[vs.length - 1].v; }
|
|
712
720
|
: (plano.audio.trilha.volume ?? 0.18)}
|
|
713
721
|
loop={plano.audio.trilha.loop !== false}
|
|
722
|
+
/* O início da faixa que casa as batidas com os cortes (batida.js). */
|
|
723
|
+
startFrom={Math.round((plano.audio.trilha.inicio || 0) * fps)}
|
|
714
724
|
/>
|
|
715
725
|
)}
|
|
716
726
|
{/* Os TRECHOS: pedaços de áudio no tempo certo — a narração partida entre
|
|
@@ -1005,4 +1015,41 @@ function renderizar({ raiz, saida, minutos = 30, aoFalar = () => {} }) {
|
|
|
1005
1015
|
});
|
|
1006
1016
|
}
|
|
1007
1017
|
|
|
1008
|
-
|
|
1018
|
+
/* ── ABRIR PARA EDITAR ────────────────────────────────────────────────
|
|
1019
|
+
"Quando eu falar pra editar, edita no editor de vídeo open source." O
|
|
1020
|
+
projeto já é Remotion: editar é abrir o Remotion Studio na pasta dele. Ele
|
|
1021
|
+
sobe solto (o PrimoCode não espera) e abre o navegador sozinho. */
|
|
1022
|
+
function comandoStudio(raiz) {
|
|
1023
|
+
return { cmd: 'npx', args: ['remotion', 'studio'], cwd: raiz };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function abrirStudio({ raiz, spawnar = spawn } = {}) {
|
|
1027
|
+
if (!raiz || !fs.existsSync(path.join(raiz, 'package.json'))) {
|
|
1028
|
+
return { ok: false, error: 'não achei o projeto Remotion nesta pasta.' };
|
|
1029
|
+
}
|
|
1030
|
+
// O Studio precisa do Remotion instalado. A base compartilhada já vem do primeiro render.
|
|
1031
|
+
if (!fs.existsSync(path.join(raiz, 'node_modules'))) {
|
|
1032
|
+
const comp = process.platform === 'win32' ? null : baseCompartilhada(raiz);
|
|
1033
|
+
if (comp && comp.pronta) {
|
|
1034
|
+
try { fs.symlinkSync(path.join(comp.base, 'node_modules'), path.join(raiz, 'node_modules'), 'dir'); } catch { /* segue */ }
|
|
1035
|
+
}
|
|
1036
|
+
if (!fs.existsSync(path.join(raiz, 'node_modules'))) {
|
|
1037
|
+
return { ok: false, error: 'o Remotion ainda não está instalado para este projeto.',
|
|
1038
|
+
comoResolver: 'renderize uma vez com estudio_render (instala o Remotion), depois abra o editor.' };
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const c = comandoStudio(raiz);
|
|
1042
|
+
const win = process.platform === 'win32';
|
|
1043
|
+
try {
|
|
1044
|
+
const p = spawnar(c.cmd, win ? c.args.map(citarWin) : c.args,
|
|
1045
|
+
{ cwd: c.cwd, detached: true, stdio: 'ignore', shell: win });
|
|
1046
|
+
if (p && p.on) p.on('error', () => { /* sem npx: o caminho vai na resposta */ });
|
|
1047
|
+
if (p && p.unref) p.unref();
|
|
1048
|
+
} catch (e) {
|
|
1049
|
+
return { ok: false, error: e.message };
|
|
1050
|
+
}
|
|
1051
|
+
return { ok: true, raiz, comando: `cd "${raiz}" && npx remotion studio` };
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
module.exports = { montar, renderizar, fontesCSS, VERSAO_REMOTION, CENA_JSX, pkgJson, baseCompartilhada, TSCONFIG,
|
|
1055
|
+
comandoStudio, abrirStudio };
|