Spaces:
Sleeping
Sleeping
| """Streamlit UI for Anticipatory Music Transformer accompaniment.""" | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| import warnings | |
| from pathlib import Path | |
| # Show tqdm-style download bars in the terminal where Streamlit runs | |
| os.environ.setdefault("HF_HUB_ENABLE_PROGRESS_BARS", "1") | |
| import streamlit as st | |
| # transformers 4.29.x triggers huggingface_hub's deprecated resume_download warning | |
| warnings.filterwarnings( | |
| "ignore", | |
| message=".*resume_download.*", | |
| category=FutureWarning, | |
| module="huggingface_hub.file_download", | |
| ) | |
| _SRC = Path(__file__).resolve().parent | |
| if str(_SRC) not in sys.path: | |
| sys.path.insert(0, str(_SRC)) | |
| from generation_service import ( # noqa: E402 | |
| Mode, | |
| default_melody_programs, | |
| events_to_midi_bytes, | |
| events_to_musicxml_bytes, | |
| events_to_pdf_bytes, | |
| instrument_choices, | |
| parse_upload_to_events, | |
| prepare_events_window, | |
| run_accompaniment, | |
| validate_melody_vs_events, | |
| ) | |
| # ~128M params — faster download & load (default). Medium is ~360M. | |
| MODEL_FAST = "stanford-crfm/music-small-800k" | |
| MODEL_QUALITY = "stanford-crfm/music-medium-800k" | |
| def _safe_upload_stem(uploaded_file) -> str: | |
| if uploaded_file is None or not getattr(uploaded_file, "name", None): | |
| return "anticipation_output" | |
| stem = Path(str(uploaded_file.name)).name | |
| stem = Path(stem).stem | |
| if not stem or stem in (".", ".."): | |
| return "anticipation_output" | |
| return stem | |
| def load_model(model_id: str): | |
| import torch | |
| from transformers import AutoModelForCausalLM | |
| from transformers import logging as tr_logging | |
| os.environ.setdefault("TOKENIZERS_PARALLELISM", "false") | |
| if torch.cuda.is_available(): | |
| device = "cuda" | |
| elif getattr(torch.backends, "mps", None) and torch.backends.mps.is_available(): | |
| device = "mps" | |
| else: | |
| device = "cpu" | |
| extra: dict = {"torch_dtype": torch.float32} | |
| try: | |
| import accelerate # noqa: F401 | |
| extra["low_cpu_mem_usage"] = True | |
| except ImportError: | |
| pass | |
| # float32 everywhere matches Hugging Face CPU Spaces and avoids fp16 vs fp32 sampling drift. | |
| # CRFM checkpoints include unused keys (e.g. token_out_embeddings); harmless noise otherwise. | |
| _v = tr_logging.get_verbosity() | |
| tr_logging.set_verbosity_error() | |
| try: | |
| model = AutoModelForCausalLM.from_pretrained(model_id, **extra) | |
| finally: | |
| tr_logging.set_verbosity(_v) | |
| model = model.to(device) | |
| model.eval() | |
| return model, device | |
| def _reset_outputs(): | |
| for key in ( | |
| "out_midi", | |
| "out_midi_err", | |
| "out_musicxml", | |
| "out_musicxml_err", | |
| "out_pdf", | |
| "out_pdf_err", | |
| ): | |
| st.session_state.pop(key, None) | |
| st.set_page_config(page_title="Harmony from your melody", layout="wide") | |
| st.title("Add harmony to your melody") | |
| st.caption( | |
| "Upload a tune you wrote (sheet music or MIDI). Lock in your **melody**; the model **adds other parts**. " | |
| "Then download MIDI, sheet-music XML, or PDF." | |
| ) | |
| st.subheader("1. Upload your file") | |
| uploaded = st.file_uploader( | |
| "Choose one score file", | |
| type=["xml", "musicxml", "mxl", "mid", "midi"], | |
| help=( | |
| "From MuseScore, Finale, Dorico, etc.: export as MusicXML or MIDI. " | |
| "Select a **file**, not a project folder. Types: .xml, .musicxml, .mxl, .mid, .midi." | |
| ), | |
| ) | |
| st.subheader("2. How much of the piece to use") | |
| st.markdown( | |
| "The AI works on a **short slice** from the start of your file (long scores are trimmed). " | |
| "A smaller slice runs faster." | |
| ) | |
| col_len_a, col_len_b = st.columns(2) | |
| with col_len_a: | |
| clip_sec = st.slider( | |
| "Length of the excerpt (seconds)", | |
| min_value=5, | |
| max_value=120, | |
| value=20, | |
| step=1, | |
| help="Only this many seconds from the beginning of your file are loaded and harmonized.", | |
| ) | |
| with col_len_b: | |
| prompt_sec = st.slider( | |
| "How many seconds stay “exactly yours” first", | |
| min_value=0, | |
| max_value=int(clip_sec), | |
| value=min(5, int(clip_sec)), | |
| step=1, | |
| help=( | |
| "At the start of the excerpt, this many seconds are treated as fixed context; " | |
| "the model continues and adds harmony after that point within the same window." | |
| ), | |
| ) | |
| events = None | |
| choices = [] | |
| parse_error = None | |
| if uploaded is not None: | |
| try: | |
| raw = uploaded.getvalue() | |
| events = parse_upload_to_events(raw, uploaded.name) | |
| events = prepare_events_window(events, float(clip_sec)) | |
| choices = instrument_choices(events) | |
| except Exception as e: | |
| parse_error = str(e) | |
| events = None | |
| if parse_error: | |
| st.error(f"We could not read that file: {parse_error}") | |
| if uploaded is None: | |
| st.info("Upload a file in step 1 to continue.") | |
| st.stop() | |
| if not events: | |
| st.warning("Once the file loads correctly, you will choose your melody part below.") | |
| st.stop() | |
| if not choices: | |
| st.error("This file has no notes we could read. Try exporting again as MusicXML or MIDI.") | |
| st.stop() | |
| st.subheader("3. Which part is your melody?") | |
| st.markdown( | |
| "Each line is an **instrument sound** (MIDI program) and how many notes it has. " | |
| "**Select the part(s) you want to keep exactly as written** (usually your melody). " | |
| "The AI adds **other instruments** you did not write. " | |
| "If the file is only one instrument, select that one—the model still creates backing parts." | |
| ) | |
| options = [f"Instrument {prog} — {count} notes" for prog, count in choices] | |
| prog_by_label = {options[i]: choices[i][0] for i in range(len(options))} | |
| count_map = {p: c for p, c in choices} | |
| defaults = default_melody_programs(choices) | |
| default_labels = [f"Instrument {p} — {count_map[p]} notes" for p in defaults if p in count_map] | |
| selected_labels = st.multiselect( | |
| "Melody (locked parts)", | |
| options=options, | |
| default=[lbl for lbl in default_labels if lbl in options], | |
| help="Locked parts stay note-for-note. New harmony uses other MIDI instruments (fine for a single-line melody).", | |
| ) | |
| st.subheader("4. Model size") | |
| st.caption( | |
| "Inference uses **float32** everywhere (same as CPU Spaces) so results line up across machines. " | |
| "First time only: the checkpoint downloads from Hugging Face (watch the **terminal** for a progress bar). " | |
| "After that, the same model stays cached until you restart the app or pick a different model." | |
| ) | |
| model_tier = st.radio( | |
| "Choose one", | |
| options=["fast", "quality"], | |
| index=0, | |
| horizontal=True, | |
| format_func=lambda x: ( | |
| "Faster — smaller download (~128M), good for trying the app" | |
| if x == "fast" | |
| else "Higher quality — larger download (~360M), slower on CPU" | |
| ), | |
| ) | |
| preset_model_id = MODEL_FAST if model_tier == "fast" else MODEL_QUALITY | |
| with st.expander("Advanced options (optional)"): | |
| st.markdown("You can usually leave these as they are.") | |
| custom_model_id = st.text_input( | |
| "Custom Hugging Face model id (optional)", | |
| value="", | |
| key="hf_model_override", | |
| placeholder="Leave empty to use the model size you chose above", | |
| help="Only if you use another compatible stanford-crfm checkpoint.", | |
| ) | |
| mode: Mode = st.radio( | |
| "Generation style", | |
| options=["anticipatory", "autoregressive"], | |
| index=0, | |
| format_func=lambda x: "Standard — anticipatory (recommended)" if x == "anticipatory" else "Research baseline — plain autoregressive", | |
| help="Anticipatory mode matches the paper’s infilling setup. Autoregressive is a simpler baseline.", | |
| ) | |
| top_p = st.slider( | |
| "How surprising / varied the harmony is", | |
| min_value=0.5, | |
| max_value=1.0, | |
| value=0.95, | |
| step=0.01, | |
| help="Higher values (e.g. 0.95–1.0) allow more diverse notes; lower values are more conservative.", | |
| ) | |
| generation_seed = st.number_input( | |
| "Random seed", | |
| min_value=0, | |
| max_value=2**31 - 1, | |
| value=42, | |
| step=1, | |
| help=( | |
| "The model samples notes at random; this fixes the RNG so **the same file, settings, model, " | |
| "and seed** match between your laptop and Hugging Face. Change the seed for a new variation." | |
| ), | |
| ) | |
| model_id = custom_model_id.strip() if custom_model_id.strip() else preset_model_id | |
| st.subheader("5. Generate") | |
| gen = st.button("Generate harmony", type="primary") | |
| if gen: | |
| _reset_outputs() | |
| melody_programs = [prog_by_label[lbl] for lbl in selected_labels] | |
| if not melody_programs: | |
| st.error("Choose at least one instrument as your melody.") | |
| else: | |
| try: | |
| validate_melody_vs_events(events, melody_programs) | |
| except ValueError as e: | |
| st.error(str(e)) | |
| else: | |
| with st.status("Loading the AI model…", expanded=True) as load_status: | |
| load_status.write( | |
| "**First run:** downloading can take **several minutes** on a slow connection " | |
| "(hundreds of MB). Check the terminal for a download bar. " | |
| "**Next runs** with the same model reuse the cache and are much quicker." | |
| ) | |
| load_status.write(f"Using: `{model_id}`") | |
| try: | |
| model, _device = load_model(model_id.strip()) | |
| except Exception as e: | |
| load_status.update(label="Model load failed", state="error") | |
| st.error(f"Could not load the model: {e}") | |
| model = None | |
| else: | |
| load_status.update(label="Model ready", state="complete") | |
| if model is not None: | |
| with st.spinner("Writing harmony… Often **1–5+ minutes** on CPU; faster on GPU."): | |
| try: | |
| out = run_accompaniment( | |
| model, | |
| events, | |
| melody_programs, | |
| float(prompt_sec), | |
| float(clip_sec), | |
| top_p, | |
| mode, | |
| seed=int(generation_seed), | |
| ) | |
| except Exception as e: | |
| st.error(f"Generation stopped: {e}") | |
| else: | |
| st.success("Here is your new score. Download below.") | |
| try: | |
| st.session_state["out_midi"] = events_to_midi_bytes(out) | |
| st.session_state.pop("out_midi_err", None) | |
| except Exception as e: | |
| st.session_state.pop("out_midi", None) | |
| st.session_state["out_midi_err"] = str(e) | |
| try: | |
| st.session_state["out_musicxml"] = events_to_musicxml_bytes(out) | |
| st.session_state.pop("out_musicxml_err", None) | |
| except Exception as e: | |
| st.session_state.pop("out_musicxml", None) | |
| st.session_state["out_musicxml_err"] = str(e) | |
| try: | |
| st.session_state["out_pdf"] = events_to_pdf_bytes(out) | |
| st.session_state.pop("out_pdf_err", None) | |
| except Exception as e: | |
| st.session_state["out_pdf"] = None | |
| st.session_state["out_pdf_err"] = str(e) | |
| st.divider() | |
| st.subheader("6. Download") | |
| base = _safe_upload_stem(uploaded) | |
| if st.session_state.get("out_midi"): | |
| st.download_button( | |
| label="Download as MIDI (for DAWs, players)", | |
| data=st.session_state["out_midi"], | |
| file_name=f"{base}_with_harmony.mid", | |
| mime="audio/midi", | |
| ) | |
| if st.session_state.get("out_musicxml"): | |
| st.download_button( | |
| label="Download as MusicXML (open in notation software)", | |
| data=st.session_state["out_musicxml"], | |
| file_name=f"{base}_with_harmony.musicxml", | |
| mime="application/vnd.recordare.musicxml+xml", | |
| ) | |
| if st.session_state.get("out_pdf"): | |
| st.download_button( | |
| label="Download as PDF (printable score)", | |
| data=st.session_state["out_pdf"], | |
| file_name=f"{base}_with_harmony.pdf", | |
| mime="application/pdf", | |
| ) | |
| elif st.session_state.get("out_pdf_err"): | |
| st.caption( | |
| f"**PDF export:** {st.session_state['out_pdf_err']} " | |
| "On macOS, install LilyPond with: `brew install lilypond`, then restart Streamlit." | |
| ) | |
| if st.session_state.get("out_midi_err"): | |
| st.caption(f"MIDI export issue: {st.session_state['out_midi_err']}") | |
| if st.session_state.get("out_musicxml_err"): | |
| st.caption(f"MusicXML export issue: {st.session_state['out_musicxml_err']}") | |
| if not any( | |
| st.session_state.get(k) | |
| for k in ("out_midi", "out_musicxml", "out_pdf") | |
| ): | |
| st.caption("Run step 4 to create files you can download.") | |
| st.divider() | |
| with st.expander("About limits & credits"): | |
| st.markdown( | |
| """ | |
| - Very long or very dense scores may time out; use a shorter excerpt in step 2. | |
| - Sheet music is converted through MIDI internally, so fine engraving may change slightly. | |
| - **Anticipatory Music Transformer** (Apache-2.0): Thickstun et al.; inference code from | |
| [anticipation](https://github.com/jthickstun/anticipation). Weights from | |
| [Stanford CRFM on Hugging Face](https://huggingface.co/stanford-crfm). | |
| """ | |
| ) | |