Series: AgentZero Lite — Part 8. The previous post (Part 7 — Step 2 Landed: Akka.NET Streams Now Carry the Whole Voice Path) put a streaming substrate under the voice subsystem. This one puts a measurement instrument on top — so we know whether it's getting better or worse between commits.


TL;DR

Implementation: Project/AgentTest/Voice/TtsSttRoundTripTests.cs, commit b1a0ec8.


Why we needed this now

The Part 7 → Part 8 gap is a classic refactor-then-instrument move.

By the end of Part 7 the voice pipeline had a streaming substrate, a barge-in detector, a sentence chunker, and an opt-in feature flag. What it didn't have was a way to answer the question "is the new version better?" — every quality check was a human listening to a speaker and squinting at a transcript.

For a single change that's fine. For a series of small tuning passes (slow the SAPI rate by 15 %, pad the WAV with 1 s of room noise, switch the resampler, swap the model) it's the wrong loop. We were debating whether 안녕하세요 came out as 안녕하세요 or as Thank you for watching, please subscribe and like based on memory. Time to measure.

Constraints I set going in:

That gave the topology that landed:

flowchart LR
    A["input string"] --> B["WindowsTts.SynthesizeAsync"]
    B --> C["WAV bytes"]
    C --> D["File.WriteAllBytes<br/>*-tts.wav"]
    C --> E["WavToPcm.To16kMono<br/>(NAudio resample)"]
    E --> F["PCM 16k mono"]
    F --> G["File.WriteAllBytes<br/>*-stt-input-16k.wav"]
    F --> H["WhisperLocalStt.TranscribeAsync<br/>(medium, CPU)"]
    H --> I["transcript string"]
    A --> J["Normalize<br/>(case + punct + WS)"]
    I --> J
    J --> K{"== ?"}
    K --> L["VERDICT + diff + timings"]

    classDef io fill:#1f2937,stroke:#0ea5e9,color:#e5e7eb
    classDef pure fill:#0b3d2e,stroke:#10b981,color:#d1fae5
    class B,H,E pure
    class D,G io