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.
Files changed (104) 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 +42 -195
  7. package/lib/catalogo.js +100 -61
  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 +576 -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 +644 -0
  20. package/lib/estudio/slides.js +190 -0
  21. package/lib/estudio/video.js +296 -0
  22. package/lib/fala.js +49 -6
  23. package/lib/marca.js +4 -1
  24. package/lib/markdown.js +376 -0
  25. package/lib/marketplace.js +189 -0
  26. package/lib/pasta.js +15 -4
  27. package/lib/permissao.js +137 -0
  28. package/lib/registro.js +229 -0
  29. package/lib/skills.js +329 -0
  30. package/lib/tools.js +106 -187
  31. package/lib/ui.js +7 -3
  32. package/lib/voz.js +239 -0
  33. package/package.json +12 -3
  34. package/skills/acessibilidade/SKILL.md +28 -0
  35. package/skills/api-que-nao-quebra/SKILL.md +27 -0
  36. package/skills/depurar-de-verdade/SKILL.md +29 -0
  37. package/skills/editar-meu-video/SKILL.md +59 -0
  38. package/skills/escrever-em-portugues/SKILL.md +28 -0
  39. package/skills/git-limpo/SKILL.md +28 -0
  40. package/skills/motion-de-primeira/SKILL.md +62 -0
  41. package/skills/pagina-que-converte/SKILL.md +28 -0
  42. package/skills/performance-web/SKILL.md +28 -0
  43. package/skills/revisao-antes-de-entregar/SKILL.md +27 -0
  44. package/skills/slides-que-prendem/SKILL.md +40 -0
  45. package/studio/edge.py +16 -66
  46. package/studio/narrar.py +274 -0
  47. package/studio/pessoa.py +199 -0
  48. package/voz/primo_voz/__init__.py +0 -0
  49. package/voz/primo_voz/__main__.py +383 -0
  50. package/voz/primo_voz/escuta.py +235 -0
  51. package/voz/primo_voz/orbe.py +216 -0
  52. package/lib/audio-livre.js +0 -177
  53. package/lib/chaves.js +0 -149
  54. package/lib/claude-engine.js +0 -631
  55. package/lib/codex-engine.js +0 -208
  56. package/lib/remotion.js +0 -663
  57. package/lib/studio.js +0 -960
  58. package/studio/PRIMOCODE.md +0 -58
  59. package/studio/README.md +0 -154
  60. package/studio/__pycache__/edge.cpython-311.pyc +0 -0
  61. package/studio/__pycache__/elevenlabs.cpython-311.pyc +0 -0
  62. package/studio/__pycache__/voz.cpython-311.pyc +0 -0
  63. package/studio/arquivos/corte-teste-de-camera-fb823.json +0 -57
  64. package/studio/ferramentas/00-protocolo.md +0 -125
  65. package/studio/ferramentas/01-modelos.md +0 -270
  66. package/studio/ferramentas/README.md +0 -32
  67. package/studio/ferramentas/corte.md +0 -919
  68. package/studio/ferramentas/grade.md +0 -92
  69. package/studio/ferramentas/prisma.md +0 -107
  70. package/studio/ferramentas/prosa.md +0 -86
  71. package/studio/ferramentas/tela.md +0 -93
  72. package/studio/ferramentas/traco.md +0 -79
  73. package/studio/server.py +0 -2254
  74. package/studio/studio.log +0 -57
  75. package/studio/voz.py +0 -388
  76. package/studio/web/analise.html +0 -52
  77. package/studio/web/core.css +0 -1167
  78. package/studio/web/fontes/bebas.woff2 +0 -0
  79. package/studio/web/fontes/grotesk-700.woff2 +0 -0
  80. package/studio/web/fontes/inter-400.woff2 +0 -0
  81. package/studio/web/fontes/inter-700.woff2 +0 -0
  82. package/studio/web/fontes/jetbrains-400.woff2 +0 -0
  83. package/studio/web/fontes/playfair-700.woff2 +0 -0
  84. package/studio/web/fontes.css +0 -23
  85. package/studio/web/home.html +0 -217
  86. package/studio/web/js/analise.js +0 -510
  87. package/studio/web/js/audio.js +0 -607
  88. package/studio/web/js/editor.js +0 -1802
  89. package/studio/web/js/exportar.js +0 -222
  90. package/studio/web/js/ferramentas.js +0 -108
  91. package/studio/web/js/icones.js +0 -120
  92. package/studio/web/js/midia.js +0 -157
  93. package/studio/web/js/narracao.js +0 -325
  94. package/studio/web/js/player.js +0 -903
  95. package/studio/web/js/render-grade.js +0 -243
  96. package/studio/web/js/render-prosa.js +0 -104
  97. package/studio/web/js/render.js +0 -1852
  98. package/studio/web/js/sfx.js +0 -233
  99. package/studio/web/js/video.js +0 -464
  100. package/studio/web/js/voz-neural.js +0 -152
  101. package/studio/web/render.css +0 -1352
  102. package/studio/web/tf.html +0 -15
  103. package/studio/web/tool.html +0 -78
  104. package/studio/web/view.html +0 -27
