# Auftrag: Sprachmodelle (Spracherkennung + Sprachausgabe) für NEX auf dem knaster-Cluster Eingang 12.10.2026 (vom NEX-Team, ohne Schlüssel). Ausführung **nach** Task 3.2 (GPU-VMs mit Treiber), gern durch einen Subagenten. Im Plan als Task 6.5 geführt. ## Abgleich mit dem echten Cluster (Stand 12.10.2026, vor Ausführung lesen) Der Auftrag geht von einem älteren Bild aus. Es gilt: | Auftrag sagt | Wirklichkeit | |---|---| | 2 Knoten + QDevice | 5 Knoten (pve44, pve46, pve48, pve52, pve61), 5 Stimmen, kein QDevice | | Knoten A 2× P2000, Knoten B 2× M4000, GPU per LXC oder VM | Eine Ubuntu-24.04-VM pro GPU-Knoten mit allen Karten (D2): `gpu-pve44`, `gpu-pve46`, `gpu-pve48` je M4000 + P2000, `gpu-pve52` nur M4000. Treiber R580 in der VM | | Feste IP per DHCP-Reservierung | D5 ist zurückgestellt. Gäste laufen per DHCP, der Nutzer hat keinen Zugriff auf den DHCP-Server. **Vor der Abnahme klären:** Reservierung durch die DHCP-Verwaltung oder statische IP außerhalb des DHCP-Bereichs | Daraus folgt für die Umsetzung: - **Erkennung:** whisper.cpp (CUDA 12, `CMAKE_CUDA_ARCHITECTURES="52;61"`) auf einer **M4000**. Naheliegend ist `gpu-pve52`: Dort steckt nur die M4000, und Placement verteilt keine andere Rolle dorthin. Die P2000 bleiben für embed/rerank (pve44, pve46) und `gpu-small` (pve48). Die M4000 sind im Inventar `disabled` bis zum Benchmark-Tag (Task 3.3); die Messwerte aus 4. Abnahme liefern genau diesen Benchmark für whisper und gehören in Task 3.3. - **Ausgabe Stufe 1 (Piper, CPU):** laut Registry `tts-de` im Infra-LXC (`infra`, Task 5.2). Solange `infra` nicht steht, als Container in derselben GPU-VM wie die Erkennung (CPU reicht). - **Firewall „nur von 192.168.100.29“:** Proxmox-Firewall an der Netzkarte der VM (`firewall=1`, Regeln für 8178/8179) plus `ufw` in der VM. - Globale Vorgabe aus dem Plan passt: whisper-server auf Port 8178, nur von `.29`, `stt.openai.base_url` nur als IP-Literal. ## Auftrag (Originaltext) NEX sind Hermes-Agenten auf appsrv08 (192.168.100.29). Sprache läuft dort: Hermes → nex-router → LiteLLM → Sprach-Backend. Das Backend ist austauschbar (Eintrag in LiteLLM). Der Dienst auf dem Cluster ist das **Rückfall-Modell**. Solange kein Cloud-Anbieter Guthaben hat, ist er der einzige aktive Weg. Die NEX-Seite trägt danach nur IP:Port und Modellnamen in LiteLLM ein. Gebraucht werden zwei **OpenAI-kompatible** HTTP-Dienste im internen Netz. Ein Dienst für beides ist auch in Ordnung. Formate laut Hermes-Code: mp3 als Standard, Opus für Sprachnachrichten, rohes PCM mit Sample-Rate-Header für das Streaming. ### Hardware und Grenzen - Treiber **R580**, die letzte Serie für Maxwell/Pascal. - **faster-whisper/CTranslate2 laufen nicht auf CC 5.2.** Auf der M4000 nur whisper.cpp, und zwar als **CUDA-12-Build**, nicht CUDA 13 (`-DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES="52;61"`). - PyTorch-Unterstützung für sm_50/sm_61 in aktuellen Wheels **vorher prüfen** (`python -c "import torch; print(torch.cuda.get_arch_list())"`), nicht annehmen. Keine Tensor-Cores: bei faster-whisper `int8` oder `int8_float32`. ### 1. Spracherkennung (Pflicht) - Modell **Whisper large-v3-turbo**, Sprache fest `de`. - Empfehlung M4000: whisper.cpp `whisper-server` mit `-m ggml-large-v3-turbo.bin -l de -bs 5 --convert --inference-path /v1/audio/transcriptions`, **ohne** `--print-realtime`, `--tmp-dir` auf tmpfs (z. B. `/run/nex-whisper`). Nach dem Start einmal mit 1 s Stille aufwärmen. - Alternative P2000: speaches (faster-whisper) mit `deepdml/faster-whisper-large-v3-turbo-ct2`, `int8_float32`. - Optional mitmessen: `primeline/whisper-large-v3-turbo-german`. - Schnittstelle: `POST /v1/audio/transcriptions`, multipart - Felder: `file`, `model`, `language=de`, `response_format`. - `file` muss wav, mp3, ogg/opus, webm und m4a annehmen (Umwandlung mit ffmpeg). - `response_format`: `json` → `{"text": "…"}`, `text`, `verbose_json` (wenn möglich mit Wort-Wahrscheinlichkeiten). - Unbekannte Felder ignorieren, nicht mit Fehler antworten. ### 2. Sprachausgabe (Pflicht Stufe 1, optional Stufe 2) - **Stufe 1:** Piper Deutsch, CPU reicht. Stimmen: `de_DE-thorsten-high` und eine weibliche deutsche Stimme. Genauen Namen nennen. Server OpenAI-kompatibel, z. B. speaches (kann Piper) oder ein schlanker Wrapper. - Schnittstelle: `POST /v1/audio/speech` - JSON-Body `{model, voice, input, response_format, speed}`. - `response_format` muss können: `mp3` (Standard), `opus` (im Ogg-Container), `wav`, `flac`, `pcm` (rohes int16, little-endian, mono; Sample-Rate im Header `X-Audio-Sample-Rate` oder in `Content-Type: audio/pcm; rate=`). - Antwort möglichst gestreamt (chunked), damit die Wiedergabe früh beginnt. - **Stufe 2 (optional, GPU, nach PyTorch-Prüfung):** eine natürlichere deutsche Stimme. - **Lizenz für kommerzielle Nutzung prüfen.** Nach Stand des NEX-Teams sind XTTS-v2 (Coqui Public Model License) und die Gewichte von Fish Speech/OpenAudio **nicht kommerziell**. - Chatterbox Multilingual (MIT) ist ein Kandidat. - Kokoro hat nach diesem Stand keine deutsche Stimme. Prüfen, nicht voraussetzen. ### 3. Für beide Dienste - `GET /health` → 200. - Feste IP in `192.168.100.0/24` (siehe Abgleich oben). Ports zum Beispiel 8178 (Erkennung) und 8179 (Ausgabe). - Firewall: Zugriff **nur von 192.168.100.29**. LiteLLM läuft dort in einem Bridge-Netz und erscheint mit der Host-IP. - Kein API-Key nötig. Erzwingt der Server einen, einen festen Dummy-Wert nennen (kein Geheimnis). - Autostart nach Neustart (systemd oder Docker `restart: unless-stopped`), Energiesparen aus, Sicherheitsupdates. - **Datenschutz:** keine Ablage von Audio oder Text; Temp-Dateien nur im RAM, alte regelmäßig löschen; **keine Transkripte oder vorgelesenen Texte in Logs.** ### 4. Abnahme Von appsrv08 aus prüfen, Testaudio ohne Kundendaten, danach löschen. - **Erkennung:** - `curl -s -w ' %{time_total}s\n' -F file=@t10.wav -F model=whisper -F language=de -F response_format=json http://IP:8178/v1/audio/transcriptions` - Text muss deutsch und plausibel sein. - Median aus 3 Läufen: 10 s Audio **≤ 4 s** (Ziel ≤ 3 s), 60 s Audio **≤ 12 s**. Den ersten Aufruf getrennt notieren. - **Ausgabe:** `curl -s -o t.mp3 -w ' TTFB %{time_starttransfer}s gesamt %{time_total}s\n' -H 'Content-Type: application/json' -d '{"model":"piper","voice":"de_DE-thorsten-high","input":"Guten Tag, hier ist Ihr NEX-Assistent. Die Baubesprechung in Haslach ist am vierten November.","response_format":"mp3"}' http://IP:8179/v1/audio/speech` - Ergebnis abspielbar, Zeit bis zum ersten Byte möglichst ≤ 1 s. - Dasselbe mit `pcm` (Sample-Rate-Header prüfen) und `opus`. - **Last:** 2 Erkennungen gleichzeitig. Mit `nvidia-smi` prüfen, dass GPU-Last und VRAM steigen. ### 5. Rückmeldung (genau so) Je Dienst: Basis-URL (IP:Port) · Knoten und GPU · Software mit Version oder Image-Digest · Modellname(n), wie im Feld `model` erwartet · Stimmen-IDs · unterstützte `response_format` · PCM-Sample-Rate · Messwerte (Median, erster Aufruf, VRAM, RAM) · Autostart nach Neustart getestet (ja/nein) · Auffälligkeiten. ### Gegenseite (NEX, zur Information) Die Agenten fragen nur noch neutrale Namen an: `nex-stt` für die Erkennung, `nex-tts` für die Ausgabe. Welches Backend dahinter antwortet und in welcher Reihenfolge, legt allein LiteLLM fest: heute das Cluster, später ein Cloud-Anbieter davor und das Cluster als Rückfall. nex-router 0.2.3 wird um die Sprachausgabe (gestreamt) erweitert. Nach der Rückmeldung trägt das NEX-Team die Endpunkte in LiteLLM ein und testet die Kette. Live erst mit dem neuen Router-Image. ## Rückmeldung (Stand 12.10.2026, Task 6.2) Umgesetzt in `gpu-pve52` (VM 252 auf pve52). Getestet in der VM (localhost) und von ops mit einer temporären ufw-Regel, die danach wieder entfernt wurde. Von appsrv08 noch **nicht** geprüft, das bleibt für das NEX-Team. Testaudio kam aus Piper (zwei Sätze zur Baubesprechung, keine Kundendaten) und wurde danach gelöscht. **Feste IP fehlt (D5):** - Die VM hat ihre Adresse per DHCP: heute `192.168.100.174`, MAC `bc:24:11:92:4b:eb`. - Die Adresse hat den Neustart überstanden, ist aber nicht garantiert. - Vor dem Eintrag in LiteLLM: Reservierung auf diese MAC einrichten oder eine statische Adresse vergeben. **Energiesparen und Updates:** - Die VM hat kein Suspend und kein Energiesparen (Ubuntu-Server-Cloud-Image). - Die GPU läuft im Leerlauf in P5 mit 17 W. Der Kontext bleibt geladen, weil whisper-server läuft. - Die Energieeinstellungen der Proxmox-Knoten habe ich laut Vorgabe nicht angefasst. - Sicherheitsupdates laufen per `unattended-upgrades`. **API-Key:** Keiner nötig, kein Dummy-Wert. Beide Dienste ignorieren das Feld `model`, jeder Wert geht. ### Erkennung (`nex-stt`) **Basis-URL:** `http://192.168.100.174:8178` - Pfad: `POST /v1/audio/transcriptions` - Zustand: `GET /health` → `{"status":"ok"}`, solange das Modell lädt: 503 **Knoten und GPU:** pve52, VM `gpu-pve52`, Quadro M4000 (8 GB, CC 5.2), Treiber 580.178.04 **Software:** - whisper.cpp `whisper-server` **v1.9.4** (Commit `927cfce34f31707e17f2bff35c349632fb9e2c3a`) - Build: CUDA 12.6.3, `-DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES="52;61"`, statisch gelinkt - Image `knaster/nex-whisper:v1.9.4-cu12.6`, ID `sha256:be4987ff63df…` - Basis-Images per Digest gepinnt: `nvidia/cuda:12.6.3-{devel,runtime}-ubuntu24.04` - ffmpeg 6.1.1 für `--convert` **Modell:** `ggml-large-v3-turbo.bin` (f16) - Quelle: HF `ggerganov/whisper.cpp`, Revision `5359861c…` - sha256 `1fc70f77…e2bc69`, gegen Hugging Face geprüft - Feld `model`: beliebig, z. B. `whisper` oder `nex-stt` **Parameter:** - `-l de -bs 5 --convert --inference-path /v1/audio/transcriptions --tmp-dir /run/nex-whisper` - `--tmp-dir` liegt auf tmpfs (512 MB) - ohne `--print-realtime`, Flash-Attention an - Nach jedem Start wird mit 1 s Stille aufgewärmt. **Eingangsformate:** wav, mp3, ogg/opus, webm/opus und m4a/aac, alle über ffmpeg getestet. **`response_format`:** - `json` → `{"text": "…"}` - `text` - `verbose_json`: Segmente mit `words[].probability`, `avg_logprob`, `no_speech_prob`, dazu `language_probabilities` - außerdem `srt` und `vtt` **Unbekannte Felder werden ignoriert.** Geprüft mit `model=gpt-4o-transcribe`, `timestamp_granularities[]=word`, `foo=bar` und `stream=false`, Antwort jeweils 200. **PCM-Sample-Rate:** entfällt; ffmpeg wandelt intern auf 16 kHz mono. **Messwerte (M4000, `-bs 5`):** | Messung | Ergebnis | |---|---| | 10 s Audio, erster Aufruf nach Start und Aufwärmen | 3,00 s (nach `docker restart`), 2,94 s (erster Aufruf der Abnahmeserie) | | 10 s Audio, Median aus 3 Läufen | **2,94 s** (Grenze 4 s, Ziel ≤ 3 s erreicht) | | 60 s Audio, erster Aufruf | 8,63 s | | 60 s Audio, Median aus 3 Läufen | **8,63 s** (Grenze 12 s) | | 5 s Audio, von ops über das Netz | 2,43 s | | Start bis bereit (Modell im Cache, inkl. Aufwärmen) | 4,1 s | | VRAM | 1965 MiB nach dem Laden und bei 10-s-Aufrufen, bis 2179 MiB nach 60-s-Audio, im Paralleltest kein weiterer Anstieg | | RAM des Containers | 286 MiB | | Leerlauf der Karte | P5, 17 W | **Last mit 2 Erkennungen gleichzeitig (je 60 s):** - Antworten nach 8,8 s und 17,5 s. whisper-server arbeitet sie nacheinander ab (globaler Mutex), die zweite wartet. - GPU-Last stieg auf 100 %, Mittel 78 %. Der Leerlauf (0 %) wurde separat gemessen; der erste Wert im Log (85 %) war noch Nachlauf. - **Der VRAM steigt nicht**, er bleibt bei 2179 MiB, weil der Kontext einmal geladen wird. **Autostart nach Neustart getestet: ja.** Die VM wurde über Proxmox neu gestartet (`qm reboot 252`). 34 s nach dem Boot lieferten beide `/health` 200, die erste Erkennung danach war normal. **Auffälligkeiten:** 1. **Gleichzeitige Anfragen laufen nacheinander** (siehe Last). 2. **Mindestdauer etwa 2,3 s pro Aufruf**, auch bei kurzen Clips. Der Encoder rechnet immer ein 30-s-Fenster. 3. **Format der `json`-Antwort:** Der Text beginnt mit einem Leerzeichen und hat Zeilenumbrüche zwischen den Segmenten. Für Hermes ggf. `strip()` bzw. Umbrüche durch Leerzeichen ersetzen. 4. **Content-Type bei `text`:** `text/html; charset=utf-8`. 5. **Qualität am synthetischen Testaudio:** - Sauber: „Baubesprechung in Haslach ist am 4. November um 10 Uhr“. - „NEX“ wurde meist als „NX“ erkannt. - Im Teil mit der leisen Kerstin-Stimme gab es einzelne Fehler, z. B. „Baugüro“ und „Baulätung“. - Echte Aufnahmen und die Dialekt-Testsätze stehen in Task 3.3 an. 6. **Log:** Es enthält nur den Upload-Dateinamen und die Audiolänge (z. B. `Received request: t10.wav`), **keinen Text**. Gegen den Testtext gegrepped: 0 Treffer. - ffmpeg läuft mit `-loglevel error`. - Log-Rotation: Treiber `local`, 2 × 5 MB. 7. **Temp-Dateien:** - Nur im tmpfs, nach jeder Anfrage gelöscht; nach den Tests 0 Dateien. - Bei ffmpeg-Fehlern bleibt eine Upload-Datei liegen. Ein Aufräumer löscht Dateien älter als 10 min. 8. **Endpunkt `POST /load`:** Er existiert (Modellwechsel). Erreichbar ist er nur von `.29`, und im Container liegt nur dieses eine Modell. 9. **Proxmox-Firewall:** Abweichend vom Abgleich oben ist sie **nicht** aktiviert (Vorgabe). Es gilt nur **ufw in der VM**: - 8178/8179 nur von `192.168.100.29` - SSH aus `192.168.100.0/24` - sonst `deny incoming` - Beide Dienste laufen im Host-Netz, weil veröffentlichte Docker-Ports ufw umgehen würden. - Negativtest von ops (.173): ohne Regel Zeitüberschreitung auf beiden Ports. ### Ausgabe (`nex-tts`) **Basis-URL:** `http://192.168.100.174:8179` - Pfad: `POST /v1/audio/speech` - Zustand: `GET /health` → `{"status":"ok","voices":[…],"default":"de_DE-thorsten-high"}` - außerdem `GET /v1/models` und `GET /v1/audio/voices` **Knoten und Hardware:** pve52, VM `gpu-pve52`, nur CPU. Die VM hat 4 Kerne (i7-7700); der Container ist auf 3 Kerne und 2 GB begrenzt. **Software:** - eigener schlanker Wrapper: FastAPI 0.141.1, uvicorn 0.53.0, piper-tts **1.8.0** (GPL-3.0, eigener Dienst), onnxruntime 1.30.0, ffmpeg 5.1.9 - Image `knaster/nex-tts-piper:1.8.0`, ID `sha256:d9e65ee77836…` - Basis `python:3.12-slim-bookworm`, per Digest gepinnt **Warum nicht speaches:** Der Wrapper ist klein und gut zu prüfen. Er streamt satzweise, setzt die PCM-Header nach Vorgabe und schreibt weder Text noch Audio auf die Platte. Der bisherige knaster-`tts_server` konnte nur wav. **Modellname (`model`):** beliebig, z. B. `piper` oder `nex-tts`. **Stimmen-IDs:** - `de_DE-thorsten-high`: männlich, 22050 Hz, CC0, Standard - `de_DE-kerstin-low`: weiblich, 16000 Hz, Datensatz CC0 - Dateien aus HF `rhasspy/piper-voices`, Revision `c10ece1a…`, sha256 geprüft. - Kurzformen `thorsten` und `kerstin` gehen ebenfalls. - Unbekannte Stimmen (z. B. `alloy`) fallen ohne Fehler auf die Standardstimme. Der Header `X-Voice` nennt die verwendete Stimme. **Weitere weibliche Stimmen**, nicht genommen: - `de_DE-ramona-low` und `de_DE-eva_k-x_low`: M-AILABS, Lizenz „See URL“, kommerzielle Nutzung nicht belegt - `de_DE-mls-medium`: mehrere Sprecher **`response_format`:** - Standard ist `mp3` (`audio/mpeg`). - `opus` (Ogg, 48 kHz, `audio/ogg`), `aac`, `flac` und `pcm` werden **gestreamt** (chunked). Die Header kommen erst mit dem ersten Audioblock, deshalb misst TTFB echtes Audio. - `wav` kommt vollständig mit korrektem Header; das passt zu `knaster bench tts` und zum Canary. - `speed` (0,25–4) wird auf `length_scale` abgebildet. - Unbekannte Felder werden ignoriert. - `input` ist auf 5000 Zeichen begrenzt. **PCM-Sample-Rate:** **24000 Hz** für alle Stimmen, wie OpenAI. - Format: int16 LE, mono, umgerechnet per ffmpeg. - Header: `X-Audio-Sample-Rate: 24000` und `Content-Type: audio/pcm; rate=24000`. - Mit `PCM_SAMPLE_RATE=0` käme die native Rate der Stimme. **Messwerte (Auftragssatz, thorsten-high, je 3 Läufe):** | Format | TTFB | gesamt | Audio | |---|---|---|---| | mp3 | 0,67–0,91 s | 1,42–1,68 s | 5,25 s | | pcm | 0,72–0,87 s | 1,48–1,62 s | 5,31 s | | opus | 0,64–0,72 s | 1,54–1,61 s | 5,28 s | - Von ops über das Netz: mp3 TTFB 0,72 s. - Erste Synthese nach einem Neustart (wav, 10 s Audio): 2,7 s gesamt. Die Stimmen werden beim Start aufgewärmt. - RAM: 616 MiB. Kein VRAM. **Autostart nach Neustart getestet: ja**, wie bei der Erkennung. **Auffälligkeiten:** 1. **Log:** kein Access-Log, Level `warning`. - Gegen den Testtext gegrepped: 0 Treffer. - Piper meldet gelegentlich `Missing phoneme from id map` (ohne Text). - onnxruntime meldet beim Start eine harmlose Telemetrie-Warnung. 2. **Gleichzeitige Synthesen:** höchstens 2 (`TTS_PARALLEL`). Die CPU teilt sich der Dienst mit whisper-server, das die CPU aber kaum nutzt. 3. **Ablageort:** Laut Registry (`tts-de`) gehört Piper in den Infra-LXC. Vorerst läuft es hier. ### Stufe 2 (nur geprüft, nichts installiert) **PyTorch auf der M4000:** - In einem Wegwerf-Container geprüft: `torch 2.9.1+cu126`, `get_arch_list()` = `sm_50, sm_60, sm_70, sm_75, sm_80, sm_86, sm_90`. - Die M4000 (5, 2) rechnet eine Matrixmultiplikation korrekt. - **2.9.1 ist die letzte Version mit cu126-Wheels.** Laut den Build-Skripten haben cu128/cu129/cu130 kein sm_5x/sm_6x mehr, also ist das die Obergrenze für Maxwell/Pascal. **Chatterbox Multilingual (ResembleAI):** - Lizenz **MIT** (HF-Karte), Deutsch enthalten. - Läuft grundsätzlich mit PyTorch ≤ 2.9.1 auf sm_52, aber ohne Tensor-Cores in fp32. - Langsam erwartet. Nicht gemessen. **Kokoro-82M:** `VOICES.md` listet **keine deutsche Stimme**, nur en, ja, zh, es, fr, hi, it und pt-br. **XTTS-v2 und Fish Speech:** nicht geprüft, laut NEX-Team nicht kommerziell. **Empfehlung:** Stufe 2 mit `tts-de-hq` (Qwen3-TTS) auf der Ada (Task 6.4) statt auf der M4000. ### Notiz für Task 3.3 (Benchmark M4000, whisper) **Ergebnis:** - CC 5.2 **rechnet** mit dem CUDA-12.6-Build. `system_info` zeigt `CUDA : ARCHS = 520,610`. - `large-v3-turbo` f16 mit `-bs 5`: 10 s Audio in 2,94 s (RTF 0,29), 60 s in 8,63 s (RTF 0,14). - Mit `-nfa` (Flash-Attention aus) sind es 3,21 s für 10 s, also bleibt Flash-Attention an. - Speicher: VRAM 2,2 GB, RAM 0,3 GB. **Noch offen:** - P2000 nicht gemessen - Dialekt-Testsätze - `primeline/whisper-large-v3-turbo-german`: bräuchte eine ggml-Umwandlung - Strom unter Last **Offener Punkt Registry:** Den Slot `stt-de` habe ich **nicht** angelegt. - `SlotSpec.kind` (core `registry.py`) kennt kein `stt`, Placement (`placement.py`) und Canary ebenfalls nicht. - Ohne Core-Änderung schlägt `knaster validate` fehl, und der Canary würde den Slot als Chat proben. - Gehört zu 3.3 bzw. 5.2, zusammen mit dem Umzug von `tts-de`. ### Betrieb **Dateien im Repo:** - `deploy/compose/nex-speech/` (Compose, `.env.example`, Dockerfiles, `piper/app.py`) - Rolle `platform/proxmox/ansible/roles/nex-speech` - Playbook `platform/proxmox/ansible/playbooks/nex-speech.yml`, nur für `gpu-pve52` **Ausrollen von ops:** ```sh cd /root/cluster-knaster && git pull cd platform/proxmox/tofu && . /root/.config/knaster/tofu.env && bash ../scripts/refresh-vm-access.sh cd ../ansible && ansible-playbook playbooks/nex-speech.yml ``` **Was die Rolle macht:** - ufw-Regeln setzen und ufw einschalten - `docker-buildx` installieren - Modelle nach `/srv/models/{whisper,piper}` laden, mit sha256 - Compose nach `/opt/nex-speech` kopieren, `.env` aus den Rollen-Variablen schreiben - bauen und starten, danach auf `/health` beider Dienste warten - Ein zweiter Lauf ändert nichts (`changed=0`). **Build:** - Der erste whisper-Build dauert in der VM etwa 15 min. - Das devel-Image (11,5 GB) bleibt als Cache liegen. Freier Platz auf `/` danach: 18 GB. **Prüfen in der VM:** - `curl -s localhost:8178/health; curl -s localhost:8179/health` - `sudo docker compose -f /opt/nex-speech/docker-compose.yml ps` - Logs mit `sudo docker logs nex-stt` bzw. `nex-tts`. Sie enthalten keine Texte. **Firewall in der VM:** - `sudo ufw status` - Zum Testen von einem anderen Rechner eine Regel nur vorübergehend setzen: `sudo ufw insert 1 allow from to any port 8178:8179 proto tcp`, danach `sudo ufw delete allow from to any port 8178:8179 proto tcp`. - Weitere Compose-Stacks in dieser VM mit veröffentlichten Ports (`ports:`) umgehen ufw. Für Sprachdienste deshalb im Host-Netz bleiben. **Updates:** - Ubuntu-Sicherheitsupdates laufen per `unattended-upgrades`. Der NVIDIA-Treiber ist gehalten. - Images sind gepinnt. Ein Wechsel geht nur bewusst über die Rollenvariablen `nex_speech_env`: - whisper: `WHISPER_TAG` und `WHISPER_COMMIT` gemeinsam - Piper: `requirements.txt` und `PIPER_TTS_VERSION` - Danach das Playbook erneut laufen lassen. **Parameter ändern:** In `nex_speech_env`, z. B. `WHISPER_EXTRA_ARGS: "-nfa"` oder `PCM_SAMPLE_RATE: "0"`, dann das Playbook laufen lassen.