Docker er driftsformen, ikke bare en testrigg

Announcer skal kjøre standalone på klienten som er koblet til anlegget, i
Docker – aldri på utviklingsmaskina. install.sh installerte nativt med apt
og uv, og det passer ikke lenger.

- install.sh er nå en Docker-bootstrap: installerer Docker om den mangler,
  lager .env med riktig bruker-ID og portbinding, oppretter data/, bygger,
  starter, og setter ufw. Den rydder også bort gamle native enheter, både
  announcer.service og sg-announcer.service.
- Dockerfile kopierer koden inn istedenfor å låne den fra et bind-mount, så
  det som kjører i drift er uavhengig av hva som ligger i mappa.
- Alt som skal overleve en ny container ligger i data/ på verten. Bind-mount
  og ikke navngitt volum, med vilje: da kan man lese
  data/logs/announcements.jsonl og slippe egne jingler i data/chimes/ uten å
  gå inn i containeren, og eierskapet følger brukeren istedenfor root.
- Testriggen er flyttet til compose.test.yaml, med eget prosjektnavn, så den
  ikke kan forveksles med eller forstyrre drift.

Lyden ut av containeren krever tre ting, og de er dokumentert i AGENTS.md:
hele XDG_RUNTIME_DIR montert inn (ikke bare pipewire-socketen – PulseAudio
trenger pulse/native og en cookie i samme mappe, og da virker begge
lydservere), user: PUID:PGID lik eieren av socketen, og en innlogget grafisk
økt. Containeren starter ved boot, altså før innlogging, så avspilling feiler
til noen logger inn – det retter seg selv, siden player_cmd() slås opp per
melding. Verifisert: lyden spiller fra driftscontaineren.

Brannmuren er den viktige endringen fra nativ tid. Dockers publiserte porter
settes opp i nat-tabellen, før ufws regler, så en ufw-regel på porten gjør
ingenting. Tilgangen styres nå av BIND_IP i .env, og install.sh setter den til
NetBird-adressen når wt0 finnes. ufw settes fortsatt for ssh, men er ikke
involvert i tilgangen til announcer.

Testriggen bindes nå til 127.0.0.1. Den sto med PIN 1234 publisert på
0.0.0.0, og gikk altså forbi ufw ut på LAN-et.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kent 2026-10-08 16:07:59 +02:00
parent 6aaa98d8b1
commit 6c1253f54c
7 changed files with 372 additions and 177 deletions

34
.env.example Normal file
View file

@ -0,0 +1,34 @@
# Kopier til .env. install.sh lager den for deg med riktige verdier.
#
# cp .env.example .env
# Bruker-ID som eier lydsocketen på verten. Stemmer ikke denne, kommer det
# ingen lyd. Finn den med: id -u ; id -g
PUID=1000
PGID=1000
XDG_RUNTIME_DIR=/run/user/1000
# Hvilken adresse sida skal svare på. Dette er det som avgrenser tilgang:
# Docker publiserer forbi ufw, så en ufw-regel hjelper ikke her.
# 0.0.0.0 alle nett maskina er på
# 100.75.x.x bare NetBird
# 127.0.0.1 bare maskina selv
BIND_IP=0.0.0.0
ANNOUNCER_PORT=8090
# Lydutgang. Tom = systemets standard. Finn navn med: pactl list short sinks
ANNOUNCER_SINK=
# Overstyrer tittelen uten en config.toml.
ANNOUNCER_TITLE=
# Full URL utenfra. Trengs bare for OIDC bak en omvendt proxy.
ANNOUNCER_PUBLIC_URL=
# --- hemmeligheter -------------------------------------------------------
# Tom = av. Se config.example.toml og docs/crew-integration.md.
ANNOUNCER_PIN=
ANNOUNCER_JWT_SECRET=
ANNOUNCER_OIDC_CLIENT_SECRET=
# Tom = genereres og lagres i data/instance/secret_key.
ANNOUNCER_SECRET_KEY=

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ instance/
.docker-data/ .docker-data/
# onnxruntime slenger fra seg denne når telemetri-ID ikke kan lagres # onnxruntime slenger fra seg denne når telemetri-ID ikke kan lagres
:memory:.ses :memory:.ses
data/
.env

122
AGENTS.md
View file

