Spaces:
Running on Zero
Running on Zero
| 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 | |
| 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) | |