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.
- package/README.md +2 -2
- package/bin/primocode.js +6 -26
- package/lib/act.js +5 -0
- package/lib/browser.js +1 -1
- package/lib/catalogo.js +17 -8
- package/lib/estudio/assets.js +75 -1
- package/lib/estudio/assinatura.js +108 -0
- package/lib/estudio/assinaturas/fluido.json +15 -0
- package/lib/estudio/assinaturas/gerativo.json +15 -0
- package/lib/estudio/batida.js +184 -0
- package/lib/estudio/biblioteca.js +52 -12
- package/lib/estudio/decisao.js +106 -0
- package/lib/estudio/diretor.js +70 -4
- package/lib/estudio/edicao.js +34 -16
- package/lib/estudio/efeitos.js +95 -0
- package/lib/estudio/estilo.js +17 -6
- package/lib/estudio/falantes.js +97 -0
- package/lib/estudio/fluido.js +110 -16
- package/lib/estudio/gerativo.js +309 -85
- package/lib/estudio/hibrido.js +1 -1
- package/lib/estudio/index.js +252 -45
- package/lib/estudio/kit/fluido.jsx +130 -9
- package/lib/estudio/kit/gerativo.jsx +90 -21
- package/lib/estudio/kit/marca.jsx +58 -1
- package/lib/estudio/kit/tres.jsx +4 -1
- package/lib/estudio/kit.js +2 -0
- package/lib/estudio/plano.js +5 -1
- package/lib/estudio/print-site.js +152 -0
- package/lib/estudio/referencias.js +47 -25
- package/lib/estudio/remotion.js +74 -3
- package/lib/estudio/revisor.js +117 -0
- package/lib/estudio/variedade.js +83 -21
- package/lib/video/filters.js +6 -1
- package/lib/video/ops.js +4 -3
- package/lib/video/timeline.js +1 -1
- package/lib/video/zoom.js +275 -0
- package/package.json +2 -2
- package/skills/motion-de-primeira/SKILL.md +3 -5
|
@@ -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 };
|
package/lib/estudio/fluido.js
CHANGED
|
@@ -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 %
|
|
55
|
-
: /escuro|dark/.test(tema) ? ESCURAS[semente %
|
|
56
|
-
: Object.keys(PALETAS)[semente %
|
|
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
|
-
|
|
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
|
-
:
|
|
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 =
|
|
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 +
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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 };
|