File size: 5,512 Bytes
fed4376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db50e80
fed4376
 
 
 
 
 
db50e80
fed4376
 
 
 
 
 
 
 
 
 
 
db50e80
 
fed4376
 
 
 
 
 
 
 
 
 
 
 
 
 
 
db50e80
 
fed4376
db50e80
 
 
fed4376
 
 
db50e80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fed4376
 
 
 
 
 
 
 
 
 
db50e80
 
fed4376
db50e80
fed4376
 
 
 
 
 
 
 
 
 
 
 
db50e80
 
 
 
 
 
fed4376
 
 
 
 
 
 
 
 
 
 
 
 
 
9e7aa57
fed4376
 
 
 
 
 
 
 
 
 
 
9e7aa57
fed4376
 
 
 
 
 
db50e80
 
 
fed4376
 
 
 
 
 
 
 
 
 
 
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
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)