File size: 6,914 Bytes
130ece6
 
69c07f8
14fdb19
550f5fe
69c07f8
 
 
 
bad2121
69c07f8
 
 
 
 
 
3d82c4d
24aa6e2
69c07f8
 
 
 
bad2121
 
 
 
9688394
bad2121
 
 
 
 
 
955763d
bad2121
 
69c07f8
 
bad2121
 
25bcb36
bad2121
69c07f8
 
 
 
 
 
bad2121
69c07f8
550f5fe
69c07f8
550f5fe
 
69c07f8
4a31742
69c07f8
 
550f5fe
 
69c07f8
4a31742
69c07f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550f5fe
69c07f8
 
550f5fe
69c07f8
 
bad2121
 
 
02899a1
bad2121
69c07f8
 
550f5fe
54e2282
 
69c07f8
 
bad2121
 
 
 
 
 
 
 
 
 
69c07f8
bad2121
54e2282
 
 
bad2121
54e2282
 
550f5fe
69c07f8
14fdb19
 
bad2121
 
 
14fdb19
 
69c07f8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14fdb19
 
 
 
9688394
14fdb19
 
 
bad2121
 
 
69c07f8
14fdb19
69c07f8
 
 
 
42b239e
 
 
0220997
9688394
0220997
 
 
 
 
 
42b239e
 
 
bad2121
 
42b239e
 
 
 
 
 
 
 
 
69c07f8
e08e7d0
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
import spaces  # imported first, as ZeroGPU requires

import sys
import numpy as np
sys.stdout.reconfigure(line_buffering=True)  # flush print() calls immediately so HF Space logs are live

import os
import tempfile
import traceback
from pathlib import Path

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

from models.resunet import ResUNet30
from models.clap_encoder import CLAP_Encoder

model_card = ModelCard(
    name="AudioSep",
    description=(
        "Separate any sound from a mixture using a natural language text "
        "description. Separation runs at 32 kHz in mono. 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 not its stereo image. "
        "Repository: https://github.com/Audio-AGI/AudioSep Paper: "
        "https://arxiv.org/abs/2308.05037"
    ),
    author="Xubo Liu, Qiuqiang Kong, Yan Zhao, Haohe Liu, Yi Yuan, Yuzhuo Liu, Rui Xia, Yuxuan Wang, Mark D. Plumbley, Wenwu Wang",
    tags=[Subcategory.TARGET_SOURCE_EXTRACTION, SampleRate(32000), Channels(1), 'text-queried', 'domain:general-audio'],
)

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

# 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"}

ss_model = None
query_encoder = None
model_error = None


def load_model():
    global ss_model, query_encoder, model_error
    try:
        print("Downloading checkpoints...")
        main_ckpt_path = hf_hub_download(
            repo_id="Audio-AGI/AudioSep",
            repo_type="space",
            filename="checkpoint/audiosep_base_4M_steps.ckpt",
            revision="0807a713283035d8b0d0628f1b8e3d6580459f7b",
        )
        clap_ckpt_path = hf_hub_download(
            repo_id="Audio-AGI/AudioSep",
            repo_type="space",
            filename="checkpoint/music_speech_audioset_epoch_15_esc_89.98.pt",
            revision="0807a713283035d8b0d0628f1b8e3d6580459f7b",
        )

        query_encoder = CLAP_Encoder(pretrained_path=clap_ckpt_path).eval()
        _ss_model = ResUNet30(input_channels=1, output_channels=1, condition_size=512)

        # Load weights from the Lightning checkpoint — keys are prefixed with "ss_model."
        state_dict = torch.load(main_ckpt_path, map_location="cpu", weights_only=False)["state_dict"]
        weights = {
            k.removeprefix("ss_model."): v
            for k, v in state_dict.items()
            if k.startswith("ss_model.")
        }
        _ss_model.load_state_dict(weights)
        _ss_model.eval().to(DEVICE)

        ss_model = _ss_model
        print("Model loaded successfully.")
    except Exception as e:
        model_error = str(e)
        print(f"Error loading model: {traceback.format_exc()}")


# pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the
# server, so the model is loaded there alone, before process_fn can reserve a GPU. A failure is
# kept rather than raised, so that each request can report it.
if __name__ != "__main__":
    load_model()


def process_fn(audio_path: str, text_query: str):
    # Checked before the GPU is reserved, so a request that cannot run spends none of the caller's
    # ZeroGPU allowance
    if ss_model is None:
        raise gr.Error(f"Model failed to load: {model_error}")
    if not audio_path:
        raise gr.Error("Provide the mixture to separate.")
    try:
        info = sf.info(audio_path)
    except Exception as exc:
        raise gr.Error(f"Could not read the audio file: {exc}") from exc
    if info.frames == 0:
        raise gr.Error("The audio file is empty.")
    if not (text_query or "").strip():
        raise gr.Error("Describe the sound to separate, such as \"acoustic guitar\".")

    return separate(audio_path, text_query.strip())


@spaces.GPU(duration=120)
@torch.inference_mode()
def separate(audio_path: str, text_query: str):

    print(f"Separating [{audio_path}] with query [{text_query}]")

    # The model expects 32 kHz mono. Both are restored after inference, so the
    # result drops into the session it came from rather than at the model's rate
    info = sf.info(audio_path)
    original_sr = info.samplerate
    original_channels = info.channels

    mixture, _ = librosa.load(audio_path, sr=32000, mono=True)

    conditions = query_encoder.get_query_embed(
        modality="text",
        text=[text_query],
        device=DEVICE,
    )

    input_dict = {
        "mixture": torch.tensor(mixture, dtype=torch.float32)[None, None, :].to(DEVICE),
        "condition": conditions,
    }

    # Note: using ss_model.forward() directly.
    # chunk_inference() is not used — it has a latent self.sampling_rate AttributeError
    # in the original source and is only needed for very long audio to reduce peak memory.
    sep_segment = ss_model(input_dict)["waveform"]
    sep_np = sep_segment.squeeze(0).squeeze(0).cpu().numpy()

    if original_sr != 32000:
        sep_np = librosa.resample(sep_np, orig_sr=32000, target_sr=original_sr)

    # Separation here is mono, so the channels are rebuilt by copying rather
    # than recovered, as the model card says
    if original_channels > 1:
        sep_np = np.tile(sep_np[:, None], (1, original_channels))

    input_ext = Path(audio_path).suffix.lower()
    with tempfile.NamedTemporaryFile(suffix=input_ext if input_ext in LOSSLESS else ".wav",
                                     delete=False) as f:
        out_path = f.name
    sf.write(out_path, sep_np, original_sr)

    return out_path


if __name__ == "__main__":
    with gr.Blocks() as demo:
        input_components = [
            gr.Audio(type="filepath", label="Input Audio (Mixture)").harp_required(True).set_info((
                "The mixture containing the sound to isolate."
            )),
            gr.Textbox(label="Text Query", placeholder="e.g. a dog barking",
            info=(
                "Describe the sound to isolate, such as \"acoustic guitar\" or \"a "
                "dog barking\". Plain words work better than studio terms."
            )),
        ]
        output_components = [
            gr.Audio(type="filepath", label="Separated Audio").set_info(
                "The sound matching the description, at the sample rate and channel count of "
                "the input."
            ),
        ]

        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, ssr_mode=False)