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,152 @@
1
+ /**
2
+ * plano.js — etapa 3 do motion: o PLANO cena por cena.
3
+ *
4
+ * { texto, palavra_chave, heroi, duracao_s, entrada, foto?, tamanho_px, papel }
5
+ *
6
+ * O modelo escolhe herói, duração e entrada seguindo a régua do Destrava (regua.js: números, não adjetivos).
7
+ * O código confere o que dá pra conferir e CONSERTA o resto — foto sem foto real, marca fora do fim, duas cores cheias,
8
+ * duração fora de 1,2–5,5 s — porque regra no prompt o modelo esquece. O plano que sai daqui é o que o motor recebe.
9
+ */
10
+ 'use strict';
11
+
12
+ const { pedirJson } = require('./llm.js');
13
+ const { REGUA, tamanho, encaixar, textoDoPrompt } = require('./regua.js');
14
+ const { marcar, contar, MAX_PALAVRAS } = require('./estrutura.js');
15
+ const Fatos = require('./fatos.js');
16
+
17
+ const HEROIS = ['tipografia', 'foto', 'numero', 'cor_cheia', 'marca'];
18
+ const ENTRADAS = ['mascara-sobe', 'escala-assenta', 'palavra-a-palavra', 'zoom-atravessa', 'cartao-voa', 'cor-expande'];
19
+ const ENTRADA_DO_HEROI = { foto: 'cartao-voa', cor_cheia: 'cor-expande', marca: 'mascara-sobe', numero: 'escala-assenta', tipografia: 'palavra-a-palavra' };
20
+
21
+ const sistema = () => `Você é o diretor de motion de um vídeo vertical em PT-BR. Recebe as batidas do roteiro e devolve o PLANO cena por cena, uma cena por batida, na mesma ordem.
22
+ Responda SÓ JSON: {"cenas":[{"texto":"a frase, com a palavra-chave entre *asteriscos*","palavra_chave":"...","heroi":"${HEROIS.join('|')}","duracao_s":3,"entrada":"${ENTRADAS.join('|')}","foto":"caminho de uma foto da lista, só se heroi=foto"}]}
23
+ Significado: heroi = o que domina o quadro. tipografia = a palavra gigante. foto = cartão de foto real. numero = um número real dos fatos em tamanho enorme. cor_cheia = o fundo vira cor cheia (uma única vez no vídeo). marca = a marca real (só na última).
24
+ entrada: mascara-sobe = a linha varre para cima; escala-assenta = nasce grande e assenta; palavra-a-palavra = a frase se monta em pedaços; zoom-atravessa = a câmera atravessa a palavra (use no máximo uma vez); cartao-voa = o cartão de foto voa em 3D; cor-expande = o círculo de cor cresce.
25
+ Não troque o sentido nem o assunto das batidas; pode apertar o texto. NÃO invente fato: datas, nomes, números, lugares e marca só dos fatos.
26
+
27
+ ${textoDoPrompt()}`;
28
+
29
+ const txt = (v, n) => String(v || '').replace(/\s+/g, ' ').trim().slice(0, n);
30
+
31
+ /** Conserta o plano do modelo contra as regras. Puro. Devolve { cenas, ajustes[] }. */
32
+ function consertar(bruto, batidas, { fotos = [], marca = '', alvoS = [15, 30], fonte = null } = {}) {
33
+ const ajustes = [];
34
+ const brutas = Array.isArray(bruto && bruto.cenas) ? bruto.cenas : [];
35
+ const n = batidas.length;
36
+ let cenas = batidas.map((b, i) => {
37
+ const c = brutas[i] || {};
38
+ let texto = txt(c.texto, 120) || b.frase;
39
+ if (contar(texto) > MAX_PALAVRAS) { ajustes.push(`cena ${i + 1}: texto longo, voltou pra frase da batida`); texto = b.frase; }
40
+ if (fonte) {
41
+ const l = Fatos.limpar(texto, fonte);
42
+ if (!l.removidos.length && l.frase && Fatos.norm(l.frase) !== Fatos.norm(texto)) { ajustes.push(`cena ${i + 1}: o fato do usuário voltou inteiro (${l.frase})`); texto = l.frase; }
43
+ if (l.removidos.length) { ajustes.push(`cena ${i + 1}: fato que o usuário não disse (${l.removidos.join(', ')}), ${l.frase ? 'tirado' : 'voltou pra frase da batida'}`); texto = l.frase || b.frase; }
44
+ }
45
+ const m = marcar(texto, c.palavra_chave || b.palavra_chave);
46
+ return {
47
+ papel: b.papel, texto: m.frase, palavra_chave: m.chave,
48
+ heroi: HEROIS.includes(c.heroi) ? c.heroi : 'tipografia',
49
+ duracao_s: Number(c.duracao_s) || REGUA.cena.media_s,
50
+ entrada: ENTRADAS.includes(c.entrada) ? c.entrada : null,
51
+ foto: c.foto ? String(c.foto) : undefined,
52
+ };
53
+ });
54
+ // regra 4: foto só com foto real; no máximo 3 de 9 (≈ 1/3) das cenas
55
+ const maxFoto = fotos.length ? Math.max(1, Math.round(n * REGUA.foto.cenas_com_foto / REGUA.foto.de)) : 0;
56
+ let usadas = 0;
57
+ cenas.forEach((c, i) => {
58
+ if (c.heroi === 'foto') {
59
+ if (!fotos.length || usadas >= maxFoto) { ajustes.push(`cena ${i + 1}: ${fotos.length ? 'foto demais' : 'sem foto real'}, virou tipografia`); c.heroi = 'tipografia'; c.foto = undefined; }
60
+ else { c.foto = fotos.includes(c.foto) ? c.foto : fotos[usadas % fotos.length]; usadas++; }
61
+ } else c.foto = undefined;
62
+ });
63
+ // a pessoa mandou foto real e o modelo não usou nenhuma: a melhor cena de prova (nem a primeira, nem a última) vira o cartão de foto
64
+ if (fotos.length && n >= 4 && !cenas.some((c) => c.heroi === 'foto')) {
65
+ const k = [cenas.findIndex((c, i) => i > 0 && i < n - 1 && c.papel === 'prova' && c.heroi === 'tipografia'), cenas.findIndex((c, i) => i > 0 && i < n - 1 && c.heroi === 'tipografia')].find((i) => i >= 0);
66
+ if (k != null) { cenas[k].heroi = 'foto'; cenas[k].foto = fotos[0]; ajustes.push(`cena ${k + 1}: a foto real não foi usada, entrou aqui`); }
67
+ }
68
+ // regra 6: marca real só na última (e só se existe)
69
+ cenas.forEach((c, i) => {
70
+ const ultima = i === n - 1;
71
+ if (c.heroi === 'marca' && (!ultima || !marca)) { ajustes.push(`cena ${i + 1}: marca fora do fim, virou tipografia`); c.heroi = 'tipografia'; }
72
+ });
73
+ if (marca && cenas[n - 1].heroi !== 'marca') { cenas[n - 1].heroi = 'marca'; ajustes.push('última cena: marca real entrou no fim'); }
74
+ // regra 5: exatamente 1 quadro de cor cheia (quando há ≥ 5 cenas); nunca na primeira nem na marca
75
+ const cheias = cenas.map((c, i) => (c.heroi === 'cor_cheia' ? i : -1)).filter((i) => i >= 0);
76
+ if (n >= 5) {
77
+ let manter = cheias.find((i) => i > 0 && i < n - 1);
78
+ if (manter == null) {
79
+ manter = cenas.findIndex((c, i) => c.papel === 'virada' && i > 0 && c.heroi === 'tipografia');
80
+ if (manter < 0) manter = Math.floor(n * 0.6);
81
+ cenas[manter].heroi = 'cor_cheia';
82
+ ajustes.push(`cena ${manter + 1}: virou o quadro de cor cheia`);
83
+ }
84
+ cenas.forEach((c, i) => { if (c.heroi === 'cor_cheia' && i !== manter) { c.heroi = 'tipografia'; ajustes.push(`cena ${i + 1}: só uma cor cheia, virou tipografia`); } });
85
+ } else cenas.forEach((c) => { if (c.heroi === 'cor_cheia') c.heroi = 'tipografia'; });
86
+ // tempo: 1,2–5,5 s por cena; o total entre 15 e 30 s (escala proporcional, depois reclampa)
87
+ cenas.forEach((c) => { c.duracao_s = Math.min(REGUA.cena.max_s, Math.max(REGUA.cena.min_s, c.duracao_s)); });
88
+ let total = cenas.reduce((a, c) => a + c.duracao_s, 0);
89
+ if (total < alvoS[0] || total > alvoS[1]) {
90
+ const k = (total < alvoS[0] ? alvoS[0] : alvoS[1]) / total;
91
+ cenas.forEach((c) => { c.duracao_s = Math.min(REGUA.cena.max_s, Math.max(REGUA.cena.min_s, c.duracao_s * k)); });
92
+ ajustes.push(`duração total ${total.toFixed(1)} s reescalada para o intervalo de ${alvoS[0]} a ${alvoS[1]} s`);
93
+ }
94
+ cenas.forEach((c, i) => {
95
+ c.duracao_s = Math.round(c.duracao_s * 10) / 10;
96
+ if (!c.entrada) c.entrada = ENTRADA_DO_HEROI[c.heroi];
97
+ if (c.heroi === 'foto') c.entrada = 'cartao-voa';
98
+ if (c.heroi === 'cor_cheia') c.entrada = 'cor-expande';
99
+ if (c.heroi === 'foto' || c.heroi === 'marca') c.tamanho_px = REGUA.texto.apoio_px[1]; // apoio: nunca abaixo de 56 px
100
+ else {
101
+ // o herói nunca fica abaixo do mínimo da régua (150 px) e cabe em até 3 linhas: sobrou palavra, ela sai (a palavra-chave fica)
102
+ const e = encaixar(c.texto);
103
+ if (e.cortou) { ajustes.push(`cena ${i + 1}: texto longo pro herói (150 px, 3 linhas), ficou "${e.texto.replace(/\*/g, '')}"`); c.texto = e.texto; }
104
+ c.tamanho_px = e.tam;
105
+ }
106
+ });
107
+ // variedade: se quase tudo entra do mesmo jeito, cada papel ganha o seu (a régua tem um movimento por cena)
108
+ const POR_PAPEL = { gancho: 'mascara-sobe', virada: 'escala-assenta', prova: 'palavra-a-palavra', fecho: 'mascara-sobe' };
109
+ const freq = {};
110
+ cenas.forEach((c) => { freq[c.entrada] = (freq[c.entrada] || 0) + 1; });
111
+ if (Math.max(...Object.values(freq)) > cenas.length * 0.6) {
112
+ cenas.forEach((c) => { if (!['foto', 'cor_cheia'].includes(c.heroi)) c.entrada = POR_PAPEL[c.papel] || c.entrada; });
113
+ ajustes.push('entradas iguais demais: cada papel ganhou a sua');
114
+ }
115
+ // zoom-atravessa no máximo uma vez
116
+ let z = 0;
117
+ cenas.forEach((c, i) => { if (c.entrada === 'zoom-atravessa' && ++z > 1) { c.entrada = 'escala-assenta'; ajustes.push(`cena ${i + 1}: só um zoom-atravessa`); } });
118
+ total = Math.round(cenas.reduce((a, c) => a + c.duracao_s, 0) * 10) / 10;
119
+ return { cenas, total_s: total, ajustes };
120
+ }
121
+
122
+ /** O que a régua ainda reprova depois do conserto (deve ser vazio). Puro. */
123
+ function conferir(plano, { fotos = [], marca = '' } = {}) {
124
+ const erros = [];
125
+ const cs = plano.cenas;
126
+ if (cs.filter((c) => c.heroi === 'cor_cheia').length > 1) erros.push('mais de uma cor cheia');
127
+ if (cs.length >= 5 && cs.filter((c) => c.heroi === 'cor_cheia').length !== 1) erros.push('falta o quadro de cor cheia');
128
+ if (cs.some((c, i) => c.heroi === 'marca' && i !== cs.length - 1)) erros.push('marca fora do fim');
129
+ if (cs.some((c) => c.heroi === 'foto' && !fotos.length)) erros.push('foto sem foto real');
130
+ if (marca && cs[cs.length - 1].heroi !== 'marca') erros.push('sem marca no fim');
131
+ if (cs.some((c) => c.duracao_s < REGUA.cena.min_s || c.duracao_s > REGUA.cena.max_s)) erros.push('cena fora de 1,2 a 5,5 s');
132
+ if (cs.some((c) => !/\*[^*]+\*/.test(c.texto))) erros.push('frase sem palavra-chave');
133
+ return erros;
134
+ }
135
+
136
+ /** @param {object} briefing @param {Array} batidas @param {object} deps { llm } */
137
+ async function montarPlano(briefing, batidas, deps = {}) {
138
+ const llm = deps.llm;
139
+ if (!llm) return { ok: false, error: 'sem modelo para montar o plano.' };
140
+ const fotos = (briefing.fotos || []).map(String);
141
+ const marca = (briefing.fatos && briefing.fatos.marca) || '';
142
+ const corpo = JSON.stringify({ batidas: batidas.map((b) => ({ papel: b.papel, frase: b.frase, palavra_chave: b.palavra_chave })), fatos: briefing.fatos, tom: briefing.tom, fotos_reais: fotos, marca_real: marca || null });
143
+ const bruto = await pedirJson(llm, [{ role: 'system', content: sistema() }, { role: 'user', content: corpo }], { temperatura: 0.6, maxTokens: 4000 });
144
+ const alvoS = deps.alvoS || [15, 30];
145
+ const r = consertar(bruto, batidas, { fotos, marca, alvoS, fonte: deps.fonte });
146
+ const plano = { assunto: briefing.pedido, objetivo: briefing.objetivo, tom: briefing.tom, virada: briefing.virada, marca: marca || null, cenas: r.cenas, total_s: r.total_s, ajustes: r.ajustes };
147
+ const erros = conferir(plano, { fotos, marca });
148
+ if (erros.length) return { ok: false, error: `plano reprovado pela régua: ${erros.join('; ')}`, plano };
149
+ return { ok: true, plano };
150
+ }
151
+
152
+ module.exports = { montarPlano, consertar, conferir, HEROIS, ENTRADAS, sistema };
@@ -0,0 +1,100 @@
1
+ /**
2
+ * regua.js — a gramática do vídeo "Destrava Essencial" (aprovada pelo Joel), em NÚMEROS.
3
+ *
4
+ * Fonte: ~/www/videos/destrava-essencial-pc/video (src/style.json, src/lib/anim.ts, src/scenes/Kinetic.tsx,
5
+ * src/scene-times.json). Canvas 1080×1920, 30 fps, 9 cenas, 27 s. Cada número abaixo aponta de onde veio.
6
+ * Os frames viram segundos aqui (÷ 30) porque o plano fala em segundos.
7
+ *
8
+ * As 6 regras:
9
+ * 1 tipografia enorme é o herói; só a palavra-chave ganha degradê/cor de destaque
10
+ * 2 uma ideia por quadro; a frase se monta em pedaços na hora da fala
11
+ * 3 copy com virada
12
+ * 4 fotos reais do assunto em cartões arredondados que se mexem; nunca UI/ícone inventado
13
+ * 5 um único quadro de cor cheia quebra o fundo
14
+ * 6 marca real só no fim
15
+ */
16
+ 'use strict';
17
+
18
+ const FPS = 30;
19
+ const s = (frames) => Math.round((frames / FPS) * 100) / 100;
20
+
21
+ // scene-times.json: [0,101,265,359,395,463,518,605,691] + fim 810 → duração de cada cena
22
+ const CENAS_S = [101, 164, 94, 36, 68, 55, 87, 86, 119].map(s);
23
+
24
+ const REGUA = Object.freeze({
25
+ fps: FPS,
26
+ // — tempo —
27
+ cena: { min_s: Math.min(...CENAS_S), max_s: Math.round(Math.max(...CENAS_S) * 10) / 10, media_s: s(810 / 9), exemplos_s: CENAS_S }, // 1,2 a 5,5 s; média 3 s
28
+ video: { cenas: 9, duracao_s: 27 },
29
+ palavra: { entrada_s: s(12), passo_entre_palavras_s: [s(4), s(5)], blur_px: 10, sobe: 0.28, mola: { damping: 11, stiffness: 140, mass: 0.7 } }, // style.json motion.word
30
+ linha: { varre_s: s(18), assenta_s: s(22), escala_de: [1.3, 1.7], tracking_em: [0.05, -0.045], blur_px: 9 }, // Kinetic.tsx Line
31
+ cena_entra_s: s(12), cena_sai_s: s(10), cena_sobreposicao_s: s(17), cena_desloca_px: 260, cena_blur_px: 16, // Kinetic.tsx Scn / style.json scene
32
+ easing: { saida: [0.22, 1, 0.36, 1], entra_e_sai: [0.65, 0, 0.35, 1], e5: '1-(1-t)^5' }, // style.json ease; Kinetic e5
33
+ batida_s: s(14.4), // 14,4 f ≈ 0,48 s
34
+ camera: { deriva_zoom: [1.018, 0.012], soco_zoom: 0.03, soco_decai_s: s(7), soco_na_batida: true }, // camAt()
35
+ atravessa: { zoom_final: 12, s: s(17) }, // S7 zoom-through
36
+ // — tipografia (px em 1080 de largura) —
37
+ texto: { peso: [800, 900], linhas_max: 2, palavras_por_linha_max: 7, tracking_em: -0.02, entreLinhas: 1.08, destaque_por_frase: 1,
38
+ apoio_px: [64, 118], heroi_px: [150, 410], heroi_linhas_max: 3, apoio_min_px: 56 }, // style.json font; Kinetic sizes
39
+ // — cor cheia, foto, marca —
40
+ cor_cheia: { quadros: 1, de: 9, expande_s: s(16) }, // S8: círculo radial, 1 cena
41
+ foto: { cartao_raio_px: 36, sombra: '0 40px 90px rgba(29,29,31,.28)', cartoes: [[400, 530], [980, 1060], [330, 440]], cenas_com_foto: 3, de: 9, voa_s: s(14) },
42
+ marca: { cenas: 1, de: 9, posicao: 'ultima', revela_s: s(26) }, // S9: logo só no fim
43
+ });
44
+
45
+ /** Tamanho do herói em px pela contagem de palavras (Destrava: "Não." 410 · "IA." 282 · "Usar IA" 248 · "Mais uma palavra" 180–210 · "Sábado," 150). Nunca abaixo do mínimo da régua (150). */
46
+ function tamanho(nPalavras) {
47
+ const min = REGUA.texto.heroi_px[0];
48
+ if (nPalavras <= 1) return 360;
49
+ if (nPalavras === 2) return 250;
50
+ if (nPalavras === 3) return 200;
51
+ return min;
52
+ }
53
+
54
+ /** Quantas linhas o texto ocupa em `tam` px (Inter 800, tracking -0.02 em ≈ 0,6 em por letra; a palavra-chave, 18% maior). Mesma conta do kit. */
55
+ function linhasDe(texto, tam, { largura = 1080 * (1 - 2 * 0.08) * 0.98 } = {}) {
56
+ const ps = String(texto || '').split(/\s+/).filter(Boolean);
57
+ const comKw = ps.length > 1;
58
+ let linhas = 1, uso = 0;
59
+ for (const p of ps) {
60
+ const n = p.replace(/\*/g, '').length, w0 = n * tam * 0.58;
61
+ const t = Math.min(p.includes('*') && comKw ? tam * Math.min(1.18, Math.max(1, 0.98 * largura / w0)) : tam, REGUA.texto.heroi_px[1]); // a palavra-chave cresce até 18%, só enquanto cabe
62
+ const w = n * t * 0.58, esp = tam * 0.27;
63
+ if (w > largura) return 99; // palavra que não cabe sozinha na linha
64
+ if (uso && uso + esp + w > largura) { linhas++; uso = w; } else uso = uso ? uso + esp + w : w;
65
+ }
66
+ return linhas;
67
+ }
68
+
69
+ /** O herói que cabe: no mínimo da régua (150 px) ele tem que caber em até 3 linhas; sobrando, tira palavras longe da palavra-chave (começo antes do fim).
70
+ * O tamanho é o da contagem de palavras, reduzido em passos de 8% só até caber, e nunca abaixo de 150. Devolve { texto, tam, cortou }. */
71
+ function encaixar(texto, { max = REGUA.texto.heroi_linhas_max } = {}) {
72
+ const min = REGUA.texto.heroi_px[0];
73
+ let ps = String(texto || '').split(/\s+/).filter(Boolean), cortou = false;
74
+ const kw = () => Math.max(0, ps.findIndex((p) => p.includes('*')));
75
+ while (ps.length > 1 && linhasDe(ps.join(' '), min) > max) {
76
+ const k = kw();
77
+ if (k > 0 && k >= ps.length - 1 - k) ps = ps.slice(1); else if (k < ps.length - 1) ps = ps.slice(0, -1); else ps = ps.slice(1);
78
+ cortou = true;
79
+ }
80
+ const t = ps.join(' ');
81
+ let tam = tamanho(ps.length);
82
+ while (tam > min && linhasDe(t, tam) > max) tam = Math.max(min, Math.round(tam * 0.92));
83
+ return { texto: t, tam, cortou };
84
+ }
85
+
86
+ /** O bloco de regras que vai no prompt do plano. Números, não adjetivos. */
87
+ function textoDoPrompt() {
88
+ const R = REGUA;
89
+ return `RÉGUA (medida num vídeo aprovado de 27 s, 9 cenas, 1080×1920, 30 fps):
90
+ 1. TIPOGRAFIA É O HERÓI. Texto de herói: ${R.texto.heroi_px[0]} a ${R.texto.heroi_px[1]} px, peso 800 a 900, no máximo ${R.texto.heroi_linhas_max} linhas e ${R.texto.palavras_por_linha_max} palavras por linha; texto de apoio ${R.texto.apoio_px[0]} a ${R.texto.apoio_px[1]} px. Tamanho por palavras: 1 palavra ≈ 360 px, 2 ≈ 250, 3 ≈ 200, 4 ou mais = 150 (nunca abaixo disso; o texto herói é curto, até 3 linhas, com a palavra-chave maior). SÓ a palavra-chave leva a cor de destaque (${R.texto.destaque_por_frase} por frase).
91
+ 2. UMA IDEIA POR QUADRO. A frase se monta em pedaços: cada palavra entra em ${R.palavra.entrada_s} s, com ${R.palavra.passo_entre_palavras_s[0]} a ${R.palavra.passo_entre_palavras_s[1]} s entre palavras (blur ${R.palavra.blur_px} px sumindo, sobe ${R.palavra.sobe * 100}% do tamanho). A linha de herói varre para cima em ${R.linha.varre_s} s e assenta em ${R.linha.assenta_s} s, vinda de escala ${R.linha.escala_de[0]} a ${R.linha.escala_de[1]}.
92
+ 3. COPY COM VIRADA. A pergunta que o público faria (ou o clichê do assunto) no gancho, depois a resposta curta que quebra a expectativa, específica do assunto. Nada de frase de catálogo.
93
+ 4. FOTO REAL EM CARTÃO ARREDONDADO (raio ${R.foto.cartao_raio_px} px, sombra suave) que se mexe: voa em ${R.foto.voa_s} s, carrossel 3D ou grade inclinada. ${R.foto.cenas_com_foto} de ${R.foto.de} cenas levam foto, e SÓ se existe foto real do assunto. Sem foto, a cena é só tipografia. Nunca UI, ícone ou ilustração inventada.
94
+ 5. UM ÚNICO QUADRO DE COR CHEIA quebra o fundo (${R.cor_cheia.quadros} de ${R.cor_cheia.de} cenas; o círculo de cor expande em ${R.cor_cheia.expande_s} s). Só um.
95
+ 6. MARCA REAL SÓ NO FIM: a última cena (e só ela) mostra a marca, se a marca está nos fatos.
96
+ TEMPO: cena de ${R.cena.min_s} a ${R.cena.max_s} s, média ${R.cena.media_s} s (cena de uma palavra de virada é a mais curta, 1,2 a 1,8 s; cena que explica pode ir a 4 a 5,5 s). Vídeo de 15 a 30 s. Troca de cena: entra em ${R.cena_entra_s} s com deslocamento de ${R.cena_desloca_px} px e blur ${R.cena_blur_px} px, sai em ${R.cena_sai_s} s, sobrepõe ${R.cena_sobreposicao_s} s. Câmera: deriva contínua de ±${R.camera.deriva_zoom[1] * 100}% de zoom e um soco de ${R.camera.soco_zoom * 100}% que decai em ${R.camera.soco_decai_s} s, na batida (a cada ${R.batida_s} s).
97
+ EASING: saída cubic-bezier(${R.easing.saida.join(', ')}); entra-e-sai cubic-bezier(${R.easing.entra_e_sai.join(', ')}); assentar = ${R.easing.e5}. Nunca linear.`;
98
+ }
99
+
100
+ module.exports = { REGUA, tamanho, linhasDe, encaixar, textoDoPrompt, CENAS_S };
@@ -485,6 +485,8 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
485
485
  const partitura = partituraDe({ roteiro: { ...roteiro, partes }, largura, altura, tempos });
