Word lookups now come from DreamDict's dict.db for every pair but Chinese — opened read-only beside petal.db, no service, nothing over the VPN, because a hover gloss has to answer in milliseconds. `Provider` is the two questions the popover and the tooltip already asked, so the embedded *Lexicon satisfies it with no changes at all; Set.For(lang) is the single place the choice between them is made. The prerequisite in the dreamdict repo turned out to be two things, not one: the module path was unfetchable *and* the query layer sat in internal/, which no other module may import whatever the module is called. Both fixed upstream. The plan's central assumption did not survive the data. It mapped Gloss ← Translate(word, "en", L1) one-to-one; against the real 452 MB database that table answers for 17% of the 2,000 commonest English words into pt-PT. Wiktionary's translation sections are thin in that direction — "ephemeral", "think" and "quickly" have no en→pt-PT row at all. Shared WordNet synsets answer for 61%, so DreamDict gained Equivalents() and Petal glosses through it. Ordering those was wrong in an instructive way too: sorting by frequency glosses "think" as lembrar, "remember", because lembrar is the commoner Portuguese word even though pensar shares six of think's synsets to lembrar's one. Counting sense agreement first asks the right question. The same measurement is why zh stays on ECDICT: DreamDict reaches a Chinese gloss for 53% of those words, ECDICT for nearly all of them. The plan said converge only if quality holds. It didn't, so nothing converged. Two decisions about failure worth keeping. A missing dict.db is not an error — a laptop checkout has never had one — but a present-and-never-imported one is, because that is a half-finished deploy. And a pt-PT writer with no dictionary falls back to the embedded datasets with the gloss suppressed, keeping definitions, synonyms and phonetics rather than blanking the popover: an empty field reads as "not found", the wrong language reads as broken. The new fields surface as an etymology line and a three-band chip. Three, not five: the difficulty score separates "everyday" from "you'll have to explain this" but cannot rank obfuscate against serendipity, and a finer scale would be a confident-looking lie. An unscored word gets no chip. Writing the tests found two bugs first — trimEtymology sliced by byte, which would have emitted invalid UTF-8 for exactly the Greek and Latin etymologies the feature exists for, and its ellipsis path overran its own cap. go build/vet/test, tsc, vite, vitest 96/96 clean; live smoke against the real dict.db with one instance flipped from zh to pt-PT mid-run. Not deployed: go.mod still replaces github.com/prosolis/dreamdict with ../dreamdict, so the Docker build needs the two upstream commits pushed and the replace dropped. The deployed dict.db also predates DreamDict's Spanish data. Claude-Session: https://claude.ai/code/session_016y6gyuHkQXPiEuW8RGQyua
138 lines
5.7 KiB
YAML
138 lines
5.7 KiB
YAML
# Petal on the parodia.dev VPS.
|
|
#
|
|
# docker compose up -d --build
|
|
#
|
|
# Fronted by the host's existing Traefik (external `traefik` network, the
|
|
# `web-secure` entrypoint and the `default` cert resolver — same convention the
|
|
# other services on this box use). Petal itself never binds a host port; the
|
|
# only way in is through Traefik over HTTPS.
|
|
#
|
|
# Read-aloud runs as two sibling containers rather than host systemd services:
|
|
# each Piper HTTP server loads exactly one voice, the host has no lingering
|
|
# user session to keep systemd units alive, and keeping them on the internal
|
|
# network means the TTS ports are unreachable from anywhere but Petal.
|
|
#
|
|
# Copy deploy/petal.env.example to .env before the first `up`.
|
|
|
|
name: petal
|
|
|
|
services:
|
|
petal:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
image: petal:local
|
|
container_name: petal
|
|
restart: unless-stopped
|
|
# ./data is a bind mount, so the image's own `petal` user (uid 10001) has no
|
|
# claim on it — the host's ownership wins and the container can't open
|
|
# petal.db. Run as whoever owns the stack directory instead. Keeping it the
|
|
# host user (rather than chowning ./data to 10001) is deliberate: the backup
|
|
# script gzips snapshots in place from the host, so the host account needs
|
|
# write access to the same directory. Still never root.
|
|
user: "${PETAL_UID:-1001}:${PETAL_GID:-1001}"
|
|
env_file: .env
|
|
environment:
|
|
# Fixed by the image layout; kept here so they're visible at a glance.
|
|
PORT: "8080"
|
|
DATABASE_PATH: /data/petal.db
|
|
IMAGE_DIR: /data/images
|
|
TTS_CACHE_DIR: /data/tts
|
|
# DreamDict's built dictionary, read-only, deployed into the data volume
|
|
# (see deploy/README.md). Absent it, word lookups fall back to the
|
|
# embedded English/Chinese datasets rather than failing.
|
|
DICT_PATH: /data/dict.db
|
|
# Piper sidecars. Each server loads one voice, so English and Chinese are
|
|
# separate containers; the handler maps language → instance from config.
|
|
TTS_ENDPOINT: http://piper-en:5000
|
|
TTS_ENDPOINT_ZH: http://piper-zh:5000
|
|
# The sidecars run piper-tts 1.6.0, which serves synthesis on
|
|
# /synthesize; millenia's older server keeps the default "/".
|
|
TTS_PATH: /synthesize
|
|
# The companion's bedtime nag and night mode read the local clock.
|
|
TZ: ${TZ:-Europe/Lisbon}
|
|
volumes:
|
|
# A bind mount, not a named volume: petal.db must be trivially reachable
|
|
# from the host for the nightly backup and for a restore.
|
|
- ./data:/data
|
|
# Mount-liveness guard. On the VPS ./data is an encrypted LUKS volume, and
|
|
# the mountpoint directory still exists when that volume is NOT mounted —
|
|
# so without this, a boot where the unlock failed would start Petal
|
|
# against an empty unencrypted directory and quietly serve a blank
|
|
# database. .volume-ok lives on the encrypted filesystem, and
|
|
# create_host_path: false turns its absence into a container start
|
|
# failure instead. Harmless elsewhere: create the file once and it is a
|
|
# no-op. See deploy/README.md §6.
|
|
- type: bind
|
|
source: ./data/.volume-ok
|
|
target: /data/.volume-ok
|
|
read_only: true
|
|
bind:
|
|
create_host_path: false
|
|
networks:
|
|
- traefik
|
|
- internal
|
|
depends_on:
|
|
- piper-en
|
|
- piper-zh
|
|
labels:
|
|
traefik.enable: "true"
|
|
traefik.docker.network: traefik
|
|
traefik.http.routers.petal.rule: Host(`${PETAL_HOST:-petal.parodia.dev}`)
|
|
traefik.http.routers.petal.entrypoints: web-secure
|
|
traefik.http.routers.petal.tls: "true"
|
|
traefik.http.routers.petal.tls.certResolver: default
|
|
traefik.http.routers.petal.service: petal
|
|
# No edge gate: Petal authenticates for itself now (Authentik OIDC), so
|
|
# every /api route answers 401 without a session and the only thing served
|
|
# to an anonymous visitor is the app shell and its sign-in redirect. The
|
|
# basic-auth middleware that stood here until Phase 16 — plus the separate
|
|
# unauthenticated router /api/health needed to escape it — is gone; a
|
|
# second password in front of a real login is just one more thing to lose.
|
|
traefik.http.routers.petal.middlewares: compression@file,petal-headers
|
|
traefik.http.services.petal.loadbalancer.server.port: "8080"
|
|
# Petal is a private writing space: no framing, no sniffing, HSTS on.
|
|
traefik.http.middlewares.petal-headers.headers.customresponseheaders.Content-Security-Policy: frame-ancestors 'self'
|
|
traefik.http.middlewares.petal-headers.headers.customresponseheaders.Strict-Transport-Security: max-age=31536000; includeSubDomains
|
|
traefik.http.middlewares.petal-headers.headers.customresponseheaders.X-Content-Type-Options: nosniff
|
|
traefik.http.middlewares.petal-headers.headers.customresponseheaders.Referrer-Policy: same-origin
|
|
|
|
piper-en:
|
|
build:
|
|
context: deploy/piper
|
|
image: petal-piper:local
|
|
container_name: petal-piper-en
|
|
restart: unless-stopped
|
|
environment:
|
|
PIPER_VOICE: ${TTS_VOICE_EN:-en_US-amy-medium}
|
|
volumes:
|
|
- piper-voices:/voices
|
|
networks:
|
|
- internal
|
|
|
|
piper-zh:
|
|
build:
|
|
context: deploy/piper
|
|
image: petal-piper:local
|
|
container_name: petal-piper-zh
|
|
restart: unless-stopped
|
|
environment:
|
|
PIPER_VOICE: ${TTS_VOICE_ZH:-zh_CN-huayan-medium}
|
|
volumes:
|
|
- piper-voices:/voices
|
|
networks:
|
|
- internal
|
|
|
|
networks:
|
|
# Created and owned by the host's Traefik stack.
|
|
traefik:
|
|
external: true
|
|
# Petal ↔ Piper only. Not reachable from the internet or the other stacks.
|
|
internal:
|
|
driver: bridge
|
|
|
|
volumes:
|
|
# Downloaded voice models, shared read-mostly by both Piper instances so the
|
|
# same model is never fetched twice.
|
|
piper-voices:
|