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