File size: 5,323 Bytes
6dea0da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dc8400f
 
6dea0da
 
dc8400f
 
6dea0da
dc8400f
6dea0da
 
 
dc8400f
6dea0da
 
dc8400f
6dea0da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dc8400f
 
6dea0da
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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 tempfile

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

from model.config import CHECKPOINTS, CHECKPOINTS_REPO
from model.cqtdiff import Unet_CQT_oct_with_attention
from model.edm import EDM
from model.restoration import restore_audio

# one entry per architecture ("piano"/"singing"), holding everything that's
# lazily built/loaded once and reused across requests for that architecture
_state = {}


def get_arch_state(base_cfg, device):
    """Lazily builds (once per architecture) and caches the network and EDM diffusion parameters on `device`."""
    key = base_cfg.architecture
    if key not in _state:
        net = Unet_CQT_oct_with_attention(base_cfg, device)
        net.to(device)
        net.eval()
        _state[key] = {"network": net, "diff_params": EDM(base_cfg), "loaded_checkpoint": None, "ltas_ref": None}
    return _state[key]


def load_checkpoint(label, device):
    """Downloads (if not already cached locally) and loads the checkpoint for the selected voice/instrument, reusing the shared network for its architecture."""
    filename, base_cfg = CHECKPOINTS[label]
    state = get_arch_state(base_cfg, device)

    if state["loaded_checkpoint"] != filename:
        ckpt_path = hf_hub_download(repo_id=CHECKPOINTS_REPO, filename=filename)
        checkpoint = torch.load(ckpt_path, map_location="cpu", weights_only=False)
        state["network"].load_state_dict(checkpoint["ema"])
        state["ltas_ref"] = checkpoint["LTAS"]
        state["loaded_checkpoint"] = filename

    return state["network"], state["diff_params"], state["ltas_ref"], base_cfg


model_card = ModelCard(
    name="BABE-2",
    description="Restores degraded historical piano or singing-voice recordings with a diffusion-based generative equalizer.",
    author="Eloi Moliner, Maija Turunen, Filip Elvander, Vesa Välimäki",
    tags=["restoration", "equalizer", "diffusion", "historical recordings"],
)


@spaces.GPU(duration=180)
def process_fn(input_audio_path: str, checkpoint_label: str, steps: int, strength: float) -> str:
    """Restores the input recording using the selected voice/instrument model, diffusion step count, and restoration strength."""
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    net, diff_params, ltas_ref, base_cfg = load_checkpoint(checkpoint_label, device)
    ltas_ref = ltas_ref.to(device)

    cfg = OmegaConf.create(OmegaConf.to_container(base_cfg, resolve=True))
    cfg.tester.T = int(steps)
    cfg.tester.posterior_sampling.xi = float(strength)

    audio, sr = sf.read(input_audio_path)
    sig = torch.tensor(audio, dtype=torch.float32)
    if sig.dim() > 1:
        sig = sig.mean(dim=-1)
    if sr != cfg.exp.sample_rate:
        sig = torchaudio.functional.resample(sig, sr, cfg.exp.sample_rate)

    def on_progress(fraction, desc):
        print(f"[{fraction:.0%}] {desc}")

    restored = restore_audio(sig, cfg, net, diff_params, ltas_ref, device, on_progress=on_progress)

    out_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
    sf.write(out_path, restored.cpu().numpy(), cfg.exp.sample_rate)
    return out_path


def update_strength_default(checkpoint_label):
    """Updates the Restoration Strength slider to the selected model's paper-recommended default."""
    _, base_cfg = CHECKPOINTS[checkpoint_label]
    return gr.update(value=float(base_cfg.tester.posterior_sampling.xi))


with gr.Blocks() as demo:
    checkpoint_dropdown = gr.Dropdown(
        choices=list(CHECKPOINTS.keys()),
        value="Piano (MAESTRO)",
        label="Voice / Instrument",
        info="Singer-specific models give the most faithful restoration when your recording actually resembles that singer.",
    )

    input_components = [
        gr.Audio(type="filepath", label="Input Audio").harp_required(True),
        checkpoint_dropdown,
        gr.Slider(minimum=10, maximum=100, step=1, value=51, label="Processing Steps",
                   info="More steps can improve quality at the cost of processing time."),
        gr.Slider(minimum=0.0, maximum=2.0, step=0.05, value=1.0, label="Restoration Strength",
                   info="How strongly the output is guided to match the input recording."),
    ]
    output_components = [
        gr.Audio(type="filepath", label="Restored Audio").set_info("The restored recording."),
    ]

    checkpoint_dropdown.change(fn=update_strength_default, inputs=checkpoint_dropdown, outputs=input_components[3])

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

demo.queue().launch(pwa=True)