Reubencf commited on
Commit
3a61dc6
·
verified ·
1 Parent(s): 83401f4

Register Kiku AI redirect URI for OAuth

Browse files
Files changed (1) hide show
  1. app.py +143 -140
app.py CHANGED
@@ -1,158 +1,161 @@
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
8
- import tempfile
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"
18
- ROOT = Path(tempfile.gettempdir()) / "audio-to-score"
19
- ROOT.mkdir(exist_ok=True)
20
- logging.basicConfig(level=logging.INFO)
21
-
22
- # ZeroGPU's CUDA emulation lets the model be placed during startup; the actual
23
- # 48 GB GPU is attached only while the decorated conversion function runs.
24
- MODEL = AutoModel.from_pretrained(
25
- MODEL_ID, revision=REVISION, trust_remote_code=True,
26
- torch_dtype=torch.bfloat16,
27
- ).eval().to("cuda")
28
-
29
- # The upstream renderer is fully offline but needs its matching headless browser.
30
- subprocess.run([sys.executable, "-m", "playwright", "install", "--only-shell", "chromium"], check=True)
31
-
32
-
33
  def gpu_seconds(audio, start, duration, melody_only, piano, progress=None, **kwargs):
34
- # Short excerpts should fit visitor quotas instead of reserving five minutes.
35
- return max(45, min(300, int(float(duration) * 0.4) + 30))
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:
43
- raise gr.Error("Choose a start of 0 or later and a duration from 5 to 600 seconds.")
44
- # Clear the previous run immediately so failed requests cannot show stale scores.
45
- yield "Preparing your audio…", [], None, None, [], ""
46
- # Keep the current result until Gradio has copied it to its managed cache.
47
- # Remove abandoned job directories from earlier runs after one day.
48
- cutoff = time.time() - 86400
49
- for old in ROOT.glob("score-*"):
50
- if old.is_dir() and not old.is_symlink() and old.stat().st_mtime < cutoff:
51
- shutil.rmtree(old, ignore_errors=True)
52
- job = Path(tempfile.mkdtemp(prefix="score-", dir=ROOT))
53
- clip = job / "input.wav"
54
- output = job / "results"
55
- output.mkdir()
56
- try:
57
- progress(0.03, desc="Preparing audio excerpt")
58
- completed = subprocess.run(
59
- ["ffmpeg", "-nostdin", "-v", "error", "-ss", str(start),
60
- "-i", str(audio), "-t", str(duration), "-ac", "1", "-ar", "24000",
61
- "-y", str(clip)], capture_output=True, timeout=120,
62
- )
63
- if completed.returncode or not clip.exists() or clip.stat().st_size < 2048:
64
- raise gr.Error("Could not read this excerpt. Check the file and start time.")
65
- progress(0.06, desc="Loading SheetSage2 — first run takes longer")
66
- model = MODEL
67
- started = time.monotonic()
68
-
69
- def update(event):
70
- stage = event.get("stage", "working")
71
- labels = {"audio": "Reading audio", "encoding": "Listening to the music",
72
- "decoding": "Writing notes and chords", "notation": "Preparing notation",
73
- "complete": "Rendering printable score"}
74
- detail = labels.get(stage, "Processing music")
75
- if "window" in event:
76
- detail += f" · section {event['window']}/{event['windows']}"
77
- if "tokens" in event:
78
- detail += f" · {event['tokens']} symbols"
79
- progress(None, desc=detail)
80
-
81
- notice = ""
82
- try:
83
- with torch.inference_mode():
84
- result = model.transcribe(str(clip), output_dir=str(output),
85
- melody_only=bool(melody_only), render_audio=bool(piano),
86
- render_score="pdf,png", dtype="bf16", progress=update)
87
- except Exception as exc:
88
- # Upstream preserves valid MIDI/ABC when notation or rendering fails.
89
- result = getattr(exc, "result", None)
90
- if result is None or not any(output.glob("*.mid")):
91
- raise
92
- logging.exception("Partial transcription")
93
- notice = " Some score rendering failed; available transcription files are included."
94
- progress(0.97, desc="Preparing downloads")
95
- files = sorted(p for p in output.iterdir() if p.is_file() and p.suffix != ".partial")
96
- if not files:
97
- raise RuntimeError("The model returned no output files.")
98
- archive = shutil.make_archive(str(job / "score-downloads"), "zip", output)
99
- pngs = [str(p) for p in sorted(output.glob("score_*.png"))]
100
- pdf = output / "score.pdf"
101
- wavs = sorted(output.glob("*.wav"))
102
- abc_path = output / "score.abc"
103
- abc = abc_path.read_text(encoding="utf-8") if abc_path.exists() else ""
104
- elapsed = round(time.monotonic() - started)
105
- warnings = result.get("warnings", [])
106
- status = f"Finished in {elapsed // 60}m {elapsed % 60}s. Download your files below.{notice}"
107
- if warnings:
108
- status += " The model reported warnings; review result.json in the download."
109
- yield status, pngs, str(pdf) if pdf.exists() else None, str(wavs[0]) if wavs else None, [archive] + [str(p) for p in files], abc
110
- except gr.Error:
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:
118
- gr.Markdown("# Score Studio API\nThe ZeroGPU transcription engine for Score Studio. "
119
- "Use the API below to turn audio into editable ABC, MIDI, PDF and piano audio.")
120
- audio = gr.Audio(type="filepath", label="Audio", sources=["upload"])
121
- start = gr.Number(value=0, label="Start (seconds)", minimum=0)
122
- duration = gr.Number(value=30, label="Duration (seconds)", minimum=5, maximum=600)
123
- melody = gr.Checkbox(value=False, label="Melody only")
124
- piano = gr.Checkbox(value=True, label="Render piano preview")
125
- run = gr.Button("Transcribe")
126
- status = gr.Textbox(label="Status")
127
- gallery = gr.Gallery(label="Score pages")
128
- pdf = gr.File(label="PDF")
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
-
138
  def oauth_metadata():
