> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vexa.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Use a custom STT endpoint

> Point Vexa at any OpenAI-compatible speech-to-text service, including self-hosted FunASR or SenseVoice gateways.

Vexa sends meeting audio to one OpenAI-compatible speech-to-text endpoint. The endpoint can be the
bundled faster-whisper service, Vexa's hosted transcription service, or a self-hosted gateway that
implements the same request shape.

Use this path when you want a different ASR model without changing the bot, meeting-api, or live
transcript pipeline.

**Don't have a key yet?** Hosted: sign in at [vexa.ai/signin](https://vexa.ai/signin) with a Google
account and copy your key from [your account page](https://vexa.ai/account) — free credit, no card
required. Self-hosted: `make all` prints a key when the stack comes up.

To transcribe **later** instead — or on hardware Vexa never talks to — send the bot with
transcription off and collect the audio afterwards:
[Capture now, transcribe later](/how-to/deferred-transcription).

## Contract

Vexa appends `/v1/audio/transcriptions` to `TRANSCRIPTION_SERVICE_URL` and sends multipart form data:

| Field             | Required | Notes                                                        |
| ----------------- | -------: | ------------------------------------------------------------ |
| `file`            |      yes | Audio window captured by the bot.                            |
| `model`           |      yes | Comes from `TRANSCRIPTION_MODEL`, or `whisper-1` when unset. |
| `language`        |       no | Present when the bot request pins a language.                |
| `response_format` |       no | Backends may return plain JSON or verbose JSON.              |

The minimum response Vexa needs is:

```json theme={null}
{ "text": "transcribed speech" }
```

Backends may include segments, words, timestamps, duration, or usage metadata. Keep those fields
additive: the meeting pipeline must still work when only `text` is present.

## Configure Vexa

Set these variables in `deploy/compose/.env`:

```bash theme={null}
TRANSCRIPTION_SERVICE_URL=http://<stt-host>:8000
TRANSCRIPTION_SERVICE_TOKEN=<token-if-required>
TRANSCRIPTION_MODEL=<model-id-your-backend-validates>
```

`TRANSCRIPTION_SERVICE_URL` is the base URL. Do not include `/v1/audio/transcriptions`; Vexa appends
that path.

If the backend ignores the `model` form part, leave `TRANSCRIPTION_MODEL` unset. If it validates model
ids, set the exact served name.

## Before you swap: the bundled model has a knob

The bundled faster-whisper service chooses its model from `MODEL_SIZE`, which defaults to
**`large-v3-turbo`** (`core/meetings/services/transcription/src/transcription/main.py`). Turbo is a
faster, lighter distillation of `large-v3` — it trades some accuracy for speed and VRAM, and how much
depends on the language. If accuracy on yours matters more than latency, change one variable before
you go looking for a different engine:

```bash theme={null}
MODEL_SIZE=large-v3
```

Set it on the **transcription unit** (`deploy/transcription/.env`), not on the main stack. That unit
ignores the `model` form part a caller sends and serves whatever `MODEL_SIZE` names, so
`TRANSCRIPTION_MODEL` has no effect on it — `TRANSCRIPTION_MODEL` exists for backends that *validate*
a model id. See `deploy/transcription/README.md`.

## Example: FunASR or SenseVoice

<Note>
  **Client path validated upstream; complete meeting deployment still needs a witness.** The FunASR
  maintainers ran Vexa's own `TranscriptionClient` against the FunASR 1.3.26 OpenAI-compatible server
  with SenseVoice on CPU ([#928](https://github.com/Vexa-ai/vexa/pull/928)). Chinese, English,
  Cantonese, Japanese, Korean, and concatenated Chinese-English samples all produced non-empty
  transcripts, verbose segments, and duration metadata. **No Vexa maintainer has reproduced this** — a
  bot-in-meeting run and the `platform_settings` override path remain unwitnessed on either side, and
  are tracked in [#863](https://github.com/Vexa-ai/vexa/issues/863).
</Note>

A self-hosted FunASR/SenseVoice gateway is a **working example of this contract**: a real non-Vexa
backend that has been run against Vexa's own client and answered with non-empty verbose responses.
That is a claim about **portability**, not about quality. **Which model transcribes your languages
best is your decision — Vexa does not benchmark third-party models.** Measure your own audio before
you switch.

Run or deploy a gateway that accepts:

```bash theme={null}
curl -sS http://<stt-host>:8000/v1/audio/transcriptions \
  -H "Authorization: Bearer <token-if-required>" \
  -F file=@sample.wav \
  -F model=sensevoice \
  -F language=zh
```

Then point Vexa at the same host:

```bash theme={null}
TRANSCRIPTION_SERVICE_URL=http://<stt-host>:8000
TRANSCRIPTION_SERVICE_TOKEN=<token-if-required>
TRANSCRIPTION_MODEL=sensevoice
```

The official `funasr-server --model sensevoice` exposes `sensevoice` as its served model id. A Hub
checkpoint path such as `FunAudioLLM/SenseVoiceSmall` is not automatically an API model id; verify
the names exposed by your gateway's `GET /v1/models` route. The official Fun-ASR-Nano server uses
`fun-asr-nano`. Other gateways may expose different names.

<Warning>
  FunASR 1.3.26 transcribes all five languages correctly, but when `language` is omitted it reports
  `language: "zh"` for every verbose response. Pin the meeting language when accurate metadata matters.
  The upstream fix is tracked in
  [modelscope/FunASR#3400](https://github.com/modelscope/FunASR/pull/3400).
</Warning>

<Note>
  Vexa treats the STT service as a network dependency. Put the ASR runtime on a GPU box if needed, and
  keep the main stack CPU-only.
</Note>

## Preflight before a meeting

Test the endpoint from the Vexa host before sending a bot:

```bash theme={null}
curl -sS "$TRANSCRIPTION_SERVICE_URL/v1/audio/transcriptions" \
  -H "Authorization: Bearer $TRANSCRIPTION_SERVICE_TOKEN" \
  -F file=@sample.wav \
  -F model="${TRANSCRIPTION_MODEL:-whisper-1}"
```

Accept the endpoint only if it returns HTTP 2xx and a non-empty `text` field. A 404 usually means the
base URL already included `/v1` or `/v1/audio/transcriptions`. A 401 or 403 means the token is missing
or wrong. A `model_not_found` response means `TRANSCRIPTION_MODEL` does not match the served model id.

## Keep provider errors attributable

When adding or operating a custom STT backend:

* Preserve the raw HTTP status in logs.
* Keep provider error messages sanitized, but do not collapse every failure into "transcription failed".
* Separate route errors, auth errors, model-id errors, and audio-format errors.
* Keep long-audio chunking on the backend side if the model has a fixed window limit.

That makes a bad URL, expired token, unsupported model id, or model-specific audio limit diagnosable
without changing the meeting bot.
