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.
모래 (sand) to the contextually correct 모레 (day after tomorrow). Edit distance 1, similarity 96.8 %. The test reports the drift honestly; the post argues it should.Implementation: Project/AgentTest/Voice/TtsSttRoundTripTests.cs, commit b1a0ec8.
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