Document the Phase 15 deploy and ungate the Spanish pair
deploy/README.md becomes the real runbook: the VPS stack, Traefik, the headscale LLM link, the interim edge gate, backups and restore. The millenia Piper notes move to an appendix -- that instance still runs them, and it is still canonical. Two items are called out as outstanding rather than done, because both need access to millenia: vLLM is not bound to its headscale interface, so no AI pass works from the VPS yet, and parodia's ssh key is not authorized there, so backups are VPS-local only -- which is not a backup in the sense that matters. Each has its one-command fix written down. Also folds in DreamDict gaining Spanish: es was explicitly gated on that dataset existing, so it moves from "Later / not now" to a normal follow-on pair after pt-PT, and the Phase 20 provider seam should cover it from the start.
This commit is contained in:
+226
-48
@@ -1,57 +1,216 @@
|
||||
# Deploying read-aloud (Piper TTS) to millenia
|
||||
# Deploying Petal
|
||||
|
||||
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.
|
||||
Two deployments exist right now:
|
||||
|
||||
## 1. Piper as a systemd service (one-time)
|
||||
| | host | shape | status |
|
||||
|---|---|---|---|
|
||||
| **millenia** | `192.168.1.212` / `100.64.0.2` | bare binary in a screen session on `:8088`, Piper as user systemd units | **canonical** — her real writing lives here |
|
||||
| **parodia** | `petal.parodia.dev` / `100.64.0.1` | docker compose behind the host's Traefik | staging; empty database |
|
||||
|
||||
millenia stays canonical until Phase 16's auth lands, so she only moves accounts
|
||||
once. Everything below is the VPS side; the millenia Piper notes are kept in the
|
||||
appendix because that instance still runs them.
|
||||
|
||||
---
|
||||
|
||||
## 1. The VPS stack
|
||||
|
||||
`docker-compose.yml` at the repo root brings up three containers:
|
||||
|
||||
- **petal** — the single Go binary with the frontend embedded. Publishes no host
|
||||
port; Traefik is the only way in.
|
||||
- **piper-en** / **piper-zh** — read-aloud. Each Piper HTTP server loads exactly
|
||||
one voice, so English and Chinese are separate containers off one image, with
|
||||
the models cached in a shared volume. They sit on an internal network with no
|
||||
published ports, so only Petal can reach them. Adding pt-PT in Phase 21 is a
|
||||
fourth service, not a new image.
|
||||
|
||||
They run as containers rather than the host systemd units millenia uses because
|
||||
Piper was never actually installed on the VPS, and the `reala` account has no
|
||||
lingering session to keep user units alive across logout.
|
||||
|
||||
### Prerequisites on the host
|
||||
|
||||
- Docker with the compose plugin, and the existing external `traefik` network
|
||||
- A DNS A record for the hostname pointing at the VPS (`petal.parodia.dev` is
|
||||
already in place)
|
||||
|
||||
### First deploy
|
||||
|
||||
```bash
|
||||
ssh reala@100.64.0.1
|
||||
git clone https://gitea.parodia.dev/drwily/petal.git ~/petal
|
||||
cd ~/petal
|
||||
cp deploy/petal.env.example .env
|
||||
```
|
||||
|
||||
Then edit `.env`:
|
||||
|
||||
- `PETAL_UID` / `PETAL_GID` — `id -u` / `id -g` for this account. `./data` is a
|
||||
bind mount, so the image's own `petal` user has no claim on it; a mismatch
|
||||
shows up as `unable to open database file (14)` and a restart loop.
|
||||
- `PETAL_BASIC_AUTH` — the interim edge gate, see §4.
|
||||
- `LLM_MODEL` / `LLM_CHAT_MODEL` — see §3.
|
||||
|
||||
```bash
|
||||
mkdir -p data/backups
|
||||
docker compose up -d --build
|
||||
docker compose ps # all three healthy
|
||||
```
|
||||
|
||||
### Updating
|
||||
|
||||
```bash
|
||||
cd ~/petal && git pull && docker compose up -d --build
|
||||
```
|
||||
|
||||
The frontend is embedded in the binary, so a rebuild is the whole deploy. The
|
||||
client polls `/api/version` (a hash of the built `index.html`) and offers a
|
||||
refresh when it changes.
|
||||
|
||||
---
|
||||
|
||||
## 2. What Traefik does
|
||||
|
||||
Labels follow the convention the other services on this box use: the external
|
||||
`traefik` network, the `web-secure` entrypoint, the `default` cert resolver and
|
||||
`compression@file`. Petal adds its own response-header middleware
|
||||
(`frame-ancestors 'self'`, HSTS, nosniff, `Referrer-Policy: same-origin`).
|
||||
|
||||
`/api/health` is deliberately on its own higher-priority router with no
|
||||
middleware: a monitoring probe must not need a credential, and the endpoint
|
||||
carries no user data.
|
||||
|
||||
---
|
||||
|
||||
## 3. The LLM link over headscale
|
||||
|
||||
The vLLM backend stays on millenia and is reached over headscale
|
||||
(`100.64.0.2`). **This is the only cross-VPN dependency**, and by the
|
||||
LLM-minimalism principle it never gates essential functionality — spell check,
|
||||
gloss, vocabulary garden, search, export and read-aloud all keep working with
|
||||
the link down, and the status bar shows the warm
|
||||
`🌙 小助手在休息 · Petal's helper is resting · 文字已保存`.
|
||||
|
||||
`LLM_TIMEOUT` is raised from the local-network default of 30s to **90s**: the
|
||||
voice and collocation passes send a whole document, the timeout is a hard
|
||||
deadline on the completion call, and a WAN+VPN round trip eats the margin.
|
||||
|
||||
### Outstanding — vLLM is not yet listening on headscale
|
||||
|
||||
As of the Phase 15 deploy, `100.64.0.2:8000` refuses connections from the VPS:
|
||||
vLLM is bound to loopback or the LAN interface, not the headscale one. Petal
|
||||
degrades correctly (verified — `POST /api/docs/{id}/check` returns the warm
|
||||
502), but no AI pass will work until this is fixed on millenia:
|
||||
|
||||
- start vLLM with `--host 100.64.0.2` — the headscale address specifically,
|
||||
**not** `0.0.0.0`; the other end of this link is a public host
|
||||
- confirm with `ss -lntp | grep 8000` that it binds only that interface
|
||||
- from the VPS: `curl -s http://100.64.0.2:8000/v1/models`
|
||||
- put the model id it reports into `LLM_MODEL` / `LLM_CHAT_MODEL` in `.env`,
|
||||
then `docker compose up -d`
|
||||
|
||||
---
|
||||
|
||||
## 4. Interim edge gate (delete when Phase 16 lands)
|
||||
|
||||
Petal authenticates nobody yet — `StaticResolver` hands every request the same
|
||||
`local` user. On a public host that means anyone who finds the hostname can read
|
||||
and write documents and fill the disk with image uploads, so Traefik holds the
|
||||
door with basic auth until the OIDC flow exists.
|
||||
|
||||
Generate a credential:
|
||||
|
||||
```bash
|
||||
htpasswd -nbB petal 'your-password' # or any bcrypt htpasswd generator
|
||||
```
|
||||
|
||||
and put the resulting `user:hash` pair in `.env` as `PETAL_BASIC_AUTH`.
|
||||
|
||||
When Phase 16 lands, delete the `petal-auth` middleware label, the
|
||||
`petal-health` router labels, and this section.
|
||||
|
||||
---
|
||||
|
||||
## 5. Backups
|
||||
|
||||
`deploy/backup-petal.sh` runs nightly from cron at 03:15:
|
||||
|
||||
```
|
||||
15 3 * * * REMOTE_HOST= /bin/bash $HOME/petal/deploy/backup-petal.sh >> $HOME/petal/data/backups/backup.log 2>&1
|
||||
```
|
||||
|
||||
The snapshot goes through `petal -backup`, which uses SQLite's `VACUUM INTO`.
|
||||
That matters: Petal runs in WAL mode, so the newest committed pages may live in
|
||||
`petal.db-wal` rather than `petal.db`, and copying the three files separately can
|
||||
capture a torn mid-checkpoint state. `VACUUM INTO` reads one coherent snapshot
|
||||
including the WAL, takes no write lock (so it is safe against the live app), and
|
||||
emits a single file with no `-wal`/`-shm` companions. It refuses an existing
|
||||
destination, so a failed run cannot destroy the last good backup.
|
||||
|
||||
The script then gzips, pushes off-box, verifies the transferred size, and prunes
|
||||
both sides (7 days local, 30 days remote).
|
||||
|
||||
### Outstanding — the off-VPS push is not yet enabled
|
||||
|
||||
`REMOTE_HOST` is empty in the cron line, so backups are currently **local to the
|
||||
VPS only** — which is not a backup in the sense that matters. parodia's ssh key
|
||||
is not authorized on millenia. To enable it, on millenia:
|
||||
|
||||
```bash
|
||||
echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICGL6fS7IKCs8xFxUoH/bI/0kq4AzW05bcfV2XHoEXYM ditto-deploy@parodia-box' >> ~/.ssh/authorized_keys
|
||||
```
|
||||
|
||||
then on the VPS, prove it works by hand before touching cron:
|
||||
|
||||
```bash
|
||||
cd ~/petal && REMOTE_USER=<millenia-user> ./deploy/backup-petal.sh
|
||||
```
|
||||
|
||||
and set `REMOTE_USER=<millenia-user> REMOTE_HOST=100.64.0.2` in the cron line.
|
||||
|
||||
### Restore
|
||||
|
||||
```bash
|
||||
cd ~/petal
|
||||
docker compose stop petal # stop writers first
|
||||
mv data/petal.db data/petal.db.before-restore # keep the current state
|
||||
rm -f data/petal.db-wal data/petal.db-shm # a stale WAL against a new file
|
||||
gunzip -c data/backups/petal-<stamp>.db.gz > data/petal.db
|
||||
docker compose start petal
|
||||
docker compose logs petal --tail 5 # expect "database ready"
|
||||
```
|
||||
|
||||
To sanity-check an archive before committing to it, restore it into a scratch
|
||||
directory and have Petal open it:
|
||||
|
||||
```bash
|
||||
mkdir -p /tmp/restore-check
|
||||
gunzip -c data/backups/petal-<stamp>.db.gz > /tmp/restore-check/petal.db
|
||||
docker run --rm -v /tmp/restore-check:/data --user "$(id -u):$(id -g)" \
|
||||
--entrypoint sh petal:local -c '/app/petal -backup /data/verify.db'
|
||||
```
|
||||
|
||||
A clean exit means the file opens and reads end to end.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — Piper on millenia (user systemd units)
|
||||
|
||||
millenia still runs Piper as user services; these are the original notes.
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
`setup-piper.sh` creates `~/piper/venv`, installs `piper-tts[http]`, downloads
|
||||
`en_US-amy-medium` into `~/piper/voices`, installs and enables `piper.service`
|
||||
(loopback `:5005`), and smoke-tests it. Idempotent. Check it with
|
||||
`systemctl status piper` / `journalctl -u piper -f`.
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
# 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:
|
||||
Chinese runs as a second instance (`piper-zh.service`, `:5006`,
|
||||
`zh_CN-huayan-medium`):
|
||||
|
||||
```bash
|
||||
scp deploy/piper-zh.service 192.168.1.212:~/.config/systemd/user/
|
||||
@@ -60,6 +219,25 @@ ssh 192.168.1.212 'export XDG_RUNTIME_DIR=/run/user/$(id -u)
|
||||
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).
|
||||
Petal's env then carries `TTS_ENDPOINT=http://127.0.0.1:5005`,
|
||||
`TTS_ENDPOINT_ZH=http://127.0.0.1:5006` and the matching voice ids. The handler
|
||||
maps language → instance from config, so another language is another instance
|
||||
plus an env pair, no code change.
|
||||
|
||||
**Piper version note:** piper-tts moved synthesis from `POST /` to
|
||||
`POST /synthesize` in 1.6.0, with an identical request body. `TTS_PATH` selects
|
||||
which — it defaults to `/` for millenia's older server, and the VPS compose sets
|
||||
`/synthesize` for the 1.6.0 sidecars. If read-aloud starts returning 502 after a
|
||||
Piper upgrade on millenia, that flag is the fix.
|
||||
|
||||
Verify end to end (through Petal, including the ffmpeg transcode):
|
||||
|
||||
```bash
|
||||
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: MPEG ADTS, layer III
|
||||
```
|
||||
|
||||
A second identical call is served from the cache; a language with no configured
|
||||
instance returns 404 so the client falls back to Web Speech.
|
||||
|
||||
Reference in New Issue
Block a user