File size: 8,788 Bytes
e7c9e0b
cfc17a5
752fd9c
2a0c658
cfc17a5
67ce405
 
1db7b87
20daaa7
5a30378
49e9b94
5a30378
 
 
 
e878da6
e671ac4
5a30378
 
 
20daaa7
 
49e9b94
20daaa7
2a0c658
49e9b94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25d08d8
 
49e9b94
 
 
cf241da
49e9b94
 
 
 
 
 
 
 
 
 
5a30378
67ce405
 
 
4103284
 
f3ded26
752fd9c
 
 
 
4103284
 
 
 
58a0857
67ce405
 
49e9b94
0f8135a
 
 
 
 
 
 
 
 
20daaa7
25d08d8
 
 
 
 
 
 
 
 
 
20daaa7
8d50d70
1eb50b8
 
49e9b94
 
20daaa7
49e9b94
20daaa7
 
 
 
 
 
 
1eb50b8
 
 
 
 
5a30378
1eb50b8
 
 
 
20daaa7
752fd9c
 
bdc9603
752fd9c
 
 
20daaa7
 
 
 
 
67ce405
5cddfb1
20daaa7
 
 
8d50d70
20daaa7
 
 
 
 
 
 
 
 
1db7b87
 
 
 
20daaa7
752fd9c
 
 
 
 
 
 
 
 
 
 
 
 
20daaa7
67ce405
20daaa7
 
 
 
 
 
254373c
 
 
 
 
 
994755c
 
 
 
254373c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492f582
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
import spaces  # imported before torch, as ZeroGPU requires
import sys
import numpy as np
sys.stdout.reconfigure(line_buffering=True) # ensures print() appears in HF Spaces logs in real time

import traceback
import contextlib
import tempfile
import torch
import os
import threading
import librosa
import soundfile as sf
import gradio as gr
from huggingface_hub import snapshot_download
from pyharp import ModelCard, build_endpoint, SampleRate, Channels
from pyharp.tags import Subcategory

from anyaccomp.inference_utils import Sing2SongInferencePipeline

repo_id = "amphion/anyaccomp"
base_dir = os.path.dirname(os.path.abspath(__file__))
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

# Model loading state. Populated by the background thread.
inference_pipeline = None
model_loading = True
model_error = None


def load_model():
    global inference_pipeline, model_loading, model_error
    try:
        checkpoint_marker = os.path.join(base_dir, "pretrained", "flow_matching")
        if not os.path.exists(checkpoint_marker):
            print(f"Downloading model files from {repo_id}...", flush=True)
            model_dir = snapshot_download(repo_id=repo_id, local_dir=base_dir)
            print(f"Model files downloaded to: {model_dir}", flush=True)
        else:
            model_dir = base_dir
            print("Model files already present, skipping download.", flush=True)

        cfg_path = os.path.join(base_dir, "config/flow_matching.json")
        vocoder_cfg_path = os.path.join(base_dir, "config/vocoder.json")
        checkpoint_path = os.path.join(model_dir, "pretrained/flow_matching")
        vocoder_checkpoint_path = os.path.join(model_dir, "pretrained/vocoder")

        print("Initializing AnyAccomp InferencePipeline...", flush=True)
        # Loaded on the CPU, since this runs in a background thread. The worker
        # moves the modules to the GPU once loading is done (see below).
        pipeline = Sing2SongInferencePipeline(
            checkpoint_path, cfg_path,
            vocoder_checkpoint_path, vocoder_cfg_path,
            device="cpu",
        )
        pipeline.sample_rate = 24000
        inference_pipeline = pipeline
        print("Model loaded successfully.", flush=True)
    except Exception as e:
        model_error = str(e)
        print(f"Error loading model: {e}", flush=True)
    finally:
        model_loading = False


# Create a ModelCard describing this endpoint for HARP.
model_card = ModelCard(
    name="AnyAccomp",
    description=(
        "Upload a 3-30 second vocal or instrument track and the model will "
        "generate an accompaniment. The code is MIT and the weights are "
        "CC-BY-4.0, which requires attribution. Generation runs at 24 kHz in "
        "mono; both outputs are resampled back to the input's rate and copied "
        "across its channels, so they match the format they came in with but "
        "carry no stereo image. Repository: "
        "https://github.com/AmphionTeam/AnyAccomp Paper: "
        "https://arxiv.org/abs/2509.14052"
    ),
    author="Junan Zhang, Yunjia Zhang, Xueyao Zhang, Zhizheng Wu",
    tags=[Subcategory.ACCOMPANIMENT, SampleRate(24000), Channels(1), 'music'],
)

# Start model loading in the background so the server can start immediately.
_loader = threading.Thread(target=load_model, daemon=True)
_loader.start()

