// supersep.ts — SuperSep stem separation route (proxy to ace-server) // // Routes: // POST /api/supersep/separate — start separation (reads file from disk) // GET /api/supersep/:jobId/progress — poll job progress // GET /api/supersep/:jobId/result — get stem list metadata // GET /api/supersep/:jobId/stem/:index — download individual stem WAV // POST /api/supersep/recombine — remix stems with volume/mute controls import { Router } from 'express'; import { config } from '../config.js'; import { ensureEngineFormat } from '../services/audioConvert.js'; import path from 'path'; const router = Router(); const ACE_URL = config.aceServer.url; // POST /api/supersep/separate // Body (JSON): { audioUrl: "/references/uuid.flac" } // Query: level=0..5 (BASIC/VOCAL_SPLIT/FULL/MAXIMUM/VOCALS_ONLY/STABLESTEP) // 4 = 2-stem via the 6-stem BS-RoFormer (instrumental = mix − vocals) // 5 = 2-stem via the dual BS-Roformer-Leap Xe models (both stems neural) // See SuperSepLevel in engine/src/supersep.h. // Reads the file from disk, converts non-WAV/MP3 to WAV, forwards to ace-server. router.post('/separate', async (req, res) => { try { const level = parseInt(String(req.query.level ?? '0'), 10); const { audioUrl } = req.body || {}; if (!audioUrl || typeof audioUrl !== 'string') { return res.status(400).json({ error: 'audioUrl required in request body' }); } // Resolve server-side file path from URL // audioUrl is like "/references/uuid.flac" → data/references/uuid.flac const basename = path.basename(audioUrl); let filePath: string; if (audioUrl.startsWith('/references/')) { filePath = path.join(config.data.dir, 'references', basename); } else if (audioUrl.startsWith('/audio/')) { filePath = path.join(config.data.audioDir, basename); } else { // Fallback: try in references filePath = path.join(config.data.dir, 'references', basename); } console.log(`[SuperSep] separate: level=${level}, file=${filePath}`); // Read and convert to engine-compatible format (WAV/MP3) let audioBody: Buffer; try { audioBody = ensureEngineFormat(filePath); } catch (err: any) { console.error(`[SuperSep] Format conversion failed:`, err.message); return res.status(400).json({ error: `Audio conversion failed: ${err.message}` }); } console.log(`[SuperSep] Forwarding ${audioBody.length} bytes to ace-server`); // Forward WAV/MP3 body to ace-server const aceRes = await fetch( `${ACE_URL}/supersep/separate?level=${level}`, { method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: audioBody, } ); if (!aceRes.ok) { const err = await aceRes.text(); console.error(`[SuperSep] ace-server returned ${aceRes.status}: ${err}`); return res.status(aceRes.status).json({ error: err }); } const data = await aceRes.json(); res.json(data); } catch (err: any) { console.error('[SuperSep] separate error:', err.message); res.status(500).json({ error: err.message }); } }); // GET /api/supersep/:jobId/progress router.get('/:jobId/progress', async (req, res) => { try { const aceRes = await fetch( `${ACE_URL}/supersep/progress?id=${req.params.jobId}` ); const data = await aceRes.json(); res.json(data); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // GET /api/supersep/:jobId/result router.get('/:jobId/result', async (req, res) => { try { const aceRes = await fetch( `${ACE_URL}/supersep/result?id=${req.params.jobId}` ); if (!aceRes.ok) { return res.status(aceRes.status).json(await aceRes.json()); } const data = await aceRes.json(); res.json(data); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // GET /api/supersep/:jobId/stem/:index — proxy WAV download router.get('/:jobId/stem/:index', async (req, res) => { try { const aceRes = await fetch( `${ACE_URL}/supersep/serve?id=${req.params.jobId}&stem=${req.params.index}` ); if (!aceRes.ok) { return res.status(aceRes.status).json({ error: 'Failed to fetch stem' }); } const buf = Buffer.from(await aceRes.arrayBuffer()); res.set('Content-Type', 'audio/wav'); res.set('Content-Disposition', `attachment; filename="stem_${req.params.index}.wav"`); res.send(buf); } catch (err: any) { res.status(500).json({ error: err.message }); } }); // POST /api/supersep/recombine — remix stems and return WAV router.post('/recombine', async (req, res) => { try { const aceRes = await fetch(`${ACE_URL}/supersep/recombine`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(req.body), }); if (!aceRes.ok) { return res.status(aceRes.status).json(await aceRes.json()); } const buf = Buffer.from(await aceRes.arrayBuffer()); res.set('Content-Type', 'audio/wav'); res.send(buf); } catch (err: any) { res.status(500).json({ error: err.message }); } }); export default router;