Spaces:
Running
Running
Download app.py from teamup-tech/DeepFilterNet: direct link, hf CLI and curl.
- Browser
- Download file 6.59 kB
-
https://huggingface.co/spaces/teamup-tech/DeepFilterNet/resolve/main/app.py
- Command line
-
hf download hf://spaces/teamup-tech/DeepFilterNet/app.py
-
curl -L -o app.py https://huggingface.co/spaces/teamup-tech/DeepFilterNet/resolve/main/app.py
6.59 kB
| import spaces # imported first, as ZeroGPU requires | |
| import os | |
| import tempfile | |
| from pathlib import Path | |
| import gradio as gr | |
| import numpy as np | |
| import soundfile as sf | |
| import librosa | |
| import torch | |
| from huggingface_hub import snapshot_download | |
| from pyharp import ModelCard, build_endpoint, SampleRate, Channels | |
| from pyharp.tags import Subcategory | |
| from df import enhance, init_df | |
| # ----------------------------- | |
| # Model metadata for HARP | |
| # ----------------------------- | |
| model_card = ModelCard( | |
| name="DeepFilterNet", | |
| description=( | |
| "Background noise suppression / speech enhancement using " | |
| "DeepFilterNet3. Slider controls strength through wet/dry blend. Denoising runs at " | |
| "48 kHz in mono. The result is resampled back to the input's rate and copied across its " | |
| "channels, so the output matches the format it came in with but carries " | |
| "no stereo image. Repository: https://github.com/Rikorose/DeepFilterNet " | |
| "Paper: https://arxiv.org/abs/2305.08227" | |
| ), | |
| author="Hendrik Schröter, Alberto N. Escalante-B., Tobias Rosenkranz, Andreas Maier", | |
| tags=[Subcategory.DENOISING, SampleRate(48000), Channels(1), 'domain:speech'], | |
| ) | |
| # 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"} | |
| # ----------------------------- | |
| # Device & Model Initialization | |
| # ----------------------------- | |
| # Auto-detect GPU for Hugging Face deployment | |
| DEVICE_STR = "cuda" if torch.cuda.is_available() else "cpu" | |
| DEVICE = torch.device(DEVICE_STR) | |
| # pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the | |
| # server, so the model is loaded there alone. deepfilternet would download it from the main branch | |
| # of its GitHub repository, so it is read from a pinned mirror on the Hub instead, into a directory | |
| # of its own, since loading writes beside the configuration. | |
| if __name__ != "__main__": | |
| _weights = snapshot_download( | |
| "teamup-tech/DeepFilterNet-weights", revision="b2ec0e0c85db91c696d699a5cfc43ae7a3e3136a", | |
| local_dir=os.path.join(tempfile.gettempdir(), "deepfilternet-weights"), | |
| ) | |
| MODEL, DF_STATE, _ = init_df(model_base_dir=os.path.join(_weights, "DeepFilterNet3"), | |
| log_file=None) | |
| MODEL = MODEL.to(DEVICE) | |
| def load_audio_mono_48k(path: str): | |
| # Load audio from filepath, convert to mono float32, resample to 48kHz. | |
| try: | |
| audio, sr = sf.read(path, always_2d=False) | |
| except Exception: | |
| audio, sr = librosa.load(path, sr=None, mono=False) | |
| audio = np.asarray(audio) | |
| # stereo to mono | |
| if audio.ndim == 2: | |
| audio = audio.mean(axis=1) | |
| audio = audio.astype(np.float32) | |
| # resample to 48k | |
| if sr != 48000: | |
| audio = librosa.resample(audio, orig_sr=sr, target_sr=48000) | |
| sr = 48000 | |
| return audio, sr | |
| def apply_attenuation_db(noisy: np.ndarray, enhanced: np.ndarray, noise_atten_db: float, max_db: float = 30.0): | |
| """ | |
| Map Noise Attenuation (dB) slider to a stable strength in [0,1] | |
| and crossfade between original and enhanced audio. | |
| """ | |
| # convert dB slider into [0,1] | |
| s = float(noise_atten_db) / float(max_db) | |
| s = max(0.0, min(1.0, s)) | |
| # make same length | |
| n = min(len(noisy), len(enhanced)) | |
| noisy = noisy[:n] | |
| enhanced = enhanced[:n] | |
| out = (1.0 - s) * noisy + s * enhanced | |
| return out.astype(np.float32) | |
| def process_fn(input_audio_path: str, noise_atten_db: float) -> str: | |
| # 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_path: | |
| raise gr.Error("Provide the speech to denoise.") | |
| try: | |
| info = sf.info(input_audio_path) | |
| except Exception as exc: | |
| raise gr.Error(f"Could not read the audio file: {exc}") from exc | |
| if info.frames == 0: | |
| raise gr.Error("The audio file is empty.") | |
| return denoise(input_audio_path, noise_atten_db) | |
| def denoise(input_audio_path: str, noise_atten_db: float) -> str: | |
| # The session's format, read before the downmix and resample that | |
| # DeepFilterNet needs, and restored before saving | |
| info = sf.info(input_audio_path) | |
| original_sr = info.samplerate | |
| original_channels = info.channels | |
| # Load and normalize | |
| noisy, sr = load_audio_mono_48k(input_audio_path) | |
| # numpy to torch, add channel dim [T] to [1, T] | |
| noisy_t = torch.from_numpy(noisy).float().unsqueeze(0).to(DEVICE) | |
| # Denoise, then remove added channel and back to numpy | |
| enhanced_t = enhance(MODEL, DF_STATE, noisy_t) | |
| enhanced = enhanced_t.squeeze(0).detach().cpu().numpy() | |
| # Slider strength | |
| out = apply_attenuation_db(noisy, enhanced, noise_atten_db, max_db=30.0) | |
| if original_sr != sr: | |
| out = librosa.resample(out, orig_sr=sr, target_sr=original_sr) | |
| # Denoising is mono here, so the channels are copies rather than a | |
| # recovered image, which the model card states | |
| if original_channels > 1: | |
| out = np.tile(out[:, None], (1, original_channels)) | |
| input_ext = Path(input_audio_path).suffix.lower() | |
| out_path = Path(tempfile.mkdtemp()) / f"denoised{input_ext if input_ext in LOSSLESS else '.wav'}" | |
| sf.write(str(out_path), out, original_sr) | |
| return str(out_path) | |
| # ----------------------------- | |
| # Gradio endpoint | |
| # ----------------------------- | |
| if __name__ == "__main__": | |
| with gr.Blocks() as demo: | |
| input_components = [ | |
| gr.Audio(type="filepath", label="Input Audio").harp_required(True).set_info(( | |
| "A noisy speech recording. Converted to mono 48 kHz internally, so " | |
| "a stereo image is not preserved." | |
| )), | |
| gr.Slider( | |
| minimum=0, | |
| maximum=30, | |
| step=1, | |
| value=12, | |
| label="Noise Attenuation (dB)", | |
| info="0 = no change, 30 = strongest. Implemented as wet/dry strength.", | |
| ), | |
| ] | |
| output_components = [ | |
| gr.Audio(type="filepath", label="Denoised Audio") | |
| .set_info("Denoised audio output."), | |
| ] | |
| app = 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, ssr_mode=False) | |