primocode 8.42.1 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -37
- package/bin/primocode.js +599 -327
- package/lib/acervo.js +8 -1
- package/lib/act.js +205 -114
- package/lib/api.js +35 -4
- package/lib/boas-vindas.js +31 -192
- package/lib/catalogo.js +60 -60
- 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 +545 -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 +606 -0
- package/lib/estudio/slides.js +190 -0
- package/lib/estudio/video.js +296 -0
- package/lib/fala.js +13 -1
- package/lib/marca.js +4 -1
- package/lib/marketplace.js +189 -0
- package/lib/permissao.js +137 -0
- package/lib/registro.js +229 -0
- package/lib/skills.js +329 -0
- package/lib/tools.js +51 -181
- 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 +167 -0
- package/voz/primo_voz/__init__.py +0 -0
- package/voz/primo_voz/__main__.py +325 -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
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 };
|