primocode 10.1.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/primocode.js +6 -26
- package/lib/catalogo.js +3 -3
- package/lib/estudio/assets.js +75 -1
- package/lib/estudio/batida.js +184 -0
- package/lib/estudio/biblioteca.js +52 -12
- package/lib/estudio/decisao.js +106 -0
- package/lib/estudio/edicao.js +34 -16
- package/lib/estudio/estilo.js +17 -6
- package/lib/estudio/fluido.js +82 -13
- package/lib/estudio/index.js +122 -30
- package/lib/estudio/kit/fluido.jsx +130 -9
- package/lib/estudio/referencias.js +36 -25
- package/lib/estudio/remotion.js +2 -0
- package/lib/estudio/revisor.js +117 -0
- package/lib/estudio/variedade.js +71 -17
- 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
package/lib/estudio/edicao.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
|
|
27
27
|
'use strict';
|
|
28
28
|
|
|
29
|
+
const decisao = require('./decisao.js');
|
|
30
|
+
|
|
29
31
|
// ── as palavras que não carregam ideia ───────────────────────────────────
|
|
30
32
|
const VAZIAS = new Set((
|
|
31
33
|
'a o as os um uma uns umas de do da dos das no na nos nas em por para pra pro com sem que se e '
|
|
@@ -88,6 +90,25 @@ const ICONES = [
|
|
|
88
90
|
[['surpre', 'chocad', 'absurd'], 'exploding-head'],
|
|
89
91
|
];
|
|
90
92
|
|
|
93
|
+
/* AS OUTRAS FORMAS DE DIZER O MESMO (07/10): "não quero que o sistema fique
|
|
94
|
+
repetindo os mesmos ícones". Cada conceito tinha UM emoji — todo vídeo de
|
|
95
|
+
dinheiro saía com o mesmo saco de dinheiro. O ícone automático continua
|
|
96
|
+
saindo daqui pelo primeiro nome (o de ICONES); a passada de variedade do
|
|
97
|
+
vídeo (index.js, variarIcones) troca pelo que os últimos vídeos menos
|
|
98
|
+
usaram, o mesmo conceito com o mesmo ícone dentro do vídeo. Nomes
|
|
99
|
+
conferidos no fluent-emoji-flat do Iconify em 07/10. */
|
|
100
|
+
const ALTERNATIVAS = {
|
|
101
|
+
robot: ['mechanical-arm', 'desktop-computer'], brain: ['thought-balloon', 'light-bulb'], 'light-bulb': ['sparkles', 'glowing-star', 'gem-stone'],
|
|
102
|
+
'money-bag': ['dollar-banknote', 'coin', 'money-with-wings', 'credit-card'], stopwatch: ['hourglass-not-done', 'alarm-clock', 'mantelpiece-clock', 'watch'],
|
|
103
|
+
'chart-increasing': ['seedling', 'mountain', 'chequered-flag'], 'bar-chart': ['clipboard', 'hundred-points'], rocket: ['high-voltage', 'airplane'],
|
|
104
|
+
fire: ['collision', 'high-voltage', 'dizzy'], 'check-mark-button': ['check-mark', 'thumbs-up', 'hundred-points'], 'cross-mark': ['stop-sign', 'no-entry'],
|
|
105
|
+
laptop: ['desktop-computer', 'keyboard'], 'globe-showing-americas': ['globe-with-meridians', 'satellite-antenna'], 'speaker-high-volume': ['microphone', 'headphone', 'megaphone'],
|
|
106
|
+
memo: ['pencil', 'open-book', 'clipboard'], books: ['open-book', 'pencil'], 'speech-balloon': ['speaking-head', 'thought-balloon', 'megaphone'],
|
|
107
|
+
gear: ['hammer-and-wrench', 'toolbox', 'link'], calendar: ['spiral-calendar'], trophy: ['sports-medal', 'crown', 'glowing-star'],
|
|
108
|
+
'flexed-biceps': ['high-voltage'], handshake: ['people-hugging', 'link'], locked: ['shield', 'key'], briefcase: ['bank', 'classical-building'],
|
|
109
|
+
'party-popper': ['confetti-ball', 'sparkles'], 'red-heart': ['sparkling-heart'], 'shopping-cart': ['receipt', 'credit-card'], 'clapper-board': ['video-camera', 'camera'],
|
|
110
|
+
};
|
|
111
|
+
|
|
91
112
|
/* O termo de busca da IMAGEM DE APOIO, pelo emoji que a palavra ganhou. Em
|
|
92
113
|
inglês de propósito: os acervos abertos respondem muito melhor assim. */
|
|
93
114
|
const FOTO = {
|
|
@@ -225,14 +246,6 @@ function blocos(palavras, maximo) {
|
|
|
225
246
|
return out;
|
|
226
247
|
}
|
|
227
248
|
|
|
228
|
-
// O zoom de cada cena, em rodízio: parado, punch-in, empurrando devagar.
|
|
229
|
-
function camera(i, dur) {
|
|
230
|
-
const tipo = i % 3;
|
|
231
|
-
if (tipo === 0) return { zoom: [{ t: 0, v: 1 }, { t: dur, v: 1.04, e: 'linear' }] };
|
|
232
|
-
if (tipo === 1) return { zoom: [{ t: 0, v: 1.18 }, { t: dur, v: 1.21, e: 'linear' }] };
|
|
233
|
-
return { zoom: [{ t: 0, v: 1.07 }, { t: dur, v: 1.13, e: 'suave' }] };
|
|
234
|
-
}
|
|
235
|
-
|
|
236
249
|
const ESTILOS = {
|
|
237
250
|
viral: { icones: true, letreiro: true, sons: true, barra: true, apoio: true, recorte: true, fundoRecorte: ['#1B1F3B', '#3A1C71'], destaque: '#FFD400', forte: '#00E0A4' },
|
|
238
251
|
limpo: { icones: false, letreiro: false, sons: false, barra: false, apoio: true, recorte: false, fundoRecorte: ['#111111', '#333333'], destaque: '#FFFFFF', forte: '#FFD400' },
|
|
@@ -251,8 +264,6 @@ const ESTILOS = {
|
|
|
251
264
|
* @param {object} op.sons { pop, whoosh } — caminhos na pasta do projeto, opcionais
|
|
252
265
|
* @param {Set} op.iconesOk os nomes de ícone que existem de verdade (sem isso, todos valem)
|
|
253
266
|
*/
|
|
254
|
-
// Em quais cenas a pessoa é recortada, com a palavra gigante atrás dela.
|
|
255
|
-
const temRecorte = (i) => i % 4 === 2;
|
|
256
267
|
|
|
257
268
|
function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, estilo = 'viral', sons = {}, iconesOk = null, imagens = {}, trilha = null, recortes = {}, marca = null }) {
|
|
258
269
|
const e = ESTILOS[estilo] || ESTILOS.viral;
|
|
@@ -263,6 +274,10 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
263
274
|
const base = Math.min(largura, altura);
|
|
264
275
|
const grupos = frases(lista);
|
|
265
276
|
const total = duracao || lista[lista.length - 1].ate + 0.3;
|
|
277
|
+
/* O QUE CADA FRASE PEDE (decisao.js): movimento, recorte e whoosh pela
|
|
278
|
+
intensidade dela, não pelo rodízio i % 3 — e depois do golpe, respiro. */
|
|
279
|
+
const decisoes = decisao.decidir(grupos, { peso });
|
|
280
|
+
let ladoQuadro = 0;
|
|
266
281
|
const cenas = [];
|
|
267
282
|
const efeitos = [];
|
|
268
283
|
const infos = []; // onde cada cena começa e a palavra forte dela — para o recorte
|
|
@@ -283,12 +298,13 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
283
298
|
deOrigem: r2(ini), ateOrigem: r2(ini + dur), volume: 1,
|
|
284
299
|
}];
|
|
285
300
|
|
|
286
|
-
infos.push({ ini: r2(ini), dur, forte: forte ? { de: local(forte.de), texto: forte.texto, peso: peso(forte.texto) } : null
|
|
301
|
+
infos.push({ ini: r2(ini), dur, forte: forte ? { de: local(forte.de), texto: forte.texto, peso: peso(forte.texto) } : null,
|
|
302
|
+
recorte: decisoes[i].recorte, intensidade: decisoes[i].intensidade });
|
|
287
303
|
|
|
288
304
|
/* A PALAVRA ATRÁS DA PESSOA: o fundo do quarto some, entra a cor da
|
|
289
305
|
marca, a palavra forte aparece enorme e a pessoa, recortada, fica
|
|
290
306
|
na frente dela. É o efeito que mais denuncia edição de videomaker. */
|
|
291
|
-
const rec = e.recorte &&
|
|
307
|
+
const rec = e.recorte && decisoes[i].recorte ? recortes[i] : null;
|
|
292
308
|
if (rec && forte) {
|
|
293
309
|
const vida = r2(rec.ate - rec.de);
|
|
294
310
|
const some = [{ t: 0, v: 0 }, { t: 0.1, v: 1 }, { t: vida - 0.12, v: 1 }, { t: vida, v: 0 }];
|
|
@@ -403,10 +419,12 @@ function montarEdicao({ src, duracao, palavras, largura = 1080, altura = 1920, e
|
|
|
403
419
|
});
|
|
404
420
|
}
|
|
405
421
|
|
|
406
|
-
// O whoosh entra no
|
|
407
|
-
if (e.sons && sons.whoosh && i
|
|
422
|
+
// O whoosh entra no soco: é o corte que ele anuncia (e só nele).
|
|
423
|
+
if (e.sons && sons.whoosh && decisoes[i].whoosh) efeitos.push({ t: r2(Math.max(0, ini - 0.12)), src: sons.whoosh, volume: 0.6 });
|
|
408
424
|
|
|
409
|
-
|
|
425
|
+
const d = decisoes[i];
|
|
426
|
+
if (d.movimento === 'reenquadra') ladoQuadro = 1 - ladoQuadro;
|
|
427
|
+
cenas.push({ duracao: dur, camera: decisao.camera(d.movimento, dur, d.intensidade, ladoQuadro, base), elementos, transicao: { tipo: 'corte' }, impacto: d.movimento === 'soco' || undefined });
|
|
410
428
|
});
|
|
411
429
|
|
|
412
430
|
return {
|
|
@@ -473,4 +491,4 @@ async function prepararSons(pasta, video) {
|
|
|
473
491
|
return sons;
|
|
474
492
|
}
|
|
475
493
|
|
|
476
|
-
module.exports = { montarEdicao,
|
|
494
|
+
module.exports = { montarEdicao, ALTERNATIVAS, prepararSons, iconeDe, peso, frases, blocos, termosDeApoio, termoDeApoio, rotaDeApoio, DEPOIS_DA_PALAVRA, ESTILOS, ICONES, FOTO };
|
package/lib/estudio/estilo.js
CHANGED
|
@@ -51,13 +51,23 @@ const normal = (e) => {
|
|
|
51
51
|
/* O RODÍZIO: quando ninguém escolheu ("vários estilos... que não sejam padrão
|
|
52
52
|
da IA", Joel 05/10), o padrão não é sempre o híbrido — gira entre os que
|
|
53
53
|
servem a qualquer roteiro, sem repetir os últimos (memória 'estilo'). */
|
|
54
|
-
|
|
54
|
+
/* Com imagem pedida (08/10): o padrão é o FLUIDO MISTO — motion com a
|
|
55
|
+
imagem em poucos momentos, cada uma entrando de um jeito. O híbrido e o
|
|
56
|
+
cinema ("10 mil imagens de banco e a mesma animação") ficam pelo nome. */
|
|
57
|
+
const RODIZIO = ['fluido'];
|
|
58
|
+
/* Sem imagem (o padrão desde 07/10), o rodízio fica nos dois motions que
|
|
59
|
+
nasceram das referências boas (motions-exemplos/motion-god): o fluido e o
|
|
60
|
+
gerativo. O impacto (emoji 3D em cada batida, cara de anúncio) saiu do
|
|
61
|
+
rodízio em 08/10 — "horríveis, nada a ver com as referências" — e fica
|
|
62
|
+
para quem o pede pelo nome (o agente ou o roteiro). */
|
|
63
|
+
const RODIZIO_SEM_IMAGEM = ['fluido', 'gerativo'];
|
|
55
64
|
|
|
56
|
-
function estiloDaVez(aleatorio = Math.random) {
|
|
57
|
-
|
|
65
|
+
function estiloDaVez(aleatorio = Math.random, semImagem = false) {
|
|
66
|
+
const lista = semImagem ? RODIZIO_SEM_IMAGEM : RODIZIO;
|
|
67
|
+
return variedade.sortear(lista, 'estilo', { aleatorio }) || lista[0];
|
|
58
68
|
}
|
|
59
69
|
|
|
60
|
-
function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, aleatorio } = {}) {
|
|
70
|
+
function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = false, semImagem = false, aleatorio } = {}) {
|
|
61
71
|
const pedidoAgente = normal(agente);
|
|
62
72
|
if (pedidoAgente) return pedidoAgente;
|
|
63
73
|
const doRoteiro = normal(roteiro);
|
|
@@ -73,10 +83,11 @@ function escolherEstilo({ pedido = '', agente = null, roteiro = null, girar = fa
|
|
|
73
83
|
/* Anúncio, reels e o resto: o HÍBRIDO. "Um mix da versão antiga (o reels
|
|
74
84
|
da imersão, no cinema) e da última (o kit) — absurdo, que nem parece
|
|
75
85
|
que foi IA" (29/09). Com `girar`, o padrão roda (RODIZIO). */
|
|
76
|
-
|
|
86
|
+
if (girar) return estiloDaVez(aleatorio, semImagem);
|
|
87
|
+
return semImagem ? 'gerativo' : 'hibrido';
|
|
77
88
|
}
|
|
78
89
|
|
|
79
90
|
/** Os estilos que usam os efeitos leves e a trilha do ende.app (os de motion, não os de corte). */
|
|
80
91
|
const LEVES = new Set(['gerativo', 'motion', 'vox', 'marca', 'lancamento', 'fluido']);
|
|
81
92
|
|
|
82
|
-
module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO };
|
|
93
|
+
module.exports = { escolherEstilo, estiloDaVez, ESTILOS, LEVES, PEDE_MOTION, PEDE_FLUIDO, RODIZIO, RODIZIO_SEM_IMAGEM };
|
package/lib/estudio/fluido.js
CHANGED
|
@@ -38,11 +38,18 @@ 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'];
|
|
46
53
|
|
|
47
54
|
const hash = (s) => { let h = 2166136261; for (const c of String(s)) h = Math.imul(h ^ c.charCodeAt(0), 16777619); return h >>> 0; };
|
|
48
55
|
|
|
@@ -51,9 +58,9 @@ function paletaDe(roteiro, semente) {
|
|
|
51
58
|
const pedida = String(roteiro.paleta || '').toLowerCase();
|
|
52
59
|
const tema = String(roteiro.tema || '').toLowerCase();
|
|
53
60
|
const nome = PALETAS[pedida] ? pedida
|
|
54
|
-
: /claro|light/.test(tema) ? CLARAS[semente %
|
|
55
|
-
: /escuro|dark/.test(tema) ? ESCURAS[semente %
|
|
56
|
-
: Object.keys(PALETAS)[semente %
|
|
61
|
+
: /claro|light/.test(tema) ? CLARAS[semente % CLARAS.length]
|
|
62
|
+
: /escuro|dark/.test(tema) ? ESCURAS[semente % ESCURAS.length]
|
|
63
|
+
: Object.keys(PALETAS)[semente % Object.keys(PALETAS).length];
|
|
57
64
|
const p = { nome, ...PALETAS[nome], acentos: [...PALETAS[nome].acentos] };
|
|
58
65
|
// A cor da marca vira o acento da palavra (e só ela: a paleta não cresce).
|
|
59
66
|
const dest = roteiro.marca && roteiro.marca.cores && roteiro.marca.cores.destaque;
|
|
@@ -106,7 +113,7 @@ const TIPO = { logo: 'abertura', imagem: 'titulo', produto: 'titulo', grafico: '
|
|
|
106
113
|
* A partitura: as batidas, cada uma com as frases (e quando cada uma entra),
|
|
107
114
|
* a forma do portador e a transição. Sem áudio nem plano — é o que os testes olham.
|
|
108
115
|
*/
|
|
109
|
-
function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
116
|
+
function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [], imagens = {} }) {
|
|
110
117
|
const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object');
|
|
111
118
|
const semente = Number.isFinite(roteiro.semente) ? roteiro.semente : hash(JSON.stringify(partes.map((p) => [p.tipo, p.titulo, p.valor])));
|
|
112
119
|
const paleta = paletaDe(roteiro, semente);
|
|
@@ -116,9 +123,15 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
116
123
|
const link = (partes.find((p) => p.tipo === 'cta') || {}).link || '';
|
|
117
124
|
const cmd = /^(npm|npx|pnpm|yarn|pip|brew|curl|git|bun)\b/.test(link) ? link : String(marca.nome || 'app').toLowerCase().replace(/\s+/g, '-');
|
|
118
125
|
let comCartao = 0;
|
|
126
|
+
/* O cartão de produto só quando o vídeo É de produto: tem marca, um
|
|
127
|
+
comando de instalar ou um link. Um vídeo sobre sono saía com um
|
|
128
|
+
terminal "$ app" ao lado do título (teste de 08/10). `p.ui` explícito vale sempre. */
|
|
129
|
+
const deProduto = !!(marca.nome || link || /\b(app|software|plataforma|ferramenta|produto|sistema|saas|api|cli)\b/i.test(String(roteiro.pedido || '')));
|
|
119
130
|
|
|
120
131
|
const batidas = partes.map((p, i) => {
|
|
121
|
-
|
|
132
|
+
/* Fecho sem marca não monta a "marca de bolinhas" (vício, 08/10): vira
|
|
133
|
+
uma frase — e, curta, a gigante do fim. */
|
|
134
|
+
const tipo = p.tipo === 'cta' && !marca.nome ? 'titulo' : TIPO[p.tipo] || (FORMA[p.tipo] ? p.tipo : 'titulo');
|
|
122
135
|
const titulo = p.titulo || p.texto || '';
|
|
123
136
|
const fr = [];
|
|
124
137
|
const poe = (papel, texto, o = {}) => { if (String(texto || '').trim()) fr.push({ id: `${i}-${fr.length}`, papel, palavras: frase(texto, o) }); };
|
|
@@ -126,7 +139,8 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
126
139
|
/* O CARTÃO DE PRODUTO que flutua ao lado (terminal, chat, painel), feito
|
|
127
140
|
de forma e texto — e o texto vai para o outro lado. `ui: false` tira. */
|
|
128
141
|
const ui = p.ui === false ? null : typeof p.ui === 'string' ? p.ui
|
|
129
|
-
:
|
|
142
|
+
: !deProduto ? null
|
|
143
|
+
: tipo === 'titulo' || tipo === 'abertura' ? ['terminal', 'chat'][comCartao % 2] : tipo === 'numero' ? 'painel' : null;
|
|
130
144
|
if (ui) {
|
|
131
145
|
b.lado = comCartao++ % 2 ? 'dir' : 'esq';
|
|
132
146
|
b.ui = ui === 'terminal' ? { tipo: ui, linhas: ['$ ' + cmd, '✓ ' + caixaNormal(p.sub || titulo)] }
|
|
@@ -165,6 +179,53 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
165
179
|
return b;
|
|
166
180
|
});
|
|
167
181
|
|
|
182
|
+
/* ── A PALAVRA GIGANTE e a VIRADA (08/10): nas referências boas, o
|
|
183
|
+
momento forte é uma frase curta que ENCHE a tela, e o fundo vira
|
|
184
|
+
(o ponto cresce até cobrir tudo de cor). Sem isso o vídeo era a mesma
|
|
185
|
+
pílula pequena no mesmo azul por 36 s — "horríveis". Regras de editor:
|
|
186
|
+
só frase de até 4 palavras, abertura ou com destaque (a palavra-tese)
|
|
187
|
+
primeiro; gigante em até metade das batidas; a virada só nas que têm
|
|
188
|
+
destaque, 1 em 3 e nunca seguida. `p.gigante` explícito manda. */
|
|
189
|
+
const curta = (b) => { const f = b.frases.find((x) => x.papel === 'principal'); return f && f.palavras.length <= 4 && b.frases.length <= 2; };
|
|
190
|
+
const candidatas = batidas.map((b, i) => ({ b, i, p: partes[i] }))
|
|
191
|
+
.filter(({ b, p }) => p.gigante !== false && (b.tipo === 'titulo' || b.tipo === 'abertura') && !b.ui && !b.imagem && curta(b))
|
|
192
|
+
.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);
|
|
193
|
+
/* ── AS IMAGENS, quando a pessoa pediu (08/10: "deve ser misto com imagens
|
|
194
|
+
e motion, e com variadas animações"; o híbrido era "10 mil imagens de
|
|
195
|
+
banco e a mesma animação"). A imagem é UM momento: só nas batidas que
|
|
196
|
+
têm imagem baixada, no máximo 1 em 3, nunca duas seguidas. E cada uma
|
|
197
|
+
entra de um JEITO diferente, sem repetir no vídeo:
|
|
198
|
+
janela a foto abre da esquerda e cobre a tela, o texto por cima
|
|
199
|
+
circulo o ponto cresce em círculo com a foto dentro
|
|
200
|
+
cartao a foto num cartão que gira em 3D, o texto acima
|
|
201
|
+
faixa uma faixa da foto abre na altura, o texto abaixo
|
|
202
|
+
vazado o título gigante com a foto por DENTRO das letras */
|
|
203
|
+
const JEITOS = ['janela', 'circulo', 'cartao', 'faixa', 'vazado'];
|
|
204
|
+
const ordemJeitos = JEITOS.map((j, k) => ({ j, o: hash(j + semente) })).sort((a, b) => a.o - b.o).map((x) => x.j);
|
|
205
|
+
const tetoImagem = Math.max(1, Math.floor(batidas.length / 3));
|
|
206
|
+
let nImagem = 0;
|
|
207
|
+
for (const b of batidas) {
|
|
208
|
+
const img = imagens[b.i];
|
|
209
|
+
if (!img || !img.src || b.tipo === 'cta' || nImagem >= tetoImagem) continue;
|
|
210
|
+
if (batidas[b.i - 1] && batidas[b.i - 1].imagem) continue;
|
|
211
|
+
const pr = b.frases.find((f) => f.papel === 'principal');
|
|
212
|
+
let jeito = ordemJeitos[nImagem % ordemJeitos.length];
|
|
213
|
+
if (jeito === 'vazado' && !(pr && pr.palavras.length <= 3)) jeito = ordemJeitos[(nImagem + 1) % ordemJeitos.length] === 'vazado' ? 'janela' : ordemJeitos[(nImagem + 1) % ordemJeitos.length];
|
|
214
|
+
b.imagem = { src: img.src, jeito };
|
|
215
|
+
b.ui = null; b.lado = 'centro';
|
|
216
|
+
nImagem++;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A gigante pode seguir outra (as referências fazem isso); a VIRADA é o golpe: 1 em 3, nunca seguida.
|
|
220
|
+
const teto = Math.max(1, Math.ceil(batidas.length / 2)), tetoVirada = Math.max(1, Math.floor(batidas.length / 3));
|
|
221
|
+
for (const { b, i, p } of candidatas) {
|
|
222
|
+
if (batidas.filter((x) => x.gigante).length >= teto && p.gigante !== true) break;
|
|
223
|
+
b.gigante = true;
|
|
224
|
+
const viradas = batidas.filter((x) => x.virada).length;
|
|
225
|
+
const vizinha = (batidas[i - 1] && batidas[i - 1].virada) || (batidas[i + 1] && batidas[i + 1].virada);
|
|
226
|
+
if (i > 0 && viradas < tetoVirada && !vizinha && (p.destaque || p.gigante === true)) b.virada = true;
|
|
227
|
+
}
|
|
228
|
+
|
|
168
229
|
/* ── o tempo: a VOZ manda. A batida troca na fronteira da frase (um
|
|
169
230
|
pouco antes da primeira palavra da parte) e cada palavra na tela
|
|
170
231
|
aparece no instante em que é dita (os tempos por palavra da narração).
|
|
@@ -185,13 +246,16 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
185
246
|
cada palavra (a frase da tela pode vir em outra ordem que a falada)
|
|
186
247
|
e fica com o casamento que acha mais palavras. */
|
|
187
248
|
const casa = (k, d) => d === k || (k.length >= 4 && d.length >= 4 && d.slice(0, 5) === k.slice(0, 5));
|
|
249
|
+
/* Cada frase procura as suas palavras na fala INTEIRA da batida: com
|
|
250
|
+
um cursor só, o título que casava com o fim da fala deixava o
|
|
251
|
+
subtítulo sem achar as dele (entrava 0,45 s atrasado, 08/10). */
|
|
188
252
|
let ts = f.palavras.map(() => null), fim = cursor;
|
|
189
253
|
for (let ini = 0; ini < f.palavras.length; ini++) {
|
|
190
|
-
let c =
|
|
254
|
+
let c = 0;
|
|
191
255
|
const tt = f.palavras.map((w, j) => {
|
|
192
256
|
const k = limpa(w.t);
|
|
193
257
|
if (j < ini || !k) return null;
|
|
194
|
-
for (let q = c; q < Math.min(ditas.length, c +
|
|
258
|
+
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
259
|
return null;
|
|
196
260
|
});
|
|
197
261
|
if (tt.filter((x) => x != null).length > ts.filter((x) => x != null).length) { ts = tt; fim = c; }
|
|
@@ -201,7 +265,11 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
201
265
|
const ach = ts.findIndex((x) => x != null);
|
|
202
266
|
/* O que a voz não diz entra cedo o bastante para ser lido antes de a
|
|
203
267
|
frase acabar (e nunca antes da batida). */
|
|
204
|
-
|
|
268
|
+
/* O que a voz DIZ entra quando ela diz — nunca antes. Antes de 08/10
|
|
269
|
+
a frase entrava no começo da batida se a voz demorasse: "ontem à
|
|
270
|
+
noite" aparecia 2,8 s antes de ser dito, e o Joel viu "atrasos
|
|
271
|
+
entre a narração e o vídeo". */
|
|
272
|
+
let t = ach >= 0 && ts[ach] != null ? ts[ach] - ach * STAGGER
|
|
205
273
|
: tp ? Math.max(b.de + 0.25, Math.min(livre, fimVoz - leitura(f.palavras.length) - 0.05)) : livre;
|
|
206
274
|
f.palavras.forEach((w, j) => {
|
|
207
275
|
const falada = ts[j];
|
|
@@ -226,6 +294,7 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
226
294
|
|
|
227
295
|
return {
|
|
228
296
|
versao: 2, semente, fps: FPS, atraso, sincronia, largura, altura, duracao: ult.ate, paleta,
|
|
297
|
+
marcador: roteiro.marcador || ['brilho', 'numero', 'traco', 'ponto', 'nenhum'][hash('marcador' + semente) % 5],
|
|
229
298
|
fontes: { texto: 'Inter', serifa: 'Instrument Serif', mono: 'JetBrains Mono' },
|
|
230
299
|
logo: marca.logo || null,
|
|
231
300
|
batidas,
|
|
@@ -238,10 +307,10 @@ function partituraDe({ roteiro, largura = 1920, altura = 1080, tempos = [] }) {
|
|
|
238
307
|
* @param {Array} op.tempos [{de, ate, palavras}] por parte (da narração)
|
|
239
308
|
* @param {object} op.sons os efeitos leves (sons-leves.js)
|
|
240
309
|
*/
|
|
241
|
-
function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null }) {
|
|
310
|
+
function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], narracao = null, sons = {}, trilha = null, imagens = {} }) {
|
|
242
311
|
const partes = ((roteiro && roteiro.partes) || []).filter((p) => p && typeof p === 'object');
|
|
243
312
|
if (!partes.length) return { ok: false, error: 'o roteiro veio sem partes.' };
|
|
244
|
-
const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos });
|
|
313
|
+
const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos, imagens });
|
|
245
314
|
const DUR = partitura.duracao;
|
|
246
315
|
/* O SOM segue o movimento (sons-leves.js, tudo sintetizado): whoosh em
|
|
247
316
|
cada transformação, pop quando pílula ou cartão assenta, clique na roda e
|
|
@@ -277,7 +346,7 @@ function montarFluido({ roteiro, largura = 1920, altura = 1080, tempos = [], nar
|
|
|
277
346
|
efeitos,
|
|
278
347
|
},
|
|
279
348
|
};
|
|
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 } };
|
|
349
|
+
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
350
|
}
|
|
282
351
|
|
|
283
352
|
module.exports = { montarFluido, partituraDe, caixaNormal, PALETAS, POR_PALAVRA, MINIMO };
|
package/lib/estudio/index.js
CHANGED
|
@@ -43,6 +43,8 @@ const plano = require('./plano.js');
|
|
|
43
43
|
const remotion = require('./remotion.js');
|
|
44
44
|
const assets = require('./assets.js');
|
|
45
45
|
const video = require('./video.js');
|
|
46
|
+
const batida = require('./batida.js');
|
|
47
|
+
const revisor = require('./revisor.js');
|
|
46
48
|
const identidade = require('./identidade.js');
|
|
47
49
|
const slides = require('./slides.js');
|
|
48
50
|
const edicao = require('./edicao.js');
|
|
@@ -666,7 +668,7 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
666
668
|
if ((args.estilo || 'viral') === 'viral' && args.recorte !== false) {
|
|
667
669
|
const recortes = {};
|
|
668
670
|
for (const [i, info] of montado.infos.entries()) {
|
|
669
|
-
if (!
|
|
671
|
+
if (!info.recorte || !info.forte || info.forte.peso < 9 || Object.keys(recortes).length >= 3) continue;
|
|
670
672
|
const de = Math.max(0, info.forte.de - 0.15);
|
|
671
673
|
const ate = Math.min(info.dur, de + 2.2);
|
|
672
674
|
if (ate - de < 1) continue;
|
|
@@ -715,6 +717,18 @@ async function ferramentaVideo(args, ctx = {}) {
|
|
|
715
717
|
senão uma foto. Devolve {src, tipo} ou null. O clipe sai sorteado entre
|
|
716
718
|
os que os últimos vídeos não usaram (variedade.js), não o primeiro da
|
|
717
719
|
busca — "o b-roll veio igual nos dois vídeos" (Joel, 05/10). */
|
|
720
|
+
/** A foto editorial (Wikimedia, museu) que os últimos vídeos não usaram; o crédito fica no caderno. */
|
|
721
|
+
async function baixarEditorial(p, termo) {
|
|
722
|
+
try {
|
|
723
|
+
const b = await assets.procurarEditorial(termo, { quantas: 8 });
|
|
724
|
+
for (const item of variedade.ordenar(b.imagens, 'broll').slice(0, 4)) {
|
|
725
|
+
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'foto-' + termo });
|
|
726
|
+
if (d.ok) { variedade.marcar('broll', item.url); projeto.anotar(p.id, 'creditos', item.creditos); return { src: d.relativo, tipo: 'imagem' }; }
|
|
727
|
+
}
|
|
728
|
+
} catch { /* sem foto */ }
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
|
|
718
732
|
async function baixarApoio(p, termo, { orientacao = null, video: querVideo = false } = {}) {
|
|
719
733
|
if (querVideo) {
|
|
720
734
|
try {
|
|
@@ -749,11 +763,26 @@ async function variarIcones(plano, roteiro) {
|
|
|
749
763
|
const explicitos = [];
|
|
750
764
|
try { JSON.stringify(roteiro || {}, (k, v) => { if (k === 'icone' && typeof v === 'string') explicitos.push(v); return v; }); } catch { /* sem */ }
|
|
751
765
|
const troca = {};
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
766
|
+
for (const nome of variedade.iconesTrocaveis(plano, explicitos)) {
|
|
767
|
+
/* Primeiro O ÍCONE: o conceito escolhe entre as formas dele
|
|
768
|
+
(edicao.ALTERNATIVAS) pelos pontos da variedade — o que os últimos
|
|
769
|
+
vídeos menos usaram. Depois A COLEÇÃO. */
|
|
770
|
+
const base = nome.split(':')[1];
|
|
771
|
+
let escolhido = base;
|
|
772
|
+
const alts = edicao.ALTERNATIVAS[base];
|
|
773
|
+
if (alts) {
|
|
774
|
+
const ordem = variedade.ordenar([base, ...alts].map((url) => ({ url })), 'icone-nome').map((x) => x.url);
|
|
775
|
+
for (const n of ordem) {
|
|
776
|
+
if (n === base) { escolhido = base; break; }
|
|
777
|
+
try { const r = await assets.icone('fluent-3d:' + n); if (r && r.ok) { escolhido = n; break; } } catch { /* tenta o próximo */ }
|
|
778
|
+
}
|
|
779
|
+
variedade.marcar('icone-nome', escolhido);
|
|
756
780
|
}
|
|
781
|
+
let novo = 'fluent-3d:' + escolhido;
|
|
782
|
+
if (colecao !== 'fluent-3d') {
|
|
783
|
+
try { const r = await assets.icone(colecao + ':' + escolhido); if (r && r.ok) novo = colecao + ':' + escolhido; } catch { /* fica o 3D */ }
|
|
784
|
+
}
|
|
785
|
+
if (novo !== nome) troca[nome] = novo;
|
|
757
786
|
}
|
|
758
787
|
const n = variedade.trocarIcones(plano, troca);
|
|
759
788
|
if (n || colecao === 'fluent-3d') variedade.marcar('icones', colecao);
|
|
@@ -1008,16 +1037,13 @@ async function prepararRoteiro(p, args) {
|
|
|
1008
1037
|
IMPACTO. O que o agente pediu vale mais que tudo. */
|
|
1009
1038
|
const pedidoTxt = `${p.pedido || ''} ${args.pedido || ''}`;
|
|
1010
1039
|
const ref = referencias.atual();
|
|
1011
|
-
/*
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
produto em 3D entra numa batida dele, logo abaixo). */
|
|
1019
|
-
if (ref.modo === 'sem' && estilo === 'hibrido') estilo = 'gerativo';
|
|
1020
|
-
void produtoNoPedido;
|
|
1040
|
+
/* Sem imagem é o padrão (07/10): sem o pedido dizer, nada de foto nem
|
|
1041
|
+
vídeo de fora. Ninguém escolheu o estilo? Ele gira (estilo.js) — sem
|
|
1042
|
+
imagem, só entre os que não vivem de foto. O híbrido e o cinema, pedidos
|
|
1043
|
+
pelo nome sem imagem, caem para o gerativo (sem nada baixado). */
|
|
1044
|
+
const semImagem = referencias.semImagem(ref);
|
|
1045
|
+
let estilo = escolherEstilo({ pedido: pedidoTxt, agente: args.estilo, roteiro: roteiro.estilo, girar: true, semImagem });
|
|
1046
|
+
if (semImagem && (estilo === 'hibrido' || estilo === 'cinema')) estilo = 'gerativo';
|
|
1021
1047
|
/* "O vídeo deve ter um 3D do fone" (30/09): pedido de 3D vira o gerativo
|
|
1022
1048
|
com o produto modelado girando numa batida (kit/tres.jsx) — a menos que
|
|
1023
1049
|
o agente tenha escolhido outro estilo pelo nome. */
|
|
@@ -1046,8 +1072,8 @@ async function prepararRoteiro(p, args) {
|
|
|
1046
1072
|
todos iguais" (02/10). A marca manda; depois o roteiro.visual da IA;
|
|
1047
1073
|
sem nada, o sorteio que pula o que os últimos vídeos usaram. */
|
|
1048
1074
|
if (['hibrido', 'cinema', 'impacto'].includes(estilo)) {
|
|
1049
|
-
const v = biblioteca.vestir(roteiro);
|
|
1050
|
-
if (v.mudou) projeto.anotar(p.id, 'visual', [v.titulo && `fonte ${v.titulo} + ${v.texto}`, v.paleta && `paleta ${v.paleta}`].filter(Boolean).join(', '));
|
|
1075
|
+
const v = biblioteca.vestir(roteiro, { estilo });
|
|
1076
|
+
if (v.mudou) projeto.anotar(p.id, 'visual', [v.titulo && `fonte ${v.titulo} + ${v.texto} (tom ${v.tom})`, v.paleta && `paleta ${v.paleta}`].filter(Boolean).join(', '));
|
|
1051
1077
|
}
|
|
1052
1078
|
// Os estilos de motion usam os efeitos leves e a trilha do ende.app; os de corte, os cheios.
|
|
1053
1079
|
const ehMotion = LEVES.has(estilo);
|
|
@@ -1079,13 +1105,13 @@ async function prepararRoteiro(p, args) {
|
|
|
1079
1105
|
const videosMarca = [...(acervo.videos || [])];
|
|
1080
1106
|
const imagens = {}, fundos = {};
|
|
1081
1107
|
for (const [i, parte] of partes.entries()) {
|
|
1082
|
-
if (parte.tipo !== 'imagem' || parte.src) continue;
|
|
1108
|
+
if (semImagem || parte.tipo !== 'imagem' || parte.src) continue;
|
|
1083
1109
|
const texto = `${parte.titulo || ''} ${parte.sub || ''} ${parte.fala || ''} ${parte.busca || ''}`;
|
|
1084
1110
|
if (modo === 'video' && videosMarca.length && i % 2 === 1) { imagens[i] = { src: videosMarca.shift().src, tipo: 'video' }; continue; }
|
|
1085
1111
|
const m = melhorFoto(texto) || (parte.busca ? await baixarApoio(p, parte.busca, { orientacao: largura >= altura ? 'landscape' : 'portrait', video: modo === 'video' }) : null);
|
|
1086
1112
|
if (m) imagens[i] = m;
|
|
1087
1113
|
}
|
|
1088
|
-
if (acervo.fotos.length) {
|
|
1114
|
+
if (acervo.fotos.length && !semImagem) {
|
|
1089
1115
|
const comFundo = ['abertura', 'titulo', 'contraste', 'citacao', 'cta', 'passos'];
|
|
1090
1116
|
let alterna = 0;
|
|
1091
1117
|
for (const [i, parte] of partes.entries()) {
|
|
@@ -1408,9 +1434,8 @@ async function prepararRoteiro(p, args) {
|
|
|
1408
1434
|
funk para o fone, eletrônica para IA…), sorteia a faixa e não
|
|
1409
1435
|
repete as últimas usadas — nada de "inspiring corporate". */
|
|
1410
1436
|
let m = null;
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
try { usadas = JSON.parse(fs.readFileSync(usadasArq, 'utf8')); } catch { usadas = []; }
|
|
1437
|
+
// A memória da trilha é a da variedade ('trilha'): antes era um arquivo à parte (trilhas-usadas.json).
|
|
1438
|
+
const usadas = variedade.usadas('trilha');
|
|
1414
1439
|
const mus = estilo === 'gerativo' ? gerativo.musicaDe(roteiro) : null;
|
|
1415
1440
|
/* A trilha do gerativo vem do Jamendo (Openverse), pelo GÊNERO do
|
|
1416
1441
|
assunto, instrumental, sorteada e sem repetir as últimas — a
|
|
@@ -1421,9 +1446,10 @@ async function prepararRoteiro(p, args) {
|
|
|
1421
1446
|
classical: 'piano classical instrumental', 'soul-rnb': 'soul instrumental' };
|
|
1422
1447
|
try {
|
|
1423
1448
|
const b = await assets.procurarMusica(BUSCA[roteiro.genero || mus.genero] || ((roteiro.genero || mus.genero) + ' instrumental'), { quantas: 8, duracaoMin: Math.max(45, segundos + 10) });
|
|
1424
|
-
|
|
1449
|
+
// Pelos pontos da variedade: a recente e a muito usada vão para o fim, não somem.
|
|
1450
|
+
const opcoes = variedade.ordenar((b.ok ? b.sons : []).filter((x) => !/vocal|voice|feat|ft\.|remix/i.test(x.titulo || '')), 'trilha');
|
|
1425
1451
|
for (let k = 0; k < opcoes.length && !m; k++) {
|
|
1426
|
-
const item = opcoes[
|
|
1452
|
+
const item = opcoes[k];
|
|
1427
1453
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha-' + (roteiro.genero || mus.genero) });
|
|
1428
1454
|
if (!d.ok) continue;
|
|
1429
1455
|
if (await temLetra(path.join(p.pasta, d.relativo))) { projeto.anotar(p.id, 'midia', `trilha com voz descartada: ${item.titulo}`); continue; }
|
|
@@ -1432,12 +1458,12 @@ async function prepararRoteiro(p, args) {
|
|
|
1432
1458
|
} catch { /* cai na ende.app */ }
|
|
1433
1459
|
}
|
|
1434
1460
|
if (mus && m) {
|
|
1435
|
-
|
|
1461
|
+
variedade.marcar('trilha', m.id);
|
|
1436
1462
|
projeto.anotar(p.id, 'trilha', `gênero ${roteiro.genero || mus.genero} (Jamendo)`);
|
|
1437
1463
|
} else if (mus) {
|
|
1438
1464
|
m = await ende.provedor({ pasta: path.join(p.pasta, 'midia') }).gerar({ clima: roteiro.clima || 'energetico', segundos, genero: roteiro.genero || mus.genero, semente: mus.semente, evitar: usadas });
|
|
1439
1465
|
const id = m && Number((/ende-(\d+)\.mp3$/.exec(m.src) || [])[1]);
|
|
1440
|
-
if (id)
|
|
1466
|
+
if (id) variedade.marcar('trilha', id);
|
|
1441
1467
|
if (m) projeto.anotar(p.id, 'trilha', `gênero ${roteiro.genero || mus.genero}`);
|
|
1442
1468
|
} else m = await ende.provedor({ pasta: path.join(p.pasta, 'midia') }).gerar({ clima: roteiro.clima || 'tecnologia', segundos });
|
|
1443
1469
|
if (m) { trilha = path.relative(p.pasta, m.src).replace(/\\/g, '/'); projeto.anotar(p.id, 'trilha', m.credito); }
|
|
@@ -1512,8 +1538,48 @@ async function prepararRoteiro(p, args) {
|
|
|
1512
1538
|
return r;
|
|
1513
1539
|
}
|
|
1514
1540
|
if (estilo === 'fluido' && modo === 'video') {
|
|
1515
|
-
|
|
1516
|
-
|
|
1541
|
+
/* A CARA de cada vídeo do fluido, por PROJETO (ajustar uma frase não
|
|
1542
|
+
troca): a semente (marcador, jeito das imagens) e a paleta, que a
|
|
1543
|
+
memória de variedade não repete do vídeo anterior (08/10: "sempre o
|
|
1544
|
+
mesmo azul"). A paleta ou o tema pedidos mandam. */
|
|
1545
|
+
if (roteiro.semente == null) {
|
|
1546
|
+
roteiro.semente = p.semente != null ? p.semente : Math.floor(Math.random() * 1e9);
|
|
1547
|
+
if (p.semente == null) projeto.salvar(p.id, { semente: roteiro.semente });
|
|
1548
|
+
}
|
|
1549
|
+
/* O modelo também vicia: pedia "noite" em todo vídeo (era a primeira da
|
|
1550
|
+
lista). A paleta dele vale, menos quando é uma das 2 últimas usadas —
|
|
1551
|
+
aí vem a menos usada do mesmo tema (escura ou clara). */
|
|
1552
|
+
if (!p.paletaFluido) {
|
|
1553
|
+
const P = fluido.PALETAS, nomes = Object.keys(P);
|
|
1554
|
+
const pedida = P[String(roteiro.paleta || '').toLowerCase()] ? String(roteiro.paleta).toLowerCase() : null;
|
|
1555
|
+
const recentes = variedade.usadas('paleta-fluido').slice(0, 2);
|
|
1556
|
+
const escuro = pedida ? P[pedida].escuro : /escuro|dark/i.test(roteiro.tema || '') ? true : /claro|light/i.test(roteiro.tema || '') ? false : null;
|
|
1557
|
+
let escolha = pedida && !recentes.includes(pedida) ? pedida : null;
|
|
1558
|
+
if (!escolha) escolha = variedade.sortear(nomes.filter((n) => escuro == null || P[n].escuro === escuro), 'paleta-fluido');
|
|
1559
|
+
else variedade.marcar('paleta-fluido', escolha);
|
|
1560
|
+
if (pedida && escolha !== pedida) projeto.anotar(p.id, 'visual', `paleta ${pedida} pedida, mas foi a de um dos últimos vídeos: ${escolha}`);
|
|
1561
|
+
projeto.salvar(p.id, { paletaFluido: escolha });
|
|
1562
|
+
p.paletaFluido = escolha;
|
|
1563
|
+
}
|
|
1564
|
+
roteiro.paleta = p.paletaFluido;
|
|
1565
|
+
/* As IMAGENS do fluido misto, só com imagem pedida: as partes com
|
|
1566
|
+
"busca", no máximo 1 em 3 e espalhadas (fluido.js decide o jeito). */
|
|
1567
|
+
const fotosFluido = {};
|
|
1568
|
+
if (!semImagem) {
|
|
1569
|
+
const comBusca = partes.map((pt, k) => (pt.busca || pt.src ? k : -1)).filter((k) => k >= 0 && (partes[k].tipo !== 'cta'));
|
|
1570
|
+
const teto = Math.max(1, Math.floor(partes.length / 3));
|
|
1571
|
+
const escolhidas = [];
|
|
1572
|
+
for (const k of comBusca) if (escolhidas.length < teto && !escolhidas.some((j) => Math.abs(j - k) < 2)) escolhidas.push(k);
|
|
1573
|
+
for (const k of escolhidas) {
|
|
1574
|
+
const pt = partes[k];
|
|
1575
|
+
// O acervo EDITORIAL (assets.procurarEditorial), não o banco de stock: sem foto que sirva, a parte fica sem imagem.
|
|
1576
|
+
const m = pt.src ? { src: pt.src } : await baixarEditorial(p, pt.busca);
|
|
1577
|
+
if (m && m.src) fotosFluido[k] = { src: m.src };
|
|
1578
|
+
}
|
|
1579
|
+
if (Object.keys(fotosFluido).length) projeto.anotar(p.id, 'midia', `fluido misto: imagem nas partes ${Object.keys(fotosFluido).map((k) => Number(k) + 1).join(', ')}`);
|
|
1580
|
+
}
|
|
1581
|
+
const r = fluido.montarFluido({ imagens: fotosFluido, roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt }, largura, altura, tempos, narracao, sons, trilha });
|
|
1582
|
+
if (r.ok) projeto.anotar(p.id, 'plano', `estilo fluido: ${r.resumo.batidas} batidas${r.resumo.imagens.length ? ` (imagens: ${r.resumo.imagens.join(', ')})` : ''}, paleta ${r.resumo.paleta}, ${r.resumo.segundos}s, batida atrasada no máximo ${r.resumo.atraso}s, palavra atrás da voz no máximo ${r.resumo.sincronia}s`);
|
|
1517
1583
|
return r;
|
|
1518
1584
|
}
|
|
1519
1585
|
if (estilo === 'motion' && modo === 'video') {
|
|
@@ -1600,10 +1666,34 @@ async function ferramentaRender(args, ctx = {}) {
|
|
|
1600
1666
|
problemas: conferida.problemas.slice(0, 12) };
|
|
1601
1667
|
}
|
|
1602
1668
|
|
|
1669
|
+
/* A REVISÃO (revisor.js): o plano roda; agora, ele está bom? Conserta a
|
|
1670
|
+
receita (transição repetida, câmera sem respiro, som duplicado) e
|
|
1671
|
+
devolve o resto como aviso para quem fez o plano. */
|
|
1672
|
+
const revisao = revisor.revisar(pl, { semImagem: referencias.semImagem() });
|
|
1673
|
+
if (revisao.consertos.length) projeto.anotar(args.id, 'revisao', revisao.consertos.join('; '));
|
|
1674
|
+
if (revisao.avisos.length) projeto.anotar(args.id, 'revisao', 'avisos: ' + revisao.avisos.join('; '));
|
|
1675
|
+
|
|
1603
1676
|
// Os ícones vêm ANTES do render: um render que busca ícone na internet
|
|
1604
1677
|
// falha no meio, depois de vinte minutos.
|
|
1605
1678
|
const { mapa, faltaram } = await assets.iconesDoPlano(pl, p.pasta);
|
|
1606
1679
|
|
|
1680
|
+
/* A MÚSICA NO CORTE (batida.js): a faixa começa no ponto em que as
|
|
1681
|
+
batidas caem nos cortes do plano, e o corte de impacto num acento. Só
|
|
1682
|
+
quando ninguém disse o início (audio.trilha.inicio). */
|
|
1683
|
+
const tr = pl.audio && pl.audio.trilha;
|
|
1684
|
+
if (tr && !(typeof tr === 'object' && tr.inicio != null) && !(p.formato === 'slides' || p.formato === 'arte' || args.paginas)) {
|
|
1685
|
+
try {
|
|
1686
|
+
const src = path.join(p.pasta, tr.src || tr);
|
|
1687
|
+
const analise = await batida.analisar(src, { ffmpeg: video.ffmpeg() || 'ffmpeg' });
|
|
1688
|
+
if (analise && analise.batidas.length) {
|
|
1689
|
+
const duracao = (pl.cenas || []).reduce((n, c) => n + (Number(c.duracao) || 0), 0);
|
|
1690
|
+
const r = batida.melhorInicio(batida.cortesDoPlano(pl), analise, { maximo: Math.min(20, analise.segundos - duracao - 0.5) });
|
|
1691
|
+
pl.audio.trilha = { ...(typeof tr === 'object' ? tr : { src: tr }), inicio: r.inicio };
|
|
1692
|
+
projeto.anotar(args.id, 'trilha', `${analise.bpm} BPM; a faixa entra em ${r.inicio}s e ${r.alinhados} de ${r.total} cortes caem na batida`);
|
|
1693
|
+
}
|
|
1694
|
+
} catch { /* sem análise, a faixa entra do começo */ }
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1607
1697
|
const montado = remotion.montar({ pasta: p.pasta, titulo: p.titulo, plano: pl, icones: mapa });
|
|
1608
1698
|
|
|
1609
1699
|
if (args.somenteProjeto) {
|
|
@@ -1669,7 +1759,9 @@ async function ferramentaRender(args, ctx = {}) {
|
|
|
1669
1759
|
return {
|
|
1670
1760
|
ok: true, arquivo: saida, bytes, segundos: conferida.duracao,
|
|
1671
1761
|
faltaram: faltaram.length ? faltaram : undefined,
|
|
1672
|
-
|
|
1762
|
+
revisao: revisao.consertos.length || revisao.avisos.length ? { consertado: revisao.consertos, avisos: revisao.avisos } : undefined,
|
|
1763
|
+
aviso: 'ABERTO para o usuário assistir. Chame finish dizendo o que foi feito'
|
|
1764
|
+
+ (revisao.avisos.length ? ' — e cite os avisos da revisão que valem um novo plano.' : '.'),
|
|
1673
1765
|
};
|
|
1674
1766
|
}
|
|
1675
1767
|
|
|
@@ -1803,7 +1895,7 @@ async function ferramentaSite(args) {
|
|
|
1803
1895
|
return r;
|
|
1804
1896
|
}
|
|
1805
1897
|
|
|
1806
|
-
module.exports = {
|
|
1898
|
+
module.exports = { variarIcones,
|
|
1807
1899
|
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
|
|
1808
1900
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
1809
1901
|
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro,
|