Downloads · 30 days
0
TuyaNomo/nomo-pvad
nomo-pvad is a voice activity detection model from TuyaNomo. Use it for the voice activity detection task on the model card, and read the license before you ship it in a product. It is set up for pytorch. The card lists the license as apache-2.0.
Given a target speaker's enrollment audio and an incoming audio stream, nomo-pvad determines whether the target speaker is speaking, in real time, at 160 ms intervals. It supports applications that respond only to an…
Downloads · 30 days
0
Access
Public
Updated Sep 10, 2026
Repo size
36.9 MB
Likes
0
Public
Click a slice to open those files.
.pt110 MB · 99%
From the Hugging Face model README
Given a target speaker's enrollment audio and an incoming audio stream, nomo-pvad determines whether the target speaker is speaking, in real time, at 160 ms intervals. It supports applications that respond only to an enrolled speaker, including scenarios with multiple speakers and background noise. Inference operates on streaming audio chunks and supports on-device deployment.
1.1 (tag release-1.1).📄 Technical report (task definition, module architecture and configuration, training settings, and measured latency and accuracy): https://tuya.com/model/doc/pvad
🎧 Online demo: https://tuya.com/model/demo/pvad
⚠️ The demo includes substantial additional engineering beyond the open-source baseline in this repository (model only, no pre/post-processing). Those additions are not included in this release.
# After cloning the repo, install dependencies:
pip install -r requirements.txt
The nomo-pvad weights (approximately 35 MB) are bundled at
weights/nomo_pvad.pt, so no separate download is required for these weights. The original ERes2NetV2 speaker model is downloaded automatically on first run if it is not already available locally (see Architecture).
The repository includes enrollment and test audio for a quick check. The first portion of the test clip contains the target speaker and should be classified as target speech; the later portion contains a non-target speaker and should be classified as non-target speech:
python infer.py --enroll examples/enroll.wav --test examples/test.wav --verbose
Illustrative output (excerpt; p is the target-speaker probability, followed by a timeline with 160 ms resolution):
▶ streaming detection (one frame per 160 ms) …
0.16s p=0.92 █ target speaker
0.32s p=0.90 █ target speaker
...
2.72s p=0.11 · non-target / silence
2.88s p=0.08 · non-target / silence
0s 2s 4s
████████████████··········
█ target speaking · non-target / silence
Result: ✅ target speaker detected (≈ 2.4 s of target speech, 55%)
Or use your own audio:
python infer.py --enroll enroll.wav --test test.wav
# per-chunk log: --verbose ; GPU: --device cuda:0 ; threshold: --threshold 0.7
process_chunk returns the target-speaker probability P(target) ∈ [0, 1]. The decision threshold defines the operating point selected for deployment and should be adjusted to the application:
| Scenario | Suggested threshold | Trade-off |
|---|---|---|
| Speaker-based gating / false-acceptance prevention (high cost of false acceptance) | 0.7–0.85 | Lower false acceptance rate, slightly higher miss rate (the target speaker may occasionally need to repeat themselves); default 0.7 |
| Balanced | 0.6 | Balance between recall and false acceptance rate |
| Multi-speaker turn-taking / high recall | 0.5–0.6 | Prioritize recall while tolerating more false acceptances |
A higher threshold makes acceptance more conservative. Streaming deployments typically also use a state machine with hysteresis, requiring N consecutive frames before changing the detection state, to reduce rapid state changes.
from nomo_pvad import NomoPVAD, NomoPVADSession
m = NomoPVAD("weights/nomo_pvad.pt", device="cpu") # ERes2NetV2 auto-downloads on first run
sess = NomoPVADSession(m)
sess.set_enrollment(enroll_pcm_f32_16k) # enroll the target speaker
for chunk_f32 in stream_160ms_chunks: # 160 ms each (2560 samples @ 16k)
p = sess.process_chunk(chunk_f32) # target-speaker probability [0, 1]
infer.py Command-line inference example
nomo_pvad.py Streaming inference wrapper (enrollment → per-chunk probability)
core/ Model architecture (model + eres2netv2_backbone + film + frame_adapter)
weights/ Bundled nomo_pvad.pt weights; the original ERes2NetV2 model is downloaded on first run
examples/ Sample audio (enroll.wav + test.wav)
enroll (≥10s) ─► ERes2NetV2 full model (with pooling, frozen) ─► d-vector 192-d (L2-normalized)
│ one-time; runtime only needs this vector
▼
stream ─► Fbank ─► ERes2NetV2 trunk ─► FiLM ─► proj ─► speech encoder ─► det decoder ─► chunk head ─► P(target)
16kHz 80-d pooling removed·8× ds γ·(x+β) 1024→256 causal ×2 causal ×1 every 2 frames every 160ms
| Module | Configuration | Parameters |
|---|---|---|
| Acoustic features | 80-dimensional filterbank (Fbank) features, 25 ms window / 10 ms hop, dither=0, mean subtraction over the time dimension | — |
| Speaker backbone | ERes2NetV2 (baseWidth=26, scale=2, expansion=2), final pooling removed; 8× temporal downsampling → 12.5 Hz (80 ms/frame), mean pooling over the frequency dimension, 1024-dimensional output | 13.93M |
| FiLM modulation | 192 → LayerNorm→Linear(512)→SiLU→Dropout(0.1)→Linear(2048), split into (γ̃, β), γ=1+γ̃, output γ·(x+β); final layer initialized to zero → identity mapping at the start of training | 1.15M |
| Feature projection | LayerNorm(1024) + Linear(1024→256) | 0.26M |
| Speech encoder | CausalConv1d + GLU residual blocks ×2, d_model=256, kernel=7 | 1.84M |
| Detection decoder | Same residual block ×1 | 0.92M |
| Chunk classification head | Mean pooling over every 2 backbone frames (2 × 80 ms) to produce 1 chunk → LayerNorm + Linear(256) + SiLU + Dropout + Linear(2), softmax yields the target-class probability | 0.07M |
| Total | 18.16M, FP16 weights: 36.6 MB |
The bundled
nomo_pvad.ptcontains the fine-tuned backbone. The full original ERes2NetV2 (with pooling) extracts the enrollment embedding. If enrollment happens in the cloud or during provisioning, pass the precomputed 192-dimensional vector tosess.set_enrollment_emb(...). The current Python wrapper also loads the original ERes2NetV2 checkpoint during initialization, even with a precomputed embedding. For offline use, cache that model in advance or pass its local directory viaeres2netv2_dir(CLI:--eres2netv2_dir); otherwise, it is downloaded automatically on first run.
Key design choices:
LICENSE).iic/speech_eres2netv2_sv_zh-cn_16k-common), declared Apache-2.0 upstream.
weights/nomo_pvad.pt contains weights fine-tuned from that backbone (final pooling removed, all parameters fine-tuned for this task) — a derivative work, redistributed under Apache-2.0 with attribution here.