vibeeditor-render 0.2.0 → 0.3.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 (63) hide show
  1. package/README.md +19 -2
  2. package/cli/vibeeditor-render.js +19 -3
  3. package/core/audio.js +140 -18
  4. package/core/cache.js +21 -1
  5. package/core/config.js +5 -0
  6. package/core/ffmpeg.js +149 -0
  7. package/core/jobs.js +169 -24
  8. package/core/normalizar.js +454 -0
  9. package/core/nucleo.js +8 -3
  10. package/core/servidor.js +22 -1
  11. package/core/transcricao.js +197 -0
  12. package/core/voz.js +227 -0
  13. package/host-dist/15fb600d42056579.woff2 +0 -0
  14. package/host-dist/1895916a733c4d0e.woff2 +0 -0
  15. package/host-dist/1e96c5356ee905bc.woff2 +0 -0
  16. package/host-dist/2a8028ec716097bf.woff2 +0 -0
  17. package/host-dist/3061f5b6eb250216.woff2 +0 -0
  18. package/host-dist/376dd8dc38524313.woff2 +0 -0
  19. package/host-dist/41be664a74a50674.woff2 +0 -0
  20. package/host-dist/428.bundle.js +1 -1
  21. package/host-dist/46019c506ae1d42c.woff2 +0 -0
  22. package/host-dist/50129cb1dcd58da5.woff2 +0 -0
  23. package/host-dist/5ab57485954531d6.woff2 +0 -0
  24. package/host-dist/606cdd767ee835e0.woff2 +0 -0
  25. package/host-dist/66df0abfcfe8da3e.woff2 +0 -0
  26. package/host-dist/6903de9124a5cab6.woff2 +0 -0
  27. package/host-dist/693cf6c39ab15de9.woff2 +0 -0
  28. package/host-dist/72884976680cf128.woff2 +0 -0
  29. package/host-dist/7777133e901cd5ed.woff2 +0 -0
  30. package/host-dist/78db4f537e4b09d0.woff2 +0 -0
  31. package/host-dist/7a78f1ce0329757f.woff2 +0 -0
  32. package/host-dist/7c3e92e6007b76fd.woff2 +0 -0
  33. package/host-dist/81df42c325a360a6.woff2 +0 -0
  34. package/host-dist/8633e74025a037d3.woff2 +0 -0
  35. package/host-dist/872.bundle.js +1 -1
  36. package/host-dist/8cf68dfb15aca3e4.woff2 +0 -0
  37. package/host-dist/8fb1a96451cec840.woff2 +0 -0
  38. package/host-dist/916d3686010a8de2.woff2 +0 -0
  39. package/host-dist/92d1bbe48d4eec3e.woff2 +0 -0
  40. package/host-dist/93e25f31623ab732.woff2 +0 -0
  41. package/host-dist/95045d685be8648f.woff2 +0 -0
  42. package/host-dist/9545a3d1fceacf5c.woff2 +0 -0
  43. package/host-dist/96be080130d6f32c.woff2 +0 -0
  44. package/host-dist/9a881e2ac07d406b.woff2 +0 -0
  45. package/host-dist/ad20d13d2a924905.woff2 +0 -0
  46. package/host-dist/af2e87f5d61b5665.woff2 +0 -0
  47. package/host-dist/b19b14aaaeb386b9.woff2 +0 -0
  48. package/host-dist/b447814dc676cec1.woff2 +0 -0
  49. package/host-dist/b7b43acee81d05a8.woff2 +0 -0
  50. package/host-dist/bd427f25ac24d036.woff2 +0 -0
  51. package/host-dist/bundle.js +1509 -12
  52. package/host-dist/bundle.js.map +1 -1
  53. package/host-dist/c04551857776278f.woff2 +0 -0
  54. package/host-dist/c050c0dd427d93a6.woff2 +0 -0
  55. package/host-dist/cb2a73f0b29f1031.woff2 +0 -0
  56. package/host-dist/d36a2a2bb416f59e.woff2 +0 -0
  57. package/host-dist/d4df281bd2c98884.woff2 +0 -0
  58. package/host-dist/d869208648ca5469.woff2 +0 -0
  59. package/host-dist/db9d659a1acad1e2.woff2 +0 -0
  60. package/host-dist/ebeb24f1e7287d3a.woff2 +0 -0
  61. package/host-dist/ef6d3f52c547a6e3.woff2 +0 -0
  62. package/host-dist/f4646f71883e1c80.woff2 +0 -0
  63. package/package.json +13 -2
