Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files
app.py
CHANGED
|
@@ -67,6 +67,7 @@ import partitura
|
|
| 67 |
|
| 68 |
from drum_dynamics.serve.models import Engine
|
| 69 |
from drum_dynamics.viz.playback import set_soundfont, _ensure_fluidsynth_discoverable
|
|
|
|
| 70 |
|
| 71 |
# --- load once at module scope ------------------------------------------------
|
| 72 |
ENGINE = Engine.load(os.path.join(HERE, "models"))
|
|
@@ -97,8 +98,14 @@ def _load_notes(midi_path: str):
|
|
| 97 |
return perf, pp
|
| 98 |
|
| 99 |
|
| 100 |
-
def
|
| 101 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
_ensure_fluidsynth_discoverable() # no-op off macOS; helps on local dev
|
| 103 |
import fluidsynth # lazy: keeps the app importable without the native lib
|
| 104 |
|
|
@@ -106,14 +113,14 @@ def _render_audio(pp) -> tuple[int, np.ndarray]:
|
|
| 106 |
sfid = fl.sfload(SF2)
|
| 107 |
fl.program_select(9, sfid, 128, 0) # GM percussion set on channel 9
|
| 108 |
|
| 109 |
-
|
| 110 |
-
for
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
|
| 115 |
audio = []
|
| 116 |
-
for (t0, is_on, pitch, vel), (t1, *_rest) in zip(
|
| 117 |
if is_on:
|
| 118 |
fl.noteon(9, pitch, vel)
|
| 119 |
else:
|
|
@@ -121,24 +128,47 @@ def _render_audio(pp) -> tuple[int, np.ndarray]:
|
|
| 121 |
n = int(max(0, (t1 - t0)) * SR)
|
| 122 |
if n:
|
| 123 |
audio.extend(fl.get_samples(n)[::2]) # interleaved stereo -> mono
|
| 124 |
-
# let the last hits ring out
|
| 125 |
-
audio.extend(fl.get_samples(SR)[::2])
|
| 126 |
fl.delete()
|
| 127 |
return SR, np.asarray(audio, dtype=np.int16)
|
| 128 |
|
| 129 |
|
| 130 |
-
def
|
| 131 |
-
"""
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
return fig
|
| 143 |
|
| 144 |
|
|
@@ -153,7 +183,8 @@ def humanize(midi_file, model_label, style, bpm, time_signature, beat_type,
|
|
| 153 |
if not pp.notes:
|
| 154 |
raise gr.Error("No notes found in that MIDI file.")
|
| 155 |
|
| 156 |
-
|
|
|
|
| 157 |
notes = [
|
| 158 |
{"index": i, "onset_sec": float(nd["note_on"]), "pitch": int(nd["midi_pitch"]),
|
| 159 |
"velocity": int(nd["velocity"]), "selected": True}
|
|
@@ -169,27 +200,61 @@ def humanize(midi_file, model_label, style, bpm, time_signature, beat_type,
|
|
| 169 |
for i, nd in enumerate(pp.notes):
|
| 170 |
if i in pred:
|
| 171 |
nd["velocity"] = int(pred[i])
|
| 172 |
-
|
|
|
|
| 173 |
|
| 174 |
out_midi = os.path.join(tempfile.mkdtemp(), "humanized.mid")
|
| 175 |
partitura.save_performance_midi(perf, out_midi)
|
| 176 |
|
| 177 |
-
fig =
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
| 180 |
|
| 181 |
|
| 182 |
-
|
| 183 |
_EX_DIR = os.path.join(HERE, "examples")
|
| 184 |
EXAMPLES = [
|
| 185 |
-
[os.path.join(_EX_DIR, "rock_65_beat_4-4.midi"),
|
| 186 |
-
|
| 187 |
-
[os.path.join(_EX_DIR, "
|
| 188 |
-
"LightGBM (fast, deterministic)", "pop", 138, "4-4", "beat", 1.0, 1.0, 42, True],
|
| 189 |
-
[os.path.join(_EX_DIR, "soul_105_beat_4-4.midi"),
|
| 190 |
-
"Transformer - Categorical", "soul", 105, "4-4", "beat", 1.0, 1.0, 42, True],
|
| 191 |
]
|
| 192 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
DESC = """\
|
| 194 |
# 🥁 Dynamics Needed
|
| 195 |
|
|
@@ -199,18 +264,24 @@ alone** - never from the note's own loudness - using models trained on the
|
|
| 199 |
[E-GMD](https://magenta.tensorflow.org/datasets/e-gmd) dataset of real
|
| 200 |
performances.
|
| 201 |
|
| 202 |
-
Upload a drum-MIDI groove
|
| 203 |
-
compare the result by ear
|
|
|
|
| 204 |
plugin, behind a web UI.
|
| 205 |
"""
|
| 206 |
|
| 207 |
|
| 208 |
-
with gr.Blocks(title="Dynamics Needed"
|
| 209 |
gr.Markdown(DESC)
|
| 210 |
with gr.Row():
|
| 211 |
with gr.Column(scale=1):
|
| 212 |
-
midi_in = gr.File(label="
|
| 213 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
style_in = gr.Dropdown(STYLES, value="rock", label="Style / genre",
|
| 215 |
info="Conditions the transformer; the genre is the part before '/'.")
|
| 216 |
with gr.Row():
|
|
@@ -224,21 +295,24 @@ with gr.Blocks(title="Dynamics Needed", theme=gr.themes.Soft()) as demo:
|
|
| 224 |
blend_in = gr.Slider(0.0, 1.0, value=1.0, step=0.05, label="Blend",
|
| 225 |
info="1.0 = fully predicted, 0.0 = keep original velocities.")
|
| 226 |
seed_in = gr.Number(value=42, label="Seed", precision=0)
|
| 227 |
-
audio_chk = gr.Checkbox(value=True, label="Render audio
|
| 228 |
-
run_btn = gr.Button("Predict velocities", variant="primary")
|
| 229 |
with gr.Column(scale=1):
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
|
| 234 |
inputs = [midi_in, model_in, style_in, bpm_in, ts_in, beat_in,
|
| 235 |
temp_in, blend_in, seed_in, audio_chk]
|
| 236 |
-
outputs = [midi_out, plot_out,
|
| 237 |
run_btn.click(humanize, inputs=inputs, outputs=outputs)
|
| 238 |
|
| 239 |
-
gr.Examples(examples=EXAMPLES, inputs=inputs, outputs=outputs, fn=humanize,
|
| 240 |
-
cache_examples=False, label="Example grooves (E-GMD, CC-BY 4.0)")
|
| 241 |
-
|
| 242 |
|
| 243 |
if __name__ == "__main__":
|
| 244 |
-
demo.launch(mcp_server=True)
|
|
|
|
| 67 |
|
| 68 |
from drum_dynamics.serve.models import Engine
|
| 69 |
from drum_dynamics.viz.playback import set_soundfont, _ensure_fluidsynth_discoverable
|
| 70 |
+
from drum_dynamics.core.midi import drum_name
|
| 71 |
|
| 72 |
# --- load once at module scope ------------------------------------------------
|
| 73 |
ENGINE = Engine.load(os.path.join(HERE, "models"))
|
|
|
|
| 98 |
return perf, pp
|
| 99 |
|
| 100 |
|
| 101 |
+
def _note_events(pp):
|
| 102 |
+
"""Snapshot the current notes as (on_sec, off_sec, pitch, velocity) tuples."""
|
| 103 |
+
return [(float(nd["note_on"]), float(nd["note_off"]),
|
| 104 |
+
int(nd["midi_pitch"]), int(nd["velocity"])) for nd in pp.notes]
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _render_events(events) -> tuple[int, np.ndarray]:
|
| 108 |
+
"""Render (on, off, pitch, velocity) drum events to a mono int16 waveform."""
|
| 109 |
_ensure_fluidsynth_discoverable() # no-op off macOS; helps on local dev
|
| 110 |
import fluidsynth # lazy: keeps the app importable without the native lib
|
| 111 |
|
|
|
|
| 113 |
sfid = fl.sfload(SF2)
|
| 114 |
fl.program_select(9, sfid, 128, 0) # GM percussion set on channel 9
|
| 115 |
|
| 116 |
+
timeline = [] # (time_sec, is_on, pitch, velocity)
|
| 117 |
+
for on, off, pitch, vel in events:
|
| 118 |
+
timeline.append((on, True, pitch, vel))
|
| 119 |
+
timeline.append((off, False, pitch, 0))
|
| 120 |
+
timeline.sort(key=lambda e: e[0])
|
| 121 |
|
| 122 |
audio = []
|
| 123 |
+
for (t0, is_on, pitch, vel), (t1, *_rest) in zip(timeline[:-1], timeline[1:]):
|
| 124 |
if is_on:
|
| 125 |
fl.noteon(9, pitch, vel)
|
| 126 |
else:
|
|
|
|
| 128 |
n = int(max(0, (t1 - t0)) * SR)
|
| 129 |
if n:
|
| 130 |
audio.extend(fl.get_samples(n)[::2]) # interleaved stereo -> mono
|
| 131 |
+
audio.extend(fl.get_samples(SR)[::2]) # let the last hits ring out
|
|
|
|
| 132 |
fl.delete()
|
| 133 |
return SR, np.asarray(audio, dtype=np.int16)
|
| 134 |
|
| 135 |
|
| 136 |
+
def _drumroll_figure(na_before, na_after):
|
| 137 |
+
"""Stacked drum-rolls (velocity as colour) for original vs humanized.
|
| 138 |
+
|
| 139 |
+
Only the drum pieces actually present are shown, on a shared 0-127 colour
|
| 140 |
+
scale so the change in dynamics is directly comparable between the two.
|
| 141 |
+
"""
|
| 142 |
+
pitches = sorted({int(p) for p in na_before["pitch"]}, reverse=True) # high on top
|
| 143 |
+
row_of = {p: i for i, p in enumerate(pitches)}
|
| 144 |
+
n_ticks = int(max(na_before["onset_tick"] + na_before["duration_tick"])) + 1
|
| 145 |
+
min_w = max(1, n_ticks // 200) # widen hits so short drums stay visible
|
| 146 |
+
|
| 147 |
+
def build(na):
|
| 148 |
+
roll = np.zeros((len(pitches), n_ticks))
|
| 149 |
+
for pt, on, dur, vel in zip(na["pitch"], na["onset_tick"],
|
| 150 |
+
na["duration_tick"], na["velocity"]):
|
| 151 |
+
r = row_of[int(pt)]
|
| 152 |
+
s = int(on)
|
| 153 |
+
e = min(n_ticks, s + max(int(dur), min_w))
|
| 154 |
+
roll[r, s:e] = np.maximum(roll[r, s:e], vel / 127.0)
|
| 155 |
+
return roll
|
| 156 |
+
|
| 157 |
+
fig, axes = plt.subplots(2, 1, figsize=(11, 6.2), sharex=True)
|
| 158 |
+
im = None
|
| 159 |
+
for ax, na, title in ((axes[0], na_before, "Original"),
|
| 160 |
+
(axes[1], na_after, "Humanized")):
|
| 161 |
+
im = ax.imshow(build(na), aspect="auto", origin="lower", interpolation="none",
|
| 162 |
+
cmap="magma", vmin=0.0, vmax=1.0)
|
| 163 |
+
ax.set_title(title, fontsize=10, loc="left")
|
| 164 |
+
ax.set_yticks(range(len(pitches)))
|
| 165 |
+
ax.set_yticklabels([drum_name(p) for p in pitches], fontsize=7)
|
| 166 |
+
ax.set_ylabel("drum piece", fontsize=8)
|
| 167 |
+
axes[1].set_xlabel("time (MIDI ticks)")
|
| 168 |
+
cbar = fig.colorbar(im, ax=axes, fraction=0.03, pad=0.02)
|
| 169 |
+
cbar.set_label("velocity")
|
| 170 |
+
cbar.set_ticks(np.linspace(0, 1, 8))
|
| 171 |
+
cbar.set_ticklabels(np.linspace(0, 127, 8, dtype=int))
|
| 172 |
return fig
|
| 173 |
|
| 174 |
|
|
|
|
| 183 |
if not pp.notes:
|
| 184 |
raise gr.Error("No notes found in that MIDI file.")
|
| 185 |
|
| 186 |
+
na_before = pp.note_array().copy() # original, for the roll
|
| 187 |
+
events_before = _note_events(pp) # original, for the audio
|
| 188 |
notes = [
|
| 189 |
{"index": i, "onset_sec": float(nd["note_on"]), "pitch": int(nd["midi_pitch"]),
|
| 190 |
"velocity": int(nd["velocity"]), "selected": True}
|
|
|
|
| 200 |
for i, nd in enumerate(pp.notes):
|
| 201 |
if i in pred:
|
| 202 |
nd["velocity"] = int(pred[i])
|
| 203 |
+
na_after = pp.note_array().copy() # humanized, for the roll
|
| 204 |
+
events_after = _note_events(pp) # humanized, for the audio
|
| 205 |
|
| 206 |
out_midi = os.path.join(tempfile.mkdtemp(), "humanized.mid")
|
| 207 |
partitura.save_performance_midi(perf, out_midi)
|
| 208 |
|
| 209 |
+
fig = _drumroll_figure(na_before, na_after)
|
| 210 |
+
do_audio = bool(want_audio) and os.path.exists(SF2)
|
| 211 |
+
audio_before = _render_events(events_before) if do_audio else None
|
| 212 |
+
audio_after = _render_events(events_after) if do_audio else None
|
| 213 |
+
return out_midi, fig, audio_before, audio_after
|
| 214 |
|
| 215 |
|
| 216 |
+
GENRES = LEVELS["genres"]
|
| 217 |
_EX_DIR = os.path.join(HERE, "examples")
|
| 218 |
EXAMPLES = [
|
| 219 |
+
[os.path.join(_EX_DIR, "rock_65_beat_4-4.midi")],
|
| 220 |
+
[os.path.join(_EX_DIR, "pop_138_beat_4-4.midi")],
|
| 221 |
+
[os.path.join(_EX_DIR, "soul_105_beat_4-4.midi")],
|
|
|
|
|
|
|
|
|
|
| 222 |
]
|
| 223 |
|
| 224 |
+
|
| 225 |
+
def _autofill_from_file(path):
|
| 226 |
+
"""Best-effort: read musical context from an E-GMD-style filename.
|
| 227 |
+
|
| 228 |
+
Fills style / bpm / time-signature / beat-type when the name carries them
|
| 229 |
+
(e.g. ``rock_65_beat_4-4.midi``); leaves any field it can't parse untouched.
|
| 230 |
+
Returns gr.update()s in the order (style, bpm, time_signature, beat_type).
|
| 231 |
+
"""
|
| 232 |
+
import re
|
| 233 |
+
|
| 234 |
+
keep = (gr.update(), gr.update(), gr.update(), gr.update())
|
| 235 |
+
if not path:
|
| 236 |
+
return keep
|
| 237 |
+
tokens = re.split(r"[_\s]+", os.path.splitext(os.path.basename(path))[0].lower())
|
| 238 |
+
|
| 239 |
+
bpm = next((int(t) for t in tokens if t.isdigit() and 30 <= int(t) <= 300), None)
|
| 240 |
+
ts = next((t for t in tokens if re.fullmatch(r"\d-\d", t)), None)
|
| 241 |
+
beat = next((t for t in tokens if t in ("beat", "fill")), None)
|
| 242 |
+
style = None
|
| 243 |
+
for t in tokens:
|
| 244 |
+
if t in STYLES:
|
| 245 |
+
style = t
|
| 246 |
+
break
|
| 247 |
+
if t.split("-")[0] in GENRES:
|
| 248 |
+
style = t.split("-")[0]
|
| 249 |
+
break
|
| 250 |
+
|
| 251 |
+
return (
|
| 252 |
+
gr.update(value=style) if style else gr.update(),
|
| 253 |
+
gr.update(value=bpm) if bpm else gr.update(),
|
| 254 |
+
gr.update(value=ts) if ts else gr.update(),
|
| 255 |
+
gr.update(value=beat) if beat else gr.update(),
|
| 256 |
+
)
|
| 257 |
+
|
| 258 |
DESC = """\
|
| 259 |
# 🥁 Dynamics Needed
|
| 260 |
|
|
|
|
| 264 |
[E-GMD](https://magenta.tensorflow.org/datasets/e-gmd) dataset of real
|
| 265 |
performances.
|
| 266 |
|
| 267 |
+
Upload a drum-MIDI groove **or** pick an example, set its musical context, and
|
| 268 |
+
compare the result **by ear** (original vs humanized audio) and **by eye**
|
| 269 |
+
(a drum-roll where colour is velocity). Same engine that drives the REAPER
|
| 270 |
plugin, behind a web UI.
|
| 271 |
"""
|
| 272 |
|
| 273 |
|
| 274 |
+
with gr.Blocks(title="Dynamics Needed") as demo:
|
| 275 |
gr.Markdown(DESC)
|
| 276 |
with gr.Row():
|
| 277 |
with gr.Column(scale=1):
|
| 278 |
+
midi_in = gr.File(label="① Upload a drum MIDI", file_types=[".mid", ".midi"],
|
| 279 |
+
type="filepath")
|
| 280 |
+
gr.Markdown("<div style='text-align:center;opacity:.7'>— or —</div>")
|
| 281 |
+
gr.Examples(examples=EXAMPLES, inputs=[midi_in],
|
| 282 |
+
label="Pick an example groove (E-GMD, CC-BY 4.0)")
|
| 283 |
+
model_in = gr.Dropdown(list(MODEL_LABELS), value=list(MODEL_LABELS)[0],
|
| 284 |
+
label="② Model")
|
| 285 |
style_in = gr.Dropdown(STYLES, value="rock", label="Style / genre",
|
| 286 |
info="Conditions the transformer; the genre is the part before '/'.")
|
| 287 |
with gr.Row():
|
|
|
|
| 295 |
blend_in = gr.Slider(0.0, 1.0, value=1.0, step=0.05, label="Blend",
|
| 296 |
info="1.0 = fully predicted, 0.0 = keep original velocities.")
|
| 297 |
seed_in = gr.Number(value=42, label="Seed", precision=0)
|
| 298 |
+
audio_chk = gr.Checkbox(value=True, label="Render audio previews")
|
| 299 |
+
run_btn = gr.Button("③ Predict velocities", variant="primary")
|
| 300 |
with gr.Column(scale=1):
|
| 301 |
+
with gr.Row():
|
| 302 |
+
audio_before_out = gr.Audio(label="Original (as uploaded)", type="numpy")
|
| 303 |
+
audio_after_out = gr.Audio(label="Humanized", type="numpy")
|
| 304 |
+
plot_out = gr.Plot(label="Drum-roll · colour = velocity (original vs humanized)")
|
| 305 |
+
midi_out = gr.File(label="Humanized MIDI (download)")
|
| 306 |
+
|
| 307 |
+
# Auto-fill musical context from the filename (examples and named uploads).
|
| 308 |
+
ctx_out = [style_in, bpm_in, ts_in, beat_in]
|
| 309 |
+
midi_in.change(_autofill_from_file, inputs=[midi_in], outputs=ctx_out)
|
| 310 |
|
| 311 |
inputs = [midi_in, model_in, style_in, bpm_in, ts_in, beat_in,
|
| 312 |
temp_in, blend_in, seed_in, audio_chk]
|
| 313 |
+
outputs = [midi_out, plot_out, audio_before_out, audio_after_out]
|
| 314 |
run_btn.click(humanize, inputs=inputs, outputs=outputs)
|
| 315 |
|
|
|
|
|
|
|
|
|
|
| 316 |
|
| 317 |
if __name__ == "__main__":
|
| 318 |
+
demo.launch(mcp_server=True, theme=gr.themes.Soft())
|