Take a song (uploaded audio or a YouTube link), separate it into instrument stems, and transcribe each stem into sheet music (PDF), tablature, MIDI/MusicXML, and an interactive piano roll — in the browser.
- Core stems (drums, bass, vocals): solid quality.
- Guitar & piano: best-effort / experimental (polyphonic transcription is hard — they're clearly labelled in the UI and never block the rest of a job).
Per the original request: drums get sheet music; guitar & bass get tabs + sheet music. All pitched stems also get MIDI, MusicXML, and a piano-roll view.
Browser (Next.js / Vercel)
└─ POST file or URL ─► Next.js /api/jobs ─► Modal `web` (FastAPI)
└─ spawns process_job (CPU)
├─ download (yt-dlp + ffmpeg)
├─ separate (Demucs, GPU) ← only GPU step
└─ per stem (CPU, fanned out):
transcribe → notation/tab → upload
Browser polls /api/jobs/:id ◄─ reads job row ◄─ Supabase (jobs table + artifacts bucket)
Only Demucs runs on a GPU; everything else is CPU, which keeps serverless GPU cost to
~$0.02–0.04 per song (see docs/hosting-cost-chart.md).
| Stage | Tool | Notes |
|---|---|---|
| Download | yt-dlp + ffmpeg | file upload is the reliable path; YouTube is best-effort |
| Separation | Demucs htdemucs_6s (MIT) |
6 stems incl. guitar/piano |
| Drums → notes | librosa onset + spectral-band classifier | kick/snare/hi-hat → GM percussion MIDI |
| Bass/vocals → notes | Spotify basic-pitch | strong on isolated monophonic |
| Guitar/piano → notes | MT3 (Modal GPU); basic-pitch fallback | heavier polyphonic model; still experimental |
| MIDI → tab | open-fret (guitar, when weights present) → tab.py assigner fallback |
learned fingering when trained; deterministic ASCII otherwise |
| MIDI → MusicXML → PDF | music21 + LilyPond | headless engraving |
| Browser viewers | OpenSheetMusicDisplay, html-midi-player | sheet music + piano roll |
MT3 runs in its own Modal GPU image (
mt3_imageinmodal_app.py) because its JAX/T5X stack conflicts with the PyTorch/TF base image. The local CLI and the CPU base image have no MT3, so guitar/piano transcribe via basic-pitch there — set inMT3_STEMS(config.py). Guitar tab fingering uses open-fret (pipeline/opentab.py,OPENFRET_STEMS) when a trained checkpoint is present, else the deterministic assigner. Further swappable upgrades documented inline:htdemucs_ftfor core separation, omnizart for drums.
band-transcriber/
modal_app.py # Modal deployment (GPU separate + CPU fan-out + web endpoints)
pipeline/ # the pipeline (importable + locally runnable)
config.py # per-stem behaviour (the single source of truth)
download.py separate.py notation.py tab.py storage.py pipeline.py
transcribe/ # drums.py, melodic.py, dispatch
cli.py # local runner: python -m pipeline.cli ...
tests/ # dependency-light unit tests (tab + config)
apps/web/ # Next.js app (Vercel)
supabase/migrations/ # jobs table + artifacts bucket + TTL cleanup
docs/hosting-cost-chart.md
- Python 3.11 (the Modal image pins 3.11; the heavy ML deps don't yet support 3.13+).
- Node 18+.
- Accounts: Modal, Supabase, Vercel.
# Apply the migration (via the Supabase SQL editor or CLI):
supabase db push # or paste supabase/migrations/0001_init.sql into the SQL editorThis creates the jobs table and a private artifacts bucket. To enable automatic
TTL cleanup, turn on the pg_cron extension and uncomment the cron.schedule(...) line
at the bottom of the migration.
Grab your project URL and service-role key (Settings → API).
pip install modal && modal token new
# Store Supabase creds as a Modal secret the functions expect:
modal secret create band-transcriber-supabase \
SUPABASE_URL=https://<proj>.supabase.co \
SUPABASE_SERVICE_KEY=<service-role-key> \
ARTIFACT_BUCKET=artifacts
# Optional: route yt-dlp through a residential proxy to dodge cloud-IP blocks
# ...add YTDLP_PROXY=http://user:pass@host:port to the secret above
modal deploy modal_app.pyDeploy prints the web endpoint URL — that's your MODAL_WEB_URL.
cd apps/web
cp .env.example .env.local # fill in MODAL_WEB_URL, SUPABASE_URL, SUPABASE_SERVICE_KEY
npm install
npm run dev # http://localhost:3000
# Deploy: push to a repo and import in Vercel, or `vercel` (set the 3 env vars in the dashboard).The pipeline runs end-to-end on a laptop — without Supabase, artifacts are written under
--out and the printed manifest contains local paths. You still need the ML deps and
ffmpeg/LilyPond installed locally (heavy; this is mainly for debugging a single stage):
pip install -r pipeline/requirements.txt
python -m pipeline.cli --input song.wav --out ./bt-out
python -m pipeline.cli --input "https://youtu.be/..." --url# Pure logic (no ML deps) — runs anywhere:
python -m pytest pipeline/tests # 13 tests: tab assignment + stem config
# Frontend:
cd apps/web && npx tsc --noEmit && npx next build
# Pipeline on Modal (GPU):
modal run modal_app.py::process_job --job-id smoke --source song.wav --is-url false
# then check the `jobs` row + the artifacts bucket in Supabase.
# Web end-to-end: submit a short clip in the browser and confirm all four output
# types render and the guitar/piano panels show the "experimental" badge.End-to-end success criteria: each artifact opens (PDF in a viewer, MIDI in a DAW, MusicXML in MuseScore, tab in any text view), and core stems (drums/bass/vocals) are recognizably correct by ear.
- YouTube from cloud IPs is frequently rate-limited/blocked and is a ToS gray area.
Uploading the file is the robust path; set
YTDLP_PROXYfor a residential proxy. - Guitar/piano transcription uses MT3 on Modal (heavier polyphonic model) but is still
experimental — polyphonic audio→notes is unsolved; never blocks a job. MT3's
jax/t5x/mt3 stack is version-sensitive: the
mt3_imagepins inmodal_app.pyare a starting point and must be validated on Modal (you may need to nudge jax/flax/orbax to match currentt5xmain). MT3 also bakes a ~1 GB checkpoint into its image at build. - Drum transcription here is a lightweight onset+band classifier (kick/snare/hi-hat). Swap in omnizart for richer kits.
- MIDI playback in the piano roll:
html-midi-playerpulls a@magenta/music/tonecombo with a version skew that prints a build warning and can break audio playback (the piano-roll visualization still works). Pin a compatibletoneif you need in-browser playback audio. - Guitar tablature via open-fret is wired but dormant by default: open-fret ships
no public weights (you train them via its repo), so until a checkpoint is mounted at
OPENFRET_MODEL_DIRthe pipeline uses the deterministic fret assigner. To activate, mount/bake a trainedtiny-tab-v1/finaldir there (e.g. a Modal Volume) — its AlphaTex output then renders cleanly in alphaTab (a frontend follow-up). - Tracks are capped at 8 minutes to bound GPU cost (
MAX_DURATION_SECin config).
v1 is intentionally one-shot/ephemeral (no login; rows + files expire in 24h). To add a
saved library: add a user_id column to jobs, add Supabase Auth, and add per-user RLS
policies. No pipeline changes required.