--- license: mit base_model: MiniMaxAI/MiniMax-Music-3 tags: - music - text-to-music - lora - concept-sliders - minimax-music-3 library_name: diffusers --- # MiniMax Music 3 concept sliders Bipolar LoRA "sliders" for [MiniMax Music 3](https://huggingface.co/MiniMaxAI/MiniMax-Music-3). Each one is trained so that a single scalar moves one musical property while the prompt stays fixed: set it to `0` and nothing changes, to `+2` and the song leans one way, to `-2` and it leans the other. Training code lives in [ntc-ai/sliders-conceptmod](https://github.com/ntc-ai/sliders-conceptmod) (`conceptmod/textsliders/train_lora_music3.py` and `train_lm_slider_music3.py`). This repository holds the trained weights and the rendered listening examples, which are too large to keep in git. **Current language-model halves are v4 (live and rhyme are v5).** They add Structured Caption poles and an audio-end regularizer on top of the v3 recipe. Earlier v3 files stay in `weights/` for comparison. ## Which stage a slider attaches to MiniMax Music 3 generates in two stages, and this turns out to decide where a slider can live at all: 1. A **Qwen3 language model** autoregressively writes a *plan* — who is singing, the melody, the rhythm, the arrangement. 2. A **36-layer flow transformer** *renders* that plan into audio — timbre, loudness, tone, space. The transformer only ever sees the finished plan, concatenated onto its input. It can re-render that plan but it cannot rewrite it, so anything about *identity or performance* has to be trained on the language model: | slider | poles | host | notes | |---|---|---|---| | `gender-lm-v4` | male ↔ female | language model | a transformer version moved median F0 by 1 Hz | | `rapslow-lm-v4` | slow sung ↔ rap flow | language model | transformer version failed the same way | | `triphop-lm-v4` | glossy pop ↔ trip-hop | language model | pairs with `triphop-tf-v4` | | `energy-lm-v4` | quiet ↔ loud | language model | changes how hard the band *plays* | | `tempo-lm-v4` | slow ↔ fast | language model | changes the composed BPM | | `distortion-lm-v4` | clean ↔ heavy | language model | changes instrumentation | | `breath-lm-v4` | clean ↔ breathy | language model | breath, mouth air, inhales | | `live-lm-v5` | studio ↔ live | language model | room, crowd, performance energy | | `rhyme-lm-v5` | prose ↔ rhyme | language model | lyric delivery; structured captions + endreg | | `energy-slider-v2` | quiet ↔ loud | transformer | mix-level loudness | | `distortion-slider` | acoustic ↔ metal | transformer | tone | | `tempo-slider` | slow ↔ fast | transformer | perceived pulse | | `space-slider` | dry ↔ wet | transformer | reverb — the one axis that is transformer-only | | `triphop-tf-v4` | pop ↔ trip-hop | transformer | style/production — ships as `_unit_last` | Several axes work best with **both** halves applied together (transformer for the mix, language model for the arrangement). `samples/energy-v3-shipped-20s/` is an example of a stacked configuration as actually deployed. ### Choosing a host before you train `scripts/probe_lm_axis_signal.py` in the training repo encodes each pole's caption through the language model and reports how far the plan moves (`sep = ||pos-neg|| / ||neu||`) and how antisymmetric that move is (`cos` near 0 is good; a high value means both poles shift the same shared way): | axis | sep | cos | verdict | |---|---|---|---| | rap ↔ slow | 0.34 | −0.08 | strong | | energy | 0.33 | 0.03 | strong | | tempo | 0.31 | 0.06 | strong | | rhyme | 0.27 | −0.05 | usable | | distortion | 0.27 | −0.03 | usable | | breath | 0.27 | 0.24 | usable | | live | 0.25 | 0.32 | usable | | gender | 0.24 | −0.08 | usable — and it demonstrably works, so ~0.2 is the floor | | trip-hop | 0.22 | 0.10 | usable | | space (dry ↔ wet) | 0.17 | **0.70** | transformer only | Space is the informative failure: its two poles move the plan mostly in the *same* direction, i.e. the model encodes "this caption is about room acoustics" rather than an opposing dry/wet axis. The table above is from the v4 Structured Caption poles (v3 numbers were similar; tempo's shared-component cos dropped from 0.34 to 0.06). ## Usage Each `.safetensors` has a sidecar `.json` describing everything needed to load it — `rank`, `alpha`, `target_replace`, `prefix`, `kind`, and a calibrated `unit_scale` — so nothing has to be hardcoded. ```python import json from pathlib import Path from safetensors.torch import load_file from diffusers import ModularPipeline from conceptmod.textsliders.lora import LoRANetwork pipe = ModularPipeline.from_pretrained("path/to/MiniMax-Music3") # ... load components, move to device ... weights = Path("weights/gender-lm-v4/gender-lm-v4_last.safetensors") meta = json.loads(weights.with_suffix(".json").read_text()) host = pipe.language_model if meta["kind"] == "language_model" else pipe.transformer network = LoRANetwork( host, rank=meta["rank"], alpha=meta["alpha"], multiplier=1.0, target_replace=meta["target_replace"], train_method=meta.get("train_method", "full"), delimiter=meta.get("delimiter", "-"), prefix=meta["prefix"], ).to(device) network.load_state_dict(load_file(str(weights)), strict=False) user_scale = 1.5 # -2 .. +2, 0 is off network.set_lora_slider(user_scale * meta.get("unit_scale", 1.0)) with network: # multipliers reset to 0 on exit audio = pipe(prompt=caption, lyrics=lyrics, audio_duration=20.0, output="audios")[0] ``` Networks compose additively, so several sliders (and both halves of one axis) can be applied at once by entering each context. **Unit normalization (August 2026):** current shipped weights are self-contained — the calibrated unit strength is baked into the checkpoint's `alpha` tensors, and their sidecars read `unit_scale: 1.0`. Files named `*_unit_last.safetensors` (`triphop-tf-v4`, `energy-slider-v2`) follow this convention explicitly; for them `user_scale` multiplies the weights directly with no extra factor. For older un-normalized files, `unit_scale` still applies as above — treat values above ~2 with suspicion, and check the `axis_tracking_low` flag in the sidecar: when it is set, the LoRA delta barely tracks the intended direction and the calibration is not meaningful. ## Listening examples `samples/` holds 20-second renders. Within each folder the slider clips all use the same **neutral** caption, lyrics and seed, so only the slider scale differs. The `REF` clips change the *prompt* instead with the slider off — that is the target sound to compare against. Each folder has a `LISTEN.md` with a play order and most have a `probe.json` of measurements. Start with `samples/energy-v3-lm-20s/`, `samples/gender-v3-ballad-20s/` and `samples/triphop-v3-stack-20s/`. For the current LM halves, play `samples/v4-endreg-ab-90s/` (same-seed 90s +2 of every shipped LM slider vs base). ## August 2026 update - **LM halves retrained as v4 / v5.** Every language-model slider was retrained with Structured Caption poles (the format the studio rewriter actually emits) and an **audio-end regularizer**. Un-regularized LM LoRAs make `<|audio_end|>` less likely, so long renders hit the duration cap mid-phrase. Training now teacher-forces the end-margin from a frozen base-model composition. `samples/v4-endreg-ab-90s/` is the same-seed 90s check: eight of nine sliders at +2 end naturally; rhyme-v5 hit the cap on that one seed. `samples/live-endreg-ab-90s/` is the v3-vs-v4 ending A/B on the live axis. Final-window metrics (collapse / perc / edrift): | checkpoint | collapse | perc | edrift | |---|--:|--:|--:| | `gender-lm-v4` | −0.95 | 0.23 | 0.060 | | `rapslow-lm-v4` | −0.96 | 0.29 | 0.080 | | `triphop-lm-v4` | −0.95 | 0.25 | 0.059 | | `energy-lm-v4` | −0.97 | 0.29 | 0.072 | | `tempo-lm-v4` | −0.95 | 0.30 | 0.071 | | `distortion-lm-v4` | −0.96 | 0.33 | 0.076 | | `breath-lm-v4` | −0.95 | 0.36 | 0.069 | | `live-lm-v5` | −0.91 | 0.43 | 0.068 | | `rhyme-lm-v5` | −0.95 | 0.34 | 0.070 | All nine are `unit_scale` 1.0. v3 LM files (and `rhyme-lm-v4`) remain in `weights/` for comparison; they do not carry the end regularizer. - **`triphop-slider` is replaced by `triphop-tf-v4`.** The old transformer half was trained on a prompt that named real bands; it has been retrained from an equivalent artist-free prompt (matching metrics: loss 0.090, pole opposition −0.89) and removed from this repository. The new file ships unit-normalized. - **New rhyme axis.** `rhyme-lm-v5` is the current file: structured-caption poles plus the end regularizer. `rhyme-lm-v4` (structured captions, no endreg) and `rhyme-lm-v3` (flat captions) stay for comparison. `samples/rhyme-v4-caption-pair/` shows the structured-caption version on an unseen full caption. - **Stacking measured.** `samples/stack-sweep-20s/` renders escalating slider stacks up to all ten sliders at ±2 (combined total 27 units): rms stays within 0.7–2.0× of baseline and crest factor never collapses — no degradation knee was found, so stacking within the normal fader range is safe. ## Honest limitations - **Automatic grading only goes so far.** Tempo and distortion have no metric here that can rank even the reference clips correctly (the BPM estimator calls the fast reference slower than the slow one), so those axes are reported as "inconclusive — use ears" rather than given a verdict. - **Negative results are included on purpose.** `rapslow-v3` and `triphop-v3` are transformer attempts at axes that belong to the language model; their sidecars carry `axis_tracking_low: true`. `triphop-ab-noise` and `triphop-ab-anchor-single` are ablation runs, not sliders to use. - **Loudness entanglement.** Transformer sliders pushed past roughly raw ±4 tend to slam or hollow out the mix rather than express the concept. - **Endings are sampled.** The end regularizer makes a natural fade much more likely, but it is not a guarantee on every seed. rhyme-v5 +2 hit the 90s cap on seed 7 in `samples/v4-endreg-ab-90s/`. - Trained at 4-second conditioning; generation length is independent of that. ## Training recipe (v3, still the base) Rank 8, alpha 8, 500 steps at lr 1e-4 for transformer sliders; 800 steps at lr 5e-4 for language-model sliders. Four fixes distinguish v3 from earlier weights: 1. **On-manifold `x_t`.** The model's flow time runs 0 = noise → 1 = clean, but the original trainer fed pure noise at every timestep. v3 anchors `x_t = (1-t)·ε + t·x0` to generated clean latents. 2. **Both directions trained**, rather than assuming `-s` inverts `+s`. 3. **Antisymmetrized language-model targets**, `tgt(±1) = neutral ± (pos-neg)/2`. This moved the +1/−1 opposition from +0.53 (both directions pointing the same way) to −0.97. 4. **Module-identity dedupe** in the LoRA wrapper — targeting the model root used to wrap every attention/FF linear twice, at double gain. v4/v5 keep that recipe and add Structured Caption poles plus `--endreg_weight 1.0`. For prompt writing: language-model sliders want *divergent* poles (let genre, BPM and instrumentation all move). Rewriting energy as one swapped clause in an otherwise identical caption dropped separation from 0.30 to 0.11.