Deploy plumbing so Petal can run on the public VPS behind the Traefik already on that box, with vLLM reached over headscale. - Dockerfile: node build -> go build -> alpine runtime. CGO stays off (modernc SQLite is pure Go), so the runtime layer exists only for ffmpeg (read-aloud transcodes Piper's WAV) and tzdata (the companion's bedtime nag and night mode read the local clock). Runs as uid 10001 with /data as the single writable mount. - docker-compose.yml: Traefik labels following this host's convention (external `traefik` network, `web-secure` entrypoint, `default` cert resolver). Petal publishes no host port. ./data is a bind mount, not a named volume, so the nightly backup and a restore are reachable from the host. - Piper runs as two sibling containers rather than host systemd units. The plan assumed Piper was already installed on the VPS; it is not, the host has no lingering user session to keep user units alive, and containers keep the TTS ports on an internal network unreachable from anywhere but Petal. One image, voice chosen per service, model cached in a shared volume -- so the pt-PT voice is a new service, not a new image. - db.Backup + a `-backup` flag: VACUUM INTO, not a file copy. Petal runs in WAL mode, so the newest committed pages may live in petal.db-wal; copying the three files separately can capture a torn mid-checkpoint state. VACUUM INTO reads one coherent snapshot without taking a write lock, and emits a single file with no -wal/-shm companions. Refuses an existing destination so a failed run can't destroy the last good backup. - deploy/backup-petal.sh: nightly snapshot, compress, push to millenia over headscale with a post-transfer size check, prune both sides. - deploy/petal.env.example: LLM_TIMEOUT raised 30s -> 90s for the WAN+VPN round trip, since the voice and collocation passes send a whole document and the timeout is a hard deadline on Complete.
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).