# Speech: STT, TTS, audio backup

## Transcription (speech-to-text)

### Overview

`POST /api/transcribe` accepts `multipart/form-data` with a single audio
file (≤ 25 MB). Provider is `TRANSCRIBE_PROVIDER` env var, or auto-detected
(`google > aws > openai`) from available credentials. Each user may override
via `users.stt_model`; admin-wide default via `stt.model` in `app_settings`.

### Providers

| Provider | Transport | HIPAA (with BAA) |
|---|---|---|
| **Google Gemini** | Inline audio in `generateContent` call. Default model `gemini-2.0-flash`. | Yes |
| **Amazon Transcribe** | Streaming. `AWS_TRANSCRIBE_MEDICAL=true` + `AWS_TRANSCRIBE_SPECIALTY` switches to Transcribe Medical. Specialties: `PRIMARYCARE`, `CARDIOLOGY`, `NEUROLOGY`, `ONCOLOGY`, `RADIOLOGY`, `UROLOGY`. | Yes |
| **Local Whisper** | Spawns `whisper.cpp` or `faster-whisper` via `WHISPER_BINARY`. Fully offline. Model sizes `tiny`/`base`/`small`/`medium`/`large`. | N/A (nothing leaves host) |
| **OpenAI Whisper** | `whisper-1` via `/v1/audio/transcriptions`. Medical-context prompt prepended: `"Medical patient encounter. Pediatric."` | No |
| **LiteLLM** | Inline audio via LiteLLM's `chat.completions` endpoint (not the `/audio/transcriptions` path). Model from `LITELLM_STT_MODEL`. | Depends on LiteLLM backend |

## Browser Whisper (fully offline)

Runs entirely in the browser via WebAssembly. Zero network. Suitable when
no external transcription is acceptable.

- Runtime: `@xenova/transformers` (WASM).
- Models (bundled in the Docker image, no CDN fetch):
  - `whisper-tiny.en` — 39 MB
  - `whisper-base.en` — 74 MB
  - `whisper-small.en` — 244 MB
- Executes in a dedicated Web Worker; UI thread is never blocked.
- Models cached in IndexedDB after first load.
- Per-user toggle. On browser transcription failure, the client falls back to
  server-side transcription without user intervention.

## Live speech preview

Chrome / Edge `webkitSpeechRecognition` streams interim text to the UI during
recording. Used for real-time preview only — **not** for final transcription.
The actual transcript comes from the configured STT provider after recording
ends.

## Text-to-speech

### Overview

`POST /api/text-to-speech`. Returns `audio/mpeg`. `X-TTS-Provider` response
header identifies the provider used. 5000-character limit per request. Each
user may override via `users.tts_voice`; admin-wide default via `tts.voice`.

### Providers

| Provider | Notes |
|---|---|
| **Google Cloud TTS** | `@google-cloud/text-to-speech`. Voice families: Journey, Studio, Neural2. |
| **LiteLLM** | Configured via `LITELLM_TTS_MODEL` + `LITELLM_TTS_VOICE`. Backend-agnostic. |
| **ElevenLabs** | `eleven_turbo_v2_5`. **Not HIPAA-compliant**. |

## Audio backup

Raw audio is saved to Postgres **only when transcription fails**, providing a
retry window without persisting every recording.

### Storage

- Gzip-compressed, then AES-256-GCM encrypted (0x01 version byte prefix).
- `BYTEA` column in `audio_backups`.
- 24-hour `expires_at`, swept hourly.
- Legacy rows (gzip magic `0x1F` as first byte, no encryption envelope)
  decompress as-is — detection is deterministic because `0x1F ≠ 0x01`.

### Retry UI

Settings → Audio Backups:
- List: module, size, created, expiry.
- **Retry** — resubmits to `POST /api/transcribe`.
- **Delete** — purge now.

### Browser fallback

If the server-side save fails (network, 500, etc.), the client stores the audio
in IndexedDB so it can retry later. Cleared after successful submission.
