primocode 8.11.1 → 8.12.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/catalogo.js +3 -1
- package/lib/studio.js +115 -7
- package/lib/tools.js +2 -0
- package/lib/ui.js +1 -0
- package/package.json +2 -2
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/ferramentas/00-protocolo.md +15 -2
- package/studio/ferramentas/01-modelos.md +2 -0
- package/studio/ferramentas/corte.md +18 -2
- package/studio/server.py +314 -23
- package/studio/voz.py +307 -0
- package/studio/web/js/narracao.js +18 -2
- package/studio/web/js/voz-neural.js +88 -178
package/lib/catalogo.js
CHANGED
|
@@ -83,7 +83,9 @@ const GRUPOS = [
|
|
|
83
83
|
args: 'ferramenta: prisma | tela | prosa | grade | corte | traco. titulo. doc: objeto no formato que studio_spec daquela ferramenta descreve.' },
|
|
84
84
|
{ tool: 'studio_ler', escreve: false, faz: 'lê algo que já foi criado', args: 'id.' },
|
|
85
85
|
{ tool: 'studio_atualizar', escreve: false, faz: 'ajusta o que já existe, sem refazer do zero',
|
|
86
|
-
args: 'id. titulo (opcional). doc (opcional).' },
|
|
86
|
+
args: 'id. titulo (opcional). doc (opcional): só as chaves que MUDAM — o resto do documento fica intacto.' },
|
|
87
|
+
{ tool: 'studio_versoes', escreve: false, faz: 'histórico de um arquivo, e volta a uma versão anterior',
|
|
88
|
+
args: 'id. restaurar (opcional): "anterior" desfaz a última mudança, ou o nome de uma versão da lista.' },
|
|
87
89
|
{ tool: 'studio_listar', escreve: false, faz: 'lista o que já existe no estúdio', args: null },
|
|
88
90
|
{ tool: 'studio_midia', escreve: false, faz: 'manda imagem, música ou vídeo seu para usar nas peças',
|
|
89
91
|
args: 'arquivo (opcional): caminho no projeto — sem ele, lista a galeria. nome (opcional).' },
|
package/lib/studio.js
CHANGED
|
@@ -39,7 +39,10 @@ const BASE = `http://127.0.0.1:${PORTA}`;
|
|
|
39
39
|
|
|
40
40
|
// Só o código do Studio é atualizado; `arquivos/` e `midia/` (o que o usuário
|
|
41
41
|
// criou) nunca são tocados.
|
|
42
|
-
|
|
42
|
+
// voz.py é módulo do servidor, não enfeite: sem ele o server.py não importa
|
|
43
|
+
// e o Studio inteiro deixa de subir. Todo arquivo .py do Studio precisa
|
|
44
|
+
// entrar aqui.
|
|
45
|
+
const COPIAVEIS = new Set(['server.py', 'voz.py', 'web', 'ferramentas', 'README.md', 'PRIMOCODE.md']);
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
48
|
* Onde está o Python 3 desta máquina.
|
|
@@ -112,13 +115,36 @@ function pedir(metodo, rota, corpo, timeout = 30000) {
|
|
|
112
115
|
path: rota,
|
|
113
116
|
method: metodo,
|
|
114
117
|
timeout,
|
|
115
|
-
|
|
118
|
+
// Este cabeçalho diz ao Studio que quem fala é a IA, não o editor
|
|
119
|
+
// do navegador. O servidor valida o documento com rigor só neste
|
|
120
|
+
// caso: erro de formato tem de voltar para o agente como lição, e
|
|
121
|
+
// não pode fazer o salvamento de uma pessoa que está digitando
|
|
122
|
+
// falhar no meio da edição.
|
|
123
|
+
headers: Object.assign({ 'X-Primo-Agente': '1' },
|
|
124
|
+
dados ? { 'Content-Type': 'application/json', 'Content-Length': dados.length } : {}),
|
|
116
125
|
}, (res) => {
|
|
117
126
|
let texto = '';
|
|
118
127
|
res.setEncoding('utf8');
|
|
119
128
|
res.on('data', (c) => { texto += c; });
|
|
120
129
|
res.on('end', () => {
|
|
121
|
-
if (res.statusCode >= 400)
|
|
130
|
+
if (res.statusCode >= 400) {
|
|
131
|
+
// A mensagem do servidor é escrita PARA o agente ("tema X
|
|
132
|
+
// não existe, os temas são..."). Envolver em "HTTP 400:
|
|
133
|
+
// {json}" só afasta a lição do olho dele.
|
|
134
|
+
let motivo = texto.slice(0, 400);
|
|
135
|
+
let corpoErro = null;
|
|
136
|
+
try {
|
|
137
|
+
corpoErro = JSON.parse(texto);
|
|
138
|
+
motivo = corpoErro.erro || motivo;
|
|
139
|
+
} catch { /* texto cru serve */ }
|
|
140
|
+
const erro = new Error(motivo);
|
|
141
|
+
erro.corpo = corpoErro;
|
|
142
|
+
// O código vem no objeto, e não mais embutido no texto:
|
|
143
|
+
// quem trata 404 (comIdReal) precisa saber que é 404 sem
|
|
144
|
+
// depender de a mensagem começar com "HTTP 404".
|
|
145
|
+
erro.status = res.statusCode;
|
|
146
|
+
return reject(erro);
|
|
147
|
+
}
|
|
122
148
|
try { resolve(JSON.parse(texto)); }
|
|
123
149
|
catch { resolve({ texto }); }
|
|
124
150
|
});
|
|
@@ -264,7 +290,7 @@ async function comIdReal(id, fn) {
|
|
|
264
290
|
try {
|
|
265
291
|
return await fn();
|
|
266
292
|
} catch (e) {
|
|
267
|
-
if (
|
|
293
|
+
if (!(e && e.status === 404)) throw e;
|
|
268
294
|
let existem = [];
|
|
269
295
|
try { existem = (await pedir('GET', '/api/arquivos')).map((a) => a.id).slice(0, 8); } catch {}
|
|
270
296
|
return {
|
|
@@ -277,20 +303,102 @@ async function comIdReal(id, fn) {
|
|
|
277
303
|
|
|
278
304
|
const ler = (id) => comStudio(() => comIdReal(id, async () => ({ ok: true, arquivo: await pedir('GET', `/api/arquivos/${encodeURIComponent(id)}`) })));
|
|
279
305
|
|
|
306
|
+
// Quando o documento tem narração, o servidor começa a baixar a voz neural
|
|
307
|
+
// sozinho. São ~90 MB, e o usuário merece saber por que o primeiro vídeo sai
|
|
308
|
+
// com uma voz e o próximo com outra — melhor ele ouvir isso do agente do que
|
|
309
|
+
// achar que o programa está inconstante.
|
|
310
|
+
async function avisoDaVoz(doc) {
|
|
311
|
+
const temNarracao = !!(doc && (doc.narracao
|
|
312
|
+
|| (doc.cenas || []).some((c) => c && typeof c.narracao === 'string' && c.narracao.trim())));
|
|
313
|
+
if (!temNarracao) return null;
|
|
314
|
+
try {
|
|
315
|
+
const s = await pedir('GET', '/api/voz/status', null, 3000);
|
|
316
|
+
if (s && s.piper && !s.piper.pronta && s.piper.suportado) {
|
|
317
|
+
return 'A voz neural (offline, ilimitada) está sendo baixada agora — ~90 MB, uma vez só. '
|
|
318
|
+
+ 'Até terminar a narração sai por uma voz de reserva, mais simples. Diga isso ao usuário.';
|
|
319
|
+
}
|
|
320
|
+
} catch { /* status é enfeite; não pode atrapalhar a criação */ }
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
280
324
|
const criar = ({ ferramenta, titulo, doc }) => comStudio(async () => {
|
|
281
325
|
if (!ferramenta) return { ok: false, error: 'Diga a ferramenta: prisma, tela, prosa, grade, corte ou traco.' };
|
|
282
326
|
if (!doc) return { ok: false, error: 'Falta o doc. Leia a spec com studio_spec antes de criar.' };
|
|
283
327
|
const r = await pedir('POST', '/api/arquivos', { ferramenta, titulo: titulo || 'Sem título', doc });
|
|
284
|
-
|
|
328
|
+
const saida = { ok: true, ...r };
|
|
329
|
+
const voz = await avisoDaVoz(doc);
|
|
330
|
+
if (voz) saida.voz = voz;
|
|
331
|
+
return saida;
|
|
285
332
|
});
|
|
286
333
|
|
|
334
|
+
// O que o agente recebe de volta depois de mexer num arquivo. Devolver o
|
|
335
|
+
// documento inteiro era caro e inútil: um vídeo de oito cenas volta como
|
|
336
|
+
// milhares de tokens que ele acabou de escrever. O que ele precisa saber é
|
|
337
|
+
// que gravou, quantas peças ficaram e como voltar atrás.
|
|
338
|
+
const resumoDoArquivo = (r) => {
|
|
339
|
+
const doc = r.doc || {};
|
|
340
|
+
const pecas = (doc.cenas || doc.blocos || doc.abas || []).length;
|
|
341
|
+
return {
|
|
342
|
+
id: r.id, titulo: r.titulo, ferramenta: r.ferramenta,
|
|
343
|
+
pecas, tema: doc.tema,
|
|
344
|
+
temNarracao: !!(doc.narracao || (doc.cenas || []).some((c) => c && c.narracao)),
|
|
345
|
+
temTrilha: !!doc.trilha,
|
|
346
|
+
// O mesmo link de sempre: ajustar não cria peça nova, e é isso que o
|
|
347
|
+
// agente precisa dizer ao usuário no finish.
|
|
348
|
+
// localhost, e não 127.0.0.1, para bater com o link que studio_criar
|
|
349
|
+
// já deu ao usuário — dois endereços para a mesma peça confundem.
|
|
350
|
+
ver: `http://localhost:${PORTA}/v/${r.id}`,
|
|
351
|
+
};
|
|
352
|
+
};
|
|
353
|
+
|
|
287
354
|
const atualizar = ({ id, titulo, doc }) => comStudio(() => comIdReal(id, async () => {
|
|
288
355
|
if (!id) return { ok: false, error: 'Falta o id do arquivo.' };
|
|
289
356
|
const corpo = {};
|
|
290
357
|
if (titulo != null) corpo.titulo = titulo;
|
|
291
358
|
if (doc != null) corpo.doc = doc;
|
|
292
359
|
const r = await pedir('PUT', `/api/arquivos/${encodeURIComponent(id)}`, corpo);
|
|
293
|
-
|
|
360
|
+
const saida = { ok: true, ...resumoDoArquivo(r) };
|
|
361
|
+
// A mescla é a razão de isto não ter destruído nada: as chaves que você
|
|
362
|
+
// não mandou continuam lá. Dizer isso evita a rodada seguinte em que o
|
|
363
|
+
// agente "reconstrói" o que nunca se perdeu.
|
|
364
|
+
saida.nota = 'Só as chaves que você mandou mudaram; o resto do documento continua como estava.';
|
|
365
|
+
if (r.versao_anterior) saida.desfazer = `studio_versoes com id "${r.id}" e restaurar "${r.versao_anterior}" volta ao estado anterior.`;
|
|
366
|
+
return saida;
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
// ── Histórico ────────────────────────────────────────────────────────────
|
|
370
|
+
// Existe porque uma vez não existiu: um ajuste apagou oito cenas de trabalho
|
|
371
|
+
// e não havia de onde tirar de volta.
|
|
372
|
+
const versoesDe = ({ id, restaurar }) => comStudio(() => comIdReal(id, async () => {
|
|
373
|
+
if (!id) return { ok: false, error: 'Falta o id do arquivo.' };
|
|
374
|
+
if (restaurar) {
|
|
375
|
+
const alvo = restaurar === true || restaurar === 'anterior' ? null : String(restaurar);
|
|
376
|
+
try {
|
|
377
|
+
const r = await pedir('POST', `/api/arquivos/${encodeURIComponent(id)}/restaurar`,
|
|
378
|
+
alvo ? { versao: alvo } : {});
|
|
379
|
+
return { ok: true, ...r };
|
|
380
|
+
} catch (e) {
|
|
381
|
+
// 404 aqui pode ser o arquivo OU a versão. Quando o servidor manda
|
|
382
|
+
// a lista junto, o arquivo existe e quem não existe é a versão —
|
|
383
|
+
// deixar comIdReal dizer "o id não existe" mandaria o agente
|
|
384
|
+
// caçar um problema que não é o dele.
|
|
385
|
+
if (e && e.status === 404 && e.corpo && Array.isArray(e.corpo.versoes)) {
|
|
386
|
+
return {
|
|
387
|
+
ok: false,
|
|
388
|
+
error: `A versão "${alvo}" não existe.`,
|
|
389
|
+
versoes: e.corpo.versoes,
|
|
390
|
+
comoCorrigir: 'Use um "versao" desta lista, copiado LITERALMENTE, ou "anterior" para desfazer a última mudança.',
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
throw e;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const r = await pedir('GET', `/api/arquivos/${encodeURIComponent(id)}/versoes`);
|
|
397
|
+
return {
|
|
398
|
+
ok: true,
|
|
399
|
+
versoes: r.versoes || [],
|
|
400
|
+
comoVoltar: 'Para voltar, chame de novo com restaurar: "<versao>" — ou restaurar: "anterior" para desfazer a última mudança.',
|
|
401
|
+
};
|
|
294
402
|
}));
|
|
295
403
|
|
|
296
404
|
// ── Mídia: a imagem de fundo e a música do usuário ───────────────────────
|
|
@@ -338,6 +446,6 @@ const midiaEnviar = ({ arquivo, nome }) => comStudio(async () => {
|
|
|
338
446
|
module.exports = {
|
|
339
447
|
BASE, PORTA, CASA,
|
|
340
448
|
garantirNoAr, estaNoAr, prepararCasa, pythonCmd,
|
|
341
|
-
manual, ferramentas, spec, listar, ler, criar, atualizar,
|
|
449
|
+
manual, ferramentas, spec, listar, ler, criar, atualizar, versoesDe,
|
|
342
450
|
midiaListar, midiaEnviar,
|
|
343
451
|
};
|
package/lib/tools.js
CHANGED
|
@@ -535,6 +535,8 @@ async function despachar(name, args, ctx) {
|
|
|
535
535
|
return studio.ler(args.id);
|
|
536
536
|
case 'studio_atualizar':
|
|
537
537
|
return studio.atualizar({ id: args.id, titulo: args.titulo, doc: args.doc });
|
|
538
|
+
case 'studio_versoes':
|
|
539
|
+
return studio.versoesDe({ id: args.id, restaurar: args.restaurar });
|
|
538
540
|
case 'studio_listar':
|
|
539
541
|
return studio.listar();
|
|
540
542
|
case 'studio_midia':
|
package/lib/ui.js
CHANGED
|
@@ -278,6 +278,7 @@ function actionHeader(name, args) {
|
|
|
278
278
|
desktop_double_click:'Desktop·2click', desktop_open_app:'Desktop·app',
|
|
279
279
|
studio_manual:'Estúdio·manual', studio_spec:'Estúdio·spec', studio_criar:'Estúdio·criar',
|
|
280
280
|
studio_ler:'Estúdio·ler', studio_atualizar:'Estúdio·ajustar', studio_listar:'Estúdio·lista',
|
|
281
|
+
studio_versoes:'Estúdio·histórico',
|
|
281
282
|
desktop_windows:'Desktop·janelas', desktop_focus:'Desktop·foco',
|
|
282
283
|
desktop_elements:'Desktop·elementos', desktop_click_element:'Desktop·click',
|
|
283
284
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.12.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"
|
|
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"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
Binary file
|
|
@@ -106,6 +106,19 @@ A resposta do POST traz:
|
|
|
106
106
|
|
|
107
107
|
## Iterar
|
|
108
108
|
|
|
109
|
-
Para ajustar algo que o usuário pediu depois,
|
|
110
|
-
|
|
109
|
+
Para ajustar algo que o usuário pediu depois, mande no `PUT` **só as chaves que
|
|
110
|
+
mudam**. Elas entram por cima do documento; o que você não citou fica como estava.
|
|
111
|
+
Trocar o tema de um vídeo pronto é `{"doc":{"tema":"papel"}}` — as cenas, a narração
|
|
112
|
+
e a trilha continuam lá. Não recrie do zero: o link já está com o usuário e deve
|
|
111
113
|
continuar valendo.
|
|
114
|
+
|
|
115
|
+
Se quiser mesmo jogar fora o documento inteiro e pôr outro no lugar, mande
|
|
116
|
+
`{"doc": {...}, "substituir": true}`. Sem essa chave, nada é apagado.
|
|
117
|
+
|
|
118
|
+
| Método | Rota | O que faz |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `GET` | `/api/arquivos/<id>/versoes` | as últimas 20 versões, a mais nova primeiro |
|
|
121
|
+
| `POST` | `/api/arquivos/<id>/restaurar` | `{versao?}` — sem `versao`, desfaz a última mudança |
|
|
122
|
+
|
|
123
|
+
Toda gravação guarda a versão anterior antes de trocar. Estragou um arquivo pronto?
|
|
124
|
+
`POST /api/arquivos/<id>/restaurar` traz de volta.
|
|
@@ -136,6 +136,8 @@ Sem declarar nada, os elementos já entram em cascata na ordem em que aparecem.
|
|
|
136
136
|
|
|
137
137
|
`meia-noite` `claro` `papel` `neon` `brasa` `floresta` `retro` `oceano` `doce` `mono`
|
|
138
138
|
|
|
139
|
+
São esses dez, e só. Nome fora da lista é recusado: escuro é `meia-noite`, não
|
|
140
|
+
`noite`. Cor de marca não vira tema — ela entra em `cor` nos elementos.
|
|
139
141
|
Vale no documento ou por cena. Uma cena com tema diferente no meio cria respiro —
|
|
140
142
|
use uma vez, não cinco.
|
|
141
143
|
|
|
@@ -138,12 +138,28 @@ na hora de tocar — sem gravar nada, sem arquivo:
|
|
|
138
138
|
"elementos": [ ... ] }
|
|
139
139
|
```
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
Escolha da voz no **documento** (opcional):
|
|
142
142
|
|
|
143
143
|
```json
|
|
144
|
-
"voz": { "
|
|
144
|
+
"voz": { "voz": "jeff", "grave": true, "velocidade": 1 }
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
+
A síntese é neural e roda na máquina do usuário — offline, sem limite. As
|
|
148
|
+
vozes pt-BR, com a frequência de cada uma medida (menor = mais grave):
|
|
149
|
+
|
|
150
|
+
| id | timbre | Hz |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `jeff` | grave, de locução — **é a padrão** | 139 |
|
|
153
|
+
| `cadu` | grave, mais solta | 140 |
|
|
154
|
+
| `edresson` | média, mais leve de baixar | 151 |
|
|
155
|
+
| `faber` | clara, de leitura | 167 |
|
|
156
|
+
| `tugao` | português de Portugal | — |
|
|
157
|
+
|
|
158
|
+
`"grave": true` abaixa mais uns 10% — é o registro de abertura de vídeo. Não
|
|
159
|
+
há voz feminina em pt-BR neste motor; se pedirem uma, diga isso em vez de
|
|
160
|
+
prometer. Na primeira vez o motor é baixado sozinho (~90 MB); enquanto isso
|
|
161
|
+
a narração sai por uma voz de reserva, e o vídeo nunca fica mudo.
|
|
162
|
+
|
|
147
163
|
Regras que fazem diferença:
|
|
148
164
|
|
|
149
165
|
- **Escreva para o ouvido, não para o olho.** A narração NÃO repete o texto da
|