multimodalart HF Staff commited on
Commit
6457a83
·
verified ·
1 Parent(s): 46f13e8

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,7 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ epic_orchestral.wav filter=lfs diff=lfs merge=lfs -text
37
+ piano_strings.wav filter=lfs diff=lfs merge=lfs -text
38
+ reference_epic_orchestral_clean.wav filter=lfs diff=lfs merge=lfs -text
39
+ reference_piano_strings_clean.wav filter=lfs diff=lfs merge=lfs -text
LICENSE-REFINER ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, Any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md CHANGED
@@ -1,13 +1,47 @@
1
  ---
2
- title: Minimax Music3 Latent Refiner
3
- emoji: 🏆
4
- colorFrom: indigo
5
- colorTo: indigo
6
  sdk: gradio
7
  sdk_version: 6.26.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: MiniMax Music 3 Latent Refiner
3
+ emoji: 🎵
4
+ colorFrom: purple
5
+ colorTo: gray
6
  sdk: gradio
7
  sdk_version: 6.26.0
 
8
  app_file: app.py
9
+ short_description: Restore damaged music in MiniMax Music 3 DAV latent space
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 30m
12
  ---
13
 
14
+ # MiniMax Music 3 Latent Refiner v0.10
15
+
16
+ Restore damaged music while retaining the performance, timing, vocals, and arrangement.
17
+ This Space runs the [MiniMax Music 3 latent refiner](https://huggingface.co/terminusresearch/minimax-music3-latent-refiner-v0.10) — a 137M-parameter bridge
18
+ transformer operating directly in MiniMax Music 3's continuous DAV latent space, conditioned on
19
+ MERT frame-aligned features (layer 7), pooled CLAP music embeddings, and an SR3-style degraded
20
+ latent stream with in-context reference tokens.
21
+
22
+ ## How it works
23
+
24
+ The refiner integrates a deterministic 32-step Euler **bridge** sampler from the damaged latent
25
+ endpoint (t=1) back to the clean one (t=0), then decodes the refined latents through the DAV
26
+ decoder. Inputs are resampled to 44.1 kHz stereo; clips longer than 30 s are processed in
27
+ overlapping 30-second windows with a 2-second latent cross-fade (the training configuration).
28
+
29
+ ## Usage
30
+
31
+ 1. **Restore** — upload damaged/degraded music (bandwidth-limited, noisy, quantized, clipped
32
+ recordings) and press *Restore*. 32 bridge steps, 30 s windows by default.
33
+ 2. **Damage your own audio** — apply the training-style degradation chain (bandwidth reduction,
34
+ additive noise, bit-depth reduction, soft clipping) to a clean clip, then restore it in the
35
+ *Restore* tab and compare against the original.
36
+
37
+ The bundled examples are public-domain instrumental music (FreePD/CC0, via
38
+ [mrfakename/cc0-music-captioned](https://huggingface.co/datasets/mrfakename/cc0-music-captioned))
39
+ with the degradation chain applied; the undamaged originals are included for side-by-side
40
+ listening.
41
+
42
+ ## Notes
43
+
44
+ - FP32 is the verified release precision for the refiner.
45
+ - Inputs are trimmed to 120 s to keep ZeroGPU durations bounded.
46
+ - The `minimax_music3_latent_refiner` inference package is vendored in `src/` (Apache-2.0, from
47
+ the model repository).
app.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import spaces # MUST come before any CUDA-touching import
9
+ import torch
10
+
11
+ sys.path.insert(0, str(Path(__file__).parent / "src"))
12
+
13
+ import numpy as np
14
+ import soundfile as sf
15
+ import torchaudio.functional as AF
16
+ import gradio as gr
17
+
18
+ from minimax_music3_latent_refiner import MiniMaxMusic3RefinerPipeline
19
+
20
+ MODEL_ID = "terminusresearch/minimax-music3-latent-refiner-v0.10"
21
+ MAX_SECONDS = 120.0 # keep ZeroGPU duration honest; model is quadratic in frames
22
+
23
+ print("Loading MiniMax Music 3 latent refiner pipeline (refiner + DAV + MERT + CLAP)...")
24
+ REFINER = MiniMaxMusic3RefinerPipeline.from_pretrained(MODEL_ID, device="cuda")
25
+ REFINER.refiner.to("cuda")
26
+ REFINER.audio_vae.to("cuda")
27
+ REFINER.mert.to("cuda")
28
+ REFINER.clap.to("cuda")
29
+ for _v in REFINER.normalization.values():
30
+ _v.to("cuda")
31
+ print("Pipeline loaded.")
32
+
33
+
34
+ def _load_waveform(path: str):
35
+ """Read any audio file as float32 [channels, samples] at its native rate."""
36
+ if not path:
37
+ raise gr.Error("Please upload an audio file or pick an example first.")
38
+ data, sr = sf.read(path, dtype="float32", always_2d=True)
39
+ if data.shape[1] > 2:
40
+ data = data[:, :2]
41
+ return torch.from_numpy(data.T.copy()), sr
42
+
43
+
44
+ def _trim(waveform: torch.Tensor, sr: int, max_seconds: float):
45
+ limit = int(max_seconds * sr)
46
+ if waveform.shape[-1] > limit:
47
+ return waveform[..., :limit], True
48
+ return waveform, False
49
+
50
+
51
+ @spaces.GPU(duration=120)
52
+ def refine(
53
+ audio_path: str,
54
+ steps: int = 32,
55
+ window_seconds: float = 30.0,
56
+ overlap_seconds: float = 2.0,
57
+ direct: bool = False,
58
+ ):
59
+ """Restore a damaged music clip with the MiniMax Music 3 latent refiner.
60
+
61
+ Args:
62
+ audio_path: degraded/damaged music audio to restore.
63
+ steps: deterministic Euler bridge sampling steps (release default 32).
64
+ window_seconds: overlapping inference window in seconds (30 s matches training).
65
+ overlap_seconds: cross-fade overlap between windows in latent frames.
66
+ direct: process the whole clip as one dense sequence instead of windows.
67
+
68
+ Returns:
69
+ Tuple of (restored audio at 44.1 kHz stereo, status text).
70
+ """
71
+ if not audio_path:
72
+ raise gr.Error("Please upload an audio file or pick an example first.")
73
+ waveform, sr = _load_waveform(audio_path)
74
+ waveform, trimmed = _trim(waveform, sr, MAX_SECONDS)
75
+ steps = int(steps)
76
+ if steps < 1:
77
+ steps = 32
78
+ ws = None if direct else float(window_seconds)
79
+ result = REFINER(
80
+ waveform,
81
+ sr,
82
+ steps=steps,
83
+ window_seconds=ws,
84
+ overlap_seconds=float(overlap_seconds),
85
+ )
86
+ out = result.audio.squeeze(0).T.numpy()
87
+ out_path = Path("/tmp") / "refined.wav"
88
+ sf.write(out_path, out, result.sample_rate, subtype="PCM_16")
89
+ status = (
90
+ f"Restored {out.shape[0] / result.sample_rate:.1f}s of audio "
91
+ f"({steps} bridge steps, {'one dense sequence' if ws is None else f'{ws:.0f}s windows'})."
92
+ )
93
+ if trimmed:
94
+ status += f" Input was trimmed to the {MAX_SECONDS:.0f}s demo limit."
95
+ return str(out_path), status
96
+
97
+
98
+ def degrade(audio_path: str, lowpass_hz: float, noise_db: float, bits: float):
99
+ """Apply a degradation chain (bandwidth cut + noise + bit depth + soft clip)
100
+ to a clean clip, so you can hear what the refiner restores.
101
+
102
+ Args:
103
+ audio_path: clean music audio to damage.
104
+ lowpass_hz: lowpass cutoff in Hz (bandwidth reduction).
105
+ noise_db: additive white noise level in dBFS.
106
+ bits: bit-depth reduction (bits per sample).
107
+
108
+ Returns:
109
+ Tuple of (damaged audio, status text).
110
+ """
111
+ if not audio_path:
112
+ raise gr.Error("Upload clean audio to damage first.")
113
+ import scipy.signal as sps
114
+
115
+ data, sr = sf.read(audio_path, dtype="float32", always_2d=True)
116
+ if data.shape[1] > 2:
117
+ data = data[:, :2]
118
+ limit = int(MAX_SECONDS * sr)
119
+ data = data[:limit]
120
+ y = data.copy()
121
+ nyq = sr / 2
122
+ if lowpass_hz < nyq:
123
+ y = sps.sosfilt(sps.butter(2, lowpass_hz / nyq, btype="low", output="sos"), y, axis=0)
124
+ y = sps.sosfilt(sps.butter(2, 60.0 / nyq, btype="high", output="sos"), y, axis=0)
125
+ if noise_db > -90:
126
+ rng = np.random.default_rng(0)
127
+ y = y + rng.normal(0.0, 1.0, y.shape).astype(np.float32) * (10 ** (noise_db / 20.0))
128
+ if bits >= 2:
129
+ levels = 2 ** int(bits)
130
+ y = np.round(y * levels) / levels
131
+ peak = max(float(np.abs(y).max()), 1e-6)
132
+ y = np.tanh(y * (0.95 / peak) * 2.0) * 0.9
133
+ out_path = str(Path("/tmp") / "damaged.wav")
134
+ sf.write(out_path, np.clip(y, -1, 1), sr, subtype="PCM_16")
135
+ return out_path, f"Damaged {y.shape[0] / sr:.1f}s — now press Restore to refine it."
136
+
137
+
138
+ CSS = """
139
+ #col-container { max-width: 1100px; margin: 0 auto; }
140
+ .dark .gradio-container { color: var(--body-text-color); }
141
+ """
142
+
143
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
144
+ gr.Markdown(
145
+ """
146
+ # 🎵 MiniMax Music 3 — Latent Refiner v0.10
147
+
148
+ Restore damaged music while keeping the performance, timing, and arrangement intact.
149
+ A 137M-parameter bridge transformer that works directly in MiniMax Music 3's continuous
150
+ DAV latent space, conditioned on MERT frame features and CLAP audio embeddings.
151
+
152
+ **Upload a degraded clip** (bandwidth-limited, noisy, quantized, clipped recordings), or
153
+ use the *Damage* tab to apply the training-style degradation chain to your own clean audio,
154
+ then press **Restore**.
155
+
156
+ Model: [`terminusresearch/minimax-music3-latent-refiner-v0.10`](https://huggingface.co/terminusresearch/minimax-music3-latent-refiner-v0.10)
157
+ """
158
+ )
159
+ with gr.Column(elem_id="col-container"):
160
+ with gr.Tab("Restore"):
161
+ with gr.Row():
162
+ with gr.Column():
163
+ audio_in = gr.Audio(
164
+ label="Damaged music (input)",
165
+ type="filepath",
166
+ sources=["upload", "microphone"],
167
+ )
168
+ restore_btn = gr.Button("Restore", variant="primary")
169
+ status = gr.Textbox(label="Status", interactive=False, lines=2)
170
+ with gr.Column():
171
+ audio_out = gr.Audio(label="Restored music (output)", type="filepath")
172
+ with gr.Accordion("Advanced settings", open=False):
173
+ steps = gr.Slider(1, 64, value=32, step=1, label="Bridge steps")
174
+ window_seconds = gr.Slider(
175
+ 5.0, 60.0, value=30.0, step=5.0,
176
+ label="Window seconds (30 s matches training)",
177
+ )
178
+ overlap_seconds = gr.Slider(
179
+ 0.0, 10.0, value=2.0, step=0.5, label="Window overlap (seconds)"
180
+ )
181
+ direct = gr.Checkbox(
182
+ value=False,
183
+ label="One dense sequence (no windows; not the quality baseline)",
184
+ )
185
+ with gr.Tab("Damage your own audio"):
186
+ gr.Markdown(
187
+ "Apply the refiner's training-style degradation chain (bandwidth reduction, additive "
188
+ "noise, bit-depth reduction, soft clipping) to a clean clip, then restore it in the "
189
+ "**Restore** tab to compare. Public-domain (FreePD/CC0) source clips."
190
+ )
191
+ clean_in = gr.Audio(label="Clean music (input)", type="filepath", sources=["upload", "microphone"])
192
+ with gr.Row():
193
+ lowpass_hz = gr.Slider(500, 20000, value=3200, step=100, label="Lowpass cutoff (Hz)")
194
+ noise_db = gr.Slider(-90, -10, value=-34, step=1, label="Noise level (dBFS)")
195
+ bits = gr.Slider(2, 16, value=6, step=1, label="Bit depth")
196
+ damage_btn = gr.Button("Damage", variant="secondary")
197
+ damaged_out = gr.Audio(label="Damaged music (output)", type="filepath")
198
+ damage_status = gr.Textbox(label="Status", interactive=False, lines=1)
199
+
200
+ gr.Examples(
201
+ examples=[
202
+ ["piano_strings.wav"],
203
+ ["epic_orchestral.wav"],
204
+ ],
205
+ inputs=[audio_in],
206
+ outputs=[audio_out, status],
207
+ fn=refine,
208
+ cache_examples=True,
209
+ cache_mode="lazy",
210
+ label="Examples (public-domain music with the training-style degradation applied)",
211
+ )
212
+ gr.Markdown(
213
+ """
214
+ #### Reference clean sources
215
+ These are the undamaged originals of the two examples — listen side by side to judge the restoration.
216
+ """
217
+ )
218
+ with gr.Row():
219
+ gr.Audio(value="reference_piano_strings_clean.wav", label="Clean original — piano & strings", type="filepath", show_download_button=True)
220
+ gr.Audio(value="reference_epic_orchestral_clean.wav", label="Clean original — epic orchestral", type="filepath", show_download_button=True)
221
+
222
+ restore_btn.click(
223
+ refine,
224
+ inputs=[audio_in, steps, window_seconds, overlap_seconds, direct],
225
+ outputs=[audio_out, status],
226
+ api_name="restore",
227
+ )
228
+ damage_btn.click(
229
+ degrade,
230
+ inputs=[clean_in, lowpass_hz, noise_db, bits],
231
+ outputs=[damaged_out, damage_status],
232
+ api_name="damage",
233
+ )
234
+
235
+ demo.launch(mcp_server=True)
epic_orchestral.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b30c0018751b25f6f0e4a996c8b4cdaaeb261fe783ae83077fed4ae365b7a786
3
+ size 2116844
piano_strings.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1114afd04dbfa199ef71a10cbcb31ed961b7268f9309315c5f4f43432fe82299
3
+ size 2116844
reference_epic_orchestral_clean.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1d2ffde8dae910c53dc1402e94b80cdc1a66bcd613da960d2aae88bb24dbfab0
3
+ size 2116844
reference_piano_strings_clean.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:661c83317e51b9cee16ea755ac03402adf1da69ba1491f299c4c44782ca8176d
3
+ size 2116844
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ diffusers>=0.38.0
2
+ transformers>=4.52.0
3
+ accelerate>=1.8.0
4
+ torchaudio
5
+ soundfile>=0.12.1
6
+ safetensors>=0.5.0
7
+ numpy>=1.26
8
+ scipy
src/minimax_music3_latent_refiner/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .dav import MiniMaxMusic3DAV
2
+ from .diffusers_patch import attach_latent_refiner
3
+ from .model import MiniMaxMusic3LatentRefiner, bridge_sample
4
+ from .pipeline import MiniMaxMusic3RefinerPipeline, RefinerOutput
5
+
6
+ __all__ = [
7
+ "MiniMaxMusic3DAV",
8
+ "MiniMaxMusic3LatentRefiner",
9
+ "MiniMaxMusic3RefinerPipeline",
10
+ "RefinerOutput",
11
+ "attach_latent_refiner",
12
+ "bridge_sample",
13
+ ]
src/minimax_music3_latent_refiner/cli.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+ import soundfile as sf
7
+ import torch
8
+
9
+ from .pipeline import DEFAULT_MODEL_ID, MiniMaxMusic3RefinerPipeline
10
+
11
+
12
+ def parse_args() -> argparse.Namespace:
13
+ parser = argparse.ArgumentParser(description="Refine audio with the MiniMax Music 3 bridge latent refiner")
14
+ parser.add_argument("input", type=Path)
15
+ parser.add_argument("output", type=Path)
16
+ parser.add_argument("--model", default=DEFAULT_MODEL_ID)
17
+ parser.add_argument("--device", default="cuda")
18
+ parser.add_argument("--steps", type=int, default=32)
19
+ parser.add_argument("--window-seconds", type=float, default=30.0)
20
+ parser.add_argument("--overlap-seconds", type=float, default=2.0)
21
+ parser.add_argument("--direct", action="store_true", help="use one dense sequence instead of overlapping windows")
22
+ return parser.parse_args()
23
+
24
+
25
+ def main() -> None:
26
+ args = parse_args()
27
+ data, sample_rate = sf.read(args.input, dtype="float32", always_2d=True)
28
+ waveform = torch.from_numpy(data.T.copy())
29
+ pipeline = MiniMaxMusic3RefinerPipeline.from_pretrained(args.model, device=args.device)
30
+ output = pipeline(
31
+ waveform,
32
+ sample_rate,
33
+ steps=args.steps,
34
+ window_seconds=None if args.direct else args.window_seconds,
35
+ overlap_seconds=args.overlap_seconds,
36
+ )
37
+ args.output.parent.mkdir(parents=True, exist_ok=True)
38
+ sf.write(args.output, output.audio.squeeze(0).T.numpy(), output.sample_rate)
39
+
40
+
41
+ if __name__ == "__main__":
42
+ main()
src/minimax_music3_latent_refiner/dav.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The MiniMax Team and The HuggingFace Team. All rights reserved.
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import math
7
+
8
+ import torch
9
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
10
+ from diffusers.models.modeling_utils import ModelMixin
11
+ from torch import nn
12
+ from torch.nn.utils import weight_norm
13
+
14
+
15
+ class MiniMaxMusic3Snake1d(nn.Module):
16
+ def __init__(self, channels: int):
17
+ super().__init__()
18
+ self.alpha = nn.Parameter(torch.ones(1, channels, 1))
19
+
20
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
21
+ shape = hidden_states.shape
22
+ hidden_states = hidden_states.reshape(shape[0], shape[1], -1)
23
+ hidden_states = hidden_states + (self.alpha + 1e-9).reciprocal() * torch.sin(self.alpha * hidden_states).pow(2)
24
+ return hidden_states.reshape(shape)
25
+
26
+
27
+ class DAVResidualUnit(nn.Module):
28
+ def __init__(self, dim: int, dilation: int):
29
+ super().__init__()
30
+ self.block = nn.Sequential(
31
+ MiniMaxMusic3Snake1d(dim),
32
+ weight_norm(nn.Conv1d(dim, dim, kernel_size=7, dilation=dilation, padding=3 * dilation)),
33
+ MiniMaxMusic3Snake1d(dim),
34
+ weight_norm(nn.Conv1d(dim, dim, kernel_size=1)),
35
+ )
36
+
37
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
38
+ residual = self.block(hidden_states)
39
+ if residual.shape[-1] != hidden_states.shape[-1]:
40
+ padding = (hidden_states.shape[-1] - residual.shape[-1]) // 2
41
+ hidden_states = hidden_states[..., padding : hidden_states.shape[-1] - padding]
42
+ return hidden_states + residual
43
+
44
+
45
+ class DAVEncoderBlock(nn.Module):
46
+ def __init__(self, dim: int, stride: int):
47
+ super().__init__()
48
+ self.block = nn.Sequential(
49
+ DAVResidualUnit(dim // 2, dilation=1),
50
+ DAVResidualUnit(dim // 2, dilation=3),
51
+ DAVResidualUnit(dim // 2, dilation=9),
52
+ MiniMaxMusic3Snake1d(dim // 2),
53
+ weight_norm(nn.Conv1d(dim // 2, dim, kernel_size=2 * stride, stride=stride, padding=math.ceil(stride / 2))),
54
+ )
55
+
56
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
57
+ return self.block(hidden_states)
58
+
59
+
60
+ class DAVEncoder(nn.Module):
61
+ def __init__(self, encoder_dim: int, encoder_rates: tuple[int, ...], latent_dim: int):
62
+ super().__init__()
63
+ layers: list[nn.Module] = [weight_norm(nn.Conv1d(1, encoder_dim, kernel_size=7, padding=3))]
64
+ for stride in encoder_rates:
65
+ encoder_dim *= 2
66
+ layers.append(DAVEncoderBlock(encoder_dim, stride=stride))
67
+ layers.extend(
68
+ (
69
+ MiniMaxMusic3Snake1d(encoder_dim),
70
+ weight_norm(nn.Conv1d(encoder_dim, latent_dim, kernel_size=3, padding=1)),
71
+ )
72
+ )
73
+ self.block = nn.Sequential(*layers)
74
+
75
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
76
+ return self.block(hidden_states)
77
+
78
+
79
+ class DAVDecoderBlock(nn.Module):
80
+ def __init__(self, input_dim: int, output_dim: int, stride: int):
81
+ super().__init__()
82
+ self.block = nn.Sequential(
83
+ MiniMaxMusic3Snake1d(input_dim),
84
+ weight_norm(
85
+ nn.ConvTranspose1d(
86
+ input_dim,
87
+ output_dim,
88
+ kernel_size=2 * stride,
89
+ stride=stride,
90
+ padding=math.ceil(stride / 2),
91
+ )
92
+ ),
93
+ DAVResidualUnit(output_dim, dilation=1),
94
+ DAVResidualUnit(output_dim, dilation=3),
95
+ DAVResidualUnit(output_dim, dilation=9),
96
+ )
97
+
98
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
99
+ return self.block(hidden_states)
100
+
101
+
102
+ class DAVDecoder(nn.Module):
103
+ def __init__(self, input_dim: int, hidden_dim: int, upsampling_ratios: tuple[int, ...]):
104
+ super().__init__()
105
+ layers: list[nn.Module] = [weight_norm(nn.Conv1d(input_dim, hidden_dim, kernel_size=7, padding=3))]
106
+ output_dim = hidden_dim
107
+ for index, stride in enumerate(upsampling_ratios):
108
+ input_channels = hidden_dim // (2**index)
109
+ output_dim = hidden_dim // (2 ** (index + 1))
110
+ layers.append(DAVDecoderBlock(input_channels, output_dim, stride=stride))
111
+ layers.extend(
112
+ (
113
+ MiniMaxMusic3Snake1d(output_dim),
114
+ weight_norm(nn.Conv1d(output_dim, 1, kernel_size=7, padding=3)),
115
+ nn.Tanh(),
116
+ )
117
+ )
118
+ self.model = nn.Sequential(*layers)
119
+
120
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
121
+ return self.model(hidden_states)
122
+
123
+
124
+ class MiniMaxMusic3DAV(ModelMixin, ConfigMixin):
125
+ @register_to_config
126
+ def __init__(
127
+ self,
128
+ latent_channels: int = 128,
129
+ channel_latent_channels: int = 64,
130
+ encoder_dim: int = 64,
131
+ encoder_rates: tuple[int, ...] = (2, 4, 8, 8),
132
+ encoder_latent_dim: int = 1024,
133
+ decoder_input_dim: int = 1024,
134
+ decoder_hidden_dim: int = 1536,
135
+ upsampling_ratios: tuple[int, ...] = (8, 8, 4, 2),
136
+ sampling_rate: int = 44_100,
137
+ ):
138
+ super().__init__()
139
+ if channel_latent_channels * 2 != latent_channels:
140
+ raise ValueError("latent_channels must be twice channel_latent_channels")
141
+ self.hop_length = math.prod(encoder_rates)
142
+ self.encoder = DAVEncoder(encoder_dim, encoder_rates, encoder_latent_dim)
143
+ self.mean_proj = nn.Conv1d(encoder_latent_dim, channel_latent_channels, kernel_size=1)
144
+ self.logs_proj = nn.Conv1d(encoder_latent_dim, channel_latent_channels, kernel_size=1)
145
+ self.dec_in_proj = nn.Conv1d(channel_latent_channels, decoder_input_dim, kernel_size=1)
146
+ self.decoder = DAVDecoder(decoder_input_dim, decoder_hidden_dim, upsampling_ratios)
147
+
148
+ def _prepare_waveform(self, waveform: torch.Tensor) -> torch.Tensor:
149
+ if waveform.ndim == 1:
150
+ waveform = waveform[None, None]
151
+ elif waveform.ndim == 2:
152
+ waveform = waveform[None]
153
+ if waveform.ndim != 3:
154
+ raise ValueError("waveform must have shape [batch, channels, samples]")
155
+ if waveform.shape[1] == 1:
156
+ waveform = waveform.repeat(1, 2, 1)
157
+ elif waveform.shape[1] != 2:
158
+ raise ValueError("waveform must be mono or stereo")
159
+ remainder = waveform.shape[-1] % self.hop_length
160
+ if remainder:
161
+ waveform = torch.nn.functional.pad(waveform, (0, self.hop_length - remainder))
162
+ return waveform
163
+
164
+ def encode(self, waveform: torch.Tensor) -> torch.Tensor:
165
+ waveform = self._prepare_waveform(waveform)
166
+ batch_size = waveform.shape[0]
167
+ hidden_states = self.encoder(waveform.reshape(batch_size * 2, 1, -1))
168
+ latents = self.mean_proj(hidden_states)
169
+ return latents.reshape(batch_size, self.config.latent_channels, -1)
170
+
171
+ def decode(self, latents: torch.Tensor) -> torch.Tensor:
172
+ if latents.ndim != 3 or latents.shape[1] != self.config.latent_channels:
173
+ raise ValueError(f"latents must have shape [batch, {self.config.latent_channels}, frames]")
174
+ batch_size, _, length = latents.shape
175
+ hidden_states = latents.reshape(batch_size * 2, self.config.channel_latent_channels, length)
176
+ waveform = self.decoder(self.dec_in_proj(hidden_states))
177
+ return waveform.reshape(batch_size, 2, -1)
178
+
179
+ forward = decode
src/minimax_music3_latent_refiner/diffusers_patch.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import types
4
+
5
+ import torch
6
+
7
+ from .pipeline import MiniMaxMusic3RefinerPipeline, RefinerOutput
8
+
9
+
10
+ def attach_latent_refiner(diffusers_pipeline, refiner: MiniMaxMusic3RefinerPipeline):
11
+ """Attach `refine_audio` to a loaded MiniMax Music 3 Diffusers pipeline instance."""
12
+ if not hasattr(diffusers_pipeline, "vocoder") or diffusers_pipeline.vocoder is None:
13
+ raise TypeError("the Diffusers pipeline must expose its MiniMax Music 3 vocoder")
14
+ if hasattr(diffusers_pipeline, "refine_audio"):
15
+ raise ValueError("the Diffusers pipeline already has a refine_audio attribute")
16
+
17
+ def refine_audio(
18
+ self,
19
+ waveform: torch.Tensor,
20
+ sample_rate: int,
21
+ *,
22
+ steps: int = 32,
23
+ window_seconds: float | None = 30.0,
24
+ overlap_seconds: float = 2.0,
25
+ ) -> RefinerOutput:
26
+ return refiner(
27
+ waveform,
28
+ sample_rate,
29
+ steps=steps,
30
+ window_seconds=window_seconds,
31
+ overlap_seconds=overlap_seconds,
32
+ decoder=self.vocoder,
33
+ )
34
+
35
+ diffusers_pipeline.refine_audio = types.MethodType(refine_audio, diffusers_pipeline)
36
+ return diffusers_pipeline
src/minimax_music3_latent_refiner/model.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 SimpleTuner contributors
2
+ # Licensed under the Apache License, Version 2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import math
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
11
+ from diffusers.models.modeling_utils import ModelMixin
12
+ from torch import nn
13
+
14
+
15
+ class RotaryEmbedding(nn.Module):
16
+ def __init__(self, head_dim: int, base: float = 10_000.0):
17
+ super().__init__()
18
+ inv_freq = base ** (-torch.arange(0, head_dim, 2, dtype=torch.float32) / head_dim)
19
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
20
+
21
+ def forward(self, length: int, device: torch.device) -> tuple[torch.Tensor, torch.Tensor]:
22
+ positions = torch.arange(length, device=device, dtype=torch.float32)
23
+ angles = positions[:, None] * self.inv_freq.to(device)[None, :]
24
+ return angles.cos(), angles.sin()
25
+
26
+
27
+ def apply_rope(states: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
28
+ first, second = states.chunk(2, dim=-1)
29
+ cos = cos[None, None]
30
+ sin = sin[None, None]
31
+ return torch.cat((first * cos - second * sin, first * sin + second * cos), dim=-1)
32
+
33
+
34
+ class RefinerBlock(nn.Module):
35
+ def __init__(self, d_model: int, heads: int, cond_dim: int):
36
+ super().__init__()
37
+ if d_model % heads:
38
+ raise ValueError("d_model must be divisible by heads")
39
+ self.heads = heads
40
+ self.head_dim = d_model // heads
41
+ self.attn_norm = nn.LayerNorm(d_model, elementwise_affine=False)
42
+ self.mlp_norm = nn.LayerNorm(d_model, elementwise_affine=False)
43
+ self.qkv = nn.Linear(d_model, 3 * d_model, bias=False)
44
+ self.attn_out = nn.Linear(d_model, d_model, bias=False)
45
+ self.mlp = nn.Sequential(
46
+ nn.Linear(d_model, 4 * d_model),
47
+ nn.SiLU(),
48
+ nn.Linear(4 * d_model, d_model),
49
+ )
50
+ self.adaln = nn.Linear(d_model, 6 * d_model)
51
+ self.layer_cond_proj = nn.Linear(cond_dim, d_model, bias=False)
52
+
53
+ def forward(
54
+ self,
55
+ states: torch.Tensor,
56
+ conditioning: torch.Tensor,
57
+ cos: torch.Tensor,
58
+ sin: torch.Tensor,
59
+ layer_conditioning: torch.Tensor,
60
+ ) -> torch.Tensor:
61
+ states = states + self.layer_cond_proj(layer_conditioning)
62
+ shift_a, scale_a, gate_a, shift_m, scale_m, gate_m = self.adaln(conditioning).chunk(6, dim=-1)
63
+ batch, length, _ = states.shape
64
+ normed = self.attn_norm(states) * (1 + scale_a[:, None]) + shift_a[:, None]
65
+ query, key, value = self.qkv(normed).view(batch, length, 3, self.heads, self.head_dim).permute(2, 0, 3, 1, 4)
66
+ query = apply_rope(query, cos, sin)
67
+ key = apply_rope(key, cos, sin)
68
+ attended = F.scaled_dot_product_attention(query, key, value)
69
+ attended = attended.transpose(1, 2).reshape(batch, length, -1)
70
+ states = states + gate_a[:, None] * self.attn_out(attended)
71
+ normed = self.mlp_norm(states) * (1 + scale_m[:, None]) + shift_m[:, None]
72
+ return states + gate_m[:, None] * self.mlp(normed)
73
+
74
+
75
+ class MiniMaxMusic3LatentRefiner(ModelMixin, ConfigMixin):
76
+ @register_to_config
77
+ def __init__(
78
+ self,
79
+ latent_dim: int = 128,
80
+ cond_dim: int = 768,
81
+ d_model: int = 768,
82
+ depth: int = 12,
83
+ heads: int = 12,
84
+ mert_layer_count: int = 13,
85
+ mert_input_layer: int = 7,
86
+ style_dim: int = 512,
87
+ task_count: int = 3,
88
+ ):
89
+ super().__init__()
90
+ if not 0 <= mert_input_layer < mert_layer_count:
91
+ raise ValueError("mert_input_layer must select an available MERT layer")
92
+ self.proj_in = nn.Linear(latent_dim + cond_dim, d_model)
93
+ self.time_dim = d_model
94
+ self.time_embed = nn.Sequential(nn.Linear(256, d_model), nn.SiLU(), nn.Linear(d_model, d_model))
95
+ self.style_proj = nn.Sequential(nn.Linear(style_dim, d_model), nn.SiLU(), nn.Linear(d_model, d_model))
96
+ self.style_null = nn.Parameter(torch.zeros(style_dim))
97
+ self.task_embed = nn.Embedding(task_count + 1, d_model)
98
+ self.context_embed = nn.Parameter(torch.zeros(d_model))
99
+ self.degraded_in_proj = nn.Linear(latent_dim, d_model, bias=False)
100
+ self.degraded_null = nn.Parameter(torch.zeros(latent_dim))
101
+ self.mert_null = nn.Parameter(torch.zeros(mert_layer_count, cond_dim))
102
+ self.rope = RotaryEmbedding(d_model // heads)
103
+ self.blocks = nn.ModuleList(RefinerBlock(d_model, heads, cond_dim) for _ in range(depth))
104
+ self.layer_map = [min(1 + index, mert_layer_count - 2) for index in range(depth)]
105
+ self.out_norm = nn.LayerNorm(d_model, elementwise_affine=False)
106
+ self.proj_out = nn.Linear(d_model, latent_dim)
107
+
108
+ @staticmethod
109
+ def timestep_features(timestep: torch.Tensor) -> torch.Tensor:
110
+ half = 128
111
+ frequencies = torch.exp(
112
+ -math.log(10_000.0) * torch.arange(half, device=timestep.device, dtype=torch.float32) / half
113
+ )
114
+ angles = timestep[:, None].float() * frequencies[None, :] * 1_000.0
115
+ return torch.cat((angles.sin(), angles.cos()), dim=-1)
116
+
117
+ def forward(
118
+ self,
119
+ noisy_latents: torch.Tensor,
120
+ conditioning: torch.Tensor,
121
+ timestep: torch.Tensor,
122
+ layer_conditioning: torch.Tensor,
123
+ style: torch.Tensor,
124
+ degraded_latents: torch.Tensor,
125
+ context_latents: torch.Tensor,
126
+ task: torch.Tensor | None = None,
127
+ ) -> torch.Tensor:
128
+ target_length = noisy_latents.shape[1]
129
+ if conditioning.shape[1] != target_length or degraded_latents.shape[1] != target_length:
130
+ raise ValueError("conditioning, degraded latents, and noisy latents must share a frame count")
131
+ if context_latents.shape[1] != target_length:
132
+ raise ValueError("context latents must share the target frame count")
133
+
134
+ states = self.proj_in(torch.cat((noisy_latents, conditioning), dim=-1))
135
+ states = states + self.degraded_in_proj(degraded_latents)
136
+ context_states = self.proj_in(torch.cat((context_latents, conditioning), dim=-1))
137
+ context_states = context_states + self.context_embed[None, None]
138
+ states = torch.cat((context_states, states), dim=1)
139
+
140
+ time_conditioning = self.time_embed(self.timestep_features(timestep))
141
+ if task is None:
142
+ task = torch.full(
143
+ (timestep.shape[0],),
144
+ self.task_embed.num_embeddings - 1,
145
+ dtype=torch.long,
146
+ device=timestep.device,
147
+ )
148
+ time_conditioning = time_conditioning + self.task_embed(task)
149
+ time_conditioning = time_conditioning + self.style_proj(style)
150
+
151
+ cos, sin = self.rope(target_length, states.device)
152
+ cos = torch.cat((cos, cos), dim=0)
153
+ sin = torch.cat((sin, sin), dim=0)
154
+ for index, block in enumerate(self.blocks):
155
+ block_layers = layer_conditioning[:, self.layer_map[index]]
156
+ block_layers = torch.cat((block_layers, block_layers), dim=1)
157
+ states = block(states, time_conditioning, cos, sin, block_layers)
158
+ return self.proj_out(self.out_norm(states[:, target_length:]))
159
+
160
+
161
+ @torch.inference_mode()
162
+ def bridge_sample(
163
+ model: MiniMaxMusic3LatentRefiner,
164
+ conditioning: torch.Tensor,
165
+ layer_conditioning: torch.Tensor,
166
+ degraded_latents: torch.Tensor,
167
+ style: torch.Tensor,
168
+ steps: int = 32,
169
+ ) -> torch.Tensor:
170
+ if steps < 1:
171
+ raise ValueError("steps must be positive")
172
+ latents = degraded_latents.clone()
173
+ schedule = torch.linspace(1.0, 0.0, steps + 1, device=latents.device)
174
+ for index in range(steps):
175
+ timestep = schedule[index].expand(latents.shape[0])
176
+ velocity = model(
177
+ latents,
178
+ conditioning,
179
+ timestep,
180
+ layer_conditioning,
181
+ style,
182
+ degraded_latents,
183
+ degraded_latents,
184
+ )
185
+ latents = latents - (schedule[index] - schedule[index + 1]) * velocity
186
+ return latents
src/minimax_music3_latent_refiner/pipeline.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Protocol
7
+
8
+ import torch
9
+ import torch.nn.functional as F
10
+ import torchaudio
11
+ from huggingface_hub import hf_hub_download
12
+ from safetensors.torch import load_file
13
+ from transformers import AutoModel, ClapModel, ClapProcessor, Wav2Vec2FeatureExtractor
14
+
15
+ from .dav import MiniMaxMusic3DAV
16
+ from .model import MiniMaxMusic3LatentRefiner, bridge_sample
17
+
18
+ SAMPLE_RATE = 44_100
19
+ MERT_SAMPLE_RATE = 24_000
20
+ DAV_HOP = 512
21
+ DEFAULT_MODEL_ID = "terminusresearch/minimax-music3-latent-refiner-v0.10"
22
+ DEFAULT_AUDIO_VAE_ID = "SimpleTuner/MiniMax-Music-3-Encoder"
23
+ DEFAULT_MERT_ID = "m-a-p/MERT-v1-95M"
24
+ DEFAULT_CLAP_ID = "laion/larger_clap_music"
25
+
26
+
27
+ class AudioVAEEncoder(Protocol):
28
+ def encode(self, waveform: torch.Tensor) -> torch.Tensor: ...
29
+
30
+
31
+ @dataclass
32
+ class RefinerOutput:
33
+ latents: torch.Tensor
34
+ audio: torch.Tensor | None
35
+ sample_rate: int
36
+ original_samples: int
37
+
38
+
39
+ def _module_device(module: torch.nn.Module) -> torch.device:
40
+ return next(module.parameters()).device
41
+
42
+
43
+ def _module_dtype(module: torch.nn.Module) -> torch.dtype:
44
+ return next(module.parameters()).dtype
45
+
46
+
47
+ def prepare_waveform(waveform: torch.Tensor, sample_rate: int) -> torch.Tensor:
48
+ if waveform.ndim == 1:
49
+ waveform = waveform[None, None]
50
+ elif waveform.ndim == 2:
51
+ waveform = waveform[None]
52
+ if waveform.ndim != 3:
53
+ raise ValueError("waveform must have shape [samples], [channels, samples], or [batch, channels, samples]")
54
+ if waveform.shape[1] == 1:
55
+ waveform = waveform.repeat(1, 2, 1)
56
+ elif waveform.shape[1] != 2:
57
+ raise ValueError("waveform must be mono or stereo")
58
+ waveform = waveform.float().cpu()
59
+ if sample_rate != SAMPLE_RATE:
60
+ waveform = torchaudio.functional.resample(waveform, sample_rate, SAMPLE_RATE)
61
+ return waveform
62
+
63
+
64
+ def window_starts(total_samples: int, window_samples: int, overlap_samples: int) -> list[int]:
65
+ if window_samples <= 0:
66
+ raise ValueError("window_samples must be positive")
67
+ if not 0 <= overlap_samples < window_samples:
68
+ raise ValueError("overlap_samples must be non-negative and smaller than window_samples")
69
+ if total_samples <= window_samples:
70
+ return [0]
71
+ stride = window_samples - overlap_samples
72
+ starts = list(range(0, total_samples - window_samples + 1, stride))
73
+ final_start = total_samples - window_samples
74
+ final_start -= final_start % DAV_HOP
75
+ if final_start > starts[-1]:
76
+ starts.append(final_start)
77
+ return starts
78
+
79
+
80
+ class MiniMaxMusic3RefinerPipeline:
81
+ def __init__(
82
+ self,
83
+ refiner: MiniMaxMusic3LatentRefiner,
84
+ audio_vae: MiniMaxMusic3DAV,
85
+ mert: torch.nn.Module,
86
+ mert_processor: Wav2Vec2FeatureExtractor,
87
+ clap: ClapModel,
88
+ clap_processor: ClapProcessor,
89
+ normalization: dict[str, torch.Tensor],
90
+ ):
91
+ required = {"latent_mean", "latent_std", "layer_mean", "layer_std"}
92
+ missing = required.difference(normalization)
93
+ if missing:
94
+ raise ValueError(f"normalization is missing tensors: {sorted(missing)}")
95
+ self.refiner = refiner.eval()
96
+ self.audio_vae = audio_vae.eval()
97
+ self.mert = mert.eval()
98
+ self.mert_processor = mert_processor
99
+ self.clap = clap.eval()
100
+ self.clap_processor = clap_processor
101
+ self.normalization = normalization
102
+
103
+ @classmethod
104
+ def from_pretrained(
105
+ cls,
106
+ model_id: str | Path = DEFAULT_MODEL_ID,
107
+ *,
108
+ audio_vae_id: str = DEFAULT_AUDIO_VAE_ID,
109
+ mert_id: str = DEFAULT_MERT_ID,
110
+ clap_id: str = DEFAULT_CLAP_ID,
111
+ device: str | torch.device = "cpu",
112
+ dtype: torch.dtype = torch.float32,
113
+ cache_dir: str | Path | None = None,
114
+ local_files_only: bool = False,
115
+ ) -> MiniMaxMusic3RefinerPipeline:
116
+ device = torch.device(device)
117
+ model_id_string = str(model_id)
118
+ refiner = MiniMaxMusic3LatentRefiner.from_pretrained(
119
+ model_id_string,
120
+ torch_dtype=dtype,
121
+ cache_dir=cache_dir,
122
+ local_files_only=local_files_only,
123
+ ).to(device)
124
+ if Path(model_id_string).is_dir():
125
+ normalization_path = Path(model_id_string) / "normalization.safetensors"
126
+ else:
127
+ normalization_path = Path(
128
+ hf_hub_download(
129
+ model_id_string,
130
+ "normalization.safetensors",
131
+ cache_dir=cache_dir,
132
+ local_files_only=local_files_only,
133
+ )
134
+ )
135
+ if not normalization_path.is_file():
136
+ raise FileNotFoundError(f"required normalization file not found: {normalization_path}")
137
+ normalization = load_file(normalization_path, device=str(device))
138
+ audio_vae = MiniMaxMusic3DAV.from_pretrained(
139
+ audio_vae_id,
140
+ subfolder="audio_vae",
141
+ torch_dtype=dtype,
142
+ cache_dir=cache_dir,
143
+ local_files_only=local_files_only,
144
+ ).to(device)
145
+ mert = AutoModel.from_pretrained(
146
+ mert_id,
147
+ trust_remote_code=True,
148
+ dtype=dtype,
149
+ cache_dir=cache_dir,
150
+ local_files_only=local_files_only,
151
+ ).to(device)
152
+ mert_processor = Wav2Vec2FeatureExtractor.from_pretrained(
153
+ mert_id,
154
+ trust_remote_code=True,
155
+ cache_dir=cache_dir,
156
+ local_files_only=local_files_only,
157
+ )
158
+ clap = ClapModel.from_pretrained(
159
+ clap_id,
160
+ dtype=dtype,
161
+ cache_dir=cache_dir,
162
+ local_files_only=local_files_only,
163
+ ).to(device)
164
+ clap_processor = ClapProcessor.from_pretrained(
165
+ clap_id,
166
+ cache_dir=cache_dir,
167
+ local_files_only=local_files_only,
168
+ )
169
+ refiner.requires_grad_(False)
170
+ audio_vae.requires_grad_(False)
171
+ mert.requires_grad_(False)
172
+ clap.requires_grad_(False)
173
+ return cls(refiner, audio_vae, mert, mert_processor, clap, clap_processor, normalization)
174
+
175
+ @property
176
+ def device(self) -> torch.device:
177
+ return _module_device(self.refiner)
178
+
179
+ @property
180
+ def dtype(self) -> torch.dtype:
181
+ return _module_dtype(self.refiner)
182
+
183
+ def _normalization_tensors(self) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
184
+ device = self.device
185
+ return (
186
+ self.normalization["latent_mean"].to(device)[None, None],
187
+ self.normalization["latent_std"].to(device)[None, None],
188
+ self.normalization["layer_mean"].to(device)[None, :, None],
189
+ self.normalization["layer_std"].to(device)[None, :, None],
190
+ )
191
+
192
+ @torch.inference_mode()
193
+ def _mert_layers(self, waveform: torch.Tensor, frame_count: int) -> torch.Tensor:
194
+ mono = waveform.mean(dim=1)
195
+ mono_24k = torchaudio.functional.resample(mono, SAMPLE_RATE, MERT_SAMPLE_RATE)
196
+ inputs = self.mert_processor(
197
+ [row.numpy() for row in mono_24k],
198
+ sampling_rate=MERT_SAMPLE_RATE,
199
+ return_tensors="pt",
200
+ )
201
+ outputs = self.mert(inputs["input_values"].to(self.device), output_hidden_states=True)
202
+ layers = torch.stack(outputs.hidden_states, dim=1).float()
203
+ batch, layer_count, mert_frames, dim = layers.shape
204
+ return (
205
+ F.interpolate(
206
+ layers.reshape(batch * layer_count, mert_frames, dim).transpose(1, 2),
207
+ size=frame_count,
208
+ mode="linear",
209
+ align_corners=True,
210
+ )
211
+ .transpose(1, 2)
212
+ .reshape(batch, layer_count, frame_count, dim)
213
+ )
214
+
215
+ @torch.inference_mode()
216
+ def _clap_style(self, waveform: torch.Tensor) -> torch.Tensor:
217
+ mono = waveform.mean(dim=1)
218
+ center = mono.shape[-1] // 2
219
+ half = min(5 * SAMPLE_RATE, center)
220
+ clip = mono[..., center - half : center + half]
221
+ clip = torchaudio.functional.resample(clip, SAMPLE_RATE, 48_000)
222
+ inputs = self.clap_processor(
223
+ audio=[row.numpy() for row in clip],
224
+ sampling_rate=48_000,
225
+ return_tensors="pt",
226
+ )
227
+ features = self.clap.get_audio_features(input_features=inputs["input_features"].to(self.device))
228
+ if not torch.is_tensor(features):
229
+ features = features.pooler_output
230
+ if features.shape[-1] != self.clap.config.projection_dim:
231
+ features = self.clap.audio_projection(features)
232
+ return features.to(device=self.device, dtype=self.dtype)
233
+
234
+ @torch.inference_mode()
235
+ def _refine_chunk(
236
+ self,
237
+ waveform: torch.Tensor,
238
+ steps: int,
239
+ audio_encoder: AudioVAEEncoder,
240
+ ) -> torch.Tensor:
241
+ encoder_device = _module_device(audio_encoder) if isinstance(audio_encoder, torch.nn.Module) else self.device
242
+ encoder_dtype = _module_dtype(audio_encoder) if isinstance(audio_encoder, torch.nn.Module) else self.dtype
243
+ degraded = audio_encoder.encode(waveform.to(device=encoder_device, dtype=encoder_dtype))
244
+ if degraded.ndim != 3 or degraded.shape[1] != self.refiner.config.latent_dim:
245
+ raise ValueError("AUDIO_VAE_ENCODER.encode must return [batch, 128, frames]")
246
+ degraded = degraded.transpose(1, 2).to(device=self.device, dtype=self.dtype)
247
+ layers = self._mert_layers(waveform, degraded.shape[1])
248
+ style = self._clap_style(waveform)
249
+ latent_mean, latent_std, layer_mean, layer_std = self._normalization_tensors()
250
+ degraded = (degraded - latent_mean) / latent_std
251
+ layers = ((layers - layer_mean) / layer_std).to(self.dtype)
252
+ generated = bridge_sample(
253
+ self.refiner,
254
+ layers[:, self.refiner.config.mert_input_layer],
255
+ layers,
256
+ degraded,
257
+ style,
258
+ steps,
259
+ )
260
+ return (generated * latent_std + latent_mean).transpose(1, 2)
261
+
262
+ @torch.inference_mode()
263
+ def refine_latents(
264
+ self,
265
+ waveform: torch.Tensor,
266
+ sample_rate: int,
267
+ *,
268
+ steps: int = 32,
269
+ window_seconds: float | None = 30.0,
270
+ overlap_seconds: float = 2.0,
271
+ audio_encoder: AudioVAEEncoder | None = None,
272
+ ) -> tuple[torch.Tensor, int]:
273
+ waveform = prepare_waveform(waveform, sample_rate)
274
+ original_samples = waveform.shape[-1]
275
+ encoder = audio_encoder if audio_encoder is not None else self.audio_vae
276
+ if not hasattr(encoder, "encode"):
277
+ raise TypeError("audio_encoder must provide encode(waveform) -> [batch, 128, frames]")
278
+ if window_seconds is None:
279
+ return self._refine_chunk(waveform, steps, encoder), original_samples
280
+ if waveform.shape[0] != 1 and waveform.shape[-1] > round(window_seconds * SAMPLE_RATE):
281
+ raise ValueError("windowed refinement currently requires batch size 1")
282
+
283
+ window_samples = max(DAV_HOP, round(window_seconds * SAMPLE_RATE / DAV_HOP) * DAV_HOP)
284
+ overlap_samples = max(0, round(overlap_seconds * SAMPLE_RATE / DAV_HOP) * DAV_HOP)
285
+ starts = window_starts(original_samples, window_samples, overlap_samples)
286
+ if len(starts) == 1:
287
+ return self._refine_chunk(waveform, steps, encoder), original_samples
288
+
289
+ total_frames = math.ceil(original_samples / DAV_HOP)
290
+ accumulated = torch.zeros(1, 128, total_frames, device=self.device, dtype=self.dtype)
291
+ weights = torch.zeros(1, 1, total_frames, device=self.device, dtype=self.dtype)
292
+ chunk_frames = math.ceil(window_samples / DAV_HOP)
293
+ for index, start_sample in enumerate(starts):
294
+ chunk = waveform[..., start_sample : start_sample + window_samples]
295
+ generated = self._refine_chunk(chunk, steps, encoder)
296
+ start_frame = start_sample // DAV_HOP
297
+ available = min(generated.shape[-1], total_frames - start_frame)
298
+ blend = torch.ones(available, device=self.device, dtype=self.dtype)
299
+ if index > 0:
300
+ previous_end = starts[index - 1] // DAV_HOP + chunk_frames
301
+ left = min(available, max(0, previous_end - start_frame))
302
+ if left:
303
+ blend[:left] = torch.linspace(0.0, 1.0, left + 2, device=self.device, dtype=self.dtype)[1:-1]
304
+ if index + 1 < len(starts):
305
+ next_start = starts[index + 1] // DAV_HOP
306
+ right = min(available, max(0, start_frame + available - next_start))
307
+ if right:
308
+ fade = torch.linspace(1.0, 0.0, right + 2, device=self.device, dtype=self.dtype)[1:-1]
309
+ blend[-right:] = torch.minimum(blend[-right:], fade)
310
+ accumulated[..., start_frame : start_frame + available] += generated[..., :available] * blend
311
+ weights[..., start_frame : start_frame + available] += blend
312
+ if torch.any(weights == 0):
313
+ raise RuntimeError("window overlap left uncovered latent frames")
314
+ return accumulated / weights, original_samples
315
+
316
+ @torch.inference_mode()
317
+ def decode_latents(
318
+ self,
319
+ latents: torch.Tensor,
320
+ original_samples: int,
321
+ decoder: torch.nn.Module | None = None,
322
+ ) -> torch.Tensor:
323
+ decoder = decoder if decoder is not None else self.audio_vae
324
+ if isinstance(decoder, torch.nn.Module):
325
+ decoder_input = latents.to(device=_module_device(decoder), dtype=_module_dtype(decoder))
326
+ else:
327
+ decoder_input = latents.cpu()
328
+ if hasattr(decoder, "decode"):
329
+ audio = decoder.decode(decoder_input)
330
+ else:
331
+ audio = decoder(decoder_input)
332
+ if audio.ndim != 3:
333
+ raise ValueError("audio decoder must return a rank-3 waveform tensor")
334
+ if audio.shape[1] != 2 and audio.shape[-1] == 2:
335
+ audio = audio.movedim(-1, 1)
336
+ if audio.shape[1] != 2:
337
+ raise ValueError("audio decoder must return stereo [batch, 2, samples]")
338
+ return audio[..., :original_samples].float().cpu()
339
+
340
+ def __call__(
341
+ self,
342
+ waveform: torch.Tensor,
343
+ sample_rate: int,
344
+ *,
345
+ steps: int = 32,
346
+ window_seconds: float | None = 30.0,
347
+ overlap_seconds: float = 2.0,
348
+ audio_encoder: AudioVAEEncoder | None = None,
349
+ decoder: torch.nn.Module | None = None,
350
+ decode: bool = True,
351
+ ) -> RefinerOutput:
352
+ latents, original_samples = self.refine_latents(
353
+ waveform,
354
+ sample_rate,
355
+ steps=steps,
356
+ window_seconds=window_seconds,
357
+ overlap_seconds=overlap_seconds,
358
+ audio_encoder=audio_encoder,
359
+ )
360
+ audio = self.decode_latents(latents, original_samples, decoder) if decode else None
361
+ return RefinerOutput(latents, audio, SAMPLE_RATE, original_samples)