Spaces:
Running on Zero
Running on Zero
File size: 8,486 Bytes
e7ab6bb | 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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | """Universal speech enhancement with USES.
Published as https://huggingface.co/spaces/espnet/universal-se; the source
lives in espnet, at egs2/universal_se_v1/enh1/demo. The recipe that trained
the checkpoint is the directory above this one.
One of the five demo Spaces this repository maintains; the others are
egs2/owsm_ctc_v4/s2t1/demo, egs2/owsm_v4/s2t1/demo, egs2/ljspeech/tts1/demo
and egs2/voxceleb/spk1/demo.
"""
# The ZeroGPU package patches torch as it is imported, so it has to come
# first - before torch, and before anything that imports torch.
try: # only Hugging Face's runners have it
import spaces # isort: skip
except ImportError: # running elsewhere: the decorator does nothing
class spaces: # noqa: N801 - stands in for the module
@staticmethod
def GPU(func=None, **kwargs):
return func if func is not None else (lambda f: f)
import os # noqa: E402
import re # noqa: E402
import gradio as gr # noqa: E402
import librosa # noqa: E402
import torch # noqa: E402
from espnet2.bin.enh_inference import SeparateSpeech # noqa: E402
# USES runs a recurrent block over the whole recording rather than over a
# fixed window, so the cost is linear in the input and half a minute is what
# comfortably fits the GPU slice asked for below. Longer audio is trimmed
# rather than refused.
MAX_SECS = 30
GPU_SECONDS = 120
MODEL_TAG = os.environ.get(
"ENH_MODEL_TAG", "espnet/Wangyou_Zhang_universal_train_enh_uses_refch0_2mem_raw"
)
# ZeroGPU attaches the GPU only while a @spaces.GPU function runs, so
# torch.cuda.is_available() is False here and asking it would pin the models to
# the CPU on the very hardware bought to run them. SPACES_ZERO_GPU is the
# runtime's own marker; `spaces` being importable is not, since anyone can
# install it.
ZERO_GPU = bool(os.environ.get("SPACES_ZERO_GPU"))
if os.environ.get("DEVICE"):
DEVICE = os.environ["DEVICE"]
elif ZERO_GPU or torch.cuda.is_available():
DEVICE = "cuda"
else:
DEVICE = "cpu"
KEEP = "Same as the file"
EXAMPLE_WAV = (
"https://github.com/espnet/espnet/raw/master/test_utils/ctc_align_test.wav"
)
def _categories(model):
"""The training conditions a checkpoint names, in a stable order.
ESPnet stores them as {index: name} so that a category id in the data
means the same thing at inference; a checkpoint trained without them has
none, and every menu below is then empty.
"""
categories = getattr(model, "categories", None) or {}
if isinstance(categories, dict):
return [categories[key] for key in sorted(categories)]
return list(categories)
def _rates(categories):
"""The sample rates a category list names, highest first.
A category reads like "2ch_16k" or "1ch_16000Hz": two channels at
16 kHz, one channel at 16 kHz. Recipes spell the rate both ways - USES
one way, the URGENT 2025 recipe the other - and a checkpoint reached
through ENH_MODEL_TAG comes from whichever recipe trained it, so both
spellings fill the menu.
"""
found = set()
for category in categories:
match = re.search(r"(\d+)k(?:Hz)?\b", category)
if match:
found.add(int(match.group(1)) * 1000)
continue
match = re.search(r"(\d+)\s*Hz", category)
if match:
found.add(int(match.group(1)))
return sorted(found, reverse=True)
def _channels(categories):
"""The largest channel count a category list names, at least one."""
found = {1}
for category in categories:
match = re.match(r"(\d+)ch", category)
if match:
found.add(int(match.group(1)))
return max(found)
TITLE = "Universal speech enhancement"
DESCRIPTION = """# Universal speech enhancement
USES is a single speech enhancement model for every condition the task
usually splits into separate ones: noise, reverberation, one microphone or
several, and any sample rate. It was trained on the
[`egs2/universal_se_v1`](https://github.com/espnet/espnet/tree/master/egs2/universal_se_v1)
data with [ESPnet](https://github.com/espnet/espnet), and it is the model
`espnet enhance` uses by default.
Upload something noisy. Multi-channel files keep their channels on the way
in, because a model that can use them should be given them; what comes back
is one channel.
"""
ARTICLE = """Model:
[`espnet/Wangyou_Zhang_universal_train_enh_uses_refch0_2mem_raw`](https://huggingface.co/espnet/Wangyou_Zhang_universal_train_enh_uses_refch0_2mem_raw).
Source of this Space:
[`egs2/universal_se_v1/enh1/demo`](https://github.com/espnet/espnet/tree/master/egs2/universal_se_v1/enh1/demo).
```bibtex
@inproceedings{zhang2023uses,
title={Toward Universal Speech Enhancement for Diverse Input Conditions},
author={Zhang, Wangyou and Saijo, Kohei and Wang, Zhong-Qiu and
Watanabe, Shinji and Qian, Yanmin},
booktitle={Proc. ASRU},
year={2023}
}
```"""
enh = SeparateSpeech.from_pretrained(MODEL_TAG, device=DEVICE)
# The menu comes from the checkpoint. USES is sampling-frequency independent,
# so any rate runs; the rates it was trained on are the ones worth offering,
# and the recipe records them in the category names. The same names say how
# many channels it has ever seen, which is what the warning below compares
# against.
CATEGORIES = _categories(enh.enh_model)
RATES = _rates(CATEGORIES)
MAX_CHANNELS = _channels(CATEGORIES)
RATE_LABELS = [f"{rate // 1000} kHz" for rate in RATES]
RATE_OF_LABEL = dict(zip(RATE_LABELS, RATES))
# One output per speaker the checkpoint separates: this one enhances, so it
# has a single output, and a separation checkpoint reached through
# ENH_MODEL_TAG grows the column instead of dropping speakers on the floor.
OUTPUT_LABELS = (
["Enhanced"]
if enh.num_spk == 1
else [f"Speaker {i}" for i in range(1, enh.num_spk + 1)]
)
def _read(path):
"""The recording as (samples, channels) or (samples,), at its own rate."""
if path is None:
raise gr.Error("Record or upload some audio first.")
# The header says how long the file is, so only the part that is enhanced
# is decoded: an hour of audio uploaded to a 30 s demo is not read in full
# first, and the warning is still about the file rather than about what
# came back.
if librosa.get_duration(path=path) > MAX_SECS:
gr.Warning(
f"Only the first {MAX_SECS} s were enhanced. "
"Run the app yourself for longer audio - the model has no such "
"limit."
)
# sr=None keeps the file's rate: the model is free to work at it, and
# resampling is the user's choice below rather than a silent one here.
speech, rate = librosa.load(path, sr=None, mono=False, duration=MAX_SECS)
if speech.ndim > 1: # librosa hands back (channels, samples)
speech = speech.T
channels = speech.shape[1] if speech.ndim > 1 else 1
if channels > MAX_CHANNELS:
gr.Warning(
f"This file has {channels} channels and the model was trained on "
f"at most {MAX_CHANNELS}; it will run, but off what it knows."
)
return speech, rate
@spaces.GPU(duration=GPU_SECONDS)
def predict(audio_path, rate_label):
speech, rate = _read(audio_path)
target = RATE_OF_LABEL.get(rate_label, rate)
if target != rate:
# axis=0 is the sample axis for both shapes _read returns
speech = librosa.resample(speech, orig_sr=rate, target_sr=target, axis=0)
rate = target
waves = enh(speech[None, ...], fs=rate)
return [(rate, wave[0]) for wave in waves]
EXAMPLES = [[EXAMPLE_WAV, KEEP]]
with gr.Blocks(title=TITLE) as demo:
gr.Markdown(DESCRIPTION)
with gr.Row():
with gr.Column():
audio = gr.Audio(
sources=["microphone", "upload"], type="filepath", label="Noisy audio"
)
rate = gr.Dropdown(
[KEEP] + RATE_LABELS,
value=KEEP,
label="Process at",
info="The rates this model was trained on",
)
button = gr.Button("Enhance", variant="primary")
with gr.Column():
outputs = [gr.Audio(label=label) for label in OUTPUT_LABELS]
button.click(predict, [audio, rate], outputs)
gr.Examples(
EXAMPLES,
inputs=[audio, rate],
outputs=outputs,
fn=predict,
cache_examples=False,
)
gr.Markdown(ARTICLE)
if __name__ == "__main__":
demo.launch()
|