CreatorKit Claude Opus 4.8 commited on
Commit
f00aab1
Β·
1 Parent(s): 2380c62

fix: estabilidade em instancias com pouca memoria (ex.: Render free)

Browse files

A transcricao (Whisper) consome muita RAM e estourava o limite de
512MB do plano gratuito, causando reinicios.

- ENABLE_TRANSCRIPTION=false desliga so a transcricao; as 6 ferramentas
de texto seguem funcionando normalmente (leves).
- Limite de 1 transcricao simultanea (MAX_CONCURRENT) evita picos de RAM
por jobs paralelos; excedente recebe 429 com mensagem clara.
- Endpoint /config informa ao front se a transcricao esta ativa; quando
desativada, a UI mostra aviso e desabilita o botao.
- Default de modelo no backend tambem passa a tiny.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (2) hide show
  1. server.js +28 -4
  2. templates/index.html +13 -0
server.js CHANGED
@@ -21,6 +21,12 @@ const JOB_TTL_MS = 60 * 60 * 1000; // 1h: jobs e arquivos temporΓ‘rios expira
21
  const DL_TIMEOUT_MS = 3 * 60 * 1000; // 3min: timeout do download (yt-dlp)
22
  const TR_TIMEOUT_MS = 10 * 60 * 1000; // 10min: timeout da transcriΓ§Γ£o (Whisper)
23
 
 
 
 
 
 
 
24
  [DOWNLOADS_DIR, UPLOADS_DIR, path.join(__dirname, 'bin')].forEach(d => fs.mkdirSync(d, { recursive: true }));
25
 
26
  // ── App ───────────────────────────────────────────────────────
@@ -198,7 +204,7 @@ async function runUrlJob(jobId, url, model, language, task) {
198
  jobs[jobId].status = 'transcribing';
199
  finishJob(jobId, await transcribeAudio(mp3, model, language, task));
200
  } catch (e) { failJob(jobId, e.message); }
201
- finally { if (mp3 && fs.existsSync(mp3)) fs.unlink(mp3, () => {}); }
202
  }
203
 
204
  async function runFileJob(jobId, filePath, model, language, task) {
@@ -212,12 +218,26 @@ async function runFileJob(jobId, filePath, model, language, task) {
212
  } catch (e) {
213
  failJob(jobId, e.message);
214
  if (filePath && fs.existsSync(filePath)) fs.unlink(filePath, () => {});
215
- } finally { if (mp3 && fs.existsSync(mp3)) fs.unlink(mp3, () => {}); }
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  }
217
 
218
  // ── Routes ────────────────────────────────────────────────────
219
  app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'templates', 'index.html')));
220
  app.get('/health', (req, res) => res.json({ ok: true, uptime: process.uptime() }));
 
221
 
