primocode 10.5.0 → 10.6.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 +117 -24
- package/lib/act.js +141 -9
- package/lib/agents-md.js +140 -0
- package/lib/auditoria.js +122 -0
- package/lib/browser.js +48 -1
- package/lib/catalogo.js +24 -4
- package/lib/comandos-extra.js +2 -0
- package/lib/config.js +32 -4
- package/lib/entrada.js +33 -11
- package/lib/estudio/colecao.js +34 -1
- package/lib/estudio/diretor.js +1 -0
- package/lib/estudio/falantes.js +30 -6
- package/lib/estudio/hibrido.js +144 -8
- package/lib/estudio/index.js +151 -59
- package/lib/estudio/kit/mundo.jsx +1060 -0
- package/lib/estudio/kit/voxel.jsx +315 -70
- package/lib/estudio/kit.js +1 -1
- package/lib/estudio/kokoro.js +18 -4
- package/lib/estudio/margem.js +1 -1
- package/lib/estudio/mundo.js +305 -0
- package/lib/estudio/personagem.js +164 -11
- package/lib/estudio/producao.js +107 -17
- package/lib/estudio/projeto.js +77 -22
- package/lib/leitura.js +176 -0
- package/lib/local/cerebro.json +180 -3
- package/lib/local/entrada.js +61 -4
- package/lib/mcp.js +296 -12
- package/lib/memoria-conversa.js +32 -3
- package/lib/mencoes.js +5 -1
- package/lib/nome-curto.js +126 -0
- package/lib/nome-projeto.js +5 -2
- package/lib/outra-pasta.js +161 -0
- package/lib/perigo.js +34 -2
- package/lib/referencia.js +40 -11
- package/lib/repeticao.js +20 -1
- package/lib/tela.js +1 -1
- package/lib/tools.js +76 -6
- package/lib/travado.js +141 -0
- package/lib/ui.js +1 -0
- package/package.json +2 -2
package/lib/estudio/diretor.js
CHANGED
|
@@ -288,6 +288,7 @@ REGRAS QUE SEPARAM UM VÍDEO BOM DE UM GENÉRICO:
|
|
|
288
288
|
5. RITMO VARIADO. Cenas de durações diferentes e transições diferentes. Tudo em fade parece slide.
|
|
289
289
|
5b. NADA DE GAVETA. Não existe tema pronto, layout pronto nem fundo pronto: é você que decide a cor, a tipografia e a composição DESTE vídeo. Dois vídeos seus sobre assuntos diferentes não podem sair parecidos.
|
|
290
290
|
6. MÍDIA COM PROPÓSITO. Só peça imagem ou logo quando ela PROVA algo. Diga o termo de busca de verdade ("painel solar em telhado residencial", não "imagem legal").
|
|
291
|
+
6b. CADA FRASE PEDE UMA ROTA DE IMAGEM, nesta ordem: número ou citação → nenhuma (a tipografia é a imagem); fala do produto → a tela ou a logo dele; coisa concreta → o vídeo DELA, com a busca no substantivo ("espresso cup", não a frase inteira); ideia abstrata → textura que se move. A imagem entra logo DEPOIS da palavra que a nomeia, nunca antes.
|
|
291
292
|
7. MOVIMENTO. Em cada cena diga em "chaves" o movimento principal — é ele que tira a cara de apresentação parada.
|
|
292
293
|
8. CÂMERA COM PARCIMÔNIA. Movimento de quadro em UMA cena a cada duas ou três ("camera": "nenhuma" nas outras): o que dá ritmo é alternar cena parada e cena que se move. Em cena com foto de fundo, prefira "aproximar" ou "empurrar".
|
|
293
294
|
9. UMA VOZ OU DUAS. Se o pedido fala em dois narradores, duas vozes, diálogo, conversa ou entrevista, escreva como DIÁLOGO: dê nome a cada um em "vozes" (com vozes diferentes) e diga em cada cena quem fala ("falante"). Com uma voz só, deixe "vozes" e "falante" de fora.
|
package/lib/estudio/falantes.js
CHANGED
|
@@ -35,22 +35,46 @@ function normalizar(roteiro) {
|
|
|
35
35
|
return r;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/* A VOZ PELO GÊNERO (11/10, "os personagens falam com vozes diferentes em
|
|
39
|
+
cada cena"): o par antigo dava a primeira voz livre a quem falasse
|
|
40
|
+
primeiro — a Ana saía com a voz do Antonio. Agora o NOME decide o gênero e
|
|
41
|
+
cada falante ganha uma voz do Gemini só dele, do começo ao fim do vídeo.
|
|
42
|
+
O que a IA disse (roteiro.vozes, personagens.x.genero) manda. */
|
|
43
|
+
const MASCULINAS = ['Charon', 'Puck', 'Orus', 'Fenrir'];
|
|
44
|
+
const FEMININAS = ['Kore', 'Aoede', 'Leda', 'Zephyr'];
|
|
45
|
+
const ROBO = /\b(ia|ai|assistente|robo|robô|bot|claude|gpt|chatgpt|gemini|primo|primocode|copilot|agente|mascote)\b/i;
|
|
46
|
+
const NOMES_FEMININOS = /^(beatriz|raquel|isabel|carmen|ester|ines|inês|alice|ruth|nicole|lis|luz|rose|ingrid|kelly|jennifer|michelle|rachel|karen|manu|gabi|bel|mel|vitoria|vitória|yasmin|jasmin|carol|isis|ísis|iris|íris|liz|mirian|miriam|ellen|helen|lourdes|mercedes|dolores|socorro|aline|daiane|tais|taís|thais|thaís|lilian|vivian|suellen|jaqueline|simone|rosangela|rosângela|irene|lucimar|nair|jussara|joice|joyce|cris|dani|fer|ju|lu|bia|mãe|mae|vó|vo|avó|avo|tia|dona|senhora|professora|menina|garota|mulher|rainha|princesa|moça|moca)$/;
|
|
47
|
+
const NOMES_MASCULINOS_EM_A = /^(luca|lucca|joshua|nicola|andrea|elias|matias|tobias|jonas|lucas|thomas|tomas|tomás|bruna?x|zuma)$/;
|
|
48
|
+
function generoDe(nome, pedido = {}) {
|
|
49
|
+
const g = String((pedido && pedido.genero) || '').toLowerCase();
|
|
50
|
+
if (/^(f|fem|feminin|mulher)/.test(g)) return 'f';
|
|
51
|
+
if (/^(m|masc|homem)/.test(g)) return 'm';
|
|
52
|
+
const n = String(nome || '').trim().toLowerCase().split(/\s+/)[0];
|
|
53
|
+
if (ROBO.test(n)) return 'robo';
|
|
54
|
+
if (NOMES_FEMININOS.test(n)) return 'f';
|
|
55
|
+
if (NOMES_MASCULINOS_EM_A.test(n)) return 'm';
|
|
56
|
+
return /a$/.test(n) ? 'f' : 'm';
|
|
57
|
+
}
|
|
58
|
+
|
|
38
59
|
/**
|
|
39
60
|
* A voz de cada parte, na ordem. Com falante e voz no mapa, a do mapa; com
|
|
40
|
-
* falante FORA do mapa
|
|
41
|
-
*
|
|
42
|
-
* sem falante, a `padrao` (roteiro.voz).
|
|
61
|
+
* falante FORA do mapa, uma voz do Gemini do gênero do nome que ainda não
|
|
62
|
+
* fala neste vídeo; sem falante, a `padrao` (roteiro.voz).
|
|
43
63
|
*/
|
|
44
64
|
function vozesDasPartes(partes, roteiro = {}, padrao = '') {
|
|
45
65
|
const mapa = { ...((roteiro && roteiro.vozes) || {}) };
|
|
46
66
|
const ocupadas = new Set(Object.values(mapa).map((v) => String(v).toLowerCase()));
|
|
47
67
|
if (padrao) ocupadas.add(String(padrao).toLowerCase());
|
|
68
|
+
const pedidos = (roteiro && roteiro.personagens) || {};
|
|
69
|
+
const livre = (lista, k) => lista.find((v) => !ocupadas.has(v.toLowerCase())) || lista[k % lista.length];
|
|
48
70
|
return (partes || []).map((pt) => {
|
|
49
71
|
const f = nomeFalante(pt && pt.falante);
|
|
50
72
|
if (!f) return padrao || '';
|
|
51
73
|
if (!mapa[f]) {
|
|
52
|
-
|
|
53
|
-
|
|
74
|
+
const g = generoDe(f, pedidos[f] || pedidos[Object.keys(pedidos).find((k) => k.toLowerCase() === f)] || {});
|
|
75
|
+
// O mascote tem a voz mais leve das masculinas (Puck), se estiver livre.
|
|
76
|
+
mapa[f] = g === 'f' ? livre(FEMININAS, Object.keys(mapa).length) : g === 'robo' && !ocupadas.has('puck') ? 'Puck' : livre(MASCULINAS, Object.keys(mapa).length);
|
|
77
|
+
ocupadas.add(mapa[f].toLowerCase());
|
|
54
78
|
}
|
|
55
79
|
return mapa[f];
|
|
56
80
|
});
|
|
@@ -94,4 +118,4 @@ async function narrarPorFalante(lista, mp3, { narrarUm, juntar }) {
|
|
|
94
118
|
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
119
|
}
|
|
96
120
|
|
|
97
|
-
module.exports = { normalizar, vozesDasPartes, narrarPorFalante, PAR };
|
|
121
|
+
module.exports = { generoDe, MASCULINAS, FEMININAS, normalizar, vozesDasPartes, narrarPorFalante, PAR };
|
package/lib/estudio/hibrido.js
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
* traduzir(roteiro) qualquer roteiro (vox, motion, composição) vira os
|
|
22
22
|
* tipos do cinema; o que o cinema não usa (a fonte, o
|
|
23
23
|
* destaque) fica guardado por parte.
|
|
24
|
+
* rotaDaParte(parte, …) qual imagem a frase pede (4 rotas) e em que palavra
|
|
25
|
+
* o corte se ancora (b-roll-finder, Arthur 17/09).
|
|
24
26
|
* termosDeBusca(roteiro) o que procurar de b-roll para cada parte.
|
|
25
27
|
* enfeitar(plano, …) o kit por cima do plano que o cinema montou.
|
|
26
28
|
*/
|
|
@@ -36,6 +38,65 @@ const limpo = (x) => String(x || '').replace(/\s+/g, ' ').trim();
|
|
|
36
38
|
const BUSCA_PADRAO = ['technology abstract light', 'people working on laptop', 'hands typing keyboard close up',
|
|
37
39
|
'data center servers', 'city night timelapse', 'team meeting office', 'developer code screen', 'futuristic digital network'];
|
|
38
40
|
|
|
41
|
+
/* AS 4 ROTAS DO B-ROLL (do b-roll-finder): a frase diz qual imagem pede, e
|
|
42
|
+
a primeira regra que casa decide. O roteiro manda: `rota` e `busca` na parte.
|
|
43
|
+
tipografia número, gráfico, citação → sem imagem: o texto é a imagem
|
|
44
|
+
produto a frase fala da marca/produto → a tela/foto da marca
|
|
45
|
+
literal uma coisa concreta ("café") → o vídeo de banco DELA
|
|
46
|
+
abstrato ideia que teria de ser desenhada → textura que se move
|
|
47
|
+
O termo sai da PALAVRA-ÂNCORA, não da frase inteira; e o clipe troca 0,2 s
|
|
48
|
+
DEPOIS dela — "atrasado parece intenção, adiantado parece erro". */
|
|
49
|
+
const ROTAS = ['tipografia', 'produto', 'literal', 'abstrato'];
|
|
50
|
+
const ATRASO_ANCORA = require('./edicao.js').DEPOIS_DA_PALAVRA;
|
|
51
|
+
const PRODUTO = /^(app|aplicativ|ferrament|painel|dashboard|plataform|site|terminal|cli|sistema|interface|software|agente)/;
|
|
52
|
+
/* As COISAS que se filmam: substantivo concreto → o termo do banco, em inglês.
|
|
53
|
+
O ícone da edição (edicao.iconeDe) casa verbo também ("começa" → foguete),
|
|
54
|
+
bom para emoji, ruim para b-roll literal; aqui só entra o que tem cara. */
|
|
55
|
+
const COISAS = [
|
|
56
|
+
[['cafe'], 'coffee cup morning'], [['cachorro', 'cao'], 'dog'], [['gato'], 'cat'], [['casa', 'lar'], 'modern house'],
|
|
57
|
+
[['carro'], 'car driving road'], [['cidade', 'rua'], 'city street people'], [['mar', 'praia', 'oceano'], 'ocean waves'],
|
|
58
|
+
[['escritorio', 'reuniao'], 'team meeting office'], [['mao', 'maos'], 'hands close up'], [['teclado', 'digit'], 'hands typing keyboard close up'],
|
|
59
|
+
[['computador', 'notebook', 'laptop'], 'laptop on desk'], [['codigo', 'programa'], 'developer code screen'], [['celular', 'telefone'], 'smartphone in hand'],
|
|
60
|
+
[['livro', 'aula', 'escola'], 'library books'], [['musica'], 'music concert'], [['pessoa', 'equipe', 'cliente', 'gente'], 'people working together'],
|
|
61
|
+
[['servidor', 'nuvem'], 'data center servers'], [['dinheiro'], 'money cash'], [['relogio'], 'clock ticking'],
|
|
62
|
+
];
|
|
63
|
+
const coisaDe = (limpo) => { for (const [raizes, termo] of COISAS) if (raizes.some((r) => (r.length <= 3 ? limpo === r : limpo.startsWith(r)))) return termo; return null; };
|
|
64
|
+
const ABSTRATO = ['abstract light texture', 'ink in water slow motion', 'bokeh lights dark background', 'smoke texture dark'];
|
|
65
|
+
|
|
66
|
+
/** A rota e a âncora de uma parte. `palavras`: [{texto, de, ate}] da voz (ou só o texto). */
|
|
67
|
+
function rotaDaParte(parte, palavras, { marca = null, i = 0 } = {}) {
|
|
68
|
+
const p = parte || {};
|
|
69
|
+
const ed = require('./edicao.js');
|
|
70
|
+
const ws = (palavras && palavras.length ? palavras : String(p.fala || p.texto || p.titulo || '').split(/\s+/).filter(Boolean).map((texto) => ({ texto })));
|
|
71
|
+
const limpa = (x) => String(x || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
72
|
+
const achar = (alvo) => { const a = limpa(String(alvo || '').split(/\s+/)[0]); return a ? ws.find((w) => limpa(w.texto) === a) || null : null; };
|
|
73
|
+
// A palavra que carrega a frase: a mais longa que não é vazia ("confiança", não "um").
|
|
74
|
+
const forte = () => ws.reduce((f, w) => (ed.peso(w.texto) && limpa(w.texto).length > (f ? limpa(f.texto).length : 0) ? w : f), null);
|
|
75
|
+
const tipo = p.tipoOriginal || p.tipo || '';
|
|
76
|
+
let r;
|
|
77
|
+
if (ROTAS.includes(p.rota)) r = { rota: p.rota, palavra: null };
|
|
78
|
+
else if (['numero', 'grafico', 'citacao', 'numeros'].includes(tipo)) r = { rota: 'tipografia', palavra: null };
|
|
79
|
+
else {
|
|
80
|
+
const e = ed.rotaDeApoio(ws, { marca });
|
|
81
|
+
const prod = ws.find((w) => ed.peso(w.texto) && PRODUTO.test(limpa(w.texto)));
|
|
82
|
+
const coisa = ws.find((w) => ed.peso(w.texto) && coisaDe(limpa(w.texto)));
|
|
83
|
+
if (e.rota === 'marca' || prod) r = { rota: 'produto', palavra: e.rota === 'marca' ? e.palavra : prod, termo: 'software interface on laptop screen' };
|
|
84
|
+
else if (e.rota === 'numero') r = { rota: 'tipografia', palavra: e.palavra };
|
|
85
|
+
else if (coisa) r = { rota: 'literal', palavra: coisa, termo: coisaDe(limpa(coisa.texto)) };
|
|
86
|
+
else r = { rota: p.busca ? 'literal' : 'abstrato', palavra: forte(), termo: ABSTRATO[i % ABSTRATO.length] };
|
|
87
|
+
}
|
|
88
|
+
// A âncora: a do roteiro, a palavra-tese, ou a que a regra achou.
|
|
89
|
+
r.palavra = achar(p.ancora) || r.palavra || achar(p.destaque) || forte();
|
|
90
|
+
if (r.rota === 'tipografia') r.termo = null;
|
|
91
|
+
else if (p.busca) r.termo = String(p.busca);
|
|
92
|
+
return r;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Quando o clipe troca, no tempo da voz: 0,2 s depois da âncora. null sem tempo. */
|
|
96
|
+
function tempoDaAncora(palavra) {
|
|
97
|
+
return palavra && Number.isFinite(palavra.de) ? r2(palavra.de + ATRASO_ANCORA) : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
39
100
|
/** O roteiro de qualquer estilo → o roteiro do cinema. Devolve {roteiro, extras}. */
|
|
40
101
|
function traduzir(roteiro) {
|
|
41
102
|
const partes = (roteiro && roteiro.partes) || [];
|
|
@@ -71,12 +132,16 @@ function traduzir(roteiro) {
|
|
|
71
132
|
else if (tipo === 'cta' || tipo === 'logo' || tipo === 'fim') q = { tipo: 'final', linhas: [p.titulo || p.texto, p.link].filter(Boolean).map(limpo), ...base };
|
|
72
133
|
else q = { tipo: 'montagem', linhas: [p.titulo || p.texto || p.fala].filter(Boolean).map(limpo), ...base };
|
|
73
134
|
saida.push(q);
|
|
74
|
-
extras.push({ tipoOriginal: tipo, fonte: tipo === 'abertura' && p.sub ? p.sub : (p.fonte || (tipo === 'citacao' ? p.autor : '')), autor: p.autor || '', busca: p.busca || ''
|
|
135
|
+
extras.push({ tipoOriginal: tipo, fonte: tipo === 'abertura' && p.sub ? p.sub : (p.fonte || (tipo === 'citacao' ? p.autor : '')), autor: p.autor || '', busca: p.busca || '',
|
|
136
|
+
...(p.rota ? { rota: p.rota } : {}), ...(p.ancora ? { ancora: p.ancora } : {}) });
|
|
75
137
|
});
|
|
76
138
|
return { roteiro: { ...roteiro, partes: saida, estilo: 'hibrido' }, extras };
|
|
77
139
|
}
|
|
78
140
|
|
|
79
|
-
|
|
141
|
+
/* A parte com o que o roteiro original disse dela (o tipo, a busca, a rota). */
|
|
142
|
+
const comExtras = (p, ex) => ({ ...p, ...(ex ? { tipoOriginal: ex.tipoOriginal, busca: ex.busca || p.busca, rota: ex.rota || p.rota, ancora: ex.ancora || p.ancora } : {}) });
|
|
143
|
+
|
|
144
|
+
/** O que procurar de b-roll para cada parte: o termo da rota dela, a busca do roteiro, ou o padrão. */
|
|
80
145
|
function termosDeBusca(roteiro, extras = []) {
|
|
81
146
|
const global = [].concat(roteiro.busca || []).filter(Boolean);
|
|
82
147
|
// Escopo fechado: só os termos da pessoa, nunca o padrão genérico.
|
|
@@ -84,11 +149,20 @@ function termosDeBusca(roteiro, extras = []) {
|
|
|
84
149
|
const lista = global.length ? global : fechado ? [] : BUSCA_PADRAO;
|
|
85
150
|
const termos = new Set();
|
|
86
151
|
(roteiro.partes || []).forEach((p, i) => {
|
|
87
|
-
const
|
|
88
|
-
|
|
152
|
+
const q = comExtras(p, extras[i]);
|
|
153
|
+
const r = rotaDaParte(q, null, { marca: roteiro.marca && roteiro.marca.nome, i });
|
|
154
|
+
/* Escopo fechado: só a busca que o roteiro deu à parte. O termo da rota sai
|
|
155
|
+
do dicionário (textura, tela de app, e o literal: "cachorro" → "dog") e
|
|
156
|
+
furava o escopo da pessoa (revisão do #99). */
|
|
157
|
+
if (r.termo && (!fechado || q.busca)) termos.add(r.termo);
|
|
89
158
|
});
|
|
90
|
-
|
|
91
|
-
|
|
159
|
+
/* O corte em 8 guarda lugar para a busca global do roteiro (no fechado, é o
|
|
160
|
+
escopo da pessoa): as frases vinham antes e a empurravam para fora
|
|
161
|
+
(revisão do #99). O padrão genérico é só enchimento, esse pode cair. */
|
|
162
|
+
const fixos = global.slice(0, 8);
|
|
163
|
+
const frases = [...termos].filter((t) => !fixos.includes(t)).slice(0, 8 - fixos.length);
|
|
164
|
+
const resto = global.length ? [] : lista.filter((t) => !termos.has(t));
|
|
165
|
+
return [...frases, ...fixos, ...resto].slice(0, 8);
|
|
92
166
|
}
|
|
93
167
|
|
|
94
168
|
/* A câmera de cada cena do cinema: empurra e desliza, alternando o lado. A
|
|
@@ -109,7 +183,7 @@ function camera(d, i, deitado, forte) {
|
|
|
109
183
|
/* SEM CARA DE IA (05/10): o HUD (cantos, timecode, fps, rótulos nas bordas)
|
|
110
184
|
e as partículas eram exatamente a lista do "ruim" que o Joel juntou. Agora
|
|
111
185
|
só entram quando o roteiro pede: roteiro.hud === true, roteiro.particulas === true. */
|
|
112
|
-
function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura, hud = false, particulas = false } = {}) {
|
|
186
|
+
function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura, hud = false, particulas = false, tempos = null, fortes = {}, fotos = [] } = {}) {
|
|
113
187
|
if (!plano || !Array.isArray(plano.cenas)) return plano;
|
|
114
188
|
const W = largura || (plano.formato && plano.formato.largura) || 1920;
|
|
115
189
|
const H = altura || (plano.formato && plano.formato.altura) || 1080;
|
|
@@ -126,6 +200,7 @@ function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura
|
|
|
126
200
|
const n = p.tipo === 'depoimento' ? Math.max(1, (p.clipes || []).length) : 1;
|
|
127
201
|
for (let q = 0; q < n; q++) parteDaCena.push(i);
|
|
128
202
|
});
|
|
203
|
+
if (tempos) ancorar(plano, { extras, partes, marca, tempos, fortes, fotos, parteDaCena });
|
|
129
204
|
plano.cenas.forEach((cena, c) => {
|
|
130
205
|
const iParte = parteDaCena[c] != null ? parteDaCena[c] : c;
|
|
131
206
|
const ex = extras[iParte] || {};
|
|
@@ -166,4 +241,65 @@ function enfeitar(plano, { extras = [], partes = [], marca = {}, largura, altura
|
|
|
166
241
|
return plano;
|
|
167
242
|
}
|
|
168
243
|
|
|
169
|
-
|
|
244
|
+
/* O B-ROLL NA PALAVRA: a cena de montagem (ou de frase) troca para o clipe da
|
|
245
|
+
rota dela 0,2 s depois da âncora — antes, segue a montagem do cinema. Rota
|
|
246
|
+
tipografia tira as fotos: o número já é a imagem. O tempo da cena é o do
|
|
247
|
+
cinema: a voz entra 0,2 s depois do corte (off). */
|
|
248
|
+
function ancorar(plano, { extras, partes, marca, tempos, fortes, fotos, parteDaCena }) {
|
|
249
|
+
const cinema = require('./cinema.js');
|
|
250
|
+
const off = 0.2;
|
|
251
|
+
let narrI = 0;
|
|
252
|
+
const tempoDe = partes.map((p, i) => (p.tipo !== 'depoimento' && cinema.falaDe(p) !== '' && !(p.falaForte && fortes[i]) ? tempos[narrI++] || null : null));
|
|
253
|
+
const ehFoto = (e) => (e.tipo === 'imagem' || e.tipo === 'video' || (e.tipo === 'forma' && e.fundo && e.fundo.tipo === 'gradiente')) && e.largura === '100%';
|
|
254
|
+
const plan = [], vez = [];
|
|
255
|
+
plano.cenas.forEach((cena, c) => {
|
|
256
|
+
const i = parteDaCena[c] != null ? parteDaCena[c] : c;
|
|
257
|
+
const p = partes[i], tp = tempoDe[i];
|
|
258
|
+
if (!p || !['montagem', 'frase', 'numeros'].includes(p.tipo) || (parteDaCena.indexOf(i) !== c)) return;
|
|
259
|
+
const r = rotaDaParte(comExtras(p, extras[i]), tp && tp.palavras, { marca: marca.nome, i });
|
|
260
|
+
plan.push({ parte: i, rota: r.rota, ancora: r.palavra ? r.palavra.texto : null, termo: r.termo || null });
|
|
261
|
+
/* Produto: a tela DA MARCA (o print do acervo) antes do vídeo de banco. Antes
|
|
262
|
+
a regra perguntava se o alt não era termo de busca, mas o conjunto tinha o
|
|
263
|
+
alt de todas as fotos: nunca casava, e o painel ficava de fora (revisão do #99). */
|
|
264
|
+
const f = r.rota === 'tipografia' ? null : r.rota === 'produto' ? fotos.find((x) => x.cat === 'tela' && !x.banco) || fotos.find((x) => x.alt === r.termo)
|
|
265
|
+
: fotos.find((x) => x.alt === r.termo && x.tipo === 'video') || fotos.find((x) => x.alt === r.termo);
|
|
266
|
+
const ta = tp ? tempoDaAncora(r.palavra) : null;
|
|
267
|
+
const de = ta == null ? null : r2(off + ta - tp.de);
|
|
268
|
+
vez.push({ cena, r, f: f && de != null && (cena.duracao || 2) - de >= 0.8 ? f : null, de }); // sem 0,8 s para ver, fica a montagem
|
|
269
|
+
});
|
|
270
|
+
/* O clipe de uma frase não aparece ANTES da palavra dela (nem na montagem
|
|
271
|
+
de outra cena): quem vê o café antes de ouvir "café" sente o erro. */
|
|
272
|
+
const reservado = new Set(vez.filter((x) => x.f).map((x) => x.f.src));
|
|
273
|
+
const livres = fotos.filter((x) => !reservado.has(x.src));
|
|
274
|
+
let n = 0;
|
|
275
|
+
for (const { cena, r, f, de } of vez) {
|
|
276
|
+
let els = cena.elementos || [];
|
|
277
|
+
if (r.rota === 'tipografia') { cena.elementos = els.filter((e) => !ehFoto(e)); continue; }
|
|
278
|
+
for (const e of els) if ((e.tipo === 'imagem' || e.tipo === 'video') && reservado.has(e.src) && livres.length) {
|
|
279
|
+
const g = livres[n++ % livres.length];
|
|
280
|
+
Object.assign(e, { tipo: g.tipo === 'video' ? 'video' : 'imagem', src: g.src, ...(g.tipo === 'video' ? { volume: 0 } : {}) });
|
|
281
|
+
}
|
|
282
|
+
if (!f) continue;
|
|
283
|
+
const d = cena.duracao || 2;
|
|
284
|
+
els = els.filter((e) => !(ehFoto(e) && e.de >= de));
|
|
285
|
+
for (const e of els) if (ehFoto(e) && (e.ate == null || e.ate > de)) e.ate = de;
|
|
286
|
+
// Um resto de foto de um instante vira um piscar: a foto de antes cobre o buraco.
|
|
287
|
+
for (const e of els.filter((x) => ehFoto(x) && x.ate - (x.de || 0) < 0.25)) {
|
|
288
|
+
for (const x of els) if (x !== e && ehFoto(x) && x.ate === e.de && x.tipo === e.tipo) x.ate = e.ate;
|
|
289
|
+
els = els.filter((x) => x !== e);
|
|
290
|
+
}
|
|
291
|
+
const v = f.tipo === 'video';
|
|
292
|
+
const vida = r2(d - de);
|
|
293
|
+
const novo = [{ tipo: v ? 'video' : 'imagem', src: f.src, ...(v ? { volume: 0 } : {}), x: 0, y: 0, largura: '100%', altura: '100%', ajuste: 'cover', de, ate: r2(d), broll: r.rota,
|
|
294
|
+
anima: { escala: [{ t: 0, v: 1.12 }, { t: vida, v: 1.02, e: 'linear' }] } },
|
|
295
|
+
{ tipo: 'forma', forma: 'retangulo', x: 0, y: 0, largura: '100%', altura: '100%', de, ate: r2(d),
|
|
296
|
+
fundo: { tipo: 'gradiente', cores: ['rgba(7,7,10,0.35)', 'rgba(7,7,10,0.5)', 'rgba(7,7,10,0.75)'], angulo: 180 } }];
|
|
297
|
+
const k = els.findIndex((e) => e.tipo === 'texto' || e.tipo === 'kit');
|
|
298
|
+
els.splice(k < 0 ? els.length : k, 0, ...novo);
|
|
299
|
+
cena.elementos = els;
|
|
300
|
+
}
|
|
301
|
+
plano.broll = plan;
|
|
302
|
+
return plan;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = { traduzir, termosDeBusca, enfeitar, rotaDaParte, tempoDaAncora, ancorar, ROTAS, ATRASO_ANCORA, BUSCA_PADRAO };
|
package/lib/estudio/index.js
CHANGED
|
@@ -66,10 +66,12 @@ const kokoro = require('./kokoro.js');
|
|
|
66
66
|
const lancamento = require('./lancamento.js');
|
|
67
67
|
const glifos = require('./glifos.js');
|
|
68
68
|
const personagem = require('./personagem.js');
|
|
69
|
+
const mundo = require('./mundo.js');
|
|
69
70
|
const colecao = require('./colecao.js');
|
|
70
71
|
const { escolherEstilo, LEVES } = require('./estilo.js');
|
|
71
72
|
const sonsLeves = require('./sons-leves.js');
|
|
72
73
|
const ende = require('./ende.js');
|
|
74
|
+
const producao = require('./producao.js');
|
|
73
75
|
|
|
74
76
|
const PY = path.join(__dirname, '..', '..', 'studio');
|
|
75
77
|
|
|
@@ -163,7 +165,7 @@ function semProjeto(id) {
|
|
|
163
165
|
ok: false,
|
|
164
166
|
error: `não achei o projeto "${id}".`,
|
|
165
167
|
comoResolver: 'liste com estudio_projeto acao:"listar" e use um id de lá — '
|
|
166
|
-
+ 'o id é o nome da pasta (ex: "
|
|
168
|
+
+ 'o id é o nome da pasta (ex: "anuncio-picpay"), não o título.',
|
|
167
169
|
};
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -188,6 +190,9 @@ async function ferramentaProjeto(args, ctx = {}) {
|
|
|
188
190
|
// O caderno é o que faz "mexe naquela parte" funcionar semanas
|
|
189
191
|
// depois: sem ele, reabrir diz o que existe e nunca por quê.
|
|
190
192
|
caderno: projeto.caderno(args.id, { ultimas: 25 }),
|
|
193
|
+
// A conversa das sessões em que se mexeu neste projeto (AGENTS.md
|
|
194
|
+
// da pasta dele, lib/agents-md.js): o que foi pedido e decidido.
|
|
195
|
+
memoria: require('../agents-md.js').ler(p.pasta) || undefined,
|
|
191
196
|
plano: pl ? plano.resumo(pl) : 'ainda não há plano',
|
|
192
197
|
versoes: projeto.versoes(args.id),
|
|
193
198
|
arquivos: listarArquivos(p.pasta),
|
|
@@ -1097,13 +1102,42 @@ async function narrarComCache(lista, mp3, { voz, direcao } = {}) {
|
|
|
1097
1102
|
return v;
|
|
1098
1103
|
}
|
|
1099
1104
|
|
|
1105
|
+
/* A PRODUÇÃO (producao.js): cada necessidade — voz, trilha, efeitos — pela
|
|
1106
|
+
corrente de provedores dela, com o cache do projeto. `roteiro.provedores`
|
|
1107
|
+
({ voz: 'kokoro' }) passa um provedor para a frente, e o caderno guarda
|
|
1108
|
+
quem produziu cada coisa. */
|
|
1109
|
+
async function produzirNecessidade(p, roteiro, pedido, provedores) {
|
|
1110
|
+
const r = await producao.produzirUma(pedido, provedores, { escolha: ((roteiro && roteiro.provedores) || {})[pedido.tipo], cache: producao.cacheDe(p.pasta) });
|
|
1111
|
+
projeto.anotar(p.id, 'producao', producao.linhaCaderno(r), { necessidade: pedido.tipo, provedor: r.quem || null, cache: !!r.doCache });
|
|
1112
|
+
return r;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
/* A voz como provedores, na ordem do kokoro.ordem: o Gemini (com o cache e a
|
|
1116
|
+
direção dele), o Kokoro local e o Edge. A falha de uma passa para a próxima. */
|
|
1117
|
+
function provedoresDeVoz(lista, mp3, arq, voz, vozEdge, { soEdge = false, gemini, escolha } = {}) {
|
|
1118
|
+
const todos = {
|
|
1119
|
+
gemini: () => gemini(),
|
|
1120
|
+
kokoro: () => kokoro.narrar(lista, mp3, { voz, uv: acharUv() }),
|
|
1121
|
+
edge: () => {
|
|
1122
|
+
// Ritmo NORMAL: o "citação" (-10%, respiro longo) esticou o vídeo para 125 s.
|
|
1123
|
+
if (!fs.existsSync(arq)) fs.writeFileSync(arq, JSON.stringify(lista.map((x) => ({ ...x, registro: 'normal' }))));
|
|
1124
|
+
return narrarReserva(lista, mp3, arq, voz, vozEdge, { soEdge: true });
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
const comResultado = (f) => async () => { const v = await f(); return v && v.ok ? { ...v, arquivo: mp3 } : v; };
|
|
1128
|
+
const ordem = kokoro.ordem(voz, process.env, { soEdge });
|
|
1129
|
+
// A escolhida no roteiro entra mesmo fora da ordem (o produzirUma a põe na frente).
|
|
1130
|
+
if (escolha && todos[escolha] && !ordem.includes(escolha)) ordem.push(escolha);
|
|
1131
|
+
return ordem.map((nome) => ({ nome, gerar: comResultado(todos[nome]) }));
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1100
1134
|
/* A VOZ DE RESERVA, quando o Gemini falha: o Kokoro (local, sem cota) e
|
|
1101
1135
|
depois o Edge. A falha de uma passa para a próxima, nunca derruba. */
|
|
1102
1136
|
function narrarReserva(lista, mp3, arq, voz, vozEdge, { soEdge = false, semKokoro = false } = {}) {
|
|
1103
1137
|
const passos = kokoro.ordem(voz, process.env, { soEdge }).filter((n) => n !== 'gemini' && !(semKokoro && n === 'kokoro'));
|
|
1104
1138
|
return kokoro.corrente(passos, {
|
|
1105
1139
|
kokoro: () => kokoro.narrar(lista, mp3, { voz, uv: acharUv() }),
|
|
1106
|
-
edge: () => rodarPython('narrar.py', ['roteiro', '--arquivo', arq, '--saida', mp3, '--voz',
|
|
1140
|
+
edge: () => rodarPython('narrar.py', ['roteiro', '--arquivo', arq, '--saida', mp3, '--voz', kokoro.vozNoEdge(voz, vozEdge)], { timeout: 10 * 60000 }),
|
|
1107
1141
|
});
|
|
1108
1142
|
}
|
|
1109
1143
|
|
|
@@ -1211,6 +1245,11 @@ async function prepararRoteiro(p, args) {
|
|
|
1211
1245
|
if (!args.estilo && estilo !== 'lancamento') estilo = 'gerativo';
|
|
1212
1246
|
if (!roteiro.modelo3d) roteiro.modelo3d = variacao3d.modeloDoPedido(pedidoTxt);
|
|
1213
1247
|
}
|
|
1248
|
+
// O bicho do vídeo 3D não fala com voz de gente (a menos que o pedido peça): sai da narração e fica na cena.
|
|
1249
|
+
if (estilo === 'personagem') {
|
|
1250
|
+
const calados = personagem.calarBichos(roteiro, pedidoTxt);
|
|
1251
|
+
if (calados.length) projeto.anotar(p.id, 'plano', `sem fala (bicho): ${calados.map((c) => `${c.nome} (${c.especie})`).join(', ')} — fica na cena; para falar, peça "${calados[0].especie} falante"`);
|
|
1252
|
+
}
|
|
1214
1253
|
// O 3D de cada vídeo: acabamento, câmera, luz e ritmo sorteados sem repetir os últimos.
|
|
1215
1254
|
/* Por PROJETO, não por chamada: mandar o roteiro de novo para ajustar uma
|
|
1216
1255
|
frase não pode sortear outro 3D nem outro vídeo. Projeto novo, sorteio novo. */
|
|
@@ -1332,35 +1371,38 @@ async function prepararRoteiro(p, args) {
|
|
|
1332
1371
|
return narrarReserva(l, m, a, voz, 'antonio', { soEdge });
|
|
1333
1372
|
} });
|
|
1334
1373
|
if (!v.ok) projeto.anotar(p.id, 'narracao', `as duas vozes falharam (${v.error}); saiu numa voz só`);
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
v = { ok: true, ...JSON.parse(fs.readFileSync(cacheJson, 'utf8')), doCache: true };
|
|
1340
|
-
// Cache sem as palavras (vindo de um plano antigo): transcreve de novo e realinha — sem gastar voz.
|
|
1341
|
-
if (!(v.partes || []).some((pt) => (pt.palavras || []).length)) {
|
|
1342
|
-
try {
|
|
1343
|
-
const cfg = require('../config').loadConfig();
|
|
1344
|
-
const tr = await require('../api').requestTranscricao(cfg.server, { token: cfg.token, audio: fs.readFileSync(mp3).toString('base64'), nome: 'voz.mp3', idioma: 'pt' });
|
|
1345
|
-
if (tr && (tr.palavras || []).length) {
|
|
1346
|
-
v.partes = alinharPartes(lista, tr.palavras, v.segundos);
|
|
1347
|
-
fs.writeFileSync(cacheJson, JSON.stringify({ voz: v.voz, segundos: v.segundos, partes: v.partes }));
|
|
1348
|
-
}
|
|
1349
|
-
} catch { /* fica o alinhamento guardado */ }
|
|
1374
|
+
if (!v.ok) {
|
|
1375
|
+
const arq = path.join(p.pasta, 'midia', `roteiro-${Date.now().toString(36)}.json`);
|
|
1376
|
+
fs.writeFileSync(arq, JSON.stringify(lista.map((x) => ({ ...x, registro: 'normal' }))));
|
|
1377
|
+
v = await narrarReserva(lista, mp3, arq, args.voz || roteiro.voz, 'antonio', { soEdge });
|
|
1350
1378
|
}
|
|
1351
|
-
} else if (kokoro.querKokoro(args.voz || roteiro.voz)) {
|
|
1352
|
-
v = { ok: false, error: 'kokoro pedido' };
|
|
1353
1379
|
} else {
|
|
1354
|
-
v = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz, direcao: DIRECAO_CINEMA });
|
|
1355
|
-
if (v.ok) {
|
|
1356
|
-
try { fs.mkdirSync(cacheDir, { recursive: true }); fs.copyFileSync(mp3, cacheMp3); fs.writeFileSync(cacheJson, JSON.stringify({ voz: v.voz, segundos: v.segundos, partes: v.partes })); } catch { /* sem cache */ }
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
if (!v.ok) {
|
|
1360
1380
|
const arq = path.join(p.pasta, 'midia', `roteiro-${Date.now().toString(36)}.json`);
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1381
|
+
const gemini = async () => {
|
|
1382
|
+
if (fs.existsSync(cacheMp3) && fs.existsSync(cacheJson)) {
|
|
1383
|
+
fs.copyFileSync(cacheMp3, mp3);
|
|
1384
|
+
const g = { ok: true, ...JSON.parse(fs.readFileSync(cacheJson, 'utf8')), doCache: true };
|
|
1385
|
+
// Cache sem as palavras (vindo de um plano antigo): transcreve de novo e realinha — sem gastar voz.
|
|
1386
|
+
if (!(g.partes || []).some((pt) => (pt.palavras || []).length)) {
|
|
1387
|
+
try {
|
|
1388
|
+
const cfg = require('../config').loadConfig();
|
|
1389
|
+
const tr = await require('../api').requestTranscricao(cfg.server, { token: cfg.token, audio: fs.readFileSync(mp3).toString('base64'), nome: 'voz.mp3', idioma: 'pt' });
|
|
1390
|
+
if (tr && (tr.palavras || []).length) {
|
|
1391
|
+
g.partes = alinharPartes(lista, tr.palavras, g.segundos);
|
|
1392
|
+
fs.writeFileSync(cacheJson, JSON.stringify({ voz: g.voz, segundos: g.segundos, partes: g.partes }));
|
|
1393
|
+
}
|
|
1394
|
+
} catch { /* fica o alinhamento guardado */ }
|
|
1395
|
+
}
|
|
1396
|
+
return g;
|
|
1397
|
+
}
|
|
1398
|
+
const g = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz, direcao: DIRECAO_CINEMA });
|
|
1399
|
+
if (g.ok) {
|
|
1400
|
+
try { fs.mkdirSync(cacheDir, { recursive: true }); fs.copyFileSync(mp3, cacheMp3); fs.writeFileSync(cacheJson, JSON.stringify({ voz: g.voz, segundos: g.segundos, partes: g.partes })); } catch { /* sem cache */ }
|
|
1401
|
+
}
|
|
1402
|
+
return g;
|
|
1403
|
+
};
|
|
1404
|
+
v = await produzirNecessidade(p, roteiro, { tipo: 'voz', chave: [lista.map((x) => x.texto), args.voz || roteiro.voz || '', 'cinema-v1', soEdge], destino: mp3 },
|
|
1405
|
+
provedoresDeVoz(lista, mp3, arq, args.voz || roteiro.voz, 'antonio', { soEdge, gemini, escolha: (roteiro.provedores || {}).voz }));
|
|
1364
1406
|
}
|
|
1365
1407
|
if (!v.ok) return v;
|
|
1366
1408
|
/* A DURAÇÃO que o roteiro pede ("60–75 s"): se a voz passa do que cabe,
|
|
@@ -1404,25 +1446,32 @@ async function prepararRoteiro(p, args) {
|
|
|
1404
1446
|
}
|
|
1405
1447
|
}
|
|
1406
1448
|
projeto.anotar(p.id, 'narracao', `${v.segundos}s em ${lista.length} partes, voz ${v.voz}`);
|
|
1407
|
-
|
|
1408
|
-
|
|
1449
|
+
/* A chave leva o id do projeto e o hash das falas. Só `cinema` fazia um
|
|
1450
|
+
roteiro novo, no mesmo projeto, reaproveitar os efeitos do anterior. */
|
|
1451
|
+
const marcaSfx = require('crypto').createHash('sha1').update(JSON.stringify({ id: p.id || '', falas: partes.map((pt) => cinema.falaDe(pt)) })).digest('hex').slice(0, 16);
|
|
1452
|
+
const sonsC = (await produzirNecessidade(p, roteiro, { tipo: 'sfx', chave: [p.id || '', marcaSfx] },
|
|
1453
|
+
[{ nome: 'openverse', gerar: async () => ({ sons: await sonsReais(p, video) }) }])).sons || {};
|
|
1409
1454
|
// Vários termos e durações: vídeo sem música foi o que saiu na primeira vez (25/09).
|
|
1410
1455
|
// E só instrumental: "essa música daria pra deixar só o instrumental?" — a faixa é OUVIDA.
|
|
1411
1456
|
// O termo sai do clima do roteiro (variedade.js), não de uma lista fixa de "cinematic inspiring".
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1457
|
+
let trilhaC = (await produzirNecessidade(p, roteiro, { tipo: 'trilha', chave: ['cinema', roteiro.clima || '', roteiro.genero || ''] },
|
|
1458
|
+
[{ nome: 'jamendo', gerar: async () => {
|
|
1459
|
+
for (const termo of [...variedade.termosTrilha(roteiro, 'cinema'), 'instrumental']) {
|
|
1460
|
+
try {
|
|
1461
|
+
const b = await assets.procurarMusica(termo, { quantas: 10, duracaoMin: 45 });
|
|
1462
|
+
for (const item of variedade.ordenar(b.ok ? b.sons : [], 'trilha').filter((x) => !/vocal|voice|feat|ft\.|remix/i.test(x.titulo || '')).slice(0, 4)) {
|
|
1463
|
+
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha' });
|
|
1464
|
+
if (!d.ok) continue;
|
|
1465
|
+
if (await temLetra(path.join(p.pasta, d.relativo))) { projeto.anotar(p.id, 'midia', `trilha com voz descartada: ${item.titulo}`); continue; }
|
|
1466
|
+
variedade.marcar('trilha', item.url);
|
|
1467
|
+
const credito = `"${termo}": ${item.titulo} — ${item.autor || 'Jamendo'} (${item.licenca || 'CC'})`;
|
|
1468
|
+
projeto.anotar(p.id, 'trilha', credito);
|
|
1469
|
+
return { src: d.relativo, credito };
|
|
1470
|
+
}
|
|
1471
|
+
} catch { /* o próximo termo */ }
|
|
1423
1472
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1473
|
+
return null;
|
|
1474
|
+
} }])).src || null;
|
|
1426
1475
|
/* A MÚSICA mais baixa na origem: a faixa vem masterizada alta e comprimida,
|
|
1427
1476
|
e mesmo a 30% ela cobria a voz e o depoimento ("a música tá muito
|
|
1428
1477
|
alta, não dá pra ouvir as pessoas", 25/09). Ela vai a -28 LUFS antes
|
|
@@ -1469,11 +1518,12 @@ async function prepararRoteiro(p, args) {
|
|
|
1469
1518
|
try { fs.unlinkSync(path.join(p.pasta, rel)); } catch { /* fica o original */ }
|
|
1470
1519
|
return 'midia/' + destino;
|
|
1471
1520
|
};
|
|
1521
|
+
// banco: true marca o que veio do banco (Pexels): a rota produto prefere a tela da marca (hibrido.ancorar).
|
|
1472
1522
|
const achados = await Promise.all(hibrido.termosDeBusca(roteiro, hib.extras).map(async (termo) => {
|
|
1473
1523
|
const out = [];
|
|
1474
1524
|
const slug = 'broll-' + String(termo).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
1475
1525
|
const proxy = `${slug}.leve.mp4`;
|
|
1476
|
-
if (jaTem.includes(proxy)) out.push({ src: 'midia/' + proxy, tipo: 'video', cat: cat(termo), alt: termo });
|
|
1526
|
+
if (jaTem.includes(proxy)) out.push({ src: 'midia/' + proxy, tipo: 'video', cat: cat(termo), alt: termo, banco: true });
|
|
1477
1527
|
else try {
|
|
1478
1528
|
// Sorteado entre os não usados, com o termo variado (variedade.js, 05/10).
|
|
1479
1529
|
const itens = await variedade.buscarSemRepetir(async (t) => {
|
|
@@ -1483,11 +1533,11 @@ async function prepararRoteiro(p, args) {
|
|
|
1483
1533
|
for (const item of itens.slice(0, 1)) {
|
|
1484
1534
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: slug });
|
|
1485
1535
|
const l = d.ok ? await leve(d.relativo, proxy) : null;
|
|
1486
|
-
if (l) { out.push({ src: l, tipo: 'video', cat: cat(termo), alt: termo }); variedade.marcar('broll', item.url); }
|
|
1536
|
+
if (l) { out.push({ src: l, tipo: 'video', cat: cat(termo), alt: termo, banco: true }); variedade.marcar('broll', item.url); }
|
|
1487
1537
|
}
|
|
1488
1538
|
} catch { /* segue com a foto */ }
|
|
1489
1539
|
const foto = jaTem.find((f) => f.startsWith(slug + '-') && /\.(jpe?g|png|webp)$/i.test(f));
|
|
1490
|
-
if (foto) out.push({ src: 'midia/' + foto, tipo: 'imagem', cat: cat(termo), alt: termo });
|
|
1540
|
+
if (foto) out.push({ src: 'midia/' + foto, tipo: 'imagem', cat: cat(termo), alt: termo, banco: true });
|
|
1491
1541
|
else try {
|
|
1492
1542
|
const itens = await variedade.buscarSemRepetir(async (t) => {
|
|
1493
1543
|
const b = await assets.procurarImagens(t, { quantas: 8, orientacao });
|
|
@@ -1495,7 +1545,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1495
1545
|
}, termo);
|
|
1496
1546
|
for (const item of itens.slice(0, 1)) {
|
|
1497
1547
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: slug });
|
|
1498
|
-
if (d.ok) { out.push({ src: d.relativo, tipo: 'imagem', cat: cat(termo), alt: termo }); variedade.marcar('broll', item.url); }
|
|
1548
|
+
if (d.ok) { out.push({ src: d.relativo, tipo: 'imagem', cat: cat(termo), alt: termo, banco: true }); variedade.marcar('broll', item.url); }
|
|
1499
1549
|
}
|
|
1500
1550
|
} catch { /* sem */ }
|
|
1501
1551
|
return out;
|
|
@@ -1554,7 +1604,9 @@ async function prepararRoteiro(p, args) {
|
|
|
1554
1604
|
const r = cinema.montarCinema({ roteiro, largura, altura, tempos: v.partes, narracao: path.relative(p.pasta, mp3).replace(/\\/g, '/'),
|
|
1555
1605
|
sons: sonsC, trilha: trilhaC, fotos: fotosC, fortes, ritmoLeitura: hib ? 0.55 : 1 });
|
|
1556
1606
|
if (r.ok && hib) {
|
|
1557
|
-
hibrido.enfeitar(r.plano, { extras: hib.extras, partes, marca: roteiro.marca || {}, largura, altura, hud: roteiro.hud === true, particulas: roteiro.particulas === true
|
|
1607
|
+
hibrido.enfeitar(r.plano, { extras: hib.extras, partes, marca: roteiro.marca || {}, largura, altura, hud: roteiro.hud === true, particulas: roteiro.particulas === true,
|
|
1608
|
+
// O b-roll de cada frase troca 0,2 s depois da palavra-âncora (hibrido.ancorar).
|
|
1609
|
+
tempos: v.partes, fortes, fotos: fotosC });
|
|
1558
1610
|
r.resumo.estilo = 'hibrido';
|
|
1559
1611
|
}
|
|
1560
1612
|
if (r.ok) projeto.anotar(p.id, 'plano', `estilo ${hib ? 'hibrido' : 'cinema'}: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s`);
|
|
@@ -1579,15 +1631,41 @@ async function prepararRoteiro(p, args) {
|
|
|
1579
1631
|
/* Uma voz por falante (falantes.js). Com uma voz só, um grupo só: é a
|
|
1580
1632
|
narração de sempre, com o mesmo cache. */
|
|
1581
1633
|
const vozesL = falantes.vozesDasPartes(partes, roteiro, args.voz || roteiro.voz);
|
|
1582
|
-
let v = await
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1634
|
+
let v = await produzirNecessidade(p, roteiro, { tipo: 'voz', chave: [lista.map((x) => x.texto), vozesL, direcao], destino: mp3 }, [{
|
|
1635
|
+
nome: ((roteiro.provedores || {}).voz) || 'estudio',
|
|
1636
|
+
gerar: async () => {
|
|
1637
|
+
let reserva = false, gemini = false;
|
|
1638
|
+
const comLista = lista.map((x, i) => ({ ...x, voz: vozesL[i] }));
|
|
1639
|
+
const pelaReserva = async (l, m, voz) => {
|
|
1640
|
+
// O Kokoro pedido já foi tentado no narrarComCache: a reserva não repete.
|
|
1641
|
+
const a = m === mp3 ? arq : m.replace(/\.mp3$/, '.json');
|
|
1642
|
+
if (a !== arq) fs.writeFileSync(a, JSON.stringify(l));
|
|
1643
|
+
const rr = await narrarReserva(l, m, a, voz, 'thalita', { semKokoro: kokoro.querKokoro(voz) });
|
|
1644
|
+
if (rr && rr.ok) reserva = true;
|
|
1645
|
+
return rr;
|
|
1646
|
+
};
|
|
1647
|
+
let r = await falantes.narrarPorFalante(comLista, mp3, { juntar: juntarVozes,
|
|
1648
|
+
narrarUm: async (l, m, voz) => {
|
|
1649
|
+
const g = await narrarComCache(l, m, { voz, direcao });
|
|
1650
|
+
if (g.ok) { gemini = true; return g; }
|
|
1651
|
+
return pelaReserva(l, m, voz);
|
|
1652
|
+
} });
|
|
1653
|
+
/* UMA VOZ SÓ POR VÍDEO (11/10, "os personagens falam com vozes
|
|
1654
|
+
diferentes em cada cena"): a cota do Gemini acabava no meio, e o
|
|
1655
|
+
mesmo personagem saía Gemini numa fala e reserva na outra. Se
|
|
1656
|
+
misturou, o vídeo inteiro é refeito na reserva — pior que o
|
|
1657
|
+
Gemini, mas cada um com a mesma voz do começo ao fim. */
|
|
1658
|
+
if (r && r.ok && reserva && gemini) {
|
|
1659
|
+
gemini = false;
|
|
1660
|
+
r = await falantes.narrarPorFalante(comLista, mp3, { juntar: juntarVozes, narrarUm: pelaReserva });
|
|
1661
|
+
}
|
|
1662
|
+
/* A voz de RESERVA (Kokoro/Edge, quando o Gemini falhou) não vai para o
|
|
1663
|
+
cache do projeto: guardada, ela voltava em todo refazer do mesmo
|
|
1664
|
+
texto, mesmo com o Gemini de pé — era a "voz robótica que não sai"
|
|
1665
|
+
(11/10). Na próxima vez o Gemini é tentado de novo. */
|
|
1666
|
+
return r && r.ok ? { ...r, arquivo: mp3, ...(reserva ? { naoGuardar: true } : {}) } : r;
|
|
1667
|
+
},
|
|
1668
|
+
}]);
|
|
1591
1669
|
if (!v.ok) return v;
|
|
1592
1670
|
/* O RITMO DA VOZ. Mesmo com a direção, a voz sai no passo de leitura, e
|
|
1593
1671
|
o vídeo — que segue a voz palavra por palavra — sai lento junto. O
|
|
@@ -1793,8 +1871,22 @@ async function prepararRoteiro(p, args) {
|
|
|
1793
1871
|
return r;
|
|
1794
1872
|
}
|
|
1795
1873
|
if (estilo === 'personagem' && modo === 'video') {
|
|
1796
|
-
const r = personagem.montarPersonagem({ roteiro, largura, altura, tempos, narracao, sons, trilha });
|
|
1797
|
-
if (r.ok)
|
|
1874
|
+
const r = personagem.montarPersonagem({ roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt }, largura, altura, tempos, narracao, sons, trilha });
|
|
1875
|
+
if (r.ok) {
|
|
1876
|
+
/* O que não é peça do catálogo vira ícone 3D da biblioteca, em blocos
|
|
1877
|
+
(kit/mundo.jsx): "uma pizza que fala", "um violão na praia". Sem
|
|
1878
|
+
ícone, a peça sai da cena (e a coisa que fala vira o mascote). */
|
|
1879
|
+
const achados = {};
|
|
1880
|
+
for (const nome of mundo.iconesPendentes(r.partitura)) {
|
|
1881
|
+
const id = colecao.melhorIcone3d(nome, 40);
|
|
1882
|
+
if (!id) continue;
|
|
1883
|
+
const b = await colecao.baixar(id, { pasta: p.pasta, largura, altura }).catch(() => null);
|
|
1884
|
+
if (b && b.ok) achados[nome] = b.src;
|
|
1885
|
+
}
|
|
1886
|
+
mundo.aplicarIcones(r.partitura, achados);
|
|
1887
|
+
const icones = Object.keys(achados);
|
|
1888
|
+
projeto.anotar(p.id, 'plano', `estilo personagem: ${r.resumo.elenco.join(' e ')}${r.resumo.figurantes.length ? ` (+ ${r.resumo.figurantes.join(', ')})` : ''} em ${r.resumo.lugares.join(' → ')}, ${r.resumo.planos} planos, ${r.resumo.segundos}s${icones.length ? `; em blocos: ${icones.join(', ')}` : ''}`);
|
|
1889
|
+
}
|
|
1798
1890
|
return r;
|
|
1799
1891
|
}
|
|
1800
1892
|
if (estilo === 'motion' && modo === 'video') {
|