primocode 8.36.0 → 8.38.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/bin/primocode.js +65 -50
- package/lib/act.js +121 -78
- package/lib/area.js +104 -0
- package/lib/browser.js +110 -2
- package/lib/catalogo.js +7 -2
- package/lib/conta.js +122 -29
- package/lib/especialistas.js +123 -0
- package/lib/fala.js +146 -0
- package/lib/studio.js +319 -10
- package/lib/tools.js +19 -5
- package/lib/ui.js +57 -22
- package/package.json +2 -2
- package/studio/ferramentas/01-modelos.md +3 -1
- package/studio/ferramentas/corte.md +108 -0
- package/studio/ferramentas/prisma.md +8 -0
- package/studio/server.py +31 -1
- package/studio/web/js/icones.js +30 -1
- package/studio/web/js/player.js +58 -16
- package/studio/web/js/render.js +296 -2
- package/studio/web/js/video.js +44 -18
- package/studio/web/render.css +307 -3
package/lib/ui.js
CHANGED
|
@@ -243,28 +243,45 @@ function welcomeBox(version, contexto, opts = {}) {
|
|
|
243
243
|
return out.join('\n');
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/* As frases que giram enquanto o Primo trabalha. Trocam sozinhas a cada
|
|
247
|
+
poucos segundos, em ordem, e NENHUMA fala de limite, cota, modelo ou espera
|
|
248
|
+
— de propósito: a mesma lista cobre o pensamento comum e a janela do minuto
|
|
249
|
+
que estourou. Quem olha não distingue um do outro, e é isso que o dono
|
|
250
|
+
pediu ("nunca mostrar limitações tipo limite do minuto, tudo deve ocorrer
|
|
251
|
+
ocultamente"). */
|
|
252
|
+
const PENSANDO = ['Primo Code pensando', 'organizando as ideias', 'juntando as peças', 'revendo o caminho', 'pensando no próximo passo'];
|
|
253
|
+
|
|
246
254
|
class Spinner {
|
|
247
255
|
constructor(text='pensando') {
|
|
248
|
-
this.
|
|
256
|
+
this.textos = Array.isArray(text) ? text : [text];
|
|
257
|
+
this.text = this.textos[0];
|
|
249
258
|
this.frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
250
|
-
this.i = 0; this.timer = null;
|
|
259
|
+
this.i = 0; this.timer = null; this.desde = 0; this.largura = 0;
|
|
251
260
|
}
|
|
252
261
|
start() {
|
|
253
262
|
// Fora de um terminal (pipe, CI) a animação só sujaria a saída.
|
|
254
263
|
if (!process.stdout.isTTY) return this;
|
|
264
|
+
this.desde = Date.now();
|
|
255
265
|
this.timer = setInterval(() => {
|
|
256
|
-
|
|
266
|
+
// Com mais de uma frase, troca a cada 4s. A frase nova pode ser mais
|
|
267
|
+
// curta que a anterior: o resto da linha velha ficaria na tela, então
|
|
268
|
+
// a linha é sempre preenchida até a maior largura já escrita.
|
|
269
|
+
if (this.textos.length > 1) this.text = this.textos[Math.floor((Date.now() - this.desde) / 4000) % this.textos.length];
|
|
270
|
+
const cru = ` ${this.frames[this.i]} ${this.text}`;
|
|
271
|
+
const falta = Math.max(0, this.largura - cru.length);
|
|
272
|
+
this.largura = Math.max(this.largura, cru.length);
|
|
273
|
+
process.stdout.write(`\r ${c.muted(this.frames[this.i])} ${c.muted(this.text)}${' '.repeat(falta)}`);
|
|
257
274
|
this.i = (this.i+1) % this.frames.length;
|
|
258
275
|
}, 80);
|
|
259
276
|
return this;
|
|
260
277
|
}
|
|
261
278
|
stop() {
|
|
262
|
-
if (this.timer) { clearInterval(this.timer); this.timer = null; process.stdout.write(`\r${' '.repeat(80)}\r`); }
|
|
279
|
+
if (this.timer) { clearInterval(this.timer); this.timer = null; process.stdout.write(`\r${' '.repeat(Math.max(80, this.largura + 4))}\r`); }
|
|
263
280
|
}
|
|
264
281
|
}
|
|
265
282
|
|
|
266
|
-
|
|
267
|
-
|
|
283
|
+
// O rótulo de cada tool na tela — o que a pessoa lê no lugar do nome interno.
|
|
284
|
+
const ROTULOS = {
|
|
268
285
|
read_file:'Read', write_file:'Write', edit_file:'Edit', multi_edit:'MultiEdit',
|
|
269
286
|
list_dir:'List', glob:'Glob', grep:'Grep', run_shell:'Bash',
|
|
270
287
|
run_in_new_terminal:'Terminal', open_in_browser:'Open', spawn_agent:'Agent',
|
|
@@ -281,14 +298,32 @@ function actionHeader(name, args) {
|
|
|
281
298
|
studio_versoes:'Estúdio·histórico',
|
|
282
299
|
desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
|
|
283
300
|
desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
301
|
+
studio_midia:'Estúdio·mídia', studio_analisar:'Estúdio·analisar', construir_app:'Construir',
|
|
302
|
+
remember_general:'Lembrar',
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/* A linha da AÇÃO: `●` na cor da marca e o rótulo em negrito. O resultado
|
|
306
|
+
dela vem embaixo, com `⎿`, um degrau para dentro (ver lib/fala.js). Antes
|
|
307
|
+
cabeçalho e resultado usavam o mesmo `⎿` no mesmo nível, e dez tools
|
|
308
|
+
seguidas eram uma coluna de ganchos sem dizer o que era ação e o que era
|
|
309
|
+
saída. O argumento vem numa linha só e cortado: um comando de três linhas
|
|
310
|
+
no cabeçalho empurra o resultado para fora da vista. */
|
|
311
|
+
function actionHeader(name, args) {
|
|
312
|
+
const label = ROTULOS[name] || name;
|
|
313
|
+
let argStr = args ? (args.consulta || args.path || args.pattern || args.command || args.target || args.url || args.selector || args.text || args.app || args.window || args.name || args.ferramenta || args.titulo || args.description || (args.todos ? `${args.todos.length} tarefas` : '') || (args.x != null ? `${args.x},${args.y}` : '') || args.key || args.arquivo || args.id || JSON.stringify(args).slice(0,60)) : '';
|
|
314
|
+
argStr = String(argStr || '').replace(/\s+/g, ' ').trim();
|
|
315
|
+
if (argStr.length > 96) argStr = argStr.slice(0, 95) + '…';
|
|
316
|
+
return ` ${c.brand('●')} ${c.bold(label)}${argStr ? ' '+c.muted(argStr) : ''}`;
|
|
288
317
|
}
|
|
289
318
|
|
|
290
319
|
// Transforma erro cru (JSON do provedor, código HTTP, erro de rede) numa frase
|
|
291
320
|
// humana. O usuário nunca deveria ver um paredão de JSON.
|
|
321
|
+
//
|
|
322
|
+
// Sem emoji na frente, de propósito: um 💳 ou um 🔧 no começo da linha é a
|
|
323
|
+
// tela gritando "ERRO" antes de a pessoa ler a frase — e o que chega aqui é
|
|
324
|
+
// o que ela PRECISA fazer (crédito, chave, rede), dito com calma por
|
|
325
|
+
// lib/fala.js. O que é passageiro (minuto, 429, 5xx) nem chega: o agente e o
|
|
326
|
+
// chat esperam e repetem sem contar a ninguém.
|
|
292
327
|
function friendlyError(raw) {
|
|
293
328
|
const s = String(raw && raw.message ? raw.message : raw || '').trim();
|
|
294
329
|
const low = s.toLowerCase();
|
|
@@ -308,7 +343,7 @@ function friendlyError(raw) {
|
|
|
308
343
|
// é porque nem o mínimo cabe.
|
|
309
344
|
if (/can only afford|requires more credits|\b402\b/i.test(low)) {
|
|
310
345
|
const q = s.match(/afford\s+(\d+)/i);
|
|
311
|
-
return '
|
|
346
|
+
return 'A conta do OpenRouter não tem crédito para uma resposta deste tamanho'
|
|
312
347
|
+ (q ? ` (dá para ${q[1]} tokens)` : '')
|
|
313
348
|
+ '. Adicione crédito em openrouter.ai/settings/credits, ou use /model fast'
|
|
314
349
|
+ ' para pedidos menores.';
|
|
@@ -318,7 +353,7 @@ function friendlyError(raw) {
|
|
|
318
353
|
// apelidos padrão já são grátis, então quem cai aqui forçou um modelo pago.
|
|
319
354
|
if (/prompt tokens limit exceeded/i.test(low)) {
|
|
320
355
|
const n = s.match(/exceeded:\s*(\d+)\s*>\s*(\d+)/i);
|
|
321
|
-
return '
|
|
356
|
+
return 'Este modelo é pago e a conta não tem crédito, então o provedor corta o pedido'
|
|
322
357
|
+ (n ? ` em ${n[2]} tokens (o seu tem ${n[1]})` : '')
|
|
323
358
|
+ '. Use /model main — os modelos padrão são grátis e não têm esse corte.';
|
|
324
359
|
}
|
|
@@ -329,8 +364,8 @@ function friendlyError(raw) {
|
|
|
329
364
|
|| /conversa passou do teto/i.test(low);
|
|
330
365
|
if (porTamanho) {
|
|
331
366
|
const nums = s.match(/Limit\s+(\d+),\s*Requested\s+(\d+)/i);
|
|
332
|
-
if (/conversa passou do teto/i.test(low)) return
|
|
333
|
-
return '
|
|
367
|
+
if (/conversa passou do teto/i.test(low)) return `${inner.replace(/\s+/g, ' ').trim()}`;
|
|
368
|
+
return 'Este pedido é maior que o teto do plano'
|
|
334
369
|
+ (nums ? ` (${nums[2]} tokens contra um limite de ${nums[1]} por minuto)` : '')
|
|
335
370
|
+ '. Esperar não resolve: use /clear para encurtar a conversa, ou divida o pedido em partes menores.';
|
|
336
371
|
}
|
|
@@ -343,7 +378,7 @@ function friendlyError(raw) {
|
|
|
343
378
|
const bruto = (s.match(/try again in ([0-9hms.]+)/i) || [])[1];
|
|
344
379
|
const quando = bruto && bruto.replace(/\.+$/, '').replace(/\.\d+s$/, 's');
|
|
345
380
|
const uso = s.match(/Limit\s+(\d+),\s*Used\s+(\d+)/i);
|
|
346
|
-
return '
|
|
381
|
+
return 'A cota de tokens do dia acabou'
|
|
347
382
|
+ (uso ? ` (${uso[2]} de ${uso[1]} tokens)` : '')
|
|
348
383
|
+ (quando ? `, libera em ${quando}` : '')
|
|
349
384
|
+ '. Enquanto isso o /mode chat gasta bem menos por pergunta.';
|
|
@@ -358,12 +393,12 @@ function friendlyError(raw) {
|
|
|
358
393
|
// texto genérico faria o usuário achar que não havia nada a fazer.
|
|
359
394
|
const semChave = s.match(/provedor de reserva \(([^)]+)\) está sem chave/i);
|
|
360
395
|
if (semChave) {
|
|
361
|
-
return
|
|
396
|
+
return `A cota diária de grátis do OpenRouter acabou (são 50 por dia na conta sem crédito),`
|
|
362
397
|
+ ` e o provedor de reserva (${semChave[1]}) está sem chave no servidor — não houve para onde cair.`
|
|
363
398
|
+ ` Configure a chave dele no Render, ou adicione 10 dólares de crédito no OpenRouter`
|
|
364
399
|
+ ` para subir de 50 para 1.000 pedidos por dia (esse crédito não é gasto pelos modelos grátis).`;
|
|
365
400
|
}
|
|
366
|
-
return '
|
|
401
|
+
return 'A cota diária de grátis acabou nos DOIS provedores (OpenRouter e Groq).'
|
|
367
402
|
+ ' Se chegou até aqui, o servidor já tentou os dez modelos da corrente.'
|
|
368
403
|
+ ' Trocar de modelo não resolve: o limite é da conta, não do modelo.'
|
|
369
404
|
+ ' Adicionar 10 dólares de crédito no OpenRouter sobe de 50 para 1.000 pedidos'
|
|
@@ -371,18 +406,18 @@ function friendlyError(raw) {
|
|
|
371
406
|
+ ' Sem isso, libera amanhã.';
|
|
372
407
|
}
|
|
373
408
|
if (/rate.?limit|too many requests|\b429\b/i.test(low)) {
|
|
374
|
-
return '
|
|
409
|
+
return 'Muitos pedidos em pouco tempo. Espere alguns instantes e tente de novo.';
|
|
375
410
|
}
|
|
376
411
|
// "token" solto não serve de pista: a mensagem de TPM tem a palavra e não
|
|
377
412
|
// tem nada a ver com autenticação. Exige o código ou a frase inteira.
|
|
378
413
|
if (/\b401\b|unauthorized|forbidden|token (inv[aá]lido|ausente|expirado)|invalid token|missing token/i.test(low)) {
|
|
379
|
-
return '
|
|
414
|
+
return 'Este servidor pede um token. Configure PRIMOCODE_TOKEN ou "token" em ~/.primocode/config.json.';
|
|
380
415
|
}
|
|
381
416
|
if (/econnrefused|enotfound|etimedout|timeout|socket hang up|network|getaddrinfo|econnreset/i.test(low)) {
|
|
382
|
-
return '
|
|
417
|
+
return 'Não consegui falar com o servidor. Verifique sua conexão e tente de novo.';
|
|
383
418
|
}
|
|
384
419
|
if (/\b5\d\d\b|internal server|bad gateway|service unavailable|gateway timeout/i.test(low)) {
|
|
385
|
-
return '
|
|
420
|
+
return 'O servidor está fora do ar no momento. Tente de novo em instantes.';
|
|
386
421
|
}
|
|
387
422
|
// Sobrou algo específico: mostra só a mensagem interna, sem o JSON gigante.
|
|
388
423
|
const clean = inner.replace(/\s+/g, ' ').trim();
|
|
@@ -390,4 +425,4 @@ function friendlyError(raw) {
|
|
|
390
425
|
}
|
|
391
426
|
|
|
392
427
|
|
|
393
|
-
module.exports = { c, mascot, welcomeBox, Spinner, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
|
|
428
|
+
module.exports = { c, mascot, welcomeBox, Spinner, PENSANDO, actionHeader, friendlyError, visibleWidth, stripAnsi, truncate, padRight };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.38.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Requer conta Conecta Primo AI (Premium ou Super). 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 && node test/fala-repetida.test.js && node test/trilha.test.js && node test/pipeline.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.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/fala-repetida.test.js && node test/trilha.test.js && node test/anuncio.test.js && node test/pipeline.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs && node test/nuvem.test.js && node test/codex.test.js && node test/chaves.test.js && node test/prazo.test.js && node test/primeira-vez.test.js && node test/janela.test.js && node test/pareamento.test.js && node test/fala.test.js && node test/especialistas.test.js && node test/icone.test.js && node test/exportar.test.js"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -85,8 +85,10 @@ um elemento flutuando por cima do layout.
|
|
|
85
85
|
// IMAGEM
|
|
86
86
|
{ "tipo":"imagem", "src":"https://…", "ajuste":"cobrir", "raio":18 }
|
|
87
87
|
|
|
88
|
-
// ÍCONE — nomes em /api/ferramentas/tela
|
|
88
|
+
// ÍCONE — nomes em /api/ferramentas/tela; ou DESENHE o seu: "caminho" é o
|
|
89
|
+
// path SVG numa caixa 24×24 (string ou lista de strings), no traço da casa
|
|
89
90
|
{ "tipo":"icone", "nome":"raio", "tamanho":120, "cor":"#ffb03a" }
|
|
91
|
+
{ "tipo":"icone", "caminho":"M3 6h18l-2 10H5zM8 20h.01M16 20h.01", "tamanho":96 }
|
|
90
92
|
|
|
91
93
|
// FORMA
|
|
92
94
|
{ "tipo":"forma", "forma":"retangulo", "x":200,"y":200,"l":400,"a":220,
|
|
@@ -227,17 +227,125 @@ baratos de escrever e mudam completamente o resultado.
|
|
|
227
227
|
último, é o vídeo que o usuário chamou de "sem efeito nenhum" — e não é falta
|
|
228
228
|
de recurso: é este bloco não ter sido lido.
|
|
229
229
|
|
|
230
|
+
## Anúncio de marca — o kit
|
|
231
|
+
|
|
232
|
+
Quando o pedido é o COMERCIAL de um produto (um cartão, um app, um plano), o
|
|
233
|
+
que separa uma peça de agência de um slide com música são quatro coisas, e as
|
|
234
|
+
quatro têm elemento próprio. Uma peça de marca sem elas sai genérica por mais
|
|
235
|
+
bonito que esteja o texto.
|
|
236
|
+
|
|
237
|
+
**Primeiro, declare a marca no documento.** Ela pinta o destaque de tudo —
|
|
238
|
+
número, botão, detalhe, o cartão — e libera o fundo `"marca"`:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
"marca": { "cor": "#21C25E", "logo": "/midia/logo.svg" },
|
|
242
|
+
"tema": "claro"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Tema CLARO na maioria dos anúncios de marca. O escuro é para a cena de
|
|
246
|
+
produto e para a vitrine; o resto da peça respira no branco.
|
|
247
|
+
|
|
248
|
+
### `cartao` — o produto
|
|
249
|
+
|
|
250
|
+
```json
|
|
251
|
+
{ "tipo": "cartao", "nome": "PicPay", "variante": "Gold", "movimento": "flutuar" }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Sai um cartão desenhado: chip, símbolo de aproximação, numeração mascarada,
|
|
255
|
+
nome e validade, com luz e sombra. `cor` (sem ela, a da marca), `largura`,
|
|
256
|
+
`numero` (só os 4 últimos aparecem), `titular`, `validade`, `face: "verso"`,
|
|
257
|
+
`movimento`: `flutuar` | `girar`.
|
|
258
|
+
|
|
259
|
+
**Mostre o produto antes de falar dele.** Um anúncio de cartão que só tem
|
|
260
|
+
frases é um anúncio sem produto.
|
|
261
|
+
|
|
262
|
+
### `app` — a tela do aplicativo
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{ "tipo": "app", "topo": "Minha conta", "rotulo": "saldo disponível",
|
|
266
|
+
"valor": "R$ 1.284,90",
|
|
267
|
+
"linhas": [["Café da manhã", "R$ 18,90"], ["Mercado", "R$ 92,40"]],
|
|
268
|
+
"botao": "Pagar com o cartão" }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Cabeçalho, um número grande, linhas com o nome à esquerda e o valor à direita
|
|
272
|
+
separadas por fio, e o botão cheio embaixo — que é o que faz o olho ler
|
|
273
|
+
"aplicativo". Aceita `["nome", "valor"]` e `"nome|valor"`. `escuro: true` para
|
|
274
|
+
a versão noturna.
|
|
275
|
+
|
|
276
|
+
**Ele vai DENTRO do `dispositivo`**, e aí ocupa a tela inteira do aparelho:
|
|
277
|
+
|
|
278
|
+
```json
|
|
279
|
+
{ "tipo": "dispositivo", "forma": "celular", "dentro": [ { "tipo": "app", ... } ] }
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Solto na cena ele vira o print da campanha. É o mesmo elemento de propósito:
|
|
283
|
+
com dois, a tela do celular e o print divergiriam na primeira mudança.
|
|
284
|
+
|
|
285
|
+
### `vitrine` — a linha de produtos
|
|
286
|
+
|
|
287
|
+
```json
|
|
288
|
+
{ "tipo": "vitrine", "titulo": "Escolha seu cartão", "atual": 1, "nome": "PicPay",
|
|
289
|
+
"itens": [ { "nome": "Gold", "cor": "#C9A227", "detalhe": "sem anuidade" },
|
|
290
|
+
{ "nome": "Verde", "detalhe": "cashback" },
|
|
291
|
+
{ "nome": "Black", "cor": "#181B1F", "detalhe": "sala VIP" } ] }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Os cartões numa fila, o `atual` à frente e os outros recuados. Vai bem sobre
|
|
295
|
+
fundo escuro (`"fundo": "#0b0f0c"`), que é como toda tela de "escolha o seu" é
|
|
296
|
+
desenhada.
|
|
297
|
+
|
|
298
|
+
### `assinatura` — o ponto final
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{ "tipo": "assinatura", "nome": "PicPay", "frase": "o jeito fácil de pagar", "sobre": "marca" }
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Ela toma a CENA INTEIRA: o logo sozinho, grande, sobre a cor da casa. `sobre`:
|
|
305
|
+
`marca` | `claro` | `escuro`. Com `logo` ela usa a imagem; sem, o `nome`.
|
|
306
|
+
|
|
307
|
+
**Todo anúncio termina assim.** Sem a assinatura a peça acaba numa frase, e
|
|
308
|
+
ninguém fica sabendo quem falou.
|
|
309
|
+
|
|
310
|
+
### A ordem que funciona
|
|
311
|
+
|
|
312
|
+
Uma peça de marca de 20 a 30 segundos, com 8 a 10 cenas:
|
|
313
|
+
|
|
314
|
+
| # | cena | o que entra |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| 1 | a promessa, em uma frase curta | `texto` pequeno, muito ar, fundo claro |
|
|
317
|
+
| 2 | o produto | `cartao` com `movimento` |
|
|
318
|
+
| 3 | o mote, na cor da casa | `fundo: "marca"`, `texto` branco, `anim: "palavra"` |
|
|
319
|
+
| 4 | funcionando | `dispositivo` + `app` |
|
|
320
|
+
| 5 | o benefício | `texto` |
|
|
321
|
+
| 6 | a linha | `vitrine` sobre fundo escuro |
|
|
322
|
+
| 7 | a última razão | `texto` |
|
|
323
|
+
| 8 | assinatura | `assinatura` |
|
|
324
|
+
|
|
325
|
+
E o rosto de quem fala, quando houver, entra por `clipe` entre a 4 e a 5 — o
|
|
326
|
+
depoimento é o que dá confiança, e nenhum desenho substitui.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
230
330
|
### `icone` — o desenho ao lado do argumento
|
|
231
331
|
|
|
232
332
|
```json
|
|
233
333
|
{ "tipo": "icone", "nome": "raio", "tamanho": 120 }
|
|
234
334
|
{ "tipo": "icone", "nome": "crescimento", "tamanho": 90, "cor": "#22d3ee", "traco": 2 }
|
|
335
|
+
{ "tipo": "icone", "caminho": "M4 7h16M4 12h10M4 17h7", "tamanho": 100 }
|
|
336
|
+
{ "tipo": "icone", "caminho": ["M12 3a9 9 0 100 18 9 9 0 000-18z", "M9 12l2 2 4-4"], "preenchido": false }
|
|
235
337
|
```
|
|
236
338
|
|
|
237
339
|
O jeito mais barato de uma cena deixar de ser um slide. Um ícone grande acima
|
|
238
340
|
do título, ou um por item de uma lista de três, e a mesma frase passa a ter
|
|
239
341
|
imagem.
|
|
240
342
|
|
|
343
|
+
**O ícone que a lista não tem, você DESENHA** — `caminho` é o path SVG numa
|
|
344
|
+
caixa de 24×24 (uma string, ou uma lista com um traço por peça). Ele sai no
|
|
345
|
+
mesmo traço dos ícones prontos, na cor do tema, então o seu não destoa. Um
|
|
346
|
+
carrinho de compras, uma xícara, o símbolo do produto do cliente: três ou
|
|
347
|
+
quatro comandos `M`/`L`/`A`/`C` resolvem. Nunca emoji no lugar de ícone.
|
|
348
|
+
|
|
241
349
|
Os nomes que existem (traço, no tom do tema):
|
|
242
350
|
|
|
243
351
|
`raio` `alvo` `foguete` `escudo` `relogio` `usuario` `pessoas` `coracao`
|
|
@@ -87,6 +87,14 @@ Entre 8 e 14 cenas para 10 minutos. Uma ideia por cena.
|
|
|
87
87
|
|
|
88
88
|
## Detalhes que valem
|
|
89
89
|
|
|
90
|
+
- **A cena compõe sozinha.** Com título e conteúdo, o título ancora no alto e o
|
|
91
|
+
conteúdo ocupa o corpo, em escala de projetor; o rodapé com o nome do deck e
|
|
92
|
+
"03 / 08" entra sozinho (menos na capa e nas seções). Não simule isso com
|
|
93
|
+
`livre`.
|
|
94
|
+
- **Varie o layout entre as cenas**: `numeros` para o dado que decide,
|
|
95
|
+
`imagem-direita` com foto ou `dispositivo` quando há produto, `cartoes` em
|
|
96
|
+
`tres-colunas` para as opções, `citacao` para a voz de fora, `tabela` para
|
|
97
|
+
comparar. Cinco cenas `titulo` + `lista` seguidas é o deck que ninguém lembra.
|
|
90
98
|
- **`tema` por cena** sobrepõe o do deck. Uma citação em `papel` no meio de um deck
|
|
91
99
|
`meia-noite` cria respiro — use uma vez, não cinco.
|
|
92
100
|
- **`revelar: true`** na lista faz os itens aparecerem um a um.
|
package/studio/server.py
CHANGED
|
@@ -881,7 +881,8 @@ def revisar_monotonia(doc, ferramenta):
|
|
|
881
881
|
avisos.append(
|
|
882
882
|
"o vídeo é só texto e imagem: nenhum elemento de apoio em %d cenas. "
|
|
883
883
|
'Um "icone" ao lado do argumento (raio, alvo, foguete, escudo, relogio, '
|
|
884
|
-
"lampada, cadeado, crescimento, pessoas, dinheiro, globo, fogo…
|
|
884
|
+
"lampada, cadeado, crescimento, pessoas, dinheiro, globo, fogo… ou um "
|
|
885
|
+
'DESENHADO por você, com "caminho": path SVG numa caixa 24x24), um '
|
|
885
886
|
'"selo" de estado, um "numeros" com "contar": true — é isto que faz a '
|
|
886
887
|
"tela parecer produzida em vez de um slide com legenda." % len(cenas))
|
|
887
888
|
|
|
@@ -893,6 +894,28 @@ def revisar_monotonia(doc, ferramenta):
|
|
|
893
894
|
"(aproximar, afastar, esquerda, direita, cima), e uma ou duas peças por "
|
|
894
895
|
"vídeo levam chaves de animação.")
|
|
895
896
|
|
|
897
|
+
# ── ANÚNCIO DE MARCA SEM PRODUTO E SEM ASSINATURA ───────────────────
|
|
898
|
+
#
|
|
899
|
+
# "Ainda não tá bom, tá genérico. Quero nesse nível." — o dono, sobre um
|
|
900
|
+
# comercial de fintech mandado como referência.
|
|
901
|
+
#
|
|
902
|
+
# O que separava a peça daqui daquela não era tema nem cor: era não ter o
|
|
903
|
+
# PRODUTO na tela e não assinar no fim. Quem declarou uma `marca` está
|
|
904
|
+
# fazendo peça de marca, e aí estas duas ausências são o defeito inteiro.
|
|
905
|
+
if doc.get("marca") and len(cenas) >= 4:
|
|
906
|
+
if not (tipos & {"cartao", "app", "vitrine", "dispositivo", "imagem", "clipe"}):
|
|
907
|
+
avisos.append(
|
|
908
|
+
"esta peça tem marca declarada e nenhuma cena mostra o PRODUTO — são %d cenas "
|
|
909
|
+
"de texto sobre fundo. Um anúncio de cartão mostra o cartão (`cartao`), o app "
|
|
910
|
+
"funcionando dentro do celular (`dispositivo` + `app`) e a linha de produtos "
|
|
911
|
+
"(`vitrine`). É essa ausência que se lê como genérico." % len(cenas))
|
|
912
|
+
if "assinatura" not in tipos:
|
|
913
|
+
avisos.append(
|
|
914
|
+
'o anúncio acaba sem assinar. A última cena de uma peça de marca é '
|
|
915
|
+
'{"tipo":"assinatura","nome":"…","sobre":"marca"} — o logo sozinho, grande, '
|
|
916
|
+
"sobre a cor da casa. Sem ela a peça termina numa frase e ninguém fica "
|
|
917
|
+
"sabendo quem falou.")
|
|
918
|
+
|
|
896
919
|
# Só a voz com tela montada: existe e quase nunca é lembrado, porque é o
|
|
897
920
|
# contrário do `mudo` e o contrário não estava no vocabulário até agora.
|
|
898
921
|
clipes = [e for e in todos if e.get("tipo") == "clipe"]
|
|
@@ -1061,9 +1084,16 @@ def validar_doc(modelo, doc):
|
|
|
1061
1084
|
# default do renderizador: texto cru de 16px num canvas de 1920 — ou nada,
|
|
1062
1085
|
# quando o texto estava noutro campo. Era o "vídeo com música mas tudo
|
|
1063
1086
|
# preto": documento válido na forma, invisível na tela.
|
|
1087
|
+
# O kit de anúncio (cartao, app, vitrine, assinatura) entra na lista abaixo.
|
|
1088
|
+
#
|
|
1089
|
+
# A LISTA É LIDA POR UM TESTE (`vocabulario.test.js`), que a recorta com uma
|
|
1090
|
+
# expressão regular e pega as palavras — então comentário DENTRO dela vira
|
|
1091
|
+
# "tipo que o render precisa desenhar", e o teste passa a cobrar `produto`,
|
|
1092
|
+
# `linha` e `fim`. Texto sobre os tipos mora aqui fora.
|
|
1064
1093
|
TIPOS_VALIDOS = {"texto", "lista", "cartoes", "numeros", "tabela", "codigo",
|
|
1065
1094
|
"citacao", "forma", "imagem", "icone", "grafico", "no", "conector",
|
|
1066
1095
|
"objeto3d", "clipe",
|
|
1096
|
+
"cartao", "app", "vitrine", "assinatura",
|
|
1067
1097
|
"selo", "progresso", "avatar", "dispositivo", "conversa",
|
|
1068
1098
|
"passos", "opcoes"}
|
|
1069
1099
|
# Os sete últimos são o vocabulário do vídeo de referência. O comentário
|
package/studio/web/js/icones.js
CHANGED
|
@@ -88,4 +88,33 @@ function nomesIcones() {
|
|
|
88
88
|
return Object.keys(ICONES);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
/* O ícone que a IA DESENHA, quando o que ela precisa não está na lista.
|
|
92
|
+
*
|
|
93
|
+
* "A IA irá desenvolver ícones."
|
|
94
|
+
*
|
|
95
|
+
* `caminho` é o path SVG numa caixa de 24×24 — uma string, ou uma lista de
|
|
96
|
+
* strings (um traço por peça). Sai no MESMO traço dos ícones da casa (1.7,
|
|
97
|
+
* cantos redondos, cor do texto), para o desenhado não destoar do pronto.
|
|
98
|
+
*
|
|
99
|
+
* Só entram os caracteres de path: letra de comando, número, espaço,
|
|
100
|
+
* vírgula. Nada mais — isto vira innerHTML numa página nossa, e o documento
|
|
101
|
+
* foi escrito por um modelo. Path inválido cai no círculo, que é o mesmo
|
|
102
|
+
* destino de um nome que não existe: o erro fica visível e inofensivo. */
|
|
103
|
+
// Começa em M/m: todo path de verdade abre com um moveto, e "42" sozinho
|
|
104
|
+
// não é desenho nenhum.
|
|
105
|
+
var CAMINHO_OK = /^[Mm][MmZzLlHhVvCcSsQqTtAa0-9\s,.\-+eE]*$/;
|
|
106
|
+
|
|
107
|
+
function svgCaminho(caminho, op) {
|
|
108
|
+
op = op || {};
|
|
109
|
+
var lista = Array.isArray(caminho) ? caminho : [caminho];
|
|
110
|
+
var ds = lista.map(function (d) { return String(d == null ? "" : d).trim(); })
|
|
111
|
+
.filter(function (d) { return d && d.length <= 4000 && CAMINHO_OK.test(d); });
|
|
112
|
+
if (!ds.length) return svgIcone("circulo", op.cls);
|
|
113
|
+
var fill = op.preenchido ? "currentColor" : "none";
|
|
114
|
+
return '<svg viewBox="0 0 24 24" fill="' + fill + '" stroke="currentColor" stroke-width="' +
|
|
115
|
+
(Number(op.traco) > 0 ? Number(op.traco) : 1.7) + '" stroke-linecap="round" stroke-linejoin="round"' +
|
|
116
|
+
(op.cls ? ' class="' + op.cls + '"' : "") + ">" +
|
|
117
|
+
ds.map(function (d) { return '<path d="' + d + '"/>'; }).join("") + "</svg>";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof module !== "undefined" && module.exports) module.exports = { ICONES: ICONES, svgIcone: svgIcone, svgCaminho: svgCaminho, CAMINHO_OK: CAMINHO_OK };
|
package/studio/web/js/player.js
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
var arq = null, doc = {}, cenas = [], atual = 0;
|
|
13
13
|
var tocando = false, quadro = null, t0 = 0, decorrido = 0;
|
|
14
14
|
var trilha = null, gravador = null, pedacos = [];
|
|
15
|
+
/* Resolve quando o arquivo está MONTADO no palco. É por ela que a exportação
|
|
16
|
+
escondida (studio_exportar) sabe quando pode começar — antes disto não há
|
|
17
|
+
cena para fotografar, e um export que começa cedo sai preto. */
|
|
18
|
+
var carregadoOk = null;
|
|
19
|
+
var carregado = new Promise(function (ok) { carregadoOk = ok; });
|
|
15
20
|
var ehVideo = false, transporte = null;
|
|
16
21
|
|
|
17
22
|
/* ------------------------------------------------------------- avisos -- */
|
|
@@ -81,8 +86,14 @@
|
|
|
81
86
|
if (a.ferramenta === "grade") return montarGrade();
|
|
82
87
|
|
|
83
88
|
cenas = doc.cenas || [];
|
|
89
|
+
// O rodapé (nome do deck, "03 / 08") é coisa de APRESENTAÇÃO: o vídeo e
|
|
90
|
+
// a arte não têm. Ver o rodapé em render.js.
|
|
91
|
+
var rodape = a.ferramenta === "prisma" ? { titulo: a.titulo, total: cenas.length } : null;
|
|
84
92
|
cenas.forEach(function (c, i) {
|
|
85
|
-
var no = renderCena(c, doc, {
|
|
93
|
+
var no = renderCena(c, doc, {
|
|
94
|
+
animar: true,
|
|
95
|
+
rodape: rodape ? { titulo: rodape.titulo, indice: i + 1, total: rodape.total } : null,
|
|
96
|
+
});
|
|
86
97
|
no.style.position = "absolute";
|
|
87
98
|
no.style.inset = "0";
|
|
88
99
|
no.style.display = i === 0 ? "block" : "none";
|
|
@@ -93,6 +104,7 @@
|
|
|
93
104
|
|
|
94
105
|
ajustar();
|
|
95
106
|
ir(0);
|
|
107
|
+
if (carregadoOk) { carregadoOk(a); carregadoOk = null; }
|
|
96
108
|
|
|
97
109
|
if (location.hash === "#imprimir") return setTimeout(imprimirTudo, 350);
|
|
98
110
|
|
|
@@ -259,7 +271,17 @@
|
|
|
259
271
|
que vem depois no arquivo — invadir é justamente a pausa que se
|
|
260
272
|
queria cortar. */
|
|
261
273
|
if (alvo >= t.ate) { if (!v.paused) v.pause(); return; }
|
|
262
|
-
|
|
274
|
+
/* Uma busca por vez, e com respiro. Numa máquina lenta (ou na
|
|
275
|
+
exportação escondida, que rasteriza a cena a cada quadro no mesmo
|
|
276
|
+
fio), o <video> fica atrás do relógio por mais de DERIVA em TODO
|
|
277
|
+
quadro: puxá-lo de volta a cada rAF é uma busca em cima da outra, e
|
|
278
|
+
um vídeo que só busca nunca tem quadro pronto — readyState fica em 1,
|
|
279
|
+
o export não desenha o clipe e o arquivo sai com o chão da cena no
|
|
280
|
+
lugar da gravação. Medido quadro a quadro no diagnóstico. */
|
|
281
|
+
if (v.seeking) return;
|
|
282
|
+
var agora = performance.now();
|
|
283
|
+
if (Math.abs(v.currentTime - alvo) > DERIVA && (!v._ultimaBusca || agora - v._ultimaBusca > 600)) {
|
|
284
|
+
v._ultimaBusca = agora;
|
|
263
285
|
try { v.currentTime = alvo; } catch (e) {}
|
|
264
286
|
}
|
|
265
287
|
if (tocando && v.paused) tocarClipe(v);
|
|
@@ -679,19 +701,17 @@ function gravaMp4() { return /mp4/.test(escolherFormato()); }
|
|
|
679
701
|
* o mesmo grafo de sempre — trilha, narração e efeitos, já mixados com o
|
|
680
702
|
* ducking — então o arquivo sai idêntico ao que se ouve aqui.
|
|
681
703
|
*/
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
704
|
+
/* Monta o arquivo com o PLAYER como fonte, sem baixar. É o que o botão usa
|
|
705
|
+
por baixo e o que a exportação escondida (studio_exportar) chama por
|
|
706
|
+
fora — um caminho só, para o arquivo sair igual nos dois.
|
|
707
|
+
|
|
708
|
+
O player é a fonte: ele sabe encadear cenas, narrar no tempo certo e
|
|
709
|
+
tocar os efeitos. O exportador só pergunta o que está na tela. */
|
|
710
|
+
function gerarArquivo(op) {
|
|
711
|
+
op = op || {};
|
|
685
712
|
if (typeof VideoExport === "undefined" || !VideoExport.disponivel()) {
|
|
686
|
-
return
|
|
713
|
+
return Promise.reject(new Error("este navegador não sabe gerar vídeo"));
|
|
687
714
|
}
|
|
688
|
-
|
|
689
|
-
b.dataset.exportando = "1";
|
|
690
|
-
b.classList.add("gravando");
|
|
691
|
-
aviso("montando o vídeo — não feche esta aba", 3000);
|
|
692
|
-
|
|
693
|
-
// O player é a fonte: ele sabe encadear cenas, narrar no tempo certo e
|
|
694
|
-
// tocar os efeitos. O exportador só pergunta o que está na tela.
|
|
695
715
|
var fonte = {
|
|
696
716
|
duracao: total(),
|
|
697
717
|
// A gravação do usuário precisa estar decodificada antes do primeiro
|
|
@@ -701,13 +721,32 @@ function gravaMp4() { return /mp4/.test(escolherFormato()); }
|
|
|
701
721
|
parar: function () { pausar(); },
|
|
702
722
|
no: function () { return palco.children[atual] || null; },
|
|
703
723
|
};
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
724
|
+
// O AudioContext nasce suspenso sem gesto; a exportação escondida chega
|
|
725
|
+
// com um gesto emprestado, e este resume é o que o gasta.
|
|
726
|
+
if (trilha && trilha.ctx && trilha.ctx.state === "suspended") { try { trilha.ctx.resume(); } catch (e) {} }
|
|
727
|
+
return VideoExport.gerar(arq, {
|
|
707
728
|
fonte: fonte,
|
|
729
|
+
fps: op.fps,
|
|
708
730
|
fluxoAudio: trilha && trilha.fluxo,
|
|
731
|
+
aoAndar: op.aoAndar,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function iniciarGravacao() {
|
|
736
|
+
var b = document.getElementById("tp-gravar");
|
|
737
|
+
if (!b || b.dataset.exportando) return;
|
|
738
|
+
if (typeof VideoExport === "undefined" || !VideoExport.disponivel()) {
|
|
739
|
+
return aviso("este navegador não sabe gerar vídeo", 3200);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
b.dataset.exportando = "1";
|
|
743
|
+
b.classList.add("gravando");
|
|
744
|
+
aviso("montando o vídeo — não feche esta aba", 3000);
|
|
745
|
+
|
|
746
|
+
gerarArquivo({
|
|
709
747
|
aoAndar: function (f) { b.textContent = Math.round(f * 100) + "%"; },
|
|
710
748
|
}).then(function (r) {
|
|
749
|
+
VideoExport.salvar(r, arq.titulo);
|
|
711
750
|
aviso(r.mp4 ? "vídeo salvo em .mp4" : "salvo em .webm (este navegador não faz mp4)", 3400);
|
|
712
751
|
}).catch(function (e) {
|
|
713
752
|
aviso("não consegui gerar: " + e.message, 4200);
|
|
@@ -829,5 +868,8 @@ function gravaMp4() { return /mp4/.test(escolherFormato()); }
|
|
|
829
868
|
ir: function (n) { ir(n - 1); }, proxima: proxima, anterior: anterior,
|
|
830
869
|
tocar: tocar, parar: pausar, buscar: buscar, gravar: alternarGravacao,
|
|
831
870
|
get cena() { return atual + 1; }, get total() { return cenas.length; },
|
|
871
|
+
// Para a exportação escondida: espera o arquivo montado, troca o que
|
|
872
|
+
// precisar (um clipe convertido) e gera pelo mesmo caminho do botão.
|
|
873
|
+
carregado: carregado, remontar: montar, exportar: gerarArquivo,
|
|
832
874
|
};
|
|
833
875
|
})();
|