Skip to content

Whisper-Server (Flurfunk) — Setup & Operations

Der prilog-wisper Server ist der zentrale Voice-to-Text-Dienst für alle Prilog-Tenants. Er läuft als einzelne Hetzner-Instanz im Tailnet und erreicht alle Kunden-Server (und das Backend) über stabile Tailscale-MagicDNS-Hostnamen. Audio wird zu keinem Zeitpunkt persistiert — der Server ist stateless im Sinne der Inhalte, hat aber einen Modell-Cache auf der lokalen Disk damit Restarts schnell sind.

Diese Seite ist das vollständige Howto für Provisionierung, Inbetriebnahme, Monitoring und Recovery.

Architektur in 30 Sekunden

                                    ┌──────────────────────┐
                                    │  prilog-wisper       │
                                    │  (Hetzner CCX33)     │
                                    │                      │
   ┌────────────┐                   │  Docker:             │
   │ Synapse    │                   │  faster-whisper-     │
   │ (Kunde 1)  │──┐                │  server :8000        │
   └────────────┘  │                │                      │
                   │                │  Modell:             │
   ┌────────────┐  │                │  large-v3 int8       │
   │ Synapse    │  │   Tailnet      │                      │
   │ (Kunde 2)  │──┼───────────────▶│  RAM: ~3 GB          │
   └────────────┘  │                │  Disk: ~3 GB Cache   │
                   │                └──────────────────────┘
   ┌────────────┐  │                          ▲
   │ Backend-API│──┘                          │
   │ (api.*)    │                             │
   └────────────┘                             │
        │                                     │
        │  POST /v1/audio/transcriptions      │
        └─────────────────────────────────────┘

Datenfluss pro Sprachnachricht:

  1. Mitarbeiter nimmt Audio im Web-Client auf, sendet als m.audio an Synapse
  2. Synapse persistiert das Event, der Prilog-Matrix-Connector-Hook (on_new_event) erkennt msgtype: m.audio und filtert
  3. Connector-Hook ruft POST https://api.prilog.chat/api/matrix-connector/transcribe-voice mit tenantKey, roomId, eventId, mxcUri
  4. Backend orchestriert: lädt Audio via Synapse-Admin-API, schickt sie an http://prilog-wisper:8000/v1/audio/transcriptions (Tailnet-Hostname!)
  5. Whisper transkribiert und antwortet mit {"text": "..."} — Latenz ca. 7s + 0.4s pro Audio-Sekunde (large-v3 int8 auf CCX33)
  6. Backend postet das Transkript als Reply-Message via Synapse-Admin-API in den Original-Raum, mit Custom-Feldern org.prilog.transcript_for und transcript_text
  7. Web-Client empfängt das Reply-Event via Sync, hängt das Transkript inline unter die Audio-Bubble

Wichtig: Schritt 4 nutzt den MagicDNS-Hostnamen prilog-wisper, nicht eine IP. Tailscale verteilt den Namen über das Tailnet, die IP kann sich ändern (selten, aber möglich), der Name bleibt stabil.

Hardware-Empfehlung

TierHetznervCPURAMDiskLatenz 30s AudioModell
EmpfohlenCCX334 dedicated EPYC32 GB240 GB~20 slarge-v3 int8
Budget-NotlösungCX312 shared8 GB80 GB~60 sdistil-large-v3
WachstumCCX438 dedicated EPYC64 GB360 GB~10 slarge-v3 int8
GPU-UpgradeGEX448 vCPU + RTX 400064 GB360 GB~3 slarge-v3 fp16

Bei < 100 aktiven Mitarbeitern reicht der CCX33 aus, bei 100–500 sollte man zum CCX43 wechseln, bei > 500 lohnt sich der GPU-Switch.

Burst vs. Average

Die Latenz-Werte oben sind single-request. Bei parallelen Aufnahmen wird der Server linear langsamer (single-thread pro Request). 5 parallele 30s-Aufnahmen auf einem CCX33 = ~100 s für die letzte. Das ist okay solange „peak hours" nicht voll mit Sprachnachrichten überfüllt sind.

Provisionierung Schritt für Schritt

