"""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()