Spaces:
Running
Running
Download app.py from teamup-tech/AudioSeal: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/teamup-tech/AudioSeal/resolve/main/app.py
- Command line
-
hf download hf://spaces/teamup-tech/AudioSeal/app.py
-
curl -L -o app.py https://huggingface.co/spaces/teamup-tech/AudioSeal/resolve/main/app.py
10.4 kB
| import spaces # imported first, as ZeroGPU requires | |
| import os | |
| import sys | |
| import uuid | |
| from pathlib import Path | |
| from contextlib import contextmanager | |
| import ruptures as rpt | |
| import numpy as np | |
| import soundfile as sf | |
| import torch | |
| import gradio as gr | |
| import librosa | |
| from pyharp.core import ModelCard, build_endpoint | |
| from pyharp.media.audio import save_audio | |
| from pyharp import LabelList, AudioLabel, OutputLabel, SampleRate, Channels | |
| from pyharp.tags import Category | |
| from audiotools import AudioSignal | |
| from audioseal import AudioSeal | |
| LOUDNESS_DB = -16. | |
| SAMPLE_RATE = 48_000 | |
| ENCODEC_SAMPLE_RATE = 16_000 | |
| AUDIOSEAL_SAMPLE_RATE = 16_000 | |
| model_card = ModelCard( | |
| name="AudioSeal", | |
| description=( | |
| "Meta AudioSeal watermarking generation and detection model The " | |
| "watermark is applied under 16kHz. 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/facebookresearch/audioseal " | |
| "Paper: https://arxiv.org/abs/2401.17264" | |
| ), | |
| author="Robin San Roman, Pierre Fernandez, Alexandre Défossez, Teddy Furon, Tuan Tran, Hady Elsahar", | |
| tags=[Category.UTILITY, SampleRate(16000), Channels(1), 'domain:speech'] | |
| ) | |
| DEVICE = "cuda" if torch.cuda.is_available() else "cpu" | |
| # 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"} | |
| # pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the | |
| # server, so both models are loaded there alone | |
| if __name__ != "__main__": | |
| print("Initializing AudioSeal model...") | |
| generator = AudioSeal.load_generator("audioseal_wm_16bits") | |
| detector = AudioSeal.load_detector("audioseal_detector_16bits") | |
| generator.eval().to(DEVICE) | |
| detector.eval().to(DEVICE) | |
| def load_audio(audio_path): | |
| try: | |
| wav, sr = librosa.load(audio_path, mono=True) | |
| return wav, sr | |
| except Exception as e: | |
| print(f"Audio preprocessing failed: {e}") | |
| raise ValueError(f"Failed to load audio: {str(e)}") | |
| def split_bands(signal: AudioSignal, sample_rate: float = ENCODEC_SAMPLE_RATE): | |
| nyq = sample_rate // 2 | |
| high = signal.clone().high_pass(cutoffs=int(nyq * 0.95), zeros=51) | |
| low = signal.clone().low_pass(cutoffs=int(nyq * 1.05), zeros=51) | |
| loud_db = low.loudness() | |
| low = low.resample(sample_rate) | |
| return low, high, loud_db | |
| def merge_bands(low, high, loud_db): | |
| low = low.clone().to(high.device).resample(high.sample_rate) | |
| low.audio_data = low.audio_data[..., :high.signal_length] | |
| low.audio_data = torch.nn.functional.pad( | |
| low.audio_data, (0, max(0, high.signal_length - low.signal_length)) | |
| ) | |
| return low.normalize(loud_db) + high | |
| def embed(signal: AudioSignal, embedder: torch.nn.Module): | |
| orig_ch, orig_sr = signal.num_channels, signal.sample_rate | |
| sig = signal.clone().resample(SAMPLE_RATE) | |
| if orig_ch > 1: | |
| b, c, n = sig.audio_data.shape | |
| sig.audio_data = sig.audio_data.reshape(b * c, 1, n) | |
| low, high, loud = split_bands(sig.clone(), AUDIOSEAL_SAMPLE_RATE) | |
| wm = embedder.get_watermark(low.audio_data, AUDIOSEAL_SAMPLE_RATE) | |
| low.audio_data = low.audio_data + wm | |
| merged = merge_bands(low, high, loud) | |
| if orig_ch > 1: | |
| b2, c2, n2 = merged.audio_data.shape | |
| merged.audio_data = merged.audio_data.reshape(-1, orig_ch * c2, n2) | |
| return merged.resample(orig_sr) | |
| def detect(signal: AudioSignal, detector: torch.nn.Module): | |
| sig = signal.clone().to_mono().resample(AUDIOSEAL_SAMPLE_RATE) | |
| result, _ = detector.forward(sig.audio_data, sample_rate=AUDIOSEAL_SAMPLE_RATE) | |
| return result[0, 1, :].detach().cpu().numpy() | |
| 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 | |
| def process_fn(inp_audio, option_text): | |
| # Checked before the GPU is reserved, so a request the model cannot take spends none of the | |
| # caller's ZeroGPU allowance | |
| if not inp_audio: | |
| raise gr.Error("Provide the audio to watermark, or to test for a watermark.") | |
| try: | |
| info = sf.info(inp_audio) | |
| 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.") | |
| output_audio, output_labels = watermark(inp_audio, option_text) | |
| if output_audio == inp_audio: | |
| return output_audio, output_labels | |
| # The format is matched outside the GPU call, which it does not need | |
| output_audio = match_input_format(output_audio, inp_audio) | |
| input_ext = Path(inp_audio).suffix.lower() | |
| if input_ext in LOSSLESS and input_ext != ".wav": | |
| data, rate = sf.read(output_audio, always_2d=True) | |
| output_audio = str(Path(output_audio).with_suffix(input_ext)) | |
| sf.write(output_audio, data, rate) | |
| return output_audio, output_labels | |
| def watermark(inp_audio, option_text): | |
| audio_np, sr = load_audio(inp_audio) | |
| print(f"sr: {sr}, audio shape: {audio_np.shape}") | |
| if audio_np.ndim == 1: | |
| audio_np = audio_np[None, None, :] | |
| else: | |
| audio_np = np.transpose(audio_np, (1, 0))[None, ...] | |
| print(f"formatted audio: {audio_np.shape}") | |
| ori_sig = AudioSignal(torch.from_numpy(audio_np).float(), sample_rate=sr) | |
| orig_loud = ori_sig.loudness() | |
| sig = ori_sig.to_mono().resample(SAMPLE_RATE).normalize(LOUDNESS_DB).ensure_max_of_audio() | |
| sig = sig.to(DEVICE) | |
| output_labels = LabelList() | |
| if option_text == "Generate Watermark": | |
| with torch.no_grad(): | |
| wm_sig = embed(sig.clone(), generator).normalize(orig_loud).ensure_max_of_audio() | |
| output_labels.labels.append( | |
| AudioLabel( | |
| t = 0, | |
| label = "watermark: 1.0", | |
| duration = wm_sig.duration, | |
| description = f"watermark confidence: 1.0, start: 0.0s, end: {wm_sig.duration:.2f}s", | |
| color = OutputLabel.rgb_color_to_int(255, 0, 0), | |
| amplitude = 1.0 | |
| ) | |
| ) | |
| return save_audio(wm_sig.cpu()), output_labels | |
| else: | |
| with torch.no_grad(): | |
| scores = detect(sig, detector) # AUDIOSEAL_SAMPLE_RATE | |
| N = len(scores) | |
| hop = int(0.01 * AUDIOSEAL_SAMPLE_RATE) | |
| avg_curve = [] | |
| for i in range(0, N, hop): | |
| seg = scores[i:i+hop] | |
| value = np.mean(seg) | |
| avg_curve.append(value) | |
| avg_curve = np.array(avg_curve) | |
| print(avg_curve.shape) | |
| min_size = max(2, int(0.25 * AUDIOSEAL_SAMPLE_RATE)) | |
| bkps = rpt.Pelt(model="l2", min_size=1).fit_predict(avg_curve, 1.0) | |
| t0 = 0 | |
| for t1 in bkps: | |
| print(t0, t1) | |
| seg = avg_curve[t0:t1] | |
| value = seg.mean() | |
| output_labels.labels.append( | |
| AudioLabel( | |
| t = (t0 / 100), | |
| label = f"watermark: {value:.2f}", | |
| duration = (t1 - t0) / 100, | |
| description = f"watermark confidence: {value:.2f}, start: {(t0 / 100):.2f}s, end: {(t1 / 100):.2f}s", | |
| color = OutputLabel.rgb_color_to_int(int(value * 255), int((1 - value) * 255), 0), | |
| amplitude = value * 2 - 1 | |
| ) | |
| ) | |
| t0 = t1 | |
| return inp_audio, output_labels | |
| if __name__ == "__main__": | |
| with gr.Blocks() as app: | |
| gr.Markdown("## Meta AudioSeal Watermarking") | |
| # Inputs | |
| input_audio = gr.Audio( | |
| label="Input Audio", | |
| type="filepath", | |
| sources=["upload", "microphone"] | |
| ).set_info("Speech to watermark, or an already-watermarked file to test.") | |
| option_dropdown = gr.Dropdown( | |
| ["Generate Watermark", "Detect Watermark"], | |
| value='Generate Watermark', | |
| label='Option', | |
| info='Model Options' | |
| ) | |
| # Outputs | |
| output_wav = gr.Audio( | |
| type="filepath", | |
| label="Watermarked Speech" | |
| ).set_info("The input with the watermark embedded, which should be inaudible.") | |
| output_label = gr.JSON(label="Watermark Confidence").set_info( | |
| "How sure the detector is that a watermark is present: one label spanning the " | |
| "clip after watermarking, or one per segment after detection." | |
| ) | |
| _ = build_endpoint( | |
| model_card=model_card, | |
| input_components=[ | |
| input_audio, | |
| option_dropdown | |
| ], | |
| output_components=[ | |
| output_wav, | |
| output_label | |
| ], | |
| process_fn=process_fn | |
| ) | |
| app.queue().launch(pwa=True, show_error=True) |