primocode 10.5.0 → 10.7.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 +29 -6
- package/lib/comandos-extra.js +2 -0
- package/lib/config.js +32 -4
- package/lib/entrada.js +33 -11
- package/lib/estudio/cenarios.js +91 -0
- package/lib/estudio/colecao.js +35 -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 +196 -63
- 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/movimentos.js +212 -0
- package/lib/estudio/mundo.js +328 -0
- package/lib/estudio/personagem.js +164 -11
- package/lib/estudio/plano.js +6 -0
- 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/index.js
CHANGED
|
@@ -66,10 +66,14 @@ 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');
|
|
70
|
+
const cenarios = require('./cenarios.js');
|
|
71
|
+
const movimentos = require('./movimentos.js');
|
|
69
72
|
const colecao = require('./colecao.js');
|
|
70
73
|
const { escolherEstilo, LEVES } = require('./estilo.js');
|
|
71
74
|
const sonsLeves = require('./sons-leves.js');
|
|
72
75
|
const ende = require('./ende.js');
|
|
76
|
+
const producao = require('./producao.js');
|
|
73
77
|
|
|
74
78
|
const PY = path.join(__dirname, '..', '..', 'studio');
|
|
75
79
|
|
|
@@ -163,7 +167,7 @@ function semProjeto(id) {
|
|
|
163
167
|
ok: false,
|
|
164
168
|
error: `não achei o projeto "${id}".`,
|
|
165
169
|
comoResolver: 'liste com estudio_projeto acao:"listar" e use um id de lá — '
|
|
166
|
-
+ 'o id é o nome da pasta (ex: "
|
|
170
|
+
+ 'o id é o nome da pasta (ex: "anuncio-picpay"), não o título.',
|
|
167
171
|
};
|
|
168
172
|
}
|
|
169
173
|
|
|
@@ -188,6 +192,9 @@ async function ferramentaProjeto(args, ctx = {}) {
|
|
|
188
192
|
// O caderno é o que faz "mexe naquela parte" funcionar semanas
|
|
189
193
|
// depois: sem ele, reabrir diz o que existe e nunca por quê.
|
|
190
194
|
caderno: projeto.caderno(args.id, { ultimas: 25 }),
|
|
195
|
+
// A conversa das sessões em que se mexeu neste projeto (AGENTS.md
|
|
196
|
+
// da pasta dele, lib/agents-md.js): o que foi pedido e decidido.
|
|
197
|
+
memoria: require('../agents-md.js').ler(p.pasta) || undefined,
|
|
191
198
|
plano: pl ? plano.resumo(pl) : 'ainda não há plano',
|
|
192
199
|
versoes: projeto.versoes(args.id),
|
|
193
200
|
arquivos: listarArquivos(p.pasta),
|
|
@@ -272,6 +279,28 @@ function projetoRecente() {
|
|
|
272
279
|
return lista[0] || null;
|
|
273
280
|
}
|
|
274
281
|
|
|
282
|
+
/* Sem id, o projeto é o que acabou de ser criado. Visto em 12/10: o agente
|
|
283
|
+
criou o projeto, chamou estudio_narrar sem id e o vídeo morreu ali. Só vale
|
|
284
|
+
para chamada SEM id (id errado continua sendo erro) e para projeto de até
|
|
285
|
+
30 minutos, como no estudio_plano. */
|
|
286
|
+
function projetoOuRecente(id) {
|
|
287
|
+
if (id != null && String(id).trim() !== '') return projeto.ler(id);
|
|
288
|
+
return projetoRecente();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* roteiro.objetos: [{nome, src}] com o PNG DENTRO da pasta do projeto (o que
|
|
292
|
+
o estudio_midia baixou ou recortou). Caminho de fora some: o render só
|
|
293
|
+
enxerga a pasta do projeto, e a IA não escolhe arquivo da máquina. */
|
|
294
|
+
function objetosDoRoteiro(objetos, pasta) {
|
|
295
|
+
const raiz = path.resolve(pasta);
|
|
296
|
+
return (Array.isArray(objetos) ? objetos : []).map((o) => {
|
|
297
|
+
if (!o || typeof o.src !== 'string') return null;
|
|
298
|
+
const abs = path.resolve(raiz, o.src);
|
|
299
|
+
if (!abs.startsWith(raiz + path.sep) || !/\.png$/i.test(abs) || !fs.existsSync(abs)) return null;
|
|
300
|
+
return { nome: o.nome, src: path.relative(raiz, abs).replace(/\\/g, '/') };
|
|
301
|
+
}).filter(Boolean);
|
|
302
|
+
}
|
|
303
|
+
|
|
275
304
|
/** O roteiro é utilizável como veio? (objeto ou JSON, com partes de tipo conhecido) */
|
|
276
305
|
function roteiroValido(r) {
|
|
277
306
|
let x = r;
|
|
@@ -522,6 +551,19 @@ async function ferramentaMidia(args) {
|
|
|
522
551
|
/* A BIBLIOTECA (colecao.js, 11/10): o cardápio inteiro, o ícone 3D e a
|
|
523
552
|
animação pronta. Procurar não usa rede (o índice mora no pacote); com
|
|
524
553
|
id, baixa para o projeto e devolve o elemento pronto. */
|
|
554
|
+
/* OS MOVIMENTOS (movimentos.js, 12/10): 100+ entradas, saídas, ênfases e loops com nome. */
|
|
555
|
+
if (acao === 'movimentos' || acao === 'movimento') {
|
|
556
|
+
return movimentos.procurar({ termo: args.termo, categoria: args.categoria, limite: Number(args.limite) || 30 });
|
|
557
|
+
}
|
|
558
|
+
/* OS CENÁRIOS (cenarios.js, 12/10): os lugares prontos + os que a IA criou
|
|
559
|
+
e a pessoa deixou guardar. Salvar só com o sim dela (autorizado:true). */
|
|
560
|
+
if (acao === 'cenarios' || acao === 'cenários') {
|
|
561
|
+
return { ok: true, prontos: Object.keys(mundo.LUGARES), salvos: cenarios.listar(),
|
|
562
|
+
comoCriar: 'na cena do estilo personagem: lugar (um pronto como base), ceu [cor de cima, cor do horizonte], chao {tipo, cor, cor2}, parede, luz, acento, hora e elementos. Ficou bom? Pergunte se pode salvar e use acao:"salvar_cenario".' };
|
|
563
|
+
}
|
|
564
|
+
if (acao === 'salvar_cenario' || acao === 'salvar_cenário') {
|
|
565
|
+
return cenarios.salvar({ nome: args.nome, cenario: args.cenario, autorizado: args.autorizado, bases: Object.keys(mundo.LUGARES) });
|
|
566
|
+
}
|
|
525
567
|
if (acao === 'biblioteca' || acao === 'cardapio' || acao === 'cardápio') return colecao.cardapio();
|
|
526
568
|
if (acao === 'icones3d' || acao === 'icone3d' || acao === 'animacoes' || acao === 'animacao' || acao === 'animação' || acao === 'animações') {
|
|
527
569
|
const ehAnim = /^anima/.test(acao);
|
|
@@ -580,8 +622,9 @@ async function baixarEscolhido(lista, args, p) {
|
|
|
580
622
|
async function ferramentaNarrar(args) {
|
|
581
623
|
if (args.vozes) return rodarPython('narrar.py', ['vozes'], { timeout: 30000 });
|
|
582
624
|
|
|
583
|
-
const p =
|
|
625
|
+
const p = projetoOuRecente(args.id);
|
|
584
626
|
if (!p) return semProjeto(args.id);
|
|
627
|
+
if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
|
|
585
628
|
const texto = String(args.texto || '').trim();
|
|
586
629
|
if (!texto) return { ok: false, error: 'não há texto para narrar.' };
|
|
587
630
|
|
|
@@ -611,8 +654,9 @@ async function ferramentaNarrar(args) {
|
|
|
611
654
|
|
|
612
655
|
async function ferramentaVideo(args, ctx = {}) {
|
|
613
656
|
const acao = String(args.acao || 'medir').toLowerCase();
|
|
614
|
-
const p =
|
|
657
|
+
const p = projetoOuRecente(args.id);
|
|
615
658
|
if (!p) return semProjeto(args.id);
|
|
659
|
+
if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
|
|
616
660
|
|
|
617
661
|
const pronto = video.conferir();
|
|
618
662
|
if (!pronto.ok) return pronto;
|
|
@@ -1097,13 +1141,42 @@ async function narrarComCache(lista, mp3, { voz, direcao } = {}) {
|
|
|
1097
1141
|
return v;
|
|
1098
1142
|
}
|
|
1099
1143
|
|
|
1144
|
+
/* A PRODUÇÃO (producao.js): cada necessidade — voz, trilha, efeitos — pela
|
|
1145
|
+
corrente de provedores dela, com o cache do projeto. `roteiro.provedores`
|
|
1146
|
+
({ voz: 'kokoro' }) passa um provedor para a frente, e o caderno guarda
|
|
1147
|
+
quem produziu cada coisa. */
|
|
1148
|
+
async function produzirNecessidade(p, roteiro, pedido, provedores) {
|
|
1149
|
+
const r = await producao.produzirUma(pedido, provedores, { escolha: ((roteiro && roteiro.provedores) || {})[pedido.tipo], cache: producao.cacheDe(p.pasta) });
|
|
1150
|
+
projeto.anotar(p.id, 'producao', producao.linhaCaderno(r), { necessidade: pedido.tipo, provedor: r.quem || null, cache: !!r.doCache });
|
|
1151
|
+
return r;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/* A voz como provedores, na ordem do kokoro.ordem: o Gemini (com o cache e a
|
|
1155
|
+
direção dele), o Kokoro local e o Edge. A falha de uma passa para a próxima. */
|
|
1156
|
+
function provedoresDeVoz(lista, mp3, arq, voz, vozEdge, { soEdge = false, gemini, escolha } = {}) {
|
|
1157
|
+
const todos = {
|
|
1158
|
+
gemini: () => gemini(),
|
|
1159
|
+
kokoro: () => kokoro.narrar(lista, mp3, { voz, uv: acharUv() }),
|
|
1160
|
+
edge: () => {
|
|
1161
|
+
// Ritmo NORMAL: o "citação" (-10%, respiro longo) esticou o vídeo para 125 s.
|
|
1162
|
+
if (!fs.existsSync(arq)) fs.writeFileSync(arq, JSON.stringify(lista.map((x) => ({ ...x, registro: 'normal' }))));
|
|
1163
|
+
return narrarReserva(lista, mp3, arq, voz, vozEdge, { soEdge: true });
|
|
1164
|
+
},
|
|
1165
|
+
};
|
|
1166
|
+
const comResultado = (f) => async () => { const v = await f(); return v && v.ok ? { ...v, arquivo: mp3 } : v; };
|
|
1167
|
+
const ordem = kokoro.ordem(voz, process.env, { soEdge });
|
|
1168
|
+
// A escolhida no roteiro entra mesmo fora da ordem (o produzirUma a põe na frente).
|
|
1169
|
+
if (escolha && todos[escolha] && !ordem.includes(escolha)) ordem.push(escolha);
|
|
1170
|
+
return ordem.map((nome) => ({ nome, gerar: comResultado(todos[nome]) }));
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1100
1173
|
/* A VOZ DE RESERVA, quando o Gemini falha: o Kokoro (local, sem cota) e
|
|
1101
1174
|
depois o Edge. A falha de uma passa para a próxima, nunca derruba. */
|
|
1102
1175
|
function narrarReserva(lista, mp3, arq, voz, vozEdge, { soEdge = false, semKokoro = false } = {}) {
|
|
1103
1176
|
const passos = kokoro.ordem(voz, process.env, { soEdge }).filter((n) => n !== 'gemini' && !(semKokoro && n === 'kokoro'));
|
|
1104
1177
|
return kokoro.corrente(passos, {
|
|
1105
1178
|
kokoro: () => kokoro.narrar(lista, mp3, { voz, uv: acharUv() }),
|
|
1106
|
-
edge: () => rodarPython('narrar.py', ['roteiro', '--arquivo', arq, '--saida', mp3, '--voz',
|
|
1179
|
+
edge: () => rodarPython('narrar.py', ['roteiro', '--arquivo', arq, '--saida', mp3, '--voz', kokoro.vozNoEdge(voz, vozEdge)], { timeout: 10 * 60000 }),
|
|
1107
1180
|
});
|
|
1108
1181
|
}
|
|
1109
1182
|
|
|
@@ -1211,6 +1284,11 @@ async function prepararRoteiro(p, args) {
|
|
|
1211
1284
|
if (!args.estilo && estilo !== 'lancamento') estilo = 'gerativo';
|
|
1212
1285
|
if (!roteiro.modelo3d) roteiro.modelo3d = variacao3d.modeloDoPedido(pedidoTxt);
|
|
1213
1286
|
}
|
|
1287
|
+
// 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.
|
|
1288
|
+
if (estilo === 'personagem') {
|
|
1289
|
+
const calados = personagem.calarBichos(roteiro, pedidoTxt);
|
|
1290
|
+
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"`);
|
|
1291
|
+
}
|
|
1214
1292
|
// O 3D de cada vídeo: acabamento, câmera, luz e ritmo sorteados sem repetir os últimos.
|
|
1215
1293
|
/* Por PROJETO, não por chamada: mandar o roteiro de novo para ajustar uma
|
|
1216
1294
|
frase não pode sortear outro 3D nem outro vídeo. Projeto novo, sorteio novo. */
|
|
@@ -1332,35 +1410,38 @@ async function prepararRoteiro(p, args) {
|
|
|
1332
1410
|
return narrarReserva(l, m, a, voz, 'antonio', { soEdge });
|
|
1333
1411
|
} });
|
|
1334
1412
|
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 */ }
|
|
1413
|
+
if (!v.ok) {
|
|
1414
|
+
const arq = path.join(p.pasta, 'midia', `roteiro-${Date.now().toString(36)}.json`);
|
|
1415
|
+
fs.writeFileSync(arq, JSON.stringify(lista.map((x) => ({ ...x, registro: 'normal' }))));
|
|
1416
|
+
v = await narrarReserva(lista, mp3, arq, args.voz || roteiro.voz, 'antonio', { soEdge });
|
|
1350
1417
|
}
|
|
1351
|
-
} else if (kokoro.querKokoro(args.voz || roteiro.voz)) {
|
|
1352
|
-
v = { ok: false, error: 'kokoro pedido' };
|
|
1353
1418
|
} 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
1419
|
const arq = path.join(p.pasta, 'midia', `roteiro-${Date.now().toString(36)}.json`);
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1420
|
+
const gemini = async () => {
|
|
1421
|
+
if (fs.existsSync(cacheMp3) && fs.existsSync(cacheJson)) {
|
|
1422
|
+
fs.copyFileSync(cacheMp3, mp3);
|
|
1423
|
+
const g = { ok: true, ...JSON.parse(fs.readFileSync(cacheJson, 'utf8')), doCache: true };
|
|
1424
|
+
// Cache sem as palavras (vindo de um plano antigo): transcreve de novo e realinha — sem gastar voz.
|
|
1425
|
+
if (!(g.partes || []).some((pt) => (pt.palavras || []).length)) {
|
|
1426
|
+
try {
|
|
1427
|
+
const cfg = require('../config').loadConfig();
|
|
1428
|
+
const tr = await require('../api').requestTranscricao(cfg.server, { token: cfg.token, audio: fs.readFileSync(mp3).toString('base64'), nome: 'voz.mp3', idioma: 'pt' });
|
|
1429
|
+
if (tr && (tr.palavras || []).length) {
|
|
1430
|
+
g.partes = alinharPartes(lista, tr.palavras, g.segundos);
|
|
1431
|
+
fs.writeFileSync(cacheJson, JSON.stringify({ voz: g.voz, segundos: g.segundos, partes: g.partes }));
|
|
1432
|
+
}
|
|
1433
|
+
} catch { /* fica o alinhamento guardado */ }
|
|
1434
|
+
}
|
|
1435
|
+
return g;
|
|
1436
|
+
}
|
|
1437
|
+
const g = await narrarComGemini(lista, mp3, { voz: args.voz || roteiro.voz, direcao: DIRECAO_CINEMA });
|
|
1438
|
+
if (g.ok) {
|
|
1439
|
+
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 */ }
|
|
1440
|
+
}
|
|
1441
|
+
return g;
|
|
1442
|
+
};
|
|
1443
|
+
v = await produzirNecessidade(p, roteiro, { tipo: 'voz', chave: [lista.map((x) => x.texto), args.voz || roteiro.voz || '', 'cinema-v1', soEdge], destino: mp3 },
|
|
1444
|
+
provedoresDeVoz(lista, mp3, arq, args.voz || roteiro.voz, 'antonio', { soEdge, gemini, escolha: (roteiro.provedores || {}).voz }));
|
|
1364
1445
|
}
|
|
1365
1446
|
if (!v.ok) return v;
|
|
1366
1447
|
/* A DURAÇÃO que o roteiro pede ("60–75 s"): se a voz passa do que cabe,
|
|
@@ -1404,25 +1485,32 @@ async function prepararRoteiro(p, args) {
|
|
|
1404
1485
|
}
|
|
1405
1486
|
}
|
|
1406
1487
|
projeto.anotar(p.id, 'narracao', `${v.segundos}s em ${lista.length} partes, voz ${v.voz}`);
|
|
1407
|
-
|
|
1408
|
-
|
|
1488
|
+
/* A chave leva o id do projeto e o hash das falas. Só `cinema` fazia um
|
|
1489
|
+
roteiro novo, no mesmo projeto, reaproveitar os efeitos do anterior. */
|
|
1490
|
+
const marcaSfx = require('crypto').createHash('sha1').update(JSON.stringify({ id: p.id || '', falas: partes.map((pt) => cinema.falaDe(pt)) })).digest('hex').slice(0, 16);
|
|
1491
|
+
const sonsC = (await produzirNecessidade(p, roteiro, { tipo: 'sfx', chave: [p.id || '', marcaSfx] },
|
|
1492
|
+
[{ nome: 'openverse', gerar: async () => ({ sons: await sonsReais(p, video) }) }])).sons || {};
|
|
1409
1493
|
// Vários termos e durações: vídeo sem música foi o que saiu na primeira vez (25/09).
|
|
1410
1494
|
// E só instrumental: "essa música daria pra deixar só o instrumental?" — a faixa é OUVIDA.
|
|
1411
1495
|
// 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
|
-
|
|
1496
|
+
let trilhaC = (await produzirNecessidade(p, roteiro, { tipo: 'trilha', chave: ['cinema', roteiro.clima || '', roteiro.genero || ''] },
|
|
1497
|
+
[{ nome: 'jamendo', gerar: async () => {
|
|
1498
|
+
for (const termo of [...variedade.termosTrilha(roteiro, 'cinema'), 'instrumental']) {
|
|
1499
|
+
try {
|
|
1500
|
+
const b = await assets.procurarMusica(termo, { quantas: 10, duracaoMin: 45 });
|
|
1501
|
+
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)) {
|
|
1502
|
+
const d = await assets.baixar(item, { pasta: p.pasta, nome: 'trilha' });
|
|
1503
|
+
if (!d.ok) continue;
|
|
1504
|
+
if (await temLetra(path.join(p.pasta, d.relativo))) { projeto.anotar(p.id, 'midia', `trilha com voz descartada: ${item.titulo}`); continue; }
|
|
1505
|
+
variedade.marcar('trilha', item.url);
|
|
1506
|
+
const credito = `"${termo}": ${item.titulo} — ${item.autor || 'Jamendo'} (${item.licenca || 'CC'})`;
|
|
1507
|
+
projeto.anotar(p.id, 'trilha', credito);
|
|
1508
|
+
return { src: d.relativo, credito };
|
|
1509
|
+
}
|
|
1510
|
+
} catch { /* o próximo termo */ }
|
|
1423
1511
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1512
|
+
return null;
|
|
1513
|
+
} }])).src || null;
|
|
1426
1514
|
/* A MÚSICA mais baixa na origem: a faixa vem masterizada alta e comprimida,
|
|
1427
1515
|
e mesmo a 30% ela cobria a voz e o depoimento ("a música tá muito
|
|
1428
1516
|
alta, não dá pra ouvir as pessoas", 25/09). Ela vai a -28 LUFS antes
|
|
@@ -1469,11 +1557,12 @@ async function prepararRoteiro(p, args) {
|
|
|
1469
1557
|
try { fs.unlinkSync(path.join(p.pasta, rel)); } catch { /* fica o original */ }
|
|
1470
1558
|
return 'midia/' + destino;
|
|
1471
1559
|
};
|
|
1560
|
+
// banco: true marca o que veio do banco (Pexels): a rota produto prefere a tela da marca (hibrido.ancorar).
|
|
1472
1561
|
const achados = await Promise.all(hibrido.termosDeBusca(roteiro, hib.extras).map(async (termo) => {
|
|
1473
1562
|
const out = [];
|
|
1474
1563
|
const slug = 'broll-' + String(termo).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
1475
1564
|
const proxy = `${slug}.leve.mp4`;
|
|
1476
|
-
if (jaTem.includes(proxy)) out.push({ src: 'midia/' + proxy, tipo: 'video', cat: cat(termo), alt: termo });
|
|
1565
|
+
if (jaTem.includes(proxy)) out.push({ src: 'midia/' + proxy, tipo: 'video', cat: cat(termo), alt: termo, banco: true });
|
|
1477
1566
|
else try {
|
|
1478
1567
|
// Sorteado entre os não usados, com o termo variado (variedade.js, 05/10).
|
|
1479
1568
|
const itens = await variedade.buscarSemRepetir(async (t) => {
|
|
@@ -1483,11 +1572,11 @@ async function prepararRoteiro(p, args) {
|
|
|
1483
1572
|
for (const item of itens.slice(0, 1)) {
|
|
1484
1573
|
const d = await assets.baixar(item, { pasta: p.pasta, nome: slug });
|
|
1485
1574
|
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); }
|
|
1575
|
+
if (l) { out.push({ src: l, tipo: 'video', cat: cat(termo), alt: termo, banco: true }); variedade.marcar('broll', item.url); }
|
|
1487
1576
|
}
|
|
1488
1577
|
} catch { /* segue com a foto */ }
|
|
1489
1578
|
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 });
|
|
1579
|
+
if (foto) out.push({ src: 'midia/' + foto, tipo: 'imagem', cat: cat(termo), alt: termo, banco: true });
|
|
1491
1580
|
else try {
|
|
1492
1581
|
const itens = await variedade.buscarSemRepetir(async (t) => {
|
|
1493
1582
|
const b = await assets.procurarImagens(t, { quantas: 8, orientacao });
|
|
@@ -1495,7 +1584,7 @@ async function prepararRoteiro(p, args) {
|
|
|
1495
1584
|
}, termo);
|
|
1496
1585
|
for (const item of itens.slice(0, 1)) {
|
|
1497
1586
|
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); }
|
|
1587
|
+
if (d.ok) { out.push({ src: d.relativo, tipo: 'imagem', cat: cat(termo), alt: termo, banco: true }); variedade.marcar('broll', item.url); }
|
|
1499
1588
|
}
|
|
1500
1589
|
} catch { /* sem */ }
|
|
1501
1590
|
return out;
|
|
@@ -1554,7 +1643,9 @@ async function prepararRoteiro(p, args) {
|
|
|
1554
1643
|
const r = cinema.montarCinema({ roteiro, largura, altura, tempos: v.partes, narracao: path.relative(p.pasta, mp3).replace(/\\/g, '/'),
|
|
1555
1644
|
sons: sonsC, trilha: trilhaC, fotos: fotosC, fortes, ritmoLeitura: hib ? 0.55 : 1 });
|
|
1556
1645
|
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
|
|
1646
|
+
hibrido.enfeitar(r.plano, { extras: hib.extras, partes, marca: roteiro.marca || {}, largura, altura, hud: roteiro.hud === true, particulas: roteiro.particulas === true,
|
|
1647
|
+
// O b-roll de cada frase troca 0,2 s depois da palavra-âncora (hibrido.ancorar).
|
|
1648
|
+
tempos: v.partes, fortes, fotos: fotosC });
|
|
1558
1649
|
r.resumo.estilo = 'hibrido';
|
|
1559
1650
|
}
|
|
1560
1651
|
if (r.ok) projeto.anotar(p.id, 'plano', `estilo ${hib ? 'hibrido' : 'cinema'}: ${r.resumo.cenas} cenas, ${r.resumo.segundos}s`);
|
|
@@ -1579,15 +1670,41 @@ async function prepararRoteiro(p, args) {
|
|
|
1579
1670
|
/* Uma voz por falante (falantes.js). Com uma voz só, um grupo só: é a
|
|
1580
1671
|
narração de sempre, com o mesmo cache. */
|
|
1581
1672
|
const vozesL = falantes.vozesDasPartes(partes, roteiro, args.voz || roteiro.voz);
|
|
1582
|
-
let v = await
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
const
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1673
|
+
let v = await produzirNecessidade(p, roteiro, { tipo: 'voz', chave: [lista.map((x) => x.texto), vozesL, direcao], destino: mp3 }, [{
|
|
1674
|
+
nome: ((roteiro.provedores || {}).voz) || 'estudio',
|
|
1675
|
+
gerar: async () => {
|
|
1676
|
+
let reserva = false, gemini = false;
|
|
1677
|
+
const comLista = lista.map((x, i) => ({ ...x, voz: vozesL[i] }));
|
|
1678
|
+
const pelaReserva = async (l, m, voz) => {
|
|
1679
|
+
// O Kokoro pedido já foi tentado no narrarComCache: a reserva não repete.
|
|
1680
|
+
const a = m === mp3 ? arq : m.replace(/\.mp3$/, '.json');
|
|
1681
|
+
if (a !== arq) fs.writeFileSync(a, JSON.stringify(l));
|
|
1682
|
+
const rr = await narrarReserva(l, m, a, voz, 'thalita', { semKokoro: kokoro.querKokoro(voz) });
|
|
1683
|
+
if (rr && rr.ok) reserva = true;
|
|
1684
|
+
return rr;
|
|
1685
|
+
};
|
|
1686
|
+
let r = await falantes.narrarPorFalante(comLista, mp3, { juntar: juntarVozes,
|
|
1687
|
+
narrarUm: async (l, m, voz) => {
|
|
1688
|
+
const g = await narrarComCache(l, m, { voz, direcao });
|
|
1689
|
+
if (g.ok) { gemini = true; return g; }
|
|
1690
|
+
return pelaReserva(l, m, voz);
|
|
1691
|
+
} });
|
|
1692
|
+
/* UMA VOZ SÓ POR VÍDEO (11/10, "os personagens falam com vozes
|
|
1693
|
+
diferentes em cada cena"): a cota do Gemini acabava no meio, e o
|
|
1694
|
+
mesmo personagem saía Gemini numa fala e reserva na outra. Se
|
|
1695
|
+
misturou, o vídeo inteiro é refeito na reserva — pior que o
|
|
1696
|
+
Gemini, mas cada um com a mesma voz do começo ao fim. */
|
|
1697
|
+
if (r && r.ok && reserva && gemini) {
|
|
1698
|
+
gemini = false;
|
|
1699
|
+
r = await falantes.narrarPorFalante(comLista, mp3, { juntar: juntarVozes, narrarUm: pelaReserva });
|
|
1700
|
+
}
|
|
1701
|
+
/* A voz de RESERVA (Kokoro/Edge, quando o Gemini falhou) não vai para o
|
|
1702
|
+
cache do projeto: guardada, ela voltava em todo refazer do mesmo
|
|
1703
|
+
texto, mesmo com o Gemini de pé — era a "voz robótica que não sai"
|
|
1704
|
+
(11/10). Na próxima vez o Gemini é tentado de novo. */
|
|
1705
|
+
return r && r.ok ? { ...r, arquivo: mp3, ...(reserva ? { naoGuardar: true } : {}) } : r;
|
|
1706
|
+
},
|
|
1707
|
+
}]);
|
|
1591
1708
|
if (!v.ok) return v;
|
|
1592
1709
|
/* O RITMO DA VOZ. Mesmo com a direção, a voz sai no passo de leitura, e
|
|
1593
1710
|
o vídeo — que segue a voz palavra por palavra — sai lento junto. O
|
|
@@ -1793,8 +1910,23 @@ async function prepararRoteiro(p, args) {
|
|
|
1793
1910
|
return r;
|
|
1794
1911
|
}
|
|
1795
1912
|
if (estilo === 'personagem' && modo === 'video') {
|
|
1796
|
-
const r = personagem.montarPersonagem({ roteiro, largura, altura, tempos, narracao, sons, trilha });
|
|
1797
|
-
if (r.ok)
|
|
1913
|
+
const r = personagem.montarPersonagem({ roteiro: { ...roteiro, pedido: roteiro.pedido || pedidoTxt }, largura, altura, tempos, narracao, sons, trilha });
|
|
1914
|
+
if (r.ok) {
|
|
1915
|
+
/* O que não é peça do catálogo vira ícone 3D da biblioteca, em blocos
|
|
1916
|
+
(kit/mundo.jsx): "uma pizza que fala", "um violão na praia". Sem
|
|
1917
|
+
ícone, a peça sai da cena (e a coisa que fala vira o mascote). */
|
|
1918
|
+
const achados = {};
|
|
1919
|
+
for (const nome of mundo.iconesPendentes(r.partitura)) {
|
|
1920
|
+
const id = colecao.melhorIcone3d(nome, 40);
|
|
1921
|
+
if (!id) continue;
|
|
1922
|
+
const b = await colecao.baixar(id, { pasta: p.pasta, largura, altura }).catch(() => null);
|
|
1923
|
+
if (b && b.ok) achados[nome] = b.src;
|
|
1924
|
+
}
|
|
1925
|
+
mundo.aplicarIcones(r.partitura, achados);
|
|
1926
|
+
mundo.adicionarObjetos(r.partitura, objetosDoRoteiro(roteiro.objetos, p.pasta));
|
|
1927
|
+
const icones = Object.keys(achados);
|
|
1928
|
+
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(', ')}` : ''}`);
|
|
1929
|
+
}
|
|
1798
1930
|
return r;
|
|
1799
1931
|
}
|
|
1800
1932
|
if (estilo === 'motion' && modo === 'video') {
|
|
@@ -1870,8 +2002,9 @@ async function transcrever(p, alvo, ctx) {
|
|
|
1870
2002
|
// ── estudio_render ───────────────────────────────────────────────────────
|
|
1871
2003
|
|
|
1872
2004
|
async function ferramentaRender(args, ctx = {}) {
|
|
1873
|
-
const p =
|
|
2005
|
+
const p = projetoOuRecente(args.id);
|
|
1874
2006
|
if (!p) return semProjeto(args.id);
|
|
2007
|
+
if (args.id == null || String(args.id).trim() === '') args = { ...args, id: p.id };
|
|
1875
2008
|
const pl = projeto.lerPlano(args.id);
|
|
1876
2009
|
if (!pl) return { ok: false, error: 'não há plano neste projeto.' };
|
|
1877
2010
|
|
|
@@ -2117,7 +2250,7 @@ async function ferramentaSite(args) {
|
|
|
2117
2250
|
}
|
|
2118
2251
|
|
|
2119
2252
|
module.exports = {
|
|
2120
|
-
vazamentoNoComeco, variarIcones,
|
|
2253
|
+
vazamentoNoComeco, variarIcones, objetosDoRoteiro,
|
|
2121
2254
|
ferramentaProjeto, ferramentaPlano, ferramentaMidia, ferramentaNarrar,
|
|
2122
2255
|
ferramentaVideo, ferramentaRender, ferramentaSite,
|
|
2123
2256
|
projeto, plano, remotion, assets, video, identidade, slides, python, rodarPython, alinharPartes, prepararRoteiro, roteiroValido, juntarVozes,
|