Spaces:
Running
Running
Restore example-based MuseMorphose deployment
Browse files- README.md +3 -9
- SOURCES.md +1 -1
- app.py +11 -29
- examples/example_511.remi +0 -83
- midi_adapter.py +0 -244
- 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:
|
| 12 |
---
|
| 13 |
|
| 14 |
# MuseMorphose
|
| 15 |
|
| 16 |
-
|
| 17 |
|
| 18 |
-
The
|
| 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
|
|
|
|
| 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("##
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
inputs = [
|
| 58 |
-
gr.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
label="
|
| 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="
|
| 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
|
| 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
|
| 259 |
-
raise ValueError("
|
| 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 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 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
|