Skip to content

Make speech (STT/TTS) provider-agnostic (SPEECH_PROVIDER, OpenAI-compatible backends)#12

Closed
Averroeskw wants to merge 2 commits into
MaxMaeder:masterfrom
Averroeskw:feat/speech-provider-abstraction
Closed

Make speech (STT/TTS) provider-agnostic (SPEECH_PROVIDER, OpenAI-compatible backends)#12
Averroeskw wants to merge 2 commits into
MaxMaeder:masterfrom
Averroeskw:feat/speech-provider-abstraction

Conversation

@Averroeskw

Copy link
Copy Markdown

Why

Speech is the last assistant-critical service that's hard-wired to specific vendors: TTS requires an Azure Speech key (MSFT_SPEECH_KEY) and STT a Groq key, with no way to point either at a different backend. Same single-vendor situation as search/weather before #8 — anyone without those exact keys (or running fully self-hosted) has no voice pipeline at all.

What

Speech is now provider-selectable via SPEECH_PROVIDER (server/src/services/speech/):

  • azure (default) — the stock pipeline, byte-for-byte unchanged: Azure Speech synthesis + Groq-hosted Whisper recognition. (Naming note: STT was already Groq, not Azure; "azure" here just means "the stock stack".)
  • openai — any OpenAI-compatible audio API via POST /audio/transcriptions (multipart) and POST /audio/speech: OpenAI itself, a LiteLLM proxy, self-hosted faster-whisper / Speaches, openedai-speech, etc. Configured with OPENAI_SPEECH_BASE, OPENAI_SPEECH_KEY (optional for keyless self-hosted servers), OPENAI_STT_MODEL (default whisper-1), OPENAI_TTS_MODEL (default tts-1), OPENAI_TTS_VOICE (default alloy).

If SPEECH_PROVIDER is unset, openai is auto-selected only when OPENAI_SPEECH_BASE is configured — otherwise behavior is exactly as today.

Audio formats (the honest part)

  • Azure TTS returns Ogg16Khz16BitMonoOpus, and the downstream pipeline depends on that: changeVolume / addBackgroundAudio force inputFormat("ogg") before the audio reaches the device.
  • The OpenAI provider therefore requests response_format: "opus" (Ogg/Opus, 48 kHz) and transcodes to Ogg 16 kHz mono with the ffmpeg toolchain the server already uses (new transcodeToOgg helper in services/audio.ts), so everything downstream sees the same profile regardless of provider. One extra ffmpeg pass per response; no client/device changes.
  • recognize() interface unchanged (Buffer in → transcript out); the device's Ogg capture buffer is uploaded as audio.ogg, which Whisper-style endpoints accept natively. The silence gate (WHISPER_MIN_DB) stays provider-independent.
  • The dashboard voice picker maps to Azure voice names, so it only applies to the azure provider; the openai provider takes its voice from OPENAI_TTS_VOICE. speed carries over cleanly (both are 1.0-based multipliers, clamped to OpenAI's 0.25–4 range). The translate feature's language-identifying STT (Google Speech v2) is a separate code path and is intentionally left untouched.

Compatibility & verification

  • Azure/Groq deployments: zero behavior change (default provider, code path untouched).
  • .env.template documents the new block and fixes the stale SPEECH_KEY name — the code reads MSFT_SPEECH_KEY.
  • tsc --noEmit clean; new files lint clean.
  • Smoke-tested both directions against an OpenAI-compatible stub: multipart STT (auth header, model field, filename) and TTS request shape verified, output confirmed Ogg / 16 kHz / mono via ffprobe and accepted by the existing changeVolume path.

🤖 Generated with Claude Code

Averroeskw and others added 2 commits July 2, 2026 22:37
Speech was the last hard-wired third-party dependency: TTS requires an
Azure Speech key and STT a Groq key, with no way to point either at a
different (e.g. self-hosted) backend.

Following the same pattern as the search/weather providers:

- SPEECH_PROVIDER = "azure" (stock: Azure TTS + Groq Whisper STT,
  default) | "openai" (any OpenAI-compatible audio API: OpenAI, LiteLLM,
  faster-whisper, openedai-speech, ...)
- Auto-detects "openai" when OPENAI_SPEECH_BASE is configured and
  SPEECH_PROVIDER is unset; otherwise stock behavior is unchanged
- OpenAI TTS output is transcoded to Ogg 16 kHz mono so the downstream
  ffmpeg pipeline and device see the same audio profile Azure produces
- .env.template documents the new variables and fixes the stale
  SPEECH_KEY name (the code reads MSFT_SPEECH_KEY)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…on output-stream end to avoid truncated audio

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Averroeskw

Copy link
Copy Markdown
Author

Closing for now — pulling this back until I've verified the full stack end-to-end on the physical device (interposer inbound). Will reopen once it's hardware-verified. The branch lives on in my fork history if anyone needs the diff in the meantime.

@Averroeskw Averroeskw closed this Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant