Repository navigation
feat(dart): add streaming transcription provider contract - #55
Merged
harshsaver merged 2 commits intoSep 30, 2026
Conversation
Define a provider-neutral boundary between Murmur audio and speech recognition, plus a deterministic fake provider for coordinator tests. It supplies the provider seam used by the capture coordinator (october-dev#36). Contract (lib/src/provider.dart, exported from murmur_protocol): - VoiceProvider exposes a stable id, fixed capabilities, and a synchronous, inert createSession(ProviderSessionRequest), so the caller subscribes before start and cannot miss startup events. An unsupported format throws VoiceError(unsupported_format). - ProviderSessionRequest carries only a session id and AudioFormat. It has no credentials, capture mode, device source, or metadata map. Providers stay mode-neutral, and credentials enter only through adapter config. - ProviderSession: events, start, addFrame, mute, unmute, finalize, stop. Sessions are one-shot and never reconnect; a retry is a new session. - Events: partials replace the open hypothesis, finals are append-only verbatim fragments whose concatenation is the utterance, and rejected segments are never assembled. Readiness, amplitude, and a terminal ProviderFailure complete the set. Text lives only on the event stream. - finalize is graceful: the gate closes synchronously, accepted audio is decoded, and the remaining finals are emitted. stop is a hard cancel that settles pending work with cancelled. A state matrix specifies every operation after stop, failure, or finalize. - Optional capabilities: immediateMute, warmUnmute (requires immediateMute), realAudioReadiness, amplitude, and rejectedFinals. Mute, unmute, and finalize are serialized and single-flight. - The caller owns pre-open buffering. At most one addFrame is outstanding. Adapter queues are bounded and documented. An empty queue always admits a valid frame, and an oversized frame throws. - Documented error codes: cancelled, unsupported_format, unauthorized, rate_limited (retryAfterMs), and provider_disconnected. Test support (test/support/fake_voice_provider.dart) is a manually driven fake with no timers. It enforces the contract by default and can delay or fail startup, emit partials, finals, and rejections, finalize late or empty, fail, disconnect, and ignore cancellation. The terminal state is recorded before listeners are notified, so reentrant callbacks see the ended session. Rewrite the VoiceProvider section of docs/voice-runtime.md and update the package README and CHANGELOG. No proto, fixture, dependency, or other-SDK changes. Refs october-dev#7 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
harshsaver
approved these changes
Sep 30, 2026
harshsaver
left a comment
Member
There was a problem hiding this comment.
Thanks, this is a thorough contract and it covers every #7 criterion. Local: dart format/analyze clean, 78/78 tests (59 new). It reuses VoiceError and the snake_case codes from runtime.dart, adds no dependencies, and keeps the fake in test/support. Merging — and agreed with the plan in the description: #51 rebases onto this provider.dart and fake.
Non-blocking follow-ups:
docs/voice-runtime.md:163still says "empty finals are valid outcomes and do not become actions", butProviderFinal('')now throws and "nothing said" is zero finals. Please reword that bullet so the two sections agree.- The endpointing section mentions
muted(flushing: false), butmute()(provider.dart:288) always flushes. Either note thatstopcovers the non-flushing case, or drop that doc line. fake_voice_provider.dart:296marks the stop-inducedcancellederror asretryable: true. Worth one sentence in the contract saying whethercancelledis retryable, so adapters agree.- Cross-PR consistency with #56 (graceful finalization as baseline vs. a capability; one error-code registry) — I've asked for that on #56 since it's rebasing anyway.
9 tasks done
# Conflicts: # sdks/dart/murmur_protocol/CHANGELOG.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Issue #7 needs a provider-neutral streaming transcription boundary that works with audio from any Murmur connector. It must not expose a vendor SDK, and it needs a deterministic fake provider for coordinator tests. This PR supplies the provider seam for the capture coordinator (#36).
This change:
VoiceProvider,ProviderSession,ProviderSessionRequest,ProviderCapability, and a sealedProviderEventfamily tomurmur_protocolfinalizefrom the hard-cancelstop, with a state matrix for every operation after start, finalize, stop, or failureVoiceProvidersection ofdocs/voice-runtime.mdand updates the package README and CHANGELOGContract
createSession(request)is synchronous, performs no I/O, and emits nothing. The caller subscribes toeventsbeforestart(). An undecodableAudioFormatthrowsVoiceError(code: "unsupported_format", retryable: false).ProviderSessionRequestholds onlysessionIdandformat. It deliberately has no capture mode,VoiceSource, metadata map, or credential field.ProviderPartialis the complete, replaceable hypothesis of the open segment, and''clears it.ProviderFinalcommits the segment. Finals are never revised and include their own separators, so an utterance is the exact concatenation of its finals (CJK and punctuation stay correct). Empty finals are invalid; "nothing said" is zero finals.ProviderRejectedcommits and discards the segment and never enters assembly.events.finalize()andmute()returnFuture<void>.finalize()closes the gate synchronously, lets accepted audio finish decoding, emits the remaining finals, and then closes. It is single-flight and terminal.stop()closes the gate, discards unaccepted audio, completes every pending operation withcancelled, adds no new events, and never throws. Repeated and concurrent calls share one future.immediateMute,warmUnmute(requiresimmediateMute),realAudioReadiness,amplitude, andrejectedFinals.mute/unmutethrowsUnsupportedError.StateError.start().ProviderFailure, followed by stream done, and every pending operation completes with that error.cancelled,unsupported_format,unauthorized,rate_limited(optionalretryAfterMs), andprovider_disconnected.addFrameis outstanding, and its future is the backpressure signal.ArgumentErrorsynchronously.Fake provider
test/support/fake_voice_provider.dartuses no timers and is driven entirely by the test. By default it enforces every rule above.holdStart,completeStart,failStartemitPartial,emitFinal(also while mute or finalize is pending)finalizepending, or callcompleteFinalize()with no finalsrejectFinalfail(error),disconnect()ignoreCancellation: true(off by default)holdFrames,releaseFrameThe fake records the terminal state before it notifies listeners, so a synchronous listener that reenters the session sees the ended session. Driver callbacks that arrive after the session ends add nothing.
The fake lives in test support next to the future coordinator tests. No public testing library is published until a second package needs it.
Tests
test/provider_test.dartadds 59 tests in 11 groups:Together they cover partials, multi-final assembly, finalization, duplicate finalize, cancellation, late events, and failure. They also cover these edge cases:
Audit
The first audit found five issues, and all five are fixed:
stop()future identityfinalize()future identityFor fixes 1–3, I checked that reverting each fix makes its new regression test fail. The re-audit passed with no findings.
Scope
spec/proto, conformance fixtures, other language SDKs, connectors, orpubspec.yamldependencies.provider.dartand fake, mapstart(format)tocreateSessionplusstart(), mapsetInputGatetomute/unmute, and handleProviderClosedasProviderFailureplus stream done.Validation
make check-dartequivalent indart:stable(Dart 3.13.3): format clean, analyzer clean, and all 78 tests pass (59 new, 19 existing)python3 tool/check_conformance.py: consistent (15 sets, 44 lines)git diff --checkmake check-protocoldid not run becauseprotocis not available locally. No proto file changed.Closes #7
🤖 Generated with Claude Code