Initial release

This commit is contained in:
civ
2026-08-16 18:24:52 +07:00
commit 876886a39a
13244 changed files with 2353959 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
// 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;