139
- return {
140
- "client_id": "https://reubencf-score-studio-api.hf.space/.well-known/oauth-cimd",
141
- "client_name": "Score Studio",
142
- "client_uri": "https://huggingface.co/spaces/Reubencf/Score-Studio-API",
143
- "redirect_uris": [
144
- "http://localhost/auth/callback", "http://127.0.0.1/auth/callback",
145
- "https://reubencf-score-studio.hf.space/auth/callback",
146
- ],
147
- "token_endpoint_auth_method": "none",
148
- "grant_types": ["authorization_code"],
149
- "response_types": ["code"],
150
- "scope": "openid profile",
151
- }
152
-
 
 
 
153
  def health():
154
- return {"status": "ready", "model": MODEL_ID, "revision": REVISION}
155
-
156
  if __name__ == "__main__":
157
  # The native Gradio launch hook registers the GPU worker with Spaces.
158
  app, _, _ = demo.queue(max_size=5).launch(
 
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
8
+ import tempfile
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"
18
+ ROOT = Path(tempfile.gettempdir()) / "audio-to-score"
19
+ ROOT.mkdir(exist_ok=True)
20
+ logging.basicConfig(level=logging.INFO)
21
+
22
+ # ZeroGPU's CUDA emulation lets the model be placed during startup; the actual
23
+ # 48 GB GPU is attached only while the decorated conversion function runs.
24
+ MODEL = AutoModel.from_pretrained(
25
+ MODEL_ID, revision=REVISION, trust_remote_code=True,
26
+ torch_dtype=torch.bfloat16,
27
+ ).eval().to("cuda")
28
+
29
+ # The upstream renderer is fully offline but needs its matching headless browser.
30
+ subprocess.run([sys.executable, "-m", "playwright", "install", "--only-shell", "chromium"], check=True)
31
+
32
+
33
  def gpu_seconds(audio, start, duration, melody_only, piano, progress=None, **kwargs):
34
+ # Short excerpts should fit visitor quotas instead of reserving five minutes.
35
+ return max(45, min(300, int(float(duration) * 0.4) + 30))
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:
43
+ raise gr.Error("Choose a start of 0 or later and a duration from 5 to 600 seconds.")
44
+ # Clear the previous run immediately so failed requests cannot show stale scores.
45
+ yield "Preparing your audio…", [], None, None, [], ""
46
+ # Keep the current result until Gradio has copied it to its managed cache.
47
+ # Remove abandoned job directories from earlier runs after one day.
48
+ cutoff = time.time() - 86400
49
+ for old in ROOT.glob("score-*"):
50
+ if old.is_dir() and not old.is_symlink() and old.stat().st_mtime < cutoff:
51
+ shutil.rmtree(old, ignore_errors=True)
52
+ job = Path(tempfile.mkdtemp(prefix="score-", dir=ROOT))
53
+ clip = job / "input.wav"
54
+ output = job / "results"
55
+ output.mkdir()
56
+ try:
57
+ progress(0.03, desc="Preparing audio excerpt")
58
+ completed = subprocess.run(
59
+ ["ffmpeg", "-nostdin", "-v", "error", "-ss", str(start),
60
+ "-i", str(audio), "-t", str(duration), "-ac", "1", "-ar", "24000",
61
+ "-y", str(clip)], capture_output=True, timeout=120,
62
+ )
63
+ if completed.returncode or not clip.exists() or clip.stat().st_size < 2048:
64
+ raise gr.Error("Could not read this excerpt. Check the file and start time.")
65
+ progress(0.06, desc="Loading SheetSage2 — first run takes longer")
66
+ model = MODEL
67
+ started = time.monotonic()
68
+
69
+ def update(event):
70
+ stage = event.get("stage", "working")
71
+ labels = {"audio": "Reading audio", "encoding": "Listening to the music",
72
+ "decoding": "Writing notes and chords", "notation": "Preparing notation",
73
+ "complete": "Rendering printable score"}
74
+ detail = labels.get(stage, "Processing music")
75
+ if "window" in event:
76
+ detail += f" · section {event['window']}/{event['windows']}"
77
+ if "tokens" in event:
78
+ detail += f" · {event['tokens']} symbols"
79
+ progress(None, desc=detail)
80
+
81
+ notice = ""
82
+ try:
83
+ with torch.inference_mode():
84
+ result = model.transcribe(str(clip), output_dir=str(output),
85
+ melody_only=bool(melody_only), render_audio=bool(piano),
86
+ render_score="pdf,png", dtype="bf16", progress=update)
87
+ except Exception as exc:
88
+ # Upstream preserves valid MIDI/ABC when notation or rendering fails.
89
+ result = getattr(exc, "result", None)
90
+ if result is None or not any(output.glob("*.mid")):
91
+ raise
92
+ logging.exception("Partial transcription")
93
+ notice = " Some score rendering failed; available transcription files are included."
94
+ progress(0.97, desc="Preparing downloads")
95
+ files = sorted(p for p in output.iterdir() if p.is_file() and p.suffix != ".partial")
96
+ if not files:
97
+ raise RuntimeError("The model returned no output files.")
98
+ archive = shutil.make_archive(str(job / "score-downloads"), "zip", output)
99
+ pngs = [str(p) for p in sorted(output.glob("score_*.png"))]
100
+ pdf = output / "score.pdf"
101
+ wavs = sorted(output.glob("*.wav"))
102
+ abc_path = output / "score.abc"
103
+ abc = abc_path.read_text(encoding="utf-8") if abc_path.exists() else ""
104
+ elapsed = round(time.monotonic() - started)
105
+ warnings = result.get("warnings", [])
106
+ status = f"Finished in {elapsed // 60}m {elapsed % 60}s. Download your files below.{notice}"
107
+ if warnings:
108
+ status += " The model reported warnings; review result.json in the download."
109
+ yield status, pngs, str(pdf) if pdf.exists() else None, str(wavs[0]) if wavs else None, [archive] + [str(p) for p in files], abc
110
+ except gr.Error:
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:
118
+ gr.Markdown("# Score Studio API\nThe ZeroGPU transcription engine for Score Studio. "
119
+ "Use the API below to turn audio into editable ABC, MIDI, PDF and piano audio.")
120
+ audio = gr.Audio(type="filepath", label="Audio", sources=["upload"])
121
+ start = gr.Number(value=0, label="Start (seconds)", minimum=0)
122
+ duration = gr.Number(value=30, label="Duration (seconds)", minimum=5, maximum=600)
123
+ melody = gr.Checkbox(value=False, label="Melody only")
124
+ piano = gr.Checkbox(value=True, label="Render piano preview")
125
+ run = gr.Button("Transcribe")
126
+ status = gr.Textbox(label="Status")
127
+ gallery = gr.Gallery(label="Score pages")
128
+ pdf = gr.File(label="PDF")
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
+
138
  def oauth_metadata():
139
+ return {
140
+ "client_id": "https://reubencf-score-studio-api.hf.space/.well-known/oauth-cimd",
141
+ "client_name": "Kiku AI",
142
+ "client_uri": "https://huggingface.co/spaces/Reubencf/Score-Studio-API",
143
+ # Every origin that signs users in must be listed here, so the rebranded
144
+ # app and the original Score Studio deployment are both kept valid.
145
+ "redirect_uris": [
146
+ "http://localhost/auth/callback", "http://127.0.0.1/auth/callback",
147
+ "https://reubencf-kiku-ai.hf.space/auth/callback",
148
+ "https://reubencf-score-studio.hf.space/auth/callback",
149
+ ],
150
+ "token_endpoint_auth_method": "none",
151
+ "grant_types": ["authorization_code"],
152
+ "response_types": ["code"],
153
+ "scope": "openid profile",
154
+ }
155
+
156
  def health():
157
+ return {"status": "ready", "model": MODEL_ID, "revision": REVISION}
158
+
159
  if __name__ == "__main__":
160
  # The native Gradio launch hook registers the GPU worker with Spaces.
161
  app, _, _ = demo.queue(max_size=5).launch(