primocode 8.10.4 → 8.11.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 +31 -0
- package/lib/act.js +13 -6
- package/lib/claude-engine.js +44 -10
- package/lib/tools.js +17 -3
- 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/server.py +19 -0
- package/studio/web/js/player.js +26 -6
- package/studio/web/js/render.js +13 -0
package/bin/primocode.js
CHANGED
|
@@ -531,6 +531,13 @@ async function sendPrompt(userText) {
|
|
|
531
531
|
}
|
|
532
532
|
|
|
533
533
|
if (modo === 'primocode') {
|
|
534
|
+
// "baixa o vídeo" / "exporta em mp4" não precisa de IA NENHUMA: é
|
|
535
|
+
// abrir a última criação do Studio com #gravar — o navegador pede a
|
|
536
|
+
// permissão de tela (a aprovação), grava a passada e baixa no fim.
|
|
537
|
+
// Antes isso virava uma tarefa inteira de modelo (id inventado,
|
|
538
|
+
// mídia inventada, limite estourado) para chegar no mesmo lugar.
|
|
539
|
+
if (await baixarCriacaoDireto(userText)) { refreshPrompt(); return; }
|
|
540
|
+
|
|
534
541
|
const plan = prepararPasta(userText);
|
|
535
542
|
|
|
536
543
|
// Antes de gastar com a tarefa, dobra no resumo o que já passou. A
|
|
@@ -632,6 +639,30 @@ async function sendPrompt(userText) {
|
|
|
632
639
|
const RE_DESCOBRIR = /\b(pesquis|busca|procur|descubr|investig|analis|leia|estud|olh[ae] (o site|a p[áa]gina)|ver o que)\w*/i;
|
|
633
640
|
const RE_CRIAR_DEPOIS = /\b(cri[ae]|faz|faça|mont[ae]|ger[ae]|escrev[ae]|produz|desenvolv)\w*/i;
|
|
634
641
|
|
|
642
|
+
/** "baixa/exporta o vídeo" resolvido na hora, sem gastar modelo. */
|
|
643
|
+
const RE_BAIXAR = /\b(baixa\w*|exporta\w*|download|salva\w*)\b/i;
|
|
644
|
+
const RE_PECA_STUDIO = /\b(v[ií]deo|mp4|mp3|apresenta[çc][ãa]o|slides?|reels?)\b/i;
|
|
645
|
+
|
|
646
|
+
async function baixarCriacaoDireto(texto) {
|
|
647
|
+
if (!(RE_BAIXAR.test(texto) && RE_PECA_STUDIO.test(texto))) return false;
|
|
648
|
+
try {
|
|
649
|
+
const studio = require('../lib/studio');
|
|
650
|
+
const r = await studio.listar();
|
|
651
|
+
const lista = (r && r.arquivos) || [];
|
|
652
|
+
// a criação mais nova que é vídeo; sem vídeo, a mais nova de todas
|
|
653
|
+
const alvo = lista.find((a) => a.ferramenta === 'corte') || lista[0];
|
|
654
|
+
if (!alvo) return false;
|
|
655
|
+
const url = `http://localhost:7777/v/${alvo.id}#gravar`;
|
|
656
|
+
await tools.execute('open_in_browser', { target: url }, { projectDir: state.workDir || os.homedir() });
|
|
657
|
+
console.log(c.ok(` ⎿ abrindo "${alvo.titulo}" para gravar`));
|
|
658
|
+
console.log(c.muted(' O navegador vai pedir permissão de tela — aceite (marque "compartilhar áudio da aba"'));
|
|
659
|
+
console.log(c.muted(' para levar a narração junto). O vídeo roda sozinho e baixa no fim.'));
|
|
660
|
+
return true;
|
|
661
|
+
} catch {
|
|
662
|
+
return false; // Studio fora do ar ou sem criações: segue o caminho normal
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
635
666
|
async function dividirTarefa(pedido) {
|
|
636
667
|
const inteiro = [{ titulo: 'tarefa', pedido }];
|
|
637
668
|
if (!pedido || pedido.trim().length < 12) return inteiro;
|
package/lib/act.js
CHANGED
|
@@ -295,9 +295,11 @@ function encolherParaContinuar(messages) {
|
|
|
295
295
|
|
|
296
296
|
/** Quanto esperar quando o minuto estourou. O provedor costuma dizer
|
|
297
297
|
* ("Please try again in 7.664s"); sem isso, meio minuto é seguro. */
|
|
298
|
-
function esperaDoErro(e) {
|
|
298
|
+
function esperaDoErro(e, tentativa = 0) {
|
|
299
299
|
const m = /try again in ([0-9.]+)s/i.exec(String((e && e.message) || e || ''));
|
|
300
|
-
|
|
300
|
+
// Sem o tempo do provedor, cresce a passada a cada tentativa: bater na
|
|
301
|
+
// mesma porta a cada 30s por horas só esquenta o rate limit.
|
|
302
|
+
const s = m ? Math.ceil(parseFloat(m[1])) + 2 : Math.min(70, 30 + tentativa * 10);
|
|
301
303
|
return Math.min(70, Math.max(5, s)) * 1000;
|
|
302
304
|
}
|
|
303
305
|
|
|
@@ -381,7 +383,12 @@ async function runAct(userPrompt, opts) {
|
|
|
381
383
|
// isso (num caso real "encolheu" de 2 para 2 mensagens); esperar a janela
|
|
382
384
|
// virar, sim.
|
|
383
385
|
let esperas = 0;
|
|
384
|
-
|
|
386
|
+
// Ordem do dono: limite de token NUNCA mais encerra a tarefa. Ele espera
|
|
387
|
+
// o quanto precisar — Esc Esc continua sendo a saída na mão do usuário.
|
|
388
|
+
// 200 esperas de até 70s são horas: na prática, para quem não apertou
|
|
389
|
+
// Esc, é "espera até dar".
|
|
390
|
+
// O teste derruba para 2 via env; em produção são 200.
|
|
391
|
+
const MAX_ESPERAS = Number(process.env.PRIMOCODE_MAX_ESPERAS || 200);
|
|
385
392
|
|
|
386
393
|
let voltas = 0;
|
|
387
394
|
for (let iteration = 0; iteration < MAX_ITERATIONS && !PARAR; iteration++) {
|
|
@@ -424,7 +431,7 @@ async function runAct(userPrompt, opts) {
|
|
|
424
431
|
}
|
|
425
432
|
if (esperas < MAX_ESPERAS) {
|
|
426
433
|
esperas++;
|
|
427
|
-
const ms = esperaDoErro(e);
|
|
434
|
+
const ms = esperaDoErro(e, esperas);
|
|
428
435
|
if (!subagent) {
|
|
429
436
|
const sp2 = new Spinner(`limite do minuto — continuando em ${Math.round(ms / 1000)}s`).start();
|
|
430
437
|
await new Promise((r) => setTimeout(r, ms));
|
|
@@ -451,7 +458,7 @@ async function runAct(userPrompt, opts) {
|
|
|
451
458
|
// adjust your prompt.") caía na tela de quem não tem prompt nenhum
|
|
452
459
|
// para ajustar.
|
|
453
460
|
if (streamError && /failed to call a function|failed_generation|tool_use_failed/i.test(String(streamError))
|
|
454
|
-
&& funcoesQuebradas <
|
|
461
|
+
&& funcoesQuebradas < 4 && !PARAR) {
|
|
455
462
|
funcoesQuebradas++;
|
|
456
463
|
if (!subagent) console.log(ident(profundidade) + c.dim(' ⎿ o modelo tropeçou na chamada — tentando de novo'));
|
|
457
464
|
iteration--;
|
|
@@ -474,7 +481,7 @@ async function runAct(userPrompt, opts) {
|
|
|
474
481
|
// anteriores gastaram a cota. Espera a janela virar e repete.
|
|
475
482
|
if (esperas < MAX_ESPERAS) {
|
|
476
483
|
esperas++;
|
|
477
|
-
const ms = esperaDoErro(streamError);
|
|
484
|
+
const ms = esperaDoErro(streamError, esperas);
|
|
478
485
|
if (!subagent) {
|
|
479
486
|
const sp2 = new Spinner(`limite do minuto — continuando em ${Math.round(ms / 1000)}s`).start();
|
|
480
487
|
await new Promise((r) => setTimeout(r, ms));
|
package/lib/claude-engine.js
CHANGED
|
@@ -51,12 +51,35 @@ function extrairDecisao(texto) {
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** JSON com quebra de linha CRUA dentro das aspas — o jeito mais comum de o
|
|
55
|
+
* motor falhar. O Claude escreve o content de um write_file com linhas de
|
|
56
|
+
* verdade em vez de \n, o JSON.parse recusa, e o turno inteiro morria com
|
|
57
|
+
* "decisão que não consegui ler". O conserto é mecânico: dentro de string,
|
|
58
|
+
* controle vira escape. */
|
|
59
|
+
function consertarControles(s) {
|
|
60
|
+
let saida = '';
|
|
61
|
+
let dentro = false, escapa = false;
|
|
62
|
+
for (const c of String(s || '')) {
|
|
63
|
+
if (dentro && !escapa && (c === '\n' || c === '\r' || c === '\t')) {
|
|
64
|
+
saida += c === '\n' ? '\\n' : c === '\r' ? '\\r' : '\\t';
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (escapa) { escapa = false; saida += c; continue; }
|
|
68
|
+
if (c === '\\') { escapa = true; saida += c; continue; }
|
|
69
|
+
if (c === '"') dentro = !dentro;
|
|
70
|
+
saida += c;
|
|
71
|
+
}
|
|
72
|
+
return saida;
|
|
73
|
+
}
|
|
74
|
+
|
|
54
75
|
/** O JSON do bloco, tolerando o que o modelo costuma acrescentar sem querer. */
|
|
55
76
|
function lerJSON(bruto) {
|
|
56
77
|
let s = String(bruto || '').trim();
|
|
57
78
|
// Cerca de markdown em volta do JSON — o modelo põe por hábito.
|
|
58
79
|
s = s.replace(/^```(?:json)?\s*/i, '').replace(/```\s*$/, '').trim();
|
|
59
80
|
try { return JSON.parse(s); } catch {}
|
|
81
|
+
try { return JSON.parse(consertarControles(s)); } catch {}
|
|
82
|
+
s = consertarControles(s);
|
|
60
83
|
// Sobrou texto depois do objeto? Corta no fecha-chaves que casa com o primeiro.
|
|
61
84
|
const abre = s.indexOf('{');
|
|
62
85
|
if (abre === -1) return null;
|
|
@@ -69,7 +92,9 @@ function lerJSON(bruto) {
|
|
|
69
92
|
if (dentro) continue;
|
|
70
93
|
if (c === '{') nivel++;
|
|
71
94
|
else if (c === '}' && --nivel === 0) {
|
|
72
|
-
|
|
95
|
+
const trecho = s.slice(abre, k + 1);
|
|
96
|
+
try { return JSON.parse(trecho); } catch {}
|
|
97
|
+
try { return JSON.parse(consertarControles(trecho)); } catch { return null; }
|
|
73
98
|
}
|
|
74
99
|
}
|
|
75
100
|
return null;
|
|
@@ -268,15 +293,17 @@ function ultimaMensagem(messages) {
|
|
|
268
293
|
function emitirDecisao(textoLido, onEvent) {
|
|
269
294
|
const decisao = extrairDecisao(textoLido);
|
|
270
295
|
if (!decisao) {
|
|
271
|
-
|
|
272
|
-
// protocolo
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
296
|
+
const cru = String(textoLido || '').trim();
|
|
297
|
+
// Traz a marca do protocolo mas o bloco não dá para ler? O motor
|
|
298
|
+
// TENTOU chamar ferramenta e errou a sintaxe. Antes isso virava
|
|
299
|
+
// desistência na tela; agora vira o mesmo erro que o loop já sabe
|
|
300
|
+
// repetir ("Failed to call a function" dispara retry da rodada).
|
|
301
|
+
if (cru.includes(MARCA_INICIO) || cru.includes(MARCA_FIM)) {
|
|
302
|
+
onEvent({ erro: 'Failed to call a function: o motor Claude devolveu um bloco ilegível — repetindo a rodada.' });
|
|
303
|
+
return;
|
|
278
304
|
}
|
|
279
|
-
|
|
305
|
+
// Sem marca nenhuma: é resposta em texto, e texto é resposta válida.
|
|
306
|
+
onEvent({ assistant_message: { content: cru } });
|
|
280
307
|
return;
|
|
281
308
|
}
|
|
282
309
|
if (decisao.tool_call) {
|
|
@@ -361,7 +388,14 @@ function linhaDoHistorico(m) {
|
|
|
361
388
|
|
|
362
389
|
async function requestActViaHeadless(payload, onEvent) {
|
|
363
390
|
const historico = (payload.messages || []).map(linhaDoHistorico).join('\n\n');
|
|
364
|
-
|
|
391
|
+
// O protocolo vai INTEIRO no prompt. A versão anterior dizia "use a skill
|
|
392
|
+
// primocode-engine" e confiava que o headless a carregaria — quando não
|
|
393
|
+
// carregava, o Claude respondia sem bloco nenhum e o turno morria. Prompt
|
|
394
|
+
// maior, mas o Claude Code tem contexto de sobra, e o formato SEMPRE chega.
|
|
395
|
+
const prompt = textoBootstrap()
|
|
396
|
+
+ '\n\nIMPORTANTE: no JSON do bloco, escreva quebras de linha como \\n (escapadas), nunca linha de verdade.'
|
|
397
|
+
+ '\n\n# A conversa até aqui\n\n' + historico
|
|
398
|
+
+ '\n\nResponda com exatamente UM bloco marcado.';
|
|
365
399
|
// --dangerously-skip-permissions: mesma razão do caminho ORCA acima —
|
|
366
400
|
// a skill já proíbe o Claude Code de usar suas próprias ferramentas
|
|
367
401
|
// nativas; a flag só evita que ele pare pedindo permissão pra si mesmo
|
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']
|
|
@@ -511,9 +514,20 @@ async function despachar(name, args, ctx) {
|
|
|
511
514
|
// uma tarefa inteira de tokens para o modelo redescobrir o
|
|
512
515
|
// link. Abrir é determinístico: não se gasta modelo com isso.
|
|
513
516
|
if (r && r.ok !== false && r.ver) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
+
}
|
|
517
531
|
}
|
|
518
532
|
return r;
|
|
519
533
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.11.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": {
|
|
@@ -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
|
+
}
|
package/studio/server.py
CHANGED
|
@@ -324,6 +324,14 @@ def validar_doc(modelo, doc):
|
|
|
324
324
|
'do usuário. O exemplo ensina o FORMATO; o conteúdo é seu: reescreva cada '
|
|
325
325
|
'cena com os fatos da tarefa (a pesquisa que você fez) e crie de novo.' % frase)
|
|
326
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
|
+
|
|
327
335
|
if modelo == "cena":
|
|
328
336
|
cenas = doc.get("cenas")
|
|
329
337
|
if not isinstance(cenas, list) or not cenas:
|
|
@@ -337,9 +345,20 @@ def validar_doc(modelo, doc):
|
|
|
337
345
|
if not isinstance(els, list) or not els:
|
|
338
346
|
return ('cena %d sem "elementos". Cada cena precisa de elementos:[...] — '
|
|
339
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))
|
|
340
352
|
for j, e in enumerate(els):
|
|
341
353
|
if not isinstance(e, dict) or not e.get("tipo"):
|
|
342
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))
|
|
343
362
|
elif modelo == "documento":
|
|
344
363
|
blocos = doc.get("blocos")
|
|
345
364
|
if not isinstance(blocos, list) or not blocos:
|
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
|
|
|
@@ -66,6 +71,16 @@
|
|
|
66
71
|
montarTransporte();
|
|
67
72
|
prepararTrilha();
|
|
68
73
|
tocar();
|
|
74
|
+
// /v/<id>#gravar: o CLI abre assim quando o usuário pede "baixa o
|
|
75
|
+
// vídeo". O navegador pede a permissão de tela (a "aprovação"), o
|
|
76
|
+
// usuário aceita, o vídeo roda do zero gravando e baixa no fim —
|
|
77
|
+
// nenhuma chamada de IA no caminho inteiro.
|
|
78
|
+
if (location.hash.indexOf("gravar") !== -1) {
|
|
79
|
+
setTimeout(function () {
|
|
80
|
+
aviso("aceite a permissão para gravar — o download sai no fim", 4000);
|
|
81
|
+
iniciarGravacao();
|
|
82
|
+
}, 700);
|
|
83
|
+
}
|
|
69
84
|
}
|
|
70
85
|
}
|
|
71
86
|
|
|
@@ -395,8 +410,11 @@
|
|
|
395
410
|
}
|
|
396
411
|
var misto = new MediaStream(faixas);
|
|
397
412
|
pedacos = [];
|
|
398
|
-
|
|
399
|
-
|
|
413
|
+
// MP4 primeiro, quando o navegador sabe gravar mp4 (Safari sabe;
|
|
414
|
+
// Chrome grava webm). O usuário pediu "baixar em mp4": onde dá, dá.
|
|
415
|
+
var tipo = MediaRecorder.isTypeSupported("video/mp4") ? "video/mp4"
|
|
416
|
+
: MediaRecorder.isTypeSupported("video/webm;codecs=vp9,opus")
|
|
417
|
+
? "video/webm;codecs=vp9,opus" : "video/webm";
|
|
400
418
|
gravador = new MediaRecorder(misto, { mimeType: tipo, videoBitsPerSecond: 8000000 });
|
|
401
419
|
gravador.ondataavailable = function (ev) { if (ev.data.size) pedacos.push(ev.data); };
|
|
402
420
|
gravador.onstop = baixarGravacao;
|
|
@@ -419,14 +437,16 @@
|
|
|
419
437
|
}
|
|
420
438
|
|
|
421
439
|
function baixarGravacao() {
|
|
422
|
-
var
|
|
440
|
+
var mime = (gravador && gravador.mimeType) || "video/webm";
|
|
441
|
+
var ext = /mp4/.test(mime) ? ".mp4" : ".webm";
|
|
442
|
+
var blob = new Blob(pedacos, { type: mime.split(";")[0] });
|
|
423
443
|
var url = URL.createObjectURL(blob);
|
|
424
444
|
var a = document.createElement("a");
|
|
425
445
|
a.href = url;
|
|
426
|
-
a.download = (arq.titulo || "video").replace(/[^\w\-]+/g, "-").toLowerCase() +
|
|
446
|
+
a.download = (arq.titulo || "video").replace(/[^\w\-]+/g, "-").toLowerCase() + ext;
|
|
427
447
|
a.click();
|
|
428
448
|
setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
|
|
429
|
-
aviso("vídeo salvo em
|
|
449
|
+
aviso("vídeo salvo em " + ext, 2600);
|
|
430
450
|
}
|
|
431
451
|
|
|
432
452
|
/* -------------------------------------------------------------- print -- */
|
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
|
|