primocode 10.0.0 → 10.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/bin/primocode.js +88 -3
- package/lib/act.js +122 -20
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +6 -3
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -2
- package/lib/entrada.js +73 -9
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +63 -25
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +41 -10
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +1471 -0
- package/lib/estudio/hibrido.js +169 -0
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +366 -47
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +990 -0
- package/lib/estudio/kit/index.jsx +4 -1
- package/lib/estudio/kit/tres.jsx +375 -0
- package/lib/estudio/kit.js +9 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +27 -0
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +103 -14
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +183 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +51 -1
- package/lib/tools.js +53 -8
- package/lib/ui.js +41 -6
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +1 -1
- package/lib/video/filters.js +39 -6
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +30 -5
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +67 -14
- package/lib/video/texto.js +6 -4
- package/lib/video/timeline.js +9 -1
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +5 -4
- 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/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/voz_kokoro.py +98 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fluido.js — o estilo FLUIDO: uma ideia por batida, muito respiro, e nada
|
|
3
|
+
* de corte. O elemento de uma batida VIRA o da próxima.
|
|
4
|
+
*
|
|
5
|
+
* "Uma ideia por quadro, muito espaço vazio. Texto pequeno, caixa normal,
|
|
6
|
+
* sans limpa. Movimento contínuo: um elemento se transforma no próximo."
|
|
7
|
+
* — a referência de motion do Joel (05/10), os vídeos bons.
|
|
8
|
+
*
|
|
9
|
+
* Existe UMA forma que vive o vídeo inteiro (o "portador"): é o ponto da
|
|
10
|
+
* abertura, a pílula do título, o anel do número, a primeira pílula da lista,
|
|
11
|
+
* a linha que risca o "não", o cartão da citação e o primeiro círculo da
|
|
12
|
+
* marca no fim. Entre uma batida e outra ela muda de lugar, de tamanho, de
|
|
13
|
+
* raio e de preenchimento — o olho segue a forma, e o corte some.
|
|
14
|
+
*
|
|
15
|
+
* O texto entra palavra por palavra (stagger curto, ease-out forte, sem
|
|
16
|
+
* linear) e cada frase tem UM acento: uma palavra em serifa itálica na cor de
|
|
17
|
+
* acento. Fundo em gradiente grande e macio, de uma paleta curta (no máximo
|
|
18
|
+
* dois acentos e os neutros), clara ou escura.
|
|
19
|
+
*
|
|
20
|
+
* Proibido aqui, de propósito (é a lista do "cara de IA"): HUD, cantoneira,
|
|
21
|
+
* timecode, caixa na palavra-chave, neon, partícula e CAIXA ALTA.
|
|
22
|
+
*
|
|
23
|
+
* Puro: devolve a partitura (os testes olham) e o plano com UMA peça do kit
|
|
24
|
+
* (kit/fluido.jsx), que desenha a partitura quadro a quadro.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const { volumesDaTrilha } = require('./ritmo.js');
|
|
30
|
+
|
|
31
|
+
const FPS = 30;
|
|
32
|
+
const r2 = (n) => Math.round(n * 100) / 100;
|
|
33
|
+
const POR_PALAVRA = 0.3, MINIMO = 0.8; // o tempo de leitura: 0,3 s por palavra, nunca menos de 0,8 s
|
|
34
|
+
const STAGGER = 0.07; // entre uma palavra e a próxima (a que a voz não diz)
|
|
35
|
+
const ANTES = 0.12; // a batida troca isto antes da primeira palavra da frase
|
|
36
|
+
|
|
37
|
+
/* As paletas: um fundo em três tons (o gradiente), a tinta, a tinta suave e
|
|
38
|
+
NO MÁXIMO dois acentos (o da palavra e o brilho difuso). */
|
|
39
|
+
const PALETAS = {
|
|
40
|
+
noite: { escuro: true, fundo: ['#020615', '#061a5c', '#1240c9'], tinta: '#eef2ff', suave: '#9fb0d9', acentos: ['#8fb8ff', '#2f6bff'] },
|
|
41
|
+
breu: { escuro: true, fundo: ['#030304', '#0b0a12', '#2a2156'], tinta: '#f4f4f6', suave: '#8d8a9c', acentos: ['#b4a6ff', '#5b45ff'] },
|
|
42
|
+
lavanda: { escuro: false, fundo: ['#f5f4ff', '#e4e1ff', '#b9b2ff'], tinta: '#17153a', suave: '#6e6a92', acentos: ['#4a3ce0', '#8f86ff'] },
|
|
43
|
+
creme: { escuro: false, fundo: ['#fbf8f2', '#f2e8d8', '#e7c9a5'], tinta: '#1e1a15', suave: '#857a6c', acentos: ['#c2502a', '#f0a36b'] },
|
|
44
|
+
};
|
|
45
|
+
const ESCURAS = ['noite', 'breu'], CLARAS = ['lavanda', 'creme'];
|
|
46
|
+
|
|
47
|
+
const hash = (s) => { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; };
|
|
48
|
+
|
|
49
|
+
/** A paleta: a pedida pelo nome; senão o tema (claro/escuro); senão a semente. */
|
|
50
|
+
function paletaDe(roteiro, semente) {
|
|
51
|
+
const pedida = String(roteiro.paleta || '').toLowerCase();
|
|
52
|
+
const tema = String(roteiro.tema || '').toLowerCase();
|
|
53
|
+
const nome = PALETAS[pedida] ? pedida
|
|
54
|
+
: /claro|light/.test(tema) ? CLARAS[semente % 2]
|
|
55
|
+
: /escuro|dark/.test(tema) ? ESCURAS[semente % 2]
|
|
56
|
+
: Object.keys(PALETAS)[semente % 4];
|
|
57
|
+
const p = { nome, ...PALETAS[nome], acentos: [...PALETAS[nome].acentos] };
|
|
58
|
+
// A cor da marca vira o acento da palavra (e só ela: a paleta não cresce).
|
|
59
|
+
const dest = roteiro.marca && roteiro.marca.cores && roteiro.marca.cores.destaque;
|
|
60
|
+
if (/^#[0-9a-f]{6}$/i.test(dest || '')) p.acentos[0] = dest;
|
|
61
|
+
return p;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Caixa alta não entra: frase GRITADA vira caixa normal (sigla curta fica). */
|
|
65
|
+
function caixaNormal(t) {
|
|
66
|
+
const s = String(t || '').trim();
|
|
67
|
+
const letras = s.replace(/[^A-Za-zÀ-ÿ]/g, '');
|
|
68
|
+
if (letras.length < 4 || letras !== letras.toUpperCase()) return s;
|
|
69
|
+
const m = s.toLowerCase();
|
|
70
|
+
return m.charAt(0).toUpperCase() + m.slice(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const VAZIAS = new Set('a o as os um uma de da do das dos e é em no na nos nas que com por para pra se seu sua você ao à mais muito isso este esta não sim'.split(' '));
|
|
74
|
+
const limpa = (w) => w.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]/g, '');
|
|
75
|
+
|
|
76
|
+
/** A frase em palavras, com UM acento: o destaque pedido, ou a palavra de mais peso. */
|
|
77
|
+
function frase(texto, { destaque = '', semAcento = false } = {}) {
|
|
78
|
+
const ws = caixaNormal(texto).split(/\s+/).filter(Boolean);
|
|
79
|
+
let ac = -1;
|
|
80
|
+
if (!semAcento && ws.length > 1) {
|
|
81
|
+
const d = limpa(String(destaque).split(/\s+/)[0] || '');
|
|
82
|
+
if (d) ac = ws.findIndex((w) => limpa(w) === d);
|
|
83
|
+
if (ac < 0) {
|
|
84
|
+
// A palavra mais longa que não é vazia; no empate, a mais tarde (a frase termina nela).
|
|
85
|
+
let melhor = 0;
|
|
86
|
+
ws.forEach((w, i) => { const n = limpa(w).length; if (!VAZIAS.has(limpa(w)) && n >= 4 && n >= melhor) { melhor = n; ac = i; } });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return ws.map((t, i) => ({ t, acento: i === ac }));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const numeroDe = (v) => {
|
|
93
|
+
const m = String(v == null ? '' : v).match(/^(\D*)([\d.,]+)(.*)$/);
|
|
94
|
+
if (!m) return null;
|
|
95
|
+
const n = Number(m[2].replace(/\./g, '').replace(',', '.'));
|
|
96
|
+
return Number.isFinite(n) ? { antes: m[1], ate: n, depois: m[3], casas: /,\d/.test(m[2]) ? 1 : 0 } : null;
|
|
97
|
+
};
|
|
98
|
+
const itensDe = (p) => (p.itens || []).map((i) => (typeof i === 'string' ? i : i.texto || i.titulo || '')).filter(Boolean);
|
|
99
|
+
const leitura = (n) => Math.max(MINIMO, POR_PALAVRA * n);
|
|
100
|
+
|
|
101
|
+
/* A forma do portador em cada tipo de parte (kit/fluido.jsx dá o tamanho). */
|
|
102
|
+
const FORMA = { abertura: 'ponto', titulo: 'pilula', numero: 'anel', lista: 'pilula', passos: 'ponto', contraste: 'linha', citacao: 'cartao', cta: 'marca' };
|
|
103
|
+
const TIPO = { logo: 'abertura', imagem: 'titulo', produto: 'titulo', grafico: 'lista', cores: 'lista' };
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A partitura: as batidas, cada uma com as frases (e quando cada uma entra),
|
|
107
|
+
* a forma do portador e a transição. Sem áudio nem plano — é o que os testes olham.
|
|
108
|
+
*/
|
|
109
|
+
function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
110
|
+
const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object');
|
|
111
|
+
const semente = Number.isFinite(roteiro.semente) ? roteiro.semente : hash(JSON.stringify(partes.map((p) => [p.tipo, p.titulo, p.valor])));
|
|
112
|
+
const paleta = paletaDe(roteiro, semente);
|
|
113
|
+
const marca = roteiro.marca || {};
|
|
114
|
+
const temVoz = tempos.length === partes.length && tempos.every((t) => t && t.ate > t.de);
|
|
115
|
+
/* O comando do terminal: o link da cta, se for comando; senão o nome da marca. */
|
|
116
|
+
const link = (partes.find((p) => p.tipo === 'cta') || {}).link || '';
|
|
117
|
+
const cmd = /^(npm|npx|pnpm|yarn|pip|brew|curl|git|bun)\b/.test(link) ? link : String(marca.nome || 'app').toLowerCase().replace(/\s+/g, '-');
|
|
118
|
+
let comCartao = 0;
|
|
119
|
+
|
|
120
|
+
const batidas = partes.map((p, i) => {
|
|
121
|
+
const tipo = TIPO[p.tipo] || (FORMA[p.tipo] ? p.tipo : 'titulo');
|
|
122
|
+
const titulo = p.titulo || p.texto || '';
|
|
123
|
+
const fr = [];
|
|
124
|
+
const poe = (papel, texto, o = {}) => { if (String(texto || '').trim()) fr.push({ id: `${i}-${fr.length}`, papel, palavras: frase(texto, o) }); };
|
|
125
|
+
const b = { i, tipo, forma: FORMA[tipo], transicao: p.corte === true && i > 0 ? 'corte' : 'morfa', frases: fr, lado: 'centro' };
|
|
126
|
+
/* O CARTÃO DE PRODUTO que flutua ao lado (terminal, chat, painel), feito
|
|
127
|
+
de forma e texto — e o texto vai para o outro lado. `ui: false` tira. */
|
|
128
|
+
const ui = p.ui === false ? null : typeof p.ui === 'string' ? p.ui
|
|
129
|
+
: tipo === 'titulo' || tipo === 'abertura' ? ['terminal', 'chat'][comCartao % 2] : tipo === 'numero' ? 'painel' : null;
|
|
130
|
+
if (ui) {
|
|
131
|
+
b.lado = comCartao++ % 2 ? 'dir' : 'esq';
|
|
132
|
+
b.ui = ui === 'terminal' ? { tipo: ui, linhas: ['$ ' + cmd, '✓ ' + caixaNormal(p.sub || titulo)] }
|
|
133
|
+
: ui === 'chat' ? { tipo: ui, pergunta: caixaNormal(titulo) }
|
|
134
|
+
: { tipo: 'painel', rotulo: caixaNormal(p.rotulo || titulo), barras: [0, 1, 2, 3, 4, 5, 6].map((k) => r2(0.35 + ((semente >>> (k * 3)) % 7) / 10)) };
|
|
135
|
+
}
|
|
136
|
+
switch (tipo) {
|
|
137
|
+
case 'numero': {
|
|
138
|
+
b.numero = numeroDe(p.valor);
|
|
139
|
+
poe('numero', p.valor, { semAcento: true });
|
|
140
|
+
poe('rotulo', p.rotulo || titulo, { destaque: p.destaque });
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
case 'lista': case 'passos':
|
|
144
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
145
|
+
itensDe(p).slice(0, tipo === 'passos' ? 6 : 4).forEach((it) => poe('item', it, { semAcento: true }));
|
|
146
|
+
break;
|
|
147
|
+
case 'contraste':
|
|
148
|
+
poe('nao', p.nao, { semAcento: true });
|
|
149
|
+
poe('sim', p.sim || titulo, { destaque: p.destaque });
|
|
150
|
+
break;
|
|
151
|
+
case 'citacao':
|
|
152
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
153
|
+
poe('autor', p.autor ? `— ${p.autor}` : '', { semAcento: true });
|
|
154
|
+
break;
|
|
155
|
+
case 'cta':
|
|
156
|
+
b.marca = { nome: caixaNormal(marca.nome || titulo) };
|
|
157
|
+
poe('marca', b.marca.nome, { semAcento: true });
|
|
158
|
+
poe('principal', marca.nome ? titulo : '', { destaque: p.destaque });
|
|
159
|
+
poe('sub', p.link || p.sub, { semAcento: true });
|
|
160
|
+
break;
|
|
161
|
+
default:
|
|
162
|
+
poe('principal', titulo, { destaque: p.destaque });
|
|
163
|
+
poe('sub', p.sub, { semAcento: true });
|
|
164
|
+
}
|
|
165
|
+
return b;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
/* ── o tempo: a VOZ manda. A batida troca na fronteira da frase (um
|
|
169
|
+
pouco antes da primeira palavra da parte) e cada palavra na tela
|
|
170
|
+
aparece no instante em que é dita (os tempos por palavra da narração).
|
|
171
|
+
A palavra que a voz não diz entra em cascata curta. Se o texto pede
|
|
172
|
+
mais leitura do que a frase deu, a batida estica e a próxima volta a
|
|
173
|
+
casar com a voz na primeira folga; o atraso medido vai na partitura. */
|
|
174
|
+
let atraso = 0, sincronia = 0;
|
|
175
|
+
batidas.forEach((b, i) => {
|
|
176
|
+
const tp = temVoz ? tempos[i] : null;
|
|
177
|
+
const deVoz = tp ? (i === 0 ? 0 : Math.max(0, tp.de - ANTES)) : null;
|
|
178
|
+
b.de = r2(i === 0 ? 0 : Math.max(batidas[i - 1].ate, deVoz == null ? 0 : deVoz));
|
|
179
|
+
if (deVoz != null) atraso = Math.max(atraso, r2(b.de - deVoz));
|
|
180
|
+
const ditas = ((tp && tp.palavras) || []).map((w) => ({ k: limpa(w.texto), de: w.de }));
|
|
181
|
+
let cursor = 0, livre = b.de + (b.tipo === 'cta' ? 0.55 : 0.12);
|
|
182
|
+
const fimVoz = tp ? (i < batidas.length - 1 ? tempos[i + 1].de - ANTES : tp.ate + 0.2) : 0;
|
|
183
|
+
for (const f of b.frases) {
|
|
184
|
+
/* Casa as palavras da tela com as faladas, na ordem. Tenta começar de
|
|
185
|
+
cada palavra (a frase da tela pode vir em outra ordem que a falada)
|
|
186
|
+
e fica com o casamento que acha mais palavras. */
|
|
187
|
+
const casa = (k, d) => d === k || (k.length >= 4 && d.length >= 4 && d.slice(0, 5) === k.slice(0, 5));
|
|
188
|
+
let ts = f.palavras.map(() => null), fim = cursor;
|
|
189
|
+
for (let ini = 0; ini < f.palavras.length; ini++) {
|
|
190
|
+
let c = cursor;
|
|
191
|
+
const tt = f.palavras.map((w, j) => {
|
|
192
|
+
const k = limpa(w.t);
|
|
193
|
+
if (j < ini || !k) return null;
|
|
194
|
+
for (let q = c; q < Math.min(ditas.length, c + 12); q++) if (casa(k, ditas[q].k)) { c = q + 1; return ditas[q].de; }
|
|
195
|
+
return null;
|
|
196
|
+
});
|
|
197
|
+
if (tt.filter((x) => x != null).length > ts.filter((x) => x != null).length) { ts = tt; fim = c; }
|
|
198
|
+
}
|
|
199
|
+
cursor = fim;
|
|
200
|
+
f.falada = ts.filter((x) => x != null).length >= f.palavras.length / 2;
|
|
201
|
+
const ach = ts.findIndex((x) => x != null);
|
|
202
|
+
/* O que a voz não diz entra cedo o bastante para ser lido antes de a
|
|
203
|
+
frase acabar (e nunca antes da batida). */
|
|
204
|
+
let t = ach >= 0 ? ts[ach] - ach * STAGGER
|
|
205
|
+
: tp ? Math.max(b.de + 0.25, Math.min(livre, fimVoz - leitura(f.palavras.length) - 0.05)) : livre;
|
|
206
|
+
f.palavras.forEach((w, j) => {
|
|
207
|
+
const falada = ts[j];
|
|
208
|
+
t = Math.max(b.de + 0.06, falada != null ? Math.max(t, falada - 0.05) : t);
|
|
209
|
+
w.em = r2(t);
|
|
210
|
+
if (falada != null) sincronia = Math.max(sincronia, r2(w.em - falada));
|
|
211
|
+
t += STAGGER;
|
|
212
|
+
});
|
|
213
|
+
f.de = f.palavras[0].em;
|
|
214
|
+
// O que não é dito (rótulo, item, link) entra logo depois do anterior.
|
|
215
|
+
livre = Math.max(livre, f.papel === 'item' && b.tipo === 'passos' ? f.de + leitura(f.palavras.length) * 0.8 : f.palavras[f.palavras.length - 1].em + 0.18);
|
|
216
|
+
}
|
|
217
|
+
/* O texto que a voz DIZ é lido junto com ela: fica até depois da última
|
|
218
|
+
palavra dita (e nunca menos de 0,8 s). O que a voz não diz pede 0,3 s por palavra. */
|
|
219
|
+
const precisa = Math.max(...b.frases.map((f) => f.de + (f.falada
|
|
220
|
+
? Math.max(MINIMO, f.palavras[f.palavras.length - 1].em - f.de + 0.3) : leitura(f.palavras.length))), b.de + 0.9);
|
|
221
|
+
b.ate = r2(Math.max(precisa, fimVoz));
|
|
222
|
+
});
|
|
223
|
+
// A marca do fim segura um instante; o resto não espera.
|
|
224
|
+
const ult = batidas[batidas.length - 1];
|
|
225
|
+
ult.ate = r2(ult.ate + (ult.tipo === 'cta' ? 1.0 : 0.5));
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
versao: 2, semente, fps: FPS, atraso, sincronia, largura, altura, duracao: ult.ate, paleta,
|
|
229
|
+
fontes: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono' },
|
|
230
|
+
logo: marca.logo || null,
|
|
231
|
+
batidas,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {object} op
|
|
237
|
+
* @param {object} op.roteiro { marca, paleta?: noite|breu|lavanda|creme, tema?: claro|escuro, partes }
|
|
238
|
+
* @param {Array} op.tempos [{de, ate, palavras}] por parte (da narração)
|
|
239
|
+
* @param {object} op.sons os efeitos leves (sons-leves.js)
|
|
240
|
+
*/
|
|
241
|
+
function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null }) {
|
|
242
|
+
const partes = ((roteiro && roteiro.partes) || []).filter((p) => p && typeof p === 'object');
|
|
243
|
+
if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
|
|
244
|
+
const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos });
|
|
245
|
+
const DUR = partitura.duracao;
|
|
246
|
+
/* O SOM segue o movimento (sons-leves.js, tudo sintetizado): whoosh em
|
|
247
|
+
cada transformação, pop quando pílula ou cartão assenta, clique na roda e
|
|
248
|
+
no terminal, um bolha discreto na palavra de acento, o riser que leva à
|
|
249
|
+
marca e o impacto quando ela trava. Baixo: a voz vem primeiro. */
|
|
250
|
+
const efeitos = [];
|
|
251
|
+
const som = (t, nome, volume) => { if (sons[nome]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome], volume }); };
|
|
252
|
+
for (const b of partitura.batidas) {
|
|
253
|
+
if (b.i > 0 && b.tipo !== 'cta') som(b.de - 0.18, 'whoosh', 0.2);
|
|
254
|
+
if (b.ui) { som(b.de + 0.6, 'pop', 0.16); if (b.ui.tipo === 'terminal') b.ui.linhas.forEach((_, k) => som(b.de + 0.75 + k * 0.5, 'clique', 0.1)); }
|
|
255
|
+
for (const f of b.frases) {
|
|
256
|
+
if (f.papel === 'item') som(f.de - 0.06, b.tipo === 'passos' ? 'clique' : 'pop', 0.15);
|
|
257
|
+
const ac = f.palavras.find((w) => w.acento);
|
|
258
|
+
if (ac && f.papel !== 'item') som(ac.em, 'bolha', 0.09);
|
|
259
|
+
}
|
|
260
|
+
if (b.tipo === 'citacao') som(b.de + 0.3, 'pop', 0.14);
|
|
261
|
+
if (b.tipo === 'cta') { som(b.de - 0.75, 'riser', 0.2); som(b.de + 0.45, 'impacto', 0.26); som(b.de + 0.5, 'brilho', 0.16); }
|
|
262
|
+
}
|
|
263
|
+
const volumes = volumesDaTrilha(tempos.length === partes.length ? tempos : [], DUR, { alto: 0.55, baixo: 0.22 });
|
|
264
|
+
const P = partitura.paleta;
|
|
265
|
+
const plano = {
|
|
266
|
+
formato: { largura, altura, fps: FPS },
|
|
267
|
+
fundoGeral: P.fundo[0],
|
|
268
|
+
fontesVariaveis: { Inter: 'wght@300..700', 'Instrument Serif': 'ital@0;1', 'JetBrains Mono': 'wght@400;500' },
|
|
269
|
+
cenas: [{
|
|
270
|
+
duracao: DUR,
|
|
271
|
+
fundo: P.fundo[0],
|
|
272
|
+
elementos: [{ tipo: 'kit', peca: 'fluido', partitura, fonte: partitura.fontes.texto, fonteSerifa: partitura.fontes.serifa, fonteMono: partitura.fontes.mono }],
|
|
273
|
+
}],
|
|
274
|
+
audio: {
|
|
275
|
+
...(narracao ? { narracao } : {}),
|
|
276
|
+
...(trilha ? { trilha: { src: trilha, volumes, loop: true } } : {}),
|
|
277
|
+
efeitos,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
return { ok: true, plano, partitura, resumo: { cenas: 1, batidas: partitura.batidas.length, segundos: DUR, fps: FPS, paleta: P.nome, atraso: partitura.atraso, sincronia: partitura.sincronia } };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = { montarFluido, partituraDe, caixaNormal, PALETAS, POR_PALAVRA, MINIMO };
|