primocode 8.38.0 → 8.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/app.js +6 -2
- package/app/primo-marca.js +32 -10
- package/bin/primocode.js +22 -15
- package/lib/act.js +87 -2
- package/lib/audio-livre.js +172 -0
- package/lib/boas-vindas.js +4 -4
- package/lib/catalogo.js +7 -0
- package/lib/marca.js +280 -0
- package/lib/referencia.js +210 -63
- package/lib/studio.js +22 -1
- package/lib/tools.js +10 -0
- package/package.json +2 -2
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/arquivos/tela-motion-3ecaa.json +135 -0
- package/studio/arquivos/tela-motion-d6150.json +135 -0
- package/studio/edge.py +66 -16
- package/studio/ferramentas/01-modelos.md +20 -2
- package/studio/ferramentas/corte.md +121 -11
- package/studio/ferramentas/tela.md +21 -0
- package/studio/server.py +47 -11
- package/studio/studio.log +28 -0
- package/studio/web/js/player.js +30 -2
- package/studio/web/js/render.js +74 -4
- package/studio/web/js/sfx.js +128 -2
- package/studio/web/render.css +95 -0
package/app/app.js
CHANGED
|
@@ -316,12 +316,16 @@
|
|
|
316
316
|
marcarEscolha("cores", "cor", cor);
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
/* A LOGO — a mesma do ícone do sistema e da barra lateral do Conecta. Onde
|
|
320
|
+
o produto se apresenta vai ela; o mascote em pixel fica para onde ele é
|
|
321
|
+
personagem. */
|
|
319
322
|
function desenharMarca() {
|
|
320
323
|
if (!window.PrimoMarca) return;
|
|
321
324
|
var t = document.getElementById("marcaTopo");
|
|
322
325
|
var g = document.getElementById("marcaGrande");
|
|
323
|
-
|
|
324
|
-
if (
|
|
326
|
+
var desenha = window.PrimoMarca.logo || window.PrimoMarca.svg;
|
|
327
|
+
if (t) t.innerHTML = desenha({ tamanho: 22 });
|
|
328
|
+
if (g) g.innerHTML = desenha({ tamanho: 82, rotulo: "PrimoCode" });
|
|
325
329
|
}
|
|
326
330
|
|
|
327
331
|
function quando(iso) {
|
package/app/primo-marca.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* primo-marca.js — o mascote do PrimoCode, desenhado em pixel.
|
|
3
3
|
*
|
|
4
|
-
* CÓPIA de conecta-primo-ai/frontend/primo-marca.js. São repositórios
|
|
5
|
-
* diferentes e não há como importar de um para o outro; o que dá para fazer é
|
|
6
|
-
* manter os dois IDÊNTICOS e o teste conferir isso — um mascote que muda num
|
|
7
|
-
* lado e não no outro é a marca do produto se partindo em duas sem ninguém
|
|
8
|
-
* ver.
|
|
9
|
-
*
|
|
10
4
|
* ── POR QUE MATRIZ, E NÃO O PNG ──────────────────────────────────────────
|
|
11
5
|
* O logo veio como imagem. Guardar o PNG funcionaria, mas ele já é pixel art:
|
|
12
6
|
* redesenhá-lo como MATRIZ dá o mesmo desenho nítido em qualquer tamanho —
|
|
@@ -103,11 +97,39 @@
|
|
|
103
97
|
+ '" height="' + lado + '"' + acessivel + ">" + partes.join("") + "</svg>";
|
|
104
98
|
}
|
|
105
99
|
|
|
106
|
-
|
|
100
|
+
/* A LOGO — a que o dono mandou, e a mesma do ícone do sistema
|
|
101
|
+
(`app/icone.svg` no PrimoCode): quadrado escuro arredondado, o chevron do
|
|
102
|
+
prompt e o sublinhado do cursor, em ciano. Onde o produto se apresenta
|
|
103
|
+
(botão da barra lateral, cabeçalho, favicon) vai ELA; o mascote em pixel
|
|
104
|
+
acima fica para onde ele é personagem, não marca. Foi por confundir os
|
|
105
|
+
dois que a barra lateral mostrou o bicho onde devia estar a logo. */
|
|
106
|
+
var LOGO_FUNDO = "#0a0f16";
|
|
107
|
+
var LOGO_TRACO = "#22d3ee";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* A logo como SVG.
|
|
111
|
+
* @param {object} op
|
|
112
|
+
* @param {number} [op.tamanho] lado em px
|
|
113
|
+
* @param {string} [op.rotulo] texto alternativo; vazio = decorativo
|
|
114
|
+
*/
|
|
115
|
+
function logo(op) {
|
|
116
|
+
op = op || {};
|
|
117
|
+
var lado = op.tamanho || 32;
|
|
118
|
+
var acessivel = op.rotulo
|
|
119
|
+
? ' role="img" aria-label="' + String(op.rotulo).replace(/"/g, """) + '"'
|
|
120
|
+
: ' aria-hidden="true"';
|
|
121
|
+
return '<svg viewBox="0 0 32 32" width="' + lado + '" height="' + lado + '"' + acessivel + ">"
|
|
122
|
+
+ '<rect width="32" height="32" rx="7" fill="' + LOGO_FUNDO + '"/>'
|
|
123
|
+
+ '<path d="M8 10.5 13 16l-5 5.5" fill="none" stroke="' + LOGO_TRACO
|
|
124
|
+
+ '" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>'
|
|
125
|
+
+ '<path d="M16.5 22H24" fill="none" stroke="' + LOGO_TRACO + '" stroke-width="2.6" stroke-linecap="round"/>'
|
|
126
|
+
+ "</svg>";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A logo como favicon, sem pedir arquivo ao servidor. */
|
|
107
130
|
function favicon() {
|
|
108
|
-
|
|
109
|
-
return "data:image/svg+xml," + encodeURIComponent(s);
|
|
131
|
+
return "data:image/svg+xml," + encodeURIComponent(logo({ tamanho: 32 }));
|
|
110
132
|
}
|
|
111
133
|
|
|
112
|
-
raiz.PrimoMarca = { svg: svg, favicon: favicon, MATRIZ: MATRIZ, CORPO: CORPO, DETALHE: DETALHE };
|
|
134
|
+
raiz.PrimoMarca = { svg: svg, logo: logo, favicon: favicon, MATRIZ: MATRIZ, CORPO: CORPO, DETALHE: DETALHE };
|
|
113
135
|
})(typeof window !== "undefined" ? window : globalThis);
|
package/bin/primocode.js
CHANGED
|
@@ -1269,26 +1269,33 @@ async function handleSlash(text) {
|
|
|
1269
1269
|
abrir: (u) => tools.openInBrowser(u),
|
|
1270
1270
|
contar: (info) => {
|
|
1271
1271
|
sp.stop();
|
|
1272
|
-
/* O código continua aparecendo — grande, partido em dois,
|
|
1273
|
-
porque é o que se lê de uma tela e se confere noutra. Mas
|
|
1274
|
-
agora ele já FOI para a página: o CLI serve o código em
|
|
1275
|
-
127.0.0.1 e a página pergunta ali; e foi para a área de
|
|
1276
|
-
transferência, para o "Colar" de um toque onde o
|
|
1277
|
-
navegador bloqueia o 127.0.0.1 (Safari). Digitar virou a
|
|
1278
|
-
terceira saída, não a primeira. */
|
|
1279
1272
|
const meio = info.codigo.length === 8
|
|
1280
1273
|
? info.codigo.slice(0, 4) + '-' + info.codigo.slice(4) : info.codigo;
|
|
1281
1274
|
console.log(c.muted(' Abrindo no navegador:'));
|
|
1282
1275
|
console.log(' ' + c.brand(info.url.replace(/[?&]porta=\d+/, '')));
|
|
1283
1276
|
console.log('');
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1277
|
+
if (info.porta) {
|
|
1278
|
+
/* COM O SERVIDOR LOCAL DE PÉ, NÃO HÁ CÓDIGO PARA LER. A
|
|
1279
|
+
página pergunta ao 127.0.0.1 desta máquina, recebe o
|
|
1280
|
+
código e aprova sozinha assim que a pessoa entra com a
|
|
1281
|
+
conta — "não precisa colocar código, não precisa pra
|
|
1282
|
+
nada, porque ele já libera". Mostrar as oito letras
|
|
1283
|
+
grandes aqui era pedir para ler o que ninguém precisa
|
|
1284
|
+
mais; elas ficam numa linha apagada, só para o caso
|
|
1285
|
+
de o navegador bloquear o 127.0.0.1 (Safari) e a
|
|
1286
|
+
página voltar a pedir. */
|
|
1287
|
+
console.log(' ' + c.white('Entre com a sua conta na página que abriu — o resto é automático.'));
|
|
1288
|
+
console.log(c.dim(` Se a página pedir um código: ${meio}${info.copiado ? ' (já copiado)' : ''}.`));
|
|
1289
|
+
} else {
|
|
1290
|
+
/* Sem porta local (ocupadas, ou bloqueadas), o código
|
|
1291
|
+
volta a ser o caminho — grande, partido em dois,
|
|
1292
|
+
porque é o que se lê de uma tela e se confere noutra. */
|
|
1293
|
+
console.log(' ' + c.bold(c.brand(meio.split('').join(' '))));
|
|
1294
|
+
console.log('');
|
|
1295
|
+
console.log(c.dim(' ' + (info.copiado ? 'O código já está copiado — cole na página.'
|
|
1296
|
+
: 'Digite o código na página.')));
|
|
1297
|
+
}
|
|
1298
|
+
console.log(c.dim(' Esperando o navegador…'));
|
|
1292
1299
|
},
|
|
1293
1300
|
});
|
|
1294
1301
|
sp.stop();
|
package/lib/act.js
CHANGED
|
@@ -19,7 +19,13 @@ const cfg = require('./config');
|
|
|
19
19
|
const catalogo = require('./catalogo');
|
|
20
20
|
const repeticao = require('./repeticao');
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// 40, não 15: com o freio de repetição (repeticao.js) e o de rodada parada,
|
|
23
|
+
// volta a mais não é volta em círculo — é trabalho. Quinze acabavam no meio
|
|
24
|
+
// de um vídeo de dez cenas com pesquisa, e a pessoa lia "gastei as voltas".
|
|
25
|
+
const MAX_ITERATIONS = Number(process.env.PRIMOCODE_MAX_VOLTAS || 40);
|
|
26
|
+
// Quantas vezes o agente é EMPURRADO quando para no meio para narrar
|
|
27
|
+
// ("agora vou criar o arquivo…") em vez de fazer. Ver `pareceParadaNoMeio`.
|
|
28
|
+
const MAX_EMPURROES = 3;
|
|
23
29
|
// Voltas seguidas sem executar nada antes de desistir.
|
|
24
30
|
const MAX_RODADAS_PARADAS = 3;
|
|
25
31
|
const MAX_PROFUNDIDADE = 2; // principal + 1 nível de subagente
|
|
@@ -182,6 +188,20 @@ function logResult(kind, result, profundidade) {
|
|
|
182
188
|
}
|
|
183
189
|
return;
|
|
184
190
|
}
|
|
191
|
+
case 'buscar_audio': {
|
|
192
|
+
console.log(p + c.muted(`${result.tipo === 'efeito' ? 'efeito' : 'música'}: `) + c.white(`${result.titulo}`)
|
|
193
|
+
+ c.muted(` — ${result.autor || 'autor desconhecido'}${result.duracao ? ` · ${result.duracao}s` : ''} · ${result.licenca}`));
|
|
194
|
+
console.log(p + c.dim(`${result.url}`));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
case 'marca_da_empresa': {
|
|
198
|
+
const cores = (result.cores && result.cores.paleta || []).slice(0, 4).join(' ');
|
|
199
|
+
console.log(p + c.white(result.nome || '') + c.muted(` · ${result.site || ''}`));
|
|
200
|
+
console.log(p + c.dim(`cor ${(result.cores && result.cores.principal) || '—'}${cores ? ` · paleta ${cores}` : ''}`
|
|
201
|
+
+ `${(result.fontes || []).length ? ` · fonte ${result.fontes[0]}` : ''}`
|
|
202
|
+
+ `${result.logo ? ` · logo ${result.logo.formato} (${result.logo.fonte})` : ' · sem logo'}`));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
185
205
|
case 'desktop_windows': {
|
|
186
206
|
console.log(p + c.muted(`${result.total} janela${result.total === 1 ? '' : 's'}`));
|
|
187
207
|
for (const j of (result.janelas || []).slice(0, 5)) console.log(p + c.dim(`${j.app ? j.app + ' · ' : ''}${j.titulo}`.slice(0, 120)));
|
|
@@ -338,6 +358,39 @@ async function pausar(ms, subagent) {
|
|
|
338
358
|
if (sp) sp.stop();
|
|
339
359
|
}
|
|
340
360
|
|
|
361
|
+
/**
|
|
362
|
+
* O modelo devolveu TEXTO, sem tool e sem finish. Isso é resposta — ou é o
|
|
363
|
+
* agente parando no meio para narrar o que ia fazer?
|
|
364
|
+
*
|
|
365
|
+
* Reclamação do dono: "ele tá parando sem concluir, ele deve continuar até o
|
|
366
|
+
* fim". Medido: o modelo escreve "Agora vou criar o vídeo com as cenas…" e
|
|
367
|
+
* para, porque texto sem tool encerra a rodada. Quem lê vê uma promessa no
|
|
368
|
+
* lugar da entrega. Três sinais dizem que foi parada, não resposta:
|
|
369
|
+
*
|
|
370
|
+
* 1. Há um plano (write_todos) com item por fazer — ele mesmo disse que
|
|
371
|
+
* não acabou.
|
|
372
|
+
* 2. O texto é INTENÇÃO: começa ou termina anunciando o próximo passo
|
|
373
|
+
* ("vou", "em seguida", "próximo passo", dois-pontos no fim, reticências).
|
|
374
|
+
* 3. É curto. Uma resposta de verdade a "explique este erro" tem corpo; um
|
|
375
|
+
* "vou abrir o arquivo agora" não tem.
|
|
376
|
+
*
|
|
377
|
+
* Pergunta ao usuário NÃO conta como parada: o prompt proíbe perguntar, mas
|
|
378
|
+
* se o modelo perguntou, empurrá-lo faria ele decidir sozinho o que talvez
|
|
379
|
+
* não devesse — e o texto de conversa ("oi, sou o PrimoCode") também não é
|
|
380
|
+
* parada. Só intenção declarada e plano aberto.
|
|
381
|
+
*/
|
|
382
|
+
const INTENCAO = /(^|[.!\n]\s*)(agora\s+)?(vou|irei|deixa eu|deixe-me|preciso (agora )?(de )?(criar|ler|abrir|rodar|ver|conferir|pesquisar|mandar|fazer|gerar|escrever|ajustar)|primeiro (vou|eu)|em seguida|pr[óo]ximo passo|a seguir,?\s+(vou|eu)|vamos (criar|ler|abrir|rodar|ver|conferir|come[çc]ar|fazer|gerar|escrever))\b/i;
|
|
383
|
+
function pareceParadaNoMeio(texto, { todosPendentes = 0, rodouTool = false } = {}) {
|
|
384
|
+
const t = String(texto || '').trim();
|
|
385
|
+
if (!t) return false;
|
|
386
|
+
if (todosPendentes > 0) return true;
|
|
387
|
+
if (t.length > 700) return false; // resposta com corpo
|
|
388
|
+
if (/\?\s*$/.test(t)) return false; // perguntou: não é parada
|
|
389
|
+
if (INTENCAO.test(t)) return true;
|
|
390
|
+
if (rodouTool && /[:…]\s*$|\.\.\.\s*$/.test(t)) return true; // "Criando as cenas:" e nada depois
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
341
394
|
function motivoDaParada({ voltas, voltasEmFalso, falhasPendentes, travou }) {
|
|
342
395
|
if (falhasPendentes.length) {
|
|
343
396
|
return `Parei sem concluir. A última falha foi: ${falhasPendentes[falhasPendentes.length - 1].texto}`;
|
|
@@ -391,6 +444,13 @@ async function runAct(userPrompt, opts) {
|
|
|
391
444
|
// assim que "crie um vídeo para a tostudy" morreu sem entregar nada.
|
|
392
445
|
let pesquisasSecas = 0;
|
|
393
446
|
let rodadasParadas = 0;
|
|
447
|
+
// Para o `pareceParadaNoMeio`: alguma tool já rodou nesta tarefa, e o
|
|
448
|
+
// último plano que o modelo escreveu (write_todos) — desta tarefa, não da
|
|
449
|
+
// sessão: um plano velho de um pedido interrompido não pode empurrar o
|
|
450
|
+
// pedido seguinte.
|
|
451
|
+
let rodouTool = false;
|
|
452
|
+
let todosDaTarefa = [];
|
|
453
|
+
let empurroes = 0;
|
|
394
454
|
|
|
395
455
|
const messages = [];
|
|
396
456
|
if (!subagent) {
|
|
@@ -579,6 +639,29 @@ async function runAct(userPrompt, opts) {
|
|
|
579
639
|
iteration--;
|
|
580
640
|
continue;
|
|
581
641
|
}
|
|
642
|
+
// PAROU NO MEIO? Texto de intenção ("agora vou criar…") ou plano
|
|
643
|
+
// com item aberto não é entrega. Em vez de encerrar, a tarefa é
|
|
644
|
+
// devolvida: "isso foi uma promessa — faça". Três empurrões; se
|
|
645
|
+
// depois disso ele insistir em só falar, a resposta é dele.
|
|
646
|
+
const pendentes = todosDaTarefa.filter((t) => t.status !== 'completed').length;
|
|
647
|
+
if (finalText && empurroes < MAX_EMPURROES && !PARAR
|
|
648
|
+
&& pareceParadaNoMeio(finalText, { todosPendentes: pendentes, rodouTool })) {
|
|
649
|
+
empurroes++;
|
|
650
|
+
if (!subagent) {
|
|
651
|
+
if (fluxo) fluxo.fechar();
|
|
652
|
+
else fala.pensamento(finalText, profundidade);
|
|
653
|
+
}
|
|
654
|
+
messages.push({ role: 'assistant', content: finalText });
|
|
655
|
+
messages.push({
|
|
656
|
+
role: 'user',
|
|
657
|
+
content: 'Isso foi uma intenção, não uma entrega: você parou no meio. Não narre o próximo '
|
|
658
|
+
+ 'passo — CHAME a tool agora e siga até o fim'
|
|
659
|
+
+ (pendentes ? `, o seu plano ainda tem ${pendentes} item(ns) por fazer` : '')
|
|
660
|
+
+ '. Só pare quando tudo estiver feito, com finish("o que foi feito").',
|
|
661
|
+
});
|
|
662
|
+
finalText = '';
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
582
665
|
if (!subagent) {
|
|
583
666
|
if (fluxo) fluxo.fechar();
|
|
584
667
|
else fala.resposta(finalText);
|
|
@@ -711,9 +794,11 @@ async function runAct(userPrompt, opts) {
|
|
|
711
794
|
}
|
|
712
795
|
|
|
713
796
|
algoRodou = true;
|
|
797
|
+
rodouTool = true;
|
|
714
798
|
fala.acao(tc.name, tc.arguments, profundidade);
|
|
715
799
|
const result = await tools.execute(tc.name, tc.arguments, { projectDir: pastaAtual });
|
|
716
800
|
logResult(tc.name, result, profundidade);
|
|
801
|
+
if (tc.name === 'write_todos' && result && Array.isArray(result.todos)) todosDaTarefa = result.todos;
|
|
717
802
|
|
|
718
803
|
// Uma escrita bem-sucedida limpa a dívida — mas só a do MESMO
|
|
719
804
|
// arquivo. Escrever algo sem relação (ou um run_shell qualquer)
|
|
@@ -910,4 +995,4 @@ async function rodarPipeline(args, ctx) {
|
|
|
910
995
|
}
|
|
911
996
|
|
|
912
997
|
module.exports = { runAct, interromper, compactar, motivoDaParada, ehTetoDeToken, ehTransitorio, esperaDoErro,
|
|
913
|
-
anotacaoDoQueJaFoi, encolherParaContinuar, MAX_ITERATIONS };
|
|
998
|
+
anotacaoDoQueJaFoi, encolherParaContinuar, pareceParadaNoMeio, MAX_ITERATIONS, MAX_EMPURROES };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audio-livre.js — música e efeitos sonoros da internet, com licença.
|
|
3
|
+
*
|
|
4
|
+
* Pedido do dono: "selecione músicas da internet, efeitos sonoros". O que
|
|
5
|
+
* existia era a trilha SINTETIZADA (quatro osciladores e um ruído) e o
|
|
6
|
+
* arquivo do próprio usuário. Faltava o meio: uma faixa de verdade, tocada
|
|
7
|
+
* por gente, que ele não precise ter no disco.
|
|
8
|
+
*
|
|
9
|
+
* ── DE ONDE VEM ──────────────────────────────────────────────────────────
|
|
10
|
+
* Da API do Openverse (openverse.org, mantida pela WordPress Foundation):
|
|
11
|
+
* ela indexa áudio com licença Creative Commons de fontes abertas — música
|
|
12
|
+
* do Jamendo, efeitos do Freesound, arquivos da Wikimedia — e devolve, para
|
|
13
|
+
* cada faixa, a URL do arquivo, a licença e o crédito pronto. Sem chave, sem
|
|
14
|
+
* conta. Não é YouTube nem Spotify: aqui a licença permite usar, e o crédito
|
|
15
|
+
* vai junto para a peça não sair devendo.
|
|
16
|
+
*
|
|
17
|
+
* ── O QUE ELA DEVOLVE ────────────────────────────────────────────────────
|
|
18
|
+
* O arquivo já BAIXADO para a pasta do projeto e ENVIADO ao estúdio: a URL
|
|
19
|
+
* "/midia/…" que entra direto em `trilha.src` ou em `som: {arquivo}`, mais o
|
|
20
|
+
* crédito ("Título — Autor, CC BY 4.0"). A licença decide o que o agente
|
|
21
|
+
* tem de fazer: BY pede o crédito na peça; CC0/PDM não pede nada. Licença
|
|
22
|
+
* NC (não comercial) não entra por padrão — a peça pode ser um anúncio.
|
|
23
|
+
*
|
|
24
|
+
* Zero dependências: o `pegar` do referencia.js, em modo binário.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
'use strict';
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const referencia = require('./referencia.js');
|
|
32
|
+
|
|
33
|
+
const API = 'https://api.openverse.org/v1/audio/';
|
|
34
|
+
|
|
35
|
+
// Licenças que permitem uso comercial sem pedir nada além do crédito.
|
|
36
|
+
const LIVRES = new Set(['by', 'by-sa', 'cc0', 'pdm']);
|
|
37
|
+
const NOME_LICENCA = {
|
|
38
|
+
by: 'CC BY', 'by-sa': 'CC BY-SA', 'by-nc': 'CC BY-NC', 'by-nd': 'CC BY-ND',
|
|
39
|
+
'by-nc-sa': 'CC BY-NC-SA', 'by-nc-nd': 'CC BY-NC-ND', cc0: 'CC0', pdm: 'Domínio público',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function slug(t) {
|
|
43
|
+
return String(t || '').normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
44
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 48) || 'audio';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function credito(f) {
|
|
48
|
+
const lic = NOME_LICENCA[f.license] || String(f.license || '').toUpperCase();
|
|
49
|
+
const versao = f.license_version && !/cc0|pdm/.test(f.license) ? ' ' + f.license_version : '';
|
|
50
|
+
return `${f.title || 'Sem título'} — ${f.creator || 'autor desconhecido'} (${lic}${versao}, via ${f.provider || 'Openverse'})`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pergunta ao Openverse. `tipo` "musica" restringe à categoria music; "efeito"
|
|
55
|
+
* pede coisa curta (até ~12 s). `dur` em segundos aproxima a duração desejada
|
|
56
|
+
* da trilha — uma faixa de 4 min para um reels de 30 s é desperdício, mas
|
|
57
|
+
* uma de 20 s para um vídeo de 60 s acaba antes; a nota pesa isso.
|
|
58
|
+
*/
|
|
59
|
+
async function procurar({ consulta, tipo = 'musica', dur = 0, comercial = true, pagina = 1 }) {
|
|
60
|
+
const q = new URLSearchParams({ q: consulta, page_size: '20', page: String(pagina) });
|
|
61
|
+
if (tipo === 'musica') q.set('category', 'music');
|
|
62
|
+
if (comercial) q.set('license_type', 'commercial');
|
|
63
|
+
const bruto = await referencia.pegar(`${API}?${q}`, { timeout: 12000 });
|
|
64
|
+
if (!bruto) return { ok: false, error: 'o Openverse não respondeu (rede?)' };
|
|
65
|
+
let j;
|
|
66
|
+
try { j = JSON.parse(bruto); } catch { return { ok: false, error: 'resposta ilegível do Openverse' }; }
|
|
67
|
+
const itens = (j.results || []).filter((f) => f.url && LIVRES.has(String(f.license || '').toLowerCase()) || !comercial);
|
|
68
|
+
const alvoMs = tipo === 'efeito' ? 2500 : (dur ? dur * 1000 : 90000);
|
|
69
|
+
const notas = itens.map((f) => {
|
|
70
|
+
let n = 0;
|
|
71
|
+
const d = Number(f.duration) || 0;
|
|
72
|
+
if (tipo === 'efeito') {
|
|
73
|
+
if (d > 15000) n -= 50; // não é efeito, é faixa
|
|
74
|
+
else n += 20 - Math.abs(d - alvoMs) / 500;
|
|
75
|
+
} else {
|
|
76
|
+
if (d && d < alvoMs * 0.6) n -= 20; // acaba antes do vídeo
|
|
77
|
+
n -= Math.abs(d - alvoMs) / 20000; // perto da duração pedida
|
|
78
|
+
if (f.category === 'music') n += 5;
|
|
79
|
+
}
|
|
80
|
+
if (/cc0|pdm/.test(f.license)) n += 3; // sem crédito obrigatório
|
|
81
|
+
if (/mp3/i.test(f.filetype || '') || /\.mp3(\?|$)/i.test(f.url)) n += 4; // toca em qualquer lugar
|
|
82
|
+
// As palavras da consulta no título/tags contam.
|
|
83
|
+
const alvo = `${f.title} ${(f.tags || []).map((t) => t.name).join(' ')} ${(f.genres || []).join(' ')}`.toLowerCase();
|
|
84
|
+
for (const p of consulta.toLowerCase().split(/\s+/)) if (p.length > 2 && alvo.includes(p)) n += 3;
|
|
85
|
+
return { f, n };
|
|
86
|
+
}).sort((a, b) => b.n - a.n);
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
total: j.result_count || itens.length,
|
|
90
|
+
itens: notas.map(({ f }) => ({
|
|
91
|
+
id: f.id, titulo: f.title, autor: f.creator, licenca: f.license, licencaUrl: f.license_url,
|
|
92
|
+
duracao: f.duration ? Math.round(f.duration / 1000) : null, url: f.url, pagina: f.foreign_landing_url,
|
|
93
|
+
provedor: f.provider, tipoArquivo: f.filetype || null, generos: f.genres || [], credito: credito(f),
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Baixa a faixa para <projeto>/midia/ e manda ao estúdio. */
|
|
99
|
+
async function trazer(item, { projectDir, studio }) {
|
|
100
|
+
const bytes = await referencia.pegar(item.url, { timeout: 45000, binario: true });
|
|
101
|
+
if (!bytes || bytes.length < 2000) return { ok: false, error: `não consegui baixar "${item.titulo}" de ${item.provedor}` };
|
|
102
|
+
// A extensão pelo conteúdo, não pela URL: o Jamendo entrega mp3 numa URL
|
|
103
|
+
// sem extensão ("?trackid=…&format=mp32").
|
|
104
|
+
const cab = bytes.subarray(0, 12);
|
|
105
|
+
const ext = cab.subarray(0, 3).toString() === 'ID3' || (cab[0] === 0xff && (cab[1] & 0xe0) === 0xe0) ? '.mp3'
|
|
106
|
+
: cab.subarray(0, 4).toString() === 'RIFF' ? '.wav'
|
|
107
|
+
: cab.subarray(0, 4).toString() === 'OggS' ? '.ogg'
|
|
108
|
+
: cab.subarray(0, 4).toString() === 'fLaC' ? '.flac'
|
|
109
|
+
: (item.tipoArquivo ? '.' + item.tipoArquivo.replace(/^\./, '') : '.mp3');
|
|
110
|
+
const pasta = path.join(projectDir || process.cwd(), 'midia');
|
|
111
|
+
fs.mkdirSync(pasta, { recursive: true });
|
|
112
|
+
const nome = `${slug(item.titulo)}-${String(item.id || '').slice(0, 6)}${ext}`;
|
|
113
|
+
const caminho = path.join(pasta, nome);
|
|
114
|
+
fs.writeFileSync(caminho, bytes);
|
|
115
|
+
// FLAC/OGG do Freesound: o navegador toca, mas o MP4 exportado quer AAC —
|
|
116
|
+
// o estúdio cuida disso na exportação; aqui só entregamos o arquivo.
|
|
117
|
+
const envio = await studio.midiaEnviar({ arquivo: caminho, nome });
|
|
118
|
+
if (!envio || envio.ok === false) return { ok: false, error: (envio && envio.error) || 'o estúdio não aceitou o arquivo', arquivoLocal: caminho };
|
|
119
|
+
return { ok: true, url: envio.url, arquivoLocal: path.relative(projectDir || process.cwd(), caminho), bytes: bytes.length };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A tool: procura, escolhe a melhor, baixa e devolve a URL pronta — e as
|
|
124
|
+
* alternativas, para o agente trocar sem pesquisar de novo.
|
|
125
|
+
*/
|
|
126
|
+
async function buscarAudio(args, { projectDir, studio }) {
|
|
127
|
+
const consulta = String(args.consulta || args.query || '').trim();
|
|
128
|
+
if (!consulta) return { ok: false, error: 'Diga o que procurar: o clima da música ("upbeat corporate", "piano calmo") ou o efeito ("whoosh", "ding", "click").' };
|
|
129
|
+
const tipo = /^(efeito|som|sfx|sound)/i.test(String(args.tipo || '')) ? 'efeito' : 'musica';
|
|
130
|
+
const dur = Number(args.duracao) || 0;
|
|
131
|
+
const busca = await procurar({ consulta, tipo, dur, comercial: args.comercial !== false });
|
|
132
|
+
if (!busca.ok) return busca;
|
|
133
|
+
if (!busca.itens.length) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `Nenhuma faixa livre para "${consulta}".`,
|
|
137
|
+
comoCorrigir: tipo === 'efeito'
|
|
138
|
+
? 'Use um som SINTETIZADO do estúdio, que não precisa de arquivo: som: "whoosh" | "impacto" | "click" | "pop" | "ding" | "subida" | "tique" | "brilho".'
|
|
139
|
+
: 'Tente a consulta em inglês e mais genérica ("upbeat electronic", "calm piano", "epic cinematic"), ou use a trilha GERADA: "trilha": {"tipo":"gerada","estilo":"pulso"}.',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Baixa a melhor; se ela falhar, a próxima. Três tentativas.
|
|
143
|
+
let trazido = null;
|
|
144
|
+
let escolhida = null;
|
|
145
|
+
for (const item of busca.itens.slice(0, 3)) {
|
|
146
|
+
trazido = await trazer(item, { projectDir, studio });
|
|
147
|
+
if (trazido.ok) { escolhida = item; break; }
|
|
148
|
+
}
|
|
149
|
+
if (!trazido || !trazido.ok) return { ok: false, error: (trazido && trazido.error) || 'não consegui baixar nenhuma faixa', alternativas: busca.itens.slice(0, 5) };
|
|
150
|
+
const precisaCredito = !/cc0|pdm/.test(escolhida.licenca);
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
tipo,
|
|
154
|
+
url: trazido.url,
|
|
155
|
+
titulo: escolhida.titulo,
|
|
156
|
+
autor: escolhida.autor,
|
|
157
|
+
duracao: escolhida.duracao,
|
|
158
|
+
licenca: NOME_LICENCA[escolhida.licenca] || escolhida.licenca,
|
|
159
|
+
licencaUrl: escolhida.licencaUrl,
|
|
160
|
+
credito: escolhida.credito,
|
|
161
|
+
pagina: escolhida.pagina,
|
|
162
|
+
arquivoLocal: trazido.arquivoLocal,
|
|
163
|
+
comoUsar: tipo === 'efeito'
|
|
164
|
+
? `No elemento ou na cena: "som": {"arquivo": "${trazido.url}", "volume": 0.8}.`
|
|
165
|
+
: `No documento: "trilha": {"tipo":"arquivo","src":"${trazido.url}","volume":0.55,"repetir":true}.`
|
|
166
|
+
+ (precisaCredito ? ` A licença pede CRÉDITO: ponha "${escolhida.credito}" na assinatura ou numa legenda da última cena.` : ' Licença sem exigência de crédito.'),
|
|
167
|
+
alternativas: busca.itens.filter((i) => i.id !== escolhida.id).slice(0, 4)
|
|
168
|
+
.map((i) => ({ titulo: i.titulo, autor: i.autor, duracao: i.duracao, licenca: i.licenca, id: i.id })),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { buscarAudio, procurar, trazer, credito, LIVRES, NOME_LICENCA };
|
package/lib/boas-vindas.js
CHANGED
|
@@ -157,13 +157,13 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
157
157
|
mostrar('');
|
|
158
158
|
mostrar(c.bold(c.brand(' Bem-vindo ao PrimoCode.')));
|
|
159
159
|
mostrar(c.muted(' Primeiro, entre na sua conta do Conecta Primo AI.'));
|
|
160
|
-
mostrar(c.dim(' Vou abrir o navegador; é só
|
|
160
|
+
mostrar(c.dim(' Vou abrir o navegador; é só entrar com a conta por lá — o resto é automático.'));
|
|
161
161
|
mostrar('');
|
|
162
162
|
const r = await conectar();
|
|
163
163
|
if (!r || r.ok === false) {
|
|
164
164
|
mostrar('');
|
|
165
165
|
mostrar(c.err(' Não deu para conectar' + (r && r.error ? `: ${r.error}` : '.')));
|
|
166
|
-
mostrar(c.muted(' Tente de novo com ') + c.brand('/
|
|
166
|
+
mostrar(c.muted(' Tente de novo com ') + c.brand('/connect') + c.muted('.'));
|
|
167
167
|
return { liberado: false, motivo: 'sem-login' };
|
|
168
168
|
}
|
|
169
169
|
credencial = conectado ? conectado() : { plano: r.plano };
|
|
@@ -181,7 +181,7 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
181
181
|
} else if (!s || !s.ok) {
|
|
182
182
|
mostrar('');
|
|
183
183
|
mostrar(c.err(' Não consegui confirmar sua conta' + (s && s.error ? `: ${s.error}` : '.')));
|
|
184
|
-
mostrar(c.muted(' Entre de novo com ') + c.brand('/
|
|
184
|
+
mostrar(c.muted(' Entre de novo com ') + c.brand('/connect') + c.muted('.'));
|
|
185
185
|
return { liberado: false, motivo: 'sessao-invalida' };
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -195,7 +195,7 @@ async function rodar({ perguntar, mostrar, cores: c, interativo = true,
|
|
|
195
195
|
mostrar('');
|
|
196
196
|
mostrar(c.dim(' Assine em ') + c.brand('conecta-primo-ai.vercel.app')
|
|
197
197
|
+ c.dim(' e rode ') + c.brand('primo') + c.dim(' de novo.'));
|
|
198
|
-
mostrar(c.dim(' Já assinou? ') + c.brand('/
|
|
198
|
+
mostrar(c.dim(' Já assinou? ') + c.brand('/connect')
|
|
199
199
|
+ c.dim(' atualiza o plano nesta máquina.'));
|
|
200
200
|
mostrar('');
|
|
201
201
|
return { liberado: false, motivo: 'sem-plano', plano };
|
package/lib/catalogo.js
CHANGED
|
@@ -143,6 +143,13 @@ const GRUPOS = [
|
|
|
143
143
|
args: 'todos: lista de {content: a tarefa curta no imperativo, status: pending|in_progress|completed}. Reescreve a lista inteira a cada chamada; só uma "in_progress" por vez.' },
|
|
144
144
|
{ tool: 'pesquisar_referencia', escreve: false, faz: 'lê docs e exemplos antes de escrever — sobe a qualidade',
|
|
145
145
|
args: 'consulta: o que pesquisar, de preferência em inglês. paginas (opcional, número, padrão 3, máximo 5).' },
|
|
146
|
+
// Baixa o arquivo para <projeto>/midia/ — por isso `escreve: true`.
|
|
147
|
+
{ tool: 'buscar_audio', escreve: true, faz: 'acha música ou efeito sonoro livre na internet (Creative Commons), baixa e manda para o estúdio, com o crédito',
|
|
148
|
+
args: 'consulta: o clima ou o som, em inglês ("upbeat corporate", "calm piano", "whoosh", "ding"). tipo (opcional): "musica" (padrão) ou "efeito". '
|
|
149
|
+
+ 'duracao (opcional, segundos): quanto o vídeo dura, para a faixa combinar. Devolve url para trilha.src ou som.arquivo, e o crédito a pôr na peça quando a licença pedir.' },
|
|
150
|
+
{ tool: 'marca_da_empresa', escreve: true, faz: 'puxa do site da empresa a logo, a cor principal, a paleta, as fontes e a frase — pronto para `marca` no estúdio',
|
|
151
|
+
args: 'nome: a empresa como o usuário falou. site (opcional): o endereço, se souber — sem ele, o site é descoberto. '
|
|
152
|
+
+ 'Devolve marca:{cor, logo} para colar no documento, mais cores.paleta, fontes e frase. Chame ANTES de criar uma peça de marca.' },
|
|
146
153
|
// Vai para memory-geral.md, FORA da pasta do projeto — por isso não
|
|
147
154
|
// é write_file/edit_file (a sandbox de resolveInProject bloqueia
|
|
148
155
|
// qualquer caminho fora do projeto, e é assim mesmo que tem que
|