File size: 11,493 Bytes
01f0475
a8759fd
 
 
 
 
 
 
 
cbafa30
 
 
a8759fd
 
 
 
 
 
 
a189727
 
 
3a0da16
a189727
 
01f0475
 
f021bc1
a189727
 
01f0475
a189727
 
01f0475
 
 
 
 
9eb90fb
 
 
 
 
 
 
 
01f0475
 
 
 
 
 
d8afa22
01f0475
 
 
a189727
01f0475
a189727
01f0475
9eb90fb
 
 
 
aa97841
9eb90fb
01f0475
 
 
 
9eb90fb
 
 
 
01f0475
a189727
 
 
 
 
01f0475
 
a189727
 
 
 
 
 
01f0475
 
 
 
 
 
a189727
 
 
 
 
 
 
 
 
 
 
01f0475
a189727
 
 
 
 
9eb90fb
 
a189727
 
 
 
 
 
 
 
01f0475
a189727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
01f0475
 
 
a189727
 
 
 
 
 
 
 
 
01f0475
a189727
 
e78811f
 
 
 
 
 
3a0da16
 
 
 
 
 
 
 
 
 
 
 
 
01f0475
 
3a0da16
 
 
 
 
 
a017c1f
 
3a0da16
e78811f
 
3a0da16
e78811f
 
 
3a0da16
 
 
 
 
 
 
a189727
91a5618
 
 
 
 
 
 
 
a189727
 
 
 
 
 
 
 
01f0475
a189727
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91a5618
 
 
 
 
 
 
 
 
9cd9496
a189727
 
0ea1873
 
 
7fd48be
 
e78811f
7fd48be
0ea1873
 
 
 
 
 
 
 
01f0475
0ea1873
3a0da16
 
0ea1873
 
 
7fd48be
 
 
0ea1873
 
653006d
01f0475
653006d
 
3a0da16
653006d
0ea1873
01f0475
b71c4b3
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
import spaces  # imported first, as ZeroGPU requires

try:  # torch>=2.6 flipped torch.load(weights_only) to True; legacy ckpts need False
    import torch as _torch

    if getattr(_torch.load, "__harp_compat__", False) is False:
        _torch_load_orig = _torch.load

        def _torch_load_compat(*args, **kwargs):
            # Lightning passes weights_only=None, which torch reads as True
            if kwargs.get("weights_only") is None:
                kwargs["weights_only"] = False
            return _torch_load_orig(*args, **kwargs)

        _torch_load_compat.__harp_compat__ = True
        _torch.load = _torch_load_compat
except Exception:  # torch not installed / unexpected API -- nothing to patch
    pass

import time

import numpy as np
import soundfile as sf
import audiotools as at
import torch
from pyharp.core import build_endpoint, ModelCard
from pyharp.media.audio import save_audio
from pyharp.tags import Subcategory, SampleRate, Channels

import gradio as gr
from vampnet.interface import Interface
from vampnet import mask as pmask

model_card = ModelCard(
    name="VampNet",
    description=(
        "Generates new audio by remixing and extending your input, filling in "
        "the blanks to create a coherent continuation in the same style. "
        "Besides the default, three fine-tuned models are available. cat10 was "
        "fine-tuned on expressive cat sounds, lazaro-ros-sep on Afro-Cuban "
        "percussion extracted from the Lázaro Ros discography, and supermetroid "
        "on Super Metroid (SNES) gameplay audio, for retro, game-style "
        "soundscapes. Generation runs in mono at the model's own rate. The "
        "result is resampled back to the input's rate and copied across its "
        "channels, so the output has the format it came in with but no stereo "
        "image. The code is MIT, but the "
        "pretrained weights are CC-BY-NC-SA-4.0, so results cannot be used "
        "commercially and must be shared alike. Repository: "
        "https://github.com/hugofloresgarcia/vampnet Paper: "
        "https://arxiv.org/abs/2307.04686"
    ),
    author="Hugo Flores Garcia, Prem Seetharaman, Rithesh Kumar, Bryan Pardo",
    tags=[Subcategory.INFILLING, SampleRate(44100), Channels(1), 'sound-effects', 'domain:music']
)

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
init_model_choice = open("DEFAULT_MODEL").read().strip()
MODEL_CHOICES = Interface.available_models()

# pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the
# server, so the default model is built and placed there alone. Each weight set is about 2.4 GB,
# too much to hold all four together, so the others are only fetched here, and built inside the
# request that picks one. Their paths are kept, since load_finetuned would list the model
# repository over the network from inside that request's GPU reservation.
if __name__ != "__main__":
    from vampnet import download_default, download_finetuned

    interface = Interface.default()
    interface.load_finetuned(init_model_choice)
    interface.to(DEVICE)
    CHECKPOINTS = {
        name: download_default() if name == "default" else download_finetuned(name)
        for name in MODEL_CHOICES
    }
    print(f"Loaded initial model: {init_model_choice}")


from torch_pitch_shift import pitch_shift, get_fast_shifts
def shift_pitch(signal, interval: int):
    signal.samples = pitch_shift(
        signal.samples,
        shift=interval,
        sample_rate=signal.sample_rate
    )
    return signal


def _vamp_internal(
        seed, input_audio, model_choice,
        pitch_shift_amt, periodic_p,
        n_mask_codebooks, onset_mask_width,
        dropout, sampletemp, typical_filtering,
        typical_mass, typical_min_tokens, top_p,
        sample_cutoff, stretch_factor, sampling_steps, beat_mask_ms, num_feedback_steps
    ):
    t0 = time.time()
    _seed = seed if seed > 0 else None
    if _seed is None:
        _seed = int(torch.randint(0, 2**32, (1,)).item())
    at.util.seed(_seed)

    if input_audio is None:
        raise gr.Error("no input audio received!")
    sr, input_audio = input_audio
    input_audio = input_audio / np.iinfo(input_audio.dtype).max

    sig = at.AudioSignal(input_audio, sr).to_mono()

    loudness = sig.loudness()
    sig = interface._preprocess(sig)

    # Builds the chosen model's checkpoints, unless they are the ones already built
    interface.reload(*CHECKPOINTS[model_choice])

    if pitch_shift_amt != 0:
        sig = shift_pitch(sig, pitch_shift_amt)

    codes = interface.encode(sig)

    mask = interface.build_mask(
        codes,
        sig=sig,
        periodic_prompt=periodic_p,
        onset_mask_width=onset_mask_width,
        _dropout=dropout,
        upper_codebook_mask=n_mask_codebooks,
    )
    if beat_mask_ms > 0:
        mask = pmask.mask_and(
            mask, interface.make_beat_mask(
                sig, after_beat_s=beat_mask_ms/1000.,
            )
        )
        mask = pmask.codebook_mask(mask, n_mask_codebooks)

    interface.set_chunk_size(10.0)

    # lord help me
    if top_p is not None:
        if top_p > 0:
            pass
        else:
            top_p = None

    codes, mask_z = interface.vamp(
        codes, mask,
        batch_size=2,
        feedback_steps=num_feedback_steps,
        _sampling_steps=sampling_steps,
        time_stretch_factor=stretch_factor,
        return_mask=True,
        temperature=sampletemp,
        typical_filtering=typical_filtering,
        typical_mass=typical_mass,
        typical_min_tokens=typical_min_tokens,
        top_p=top_p,
        seed=_seed,
        sample_cutoff=sample_cutoff,
    )
    print(f"vamp took {time.time() - t0} seconds")

    sig = interface.decode(codes)
    sig = sig.normalize(loudness)

    return sig


# Generation covers the whole input, stretched, in 10 s chunks, so the cost grows with the
# output's length
# PyHARP stops a job that runs past its timeout_s, 15 minutes by default, so a request whose
# reservation would exceed that is refused rather than started. A shorter one is left to
# ZeroGPU, which admits it when the caller's own allowance covers it.
MAX_GPU_SECONDS = 900


def gpu_seconds(input_audio, sampletemp, periodic_p, dropout, n_mask_codebooks, model_choice,
                stretch_factor):
    # Measured at about 15 s for 30 s of output and 19 s for 64 s, padded, with room for loading
    # a fine-tuned model
    return 15 + 0.4 * sf.info(input_audio).duration * max(1, int(stretch_factor))


