moshedd's picture
Add per-model collaborator credits + Acknowledgements section
f4bcac4 verified
Raw
History Blame Contribute Delete
11.6 kB
"""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)