primocode 9.8.0-beta.16 → 9.8.0-beta.18
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/lib/estudio/assets.js +34 -0
- package/lib/estudio/composicao.js +38 -10
- package/lib/estudio/edicao.js +10 -1
- package/lib/estudio/impacto.js +263 -0
- package/lib/estudio/index.js +74 -8
- package/lib/estudio/plano.js +1 -1
- package/lib/estudio/remotion.js +63 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v9.8.0-beta.
|
|
1
|
+
# PrimoCode v9.8.0-beta.18
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v9.8.0-beta.
|
|
12
|
+
▐▛███▜▌ Primo Code v9.8.0-beta.18
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
package/lib/estudio/assets.js
CHANGED
|
@@ -120,7 +120,41 @@ const COLECOES = [
|
|
|
120
120
|
* O SVG vem com `currentColor` no traço para a cor ser decidida na cena — um
|
|
121
121
|
* ícone com cor cravada no arquivo obriga a baixar de novo para mudar de cor.
|
|
122
122
|
*/
|
|
123
|
+
/* ── EMOJI 3D ─────────────────────────────────────────────────────────
|
|
124
|
+
* "Os emojis, quero emojis da Apple." (25/09/2026) Os da Apple têm direito
|
|
125
|
+
* autoral e não podem ir num vídeo publicado; os Fluent 3D da Microsoft têm
|
|
126
|
+
* licença MIT e o mesmo visual brilhante e com volume. "fluent-3d:robot" é o
|
|
127
|
+
* PNG do repositório oficial (pelo jsDelivr), embutido como <img> — o resto
|
|
128
|
+
* do render trata igual a um ícone SVG. Sem o 3D, cai no emoji plano. */
|
|
129
|
+
const cache3d = new Map();
|
|
130
|
+
async function emoji3d(nome, { pasta = null } = {}) {
|
|
131
|
+
if (cache3d.has(nome)) return cache3d.get(nome);
|
|
132
|
+
const palavras = nome.split('-');
|
|
133
|
+
const pastas = [
|
|
134
|
+
palavras.join(' ').replace(/^./, (c) => c.toUpperCase()),
|
|
135
|
+
palavras.map((w) => w[0].toUpperCase() + w.slice(1)).join(' '),
|
|
136
|
+
];
|
|
137
|
+
for (const pastaGit of pastas) {
|
|
138
|
+
const url = `https://cdn.jsdelivr.net/gh/microsoft/fluentui-emoji@main/assets/${encodeURIComponent(pastaGit)}/3D/${palavras.join('_')}_3d.png`;
|
|
139
|
+
try {
|
|
140
|
+
const png = await pegar(url, { binario: true });
|
|
141
|
+
if (png && png.length > 1000 && png[0] === 0x89) {
|
|
142
|
+
if (pasta) guardar(path.join(pasta, 'midia', 'icones', `fluent-3d-${nome}.png`), png, { licenca: 'MIT (Microsoft Fluent UI Emoji)', origem: url });
|
|
143
|
+
const r = { ok: true, nome: 'fluent-3d:' + nome,
|
|
144
|
+
svg: `<img src="data:image/png;base64,${png.toString('base64')}" style="width:100%;height:100%;object-fit:contain;display:block" />` };
|
|
145
|
+
cache3d.set(nome, r);
|
|
146
|
+
return r;
|
|
147
|
+
}
|
|
148
|
+
} catch { /* a próxima grafia */ }
|
|
149
|
+
}
|
|
150
|
+
const plano = await icone('fluent-emoji-flat:' + nome, { pasta });
|
|
151
|
+
const r = plano.ok ? { ...plano, nome: 'fluent-3d:' + nome } : plano;
|
|
152
|
+
cache3d.set(nome, r);
|
|
153
|
+
return r;
|
|
154
|
+
}
|
|
155
|
+
|
|
123
156
|
async function icone(nome, { pasta = null } = {}) {
|
|
157
|
+
if (/^fluent-3d:/i.test(String(nome))) return emoji3d(String(nome).split(':')[1], { pasta });
|
|
124
158
|
const m = String(nome || '').match(/^([a-z0-9-]+):([a-z0-9-]+)$/i);
|
|
125
159
|
if (!m) return { ok: false, error: 'o ícone é "coleção:nome", como "lucide:zap"' };
|
|
126
160
|
const url = `https://api.iconify.design/${m[1]}/${m[2]}.svg?color=currentColor`;
|
|
@@ -64,7 +64,8 @@ function paleta(marca = {}) {
|
|
|
64
64
|
/* ── a letra que cabe ─────────────────────────────────────────────────────
|
|
65
65
|
* Largura média do caractere em "em" por família. Não é exata — é o
|
|
66
66
|
* bastante para escolher o maior tamanho que cabe em N linhas, com folga. */
|
|
67
|
-
|
|
67
|
+
// Com folga: medido em 25/09, 0,52 deixava "Fundadores" quebrar no meio no card.
|
|
68
|
+
const LARGURA = { condensada: 0.46, display: 0.6, texto: 0.58, serifa: 0.54 };
|
|
68
69
|
const FAMILIA_TIPO = (f) => (/condensed|narrow|anton|oswald|bebas|teko|league gothic/i.test(f) ? 'condensada'
|
|
69
70
|
: /serif|fraunces|playfair|garamond|lora|newsreader|spectral|baskerville|dm serif/i.test(f) ? 'serifa'
|
|
70
71
|
: /black|bricolage|archivo|sora|grotesk|syne|unbounded/i.test(f) ? 'display' : 'texto');
|
|
@@ -170,7 +171,9 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
170
171
|
const base = vid ? Math.max(1.6, (proxDe != null ? proxDe : (tp.ate || 0) + 0.9) - (tp.de || 0)) : 3;
|
|
171
172
|
const trans = !vid || i === 0 ? 'corte' : TRANSICOES[i % TRANSICOES.length];
|
|
172
173
|
const cruz = trans === 'corte' ? 0 : 0.35;
|
|
173
|
-
|
|
174
|
+
// A cena fica no ar até a SEGUINTE terminar de entrar (o cruzamento é dela).
|
|
175
|
+
const cruzProx = vid && i + 1 < partes.length ? 0.35 : 0;
|
|
176
|
+
const dur = r2(base + cruzProx);
|
|
174
177
|
const els = [];
|
|
175
178
|
const variante = i % 2;
|
|
176
179
|
|
|
@@ -183,7 +186,7 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
183
186
|
const fotoFundo = fundos[i];
|
|
184
187
|
if (fotoFundo && p.tipo !== 'imagem') {
|
|
185
188
|
const ehV = fotoFundo.tipo === 'video';
|
|
186
|
-
els.push({ tipo: ehV ? 'video' : 'imagem', src: fotoFundo.src, ...(ehV ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover',
|
|
189
|
+
els.push({ tipo: ehV ? 'video' : 'imagem', src: fotoFundo.src, ...(ehV ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', foco: 'center 28%',
|
|
187
190
|
anima: { escala: [{ t: 0, v: variante ? 1.14 : 1.02 }, { t: dur, v: variante ? 1.02 : 1.14, e: 'suave' }] } });
|
|
188
191
|
els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%',
|
|
189
192
|
fundo: { tipo: 'gradiente', cores: [alfa(cor.fundo, 0.93), alfa(cor.fundo, 0.72), alfa(cor.fundo, 0.55)], angulo: 90 } });
|
|
@@ -192,7 +195,8 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
192
195
|
const canto = [[W - R * 0.55, -R * 0.45], [-R * 0.45, H - R * 0.5], [W - R * 0.4, H - R * 0.55], [-R * 0.5, -R * 0.4]][i % 4];
|
|
193
196
|
els.push({
|
|
194
197
|
tipo: 'forma', forma: 'circulo', largura: R, altura: R, x: Math.round(canto[0]), y: Math.round(canto[1]),
|
|
195
|
-
|
|
198
|
+
// O brilho alterna entre o destaque e a cor de apoio: as duas cores da marca aparecem.
|
|
199
|
+
cor: p.tipo === 'numero' ? cor.sobreDestaque : (variante && marca.cores && marca.cores.apoio ? cor.apoio : cor.destaque), opacidade: p.tipo === 'numero' ? 0.12 : (cor.escuro ? 0.26 : 0.16),
|
|
196
200
|
anima: {
|
|
197
201
|
desfoque: [{ t: 0, v: Math.round(R * 0.22) }],
|
|
198
202
|
x: [{ t: 0, v: 0 }, { t: dur, v: variante ? 60 : -60, e: 'suave' }],
|
|
@@ -293,7 +297,7 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
293
297
|
vazia embaixo. */
|
|
294
298
|
const fT = p.titulo ? caber(p.titulo, { largura: W0, linhas: 2, max: H * 0.1, fonte: F.titulo }) : { tamanho: 0, linhas: 0 };
|
|
295
299
|
const hTit = fT.tamanho * 1.04 * fT.linhas;
|
|
296
|
-
const hCard0 = colunas > 1 ? (p.tipo === 'passos' ? H * 0.26 : H * 0.44) : H * 0.14;
|
|
300
|
+
const hCard0 = colunas > 1 ? (p.tipo === 'passos' ? H * 0.26 : H * 0.44) : H * (deitado ? 0.14 : 0.13);
|
|
297
301
|
const hItens = colunas > 1 ? hCard0 : n * hCard0 + (n - 1) * gap;
|
|
298
302
|
const bloco = hTit + (hTit ? H * 0.07 : 0) + hItens;
|
|
299
303
|
const yTit = Math.max(M, (H - bloco) / 2);
|
|
@@ -301,6 +305,23 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
301
305
|
const topo = yTit + hTit + (hTit ? H * 0.07 : 0);
|
|
302
306
|
const hCard = Math.min(hCard0, colunas > 1 ? H - topo - M : (H - topo - M - gap * (n - 1)) / n);
|
|
303
307
|
const dur0 = vid ? Math.max(1, (tp.ate || dur) - (tp.de || 0)) : 1;
|
|
308
|
+
/* Um tamanho de letra para TODOS os itens: o do mais apertado. Cada
|
|
309
|
+
item com o seu tamanho deixava um card com a letra maior que os
|
|
310
|
+
outros, e esse texto saía do card (medido em 25/09). */
|
|
311
|
+
const padG = Math.round(Math.min(wCard, hCard) * 0.14);
|
|
312
|
+
const tamIG = Math.round(Math.min(hCard * (colunas > 1 ? 0.28 : 0.5), H * 0.09));
|
|
313
|
+
const lwG = colunas > 1 ? wCard - 2 * padG : wCard - (tamIG + 3 * padG);
|
|
314
|
+
// No vertical a linha é estreita: 3 linhas por item, para a letra caber grande.
|
|
315
|
+
const linhasG = colunas > 1 ? 4 : (deitado ? 2 : 3);
|
|
316
|
+
const hTexto = colunas > 1 ? hCard - (padG * 1.8 + tamIG + padG) : hCard - padG;
|
|
317
|
+
const maxItem = deitado ? Math.min(H * 0.05, hCard * 0.3) : Math.min(W * 0.062, hCard * 0.26);
|
|
318
|
+
const fits = itens.map((it) => caber(it.texto, { largura: lwG, linhas: linhasG, max: maxItem, fonte: F.texto }));
|
|
319
|
+
let tamItens = Math.min(...fits.map((f) => f.tamanho));
|
|
320
|
+
// Só na lista o texto mora DENTRO do card: ali a altura também manda.
|
|
321
|
+
if (p.tipo === 'lista') {
|
|
322
|
+
const linhasReais = Math.max(...itens.map((it) => caber(it.texto, { largura: lwG, linhas: linhasG, max: tamItens, min: tamItens, fonte: F.texto }).linhas));
|
|
323
|
+
tamItens = Math.min(tamItens, Math.floor(hTexto / (1.2 * linhasReais)));
|
|
324
|
+
}
|
|
304
325
|
itens.forEach((it, k) => {
|
|
305
326
|
const cx = colunas > 1 ? M + k * (wCard + gap) : M;
|
|
306
327
|
const cy = colunas > 1 ? topo : topo + k * (hCard + gap);
|
|
@@ -327,7 +348,8 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
327
348
|
const tx = colunas > 1 ? cx + pad : cx + pad + tamI + pad;
|
|
328
349
|
const ty = colunas > 1 ? cy + pad + tamI + pad * 0.8 : cy + hCard / 2;
|
|
329
350
|
const lw = colunas > 1 ? wCard - 2 * pad : wCard - (tamI + 3 * pad);
|
|
330
|
-
const
|
|
351
|
+
const fit0 = caber(it.texto, { largura: lw, linhas: linhasG, max: tamItens, min: Math.min(18, tamItens), fonte: F.texto });
|
|
352
|
+
const fit = { tamanho: Math.min(fit0.tamanho, tamItens), linhas: fit0.linhas };
|
|
331
353
|
els.push({
|
|
332
354
|
tipo: 'texto', texto: it.texto, fonte: F.texto, peso: 650, tamanho: fit.tamanho, cor: tinta, entrelinha: 1.2,
|
|
333
355
|
x: Math.round(tx), y: Math.round(colunas > 1 ? ty : ty - fit.tamanho * 1.2 * fit.linhas / 2), largura: Math.round(lw), anima: entra,
|
|
@@ -365,13 +387,19 @@ function comporRoteiro({ roteiro, modo = 'video', largura = 1920, altura = 1080,
|
|
|
365
387
|
const src = m && typeof m === 'object' ? m.src : m;
|
|
366
388
|
if (src) {
|
|
367
389
|
const ehVideo = m && typeof m === 'object' && m.tipo === 'video';
|
|
368
|
-
|
|
390
|
+
// O enquadramento puxa para cima: é onde ficam os rostos.
|
|
391
|
+
els.push({ tipo: ehVideo ? 'video' : 'imagem', src, ...(ehVideo ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', foco: 'center 28%', anima: { escala: [{ t: 0, v: 1.12 }, { t: dur, v: 1, e: 'suave' }] } });
|
|
369
392
|
els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(0,0,0,0)', 'rgba(0,0,0,0.25)', 'rgba(0,0,0,0.85)'], angulo: 180 } });
|
|
370
393
|
}
|
|
371
|
-
const larg = W0 * 0.8;
|
|
372
|
-
const fit = caber(p.titulo || '', { largura: larg, linhas: 2, max: H * 0.1, fonte: F.titulo });
|
|
394
|
+
const larg = W0 * (deitado ? 0.8 : 1);
|
|
395
|
+
const fit = caber(p.titulo || '', { largura: larg, linhas: 2, max: H * (deitado ? 0.1 : 0.06), fonte: F.titulo });
|
|
373
396
|
const hT = fit.tamanho * 1.04 * fit.linhas;
|
|
374
|
-
|
|
397
|
+
/* Vídeo da marca (depoimento, bastidor) costuma ter o nome da
|
|
398
|
+
pessoa gravado embaixo: o título vai para o alto, com o véu
|
|
399
|
+
de cima, para não escrever por cima dele. */
|
|
400
|
+
const noTopo = Boolean(m && typeof m === 'object' && m.tipo === 'video');
|
|
401
|
+
if (noTopo) els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', fundo: { tipo: 'gradiente', cores: ['rgba(0,0,0,0.8)', 'rgba(0,0,0,0.2)', 'rgba(0,0,0,0)'], angulo: 180 } });
|
|
402
|
+
const yT = noTopo ? M * 1.2 : H - M - hT - (p.sub ? H * 0.08 : 0);
|
|
375
403
|
if (p.titulo) titulo(p.titulo, { y: yT, larg, max: fit.tamanho, cor: '#FFFFFF', de: 0.3 });
|
|
376
404
|
if (p.sub) linhaFina(p.sub, { y: yT + hT + H * 0.02, larg, c: 'rgba(255,255,255,0.8)', de: 0.7 });
|
|
377
405
|
break;
|
package/lib/estudio/edicao.js
CHANGED
|
@@ -116,7 +116,7 @@ function iconeDe(palavra) {
|
|
|
116
116
|
if (!w || VAZIAS.has(w)) return null;
|
|
117
117
|
for (const [raizes, nome] of ICONES) {
|
|
118
118
|
for (const r of raizes) {
|
|
119
|
-
if (r.length <= 3 ? w === r : w.startsWith(r)) return 'fluent-
|
|
119
|
+
if (r.length <= 3 ? w === r : w.startsWith(r)) return 'fluent-3d:' + nome;
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
return null;
|
|
@@ -419,6 +419,15 @@ const SONS = {
|
|
|
419
419
|
pop: ['-f', 'lavfi', '-i', "aevalsrc='0.9*sin(2*PI*(1500-7000*t)*t)*exp(-38*t)':s=44100:d=0.14"],
|
|
420
420
|
whoosh: ['-f', 'lavfi', '-i', 'anoisesrc=d=0.55:c=pink:a=0.6',
|
|
421
421
|
'-af', 'highpass=f=500,lowpass=f=5000,afade=t=in:st=0:d=0.3:curve=exp,afade=t=out:st=0.3:d=0.25,volume=0.9'],
|
|
422
|
+
// O impacto: um grave que cai de 60 para 30 Hz, com o estalo do ataque.
|
|
423
|
+
impacto: ['-f', 'lavfi', '-i', "aevalsrc='0.95*sin(2*PI*(62-30*t)*t)*exp(-3.6*t)+0.3*(random(0)*2-1)*exp(-45*t)':s=44100:d=1.3",
|
|
424
|
+
'-af', 'lowpass=f=2500,volume=1.4'],
|
|
425
|
+
// O riser: sobe dois segundos e corta — anuncia o que vem.
|
|
426
|
+
riser: ['-f', 'lavfi', '-i', "aevalsrc='0.4*sin(2*PI*(160+700*t*t)*t)*(t/2)+0.3*(random(0)*2-1)*pow(t/2,2)':s=44100:d=2",
|
|
427
|
+
'-af', 'highpass=f=120,volume=1.2'],
|
|
428
|
+
// O estalo do texto batendo na tela.
|
|
429
|
+
estalo: ['-f', 'lavfi', '-i', "aevalsrc='(random(0)*2-1)*exp(-38*t)*0.9':s=44100:d=0.22",
|
|
430
|
+
'-af', 'highpass=f=900,lowpass=f=9000'],
|
|
422
431
|
};
|
|
423
432
|
|
|
424
433
|
async function prepararSons(pasta, video) {
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* impacto.js — o estilo de anúncio: corte a cada batida, palavra que bate
|
|
3
|
+
* na tela, foto que pisca, tremor, soco, chicote, flash e som de impacto.
|
|
4
|
+
*
|
|
5
|
+
* "Falta mais ação, o vídeo ser mais agressivo! Mais vendável, mais
|
|
6
|
+
* atraente… falta aplicar a identidade visual… falta mais efeito, e o
|
|
7
|
+
* vídeo parece um vídeo de 2010." (25/09/2026)
|
|
8
|
+
*
|
|
9
|
+
* O estilo normal (composicao.js) dá uma cena por parte, de 4 a 5 s, com
|
|
10
|
+
* entrada suave: é apresentação. Anúncio de 2026 corta a cada 0,5 a 1,5 s.
|
|
11
|
+
* Aqui cada parte do roteiro vira várias BATIDAS, no tempo das palavras da
|
|
12
|
+
* voz, e cada batida é uma de três telas:
|
|
13
|
+
*
|
|
14
|
+
* PALAVRA a fala em letra gigante no preto, com soco de zoom e estalo;
|
|
15
|
+
* FOTO a foto real da marca, rápida, com a fala por cima;
|
|
16
|
+
* MARCA o gradiente da marca em tela cheia, com o emoji 3D.
|
|
17
|
+
*
|
|
18
|
+
* O número conta com tremor, a lista mostra um item por batida, e a chamada
|
|
19
|
+
* final vem com riser, impacto e a logo. Por cima de tudo: granulação,
|
|
20
|
+
* vinheta e a luz da marca passando.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
'use strict';
|
|
24
|
+
|
|
25
|
+
const { caber, paleta } = require('./composicao.js');
|
|
26
|
+
const { iconeDe, peso } = require('./edicao.js');
|
|
27
|
+
|
|
28
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
29
|
+
const limpa = (t) => String(t || '').replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}%!?]+$/gu, '');
|
|
30
|
+
|
|
31
|
+
/* As batidas de uma parte: grupos de 1 a 4 palavras, de ~0,6 s ou mais,
|
|
32
|
+
quebrando na pontuação. Batida curta demais gruda na seguinte. */
|
|
33
|
+
function batidas(palavras, fim) {
|
|
34
|
+
const out = [];
|
|
35
|
+
let atual = [];
|
|
36
|
+
palavras.forEach((p, i) => {
|
|
37
|
+
atual.push(p);
|
|
38
|
+
const prox = palavras[i + 1];
|
|
39
|
+
const dur = (prox ? prox.de : fim) - atual[0].de;
|
|
40
|
+
// A batida não termina em palavra solta ("CLIQUE E | confirme"): ela gruda na seguinte.
|
|
41
|
+
const solta = /^(e|de|da|do|a|o|as|os|que|com|sua|seu|no|na|em|pra|para|por|um|uma|não|é)$/i.test(limpa(p.texto));
|
|
42
|
+
if (!prox || /[,.!?;:…]$/.test(p.texto) || atual.length >= 5 || (!solta && (atual.length >= 4 || (dur >= 0.9 && atual.length >= 2)))) {
|
|
43
|
+
out.push(atual);
|
|
44
|
+
atual = [];
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const juntas = [];
|
|
48
|
+
for (const b of out) {
|
|
49
|
+
const ult = juntas[juntas.length - 1];
|
|
50
|
+
const dur = b[b.length - 1].ate - b[0].de;
|
|
51
|
+
if (ult && dur < 0.45 && ult.length + b.length <= 5) ult.push(...b);
|
|
52
|
+
else juntas.push([...b]);
|
|
53
|
+
}
|
|
54
|
+
return juntas;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function montarImpacto({ roteiro, largura = 1080, altura = 1920, tempos = [], narracao = null, sons = {}, trilha = null,
|
|
58
|
+
fotos = [], iconesOk = null }) {
|
|
59
|
+
const partes = (roteiro && roteiro.partes) || [];
|
|
60
|
+
if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
|
|
61
|
+
const marca = roteiro.marca || {};
|
|
62
|
+
const cor = paleta(marca);
|
|
63
|
+
const grad = (marca.gradiente && marca.gradiente.length >= 2) ? marca.gradiente : [cor.apoio, cor.destaque];
|
|
64
|
+
const F = { titulo: (marca.fontes && marca.fontes.titulo) || 'Inter', texto: (marca.fontes && marca.fontes.texto) || 'Inter' };
|
|
65
|
+
const W = largura, H = altura, deitado = W >= H;
|
|
66
|
+
const M = Math.round(Math.min(W, H) * 0.07);
|
|
67
|
+
const W0 = W - 2 * M;
|
|
68
|
+
const cenas = [], efeitos = [];
|
|
69
|
+
let fotoI = 0, nBatida = 0;
|
|
70
|
+
const proxFoto = () => (fotos.length ? fotos[fotoI++ % fotos.length] : null);
|
|
71
|
+
const som = (t, nome, volume) => { if (sons[nome]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome], volume }); };
|
|
72
|
+
const icOk = (ic) => ic && (!iconesOk || iconesOk.has(ic));
|
|
73
|
+
|
|
74
|
+
// A palavra que bate: letra gigante, a palavra forte na cor de destaque.
|
|
75
|
+
const bater = (els, texto, { y = null, corTexto = cor.tinta, destaque = cor.destaque, max = (deitado ? H * 0.2 : W * 0.2), linhas = 3, glitch = false, girar = 0 } = {}) => {
|
|
76
|
+
const t = String(texto || '').toUpperCase();
|
|
77
|
+
/* Caixa alta em peso 900 é ~20% mais larga que a letra comum, e a
|
|
78
|
+
batida ainda cresce 5%: mede numa caixa mais estreita (medido em
|
|
79
|
+
25/09: "CONSTRUÇÃO" e "DEMONSTRAÇÃO" saíam pela borda). */
|
|
80
|
+
const fit = caber(t, { largura: W0 * 0.8, linhas, max, fonte: F.titulo });
|
|
81
|
+
const h = fit.tamanho * 0.98 * fit.linhas;
|
|
82
|
+
const ws = t.split(/\s+/);
|
|
83
|
+
let forte = -1, pf = 0;
|
|
84
|
+
ws.forEach((w, k) => { const v = peso(w); if (v > pf) { pf = v; forte = k; } });
|
|
85
|
+
els.push({
|
|
86
|
+
tipo: 'texto', texto: t, fonte: F.titulo, peso: 900, tamanho: fit.tamanho, cor: corTexto, entrelinha: 0.98,
|
|
87
|
+
x: M, y: Math.round(y == null ? (H - h) / 2 : y), largura: W0, alinhamento: 'center', espacamento: -1,
|
|
88
|
+
...(forte >= 0 && ws.length > 1 ? { revelar: ws.map(() => 0), destaques: [forte], corDestaque: destaque } : {}),
|
|
89
|
+
...(glitch ? { glitch: { forca: 14, dur: 0.3 } } : {}),
|
|
90
|
+
anima: {
|
|
91
|
+
escala: [{ t: 0, v: 1.45 }, { t: 0.13, v: 0.97, e: 'saida' }, { t: 0.22, v: 1, e: 'suave' }, { t: 3, v: 1.05, e: 'linear' }],
|
|
92
|
+
opacidade: [{ t: 0, v: 0 }, { t: 0.05, v: 1 }],
|
|
93
|
+
rotacao: [{ t: 0, v: girar * 2 }, { t: 0.2, v: girar, e: 'saida' }],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
return h;
|
|
97
|
+
};
|
|
98
|
+
const emoji = (els, texto, { x = W / 2, y = H * 0.24, tam = Math.min(W, H) * 0.26 } = {}) => {
|
|
99
|
+
let ic = null;
|
|
100
|
+
for (const w of String(texto || '').split(/\s+/)) { ic = iconeDe(w); if (icOk(ic)) break; ic = null; }
|
|
101
|
+
if (!ic) return false;
|
|
102
|
+
els.push({ tipo: 'icone', nome: ic, tamanho: Math.round(tam), x: Math.round(x), y: Math.round(y), ancora: 'centro',
|
|
103
|
+
anima: { escala: [{ t: 0.05, v: 0 }, { t: 0.22, v: 1.2, e: 'saida' }, { t: 0.36, v: 1 }], rotacao: [{ t: 0.05, v: -25 }, { t: 0.36, v: 0, e: 'saida' }, { t: 3, v: 8 }],
|
|
104
|
+
y: [{ t: 0.36, v: 0 }, { t: 3, v: -30, e: 'suave' }] } });
|
|
105
|
+
return true;
|
|
106
|
+
};
|
|
107
|
+
const fotoCheia = (els, f, { veu = 0.55 } = {}) => {
|
|
108
|
+
if (!f) return;
|
|
109
|
+
const v = f.tipo === 'video';
|
|
110
|
+
els.push({ tipo: v ? 'video' : 'imagem', src: f.src, ...(v ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', foco: 'center 28%',
|
|
111
|
+
anima: { escala: [{ t: 0, v: 1.28 }, { t: 0.25, v: 1.1, e: 'saida' }, { t: 3, v: 1.02, e: 'linear' }] } });
|
|
112
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%',
|
|
113
|
+
fundo: { tipo: 'gradiente', cores: [`rgba(0,0,0,${veu * 0.4})`, `rgba(0,0,0,${veu * 0.6})`, `rgba(0,0,0,${veu + 0.25})`], angulo: 180 } });
|
|
114
|
+
};
|
|
115
|
+
const logoTopo = (els, forte = false) => {
|
|
116
|
+
if (!marca.logo) return;
|
|
117
|
+
const h = Math.round(H * (forte ? 0.07 : (deitado ? 0.05 : 0.028)));
|
|
118
|
+
els.push({ tipo: 'imagem', src: marca.logo, x: Math.round(W / 2 - h * 2), y: forte ? Math.round(H * 0.78) : M, largura: h * 4, altura: h, ajuste: 'contain', opacidade: forte ? 1 : 0.9,
|
|
119
|
+
...(forte ? { anima: { escala: [{ t: 0, v: 0.6 }, { t: 0.3, v: 1.08, e: 'saida' }, { t: 0.5, v: 1 }], opacidade: [{ t: 0, v: 0 }, { t: 0.15, v: 1 }] } } : {}) });
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
partes.forEach((p, i) => {
|
|
123
|
+
const tp = tempos[i] || { de: i * 3, ate: i * 3 + 2.6, palavras: [] };
|
|
124
|
+
const fim = tempos[i + 1] ? tempos[i + 1].de : (tp.ate || 0) + 0.9;
|
|
125
|
+
const ini = i === 0 ? 0 : tp.de;
|
|
126
|
+
let bs = batidas((tp.palavras || []).filter((w) => w && w.texto), fim);
|
|
127
|
+
if (!bs.length) bs = [[{ de: ini, ate: fim, texto: p.titulo || p.sim || p.valor || '' }]];
|
|
128
|
+
const itens = (p.itens || []).map((it) => (typeof it === 'string' ? it : it.texto));
|
|
129
|
+
|
|
130
|
+
// Impacto no começo de cada parte; o riser anuncia a chamada final.
|
|
131
|
+
som(ini, 'impacto', i === 0 ? 0.9 : 0.6);
|
|
132
|
+
if (p.tipo === 'cta') som(ini - 2, 'riser', 0.6);
|
|
133
|
+
|
|
134
|
+
bs.forEach((b, k) => {
|
|
135
|
+
const de = k === 0 ? ini : b[0].de - 0.04;
|
|
136
|
+
const ate = k < bs.length - 1 ? bs[k + 1][0].de - 0.04 : fim;
|
|
137
|
+
const falado = b.map((w) => limpa(w.texto)).join(' ');
|
|
138
|
+
const els = [];
|
|
139
|
+
const cena = { fundo: { tipo: 'cor', valor: cor.fundo }, elementos: els };
|
|
140
|
+
const ultimo = k === bs.length - 1;
|
|
141
|
+
let tela; // 'palavra' | 'foto' | 'marca'
|
|
142
|
+
|
|
143
|
+
if (p.tipo === 'numero' && k === 0) {
|
|
144
|
+
// O número em tela cheia, contando, com a câmera tremendo.
|
|
145
|
+
cena.fundo = { tipo: 'gradiente', cores: [grad[0], grad[1]], angulo: 150 };
|
|
146
|
+
const valor = String(p.valor);
|
|
147
|
+
const m = valor.match(/^([^\d-]*)(-?[\d.,]+)(.*)$/);
|
|
148
|
+
const fit = caber(valor, { largura: W0, linhas: 1, max: H * (deitado ? 0.55 : 0.32), fonte: F.titulo });
|
|
149
|
+
els.push({ tipo: 'texto', texto: valor, fonte: F.titulo, peso: 900, tamanho: fit.tamanho, cor: '#FFFFFF', x: M, y: Math.round(H * 0.5 - fit.tamanho * 0.62), largura: W0, alinhamento: 'center',
|
|
150
|
+
...(m ? { contar: { ate: Number(m[2].replace(/\./g, '').replace(',', '.')), dur: 0.7, antes: m[1], depois: m[3] } } : {}),
|
|
151
|
+
anima: { escala: [{ t: 0, v: 1.6 }, { t: 0.18, v: 1, e: 'saida' }] } });
|
|
152
|
+
bater(els, p.rotulo || '', { y: H * 0.5 + fit.tamanho * 0.45, max: H * 0.07, linhas: 2, corTexto: '#FFFFFF', destaque: '#FFFFFF' });
|
|
153
|
+
cena.tremor = [{ de: 0, ate: 0.5, forca: 26 }];
|
|
154
|
+
tela = 'marca';
|
|
155
|
+
} else if ((p.tipo === 'lista' || p.tipo === 'passos') && itens.length) {
|
|
156
|
+
// Um item por batida: o emoji 3D grande e o item batendo.
|
|
157
|
+
const it = itens[Math.min(k, itens.length - 1)];
|
|
158
|
+
if (k % 2 === 1) cena.fundo = { tipo: 'gradiente', cores: [grad[0], grad[1]], angulo: 135 };
|
|
159
|
+
const rot = p.tipo === 'passos' ? `${k + 1}. ${it}` : it;
|
|
160
|
+
const temE = emoji(els, it, { y: H * (deitado ? 0.28 : 0.3) });
|
|
161
|
+
// No gradiente, o destaque é o escuro da marca: laranja sobre azul some.
|
|
162
|
+
bater(els, rot, { y: temE ? H * (deitado ? 0.48 : 0.45) : null, max: deitado ? H * 0.11 : W * 0.12, linhas: 4, glitch: k === 0,
|
|
163
|
+
...(k % 2 === 1 ? { corTexto: '#FFFFFF', destaque: cor.fundo } : {}) });
|
|
164
|
+
if (k === 0 && p.titulo) els.push({ tipo: 'texto', texto: String(p.titulo).toUpperCase(), fonte: F.texto, peso: 800, tamanho: Math.round(Math.min(W, H) * 0.045), cor: cor.destaque, x: M, y: M * 2.2, largura: W0, alinhamento: 'center', espacamento: 3 });
|
|
165
|
+
tela = k % 2 ? 'marca' : 'palavra';
|
|
166
|
+
cena.socos = [{ t: 0, forca: 0.14 }];
|
|
167
|
+
som(de, 'estalo', 0.55);
|
|
168
|
+
} else if (p.tipo === 'cta' && ultimo) {
|
|
169
|
+
// O fecho: a logo grande, a chamada e o link.
|
|
170
|
+
/* O fecho no escuro da marca, com o brilho do gradiente atrás: a logo
|
|
171
|
+
tem as cores do gradiente e sumia sobre ele. */
|
|
172
|
+
const R = Math.round(Math.max(W, H) * 0.9);
|
|
173
|
+
els.push({ tipo: 'forma', forma: 'circulo', largura: R, altura: R, x: Math.round(W / 2 - R / 2), y: Math.round(H * 0.4 - R / 2), cor: grad[1], opacidade: 0.4,
|
|
174
|
+
anima: { desfoque: [{ t: 0, v: Math.round(R * 0.2) }], escala: [{ t: 0, v: 0.7 }, { t: 1, v: 1.1, e: 'suave' }] } });
|
|
175
|
+
const yT = H * 0.26;
|
|
176
|
+
const hT = bater(els, p.titulo || falado, { y: yT, max: deitado ? H * 0.14 : W * 0.14, linhas: 3, corTexto: '#FFFFFF', destaque: cor.destaque });
|
|
177
|
+
if (p.link) {
|
|
178
|
+
const fl = Math.round(Math.min(W, H) * 0.055);
|
|
179
|
+
const wP = Math.min(W0, String(p.link).length * fl * 0.62 + fl * 2.4);
|
|
180
|
+
// O botão vem DEPOIS do título, com folga — nunca encostado nele.
|
|
181
|
+
const yP = Math.round(yT + hT * 1.08 + H * 0.04);
|
|
182
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: Math.round((W - wP) / 2), y: yP, largura: Math.round(wP), altura: Math.round(fl * 2.1), cor: cor.destaque, raio: Math.round(fl * 1.05),
|
|
183
|
+
anima: { escala: [{ t: 0.2, v: 0 }, { t: 0.45, v: 1.1, e: 'saida' }, { t: 0.6, v: 1 }] } });
|
|
184
|
+
els.push({ tipo: 'texto', texto: String(p.link), fonte: F.texto, peso: 800, tamanho: fl, cor: cor.sobreDestaque, x: Math.round(W / 2), y: Math.round(yP + fl * 1.05), ancora: 'centro',
|
|
185
|
+
anima: { opacidade: [{ t: 0.3, v: 0 }, { t: 0.45, v: 1 }] } });
|
|
186
|
+
}
|
|
187
|
+
logoTopo(els, true);
|
|
188
|
+
cena.tremor = [{ de: 0, ate: 0.4, forca: 20 }];
|
|
189
|
+
cena.flashes = [0];
|
|
190
|
+
som(de, 'impacto', 0.9);
|
|
191
|
+
tela = 'marca';
|
|
192
|
+
} else {
|
|
193
|
+
// O rodízio das três telas, com a primeira batida sempre na palavra.
|
|
194
|
+
const ciclo = ['palavra', 'foto', 'marca', 'foto'];
|
|
195
|
+
tela = k === 0 ? (p.tipo === 'imagem' || i === 0 ? 'foto' : 'palavra') : ciclo[(nBatida + k) % ciclo.length];
|
|
196
|
+
const semNaoE = (t) => String(t || '').replace(/^\s*(n[aã]o\s+[ée]\s+|[ée]\s+)/i, '');
|
|
197
|
+
const texto = p.tipo === 'contraste' ? (k < bs.length / 2 ? `não é ${semNaoE(p.nao)}` : semNaoE(p.sim))
|
|
198
|
+
: (ultimo && p.titulo && k > 0 ? p.titulo : falado);
|
|
199
|
+
if (tela === 'foto' && fotos.length) {
|
|
200
|
+
fotoCheia(els, proxFoto());
|
|
201
|
+
bater(els, texto, { y: H * (deitado ? 0.56 : 0.6), max: deitado ? H * 0.12 : W * 0.13, linhas: 3 });
|
|
202
|
+
if (k % 2 === 0) cena.flashes = [0];
|
|
203
|
+
} else if (tela === 'marca') {
|
|
204
|
+
cena.fundo = { tipo: 'gradiente', cores: [grad[0], grad[1]], angulo: 135 };
|
|
205
|
+
const temE = emoji(els, texto, { y: H * (deitado ? 0.26 : 0.28) });
|
|
206
|
+
bater(els, texto, { y: temE ? H * (deitado ? 0.46 : 0.44) : null, corTexto: '#FFFFFF', destaque: cor.fundo, max: deitado ? H * 0.16 : W * 0.16 });
|
|
207
|
+
} else {
|
|
208
|
+
tela = 'palavra';
|
|
209
|
+
const temE = emoji(els, texto, { y: H * (deitado ? 0.25 : 0.27) });
|
|
210
|
+
bater(els, texto, { y: temE ? H * (deitado ? 0.45 : 0.43) : null, glitch: p.tipo === 'contraste' || k % 3 === 0,
|
|
211
|
+
girar: k % 2 ? -2 : 2, corTexto: p.tipo === 'contraste' && k < bs.length / 2 ? 'rgba(255,255,255,0.55)' : cor.tinta });
|
|
212
|
+
cena.socos = [{ t: 0, forca: 0.2 }];
|
|
213
|
+
}
|
|
214
|
+
if (k === 0) cena.tremor = [{ de: 0, ate: 0.35, forca: 18 }];
|
|
215
|
+
som(de, tela === 'foto' ? 'whoosh' : 'estalo', tela === 'foto' ? 0.5 : 0.5);
|
|
216
|
+
}
|
|
217
|
+
if (!(p.tipo === 'cta' && ultimo)) logoTopo(els);
|
|
218
|
+
/* A FAIXA: o subtítulo da parte (é onde moram a data e o local) em
|
|
219
|
+
letra pequena e espaçada no alto — o anúncio não pode esconder a data. */
|
|
220
|
+
if (p.sub && p.tipo !== 'numero' && !(p.tipo === 'lista' || p.tipo === 'passos')) {
|
|
221
|
+
const fk = Math.round(Math.min(W, H) * 0.042);
|
|
222
|
+
els.push({ tipo: 'forma', forma: 'retangulo', x: M, y: Math.round(H * (deitado ? 0.14 : 0.1)), largura: W0, altura: Math.round(fk * 2), cor: cor.destaque, raio: Math.round(fk),
|
|
223
|
+
anima: { escalaX: [{ t: 0.05, v: 0 }, { t: 0.3, v: 1, e: 'saida' }] }, origem: 'center' });
|
|
224
|
+
// Com largura, e não pela âncora: sem largura o texto quebrava na metade da tela.
|
|
225
|
+
const tk = caber(String(p.sub).toUpperCase(), { largura: W0 * 0.78, linhas: 1, max: fk, fonte: F.texto }).tamanho;
|
|
226
|
+
els.push({ tipo: 'texto', texto: String(p.sub).toUpperCase(), fonte: F.texto, peso: 800, tamanho: tk, entrelinha: 1,
|
|
227
|
+
cor: cor.sobreDestaque === '#FFFFFF' ? '#FFFFFF' : '#121218', x: M, largura: W0, alinhamento: 'center', y: Math.round(H * (deitado ? 0.14 : 0.1) + fk - tk * 0.5), espacamento: 2,
|
|
228
|
+
anima: { opacidade: [{ t: 0.2, v: 0 }, { t: 0.35, v: 1 }] } });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* Uma transição curta a cada tanto; o resto é corte seco — é o
|
|
232
|
+
corte que dá o ritmo. */
|
|
233
|
+
const nT = nBatida++;
|
|
234
|
+
const trans = nT === 0 ? 'corte' : (nT % 4 === 1 ? 'chicote' : nT % 4 === 3 ? 'soco' : 'corte');
|
|
235
|
+
const cruz = trans === 'corte' ? 0 : 0.16;
|
|
236
|
+
if (trans === 'chicote') som(de - 0.08, 'whoosh', 0.55);
|
|
237
|
+
cena._base = Math.max(0.3, ate - de);
|
|
238
|
+
cena.transicao = { tipo: trans, duracao: cruz };
|
|
239
|
+
cenas.push(cena);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Cada batida fica no ar até a seguinte terminar de entrar (o cruzamento é da que entra).
|
|
244
|
+
cenas.forEach((c, k) => {
|
|
245
|
+
c.duracao = r2(c._base + (cenas[k + 1] ? cenas[k + 1].transicao.duracao : 0));
|
|
246
|
+
delete c._base;
|
|
247
|
+
c.camera = { zoom: [{ t: 0, v: 1 }, { t: c.duracao, v: 1.06, e: 'linear' }] };
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const audio = { efeitos };
|
|
251
|
+
if (narracao) audio.narracao = narracao;
|
|
252
|
+
if (trilha) audio.trilha = { src: trilha, volume: 0.17, loop: true };
|
|
253
|
+
return {
|
|
254
|
+
ok: true,
|
|
255
|
+
plano: {
|
|
256
|
+
formato: { largura: W, altura: H, fps: 30 }, fundoGeral: cor.fundo, cenas, audio,
|
|
257
|
+
acabamento: { grao: 0.07, vinheta: 0.55, luz: [grad[0], cor.destaque], luzForca: 0.16 },
|
|
258
|
+
},
|
|
259
|
+
resumo: { cenas: cenas.length, efeitos: efeitos.length, musica: Boolean(trilha), voz: Boolean(narracao), estilo: 'impacto' },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = { montarImpacto, batidas };
|
package/lib/estudio/index.js
CHANGED
|
@@ -43,6 +43,7 @@ const slides = require('./slides.js');
|
|
|
43
43
|
const edicao = require('./edicao.js');
|
|
44
44
|
const composicao = require('./composicao.js');
|
|
45
45
|
const acervoSite = require('./acervo-site.js');
|
|
46
|
+
const impacto = require('./impacto.js');
|
|
46
47
|
|
|
47
48
|
const PY = path.join(__dirname, '..', '..', 'studio');
|
|
48
49
|
|
|
@@ -250,6 +251,7 @@ async function roteiroDoServidor(p, args) {
|
|
|
250
251
|
fotos: (acervo.fotos || []).length ? `${acervo.fotos.length} fotos reais` : '',
|
|
251
252
|
site: acervo.site ? new URL(acervo.site).hostname.replace(/^www\./, '') : '',
|
|
252
253
|
rascunho: typeof args.roteiro === 'string' ? args.roteiro : '',
|
|
254
|
+
estilo: args.estilo || (/an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i.test(pedido) ? 'impacto' : undefined),
|
|
253
255
|
}, cfg.token);
|
|
254
256
|
if (!r || !r.ok) return { ok: false, error: (r && r.erro) || 'o roteirista não respondeu.' };
|
|
255
257
|
return { ok: true, roteiro: { ...r.roteiro, marca: (args.roteiro && args.roteiro.marca) || p.marca || undefined } };
|
|
@@ -306,6 +308,8 @@ async function ferramentaPlano(args) {
|
|
|
306
308
|
return { ok: false, error: 'o roteiro virou um plano com problemas (defeito do PrimoCode, não seu).', problemas: conferida.problemas.slice(0, 12) };
|
|
307
309
|
}
|
|
308
310
|
projeto.salvarPlano(args.id, r.plano, args.motivo || 'montado do roteiro');
|
|
311
|
+
// O roteiro fica guardado: ajustar uma frase é mandar o roteiro de novo, não reescrever tudo.
|
|
312
|
+
try { fs.writeFileSync(path.join(p.pasta, 'roteiro.json'), JSON.stringify(args.roteiro, null, 2)); } catch { /* segue */ }
|
|
309
313
|
projeto.salvar(args.id, { duracao: conferida.duracao });
|
|
310
314
|
return {
|
|
311
315
|
ok: true, duracao: conferida.duracao, ...r.resumo, resumo: plano.resumo(r.plano),
|
|
@@ -706,17 +710,20 @@ function alinharPartes(lista, palavras, total) {
|
|
|
706
710
|
}
|
|
707
711
|
return lista.map((_, i) => {
|
|
708
712
|
const a = ws[inicios[i]], b = ws[(inicios[i + 1] || ws.length) - 1];
|
|
709
|
-
|
|
713
|
+
// As palavras da parte vão junto: o estilo impacto corta no tempo delas.
|
|
714
|
+
const palavras = ws.slice(inicios[i], inicios[i + 1] || ws.length).map(({ de, ate, texto }) => ({ de, ate, texto }));
|
|
715
|
+
return { de: i === 0 ? 0 : (a ? a.de : 0), ate: b ? b.ate : total, palavras };
|
|
710
716
|
});
|
|
711
717
|
}
|
|
712
718
|
|
|
713
|
-
async function narrarComGemini(lista, mp3, { voz } = {}) {
|
|
719
|
+
async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
|
|
714
720
|
try {
|
|
715
721
|
const cfg = require('../config').loadConfig();
|
|
716
722
|
const api = require('../api');
|
|
717
723
|
const r = await api.request(cfg.server, '/api/voz', {
|
|
718
724
|
texto: lista.map((x) => x.texto).join('\n\n'),
|
|
719
725
|
voz: VOZ_GEMINI[String(voz || '').toLowerCase()] || voz || 'Charon',
|
|
726
|
+
...(direcao ? { direcao } : {}),
|
|
720
727
|
}, cfg.token);
|
|
721
728
|
if (!r || !r.ok) return { ok: false, error: (r && r.erro) || 'sem voz no servidor' };
|
|
722
729
|
const pcm = mp3.replace(/\.mp3$/, '.pcm');
|
|
@@ -754,6 +761,11 @@ async function prepararRoteiro(p, args) {
|
|
|
754
761
|
if (roteiro.marca.logo && !existe(roteiro.marca.logo)) roteiro.marca.logo = (p.marca && existe(p.marca.logo)) ? p.marca.logo : undefined;
|
|
755
762
|
} else if (p.marca) roteiro.marca = p.marca;
|
|
756
763
|
const partes = roteiro.partes || [];
|
|
764
|
+
/* O ESTILO: "impacto" (anúncio, corte a cada batida) ou "limpo" (uma cena
|
|
765
|
+
por parte). Pedido de anúncio, reels, divulgação ou venda já vem em
|
|
766
|
+
impacto — é o que se espera de uma peça dessas em 2026. */
|
|
767
|
+
const estilo = args.estilo || roteiro.estilo
|
|
768
|
+
|| (/an[uú]nci|reels?|divulg|promo|lan[çc]a|vend|campanha|convite|inscri/i.test(`${p.pedido || ''} ${args.pedido || ''}`) ? 'impacto' : 'limpo');
|
|
757
769
|
const modo = (args.modo || roteiro.modo || (p.formato === 'slides' || p.formato === 'arte' ? 'slides' : 'video'));
|
|
758
770
|
const [largura, altura] = PROPORCOES[roteiro.proporcao || args.proporcao] || (modo === 'slides' ? PROPORCOES['16:9'] : PROPORCOES['16:9']);
|
|
759
771
|
|
|
@@ -807,7 +819,9 @@ async function prepararRoteiro(p, args) {
|
|
|
807
819
|
fs.writeFileSync(arq, JSON.stringify(lista));
|
|
808
820
|
const mp3 = path.join(p.pasta, 'midia', `narracao-${Date.now().toString(36)}.mp3`);
|
|
809
821
|
// A voz de gente primeiro (Gemini, pelo servidor); o Edge é a reserva.
|
|
810
|
-
let v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz
|
|
822
|
+
let v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz,
|
|
823
|
+
direcao: estilo === 'impacto' ? 'Leia em português do Brasil como locutor de comercial agressivo e vendedor: energia alta, ritmo rápido, '
|
|
824
|
+
+ 'voz encorpada, ênfase forte nas palavras de impacto, pausas curtas e secas entre as frases, sorrindo na voz.' : undefined });
|
|
811
825
|
if (!v.ok) v = await rodarPython('narrar.py', ['roteiro', '--arquivo', arq, '--saida', mp3, '--voz', String(args.voz || roteiro.voz || 'thalita')], { timeout: 10 * 60000 });
|
|
812
826
|
if (!v.ok) return v;
|
|
813
827
|
tempos = v.partes;
|
|
@@ -815,13 +829,29 @@ async function prepararRoteiro(p, args) {
|
|
|
815
829
|
projeto.anotar(p.id, 'narracao', `${v.segundos}s em ${partes.length} partes, voz ${v.voz}`);
|
|
816
830
|
sons = await edicao.prepararSons(p.pasta, video);
|
|
817
831
|
try {
|
|
818
|
-
const b = await assets.procurarSom(roteiro.musica || args.trilha || 'inspiring corporate background', { quantas: 6, duracaoMin: 45 });
|
|
832
|
+
const b = await assets.procurarSom(roteiro.musica || args.trilha || (estilo === 'impacto' ? 'epic energetic trailer' : 'inspiring corporate background'), { quantas: 6, duracaoMin: 45 });
|
|
819
833
|
for (const item of (b.ok ? b.sons : [])) {
|
|
820
834
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha' });
|
|
821
835
|
if (d.ok) { trilha = d.relativo; break; }
|
|
822
836
|
}
|
|
823
837
|
} catch { /* sem música */ }
|
|
824
838
|
}
|
|
839
|
+
if (estilo === 'impacto' && modo === 'video') {
|
|
840
|
+
// Os emojis das palavras FALADAS também (a batida mostra a fala).
|
|
841
|
+
const faladas = tempos.flatMap((t) => (t.palavras || []).map((w) => w.texto));
|
|
842
|
+
for (const nome of new Set(faladas.map((w) => edicao.iconeDe(w)).filter(Boolean))) {
|
|
843
|
+
if (iconesOk.has(nome)) continue;
|
|
844
|
+
try { const r = await assets.icone(nome); if (r && r.ok) iconesOk.add(nome); } catch { /* sem */ }
|
|
845
|
+
}
|
|
846
|
+
const fotos = [
|
|
847
|
+
...acervo.fotos.map((f) => ({ src: f.src, tipo: 'imagem' })),
|
|
848
|
+
...(acervo.videos || []).map((v) => ({ src: v.src, tipo: 'video' })),
|
|
849
|
+
...Object.values(imagens),
|
|
850
|
+
];
|
|
851
|
+
const r = impacto.montarImpacto({ roteiro, largura, altura, tempos, narracao, sons, trilha, fotos, iconesOk });
|
|
852
|
+
if (r.ok) projeto.anotar(p.id, 'plano', `estilo impacto: ${r.resumo.cenas} batidas`);
|
|
853
|
+
return r;
|
|
854
|
+
}
|
|
825
855
|
return composicao.comporRoteiro({ roteiro, modo, largura, altura, tempos, narracao, sons, trilha, imagens, iconesOk, fundos });
|
|
826
856
|
}
|
|
827
857
|
|
|
@@ -959,6 +989,36 @@ async function ferramentaRender(args, ctx = {}) {
|
|
|
959
989
|
|
|
960
990
|
// ── estudio_site ─────────────────────────────────────────────────────────
|
|
961
991
|
|
|
992
|
+
/* A PALETA INTEIRA do site, não só a primeira cor. O fundo é o neutro
|
|
993
|
+
escuro que o site mais usa (#121218 na Paris Group); o destaque é a cor da
|
|
994
|
+
marca que mais aparece sobre ele (o laranja, não o azul escuro que some); o
|
|
995
|
+
apoio é a seguinte. A fonte serifada, quando existe, vai para citação. */
|
|
996
|
+
function paletaDoSite(r) {
|
|
997
|
+
const hexRgb = (h) => { const n = parseInt(String(h).slice(1).padEnd(6, '0').slice(0, 6), 16); return [n >> 16, (n >> 8) & 255, n & 255]; };
|
|
998
|
+
const luz = (h) => { const [a, b, c] = hexRgb(h); return (0.299 * a + 0.587 * b + 0.114 * c) / 255; };
|
|
999
|
+
const sat = (h) => { const v = hexRgb(h); return (Math.max(...v) - Math.min(...v)) / 255; };
|
|
1000
|
+
const neutras = ((r.cores && (r.cores.neutras || r.cores.fundo)) || []).map((x) => x.cor || x).filter((c) => /^#/.test(c));
|
|
1001
|
+
const fundo = neutras.find((c) => luz(c) < 0.15 && luz(c) > 0.02) || neutras.find((c) => luz(c) < 0.2) || undefined;
|
|
1002
|
+
const base = fundo || '#121218';
|
|
1003
|
+
const marca = ((r.cores && r.cores.marca) || []).map((x) => x.cor).filter(Boolean);
|
|
1004
|
+
const nota = (c) => sat(c) * 1.2 + Math.abs(luz(c) - luz(base));
|
|
1005
|
+
const ordenadas = [...marca].sort((a, b) => nota(b) - nota(a));
|
|
1006
|
+
const destaque = ordenadas[0];
|
|
1007
|
+
// O apoio é OUTRA cor, de outro matiz: dois laranjas não fazem paleta.
|
|
1008
|
+
const matiz = (h) => { const [a, b, c] = hexRgb(h).map((v) => v / 255); const mx = Math.max(a, b, c), mn = Math.min(a, b, c), d = mx - mn;
|
|
1009
|
+
if (!d) return 0; const x = mx === a ? ((b - c) / d) % 6 : mx === b ? (c - a) / d + 2 : (a - b) / d + 4; return (x * 60 + 360) % 360; };
|
|
1010
|
+
const longe = (a, b) => { const d = Math.abs(matiz(a) - matiz(b)); return Math.min(d, 360 - d) > 60; };
|
|
1011
|
+
const apoio = ordenadas.find((c) => c !== destaque && longe(c, destaque) && sat(c) > 0.3) || ordenadas[1];
|
|
1012
|
+
const fontes = ((r.fontes && r.fontes.declaradas) || []).map((x) => x.fonte).filter((f) => !/mono|menlo|courier|system|arial|helvetica/i.test(f));
|
|
1013
|
+
return {
|
|
1014
|
+
cores: { ...(fundo ? { fundo } : {}), ...(destaque ? { destaque } : {}), ...(apoio ? { apoio } : {}) },
|
|
1015
|
+
fontes: {
|
|
1016
|
+
...(fontes[0] ? { titulo: fontes.find((f) => !/serif/i.test(f) || /sans/i.test(f)) || fontes[0], texto: fontes.find((f) => !/serif/i.test(f) || /sans/i.test(f)) || fontes[0] } : {}),
|
|
1017
|
+
...(fontes.find((f) => /serif/i.test(f) && !/sans/i.test(f)) ? { serifa: fontes.find((f) => /serif/i.test(f) && !/sans/i.test(f)) } : {}),
|
|
1018
|
+
},
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
962
1022
|
async function ferramentaSite(args) {
|
|
963
1023
|
let alvo = String(args.url || args.site || '').trim();
|
|
964
1024
|
|
|
@@ -989,10 +1049,7 @@ async function ferramentaSite(args) {
|
|
|
989
1049
|
const html = r.html;
|
|
990
1050
|
delete r.html; // o HTML cru não vai para o modelo
|
|
991
1051
|
if (r.ok) {
|
|
992
|
-
const marcaLida =
|
|
993
|
-
cores: { destaque: ((r.cores && r.cores.marca) || [])[0] ? r.cores.marca[0].cor : undefined },
|
|
994
|
-
fontes: { titulo: ((r.fontes && r.fontes.declaradas) || [])[0] ? r.fontes.declaradas[0].fonte : undefined },
|
|
995
|
-
};
|
|
1052
|
+
const marcaLida = paletaDoSite(r);
|
|
996
1053
|
ultimoSite = { url: r.url, html, quando: Date.now(), marca: marcaLida };
|
|
997
1054
|
}
|
|
998
1055
|
if (r.ok && args.id && args.logo !== false) {
|
|
@@ -1025,6 +1082,15 @@ async function ferramentaSite(args) {
|
|
|
1025
1082
|
buscar: identidade.buscar, baixar: assets.baixar,
|
|
1026
1083
|
});
|
|
1027
1084
|
const marca = { ...ultimoSite.marca, ...(r.logo && r.logo.ok ? { logo: r.logo.src } : {}) };
|
|
1085
|
+
/* O GRADIENTE da marca sai da própria logo, quando ela é SVG com
|
|
1086
|
+
gradiente (a da Paris Group vai de #2E81ED a #9567DB). */
|
|
1087
|
+
try {
|
|
1088
|
+
if (marca.logo && /\.svg$/i.test(marca.logo)) {
|
|
1089
|
+
const svg = fs.readFileSync(path.join(p.pasta, marca.logo), 'utf8');
|
|
1090
|
+
const stops = [...new Set((svg.match(/stop-color=["'](#[0-9a-f]{3,6})["']/gi) || []).map((x) => x.match(/#[0-9a-f]{3,6}/i)[0].toUpperCase()))];
|
|
1091
|
+
if (stops.length >= 2) marca.gradiente = stops.slice(0, 2);
|
|
1092
|
+
}
|
|
1093
|
+
} catch { /* sem gradiente */ }
|
|
1028
1094
|
projeto.salvar(args.id, { acervo: { fotos: a.fotos, videos: a.videos, fatos: a.fatos, site: r.url }, marca });
|
|
1029
1095
|
projeto.anotar(args.id, 'midia', `acervo do site: ${a.fotos.length} fotos, ${a.videos.length} vídeos, ${a.paginas.length} páginas lidas`);
|
|
1030
1096
|
let orcamento = 4200;
|
package/lib/estudio/plano.js
CHANGED
|
@@ -51,7 +51,7 @@ const path = require('path');
|
|
|
51
51
|
const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente']);
|
|
52
52
|
const FORMAS = new Set(['retangulo', 'circulo', 'linha', 'poligono', 'traco']);
|
|
53
53
|
const AJUSTES = new Set(['cover', 'contain', 'preencher']);
|
|
54
|
-
const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph']);
|
|
54
|
+
const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph', 'chicote', 'soco', 'flash']);
|
|
55
55
|
|
|
56
56
|
/* As propriedades que a animação por quadro-chave alcança. Cada uma é um
|
|
57
57
|
número no tempo; a curva entre dois quadros é a suavização declarada.
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -283,6 +283,12 @@ function Elemento({ el, t, dados }) {
|
|
|
283
283
|
WebkitBackgroundClip: el.gradienteTexto ? 'text' : undefined,
|
|
284
284
|
WebkitTextFillColor: el.gradienteTexto ? 'transparent' : undefined,
|
|
285
285
|
fontStyle: el.italico ? 'italic' : undefined,
|
|
286
|
+
WebkitTextStroke: el.contorno || undefined,
|
|
287
|
+
// O GLITCH: o texto se parte em vermelho e ciano por um instante.
|
|
288
|
+
textShadow: el.glitch && tLocal < (el.glitch.dur || 0.35)
|
|
289
|
+
? (() => { const f = Math.round(t * 30); const d = (Math.sin(f * 7.1) * 0.5 + 0.5) * (el.glitch.forca || 10) * (1 - tLocal / (el.glitch.dur || 0.35));
|
|
290
|
+
return d.toFixed(1) + 'px 0 #ff2d55, ' + (-d).toFixed(1) + 'px 0 #00e5ff'; })()
|
|
291
|
+
: (el.sombraTexto || undefined),
|
|
286
292
|
textDecoration: el.riscado ? 'line-through' : undefined,
|
|
287
293
|
textDecorationThickness: el.riscado ? '0.08em' : undefined,
|
|
288
294
|
wordBreak: 'normal', overflowWrap: 'break-word',
|
|
@@ -323,7 +329,7 @@ function Elemento({ el, t, dados }) {
|
|
|
323
329
|
startFrom={el.deOrigem ? Math.round(el.deOrigem * dados.fps) : undefined}
|
|
324
330
|
endAt={el.ateOrigem ? Math.round(el.ateOrigem * dados.fps) : undefined}
|
|
325
331
|
volume={el.volume ?? 0}
|
|
326
|
-
style={{ width: '100%', height: '100%', objectFit: el.ajuste || 'cover' }}
|
|
332
|
+
style={{ width: '100%', height: '100%', objectFit: el.ajuste || 'cover', objectPosition: el.foco || 'center' }}
|
|
327
333
|
/>
|
|
328
334
|
</div>
|
|
329
335
|
);
|
|
@@ -419,10 +425,26 @@ export function Cena({ cena, dados, tFixo }) {
|
|
|
419
425
|
const t = tFixo != null ? tFixo : frame / fps;
|
|
420
426
|
|
|
421
427
|
const cam = animado(cena.camera, t);
|
|
428
|
+
/* O IMPACTO: tremor (a câmera treme e assenta), soco (o zoom que bate e
|
|
429
|
+
volta) e flash (o branco que estoura e some). O tremor é pseudoaleatório
|
|
430
|
+
pelo QUADRO, então o mesmo render sai sempre igual. */
|
|
431
|
+
const acaso = (n) => { const x = Math.sin(n * 12.9898 + 78.233) * 43758.5453; return x - Math.floor(x); };
|
|
432
|
+
let tx = 0, ty = 0, tr = 0, soco = 1;
|
|
433
|
+
for (const w of cena.tremor || []) {
|
|
434
|
+
if (t < w.de || t > w.ate) continue;
|
|
435
|
+
const k = (w.forca || 14) * (1 - (t - w.de) / Math.max(0.01, w.ate - w.de));
|
|
436
|
+
tx += (acaso(frame) - 0.5) * 2 * k; ty += (acaso(frame + 91) - 0.5) * 2 * k; tr += (acaso(frame + 37) - 0.5) * k * 0.08;
|
|
437
|
+
}
|
|
438
|
+
for (const s0 of cena.socos || []) {
|
|
439
|
+
if (t >= s0.t) soco *= 1 + (s0.forca || 0.18) * Math.exp(-14 * (t - s0.t));
|
|
440
|
+
}
|
|
441
|
+
let flash = 0;
|
|
442
|
+
for (const f of cena.flashes || []) if (t >= f && t < f + 0.25) flash = Math.max(flash, Math.exp(-18 * (t - f)));
|
|
422
443
|
const camTransform = [
|
|
423
444
|
cam.x != null || cam.y != null ? 'translate(' + (-(cam.x || 0)) + 'px,' + (-(cam.y || 0)) + 'px)' : '',
|
|
424
|
-
|
|
425
|
-
cam.
|
|
445
|
+
tx || ty ? 'translate(' + tx + 'px,' + ty + 'px)' : '',
|
|
446
|
+
cam.zoom != null || soco !== 1 ? 'scale(' + ((cam.zoom ?? 1) * soco) + ')' : '',
|
|
447
|
+
cam.rotacao != null || tr ? 'rotate(' + ((cam.rotacao || 0) + tr) + 'deg)' : '',
|
|
426
448
|
].filter(Boolean).join(' ');
|
|
427
449
|
|
|
428
450
|
return (
|
|
@@ -451,6 +473,7 @@ export function Cena({ cena, dados, tFixo }) {
|
|
|
451
473
|
<Elemento key={i} el={el} t={t} dados={dados} />
|
|
452
474
|
))}
|
|
453
475
|
</AbsoluteFill>
|
|
476
|
+
{flash > 0.01 && <AbsoluteFill style={{ backgroundColor: cena.corFlash || '#fff', opacity: flash * 0.85 }} />}
|
|
454
477
|
</AbsoluteFill>
|
|
455
478
|
);
|
|
456
479
|
}
|
|
@@ -474,6 +497,11 @@ function Transicao({ tipo, progresso, children }) {
|
|
|
474
497
|
else if (tipo === 'giro') estilo = { transform: 'rotateY(' + (1 - p) * 35 + 'deg) scale(' + (0.9 + 0.1 * p) + ')', opacity: p };
|
|
475
498
|
else if (tipo === 'mascara') estilo = { clipPath: 'circle(' + (p * 75) + '% at 50% 50%)' };
|
|
476
499
|
else if (tipo === 'morph') estilo = { opacity: p, filter: 'blur(' + (1 - p) * 14 + 'px)', transform: 'scale(' + (1.06 - 0.06 * p) + ')' };
|
|
500
|
+
// O CHICOTE: a cena chega rasgando de lado, borrada de velocidade.
|
|
501
|
+
else if (tipo === 'chicote') estilo = { transform: 'translateX(' + (1 - p) * 70 + '%) skewX(' + (1 - p) * -12 + 'deg)', filter: 'blur(' + (1 - p) * 40 + 'px)' };
|
|
502
|
+
// O SOCO: a cena entra grande e bate no lugar.
|
|
503
|
+
else if (tipo === 'soco') estilo = { transform: 'scale(' + (1.35 - 0.35 * p) + ')', opacity: Math.min(1, p * 3) };
|
|
504
|
+
else if (tipo === 'flash') estilo = { opacity: Math.min(1, p * 2), filter: 'brightness(' + (1 + (1 - p) * 3) + ')' };
|
|
477
505
|
return <AbsoluteFill style={estilo}>{children}</AbsoluteFill>;
|
|
478
506
|
}
|
|
479
507
|
|
|
@@ -497,14 +525,24 @@ export const Video = () => {
|
|
|
497
525
|
const frame = useCurrentFrame();
|
|
498
526
|
const dados = { fps, icones };
|
|
499
527
|
|
|
528
|
+
/* O cruzamento é da cena que ENTRA: ela começa "cruz" quadros antes de a
|
|
529
|
+
anterior acabar, e as duas ficam no ar juntas. Contar pela que sai deixava
|
|
530
|
+
a que entra começando sobre o preto (medido em 25/09: o "soco" abria com
|
|
531
|
+
um quadro preto). */
|
|
532
|
+
const cruzDe = (cena, i) => {
|
|
533
|
+
if (!cena || i === 0) return 0;
|
|
534
|
+
const t = cena.transicao || {};
|
|
535
|
+
const tipo = t.tipo || t || 'corte';
|
|
536
|
+
return tipo === 'corte' ? 0 : Math.round((t.duracao ?? 0.4) * fps);
|
|
537
|
+
};
|
|
500
538
|
let inicio = 0;
|
|
501
539
|
const pedacos = plano.cenas.map((cena, i) => {
|
|
502
540
|
const dur = Math.max(1, Math.round(cena.duracao * fps));
|
|
503
541
|
const t = cena.transicao || {};
|
|
504
542
|
const tipo = t.tipo || t || 'corte';
|
|
505
|
-
const cruz =
|
|
543
|
+
const cruz = cruzDe(cena, i);
|
|
506
544
|
const de = inicio;
|
|
507
|
-
inicio += dur -
|
|
545
|
+
inicio += dur - cruzDe(plano.cenas[i + 1], i + 1);
|
|
508
546
|
return { cena, de, dur, tipo, cruz, i };
|
|
509
547
|
});
|
|
510
548
|
|
|
@@ -524,6 +562,26 @@ export const Video = () => {
|
|
|
524
562
|
);
|
|
525
563
|
})}
|
|
526
564
|
|
|
565
|
+
{/* O ACABAMENTO: granulação de filme, vinheta e a luz da marca passando.
|
|
566
|
+
É o que tira a cara de apresentação e dá a cara de peça de 2026. */}
|
|
567
|
+
{plano.acabamento && plano.acabamento.luz && (
|
|
568
|
+
<AbsoluteFill style={{ pointerEvents: 'none', mixBlendMode: 'screen', opacity: plano.acabamento.luzForca ?? 0.22,
|
|
569
|
+
background: 'radial-gradient(circle at ' + (50 + 40 * Math.sin(frame / 38)) + '% ' + (30 + 25 * Math.cos(frame / 51)) + '%, '
|
|
570
|
+
+ plano.acabamento.luz[0] + ' 0%, transparent 45%), radial-gradient(circle at ' + (50 - 40 * Math.sin(frame / 44)) + '% '
|
|
571
|
+
+ (75 - 20 * Math.sin(frame / 33)) + '%, ' + (plano.acabamento.luz[1] || plano.acabamento.luz[0]) + ' 0%, transparent 40%)' }} />
|
|
572
|
+
)}
|
|
573
|
+
{plano.acabamento && plano.acabamento.vinheta && (
|
|
574
|
+
<AbsoluteFill style={{ pointerEvents: 'none', background: 'radial-gradient(ellipse at center, transparent 55%, rgba(0,0,0,' + plano.acabamento.vinheta + ') 100%)' }} />
|
|
575
|
+
)}
|
|
576
|
+
{plano.acabamento && plano.acabamento.grao && (
|
|
577
|
+
<AbsoluteFill style={{ pointerEvents: 'none', opacity: plano.acabamento.grao, mixBlendMode: 'overlay' }}>
|
|
578
|
+
<svg width="100%" height="100%">
|
|
579
|
+
<filter id="grao"><feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" seed={frame % 60} /></filter>
|
|
580
|
+
<rect width="100%" height="100%" filter="url(#grao)" />
|
|
581
|
+
</svg>
|
|
582
|
+
</AbsoluteFill>
|
|
583
|
+
)}
|
|
584
|
+
|
|
527
585
|
{plano.audio && plano.audio.narracao && (
|
|
528
586
|
<Audio src={staticFile(plano.audio.narracao)} volume={plano.audio.volumeNarracao ?? 1} />
|
|
529
587
|
)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "9.8.0-beta.
|
|
3
|
+
"version": "9.8.0-beta.18",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/entrada.test.js && node test/transparencia.test.js && node test/permissao.test.js && node test/compatibilidade.test.js && node test/skills.test.js && node test/skills-de-fora.test.js && node test/skills-novas.test.js && node test/comandos.test.js && node test/estudio.test.js && node test/voz.test.js && node test/voz-conversa.test.js && node test/modo.test.js && node test/pasta.test.js && node test/projeto.test.js && node test/nome-projeto.test.js && node test/api-propria.test.js && node test/claude-engine.test.js && node test/codex.test.js && node test/conferir.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/seta.test.js && node test/effort.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/autonomia.test.js && node test/desfazer.test.js && node test/desfazer-git.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/pipeline.test.js && node test/app.test.js && node test/app-janela.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/empurrao.test.js && node test/terminal-novo.test.js && node test/markdown.test.js && node test/coerencia.test.js && node test/contexto.test.js && node test/perigo.test.js && node test/rapidez.test.js && node test/mascotes.test.js && node test/comandos-extra.test.js && node test/offline.test.js && node test/edicao.test.js && node test/composicao.test.js && node test/acervo-site.test.js && node test/impacto.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|