Files
hot-step-cpp-ROCm/server/src/routes/supersep.ts
T
2026-08-16 18:24:52 +07:00

152 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;