# CAPTION.Ninja + Caption Local Self-hosted live captioning: [CAPTION.Ninja](https://github.com/steveseguin/captionninja)'s capture/editor/overlay pages and private caption relay, plus [Caption Local](https://github.com/steveseguin/caption-local) Whisper speech-to-text. Nothing is sent to caption.ninja, Google or any cloud STT provider. * **Category**: Apps * **Status**: 1, alpha * **Image**: [`git.autonomic.zone/recipe-maintainers/caption-ninja-*`](https://git.autonomic.zone/recipe-maintainers/-/packages), 1, built from upstream source (see [Images](#images)) * **Healthcheck**: Yes * **Backups**: No (no user data; the model volume is a cache) * **Email**: No * **Tests**: 3 * **SSO**: No ## Services | Service | Hostname | What | | --- | --- | --- | | `app` | `$DOMAIN` | nginx serving the caption.ninja pages | | `relay` | `wss://$DOMAIN/relay` | private caption relay (Node, `ws`), rooms `source` and `output` | | `stt` | `$STT_DOMAIN` | Caption Local (faster-whisper, CPU), also serves its own capture page | Captions flow: **capture** (microphone → STT) publishes to room `source` → **editor** reviews and forwards to room `output` → **overlay** (OBS browser source / audience). ## Basic usage 1. Set up Docker Swarm, [`abra`][abra] and [`coop-cloud/traefik`][cc-traefik] 2. Make the images available on the server (see [Images](#images)) 3. `abra app new caption-ninja --secrets` 4. `abra app config YOURAPPDOMAIN`: set `DOMAIN` and `STT_DOMAIN` (both need DNS). The STT host needs RAM for the model: about 1.5 GB for `small`. Set `CAPTION_THREADS` to at most your core count. 5. `abra app deploy YOURAPPDOMAIN`. On first start `stt` downloads the Whisper model into the `models` volume, so it can take several minutes to turn healthy. ## Using it Read the tokens (keep them private; read tokens are for viewers, write tokens publish): ``` abra app run YOURAPPDOMAIN relay -- sh -c 'for s in src_read src_write out_read out_write; do echo "$s=$(cat /run/secrets/$s)"; done' abra app run YOURAPPDOMAIN stt -- cat /run/secrets/stt_api_key ``` 1. Open `https://$STT_DOMAIN/`, enter the `stt_api_key` service token, then pick a microphone. 2. Under **Send captions to caption.ninja → Use a private relay**, enter: relay `wss://$DOMAIN/relay`, caption website `https://$DOMAIN/`, source room `source` + `src_write` token, editor output room `output`. Enable sharing. 3. Open the generated **editor** link and enter `src_read` and `out_write`. 4. In the editor, create the view-only OBS/overlay link with `out_read`. Alternatively use `https://$DOMAIN/capture-local.html` with service address `https://$STT_DOMAIN`. **Note:** the other caption.ninja pages (`index.html`, `translate.html`, …) still default to the public relay `wss://api.caption.ninja`. Only `capture-local.html`, `editor.html` and `overlay.html` support the private relay. ## Images Upstream publishes no Docker images, so `images/build.sh` builds them from pinned upstream commits. It needs Docker, git and no registry credentials: ``` scp -r images SERVER:caption-ninja-build/ ssh SERVER 'bash caption-ninja-build/images/build.sh' ``` The images are tagged with their registry names, so a single-node swarm can deploy straight from the local tags. To publish them to the registry from a machine that has `docker login git.autonomic.zone`: ``` for img in $(ssh SERVER "docker images --format '{{.Repository}}:{{.Tag}}' | grep caption-ninja-"); do ssh SERVER docker save "$img" | docker load && docker push "$img" done ``` To upgrade, bump the commits in `images/build.sh` and the tags in `compose.yml`. ## Security notes * Caption Local only accepts `Host: localhost` (upstream expects an SSH tunnel), so Traefik rewrites the Host header for `$STT_DOMAIN`; `stt` is still only reachable through Traefik, and every API call needs the token. * The STT token is a single shared token, not per-user auth. Anyone with it can use your CPU for transcription. Rotate it with `abra app secret` + redeploy. * Relay origins are `https://$DOMAIN` and `https://$STT_DOMAIN`; add more exact origins with `RELAY_EXTRA_ORIGINS` (e.g. `https://steveseguin.github.io`). * To revoke relay tokens, generate new secret versions and redeploy (restart disconnects all clients and clears the 2-minute replay buffer). [abra]: https://git.coopcloud.tech/coop-cloud/abra [cc-traefik]: https://git.coopcloud.tech/coop-cloud/traefik