primocode 10.19.0 → 11.0.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/opencode-MIT.txt +24 -0
- package/LICENSES/pptxgenjs-MIT.txt +31 -0
- package/README.md +16 -3
- package/agentes/acessibilidade.md +10 -0
- package/agentes/ambiente.md +10 -0
- package/agentes/api.md +10 -0
- package/agentes/backend.md +10 -0
- package/agentes/copy.md +10 -0
- package/agentes/css-layout.md +10 -0
- package/agentes/dados.md +10 -0
- package/agentes/depuracao.md +10 -0
- package/agentes/design.md +10 -0
- package/agentes/docs.md +10 -0
- package/agentes/frontend.md +10 -0
- package/agentes/geral/capacidades/arquivos.json +132 -0
- package/agentes/geral/capacidades/codigo.json +132 -0
- package/agentes/geral/capacidades/dados.json +126 -0
- package/agentes/geral/capacidades/depuracao.json +126 -0
- package/agentes/geral/capacidades/escrita.json +126 -0
- package/agentes/geral/capacidades/git.json +126 -0
- package/agentes/geral/capacidades/pesquisa.json +126 -0
- package/agentes/geral/capacidades/seguranca.json +132 -0
- package/agentes/geral/capacidades/shell.json +132 -0
- package/agentes/geral/capacidades/web.json +126 -0
- package/agentes/git-pr.md +10 -0
- package/agentes/indice.md +86 -0
- package/agentes/nextjs.md +10 -0
- package/agentes/node.md +10 -0
- package/agentes/performance.md +10 -0
- package/agentes/pesquisa.md +10 -0
- package/agentes/python.md +10 -0
- package/agentes/react.md +10 -0
- package/agentes/refatoracao.md +10 -0
- package/agentes/revisor.md +10 -0
- package/agentes/seguranca.md +10 -0
- package/agentes/sql.md +10 -0
- package/agentes/testes.md +10 -0
- package/agentes/typescript.md +10 -0
- package/agentes/ux.md +10 -0
- package/bin/primocode.js +65 -8
- package/editor/helpers/camera_viva.py +28 -2
- package/lib/act.js +79 -10
- package/lib/agentes/index.js +146 -0
- package/lib/agentes/nucleo.js +18 -0
- package/lib/catalogo.js +12 -2
- package/lib/cli/review.js +64 -0
- package/lib/cli/subcomandos.js +34 -0
- package/lib/cli/tentativas.js +107 -0
- package/lib/cli/util.js +51 -0
- package/lib/comandos-extra.js +4 -2
- package/lib/editor-pro-maquina.js +165 -0
- package/lib/editor-pro.js +10 -7
- package/lib/entregaveis/cli.js +26 -0
- package/lib/entregaveis/especialistas.js +50 -0
- package/lib/entregaveis/nucleo.js +111 -0
- package/lib/entregaveis/slides.js +221 -0
- package/lib/estudio/materiais.js +172 -0
- package/lib/fundo.js +34 -1
- package/lib/kits.js +41 -2
- package/lib/nomes.js +6 -0
- package/lib/placar.js +173 -0
- package/lib/plugin/autoteste.js +99 -0
- package/lib/plugin/edicao-visual.js +133 -0
- package/lib/plugin/pesquisar.js +92 -0
- package/lib/plugin/preview-cli.js +38 -0
- package/lib/plugin/preview.js +89 -0
- package/lib/plugin/tunel.js +27 -0
- package/lib/review.js +134 -0
- package/lib/session/compactacao.js +124 -0
- package/lib/statusline.js +52 -0
- package/lib/tool/diagnostico.js +100 -0
- package/lib/tool/perguntar.js +60 -0
- package/lib/tool/reparar-entrada.js +66 -0
- package/lib/tool/web.js +102 -0
- package/lib/tools.js +20 -1
- package/lib/ui.js +1 -1
- package/materiais/saas-linear/base.ts +84 -0
- package/materiais/saas-linear/camera/Camera.tsx +38 -0
- package/materiais/saas-linear/fontes/OFL-Inter.txt +93 -0
- package/materiais/saas-linear/fontes/OFL-Newsreader.txt +93 -0
- package/materiais/saas-linear/fontes/inter-400.woff2 +0 -0
- package/materiais/saas-linear/fontes/inter-600.woff2 +0 -0
- package/materiais/saas-linear/fontes/newsreader-400.woff2 +0 -0
- package/materiais/saas-linear/formas/Agenda.tsx +46 -0
- package/materiais/saas-linear/formas/CaixaPrompt.tsx +41 -0
- package/materiais/saas-linear/formas/CartoesIntegracao.tsx +57 -0
- package/materiais/saas-linear/formas/Cursor.tsx +40 -0
- package/materiais/saas-linear/formas/Menu.tsx +46 -0
- package/materiais/saas-linear/formas/Monitor.tsx +28 -0
- package/materiais/saas-linear/paleta.json +18 -0
- package/materiais/saas-linear/pecas.json +590 -0
- package/materiais/saas-linear/regras.md +40 -0
- package/materiais/saas-linear/sons/Som.tsx +39 -0
- package/materiais/saas-linear/sons/assinatura.mp3 +0 -0
- package/materiais/saas-linear/sons/clique.mp3 +0 -0
- package/materiais/saas-linear/sons/impacto.mp3 +0 -0
- package/materiais/saas-linear/sons/pop-cartao.mp3 +0 -0
- package/materiais/saas-linear/sons/pop-palavra.mp3 +0 -0
- package/materiais/saas-linear/sons/subida.mp3 +0 -0
- package/materiais/saas-linear/sons/swoosh.mp3 +0 -0
- package/materiais/saas-linear/sons/trilha.mp3 +0 -0
- package/materiais/saas-linear/sons/whoosh-entra.mp3 +0 -0
- package/materiais/saas-linear/sons/whoosh-sai.mp3 +0 -0
- package/materiais/saas-linear/texturas/Fundo.tsx +18 -0
- package/materiais/saas-linear/texturas/Particulas.tsx +22 -0
- package/materiais/saas-linear/tipografia/FraseSerifa.tsx +25 -0
- package/materiais/saas-linear/tipografia/Marca.tsx +20 -0
- package/materiais/saas-linear/tipografia/MarcaFinal.tsx +22 -0
- package/materiais/saas-linear/tipografia/Saudacao.tsx +27 -0
- package/materiais/saas-linear/transicoes/Cena.tsx +38 -0
- package/materiais/saas-linear/transicoes/CorteImpacto.tsx +16 -0
- package/package.json +3 -1
- package/skills/deck-conferido/SKILL.md +29 -0
- package/vendor/pptxgenjs/carregar.js +12 -0
- package/vendor/pptxgenjs/jszip.min.js +13 -0
- package/vendor/pptxgenjs/pptxgen.cjs.js +7370 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review.js — `primocode review --pr <n> [--postar]` (ideia do Bugbot): lê o diff do PR com `gh`, pede ao modelo
|
|
3
|
+
* uma revisão e devolve comentários em markdown. Postar no PR só com --postar (o padrão só mostra e grava em
|
|
4
|
+
* .primocode/review/pr-<n>.md). Nada de aprovar nem pedir mudanças no PR: só um comentário.
|
|
5
|
+
*
|
|
6
|
+
* NÃO é o /review de dentro da sessão (esse é da branch rebuild/paridade-cc). Este roda sem sessão, em CI.
|
|
7
|
+
* `gh` e o modelo são injetáveis. O workflow do GitHub: .github/workflows/primocode-review.yml.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { sh, perguntarPadrao } = require('./util.js');
|
|
14
|
+
|
|
15
|
+
const MAX_DIFF = 60000;
|
|
16
|
+
const SISTEMA = 'Você é um revisor de código. Leia o diff e aponte SÓ problemas reais: bugs, falhas de segurança, casos de borda sem tratamento, testes que faltam. '
|
|
17
|
+
+ 'Formato markdown: uma seção "### arquivo:linha — título curto" por problema, com 1-3 frases do porquê e a correção sugerida. '
|
|
18
|
+
+ 'Ordene do mais grave ao menos. Sem elogio, sem resumir o diff. Se não achou nada relevante, escreva exatamente: "Nada relevante encontrado."';
|
|
19
|
+
|
|
20
|
+
function analisar(args) {
|
|
21
|
+
const r = { pr: null, postar: false, erro: null };
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
if (args[i] === '--pr') r.pr = args[++i];
|
|
24
|
+
else if (args[i].startsWith('--pr=')) r.pr = args[i].slice(5);
|
|
25
|
+
else if (args[i] === '--postar') r.postar = true;
|
|
26
|
+
}
|
|
27
|
+
if (!r.pr || !/^\d+$/.test(String(r.pr))) r.erro = 'Uso: primocode review --pr <número> [--postar]';
|
|
28
|
+
return r;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function cortar(diff) {
|
|
32
|
+
if (diff.length <= MAX_DIFF) return { diff, cortado: false };
|
|
33
|
+
return { diff: diff.slice(0, MAX_DIFF), cortado: true };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function revisar({ pr, dir = process.cwd(), postar = false, gh = (a) => sh('gh', a, { cwd: dir }), perguntar = perguntarPadrao }) {
|
|
37
|
+
const d = gh(['pr', 'diff', String(pr)]);
|
|
38
|
+
if (!d.ok) return { ok: false, erro: `não consegui ler o diff do PR #${pr} (${(d.err || 'gh falhou').trim().split('\n')[0]})` };
|
|
39
|
+
if (!d.out.trim()) return { ok: false, erro: `o PR #${pr} não tem diff` };
|
|
40
|
+
const { diff, cortado } = cortar(d.out);
|
|
41
|
+
const corpo = String(await perguntar(null, SISTEMA, `Diff do PR #${pr}:\n\n${diff}`)).trim();
|
|
42
|
+
const md = `## Revisão do PrimoCode — PR #${pr}\n\n${corpo}\n` + (cortado ? `\n_O diff passou de ${MAX_DIFF} caracteres: revisei só o começo._\n` : '')
|
|
43
|
+
+ '\n<sub>Revisão automática: pode errar. Quem decide é a pessoa.</sub>\n';
|
|
44
|
+
const arquivo = path.join(dir, '.primocode', 'review', `pr-${pr}.md`);
|
|
45
|
+
fs.mkdirSync(path.dirname(arquivo), { recursive: true }); fs.writeFileSync(arquivo, md);
|
|
46
|
+
let postado = false, erroPost = null;
|
|
47
|
+
if (postar) {
|
|
48
|
+
const r = gh(['pr', 'comment', String(pr), '--body-file', arquivo]);
|
|
49
|
+
postado = r.ok; if (!r.ok) erroPost = (r.err || '').trim().split('\n')[0];
|
|
50
|
+
}
|
|
51
|
+
return { ok: true, md, arquivo, postado, erroPost };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function main(args, deps = {}) {
|
|
55
|
+
const a = analisar(args);
|
|
56
|
+
if (a.erro) { console.error(a.erro); return 1; }
|
|
57
|
+
const r = await revisar({ pr: a.pr, postar: a.postar, ...deps });
|
|
58
|
+
if (!r.ok) { console.error(' ' + r.erro); return 1; }
|
|
59
|
+
console.log(r.md);
|
|
60
|
+
console.log(a.postar ? (r.postado ? ` Comentário postado no PR #${a.pr}.` : ` Não consegui postar: ${r.erroPost}`) : ` Não postei nada (use --postar). Salvo em ${r.arquivo}`);
|
|
61
|
+
return a.postar && !r.postado ? 1 : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { analisar, revisar, main, SISTEMA, MAX_DIFF };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subcomandos.js — as entradas `primocode <nome> ...` que saem ANTES da sessão (sem tela):
|
|
3
|
+
* autoteste · preview · review · pesquisar
|
|
4
|
+
* Cada uma chama o módulo de lib/plugin/ ou lib/cli/. O bin só faz `if (NOMES.includes(argv[2]))`.
|
|
5
|
+
*/
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { sh } = require('./util.js');
|
|
10
|
+
|
|
11
|
+
const NOMES = ['autoteste', 'preview', 'review', 'pesquisar'];
|
|
12
|
+
|
|
13
|
+
async function autoteste(args) {
|
|
14
|
+
const A = require('../plugin/autoteste.js');
|
|
15
|
+
const dir = path.resolve(args.find((a) => !a.startsWith('-')) || process.cwd());
|
|
16
|
+
const bin = path.join(__dirname, '..', '..', 'bin', 'primocode.js');
|
|
17
|
+
const r = await A.testar({
|
|
18
|
+
dir, log: (l) => console.log(' ' + l),
|
|
19
|
+
consertar: async (erros, prints) => { console.log(' mandando as falhas pro agente consertar…'); sh(process.execPath, [bin, '-p', A.pedidoDeConserto(erros, prints)], { cwd: dir }); },
|
|
20
|
+
});
|
|
21
|
+
if (r.ok) console.log(` Passou${r.rodadas ? ` depois de ${r.rodadas} conserto(s)` : ''}.`);
|
|
22
|
+
else { console.log(' ' + r.erro); (r.relatorio.slice(-1)[0] || { erros: [] }).erros.forEach((e) => console.log(' - ' + e)); }
|
|
23
|
+
return r.ok ? 0 : 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function main(nome, args) {
|
|
27
|
+
if (nome === 'autoteste') return autoteste(args);
|
|
28
|
+
if (nome === 'preview') return require('../plugin/preview-cli.js').main(args);
|
|
29
|
+
if (nome === 'review') return require('./review.js').main(args);
|
|
30
|
+
if (nome === 'pesquisar') return require('../plugin/pesquisar.js').main(args);
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { NOMES, main };
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tentativas.js — `primocode --tentativas N "pedido"` (ideia do Codex, "best of N"): roda a tarefa N vezes, cada
|
|
3
|
+
* uma num git worktree separado, roda o check do projeto em cada uma, e fica com a que passou e mexeu menos.
|
|
4
|
+
*
|
|
5
|
+
* N: padrão 3, máximo 4. Check: `npm test` se o package.json tiver script "test" (ou --check "<comando>").
|
|
6
|
+
* Uma linha por tentativa: "#2 passou · +12 −3 · 2 arquivos".
|
|
7
|
+
*
|
|
8
|
+
* `executar(dirDoWorktree, pedido)` e `checar(dir)` são injetáveis (os testes usam falsos; o padrão roda
|
|
9
|
+
* `primocode -p`). O vencedor vira um patch aplicado na pasta principal; os worktrees são apagados no fim.
|
|
10
|
+
*/
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const os = require('os');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { sh } = require('./util.js');
|
|
17
|
+
|
|
18
|
+
const PADRAO = 3, MAX = 4;
|
|
19
|
+
const limitar = (n) => { const v = Number(n); return Number.isFinite(v) && v >= 1 ? Math.min(MAX, Math.floor(v)) : PADRAO; };
|
|
20
|
+
|
|
21
|
+
/** Tira `--tentativas N` / `--tentativas=N` / `--check cmd` dos argumentos. null = a flag não veio. */
|
|
22
|
+
function analisar(argv) {
|
|
23
|
+
const r = { tentativas: null, check: null, resto: [] };
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const a = argv[i];
|
|
26
|
+
if (a === '--tentativas') { r.tentativas = limitar(argv[i + 1]); i++; }
|
|
27
|
+
else if (a.startsWith('--tentativas=')) r.tentativas = limitar(a.split('=')[1]);
|
|
28
|
+
else if (a === '--check') { r.check = argv[++i]; }
|
|
29
|
+
else r.resto.push(a);
|
|
30
|
+
}
|
|
31
|
+
return r;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** O comando de check do projeto, ou null. */
|
|
35
|
+
function comandoDeCheck(dir, forcado) {
|
|
36
|
+
if (forcado) return ['sh', ['-c', forcado]];
|
|
37
|
+
try {
|
|
38
|
+
const p = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
39
|
+
const t = p.scripts && p.scripts.test;
|
|
40
|
+
if (t && !/no test specified/.test(t)) return ['npm', ['test', '--silent']];
|
|
41
|
+
} catch { /* sem package.json */ }
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Tamanho do diff do worktree contra o HEAD, contando arquivos novos. { mais, menos, arquivos, patch }. */
|
|
46
|
+
function medirDiff(dir) {
|
|
47
|
+
sh('git', ['add', '-A', '-N'], { cwd: dir });
|
|
48
|
+
const ns = sh('git', ['diff', '--numstat', 'HEAD'], { cwd: dir }).out.trim().split('\n').filter(Boolean);
|
|
49
|
+
let mais = 0, menos = 0;
|
|
50
|
+
for (const l of ns) { const [a, b] = l.split('\t'); mais += Number(a) || 0; menos += Number(b) || 0; }
|
|
51
|
+
return { mais, menos, arquivos: ns.length, patch: sh('git', ['diff', '--binary', 'HEAD'], { cwd: dir }).out };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const linha = (t) => `#${t.n} ${t.erro ? 'falhou (' + t.erro + ')' : t.passou ? 'passou' : 'reprovou no check'}`
|
|
55
|
+
+ (t.erro ? '' : ` · +${t.diff.mais} −${t.diff.menos} · ${t.diff.arquivos} arquivo${t.diff.arquivos === 1 ? '' : 's'}`);
|
|
56
|
+
|
|
57
|
+
/** A melhor: passou, e com o menor diff (linhas mexidas, depois arquivos). null se nenhuma passou. */
|
|
58
|
+
function escolher(ts) {
|
|
59
|
+
const ok = ts.filter((t) => t.passou && !t.erro);
|
|
60
|
+
ok.sort((a, b) => (a.diff.mais + a.diff.menos) - (b.diff.mais + b.diff.menos) || a.diff.arquivos - b.diff.arquivos || a.n - b.n);
|
|
61
|
+
return ok[0] || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function rodar({ dir, pedido, n = PADRAO, check, executar, checar, log = () => {}, aplicar = true }) {
|
|
65
|
+
n = limitar(n);
|
|
66
|
+
if (!sh('git', ['rev-parse', '--is-inside-work-tree'], { cwd: dir }).ok) return { ok: false, erro: 'precisa de um repositório git (as tentativas usam git worktree)' };
|
|
67
|
+
const cmd = comandoDeCheck(dir, check);
|
|
68
|
+
executar = executar || ((d, p) => sh(process.execPath, [path.join(__dirname, '..', '..', 'bin', 'primocode.js'), '-p', p], { cwd: d }));
|
|
69
|
+
checar = checar || ((d) => (cmd ? sh(cmd[0], cmd[1], { cwd: d }).ok : true));
|
|
70
|
+
const base = fs.mkdtempSync(path.join(os.tmpdir(), 'primocode-tentativas-'));
|
|
71
|
+
const ts = [];
|
|
72
|
+
for (let i = 1; i <= n; i++) {
|
|
73
|
+
const wt = path.join(base, `t${i}`);
|
|
74
|
+
const t = { n: i, dir: wt };
|
|
75
|
+
const add = sh('git', ['worktree', 'add', '--detach', wt, 'HEAD'], { cwd: dir });
|
|
76
|
+
if (!add.ok) { t.erro = 'worktree: ' + add.err.trim().split('\n')[0]; t.diff = { mais: 0, menos: 0, arquivos: 0, patch: '' }; ts.push(t); log(linha(t)); continue; }
|
|
77
|
+
try {
|
|
78
|
+
await executar(wt, pedido);
|
|
79
|
+
t.diff = medirDiff(wt);
|
|
80
|
+
t.passou = t.diff.arquivos > 0 && !!(await checar(wt));
|
|
81
|
+
} catch (e) { t.erro = String(e.message || e).slice(0, 60); t.diff = t.diff || { mais: 0, menos: 0, arquivos: 0, patch: '' }; }
|
|
82
|
+
ts.push(t); log(linha(t));
|
|
83
|
+
}
|
|
84
|
+
const melhor = escolher(ts);
|
|
85
|
+
let aplicado = false, patchPath = null;
|
|
86
|
+
if (melhor) {
|
|
87
|
+
patchPath = path.join(dir, '.primocode', 'tentativas', `${Date.now()}-melhor.patch`);
|
|
88
|
+
fs.mkdirSync(path.dirname(patchPath), { recursive: true }); fs.writeFileSync(patchPath, melhor.diff.patch);
|
|
89
|
+
if (aplicar) aplicado = sh('git', ['apply', patchPath], { cwd: dir }).ok;
|
|
90
|
+
}
|
|
91
|
+
for (const t of ts) { sh('git', ['worktree', 'remove', '--force', t.dir], { cwd: dir }); }
|
|
92
|
+
try { fs.rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
93
|
+
return { ok: !!melhor, melhor: melhor && melhor.n, resumo: ts.map(linha), aplicado, patchPath,
|
|
94
|
+
erro: melhor ? null : 'nenhuma tentativa passou no check — nada foi aplicado' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main(argv, deps = {}) {
|
|
98
|
+
const a = analisar(argv);
|
|
99
|
+
const pedido = a.resto.filter((x) => !x.startsWith('-')).join(' ').trim();
|
|
100
|
+
if (!pedido) { console.error('Uso: primocode --tentativas 3 "pedido" [--check "npm test"]'); return 1; }
|
|
101
|
+
const r = await rodar({ dir: process.cwd(), pedido, n: a.tentativas, check: a.check, log: (l) => console.log(' ' + l), ...deps });
|
|
102
|
+
if (r.ok) console.log(` Fiquei com a #${r.melhor}. ` + (r.aplicado ? 'Aplicada na pasta.' : `Não consegui aplicar sozinho; o patch está em ${r.patchPath}`));
|
|
103
|
+
else console.log(' ' + r.erro);
|
|
104
|
+
return r.ok ? 0 : 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { analisar, limitar, comandoDeCheck, medirDiff, escolher, rodar, main, PADRAO, MAX };
|
package/lib/cli/util.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* util.js — peças pequenas que os subcomandos `primocode issue|review|headless|...` dividem.
|
|
3
|
+
* Tudo que fala com o mundo (shell, modelo) é injetável: os testes trocam por falsos.
|
|
4
|
+
*/
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { spawnSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
/** Roda um comando sem shell. Devolve { ok, status, out, err }. */
|
|
10
|
+
function sh(cmd, args = [], { cwd, input, timeout = 10 * 60 * 1000, env } = {}) {
|
|
11
|
+
const r = spawnSync(cmd, args, { cwd, input, encoding: 'utf8', timeout, env: env || process.env, maxBuffer: 64 * 1024 * 1024 });
|
|
12
|
+
return { ok: r.status === 0, status: r.status, out: r.stdout || '', err: (r.stderr || '') + (r.error ? String(r.error.message) : '') };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const temComando = (nome) => sh(process.platform === 'win32' ? 'where' : 'which', [nome]).ok;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* O modelo, de fora da sessão: pergunta única, sem ferramentas.
|
|
19
|
+
* `modelo` vazio = o modelo da conta. Falha com mensagem clara (nunca inventa resposta).
|
|
20
|
+
*/
|
|
21
|
+
async function perguntarPadrao(modelo, sistema, usuario) {
|
|
22
|
+
const cfg = require('../config.js');
|
|
23
|
+
const api = require('../api.js');
|
|
24
|
+
const conf = cfg.loadConfig();
|
|
25
|
+
let resposta = '';
|
|
26
|
+
await api.requestChat(conf.server, {
|
|
27
|
+
prompt: sistema ? `${sistema}\n\n${usuario}` : usuario, history: [], model: modelo || conf.model, token: conf.token || cfg.tokenDaConta(),
|
|
28
|
+
}, (evt) => { if (evt.delta) resposta += evt.delta; });
|
|
29
|
+
return resposta;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Tira cerca de código (```json ... ```) e acha o primeiro JSON do texto. */
|
|
33
|
+
function acharJson(texto) {
|
|
34
|
+
const t = String(texto || '');
|
|
35
|
+
const cerca = t.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
36
|
+
const candidatos = [cerca && cerca[1], t];
|
|
37
|
+
for (const c of candidatos) {
|
|
38
|
+
if (!c) continue;
|
|
39
|
+
const i = c.search(/[[{]/);
|
|
40
|
+
if (i < 0) continue;
|
|
41
|
+
for (let fim = c.length; fim > i; fim = c.lastIndexOf(c[i] === '{' ? '}' : ']', fim - 1) + 1) {
|
|
42
|
+
if (fim <= 0) break;
|
|
43
|
+
try { return JSON.parse(c.slice(i, fim)); } catch { /* tenta mais curto */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const slug = (s) => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'tarefa';
|
|
50
|
+
|
|
51
|
+
module.exports = { sh, temComando, perguntarPadrao, acharJson, slug };
|
package/lib/comandos-extra.js
CHANGED
|
@@ -379,6 +379,7 @@ function medirEvento(b, evt) {
|
|
|
379
379
|
b.tokEnviados += Number(u.prompt_tokens || u.input_tokens || 0);
|
|
380
380
|
b.tokRecebidos += Number(u.completion_tokens || u.output_tokens || 0);
|
|
381
381
|
b.exatos++;
|
|
382
|
+
b.tokEmCache = (b.tokEmCache || 0) + Number(u.cached_tokens || (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || u.cache_read_input_tokens || 0);
|
|
382
383
|
}
|
|
383
384
|
}
|
|
384
385
|
|
|
@@ -415,15 +416,16 @@ function instalarContador(api, cont, quemAtende) {
|
|
|
415
416
|
/** As linhas do /custo: por provedor, e o total. */
|
|
416
417
|
function resumoCusto(cont) {
|
|
417
418
|
const linhas = [];
|
|
418
|
-
let total = { pedidos: 0, enviados: 0, recebidos: 0 };
|
|
419
|
+
let total = { pedidos: 0, enviados: 0, recebidos: 0, emCache: 0 };
|
|
419
420
|
for (const [provedor, b] of Object.entries(cont.porProvedor)) {
|
|
420
421
|
// Com `usage` em todos os pedidos, vale o número do provedor; senão a
|
|
421
422
|
// estimativa inteira, para não somar exato com chute.
|
|
422
423
|
const exato = b.exatos > 0 && b.exatos === b.pedidos;
|
|
423
424
|
const enviados = exato ? b.tokEnviados : Math.ceil(b.charsEnviados / 4);
|
|
424
425
|
const recebidos = exato ? b.tokRecebidos : Math.ceil(b.charsRecebidos / 4);
|
|
425
|
-
linhas.push({ provedor, pedidos: b.pedidos, enviados, recebidos, exato });
|
|
426
|
+
linhas.push({ provedor, pedidos: b.pedidos, enviados, recebidos, exato, emCache: exato ? (b.tokEmCache || 0) : 0 });
|
|
426
427
|
total.pedidos += b.pedidos; total.enviados += enviados; total.recebidos += recebidos;
|
|
428
|
+
total.emCache += exato ? (b.tokEmCache || 0) : 0;
|
|
427
429
|
}
|
|
428
430
|
return { linhas, total };
|
|
429
431
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* A máquina de vídeo do editor: entende cada frase, escolhe gancho e estilo pelo CONTEÚDO (e pela variante),
|
|
2
|
+
e confere o plano com uma régua numérica (criticar). Sem rede, sem Python: só texto e números.
|
|
3
|
+
Ver docs/rebuild/editor-maquina-arquitetura.md. */
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const FRACAS = new Set('a o e é de da do das dos em no na nos nas um uma uns umas que para pra por com como eu você ele ela isso esse essa este esta hoje vou vai ou se mas'.split(' '));
|
|
7
|
+
const NEGACAO = /\b(nunca|n[aã]o|ningu[eé]m|nada|jamais|zero|sem)\b/i;
|
|
8
|
+
const NUMERO = /\d|\b(dobro|metade|triplo|mil|milh[aã]o|dez|cem|vinte|trinta)\b/i;
|
|
9
|
+
const PROMESSA = /\b(como|jeito|segredo|passo|m[eé]todo|f[aá]cil|r[aá]pido|grátis|gr[aá]tis|consegui|descobri)\b/i;
|
|
10
|
+
const URGENCIA = /\b(agora|hoje|[uú]ltim[ao]s?|acaba|corre|r[aá]pido|j[aá])\b/i;
|
|
11
|
+
const EMOCAO_FORTE = /\b(erro|medo|perd[ei]|caro|problema|pior|absurdo|chocante|incr[ií]vel|louco|nunca|dobro|segredo)\b/i;
|
|
12
|
+
|
|
13
|
+
const limpar = (l) => String(l || '').replace(/^[\d.]+-[\d.]+\s+/, '').trim();
|
|
14
|
+
const tempos = (l) => { const m = /^([\d.]+)-([\d.]+)/.exec(String(l || '')); return m ? [Number(m[1]), Number(m[2])] : [null, null]; };
|
|
15
|
+
|
|
16
|
+
/** O que cada frase quer: intenção, emoção, palavra-chave e energia (0 a 1). Tudo medido no texto. */
|
|
17
|
+
function entenderFrases(fala) {
|
|
18
|
+
return (fala || []).map((linha, i) => {
|
|
19
|
+
const texto = limpar(linha);
|
|
20
|
+
const [t0, t1] = tempos(linha);
|
|
21
|
+
const palavras = texto.replace(/[.,!?;:]/g, '').split(/\s+/).filter(Boolean);
|
|
22
|
+
const fortes = palavras.filter((p) => !FRACAS.has(p.toLowerCase()) && (p.length > 2 || /\d/.test(p) || /^[A-ZÀ-Ú]{2}$/.test(p)));
|
|
23
|
+
const chave = fortes.slice().sort((a, b) => (NUMERO.test(b) - NUMERO.test(a)) || (b.length - a.length))[0] || null;
|
|
24
|
+
let intencao = 'afirma';
|
|
25
|
+
if (/\?\s*$/.test(texto)) intencao = 'pergunta';
|
|
26
|
+
else if (NUMERO.test(texto)) intencao = 'prova';
|
|
27
|
+
else if (NEGACAO.test(texto)) intencao = 'nega';
|
|
28
|
+
else if (PROMESSA.test(texto)) intencao = 'promete';
|
|
29
|
+
else if (URGENCIA.test(texto)) intencao = 'urge';
|
|
30
|
+
const energia = Math.min(1, 0.25 + (/!/.test(texto) ? 0.25 : 0) + (EMOCAO_FORTE.test(texto) ? 0.25 : 0) + (NUMERO.test(texto) ? 0.15 : 0) + (NEGACAO.test(texto) ? 0.1 : 0));
|
|
31
|
+
return { i, texto, t0, t1, palavras, fortes, chave, intencao, energia, emocao: energia >= 0.6 ? 'forte' : energia >= 0.4 ? 'media' : 'calma' };
|
|
32
|
+
}).filter((f) => f.texto);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const marcar = (ps, ult) => (ps.length ? ps.join(' ') + ' ' : '') + `*${ult}*`;
|
|
36
|
+
|
|
37
|
+
/** Candidatos a gancho, de tipos DIFERENTES, tirados das primeiras frases (o gancho pode nascer de qualquer uma). */
|
|
38
|
+
function ganchosCandidatos(fala) {
|
|
39
|
+
const frases = entenderFrases(fala);
|
|
40
|
+
const out = [];
|
|
41
|
+
const jaTem = new Set();
|
|
42
|
+
const por = (tipo, f, nota, fn) => {
|
|
43
|
+
if (!f || !f.fortes.length) return;
|
|
44
|
+
const texto = fn(f);
|
|
45
|
+
if (!texto || jaTem.has(texto.toLowerCase())) return;
|
|
46
|
+
jaTem.add(texto.toLowerCase());
|
|
47
|
+
out.push({ tipo, texto, de: f.i, nota });
|
|
48
|
+
};
|
|
49
|
+
for (const f of frases.slice(0, 6)) {
|
|
50
|
+
if (f.intencao === 'prova') por('numero', f, 3 + f.energia, (x) => { const c = x.fortes.find((p) => NUMERO.test(p)) || x.chave; const ix = x.fortes.indexOf(c); return marcar(x.fortes.slice(Math.max(0, ix - 2), ix), c); });
|
|
51
|
+
if (f.intencao === 'nega') por('negacao', f, 2.5 + f.energia, (x) => marcar(x.fortes.slice(-3, -1), x.fortes[x.fortes.length - 1]));
|
|
52
|
+
if (f.intencao === 'pergunta') por('pergunta', f, 2 + f.energia, (x) => marcar(x.fortes.slice(0, 3).slice(0, -1), x.fortes.slice(0, 3).slice(-1)[0]));
|
|
53
|
+
if (f.intencao === 'promete') por('promessa', f, 2 + f.energia, (x) => marcar(x.fortes.slice(0, 3), x.chave || x.fortes[x.fortes.length - 1]).replace(/\*\*/g, '*'));
|
|
54
|
+
if (f.intencao === 'urge') por('urgencia', f, 1.5 + f.energia, (x) => marcar(x.fortes.slice(-3, -1), x.fortes[x.fortes.length - 1]));
|
|
55
|
+
}
|
|
56
|
+
if (frases[0]) por('palavra-forte', frases[0], 1, (x) => { const fs = x.fortes.slice(-4); const u = fs.pop(); return marcar(fs, u); });
|
|
57
|
+
return out.sort((a, b) => b.nota - a.nota);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** O gancho da variante `variante` (0 = o de maior nota; 1 = o próximo TIPO; e assim por diante, girando). */
|
|
61
|
+
function escolherGancho(fala, variante = 0) {
|
|
62
|
+
const c = ganchosCandidatos(fala);
|
|
63
|
+
if (!c.length) return null;
|
|
64
|
+
return c[((variante % c.length) + c.length) % c.length].texto;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const hash = (s) => { let h = 2166136261; for (const ch of String(s)) { h ^= ch.charCodeAt(0); h = Math.imul(h, 16777619); } return h >>> 0; };
|
|
68
|
+
|
|
69
|
+
/** O estilo vem da energia do que é dito, não de um padrão fixo: conteúdo forte pede escuro/laranja; calmo, limpo.
|
|
70
|
+
A variante gira entre os estilos que servem, e o mesmo roteiro sempre dá a mesma resposta (sem sorteio). */
|
|
71
|
+
function escolherEstilo(fala, variante = 0) {
|
|
72
|
+
const fr = entenderFrases(fala);
|
|
73
|
+
const e = fr.length ? fr.reduce((s, f) => s + f.energia, 0) / fr.length : 0.3;
|
|
74
|
+
const ordem = e >= 0.55 ? ['laranja', 'noite', 'papelaria', 'limpo'] : e >= 0.38 ? ['noite', 'laranja', 'limpo', 'papelaria'] : ['limpo', 'papelaria', 'noite', 'laranja'];
|
|
75
|
+
const sal = hash(fr.map((f) => f.texto).join('|')) % 2;
|
|
76
|
+
return ordem[(variante + sal) % ordem.length];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── a régua (autocrítica) ────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
const REGUA = {
|
|
82
|
+
parado_max_s: 3.0, // nenhum trecho maior que isso sem mudança visual
|
|
83
|
+
tratamentos_min_por_min: 12, // eventos visuais (peça, bloco, zoom) por minuto
|
|
84
|
+
tipos_distintos_min: 4, // famílias de tratamento diferentes no vídeo
|
|
85
|
+
sfx_no_corte_s: 0.15, // todo corte de bloco tem som a até 150 ms
|
|
86
|
+
sfx_min_por_min: 10, // camadas de som por minuto
|
|
87
|
+
gancho_ate_s: 1.0, // o gancho aparece no primeiro segundo
|
|
88
|
+
zooms_distintos_min: 3, // escalas de enquadramento diferentes
|
|
89
|
+
legenda_sobre_rosto: 0, // trechos com texto sobre o rosto
|
|
90
|
+
};
|
|
91
|
+
const POS_ROSTO = new Set(['centro', 'rosto', 'meio-rosto']);
|
|
92
|
+
const num = (x) => (typeof x === 'number' && isFinite(x) ? x : null);
|
|
93
|
+
|
|
94
|
+
/** Pontos do tempo em que a imagem muda: começo e fim de peça, bloco e zoom. */
|
|
95
|
+
function mudancasVisuais(cfg, duracao) {
|
|
96
|
+
const pts = [0, duracao];
|
|
97
|
+
const tipos = new Set();
|
|
98
|
+
let eventos = 0;
|
|
99
|
+
for (const a of cfg.animacoes || []) {
|
|
100
|
+
const e = num(a.em) ?? num(a.depois_de); const s = num(a.sai);
|
|
101
|
+
if (e != null) { pts.push(e); eventos++; } if (s != null) pts.push(s);
|
|
102
|
+
tipos.add('anim:' + (a.tipo || '?'));
|
|
103
|
+
}
|
|
104
|
+
for (const b of cfg.blocos || []) {
|
|
105
|
+
const d = num(b.de); const t = num(b.ate);
|
|
106
|
+
if (d != null) { pts.push(d); eventos++; } if (t != null) pts.push(t);
|
|
107
|
+
tipos.add('bloco:' + (b.cena || '?'));
|
|
108
|
+
for (const z of b.zooms || []) { if (num(z.em) != null) { pts.push(z.em); eventos++; } if (num(z.ate) != null) pts.push(z.ate); tipos.add('punch'); }
|
|
109
|
+
}
|
|
110
|
+
return { pts: pts.filter((p) => p >= 0 && p <= duracao).sort((a, b) => a - b), tipos, eventos };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Dá nota (0 a 100) a um plano montado (o projeto.json) e devolve as falhas, cada uma com o número e o que fazer. */
|
|
114
|
+
function criticar(cfg, { duracao } = {}, regua = REGUA) {
|
|
115
|
+
const dur = duracao || Math.max(0, ...(cfg.blocos || []).map((b) => num(b.ate) || 0), ...(cfg.animacoes || []).map((a) => num(a.sai) || 0));
|
|
116
|
+
const falhas = [];
|
|
117
|
+
const m = {};
|
|
118
|
+
if (!dur) return { nota: 0, falhas: [{ regra: 'duracao', valor: 0, limite: '>0', conserto: 'sem duração não há o que conferir' }], metricas: m };
|
|
119
|
+
const { pts, tipos, eventos } = mudancasVisuais(cfg, dur);
|
|
120
|
+
let parado = 0; let onde = 0;
|
|
121
|
+
for (let k = 1; k < pts.length; k++) if (pts[k] - pts[k - 1] > parado) { parado = pts[k] - pts[k - 1]; onde = pts[k - 1]; }
|
|
122
|
+
m.parado_max_s = Math.round(parado * 100) / 100;
|
|
123
|
+
if (parado > regua.parado_max_s) falhas.push({ regra: 'parado_max_s', valor: m.parado_max_s, limite: regua.parado_max_s, em: onde, conserto: `ponha peça, zoom ou b-roll a partir de ${onde.toFixed(1)} s` });
|
|
124
|
+
m.tratamentos_por_min = Math.round((eventos / dur) * 60 * 10) / 10;
|
|
125
|
+
if (m.tratamentos_por_min < regua.tratamentos_min_por_min) falhas.push({ regra: 'tratamentos_por_min', valor: m.tratamentos_por_min, limite: regua.tratamentos_min_por_min, conserto: 'mais mudanças de imagem ligadas ao que se diz' });
|
|
126
|
+
m.tipos_distintos = tipos.size;
|
|
127
|
+
if (tipos.size < regua.tipos_distintos_min) falhas.push({ regra: 'tipos_distintos', valor: tipos.size, limite: regua.tipos_distintos_min, conserto: 'misture famílias: lettering, moldura, contador, b-roll, punch' });
|
|
128
|
+
const sons = (cfg.sons || []).map((s) => num(s.em)).filter((x) => x != null);
|
|
129
|
+
m.sfx_por_min = Math.round((sons.length / dur) * 60 * 10) / 10;
|
|
130
|
+
if (m.sfx_por_min < regua.sfx_min_por_min) falhas.push({ regra: 'sfx_por_min', valor: m.sfx_por_min, limite: regua.sfx_min_por_min, conserto: 'rode o plano_sfx com --variar e mais camadas' });
|
|
131
|
+
const cortes = (cfg.blocos || []).map((b) => num(b.de)).filter((x) => x != null && x > 0.05);
|
|
132
|
+
const semSom = cortes.filter((c) => !sons.some((s) => Math.abs(s - c) <= regua.sfx_no_corte_s));
|
|
133
|
+
m.cortes_sem_som = semSom.length;
|
|
134
|
+
if (semSom.length) falhas.push({ regra: 'sfx_no_corte', valor: semSom.length, limite: 0, em: semSom[0], conserto: `som nos cortes: ${semSom.slice(0, 4).join(', ')} s` });
|
|
135
|
+
const g = (cfg.animacoes || []).find((a) => a.id === 'gancho' || a.tipo === 'lettering');
|
|
136
|
+
m.gancho_em_s = g ? num(g.em) : null;
|
|
137
|
+
if (!g || (num(g.em) ?? 99) > regua.gancho_ate_s) falhas.push({ regra: 'gancho_ate_s', valor: m.gancho_em_s, limite: regua.gancho_ate_s, conserto: 'o gancho entra no primeiro segundo' });
|
|
138
|
+
const escalas = new Set();
|
|
139
|
+
for (const b of cfg.blocos || []) { if (num(b.zoom) != null) escalas.add(b.zoom); for (const z of b.zooms || []) if (num(z.escala) != null) escalas.add(z.escala); }
|
|
140
|
+
m.zooms_distintos = escalas.size;
|
|
141
|
+
if (escalas.size < regua.zooms_distintos_min) falhas.push({ regra: 'zooms_distintos', valor: escalas.size, limite: regua.zooms_distintos_min, conserto: 'varie o enquadramento (mais de duas escalas)' });
|
|
142
|
+
const sobreRosto = (cfg.animacoes || []).filter((a) => POS_ROSTO.has(a.posicao) || a.cobre_rosto === true);
|
|
143
|
+
m.texto_sobre_rosto = sobreRosto.length;
|
|
144
|
+
if (sobreRosto.length > regua.legenda_sobre_rosto) falhas.push({ regra: 'texto_sobre_rosto', valor: sobreRosto.length, limite: regua.legenda_sobre_rosto, conserto: 'mova o texto para baixo do rosto ou ponha o rosto em moldura' });
|
|
145
|
+
const nota = Math.max(0, Math.round(100 - falhas.length * (100 / 8)));
|
|
146
|
+
return { nota, passou: falhas.length === 0, falhas, metricas: m };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Duas variantes não podem abrir igual nem ter o mesmo jeito (gancho, estilo e ritmo). */
|
|
150
|
+
function compararVariantes(cfgs) {
|
|
151
|
+
const falhas = [];
|
|
152
|
+
const sig = (c) => {
|
|
153
|
+
const g = (c.animacoes || []).find((a) => a.id === 'gancho');
|
|
154
|
+
const t = (c.blocos || []).length;
|
|
155
|
+
return { gancho: g ? JSON.stringify(g.principal || g.texto || g.titulo || '').toLowerCase() : '', estilo: c.estilo || '', ritmo: Math.round(t / 3) };
|
|
156
|
+
};
|
|
157
|
+
const s = cfgs.map(sig);
|
|
158
|
+
for (let a = 0; a < s.length; a++) for (let b = a + 1; b < s.length; b++) {
|
|
159
|
+
if (s[a].gancho && s[a].gancho === s[b].gancho) falhas.push({ regra: 'mesmo_gancho', entre: [a, b], valor: s[a].gancho });
|
|
160
|
+
if (s[a].estilo === s[b].estilo && s[a].ritmo === s[b].ritmo) falhas.push({ regra: 'mesmo_estilo_e_ritmo', entre: [a, b], valor: `${s[a].estilo}/${s[a].ritmo}` });
|
|
161
|
+
}
|
|
162
|
+
return { passou: falhas.length === 0, falhas };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { REGUA, entenderFrases, ganchosCandidatos, escolherGancho, escolherEstilo, criticar, compararVariantes, mudancasVisuais };
|
package/lib/editor-pro.js
CHANGED
|
@@ -45,6 +45,7 @@ const crypto = require('crypto');
|
|
|
45
45
|
const { spawn, spawnSync } = require('child_process');
|
|
46
46
|
|
|
47
47
|
const ORIGEM = path.join(__dirname, '..', 'editor');
|
|
48
|
+
const MAQ = require('./editor-pro-maquina.js');
|
|
48
49
|
const casa = () => process.env.PRIMOCODE_EDITOR_CASA || path.join(os.homedir(), '.primocode', 'editor');
|
|
49
50
|
const venvDir = () => process.env.PRIMOCODE_EDITOR_VENV || path.join(os.homedir(), '.primocode', 'editor-venv');
|
|
50
51
|
const WIN = process.platform === 'win32';
|
|
@@ -641,7 +642,7 @@ function aplicarPlano(cfg, plano, { fala, duracao, broll = [], marca = null }) {
|
|
|
641
642
|
cfg.titulo = plano.titulo || cfg.titulo;
|
|
642
643
|
cfg.formato = plano.formato || cfg.formato || '9:16';
|
|
643
644
|
// limpo (a régua: chão claro, cartão branco, sem brilho) é o padrão; papelaria, noite e laranja continuam
|
|
644
|
-
cfg.estilo = plano.estilo ||
|
|
645
|
+
cfg.estilo = plano.estilo || MAQ.escolherEstilo(fala, plano.variante || 0);
|
|
645
646
|
// a legenda cinética é o padrão (Inter 800, palavra por palavra, a palavra-chave no degradê); padrao e discreta continuam
|
|
646
647
|
cfg.legenda = Object.assign({ ligada: true, estilo: 'cinetica', max_palavras: 3, na_moldura: false }, cfg.legenda || {}, typeof plano.legenda === 'string' ? { estilo: plano.legenda } : (plano.legenda || {}));
|
|
647
648
|
const pedida = typeof plano.legenda === 'string' ? plano.legenda : (plano.legenda && plano.legenda.estilo);
|
|
@@ -653,7 +654,7 @@ function aplicarPlano(cfg, plano, { fala, duracao, broll = [], marca = null }) {
|
|
|
653
654
|
if (Array.isArray(grad) && grad.length >= 2) cfg.gradiente = grad.slice(0, 2);
|
|
654
655
|
if (plano.correcoes) cfg.correcoes = Object.assign({}, cfg.correcoes || {}, plano.correcoes);
|
|
655
656
|
const animacoes = [];
|
|
656
|
-
const gancho = plano.gancho === false ? null : (plano.gancho || ganchoAutomatico(fala));
|
|
657
|
+
const gancho = plano.gancho === false ? null : (plano.gancho || MAQ.escolherGancho(fala, plano.variante || 0) || ganchoAutomatico(fala));
|
|
657
658
|
if (gancho) {
|
|
658
659
|
const prim = fala[0] ? Number(fala[0].split('-')[0]) : 0;
|
|
659
660
|
const fimFrase = fala[0] ? Number(fala[0].split(' ')[0].split('-')[1]) : 3;
|
|
@@ -817,11 +818,13 @@ async function montar(opts = {}, ctx = {}) {
|
|
|
817
818
|
}
|
|
818
819
|
await passo('montando a edição…', () => build(E, P.proj, ['completo']));
|
|
819
820
|
if (plano.sons !== false) {
|
|
820
|
-
await passo('escolhendo os efeitos sonoros casados com cada animação…', () => helper(E, 'plano_sfx', ['--projeto', P.proj]));
|
|
821
|
+
await passo('escolhendo os efeitos sonoros casados com cada animação…', () => helper(E, 'plano_sfx', ['--projeto', P.proj, '--variar']));
|
|
821
822
|
await passo('montando a edição com os sons…', () => build(E, P.proj, ['completo']));
|
|
822
823
|
}
|
|
824
|
+
const critica = MAQ.criticar(lerJson(cfgPath, {}), { duracao: tl.duration || 0 });
|
|
825
|
+
for (const f of critica.falhas) avisosPlano.push(`régua: ${f.regra} = ${f.valor} (limite ${f.limite}): ${f.conserto}`);
|
|
823
826
|
const lint = await build(E, P.proj, ['hf', 'lint']);
|
|
824
|
-
if (opts.render === false) return { ok: true, pasta: P.pasta, projeto: P.proj, lint: cauda(lint.saida, 6), broll, camera, avisos: avisosPlano };
|
|
827
|
+
if (opts.render === false) return { ok: true, pasta: P.pasta, projeto: P.proj, lint: cauda(lint.saida, 6), broll, camera, critica, avisos: avisosPlano };
|
|
825
828
|
await passo('renderizando o vídeo (leva alguns minutos)…', () => build(E, P.proj, ['hf', 'render', '--quality', 'delivery', '--output', 'renders/final.mp4'], { timeout: 5400000 }));
|
|
826
829
|
let final = path.join(P.proj, 'renders', 'final.mp4');
|
|
827
830
|
// trilha: arquivo da pessoa, ou a busca do PrimoCode (energia alta), ou nenhuma
|
|
@@ -856,7 +859,7 @@ async function montar(opts = {}, ctx = {}) {
|
|
|
856
859
|
ok: !!(rel && rel.passou), passou: !!(rel && rel.passou), video: saida, quadros: fotos,
|
|
857
860
|
audio, portao: rel ? { passou: rel.passou, falhas: rel.falhas, modos: rel.modos_do_volume, clipadas: rel.amostras_clipadas, folha: rel.folha, leitor: rel.leitor_de_texto } : cauda(portao.saida),
|
|
858
861
|
trilha: trilha ? { arquivo: trilha.arquivo, titulo: trilha.titulo, creditos: trilha.creditos } : null,
|
|
859
|
-
broll, avisos: avisosPlano, camera, projeto: P.proj, pasta: P.pasta,
|
|
862
|
+
broll, avisos: avisosPlano, camera, critica, projeto: P.proj, pasta: P.pasta,
|
|
860
863
|
...(rel && !rel.passou ? { error: 'o portão REPROVOU: ' + (rel.falhas || []).join(' | ') + ' — conserte no plano e chame montar de novo' } : {}),
|
|
861
864
|
};
|
|
862
865
|
} catch (e) {
|
|
@@ -882,7 +885,7 @@ const AJUDA = `editor_pro — edição completa de VÍDEO FALADO (motor EDIT IA
|
|
|
882
885
|
acao "montar" {pasta, plano?, trilha?} gancho, peças, b-roll, câmera viva, sons, render, trilha com ducking, portão
|
|
883
886
|
acao "preparar" baixa os arquivos e cria o Python do editor (sem editar nada)
|
|
884
887
|
acao "estado" {pasta} onde a edição parou
|
|
885
|
-
plano: {gancho:"Texto *destaque*", pecas:[peças do catálogo], broll:[{busca,de,ate,rotulo?}], moldura?, comparar?, blocos?, estilo?, legenda?, correcoes?, trilha?}
|
|
888
|
+
plano: {gancho:"Texto *destaque*", pecas:[peças do catálogo], broll:[{busca,de,ate,rotulo?}], moldura?, comparar?, blocos?, estilo?, variante? (0,1,2…: outro gancho e outro estilo, pelo conteúdo), legenda?, correcoes?, trilha?}
|
|
886
889
|
estilo: "limpo" (padrão: chão claro, cartão branco, sem brilho) | "laranja" | "noite" | "papelaria"
|
|
887
890
|
moldura: [{de, ate, fundo?:"papel"|"desfocado"}] a pessoa num quadro arredondado que flutua devagar sobre o chão
|
|
888
891
|
comparar: [{de, ate, arranjo:"lado"|"empilhado"|"auto", a:{arquivo|"corte", inicio, rotulo}, b:{...}}] dois vídeos na mesma escala, rótulo literal
|
|
@@ -923,5 +926,5 @@ module.exports = {
|
|
|
923
926
|
ORIGEM, AJUDA, casa, venvDir, venvPython, impressao, instalarArquivos, garantirArquivo, garantirVenv, gerarSintetizados,
|
|
924
927
|
dependencias, acharUv, acharPython, garantir, helper, cortar, montar, editar, ferramenta, main,
|
|
925
928
|
limparTrechos, trechosDoModelo, ganchoAutomatico, pecasDasSugestoes, aplicarPlano, prepararPasta, falaCompacta, urlDoEspelho,
|
|
926
|
-
palavrasDoCorte, naOnda, limiaresDaOnda, resumoCamera, palavrasChave, gradienteDaMarca,
|
|
929
|
+
palavrasDoCorte, naOnda, limiaresDaOnda, resumoCamera, palavrasChave, gradienteDaMarca, maquina: MAQ,
|
|
927
930
|
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cli.js — a linha de comando que a skill e os especialistas chamam.
|
|
4
|
+
* node lib/entregaveis/cli.js conferir plano.json
|
|
5
|
+
* node lib/entregaveis/cli.js exportar plano.json [pasta] [--forcar] [--sem-pdf]
|
|
6
|
+
* Sai com 1 se a conferência reprova (ou se não exportou).
|
|
7
|
+
*/
|
|
8
|
+
'use strict';
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const slides = require('./slides');
|
|
12
|
+
|
|
13
|
+
async function main(argv) {
|
|
14
|
+
const [cmd, arq, pasta] = argv.filter((a) => !a.startsWith('--'));
|
|
15
|
+
if (!['conferir', 'exportar'].includes(cmd) || !arq) { console.error('uso: cli.js conferir|exportar plano.json [pasta]'); return 2; }
|
|
16
|
+
let plano;
|
|
17
|
+
try { plano = JSON.parse(fs.readFileSync(arq, 'utf8')); } catch (e) { console.error('plano ilegível: ' + e.message); return 2; }
|
|
18
|
+
if (cmd === 'conferir') { const r = slides.conferirDeck(plano); console.log(slides.relatorio(r)); return r.passou ? 0 : 1; }
|
|
19
|
+
const r = await slides.exportarDeck(plano, path.resolve(pasta || path.dirname(arq)), { forcar: argv.includes('--forcar'), pdf: !argv.includes('--sem-pdf') });
|
|
20
|
+
console.log(slides.relatorio(r.conferencia));
|
|
21
|
+
if (!r.exportado) { console.log('NÃO exportou: conserte as falhas (ou --forcar).'); return 1; }
|
|
22
|
+
r.arquivos.forEach((a) => console.log('arquivo: ' + a));
|
|
23
|
+
if (r.erroPdf) console.log('sem PDF: ' + r.erroPdf);
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
main(process.argv.slice(2)).then((c) => process.exit(c));
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* especialistas.js (entregáveis) — quem faz cada parte do slide, do cartaz e do documento.
|
|
3
|
+
*
|
|
4
|
+
* Mesmo formato de lib/especialistas.js (faz + briefing curto que vai na frente da tarefa).
|
|
5
|
+
* Ficam separados de propósito: os nomes de lib/especialistas.js são um acordo com o enum
|
|
6
|
+
* do spawn_agent do servidor (test/especialistas.test.js), e aqui os papéis são de ofício
|
|
7
|
+
* de entregável. O briefing termina sempre em como PROVAR que acabou.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const NUCLEO = 'Conferência é código: rode `node lib/entregaveis/cli.js conferir plano.json` e só diga "pronto" com nota >= 80 e sem falha de bloqueio.';
|
|
12
|
+
|
|
13
|
+
const ESPECIALISTAS_ENTREGAVEIS = {
|
|
14
|
+
roteirista: {
|
|
15
|
+
tipos: ['slides', 'documento', 'cartaz'],
|
|
16
|
+
faz: 'transforma o brief em estrutura: uma ideia por slide/seção, título que é afirmação',
|
|
17
|
+
briefing: `Você é o ROTEIRISTA. Parta do brief (tema, público, objetivo). Escreva primeiro a história em uma frase, depois a lista de slides com UMA afirmação cada (3 a 12 palavras, nunca "Visão geral"). O texto do slide é o que se lê de longe; o que se fala vai em "nota". Abra com capa, feche com ação concreta (campo "acao"). Cada número citado tem fonte [LI] ou vira "[VERIFICAR]". Prova: plano.json com slides e notas, sem conferência de texto ainda.`,
|
|
18
|
+
},
|
|
19
|
+
'diretor-de-arte': {
|
|
20
|
+
tipos: ['slides', 'cartaz', 'documento'],
|
|
21
|
+
faz: 'escolhe a direção visual a partir de material solto, nunca de modelo pronto',
|
|
22
|
+
briefing: `Você é o DIRETOR DE ARTE. NÃO use template: junte material solto (objetos, cores e textura do assunto; referências que o dono aprovou) e escreva a direção em "direcao": conceito (por que este visual é DESTE assunto, >= 6 palavras), paleta {fundo,texto,destaque,destaque2?,suave?} em hex, fonteTitulo, fonteCorpo (fonte que a outra máquina tem, ou a que será embutida no PDF). Título gigante em cor de destaque (degradê de texto ainda não sai no PDF). Todo elemento visual extra leva "visual.motivo": o que ele tem a ver com o conteúdo. Prova: contraste texto/fundo >= 4.5 calculado, não olhado.`,
|
|
23
|
+
},
|
|
24
|
+
tipografo: {
|
|
25
|
+
tipos: ['slides', 'cartaz', 'documento'],
|
|
26
|
+
faz: 'hierarquia, tamanhos e encaixe do texto',
|
|
27
|
+
briefing: `Você é o TIPÓGRAFO. Defina a escala (título 40-80pt, corpo >= 24pt em slide) e dê UM peso de destaque por slide. Encurte até o texto caber na caixa (a conferência estima o encaixe). Nada de três fontes. Prova: regras "tamanho-da-letra" e "encaixe" sem falha.`,
|
|
28
|
+
},
|
|
29
|
+
revisor: {
|
|
30
|
+
tipos: ['slides', 'cartaz', 'documento'],
|
|
31
|
+
faz: 'confere o resultado com os números e aponta; não reescreve',
|
|
32
|
+
briefing: `Você é o REVISOR e não fez este plano: aponte, não conserte. ${NUCLEO} Depois abra o PDF exportado e olhe de verdade 3 slides (capa, o mais cheio, o último): texto vazando, contraste, layout repetido. Liste falha por slide com o número. Prova: relatório da conferência + o que viu no PDF.`,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const NOMES = Object.keys(ESPECIALISTAS_ENTREGAVEIS);
|
|
37
|
+
|
|
38
|
+
function porNome(nome) {
|
|
39
|
+
const n = String(nome || '').trim().toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/\s+/g, '-');
|
|
40
|
+
const mapa = { diretor: 'diretor-de-arte', arte: 'diretor-de-arte', designer: 'diretor-de-arte', roteiro: 'roteirista', tipografia: 'tipografo', review: 'revisor', reviewer: 'revisor' };
|
|
41
|
+
const k = mapa[n] || n;
|
|
42
|
+
return ESPECIALISTAS_ENTREGAVEIS[k] ? { nome: k, ...ESPECIALISTAS_ENTREGAVEIS[k] } : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A ordem em que os especialistas trabalham num tipo de entregável. */
|
|
46
|
+
function equipe(tipo) {
|
|
47
|
+
return ['roteirista', 'diretor-de-arte', 'tipografo', 'revisor'].filter((n) => ESPECIALISTAS_ENTREGAVEIS[n].tipos.includes(tipo));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { ESPECIALISTAS_ENTREGAVEIS, NOMES, porNome, equipe };
|