primocode 8.22.0 → 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.
- package/lib/act.js +41 -1
- package/lib/catalogo.js +2 -0
- package/lib/config.js +53 -2
- package/lib/referencia.js +145 -18
- package/lib/studio.js +125 -11
- package/lib/tools.js +35 -1
- package/package.json +2 -2
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-estatico-corte-a5bab.json +150 -0
- package/studio/arquivos/corte-founders-ai-teste-do-player-2de1d.json +65 -0
- package/studio/arquivos/corte-legado-teste.json +1 -0
- package/studio/arquivos/corte-legado2.json +1 -0
- package/studio/arquivos/corte-minimo-2a829.json +24 -0
- package/studio/arquivos/corte-ok-00a3b.json +18 -0
- package/studio/arquivos/corte-ok-1270e.json +22 -0
- package/studio/arquivos/corte-sweep-corte-cfb29.json +147 -0
- package/studio/arquivos/corte-visual-completo-78f50.json +35 -0
- package/studio/arquivos/grade-sweep-grade-0c2f1.json +119 -0
- package/studio/arquivos/prisma-estatico-prisma-d8740.json +182 -0
- package/studio/arquivos/prisma-sweep-prisma-4c58c.json +166 -0
- package/studio/arquivos/prisma-y-b0785.json +52 -0
- package/studio/arquivos/prosa-sweep-prosa-f29cf.json +80 -0
- package/studio/arquivos/tela-estatico-tela-c7daf.json +90 -0
- package/studio/arquivos/tela-sweep-tela-0d2bf.json +84 -0
- package/studio/arquivos/traco-estatico-traco-25872.json +113 -0
- package/studio/arquivos/traco-sweep-traco-0bcf4.json +102 -0
- package/studio/ferramentas/corte.md +159 -12
- package/studio/server.py +264 -8
- package/studio/web/analise.html +52 -0
- package/studio/web/js/analise.js +202 -0
- package/studio/web/js/exportar.js +88 -1
- package/studio/web/js/player.js +172 -46
- package/studio/web/js/render.js +99 -1
- package/studio/web/js/video.js +434 -0
- package/studio/web/render.css +27 -0
- package/studio/web/tool.html +1 -0
- package/studio/web/view.html +2 -0
package/lib/act.js
CHANGED
|
@@ -349,6 +349,12 @@ async function runAct(userPrompt, opts) {
|
|
|
349
349
|
// Quem já foi chamado, e quantas vezes. Ver lib/repeticao.js.
|
|
350
350
|
const jaChamado = repeticao.novo();
|
|
351
351
|
let voltasEmFalso = 0;
|
|
352
|
+
// Pesquisas que voltaram de mãos vazias. A partir da terceira, a tool
|
|
353
|
+
// para de rodar e devolve ORDEM DE SEGUIR: sem isto o agente entrava em
|
|
354
|
+
// ciclo — buscava, não achava, lia "tente outra consulta", buscava de
|
|
355
|
+
// novo — até a cota do minuto acabar e o modelo responder vazio. Foi
|
|
356
|
+
// assim que "crie um vídeo para a tostudy" morreu sem entregar nada.
|
|
357
|
+
let pesquisasSecas = 0;
|
|
352
358
|
let rodadasParadas = 0;
|
|
353
359
|
|
|
354
360
|
const messages = [];
|
|
@@ -515,9 +521,22 @@ async function runAct(userPrompt, opts) {
|
|
|
515
521
|
// o modelo grátis engasgando. Cair direto no "parei sem concluir"
|
|
516
522
|
// jogava a tarefa fora quando repetir a MESMA rodada quase sempre
|
|
517
523
|
// resolve — visto em produção no primeiro pedido de vídeo.
|
|
518
|
-
if (!finalText && respostasVazias <
|
|
524
|
+
if (!finalText && respostasVazias < 3 && !PARAR) {
|
|
519
525
|
respostasVazias++;
|
|
520
526
|
if (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo respondeu vazio — tentando de novo'));
|
|
527
|
+
// Repetir o MESMO contexto costuma produzir o mesmo vazio: se
|
|
528
|
+
// o turno anterior já engasgou, o que falta é um empurrão
|
|
529
|
+
// curto e concreto. A partir da segunda vez, entra um pedido
|
|
530
|
+
// explícito de fechamento — barato em tokens (é justamente
|
|
531
|
+
// quando a cota está no fim) e suficiente para o modelo
|
|
532
|
+
// escrever a resposta ou chamar finish.
|
|
533
|
+
if (respostasVazias >= 2) {
|
|
534
|
+
messages.push({
|
|
535
|
+
role: 'user',
|
|
536
|
+
content: 'Sua última resposta veio vazia. Não pesquise mais nem explique o processo: '
|
|
537
|
+
+ 'faça agora a entrega pedida com o que você já tem e finalize.',
|
|
538
|
+
});
|
|
539
|
+
}
|
|
521
540
|
iteration--;
|
|
522
541
|
continue;
|
|
523
542
|
}
|
|
@@ -616,6 +635,22 @@ async function runAct(userPrompt, opts) {
|
|
|
616
635
|
temPasta = true;
|
|
617
636
|
}
|
|
618
637
|
|
|
638
|
+
if (tc.name === 'pesquisar_referencia' && pesquisasSecas >= 3) {
|
|
639
|
+
voltasEmFalso++;
|
|
640
|
+
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
641
|
+
console.log(ident(profundidade) + c.muted(' ⎿ ') + c.warn('3 pesquisas sem resultado — siga com o que já tem'));
|
|
642
|
+
messages.push({
|
|
643
|
+
role: 'tool',
|
|
644
|
+
tool_call_id: tc.id,
|
|
645
|
+
content: 'PARE DE PESQUISAR. Três buscas seguidas não trouxeram fonte nenhuma; '
|
|
646
|
+
+ 'mais uma vai gastar a cota e devolver o mesmo nada. FAÇA AGORA a entrega '
|
|
647
|
+
+ 'pedida com o que você já sabe. Se faltar um fato específico (número, data, '
|
|
648
|
+
+ 'preço, nome de pessoa), NÃO invente: escreva a peça sem ele e avise o '
|
|
649
|
+
+ 'usuário no final, em uma linha, o que não deu para confirmar.',
|
|
650
|
+
});
|
|
651
|
+
continue;
|
|
652
|
+
}
|
|
653
|
+
|
|
619
654
|
algoRodou = true;
|
|
620
655
|
console.log(ident(profundidade) + actionHeader(tc.name, tc.arguments));
|
|
621
656
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
@@ -626,6 +661,11 @@ async function runAct(userPrompt, opts) {
|
|
|
626
661
|
// não corrige um teste quebrado noutro lugar; antes isto zerava
|
|
627
662
|
// a lista inteira e o agente conseguia dizer "concluído" com uma
|
|
628
663
|
// falha real ainda pendente.
|
|
664
|
+
if (tc.name === 'pesquisar_referencia') {
|
|
665
|
+
if (result && result.ok === false) pesquisasSecas++;
|
|
666
|
+
else pesquisasSecas = 0; // achou fonte: o ciclo se rompeu
|
|
667
|
+
}
|
|
668
|
+
|
|
629
669
|
if (result && result.ok === false) {
|
|
630
670
|
falhasPendentes.push({
|
|
631
671
|
tool: tc.name,
|
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
|
-
|
|
139
|
-
|
|
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/referencia.js
CHANGED
|
@@ -244,21 +244,116 @@ function relevante(cand, palavras) {
|
|
|
244
244
|
* Foi o que salvou este caso — parisgroup.ai abre e tem o texto todo, enquanto
|
|
245
245
|
* DuckDuckGo devolvia 202 e o Bing devolvia Paris, a cidade.
|
|
246
246
|
*
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
247
|
+
* Reconhece nome de DUAS palavras com maiúscula ("Paris Group") e também de
|
|
248
|
+
* UMA palavra, mesmo em minúscula — que é como as pessoas digitam no meio de
|
|
249
|
+
* uma frase. Exigir duas maiúsculas custou caro: para "crie um vídeo para a
|
|
250
|
+
* tostudy, plataforma de aprendizagem com IA" a função devolvia lista vazia,
|
|
251
|
+
* o buscador não conhecia o nome, a pesquisa falhava — e o agente repetia
|
|
252
|
+
* buscas até a tarefa morrer, com tostudy.ai no ar o tempo todo.
|
|
250
253
|
*/
|
|
254
|
+
|
|
255
|
+
// Palavras que aparecem em qualquer pedido e NUNCA são o nome da marca. Sem
|
|
256
|
+
// esta peneira o chute viraria plataforma.com, aprendizagem.com, video.com.
|
|
257
|
+
const NAO_E_MARCA = new Set([
|
|
258
|
+
'plataforma', 'plataformas', 'empresa', 'startup', 'site', 'oficial', 'aplicativo', 'app',
|
|
259
|
+
'aprendizagem', 'aprendizado', 'ensino', 'educacao', 'educacional', 'curso', 'cursos', 'escola',
|
|
260
|
+
'video', 'videos', 'reels', 'anuncio', 'campanha', 'marketing', 'institucional', 'apresentacao',
|
|
261
|
+
'inteligencia', 'artificial', 'brasil', 'brasileira', 'sobre', 'para', 'como', 'uma', 'que',
|
|
262
|
+
'crie', 'criar', 'faca', 'fazer', 'quero', 'preciso', 'estudo', 'estudos', 'tecnologia',
|
|
263
|
+
'software', 'sistema', 'servico', 'servicos', 'produto', 'produtos', 'negocio', 'digital',
|
|
264
|
+
'documentacao', 'tutorial', 'exemplo', 'exemplos', 'guia', 'referencia',
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
function _slug(txt) {
|
|
268
|
+
return String(txt || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
269
|
+
.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
270
|
+
}
|
|
271
|
+
|
|
251
272
|
function sitesProvaveis(consulta) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
273
|
+
const texto = String(consulta || '');
|
|
274
|
+
const marcas = [];
|
|
275
|
+
|
|
276
|
+
// Entre aspas é a forma de a pessoa dizer "o nome é ESTE". Vem primeiro.
|
|
277
|
+
const citado = /["“'”]([^"“'”]{3,40})["“'”]/.exec(texto);
|
|
278
|
+
if (citado) marcas.push({ rotulo: citado[1].trim(), slug: _slug(citado[1]) });
|
|
279
|
+
|
|
280
|
+
const duplo = /\b([A-ZÀ-Ý][\wÀ-ɏ]{1,})\s+([A-ZÀ-Ý][\wÀ-ɏ]{1,})\b/.exec(texto);
|
|
281
|
+
if (duplo) marcas.push({ rotulo: `${duplo[1]} ${duplo[2]}`, slug: _slug(duplo[1] + duplo[2]) });
|
|
282
|
+
|
|
283
|
+
// Marca de UMA palavra só é chutada quando a consulta dá SINAL de que
|
|
284
|
+
// fala de um negócio ("plataforma", "empresa", "app", "site"...) ou põe o
|
|
285
|
+
// nome entre aspas. Sem essa exigência, "react hooks tutorial" viraria
|
|
286
|
+
// react.com e "como fazer um bolo" viraria fazer.com — dez buscas
|
|
287
|
+
// inúteis para cada pesquisa técnica.
|
|
288
|
+
const SINAL_DE_MARCA = /\b(plataforma|empresa|startup|site|marca|neg[óo]cio|produto|servi[çc]o|app|aplicativo|institucional|cliente|fabricante|loja|ag[êe]ncia|ecommerce|e-commerce|saas)\b/i;
|
|
289
|
+
const temSinal = SINAL_DE_MARCA.test(texto) || !!citado;
|
|
290
|
+
if (temSinal) {
|
|
291
|
+
const util = (bruto) => {
|
|
292
|
+
const s = _slug(bruto);
|
|
293
|
+
return (s.length >= 4 && s.length <= 24 && !NAO_E_MARCA.has(s) && !/^\d+$/.test(s)) ? s : null;
|
|
294
|
+
};
|
|
295
|
+
// O nome vem quase sempre depois de artigo ou preposição: "um mouse
|
|
296
|
+
// DA multilaser", "vídeo para A tostudy". Pegar simplesmente a
|
|
297
|
+
// primeira palavra não-genérica escolhia o verbo do pedido
|
|
298
|
+
// ("anunciar") e chutava anunciar.ai.
|
|
299
|
+
const depoisDeArtigo = [...texto.matchAll(/\b(?:d[aeo]s?|[ao]|n[ao]s?|à)\s+([A-Za-zÀ-ÿ][\wÀ-ÿ-]{3,23})\b/gi)];
|
|
300
|
+
for (const m of depoisDeArtigo) {
|
|
301
|
+
const s = util(m[1]);
|
|
302
|
+
if (s) { marcas.push({ rotulo: m[1], slug: s }); break; }
|
|
303
|
+
}
|
|
304
|
+
// Só quando o artigo não entregou nada: senão o verbo do pedido
|
|
305
|
+
// ("anunciar") entraria junto e gastaria dez sondas à toa.
|
|
306
|
+
if (!marcas.length) {
|
|
307
|
+
for (const bruto of texto.split(/[\s,.;:!?()]+/)) {
|
|
308
|
+
const s = util(bruto);
|
|
309
|
+
if (s) { marcas.push({ rotulo: bruto, slug: s }); break; }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const vistos = new Set();
|
|
315
|
+
const saida = [];
|
|
316
|
+
for (const m of marcas.slice(0, 2)) {
|
|
317
|
+
if (!m.slug || m.slug.length < 4 || vistos.has(m.slug)) continue;
|
|
318
|
+
vistos.add(m.slug);
|
|
319
|
+
for (const tld of ['ai', 'com', 'com.br', 'io', 'co']) {
|
|
320
|
+
// Com e sem "www": um resolve onde o outro não, e quem decide é a
|
|
321
|
+
// hospedagem. Como estes candidatos passam pela sonda antes de
|
|
322
|
+
// entrar na fila de leitura, tentar os dois não custa vaga.
|
|
323
|
+
saida.push({ titulo: `${m.rotulo} — site oficial`, url: `https://${m.slug}.${tld}`, nota: 'site provável da empresa' });
|
|
324
|
+
saida.push({ titulo: `${m.rotulo} — site oficial`, url: `https://www.${m.slug}.${tld}`, nota: 'site provável da empresa' });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return saida;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Sonda os domínios chutados e devolve só os que responderam, com o HTML já
|
|
332
|
+
* na mão. Sem isto, dez chutes entrariam na fila de leitura e expulsariam os
|
|
333
|
+
* resultados de verdade do buscador — e a maioria deles nem existe.
|
|
334
|
+
*/
|
|
335
|
+
async function chutesVivos(chutes, limite = 2) {
|
|
336
|
+
if (!chutes.length) return { vivos: [], html: new Map() };
|
|
337
|
+
// 12 s, não 6: a sonda roda em paralelo e a home de uma empresa hoje pesa
|
|
338
|
+
// centenas de KB. Com 6 s o site CERTO chegava atrasado, era descartado
|
|
339
|
+
// como morto, e a pesquisa falhava com o site no ar — medido.
|
|
340
|
+
const respostas = await Promise.all(chutes.slice(0, 12).map(async (c) => {
|
|
341
|
+
const html = await pegar(c.url, { timeout: 12000 });
|
|
342
|
+
return html && html.length > 500 ? { cand: c, html } : null;
|
|
261
343
|
}));
|
|
344
|
+
const vivos = [];
|
|
345
|
+
const html = new Map();
|
|
346
|
+
const dominios = new Set();
|
|
347
|
+
for (const r of respostas.filter(Boolean)) {
|
|
348
|
+
// www.x.ai e x.ai são o mesmo site: vale o primeiro que respondeu.
|
|
349
|
+
const raiz = r.cand.url.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '');
|
|
350
|
+
if (dominios.has(raiz)) continue;
|
|
351
|
+
dominios.add(raiz);
|
|
352
|
+
vivos.push(r.cand);
|
|
353
|
+
html.set(r.cand.url, r.html);
|
|
354
|
+
if (vivos.length >= limite) break;
|
|
355
|
+
}
|
|
356
|
+
return { vivos, html };
|
|
262
357
|
}
|
|
263
358
|
|
|
264
359
|
// Links que não são referência de nada — resultado de busca sujo.
|
|
@@ -460,7 +555,17 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
460
555
|
// O site provável da empresa vai NA FRENTE: quando a consulta nomeia um
|
|
461
556
|
// negócio, o site dele é a melhor fonte que existe, e não depende de
|
|
462
557
|
// buscador nenhum estar de bom humor.
|
|
463
|
-
|
|
558
|
+
// Os chutes de domínio são SONDADOS antes de entrar na fila: só os que
|
|
559
|
+
// respondem ocupam vaga. São dez palpites (5 TLDs x com/sem www) e a
|
|
560
|
+
// maioria não existe — enfileirar todos expulsaria os resultados reais
|
|
561
|
+
// do buscador. A sonda roda em paralelo e o HTML já volta aproveitado.
|
|
562
|
+
const palpites = sitesProvaveis(termo);
|
|
563
|
+
const { vivos, html: htmlChutes } = await chutesVivos(palpites);
|
|
564
|
+
// A sonda ORDENA, não faz porteiro. Ela é uma corrida contra o relógio
|
|
565
|
+
// contra home pesada e rede lenta, e perdê-la não pode significar perder
|
|
566
|
+
// o site certo: quando nada responde a tempo, os primeiros palpites vão
|
|
567
|
+
// para a fila mesmo assim e o laço de leitura tenta de novo, sem pressa.
|
|
568
|
+
const chutes = vivos.length ? vivos : palpites.slice(0, 3);
|
|
464
569
|
const palavras = palavrasDaConsulta(termo);
|
|
465
570
|
const brutos = [...chutes, ...web, ...so, ...docsConhecidas(termo)];
|
|
466
571
|
|
|
@@ -481,7 +586,15 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
481
586
|
provavelCausa: descartados
|
|
482
587
|
? `Vieram ${descartados} link(s), mas nenhum era sobre "${termo}" — o buscador não achou o assunto e devolveu página aleatória. Fonte errada é pior que fonte nenhuma, então foram descartadas.`
|
|
483
588
|
: 'Os buscadores costumam recusar chamadas automáticas (bloqueio ou limite por excesso de buscas seguidas), e não a sua conexão.',
|
|
484
|
-
|
|
589
|
+
// "Tente outra consulta" era um convite ao ciclo: o agente
|
|
590
|
+
// reformulava a busca, falhava igual, e repetia até a cota acabar.
|
|
591
|
+
// A ordem agora é explícita — no máximo UMA nova tentativa, e por
|
|
592
|
+
// um caminho DIFERENTE (o site direto), senão segue sem a fonte.
|
|
593
|
+
comoCorrigir: 'NÃO repita esta busca com outras palavras — o buscador não conhece o assunto e '
|
|
594
|
+
+ 'vai devolver o mesmo nada. Se souber (ou conseguir deduzir) o site, tente UMA vez '
|
|
595
|
+
+ 'browser_open + browser_read nele. Se também não abrir, SIGA COM A TAREFA usando o que '
|
|
596
|
+
+ 'você já sabe e avise ao usuário, no final, qual fato não deu para confirmar. Nunca invente '
|
|
597
|
+
+ 'dado sobre empresa, número, data, preço ou pessoa.',
|
|
485
598
|
};
|
|
486
599
|
}
|
|
487
600
|
|
|
@@ -491,7 +604,9 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
491
604
|
const chutesLidos = [];
|
|
492
605
|
for (const cand of candidatos) {
|
|
493
606
|
if (lidas.length >= paginas) break;
|
|
494
|
-
|
|
607
|
+
// A sonda já baixou o HTML dos chutes: reaproveita em vez de pedir
|
|
608
|
+
// a mesma página duas vezes.
|
|
609
|
+
const html = htmlChutes.get(cand.url) || await pegar(cand.url);
|
|
495
610
|
if (!html) continue;
|
|
496
611
|
const texto = textoDe(html);
|
|
497
612
|
if (texto.length < 200) continue;
|
|
@@ -502,7 +617,17 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
502
617
|
// da página, sim. Sem esta prova o agente escreveria sobre a errada.
|
|
503
618
|
const ehChute = cand.nota === 'site provável da empresa';
|
|
504
619
|
if (ehChute) {
|
|
505
|
-
|
|
620
|
+
let nota = quantoBate({ titulo: texto.slice(0, 4000), url: cand.url }, palavras);
|
|
621
|
+
// O NOME DA MARCA no domínio, confirmado no texto da página, já é
|
|
622
|
+
// prova — e é a prova mais forte que existe. Exigir duas palavras
|
|
623
|
+
// genéricas da consulta ("plataforma", "aprendizagem") reprovava o
|
|
624
|
+
// site certo: tostudy.ai abria, dizia "ToStudy" na primeira linha,
|
|
625
|
+
// e mesmo assim era descartado por não repetir o vocabulário do
|
|
626
|
+
// pedido. O agente então repetia buscas até a tarefa morrer.
|
|
627
|
+
const marca = (cand.url.match(/^https?:\/\/(?:www\.)?([a-z0-9-]+)\./i) || [])[1] || '';
|
|
628
|
+
const textoLimpo = texto.slice(0, 8000).normalize('NFD')
|
|
629
|
+
.replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
630
|
+
if (marca.length >= 4 && textoLimpo.includes(marca.toLowerCase())) nota = Math.max(nota, 2);
|
|
506
631
|
if (nota < Math.min(2, palavras.length)) continue;
|
|
507
632
|
// Passar não basta: entre dois domínios do MESMO nome, fica o que
|
|
508
633
|
// fala mais do assunto. parisgroup.ai e parisgroup.co os dois
|
|
@@ -532,8 +657,10 @@ async function pesquisar({ consulta, paginas = 3 }) {
|
|
|
532
657
|
error: 'achei os links mas não consegui abrir nenhum',
|
|
533
658
|
consulta: termo,
|
|
534
659
|
links: candidatos.map((c) => c.url),
|
|
535
|
-
comoCorrigir: 'Tente browser_open + browser_read num desses links
|
|
536
|
-
+ '
|
|
660
|
+
comoCorrigir: 'Tente browser_open + browser_read num desses links — UMA vez. Não refaça a busca '
|
|
661
|
+
+ 'com outras palavras: os links são estes mesmos. Se nada abrir, SIGA COM A TAREFA e diga ao '
|
|
662
|
+
+ 'usuário, no final, que não conseguiu confirmar os fatos — NUNCA invente dado sobre empresa, '
|
|
663
|
+
+ 'data, preço ou pessoa.',
|
|
537
664
|
};
|
|
538
665
|
}
|
|
539
666
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
return
|
|
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
|
-
|
|
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.
|
|
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"
|
|
Binary file
|