# 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 `