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

backend → api: multipart WAV + X-API-Keyagent → api: multipart WAV + X-API-Keyweb → api: JSON series + X-API-Keyapi → models: audio bytesmodels → io: decode / filtermodels → gate: waveformmodels → feat: gated waveformmodels → stats: feature vs baselineapi → stats: /baseline /drift /trendVoxtar backend (Hono)Voxtar backend (Hono) (client) patient capturesVoxtar LiveKit agentVoxtar LiveKit agent (client) in-call vowel/coughVoxtar web trend routeVoxtar web trend route (client) respiratory trendFastAPI routerFastAPI router (service) 11 routes, X-API-KeyIndication pipelinesIndication pipelines (model) RLN, PD, cognitive, stroke, respAudioLoader +telephony bandpassAudioLoader + telephony bandpass (service) decode, 300-3400 HzQualityGatorQualityGator (service) duration, SNR, clippingFeature calculatorsFeature calculators (model) 9: jitter, shimmer, MPT, DDK...Baseline / drift /trendBaseline / drift / trend (model) trimmed mean, MAD, z, CDImultipart WAV + X-API…multipart WAV + X-API…JSON series + X-API-K…audio bytesdecode / filterwaveformgated waveformfeature vs baseline/baseline /drift /tre… backend → api: multipart WAV + X-API-Keyagent → api: multipart WAV + X-API-Keyweb → api: JSON series + X-API-Keyapi → models: audio bytesmodels → io: decode / filtermodels → gate: waveformmodels → feat: gated waveformmodels → stats: feature vs baselineapi → stats: /baseline /drift /trendVoxtar backend(Hono)Voxtar backend (Hono) (client) patient capturesVoxtar LiveKit agentVoxtar LiveKit agent (client) in-call vowel/coughVoxtar web trendrouteVoxtar web trend route (client) respiratory trendFastAPI routerFastAPI router (service) 11 routes, X-API-KeyIndication pipelinesIndication pipelines (model) RLN, PD, cognitive, stroke, respAudioLoader +telephony bandpassAudioLoader + telephony bandpass (service) decode, 300-3400 HzQualityGatorQualityGator (service) duration, SNR, clippingFeature calculatorsFeature calculators (model) 9: jitter, shimmer, MPT, DDK...Baseline / drift /trendBaseline / drift / trend (model) trimmed mean, MAD, z, CDImultipart WAV + X-API…multipart WAV + X-API…JSON series + X-API-K…audio bytesdecode / filterwaveformgated waveformfeature vs baseline/baseline /drift /tre…
clientservicemodelasync
  • [*]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

  1. 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:32 pyzheimer/quality/snr.py:10 pyzheimer/models/rln.py:51
  2. 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:29 pyzheimer/baseline/calculator.py:41
  3. 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:40 pyzheimer/models/rln.py:84
  4. 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:43 pyzheimer/drift/trends.py:119 pyzheimer/api/routes.py:238
  5. 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:11 pyzheimer/models/rln.py:47
  6. 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/'