@ -22,45 +22,98 @@ Filer:
| `config.example.toml` | kommentert mal; kopier til `config.toml` | | `config.example.toml` | kommentert mal; kopier til `config.toml` |
| `docs/crew-integration.md` | kontrakten mot crew-panelet | | `docs/crew-integration.md` | kontrakten mot crew-panelet |
| `tests/test_auth.py` | `uv run pytest` | | `tests/test_auth.py` | `uv run pytest` |
| `compose.yaml` + `Dockerfile` | testoppsett, se under. Ikke produksjon | | `Dockerfile` + `compose.yaml` | **drift** – appen kjøres i Docker |
| `.env.example` | bruker-ID, portbinding og hemmeligheter |
| `compose.test.yaml` | testoppsett med dummy-IdP, se under |
| `docker/dummy_idp.py` | falsk crew-panel, så OIDC kan prøves | | `docker/dummy_idp.py` | falsk crew-panel, så OIDC kan prøves |
## Kjøring ## Kjøring
Tjenesten er en systemd-brukerenhet: `~/.config/systemd/user/announcer.service` **Announcer kjører i Docker, på en egen klient som er koblet til
(enabled, port 8090). `systemctl --user restart announcer` etter endringer i høyttaleranlegget – aldri på kitsune.** Kitsune er utviklingsmaskin; der er
koden. Manuelt: `uv run announcer.py --port 8090`. det bare koden som bor.
Ny maskin (Debian/Ubuntu/Mint): `git clone https://git.1x1.no/kent/announcer.git` Ny klient (Debian/Ubuntu/Mint):
og `./install.sh` gjør alt (apt, uv, Python 3.13, stemmer, enhet, ufw). Finnes
NetBird (`wt0`) skrur det på ufw med deny incoming og åpner 22, 22022 og porten
kun på `wt0`. Kan kjøres på nytt, og rydder da bort en gammel
`sg-announcer.service` hvis den ligger der. Avspilling bruker `pw-play` når
PipeWire kjører, ellers `paplay` (`player_cmd()`), så Mint 21 med PulseAudio
virker også.
### Omdøpingen: ett punkt gjenstår ```bash
git clone https://git.1x1.no/kent/announcer.git
cd announcer && ./install.sh
```
Mappa er flyttet til `~/Work/announcer`, enheten heter `announcer.service`, og `install.sh` installerer Docker om den mangler, lager `.env` med riktig
remote peker på `https://git.1x1.no/kent/announcer.git`. **Men repoet i Forgejo bruker-ID og portbinding, oppretter `data/`, bygger bildet, starter
heter `annuoncer`** – en skrivefeil. Rett navnet i Forgejo, så virker remote; containeren og setter ufw. Den rydder også bort en eventuell gammel nativ
Forgejo legger selv inn en omdirigering fra det gamle navnet. `announcer.service`/`sg-announcer.service`. Kan kjøres på nytt; en `.env` som
finnes blir ikke overskrevet.
Flytter du mappa igjen: `.venv` har absolutte stier i `pyvenv.cfg` og i Drift:
shebangen til hvert konsollskript, så `uv sync` alene er ikke nok. Da må
`rm -rf .venv && uv sync` til, og tjenesten startes om.
```bash
docker compose logs -f # følg loggen
docker compose restart # etter endringer i data/config.toml
docker compose up -d --build # etter endringer i koden
cat data/logs/announcements.jsonl # hvem som sendte hva
```
Alt som skal overleve en ny container ligger i `data/` på verten
(bind-mount, ikke navngitt volum, så du kan lese loggen og slippe egne
jingler i `data/chimes/` uten å gå inn i containeren):
| | |
|---|---|
| `data/voices/` | Piper-modellene, ~670 MB, lastes ned ved første oppstart |
| `data/cache/` | ferdige WAV-er etter innholdshash. Trygt å slette |
| `data/chimes/` | egne jinglefiler |
| `data/logs/` | revisjonsloggen |
| `data/instance/` | sesjonsnøkkelen |
| `data/config.toml` | valgfri. Uten den er sida åpen |
### Lyden ut av containeren
Det eneste virkelig uvanlige i oppsettet. Tre ting må stemme:
1. Hele vertens `XDG_RUNTIME_DIR` monteres inn på samme sti. Ikke bare
`pipewire-0`: PulseAudio bruker `pulse/native` pluss en cookie i samme
mappe, og ved å ta hele mappa virker begge lydservere uten at compose må
vite hvilken maskina har. `player_cmd()` velger `pw-play` eller `paplay`.
2. `user: "${PUID}:${PGID}"` må være brukeren som **eier** socketen på
verten. Stemmer ikke ID-en, kommer det ingen lyd. `install.sh` fyller inn
`id -u`/`id -g`.
3. Maskina må være logget inn grafisk, ellers finnes socketen ikke.
Containeren starter ved boot, altså før innlogging, og avspilling feiler
til noen logger inn. Det retter seg selv: `player_cmd()` slås opp per
melding, så første utrop etter innlogging virker uten omstart. Derfor
anbefaler `install.sh` automatisk innlogging.
### Brannmur: BIND_IP, ikke ufw
**Dockers publiserte porter går forbi ufw.** De settes opp i nat-tabellen,
før ufws regler, så `ufw deny` på porten gjør ingenting. Tilgangen styres
derfor av `BIND_IP` i `.env`:
| | |
|---|---|
| `0.0.0.0` | alle nett maskina er på |
| `100.75.x.x` | bare NetBird. `install.sh` velger dette når `wt0` finnes |
| `127.0.0.1` | bare maskina selv |
ufw settes fortsatt for maskinens øvrige porter (ssh på `wt0`), men den er
ikke involvert i tilgangen til announcer. Dette er en endring fra den native
tida, da porten måtte åpnes eksplisitt i ufw.
## Testing i Docker ## Testing i Docker
`docker compose up --build` gir to containere: `docker compose -f compose.test.yaml up --build` gir to containere:
| | | | | |
|---|---| |---|---|
| `http://localhost:8091` | announcer med PIN, JWT **og** OIDC slått på (`docker/config.test.toml`) | | `http://localhost:8091` | announcer med PIN, JWT **og** OIDC slått på (`docker/config.test.toml`) |
| `http://localhost:9000` | `docker/dummy_idp.py`, som later som den er crew-panelet | | `http://localhost:9000` | `docker/dummy_idp.py`, som later som den er crew-panelet |
Port 8091 med vilje, så den ikke kolliderer med tjenesten på 8090. Egen fil, eget prosjektnavn og port 8091, så den ikke kan forveksles med
eller forstyrre den som kjører i drift. Begge porter er bundet til
`127.0.0.1`, så testriggen – som har PIN `1234` – aldri ligger åpen på
LAN-et.
Dummy-IdP-en finnes fordi crew-panelet ikke har OIDC ennå: uten den kan Dummy-IdP-en finnes fordi crew-panelet ikke har OIDC ennå: uten den kan
OIDC-veien ikke prøves i det hele tatt. Den kjører en ekte OIDC-veien ikke prøves i det hele tatt. Den kjører en ekte
@ -80,9 +133,11 @@ Lyd: `/run/user/1000` er montert inn, så `pw-play` i containeren spiller på
vertens anlegg. Kommenter ut den linja i `compose.yaml` hvis du bare skal vertens anlegg. Kommenter ut den linja i `compose.yaml` hvis du bare skal
teste innlogging. teste innlogging.
`UV_PROJECT_ENVIRONMENT=/opt/venv` i bildet er ikke pynt: `/app` er et `compose.test.yaml` bind-monterer koden, så endringer slår inn med en
bind-mount av repoet, og uten det ville containerens `uv sync` overskrevet restart. Driftsbildet kopierer koden inn i stedet, så det som kjører er
`.venv` som den kjørende tjenesten bruker. uavhengig av hva som ligger i mappa. `UV_PROJECT_ENVIRONMENT=/opt/venv` er
derfor ikke pynt: med bind-mount ville containerens `uv sync` ellers
overskrevet `.venv` på verten.
## Konfigurasjon ## Konfigurasjon
@ -138,6 +193,14 @@ Uten den logges alle ut ved hver `systemctl restart`. `SESSION_COOKIE_SECURE`
settes bare når `server.behind_tls` er sann – appen kjører på rå HTTP over LAN settes bare når `server.behind_tls` er sann – appen kjører på rå HTTP over LAN
og NetBird, og med Secure på ville cookien aldri blitt sendt. og NetBird, og med Secure på ville cookien aldri blitt sendt.
## Utvikling på kitsune
`uv run announcer.py --port 8099` kjører appen direkte, uten Docker. Det er
veien for å prøve en endring kjapt; `uv run pytest` for testene. Men
**ingen tjeneste skal stå og kjøre på kitsune** – det var slik det var før, og
det er ryddet bort. Skal noe prøves med innlogging, bruk
`compose.test.yaml`.
## Hvordan det henger sammen ## Hvordan det henger sammen
- `/api/config` er alt sida trenger for å tegne seg selv: branding, stemmer, - `/api/config` er alt sida trenger for å tegne seg selv: branding, stemmer,
@ -187,10 +250,15 @@ Vil Kent ha enda bedre lyd: ElevenLabs (nøkkel, best norsk) eller OpenAI
## Feller ## Feller
- **ufw**: appen kjører nativt, så Docker-smutthullet gjelder ikke. Porten må - **ufw styrer ikke Docker-porter.** Se «Brannmur» over: tilgangen avgrenses
åpnes eksplisitt for LAN-et (`sudo ufw allow from 10.20.70.0/23 to any port 8090 proto tcp`). med `BIND_IP` i `.env`. En `ufw deny` på porten gir falsk trygghet.
- **Ingen lyd?** Nesten alltid `PUID`/`PGID` i `.env` som ikke er brukeren
som eier lydsocketen, eller at maskina ikke er logget inn grafisk. Sjekk
hva containeren velger:
`docker compose exec announcer python -c "import announcer,pathlib;print(announcer.player_cmd(pathlib.Path('/tmp/x.wav')))"`
- `pkill -f announcer.py` fra et skall som selv har `announcer.py` i - `pkill -f announcer.py` fra et skall som selv har `announcer.py` i
kommandolinja dreper skallet. Bruk `systemctl --user`. kommandolinja dreper skallet. Gjelder utvikling med `uv run`; i drift er
det `docker compose`.
- `player = Player()` står på modulnivå, så avspillingstråden starter ved - `player = Player()` står på modulnivå, så avspillingstråden starter ved
`import announcer` – også i `install.sh`, som importerer modulen for å laste `import announcer` – også i `install.sh`, som importerer modulen for å laste
ned stemmer. Harmløst, men greit å vite. ned stemmer. Harmløst, men greit å vite.

