docs: Rückmeldung Sprachdienste NEX (Task 6.2) mit Messwerten, Betrieb und Plan-Fortschritt
ci / test (push) Successful in 27s

Erkennung 10 s → 2,94 s, 60 s → 8,63 s (M4000, Median), TTS-TTFB < 1 s,
Autostart getestet, Stufe 2 geprüft (torch 2.9.1+cu126 letzte Version mit sm_50).
Offen: feste IP (D5), Abnahme von appsrv08, Slot stt-de.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Robert Rapp
2026-10-12 21:05:18 +02:00
co-authored by Claude Opus 5.5
parent 365549b1f4
commit b9eb55bf44
2 changed files with 244 additions and 0 deletions
@@ -75,3 +75,244 @@ Je Dienst: Basis-URL (IP:Port) · Knoten und GPU · Software mit Version oder Im
### Gegenseite (NEX, zur Information) ### 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. 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.
**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, 2179 MiB ab der ersten Erkennung, danach konstant |
| 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 % (Leerlauf 0 %, Mittel 78 %).
- **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 <IP> to any port 8178:8179 proto tcp`, danach `sudo ufw delete allow from <IP> 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.
@@ -349,6 +349,9 @@ Ersetzt PLAN §8 „1 Data Plane“.
- [ ] **6.1 GPU-Rollen:** `deploy/compose/gpu-llamacpp` in den GPU-VMs nach der Zuordnung aus Task 3.3, also Embedding und Rerank redundant auf zwei Knoten, Chat für verifier und signature. Prüfung: Canary-Checks grün (`knaster` Canary-Prober). - [ ] **6.1 GPU-Rollen:** `deploy/compose/gpu-llamacpp` in den GPU-VMs nach der Zuordnung aus Task 3.3, also Embedding und Rerank redundant auf zwei Knoten, Chat für verifier und signature. Prüfung: Canary-Checks grün (`knaster` Canary-Prober).
- [ ] **6.2 STT und TTS für NEX** – Auftrag vom 12.10. mit Abgleich: `docs/auftraege/2026-10-12-sprachmodelle-nex.md` (Erkennung whisper.cpp auf einer M4000, naheliegend `gpu-pve52`; Ausgabe Piper auf CPU, Port 8179; Abnahme und Rückmeldeformat dort). Läuft nach Task 3.2, gern als Subagent. **Blockiert für die Abnahme:** feste IP (D5). Bisheriger Text (nex-STT-Plan Tasks 2–4): `whisper-server` als zusätzlicher Dienst in der GPU-VM des in Task 3.3 gewählten Knotens. Neuer Slot `stt-de` im Katalog. Port 8178 nur von `192.168.100.29` (Firewall der VM bzw. des Gasts). Prüfung: Von appsrv08 liefert `curl -F file=@probe.wav http://<IP>:8178/v1/audio/transcriptions` deutschen Text, von einem anderen Rechner wird der Zugriff abgelehnt. - [ ] **6.2 STT und TTS für NEX** – Auftrag vom 12.10. mit Abgleich: `docs/auftraege/2026-10-12-sprachmodelle-nex.md` (Erkennung whisper.cpp auf einer M4000, naheliegend `gpu-pve52`; Ausgabe Piper auf CPU, Port 8179; Abnahme und Rückmeldeformat dort). Läuft nach Task 3.2, gern als Subagent. **Blockiert für die Abnahme:** feste IP (D5). Bisheriger Text (nex-STT-Plan Tasks 2–4): `whisper-server` als zusätzlicher Dienst in der GPU-VM des in Task 3.3 gewählten Knotens. Neuer Slot `stt-de` im Katalog. Port 8178 nur von `192.168.100.29` (Firewall der VM bzw. des Gasts). Prüfung: Von appsrv08 liefert `curl -F file=@probe.wav http://<IP>:8178/v1/audio/transcriptions` deutschen Text, von einem anderen Rechner wird der Zugriff abgelehnt.
- [x] (12.10.) Umgesetzt in `gpu-pve52`: `nex-stt` (whisper.cpp v1.9.4, CUDA 12.6, sm_52/61, `large-v3-turbo`, Port 8178) und `nex-tts` (Piper 1.8.0, `de_DE-thorsten-high` + `de_DE-kerstin-low`, Port 8179) als `deploy/compose/nex-speech`, Rolle/Playbook `nex-speech`. ufw in der VM (8178/8179 nur von `.29`, SSH aus dem LAN), Proxmox-Firewall bewusst aus.
- [x] (12.10.) Tests in der VM und von ops (temporäre Regel, danach entfernt): 10 s → 2,94 s, 60 s → 8,63 s (Median), TTS-TTFB 0,64–0,91 s (mp3/pcm/opus), Autostart nach `qm reboot 252` geprüft, keine Texte in Logs. Rückmeldung im Auftrag.
- [ ] Offen: feste IP (D5, heute DHCP `192.168.100.174`), Abnahme von appsrv08 durch das NEX-Team, Slot `stt-de` (Core kennt kein `kind: stt`), `tts-de` später nach `infra`.
- [ ] **6.3 Lokale Slots in LiteLLM (D4):** `embed-default`, `rerank-default`, `verifier-small` und `signature-extractor` als Modelle im LiteLLM auf appsrv08 eintragen (IP-Literale). Prüfung: `curl http://192.168.100.29:<port>/v1/models` listet die Slots, ein Aufruf je Slot antwortet. - [ ] **6.3 Lokale Slots in LiteLLM (D4):** `embed-default`, `rerank-default`, `verifier-small` und `signature-extractor` als Modelle im LiteLLM auf appsrv08 eintragen (IP-Literale). Prüfung: `curl http://192.168.100.29:<port>/v1/models` listet die Slots, ein Aufruf je Slot antwortet.
- [ ] **6.4 Ada-Worker (D3)** · **Planner:** „LLM-Worker: direkte Inferenz-Schnittstelle für LiteLLM (zusätzlich zum Job-Poll), Tunnel, Registry, Fallback-Kette“ - [ ] **6.4 Ada-Worker (D3)** · **Planner:** „LLM-Worker: direkte Inferenz-Schnittstelle für LiteLLM (zusätzlich zum Job-Poll), Tunnel, Registry, Fallback-Kette“
- Offene Schritte der Worker-Registry v2 im Repo `mygodel/nex` abschließen. - Offene Schritte der Worker-Registry v2 im Repo `mygodel/nex` abschließen.