Durable reference for humans and agents maintaining voice-detect.cpp.
This project follows the Linux kernel project's guidelines for AI coding assistants (the same policy LocalAI uses). Key rules for commits:
- No
Signed-off-byfrom AI. Only a human submitter may sign off on the Developer Certificate of Origin. - No
Co-Authored-By: <AI>trailers. The human contributor owns the change. - Use an
Assisted-by:trailer to attribute AI involvement. Format:Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2](e.g.Assisted-by: Claude:claude-opus-4-8 [Claude Code]). - The human submitter is responsible for reviewing, testing, and understanding every line of generated code.
voice-detect.cpp is a C++17/ggml inference engine for speaker recognition (voice biometrics). A Python converter turns a speaker-embedding checkpoint (SpeechBrain ECAPA-TDNN, or an ONNX WeSpeaker / 3D-Speaker encoder) into a metadata-driven GGUF; the C++ model loader + encoder run the same computation natively with no Python dependency at inference time.
Pipeline: decode/resample audio -> 16 kHz mono -> 80-dim Kaldi-compatible FBank
features -> speaker encoder -> L2-normalized embedding. Plus verify (cosine
distance vs threshold) and analyze (age/gender/emotion, phased last).
The public surface ships as a flat C-API (include/voicedetect_capi.h +
libvoicedetect.so) suitable for dlopen/FFI, and is the native replacement for
LocalAI's Python speaker-recognition backend.
These mirror parakeet.cpp's measured wins; keep them when implementing graphs:
- Keep the persistent
ggml_gallocrinsrc/backend.cpp. Reusing one allocator across the many small per-utterance graphs (no per-call alloc/free) is the core throughput lever. The scheduler is used ONLY as a per-graph fallback when the active GPU backend lacks a kernel for some op. - Zero-copy weights.
clone_weightreturns loader tensors directly so the same device buffer is reused every call; do not copy weights per call.
include/ public C/C++ headers
voicedetect.h , version + tiny C++ namespace vd layer
voicedetect_capi.h , flat C-API for FFI / dlopen
src/ libvoicedetect implementation
voicedetect.cpp , version + thin vd::embed/verify wrappers
voicedetect_capi.cpp , flat C-API implementation
model.hpp/cpp , load-once vd::Model orchestration (stub)
model_loader.hpp/cpp , GGUF -> VoiceDetectConfig + name->tensor
backend.hpp/cpp , persistent ggml_backend_t + gallocr
fbank.hpp/cpp , Kaldi-compatible FBank front end (stub)
audio_io.hpp/cpp , dr_wav load + linear resample to 16k
common.hpp/cpp , logging helpers
examples/cli/ voicedetect-cli binary (info, embed, verify, analyze, bench)
scripts/ Python tooling (reference-side only)
convert_voicedetect_to_gguf.py , checkpoint -> GGUF (--dtype f32|f16|q8_0)
gen_baseline.py , reference intermediates -> baseline.gguf
apply_ggml_patches.sh , optional in-tree ggml patches
requirements.txt , speechbrain/torch/torchaudio/onnxruntime/gguf
tests/ ctest targets
parity.hpp , header-only golden compare + cosine
test_smoke.cpp , version + ABI (model-independent)
test_fbank.cpp , FBank golden parity (RC-77 skip)
fixtures/ , small clips (not committed; see README)
python/check_convert.py , converter round-trip
docs/
conversion.md , GGUF schema reference
parity.md , coverage matrix + parity gates (cosine>=0.9999, max|d|<=1e-3, identical verdict)
quantization.md , quantization allowlist + policy
third_party/ vendored deps: ggml/ (submodule), dr_wav.h (single header)
models/ output dir for converted GGUFs (gitignored; MANIFEST.md tracked)
.github/workflows/ ci.yml, docker.yml, release.yml
cmake -B build -DVOICEDETECT_BUILD_TESTS=ON -DGGML_NATIVE=ON && cmake --build build -j
Use -DGGML_NATIVE=OFF when building for CI or portable binaries.
# Model-independent (run anywhere)
ctest --test-dir build --output-on-failure -LE model # test_smoke
# Model/baseline-dependent (need venv + reference baseline; SKIP/77 without)
export VOICEDETECT_TEST_GGUF=/tmp/model.gguf
export VOICEDETECT_TEST_BASELINE=/tmp/baseline.gguf
export VOICEDETECT_TEST_AUDIO=tests/fixtures/clip.wav
ctest --test-dir build --output-on-failure
include/voicedetect_capi.h defines the flat C-API. Build libvoicedetect.so
with -DVOICEDETECT_SHARED=ON. Verify exports with
nm -D build-shared/libvoicedetect.so | grep voicedetect_capi.
The LocalAI backend lives in the LocalAI repo and dlopens libvoicedetect.so.
These are the frozen symbols LocalAI depends on - do not remove or change any
signature without a coordinated bump on the LocalAI side AND on
voicedetect_capi_abi_version:
voicedetect_capi_abi_version
voicedetect_capi_load
voicedetect_capi_free
voicedetect_capi_last_error
voicedetect_capi_free_string
voicedetect_capi_free_vec
voicedetect_capi_embed_path # WAV path -> L2-normalized embedding (out_vec,out_dim)
voicedetect_capi_embed_pcm # raw f32 PCM (+ sample_rate) -> embedding
voicedetect_capi_verify_paths # two clips -> cosine distance + verdict vs threshold
voicedetect_capi_analyze_path_json # WAV path -> {age, gender, emotion} JSON
voicedetect_capi_abi_version() returns an integer (currently 1) that
LocalAI checks for compatibility. Bump it on any breaking change to the
signatures or semantics of the frozen symbols above (the changelog block lives at
the top of include/voicedetect_capi.h AND src/voicedetect_capi.cpp - keep
them in sync). Additive changes (brand-new functions that do not alter
existing ones) are fine without a bump. Never let a C++ exception cross the C
boundary: every entry point catches and routes to the context's last-error
buffer.
See docs/conversion.md. Quick summary:
general.architecture = "voicedetect"- All metadata keys use the
voicedetect.*prefix. - Tensor names are verbatim source
state_dict/ ONNX initializer names, no remapping. The C++ loader mapsname -> ggml_tensor*by exact string. Never remap tensor names at conversion time.
Tracked at third_party/ggml. No local patches today; the
scripts/apply_ggml_patches.sh hook only runs if third_party/ggml-patches/
exists. To bump: update the submodule SHA, rebuild, run the tests, fix any API
breakage in src/model_loader.cpp / src/backend.cpp.
src/fbank.cpp-FBank::computeis a documented stub; implement the Kaldi-compatible front end (parity vstorchaudio.compliance.kaldi).src/model.cpp-embed_16kthrows "not implemented"; build the encoder graphs (ECAPA first), pooling, embedding projection + L2-normalize.analyzeheads - phased last;voicedetect.analyze.presentgates them.scripts/convert_voicedetect_to_gguf.py-load_state_dictraises NotImplementedError; wire the SpeechBrain + ONNX loader branches.scripts/gen_baseline.py- reference forward + tensor capture skeleton.tests/fixtures/- add real clips (with provenance) once the engine runs.