View file

@ -1,23 +1,20 @@
# Testbilde for announcer. Ikke ment for produksjon – på en ekte maskin # Announcer kjøres i Docker på klienten som er koblet til høyttaleranlegget.
# kjører appen nativt med install.sh, fordi lyden skal ut på maskinens egen
# lydutgang og det er vondt å få til gjennom et containerlag.
# #
# Her er poenget å kunne prøve innlogging (PIN, JWT, OIDC) uten å røre # Lyden er det som gjør bildet litt uvanlig: containeren må nå vertens
# tjenesten som kjører, og uten sudo eller brannmurendringer. # lydserver. Det løses i compose.yaml ved å montere inn vertens
# XDG_RUNTIME_DIR og kjøre som samme bruker-ID som eier socketen – ikke her.
FROM python:3.13-slim FROM python:3.13-slim
# ffmpeg trengs av edge_pcm() og av egne jinglefiler. pw-play/paplay er bare # ffmpeg: edge_pcm() dekoder MP3 med den, og egne jinglefiler normaliseres
# nyttig hvis vertens PipeWire-socket monteres inn; uten den feiler selve # med den. pipewire-bin gir pw-play, pulseaudio-utils gir paplay; koden
# avspillingen, men resten av appen virker. # velger selv hvilken som finnes (player_cmd()), så begge lydservere virker.
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends ffmpeg ca-certificates curl \ && apt-get install -y --no-install-recommends \
&& (apt-get install -y --no-install-recommends pipewire-bin pulseaudio-utils || true) \ ffmpeg ca-certificates pipewire-bin pulseaudio-utils \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
# Venv utenfor /app: /app er et bind-mount av repoet, og en .venv bygget her
# ville ellers overskrevet den verten bruker til den kjørende tjenesten.
ENV UV_PROJECT_ENVIRONMENT=/opt/venv \ ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
UV_COMPILE_BYTECODE=1 \ UV_COMPILE_BYTECODE=1 \
PATH=/opt/venv/bin:$PATH \ PATH=/opt/venv/bin:$PATH \
@ -25,7 +22,20 @@ ENV UV_PROJECT_ENVIRONMENT=/opt/venv \
WORKDIR /app WORKDIR /app
COPY pyproject.toml uv.lock ./ COPY pyproject.toml uv.lock ./
RUN uv sync --frozen RUN uv sync --frozen --no-dev
# Koden sist, så avhengighetslaget gjenbrukes når bare appen endres.
COPY announcer.py auth.py config.py index.html ./
# Alt som skal overleve en ny container ligger under /data, som monteres
# inn fra verten. Stemmene er ~670 MB og lastes ned ved første oppstart.
ENV ANNOUNCER_VOICES=/data/voices \
ANNOUNCER_CACHE=/data/cache \
ANNOUNCER_CHIMES=/data/chimes \
ANNOUNCER_LOG=/data/logs/announcements.jsonl \
ANNOUNCER_STATE=/data/instance \
ANNOUNCER_CONFIG=/data/config.toml \
ANNOUNCER_PORT=8090
EXPOSE 8090 EXPOSE 8090
CMD ["python", "announcer.py", "--host", "0.0.0.0", "--port", "8090"] CMD ["python", "announcer.py", "--host", "0.0.0.0"]

