primocode 11.3.3 → 11.5.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.
Files changed (89) hide show
  1. package/LICENSES/edit2-terceiros.md +31 -0
  2. package/LICENSES/models-dev-MIT.txt +25 -0
  3. package/README.md +2 -2
  4. package/editor/SKILL.md +24 -0
  5. package/editor/THIRD_PARTY_NOTICES.md +1 -0
  6. package/editor/assets/baixar.json +9 -1
  7. package/editor/helpers/df_limpar.py +29 -0
  8. package/editor/helpers/edit2.py +155 -0
  9. package/editor/helpers/enquadrar_camera.py +77 -0
  10. package/editor/helpers/face_layout.py +20 -0
  11. package/editor/helpers/face_mediapipe.py +147 -0
  12. package/editor/helpers/instalar_df.sh +7 -0
  13. package/editor/helpers/legenda_ass.py +124 -0
  14. package/editor/helpers/look_ffmpeg.py +105 -0
  15. package/editor/helpers/motor_palavras.py +157 -0
  16. package/editor/helpers/primeiro_passe.py +214 -0
  17. package/editor/helpers/som_edit2.py +119 -0
  18. package/editor/helpers/transcribe.py +31 -2
  19. package/editor/pyproject.toml +9 -0
  20. package/editor/references/edit2.md +22 -0
  21. package/editor/uv.lock +3342 -214
  22. package/kits/3d/LICENSES/README.md +11 -0
  23. package/kits/3d/animacoes.js +87 -0
  24. package/kits/3d/base.js +44 -0
  25. package/kits/3d/catalogo.js +38 -0
  26. package/kits/3d/cenario.js +162 -0
  27. package/kits/3d/exemplos/asfalto.mjs +91 -0
  28. package/kits/3d/maquinas.js +97 -0
  29. package/kits/3d/montador.js +142 -0
  30. package/kits/3d/package.json +1 -0
  31. package/kits/3d/personagem.js +86 -0
  32. package/kits/3d/remotion/Maquete.jsx +42 -0
  33. package/kits/3d/remotion/Root.jsx +10 -0
  34. package/kits/3d/render.mjs +73 -0
  35. package/lib/cli/comando-api.js +30 -8
  36. package/lib/cli/modelos-lista.js +58 -0
  37. package/lib/cli/sessao.js +11 -0
  38. package/lib/cli/subcomandos.js +3 -2
  39. package/lib/outra-pasta.js +1 -1
  40. package/lib/permissao-sistema.js +60 -0
  41. package/lib/permission/perigo.js +2 -1
  42. package/lib/permission/permissao.js +1 -0
  43. package/lib/permission/sem-perguntar.js +29 -0
  44. package/lib/plugin/desktop.js +5 -4
  45. package/lib/plugin/estudio/cota.js +4 -2
  46. package/lib/plugin/estudio/dialogo3d.js +71 -0
  47. package/lib/plugin/estudio/effectcraft.js +1 -1
  48. package/lib/plugin/estudio/glifos.js +2 -0
  49. package/lib/plugin/estudio/gsap-local.js +43 -0
  50. package/lib/plugin/estudio/index.js +63 -10
  51. package/lib/plugin/estudio/kit-ae/base.jsx +1 -1
  52. package/lib/plugin/estudio/kit-ae/cenas.jsx +7 -4
  53. package/lib/plugin/estudio/kit-ae/destrava.jsx +211 -0
  54. package/lib/plugin/estudio/maquina-diretor.js +1 -1
  55. package/lib/plugin/estudio/maquina-elementos.js +2 -2
  56. package/lib/plugin/estudio/maquina.js +26 -8
  57. package/lib/plugin/estudio/motion-plano/ajustar.js +139 -0
  58. package/lib/plugin/estudio/motion-plano/critico.js +59 -0
  59. package/lib/plugin/estudio/motion-plano/estrutura.js +111 -0
  60. package/lib/plugin/estudio/motion-plano/fatos.js +137 -0
  61. package/lib/plugin/estudio/motion-plano/index.js +75 -0
  62. package/lib/plugin/estudio/motion-plano/llm.js +59 -0
  63. package/lib/plugin/estudio/motion-plano/motor.js +50 -0
  64. package/lib/plugin/estudio/motion-plano/passos.js +38 -0
  65. package/lib/plugin/estudio/motion-plano/pele.js +36 -0
  66. package/lib/plugin/estudio/motion-plano/plano.js +152 -0
  67. package/lib/plugin/estudio/motion-plano/regua.js +100 -0
  68. package/lib/plugin/estudio/personagem.js +3 -1
  69. package/lib/plugin/estudio/referencias.js +1 -1
  70. package/lib/provider/catalogo.js +161 -0
  71. package/lib/provider/local/catalogo-embutido.json.gz +0 -0
  72. package/lib/provider/local/cerebro.json +1 -1
  73. package/lib/provider/local/entrada.js +10 -0
  74. package/lib/provider/local/motor.js +140 -27
  75. package/lib/provider/local/orcamento.js +193 -0
  76. package/lib/provider/local/provedor.js +127 -14
  77. package/lib/provider/local/recorte.js +170 -0
  78. package/lib/provider/local/revezamento.js +121 -0
  79. package/lib/session/runner/act.js +28 -3
  80. package/lib/skill/catalogo.js +9 -1
  81. package/lib/tool/reparar-entrada.js +1 -0
  82. package/lib/tool/tools.js +58 -1
  83. package/lib/tui/ui.js +1 -1
  84. package/package.json +2 -2
  85. package/vendor/fontes/ttf/ORIGEM.txt +1 -0
  86. package/vendor/fontes/ttf/inter-800.ttf +0 -0
  87. package/vendor/gsap/ORIGEM.txt +6 -0
  88. package/vendor/gsap/gsap.min.js +11 -0
  89. package/voz/primo_voz/chat.html +206 -5
