Spaces:
Sleeping
Sleeping
CreatorKit Claude Opus 4.8 commited on
Commit Β·
f00aab1
1
Parent(s): 2380c62
fix: estabilidade em instancias com pouca memoria (ex.: Render free)
Browse filesA 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>
- server.js +28 -4
- 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-
|
| 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 |
-
|
|
|
|
|
|
|
| 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>
|