Diese Anleitung beschreibt wie ein neuer Whisper-Server aufgesetzt wird. Brauchst du nur, wenn der bestehende ausfällt oder du ein Upgrade machen willst.

1. Hetzner-Server bestellen

  1. Hetzner Cloud Console → Add Server
  2. Location: nbg1 (Nürnberg) oder fsn1 (Falkenstein) — egal welche
  3. Image: Ubuntu 24.04
  4. Type: CCX33 (oder größer)
  5. Networking: nur IPv4 + IPv6 öffentlich, kein Firewall-Profil zuweisen (das machen wir mit ufw selbst)
  6. SSH Keys: lee-personal (oder welcher Key auch immer der Prilog-Operator nutzt)
  7. Name: prilog-wisper (oder bei Wachstum: prilog-wisper-2 etc.)
  8. Create

Notiere dir die öffentliche IP — du brauchst sie nur einmal, für den ersten SSH-Login.

2. Erstkonfiguration: User anlegen, SSH absichern

bash
ssh root@<public-ip>
adduser lee
usermod -aG sudo lee
echo 'lee ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/lee-nopasswd
chmod 440 /etc/sudoers.d/lee-nopasswd

# SSH-Key kopieren
mkdir -p /home/lee/.ssh && chmod 700 /home/lee/.ssh
cp /root/.ssh/authorized_keys /home/lee/.ssh/
chown -R lee:lee /home/lee/.ssh

3. Tailscale verbinden

Tailscale ist Pflicht — ohne ist der Server nicht im Prilog-Netz und nicht erreichbar.

bash
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --auth-key=<TAILSCALE_AUTH_KEY> --hostname=prilog-wisper
tailscale ip -4   # zeigt die zugeteilte 100.x.y.z Adresse

Auth-Key

Der Tailscale Auth-Key liegt in den Prilog-Secrets. Verwende einen ephemeral=false reusable=true Key. Hostname unbedingt prilog-wisper setzen — der Backend ruft den Server unter genau diesem Namen via MagicDNS.

Verifizieren: von einem anderen Tailnet-Mitglied (z.B. api.prilog.chat):

bash
ping prilog-wisper            # MagicDNS-Lookup sollte funktionieren

4. SSH-Konfig auf der Operator-Maschine

Auf deiner lokalen Maschine in ~/.ssh/config:

Host prilog-wisper
    User lee
    IdentityFile ~/.ssh/prilog
    IdentitiesOnly yes

Ab jetzt: ssh prilog-wisper — kein IP-Tippen mehr nötig.

5. UFW-Firewall einrichten

Wir lassen nur Tailnet-Traffic auf Port 8000 zu. Port 22 bleibt für SSH offen (öffentlich, da wir keinen Tailscale-only SSH-Zugang konfiguriert haben).

bash
ssh prilog-wisper
sudo apt-get install -y ufw
sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow in on tailscale0 to any port 22
sudo ufw allow in on tailscale0 to any port 8000
sudo ufw allow in 22/tcp
sudo ufw --force enable
sudo ufw status verbose

Erwartete Ausgabe:

22 on tailscale0           ALLOW IN    Anywhere
8000 on tailscale0         ALLOW IN    Anywhere
22/tcp                     ALLOW IN    Anywhere

6. Docker installieren

Wir nehmen die offizielle Docker-Repo, nicht das Ubuntu-Standard-Paket.

bash
sudo apt-get update -qq
sudo apt-get install -y -qq ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
    -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \
    | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null

sudo apt-get update -qq
sudo apt-get install -y -qq docker-ce docker-ce-cli containerd.io \
    docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker lee

docker --version           # Sollte 27.x oder neuer sein
docker compose version     # v2.30+ oder neuer

groupchange

Nach usermod -aG docker lee musst du die SSH-Session neu öffnen, sonst hat dein Shell die neue Gruppe noch nicht.

7. faster-whisper-server in Docker starten

bash
sudo mkdir -p /opt/whisper && sudo chown lee:lee /opt/whisper
mkdir -p /opt/whisper/cache

