File size: 8,753 Bytes
f959e94
21e9df7
 
 
 
 
f959e94
21e9df7
 
 
 
 
 
d8d68b8
f959e94
21e9df7
 
 
 
 
 
f959e94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847b553
f959e94
 
 
 
6b58e33
f959e94
21e9df7
 
 
 
 
 
 
f959e94
6b1311d
21e9df7
7d26c02
 
21e9df7
 
 
 
 
6242229
f959e94
 
 
6242229
f959e94
21e9df7
 
 
 
 
a1ad053
 
 
21e9df7
 
3eb209f
 
ec2f6de
3eb209f
21e9df7
 
 
a1ad053
 
 
 
 
21e9df7
 
a1ad053
 
 
 
21e9df7
 
a1ad053
 
21e9df7
 
bbac5be
 
 
 
 
 
69da1b7
 
bbac5be
 
 
 
21e9df7
a1ad053
 
 
114c65e
 
 
bbac5be
 
 
421e12a
 
bbac5be
 
 
a1ad053
 
 
55cbfca
 
 
 
21e9df7
1b23105
21e9df7
 
55cbfca
21e9df7
 
 
 
 
 
 
 
 
 
55cbfca
21e9df7
 
 
 
 
 
 
 
55cbfca
21e9df7
 
 
 
 
 
 
 
 
0c50d1c
21e9df7
 
 
 
1b23105
 
 
 
 
 
f959e94
 
 
21e9df7
1b23105
21e9df7
 
 
 
81e6427
 
2f5f7fe
114c65e
bbac5be
2f5f7fe
81e6427
 
 
 
6b58e33
81e6427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8cabbcb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import spaces  # imported first, as ZeroGPU requires

import sys
sys.stdout.reconfigure(line_buffering=True)

import tempfile
from pathlib import Path

import gradio as gr
import soundfile as sf
import torch
import torchaudio
from huggingface_hub import hf_hub_download
from pyharp import ModelCard, build_endpoint
from pyharp.tags import Channels, SampleRate, Subcategory

from src.constants import CODEBOOK_SIZE, SAMPLE_RATE
from src.models.instructmusicgenadapter_module import InstructMusicGenAdapterLitModule

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

model_card = ModelCard(
    name="Instruct-MusicGen",
    description=(
        "Edits a music recording to follow a text instruction, e.g. adding, "
        "removing, or isolating an instrument. The model is fixed-rate and "
        "mono, so the result is resampled back to the input's rate and copied "
        "across its channels: the output has the format it came in with, but "
        "no stereo image. The code is Apache-2.0, but the "
        "model loads Meta's MusicGen weights, which are CC-BY-NC-4.0, so "
        "results cannot be used commercially. Repository: "
        "https://github.com/ldzhangyx/instruct-MusicGen Paper: "
        "https://arxiv.org/abs/2405.18386"
    ),
    author="Yixiao Zhang, Yukara Ikemiya, Woosung Choi, Naoki Murata, "
           "Marco A. Martinez-Ramirez, Liwei Lin, Gus Xia, Wei-Hsiang Liao, "
           "Yuki Mitsufuji, Simon Dixon",
    tags=[Subcategory.EDITING, SampleRate(SAMPLE_RATE), Channels(1), 'instruction-tuning', 'domain:music'],
)

# A lossless input keeps its container. A lossy one comes back as WAV, since encoding the result
# lossily again would expose the artifacts of the first encode and add a second set.
LOSSLESS = {".wav", ".flac"}

ckpt_path = None
ckpt_error = None

model = None


def download_checkpoint():
    """Fetch the finetuned adapter weights, 14.6 GB."""
    global ckpt_path, ckpt_error
    try:
        ckpt_path = hf_hub_download(repo_id="ldzhangyx/instruct-MusicGen", filename="finetuned.ckpt",
                                    revision="2c96cb10fc7abc69cd69a28d79397be7003080b9")
        print("Checkpoint downloaded.")
    except Exception as e:
        ckpt_error = str(e)
        print(f"Download error: {e}")


# pyharp runs process_fn in a worker, which imports this file as __mp_main__ (rather than
# __main__) and starts with the server, so the checkpoint is fetched there alone, before
# process_fn can reserve a GPU. The server never runs process_fn, so it never needs it.
if __name__ != "__main__":
    download_checkpoint()


def load_model():
    """Build the model and load the finetuned weights.

    Construction builds the backbone on CUDA, which ZeroGPU's emulation records only
    from the main thread, so this runs at module level in the worker (see below)
    rather than in a background thread.
    """
    global model
    # weights_only=False bc ckpt has non-tensor objects
    model = InstructMusicGenAdapterLitModule.load_from_checkpoint(
        ckpt_path, weights_only=False
    )
    model.eval()


# Built at module level in the worker, which runs process_fn, so that no request spends the
# caller's ZeroGPU allowance on loading. The server never runs process_fn, so it never holds the
# model.
if __name__ != "__main__" and ckpt_error is None:
    load_model()


