backend / pyzheimer
pyzheimer
A FastAPI service that checks a voice recording's quality, extracts acoustic biomarkers (jitter, shimmer, phonation time, DDK rate, cough spectrum), and scores drift against a patient's robust baseline.
2026 · paused · private repository
- [*]LanguagesPython
- [*]FrameworksFastAPI · Pydantic v2 · librosa · SciPy · NumPy · uvicorn
- [*]InfraDocker (python:3.12-slim + uv) · AWS EC2 (Voxtar compose stack)
pyzheimer is the analysis side of Voxtar: about 2,200 lines of Python (numpy, scipy, librosa) behind 11 FastAPI routes. Audio comes in as multipart uploads, passes a quality gate, and goes through one of 5 rule-based indication pipelines (RLN injury, Parkinson's, cognitive, stroke/dysarthria, respiratory) built on 9 feature calculators. Each pipeline returns features, per-feature quality tags and a flagged_for_diagnosis boolean.
The quality gate rejects audio shorter than 2 s, with an SNR below 15 dB (frame-energy estimate) or with more than 1% clipping. A rejection comes back as HTTP 422 with the reason, which Voxtar shows to the patient as a re-record prompt. Telephony audio, and Voxtar's own vowel uploads, go through a 300–3400 Hz 6th-order bandpass first so web and phone recordings are measured on the same band.
Baselines use a 10% trimmed mean and a MAD-scaled standard deviation (×1.4826) with a sigma floor. Drift is a direction-aware z-score (adverse_high, adverse_low or two_sided) mapped to normal, mild, moderate or severe at 1.5, 2 and 3. A respiratory trend endpoint combines least-squares slopes over the last 5 sessions with a 4-session cumulative deviation index into a risk level.
Voxtar calls it over HTTP with an X-API-Key: the backend routes cough to /process/respiratory, vowel to /process and ddk to /process/parkinsons/ddk; the LiveKit agent posts in-call recordings; and the web app calls /respiratory/trend. In production it runs as a uv-built python:3.12-slim container on the Voxtar EC2 box, reachable only on localhost:8001.
How it holds up
- Auth
- Every analysis route depends on verify_api_key, which reads the X-API-Key header and compares it to PYZHEIMER_API_KEY with secrets.compare_digest, failing closed on a missing or empty key (monorepo copy). /health is open. CORS allows all origins.
- Data
- Stateless: no database. Callers pass baseline histories, baseline mean/std and session series in the request body, and Voxtar stores the outputs in its own capture_recording, vital_observation and *_baseline tables.
- Consistency
- Every request is a pure function of its inputs, with model objects built once at import and no shared mutable state, so retries are safe. Request handlers are async but run CPU-bound DSP inline on the event loop, so one slow upload blocks other requests on that worker.
- Failure
- Undecodable audio or a failed read returns 400 or 422 with the reason. A quality-gate rejection returns 422 with the SNR, clipping and duration figures. Trend analysis with fewer than 3 sessions returns 422. A zero-variance baseline gives a z-score of 0 instead of dividing by zero, and short histories fall back to the sample std or the sigma floor. There are no request size limits or timeouts.
- Testing
- No test files in either copy: git ls-files shows no test_*.py or tests/ directory, and pyproject.toml has no test dependencies.
Techniques
- Quality gate before feature extractionRecordings shorter than 2 s, with a frame-energy SNR below 15 dB, or with more than 1% clipped samples are rejected with the measured figures before any biomarker is computed, so bad recordings never become clinical numbers and the patient is told why.
pyzheimer/quality/gator.py:32pyzheimer/quality/snr.py:10pyzheimer/models/rln.py:51 - Robust per-patient baselineA 10% trimmed mean and a MAD-based sigma (×1.4826 to put it on a normal scale) with a configurable floor resist single outlier sessions and avoid zero-variance z-scores. Short histories fall back to the sample std.
pyzheimer/baseline/calculator.py:29pyzheimer/baseline/calculator.py:41 - Direction-aware drift severityEach feature declares whether high values (jitter, shimmer) or low values (MPT) are adverse. The signed z-score becomes an adverse score with fixed 1.5/2/3 severity bands, and the RLN pipeline flags the session if any tracked feature is anomalous.
pyzheimer/drift/detector.py:40pyzheimer/models/rln.py:84 - Anticipatory respiratory trendCombines least-squares slopes over the last 5 sessions of phonation time and cough spectral centroid with a 4-session cumulative z-score sum and a latest-session severity check, giving a normal/low/moderate/high/critical risk level intended to flag decline before symptoms appear.
pyzheimer/drift/trends.py:43pyzheimer/drift/trends.py:119pyzheimer/api/routes.py:238 - Channel normalisationAudio marked as telephony passes a 6th-order 300–3400 Hz bandpass before gating, so phone and web recordings are measured on the same band. Voxtar sends every vowel upload as telephony.
pyzheimer/io/filters.py:11pyzheimer/models/rln.py:47 - Fail-closed constant-time API keyThe monorepo copy replaced a check that accepted any key while the server key was the default with secrets.compare_digest and an explicit missing-key rejection (commit 16ad875, 2026-07-10).
pyzheimer/api/routes.py:38
Counted
- 3commits (monorepo services/pyzheimer)
git log --oneline -- services/pyzheimer (2026-07-09 to 2026-07-10) - 1commits (standalone hallelx2/pyzheimer)
git rev-list --count HEAD in ~/dev/pyzheimer (single commit 2026-08-25) - 11HTTP routes
grep -cE '@router\.(get|post)' pyzheimer/api/routes.py - 5indication pipelines
ls pyzheimer/models (rln, parkinsons, cognitive, stroke, respiratory) - 9feature calculators
ls pyzheimer/features (cough, ddk, formants, jitter, mpt, pitch, shimmer, syllables, sz_ratio) - 2191Python LOC (monorepo copy)
git ls-files '*.py' | xargs wc -l - 2186Python LOC (standalone)
git ls-files '*.py' | xargs wc -l in ~/dev/pyzheimer - 12reference docs (model cards, research notes)
ls docs/reference - 0test files
git ls-files | grep -E 'test_.*\.py|tests/'