primocode 10.4.7 → 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 +29 -5
- package/lib/comandos-extra.js +2 -0
- package/lib/config.js +32 -4
- package/lib/conta.js +9 -3
- package/lib/entrada.js +33 -11
- package/lib/estudio/colecao.js +218 -0
- package/lib/estudio/dados/fluent-3d.json +1 -0
- package/lib/estudio/dados/noto-animado.json +1 -0
- package/lib/estudio/diretor.js +1 -0
- package/lib/estudio/estilo.js +6 -2
- package/lib/estudio/falantes.js +30 -6
- package/lib/estudio/gerativo.js +8 -0
- package/lib/estudio/hibrido.js +144 -8
- package/lib/estudio/index.js +202 -83
- package/lib/estudio/kit/animacao.jsx +41 -0
- package/lib/estudio/kit/gerativo.jsx +21 -1
- package/lib/estudio/kit/index.jsx +3 -1
- package/lib/estudio/kit/mundo.jsx +1060 -0
- package/lib/estudio/kit/voxel.jsx +854 -0
- package/lib/estudio/kit.js +5 -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 +234 -10
- package/lib/estudio/producao.js +107 -17
- package/lib/estudio/projeto.js +77 -22
- package/lib/estudio/remotion.js +2 -0
- 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;
|
|
@@ -50,6 +52,26 @@ const CENARIOS = {
|
|
|
50
52
|
noite: { parede: '#120f1f', parede2: '#1a1530', chao: '#141218', acento: '#7b5cff', luz: '#c8b8ff', mesa: true, microfones: true, neon: 'ON' },
|
|
51
53
|
};
|
|
52
54
|
|
|
55
|
+
/* Os sets do VOXEL (kit/voxel.jsx, o padrão desde 11/10): o chão de
|
|
56
|
+
ladrilhos (duas cores alternadas), a parede escura, a cor das telas, o
|
|
57
|
+
acento (as luzinhas e o cartão de destaque), a luz principal e o recorte.
|
|
58
|
+
"telas" é o da referência do Joel: roxo, telas azuis e o laranja do Claude. */
|
|
59
|
+
const CENARIOS_VOXEL = {
|
|
60
|
+
telas: { parede: '#07061a', chao: '#4338ca', chao2: '#3b32b8', tela: '#101734', acento: '#ff7a45', luz: '#ffd2b0', recorte: '#8b7bff' },
|
|
61
|
+
neon: { parede: '#06030c', chao: '#6d28d9', chao2: '#5b21b6', tela: '#140b24', acento: '#22d3ee', luz: '#ffe1f5', recorte: '#ff4fd8' },
|
|
62
|
+
oceano: { parede: '#020c12', chao: '#0e7490', chao2: '#0b6079', tela: '#06202b', acento: '#f97316', luz: '#fff1dc', recorte: '#38bdf8' },
|
|
63
|
+
terra: { parede: '#120c08', chao: '#b45309', chao2: '#9a4708', tela: '#1c140e', acento: '#facc15', luz: '#fff4dc', recorte: '#fb923c' },
|
|
64
|
+
};
|
|
65
|
+
// O nome antigo do set (podcast, estúdio…) tem um par no voxel.
|
|
66
|
+
const VOXEL_DO_LISO = { podcast: 'telas', estudio: 'oceano', escritorio: 'terra', noite: 'neon' };
|
|
67
|
+
// O mascote é laranja (o do Claude) a menos que o roteiro diga a cor.
|
|
68
|
+
const CORES_MASCOTE = { claude: '#dd5a2b', gpt: '#10a37f', chatgpt: '#10a37f', gemini: '#4f7cff', primo: '#22b8d8', primocode: '#22b8d8' };
|
|
69
|
+
const ROUPAS_VOXEL = ['#c2452f', '#2f4cdd', '#1f8a5b', '#d08a1e', '#7b5cff', '#e8e2d6'];
|
|
70
|
+
// A pele de bloco é um tom mais fundo: sob a luz quente, o tom claro estourava.
|
|
71
|
+
const PELES_VOXEL = ['#d9a07a', '#c08660', '#a8704c', '#8a5a3c', '#6b4229'];
|
|
72
|
+
// O cabelo de bloco é escuro: o loiro sumia na pele de bloco (11/10).
|
|
73
|
+
const CABELOS_VOXEL = ['#1d1612', '#3b2618', '#2b2b30', '#4a2e1c', '#141016'];
|
|
74
|
+
|
|
53
75
|
/** Um número de 0 a 1 a partir de um texto: a mesma pessoa sai sempre igual. */
|
|
54
76
|
function hashDe(txt) {
|
|
55
77
|
let h = 2166136261;
|
|
@@ -64,12 +86,44 @@ function rng(semente) {
|
|
|
64
86
|
}
|
|
65
87
|
|
|
66
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
|
+
|
|
67
113
|
function aparencia(nome, pedido = {}) {
|
|
68
114
|
const k = (s) => hashDe(nome + ':' + s);
|
|
69
|
-
const tipo =
|
|
115
|
+
const tipo = tipoDe(nome, pedido);
|
|
70
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
|
+
}
|
|
71
125
|
if (tipo === 'robo') {
|
|
72
|
-
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 } : {}) };
|
|
73
127
|
}
|
|
74
128
|
return {
|
|
75
129
|
nome, rotulo, tipo,
|
|
@@ -79,6 +133,9 @@ function aparencia(nome, pedido = {}) {
|
|
|
79
133
|
roupa: pedido.roupa || pegar(ROUPAS, k('roupa')),
|
|
80
134
|
oculos: pedido.oculos != null ? !!pedido.oculos : k('oculos') < 0.3,
|
|
81
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' } : {}),
|
|
82
139
|
};
|
|
83
140
|
}
|
|
84
141
|
|
|
@@ -99,6 +156,22 @@ function destaqueDe(pt, texto) {
|
|
|
99
156
|
return m ? m[1].trim() : null;
|
|
100
157
|
}
|
|
101
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
|
+
|
|
102
175
|
const falaDe = (pt) => String(pt.texto || pt.fala || pt.narracao || pt.titulo || '').trim();
|
|
103
176
|
|
|
104
177
|
/* Sem os tempos da voz, cada fala ganha o tempo de leitura (2,6 palavras/s)
|
|
@@ -154,6 +227,68 @@ function planosDe(falas, { dois, duracao, aleatorio }) {
|
|
|
154
227
|
return planos;
|
|
155
228
|
}
|
|
156
229
|
|
|
230
|
+
/* OS CARTÕES DA PAREDE DE TELAS. Na referência, cada tela é um chat
|
|
231
|
+
trabalhando ("Leitura do app", "Testes", "Banco de dados"). Aqui os títulos
|
|
232
|
+
saem do ASSUNTO: o que o roteiro mandou (roteiro.paineis), o destaque de cada
|
|
233
|
+
fala e as palavras fortes — nunca texto inventado sem relação com o vídeo. */
|
|
234
|
+
const PALAVRA_FRACA = /^(que|com|para|pra|por|uma|um|uns|umas|não|nao|mas|mais|isso|esse|essa|este|esta|você|voce|eu|ele|ela|nós|nos|meu|minha|seu|sua|tudo|todo|toda|quando|como|onde|muito|muita|então|entao|aqui|agora|ainda|também|tambem|sobre|depois|antes|porque|qual|quem|cada|está|esta|estou|são|sao|foi|ser|ter|tem|vai|vou|faz|fazer|dele|dela|disso|nisso|desse|dessa|pelo|pela|seus|suas|isso)$/i;
|
|
235
|
+
// Corta no fim de uma palavra: "robô que não dá pa" é pior que "robô que não dá".
|
|
236
|
+
function cortarNaPalavra(txt, n) {
|
|
237
|
+
const t = String(txt).trim();
|
|
238
|
+
if (t.length <= n) return t;
|
|
239
|
+
const c = t.slice(0, n + 1).replace(/\s+\S*$/, '');
|
|
240
|
+
return (c || t.slice(0, n)).replace(/[,;:·-]+$/, '');
|
|
241
|
+
}
|
|
242
|
+
function paineisDe(falas, roteiro = {}) {
|
|
243
|
+
if (Array.isArray(roteiro.paineis) && roteiro.paineis.length) {
|
|
244
|
+
return roteiro.paineis.slice(0, 12).map((x) => (typeof x === 'string' ? { titulo: x.slice(0, 18), linha: '' } : { titulo: String(x.titulo || '').slice(0, 18), linha: String(x.linha || '').slice(0, 30) }));
|
|
245
|
+
}
|
|
246
|
+
const out = [];
|
|
247
|
+
const ja = new Set();
|
|
248
|
+
const por = (titulo, linha) => {
|
|
249
|
+
const tt = String(titulo || '').trim();
|
|
250
|
+
if (!tt || ja.has(tt.toLowerCase()) || out.length >= 8) return;
|
|
251
|
+
ja.add(tt.toLowerCase());
|
|
252
|
+
out.push({ titulo: cortarNaPalavra(tt, 18), linha: cortarNaPalavra(String(linha || ''), 30) });
|
|
253
|
+
};
|
|
254
|
+
for (const f of falas) if (f.destaque) por(f.destaque, f.texto);
|
|
255
|
+
for (const f of falas) {
|
|
256
|
+
const fortes = f.texto.split(/\s+/).map((w) => w.replace(/[^\p{L}\d%$-]/gu, '')).filter((w) => w.length >= 5 && !PALAVRA_FRACA.test(w));
|
|
257
|
+
if (fortes[0]) por(fortes[0].replace(/^./, (x) => x.toUpperCase()), f.texto);
|
|
258
|
+
}
|
|
259
|
+
return out.length ? out : [{ titulo: String(roteiro.titulo || 'Agora').slice(0, 18), linha: '' }];
|
|
260
|
+
}
|
|
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
|
+
|
|
157
292
|
/** A partitura inteira: o que o kit/personagem.jsx desenha, quadro a quadro. */
|
|
158
293
|
function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = [] }) {
|
|
159
294
|
const partes = (roteiro.partes || []).filter((p) => p && typeof p === 'object' && falaDe(p));
|
|
@@ -168,9 +303,20 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
|
|
|
168
303
|
if (!nomes.includes(f)) nomes.push(f);
|
|
169
304
|
}
|
|
170
305
|
const pedidos = roteiro.personagens && typeof roteiro.personagens === 'object' ? roteiro.personagens : {};
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 }));
|
|
174
320
|
const quemDe = (pt) => {
|
|
175
321
|
const f = typeof pt.falante === 'string' && pt.falante.trim() ? pt.falante.trim().toLowerCase() : 'apresentador';
|
|
176
322
|
return nomes.indexOf(f) % Math.max(1, elenco.length);
|
|
@@ -183,7 +329,7 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
|
|
|
183
329
|
.map((w) => ({ t: String(w.texto || w.word || ''), de: r2(w.de), ate: r2(w.ate) }));
|
|
184
330
|
return {
|
|
185
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),
|
|
186
|
-
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),
|
|
187
333
|
};
|
|
188
334
|
});
|
|
189
335
|
const duracao = r2((falas.length ? falas[falas.length - 1].ate : 3) + 1.2);
|
|
@@ -194,8 +340,85 @@ function partituraDe({ roteiro = {}, largura = 1920, altura = 1080, tempos = []
|
|
|
194
340
|
if (roteiro.acento && /^#[0-9a-f]{6}$/i.test(roteiro.acento)) cenario.acento = roteiro.acento;
|
|
195
341
|
|
|
196
342
|
const planos = planosDe(falas, { dois: elenco.length > 1, duracao, aleatorio });
|
|
343
|
+
|
|
344
|
+
/* O VISUAL. "voxel" (blocos, kit/voxel.jsx) é o padrão: é o 3D da
|
|
345
|
+
referência do Joel (11/10). "liso" é o boneco de esferas de antes, só
|
|
346
|
+
quando o roteiro pede pelo nome. */
|
|
347
|
+
const visual = String(roteiro.visual || '').toLowerCase() === 'liso' ? 'liso' : 'voxel';
|
|
348
|
+
let cenarioFinal = cenario;
|
|
349
|
+
if (visual === 'voxel') {
|
|
350
|
+
const pedido = String(roteiro.cenario || '').toLowerCase();
|
|
351
|
+
const nomeV = CENARIOS_VOXEL[pedido] ? pedido : VOXEL_DO_LISO[pedido] || 'telas';
|
|
352
|
+
cenarioFinal = { nome: nomeV, ...CENARIOS_VOXEL[nomeV] };
|
|
353
|
+
if (roteiro.acento && /^#[0-9a-f]{6}$/i.test(roteiro.acento)) cenarioFinal.acento = roteiro.acento;
|
|
354
|
+
for (const e of elenco) {
|
|
355
|
+
if (e.tipo === 'robo' && !(pedidoDe(e.nome) || {}).cor) e.cor = CORES_MASCOTE[e.nome] || '#dd5a2b';
|
|
356
|
+
if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).roupa) e.roupa = pegar(ROUPAS_VOXEL, hashDe(e.nome + ':roupa'));
|
|
357
|
+
if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).pele) e.pele = pegar(PELES_VOXEL, hashDe(e.nome + ':pele'));
|
|
358
|
+
if (e.tipo === 'pessoa' && !(pedidoDe(e.nome) || {}).corCabelo) e.corCabelo = pegar(CABELOS_VOXEL, hashDe(e.nome + ':corCabelo'));
|
|
359
|
+
}
|
|
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
|
+
|
|
197
419
|
return {
|
|
198
|
-
versao:
|
|
420
|
+
versao: 2, fps: FPS, largura, altura, duracao, semente, cenario: cenarioFinal, elenco, falas, planos, visual, mundos, figurantes,
|
|
421
|
+
paineis: paineisDe(falas, roteiro),
|
|
199
422
|
legenda: roteiro.legenda !== false,
|
|
200
423
|
titulo: typeof roteiro.titulo === 'string' ? roteiro.titulo.slice(0, 60) : '',
|
|
201
424
|
filme: { grao: roteiro.grao !== false, vinheta: true },
|
|
@@ -216,7 +439,7 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
|
|
|
216
439
|
const plano = {
|
|
217
440
|
formato: { largura, altura, fps: FPS },
|
|
218
441
|
fundoGeral: partitura.cenario.parede,
|
|
219
|
-
cenas: [{ duracao: DUR, fundo: partitura.cenario.parede, elementos: [{ tipo: 'kit', peca: 'personagem', partitura, x: 0, y: 0, largura, altura }] }],
|
|
442
|
+
cenas: [{ duracao: DUR, fundo: partitura.cenario.parede, elementos: [{ tipo: 'kit', peca: partitura.visual === 'liso' ? 'personagem' : 'voxel', partitura, x: 0, y: 0, largura, altura }] }],
|
|
220
443
|
audio: {
|
|
221
444
|
...(narracao ? { narracao } : {}),
|
|
222
445
|
...(trilha ? { trilha: { src: trilha, volumes, loop: true } } : {}),
|
|
@@ -225,8 +448,9 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
|
|
|
225
448
|
};
|
|
226
449
|
return {
|
|
227
450
|
ok: true, plano, partitura,
|
|
228
|
-
resumo: { 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 },
|
|
229
453
|
};
|
|
230
454
|
}
|
|
231
455
|
|
|
232
|
-
module.exports = { montarPersonagem, partituraDe, planosDe, aparencia, destaqueDe, emocaoDe, CENARIOS, 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/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,
|
package/lib/estudio/remotion.js
CHANGED
|
@@ -78,6 +78,8 @@ function pkgJson(nome) {
|
|
|
78
78
|
'@remotion/three': VERSAO_REMOTION,
|
|
79
79
|
three: '0.186.1',
|
|
80
80
|
'@react-three/fiber': '9.8.1',
|
|
81
|
+
// A animação pronta da biblioteca (kit/animacao.jsx): Lottie, tocado quadro a quadro.
|
|
82
|
+
'lottie-web': '5.13.0',
|
|
81
83
|
},
|
|
82
84
|
}, null, 2) + '\n';
|
|
83
85
|
}
|