Replace the browser's robotic Web Speech API (espeak on Chromium) as the
primary read-aloud path with server-side Piper neural TTS, served by petal
and kept fully offline on millenia next to Ollama.
- internal/tts: proxy short passages to Piper, transcode WAV -> mp3/opus via
ffmpeg, content-addressed disk cache (instant re-taps). Each Piper instance
loads one voice, so language routes to its own endpoint:{voice}. Unknown
language -> 404 so the client falls back to Web Speech. UTF-8-safe truncation
for multibyte (Chinese) text.
- config: TTS_ENDPOINT / TTS_ENDPOINT_ZH / TTS_VOICE_EN / TTS_VOICE_ZH /
TTS_CACHE_DIR / TTS_TIMEOUT / TTS_AUDIO_FORMAT. Route mounts only when
TTS_ENDPOINT is set; otherwise unchanged behavior.
- web/audio/speech.ts: speak() hits /api/tts first, falls back to Web Speech on
any failure; rapid-tap-safe via a request token. Call sites unchanged.
- deploy/: Piper user systemd units (EN :5005, ZH :5006), setup script, README.
English (en_US-amy-medium) and Chinese (zh_CN-huayan-medium) are both live.
Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
2.6 KiB
Deploying read-aloud (Piper TTS) to millenia
Petal's read-aloud generates audio with a local Piper neural-TTS server and
transcodes it to mp3 with ffmpeg. Both run on millenia (192.168.1.212);
nothing leaves the box. If Piper is down or TTS_ENDPOINT is unset, the frontend
falls back to the browser's Web Speech API automatically.
1. Piper as a systemd service (one-time)
# from this repo, on your workstation:
scp deploy/piper.service deploy/setup-piper.sh 192.168.1.212:/tmp/
ssh 192.168.1.212 'cd /tmp && sudo ./setup-piper.sh'
setup-piper.sh creates ~/piper/venv, installs piper-tts[http], downloads the
en_US-amy-medium voice into ~/piper/voices, installs+enables piper.service
(loopback :5005), and smoke-tests it. Idempotent.
Check it any time: systemctl status piper, journalctl -u piper -f.
2. Point petal at Piper + redeploy the binary
Add to petal's environment (its .env or launch env):
TTS_ENDPOINT=http://127.0.0.1:5005
TTS_VOICE_EN=en_US-amy-medium
TTS_AUDIO_FORMAT=mp3
Then ship the rebuilt binary (go build -o petal ./cmd/server already done) and
restart the petal :8088 session. Confirm the log line:
read-aloud enabled (TTS endpoint=http://127.0.0.1:5005).
3. Verify
# on millenia — end-to-end through petal, including ffmpeg transcode:
curl -sf -X POST localhost:8088/api/tts \
-H 'Content-Type: application/json' \
-d '{"text":"hello there","lang":"en-US"}' -o /tmp/petal-tts.mp3 \
&& file /tmp/petal-tts.mp3 # expect: Audio file ... MPEG ... layer III
- Second identical call is served from the cache (
~/petal/.../data/tts/*.mp3). - A language with no configured instance returns 404 → client uses Web Speech.
- Browser check via the uitest harness (
~/petal/uitest): tap a word in the WordCard / select a sentence and hit speak — expect the natural Piper voice.
Chinese voice (live)
Each Piper HTTP server loads ONE model, so Chinese runs as a second instance:
piper-zh.service on :5006 with zh_CN-huayan-medium. Deployed via:
scp deploy/piper-zh.service 192.168.1.212:~/.config/systemd/user/
ssh 192.168.1.212 'export XDG_RUNTIME_DIR=/run/user/$(id -u)
~/piper/venv/bin/python -m piper.download_voices zh_CN-huayan-medium --data-dir ~/piper/voices
systemctl --user daemon-reload && systemctl --user enable --now piper-zh.service'
Then in petal's start.sh: TTS_ENDPOINT_ZH=http://127.0.0.1:5006 and
TTS_VOICE_ZH=zh_CN-huayan-medium. The handler maps language → instance from config,
so adding more languages is just another instance + env pair (no code change).