Text to speech (TTS)
TTS is entirely optional. It is off by default, consumes the same structured events described in structured-events.md, and never affects routing, fallback, or the returned ChatResponse, even if it fails.
The announcer
Every AIR instance has an announcer attribute (air.announcer.Announcer):
air.announcer.enable(events=["provider_switch", "all_exhausted"])Announcer.enable(events=None, backend=None):
events: a list of event type strings to speak. If omitted (None), all event types are enabled (PROVIDER_SWITCH,PROVIDER_EXHAUSTED,PROVIDER_RECOVERED,USAGE_THRESHOLD,ALL_EXHAUSTED,CAPACITY_RESTORED,CAPACITY_SKIPPED).backend: an object with aspeak(text: str) -> Nonemethod. If omitted,SystemTTSBackend()is constructed and used (see below).
Announcer.disable() turns announcements back off. Announcer.enabled, Announcer.events, and Announcer.backend are readable attributes reflecting current state; air.announcer.enabled is False until enable(...) is called.
Announced text is the event's message with the leading "[AIR]: " prefix stripped (air.announcer.to_speech), so TTS output does not include that console style prefix.
Default backend: SystemTTSBackend
from air.tts import SystemTTSBackendSystemTTSBackend() detects, at construction time, one of: macOS say, Linux espeak-ng, Linux espeak, or Windows (using System.Speech through PowerShell). If none of these is available, speak(...) is a safe no-op; SystemTTSBackend.available reports whether a backend was actually detected.
On Windows, text is passed to the PowerShell script through standard input, not embedded into the command line or script text, so quotes, apostrophes, and other shell-significant characters are handled safely. shell=True is never used on any platform.
SystemTTSBackend is a desktop convenience only. It is not designed for and does not work on mobile platforms (Android, iOS) or in a browser; those environments have no say/espeak/PowerShell available. It is also not useful in a typical server/headless environment. See "Headless and non desktop use" below.
Custom TTS backend
class MyBackend:
def speak(self, text: str) -> None:
... # send to Android TextToSpeech, iOS AVSpeechSynthesizer, Piper, Kokoro,
# a Web Speech API bridge, a cloud TTS service, or anything else
air.announcer.enable(events=["provider_switch"], backend=MyBackend())The only contract (air.tts.base.TTSBackend, a typing.Protocol) is a speak(text: str) -> None method. AIR core never imports or depends on any specific speech engine; SystemTTSBackend is just the one convenience implementation shipped alongside it.
How speak() is called
Announcer.announce(event_type, message) runs backend.speak(message) in a background thread (asyncio.to_thread), inside a try/except that discards any exception. This means:
- A raising or hanging
speak()implementation cannot raise an exception into yourchat(...)call. speak()does not block the event loop itself, though the current call tochat(...)does wait for the announcement dispatch to finish before returning, since announcements are dispatched synchronously as part of handling that call.
Headless and non desktop use
A backend/server application with no audio device, no terminal, and no GUI can simply never call air.announcer.enable(...). Routing, sessions, and structured events all work exactly the same either way; nothing about TTS is required for AIR to function. If a backend service wants to notify a browser or mobile client about routing events, the supported approach is subscribing with air.on_event(...) and forwarding the structured Event data yourself (see structured-events.md), not relying on SystemTTSBackend, which speaks through the local operating system AIR itself is running on.
Console formatting (separate utility)
air.console.format_error(error) and air.console.print_error(error) format a caught ProviderError as a plain, optionally colored "[AIR]: {message}" string, for developers who want to print an error nicely themselves. This is unrelated to the Announcer/TTS system and unrelated to the structured event system; it operates on exceptions you catch yourself, not on Event objects, and it is never called automatically by AIR.