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
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
'use strict';
|
|
29
29
|
|
|
30
30
|
const { volumesDaTrilha } = require('./ritmo.js');
|
|
31
|
+
const mundo = require('./mundo.js');
|
|
32
|
+
const colecao = require('./colecao.js');
|
|
31
33
|
|
|
32
34
|
const FPS = 30;
|
|
33
35
|
const r2 = (x) => Math.round(x * 100) / 100;
|
|
@@ -63,7 +65,7 @@ const CENARIOS_VOXEL = {
|
|
|
63
65
|
// O nome antigo do set (podcast, estúdio…) tem um par no voxel.
|
|
64
66
|
const VOXEL_DO_LISO = { podcast: 'telas', estudio: 'oceano', escritorio: 'terra', noite: 'neon' };
|
|
65
67
|
// O mascote é laranja (o do Claude) a menos que o roteiro diga a cor.
|
|
66
|
-
const CORES_MASCOTE = { claude: '#
|
|
68
|
+
const CORES_MASCOTE = { claude: '#dd5a2b', gpt: '#10a37f', chatgpt: '#10a37f', gemini: '#4f7cff', primo: '#22b8d8', primocode: '#22b8d8' };
|
|
67
69
|
const ROUPAS_VOXEL = ['#c2452f', '#2f4cdd', '#1f8a5b', '#d08a1e', '#7b5cff', '#e8e2d6'];
|
|
68
70
|
// A pele de bloco é um tom mais fundo: sob a luz quente, o tom claro estourava.
|
|
69
71
|
const PELES_VOXEL = ['#d9a07a', '#c08660', '#a8704c', '#8a5a3c', '#6b4229'];
|
|
@@ -84,12 +86,44 @@ function rng(semente) {
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/** A aparência de quem fala: o que o roteiro disse vale; o resto sai do nome. */
|
|
89
|
+
/* O TIPO de quem fala (11/10, "se eu pedir um cachorro ele vai fazer"):
|
|
90
|
+
pessoa · robo (o mascote) · animal (uma espécie que o kit/mundo.jsx monta) ·
|
|
91
|
+
coisa (qualquer objeto com ícone 3D, em blocos: "a pizza conversando com o
|
|
92
|
+
café"). O que o roteiro disse vale; senão, o NOME decide. */
|
|
93
|
+
const TIPOS = ['pessoa', 'robo', 'animal', 'coisa'];
|
|
94
|
+
function especieDe(txt) {
|
|
95
|
+
const s = mundo.sem(txt);
|
|
96
|
+
return Object.keys(mundo.ESPECIES).find((k) => mundo.ESPECIES[k].test(s) || k === s) || null;
|
|
97
|
+
}
|
|
98
|
+
function iconeDe(txt) {
|
|
99
|
+
const t = String(txt || '').trim().toLowerCase();
|
|
100
|
+
if (!t) return null;
|
|
101
|
+
return colecao.melhorIcone3d(t, 80) || (colecao.PT_EN[t] || colecao.PT_EN[mundo.sem(t)] ? colecao.melhorIcone3d(t, 40) : null);
|
|
102
|
+
}
|
|
103
|
+
function tipoDe(nome, pedido = {}) {
|
|
104
|
+
if (TIPOS.includes(pedido.tipo)) return pedido.tipo;
|
|
105
|
+
if (pedido.especie) return 'animal';
|
|
106
|
+
if (pedido.icone || pedido.coisa) return 'coisa';
|
|
107
|
+
if (especieDe(nome)) return 'animal';
|
|
108
|
+
if (EH_ROBO.test(nome)) return 'robo';
|
|
109
|
+
if (iconeDe(nome)) return 'coisa';
|
|
110
|
+
return 'pessoa';
|
|
111
|
+
}
|
|
112
|
+
|
|
87
113
|
function aparencia(nome, pedido = {}) {
|
|
88
114
|
const k = (s) => hashDe(nome + ':' + s);
|
|
89
|
-
const tipo =
|
|
115
|
+
const tipo = tipoDe(nome, pedido);
|
|
90
116
|
const rotulo = String(pedido.rotulo || pedido.nome || nome || '').trim().slice(0, 24);
|
|
117
|
+
if (tipo === 'animal') {
|
|
118
|
+
const especie = especieDe(pedido.especie || '') || especieDe(nome) || 'cachorro';
|
|
119
|
+
return { nome, rotulo, tipo, especie, ...(pedido.cor ? { cor: pedido.cor } : {}), ...(pedido.cor2 ? { cor2: pedido.cor2 } : {}) };
|
|
120
|
+
}
|
|
121
|
+
if (tipo === 'coisa') {
|
|
122
|
+
const termo = String(pedido.icone || pedido.coisa || nome);
|
|
123
|
+
return { nome, rotulo, tipo, icone: termo, ...(pedido.src ? { src: pedido.src } : {}) };
|
|
124
|
+
}
|
|
91
125
|
if (tipo === 'robo') {
|
|
92
|
-
return { nome, rotulo, tipo, cor: pedido.cor || pegar(ROBOS, k('cor')), olhos: pedido.olhos || null };
|
|
126
|
+
return { nome, rotulo, tipo, cor: pedido.cor || pegar(ROBOS, k('cor')), olhos: pedido.olhos || null, ...(pedido.bigode ? { bigode: true } : {}) };
|
|
93
127
|
}
|
|
94
128
|
return {
|
|
95
129
|
nome, rotulo, tipo,
|
|
@@ -99,6 +133,9 @@ function aparencia(nome, pedido = {}) {
|
|
|
99
133
|
roupa: pedido.roupa || pegar(ROUPAS, k('roupa')),
|
|
100
134
|
oculos: pedido.oculos != null ? !!pedido.oculos : k('oculos') < 0.3,
|
|
101
135
|
barba: !!pedido.barba, // só quando pedida: sorteada, ficava estranha em quem tem cabelo longo
|
|
136
|
+
bigode: !!pedido.bigode,
|
|
137
|
+
// A cabeça facetada (low-poly) é o padrão; 'cubo' é o rosto de pixel da 10.5.
|
|
138
|
+
...(pedido.cabeca === 'cubo' ? { cabeca: 'cubo' } : {}),
|
|
102
139
|
};
|
|
103
140
|
}
|
|
104
141
|
|
|
@@ -119,6 +156,22 @@ function destaqueDe(pt, texto) {
|
|
|
119
156
|
return m ? m[1].trim() : null;
|
|
120
157
|
}
|
|
121
158
|
|
|
159
|
+
/* O que o corpo faz na fala, além de falar. O roteiro diz (parte.acao); sem
|
|
160
|
+
ele, o texto sugere: "oi"/"tchau" acena, "olha"/"aqui" aponta, "!" de
|
|
161
|
+
vitória comemora. kit/voxel.jsx e kit/mundo.jsx desenham cada uma. */
|
|
162
|
+
const ACOES = ['falar', 'acenar', 'apontar', 'pular', 'dancar', 'comemorar', 'pensar', 'andar', 'correr', 'sentar'];
|
|
163
|
+
function acaoDe(pt, texto) {
|
|
164
|
+
const a = mundo.sem(pt.acao || '').replace(/[^a-z]/g, '');
|
|
165
|
+
const achada = ACOES.find((x) => a.startsWith(x.slice(0, 5)));
|
|
166
|
+
if (achada) return achada;
|
|
167
|
+
const s = mundo.sem(texto);
|
|
168
|
+
if (/^(oi|ola|e ai|fala,|bom dia|boa tarde|boa noite)\b|\b(tchau|ate mais|ate logo|valeu, ate)\b/.test(s)) return 'acenar';
|
|
169
|
+
if (/\b(consegui|conseguimos|ganhamos|vencemos|incrivel|isso ai|uhu|eba|viva)\b.*!/.test(s)) return 'comemorar';
|
|
170
|
+
if (/\b(olha|olhe|veja|repara|ali|aqui o|esse aqui)\b/.test(s)) return 'apontar';
|
|
171
|
+
if (/\b(sera que|hmm+|deixa eu pensar|pensando)\b/.test(s)) return 'pensar';
|
|
172
|
+
return 'falar';
|
|
173
|
+
}
|
|
174
|
+
|
|
122
175
|
const falaDe = (pt) => String(pt.texto || pt.fala || pt.narracao || pt.titulo || '').trim();
|
|
123
176
|
|
|
124
177
|
/* Sem os tempos da voz, cada fala ganha o tempo de leitura (2,6 palavras/s)
|
|
@@ -206,6 +259,36 @@ function paineisDe(falas, roteiro = {}) {
|
|
|
206
259
|
return out.length ? out : [{ titulo: String(roteiro.titulo || 'Agora').slice(0, 18), linha: '' }];
|
|
207
260
|
}
|
|
208
261
|
|
|
262
|
+
|
|
263
|
+
/* BICHO NÃO FALA (11/10, "e o cachorro fala? mds kkk"): fala de bicho com voz
|
|
264
|
+
de gente fica ridícula. A parte do bicho SAI da narração e ele fica na cena
|
|
265
|
+
como figurante (abanando o rabo). Ele só fala se o pedido disser que fala:
|
|
266
|
+
"cachorro falante", "um gato que fala", "o cachorro falando". Muda o
|
|
267
|
+
roteiro no lugar (o array de partes é o mesmo) e devolve quem calou. */
|
|
268
|
+
function calarBichos(roteiro, pedido = '') {
|
|
269
|
+
const partes = Array.isArray(roteiro && roteiro.partes) ? roteiro.partes : [];
|
|
270
|
+
const pedidos = (roteiro && roteiro.personagens) || {};
|
|
271
|
+
const ped = mundo.sem(pedido);
|
|
272
|
+
const calados = [];
|
|
273
|
+
for (let i = partes.length - 1; i >= 0; i--) {
|
|
274
|
+
const pt = partes[i];
|
|
275
|
+
const nome = typeof (pt && pt.falante) === 'string' ? pt.falante.trim().toLowerCase() : '';
|
|
276
|
+
if (!nome) continue;
|
|
277
|
+
const dado = pedidos[nome] || pedidos[Object.keys(pedidos).find((k) => k.toLowerCase() === nome)] || {};
|
|
278
|
+
const fala = mundo.sem(partes.filter((x) => String(x.falante || '').trim().toLowerCase() === nome).map(falaDe).join(' '));
|
|
279
|
+
const latido = /\bau[ -]?au\b|\bau!|\bmiau|\bmu+u\b|\boinc\b|\bquack\b|\bcocoric/.test(fala);
|
|
280
|
+
const tipo = dado.tipo || (dado.especie ? 'animal' : (especieDe(nome) || latido ? 'animal' : null));
|
|
281
|
+
if (tipo !== 'animal' || dado.fala === true) continue;
|
|
282
|
+
const esp = especieDe(dado.especie || '') || especieDe(nome) || (/\bmiau/.test(fala) ? 'gato' : 'cachorro');
|
|
283
|
+
const falante = new RegExp(`\\b(${Object.keys(mundo.ESPECIES).join('|')}|bicho|animal|${nome})\\w*\\s+(falante|que fala|falando|fala)\\b`).test(ped);
|
|
284
|
+
if (falante) continue;
|
|
285
|
+
partes.splice(i, 1);
|
|
286
|
+
if (!calados.some((c) => c.nome === nome)) calados.push({ nome, especie: esp });
|
|
287
|
+
}
|
|
288
|
+
if (calados.length) roteiro.figurantesExtra = [...(roteiro.figurantesExtra || []), ...calados.map((c) => ({ nome: c.nome, tipo: 'animal', especie: c.especie }))];
|
|
289
|
+
return calados;
|
|
290
|
+
}
|
|
291
|
+
|
|
209
292
|
/** A partitura inteira: o que o kit/personagem.jsx desenha, quadro a quadro. */
|
|
210
293
|
function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = [] }) {
|
|
211
294
|
const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object' && falaDe(p));
|
|
@@ -220,9 +303,20 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
|
|
|
220
303
|
if (!nomes.includes(f)) nomes.push(f);
|
|
221
304
|
}
|
|
222
305
|
const pedidos = roteiro.personagens && typeof roteiro.personagens === 'object' ? roteiro.personagens : {};
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
306
|
+
/* Quem LATE é cachorro: um falante sem tipo cuja fala tem a voz de um
|
|
307
|
+
bicho ("Au au!", "Miau") vira aquele bicho — o modelo pequeno escreve o
|
|
308
|
+
diálogo e esquece de dizer que o Rex é um cachorro. */
|
|
309
|
+
const VOZ_DE_BICHO = [[/\bau[ -]?au\b|\bau!|\bauau\b|\bwoof\b/, 'cachorro'], [/\bmiau+\b/, 'gato'], [/\bmu+u\b/, 'vaca'], [/\boinc\b/, 'porco'],
|
|
310
|
+
[/\bquack\b|\bqua[ -]?qua\b/, 'pato'], [/\bcocoric[oó]\b/, 'galinha'], [/\bpiu[ -]?piu\b/, 'passaro'], [/\bme+h+\b|\bbe+h+\b/, 'ovelha'], [/\brua+r+\b|\broar\b/, 'leao']];
|
|
311
|
+
const pedidoDe = (n) => {
|
|
312
|
+
const dado = pedidos[n] || pedidos[Object.keys(pedidos).find((k) => k.toLowerCase() === n)] || {};
|
|
313
|
+
if (dado.tipo || dado.especie) return dado;
|
|
314
|
+
const fala = mundo.sem(partes.filter((pt) => String(pt.falante || '').trim().toLowerCase() === n).map(falaDe).join(' '));
|
|
315
|
+
const bicho = VOZ_DE_BICHO.find(([re]) => re.test(fala));
|
|
316
|
+
return bicho ? { ...dado, tipo: 'animal', especie: bicho[1] } : dado;
|
|
317
|
+
};
|
|
318
|
+
// Até QUATRO falam no set ("três amigos no parque"); do quinto em diante dividem o lugar.
|
|
319
|
+
const elenco = nomes.slice(0, 4).map((n, i) => ({ ...aparencia(n, pedidoDe(n)), lado: nomes.length > 1 ? (i < nomes.length / 2 ? -1 : 1) : 0, ordem: i }));
|
|
226
320
|
const quemDe = (pt) => {
|
|
227
321
|
const f = typeof pt.falante === 'string' && pt.falante.trim() ? pt.falante.trim().toLowerCase() : 'apresentador';
|
|
228
322
|
return nomes.indexOf(f) % Math.max(1, elenco.length);
|
|
@@ -235,7 +329,7 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
|
|
|
235
329
|
.map((w) => ({ t: String(w.texto || w.word || ''), de: r2(w.de), ate: r2(w.ate) }));
|
|
236
330
|
return {
|
|
237
331
|
quem: quemDe(pt), de: r2(t.de != null ? t.de : (palavras[0] || {}).de || 0), ate: r2(t.ate != null ? t.ate : (palavras[palavras.length - 1] || {}).ate || 0),
|
|
238
|
-
texto, palavras, emocao: emocaoDe(pt, texto), destaque: destaqueDe(pt, texto),
|
|
332
|
+
texto, palavras, emocao: emocaoDe(pt, texto), destaque: destaqueDe(pt, texto), acao: acaoDe(pt, texto),
|
|
239
333
|
};
|
|
240
334
|
});
|
|
241
335
|
const duracao = r2((falas.length ? falas[falas.length - 1].ate : 3) + 1.2);
|
|
@@ -258,14 +352,72 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
|
|
|
258
352
|
cenarioFinal = { nome: nomeV, ...CENARIOS_VOXEL[nomeV] };
|
|
259
353
|
if (roteiro.acento && /^#[0-9a-f]{6}$/i.test(roteiro.acento)) cenarioFinal.acento = roteiro.acento;
|
|
260
354
|
for (const e of elenco) {
|
|
261
|
-
if (e.tipo === 'robo' && !(pedidoDe(e.nome) || {}).cor) e.cor = CORES_MASCOTE[e.nome] || '#
|
|
355
|
+
if (e.tipo === 'robo' && !(pedidoDe(e.nome) || {}).cor) e.cor = CORES_MASCOTE[e.nome] || '#dd5a2b';
|
|
262
356
|
if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).roupa) e.roupa = pegar(ROUPAS_VOXEL, hashDe(e.nome + ':roupa'));
|
|
263
357
|
if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).pele) e.pele = pegar(PELES_VOXEL, hashDe(e.nome + ':pele'));
|
|
264
358
|
if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).corCabelo) e.corCabelo = pegar(CABELOS_VOXEL, hashDe(e.nome + ':corCabelo'));
|
|
265
359
|
}
|
|
266
360
|
}
|
|
361
|
+
/* O MUNDO (mundo.js): o lugar e as coisas. roteiro.cena (texto livre ou
|
|
362
|
+
objeto) é o do vídeo; parte.cenario muda o lugar a partir daquela fala.
|
|
363
|
+
O pedido da pessoa e as falas completam o que a cena não disse. */
|
|
364
|
+
const pedidoTexto = String(roteiro.pedido || roteiro.descricao || '');
|
|
365
|
+
const textoTodo = [pedidoTexto, roteiro.titulo, ...falas.map((f) => f.texto)].filter(Boolean).join(' ');
|
|
366
|
+
const cenaBase = roteiro.cena || roteiro.cenario || '';
|
|
367
|
+
const mundos = [];
|
|
368
|
+
const indiceDe = (pedido) => {
|
|
369
|
+
const chave = JSON.stringify(pedido || '');
|
|
370
|
+
const ja = mundos.findIndex((m) => m.chave === chave);
|
|
371
|
+
if (ja >= 0) return ja;
|
|
372
|
+
mundos.push({ chave, ...mundo.mundoDe(pedido, { texto: textoTodo, pedidoTexto, semente: semente + mundos.length * 7, nElenco: elenco.length }) });
|
|
373
|
+
return mundos.length - 1;
|
|
374
|
+
};
|
|
375
|
+
indiceDe(cenaBase);
|
|
376
|
+
let atual = 0;
|
|
377
|
+
partes.forEach((pt, i) => {
|
|
378
|
+
const pedidoParte = pt.cena || pt.cenario;
|
|
379
|
+
if (pedidoParte) atual = indiceDe(pedidoParte);
|
|
380
|
+
if (falas[i]) falas[i].mundo = atual;
|
|
381
|
+
});
|
|
382
|
+
for (const m of mundos) delete m.chave;
|
|
383
|
+
// O set da referência (telas) segue com as cores dele; os outros lugares trazem as suas.
|
|
384
|
+
if (mundos[0].lugar !== 'telas') cenarioFinal = { ...cenarioFinal, nome: mundos[0].nome, parede: mundos[0].parede || mundos[0].ceu?.[1] || cenarioFinal.parede, acento: mundos[0].acento };
|
|
385
|
+
|
|
386
|
+
/* OS FIGURANTES: quem o pedido põe na cena e não fala ("um humano com o
|
|
387
|
+
cachorro", "dois humanos" com um narrador só). roteiro.figurantes manda;
|
|
388
|
+
senão, os bichos e as pessoas do pedido que sobraram do elenco. */
|
|
389
|
+
const figurantes = [];
|
|
390
|
+
if (Array.isArray(roteiro.figurantes)) {
|
|
391
|
+
for (const f of roteiro.figurantes.slice(0, 4)) {
|
|
392
|
+
const pedidoF = typeof f === 'string' ? { tipo: tipoDe(f, {}), especie: especieDe(f) || undefined, icone: f } : f || {};
|
|
393
|
+
const nomeF = typeof f === 'string' ? f : String(f.nome || f.especie || f.icone || 'figurante');
|
|
394
|
+
figurantes.push({ ...aparencia(nomeF, pedidoF) });
|
|
395
|
+
}
|
|
396
|
+
} else if (pedidoTexto) {
|
|
397
|
+
const lido = mundo.lerElenco(pedidoTexto);
|
|
398
|
+
const temEspecie = (esp) => elenco.some((e) => e.tipo === 'animal' && e.especie === esp);
|
|
399
|
+
for (const b of lido.bichos) {
|
|
400
|
+
for (let k = 0; k < b.quantos - (temEspecie(b.especie) ? 1 : 0) && figurantes.length < 3; k++) {
|
|
401
|
+
figurantes.push(aparencia(`${b.especie} ${k + 1}`, { tipo: 'animal', especie: b.especie }));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const pessoas = elenco.filter((e) => e.tipo === 'pessoa').length;
|
|
405
|
+
for (let k = pessoas; k < lido.pessoas && figurantes.length < 4; k++) figurantes.push(aparencia(`pessoa ${k + 1}`, { tipo: 'pessoa' }));
|
|
406
|
+
}
|
|
407
|
+
// Os bichos que calaram (calarBichos) entram como figurantes, antes de tudo.
|
|
408
|
+
for (const f of (Array.isArray(roteiro.figurantesExtra) ? roteiro.figurantesExtra : []).slice(0, 3)) {
|
|
409
|
+
if (!figurantes.some((x) => x.nome === f.nome)) figurantes.unshift(aparencia(f.nome, { tipo: 'animal', especie: f.especie }));
|
|
410
|
+
}
|
|
411
|
+
// O mesmo bicho que o pedido já pôs como figurante não aparece duas vezes.
|
|
412
|
+
for (let k = figurantes.length - 1; k > 0; k--) if (figurantes.slice(0, k).some((x) => x.tipo === 'animal' && x.especie === figurantes[k].especie && x !== figurantes[k] && /\d$/.test(figurantes[k].nome))) figurantes.splice(k, 1);
|
|
413
|
+
if (visual === 'voxel') {
|
|
414
|
+
for (const e of figurantes) {
|
|
415
|
+
if (e.tipo === 'pessoa') { e.roupa = pegar(ROUPAS_VOXEL, hashDe(e.nome + ':roupa')); e.pele = pegar(PELES_VOXEL, hashDe(e.nome + ':pele')); e.corCabelo = pegar(CABELOS_VOXEL, hashDe(e.nome + ':corCabelo')); }
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
267
419
|
return {
|
|
268
|
-
versao:
|
|
420
|
+
versao: 2, fps: FPS, largura, altura, duracao, semente, cenario: cenarioFinal, elenco, falas, planos, visual, mundos, figurantes,
|
|
269
421
|
paineis: paineisDe(falas, roteiro),
|
|
270
422
|
legenda: roteiro.legenda !== false,
|
|
271
423
|
titulo: typeof roteiro.titulo === 'string' ? roteiro.titulo.slice(0, 60) : '',
|
|
@@ -296,8 +448,9 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
|
|
|
296
448
|
};
|
|
297
449
|
return {
|
|
298
450
|
ok: true, plano, partitura,
|
|
299
|
-
resumo: { visual: partitura.visual, cenas: 1, segundos: DUR, fps: FPS, cenario: partitura.cenario.nome, elenco: partitura.elenco.map((e) => `${e.rotulo} (${e.tipo
|
|
451
|
+
resumo: { visual: partitura.visual, cenas: 1, segundos: DUR, fps: FPS, cenario: partitura.cenario.nome, elenco: partitura.elenco.map((e) => `${e.rotulo} (${e.tipo === 'animal' ? e.especie : e.tipo})`),
|
|
452
|
+
figurantes: partitura.figurantes.map((e) => (e.tipo === 'animal' ? e.especie : e.tipo)), lugares: partitura.mundos.map((m) => m.nome), planos: partitura.planos.length, falas: partitura.falas.length },
|
|
300
453
|
};
|
|
301
454
|
}
|
|
302
455
|
|
|
303
|
-
module.exports = { montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
|
|
456
|
+
module.exports = { calarBichos, montarPersonagem, partituraDe, planosDe, paineisDe, aparencia, tipoDe, acaoDe, destaqueDe, emocaoDe, CENARIOS, CENARIOS_VOXEL, EH_ROBO, FPS };
|
package/lib/estudio/plano.js
CHANGED
|
@@ -49,6 +49,7 @@ const fs = require('fs');
|
|
|
49
49
|
const path = require('path');
|
|
50
50
|
const morfar = require('./morfar.js');
|
|
51
51
|
const kit = require('./kit.js');
|
|
52
|
+
const movimentos = require('./movimentos.js');
|
|
52
53
|
|
|
53
54
|
// "kit": uma peça pronta do kit de motion (kit.js), pelo nome — ver kit/*.jsx.
|
|
54
55
|
const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente', 'grupo', 'kit']);
|
|
@@ -283,6 +284,11 @@ function conferir(plano, { pasta = null } = {}) {
|
|
|
283
284
|
if (!plano || typeof plano !== 'object') {
|
|
284
285
|
return { ok: false, problemas: [erro('plano', 'o plano é um objeto JSON')], duracao: 0 };
|
|
285
286
|
}
|
|
287
|
+
/* `movimento: "quicar-de-baixo"` vira quadros-chave (movimentos.js) antes
|
|
288
|
+
de tudo: o que segue confere o `anima` como se a IA o tivesse escrito. */
|
|
289
|
+
for (const d of movimentos.expandir(plano)) {
|
|
290
|
+
problemas.push(erro(d.onde, `o movimento "${d.nome}" não existe — procure com estudio_midia acao:"movimentos" termo:"…"`));
|
|
291
|
+
}
|
|
286
292
|
|
|
287
293
|
const f = plano.formato || {};
|
|
288
294
|
const largura = Number(f.largura) || 1920;
|
package/lib/estudio/producao.js
CHANGED
|
@@ -22,11 +22,22 @@
|
|
|
22
22
|
* provedor nenhum.
|
|
23
23
|
*
|
|
24
24
|
* Puro de propósito: a rede e o disco moram nos provedores, então o teste roda
|
|
25
|
-
* sem chave e sem internet.
|
|
25
|
+
* sem chave e sem internet. A exceção é o cacheDe(pasta), que guarda o que já
|
|
26
|
+
* foi produzido num projeto.
|
|
27
|
+
*
|
|
28
|
+
* ── UMA NECESSIDADE, UMA CORRENTE ────────────────────────────────────────
|
|
29
|
+
* produzirUma(pedido, provedores, { escolha, cache }) a mesma corrente para
|
|
30
|
+
* a voz, a trilha e os efeitos do index.js. `escolha` (do roteiro:
|
|
31
|
+
* `provedores: { voz: 'kokoro' }`) passa um provedor para a frente.
|
|
32
|
+
* O cache é por necessidade + provedor: trocar a voz só refaz a voz.
|
|
26
33
|
*/
|
|
27
34
|
|
|
28
35
|
'use strict';
|
|
29
36
|
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const crypto = require('crypto');
|
|
40
|
+
|
|
30
41
|
// Quantas fotos cada tipo de parte do cinema costuma consumir.
|
|
31
42
|
const FOTOS = { montagem: 3, climax: 4, numeros: 2, escada: 2, dia: 1 };
|
|
32
43
|
|
|
@@ -63,28 +74,107 @@ function necessidades(roteiro, { falaDe = textoPadrao } = {}) {
|
|
|
63
74
|
return pedidos;
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
/** O provedor escolhido no roteiro vai para a frente; o resto segue na ordem. */
|
|
78
|
+
function ordenar(provedores, escolha) {
|
|
79
|
+
const lista = (provedores || []).filter(Boolean);
|
|
80
|
+
const i = escolha ? lista.findIndex((pv) => pv.nome === String(escolha).toLowerCase()) : -1;
|
|
81
|
+
return i > 0 ? [lista[i], ...lista.slice(0, i), ...lista.slice(i + 1)] : lista;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A chave do cache: o que a cena pede (pedido.chave, ou o pedido todo) + quem gera. */
|
|
85
|
+
function chaveDe(pedido, nome) {
|
|
86
|
+
const { destino, ...resto } = pedido || {};
|
|
87
|
+
const quer = resto.chave !== undefined ? [resto.tipo, resto.chave] : resto;
|
|
88
|
+
return crypto.createHash('sha1').update(JSON.stringify([quer, nome])).digest('hex').slice(0, 16);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Uma necessidade pela corrente: o primeiro provedor que entrega ganha. Erro,
|
|
93
|
+
* null ou { ok: false } cai no próximo; indisponível nem é chamado. Antes de
|
|
94
|
+
* gerar, cada provedor olha o cache dele (a mesma necessidade, o mesmo provedor).
|
|
95
|
+
*/
|
|
96
|
+
async function produzirUma(pedido, provedores, { escolha, cache, log = () => {} } = {}) {
|
|
97
|
+
const tentou = [];
|
|
98
|
+
let ultimo = null;
|
|
99
|
+
for (const pv of ordenar(provedores, escolha)) {
|
|
100
|
+
if (pv.disponivel && !pv.disponivel()) continue;
|
|
101
|
+
const chave = chaveDe(pedido, pv.nome);
|
|
102
|
+
const guardado = cache && cache.ler(chave, pedido);
|
|
103
|
+
if (guardado) return { ...pedido, ...guardado, ok: true, quem: pv.nome, doCache: true, tentou };
|
|
104
|
+
let r;
|
|
105
|
+
try {
|
|
106
|
+
r = await pv.gerar(pedido);
|
|
107
|
+
if (r && r.ok !== false) {
|
|
108
|
+
if (cache && !r.naoGuardar) cache.gravar(chave, r);
|
|
109
|
+
return { ...pedido, ...r, ok: true, quem: pv.nome, tentou };
|
|
110
|
+
}
|
|
111
|
+
tentou.push({ nome: pv.nome, motivo: (r && r.error) || 'não devolveu nada' });
|
|
112
|
+
} catch (e) {
|
|
113
|
+
tentou.push({ nome: pv.nome, motivo: (e && e.message) || String(e) });
|
|
114
|
+
}
|
|
115
|
+
if (r) ultimo = r;
|
|
116
|
+
log(`${pedido.tipo}: ${pv.nome} falhou, próximo`);
|
|
117
|
+
}
|
|
118
|
+
return { ...pedido, ...(ultimo || {}), ok: false, error: (ultimo && ultimo.error) || `ninguém produziu ${pedido.tipo}`, tentou };
|
|
119
|
+
}
|
|
120
|
+
|
|
66
121
|
/** Cada pedido pela corrente dos provedores do tipo dele, na ordem da lista. */
|
|
67
|
-
async function produzir(pedidos, provedores, { log = () => {} } = {}) {
|
|
122
|
+
async function produzir(pedidos, provedores, { log = () => {}, escolhas = {}, cache } = {}) {
|
|
68
123
|
const itens = [];
|
|
69
124
|
for (const pedido of pedidos || []) {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
for (const pv of (provedores || []).filter((x) => x && x.tipo === pedido.tipo)) {
|
|
73
|
-
if (pv.disponivel && !pv.disponivel()) continue;
|
|
74
|
-
try {
|
|
75
|
-
const r = await pv.gerar(pedido);
|
|
76
|
-
if (r) { feito = { ...pedido, ...r, ok: true, quem: pv.nome, tentou }; break; }
|
|
77
|
-
tentou.push({ nome: pv.nome, motivo: 'não devolveu nada' });
|
|
78
|
-
} catch (e) {
|
|
79
|
-
tentou.push({ nome: pv.nome, motivo: (e && e.message) || String(e) });
|
|
80
|
-
}
|
|
81
|
-
log(`${pedido.tipo}: ${pv.nome} falhou, próximo`);
|
|
82
|
-
}
|
|
83
|
-
itens.push(feito || { ...pedido, ok: false, tentou });
|
|
125
|
+
const daqui = (provedores || []).filter((x) => x && x.tipo === pedido.tipo);
|
|
126
|
+
itens.push(await produzirUma(pedido, daqui, { escolha: escolhas[pedido.tipo], cache, log }));
|
|
84
127
|
}
|
|
85
128
|
return { itens, faltou: itens.filter((i) => !i.ok) };
|
|
86
129
|
}
|
|
87
130
|
|
|
131
|
+
/** Os créditos (licença aberta pede atribuição) de tudo o que foi produzido. */
|
|
132
|
+
function creditos(itens) {
|
|
133
|
+
return [...new Set((itens || []).filter((i) => i && i.ok).map((i) => i.credito).filter(Boolean))];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A linha do caderno: quem produziu cada coisa, e se veio do cache. */
|
|
137
|
+
function linhaCaderno(item) {
|
|
138
|
+
if (!item.ok) return `${item.tipo}: ninguém produziu (${(item.tentou || []).map((t) => t.nome).join(', ') || 'sem provedor'})`;
|
|
139
|
+
const antes = (item.tentou || []).length ? `; antes falhou: ${item.tentou.map((t) => t.nome).join(', ')}` : '';
|
|
140
|
+
return `${item.tipo}: ${item.quem} (${item.doCache ? 'do cache' : 'gerou'}${antes})${item.credito ? ' — ' + item.credito : ''}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* O cache de um projeto: midia/producao/cache.json. O arquivo de `r.arquivo`
|
|
145
|
+
* (a voz, que depois é acelerada no lugar) é copiado para lá e volta para o
|
|
146
|
+
* `pedido.destino` no acerto. Caminho "midia/..." que sumiu do disco é erro.
|
|
147
|
+
*/
|
|
148
|
+
function cacheDe(pasta) {
|
|
149
|
+
const dir = path.join(pasta, 'midia', 'producao');
|
|
150
|
+
const arq = path.join(dir, 'cache.json');
|
|
151
|
+
const ler = () => { try { return JSON.parse(fs.readFileSync(arq, 'utf8')); } catch { return {}; } };
|
|
152
|
+
const caminhos = (v) => (typeof v === 'string' ? (/^midia\//.test(v) ? [v] : [])
|
|
153
|
+
: v && typeof v === 'object' ? Object.values(v).flatMap(caminhos) : []);
|
|
154
|
+
return {
|
|
155
|
+
ler(chave, pedido = {}) {
|
|
156
|
+
const e = ler()[chave];
|
|
157
|
+
if (!e) return null;
|
|
158
|
+
const { arquivoCache, ...r } = e;
|
|
159
|
+
if (caminhos(r).some((c) => !fs.existsSync(path.join(pasta, c)))) return null;
|
|
160
|
+
if (arquivoCache) {
|
|
161
|
+
if (!pedido.destino || !fs.existsSync(path.join(dir, arquivoCache))) return null;
|
|
162
|
+
fs.copyFileSync(path.join(dir, arquivoCache), pedido.destino);
|
|
163
|
+
}
|
|
164
|
+
return r;
|
|
165
|
+
},
|
|
166
|
+
gravar(chave, r) {
|
|
167
|
+
try {
|
|
168
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
169
|
+
const { arquivo, ...resto } = r;
|
|
170
|
+
const e = { ...resto };
|
|
171
|
+
if (arquivo && fs.existsSync(arquivo)) { e.arquivoCache = chave + path.extname(arquivo); fs.copyFileSync(arquivo, path.join(dir, e.arquivoCache)); }
|
|
172
|
+
fs.writeFileSync(arq, JSON.stringify({ ...ler(), [chave]: e }));
|
|
173
|
+
} catch { /* sem cache */ }
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
88
178
|
/** O resultado no formato que o montarCinema já recebe. */
|
|
89
179
|
function paraCinema({ itens = [] } = {}) {
|
|
90
180
|
const ok = itens.filter((i) => i.ok);
|
|
@@ -103,4 +193,4 @@ function paraCinema({ itens = [] } = {}) {
|
|
|
103
193
|
};
|
|
104
194
|
}
|
|
105
195
|
|
|
106
|
-
module.exports = { necessidades, produzir, paraCinema, FOTOS };
|
|
196
|
+
module.exports = { necessidades, produzir, produzirUma, ordenar, chaveDe, creditos, linhaCaderno, cacheDe, paraCinema, FOTOS };
|
package/lib/estudio/projeto.js
CHANGED
|
@@ -39,10 +39,13 @@ const os = require('os');
|
|
|
39
39
|
const path = require('path');
|
|
40
40
|
const crypto = require('crypto');
|
|
41
41
|
|
|
42
|
+
const { nomeCurto, semColisao } = require('../nome-curto.js');
|
|
43
|
+
|
|
42
44
|
const CASA = path.join(os.homedir(), '.primocode', 'studio');
|
|
43
45
|
const PROJETOS = path.join(CASA, 'projetos');
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
/* Nome de ARQUIVO (o mp4, o pdf da saída). A pasta do projeto não usa mais
|
|
48
|
+
isto: ver `criar` e lib/nome-curto.js. */
|
|
46
49
|
function apelidar(bruto) {
|
|
47
50
|
return String(bruto || '')
|
|
48
51
|
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
@@ -59,23 +62,64 @@ function garantirCasa() {
|
|
|
59
62
|
return PROJETOS;
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
*
|
|
65
|
+
/* ── ONDE MORA CADA PROJETO ──────────────────────────────────────────────
|
|
66
|
+
* Antes: `projetos/2026-10-11-claude-e-humano-conversam-sobre-o-segund`. A
|
|
67
|
+
* data na frente servia para a lista sair em ordem, e o resto era a frase
|
|
68
|
+
* cortada no meio. O dono: "essa estrutura de pastas está horrível".
|
|
69
|
+
*
|
|
70
|
+
* Agora: `projetos/2026-10/claude-segundo-cerebro`. O MÊS vira pasta (a
|
|
71
|
+
* ordem cronológica continua de graça, e um mês cabe numa tela), e o nome é
|
|
72
|
+
* curto e legível (lib/nome-curto.js). O dia exato fica no projeto.json.
|
|
64
73
|
*
|
|
65
|
-
* O id
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
74
|
+
* O id continua sendo SÓ o nome da pasta — sem o mês. É ele que o modelo
|
|
75
|
+
* escreve, que vai na memória e que a pessoa cita; pôr barra nele faria o
|
|
76
|
+
* `pastaDe` recusar (e com razão). Por isso o nome é único entre TODOS os
|
|
77
|
+
* meses, e quem acha a pasta pelo id é o `pastaDe`, que olha os meses.
|
|
78
|
+
*
|
|
79
|
+
* Os projetos antigos (`projetos/2026-09-25-...`) ficam onde estão: nada é
|
|
80
|
+
* renomeado, e o id antigo continua abrindo o mesmo projeto. */
|
|
81
|
+
const MES = /^\d{4}-\d{2}$/;
|
|
82
|
+
|
|
83
|
+
function mesDe(data = new Date()) {
|
|
84
|
+
return `${data.getFullYear()}-${String(data.getMonth() + 1).padStart(2, '0')}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** As pastas de mês que existem, da mais nova para a mais velha. */
|
|
88
|
+
function meses() {
|
|
89
|
+
try {
|
|
90
|
+
return fs.readdirSync(PROJETOS, { withFileTypes: true })
|
|
91
|
+
.filter((d) => d.isDirectory() && MES.test(d.name)).map((d) => d.name).sort().reverse();
|
|
92
|
+
} catch { return []; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Todos os ids que existem, antigos (soltos) e novos (dentro do mês). */
|
|
96
|
+
function idsTomados() {
|
|
97
|
+
const ids = [];
|
|
98
|
+
try {
|
|
99
|
+
for (const d of fs.readdirSync(PROJETOS, { withFileTypes: true })) {
|
|
100
|
+
if (d.isDirectory() && !MES.test(d.name)) ids.push(d.name);
|
|
101
|
+
}
|
|
102
|
+
} catch { /* casa vazia */ }
|
|
103
|
+
for (const m of meses()) {
|
|
104
|
+
try {
|
|
105
|
+
for (const d of fs.readdirSync(path.join(PROJETOS, m), { withFileTypes: true })) {
|
|
106
|
+
if (d.isDirectory()) ids.push(d.name);
|
|
107
|
+
}
|
|
108
|
+
} catch { /* mês sumiu no meio */ }
|
|
109
|
+
}
|
|
110
|
+
return ids;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Abre um projeto novo, em `projetos/<AAAA-MM>/<nome-curto>`.
|
|
69
115
|
*/
|
|
70
116
|
function criar({ titulo, pedido, formato = 'video' }) {
|
|
71
117
|
garantirCasa();
|
|
72
|
-
const
|
|
73
|
-
const base =
|
|
74
|
-
|
|
75
|
-
let n = 2;
|
|
76
|
-
while (fs.existsSync(path.join(PROJETOS, id))) { id = `${base}-${n}`; n++; }
|
|
118
|
+
const texto = titulo || pedido;
|
|
119
|
+
const base = nomeCurto(texto) || 'projeto';
|
|
120
|
+
const id = semColisao(base, idsTomados(), `${titulo || ''} ${pedido || ''}`);
|
|
77
121
|
|
|
78
|
-
const pasta = path.join(PROJETOS, id);
|
|
122
|
+
const pasta = path.join(PROJETOS, mesDe(), id);
|
|
79
123
|
for (const sub of ['midia', 'saida', 'entrada']) {
|
|
80
124
|
fs.mkdirSync(path.join(pasta, sub), { recursive: true });
|
|
81
125
|
}
|
|
@@ -125,7 +169,18 @@ function pastaDe(id) {
|
|
|
125
169
|
if (typeof id !== 'string' || !id.trim()) return null;
|
|
126
170
|
const limpo = id.trim();
|
|
127
171
|
if (limpo.includes('/') || limpo.includes('\\') || limpo.includes('..')) return null;
|
|
128
|
-
|
|
172
|
+
// Um mês não é projeto: "2026-10" como id abriria a pasta do mês inteiro.
|
|
173
|
+
if (MES.test(limpo)) return null;
|
|
174
|
+
// O antigo, solto na casa (`2026-09-25-...`), continua valendo.
|
|
175
|
+
const solto = path.join(PROJETOS, limpo);
|
|
176
|
+
if (fs.existsSync(solto)) return solto;
|
|
177
|
+
for (const m of meses()) {
|
|
178
|
+
const dentro = path.join(PROJETOS, m, limpo);
|
|
179
|
+
if (fs.existsSync(dentro)) return dentro;
|
|
180
|
+
}
|
|
181
|
+
// Não existe ainda: o lugar de antes. Quem precisa que exista (`ler`)
|
|
182
|
+
// confere o projeto.json; este caminho só serve para dizer onde seria.
|
|
183
|
+
return solto;
|
|
129
184
|
}
|
|
130
185
|
|
|
131
186
|
/** O meta de um projeto, ou null. */
|
|
@@ -147,14 +202,13 @@ function salvar(id, mudancas) {
|
|
|
147
202
|
}
|
|
148
203
|
|
|
149
204
|
/** Todos, do mais novo para o mais velho. */
|
|
205
|
+
/* A ordem vinha do nome (a data na frente). Sem data no nome, ela vem do
|
|
206
|
+
`criado` do projeto.json — que todo projeto, velho ou novo, tem. */
|
|
150
207
|
function listar({ limite = 50 } = {}) {
|
|
151
208
|
garantirCasa();
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
|
|
156
|
-
} catch { return []; }
|
|
157
|
-
return nomes.slice(0, limite).map(ler).filter(Boolean);
|
|
209
|
+
const todos = idsTomados().map(ler).filter(Boolean);
|
|
210
|
+
todos.sort((a, b) => String(b.criado || '').localeCompare(String(a.criado || '')) || (a.id < b.id ? 1 : -1));
|
|
211
|
+
return todos.slice(0, limite);
|
|
158
212
|
}
|
|
159
213
|
|
|
160
214
|
// ── o caderno ────────────────────────────────────────────────────────────
|
|
@@ -216,7 +270,8 @@ function procurar(texto, { limite = 5 } = {}) {
|
|
|
216
270
|
return { projeto: p, pontos };
|
|
217
271
|
}).filter((x) => x.pontos > 0);
|
|
218
272
|
|
|
219
|
-
pontuados.sort((a, b) => b.pontos - a.pontos
|
|
273
|
+
pontuados.sort((a, b) => b.pontos - a.pontos
|
|
274
|
+
|| String(b.projeto.criado || '').localeCompare(String(a.projeto.criado || '')));
|
|
220
275
|
return pontuados.slice(0, limite).map((x) => x.projeto);
|
|
221
276
|
}
|
|
222
277
|
|
|
@@ -278,7 +333,7 @@ function caminhoDeMidia(id, nome) {
|
|
|
278
333
|
|
|
279
334
|
module.exports = {
|
|
280
335
|
CASA, PROJETOS,
|
|
281
|
-
criar, ler, salvar, listar, procurar, pastaDe, apelidar,
|
|
336
|
+
criar, ler, salvar, listar, procurar, pastaDe, apelidar, mesDe,
|
|
282
337
|
anotar, caderno,
|
|
283
338
|
lerPlano, salvarPlano, versoes, voltarPara,
|
|
284
339
|
caminhoDeMidia, escreverJSON, lerJSON,
|