primocode 8.1.1 → 8.2.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 CHANGED
@@ -516,7 +516,9 @@ async function sendPrompt(userText) {
516
516
  // tokens). No auto, quem não vai usar ferramenta não paga por ela.
517
517
  let modo = state.mode;
518
518
  if (modo === 'auto') {
519
- const escolha = escolherModo(userText);
519
+ // O modo do pedido ANTERIOR entra na decisão: no meio de uma tarefa,
520
+ // "refaz" e "agora põe música" são continuação, não papo novo.
521
+ const escolha = escolherModo(userText, { ultimoModo: state.modoEfetivo });
520
522
  modo = escolha.modo;
521
523
  state.modoEfetivo = modo;
522
524
  const rotulo = modo === 'primocode' ? c.warn('Primo Code') : c.brandDim('Chat');
package/lib/catalogo.js CHANGED
@@ -85,6 +85,8 @@ const GRUPOS = [
85
85
  { tool: 'studio_atualizar', escreve: false, faz: 'ajusta o que já existe, sem refazer do zero',
86
86
  args: 'id. titulo (opcional). doc (opcional).' },
87
87
  { tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
88
+ { tool: 'studio_midia', escreve: false, faz: 'manda imagem, música ou vídeo seu para usar nas peças',
89
+ args: 'arquivo (opcional): caminho no projeto — sem ele, lista a galeria. nome (opcional).' },
88
90
  ],
89
91
  },
90
92
  {
package/lib/modo.js CHANGED
@@ -1,19 +1,28 @@
1
1
  /**
2
2
  * Escolha automática de modo.
3
3
  *
4
- * O modo primocode manda o schema das 44 ferramentas em todo pedido — ~4.700
5
- * tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que uma
6
- * refatoração, e num plano com teto diário isso é dinheiro jogado fora (foi
4
+ * O modo primocode manda o schema das 45 ferramentas em todo pedido — milhares
5
+ * de tokens antes de você digitar a primeira letra. Um "oi" gasta o mesmo que
6
+ * uma refatoração, e num plano com teto diário isso é dinheiro jogado fora (foi
7
7
  * assim que a cota de 100 mil tokens/dia acabou às 15h).
8
8
  *
9
9
  * O modo chat não manda schema nenhum. Então a regra é: só paga o preço das
10
10
  * ferramentas quem vai usar ferramenta.
11
11
  *
12
12
  * A decisão é local e sem custo — nenhuma chamada ao modelo para descobrir a
13
- * intenção, senão o remédio custaria como a doença. É um classificador de
14
- * pistas, deliberadamente conservador: na dúvida, chat, porque errar para chat
15
- * custa uma repetição do pedido, e errar para primocode custa tokens e pode
16
- * escrever arquivo que ninguém pediu.
13
+ * intenção, senão o remédio custaria como a doença.
14
+ *
15
+ * ── POR QUE ELE OLHA O TURNO ANTERIOR ────────────────────────────────────
16
+ * A primeira versão julgava cada frase sozinha, e errava feio no uso real: o
17
+ * usuário pedia um vídeo (tarefa), e o "refaz", o "continua" e o "agora põe
18
+ * música de fundo" logo depois caíam no chat — o agente perdia o fio no meio
19
+ * do trabalho. Medido na conversa do dono: 7 erros em 18 frases, quase todos
20
+ * de continuação.
21
+ *
22
+ * Conversa humana não repete o objeto a cada frase. Então, QUANDO O TURNO
23
+ * ANTERIOR FOI UMA TAREFA, a régua abaixa: o que não é claramente cumprimento
24
+ * ou dúvida conceitual continua sendo trabalho. Fora de uma tarefa, a régua
25
+ * volta a subir — na dúvida, chat.
17
26
  */
18
27
 
19
28
  'use strict';
@@ -34,6 +43,13 @@ const VERBOS = [
34
43
  // "pesquisa no google", "aperta enter", "arrasta o arquivo".
35
44
  'clic[ao]r?', 'clique', 'digit[ae]r?', 'pesquis[ae]r?', 'apert[ae]r?',
36
45
  'arrast[ae]r?', 'rol[ae]r?', 'minimiz[ae]r?', 'maximiz[ae]r?', 'fech[ae]r?',
46
+ // Ajuste do que já foi criado — o vocabulário do estúdio. Sem estes, "agora
47
+ // põe música de fundo" caía no chat, no meio de um vídeo.
48
+ 'p[õo]e', 'ponha', 'p[oô]r', 'coloc[ae]r?', 'tir[ae]r?', 'troc[ae]r?',
49
+ 'inclu[ai]r?', 'insir[ae]', 'insert', 'aument[ae]r?', 'diminu[ai]r?',
50
+ 'grav[ae]r?', 'narr[ae]r?', 'export[ae]r?', 'salv[ae]r?', 'envi[ae]r?',
51
+ 'convert[ae]r?', 'transform[ae]r?', 'divid[ai]r?', 'junt[ae]r?',
52
+ 'desenh[ae]r?', 'ilustr[ae]r?', 'resum[ae]r?', 'traduz[ai]r?',
37
53
  ];
38
54
  const RE_VERBO = new RegExp(`(^|\\s)(${VERBOS.join('|')})\\b`, 'i');
39
55
 
@@ -49,33 +65,75 @@ const RE_ARTEFATO = /\.(js|ts|tsx|jsx|html|css|json|md|py|sh|yml|yaml|txt|csv|en
49
65
  const RE_FERRAMENTA = /\b(navegador|browser|desktop|terminal|shell|git|npm|node|deploy|servidor|localhost|porta \d+|screenshot|print da tela|mouse|teclado|janela|[áa]rea de trabalho|aplicativo|[íi]cone)\b/i;
50
66
  const RE_PASTA = /\b(pasta|diret[óo]rio|arquivo|arquivos|reposit[óo]rio|repo|projeto)\b/i;
51
67
 
68
+ // O que o estúdio produz. "faz uns slides", "quero um post", "monta a planilha"
69
+ // — o substantivo sozinho já denuncia a tarefa, mesmo sem verbo claro.
70
+ const RE_PECA = /\b(slides?|apresenta[çc][ãa]o|deck|v[íi]deo|reels?|anima[çc][ãa]o|post|banner|capa|thumbnail|arte|cartaz|planilha|tabela|documento|relat[óo]rio|artigo|proposta|contrato|diagrama|fluxograma|organograma|mapa mental|narra[çc][ãa]o|trilha|legenda)\b/i;
71
+
52
72
  // Conversa: cumprimento, agradecimento, pergunta conceitual.
53
- const RE_SAUDACAO = /^\s*(oi+|ol[áa]|e?a[íi]|bom dia|boa tarde|boa noite|tudo bem|beleza|valeu|obrigad[oa]|brigad[oa]|tchau|até|ok|okay|entendi|legal|show|massa)\b[\s!.?]*$/i;
73
+ const RE_SAUDACAO = /^\s*(oi+|ol[áa]|e?a[íi]|bom dia|boa tarde|boa noite|tudo bem|beleza|valeu|obrigad[oa]|brigad[oa]|tchau|at[ée]|ok|okay|entendi|legal|show|massa|top|perfeito|isso|isso a[íi]|ta bom|t[áa] bom|certo)\b[\s!.?]*$/i;
54
74
  const RE_CONCEITUAL = /\b(o que [ée]|o q [ée]|que [ée]|qual (a|o|as|os)?\s*(diferen[çc]a|melhor|sentido)|por qu[êe]|pra que serve|para que serve|como funciona|como (fa[çc]o|posso|se faz|fazer)|explica|explique|me explica|significa|vale a pena|devo usar|qual usar|prefere)\b/i;
75
+ // "o que você faz?", "o que vc sabe fazer?" — é pergunta sobre ele, não pedido.
76
+ // Sem esta linha o "faz" batia no verbo e virava tarefa.
77
+ const RE_SOBRE_ELE = /\bo que (voc[êe]|vc|tu)\s+(faz|sabe|consegue|pode|[ée])\b|\bquem [ée] voc[êe]\b|\bcomo voc[êe] funciona\b/i;
78
+
79
+ // Satisfação: "ficou bom", "tá bom assim", "pode parar" — o usuário está
80
+ // dizendo que ACABOU, não pedindo mais trabalho. Sem isto, no meio de uma
81
+ // tarefa a régua abaixada mandava um elogio de volta para o agente.
82
+ const RE_SATISFEITO = /^\s*(t[áa]|ta|ficou|est[áa]|ta[' ]?)?\s*(bom|[óo]timo|legal|show|massa|perfeito|lindo|maneiro|isso)\b(\s+(assim|mesmo|ent[ãa]o|demais))?[\s!.]*$|^\s*(pode parar|chega|s[óo] isso|era isso|nada mais)\b/i;
83
+
84
+ // Continuação forte: sozinhas já significam "siga com o que estava fazendo".
85
+ const RE_CONTINUACAO = /^\s*(refa[çz]a?|refaz|de novo|novamente|outra vez|mais uma vez|continu[ae]|prossiga|segue|siga|vai|manda|tenta( de novo| outra vez)?|tente( de novo| outra vez)?|repete|repita)\b/i;
86
+
87
+ // Continuação fraca: só vale quando o turno anterior foi tarefa.
88
+ // "agora …", "e o …?", "também …", "mais …", "sem …", "com …".
89
+ const RE_CONTINUACAO_FRACA = /^\s*(agora|depois|ent[ãa]o|e (o|a|os|as|no|na)\b|tamb[ée]m|mais|menos|sem|com|s[óo]|apenas|troca|muda)\b/i;
55
90
 
56
91
  /**
57
92
  * Decide o modo de um pedido.
58
- * Devolve { modo: 'primocode'|'chat', motivo: string }.
93
+ *
94
+ * @param {string} texto o que o usuário digitou
95
+ * @param {{ultimoModo?: string}} ctx contexto da conversa; `ultimoModo` é o
96
+ * modo que o pedido ANTERIOR usou de fato ('primocode' | 'chat').
97
+ * @returns {{modo:'primocode'|'chat', motivo:string}}
98
+ *
59
99
  * O motivo existe para o CLI mostrar por que escolheu — automode que decide em
60
100
  * silêncio parece defeito na primeira vez que erra.
61
101
  */
62
- function escolherModo(texto) {
102
+ function escolherModo(texto, ctx = {}) {
63
103
  const t = String(texto || '').trim();
64
104
  if (!t) return { modo: 'chat', motivo: 'nada a fazer' };
65
105
 
106
+ const noMeioDeUmaTarefa = (ctx && ctx.ultimoModo) === 'primocode';
107
+
66
108
  // Cumprimento é chat mesmo que traga um verbo depois ("oi, tudo bem?").
67
109
  if (RE_SAUDACAO.test(t)) return { modo: 'chat', motivo: 'cumprimento' };
110
+ if (RE_SATISFEITO.test(t)) return { modo: 'chat', motivo: 'está satisfeito' };
68
111
 
69
- // Pergunta conceitual ganha do verbo: "como faço para criar um site?" é
70
- // dúvida, não pedido de execução.
112
+ // Pergunta sobre ele mesmo, e pergunta conceitual, ganham do verbo:
113
+ // "como faço para criar um site?" é dúvida, não pedido de execução.
114
+ if (RE_SOBRE_ELE.test(t)) return { modo: 'chat', motivo: 'pergunta sobre ele' };
71
115
  if (RE_CONCEITUAL.test(t)) return { modo: 'chat', motivo: 'pergunta conceitual' };
72
116
 
117
+ // "refaz", "de novo", "continua" — retomada, com ou sem contexto.
118
+ if (RE_CONTINUACAO.test(t)) return { modo: 'primocode', motivo: 'retoma a tarefa' };
119
+
120
+ // Pistas diretas.
73
121
  if (RE_ARTEFATO.test(t)) return { modo: 'primocode', motivo: 'cita arquivo ou caminho' };
74
122
  if (RE_VERBO.test(t)) return { modo: 'primocode', motivo: 'pede uma ação' };
123
+ if (RE_PECA.test(t)) return { modo: 'primocode', motivo: 'fala de algo para criar' };
75
124
  if (RE_DESEJO.test(t)) return { modo: 'primocode', motivo: 'expressa o que quer que exista' };
76
125
  if (RE_FERRAMENTA.test(t)) return { modo: 'primocode', motivo: 'menciona ferramenta ou máquina' };
77
126
  if (RE_PASTA.test(t)) return { modo: 'primocode', motivo: 'fala de arquivo ou projeto' };
78
127
 
128
+ // No meio de uma tarefa a régua abaixa: conversa não repete o objeto a cada
129
+ // frase, e o custo de errar aqui é o agente perder o fio.
130
+ if (noMeioDeUmaTarefa) {
131
+ if (RE_CONTINUACAO_FRACA.test(t)) return { modo: 'primocode', motivo: 'continua o que estava fazendo' };
132
+ // Pergunta curta no meio do trabalho costuma ser sobre o trabalho
133
+ // ("e não dá pra deixar rodando?"), não papo novo.
134
+ if (t.split(/\s+/).length <= 12) return { modo: 'primocode', motivo: 'segue a tarefa em andamento' };
135
+ }
136
+
79
137
  return { modo: 'chat', motivo: 'sem pista de tarefa' };
80
138
  }
81
139
 
package/lib/studio.js CHANGED
@@ -191,8 +191,43 @@ const atualizar = ({ id, titulo, doc }) => comStudio(async () => {
191
191
  return { ok: true, ...r };
192
192
  });
193
193
 
194
+ // ── Mídia: a imagem de fundo e a música do usuário ───────────────────────
195
+ // Sem isto o agente não tinha como pôr foto de fundo nem trilha própria: a
196
+ // API pede base64, e ele não ia montar data URI na mão dentro de um curl.
197
+ const TIPOS_MIDIA = {
198
+ '.png': 1, '.jpg': 1, '.jpeg': 1, '.gif': 1, '.webp': 1, '.svg': 1, '.avif': 1,
199
+ '.mp3': 1, '.m4a': 1, '.wav': 1, '.ogg': 1, '.aac': 1, '.flac': 1, '.opus': 1, '.weba': 1,
200
+ '.mp4': 1, '.webm': 1, '.mov': 1,
201
+ };
202
+
203
+ const midiaListar = () => comStudio(async () => ({ ok: true, midia: await pedir('GET', '/api/midia') }));
204
+
205
+ const midiaEnviar = ({ arquivo, nome }) => comStudio(async () => {
206
+ if (!arquivo) return { ok: false, error: 'Diga o caminho do arquivo (arquivo: "foto.jpg").' };
207
+ const caminho = path.resolve(arquivo);
208
+ if (!fs.existsSync(caminho)) return { ok: false, error: `Arquivo não existe: ${arquivo}` };
209
+
210
+ const ext = path.extname(caminho).toLowerCase();
211
+ if (!TIPOS_MIDIA[ext]) {
212
+ return { ok: false, error: `Formato não aceito: ${ext}`, aceitos: Object.keys(TIPOS_MIDIA).join(' ') };
213
+ }
214
+ const bytes = fs.statSync(caminho).size;
215
+ if (bytes > 60 * 1024 * 1024) return { ok: false, error: `Arquivo tem ${(bytes / 1048576).toFixed(1)} MB; o limite é 60 MB.` };
216
+
217
+ const tipo = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
218
+ '.webp': 'image/webp', '.svg': 'image/svg+xml', '.avif': 'image/avif', '.mp3': 'audio/mpeg',
219
+ '.m4a': 'audio/mp4', '.wav': 'audio/wav', '.ogg': 'audio/ogg', '.aac': 'audio/aac',
220
+ '.flac': 'audio/flac', '.opus': 'audio/opus', '.weba': 'audio/webm', '.mp4': 'video/mp4',
221
+ '.webm': 'video/webm', '.mov': 'video/quicktime' }[ext] || 'application/octet-stream';
222
+
223
+ const dados = `data:${tipo};base64,` + fs.readFileSync(caminho).toString('base64');
224
+ const r = await pedir('POST', '/api/midia', { nome: nome || path.basename(caminho), dados }, 120000);
225
+ return { ok: true, ...r, comoUsar: `Use "${r.url}" em fundo.imagem, em src de uma imagem, ou em trilha.src.` };
226
+ });
227
+
194
228
  module.exports = {
195
229
  BASE, PORTA, CASA,
196
230
  garantirNoAr, estaNoAr, prepararCasa, pythonCmd,
197
231
  manual, ferramentas, spec, listar, ler, criar, atualizar,
232
+ midiaListar, midiaEnviar,
198
233
  };
package/lib/tools.js CHANGED
@@ -498,6 +498,11 @@ async function despachar(name, args, ctx) {
498
498
  return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
499
499
  case 'studio_listar':
500
500
  return studio.listar();
501
+ case 'studio_midia':
502
+ // Sem arquivo, lista o que já foi enviado; com arquivo, envia.
503
+ return args.arquivo
504
+ ? studio.midiaEnviar({ arquivo: resolveInProject(args.arquivo, projectDir), nome: args.nome })
505
+ : studio.midiaListar();
501
506
 
502
507
  case 'desktop_double_click':
503
508
  return desktop.doubleClick(args.x, args.y);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.1.1",
3
+ "version": "8.2.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": {
@@ -60,6 +60,39 @@ livre de direitos, ou uma das trilhas geradas acima.
60
60
 
61
61
  ---
62
62
 
63
+ ## Narração
64
+
65
+ A voz que fala por cima do vídeo. Vai **na cena**, e o estúdio narra sozinho
66
+ na hora de tocar — sem gravar nada, sem arquivo:
67
+
68
+ ```json
69
+ { "layout": "centro", "duracao": 6,
70
+ "narracao": "O texto que a voz vai falar nesta cena.",
71
+ "elementos": [ ... ] }
72
+ ```
73
+
74
+ Ajuste da voz no **documento** (opcional):
75
+
76
+ ```json
77
+ "voz": { "velocidade": 1, "tom": 1, "volume": 1, "lang": "pt-BR" }
78
+ ```
79
+
80
+ Regras que fazem diferença:
81
+
82
+ - **Escreva para o ouvido, não para o olho.** A narração NÃO repete o texto da
83
+ tela: a tela dá o título, a voz dá o argumento. Repetir os dois é o erro mais
84
+ comum, e soa a leitura de slide.
85
+ - **Caiba na duração.** Fale ~2,5 palavras por segundo: uma cena de 6s aguenta
86
+ ~15 palavras. Texto mais longo que a cena é cortado no meio.
87
+ - **Uma ideia por cena**, em uma ou duas frases curtas. Ponto final ajuda a voz
88
+ a respirar.
89
+ - Para o vídeo ficar mudo de propósito, é só não pôr `narracao` em cena nenhuma.
90
+
91
+ O usuário também pode gravar a própria voz pelo editor; aí a cena ganha
92
+ `narracaoAudio` com o arquivo, e ela tem prioridade sobre a voz sintetizada.
93
+
94
+ ---
95
+
63
96
  ## Transições
64
97
 
65
98
  Na cena, `transicao` define como ela **entra**:
@@ -65,6 +65,10 @@
65
65
  document.getElementById("btn-pdf").innerHTML = svgIcone("tela");
66
66
  document.getElementById("btn-baixar").innerHTML = svgIcone("baixar");
67
67
  </script>
68
+ <script src="/js/audio.js"></script>
69
+ <script src="/js/narracao.js"></script>
70
+ <script src="/js/midia.js"></script>
71
+ <script src="/js/exportar.js"></script>
68
72
  <script src="/js/editor.js"></script>
69
73
  </body>
70
74
  </html>
@@ -15,6 +15,8 @@
15
15
  <script src="/js/render.js"></script>
16
16
  <script src="/js/render-prosa.js"></script>
17
17
  <script src="/js/render-grade.js"></script>
18
+ <script src="/js/audio.js"></script>
19
+ <script src="/js/narracao.js"></script>
18
20
  <script src="/js/player.js"></script>
19
21
  </body>
20
22
  </html>