second-brain/deploy/tower/README.md
Travis Herbranson 24055ad686 Merge branch 'claude/laughing-shirley-52f4a6': queue-page add form + tower CUDA-12 pin
Two commits since the last merge:

- web: add-to-queue form on /queue too, with per-page HTMX dispatch.
  Shared form partial reused by /dashboard and /queue; the POST
  /sources/add endpoint dispatches the response partial by inspecting
  HX-Current-URL so each page's own list/counts refresh inline. Same
  service call, same flash behavior.

- tower: pin CUDA-12 wheels + LD_LIBRARY_PATH wrapper for the systemd
  service. ctranslate2 4.7.2 needs CUDA 12 + cuDNN 9 but Arch ships
  CUDA 13, so we pin nvidia-cublas-cu12 + nvidia-cudnn-cu12>=9,<10 in
  the `tower` extra and add deploy/tower/run-transcribe-worker.sh,
  which resolves the venv's lib dirs at runtime and prepends them to
  LD_LIBRARY_PATH before exec-ing the worker. systemd doesn't inherit
  shell exports, so this is what makes the service survive reboots
  and Python upgrades. ExecStart= now points at the wrapper, and the
  StartLimit* directives moved into [Unit] where modern systemd
  expects them.

Travis's tower-side corrections on main (`User=trucktrav`,
`gitea-pbs-trucktrav` clone alias, tower WG IP 10.99.0.3) auto-merge
cleanly against these branch edits — different lines in the same files.
2026-05-25 15:43:47 -04:00

226 lines
8.0 KiB
Markdown