cat > /opt/whisper/docker-compose.yml <<'YAML'
services:
  whisper:
    image: fedirz/faster-whisper-server:latest-cpu
    container_name: prilog-whisper
    restart: unless-stopped
    network_mode: host
    environment:
      WHISPER__MODEL: Systran/faster-whisper-large-v3
      WHISPER__INFERENCE_DEVICE: cpu
      WHISPER__COMPUTE_TYPE: int8
      ENABLE_UI: 'false'
      LOG_LEVEL: info
      DEFAULT_LANGUAGE: de
    volumes:
      - /opt/whisper/cache:/root/.cache/huggingface
YAML

cd /opt/whisper
sudo docker compose up -d

Zur Erklärung der Settings:

  • network_mode: host — Container teilt sich den Netzwerk-Stack des Hosts. Lauscht direkt auf 0.0.0.0:8000. Zusammen mit der UFW-Regel kommt nur Tailnet-Traffic durch.
  • compute_type: int8 — INT8-Quantisierung. Halbiert den RAM-Verbrauch und läuft auf EPYC-AVX2-Cores fast genauso schnell wie fp16. Qualitativ fehlerfrei für Deutsch.
  • large-v3 — das beste Modell für deutsche Sprache. Distil-Varianten sind ~2x schneller, aber ~3% Erkennungsfehler — für Schul-Kontext nicht okay.
  • cache:/root/.cache/huggingface — der Modell-Download (ca. 3 GB) wird persistent gespeichert. Nach einem Container-Restart läuft er sofort wieder ohne Re-Download.

8. Modell-Download abwarten

Der erste Start lädt das Modell von HuggingFace. Das dauert je nach Verbindung 2–10 Minuten.

bash
sudo docker logs -f prilog-whisper

Du wirst sehen, wie das Modell heruntergeladen wird. Sobald folgendes erscheint, ist der Server bereit:

INFO:     Started server process [27]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000

Ctrl+C raus aus dem Log-Tail (der Container läuft weiter im Hintergrund).

9. Health-Check

Vom Whisper-Server selbst:

bash
curl http://localhost:8000/health
# OK

Von einem anderen Tailnet-Mitglied (z.B. der Operator-Maschine oder api.prilog.chat):

bash
curl http://prilog-wisper:8000/health
# OK

Wenn der zweite Aufruf 0K liefert, ist die Provisionierung abgeschlossen.

10. Smoke-Test mit echtem Audio

Bevor du den Server in Produktion meldest, mach einen echten Transkriptions-Test mit einer kurzen deutschen Sprachprobe. Beispiel:

bash
curl -X POST http://prilog-wisper:8000/v1/audio/transcriptions \
    -F "file=@test-5s.m4a" \
    -F "model=Systran/faster-whisper-large-v3" \
    -F "language=de" \
    -F "response_format=json"

Erwartete Ausgabe (Beispiel):

json
{"text":"Dies ist eine schöne Geschichte von einem großen und kleinen Hasen."}

Latenz für eine 5-Sekunden-Datei: ~7 Sekunden im warmen Zustand (erster Call braucht ~20s wegen Modell-Lazy-Load — danach läuft das Modell im RAM).

Backend einbinden

Sobald der Server steht, muss das Prilog-Backend ihn kennen.

Backend .env setzen

Auf lee@91.99.30.243:/var/www/backend-api/.env:

env
WHISPER_BASE_URL=http://prilog-wisper:8000
WHISPER_MODEL=Systran/faster-whisper-large-v3
WHISPER_LANGUAGE=de

Diese Variablen sind optional — der Default in src/config/env.ts ist genau dieser Wert. Wenn du nichts ins .env schreibst, ist der Server trotzdem korrekt konfiguriert. Du brauchst die Variablen nur, wenn du eine zweite Whisper-Instanz hast (z.B. prilog-wisper-2) und auf die switchen willst.

Nach der Änderung: pm2 restart backend-api.

Verifizieren

Im Prilog-Admin-Dashboard sollte die „Flurfunk-Server"-Card jetzt grün und einen Health-Check-Wert von ca. 5–50 ms zeigen. Falls rot: siehe Troubleshooting unten.

