primocode 10.1.0 → 10.3.0-beta.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.
@@ -0,0 +1,97 @@
1
+ /**
2
+ * falantes.js — quem fala cada parte do roteiro.
3
+ *
4
+ * "fiz um teste de um vídeo com duas narrações e ele fez de uma narração
5
+ * só, uma voz só" (Joel, 09/10)
6
+ *
7
+ * O roteiro tinha UMA voz (roteiro.voz) e nenhum lugar para dizer quem fala o
8
+ * quê: pedir diálogo ou dois narradores não tinha como chegar na narração. O
9
+ * contrato agora tem dois campos, os dois opcionais:
10
+ * roteiro.vozes = { "ana": "thalita", "bruno": "antonio" }
11
+ * parte.falante = "ana"
12
+ * Sem eles, nada muda: roteiro.voz segue valendo para tudo. Quem decide se o
13
+ * vídeo é diálogo é o modelo, pelo pedido; aqui só se lê o que ele escreveu.
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ // As vozes do Studio (thalita | antonio | francisca). Para quem ficou sem voz no mapa.
19
+ const PAR = ['antonio', 'thalita', 'francisca'];
20
+
21
+ const nomeFalante = (x) => (typeof x === 'string' ? x.trim().toLowerCase().slice(0, 40) : '');
22
+
23
+ /** Limpa vozes e falantes do roteiro (o que não for texto sai). Devolve o mesmo objeto. */
24
+ function normalizar(roteiro) {
25
+ const r = roteiro && typeof roteiro === 'object' ? roteiro : {};
26
+ if (r.vozes && typeof r.vozes === 'object' && !Array.isArray(r.vozes)) {
27
+ const limpo = {};
28
+ for (const [k, v] of Object.entries(r.vozes)) if (nomeFalante(k) && typeof v === 'string' && v.trim()) limpo[nomeFalante(k)] = v.trim();
29
+ if (Object.keys(limpo).length) r.vozes = limpo; else delete r.vozes;
30
+ } else delete r.vozes;
31
+ for (const pt of Array.isArray(r.partes) ? r.partes : []) {
32
+ if (!pt || typeof pt !== 'object' || !('falante' in pt)) continue;
33
+ if (nomeFalante(pt.falante)) pt.falante = nomeFalante(pt.falante); else delete pt.falante;
34
+ }
35
+ return r;
36
+ }
37
+
38
+ /**
39
+ * A voz de cada parte, na ordem. Com falante e voz no mapa, a do mapa; com
40
+ * falante FORA do mapa (o modelo deu nome e esqueceu a voz), a próxima do par
41
+ * que ainda não fala — dois falantes nunca saem com a mesma voz por esquecimento;
42
+ * sem falante, a `padrao` (roteiro.voz).
43
+ */
44
+ function vozesDasPartes(partes, roteiro = {}, padrao = '') {
45
+ const mapa = { ...((roteiro && roteiro.vozes) || {}) };
46
+ const ocupadas = new Set(Object.values(mapa).map((v) => String(v).toLowerCase()));
47
+ if (padrao) ocupadas.add(String(padrao).toLowerCase());
48
+ return (partes || []).map((pt) => {
49
+ const f = nomeFalante(pt && pt.falante);
50
+ if (!f) return padrao || '';
51
+ if (!mapa[f]) {
52
+ mapa[f] = PAR.find((v) => !ocupadas.has(v)) || PAR[Object.keys(mapa).length % PAR.length];
53
+ ocupadas.add(mapa[f]);
54
+ }
55
+ return mapa[f];
56
+ });
57
+ }
58
+
59
+ /**
60
+ * A narração por falante: as falas seguidas da mesma voz viram um grupo, cada
61
+ * grupo é narrado na sua voz (narrarUm — a mesma corrente Gemini → Kokoro →
62
+ * Edge de sempre), e os mp3 são juntados na ordem (juntar). Os tempos de cada
63
+ * parte andam pela duração dos grupos de antes: o vídeo segue a voz certa.
64
+ * Uma voz só: um grupo só, e é exatamente a narração de antes.
65
+ *
66
+ * @param {Array<{texto:string, voz?:string}>} lista
67
+ * @param {(lista, mp3, voz) => Promise<{ok, segundos, partes, voz}>} op.narrarUm
68
+ * @param {(arquivos, mp3) => Promise<{ok, duracoes?:number[]}>} op.juntar
69
+ */
70
+ async function narrarPorFalante(lista, mp3, { narrarUm, juntar }) {
71
+ const grupos = [];
72
+ for (const { voz = '', ...x } of lista) {
73
+ const g = grupos[grupos.length - 1];
74
+ if (g && g.voz === voz) g.itens.push(x); else grupos.push({ voz, itens: [x] });
75
+ }
76
+ if (grupos.length <= 1) return narrarUm(grupos.length ? grupos[0].itens : [], mp3, grupos.length ? grupos[0].voz : '');
77
+ const feitos = [];
78
+ for (const [i, g] of grupos.entries()) {
79
+ const arq = mp3.replace(/\.mp3$/, '') + `-falante${i + 1}.mp3`;
80
+ const v = await narrarUm(g.itens, arq, g.voz);
81
+ if (!v || !v.ok) return { ok: false, error: `a voz ${g.voz || 'padrão'} falhou: ${(v && v.error) || 'sem resposta'}` };
82
+ feitos.push({ arq, v });
83
+ }
84
+ const j = await juntar(feitos.map((x) => x.arq), mp3);
85
+ if (!j || !j.ok) return { ok: false, error: (j && j.error) || 'não juntei as vozes' };
86
+ let antes = 0;
87
+ const partes = [];
88
+ feitos.forEach(({ v }, i) => {
89
+ const mais = (t) => Math.round((t + antes) * 1000) / 1000;
90
+ for (const pt of v.partes || []) partes.push({ ...pt, de: mais(pt.de), ate: mais(pt.ate), palavras: (pt.palavras || []).map((w) => ({ ...w, de: mais(w.de), ate: mais(w.ate) })) });
91
+ // A duração MEDIDA do arquivo quando quem juntou mediu; senão a que a voz disse.
92
+ antes += Number((j.duracoes || [])[i]) || Number(v.segundos) || 0;
93
+ });
94
+ return { ok: true, voz: [...new Set(feitos.map((x) => x.v.voz).filter(Boolean))].join(' + '), segundos: Math.round(antes * 1000) / 1000, partes, falantes: grupos.length };
95
+ }
96
+
97
+ module.exports = { normalizar, vozesDasPartes, narrarPorFalante, PAR };
@@ -38,11 +38,43 @@ const ANTES = 0.12; // a batida troca isto antes da primeir
38
38
  NO MÁXIMO dois acentos (o da palavra e o brilho difuso). */
