Inspiration
For many customers across Latin America, a bank's phone line isn't a convenience — it's the only channel they have. Yet there's no reliable way to know if the voice on the other end is real or a synthetic clone. Cloning tools can fake a voice; they can't as easily fake how a live human mind behaves under pressure — response latency, messy recovery from interruptions, re-entry after silence. That gap is what we built on.
What we built
GurtsCadence is a CPU-only HTTP service that takes a recorded stereo call (caller + AI agent channels) and returns a calibrated human/synthetic verdict — based on turn-timing behavior and voice-quality artifacts, not timbre alone.
How we built it
The team was mixed macOS/Windows, some machines without Homebrew or on ARM, which ruled out librosa and lightgbm/xgboost before we wrote any model code. We built the whole pipeline on numpy/scipy.signal and scikit-learn instead.
- Turn-timing features. Barge-in overlap depth and scale-free (CV-based) latency/duration features took speaker-disjoint val AUC from 0.9817 → 0.9873 and EER from 0.0847 → 0.0417.
- A second, independent signal: vocoder-artifact liveness. Timing is our primary bet, but a well-tuned agent could eventually mimic human timing well enough to blend in. We added a ch0-only module — pitch jitter, amplitude shimmer, a harmonic-to-noise proxy, voiced ratio, noise-floor drift — that looks at how the voice is made, not how the conversation flows. Liveness-only and turns-only each independently hit val AUC 0.987; combined, val AUC reached 1.000, with permutation importance confirming both signals carry real weight.
- VAD from scratch. /detect never gets ground-truth turn boundaries at inference, so our VAD is the production feature pipeline — an adaptive scipy.signal version, A/B validated against the dataset's real annotated turns.
- Honest calibration. With only ~280 training calls, we fit both isotonic and sigmoid calibration and kept whichever won on real val Brier score.
- Hardening. /detect never raises — malformed audio falls back to a safe verdict instead of a 500, since a wrong answer scores and a crash doesn't.
Challenges we ran into
Two caught us off guard. When the hackathon released their own test client mid-event, running it against our model exposed a contract bug: our confidence field sent raw P(synthetic) instead of confidence in the predicted label, silently collapsing our AUC tie-break score to near-random even though balanced accuracy was fine.
Then a teammate testing from their own laptop got a silent timeout — venue wifi enforces client isolation, so devices can't reach each other directly. We built a Cloudflare Tunnel fallback in response, and upgraded it from an anonymous quick tunnel to a named tunnel on our own domain once we saw the free one occasionally drop requests.
What we learned
Test against the real caller, not your assumptions about the contract — both bugs above were invisible until we ran the actual client an outside party would use. And don't bet everything on one signal type: pairing timing behavior with an independent voice-quality signal gave us a model that stays useful even if one axis gets beaten.
Built With
- cloudflare
- fastapi
- numpy
- pandas
- python
- ruff
- scikit-learn
- scipy
- soundfile
- uvicorn
Log in or sign up for Devpost to join the conversation.