File size: 4,041 Bytes
d98780c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61431ae
 
 
d98780c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96f6a55
d98780c
 
61431ae
d98780c
61431ae
96f6a55
 
d98780c
 
 
 
 
 
c25f878
d98780c
 
 
 
 
 
 
 
 
 
 
 
 
c25f878
 
d98780c
 
 
 
 
 
 
 
 
 
61431ae
 
d98780c
 
 
 
 
 
 
582f1c3
d98780c
 
582f1c3
 
d98780c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import sys

sys.stdout.reconfigure(line_buffering=True)

try:
    import spaces
except ImportError:
    # keep @spaces.GPU usable as a no-op; ZeroGPU requires this exact name.
    class spaces:
        class GPU:
            def __init__(self, func=None, duration=60):
                self.func = func

            def __call__(self, *args, **kwargs):
                if self.func is not None:
                    return self.func(*args, **kwargs)
                func = args[0]
                return func


import gradio as gr
import soundfile as sf
import torch
import torchaudio.functional as AF
from pyharp import ModelCard, build_endpoint

from model.speech_models.metricgan_generator import MetricGANGenerator
from model.speech_models.fullsubnet import PhaseInvariantFullSubNet

# Constructed directly rather than via the original repo's LightningCLI config loader,
# since only speech_model (never reverb_model/joint_loss_module) is used at inference.

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

PRESETS = {
    "Standard": {
        "cls": MetricGANGenerator,
        "ckpt": "checkpoints/unsupervised_bilstm/model.ckpt",
    },
    "Enhanced (phase-aware, weak supervision)": {
        "cls": PhaseInvariantFullSubNet,
        "ckpt": "checkpoints/phaseinv_fsn/model.ckpt",
    },
}

_models = {}  # preset name -> model instance already resident on DEVICE


def get_model(preset):
    """Builds and caches a preset's model."""
    if preset not in _models:
        cfg = PRESETS[preset]
        model = cfg["cls"](metrics=[])  # config's metrics are for validation logging, unused at inference
        model.load_state_dict_from_joint_model(cfg["ckpt"])
        model.eval()  # skips the original's .freeze(); @torch.inference_mode() already covers it
        # .to() recurses into FirstLevelModule's shared stft/istft singleton too
        model = model.to(DEVICE)
        _models[preset] = model
    return _models[preset]


model_card = ModelCard(
    name="U-DREAM",
    description="Removes room reverb from a speech recording. Mono only; stereo input is downmixed.",
    author="Louis Bahrman, Marius Rodrigues, Mathieu Fontaine, Gaël Richard",
    tags=["dereverberation", "speech enhancement"],
)


@spaces.GPU
@torch.inference_mode()
def process_fn(input_audio_path: str, preset: str) -> str:
    """Runs the selected dereverberation model on one audio file."""
    model = get_model(preset)

    data, fs = sf.read(input_audio_path, dtype="float32", always_2d=True)
    y = torch.from_numpy(data.T).to(DEVICE)  # (channels, samples)
    if y.size(0) > 1:
        y = y.mean(dim=0, keepdim=True)  # model is monaural-only (per paper)
    if fs != model.fs:
        # musicians shouldn't have to pre-convert sample rate themselves
        y = AF.resample(y, orig_freq=fs, new_freq=model.fs)
        fs = model.fs

    pred = model(y[None, ...])
    s = model.get_time(pred)
    s = s / s.abs().max()

    output_audio_path = input_audio_path.rsplit(".", 1)[0] + "_predicted_dry.wav"
    out = s[0].cpu().numpy()  # (1, samples): mono in guarantees mono out
    sf.write(output_audio_path, out[0], fs)
    return output_audio_path


with gr.Blocks() as demo:
    input_components = [
        gr.Audio(type="filepath", label="Input Audio").harp_required(True),
        gr.Dropdown(
            choices=[("Standard", "Standard"), ("Enhanced", "Enhanced (phase-aware, weak supervision)")],
            value="Standard",
            label="Preset",
            info="Standard: general-purpose reverb removal (paper's recommended BiLSTM default). "
            "Enhanced: can be better or worse (phase-invariant FullSubNet).",
        ),
    ]
    output_components = [
        gr.Audio(type="filepath", label="Output Audio").set_info("Dereverberated (dry) speech."),
    ]

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

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