diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 12:00:03 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 12:00:03 +0100 |
| commit | 722883ab4a4583b799ce8bfd0cfad70b4b89daf9 (patch) | |
| tree | 8b19ff073d3981ebc4e3a5428cd953463fa5c46e | |
| parent | contextual play should add to the last in the queue (diff) | |
| download | senator-sperma-722883ab4a4583b799ce8bfd0cfad70b4b89daf9.tar.gz senator-sperma-722883ab4a4583b799ce8bfd0cfad70b4b89daf9.zip | |
docker: bake espeak-ng data for Piper TTS
espeak-rs-sys installs espeak-ng-data into the build tree and compiles that
path in as the default, so the runtime image failed with 'phontab: No such
file or directory'. Copy the data from the builder to /usr/share and point
PIPER_ESPEAKNG_DATA_DIRECTORY there; compose overrides the host path from
.env.
Also trim trailing slashes from Copyparty base URLs, document that env_file
changes need --force-recreate, and clean inline comments in .env.example.
| -rw-r--r-- | .env.example | 15 | ||||
| -rw-r--r-- | Dockerfile | 8 | ||||
| -rw-r--r-- | README.md | 26 | ||||
| -rw-r--r-- | compose.yaml | 2 | ||||
| -rw-r--r-- | src/copyparty/mod.rs | 5 |
5 files changed, 47 insertions, 9 deletions
diff --git a/.env.example b/.env.example index 01e24b3..e67827d 100644 --- a/.env.example +++ b/.env.example | |||
| @@ -3,17 +3,28 @@ TOKEN= | |||
| 3 | 3 | ||
| 4 | # Optional - Bot Configuration | 4 | # Optional - Bot Configuration |
| 5 | STATUS_CHANNEL_ID= | 5 | STATUS_CHANNEL_ID= |
| 6 | SENATOR_DB_PATH=data/senator.db | ||
| 6 | 7 | ||
| 7 | # Optional - Copyparty File Server (for music /search commands) | 8 | # Optional - Copyparty File Server (for music /search commands) |
| 9 | # No trailing slash; a trailing slash produces double slashes in built URLs. | ||
| 8 | COPYPARTY_URL=http://localhost:8080 | 10 | COPYPARTY_URL=http://localhost:8080 |
| 9 | COPYPARTY_URL_UPLOAD=http://localhost:8080/uploads | 11 | COPYPARTY_URL_UPLOAD=http://localhost:8080/uploads |
| 10 | COPYPARTY_PASSWORD= | 12 | COPYPARTY_PASSWORD= |
| 11 | 13 | ||
| 12 | # Optional - AI functionality | 14 | # Optional - AI functionality |
| 13 | ASSISTANT_KEYWORD=jarvis # Keyword to trigger AI assistant | 15 | ASSISTANT_KEYWORD=jarvis |
| 16 | # Full chat-completions URL. For a local server running on the Docker host, | ||
| 17 | # use the host's LAN IP (localhost inside the container is the container). | ||
| 18 | LLM_API_URL=https://openrouter.ai/api/v1/chat/completions | ||
| 19 | LLM_API_KEY= | ||
| 20 | LLM_MODEL= | ||
| 21 | # Legacy OpenRouter fallbacks (LLM_* takes priority) | ||
| 14 | OPENROUTER_API_KEY= | 22 | OPENROUTER_API_KEY= |
| 15 | OPENROUTER_MODEL= | 23 | OPENROUTER_MODEL= |
| 16 | 24 | ||
| 17 | # Optional - Local TTS | 25 | # Optional - Local TTS |
| 18 | TTS_MODEL_PATH=/path/to/models # or a full path to pl_PL-bass-high.onnx | 26 | # Directory containing <voice>.onnx, or a full path to the .onnx file. |
| 27 | TTS_MODEL_PATH=/path/to/piper | ||
| 19 | TTS_VOICE=pl_PL-bass-high | 28 | TTS_VOICE=pl_PL-bass-high |
| 29 | # Parent directory of espeak-ng-data. Docker overrides this to /usr/share. | ||
| 30 | PIPER_ESPEAKNG_DATA_DIRECTORY=/path/to/piper | ||
| @@ -33,6 +33,11 @@ RUN mkdir src \ | |||
| 33 | COPY src ./src | 33 | COPY src ./src |
| 34 | RUN cargo build --release | 34 | RUN cargo build --release |
| 35 | 35 | ||
| 36 | # espeak-rs-sys installs the espeak-ng data into the build directory and | ||
| 37 | # compiles that path in as the default. Without this the runtime image has no | ||
| 38 | # data files and TTS fails with "phontab: No such file or directory". | ||
| 39 | RUN cp -a target/release/build/espeak-rs-sys-*/out/share/espeak-ng-data /app/espeak-ng-data | ||
| 40 | |||
| 36 | # ── Runtime ─────────────────────────────────────────────────────────── | 41 | # ── Runtime ─────────────────────────────────────────────────────────── |
| 37 | FROM debian:trixie-slim | 42 | FROM debian:trixie-slim |
| 38 | 43 | ||
| @@ -65,11 +70,14 @@ RUN useradd --create-home --uid 1000 --shell /usr/sbin/nologin bot \ | |||
| 65 | 70 | ||
| 66 | WORKDIR /app | 71 | WORKDIR /app |
| 67 | COPY --from=builder /app/target/release/senator-sperma /usr/local/bin/senator-sperma | 72 | COPY --from=builder /app/target/release/senator-sperma /usr/local/bin/senator-sperma |
| 73 | # espeak looks for <PIPER_ESPEAKNG_DATA_DIRECTORY>/espeak-ng-data. | ||
| 74 | COPY --from=builder /app/espeak-ng-data /usr/share/espeak-ng-data | ||
| 68 | 75 | ||
| 69 | USER bot | 76 | USER bot |
| 70 | 77 | ||
| 71 | ENV TZ=Europe/Warsaw \ | 78 | ENV TZ=Europe/Warsaw \ |
| 72 | TTS_MODEL_PATH=/app/piper \ | 79 | TTS_MODEL_PATH=/app/piper \ |
| 80 | PIPER_ESPEAKNG_DATA_DIRECTORY=/usr/share \ | ||
| 73 | RUST_LOG=info \ | 81 | RUST_LOG=info \ |
| 74 | HOME=/home/bot | 82 | HOME=/home/bot |
| 75 | 83 | ||
| @@ -69,10 +69,11 @@ docker compose up -d --build | |||
| 69 | Or without Compose: | 69 | Or without Compose: |
| 70 | 70 | ||
| 71 | ```bash | 71 | ```bash |
| 72 | docker build -t senator-sperma . | 72 | docker buildx build --load -t senator-sperma:latest . |
| 73 | docker run -d --name senator-sperma --restart unless-stopped \ | 73 | docker run -d --name senator-sperma --restart unless-stopped \ |
| 74 | --env-file .env \ | 74 | --env-file .env \ |
| 75 | -e TTS_MODEL_PATH=/app/piper \ | 75 | -e TTS_MODEL_PATH=/app/piper \ |
| 76 | -e PIPER_ESPEAKNG_DATA_DIRECTORY=/usr/share \ | ||
| 76 | -v "$PWD/data:/app/data" \ | 77 | -v "$PWD/data:/app/data" \ |
| 77 | -v "$PWD/piper:/app/piper:ro" \ | 78 | -v "$PWD/piper:/app/piper:ro" \ |
| 78 | senator-sperma | 79 | senator-sperma |
| @@ -86,14 +87,29 @@ Notes: | |||
| 86 | overrides `TTS_MODEL_PATH` to `/app/piper`, so the absolute host path in | 87 | overrides `TTS_MODEL_PATH` to `/app/piper`, so the absolute host path in |
| 87 | `.env` is harmless. Put the `.onnx` and `.onnx.json` files there (the same | 88 | `.env` is harmless. Put the `.onnx` and `.onnx.json` files there (the same |
| 88 | directory used for local runs). | 89 | directory used for local runs). |
| 90 | - The espeak-ng data used by Piper is baked into the image at | ||
| 91 | `/usr/share/espeak-ng-data`; the container sets | ||
| 92 | `PIPER_ESPEAKNG_DATA_DIRECTORY=/usr/share`. This overrides the host path in | ||
| 93 | `.env`. | ||
| 89 | - The container runs as uid 1000. Make sure `data/` is writable by that uid. | 94 | - The container runs as uid 1000. Make sure `data/` is writable by that uid. |
| 90 | - `TZ` defaults to `Europe/Warsaw` for cron jobs; override it in | 95 | - `TZ` defaults to `Europe/Warsaw` for cron jobs; override it in |
| 91 | `compose.yaml` or with `-e TZ=...` if needed. | 96 | `compose.yaml` or with `-e TZ=...` if needed. |
| 92 | - If Copyparty runs on the host, use `http://host.docker.internal:<port>` in | 97 | - If Copyparty or a local LLM runs on the host, use the host's LAN IP (for |
| 93 | `.env` and enable the commented `extra_hosts` entry in `compose.yaml`. | 98 | example `http://10.0.0.3:2137/...`) and make sure the service listens on |
| 99 | `0.0.0.0` or that LAN IP, not only `127.0.0.1`. `localhost` inside the | ||
| 100 | container is the container itself. | ||
| 101 | - After editing `.env`, recreate the container: | ||
| 102 | `docker compose up -d --force-recreate`. `docker compose restart` does not | ||
| 103 | reload `env_file` values. Verify what the container actually sees with: | ||
| 104 | `docker compose exec senator-sperma env | grep -E 'LLM_API_URL|TTS_MODEL_PATH|PIPER_ESPEAKNG'` | ||
| 105 | and check the resolved LLM URL with `docker compose logs | grep 'LLM client'`. | ||
| 106 | - To test the LLM endpoint from inside the container: | ||
| 107 | `docker compose exec senator-sperma python3 -c 'import urllib.request; print(urllib.request.urlopen("http://10.0.0.3:2137/v1/models", timeout=5).status)'` | ||
| 108 | If that fails, the host service is probably listening only on `127.0.0.1`; | ||
| 109 | bind it to `0.0.0.0` (or the LAN IP) and allow the port through the firewall. | ||
| 94 | - If the build fails to resolve `crates.io` or `deb.debian.org`, run | 110 | - If the build fails to resolve `crates.io` or `deb.debian.org`, run |
| 95 | `docker build --network=host -t senator-sperma .`; `compose.yaml` already | 111 | `docker buildx build --load --network=host -t senator-sperma:latest .`; |
| 96 | sets `network: host` for builds on this host. | 112 | `compose.yaml` already sets `network: host` for builds on this host. |
| 97 | - yt-dlp is fetched from the latest upstream release at image build time. | 113 | - yt-dlp is fetched from the latest upstream release at image build time. |
| 98 | Rebuild with `--no-cache` (`docker compose build --no-cache`) to refresh it. | 114 | Rebuild with `--no-cache` (`docker compose build --no-cache`) to refresh it. |
| 99 | 115 | ||
diff --git a/compose.yaml b/compose.yaml index eac326f..68b7cae 100644 --- a/compose.yaml +++ b/compose.yaml | |||
| @@ -16,6 +16,8 @@ services: | |||
| 16 | # The host .env points TTS_MODEL_PATH at an absolute host path, which | 16 | # The host .env points TTS_MODEL_PATH at an absolute host path, which |
| 17 | # does not exist inside the container. The model is mounted below. | 17 | # does not exist inside the container. The model is mounted below. |
| 18 | TTS_MODEL_PATH: /app/piper | 18 | TTS_MODEL_PATH: /app/piper |
| 19 | # espeak-ng data is baked into the image by the Dockerfile. | ||
| 20 | PIPER_ESPEAKNG_DATA_DIRECTORY: /usr/share | ||
| 19 | # Override if the bot should not use Polish local time for cron jobs. | 21 | # Override if the bot should not use Polish local time for cron jobs. |
| 20 | # TZ: Europe/Warsaw | 22 | # TZ: Europe/Warsaw |
| 21 | volumes: | 23 | volumes: |
diff --git a/src/copyparty/mod.rs b/src/copyparty/mod.rs index a9f7d01..d3620b2 100644 --- a/src/copyparty/mod.rs +++ b/src/copyparty/mod.rs | |||
| @@ -147,8 +147,9 @@ pub struct UploadResponse { | |||
| 147 | impl CopypartyClient { | 147 | impl CopypartyClient { |
| 148 | pub fn new(base_url: String, upload_url: String, password: Option<String>) -> Self { | 148 | pub fn new(base_url: String, upload_url: String, password: Option<String>) -> Self { |
| 149 | Self { | 149 | Self { |
| 150 | base_url, | 150 | // A trailing slash would produce `//` when a path is appended. |
| 151 | upload_url, | 151 | base_url: base_url.trim_end_matches('/').to_string(), |
| 152 | upload_url: upload_url.trim_end_matches('/').to_string(), | ||
| 152 | password, | 153 | password, |
| 153 | client: Client::new(), | 154 | client: Client::new(), |
| 154 | } | 155 | } |