39
39
  const PALETAS = {
40
40
  noite: { escuro: true, fundo: ['#020615', '#061a5c', '#1240c9'], tinta: '#eef2ff', suave: '#9fb0d9', acentos: ['#8fb8ff', '#2f6bff'] },
41
+ /* As CHAPADAS das referências boas (08/10): fundo de uma cor só, sem o
42
+ gradiente — o laranja da "criatividade", o branco com azul, o preto puro
43
+ e o cobalto. O "sempre o mesmo azul" era vício. */
44
+ laranja: { escuro: false, fundo: ['#f2621f', '#f06a28', '#ec7a3c'], tinta: '#1a0d05', suave: '#5a2a10', acentos: ['#fff4ea', '#ffd2b0'] },
45
+ branco: { escuro: false, fundo: ['#ffffff', '#f6f7fa', '#eceff6'], tinta: '#0b0d12', suave: '#6b7180', acentos: ['#1f4bff', '#9fb3ff'] },
46
+ preto: { escuro: true, fundo: ['#000000', '#050505', '#121212'], tinta: '#f5f5f5', suave: '#8a8a8a', acentos: ['#ff7a3d', '#ff9a66'] },
47
+ cobalto: { escuro: true, fundo: ['#1238e6', '#1640f0', '#2350ff'], tinta: '#ffffff', suave: '#c8d4ff', acentos: ['#ffe14d', '#ffffff'] },
41
48
  breu: { escuro: true, fundo: ['#030304', '#0b0a12', '#2a2156'], tinta: '#f4f4f6', suave: '#8d8a9c', acentos: ['#b4a6ff', '#5b45ff'] },
42
49
  lavanda: { escuro: false, fundo: ['#f5f4ff', '#e4e1ff', '#b9b2ff'], tinta: '#17153a', suave: '#6e6a92', acentos: ['#4a3ce0', '#8f86ff'] },
43
50
  creme: { escuro: false, fundo: ['#fbf8f2', '#f2e8d8', '#e7c9a5'], tinta: '#1e1a15', suave: '#857a6c', acentos: ['#c2502a', '#f0a36b'] },
44
51
  };
