BUDDy / app.py
Vansh Chugh
move to zerogpu
ee2c2c5
Raw
History Blame Contribute Delete
6.33 kB
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)