Spaces:
Running on Zero
Running on Zero
| 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 ---- | |
| 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) | |