xr_ai_voicegate#

Public surface of the xr-ai-voicegate package.

Classes#

AudioSink

Consumer-supplied return-audio writer.

TTSLike

Duck-typed text-to-speech client used for say_stop_ack.

VoiceGateConfig

Voice-gate behavior settings.

VoiceGate

Speech-input gating state machine.

Functions#

load_voice_gate_config(→ VoiceGateConfig)

Load + parse a voice_gate YAML file into a VoiceGateConfig.

Package Contents#

class xr_ai_voicegate.AudioSink#

Consumer-supplied return-audio writer.

async play_wav(pid: str, wav_bytes: bytes) → None#

Play WAV bytes for participant pid.

class xr_ai_voicegate.TTSLike#

Duck-typed text-to-speech client used for say_stop_ack.

async synthesize(text: str) → bytes#

Synthesize text and return WAV bytes.

class xr_ai_voicegate.VoiceGateConfig#

Voice-gate behavior settings.

magic_phrases: tuple[str, Ellipsis] = ()#

Sentence-boundary opt-in phrases.

A match is valid at transcript start or after ., ?, or !. An empty tuple dispatches every non-STOP STT transcript, or every transcript when stop_commands_enabled is false.

followup_grace_s: float = 5.0#

Seconds in which the next utterance may begin without another phrase.

The utterance may finish after the grace period.

listening_chime: bool = True#

Whether phrase matches may emit a listening chime.

The chime is available only when magic_phrases is non-empty and plays when the consumer calls VoiceGate.play_chime. It defaults to true; set listening_chime: false to disable it.

stop_commands_enabled: bool = True#

Whether STOP recognition interrupts speech or invokes the stop handler.

Applies to early probes, streaming partials, and final transcripts, including conversations without a wake phrase. Disable for passive narration: STOP words then follow normal transcript and query gating without a STOP interruption or acknowledgement. Probes may remain enabled for wake acknowledgements. This does not disable other conversation controls or speech-start events. Scoped application commands such as “stop recording” remain queries.

xr_ai_voicegate.load_voice_gate_config(path: pathlib.Path) → VoiceGateConfig#

Load + parse a voice_gate YAML file into a VoiceGateConfig.

Schema: a top-level mapping with mode (always_on, wake_only, conversation or hybrid), magic_phrases (list[str] or bare str), listening_chime (bool), followup_grace_s (float), stop_commands_enabled (bool, default true). Optional conversation and speaker mappings configure session controls and speech-backend selection for the unified voice pipeline. Their settings remain private to that path. The package’s annotated voice_gate.yaml supplies defaults; the selected file overrides individual top-level and section fields. Missing or empty files use the packaged always_on baseline. Mode chooses activation; the optional mappings tune controls and choose ASR independently. Addressed modes require non-empty phrases. Current mode-less configurations normalize once using their conversation and phrase settings; explicit mode must not be mixed with conversation.enabled. magic_phrases: null and trailing whitespace are normalized before validation.

class xr_ai_voicegate.VoiceGate(
cfg: xr_ai_voicegate.VoiceGateConfig,
*,
audio_sink: xr_ai_voicegate.AudioSink,
tts: xr_ai_voicegate.TTSLike,
)#

Speech-input gating state machine.

Event ladder in feed — exactly one event fires per call, in this deterministic priority order:

  1. STOP enabled and detected on either raw text or the magic-phrase-stripped tail → on_stop(pid); closes the follow-up window.

  2. Magic phrase matched AND query non-empty → on_query(pid, query, fresh_match=True); closes the follow-up window.

  3. Follow-up window still open (and not STOP) → on_query(pid, raw_text, fresh_match=False); closes the follow-up window. fresh_match distinguishes this continuation from case 2 so consumers can suppress one-shot side effects (e.g. the listening chime) on the follow-up dispatch.

  4. Magic phrase matched AND query empty → on_phrase_only(pid); opens the follow-up window.

  5. Otherwise → on_drop(pid, raw_text); closes the window defensively.