package/lib/remotion.js DELETED
@@ -1,663 +0,0 @@
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, Sequence, staticFile, delayRender, continueRender } from "remotion";
218
- import { useLayoutEffect, 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
- /* ── POR QUE ESTE ARQUIVO MUDOU ──────────────────────────────────────────
225
- O primeiro vídeo que saiu daqui estava MUDO e PARADO: \`animar: false\`
226
- congelava toda animação (o número não contava, a lista não revelava, o
227
- vidro não brilhava), e o único áudio era \`trilha.src\` — sem narração, sem
228
- trilha gerada, sem efeitos. Tecnicamente perfeito, visualmente morto.
229
-
230
- Agora a cena é desenhada COM as animações ligadas (a mesma classe
231
- \`animar\` do player) e, a cada quadro, TODAS elas são levadas ao instante
232
- certo pela Web Animations API (\`getAnimations().currentTime\`). É o que
233
- torna o render determinístico: o mesmo quadro em qualquer máquina, sem
234
- depender de relógio. A contagem dos números e o instante dos clipes seguem
235
- a mesma conta do player. A narração de cada cena, a trilha em arquivo, o
236
- áudio dos clipes e os sons por arquivo entram como <Audio> do Remotion.
237
-
238
- O que o Remotion NÃO faz sozinho: a trilha GERADA e os sons SINTETIZADOS
239
- (são WebAudio ao vivo). Por isso o studio_exportar do PrimoCode renderiza
240
- este projeto sem áudio e MISTURA o áudio do Studio — é a mescla dos dois.
241
- Aqui dentro, \`doc.__semAudio\` é como o PrimoCode pede esse silêncio. */
242
-
243
- const src = (u) => (/^(https?:|data:|blob:)/.test(u) ? u : staticFile(String(u).replace(/^\\//, "")));
244
-
245
- function inicioDaCena(cenas, k, fps) {
246
- let q = 0;
247
- for (let i = 0; i < k; i++) q += quadrosDaCena(cenas[i], fps);
248
- return q;
249
- }
250
-
251
- /* A contagem dos números, determinística: a MESMA curva do player
252
- (1,1 s, easeOutCubic), só que em função do tempo da cena e não do relógio. */
253
- function contarEm(el, t) {
254
- el.querySelectorAll("b[data-contar]").forEach((b) => {
255
- const alvo = parseFloat(b.getAttribute("data-contar"));
256
- const molde = b.getAttribute("data-molde") || String(alvo);
257
- const casas = (String(alvo).split(".")[1] || "").length;
258
- const trecho = molde.match(/-?[\\d.,]+/);
259
- if (!trecho) return;
260
- const p = Math.min(1, t / 1.1);
261
- const suave = 1 - Math.pow(1 - p, 3);
262
- const v = (alvo * suave).toFixed(casas);
263
- b.textContent = p >= 1 ? molde : molde.replace(trecho[0], String(v).replace(".", casas ? "," : ""));
264
- });
265
- }
266
-
267
- function Cena({ cena, doc, dentro, fps }) {
268
- const caixa = useRef(null);
269
- const no = useRef(null);
270
-
271
- useLayoutEffect(() => {
272
- if (!caixa.current) return;
273
- caixa.current.innerHTML = "";
274
- no.current = renderCena(cena, doc, { animar: true });
275
- caixa.current.appendChild(no.current);
276
- }, [cena, doc]);
277
-
278
- useLayoutEffect(() => {
279
- const el = no.current;
280
- if (!el) return;
281
- const t = dentro / fps;
282
- // 1. Toda animação CSS — entrada, chaves, transição da cena, brilho do
283
- // vidro, cubo girando — no instante t. Pausada: quem avança é o quadro.
284
- let anims = [];
285
- try { anims = el.getAnimations({ subtree: true }); } catch (e) { anims = []; }
286
- for (const a of anims) {
287
- try { a.pause(); a.currentTime = t * 1000; } catch (e) { /* animação já encerrada */ }
288
- }
289
- // 2. Os números que contam.
290
- contarEm(el, t);
291
- // 3. Os clipes do usuário: o quadro certo do arquivo, esperado de verdade.
292
- el.querySelectorAll("video").forEach((v) => {
293
- const de = Number(v.dataset.de || 0);
294
- const alvo = de + t;
295
- v.pause();
296
- const buscar = () => {
297
- if (Math.abs(v.currentTime - alvo) < 1 / (fps * 2)) return;
298
- const h = delayRender("clipe em " + alvo.toFixed(2) + "s");
299
- const fim = () => { v.removeEventListener("seeked", fim); continueRender(h); };
300
- v.addEventListener("seeked", fim);
301
- v.currentTime = alvo;
302
- };
303
- if (v.readyState >= 1) buscar();
304
- else {
305
- const h = delayRender("carregando clipe");
306
- v.addEventListener("loadedmetadata", () => { buscar(); continueRender(h); }, { once: true });
307
- }
308
- });
309
- }, [dentro, fps, cena, doc]);
310
-
311
- return <div ref={caixa} style={{ width: "100%", height: "100%" }} />;
312
- }
313
-
314
- /* O som da peça, fora o que é sintetizado. */
315
- function Sons({ doc, fps }) {
316
- if (doc.__semAudio) return null;
317
- const cenas = doc.cenas || [];
318
- const pecas = [];
319
- const trilha = doc.trilha || {};
320
- if (trilha.src && trilha.tipo !== "gerada") {
321
- pecas.push(<Audio key="trilha" src={src(trilha.src)} loop={trilha.repetir !== false}
322
- volume={trilha.volume == null ? 0.7 : trilha.volume}
323
- startFrom={Math.round((Number(trilha.inicio) || 0) * fps)} />);
324
- }
325
- cenas.forEach((c, k) => {
326
- const de = inicioDaCena(cenas, k, fps);
327
- const dur = quadrosDaCena(c, fps);
328
- if (c.narracaoAudio) {
329
- pecas.push(<Sequence key={"n" + k} from={de} durationInFrames={dur}>
330
- <Audio src={src(c.narracaoAudio)} volume={doc.voz && doc.voz.volume != null ? doc.voz.volume : 1} />
331
- </Sequence>);
332
- }
333
- (c.elementos || []).forEach((e, i) => {
334
- if (!e) return;
335
- if (e.tipo === "clipe" && e.src && !e.mudo) {
336
- pecas.push(<Sequence key={"c" + k + "-" + i} from={de} durationInFrames={dur}>
337
- <Audio src={src(e.src)} startFrom={Math.round((Number(e.de) || 0) * fps)}
338
- volume={e.volume == null ? 1 : e.volume} />
339
- </Sequence>);
340
- }
341
- if (e.som && typeof e.som === "object" && e.som.arquivo) {
342
- const atraso = e.som.em != null ? Number(e.som.em)
343
- : (e.anim && typeof e.anim === "object" && e.anim.atraso != null) ? Number(e.anim.atraso)
344
- : (e.chaves && e.chaves.length > 1 ? 0 : i * 0.09 + 0.1);
345
- pecas.push(<Sequence key={"s" + k + "-" + i} from={de + Math.round(atraso * fps)}>
346
- <Audio src={src(e.som.arquivo)} volume={e.som.volume == null ? 0.7 : e.som.volume} />
347
- </Sequence>);
348
- }
349
- });
350
- });
351
- return <>{pecas}</>;
352
- }
353
-
354
- export const Filme = ({ doc }) => {
355
- const quadro = useCurrentFrame();
356
- const { fps } = useVideoConfig();
357
- const { i, dentro } = cenaNoQuadro(doc, quadro, fps);
358
- const cenas = doc.cenas || [];
359
- const cena = cenas[i] || {};
360
-
361
- return (
362
- <AbsoluteFill style={{ backgroundColor: "#000" }}>
363
- <Cena cena={cena} doc={doc} dentro={dentro} fps={fps} />
364
- <Sons doc={doc} fps={fps} />
365
- </AbsoluteFill>
366
- );
367
- };
368
- `;
369
-
370
- /* O render.js do estúdio é um script de navegador: define funções soltas, sem
371
- * export nenhum. O bundler do Remotion precisa de módulo.
372
- *
373
- * A saída é o arquivo ORIGINAL, sem uma linha alterada, com um `export` no
374
- * fim. Reescrever o conteúdo seria criar uma segunda versão do desenhista
375
- * para manter — e a que ficasse velha seria justamente a que renderiza o
376
- * arquivo final, onde ninguém olha até estar publicado. */
377
- function comoModulo(fonte, nomes) {
378
- return fonte
379
- + '\n\n/* ── acrescentado pelo PrimoCode ─────────────────────────────────────\n'
380
- + ' O arquivo acima é o render do estúdio, byte por byte. Só o export é\n'
381
- + ' novo: no navegador ele é um script solto, e aqui precisa ser módulo.\n'
382
- + ' Não edite o que está em cima — atualize o PrimoCode e gere de novo. */\n'
383
- + `export { ${nomes.join(', ')} };\n`;
384
- }
385
-
386
- /* `icones.js` é lido pelo render.js pelo nome global. Em módulo, o nome não
387
- vaza para o escopo global sozinho — daí o empurrão explícito. */
388
- function iconesComoModulo(fonte) {
389
- return fonte
390
- + '\n\n/* ── acrescentado pelo PrimoCode ────────────────────────────────────\n'
391
- + ' O render.js procura os ícones no escopo global, como no navegador. */\n'
392
- + 'if (typeof globalThis !== "undefined") {\n'
393
- + ' if (typeof ICONES !== "undefined") globalThis.ICONES = ICONES;\n'
394
- + ' if (typeof svgIcone === "function") globalThis.svgIcone = svgIcone;\n'
395
- + '}\n';
396
- }
397
-
398
- function readme(titulo, doc) {
399
- const seg = totalSegundos(doc);
400
- return `# ${titulo || 'Vídeo'} — projeto Remotion
401
-
402
- Gerado pelo PrimoCode a partir da peça do Studio. As cenas estão em
403
- \`src/doc.json\`; o desenho delas é o mesmo do Studio (\`src/render.js\`).
404
-
405
- ## Rodar
406
-
407
- \`\`\`bash
408
- npm install # uma vez, e demora — Remotion traz o próprio navegador
409
- npm run studio # pré-visualização com linha do tempo
410
- npm run render # escreve saida/video.mp4
411
- \`\`\`
412
-
413
- Duração: ${seg.toFixed(1)}s · ${doc.largura || 1920}×${doc.altura || 1080}
414
- · ${(doc.cenas || []).length} cena(s).
415
-
416
- ## Quando usar isto em vez do Studio
417
-
418
- O Studio exporta o MP4 num clique, sem instalar nada, e é o caminho normal.
419
- Este projeto existe para o que ele não faz:
420
-
421
- - **quadro a quadro determinístico** — o mesmo arquivo em qualquer máquina,
422
- sem depender de a aba estar em foco nem da carga do computador;
423
- - **vídeo longo** sem risco de o navegador engasgar no meio;
424
- - **fps, CRF, ProRes, canal alfa** e renderização paralela;
425
- - **mexer no código**: daqui para a frente é React, e é seu.
426
-
427
- ## Editar
428
-
429
- - Conteúdo, cores e tempo: \`src/doc.json\`.
430
- - Como cada cena entra: \`entrada()\` em \`src/Filme.jsx\`.
431
- - Como um elemento é desenhado: \`src/render.js\` — mas ele é copiado do
432
- Studio. Mexendo ali, um novo \`remotion_projeto\` sobrescreve. Se for
433
- mudança para valer, mude no PrimoCode.
434
-
435
- ## Licença do Remotion
436
-
437
- Remotion é **grátis para pessoa física e para empresa de até três pessoas**.
438
- Acima disso, a empresa precisa de licença paga — veja
439
- <https://remotion.dev/license>. Isso vale para ESTE projeto, que é seu; o
440
- PrimoCode não depende de Remotion e não muda de licença por causa disto.
441
- `;
442
- }
443
-
444
- const GITIGNORE = 'node_modules/\nsaida/\n.remotion/\n';
445
-
446
- // ── montar o projeto ─────────────────────────────────────────────────────
447
-
448
- /**
449
- * Escreve um projeto Remotion completo a partir de uma peça do Studio.
450
- *
451
- * @param {object} op
452
- * @param {string} op.destino pasta onde o projeto nasce
453
- * @param {string} op.titulo
454
- * @param {object} op.doc o documento da peça (cenas, tema, trilha…)
455
- * @param {number} [op.fps]
456
- */
457
- function montar({ destino, titulo, doc, fps, midiaDir, semAudio }) {
458
- if (!destino) return { ok: false, error: 'diga em que pasta o projeto vai nascer' };
459
- if (!doc || !Array.isArray(doc.cenas) || !doc.cenas.length) {
460
- return { ok: false, error: 'esta peça não tem cenas — Remotion renderiza cenas, como o Studio' };
461
- }
462
- const taxa = Math.max(1, Math.min(120, Number(fps) || 30));
463
-
464
- const src = path.join(destino, 'src');
465
- fs.mkdirSync(src, { recursive: true });
466
-
467
- // A MÍDIA vai junto: "/midia/x.mp3" no doc vira public/midia/x.mp3 no
468
- // projeto, que o Remotion serve por staticFile(). Sem isto a narração, a
469
- // trilha e os clipes apontavam para o servidor do Studio — que não existe
470
- // na hora do render, e o projeto nascia mudo sem dizer por quê.
471
- const midia = [];
472
- if (midiaDir) {
473
- const andar = (o) => {
474
- if (!o) return;
475
- if (typeof o === 'string') { if (o.startsWith('/midia/')) midia.push(o); return; }
476
- if (Array.isArray(o)) return o.forEach(andar);
477
- if (typeof o === 'object') Object.values(o).forEach(andar);
478
- };
479
- andar(doc);
480
- for (const u of new Set(midia)) {
481
- const origem = path.join(midiaDir, path.basename(u));
482
- if (!fs.existsSync(origem)) continue;
483
- const alvo = path.join(destino, 'public', 'midia', path.basename(u));
484
- fs.mkdirSync(path.dirname(alvo), { recursive: true });
485
- try { fs.copyFileSync(origem, alvo); } catch { /* fica sem essa */ }
486
- }
487
- }
488
- const docFinal = semAudio ? Object.assign({}, doc, { __semAudio: true }) : doc;
489
-
490
- // Os arquivos do estúdio, sem reescrever conteúdo.
491
- for (const [de, para] of DO_ESTUDIO) {
492
- const origem = path.join(ORIGEM_ESTUDIO, de);
493
- if (!fs.existsSync(origem)) continue;
494
- let conteudo = fs.readFileSync(origem, 'utf-8');
495
- if (para === 'render.js') conteudo = comoModulo(conteudo, ['renderCena', 'renderElemento']);
496
- else if (para === 'icones.js') conteudo = iconesComoModulo(conteudo);
497
- escrever(path.join(src, para), conteudo);
498
- }
499
- // A ordem importa: render.js procura os ícones no global.
500
- const alvoRender = path.join(src, 'render.js');
501
- if (fs.existsSync(alvoRender)) {
502
- escrever(alvoRender, 'import "./icones.js";\n' + fs.readFileSync(alvoRender, 'utf-8'));
503
- }
504
-
505
- escrever(path.join(destino, 'package.json'), pkgJson(titulo));
506
- escrever(path.join(destino, 'README.md'), readme(titulo, doc));
507
- escrever(path.join(destino, '.gitignore'), GITIGNORE);
508
- escrever(path.join(src, 'index.jsx'), INDEX_JSX);
509
- escrever(path.join(src, 'Raiz.jsx'), raizJsx(docFinal, taxa));
510
- escrever(path.join(src, 'Filme.jsx'), FILME_JSX);
511
- escrever(path.join(src, 'tempo.js'), tempoJs(taxa));
512
- escrever(path.join(src, 'doc.json'), JSON.stringify(docFinal, null, 2) + '\n');
513
-
514
- return {
515
- ok: true,
516
- pasta: destino,
517
- midia: [...new Set(midia)].length,
518
- fps: taxa,
519
- segundos: +totalSegundos(doc).toFixed(2),
520
- quadros: Math.round(totalSegundos(doc) * taxa),
521
- cenas: doc.cenas.length,
522
- comoRodar: `cd ${destino} && npm install && npm run render`,
523
- licenca: 'Remotion é grátis para pessoa física e empresa de até 3 pessoas; '
524
- + 'acima disso a empresa precisa de licença paga (remotion.dev/license). '
525
- + 'Vale para este projeto, não para o PrimoCode.',
526
- };
527
- }
528
-
529
- /**
530
- * Roda o render de um projeto já montado. Só faz sentido depois do
531
- * `npm install` — Remotion baixa o próprio navegador, e isso leva minutos na
532
- * primeira vez.
533
- */
534
- function renderizar({ pasta, saida, minutos = 20 }) {
535
- return new Promise((resolve) => {
536
- if (!fs.existsSync(path.join(pasta, 'node_modules'))) {
537
- return resolve({
538
- ok: false,
539
- error: `Falta instalar. Rode: cd ${pasta} && npm install (demora — o Remotion `
540
- + 'traz o próprio navegador). Depois peça o render de novo.',
541
- });
542
- }
543
- const arquivo = saida || path.join('saida', 'video.mp4');
544
- const proc = spawn(process.platform === 'win32' ? 'npx.cmd' : 'npx',
545
- ['remotion', 'render', 'src/index.jsx', 'Video', arquivo, '--log=error'],
546
- { cwd: pasta, stdio: ['ignore', 'pipe', 'pipe'] });
547
-
548
- let fora = '';
549
- proc.stdout.on('data', (d) => { fora += d; });
550
- proc.stderr.on('data', (d) => { fora += d; });
551
-
552
- const prazo = setTimeout(() => { try { proc.kill(); } catch {} }, minutos * 60000);
553
- proc.on('close', (codigo) => {
554
- clearTimeout(prazo);
555
- const alvo = path.join(pasta, arquivo);
556
- if (codigo === 0 && fs.existsSync(alvo)) {
557
- return resolve({
558
- ok: true, arquivo: alvo,
559
- bytes: fs.statSync(alvo).size,
560
- });
561
- }
562
- resolve({ ok: false, error: (fora || `remotion saiu com ${codigo}`).slice(-1200) });
563
- });
564
- proc.on('error', (e) => { clearTimeout(prazo); resolve({ ok: false, error: e.message }); });
565
- });
566
- }
567
-
568
- /* ── A MESCLA: o Remotion como motor de render do studio_exportar ─────────
569
- *
570
- * O dono: "ele deve usar o Remotion e o Studio, deve mesclar os 2". O Studio
571
- * é o autor — a cena, o motion, a narração, a trilha gerada, os efeitos — e
572
- * grava o MP4 em tempo real, com o risco de quadro perdido quando a máquina
573
- * está ocupada. O Remotion desenha a MESMA cena quadro a quadro, sem relógio,
574
- * mas não tem WebAudio. Então: o vídeo vem do Remotion, o áudio vem do
575
- * Studio, e o ffmpeg junta os dois. Quem chama é o lib/studio.js.
576
- *
577
- * Uma instalação só, em ~/.primocode/remotion — 250 MB uma vez, para todos
578
- * os projetos —, e nunca dentro do PrimoCode (zero dependências continua
579
- * valendo). Os projetos renderizados nascem em projetos/<slug>/ e resolvem
580
- * `remotion` e `react` subindo a árvore até o node_modules compartilhado. */
581
- const os = require('os');
582
- const CASA_REMOTION = process.env.PRIMOCODE_REMOTION_CASA || path.join(os.homedir(), '.primocode', 'remotion');
583
-
584
- function instalado() {
585
- return fs.existsSync(path.join(CASA_REMOTION, 'node_modules', '@remotion', 'cli', 'package.json'))
586
- && fs.existsSync(path.join(CASA_REMOTION, 'node_modules', 'react'));
587
- }
588
-
589
- function cliRemotion() {
590
- const pasta = path.join(CASA_REMOTION, 'node_modules', '@remotion', 'cli');
591
- try {
592
- const pkg = JSON.parse(fs.readFileSync(path.join(pasta, 'package.json'), 'utf-8'));
593
- const bin = typeof pkg.bin === 'string' ? pkg.bin : (pkg.bin && (pkg.bin.remotion || Object.values(pkg.bin)[0]));
594
- if (bin) return path.join(pasta, bin);
595
- } catch { /* cai no nome conhecido */ }
596
- return path.join(pasta, 'remotion-cli.js');
597
- }
598
-
599
- let instalando = null;
600
- /** Traz o Remotion uma vez. `avisar(texto)` recebe o que dizer na tela. */
601
- function garantirInstalado({ minutos = 15, avisar } = {}) {
602
- if (instalado()) return Promise.resolve({ ok: true, jaEstava: true });
603
- if (instalando) return instalando;
604
- instalando = new Promise((resolve) => {
605
- fs.mkdirSync(CASA_REMOTION, { recursive: true });
606
- escrever(path.join(CASA_REMOTION, 'package.json'), pkgJson('primocode-remotion'));
607
- if (avisar) avisar('instalando o Remotion (uma vez, ~250 MB — alguns minutos)…');
608
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
609
- const proc = spawn(npm, ['install', '--no-audit', '--no-fund', '--loglevel=error'],
610
- { cwd: CASA_REMOTION, stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32' });
611
- let fora = '';
612
- proc.stdout.on('data', (d) => { fora += d; });
613
- proc.stderr.on('data', (d) => { fora += d; });
614
- const prazo = setTimeout(() => { try { proc.kill(); } catch {} }, minutos * 60000);
615
- proc.on('close', (codigo) => {
616
- clearTimeout(prazo);
617
- instalando = null;
618
- if (codigo === 0 && instalado()) return resolve({ ok: true });
619
- resolve({ ok: false, error: (fora || `npm saiu com ${codigo}`).slice(-600) });
620
- });
621
- proc.on('error', (e) => { clearTimeout(prazo); instalando = null; resolve({ ok: false, error: e.message }); });
622
- });
623
- return instalando;
624
- }
625
-
626
- /**
627
- * Renderiza UMA peça do Studio pelo Remotion e devolve o mp4 (só vídeo quando
628
- * `semAudio`). Monta o projeto em CASA_REMOTION/projetos/<slug>, com a mídia.
629
- */
630
- async function renderizarPeca({ doc, titulo, fps, midiaDir, semAudio = true, minutos = 30, avisar }) {
631
- const pronto = await garantirInstalado({ avisar });
632
- if (!pronto.ok) return { ok: false, error: `Remotion não instalou: ${pronto.error}` };
633
- const slug = String(titulo || 'video').normalize('NFD').replace(/[̀-ͯ]/g, '')
634
- .replace(/[^\w-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase().slice(0, 40) || 'video';
635
- const destino = path.join(CASA_REMOTION, 'projetos', slug);
636
- const m = montar({ destino, titulo, doc, fps, midiaDir, semAudio });
637
- if (!m.ok) return m;
638
- if (avisar) avisar(`renderizando ${m.quadros} quadros pelo Remotion…`);
639
- const arquivo = path.join('saida', 'video.mp4');
640
- fs.mkdirSync(path.join(destino, 'saida'), { recursive: true });
641
- const args = [cliRemotion(), 'render', 'src/index.jsx', 'Video', arquivo,
642
- '--codec=h264', '--crf=18', '--log=error', '--concurrency=50%'];
643
- if (semAudio) args.push('--muted');
644
- return new Promise((resolve) => {
645
- const proc = spawn(process.execPath, args, { cwd: destino, stdio: ['ignore', 'pipe', 'pipe'] });
646
- let fora = '';
647
- proc.stdout.on('data', (d) => { fora += d; });
648
- proc.stderr.on('data', (d) => { fora += d; });
649
- const prazo = setTimeout(() => { try { proc.kill(); } catch {} }, minutos * 60000);
650
- proc.on('close', (codigo) => {
651
- clearTimeout(prazo);
652
- const alvo = path.join(destino, arquivo);
653
- if (codigo === 0 && fs.existsSync(alvo)) {
654
- return resolve({ ok: true, arquivo: alvo, bytes: fs.statSync(alvo).size, quadros: m.quadros, segundos: m.segundos, pasta: destino });
655
- }
656
- resolve({ ok: false, error: (fora || `remotion saiu com ${codigo}`).slice(-1200) });
657
- });
658
- proc.on('error', (e) => { clearTimeout(prazo); resolve({ ok: false, error: e.message }); });
659
- });
660
- }
661
-
662
- module.exports = { montar, renderizar, renderizarPeca, garantirInstalado, instalado, cliRemotion,
663
- segundosDaCena, totalSegundos, VERSAO_REMOTION, CASA_REMOTION };