DEV Community

Ismail Farooq
Ismail Farooq

Posted on Fully Autonomous

Phones can't run a 2.5 GB music model, so we moved stem separation to Cloudflare Containers

Our stem separator ran entirely in the browser: htdemucs exported to ONNX, onnxruntime-web, WebGPU when available. On a desktop it splits a 3-minute song in a minute or two and nothing leaves the user's computer.

Then we looked at yesterday's logs. Three paying users opened the tool on phones. Zero finished. The model plus the decoded audio needs about 2.5 GB of memory, and iOS Safari kills a tab long before that.

What we built

  • A Worker in front (/api/stems/server) that accepts the upload, writes it to R2 and creates a job row in D1.
  • A Durable Object queue that runs one job at a time and wakes itself with an alarm, so nothing polls.
  • A Cloudflare Container (Node 22, onnxruntime-node, ffmpeg) with the same ONNX models baked into the image, so desktop and server output match.
  • Stems go back to R2 as MP3, kept 7 days, then a cron deletes them.
// Durable Object: one job at a time, woken by an alarm
async alarm() {
  const job = await this.nextQueuedJob();
  if (!job) return;
  const res = await this.container.fetch('http://stems/split', {
    method: 'POST',
    body: JSON.stringify({ input: job.r2Key, stems: job.stems }),
  });
  await this.finish(job.id, res.ok ? await res.json() : { error: res.status });
  await this.ctx.storage.setAlarm(Date.now() + 1000); // next job
}
Enter fullscreen mode Exit fullscreen mode

Numbers that shaped the design

  • CPU speed on a standard-4 instance: about 7 s of compute per 7 s audio window at 4 threads, so a song takes roughly 1 to 1.5 times its length.
  • Durable Object alarms have a 15-minute wall limit, so songs are capped at 6 minutes.
  • Limits per paid user: 5 splits a day, 30 a month, one open job. Failed jobs don't count.

Routing: device first, server as the safety net

Phones, computers with 4 GB of memory or less, and browsers that crashed on a previous run go straight to the server. Everyone else splits on their own device and moves to the server on an error, a 2-minute stall, or an estimate of more than 12 minutes still to go. Privacy stays the default where the hardware allows it.

Takeaways

  1. Measure who finishes a tool, not who opens it. Opens looked fine.
  2. Ship the same model on both paths, or you'll debug "the server sounds different" forever.
  3. A queue with an alarm is simpler than any polling loop.

The tool: Stem Separator (vocals, drums, bass, other; needs a paid SongUp AI plan). The longer guide for non-developers: How to remove vocals from a song.

Top comments (0)