harp-dev commited on
Commit
39377cf
·
verified ·
1 Parent(s): 4ed5067

Restore example-based MuseMorphose deployment

Browse files
Files changed (6) hide show
  1. README.md +3 -9
  2. SOURCES.md +1 -1
  3. app.py +11 -29
  4. examples/example_511.remi +0 -83
  5. midi_adapter.py +0 -244
  6. runtime.py +7 -140
README.md CHANGED
@@ -8,20 +8,14 @@ sdk_version: 5.28.0
8
  python_version: '3.12'
9
  app_file: app.py
10
  license: cc-by-4.0
11
- short_description: Transform piano MIDI with rhythm and polyphony
12
  ---
13
 
14
  # MuseMorphose
15
 
16
- MuseMorphose transforms piano music using bar-level rhythmic intensity and polyphonicity controls. **Upload your own `.mid` or `.midi` file**, select up to four bars, adjust the controls, and download the generated MIDI. A compatible REMI event file can be uploaded from the advanced section instead. You can also try one of three built-in examples without uploading anything.
17
 
18
- The official MuseMorphose project does not include a MIDI upload converter. This Space adds a compatibility adapter for ordinary **4/4 MIDI**. It merges all pitched tracks into one piano part, ignores drum tracks, quantizes notes and tempo to the released model vocabulary, and trims empty leading bars. Notes outside pitches 22–107 and excess notes in very dense bars are omitted; the output details report all omissions. Compatible chord markers are retained at their quantized positions; MIDI files without them use the model's unknown-chord token because this adapter does not infer harmony. The downloadable **Model input MIDI** shows the quantized piece sent to the model. This conversion is approximate and can differ from the unreleased training preprocessing. Inference can be slow on CPU.
19
-
20
- Select at most four bars per generation. To choose a later section of a MIDI file, change **Start bar** after uploading; bar 0 is the first bar containing pitched notes.
21
-
22
- ## Advanced REMI input
23
-
24
- Upload a UTF-8 `.remi` or `.txt` file with one MuseMorphose vocabulary token per line, starting with `Bar_None`. A `.json` file may contain an array of those tokens, an `{"events": [...]}` object, or name/value event objects. See [example_511.remi](examples/example_511.remi) for a working input. Files are limited to 1 MB, 10,000 events, 256 bars, and 128 events per bar. Pickle uploads are not accepted.
25
 
26
  The upstream code is MIT licensed. The checkpoint and the REMI Pop Piano Dataset examples are CC BY 4.0; see [SOURCES.md](SOURCES.md) for attribution.
27
 
 
8
  python_version: '3.12'
9
  app_file: app.py
10
  license: cc-by-4.0
11
+ short_description: Explore MuseMorphose on preprocessed piano examples
12
  ---
13
 
14
  # MuseMorphose
15
 
16
+ This Space runs the released MuseMorphose checkpoint on three preprocessed pieces from the REMI Pop Piano Dataset. Choose an example and a range of up to four bars, adjust rhythmic intensity and polyphonicity, and download the reference and generated MIDI files. Inference can be slow on CPU.
17
 
18
+ The model operates on REMI event tokens produced by the original preprocessing pipeline. The official release demonstrates generation from preprocessed dataset pieces; it does not provide an interface for arbitrary MIDI or REMI uploads. Converting a new MIDI into compatible tokens would require a separate preprocessing pipeline and is outside this model deployment.
 
 
 
 
 
 
19
 
20
  The upstream code is MIT licensed. The checkpoint and the REMI Pop Piano Dataset examples are CC BY 4.0; see [SOURCES.md](SOURCES.md) for attribution.
21
 
