Reubencf commited on
Commit
735083c
·
verified ·
1 Parent(s): 1664ec3

Probe YouTube imports through the dedicated API network

Browse files
Files changed (4) hide show
  1. app.py +57 -9
  2. packages.txt +1 -0
  3. requirements.txt +1 -0
  4. test_gpu_duration.py +22 -0
app.py CHANGED
@@ -1,7 +1,9 @@
1
  """The dedicated ZeroGPU transcription API for Score Studio."""
2
- import logging
3
- import os
4
- from pathlib import Path
 
 
5
  import shutil
6
  import subprocess
7
  import sys
@@ -10,8 +12,9 @@ import time
10
 
11
  import gradio as gr
12
  import spaces
13
- import torch
14
- from transformers import AutoModel
 
15
 
16
  MODEL_ID = "m-a-p/SheetSage2"
17
  REVISION = "eab522a8168e8b8b8c4856bf8609cd86198f01fe"
@@ -36,7 +39,7 @@ def gpu_seconds(audio, start, duration, melody_only, piano, progress=None, **kwa
36
 
37
 
38
  @spaces.GPU(duration=gpu_seconds)
39
- def transcribe(audio, start, duration, melody_only, piano, progress=gr.Progress()):
40
  if not audio:
41
  raise gr.Error("Upload an audio file first.")
42
  if start < 0 or not 5 <= duration <= 600:
@@ -111,7 +114,46 @@ def transcribe(audio, start, duration, melody_only, piano, progress=gr.Progress(
111
  raise
112
  except Exception:
113
  logging.exception("Transcription failed")
114
- raise gr.Error("Conversion failed. Try a shorter excerpt. Details are available in the Space logs.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
 
116
 
117
  with gr.Blocks(title="Score Studio API", delete_cache=(3600, 86400)) as demo:
@@ -129,9 +171,15 @@ with gr.Blocks(title="Score Studio API", delete_cache=(3600, 86400)) as demo:
129
  preview = gr.Audio(label="Piano preview")
130
  downloads = gr.File(label="Downloads", file_count="multiple")
131
  abc = gr.Code(label="ABC notation", language=None)
132
- run.click(transcribe, [audio, start, duration, melody, piano],
133
  [status, gallery, pdf, preview, downloads, abc],
134
- api_name="transcribe", concurrency_limit=1)
 
 
 
 
 
 
135
  gr.Markdown("Powered by [SheetSage2](https://huggingface.co/m-a-p/SheetSage2), "
136
  "CC BY-NC 4.0. Temporary files expire after 24 hours.")
137
 
 
1
  """The dedicated ZeroGPU transcription API for Score Studio."""
2
+ import logging
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import re
7
  import shutil
8
  import subprocess
9
  import sys
 
12
 
13
  import gradio as gr
14
  import spaces
15
+ import torch
16
+ from transformers import AutoModel
17
+ from yt_dlp import YoutubeDL
18
 
19
  MODEL_ID = "m-a-p/SheetSage2"
20
  REVISION = "eab522a8168e8b8b8c4856bf8609cd86198f01fe"
 
39
 
40
 
41
  @spaces.GPU(duration=gpu_seconds)
42
+ def transcribe(audio, start, duration, melody_only, piano, progress=gr.Progress()):
43
  if not audio:
44
  raise gr.Error("Upload an audio file first.")
45
  if start < 0 or not 5 <= duration <= 600:
 
114
  raise
115
  except Exception:
116
  logging.exception("Transcription failed")
117
+ raise gr.Error("Conversion failed. Try a shorter excerpt. Details are available in the Space logs.")
118
+
119
+
120
+ def import_youtube(url):
121
+ match = re.fullmatch(r"https?://(?:www\.|m\.|music\.)?(?:youtube\.com/(?:watch\?v=|shorts/|live/)|youtu\.be/)([A-Za-z0-9_-]{11})(?:[?&/].*)?", str(url).strip())
122
+ if not match:
123
+ raise gr.Error("Enter a valid YouTube video link.")
124
+ video_id = match.group(1)
125
+ job = Path(tempfile.mkdtemp(prefix="youtube-", dir=ROOT))
126
+ try:
127
+ options = {
128
+ "quiet": True, "no_warnings": True, "noplaylist": True,
129
+ "socket_timeout": 15, "retries": 1, "extractor_retries": 1,
130
+ "fragment_retries": 1, "js_runtimes": {"node": {}},
131
+ "format": "bestaudio/best", "outtmpl": str(job / "audio.%(ext)s"),
132
+ "max_filesize": 100 * 1024 * 1024,
133
+ }
134
+ with YoutubeDL(options) as ydl:
135
+ source = "https://www.youtube.com/watch?v=" + video_id
136
+ info = ydl.extract_info(source, download=False)
137
+ duration = float(info.get("duration") or 0)
138
+ if info.get("is_live") or info.get("age_limit", 0) > 0:
139
+ raise gr.Error("Live or age-restricted videos are not supported.")
140
+ if not 5 <= duration <= 600:
141
+ raise gr.Error("Choose a video between five seconds and ten minutes.")
142
+ ydl.process_info(info)
143
+ audio = next((p for p in job.glob("audio.*") if p.is_file() and p.stat().st_size > 2048), None)
144
+ if not audio:
145
+ raise gr.Error("No audio could be read from this video.")
146
+ metadata = json.dumps({"title": info.get("title", "YouTube audio"),
147
+ "artist": info.get("uploader", ""), "duration": duration,
148
+ "thumbnail": f"https://i.ytimg.com/vi/{video_id}/hqdefault.jpg", "url": source})
149
+ return str(audio), metadata
150
+ except gr.Error:
151
+ raise
152
+ except Exception as exc:
153
+ logging.warning("YouTube API import failed: %s", str(exc)[:800])
154
+ if "not a bot" in str(exc).lower() or "confirm you" in str(exc).lower():
155
+ raise gr.Error("YouTube blocked this server address.")
156
+ raise gr.Error("The API could not download this YouTube audio.")
157
 
158
 
159
  with gr.Blocks(title="Score Studio API", delete_cache=(3600, 86400)) as demo:
 
171
  preview = gr.Audio(label="Piano preview")
172
  downloads = gr.File(label="Downloads", file_count="multiple")
173
  abc = gr.Code(label="ABC notation", language=None)
174
+ run.click(transcribe, [audio, start, duration, melody, piano],
175
  [status, gallery, pdf, preview, downloads, abc],
176
+ api_name="transcribe", concurrency_limit=1)
177
+ youtube_url = gr.Textbox(visible=False)
178
+ youtube_run = gr.Button(visible=False)
179
+ youtube_audio = gr.File(visible=False)
180
+ youtube_metadata = gr.Textbox(visible=False)
181
+ youtube_run.click(import_youtube, youtube_url, [youtube_audio, youtube_metadata],
182
+ api_name="youtube_import", concurrency_limit=2)
183
  gr.Markdown("Powered by [SheetSage2](https://huggingface.co/m-a-p/SheetSage2), "
184
  "CC BY-NC 4.0. Temporary files expire after 24 hours.")
185
 
packages.txt CHANGED
@@ -1,4 +1,5 @@
1
  ffmpeg
 
2
  libsndfile1
3
  libglib2.0-0
4
  libnss3
 
1
  ffmpeg
2
+ nodejs
3
  libsndfile1
4
  libglib2.0-0
5
  libnss3
requirements.txt CHANGED
@@ -10,3 +10,4 @@ pretty_midi==0.2.10
10
  mido==1.3.3
11
  setuptools==78.1.1
12
  playwright==1.58.0
 
 
10
  mido==1.3.3
11
  setuptools==78.1.1
12
  playwright==1.58.0
13
+ yt-dlp[default]==2026.8.19
test_gpu_duration.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Check the actual Gradio-injected call signature without loading CUDA weights."""
2
+ import ast
3
+ from pathlib import Path
4
+ import unittest
5
+
6
+ tree = ast.parse(Path(__file__).with_name("app.py").read_text(encoding="utf-8"))
7
+ definition = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "gpu_seconds")
8
+ namespace = {}
9
+ exec(compile(ast.Module(body=[definition], type_ignores=[]), "gpu-duration", "exec"), namespace)
10
+
11
+ class DurationTests(unittest.TestCase):
12
+ def test_gradio_injects_progress_as_a_sixth_argument(self):
13
+ reserve = namespace["gpu_seconds"]("audio.wav", 0, 275, False, True, object())
14
+ self.assertGreaterEqual(reserve, 45)
15
+ self.assertLessEqual(reserve, 300)
16
+
17
+ def test_short_recording_does_not_reserve_full_song_budget(self):
18
+ function = namespace["gpu_seconds"]
19
+ self.assertLess(function("audio.wav", 0, 10, False, True), function("audio.wav", 0, 600, False, True))
20
+
21
+ if __name__ == "__main__":
22
+ unittest.main()