Operations

Status prüfen

Schnell: das Prilog-Admin-Dashboard zeigt den Status an, gecached für 30 Sekunden.

Manuell:

bash
ssh prilog-wisper
sudo docker ps --filter name=prilog-whisper
sudo docker logs --tail 50 prilog-whisper
sudo docker stats prilog-whisper --no-stream

Erwartete Werte für einen idle Server:

  • Container: Up X hours ohne Restarts
  • RAM: ~2.5 GB belegt (large-v3 int8 im RAM)
  • CPU: < 1% idle
  • Logs: keine Errors, nur access logs bei Requests

Restart

bash
ssh prilog-wisper
cd /opt/whisper && sudo docker compose restart whisper

Restart dauert ca. 5 Sekunden (Modell ist im persistenten Cache, kein Re-Download). Während des Restarts schlagen Transkriptionen fehl — das Backend loggt das nur als Warning, Sprachnachrichten selbst sind nicht betroffen.

Modell wechseln (z.B. auf distil)

Wenn du auf das schnellere distil-large-v3 Modell umstellen willst (z.B. weil der Server überlastet ist und die Qualität trotzdem reicht):

bash
ssh prilog-wisper
sudo nano /opt/whisper/docker-compose.yml
# WHISPER__MODEL ändern auf: Systran/faster-distil-whisper-large-v3
sudo docker compose up -d

Beim nächsten Start lädt sich das neue Modell aus HuggingFace (~1.5 GB, kleiner als das volle large-v3). Latenz halbiert sich, Erkennungsfehler steigen auf ca. 3% (für Deutsch immer noch gut).

Updaten von faster-whisper-server

bash
ssh prilog-wisper
cd /opt/whisper
sudo docker compose pull whisper
sudo docker compose up -d whisper

Update läuft transparent — alte Container wird gestoppt, neuer Container startet mit dem gleichen Modell-Cache.

Backups

Was muss gesichert werden: nichts. Der Whisper-Server ist komplett stateless:

  • Audio-Bytes werden nicht gespeichert
  • Transkripte werden im Backend (api.prilog.chat) verarbeitet und in Synapse geschrieben — Backups dort liegen
  • Modell ist im Cache, kann jederzeit re-downloaded werden
  • docker-compose.yml (eine YAML-Datei) ist im git tracked falls du prilog-infra pflegst, sonst trivial neu zu schreiben

Wenn der Server abbrennt, bestellst du einfach einen neuen, machst die Schritte oben durch (15-20 Minuten + Modell-Download), und das System läuft weiter.

Logs einsehen

bash
ssh prilog-wisper
sudo docker logs prilog-whisper --tail 100 --since 1h

Interessante Patterns:

  • Whisper request completed in X ms — normale Transkriptions-Requests
  • 400 Bad Request — Backend hat eine ungültige Audio-Datei geschickt (z.B. korrupt, falscher Content-Type)
  • Model loading... — wird nur beim Container-Start geloggt
  • Memory-Errors wären OutOfMemoryError — sollte nie passieren bei int8 + 32 GB

Troubleshooting

Status im Admin-Dashboard ist rot

1. Vom Operator-Rechner per Tailnet probieren:

bash
curl -v http://prilog-wisper:8000/health

Erwartet: OK. Wenn DNS-Auflösung scheitert, ist der Server nicht im Tailnet — prüfe tailscale status auf prilog-wisper. Falls Connection refused: Container ist down → siehe nächster Schritt.

2. Container-Status prüfen:

bash
ssh prilog-wisper
sudo docker ps -a --filter name=prilog-whisper

Wenn Exited: sudo docker logs prilog-whisper --tail 100 zeigt warum. Restart: cd /opt/whisper && sudo docker compose up -d.

3. Disk voll?

bash
ssh prilog-wisper
df -h /

Wenn / zu mehr als 95% voll: Modell-Cache prüfen, docker system prune falls viele alte Images/Container hängen.

4. Backend kann ihn nicht erreichen:

bash
ssh lee@91.99.30.243
curl http://prilog-wisper:8000/health

