"""MAJEPPA Conditional Generation — Gradio Spaces app.
Pulls the model checkpoint from a private HF model repo and exposes a small UI:
score MIDI (preset or upload) + paired performer/recording style → generated performance MIDI.
"""
from __future__ import annotations
import base64
import logging
import os
import shutil
import sys
import tempfile
from pathlib import Path
import gradio as gr
import spaces
from huggingface_hub import hf_hub_download
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger(__name__)
# Make `src/` importable
sys.path.insert(0, str(Path(__file__).resolve().parent))
from inference import ( # noqa: E402
MajeppaInference,
PERFORMER_LABELS,
RECORDING_LABELS,
)
CKPT_REPO = os.environ.get("MAJEPPA_CKPT_REPO", "Anonymous-Submission-Site/majeppa-ckpt")
CKPT_FILENAME = os.environ.get("MAJEPPA_CKPT_FILE", "last.ckpt")
HF_TOKEN = os.environ.get("HF_TOKEN") # Spaces secret
EXAMPLES_DIR = Path(__file__).resolve().parent / "examples"
EXAMPLE_FILES = sorted(EXAMPLES_DIR.glob("*.mid"))
# ---------------------------------------------------------------------------
# Paired (performer, recording) styles, ordered by training-data prevalence
# (counts from PMOS+ASAP train set, Apr 2026). These are the only combos
# offered in Custom mode; nonsensical pairs (e.g. virtuoso + sight-read)
# are excluded.
# ---------------------------------------------------------------------------
STYLE_PAIRS: list[dict] = [
{"key": "virtuoso__concert_performance", "label": "Virtuoso · Concert Performance", "performer": "virtuoso", "recording": "concert_performance"},
{"key": "piano_teacher__demo_class", "label": "Piano Teacher · Demo / Teaching", "performer": "piano_teacher", "recording": "demo_class"},
{"key": "piano_teacher__slow_demo", "label": "Piano Teacher · Slow Demo", "performer": "piano_teacher", "recording": "slow_demo"},
{"key": "child_professional__performance", "label": "Child (professional) · Performance", "performer": "child_professional", "recording": "performance"},
{"key": "adult_intermediate__practice", "label": "Adult Intermediate · Practice", "performer": "adult_intermediate", "recording": "practice"},
{"key": "adult_intermediate__sight_read", "label": "Adult Intermediate · Sight-read", "performer": "adult_intermediate", "recording": "sight_read"},
{"key": "adult_beginner__practice", "label": "Adult Beginner · Practice", "performer": "adult_beginner", "recording": "practice"},
{"key": "adult_beginner__sight_read", "label": "Adult Beginner · Sight-read", "performer": "adult_beginner", "recording": "sight_read"},
{"key": "child_beginner__practice", "label": "Child Beginner · Practice", "performer": "child_beginner", "recording": "practice"},
]
STYLE_BY_KEY = {s["key"]: s for s in STYLE_PAIRS}
DEFAULT_STYLE = STYLE_PAIRS[0]["key"]
# ---------------------------------------------------------------------------
# Curated presets — each pins (score, style, seed). Seeds are filled in after
# auditioning candidates; None = sample fresh each call (i.e. not yet tuned).
# ---------------------------------------------------------------------------
PRESETS: list[dict] = [
{
"key": "czerny__piano_teacher_demo",
"label": "Czerny — Op.740 No.8 · Piano Teacher · Demo / Teaching",
"score": "04_czerny_op740_no8.mid",
"performer": "piano_teacher", "recording": "demo_class",
"seed": 6,
},
{
"key": "czerny__piano_teacher_slow_demo",
"label": "Czerny — Op.740 No.8 · Piano Teacher · Slow Demo",
"score": "04_czerny_op740_no8.mid",
"performer": "piano_teacher", "recording": "slow_demo",
"seed": 7,
},
{
"key": "debussy__adult_intermediate_practice",
"label": "Debussy — Clair de Lune · Adult Intermediate · Practice",
"score": "01_debussy_clair_de_lune.mid",
"performer": "adult_intermediate", "recording": "practice",
"seed": None,
},
{
"key": "chopin_fantaisie__adult_intermediate_sight_read",
"label": "Chopin — Fantaisie-Impromptu Op.66 · Adult Intermediate · Sight-read",
"score": "07_chopin_fantaisie_impromptu.mid",
"performer": "adult_intermediate", "recording": "sight_read",
"seed": 1,
},
{
"key": "beethoven_op10no3__child_pro_performance",
"label": "Beethoven — Sonata No.7 Op.10/3 · Child Professional · Performance",
"score": "08_beethoven_sonata7_op10no3.mid",
"performer": "child_professional", "recording": "performance",
"seed": 6,
},
{
"key": "mozart_k310__child_pro_performance",
"label": "Mozart — Sonata K310 · Child Professional · Performance",
"score": "09_mozart_k310_mvt2.mid",
"performer": "child_professional", "recording": "performance",
"seed": 5,
},
{
"key": "fur_elise__child_beg_performance",
"label": "Beethoven — Für Elise · Child Beginner · Performance",
"score": "11_beethoven_fur_elise.mid",
"performer": "child_beginner", "recording": "performance",
"seed": 3,
},
{
"key": "burgmuller__adult_beginner_practice",
"label": "Burgmüller — Consolation · Adult Beginner · Practice",
"score": "05_burgmuller_consolation.mid",
"performer": "adult_beginner", "recording": "practice",
"seed": 7,
},
{
"key": "chopin__virtuoso_concert",
"label": "Chopin — Nocturne Op.9 No.2 · Virtuoso · Concert Performance",
"score": "03_chopin_nocturne_op9_no2.mid",
"performer": "virtuoso", "recording": "concert_performance",
"seed": 5,
},
]
PRESET_BY_KEY = {p["key"]: p for p in PRESETS}
DEFAULT_PRESET = PRESETS[0]["key"]
# ---------------------------------------------------------------------------
# MIDI → inline player markup
# ---------------------------------------------------------------------------
_PLAYER_STYLE = (
"width:100%;"
# Teal (#14b8a6) for inactive notes and amber (#f59e0b) for the currently
# playing note: both have ~50% luminance + high saturation so they read on
# both dark and light page backgrounds. Multiple CSS-variable names are set
# because html-midi-player versions differ on which one they honor.
"--midi-visualizer-note:#14b8a6;"
"--midi-visualizer-note-rgb:20,184,166;"
"--note-rgb:20,184,166;"
"--midi-visualizer-active-note:#f59e0b;"
"--midi-visualizer-active-note-rgb:245,158,11;"
"--note-active-rgb:245,158,11;"
)
def midi_to_player_html(midi_path: str | None, label: str = "") -> str:
if not midi_path or not Path(midi_path).exists():
return ""
with open(midi_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
src = f"data:audio/midi;base64,{b64}"
title = f"
{label}
" if label else ""
return (
f"{title}"
f""
f""
)
# ---------------------------------------------------------------------------
# Lazy-load model
# ---------------------------------------------------------------------------
_engine: MajeppaInference | None = None
def get_engine() -> MajeppaInference:
global _engine
if _engine is None:
log.info("Downloading checkpoint from %s ...", CKPT_REPO)
ckpt_path = hf_hub_download(
repo_id=CKPT_REPO, filename=CKPT_FILENAME, token=HF_TOKEN, repo_type="model"
)
log.info("Loading model ...")
_engine = MajeppaInference(ckpt_path)
return _engine
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def preset_score_player(preset_key: str) -> str:
p = PRESET_BY_KEY.get(preset_key)
if not p:
return ""
return midi_to_player_html(str(EXAMPLES_DIR / p["score"]), label="Score (deadpan)")
def custom_score_player(score_file: str | None, preset_choice: str) -> str:
if score_file:
return midi_to_player_html(score_file, label="Score (deadpan)")
if preset_choice and preset_choice != "(none)":
return midi_to_player_html(str(EXAMPLES_DIR / preset_choice), label="Score (deadpan)")
return ""
# ---------------------------------------------------------------------------
# Inference callbacks
# ---------------------------------------------------------------------------
@spaces.GPU(duration=120)
def run_preset(preset_key: str) -> tuple[str | None, str, str]:
p = PRESET_BY_KEY.get(preset_key)
if not p:
return None, "Unknown preset.", ""
engine = get_engine()
out = engine.generate(
score_midi_path=str(EXAMPLES_DIR / p["score"]),
performer_type=p["performer"],
recording_type=p["recording"],
max_new_tokens=512,
temperature=0.8,
top_k=50,
seed=p.get("seed"),
)
if out is None:
return None, "Generation produced an empty MIDI. Try another preset.", ""
tmp = tempfile.NamedTemporaryFile(suffix=".mid", delete=False); tmp.close()
shutil.copy(out, tmp.name)
label = (
f"Generated — {PERFORMER_LABELS[p['performer']]} / "
f"{RECORDING_LABELS[p['recording']]}"
)
return tmp.name, f"Generated: {p['label']}.", midi_to_player_html(tmp.name, label=label)
@spaces.GPU(duration=120)
def run_custom(
score_file: str | None,
preset_choice: str,
style_key: str,
max_new_tokens: int,
temperature: float,
top_k: int,
seed: int,
) -> tuple[str | None, str, str]:
score_path = score_file or (
str(EXAMPLES_DIR / preset_choice) if preset_choice and preset_choice != "(none)" else None
)
if score_path is None:
return None, "Please upload a score MIDI or pick a preset score.", ""
if not Path(score_path).exists():
return None, f"Score not found: {score_path}", ""
style = STYLE_BY_KEY.get(style_key)
if style is None:
return None, f"Unknown style: {style_key}", ""
engine = get_engine()
out = engine.generate(
score_midi_path=score_path,
performer_type=style["performer"],
recording_type=style["recording"],
max_new_tokens=int(max_new_tokens),
temperature=float(temperature),
top_k=int(top_k),
seed=int(seed) if seed is not None and seed >= 0 else None,
)
if out is None:
return None, "Generation produced an empty MIDI. Try a different style or score.", ""
tmp = tempfile.NamedTemporaryFile(suffix=".mid", delete=False); tmp.close()
shutil.copy(out, tmp.name)
label = f"Generated — {style['label']}"
return tmp.name, f"Generated: {style['label']}.", midi_to_player_html(tmp.name, label=label)
# ---------------------------------------------------------------------------
# UI
# ---------------------------------------------------------------------------
_PLAYER_HEAD = (
''
# Brute-force note colors so they read on both light and dark themes.
# html-midi-player's CSS-variable names differ by version; the rules below
# target the rendered SVG/HTML elements directly with !important.
''
)
with gr.Blocks(
title="MAJEPPA — Conditional Piano Performance Generation",
head=_PLAYER_HEAD,
) as demo:
gr.Markdown("""
# MAJEPPA — Conditional Piano Performance Generation
*Score MIDI in → expressive performance MIDI out, conditioned on performer skill and recording context.*
> ⏳ **First call after the Space wakes takes ~90–120 s**.
> Subsequent generations within the session are ~40 s each (Since we are using ZeroGPU on huggingface...).
""")
with gr.Tabs():
# =====================================================================
# Preset tab — fully curated; reviewer just clicks Generate.
# =====================================================================
with gr.Tab("Preset"):
gr.Markdown("Pick a curated example and hit **Generate**. "
"Each preset is a hand-tuned (score, style) pair.")
with gr.Row():
with gr.Column(scale=1):
preset_dd = gr.Dropdown(
choices=[(p["label"], p["key"]) for p in PRESETS],
value=DEFAULT_PRESET,
label="Preset",
)
preset_run_btn = gr.Button("Generate", variant="primary")
with gr.Column(scale=1):
preset_score_html = gr.HTML(value=preset_score_player(DEFAULT_PRESET))
preset_output_html = gr.HTML()
preset_out_file = gr.File(label="Download generated MIDI")
preset_status = gr.Markdown()
preset_dd.change(preset_score_player, inputs=[preset_dd], outputs=[preset_score_html])
preset_run_btn.click(
run_preset, inputs=[preset_dd],
outputs=[preset_out_file, preset_status, preset_output_html],
)
# =====================================================================
# Custom tab — full control: upload + paired-style dropdown + sampling.
# Conditions are restricted to data-supported pairings.
# =====================================================================
with gr.Tab("Custom"):
gr.Markdown(
"Upload your own deadpan score MIDI (or pick one of the example scores), "
"choose a target style, and generate. "
)
with gr.Row():
with gr.Column(scale=1):
custom_uploaded = gr.File(
label="Upload a score MIDI",
file_types=[".mid", ".midi"], type="filepath",
)
custom_preset = gr.Dropdown(
choices=["(none)"] + [p.name for p in EXAMPLE_FILES],
value=(EXAMPLE_FILES[0].name if EXAMPLE_FILES else "(none)"),
label="…or pick an example score",
)
custom_style = gr.Dropdown(
choices=[(s["label"], s["key"]) for s in STYLE_PAIRS],
value=DEFAULT_STYLE,
label="Target style (paired performer · recording)",
)
with gr.Accordion("Sampling", open=False):
custom_seed = gr.Number(
value=-1, precision=0,
label="Seed (−1 = random each call)",
)
custom_max_new = gr.Slider(64, 1024, value=512, step=32, label="Max new tokens")
custom_temp = gr.Slider(0.1, 1.5, value=0.8, step=0.05, label="Temperature")
custom_topk = gr.Slider(1, 200, value=50, step=1, label="Top-k")
custom_run_btn = gr.Button("Generate", variant="primary")
with gr.Column(scale=1):
custom_score_html = gr.HTML(
value=custom_score_player(
None,
EXAMPLE_FILES[0].name if EXAMPLE_FILES else "(none)",
),
)
custom_output_html = gr.HTML()
custom_out_file = gr.File(label="Download generated MIDI")
custom_status = gr.Markdown()
custom_preset.change(
custom_score_player, inputs=[custom_uploaded, custom_preset],
outputs=[custom_score_html],
)
custom_uploaded.change(
custom_score_player, inputs=[custom_uploaded, custom_preset],
outputs=[custom_score_html],
)
custom_run_btn.click(
run_custom,
inputs=[
custom_uploaded, custom_preset, custom_style,
custom_max_new, custom_temp, custom_topk, custom_seed,
],
outputs=[custom_out_file, custom_status, custom_output_html],
)
gr.Markdown(
"MAJEPPA — anonymized review demo. Generation is autoregressive over MIDI tokens "
"with a LoRA-adapted MIDI language model and condition-tagged prompts."
)
if __name__ == "__main__":
demo.queue(max_size=4).launch()