primocode 10.0.0 → 10.1.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 +4 -4
- package/bin/primocode.js +88 -3
- package/lib/act.js +122 -20
- package/lib/browser.js +89 -5
- package/lib/catalogo.js +6 -3
- package/lib/claude-engine.js +38 -17
- package/lib/codex-engine.js +27 -9
- package/lib/config.js +10 -2
- package/lib/entrada.js +73 -9
- package/lib/estudio/assets.js +12 -6
- package/lib/estudio/biblioteca.js +174 -0
- package/lib/estudio/cinema.js +63 -25
- package/lib/estudio/composicao.js +2 -1
- package/lib/estudio/diretor.js +13 -1
- package/lib/estudio/edicao.js +2 -1
- package/lib/estudio/ende.js +16 -5
- package/lib/estudio/estilo.js +41 -10
- package/lib/estudio/fluido.js +283 -0
- package/lib/estudio/gerativo.js +1471 -0
- package/lib/estudio/hibrido.js +169 -0
- package/lib/estudio/impacto.js +66 -31
- package/lib/estudio/index.js +366 -47
- package/lib/estudio/kit/fluido.jsx +493 -0
- package/lib/estudio/kit/gerativo.jsx +990 -0
- package/lib/estudio/kit/index.jsx +4 -1
- package/lib/estudio/kit/tres.jsx +375 -0
- package/lib/estudio/kit.js +9 -1
- package/lib/estudio/kokoro.js +111 -0
- package/lib/estudio/lancamento.js +27 -0
- package/lib/estudio/legibilidade.js +20 -0
- package/lib/estudio/referencias.js +105 -0
- package/lib/estudio/remotion.js +103 -14
- package/lib/estudio/ritmo.js +2 -2
- package/lib/estudio/sons-leves.js +38 -1
- package/lib/estudio/variacao3d.js +46 -0
- package/lib/estudio/variedade.js +183 -0
- package/lib/estudio/vox.js +37 -8
- package/lib/mcp.js +156 -0
- package/lib/personas.js +67 -0
- package/lib/portao-skill.js +150 -0
- package/lib/skills.js +51 -1
- package/lib/tools.js +53 -8
- package/lib/ui.js +41 -6
- package/lib/video/auto.js +329 -0
- package/lib/video/camadas.js +207 -0
- package/lib/video/cli.js +1 -1
- package/lib/video/filters.js +39 -6
- package/lib/video/fx3d/Camadas.jsx +64 -0
- package/lib/video/fx3d/Objetos.jsx +128 -0
- package/lib/video/fx3d/Root.jsx +18 -0
- package/lib/video/fx3d/modelos.jsx +304 -0
- package/lib/video/fx3d.js +108 -0
- package/lib/video/gerados.js +119 -0
- package/lib/video/objetos.js +111 -0
- package/lib/video/ops-fx.js +453 -0
- package/lib/video/ops.js +30 -5
- package/lib/video/paralelo.js +50 -0
- package/lib/video/pedido.js +191 -0
- package/lib/video/rastrear.js +95 -0
- package/lib/video/rastro.js +276 -0
- package/lib/video/render.js +67 -14
- package/lib/video/texto.js +6 -4
- package/lib/video/timeline.js +9 -1
- package/package.json +2 -2
- package/skills/escrever-em-portugues/SKILL.md +3 -0
- package/skills/interface-revisada/SKILL.md +7 -1
- package/skills/motion-de-primeira/SKILL.md +5 -4
- package/skills/projeto-no-github/SKILL.md +6 -0
- package/skills/publicar-na-internet/SKILL.md +3 -0
- package/skills/site-com-motion/SKILL.md +2 -0
- package/studio/narrar.py +10 -7
- package/studio/rastrear.py +399 -0
- package/studio/voz_kokoro.py +98 -0
package/lib/claude-engine.js
CHANGED
|
@@ -575,6 +575,40 @@ function promptDoProtocolo(payload) {
|
|
|
575
575
|
+ '\n\nResponda com exatamente UM bloco marcado.';
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
+
/* O `claude -p` ASSÍNCRONO. Era execFileSync: enquanto o Claude pensava (um
|
|
579
|
+
minuto, dois), o PrimoCode inteiro parava — sem spinner, sem recado, sem
|
|
580
|
+
Esc Esc. E o teto de 180 s estourava justo quando ele escrevia um arquivo
|
|
581
|
+
grande, e o erro saía cru na tela ("Command failed: claude -p …"). Agora
|
|
582
|
+
roda em paralelo, com 10 min de teto, uma segunda tentativa, e o erro
|
|
583
|
+
diz o que o Claude disse. */
|
|
584
|
+
function rodarClaude(args, input, { timeout = 600000, tentativas = 2 } = {}) {
|
|
585
|
+
const { spawn } = require('child_process');
|
|
586
|
+
const umaVez = () => new Promise((resolve, reject) => {
|
|
587
|
+
const p = spawn('claude', args, { env: process.env, stdio: ['pipe', 'pipe', 'pipe'], ...(SEM_APARECER.windowsHide ? { windowsHide: true } : {}) });
|
|
588
|
+
let out = '', err = '';
|
|
589
|
+
const relogio = setTimeout(() => { try { p.kill(); } catch {} reject(new Error(`o Claude Code não respondeu em ${Math.round(timeout / 60000)} min`)); }, timeout);
|
|
590
|
+
p.stdout.on('data', (d) => { out += d; });
|
|
591
|
+
p.stderr.on('data', (d) => { err += d; });
|
|
592
|
+
p.on('error', (e) => { clearTimeout(relogio); reject(e); });
|
|
593
|
+
p.on('close', (code) => {
|
|
594
|
+
clearTimeout(relogio);
|
|
595
|
+
if (code === 0) return resolve(out);
|
|
596
|
+
// O JSON às vezes vem com is_error e o motivo, mesmo com código != 0.
|
|
597
|
+
let motivo = '';
|
|
598
|
+
try { const j = JSON.parse(out); motivo = j.result || j.error || ''; } catch { /* sem json */ }
|
|
599
|
+
reject(new Error(`o Claude Code saiu com ${code}: ${String(motivo || err || out).replace(/\s+/g, ' ').trim().slice(0, 300)}`));
|
|
600
|
+
});
|
|
601
|
+
p.stdin.end(input);
|
|
602
|
+
});
|
|
603
|
+
return (async () => {
|
|
604
|
+
let ultimo;
|
|
605
|
+
for (let i = 0; i < tentativas; i++) {
|
|
606
|
+
try { return await umaVez(); } catch (e) { ultimo = e; }
|
|
607
|
+
}
|
|
608
|
+
throw ultimo;
|
|
609
|
+
})();
|
|
610
|
+
}
|
|
611
|
+
|
|
578
612
|
async function requestActViaHeadless(payload, onEvent) {
|
|
579
613
|
const corpoBootstrap = corpoDoProtocolo();
|
|
580
614
|
const prompt = promptDoProtocolo(payload);
|
|
@@ -585,27 +619,14 @@ async function requestActViaHeadless(payload, onEvent) {
|
|
|
585
619
|
// O prompt vai por STDIN, nunca por argumento: argumento tem limite de
|
|
586
620
|
// tamanho (a conversa cresce) e qualquer conteúdo começando com traço
|
|
587
621
|
// seria lido como opção. stdin não tem nenhum dos dois problemas.
|
|
588
|
-
const saida =
|
|
589
|
-
'-p',
|
|
590
|
-
'--output-format', 'json',
|
|
591
|
-
'--dangerously-skip-permissions',
|
|
592
|
-
...argsDoMotor(),
|
|
593
|
-
], { encoding: 'utf-8', timeout: 180000, input: prompt, maxBuffer: 32 * 1024 * 1024,
|
|
594
|
-
env: process.env, ...SEM_APARECER });
|
|
622
|
+
const saida = await rodarClaude(['-p', '--output-format', 'json', '--dangerously-skip-permissions', ...argsDoMotor()], prompt);
|
|
595
623
|
const r = JSON.parse(saida);
|
|
596
624
|
// A correção é uma segunda ida ao motor, com o mesmo protocolo e o
|
|
597
625
|
// diagnóstico do que saiu errado. Barata: o prompt é curto, e evita que
|
|
598
626
|
// um erro de sintaxe do motor vire erro na cara do usuário.
|
|
599
|
-
const pedirCorrecao = (aviso) => {
|
|
600
|
-
const s =
|
|
601
|
-
|
|
602
|
-
...argsDoMotor(),
|
|
603
|
-
], {
|
|
604
|
-
encoding: 'utf-8', timeout: 120000, maxBuffer: 32 * 1024 * 1024,
|
|
605
|
-
env: process.env,
|
|
606
|
-
input: promptDeCorrecao(aviso),
|
|
607
|
-
...SEM_APARECER,
|
|
608
|
-
});
|
|
627
|
+
const pedirCorrecao = async (aviso) => {
|
|
628
|
+
const s = await rodarClaude(['-p', '--output-format', 'json', '--dangerously-skip-permissions', ...argsDoMotor()],
|
|
629
|
+
promptDeCorrecao(aviso), { timeout: 300000, tentativas: 1 });
|
|
609
630
|
return JSON.parse(s).result || '';
|
|
610
631
|
};
|
|
611
632
|
await emitirDecisao(r.result || '', onEvent, pedirCorrecao, 0, nomesDasTools(payload));
|
package/lib/codex-engine.js
CHANGED
|
@@ -131,22 +131,40 @@ function recusouOModelo(texto) {
|
|
|
131
131
|
* `--model` e avisa — cair para o padrão da conta é melhor que a tarefa
|
|
132
132
|
* morrer por causa de um nome que mudou de catálogo.
|
|
133
133
|
*/
|
|
134
|
-
|
|
134
|
+
/* O Codex ASSÍNCRONO, pelo mesmo motivo do claude -p (lib/claude-engine.js):
|
|
135
|
+
o execFileSync congelava o PrimoCode inteiro — spinner, recado, Esc Esc —
|
|
136
|
+
enquanto o Codex pensava. O erro mantém stdout/stderr para o diagnóstico. */
|
|
137
|
+
function rodarAssincrono(args, entrada) {
|
|
138
|
+
const { spawn } = require('child_process');
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const p = spawn(CODEX, args, { env: process.env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
141
|
+
let out = '', err = '';
|
|
142
|
+
const relogio = setTimeout(() => { try { p.kill(); } catch {} const e = new Error(`o Codex não respondeu em ${Math.round(TEMPO / 60000)} min`); e.stderr = err; e.stdout = out; reject(e); }, TEMPO);
|
|
143
|
+
p.stdout.on('data', (d) => { out += d; if (out.length > BUFFER) { try { p.kill(); } catch {} } });
|
|
144
|
+
p.stderr.on('data', (d) => { err += d; });
|
|
145
|
+
p.on('error', (e) => { clearTimeout(relogio); reject(e); });
|
|
146
|
+
p.on('close', (code) => {
|
|
147
|
+
clearTimeout(relogio);
|
|
148
|
+
if (code === 0) return resolve(out);
|
|
149
|
+
const e = new Error(`Command failed: codex (saiu com ${code})`); e.stderr = err; e.stdout = out; reject(e);
|
|
150
|
+
});
|
|
151
|
+
p.stdin.end(entrada);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function chamarCodex(prompt, { executor, avisar = () => {} } = {}) {
|
|
135
156
|
// A chave própria da pessoa, quando ela configurou uma. Sem configuração
|
|
136
157
|
// o ambiente vai INTACTO — passar a variável vazia apagaria a chave que
|
|
137
158
|
// ela já tem exportada no shell, e o Codex pararia de funcionar por ter
|
|
138
159
|
// sido "configurado".
|
|
139
|
-
const rodar = executor ||
|
|
140
|
-
encoding: 'utf-8', timeout: TEMPO, input: entrada, maxBuffer: BUFFER,
|
|
141
|
-
env: process.env, ...SEM_APARECER,
|
|
142
|
-
}));
|
|
160
|
+
const rodar = executor || rodarAssincrono;
|
|
143
161
|
try {
|
|
144
|
-
return rodar(argsDoExec(), prompt);
|
|
162
|
+
return await rodar(argsDoExec(), prompt);
|
|
145
163
|
} catch (e) {
|
|
146
164
|
const detalhe = `${e.message || ''}\n${(e.stderr || '')}\n${(e.stdout || '')}`;
|
|
147
165
|
if (MODELO_CODEX && recusouOModelo(detalhe)) {
|
|
148
166
|
avisar(`o Codex não aceitou o modelo ${MODELO_CODEX} — indo com o padrão da sua conta`);
|
|
149
|
-
return rodar(argsDoExec({ comModelo: false }), prompt);
|
|
167
|
+
return await rodar(argsDoExec({ comModelo: false }), prompt);
|
|
150
168
|
}
|
|
151
169
|
throw new Error(primeiraLinhaUtil(detalhe) || e.message);
|
|
152
170
|
}
|
|
@@ -185,12 +203,12 @@ async function requestAct(server, payload, onEvent) {
|
|
|
185
203
|
return;
|
|
186
204
|
}
|
|
187
205
|
const prompt = claude.promptDoProtocolo(payload);
|
|
188
|
-
const saida = chamarCodex(prompt, { avisar: (m) => onEvent({ aviso: m }) });
|
|
206
|
+
const saida = await chamarCodex(prompt, { avisar: (m) => onEvent({ aviso: m }) });
|
|
189
207
|
|
|
190
208
|
// A correção é uma segunda ida ao motor, com o mesmo protocolo e o
|
|
191
209
|
// diagnóstico do que saiu errado. Barata, e evita que um erro de forma
|
|
192
210
|
// do motor vire erro na cara de quem pediu a tarefa.
|
|
193
|
-
const pedirCorrecao = (aviso) => chamarCodex(claude.promptDeCorrecao(aviso));
|
|
211
|
+
const pedirCorrecao = (aviso) => chamarCodex(claude.promptDeCorrecao(aviso)); // devolve promessa: quem chama dá await
|
|
194
212
|
|
|
195
213
|
await claude.emitirDecisao(saida, onEvent, pedirCorrecao, 0,
|
|
196
214
|
claude.nomesDasTools(payload));
|
package/lib/config.js
CHANGED
|
@@ -273,6 +273,14 @@ function podarMemoriaGeral() {
|
|
|
273
273
|
} catch {}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
/* O servidor saiu do Render pro Railway da Paris Group (05/10). Quem gravou o
|
|
277
|
+
endereço antigo no config.json vai pro novo: o Render vai ser desligado. */
|
|
278
|
+
const SERVIDOR = 'https://primocode-server-production.up.railway.app';
|
|
279
|
+
const SERVIDOR_ANTIGO = /^https?:\/\/primocode-server\.onrender\.com\/?$/;
|
|
280
|
+
function servidor(escolhido) {
|
|
281
|
+
return !escolhido || SERVIDOR_ANTIGO.test(String(escolhido).trim()) ? SERVIDOR : escolhido;
|
|
282
|
+
}
|
|
283
|
+
|
|
276
284
|
function loadConfig() {
|
|
277
285
|
ensureStructure();
|
|
278
286
|
let file = {};
|
|
@@ -280,7 +288,7 @@ function loadConfig() {
|
|
|
280
288
|
if (fs.existsSync(CONFIG_FILE)) file = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
281
289
|
} catch { file = {}; }
|
|
282
290
|
return {
|
|
283
|
-
server: process.env.PRIMOCODE_SERVER || file.server
|
|
291
|
+
server: servidor(process.env.PRIMOCODE_SERVER || file.server),
|
|
284
292
|
model: process.env.PRIMOCODE_MODEL || file.model || 'main',
|
|
285
293
|
// Esforço, estilo Claude Code (/effort). É o que o usuário escolhe; o
|
|
286
294
|
// modelo é derivado dele. Guardado para durar entre sessões.
|
|
@@ -363,7 +371,7 @@ function esquecerProjeto() {
|
|
|
363
371
|
module.exports = {
|
|
364
372
|
tokensDoAssunto, GENERICAS,
|
|
365
373
|
lembrarProjeto, projetoLembrado, esquecerProjeto, PROJETO_ATIVO_FILE,
|
|
366
|
-
loadConfig, saveConfig, appendHistory,
|
|
374
|
+
loadConfig, saveConfig, appendHistory, servidor, SERVIDOR,
|
|
367
375
|
CONFIG_DIR, CONFIG_FILE, HISTORY_FILE, ROOT_DIR, GENERAL_MEMORY,
|
|
368
376
|
ensureStructure, slugify, stripAccents,
|
|
369
377
|
createProjectDir, getProjectDir, listProjects, findSimilarProject,
|
package/lib/entrada.js
CHANGED
|
@@ -43,6 +43,10 @@ const { c, visibleWidth, truncate } = require('./ui.js');
|
|
|
43
43
|
|
|
44
44
|
// ── O desenho ────────────────────────────────────────────────────────────
|
|
45
45
|
|
|
46
|
+
/** A etiqueta de colagem em destaque (fininha, na cor da marca), sem mudar a largura. */
|
|
47
|
+
const RE_ETIQUETA = /\[Colado #\d+ · [^\]]+\]/g;
|
|
48
|
+
function pintarEtiquetas(s) { return String(s).replace(RE_ETIQUETA, (m) => c.brand(m) + '\x1b[38;5;254m'); }
|
|
49
|
+
|
|
46
50
|
const BORDA = { ne: '╭', no: '╮', se: '╰', so: '╯', h: '─', v: '│' };
|
|
47
51
|
|
|
48
52
|
/** Preenche até `w` colunas VISÍVEIS (códigos de cor não ocupam coluna). */
|
|
@@ -128,6 +132,12 @@ function montar(e) {
|
|
|
128
132
|
|
|
129
133
|
const out = [];
|
|
130
134
|
const regua = (bash ? c.warn : c.dim)(BORDA.h.repeat(largura));
|
|
135
|
+
/* O SPINNER MORA AQUI, na linha logo acima da caixa. Ele escrevia direto
|
|
136
|
+
no terminal com "\r" a cada 80 ms, sem quebra de linha — e a caixa,
|
|
137
|
+
que espera a linha fechar para se redesenhar, ficava apagada enquanto
|
|
138
|
+
ele girasse: o que se digitava não aparecia, e a barra de status
|
|
139
|
+
sumia ("não consigo digitar enquanto ele trabalha"). */
|
|
140
|
+
if (e.spinner) out.push(' ' + truncate(e.spinner, largura - 2));
|
|
131
141
|
out.push(regua);
|
|
132
142
|
|
|
133
143
|
const vazio = !String(e.texto).length;
|
|
@@ -137,7 +147,7 @@ function montar(e) {
|
|
|
137
147
|
: c.dim('┆ ');
|
|
138
148
|
// O "!" some do texto desenhado: quem vê "$ " já sabe que é shell, e
|
|
139
149
|
// mostrar os dois é mostrar a mesma informação duas vezes.
|
|
140
|
-
let conteudo = c.white(i === 0 && bash ? l.texto.replace(/^\s*!/, '') : l.texto);
|
|
150
|
+
let conteudo = pintarEtiquetas(c.white(i === 0 && bash ? l.texto.replace(/^\s*!/, '') : l.texto));
|
|
141
151
|
// O fantasma só existe na última linha e só com o cursor no fim dela:
|
|
142
152
|
// desenhado no meio do texto, ele empurraria o que vem depois.
|
|
143
153
|
if (i === linhas.length - 1 && e.fantasma && cursor === String(e.texto).length) {
|
|
@@ -326,6 +336,15 @@ class Entrada extends EventEmitter {
|
|
|
326
336
|
|
|
327
337
|
prompt() { if (!this.suspenso && this.tty && !this.fechado) this._desenhar(); }
|
|
328
338
|
|
|
339
|
+
/** O spinner (lib/ui.js) desenha por aqui quando há caixa: uma linha
|
|
340
|
+
* acima dela, redesenhada junto — nunca um "\r" solto por cima. */
|
|
341
|
+
mostrarSpinner(texto) {
|
|
342
|
+
const novo = texto || '';
|
|
343
|
+
if (novo === (this.spinnerTexto || '')) return;
|
|
344
|
+
this.spinnerTexto = novo;
|
|
345
|
+
if (this.desenhado && !this._meiaLinha) this._redesenhar();
|
|
346
|
+
}
|
|
347
|
+
|
|
329
348
|
/** Escreve texto ACIMA da caixa — é por onde todo o resto do programa fala. */
|
|
330
349
|
escreverAcima(texto) {
|
|
331
350
|
if (!this.desenhado) { this.escreverCru(texto); return; }
|
|
@@ -342,7 +361,40 @@ class Entrada extends EventEmitter {
|
|
|
342
361
|
this._mudou();
|
|
343
362
|
}
|
|
344
363
|
|
|
345
|
-
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this._mudou(); }
|
|
364
|
+
limpar() { this.line = ''; this.cursor = 0; this.menu = null; this.colados = new Map(); this._mudou(); }
|
|
365
|
+
|
|
366
|
+
/* COLAR VIRA ETIQUETA, como no Claude Code: texto grande colado (várias
|
|
367
|
+
linhas, ou mais de 300 caracteres) entra na caixa como
|
|
368
|
+
"[Colado #1 · 42 linhas]" — a caixa não vira um paredão, dá para
|
|
369
|
+
escrever o pedido em volta, e no Enter a etiqueta volta a ser o texto
|
|
370
|
+
inteiro. O que é curto entra como texto mesmo. */
|
|
371
|
+
colar(texto) {
|
|
372
|
+
const t = String(texto || '');
|
|
373
|
+
if (!t) return;
|
|
374
|
+
const linhas = t.split('\n').length;
|
|
375
|
+
if (linhas < 3 && t.length <= 300) return this.write(t);
|
|
376
|
+
if (!this.colados) this.colados = new Map();
|
|
377
|
+
const n = this.colados.size + 1;
|
|
378
|
+
const etiqueta = `[Colado #${n} · ${linhas > 1 ? `${linhas} linhas` : `${t.length} caracteres`}]`;
|
|
379
|
+
this.colados.set(etiqueta, t);
|
|
380
|
+
this.write(etiqueta);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** A linha com cada etiqueta trocada pelo texto que ela guarda. */
|
|
384
|
+
expandir(linha) {
|
|
385
|
+
if (!this.colados || !this.colados.size) return linha;
|
|
386
|
+
let out = linha;
|
|
387
|
+
for (const [et, t] of this.colados) out = out.split(et).join(t);
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** A etiqueta que termina exatamente no cursor (o backspace apaga ela inteira). */
|
|
392
|
+
_etiquetaAntes() {
|
|
393
|
+
if (!this.colados) return null;
|
|
394
|
+
const antes = this.line.slice(0, this.cursor);
|
|
395
|
+
for (const et of this.colados.keys()) if (antes.endsWith(et)) return et;
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
346
398
|
|
|
347
399
|
/**
|
|
348
400
|
* A PRÓXIMA linha é senha: a caixa desenha `•` no lugar das letras, não
|
|
@@ -363,6 +415,7 @@ class Entrada extends EventEmitter {
|
|
|
363
415
|
status: this.op.status ? this.op.status() : [],
|
|
364
416
|
menu: this.menu,
|
|
365
417
|
ocupado: this.ocupado,
|
|
418
|
+
spinner: this.spinnerTexto || '',
|
|
366
419
|
});
|
|
367
420
|
}
|
|
368
421
|
|
|
@@ -436,11 +489,11 @@ class Entrada extends EventEmitter {
|
|
|
436
489
|
const seq = (key && key.sequence) || ch || '';
|
|
437
490
|
|
|
438
491
|
// Colagem entre marcas: tudo que vier até a marca de fim é texto.
|
|
439
|
-
if (seq.includes('\x1b[200~')) { this.colando = true; }
|
|
492
|
+
if (seq.includes('\x1b[200~')) { this.colando = true; this.bufColagem = ''; }
|
|
440
493
|
if (this.colando) {
|
|
441
494
|
const limpo = seq.replace('\x1b[200~', '').replace('\x1b[201~', '');
|
|
442
|
-
if (limpo) this.
|
|
443
|
-
if (seq.includes('\x1b[201~')) this.colando = false;
|
|
495
|
+
if (limpo) this.bufColagem += limpo.replace(/\r\n?/g, '\n');
|
|
496
|
+
if (seq.includes('\x1b[201~')) { this.colando = false; this.colar(this.bufColagem); this.bufColagem = ''; }
|
|
444
497
|
return;
|
|
445
498
|
}
|
|
446
499
|
|
|
@@ -478,7 +531,15 @@ class Entrada extends EventEmitter {
|
|
|
478
531
|
return;
|
|
479
532
|
}
|
|
480
533
|
|
|
481
|
-
case 'backspace':
|
|
534
|
+
case 'backspace': {
|
|
535
|
+
const et = this._etiquetaAntes();
|
|
536
|
+
if (et) {
|
|
537
|
+
this.line = this.line.slice(0, this.cursor - et.length) + this.line.slice(this.cursor);
|
|
538
|
+
this.cursor -= et.length;
|
|
539
|
+
this.colados.delete(et);
|
|
540
|
+
return this._mudou();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
482
543
|
if (this.cursor > 0) {
|
|
483
544
|
this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
|
|
484
545
|
this.cursor--;
|
|
@@ -585,7 +646,9 @@ class Entrada extends EventEmitter {
|
|
|
585
646
|
|
|
586
647
|
_enviar() {
|
|
587
648
|
if (this.menu) return this._escolherDoMenu();
|
|
588
|
-
const
|
|
649
|
+
const visto = this.line;
|
|
650
|
+
const texto = this.expandir(visto);
|
|
651
|
+
this.colados = new Map();
|
|
589
652
|
const oculto = this.oculto;
|
|
590
653
|
this.oculto = false;
|
|
591
654
|
this.line = '';
|
|
@@ -599,7 +662,8 @@ class Entrada extends EventEmitter {
|
|
|
599
662
|
this._apagar();
|
|
600
663
|
if (texto.trim() && !oculto) {
|
|
601
664
|
const marca = c.brand('❯ ');
|
|
602
|
-
|
|
665
|
+
// Na tela fica a etiqueta (o que se viu ao mandar); quem vai ao agente é o texto inteiro.
|
|
666
|
+
this.escreverCru(visto.split('\n').map((l, i) => (i ? ' ' : marca) + pintarEtiquetas(c.white(l))).join('\n') + '\n');
|
|
603
667
|
if (this.historico[this.historico.length - 1] !== texto) this.historico.push(texto);
|
|
604
668
|
}
|
|
605
669
|
this._desenhar();
|
|
@@ -609,4 +673,4 @@ class Entrada extends EventEmitter {
|
|
|
609
673
|
|
|
610
674
|
function criar(op) { return new Entrada(op).iniciar(); }
|
|
611
675
|
|
|
612
|
-
module.exports = { criar, Entrada, montar, quebrar, encher };
|
|
676
|
+
module.exports = { criar, Entrada, montar, quebrar, encher, pintarEtiquetas };
|
package/lib/estudio/assets.js
CHANGED
|
@@ -329,12 +329,18 @@ async function procurarMusica(termo, { quantas = 8, duracaoMin = 45 } = {}) {
|
|
|
329
329
|
return procurarSom(termo, { quantas, duracaoMin });
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
-
|
|
332
|
+
/* `livre`: só domínio público (CC0 e PDM). O efeito sonoro entra no vídeo sem
|
|
333
|
+
crédito, então não pode ser CC-BY, que o "commercial" deixa passar. */
|
|
334
|
+
function urlSom(termo, { quantas = 8, duracaoMin = 0, livre = false } = {}) {
|
|
335
|
+
let url = `https://api.openverse.org/v1/audio/?q=${encodeURIComponent(termo)}`
|
|
336
|
+
+ `&page_size=${quantas}` + (livre ? '&license=cc0,pdm' : '&license_type=commercial');
|
|
337
|
+
if (duracaoMin && duracaoMin < 30) url += '&length=short';
|
|
338
|
+
return url;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function procurarSom(termo, { quantas = 8, duracaoMin = 0, duracaoMax = 0, livre = false } = {}) {
|
|
333
342
|
try {
|
|
334
|
-
|
|
335
|
-
+ `&page_size=${quantas}&license_type=commercial`;
|
|
336
|
-
if (duracaoMin && duracaoMin < 30) url += '&length=short';
|
|
337
|
-
const r = await pegar(url, { json: true });
|
|
343
|
+
const r = await pegar(urlSom(termo, { quantas, duracaoMin, livre }), { json: true });
|
|
338
344
|
const sons = (r.results || [])
|
|
339
345
|
.filter((x) => !duracaoMax || !x.duration || x.duration / 1000 <= duracaoMax)
|
|
340
346
|
.filter((x) => !duracaoMin || !x.duration || x.duration / 1000 >= duracaoMin)
|
|
@@ -412,6 +418,6 @@ module.exports = {
|
|
|
412
418
|
extensaoPorConteudo,
|
|
413
419
|
pegar, guardar,
|
|
414
420
|
icone, procurarIcones, iconesDoPlano, COLECOES, procurarVideos, procurarMusica,
|
|
415
|
-
procurarImagens, procurarSom, baixar,
|
|
421
|
+
procurarImagens, procurarSom, urlSom, baixar,
|
|
416
422
|
FONTES, conferirFonte,
|
|
417
423
|
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* biblioteca.js — o cardápio de fonte e cor dos estilos de corte (híbrido,
|
|
3
|
+
* cinema, impacto), com memória do que os últimos vídeos usaram.
|
|
4
|
+
*
|
|
5
|
+
* "por falta de variedade a gente tá fazendo os vídeos todos iguais" (Joel, 02/10)
|
|
6
|
+
*
|
|
7
|
+
* Sem fonte e cor da marca, o cinema caía sempre no Inter e no gradiente
|
|
8
|
+
* azul e roxo (#2E81ED → #9567DB), e o híbrido é o padrão: todo vídeo saía
|
|
9
|
+
* com a mesma cara. Aqui a escolha segue esta ordem:
|
|
10
|
+
* 1. a marca (fontes e cores dela) — nunca é trocada;
|
|
11
|
+
* 2. o que a IA escolheu do cardápio (roteiro.visual);
|
|
12
|
+
* 3. um sorteio que pula o que os últimos vídeos já usaram.
|
|
13
|
+
*
|
|
14
|
+
* As 128 famílias foram conferidas no Google Fonts em 02/10, com o peso mais
|
|
15
|
+
* grosso de cada uma (o título do cinema é caixa alta em 800).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// ── as fontes: "Nome:peso máximo", por tipo ──────────────────────────────
|
|
25
|
+
const TIPOS = {
|
|
26
|
+
condensada: 'Anton:400, Antonio:700, Archivo Narrow:700, Barlow Condensed:900, Bebas Neue:400, Big Shoulders Display:900, '
|
|
27
|
+
+ 'Encode Sans Condensed:900, Fjalla One:400, Khand:700, League Gothic:400, Mohave:700, Oswald:700, Pathway Gothic One:400, '
|
|
28
|
+
+ 'Saira Condensed:900, Six Caps:400, Smooch Sans:900, Sofia Sans Condensed:900, Staatliches:400, Teko:700',
|
|
29
|
+
display: 'Alfa Slab One:400, Anybody:900, Archivo Black:400, Archivo:900, Black Ops One:400, Bricolage Grotesque:800, '
|
|
30
|
+
+ 'Chakra Petch:700, Chivo:900, Darker Grotesque:900, Epilogue:900, Exo 2:900, Familjen Grotesk:700, Funnel Display:800, '
|
|
31
|
+
+ 'Gabarito:900, Lexend:900, Mona Sans:900, Passion One:900, Red Hat Display:900, Righteous:400, Rubik:900, Russo One:400, '
|
|
32
|
+
+ 'Schibsted Grotesk:900, Shrikhand:400, Urbanist:900, Wix Madefor Display:800',
|
|
33
|
+
larga: 'Audiowide:400, Bagel Fat One:400, Bowlby One:400, Bungee Shade:400, Bungee:400, Dela Gothic One:400, Krona One:400, '
|
|
34
|
+
+ 'Lexend Zetta:900, Michroma:400, Monoton:400, Orbitron:900, Press Start 2P:400, Rubik Mono One:400, Silkscreen:700, Syne:800, Ultra:400',
|
|
35
|
+
texto: 'Afacad:700, Albert Sans:900, Barlow:900, Be Vietnam Pro:900, Figtree:900, Funnel Sans:800, Golos Text:900, '
|
|
36
|
+
+ 'Hanken Grotesk:900, Host Grotesk:800, Instrument Sans:700, Karla:800, Onest:900, Public Sans:900, Reddit Sans:900, '
|
|
37
|
+
+ 'Rethink Sans:800, Sofia Sans:900, Work Sans:900',
|
|
38
|
+
serifa: 'Abril Fatface:400, Arvo:700, Bitter:900, Bodoni Moda:900, Caprasimo:400, Cormorant Garamond:700, Crimson Pro:900, '
|
|
39
|
+
+ 'DM Serif Display:400, EB Garamond:800, Fraunces:900, Gloock:400, Instrument Serif:400, Libre Baskerville:700, '
|
|
40
|
+
+ 'Libre Bodoni:700, Libre Caslon Display:400, Literata:900, Lora:700, Merriweather:900, Newsreader:800, Playfair Display:900, '
|
|
41
|
+
+ 'Prata:400, Roboto Slab:900, Rozha One:400, Source Serif 4:900, Spectral:800, Yeseva One:400, Young Serif:400, Zilla Slab:700',
|
|
42
|
+
manuscrita: 'Caveat:700, Gloria Hallelujah:400, Kalam:700, Lobster:400, Marck Script:400, Nanum Pen Script:400, Pacifico:400, '
|
|
43
|
+
+ 'Permanent Marker:400, Rock Salt:400, Satisfy:400, Shadows Into Light:400, Yellowtail:400',
|
|
44
|
+
mono: 'Azeret Mono:900, DM Mono:400, Fragment Mono:400, Geist Mono:900, IBM Plex Mono:700, JetBrains Mono:800, '
|
|
45
|
+
+ 'Major Mono Display:400, Martian Mono:800, Red Hat Mono:700, Space Mono:700, VT323:400',
|
|
46
|
+
};
|
|
47
|
+
const FONTES = Object.entries(TIPOS).flatMap(([tipo, lista]) => lista.split(', ').map((x) => {
|
|
48
|
+
const [nome, peso] = x.split(':');
|
|
49
|
+
return { nome, tipo, peso: Number(peso) };
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
/* A largura média do caractere, em "em", por tipo — o caber() de
|
|
53
|
+
composicao.js usa para escolher o tamanho. A larga é a que estoura a borda. */
|
|
54
|
+
const LARGURA = { condensada: 0.46, display: 0.6, larga: 0.74, texto: 0.58, serifa: 0.55, manuscrita: 0.52, mono: 0.62 };
|
|
55
|
+
const larguraDe = (fonte) => { const f = acharFonte(fonte); return f ? LARGURA[f.tipo] : null; };
|
|
56
|
+
|
|
57
|
+
/* O título é caixa alta grossa: fonte de texto fina demais não serve. O
|
|
58
|
+
texto (etiquetas, nomes) é qualquer uma que se lê pequena. */
|
|
59
|
+
const TITULO = FONTES.filter((f) => ['condensada', 'display', 'larga'].includes(f.tipo) || (f.tipo === 'serifa' && f.peso >= 800)
|
|
60
|
+
|| (f.tipo === 'texto' && f.peso >= 900));
|
|
61
|
+
const TEXTO = FONTES.filter((f) => ['texto', 'mono'].includes(f.tipo) || (f.tipo === 'serifa' && f.peso >= 700) || (f.tipo === 'display' && f.peso >= 800));
|
|
62
|
+
|
|
63
|
+
// ── as paletas: fundo escuro e as duas cores do gradiente ────────────────
|
|
64
|
+
/* O cinema é escuro (o texto é branco), então todo fundo é escuro. Nada do
|
|
65
|
+
violeta de SaaS nem do neon: é o que denuncia vídeo feito por IA. */
|
|
66
|
+
const BASES = [
|
|
67
|
+
['brasa', '#120B08', '#FF5A1F', '#FFB23F'], ['ferrugem', '#15100D', '#C8553D', '#F2A65A'], ['mata', '#0B1410', '#2BB673', '#D8F05A'],
|
|
68
|
+
['oceano', '#071219', '#1E88E5', '#4FD1C5'], ['tijolo', '#170C0B', '#E2583E', '#F6D6AD'], ['limao', '#0E100A', '#C6F432', '#F4F1E8'],
|
|
69
|
+
['cobre', '#120E0C', '#D9822B', '#F3C98B'], ['sangue', '#100607', '#E5383B', '#FF9F1C'], ['musgo', '#0F120C', '#8DB255', '#E9C46A'],
|
|
70
|
+
['gelo', '#0A0F14', '#8EC5FF', '#F1F7FF'], ['sol-de-inverno', '#0D0D10', '#FFC93C', '#FF6B35'], ['cereja', '#14080B', '#FF4D6D', '#FFC2D1'],
|
|
71
|
+
['jade', '#06120F', '#00A878', '#9BE564'], ['areia', '#13110D', '#E9C46A', '#F4A261'], ['safira', '#070B16', '#2F6BFF', '#7FB2FF'],
|
|
72
|
+
['tangerina', '#110A06', '#FF7A00', '#FFD166'], ['pistache', '#0C110D', '#93C572', '#F0EAD6'], ['vinho', '#13070C', '#D1495B', '#FCB9B2'],
|
|
73
|
+
['petroleo', '#061214', '#0F9D9A', '#F2C14E'], ['ouro-velho', '#0F0D08', '#C9A227', '#F5E6B3'], ['coral', '#120A0A', '#FF6F59', '#FFD6A5'],
|
|
74
|
+
['asfalto', '#0E0F10', '#FF3B30', '#FFFFFF'], ['menta', '#08110E', '#3DDC97', '#E8FFF4'], ['lava', '#0F0705', '#FF3D00', '#FFB300'],
|
|
75
|
+
['ceu', '#08101A', '#56C3E8', '#F7F7F7'], ['terracota', '#140D0A', '#CB6843', '#EBD4B4'], ['abacate', '#0D100A', '#A3B18A', '#DAD7CD'],
|
|
76
|
+
['neve-e-fogo', '#0B0B0E', '#FFFFFF', '#FF4F00'], ['framboesa', '#12070D', '#E0245E', '#FFB3C6'], ['cacau', '#120D0A', '#A47148', '#F2D0A9'],
|
|
77
|
+
['esmeralda', '#04110B', '#10B981', '#FDE68A'], ['aco', '#0C0E11', '#9DB4C0', '#E0E1DD'], ['acafrao', '#110D05', '#F4B400', '#FF6D00'],
|
|
78
|
+
['lagoa', '#051214', '#48CAE4', '#CAF0F8'], ['papoula', '#110808', '#F94144', '#F9C74F'], ['oliva', '#0F0F09', '#B5A642', '#F1E9C9'],
|
|
79
|
+
['ardosia', '#0D1014', '#FF8C42', '#E8EEF2'], ['tinta-e-rosa', '#0E0B0C', '#FF8FAB', '#FFE5EC'], ['hortela-e-ouro', '#08120F', '#52B788', '#F6C453'],
|
|
80
|
+
['noite-azul', '#060A12', '#3A86FF', '#FFBE0B'], ['cinzas', '#101010', '#D9D9D9', '#FF5F1F'], ['pessego', '#120C0A', '#FFAD87', '#FFE3D3'],
|
|
81
|
+
['floresta', '#07100A', '#40916C', '#B7E4C7'], ['rubi', '#10060A', '#EF233C', '#EDF2F4'], ['mostarda', '#100E06', '#E1AD01', '#F6F1D1'],
|
|
82
|
+
['turquesa', '#061111', '#2EC4B6', '#FFBF69'], ['caramelo', '#120C07', '#DDA15E', '#FEFAE0'], ['grafite-limao', '#0F1011', '#D4FF3A', '#F2F2F2'],
|
|
83
|
+
['alvorada', '#0D0A0C', '#FF9E00', '#FF4D6D'], ['tempestade', '#0A0D10', '#6C9BD2', '#F2E8CF'],
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
function rgb(hex) { const n = parseInt(String(hex).slice(1), 16); return [n >> 16 & 255, n >> 8 & 255, n & 255]; }
|
|
87
|
+
const hex = (c) => '#' + c.map((v) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0')).join('').toUpperCase();
|
|
88
|
+
const misturar = (a, b, k) => hex(rgb(a).map((v, i) => v + (rgb(b)[i] - v) * k));
|
|
89
|
+
const luz = (c) => { const [r, g, b] = rgb(c); return (0.299 * r + 0.587 * g + 0.114 * b) / 255; };
|
|
90
|
+
|
|
91
|
+
/* Cada base vira quatro: como veio, o gradiente invertido, o fundo tingido
|
|
92
|
+
da cor principal e a monocromática (a cor e ela mais clara). */
|
|
93
|
+
const PALETAS = BASES.flatMap(([nome, fundo, a, b]) => [
|
|
94
|
+
{ nome, base: nome, fundo, grad: [a, b] },
|
|
95
|
+
{ nome: `${nome}-invertida`, base: nome, fundo, grad: [b, a] },
|
|
96
|
+
{ nome: `${nome}-tingida`, base: nome, fundo: misturar(fundo, a, 0.1), grad: [a, b] },
|
|
97
|
+
{ nome: `${nome}-mono`, base: nome, fundo, grad: [a, misturar(a, '#FFFFFF', 0.45)] },
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
// ── a memória: o que os últimos vídeos já usaram ─────────────────────────
|
|
101
|
+
const ARQ = path.join(os.homedir(), '.primocode', 'studio', 'biblioteca-usada.json');
|
|
102
|
+
const LEMBRA = { titulo: 15, texto: 8, paleta: 20 }; // paleta conta pela base: "brasa-mono" depois de "brasa" é repetir
|
|
103
|
+
|
|
104
|
+
function lerMemoria(arq = ARQ) {
|
|
105
|
+
try { const m = JSON.parse(fs.readFileSync(arq, 'utf8')); return { titulo: [], texto: [], paleta: [], ...m }; }
|
|
106
|
+
catch { return { titulo: [], texto: [], paleta: [] }; }
|
|
107
|
+
}
|
|
108
|
+
function gravarMemoria(m, arq = ARQ) {
|
|
109
|
+
try { fs.mkdirSync(path.dirname(arq), { recursive: true }); fs.writeFileSync(arq, JSON.stringify(m)); } catch { /* sem memória, só sorteio */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const norm = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().trim();
|
|
113
|
+
function acharFonte(nome) { const n = norm(nome); return FONTES.find((f) => norm(f.nome) === n) || null; }
|
|
114
|
+
function acharPaleta(nome) { const n = norm(nome).replace(/\s+/g, '-'); return PALETAS.find((p) => p.nome === n) || null; }
|
|
115
|
+
|
|
116
|
+
/* O sorteio que pula os recentes. Se a lista inteira já foi usada, vale
|
|
117
|
+
qualquer uma — nunca fica sem. */
|
|
118
|
+
function sortear(lista, recentes, chave, sorte) {
|
|
119
|
+
const livres = lista.filter((x) => !recentes.includes(chave(x)));
|
|
120
|
+
const de = livres.length ? livres : lista;
|
|
121
|
+
return de[Math.floor(sorte() * de.length) % de.length];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Escolhe fonte e paleta para o roteiro e as põe em roteiro.marca, sem
|
|
126
|
+
* trocar nada que a marca já tem. Devolve o que escolheu e de onde veio.
|
|
127
|
+
* memoria: null não lê nem grava o disco (os testes); sorte: () => [0,1).
|
|
128
|
+
*/
|
|
129
|
+
function vestir(roteiro, { memoria = ARQ, sorte = Math.random } = {}) {
|
|
130
|
+
if (!roteiro || typeof roteiro !== 'object') return { mudou: false };
|
|
131
|
+
const marca = roteiro.marca || {};
|
|
132
|
+
const cores = marca.cores || {};
|
|
133
|
+
const visual = roteiro.visual || {};
|
|
134
|
+
const mem = memoria ? lerMemoria(memoria) : { titulo: [], texto: [], paleta: [] };
|
|
135
|
+
const escolha = { mudou: false };
|
|
136
|
+
|
|
137
|
+
if (!(marca.fontes && marca.fontes.titulo)) {
|
|
138
|
+
// A IA escolheu pelo nome: vale qualquer família do Google Fonts, mesmo fora do cardápio.
|
|
139
|
+
const titulo = visual.titulo ? String(visual.titulo) : sortear(TITULO, mem.titulo, (f) => f.nome, sorte).nome;
|
|
140
|
+
const textoLista = TEXTO.filter((f) => f.nome !== titulo);
|
|
141
|
+
const texto = visual.texto ? String(visual.texto) : sortear(textoLista, mem.texto, (f) => f.nome, sorte).nome;
|
|
142
|
+
marca.fontes = { ...(marca.fontes || {}), titulo, texto };
|
|
143
|
+
mem.titulo = [titulo, ...mem.titulo.filter((x) => x !== titulo)].slice(0, LEMBRA.titulo);
|
|
144
|
+
mem.texto = [texto, ...mem.texto.filter((x) => x !== texto)].slice(0, LEMBRA.texto);
|
|
145
|
+
Object.assign(escolha, { mudou: true, titulo, texto, fonteDaIA: !!visual.titulo });
|
|
146
|
+
}
|
|
147
|
+
// A cor da marca manda: com destaque ou gradiente declarado, a paleta não entra.
|
|
148
|
+
if (!(marca.gradiente && marca.gradiente.length >= 2) && !cores.destaque) {
|
|
149
|
+
const p = acharPaleta(visual.paleta) || sortear(PALETAS, mem.paleta, (x) => x.base, sorte);
|
|
150
|
+
marca.gradiente = [...p.grad];
|
|
151
|
+
marca.cores = { ...cores, fundo: cores.fundo || p.fundo, destaque: p.grad[1], apoio: p.grad[0] };
|
|
152
|
+
mem.paleta = [p.base, ...mem.paleta.filter((x) => x !== p.base)].slice(0, LEMBRA.paleta);
|
|
153
|
+
Object.assign(escolha, { mudou: true, paleta: p.nome, paletaDaIA: !!acharPaleta(visual.paleta) });
|
|
154
|
+
}
|
|
155
|
+
if (escolha.mudou) {
|
|
156
|
+
roteiro.marca = marca;
|
|
157
|
+
if (memoria) gravarMemoria(mem, memoria);
|
|
158
|
+
}
|
|
159
|
+
return escolha;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** O cardápio para a IA: nomes por tipo, e as paletas pela base. */
|
|
163
|
+
function cardapio() {
|
|
164
|
+
const porTipo = {};
|
|
165
|
+
for (const f of FONTES) (porTipo[f.tipo] = porTipo[f.tipo] || []).push(f.nome);
|
|
166
|
+
return {
|
|
167
|
+
fontes: porTipo,
|
|
168
|
+
paletas: BASES.map(([nome, fundo, a, b]) => `${nome} ${fundo} ${a} ${b}`),
|
|
169
|
+
variacoes: 'cada paleta também vem -invertida, -tingida e -mono (ex.: "brasa-mono")',
|
|
170
|
+
uso: 'roteiro.visual = {titulo, texto, paleta}. Sem visual, o PrimoCode sorteia e não repete os últimos vídeos.',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { vestir, cardapio, larguraDe, acharFonte, acharPaleta, FONTES, PALETAS, BASES, TITULO, TEXTO, luz, ARQ };
|