primocode 8.42.2 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -37
- package/bin/primocode.js +598 -360
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +42 -195
- package/lib/catalogo.js +100 -61
- package/lib/config.js +1 -1
- package/lib/conta.js +3 -24
- package/lib/diff.js +161 -0
- package/lib/diretor.js +37 -47
- package/lib/entrada.js +576 -0
- package/lib/especialistas.js +2 -2
- package/lib/estudio/assets.js +343 -0
- package/lib/estudio/identidade.js +560 -0
- package/lib/estudio/index.js +565 -0
- package/lib/estudio/plano.js +327 -0
- package/lib/estudio/projeto.js +246 -0
- package/lib/estudio/remotion.js +644 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +49 -6
- package/lib/marca.js +4 -1
- package/lib/markdown.js +376 -0
- package/lib/marketplace.js +189 -0
- package/lib/pasta.js +15 -4
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +106 -187
- package/lib/ui.js +7 -3
- package/lib/voz.js +239 -0
- package/package.json +12 -3
- package/skills/acessibilidade/SKILL.md +28 -0
- package/skills/api-que-nao-quebra/SKILL.md +27 -0
- package/skills/depurar-de-verdade/SKILL.md +29 -0
- package/skills/editar-meu-video/SKILL.md +59 -0
- package/skills/escrever-em-portugues/SKILL.md +28 -0
- package/skills/git-limpo/SKILL.md +28 -0
- package/skills/motion-de-primeira/SKILL.md +62 -0
- package/skills/pagina-que-converte/SKILL.md +28 -0
- package/skills/performance-web/SKILL.md +28 -0
- package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
- package/skills/slides-que-prendem/SKILL.md +40 -0
- package/studio/edge.py +16 -66
- package/studio/narrar.py +274 -0
- package/studio/pessoa.py +199 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +383 -0
- package/voz/primo_voz/escuta.py +235 -0
- package/voz/primo_voz/orbe.py +216 -0
- package/lib/audio-livre.js +0 -177
- package/lib/chaves.js +0 -149
- package/lib/claude-engine.js +0 -631
- package/lib/codex-engine.js +0 -208
- package/lib/remotion.js +0 -663
- package/lib/studio.js +0 -960
- package/studio/PRIMOCODE.md +0 -58
- package/studio/README.md +0 -154
- package/studio/__pycache__/edge.cpython-311.pyc +0 -0
- package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
- package/studio/__pycache__/voz.cpython-311.pyc +0 -0
- package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
- package/studio/ferramentas/00-protocolo.md +0 -125
- package/studio/ferramentas/01-modelos.md +0 -270
- package/studio/ferramentas/README.md +0 -32
- package/studio/ferramentas/corte.md +0 -919
- package/studio/ferramentas/grade.md +0 -92
- package/studio/ferramentas/prisma.md +0 -107
- package/studio/ferramentas/prosa.md +0 -86
- package/studio/ferramentas/tela.md +0 -93
- package/studio/ferramentas/traco.md +0 -79
- package/studio/server.py +0 -2254
- package/studio/studio.log +0 -57
- package/studio/voz.py +0 -388
- package/studio/web/analise.html +0 -52
- package/studio/web/core.css +0 -1167
- package/studio/web/fontes/bebas.woff2 +0 -0
- package/studio/web/fontes/grotesk-700.woff2 +0 -0
- package/studio/web/fontes/inter-400.woff2 +0 -0
- package/studio/web/fontes/inter-700.woff2 +0 -0
- package/studio/web/fontes/jetbrains-400.woff2 +0 -0
- package/studio/web/fontes/playfair-700.woff2 +0 -0
- package/studio/web/fontes.css +0 -23
- package/studio/web/home.html +0 -217
- package/studio/web/js/analise.js +0 -510
- package/studio/web/js/audio.js +0 -607
- package/studio/web/js/editor.js +0 -1802
- package/studio/web/js/exportar.js +0 -222
- package/studio/web/js/ferramentas.js +0 -108
- package/studio/web/js/icones.js +0 -120
- package/studio/web/js/midia.js +0 -157
- package/studio/web/js/narracao.js +0 -325
- package/studio/web/js/player.js +0 -903
- package/studio/web/js/render-grade.js +0 -243
- package/studio/web/js/render-prosa.js +0 -104
- package/studio/web/js/render.js +0 -1852
- package/studio/web/js/sfx.js +0 -233
- package/studio/web/js/video.js +0 -464
- package/studio/web/js/voz-neural.js +0 -152
- package/studio/web/render.css +0 -1352
- package/studio/web/tf.html +0 -15
- package/studio/web/tool.html +0 -78
- 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
|
+
};
|