45
- const ESCURAS = ['noite', 'breu'], CLARAS = ['lavanda', 'creme'];
52
+ const ESCURAS = ['noite', 'breu', 'preto', 'cobalto'], CLARAS = ['lavanda', 'creme', 'laranja', 'branco'];
53
+
54
+ /* AS FONTES (09/10): "vício em fontes" (Joel) — todo fluido saía em Inter +
55
+ Instrument Serif + JetBrains Mono. Agora é um TRIO do cardápio: o grotesco
56
+ leve das palavras (o kit usa 300 a 600), a serifa itálica do acento e o mono
57
+ do terminal. A memória da variedade ('fonte-fluido') não repete o trio dos
58
+ últimos vídeos; o modelo pode pedir um pelo nome (roteiro.fontesFluido) e o
59
+ pedido vale, menos quando acabou de sair. `css` é o pedido ao Google Fonts
60
+ (conferido: todos respondem 200). Sem escolha salva, o trio aprovado. */
61
+ const TRIOS = {
62
+ aprovado: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono', css: ['wght@300..700', 'ital@0;1', 'wght@400;500'] },
63
+ editorial: { texto: 'Hanken Grotesk', serifa: 'Fraunces', mono: 'Martian Mono', css: ['wght@300..700', 'ital,wght@0,400;1,400', 'wght@400;500'] },
64
+ revista: { texto: 'Figtree', serifa: 'Playfair Display', mono: 'IBM Plex Mono', css: ['wght@300..700', 'ital,wght@0,400;1,400', 'wght@400;500'] },
65
+ carater: { texto: 'Bricolage Grotesque', serifa: 'Bodoni Moda', mono: 'Space Mono', css: ['wght@300..700', 'ital,wght@0,400;1,400', 'wght@400'] },
66
+ tecnico: { texto: 'Mona Sans', serifa: 'Instrument Serif', mono: 'Fragment Mono', css: ['wght@300..700', 'ital@0;1', 'ital@0;1'] },
67
+ classico: { texto: 'Albert Sans', serifa: 'DM Serif Display', mono: 'DM Mono', css: ['wght@300..700', 'ital@0;1', 'wght@400;500'] },
68
+ };
69
+
70
+ /** O trio de um projeto novo, pelos pontos da variedade. Roda uma vez por projeto (index.js guarda). */
71
+ function escolherFontes(pedido = null, { aleatorio = Math.random } = {}) {
72
+ const variedade = require('./variedade.js');
73
+ const nome = TRIOS[String(pedido || '').toLowerCase()] ? String(pedido).toLowerCase() : null;
74
+ // O pedido do modelo vale mais (1), os outros servem (0,5): penaliza o recente, não proíbe.
75
+ return variedade.sortear(Object.keys(TRIOS), 'fonte-fluido', { aleatorio, adequacao: (k) => (!nome || k === nome ? 1 : 0.5) });
76
+ }
77
+ const trioDe = (roteiro) => TRIOS[roteiro.fontesFluido] || TRIOS.aprovado;
46
78
 
47
79
  const hash = (s) => { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; };
48
80
 
@@ -51,9 +83,9 @@ function paletaDe(roteiro, semente) {
51
83
  const pedida = String(roteiro.paleta || '').toLowerCase();
52
84
  const tema = String(roteiro.tema || '').toLowerCase();
53
85
  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];
86
+ : /claro|light/.test(tema) ? CLARAS[semente % CLARAS.length]
87
+ : /escuro|dark/.test(tema) ? ESCURAS[semente % ESCURAS.length]
88
+ : Object.keys(PALETAS)[semente % Object.keys(PALETAS).length];
57
89
  const p = { nome, ...PALETAS[nome], acentos: [...PALETAS[nome].acentos] };
58
90
  // A cor da marca vira o acento da palavra (e só ela: a paleta não cresce).
