# CLAUDE.md — SeparateTracks Project Context
## MCP Tools
Call the `jcodemunch_guide` tool and strictly follow its instructions for code retrieval.
The jCodeMunch MCP server is configured in `.claude/settings.json`. The project has
been indexed. Workflow:
1. Call `index_folder` on the project root to index or re-index after changes.
2. Then use `search_symbols`, `get_symbol_source`, `get_file_outline`, and
`search_text` for token-efficient code retrieval instead of reading whole files.
---
## Project Overview
**SeparateTracks** (`Surn/SeparateTracks`) — A HuggingFace Docker Space that:
- Downloads audio from YouTube via `yt-dlp` + Deno, using either a raw video ID
or a full YouTube URL.
- Accepts uploaded `.wav` or `.mp3` audio as a local processing override.
- Separates audio into 6 instrument stems using Demucs (`htdemucs_6s`).
- Presents results in a Gradio UI with a lightweight `AudioGallery` HTML
component and globally injected gallery JS.
- Exposes an MCP server at `/gradio_api/mcp/sse`.
---
## Key Files
| File | Purpose |
| ---- | ------- |
| `app.py` | Gradio entry point — UI, routing, progress, global gallery JS/head injection, MCP launch |
| `modules/AudioGallery.py` | `AudioGallery(gr.HTML)` — 7-stem audio grid with waveform canvas, play controls, download links, and client-side init JS |
| `modules/AudioGallery.pyi` | Type stub for AudioGallery |
| `modules/yt_audio_get_tracks.py` | `download_audio()` + `separate_tracks()` with progress callbacks |
| `modules/constants.py` | Env vars (`HF_TOKEN`, `HF_REPO_ID`, etc.), shared constants |
| `modules/version_info.py` | `versions_html()` for Gradio footer |
| `modules/file_utils.py` | File utility helpers, including Gradio file URL generation |
| `requirements.txt` | Pip dependencies |
| `Dockerfile` | Docker image — `python:3.12-slim` + ffmpeg + Deno + pip |
| `specs/build.md` | Step-by-step build plan |
| `.claude/settings.json` | MCP server config (jcodemunch) |
> **Note:** The original root-level `yt_audio_get_tracks.py` has been moved to
> `modules/yt_audio_get_tracks.py`. Do not recreate it at root.
---
## Architecture
```text
app.py
├── SEPARATED_DIR = Path("separated").resolve()
├── gr.set_static_paths(paths=["separated/", ".separated/"])
├── audio_gallery_head = f""
├── _extract_video_id(video_input) → normalizes YouTube IDs and URLs
├── _prepare_uploaded_audio(path) → copies uploaded audio into `separated/`
├── _footer_html() → modules.version_info.versions_html()
├── process_video(video_id) → MCP-exposed tool (simple, no progress)
├── process_video_with_progress(video_id, uploaded_audio)
│ ├── progress=gr.Progress(track_tqdm=True)
│ ├── upload present? use uploaded file first
│ ├── otherwise download_audio(url, id, progress_callback)
│ ├── separate_tracks(wav, id, progress_callback)
│ ├── make_gradio_file_url(path)
│ └── AudioGallery._build_html(audio_urls, labels, columns)
└── demo.launch(mcp_server=True, head=audio_gallery_head, allowed_paths=[SEPARATED_DIR])
modules/AudioGallery.py
└── AudioGallery(gr.HTML)
├── DEFAULT_LABELS = [Drums, Vocals, Guitar, Bass, Other, Piano, Music]
├── GALLERY_JS = initAudioItem/drawWaveform/MutationObserver script
├── __init__(audio_urls, *, labels, columns, ...) → builds HTML only
└── _build_html(audio_urls, labels, columns) → inline CSS + gallery HTML with per-stem download links
modules/file_utils.py
└── make_gradio_file_url(path) → converts local stem paths into
`/gradio_api/file=...` URLs
modules/yt_audio_get_tracks.py
├── _emit_progress(progress_callback, message)
├── download_audio(url, video_id, progress_callback=None) → wav path
└── separate_tracks(input_wav, video_id, progress_callback=None)
→ (drums, vocals, guitar, bass, other, piano, music_path)
```
## Gradio Progress Pattern
Use `progress=gr.Progress(track_tqdm=True)` in processing handlers when you want
interactive progress updates in the UI. The current app supports this via the
shared processing helper in `app.py`, while still collecting the stage messages
emitted by `modules/yt_audio_get_tracks.py`.
The AudioGallery controls are initialized client-side via the global gallery JS
injected from `app.py` with `head=audio_gallery_head`, so play/pause and the
time display work after each rendered update.
---
## UI Layout
```text
[YouTube Video ID or URL input .....] [Separate Tracks btn]
[Audio File Override (.wav/.mp3) ........................]
[Progress textbox — 6 lines, read-only ..................]
[AudioGallery HTML — 3-column stem grid .................]
[Each stem includes play and download controls ...........]
[Footer — versions_html() ...............................]
```
Button triggers `process_video_with_progress(video_id, uploaded_audio)` and
returns `[audio_output, progress_output]`.
The app registers `separated/` and `.separated/` as Gradio static paths so the
generated stem files can be served from the local output folder.
Audio file URLs are generated with `modules.file_utils.make_gradio_file_url()` so
the `/gradio_api/file=` endpoint receives a Gradio-safe relative path.
---
## Progress Callback Pattern
Both core functions accept an optional `progress_callback(message: str)` parameter.
`app.py` collects messages into a list and returns the joined string as status text.
```python
def on_progress(message):
progress_messages.append(message)
download_audio(url, video_id, progress_callback=on_progress)
separate_tracks(wav, video_id, progress_callback=on_progress)
```
If an uploaded audio file is provided, `download_audio()` is skipped and the
uploaded file path is passed directly into `separate_tracks()` after being
copied into `separated/` with a sanitized filename.
Progress messages emitted:
1. `"Downloading audio from YouTube..."`
2. `"Converting downloaded audio to WAV..."`
3. `"Separating tracks with Demucs..."`
4. `"Creating combined music stem..."`
5. `"Separation complete."`
---
## Stems Order
`drums, vocals, guitar, bass, other, piano, music_path`
Output dir: `separated/htdemucs_6s/{job_id}/`
`job_id` is either the extracted YouTube video ID or the sanitized uploaded
audio filename stem.
`music.mp3` = `bass + other` overlay via pydub.
---
## Copilot / Agent Rules
- **Minimal changes** — preserve existing functionality.
- **No new dependencies** without approval.
- **Use existing `modules/` functions** before writing new code; prefer overloads.
- **Gradio reference**:
- **AudioGallery** — `modules/AudioGallery.py`; extend `gr.HTML`.
- **Footer** must use `modules/version_info.versions_html()`.
- **Dockerfile** is HuggingFace-compatible (`python:3.12-slim`).
- **MCP** — Gradio built-in `mcp_server=True` in `demo.launch()`.
- **Deno** — install via `deno.land/install.sh` in Docker or add `deno.exe` to PATH locally.
- **Testing** — Playwright MCP headless, MSTest, UV.
## Python Style
- Snake_case functions and variables, CamelCase classes.
- PEP 8: 4-space indent, 79-char lines.
- Imports: stdlib → third-party → local.
- In f-strings with `