From 4fcdb2e225c3bb3d7ea2560a1ce5e5cc42ef8576 Mon Sep 17 00:00:00 2001 From: trav Date: Tue, 13 Oct 2026 14:49:42 -0700 Subject: [PATCH] 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. --- .env.sample | 9 +++++++ README.md | 37 +++++++++++++++++++---------- abra.sh | 3 +-- compose.yml | 27 +++++++++++++-------- images/build.sh | 55 ++++++++++++++++++------------------------- images/web/Dockerfile | 7 ------ nginx.conf | 15 ------------ stt-entrypoint.sh | 15 ++++++++++++ 8 files changed, 89 insertions(+), 79 deletions(-) delete mode 100644 images/web/Dockerfile delete mode 100644 nginx.conf diff --git a/.env.sample b/.env.sample index 8977198..b504b8b 100644 --- a/.env.sample +++ b/.env.sample @@ -32,6 +32,15 @@ SECRET_STT_API_KEY_VERSION=v1 # length=48 ## Metadata-only request logs (no audio/text/tokens) #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) #RELAY_EXTRA_ORIGINS= #STT_EXTRA_ORIGINS= diff --git a/README.md b/README.md index 12db061..cfd672a 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # 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. +Self-hosted live captioning from [captions.coop/captions-deploy](https://git.coopcloud.tech/captions.coop/captions-deploy): +the captions.coop forks of [CAPTION.Ninja](https://github.com/steveseguin/captionninja) +(capture/editor/overlay pages, private caption relay, local Bergamot translation) +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. * **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)) +* **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 * **Backups**: No (no user data; the model volume is a cache) * **Email**: No @@ -20,7 +21,7 @@ Nothing is sent to caption.ninja, Google or any cloud STT provider. | 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` | | `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. 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. + `app` downloads the translation models (~660 MB, or the pairs in + `TRANSLATION_MODELS`) in the background; translation works once that finishes. ## 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 ``` +`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. -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 - `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`. + `source` + `src_write` token, editor output room `output`, then enable sharing. +2. Open the generated **editor** link and enter `src_read` and `out_write` + (`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 `https://$STT_DOMAIN`. @@ -62,8 +71,10 @@ default to the public relay `wss://api.caption.ninja`. Only `capture-local.html` ## Images -Upstream publishes no Docker images, so `images/build.sh` builds them from pinned -upstream commits. It needs Docker, git and no registry credentials: +Nobody publishes Docker images, so `images/build.sh` builds all three from a pinned +[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/ @@ -80,7 +91,7 @@ for img in $(ssh SERVER "docker images --format '{{.Repository}}:{{.Tag}}' | gre 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 diff --git a/abra.sh b/abra.sh index a25380e..d0c3937 100644 --- a/abra.sh +++ b/abra.sh @@ -1,3 +1,2 @@ -export NGINX_CONF_VERSION=v1 export RELAY_ENTRYPOINT_VERSION=v1 -export STT_ENTRYPOINT_VERSION=v2 +export STT_ENTRYPOINT_VERSION=v3 diff --git a/compose.yml b/compose.yml index b92cbd4..58296db 100644 --- a/compose.yml +++ b/compose.yml @@ -3,12 +3,16 @@ version: "3.8" services: 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: - proxy - configs: - - source: nginx_conf - target: /etc/nginx/conf.d/default.conf + environment: + - RELAY_URL=wss://${DOMAIN}/relay + - CAPTURE_URL=https://${STT_DOMAIN}/ + - RELAY_UPSTREAM=${STACK_NAME}_relay:8787 + - TRANSLATION_MODELS=${TRANSLATION_MODELS:-all} + volumes: + - translation_models:/usr/share/nginx/models healthcheck: test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/"] interval: 30s @@ -25,10 +29,10 @@ services: - "traefik.http.routers.${STACK_NAME}.entrypoints=web-secure" - "traefik.http.routers.${STACK_NAME}.tls.certresolver=${LETS_ENCRYPT_ENV}" - "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: - 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: - proxy environment: @@ -67,7 +71,7 @@ services: - "traefik.http.services.${STACK_NAME}_relay.loadbalancer.server.port=8787" 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: - proxy environment: @@ -83,8 +87,13 @@ services: - CAPTION_DEVICE=cpu - CAPTION_OFFLINE=${CAPTION_OFFLINE:-0} - CAPTION_LOG_REQUESTS=${CAPTION_LOG_REQUESTS:-0} + - RELAY_AUTOFILL=${RELAY_AUTOFILL:-source} secrets: - stt_api_key + - src_read + - src_write + - out_read + - out_write configs: - source: stt_entrypoint target: /stt-entrypoint.sh @@ -135,9 +144,6 @@ secrets: external: true configs: - nginx_conf: - name: ${STACK_NAME}_nginx_conf_${NGINX_CONF_VERSION} - file: nginx.conf relay_entrypoint: name: ${STACK_NAME}_relay_entrypoint_${RELAY_ENTRYPOINT_VERSION} file: relay-entrypoint.sh @@ -151,3 +157,4 @@ networks: volumes: models: + translation_models: diff --git a/images/build.sh b/images/build.sh index ac82c7b..afe9179 100755 --- a/images/build.sh +++ b/images/build.sh @@ -1,46 +1,37 @@ #!/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 -# Docker host (it needs no registry credentials); the images are tagged with -# their final registry names so a single-node swarm can deploy them directly. +# captions-deploy (git.coopcloud.tech/captions.coop/captions-deploy) pins the +# captions.coop forks of captionninja and caption-local as submodules and owns +# 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. # # usage: images/build.sh [workdir] set -euo pipefail REGISTRY=git.autonomic.zone/recipe-maintainers -CAPTIONNINJA_COMMIT=dfcee8f551a2b175740ab9f73e3787e1458cce2d -CAPTION_LOCAL_COMMIT=7d759fe1eae100ccdb5de0eea82438c941b26582 -RELAY_VERSION=0.2.0 # captionninja relay/package.json -CAPTION_LOCAL_VERSION=1.1.0 # caption-local VERSION -WEB_VERSION=2026.09.07 # captionninja has no releases: date of CAPTIONNINJA_COMMIT +DEPLOY_REPO=https://git.coopcloud.tech/captions.coop/captions-deploy.git +DEPLOY_COMMIT=12fe0fade41ca65e1dda3704eb4fd9e204fde698 +VERSION=2026.10.13 # date of DEPLOY_COMMIT; images share one semver-like tag -here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" -work="${1:-$(mktemp -d)}" +work="${1:-$(mktemp -d)}/captions-deploy" +tag="$VERSION-${DEPLOY_COMMIT:0:7}" -checkout() { # url commit dir - rm -rf "$3" - git init -q "$3" - git -C "$3" fetch -q --depth 1 "$1" "$2" - git -C "$3" checkout -q FETCH_HEAD -} +rm -rf "$work" +git init -q "$work" +git -C "$work" fetch -q --depth 1 "$DEPLOY_REPO" "$DEPLOY_COMMIT" +git -C "$work" 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" -checkout https://github.com/steveseguin/caption-local "$CAPTION_LOCAL_COMMIT" "$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" +docker build -t "$REGISTRY/caption-ninja-web:$tag" -f "$work/web/Dockerfile" "$work" +docker build -t "$REGISTRY/caption-ninja-relay:$tag" "$work/captionninja/relay" +docker build -t "$REGISTRY/caption-ninja-stt:$tag" "$work/caption-local" echo echo "Built:" -echo " $REGISTRY/caption-ninja-web:$WEB_VERSION-$cn" -echo " $REGISTRY/caption-ninja-relay:$RELAY_VERSION-$cn" -echo " $REGISTRY/caption-ninja-stt:$CAPTION_LOCAL_VERSION-$cl" +for image in web relay stt; do echo " $REGISTRY/caption-ninja-$image:$tag"; done diff --git a/images/web/Dockerfile b/images/web/Dockerfile deleted file mode 100644 index fe1d3b3..0000000 --- a/images/web/Dockerfile +++ /dev/null @@ -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 diff --git a/nginx.conf b/nginx.conf deleted file mode 100644 index ef6d310..0000000 --- a/nginx.conf +++ /dev/null @@ -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; -} diff --git a/stt-entrypoint.sh b/stt-entrypoint.sh index 1435049..d53fefd 100755 --- a/stt-entrypoint.sh +++ b/stt-entrypoint.sh @@ -13,4 +13,19 @@ origins="https://${DOMAIN},https://${STT_DOMAIN}" [ -n "$STT_EXTRA_ORIGINS" ] && origins="${origins},${STT_EXTRA_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 "$@"