primocode 8.22.1 → 8.25.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.
Files changed (35) hide show
  1. package/lib/catalogo.js +2 -0
  2. package/lib/config.js +53 -2
  3. package/lib/studio.js +125 -11
  4. package/lib/tools.js +35 -1
  5. package/package.json +2 -2
  6. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  7. package/studio/arquivos/corte-estatico-corte-a5bab.json +150 -0
  8. package/studio/arquivos/corte-founders-ai-teste-do-player-2de1d.json +65 -0
  9. package/studio/arquivos/corte-legado-teste.json +1 -0
  10. package/studio/arquivos/corte-legado2.json +1 -0
  11. package/studio/arquivos/corte-minimo-2a829.json +24 -0
  12. package/studio/arquivos/corte-ok-00a3b.json +18 -0
  13. package/studio/arquivos/corte-ok-1270e.json +22 -0
  14. package/studio/arquivos/corte-sweep-corte-cfb29.json +147 -0
  15. package/studio/arquivos/corte-visual-completo-78f50.json +35 -0
  16. package/studio/arquivos/grade-sweep-grade-0c2f1.json +119 -0
  17. package/studio/arquivos/prisma-estatico-prisma-d8740.json +182 -0
  18. package/studio/arquivos/prisma-sweep-prisma-4c58c.json +166 -0
  19. package/studio/arquivos/prisma-y-b0785.json +52 -0
  20. package/studio/arquivos/prosa-sweep-prosa-f29cf.json +80 -0
  21. package/studio/arquivos/tela-estatico-tela-c7daf.json +90 -0
  22. package/studio/arquivos/tela-sweep-tela-0d2bf.json +84 -0
  23. package/studio/arquivos/traco-estatico-traco-25872.json +113 -0
  24. package/studio/arquivos/traco-sweep-traco-0bcf4.json +102 -0
  25. package/studio/ferramentas/corte.md +159 -12
  26. package/studio/server.py +264 -8
  27. package/studio/web/analise.html +52 -0
  28. package/studio/web/js/analise.js +202 -0
  29. package/studio/web/js/exportar.js +88 -1
  30. package/studio/web/js/player.js +172 -46
  31. package/studio/web/js/render.js +99 -1
  32. package/studio/web/js/video.js +434 -0
  33. package/studio/web/render.css +27 -0
  34. package/studio/web/tool.html +1 -0
  35. package/studio/web/view.html +2 -0
package/lib/catalogo.js CHANGED
@@ -89,6 +89,8 @@ const GRUPOS = [
89
89
  { tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
90
90
  { tool: 'studio_midia', escreve: false, faz: 'manda imagem, música ou vídeo seu para usar nas peças',
91
91
  args: 'arquivo (opcional): caminho no projeto — sem ele, lista a galeria. nome (opcional).' },
92
+ { tool: 'studio_analisar', escreve: false, faz: 'ouve um vídeo já enviado e diz onde estão as pausas, para cortá-las',
93
+ args: 'arquivo: a URL "/midia/..." do vídeo. pausa (opcional): segundos a partir dos quais conta como pausa (padrão 0.6). Devolve "trechos": vire cada um numa cena com {"tipo":"clipe","de":...,"ate":...}.' },
92
94
  ],
93
95
  },