47
compose.test.yaml Normal file
View file

@ -0,0 +1,47 @@
# Testoppsett, ikke drift:
#
# docker compose -f compose.test.yaml up --build
#
# http://localhost:8091 announcer med PIN, JWT og OIDC slått på
# http://localhost:9000 dummy-IdP som står for crew-panelet
#
# Egen fil og eget prosjektnavn, så den ikke kan forveksles med eller
# forstyrre den som kjører i drift. Porten er 8091 av samme grunn.
# Koden bind-monteres her, så endringer slår inn med en restart.
name: announcer-test
services:
announcer:
build: .
ports: ["127.0.0.1:8091:8090"]
user: "${PUID:-1000}:${PGID:-1000}"
environment:
ANNOUNCER_CONFIG: /app/docker/config.test.toml
ANNOUNCER_CACHE: /data/cache
ANNOUNCER_LOG: /data/logs/announcements.jsonl
ANNOUNCER_STATE: /data/instance
ANNOUNCER_VOICES: /data/voices
ANNOUNCER_CHIMES: /data/chimes
ANNOUNCER_JWT_SECRET: test-hemmelighet-kun-for-docker-ikke-bruk-ekte
ANNOUNCER_OIDC_CLIENT_SECRET: test-client-secret-kun-for-docker
ANNOUNCER_PUBLIC_URL: http://localhost:8091
XDG_RUNTIME_DIR: ${XDG_RUNTIME_DIR:-/run/user/1000}
volumes:
- .:/app
- ./data:/data
- ${XDG_RUNTIME_DIR:-/run/user/1000}:${XDG_RUNTIME_DIR:-/run/user/1000}
depends_on: [idp]
idp:
build: .
ports: ["127.0.0.1:9000:9000"]
user: "${PUID:-1000}:${PGID:-1000}"
environment:
IDP_ISSUER: http://localhost:9000
IDP_CLIENT_ID: announcer
IDP_CLIENT_SECRET: test-client-secret-kun-for-docker
ANNOUNCER_JWT_SECRET: test-hemmelighet-kun-for-docker-ikke-bruk-ekte
ANNOUNCER_PUBLIC_URL: http://localhost:8091
volumes:
- .:/app
command: ["python", "docker/dummy_idp.py"]

