Repository navigation
feat(protocol): add engine, TTS, wake, speaker, and batch contracts - #56
Pushkraj-Space wants to merge 1 commit into
Conversation
Extend murmur.v1 to protocol 1.1 with the wire contracts a production
desktop voice runtime needs. This is an additive v1 change. SDK runtime
APIs and engine implementations remain out of scope.
Engine-scoped concepts can exist before, after, or across capture
sessions, so they cannot live in the session envelopes. A new
EngineEvent/EngineControl pair carries them. It mirrors
RuntimeEvent/SessionControl, but engine_id is the routing key and the
ordering scope. It covers:
- engine identity, locality, platform, readiness, and open string
capability tokens (engine.proto)
- voice-pack snapshots with components, byte progress, and typed
errors, plus prepare/retry/repair requests (voice_pack.proto)
- speech output requests, cancellation, and lifecycle
(speech_output.proto)
- microphone permission, route, interruption, and device readiness
per source
Session-scoped concepts extend the existing envelopes:
- StartSession.engine_id binds a session to an engine. Unbound
sessions keep unchanged v1 behavior.
- SetSpeakerVerification sets the mode (enforce, disable, or
per-utterance bypass). Transcript.speaker_verification reports the
outcome. A speaker-rejected final reuses TRANSCRIPT_KIND_REJECTED, and
a per-message kind/result table keeps a rejection off accepted
finals.
- StartBatchTranscription runs a batch through the same session
mechanics: AudioFrame input, finalize to end input, stop to cancel,
BatchProgress, and Transcript.words. Word offsets are uint32
milliseconds on the submitted-media timeline.
- ProviderStatus gives every provider session, streaming or batch,
exactly one terminal state (FINALIZED, CANCELLED, or FAILED).
- InputGateStatus reports the effective input gate as a set of
closing causes, so half-duplex speech output and its echo tail can
never reopen input held by another cause.
- WakePhraseEvent reports wake detection, activation, and dismissal.
Command rejection has one representation. A MurmurError on the stream's
generic error arm carries metadata["request_sequence"]. It is distinct
from operation outcomes, where FAILED requires an error and other
states forbid one. spec/README.md documents the capability gating
table, error-code registry, half-duplex rules, batch time base,
evolution rules, and exclusions (product commands, entitlements,
analytics, UI state, settings, credentials, device names, paths).
Conformance moves to protocol {1, 1} with 38 new fixture sets (16
accepted, 22 invalid) and six new rejection reasons. The checker and
the Dart, TypeScript, Python, and Rust SDKs enforce one identical
single-message validation profile. Required discriminators reject
*_UNSPECIFIED. Optional enums treat an explicit *_UNSPECIFIED as
absent. Unknown capability tokens round-trip. Each SDK learns the
engine envelopes and bumps its current protocol constant. Dart adds
typed SetSpeakerVerification, StartBatchTranscription, and
EngineControl commands, and a new EngineEvent model.
The checker also stops crashing on non-string enum values.
Closes october-dev#40
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
harshsaver
left a comment
There was a problem hiding this comment.
Solid, well-specified change against #40: numbering only appends (new oneof arms 18–21 and 14–15), codes are snake_case, no new dependencies, and protoc, conformance (53 sets, 246 lines), Python, Dart and TypeScript all pass locally. I spot-checked the speaker kind/result table across the checker and all four SDKs and they match.
Blocking (you need to rebase for the #55 CHANGELOG conflict anyway, so please fold these in so the two layers land consistent):
- Graceful finalization: baseline or capability?
spec/README.md:81andengine.proto:43add agraceful_finalizationtoken that makesfinalizeoptional per engine, but #55's provider contract (now merged) makes finalize baseline, anddocs/voice-runtime.mdinvariant 3 ("Keep the tail") requires it. Suggest dropping the token and making finalize part ofstreaming_transcription. - One error-code registry.
spec/README.md:129says "new contracts use these", but omits codes already in use:cancelled(runtime.dart), plusunsupported_format,unauthorized,rate_limitedandprovider_disconnected(#55'sprovider.dart:233). Please make this the single registry covering all stable codes.
Non-blocking
3. spec/README.md:206-207: "fail-open"/"fail-closed" decide where UNCERTAIN lands, but nothing exposes which policy an engine uses. Either say it's engine-defined and hosts must handle both, or drop the policy wording.
4. session.proto:53 / :103: engine_id lives on both StartSession and StartBatchTranscription, and format duplicates requested_format. Fine as is; one sentence saying a batch session never sends start would help readers.
5. spec/README.md:28: the 1.0 -> 1.1 bump rewrote every accepted fixture's minor, so no accepted fixture now proves a 1.1 reader accepts a minor-0 message. Keep one minor-0 accepted line, or stay on 1.0 until release.
6. Growable sets now use string tokens while VoiceSource.capabilities stays a closed enum. Pre-release, consider converging on one approach now.
Summary
Extends
murmur.v1to protocol 1.1 with the engine, voice-pack, speech-output (TTS), microphone, provider-lifecycle, input-gate (half duplex), wake-phrase, speaker-verification, and batch-transcription contracts that a production desktop voice runtime needs.The change is wire behavior only, and every existing v1 field, enum value, and meaning is unchanged. Engine implementations, SDK runtime APIs, default models, and product settings are out of scope.
Closes #40.
Design
Scopes. Engine readiness, voice-pack preparation, microphone permission, and speech output can exist outside any capture session, so they cannot honestly live in
RuntimeEvent/SessionControl, which require a non-emptysession_id. One new envelope pair carries them. It mirrors the session pair field for field: protocol, routing id, ordering key, and time.EngineEventengine_idsequenceengine_status,voice_pack_status,microphone_status,speech_output,errorEngineControlengine_idrequest_sequencevoice_pack,speak,cancel_speechRuntimeEvent(extended)session_idsequenceprovider_status,input_gate_status,wake_phrase,batch_progressSessionControl(extended)session_idrequest_sequencespeaker_verification,start_batchCapability gating. Capabilities are gated by open string tokens rather than simulated:
EngineDescriptor.capabilitiesuses documented tokens:streaming_transcription,graceful_finalization,batch_transcription,word_timings,speech_output,echo_cancellation,wake_phrase,speaker_verification, andvoice_packs.engine_idis gated by that engine's capabilities.Rejection vs outcome.
MurmurErroron the stream's genericerrorarm. It carriesmetadata["request_sequence"]and, forcapability_unsupported,metadata["capability"].FINALIZED,CANCELLED, orFAILEDCOMPLETED,CANCELLED, orFAILEDFAILEDrequires an error, and other states forbid one. The one exception is an engine that isNOT_READY, which may carry an error as its reason.MurmurError.codevalues.Half duplex and echo clearance.
InputGateStatus.closed_byis the set of active causes:HOST,FINALIZATION,SPEECH_OUTPUT,ECHO_CLEARANCE, andINTERRUPTION. An empty set means open.ECHO_CLEARING.Speaker rejection cannot be mistaken for acceptance.
TRANSCRIPT_KIND_REJECTED, withspeaker_verificationset toREJECTEDorUNCERTAIN(fail-closed).FINAL.ENFORCED.Batch.
start_batch→AudioFrames →finalize→batch_progressandFINALtranscripts withwords→provider_status FINALIZED.stopcancels.[start, end).frame_duration_msis required for Opus.Excluded from the protocol: product commands, UI state, entitlement or trial fields, analytics, model or provider choice, product settings (including wake-phrase configuration), credentials, device names, and paths or URLs.
Implementation
engine.proto,voice_pack.proto, andspeech_output.proto.session.protoaddsStartSession.engine_id = 4,SetSpeakerVerification(arm 14), andStartBatchTranscription(arm 15).events.protoadds arms 18–21,Transcript.words = 5,Transcript.speaker_verification = 6, and a doc comment onTRANSCRIPT_KIND_REJECTED(not deprecated).spec/README.mdcovers the version, scope tables, capability registry, rejection and outcome rules, error codes, voice packs, the input gate, microphones, speaker verification, wake phrases, batch, evolution rules, and exclusions.conformance/README.mdgains the new messages, reasons, and the full validation table, including the verbatim kind/result table.{1, 1}. Existing accepted fixtures changed only their minor version, and the forward-compatible sets moved to minor 2.missing-engine-command,ambiguous-engine-command,invalid-identifier,invalid-progress,invalid-word-timing, andinvalid-outcome.tool/check_conformance.py):engineIddiagnosed asinvalid-protocol-versionjust likesessionId.speak.textandwords[].text.EngineEvent/EngineControl, and bumps its current protocol to 1.1.SetSpeakerVerification,StartBatchTranscription,StartSession.engineId, andEngineControlwithVoicePackRequest,SpeakRequest, andCancelSpeech.EngineEventmodel.validation.dart.parseEngineEvent/engineEventToJsonandparseEngineControl/engineControlToJson.EngineEvent,EngineControl, and their kind enums.EngineEvent/EnginePayloadandEngineControl/EngineCommand.Notes for reviewers
SessionCommandorRuntimePayloadKindmust handle the new cases, as the CHANGELOG notes.engineStatusforbids an error onPREPARING/READY, allows one onNOT_READY, and requires one onFAILED.words, and therequest_sequencekey inside the opaqueMurmurErrorbody are documented and covered by accepted fixtures, not validated.engine-command-rejectionset with a limited engine, becauseengine-eventsadvertises every capability.Tests
make check-protocolinvocation)make check-conformancemake check-pythonmake check-typescript(tsc --noEmit+ tests)make check-dart(dart format --set-exit-if-changed,dart analyze,dart test, includingruntime_test.dartafter the 1.1 bump)make check-rustcheck-flutterandcheck-flutter-plugin(analyze + app tests)Differential fuzzing. The SDK runners assert only that a line is rejected, not why, so the change was also fuzzed outside the repo:
nullfor new optional fields.nullfor existingVoiceSourcelists.Dart, Rust, and Flutter checks ran in containers locally. CI pins protoc 29.x, Python 3.13, and Flutter 3.35.x, and the change uses nothing newer.
Audit result
Verdict: PASS. The independent code audit found no blocking or non-blocking defects. It compared the full diff against the issue and plan:
It inspected every new fixture set, the manifest dispatch, exact checker diagnoses, ordering and routing, unknown-field handling, and four-SDK round-trips. It also ran 17,440 mutations against the checker and the Python SDK with zero verdict divergences, and
git diff --checkpassed.Acceptance criteria
murmur.v1, with documented field semantics and evolution rules.🤖 Generated with Claude Code