94
96
  {
package/lib/config.js CHANGED
@@ -135,9 +135,31 @@ function findSimilarProject(projectName, minScore = 0.4) {
135
135
  return best;
136
136
  }
137
137
 
138
- function readGeneralMemory(limit = 2000) {
139
- try { return fs.readFileSync(GENERAL_MEMORY, 'utf-8').slice(0, limit); }
138
+ /* A memória geral como o modelo precisa vê-la: o começo E o fim.
139
+ *
140
+ * Isto lia os PRIMEIROS 2000 caracteres — que são o cabeçalho do template.
141
+ * As anotações vão para o FIM do arquivo, então tudo que o agente aprendia
142
+ * ficava fora do corte e nunca chegava ao modelo. O usuário via o sintoma
143
+ * exato: "nunca lembra do que fez no chat anterior". O arquivo enchia, a
144
+ * poda funcionava, e nada daquilo era lido.
145
+ *
146
+ * Agora vêm as duas pontas: o cabeçalho (o que a PESSOA escreveu sobre si,
147
+ * que é permanente) e as anotações mais recentes (o que o agente aprendeu,
148
+ * que é o que muda). O meio é o que se perde — e é o certo a perder. */
149
+ function readGeneralMemory(limit = 2600) {
150
+ let texto;
151
+ try { texto = fs.readFileSync(GENERAL_MEMORY, 'utf-8'); }
140
152
  catch { return ''; }
153
+ if (texto.length <= limit) return texto;
154
+
155
+ const blocos = texto.split(/\n(?=### )/);
156
+ const cabeca = (blocos.shift() || '').slice(0, Math.floor(limit * 0.45));
157
+ let recente = '';
158
+ for (let i = blocos.length - 1; i >= 0; i--) {
159
+ if (cabeca.length + recente.length + blocos[i].length > limit) break;
160
+ recente = '\n' + blocos[i] + recente;
161
+ }
162
+ return cabeca + (recente ? '\n' + recente : '');
141
163
  }
142
164
 
143
165
  // Recebem o DIRETÓRIO de trabalho, não o nome do projeto: quando o PrimoCode
@@ -156,6 +178,34 @@ function appendProjectMemory(dir, entry) {
156
178
  } catch {}
157
179
  }
158
180
 
181
+ /* O que o usuário JÁ criou no estúdio, para "lembra do vídeo tal" funcionar.
182
+ *
183
+ * A memória geral guarda "pedido → resultado" em texto corrido, e o id da
184
+ * peça se perdia no meio da frase. O dono pediu:
185
+ *
186
+ * "Ter memória entre os chats. Ela lembrar do que fez no chat anterior.
187
+ * Quando eu falar 'lembra do vídeo tal', ela vai lembrar. Na estrutura de
188
+ * arquivos, eu quero que você crie para mim a partir de vídeos, não só de
189
+ * código."
190
+ *
191
+ * Uma linha por peça, com tipo, título e id — que é o que o agente precisa
192
+ * para chamar studio_atualizar ou abrir de novo. Sem link: ele é derivável
193
+ * do id e ocuparia espaço em todo pedido seguinte, para sempre.
194
+ *
195
+ * A entrada é DEDUPLICADA por id: recriar ou atualizar a mesma peça não pode
196
+ * encher a memória de cópias — foi o que aconteceria num vídeo ajustado dez
197
+ * vezes numa tarefa só. */
198
+ function lembrarCriacao({ ferramenta, titulo, id }) {
199
+ if (!id) return;
200
+ const linha = `- criou ${ferramenta || 'peça'} "${String(titulo || 'sem título').slice(0, 70)}" — id \`${id}\``;
201
+ try {
202
+ ensureStructure();
203
+ const texto = fs.existsSync(GENERAL_MEMORY) ? fs.readFileSync(GENERAL_MEMORY, 'utf-8') : '';
204
+ if (texto.includes(`\`${id}\``)) return; // já anotada
205
+ appendGeneralMemory(linha);
206
+ } catch {}
207
+ }
208
+
159
209
  // A contraparte de appendProjectMemory, para o que vale em TODOS os projetos
160
210
  // (nome do usuário, preferência, decisão de longo prazo). Existia a promessa
161
211
  // no prompt — "escreve aqui o que descobrir sobre você" — mas nenhuma função
@@ -240,4 +290,5 @@ module.exports = {
240
290
  ensureStructure, slugify, stripAccents,
241
291
  createProjectDir, getProjectDir, listProjects, findSimilarProject,
242
292
  readGeneralMemory, readProjectMemory, appendProjectMemory, appendGeneralMemory,
293
+ lembrarCriacao,
243
294
  };
package/lib/studio.js CHANGED
@@ -412,9 +412,32 @@ const TIPOS_MIDIA = {
412
412
 
413
413
  const midiaListar = () => comStudio(async () => ({ ok: true, midia: await pedir('GET', '/api/midia') }));
414
414
 
415
+ /* O caminho como ele chega quando a pessoa ARRASTA o arquivo para o terminal.
416
+ *
417
+ * "Eu quero que eu consiga arrastar os meus vídeos pra dentro do terminal."
418
+ * Arrastar, num terminal, cola o caminho — e cola do jeito do sistema: o
419
+ * macOS escapa espaço com barra invertida, o Windows põe aspas em volta, e
420
+ * alguns colam file:// na frente. `path.resolve` de qualquer um desses dá um
421
+ * caminho que não existe, e a resposta era "arquivo não existe" para um
422
+ * arquivo que está bem ali.
423
+ */
424
+ function limparCaminho(bruto) {
425
+ let s = String(bruto || '').trim();
426
+ if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
427
+ s = s.slice(1, -1);
428
+ }
429
+ if (/^file:\/\//i.test(s)) {
430
+ try { s = decodeURIComponent(s.replace(/^file:\/\//i, '')); } catch { /* fica como está */ }
431
+ }
432
+ // Só desescapa o que o shell escapa; um "\" de caminho do Windows fica.
433
+ s = s.replace(/\\([ ()'"&!$`\\])/g, '$1');
434
+ if (s.startsWith('~/') || s === '~') s = path.join(os.homedir(), s.slice(1));
435
+ return s.trim();
436
+ }
437
+
415
438
  const midiaEnviar = ({ arquivo, nome }) => comStudio(async () => {
416
439
  if (!arquivo) return { ok: false, error: 'Diga o caminho do arquivo (arquivo: "foto.jpg").' };
417
- const caminho = path.resolve(arquivo);
440
+ const caminho = path.resolve(limparCaminho(arquivo));
418
441
  if (!fs.existsSync(caminho)) {
419
442
  return {
420
443
  ok: false,
@@ -430,22 +453,113 @@ const midiaEnviar = ({ arquivo, nome }) => comStudio(async () => {
430
453
  return { ok: false, error: `Formato não aceito: ${ext}`, aceitos: Object.keys(TIPOS_MIDIA).join(' ') };
431
454
  }
432
455
  const bytes = fs.statSync(caminho).size;
433
- if (bytes > 60 * 1024 * 1024) return { ok: false, error: `Arquivo tem ${(bytes / 1048576).toFixed(1)} MB; o limite é 60 MB.` };
456
+ const ehVideo = ext === '.mp4' || ext === '.webm' || ext === '.mov';
457
+ const teto = ehVideo ? 2 * 1024 * 1024 * 1024 : 60 * 1024 * 1024;
458
+ if (bytes > teto) {
459
+ return { ok: false, error: `Arquivo tem ${(bytes / 1048576).toFixed(1)} MB; `
460
+ + `o limite é ${Math.round(teto / 1048576)} MB.` };
461
+ }
434
462
 
435
- const tipo = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
436
- '.webp': 'image/webp', '.svg': 'image/svg+xml', '.avif': 'image/avif', '.mp3': 'audio/mpeg',
437
- '.m4a': 'audio/mp4', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.aac': 'audio/aac',
438
- '.flac': 'audio/flac', '.opus': 'audio/opus', '.weba': 'audio/webm', '.mp4': 'video/mp4',
439
- '.webm': 'video/webm', '.mov': 'video/quicktime' }[ext] || 'application/octet-stream';
463
+ // O corpo vai em pedaços, direto do disco para o socket. Em base64 um
464
+ // vídeo de celular precisaria caber inteiro na memória três vezes — e o
465
+ // Node estourava antes de o estúdio ver um byte.
466
+ const r = await enviarArquivo(caminho, nome || path.basename(caminho));
467
+ const comoUsar = ehVideo
468
+ ? `Use "${r.url}" no "src" de um elemento {"tipo":"clipe"}. Para cortar as pausas, `
469
+ + 'escreva VÁRIOS clipes do mesmo src, cada um com "de" e "ate" em segundos.'
470
+ : `Use "${r.url}" em fundo.imagem, em src de uma imagem, ou em trilha.src.`;
471
+ return { ok: true, ...r, comoUsar };
472
+ });
440
473
 
441
- const dados = `data:${tipo};base64,` + fs.readFileSync(caminho).toString('base64');
442
- const r = await pedir('POST', '/api/midia', { nome: nome || path.basename(caminho), dados }, 120000);
443
- return { ok: true, ...r, comoUsar: `Use "${r.url}" em fundo.imagem, em src de uma imagem, ou em trilha.src.` };
474
+ /* PUT /api/midia/<nome> com o arquivo cru no corpo. */
475
+ function enviarArquivo(caminho, nome) {
476
+ return new Promise((resolve, reject) => {
477
+ const bytes = fs.statSync(caminho).size;
478
+ const req = http.request({
479
+ hostname: '127.0.0.1', port: PORTA, method: 'PUT',
480
+ path: '/api/midia/' + encodeURIComponent(nome),
481
+ // Vídeo grande copiando para o disco leva tempo; meia hora é
482
+ // folga para um arquivo de duas horas, e continua sendo um teto.
483
+ timeout: 30 * 60 * 1000,
484
+ headers: { 'X-Primo-Agente': '1', 'Content-Type': 'application/octet-stream',
485
+ 'Content-Length': bytes },
486
+ }, (res) => {
487
+ let texto = '';
488
+ res.setEncoding('utf8');
489
+ res.on('data', (c) => { texto += c; });
490
+ res.on('end', () => {
491
+ let corpo = null;
492
+ try { corpo = JSON.parse(texto); } catch { /* texto cru serve */ }
493
+ if (res.statusCode >= 400) {
494
+ return reject(new Error((corpo && corpo.erro) || texto.slice(0, 300)));
495
+ }
496
+ resolve(corpo || {});
497
+ });
498
+ });
499
+ req.on('timeout', () => { req.destroy(new Error('o envio demorou demais')); });
500
+ req.on('error', reject);
501
+ const leitura = fs.createReadStream(caminho);
502
+ leitura.on('error', (e) => { req.destroy(e); reject(e); });
503
+ leitura.pipe(req);
504
+ });
505
+ }
506
+
507
+ /* ── Ouvir o vídeo para achar as pausas ──────────────────────────────────
508
+ *
509
+ * "Ele vai cortar as pausas demoradas no vídeo."
510
+ *
511
+ * Cortar pausa exige saber onde a fala para, e nada aqui lê o áudio de um
512
+ * .mp4: o PrimoCode não tem dependência, e o Python da máquina lê WAV e mais
513
+ * nada. ffmpeg resolveria e seria mais uma instalação de 80 MB para quem só
514
+ * quer editar um vídeo.
515
+ *
516
+ * Quem decodifica MP4, MOV e webm já está aberto na mesa. A conta roda numa
517
+ * aba que abre, trabalha e se fecha; aqui a gente espera a resposta.
518
+ *
519
+ * `abrir` é injetado (é a função de abrir navegador do tools.js) para esta
520
+ * camada não passar a conhecer o sistema operacional.
521
+ */
522
+ const analisarAudio = ({ arquivo, pausa, abrir }) => comStudio(async () => {
523
+ if (!arquivo) return { ok: false, error: 'Diga qual mídia analisar (arquivo: "/midia/x.mp4").' };
524
+ const tarefa = await pedir('POST', '/api/analise', { arquivo, pausa });
525
+ if (abrir) { try { abrir(`${BASE}${tarefa.pagina}`); } catch { /* segue e expira */ } }
526
+
527
+ // Um vídeo de meia hora leva alguns segundos para decodificar; três
528
+ // minutos é folga com sobra, e continua sendo um teto — sem ele um
529
+ // navegador que nunca abriu deixaria a tarefa pendurada para sempre.
530
+ const prazo = Date.now() + 180000;
531
+ while (Date.now() < prazo) {
532
+ await new Promise((ok) => setTimeout(ok, 700));
533
+ const estado = await pedir('GET', `/api/analise/${tarefa.id}`, null, 8000).catch(() => null);
534
+ if (!estado) continue;
535
+ if (estado.estado === 'erro') {
536
+ return { ok: false, error: (estado.resultado && estado.resultado.erro) || 'a análise falhou' };
537
+ }
538
+ if (estado.estado === 'pronto') {
539
+ const r = estado.resultado || {};
540
+ return {
541
+ ok: true,
542
+ duracao: r.duracao,
543
+ cortado: r.cortado,
544
+ pausas: r.pausas,
545
+ trechos: r.trechos,
546
+ comoUsar: `${(r.trechos || []).length} trecho(s) sem pausa. Escreva UMA CENA por trecho, `
547
+ + `cada uma com {"tipo":"clipe","src":"${arquivo}","de":<início>,"ate":<fim>}. `
548
+ + 'NÃO escreva "duracao" nessas cenas: a cena já dura o trecho.',
549
+ };
550
+ }
551
+ }
552
+ return {
553
+ ok: false,
554
+ error: 'A aba de análise não respondeu em 3 minutos. Ela precisa do navegador aberto. '
555
+ + 'Siga sem o corte automático: escreva os trechos que o usuário pedir, ou use o vídeo inteiro '
556
+ + 'num clipe só (sem "de"/"ate").',
557
+ };
444
558
  });
445
559
 
446
560
  module.exports = {
447
561
  BASE, PORTA, CASA,
448
562
  garantirNoAr, estaNoAr, prepararCasa, pythonCmd,
449
563
  manual, ferramentas, spec, listar, ler, criar, atualizar, versoesDe,
450
- midiaListar, midiaEnviar,
564
+ midiaListar, midiaEnviar, analisarAudio,
451
565
  };
package/lib/tools.js CHANGED
@@ -172,6 +172,8 @@ async function runShell(command, timeout = 60000, cwd) {
172
172
  // `https://x/"; touch /tmp/pwned; echo "` injetava comando (achado de segurança).
173
173
  // Quando o auto-abrir do Studio abriu pela última vez — o freio das mil abas.
174
174
  let ultimaAberturaStudio = 0;
175
+ // Uma rodada de melhoria por tarefa (ver o aviso em studio_criar).
176
+ let melhorouStudio = false;
175
177
 
176
178
  function openInBrowser(target) {
177
179
  const p = os.platform();
@@ -513,6 +515,13 @@ async function despachar(name, args, ctx) {
513
515
  // saber abrir". E cada "abre pra mim" digitado depois custava
514
516
  // uma tarefa inteira de tokens para o modelo redescobrir o
515
517
  // link. Abrir é determinístico: não se gasta modelo com isso.
518
+ // Fica anotado na memória geral: é assim que "lembra do
519
+ // vídeo tal" funciona na conversa seguinte. O id é o que
520
+ // permite reabrir ou ajustar sem recriar.
521
+ if (r && r.ok !== false && r.id) {
522
+ cfg.lembrarCriacao({ ferramenta: r.ferramenta || args.ferramenta,
523
+ titulo: r.titulo || args.titulo, id: r.id });
524
+ }
516
525
  if (r && r.ok !== false && r.ver) {
517
526
  // UMA aba por tarefa. O modelo às vezes cria três versões
518
527
  // seguidas, e abrir todas enchia o navegador do usuário de
@@ -523,7 +532,24 @@ async function despachar(name, args, ctx) {
523
532
  ultimaAberturaStudio = agora;
524
533
  try { openInBrowser(r.ver); } catch {}
525
534
  r.abertoNoNavegador = true;
526
- r.aviso = 'JÁ ABERTO no navegador do usuário. Chame finish AGORA dizendo o que foi criado — não abra de novo, não leia de volta, não ajuste sem pedido.';
535
+ // O estúdio revisa a peça e devolve o que ficou
536
+ // monótono (mesmo fundo em tudo, transição repetida,
537
+ // sem trilha). Sem este ramo o aviso mandaria "chame
538
+ // finish, não ajuste" — e a revisão morreria ali, que
539
+ // é justamente o "fica tudo genérico".
540
+ //
541
+ // UMA rodada de conserto, e acabou: mais que isso
542
+ // vira o agente polindo sozinho enquanto o usuário
543
+ // espera.
544
+ if (Array.isArray(r.podeMelhorar) && r.podeMelhorar.length && !melhorouStudio) {
545
+ melhorouStudio = true;
546
+ r.aviso = 'JÁ ABERTO no navegador. Antes de finalizar, faça UMA correção: '
547
+ + `use studio_atualizar no id "${r.id}" mandando SÓ as chaves que mudam, `
548
+ + 'para resolver os pontos de "podeMelhorar". Depois chame finish. '
549
+ + 'Não crie outro arquivo e não repita esta correção.';
550
+ } else {
551
+ r.aviso = 'JÁ ABERTO no navegador do usuário. Chame finish AGORA dizendo o que foi criado — não abra de novo, não leia de volta, não ajuste sem pedido.';
552
+ }
527
553
  } else {
528
554
  r.aviso = `Você JÁ criou uma versão nesta tarefa, e ela está aberta no navegador. NÃO crie outra: `
529
555
  + `para mudar algo, use studio_atualizar com o id "${r.id}". Chame finish AGORA.`;
@@ -544,6 +570,14 @@ async function despachar(name, args, ctx) {
544
570
  return args.arquivo
545
571
  ? studio.midiaEnviar({ arquivo: resolveInProject(args.arquivo, projectDir), nome: args.nome })
546
572
  : studio.midiaListar();
573
+ case 'studio_analisar':
574
+ // Ouvir o vídeo para achar as pausas. A conta acontece no
575
+ // navegador (é quem sabe decodificar MP4 sem instalar nada) —
576
+ // por isso passa a função de abrir, e por isso demora alguns
577
+ // segundos em vez de responder na hora.
578
+ return studio.analisarAudio({
579
+ arquivo: args.arquivo, pausa: args.pausa, abrir: openInBrowser,
580
+ });
547
581
 
548
582
  case 'desktop_double_click':
549
583
  return desktop.doubleClick(args.x, args.y);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.22.1",
3
+ "version": "8.25.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"
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"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -0,0 +1,150 @@
1
+ {
2
+ "id": "corte-estatico-corte-a5bab",
3
+ "ferramenta": "corte",
4
+ "titulo": "estatico-corte",
5
+ "doc": {
6
+ "tema": "brasa",
7
+ "largura": 1080,
8
+ "altura": 1920,
9
+ "acabamento": "grao",
10
+ "trilha": {
11
+ "tipo": "gerada",
12
+ "estilo": "pulso",
13
+ "volume": 0.7
14
+ },
15
+ "cenas": [
16
+ {
17
+ "layout": "centro",
18
+ "duracao": 3,
19
+ "fundo": "aurora",
20
+ "transicao": "flash",
21
+ "elementos": [
22
+ {
23
+ "tipo": "texto",
24
+ "papel": "titulo",
25
+ "texto": "Sua conta subiu\nde novo.",
26
+ "tamanho": 124,
27
+ "estilo": "gradiente",
28
+ "anim": "nenhuma"
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "layout": "centro",
34
+ "duracao": 3,
35
+ "fundo": "vinheta",
36
+ "transicao": "limpar",
37
+ "elementos": [
38
+ {
39
+ "tipo": "texto",
40
+ "papel": "titulo",
41
+ "texto": "E não foi o seu chuveiro.",
42
+ "tamanho": 78,
43
+ "estilo": "marcador",
44
+ "anim": "nenhuma"
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "layout": "centro",
50
+ "duracao": 4,
51
+ "transicao": "zoom",
52
+ "acabamento": "vinheta",
53
+ "fundo": {
54
+ "imagem": "/midia/torre.jpg",
55
+ "escurecer": 0.5
56
+ },
57
+ "elementos": [
58
+ {
59
+ "tipo": "numeros",
60
+ "contar": true,
61
+ "anim": "nenhuma",
62
+ "tamanho": 180,
63
+ "itens": [
64
+ "+38% | bandeira tarifária em 12 meses"
65
+ ]
66
+ }
67
+ ]
68
+ },
69
+ {
70
+ "layout": "livre",
71
+ "duracao": 4,
72
+ "transicao": "deslizar",
73
+ "elementos": [
74
+ {
75
+ "tipo": "imagem",
76
+ "src": "/midia/campo.jpg",
77
+ "x": 0,
78
+ "y": 0,
79
+ "l": 1080,
80
+ "a": 1920,
81
+ "raio": 0,
82
+ "movimento": "aproximar",
83
+ "filtro": "contraste",
84
+ "anim": "nenhuma"
85
+ },
86
+ {
87
+ "tipo": "forma",
88
+ "forma": "retangulo",
89
+ "x": 0,
90
+ "y": 1180,
91
+ "l": 1080,
92
+ "a": 740,
93
+ "preenchimento": "rgba(10,5,6,.72)",
94
+ "anim": "nenhuma"
95
+ },
96
+ {
97
+ "tipo": "texto",
98
+ "x": 90,
99
+ "y": 1320,
100
+ "l": 900,
101
+ "a": 420,
102
+ "texto": "O custo está\nna transmissão",
103
+ "tamanho": 104,
104
+ "peso": 750,
105
+ "estilo": "sombra",
106
+ "anim": "nenhuma"
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ "layout": "centro",
112
+ "duracao": 5,
113
+ "fundo": "raios",
114
+ "transicao": "subir",
115
+ "elementos": [
116
+ {
117
+ "tipo": "lista",
118
+ "revelar": true,
119
+ "tamanho": 52,
120
+ "intervalo": 0.9,
121
+ "itens": [
122
+ "Linhas saturadas no Nordeste",
123
+ "Usina gerando sem escoar",
124
+ "A conta chega em quem consome"
125
+ ],
126
+ "anim": "nenhuma"
127
+ }
128
+ ]
129
+ },
130
+ {
131
+ "layout": "centro",
132
+ "duracao": 3,
133
+ "fundo": "brilho",
134
+ "transicao": "fade",
135
+ "acabamento": "luz",
136
+ "elementos": [
137
+ {
138
+ "tipo": "texto",
139
+ "papel": "titulo",
140
+ "texto": "Segue para\nentender a próxima.",
141
+ "tamanho": 96,
142
+ "estilo": "contorno-tom",
143
+ "anim": "nenhuma"
144
+ }
145
+ ]
146
+ }
147
+ ]
148
+ },
149
+ "atualizado": "2026-08-16T15:08:28+00:00"
150
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "id": "corte-founders-ai-teste-do-player-2de1d",
3
+ "ferramenta": "corte",
4
+ "titulo": "Founders AI — teste do player",
5
+ "doc": {
6
+ "tema": "brasa",
7
+ "largura": 1080,
8
+ "altura": 1920,
9
+ "acabamento": "grao",
10
+ "trilha": {
11
+ "tipo": "gerada",
12
+ "estilo": "pulso",
13
+ "volume": 0.7
14
+ },
15
+ "cenas": [
16
+ {
17
+ "layout": "centro",
18
+ "duracao": 3,
19
+ "fundo": "aurora",
20
+ "transicao": "flash",
21
+ "elementos": [
22
+ {
23
+ "tipo": "texto",
24
+ "papel": "titulo",
25
+ "texto": "Imersão\nFounders AI",
26
+ "tamanho": 124,
27
+ "estilo": "gradiente",
28
+ "anim": "desfoque"
29
+ }
30
+ ]
31
+ },
32
+ {
33
+ "layout": "centro",
34
+ "duracao": 3,
35
+ "fundo": "vinheta",
36
+ "elementos": [
37
+ {
38
+ "tipo": "texto",
39
+ "papel": "titulo",
40
+ "texto": "Paris Group · Chapecó",
41
+ "tamanho": 78,
42
+ "estilo": "marcador",
43
+ "anim": "palavra"
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ "layout": "centro",
49
+ "duracao": 4,
50
+ "fundo": "raios",
51
+ "elementos": [
52
+ {
53
+ "tipo": "numeros",
54
+ "contar": true,
55
+ "tamanho": 180,
56
+ "itens": [
57
+ "20 | empresários por turma"
58
+ ]
59
+ }
60
+ ]
61
+ }
62
+ ]
63
+ },
64
+ "atualizado": "2026-08-16T14:55:24+00:00"
65
+ }
@@ -0,0 +1 @@
1
+ {"id": "corte-legado-teste", "ferramenta": "corte", "titulo": "legado", "doc": {"tema": "meia-noite", "largura": 1080, "altura": 1920, "cenas": [{"duracao": "4s", "fundo": "aurora", "elementos": [{"tipo": "titulo", "texto": "Imersão Founders AI"}, {"tipo": "subtitulo", "conteudo": "Paris Group · Chapecó"}]}]}}
@@ -0,0 +1 @@
1
+ {"id": "corte-legado2", "ferramenta": "corte", "titulo": "legado2", "doc": {"tema": "meia-noite", "largura": 1080, "altura": 1920, "cenas": [{"duracao": "4s", "fundo": "aurora", "elementos": [{"tipo": "titulo", "texto": "Imersão Founders AI", "anim": "nenhuma"}, {"tipo": "subtitulo", "conteudo": "Paris Group · Chapecó", "anim": "nenhuma"}]}]}}
@@ -0,0 +1,24 @@
1
+ {
2
+ "id": "corte-minimo-2a829",
3
+ "ferramenta": "corte",
4
+ "titulo": "minimo",
5
+ "doc": {
6
+ "tema": "claro",
7
+ "largura": 1080,
8
+ "altura": 1920,
9
+ "cenas": [
10
+ {
11
+ "layout": "centro",
12
+ "duracao": 5,
13
+ "elementos": [
14
+ {
15
+ "tipo": "texto",
16
+ "texto": "TESTE VISIVEL",
17
+ "tamanho": 120
18
+ }
19
+ ]
20
+ }
21
+ ]
22
+ },
23
+ "atualizado": "2026-08-16T14:57:24+00:00"
24
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "id": "corte-ok-00a3b",
3
+ "ferramenta": "corte",
4
+ "titulo": "ok",
5
+ "doc": {
6
+ "cenas": [
7
+ {
8
+ "elementos": [
9
+ {
10
+ "tipo": "texto",
11
+ "texto": "oi"
12
+ }
13
+ ]
14
+ }
15
+ ]
16
+ },
17
+ "atualizado": "2026-08-16T15:05:32+00:00"
18
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "corte-ok-1270e",
3
+ "ferramenta": "corte",
4
+ "titulo": "ok",
5
+ "doc": {
6
+ "tema": "brasa",
7
+ "cenas": [
8
+ {
9
+ "duracao": 3,
10
+ "elementos": [
11
+ {
12
+ "tipo": "texto",
13
+ "papel": "titulo",
14
+ "texto": "Founders AI",
15
+ "anim": "nenhuma"
16
+ }
17
+ ]
18
+ }
19
+ ]
20
+ },
21
+ "atualizado": "2026-08-16T17:50:21+00:00"
22
+ }