View file

@ -1,45 +1,53 @@
# Testoppsett, ikke produksjon: docker compose up --build # Announcer i drift på klienten ved anlegget.
# #
# http://localhost:8091 announcer (PIN + JWT + OIDC slått på) # ./install.sh setter opp alt, inkludert .env
# http://localhost:9000 dummy-IdP som later som den er crew-panelet # docker compose up -d senere, for hånd
# #
# Porten er 8091 med vilje, så den ikke kolliderer med tjenesten på 8090. # Testoppsettet med dummy-IdP ligger i compose.test.yaml, ikke her.
name: announcer-test name: announcer
services: services:
announcer: announcer:
build: . build: .
ports: ["8091:8090"] image: announcer:local
user: "1000:1000" restart: unless-stopped
environment:
ANNOUNCER_CONFIG: /app/docker/config.test.toml # BIND_IP er sikkerhetsmekanismen her, ikke ufw. Dockers publiserte
# Buffer, logg og sesjonsnøkkel holdes utenfor repoet, så testing ikke # porter går forbi ufw, så en ufw-regel ville ikke stengt noen ute.
# blander seg med det den kjørende tjenesten eier. # install.sh setter den til NetBird-adressen når wt0 finnes.
ANNOUNCER_CACHE: /data/cache ports:
ANNOUNCER_LOG: /data/logs/announcements.jsonl - "${BIND_IP:-0.0.0.0}:${ANNOUNCER_PORT:-8090}:8090"
ANNOUNCER_STATE: /data/instance
ANNOUNCER_VOICES: /app/voices # Må være brukeren som eier lydsocketen på verten, ellers kommer det
ANNOUNCER_JWT_SECRET: test-hemmelighet-kun-for-docker-ikke-bruk-ekte # ingen lyd. install.sh fyller inn id -u / id -g.
ANNOUNCER_OIDC_CLIENT_SECRET: test-client-secret-kun-for-docker user: "${PUID:-1000}:${PGID:-1000}"
# Må peke dit nettleseren ser appen, ellers blir OIDC-redirecten feil.
ANNOUNCER_PUBLIC_URL: http://localhost:8091
XDG_RUNTIME_DIR: /run/user/1000
volumes:
- .:/app
- ./.docker-data:/data
# Lyd: gir containeren vertens PipeWire. Kommenter ut hvis du bare
# skal teste innlogging og UI – da feiler avspilling, men ikke annet.
- /run/user/1000:/run/user/1000
depends_on: [idp]
idp:
build: .
ports: ["9000:9000"]
user: "1000:1000"
environment: environment:
IDP_ISSUER: http://localhost:9000 ANNOUNCER_SINK: ${ANNOUNCER_SINK:-}
IDP_CLIENT_ID: announcer ANNOUNCER_TITLE: ${ANNOUNCER_TITLE:-}
IDP_CLIENT_SECRET: test-client-secret-kun-for-docker ANNOUNCER_PUBLIC_URL: ${ANNOUNCER_PUBLIC_URL:-}
# Hemmeligheter: hører i .env, ikke i config.toml.
ANNOUNCER_PIN: ${ANNOUNCER_PIN:-}
ANNOUNCER_JWT_SECRET: ${ANNOUNCER_JWT_SECRET:-}
ANNOUNCER_OIDC_CLIENT_SECRET: ${ANNOUNCER_OIDC_CLIENT_SECRET:-}
ANNOUNCER_SECRET_KEY: ${ANNOUNCER_SECRET_KEY:-}
XDG_RUNTIME_DIR: ${XDG_RUNTIME_DIR:-/run/user/1000}
volumes: volumes:
- .:/app # Stemmer, buffer, logg, sesjonsnøkkel, egne jingler og config.toml.
command: ["python", "docker/dummy_idp.py"] - ./data:/data
# Lyden. Hele XDG_RUNTIME_DIR, ikke bare én socket, så både
# PipeWire (pipewire-0) og PulseAudio (pulse/native + cookie) virker.
- ${XDG_RUNTIME_DIR:-/run/user/1000}:${XDG_RUNTIME_DIR:-/run/user/1000}
# Containeren starter ved boot, altså før noen er logget inn grafisk.
# Da finnes ikke lydsocketen ennå, og avspilling feiler. Det retter seg
# selv: player_cmd() slås opp per melding, så første utrop etter
# innlogging virker uten at noe må startes om.
healthcheck:
test: ["CMD", "python", "-c",
"import urllib.request;urllib.request.urlopen('http://127.0.0.1:8090/api/config',timeout=5)"]
interval: 30s
timeout: 10s
start_period: 180s # første oppstart laster ned ~670 MB stemmer
retries: 3

