File size: 4,361 Bytes
ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 b6bdea1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 13ccee2 ddc46a1 b6bdea1 ddc46a1 13ccee2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | """Bytley Demucs stem-separation backend (Hugging Face Space, free CPU tier).
POST /separate (multipart: file; query: mode=stems|karaoke)
→ application/zip of MP3 stems:
mode=stems → vocals.mp3, drums.mp3, bass.mp3, other.mp3
mode=karaoke → instrumental.mp3, vocals.mp3
Uses the Demucs CLI (stable across versions) with the htdemucs model baked into the
image. Files are processed in a temp dir and deleted right after the response.
"""
import io
import os
import subprocess
import sys
import tempfile
import zipfile
from fastapi import FastAPI, File, Header, HTTPException, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import Response
MAX_MB = int(os.environ.get("MAX_MB", "30"))
TOKEN = os.environ.get("BYTLEY_TOKEN", "")
app = FastAPI(title="bytley-demucs")
app.add_middleware(
CORSMiddleware,
allow_origins=[
"https://bytley.co",
"https://www.bytley.co",
"http://localhost:4321",
"http://localhost:4322",
],
allow_methods=["GET", "POST"],
allow_headers=["*"],
)
@app.get("/")
def health():
return {"ok": True, "service": "demucs-stems", "model": "htdemucs", "maxMb": MAX_MB}
def _run(cmd: list[str], timeout: int):
r = subprocess.run(cmd, capture_output=True, timeout=timeout)
if r.returncode != 0:
stderr_tail = r.stderr.decode(errors="replace")[-800:]
raise HTTPException(
500,
detail=f"Separation failed. stderr tail: {stderr_tail}",
)
@app.post("/separate")
async def separate(
file: UploadFile = File(...),
mode: str = "stems",
x_bytley_token: str | None = Header(default=None),
):
if TOKEN and x_bytley_token != TOKEN:
raise HTTPException(401, detail="Unauthorized.")
data = await file.read()
if len(data) > MAX_MB * 1024 * 1024:
raise HTTPException(413, detail=f"File too large — max {MAX_MB} MB.")
if len(data) < 1024:
raise HTTPException(400, detail="Empty file.")
with tempfile.TemporaryDirectory() as td:
ext = os.path.splitext(file.filename or "a.mp3")[1] or ".mp3"
src = os.path.join(td, "input" + ext)
wav = os.path.join(td, "track.wav")
with open(src, "wb") as f:
f.write(data)
# Normalize to 44.1 kHz stereo WAV (also validates the upload).
r = subprocess.run(
["ffmpeg", "-y", "-i", src, "-vn", "-ac", "2", "-ar", "44100", "-t", "600", "-c:a", "pcm_s16le", wav],
capture_output=True,
timeout=300,
)
if r.returncode != 0 or not os.path.exists(wav):
raise HTTPException(400, detail="Couldn't read audio from this file.")
# Demucs CLI — stable across package versions.
outdir = os.path.join(td, "sep")
# demucs v4 entry point is `python -m demucs`, NOT `python -m demucs.separate`
cmd = [sys.executable, "-m", "demucs", "-n", "htdemucs", "-d", "cpu", "-o", outdir]
if mode == "karaoke":
cmd += ["--two-stems", "vocals"]
cmd.append(wav)
_run(cmd, timeout=1700)
stemdir = os.path.join(outdir, "htdemucs", "track")
if not os.path.isdir(stemdir):
raise HTTPException(500, detail="Separation produced no output.")
names = ["instrumental", "vocals"] if mode == "karaoke" else ["vocals", "drums", "bass", "other"]
src_names = {"instrumental": "no_vocals"} # demucs names the karaoke stem no_vocals
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_STORED) as z:
for name in names:
wpath = os.path.join(stemdir, f"{src_names.get(name, name)}.wav")
if not os.path.exists(wpath):
raise HTTPException(500, detail=f"Missing stem: {name}.")
mpath = os.path.join(td, f"{name}.mp3")
subprocess.run(
["ffmpeg", "-y", "-i", wpath, "-c:a", "libmp3lame", "-b:a", "160k", mpath],
capture_output=True,
timeout=300,
check=True,
)
z.write(mpath, arcname=f"{name}.mp3")
return Response(
content=buf.getvalue(),
media_type="application/zip",
headers={"Content-Disposition": 'attachment; filename="stems.zip"'},
)
|