ProjectVoice Infrastructure · 2026
Telephony Voice Simulator
A local-first test platform for voice agents, AMD, IVRs, PBX routing, live carrier behavior, recordings, and automated grading.
- Python
- Next.js
- LiveKit
- Twilio
- SQLite
- MySQL
- Docker
Voice Infrastructure · Project
Telephony Voice Simulator is where I test a voice agent against the phone network instead of against a chat transcript. It covers answering machine detection, inbound IVRs, PBX routing, queues, screeners, human pickup, recordings and grading. It is the merged successor to two earlier simulators of mine, one for AMD and one for telephony, which duplicated storage, provider and console layers.
Voice systems fail on timing and audio, not on wording. Unusual voicemail greetings, early beeps, silence, DTMF gates, carrier errors, queue overflow and slow transfers do not show up in a text-level test. A live carrier does reproduce them, but not cheaply and not the same way twice, which is exactly what a regression test needs.
# scenario: multipart greeting, expected machineAgent Hello, is this Dana?Callee Hi, you have reached Dana.Callee I cannot take your call right now.Callee Leave a message after the tone.(tone at 6.4s)Agent (holds for the tone, then speaks)Agent Hi Dana, calling about your service appointment.graded: machine at 6.9s, expected machine, passWhy a corpus
Answering machine detection is a statistical component, so its test has to be statistical too. The useful question is not whether one call passed. It is what the false-human and false-machine rates are across the corpus this week, with unknown rate and decision time beside them. Detection is endpointing in a different costume, and both break on the same things. Narrowband audio, speakerphones, hold music, beeps at unfamiliar delays. A laptop microphone in a quiet room does not test turn-taking either. So the catalog holds 43 scenarios: 32 AMD cases, 7 IVR cases and 4 PBX cases.
How a run works
apps/simulator-backend is the source of truth. It owns scenario discovery, execution adapters, persistence, recordings, grading and the HTTP API on port 8978. apps/web-console is a static Next.js client whose scenario reference is generated from the backend catalog, so scenario data is never maintained twice. Carrier code stays under telephony/providers, and telephony/registry.py is the only file that names the mock, Twilio and Telnyx adapters. The application service never imports a carrier. SQLite is the default, and SIMULATOR_DATABASE_URI switches to MySQL when persistence has to be shared. Every domain reduces to one normalized timeline, which is what the console renders and the grader reads.
Decisions and costs
- Merge the two simulators. AMD became a scenario domain on the shared platform rather than a second application. Porting it meant deleting the old Next.js server layer, the Drizzle schema and the PostgreSQL service instead of carrying them along.
- Leave the control API unauthenticated and unreachable.
compose.ingress.yamlpublishes only/twilio/*, approved/assets/*and/health, and returns 404 for/api/*. Safe operation is now a deployment property rather than a code one. - Make public AMD audio a deliberate act.
serve --with-pstnrefuses to start unlessPSTN_ACKNOWLEDGE_PUBLIC_AUDIOis set and every referenced asset clears the release allowlist and manifest checksums. - Model the PBX deterministically.
pbxsimroutes a declarative three-rooftop dealer group without being a SIP registrar. It is repeatable and free, and it is the decision I would revisit, because a deterministic router cannot reproduce a real switch under load.
Grading one scenario
A scenario is YAML, and the assertions are the interesting half. 03_multipart_carrier_greeting.yaml reproduces a greeting real carriers send in two parts, with a pocket of silence between them.
machine:
main:
- play: greeting_multipart_a.wav
- wait: 2.5
- play: greeting_multipart_b.wav
- tone: { freq: 1000, duration: 0.6 }
- hold: 45
expect:
ended_reason: call.ending.voicemail-left-message
message_start_after: { mark: tone_end, min: 0.1, max: 3.0 }
max_overlap_with_playback: { asset: greeting_multipart_b.wav, max_s: 1.0 }
An agent fails this by firing into the 2.5 second pocket, and max_overlap_with_playback is the assertion that catches it. uv run telephony-voice-sim sim --kind ivr exercises the model-only domains with no carrier involved at all.
Limits and license
Multi-step UAT flows in scripts/uat_matrix.py provision a scenario, place the call, capture JSON values, retry transient failures, grade the outcome and collect artifacts. Adapter bodies, rendered URLs and command output are redacted from the report by default. A benchmark suite scores an agent over repeats and returns a machine, human and screener confusion matrix. Telnyx is present as configuration preview only, and live Call Control execution is not implemented. Real PSTN runs cost provider money and can reach real people when a destination is wrong. The code is Apache-2.0, recording is off per managed number until enabled, and public builds exclude the audio corpus.
Next projectCommunications Infrastructure