package/core/jobs.js CHANGED
@@ -8,12 +8,50 @@ const {LIMITES, PRESETS} = require("./config");
8
8
  const {sanitizarNome, shaValido} = require("./seguranca");
9
9
 
10
10
  const inteiro = (v, min, max) => Number.isInteger(v) && v >= min && v <= max;
11
+ const numero = (v, min, max) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max;
12
+ const NOME_ARQUIVO = /^[\w.-]{1,64}$/;
13
+
14
+ /**
15
+ * pedido de áudio tratado (opcional): antes do render, o app trata a fala de arquivos[de] com o ffmpeg (cadeia do
16
+ * montar.py) e põe o WAV no job como arquivos[para]. {de, para, trechos [[ini, fim]], ganhos_db [], lufs_alvo,
17
+ * pico_dbtp, passa_alta_hz, reducao_ruido_db, voz_gerada, pico_final_dbtp (opcional: limitador no MP4 final)}
18
+ */
19
+ function validarAudio(a, arquivos, erro) {
20
+ if (a == null) return null;
21
+ if (typeof a !== "object" || Array.isArray(a)) throw erro("audio precisa ser um objeto");
22
+ if (typeof a.de !== "string" || !Object.prototype.hasOwnProperty.call(arquivos, a.de)) throw erro("audio.de precisa ser um dos arquivos do job");
23
+ if (typeof a.para !== "string" || !NOME_ARQUIVO.test(a.para) || Object.prototype.hasOwnProperty.call(arquivos, a.para)) throw erro("audio.para precisa ser um nome novo (letras, números, . _ -)");
24
+ const t = a.trechos;
25
+ if (!Array.isArray(t) || !t.length || t.length > 2000) throw erro("audio.trechos precisa de 1 a 2000 trechos");
26
+ let ant = 0;
27
+ for (const x of t) {
28
+ if (!Array.isArray(x) || x.length !== 2 || !numero(x[0], 0, 86400) || !numero(x[1], 0, 86400) || x[1] < x[0] || x[0] < ant) throw erro("audio.trechos: [[ini, fim]] em ordem, em segundos");
29
+ ant = x[1];
30
+ }
31
+ const g = a.ganhos_db == null ? t.map(() => 0) : a.ganhos_db;
32
+ if (!Array.isArray(g) || g.length !== t.length || !g.every((v) => numero(v, -24, 24))) throw erro("audio.ganhos_db: um ganho (−24 a 24 dB) por trecho");
33
+ if (!numero(a.lufs_alvo, -40, -5) || !numero(a.pico_dbtp, -12, 0) || !numero(a.passa_alta_hz ?? 70, 0, 500) || !numero(a.reducao_ruido_db ?? 0, 0, 97)) {
34
+ throw erro("audio: lufs_alvo (−40 a −5), pico_dbtp (−12 a 0), passa_alta_hz (0 a 500), reducao_ruido_db (0 a 97)");
35
+ }
36
+ if (a.pico_final_dbtp != null && !numero(a.pico_final_dbtp, -12, 0)) throw erro("audio.pico_final_dbtp: −12 a 0");
37
+ return {de: a.de, para: a.para, trechos: t.map(([x, y]) => [x, y]), ganhos_db: [...g], lufs_alvo: a.lufs_alvo, pico_dbtp: a.pico_dbtp,
38
+ passa_alta_hz: a.passa_alta_hz ?? 70, reducao_ruido_db: a.reducao_ruido_db ?? 0, voz_gerada: Boolean(a.voz_gerada),
39
+ pico_final_dbtp: a.pico_final_dbtp ?? null};
40
+ }
41
+
42
+ /** arquivos do job que são o vídeo original da Edição Simples (plat/src/motor/job-edicao.js, NOME_VIDEO) */
43
+ const NOMES_NORMALIZAVEIS = ["original"];
44
+
45
+ /** quantas vezes um job tenta renderizar quando a falha é passageira (do compositor, não do código do vídeo) */
46
+ const TENTATIVAS = 2;
47
+ /** erros do compositor do Remotion que não dependem do código nem dos arquivos: vale tentar de novo */
48
+ const falhaPassageira = (e) => /No frame found at position|Compositor exited|Compositor quit|Request closed/i.test(String((e && e.message) || e));
11
49
 
