primocode 8.8.0 → 8.9.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 +24 -47
- package/package.json +1 -1
- package/studio/server.py +116 -5
package/bin/primocode.js
CHANGED
|
@@ -540,11 +540,11 @@ async function sendPrompt(userText) {
|
|
|
540
540
|
|
|
541
541
|
// O pedido passa por uma cabeça antes de virar trabalho: conserta o
|
|
542
542
|
// que está torto, diz qual é o entregável e o que conta como pronto.
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
const passos = await dividirTarefa(
|
|
543
|
+
// UMA chamada faz as duas coisas: arruma o pedido torto e, se fizer
|
|
544
|
+
// sentido, divide em passos. Eram duas idas ao modelo por pedido, e
|
|
545
|
+
// num plano limitado por REQUISIÇÕES (o grátis da Groq são 30 por
|
|
546
|
+
// minuto) cada chamada extra custa tanto quanto token.
|
|
547
|
+
const passos = await dividirTarefa(prompt);
|
|
548
548
|
|
|
549
549
|
let r = null;
|
|
550
550
|
let jaFeito = '';
|
|
@@ -585,66 +585,43 @@ async function sendPrompt(userText) {
|
|
|
585
585
|
}
|
|
586
586
|
|
|
587
587
|
/**
|
|
588
|
-
*
|
|
588
|
+
* Arruma o pedido e divide em passos — as duas coisas, ESCONDIDAS.
|
|
589
|
+
*
|
|
590
|
+
* ARRUMAR: gente pede curto e torto ("cria um vidro anunciando o founders
|
|
591
|
+
* ai"). Sem isso o agente adivinha, e adivinha errado. É oculto porque
|
|
592
|
+
* encanamento não é conversa: o usuário quer ver a coisa acontecendo, não um
|
|
593
|
+
* relatório do que a máquina entendeu — e não há o que aprovar, já que o
|
|
594
|
+
* refino não decide nada, só desambigua o que ele já disse.
|
|
589
595
|
*
|
|
590
|
-
*
|
|
591
|
-
* crie um vídeo" não cabe numa conversa
|
|
592
|
-
*
|
|
593
|
-
*
|
|
594
|
-
*
|
|
596
|
+
* DIVIDIR: o motivo é aritmético. Com 6.000 tokens por minuto, "pesquise
|
|
597
|
+
* sobre a X e crie um vídeo" não cabe numa conversa só — custo fixo ~3.700,
|
|
598
|
+
* spec do formato ~1.400, fatos da pesquisa o resto, e sobram 512 para o
|
|
599
|
+
* agente escrever. Em dois pedidos cada um cabe folgado, porque cada um leva
|
|
600
|
+
* só as ferramentas e o prompt do seu passo.
|
|
595
601
|
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
* está a fronteira barata, e pode partir num pedaço que ainda não cabe.
|
|
602
|
+
* As duas numa chamada só: elas leem o mesmo texto, e num plano limitado por
|
|
603
|
+
* requisições por minuto a segunda ida ao modelo custa tanto quanto token.
|
|
599
604
|
*
|
|
600
|
-
* Falhou?
|
|
605
|
+
* Falhou? O pedido do usuário, inteiro, como ele escreveu.
|
|
601
606
|
*/
|
|
602
607
|
async function dividirTarefa(pedido) {
|
|
603
608
|
const inteiro = [{ titulo: 'tarefa', pedido }];
|
|
609
|
+
if (!pedido || pedido.trim().length < 12) return inteiro;
|
|
610
|
+
const sp = new Spinner('Primo Code pensando').start();
|
|
604
611
|
try {
|
|
605
612
|
const r = await api.requestPlano(state.config.server, {
|
|
606
613
|
prompt: pedido, model: state.config.model, token: state.config.token,
|
|
607
614
|
});
|
|
615
|
+
sp.stop();
|
|
608
616
|
const passos = (r && Array.isArray(r.passos) && r.passos.length) ? r.passos : inteiro;
|
|
609
617
|
if (passos.length > 1) console.log(c.dim(` ⎿ ${passos[0].titulo}`));
|
|
610
618
|
return passos;
|
|
611
619
|
} catch {
|
|
620
|
+
sp.stop(); // preparar o pedido é melhoria, não requisito
|
|
612
621
|
return inteiro;
|
|
613
622
|
}
|
|
614
623
|
}
|
|
615
624
|
|
|
616
|
-
/**
|
|
617
|
-
* Estrutura o pedido antes de mandar o agente executar.
|
|
618
|
-
*
|
|
619
|
-
* Gente pede curto e torto — "cria um vidro anunciando o founders ai". Sem
|
|
620
|
-
* isto o agente adivinha, e adivinha errado: sai o genérico, ou ele pesquisa
|
|
621
|
-
* em círculo porque não sabe o que é "pronto". Uma passada barata antes de
|
|
622
|
-
* começar vira ordem de serviço: o que entregar, com que fatos, quando parar.
|
|
623
|
-
*
|
|
624
|
-
* É OCULTO de propósito. A primeira versão mostrava "entendi assim:" na tela,
|
|
625
|
-
* e o dono cortou: encanamento não é conversa. O usuário pediu uma coisa e
|
|
626
|
-
* quer ver a coisa acontecendo, não um relatório do que a máquina entendeu.
|
|
627
|
-
* O refino também não decide nada — só desambigua o que ele já disse —, então
|
|
628
|
-
* não há o que ele precise aprovar.
|
|
629
|
-
*
|
|
630
|
-
* Nunca bloqueia: se o refino falhar ou vier estranho, vale o pedido original.
|
|
631
|
-
*/
|
|
632
|
-
async function refinarPedido(cru) {
|
|
633
|
-
if (!cru || cru.trim().length < 12) return cru;
|
|
634
|
-
const sp = new Spinner('Primo Code pensando').start();
|
|
635
|
-
try {
|
|
636
|
-
const r = await api.requestRefino(state.config.server, {
|
|
637
|
-
prompt: cru, model: state.config.model, token: state.config.token,
|
|
638
|
-
});
|
|
639
|
-
sp.stop();
|
|
640
|
-
if (!r || !r.refinado || !r.prompt) return cru;
|
|
641
|
-
return r.prompt;
|
|
642
|
-
} catch {
|
|
643
|
-
sp.stop(); // refinar é melhoria, não requisito
|
|
644
|
-
return cru;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
|
|
648
625
|
/**
|
|
649
626
|
* Atualiza o resumo da conversa, se já compensar.
|
|
650
627
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.9.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": {
|
package/studio/server.py
CHANGED
|
@@ -15,6 +15,7 @@ import mimetypes
|
|
|
15
15
|
import os
|
|
16
16
|
import re
|
|
17
17
|
import sys
|
|
18
|
+
import time
|
|
18
19
|
import unicodedata
|
|
19
20
|
import uuid
|
|
20
21
|
from datetime import datetime, timezone
|
|
@@ -461,6 +462,8 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
461
462
|
|
|
462
463
|
if p == "/api/voz":
|
|
463
464
|
return self.voz(urlparse(self.path).query)
|
|
465
|
+
if p == "/api/voz/status":
|
|
466
|
+
return self.voz_status()
|
|
464
467
|
|
|
465
468
|
if p == "/api/midia":
|
|
466
469
|
return self.js(listar_midia())
|
|
@@ -506,20 +509,128 @@ class Handler(SimpleHTTPRequestHandler):
|
|
|
506
509
|
if len(texto) > 4000:
|
|
507
510
|
texto = texto[:4000]
|
|
508
511
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
512
|
+
voz = (q.get("voz", [""])[0] or "").strip()
|
|
513
|
+
instrucao = (q.get("instrucao", [""])[0] or "").strip()
|
|
514
|
+
motor = (q.get("motor", [""])[0] or "").strip()
|
|
515
|
+
|
|
516
|
+
tipo = "audio/mpeg"
|
|
517
|
+
audio = b""
|
|
518
|
+
# 1) Voicebox, se o usuário tiver instalado: voz clonável, ilimitada e
|
|
519
|
+
# sem depender de nada na internet.
|
|
520
|
+
if motor != "google" and self._voicebox_vivo():
|
|
521
|
+
try:
|
|
522
|
+
audio, tipo = self._voz_voicebox(texto, voz, instrucao)
|
|
523
|
+
except Exception as e:
|
|
524
|
+
sys.stderr.write("[voz] Voicebox falhou (%s); indo de Google\n" % e)
|
|
525
|
+
audio = b""
|
|
526
|
+
# 2) Google: uma voz só, mas responde sempre.
|
|
527
|
+
if not audio:
|
|
528
|
+
try:
|
|
529
|
+
audio = b"".join(self._trecho_de_voz(t, idioma) for t in _picar(texto, 190))
|
|
530
|
+
except Exception as e: # rede, bloqueio, mudança do serviço
|
|
531
|
+
return self.js({"erro": "voz indisponível: %s" % e}, 502)
|
|
513
532
|
if not audio:
|
|
514
533
|
return self.js({"erro": "voz não devolveu áudio"}, 502)
|
|
515
534
|
|
|
516
535
|
self.send_response(200)
|
|
517
|
-
self.send_header("Content-Type",
|
|
536
|
+
self.send_header("Content-Type", tipo)
|
|
518
537
|
self.send_header("Content-Length", str(len(audio)))
|
|
519
538
|
self.send_header("Cache-Control", "no-store")
|
|
520
539
|
self.end_headers()
|
|
521
540
|
self.wfile.write(audio)
|
|
522
541
|
|
|
542
|
+
# ── Voicebox ───────────────────────────────────────────────────────
|
|
543
|
+
# https://voicebox.sh — app local, código aberto, de graça e sem limite,
|
|
544
|
+
# com clonagem de voz. É a melhor voz que existe para este uso e não
|
|
545
|
+
# depende de serviço nenhum na internet: roda na máquina do usuário.
|
|
546
|
+
#
|
|
547
|
+
# A ponte é FEITA AQUI, e não no navegador, por causa do CORS: o Voicebox
|
|
548
|
+
# só libera as portas dele (5173 e 17493), e o Studio serve em 7777 — do
|
|
549
|
+
# navegador o pedido morreria antes de sair.
|
|
550
|
+
#
|
|
551
|
+
# Não está instalado? A rota cai para o Google, e ninguém fica sem voz.
|
|
552
|
+
VOICEBOX = "http://127.0.0.1:17493"
|
|
553
|
+
|
|
554
|
+
def voz_status(self):
|
|
555
|
+
"""O que está disponível para narrar, para a interface não oferecer o
|
|
556
|
+
que não existe."""
|
|
557
|
+
vivo = self._voicebox_vivo()
|
|
558
|
+
vozes = []
|
|
559
|
+
if vivo:
|
|
560
|
+
try:
|
|
561
|
+
with urllib.request.urlopen(self.VOICEBOX + "/profiles", timeout=6) as r:
|
|
562
|
+
vozes = [{"id": v.get("id"), "nome": v.get("name"), "idioma": v.get("language")}
|
|
563
|
+
for v in json.loads(r.read().decode("utf-8"))]
|
|
564
|
+
except Exception:
|
|
565
|
+
vozes = []
|
|
566
|
+
return self.js({
|
|
567
|
+
"voicebox": {"ativo": vivo, "vozes": vozes, "site": "https://voicebox.sh"},
|
|
568
|
+
"google": {"ativo": True},
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
def _voicebox_vivo(self):
|
|
572
|
+
try:
|
|
573
|
+
with urllib.request.urlopen(self.VOICEBOX + "/health", timeout=2):
|
|
574
|
+
return True
|
|
575
|
+
except Exception:
|
|
576
|
+
return False
|
|
577
|
+
|
|
578
|
+
def _voicebox_perfil(self, pedido):
|
|
579
|
+
"""A voz a usar: a que o usuário pediu pelo nome, ou a primeira que
|
|
580
|
+
houver. Sem perfil não há geração — o Voicebox exige um."""
|
|
581
|
+
req = urllib.request.Request(self.VOICEBOX + "/profiles")
|
|
582
|
+
with urllib.request.urlopen(req, timeout=8) as r:
|
|
583
|
+
perfis = json.loads(r.read().decode("utf-8"))
|
|
584
|
+
if not perfis:
|
|
585
|
+
raise RuntimeError("Voicebox está no ar mas sem nenhuma voz criada")
|
|
586
|
+
if pedido:
|
|
587
|
+
alvo = pedido.strip().lower()
|
|
588
|
+
for p in perfis:
|
|
589
|
+
if alvo in (p.get("name") or "").lower() or alvo == p.get("id"):
|
|
590
|
+
return p
|
|
591
|
+
# Sem escolha explícita, prefere uma voz em português.
|
|
592
|
+
for p in perfis:
|
|
593
|
+
if (p.get("language") or "").startswith("pt"):
|
|
594
|
+
return p
|
|
595
|
+
return perfis[0]
|
|
596
|
+
|
|
597
|
+
def _voz_voicebox(self, texto, voz, instrucao):
|
|
598
|
+
perfil = self._voicebox_perfil(voz)
|
|
599
|
+
corpo = json.dumps({
|
|
600
|
+
"profile_id": perfil["id"],
|
|
601
|
+
"text": texto[:50000],
|
|
602
|
+
"language": "pt",
|
|
603
|
+
# A instrução de entrega é o que dá voz de cinema em vez de voz de
|
|
604
|
+
# leitura. O Voicebox aceita em linguagem natural, até 500 chars.
|
|
605
|
+
"instruct": (instrucao or "Narração institucional: voz grave, ritmo pausado, tom seguro.")[:500],
|
|
606
|
+
"max_chunk_chars": 800,
|
|
607
|
+
"crossfade_ms": 50,
|
|
608
|
+
"normalize": True,
|
|
609
|
+
}).encode("utf-8")
|
|
610
|
+
req = urllib.request.Request(self.VOICEBOX + "/generate", data=corpo,
|
|
611
|
+
headers={"Content-Type": "application/json"})
|
|
612
|
+
with urllib.request.urlopen(req, timeout=30) as r:
|
|
613
|
+
g = json.loads(r.read().decode("utf-8"))
|
|
614
|
+
gid = g.get("id")
|
|
615
|
+
if not gid:
|
|
616
|
+
raise RuntimeError("Voicebox não devolveu id de geração")
|
|
617
|
+
|
|
618
|
+
# /generate volta na hora com status "generating" e sintetiza em
|
|
619
|
+
# segundo plano — então é preciso esperar de verdade.
|
|
620
|
+
prazo = time.time() + 180
|
|
621
|
+
estado = g.get("status")
|
|
622
|
+
while estado in (None, "generating", "pending", "processing"):
|
|
623
|
+
if time.time() > prazo:
|
|
624
|
+
raise RuntimeError("Voicebox demorou demais")
|
|
625
|
+
time.sleep(1.0)
|
|
626
|
+
with urllib.request.urlopen(self.VOICEBOX + "/generate/%s/status" % gid, timeout=8) as r:
|
|
627
|
+
estado = (json.loads(r.read().decode("utf-8")) or {}).get("status")
|
|
628
|
+
if estado != "completed":
|
|
629
|
+
raise RuntimeError("Voicebox falhou (%s)" % estado)
|
|
630
|
+
|
|
631
|
+
with urllib.request.urlopen(self.VOICEBOX + "/audio/%s" % gid, timeout=30) as r:
|
|
632
|
+
return r.read(), (r.headers.get("Content-Type") or "audio/wav")
|
|
633
|
+
|
|
523
634
|
def _trecho_de_voz(self, texto, idioma):
|
|
524
635
|
url = "https://translate.google.com/translate_tts?" + urlencode({
|
|
525
636
|
"ie": "UTF-8", "q": texto, "tl": idioma, "client": "tw-ob", "total": 1, "idx": 0,
|