primocode 10.5.0 → 10.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/primocode.js +117 -24
- package/lib/act.js +141 -9
- package/lib/agents-md.js +140 -0
- package/lib/auditoria.js +122 -0
- package/lib/browser.js +48 -1
- package/lib/catalogo.js +24 -4
- package/lib/comandos-extra.js +2 -0
- package/lib/config.js +32 -4
- package/lib/entrada.js +33 -11
- package/lib/estudio/colecao.js +34 -1
- package/lib/estudio/diretor.js +1 -0
- package/lib/estudio/falantes.js +30 -6
- package/lib/estudio/hibrido.js +144 -8
- package/lib/estudio/index.js +151 -59
- package/lib/estudio/kit/mundo.jsx +1060 -0
- package/lib/estudio/kit/voxel.jsx +315 -70
- package/lib/estudio/kit.js +1 -1
- package/lib/estudio/kokoro.js +18 -4
- package/lib/estudio/margem.js +1 -1
- package/lib/estudio/mundo.js +305 -0
- package/lib/estudio/personagem.js +164 -11
- package/lib/estudio/producao.js +107 -17
- package/lib/estudio/projeto.js +77 -22
- package/lib/leitura.js +176 -0
- package/lib/local/cerebro.json +180 -3
- package/lib/local/entrada.js +61 -4
- package/lib/mcp.js +296 -12
- package/lib/memoria-conversa.js +32 -3
- package/lib/mencoes.js +5 -1
- package/lib/nome-curto.js +126 -0
- package/lib/nome-projeto.js +5 -2
- package/lib/outra-pasta.js +161 -0
- package/lib/perigo.js +34 -2
- package/lib/referencia.js +40 -11
- package/lib/repeticao.js +20 -1
- package/lib/tela.js +1 -1
- package/lib/tools.js +76 -6
- package/lib/travado.js +141 -0
- package/lib/ui.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mundo.js — o lugar, os bichos e as coisas do vídeo 3D, a partir do PEDIDO.
|
|
3
|
+
*
|
|
4
|
+
* "não quero que ele só troque as falas e os textos ... se eu pedir dois
|
|
5
|
+
* humanos ele vai fazer dois humanos, se eu pedir um cachorro ele vai fazer,
|
|
6
|
+
* se eu pedir um humano na praia ele vai fazer — vai mudar o cenário, vai
|
|
7
|
+
* mudar as coisas, e não é coisa pré-definida" (Joel, 11/10)
|
|
8
|
+
*
|
|
9
|
+
* Até a 10.5 o 3D tinha UM set (a parede de telas) e o elenco era gente ou o
|
|
10
|
+
* mascote. Aqui o set vira um MUNDO montado de peças:
|
|
11
|
+
*
|
|
12
|
+
* · o LUGAR (praia, cidade, floresta, espaço, cozinha…): céu, chão, água,
|
|
13
|
+
* luz e hora do dia; vem do texto livre ("numa praia ao pôr do sol");
|
|
14
|
+
* · os ELEMENTOS: o que há no lugar. Uma palavra do catálogo (palmeira, carro,
|
|
15
|
+
* prédio, foguete…) vira uma peça de blocos desenhada no kit/mundo.jsx; uma
|
|
16
|
+
* palavra FORA do catálogo vira um ícone 3D da biblioteca (colecao.js,
|
|
17
|
+
* 1.595 objetos) transformado em blocos na hora do render — é isso que
|
|
18
|
+
* tira o "pré-definido": qualquer coisa que tenha ícone entra na cena;
|
|
19
|
+
* · o ELENCO: pessoa, robô/mascote, BICHO (cachorro, gato, cavalo, vaca,
|
|
20
|
+
* pinguim, peixe, dinossauro… — o kit/mundo.jsx monta cada espécie) ou
|
|
21
|
+
* COISA (um ícone em blocos que fala: "uma pizza conversando com um café").
|
|
22
|
+
* E os FIGURANTES: quem está na cena sem falar ("um humano com o cachorro").
|
|
23
|
+
*
|
|
24
|
+
* A IA decide (roteiro.cena, roteiro.personagens, parte.cenario); o que ela
|
|
25
|
+
* não disse sai do texto do pedido e das falas — a mesma regra do resto do
|
|
26
|
+
* Estúdio: o modelo manda, a leitura do texto só completa. É o que faz um
|
|
27
|
+
* modelo local pequeno, que só escreve as falas, ainda ganhar a praia que a
|
|
28
|
+
* pessoa pediu.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
const sem = (s) => String(s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
|
|
34
|
+
|
|
35
|
+
/* ── OS LUGARES ─────────────────────────────────────────────────────────
|
|
36
|
+
ceu: [alto, horizonte] · chao: {tipo, cor, cor2} · agua: cor do mar ·
|
|
37
|
+
dentro: set fechado (parede no fundo) · props: o que o lugar tem quando a
|
|
38
|
+
cena não disse. As palavras são lidas sem acento. */
|
|
39
|
+
const LUGARES = {
|
|
40
|
+
/* A COBERTURA À NOITE (2ª referência do Joel, 11/10): a janela de vidro
|
|
41
|
+
com a cidade acesa lá fora, as luzes grandes desfocadas (bokeh), o
|
|
42
|
+
parapeito escuro e os CARTÕES de interface flutuando na frente (o
|
|
43
|
+
assunto do vídeo, desfocados pela lente). É o set padrão quando o
|
|
44
|
+
pedido não diz lugar. */
|
|
45
|
+
cobertura: { palavras: /\b(cobertura|terraco|rooftop|janela|vista da cidade|apartamento alto|predio alto|escritorio a noite)\b/, dentro: true, cobertura: true,
|
|
46
|
+
parede: '#050816', chao: { tipo: 'madeira', cor: '#2a1f1a', cor2: '#241a16' }, luz: '#ffd2b0', recorte: '#6f8bff', acento: '#ff6a2b', props: [] },
|
|
47
|
+
telas: { palavras: /\b(estudio|podcast|tecnologia|telas?|set|programa|live|transmissao)\b/, dentro: true, telas: true,
|
|
48
|
+
parede: '#07061a', chao: { tipo: 'ladrilho', cor: '#4338ca', cor2: '#3b32b8' }, luz: '#ffd2b0', recorte: '#8b7bff', acento: '#ff7a45', props: [] },
|
|
49
|
+
praia: { palavras: /\b(praia|mar|litoral|beira[ -]mar|areia|orla|surf\w*|ilha)\b/, ceu: ['#3d8fe0', '#c4e8ff'], agua: '#1d8bc9',
|
|
50
|
+
chao: { tipo: 'areia', cor: '#ead29c', cor2: '#dfc48a' }, luz: '#fff2d6', recorte: '#ffd08a', acento: '#ff7a45', sol: true,
|
|
51
|
+
props: ['palmeira', 'palmeira', 'guarda-sol', 'cadeira de praia', 'palmeira', 'castelo de areia', 'pedra'] },
|
|
52
|
+
cidade: { palavras: /\b(cidade|rua|avenida|centro|predios?|esquina|calcada|metropole|urbano)\b/, ceu: ['#5b9be6', '#dbeaff'],
|
|
53
|
+
chao: { tipo: 'asfalto', cor: '#5b5e67', cor2: '#53565f' }, luz: '#fff1dc', recorte: '#9fc3ff', acento: '#facc15', sol: true,
|
|
54
|
+
props: ['predio', 'predio', 'predio', 'poste', 'carro', 'arvore', 'predio', 'poste', 'carro', 'predio'] },
|
|
55
|
+
floresta: { palavras: /\b(floresta|mata|selva|bosque|trilha|acampamento|amazonia)\b/, ceu: ['#6fb3e8', '#dcf2d6'],
|
|
56
|
+
chao: { tipo: 'grama', cor: '#4f9a3c', cor2: '#468c35' }, luz: '#fff0d0', recorte: '#b6f0a0', acento: '#facc15', sol: true, neblina: 0.6,
|
|
57
|
+
props: ['arvore', 'pinheiro', 'arvore', 'pinheiro', 'arbusto', 'pedra', 'arvore', 'pinheiro', 'cogumelo', 'arvore'] },
|
|
58
|
+
parque: { palavras: /\b(parque|jardim|praca|gramado|piquenique)\b/, ceu: ['#58a6ee', '#d9eeff'],
|
|
59
|
+
chao: { tipo: 'grama', cor: '#5aa845', cor2: '#4f9a3c' }, luz: '#fff2d6', recorte: '#c8f0b0', acento: '#f472b6', sol: true,
|
|
60
|
+
props: ['arvore', 'banco', 'flor', 'arvore', 'poste', 'flor', 'arbusto', 'arvore', 'flor'] },
|
|
61
|
+
fazenda: { palavras: /\b(fazenda|sitio|roca|interior|celeiro|curral|plantacao|colheita)\b/, ceu: ['#68aef0', '#f3ecd0'],
|
|
62
|
+
chao: { tipo: 'grama', cor: '#6aa84f', cor2: '#5e9a44' }, luz: '#fff1d0', recorte: '#ffe0a0', acento: '#dc2626', sol: true,
|
|
63
|
+
props: ['celeiro', 'cerca', 'cerca', 'arvore', 'feno', 'cerca', 'arvore', 'feno'] },
|
|
64
|
+
neve: { palavras: /\b(neve|inverno|gelo|polo|nevando|esqui|frio)\b/, ceu: ['#9fbfe6', '#eef4fb'],
|
|
65
|
+
chao: { tipo: 'neve', cor: '#f4f8fc', cor2: '#e6eef6' }, luz: '#eef4ff', recorte: '#a8c8ff', acento: '#38bdf8', sol: true,
|
|
66
|
+
props: ['pinheiro nevado', 'pinheiro nevado', 'boneco de neve', 'montanha', 'pinheiro nevado', 'pedra', 'pinheiro nevado'] },
|
|
67
|
+
deserto: { palavras: /\b(deserto|duna|sertao|saara|oasis)\b/, ceu: ['#f0a35a', '#ffe2ae'],
|
|
68
|
+
chao: { tipo: 'areia', cor: '#e2b06c', cor2: '#d6a05c' }, luz: '#ffe6c0', recorte: '#ffb070', acento: '#ea580c', sol: true,
|
|
69
|
+
props: ['cacto', 'cacto', 'pedra', 'montanha', 'cacto', 'pedra'] },
|
|
70
|
+
espaco: { palavras: /\b(espaco|lua|planeta|galaxia|foguete|astronauta|marte|orbita|estrelas|universo|nave)\b/, ceu: ['#02020f', '#0d0b2c'], estrelas: true,
|
|
71
|
+
chao: { tipo: 'lua', cor: '#a1a1ad', cor2: '#90909c' }, luz: '#e8ecff', recorte: '#8b7bff', acento: '#a78bfa',
|
|
72
|
+
props: ['planeta', 'foguete', 'pedra', 'planeta', 'cratera', 'pedra'] },
|
|
73
|
+
fundo_do_mar: { palavras: /\b(fundo do mar|submarino|oceano|aquario|mergulh\w*|recife|coral)\b/, ceu: ['#022c46', '#0b6f94'], subaquatico: true,
|
|
74
|
+
chao: { tipo: 'areia', cor: '#c9b98a', cor2: '#bba97a' }, luz: '#bfefff', recorte: '#38bdf8', acento: '#f97316', neblina: 1,
|
|
75
|
+
props: ['alga', 'coral', 'pedra', 'alga', 'coral', 'bau', 'alga'] },
|
|
76
|
+
escritorio: { palavras: /\b(escritorio|trabalho|reuniao|empresa|firma|coworking|startup)\b/, dentro: true, parede: '#d8d0c2',
|
|
77
|
+
chao: { tipo: 'madeira', cor: '#9a7350', cor2: '#8d6846' }, luz: '#fff1dc', recorte: '#ffe2b8', acento: '#2563eb',
|
|
78
|
+
props: ['mesa', 'computador', 'planta', 'estante', 'cadeira', 'quadro'] },
|
|
79
|
+
sala: { palavras: /\b(sala|casa|sofa|quarto|lar|apartamento|sala de estar)\b/, dentro: true, parede: '#e6d6c3',
|
|
80
|
+
chao: { tipo: 'madeira', cor: '#a87b52', cor2: '#9a6e47' }, luz: '#ffe8c8', recorte: '#ffd6a0', acento: '#e11d48',
|
|
81
|
+
props: ['sofa', 'luminaria', 'planta', 'estante', 'tapete', 'quadro'] },
|
|
82
|
+
cozinha: { palavras: /\b(cozinha|restaurante|padaria|cafeteria|cafe|lanchonete|confeitaria|pizzaria|bar)\b/, dentro: true, parede: '#efe4d2',
|
|
83
|
+
chao: { tipo: 'ladrilho', cor: '#e8e1d4', cor2: '#cfc5b4' }, luz: '#fff0d6', recorte: '#ffd9a0', acento: '#dc2626',
|
|
84
|
+
props: ['balcao', 'mesa', 'cadeira', 'planta', 'estante', 'luminaria'] },
|
|
85
|
+
escola: { palavras: /\b(escola|sala de aula|aula|professor|faculdade|universidade|colegio)\b/, dentro: true, parede: '#dfe8dc',
|
|
86
|
+
chao: { tipo: 'madeira', cor: '#b08458', cor2: '#a3794f' }, luz: '#fffaf0', recorte: '#d6f0ff', acento: '#16a34a',
|
|
87
|
+
props: ['lousa', 'mesa', 'mesa', 'estante', 'planta'] },
|
|
88
|
+
palco: { palavras: /\b(palco|show|teatro|concerto|festa|balada|evento)\b/, dentro: true, parede: '#0b0612',
|
|
89
|
+
chao: { tipo: 'madeira', cor: '#3a2a20', cor2: '#33251c' }, luz: '#ffe1f5', recorte: '#ff4fd8', acento: '#22d3ee', holofotes: true,
|
|
90
|
+
props: ['caixa de som', 'caixa de som', 'luminaria'] },
|
|
91
|
+
campo: { palavras: /\b(campo|montanhas?|colina|natureza|ao ar livre|vale)\b/, ceu: ['#5aa3ec', '#dcefff'],
|
|
92
|
+
chao: { tipo: 'grama', cor: '#5ca846', cor2: '#529b3e' }, luz: '#fff2d6', recorte: '#c8f0b0', acento: '#facc15', sol: true,
|
|
93
|
+
props: ['montanha', 'arvore', 'flor', 'arvore', 'pedra', 'montanha', 'flor'] },
|
|
94
|
+
};
|
|
95
|
+
// O que o set antigo chamava por outro nome.
|
|
96
|
+
const APELIDOS_LUGAR = { neon: 'palco', oceano: 'fundo_do_mar', terra: 'deserto', podcast: 'telas', estudio: 'telas', noite: 'telas' };
|
|
97
|
+
|
|
98
|
+
const HORAS = {
|
|
99
|
+
noite: { palavras: /\b(noite|noturn\w*|madrugada|luar|a noite|de noite)\b/, ceu: ['#070b24', '#1c2552'], luz: '#b8c6ff', forca: 0.45, lua: true, estrelas: true },
|
|
100
|
+
por_do_sol: { palavras: /\b(por do sol|entardecer|fim de tarde|crepusculo|poente|anoitecer)\b/, ceu: ['#46368a', '#ff9a5a'], luz: '#ffb27a', forca: 0.8, solBaixo: true },
|
|
101
|
+
amanhecer: { palavras: /\b(amanhecer|nascer do sol|alvorada|de manha cedo)\b/, ceu: ['#6a8fd8', '#ffd2a8'], luz: '#ffd9b0', forca: 0.85, solBaixo: true },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/* ── O CATÁLOGO DE PEÇAS (kit/mundo.jsx desenha cada uma) ───────────────
|
|
105
|
+
A chave é a peça; as palavras são como a pessoa (ou a IA) diz. */
|
|
106
|
+
const PECAS = {
|
|
107
|
+
palmeira: /\bpalmeiras?|coqueiros?\b/, arvore: /\barvores?|arvorezinha\b/, pinheiro: /\bpinheiros?\b/, 'pinheiro nevado': /\bpinheiros? nevados?\b/,
|
|
108
|
+
arbusto: /\barbustos?|moitas?\b/, flor: /\bflor(es)?\b/, pedra: /\bpedras?|rochas?\b/, cacto: /\bcactos?\b/, montanha: /\bmontanhas?|morros?\b/,
|
|
109
|
+
casa: /\bcasas?|casinha\b/, predio: /\bpredios?|edificios?|arranha[- ]ceus?\b/, celeiro: /\bceleiros?\b/, poste: /\bpostes?\b/,
|
|
110
|
+
carro: /\bcarros?|automove(l|is)\b/, 'guarda-sol': /\bguarda[- ]sois?|guarda[- ]sol\b/, 'cadeira de praia': /\bcadeiras? de praia\b/,
|
|
111
|
+
'castelo de areia': /\bcastelos? de areia\b/, mesa: /\bmesas?\b/, cadeira: /\bcadeiras?\b/, sofa: /\bsofas?\b/, planta: /\bplantas?|vasos?\b/,
|
|
112
|
+
estante: /\bestantes?|prateleiras?\b/, computador: /\bcomputador(es)?|notebooks?|pc\b/, luminaria: /\bluminarias?|abajur\b/,
|
|
113
|
+
quadro: /\bquadros?\b/, lousa: /\blousas?|quadro negro\b/, balcao: /\bbalca(o|oes)\b/, tapete: /\btapetes?\b/, banco: /\bbancos? de praca|banquinhos?\b/,
|
|
114
|
+
cerca: /\bcercas?\b/, feno: /\bfenos?\b/, 'boneco de neve': /\bbonecos? de neve\b/, foguete: /\bfoguetes?\b/, planeta: /\bplanetas?\b/,
|
|
115
|
+
cratera: /\bcrateras?\b/, alga: /\balgas?\b/, coral: /\bcorais?|coral\b/, bau: /\bbaus?|tesouros?\b/, cogumelo: /\bcogumelos?\b/,
|
|
116
|
+
nuvem: /\bnuvens?\b/, fogueira: /\bfogueiras?\b/, barraca: /\bbarracas?|tendas?\b/, barco: /\bbarcos?|canoas?\b/,
|
|
117
|
+
'caixa de som': /\bcaixas? de som|alto[- ]falantes?\b/,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/* ── AS ESPÉCIES (kit/mundo.jsx monta cada uma de blocos) ────────────── */
|
|
121
|
+
const ESPECIES = {
|
|
122
|
+
cachorro: /\b(cachorr\w*|cao|caes|cachorrinho|dog|vira[- ]lata|labrador|filhote)\b/, gato: /\b(gat[oa]s?|gatinh\w*|felino)\b/,
|
|
123
|
+
cavalo: /\b(caval\w*|egua|potro|pônei|ponei)\b/, vaca: /\b(vacas?|boi|bois|touro|bezerro)\b/, porco: /\b(porc\w*|leitao)\b/,
|
|
124
|
+
ovelha: /\b(ovelhas?|carneiros?|cordeiro)\b/, raposa: /\b(raposas?)\b/, lobo: /\b(lob[oa]s?)\b/, urso: /\b(urs[oa]s?)\b/,
|
|
125
|
+
panda: /\b(pandas?)\b/, coelho: /\b(coelh\w*)\b/, leao: /\b(leao|leoes|leoa)\b/, tigre: /\b(tigres?)\b/, elefante: /\b(elefantes?)\b/,
|
|
126
|
+
girafa: /\b(girafas?)\b/, galinha: /\b(galinhas?|galos?|pintinhos?)\b/, pato: /\b(pat[oa]s?|patinho)\b/, pinguim: /\b(pinguins?)\b/,
|
|
127
|
+
passaro: /\b(passar\w*|ave|aves|passarinho)\b/, papagaio: /\b(papagaios?|arara)\b/, coruja: /\b(corujas?)\b/,
|
|
128
|
+
peixe: /\b(peixes?|peixinho)\b/, tubarao: /\b(tubara(o|oes))\b/, golfinho: /\b(golfinhos?)\b/, tartaruga: /\b(tartarugas?|jabuti)\b/,
|
|
129
|
+
dinossauro: /\b(dinossauros?|dino|t[- ]?rex)\b/, sapo: /\b(sapos?|ra)\b/, macaco: /\b(macac\w*)\b/,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const PESSOA = /\b(humanos?|pessoas?|homens?|mulher(es)?|menin[oa]s?|crianc\w*|amig[oa]s?|garot[oa]s?|rapaz|mo[cç][oa]s?|senhor(a|es)?|vov[oóô]|av[oóô]s?|casal|familia|cliente|vendedor\w*|professor\w*|alun[oa]s?|medic[oa]s?|astronautas?|surfistas?|chef|cozinheir[oa]s?)\b/;
|
|
133
|
+
const ROBO = /\b(robo|robos|ia|inteligencia artificial|mascote|claude|chatgpt|gpt|gemini|assistente|bot)\b/;
|
|
134
|
+
const NUMEROS = { um: 1, uma: 1, dois: 2, duas: 2, tres: 3, quatro: 4, cinco: 5, casal: 2, par: 2 };
|
|
135
|
+
|
|
136
|
+
function contar(texto, re) {
|
|
137
|
+
// "dois humanos", "3 cachorros", "um casal": o número logo antes da palavra.
|
|
138
|
+
const s = sem(texto);
|
|
139
|
+
if (re === PESSOA && /\b(um )?casal\b/.test(s)) return 2;
|
|
140
|
+
const m = new RegExp(`(\\d+|um|uma|dois|duas|tres|quatro|cinco)\\s+(?:\\w+\\s+)?${re.source.replace(/^\\b|\\b$/g, '')}`).exec(s);
|
|
141
|
+
if (m) return Math.min(4, Number(m[1]) || NUMEROS[m[1]] || 1);
|
|
142
|
+
return re.test(s) ? 1 : 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** O lugar e a hora que o texto pede, ou null. */
|
|
146
|
+
function lerLugar(texto) {
|
|
147
|
+
const s = sem(texto);
|
|
148
|
+
let lugar = null, onde = -1;
|
|
149
|
+
// O mais específico primeiro: "fundo do mar" antes de "mar", "sala de aula" antes de "sala".
|
|
150
|
+
const ordem = ['cobertura', 'fundo_do_mar', 'escola', 'praia', 'espaco', 'neve', 'deserto', 'fazenda', 'floresta', 'parque', 'cidade', 'cozinha', 'escritorio', 'palco', 'sala', 'campo', 'telas'];
|
|
151
|
+
for (const n of ordem) { const m = LUGARES[n].palavras.exec(s); if (m) { lugar = n; onde = m.index; break; } }
|
|
152
|
+
/* A hora vale para AQUELE lugar: "na praia, depois na cidade à noite" é
|
|
153
|
+
praia de dia. Com lugar no texto, só a hora a até 40 letras dele. */
|
|
154
|
+
let hora = null;
|
|
155
|
+
for (const [n, h] of Object.entries(HORAS)) {
|
|
156
|
+
const m = h.palavras.exec(s);
|
|
157
|
+
if (!m) continue;
|
|
158
|
+
if (onde < 0) { hora = n; break; }
|
|
159
|
+
// Outro lugar no meio ("praia, depois vão pra cidade à noite"): a hora é DELE.
|
|
160
|
+
const entre = s.slice(Math.min(m.index, onde) + 1, Math.max(m.index, onde));
|
|
161
|
+
const outroLugar = Object.entries(LUGARES).some(([k, L]) => k !== lugar && L.palavras.test(entre));
|
|
162
|
+
if (!outroLugar && Math.abs(m.index - onde) <= 40) { hora = n; break; }
|
|
163
|
+
}
|
|
164
|
+
return { lugar, hora };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** As peças do catálogo e os bichos que o texto cita (em ordem de aparição). */
|
|
168
|
+
function lerCoisas(texto) {
|
|
169
|
+
const s = sem(texto);
|
|
170
|
+
const pecas = [];
|
|
171
|
+
for (const [nome, re] of Object.entries(PECAS)) {
|
|
172
|
+
const m = re.exec(s);
|
|
173
|
+
if (m) pecas.push({ nome, em: m.index });
|
|
174
|
+
}
|
|
175
|
+
// "pinheiro nevado" ganha de "pinheiro"; "cadeira de praia" ganha de "cadeira".
|
|
176
|
+
const filtradas = pecas.filter((p) => !pecas.some((q) => q !== p && q.nome.startsWith(p.nome + ' ') && Math.abs(q.em - p.em) < 3)
|
|
177
|
+
&& !pecas.some((q) => q !== p && q.nome.includes(p.nome) && q.nome !== p.nome && Math.abs(q.em - p.em) < 12));
|
|
178
|
+
const bichos = [];
|
|
179
|
+
for (const [esp, re] of Object.entries(ESPECIES)) {
|
|
180
|
+
const m = re.exec(s);
|
|
181
|
+
if (m) bichos.push({ especie: esp, em: m.index, quantos: contar(texto, re) || 1 });
|
|
182
|
+
}
|
|
183
|
+
return { pecas: filtradas.sort((a, b) => a.em - b.em).map((p) => p.nome), bichos: bichos.sort((a, b) => a.em - b.em) };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** O que o PEDIDO descreve do elenco: quantas pessoas, que bichos, se há IA. */
|
|
187
|
+
function lerElenco(texto) {
|
|
188
|
+
const s = sem(texto);
|
|
189
|
+
const { bichos } = lerCoisas(texto);
|
|
190
|
+
return { pessoas: contar(texto, PESSOA), robo: ROBO.test(s), bichos };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ── O MUNDO MONTADO ────────────────────────────────────────────────────
|
|
194
|
+
A peça é posta numa faixa ATRÁS e DOS LADOS do elenco, nunca no corredor
|
|
195
|
+
entre a câmera e quem fala. Com mar, a faixa é a areia antes da água. */
|
|
196
|
+
function rngDe(semente) {
|
|
197
|
+
let s = (Number(semente) >>> 0) || 1;
|
|
198
|
+
return () => { s = (Math.imul(s ^ (s >>> 15), 2246822507) + 0x9e3779b9) >>> 0; return s / 4294967296; };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const ehCor = (c) => typeof c === 'string' && /^#[0-9a-f]{6}$/i.test(c);
|
|
202
|
+
|
|
203
|
+
function posicionar(nomes, { semente = 1, agua = false, dentro = false, largura = 1 } = {}) {
|
|
204
|
+
const r = rngDe(semente);
|
|
205
|
+
const out = [];
|
|
206
|
+
const livre = (x, z, raio) => out.every((p) => Math.hypot(p.x - x, p.z - z) > raio + (p.raio || 0.8));
|
|
207
|
+
nomes.forEach((nome, i) => {
|
|
208
|
+
const grande = /predio|montanha|celeiro|planeta|foguete|palmeira|pinheiro|arvore/.test(typeof nome === 'string' ? nome : '');
|
|
209
|
+
const raio = grande ? 1.3 : 0.75;
|
|
210
|
+
for (let tent = 0; tent < 40; tent++) {
|
|
211
|
+
let x, z;
|
|
212
|
+
if (dentro) {
|
|
213
|
+
// Encostado na parede do fundo ou nos lados do set.
|
|
214
|
+
x = (r() * 2 - 1) * 4.6 * largura; z = -3.1 + r() * 0.9;
|
|
215
|
+
if (Math.abs(x) < 1.7 && tent < 30) continue;
|
|
216
|
+
} else if (agua) {
|
|
217
|
+
const lado = i % 2 ? 1 : -1;
|
|
218
|
+
x = lado * (1.9 + r() * 4.5) * largura; z = -4.2 + r() * 3.6;
|
|
219
|
+
} else {
|
|
220
|
+
const lado = i % 2 ? 1 : -1;
|
|
221
|
+
const longe = grande ? 0.55 + r() * 0.45 : r();
|
|
222
|
+
x = lado * (1.8 + r() * 5.5) * largura; z = -1.6 - longe * 9;
|
|
223
|
+
if (r() < 0.25 && !grande) { x = lado * (2.2 + r() * 1.5); z = -0.4 + r() * 1.4; }
|
|
224
|
+
}
|
|
225
|
+
if (!livre(x, z, raio) && tent < 39) continue;
|
|
226
|
+
out.push({ x: Math.round(x * 100) / 100, z: Math.round(z * 100) / 100, raio, giro: Math.round((r() * 2 - 1) * 60) / 100, escala: Math.round((0.85 + r() * 0.35) * 100) / 100 });
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* O mundo de um trecho do vídeo.
|
|
235
|
+
* @param {string|object} pedido "praia ao pôr do sol" ou {lugar, hora, elementos, ceu, chao, ...}
|
|
236
|
+
* @param {object} op {texto (o pedido e as falas: completa o lugar), pedidoTexto (só o
|
|
237
|
+
* pedido da pessoa: as coisas que ele cita entram na cena), semente, nElenco}
|
|
238
|
+
*/
|
|
239
|
+
function mundoDe(pedido, { texto = '', pedidoTexto = '', semente = 1, nElenco = 1 } = {}) {
|
|
240
|
+
const obj = pedido && typeof pedido === 'object' ? pedido : { lugar: String(pedido || '') };
|
|
241
|
+
const descr = [obj.lugar, obj.descricao, obj.hora].filter(Boolean).join(' ');
|
|
242
|
+
let { lugar, hora } = lerLugar(descr);
|
|
243
|
+
const nomeDireto = sem(obj.lugar).replace(/\s+/g, '_');
|
|
244
|
+
if (!lugar && LUGARES[nomeDireto]) lugar = nomeDireto;
|
|
245
|
+
if (!lugar && APELIDOS_LUGAR[nomeDireto]) lugar = APELIDOS_LUGAR[nomeDireto];
|
|
246
|
+
// Sem lugar no pedido da cena, o texto do vídeo (o pedido da pessoa e as falas) completa.
|
|
247
|
+
const doTexto = lerLugar(texto);
|
|
248
|
+
if (!lugar) lugar = doTexto.lugar || 'cobertura';
|
|
249
|
+
if (!hora) hora = doTexto.hora;
|
|
250
|
+
if (APELIDOS_LUGAR[lugar]) lugar = APELIDOS_LUGAR[lugar];
|
|
251
|
+
const L = LUGARES[lugar];
|
|
252
|
+
const H = hora && !L.dentro ? HORAS[hora] : null;
|
|
253
|
+
|
|
254
|
+
// Os elementos: os que a cena listou (qualquer palavra) + os que o texto cita + os do lugar.
|
|
255
|
+
const pedidos = Array.isArray(obj.elementos) ? obj.elementos.map((e) => (typeof e === 'string' ? e : (e && (e.nome || e.tipo)) || '')).filter(Boolean) : [];
|
|
256
|
+
// As falas citam coisa de passagem ("comprei um carro"): só o PEDIDO põe coisa na cena.
|
|
257
|
+
const citados = lerCoisas([descr, pedidoTexto, Array.isArray(obj.elementos) ? '' : String(obj.elementos || '')].join(' ')).pecas;
|
|
258
|
+
const nomes = [];
|
|
259
|
+
for (const e of [...pedidos, ...citados]) {
|
|
260
|
+
const s = sem(e).trim();
|
|
261
|
+
const peca = Object.keys(PECAS).find((k) => PECAS[k].test(s) || sem(k) === s);
|
|
262
|
+
nomes.push(peca ? { peca } : { icone: s.slice(0, 40) });
|
|
263
|
+
}
|
|
264
|
+
// O lugar completa até 7 peças: a cena nunca fica vazia, e o que a IA pediu vem primeiro.
|
|
265
|
+
for (const p of L.props) if (nomes.length < 7 || nomes.length < L.props.length) nomes.push({ peca: p });
|
|
266
|
+
const lugares = posicionar(nomes.map((n) => n.peca || n.icone), { semente, agua: !!L.agua, dentro: !!L.dentro, largura: nElenco > 2 ? 1.25 : 1 });
|
|
267
|
+
const props = nomes.slice(0, lugares.length).map((n, i) => ({ ...n, ...lugares[i] }));
|
|
268
|
+
|
|
269
|
+
const ceu = Array.isArray(obj.ceu) && obj.ceu.every(ehCor) ? obj.ceu : (H ? H.ceu : L.ceu) || null;
|
|
270
|
+
const chao = { ...L.chao, ...(obj.chao && typeof obj.chao === 'object' ? obj.chao : {}), ...(ehCor(obj.chao) ? { cor: obj.chao, cor2: obj.chao } : {}) };
|
|
271
|
+
return {
|
|
272
|
+
nome: lugar + (H ? ':' + hora : ''), lugar, hora: H ? hora : null,
|
|
273
|
+
dentro: !!L.dentro, telas: !!L.telas, cobertura: !!L.cobertura, parede: ehCor(obj.parede) ? obj.parede : L.parede || null,
|
|
274
|
+
ceu, chao, agua: L.agua || null, subaquatico: !!L.subaquatico, holofotes: !!L.holofotes,
|
|
275
|
+
sol: !!(L.sol && !(H && H.lua)), solBaixo: !!(H && H.solBaixo), lua: !!(H && H.lua), estrelas: !!(L.estrelas || (H && H.estrelas)),
|
|
276
|
+
neblina: L.neblina || 0,
|
|
277
|
+
luz: ehCor(obj.luz) ? obj.luz : (H ? H.luz : L.luz), forca: H ? H.forca : 1,
|
|
278
|
+
recorte: L.recorte, acento: ehCor(obj.acento) ? obj.acento : L.acento,
|
|
279
|
+
props,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Os ícones que o mundo e o elenco precisam baixar (colecao.js) antes do render. */
|
|
284
|
+
function iconesPendentes(partitura) {
|
|
285
|
+
const out = new Set();
|
|
286
|
+
for (const m of partitura.mundos || []) for (const p of m.props) if (p.icone && !p.src) out.add(p.icone);
|
|
287
|
+
for (const e of [...(partitura.elenco || []), ...(partitura.figurantes || [])]) if (e.tipo === 'coisa' && !e.src) out.add(e.icone);
|
|
288
|
+
return [...out];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Põe o PNG baixado em cada peça e personagem que pediu aquele ícone; o que não achou ícone sai da cena. */
|
|
292
|
+
function aplicarIcones(partitura, achados) {
|
|
293
|
+
for (const m of partitura.mundos || []) {
|
|
294
|
+
m.props = m.props.filter((p) => !p.icone || achados[p.icone]).map((p) => (p.icone ? { ...p, src: achados[p.icone] } : p));
|
|
295
|
+
}
|
|
296
|
+
for (const e of [...(partitura.elenco || []), ...(partitura.figurantes || [])]) {
|
|
297
|
+
if (e.tipo === 'coisa') {
|
|
298
|
+
if (achados[e.icone]) e.src = achados[e.icone];
|
|
299
|
+
else { e.tipo = 'robo'; e.cor = e.cor || '#d97757'; } // sem ícone, a coisa que fala vira o mascote
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return partitura;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
module.exports = { mundoDe, lerLugar, lerCoisas, lerElenco, iconesPendentes, aplicarIcones, LUGARES, HORAS, PECAS, ESPECIES, sem };
|
|
@@ -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 };
|