SOURCES.md CHANGED
@@ -5,4 +5,4 @@
5
  - Three example REMI pieces: [REMI Pop Piano Dataset](https://doi.org/10.5281/zenodo.4782721) by Shih-Lun Wu, Wen-Yi Hsiao, and Yi-Hsuan Yang (CC BY 4.0). Archive SHA-256 `b3e38e03f2f979826f6b5dc700fcad2f9b5d19f837308c5050825e14cf043802`. The vocabulary is from the official MuseMorphose repository.
6
  - Paper: https://arxiv.org/abs/2105.04090.
7
 
8
- The official release provides generation from preprocessed REMI dataset pieces. It does not provide conversion of arbitrary uploaded MIDI files to its training representation. This endpoint adds an approximate MIDI-to-REMI compatibility adapter based on the released MuseMorphose vocabulary and event ordering, using `miditoolkit` to read MIDI. The adapter is our implementation; no code from the GPL-licensed [YatingMusic REMI repository](https://github.com/YatingMusic/remi) was copied. Advanced UTF-8 REMI event uploads and three official example pieces remain supported.
 
5
  - Three example REMI pieces: [REMI Pop Piano Dataset](https://doi.org/10.5281/zenodo.4782721) by Shih-Lun Wu, Wen-Yi Hsiao, and Yi-Hsuan Yang (CC BY 4.0). Archive SHA-256 `b3e38e03f2f979826f6b5dc700fcad2f9b5d19f837308c5050825e14cf043802`. The vocabulary is from the official MuseMorphose repository.
6
  - Paper: https://arxiv.org/abs/2105.04090.
7
 
8
+ The official release provides generation from preprocessed REMI dataset pieces. It does not provide an interface for arbitrary MIDI or REMI uploads. This Space wraps the released checkpoint and three dataset examples in a Gradio interface, with a four-bar generation limit for its CPU demo.
app.py CHANGED
@@ -17,9 +17,7 @@ model_card = ModelCard(
17
 
18
 
19
  def process_fn(
20
- midi_path,
21
  sample_name,
22
- remi_path,
23
  start_bar,
24
  bars,
25
  rhythm_shift,
@@ -40,8 +38,6 @@ def process_fn(
40
  top_p,
41
  seed,
42
  Path(tmp),
43
- midi_path=midi_path,
44
- remi_path=remi_path,
45
  )
46
  return (
47
  outputs[0].move_resource_to_block_cache(reference),
@@ -53,30 +49,18 @@ def process_fn(
53
 
54
 
55
  with gr.Blocks(title="MuseMorphose", delete_cache=(3600, 86400)) as demo:
56
- gr.Markdown("## Transform your own MIDI")
 
 
 
 
57
  inputs = [
58
- gr.File(
59
- type="filepath",
60
- file_types=[".mid", ".midi"],
61
- label="Upload your MIDI (.mid or .midi)",
62
- height=140,
63
- ).set_info(
64
- "Pitched tracks are merged into one piano part; drum tracks are ignored."
65
  ),
66
  ]
67
- with gr.Accordion("Advanced inputs: examples or REMI", open=False):
68
- example_input = gr.Dropdown(
69
- choices=list(EXAMPLES),
70
- value=None,
71
- label="Built-in example (optional)",
72
- ).set_info("Used when no file is uploaded.")
73
- remi_input = gr.File(
74
- type="filepath",
75
- file_types=[".remi", ".txt", ".json"],
76
- label="Upload REMI events (optional)",
77
- height=100,
78
- ).set_info("Use a compatible UTF-8 MuseMorphose event list; no pickle files.")
79
- inputs.extend([example_input, remi_input])
80
  inputs.extend(
81
  [
82
  gr.Number(value=0, minimum=0, maximum=9999, precision=0, label="Start bar"),
@@ -93,7 +77,7 @@ with gr.Blocks(title="MuseMorphose", delete_cache=(3600, 86400)) as demo:
93
  ]
94
  )
95
  outputs = [
96
- gr.File(label="Model input MIDI", file_types=[".mid"]),
97
  gr.File(label="Transformed MIDI", file_types=[".mid"]),
98
  gr.JSON(label="Generation details"),
99
  ]
@@ -106,6 +90,4 @@ with gr.Blocks(title="MuseMorphose", delete_cache=(3600, 86400)) as demo:
106
 
107
 
108
  if __name__ == "__main__":
109
- demo.queue(default_concurrency_limit=1, max_size=8).launch(
110
- show_error=True, max_file_size="10mb"
111
- )
 
17
 
18
 
19
  def process_fn(
 
20
  sample_name,
 
21
  start_bar,
22
  bars,
23
  rhythm_shift,
 
38
  top_p,
39
  seed,
40
  Path(tmp),
 
 
41
  )
42
  return (
43
  outputs[0].move_resource_to_block_cache(reference),
 
49
 
50
 
51
  with gr.Blocks(title="MuseMorphose", delete_cache=(3600, 86400)) as demo:
52
+ gr.Markdown("## MuseMorphose piano examples")
53
+ gr.Markdown(
54
+ "Choose a preprocessed piano example, adjust its rhythmic intensity or "
55
+ "polyphonicity, and generate a new MIDI performance."
56
+ )
57
  inputs = [
58
+ gr.Dropdown(
59
+ choices=list(EXAMPLES),
60
+ value="Piano example 511",
61
+ label="Preprocessed piano example",
 
 
 
62
  ),
63
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  inputs.extend(
65
  [
66
  gr.Number(value=0, minimum=0, maximum=9999, precision=0, label="Start bar"),
 
77
  ]
78
  )
79
  outputs = [
80
+ gr.File(label="Reference MIDI", file_types=[".mid"]),
81
  gr.File(label="Transformed MIDI", file_types=[".mid"]),
82
  gr.JSON(label="Generation details"),
83
  ]
 
90
 
91
 
92
  if __name__ == "__main__":
93
+ demo.queue(default_concurrency_limit=1, max_size=8).launch(show_error=True)
 
 
examples/example_511.remi DELETED
@@ -1,83 +0,0 @@
1
- Bar_None
2
- Beat_0
3
- Chord_N_N
4
- Tempo_119
5
- Note_Pitch_67
6
- Note_Velocity_58
7
- Note_Duration_1920
8
- Note_Pitch_64
9
- Note_Velocity_58
10
- Note_Duration_1920
11
- Note_Pitch_62
12
- Note_Velocity_56
13
- Note_Duration_1920
14
- Note_Pitch_59
15
- Note_Velocity_54
16
- Note_Duration_1920
17
- Beat_12
18
- Tempo_140
19
- Bar_None
20
- Beat_0
21
- Chord_D_m7
22
- Tempo_146
23
- Note_Pitch_81
24
- Note_Velocity_66
25
- Note_Duration_1920
26
- Note_Pitch_69
27
- Note_Velocity_60
28
- Note_Duration_1920
29
- Note_Pitch_65
30
- Note_Velocity_50
31
- Note_Duration_1920
32
- Note_Pitch_62
33
- Note_Velocity_56
34
- Note_Duration_1920
35
- Note_Pitch_60
36
- Note_Velocity_52
37
- Note_Duration_1920
38
- Beat_4
39
- Tempo_149
40
- Beat_8
41
- Tempo_143
42
- Beat_12
43
- Tempo_140
44
- Bar_None
45
- Beat_0
46
- Chord_E_m7
47
- Tempo_149
48
- Note_Pitch_67
49
- Note_Velocity_58
50
- Note_Duration_1920
51
- Note_Pitch_64
52
- Note_Velocity_54
53
- Note_Duration_1920
54
- Note_Pitch_62
55
- Note_Velocity_58
56
- Note_Duration_1920
57
- Note_Pitch_59
58
- Note_Velocity_56
59
- Note_Duration_1920
60
- Beat_4
61
- Tempo_143
62
- Beat_12
63
- Tempo_140
64
- Bar_None
65
- Beat_0
66
- Chord_D_m7
67
- Tempo_146
68
- Note_Pitch_81
69
- Note_Velocity_66
70
- Note_Duration_1920
71
- Note_Pitch_69
72
- Note_Velocity_62
73
- Note_Duration_1920
74
- Note_Pitch_65
75
- Note_Velocity_56
76
- Note_Duration_1920
77
- Note_Pitch_60
78
- Note_Velocity_52
79
- Note_Duration_1920
80
- Beat_4
81
- Tempo_143
82
- Beat_12
83
- Tempo_140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
midi_adapter.py DELETED
@@ -1,244 +0,0 @@
1
- """Convert ordinary 4/4 MIDI into the MuseMorphose checkpoint's REMI vocabulary.
2
-
3
- This is a compatibility adapter, not the unavailable MuseMorphose training
4
- preprocessor. It merges pitched tracks into one piano part and quantizes notes
5
- to the model's 16th-note grid and released vocabulary.
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from collections import defaultdict
11
- from pathlib import Path
12
-
13
- import miditoolkit
14
-
15
- TICKS_PER_BEAT = 480
16
- GRID = 120
17
- TICKS_PER_BAR = 1920
18
- MAX_MIDI_BYTES = 10_000_000
19
- MAX_BARS = 256
20
-
21
-
22
- def _nearest(value: float, options: list[int]) -> int:
23
- return min(options, key=lambda candidate: (abs(candidate - value), candidate))
24
-
25
-
26
- def _grid_tick(tick: int) -> int:
27
- return max(0, ((tick + GRID // 2) // GRID) * GRID)
28
-
29
-
30
- def _numeric_vocab(event2idx: dict[str, int], prefix: str) -> list[int]:
31
- return sorted(
32
- int(token.removeprefix(prefix))
33
- for token in event2idx
34
- if token.startswith(prefix)
35
- )
36
-
37
-
38
- def midi_to_events(path: str | Path, event2idx: dict[str, int]):
39
- """Return bar offsets, REMI events, and conversion details for one MIDI."""
40
- path = Path(path)
41
- if path.suffix.lower() not in {".mid", ".midi"}:
42
- raise ValueError("Upload a .mid or .midi file.")
43
- if path.stat().st_size > MAX_MIDI_BYTES:
44
- raise ValueError("MIDI files must be at most 10 MB.")
45
- try:
46
- midi = miditoolkit.MidiFile(str(path))
47
- except Exception as exc:
48
- raise ValueError(f"Could not read this MIDI file: {exc}") from exc
49
- if midi.ticks_per_beat <= 0:
50
- raise ValueError("The MIDI file has an invalid tick resolution.")
51
- for signature in midi.time_signature_changes:
52
- if (signature.numerator, signature.denominator) != (4, 4):
53
- raise ValueError(
54
- "MuseMorphose MIDI upload currently supports only 4/4 time."
55
- )
56
-
57
- pitches = _numeric_vocab(event2idx, "Note_Pitch_")
58
- velocities = _numeric_vocab(event2idx, "Note_Velocity_")
59
- durations = [
60
- value for value in _numeric_vocab(event2idx, "Note_Duration_") if value > 0
61
- ]
62
- tempos = _numeric_vocab(event2idx, "Tempo_")
63
- pitch_min, pitch_max = min(pitches), max(pitches)
64
- scale = TICKS_PER_BEAT / midi.ticks_per_beat
65
-
66
- pitched_tracks = [track for track in midi.instruments if not track.is_drum]
67
- ignored_drum_tracks = sum(track.is_drum for track in midi.instruments)
68
- if not pitched_tracks:
69
- raise ValueError(
70
- "The MIDI file contains no pitched notes; drum-only files are unsupported."
71
- )
72
-
73
- raw_notes = [note for track in pitched_tracks for note in track.notes]
74
- if not raw_notes:
75
- raise ValueError("The MIDI file contains no pitched notes.")
76
- supported = []
77
- omitted_pitch = 0
78
- omitted_invalid = 0
79
- for note in raw_notes:
80
- if not pitch_min <= note.pitch <= pitch_max:
81
- omitted_pitch += 1
82
- continue
83
- if note.end <= note.start:
84
- omitted_invalid += 1
85
- continue
86
- start = _grid_tick(round(note.start * scale))
87
- length = _nearest((note.end - note.start) * scale, durations)
88
- supported.append(
89
- (start, note.pitch, _nearest(note.velocity, velocities), length)
90
- )
91
- if not supported:
92
- raise ValueError(
93
- f"No supported piano notes remain (pitch range {pitch_min}–{pitch_max})."
94
- )
95
-
96
- leading_bars = min(note[0] for note in supported) // TICKS_PER_BAR
97
- offset = leading_bars * TICKS_PER_BAR
98
- unique_notes = {}
99
- for start, pitch, velocity, duration in supported:
100
- key = (start - offset, pitch)
101
- candidate = (start - offset, pitch, velocity, duration)
102
- previous = unique_notes.get(key)
103
- if previous is None or (duration, velocity) > (previous[3], previous[2]):
104
- unique_notes[key] = candidate
105
- notes = sorted(unique_notes.values(), key=lambda item: (item[0], -item[1]))
106
- n_bars = notes[-1][0] // TICKS_PER_BAR + 1
107
- if n_bars > MAX_BARS:
108
- raise ValueError(
109
- f"The MIDI has more than {MAX_BARS} bars. Upload a shorter excerpt."
110
- )
111
-
112
- # Keep source tempo changes in beat time, then repeat the active tempo at
113
- # each bar start so the excerpt is self-contained when users select bars.
114
- raw_tempos = sorted(midi.tempo_changes, key=lambda change: change.time)
115
- active_bpm = 120.0
116
- tempo_at_step = {}
117
- for change in raw_tempos:
118
- tick = _grid_tick(round(change.time * scale))
119
- if tick <= offset:
120
- active_bpm = change.tempo
121
- elif tick - offset < n_bars * TICKS_PER_BAR:
122
- tempo_at_step[tick - offset] = change.tempo
123
- tempo_at_step[0] = active_bpm
124
- for bar in range(n_bars):
125
- bar_start = bar * TICKS_PER_BAR
126
- candidates = [position for position in tempo_at_step if position <= bar_start]
127
- if candidates:
128
- tempo_at_step[bar_start] = tempo_at_step[max(candidates)]
129
-
130
- chord_at_step = {}
131
- active_chord = "Chord_N_N"
132
- for marker in sorted(midi.markers, key=lambda item: item.time):
133
- label = marker.text
134
- if label.startswith("Chord-"):
135
- chord = "Chord_" + label.removeprefix("Chord-")
136
- elif label.startswith("Chord_"):
137
- chord = label
138
- else:
139
- continue
140
- if chord in event2idx:
141
- tick = _grid_tick(round(marker.time * scale))
142
- if tick <= offset:
143
- active_chord = chord
144
- elif tick - offset < n_bars * TICKS_PER_BAR:
145
- chord_at_step[tick - offset] = chord
146
- chord_at_step[0] = active_chord
147
- for bar in range(n_bars):
148
- bar_start = bar * TICKS_PER_BAR
149
- candidates = [position for position in chord_at_step if position <= bar_start]
150
- if candidates:
151
- chord_at_step[bar_start] = chord_at_step[max(candidates)]
152
-
153
- notes_by_bar = defaultdict(list)
154
- for note in notes:
155
- notes_by_bar[note[0] // TICKS_PER_BAR].append(note)
156
- dropped_for_density = 0
157
- events = []
158
- bar_positions = []
159
-
160
- def build_bar(bar: int, bar_notes: list[tuple[int, int, int, int]]):
161
- bar_start = bar * TICKS_PER_BAR
162
- beat_notes = defaultdict(list)
163
- for note in bar_notes:
164
- beat_notes[(note[0] - bar_start) // GRID].append(note)
165
- beat_tempos = {
166
- (tick - bar_start) // GRID: _nearest(bpm, tempos)
167
- for tick, bpm in tempo_at_step.items()
168
- if bar_start <= tick < bar_start + TICKS_PER_BAR
169
- }
170
- beat_chords = {
171
- (tick - bar_start) // GRID: chord
172
- for tick, chord in chord_at_step.items()
173
- if bar_start <= tick < bar_start + TICKS_PER_BAR
174
- }
175
- result = [{"name": "Bar", "value": None}]
176
- for beat in sorted({0, *beat_notes, *beat_tempos, *beat_chords}):
177
- result.append({"name": "Beat", "value": beat})
178
- if beat in beat_chords:
179
- result.append(
180
- {
181
- "name": "Chord",
182
- "value": beat_chords[beat].removeprefix("Chord_"),
183
- }
184
- )
185
- if beat in beat_tempos:
186
- result.append({"name": "Tempo", "value": beat_tempos[beat]})
187
- for _, pitch, velocity, duration in sorted(
188
- beat_notes[beat], key=lambda note: -note[1]
189
- ):
190
- result.extend(
191
- (
192
- {"name": "Note_Pitch", "value": pitch},
193
- {"name": "Note_Velocity", "value": velocity},
194
- {"name": "Note_Duration", "value": duration},
195
- )
196
- )
197
- return result
198
-
199
- for bar in range(n_bars):
200
- selected = list(notes_by_bar[bar])
201
- bar_events = build_bar(bar, selected)
202
- if len(bar_events) > 128:
203
- strongest = sorted(
204
- selected,
205
- key=lambda note: (note[2] * note[3], note[0], note[1]),
206
- reverse=True,
207
- )
208
- low, high = 0, len(strongest)
209
- while low < high:
210
- keep = (low + high + 1) // 2
211
- candidate = build_bar(bar, strongest[:keep])
212
- if len(candidate) <= 128:
213
- low = keep
214
- else:
215
- high = keep - 1
216
- dropped_for_density += len(selected) - low
217
- selected = strongest[:low]
218
- bar_events = build_bar(bar, selected)
219
- if len(bar_events) > 128:
220
- raise ValueError(
221
- f"Bar {bar + 1} is too dense for the 128-event encoder limit."
222
- )
223
- bar_positions.append(len(events))
224
- events.extend(bar_events)
225
-
226
- for position, event in enumerate(events, start=1):
227
- token = f"{event['name']}_{event['value']}"
228
- if token not in event2idx:
229
- raise ValueError(
230
- f"MIDI conversion produced an unsupported event at position {position}: {token}"
231
- )
232
- details = {
233
- "source_tracks_merged": len(pitched_tracks),
234
- "ignored_drum_tracks": ignored_drum_tracks,
235
- "source_notes": len(raw_notes),
236
- "encoded_notes": len(notes) - dropped_for_density,
237
- "omitted_out_of_range_notes": omitted_pitch,
238
- "omitted_invalid_notes": omitted_invalid,
239
- "omitted_duplicate_notes": len(supported) - len(notes),
240
- "omitted_for_density": dropped_for_density,
241
- "trimmed_leading_bars": leading_bars,
242
- "available_bars": n_bars,
243
- }
244
- return bar_positions, events, details
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
runtime.py CHANGED
@@ -1,8 +1,7 @@
1
- """Inference adapter for the official MuseMorphose checkpoint and REMI events."""
2
 
3
  from __future__ import annotations
4
 
5
- import json
6
  import pickle
7
  import random
8
  import sys
@@ -16,7 +15,6 @@ import torch
16
  ROOT = Path(__file__).resolve().parent
17
  sys.path.insert(0, str(ROOT / "model")) # Official model uses absolute sibling imports.
18
 
19
- from midi_adapter import midi_to_events
20
  from model.musemorphose import MuseMorphose
21
  from remi2midi import remi2midi
22
 
@@ -28,16 +26,6 @@ EXAMPLES = {
28
  "Piano example 1719": 1719,
29
  "Piano example 144": 144,
30
  }
31
- MAX_REMI_BYTES = 1_000_000
32
- EVENT_PREFIXES = (
33
- "Note_Pitch_",
34
- "Note_Velocity_",
35
- "Note_Duration_",
36
- "Beat_",
37
- "Tempo_",
38
- "Chord_",
39
- "Bar_",
40
- )
41
  RHYTHM_BOUNDS = [0.2, 0.25, 0.32, 0.38, 0.44, 0.5, 0.63]
42
  POLYPHONY_BOUNDS = [2.63, 3.06, 3.50, 4.00, 4.63, 5.44, 6.44]
43
 
@@ -76,107 +64,6 @@ def _event_name(event):
76
  return f"{event['name']}_{event['value']}"
77
 
78
 
79
- def _parse_event(token):
80
- for prefix in EVENT_PREFIXES:
81
- if token.startswith(prefix):
82
- value = token[len(prefix) :]
83
- return {
84
- "name": prefix[:-1],
85
- "value": None if token == "Bar_None" else value,
86
- }
87
- raise ValueError(f"Unsupported REMI event: {token}")
88
-
89
-
90
- def load_uploaded_events(path, event2idx):
91
- """Read text/JSON events without deserializing untrusted pickle files."""
92
- path = Path(path)
93
- if path.suffix.lower() not in {".remi", ".txt", ".json"}:
94
- raise ValueError("Upload a .remi, .txt, or .json event file.")
95
- if path.stat().st_size > MAX_REMI_BYTES:
96
- raise ValueError("REMI files must be at most 1 MB.")
97
- try:
98
- content = path.read_text(encoding="utf-8-sig")
99
- except UnicodeError as exc:
100
- raise ValueError("The REMI file must use UTF-8 text.") from exc
101
- if path.suffix.lower() == ".json":
102
- try:
103
- data = json.loads(content)
104
- except json.JSONDecodeError as exc:
105
- raise ValueError(f"Invalid JSON at line {exc.lineno}.") from exc
106
- if isinstance(data, dict):
107
- data = data.get("events")
108
- if not isinstance(data, list):
109
- raise ValueError(
110
- 'JSON must be an event list or an object with an "events" list.'
111
- )
112
- tokens = []
113
- for item in data:
114
- if isinstance(item, str):
115
- tokens.append(item)
116
- elif isinstance(item, dict) and isinstance(item.get("name"), str):
117
- tokens.append(_event_name(item))
118
- else:
119
- raise TypeError(
120
- "Each JSON event must be a token string or name/value object."
121
- )
122
- else:
123
- tokens = [line.strip() for line in content.splitlines() if line.strip()]
124
- if not tokens or len(tokens) > 10_000:
125
- raise ValueError("The REMI file must contain 1–10,000 events.")
126
- if tokens[0] != "Bar_None":
127
- raise ValueError("The first event must be Bar_None.")
128
- for position, token in enumerate(tokens, start=1):
129
- if (
130
- not isinstance(token, str)
131
- or token not in event2idx
132
- or not token.startswith(EVENT_PREFIXES)
133
- ):
134
- raise ValueError(
135
- f"Unknown or unsupported event at position {position}: {token}"
136
- )
137
- positions = [i for i, token in enumerate(tokens) if token == "Bar_None"]
138
- if len(positions) > 256:
139
- raise ValueError("REMI files must have at most 256 bars.")
140
- for bar_number, (left, right) in enumerate(
141
- zip(positions, positions[1:] + [len(tokens)]), start=1
142
- ):
143
- if right - left > 128:
144
- raise ValueError(f"Bar {bar_number} has more than 128 events.")
145
- beat = -1
146
- for i in range(left + 1, right):
147
- token = tokens[i]
148
- if token.startswith("Beat_"):
149
- next_beat = int(token.removeprefix("Beat_"))
150
- if next_beat < beat:
151
- raise ValueError(
152
- f"Beat positions must increase within bar {bar_number}."
153
- )
154
- beat = next_beat
155
- elif beat < 0:
156
- raise ValueError(
157
- f"Bar {bar_number} must have a Beat event before musical events."
158
- )
159
- if token.startswith("Note_Pitch_") and (
160
- i + 2 >= right
161
- or not tokens[i + 1].startswith("Note_Velocity_")
162
- or not tokens[i + 2].startswith("Note_Duration_")
163
- ):
164
- raise ValueError(
165
- f"Bar {bar_number} has a note without velocity and duration events."
166
- )
167
- if token.startswith("Note_Velocity_") and (
168
- i == left or not tokens[i - 1].startswith("Note_Pitch_")
169
- ):
170
- raise ValueError(f"Bar {bar_number} has an orphan note velocity event.")
171
- if token.startswith("Note_Duration_") and (
172
- i < left + 2
173
- or not tokens[i - 2].startswith("Note_Pitch_")
174
- or not tokens[i - 1].startswith("Note_Velocity_")
175
- ):
176
- raise ValueError(f"Bar {bar_number} has an orphan note duration event.")
177
- return positions, [_parse_event(token) for token in tokens]
178
-
179
-
180
  def _classes(events, n_bars):
181
  poly = np.zeros((n_bars * 16,), dtype=np.float32)
182
  rhythm = np.zeros_like(poly)
@@ -252,13 +139,9 @@ def generate(
252
  top_p,
253
  seed,
254
  output_dir,
255
- midi_path=None,
256
- remi_path=None,
257
  ):
258
- if midi_path and remi_path:
259
- raise ValueError("Upload either MIDI or REMI, not both.")
260
- if not midi_path and not remi_path and sample_name not in EXAMPLES:
261
- raise ValueError("Choose an example or upload a MIDI or REMI file.")
262
  n_bars, start_bar, seed = int(n_bars), int(start_bar), int(seed)
263
  if n_bars > 4:
264
  raise ValueError("Choose at most four bars per request.")
@@ -270,24 +153,10 @@ def generate(
270
  random.seed(seed)
271
  np.random.seed(seed)
272
  torch.manual_seed(seed)
273
- conversion = None
274
- if midi_path:
275
- positions, all_events, conversion = midi_to_events(midi_path, event2idx)
276
- events, bars, rhythm, poly = _piece(
277
- positions, all_events, start_bar, n_bars, event2idx
278
- )
279
- source = Path(midi_path).name
280
- elif remi_path:
281
- positions, all_events = load_uploaded_events(remi_path, event2idx)
282
- events, bars, rhythm, poly = _piece(
283
- positions, all_events, start_bar, n_bars, event2idx
284
- )
285
- source = Path(remi_path).name
286
- else:
287
- events, bars, rhythm, poly = _sample(
288
- EXAMPLES[sample_name], start_bar, n_bars, event2idx
289
- )
290
- source = sample_name
291
  lengths = [len(bar) for bar in bars]
292
  encoder = torch.full((128, n_bars), len(event2idx), dtype=torch.long)
293
  mask = torch.ones((n_bars, 128), dtype=torch.bool)
@@ -374,6 +243,4 @@ def generate(
374
  "generated_events": len(tokens),
375
  "generated_notes": saved_notes,
376
  }
377
- if conversion is not None:
378
- details["midi_conversion"] = conversion
379
  return str(reference_path), str(output_path), details
 
1
+ """Inference adapter for the official checkpoint and preprocessed REMI examples."""
2
 
3
  from __future__ import annotations
4
 
 
5
  import pickle
6
  import random
7
  import sys
 
15
  ROOT = Path(__file__).resolve().parent
16
  sys.path.insert(0, str(ROOT / "model")) # Official model uses absolute sibling imports.
17
 
 
18
  from model.musemorphose import MuseMorphose
19
  from remi2midi import remi2midi
20
 
 
26
  "Piano example 1719": 1719,
27
  "Piano example 144": 144,
28
  }
 
 
 
 
 
 
 
 
 
 
29
  RHYTHM_BOUNDS = [0.2, 0.25, 0.32, 0.38, 0.44, 0.5, 0.63]
30
  POLYPHONY_BOUNDS = [2.63, 3.06, 3.50, 4.00, 4.63, 5.44, 6.44]
31
 
 
64
  return f"{event['name']}_{event['value']}"
65
 
66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def _classes(events, n_bars):
68
  poly = np.zeros((n_bars * 16,), dtype=np.float32)
69
  rhythm = np.zeros_like(poly)
 
139
  top_p,
140
  seed,
141
  output_dir,
 
 
142
  ):
143
+ if sample_name not in EXAMPLES:
144
+ raise ValueError("Choose a preprocessed piano example.")
 
 
145
  n_bars, start_bar, seed = int(n_bars), int(start_bar), int(seed)
146
  if n_bars > 4:
147
  raise ValueError("Choose at most four bars per request.")
 
153
  random.seed(seed)
154
  np.random.seed(seed)
155
  torch.manual_seed(seed)
156
+ events, bars, rhythm, poly = _sample(
157
+ EXAMPLES[sample_name], start_bar, n_bars, event2idx
158
+ )
159
+ source = sample_name
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  lengths = [len(bar) for bar in bars]
161
  encoder = torch.full((128, n_bars), len(event2idx), dtype=torch.long)
162
  mask = torch.ones((n_bars, 128), dtype=torch.bool)
 
243
  "generated_events": len(tokens),
244
  "generated_notes": saved_notes,
245
  }
 
 
246
  return str(reference_path), str(output_path), details