Wenn das fehlschlägt, ist Tailnet-Routing kaputt — sehr selten, neu starten mit sudo systemctl restart tailscaled auf beiden Seiten.

Transkriptions-Latenz sehr hoch (>60s für 30s Audio)

Mehrere Ursachen möglich:

  • Burst-Last: mehrere Requests gleichzeitig. Whisper läuft single-thread pro Request, also ist der zweite Request blockiert bis der erste durch ist. Lösung: warten bis Last sich verteilt, oder Server upgraden auf CCX43.
  • Fremd-Last auf der CPU: prüfe sudo docker stats prilog-whisper. Wenn CPU > 100% vergeben aber Latenz hoch ist → System-Load prüfen via top. Auf einem dediziertem Whisper-Server sollte sonst nichts laufen.
  • Modell-Lazy-Load: der allererste Request nach einem Restart braucht ~20s länger weil das Modell aus dem Cache in den RAM geladen wird. Ist einmalig, danach läuft es im warmen Zustand.

Whisper antwortet mit halluziniertem Text bei Stille

Whisper hat eine bekannte Macke: bei sehr leiser oder rauschiger Aufnahme erfindet es manchmal Trainingsdaten („Untertitel von Stephanie Geiges", „Vielen Dank fürs Zuschauen"). Gegenmittel sind im Default-Setup eingebaut:

  • VAD (Voice Activity Detection) filtert Stille raus
  • Temperature 0 macht das Modell deterministisch
  • language=de Hint gibt klare Sprache vor

Wenn trotzdem Halluzinationen kommen: ist die Audio einfach zu schlecht (zu leise, zu viel Hintergrundlärm) — User soll nochmal näher am Mikro aufnehmen.

Server ist erreichbar aber jeder Request 500'd

Container-Logs prüfen:

bash
ssh prilog-wisper
sudo docker logs prilog-whisper --tail 100

Häufigste Ursachen:

  • HuggingFace-Modell konnte nicht geladen werden (HF down oder Cache korrupt) — rm -rf /opt/whisper/cache && sudo docker compose up -d, dann lädt es neu
  • Out-of-Memory — sollte bei 32 GB RAM und int8 nie passieren, aber wenn: Server hat zu wenig RAM, Upgrade auf größere Instanz nötig

Wie weiß ich ob Flurfunk gerade läuft

Drei Check-Punkte:

  1. Whisper-Server alive: Admin-Dashboard → Flurfunk-Server-Card grün
  2. Backend kann ihn ansprechen: ssh lee@91.99.30.243 'curl -sS http://prilog-wisper:8000/health'
  3. End-to-End: nimm im Web-Client eine Test-Sprachnachricht auf, prüfe ob das Transkript innerhalb von 30 Sekunden ankommt

Wenn alle drei grün sind, läuft Flurfunk fehlerfrei.

Migrations-Pfad zu GPU

Wenn du irgendwann auf einen GPU-Server (Hetzner GEX44) wechseln willst:

  1. Neuen GEX44 mit gleichem Hostname prilog-wisper-gpu provisionieren

  2. Die Schritte 1–6 dieser Anleitung wiederholen (mit zusätzlich nvidia-container-toolkit für Docker-GPU-Support)

  3. docker-compose.yml anpassen:

    yaml
    image: fedirz/faster-whisper-server:latest-cuda
    environment:
      WHISPER__INFERENCE_DEVICE: cuda
      WHISPER__COMPUTE_TYPE: float16
    deploy:
      resources:
        reservations:
          devices:
            - capabilities: [gpu]
  4. Modell ist gleich — WHISPER__MODEL bleibt Systran/faster-whisper-large-v3, nur compute_type ändert sich

  5. Backend .env umstellen: WHISPER_BASE_URL=http://prilog-wisper-gpu:8000

  6. pm2 restart backend-api auf api.prilog.chat

  7. Smoke-Test, dann alten CCX33 abschalten

Latenz auf GPU: ca. 3 Sekunden für 30 Sekunden Audio — Faktor 7x schneller. Damit wird Flurfunk fast-synchron, das Transkript erscheint quasi unmittelbar.

Verwandte Themen