486
486
  if (!partitura.falas.length) return { ok: false, error: 'o roteiro veio sem fala: o personagem precisa de texto em cada parte.' };
487
487
  const DUR = partitura.duracao;
488
+ // motor novo (kits/3d): a mesma partitura, traduzida para o montador de maquete. O plano antigo segue junto.
489
+ const roteiro3d = roteiro.motor === 'montador' ? require('./dialogo3d.js').roteiroDeDialogo(partitura) : undefined;
488
490
  const efeitos = [];
489
491
  const som = (t, nome, volume) => { if (sons[nome]) efeitos.push({ t: r2(Math.max(0, t)), src: sons[nome], volume }); };
490
492
  for (const f of partitura.falas) if (f.destaque) som(f.de + 0.3, 'pop', 0.16);
@@ -501,7 +503,7 @@ function montarPersonagem({ roteiro, largura = 1920, altura = 1080, tempos = [],
501
503
  },
502
504
  };
503
505
  return {
504
- ok: true, plano, partitura,
506
+ ok: true, plano, partitura, ...(roteiro3d ? { roteiro3d } : {}),
505
507
  resumo: { visual: partitura.visual, cenas: 1, segundos: DUR, fps: FPS, cenario: partitura.cenario.nome, elenco: partitura.elenco.map((e) => `${e.rotulo} (${e.tipo === 'animal' ? e.especie : e.tipo})`),
506
508
  figurantes: partitura.figurantes.map((e) => (e.tipo === 'animal' ? e.especie : e.tipo)), lugares: partitura.mundos.map((m) => m.nome), planos: partitura.planos.length, falas: partitura.falas.length },
507
509
  };