@@ -0,0 +1,111 @@
1
+ /**
2
+ * estrutura.js — etapa 2 do motion: ESTRUTURAR.
3
+ *
4
+ * O briefing vira batidas na ordem gancho → virada → prova → fecho. Uma ideia por quadro; cada batida traz
5
+ * a frase (até 8 palavras) com a palavra-chave marcada entre *asteriscos*. A virada do briefing é obrigatória
6
+ * numa batida de virada. O que o modelo errar de forma mecânica (ordem, palavra-chave faltando) o código conserta.
7
+ */
8
+ 'use strict';
9
+
10
+ const { pedirJson } = require('./llm.js');
11
+ const Fatos = require('./fatos.js');
12
+
13
+ const PAPEIS = ['gancho', 'virada', 'prova', 'fecho'];
14
+ const MAX_PALAVRAS = 8; // ≤ 7 por linha na régua (style.json maxWordsPerLine); a frase de um quadro cabe em 1 a 2 linhas
15
+ const MIN_BATIDAS = 5, MAX_BATIDAS = 9;
16
+
17
+ const SISTEMA = `Você estrutura o roteiro de um vídeo de motion graphics curto (vertical, 15 a 30 s) em PT-BR.
18
+ Responda SÓ JSON: {"batidas":[{"papel":"gancho|virada|prova|fecho","ideia":"a única ideia do quadro, em 1 frase interna","frase":"o texto que aparece na tela, até ${MAX_PALAVRAS} palavras, com a palavra-chave entre *asteriscos*","palavra_chave":"a mesma palavra marcada"}]}
19
+ Regras:
20
+ - Ordem: começa em gancho, depois virada, depois prova (1 a 4 batidas), termina em fecho. De ${MIN_BATIDAS} a ${MAX_BATIDAS} batidas no total.
21
+ - Uma ideia por quadro. Nunca duas ideias na mesma frase.
22
+ - O gancho abre com a pergunta que o público faria (ou o clichê do assunto). A virada é a resposta que quebra essa expectativa, específica do assunto: use a virada do briefing, sem copiar nenhum exemplo de outro assunto. Frase de virada pode ter 1 a 3 palavras.
23
+ - Prova: só repete o que está em "fatos" ou no pedido (data, local, o que é o negócio). NUNCA invente data, preço, endereço, nome, número, depoimento, lotação, fila, qualidade ou diferencial ("ambiente moderno", "já está cheia", "o melhor"). Se os fatos são poucos, a prova é curta e literal, só com palavras do pedido, e o resto do vídeo é gancho, virada e convite.
24
+ - Fecho: marca/nome e chamada. Só use a marca se estiver em "fatos"; sem marca, feche com a ação do pedido, em 1 a 3 palavras.
25
+ - Nada de frase de catálogo ("qualidade e excelência", "soluções completas", "o melhor do mercado").
26
+ - Exatamente UMA palavra-chave por frase, entre *asteriscos*, a que carrega a ideia.`;
27
+
28
+ const txt = (v, n) => String(v || '').replace(/\s+/g, ' ').trim().slice(0, n);
29
+ const contar = (s) => String(s || '').replace(/\*/g, '').split(/\s+/).filter(Boolean).length;
30
+
31
+ /** Garante 1 palavra-chave marcada na frase. Puro. */
32
+ function marcar(frase, chave) {
33
+ let f = txt(frase, 120); // mantém os asteriscos que o modelo pôs
34
+ const marcadas = f.match(/\*[^*]+\*/g) || [];
35
+ if (marcadas.length === 1) return { frase: f, chave: marcadas[0].slice(1, -1) };
36
+ f = f.replace(/\*/g, '');
37
+ const ps = f.split(' ');
38
+ let i = chave ? ps.findIndex((p) => p.replace(/[^\p{L}\p{N}]/gu, '').toLowerCase() === String(chave).replace(/[^\p{L}\p{N}]/gu, '').toLowerCase()) : -1;
39
+ if (i < 0) { let tam = 0; ps.forEach((p, k) => { const l = p.replace(/[^\p{L}\p{N}]/gu, '').length; if (l > tam) { tam = l; i = k; } }); }
40
+ if (i < 0) return { frase: f, chave: '' };
41
+ const limpo = ps[i].replace(/[.,!?;:]+$/, ''), sufixo = ps[i].slice(limpo.length);
42
+ ps[i] = `*${limpo}*${sufixo}`;
43
+ return { frase: ps.join(' '), chave: limpo };
44
+ }
45
+
46
+ /** Confere e conserta a resposta do modelo. Devolve { batidas, avisos }. Puro. */
47
+ function normalizar(j) {
48
+ const avisos = [];
49
+ let bs = (Array.isArray(j && j.batidas) ? j.batidas : []).map((b) => {
50
+ const papel = PAPEIS.includes(String(b && b.papel).toLowerCase()) ? String(b.papel).toLowerCase() : null;
51
+ const m = marcar(b && b.frase, b && b.palavra_chave);
52
+ return { papel, ideia: txt(b && b.ideia, 140), frase: m.frase, palavra_chave: m.chave };
53
+ }).filter((b) => b.papel && b.frase);
54
+ // ordem: o papel nunca volta atrás (gancho, virada, prova, fecho)
55
+ let nivel = 0;
56
+ bs = bs.filter((b) => { const k = PAPEIS.indexOf(b.papel); if (k < nivel) { avisos.push(`batida "${b.frase}" fora de ordem, descartada`); return false; } nivel = k; return true; });
57
+ for (const b of bs) if (contar(b.frase) > MAX_PALAVRAS) { avisos.push(`frase longa (${contar(b.frase)} palavras): "${b.frase}"`); b.longa = true; }
58
+ return { batidas: bs, avisos };
59
+ }
60
+
61
+ /** Filtro de fatos: tira da frase o que o usuário não disse; frase que perde o sentido cai. Puro. */
62
+ function filtrarFatos(bs, f) {
63
+ const barrados = [];
64
+ const saida = [];
65
+ for (const b of bs) {
66
+ const r = Fatos.limpar(b.frase, f);
67
+ r.removidos.forEach((x) => barrados.push(x));
68
+ if (!r.removidos.length) {
69
+ if (r.frase && Fatos.norm(r.frase) !== Fatos.norm(b.frase)) { const m = marcar(r.frase, b.palavra_chave); saida.push({ ...b, frase: m.frase, palavra_chave: m.chave }); } // o fato do usuário volta inteiro, como ele disse
70
+ else saida.push(b);
71
+ continue;
72
+ }
73
+ if (!r.frase) continue; // sobrou nada: a batida cai
74
+ const m = marcar(r.frase, b.palavra_chave);
75
+ saida.push({ ...b, frase: m.frase, palavra_chave: m.chave, longa: undefined });
76
+ }
77
+ return { batidas: saida, barrados };
78
+ }
79
+
80
+ /** O que impede de seguir. Puro. */
81
+ function validar(bs, briefing) {
82
+ const erros = [];
83
+ if (bs.length < 4) erros.push(`poucas batidas (${bs.length})`);
84
+ if (bs.length && bs[0].papel !== 'gancho') erros.push('não abre em gancho');
85
+ if (bs.length && bs[bs.length - 1].papel !== 'fecho') erros.push('não fecha em fecho');
86
+ if (!bs.some((b) => b.papel === 'virada')) erros.push('sem virada');
87
+ if (!bs.some((b) => b.papel === 'prova')) erros.push('sem prova');
88
+ if (bs.some((b) => b.longa)) erros.push('frase com mais de ' + MAX_PALAVRAS + ' palavras');
89
+ void briefing;
90
+ return erros;
91
+ }
92
+
93
+ async function estruturar(briefing, deps = {}) {
94
+ const llm = deps.llm;
95
+ if (!llm) return { ok: false, error: 'sem modelo para estruturar.' };
96
+ const corpo = JSON.stringify({ objetivo: briefing.objetivo, publico: briefing.publico, tom: briefing.tom, virada: briefing.virada, fatos: briefing.fatos, pedido: briefing.pedido });
97
+ let msgs = [{ role: 'system', content: SISTEMA }, { role: 'user', content: `Briefing:\n${corpo}` }];
98
+ let r = null, erros = [];
99
+ const barrados = [];
100
+ for (let t = 0; t < 2; t++) {
101
+ r = normalizar(await pedirJson(llm, msgs, { temperatura: 0.7 }));
102
+ if (deps.fonte) { const fl = filtrarFatos(r.batidas, deps.fonte); r = { ...r, batidas: fl.batidas }; barrados.push(...fl.barrados); }
103
+ erros = validar(r.batidas, briefing);
104
+ if (!erros.length) break;
105
+ msgs = [...msgs, { role: 'assistant', content: JSON.stringify({ batidas: r.batidas }) }, { role: 'user', content: `Conserte: ${erros.join('; ')}.${barrados.length ? ` O código tirou fatos que o usuário não disse (${barrados.join(', ')}): não os use.` : ''} Responda o JSON inteiro de novo.` }];
106
+ }
107
+ if (erros.length) return { ok: false, error: `estrutura inválida: ${erros.join('; ')}`, batidas: r.batidas };
108
+ return { ok: true, batidas: r.batidas, avisos: r.avisos, barrados };
109
+ }
110
+
111
+ module.exports = { estruturar, normalizar, validar, filtrarFatos, marcar, contar, PAPEIS, MAX_PALAVRAS, SISTEMA };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * fatos.js — o filtro de fatos, NO CÓDIGO (regra no prompt o modelo esquece).
3
+ *
4
+ * Número, data, mês, dia da semana, horário, preço, nome próprio, lugar, superlativo e promessa SÓ passam se
5
+ * estiverem no pedido do usuário ou nas respostas das perguntas. Senão a palavra cai da frase; se a frase
6
+ * perde o sentido, a frase inteira cai. Puro, sem rede.
7
+ */
8
+ 'use strict';
9
+
10
+ const norm = (s) => String(s || '').toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '').replace(/[^a-z0-9]/g, '');
11
+ const MESES = ['janeiro', 'fevereiro', 'marco', 'abril', 'maio', 'junho', 'julho', 'agosto', 'setembro', 'outubro', 'novembro', 'dezembro'];
12
+ const SEMANA = ['segunda', 'terca', 'quarta', 'quinta', 'sexta', 'sabado', 'domingo', 'hoje', 'amanha', 'ontem'];
13
+ const LUGAR = ['regiao', 'bairro', 'cidade', 'estado', 'pais', 'brasil', 'mundo', 'centro', 'oeste', 'sul', 'norte', 'leste', 'vizinhanca'];
14
+ const PROMESSA = ['gratis', 'gratuito', 'gratuita', 'desconto', 'promocao', 'brinde', 'garantia', 'garantido', 'garantida', 'garantidos', 'garantidas', 'certificado', 'bonus', 'cashback', 'reais', 'vagas', 'limitadas', 'esgotando', 'ultimas'];
15
+ const ADJETIVO = /^(modern[oa]s?|premium|sofisticad[oa]s?|elegantes?|luxo|luxuos[oa]s?|excelentes?|excelencia|qualidade|autentic[oa]s?|tradicion(?:al|ais)|ideal|ideais|diferenciad[oa]s?|inovador[a]?s?|impecavel|impecaveis|refinad[oa]s?|especial|especiais|irresistivel|irresistiveis|estilos[oa]s?|profissionais|profissional|completo|completa|completos|completas)$/;
16
+ const SUPERLATIVO = /^(melhor|melhores|pior|maior|maiores|menor|unico|unica|unicos|lider|lideres|premiad[oa]s?|exclusiv[oa]s?|inedit[oa]s?|perfeit[oa]s?|incrivel|imbativel|referencia|top|numero1|[a-z]+issim[oa]s?)$/;
17
+ const POSSESSIVO = new Set(['sua', 'suas', 'seu', 'seus', 'minha', 'minhas', 'meu', 'meus', 'nossa', 'nossas', 'nosso', 'nossos']); // sobra pendurada quando o substantivo cai ("Na sua" depois de cortar "vizinhança")
18
+ const LIGACAO = new Set(['sem', 'sobre', 'entre', 'de', 'da', 'do', 'das', 'dos', 'na', 'no', 'nas', 'nos', 'em', 'a', 'o', 'as', 'os', 'e', 'com', 'para', 'pra', 'por', 'as', 'ate', 'que', 'um', 'uma']);
19
+
20
+ /** O texto que vale como fonte: pedido + respostas das perguntas. Nada que o modelo escreveu. */
21
+ function fonte(pedido, respostas = []) {
22
+ const texto = [pedido, ...respostas.map((r) => (r && r.resposta) || r)].filter(Boolean).join(' ');
23
+ const palavras = new Set((texto.match(/[\p{L}\p{N}$%]+/gu) || []).map(norm).filter(Boolean));
24
+ const numeros = new Set((texto.match(/\d+/g) || []).map(String));
25
+ return { texto, palavras, numeros };
26
+ }
27
+
28
+ /** Quase a mesma palavra: edição de 1 letra (até 6 letras) ou 2 (7 ou mais), com a mesma primeira letra. */
29
+ function perto(a, b) {
30
+ if (a[0] !== b[0] || Math.abs(a.length - b.length) > 2) return false;
31
+ const m = Math.min(a.length, b.length) >= 7 ? 2 : 1;
32
+ let d = Array.from({ length: b.length + 1 }, (_, j) => j);
33
+ for (let i = 1; i <= a.length; i++) { const n = [i]; for (let j = 1; j <= b.length; j++) n[j] = Math.min(d[j] + 1, n[j - 1] + 1, d[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)); d = n; }
34
+ return d[b.length] <= m;
35
+ }
36
+
37
+ /** Por que a palavra não pode entrar (ou null). `inicio` = abre a frase (maiúscula ali não prova nome próprio). */
38
+ function motivo(palavra, f, inicio) {
39
+ const limpa = palavra.replace(/\*/g, '');
40
+ const n = norm(limpa);
41
+ if (!n || f.palavras.has(n)) return null;
42
+ const dig = limpa.match(/\d+/g);
43
+ if (dig) return dig.every((d) => f.numeros.has(d)) ? null : 'número/horário';
44
+ if (MESES.includes(n)) return 'mês';
45
+ if (SEMANA.includes(n)) return 'data';
46
+ if (LUGAR.includes(n)) return 'lugar';
47
+ if (PROMESSA.includes(n)) return 'promessa';
48
+ if (SUPERLATIVO.test(n)) return 'superlativo';
49
+ if (ADJETIVO.test(n)) return 'adjetivo de diferencial';
50
+ if (!inicio && /^\p{Lu}/u.test(limpa.replace(/^[^\p{L}]+/u, '')) && n.length > 2) return 'nome próprio';
51
+ return null;
52
+ }
53
+
54
+ /** Palavra que carrega fato do usuário: número ou nome próprio (fora do começo do pedido). */
55
+ const ehFato = (p) => /\d/.test(p) || /^\p{Lu}/u.test(p.replace(/^[^\p{L}\p{N}]+/u, ''));
56
+ const PREFIXO = new Set(['dia', 'em', 'no', 'na', 'as', 'a', 'de', 'do', 'da']);
57
+
58
+ /**
59
+ * O trecho de fato que o usuário disse vale INTEIRO e na ordem dele ("dia 30 em Chapecó"). Se a frase carrega dois ou mais
60
+ * fatos do usuário (30 e Chapecó) mas o trecho saiu reescrito ("30 de Chapecó"), volta o trecho exato do pedido.
61
+ * Só repõe o que o usuário escreveu; nunca inventa. Puro.
62
+ */
63
+ function restaurar(frase, f) {
64
+ const ps = f.texto.split(/\s+/).filter(Boolean);
65
+ const fs = String(frase).split(/\s+/).filter(Boolean);
66
+ const nf = (p) => norm(p.replace(/\*/g, ''));
67
+ const pos = []; // posição de cada palavra-fato da frase dentro do pedido
68
+ fs.forEach((p, k) => { if (ehFato(p.replace(/\*/g, '')) && !(k === 0 && !/\d/.test(p))) { const j = ps.findIndex((q) => nf(q) === nf(p)); if (j >= 0) pos.push({ k, j }); } });
69
+ if (pos.length < 2) return frase;
70
+ const a = pos[0], b = pos[pos.length - 1];
71
+ if (b.j <= a.j) return frase;
72
+ let j0 = a.j; while (j0 > 0 && a.j - j0 < 1 && PREFIXO.has(nf(ps[j0 - 1])) && !/[.!?]$/.test(ps[j0 - 1])) j0--;
73
+ let k0 = a.k; while (k0 > 0 && a.k - k0 < 1 && PREFIXO.has(nf(fs[k0 - 1]))) k0--;
74
+ const trecho = ps.slice(j0, b.j + 1).map((p) => p.replace(/[.!?,;:]+$/, ''));
75
+ if (trecho.length > 6 || trecho.length < 2) return frase;
76
+ const atual = fs.slice(k0, b.k + 1).map(nf).join('');
77
+ if (atual === trecho.map(nf).join('')) return frase;
78
+ const kwEm = fs.slice(k0, b.k + 1).some((p) => /\*/.test(p));
79
+ const fim = (fs[b.k].match(/[.!?,;:]+$/) || [''])[0];
80
+ let novo = trecho.join(' ');
81
+ if (kwEm) { const kw = fs.slice(k0, b.k + 1).find((p) => /\*/.test(p)).replace(/[^\p{L}\p{N}*]/gu, ''); const w = kw.replace(/\*/g, ''); novo = novo.replace(new RegExp(`(^|\\s)${w.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=\\s|$)`), `$1*${w}*`); }
82
+ return [...fs.slice(0, k0), novo + fim, ...fs.slice(b.k + 1)].join(' ');
83
+ }
84
+
85
+ /**
86
+ * Tira da frase o que não é fato do usuário. Devolve { frase, removidos[] } ou { frase: null } se nada sobra.
87
+ * Os asteriscos da palavra-chave são preservados nas palavras que ficam.
88
+ */
89
+ function limpar(frase, f) {
90
+ // erro de digitação de palavra do usuário ("Destrua" por "Destrava"): volta a palavra exata que ele escreveu
91
+ const doUsuario = [...new Set((f.texto || '').split(/\s+/).map((w) => w.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, '')).filter((w) => norm(w).length >= 5))];
92
+ const ps = String(frase || '').split(/\s+/).filter(Boolean).map((p) => {
93
+ const nu = p.replace(/\*/g, ''), nn = norm(nu);
94
+ if (nn.length < 5 || f.palavras.has(nn) || /\d/.test(nu)) return p;
95
+ const u = doUsuario.find((w) => norm(w) !== nn && perto(norm(w), nn));
96
+ return u ? p.replace(nu.replace(/[.!?,;:]+$/, '').replace(/^[^\p{L}\p{N}]+/u, ''), u) : p;
97
+ });
98
+ const removidos = [];
99
+ let inicio = true;
100
+ const ficam = [];
101
+ let quebrou = false; // número no meio da frase ("10 minutos por dia"): o resto perde o sentido
102
+ ps.forEach((p, k) => {
103
+ const m = motivo(p, f, inicio);
104
+ inicio = /[.!?]["')]*$/.test(p);
105
+ if (m) { removidos.push(`${p.replace(/\*/g, '')} (${m})`); if (m === 'número/horário' && k < ps.length - 1) quebrou = true; return; }
106
+ ficam.push(p);
107
+ });
108
+ if (quebrou) return { frase: null, removidos };
109
+ // duas ligações coladas depois de um corte ("dia 30 de em Chapecó"): fica a que o usuário disse, nunca uma inventada
110
+ if (removidos.length) {
111
+ const lig = (p) => LIGACAO.has(norm(p.replace(/[.!?,;:]+$/, '')));
112
+ for (let k = ficam.length - 2; k >= 0; k--) {
113
+ if (lig(ficam[k]) && lig(ficam[k + 1])) {
114
+ const dito = (p) => f.palavras.has(norm(p));
115
+ if (dito(ficam[k + 1]) || !dito(ficam[k])) ficam.splice(k, 1); else ficam.splice(k + 1, 1);
116
+ }
117
+ }
118
+ }
119
+ // sobras de ligação ("Nova na" depois de cortar "região"; "dia 30 de" depois de cortar "novembro")
120
+ const solta = (p) => { const n = norm(p.replace(/[.!?,;:]+$/, '')); return LIGACAO.has(n) || (removidos.length > 0 && POSSESSIVO.has(n)); };
121
+ while (ficam.length && solta(ficam[ficam.length - 1])) ficam.pop();
122
+ if (removidos.length && ficam.length) { const u = ficam.length - 1; if (!/[.!?]$/.test(ficam[u]) && /[.!?]$/.test(ps[ps.length - 1])) ficam[u] += ps[ps.length - 1].match(/[.!?]+$/)[0]; }
123
+ const texto = restaurar(ficam.join(' ').replace(/\*\s*\*/g, '').trim(), f);
124
+ if (!/\p{L}|\d/u.test(texto)) return { frase: null, removidos };
125
+ return { frase: texto, removidos };
126
+ }
127
+
128
+ /** Os fatos do briefing que o modelo extraiu só valem se o usuário disse. Puro. */
129
+ function fatosDoUsuario(fatos, f) {
130
+ const SOLTAS = new Set(['dia', 'dias', 'hora', 'horas', 'h']); // "dia 30" == "30": a palavra de moldura não faz o fato ser inventado
131
+ const ok = (v) => { const t = String(v || '').trim(); return t && (String(t).match(/[\p{L}\p{N}]+/gu) || []).map(norm).every((w) => f.palavras.has(w) || LIGACAO.has(w) || SOLTAS.has(w)); };
132
+ const lista = (l) => (l || []).filter(ok);
133
+ const o = fatos || {};
134
+ return { ...o, datas: lista(o.datas), nomes: lista(o.nomes), marca: ok(o.marca) ? o.marca : '', local: ok(o.local) ? o.local : '', oferta: ok(o.oferta) ? o.oferta : '', outros: lista(o.outros) };
135
+ }
136
+
137
+ module.exports = { fonte, limpar, restaurar, motivo, fatosDoUsuario, norm };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * motion-plano/index.js — as 3 etapas na FRENTE do motor de motion (maquina.js), sem reescrevê-lo.
3
+ *
4
+ * pedido → 1 ajustar (briefing + perguntas) → 2 estruturar (batidas) → 3 plano (cena por cena) → 4 motor (args.cenas)
5
+ *
6
+ * Desligar (caminho antigo): `etapas:false` nos argumentos do estudio_motion, ou PRIMOCODE_MOTION_ETAPAS=0.
7
+ * Sem modelo (sem GROQ_API_KEY) ou se uma etapa falhar, o motion segue pelo caminho antigo com um aviso,
8
+ * a não ser que `estrito` esteja ligado (a prova usa estrito: falhou, para e diz).
9
+ */
10
+ 'use strict';
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const ligado = (args = {}) => args.etapas !== false && String(process.env.PRIMOCODE_MOTION_ETAPAS || '') !== '0' && !(Array.isArray(args.cenas) && args.cenas.length);
16
+
17
+ /**
18
+ * @param {object} args os argumentos do estudio_motion (assunto|pedido, formato, duracao, fotos, ...)
19
+ * @param {object} ctx { escolher, linha, projectDir }
20
+ * @param {object} deps { llm, pasta, estrito, fotos }
21
+ * @returns {Promise<{ok, args?, etapas?, passos?, error?, etapa?}>}
22
+ */
23
+ async function preparar(args, ctx = {}, deps = {}) {
24
+ const L = require('./llm.js');
25
+ const llm = deps.llm || (L.disponivel() ? L.chamar : null);
26
+ if (!llm) return { ok: false, etapa: 0, error: 'sem modelo para as etapas (falta GROQ_API_KEY).' };
27
+ const pedido = String(args.pedido || args.assunto || '').trim();
28
+ if (pedido.length < 3) return { ok: false, etapa: 0, error: 'sem pedido.' };
29
+ const fotos = (Array.isArray(deps.fotos) ? deps.fotos : (Array.isArray(args.fotos) ? args.fotos : args.fotos ? [args.fotos] : [])).map(String);
30
+ const passos = require('./passos.js').acompanhar(ctx);
31
+ const falhou = (etapa, error, extra) => ({ ok: false, etapa, error, passos: passos.registro, ...extra });
32
+
33
+ passos.feito(0);
34
+ const a = await require('./ajustar.js').ajustarPedido(pedido, { llm, escolher: ctx.escolher, fotos }).catch((e) => ({ ok: false, error: e.message }));
35
+ if (!a.ok) return falhou(1, `ajustar o pedido: ${a.error}`);
36
+ passos.detalhe(0, `virada: ${a.briefing.virada}${a.puladas ? ` (${a.puladas} pergunta(s) puladas)` : ''}`);
37
+ passos.feito(1);
38
+
39
+ const Fatos = require('./fatos.js');
40
+ const fonte = Fatos.fonte(pedido, a.respostas || []);
41
+ a.briefing.fatos = Fatos.fatosDoUsuario(a.briefing.fatos, fonte); // o que o modelo "extraiu" só vale se o usuário disse
42
+ const e = await require('./estrutura.js').estruturar(a.briefing, { llm, fonte }).catch((x) => ({ ok: false, error: x.message }));
43
+ if (!e.ok) return falhou(2, `estruturar: ${e.error}`, { briefing: a.briefing });
44
+ passos.detalhe(1, `${e.batidas.length} batidas (${e.batidas.map((b) => b.papel[0]).join('')})`);
45
+ if (e.barrados && e.barrados.length) passos.detalhe(1, `${e.batidas.length} batidas; fatos barrados: ${e.barrados.join(', ')}`);
46
+ passos.feito(2);
47
+
48
+ const alvo = Number(args.duracao) > 0 ? [Math.max(15, Number(args.duracao) - 3), Math.min(30, Number(args.duracao) + 3)] : [15, 30];
49
+ const p = await require('./plano.js').montarPlano(a.briefing, e.batidas, { llm, alvoS: alvo, fonte }).catch((x) => ({ ok: false, error: x.message }));
50
+ if (!p.ok) return falhou(3, `plano: ${p.error}`, { briefing: a.briefing, batidas: e.batidas, plano: p.plano });
51
+ const linhas = require('./passos.js').linhasDoPlano(p.plano);
52
+ passos.detalhe(2, `${p.plano.cenas.length} cenas, ${p.plano.total_s} s`, linhas);
53
+ passos.feito(3);
54
+
55
+ const motorArgs = require('./motor.js').argumentosDoMotor(p.plano, args, { fotos });
56
+ if (deps.pasta) {
57
+ try {
58
+ fs.mkdirSync(path.join(deps.pasta, 'motion'), { recursive: true });
59
+ fs.writeFileSync(path.join(deps.pasta, 'motion', 'plano.json'), JSON.stringify({ briefing: a.briefing, batidas: e.batidas, plano: p.plano, barrados: e.barrados || [], perguntas: a.perguntas, respostas: a.respostas }, null, 2));
60
+ } catch { /* o plano na pasta é só rastro */ }
61
+ }
62
+ return { ok: true, args: motorArgs, etapas: { briefing: a.briefing, batidas: e.batidas, plano: p.plano, barrados: e.barrados || [], perguntas: a.perguntas, respostas: a.respostas }, passos: passos.registro };
63
+ }
64
+
65
+ /** Depois do render: folha de contato + as contas mecânicas (flash branco, imagem parada). Nunca derruba o vídeo. */
66
+ function critico(video, pasta) {
67
+ try {
68
+ const C = require('./critico.js');
69
+ const folha = C.folha(video, path.join(pasta, 'folha-contato.png'), { n: 8 });
70
+ const m = C.mecanico(video);
71
+ return { ok: m.ok, motivos: m.motivos, folha: folha.ok ? folha.arquivo : null };
72
+ } catch (e) { return { ok: null, motivos: [], erro: e.message }; }
73
+ }
74
+
75
+ module.exports = { preparar, critico, ligado };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * llm.js — o modelo que as etapas do motion chamam: o provedor grátis do PrimoCode (Groq, GROQ_API_KEY).
3
+ * A corrente segue server/lib/modelos.js (gpt-oss-120b, depois gpt-oss-20b); erro de limite/modelo troca de elo.
4
+ * Tudo injetável: o resto do motion-plano só recebe `llm(mensagens, opcoes) => texto`.
5
+ */
6
+ 'use strict';
7
+
8
+ const BASE = process.env.GROQ_BASE_URL || 'https://api.groq.com/openai/v1';
9
+ const CORRENTE = (process.env.MOTION_PLANO_MODELOS || 'openai/gpt-oss-120b,openai/gpt-oss-20b').split(',').map((s) => s.trim()).filter(Boolean);
10
+
11
+ const disponivel = () => !!process.env.GROQ_API_KEY;
12
+
13
+ /** Devolve o texto da resposta. Troca de modelo em 429/5xx/404; qualquer outro erro sobe. */
14
+ async function chamar(mensagens, { json = true, temperatura = 0.6, maxTokens = 3500 } = {}) {
15
+ if (!disponivel()) throw new Error('sem GROQ_API_KEY');
16
+ let ultimo = null;
17
+ for (const model of CORRENTE) {
18
+ const corpo = { model, messages: mensagens, temperature: temperatura, max_tokens: maxTokens, reasoning_effort: 'low' };
19
+ if (json) corpo.response_format = { type: 'json_object' };
20
+ let r;
21
+ try {
22
+ r = await fetch(`${BASE}/chat/completions`, {
23
+ method: 'POST',
24
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.GROQ_API_KEY}` },
25
+ body: JSON.stringify(corpo),
26
+ signal: AbortSignal.timeout(90000),
27
+ });
28
+ } catch (e) { ultimo = new Error(`${model}: ${e.message}`); continue; }
29
+ if (r.ok) {
30
+ const j = await r.json();
31
+ const t = String(j.choices && j.choices[0] && j.choices[0].message && j.choices[0].message.content || '');
32
+ if (t.trim()) return t;
33
+ ultimo = new Error(`${model}: resposta vazia`);
34
+ continue;
35
+ }
36
+ ultimo = new Error(`${model}: HTTP ${r.status} ${(await r.text()).slice(0, 160)}`);
37
+ if (![400, 404, 408, 429, 500, 502, 503].includes(r.status)) break;
38
+ }
39
+ throw ultimo || new Error('nenhum modelo respondeu');
40
+ }
41
+
42
+ /** Tira cerca de ``` e acha o primeiro objeto JSON da resposta. */
43
+ function extrairJson(texto) {
44
+ const t = String(texto).replace(/```(?:json)?/g, '');
45
+ const i = t.indexOf('{'), f = t.lastIndexOf('}');
46
+ if (i < 0 || f < i) throw new Error('a resposta não tem JSON');
47
+ return JSON.parse(t.slice(i, f + 1));
48
+ }
49
+
50
+ /** Pede JSON e tenta de novo UMA vez se vier torto. */
51
+ async function pedirJson(llm, mensagens, opcoes = {}) {
52
+ let erro;
53
+ for (let t = 0; t < 2; t++) {
54
+ try { return extrairJson(await llm(mensagens, { json: true, ...opcoes })); } catch (e) { erro = e; }
55
+ }
56
+ throw erro;
57
+ }
58
+
59
+ module.exports = { chamar, disponivel, extrairJson, pedirJson, CORRENTE };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * motor.js — etapa 4: o PLANO vira a entrada do motor atual (maquina.js, o do estudio_motion).
3
+ *
4
+ * Não mexe no motor: só traduz `plano.cenas` para o `args.cenas` que ele já entende ({papel, tipo, titulo, dur,
5
+ * botao, valor, foto}) e escolhe tipografia, transições e câmera pelas entradas do plano. O kit de marca
6
+ * (renderMotion em index.js) segue valendo: cores, fontes e logo entram como sempre.
7
+ */
8
+ 'use strict';
9
+
10
+ const HEROI_PARA_TIPO = { tipografia: 'cinetico', foto: 'foto', numero: 'numero', cor_cheia: 'cinetico', marca: 'cta' };
11
+ const PAPEL_PARA_PAPEL = { gancho: 'gancho', virada: 'virada', prova: 'prova', fecho: 'cta' };
12
+ const TIPOGRAFIA_DA_ENTRADA = { 'mascara-sobe': 'poster', 'palavra-a-palavra': 'pesada', 'escala-assenta': 'poster' };
13
+ const TRANSICAO_DA_ENTRADA = { 'zoom-atravessa': 'zoom', 'cartao-voa': 'empurra', 'cor-expande': 'abre', 'mascara-sobe': 'cortina', 'escala-assenta': 'encolhe', 'palavra-a-palavra': 'desfoque' };
14
+
15
+ const MODO_DA_ENTRADA = { 'palavra-a-palavra': 'suave', 'mascara-sobe': 'mascara', 'escala-assenta': 'escala', 'zoom-atravessa': 'escala', 'cor-expande': 'escala', 'cartao-voa': 'suave' };
16
+ const sem = (s) => String(s || '').replace(/\*/g, '');
17
+
18
+ /** plano → cenas do motor. Puro. */
19
+ function planoParaCenas(plano, { fotos = [] } = {}) {
20
+ const ultima = plano.cenas.length - 1;
21
+ return plano.cenas.map((c, i) => {
22
+ let tipo = HEROI_PARA_TIPO[c.heroi] || 'cinetico';
23
+ let valor;
24
+ if (c.heroi === 'numero') { const m = sem(c.texto).match(/\d[\d.,:%hH]*/); if (m) valor = m[0]; else tipo = 'cinetico'; }
25
+ if (tipo === 'foto' && !fotos.length) tipo = 'cinetico';
26
+ const cena = { papel: i === ultima ? 'cta' : (PAPEL_PARA_PAPEL[c.papel] || 'prova'), tipo: i === ultima && tipo === 'cinetico' ? 'cta' : tipo, titulo: c.texto, dur: c.duracao_s };
27
+ if (cena.tipo === 'cta') cena.botao = plano.marca || c.palavra_chave || sem(c.texto).split(' ')[0];
28
+ if (valor) { cena.valor = valor; cena.rotulo = sem(c.texto).replace(valor, '').trim() || undefined; }
29
+ if (cena.tipo === 'foto') cena.foto = Math.max(0, fotos.indexOf(c.foto));
30
+ // a peça do Destrava que o kit desenha (kit-ae/destrava.jsx): foto → cartão; cor cheia → círculo; marca → fecho; o resto → frase
31
+ cena.dz = c.heroi === 'foto' && cena.tipo === 'foto' ? 'foto' : c.heroi === 'cor_cheia' ? 'cor' : c.heroi === 'marca' && plano.marca ? 'marca' : (c.heroi === 'numero' && cena.tipo === 'numero' ? undefined : (c.heroi === 'foto' && cena.tipo === 'foto' ? undefined : 'frase'));
32
+ if (cena.dz) { cena.modo = MODO_DA_ENTRADA[c.entrada] || 'suave'; cena.tam = c.tamanho_px; }
33
+ return cena;
34
+ });
35
+ }
36
+
37
+ /** Os argumentos do estudio_motion a partir do plano. Puro. */
38
+ function argumentosDoMotor(plano, base = {}, { fotos = [] } = {}) {
39
+ const entradas = plano.cenas.map((c) => c.entrada);
40
+ const conta = {};
41
+ entradas.forEach((e) => { const t = TIPOGRAFIA_DA_ENTRADA[e]; if (t) conta[t] = (conta[t] || 0) + 1; });
42
+ const tipografia = Object.keys(conta).sort((a, b) => conta[b] - conta[a])[0] || 'poster';
43
+ const transicoes = plano.cenas.slice(1).map((c) => TRANSICAO_DA_ENTRADA[c.entrada] || 'corte');
44
+ const { cenas: _c, ...resto } = base;
45
+ void _c;
46
+ return { ...resto, assunto: String(plano.assunto || base.assunto || '').slice(0, 160), cenas: planoParaCenas(plano, { fotos }), tipografia, transicoes, camera: base.camera || 'pulso',
47
+ ...(fotos.length ? { fotos } : {}), _planoDasEtapas: true };
48
+ }
49
+
50
+ module.exports = { planoParaCenas, argumentosDoMotor, HEROI_PARA_TIPO };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * passos.js — o "Plano atualizado" na tela: os passos das etapas com ✔ e □, do jeito do protótipo do app
3
+ * (docs/prototipo/primocode-desktop.html, planCell). No app/terminal chega pelo `ctx.linha` (evento 'linha');
4
+ * sem ele, vai pro stderr. O fluxo NÃO espera ninguém: é só mostrar.
5
+ */
6
+ 'use strict';
7
+
8
+ const PASSOS = ['Ajustar o pedido', 'Estruturar as batidas', 'Montar o plano cena por cena', 'Fazer o motion'];
9
+
10
+ /** O bloco de texto: "Plano atualizado · 2/4 feitos" + os passos + detalhes opcionais. Puro. */
11
+ function bloco(feitos, detalhes = {}, passos = PASSOS) {
12
+ const n = Math.min(feitos, passos.length);
13
+ const linhas = [`Plano atualizado · ${n}/${passos.length} feitos`];
14
+ passos.forEach((p, i) => {
15
+ linhas.push(` ${i < n ? '✔' : '□'} ${p}${detalhes[i] ? ` — ${detalhes[i]}` : ''}`);
16
+ for (const extra of (detalhes[`${i}+`] || [])) linhas.push(` ${extra}`);
17
+ });
18
+ return linhas.join('\n');
19
+ }
20
+
21
+ /** Quem acompanha as etapas. `mostrar(texto)` padrão: ctx.linha, senão stderr. */
22
+ function acompanhar(ctx = {}) {
23
+ const mostrar = ctx.linha || ((t) => process.stderr.write(t + '\n'));
24
+ const detalhes = {};
25
+ const registro = [];
26
+ return {
27
+ registro,
28
+ detalhe(i, texto, extras) { detalhes[i] = texto; if (extras) detalhes[`${i}+`] = extras; },
29
+ feito(n) { const t = bloco(n, detalhes); registro.push(t); try { mostrar(t); } catch { /* tela é só enfeite */ } return t; },
30
+ };
31
+ }
32
+
33
+ /** As cenas do plano em linhas curtas, para o passo 3 mostrar. Puro. */
34
+ function linhasDoPlano(plano) {
35
+ return plano.cenas.map((c, i) => `${i + 1}. ${c.texto.replace(/\*/g, '')} [${c.heroi}, ${c.duracao_s} s, ${c.entrada}]`);
36
+ }
37
+
38
+ module.exports = { PASSOS, bloco, acompanhar, linhasDoPlano };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * pele.js — a pele PADRÃO do motion com o plano ativo: a do Destrava (creme, tinta escura, Inter 800, degradê rosa → violeta).
3
+ *
4
+ * Vale quando o pedido NÃO traz cor de marca nem estilo pedido. Kit de marca ativo, cor de marca (args.marca.cores, site lido),
5
+ * paleta/estilo/tipografia pedidos, ou o próprio pedido falando de cor/estilo: a pele do usuário manda e esta não entra. Puro.
6
+ */
7
+ 'use strict';
8
+
9
+ const PELE = Object.freeze({
10
+ cores: Object.freeze({ fundo: '#f2efea', tinta: '#111111', destaque: '#ff4d7d', destaque2: '#7c3aed' }),
11
+ fontes: Object.freeze({ titulo: 'Inter-ExtraBold', texto: 'Inter-ExtraBold', destaque: 'Inter-ExtraBold' }),
12
+ });
13
+
14
+ // o pedido fala de cor ou de estilo: então a escolha é dele, não a pele padrão
15
+ const PEDIU_ESTILO = /\b(cor(es)?|paleta|estilo|tema|visual|minimalista|neon|retr[oô]|vintage|futurista|dark|escur[oa]|claro|colorid[oa]|pastel|azul|verde|vermelh[oa]|amarel[oa]|roxo|laranja|preto|branco|rosa|dourad[oa]|marrom|cinza)\b|#[0-9a-f]{3,6}\b/i;
16
+
17
+ const cheio = (v) => !!v && (Array.isArray(v) ? v.length > 0 : typeof v === 'object' ? Object.keys(v).length > 0 : true);
18
+
19
+ /** A pele do Destrava entra? `kit` = o que marca-kit.preencher devolveu (kit ativo tem `.kit`). */
20
+ function deveUsar(args = {}, kit = null) {
21
+ if (!args._planoDasEtapas) return false; // só com o plano ativo
22
+ if (kit && kit.kit) return false; // kit escolhido pelo usuário
23
+ const m = args.marca && typeof args.marca === 'object' ? args.marca : {};
24
+ if (cheio(m.cores) || cheio(m.fontes)) return false; // cor ou fonte de marca
25
+ if (args.paleta || args.estilo || args.estiloVisual || args.kit || args.pele) return false;
26
+ if (PEDIU_ESTILO.test(String(args.assunto || '') + ' ' + String(args.pedido || ''))) return false;
27
+ return true;
28
+ }
29
+
30
+ /** Os args com a pele do Destrava. Mantém o resto da marca (nome, logo). */
31
+ function aplicar(args) {
32
+ const m = args.marca && typeof args.marca === 'object' ? args.marca : {};
33
+ return { ...args, pele: 'destrava', marca: { ...m, cores: { ...PELE.cores }, fontes: { ...PELE.fontes } } };
34
+ }
35
+
36
+ module.exports = { PELE, deveUsar, aplicar, PEDIU_ESTILO };