primocode 8.10.3 → 8.10.5
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/tools.js +28 -2
- package/package.json +1 -1
- package/studio/arquivos/corte-legado-teste.json +1 -0
- package/studio/arquivos/corte-legado2.json +1 -0
- package/studio/arquivos/corte-ok-1270e.json +22 -0
- package/studio/ferramentas/corte.md +5 -0
- package/studio/server.py +59 -0
- package/studio/web/js/narracao.js +6 -1
- package/studio/web/js/player.js +25 -1
- package/studio/web/js/render.js +13 -0
- package/studio/web/js/voz-neural.js +7 -2
package/lib/tools.js
CHANGED
|
@@ -170,6 +170,9 @@ async function runShell(command, timeout = 60000, cwd) {
|
|
|
170
170
|
// execFile com array de argumentos nunca passa por shell nenhum: "target" vira
|
|
171
171
|
// UM argumento, não texto que um shell reinterpreta — sem isso, um valor tipo
|
|
172
172
|
// `https://x/"; touch /tmp/pwned; echo "` injetava comando (achado de segurança).
|
|
173
|
+
// Quando o auto-abrir do Studio abriu pela última vez — o freio das mil abas.
|
|
174
|
+
let ultimaAberturaStudio = 0;
|
|
175
|
+
|
|
173
176
|
function openInBrowser(target) {
|
|
174
177
|
const p = os.platform();
|
|
175
178
|
const [cmd, args, opener] = p === 'darwin' ? ['open', [target], 'open']
|
|
@@ -503,8 +506,31 @@ async function despachar(name, args, ctx) {
|
|
|
503
506
|
return studio.manual();
|
|
504
507
|
case 'studio_spec':
|
|
505
508
|
return studio.spec(args.ferramenta || args.slug || '');
|
|
506
|
-
case 'studio_criar':
|
|
507
|
-
|
|
509
|
+
case 'studio_criar': {
|
|
510
|
+
const r = await studio.criar({ ferramenta: args.ferramenta, titulo: args.titulo, doc: args.doc });
|
|
511
|
+
// Criou? ABRE NO NAVEGADOR, sozinho. O dono: "quando ele faz um
|
|
512
|
+
// vídeo, uma apresentação, abre automático — a pessoa não vai
|
|
513
|
+
// saber abrir". E cada "abre pra mim" digitado depois custava
|
|
514
|
+
// uma tarefa inteira de tokens para o modelo redescobrir o
|
|
515
|
+
// link. Abrir é determinístico: não se gasta modelo com isso.
|
|
516
|
+
if (r && r.ok !== false && r.ver) {
|
|
517
|
+
// UMA aba por tarefa. O modelo às vezes cria três versões
|
|
518
|
+
// seguidas, e abrir todas enchia o navegador do usuário de
|
|
519
|
+
// abas — "abre 1000 versões". A primeira abre; as outras
|
|
520
|
+
// são avisadas a ATUALIZAR a que já existe.
|
|
521
|
+
const agora = Date.now();
|
|
522
|
+
if (agora - ultimaAberturaStudio > 45000) {
|
|
523
|
+
ultimaAberturaStudio = agora;
|
|
524
|
+
try { openInBrowser(r.ver); } catch {}
|
|
525
|
+
r.abertoNoNavegador = true;
|
|
526
|
+
r.aviso = 'JÁ ABERTO no navegador do usuário. Chame finish AGORA dizendo o que foi criado — não abra de novo, não leia de volta, não ajuste sem pedido.';
|
|
527
|
+
} else {
|
|
528
|
+
r.aviso = `Você JÁ criou uma versão nesta tarefa, e ela está aberta no navegador. NÃO crie outra: `
|
|
529
|
+
+ `para mudar algo, use studio_atualizar com o id "${r.id}". Chame finish AGORA.`;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return r;
|
|
533
|
+
}
|
|
508
534
|
case 'studio_ler':
|
|
509
535
|
return studio.ler(args.id);
|
|
510
536
|
case 'studio_atualizar':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.10.
|
|
3
|
+
"version": "8.10.5",
|
|
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": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id": "corte-legado-teste", "ferramenta": "corte", "titulo": "legado", "doc": {"tema": "meia-noite", "largura": 1080, "altura": 1920, "cenas": [{"duracao": "4s", "fundo": "aurora", "elementos": [{"tipo": "titulo", "texto": "Imersão Founders AI"}, {"tipo": "subtitulo", "conteudo": "Paris Group · Chapecó"}]}]}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"id": "corte-legado2", "ferramenta": "corte", "titulo": "legado2", "doc": {"tema": "meia-noite", "largura": 1080, "altura": 1920, "cenas": [{"duracao": "4s", "fundo": "aurora", "elementos": [{"tipo": "titulo", "texto": "Imersão Founders AI", "anim": "nenhuma"}, {"tipo": "subtitulo", "conteudo": "Paris Group · Chapecó", "anim": "nenhuma"}]}]}}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "corte-ok-1270e",
|
|
3
|
+
"ferramenta": "corte",
|
|
4
|
+
"titulo": "ok",
|
|
5
|
+
"doc": {
|
|
6
|
+
"tema": "brasa",
|
|
7
|
+
"cenas": [
|
|
8
|
+
{
|
|
9
|
+
"duracao": 3,
|
|
10
|
+
"elementos": [
|
|
11
|
+
{
|
|
12
|
+
"tipo": "texto",
|
|
13
|
+
"papel": "titulo",
|
|
14
|
+
"texto": "Founders AI",
|
|
15
|
+
"anim": "nenhuma"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"atualizado": "2026-08-16T17:50:21+00:00"
|
|
22
|
+
}
|
|
@@ -10,6 +10,11 @@ corte e o agente inventou a estrutura. Formato primeiro; detalhe depois. -->
|
|
|
10
10
|
|
|
11
11
|
## Exemplo completo
|
|
12
12
|
|
|
13
|
+
**ATENÇÃO — este exemplo ensina o FORMATO, não o conteúdo.** O assunto dele
|
|
14
|
+
(conta de luz, bandeira tarifária) é inventado para a demonstração. Copiar
|
|
15
|
+
qualquer frase dele para o SEU documento é ERRO: escreva cada cena com os
|
|
16
|
+
fatos da SUA tarefa. O servidor recusa documento que venha com estas frases.
|
|
17
|
+
|
|
13
18
|
```json
|
|
14
19
|
{
|
|
15
20
|
"ferramenta": "corte",
|
package/studio/server.py
CHANGED
|
@@ -278,12 +278,60 @@ def criar(dados):
|
|
|
278
278
|
})
|
|
279
279
|
|
|
280
280
|
|
|
281
|
+
# Frases que só existem nos EXEMPLOS das specs. Documento que chega com uma
|
|
282
|
+
# delas é o agente copiando o exemplo em vez de escrever o conteúdo do pedido
|
|
283
|
+
# — um usuário pediu um vídeo da empresa dele e recebeu cenas sobre "bandeira
|
|
284
|
+
# tarifária", o assunto fictício do exemplo do corte.
|
|
285
|
+
# Só frases INCONFUNDÍVEIS entram aqui: "energia solar no brasil" seria um
|
|
286
|
+
# pedido legítimo possível, e bloquear pedido legítimo é pior que deixar um
|
|
287
|
+
# plágio passar. Estas seis não existem fora do exemplo.
|
|
288
|
+
FRASES_DE_EXEMPLO = [
|
|
289
|
+
"bandeira tarifária em 12 meses", "bandeira tarifaria em 12 meses",
|
|
290
|
+
"não foi o seu chuveiro", "nao foi o seu chuveiro",
|
|
291
|
+
"saturadas no nordeste", "usina gerando sem escoar",
|
|
292
|
+
"segue para entender a próxima", "segue para entender a proxima",
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _texto_do_doc(doc):
|
|
297
|
+
"""Todo texto visível do documento, achatado, para inspeção."""
|
|
298
|
+
pedacos = []
|
|
299
|
+
|
|
300
|
+
def anda(x):
|
|
301
|
+
if isinstance(x, dict):
|
|
302
|
+
for v in x.values():
|
|
303
|
+
anda(v)
|
|
304
|
+
elif isinstance(x, list):
|
|
305
|
+
for v in x:
|
|
306
|
+
anda(v)
|
|
307
|
+
elif isinstance(x, str):
|
|
308
|
+
pedacos.append(x)
|
|
309
|
+
|
|
310
|
+
anda(doc)
|
|
311
|
+
return " ".join(pedacos).lower()
|
|
312
|
+
|
|
313
|
+
|
|
281
314
|
def validar_doc(modelo, doc):
|
|
282
315
|
"""None quando o doc serve; senão, a frase que ENSINA o formato certo.
|
|
283
316
|
|
|
284
317
|
A mensagem repete a estrutura mínima de propósito: ela volta para o agente
|
|
285
318
|
como resultado da tool, e é a única chance de ele se corrigir sem o
|
|
286
319
|
usuário ver um vídeo preto."""
|
|
320
|
+
texto = _texto_do_doc(doc)
|
|
321
|
+
for frase in FRASES_DE_EXEMPLO:
|
|
322
|
+
if frase in texto:
|
|
323
|
+
return ('o documento contém "%s" — isso é texto do EXEMPLO da spec, não do pedido '
|
|
324
|
+
'do usuário. O exemplo ensina o FORMATO; o conteúdo é seu: reescreva cada '
|
|
325
|
+
'cena com os fatos da tarefa (a pesquisa que você fez) e crie de novo.' % frase)
|
|
326
|
+
|
|
327
|
+
# Os tipos que o render.js de fato desenha. Elemento de tipo inventado
|
|
328
|
+
# ("titulo", "subtitulo", "video"...) passava na validação e caía no
|
|
329
|
+
# default do renderizador: texto cru de 16px num canvas de 1920 — ou nada,
|
|
330
|
+
# quando o texto estava noutro campo. Era o "vídeo com música mas tudo
|
|
331
|
+
# preto": documento válido na forma, invisível na tela.
|
|
332
|
+
TIPOS_VALIDOS = {"texto", "lista", "cartoes", "numeros", "tabela", "codigo",
|
|
333
|
+
"citacao", "forma", "imagem", "icone", "grafico", "no", "conector"}
|
|
334
|
+
|
|
287
335
|
if modelo == "cena":
|
|
288
336
|
cenas = doc.get("cenas")
|
|
289
337
|
if not isinstance(cenas, list) or not cenas:
|
|
@@ -297,9 +345,20 @@ def validar_doc(modelo, doc):
|
|
|
297
345
|
if not isinstance(els, list) or not els:
|
|
298
346
|
return ('cena %d sem "elementos". Cada cena precisa de elementos:[...] — '
|
|
299
347
|
'ex.: {"tipo":"texto","texto":"..."}. Leia studio_spec e refaça.' % (i + 1))
|
|
348
|
+
dur = c.get("duracao")
|
|
349
|
+
if dur is not None and not isinstance(dur, (int, float)):
|
|
350
|
+
return ('cena %d com "duracao" que não é número (veio %r). Use número de segundos: '
|
|
351
|
+
'"duracao": 3' % (i + 1, dur))
|
|
300
352
|
for j, e in enumerate(els):
|
|
301
353
|
if not isinstance(e, dict) or not e.get("tipo"):
|
|
302
354
|
return 'elemento %d da cena %d sem "tipo" (texto, imagem, lista, numeros, forma...)' % (j + 1, i + 1)
|
|
355
|
+
if e.get("tipo") not in TIPOS_VALIDOS:
|
|
356
|
+
return ('elemento %d da cena %d tem tipo "%s", que NÃO existe. Os tipos válidos são: %s. '
|
|
357
|
+
'Para título use {"tipo":"texto","papel":"titulo","texto":"..."}.'
|
|
358
|
+
% (j + 1, i + 1, e.get("tipo"), ", ".join(sorted(TIPOS_VALIDOS))))
|
|
359
|
+
if e.get("tipo") == "texto" and not (e.get("texto") or "").strip():
|
|
360
|
+
return ('elemento %d da cena %d é "texto" mas o campo "texto" está vazio — o conteúdo '
|
|
361
|
+
'vai SEMPRE no campo "texto".' % (j + 1, i + 1))
|
|
303
362
|
elif modelo == "documento":
|
|
304
363
|
blocos = doc.get("blocos")
|
|
305
364
|
if not isinstance(blocos, list) or not blocos:
|
|
@@ -228,7 +228,12 @@ var Narracao = (function () {
|
|
|
228
228
|
return VozNeural.audioDe(texto, cfg);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
function falando() {
|
|
231
|
+
function falando() {
|
|
232
|
+
/* A voz pode estar saindo por três lugares; o player precisa da verdade
|
|
233
|
+
de todos, senão corta a cena no meio da fala neural. */
|
|
234
|
+
if (typeof VozNeural !== "undefined" && VozNeural.falando && VozNeural.falando()) return true;
|
|
235
|
+
return !!sintese && sintese.speaking;
|
|
236
|
+
}
|
|
232
237
|
|
|
233
238
|
function estimar(texto, velocidade) {
|
|
234
239
|
var palavras = String(texto || "").trim().split(/\s+/).filter(Boolean).length;
|
package/studio/web/js/player.js
CHANGED
|
@@ -30,7 +30,12 @@
|
|
|
30
30
|
return String(Math.floor(s / 60)).padStart(2, "0") + ":" + String(s % 60).padStart(2, "0");
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function duracaoDe(i) {
|
|
33
|
+
function duracaoDe(i) {
|
|
34
|
+
/* "duracao": "3s" (texto) fazia acc virar concatenação e o relógio dava
|
|
35
|
+
vídeo de 00:00. Número são ou o padrão — nunca corrente de texto. */
|
|
36
|
+
var d = Number(cenas[i] && cenas[i].duracao);
|
|
37
|
+
return (isFinite(d) && d > 0) ? Math.min(60, d) : 4;
|
|
38
|
+
}
|
|
34
39
|
function total() { return cenas.reduce(function (s, _, i) { return s + duracaoDe(i); }, 0); }
|
|
35
40
|
function inicioDe(i) { var s = 0; for (var k = 0; k < i; k++) s += duracaoDe(k); return s; }
|
|
36
41
|
|
|
@@ -185,6 +190,13 @@
|
|
|
185
190
|
if (gravador && gravador.state === "recording") setTimeout(pararGravacao, 600);
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
/* A narração da cena ainda está no ar? */
|
|
194
|
+
function narrando() {
|
|
195
|
+
if (vozGravada && !vozGravada.ended && !vozGravada.paused) return true;
|
|
196
|
+
if (typeof Narracao !== "undefined" && Narracao.falando && Narracao.falando()) return true;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
188
200
|
function laco() {
|
|
189
201
|
if (!tocando) return;
|
|
190
202
|
decorrido = (performance.now() - t0) / 1000;
|
|
@@ -194,6 +206,18 @@
|
|
|
194
206
|
acc += duracaoDe(i);
|
|
195
207
|
if (decorrido < acc) { alvo = i; break; }
|
|
196
208
|
}
|
|
209
|
+
/* A CENA ESPERA A VOZ. O relógio andava só pela `duracao`, e uma narração
|
|
210
|
+
mais longa que a cena vazava por cima da seguinte — era a "narração
|
|
211
|
+
fora de sincronia". Se a fala da cena atual ainda está no ar, o relógio
|
|
212
|
+
congela na beirada da cena: quem dita o corte é a voz. */
|
|
213
|
+
var fimDaAtual = inicioDe(atual) + duracaoDe(atual);
|
|
214
|
+
if ((alvo !== atual || decorrido >= total()) && narrando()) {
|
|
215
|
+
t0 = performance.now() - (fimDaAtual - 0.05) * 1000;
|
|
216
|
+
decorrido = fimDaAtual - 0.05;
|
|
217
|
+
pintar();
|
|
218
|
+
quadro = requestAnimationFrame(laco);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
197
221
|
if (decorrido >= total()) return fim();
|
|
198
222
|
if (alvo !== atual) ir(alvo, true);
|
|
199
223
|
pintar();
|
package/studio/web/js/render.js
CHANGED
|
@@ -137,9 +137,22 @@ function svgGrafico(e, l, a) {
|
|
|
137
137
|
|
|
138
138
|
/* -------------------------------------------------------------- elementos */
|
|
139
139
|
|
|
140
|
+
var TIPOS_CONHECIDOS = { texto: 1, lista: 1, cartoes: 1, numeros: 1, tabela: 1, codigo: 1,
|
|
141
|
+
citacao: 1, forma: 1, imagem: 1, icone: 1, grafico: 1, no: 1, conector: 1 };
|
|
142
|
+
|
|
140
143
|
function renderElemento(e, ctx) {
|
|
141
144
|
var no = document.createElement("div");
|
|
142
145
|
var tipo = e.tipo || "texto";
|
|
146
|
+
/* Tipo inventado ("titulo", "subtitulo"...) caía num default sem estilo:
|
|
147
|
+
texto de 16px num canvas de 1920 — invisível — ou div vazio quando o
|
|
148
|
+
conteúdo estava noutro campo. O servidor novo recusa; documentos velhos
|
|
149
|
+
e outros clientes ganham a leitura generosa: vira texto de verdade. */
|
|
150
|
+
if (!TIPOS_CONHECIDOS[tipo]) {
|
|
151
|
+
e = { tipo: "texto", papel: /titulo|title/i.test(tipo) ? "titulo" : (e.papel || null),
|
|
152
|
+
texto: e.texto || e.conteudo || e.titulo || e.valor || e.legenda || "",
|
|
153
|
+
tamanho: e.tamanho, estilo: e.estilo, anim: e.anim, x: e.x, y: e.y, l: e.l, a: e.a };
|
|
154
|
+
tipo = "texto";
|
|
155
|
+
}
|
|
143
156
|
no.className = "el " + tipo;
|
|
144
157
|
no.dataset.tipo = tipo;
|
|
145
158
|
|
|
@@ -106,7 +106,7 @@ var VozNeural = (function () {
|
|
|
106
106
|
var fechou = false;
|
|
107
107
|
var relogio = setTimeout(function () {
|
|
108
108
|
if (!fechou) { fechou = true; try { ws.close(); } catch (e) {} erro(new Error("voz neural nao respondeu")); }
|
|
109
|
-
},
|
|
109
|
+
}, 6000);
|
|
110
110
|
|
|
111
111
|
function desistir(e) {
|
|
112
112
|
if (fechou) return;
|
|
@@ -220,6 +220,11 @@ var VozNeural = (function () {
|
|
|
220
220
|
if (tocando) { try { tocando.pause(); } catch (e) {} tocando = null; }
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/* O player pergunta isto para segurar a cena até a voz acabar. */
|
|
224
|
+
function falando() {
|
|
225
|
+
return !!(tocando && !tocando.paused && !tocando.ended);
|
|
226
|
+
}
|
|
227
|
+
|
|
223
228
|
/* Serve para a interface avisar antes de o usuário montar o vídeo inteiro. */
|
|
224
229
|
function testar(cfg) {
|
|
225
230
|
return audioDe("Teste de voz do PrimoCode Studio.", cfg)
|
|
@@ -229,7 +234,7 @@ var VozNeural = (function () {
|
|
|
229
234
|
|
|
230
235
|
return {
|
|
231
236
|
vozes: vozes, vozPadrao: vozPadrao, graves: graves,
|
|
232
|
-
audioDe: audioDe, falar: falar, parar: parar, testar: testar,
|
|
237
|
+
audioDe: audioDe, falar: falar, parar: parar, falando: falando, testar: testar,
|
|
233
238
|
_selo: selo
|
|
234
239
|
};
|
|
235
240
|
})();
|