ILLATE
Demo, synthetic calls

Voice AI Cost Audit: a demonstration of the audit calculation workflow

Synthetic demo workload run on ILLATE's own machine. Not a client result.

A demonstration of the audit calculation workflow behind the Voice AI Cost Audit (illate.dev/voice), on 30 scripted customer-service calls, on one 8-vCPU Linux machine, CPU only, with open models. Only the STT benchmark was measured; the costs are list-price and assumed projections. No paid API was called and no money was spent. Key cost and performance figures carry tags: MEASURED from this run on ILLATE's machine, LIST vendor list price read 21 Oct 2026 (regular rates, never promos), ASSUMED a stated input, LIST x ASSUMED a list price times an assumed usage profile, LIST (DERIVED) a rate computed from a vendor's published plan prices (here Cartesia's Scale plan with overage) rather than a published pay-as-you-go price. Combined tags (e.g. MEASURED x LIST x ASSUMED) mean the number is computed from inputs of those kinds.

The plain list-price version, with no measurements, is the worked example. The offer itself is on /voice.

The workflow, step by step

  1. Workload (§2): build 30 synthetic calls, 160 caller utterances, 8 kHz telephone-band audio, with ground-truth text.
  2. STT bench (§3–4): run open STT models (INT8, CPU) on the workload; measure WER, real-time factor (RTF) and p95 latency, alone and under concurrent load; check the quality gates.
  3. Cost per connected minute, per path (§5–7): price today's bill, then (A) tune the current stack, (B) a cheaper vendor API from list prices and the call profile, (C) self-host the measured open model.
  4. 20% refund test (§8): apply the straddle rule to the best candidate path (illustrative here, because its quality gates are open).

Exact commands: §11. Machine details: §3. What needs a real client's data: §9.

Sample limit. This run uses 30 scripted synthetic calls with 497.9 s (8.3 min) of caller speech MEASURED. That is far below the audit's default client sample, which needs both 200 calls and 1,000 connected minutes (audit rule). Synthetic TTS callers are also cleaner than real ones. Treat every result below as a demonstration of the method, not as evidence about any real stack.

This demo uses a different assumed stack from the /voice-example page: its own usage profile, silence share, budget-voice and Cartesia lines, so 'today' here is $0.1389/min LIST x ASSUMED while /voice-example shows $0.123/min LIST x ASSUMED. Both are labelled examples; they are not meant to agree, and neither is a client's bill.

Result in one screen

  • Before: $0.1389 per connected minute (all-in, Vapi-style cascaded stack) LIST x ASSUMED.
  • Path 1, tune the current stack (best candidate, pending quality gates): $0.1142 to $0.0889/min, saving 17.8% to 36.0% LIST x ASSUMED.
  • Path 2, a cheaper same-tier API: $0.1145/min, saving 17.5% (quality not verified on your calls; the Cartesia rate is LIST (DERIVED) from the published Scale plan with overage) LIST (DERIVED) x ASSUMED.
  • Path 3, self-host STT on CPU: $0.1468/min on an invoice-comparable basis (compute only; internal on-call labour excluded), i.e. 5.7% more expensive at the assumed 100K min/month; $0.1498/min (7.9% more) in the separate full-cost view. Its latency gate is unverified: latency was not measured under a real-time streaming test, and the measured batch STT p95 of 1.52 s under load suggests a risk. With a negative saving it counts as zero MEASURED x LIST x ASSUMED.
  • Recommendation: Path 1, the best candidate, pending quality gates (prompt caching first, then the budget voice if it passes the listening check). On these assumptions, self-hosting STT on these CPU configurations does not pay at the assumed volume.
  • 20% test (illustrative): On the assumed bill, Path 1 spans 17.8%–36.0%, so the illustrative 20% test lands on refund. That is not a client's refund verdict: its LLM and listening gates remain open, and the bill has not been verified. If both gates pass and the cost inputs hold, the saving floor would rise to 33.6% LIST x ASSUMED.
  • Measured STT on this machine: faster-whisper small.en INT8 WER 2.0%, RTF 0.33, p95 1.14 s; large-v3-turbo INT8 WER 1.6%, RTF 1.27, p95 4.55 s MEASURED.

1. Input: agent config and usage

A sample client config (synthetic). The usage profile is stated, not measured, because there is no client here. In a real audit these come from invoices, CDRs and token logs (D1, D2, D4, D6).

# Sample client agent config (synthetic; ASSUMED inputs, not a real client)
agent:
  name: "Riverside / Northwind / Brightline front desk (synthetic)"
  platform: vapi            # Vapi-style orchestration, $0.05/min hosting (LIST)
  architecture: cascaded    # STT -> LLM -> TTS
  language: en-US
  stt:
    provider: deepgram
    model: nova-3           # streaming, regular list $0.0077/min (promo $0.0048 not used)
    billed_on: session_time # silence is billed
  llm:
    provider: openai
    model: gpt-6.1-sol      # $2.00 in / $0.10 cached / $10.00 out per 1M tokens (LIST)
    system_prompt_tokens: 4000        # ASSUMED
    prompt_order: dynamic-first       # ASSUMED: caller/date fields at the top, so prefix caching misses
    turns_per_call: 12                # ASSUMED (3 per minute)
    output_tokens_per_turn: 40        # ASSUMED
  tts:
    provider: elevenlabs
    model: eleven_v4_multilingual     # $0.08 per 1K chars (LIST)
    billed_chars_per_min: 550         # ASSUMED (500 + 10% barge-in waste)
  telephony:
    carrier: twilio
    direction: inbound, US local      # $0.0085/min (LIST), rounded up to the next minute
  silence:
    share_of_connected_time: 0.15     # ASSUMED
    idle_timeout_s: 30                # ASSUMED current setting
    voicemail_detection: off          # ASSUMED
usage:
  connected_minutes_per_month: 100000 # ASSUMED
  avg_call_minutes: 4.0               # ASSUMED
  peak_concurrent_calls: 20           # ASSUMED
InputValueTagNote
minutes_per_month100,000ASSUMEDconnected minutes / month
avg_call_min4ASSUMEDaverage connected call length
silence_share0.15ASSUMEDshare of connected time with no speech
caller_speech_share0.35ASSUMEDcaller talk time / connected time (only this is sent to a self-hosted VAD-gated STT)
tts_chars_per_min550ASSUMEDbilled agent TTS chars per connected min (500 + 10% barge-in waste; voice-cost-lab reference call)
llm_in_tok_per_min_uncached13,210ASSUMED4K-token system prompt resent on 12 turns/call; voice-cost-lab root-causes §1
llm_cached_tok_per_call51,942ASSUMEDstatic prefix + prior history servable from cache once prompt is ordered static-first
llm_new_tok_per_call900ASSUMED
llm_out_tok_per_call480ASSUMED12 turns x 40 tokens
twilio_roundup_factor1.125ASSUMEDTwilio rounds each call up to the next minute; 4-min calls with uniform remainder bill 4.5 min per 4
silence_removable_share0.333ASSUMEDshare of silence removable with idle/silence-timeout + voicemail settings in the current stack (R4)
peak_concurrent_calls20ASSUMEDpeak simultaneous calls (average is ~2.3 at 100K min/month)
target_cpu_load0.6ASSUMEDmax sustained load per instance at peak (headroom)
redundancy_instances1ASSUMEDN+1 for HA
selfhost_invoiced_ops_usd_month0ASSUMEDinvoiced monitoring/support for the STT service; none priced in this demo (self-run open-source tooling). A real audit adds any such invoice, plus paid idle capacity (already in compute: instances are always on)
selfhost_internal_labour_usd_month300ASSUMEDinternal on-call share and upgrades for one STT service; per section 7C this is internal labour, so it appears ONLY in the separate full-cost view, never in the invoice-comparable 20% test
c7i_4xlarge_usd_h0.714LISTAWS c7i.4xlarge on-demand Linux us-east-1, 16 vCPU, 32 GiB; confirmed 21 Oct 2026 23:50 IST against AWS's own public on-demand price feed (b0.p.awsstatic.com, ec2-ondemand-without-sec-sel, US East N. Virginia, Linux; published 15 Oct 2026), which backs https://aws.amazon.com/ec2/pricing/on-demand/
c7i_vcpus16LISTsame AWS feed
box_to_c7i_vcpu_ratio1ASSUMEDone vCPU of ILLATE's box (Sapphire Rapids, AMX) ~ one c7i vCPU; c7i vCPUs are hyperthreads, so this may be optimistic

2. Workload: 30 synthetic phone calls

Three scripted call types (appointment booking, order status, billing), with names, dates, order numbers, amounts and meter readings drawn from a seeded RNG. Caller turns were synthesized with Piper (open-source CPU TTS, 4 voices: 3 US, 1 UK), band-passed to 300–3,400 Hz, downsampled to 8 kHz mono and mixed with white noise at a random 15–25 dB SNR. Ground-truth text is kept per utterance.

CallsCaller utterancesCaller audioMean utteranceBy type (utterances)Tag
30160497.9 s3.11 sbooking: 50, order: 50, billing: 60MEASURED

Sample transcript (call00, booking)

AGENT  Thank you for calling Riverside Clinic. How can I help you today?
CALLER Hi, I'd like to book a dental cleaning for next week if possible.
AGENT  Of course. Can I have your full name please?
CALLER Yes, it's Maria Lopez.
AGENT  Thanks. Which day works best for you?
CALLER Would Tuesday at eleven fifteen be available?
AGENT  Yes, Tuesday at eleven fifteen is open. Shall I confirm it?
CALLER Yes please, and could you send me a text reminder the day before?
AGENT  Done. You will get a reminder. Anything else?
CALLER No, that's everything. Thank you so much.

Command and output

$ python scripts/01_make_workload.py --voices /workspace/demo-cache/voices --out workload --calls 30
calls 30 voices ['en_GB-alan-medium', 'en_US-amy-medium', 'en_US-lessac-medium', 'en_US-ryan-medium']
real	0m22.770s
user	1m58.035s
sys	0m6.001s
$ wc -l workload/*.jsonl
  160 workload/manifest.jsonl
   30 workload/transcripts.jsonl

Limit: TTS speech is cleaner and more regular than real callers (no accents beyond 4 voices, no crosstalk, no codec artefacts beyond band-limiting). WER here likely understates what real calls would show.

3. Measure: open STT models, INT8, on CPU

Machine:

$ lscpu (summary)
CPU(s):                                  8
Model name:                              Intel(R) Xeon(R) Processor
Thread(s) per core:                      1
Core(s) per socket:                      8
Socket(s):                               1
L3 cache:                                320 MiB (1 instance)
$ ISA flags of interest
amx_bf16 amx_int8 amx_tile avx512_vnni 
$ grep MemTotal /proc/meminfo
MemTotal:       16386368 kB
$ uname -r
6.12.94+
$ pip versions
ctranslate2        4.8.3
faster-whisper     1.2.1
jiwer              4.0.0
numpy              2.5.4
onnxruntime        1.31.0
piper-tts          1.8.0
scipy              1.18.1
whisper_normalizer 0.1.15
Python 3.13.5
The VM reports a generic CPU name; the ISA flags (AMX, AVX-512 VNNI) are those of Intel Sapphire Rapids, the same generation as AWS c7i.

Method: faster-whisper (CTranslate2) with compute_type=int8, greedy decoding, English, no VAD. WER uses jiwer on text passed through the Whisper English normalizer on both sides, so '25194' and 'two five one nine four' match. Pass A: one stream at a time, 8 threads, latency = time from the end of an utterance's audio to its final text. Pass B: W processes x T threads, all 160 utterances split across them and started together, giving throughput (audio seconds processed per wall second) and latency under load.

$ ./scripts/run_stt.sh     # sets ONEDNN_MAX_CPU_ISA=AVX512_CORE_VNNI MKL_ENABLE_INSTRUCTIONS=AVX512, runs 02_bench_stt.py per model
--- results/log_small.en.txt ---
{"model": "small.en", "compute_type": "int8", "decode": "greedy beam=1, language=en", "pass": "A_latency", "threads": 8, "utterances": 160, "audio_s": 497.9, "wall_s": 161.84, "rtf": 0.3250276648183471, "latency_p50_s": 1.0139524534988595, "latency_p95_s": 1.1383606195495304, "latency_max_s": 1.1998456770015764, "wer": 0.019849418206707735, "substitutions": 18, "deletions": 1, "insertions": 10, "ref_words": 1461}
{"model": "small.en", "pass": "B_throughput", "workers": 2, "threads_per_worker": 4, "wall_s": 107.03, "audio_s": 497.9, "throughput_x_realtime": 4.65211337975471, "throughput_x_realtime_per_vcpu": 0.5815141724693388, "latency_p50_s": 1.3106935109990445, "latency_p95_s": 1.518264601451119, "wer": 0.019849418206707735, "substitutions": 18, "deletions": 1, "insertions": 10, "ref_words": 1461}
{"model": "small.en", "pass": "B_throughput", "workers": 8, "threads_per_worker": 1, "wall_s": 81.88, "audio_s": 497.9, "throughput_x_realtime": 6.080995802228017, "throughput_x_realtime_per_vcpu": 0.7601244752785021, "latency_p50_s": 3.868212017501719, "latency_p95_s": 4.377122599201902, "wer": 0.019849418206707735, "substitutions": 18, "deletions": 1, "insertions": 10, "ref_words": 1461}
real	6m5.446s
user	32m7.243s
sys	5m34.615s
--- results/log_large-v3-turbo.txt ---
# Reconstructed 21 Oct 2026 22:27 IST: the original tee capture of this run was empty (stdout block-buffered, job stopped after the 8x1 OOM).
# Lines below are the summary JSON each pass wrote to disk, in the same format 02_bench_stt.py prints.
{"model": "large-v3-turbo", "compute_type": "int8", "decode": "greedy beam=1, language=en", "pass": "A_latency", "threads": 8, "utterances": 160, "audio_s": 497.9, "wall_s": 630.28, "rtf": 1.2658051377763901, "latency_p50_s": 3.965766066499782, "latency_p95_s": 4.551989795200461, "latency_max_s": 4.991652336997504, "wer": 0.01574264202600958, "substitutions": 19, "deletions": 0, "insertions": 4, "ref_words": 1461}
{"model": "large-v3-turbo", "pass": "B_throughput", "workers": 2, "threads_per_worker": 4, "wall_s": 475.68, "audio_s": 497.9, "throughput_x_realtime": 1.0467793647983024, "throughput_x_realtime_per_vcpu": 0.1308474205997878, "latency_p50_s": 5.863953725000101, "latency_p95_s": 6.6741488702002245, "wer": 0.01574264202600958, "substitutions": 19, "deletions": 0, "insertions": 4, "ref_words": 1461}
# 8x1: not completed, OOM (see runs_not_completed.json)
# 4x2: separate run, python 02_bench_stt.py --model large-v3-turbo --workload ../workload --out ../results --workers 4 --skip-a
{"model": "large-v3-turbo", "pass": "B_throughput", "workers": 4, "threads_per_worker": 2, "wall_s": 338.72, "audio_s": 497.9, "throughput_x_realtime": 1.470050918476962, "throughput_x_realtime_per_vcpu": 0.18375636480962024, "latency_p50_s": 8.320900136999626, "latency_p95_s": 9.101172161300928, "wer": 0.01574264202600958, "substitutions": 19, "deletions": 0, "insertions": 4, "ref_words": 1461}
real	5m56.056s
user	38m11.593s
sys	6m56.927s
Model (INT8, CPU)WERRTF, 1 stream, 8 threadsp50 / p95 latency, 1 streamLoad configThroughput per vCPU (x real time)p95 under loadWER under loadCalls per c7i.4xlarge*Tag (measured columns; calls per instance)
small.en2.0%0.3251.01 / 1.14 s2 proc x 4 thr0.581.52 s2.0%16.0MEASURED; MEASURED x ASSUMED
8 proc x 1 thr0.764.38 s2.0%20.8MEASURED; MEASURED x ASSUMED
large-v3-turbo1.6%1.2663.97 / 4.55 s2 proc x 4 thr0.136.67 s1.6%3.6MEASURED; MEASURED x ASSUMED
4 proc x 2 thr0.189.10 s1.6%5.0MEASURED; MEASURED x ASSUMED
large-v3-turbo(see row above)8 proc x 1 thrnot completed (out of memory). Started 21 Oct 2026 22:09 IST. Each large-v3-turbo INT8 worker process peaks at about 2.07 GB RSS (MEASURED, VmHWM), so 8 workers plus the parent need more than the 16 GB on this box. The kernel OOM killer stopped 2 of the 8 workers at about 22:10 IST; the other 6 waited at the start barrier with the CPU idle until the job was stopped at 22:20 IST. 4 workers x 2 threads was run instead (stt_large-v3-turbo_B_w4.json). MEASURED

* Calls per instance = throughput per vCPU x 16 vCPU x 60% max load / 35% caller-speech share (VAD-gated STT only transcribes caller speech). The vCPU equivalence, load cap and speech share are ASSUMED. Calls per instance (16 vCPU) is derived, not measured on a c7i.

Why RTF is high for short turns: Whisper encodes a fixed 30-second window, so a 3-second utterance costs about as much compute as a 30-second one. Per-utterance cost is roughly constant, and the real-time factor on short phone turns is far worse than on long recordings. A streaming model (e.g. Parakeet TDT, or a chunked encoder) would remove this; we did not measure one in this run.

Finding: the quality gate caught a kernel problem

On the first run, CTranslate2 used the CPU's AMX int8 kernels. small.en returned truncated transcripts (e.g. 'Yes, it's Maria Lopez.' → 'Yes, it'), and the damage grew with concurrency: WER was 4.4% with one stream at 8 threads and 17.8–24.0% with 2–8 parallel processes. The same audio decoded correctly when run alone. With AMX disabled (AVX-512 VNNI kernels), WER was 1.98% at every concurrency level. Throughput per vCPU dropped by 6% (2x4) and 10% (8x1), and single-stream RTF rose from 0.27 to 0.33. All numbers above use the AVX-512 VNNI setting. We did not isolate the root cause (library, VM or microcode). In a client audit the same check would run on the client's own hardware.

{"model": "small.en", "compute_type": "int8", "decode": "greedy beam=1, language=en", "pass": "A_latency", "threads": 8, "utterances": 160, "audio_s": 497.9, "wall_s": 136.51, "rtf": 0.2741462194726181, "latency_p50_s": 0.8311043414996675, "latency_p95_s": 1.0509468968486542, "latency_max_s": 1.5404997050027305, "wer": 0.044490075290896644, "substitutions": 35, "deletions": 24, "insertions": 6, "ref_words": 1461}
{"model": "small.en", "pass": "B_throughput", "workers": 2, "threads_per_worker": 4, "wall_s": 100.2, "audio_s": 497.9, "throughput_x_realtime": 4.969511993122246, "throughput_x_realtime_per_vcpu": 0.6211889991402807, "latency_p50_s": 1.0853319044981617, "latency_p95_s": 2.2375230923000338, "wer": 0.17796030116358658, "substitutions": 46, "deletions": 199, "insertions": 15, "ref_words": 1461}
{"model": "small.en", "pass": "B_throughput", "workers": 4, "threads_per_worker": 2, "wall_s": 83.32, "audio_s": 497.9, "throughput_x_realtime": 5.976213407822283, "throughput_x_realtime_per_vcpu": 0.7470266759777854, "latency_p50_s": 1.700960041500366, "latency_p95_s": 2.8505559712993964, "wer": 0.21286789869952089, "substitutions": 53, "deletions": 213, "insertions": 45, "ref_words": 1461}
{"model": "small.en", "pass": "B_throughput", "workers": 8, "threads_per_worker": 1, "wall_s": 73.65, "audio_s": 497.9, "throughput_x_realtime": 6.7609373700589055, "throughput_x_realtime_per_vcpu": 0.8451171712573632, "latency_p50_s": 2.969289602000572, "latency_p95_s": 5.360957835998306, "wer": 0.23956194387405885, "substitutions": 57, "deletions": 246, "insertions": 47, "ref_words": 1461}
real	6m53.725s
user	33m40.850s
sys	8m17.416s

4. Quality gates (runbook §7.4, R5 margins)

GateCandidateResultStatus
STT WER: ≤ 1 point worse than the current APIself-host faster-whispersmall.en 2.0%, turbo 1.6% MEASURED. Deepgram Nova-3 on the same audio: not measured (paid API).ASSUMED within 1 point; not verified
p95 latency: ≤ 100 ms worse than the current stackself-host faster-whisperSTT alone, end of speech → final text: p95 1.14 s with one stream on 8 threads; 1.52 s in the deployable config under load MEASURED. The current stack's streaming STT p95 was not measured.UNVERIFIED: latency was not measured under a real-time streaming test, and the current stack was not measured. The measured batch p95 suggests a risk; to be settled against the client's D5 logs.
Listening check (≥ 30 utterances, client raters)Path 1 budget voice; Path 2 TTSNot run: no client raters on a synthetic workloadOpen. The budget voice counts in S_high only.
LLM task success on held-out transcriptsPath 1 caching (prompt reorder)Not run (no client transcripts). Caching does not change model weights; reordering the prompt can still change behaviour.Open
Same tier, untested API (R9)Path 2Priced at regular list priceCounts, labelled 'quality not verified on your calls'

5. Before: all-in cost per connected minute = $0.1389

Runbook §3.2 split. Silence is moved out of the per-minute lines (platform, STT, telephony) into its own line, so the total is unchanged.

Line$/minShareTagHow
platform$0.0425030.6%LIST x ASSUMEDVapi $0.05 x speech share
stt$0.006554.7%LIST x ASSUMEDNova-3 streaming x speech share
llm$0.0276219.9%LIST x ASSUMEDgpt-6.1-sol, caching ineffective (prompt not static-first)
tts$0.0440031.7%LIST x ASSUMEDElevenLabs v4 x 550 chars/min
telephony$0.007235.2%LIST x ASSUMEDTwilio inbound x speech share
silence$0.009937.1%LIST x ASSUMED15% x (platform + STT + telephony per-minute rates)
rounding$0.001060.8%LIST x ASSUMEDTwilio next-minute round-up
Total$0.13888100%

Prices used (all LIST except Cartesia, which is LIST (DERIVED) from the published Scale plan with overage): vapi_platform 0.05 (Vapi hosting, nominal $0.05/min, https://vapi.ai/pricing. After the silence split, the allocated platform line is $0.05 x 85% = $0.0425 (30.6% of this bill); its silent-time share sits in the silence line); deepgram_nova3_stream 0.0077 (regular rate, not the $0.0048 promo, https://deepgram.com/pricing); gpt61sol_in 2.0 ($/1M, https://platform.openai.com/docs/pricing); gpt61sol_cached 0.1 ($/1M); gpt61sol_out 10.0 ($/1M); eleven_v4_per_1k 0.08 (ElevenLabs v4 Multilingual, https://elevenlabs.io/pricing/api); eleven_flash_per_1k 0.04 (ElevenLabs Flash/Turbo, same page); twilio_in 0.0085 (Twilio US local inbound, https://www.twilio.com/en-us/voice/pricing/us); scribe_v2_rt 0.0065 (ElevenLabs Scribe v2 Realtime $0.39/h, https://elevenlabs.io/pricing/api); cartesia_sonic_per_1k 0.03791 (Cartesia Sonic-3.6: NOT a published pay-as-you-go rate. DERIVED from the published Scale plan with overage (1 credit = 1 char): this workload is 100,000 min x 550 chars = 55M chars/month; Scale $299/mo includes 8M credits, the other 47M credits are overage at $38 per 1M = $1,786; $299 + $1,786 = $2,085/month / 55,000K chars = $0.03791/1K. Get a vendor quote before using this in a client refund test. https://www.cartesia.ai/pricing); grok_voice3 0.08 (xAI Grok Voice 3.0 S2S $/min + $0.004/text input, https://docs.x.ai/developers/pricing); voxtral3_agent_large 0.08 (Voxtral 3 agent API with larger model, https://mistral.ai/news/voxtral-3).

6. The three paths

Path 1: tune what runs now (runbook §5)

Caching math: an uncached LLM line resends 13,210 input tokens a minute at $2.00/1M. With a static-first prompt, 51,942 of 52,842 input tokens per call are served from cache at $0.10/1M. Token counts ASSUMED, prices LIST.

LeverLineSaving $/min% of billTagCounted inGate / note
Prompt caching (static-first prompt)LLM$0.0246717.8%LIST x ASSUMEDlow + highNo audio change. LLM task-success check on held-out transcripts still required (not run: no client transcripts).
Budget voice: ElevenLabs v4 -> Flash/TurboTTS$0.0220015.8%LIST x ASSUMEDhigh onlyListening check (>=30 utterances, client staff) NOT RUN: synthetic workload, no client raters. Counted in high only.
Silence settings: idle/silence timeout, voicemail detection (current stack only, R4)Silence$0.003312.4%ASSUMEDhigh onlyRemovable share is assumed; an ASSUMED number never passes the 20% test on its own. High only.
After$0.1142 (low) / $0.0889 (high)saving 17.8% to 36.0%

Path 2: cheaper API, same tier only (R9)

LeverLineSaving $/min% of billTagCounted inGate / note
STT: Deepgram Nova-3 -> ElevenLabs Scribe v2 Realtime (vendor's top realtime tier)STT+silence$0.001200.9%LISTlow + highquality not verified on your calls
TTS: ElevenLabs v4 -> Cartesia Sonic-3.6 (premium tier)TTS$0.0231516.7%LIST (DERIVED) x ASSUMEDlow + highquality not verified on your calls; Cartesia $0.03791/1K is LIST (DERIVED) from the published Scale plan arithmetic ($299 for 8M credits + 47M overage credits x $38 per 1M = $2,085/month for 55M chars), not a published pay-as-you-go rate; vendor quote needed before a client refund test; concurrency UNVERIFIED: Cartesia Scale allows 15 concurrent TTS requests vs the assumed 20 peak calls, and concurrent TTS requests are not the same as calls, so this may need a higher plan
After$0.1145 (low) / $0.1145 (high)saving 17.5% to 17.5%

Speech-to-speech options checked against the STT + LLM + TTS lines they would replace:

Option$/minReplacesSavingTag
Grok Voice 3.0 ($0.08/min + $0.004/text input)$0.0800$0.0793$-0.0007LIST; replaced lines LIST x ASSUMED
Voxtral 3 agent, larger model ($0.08/min)$0.0800$0.0793$-0.0007LIST; replaced lines LIST x ASSUMED

Lower tiers were not counted (R9), for example AssemblyAI Universal-Streaming at $0.0025/min or the Voxtral 3 agent API with Mistral Small 4 at $0.04/min, which swaps the LLM tier. They could still be tested from the client's own account.

Path 3: self-host on CPU (runbook §7)

Priced with the deployable config (the lowest p95 under load): small.en, 2 proc x 4 threads. Measured throughput is 0.58x real time per vCPU MEASURED, giving 16.0 calls per c7i.4xlarge. Instances = ceil(20 peak / 16.0) + 1 (N+1) = 3 x $0.714/h LIST x 730 h = $1,564/month MEASURED x LIST x ASSUMED. Invoiced monitoring/support: $0/month ASSUMED (none priced in this demo; idle capacity is already paid because instances are always on). Invoice-comparable: $0.0156/min against Deepgram's $0.0077 LIST; this is the figure used in the 20% test.

Separate full-cost view (not used in the 20% test, section 7C). Adding internal on-call and upgrade labour of $300/month ASSUMED gives $0.0186/min for STT and $0.1498/min all-in (7.9% more than today) MEASURED x LIST x ASSUMED.
ModelConfigInstancesCompute $/monthSelf-host STT $/min, invoice-comparableFull-cost view $/min (+ internal labour)Marginal $/min at full loadBreak-even volume vs Nova-3 (invoice-comparable)Tag
small.en2x4 (p95 1.52 s)3$1,564$0.0156$0.0186$0.00045550K min/moMEASURED x LIST x ASSUMED
small.en8x1 (p95 4.38 s)2$1,042$0.0104$0.0134$0.00034280K min/moMEASURED x LIST x ASSUMED
large-v3-turbo2x4 (p95 6.67 s)7$3,649$0.0365$0.0395$0.00199not reached up to 20M min/moMEASURED x LIST x ASSUMED
large-v3-turbo4x2 (p95 9.10 s)5$2,606$0.0261$0.0291$0.00142not reached up to 20M min/moMEASURED x LIST x ASSUMED

Reading: once the CPU is busy, the compute is a fraction of a cent per minute MEASURED x LIST. At 100K min/month, though, average load is about 2.3 calls ASSUMED, and sizing for 20 peak calls needs 2 to 7 always-on instances (N+1) depending on model and config; every config costs more than the API even before internal labour. STT (including its share of silence) is only 5.5% of this bill, so even a self-hosted STT line at $0 would save just 5.5%. large-v3-turbo is about 4x slower than small.en on this CPU, so it needs several times as many instances. On the self-hosted path no audio goes to a model vendor: STT runs on instances the client controls. LLM and TTS self-hosting (e.g. Qwen3-8B, Kokoro-82M on GPU) were not measured and are not priced here. They would also change the LLM tier, so they would need the LLM task check and the listening check before they could count.

Command and output (pricing)

$ python scripts/03_price_paths.py --results results
B = 0.1389 $/min
platform 0.0425
stt 0.00655
llm 0.02762
tts 0.044
telephony 0.00723
silence 0.00993
rounding 0.00106
Path 1: tune current stack S_low 0.0247 (17.8%)  S_high 0.0500 (36.0%)  after 0.1142..0.0889
Path 2: cheaper API, same tier S_low 0.0243 (17.5%)  S_high 0.0243 (17.5%)  after 0.1145..0.1145
Path 3: self-host STT on CPU (LLM, TTS stay on API) S_low -0.0079 (-5.7%)  S_high -0.0079 (-5.7%)  after 0.1468..0.1468
S2S {'option': 'Grok Voice 3.0 ($0.08/min + $0.004/text input)', 'usd_min': 0.08, 'replaces_stt_llm_tts': 0.07932, 'saving': -0.00068}
S2S {'option': 'Voxtral 3 agent, larger model ($0.08/min)', 'usd_min': 0.08, 'replaces_stt_llm_tts': 0.07932, 'saving': -0.00068}
small.en {'wer': 0.0198, 'rtf_single_stream_8t': 0.325, 'p50_s': 1.014, 'p95_s': 1.1384}
    {'workers': 2, 'threads': 4, 'x_realtime_per_vcpu': 0.58151, 'p95_latency_s_under_load': 1.51826, 'wer_under_load': 0.01985, 'calls_per_instance': 15.9501, 'instances': 3, 'compute_usd_month': 1563.66, 'selfhost_stt_usd_min_invoice_comparable': 0.01564, 'selfhost_stt_usd_min_full_cost': 0.01864, 'marginal_usd_min_at_full_load': 0.00045, 'breakeven_min_month_vs_nova3': 550000}
    {'workers': 8, 'threads': 1, 'x_realtime_per_vcpu': 0.76012, 'p95_latency_s_under_load': 4.37712, 'wer_under_load': 0.01985, 'calls_per_instance': 20.84913, 'instances': 2, 'compute_usd_month': 1042.44, 'selfhost_stt_usd_min_invoice_comparable': 0.01042, 'selfhost_stt_usd_min_full_cost': 0.01342, 'marginal_usd_min_at_full_load': 0.00034, 'breakeven_min_month_vs_nova3': 280000}
large-v3-turbo {'wer': 0.0157, 'rtf_single_stream_8t': 1.2658, 'p50_s': 3.9658, 'p95_s': 4.552}
    {'workers': 2, 'threads': 4, 'x_realtime_per_vcpu': 0.13085, 'p95_latency_s_under_load': 6.67415, 'wer_under_load': 0.01574, 'calls_per_instance': 3.58896, 'instances': 7, 'compute_usd_month': 3648.54, 'selfhost_stt_usd_min_invoice_comparable': 0.03649, 'selfhost_stt_usd_min_full_cost': 0.03949, 'marginal_usd_min_at_full_load': 0.00199, 'breakeven_min_month_vs_nova3': None}
    {'workers': 4, 'threads': 2, 'x_realtime_per_vcpu': 0.18376, 'p95_latency_s_under_load': 9.10117, 'wer_under_load': 0.01574, 'calls_per_instance': 5.04017, 'instances': 5, 'compute_usd_month': 2606.1, 'selfhost_stt_usd_min_invoice_comparable': 0.02606, 'selfhost_stt_usd_min_full_cost': 0.02906, 'marginal_usd_min_at_full_load': 0.00142, 'breakeven_min_month_vs_nova3': None}
{
 "baseline_B": 0.1388825,
 "best_candidate_pending_gates": "Path 1: tune current stack",
 "r_low": 0.17764261156013175,
 "r_high": 0.3598829946177524,
 "illustrative_verdict": "straddle (r_low < 20% <= r_high): full refund",
 "status": "illustrative only: Path 1 is the best candidate, pending gates (LLM task-success and listening checks open; bill not verified)",
 "if_both_gates_pass_and_inputs_hold": {
  "r_low": 0.33604989829532156,
  "note": "saving floor would rise to this only if the budget voice (listening check) and the prompt reorder (LLM task-success check) both pass on a client's calls and invoices/logs confirm the cost inputs"
 },
 "path2": "r_high < 20%: full refund",
 "path3": "gates unverified and negative saving: S = 0 (R3)"
}

7. Before vs after, and the recommendation

PathAfter $/min (low saving)After $/min (high saving)Saving r_low – r_highGatesCounts for the 20% testTag
Before$0.1389$0.1389LIST x ASSUMED
Path 1: tune current stack$0.1142$0.088917.8% – 36.0%Best candidate, pending quality gates: listening + LLM task-success checks openIllustratively; not a verdictLIST x ASSUMED
Path 2: cheaper API, same tier$0.1145$0.114517.5%quality not verified on your calls; Cartesia rate LIST (DERIVED); Cartesia concurrency unverifiedConditionally (R9)LIST (DERIVED) x ASSUMED
Path 3: self-host STT on CPU (LLM, TTS stay on API)$0.1468$0.1468-5.7%Latency gate unverified (no real-time streaming test); WER vs API unverifiedS = 0 (R3)MEASURED x LIST x ASSUMED
Recommendation: Path 1, tune the current stack (best candidate, pending quality gates). Order the system prompt static-first so that prefix caching works (largest single lever, list-price arithmetic, no audio change). Then run the listening check on ElevenLabs Flash/Turbo against v4 with the client's staff. Then set the idle/silence timeouts and voicemail detection. Self-hosting STT on these CPU configurations does not pay at the assumed volume, and the best same-tier API swap (17.5%) is smaller than tuning. Path 1 is the best candidate, pending quality gates, not a passed path. The biggest line, the platform fee (nominal rate $0.05/min LIST; allocated platform line $0.0425, 30.6% of this bill LIST x ASSUMED), is not touched by any counted lever. Leaving the platform is a product change, so it is reported as upside only.

8. 20% refund test (runbook §9)

StepValue
All values in this table are LIST x ASSUMED: list prices times the stated usage profile. The 20% threshold is the offer's rule.
B (all-in $/connected min, excl. labour)$0.13888
Best candidate, pending quality gatesPath 1: tune current stack
S_low (caching only: LIST prices x stated profile)$0.02467 → r_low = 17.8%
S_high (+ budget voice, + assumed silence removal)$0.04998 → r_high = 36.0%
Illustrative result on the assumed billstraddle (r_low < 20% <= r_high): full refund. Not a client's refund verdict: the LLM and listening gates are open and the bill is unverified.
If both gates pass on a client's calls and the cost inputs holdsaving floor would rise to r_low = 33.6% (budget voice via the listening check, prompt reorder via the LLM task-success check, inputs confirmed from invoices and logs)
Path 2 aloner_high < 20%: full refund (17.5%)
Path 3gates unverified and negative saving: S = 0 (R3)

Straddle rule: r_low < 20% ≤ r_high gives a full refund. Here the 20% calculation is illustrative and cannot establish a real refund decision: two gates are open (the listening check and the LLM task-success check), and a real audit settles them on the client's calls. The profile here is ASSUMED. In a real audit, the S_low inputs must be MEASURED or LIST, because an ASSUMED number never passes the test on its own.

9. What needs a real client's data

This demo shows the workflow runs and is observable. It cannot produce a client's answer. These inputs only a client can supply:

10. Not measured in this run

11. A real run on Modal, 22 October 2026

Two voice-agent turns on one synthetic call, run for real on one rented GPU, to show how a cost gets measured. The same call went through the whole pipeline twice: a first run, then a warm one. It's not a client result, and at this tiny volume most of the bill is start-up.

StageFirst runWarm run
Speech-to-text1.75 s1.61 s
LLM (176 tokens in, 88 out)3.78 s2.53 s
Text-to-speech (30.9 s of audio out)1.23 s0.68 s
One full turn6.76 s4.82 s

Start-up, before the first turn: 12.2 s for the container to start, then 3.7 s to load text-to-speech, 2.7 s for speech-to-text and 6.0 s for the LLM.

CostAmount
Modal's bill for this whole demo: container start, model loading, making the input clip, and both turns (CPU $0.0060, T4 $0.0062, memory $0.0003, network $0.0012)$0.0137
The same two turns at vendor list prices, usage charges only: Deepgram Nova-3 ($0.0077/min, regular price), GPT-5.4 mini ($0.75 and $4.50 per million tokens in and out), ElevenLabs Flash ($0.05 per 1,000 characters)$0.053

These two rows cover different things, so read them separately, not as a saving. Modal's bill includes start-up and loading, which a real deployment spreads over many calls, and leaves out idle time between calls; the list-price row is per-use charges only. Neither is a cost per minute. The phone line and any platform fee are left out of both. Modal's figures are from its billing report for app ap-4Ubdw0ij5ZrjsqGkPmQ6bF. The list-price row counts tokens with Qwen's tokenizer, so OpenAI's own count would differ slightly.

What this shows, and what it doesn't

Listen to the caller and the agent's reply, or read the script that ran it.

12. Reproduce

All scripts, raw JSON/CSV results and logs are in scripts/ and results/ next to this page (see scripts/README.md). The run is CPU only, on open models. The Piper voices and Whisper weights are downloaded from Hugging Face on first run. Timings vary with machine load; WER was identical across runs with AMX off.

# environment (exact versions: scripts/requirements.lock.txt)
python3 -m venv venv && source venv/bin/activate
pip install -r scripts/requirements.lock.txt
mkdir -p voices && (cd voices && for v in en_US-lessac-medium en_US-ryan-medium en_US-amy-medium en_GB-alan-medium; do python -m piper.download_voices $v; done)
cd scripts
# step 1: workload (seeded: identical transcripts each run)
python 01_make_workload.py --voices ../voices --out ../workload --calls 30
# step 2: STT bench, AMX kernels off (see results/amx_on_finding/)
export ONEDNN_MAX_CPU_ISA=AVX512_CORE_VNNI MKL_ENABLE_INSTRUCTIONS=AVX512
python 02_bench_stt.py --model small.en       --workload ../workload --out ../results --workers 2,8
python 02_bench_stt.py --model large-v3-turbo --workload ../workload --out ../results --workers 2,4
#   (large-v3-turbo with 8 workers needs more than 16 GB RAM; on this box it was OOM-killed)
#   ./run_stt.sh runs the same two models in one go
# step 3 + 4: cost per path and the 20% test
python 03_price_paths.py --results ../results | tee ../results/log_price.txt
# page
python 04_build_html.py --demo ..

Generated by scripts/04_build_html.py. Banner, tags and numbers are produced from results/costs.json. Demo date: 21 Oct 2026.

Run this on your own stack

The Voice Cost Audit runs this workflow on your invoices and a sample of your calls, inside your infrastructure: $1,000 fixed, or ₹75,000 for India-based teams.