primocode 8.42.2 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +41 -37
  2. package/bin/primocode.js +598 -360
  3. package/lib/acervo.js +8 -1
  4. package/lib/act.js +205 -114
  5. package/lib/api.js +35 -4
  6. package/lib/boas-vindas.js +31 -192
  7. package/lib/catalogo.js +60 -60
  8. package/lib/config.js +1 -1
  9. package/lib/conta.js +3 -24
  10. package/lib/diff.js +161 -0
  11. package/lib/diretor.js +37 -47
  12. package/lib/entrada.js +545 -0
  13. package/lib/especialistas.js +2 -2
  14. package/lib/estudio/assets.js +343 -0
  15. package/lib/estudio/identidade.js +560 -0
  16. package/lib/estudio/index.js +565 -0
  17. package/lib/estudio/plano.js +327 -0
  18. package/lib/estudio/projeto.js +246 -0
  19. package/lib/estudio/remotion.js +606 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +13 -1
  23. package/lib/marca.js +4 -1
  24. package/lib/marketplace.js +189 -0
  25. package/lib/permissao.js +137 -0
  26. package/lib/registro.js +229 -0
  27. package/lib/skills.js +329 -0
  28. package/lib/tools.js +51 -181
  29. package/lib/ui.js +7 -3
  30. package/lib/voz.js +239 -0
  31. package/package.json +12 -3
  32. package/skills/acessibilidade/SKILL.md +28 -0
  33. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  34. package/skills/depurar-de-verdade/SKILL.md +29 -0
  35. package/skills/editar-meu-video/SKILL.md +59 -0
  36. package/skills/escrever-em-portugues/SKILL.md +28 -0
  37. package/skills/git-limpo/SKILL.md +28 -0
  38. package/skills/motion-de-primeira/SKILL.md +62 -0
  39. package/skills/pagina-que-converte/SKILL.md +28 -0
  40. package/skills/performance-web/SKILL.md +28 -0
  41. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  42. package/skills/slides-que-prendem/SKILL.md +40 -0
  43. package/studio/edge.py +16 -66
  44. package/studio/narrar.py +274 -0
  45. package/studio/pessoa.py +167 -0
  46. package/voz/primo_voz/__init__.py +0 -0
  47. package/voz/primo_voz/__main__.py +325 -0
  48. package/voz/primo_voz/escuta.py +235 -0
  49. package/voz/primo_voz/orbe.py +216 -0
  50. package/lib/audio-livre.js +0 -177
  51. package/lib/chaves.js +0 -149
  52. package/lib/claude-engine.js +0 -631
  53. package/lib/codex-engine.js +0 -208
  54. package/lib/remotion.js +0 -663
  55. package/lib/studio.js +0 -960
  56. package/studio/PRIMOCODE.md +0 -58
  57. package/studio/README.md +0 -154
  58. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  59. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  60. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  61. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  62. package/studio/ferramentas/00-protocolo.md +0 -125
  63. package/studio/ferramentas/01-modelos.md +0 -270
  64. package/studio/ferramentas/README.md +0 -32
  65. package/studio/ferramentas/corte.md +0 -919
  66. package/studio/ferramentas/grade.md +0 -92
  67. package/studio/ferramentas/prisma.md +0 -107
  68. package/studio/ferramentas/prosa.md +0 -86
  69. package/studio/ferramentas/tela.md +0 -93
  70. package/studio/ferramentas/traco.md +0 -79
  71. package/studio/server.py +0 -2254
  72. package/studio/studio.log +0 -57
  73. package/studio/voz.py +0 -388
  74. package/studio/web/analise.html +0 -52
  75. package/studio/web/core.css +0 -1167
  76. package/studio/web/fontes/bebas.woff2 +0 -0
  77. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  78. package/studio/web/fontes/inter-400.woff2 +0 -0
  79. package/studio/web/fontes/inter-700.woff2 +0 -0
  80. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  81. package/studio/web/fontes/playfair-700.woff2 +0 -0
  82. package/studio/web/fontes.css +0 -23
  83. package/studio/web/home.html +0 -217
  84. package/studio/web/js/analise.js +0 -510
  85. package/studio/web/js/audio.js +0 -607
  86. package/studio/web/js/editor.js +0 -1802
  87. package/studio/web/js/exportar.js +0 -222
  88. package/studio/web/js/ferramentas.js +0 -108
  89. package/studio/web/js/icones.js +0 -120
  90. package/studio/web/js/midia.js +0 -157
  91. package/studio/web/js/narracao.js +0 -325
  92. package/studio/web/js/player.js +0 -903
  93. package/studio/web/js/render-grade.js +0 -243
  94. package/studio/web/js/render-prosa.js +0 -104
  95. package/studio/web/js/render.js +0 -1852
  96. package/studio/web/js/sfx.js +0 -233
  97. package/studio/web/js/video.js +0 -464
  98. package/studio/web/js/voz-neural.js +0 -152
  99. package/studio/web/render.css +0 -1352
  100. package/studio/web/tf.html +0 -15
  101. package/studio/web/tool.html +0 -78
  102. package/studio/web/view.html +0 -27
