TTS — How the Intern Sounds
HAL turns every reply into speech with the configured TTS provider. Intern v2 ships with ElevenLabs through the Autonomous proxy; you can change the provider, the voice and the speed, bring your own key, or go fully offline with Piper.
Providers
tts_provider | What it is | Model used |
|---|---|---|
elevenlabs | ElevenLabs (Intern v2 default) | eleven_v4_turbo (over HTTP, or WebSocket with HAL_TTS_ELEVENLABS_WS=true) |
openai | OpenAI TTS, or any OpenAI-compatible /audio/speech server | tts-1 |
gemini | Gemini TTS through the Autonomous proxy | HAL_TTS_GEMINI_MODEL (default gemini-3.8-flash-tts) |
piper | Local, offline Piper voices on the device | the .onnx voice file |
An empty or unknown provider falls back to the OpenAI-compatible backend.
The keys
Key (config.json) | Default | Notes |
|---|---|---|
tts_provider | elevenlabs on Intern v2 | See table above |
tts_voice | By language: Rachel (en), Ngan (vi), Amy (zh) | Voice name or provider voice id |
tts_speed | 1.1 (from HAL_TTS_SPEED in /opt/hal/.env) | 0.25–4.0; the UI slider covers 0.5–2.0 |
tts_api_key | Empty → uses llm_api_key | Your own provider key |
tts_base_url | Empty → uses llm_base_url | Your own provider URL |
tts_instructions | "Friendly" (HAL_TTS_INSTRUCTIONS) | OpenAI only: style prompt. Hand-edit config.json and restart HAL — not in the API. |
Change voice / provider
Web UI: http://<device-ip>/setting?debug=true#tts → Voice. Choose
Autonomous (proxy) (with an OpenAI / ElevenLabs / Gemini sub-select),
OpenAI (direct), ElevenLabs (direct), Piper (Local — free) or Custom (BYO URL);
pick a voice and speed; use Test Voice to hear it before saving.
API:
curl -s -X PUT $DEVICE/api/device/config \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"tts_provider":"elevenlabs","tts_voice":"Brian","tts_speed":1.0}'
TTS changes are pushed to HAL live — the next sentence uses the new voice. (If the live push fails, os-server restarts HAL instead.)
Your own ElevenLabs or OpenAI key
# ElevenLabs direct — HAL detects api.elevenlabs.io (also api.us. / api.eu.)
curl -s -X PUT $DEVICE/api/device/config \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"tts_provider":"elevenlabs",
"tts_base_url":"https://api.elevenlabs.io",
"tts_api_key":"<elevenlabs-key>",
"tts_voice":"<voice-id-from-your-library>"}'
# OpenAI direct
curl -s -X PUT $DEVICE/api/device/config \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"tts_provider":"openai",
"tts_base_url":"https://api.openai.com/v1",
"tts_api_key":"sk-…",
"tts_voice":"nova"}'
Always set tts_base_url explicitly when you set your own key — an empty
base URL falls back to the LLM endpoint. To remove a custom key, send
{"clear_tts_api_key": true}.
Voices
List what's available for a provider (optionally filtered by lang):
curl -s -H "Authorization: Bearer $TOKEN" "$DEVICE/api/device/voices?provider=elevenlabs&lang=vi"
| Provider | Voices |
|---|---|
| ElevenLabs (built-in names) | en: Rachel, Sarah, Nicole, Terra, Maria, Sophie, Piper, Mia, Kimmy, Brianna, Ally, Tori, Brian, Adam, Daniel, George, James, Liam, Charlie, Sam, Sean, Kael, Brooks, Erion · vi: Ngan, Linh, Huyen, Freya, Nathan, Quan · zh: Amy, Sage, Xiaoxi, Yun, Evan Zhao, Jin |
| ElevenLabs (any other) | Put a raw ElevenLabs voice_id in tts_voice — it's passed through as-is, so any voice in your ElevenLabs account works |
| OpenAI | alloy, ash, coral, echo, fable, onyx, nova, sage, shimmer |
| Gemini | 30 multilingual prebuilt voices, default Kore (unknown names fall back to Kore) |
| Piper | Any .onnx voice installed on the device (below) |
Offline TTS with Piper
Piper runs entirely on the device — no network, no API cost — at lower quality than the cloud voices. It isn't pre-installed.
Web UI: Voice → Piper (Local — free) → install the engine and download a voice in the Piper panel → Save.
API (proxied to HAL, admin-gated):
# 1. Install the engine (downloads the aarch64 Piper binary to /opt/piper)
curl -s -X POST -H "Authorization: Bearer $TOKEN" $DEVICE/api/voice/piper/install
# 2. Download a voice from the catalog
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"name":"en_US-kristin-medium"}' $DEVICE/api/voice/piper/voice
# 3. Status (engine + installed voices)
curl -s -H "Authorization: Bearer $TOKEN" $DEVICE/api/voice/piper/status
# 4. Use it
curl -s -X PUT $DEVICE/api/device/config \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"tts_provider":"piper","tts_voice":"en_US-kristin-medium"}'
Catalog voices: en_US-ljspeech-medium, en_US-kristin-medium,
en_US-libritts_r-medium, vi_VN-vais1000-medium, es_ES-davefx-medium,
de_DE-thorsten-medium, pt_BR-faber-medium, fr_FR-siwis-medium.
To use any other Piper model, copy its .onnx (and .onnx.json) into
/opt/piper/voices/ and set tts_voice to the file name without extension —
or to an absolute path. Remove a voice with
POST /api/voice/piper/voice/remove (the last voice can't be removed).
| HAL env | Default |
|---|---|
HAL_PIPER_BIN | /opt/piper/piper |
HAL_PIPER_VOICES | /opt/piper/voices |
HAL_PIPER_VOICE | en_US-ljspeech-medium |
Try a voice without saving
This is what Test Voice in the UI calls — it speaks one sentence with the given voice and leaves the saved config alone:
curl -s -X POST $DEVICE/api/voice/preview \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"text":"Testing a different voice","provider":"elevenlabs","voice":"Brian","speed":1.0}'
To make the device speak from your own code (including per-utterance voice overrides), see HAL API → TTS.
TTS cache
Rendered phrases are cached as WAV files in /var/lib/hal/tts_cache
(HAL_TTS_CACHE_DIR). The cache key includes provider, voice, model, speed and
text, so changing any of them automatically uses fresh audio. Delete the
directory to reclaim space; it refills on demand.
Related HAL env (/opt/hal/.env)
| Var | Default | Effect |
|---|---|---|
HAL_TTS_SPEED | 1.1 on Intern v2 | Speed when tts_speed isn't set |
HAL_TTS_INSTRUCTIONS | Friendly | OpenAI style prompt when tts_instructions isn't set |
HAL_TTS_ELEVENLABS_WS | false | Stream ElevenLabs over a reused WebSocket session (lower latency) |
HAL_TTS_GEMINI_MODEL | gemini-3.8-flash-tts | Gemini TTS model |
TTS_VOICE | nova | Voice when tts_voice is empty |
HAL_TTS_CACHE_DIR | /var/lib/hal/tts_cache | Cache location |
Restart HAL after editing: sudo systemctl restart hal.