Surn commited on
Commit
18eff79
·
1 Parent(s): 2cfca39

Refactor audio processing to improve job ID handling and update .gitignore for separated files

Browse files
Files changed (4) hide show
  1. .gitignore +2 -1
  2. README.md +6 -5
  3. app.py +26 -17
  4. modules/yt_audio_get_tracks.py +30 -9
.gitignore CHANGED
@@ -15,7 +15,8 @@ __pycache__/
15
  /__pycache__
16
  separated/htdemucs/
17
  separated/htdemucs_6s/
18
- *.webm
 
19
  *.pyi
20
  .claude/settings.json
21
  /.claude/settings.json
 
15
  /__pycache__
16
  separated/htdemucs/
17
  separated/htdemucs_6s/
18
+ separated/*.mp4
19
+ separated/*.webm
20
  *.pyi
21
  .claude/settings.json
22
  /.claude/settings.json
README.md CHANGED
@@ -55,15 +55,16 @@ If an upload is present, it takes precedence over the YouTube field.
55
  ## Outputs
56
 
57
  Separated files are written under `separated/htdemucs_6s/{job_id}/`.
58
- For YouTube sources, `job_id` is the extracted video ID. For uploaded audio,
59
- `job_id` is a sanitized version of the uploaded filename stem.
60
- Each stem card in the gallery can be played in place or downloaded directly.
 
61
 
62
  ## Extractors & Cookies
63
 
64
  For details about yt-dlp extractor behavior, see the official documentation:
65
 
66
- - https://github.com/yt-dlp/yt-dlp-wiki/blob/master/Extractors.md
67
 
68
  If you need authenticated extraction (for age-restricted or otherwise protected
69
  content), one way to provide cookies safely is via a private/incognito browser
@@ -71,7 +72,7 @@ session:
71
 
72
  1. Open a new private browsing/incognito window and log into YouTube.
73
  2. In the same window and same tab from step 1, navigate to
74
- https://www.youtube.com/robots.txt (this should be the only
75
  private/incognito browsing tab open).
76
  3. Export youtube.com cookies from the browser, then close the private/
77
  incognito window so that the session is never opened in the browser again.
 
55
  ## Outputs
56
 
57
  Separated files are written under `separated/htdemucs_6s/{job_id}/`.
58
+ For YouTube sources, `job_id` is the sanitized video title when available,
59
+ falling back to the video ID. For uploaded audio, `job_id` is a sanitized
60
+ version of the uploaded filename stem. Each stem card in the gallery can be
61
+ played in place or downloaded directly.
62
 
63
  ## Extractors & Cookies
64
 
65
  For details about yt-dlp extractor behavior, see the official documentation:
66
 
67
+ - <https://github.com/yt-dlp/yt-dlp-wiki/blob/master/Extractors.md>
68
 
69
  If you need authenticated extraction (for age-restricted or otherwise protected
70
  content), one way to provide cookies safely is via a private/incognito browser
 
72
 
73
  1. Open a new private browsing/incognito window and log into YouTube.
74
  2. In the same window and same tab from step 1, navigate to
75
+ <https://www.youtube.com/robots.txt> (this should be the only
76
  private/incognito browsing tab open).
77
  3. Export youtube.com cookies from the browser, then close the private/
78
  incognito window so that the session is never opened in the browser again.
app.py CHANGED
@@ -1,8 +1,6 @@
1
  # app.py — SeparateTracks Gradio application
2
  # Entry point: python app.py (runs on http://localhost:7860)
3
  # MCP endpoint: http://localhost:7860/gradio_api/mcp/sse
4
- import os
5
- import re
6
  import shutil
7
  import sys
8
  from importlib import import_module
@@ -11,7 +9,12 @@ from urllib.parse import parse_qs, urlparse
11
 
12
  import gradio as gr
13
 
14
- from modules.yt_audio_get_tracks import download_audio, separate_tracks
 
 
 
 
 
15
  from modules.file_utils import make_gradio_file_url
16
 
17
 
@@ -60,10 +63,6 @@ def _extract_video_id(video_input: str) -> str:
60
  return ""
61
 
62
 
63
- def _sanitize_job_id(name: str) -> str:
64
- return re.sub(r"[^A-Za-z0-9_-]+", "_", name).strip("_") or "uploaded_audio"
65
-
66
-
67
  def _build_audio_gallery(paths) -> str:
68
  audio_urls = [make_gradio_file_url(path) for path in paths]
69
  return audio_gallery_module.AudioGallery._build_html(
@@ -79,10 +78,20 @@ def _prepare_uploaded_audio(uploaded_audio: str) -> tuple[str, str]:
79
  if suffix not in {".wav", ".mp3"}:
80
  raise ValueError("Please upload a .wav or .mp3 file.")
81
 
82
- job_id = _sanitize_job_id(source_path.stem)
83
  target_path = SEPARATED_DIR / f"{job_id}{suffix}"
84
  shutil.copy2(source_path, target_path)
85
  return str(target_path), job_id
 
 
 
 
 
 
 
 
 
 
86
  # ---------------------------------------------------------------------------
87
  # AudioGallery CSS — injected inline so the component is self-contained
88
  # ---------------------------------------------------------------------------
@@ -115,7 +124,7 @@ def _process_video_impl(video_id: str, progress=None):
115
  def on_progress(message):
116
  progress_messages.append(message)
117
 
118
- video_id = _extract_video_id(video_id)
119
  if not video_id:
120
  return (
121
  "<p style='color:red;'>Please enter a YouTube video ID or URL.</p>",
@@ -128,12 +137,12 @@ def _process_video_impl(video_id: str, progress=None):
128
  url = f"https://www.youtube.com/watch?v={video_id}"
129
  if progress is not None:
130
  progress(0.15, desc="Downloading audio")
131
- wav = download_audio(url, video_id, progress_callback=on_progress)
132
  if progress is not None:
133
  progress(0.45, desc="Separating tracks")
134
  drums, vocals, guitar, bass, other, piano, music = separate_tracks(
135
  wav,
136
- video_id,
137
  progress_callback=on_progress,
138
  )
139
  if progress is not None:
@@ -164,14 +173,14 @@ def process_video(video_id: str, progress=gr.Progress(track_tqdm=True)) -> str:
164
  Returns:
165
  HTML string containing the AudioGallery with all separated stems.
166
  """
167
- video_id = _extract_video_id(video_id)
168
  if not video_id:
169
  return "<p style='color:red;'>Please enter a YouTube video ID or URL.</p>"
170
 
171
  try:
172
  url = f"https://www.youtube.com/watch?v={video_id}"
173
- wav = download_audio(url, video_id)
174
- drums, vocals, guitar, bass, other, piano, music = separate_tracks(wav, video_id)
175
  except Exception as exc:
176
  return f"<p style='color:red;'>Error: {exc}</p>"
177
 
@@ -199,15 +208,15 @@ def process_video_with_progress(
199
  audio_path, job_id = _prepare_uploaded_audio(uploaded_audio)
200
  status_lines.append("Using uploaded audio file.")
201
  else:
202
- job_id = _extract_video_id(video_id)
203
- if not job_id:
204
  yield (
205
  "<p style='color:red;'>Please enter a YouTube video ID or URL, or upload an audio file.</p>",
206
  "No video ID, URL, or audio file provided.",
207
  )
208
  return
209
 
210
- url = f"https://www.youtube.com/watch?v={job_id}"
211
  progress(0.05, desc="Downloading audio")
212
  yield "", "Downloading audio from YouTube..."
213
  audio_path = download_audio(url, job_id, progress_callback=on_progress)
 
1
  # app.py — SeparateTracks Gradio application
2
  # Entry point: python app.py (runs on http://localhost:7860)
3
  # MCP endpoint: http://localhost:7860/gradio_api/mcp/sse
 
 
4
  import shutil
5
  import sys
6
  from importlib import import_module
 
9
 
10
  import gradio as gr
11
 
12
+ from modules.yt_audio_get_tracks import (
13
+ download_audio,
14
+ get_title,
15
+ sanitize_job_id,
16
+ separate_tracks,
17
+ )
18
  from modules.file_utils import make_gradio_file_url
19
 
20
 
 
63
  return ""
64
 
65
 
 
 
 
 
66
  def _build_audio_gallery(paths) -> str:
67
  audio_urls = [make_gradio_file_url(path) for path in paths]
68
  return audio_gallery_module.AudioGallery._build_html(
 
78
  if suffix not in {".wav", ".mp3"}:
79
  raise ValueError("Please upload a .wav or .mp3 file.")
80
 
81
+ job_id = sanitize_job_id(source_path.stem)
82
  target_path = SEPARATED_DIR / f"{job_id}{suffix}"
83
  shutil.copy2(source_path, target_path)
84
  return str(target_path), job_id
85
+
86
+
87
+ def _resolve_youtube_job_id(video_input: str) -> tuple[str, str]:
88
+ video_id = _extract_video_id(video_input)
89
+ if not video_id:
90
+ return "", ""
91
+
92
+ url = f"https://www.youtube.com/watch?v={video_id}"
93
+ title = get_title(url)
94
+ return video_id, sanitize_job_id(title or video_id)
95
  # ---------------------------------------------------------------------------
96
  # AudioGallery CSS — injected inline so the component is self-contained
97
  # ---------------------------------------------------------------------------
 
124
  def on_progress(message):
125
  progress_messages.append(message)
126
 
127
+ video_id, job_id = _resolve_youtube_job_id(video_id)
128
  if not video_id:
129
  return (
130
  "<p style='color:red;'>Please enter a YouTube video ID or URL.</p>",
 
137
  url = f"https://www.youtube.com/watch?v={video_id}"
138
  if progress is not None:
139
  progress(0.15, desc="Downloading audio")
140
+ wav = download_audio(url, job_id, progress_callback=on_progress)
141
  if progress is not None:
142
  progress(0.45, desc="Separating tracks")
143
  drums, vocals, guitar, bass, other, piano, music = separate_tracks(
144
  wav,
145
+ job_id,
146
  progress_callback=on_progress,
147
  )
148
  if progress is not None:
 
173
  Returns:
174
  HTML string containing the AudioGallery with all separated stems.
175
  """
176
+ video_id, job_id = _resolve_youtube_job_id(video_id)
177
  if not video_id:
178
  return "<p style='color:red;'>Please enter a YouTube video ID or URL.</p>"
179
 
180
  try:
181
  url = f"https://www.youtube.com/watch?v={video_id}"
182
+ wav = download_audio(url, job_id)
183
+ drums, vocals, guitar, bass, other, piano, music = separate_tracks(wav, job_id)
184
  except Exception as exc:
185
  return f"<p style='color:red;'>Error: {exc}</p>"
186
 
 
208
  audio_path, job_id = _prepare_uploaded_audio(uploaded_audio)
209
  status_lines.append("Using uploaded audio file.")
210
  else:
211
+ video_id, job_id = _resolve_youtube_job_id(video_id)
212
+ if not video_id:
213
  yield (
214
  "<p style='color:red;'>Please enter a YouTube video ID or URL, or upload an audio file.</p>",
215
  "No video ID, URL, or audio file provided.",
216
  )
217
  return
218
 
219
+ url = f"https://www.youtube.com/watch?v={video_id}"
220
  progress(0.05, desc="Downloading audio")
221
  yield "", "Downloading audio from YouTube..."
222
  audio_path = download_audio(url, job_id, progress_callback=on_progress)
modules/yt_audio_get_tracks.py CHANGED
@@ -1,7 +1,12 @@
1
  # yt_separator.py
2
  # pip install yt-dlp demucs pydub (ffmpeg required)
3
- import os, sys, subprocess
 
4
  import shutil
 
 
 
 
5
  import yt_dlp
6
  from pydub import AudioSegment
7
 
@@ -9,9 +14,23 @@ def _emit_progress(progress_callback, message):
9
  if progress_callback is not None:
10
  progress_callback(message)
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  cookie_path = os.path.join(os.path.dirname(__file__), 'cookies.txt')
13
 
14
- def download_audio(url, video_id, progress_callback=None):
15
  temp_dir = 'separated'
16
  os.makedirs(temp_dir, exist_ok=True)
17
  _emit_progress(progress_callback, 'Downloading audio from YouTube...')
@@ -24,7 +43,7 @@ def download_audio(url, video_id, progress_callback=None):
24
 
25
  ydl_opts = {
26
  'format': 'bestaudio/best',
27
- 'outtmpl': os.path.join(temp_dir, f'{video_id}.%(ext)s'),
28
  'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'wav'}],
29
  'keepvideo': True,
30
  'quiet': False,
@@ -55,12 +74,12 @@ def download_audio(url, video_id, progress_callback=None):
55
  # return None
56
  # _emit_progress(progress_callback, f"Found {len(audio)} audio formats")
57
 
58
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
59
  ydl.download([url])
60
  _emit_progress(progress_callback, 'Converting downloaded audio to WAV...')
61
- return os.path.join(temp_dir, f'{video_id}.wav')
62
 
63
- def separate_tracks(input_wav, video_id, progress_callback=None):
64
  if not os.path.exists(input_wav):
65
  raise FileNotFoundError(f"{input_wav} does not exist")
66
 
@@ -68,7 +87,7 @@ def separate_tracks(input_wav, video_id, progress_callback=None):
68
  _emit_progress(progress_callback, 'Separating tracks with Demucs...')
69
  subprocess.run(['demucs', '-n', 'htdemucs_6s', '--mp3', '--out', output_dir, input_wav], check=True)
70
 
71
- base = os.path.join('.', output_dir, 'htdemucs_6s', video_id)
72
 
73
  drums = f'{base}/drums.mp3'
74
  vocals = f'{base}/vocals.mp3'
@@ -92,8 +111,10 @@ def main():
92
  video_id = input("enter youtube video id: ")
93
  url = f"https://www.youtube.com/watch?v={video_id}"
94
  try:
95
- wav = download_audio(url, video_id)
96
- d, v, g, b, o, p, m = separate_tracks(wav, video_id)
 
 
97
  print(d, v, g, b, o, p, m)
98
  except Exception as exc:
99
  print(exc)
 
1
  # yt_separator.py
2
  # pip install yt-dlp demucs pydub (ffmpeg required)
3
+ import os
4
+ import re
5
  import shutil
6
+ import subprocess
7
+ import sys
8
+ from typing import Any, cast
9
+
10
  import yt_dlp
11
  from pydub import AudioSegment
12
 
 
14
  if progress_callback is not None:
15
  progress_callback(message)
16
 
17
+
18
+ def sanitize_job_id(name):
19
+ return re.sub(r"[^A-Za-z0-9_-]+", "_", name).strip("_") or "uploaded_audio"
20
+
21
+
22
+ def get_title(url_or_id):
23
+ with yt_dlp.YoutubeDL({"quiet": True, "no_warnings": True}) as ydl:
24
+ try:
25
+ info = ydl.extract_info(url_or_id, download=False) or {}
26
+ except Exception:
27
+ return ""
28
+
29
+ return info.get("title") or info.get("id") or ""
30
+
31
  cookie_path = os.path.join(os.path.dirname(__file__), 'cookies.txt')
32
 
33
+ def download_audio(url, job_id, progress_callback=None):
34
  temp_dir = 'separated'
35
  os.makedirs(temp_dir, exist_ok=True)
36
  _emit_progress(progress_callback, 'Downloading audio from YouTube...')
 
43
 
44
  ydl_opts = {
45
  'format': 'bestaudio/best',
46
+ 'outtmpl': os.path.join(temp_dir, f'{job_id}.%(ext)s'),
47
  'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'wav'}],
48
  'keepvideo': True,
49
  'quiet': False,
 
74
  # return None
75
  # _emit_progress(progress_callback, f"Found {len(audio)} audio formats")
76
 
77
+ with yt_dlp.YoutubeDL(cast(Any, ydl_opts)) as ydl:
78
  ydl.download([url])
79
  _emit_progress(progress_callback, 'Converting downloaded audio to WAV...')
80
+ return os.path.join(temp_dir, f'{job_id}.wav')
81
 
82
+ def separate_tracks(input_wav, job_id, progress_callback=None):
83
  if not os.path.exists(input_wav):
84
  raise FileNotFoundError(f"{input_wav} does not exist")
85
 
 
87
  _emit_progress(progress_callback, 'Separating tracks with Demucs...')
88
  subprocess.run(['demucs', '-n', 'htdemucs_6s', '--mp3', '--out', output_dir, input_wav], check=True)
89
 
90
+ base = os.path.join('.', output_dir, 'htdemucs_6s', job_id)
91
 
92
  drums = f'{base}/drums.mp3'
93
  vocals = f'{base}/vocals.mp3'
 
111
  video_id = input("enter youtube video id: ")
112
  url = f"https://www.youtube.com/watch?v={video_id}"
113
  try:
114
+ title = get_title(url)
115
+ job_id = sanitize_job_id(title or video_id)
116
+ wav = download_audio(url, job_id)
117
+ d, v, g, b, o, p, m = separate_tracks(wav, job_id)
118
  print(d, v, g, b, o, p, m)
119
  except Exception as exc:
120
  print(exc)