primocode 8.25.0 → 8.27.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/app/app.css +230 -0
- package/app/app.js +273 -0
- package/app/icone.svg +6 -0
- package/app/index.html +68 -0
- package/bin/primocode.js +52 -0
- package/lib/app.js +334 -0
- package/package.json +3 -2
- package/studio/ferramentas/00-protocolo.md +2 -1
- package/studio/ferramentas/01-modelos.md +3 -2
- package/studio/ferramentas/corte.md +81 -0
- package/studio/server.py +9 -3
- package/studio/web/js/ferramentas.js +2 -2
- package/studio/web/js/render.js +219 -5
- package/studio/web/render.css +277 -0
package/lib/app.js
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app.js — o PrimoCode como aplicativo de janela.
|
|
3
|
+
*
|
|
4
|
+
* "Se mandar /app pra ele, ele abre o aplicativo que você vai desenvolver
|
|
5
|
+
* para o Mac e o Windows, já instala junto com o npm. Vai ser um aplicativo
|
|
6
|
+
* desktop bem bonito com as cores do PrimoCode, com aquele azul. Bem estilo
|
|
7
|
+
* Codex. Vai ter o chat em localhost. Um chat estilo terminal para quem não
|
|
8
|
+
* quer o terminal."
|
|
9
|
+
*
|
|
10
|
+
* ── POR QUE NÃO É ELECTRON ───────────────────────────────────────────────
|
|
11
|
+
* O PrimoCode tem ZERO dependência de npm, e isso não é purismo: é o que faz
|
|
12
|
+
* `npm i -g primocode` levar segundos em vez de baixar 200 MB de navegador
|
|
13
|
+
* empacotado. Electron levaria os dois embora — a promessa de "já instala
|
|
14
|
+
* junto com o npm" e a instalação leve.
|
|
15
|
+
*
|
|
16
|
+
* O que Electron entrega, no fundo, é uma janela sem barra de endereço
|
|
17
|
+
* mostrando uma página local. Chrome, Edge e Brave fazem exatamente isso com
|
|
18
|
+
* `--app=<url>`: janela limpa, ícone próprio na barra de tarefas, sem abas.
|
|
19
|
+
* O navegador já está na máquina do usuário — no Windows o Edge vem com o
|
|
20
|
+
* sistema, no Mac o Safari (e quase sempre o Chrome).
|
|
21
|
+
*
|
|
22
|
+
* Então: servidor HTTP local (http do Node, nada instalado) + janela do
|
|
23
|
+
* navegador em modo app. Se não houver nenhum navegador de Chromium, abre
|
|
24
|
+
* numa aba normal, que continua funcionando.
|
|
25
|
+
*
|
|
26
|
+
* ── POR QUE ELE ESPELHA O TERMINAL ───────────────────────────────────────
|
|
27
|
+
* O pedido foi "um chat estilo terminal para quem não quer o terminal" — e o
|
|
28
|
+
* agente já conta o que faz, linha por linha, com cor. Reescrever essa saída
|
|
29
|
+
* para a web daria duas verdades para manter, e uma delas ficaria velha.
|
|
30
|
+
*
|
|
31
|
+
* Aqui a saída é a MESMA: o `process.stdout.write` é espelhado, os códigos
|
|
32
|
+
* ANSI viram cor de verdade na página, e o terminal continua imprimindo
|
|
33
|
+
* normalmente. Quem abrir os dois vê a mesma coisa nos dois.
|
|
34
|
+
*
|
|
35
|
+
* ── SEGURANÇA ────────────────────────────────────────────────────────────
|
|
36
|
+
* Este servidor MANDA NO AGENTE: quem fala com ele executa comando, escreve
|
|
37
|
+
* arquivo e gasta a cota do usuário. Por isso ele só escuta em 127.0.0.1 e
|
|
38
|
+
* exige uma chave sorteada a cada abertura — sem ela, uma página qualquer
|
|
39
|
+
* aberta no mesmo navegador dispararia pedidos para cá.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
'use strict';
|
|
43
|
+
|
|
44
|
+
const http = require('http');
|
|
45
|
+
const fs = require('fs');
|
|
46
|
+
const os = require('os');
|
|
47
|
+
const path = require('path');
|
|
48
|
+
const crypto = require('crypto');
|
|
49
|
+
const { execFile } = require('child_process');
|
|
50
|
+
|
|
51
|
+
const WEB = path.join(__dirname, '..', 'app');
|
|
52
|
+
const PORTA_PADRAO = Number(process.env.PRIMOCODE_APP_PORT) || 7788;
|
|
53
|
+
|
|
54
|
+
// ── espelho da saída ─────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/* Duplica tudo que vai para o terminal, sem tirar nada de lá.
|
|
57
|
+
*
|
|
58
|
+
* O `write` original continua sendo chamado com os mesmos argumentos e o mesmo
|
|
59
|
+
* retorno: se este espelho mudasse o valor de volta, o Node passaria a achar
|
|
60
|
+
* que o buffer encheu (ou que não encheu) e a saída sairia picada. */
|
|
61
|
+
function espelhar(aoEscrever) {
|
|
62
|
+
const real = process.stdout.write.bind(process.stdout);
|
|
63
|
+
process.stdout.write = function (pedaco, enc, cb) {
|
|
64
|
+
try {
|
|
65
|
+
aoEscrever(typeof pedaco === 'string' ? pedaco : Buffer.from(pedaco).toString('utf8'));
|
|
66
|
+
} catch { /* o espelho nunca pode derrubar a saída */ }
|
|
67
|
+
return real(pedaco, enc, cb);
|
|
68
|
+
};
|
|
69
|
+
return function parar() { process.stdout.write = real; };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* O prompt do readline e os quadros do rodinha não são conteúdo: são a
|
|
73
|
+
interface do TERMINAL se redesenhando. Na página eles viravam lixo piscando
|
|
74
|
+
no meio da conversa. O jeito de reconhecê-los é o que sobra depois de tirar
|
|
75
|
+
o ANSI: pedaço curto e sem quebra de linha é desenho, não texto. */
|
|
76
|
+
function ehRuido(texto) {
|
|
77
|
+
const limpo = texto.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').replace(/\r/g, '');
|
|
78
|
+
if (limpo.includes('\n')) return false;
|
|
79
|
+
return limpo.trim().length < 12;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── o servidor ───────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function tipoDe(arquivo) {
|
|
85
|
+
if (arquivo.endsWith('.html')) return 'text/html; charset=utf-8';
|
|
86
|
+
if (arquivo.endsWith('.css')) return 'text/css; charset=utf-8';
|
|
87
|
+
if (arquivo.endsWith('.js')) return 'application/javascript; charset=utf-8';
|
|
88
|
+
if (arquivo.endsWith('.svg')) return 'image/svg+xml';
|
|
89
|
+
return 'application/octet-stream';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sobe a janela do PrimoCode.
|
|
94
|
+
*
|
|
95
|
+
* @param {object} op
|
|
96
|
+
* @param {(texto:string)=>Promise} op.enviar manda um pedido ao agente
|
|
97
|
+
* @param {(texto:string)=>Promise} op.barra trata um comando /barra
|
|
98
|
+
* @param {()=>void} op.interromper para o que estiver rodando
|
|
99
|
+
* @param {()=>object} op.estado { pasta, modo, esforco, projeto }
|
|
100
|
+
* @param {string} op.versao
|
|
101
|
+
*/
|
|
102
|
+
async function abrir(op) {
|
|
103
|
+
const chave = crypto.randomBytes(16).toString('hex');
|
|
104
|
+
const ouvintes = new Set();
|
|
105
|
+
let ocupado = false;
|
|
106
|
+
const fila = [];
|
|
107
|
+
|
|
108
|
+
// O histórico de saída para quem abrir a janela DEPOIS de o agente já ter
|
|
109
|
+
// começado — sem isto a página nasceria vazia no meio de uma tarefa.
|
|
110
|
+
const memoria = [];
|
|
111
|
+
const MEMORIA_MAX = 400;
|
|
112
|
+
|
|
113
|
+
function emitir(evento, dados) {
|
|
114
|
+
const linha = `event: ${evento}\ndata: ${JSON.stringify(dados)}\n\n`;
|
|
115
|
+
for (const res of ouvintes) {
|
|
116
|
+
try { res.write(linha); } catch { ouvintes.delete(res); }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pararEspelho = espelhar((pedaco) => {
|
|
121
|
+
if (ehRuido(pedaco)) return;
|
|
122
|
+
memoria.push(pedaco);
|
|
123
|
+
if (memoria.length > MEMORIA_MAX) memoria.shift();
|
|
124
|
+
emitir('saida', pedaco);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
async function processar(texto) {
|
|
128
|
+
fila.push(texto);
|
|
129
|
+
if (ocupado) return;
|
|
130
|
+
ocupado = true;
|
|
131
|
+
emitir('ocupado', true);
|
|
132
|
+
while (fila.length) {
|
|
133
|
+
const t = String(fila.shift() || '').trim();
|
|
134
|
+
if (!t) continue;
|
|
135
|
+
emitir('voce', t);
|
|
136
|
+
try {
|
|
137
|
+
if (t.startsWith('/')) await op.barra(t);
|
|
138
|
+
else await op.enviar(t);
|
|
139
|
+
} catch (e) {
|
|
140
|
+
emitir('erro', (e && e.message) || String(e));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
ocupado = false;
|
|
144
|
+
emitir('ocupado', false);
|
|
145
|
+
emitir('estado', op.estado());
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const servidor = http.createServer((req, res) => {
|
|
149
|
+
const url = new URL(req.url, 'http://127.0.0.1');
|
|
150
|
+
const rota = url.pathname;
|
|
151
|
+
|
|
152
|
+
/* A chave protege a API, que é onde está tudo o que importa: a conversa
|
|
153
|
+
do usuário e a capacidade de dar ordens ao agente. A página em si —
|
|
154
|
+
HTML, CSS, JS — não guarda segredo nenhum e não faz nada sem a
|
|
155
|
+
chave, então ela é servida solta.
|
|
156
|
+
|
|
157
|
+
Não é frouxidão, é o contrário: exigir chave nos estáticos também
|
|
158
|
+
quebrava a própria janela, porque o navegador pede /app.css e
|
|
159
|
+
/app.js sem parâmetro nenhum. A página vinha, ficava sem estilo e
|
|
160
|
+
sem script, e o "app bonito" era um formulário branco. */
|
|
161
|
+
if (rota.startsWith('/api/')) {
|
|
162
|
+
const dada = url.searchParams.get('k') || req.headers['x-primo-chave'];
|
|
163
|
+
if (dada !== chave) {
|
|
164
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
165
|
+
return res.end('chave inválida — abra a janela com o comando /app');
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/* CSRF: uma página de terceiro consegue disparar POST simples para
|
|
169
|
+
127.0.0.1 sem preflight. Ela não saberia a chave, mas o cinto e o
|
|
170
|
+
suspensório custam três linhas. */
|
|
171
|
+
const origem = req.headers.origin;
|
|
172
|
+
if (origem && !origem.startsWith(`http://127.0.0.1:${servidor.__porta}`)
|
|
173
|
+
&& !origem.startsWith(`http://localhost:${servidor.__porta}`)) {
|
|
174
|
+
res.writeHead(403).end();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (rota === '/api/fluxo') {
|
|
179
|
+
res.writeHead(200, {
|
|
180
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
181
|
+
'Cache-Control': 'no-cache',
|
|
182
|
+
Connection: 'keep-alive',
|
|
183
|
+
// Sem isto um proxy no meio segura os eventos até fechar.
|
|
184
|
+
'X-Accel-Buffering': 'no',
|
|
185
|
+
});
|
|
186
|
+
res.write(': ligado\n\n');
|
|
187
|
+
ouvintes.add(res);
|
|
188
|
+
res.write(`event: estado\ndata: ${JSON.stringify(op.estado())}\n\n`);
|
|
189
|
+
for (const pedaco of memoria) {
|
|
190
|
+
res.write(`event: saida\ndata: ${JSON.stringify(pedaco)}\n\n`);
|
|
191
|
+
}
|
|
192
|
+
res.write(`event: ocupado\ndata: ${JSON.stringify(ocupado)}\n\n`);
|
|
193
|
+
// Batida a cada 20s: proxy e sistema derrubam conexão parada, e
|
|
194
|
+
// uma janela que "morre sozinha depois de um tempo" seria um bug
|
|
195
|
+
// impossível de achar depois.
|
|
196
|
+
const batida = setInterval(() => { try { res.write(': ok\n\n'); } catch {} }, 20000);
|
|
197
|
+
req.on('close', () => { clearInterval(batida); ouvintes.delete(res); });
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (rota === '/api/enviar' && req.method === 'POST') {
|
|
202
|
+
let corpo = '';
|
|
203
|
+
req.on('data', (c) => { corpo += c; if (corpo.length > 200000) req.destroy(); });
|
|
204
|
+
req.on('end', () => {
|
|
205
|
+
let texto = '';
|
|
206
|
+
try { texto = JSON.parse(corpo).texto || ''; } catch { /* vazio */ }
|
|
207
|
+
res.writeHead(200, { 'Content-Type': 'application/json' }).end('{"ok":true}');
|
|
208
|
+
if (texto.trim()) processar(texto);
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (rota === '/api/parar' && req.method === 'POST') {
|
|
214
|
+
try { op.interromper(); } catch {}
|
|
215
|
+
res.writeHead(200, { 'Content-Type': 'application/json' }).end('{"ok":true}');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (rota === '/api/estado') {
|
|
220
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
|
|
221
|
+
return res.end(JSON.stringify(Object.assign({ versao: op.versao }, op.estado())));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Estáticos. `basename` corta qualquer "../" antes de virar caminho:
|
|
225
|
+
// este servidor fala com o disco do usuário e não pode servir o que
|
|
226
|
+
// estiver fora da pasta do aplicativo.
|
|
227
|
+
const nome = rota === '/' ? 'index.html' : path.basename(rota);
|
|
228
|
+
const arquivo = path.join(WEB, nome);
|
|
229
|
+
fs.readFile(arquivo, (erro, dados) => {
|
|
230
|
+
if (erro) { res.writeHead(404).end('não encontrado'); return; }
|
|
231
|
+
res.writeHead(200, { 'Content-Type': tipoDe(nome) });
|
|
232
|
+
res.end(dados);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const porta = await escutar(servidor, PORTA_PADRAO);
|
|
237
|
+
servidor.__porta = porta;
|
|
238
|
+
const url = `http://127.0.0.1:${porta}/?k=${chave}`;
|
|
239
|
+
|
|
240
|
+
const janela = await abrirJanela(url);
|
|
241
|
+
return {
|
|
242
|
+
url, porta, chave, servidor, janela,
|
|
243
|
+
fechar() { try { pararEspelho(); } catch {} try { servidor.close(); } catch {} },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* Porta ocupada não é erro: é outra janela do PrimoCode, ou qualquer coisa
|
|
248
|
+
que pegou a 7788 primeiro. Anda dez casas e desiste — se dez portas
|
|
249
|
+
seguidas estão tomadas, o problema é outro. */
|
|
250
|
+
function escutar(servidor, inicial) {
|
|
251
|
+
return new Promise((ok, falha) => {
|
|
252
|
+
let porta = inicial;
|
|
253
|
+
const tentar = () => {
|
|
254
|
+
servidor.once('error', (e) => {
|
|
255
|
+
if (e.code === 'EADDRINUSE' && porta < inicial + 10) { porta++; tentar(); }
|
|
256
|
+
else falha(e);
|
|
257
|
+
});
|
|
258
|
+
servidor.listen(porta, '127.0.0.1', () => ok(porta));
|
|
259
|
+
};
|
|
260
|
+
tentar();
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* ── a janela ────────────────────────────────────────────────────────────
|
|
265
|
+
`--app=` abre sem barra de endereço, sem abas e com entrada própria na
|
|
266
|
+
barra de tarefas: é o que faz parecer aplicativo em vez de site. Chrome,
|
|
267
|
+
Edge, Brave e Vivaldi entendem, porque todos são Chromium.
|
|
268
|
+
|
|
269
|
+
`--user-data-dir` aponta para uma pasta do PrimoCode de propósito: sem ele,
|
|
270
|
+
o Chrome que JÁ ESTÁ ABERTO recebe o pedido e devolve uma aba comum — o
|
|
271
|
+
modo app é ignorado quando há um perfil em uso. Com perfil próprio nasce
|
|
272
|
+
uma janela de verdade, e o histórico do usuário fica fora disto. */
|
|
273
|
+
function candidatosDeNavegador() {
|
|
274
|
+
const p = os.platform();
|
|
275
|
+
if (p === 'darwin') {
|
|
276
|
+
return [
|
|
277
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
278
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
279
|
+
'/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
|
|
280
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
281
|
+
'/Applications/Arc.app/Contents/MacOS/Arc',
|
|
282
|
+
];
|
|
283
|
+
}
|
|
284
|
+
if (p === 'win32') {
|
|
285
|
+
const bases = [process.env['PROGRAMFILES'], process.env['PROGRAMFILES(X86)'],
|
|
286
|
+
process.env.LOCALAPPDATA].filter(Boolean);
|
|
287
|
+
const relativos = [
|
|
288
|
+
'Google\\Chrome\\Application\\chrome.exe',
|
|
289
|
+
'Microsoft\\Edge\\Application\\msedge.exe',
|
|
290
|
+
'BraveSoftware\\Brave-Browser\\Application\\brave.exe',
|
|
291
|
+
];
|
|
292
|
+
const fora = [];
|
|
293
|
+
for (const b of bases) for (const r of relativos) fora.push(path.join(b, r));
|
|
294
|
+
return fora;
|
|
295
|
+
}
|
|
296
|
+
return ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium',
|
|
297
|
+
'/usr/bin/chromium-browser', '/usr/bin/microsoft-edge', '/usr/bin/brave-browser'];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function abrirJanela(url) {
|
|
301
|
+
const perfil = path.join(os.homedir(), '.primocode', 'janela');
|
|
302
|
+
try { fs.mkdirSync(perfil, { recursive: true }); } catch {}
|
|
303
|
+
|
|
304
|
+
for (const exe of candidatosDeNavegador()) {
|
|
305
|
+
if (!fs.existsSync(exe)) continue;
|
|
306
|
+
try {
|
|
307
|
+
const filho = execFile(exe, [
|
|
308
|
+
`--app=${url}`,
|
|
309
|
+
`--user-data-dir=${perfil}`,
|
|
310
|
+
'--window-size=1180,820',
|
|
311
|
+
'--no-first-run',
|
|
312
|
+
'--no-default-browser-check',
|
|
313
|
+
], () => {});
|
|
314
|
+
// A janela é do usuário: se o PrimoCode do terminal terminar, ela
|
|
315
|
+
// continua de pé.
|
|
316
|
+
filho.unref();
|
|
317
|
+
return Promise.resolve({ modo: 'janela', navegador: path.basename(exe) });
|
|
318
|
+
} catch { /* tenta o próximo */ }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Nenhum Chromium instalado: aba comum no navegador padrão. Funciona
|
|
322
|
+
// igual — só não fica com cara de aplicativo.
|
|
323
|
+
const p = os.platform();
|
|
324
|
+
const [cmd, args] = p === 'darwin' ? ['open', [url]]
|
|
325
|
+
: p === 'win32' ? ['cmd.exe', ['/c', 'start', '', url]]
|
|
326
|
+
: ['xdg-open', [url]];
|
|
327
|
+
return new Promise((ok) => {
|
|
328
|
+
execFile(cmd, args, { timeout: 10000 }, (erro) => {
|
|
329
|
+
ok(erro ? { modo: 'nenhum', erro: erro.message } : { modo: 'aba' });
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = { abrir, abrirJanela, espelhar, ehRuido, candidatosDeNavegador };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.27.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js && node test/elementos.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"bin/",
|
|
29
29
|
"lib/",
|
|
30
|
+
"app/",
|
|
30
31
|
"studio/",
|
|
31
32
|
"README.md"
|
|
32
33
|
]
|
|
@@ -57,7 +57,8 @@ Em dúvida entre duas, escolha a que o usuário vai **mostrar para outra pessoa*
|
|
|
57
57
|
- **Ritmo.** Em decks longos, um `layout: "secao"` a cada 3–5 cenas.
|
|
58
58
|
- **Escolha o tema pelo assunto:** `meia-noite` produto e tecnologia · `claro`
|
|
59
59
|
corporativo e projetor · `papel` editorial e longo · `neon` técnico e dev ·
|
|
60
|
-
`brasa` energia e urgência · `floresta` sustentabilidade e saúde
|
|
60
|
+
`brasa` energia e urgência · `floresta` sustentabilidade e saúde ·
|
|
61
|
+
`ambar` curso, aula e conteúdo longo · `grafite` técnico sóbrio e relatório.
|
|
61
62
|
- **Notas do apresentador** (`notas` na cena) em quem vai falar por cima.
|
|
62
63
|
- **Nunca invente dado com cara de real.** Sem fonte, escreva a ordem de grandeza
|
|
63
64
|
ou deixe o campo como pergunta explícita ao usuário.
|
|
@@ -134,9 +134,10 @@ Sem declarar nada, os elementos já entram em cascata na ordem em que aparecem.
|
|
|
134
134
|
|
|
135
135
|
### Temas
|
|
136
136
|
|
|
137
|
-
`meia-noite` `claro` `papel` `neon` `brasa` `floresta` `retro` `oceano` `doce`
|
|
137
|
+
`meia-noite` `claro` `papel` `neon` `brasa` `floresta` `retro` `oceano` `doce`
|
|
138
|
+
`mono` `ambar` `grafite`
|
|
138
139
|
|
|
139
|
-
São esses
|
|
140
|
+
São esses doze, e só. Nome fora da lista é recusado: escuro é `meia-noite`, não
|
|
140
141
|
`noite`. Vale no documento ou por cena.
|
|
141
142
|
|
|
142
143
|
### Identidade — o que separa um vídeo genérico de um da marca
|
|
@@ -89,6 +89,8 @@ cena. Se o assunto não empurrar para nenhuma, escolha a que você NÃO usou da
|
|
|
89
89
|
| explicação, tutorial, passo a passo | `papel` ou `claro` | `lofi` | **Aula** |
|
|
90
90
|
| conquista, agradecimento, celebração | `doce` ou `neon` | `alegre` | **Brinde** |
|
|
91
91
|
| memória, história, retrospectiva | `retro` ou `mono` | `lofi` | **Linha** |
|
|
92
|
+
| curso, aula, treinamento, método | `ambar` | `lofi` | **Aula** |
|
|
93
|
+
| relatório, número, análise técnica | `grafite` | `ambiente` | **Manchete** |
|
|
92
94
|
|
|
93
95
|
### As seis estruturas
|
|
94
96
|
|
|
@@ -178,6 +180,85 @@ livre de direitos, ou uma das trilhas geradas acima.
|
|
|
178
180
|
|
|
179
181
|
---
|
|
180
182
|
|
|
183
|
+
## Elementos que dão cara de produção
|
|
184
|
+
|
|
185
|
+
Estes seis existem porque um vídeo genérico é feito só de título + texto. São
|
|
186
|
+
baratos de escrever e mudam completamente o resultado.
|
|
187
|
+
|
|
188
|
+
### `selo` — a pílula de estado
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{ "tipo": "selo", "texto": "Módulo 2 de 5" }
|
|
192
|
+
{ "tipo": "selo", "texto": "Resultado 4", "icone": "★", "estilo": "vivo" }
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`estilo`: sem nada (contorno), `"vivo"` (preenchido) ou `"solto"` (só o texto
|
|
196
|
+
em versalete). Vai sozinho para cima do título.
|
|
197
|
+
|
|
198
|
+
### `progresso` — onde estamos no caminho
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{ "tipo": "progresso", "atual": 4, "total": 15 }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Mostra "4/15" e os pontinhos. Use em série de cenas: diz ao espectador que
|
|
205
|
+
existe um percurso, e isso segura ele até o fim. `"numero": false` deixa só
|
|
206
|
+
os pontos.
|
|
207
|
+
|
|
208
|
+
### `avatar` — a pessoa
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{ "tipo": "avatar", "src": "/midia/foto.jpg", "nome": "Cleiton Paris", "tamanho": 280 }
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Círculo com anel aceso. Sem `src`, mostra as iniciais do `nome` — melhor que
|
|
215
|
+
um buraco cinza. Vai para cima do nome sozinho.
|
|
216
|
+
|
|
217
|
+
### `dispositivo` — celular, notebook ou navegador com conteúdo dentro
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{ "tipo": "dispositivo", "forma": "celular", "dentro": [ ...elementos... ] }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
`forma`: `celular` | `notebook` | `tablet` | `navegador` (este aceita
|
|
224
|
+
`"endereco": "primocode.dev"`). `dentro` é uma lista de elementos comuns — o
|
|
225
|
+
que couber. Nos layouts `imagem-direita` / `imagem-esquerda` o aparelho vai
|
|
226
|
+
para o lado sozinho, com o texto do outro. É o enquadramento mais forte que
|
|
227
|
+
existe aqui: use quando estiver mostrando uma tela.
|
|
228
|
+
|
|
229
|
+
### `conversa` — balões de pergunta e resposta
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{ "tipo": "conversa", "itens": [
|
|
233
|
+
{ "de": "eu", "texto": "Como acelero meu workflow?" },
|
|
234
|
+
{ "de": "ela", "texto": "Peça o storyboard em seis quadros." } ] }
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Entram um de cada vez, como diálogo. `de: "eu"` à esquerda; qualquer outra
|
|
238
|
+
coisa à direita, com a cor do tema. Combina com `dispositivo` — a conversa
|
|
239
|
+
dentro do celular é a cena mais usada em vídeo de IA.
|
|
240
|
+
|
|
241
|
+
### `passos` e `opcoes`
|
|
242
|
+
|
|
243
|
+
```json
|
|
244
|
+
{ "tipo": "passos", "atual": 2, "itens": ["Passo 1|roteiro", "Passo 2|storyboard", "Passo 3|corte"] }
|
|
245
|
+
{ "tipo": "opcoes", "pergunta": "O que garante o look?", "certa": 1,
|
|
246
|
+
"itens": ["LUT uniforme", "Saturação aleatória", "Espaços de cor diferentes"] }
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
`passos` acende a etapa `atual` e apaga as outras — mude o `atual` a cada
|
|
250
|
+
cena e a série ganha movimento sem esforço. `opcoes` marca a alternativa
|
|
251
|
+
`certa` com ✓ (não escreva o ✓ no texto: ele vem sozinho). Alternativa faz o
|
|
252
|
+
espectador pensar antes da resposta, e pensar é o que segura alguém no vídeo.
|
|
253
|
+
|
|
254
|
+
### Vidro
|
|
255
|
+
|
|
256
|
+
`"estilo": "vidro"` em `cartoes` dá painel translúcido com bisel de luz em
|
|
257
|
+
cima e sombra embaixo — o "liquid glass". Funciona em tema escuro; em tema
|
|
258
|
+
claro use os cartões normais.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
181
262
|
## Editar o vídeo do usuário (`clipe`)
|
|
182
263
|
|
|
183
264
|
Quando a pessoa manda uma gravação dela — o rosto falando, uma tela gravada,
|
package/studio/server.py
CHANGED
|
@@ -784,7 +784,7 @@ def gravar_analise(ident, dados):
|
|
|
784
784
|
# "Ele não alterna entre os temas, usa um tema só pra todos os vídeos, e o
|
|
785
785
|
# vídeo sempre tem a mesma estrutura. Fica tudo genérico."
|
|
786
786
|
#
|
|
787
|
-
# O estúdio tem
|
|
787
|
+
# O estúdio tem doze temas, onze fundos, oito transições e seis trilhas. O
|
|
788
788
|
# agente usa uma fatia estreita e repete — porque a spec tem UM exemplo
|
|
789
789
|
# completo e modelo ancora em exemplo. Conselho escrito na spec ajuda pouco
|
|
790
790
|
# quando ela nem sempre é relida.
|
|
@@ -922,7 +922,7 @@ def _texto_do_doc(doc):
|
|
|
922
922
|
# Os temas que o render.css de fato define. Fora desta lista não existe cor
|
|
923
923
|
# nenhuma — foi o que aconteceu com "noite", que passou como se fosse tema.
|
|
924
924
|
TEMAS_VALIDOS = {"claro", "meia-noite", "papel", "neon", "brasa", "floresta",
|
|
925
|
-
"retro", "oceano", "doce", "mono"}
|
|
925
|
+
"retro", "oceano", "doce", "mono", "ambar", "grafite"}
|
|
926
926
|
|
|
927
927
|
|
|
928
928
|
def _tema_invalido(tema, onde=""):
|
|
@@ -989,7 +989,13 @@ def validar_doc(modelo, doc):
|
|
|
989
989
|
# preto": documento válido na forma, invisível na tela.
|
|
990
990
|
TIPOS_VALIDOS = {"texto", "lista", "cartoes", "numeros", "tabela", "codigo",
|
|
991
991
|
"citacao", "forma", "imagem", "icone", "grafico", "no", "conector",
|
|
992
|
-
"objeto3d", "clipe"
|
|
992
|
+
"objeto3d", "clipe",
|
|
993
|
+
"selo", "progresso", "avatar", "dispositivo", "conversa",
|
|
994
|
+
"passos", "opcoes"}
|
|
995
|
+
# Os sete últimos são o vocabulário do vídeo de referência. O comentário
|
|
996
|
+
# mora aqui fora porque test/vocabulario.test.js lê esta lista com
|
|
997
|
+
# expressão regular, comparando com a do render.js — palavra de comentário
|
|
998
|
+
# dentro das chaves entrava na lista como se fosse um tipo válido.
|
|
993
999
|
|
|
994
1000
|
# Tema inventado tinha o mesmo destino silencioso do tipo inventado: o CSS
|
|
995
1001
|
# não casa com [data-tema="noite"], nenhuma cor é definida e a peça sai
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
ferramentas.js — o que cada ferramenta oferece na interface.
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
|
-
// Os
|
|
5
|
+
// Os doze que o render.css define. A lista tinha seis: quatro temas existiam
|
|
6
6
|
// no CSS, a IA podia usá-los, e o editor não os oferecia a ninguém.
|
|
7
7
|
var TEMAS = ["meia-noite", "claro", "papel", "neon", "brasa", "floresta",
|
|
8
|
-
"retro", "oceano", "doce", "mono"];
|
|
8
|
+
"retro", "oceano", "doce", "mono", "ambar", "grafite"];
|
|
9
9
|
|
|
10
10
|
var PALETA = ["#ff5a36", "#ffb03a", "#f4e04d", "#2fbf7f", "#1f8fe0", "#8b5cf6",
|
|
11
11
|
"#f472b6", "#ffffff", "#9aa4b2", "#111318"];
|