# app.py — SeparateTracks Gradio application # Entry point: python app.py (runs on http://localhost:7860) # MCP endpoint: http://localhost:7860/gradio_api/mcp/sse import shutil import sys from importlib import import_module from pathlib import Path from urllib.parse import parse_qs, urlparse import gradio as gr from modules.yt_audio_get_tracks import ( download_audio, get_title, sanitize_job_id, separate_tracks, ) from modules.file_utils import make_gradio_file_url audio_gallery_module = import_module("modules.AudioGallery") audio_gallery_head = f"" SEPARATED_DIR = Path("separated").resolve() gr.set_static_paths(paths=["separated/", SEPARATED_DIR.as_posix()]) def _extract_video_id(video_input: str) -> str: candidate = video_input.strip() if not candidate: return "" is_raw_id = "://" not in candidate and all( marker not in candidate for marker in ("/", "?", "&") ) if "://" not in candidate and ( "youtube.com" in candidate or "youtu.be" in candidate ): candidate = f"https://{candidate}" if is_raw_id: return candidate parsed = urlparse(candidate) host = parsed.netloc.lower() if host.endswith("youtu.be"): return parsed.path.strip("/").split("/")[0] if "youtube.com" in host: video_id = parse_qs(parsed.query).get("v", [""])[0] if video_id: return video_id path_parts = [part for part in parsed.path.split("/") if part] for prefix in ("shorts", "embed", "live", "v"): if prefix in path_parts: prefix_index = path_parts.index(prefix) if prefix_index + 1 < len(path_parts): return path_parts[prefix_index + 1] return "" def _build_audio_gallery(paths) -> str: audio_urls = [make_gradio_file_url(path) for path in paths] return audio_gallery_module.AudioGallery._build_html( audio_urls=audio_urls, labels=audio_gallery_module.AudioGallery.DEFAULT_LABELS, columns=3, ) def _prepare_uploaded_audio(uploaded_audio: str) -> tuple[str, str]: source_path = Path(uploaded_audio) suffix = source_path.suffix.lower() if suffix not in {".wav", ".mp3"}: raise ValueError("Please upload a .wav or .mp3 file.") job_id = sanitize_job_id(source_path.stem) target_path = SEPARATED_DIR / f"{job_id}{suffix}" shutil.copy2(source_path, target_path) return str(target_path), job_id def _resolve_youtube_job_id(video_input: str) -> tuple[str, str, str]: video_id = _extract_video_id(video_input) if not video_id: return "", "", "" url = f"https://www.youtube.com/watch?v={video_id}" youtube_title = get_title(url) return video_id, youtube_title, sanitize_job_id(youtube_title or video_id) # --------------------------------------------------------------------------- # AudioGallery CSS — injected inline so the component is self-contained # --------------------------------------------------------------------------- _CSS = """ #versions { margin-top: 1em; width: 100%; text-align: center; } """ # --------------------------------------------------------------------------- # Version footer (graceful fallback if torch/cuda not available) # --------------------------------------------------------------------------- def _footer_html(): try: from modules.version_info import versions_html return versions_html() except Exception: python_ver = ".".join(str(x) for x in sys.version_info[:3]) return f"python: {python_ver} • gradio: {gr.__version__}" # --------------------------------------------------------------------------- # Core processing function (also exposed as MCP tool) # --------------------------------------------------------------------------- def _process_video_impl(video_id: str, progress=None): progress_messages = [] def on_progress(message): progress_messages.append(message) video_id, youtube_title, job_id = _resolve_youtube_job_id(video_id) if not video_id: return ( "
Please enter a YouTube video ID or URL.
", "No video ID provided.", ) try: on_progress(f"YouTube title: {youtube_title}") if progress is not None: progress(0.0, desc="Preparing request") url = f"https://www.youtube.com/watch?v={video_id}" if progress is not None: progress(0.15, desc="Downloading audio") wav = download_audio(url, job_id, progress_callback=on_progress) if progress is not None: progress(0.45, desc="Separating tracks") drums, vocals, guitar, bass, other, piano, music = separate_tracks( wav, job_id, progress_callback=on_progress, ) if progress is not None: progress(0.9, desc="Building audio gallery") except Exception as exc: status = "\n".join(progress_messages) if progress_messages else "Starting..." return f"Error: {exc}
", f"{status}\nError: {exc}" paths = [drums, vocals, guitar, bass, other, piano, music] status = "\n".join(progress_messages + ["Done."]) if progress is not None: progress(1.0, desc="Done") return ( _build_audio_gallery(paths), status, ) def process_video(video_id: str, progress=gr.Progress(track_tqdm=True)) -> str: """Download audio from a YouTube video and separate it into instrument stems. Uses Demucs htdemucs_6s to produce drums, vocals, guitar, bass, piano, other, and a combined music track. Results are displayed as an audio gallery. Args: video_id: YouTube video ID or URL (e.g. dQw4w9WgXcQ). Returns: HTML string containing the AudioGallery with all separated stems. """ video_id, _youtube_title, job_id = _resolve_youtube_job_id(video_id) if not video_id: return "Please enter a YouTube video ID or URL.
" try: url = f"https://www.youtube.com/watch?v={video_id}" wav = download_audio(url, job_id) drums, vocals, guitar, bass, other, piano, music = separate_tracks(wav, job_id) except Exception as exc: return f"Error: {exc}
" paths = [drums, vocals, guitar, bass, other, piano, music] return _build_audio_gallery(paths) def process_video_with_progress( video_id: str, uploaded_audio: str | None, cookies_upload: str | None, progress=gr.Progress(track_tqdm=True), ): status_lines = [] def on_progress(message): status_lines.append(message) try: if cookies_upload is not None: shutil.copy(cookies_upload, "modules/cookies.txt") if uploaded_audio: progress(0.05, desc="Preparing uploaded audio") yield "", "Preparing uploaded audio..." audio_path, job_id = _prepare_uploaded_audio(uploaded_audio) status_lines.append("Using uploaded audio file.") else: video_id, youtube_title, job_id = _resolve_youtube_job_id(video_id) if not video_id: yield ( "Please enter a YouTube video ID or URL, or upload an audio file.
", "No video ID, URL, or audio file provided.", ) return status_lines.append(f"YouTube title: {youtube_title}") url = f"https://www.youtube.com/watch?v={video_id}" progress(0.05, desc="Downloading audio") yield "", "\n".join(status_lines + ["Downloading audio from YouTube..."]) audio_path = download_audio(url, job_id, progress_callback=on_progress) progress(0.4, desc="Separating tracks") yield "", "\n".join(status_lines) drums, vocals, guitar, bass, other, piano, music = separate_tracks( audio_path, job_id, progress_callback=on_progress ) progress(0.9, desc="Building gallery") yield "", "\n".join(status_lines) except Exception as exc: yield ( f"Error: {exc}
", "\n".join(status_lines) + f"\nError: {exc}", ) return paths = [drums, vocals, guitar, bass, other, piano, music] status_lines.append("Done.") progress(1.0, desc="Done") yield ( _build_audio_gallery(paths), "\n".join(status_lines), ) # --------------------------------------------------------------------------- # Gradio UI # --------------------------------------------------------------------------- with gr.Blocks(title="SeparateTracks") as demo: gr.Markdown( "## \U0001f3bc SeparateTracks\n" "Enter a YouTube video URL or ID, or upload a WAV/MP3 file, to " "separate the audio into instrument stems " "using [Demucs htdemucs\\_6s](https://github.com/adefossez/demucs)." ) with gr.Row(): video_id_input = gr.Textbox( label="YouTube Video ID or URL", placeholder="dQw4w9WgXcQ or https://www.youtube.com/watch?v=dQw4w9WgXcQ", scale=4, ) run_btn = gr.Button("Separate Tracks", variant="primary", scale=1) with gr.Row(): cookies_upload = gr.File( label="Upload Chrome cookies.txt (Netscape format)", file_types=[".txt"], type="filepath", ) gr.Markdown(""" **How to get cookies.txt:** 1. Install [Get cookies.txt LOCALLY](https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc) 2. Log into YouTube in Chrome 3. Click extension → Export cookies 4. Upload the file here """) gr.Markdown( "\n\nFor details about yt-dlp's extractor behavior, see: https://github.com/yt-dlp/yt-dlp-wiki/blob/master/Extractors.md\n\n" "One way to provide cookies safely is through a private browsing/incognito window:\n\n" "1. Open a new private browsing/incognito window and log into YouTube.\n" "2. chrome://extensions and specify Allow in Incognito.\n" "2. In the same window and same tab from step 1, navigate to https://www.youtube.com/robots.txt (this should be the only private/incognito browsing tab open).\n" "3. Export youtube.com cookies from the browser, then close the private browsing/incognito window so that the session is never opened in the browser again.\n" ) # https://github.com/yt-dlp/yt-dlp-wiki/ upload_input = gr.File( label="Audio File Override (.wav or .mp3)", file_types=[".wav", ".mp3"], type="filepath", ) progress_output = gr.Textbox(label="Progress", interactive=False, lines=6) audio_output = gr.HTML(label="Separated Tracks") gr.HTML(value=_footer_html(), elem_id="versions", elem_classes="version-info") run_btn.click( fn=process_video_with_progress, inputs=[video_id_input, upload_input, cookies_upload], outputs=[audio_output, progress_output], ) if __name__ == "__main__": demo.launch( mcp_server=True, server_name="0.0.0.0", server_port=7860, allowed_paths=[SEPARATED_DIR.as_posix(), "separated/", ".separated/"], favicon_path="separated/favicon.ico", css=_CSS, head=audio_gallery_head, )