* feat(settings): run local VocaGateway from Speech Engine (podman-first) Add an optional Local VocaGateway panel next to Remote Server so Linux users can start the pinned v0.1.0 compose project with podman (docker fallback), watch Live/Pairable/Ready status, pair a phone without loopback QR URLs, and preset remote_api to the gateway transcriptions endpoint. Stop preserves volumes; Flatpak fails closed; tray offers Stop only when this session manages the gateway. Default on-device engine stays whisper.cpp. * fix(gateway-embed): drop nonexistent compose cpu profile VocaGateway v0.1.0 exposes default service gateway plus native/cuda/vulkan profiles, not cpu. Start and stop the default service without --profile, keep the latest image pin rejection, and prefer save_config for the remote_api preset. * fix(gateway-embed): keep compose --profile cpu per embed contract v0.1.0 has no cpu profile entry, but Compose still starts the unprofiled gateway service when --profile cpu is set, and keeps cuda/vulkan/native off. That matches the frozen desktop embed contract and stays forward-compatible. * fix(gateway-embed): harden embed path for settings and compose Keep podman/docker probing off the GTK thread, reject link-local and loopback public URLs for phone QR, sanitize compose .env image overrides, and cap bootstrap token reads so a hostile file cannot blow memory. * fix(gateway-embed): keep LAN toggle and phone QR in sync When Allow LAN flips while compose is already managed, rewrite publish host and PUBLIC_URL and recreate the gateway before Pairable comes back. Also reject default docker/podman bridge addresses for phone QR. * test(gateway-embed): pin bridge and LAN-gate pairing cases Cover 172.17.0.2 / link-local not becoming Pairable, and the mid-run LAN toggle path that must wait for compose republish before advertising a phone URL. * style(gateway-embed): black format embed modules * fix(gateway-embed): reject short-form loopback and localhost.* for QR Align Pairable/QR host checks with vocagateway desktop-embed policy so 127.1 / localhost.* never become phone-reachable display URLs. * fix(gateway-embed): detach Settings status listener on destroy Gateway poll callbacks were left registered after Settings closed, so idle_add could touch destroyed GTK widgets while compose stayed up. * fix(gateway-embed): pass host_env to bridge ip probe _bridge_iface_ips shells out to host ip; without host_env the AST gate fails and AppImage bundles can hand the wrong GLib to ip. * fix(packaging): ship gateway_embed in the AppImage The explicit setuptools packages list omitted the new package, so the bundle booted from source trees but died with ModuleNotFoundError on every distro boot test. * style(packaging): black the gateway_embed package guard * fix(settings): pack local VocaGateway into the Advanced island The rebase onto main auto-merged textually onto the simple Speech Model page. Keep the gateway card on advanced_box, next to Remote Server. * fix(gateway-embed): treat missing Compose as unavailable When podman or docker is present but Compose is not, stop advertising Run. Return empty compose_args, show an install-Compose hint, and cover it with a unit test. Also add module loggers on the new gateway_embed modules and annotate the gateway Settings/tray callbacks Greptile flagged. just lock found no lockfile drift. * fix(gateway-embed): keep Allow LAN bind matched to the switch Snapshot the LAN flag for start/republish, record what compose actually got, then republish again if the switch moved mid-flight so 0.0.0.0 never lingers while Allow LAN reads off. * fix(gateway-embed): detect leftover compose after Quit Quit does not stop compose, so a LAN-published gateway can keep running. Settings and the tray treated that as Stopped because managed_by_us is only process memory. Probe compose after runtime detection, show Stop when it is actually up, and let Stop work even if this session did not start it. * fix(gateway-embed): restore LAN switch from leftover compose Quit leaves compose running, so the next launch now reads VOCAGATEWAY_PUBLISH_HOST from the embed .env, adopts the project as managed, and keeps Allow LAN honest so republish can close 0.0.0.0. * fix(gateway-embed): restore .env if LAN republish fails compose recreate can fail after write_env_file already pointed VOCAGATEWAY_PUBLISH_HOST at loopback. Restore the previous .env so orphan adopt still sees a live 0.0.0.0 bind, and snap Allow LAN back to the running publish mode. * style(gateway-embed): type helper args and black MagicMock * refactor(types): move Readable protocol to common_types AGENTS.md keeps Protocol interfaces in common_types. Pairing now imports the shared Readable surface instead of a local _Readable helper. * fix(gateway-embed): satisfy mypy on rebased embed code * fix(audio): keep duck restore point when a set reports failure
283 lines
11 KiB
Markdown
283 lines
11 KiB
Markdown
# Remote HTTP transcription
|
|
|
|
Vocalinux can offload speech recognition to a remote HTTP server instead of running a local model. Use this when you want faster transcription on a more powerful machine, to share one model across clients, or to keep a laptop free of GPU/model overhead.
|
|
|
|
Vocalinux speaks two wire formats out of the box:
|
|
|
|
- **OpenAI-compatible**: `POST /v1/audio/transcriptions` (e.g. OpenAI, [Speaches](https://github.com/speaches-ai/speaches), LocalAI, FunASR/SenseVoice)
|
|
- **whisper.cpp server**: `POST /inference` (the binary shipped with [whisper.cpp](https://github.com/ggerganov/whisper.cpp))
|
|
|
|
Pick whichever your server exposes. The rest of this guide applies to both.
|
|
|
|
> **Tip: share the server with your phone.** These HTTP formats are open enough that the same self-hosted server can also back mobile dictation apps. On Android, apps like *Dictate* and *Transcribro* speak OpenAI-compatible Whisper; on iOS, Shortcuts-based dictation clients can hit the same endpoint. Run one Whisper server, point your laptop and phone at it, and you get consistent dictation everywhere without uploading audio to a third party.
|
|
|
|
## Local VocaGateway (optional)
|
|
|
|
On Linux you can start an optional local [VocaGateway](https://github.com/VocaHQ/vocagateway) from **Settings → Speech Model → Advanced** (podman-first, docker fallback). That path uses the same Remote API settings described below, with endpoint `/v1/audio/transcriptions`. It is **not** on-device recognition: audio goes to the gateway container on your machine. Details: [GATEWAY_EMBED.md](GATEWAY_EMBED.md).
|
|
|
|
## How it works
|
|
|
|
When the **Remote API** engine is active, Vocalinux:
|
|
|
|
1. Captures microphone audio locally (16 kHz, mono, 16-bit PCM).
|
|
2. On end-of-utterance, packages the buffer as a WAV file in memory.
|
|
3. Uploads it using the configured HTTP format.
|
|
4. Reads the transcribed text from the JSON response and types it into the focused window.
|
|
|
|
No audio is written to disk. VAD and segmentation stay local; only the heavy ASR step is remote.
|
|
|
|
## Server-side setup
|
|
|
|
You need a server that accepts audio uploads and returns text. Three common setups:
|
|
|
|
### Option A: whisper.cpp server
|
|
|
|
Build and run the bundled HTTP server from the [whisper.cpp](https://github.com/ggerganov/whisper.cpp) repo:
|
|
|
|
```bash
|
|
git clone https://github.com/ggerganov/whisper.cpp
|
|
cd whisper.cpp
|
|
make server
|
|
./models/download-ggml-model.sh medium.en
|
|
./build/bin/whisper-server \
|
|
--model models/ggml-medium.en.bin \
|
|
--host 0.0.0.0 \
|
|
--port 8080
|
|
```
|
|
|
|
The server now listens on `http://<server-ip>:8080`. The endpoint is `/inference`.
|
|
|
|
### Option B: OpenAI-compatible server (Speaches, LocalAI, etc.)
|
|
|
|
Run any server that implements `POST /v1/audio/transcriptions`. Example with [Speaches](https://github.com/speaches-ai/speaches):
|
|
|
|
```bash
|
|
docker run --rm -p 8000:8000 ghcr.io/speaches-ai/speaches:latest
|
|
```
|
|
|
|
The endpoint is `/v1/audio/transcriptions`.
|
|
|
|
### Option C: FunASR/SenseVoice local server
|
|
|
|
[FunASR](https://github.com/modelscope/FunASR) can run a local
|
|
OpenAI-compatible transcription service, which keeps audio on your own machine
|
|
or LAN while letting Vocalinux use SenseVoice or Paraformer-family models:
|
|
|
|
```bash
|
|
pip install 'torch>=2.13.0' torchaudio
|
|
pip install funasr vllm fastapi uvicorn python-multipart
|
|
funasr-server --device cuda
|
|
```
|
|
|
|
The server listens on `http://localhost:8000` by default and exposes
|
|
`POST /v1/audio/transcriptions`. In Vocalinux, set:
|
|
|
|
- **Server URL**: `http://localhost:8000`
|
|
- **API Endpoint**: `OpenAI/FunASR (/v1/audio/transcriptions)`
|
|
- **Model**: `sensevoice`
|
|
|
|
Use `--device cpu` for CPU-only testing if your FunASR install supports it. A
|
|
remote GPU host works too: bind the server to the LAN interface, open the port,
|
|
and point Vocalinux at that host.
|
|
|
|
### Network checklist
|
|
|
|
- The server must be reachable from the client over HTTP or HTTPS.
|
|
- If you bind to `0.0.0.0`, open the port in your firewall (`ufw allow 8080/tcp`, etc.).
|
|
- For anything outside a trusted LAN, terminate TLS in front of the server (Caddy, nginx, Traefik) and require an API key. The audio you upload is private speech.
|
|
- If you also use the server from a mobile dictation app, terminate TLS even on a LAN. Most Android and iOS apps refuse cleartext HTTP.
|
|
|
|
## Client-side setup (Vocalinux)
|
|
|
|
### Via the Settings dialog
|
|
|
|
The remote engine is a power-user option in the **Speech Engine** settings.
|
|
|
|
1. Launch Vocalinux and open **Settings → Speech Engine → Remote Server**.
|
|
2. Toggle **Use remote server** on. This overrides whatever local engine is selected above it.
|
|
3. Fill in the fields:
|
|
- **Server URL**: base URL of the server, e.g. `http://192.168.1.100:8080` (no trailing slash needed; one is stripped automatically).
|
|
- **API Key** (optional): sent as `Authorization: Bearer <key>`. Leave blank if your server doesn't require auth.
|
|
- **API Endpoint**: pick **Whisper.cpp (`/inference`)** or **OpenAI/FunASR (`/v1/audio/transcriptions`)** to match your server.
|
|
- **Model**: model identifier sent to OpenAI-compatible servers. Use `whisper-1` for classic Whisper servers, or `sensevoice` for FunASR/SenseVoice.
|
|
4. Click **Test Connection**. Success means the URL is reachable and credentials (if any) are accepted. A failure here is only a warning; Vocalinux will try again on the first transcription.
|
|
|
|
Settings auto-save and re-initialise the engine immediately, so you can start dictating as soon as the test passes. Toggling the switch off restores the local engine selected on the Speech Engine page in Settings.
|
|
|
|
### Via the config file
|
|
|
|
The same options live in `~/.config/vocalinux/config.json`:
|
|
|
|
```json
|
|
{
|
|
"speech_recognition": {
|
|
"engine": "remote_api",
|
|
"remote_api_url": "http://192.168.1.100:8080",
|
|
"remote_api_key": "",
|
|
"remote_api_endpoint": "/inference",
|
|
"remote_api_model": "whisper-1"
|
|
}
|
|
}
|
|
```
|
|
|
|
Restart Vocalinux after editing the file by hand.
|
|
|
|
FunASR/SenseVoice example:
|
|
|
|
```json
|
|
{
|
|
"speech_recognition": {
|
|
"engine": "remote_api",
|
|
"remote_api_url": "http://localhost:8000",
|
|
"remote_api_key": "",
|
|
"remote_api_endpoint": "/v1/audio/transcriptions",
|
|
"remote_api_model": "sensevoice"
|
|
}
|
|
}
|
|
```
|
|
|
|
## Wire protocol reference
|
|
|
|
If you're writing a custom server, this is exactly what the client sends.
|
|
|
|
### Request (whisper.cpp format, `/inference`)
|
|
|
|
```
|
|
POST {server_url}/inference HTTP/1.1
|
|
Authorization: Bearer <api_key> # only if configured
|
|
Content-Type: multipart/form-data; boundary=...
|
|
|
|
--boundary
|
|
Content-Disposition: form-data; name="file"; filename="audio.wav"
|
|
Content-Type: audio/wav
|
|
|
|
<WAV bytes: 16 kHz, mono, 16-bit PCM>
|
|
--boundary
|
|
Content-Disposition: form-data; name="temperature"
|
|
|
|
0.0
|
|
--boundary
|
|
Content-Disposition: form-data; name="temperature_inc"
|
|
|
|
0.2
|
|
--boundary
|
|
Content-Disposition: form-data; name="response_format"
|
|
|
|
json
|
|
--boundary
|
|
Content-Disposition: form-data; name="language"
|
|
|
|
en
|
|
--boundary--
|
|
```
|
|
|
|
### Request (OpenAI-compatible format, `/v1/audio/transcriptions`)
|
|
|
|
```
|
|
POST {server_url}/v1/audio/transcriptions HTTP/1.1
|
|
Authorization: Bearer <api_key>
|
|
Content-Type: multipart/form-data; boundary=...
|
|
|
|
--boundary
|
|
Content-Disposition: form-data; name="file"; filename="audio.wav"
|
|
Content-Type: audio/wav
|
|
|
|
<WAV bytes>
|
|
--boundary
|
|
Content-Disposition: form-data; name="model"
|
|
|
|
whisper-1
|
|
--boundary
|
|
Content-Disposition: form-data; name="language"
|
|
|
|
en
|
|
--boundary--
|
|
```
|
|
|
|
### Response (`/inference` and `/v1/audio/transcriptions`)
|
|
|
|
```json
|
|
{ "text": "the transcribed utterance" }
|
|
```
|
|
|
|
For OpenAI-compatible FunASR/SenseVoice wrappers, extra metadata is allowed and
|
|
ignored by Vocalinux:
|
|
|
|
```json
|
|
{
|
|
"text": "the transcribed utterance",
|
|
"language": "en",
|
|
"emotion": "neutral",
|
|
"segments": [
|
|
{ "text": "the transcribed utterance", "start": 0.0, "end": 1.4 }
|
|
]
|
|
}
|
|
```
|
|
|
|
If `text` is not present, Vocalinux also accepts `transcript`, `transcription`,
|
|
or a `segments` list with per-segment `text` fields. If a SenseVoice wrapper
|
|
returns rich labels inline, for example `<|en|><|NEUTRAL|><|Speech|>hello`,
|
|
Vocalinux strips the leading labels before text injection.
|
|
|
|
Empty utterances should return `{"text": ""}`. HTTP 4xx/5xx responses are surfaced as transcription errors in the Vocalinux log.
|
|
|
|
### Language handling
|
|
|
|
- Vocalinux's UI language `auto` is sent as **no `language` field** (server auto-detects).
|
|
- `en-us` is normalised to `en`.
|
|
- Anything else is forwarded verbatim.
|
|
|
|
### Timeouts
|
|
|
|
- Connection test: **5 s**
|
|
- Transcription request: **30 s**
|
|
|
|
A request that exceeds the transcription timeout is logged and dropped; the next utterance is sent fresh.
|
|
|
|
## Verifying with `curl`
|
|
|
|
Quick sanity check without launching the GUI:
|
|
|
|
```bash
|
|
# Whisper.cpp server
|
|
curl -F file=@sample.wav \
|
|
-F temperature=0.0 \
|
|
-F response_format=json \
|
|
http://192.168.1.100:8080/inference
|
|
|
|
# OpenAI-compatible
|
|
curl -H "Authorization: Bearer $API_KEY" \
|
|
-F file=@sample.wav \
|
|
-F model=whisper-1 \
|
|
http://192.168.1.100:8000/v1/audio/transcriptions
|
|
|
|
# FunASR/SenseVoice
|
|
curl -F file=@sample.wav \
|
|
-F model=sensevoice \
|
|
http://localhost:8000/v1/audio/transcriptions
|
|
```
|
|
|
|
The multipart endpoints should return JSON with a text field or one of the
|
|
compatible response shapes above.
|
|
|
|
## Troubleshooting
|
|
|
|
| Symptom | Likely cause | Fix |
|
|
|---|---|---|
|
|
| Connection test reports "failed" | Wrong host/port, firewall, or server not running | Check `curl <url>` from the client; check server logs |
|
|
| HTTP 401/403 | API key missing or wrong | Set the **API Key** field; ensure the server is configured for that key |
|
|
| HTTP 404 on first send | Endpoint format mismatch | Switch the **API Endpoint** dropdown to the format your server actually exposes |
|
|
| FunASR/SenseVoice server returns model errors | Model field does not match the served model | Try `sensevoice`, `paraformer`, or the exact model configured on the server |
|
|
| SenseVoice labels appear in injected text | Wrapper returned labels somewhere other than the leading transcript prefix | Return plain text in `text` and put language/emotion/speech labels in JSON metadata |
|
|
| Transcription always empty | Server rejected the WAV (sample rate, channels) | Confirm server accepts 16 kHz mono 16-bit WAV; check server logs |
|
|
| First transcription is slow | Cold model load on the server | Warm the server before dictating, or increase the client timeout server-side |
|
|
| Garbled text in foreign language | Wrong `language` setting | Set the language explicitly in Settings instead of `auto` |
|
|
|
|
Logs live at `~/.local/share/vocalinux/vocalinux.log` (or wherever your `XDG_DATA_HOME` points). Look for lines tagged `Remote API` for client-side detail.
|
|
|
|
## Security notes
|
|
|
|
- The API key is stored in plain JSON in `~/.config/vocalinux/config.json`. Treat that file the same way you treat any other secret on disk.
|
|
- Audio is sent as raw WAV. Anyone on the wire can hear it, so use HTTPS outside a fully trusted network.
|
|
- Vocalinux does **not** validate TLS certificate pins; standard system trust is used. Self-signed certs require importing the CA into your system trust store.
|