File size: 6,768 Bytes
b3b65a0
 
2be1765
b3b65a0
2be1765
b3b65a0
 
 
 
2bb52a1
b3b65a0
2bb52a1
 
2b54be1
 
2bb52a1
 
b3b65a0
 
f3d9515
 
 
0220423
 
 
 
 
f3d9515
 
554e2b1
c266981
b3b65a0
 
 
02915a5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0220423
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2b54be1
3153851
 
 
 
2b54be1
 
 
 
 
 
 
 
 
 
 
 
b3b65a0
2b54be1
 
 
 
 
 
 
 
8ae09f3
 
3153851
2b54be1
3153851
 
2b54be1
 
b3b65a0
2b54be1
0220423
2b54be1
2be1765
 
 
 
 
0c7b128
2be1765
 
 
 
 
b3b65a0
 
4b53fbf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28eed4a
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
from __future__ import annotations

import spaces  # imported first, as ZeroGPU requires

import gradio as gr

from pyharp import *


import contextlib
import tempfile
from pathlib import Path

import soundfile as sf
import torch
from huggingface_hub import hf_hub_download

model_card = ModelCard(
    name="VoiceFixer",
    description=(
        "VoiceFixer aims to restore human speech regardless how serious its "
        "degraded. It can handle noise, reverberation, low resolution "
        "(2kHz~44.1kHz) and clipping (0.1-1.0 threshold) effect within one "
        "model. The model is fixed-rate and mono, so the result is resampled "
        "back to the input's rate and copied across its channels: the output "
        "has the format it came in with, but no stereo image. Repository: "
        "https://github.com/haoheliu/voicefixer Paper: "
        "https://arxiv.org/abs/2109.13731"
    ),
    author="Haohe Liu, Qiuqiang Kong, Qiao Tian, Yan Zhao, DeLiang Wang, Chuanzeng Huang, Yuxuan Wang",
    tags=[Subcategory.RESTORATION, SampleRate(44100), Channels(1), 'speech', 'domain:speech'],
)


# pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the
# server, so the model is fetched and built there alone. voicefixer downloads its checkpoints from
# Zenodo when it is imported, at about 1 MB/s, so the same files, mirrored on the Hub, are linked
# where it looks before downloading, ahead of the import. It picks CUDA itself where there is one.
if __name__ != "__main__":
    _VOICEFIXER_CACHE = Path.home() / ".cache" / "voicefixer"
    for _name, _path in (
        ("vf.ckpt", "analysis_module/checkpoints/vf.ckpt"),
        ("model.ckpt-1490000_trimed.pt", "synthesis_module/44100/model.ckpt-1490000_trimed.pt"),
    ):
        _link = _VOICEFIXER_CACHE / _path
        if not _link.exists():
            _link.parent.mkdir(parents=True, exist_ok=True)
            with contextlib.suppress(FileExistsError):
                _link.symlink_to(hf_hub_download(
                    "teamup-tech/VoiceFixer-weights", _name,
                    revision="11ca799c59df55bbbc438f223bf3b55a64ebced6",
                ))

    from voicefixer import VoiceFixer

    voicefixer_model = VoiceFixer()


def match_input_format(output_path: str, input_path: str) -> str:
    """
    Rewrite an output file at the sample rate and channel count it came in with.

    The models here are fixed-rate and mono, and hand the writing to vendored
    upstream code, so the format is corrected afterwards rather than threaded
    through inference. Duplicated channels are copies, not a recovered stereo
    image; the model card says so.

    Args:
        output_path (str): File the model wrote. Overwritten in place.
        input_path (str): File the request supplied.

    Returns:
        output_path (str): The same path, for use in a return statement.
    """

    import librosa
    import numpy as np
    import soundfile

    want = soundfile.info(input_path)
    have = soundfile.info(output_path)

    if have.samplerate == want.samplerate and have.channels == want.channels:
        return output_path

    data, rate = soundfile.read(output_path, always_2d=True)

    if rate != want.samplerate:
        data = librosa.resample(
            data.T, orig_sr=rate, target_sr=want.samplerate
        ).T

    if data.shape[1] == 1 and want.channels > 1:
        data = np.tile(data, (1, want.channels))
    elif data.shape[1] > 1 and want.channels == 1:
        data = data.mean(axis=1, keepdims=True)

    soundfile.write(output_path, data, want.samplerate)

    return output_path


# The recording is restored in 30-second segments, so the cost grows with its length
# PyHARP stops a job that runs past its timeout_s, 15 minutes by default, so a request whose
# reservation would exceed that is refused rather than started. A shorter one is left to
# ZeroGPU, which admits it when the caller's own allowance covers it.
MAX_GPU_SECONDS = 900


def gpu_seconds(input_audio, output_file, mode):
    return 15 + 0.1 * sf.info(input_audio).duration


@spaces.GPU(duration=gpu_seconds)
def restore(input_audio, output_file, mode):
    voicefixer_model.restore(input_audio, output_file, cuda=torch.cuda.is_available(),
                             mode=int(mode))


def process_fn(input_audio, mode):
    # Checked before the GPU is reserved, so a request the model cannot take spends none of the
    # caller's ZeroGPU allowance
    if not input_audio:
        raise gr.Error("Provide the speech to restore.")
    try:
        duration = sf.info(input_audio).duration
    except Exception as exc:
        raise gr.Error(f"Could not read the audio file: {exc}") from exc
    if duration == 0:
        raise gr.Error("The audio file is empty.")
    if gpu_seconds(input_audio, None, mode) > MAX_GPU_SECONDS:
        raise gr.Error(
            f"The recording is {duration:.0f} s long, more than one request can restore in the "
            f"{MAX_GPU_SECONDS // 60} minutes it may run. Trim it."
        )

    output_file = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
    restore(input_audio, output_file, mode)

    # The format is matched outside the GPU call, which it does not need
    output_file = match_input_format(output_file, input_audio)

    # A lossless input keeps its container. A lossy one comes back as WAV, since encoding the
    # result lossily again would expose the artifacts of the first encode and add a second set.
    input_ext = Path(input_audio).suffix.lower()
    if input_ext in {".flac"}:
        data, rate = sf.read(output_file, always_2d=True)
        lossless_file = str(Path(output_file).with_suffix(input_ext))
        sf.write(lossless_file, data, rate)
        output_file = lossless_file
    return output_file


if __name__ == "__main__":
    with gr.Blocks() as demo:
        input_components = [
            gr.Audio(type="filepath", label="Input Audio").harp_required(True).set_info("Upload an audio file to be processed by VoiceFixer."),
            gr.Dropdown(choices=["0", "1", "2"], value="0", label="Processing Mode", info="Select the VoiceFixer processing mode:\n0: Original Model (suggested by default)\n1: Add preprocessing module (remove higher frequency)\n2: Train mode (might work sometimes on seriously degraded real speech)"),
        ]
        output_components = [
            gr.Audio(type="filepath", label="Fixed Audio").set_info("The enhanced audio output from VoiceFixer."),
        ]
        build_endpoint(
            model_card=model_card,
            input_components=input_components,
            output_components=output_components,
            process_fn=process_fn,
        )

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