# Tower-side transcribe worker — deploy notes
Target host: **EndeavourOS tower** (Arch base), RTX 3080. Runs the
`second-brain transcribe-worker` daemon under systemd. Reaches the
petalbrain Postgres over WireGuard at `db.wg.herbylab.dev:5432`.
This worker only does **pull + transcribe** for VIDEO sources. Articles
and all extraction/embedding stay on the dev side.
---
## 1. System packages
```bash
# Build / runtime tooling.
sudo pacman -S --needed base-devel git ffmpeg python uv
# NVIDIA driver only — see the CUDA-12 gotcha below before installing
# the distro's `cuda` / `cudnn` packages.
sudo pacman -S --needed nvidia nvidia-utils
# Confirm the GPU is visible.
nvidia-smi
```
### ⚠ CUDA-12 gotcha — do NOT install pacman's `cuda` / `cudnn`
Arch / EndeavourOS now ships **CUDA 13** (`libcublas.so.13`). The
`ctranslate2` 4.7.x backend that `faster-whisper` 1.x rides on top of
wants **CUDA 12** (`libcublas.so.12`) + **cuDNN 9** (`libcudnn.so.9`)
and will not load against the system's CUDA-13 libs.
Rather than fight the distro, this project pins the CUDA-12 runtime as
pip wheels in the venv via the `tower` extra (`nvidia-cublas-cu12`,
`nvidia-cudnn-cu12>=9,<10`). The `uv sync --extra tower` step in §2
brings them in automatically; the systemd `ExecStart` wrapper
(`deploy/tower/run-transcribe-worker.sh`) prepends those wheel-bundled
`.so` directories to `LD_LIBRARY_PATH` at start time.
If you've already installed pacman's `cuda` / `cudnn`, you can leave
them — the wrapper's `LD_LIBRARY_PATH` prepend wins for this service.
yt-dlp comes in via the Python project (`uv sync` step below), so the
distro yt-dlp is *optional*. Leave it off the host unless you want it on
your CLI PATH.
---
## 2. Project checkout + venv
```bash
sudo mkdir -p /opt/projects/homelab
sudo chown trucktrav:trucktrav /opt/projects/homelab
cd /opt/projects/homelab
git clone gitea-pbs-trucktrav:petal-power/second-brain.git
cd second-brain
# Install the runtime deps + the `tower` extra (faster-whisper / CTranslate2).
# The dev side does NOT install --extra tower; only the tower needs it.
uv sync --extra tower
```
`uv sync --extra tower` will pull ~1 GB of pinned CUDA-12 wheels
(`nvidia-cublas-cu12`, `nvidia-cudnn-cu12`, plus `faster-whisper` /
`ctranslate2`). First-run model download (~3 GB for large-v3) lands
under `~/.cache/huggingface/` the first time the worker transcribes;
pre-warm if you want:
```bash
# Run the wrapper so the CUDA-12 lib paths are pinned exactly the way
# the systemd service will see them — this is the cheapest end-to-end
# sanity check that the venv + LD_LIBRARY_PATH plumbing is correct.
deploy/tower/run-transcribe-worker.sh --once
```
Or just instantiate the model directly:
```bash
LD_LIBRARY_PATH="$(uv run python -c 'import os, nvidia.cublas, nvidia.cudnn; print(":".join(os.path.join(os.path.dirname(m.__file__),"lib") for m in [nvidia.cublas, nvidia.cudnn]))')" \
uv run python -c "from faster_whisper import WhisperModel; WhisperModel('large-v3', device='cuda', compute_type='float16')"
```
---
## 3. WireGuard
The worker assumes the tunnel is already configured and that
`wg-quick@wg-lan.service` brings it up at boot. Topology:
| Peer | WG IP |
|------|-------|
| hub (herbydev) | `10.99.0.1` |
| tower | `10.99.0.3` |
`db.wg.herbylab.dev` resolves to `10.99.0.1`. Confirm connectivity from
the tower:
```bash
ping -c2 db.wg.herbylab.dev
nc -vz db.wg.herbylab.dev 5432
```
Tunnel setup itself is **out of scope for this README** — that's
configured separately as part of Travis's infra.
---
## 4. EnvironmentFile
systemd reads this BEFORE dropping to `User=herbyadmin`. The file is
**not** a shell init script — `export VAR=…` in your shell does not
apply here, and changes require `systemctl restart` to take effect.
There's a ready-to-scp file on **herbys-dev** with the real lovebug
password already populated, generated from
`/opt/backups/postgres-consolidation/credentials.env`:
```
/opt/backups/postgres-consolidation/second-brain-transcribe.env (mode 0600)
```
Pull it across instead of hand-copying the password:
```bash
# from the tower
scp herbys-dev:/opt/backups/postgres-consolidation/second-brain-transcribe.env /tmp/sb-env
sudo install -m 0600 -o root -g root /tmp/sb-env /etc/default/second-brain-transcribe
rm /tmp/sb-env
```
If you ever need to regenerate the dev-host file (e.g. after a password
rotation), the recipe is:
```bash
# on herbys-dev — never echoes the password, mode 0600
PW=$(grep ^LOVEBUG_PG_PASSWORD= /opt/backups/postgres-consolidation/credentials.env | cut -d= -f2-)
umask 077
echo "SECOND_BRAIN_DATABASE_URL=postgresql://lovebug:${PW}@db.wg.herbylab.dev:5432/petalbrain" \
> /opt/backups/postgres-consolidation/second-brain-transcribe.env
unset PW
```
The committed `deploy/tower/second-brain-transcribe.env.example`
documents the format and never carries a real secret.
---
## 5. systemd unit
```bash
sudo install -m 0644 deploy/tower/second-brain-transcribe.service \
/etc/systemd/system/second-brain-transcribe.service
sudo systemctl daemon-reload
sudo systemctl enable --now second-brain-transcribe.service
```
The unit declares `After=` + `Wants=` on `wg-quick@wg-lan.service`, so
the worker starts after the tunnel comes up at boot. If the tunnel
disappears mid-run the worker keeps polling and logs DB-unreachable
warnings; it doesn't crash-loop.
`ExecStart=` calls the in-repo `deploy/tower/run-transcribe-worker.sh`
wrapper, which resolves the venv's CUDA-12 lib dirs at runtime and
prepends them to `LD_LIBRARY_PATH` before exec-ing `uv run`. systemd
does NOT inherit your shell's `LD_LIBRARY_PATH`, so the wrapper is
how the CUDA-12 pin survives across reboots and python upgrades.
### Day-to-day commands
```bash
# What's it doing right now?
systemctl status second-brain-transcribe.service
journalctl -u second-brain-transcribe.service -f
# Pause without uninstalling — toggle from the dashboard at
# https://brain.herbylab.dev/settings (or whichever host).
# Or stop the unit:
sudo systemctl stop second-brain-transcribe.service
# Pick up code updates:
cd /opt/projects/homelab/second-brain
git pull
uv sync --extra tower
sudo systemctl restart second-brain-transcribe.service
```
---
## 6. Validation — what to confirm once WG is live
These can only be verified on the tower itself:
1. **WG reachability.** `nc -vz db.wg.herbylab.dev 5432` succeeds.
2. **DB auth.** `uv run alembic current` returns the latest revision id.
3. **GPU large-v3 path.** With a short test video queued via
`second-brain add <url>` on the dev side, run `uv run second-brain transcribe-worker --once`
on the tower. Watch `journalctl` — first run pulls the model from
HuggingFace (slow), subsequent runs are fast. The source row should
end up in `TRANSCRIBED` with `transcript_text` populated and
`claimed_by` cleared.
4. **Race safety smoke test.** Run two `--once` invocations in parallel
against an empty queue, then with one PENDING video. Only one should
claim it; the other should see no work.
5. **Settings round-trip.** Flip `transcription_enabled` off on the
dashboard; within one poll cycle (~15s) the worker logs
`transcription_enabled=false — sleeping`. Flip back on, the next
PENDING video gets picked up.
---
## Follow-ups (out of scope for this round)
- **Media + subtitles directory currently lives on the tower's local
disk** (under `~/.local/share/second-brain/`). Move to a NAS mount
later so the dev side can compile/inspect the SRTs without a
cross-machine fetch. Open question on the right mount point + cred
flow — see the project's `postgres-migration-planning.md` for the
related vault-location decision Travis still owes.
- **Tower-side observability.** No metrics endpoint yet; journald is
the only signal. A `/api/worker-status` heartbeat the dashboard could
read would be nice once two workers exist.
- **GPU concurrency > 1.** `transcription_max_concurrent_gpu_jobs`
exists in `pipeline_settings` but the worker only runs one job at a
time. Scale-out would need a per-job semaphore (or just running N
worker instances with distinct `SECOND_BRAIN_WORKER_ID`s).