@@ -0,0 +1,327 @@
1
+ /**
2
+ * plano.js — o que é um vídeo, agora que não há mais modelos prontos.
3
+ *
4
+ * "REMOVER todos os templates e estilos pré-prontos que existem hoje. Eles
5
+ * deixam a IA preguiçosa e todos os vídeos acabam iguais e genéricos. A IA
6
+ * cria tudo do zero, a cada vídeo."
7
+ *
8
+ * ── O QUE SAIU, E O QUE ENTROU NO LUGAR ──────────────────────────────────
9
+ * Saiu o vocabulário de LOOKS. O Studio antigo tinha `layout: "capa"`,
10
+ * `layout: "duas-colunas"`, `tema: "meia-noite"`, e um `corte.md` de 772
11
+ * linhas com um exemplo completo de vídeo. Pedir um vídeo era escolher entre
12
+ * as caixas que já existiam — e o resultado saía com a cara das caixas, não
13
+ * do assunto. Era isso que fazia todo vídeo parecer o anterior.
14
+ *
15
+ * Entrou um vocabulário de PRIMITIVAS: texto, forma, imagem, vídeo, ícone,
16
+ * legenda — cada um com posição, tamanho, cor e animação por quadro-chave,
17
+ * mais uma câmera por cena. Nenhuma primitiva tem aparência própria; ela não
18
+ * sabe o que é uma "capa".
19
+ *
20
+ * A diferença entre isso e um template é a mesma entre tinta e um quadro
21
+ * pintado. Duas peças feitas com estas primitivas não se parecem, a menos que
22
+ * quem escreveu tenha escolhido que se parecessem.
23
+ *
24
+ * ── E QUANDO A PRIMITIVA NÃO ALCANÇA ─────────────────────────────────────
25
+ * Existe cena que nenhum vocabulário fechado descreve: uma malha 3D girando,
26
+ * um morfismo entre dois traçados, um efeito escrito na hora. Para essas, a
27
+ * cena aponta um `componente`: um arquivo React que a própria IA escreve na
28
+ * pasta do projeto, e que o Remotion monta como qualquer outra cena.
29
+ *
30
+ * É a válvula que impede este arquivo de virar o que ele acabou de
31
+ * substituir — uma lista de efeitos permitidos, crescendo para sempre.
32
+ *
33
+ * ── O QUE ESTE ARQUIVO NÃO FAZ ───────────────────────────────────────────
34
+ * Ele não tem opinião sobre estética. Não recusa fundo roxo, não exige
35
+ * paleta, não conta quantas fontes você usou. Isso é assunto da skill que vai
36
+ * na frente do pedido (skills/motion-de-primeira/SKILL.md) — conselho que o
37
+ * modelo lê e pondera, não regra que o código impõe. Código que recusa uma
38
+ * cor é código que um dia recusa a cor certa.
39
+ *
40
+ * O que ele faz é conferir se o plano É EXECUTÁVEL: cena sem duração, cor
41
+ * escrita errada, imagem que não existe no disco, quadro-chave fora de ordem.
42
+ * Tudo isso vira vídeo quebrado ou renderização que morre no meio, e o erro
43
+ * aparece vinte minutos depois — então ele aparece agora.
44
+ */
45
+
46
+ 'use strict';
47
+
48
+ const fs = require('fs');
49
+ const path = require('path');
50
+
51
+ const TIPOS = new Set(['texto', 'forma', 'imagem', 'video', 'icone', 'legenda', 'componente']);
52
+ const FORMAS = new Set(['retangulo', 'circulo', 'linha', 'poligono', 'traco']);
53
+ const AJUSTES = new Set(['cover', 'contain', 'preencher']);
54
+ const TRANSICOES = new Set(['corte', 'esmaecer', 'deslizar', 'zoom', 'giro', 'mascara', 'morph']);
55
+
56
+ /* As propriedades que a animação por quadro-chave alcança. Cada uma é um
57
+ número no tempo; a curva entre dois quadros é a suavização declarada.
58
+ `z` e as rotações em X e Y são o que dá 3D de verdade quando a cena declara
59
+ perspectiva — não é um efeito à parte, é a mesma máquina. */
60
+ const ANIMAVEIS = new Set([
61
+ 'opacidade', 'x', 'y', 'z', 'escala', 'escalaX', 'escalaY',
62
+ 'rotacao', 'rotacaoX', 'rotacaoY', 'desfoque', 'brilho', 'saturacao',
63
+ 'corte', 'raio', 'espacamento',
64
+ ]);
65
+
66
+ /* A CÂMERA tem vocabulário próprio, e é de propósito.
67
+ *
68
+ * Um elemento tem `escala` (ele cresce); a câmera tem `zoom` (o mundo inteiro
69
+ * se aproxima). São coisas diferentes, e chamar as duas de "escala" faria o
70
+ * agente escrever zoom de câmera dentro de um elemento — o que dá quase o
71
+ * mesmo resultado na cena de uma coisa só, e nenhum resultado na cena com
72
+ * cinco. Vocabulários separados tornam o erro impossível em vez de sutil. */
73
+ const ANIMAVEIS_CAMERA = new Set(['zoom', 'x', 'y', 'rotacao']);
74
+
75
+ const SUAVIZACOES = new Set(['linear', 'suave', 'entrada', 'saida', 'elastico', 'salto']);
76
+
77
+ const COR = /^(#[0-9a-f]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|transparent|currentColor)$/i;
78
+
79
+ const erro = (onde, oque) => ({ onde, oque });
80
+
81
+ /** Um número, aceitando "50%" e "120px" nas posições. */
82
+ function medida(v) {
83
+ if (typeof v === 'number' && Number.isFinite(v)) return true;
84
+ return typeof v === 'string' && /^-?\d+(\.\d+)?(%|px|vw|vh)?$/.test(v.trim());
85
+ }
86
+
87
+ /**
88
+ * Confere uma trilha de quadros-chave.
89
+ *
90
+ * Fora de ordem é o defeito que não dá erro: o Remotion interpola entre os
91
+ * valores na ordem em que vêm, então um quadro com tempo menor depois de um
92
+ * maior faz o elemento voltar no tempo, e o que se vê é um tremor que
93
+ * ninguém consegue explicar olhando o resultado.
94
+ */
95
+ function conferirTrilha(caminho, trilha, problemas) {
96
+ if (!Array.isArray(trilha)) {
97
+ problemas.push(erro(caminho, 'a animação de uma propriedade é uma lista de quadros-chave'));
98
+ return;
99
+ }
100
+ let anterior = -Infinity;
101
+ trilha.forEach((q, i) => {
102
+ const onde = `${caminho}[${i}]`;
103
+ if (!q || typeof q !== 'object') return problemas.push(erro(onde, 'cada quadro é {t, v}'));
104
+ if (typeof q.t !== 'number' || !Number.isFinite(q.t) || q.t < 0) {
105
+ problemas.push(erro(onde, 't é o tempo em segundos dentro da cena, a partir de 0'));
106
+ }
107
+ if (typeof q.v !== 'number' || !Number.isFinite(q.v)) {
108
+ problemas.push(erro(onde, 'v é o valor numérico da propriedade nesse instante'));
109
+ }
110
+ if (q.e && !SUAVIZACOES.has(q.e)) {
111
+ problemas.push(erro(onde, `suavização "${q.e}" não existe — use ${[...SUAVIZACOES].join(', ')}`));
112
+ }
113
+ if (typeof q.t === 'number' && q.t < anterior) {
114
+ problemas.push(erro(onde, 'os quadros-chave vêm em ordem de tempo; fora de ordem o elemento volta no tempo'));
115
+ }
116
+ anterior = typeof q.t === 'number' ? q.t : anterior;
117
+ });
118
+ }
119
+
120
+ function conferirAnima(caminho, anima, problemas, permitidas = ANIMAVEIS) {
121
+ if (anima == null) return;
122
+ if (typeof anima !== 'object') return problemas.push(erro(caminho, 'anima é um objeto de propriedade → quadros'));
123
+ for (const [prop, trilha] of Object.entries(anima)) {
124
+ if (!permitidas.has(prop)) {
125
+ problemas.push(erro(`${caminho}.${prop}`,
126
+ `não dá para animar "${prop}" aqui. Dá para: ${[...permitidas].join(', ')}`));
127
+ continue;
128
+ }
129
+ conferirTrilha(`${caminho}.${prop}`, trilha, problemas);
130
+ }
131
+ }
132
+
133
+ function conferirCor(caminho, valor, problemas) {
134
+ if (valor == null) return;
135
+ if (typeof valor !== 'string' || !COR.test(valor.trim())) {
136
+ problemas.push(erro(caminho, `"${valor}" não é uma cor — use #rrggbb, rgb()/rgba() ou hsl()`));
137
+ }
138
+ }
139
+
140
+ /** O arquivo existe dentro da pasta do projeto? */
141
+ function conferirArquivo(caminho, src, pasta, problemas) {
142
+ if (!src) return problemas.push(erro(caminho, 'falta o caminho do arquivo (src)'));
143
+ if (/^https?:/i.test(src)) {
144
+ return problemas.push(erro(caminho,
145
+ 'endereço da internet não entra no plano — baixe com estudio_midia e aponte para o arquivo. '
146
+ + 'Render que depende da rede falha no meio, depois de tudo pronto.'));
147
+ }
148
+ if (!pasta) return;
149
+ const cheio = path.resolve(pasta, src);
150
+ if (!cheio.startsWith(path.resolve(pasta))) {
151
+ return problemas.push(erro(caminho, 'o arquivo tem de estar dentro da pasta do projeto'));
152
+ }
153
+ if (!fs.existsSync(cheio)) problemas.push(erro(caminho, `o arquivo "${src}" não existe na pasta do projeto`));
154
+ }
155
+
156
+ function conferirElemento(caminho, el, pasta, problemas, duracao) {
157
+ if (!el || typeof el !== 'object') return problemas.push(erro(caminho, 'cada elemento é um objeto'));
158
+ if (!TIPOS.has(el.tipo)) {
159
+ return problemas.push(erro(caminho, `tipo "${el.tipo}" não existe — use ${[...TIPOS].join(', ')}`));
160
+ }
161
+
162
+ for (const p of ['x', 'y', 'largura', 'altura']) {
163
+ if (el[p] != null && !medida(el[p])) {
164
+ problemas.push(erro(`${caminho}.${p}`, `"${el[p]}" não é medida — número, "50%" ou "120px"`));
165
+ }
166
+ }
167
+ conferirCor(`${caminho}.cor`, el.cor, problemas);
168
+ conferirCor(`${caminho}.fundo`, el.fundo, problemas);
169
+ conferirAnima(`${caminho}.anima`, el.anima, problemas);
170
+
171
+ // `de` e `ate` recortam o elemento no tempo DA CENA. Um elemento que
172
+ // entra depois de a cena acabar é trabalho jogado fora, e ninguém vê.
173
+ if (el.de != null && el.ate != null && Number(el.de) >= Number(el.ate)) {
174
+ problemas.push(erro(caminho, '"de" precisa ser menor que "ate"'));
175
+ }
176
+ if (el.de != null && duracao && Number(el.de) >= duracao) {
177
+ problemas.push(erro(caminho, `este elemento entra em ${el.de}s, mas a cena acaba em ${duracao}s`));
178
+ }
179
+
180
+ switch (el.tipo) {
181
+ case 'texto':
182
+ if (!String(el.texto || '').trim()) problemas.push(erro(caminho, 'texto vazio'));
183
+ // Texto de exemplo entregue como conteúdo é a falha que mais
184
+ // passa despercebida: o vídeo fica lindo e não diz nada.
185
+ if (/lorem ipsum|\[inserir|texto aqui|seu texto/i.test(String(el.texto || ''))) {
186
+ problemas.push(erro(caminho, 'isto é texto de exemplo — escreva o conteúdo de verdade'));
187
+ }
188
+ break;
189
+ case 'forma':
190
+ if (el.forma && !FORMAS.has(el.forma)) {
191
+ problemas.push(erro(`${caminho}.forma`, `forma "${el.forma}" não existe — ${[...FORMAS].join(', ')}`));
192
+ }
193
+ break;
194
+ case 'imagem':
195
+ case 'video':
196
+ conferirArquivo(`${caminho}.src`, el.src, pasta, problemas);
197
+ if (el.ajuste && !AJUSTES.has(el.ajuste)) {
198
+ problemas.push(erro(`${caminho}.ajuste`, `use ${[...AJUSTES].join(', ')}`));
199
+ }
200
+ break;
201
+ case 'icone':
202
+ // O nome do ícone é "coleção:nome" (ver estudio/assets.js). Sem a
203
+ // coleção não dá para buscar, e o ícone some do vídeo sem aviso.
204
+ if (!/^[a-z0-9-]+:[a-z0-9-]+$/i.test(String(el.nome || ''))) {
205
+ problemas.push(erro(`${caminho}.nome`,
206
+ 'o ícone é "coleção:nome", como "lucide:zap" ou "simple-icons:pix"'));
207
+ }
208
+ break;
209
+ case 'legenda':
210
+ if (!Array.isArray(el.fala) || !el.fala.length) {
211
+ problemas.push(erro(caminho, 'a legenda precisa da lista de falas com tempo'));
212
+ }
213
+ break;
214
+ case 'componente':
215
+ // A válvula de escape: um arquivo React escrito pela própria IA.
216
+ conferirArquivo(`${caminho}.src`, el.src, pasta, problemas);
217
+ break;
218
+ }
219
+ }
220
+
221
+ function conferirFundo(caminho, fundo, pasta, problemas) {
222
+ if (fundo == null) return;
223
+ if (typeof fundo === 'string') return conferirCor(caminho, fundo, problemas);
224
+ if (typeof fundo !== 'object') return problemas.push(erro(caminho, 'fundo é uma cor ou um objeto'));
225
+ if (fundo.tipo === 'cor') return conferirCor(`${caminho}.valor`, fundo.valor, problemas);
226
+ if (fundo.tipo === 'gradiente') {
227
+ if (!Array.isArray(fundo.cores) || fundo.cores.length < 2) {
228
+ problemas.push(erro(`${caminho}.cores`, 'um gradiente tem pelo menos duas cores'));
229
+ } else fundo.cores.forEach((cor, i) => conferirCor(`${caminho}.cores[${i}]`, cor, problemas));
230
+ return;
231
+ }
232
+ if (fundo.tipo === 'imagem' || fundo.tipo === 'video') {
233
+ return conferirArquivo(`${caminho}.src`, fundo.src, pasta, problemas);
234
+ }
235
+ problemas.push(erro(caminho, 'fundo.tipo é cor, gradiente, imagem ou video'));
236
+ }
237
+
238
+ /**
239
+ * Confere o plano inteiro.
240
+ *
241
+ * @returns {{ok: boolean, problemas: Array, duracao: number}}
242
+ */
243
+ function conferir(plano, { pasta = null } = {}) {
244
+ const problemas = [];
245
+ if (!plano || typeof plano !== 'object') {
246
+ return { ok: false, problemas: [erro('plano', 'o plano é um objeto JSON')], duracao: 0 };
247
+ }
248
+
249
+ const f = plano.formato || {};
250
+ const largura = Number(f.largura) || 1920;
251
+ const altura = Number(f.altura) || 1080;
252
+ const fps = Number(f.fps) || 30;
253
+ if (largura < 64 || altura < 64) problemas.push(erro('formato', 'largura e altura em pixels'));
254
+ if (fps < 1 || fps > 120) problemas.push(erro('formato.fps', 'fps entre 1 e 120'));
255
+
256
+ if (!Array.isArray(plano.cenas) || !plano.cenas.length) {
257
+ problemas.push(erro('cenas', 'o plano precisa de pelo menos uma cena'));
258
+ return { ok: false, problemas, duracao: 0 };
259
+ }
260
+
261
+ let duracao = 0;
262
+ plano.cenas.forEach((cena, i) => {
263
+ const onde = `cenas[${i}]`;
264
+ if (!cena || typeof cena !== 'object') return problemas.push(erro(onde, 'cada cena é um objeto'));
265
+
266
+ const d = Number(cena.duracao);
267
+ if (!Number.isFinite(d) || d <= 0) {
268
+ problemas.push(erro(`${onde}.duracao`, 'a cena precisa de duração em segundos'));
269
+ } else if (d > 120) {
270
+ problemas.push(erro(`${onde}.duracao`, `${d}s numa cena só — divida, ou o espectador sai antes`));
271
+ } else duracao += d;
272
+
273
+ conferirFundo(`${onde}.fundo`, cena.fundo, pasta, problemas);
274
+ conferirAnima(`${onde}.camera`, cena.camera, problemas, ANIMAVEIS_CAMERA);
275
+
276
+ if (cena.transicao) {
277
+ const t = cena.transicao.tipo || cena.transicao;
278
+ if (!TRANSICOES.has(t)) {
279
+ problemas.push(erro(`${onde}.transicao`, `transição "${t}" não existe — ${[...TRANSICOES].join(', ')}`));
280
+ }
281
+ }
282
+
283
+ const elementos = cena.elementos || [];
284
+ if (!Array.isArray(elementos)) problemas.push(erro(`${onde}.elementos`, 'elementos é uma lista'));
285
+ else elementos.forEach((el, k) => conferirElemento(`${onde}.elementos[${k}]`, el, pasta, problemas, d));
286
+ });
287
+
288
+ const audio = plano.audio || {};
289
+ if (audio.narracao) conferirArquivo('audio.narracao', audio.narracao, pasta, problemas);
290
+ if (audio.trilha) conferirArquivo('audio.trilha.src', audio.trilha.src || audio.trilha, pasta, problemas);
291
+ for (const [i, e] of (audio.efeitos || []).entries()) {
292
+ conferirArquivo(`audio.efeitos[${i}].src`, e && e.src, pasta, problemas);
293
+ }
294
+
295
+ return { ok: problemas.length === 0, problemas, duracao: Number(duracao.toFixed(3)) };
296
+ }
297
+
298
+ /** Quantos quadros dura o vídeo inteiro. Uma conta, num lugar só. */
299
+ function quadros(plano) {
300
+ const fps = Number((plano.formato || {}).fps) || 30;
301
+ const segundos = (plano.cenas || []).reduce((n, c) => n + (Number(c.duracao) || 0), 0);
302
+ return Math.max(1, Math.round(segundos * fps));
303
+ }
304
+
305
+ /** O que existe no plano, em texto curto — para o agente se situar sem reler tudo. */
306
+ function resumo(plano) {
307
+ if (!plano || !Array.isArray(plano.cenas)) return 'plano vazio';
308
+ const f = plano.formato || {};
309
+ const linhas = [`${plano.cenas.length} cenas · ${(f.largura || 1920)}×${(f.altura || 1080)} · ${(f.fps || 30)}fps`];
310
+ plano.cenas.forEach((cena, i) => {
311
+ const tipos = {};
312
+ for (const el of cena.elementos || []) tipos[el.tipo] = (tipos[el.tipo] || 0) + 1;
313
+ const conteudo = (cena.elementos || [])
314
+ .filter((el) => el.tipo === 'texto')
315
+ .map((el) => String(el.texto).replace(/\s+/g, ' ').slice(0, 40))
316
+ .join(' / ');
317
+ linhas.push(` ${i + 1}. ${cena.duracao}s`
318
+ + (Object.keys(tipos).length ? ` · ${Object.entries(tipos).map(([t, n]) => `${n} ${t}`).join(', ')}` : '')
319
+ + (conteudo ? ` · "${conteudo}"` : ''));
320
+ });
321
+ return linhas.join('\n');
322
+ }
323
+
324
+ module.exports = {
325
+ conferir, quadros, resumo,
326
+ TIPOS, FORMAS, ANIMAVEIS, ANIMAVEIS_CAMERA, SUAVIZACOES, TRANSICOES, AJUSTES,
327
+ };
@@ -0,0 +1,246 @@
1
+ /**
2
+ * projeto.js — cada vídeo é uma pasta, e ela não some.
3
+ *
4
+ * "Memória de projetos: todos os vídeos gerados ficam salvos. Semanas depois
5
+ * eu posso dizer 'não gostei de tal parte daquele vídeo' e a IA precisa
6
+ * localizar o projeto, lembrar o que fez e editar aquele trecho."
7
+ *
8
+ * ── O QUE MUDOU DE VERDADE ───────────────────────────────────────────────
9
+ * O Studio antigo guardava as peças dentro de um servidor Python, num
10
+ * `arquivos/` com id numérico, e o "histórico" era uma lista de versões do
11
+ * documento JSON. Duas coisas não funcionavam ali:
12
+ *
13
+ * · para achar "aquele vídeo do PicPay de três semanas atrás" era preciso
14
+ * saber o id; e
15
+ * · o que ficava guardado era o RESULTADO, não as decisões. Reabrir um
16
+ * projeto dizia o que existia, nunca por quê — e sem o porquê, mexer num
17
+ * trecho vira refazer do zero.
18
+ *
19
+ * Aqui a pasta guarda as duas coisas: o plano (o que é cada cena) e o
20
+ * caderno (o que foi pedido, o que foi decidido e o que mudou, em ordem).
21
+ * Semanas depois, "não gostei da parte do cartão" acha o projeto pelo texto,
22
+ * lê o caderno, e mexe SÓ naquela cena.
23
+ *
24
+ * ── POR QUE NÃO HÁ SERVIDOR ──────────────────────────────────────────────
25
+ * O Studio antigo subia um servidor Python em localhost:7777 e falava com ele
26
+ * por HTTP. Isso existia porque a interface de edição era uma página web. A
27
+ * interface morreu — a IA faz tudo e abre o vídeo pronto —, e com ela morre o
28
+ * servidor. Sobra o que importa: pastas, arquivos e ffmpeg.
29
+ *
30
+ * Isso apaga uma classe inteira de defeito. "O estúdio não está rodando na sua
31
+ * máquina" era uma resposta real que o agente dava, e era um pedido de vídeo
32
+ * que morria por causa de uma porta ocupada.
33
+ */
34
+
35
+ 'use strict';
36
+
37
+ const fs = require('fs');
38
+ const os = require('os');
39
+ const path = require('path');
40
+ const crypto = require('crypto');
41
+
42
+ const CASA = path.join(os.homedir(), '.primocode', 'studio');
43
+ const PROJETOS = path.join(CASA, 'projetos');
44
+
45
+ /** Nome de pasta a partir do que a pessoa pediu. */
46
+ function apelidar(bruto) {
47
+ return String(bruto || '')
48
+ .normalize('NFD').replace(/[̀-ͯ]/g, '')
49
+ .toLowerCase()
50
+ .replace(/[^a-z0-9]+/g, '-')
51
+ .replace(/^-+|-+$/g, '')
52
+ .slice(0, 40) || 'projeto';
53
+ }
54
+
55
+ function agora() { return new Date().toISOString(); }
56
+
57
+ function garantirCasa() {
58
+ fs.mkdirSync(PROJETOS, { recursive: true });
59
+ return PROJETOS;
60
+ }
61
+
62
+ /**
63
+ * Abre um projeto novo.
64
+ *
65
+ * O id leva a DATA na frente. Não é enfeite: a pasta é listada em ordem
66
+ * alfabética por quase tudo que lê disco, e com a data na frente essa ordem
67
+ * já é a cronológica — que é como se procura um vídeo ("o de semana
68
+ * passada"), não por nome.
69
+ */
70
+ function criar({ titulo, pedido, formato = 'video' }) {
71
+ garantirCasa();
72
+ const dia = new Date().toISOString().slice(0, 10);
73
+ const base = `${dia}-${apelidar(titulo || pedido)}`;
74
+ let id = base;
75
+ let n = 2;
76
+ while (fs.existsSync(path.join(PROJETOS, id))) { id = `${base}-${n}`; n++; }
77
+
78
+ const pasta = path.join(PROJETOS, id);
79
+ for (const sub of ['midia', 'saida', 'entrada']) {
80
+ fs.mkdirSync(path.join(pasta, sub), { recursive: true });
81
+ }
82
+
83
+ const meta = {
84
+ id,
85
+ titulo: String(titulo || pedido || 'sem título').slice(0, 120),
86
+ formato, // video | slides | arte | documento
87
+ pedido: String(pedido || ''),
88
+ criado: agora(),
89
+ mudado: agora(),
90
+ renders: [],
91
+ };
92
+ escreverJSON(path.join(pasta, 'projeto.json'), meta);
93
+ anotar(id, 'nasceu', String(pedido || ''));
94
+ return { ok: true, id, pasta, meta };
95
+ }
96
+
97
+ function escreverJSON(arquivo, valor) {
98
+ fs.mkdirSync(path.dirname(arquivo), { recursive: true });
99
+ fs.writeFileSync(arquivo, JSON.stringify(valor, null, 2), 'utf8');
100
+ }
101
+
102
+ function lerJSON(arquivo, padrao = null) {
103
+ try { return JSON.parse(fs.readFileSync(arquivo, 'utf8')); } catch { return padrao; }
104
+ }
105
+
106
+ const pastaDe = (id) => path.join(PROJETOS, id);
107
+
108
+ /** O meta de um projeto, ou null. */
109
+ function ler(id) {
110
+ const meta = lerJSON(path.join(pastaDe(id), 'projeto.json'));
111
+ if (!meta) return null;
112
+ return { ...meta, pasta: pastaDe(id) };
113
+ }
114
+
115
+ function salvar(id, mudancas) {
116
+ const meta = ler(id);
117
+ if (!meta) return { ok: false, error: `Projeto "${id}" não existe.` };
118
+ const novo = { ...meta, ...mudancas, id, mudado: agora() };
119
+ delete novo.pasta;
120
+ escreverJSON(path.join(pastaDe(id), 'projeto.json'), novo);
121
+ return { ok: true, meta: { ...novo, pasta: pastaDe(id) } };
122
+ }
123
+
124
+ /** Todos, do mais novo para o mais velho. */
125
+ function listar({ limite = 50 } = {}) {
126
+ garantirCasa();
127
+ let nomes = [];
128
+ try {
129
+ nomes = fs.readdirSync(PROJETOS, { withFileTypes: true })
130
+ .filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
131
+ } catch { return []; }
132
+ return nomes.slice(0, limite).map(ler).filter(Boolean);
133
+ }
134
+
135
+ // ── o caderno ────────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * O que foi pedido, decidido e mudado — em ordem, uma linha por evento.
139
+ *
140
+ * Guardado como texto (JSONL), não como JSON de uma lista só: um caderno
141
+ * cresce sempre, e reescrever o arquivo inteiro a cada anotação é como se
142
+ * perde um caderno quando o processo morre no meio.
143
+ */
144
+ function anotar(id, tipo, texto, extra = {}) {
145
+ const linha = JSON.stringify({ quando: agora(), tipo, texto: String(texto || ''), ...extra });
146
+ try {
147
+ fs.mkdirSync(pastaDe(id), { recursive: true });
148
+ fs.appendFileSync(path.join(pastaDe(id), 'caderno.jsonl'), linha + '\n', 'utf8');
149
+ return true;
150
+ } catch { return false; }
151
+ }
152
+
153
+ function caderno(id, { ultimas = 40 } = {}) {
154
+ let bruto = '';
155
+ try { bruto = fs.readFileSync(path.join(pastaDe(id), 'caderno.jsonl'), 'utf8'); } catch { return []; }
156
+ const linhas = bruto.split('\n').filter(Boolean).slice(-ultimas);
157
+ return linhas.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
158
+ }
159
+
160
+ // ── achar de novo ────────────────────────────────────────────────────────
161
+
162
+ const semAcento = (v) => String(v || '').toLowerCase()
163
+ .normalize('NFD').replace(/[̀-ͯ]/g, '');
164
+
165
+ /**
166
+ * Acha o projeto pelo que a pessoa lembra.
167
+ *
168
+ * Ela não lembra do id. Ela diz "o vídeo do PicPay", "aquele do olho", "o de
169
+ * semana passada". Então a busca olha título, pedido e o CADERNO inteiro —
170
+ * porque o que ela lembra costuma ser um detalhe do meio do processo ("aquele
171
+ * com o cartão passando"), que nunca esteve no título.
172
+ *
173
+ * Empate desempata pelo mais novo: quem diz "aquele vídeo" quase sempre fala
174
+ * do último.
175
+ */
176
+ function procurar(texto, { limite = 5 } = {}) {
177
+ const termos = semAcento(texto).split(/\s+/).filter((t) => t.length >= 3);
178
+ if (!termos.length) return listar({ limite });
179
+
180
+ const pontuados = listar({ limite: 200 }).map((p) => {
181
+ const palha = semAcento([
182
+ p.titulo, p.pedido,
183
+ ...caderno(p.id, { ultimas: 200 }).map((e) => e.texto),
184
+ ].join(' '));
185
+ let pontos = 0;
186
+ for (const t of termos) if (palha.includes(t)) pontos++;
187
+ return { projeto: p, pontos };
188
+ }).filter((x) => x.pontos > 0);
189
+
190
+ pontuados.sort((a, b) => b.pontos - a.pontos || (a.projeto.id < b.projeto.id ? 1 : -1));
191
+ return pontuados.slice(0, limite).map((x) => x.projeto);
192
+ }
193
+
194
+ // ── o plano ──────────────────────────────────────────────────────────────
195
+
196
+ const planoDe = (id) => path.join(pastaDe(id), 'plano.json');
197
+
198
+ function lerPlano(id) { return lerJSON(planoDe(id)); }
199
+
200
+ /**
201
+ * Grava o plano e guarda a versão anterior.
202
+ *
203
+ * As versões existem para "volta como estava antes" e para o caso de uma
204
+ * edição pontual estragar o resto. Elas são numeradas e ficam em `versoes/`;
205
+ * nada é sobrescrito sem uma cópia ficar para trás.
206
+ */
207
+ function salvarPlano(id, plano, motivo = '') {
208
+ const anterior = lerPlano(id);
209
+ if (anterior) {
210
+ const versoes = path.join(pastaDe(id), 'versoes');
211
+ fs.mkdirSync(versoes, { recursive: true });
212
+ const n = fs.readdirSync(versoes).filter((f) => f.endsWith('.json')).length + 1;
213
+ escreverJSON(path.join(versoes, `${String(n).padStart(3, '0')}.json`), anterior);
214
+ }
215
+ escreverJSON(planoDe(id), plano);
216
+ anotar(id, 'plano', motivo || 'plano atualizado', { cenas: (plano.cenas || []).length });
217
+ return { ok: true };
218
+ }
219
+
220
+ function versoes(id) {
221
+ const dir = path.join(pastaDe(id), 'versoes');
222
+ try { return fs.readdirSync(dir).filter((f) => f.endsWith('.json')).sort(); } catch { return []; }
223
+ }
224
+
225
+ function voltarPara(id, versao) {
226
+ const arquivo = path.join(pastaDe(id), 'versoes', versao);
227
+ const plano = lerJSON(arquivo);
228
+ if (!plano) return { ok: false, error: `Versão "${versao}" não existe.` };
229
+ salvarPlano(id, plano, `voltou para a versão ${versao}`);
230
+ return { ok: true, plano };
231
+ }
232
+
233
+ /** Onde guardar uma mídia baixada, com nome que não colide. */
234
+ function caminhoDeMidia(id, nome) {
235
+ const limpo = String(nome || 'arquivo').replace(/[^\w.-]+/g, '-').slice(-60);
236
+ const marca = crypto.randomBytes(3).toString('hex');
237
+ return path.join(pastaDe(id), 'midia', `${marca}-${limpo}`);
238
+ }
239
+
240
+ module.exports = {
241
+ CASA, PROJETOS,
242
+ criar, ler, salvar, listar, procurar, pastaDe, apelidar,
243
+ anotar, caderno,
244
+ lerPlano, salvarPlano, versoes, voltarPara,
245
+ caminhoDeMidia, escreverJSON, lerJSON,
246
+ };