primocode 10.12.0 → 10.14.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/README.md +74 -12
- package/bin/primocode.js +148 -8
- package/lib/act.js +48 -1
- package/lib/catalogo.js +12 -1
- package/lib/config.js +1 -0
- package/lib/estudio/assinaturas/gerativo.json +1 -1
- package/lib/estudio/camadas.js +4 -0
- package/lib/estudio/cota.js +150 -0
- package/lib/estudio/dados/projetos/clube-ad5.json +105 -0
- package/lib/estudio/dados/projetos/clube-ae-sem-memoria.json +107 -0
- package/lib/estudio/dados/projetos/clube-agent.json +120 -0
- package/lib/estudio/dados/projetos/clube-antes-e-agora.json +86 -0
- package/lib/estudio/dados/projetos/clube-apresentando-clube.json +110 -0
- package/lib/estudio/dados/projetos/clube-bluevisa.json +106 -0
- package/lib/estudio/dados/projetos/clube-caught-in-the-fire.json +127 -0
- package/lib/estudio/dados/projetos/clube-chat-cliente-satisfeito.json +98 -0
- package/lib/estudio/dados/projetos/clube-chatgpt-anuncios.json +120 -0
- package/lib/estudio/dados/projetos/clube-claude-dj.json +140 -0
- package/lib/estudio/dados/projetos/clube-claude-mcp.json +142 -0
- package/lib/estudio/dados/projetos/clube-cleanui.json +106 -0
- package/lib/estudio/dados/projetos/clube-clinica-take-unico.json +128 -0
- package/lib/estudio/dados/projetos/clube-clinica.json +104 -0
- package/lib/estudio/dados/projetos/clube-clube-opener.json +125 -0
- package/lib/estudio/dados/projetos/clube-clube.json +108 -0
- package/lib/estudio/dados/projetos/clube-codigo.json +101 -0
- package/lib/estudio/dados/projetos/clube-distilbook.json +104 -0
- package/lib/estudio/dados/projetos/clube-drive-upload.json +115 -0
- package/lib/estudio/dados/projetos/clube-dub.json +119 -0
- package/lib/estudio/dados/projetos/clube-enxovaly.json +105 -0
- package/lib/estudio/dados/projetos/clube-fecha7.json +118 -0
- package/lib/estudio/dados/projetos/clube-filatov.json +128 -0
- package/lib/estudio/dados/projetos/clube-frame.json +108 -0
- package/lib/estudio/dados/projetos/clube-glass.json +118 -0
- package/lib/estudio/dados/projetos/clube-letra-em-apps.json +84 -0
- package/lib/estudio/dados/projetos/clube-letra-globo-3d.json +111 -0
- package/lib/estudio/dados/projetos/clube-letra-nos-apps.json +146 -0
- package/lib/estudio/dados/projetos/clube-letra-seis-mundos.json +100 -0
- package/lib/estudio/dados/projetos/clube-lula-3.json +146 -0
- package/lib/estudio/dados/projetos/clube-morph.json +119 -0
- package/lib/estudio/dados/projetos/clube-motion-designer.json +115 -0
- package/lib/estudio/dados/projetos/clube-motion-pacote.json +112 -0
- package/lib/estudio/dados/projetos/clube-movimento.json +103 -0
- package/lib/estudio/dados/projetos/clube-not-like-us.json +117 -0
- package/lib/estudio/dados/projetos/clube-pinterest-inspiracao.json +86 -0
- package/lib/estudio/dados/projetos/clube-pixel-e-tinta.json +108 -0
- package/lib/estudio/dados/projetos/clube-ponte.json +118 -0
- package/lib/estudio/dados/projetos/clube-processo-edit-tonal.json +126 -0
- package/lib/estudio/dados/projetos/clube-processo-motion-100-ia.json +109 -0
- package/lib/estudio/dados/projetos/clube-promo.json +114 -0
- package/lib/estudio/dados/projetos/clube-proposito.json +115 -0
- package/lib/estudio/dados/projetos/clube-rankhog.json +128 -0
- package/lib/estudio/dados/projetos/clube-recursao.json +116 -0
- package/lib/estudio/dados/projetos/clube-revelar.json +118 -0
- package/lib/estudio/dados/projetos/clube-robo.json +125 -0
- package/lib/estudio/dados/projetos/clube-shelby-ep2.json +144 -0
- package/lib/estudio/dados/projetos/clube-shelby.json +97 -0
- package/lib/estudio/dados/projetos/clube-solar-eclipse.json +96 -0
- package/lib/estudio/dados/projetos/clube-soldier.json +128 -0
- package/lib/estudio/dados/projetos/clube-som-de-cena.json +108 -0
- package/lib/estudio/dados/projetos/clube-spotify-candidatura.json +112 -0
- package/lib/estudio/dados/projetos/clube-tda-app.json +118 -0
- package/lib/estudio/dados/projetos/clube-tda-ia.json +75 -0
- package/lib/estudio/dados/projetos/clube-telegram-chat.json +118 -0
- package/lib/estudio/dados/projetos/clube-telegram-ilha.json +106 -0
- package/lib/estudio/dados/projetos/clube-tennessee-nike.json +132 -0
- package/lib/estudio/dados/projetos/clube-tudo-se-move.json +112 -0
- package/lib/estudio/dados/projetos/clube-uiad.json +107 -0
- package/lib/estudio/dados/projetos/clube-viralgrow.json +108 -0
- package/lib/estudio/dados/projetos/clube-whose-side.json +140 -0
- package/lib/estudio/dados/projetos/clube-youtube-inscrever.json +108 -0
- package/lib/estudio/dados/projetos/elementos-do-site-em-movimento.json +49 -0
- package/lib/estudio/dados/projetos/explainer-narrado.json +51 -0
- package/lib/estudio/dados/projetos/lancamento-de-produto.json +51 -0
- package/lib/estudio/dados/projetos/motion-so-com-sons.json +50 -0
- package/lib/estudio/dados/projetos/titulo-em-after-effects.json +51 -0
- package/lib/estudio/dados/projetos/tour-de-saas.json +50 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/ae/cenas.jsx +733 -0
- package/lib/estudio/dados/projetos-ae/antes-e-agora/assets/fazer_assets.py +88 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/ae/cenas.jsx +402 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/preparar_assets.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-em-apps/assets/svg_para_ae.py +25 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/BRIEF-AGENTES.md +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/README.md +53 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ROTEIRO-CENAS.md +69 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/A-agenda.jsx +148 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/B-docs.jsx +174 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/C-alttab.jsx +113 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/D-spotify.jsx +118 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/E-notas.jsx +146 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/F-jornal1.jsx +144 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/G-google.jsx +153 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/H-mapa.jsx +176 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/I-imagens.jsx +132 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/J-jornal2.jsx +191 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/K-maps.jsx +184 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera-movimentos.jsx +48 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/camera.jsx +57 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/card-spotify.jsx +98 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/digitar.jsx +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/jornal-comum.jsx +196 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra.jsx +23 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/letra_automatica.py +83 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/montagem.jsx +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-agenda/util.jsx +162 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/w-midia-comum.jsx +114 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/ae/wg-util.jsx +360 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/F-jornal1/render_html.py +13 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/render_w-midia.py +26 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/inclin.py +30 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_d.py +36 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/assets/w-midia-ferramentas/medir_e.py +27 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ajustar_movimentos.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/ancorar.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/assar.py +47 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/camera3d_jornal.py +172 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/comparar.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/dados_ae_jornal.py +46 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/folha_contato.py +28 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/homografia_jornal.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/integrar.py +44 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/medir_quadro_a_quadro.py +32 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/mesa_2d.py +64 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/plot_cenas.py +11 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/retificar_quadros.py +38 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/assar_ab.py +52 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/w-agenda/medir_ab.py +59 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ajustar_h.py +111 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_corrige_cam.py +17 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_ecc_cadeia.py +80 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_finalizar_cam.py +65 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/camera/wg_mosaico.py +61 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/01-gramatica-e-roteiro.md +55 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/02-camera.md +66 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/03-montagem-no-ae.md +50 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/04-render-e-conferencia.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces/05-abertura-celular-mergulho.md +43 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/guia-do-metodo/tutorial-lyric-edit-interfaces.md +42 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/atraso_imagem.py +31 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/fundo.py +56 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/juntar.py +45 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/montar.py +177 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/rastreio.py +74 -0
- package/lib/estudio/dados/projetos-ae/letra-nos-apps/transicao/sincronia.py +18 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/ae/cenas.jsx +530 -0
- package/lib/estudio/dados/projetos-ae/pinterest-inspiracao/assets/fazer_assets.py +107 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/assets/pessoas/figurinha.py +46 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/BRIEF-A.md +83 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/audio.py +179 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/confere.py +20 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte1.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/dados/tempo-parte2.js +1 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/gerar_emojis.py +11 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/img/filmes/_indice.js +122 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/base.js +380 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/main.js +86 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1a.js +169 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1b.js +455 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p1c.js +389 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/js/p2.js +379 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/comp-a/video.py +62 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/README.md +65 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/oraculo_take.py +137 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/render_take.py +388 -0
- package/lib/estudio/dados/projetos-ae/processo-motion-100-ia/take-unico/take.js +235 -0
- package/lib/estudio/diretor.js +2 -1
- package/lib/estudio/editor.js +285 -0
- package/lib/estudio/efeitos-fabrica/terminal-digitando.jsx +2 -1
- package/lib/estudio/effectcraft.js +152 -0
- package/lib/estudio/exportar.js +181 -0
- package/lib/estudio/gerativo.js +44 -15
- package/lib/estudio/index.js +126 -5
- package/lib/estudio/kit/gerativo.jsx +15 -0
- package/lib/estudio/kit-ae/base.jsx +1146 -0
- package/lib/estudio/kit-ae/cenas.jsx +395 -0
- package/lib/estudio/kit-ae/primitivas.jsx +911 -0
- package/lib/estudio/kokoro.js +3 -1
- package/lib/estudio/maquina-diretor.js +393 -0
- package/lib/estudio/maquina-elementos.js +134 -0
- package/lib/estudio/maquina-layout.js +198 -0
- package/lib/estudio/maquina-som.js +478 -0
- package/lib/estudio/maquina-trilha.js +160 -0
- package/lib/estudio/maquina-voz.js +429 -0
- package/lib/estudio/maquina.js +516 -0
- package/lib/estudio/projetos.js +60 -0
- package/lib/estudio/remotion.js +6 -4
- package/lib/estudio/revisor.js +61 -0
- package/lib/estudio/voicestudio.js +81 -0
- package/lib/headless.js +71 -0
- package/lib/hooks.js +72 -0
- package/lib/local/cerebro.json +398 -12
- package/lib/local/entrada.js +46 -2
- package/lib/marca-kit.js +309 -0
- package/lib/mcp.js +286 -6
- package/lib/perigo.js +31 -3
- package/lib/planos.js +47 -19
- package/lib/preparar.js +111 -0
- package/lib/retomar.js +55 -0
- package/lib/sandbox.js +197 -0
- package/lib/skills.js +3 -1
- package/lib/tools.js +13 -4
- package/lib/ui.js +1 -1
- package/lib/video/auto.js +18 -3
- package/package.json +4 -2
- package/skills/after-effects/SKILL.md +52 -0
- package/skills/maquina-de-motion/SKILL.md +84 -0
- package/skills/motion-de-primeira/SKILL.md +33 -51
package/lib/estudio/revisor.js
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
* avisa a mesma sequência de elementos em 4+ cenas seguidas (cara de template)
|
|
17
17
|
* avisa imagem sem a pessoa pedir, ou imagem em mais da metade das cenas
|
|
18
18
|
* conserta efeito sonoro demais (mais de 1 a cada 1,5 s) → ficam os mais altos
|
|
19
|
+
* avisa texto pequeno demais para o celular (< 2% da altura no 9:16)
|
|
20
|
+
* avisa texto com contraste abaixo de 3:1 contra o fundo da cena
|
|
19
21
|
* Puro: o plano entra, o plano consertado e a lista saem.
|
|
20
22
|
*/
|
|
21
23
|
|
|
@@ -34,6 +36,62 @@ const palavras = (t) => String(t || '').split(/\s+/).filter(Boolean).length;
|
|
|
34
36
|
// Imagem ou vídeo DE FORA: o vídeo da própria gravação (deOrigem) e o recorte da pessoa não contam.
|
|
35
37
|
const imagemDeFora = (el) => el && (el.tipo === 'imagem' || (el.tipo === 'video' && el.deOrigem == null && !/pessoa-/.test(el.src || '')));
|
|
36
38
|
|
|
39
|
+
/* O TEXTO QUE NÃO SE LÊ (15/10, as referências): rótulo de 1,6% da altura
|
|
40
|
+
("NO COADOR") e laranja sobre bege. Nas referências a palavra principal
|
|
41
|
+
ocupa 55–85% da largura e a segunda voz ~5% da altura, sempre em contraste
|
|
42
|
+
duro. Contraste é o da WCAG para texto grande (3:1). */
|
|
43
|
+
const hex6 = (c) => {
|
|
44
|
+
const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(String(c || '').trim());
|
|
45
|
+
if (!m) return null;
|
|
46
|
+
const h = m[1].length === 3 ? m[1].split('').map((x) => x + x).join('') : m[1];
|
|
47
|
+
return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
48
|
+
};
|
|
49
|
+
const luminancia = (c) => {
|
|
50
|
+
const v = hex6(c);
|
|
51
|
+
if (!v) return null;
|
|
52
|
+
const [r, g, b] = v.map((x) => (x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)));
|
|
53
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
54
|
+
};
|
|
55
|
+
function contraste(a, b) {
|
|
56
|
+
const la = luminancia(a), lb = luminancia(b);
|
|
57
|
+
if (la == null || lb == null) return null;
|
|
58
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
59
|
+
}
|
|
60
|
+
const corDoFundo = (f) => (typeof f === 'string' ? f : f && (f.valor || f.cor)) || null;
|
|
61
|
+
|
|
62
|
+
function revisarLeitura(plano, avisos) {
|
|
63
|
+
const W = Number(plano.formato && plano.formato.largura) || 1920, H = Number(plano.formato && plano.formato.altura) || 1080;
|
|
64
|
+
const piso = H * (W > H ? 0.024 : 0.019);
|
|
65
|
+
const curto = (t) => String(t || '').slice(0, 24);
|
|
66
|
+
const pequenos = [], apagados = [];
|
|
67
|
+
(plano.cenas || []).forEach((c, i) => {
|
|
68
|
+
const fundo = corDoFundo(c.fundo) || plano.fundoGeral;
|
|
69
|
+
for (const e of c.elementos || []) {
|
|
70
|
+
if (!e) continue;
|
|
71
|
+
if (e.tipo === 'texto') {
|
|
72
|
+
const tam = Number(e.tamanho || (e.estilo && e.estilo.tamanho));
|
|
73
|
+
if (tam && tam < piso) pequenos.push(`cena ${i + 1} "${curto(e.texto)}" ${r2((tam / H) * 100)}%`);
|
|
74
|
+
const k = contraste(e.cor || (e.estilo && e.estilo.cor), fundo);
|
|
75
|
+
if (k != null && k < 3) apagados.push(`cena ${i + 1} "${curto(e.texto)}" ${r2(k)}:1`);
|
|
76
|
+
}
|
|
77
|
+
if (e.tipo === 'kit' && e.peca === 'gerativo' && e.partitura) {
|
|
78
|
+
const Hp = Number(e.partitura.altura) || H;
|
|
79
|
+
const pisoP = Hp * ((Number(e.partitura.largura) || W) > Hp ? 0.024 : 0.019);
|
|
80
|
+
for (const b of e.partitura.batidas || []) {
|
|
81
|
+
const cores = b.cores || {};
|
|
82
|
+
for (const l of b.linhas || []) {
|
|
83
|
+
if (l.tamanho && l.tamanho < pisoP) pequenos.push(`batida ${r2(b.de)}s "${curto(l.texto)}" ${r2((l.tamanho / Hp) * 100)}%`);
|
|
84
|
+
}
|
|
85
|
+
const k = contraste(cores.texto, cores.fundo);
|
|
86
|
+
if (k != null && k < 3) apagados.push(`batida ${r2(b.de)}s ${cores.texto} sobre ${cores.fundo} ${r2(k)}:1`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
if (pequenos.length) avisos.push(`texto pequeno demais para o celular (piso ~2% da altura): ${pequenos.slice(0, 3).join('; ')}${pequenos.length > 3 ? ` e mais ${pequenos.length - 3}` : ''}`);
|
|
92
|
+
if (apagados.length) avisos.push(`texto com contraste abaixo de 3:1 contra o fundo: ${apagados.slice(0, 3).join('; ')}${apagados.length > 3 ? ` e mais ${apagados.length - 3}` : ''}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
37
95
|
function revisar(plano, { semImagem = false } = {}) {
|
|
38
96
|
const consertos = [], avisos = [];
|
|
39
97
|
const cenas = (plano && plano.cenas) || [];
|
|
@@ -114,6 +172,9 @@ function revisar(plano, { semImagem = false } = {}) {
|
|
|
114
172
|
if (semImagem && comImagem) avisos.push(`${comImagem} cena(s) com imagem de fora, mas a pessoa não pediu imagem`);
|
|
115
173
|
else if (cenas.length >= 4 && comImagem / cenas.length > 0.5) avisos.push(`imagem em ${comImagem} de ${cenas.length} cenas — a imagem é um momento, não o vídeo inteiro`);
|
|
116
174
|
|
|
175
|
+
// 7. O texto que não se lê: pequeno demais ou sem contraste.
|
|
176
|
+
revisarLeitura(plano, avisos);
|
|
177
|
+
|
|
117
178
|
return { plano, consertos, avisos };
|
|
118
179
|
}
|
|
119
180
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* voicestudio.js — a voz local do VoiceStudio, a primeira da cadeia.
|
|
3
|
+
*
|
|
4
|
+
* "achei isso: https://voicestudio.sh/ podemos usar pra narração do motion,
|
|
5
|
+
* ele entra primeiro na cadeia e depois, se falhar, o Gemini" (Joel, 15/10)
|
|
6
|
+
*
|
|
7
|
+
* O VoiceStudio é um app de desktop (AGPL-3.0) que a pessoa instala e deixa
|
|
8
|
+
* aberto; ele serve uma API no formato da OpenAI em localhost:3900, sem chave
|
|
9
|
+
* no loopback (docs/api-auth.md do repo dele). O PrimoCode só CHAMA a API —
|
|
10
|
+
* não copia nem distribui o código dele, então a licença não pega.
|
|
11
|
+
*
|
|
12
|
+
* Fechado, a porta recusa na hora e a cadeia segue para o Gemini sem esperar.
|
|
13
|
+
* Puro de rede: http do Node, sem dependência.
|
|
14
|
+
*
|
|
15
|
+
* VOICESTUDIO_URL outro endereço (padrão http://127.0.0.1:3900)
|
|
16
|
+
* VOICESTUDIO_VOZ o perfil de voz do app (padrão "default")
|
|
17
|
+
* VOICESTUDIO_MODELO o motor (padrão "omnivoice")
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
'use strict';
|
|
21
|
+
|
|
22
|
+
const http = require('http');
|
|
23
|
+
const https = require('https');
|
|
24
|
+
|
|
25
|
+
const URL_PADRAO = 'http://127.0.0.1:3900';
|
|
26
|
+
const LIMITE = 4096; // o input máximo do /v1/audio/speech
|
|
27
|
+
|
|
28
|
+
function base(url) { return String(url || process.env.VOICESTUDIO_URL || URL_PADRAO).replace(/\/+$/, ''); }
|
|
29
|
+
|
|
30
|
+
function pedir(metodo, url, { corpo = null, timeout = 800 } = {}) {
|
|
31
|
+
return new Promise((ok) => {
|
|
32
|
+
let u;
|
|
33
|
+
try { u = new URL(url); } catch { ok({ ok: false, error: 'endereço inválido' }); return; }
|
|
34
|
+
const lib = u.protocol === 'https:' ? https : http;
|
|
35
|
+
const dados = corpo ? Buffer.from(JSON.stringify(corpo)) : null;
|
|
36
|
+
const req = lib.request(u, {
|
|
37
|
+
method: metodo,
|
|
38
|
+
headers: dados ? { 'content-type': 'application/json', 'content-length': dados.length } : {},
|
|
39
|
+
}, (res) => {
|
|
40
|
+
const partes = [];
|
|
41
|
+
res.on('data', (c) => partes.push(c));
|
|
42
|
+
res.on('end', () => ok({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, corpo: Buffer.concat(partes) }));
|
|
43
|
+
});
|
|
44
|
+
req.setTimeout(timeout, () => req.destroy(new Error('tempo esgotado')));
|
|
45
|
+
req.on('error', (e) => ok({ ok: false, error: e.message, recusou: /ECONNREFUSED|EADDRNOTAVAIL|ENOTFOUND/.test(e.code || e.message) }));
|
|
46
|
+
if (dados) req.write(dados);
|
|
47
|
+
req.end();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** O app está aberto? Pergunta rápida: fechado, a porta recusa na hora. */
|
|
52
|
+
async function disponivel({ url } = {}) {
|
|
53
|
+
const r = await pedir('GET', base(url) + '/v1/audio/voices', { timeout: 800 });
|
|
54
|
+
return r.ok === true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Um texto em áudio (mp3). Nunca lança: devolve {ok, audio} ou {ok:false, error}.
|
|
59
|
+
* O primeiro pedido carrega o modelo no app, por isso o tempo é longo.
|
|
60
|
+
*/
|
|
61
|
+
async function falar(texto, { url, voz, modelo, idioma = 'pt', timeout = 10 * 60000 } = {}) {
|
|
62
|
+
const input = String(texto || '').trim();
|
|
63
|
+
if (!input) return { ok: false, error: 'voicestudio: texto vazio' };
|
|
64
|
+
if (input.length > LIMITE) return { ok: false, error: `voicestudio: o texto passa de ${LIMITE} caracteres` };
|
|
65
|
+
if (!(await disponivel({ url }))) return { ok: false, indisponivel: true, error: 'voicestudio: o app não está aberto' };
|
|
66
|
+
const r = await pedir('POST', base(url) + '/v1/audio/speech', {
|
|
67
|
+
corpo: {
|
|
68
|
+
model: modelo || process.env.VOICESTUDIO_MODELO || 'omnivoice',
|
|
69
|
+
input,
|
|
70
|
+
voice: voz || process.env.VOICESTUDIO_VOZ || 'default',
|
|
71
|
+
response_format: 'mp3',
|
|
72
|
+
language: idioma,
|
|
73
|
+
},
|
|
74
|
+
timeout,
|
|
75
|
+
});
|
|
76
|
+
if (!r.ok) return { ok: false, error: `voicestudio: ${r.error || 'status ' + r.status}${r.corpo && r.corpo.length < 400 ? ' ' + r.corpo.toString() : ''}` };
|
|
77
|
+
if (!r.corpo || r.corpo.length < 8) return { ok: false, error: 'voicestudio: áudio vazio' };
|
|
78
|
+
return { ok: true, audio: r.corpo };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { disponivel, falar, URL_PADRAO, LIMITE };
|
package/lib/headless.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* headless.js — `primocode -p "pedido"`: sem tela, para script e CI.
|
|
3
|
+
*
|
|
4
|
+
* primocode -p "arruma o teste" texto no stdout
|
|
5
|
+
* primocode -p "..." --output-format json um JSON no fim: {type, is_error, result, duration_ms}
|
|
6
|
+
* git diff | primocode -p "revisa isso" o stdin entra no pedido
|
|
7
|
+
* primocode -p "..." --permission-mode plan só lê (ver lib/perigo.js)
|
|
8
|
+
*
|
|
9
|
+
* Mesma ideia do `claude -p` e do `codex exec` (flags documentadas deles; o
|
|
10
|
+
* código é nosso). O pedido passa pelo MESMO caminho do digitado (sendPrompt).
|
|
11
|
+
* Código de saída: 0 ok, 1 erro.
|
|
12
|
+
*/
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const { stripAnsi } = require('./ui');
|
|
16
|
+
|
|
17
|
+
const FORMATOS = ['text', 'json'];
|
|
18
|
+
|
|
19
|
+
/** Tira -p/--print, --output-format e --permission-mode dos argumentos. */
|
|
20
|
+
function analisar(argv) {
|
|
21
|
+
const r = { ativo: false, formato: 'text', permissao: null, resto: [], erro: null };
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
const [chave, valor] = a.startsWith('--') && a.includes('=') ? [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)] : [a, null];
|
|
25
|
+
if (a === '-p' || a === '--print') r.ativo = true;
|
|
26
|
+
else if (chave === '--output-format') {
|
|
27
|
+
const v = valor !== null ? valor : argv[++i];
|
|
28
|
+
if (!FORMATOS.includes(v)) r.erro = `--output-format aceita ${FORMATOS.join(' ou ')}`;
|
|
29
|
+
else r.formato = v;
|
|
30
|
+
} else if (chave === '--permission-mode') r.permissao = valor !== null ? valor : argv[++i];
|
|
31
|
+
else r.resto.push(a);
|
|
32
|
+
}
|
|
33
|
+
return r;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** O pedido: os argumentos soltos, mais o stdin (que vem depois, como contexto). */
|
|
37
|
+
function montarPedido(palavras, stdin) {
|
|
38
|
+
const pedido = palavras.join(' ').trim();
|
|
39
|
+
const extra = String(stdin || '').trim();
|
|
40
|
+
if (pedido && extra) return `${pedido}\n\n${extra}`;
|
|
41
|
+
return pedido || extra;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function lerStdin() {
|
|
45
|
+
if (process.stdin.isTTY) return Promise.resolve('');
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
let s = '';
|
|
48
|
+
process.stdin.setEncoding('utf8');
|
|
49
|
+
process.stdin.on('data', (d) => { s += d; });
|
|
50
|
+
process.stdin.on('end', () => resolve(s));
|
|
51
|
+
process.stdin.on('error', () => resolve(s));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Roda `fn` guardando tudo o que ela escreve. `calar` esconde da tela. */
|
|
56
|
+
async function capturar(fn, { calar = false } = {}) {
|
|
57
|
+
const log = console.log;
|
|
58
|
+
const escrever = process.stdout.write.bind(process.stdout);
|
|
59
|
+
let texto = '';
|
|
60
|
+
const pega = (s) => { texto += stripAnsi(s); };
|
|
61
|
+
console.log = (...a) => { pega(a.join(' ') + '\n'); if (!calar) log(...a); };
|
|
62
|
+
process.stdout.write = (s, ...resto) => { pega(typeof s === 'string' ? s : Buffer.from(s).toString()); return calar ? true : escrever(s, ...resto); };
|
|
63
|
+
try { await fn(); } finally { console.log = log; process.stdout.write = escrever; }
|
|
64
|
+
return texto.replace(/\n{3,}/g, '\n\n').trim();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resultadoJson({ texto, erro, inicio, permissao }) {
|
|
68
|
+
return JSON.stringify({ type: 'result', is_error: !!erro, result: erro ? String(erro) : texto, duration_ms: Date.now() - inicio, permission_mode: permissao || 'auto' });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { analisar, montarPedido, lerStdin, capturar, resultadoJson, FORMATOS };
|
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hooks.js — comandos seus que rodam ANTES e DEPOIS de cada ferramenta.
|
|
3
|
+
*
|
|
4
|
+
* Arquivo: ~/.primocode/hooks.json (vale em tudo) e <projeto>/.primocode/hooks.json:
|
|
5
|
+
*
|
|
6
|
+
* { "PreToolUse": [{ "matcher": "run_shell|write_file", "command": "./checar.sh" }],
|
|
7
|
+
* "PostToolUse": [{ "matcher": "write_file|edit_file", "command": "npx prettier --write ." }] }
|
|
8
|
+
*
|
|
9
|
+
* O comando recebe o evento em JSON no stdin ({evento, ferramenta, argumentos,
|
|
10
|
+
* resultado, pasta}). No PreToolUse, sair com código 2 BLOQUEIA a ferramenta e o
|
|
11
|
+
* stderr vira o motivo que o agente lê. Qualquer outro código só avisa. Sem
|
|
12
|
+
* `matcher`, vale para toda ferramenta. O contrato é o mesmo dos hooks
|
|
13
|
+
* documentados do Claude Code (code.claude.com/docs/en/hooks); o código é nosso.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { spawnSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const EVENTOS = ['PreToolUse', 'PostToolUse'];
|
|
23
|
+
const PRAZO_MS = 10000;
|
|
24
|
+
|
|
25
|
+
function ler(arquivo) {
|
|
26
|
+
try {
|
|
27
|
+
const j = JSON.parse(fs.readFileSync(arquivo, 'utf8'));
|
|
28
|
+
return j && typeof j === 'object' ? j : {};
|
|
29
|
+
} catch { return {}; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Os hooks de um evento: os da casa primeiro, depois os do projeto. */
|
|
33
|
+
function carregar(evento, projectDir) {
|
|
34
|
+
const arquivos = [path.join(os.homedir(), '.primocode', 'hooks.json')];
|
|
35
|
+
if (projectDir) arquivos.push(path.join(projectDir, '.primocode', 'hooks.json'));
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const a of arquivos) {
|
|
38
|
+
const lista = ler(a)[evento];
|
|
39
|
+
if (!Array.isArray(lista)) continue;
|
|
40
|
+
for (const h of lista) if (h && typeof h.command === 'string' && h.command.trim()) out.push(h);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function casa(matcher, nome) {
|
|
46
|
+
if (!matcher || matcher === '*') return true;
|
|
47
|
+
try { return new RegExp(`^(?:${matcher})$`).test(nome); } catch { return false; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Roda os hooks do evento. Devolve { bloqueado, motivo, avisos[] }.
|
|
52
|
+
* Só o PreToolUse bloqueia.
|
|
53
|
+
*/
|
|
54
|
+
function rodar(evento, { nome, args, resultado, projectDir }) {
|
|
55
|
+
const saida = { bloqueado: false, motivo: '', avisos: [] };
|
|
56
|
+
if (!EVENTOS.includes(evento)) return saida;
|
|
57
|
+
const entrada = JSON.stringify({ evento, ferramenta: nome, argumentos: args || {}, resultado, pasta: projectDir });
|
|
58
|
+
for (const h of carregar(evento, projectDir)) {
|
|
59
|
+
if (!casa(h.matcher, nome)) continue;
|
|
60
|
+
const r = spawnSync(h.command, { shell: true, input: entrada, cwd: projectDir || process.cwd(), encoding: 'utf8', timeout: h.timeout ? h.timeout * 1000 : PRAZO_MS });
|
|
61
|
+
const err = String(r.stderr || '').trim();
|
|
62
|
+
if (evento === 'PreToolUse' && r.status === 2) {
|
|
63
|
+
saida.bloqueado = true;
|
|
64
|
+
saida.motivo = err || `bloqueado pelo hook: ${h.command}`;
|
|
65
|
+
return saida;
|
|
66
|
+
}
|
|
67
|
+
if (r.error || r.status !== 0) saida.avisos.push(`hook "${h.command.slice(0, 40)}" ${r.error ? 'falhou: ' + r.error.message : 'saiu com ' + r.status}${err ? ' — ' + err.slice(0, 120) : ''}`);
|
|
68
|
+
}
|
|
69
|
+
return saida;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = { EVENTOS, carregar, casa, rodar };
|