59
91
  const dest = roteiro.marca && roteiro.marca.cores && roteiro.marca.cores.destaque;
@@ -106,7 +138,7 @@ const TIPO = { logo: 'abertura', imagem: 'titulo', produto: 'titulo', grafico: '
106
138
  * A partitura: as batidas, cada uma com as frases (e quando cada uma entra),
107
139
  * a forma do portador e a transição. Sem áudio nem plano — é o que os testes olham.
108
140
  */
109
- function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
141
+ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], imagens = {} }) {
110
142
  const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object');
111
143
  const semente = Number.isFinite(roteiro.semente) ? roteiro.semente : hash(JSON.stringify(partes.map((p) => [p.tipo, p.titulo, p.valor])));
112
144
  const paleta = paletaDe(roteiro, semente);
@@ -116,9 +148,15 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
116
148
  const link = (partes.find((p) => p.tipo === 'cta') || {}).link || '';
117
149
  const cmd = /^(npm|npx|pnpm|yarn|pip|brew|curl|git|bun)\b/.test(link) ? link : String(marca.nome || 'app').toLowerCase().replace(/\s+/g, '-');
118
150
  let comCartao = 0;
151
+ /* O cartão de produto só quando o vídeo É de produto: tem marca, um
152
+ comando de instalar ou um link. Um vídeo sobre sono saía com um
153
+ terminal "$ app" ao lado do título (teste de 08/10). `p.ui` explícito vale sempre. */
154
+ const deProduto = !!(marca.nome || link || /\b(app|software|plataforma|ferramenta|produto|sistema|saas|api|cli)\b/i.test(String(roteiro.pedido || '')));
119
155
 
120
156
  const batidas = partes.map((p, i) => {
121
- const tipo = TIPO[p.tipo] || (FORMA[p.tipo] ? p.tipo : 'titulo');
157
+ /* Fecho sem marca não monta a "marca de bolinhas" (vício, 08/10): vira
158
+ uma frase — e, curta, a gigante do fim. */
159
+ const tipo = p.tipo === 'cta' && !marca.nome ? 'titulo' : TIPO[p.tipo] || (FORMA[p.tipo] ? p.tipo : 'titulo');
122
160
  const titulo = p.titulo || p.texto || '';
123
161
  const fr = [];
124
162
  const poe = (papel, texto, o = {}) => { if (String(texto || '').trim()) fr.push({ id: `${i}-${fr.length}`, papel, palavras: frase(texto, o) }); };
@@ -126,7 +164,8 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
126
164
  /* O CARTÃO DE PRODUTO que flutua ao lado (terminal, chat, painel), feito
127
165
  de forma e texto — e o texto vai para o outro lado. `ui: false` tira. */
128
166
  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;
167
+ : !deProduto ? null
168
+ : tipo === 'titulo' || tipo === 'abertura' ? ['terminal', 'chat'][comCartao % 2] : tipo === 'numero' ? 'painel' : null;
130
169
  if (ui) {
131
170
  b.lado = comCartao++ % 2 ? 'dir' : 'esq';
132
171
  b.ui = ui === 'terminal' ? { tipo: ui, linhas: ['$ ' + cmd, '✓ ' + caixaNormal(p.sub || titulo)] }
@@ -165,6 +204,53 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
165
204
  return b;
166
205
  });
167
206
 
207
+ /* ── A PALAVRA GIGANTE e a VIRADA (08/10): nas referências boas, o
208
+ momento forte é uma frase curta que ENCHE a tela, e o fundo vira
209
+ (o ponto cresce até cobrir tudo de cor). Sem isso o vídeo era a mesma
210
+ pílula pequena no mesmo azul por 36 s — "horríveis". Regras de editor:
211
+ só frase de até 4 palavras, abertura ou com destaque (a palavra-tese)
212
+ primeiro; gigante em até metade das batidas; a virada só nas que têm
213
+ destaque, 1 em 3 e nunca seguida. `p.gigante` explícito manda. */
214
+ const curta = (b) => { const f = b.frases.find((x) => x.papel === 'principal'); return f && f.palavras.length <= 4 && b.frases.length <= 2; };
215
+ const candidatas = batidas.map((b, i) => ({ b, i, p: partes[i] }))
216
+ .filter(({ b, p }) => p.gigante !== false && (b.tipo === 'titulo' || b.tipo === 'abertura') && !b.ui && !b.imagem && curta(b))
217
+ .sort((x, y) => (y.p.gigante === true) - (x.p.gigante === true) || !!y.p.destaque - !!x.p.destaque || (x.b.tipo === 'abertura' ? -1 : 0) || x.i - y.i);
218
+ /* ── AS IMAGENS, quando a pessoa pediu (08/10: "deve ser misto com imagens
219
+ e motion, e com variadas animações"; o híbrido era "10 mil imagens de
220
+ banco e a mesma animação"). A imagem é UM momento: só nas batidas que
221
+ têm imagem baixada, no máximo 1 em 3, nunca duas seguidas. E cada uma
222
+ entra de um JEITO diferente, sem repetir no vídeo:
223
+ janela a foto abre da esquerda e cobre a tela, o texto por cima
224
+ circulo o ponto cresce em círculo com a foto dentro
225
+ cartao a foto num cartão que gira em 3D, o texto acima
226
+ faixa uma faixa da foto abre na altura, o texto abaixo
227
+ vazado o título gigante com a foto por DENTRO das letras */
228
+ const JEITOS = ['janela', 'circulo', 'cartao', 'faixa', 'vazado'];
229
+ const ordemJeitos = JEITOS.map((j, k) => ({ j, o: hash(j + semente) })).sort((a, b) => a.o - b.o).map((x) => x.j);
230
+ const tetoImagem = Math.max(1, Math.floor(batidas.length / 3));
231
+ let nImagem = 0;
232
+ for (const b of batidas) {
233
+ const img = imagens[b.i];
234
+ if (!img || !img.src || b.tipo === 'cta' || nImagem >= tetoImagem) continue;
235
+ if (batidas[b.i - 1] && batidas[b.i - 1].imagem) continue;
236
+ const pr = b.frases.find((f) => f.papel === 'principal');
237
+ let jeito = ordemJeitos[nImagem % ordemJeitos.length];
238
+ if (jeito === 'vazado' && !(pr && pr.palavras.length <= 3)) jeito = ordemJeitos[(nImagem + 1) % ordemJeitos.length] === 'vazado' ? 'janela' : ordemJeitos[(nImagem + 1) % ordemJeitos.length];
239
+ b.imagem = { src: img.src, jeito };
240
+ b.ui = null; b.lado = 'centro';
241
+ nImagem++;
242
+ }
243
+
244
+ // A gigante pode seguir outra (as referências fazem isso); a VIRADA é o golpe: 1 em 3, nunca seguida.
245
+ const teto = Math.max(1, Math.ceil(batidas.length / 2)), tetoVirada = Math.max(1, Math.floor(batidas.length / 3));
246
+ for (const { b, i, p } of candidatas) {
247
+ if (batidas.filter((x) => x.gigante).length >= teto && p.gigante !== true) break;
248
+ b.gigante = true;
249
+ const viradas = batidas.filter((x) => x.virada).length;
250
+ const vizinha = (batidas[i - 1] && batidas[i - 1].virada) || (batidas[i + 1] && batidas[i + 1].virada);
251
+ if (i > 0 && viradas < tetoVirada && !vizinha && (p.destaque || p.gigante === true)) b.virada = true;
252
+ }
253
+
168
254
  /* ── o tempo: a VOZ manda. A batida troca na fronteira da frase (um
169
255
  pouco antes da primeira palavra da parte) e cada palavra na tela
170
256
  aparece no instante em que é dita (os tempos por palavra da narração).
@@ -185,13 +271,16 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
185
271
  cada palavra (a frase da tela pode vir em outra ordem que a falada)
186
272
  e fica com o casamento que acha mais palavras. */
187
273
  const casa = (k, d) => d === k || (k.length >= 4 && d.length >= 4 && d.slice(0, 5) === k.slice(0, 5));
274
+ /* Cada frase procura as suas palavras na fala INTEIRA da batida: com
275
+ um cursor só, o título que casava com o fim da fala deixava o
276
+ subtítulo sem achar as dele (entrava 0,45 s atrasado, 08/10). */
188
277
  let ts = f.palavras.map(() => null), fim = cursor;
189
278
  for (let ini = 0; ini < f.palavras.length; ini++) {
190
- let c = cursor;
279
+ let c = 0;
191
280
  const tt = f.palavras.map((w, j) => {
192
281
  const k = limpa(w.t);
193
282
  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; }
283
+ for (let q = c; q < Math.min(ditas.length, c + 40); q++) if (casa(k, ditas[q].k)) { c = q + 1; return ditas[q].de; }
195
284
  return null;
196
285
  });
197
286
  if (tt.filter((x) => x != null).length > ts.filter((x) => x != null).length) { ts = tt; fim = c; }
@@ -201,7 +290,11 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
201
290
  const ach = ts.findIndex((x) => x != null);
202
291
  /* O que a voz não diz entra cedo o bastante para ser lido antes de a
203
292
  frase acabar (e nunca antes da batida). */
204
- let t = ach >= 0 ? ts[ach] - ach * STAGGER
293
+ /* O que a voz DIZ entra quando ela diz — nunca antes. Antes de 08/10
294
+ a frase entrava no começo da batida se a voz demorasse: "ontem à
295
+ noite" aparecia 2,8 s antes de ser dito, e o Joel viu "atrasos
296
+ entre a narração e o vídeo". */
297
+ let t = ach >= 0 && ts[ach] != null ? ts[ach] - ach * STAGGER
205
298
  : tp ? Math.max(b.de + 0.25, Math.min(livre, fimVoz - leitura(f.palavras.length) - 0.05)) : livre;
206
299
  f.palavras.forEach((w, j) => {
207
300
  const falada = ts[j];
@@ -226,7 +319,8 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
226
319
 
227
320
  return {
228
321
  versao: 2, semente, fps: FPS, atraso, sincronia, largura, altura, duracao: ult.ate, paleta,
229
- fontes: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono' },
322
+ marcador: roteiro.marcador || ['brilho', 'numero', 'traco', 'ponto', 'nenhum'][hash('marcador' + semente) % 5],
323
+ fontes: (({ texto, serifa, mono }) => ({ texto, serifa, mono }))(trioDe(roteiro)),
230
324
  logo: marca.logo || null,
231
325
  batidas,
232
326
  };
@@ -238,10 +332,10 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
238
332
  * @param {Array} op.tempos [{de, ate, palavras}] por parte (da narração)
239
333
  * @param {object} op.sons os efeitos leves (sons-leves.js)
240
334
  */
241
- function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null }) {
335
+ function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null, imagens = {} }) {
242
336
  const partes = ((roteiro && roteiro.partes) || []).filter((p) => p && typeof p === 'object');
243
337
  if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
244
- const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos });
338
+ const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos, imagens });
245
339
  const DUR = partitura.duracao;