12
50
  /** valida o corpo do POST /v1/jobs; devolve a especificação limpa ou lança {status, message} */
13
51
  function validarJob(d, cache) {
14
52
  const erro = (m, status = 400) => Object.assign(new Error(m), {status});
15
53
  if (!d || typeof d !== "object") throw erro("corpo inválido");
16
- if (!PRESETS[d.preset]) throw erro("preset precisa ser \"rascunho\" ou \"final\"");
54
+ if (!Object.prototype.hasOwnProperty.call(PRESETS, d.preset)) throw erro(`preset precisa ser um de: ${Object.keys(PRESETS).join(", ")}`);
17
55
  const c = d.composicao || {};
18
56
  if (!inteiro(c.largura, 16, LIMITES.ladoMax) || !inteiro(c.altura, 16, LIMITES.ladoMax)) throw erro("largura/altura inválidas");
19
57
  if (!inteiro(c.fps, 1, 120)) throw erro("fps inválido");
@@ -30,19 +68,33 @@ function validarJob(d, cache) {
30
68
  if (!cache.existe(sha)) faltam.push(nome);
31
69
  }
32
70
  if (faltam.length) throw erro(`arquivos ainda não enviados: ${faltam.join(", ")}`, 409);
33
- return {nome: sanitizarNome(d.nome), preset: d.preset, composicao: {largura: c.largura, altura: c.altura, fps: c.fps, duracaoEmQuadros: c.duracaoEmQuadros}, codigo: d.codigo, props, arquivos};
71
+ const audio = validarAudio(d.audio, arquivos, erro);
72
+ return {nome: sanitizarNome(d.nome), preset: d.preset, composicao: {largura: c.largura, altura: c.altura, fps: c.fps, duracaoEmQuadros: c.duracaoEmQuadros}, codigo: d.codigo, props, arquivos: {...arquivos}, audio};
73
+ }
74
+
75
+ /**
76
+ * cache de quadros do <OffthreadVideo>: o padrão do Remotion é metade da memória LIVRE na hora do render; com a
77
+ * máquina ocupada isso fica em centenas de MB, os quadros saem do cache logo depois de extraídos e o render da
78
+ * Edição (vários trechos do mesmo original) falha com "No frame found at position". 15% da RAM, entre 1 e 4 GB.
79
+ */
80
+ function cacheDeQuadros() {
81
+ return Math.round(Math.min(4 * 2 ** 30, Math.max(2 ** 30, os.totalmem() * 0.15)));
34
82
  }
35
83
 
36
84
  /** quantas abas do Chromium renderizam em paralelo: metade dos núcleos, limitado pela RAM (~1 GB por aba) e a 8 */
