primocode 8.27.0 → 8.30.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/bin/primocode.js +154 -0
- package/lib/catalogo.js +4 -0
- package/lib/conta.js +197 -0
- package/lib/icone.js +247 -0
- package/lib/instalar-app.js +231 -0
- package/lib/remotion.js +463 -0
- package/lib/tools.js +30 -0
- package/package.json +2 -2
- package/studio/ferramentas/corte.md +31 -0
package/lib/remotion.js
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remotion.js — a mesma peça, renderizada por Remotion.
|
|
3
|
+
*
|
|
4
|
+
* "Também ele vai poder usar o Remotion. Aí ele decide qual usar, ou usa o
|
|
5
|
+
* que o usuário pedir."
|
|
6
|
+
*
|
|
7
|
+
* ── POR QUE ISTO NÃO É UMA DEPENDÊNCIA DO PRIMOCODE ──────────────────────
|
|
8
|
+
* Remotion é React + um Chromium próprio + ffmpeg: perto de meio giga. Pôr
|
|
9
|
+
* isso no `dependencies` do PrimoCode faria `npm i -g primocode` baixar meio
|
|
10
|
+
* giga para todo mundo — inclusive para quem nunca vai fazer um vídeo.
|
|
11
|
+
*
|
|
12
|
+
* E não precisa: Remotion é um projeto, não uma biblioteca de linha de
|
|
13
|
+
* comando. O jeito certo de usá-lo é ter um projeto Remotion, e é isso que
|
|
14
|
+
* este módulo escreve — na pasta do USUÁRIO, com as dependências dele. O
|
|
15
|
+
* PrimoCode continua com zero dependência, e quem quiser Remotion tem um
|
|
16
|
+
* projeto de verdade, que abre no `remotion studio`, entra no git e pode ser
|
|
17
|
+
* editado à mão depois.
|
|
18
|
+
*
|
|
19
|
+
* ── POR QUE ELE PARTE DO DOCUMENTO DO ESTÚDIO ────────────────────────────
|
|
20
|
+
* A tentação era inventar um segundo vocabulário — "para o Remotion escreva
|
|
21
|
+
* assim". Seriam duas gramáticas para o agente aprender, duas para manter, e
|
|
22
|
+
* uma ficaria velha.
|
|
23
|
+
*
|
|
24
|
+
* O projeto gerado leva junto o `render.js` e o `render.css` do estúdio, que
|
|
25
|
+
* é quem sabe desenhar uma cena. A composição React só decide QUAL cena está
|
|
26
|
+
* no ar em cada quadro e aplica entrada e transição a partir de
|
|
27
|
+
* `useCurrentFrame()`. O mesmo `doc` que toca no navegador renderiza aqui.
|
|
28
|
+
*
|
|
29
|
+
* ── QUANDO VALE A PENA ───────────────────────────────────────────────────
|
|
30
|
+
* O estúdio exporta em segundos, sem instalar nada, e é o padrão. Remotion
|
|
31
|
+
* pede `npm install` (uma vez, minutos) e devolve outra coisa:
|
|
32
|
+
*
|
|
33
|
+
* · quadro a quadro determinístico — o mesmo arquivo em toda máquina, sem
|
|
34
|
+
* depender de a aba estar em foco nem da carga do computador;
|
|
35
|
+
* · vídeo longo sem risco de o navegador engasgar no meio;
|
|
36
|
+
* · fps e qualidade escolhidos, ProRes, canal alfa, renderização paralela;
|
|
37
|
+
* · e um projeto que a pessoa pode abrir e mexer em React.
|
|
38
|
+
*
|
|
39
|
+
* ── LICENÇA ──────────────────────────────────────────────────────────────
|
|
40
|
+
* Remotion é grátis para pessoa física e para empresa pequena, mas EMPRESA
|
|
41
|
+
* ACIMA DE TRÊS PESSOAS precisa de licença paga. Isso vale para o projeto que
|
|
42
|
+
* nasce na pasta do usuário, não para o PrimoCode — e por isso o aviso vai no
|
|
43
|
+
* README gerado e na resposta da tool. Descobrir isso depois de montar o
|
|
44
|
+
* fluxo de trabalho em cima dele seria péssimo.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
'use strict';
|
|
48
|
+
|
|
49
|
+
const fs = require('fs');
|
|
50
|
+
const path = require('path');
|
|
51
|
+
const { spawn } = require('child_process');
|
|
52
|
+
|
|
53
|
+
const ORIGEM_ESTUDIO = path.join(__dirname, '..', 'studio', 'web');
|
|
54
|
+
const VERSAO_REMOTION = '4.0.522';
|
|
55
|
+
|
|
56
|
+
// Arquivos do estúdio que o projeto Remotion precisa para desenhar uma cena.
|
|
57
|
+
// São os mesmos que o navegador carrega — nada é reescrito.
|
|
58
|
+
const DO_ESTUDIO = [
|
|
59
|
+
['js/render.js', 'render.js'],
|
|
60
|
+
['js/icones.js', 'icones.js'],
|
|
61
|
+
['render.css', 'render.css'],
|
|
62
|
+
['fontes.css', 'fontes.css'],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function escrever(destino, conteudo) {
|
|
66
|
+
fs.mkdirSync(path.dirname(destino), { recursive: true });
|
|
67
|
+
fs.writeFileSync(destino, conteudo, 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Quantos quadros dura cada cena, com a MESMA conta do player.
|
|
71
|
+
*
|
|
72
|
+
* Duplicar esta conta aqui foi tentador e seria errado: se ela divergisse do
|
|
73
|
+
* player, o vídeo exportado por Remotion teria duração diferente do que a
|
|
74
|
+
* pessoa aprovou na tela — e ninguém desconfiaria, porque os dois estariam
|
|
75
|
+
* "funcionando". Então a regra é uma só e está escrita em um lugar; o
|
|
76
|
+
* projeto gerado importa daqui. */
|
|
77
|
+
function segundosDaCena(c) {
|
|
78
|
+
let d = Number(c && c.duracao);
|
|
79
|
+
let clipe = 0;
|
|
80
|
+
for (const e of (c && c.elementos) || []) {
|
|
81
|
+
if (!e || e.tipo !== 'clipe') continue;
|
|
82
|
+
const de = Number(e.de) || 0;
|
|
83
|
+
const ate = Number(e.ate);
|
|
84
|
+
if (isFinite(ate) && ate > de) clipe = Math.max(clipe, ate - de);
|
|
85
|
+
}
|
|
86
|
+
if (!(isFinite(d) && d > 0)) d = clipe > 0 ? Math.min(600, clipe) : 4;
|
|
87
|
+
else d = Math.min(clipe > 0 ? 600 : 60, d);
|
|
88
|
+
|
|
89
|
+
const n = Number(c && c.narracaoDur);
|
|
90
|
+
if (isFinite(n) && n > 0 && c && c.narracaoAudio) d = Math.max(d, Math.min(90, n + 0.4));
|
|
91
|
+
return d;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function totalSegundos(doc) {
|
|
95
|
+
return ((doc && doc.cenas) || []).reduce((s, c) => s + segundosDaCena(c), 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── os arquivos do projeto ───────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function pkgJson(nome) {
|
|
101
|
+
return JSON.stringify({
|
|
102
|
+
name: nome || 'video-primocode',
|
|
103
|
+
version: '1.0.0',
|
|
104
|
+
private: true,
|
|
105
|
+
scripts: {
|
|
106
|
+
/* `studio` abre a pré-visualização com linha do tempo; `render`
|
|
107
|
+
escreve o mp4.
|
|
108
|
+
|
|
109
|
+
O ponto de entrada vai ESCRITO. O Remotion procura sozinho por
|
|
110
|
+
`src/index.ts` e alguns outros nomes fixos, e este projeto é
|
|
111
|
+
.jsx — sem TypeScript, para não arrastar um toolchain inteiro
|
|
112
|
+
para dentro da pasta de quem só quer um vídeo. Sem o caminho,
|
|
113
|
+
ele responde "No entry point specified" e não renderiza nada. */
|
|
114
|
+
studio: 'remotion studio src/index.jsx',
|
|
115
|
+
render: 'remotion render src/index.jsx Video saida/video.mp4',
|
|
116
|
+
build: 'remotion render src/index.jsx Video saida/video.mp4 --codec=h264 --crf=16',
|
|
117
|
+
},
|
|
118
|
+
dependencies: {
|
|
119
|
+
remotion: VERSAO_REMOTION,
|
|
120
|
+
'@remotion/cli': VERSAO_REMOTION,
|
|
121
|
+
react: '19.0.0',
|
|
122
|
+
'react-dom': '19.0.0',
|
|
123
|
+
},
|
|
124
|
+
}, null, 2) + '\n';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const INDEX_JSX = `import { registerRoot } from "remotion";
|
|
128
|
+
import { Raiz } from "./Raiz.jsx";
|
|
129
|
+
|
|
130
|
+
registerRoot(Raiz);
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
function raizJsx(doc, fps) {
|
|
134
|
+
return `import { Composition } from "remotion";
|
|
135
|
+
import { Filme } from "./Filme.jsx";
|
|
136
|
+
import doc from "./doc.json";
|
|
137
|
+
import { totalQuadros } from "./tempo.js";
|
|
138
|
+
|
|
139
|
+
/* A composição. \`durationInFrames\` sai da MESMA conta de duração que o
|
|
140
|
+
player do estúdio usa (ver tempo.js) — se as duas divergissem, o arquivo
|
|
141
|
+
exportado teria outro tamanho que o vídeo aprovado na tela, e nada
|
|
142
|
+
denunciaria. */
|
|
143
|
+
export const Raiz = () => (
|
|
144
|
+
<Composition
|
|
145
|
+
id="Video"
|
|
146
|
+
component={Filme}
|
|
147
|
+
durationInFrames={totalQuadros(doc, ${fps})}
|
|
148
|
+
fps={${fps}}
|
|
149
|
+
width={${doc.largura || 1920}}
|
|
150
|
+
height={${doc.altura || 1080}}
|
|
151
|
+
defaultProps={{ doc }}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* A conta de tempo, em módulo próprio, gerada a partir da função acima.
|
|
158
|
+
Assim o projeto do usuário não depende do PrimoCode estar instalado. */
|
|
159
|
+
function tempoJs(fps) {
|
|
160
|
+
return `/* Quanto dura cada cena — a MESMA regra do player do estúdio.
|
|
161
|
+
*
|
|
162
|
+
* Copiada de propósito para dentro do projeto: ele tem de renderizar sozinho,
|
|
163
|
+
* sem o PrimoCode instalado. Se você mudar a duração de uma cena, mude no
|
|
164
|
+
* doc.json — não aqui.
|
|
165
|
+
*/
|
|
166
|
+
export const FPS = ${fps};
|
|
167
|
+
|
|
168
|
+
export function segundosDaCena(c) {
|
|
169
|
+
let d = Number(c && c.duracao);
|
|
170
|
+
let clipe = 0;
|
|
171
|
+
for (const e of (c && c.elementos) || []) {
|
|
172
|
+
if (!e || e.tipo !== "clipe") continue;
|
|
173
|
+
const de = Number(e.de) || 0;
|
|
174
|
+
const ate = Number(e.ate);
|
|
175
|
+
if (isFinite(ate) && ate > de) clipe = Math.max(clipe, ate - de);
|
|
176
|
+
}
|
|
177
|
+
if (!(isFinite(d) && d > 0)) d = clipe > 0 ? Math.min(600, clipe) : 4;
|
|
178
|
+
else d = Math.min(clipe > 0 ? 600 : 60, d);
|
|
179
|
+
|
|
180
|
+
const n = Number(c && c.narracaoDur);
|
|
181
|
+
if (isFinite(n) && n > 0 && c && c.narracaoAudio) d = Math.max(d, Math.min(90, n + 0.4));
|
|
182
|
+
return d;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/* Quadros, não segundos: dentro do Remotion o tempo é inteiro. Arredondar
|
|
186
|
+
* para cima em cada cena evita a cena de 3,02s virar 3,0 e comer dois
|
|
187
|
+
* quadros do fim — e evita o acúmulo desse erro ao longo do filme. */
|
|
188
|
+
export function quadrosDaCena(c, fps = FPS) {
|
|
189
|
+
return Math.max(1, Math.round(segundosDaCena(c) * fps));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function totalQuadros(doc, fps = FPS) {
|
|
193
|
+
const cenas = (doc && doc.cenas) || [];
|
|
194
|
+
const soma = cenas.reduce((s, c) => s + quadrosDaCena(c, fps), 0);
|
|
195
|
+
return Math.max(1, soma);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Qual cena está no ar no quadro \`q\`, e há quantos quadros ela começou.
|
|
199
|
+
* Devolver o "dentro" junto evita o chamador refazer a soma — e refazer a
|
|
200
|
+
* soma com outra regra de arredondamento é exatamente como as duas metades
|
|
201
|
+
* de um render saem fora de sincronia. */
|
|
202
|
+
export function cenaNoQuadro(doc, q, fps = FPS) {
|
|
203
|
+
const cenas = (doc && doc.cenas) || [];
|
|
204
|
+
let acc = 0;
|
|
205
|
+
for (let i = 0; i < cenas.length; i++) {
|
|
206
|
+
const dur = quadrosDaCena(cenas[i], fps);
|
|
207
|
+
if (q < acc + dur) return { i, dentro: q - acc, dur };
|
|
208
|
+
acc += dur;
|
|
209
|
+
}
|
|
210
|
+
const ult = cenas.length - 1;
|
|
211
|
+
const dur = ult >= 0 ? quadrosDaCena(cenas[ult], fps) : 1;
|
|
212
|
+
return { i: Math.max(0, ult), dentro: dur - 1, dur };
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const FILME_JSX = `import { useCurrentFrame, useVideoConfig, AbsoluteFill, Audio, staticFile } from "remotion";
|
|
218
|
+
import { useEffect, useRef } from "react";
|
|
219
|
+
import { cenaNoQuadro, quadrosDaCena } from "./tempo.js";
|
|
220
|
+
import "./render.css";
|
|
221
|
+
import "./fontes.css";
|
|
222
|
+
import { renderCena } from "./render.js";
|
|
223
|
+
|
|
224
|
+
/* O desenho de uma cena é do ESTÚDIO — render.js é o mesmo arquivo que o
|
|
225
|
+
navegador carrega. Aqui só se decide qual cena está no ar e como ela entra.
|
|
226
|
+
Inventar um segundo desenhista daria duas gramáticas para manter, e uma
|
|
227
|
+
ficaria velha. */
|
|
228
|
+
function Cena({ cena, doc, dentro, dur }) {
|
|
229
|
+
const caixa = useRef(null);
|
|
230
|
+
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
if (!caixa.current) return;
|
|
233
|
+
caixa.current.innerHTML = "";
|
|
234
|
+
// \`animar: false\`: quem anima aqui é o Remotion, quadro a quadro. Com as
|
|
235
|
+
// animações CSS ligadas, cada quadro renderizado recomeçaria a animação
|
|
236
|
+
// do zero — o vídeo sairia com o primeiro instante repetido do começo ao
|
|
237
|
+
// fim, tecnicamente perfeito e visualmente parado.
|
|
238
|
+
caixa.current.appendChild(renderCena(cena, doc, { animar: false }));
|
|
239
|
+
}, [cena, doc]);
|
|
240
|
+
|
|
241
|
+
return <div ref={caixa} style={{ width: "100%", height: "100%" }} />;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/* Entrada e transição vindas do quadro, não do CSS.
|
|
245
|
+
|
|
246
|
+
\`t\` vai de 0 a 1 nos primeiros 18 quadros da cena. A curva é a mesma do
|
|
247
|
+
estúdio (cubic-bezier .22 1 .36 1, aproximada por um easeOutCubic): sem
|
|
248
|
+
isso a peça exportada por Remotion teria outro tempo de entrada que a
|
|
249
|
+
mesma peça vista no navegador. */
|
|
250
|
+
function entrada(transicao, t) {
|
|
251
|
+
const s = 1 - Math.pow(1 - t, 3);
|
|
252
|
+
switch (transicao) {
|
|
253
|
+
case "corte": return {};
|
|
254
|
+
case "deslizar": return { opacity: s, transform: \`translateX(\${(1 - s) * 8}%)\` };
|
|
255
|
+
case "subir": return { opacity: s, transform: \`translateY(\${(1 - s) * 8}%)\` };
|
|
256
|
+
case "zoom": return { opacity: s, transform: \`scale(\${0.92 + s * 0.08})\` };
|
|
257
|
+
case "flash": return { opacity: s, filter: \`brightness(\${1 + (1 - s) * 2.2})\` };
|
|
258
|
+
case "limpar": return { opacity: 1, clipPath: \`inset(0 \${(1 - s) * 100}% 0 0)\` };
|
|
259
|
+
case "persiana": return { opacity: 1, clipPath: \`inset(\${(1 - s) * 50}% 0 \${(1 - s) * 50}% 0)\` };
|
|
260
|
+
default: return { opacity: s }; // fade
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const Filme = ({ doc }) => {
|
|
265
|
+
const quadro = useCurrentFrame();
|
|
266
|
+
const { fps } = useVideoConfig();
|
|
267
|
+
const { i, dentro } = cenaNoQuadro(doc, quadro, fps);
|
|
268
|
+
const cenas = doc.cenas || [];
|
|
269
|
+
const cena = cenas[i] || {};
|
|
270
|
+
const dur = quadrosDaCena(cena, fps);
|
|
271
|
+
|
|
272
|
+
const QUADROS_ENTRADA = Math.max(1, Math.round(0.62 * fps));
|
|
273
|
+
const t = Math.min(1, dentro / QUADROS_ENTRADA);
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<AbsoluteFill style={{ backgroundColor: "#000" }}>
|
|
277
|
+
<AbsoluteFill style={entrada(cena.transicao, t)}>
|
|
278
|
+
<Cena cena={cena} doc={doc} dentro={dentro} dur={dur} />
|
|
279
|
+
</AbsoluteFill>
|
|
280
|
+
{doc.trilha && doc.trilha.src ? (
|
|
281
|
+
<Audio src={doc.trilha.src.startsWith("http") ? doc.trilha.src : staticFile(doc.trilha.src)}
|
|
282
|
+
volume={doc.trilha.volume == null ? 0.7 : doc.trilha.volume} />
|
|
283
|
+
) : null}
|
|
284
|
+
</AbsoluteFill>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
`;
|
|
288
|
+
|
|
289
|
+
/* O render.js do estúdio é um script de navegador: define funções soltas, sem
|
|
290
|
+
* export nenhum. O bundler do Remotion precisa de módulo.
|
|
291
|
+
*
|
|
292
|
+
* A saída é o arquivo ORIGINAL, sem uma linha alterada, com um `export` no
|
|
293
|
+
* fim. Reescrever o conteúdo seria criar uma segunda versão do desenhista
|
|
294
|
+
* para manter — e a que ficasse velha seria justamente a que renderiza o
|
|
295
|
+
* arquivo final, onde ninguém olha até estar publicado. */
|
|
296
|
+
function comoModulo(fonte, nomes) {
|
|
297
|
+
return fonte
|
|
298
|
+
+ '\n\n/* ── acrescentado pelo PrimoCode ─────────────────────────────────────\n'
|
|
299
|
+
+ ' O arquivo acima é o render do estúdio, byte por byte. Só o export é\n'
|
|
300
|
+
+ ' novo: no navegador ele é um script solto, e aqui precisa ser módulo.\n'
|
|
301
|
+
+ ' Não edite o que está em cima — atualize o PrimoCode e gere de novo. */\n'
|
|
302
|
+
+ `export { ${nomes.join(', ')} };\n`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* `icones.js` é lido pelo render.js pelo nome global. Em módulo, o nome não
|
|
306
|
+
vaza para o escopo global sozinho — daí o empurrão explícito. */
|
|
307
|
+
function iconesComoModulo(fonte) {
|
|
308
|
+
return fonte
|
|
309
|
+
+ '\n\n/* ── acrescentado pelo PrimoCode ────────────────────────────────────\n'
|
|
310
|
+
+ ' O render.js procura os ícones no escopo global, como no navegador. */\n'
|
|
311
|
+
+ 'if (typeof globalThis !== "undefined") {\n'
|
|
312
|
+
+ ' if (typeof ICONES !== "undefined") globalThis.ICONES = ICONES;\n'
|
|
313
|
+
+ ' if (typeof svgIcone === "function") globalThis.svgIcone = svgIcone;\n'
|
|
314
|
+
+ '}\n';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readme(titulo, doc) {
|
|
318
|
+
const seg = totalSegundos(doc);
|
|
319
|
+
return `# ${titulo || 'Vídeo'} — projeto Remotion
|
|
320
|
+
|
|
321
|
+
Gerado pelo PrimoCode a partir da peça do Studio. As cenas estão em
|
|
322
|
+
\`src/doc.json\`; o desenho delas é o mesmo do Studio (\`src/render.js\`).
|
|
323
|
+
|
|
324
|
+
## Rodar
|
|
325
|
+
|
|
326
|
+
\`\`\`bash
|
|
327
|
+
npm install # uma vez, e demora — Remotion traz o próprio navegador
|
|
328
|
+
npm run studio # pré-visualização com linha do tempo
|
|
329
|
+
npm run render # escreve saida/video.mp4
|
|
330
|
+
\`\`\`
|
|
331
|
+
|
|
332
|
+
Duração: ${seg.toFixed(1)}s · ${doc.largura || 1920}×${doc.altura || 1080}
|
|
333
|
+
· ${(doc.cenas || []).length} cena(s).
|
|
334
|
+
|
|
335
|
+
## Quando usar isto em vez do Studio
|
|
336
|
+
|
|
337
|
+
O Studio exporta o MP4 num clique, sem instalar nada, e é o caminho normal.
|
|
338
|
+
Este projeto existe para o que ele não faz:
|
|
339
|
+
|
|
340
|
+
- **quadro a quadro determinístico** — o mesmo arquivo em qualquer máquina,
|
|
341
|
+
sem depender de a aba estar em foco nem da carga do computador;
|
|
342
|
+
- **vídeo longo** sem risco de o navegador engasgar no meio;
|
|
343
|
+
- **fps, CRF, ProRes, canal alfa** e renderização paralela;
|
|
344
|
+
- **mexer no código**: daqui para a frente é React, e é seu.
|
|
345
|
+
|
|
346
|
+
## Editar
|
|
347
|
+
|
|
348
|
+
- Conteúdo, cores e tempo: \`src/doc.json\`.
|
|
349
|
+
- Como cada cena entra: \`entrada()\` em \`src/Filme.jsx\`.
|
|
350
|
+
- Como um elemento é desenhado: \`src/render.js\` — mas ele é copiado do
|
|
351
|
+
Studio. Mexendo ali, um novo \`remotion_projeto\` sobrescreve. Se for
|
|
352
|
+
mudança para valer, mude no PrimoCode.
|
|
353
|
+
|
|
354
|
+
## Licença do Remotion
|
|
355
|
+
|
|
356
|
+
Remotion é **grátis para pessoa física e para empresa de até três pessoas**.
|
|
357
|
+
Acima disso, a empresa precisa de licença paga — veja
|
|
358
|
+
<https://remotion.dev/license>. Isso vale para ESTE projeto, que é seu; o
|
|
359
|
+
PrimoCode não depende de Remotion e não muda de licença por causa disto.
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const GITIGNORE = 'node_modules/\nsaida/\n.remotion/\n';
|
|
364
|
+
|
|
365
|
+
// ── montar o projeto ─────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Escreve um projeto Remotion completo a partir de uma peça do Studio.
|
|
369
|
+
*
|
|
370
|
+
* @param {object} op
|
|
371
|
+
* @param {string} op.destino pasta onde o projeto nasce
|
|
372
|
+
* @param {string} op.titulo
|
|
373
|
+
* @param {object} op.doc o documento da peça (cenas, tema, trilha…)
|
|
374
|
+
* @param {number} [op.fps]
|
|
375
|
+
*/
|
|
376
|
+
function montar({ destino, titulo, doc, fps }) {
|
|
377
|
+
if (!destino) return { ok: false, error: 'diga em que pasta o projeto vai nascer' };
|
|
378
|
+
if (!doc || !Array.isArray(doc.cenas) || !doc.cenas.length) {
|
|
379
|
+
return { ok: false, error: 'esta peça não tem cenas — Remotion renderiza cenas, como o Studio' };
|
|
380
|
+
}
|
|
381
|
+
const taxa = Math.max(1, Math.min(120, Number(fps) || 30));
|
|
382
|
+
|
|
383
|
+
const src = path.join(destino, 'src');
|
|
384
|
+
fs.mkdirSync(src, { recursive: true });
|
|
385
|
+
|
|
386
|
+
// Os arquivos do estúdio, sem reescrever conteúdo.
|
|
387
|
+
for (const [de, para] of DO_ESTUDIO) {
|
|
388
|
+
const origem = path.join(ORIGEM_ESTUDIO, de);
|
|
389
|
+
if (!fs.existsSync(origem)) continue;
|
|
390
|
+
let conteudo = fs.readFileSync(origem, 'utf-8');
|
|
391
|
+
if (para === 'render.js') conteudo = comoModulo(conteudo, ['renderCena', 'renderElemento']);
|
|
392
|
+
else if (para === 'icones.js') conteudo = iconesComoModulo(conteudo);
|
|
393
|
+
escrever(path.join(src, para), conteudo);
|
|
394
|
+
}
|
|
395
|
+
// A ordem importa: render.js procura os ícones no global.
|
|
396
|
+
const alvoRender = path.join(src, 'render.js');
|
|
397
|
+
if (fs.existsSync(alvoRender)) {
|
|
398
|
+
escrever(alvoRender, 'import "./icones.js";\n' + fs.readFileSync(alvoRender, 'utf-8'));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
escrever(path.join(destino, 'package.json'), pkgJson(titulo));
|
|
402
|
+
escrever(path.join(destino, 'README.md'), readme(titulo, doc));
|
|
403
|
+
escrever(path.join(destino, '.gitignore'), GITIGNORE);
|
|
404
|
+
escrever(path.join(src, 'index.jsx'), INDEX_JSX);
|
|
405
|
+
escrever(path.join(src, 'Raiz.jsx'), raizJsx(doc, taxa));
|
|
406
|
+
escrever(path.join(src, 'Filme.jsx'), FILME_JSX);
|
|
407
|
+
escrever(path.join(src, 'tempo.js'), tempoJs(taxa));
|
|
408
|
+
escrever(path.join(src, 'doc.json'), JSON.stringify(doc, null, 2) + '\n');
|
|
409
|
+
|
|
410
|
+
return {
|
|
411
|
+
ok: true,
|
|
412
|
+
pasta: destino,
|
|
413
|
+
fps: taxa,
|
|
414
|
+
segundos: +totalSegundos(doc).toFixed(2),
|
|
415
|
+
quadros: Math.round(totalSegundos(doc) * taxa),
|
|
416
|
+
cenas: doc.cenas.length,
|
|
417
|
+
comoRodar: `cd ${destino} && npm install && npm run render`,
|
|
418
|
+
licenca: 'Remotion é grátis para pessoa física e empresa de até 3 pessoas; '
|
|
419
|
+
+ 'acima disso a empresa precisa de licença paga (remotion.dev/license). '
|
|
420
|
+
+ 'Vale para este projeto, não para o PrimoCode.',
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Roda o render de um projeto já montado. Só faz sentido depois do
|
|
426
|
+
* `npm install` — Remotion baixa o próprio navegador, e isso leva minutos na
|
|
427
|
+
* primeira vez.
|
|
428
|
+
*/
|
|
429
|
+
function renderizar({ pasta, saida, minutos = 20 }) {
|
|
430
|
+
return new Promise((resolve) => {
|
|
431
|
+
if (!fs.existsSync(path.join(pasta, 'node_modules'))) {
|
|
432
|
+
return resolve({
|
|
433
|
+
ok: false,
|
|
434
|
+
error: `Falta instalar. Rode: cd ${pasta} && npm install (demora — o Remotion `
|
|
435
|
+
+ 'traz o próprio navegador). Depois peça o render de novo.',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const arquivo = saida || path.join('saida', 'video.mp4');
|
|
439
|
+
const proc = spawn(process.platform === 'win32' ? 'npx.cmd' : 'npx',
|
|
440
|
+
['remotion', 'render', 'src/index.jsx', 'Video', arquivo, '--log=error'],
|
|
441
|
+
{ cwd: pasta, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
442
|
+
|
|
443
|
+
let fora = '';
|
|
444
|
+
proc.stdout.on('data', (d) => { fora += d; });
|
|
445
|
+
proc.stderr.on('data', (d) => { fora += d; });
|
|
446
|
+
|
|
447
|
+
const prazo = setTimeout(() => { try { proc.kill(); } catch {} }, minutos * 60000);
|
|
448
|
+
proc.on('close', (codigo) => {
|
|
449
|
+
clearTimeout(prazo);
|
|
450
|
+
const alvo = path.join(pasta, arquivo);
|
|
451
|
+
if (codigo === 0 && fs.existsSync(alvo)) {
|
|
452
|
+
return resolve({
|
|
453
|
+
ok: true, arquivo: alvo,
|
|
454
|
+
bytes: fs.statSync(alvo).size,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
resolve({ ok: false, error: (fora || `remotion saiu com ${codigo}`).slice(-1200) });
|
|
458
|
+
});
|
|
459
|
+
proc.on('error', (e) => { clearTimeout(prazo); resolve({ ok: false, error: e.message }); });
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
module.exports = { montar, renderizar, segundosDaCena, totalSegundos, VERSAO_REMOTION };
|
package/lib/tools.js
CHANGED
|
@@ -579,6 +579,36 @@ async function despachar(name, args, ctx) {
|
|
|
579
579
|
arquivo: args.arquivo, pausa: args.pausa, abrir: openInBrowser,
|
|
580
580
|
});
|
|
581
581
|
|
|
582
|
+
/* A MESMA peça, virando projeto Remotion.
|
|
583
|
+
*
|
|
584
|
+
* O Studio continua sendo o caminho normal: exporta em segundos,
|
|
585
|
+
* sem instalar nada. Isto é para quando o usuário pede Remotion
|
|
586
|
+
* pelo nome, ou quando a peça precisa do que só ele dá — render
|
|
587
|
+
* determinístico, vídeo longo, fps e codec escolhidos, ou o
|
|
588
|
+
* projeto em React para a pessoa mexer.
|
|
589
|
+
*
|
|
590
|
+
* Parte do documento que já existe: nada de um segundo
|
|
591
|
+
* vocabulário para o agente aprender. */
|
|
592
|
+
case 'remotion_projeto': {
|
|
593
|
+
const remotion = require('./remotion.js');
|
|
594
|
+
const peca = await studio.ler(args.id);
|
|
595
|
+
if (!peca || peca.ok === false) {
|
|
596
|
+
return peca || { ok: false, error: `não achei a peça ${args.id}` };
|
|
597
|
+
}
|
|
598
|
+
const arq = peca.arquivo || peca;
|
|
599
|
+
const base = projectDir || cfg.ROOT_DIR;
|
|
600
|
+
const nome = cfg.slugify(args.pasta || arq.titulo || 'video') + '-remotion';
|
|
601
|
+
const destino = path.join(base, nome);
|
|
602
|
+
const r = remotion.montar({
|
|
603
|
+
destino, titulo: arq.titulo, doc: arq.doc, fps: args.fps,
|
|
604
|
+
});
|
|
605
|
+
if (!r.ok || !args.renderizar) return r;
|
|
606
|
+
// Só renderiza quando pedido: o `npm install` demora minutos e
|
|
607
|
+
// é escolha do usuário, não efeito colateral de criar a pasta.
|
|
608
|
+
const feito = await remotion.renderizar({ pasta: destino });
|
|
609
|
+
return Object.assign(r, { render: feito });
|
|
610
|
+
}
|
|
611
|
+
|
|
582
612
|
case 'desktop_double_click':
|
|
583
613
|
return desktop.doubleClick(args.x, args.y);
|
|
584
614
|
case 'desktop_open_app':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primocode",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.30.0",
|
|
4
4
|
"description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
|
|
5
5
|
"main": "bin/primocode.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/primocode.js",
|
|
11
|
-
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js && node test/elementos.test.js"
|
|
11
|
+
"test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js && node test/elementos.test.js && node test/remotion.test.js && node test/aplicativo.test.js && node test/conta.test.mjs"
|
|
12
12
|
},
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=18.17.0"
|
|
@@ -180,6 +180,37 @@ livre de direitos, ou uma das trilhas geradas acima.
|
|
|
180
180
|
|
|
181
181
|
---
|
|
182
182
|
|
|
183
|
+
## Studio ou Remotion?
|
|
184
|
+
|
|
185
|
+
**O padrão é o Studio.** Ele exporta o MP4 num clique, em segundos, sem
|
|
186
|
+
instalar nada. Não ofereça Remotion sem motivo — trocar um clique por
|
|
187
|
+
`npm install` de 250 MB é piorar a vida de quem só quer o vídeo.
|
|
188
|
+
|
|
189
|
+
Chame `remotion_projeto` (com o `id` da peça) em três casos, e só neles:
|
|
190
|
+
|
|
191
|
+
1. **O usuário pediu Remotion pelo nome.** Não discuta, faça.
|
|
192
|
+
2. **A peça precisa do que o Studio não dá:** render determinístico (o mesmo
|
|
193
|
+
arquivo em qualquer máquina, sem depender de a aba estar em foco), vídeo
|
|
194
|
+
muito longo, fps ou codec específico (ProRes, canal alfa), renderização em
|
|
195
|
+
servidor.
|
|
196
|
+
3. **O usuário quer mexer no código.** O projeto que nasce é React, é dele, e
|
|
197
|
+
entra no git.
|
|
198
|
+
|
|
199
|
+
A peça é a MESMA — `remotion_projeto` parte do documento que você já criou
|
|
200
|
+
com `studio_criar`. Não existe um segundo formato para aprender: crie a peça
|
|
201
|
+
normalmente, veja no navegador, e só então gere o projeto se fizer sentido.
|
|
202
|
+
|
|
203
|
+
O projeto nasce sem `node_modules`. **Não rode `renderizar: true` antes de o
|
|
204
|
+
`npm install` ter sido feito** — diga ao usuário os dois comandos e deixe ele
|
|
205
|
+
decidir se quer esperar.
|
|
206
|
+
|
|
207
|
+
**Licença:** Remotion é grátis para pessoa física e empresa de até três
|
|
208
|
+
pessoas; acima disso a empresa precisa de licença paga. Avise ao oferecer —
|
|
209
|
+
descobrir isso depois de montar o fluxo de trabalho em cima seria péssimo.
|
|
210
|
+
Isso vale para o projeto do usuário, não para o PrimoCode.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
183
214
|
## Elementos que dão cara de produção
|
|
184
215
|
|
|
185
216
|
Estes seis existem porque um vídeo genérico é feito só de título + texto. São
|