# pyharp's worker imports this file to reach process_fn (as __mp_main__ rather than
# __main__), so it finishes loading here, before process_fn can reserve a GPU, instead
# of racing the check there. The server itself keeps loading in the background, which
# lets it start at once.
if __name__ != "__main__":
    _loader.join()

    # ZeroGPU wants models placed on CUDA at module level, where its emulation records each
    # placement for every GPU call to replay, rather than inside the call. The worker is the
    # process that runs process_fn, so it places them here, on its main thread, once the
    # loader is done. The server never runs process_fn, so its copy stays on the CPU.
    if inference_pipeline is not None:
        for module in (inference_pipeline.model, inference_pipeline.coco_model,
                       inference_pipeline.vocoder):
            module.to(DEVICE)
        inference_pipeline.device = DEVICE


def process_fn(vocal_filepath, n_timesteps):  # add , cfg_scale to re-enable
    # Checked before the GPU is reserved, so a request the model cannot take spends none of the
    # caller's ZeroGPU allowance
    if model_loading:
        raise gr.Error("Model is still loading, please wait a moment and try again.")
    if inference_pipeline is None:
        raise gr.Error(f"Model failed to load: {model_error}")

    if vocal_filepath is None:
        raise gr.Error("Please upload a vocal audio file.")

    try:
        duration = librosa.get_duration(path=vocal_filepath)
    except Exception as e:
        raise gr.Error(f"Cannot read the audio file: {e}")
    if not (3 <= duration <= 30):
        raise gr.Error(f"Audio must be 3 to 30 seconds long, not {duration:.1f}.")

    return accompany(vocal_filepath, n_timesteps)


@spaces.GPU(duration=60)
@torch.inference_mode()
def accompany(vocal_filepath, n_timesteps):
    try:
        # The session's format, read before the downmix and resample the model
        # needs, and restored on both outputs
        info = sf.info(vocal_filepath)
        original_sr = info.samplerate
        original_channels = info.channels

        vocal_audio, _ = librosa.load(vocal_filepath, sr=24000, mono=True)
        vocal_tensor = torch.tensor(vocal_audio).unsqueeze(0).to(DEVICE)

        vocal_mel = inference_pipeline.encode_vocal(vocal_tensor)

        autocast_ctx = torch.amp.autocast("cuda", dtype=torch.bfloat16) if torch.cuda.is_available() else contextlib.nullcontext()
        with autocast_ctx:
            mel = inference_pipeline.model.reverse_diffusion(
                vocal_mel=vocal_mel,
                n_timesteps=int(n_timesteps),
                cfg=3.0,  # replace with cfg_scale to re-enable
            )

        mel = mel.float()
        wav = inference_pipeline._generate_audio(mel)
        wav = wav.squeeze().detach().cpu().numpy()

        wav = librosa.util.fix_length(data=wav, size=len(vocal_audio))
        mixture_wav = wav + vocal_audio

        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
            accompaniment_path = f.name
        with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
            mixture_path = f.name

        def _restore(audio):
            # Generation is mono at 24 kHz, so the channels are copies rather
            # than a stereo image, which the model card states
            if original_sr != 24000:
                audio = librosa.resample(
                    audio, orig_sr=24000, target_sr=original_sr
                )
            if original_channels > 1:
                audio = np.tile(audio[:, None], (1, original_channels))
            return audio

        sf.write(accompaniment_path, _restore(wav), original_sr)
        sf.write(mixture_path, _restore(mixture_wav), original_sr)

        return accompaniment_path, mixture_path

    except Exception as e:
        traceback.print_exc()
        raise gr.Error(f"An error occurred during processing: {e}")


if __name__ == "__main__":
    with gr.Blocks() as demo:
        input_components = [
            gr.Audio(
                type="filepath",
                label="Input Vocal or Instrument Audio",
            ).harp_required(True).set_info((
                "A dry solo vocal or lead line, 3 to 30 seconds. Accompaniment is "
                "written around what it hears, so a clean take gives a better fit."
            )),
            gr.Slider(
                minimum=10, maximum=100, value=50, step=1,
                label="Inference Steps (n_timesteps)",
                info="Number of diffusion steps. More steps = slower but higher quality",
            ),
            # gr.Slider(minimum=1.0, maximum=10.0, value=3.0, step=0.1, label="CFG Scale", info="Guidance strength — higher values follow the input melody more closely."),
        ]

        output_components = [
            gr.Audio(type="filepath", label="Generated Accompaniment").set_info(
                "The generated instrumental accompaniment."
            ),
            gr.Audio(type="filepath", label="Mixture (Vocal + Accompaniment)").set_info(
                "Vocal mixed with the generated accompaniment."
            ),
        ]

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


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