Spaces:
Sleeping
Sleeping
File size: 6,616 Bytes
483b1a0 6a55892 2d6c124 6a55892 5a65c42 483b1a0 6a55892 3f9f23c 6a55892 bc7dcf8 6a55892 483b1a0 9c86e14 483b1a0 6a55892 6f5e44d 259243b 6f5e44d 6a55892 cd3b1e6 2d6c124 6a55892 ea6ba44 2d6c124 6a55892 6f5e44d cd3b1e6 2d6c124 cd3b1e6 483b1a0 cd3b1e6 6f5e44d cd3b1e6 6a55892 cd3b1e6 6f5e44d cd3b1e6 483b1a0 cd3b1e6 6a55892 483b1a0 9c86e14 483b1a0 0d7d75f 483b1a0 0d7d75f 6a55892 0c1f8cc b2336e6 0c1f8cc 6f5e44d 259243b 6f5e44d 0c1f8cc 9c86e14 0c1f8cc 6f5e44d 0c1f8cc 6a55892 6b7d106 | 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 | import spaces # imported first, as ZeroGPU requires
import tempfile
from math import ceil
from pathlib import Path
import gradio as gr
import numpy as np
import soundfile as sf
import torch
import torch.nn.functional as F
import torchaudio
from flowhigh import FlowHighSR
from huggingface_hub import snapshot_download
from pyharp import ModelCard, build_endpoint
from pyharp.tags import SampleRate, Subcategory
SAMPLE_RATE = 48000
model_card = ModelCard(
name="FLowHigh",
description=(
"Extend the bandwidth of low-resolution audio with flow matching. "
"Repository: https://github.com/resemble-ai/flowhigh Paper: "
"https://arxiv.org/abs/2501.04926"
),
author="Jun-Hak Yun, Seung-Bin Kim, Seong-Whan Lee",
tags=[Subcategory.BANDWIDTH_EXTENSION, SampleRate(SAMPLE_RATE), '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 downloaded and placed there alone
if __name__ != "__main__":
checkpoint_dir = snapshot_download(
repo_id="ResembleAI/FlowHigh",
revision="4281fe4119e5f3d209a9a893218fed85af2e5bbe",
allow_patterns=["*.json", "*.pt"],
)
model = FlowHighSR.from_local(checkpoint_dir, device="cuda")
# 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.
LOSSLESS = {".wav", ".flac"}
# The model runs over the whole recording at once, a pass per channel, so the cost grows with
# its length and its channels. Its attention spans the whole recording, so memory grows with the
# square of the length: 120 s measured at about 6 s, while 300 s ran out of GPU memory
MAX_INPUT_SECONDS = 120
def gpu_seconds(input_audio_path: str, output_sample_rate: str) -> float:
# Measured at about 6 s for 120 s of mono speech, padded
info = sf.info(input_audio_path)
return 10 + 0.1 * info.duration * info.channels
def process_fn(input_audio_path: str, output_sample_rate: str) -> str:
# Checked before the GPU is reserved, so a request the model cannot take spends none of the
# caller's ZeroGPU allowance
# Read with soundfile rather than torchaudio, whose ffmpeg decoding fails outside a GPU call
# under ZeroGPU's emulation
if not input_audio_path:
raise gr.Error("Choose an audio file.")
try:
audio, input_rate = sf.read(input_audio_path, always_2d=True)
except Exception as exc:
raise gr.Error(f"Could not read the audio file: {exc}") from exc
if len(audio) == 0:
raise gr.Error("The audio file is empty.")
duration = len(audio) / input_rate
if duration > MAX_INPUT_SECONDS:
raise gr.Error(
f"The recording is {duration:.1f} s long. Trim it to {MAX_INPUT_SECONDS // 60} "
"minutes or less."
)
# Upstream divides by the input peak, so silence bypasses inference, and the GPU with it
if not np.any(audio):
output_rate = int(output_sample_rate)
silence = torch.zeros(audio.shape[1], ceil(len(audio) * output_rate / input_rate))
return save(silence, output_rate, input_audio_path)
return extend(input_audio_path, output_sample_rate)
@spaces.GPU(duration=gpu_seconds)
@torch.inference_mode()
def extend(input_audio_path: str, output_sample_rate: str) -> str:
audio, input_rate = torchaudio.load(input_audio_path)
output_rate = int(output_sample_rate)
output_length = ceil(audio.shape[-1] * output_rate / input_rate)
native_length = ceil(audio.shape[-1] * SAMPLE_RATE / input_rate)
minimum_length = ceil(2048 * input_rate / SAMPLE_RATE)
if audio.shape[-1] < minimum_length:
audio = F.pad(audio, (0, minimum_length - audio.shape[-1]))
# Extending the bandwidth needs no other channel, so each is extended on its own and the
# stereo image survives. The model normalizes what it is given, so each result is put back at
# its channel's level relative to the loudest, and a silent channel, which it cannot
# normalize, stays silent.
peaks = audio.abs().amax(dim=-1)
extended = {
index: model.generate(audio[index:index + 1], input_rate, SAMPLE_RATE, timestep=1).cpu()
* (peaks[index] / peaks.max())
for index in range(audio.shape[0])
if peaks[index] > 0
}
length = next(iter(extended.values())).shape[-1]
result = torch.cat([extended.get(index, torch.zeros(1, length))
for index in range(audio.shape[0])])
result = result[..., :native_length]
if output_rate != SAMPLE_RATE:
result = torchaudio.functional.resample(result, SAMPLE_RATE, output_rate)
result = result[..., :output_length]
peak = result.abs().max()
if peak > 1:
result *= 0.99 / peak
return save(result, output_rate, input_audio_path)
def save(result: torch.Tensor, output_rate: int, input_audio_path: str) -> str:
# A WAV keeps the 32-bit float results, and FLAC takes 24-bit integers
input_ext = Path(input_audio_path).suffix.lower()
output_ext = input_ext if input_ext in LOSSLESS else ".wav"
directory = Path(tempfile.mkdtemp(prefix="flowhigh-"))
output_path = directory / f"{directory.name}{output_ext}"
sf.write(output_path, result.T.numpy(), output_rate,
subtype="FLOAT" if output_ext == ".wav" else "PCM_24")
return str(output_path)
if __name__ == "__main__":
with gr.Blocks(title="FLowHigh", delete_cache=(600, 3600)) as demo:
input_audio = (
gr.Audio(type="filepath", label="Input Audio")
.harp_required(True)
.set_info(
"Low-bandwidth audio to extend, up to 2 minutes. Each channel is extended on its "
"own."
)
)
output_rate = gr.Dropdown(
choices=["24000", "48000", "96000"],
value="48000",
label="Output Sample Rate",
).set_info("The model runs at 48 kHz, so 24 and 96 kHz outputs are resampled.")
output_audio = gr.Audio(type="filepath", label="Enhanced Audio").set_info(
"WAV at the selected sample rate, with the input's channels. Downloads are "
"retained for one hour."
)
build_endpoint(
model_card=model_card,
input_components=[input_audio, output_rate],
output_components=[output_audio],
process_fn=process_fn,
)
demo.queue(default_concurrency_limit=1).launch(show_error=True, pwa=True, ssr_mode=False)
|