def process_fn(input_audio_path: str, instruction: str) -> str:
    """Edit the input audio according to the instruction."""
    # Checked before the GPU is reserved, so a request that cannot run spends none of the
    # caller's ZeroGPU allowance
    if ckpt_error is not None:
        raise gr.Error(f"Checkpoint download failed: {ckpt_error}")
    if model is None:
        raise gr.Error("The model is still loading, please wait a moment and try again.")
    if not instruction or not instruction.strip():
        raise gr.Error("Instruction cannot be empty.")
    if not input_audio_path:
        raise gr.Error("Provide the music to edit.")
    try:
        duration = sf.info(input_audio_path).duration
    except Exception as exc:
        raise gr.Error(f"Could not read the audio file: {exc}") from exc
    if duration == 0:
        raise gr.Error("The audio file is empty.")
    if duration > MAX_INPUT_SECONDS:
        raise gr.Error(
            f"The music is {duration:.1f} s long. Trim it to {MAX_INPUT_SECONDS} seconds or less."
        )

    return edit(input_audio_path, instruction)


# The edit is generated as long as the input, in one pass the GPU holds whole, so the input is
# limited to the longest measured to fit
MAX_INPUT_SECONDS = 60


def gpu_seconds(input_audio_path: str, instruction: str) -> float:
    # Measured at about 24 s for 8 s, 48 s for 20 s and 74 s for 30 s, padded
    return 15 + 3 * sf.info(input_audio_path).duration


@spaces.GPU(duration=gpu_seconds)
@torch.inference_mode()
def edit(input_audio_path: str, instruction: str) -> str:
    """Adapted from generate_edited_audio in the original repo's src/inference.py."""
    # model was trained on "Music piece. Instruct: <desired_edit>." 
    # only <desired_edit> is variable, so rest is hardcoded
    instruction = f"Music piece. Instruct: {instruction.strip().rstrip('.')}."

    input_audio, sample_rate = sf.read(input_audio_path)
    input_channels = 1 if input_audio.ndim == 1 else input_audio.shape[1]
    input_audio_tensor = torch.tensor(input_audio).float()
    if input_audio_tensor.ndim == 2:
        # downmix to mono -- the original inference.py assumes a mono waveform!!
        input_audio_tensor = input_audio_tensor.mean(dim=-1)
    input_audio_tensor = input_audio_tensor.unsqueeze(0).unsqueeze(0)
    if sample_rate != SAMPLE_RATE:
        # the original inference.py never resamples, but the compression model
        # expects SAMPLE_RATE (32kHz); a DAW export at 44.1/48kHz needs converting first
        input_audio_tensor = torchaudio.functional.resample(input_audio_tensor, sample_rate, SAMPLE_RATE)
    input_audio_tensor = input_audio_tensor.to(DEVICE)

    instruction_list = [instruction]

    with torch.autocast("cuda", dtype=torch.float16):
        description, cond_code = model.model.musicgen._prepare_tokens_and_attributes(
            instruction_list, input_audio_tensor
        )

    cond_code = torch.cat(
        [cond_code, torch.ones_like(cond_code[:, :, 0:1]) * CODEBOOK_SIZE], dim=-1
    )

    with torch.autocast("cuda", dtype=torch.float16):
        audio_values = model.model.generate(
            text_description=instruction_list,
            condition_audio_code=cond_code,
            num_samples=1,
        )

    generated_audio = (
        model.model.musicgen.compression_model.decode(audio_values, None)
        .squeeze()
        .float()  # decode runs under autocast, so this is still float16, but soundfile needs float32
        .cpu()
        .detach()
    )

    # The model is mono at 32 kHz, so the result is resampled back to the input's rate and copied
    # across its channels
    if sample_rate != SAMPLE_RATE:
        generated_audio = torchaudio.functional.resample(generated_audio, SAMPLE_RATE, sample_rate)
    generated_audio = generated_audio.unsqueeze(-1).repeat(1, input_channels).numpy()

    input_ext = Path(input_audio_path).suffix.lower()
    with tempfile.NamedTemporaryFile(suffix=input_ext if input_ext in LOSSLESS else ".wav",
                                     delete=False) as f:
        output_audio_path = f.name
    sf.write(output_audio_path, generated_audio, sample_rate)
    return output_audio_path


if __name__ == "__main__":
    with gr.Blocks() as demo:
        input_components = [
            gr.Audio(type="filepath", label="Input Audio").harp_required(True).set_info((
                "The music to edit, up to 60 seconds. The instruction decides what happens "
                "to it, so the arrangement should contain whatever you are asking to change."
            )),
            gr.Textbox(
                label="Instruction",
                value="Only Drums",
                info="What to change: add/only/no + an instrument, e.g. 'Only Drums', 'No Bass', "
                     "'Add Piano', as in the model's demo examples. Piano, bass, drums, and guitar work best.",
            ),
        ]
        output_components = [
            gr.Audio(type="filepath", label="Edited Audio").set_info(
                "Edited version of the input audio reflecting the instruction."
            ),
        ]

        build_endpoint(
            model_card=model_card,
            input_components=input_components,
            output_components=output_components,
            process_fn=process_fn,
        )

    demo.queue().launch(show_error=True, pwa=True, ssr_mode=False)