222
  app.post('/info', async (req, res) => {
223
  const { url } = req.body;
@@ -227,8 +247,10 @@ app.post('/info', async (req, res) => {
227
  });
228
 
229
  app.post('/transcribe', (req, res) => {
230
- const { url, model = 'Xenova/whisper-small', language = 'auto', task = 'transcribe' } = req.body;
231
  if (!url) return res.status(400).json({ error: 'URL nΓ£o informada' });
 
 
232
  const jobId = newJob();
233
  runUrlJob(jobId, url, model, language, task);
234
  res.json({ job_id: jobId });
@@ -236,7 +258,9 @@ app.post('/transcribe', (req, res) => {
236
 
237
  app.post('/transcribe-file', upload.single('file'), (req, res) => {
238
  if (!req.file) return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
239
- const { model = 'Xenova/whisper-small', language = 'auto', task = 'transcribe' } = req.body;
 
 
240
  const jobId = newJob();
241
  runFileJob(jobId, req.file.path, model, language, task);
242
  res.json({ job_id: jobId });
 
21
  const DL_TIMEOUT_MS = 3 * 60 * 1000; // 3min: timeout do download (yt-dlp)
22
  const TR_TIMEOUT_MS = 10 * 60 * 1000; // 10min: timeout da transcriΓ§Γ£o (Whisper)
23
 
24
+ // TranscriΓ§Γ£o Γ© pesada de memΓ³ria (Whisper). Em instΓ’ncias pequenas (ex.: Render
25
+ // free, 512MB) pode estourar a RAM. Permite desligar o recurso e limitar a 1 por vez.
26
+ const ENABLE_TRANSCRIPTION = process.env.ENABLE_TRANSCRIPTION !== 'false';
27
+ const MAX_CONCURRENT = Number(process.env.MAX_CONCURRENT || 1);
28
+ let activeJobs = 0;
29
+
30
  [DOWNLOADS_DIR, UPLOADS_DIR, path.join(__dirname, 'bin')].forEach(d => fs.mkdirSync(d, { recursive: true }));
31
 
32
  // ── App ───────────────────────────────────────────────────────
 
204
  jobs[jobId].status = 'transcribing';
205
  finishJob(jobId, await transcribeAudio(mp3, model, language, task));
206
  } catch (e) { failJob(jobId, e.message); }
207
+ finally { activeJobs--; if (mp3 && fs.existsSync(mp3)) fs.unlink(mp3, () => {}); }
208
  }
209
 
210
  async function runFileJob(jobId, filePath, model, language, task) {
 
218
  } catch (e) {
219
  failJob(jobId, e.message);
220
  if (filePath && fs.existsSync(filePath)) fs.unlink(filePath, () => {});
221
+ } finally { activeJobs--; if (mp3 && fs.existsSync(mp3)) fs.unlink(mp3, () => {}); }
222
+ }
223
+
224
+ /** Verifica se dΓ‘ pra aceitar uma nova transcriΓ§Γ£o agora */
225
+ function transcriptionGate(res) {
226
+ if (!ENABLE_TRANSCRIPTION) {
227
+ res.status(503).json({ error: 'A transcriΓ§Γ£o de vΓ­deo estΓ‘ desativada neste servidor (memΓ³ria limitada). As outras ferramentas funcionam normalmente β€” para transcrever, rode o app localmente.' });
228
+ return false;
229
+ }
230
+ if (activeJobs >= MAX_CONCURRENT) {
231
+ res.status(429).json({ error: 'O servidor jΓ‘ estΓ‘ transcrevendo outro vΓ­deo. Aguarde alguns instantes e tente novamente.' });
232
+ return false;
233
+ }
234
+ return true;
235
  }
236
 
237
  // ── Routes ────────────────────────────────────────────────────
238
  app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'templates', 'index.html')));
239
  app.get('/health', (req, res) => res.json({ ok: true, uptime: process.uptime() }));
240
+ app.get('/config', (req, res) => res.json({ transcription: ENABLE_TRANSCRIPTION }));
241
 
242
  app.post('/info', async (req, res) => {
243
  const { url } = req.body;
 
247
  });
248
 
249
  app.post('/transcribe', (req, res) => {
250
+ const { url, model = 'Xenova/whisper-tiny', language = 'auto', task = 'transcribe' } = req.body;
251
  if (!url) return res.status(400).json({ error: 'URL nΓ£o informada' });
252
+ if (!transcriptionGate(res)) return;
253
+ activeJobs++;
254
  const jobId = newJob();
255
  runUrlJob(jobId, url, model, language, task);
256
  res.json({ job_id: jobId });
 
258
 
259
  app.post('/transcribe-file', upload.single('file'), (req, res) => {
260
  if (!req.file) return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
261
+ if (!transcriptionGate(res)) { fs.unlink(req.file.path, () => {}); return; }
262
+ activeJobs++;
263
+ const { model = 'Xenova/whisper-tiny', language = 'auto', task = 'transcribe' } = req.body;
264
  const jobId = newJob();
265
  runFileJob(jobId, req.file.path, model, language, task);
266
  res.json({ job_id: jobId });
templates/index.html CHANGED
@@ -543,6 +543,7 @@ mark{background:#fde68a;border-radius:2px;padding:0 1px}
543
  <div class="itab" data-task="translate">🌍 Traduzir β†’ EN</div>
544
  </div>
545
  </div>
 
546
  <button class="run-btn" id="proc-btn" style="margin-top:16px">⚑ Processar vΓ­deo</button>
547
  <div class="pg" id="pg">
548
  <div class="steps">
@@ -952,6 +953,18 @@ G('cal-btn').addEventListener('click',async()=>{
952
  G('cal-cpy').onclick=()=>cpTxt(d.plan.map(p=>`${p.day}: ${p.theme} [${p.fmt}] β€” ${p.best}`).join('\n'),G('cal-cpy'),'πŸ“‹ Copiar');
953
  }catch(e){alert(e.message);}b.disabled=false;b.textContent='πŸ“… Gerar calendΓ‘rio';
954
  });
 
 
 
 
 
 
 
 
 
 
 
 
955
  </script>
956
  </body>
957
  </html>
 
543
  <div class="itab" data-task="translate">🌍 Traduzir β†’ EN</div>
544
  </div>
545
  </div>
546
+ <div id="transc-off" class="hint" style="display:none;background:#fff7ed;border-color:#fed7aa;color:#9a3412">⚠️ <b>Transcrição indisponível neste servidor</b> (memória limitada do plano gratuito). As demais ferramentas funcionam normalmente. Para transcrever vídeos, rode o app no seu computador.</div>
547
  <button class="run-btn" id="proc-btn" style="margin-top:16px">⚑ Processar vΓ­deo</button>
548
  <div class="pg" id="pg">
549
  <div class="steps">
 
953
  G('cal-cpy').onclick=()=>cpTxt(d.plan.map(p=>`${p.day}: ${p.theme} [${p.fmt}] β€” ${p.best}`).join('\n'),G('cal-cpy'),'πŸ“‹ Copiar');
954
  }catch(e){alert(e.message);}b.disabled=false;b.textContent='πŸ“… Gerar calendΓ‘rio';
955
  });
956
+
957
+ // ════════════════════ CONFIG (transcriΓ§Γ£o on/off) ════════════════════
958
+ (async()=>{
959
+ try{
960
+ const cfg=await(await fetch('/config')).json();
961
+ if(cfg && cfg.transcription===false){
962
+ G('transc-off').style.display='block';
963
+ const pb=G('proc-btn'); pb.disabled=true; pb.textContent='TranscriΓ§Γ£o indisponΓ­vel neste servidor';
964
+ G('demo-btn').style.display='none';
965
+ }
966
+ }catch{}
967
+ })();
968
  </script>
969
  </body>
970
  </html>