View file

@ -1,15 +1,20 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Installerer Announcer på en Debian/Ubuntu-maskin (testet mot Linux Mint). # Setter opp Announcer på klienten som er koblet til høyttaleranlegget.
# Debian/Ubuntu/Mint. Appen kjører i Docker; dette skriptet ordner Docker,
# .env, datamapper, brannmur og autostart.
# #
# git clone https://git.1x1.no/kent/announcer.git # git clone https://git.1x1.no/kent/announcer.git
# cd announcer && ./install.sh # cd announcer && ./install.sh
# #
# Kjør som vanlig bruker (ikke sudo); skriptet spør om passord der det trengs. # Kjør som vanlig bruker (ikke sudo); skriptet spør om passord der det trengs.
# Valgfritt: ANNOUNCER_PIN=1234 ./install.sh -> sida krever PIN # Valgfritt:
# ANNOUNCER_PORT=8090 ./install.sh -> annen port # ANNOUNCER_PIN=1234 ./install.sh sida krever PIN
# ANNOUNCER_TITLE="Utrop" ./install.sh # ANNOUNCER_PORT=8090 ./install.sh annen port
# Resten av konfigurasjonen hører hjemme i config.toml, se config.example.toml. # ANNOUNCER_TITLE="Utrop" ./install.sh annen tittel
# Kan kjøres på nytt; da oppdateres avhengigheter og tjenesten startes om. # BIND_IP=127.0.0.1 ./install.sh bare maskina selv
#
# Kan kjøres på nytt; da bygges bildet om og containeren startes om. En .env
# som finnes blir ikke overskrevet.
set -euo pipefail set -euo pipefail
if [[ $EUID -eq 0 ]]; then if [[ $EUID -eq 0 ]]; then
@ -17,129 +22,150 @@ if [[ $EUID -eq 0 ]]; then
exit 1 exit 1
fi fi
APP="announcer"
OLD_APP="sg-announcer" # het dette før; enheten ryddes bort nedenfor
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PORT="${ANNOUNCER_PORT:-8090}" cd "$REPO_DIR"
PIN="${ANNOUNCER_PIN:-}"
TITLE="${ANNOUNCER_TITLE:-}"
UNIT_DIR="$HOME/.config/systemd/user"
UNIT="$UNIT_DIR/$APP.service"
step() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } step() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
warn() { printf '\033[33m! %s\033[0m\n' "$*"; }
step "Systempakker (apt)" # --------------------------------------------------------------------------
sudo apt-get update -qq step "Rydder bort eventuell nativ installasjon"
sudo apt-get install -y -qq git curl ca-certificates ffmpeg pulseaudio-utils # Tidligere versjoner kjørte appen direkte som en systemd-brukerenhet. To
# pw-play finnes i pipewire-bin på Mint 22 / Ubuntu 24.04. På eldre (PulseAudio) # instanser ville slåss om porten, så den gamle skal bort.
# faller appen tilbake til paplay, så dette er ikke kritisk. for old in sg-announcer announcer; do
sudo apt-get install -y -qq pipewire-bin 2>/dev/null || true unit="$HOME/.config/systemd/user/$old.service"
if [[ -f "$unit" ]]; then
step "uv (Python-pakkehåndterer)" systemctl --user disable --now "$old" >/dev/null 2>&1 || true
if command -v uv >/dev/null 2>&1; then rm -f "$unit"
UV="$(command -v uv)" echo "fjernet $old.service (nativ)"
elif [[ -x "$HOME/.local/bin/uv" ]]; then
UV="$HOME/.local/bin/uv"
else
curl -LsSf https://astral.sh/uv/install.sh | sh
UV="$HOME/.local/bin/uv"
fi fi
echo "uv: $UV ($("$UV" --version))" done
systemctl --user daemon-reload 2>/dev/null || true
step "Python 3.13 og avhengigheter" # --------------------------------------------------------------------------
cd "$REPO_DIR" step "Docker"
"$UV" sync if ! command -v docker >/dev/null 2>&1; then
mkdir -p cache chimes voices logs instance echo "installerer Docker …"
curl -fsSL https://get.docker.com | sudo sh
fi
if ! id -nG "$USER" | tr ' ' '\n' | grep -qx docker; then
sudo usermod -aG docker "$USER"
warn "La $USER i docker-gruppa. Du må logge ut og inn igjen,"
warn "og så kjøre ./install.sh på nytt. Stopper her."
exit 0
fi
if ! docker info >/dev/null 2>&1; then
warn "Docker svarer ikke. Er tjenesten startet? sudo systemctl enable --now docker"
exit 1
fi
docker --version
docker compose version | head -1
step "Laster ned Piper-stemmene (lokal reserve, ca. 600 MB, kun første gang)" # --------------------------------------------------------------------------
"$UV" run python -c "import logging, announcer; logging.basicConfig(level=logging.INFO, format='%(message)s'); announcer.ensure_voices()" step "Oppsett (.env)"
# BIND_IP er det som avgjør hvem som når sida. ufw gjør det ikke: Dockers
step "Rydder bort gammel tjeneste" # publiserte porter settes opp i nat-tabellen og går forbi ufws regler.
# Tjenesten het sg-announcer før. Uten dette slåss to enheter om porten. NB_IP="$(ip -4 addr show wt0 2>/dev/null | awk '/inet /{sub(/\/.*/,"",$2);print $2;exit}')"
if [[ -f "$UNIT_DIR/$OLD_APP.service" ]]; then if [[ -n "${BIND_IP:-}" ]]; then
systemctl --user disable --now "$OLD_APP" >/dev/null 2>&1 || true :
rm -f "$UNIT_DIR/$OLD_APP.service" elif [[ -n "$NB_IP" ]]; then
echo "fjernet $OLD_APP.service" BIND_IP="$NB_IP"
echo "NetBird funnet – binder til $BIND_IP, så bare VPN-et når sida."
else else
echo "ingenting å rydde." BIND_IP="0.0.0.0"
fi fi
step "systemd-brukertjeneste" if [[ -f .env ]]; then
mkdir -p "$UNIT_DIR" echo ".env finnes allerede, lar den stå. Slett den for å lage en ny."
{
cat <<UNIT
[Unit]
Description=Announcer – TTS-utrop på høyttaleranlegget
After=pipewire.service pulseaudio.service
[Service]
WorkingDirectory=$REPO_DIR
ExecStart=$UV run announcer.py
Environment=ANNOUNCER_PORT=$PORT
UNIT
if [[ -n "$PIN" ]]; then
echo "Environment=ANNOUNCER_PIN=$PIN"
else else
echo "# Environment=ANNOUNCER_PIN=1234" cat > .env <<ENV
# Laget av install.sh $(date +%Y-%m-%d). Se .env.example for forklaringer.
PUID=$(id -u)
PGID=$(id -g)
XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/$(id -u)}
BIND_IP=$BIND_IP
ANNOUNCER_PORT=${ANNOUNCER_PORT:-8090}
ANNOUNCER_SINK=${ANNOUNCER_SINK:-}
ANNOUNCER_TITLE=${ANNOUNCER_TITLE:-}
ANNOUNCER_PUBLIC_URL=${ANNOUNCER_PUBLIC_URL:-}
ANNOUNCER_PIN=${ANNOUNCER_PIN:-}
ANNOUNCER_JWT_SECRET=${ANNOUNCER_JWT_SECRET:-}
ANNOUNCER_OIDC_CLIENT_SECRET=${ANNOUNCER_OIDC_CLIENT_SECRET:-}
ANNOUNCER_SECRET_KEY=
ENV
chmod 600 .env # PIN og nøkler står i klartekst
echo "skrev .env (PUID=$(id -u), BIND_IP=$BIND_IP)"
fi fi
if [[ -n "$TITLE" ]]; then
echo "Environment=ANNOUNCER_TITLE=$TITLE" step "Datamapper"
else mkdir -p data/{voices,cache,chimes,logs,instance}
echo "# Environment=ANNOUNCER_TITLE=Utrop" [[ -f data/chimes/README.txt ]] || cp -n chimes/README.txt data/chimes/ 2>/dev/null || true
echo "data/ – stemmer, buffer, egne jingler, logg og sesjonsnøkkel"
if [[ ! -f data/config.toml ]]; then
echo "ingen data/config.toml: sida er åpen for alle som når den."
echo " vil du ha innlogging: cp config.example.toml data/config.toml"
fi fi
cat <<'UNIT'
# Environment=ANNOUNCER_SINK=alsa_output.xxx
# Hemmeligheter hører hjemme her og ikke i config.toml:
# Environment=ANNOUNCER_JWT_SECRET=...
# Environment=ANNOUNCER_OIDC_CLIENT_SECRET=...
Restart=on-failure
RestartSec=3
[Install] # --------------------------------------------------------------------------
WantedBy=default.target step "Bygger og starter"
UNIT docker compose up -d --build
} > "$UNIT"
chmod 600 "$UNIT" # PIN og nøkler står i klartekst her
systemctl --user daemon-reload
systemctl --user enable --now "$APP"
systemctl --user restart "$APP"
step "Venter på at tjenesten svarer"
# Første oppstart laster ned Piper-stemmene (~670 MB) før den svarer.
PORT="$(grep -E '^ANNOUNCER_PORT=' .env | cut -d= -f2)"
PROBE_IP="$BIND_IP"; [[ "$PROBE_IP" == "0.0.0.0" ]] && PROBE_IP=127.0.0.1
for i in $(seq 1 60); do
if curl -fsS -o /dev/null "http://$PROBE_IP:$PORT/api/config" 2>/dev/null; then
echo "svarer etter ~$((i*5)) s"
break
fi
[[ $i -eq 1 ]] && echo "laster ned stemmer, dette tar noen minutter første gang …"
[[ $i -eq 60 ]] && warn "svarte ikke innen 5 min. Se: docker compose logs -f"
sleep 5
done
# --------------------------------------------------------------------------
step "Brannmur" step "Brannmur"
if ip link show wt0 >/dev/null 2>&1; then # Merk at ufw *ikke* styrer tilgangen til announcer – det gjør BIND_IP i
# NetBird er installert: slipp inn kun via VPN-grensesnittet, blokker alt annet. # .env. ufw settes her bare for maskinens øvrige porter.
if [[ -n "$NB_IP" ]]; then
sudo ufw default deny incoming sudo ufw default deny incoming
sudo ufw default allow outgoing sudo ufw default allow outgoing
sudo ufw allow in on wt0 to any port 22 proto tcp comment "ssh via netbird" sudo ufw allow in on wt0 to any port 22 proto tcp comment "ssh via netbird"
sudo ufw allow in on wt0 to any port 22022 proto tcp comment "netbird ssh-server" sudo ufw allow in on wt0 to any port 22022 proto tcp comment "netbird ssh-server"
sudo ufw allow in on wt0 to any port "$PORT" proto tcp comment "$APP via netbird"
sudo ufw delete allow "$PORT/tcp" >/dev/null 2>&1 || true
sudo ufw --force enable sudo ufw --force enable
echo "ufw aktiv: port $PORT, 22 og 22022 er åpne kun på wt0 (NetBird)." echo "ufw aktiv: ssh kun på wt0."
echo "Vil du også nå sida fra lokalt LAN uten VPN:" echo "announcer er bundet til $BIND_IP:$PORT – det er NetBird-adressen,"
echo " sudo ufw allow from <lan-subnett> to any port $PORT proto tcp" echo "så sida nås bare over VPN-et. ufw er ikke involvert i det."
elif sudo ufw status 2>/dev/null | grep -q "Status: active"; then elif command -v ufw >/dev/null 2>&1 && sudo ufw status 2>/dev/null | grep -q "Status: active"; then
sudo ufw allow "$PORT/tcp" comment "$APP" warn "ufw er aktiv, men styrer ikke Docker-porter."
warn "Vil du begrense tilgangen: sett BIND_IP i .env og kjør docker compose up -d."
else else
echo "ufw er ikke aktiv, ingen regel nødvendig." echo "ufw er ikke aktiv."
[[ "$BIND_IP" == "0.0.0.0" ]] && warn "Sida er åpen på alle nett maskina er på."
fi fi
sleep 2 # --------------------------------------------------------------------------
IP="$(hostname -I 2>/dev/null | awk '{print $1}')" IP="$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{sub(/\/.*/,"",$2);print $2;exit}')"
step "Ferdig" step "Ferdig"
systemctl --user --no-pager status "$APP" | head -5 || true docker compose ps
cat <<MSG cat <<MSG
Åpne http://${IP:-<denne-maskinens-ip>}:$PORT fra hva som helst på nettet. Åpne http://${BIND_IP/0.0.0.0/${IP:-<denne-maskinens-ip>}}:$PORT
Uten config.toml er sida åpen for alle som når porten. Vil du ha innlogging: Lyden spilles i din innloggede økt, så maskinen må være logget inn grafisk.
cp config.example.toml config.toml og les kommentarene der. Containeren starter ved boot, altså før innlogging – da feiler avspilling,
Mot crew-panelet: docs/crew-integration.md men den retter seg selv ved første utrop etter at du er logget inn.
Tips for en laptop på arrangementet: Tips for en laptop på arrangementet:
- Lyden spilles i din innloggede økt, så maskinen må være logget inn. - Slå på automatisk innlogging (Innstillinger -> Påloggingsvindu).
Slå på automatisk innlogging (Innstillinger -> Påloggingsvindu).
- Hindre at den sover med lokket igjen: - Hindre at den sover med lokket igjen:
sudo sed -i 's/^#\?HandleLidSwitch=.*/HandleLidSwitch=ignore/' /etc/systemd/logind.conf sudo sed -i 's/^#\?HandleLidSwitch=.*/HandleLidSwitch=ignore/' /etc/systemd/logind.conf
sudo systemctl restart systemd-logind sudo systemctl restart systemd-logind
- Logg: journalctl --user -u $APP -f
Drift:
docker compose logs -f følg loggen
docker compose restart etter endringer i data/config.toml
docker compose up -d --build etter endringer i koden
cat data/logs/announcements.jsonl hvem som sendte hva
MSG MSG