@@ -87,7 +87,7 @@ function instrucaoDoModo({ modo, escopo, explicito } = ATUAL) {
87
87
  + 'CRIE a cena: escreva um React (write_file, ex.: componentes/cena-x.jsx, export default ({el, t}) => …, t em segundos desde o '
88
88
  + 'começo da parte; el.dados traz palavras [{texto, t}], cores {fundo, texto, acento}, fontes, largura, altura) e ponha '
89
89
  + 'componente:"componentes/cena-x.jsx" naquela parte. A voz, a trilha, os sons e o fundo continuam do Estúdio. '
90
- + 'Vídeo inteiro desenhado por você: escreva uma composição Hyperframes (pasta com index.html, data-start/data-duration, GSAP), '
90
+ + 'Vídeo inteiro desenhado por você: escreva uma composição Hyperframes (pasta com index.html, data-start/data-duration, GSAP: use `gsap.` direto, sem <script src> nem CDN, o motor embute o GSAP local), '
91
91
  + 'use a voz do estudio_narrar e os sons de midia/, e estudio_render motor:"hyperframes" html:"<pasta>". '
92
92
  /* O ROTEIRO (13/10: "o roteiro que monta é horrível" — eram os títulos da landing em fila). */
93
93
  + 'O ROTEIRO é uma história de 15 a 25 s: gancho (a situação de quem assiste) → o que o produto FAZ → UMA prova concreta '
@@ -0,0 +1,161 @@
1
+ /**
2
+ * catalogo.js — o catálogo de modelos, lido do models.dev (licença MIT).
3
+ *
4
+ * Mesma ideia do OpenCode (packages/core/src/models-dev.ts): uma lista pública
5
+ * de provedores e modelos, com env da chave, URL e capacidades de cada um.
6
+ * Três camadas, da mais nova para a mais velha:
7
+ *
8
+ * 1. rede — GET https://models.dev/api.json, com ETag (304 não rebaixa 6 MB)
9
+ * 2. cache — ~/.primocode/cache/models-dev.json, vale 12 h (stale ainda serve)
10
+ * 3. embutido — lib/provider/local/catalogo-embutido.json.gz, para offline na 1ª vez
11
+ *
12
+ * O formato guardado é ENXUTO (só o que o PrimoCode usa), não o JSON cru:
13
+ * { provedores: { <id>: { nome, env:[], api, modelos: { <id>: {
14
+ * nome, ferr, visao, raciocinio, ctx, saida, custoIn, custoOut } } } } }
15
+ * Só entram provedores que falam o formato OpenAI (/chat/completions): os que
16
+ * têm `api` no catálogo (npm openai-compatible) e os que o catálogo deixa sem
17
+ * `api` mas cuja URL é conhecida (BASES_CONHECIDAS, abaixo).
18
+ *
19
+ * Nenhuma chave e nenhuma URL privada moram aqui. Gateway da empresa entra
20
+ * pelo `~/.primocode/provedores.json` da pessoa (provedor.js), não por este arquivo.
21
+ */
22
+
23
+ 'use strict';
24
+
25
+ const fs = require('fs');
26
+ const zlib = require('zlib');
27
+ const os = require('os');
28
+ const path = require('path');
29
+
30
+ const URL_CATALOGO = 'https://models.dev/api.json';
31
+ const VALIDADE_MS = 12 * 60 * 60 * 1000;
32
+ const EMBUTIDO = path.join(__dirname, 'local', 'catalogo-embutido.json.gz'); // gzip: 1 MB -> ~150 KB no pacote
33
+ const CACHE = () => path.join(os.homedir(), '.primocode', 'cache', 'models-dev.json');
34
+
35
+ /* Provedores que o models.dev lista sem `api` (usam um SDK próprio), mas que
36
+ expõem /chat/completions no formato OpenAI nesta URL pública. */
37
+ const BASES_CONHECIDAS = {
38
+ groq: 'https://api.groq.com/openai/v1',
39
+ openai: 'https://api.openai.com/v1',
40
+ google: 'https://generativelanguage.googleapis.com/v1beta/openai',
41
+ xai: 'https://api.x.ai/v1',
42
+ mistral: 'https://api.mistral.ai/v1',
43
+ cerebras: 'https://api.cerebras.ai/v1',
44
+ deepseek: 'https://api.deepseek.com/v1',
45
+ togetherai: 'https://api.together.xyz/v1',
46
+ perplexity: 'https://api.perplexity.ai',
47
+ deepinfra: 'https://api.deepinfra.com/v1/openai',
48
+ anthropic: 'https://api.anthropic.com/v1',
49
+ };
50
+
51
+ /** Do JSON cru do models.dev para o formato enxuto. */
52
+ function normalizar(cru) {
53
+ const provedores = {};
54
+ for (const [id, p] of Object.entries(cru || {})) {
55
+ if (!p || typeof p !== 'object' || !p.models) continue;
56
+ const env = Array.isArray(p.env) ? p.env.filter(Boolean) : [];
57
+ const api = p.api || BASES_CONHECIDAS[id];
58
+ if (!api || !env.length) continue;
59
+ const modelos = {};
60
+ for (const [mid, m] of Object.entries(p.models)) {
61
+ if (!m) continue;
62
+ const saidas = (m.modalities && m.modalities.output) || ['text'];
63
+ if (!saidas.includes('text') || m.type) continue; // sem áudio/imagem/embedding
64
+ if (m.status === 'deprecated') continue;
65
+ const ent = (m.modalities && m.modalities.input) || ['text'];
66
+ modelos[mid] = {
67
+ nome: m.name || mid,
68
+ ferr: !!m.tool_call,
69
+ visao: ent.includes('image'),
70
+ raciocinio: !!m.reasoning,
71
+ ctx: (m.limit && m.limit.context) || 0,
72
+ saida: (m.limit && m.limit.output) || 0,
73
+ custoIn: m.cost && typeof m.cost.input === 'number' ? m.cost.input : null,
74
+ custoOut: m.cost && typeof m.cost.output === 'number' ? m.cost.output : null,
75
+ };
76
+ }
77
+ if (Object.keys(modelos).length) provedores[id] = { nome: p.name || id, env, api, modelos };
78
+ }
79
+ return { provedores };
80
+ }
81
+
82
+ function lerJson(arq) {
83
+ try { return JSON.parse(fs.readFileSync(arq, 'utf8')); } catch { return null; }
84
+ }
85
+
86
+ function valido(c) {
87
+ return !!(c && c.provedores && typeof c.provedores === 'object' && Object.keys(c.provedores).length);
88
+ }
89
+
90
+ /** Cache + embutido, sem rede (a CLI precisa listar na hora). */
91
+ function carregarSync() {
92
+ const cache = lerJson(CACHE());
93
+ if (cache && valido(cache.dados)) return { ...cache.dados, origem: 'cache', baixadoEm: cache.baixadoEm };
94
+ let emb = null;
95
+ try { emb = JSON.parse(zlib.gunzipSync(fs.readFileSync(EMBUTIDO)).toString('utf8')); } catch { /* sem embutido */ }
96
+ return valido(emb) ? { ...emb, origem: 'embutido' } : { provedores: {}, origem: 'vazio' };
97
+ }
98
+
99
+ function cacheFresco() {
100
+ const cache = lerJson(CACHE());
101
+ return !!(cache && valido(cache.dados) && Date.now() - cache.baixadoEm < VALIDADE_MS);
102
+ }
103
+
104
+ /**
105
+ * Baixa do models.dev se o cache venceu (ou `forcar`). Nunca lança: offline,
106
+ * fica o que já havia. Devolve { ok, origem, erro? }.
107
+ */
108
+ async function atualizar({ forcar = false, buscar = fetch, agora = Date.now } = {}) {
109
+ if (process.env.PRIMOCODE_CATALOGO_OFFLINE === '1') return { ok: false, origem: carregarSync().origem, erro: 'offline' };
110
+ if (!forcar && cacheFresco()) return { ok: true, origem: 'cache' };
111
+ const antigo = lerJson(CACHE());
112
+ try {
113
+ const headers = antigo && antigo.etag ? { 'If-None-Match': antigo.etag } : {};
114
+ const r = await buscar(process.env.PRIMOCODE_CATALOGO_URL || URL_CATALOGO, { headers, signal: AbortSignal.timeout(20000) });
115
+ if (r.status === 304 && antigo) {
116
+ gravar({ ...antigo, baixadoEm: agora() });
117
+ return { ok: true, origem: 'cache' };
118
+ }
119
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
120
+ const dados = normalizar(await r.json());
121
+ if (!valido(dados)) throw new Error('catálogo vazio');
122
+ gravar({ baixadoEm: agora(), etag: (r.headers && r.headers.get && r.headers.get('etag')) || '', dados });
123
+ return { ok: true, origem: 'rede' };
124
+ } catch (e) {
125
+ return { ok: false, origem: carregarSync().origem, erro: e.message };
126
+ }
127
+ }
128
+
129
+ function gravar(obj) {
130
+ const arq = CACHE();
131
+ fs.mkdirSync(path.dirname(arq), { recursive: true });
132
+ const tmp = `${arq}.${process.pid}.tmp`;
133
+ fs.writeFileSync(tmp, JSON.stringify(obj));
134
+ fs.renameSync(tmp, arq);
135
+ }
136
+
137
+ /** Todos os modelos como "provedor/modelo", com as capacidades. */
138
+ function listar({ catalogo = carregarSync(), provedor = '', filtro = '', soFerramentas = false, soGratis = false } = {}) {
139
+ const f = String(filtro || '').toLowerCase();
140
+ const saida = [];
141
+ for (const [pid, p] of Object.entries(catalogo.provedores)) {
142
+ if (provedor && pid !== provedor) continue;
143
+ for (const [mid, m] of Object.entries(p.modelos)) {
144
+ if (soFerramentas && !m.ferr) continue;
145
+ if (soGratis && !(m.custoIn === 0 && m.custoOut === 0)) continue;
146
+ const ref = `${pid}/${mid}`;
147
+ if (f && !`${ref} ${m.nome}`.toLowerCase().includes(f)) continue;
148
+ saida.push({ ref, provedor: pid, id: mid, ...m });
149
+ }
150
+ }
151
+ return saida.sort((a, b) => a.ref.localeCompare(b.ref));
152
+ }
153
+
154
+ /** Capacidades de um modelo, ou null se o catálogo não o conhece (aí vale o benefício da dúvida). */
155
+ function capacidades(provedor, modelo, catalogo = carregarSync()) {
156
+ const p = catalogo.provedores[provedor];
157
+ const m = p && p.modelos[modelo];
158
+ return m ? { ferramentas: m.ferr, visao: m.visao, raciocinio: m.raciocinio, contexto: m.ctx, saida: m.saida, custoIn: m.custoIn, custoOut: m.custoOut } : null;
159
+ }
160
+
161
+ module.exports = { URL_CATALOGO, VALIDADE_MS, BASES_CONHECIDAS, normalizar, carregarSync, atualizar, listar, capacidades, CACHE };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "origem": "primocode-server 8.20.0 46ada83",
3
- "SYSTEM_NUCLEO": "Você é o PrimoCode, agente de engenharia da ConectaPrimoAI.\n\nCONVERSA OU TRABALHO: cumprimento, agradecimento, pergunta sobre você ou papo\n(\"oi\", \"o que você faz?\") → responda em texto, 1 ou 2 frases, sem tool nenhuma,\ne não invente tarefa que ninguém pediu. Pedido de criar, alterar, ler, rodar ou\nconsertar → EXECUTE, até o fim, sem conversar no meio.\n\nREGRAS ABSOLUTAS (executando):\n1. NUNCA escreva código na resposta. Código vai SEMPRE dentro de uma tool.\n2. NUNCA pergunte ao usuário. Decida e execute.\n3. NUNCA invente ferramenta, serviço, porta ou API: as suas são EXATAMENTE as\n desta lista. E nunca diga que algo \"não está rodando\" para se esquivar — o\n que precisa subir, sobe sozinho quando você chama a tool. Prometer o que\n você não tem, ou culpar um serviço, é o pior erro possível.\n4. Caminhos RELATIVOS (\"index.html\", \"src/game.js\"). Você já está na pasta do\n projeto: nunca caminho absoluto, nunca \"../\".\n5. Termine SEMPRE com finish(\"frase curta do que foi feito\").\n6. Resposta em texto: no máximo 1 frase, sem markdown.\n7. NUNCA repita uma chamada que já fez. Reformular a mesma busca traz as mesmas\n fontes; spec e manual são FIXOS — leu uma vez, está lido. Pesquisou e leu a\n spec? O passo seguinte é CRIAR com o que já tem.\n\nESCOLHENDO A TOOL: arquivo novo → write_file; mudar parte → edit_file (nunca\nreescreva o arquivo inteiro para trocar poucas linhas); várias mudanças no mesmo\narquivo → multi_edit, numa chamada; não sabe onde está → grep (conteúdo) ou glob\n(nome), nunca list_dir (o mapa já está aqui); servidor de pé → run_background.\n\nANTES DE EDITAR, LEIA SÓ O ARQUIVO QUE VAI MUDAR (edit_file exige o texto\nliteral), inteiro e uma vez. Nada de ler outros \"para entender\".\n\nQUANDO FALHA: leia o erro e conserte. Se edit_file disser \"trecho não\nencontrado\", ele devolve \"trechosParecidos\" com as linhas reais — copie de lá.\nNUNCA chame finish dizendo que fez algo que falhou: \"não consegui alterar X\nporque Y\" é melhor que um \"pronto!\" falso.\n\nPLANO: tarefa de 3+ passos começa com write_todos e vai atualizando o status (só\nUMA \"in_progress\" por vez). Tarefa curta não precisa.\n\nCOMO VOCÊ FALA ENQUANTO TRABALHA: antes de uma tool que muda o rumo (conserto,\nconversão, desvio), UMA frase em primeira pessoa com o quê e o porquê (\"o vídeo\nveio em .avi, vou converter para mp4\"); resolvido, \"feito, continuando\". Passo\nóbvio não precisa. NUNCA cite limite, cota, token, modelo, provedor, tentativa\nou erro interno: quem lê vê o trabalho, não o encanamento. Contornou uma\nfalha? Conte o contorno.\n\nMEMÓRIA: você recebe memory-geral.md (o usuário) e o memory.md do projeto.\nRespeite as preferências e decisões que estão lá, e não recomece do zero um\nprojeto que já tem arquivos — veja o mapa antes de escrever. O memory.md do projeto se\ngrava sozinho. Já o memory-geral.md só guarda o que você mandar com\nremember_general: nome, preferência que vale em qualquer projeto, decisão de\nlongo prazo. Sem essa chamada, nada disso sobrevive para a próxima vez.\n\nPROCESSO: ache o alvo (mapa ou grep) → leia só ele → faça → confira → finish.\nCriar do zero: escreva direto, sem ler nada.",
3
+ "SYSTEM_NUCLEO": "Você é o PrimoCode, agente de engenharia da ConectaPrimoAI.\n\nCONVERSA OU TRABALHO: cumprimento, agradecimento, pergunta sobre você ou papo\n(\"oi\", \"o que você faz?\") → responda em texto, 1 ou 2 frases, sem tool nenhuma,\ne não invente tarefa que ninguém pediu. Pedido de criar, alterar, ler, rodar ou\nconsertar → EXECUTE, até o fim, sem conversar no meio.\n\nREGRAS ABSOLUTAS (executando):\n1. NUNCA escreva código na resposta. Código vai SEMPRE dentro de uma tool.\n2. NUNCA pergunte ao usuário. Decida e execute.\n3. NUNCA invente ferramenta, serviço, porta ou API: as suas são EXATAMENTE as\n desta lista. E nunca diga que algo \"não está rodando\" para se esquivar — o\n que precisa subir, sobe sozinho quando você chama a tool. Prometer o que\n você não tem, ou culpar um serviço, é o pior erro possível.\n4. Caminhos RELATIVOS (\"index.html\", \"src/game.js\"). Você já está na pasta do\n projeto: nunca caminho absoluto, nunca \"../\".\n5. Termine SEMPRE com finish(\"frase curta do que foi feito\").\n6. Resposta em texto: no máximo 1 frase, sem markdown.\n7. NUNCA repita uma chamada que já fez. Reformular a mesma busca traz as mesmas\n fontes; spec e manual são FIXOS — leu uma vez, está lido. Pesquisou e leu a\n spec? O passo seguinte é CRIAR com o que já tem.\n\nESCOLHENDO A TOOL: arquivo novo → write_file; mudar parte → edit_file (nunca\nreescreva o arquivo inteiro para trocar poucas linhas); várias mudanças no mesmo\narquivo → multi_edit, numa chamada; não sabe onde está → grep (conteúdo) ou glob\n(nome), nunca list_dir (o mapa já está aqui); servidor de pé → run_background.\n\nANTES DE EDITAR, LEIA SÓ O ARQUIVO QUE VAI MUDAR (edit_file exige o texto\nliteral), inteiro e uma vez. Nada de ler outros \"para entender\".\n\nQUANDO FALHA: leia o erro e conserte. Se edit_file disser \"trecho não\nencontrado\", ele devolve \"trechosParecidos\" com as linhas reais — copie de lá.\nNUNCA chame finish dizendo que fez algo que falhou: \"não consegui alterar X\nporque Y\" é melhor que um \"pronto!\" falso.\n\nPLANO: tarefa de 3+ passos começa com write_todos e vai atualizando o status (só\nUMA \"in_progress\" por vez). Tarefa curta não precisa.\n\nCOMO VOCÊ FALA ENQUANTO TRABALHA: antes de uma tool que muda o rumo (conserto,\nconversão, desvio), UMA frase em primeira pessoa com o quê e o porquê (\"o vídeo\nveio em .avi, vou converter para mp4\"); resolvido, \"feito, continuando\". Passo\nóbvio não precisa. NUNCA cite limite, cota, token, modelo, provedor, tentativa\nou erro interno: quem lê vê o trabalho, não o encanamento. Contornou uma\nfalha? Conte o contorno.\n\nTOM (como um engenheiro sênior): seguro e direto. Português do Brasil, frases curtas, uma ideia por frase.\nDê o dado concreto: arquivo, linha, comando, número. Diga o que fez e o que conferiu (\"rodei npm test: 42 ok\").\nNão sabe ou não conferiu? Diga com clareza (\"não rodei os testes\"). Proibido: \"Claro!\", \"Ótima pergunta\",\nelogio, desculpa em excesso, enrolação e emoji. Pouco jargão: se usar termo técnico, ele tem que servir ao leitor.\n\nMEMÓRIA: você recebe memory-geral.md (o usuário) e o memory.md do projeto.\nRespeite as preferências e decisões que estão lá, e não recomece do zero um\nprojeto que já tem arquivos — veja o mapa antes de escrever. O memory.md do projeto se\ngrava sozinho. Já o memory-geral.md só guarda o que você mandar com\nremember_general: nome, preferência que vale em qualquer projeto, decisão de\nlongo prazo. Sem essa chamada, nada disso sobrevive para a próxima vez.\n\nPROCESSO: ache o alvo (mapa ou grep) → leia só ele → faça → confira → finish.\nCriar do zero: escreva direto, sem ler nada.",
4
4
  "SECOES": {
5
5
  "pesquisa": "PEDIDO QUE DEPENDE DE FATO DO MUNDO — PESQUISE PRIMEIRO:\nSe o que o usuário pediu envolve algo real (uma empresa, um produto, uma\npessoa, um mercado, um número, um acontecimento), chame pesquisar_referencia\nANTES de criar qualquer coisa, em qualquer esforço. \"Pesquise sobre a empresa X\ne faça slides\" é UM pedido só: pesquisa e depois cria, sem parar para perguntar\ne sem entregar o deck com dado inventado.\n\nUm deck bonito cheio de número inventado é pior que nenhum deck — o usuário\nleva aquilo para uma reunião. Se depois da pesquisa faltar um dado, escreva na\npeça o que você sabe e deixe o buraco explícito (\"faturamento: não divulgado\"),\nnunca um número plausível de sua cabeça.",
6
6
  "estudio": "O ESTÚDIO — VÍDEO, MOTION, SLIDES E ARTE, FEITOS AQUI:\nCada peça é uma PASTA no computador do usuário, e o arquivo final sai pelo\nRemotion (.mp4 no vídeo, .pdf em slides e arte). Nada de link para abrir num\nsite: no fim, o arquivo abre sozinho para o usuário.\n\nFLUXO (vídeo, motion, slides ou arte feitos do zero):\n1. estudio_projeto acao:\"criar\" (titulo, pedido, formato: video | slides | arte).\n2. Empresa ou marca de verdade → estudio_site ANTES, e use as cores, a fonte\n e a logo que ele devolve em roteiro.marca.\n3. estudio_plano com ROTEIRO: 6 a 12 partes, cada uma com o tipo certo\n (abertura, numero, lista, passos, contraste, citacao, imagem, grafico,\n titulo, cta) e a \"fala\" — a frase dita, que não repete a tela. O PrimoCode\n faz o layout, a voz com entonação, os ícones, a música e os sons. NÃO\n escreva cenas nem posições à mão.\n4. estudio_render. Para ajustar, mande o roteiro de novo com a mudança.\n\nGRAVAÇÃO DO USUÁRIO FALANDO (\"edita meu vídeo\", \"faz um reels disso\", \"coloca\nlegenda e efeitos\", um .mp4/.mov dele): a edição completa é do editor_pro (o\nmotor do EDIT IA PRO). acao:\"cortar\" com o video → leia a fala que volta (com os\ntempos) → acao:\"montar\" com a pasta e o plano (gancho, pecas, broll) → entregue\n<pasta>/final.mp4 só com o portão PASSOU. Atalho: acao:\"editar\" faz tudo com o\nplano automático. Operações soltas na gravação (só cortar um trecho, juntar,\nexportar) seguem no estudio_video. NUNCA escreva .mp4/.mp3 com write_file e\nnunca monte vídeo com ffmpeg na mão.\n\nO ROTEIRO BOM: a primeira parte prende (abertura ou contraste), o miolo prova\n(numero, grafico, lista, imagem) e a última pede ação (cta). Varie os tipos —\ntrês \"titulo\" seguidos parecem slide parado. Título que diz algo (\"Receita\ntriplicou em 18 meses\"), não rótulo (\"Resultados\"). Número de verdade ou\nnenhum. A fala não repete a tela: a tela dá o título, a voz dá o argumento.\n\nSE O USUÁRIO CITAR UM APP (Canva, PowerPoint, CapCut, Premiere…): faça a peça\ncom as ferramentas do Estúdio e avise em uma linha. Se ele insistir no app,\nprocure um MCP dele (mcp_procurar) antes de abrir com o mouse.\n\nDocumento, planilha e PDF não são do Estúdio: faça o arquivo (.docx, .xlsx,\n.pdf) com código, na pasta do projeto.",
@@ -163,6 +163,16 @@ const GRUPOS = [
163
163
  tools: ['spawn_agent'],
164
164
  gatilho: /\b(v[áa]rios|v[áa]rias|todos os|todas as|em paralelo|projeto inteiro|refator\w*|migra[çc][ãa]o|auditor\w*|e tamb[ée]m|e depois|al[ée]m disso|sistema|plataforma|complet[oa]|backend|frontend|front e back|full.?stack|testes|telas|m[óo]dulos|camadas|arquivos|crud|api e)\b/i, // sem "site", "app", "aplicativo": casavam com quase todo pedido (13/10)
165
165
  },
166
+ {
167
+ nome: 'notebook',
168
+ tools: ['notebook_edit'],
169
+ gatilho: /\b(ipynb|notebook|jupyter|c[ée]lula)\b/i,
170
+ },
171
+ {
172
+ nome: 'skill',
173
+ tools: ['skill'],
174
+ gatilho: /\bskills?\b/i,
175
+ },
166
176
  {
167
177
  nome: 'terminal',
168
178
  tools: ['run_in_new_terminal', 'run_background', 'background_output', 'stop_background'],