37
85
  function concorrencia() {
86
+ const fixa = Number(process.env.VIBE_RENDER_CONCORRENCIA);
87
+ if (Number.isInteger(fixa) && fixa >= 1 && fixa <= 16) return fixa;
38
88
  const nucleos = os.cpus().length || 2;
39
89
  const porRam = Math.floor(os.totalmem() / 2 ** 30 - 3);
40
90
  return Math.max(1, Math.min(Math.floor(nucleos / 2) || 1, porRam, 8));
41
91
  }
42
92
 
43
- /** escala para o lado curto do preset (nunca aumenta) e mantém dimensões pares (yuv420p exige) */
93
+ /** escala para o lado curto do preset (só 2K/4K aumentam) e mantém dimensões pares (yuv420p exige) */
44
94
  function escalaDo(preset, largura, altura) {
45
- let s = Math.min(1, PRESETS[preset].ladoCurto / Math.min(largura, altura));
95
+ const p = PRESETS[preset];
96
+ let s = p.ladoCurto / Math.min(largura, altura);
97
+ if (!p.aumenta) s = Math.min(1, s);
46
98
  for (let i = 0; i < 50; i++) {
47
99
  const w = Math.round(largura * s), h = Math.round(altura * s);
48
100
  if (w % 2 === 0 && h % 2 === 0) return s;
@@ -61,18 +113,25 @@ function nomeSaida(dir, nome) {
61
113
  }
62
114
 
63
115
  /**
64
- * opc: {cache, hostDir, navegador, binarios, dirSaida, dirTmp, urlAtivo(jobId, sha, token), licenseKey, renderer?}
65
- * renderer é injetável nos testes; por padrão, @remotion/renderer.
116
+ * opc: {cache, hostDir, navegador, binarios, dirSaida, dirTmp, dirDados, urlAtivo(jobId, sha, token), licenseKey, renderer?, audio?,
117
+ * normalizar?, env?}
118
+ * renderer, audio ({garantirFfmpeg, tratarFala, limitarPico}) e normalizar ({garantirProxy, FPS}) são injetáveis nos
119
+ * testes; por padrão, @remotion/renderer, core/ffmpeg.js + core/audio.js e core/normalizar.js.
66
120
  */
67
121
  function criarJobs(opc) {
68
122
  const R = opc.renderer || require("@remotion/renderer");
123
+ const A = opc.audio || {...require("./ffmpeg"), ...require("./audio")};
124
+ const N = opc.normalizar || require("./normalizar");
69
125
  const jobs = new Map();
70
126
  const ouvintes = new Map(); // id → Set(fn)
71
127
  const fila = [];
72
128
  let rodando = null;
73
129
 
74
130
  const visao = (j) => ({id: j.id, nome: j.nome, preset: j.preset, status: j.status, progresso: j.progresso, etapa: j.etapa, erro: j.erro,
75
- arquivo: j.saida && j.status === "pronto" ? {nome: path.basename(j.saida), bytes: j.bytes} : null, tempoMs: j.tempoMs});
131
+ arquivo: j.saida && j.status === "pronto" ? {nome: path.basename(j.saida), bytes: j.bytes} : null, tempoMs: j.tempoMs,
132
+ tentativas: j.tentativas || 1,
133
+ ...(j.proxy ? {proxy: j.proxy} : {}),
134
+ ...(j.medicaoAudio ? {audio: {normalizacao: j.medicaoAudio.normalizacao, loudnorm_medido: j.medicaoAudio.loudnorm_medido, pico: j.pico || null}} : {})});
76
135
  function mudar(j, patch) {
77
136
  Object.assign(j, patch);
78
137
  for (const fn of ouvintes.get(j.id) || []) fn(visao(j));
@@ -88,6 +147,50 @@ function criarJobs(opc) {
88
147
  return visao(j);
89
148
  }
90
149
 
150
+ /**
151
+ * Troca o vídeo original (NOMES_NORMALIZAVEIS) pelo proxy de 30 fps / GOP curto (core/normalizar.js), feito uma
152
+ * vez por sha256 e reaproveitado nos próximos jobs. Só com composição a 30 fps (o proxy é 30 fps constantes).
153
+ * O áudio da fala já foi tratado a partir do original; aqui só muda o que o Chromium lê. Se a normalização
154
+ * falhar (sem ser cancelamento), o job segue com o original, como antes.
155
+ */
156
+ async function normalizarOriginais(j, ffmpeg, cancelRender) {
157
+ if ((opc.env || process.env).VIBE_RENDER_NORMALIZAR === "0" || j.composicao.fps !== N.FPS) return ffmpeg;
158
+ const nomes = NOMES_NORMALIZAVEIS.filter((n) => Object.prototype.hasOwnProperty.call(j.arquivos, n));
159
+ for (const nome of nomes) {
160
+ const sha = j.arquivos[nome];
161
+ const ab = new AbortController();
162
+ j.cancelar = () => ab.abort();
163
+ const t0 = Date.now();
164
+ try {
165
+ if (!ffmpeg) {
166
+ mudar(j, {status: "preparando", etapa: "Preparando o ffmpeg"});
167
+ ffmpeg = await A.garantirFfmpeg({dirDados: opc.dirDados, onProgresso: (p) => mudar(j, {etapa: `Baixando o ffmpeg (uma vez só) ${Math.round(p * 100)}%`})});
168
+ }
169
+ if (j.status === "cancelado") throw new Error("cancelado");
170
+ const r = await N.garantirProxy(sha, {cache: opc.cache, ffmpeg: ffmpeg.caminho, dirTmp: opc.dirTmp, sinal: ab.signal, env: opc.env || process.env,
171
+ aoSondar: () => mudar(j, {status: "preparando", etapa: "Conferindo o vídeo original"}),
172
+ onProgresso: (p) => {
173
+ const etapa = `Preparando o vídeo original (uma vez só) ${Math.round(p * 100)}%`;
174
+ if (etapa !== j.etapa) mudar(j, {etapa});
175
+ }});
176
+ j.proxy = {...(j.proxy || {}), [nome]: {usado: r.proxy, novo: r.novo, motivos: r.motivos, plano: r.plano || null,
177
+ segundos: r.novo ? Math.round((Date.now() - t0) / 100) / 10 : 0}};
178
+ if (r.proxy) {
179
+ j.originais = {...(j.originais || {}), [nome]: sha};
180
+ j.arquivos[nome] = r.sha; // o servidor só entrega ao Chromium os sha256 que estão em j.arquivos
181
+ opc.cache.tocar(r.sha);
182
+ }
183
+ } catch (e) {
184
+ if (j.status === "cancelado" || ab.signal.aborted) throw new Error("cancelado");
185
+ j.proxy = {...(j.proxy || {}), [nome]: {usado: false, erro: String((e && e.message) || e).slice(0, 500)}};
186
+ } finally {
187
+ j.cancelar = cancelRender;
188
+ }
189
+ }
190
+ if (nomes.length) mudar(j, {etapa: "Abrindo o navegador de render"});
191
+ return ffmpeg;
192
+ }
193
+
91
194
  async function proximo() {
92
195
  if (rodando || !fila.length) return;
93
196
  const j = fila.shift();
@@ -101,26 +204,68 @@ function criarJobs(opc) {
101
204
  try {
102
205
  mudar(j, {status: "preparando", etapa: "Abrindo o navegador de render"});
103
206
  for (const sha of Object.values(j.arquivos)) opc.cache.tocar(sha);
207
+ let ffmpeg = null;
208
+ if (j.audio) {
209
+ mudar(j, {status: "preparando", etapa: "Preparando o ffmpeg"});
210
+ ffmpeg = await A.garantirFfmpeg({dirDados: opc.dirDados, onProgresso: (p) => mudar(j, {etapa: `Baixando o ffmpeg (uma vez só) ${Math.round(p * 100)}%`})});
211
+ if (j.status === "cancelado") throw new Error("cancelado");
212
+ mudar(j, {etapa: "Tratando o áudio da fala"});
213
+ const wav = path.join(opc.dirTmp, `${j.id}-fala.wav`);
214
+ fs.mkdirSync(opc.dirTmp, {recursive: true});
215
+ const ab = new AbortController();
216
+ j.cancelar = () => ab.abort();
217
+ j.medicaoAudio = await A.tratarFala(opc.cache.caminho(j.arquivos[j.audio.de]), wav, j.audio, {ffmpeg: ffmpeg.caminho, versao: ffmpeg.versao, sinal: ab.signal, dirTmp: opc.dirTmp});
218
+ j.arquivos[j.audio.para] = await opc.cache.adotar(wav);
219
+ j.cancelar = cancel;
220
+ if (j.status === "cancelado") throw new Error("cancelado");
221
+ mudar(j, {etapa: "Abrindo o navegador de render"});
222
+ }
223
+ ffmpeg = await normalizarOriginais(j, ffmpeg, cancel);
104
224
  const ativos = Object.fromEntries(Object.entries(j.arquivos).map(([nome, sha]) => [nome, opc.urlAtivo(j.id, sha, j.token)]));
105
225
  const inputProps = {codigo: j.codigo, props: j.props, ativos, composicao: j.composicao};
106
226
  const comum = {browserExecutable: opc.navegador || null, binariesDirectory: opc.binarios || null, chromeMode: "headless-shell", logLevel: "warn"};
107
- browser = await R.openBrowser("chrome", {browserExecutable: comum.browserExecutable, chromeMode: "headless-shell", logLevel: "warn"});
108
- const composition = await R.selectComposition({...comum, serveUrl: opc.hostDir, id: "VibeHost", inputProps, puppeteerInstance: browser});
109
227
  const p = PRESETS[j.preset];
110
228
  fs.mkdirSync(opc.dirTmp, {recursive: true});
111
- mudar(j, {status: "renderizando", etapa: "Renderizando quadros"});
112
- await R.renderMedia({
113
- ...comum, composition, serveUrl: opc.hostDir, inputProps, puppeteerInstance: browser,
114
- codec: "h264", outputLocation: tmp, overwrite: true,
115
- scale: escalaDo(j.preset, composition.width, composition.height),
116
- crf: p.crf, x264Preset: p.x264Preset, imageFormat: "jpeg", jpegQuality: p.jpegQuality,
117
- concurrency: concorrencia(), cancelSignal, timeoutInMilliseconds: 120000,
118
- licenseKey: opc.licenseKey || null,
119
- onProgress: ({progress, stitchStage}) => {
120
- const etapa = stitchStage === "muxing" ? "Juntando áudio e vídeo" : "Renderizando quadros";
121
- if (Math.abs(progress - j.progresso) >= 0.01 || etapa !== j.etapa) mudar(j, {progresso: progress, etapa});
122
- },
123
- });
229
+ for (let tentativa = 1; ; tentativa++) {
230
+ try {
231
+ browser = await R.openBrowser("chrome", {browserExecutable: comum.browserExecutable, chromeMode: "headless-shell", logLevel: "warn"});
232
+ const composition = await R.selectComposition({...comum, serveUrl: opc.hostDir, id: "VibeHost", inputProps, puppeteerInstance: browser});
233
+ mudar(j, {status: "renderizando", etapa: "Renderizando quadros"});
234
+ // na repetição vai com 1 aba: vídeo com GOP longo / fps variável (ex.: WhatsApp, 1 quadro-chave a cada 10 s)
235
+ // com várias abas pedindo pontos distantes do mesmo original faz o compositor "não achar" o quadro
236
+ const abas = tentativa > 1 ? 1 : concorrencia();
237
+ await R.renderMedia({
238
+ ...comum, composition, serveUrl: opc.hostDir, inputProps, puppeteerInstance: browser,
239
+ codec: "h264", outputLocation: tmp, overwrite: true,
240
+ scale: escalaDo(j.preset, composition.width, composition.height),
241
+ crf: p.crf, x264Preset: p.x264Preset, imageFormat: "jpeg", jpegQuality: p.jpegQuality,
242
+ concurrency: abas, cancelSignal, timeoutInMilliseconds: 120000,
243
+ offthreadVideoCacheSizeInBytes: cacheDeQuadros(),
244
+ // o padrão (2) deixa o original 4K HEVC (iPhone) decodificando em 2 threads enquanto as abas esperam; mais
245
+ // threads só com memória livre (cada uma segura quadros 4K; com a máquina apertada o compositor cai)
246
+ offthreadVideoThreads: os.freemem() > 6 * 2 ** 30 ? Math.min(4, Math.max(2, abas)) : 2,
247
+ licenseKey: opc.licenseKey || null,
248
+ onProgress: ({progress, stitchStage}) => {
249
+ const etapa = stitchStage === "muxing" ? "Juntando áudio e vídeo" : abas === 1 && tentativa > 1 ? "Renderizando quadros (1 aba)" : "Renderizando quadros";
250
+ if (Math.abs(progress - j.progresso) >= 0.01 || etapa !== j.etapa) mudar(j, {progresso: progress, etapa});
251
+ },
252
+ });
253
+ break;
254
+ } catch (e) {
255
+ if (j.status === "cancelado" || tentativa >= TENTATIVAS || !falhaPassageira(e)) throw e;
256
+ // o compositor do Remotion às vezes "não acha" um quadro que existe no vídeo-base (<OffthreadVideo>), num
257
+ // ponto diferente a cada render; acontece também com o bundle do editor local, então não é do host.
258
+ // Recomeça do zero com navegador novo e 1 aba (docs/M1-REFERENCIAS.md, docs/M2-MOTOR-ETAPA1.md)
259
+ if (browser) await browser.close({silent: true}).catch(() => {});
260
+ browser = null;
261
+ j.tentativas = tentativa + 1;
262
+ mudar(j, {status: "preparando", progresso: 0, etapa: "Renderizando de novo, com 1 aba (vídeo difícil de buscar)"});
263
+ }
264
+ }
265
+ if (j.audio && j.audio.pico_final_dbtp != null) {
266
+ mudar(j, {etapa: "Conferindo o pico do áudio"});
267
+ j.pico = await A.limitarPico(tmp, j.audio.pico_final_dbtp, {ffmpeg: ffmpeg.caminho});
268
+ }
124
269
  const dirSaida = typeof opc.dirSaida === "function" ? opc.dirSaida() : opc.dirSaida;
125
270
  fs.mkdirSync(dirSaida, {recursive: true});
126
271
  const saida = nomeSaida(dirSaida, j.nome);
@@ -159,7 +304,7 @@ function criarJobs(opc) {
159
304
  }
160
305
  const ativos = () => [...jobs.values()].filter((j) => ["fila", "preparando", "renderizando"].includes(j.status)).map(visao);
161
306
 
162
- return {criar, obter, interno, cancelar, ouvir, ativos, emUso: () => new Set([...jobs.values()].filter((j) => j.status !== "pronto").flatMap((j) => Object.values(j.arquivos || {})))};
307
+ return {criar, obter, interno, cancelar, ouvir, ativos, emUso: () => new Set([...jobs.values()].filter((j) => j.status !== "pronto").flatMap((j) => [...Object.values(j.arquivos || {}), ...Object.values(j.originais || {})]))};
163
308
  }
164
309
 
165
- module.exports = {criarJobs, validarJob, escalaDo, concorrencia, nomeSaida};
310
+ module.exports = {criarJobs, validarJob, escalaDo, concorrencia, nomeSaida, falhaPassageira, TENTATIVAS, NOMES_NORMALIZAVEIS};