Spaces:
Running
Running
| """Deep Voice — multi-model bioacoustic call detection Space.""" | |
| from __future__ import annotations | |
| import tempfile | |
| import traceback | |
| from pathlib import Path | |
| import gradio as gr | |
| import pandas as pd | |
| from helpers import ( | |
| download_checkpoint, | |
| estimate_minutes, | |
| merge_ravens, | |
| run_inference, | |
| validate_uploads, | |
| zip_files, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Model registry: human label -> per-model settings. | |
| # `ckpt` is the path inside the deepvoice1/bioacoustic-checkpoints HF repo. | |
| # `min_sr` is the minimum input sample rate the model needs (== the model's | |
| # internal SR after resampling — uploads below this are rejected because the | |
| # missing high-freq content can't be reconstructed). | |
| # `notes` is the short blurb shown to the user beneath the model dropdown. | |
| # --------------------------------------------------------------------------- | |
| MODELS: dict[str, dict] = { | |
| # `coef` is the fraction-of-real-time we expect on HF free CPU. Calibrated | |
| # against observed runs (e.g. echo: 50 s for 5 min audio = 0.17). The cheap | |
| # downsampling models are much faster than the 96 kHz PCEN ones. | |
| "Arctic cod fish (Boreogadus saida)": dict( | |
| ckpt="xo9c3x6c/best.pth", min_sr=2000, threshold=0.5, coef=0.01, | |
| notes="1 s window · classes: Noise / Call · needs ≥ 2 kHz input.", | |
| credit="Trained in collaboration with Shaye Ogurek, University of Victoria.", | |
| ), | |
| "Greater Caribbean manatee (Trichechus manatus)": dict( | |
| ckpt="o5ot9qky/best.pth", min_sr=96000, threshold=0.5, coef=0.10, | |
| notes="0.2 s window · classes: Noise / Call · needs ≥ 96 kHz input.", | |
| credit="Trained in collaboration with Eric A. Ramos (Mote Marine Laboratory, *in memoriam*) and Beth Brady (Save the Manatee Club).", | |
| ), | |
| "Burrunan dolphin — barks (low-freq calls)": dict( | |
| ckpt="4af2w6lt/best.pth", min_sr=10000, threshold=0.5, coef=0.02, | |
| notes="3 s window · classes: bg / barks · needs ≥ 10 kHz input.", | |
| ), | |
| "Burrunan dolphin — echo (echolocation)": dict( | |
| ckpt="bki984uw/best.pth", min_sr=96000, threshold=0.9, coef=0.17, | |
| notes="3 s window · classes: bg / echo · needs ≥ 96 kHz input · threshold 0.9 recommended.", | |
| ), | |
| "Burrunan dolphin — buzz (rapid clicks)": dict( | |
| ckpt="ccgojzau/best.pth", min_sr=96000, threshold=0.8, coef=0.20, | |
| notes="0.5 s window · classes: bg / buzz · needs ≥ 96 kHz input · threshold 0.8 recommended.", | |
| ), | |
| "Burrunan dolphin — whistle (tonal signals)": dict( | |
| ckpt="g8gtuypk/best.pth", min_sr=96000, threshold=0.5, coef=0.18, | |
| notes="1 s window · classes: bg / whistle · needs ≥ 96 kHz input.", | |
| ), | |
| "Killer whale / orca (Orcinus orca) — 5-class": dict( | |
| ckpt="v5q3lg3h/best.pth", min_sr=24000, threshold=0.5, coef=0.05, | |
| notes="1.5 s window · classes: Upsweeps / Downsweeps / Tones / Squeaks / Clicks · needs ≥ 24 kHz input.", | |
| credit="Trained in collaboration with Fannie W. Shabangu, University of Pretoria.", | |
| ), | |
| "Humpback whale (Megaptera novaeangliae) — Mozambique / C1 group": dict( | |
| ckpt="2dobs988/best.pth", min_sr=16000, threshold=0.5, coef=0.06, | |
| notes="1 s window · classes: Noise / Call · needs ≥ 16 kHz input · trained on the C1 breeding subpopulation of humpback whales recorded off Mozambique.", | |
| ), | |
| } | |
| # If the estimated runtime exceeds this, we reject before starting (protects | |
| # the HF free-tier worker from timing out mid-run). | |
| MAX_RUNTIME_MIN = 15 | |
| INTRO_MD = """ | |
| # 🐳 Deep Voice — Bioacoustic Call Detection | |
| Run open-source detectors trained by **[Deep Voice](https://huggingface.co/deepvoice1)** on your own | |
| underwater recordings. Pick a species/call type, upload one or more `.wav` files, and download | |
| per-window probability scores (CSV) plus a [Raven](https://www.ravensoundsoftware.com/) selection | |
| table for further analysis. | |
| **How to use** | |
| 1. Choose a model from the dropdown — the description updates with the window size and recommended detection threshold. | |
| 2. Upload one or more `.wav` files. Total upload size capped at **500 MB**; runs estimated to exceed **15 min** of compute are rejected. | |
| 3. Click **Run inference**. Downloads appear once processing finishes. | |
| Running on free HuggingFace CPU hardware. Throughput varies by model — the | |
| pre-flight line below shows a per-model estimate once you upload. | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # Event handlers | |
| # --------------------------------------------------------------------------- | |
| def _model_notes_md(model_label: str) -> str: | |
| m = MODELS[model_label] | |
| md = f"**Model notes**: {m['notes']}" | |
| if m.get("credit"): | |
| # Underscore italics outside so the credit can use *...* inside (e.g. *in memoriam*). | |
| md += f" \n_{m['credit']}_" | |
| return md | |
| def update_model_notes(model_label: str) -> tuple[str, float]: | |
| m = MODELS[model_label] | |
| return _model_notes_md(model_label), m["threshold"] | |
| def preflight(files, model_label): | |
| if not files: | |
| return "_Upload at least one `.wav` file._" | |
| paths = [f.name if hasattr(f, "name") else f for f in files] | |
| try: | |
| info = validate_uploads(paths) | |
| except gr.Error as e: | |
| return f"⚠️ {e.args[0] if e.args else 'Validation error.'}" | |
| coef = MODELS[model_label]["coef"] | |
| eta = estimate_minutes(info["total_min"] * 60, coef=coef) | |
| warn = "" | |
| if eta > MAX_RUNTIME_MIN: | |
| warn = ( | |
| f" \n⚠️ Estimated runtime exceeds the {MAX_RUNTIME_MIN}-min cap " | |
| f"for this Space — please reduce the input or pick a faster model." | |
| ) | |
| return ( | |
| f"**{info['n_files']} file(s)** · {info['total_mb']:.1f} MB · " | |
| f"{info['total_min']:.2f} min total audio. \n" | |
| f"Estimated runtime for **{model_label.split(' — ')[0]}**: " | |
| f"**~{eta:.1f} min** on CPU (rough estimate, actual time may vary)." + warn | |
| ) | |
| def predict(model_label: str, threshold: float, files, progress=gr.Progress()): | |
| if not files: | |
| raise gr.Error("Please upload at least one .wav file.") | |
| paths = [f.name if hasattr(f, "name") else f for f in files] | |
| info = validate_uploads(paths) # raises gr.Error on cap violation | |
| model = MODELS[model_label] | |
| eta = estimate_minutes(info["total_min"] * 60, coef=model["coef"]) | |
| if eta > MAX_RUNTIME_MIN: | |
| raise gr.Error( | |
| f"Estimated runtime ~{eta:.1f} min exceeds this Space's " | |
| f"{MAX_RUNTIME_MIN}-min cap for {model_label}. Reduce the input or pick a faster model." | |
| ) | |
| progress(0.05, desc="Downloading model checkpoint…") | |
| ckpt_path = download_checkpoint(model["ckpt"]) | |
| work_dir = Path(tempfile.mkdtemp(prefix="dv_run_")) | |
| csv_paths: list[Path] = [] | |
| raven_paths: list[Path | None] = [] | |
| wav_paths = [Path(p) for p in paths] | |
| n = len(wav_paths) | |
| for i, wav in enumerate(wav_paths): | |
| progress((i + 0.1) / (n + 1), desc=f"[{i+1}/{n}] {wav.name}") | |
| # Each file gets its own subdir so soundbay's timestamped output filenames | |
| # never collide when two inferences land in the same wall-clock second. | |
| per_file_dir = work_dir / f"f{i:03d}" | |
| try: | |
| csv_p, raven_p = run_inference( | |
| wav_path=str(wav), | |
| ckpt_path=ckpt_path, | |
| threshold=threshold, | |
| output_dir=per_file_dir, | |
| ) | |
| except ValueError as e: | |
| # Friendly errors (e.g. sample-rate too low) bubble up cleanly. | |
| raise gr.Error(str(e)) | |
| except Exception: | |
| tb = traceback.format_exc() | |
| raise gr.Error(f"Inference failed on {wav.name}:\n{tb[-500:]}") | |
| csv_paths.append(csv_p) | |
| raven_paths.append(raven_p) | |
| progress(0.95, desc="Packaging outputs…") | |
| model_id = Path(ckpt_path).parent.stem | |
| if n == 1: | |
| csv_out = csv_paths[0] | |
| raven_out = raven_paths[0] | |
| else: | |
| csv_zip = work_dir / f"results_{model_id}_csvs.zip" | |
| zip_files(csv_paths, csv_zip) | |
| csv_out = csv_zip | |
| merged = work_dir / f"results_{model_id}_merged-Raven.txt" | |
| merge_ravens(raven_paths, wav_paths, merged) | |
| raven_out = merged | |
| # Preview: head of the first CSV | |
| preview_df = pd.read_csv(csv_paths[0]).head(20) | |
| return str(csv_out), (str(raven_out) if raven_out else None), preview_df | |
| # --------------------------------------------------------------------------- | |
| # UI | |
| # --------------------------------------------------------------------------- | |
| with gr.Blocks(title="Deep Voice — Bioacoustic Detection", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown(INTRO_MD) | |
| default_model = next(iter(MODELS.keys())) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| model_dd = gr.Dropdown( | |
| choices=list(MODELS.keys()), | |
| value=default_model, | |
| label="Model / species / call type", | |
| interactive=True, | |
| ) | |
| model_notes = gr.Markdown(_model_notes_md(default_model)) | |
| threshold_sl = gr.Slider( | |
| minimum=0.0, maximum=1.0, value=MODELS[default_model]["threshold"], | |
| step=0.05, label="Detection threshold (used for the Raven selection table)", | |
| ) | |
| with gr.Column(scale=3): | |
| files_in = gr.File( | |
| file_count="multiple", | |
| file_types=[".wav"], | |
| label="Upload .wav files (≤ 500 MB total; runtime ≤ 15 min)", | |
| ) | |
| preflight_md = gr.Markdown("_Upload at least one `.wav` file._") | |
| run_btn = gr.Button("🔍 Run inference", variant="primary") | |
| with gr.Row(): | |
| csv_out = gr.File(label="CSV scores (per-window probabilities)") | |
| raven_out = gr.File(label="Raven selection table (.txt)") | |
| preview = gr.Dataframe(label="Preview — first 20 rows of first CSV", interactive=False) | |
| gr.Markdown( | |
| "---\n" | |
| "### Acknowledgements\n" | |
| "Models trained in collaboration with the following researchers and organisations:\n" | |
| "- **Arctic cod** — Shaye Ogurek (University of Victoria)\n" | |
| "- **Greater Caribbean manatee** — Eric A. Ramos (Mote Marine Laboratory, *in memoriam*) and Beth Brady (Save the Manatee Club)\n" | |
| "- **Killer whale** — Fannie W. Shabangu (University of Pretoria)\n" | |
| "\n" | |
| "**Feedback & bug reports** — open a thread in the Space's " | |
| "[Community tab](https://huggingface.co/spaces/deepvoice1/deepvoice_detection/discussions) " | |
| "for anything public; for private inquiries, write to **info@deepvoicefoundation.com**. \n" | |
| "**Models & code** are open-source at " | |
| "[github.com/deep-voice/soundbay](https://github.com/deep-voice/soundbay). " | |
| "More about us at [deepvoicefoundation.com](https://www.deepvoicefoundation.com/). \n" | |
| "Built with support from the **WILDLABS Awards 2025**, funded by **Arm**." | |
| ) | |
| # Wiring | |
| model_dd.change(fn=update_model_notes, inputs=model_dd, outputs=[model_notes, threshold_sl]) | |
| model_dd.change(fn=preflight, inputs=[files_in, model_dd], outputs=preflight_md) | |
| files_in.change(fn=preflight, inputs=[files_in, model_dd], outputs=preflight_md) | |
| run_btn.click( | |
| fn=predict, | |
| inputs=[model_dd, threshold_sl, files_in], | |
| outputs=[csv_out, raven_out, preview], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(inbrowser=True) | |