def process_fn(input_audio, sampletemp, periodic_p, dropout, n_mask_codebooks, model_choice,
               stretch_factor):
    # Checked before the GPU is reserved, so a request the model cannot take spends none of the
    # caller's ZeroGPU allowance
    if model_choice not in MODEL_CHOICES:
        raise gr.Error(f"Unknown model: {model_choice}")
    if not input_audio:
        raise gr.Error("Provide the audio to rework.")
    try:
        duration = sf.info(input_audio).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.")
    output_seconds = duration * max(1, int(stretch_factor))
    if gpu_seconds(input_audio, sampletemp, periodic_p, dropout, n_mask_codebooks, model_choice,
                   stretch_factor) > MAX_GPU_SECONDS:
        raise gr.Error(
            f"That would generate {output_seconds:.0f} s of audio, more than one request can in "
            f"the {MAX_GPU_SECONDS // 60} minutes it may run. Use a shorter input or a smaller "
            "stretch factor."
        )

    return harp_vamp(input_audio, sampletemp, periodic_p, dropout, n_mask_codebooks,
                     model_choice, stretch_factor)


@spaces.GPU(duration=gpu_seconds)
def harp_vamp(input_audio, sampletemp, periodic_p, dropout, n_mask_codebooks, model_choice, stretch_factor):
    sig = at.AudioSignal(input_audio)

    # Captured before the downmix, because generation runs in mono at VampNet's
    # own rate and both have to be put back afterwards
    original_sr = sig.sample_rate
    original_channels = sig.num_channels

    sig = sig.to_mono()

    input_audio = sig.cpu().detach().numpy()[0][0]
    input_audio = input_audio * np.iinfo(np.int16).max
    input_audio = input_audio.astype(np.int16)
    input_audio = input_audio.reshape(1, -1)
    input_audio = (sig.sample_rate, input_audio)

    sig =  _vamp_internal(
        seed=0,
        input_audio=input_audio,
        model_choice=model_choice,
        pitch_shift_amt=0,
        periodic_p=int(periodic_p),
        n_mask_codebooks=int(n_mask_codebooks),
        onset_mask_width=0,
        dropout=dropout,
        sampletemp=sampletemp,
        typical_filtering=False,
        typical_mass=0.15,
        typical_min_tokens=1,
        top_p=None,
        sample_cutoff=1.0,
        stretch_factor=stretch_factor,
        sampling_steps=36,
        beat_mask_ms=int(0),
        num_feedback_steps=1,
    )

    sig = sig.detach().cpu()

    if sig.sample_rate != original_sr:
        sig = sig.resample(original_sr)

    # Copies of one generated channel, not a stereo image, which the card says
    if original_channels > 1 and sig.num_channels == 1:
        sig.audio_data = sig.audio_data.repeat(1, original_channels, 1)

    return save_audio(sig)


if __name__ == "__main__":
    with gr.Blocks() as demo:
        harp_input_components = [
            gr.Audio(type="filepath", label="Input Audio").harp_required(True).set_info((
                "The audio to rework. The model masks out parts of this recording "
                "and fills them back in, so the result keeps its character."
            )),
            gr.Slider(label="Randomness", minimum=0.1, maximum=10.0, value=1.0, step=0.001,
                  info="Controls sampling diversity- higher values yield more novel variations."),
            gr.Slider(label="Rhythmic Reference", minimum=0, maximum=13, step=1, value=7,
                      info="Determines how frequently rhythmic cues are repeated in generation."),
            gr.Slider(label="Variation Amount", minimum=0.0, maximum=1.0, step=0.01, value=0.0,
                      info="How much of the input the model changes. 0 = close copy, higher = greater remix."),
            gr.Slider(label="Timbral Reference", value=3, minimum=1, maximum=14, step=1,
                      info="Controls the richness of timbre retained from the source audio. Lower = simpler, higher = richer timbre."),
            gr.Dropdown(label="Model Choice", choices=list(MODEL_CHOICES), value=init_model_choice,
                        info="Select which fine-tuned VampNet model to use."),
            gr.Slider(label="Time Stretch Factor", minimum=1, maximum=8, step=1, value=1,
                      info="Scales the output's length relative to the input. 1 = the same length, 2 = twice as long."),
        ]

        harp_output_components = [
            gr.Audio(type="filepath", label="Generated Audio").set_info((
                "The reworked audio, at the same sample rate as the input."
            )),
        ]

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

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