File size: 6,331 Bytes
a95f6c0
 
 
ee2c2c5
 
 
 
 
 
a95f6c0
 
 
 
 
ee2c2c5
a95f6c0
 
 
 
 
 
 
928845d
a95f6c0
 
 
928845d
 
a95f6c0
5560f6c
 
a95f6c0
 
 
 
 
 
ee2c2c5
a95f6c0
 
 
 
 
 
 
 
 
 
 
 
 
928845d
0b8b6f4
 
ee2c2c5
 
 
a95f6c0
 
 
ee2c2c5
a95f6c0
 
 
928845d
c408069
 
 
a95f6c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e92a2ea
a95f6c0
ee2c2c5
a95f6c0
 
 
 
ee2c2c5
 
 
a95f6c0
 
 
 
 
 
ee2c2c5
 
 
a95f6c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee2c2c5
 
a95f6c0
 
 
ee2c2c5
a95f6c0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ee2c2c5
 
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
import sys
sys.stdout.reconfigure(line_buffering=True)  # real-time logs in HF Spaces

try:
    import spaces
    def gpu_decorator(func): return spaces.GPU(func)
except ImportError:
    def gpu_decorator(func): return func

import contextlib
import tempfile
import threading
import traceback

import soundfile as sf
import torch
import torchaudio
from omegaconf import OmegaConf
import gradio as gr
from pyharp import ModelCard, build_endpoint

# ---- Paths and device ----
CKPT_PATH = "pretrained/VCTK_16k_4s_time-190000.pt"
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

# ---- Inference config ----
ARGS = OmegaConf.load("config/inference.yaml")
OP_HP = OmegaConf.load("config/operator.yaml")

SAMPLE_RATE = ARGS.exp.sample_rate
AUDIO_LEN = ARGS.exp.audio_len


# ---- Model loading ----
sampler = None
model_loading = True
model_error = None
model_ready = False  # has the network been moved onto the GPU yet?

def load_model():
    global sampler, model_loading, model_error
    try:
        print(f"Loading checkpoint from {CKPT_PATH}...")

        # imported here (not at top) so the Gradio server starts before these load

        from networks.ncsnpp import NCSNppTime
        from diff_params.edm import EDM
        from utils.training_utils import load_state_dict
        from testing.EulerHeunSamplerDPS import EulerHeunSamplerDPS

        network_cfg = OmegaConf.load("config/network.yaml")
        # stft stays as OmegaConf — NCSNppTime uses dot access on it (stft_kwargs.n_fft)
        stft_cfg = network_cfg.pop("stft")
        # built on CPU — ZeroGPU only intercepts CUDA calls inside an
        # @spaces.GPU-decorated call, not from this background thread
        network = NCSNppTime(stft=stft_cfg, **OmegaConf.to_container(network_cfg))

        # load_state_dict tries multiple key strategies ('ema', 'model', etc.)
        # to handle checkpoints saved in different formats
        state_dict = torch.load(CKPT_PATH, map_location="cpu", weights_only=False)
        load_state_dict(state_dict, ema=network)
        network.eval()

        diff_params_cfg = OmegaConf.load("config/diff_params.yaml")
        # sde_hp stays as OmegaConf — EDM uses dot access on it (sde_hp.sigma_data)
        sde_hp = diff_params_cfg.pop("sde_hp")
        diff_params = EDM(sde_hp=sde_hp, **OmegaConf.to_container(diff_params_cfg))

        sampler = EulerHeunSamplerDPS(network, diff_params, ARGS)
        print("Model ready.")

    except Exception:
        model_error = traceback.format_exc()
        print(f"Error loading model:\n{model_error}")
    finally:
        model_loading = False

# Load in background so the Gradio server starts immediately
threading.Thread(target=load_model, daemon=True).start()


# ---- pyharp model card ----
model_card = ModelCard(
    name="BUDDy - Blind Dereverberation",
    description="Removes room reverberation from a speech recording. No room measurements needed — the model estimates the room acoustics automatically.",
    author="Lemercier, Moliner, Welker, Välimäki, Gerkmann (2024)",
    tags=["speech", "dereverberation", "effect removal"],
)


# ---- Inference ----
@gpu_decorator
def process_fn(input_audio_path: str, num_steps: int):
    global model_ready
    if model_loading:
        raise gr.Error("Model is still loading, please wait a moment and try again.")
    if sampler is None:
        raise gr.Error(f"Model failed to load: {model_error}")
    if not model_ready:
        sampler.model.to(DEVICE)  # only safe here, inside @spaces.GPU
        model_ready = True

    from testing.operators.subband_filtering import BlindSubbandFiltering

    # Update step count from slider — also update args so get_gamma() uses the right T
    sampler.T = num_steps

    # using soundfile directly — torchaudio.load/save need torchcodec, which isn't installed
    data, sr = sf.read(input_audio_path)
    waveform = torch.tensor(data.T if data.ndim > 1 else data[None]).float()  # (channels, samples)

    # resampling to 16kHz
    if sr != SAMPLE_RATE:
        waveform = torchaudio.functional.resample(waveform, sr, SAMPLE_RATE)
    
    # converting to mono
    if waveform.shape[0] > 1:
        waveform = waveform.mean(dim=0, keepdim=True)
    
    # trimming/padding to exactly AUDIO_LEN
    if waveform.shape[-1] > AUDIO_LEN:
        waveform = waveform[..., :AUDIO_LEN]
    elif waveform.shape[-1] < AUDIO_LEN:
        waveform = torch.nn.functional.pad(waveform, (0, AUDIO_LEN - waveform.shape[-1]))
    y = waveform.squeeze(0).to(DEVICE)  # (AUDIO_LEN,)

    # Normalize to match sigma_data of the training set (0.05)
    y = ARGS.tester.posterior_sampling.warm_initialization.scaling_factor * y / (y.std() + 1e-8)
    y = y.unsqueeze(0)  # (1, AUDIO_LEN) — sampler expects a batch dimension

    # tester.py line 147
    operator = BlindSubbandFiltering(OP_HP, sample_rate=SAMPLE_RATE)
    with torch.no_grad():
        operator.update_H(use_noise=True)

    # Run the DPS sampler. No torch.inference_mode() here because the DPS
    # likelihood gradient uses torch.autograd.grad() on intermediate tensors.
    with contextlib.nullcontext():
        pred = sampler.predict_conditional(y, operator, shape=(1, AUDIO_LEN), blind=True)

    pred = pred.detach().cpu()
    if pred.dim() > 1:
        pred = pred.squeeze(0)

    with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
        out_path = f.name
    sf.write(out_path, pred.numpy(), SAMPLE_RATE)
    return out_path


# ---- Gradio UI ----
with gr.Blocks() as demo:
    input_components = [
        gr.Audio(type="filepath", label="Reverberant Audio").harp_required(True),
        gr.Slider(
            minimum=30,
            maximum=400,
            step=1,
            value=201,
            label="Processing Steps",
            info="More steps = higher quality but slower. Paper default: 201. Max: 400.",
        ),
    ]
    output_components = [
        gr.Audio(type="filepath", label="Dereverberated Audio").set_info(
            "Clean speech with room reverb removed."
        ),
    ]
    build_endpoint(
        model_card=model_card,
        input_components=input_components,
        output_components=output_components,
        process_fn=process_fn,
    )

if __name__ == "__main__":
    demo.queue().launch(share=True, show_error=True, pwa=True)