Native ASR provider (GOC-33)¶
A native Rust speech-to-text provider, reachable over the engine's own
MessagePack/UDS wire protocol as Method::Asr { op }. This closes the first
half of the operator's bi-directional audio goal (audio → text); native
text-to-speech is a sibling lane (GOC-34).
Scope: what this is, and what it deliberately is not¶
This lane ships a direct, non-durable, request/response transcription
call — the compatibility surface agent-packages/agents/audio-transcriber's
pluggable TranscriptionProvider seam reaches to offer a native alternative
to its default Faster-Whisper (Python) backend. It is explicitly not:
- the isolated
epistemic-graph-voice-workerprocess the full GOC-33 lane doc describes (plans/graph-os-completion-program/lanes/GOC-33-native-asr-meetily-runtime.md, work items W03/W06) — that is a separate, larger undertaking (model-pool lifecycle, lease/fence/cancellation-generation state, CAS-backedArtifactBundle/outbox publication, AU orchestration adapter); the isolated worker - bound to a governed GOC-32 audio stream/
CarrierRef— a real, policy- authorizedasr.result.v1commit (viaeg_audio::asr::finalize_result) needs both, and neither exists at this simple RPC boundary yet; - a Parakeet/ONNX provider (see binding choice below).
Binding choice¶
Two Rust ASR bindings are proven in open-source-libraries/meetily
(frontend/src-tauri/): whisper-rs over whisper.cpp/ggml, and ort (ONNX
Runtime) over a hand-rolled Parakeet decoder. This lane implements
whisper-rs, retained/pinned from crates.io (no vendor fork) and does
not implement Parakeet:
whisper-rs0.16'sWhisperSegment/WhisperTokenAPI already exposes real provider-derived segment start/end timestamps,no_speech_probability, and per-token timing/probability — everything the frozen ASR contract needs. Meetily's own Parakeet path decodes token timestamps internally but its provider trait discards them (ParakeetEngine::transcribe_audioreturns onlyresult.text); reaching timing parity would mean writing a full ONNX decoder/tokenizer pipeline from scratch, not adapting a published crate.- Parakeet's model license (CC-BY-4.0) and custom mirror hosting are exactly the kind of acquisition/verification concern GOC-36 (governed model acquisition) owns — deferring Parakeet avoids a second, unqualified model supply chain in the same change that must prove the CPU-safety build contract below.
Full binding-choice justification, licensing/provenance record, and the
CPU-portability build contract live in crates/eg-asr-whisper/src/lib.rs's
module doc (the crate that actually links whisper-rs) — this page
summarizes; that module doc is authoritative.
Architecture¶
flowchart TB
subgraph py["Python callers"]
AT["audio-transcriber\nTranscriptionProvider seam\n(epistemic_graph/asr_provider.py)"]
AU["agent-utilities /\nother epistemic_graph.client callers"]
end
subgraph wire["epistemic_graph.client (MessagePack / UDS — no second transport)"]
C["client.asr.transcribe_file(...)"]
end
subgraph engine["epistemic-graph engine (facade, feature asr-whisper)"]
D["dispatch.rs\nMethod::Asr { op } — self-routes,\nNOT graph-scoped, ahead of\ndispatch_graph_op"]
H["handlers/asr.rs\nspawn_blocking, maps AsrOp -> JSON"]
end
subgraph provider["eg-asr-whisper crate (the ONLY whisper.cpp dependency)"]
MV["model.rs\nverify_model: sha256 fail-closed,\nnever downloads"]
WV["wav.rs\ndecode_wav_16k_mono: fail-closed\non format/rate/truncation"]
WP["WhisperAsrProvider\ntranscribe_streaming: bounded windows,\non_partial callback, abort_callback\ncancellation"]
end
subgraph contract["eg-audio::asr (frozen contract, GOC-33-W02)"]
VAL["AsrSegment::validate / SegmentTiming::validate /\nQuality::validate — every produced segment\nis checked before acceptance"]
end
subgraph native["whisper-rs -> whisper.cpp/ggml (CPU, portable baseline)"]
WC["ggml_graph_plan / whisper_full_with_state"]
end
AT --> C
AU --> C
C -->|"eg2. envelope"| D
D --> H
H --> MV
H --> WV
H --> WP
MV --> WP
WV --> WP
WP -->|"constructs & checks"| VAL
WP --> WC
WC -->|"segments, timing, no_speech_prob,\ntoken probabilities"| WP
H -->|"JSON: text, segments, language"| D
CPU portability (no x86_64 host in this fleet has AVX2)¶
Every x86_64 host was individually audited (/proc/cpuinfo): the interactive
dev host (Westmere, 2010) and both build hosts (Westmere, 2010, and Sandy
Bridge, 2012) all lack x86-64-v3/AVX2 entirely (the fleet's GPU host is a
separate aarch64 architecture). This is the decisive
reason this lane implements whisper-rs/ggml rather than ONNX Runtime: the
sibling native-TTS lane independently hit a dead end because ort's only
x86_64 CPU prebuilt hard-requires AVX2 with no baseline fallback artifact —
no host here can run it natively, and that lane could only prove correctness
under qemu-x86_64 -cpu max emulation. ggml does not share that failure
mode: it is compiled from source per target and has genuine non-AVX2 code
paths (the fixed baseline below), so it was verified to actually run
natively — see "Hardware verification" below.
whisper-rs-sys compiles whisper.cpp/ggml from source via cmake, forwarding
any GGML_*/WHISPER_*/CMAKE_* environment variable straight through as a
-D define. The repo's .cargo/config.toml pins a fixed, portable
baseline — GGML_NATIVE=OFF, GGML_AVX=OFF, GGML_AVX2=OFF,
GGML_FMA=OFF, GGML_F16C=OFF, GGML_AVX512=OFF, and (empirically required —
see below) GGML_BMI2=OFF.
This was not solved by reasoning about flags alone. The first real-fixture
run (crates/eg-asr-whisper/tests/real_transcription.rs, GOC-33) SIGILLed
inside ggml_graph_plan on a shlx (BMI2) instruction even with every AVX
flag already off* — GGML_BMI2's CMake default is ON independent of
GGML_NATIVE, an upstream detail not documented anywhere obvious. gdb's
backtrace on the actual crash was what surfaced it. ggml's genuine runtime
CPU-dispatch mechanism, GGML_CPU_ALL_VARIANTS=ON (the C-side equivalent of
is_x86_feature_detected!), was evaluated and rejected for this change: it
hard-requires GGML_BACKEND_DL=ON, which turns the CPU backend into a
runtime-dlopened .so selected from a search path — a deploy-image
packaging change outside this lane's scope, and the exact configuration class
the known upstream defect ggml-org/whisper.cpp#2963 was filed against. The
fixed baseline is always safe (never SIGILLs on any x86_64 host in the fleet)
at the cost of leaving AVX2/AVX512 throughput on the table on any FUTURE host
that does have it — an explicit, documented trade-off. See
.cargo/config.toml's own comment for the full record.
Hardware verification (native, not emulated)¶
Built and run natively (no qemu/emulation) on the Sandy Bridge build host
(Intel(R) Xeon(R) CPU E5-4620 — /proc/cpuinfo confirms
avx sse4_1 sse4_2 present, avx2/fma/f16c/bmi2 all absent). A real
ggml-tiny.en.bin model (MIT, huggingface.co/ggerganov/whisper.cpp,
digest-verified) transcribed a synthesized 16 kHz mono speech fixture
end to end, producing real, recognizable text with real timing/quality, and
a real cancellation-mid-stream Cancelled error. This is what actually
surfaced the GGML_BMI2 requirement above: the first attempt SIGILLed
precisely because it ran on real Sandy Bridge hardware, not a newer or
emulated CPU that would have masked the gap.
Reachability¶
- Facade feature
asr-whisper(off by default, not part offull) links theeg-asr-whispercrate and enablesMethod::Asr. src/server/handlers/asr.rsself-routes ahead of the per-graph dispatch chain (likeMethod::Quantum/Method::Viz) — a transcription reads no persisted graph state and commits no durable record here.epistemic_graph.client.AsrClient(client.asr.transcribe_file(...)) is the Python entry point, wired into bothEpistemicGraphClientandSyncEpistemicGraphClient.epistemic_graph/asr_provider.py'sbuild_provider()registers under theaudio_transcriber.asr_providersentry-point group soaudio-transcriberdiscovers this provider by entry point (guarded try/except on that side — an unreachable engine or missing native dependency never prevents that server from starting with Faster-Whisper).
Honesty guarantees¶
- Model verification, never silent download (GOC-36 owns acquisition):
verify_modelreads the caller-declared model path, hashes it, and fails closed — distinctly from every other rejection — on an absent file or a digest mismatch. - Fail closed, never empty-success: an unsupported sample rate/channel
layout/bit depth, a truncated
datachunk, or zero accepted segments (e.g. silence) are each a distinct typedeg_audio::asr::AsrError, never an empty transcript presented as a completed one. - Cancellation is real, not cosmetic:
WhisperAsrProvider:: transcribe_streamingchecks a sharedCancelFlagbetween windows AND wires it into whisper.cpp's ownset_abort_callback_safe, so cancellation can interrupt mid-window decode, not only at the next window boundary. A cancelled request returnsAsrError::Cancelled, never a truncated "success". - Quality is calibrated or explicitly unavailable, never a heuristic
dressed as a probability:
avg_logprobis the mean ofln(token probability)over a segment's tokens (OpenAI Whisper's own definition, not Meetily's discredited text-length heuristic — seecrates/eg-audio/src/asr.rs's module doc for why that distinction is load-bearing here), andno_speech_probcomes directly from whisper.cpp'sno_speech_probability(). - Word-level timing is provider-derived or absent, never fabricated: word spans are merged from real per-token timestamps using whisper.cpp's own space-prefix word-boundary convention; if token timestamps were not requested or a token carries no usable timing, no span is invented for it.
Residual gaps (explicit, not silently deferred)¶
- No isolated worker process, model pool, lease/fence state, or CAS-backed
durable
asr.result.v1publication (W03/W06 in the full lane doc). - No Parakeet/ONNX provider (W05).
- No WER/RTF/latency/resource qualification evidence (W07).
supports_streaming()on the Python provider honestly answersFalsetoday — it wraps the engine's streaming-capable provider in one batch call; a real streaming RPC surface is GOC-35'sVoiceSessionlane to build.- GPU execution providers (
cuda/vulkan/metal/coreml/hipblasCargo features oneg-asr-whisper) are wired but not hardware-qualified in this change.