File size: 6,146 Bytes
fed4376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db50e80
fed4376
 
 
783f44a
 
fed4376
 
db50e80
fed4376
 
 
 
699a615
 
 
 
 
fed4376
 
903f134
fed4376
 
db50e80
 
fed4376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db50e80
 
fed4376
db50e80
 
 
fed4376
 
 
db50e80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fed4376
 
 
 
 
 
 
 
 
 
db50e80
 
fed4376
db50e80
fed4376
 
 
 
 
 
 
 
 
 
 
 
db50e80
 
 
 
 
 
fed4376
 
 
3ec01ff
 
b8edec1
 
 
 
3ec01ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
02a1d0f
3ec01ff
 
 
 
 
 
 
 
 
 
 
 
 
 
968d1a0
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
168
169
170
171
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 pyharp.tags import Subcategory

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. The code is MIT, but the "
        "published weights are CC-BY-NC-4.0, so results cannot be used "
        "commercially. Repository: https://github.com/muscriptor/muscriptor "
        "Paper: https://arxiv.org/abs/2607.08168"
    ),
    author="Simon Rouard, Michael Krause, Axel Roebel, Carl-Johann Simon-Gabriel, Alexandre Défossez (Kyutai x Mirelo)",
    tags=[Subcategory.MUSIC_ANALYSIS, '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


if __name__ == "__main__":
    with gr.Blocks() as demo:
        input_components = [
            gr.Audio(type="filepath", label="Input Audio").harp_required(True).set_info((
                "Music to transcribe. Trained on a wide range of material, from "
                "classical to metal, and it names the instruments it hears."
            )),
            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="Transcription").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,
        )

    demo.queue().launch(show_error=True, pwa=True)