feat: build from captions.coop/captions-deploy, translation models, relay auto-fill

- images/build.sh builds all three images from one pinned captions-deploy
  commit (submodules pin the captions.coop forks; web/Dockerfile is the UI).
- app: UI image configured by env (landing page, site-config.js), translation
  models downloaded into a volume on start (TRANSLATION_MODELS).
- stt: RELAY_AUTOFILL=source|all|off feeds the capture page's /relay-config.
- Drops the recipe's own nginx.conf and web Dockerfile.
This commit is contained in:
2026-10-13 14:49:42 -07:00
parent 4eec2598a8
commit 4fcdb2e225
8 changed files with 89 additions and 79 deletions
+9
View File
@@ -32,6 +32,15 @@ SECRET_STT_API_KEY_VERSION=v1 # length=48
## Metadata-only request logs (no audio/text/tokens) ## Metadata-only request logs (no audio/text/tokens)
#CAPTION_LOG_REQUESTS=0 #CAPTION_LOG_REQUESTS=0
## Translation models served to browsers (downloaded on start, SHA-256 verified):
## "all" (~660 MB), a list like "enes,esen", or "none"
#TRANSLATION_MODELS=all
## Capture-page relay auto-fill: source (default; capture-room tokens only),
## all (also editor output tokens: service-token holders can then publish to the
## audience room without review), or off
#RELAY_AUTOFILL=source
## Extra exact origins (comma separated, e.g. https://steveseguin.github.io) ## Extra exact origins (comma separated, e.g. https://steveseguin.github.io)
#RELAY_EXTRA_ORIGINS= #RELAY_EXTRA_ORIGINS=
#STT_EXTRA_ORIGINS= #STT_EXTRA_ORIGINS=
+24 -13
View File
@@ -1,14 +1,15 @@
# CAPTION.Ninja + Caption Local # CAPTION.Ninja + Caption Local
Self-hosted live captioning: [CAPTION.Ninja](https://github.com/steveseguin/captionninja)'s Self-hosted live captioning from [captions.coop/captions-deploy](https://git.coopcloud.tech/captions.coop/captions-deploy):
capture/editor/overlay pages and private caption relay, plus the captions.coop forks of [CAPTION.Ninja](https://github.com/steveseguin/captionninja)
[Caption Local](https://github.com/steveseguin/caption-local) Whisper speech-to-text. (capture/editor/overlay pages, private caption relay, local Bergamot translation)
Nothing is sent to caption.ninja, Google or any cloud STT provider. and [Caption Local](https://github.com/steveseguin/caption-local) (Whisper speech-to-text).
Nothing is sent to caption.ninja, Google or any cloud STT/translation provider.
<!-- metadata --> <!-- metadata -->
* **Category**: Apps * **Category**: Apps
* **Status**: 1, alpha * **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)) * **Image**: [`git.autonomic.zone/recipe-maintainers/caption-ninja-*`](https://git.autonomic.zone/recipe-maintainers/-/packages), 1, built from captions-deploy (see [Images](#images))
* **Healthcheck**: Yes * **Healthcheck**: Yes
* **Backups**: No (no user data; the model volume is a cache) * **Backups**: No (no user data; the model volume is a cache)
* **Email**: No * **Email**: No
@@ -20,7 +21,7 @@ Nothing is sent to caption.ninja, Google or any cloud STT provider.
| Service | Hostname | What | | Service | Hostname | What |
| --- | --- | --- | | --- | --- | --- |
| `app` | `$DOMAIN` | nginx serving the caption.ninja pages | | `app` | `$DOMAIN` | nginx serving the caption.ninja pages, landing page at `/`, translation models at `/models` |
| `relay` | `wss://$DOMAIN/relay` | private caption relay (Node, `ws`), rooms `source` and `output` | | `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 | | `stt` | `$STT_DOMAIN` | Caption Local (faster-whisper, CPU), also serves its own capture page |
@@ -36,6 +37,8 @@ reviews and forwards to room `output` → **overlay** (OBS browser source / audi
The STT host needs RAM for the model: about 1.5 GB for `small`. Set `CAPTION_THREADS` to at most your core count. 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 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. model into the `models` volume, so it can take several minutes to turn healthy.
`app` downloads the translation models (~660 MB, or the pairs in
`TRANSLATION_MODELS`) in the background; translation works once that finishes.
## Using it ## Using it
@@ -46,12 +49,18 @@ abra app run YOURAPPDOMAIN relay -- sh -c 'for s in src_read src_write out_read
abra app run YOURAPPDOMAIN stt -- cat /run/secrets/stt_api_key abra app run YOURAPPDOMAIN stt -- cat /run/secrets/stt_api_key
``` ```
`https://$DOMAIN/` is a landing page linking to everything below.
1. Open `https://$STT_DOMAIN/`, enter the `stt_api_key` service token, then pick a microphone. 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: With `RELAY_AUTOFILL` (default `source`) the page fills in the private relay,
source room and publishing token itself and turns sharing on. With
`RELAY_AUTOFILL=off`, open **Send captions → Use a private relay** and enter
relay `wss://$DOMAIN/relay`, caption website `https://$DOMAIN/`, source room relay `wss://$DOMAIN/relay`, caption website `https://$DOMAIN/`, source room
`source` + `src_write` token, editor output room `output`. Enable sharing. `source` + `src_write` token, editor output room `output`, then enable sharing.
3. Open the generated **editor** link and enter `src_read` and `out_write`. 2. 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`. (`RELAY_AUTOFILL=all` puts them in the link, at the cost of giving every
service-token holder the audience room's publishing token).
3. In the editor, create the view-only OBS/overlay link with `out_read`.
Alternatively use `https://$DOMAIN/capture-local.html` with service address Alternatively use `https://$DOMAIN/capture-local.html` with service address
`https://$STT_DOMAIN`. `https://$STT_DOMAIN`.
@@ -62,8 +71,10 @@ default to the public relay `wss://api.caption.ninja`. Only `capture-local.html`
## Images ## Images
Upstream publishes no Docker images, so `images/build.sh` builds them from pinned Nobody publishes Docker images, so `images/build.sh` builds all three from a pinned
upstream commits. It needs Docker, git and no registry credentials: [captions-deploy](https://git.coopcloud.tech/captions.coop/captions-deploy) commit
(its submodules pin the forks; `web/Dockerfile` is the UI image). It needs
Docker, git and no registry credentials:
``` ```
scp -r images SERVER:caption-ninja-build/ scp -r images SERVER:caption-ninja-build/
@@ -80,7 +91,7 @@ for img in $(ssh SERVER "docker images --format '{{.Repository}}:{{.Tag}}' | gre
done done
``` ```
To upgrade, bump the commits in `images/build.sh` and the tags in `compose.yml`. To upgrade, bump `DEPLOY_COMMIT`/`VERSION` in `images/build.sh`, rebuild, and update the tags in `compose.yml`.
## Security notes ## Security notes
+1 -2
View File
@@ -1,3 +1,2 @@
export NGINX_CONF_VERSION=v1
export RELAY_ENTRYPOINT_VERSION=v1 export RELAY_ENTRYPOINT_VERSION=v1
export STT_ENTRYPOINT_VERSION=v2 export STT_ENTRYPOINT_VERSION=v3
+17 -10
View File
@@ -3,12 +3,16 @@ version: "3.8"
services: services:
app: app:
image: git.autonomic.zone/recipe-maintainers/caption-ninja-web:2026.09.07-dfcee8f image: git.autonomic.zone/recipe-maintainers/caption-ninja-web:2026.10.13-12fe0fa
networks: networks:
- proxy - proxy
configs: environment:
- source: nginx_conf - RELAY_URL=wss://${DOMAIN}/relay
target: /etc/nginx/conf.d/default.conf - CAPTURE_URL=https://${STT_DOMAIN}/
- RELAY_UPSTREAM=${STACK_NAME}_relay:8787
- TRANSLATION_MODELS=${TRANSLATION_MODELS:-all}
volumes:
- translation_models:/usr/share/nginx/models
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/"] test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/"]
interval: 30s interval: 30s
@@ -25,10 +29,10 @@ services:
- "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure"
- "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}"
- "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=80" - "traefik.http.services.${STACK_NAME}.loadbalancer.server.port=80"
- "coop-cloud.${STACK_NAME}.version=0.1.0+v2026.09.07" - "coop-cloud.${STACK_NAME}.version=0.2.0+v2026.10.13"
relay: relay:
image: git.autonomic.zone/recipe-maintainers/caption-ninja-relay:0.2.0-dfcee8f image: git.autonomic.zone/recipe-maintainers/caption-ninja-relay:2026.10.13-12fe0fa
networks: networks:
- proxy - proxy
environment: environment:
@@ -67,7 +71,7 @@ services:
- "traefik.http.services.${STACK_NAME}_relay.loadbalancer.server.port=8787" - "traefik.http.services.${STACK_NAME}_relay.loadbalancer.server.port=8787"
stt: stt:
image: git.autonomic.zone/recipe-maintainers/caption-ninja-stt:1.1.0-7d759fe image: git.autonomic.zone/recipe-maintainers/caption-ninja-stt:2026.10.13-12fe0fa
networks: networks:
- proxy - proxy
environment: environment:
@@ -83,8 +87,13 @@ services:
- CAPTION_DEVICE=cpu - CAPTION_DEVICE=cpu
- CAPTION_OFFLINE=${CAPTION_OFFLINE:-0} - CAPTION_OFFLINE=${CAPTION_OFFLINE:-0}
- CAPTION_LOG_REQUESTS=${CAPTION_LOG_REQUESTS:-0} - CAPTION_LOG_REQUESTS=${CAPTION_LOG_REQUESTS:-0}
- RELAY_AUTOFILL=${RELAY_AUTOFILL:-source}
secrets: secrets:
- stt_api_key - stt_api_key
- src_read
- src_write
- out_read
- out_write
configs: configs:
- source: stt_entrypoint - source: stt_entrypoint
target: /stt-entrypoint.sh target: /stt-entrypoint.sh
@@ -135,9 +144,6 @@ secrets:
external: true external: true
configs: configs:
nginx_conf:
name: ${STACK_NAME}_nginx_conf_${NGINX_CONF_VERSION}
file: nginx.conf
relay_entrypoint: relay_entrypoint:
name: ${STACK_NAME}_relay_entrypoint_${RELAY_ENTRYPOINT_VERSION} name: ${STACK_NAME}_relay_entrypoint_${RELAY_ENTRYPOINT_VERSION}
file: relay-entrypoint.sh file: relay-entrypoint.sh
@@ -151,3 +157,4 @@ networks:
volumes: volumes:
models: models:
translation_models:
+23 -32
View File
@@ -1,46 +1,37 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Build the three caption-ninja images from pinned upstream commits. # Build the three caption-ninja images from a pinned captions-deploy commit.
# #
# Upstream publishes no images, so we build them ourselves. Run this on a # captions-deploy (git.coopcloud.tech/captions.coop/captions-deploy) pins the
# Docker host (it needs no registry credentials); the images are tagged with # captions.coop forks of captionninja and caption-local as submodules and owns
# their final registry names so a single-node swarm can deploy them directly. # the UI image (web/Dockerfile). Nobody publishes images, so we build them here.
# Run this on a Docker host; it needs no registry credentials. Images are tagged
# with their final registry names so a single-node swarm can deploy them directly.
# See README.md "Images" for pushing them to the registry afterwards. # See README.md "Images" for pushing them to the registry afterwards.
# #
# usage: images/build.sh [workdir] # usage: images/build.sh [workdir]
set -euo pipefail set -euo pipefail
REGISTRY=git.autonomic.zone/recipe-maintainers REGISTRY=git.autonomic.zone/recipe-maintainers
CAPTIONNINJA_COMMIT=dfcee8f551a2b175740ab9f73e3787e1458cce2d DEPLOY_REPO=https://git.coopcloud.tech/captions.coop/captions-deploy.git
CAPTION_LOCAL_COMMIT=7d759fe1eae100ccdb5de0eea82438c941b26582 DEPLOY_COMMIT=12fe0fade41ca65e1dda3704eb4fd9e204fde698
RELAY_VERSION=0.2.0 # captionninja relay/package.json VERSION=2026.10.13 # date of DEPLOY_COMMIT; images share one semver-like tag
CAPTION_LOCAL_VERSION=1.1.0 # caption-local VERSION
WEB_VERSION=2026.09.07 # captionninja has no releases: date of CAPTIONNINJA_COMMIT
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" work="${1:-$(mktemp -d)}/captions-deploy"
work="${1:-$(mktemp -d)}" tag="$VERSION-${DEPLOY_COMMIT:0:7}"
checkout() { # url commit dir rm -rf "$work"
rm -rf "$3" git init -q "$work"
git init -q "$3" git -C "$work" fetch -q --depth 1 "$DEPLOY_REPO" "$DEPLOY_COMMIT"
git -C "$3" fetch -q --depth 1 "$1" "$2" git -C "$work" checkout -q FETCH_HEAD
git -C "$3" checkout -q FETCH_HEAD # Submodule URLs are SSH; fetch them anonymously over HTTPS instead.
} git -C "$work" -c url."https://git.coopcloud.tech/".insteadOf="ssh://git@git.coopcloud.tech:2222/" \
submodule update -q --init --depth 1
git -C "$work" submodule status
checkout https://github.com/steveseguin/captionninja "$CAPTIONNINJA_COMMIT" "$work/captionninja" docker build -t "$REGISTRY/caption-ninja-web:$tag" -f "$work/web/Dockerfile" "$work"
checkout https://github.com/steveseguin/caption-local "$CAPTION_LOCAL_COMMIT" "$work/caption-local" docker build -t "$REGISTRY/caption-ninja-relay:$tag" "$work/captionninja/relay"
docker build -t "$REGISTRY/caption-ninja-stt:$tag" "$work/caption-local"
cn="${CAPTIONNINJA_COMMIT:0:7}"
cl="${CAPTION_LOCAL_COMMIT:0:7}"
docker build -t "$REGISTRY/caption-ninja-web:$WEB_VERSION-$cn" \
-f "$here/web/Dockerfile" "$work/captionninja"
docker build -t "$REGISTRY/caption-ninja-relay:$RELAY_VERSION-$cn" \
"$work/captionninja/relay"
docker build -t "$REGISTRY/caption-ninja-stt:$CAPTION_LOCAL_VERSION-$cl" \
"$work/caption-local"
echo echo
echo "Built:" echo "Built:"
echo " $REGISTRY/caption-ninja-web:$WEB_VERSION-$cn" for image in web relay stt; do echo " $REGISTRY/caption-ninja-$image:$tag"; done
echo " $REGISTRY/caption-ninja-relay:$RELAY_VERSION-$cn"
echo " $REGISTRY/caption-ninja-stt:$CAPTION_LOCAL_VERSION-$cl"
-7
View File
@@ -1,7 +0,0 @@
# Static caption.ninja pages. Build context: a captionninja checkout.
FROM alpine:3.22 AS src
COPY . /src
RUN cd /src && rm -rf .git .github relay scripts *.md test-security-warning.html
FROM nginx:1.31.6-alpine
COPY --from=src /src /usr/share/nginx/html
-15
View File
@@ -1,15 +0,0 @@
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# caption.ninja links omit the extension (e.g. /overlay?room=...).
location / {
try_files $uri $uri.html $uri/ =404;
}
# Room tokens can arrive in query strings on some pages; keep them out of logs.
access_log off;
}
+15
View File
@@ -13,4 +13,19 @@ origins="https://${DOMAIN},https://${STT_DOMAIN}"
[ -n "$STT_EXTRA_ORIGINS" ] && origins="${origins},${STT_EXTRA_ORIGINS}" [ -n "$STT_EXTRA_ORIGINS" ] && origins="${origins},${STT_EXTRA_ORIGINS}"
export CAPTION_ALLOWED_ORIGINS="$origins" export CAPTION_ALLOWED_ORIGINS="$origins"
# Relay auto-fill: the capture page fetches these from /relay-config (behind the
# service token). "source" shares only the capture room's tokens; "all" also
# shares the editor output tokens, letting service-token holders publish to the
# audience room without review; "off" disables it.
secret() { cat "/run/secrets/$1"; }
case "${RELAY_AUTOFILL:-source}" in
source|all)
export RELAY_ADDRESS="wss://${DOMAIN}/relay" RELAY_SITE="https://${DOMAIN}/"
export RELAY_SOURCE_WRITE="$(secret src_write)" RELAY_SOURCE_READ="$(secret src_read)"
if [ "$RELAY_AUTOFILL" = "all" ]; then
export RELAY_OUTPUT_WRITE="$(secret out_write)" RELAY_OUTPUT_READ="$(secret out_read)"
fi
;;
esac
exec python server.py --host 0.0.0.0 "$@" exec python server.py --host 0.0.0.0 "$@"