hear-embed is a CLI and Python library for embedding human health acoustics
using Google HeAR.
HeAR (Health Acoustic Representations) is a self-supervised encoder trained on human non-speech health sounds — coughing, breathing, throat-clearing, laughing, speaking (Baur et al. 2024). It turns a 2-second, 16 kHz, mono clip into a 512-dimensional embedding. This package wraps the PyTorch model with the pieces a real pipeline needs: loading arbitrary audio, resampling to the required format, windowing long recordings into 2-second clips, batching them through the model, and writing the vectors plus their time offsets to disk.
Scope note. HeAR was trained only on human health acoustics. It is not designed for general bioacoustics (animals, birds, nature). For those, look at Perch / BirdNET instead.
uv sync # create the env (core + torch + dev tools)
# copy .env.example to .env, add your HF_TOKEN, and accept the HeAR terms — see Install > Authenticate
uv run --env-file .env hear-embed cough.wav --out embeddings.parquetSee Install for gated-model auth and non-uv setups, and CLI for every flag.
This project uses uv. One command creates the
virtual environment and installs everything — the core deps plus the dev and
model dependency groups (both on by default) — and writes/checks uv.lock:
uv sync # then prefix commands with `uv run`The HeAR encoder's heavy deps (torch, transformers) live in a separate
model group so CI can skip them (uv sync --no-group model); uv sync
installs them by default for local use.
google/hear-pytorch is gated under the Health AI Developer Foundations terms:
- Accept the terms at https://huggingface.co/google/hear-pytorch.
- Authenticate with a Hugging Face token — either
uv run huggingface-cli login, or copy.env.exampleto.env, add yourHF_TOKEN, and load it per command withuv run --env-file .env …(orexport UV_ENV_FILE=.envfor the whole shell)..envis gitignored — never commit it.
Not on PyPI. Install from source. With uv, uv sync (above) is the happy
path; to add it to another project, uv pip install "git+https://github.com/darylalim/hear-embed" "torch>=2.1" "transformers==4.50.3".
Plain pip. pip does not install dependency groups, so add the model deps explicitly:
pip install -e . "torch>=2.1" "transformers==4.50.3"Python & libsndfile. Requires Python ≥ 3.10 (the repo pins 3.11 via
.python-version; uv will fetch it if missing). soundfile needs the system
libsndfile library (brew install libsndfile on macOS; usually preinstalled on
Linux).
torch wheels. With uv, torch resolution is platform-aware: macOS gets the
CPU/Metal wheels from PyPI, Linux gets CPU-only wheels from the PyTorch CPU index
(so CI avoids the multi-GB CUDA build); for CUDA on Linux, swap that index per
uv's PyTorch guide. Plain
pip ignores uv's index config and pulls PyPI's default torch (the CUDA build on
Linux) — add --index-url https://download.pytorch.org/whl/cpu for CPU-only.
# Embed every recording in a folder, 50% window overlap, to Parquet:
uv run hear-embed ./recordings --overlap 0.5 --out embeddings.parquet
# One averaged vector per file instead of one per window:
uv run hear-embed ./recordings --pool mean --out file_embeddings.parquet
# A single file, NumPy output (writes embeddings.npy + embeddings.csv):
uv run hear-embed cough.wav --format npz --out embeddingsKey flags: --overlap (window overlap in [0, 1), default 0), --pool (none
per-window / mean per-file), --batch-size (default 64), --device
(cuda/cpu), --format (parquet/npz), --model, --extensions. Run
uv run hear-embed --help for the full, colorized reference with defaults.
Per-file errors are logged to stderr and skipped, so one unreadable recording never aborts a batch. Exit codes:
| code | meaning |
|---|---|
0 |
all files embedded |
1 |
no audio files found under the input |
2 |
model load / gating failure (accept the terms + authenticate) |
3 |
finished, but one or more files were skipped |
The default Parquet output is streamed (one row group per file), so embedding a large corpus never holds all vectors in memory.
A run reports progress on stderr:
Found 3 file(s) to embed.
Wrote 128 embedding(s) from 3/3 file(s) to embeddings.parquet.
from hear_embed import HearEmbedder, embed_file, window_audio
embedder = HearEmbedder() # loads google/hear-pytorch onto GPU if available
# Full recording -> per-window embeddings + metadata:
vectors, metadata = embed_file("cough.wav", embedder, overlap=0.5)
# vectors: (n_windows, 512) float32
# metadata[i]: source_file, clip_index, start_sample, start_sec, end_sec
# Or drive the pieces yourself:
import numpy as np
clips, offsets = window_audio(np.zeros(48000, dtype=np.float32)) # (n, 32000)
vecs = embedder.embed_clips(clips) # (n, 512)| column | type | meaning |
|---|---|---|
source_file |
string | path of the source recording |
clip_index |
int32 | window index within that recording |
start_sample |
int64 | window start, in samples (16 kHz) |
start_sec / end_sec |
float64 | window start/end, in seconds |
embedding |
list[512] | the HeAR embedding |
The writers put vectors on disk; here's how to load them back into a matrix.
Only numpy + pyarrow are needed — both are core deps, no pandas required:
import numpy as np
import pyarrow.parquet as pq
t = pq.read_table("embeddings.parquet")
X = np.stack(t["embedding"].to_numpy(zero_copy_only=False)) # (n, 512) float32
meta = t.select(["source_file", "clip_index", "start_sec", "end_sec"]) # row-aligned with X
# npz output instead: X = np.load("embeddings.npy"); metadata is in <stem>.csv,
# whose leading `row` column is the index into X (X[row] <-> that CSV line).
# Downstream: cosine similarity / nearest-neighbour search over the matrix
Xn = X / np.linalg.norm(X, axis=1, keepdims=True)
sims = Xn @ Xn.T # (n, n) cosine similarities — fine for modest n; for a large corpus
# use batched similarity or an ANN index (faiss/hnswlib). Or feed X
# to a scikit-learn classifier head instead of the full matrix.iter_audio_files → load_and_resample → window_audio → HearEmbedder.embed_clips → writer.write
- Load + normalize (
audio.load_and_resample) — read viasoundfile(which scales integer PCM to float correctly), downmix to mono, resample to 16 kHz withscipy.signal.resample. - Window (
audio.window_audio) — slide 2-second windows with configurable overlap; zero-pad the final clip to exactly 32,000 samples. - Embed (
embedder.HearEmbedder) — apply HeAR's mel-PCEN preprocessing and run the PyTorch encoder, takingpooler_outputas the 512-dim vector.
The preprocessing in hear_embed/_vendor/audio_utils.py is vendored
unmodified from Google-Health/hear
(Apache-2.0) so it matches Google's reference exactly.
uv run pytest # torch-free tests + auto-skipped model smoke test
uv run pytest -m "not model" # everything except the heavy model smoke test
uv run pytest -m model # load the real model + one forward pass (see below)The bulk of the suite is torch-free — loading/resampling, windowing,
writers, pipeline pooling, and the CLI all run without the model or a GPU (a
fake embedder stands in), so they make up CI's -m "not model" job. That job
also runs the project-invariant checks: the .claude/hooks/ guards, the
license/packaging declarations, and the CI-workflow settings.
tests/test_model_smoke.py loads the real google/hear-pytorch and runs a
forward pass, so CI can catch model load / inference breakage. It skips
automatically unless torch + transformers are installed and you have
authenticated access to the gated repo — point CI at an HF_TOKEN to make it
run there.
uv run ruff check --fix # lint + autofix (incl. import sorting)
uv run ruff format # format
uv run ty check # type check (Astral's preview checker; version pinned via uv.lock)
uv run pre-commit install # run ruff + ty automatically on every commitCI runs three jobs: lint + tests torch-free, typecheck with the model group, and a build that installs the wheel without the model group to prove it installs and runs torch-free at the distribution level. See CLAUDE.md for the full CI/tooling rationale.
This repository is Apache-2.0 (see LICENSE); the built distribution
ships that text plus the vendored Google license
(hear_embed/_vendor/LICENSE.apache-2.0,
for the unmodified audio_utils.py). The HeAR model weights are governed
separately by the
Health AI Developer Foundations terms
— notably, clinical/diagnostic use requires appropriate regulatory authorization.