primocode 10.15.2 → 10.16.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 +64 -8
- package/lib/act.js +2 -0
- package/lib/app-chat.js +97 -0
- package/lib/catalogo.js +3 -1
- package/lib/estudio/gosto/tela.html +1349 -0
- package/lib/estudio/gosto.js +264 -0
- package/lib/estudio/index.js +9 -1
- package/lib/estudio/maquina-diretor.js +9 -5
- package/lib/estudio/maquina.js +5 -1
- package/lib/headless.js +15 -3
- package/lib/local/cerebro.json +93 -1
- package/lib/memoria-usuario.js +164 -0
- package/lib/nuvem.js +22 -1
- package/lib/repeticao.js +1 -1
- package/lib/sandbox.js +57 -9
- package/lib/tools.js +20 -4
- package/lib/ui.js +1 -1
- package/package.json +2 -2
- package/voz/primo_voz/__main__.py +55 -40
- package/voz/primo_voz/app.py +460 -3
- package/voz/primo_voz/chat.html +1762 -0
- package/voz/primo_voz/conversa.py +0 -26
- package/voz/primo_voz/hud.html +8 -2
- package/voz/primo_voz/hud.py +129 -5
- package/voz/primo_voz/identidade.py +1 -1
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gosto.js — aprender o gosto da pessoa pro motion.
|
|
3
|
+
*
|
|
4
|
+
* No PRIMEIRO estudio_motion, antes de renderizar, abre uma tela local com 20 motions tocando em loop num carrossel:
|
|
5
|
+
* a pessoa escolhe os que curtiu e, numa pergunta só, marca "mais disso" e "menos disso". Fica em ~/.primocode/gosto-motion.json.
|
|
6
|
+
*
|
|
7
|
+
* Regra do dono: o gosto é CONTEXTO E PESO, não trilho. O diretor (maquina-diretor.js) continua sorteando com
|
|
8
|
+
* variedade e histórico; aqui só saem multiplicadores (pesos). Nunca vira "estilo fixo".
|
|
9
|
+
*
|
|
10
|
+
* ler() / salvar() / tem() o arquivo
|
|
11
|
+
* pesos(gosto) multiplicadores pro diretor (tudo 1 = neutro)
|
|
12
|
+
* resumo(gosto) 1 a 3 frases pro modelo ler
|
|
13
|
+
* abrirTela() servidor local 127.0.0.1 + token; resolve {ok, gosto} | {ok:false, motivo}
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const http = require('http');
|
|
21
|
+
const crypto = require('crypto');
|
|
22
|
+
|
|
23
|
+
// Só nomes do vocabulário do diretor (maquina-diretor.js: CAMERAS, TRANSICOES, TIPOGRAFIAS).
|
|
24
|
+
const ESTILOS = [
|
|
25
|
+
{ id: 'brincalhao', nome: 'Brincalhão', frase: 'Quica, gira, pula. Leve e cheio de vida.', ritmo: 'rapido', cameras: ['pulso', 'orbita'], trocas: ['giro', 'encolhe', 'abre'], tipografias: ['arredondada', 'manuscrita'], overshoot: 0.18 },
|
|
26
|
+
{ id: 'premium', nome: 'Premium', frase: 'Lento, liso, caro. Desfoque saindo e nada de susto.', ritmo: 'lento', cameras: ['cinema', 'deriva'], trocas: ['desfoque', 'dissolve', 'cortina'], tipografias: ['serifa_fina', 'editorial'], overshoot: 0 },
|
|
27
|
+
{ id: 'corporativo', nome: 'Direto', frase: 'Entra empurrando, corte seco, tudo alinhado.', ritmo: 'medio', cameras: ['trilho', 'push'], trocas: ['empurra', 'persiana', 'corte'], tipografias: ['geometrica', 'tecnica'], overshoot: 0.04 },
|
|
28
|
+
{ id: 'energico', nome: 'Enérgico', frase: 'Chicote, punch de câmera, letra entrando rápido.', ritmo: 'rapido', cameras: ['chicote', 'pulso'], trocas: ['chicote', 'tremor', 'zoom', 'corte'], tipografias: ['pesada', 'poster'], overshoot: 0.25 },
|
|
29
|
+
{ id: 'cinema3d', nome: 'Cinema 3D', frase: 'Profundidade: tudo orbita e vem do fundo.', ritmo: 'medio', cameras: ['orbita', 'cinema', 'push'], trocas: ['giro', 'zoom', 'empurra'], tipografias: ['poster', 'editorial'], overshoot: 0.1 },
|
|
30
|
+
{ id: 'cinetico', nome: 'Letra que dança', frase: 'Cada letra entra sozinha, girando e trocando de peso.', ritmo: 'rapido', cameras: ['pulso', 'push'], trocas: ['corte', 'chicote', 'encolhe'], tipografias: ['poster', 'pesada', 'tecnica'], overshoot: 0.15 },
|
|
31
|
+
{ id: 'cubo', nome: 'Cubo', frase: 'Um cubo 3D gira de face em face e dá cambalhota.', ritmo: 'medio', cameras: ['orbita', 'push'], trocas: ['giro', 'zoom'], tipografias: ['pesada', 'poster'], overshoot: 0.2 },
|
|
32
|
+
{ id: 'glitch', nome: 'Glitch', frase: 'A palavra treme e se parte em fatias coloridas.', ritmo: 'rapido', cameras: ['chicote', 'pulso'], trocas: ['tremor', 'corte'], tipografias: ['tecnica', 'pesada'], overshoot: 0.05 },
|
|
33
|
+
{ id: 'espiral', nome: 'Espiral', frase: 'Pontos giram até o centro e explodem na palavra.', ritmo: 'rapido', cameras: ['orbita', 'pulso'], trocas: ['zoom', 'encolhe'], tipografias: ['pesada', 'arredondada'], overshoot: 0.2 },
|
|
34
|
+
{ id: 'slot', nome: 'Caça-níquel', frase: 'Cada letra roda como num rolo e trava uma por vez.', ritmo: 'rapido', cameras: ['pulso', 'trilho'], trocas: ['corte', 'persiana'], tipografias: ['pesada', 'poster'], overshoot: 0.1 },
|
|
35
|
+
{ id: 'tunel', nome: 'Túnel', frase: 'Quadros saem do centro girando, a palavra pulsa.', ritmo: 'rapido', cameras: ['push', 'pulso'], trocas: ['zoom', 'giro'], tipografias: ['geometrica', 'poster'], overshoot: 0.1 },
|
|
36
|
+
{ id: 'mosaico', nome: 'Mosaico', frase: 'Azulejos viram em onda e mostram a palavra.', ritmo: 'medio', cameras: ['push', 'deriva'], trocas: ['persiana', 'abre'], tipografias: ['geometrica', 'pesada'], overshoot: 0.15 },
|
|
37
|
+
{ id: 'estilhaco', nome: 'Estilhaço', frase: 'Um impacto quebra a palavra e ela se junta de volta.', ritmo: 'rapido', cameras: ['chicote', 'pulso'], trocas: ['zoom', 'tremor'], tipografias: ['pesada', 'poster'], overshoot: 0.22 },
|
|
38
|
+
{ id: 'contador', nome: 'Contador', frase: 'Os números rolam até 100% com a barra enchendo.', ritmo: 'medio', cameras: ['push', 'trilho'], trocas: ['empurra', 'corte'], tipografias: ['tecnica', 'geometrica'], overshoot: 0.06 },
|
|
39
|
+
{ id: 'orbita', nome: 'Órbita', frase: 'Anéis inclinados giram em volta da palavra.', ritmo: 'medio', cameras: ['orbita', 'cinema'], trocas: ['giro', 'dissolve'], tipografias: ['geometrica'], overshoot: 0.08 },
|
|
40
|
+
{ id: 'equalizador', nome: 'Equalizador', frase: 'Barras dançam e a palavra quica na batida.', ritmo: 'rapido', cameras: ['pulso', 'chicote'], trocas: ['corte', 'tremor'], tipografias: ['pesada', 'poster'], overshoot: 0.12 },
|
|
41
|
+
{ id: 'cartas', nome: 'Cartas', frase: 'Um baralho cai, abre em leque e junta de novo.', ritmo: 'medio', cameras: ['cinema', 'deriva'], trocas: ['abre', 'giro'], tipografias: ['editorial', 'arredondada'], overshoot: 0.18 },
|
|
42
|
+
{ id: 'gota', nome: 'Gota', frase: 'Uma gota cai, abre ondas e a palavra sobe delas.', ritmo: 'lento', cameras: ['deriva', 'cinema'], trocas: ['desfoque', 'dissolve'], tipografias: ['serifa_fina', 'editorial'], overshoot: 0.12 },
|
|
43
|
+
{ id: 'split', nome: 'Corte ao meio', frase: 'As metades da palavra entram por lados opostos.', ritmo: 'rapido', cameras: ['trilho', 'chicote'], trocas: ['persiana', 'empurra'], tipografias: ['pesada', 'tecnica'], overshoot: 0.04 },
|
|
44
|
+
{ id: 'traco', nome: 'Traço', frase: 'Uma linha se desenha e revela a palavra atrás.', ritmo: 'medio', cameras: ['trilho', 'deriva'], tipografias: ['manuscrita', 'editorial'], trocas: ['abre', 'cortina'], overshoot: 0.08 },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** O que a pessoa pode ter gostado (passo 2). */
|
|
48
|
+
const GOSTEI = [
|
|
49
|
+
{ id: 'rapido', nome: 'a velocidade' },
|
|
50
|
+
{ id: 'quique', nome: 'o quique (passa do ponto e volta)' },
|
|
51
|
+
{ id: 'tres_d', nome: 'o 3D' },
|
|
52
|
+
{ id: 'camera', nome: 'a câmera mexendo' },
|
|
53
|
+
{ id: 'letra', nome: 'a letra' },
|
|
54
|
+
{ id: 'cortes', nome: 'os cortes' },
|
|
55
|
+
{ id: 'cores', nome: 'as cores' },
|
|
56
|
+
{ id: 'calmo', nome: 'ser calmo e liso' },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** O que a pessoa NÃO quer (passo 3). */
|
|
60
|
+
const NAO_QUERO = [
|
|
61
|
+
{ id: 'lento', nome: 'devagar demais', curto: 'devagar demais' },
|
|
62
|
+
{ id: 'liso', nome: 'liso, sem graça', curto: 'liso' },
|
|
63
|
+
{ id: 'exagero', nome: 'exagerado, sujo', curto: 'exagero' },
|
|
64
|
+
{ id: 'flash', nome: 'flash na tela', curto: 'flash' },
|
|
65
|
+
{ id: 'tremido', nome: 'tremido demais', curto: 'tremido' },
|
|
66
|
+
{ id: 'letra_pequena', nome: 'letra pequena', curto: 'letra pequena' },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const MAX_TEXTO = 280;
|
|
70
|
+
const ID_ESTILO = new Set(ESTILOS.map((e) => e.id));
|
|
71
|
+
const ID_GOSTEI = new Set(GOSTEI.map((e) => e.id));
|
|
72
|
+
const ID_NAO = new Set(NAO_QUERO.map((e) => e.id));
|
|
73
|
+
|
|
74
|
+
function arquivo(home = os.homedir()) { return path.join(home, '.primocode', 'gosto-motion.json'); }
|
|
75
|
+
|
|
76
|
+
function texto(s) {
|
|
77
|
+
// tira caracteres de controle (menos nada: quebra de linha vira espaço) e corta
|
|
78
|
+
return String(s == null ? '' : s).replace(/[\u0000-\u001f\u007f]+/g, ' ').replace(/\s+/g, ' ').trim().slice(0, MAX_TEXTO);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function unicos(lista, valido) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const x of Array.isArray(lista) ? lista : []) if (valido.has(x) && !out.includes(x)) out.push(x);
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Aceita só ids conhecidos, corta texto, tira controle. */
|
|
88
|
+
function normalizar(d) {
|
|
89
|
+
const o = d && typeof d === 'object' ? d : {};
|
|
90
|
+
const escolhidos = [];
|
|
91
|
+
for (const e of Array.isArray(o.escolhidos) ? o.escolhidos : []) {
|
|
92
|
+
if (!e || !ID_ESTILO.has(e.id) || escolhidos.some((x) => x.id === e.id)) continue;
|
|
93
|
+
escolhidos.push({ id: e.id, gostei: unicos(e.gostei, ID_GOSTEI), texto: texto(e.texto) });
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
versao: 1,
|
|
97
|
+
quando: typeof o.quando === 'string' && !Number.isNaN(Date.parse(o.quando)) ? o.quando : new Date().toISOString(),
|
|
98
|
+
escolhidos,
|
|
99
|
+
nao_quero: unicos(o.nao_quero, ID_NAO),
|
|
100
|
+
nao_texto: texto(o.nao_texto),
|
|
101
|
+
pulou: !!o.pulou,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ler(home) {
|
|
106
|
+
try { return normalizar(JSON.parse(fs.readFileSync(arquivo(home), 'utf8'))); } catch { return null; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function salvar(dados, home) {
|
|
110
|
+
const n = normalizar(dados);
|
|
111
|
+
const f = arquivo(home);
|
|
112
|
+
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
113
|
+
fs.writeFileSync(f, JSON.stringify(n, null, 2));
|
|
114
|
+
return n;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function tem(home) { return fs.existsSync(arquivo(home)); }
|
|
118
|
+
|
|
119
|
+
/** Multiplicadores pro diretor. Tudo em 1 = neutro. Nunca fixa nada: só inclina o sorteio. */
|
|
120
|
+
function pesos(gosto) {
|
|
121
|
+
const p = { ritmo: { rapido: 1, medio: 1, lento: 1 }, cameras: {}, trocas: {}, tipografias: {}, overshoot: null };
|
|
122
|
+
const mult = (grupo, nome, k, teto = 5) => { p[grupo][nome] = Math.min(teto, (p[grupo][nome] != null ? p[grupo][nome] : 1) * k); };
|
|
123
|
+
const baixa = (grupo, nome, k) => { p[grupo][nome] = (p[grupo][nome] != null ? p[grupo][nome] : 1) * k; };
|
|
124
|
+
if (!gosto || gosto.pulou || !Array.isArray(gosto.escolhidos) || !gosto.escolhidos.length) {
|
|
125
|
+
if (gosto && Array.isArray(gosto.nao_quero) && !gosto.pulou) aplicarNao(p, gosto.nao_quero, baixa);
|
|
126
|
+
return p;
|
|
127
|
+
}
|
|
128
|
+
const chips = new Set();
|
|
129
|
+
let somaOver = 0, nOver = 0;
|
|
130
|
+
for (const esc of gosto.escolhidos) {
|
|
131
|
+
const e = ESTILOS.find((x) => x.id === esc.id);
|
|
132
|
+
if (!e) continue;
|
|
133
|
+
p.ritmo[e.ritmo] *= 1.8;
|
|
134
|
+
e.cameras.forEach((n) => mult('cameras', n, 2.2));
|
|
135
|
+
e.trocas.forEach((n) => mult('trocas', n, 2.2));
|
|
136
|
+
e.tipografias.forEach((n) => mult('tipografias', n, 2.2));
|
|
137
|
+
somaOver += e.overshoot; nOver++;
|
|
138
|
+
(esc.gostei || []).forEach((g) => chips.add(g));
|
|
139
|
+
}
|
|
140
|
+
if (chips.has('rapido')) p.ritmo.rapido *= 1.5;
|
|
141
|
+
if (chips.has('calmo')) { p.ritmo.lento *= 1.5; baixa('trocas', 'tremor', 0.4); baixa('trocas', 'chicote', 0.4); }
|
|
142
|
+
if (chips.has('quique')) { mult('trocas', 'giro', 1.4); mult('trocas', 'encolhe', 1.4); }
|
|
143
|
+
if (chips.has('tres_d')) { mult('cameras', 'orbita', 1.5); mult('cameras', 'cinema', 1.5); mult('trocas', 'giro', 1.4); }
|
|
144
|
+
if (chips.has('camera')) ['pulso', 'chicote', 'orbita'].forEach((n) => mult('cameras', n, 1.3));
|
|
145
|
+
if (chips.has('cortes')) { mult('trocas', 'corte', 1.4); mult('trocas', 'chicote', 1.4); }
|
|
146
|
+
aplicarNao(p, gosto.nao_quero || [], baixa);
|
|
147
|
+
let over = nOver ? somaOver / nOver : null;
|
|
148
|
+
if (over != null) {
|
|
149
|
+
if (chips.has('quique')) over += 0.05;
|
|
150
|
+
if ((gosto.nao_quero || []).includes('exagero')) over -= 0.05;
|
|
151
|
+
over = Math.round(Math.max(0, Math.min(0.3, over)) * 1000) / 1000;
|
|
152
|
+
}
|
|
153
|
+
p.overshoot = over;
|
|
154
|
+
return p;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function aplicarNao(p, nao, baixa) {
|
|
158
|
+
if (nao.includes('lento')) p.ritmo.lento *= 0.3;
|
|
159
|
+
if (nao.includes('liso')) { baixa('trocas', 'dissolve', 0.4); baixa('trocas', 'desfoque', 0.4); }
|
|
160
|
+
if (nao.includes('exagero')) { baixa('trocas', 'tremor', 0.4); baixa('trocas', 'chicote', 0.4); }
|
|
161
|
+
if (nao.includes('tremido')) baixa('trocas', 'tremor', 0.2);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const lista = (a) => (a.length <= 1 ? a.join('') : a.slice(0, -1).join(', ') + ' e ' + a[a.length - 1]);
|
|
165
|
+
|
|
166
|
+
/** Frases em PT-BR pro modelo. Nulo → ''. */
|
|
167
|
+
function resumo(gosto) {
|
|
168
|
+
if (!gosto) return '';
|
|
169
|
+
if (gosto.pulou && !(gosto.nao_quero || []).length) return 'A pessoa pulou a tela de gosto: sem preferência salva.';
|
|
170
|
+
const partes = [];
|
|
171
|
+
const esc = (gosto.escolhidos || []).map((x) => {
|
|
172
|
+
const e = ESTILOS.find((s) => s.id === x.id);
|
|
173
|
+
if (!e) return null;
|
|
174
|
+
const g = (x.gostei || []).map((i) => (GOSTEI.find((c) => c.id === i) || {}).nome).filter(Boolean);
|
|
175
|
+
return g.length ? `${e.nome} (${g.join(', ')})` : e.nome;
|
|
176
|
+
}).filter(Boolean);
|
|
177
|
+
if (esc.length) partes.push(`Gosto salvo da pessoa: gostou de ${lista(esc)}.`);
|
|
178
|
+
const nao = (gosto.nao_quero || []).map((i) => (NAO_QUERO.find((c) => c.id === i) || {}).curto).filter(Boolean);
|
|
179
|
+
if (nao.length) partes.push(`Não quer: ${nao.join(', ')}.`);
|
|
180
|
+
const txt = [(gosto.escolhidos || []).map((x) => x.texto).filter(Boolean).join(' / '), gosto.nao_texto ? 'não quer: ' + gosto.nao_texto : ''].filter(Boolean).join(' · ');
|
|
181
|
+
if (txt) partes.push(`Escreveu: "${txt}".`);
|
|
182
|
+
if (!partes.length) return '';
|
|
183
|
+
partes.push('Use como direção, não como molde.');
|
|
184
|
+
return partes.join(' ');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Só pergunta no primeiro pedido, com gente do outro lado: nunca em -p, CI ou teste. */
|
|
188
|
+
function deveAbrir(args = {}, env = process.env, home) {
|
|
189
|
+
return !tem(home) && !(args && args.gosto === false) && !env.PRIMOCODE_SEM_TELA && !env.CI && !!process.stdout.isTTY;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function lerCorpo(req, limite = 20 * 1024) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
let tam = 0; const partes = [];
|
|
195
|
+
req.on('data', (c) => {
|
|
196
|
+
tam += c.length;
|
|
197
|
+
if (tam > limite) { reject(new Error('grande')); req.destroy(); return; }
|
|
198
|
+
partes.push(c);
|
|
199
|
+
});
|
|
200
|
+
req.on('end', () => resolve(Buffer.concat(partes).toString('utf8')));
|
|
201
|
+
req.on('error', reject);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Abre a tela no navegador. Resolve {ok:true, gosto} (salvou), {ok:false, motivo:'pulou'|'tempo'|'sem_tela'}.
|
|
207
|
+
* `abrir(url)` e `home` são injetáveis (testes).
|
|
208
|
+
*/
|
|
209
|
+
function abrirTela({ home, abrir, timeoutMs = 10 * 60 * 1000, porta = 0 } = {}) {
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
212
|
+
let html;
|
|
213
|
+
try { html = fs.readFileSync(path.join(__dirname, 'gosto', 'tela.html'), 'utf8'); } catch { return resolve({ ok: false, motivo: 'sem_tela' }); }
|
|
214
|
+
const dados = JSON.stringify({ ESTILOS, GOSTEI, NAO_QUERO }).replace(/</g, '\\u003c');
|
|
215
|
+
let fim = false, timer = null;
|
|
216
|
+
const srv = http.createServer(async (req, res) => {
|
|
217
|
+
const u = new URL(req.url, 'http://127.0.0.1');
|
|
218
|
+
const dizer = (cod, corpo, tipo = 'application/json; charset=utf-8') => { res.writeHead(cod, { 'content-type': tipo, 'cache-control': 'no-store' }); res.end(typeof corpo === 'string' ? corpo : JSON.stringify(corpo)); };
|
|
219
|
+
try {
|
|
220
|
+
if (req.method === 'GET' && u.pathname === '/') {
|
|
221
|
+
if (u.searchParams.get('t') !== token) return dizer(403, 'token inválido', 'text/plain; charset=utf-8');
|
|
222
|
+
return dizer(200, html.split('__TOKEN__').join(token).split('__DADOS__').join(dados), 'text/html; charset=utf-8');
|
|
223
|
+
}
|
|
224
|
+
if (req.method === 'POST' && (u.pathname === '/salvar' || u.pathname === '/pular')) {
|
|
225
|
+
if (req.headers['x-gosto-token'] !== token) return dizer(403, { ok: false, error: 'token inválido' });
|
|
226
|
+
if (u.pathname === '/pular') {
|
|
227
|
+
salvar({ pulou: true }, home);
|
|
228
|
+
dizer(200, { ok: true });
|
|
229
|
+
return terminar({ ok: false, motivo: 'pulou' });
|
|
230
|
+
}
|
|
231
|
+
let corpo;
|
|
232
|
+
try { corpo = JSON.parse(await lerCorpo(req)); } catch { return dizer(400, { ok: false, error: 'corpo inválido (JSON até 20 KB)' }); }
|
|
233
|
+
const g = salvar(corpo, home);
|
|
234
|
+
dizer(200, { ok: true, resumo: resumo(g) });
|
|
235
|
+
return terminar({ ok: true, gosto: g });
|
|
236
|
+
}
|
|
237
|
+
dizer(404, 'não achei', 'text/plain; charset=utf-8');
|
|
238
|
+
} catch (e) { try { dizer(500, { ok: false, error: String(e.message || e) }); } catch { /* já respondeu */ } }
|
|
239
|
+
});
|
|
240
|
+
function terminar(r) {
|
|
241
|
+
if (fim) return;
|
|
242
|
+
fim = true;
|
|
243
|
+
clearTimeout(timer);
|
|
244
|
+
try { srv.close(); } catch { /* já fechou */ }
|
|
245
|
+
// deixa a resposta sair e depois derruba o que sobrar de conexão aberta (keep-alive do navegador)
|
|
246
|
+
const t = setTimeout(() => { try { if (srv.closeAllConnections) srv.closeAllConnections(); } catch { /* ok */ } }, 300);
|
|
247
|
+
if (t.unref) t.unref();
|
|
248
|
+
resolve(r);
|
|
249
|
+
}
|
|
250
|
+
srv.on('error', () => terminar({ ok: false, motivo: 'sem_tela' }));
|
|
251
|
+
srv.listen(porta, '127.0.0.1', () => {
|
|
252
|
+
timer = setTimeout(() => terminar({ ok: false, motivo: 'tempo' }), timeoutMs);
|
|
253
|
+
const url = `http://127.0.0.1:${srv.address().port}/?t=${token}`;
|
|
254
|
+
try {
|
|
255
|
+
const r = (abrir || ((x) => require('../tools.js').openInBrowser(x)))(url);
|
|
256
|
+
if (r && typeof r.catch === 'function') r.catch(() => {});
|
|
257
|
+
} catch { /* sem navegador: a pessoa espera o tempo ou pula */ }
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const _teste = { texto, MAX_TEXTO, aplicarNao };
|
|
263
|
+
|
|
264
|
+
module.exports = { ESTILOS, GOSTEI, NAO_QUERO, arquivo, ler, salvar, tem, normalizar, pesos, resumo, abrirTela, deveAbrir, _teste };
|
package/lib/estudio/index.js
CHANGED
|
@@ -718,6 +718,12 @@ async function ferramentaMotion(args) {
|
|
|
718
718
|
if (r.bloqueado) return { ok: false, error: r.aviso, projeto: r.projeto };
|
|
719
719
|
receita = r.projeto;
|
|
720
720
|
}
|
|
721
|
+
// primeiro motion: aprende o gosto antes de renderizar (a pessoa pode fechar ou pular; o render segue de qualquer jeito)
|
|
722
|
+
const gosto = require('./gosto.js');
|
|
723
|
+
if (gosto.deveAbrir(args)) {
|
|
724
|
+
try { console.error(require('../ui.js').c.warn(' ⎿ Antes do primeiro motion: abri uma tela pra aprender o seu gosto (até 10 min). Feche ou pule pra seguir.')); } catch { /* sem terminal */ }
|
|
725
|
+
try { await gosto.abrirTela(); } catch { /* sem tela, segue */ }
|
|
726
|
+
}
|
|
721
727
|
return require('./cota.js').comCota(() => renderMotion(args, p, receita));
|
|
722
728
|
}
|
|
723
729
|
|
|
@@ -734,7 +740,9 @@ async function renderMotion(args, p, receita) {
|
|
|
734
740
|
const r = await require('./maquina.js').motion(args, { pasta: p.pasta, receita, deps: { narrar } });
|
|
735
741
|
if (r.ok) projeto.anotar(p.id, 'motion', `${r.duracao}s, ${r.cenas.length} cenas, ${r.lufs} LUFS`);
|
|
736
742
|
if (r.ok) entregar(args, p, r);
|
|
737
|
-
|
|
743
|
+
if (!r.ok) return r;
|
|
744
|
+
const gostoResumo = (() => { try { const G = require('./gosto.js'); return G.resumo(G.ler()); } catch { return ''; } })();
|
|
745
|
+
return { ...r, ...(gostoResumo ? { gosto: gostoResumo } : {}), nota: 'Abra os quadros (png em ae/) para conferir. Se algo ficou torto, mude o JSON e rode de novo.' };
|
|
738
746
|
}
|
|
739
747
|
|
|
740
748
|
// ── estudio_editar ───────────────────────────────────────────────────────
|
|
@@ -311,8 +311,11 @@ function dirigir(args = {}, { historico = lerHistorico(), semente = null, temEle
|
|
|
311
311
|
function montarDirecao({ args, assunto, meta, duracao, hist, rnd, temElemento }) {
|
|
312
312
|
const ultimo = hist[hist.length - 1];
|
|
313
313
|
const ctx = { assunto, meta, hist, historico: hist, usados: new Set(), ultimaFam: null, temElemento };
|
|
314
|
+
// o gosto da pessoa (gosto.js: pesos) só MULTIPLICA o sorteio; histórico, "nao", metáfora e anti-repetição seguem valendo
|
|
315
|
+
const G = args.gosto && typeof args.gosto === 'object' ? args.gosto : null;
|
|
316
|
+
const gm = (grupo, nome) => { const v = G && G[grupo] && G[grupo][nome]; return Number.isFinite(v) && v >= 0 ? v : 1; };
|
|
314
317
|
// ritmo: duração média por cena
|
|
315
|
-
const ritmo = args.ritmo || sortear(rnd, [['rapido', 1], ['medio', 1.4], ['lento', 0.8]].map(([r, p]) => [r, p * (ultimo && ultimo.ritmo === r ? 0.4 : 1)]));
|
|
318
|
+
const ritmo = args.ritmo || sortear(rnd, [['rapido', 1], ['medio', 1.4], ['lento', 0.8]].map(([r, p]) => [r, p * gm('ritmo', r) * (ultimo && ultimo.ritmo === r ? 0.4 : 1)]));
|
|
316
319
|
const media = ritmo === 'rapido' ? 2.5 : ritmo === 'lento' ? 3.5 : 3.0; // cena de 2,5 a 3,5 s: o corte seco da câmera dá o pulso a cada ~1 s
|
|
317
320
|
|
|
318
321
|
// as cenas pedidas (ou os papéis sorteados)
|
|
@@ -360,20 +363,20 @@ function montarDirecao({ args, assunto, meta, duracao, hist, rnd, temElemento })
|
|
|
360
363
|
let t = Array.isArray(pedidasT) && TRANSICOES.includes(pedidasT[i - 1]) ? pedidasT[i - 1] : null;
|
|
361
364
|
if (!t) {
|
|
362
365
|
if (out[i - 1].tipo === 'mergulho') t = rnd() < 0.5 ? 'corte' : 'dissolve';
|
|
363
|
-
else t = sortear(rnd, TRANSICOES.map((x) => [x, (['dissolve', 'desfoque', 'encolhe', 'mascara', 'abre', 'cortina'].includes(x) ? 0.4 : 1.4) * (meta.trocas.includes(x) ? 3 : 1) * Math.pow(0.6, recT.get(x) || 0) * (x === trocas[trocas.length - 1] ? 0 : 1) * (trocas.slice(-3).includes(x) ? 0.3 : 1)]));
|
|
366
|
+
else t = sortear(rnd, TRANSICOES.map((x) => [x, (['dissolve', 'desfoque', 'encolhe', 'mascara', 'abre', 'cortina'].includes(x) ? 0.4 : 1.4) * (meta.trocas.includes(x) ? 3 : 1) * Math.pow(0.6, recT.get(x) || 0) * (x === trocas[trocas.length - 1] ? 0 : 1) * (trocas.slice(-3).includes(x) ? 0.3 : 1) * gm('trocas', x)]));
|
|
364
367
|
}
|
|
365
368
|
trocas.push(t);
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
// câmera: um estilo principal + um secundário em algumas cenas
|
|
369
372
|
const recC = new Map(); hist.slice(-3).forEach((h) => recC.set(h.camera, (recC.get(h.camera) || 0) + 1));
|
|
370
|
-
const camera = CAMERAS.includes(args.camera) ? args.camera : sortear(rnd, CAMERAS.map((x) => [x, (meta.cameras.includes(x) ? 3 : 1) * Math.pow(0.45, recC.get(x) || 0)]));
|
|
371
|
-
const camera2 = sortear(rnd, CAMERAS.filter((x) => x !== camera).map((x) => [x, meta.cameras.includes(x) ? 2 : 1]));
|
|
373
|
+
const camera = CAMERAS.includes(args.camera) ? args.camera : sortear(rnd, CAMERAS.map((x) => [x, (meta.cameras.includes(x) ? 3 : 1) * Math.pow(0.45, recC.get(x) || 0) * gm('cameras', x)]));
|
|
374
|
+
const camera2 = sortear(rnd, CAMERAS.filter((x) => x !== camera).map((x) => [x, (meta.cameras.includes(x) ? 2 : 1) * gm('cameras', x)]));
|
|
372
375
|
out.forEach((c, i) => { c.camera = c.camera && CAMERAS.includes(c.camera) ? c.camera : (i % 3 === 2 ? camera2 : camera); });
|
|
373
376
|
|
|
374
377
|
// tipografia
|
|
375
378
|
const recTi = new Map(); hist.slice(-3).forEach((h) => recTi.set(h.tipografia, (recTi.get(h.tipografia) || 0) + 1));
|
|
376
|
-
const tipografia = TIPOGRAFIAS[args.tipografia] ? args.tipografia : sortear(rnd, Object.keys(TIPOGRAFIAS).map((x) => [x, (meta.tipografias.includes(x) ? 3 : 0.8) * Math.pow(0.4, recTi.get(x) || 0)]));
|
|
379
|
+
const tipografia = TIPOGRAFIAS[args.tipografia] ? args.tipografia : sortear(rnd, Object.keys(TIPOGRAFIAS).map((x) => [x, (meta.tipografias.includes(x) ? 3 : 0.8) * Math.pow(0.4, recTi.get(x) || 0) * gm('tipografias', x)]));
|
|
377
380
|
|
|
378
381
|
// paleta
|
|
379
382
|
const recP = new Map(); hist.slice(-3).forEach((h) => recP.set(h.paleta, (recP.get(h.paleta) || 0) + 1));
|
|
@@ -384,6 +387,7 @@ function montarDirecao({ args, assunto, meta, duracao, hist, rnd, temElemento })
|
|
|
384
387
|
metafora: meta.nome, chave: meta.chave, assunto, primitivas: out.map((c) => c.tipo), trocas, camera, camera2, tipografia, ritmo, paleta,
|
|
385
388
|
molde: papeisUsados.join(','), cenas: out.length, tipos_unicos: new Set(out.map((c) => c.tipo)).size,
|
|
386
389
|
};
|
|
390
|
+
if (G) assinatura.gosto = true;
|
|
387
391
|
return { metafora: meta, cenas: out, trocas, camera, tipografia, ritmo, paleta, ordemPal, assinatura };
|
|
388
392
|
}
|
|
389
393
|
|
package/lib/estudio/maquina.js
CHANGED
|
@@ -375,7 +375,10 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
375
375
|
const elementos = (el && el.elementos) || [];
|
|
376
376
|
|
|
377
377
|
// 2. a direção (primitivas, transições, câmera, tipografia, paleta) com semente e conferência de assinatura
|
|
378
|
-
|
|
378
|
+
// o gosto salvo da pessoa entra como PESO no sorteio do diretor (não como molde)
|
|
379
|
+
const G = require('./gosto.js');
|
|
380
|
+
const gostoP = deps.gosto !== undefined ? deps.gosto : G.pesos(G.ler());
|
|
381
|
+
let pl = plano({ ...args, assunto, gosto: gostoP }, { receita, historico, elementos });
|
|
379
382
|
const avisosDeMotor = [];
|
|
380
383
|
if (elementos.length < 3) avisosDeMotor.push(`só ${elementos.length} elemento(s) real(is) do assunto (o pedido é ≥ 3)`);
|
|
381
384
|
|
|
@@ -508,6 +511,7 @@ async function motion(args = {}, { pasta, receita = null, deps = {} } = {}) {
|
|
|
508
511
|
voz: !!voz, vozMotor: voz && voz.motor, vozMedida: voz && voz.medido, conferencia, receita: pl.receita, projeto: r.projeto, assinatura,
|
|
509
512
|
elementos: elementos.map((e) => ({ nome: e.nome, tipo: e.tipo, licenca: e.licenca })), creditos: (el && el.creditos) || [],
|
|
510
513
|
avisos: avisosDeMotor,
|
|
514
|
+
...(gostoP && gostoP.overshoot != null ? { gosto: { overshoot: gostoP.overshoot } } : {}),
|
|
511
515
|
_exportar: { palavras: voz ? voz.palavras : [], sfx: sons.filter((x) => x.arquivo).map((x) => x.arquivo), cenas: pl.cenas.map((c) => ({ papel: c.papel, titulo: c.titulo, texto: c.texto, fala: c.fala, botao: c.botao, dur: c.dur })), trilhaArquivo: trilha && trilha.arquivo },
|
|
512
516
|
...(mix.ok || args.previa ? {} : { aviso: 'o vídeo saiu sem a mixagem de áudio: ' + (mix.error || 'sem sons') }),
|
|
513
517
|
};
|
package/lib/headless.js
CHANGED
|
@@ -64,8 +64,20 @@ async function capturar(fn, { calar = false } = {}) {
|
|
|
64
64
|
return texto.replace(/\n{3,}/g, '\n\n').trim();
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function resultadoJson({ texto, erro, inicio, permissao }) {
|
|
68
|
-
return JSON.stringify({ type: 'result', is_error: !!erro, result: erro ? String(erro) : texto, duration_ms: Date.now() - inicio, permission_mode: permissao || 'auto' });
|
|
67
|
+
function resultadoJson({ texto, erro, inicio, permissao, conversa, markdown }) {
|
|
68
|
+
return JSON.stringify({ type: 'result', is_error: !!erro, result: erro ? String(erro) : texto, duration_ms: Date.now() - inicio, permission_mode: permissao || 'auto', conversa: conversa || null, markdown: erro ? null : (markdown || null) });
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
/* A última resposta do agente como ele escreveu (markdown cru), sem o que o terminal desenha em volta. */
|
|
72
|
+
function ultimaResposta(historico, desde = 0) {
|
|
73
|
+
for (let i = (historico || []).length - 1; i >= desde; i--) {
|
|
74
|
+
const m = historico[i];
|
|
75
|
+
if (!m || m.role !== 'assistant') continue;
|
|
76
|
+
const t = typeof m.content === 'string' ? m.content
|
|
77
|
+
: Array.isArray(m.content) ? m.content.map((p) => (typeof p === 'string' ? p : (p && p.type === 'text' && p.text) || '')).join('\n') : '';
|
|
78
|
+
if (t.trim()) return t.trim();
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { analisar, montarPedido, lerStdin, capturar, resultadoJson, ultimaResposta, FORMATOS };
|
package/lib/local/cerebro.json
CHANGED
|
@@ -1390,6 +1390,52 @@
|
|
|
1390
1390
|
}
|
|
1391
1391
|
}
|
|
1392
1392
|
},
|
|
1393
|
+
{
|
|
1394
|
+
"type": "function",
|
|
1395
|
+
"function": {
|
|
1396
|
+
"name": "lembrar",
|
|
1397
|
+
"description": "Guarda ou apaga uma memoria que dura entre sessoes (arquivos em ~/.primocode/memoria/). acao \"salvar\": nome (kebab-case curto; o mesmo nome atualiza, sem duplicar), descricao (uma linha), tipo, texto. acao \"apagar\": so o nome. Nunca guarde chave, token ou senha.",
|
|
1398
|
+
"parameters": {
|
|
1399
|
+
"type": "object",
|
|
1400
|
+
"properties": {
|
|
1401
|
+
"acao": {
|
|
1402
|
+
"type": "string",
|
|
1403
|
+
"enum": [
|
|
1404
|
+
"salvar",
|
|
1405
|
+
"apagar"
|
|
1406
|
+
],
|
|
1407
|
+
"description": "salvar ou apagar"
|
|
1408
|
+
},
|
|
1409
|
+
"nome": {
|
|
1410
|
+
"type": "string",
|
|
1411
|
+
"description": "Nome em kebab-case, ex: prefere-respostas-curtas"
|
|
1412
|
+
},
|
|
1413
|
+
"descricao": {
|
|
1414
|
+
"type": "string",
|
|
1415
|
+
"description": "Uma linha dizendo do que se trata (aparece no indice)"
|
|
1416
|
+
},
|
|
1417
|
+
"tipo": {
|
|
1418
|
+
"type": "string",
|
|
1419
|
+
"enum": [
|
|
1420
|
+
"usuario",
|
|
1421
|
+
"feedback",
|
|
1422
|
+
"projeto",
|
|
1423
|
+
"referencia"
|
|
1424
|
+
],
|
|
1425
|
+
"description": "usuario: quem e; feedback: correcao ou preferencia; projeto: fato do projeto; referencia: onde achar algo"
|
|
1426
|
+
},
|
|
1427
|
+
"texto": {
|
|
1428
|
+
"type": "string",
|
|
1429
|
+
"description": "O fato, com o porque quando houver"
|
|
1430
|
+
}
|
|
1431
|
+
},
|
|
1432
|
+
"required": [
|
|
1433
|
+
"acao",
|
|
1434
|
+
"nome"
|
|
1435
|
+
]
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
},
|
|
1393
1439
|
{
|
|
1394
1440
|
"type": "function",
|
|
1395
1441
|
"function": {
|
|
@@ -2839,6 +2885,52 @@
|
|
|
2839
2885
|
}
|
|
2840
2886
|
}
|
|
2841
2887
|
},
|
|
2888
|
+
{
|
|
2889
|
+
"type": "function",
|
|
2890
|
+
"function": {
|
|
2891
|
+
"name": "lembrar",
|
|
2892
|
+
"description": "Guarda ou apaga uma memoria que dura entre sessoes (arquivos em ~/.primocode/memoria/). acao \"salvar\": nome (kebab-case curto; o mesmo nome atualiza, sem duplicar), descricao (uma linha), tipo, texto. acao \"apagar\": so o nome. Nunca guarde chave, token ou senha.",
|
|
2893
|
+
"parameters": {
|
|
2894
|
+
"type": "object",
|
|
2895
|
+
"properties": {
|
|
2896
|
+
"acao": {
|
|
2897
|
+
"type": "string",
|
|
2898
|
+
"enum": [
|
|
2899
|
+
"salvar",
|
|
2900
|
+
"apagar"
|
|
2901
|
+
],
|
|
2902
|
+
"description": "salvar ou apagar"
|
|
2903
|
+
},
|
|
2904
|
+
"nome": {
|
|
2905
|
+
"type": "string",
|
|
2906
|
+
"description": "Nome em kebab-case, ex: prefere-respostas-curtas"
|
|
2907
|
+
},
|
|
2908
|
+
"descricao": {
|
|
2909
|
+
"type": "string",
|
|
2910
|
+
"description": "Uma linha dizendo do que se trata (aparece no indice)"
|
|
2911
|
+
},
|
|
2912
|
+
"tipo": {
|
|
2913
|
+
"type": "string",
|
|
2914
|
+
"enum": [
|
|
2915
|
+
"usuario",
|
|
2916
|
+
"feedback",
|
|
2917
|
+
"projeto",
|
|
2918
|
+
"referencia"
|
|
2919
|
+
],
|
|
2920
|
+
"description": "usuario: quem e; feedback: correcao ou preferencia; projeto: fato do projeto; referencia: onde achar algo"
|
|
2921
|
+
},
|
|
2922
|
+
"texto": {
|
|
2923
|
+
"type": "string",
|
|
2924
|
+
"description": "O fato, com o porque quando houver"
|
|
2925
|
+
}
|
|
2926
|
+
},
|
|
2927
|
+
"required": [
|
|
2928
|
+
"acao",
|
|
2929
|
+
"nome"
|
|
2930
|
+
]
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
},
|
|
2842
2934
|
{
|
|
2843
2935
|
"type": "function",
|
|
2844
2936
|
"function": {
|
|
@@ -2857,4 +2949,4 @@
|
|
|
2857
2949
|
}
|
|
2858
2950
|
}
|
|
2859
2951
|
]
|
|
2860
|
-
}
|
|
2952
|
+
}
|