primocode 10.14.0 → 10.15.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/LICENSES/robo.txt +19 -0
- package/README.md +6 -2
- package/bin/primocode.js +46 -3
- package/lib/act.js +9 -2
- package/lib/estudio/cota.js +10 -3
- package/lib/estudio/editor.js +5 -0
- package/lib/estudio/index.js +9 -2
- package/lib/estudio/projetos.js +13 -1
- package/lib/limites.js +100 -0
- package/lib/marca-kit.js +3 -0
- package/lib/planos.js +10 -8
- package/lib/preparar.js +2 -0
- package/lib/voz-ponte.js +15 -0
- package/lib/voz.js +75 -5
- package/package.json +3 -2
- package/voz/primo_voz/__main__.py +307 -54
- package/voz/primo_voz/conversa.py +236 -0
- package/voz/primo_voz/escuta.py +32 -2
- package/voz/primo_voz/hud.html +550 -0
- package/voz/primo_voz/hud.py +224 -0
- package/voz/primo_voz/identidade.py +88 -0
- package/voz/primo_voz/luz.py +137 -0
- package/voz/primo_voz/painel.py +459 -0
- package/voz/primo_voz/painel_tui.py +26 -0
- package/voz/primo_voz/sons.py +96 -0
- package/voz/primo_voz/tela.py +337 -0
- package/voz/primo_voz/vendor/RobotExpressive.glb +0 -0
- package/voz/primo_voz/vendor/spline-runtime.min.js +8219 -0
- package/voz/primo_voz/vendor/three-robo.min.js +3824 -0
- package/voz/primo_voz/vendor/three.LICENSE.txt +21 -0
- package/voz/primo_voz/vivo.py +34 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Robô 3D da tela do Primo (voz/primo_voz/vendor/RobotExpressive.glb)
|
|
2
|
+
|
|
3
|
+
Modelo: RobotExpressive, de Tomás Laulhé (https://www.patreon.com/quaternius). CC0 1.0 (domínio público).
|
|
4
|
+
Modificações de Don McCurdy: três morph targets de expressão, conversão com FBX2GLTF, materiais deduplicados.
|
|
5
|
+
Fonte: https://github.com/mrdoob/three.js/tree/r170/examples/models/gltf/RobotExpressive (README conferido na tag r170).
|
|
6
|
+
Aqui as cores dos materiais são trocadas em tempo de execução (branco/grafite + ciano); o arquivo .glb é o original.
|
|
7
|
+
|
|
8
|
+
three.js r170 + GLTFLoader (voz/primo_voz/vendor/three-robo.min.js, bundle mínimo gerado com esbuild)
|
|
9
|
+
Licença MIT, Copyright 2010-2024 three.js authors. Texto completo: voz/primo_voz/vendor/three.LICENSE.txt
|
|
10
|
+
|
|
11
|
+
--- Robô do Spline (voz.json: "robo": "spline", o padrão; "expressive" volta ao RobotExpressive acima) ---
|
|
12
|
+
|
|
13
|
+
Cena: "AI Robotics Landing Page" (template da comunidade v0/Spline)
|
|
14
|
+
https://v0.app/templates/ai-robotics-landing-page-iArz1CoDHnf
|
|
15
|
+
https://prod.spline.design/kZDDjO5HuC9GJUM2/scene.splinecode (baixada em tempo de execução, precisa de internet; não vai no repo)
|
|
16
|
+
Licença da cena: [VERIFICAR] — confirmar com o autor/Spline antes de uso comercial. Uso atual: pedido do Joel, uso interno.
|
|
17
|
+
Runtime: @splinetool/runtime 1.12.95 (voz/primo_voz/vendor/spline-runtime.min.js, bundle IIFE gerado com esbuild; local, sem CDN).
|
|
18
|
+
O pacote npm não declara campo "license" [VERIFICAR] — o Joel informou MIT; conferir no repositório splinetool antes de redistribuir.
|
|
19
|
+
Sem internet ou cena lenta (> 6 s): cai no RobotExpressive (CC0).
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PrimoCode v10.
|
|
1
|
+
# PrimoCode v10.15.0
|
|
2
2
|
|
|
3
3
|
Agente de engenharia brasileiro para o terminal, no estilo Claude Code.
|
|
4
4
|
Cria arquivos de verdade, roda comandos, **controla o navegador e o desktop** —
|
|
@@ -9,7 +9,7 @@ OpenRouter, e cada apelido (`top`/`main`/`fast`) é uma **corrente**: se um mode
|
|
|
9
9
|
está no limite de pedidos, o próximo atende. Você não vê o erro, e não paga nada.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
▐▛███▜▌ Primo Code v10.
|
|
12
|
+
▐▛███▜▌ Primo Code v10.15.0
|
|
13
13
|
▝▜█████▛▘ Bem-vindo de volta, Joel
|
|
14
14
|
▘▘ ▝▝ ~/primocode · /dir <pasta> muda
|
|
15
15
|
```
|
|
@@ -17,6 +17,10 @@ está no limite de pedidos, o próximo atende. Você não vê o erro, e não pag
|
|
|
17
17
|
Personalize o mascote (`primo`, `fox`, `cat`) e o nome com que ele te chama —
|
|
18
18
|
veja `/mascot` e `/name`.
|
|
19
19
|
|
|
20
|
+
## Primo (voz)
|
|
21
|
+
|
|
22
|
+
`/voice` abre a tela do Primo: agenda, clima, música, máquina e um robô 3D no centro. Diga **"Primo"** para chamar; ele fala frase a frase e você pode interromper. Na primeira vez cria `~/.primocode/voz-venv` sozinho (`primocode doutor` mostra o estado). Robô offline: `"robo":"expressive"` em `~/.primocode/voz.json`.
|
|
23
|
+
|
|
20
24
|
## Novidades da 10.14
|
|
21
25
|
|
|
22
26
|
- **Vídeo de motion de ponta a ponta** (EffectCraft, 68 receitas, voz, trilha, prévia e editor por comando). Detalhes no `CHANGELOG.md`.
|
package/bin/primocode.js
CHANGED
|
@@ -212,7 +212,9 @@ const SLASH_COMANDOS = [
|
|
|
212
212
|
['/chats', 'suas conversas salvas, as mesmas do app e do celular'],
|
|
213
213
|
['/logout', 'desconecta este computador da conta'],
|
|
214
214
|
['!comando', 'roda direto no shell, sem passar pelo agente (o campo fica âmbar)'],
|
|
215
|
-
['/voice', 'liga o
|
|
215
|
+
['/voice', 'liga o Primo: diga "Primo, ..."; ele faz e responde em voz alta, com a tela dele (robô, agenda, clima, música)'],
|
|
216
|
+
['/voice painel', 'só o painel do Primo (ESC esconde)'],
|
|
217
|
+
['/voice nome <Nome>', 'dá outro nome ao assistente (vira a palavra de ativação)'],
|
|
216
218
|
['/voice parar', 'desliga o modo voz e fecha o microfone'],
|
|
217
219
|
['/skills', 'o que ele sabe fazer bem — ligue e desligue'],
|
|
218
220
|
['/marketplace', 'a tela das skills: navegue, ligue, crie'],
|
|
@@ -1619,6 +1621,7 @@ let chatEmVoo = null;
|
|
|
1619
1621
|
/* O ouvinte de voz em segundo plano.
|
|
1620
1622
|
Um por sessão: dois processos disputando o mesmo microfone dão um vazio
|
|
1621
1623
|
intermitente que parece defeito de hardware e não é. */
|
|
1624
|
+
let vozPainel = null; // /voice painel: só o HUD
|
|
1622
1625
|
let vozEmEspera = null; // a sessão do /voice: { processo, falar, calar }
|
|
1623
1626
|
/* Por onde a fala entra no terminal. Nasce no main(), junto da fila — é lá
|
|
1624
1627
|
que moram o `busy`, a fila e o `ask` pendente. */
|
|
@@ -2474,8 +2477,26 @@ async function handleSlash(text) {
|
|
|
2474
2477
|
|
|
2475
2478
|
Desligar é o primeiro caso de propósito: quem quer fechar o
|
|
2476
2479
|
microfone quer AGORA, sem esbarrar em checagem de dependência. */
|
|
2477
|
-
|
|
2478
|
-
|
|
2480
|
+
const argVoz = (rest || '').trim();
|
|
2481
|
+
// Só o painel (HUD em tela cheia no Mac; quadro no terminal fora dele).
|
|
2482
|
+
if (/^painel$/i.test(argVoz)) {
|
|
2483
|
+
const py = vozModo.python();
|
|
2484
|
+
if (!py) { console.log(c.err(' O painel precisa do Python 3.')); break; }
|
|
2485
|
+
if (vozPainel) { try { vozPainel.processo.kill(); } catch {} vozPainel = null; }
|
|
2486
|
+
vozPainel = vozModo.abrirPainel({ python: py });
|
|
2487
|
+
console.log(c.muted(vozPainel.tela ? ' Tela do Primo aberta — ESC esconde.' : ' Sem o painel gráfico aqui: rode `primocode voz painel` num terminal separado.'));
|
|
2488
|
+
break;
|
|
2489
|
+
}
|
|
2490
|
+
// /voice nome <Nome>: a pessoa dá o nome ao assistente (e a palavra de ativação passa a ser ele).
|
|
2491
|
+
const mNome = argVoz.match(/^nome\s+(.{2,20})$/i);
|
|
2492
|
+
if (mNome) {
|
|
2493
|
+
const salvo = vozModo.definirNome(mNome[1]);
|
|
2494
|
+
console.log(salvo ? c.ok(` Fechado: o assistente agora se chama ${salvo}. Vale na próxima vez que ligar o /voice.`) : c.err(' Nome inválido (1 ou 2 palavras, até 20 letras).'));
|
|
2495
|
+
break;
|
|
2496
|
+
}
|
|
2497
|
+
if (/^(parar|stop|off|sair)$/i.test(argVoz)) {
|
|
2498
|
+
if (vozPainel) { try { vozPainel.processo.kill(); } catch {} vozPainel = null; }
|
|
2499
|
+
if (!vozEmEspera) { console.log(c.muted(vozPainel === null ? ' Modo voz e painel fechados.' : ' O modo voz não está ligado.')); break; }
|
|
2479
2500
|
try { vozEmEspera.processo.kill(); } catch {}
|
|
2480
2501
|
vozEmEspera = null;
|
|
2481
2502
|
console.log(c.muted(' Modo voz desligado. O microfone fechou.'));
|
|
@@ -2508,10 +2529,12 @@ async function handleSlash(text) {
|
|
|
2508
2529
|
console.log(' ' + c.bold(c.brand('🎙 Modo voz ligado')) + c.muted(' — é só falar. A resposta sai em voz alta.'));
|
|
2509
2530
|
console.log(' ' + c.dim('Falar por cima corta a voz · digitar continua valendo · /voice parar desliga'));
|
|
2510
2531
|
},
|
|
2532
|
+
aoParar: () => { try { require('../lib/act.js').interromper(); } catch { /* nada rodando */ } },
|
|
2511
2533
|
aoOuvir: (texto) => { if (entradaDeVoz) entradaDeVoz(texto); },
|
|
2512
2534
|
mostrar: (texto) => console.log(' ' + c.dim(texto)),
|
|
2513
2535
|
});
|
|
2514
2536
|
vozEmEspera = sessao;
|
|
2537
|
+
require('../lib/voz-ponte').escutar((nome, args) => sessao.ferramenta(nome, args));
|
|
2515
2538
|
sessao.pronto.then((r) => {
|
|
2516
2539
|
if (vozEmEspera === sessao) vozEmEspera = null;
|
|
2517
2540
|
if (r && !r.ok) console.log(c.err(` o modo voz fechou: ${r.error}`));
|
|
@@ -2883,6 +2906,25 @@ async function main() {
|
|
|
2883
2906
|
process.exit(require('../lib/skills.js').main(process.argv.slice(3)));
|
|
2884
2907
|
}
|
|
2885
2908
|
// `primocode planos`: a tabela comparativa, sem abrir a sessão.
|
|
2909
|
+
// `primocode voz nome <Nome>` / `primocode voz painel`: o Primo sem abrir a sessão.
|
|
2910
|
+
if (process.argv[2] === 'voz') {
|
|
2911
|
+
const vz = require('../lib/voz.js');
|
|
2912
|
+
const [sub, ...resto] = process.argv.slice(3);
|
|
2913
|
+
if (sub === 'nome') {
|
|
2914
|
+
const n = vz.definirNome(resto.join(' '));
|
|
2915
|
+
console.log(n ? `Fechado: o assistente agora se chama ${n}.` : 'Uso: primocode voz nome <Nome> (1 ou 2 palavras)');
|
|
2916
|
+
process.exit(n ? 0 : 1);
|
|
2917
|
+
}
|
|
2918
|
+
if (sub === 'painel') {
|
|
2919
|
+
const py = vz.python();
|
|
2920
|
+
if (!py) { console.log('O painel precisa do Python 3.'); process.exit(1); }
|
|
2921
|
+
const r = vz.abrirPainel({ python: py });
|
|
2922
|
+
r.processo.on('close', () => process.exit(0));
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
console.log('Uso: primocode voz nome <Nome> | primocode voz painel (o resto é /voice dentro da sessão)');
|
|
2926
|
+
process.exit(0);
|
|
2927
|
+
}
|
|
2886
2928
|
if (['doutor', 'doctor', 'preparar'].includes(process.argv[2])) {
|
|
2887
2929
|
process.exit(await require('../lib/preparar.js').main(process.argv.slice(2)));
|
|
2888
2930
|
}
|
|
@@ -3129,6 +3171,7 @@ async function main() {
|
|
|
3129
3171
|
}
|
|
3130
3172
|
} catch (e) {
|
|
3131
3173
|
fala.problema(e);
|
|
3174
|
+
if (vozEmEspera && vozEmEspera.estado) vozEmEspera.estado('erro');
|
|
3132
3175
|
}
|
|
3133
3176
|
// Pedido falado (ou tarefa que recebeu recado falado): a resposta
|
|
3134
3177
|
// final sai em voz alta, resumida. O detalhe fica na tela.
|
package/lib/act.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
const { c, Spinner, PENSANDO } = require('./ui');
|
|
14
14
|
const fala = require('./fala');
|
|
15
15
|
const especialistas = require('./especialistas');
|
|
16
|
+
const limites = require('./limites');
|
|
17
|
+
let avisouParalelo = false;
|
|
16
18
|
const tools = require('./tools');
|
|
17
19
|
const api = require('./api');
|
|
18
20
|
const cfg = require('./config');
|
|
@@ -844,6 +846,7 @@ async function runAct(userPrompt, opts) {
|
|
|
844
846
|
let paradoPorTravar = null;
|
|
845
847
|
/* Anota ANTES de agir (lib/auditoria.js): recusado também fica no livro. */
|
|
846
848
|
const anotar = (tc, decisao, motivo) => {
|
|
849
|
+
if (decisao !== 'recusada' && decisao !== 'negada') require('./voz-ponte').ferramenta(tc.name, tc.arguments);
|
|
847
850
|
const id = auditoria.antes({ agente: rotulo, ferramenta: tc.name, args: tc.arguments, decisao, motivo });
|
|
848
851
|
const falhou = auditoria.problema();
|
|
849
852
|
if (falhou) console.log(c.warn(`⚠ ${falhou}`));
|
|
@@ -1322,8 +1325,12 @@ async function runAct(userPrompt, opts) {
|
|
|
1322
1325
|
const spawns = toolCalls.filter((tc) => tc.name === 'spawn_agent');
|
|
1323
1326
|
const resultadosSpawn = new Map();
|
|
1324
1327
|
if (spawns.length && !PARAR) garantirPasta();
|
|
1325
|
-
|
|
1326
|
-
|
|
1328
|
+
/* O plano decide quantos subagentes rodam JUNTOS (lib/limites.js; grátis e
|
|
1329
|
+
Primo: um de cada vez, o resto continua em fila). Nunca recusa: só diz o porquê. */
|
|
1330
|
+
const par = spawns.length > 1 ? await limites.paralelo() : null;
|
|
1331
|
+
if (par) { const av = par.aviso(spawns.length); if (av && !avisouParalelo) { avisouParalelo = true; console.log(` ${av}`); } }
|
|
1332
|
+
if (spawns.length > 1 && par.max > 1 && !PARAR) {
|
|
1333
|
+
const juntos = spawns.slice(0, par.max);
|
|
1327
1334
|
fala.paralelo(juntos.length, profundidade);
|
|
1328
1335
|
const ids = juntos.map((tc) => anotar(tc, 'permitida'));
|
|
1329
1336
|
const rs = await Promise.all(juntos.map((tc) =>
|
package/lib/estudio/cota.js
CHANGED
|
@@ -19,7 +19,7 @@ const zlib = require('zlib');
|
|
|
19
19
|
const { spawnSync } = require('child_process');
|
|
20
20
|
|
|
21
21
|
const LOCAL = { ok: true, offline: true, liberado: true, plano: 'local', restam: null, usados: 0,
|
|
22
|
-
recursos: { videosMes: 0, resolucao: 2160, marcaDagua: false } };
|
|
22
|
+
recursos: { videosMes: 0, resolucao: 2160, marcaDagua: false, narracaoPremium: true, clubeCompleto: true, scriptsAE: true, subagentes: 7, kitsMarca: 0, versoesEditor: 0 } };
|
|
23
23
|
|
|
24
24
|
/** Pergunta (acao "checar") ou conta (acao "gastar") no servidor. */
|
|
25
25
|
async function consultar(acao = 'checar', { buscar = fetch, servidor, token, prazo = 6000 } = {}) {
|
|
@@ -33,6 +33,7 @@ async function consultar(acao = 'checar', { buscar = fetch, servidor, token, pra
|
|
|
33
33
|
});
|
|
34
34
|
const j = await r.json();
|
|
35
35
|
if (!r.ok || !j || !j.recursos) return LOCAL;
|
|
36
|
+
require('../limites.js').guardar(j);
|
|
36
37
|
return j;
|
|
37
38
|
} catch { return LOCAL; }
|
|
38
39
|
}
|
|
@@ -137,14 +138,20 @@ function aplicar(arquivo, recursos, { rodar = spawnSync, dim = dimensoes } = {})
|
|
|
137
138
|
async function comCota(render, deps = {}) {
|
|
138
139
|
const c = await (deps.consultar || consultar)('checar');
|
|
139
140
|
if (!c.liberado) return { ok: false, error: mensagemLimite(c), cota: { plano: c.plano, restam: 0, upgrade: c.upgrade } };
|
|
141
|
+
const lim = require('../limites.js');
|
|
142
|
+
lim.guardar(c); lim.pegarAvisos();
|
|
140
143
|
const r = await render();
|
|
144
|
+
const extra = lim.pegarAvisos();
|
|
141
145
|
if (!r || !r.ok) return r;
|
|
142
146
|
const a = c.offline ? { ok: true, mudou: false } : (deps.aplicar || aplicar)(r.video, c.recursos);
|
|
143
147
|
if (!c.offline) await (deps.consultar || consultar)('gastar');
|
|
144
148
|
const restam = c.restam == null ? null : Math.max(0, c.restam - 1);
|
|
145
149
|
return { ...r, plano: { id: c.plano, restam, resolucao: c.recursos.resolucao, marcaDagua: !!(a.mudou && c.recursos.marcaDagua) },
|
|
146
|
-
...(
|
|
147
|
-
|
|
150
|
+
...((() => {
|
|
151
|
+
const a = [...(c.offline ? ['sem rede: modo local, sem cota nem marca d\'água.'] : []),
|
|
152
|
+
...(!c.offline && restam === 0 ? ['Foi o seu último vídeo do mês. Veja os planos com: primocode planos'] : []), ...extra];
|
|
153
|
+
return a.length ? { aviso: a.join(' ') } : {};
|
|
154
|
+
})()) };
|
|
148
155
|
}
|
|
149
156
|
|
|
150
157
|
module.exports = { LOCAL, consultar, mensagemLimite, pngMarca, filtros, aplicar, comCota };
|
package/lib/estudio/editor.js
CHANGED
|
@@ -249,6 +249,11 @@ async function editar(args = {}, { pasta, deps = {} } = {}) {
|
|
|
249
249
|
return { ok: true, versao: v.n, video, versoes: lista(), nota: `voltou para a v${v.n}; a próxima mudança vira a v${ix.versoes.reduce((m, x) => Math.max(m, x.n), 0) + 1}.` };
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
// O plano limita as edições guardadas por vídeo (grátis 3). Prévia e a versão original não contam.
|
|
253
|
+
const gastas = indice(pasta).versoes.filter((v) => !v.previa && v.resumo !== 'original').length;
|
|
254
|
+
const lim = await (deps.cabeMais ? deps.cabeMais(gastas) : require('../limites.js').cabeMais('versoesEditor', gastas));
|
|
255
|
+
if (!lim.ok && !args.previa) return { ok: false, error: lim.error + ' Dá para voltar a uma versão (voltar_para) ou ver as prévias.' };
|
|
256
|
+
|
|
252
257
|
const spec = ler(ARQ(pasta));
|
|
253
258
|
const mudancas = Array.isArray(args.mudancas) ? args.mudancas : [];
|
|
254
259
|
if (!mudancas.length && !args.final) return { ok: false, error: 'mudancas está vazio (ou use final:true para renderizar a prévia em qualidade final).' };
|
package/lib/estudio/index.js
CHANGED
|
@@ -508,7 +508,7 @@ async function ferramentaMidia(args) {
|
|
|
508
508
|
if (args.id && !p) return semProjeto(args.id);
|
|
509
509
|
|
|
510
510
|
if (acao === 'projetos') return require('./projetos.js').procurar({ termo: args.termo, categoria: args.categoria, limite: args.limite });
|
|
511
|
-
if (acao === 'projeto') return require('./projetos.js').ver(args.nome || args.id_item || args.termo);
|
|
511
|
+
if (acao === 'projeto') return require('./projetos.js').ver(args.nome || args.id_item || args.termo, { c: await require('../limites.js').situacao() });
|
|
512
512
|
|
|
513
513
|
if (acao === 'fontes') {
|
|
514
514
|
if (args.conferir) return assets.conferirFonte(args.conferir);
|
|
@@ -712,8 +712,10 @@ async function ferramentaMotion(args) {
|
|
|
712
712
|
if (!args.id || !p) return semProjeto(args.id);
|
|
713
713
|
let receita = null;
|
|
714
714
|
if (args.receita) {
|
|
715
|
-
const r = require('./projetos.js').ver(args.receita);
|
|
715
|
+
const r = require('./projetos.js').ver(args.receita, { c: await require('../limites.js').situacao() });
|
|
716
716
|
if (!r.ok) return r;
|
|
717
|
+
// Grátis vê o resumo da receita, não monta a partir dela: diz o porquê em vez de renderizar pela metade.
|
|
718
|
+
if (r.bloqueado) return { ok: false, error: r.aviso, projeto: r.projeto };
|
|
717
719
|
receita = r.projeto;
|
|
718
720
|
}
|
|
719
721
|
return require('./cota.js').comCota(() => renderMotion(args, p, receita));
|
|
@@ -1204,6 +1206,11 @@ function vazamentoNoComeco(lista, palavras, direcao = '') {
|
|
|
1204
1206
|
}
|
|
1205
1207
|
|
|
1206
1208
|
async function narrarComGemini(lista, mp3, { voz, direcao } = {}) {
|
|
1209
|
+
/* A narração premium é do Pro em diante (lib/limites.js). Sem ela a corrente
|
|
1210
|
+
segue para a voz local (Kokoro/Edge) e o aviso explica o porquê. */
|
|
1211
|
+
const lim = require('../limites.js');
|
|
1212
|
+
const prem = await lim.exigir('narracaoPremium');
|
|
1213
|
+
if (!prem.ok) { lim.avisar(prem.error + ' A narração saiu com a voz local.'); return { ok: false, error: prem.error }; }
|
|
1207
1214
|
try {
|
|
1208
1215
|
const cfg = require('../config').loadConfig();
|
|
1209
1216
|
const api = require('../api');
|
package/lib/estudio/projetos.js
CHANGED
|
@@ -47,11 +47,23 @@ function procurar({ termo, categoria, limite = 10 } = {}) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
/** A receita inteira, pelo id (ou, na falta, o melhor palpite do texto). */
|
|
50
|
-
function ver(id) {
|
|
50
|
+
function ver(id, { c } = {}) {
|
|
51
51
|
const q = sem(id).trim();
|
|
52
52
|
if (!q) return { ok: false, error: 'diga o id da receita (acao:"projetos" lista).' };
|
|
53
53
|
const p = todos().find((x) => x.id === q) || procurar({ termo: q, limite: 1 }).projetos.map((x) => todos().find((y) => y.id === x.id))[0];
|
|
54
54
|
if (!p) return { ok: false, error: `não há receita "${id}".`, ids: todos().map((x) => x.id) };
|
|
55
|
+
// O plano decide (lib/limites.js): grátis vê o resumo; sem scriptsAE a receita vem sem os scripts.
|
|
56
|
+
if (c && c.recursos) {
|
|
57
|
+
const lim = require('../limites.js');
|
|
58
|
+
if (!c.recursos.clubeCompleto) {
|
|
59
|
+
const { id: i, titulo, categoria, resumo, indicado, estilo, ficha } = p;
|
|
60
|
+
return { ok: true, bloqueado: true, projeto: { id: i, titulo, categoria, resumo, indicado, estilo, ficha }, aviso: lim.bloqueio('clubeCompleto', c) };
|
|
61
|
+
}
|
|
62
|
+
if (p.scripts_ae && !c.recursos.scriptsAE) {
|
|
63
|
+
const { scripts_ae: _, ...resto } = p;
|
|
64
|
+
return { ok: true, projeto: resto, scripts_ae_bloqueado: lim.bloqueio('scriptsAE', c) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
55
67
|
// Os scripts do After Effects ficam em disco: a resposta leva o caminho, e o agente lê só o que precisar.
|
|
56
68
|
if (p.scripts_ae) p.scripts_ae = { ...p.scripts_ae, caminho: path.join(__dirname, 'dados', 'projetos-ae', p.id.replace(/^clube-/, '')) };
|
|
57
69
|
return { ok: true, projeto: p };
|
package/lib/limites.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* limites.js — o que o plano libera, aplicado de verdade na CLI.
|
|
3
|
+
*
|
|
4
|
+
* A tabela mora em server/lib/planos.js; o servidor responde o plano da pessoa
|
|
5
|
+
* em POST /api/video/cota (recursos + o catálogo enxuto). Aqui ficam:
|
|
6
|
+
* - situacao(): o plano agora, guardado 5 min na memória do processo;
|
|
7
|
+
* - o MODO LOCAL generoso (sem rede, servidor mudo ou campo que o servidor
|
|
8
|
+
* ainda não manda): nada é barrado. Errar para o lado de quem trabalha;
|
|
9
|
+
* - bloqueio(): a mensagem curta e honesta — o quê, em que plano você está,
|
|
10
|
+
* qual plano libera e o comando para assinar. Nunca barra sem dizer o porquê.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const GENEROSO = { videosMes: 0, resolucao: 2160, marcaDagua: false, narracaoPremium: true, clubeCompleto: true,
|
|
16
|
+
scriptsAE: true, prioridade: 0, subagentes: 7, kitsMarca: 0, versoesEditor: 0 };
|
|
17
|
+
const MAX_PARALELO = 8;
|
|
18
|
+
const VALIDADE = 5 * 60000;
|
|
19
|
+
|
|
20
|
+
let cache = null;
|
|
21
|
+
const avisos = [];
|
|
22
|
+
|
|
23
|
+
/** Completa o que o servidor não mandou com o generoso (campo novo, servidor velho). */
|
|
24
|
+
function completar(c) {
|
|
25
|
+
const r = { ...GENEROSO };
|
|
26
|
+
for (const [k, v] of Object.entries((c && c.recursos) || {})) if (v !== undefined && v !== null) r[k] = v;
|
|
27
|
+
return { ...(c || {}), recursos: r };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function guardar(c) { cache = { em: Date.now(), c: completar(c) }; }
|
|
31
|
+
function esquecer() { cache = null; }
|
|
32
|
+
|
|
33
|
+
/** Nos testes (node test/x.test.js) ninguém bate no servidor de verdade: vale o modo local. */
|
|
34
|
+
const emTeste = () => /\.test\.m?js$/.test(String(process.argv[1] || '')) || process.env.PRIMOCODE_LIMITES === 'local';
|
|
35
|
+
|
|
36
|
+
/** O plano agora: {plano, recursos, planos, offline}. Nunca lança. */
|
|
37
|
+
async function situacao(deps = {}) {
|
|
38
|
+
if (cache && Date.now() - cache.em < VALIDADE) return cache.c;
|
|
39
|
+
if (emTeste() && !deps.consultar) return completar(require('./estudio/cota.js').LOCAL);
|
|
40
|
+
let c;
|
|
41
|
+
try { c = await (deps.consultar || require('./estudio/cota.js').consultar)('checar'); } catch { c = require('./estudio/cota.js').LOCAL; }
|
|
42
|
+
guardar(c);
|
|
43
|
+
return cache.c;
|
|
44
|
+
}
|
|
45
|
+
const recursos = async (deps) => (await situacao(deps)).recursos;
|
|
46
|
+
|
|
47
|
+
const reais = (n) => `R$ ${Number(n).toFixed(2).replace('.', ',')}`;
|
|
48
|
+
|
|
49
|
+
/* Cada recurso: o que o plano ganha ao passar, e o texto de quando bate. */
|
|
50
|
+
const RECURSOS = {
|
|
51
|
+
narracaoPremium: { de: (r) => !!r.narracaoPremium, texto: 'A narração premium (voz do Gemini)', libera: 'a narração premium' },
|
|
52
|
+
clubeCompleto: { de: (r) => !!r.clubeCompleto, texto: 'A receita completa do Clube (etapas e prompt)', libera: 'as receitas completas do Clube' },
|
|
53
|
+
scriptsAE: { de: (r) => !!r.scriptsAE, texto: 'Os scripts de After Effects modelo', libera: 'os scripts de After Effects modelo' },
|
|
54
|
+
kitsMarca: { n: (r) => r.kitsMarca, texto: (r) => `Você já tem ${r.kitsMarca} kit${r.kitsMarca > 1 ? 's' : ''} de marca, o limite do plano`, libera: (r) => (r.kitsMarca ? `${r.kitsMarca} kits de marca` : 'kits de marca sem teto') },
|
|
55
|
+
versoesEditor: { n: (r) => r.versoesEditor, texto: (r) => `Este vídeo já tem ${r.versoesEditor} edições, o limite do plano`, libera: (r) => (r.versoesEditor ? `${r.versoesEditor} edições por vídeo` : 'edições sem teto') },
|
|
56
|
+
subagentes: { n: (r) => r.subagentes + 1, texto: (r) => `Seu plano roda ${r.subagentes + 1} subagente${r.subagentes ? 's juntos' : ' por vez'}`, libera: (r) => (r.subagentes ? `${r.subagentes + 1} subagentes juntos` : 'subagentes em paralelo') },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** O primeiro plano acima do atual que libera `chave` (ou null). */
|
|
60
|
+
function quemLibera(chave, c) {
|
|
61
|
+
const def = RECURSOS[chave], lista = c.planos || [];
|
|
62
|
+
const i = lista.findIndex((p) => p.id === c.plano);
|
|
63
|
+
return lista.slice(i + 1).find((p) => (def.de ? def.de(p.recursos) : (def.n(p.recursos) === 0 || def.n(p.recursos) > def.n(c.recursos)))) || null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A mensagem de limite: curta, com o motivo, o degrau que libera e o comando. */
|
|
67
|
+
function bloqueio(chave, c) {
|
|
68
|
+
const def = RECURSOS[chave], r = c.recursos;
|
|
69
|
+
const nome = ((c.planos || []).find((p) => p.id === c.plano) || {}).nome || c.plano;
|
|
70
|
+
const p = quemLibera(chave, c);
|
|
71
|
+
const texto = typeof def.texto === 'function' ? def.texto(r) : `${def.texto} não vêm no plano ${nome}`;
|
|
72
|
+
const libera = typeof def.libera === 'function' ? def.libera(p ? p.recursos : r) : def.libera;
|
|
73
|
+
return `${texto} (plano ${nome}).`
|
|
74
|
+
+ (p ? ` O ${p.nome} (${reais(p.preco)}/mês) libera ${libera}.` : '')
|
|
75
|
+
+ ' Veja e assine: primocode planos (ou primocode.web.app/planos).';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Libera o que é só ligado/desligado. {ok:true} ou {ok:false, error}. */
|
|
79
|
+
async function exigir(chave, deps) {
|
|
80
|
+
const c = await situacao(deps);
|
|
81
|
+
return RECURSOS[chave].de(c.recursos) ? { ok: true } : { ok: false, error: bloqueio(chave, c) };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Limite numérico: `usados` já é o que existe; passa se o teto é 0 (sem teto) ou se ainda cabe mais um. */
|
|
85
|
+
async function cabeMais(chave, usados, deps) {
|
|
86
|
+
const c = await situacao(deps), teto = RECURSOS[chave].n(c.recursos);
|
|
87
|
+
return !teto || usados < teto ? { ok: true } : { ok: false, error: bloqueio(chave, c) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Quantos subagentes rodam juntos na mesma rodada (o principal fica de fora). */
|
|
91
|
+
async function paralelo(deps) {
|
|
92
|
+
const c = await situacao(deps);
|
|
93
|
+
return { max: Math.min(MAX_PARALELO, c.recursos.subagentes + 1), aviso: (n) => (n > c.recursos.subagentes + 1 ? bloqueio('subagentes', c) : null) };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Avisos que o resultado da ferramenta leva ao fim da vez (ex.: narração premium trocada pela local). */
|
|
97
|
+
function avisar(msg) { if (msg && !avisos.includes(msg)) avisos.push(msg); }
|
|
98
|
+
function pegarAvisos() { return avisos.splice(0); }
|
|
99
|
+
|
|
100
|
+
module.exports = { GENEROSO, MAX_PARALELO, situacao, recursos, guardar, esquecer, completar, bloqueio, quemLibera, exigir, cabeMais, paralelo, avisar, pegarAvisos, RECURSOS };
|
package/lib/marca-kit.js
CHANGED
|
@@ -280,6 +280,9 @@ async function main(argv, { perguntar, dizer = console.log, pasta = process.cwd(
|
|
|
280
280
|
const aqui = resto.includes('aqui');
|
|
281
281
|
switch (op) {
|
|
282
282
|
case 'criar': {
|
|
283
|
+
// O plano limita quantos kits se guarda (grátis 1): diz o porquê antes de perguntar qualquer coisa.
|
|
284
|
+
const lim = await (deps && deps.cabeMais ? deps.cabeMais(listar().length) : require('./limites.js').cabeMais('kitsMarca', listar().length));
|
|
285
|
+
if (!lim.ok) { dizer(lim.error); return 1; }
|
|
283
286
|
const t = perguntar ? null : perguntarNoTerminal();
|
|
284
287
|
try {
|
|
285
288
|
const r = await criar({ perguntar: perguntar || t.ask, dizer, deps });
|
package/lib/planos.js
CHANGED
|
@@ -15,14 +15,14 @@
|
|
|
15
15
|
const https = require('https');
|
|
16
16
|
const http = require('http');
|
|
17
17
|
|
|
18
|
-
const R = (videosMes, resolucao, marcaDagua, narracaoPremium, clubeCompleto, scriptsAE, prioridade, subagentes) =>
|
|
19
|
-
({ videosMes, resolucao, marcaDagua, narracaoPremium, clubeCompleto, scriptsAE, prioridade, subagentes });
|
|
20
|
-
const GRATIS = { id: 'free', nome: 'Grátis', preco: 0, recursos: R(3, 720, true, false, false, false, 0, 0), inclui: [] };
|
|
18
|
+
const R = (videosMes, resolucao, marcaDagua, narracaoPremium, clubeCompleto, scriptsAE, prioridade, subagentes, kitsMarca, versoesEditor) =>
|
|
19
|
+
({ videosMes, resolucao, marcaDagua, narracaoPremium, clubeCompleto, scriptsAE, prioridade, subagentes, kitsMarca, versoesEditor });
|
|
20
|
+
const GRATIS = { id: 'free', nome: 'Grátis', preco: 0, recursos: R(3, 720, true, false, false, false, 0, 0, 1, 3), inclui: [] };
|
|
21
21
|
const RESERVA = [
|
|
22
|
-
{ id: 'primo', nome: 'Primo', preco: 69.9, recursos: R(15, 720, false, false, true, false, 0, 0), inclui: [] },
|
|
23
|
-
{ id: 'pro', nome: 'Primo Pro', preco: 129.9, recursos: R(60, 1080, false, true, true, true, 1, 1), inclui: [] },
|
|
24
|
-
{ id: 'max', nome: 'Primo Max', preco: 299.9, recursos: R(200, 2160, false, true, true, true, 2, 3), inclui: [] },
|
|
25
|
-
{ id: 'studio', nome: 'Primo Studio', preco: 599.9, recursos: R(800, 2160, false, true, true, true, 3, 8), inclui: [] },
|
|
22
|
+
{ id: 'primo', nome: 'Primo', preco: 69.9, recursos: R(15, 720, false, false, true, false, 0, 0, 5, 10), inclui: [] },
|
|
23
|
+
{ id: 'pro', nome: 'Primo Pro', preco: 129.9, recursos: R(60, 1080, false, true, true, true, 1, 1, 20, 30), inclui: [] },
|
|
24
|
+
{ id: 'max', nome: 'Primo Max', preco: 299.9, recursos: R(200, 2160, false, true, true, true, 2, 3, 100, 0), inclui: [] },
|
|
25
|
+
{ id: 'studio', nome: 'Primo Studio', preco: 599.9, recursos: R(800, 2160, false, true, true, true, 3, 8, 0, 0), inclui: [] },
|
|
26
26
|
];
|
|
27
27
|
|
|
28
28
|
function buscarPlanos(servidor, { timeout = 8000 } = {}) {
|
|
@@ -60,7 +60,9 @@ const LINHAS = [
|
|
|
60
60
|
['Projetos do Clube', (r) => (r.clubeCompleto ? 'completos' : 'resumo')],
|
|
61
61
|
['Scripts AE modelo', (r) => (r.scriptsAE ? '✓' : '—')],
|
|
62
62
|
['Prioridade na cadeia', (r) => (r.prioridade ? '●'.repeat(r.prioridade) : '—')],
|
|
63
|
-
['Subagentes', (r) => (r.subagentes ? String(r.subagentes) : '
|
|
63
|
+
['Subagentes juntos', (r) => (r.subagentes ? String(r.subagentes) : 'em fila')],
|
|
64
|
+
['Kits de marca', (r) => (r.kitsMarca == null || r.kitsMarca === 0 ? 'sem teto' : String(r.kitsMarca))],
|
|
65
|
+
['Edições por vídeo', (r) => (r.versoesEditor == null || r.versoesEditor === 0 ? 'sem teto' : String(r.versoesEditor))],
|
|
64
66
|
];
|
|
65
67
|
|
|
66
68
|
/**
|
package/lib/preparar.js
CHANGED
|
@@ -70,6 +70,8 @@ function estudioLinhas(dep = {}) {
|
|
|
70
70
|
// marca ativa
|
|
71
71
|
const mk = tenta(() => require('./marca-kit.js').ativa([process.cwd()]), null);
|
|
72
72
|
poe(null, 'Marca ativa', mk ? (mk.nome || mk.slug || 'sim') : 'nenhuma — primocode marca criar <nome>');
|
|
73
|
+
// Primo (modo voz)
|
|
74
|
+
tenta(() => linhas.push(...require('./voz.js').linhasDoutor()), null);
|
|
73
75
|
// MCP
|
|
74
76
|
const mcp = tenta(() => Object.keys(require('./mcp.js').servidores()), []);
|
|
75
77
|
poe(null, 'MCP', mcp.length ? mcp.join(', ') : 'nenhum servidor');
|
package/lib/voz-ponte.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* voz-ponte.js — o aviso do agente para o modo voz, sem um depender do outro.
|
|
3
|
+
*
|
|
4
|
+
* O agente (lib/act.js) diz "comecei a ferramenta X" aqui; o /voice, se estiver
|
|
5
|
+
* ligado, se inscreve e repassa ao Primo (painel mostra "executando" e ele
|
|
6
|
+
* confirma em voz alta: "rodando os testes"). Sem /voice, ninguém escuta.
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
let ouvinte = null;
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
escutar: (fn) => { ouvinte = typeof fn === 'function' ? fn : null; },
|
|
14
|
+
ferramenta: (nome, args) => { if (ouvinte) { try { ouvinte(String(nome || ''), args || {}); } catch { /* o aviso nunca derruba o agente */ } } },
|
|
15
|
+
};
|
package/lib/voz.js
CHANGED
|
@@ -63,8 +63,19 @@ function paraFalar(texto, { frases = 3, teto = 420 } = {}) {
|
|
|
63
63
|
return (fala || t.slice(0, teto)).trim();
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
/* O VENV DO PRIMO. As dependências opcionais (VAD, painel em tela cheia) moram
|
|
67
|
+
* em ~/.primocode/voz-venv, instaladas sob demanda: ninguém precisa quebrar o
|
|
68
|
+
* Python do sistema (PEP 668) nem perde nada se não instalar. */
|
|
69
|
+
const VENV = path.join(os.homedir(), '.primocode', 'voz-venv');
|
|
70
|
+
const venvPython = () => path.join(VENV, process.platform === 'win32' ? 'Scripts' : 'bin', process.platform === 'win32' ? 'python.exe' : 'python');
|
|
71
|
+
|
|
72
|
+
/** Onde está o Python 3. O do venv do Primo ganha, se já existir. */
|
|
67
73
|
function python() {
|
|
74
|
+
if (fs.existsSync(venvPython())) return venvPython();
|
|
75
|
+
return pythonDoSistema();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function pythonDoSistema() {
|
|
68
79
|
const nomes = process.platform === 'win32'
|
|
69
80
|
? ['python', 'py', 'python3']
|
|
70
81
|
: ['python3', 'python', '/opt/homebrew/bin/python3', '/usr/local/bin/python3', '/usr/bin/python3'];
|
|
@@ -223,7 +234,7 @@ function instalar(py, pacotes, aoFalar = () => {}) {
|
|
|
223
234
|
* @param {()=>void} [op.aoPronto] o microfone abriu
|
|
224
235
|
* @returns {{processo, pronto: Promise, falar:(t:string)=>void, calar:()=>void}}
|
|
225
236
|
*/
|
|
226
|
-
function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {}, aoPronto = () => {} }) {
|
|
237
|
+
function abrir({ python: py, servidor, token, voz, aoParar = () => {}, aoOuvir, mostrar = () => {}, aoPronto = () => {} }) {
|
|
227
238
|
let p;
|
|
228
239
|
const manda = (obj) => {
|
|
229
240
|
try { p.stdin.write(JSON.stringify(obj) + '\n'); } catch { /* já fechou */ }
|
|
@@ -237,6 +248,7 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
|
|
|
237
248
|
PRIMOCODE_SERVER: servidor,
|
|
238
249
|
PRIMOCODE_TOKEN: token || '',
|
|
239
250
|
PRIMOCODE_VOZ: voz || 'antonio',
|
|
251
|
+
PRIMOCODE_CWD: process.cwd(),
|
|
240
252
|
PYTHONUNBUFFERED: '1',
|
|
241
253
|
// O Whisper deixa um semáforo para o sistema recolher na saída,
|
|
242
254
|
// e o Python avisa disso no stderr. É inofensivo; o aviso não é.
|
|
@@ -254,6 +266,7 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
|
|
|
254
266
|
try { msg = JSON.parse(linha); } catch { continue; }
|
|
255
267
|
if (msg.tipo === 'fim') { try { p.kill(); } catch {} return; }
|
|
256
268
|
if (msg.tipo === 'pronto') { try { aoPronto(); } catch {} continue; }
|
|
269
|
+
if (msg.tipo === 'parar') { try { aoParar(); } catch {} continue; }
|
|
257
270
|
if (msg.tipo === 'erro') { mostrar(String(msg.texto || ''), true); continue; }
|
|
258
271
|
if (msg.tipo === 'ouvi' && String(msg.texto || '').trim()) {
|
|
259
272
|
try { aoOuvir(String(msg.texto).trim()); } catch (e) { mostrar(e.message, true); }
|
|
@@ -276,7 +289,12 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
|
|
|
276
289
|
return {
|
|
277
290
|
processo: p,
|
|
278
291
|
pronto,
|
|
279
|
-
|
|
292
|
+
/* O texto vai BRUTO: quem resume é o Primo (voz/primo_voz/conversa.py) —
|
|
293
|
+
a voz diz o resumo, o painel mostra o resto (código, listas). */
|
|
294
|
+
falar: (texto) => { const t = String(texto || '').trim(); if (t) manda({ tipo: 'falar', texto: t, bruto: true }); },
|
|
295
|
+
// O agente começou uma ferramenta: o painel mostra "executando" e o Primo confirma ("rodando os testes").
|
|
296
|
+
ferramenta: (nome, args) => manda({ tipo: 'ferramenta', nome, args: args || {} }),
|
|
297
|
+
painel: (acao) => manda({ tipo: 'painel', acao }),
|
|
280
298
|
calar: () => manda({ tipo: 'calar' }),
|
|
281
299
|
// A bolinha na tela: 'esperando' quando o terminal termina sem falar.
|
|
282
300
|
estado: (e) => manda({ tipo: 'estado', estado: e }),
|
|
@@ -303,7 +321,10 @@ function abrir({ python: py, servidor, token, voz, aoOuvir, mostrar = () => {},
|
|
|
303
321
|
*/
|
|
304
322
|
const OPCIONAIS = () => [
|
|
305
323
|
['faster_whisper', 'faster-whisper', 'a transcrição local (Whisper)'],
|
|
324
|
+
['pysilero_vad', 'pysilero-vad', 'o fim de fala preciso (Silero VAD)'],
|
|
306
325
|
...(process.platform === 'darwin' ? [
|
|
326
|
+
['WebKit', 'pyobjc-framework-WebKit', 'o painel em tela cheia do Primo'],
|
|
327
|
+
['EventKit', 'pyobjc-framework-EventKit', 'os compromissos de hoje do app Calendário'],
|
|
307
328
|
['Quartz', 'pyobjc-framework-Quartz', 'o cursor do Primo na tela'],
|
|
308
329
|
['Vision', 'pyobjc-framework-Vision', 'ler o texto da tela (clicar pelo nome em qualquer app)'],
|
|
309
330
|
] : []),
|
|
@@ -394,13 +415,23 @@ async function baixarWhisper(py) {
|
|
|
394
415
|
* @returns {Promise<{ok:boolean, python?:string, esperando?:boolean,
|
|
395
416
|
* avisos:string[], error?:string, comoResolver?:string}>}
|
|
396
417
|
*/
|
|
418
|
+
async function criarVenv(aoFalar) {
|
|
419
|
+
if (fs.existsSync(venvPython())) return null;
|
|
420
|
+
const base = pythonDoSistema();
|
|
421
|
+
if (!base) return null;
|
|
422
|
+
aoFalar('criando o ambiente do Primo (~/.primocode/voz-venv)');
|
|
423
|
+
const r = await rodar(base, ['-m', 'venv', VENV], 5 * 60 * 1000);
|
|
424
|
+
return r.ok ? null : `não consegui criar o ambiente do Primo (${porqueOPipFalhou(r.saida)}) — uso o Python do sistema.`;
|
|
425
|
+
}
|
|
426
|
+
|
|
397
427
|
async function preparar(aoFalar = () => {}) {
|
|
428
|
+
const avisoVenv = await criarVenv(aoFalar);
|
|
398
429
|
const py = python();
|
|
399
430
|
if (!py) {
|
|
400
431
|
return { ok: false, avisos: [], error: 'o modo voz precisa do Python 3, e não achei nenhum nesta máquina.',
|
|
401
432
|
comoResolver: process.platform === 'darwin' ? 'brew install python' : 'instale em python.org e abra o terminal de novo' };
|
|
402
433
|
}
|
|
403
|
-
const avisos = [];
|
|
434
|
+
const avisos = avisoVenv ? [avisoVenv] : [];
|
|
404
435
|
|
|
405
436
|
// 1. sistema, depois 2. os obrigatórios pelo pip.
|
|
406
437
|
let falta = faltando(py);
|
|
@@ -435,4 +466,43 @@ async function preparar(aoFalar = () => {}) {
|
|
|
435
466
|
return { ok: true, python: py, avisos };
|
|
436
467
|
}
|
|
437
468
|
|
|
438
|
-
|
|
469
|
+
/** `/voice painel`: só o HUD (sem microfone). Mac com PyObjC = tela cheia; senão, o quadro no terminal. */
|
|
470
|
+
function abrirPainel({ python: py }) {
|
|
471
|
+
const mac = process.platform === 'darwin' && temModulo(py, 'WebKit');
|
|
472
|
+
const p = spawn(py, ['-m', mac ? 'primo_voz.hud' : 'primo_voz.painel_tui', ...(mac ? ['--livre'] : [])], {
|
|
473
|
+
cwd: PASTA, env: { ...process.env, PYTHONPATH: PASTA }, stdio: mac ? 'ignore' : ['pipe', 'inherit', 'inherit'],
|
|
474
|
+
});
|
|
475
|
+
return { processo: p, tela: mac };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Grava o nome do assistente em ~/.primocode/voz.json (o mesmo arquivo que voz/primo_voz/identidade.py lê). */
|
|
479
|
+
function definirNome(nome) {
|
|
480
|
+
const limpo = String(nome || '').replace(/[^\p{L}\p{N} -]/gu, '').trim();
|
|
481
|
+
if (!limpo || limpo.split(/\s+/).length > 2 || limpo.length < 2 || limpo.length > 20) return null;
|
|
482
|
+
const arq = process.env.PRIMOCODE_VOZ_CONFIG || path.join(os.homedir(), '.primocode', 'voz.json');
|
|
483
|
+
let cfg = {};
|
|
484
|
+
try { cfg = JSON.parse(fs.readFileSync(arq, 'utf8')); } catch { /* primeira vez */ }
|
|
485
|
+
cfg.nome = limpo.split(/\s+/).map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
|
|
486
|
+
fs.mkdirSync(path.dirname(arq), { recursive: true });
|
|
487
|
+
fs.writeFileSync(arq, JSON.stringify(cfg, null, 2));
|
|
488
|
+
return cfg.nome;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** As linhas do `primocode doutor` sobre o modo voz. */
|
|
492
|
+
function linhasDoutor() {
|
|
493
|
+
const py = python();
|
|
494
|
+
if (!py) return [{ ok: false, item: 'Primo (voz)', detalhe: 'sem Python 3', conserto: 'brew install python' }];
|
|
495
|
+
const tem = (m) => temModulo(py, m);
|
|
496
|
+
const venv = fs.existsSync(venvPython());
|
|
497
|
+
const linha = (ok, item, detalhe, conserto) => ({ ok, item, detalhe, conserto: ok === false ? conserto : undefined });
|
|
498
|
+
return [
|
|
499
|
+
linha(venv ? true : null, 'Primo: ambiente', venv ? VENV : 'ainda não criado — /voice cria sozinho', undefined),
|
|
500
|
+
linha(tem('sounddevice') && tem('numpy'), 'Primo: microfone', 'sounddevice + numpy', '/voice instala; ou: ' + py + ' -m pip install sounddevice numpy'),
|
|
501
|
+
linha(tem('faster_whisper') ? true : null, 'Primo: Whisper local', tem('faster_whisper') ? 'instalado' : 'não instalado — usa o servidor'),
|
|
502
|
+
linha(tem('pysilero_vad') ? true : null, 'Primo: Silero VAD', tem('pysilero_vad') ? 'fim de fala preciso' : 'não instalado — usa o corte por volume'),
|
|
503
|
+
linha(process.platform === 'darwin' ? (tem('WebKit') ? true : null) : null, 'Primo: tela cheia',
|
|
504
|
+
process.platform !== 'darwin' ? 'só no Mac — aqui, painel no terminal' : tem('WebKit') ? 'pronto' : 'sem PyObjC WebKit — usa a orbe'),
|
|
505
|
+
];
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
module.exports = { definirNome, VENV, abrirPainel, linhasDoutor, DICA_COMPUTADOR, paraFalar, conferir, instalar, preparar, porqueOPipFalhou, abrir, python, PASTA };
|