When magic_phrases is empty the gate is in always-on mode: STOP takes priority when enabled, and every other utterance dispatches straight to on_query with fresh_match=True. The follow-up / phrase-only / drop branches are inert in this mode — they only make sense once a phrase exists to gate against.

STOP is a closed imperative grammar: direct requests such as stop, stop it, stop talking, be quiet, and shut up may use a bounded set of conversational prefixes and punctuation. Negations, questions, reported speech, unconfigured arbitrary prefixes, and scoped or multi-action commands such as stop monitoring are ordinary queries, not global stops.

Set stop_commands_enabled=False for passive transcription. In that mode neither partial nor final transcripts trigger STOP; their words instead follow the ordinary query-gating rules.

Handler exceptions are logged and swallowed so one bad handler does not kill the gate.

on_query(h: Callable[[str, str, bool], Awaitable[None]]) → None#

Register the handler for accepted user queries.

on_stop(h: Callable[[str], Awaitable[None]]) → None#

Register the handler for stop commands.

on_phrase_only(h: Callable[[str], Awaitable[None]]) → None#

Register the handler for a wake phrase without a query payload.

on_drop(h: Callable[[str, str], Awaitable[None]]) → None#

Register the handler for transcripts rejected by the gate.

on_participant_joined(h: Callable[[str], Awaitable[None]]) → None#

Register the handler for participant-joined notifications.

bind(
*,
on_query: Callable[[str, str, bool], Awaitable[None]],
on_stop: Callable[[str], Awaitable[None]],
on_phrase_only: Callable[[str], Awaitable[None]] | None = None,
on_drop: Callable[[str, str], Awaitable[None]] | None = None,
on_participant_joined: Callable[[str], Awaitable[None]] | None = None,
) → None#

Register every handler in one call.

Equivalent to calling the individual on_* setters; offered as a single bind point for external consumers that have all handlers in hand. on_query and on_stop are required because the gate is useless without them; the others are optional and default to no-op when unset.

async participant_joined(pid: str) → None#

Notify the registered handler that participant pid joined.

forget(pid: str) → None#

Discard all gate state for participant pid.

begin_utterance(pid: str) → None#

Remember when a follow-up starts before its transcript is ready.

property wake_ack_enabled: bool#

Whether configured wake phrases should produce a listening chime.

matches_magic_phrase(text: str) → bool#

Return whether text has a phrase at a sentence boundary.

could_match_magic_phrase(text: str) → bool#

Return whether the current partial sentence can become a match.

async feed(pid: str, text: str) → None#

Run one transcript through the event ladder; fires exactly one event.

Not re-entrant per-pid: the follow-up window (_followup_until) is read-then-mutated without locking, so two concurrent feed calls for the same pid can race the window state. Consumers must serialize calls per participant (e.g. a per-pid transcribing flag).

async play_chime(pid: str) → bool#

Emit the listening chime on the consumer’s audio sink.

No-op when the chime is disabled or has not been built yet (the chime is lazily synthesized to match the TTS sample rate the first time observe_tts_wav is called). Returns whether audio was emitted so callers can avoid suppressing a later fallback chime.

format_phrase_help() → str | None#

Return a sentence fragment telling the user how to address the agent given the configured phrases. None when no phrases are configured (the caller picks a generic greeting). The wording carries over from the original _greet implementation.

async say_stop_ack(pid: str) → None#

Synthesize a short canned stop acknowledgement and play it on the consumer’s audio sink. Also observes the WAV so the lazy chime build can pick up the sample rate from this path.

observe_tts_wav(wav_bytes: bytes) → None#

Build the chime at the TTS sample rate the first time a real TTS WAV passes through. No-op once built or when chime is disabled. A malformed WAV header disables the chime so the consumer doesn’t keep paying for failed builds.