MuScriptor / app.py
Vansh Chugh
clearer info text for instruments and temperature controls
9e7aa57
Raw
History Blame Contribute Delete
5.51 kB
import sys
sys.stdout.reconfigure(line_buffering=True)
try:
import spaces
except ImportError:
# keep @spaces.GPU usable as a no-op; ZeroGPU requires this exact name.
class spaces:
class GPU:
def __init__(self, func=None, duration=60):
self.func = func
def __call__(self, *args, **kwargs):
if self.func is not None:
return self.func(*args, **kwargs)
func = args[0]
return func
import tempfile
from pathlib import Path
import gradio as gr
import torch
from pyharp import ModelCard, build_endpoint
from muscriptor import TranscriptionModel
from muscriptor.tokenizer.mt3 import MT3_FULL_PLUS_GROUP_NAMES, resolve_instrument_names
model_card = ModelCard(
name="MuScriptor",
description=(
"Multi-instrument audio-to-MIDI transcription, trained on 170k songs "
"from classical music to heavy metal."
),
author="Simon Rouard, Michael Krause, Axel Roebel, Carl-Johann Simon-Gabriel, Alexandre Défossez (Kyutai x Mirelo)",
tags=["transcription", "midi", "multi-instrument"],
)
_VALID_INSTRUMENTS = ", ".join(MT3_FULL_PLUS_GROUP_NAMES)
_models: dict[str, TranscriptionModel] = {}
def _get_model(variant: str) -> TranscriptionModel:
"""Load and cache a TranscriptionModel for a given size, one per variant.
small/medium run on CPU; large runs on GPU. Built directly on its target
device inside this GPU-decorated call, per ZeroGPU rules.
"""
if variant not in _models:
device = "cuda" if variant == "large" else "cpu"
_models[variant] = TranscriptionModel.load_model(variant, device=device)
return _models[variant]
def _resolve_instruments(text: str) -> tuple[list[str] | None, str]:
"""Resolve the comma-separated instruments box into exact group names.
Names that don't resolve are dropped rather than raised: HARP's client
shows generic error message, so an error here would not reach the user.
Second value returns what happened as a .txt output
"""
tokens = [t for t in text.split(",") if t.strip()]
if not tokens:
return None, "No instrument restriction requested."
resolved: list[str] = []
problems: list[str] = []
for token in tokens:
try:
resolved.extend(resolve_instrument_names([token]))
except ValueError as e:
problems.append(str(e))
if not problems:
return resolved, f"Matched: {', '.join(resolved)}."
note = "; ".join(problems)
if resolved:
note = f"Matched: {', '.join(resolved)}. Ignored: {note}"
else:
note = f"No valid instrument names found, transcribing unrestricted. {note}"
return (resolved or None), note
@spaces.GPU
@torch.inference_mode()
def process_fn(
input_audio_path: str,
variant: str,
instruments_text: str,
use_sampling: bool,
temperature: float,
) -> tuple[str, str]:
"""Transcribe the input audio to MIDI, plus a note on the Instruments field."""
model = _get_model(variant)
instruments, instrument_note = _resolve_instruments(instruments_text)
midi_bytes = model.transcribe_to_midi(
input_audio_path,
use_sampling=use_sampling,
temperature=temperature,
instruments=instruments,
)
with tempfile.NamedTemporaryFile(suffix=".mid", delete=False) as f:
f.write(midi_bytes)
output_midi_path = f.name
notes_text = f"{Path(input_audio_path).name}\n\nInstrument Matching\n{instrument_note}\n"
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
f.write(notes_text)
output_notes_path = f.name
return output_midi_path, output_notes_path
with gr.Blocks() as demo:
input_components = [
gr.Audio(type="filepath", label="Input Audio").harp_required(True),
gr.Dropdown(
choices=["small", "medium", "large"],
value="medium",
label="Model Size",
info="small = fastest, least accurate. medium = balanced (default, per repo). large = most accurate, slower.",
),
gr.Textbox(
value="",
label="Instruments",
info=f"Separate names with commas. Leave blank to let the model detect instruments on its own. Valid names: {_VALID_INSTRUMENTS}.",
),
gr.Checkbox(
label="Use Sampling",
info="Temperature sampling instead of greedy decoding (default: False, per repo).",
),
gr.Slider(
minimum=0.1,
maximum=2.0,
step=0.1,
value=1.0,
label="Temperature",
info="Only used when Use Sampling is on. Higher values = note choices are more varied and unpredictable. Lower values = values are closer to model's most confident guesses. (default: 1.0, per repo).",
),
]
output_components = [
gr.File(type="filepath", file_types=[".mid", ".midi"], label="Output MIDI").set_info(
"Transcribed MIDI notes."
),
gr.File(type="filepath", file_types=[".txt"], label="Instrument Matching").set_info(
"Which requested instrument names were matched or ignored."
),
]
build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)
if __name__ == "__main__":
demo.queue().launch(pwa=True)