246
340
  /* O SOM segue o movimento (sons-leves.js, tudo sintetizado): whoosh em
247
341
  cada transformação, pop quando pílula ou cartão assenta, clique na roda e
@@ -265,7 +359,7 @@ function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
265
359
  const plano = {
266
360
  formato: { largura, altura, fps: FPS },
267
361
  fundoGeral: P.fundo[0],
268
- fontesVariaveis: { Inter: 'wght@300..700', 'Instrument Serif': 'ital@0;1', 'JetBrains Mono': 'wght@400;500' },
362
+ fontesVariaveis: (({ texto, serifa, mono, css }) => ({ [texto]: css[0], [serifa]: css[1], [mono]: css[2] }))(trioDe(roteiro)),
269
363
  cenas: [{
270
364
  duracao: DUR,
271
365
  fundo: P.fundo[0],
@@ -277,7 +371,7 @@ function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
277
371
  efeitos,
278
372
  },
279
373
  };
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 } };
374
+ return { ok: true, plano, partitura, resumo: { cenas: 1, batidas: partitura.batidas.length, imagens: partitura.batidas.filter((b) => b.imagem).map((b) => b.imagem.jeito), segundos: DUR, fps: FPS, paleta: P.nome, atraso: partitura.atraso, sincronia: partitura.sincronia } };
281
375
  }
282
376
 
283
- module.exports = { montarFluido, partituraDe, caixaNormal, PALETAS, POR_PALAVRA, MINIMO };
377
+ module.exports = { montarFluido, partituraDe, caixaNormal, escolherFontes, PALETAS, TRIOS, POR_PALAVRA, MINIMO };