Commit Graph
32 Commits
Author SHA1 Message Date
duffyduckandClaude Opus 5.5 9529bb0b16 feat(app): Aufnahme verwerfen — Muelleimer neben dem Aufnahmeknopf
Bisher hiess "Stopp" immer "senden"; wer es sich anders ueberlegt hatte,
konnte die Aufnahme nicht zuruecknehmen.

- App: waehrend der Aufnahme 🗑 links neben dem roten Knopf. Verwirft ueber
  ariaStopRecording('user-discard') → cancelStreamingRecording; das leere
  Endpoint raeumt die Sprechblase ab, zurueck aufs Wake-Word.
- Fund dabei: "cancel" schickte der STT-Bridge nur stt_stream_end — die
  transkribierte trotzdem und der Text ging an ARIA (fiel nur nicht auf,
  weil cancel bisher bei Stille kam). Jetzt:
  - Whisper- und Voxtral-Bridge: stream_end mit reason "cancel:*" →
    discard, kein Transcribe, leerer Text (reason "cancelled").
  - aria-bridge merkt sich verworfene requestIds (2 min) und wirft einen
    trotzdem ankommenden Text weg — Schutz auch bei alter STT-Bridge.

Getestet: Whisper-Bridge mit nachgebautem Runner (Stopp → Text, Muelleimer
→ kein Transcribe, leer). App: TypeScript ohne Fehler; auf dem Handy noch
nicht ausprobiert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:05:35 +01:00
duffyduckandClaude Opus 5.5 5a59ea3930 fix(xtts): Flotte zeigt die echte Karte statt "GPU void"
Das NVIDIA-Container-Toolkit ersetzt NVIDIA_VISIBLE_DEVICES im Prozess durch
"void" — whisper/f5tts/voxtral meldeten das als GPU. Die Compose gibt die
Karte jetzt zusaetzlich als ARIA_GPU mit (nur Anzeige).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:51:41 +01:00
duffyduckandClaude Opus 5 cb89ebcd10 fix(stt): lange Sprachnachrichten brachen mitten im Satz ab
Der Endpointer hat seine EIGENE Rechenzeit als Stille gezaehlt.

Der Tick-Loop laeuft alle 200ms und ist die einzige Stelle, die akustische
Sprach-Energie misst (last_voice_at). Er hat aber auf die Zwischen-
Transkription gewartet — und die laeuft jedes Mal ueber den KOMPLETTEN Buffer.
Bei 10s Audio harmlos, bei zwei Minuten dauert sie mehrere Sekunden. In dieser
Zeit tickte nichts, danach war `now - last_voice_at` um die ganze Rechenzeit
groesser als endpoint_ms (5s) — Endpoint mitten im Satz, obwohl durchgehend
gesprochen wurde. Deshalb traf es genau die LANGEN Nachrichten, und zwar immer
spaeter werdend je laenger die Aufnahme.

Nachgewiesen: die Nachricht vom 02.10. 21:28 (1231 Zeichen) war die einzige mit
reason=endpoint statt stream_end und endet mitten im Satz ("...weil ich
weiss..."). Alle vollstaendigen Nachrichten im Thread haben reason=stream_end,
also den Stop-Knopf.

Fix:
- Zwischen-Transkription laeuft als eigener Task. Der Tick bleibt frei und
  misst weiter alle 200ms — die Rechenzeit kann nicht mehr als Stille gelten.
  Laeuft noch eine, wird keine zweite gestartet.
- Zwischen-Transkriptionen sehen nur noch die letzten 30s an. Damit bleibt ihre
  Kosten konstant statt mit der Aufnahmelaenge zu wachsen. Der FINALE Text
  entsteht unveraendert aus dem gesamten Buffer mit beam_size=5 — es geht kein
  Wort verloren. stt_partial bekommt dafuer ein Feld 'windowed'.
- _finalize und drop brechen eine laufende Zwischen-Transkription ab, damit der
  finale Text nicht hinter ihr auf den GPU-Lock warten muss.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-03 10:18:02 +02:00
duffyduckandClaude Opus 4.8 5761c5f6b9 fix(xtts): Worker-Neustartschleife — max_size an den RVS angleichen (50MB -> 1500MB)
Ursache der staendigen whisper/f5tts-Neustarts, direkt aus dem Box-Log:
  sent 1009 (message too big) frame with 209221780 bytes
  exceeds limit of 52428800 bytes

Der RVS ist ein BROADCAST-Relay: eine grosse Nachricht (Datei-Transfer,
Voice-Upload, APK-Update) geht an JEDEN Client im Raum. Die aria-bridge nutzt
deshalb max_size=1500MB — die Worker standen auf 50MB (llm-adapter sogar 16MB).
Ein 209-MB-Frame liess sie die Verbindung mit Code 1009 schliessen; danach
TLS-Fallback auf ws:// (scheitert an Caddy), Reconnect, naechster Frame, von
vorn. Ergebnis: Neustart im Minutentakt, und jede laufende Aufnahme war weg —
das ist die eigentliche Ursache fuer 'erste Sprachnachricht kommt nicht durch'.

Alle vier Worker nutzen jetzt RVS_MAX_SIZE_MB (Default 1500), passend zu
RVS/Bridge. Die GPU war NICHT das Problem (4,3 von 8 GB belegt, zweite Karte leer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 01:33:22 +02:00
duffyduckandClaude Opus 4.8 d030cd7bc6 fix(voice): verlorene Sprachnachrichten — Allow-List, Kaltstart, Dedup, Voice-Router
Vier unabhaengige Ursachen fuer 'erst beim zweiten Aufnehmen' bzw. 'wird gar
nicht verarbeitet' (server-/box-seitig, ohne App-Update wirksam):

1) rvs/server.js: stt_lease, stt_lease_request, stt_transcribe_blob,
   stt_transcribe_result und chat_ack fehlten in ALLOWED_TYPES und wurden STILL
   verworfen (dieselbe Falle wie bei worker_*/host_*). Folgen: das Lease lief
   immer in den 250ms-Timeout -> jede Aufnahme startete verspaetet und OHNE
   targetInstance nahmen alle STT-Worker dieselbe Session an (doppelte
   stt_endpoint); chat_ack kam nie an -> getippte Nachrichten galten in der App
   nach 3 Versuchen als fehlgeschlagen, obwohl sie verarbeitet waren.

2) xtts/whisper/bridge.py: Beim Kaltstart wurde die Session erst NACH dem
   Modell-Load angelegt — alle Chunks der ersten Aufnahme liefen ins Leere und
   der Take war weg (die zweite klappte, weil das Modell dann warm war). Session
   wird jetzt sofort angelegt, der Endpointer wartet solange (runner.model None).

3) bridge/aria_bridge.py: Text-Dedup-Fenster 5s -> 1.5s. Es soll nur den
   Zwilling zweier Worker abfangen; mit 5s fielen echte Wiederholungen weg —
   also genau das Nachsprechen nach einem verschluckten Take, und kurze
   Kommandos ('lauter', 'nächstes').

4) bridge/aria_bridge.py Voice-Router: Komma als Prefix-Trenner entfernt (nur
   noch ':' und '-'), Fuzzy-Schwelle 0.6 -> 0.82, Sticky wird nicht mehr bei
   jeder Nachricht verlaengert. Vorher matchten normale Saetze wie 'In Ordnung,
   mach mal ...' als Projekt-Prefix: die Nachricht landete still in einem
   Projekt und war im Hauptchat unsichtbar — verifiziert mit Beispielsaetzen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-30 11:05:06 +02:00
duffyduckandClaude Opus 4.8 df5cd3be99 fix(rvs-clients): RVS_SNI via getaddrinfo-Override statt host=/port= (HTTP-200-Fix)
Symptom mit RVS_SNI: 'server rejected WebSocket connection: HTTP 200'. Ursache:
die IP stand in der URI -> HTTP-Host-Header = IP -> Caddy findet keinen RVS-Site-
Block (routet nach Host) und liefert eine Default-200-Seite statt WS-Upgrade. Der
vorige Fix korrigierte nur SNI/Cert (server_hostname), nicht den Host-Header.

Zudem: host=/port= als websockets.connect-kwargs kollidieren in der Legacy-API
mit dem aus der URI abgeleiteten Host (TypeError).

Loesung (host-agent, satellite, f5tts/whisper/voxtral/llm-adapter): die URI nutzt
den HOSTNAMEN (RVS_SNI) -> Host-Header + SNI + Cert stimmen; ein In-Process-
getaddrinfo-Override mappt RVS_SNI -> RVS_HOST (IP) fuer den TCP-Connect.
Versionsunabhaengig, nicht-blockierend, nur aktiv wenn RVS_SNI gesetzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:54:38 +02:00
duffyduckandClaude Opus 4.8 9d93c3fe3d feat(xtts): RVS_SNI in allen Compute-Bridges — AI-Box im RZ-Netz auf interne IP
Damit eine AI-Box (f5tts/whisper/voxtral/llm-adapter) auch im selben Netz wie
der RVS direkt auf dessen interne IP verbinden kann (kein NAT-Hairpin ueber den
externen Hostnamen), ohne am SNI/Cert zu scheitern: RVS_HOST=<ip> + RVS_SNI=<name>.
server_hostname im ssl-Context, gated auf use_tls (respektiert den TLS-Fallback).
Leer = Verhalten wie bisher. Szenario: zweite Box im RZ + eine zuhause.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:22:27 +02:00
duffyduckandClaude Opus 4.8 3a908d246a fix(fleet): Empfangs-Watchdog in Workern — halb-tote RVS-Verbindung erkennen + reconnect
Root Cause der unsichtbaren Box: die WS-Verbindung wird ueber die Zeit halb-tot
— Caddy (TLS-Terminator vor dem RVS) haelt sie offen und pongt die WS-Pings
selbst, waehrend der rvs-Backend sie laengst fallen liess (rooms:1 = Box nicht
mehr Raum-Mitglied). Die Box sendet worker_hello ins Leere; der WS-Ping erkennt
den toten Backend nie. (Token/URL/Port/IP/RVS-Instanz alle bewiesen identisch.)

Fix: recv() mit Timeout (RX_STALE_S=60s) statt `async for`. In einem echten Raum
kommt staendig Broadcast-Traffic rein (sat_hello alle 25s, Brain-Polling) →
Timer wird laufend resettet. Bleibt der Traffic aus, ist die Verbindung tot →
ConnectionError → Reconnect ueber die bestehende run_loop-Backoff-Logik.
Betrifft alle vier Worker (f5tts/whisper/voxtral/llm-adapter).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 11:10:49 +02:00
duffyduckandClaude Opus 4.8 3474d2eb6f fix(fleet): Worker-RVS-URL ohne /ws — an Bridge/App/Satellit/Diagnostic angleichen
DIE Ursache, warum die Box nie in der Compute-Flotte auftauchte: die vier
xtts-Worker verbanden zu wss://host:PORT/ws?token=… — ALLE anderen Komponenten
(Bridge, App, Satellit, Diagnostic) verbinden ohne /ws (…:PORT?token=…). Das RVS
selbst ignoriert den Pfad, aber der TLS-Terminator vor :444 routet nach Pfad →
/ws landete in einem anderen Endpunkt/Raum. Folge: worker_hello/ping erreichten
den ARIA-Raum nie (rooms:1 = nur ARIA), das Diagnostic sah die Worker nicht (und
das erklaert auch das RVS-Flappen, das NUR die Worker hatten).

Fix: /ws aus der URL aller vier Worker entfernt → gleicher Pfad wie alle anderen
→ gleicher Raum. Danach: Box neu bauen, Worker erscheinen in der Flotte.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 04:47:03 +02:00
duffyduckandClaude Opus 4.8 fc198f90da fix(fleet): worker_hello periodisch wiederholen (wie Satellit) — Box wird nach Diagnostic/Bridge-Neustart wieder sichtbar
Worker sendeten worker_hello nur EINMAL beim Connect. Startet das Diagnostic
oder die Bridge NACH der Box neu, verpassen sie das hello (RVS spielt es nicht
nach) und sehen nur noch worker_ping → die Box taucht nicht in der Compute-
Flotte auf. Der Satellit ist genau deshalb zuverlaessig sichtbar: er wiederholt
sat_hello periodisch.

- alle vier Worker (f5tts/whisper/voxtral/llm-adapter): worker_hello wird jetzt
  zusaetzlich alle ~30s (jeder 3. Ping-Zyklus) wiederholt → ein neu gestartetes
  Diagnostic/Bridge lernt die Box innerhalb von 30s, ohne Box-Neustart.
- diagnostic/server.js: Voice-Reconcile (f5tts) laeuft nur beim ERSTEN/erneuten
  Auftauchen der Box, nicht bei jedem 30s-hello-Resend (sonst Push/Pull-Dauerlauf).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 04:12:16 +02:00
duffyduckandClaude Opus 4.8 51985f7c2e feat(fleet): Auslastungs-Monitor pro Box — live nvidia-smi + Graphen (Stage E)
Pro Box ein "Auslastung"-Button in der Compute-Flotte → Modal mit live
nvidia-smi (1s), Graphen (GPU-Auslastung + Tokens/Intervall) und Besen-Reset.
Historie liegt auf der Box, Diagnostic holt sie via RVS.

- node_stats.py (identisch in allen 4 Worker-Build-Contexts): Sampler alle 15s
  (nvidia-smi + Token-Delta → Ringpuffer ~500 Punkte, persistent als JSON auf
  der Box), Live-Stream (node_stats, 1s, Auto-Stop 300s), History-Request,
  Reset. nvidia-smi via async subprocess, fail-safe ohne GPU.
- Worker-Wiring (f5tts/whisper/voxtral/llm-adapter): Import, Sampler-Task,
  _stats.handle() nach dem targetInstance-Filter. llm-adapter zaehlt Tokens
  (usage.total_tokens) → Token-Graph nur bei LLM-Boxen. Dockerfiles kopieren
  node_stats.py.
- compose: llm-adapter bekommt runtime:nvidia + NVIDIA_VISIBLE_DEVICES=all +
  DRIVER_CAPABILITIES=utility (nur nvidia-smi, KEIN VRAM/Compute).
- diagnostic/server.js: relay node_stats_* (Browser→Box) + forward (Box→Browser).
- diagnostic/index.html: Auslastung-Button pro Node (Ziel bevorzugt llm-Instanz),
  Modal mit live nvidia-smi + Inline-SVG-Sparklines, Besen-Reset.

Reporter-Wahl bevorzugt die llm-Instanz (sieht alle GPUs + Tokens); GPU-Worker
sehen ihre gepinnte Karte. Gitignored Historie stoert git-Baum der Box nicht.
Deploy: diagnostic + GPU-Boxen neu bauen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 03:26:17 +02:00
duffyduckandClaude Opus 4.8 254031bb7d refactor: "Gamebox" ueberall durch "AI-Box(en)" ersetzen
Rein terminologisch: die feste "Gamebox" ist konzeptuell zu beliebig vielen
AI-Boxen geworden (Multi-Node-Compute-Fleet). Wort-Sweep ueber Code-Kommentare,
Strings, README, docs, .env.example, CHANGELOG — keine Identifier/Keys betroffen
(Gamebox kam nirgends in Variablennamen vor). Casing erhalten: Gamebox→AI-Box,
gamebox→ai-box. NODE_NAME-Default gamebox→ai-box, Instanz-IDs f5tts@ai-box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-19 00:34:21 +02:00
duffyduckandClaude Opus 4.8 e39599ecea feat(compute): Redundanz-Routing per targetInstance (Stage 3)
Mehrere Instanzen pro Dienst nutzbar: Anfragen werden gezielt an eine freie
Instanz adressiert statt an alle gebroadcastet. Mehrere f5tts → naechstes
freies; mehrere LLM → parallele Turns (Multitasking); STT-Redundanz ueber
mehrere Apps via Lease.

- Worker (alle vier): filtern am Loop-Eingang — targetInstance gesetzt und
  != eigener INSTANCE_ID → Nachricht ignorieren. Feld fehlt → wie bisher.
- bridge (TTS/LLM, emittiert die Bridge selbst): _pick_worker() waehlt eine
  online+freie Instanz (Round-Robin), stempelt targetInstance auf
  xtts_request / llm_request. Keine Instanz bekannt → Broadcast.
- bridge (STT-Lease, emittiert die App): neuer stt_lease_request-Handler →
  _pick_stt_worker() (voxtral vor whisper) → stt_lease {instanceId}.
- app (audio.ts): requestSttLease() vor dem Stream, stempelt targetInstance
  auf stt_stream_start / stt_audio_chunk / stt_stream_end (+cancel). Kurzer
  Timeout → '' (Broadcast), Aufnahme haengt nie.

Voll rueckwaertskompatibel: Routing aktiviert sich erst, wenn Worker sich per
worker_hello (Stage 2) registriert haben — sonst bleibt alles Broadcast.
Braucht APK-Rebuild fuer die STT-Lease-Seite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-18 12:27:51 +02:00
duffyduckandClaude Opus 4.8 e255d7a659 feat(compute): Worker-Selbstanmeldung ueber RVS + Flotten-Anzeige (Stage 2)
Jeder GPU-Dienst meldet sich beim Connect mit worker_hello {instanceId,
service, node, gpus, model} und haelt die Registry per periodischem
worker_ping {instanceId, busy} (~10s) frisch. So weiss ARIA, was wo laeuft.

- xtts/{voxtral,whisper,f5tts}/bridge.py + llm-adapter/adapter.py:
  INSTANCE_ID=service@NODE_NAME, _worker_register()-Coroutine (hello + ping),
  busy-Quelle je Worker (aktive STT-Sessions / TTS-Render / in-flight LLM);
  Task sauber gecancelt bei Reconnect.
- bridge/aria_bridge.py: self._workers-Registry + Handler worker_hello/
  worker_ping (spiegelt sat_hello), _worker_list() (35s-Offline-TTL),
  _pick_worker() (Round-Robin freie Instanz, fuer Stage-3-Routing),
  /internal/worker-list-Endpoint.
- diagnostic/server.js: workers-Map, worker_hello/worker_ping-Tracking,
  worker_update-Broadcast + worker_list-Action + on-connect-Snapshot.
- diagnostic/index.html: "Compute-Flotte"-Panel im Satelliten-Tab — pro Node
  gruppiert, mit Dienst/Modell/GPU und frei/beschaeftigt/offline-Status.

Stage 2 von 3. Reine Sichtbarkeit, kein Routing-Verhalten geaendert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-18 12:22:47 +02:00
duffyduckandClaude Opus 4.8 95bce35cab feat(speaker-id): Gating als bewusster Schalter — Default AUS (fail-open)
Stefans Repro "nichts geht mehr" kam NICHT von den Marker/Guard-Aenderungen,
sondern von der Speaker-ID: die Bridge-Logs zeigten fast durchgehend
"stt_endpoint mit leerem Text — ignoriert (reason=speaker_mismatch)" — ein aus
einem kaputten Enroll (AAC-als-PCM) entstandener Muell-Fingerprint hat Stefans
EIGENE Stimme abgelehnt und damit die komplette STT lahmgelegt.

Fix: Speaker-ID-Gating ist jetzt ein expliziter Schalter, Default AUS. Bei aus
laeuft die Pruefung GAR NICHT (fail-open, alle Stimmen durch) — ein schlechter
Enroll kann nie wieder alles abwuergen. Damit ist Stefans Problem schon durch den
Voxtral-Rebuild geloest (kein Loeschen noetig, der Check greift einfach nicht).

- voxtral + whisper bridge: SPEAKER_ID_ENABLED (Default False, ENV VOICE_ID_ENABLED),
  _check_speaker faellt bei aus sofort fail-open zurueck; config-Broadcast
  voiceIdEnabled setzt es zur Laufzeit.
- diagnostic: Schalter "Nur meine Stimme" in der Voice-ID-Sektion (Default aus,
  Hinweis: erst an wenn enrollt), broadcastet + persistiert voiceIdEnabled.

Reihenfolge lt. Stefan: erst Konversation sauber, dann Multi-Person/nur-ich.

Deploy: docker compose up -d --build voxtral-bridge; aria-diagnostic neu starten.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-16 01:00:58 +02:00
duffyduckandClaude Opus 4.8 5700b53b56 fix(stt): adaptiver VAD-Schwellwert gegen Satz-Cutoff
Feste RMS-Grenze (0.012) durch rauschboden-relativen Schwellwert ersetzt (fast-down/slow-up, geklammert). Leises/entferntes Sprechen gilt nicht mehr faelschlich als Stille und wird nicht mitten im Satz gecuttet. audio.ts: Fallback-endpointMs 1500->2400 vereinheitlicht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-15 01:21:43 +02:00
duffyduckandClaude Opus 4.8 f2d68e87dd fix(voice): robusteres STT-Endpointing via akustische Stille (nicht nur semantisch)
Bug: die Spracherkennung bricht mal zu frueh ab (nach zwei Worten obwohl noch
gesprochen wird) und merkt mal gar nicht dass man aufgehoert hat.

Ursache: der Endpointer feuerte rein auf semantischer Stagnation (Whisper-
Transkript waechst endpoint_ms lang nicht). Das ist fragil — kurze Sprech-
Pausen oder beam_size=1-Instabilitaet → vorzeitiger Cut; Oszillation/
Halluzination → nie ein Endpoint.

Fix: akustische Stille als robustes Primaersignal. Jeder Tick (~200ms) misst
die RMS-Energie der letzten 300ms:
- Solange Sprach-Energie da ist, bleibt die Session am Leben (kein Cut mitten
  im Reden).
- Endpoint feuert wenn seit endpoint_ms keine Sprach-Energie mehr da ist.
- Semantischer Backstop (2x endpoint_ms) fuer laute Umgebungen (Auto), wo die
  Energie nie faellt — dort degradiert es sauber aufs bisherige Verhalten.

RMS-Threshold (0.012) gegen Stille/Sprache/Fahrgeraeusch verifiziert.
Konstanten oben in der Datei tunebar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 00:31:48 +02:00
duffyduckandClaude Opus 4.7 d807744e35 feat(speaker-id): Phase 3 — Speaker-Gating im Streaming-STT
Sobald eine Streaming-Session ~1.5s Audio im Buffer hat, wird einmal pro
Session der Speaker-ID-Check ausgefuehrt (im Executor, ~50-100ms auf GPU).
Bei Match → Session laeuft normal weiter. Bei Mismatch → synthetisches
stt_endpoint mit text='' reason='speaker_mismatch' + stt_stream_done →
App ruft endConversation. Kein Whisper-Transcribe fuer fremde Stimmen →
Token + Latenz gespart.

- StreamSession: 3 neue Felder (speaker_checked, speaker_match,
  speaker_similarity).
- SessionManager._check_speaker / _finalize_speaker_mismatch:
  Check + sauberes Beenden bei Mismatch.
- _tick_session: Check-Gate vor STREAM_MIN_AUDIO_MS-Check eingehaengt.
- speaker_id.verify: threshold=None statt =DEFAULT_THRESHOLD damit
  config-Broadcast-Updates zur Laufzeit greifen (Default-Arg wird sonst
  zur Def-Zeit gebunden).

Fail-open: ohne Fingerprint returnt verify() (True, 0.0) — keine
Auswirkung. Stefan kann ohne Enrollment weiter wie bisher arbeiten.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 20:41:49 +02:00
duffyduckandClaude Opus 4.7 255e5e5cfe feat(speaker-id): Phase 2 — Enrollment-UI (App) + Voice-ID-Section (Diagnostic)
App-Seite:
- VoiceIdEnrollment.tsx (neue Komponente, ~370 Zeilen): Status-Karte
  (loading/unenrolled/enrolled/error), Sample-Recorder mit Countdown
  (4s fest pro Sample), Liste mit einzelnem Loeschen, Save-Button
  (disabled bis 5 Samples), Fingerprint-Delete mit Confirm.
- SettingsScreen.tsx: neue Section 🎤 'Stimme einrichten' zwischen
  Wake-Word und Sprachausgabe.
- Sample-Format: WAV via audioService.startRecording — wird
  whisper-bridge-seitig per wave-Modul gestrippt.

Diagnostic-Seite:
- Neue settings-section 'Voice-ID (Sprecher-Erkennung)': Status-Anzeige
  (live ueber voice_id_status_response), Threshold-Slider 0.30-0.70
  (persistiert in voice_config.json, broadcast als config-Message),
  Refresh + Delete-Button.
- server.js: 2 neue actions (voice_id_status, voice_id_delete),
  send_voice_config nimmt voiceIdThreshold mit auf.

Backend:
- speaker_id.py: _normalize_audio_bytes erkennt jetzt WAV-Header
  (RIFF/WAVE) und strippt auf rohes PCM — sonst werfen die ECAPA-
  Embeddings auf den 44-Byte-Header rein.
- bridge.py: config-Broadcast-Handler setzt voiceIdThreshold auf
  speaker_id.DEFAULT_THRESHOLD (wird erst in Phase 3 beim Gating
  genutzt, persistiert aber schon).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 20:36:06 +02:00
duffyduckandClaude Opus 4.7 0b28a498b5 feat(speaker-id): Phase 1 — SpeechBrain ECAPA-TDNN Backend in whisper-bridge
Speaker-ID-Modul (Hermes-Style „echtes Gespraech ohne Wake-Word"-Vision,
Phase 1 von 5). Erkennt Stefans Stimme via 192-dim Embedding + Cosine-
Match gegen einen persistierten Fingerprint.

Module:
- speaker_id.py: lazy-loaded ECAPA-TDNN (HuggingFace), enroll/verify/
  status/delete. Fingerprint = L2-normalisierter Mittelwert aus N
  Enrollment-Samples in /voice-id/fingerprint.json.
  Fail-open: kein Fingerprint → verify() returnt (True, 0.0).
- bridge.py: 3 Message-Handler — voice_id_status_request,
  voice_id_enroll_request (samples[]: base64 16kHz int16 PCM),
  voice_id_delete_request. Enrollment laeuft im Executor (Torch
  blockt sonst die Event-Loop).
- Dockerfile: torch 2.3.1 + torchaudio mit CUDA-12.1-Wheels (sonst
  zieht speechbrain CPU-only Torch rein). Container ~1 GB groesser.
- docker-compose.yml: ./voice-id:/voice-id Bind-Mount fuer Fingerprint-
  Persistenz (ueberlebt Container-Restart).
- rvs/server.js: 6 neue Message-Types in ALLOWED_TYPES.

Phase 2 (next): App-Enrollment-Flow + Diagnostic-Voice-ID-Section.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-06 20:26:12 +02:00
duffyduck 968d83a58b fix(whisper): Halluzinations-Filter — kein 'Untertitelung des ZDF' bei Stille
Stefan-Reproduktion: nach Wake-Word + ARIA-Antwort oeffnet das
Conversation-Window automatisch das Mikro fuer Follow-Up. Wenn Stefan
nichts sagt, ist das 4-8s Stille. Whisper halluziniert dann YouTube-
Untertitel-Patterns aus seinem Trainings-Corpus — gemessen 'Untertitelung
des ZDF, 2020' — und ARIA antwortet brav darauf. Endlos-Loop bis Stefan
manuell stoppt.

Fix in faster-whisper-transcribe:

1. Per-Segment no_speech_prob auswerten. Bei >= 0.6 (relativ konservativ:
   echte leise Sprache geht noch durch) → Segment verwerfen. Das eliminiert
   die offensichtlichen Halluzinationen schon zu 90%.

2. Bekannte Hallucination-Phrasen-Blacklist:
     - Untertitelung/Untertitel des ZDF (mit/ohne Jahr)
     - Amara.org community
     - Vielen Dank fuer's Zuschauen (mit allen Umlaut/Apostroph-Varianten)
     - Thanks for watching / Subs by ...
   Substring-Match (case-insensitive) auf normalisiertem Text (lowercase,
   Trailing-Punctuation und Jahres-Suffix '2020' weg).

3. Wenn ALLE Segmente einer Aufnahme rausgefiltert werden, ist text=''
   → App behandelt das via existierende no-speech-Pfad: Conversation-
   Window endet sauber, kein TTS-Echo-Loop.

Tradeoff: echte Phrasen wie 'Vielen Dank' allein gehen durch (Pattern
ist 'vielen dank fuer's zuschauen' — voller Match). Nur die bekannten
Halluzinations-Phrasen werden weggefiltert.

Falls in Zukunft neue Patterns auftauchen (Whispers Modell ändert sich):
einfach _HALLUCINATION_PHRASES erweitern, kein Brain-Restart noetig (lebt
in der Whisper-Bridge, die hot-reloaded werden kann).
2026-06-02 14:19:22 +02:00
duffyduck 9f9c35cf60 feat(diagnostic): RVS-Debug-Logs fuer Whisper- und F5TTS-Bridge
Stefan's Gamebox ist Windows (kein SSH-Zugriff), und in Zukunft
koennten whisper/f5tts auf separaten Hosts laufen. Wir brauchen
deshalb einen Logging-Pfad ueber RVS — gleicher Mechanismus wie
fuer die App (reportAppDebug).

Beide Bridges senden jetzt app_log-Messages mit platform="whisper"
bzw. "f5tts". aria-bridge schreibt sie in /shared/logs/app.log
(unverändert), Live-Logs-Tab + Diagnostic /api/app-log lesen mit.

Toggle via aria-bridge config:
  whisperDebugLog: bool   — default OFF (aktuell aber ON in
                            whisper-bridge weil wir Phase-1/2-
                            Pipeline einfahren)
  f5ttsDebugLog:   bool   — default OFF

Beide werden in voice_config.json persistiert + nach RVS-Connect
rebroadcastet, damit Toggle Container-Restart ueberlebt.

Whisper-Bridge logt aktuell:
  boot                  → Streaming-Mode-Marker (sehen wir damit ob
                          neue Version aktiv ist)
  stream.start          → stt_stream_start angekommen
  stream.chunk          → alle 25 Chunks (=5s Audio) einer
  stream.chunk.reject   → Chunk fuer unbekannte Session
  stream.partial        → Whisper hat neuen Text erkannt
  stream.final          → Endpoint detected, finaler Text raus
  stream.end            → stt_stream_end angekommen
  config                → Toggle umgeschaltet

F5TTS-Helper ist da (gleicher Pattern), Logging-Punkte kommen
spaeter wenn wir ein konkretes TTS-Problem zu debuggen haben.
2026-05-30 22:00:55 +02:00
duffyduck 9488c8a789 feat(whisper): Streaming-Modus mit ML-Endpointer — Phase 1
Neue RVS-Messages auf der Whisper-Bridge:

  stt_stream_start  {requestId, audioRequestId, language?, model?,
                     endpointMs?=1500, hardCapMs?=60000, voice, speed,
                     interrupted, location, sampleRate?=16000}
  stt_audio_chunk   {requestId, pcm: base64-s16le, seq}
  stt_stream_end    {requestId, reason}
  stt_partial       (Bridge→App, alle ~700ms, fuer Live-UI-Feedback)
  stt_endpoint      (Bridge→App+aria-bridge, finaler Text + alle Echos)
  stt_stream_done   (Bridge→App, signalisiert Session-Ende)

Endpointer-Logik:
  - alle 700ms transkribiert die Bridge den Ringbuffer (beam_size=1, schnell)
  - waechst der Transkript-String → Stagnation-Timer reset
  - waechst er nicht → bei endpointMs ohne Wachstum: finalisiert
  - bei hardCapMs (60s) sowieso finalisiert egal ob stagnierend
  - Final-Transcribe nochmal mit beam_size=5 fuer Qualitaet
  - stt_endpoint enthaelt voice/speed/interrupted/location echos,
    damit aria-bridge in Phase 2 direkt an Brain weiterleiten kann

Legacy stt_request (One-Shot mit base64-mp4/wav) bleibt unveraendert
als Fallback.

Default-Parameter (alle vom App-Payload uebersteuerbar):
  STREAM_TRANSCRIBE_INTERVAL_MS = 700    (Throttle)
  STREAM_DEFAULT_ENDPOINT_MS    = 1500   (Stille = kein neuer Text)
  STREAM_DEFAULT_HARD_CAP_MS    = 60000  (Schmerzgrenze)
  STREAM_MIN_AUDIO_MS           = 600    (erst transkribieren ab N Audio)
  STREAM_SESSION_TTL_S          = 120    (tote Sessions aufraeumen)

Ersetzt den dB/VAD-Stille-Trigger auf der App-Seite — Endpointer
hoert auf SEMANTISCHE Stille (kein neuer Text), nicht akustische.
Funktioniert im Auto / mit Musik im Hintergrund / in lauten
Umgebungen wo VAD versagt.
2026-05-30 21:29:51 +02:00
duffyduckandClaude Opus 4.7 4c9116db58 fix(xtts): Sticky-TLS-Fallback in whisper + f5tts Bridges — gleicher Bug wie damals App/Bridge
Stefan-Bug-Report: Diagnostic zeigt seit Tagen 'XTTS-Server: Nicht
verbunden (starte xtts/ auf dem Gaming-PC)' obwohl der Container
laeuft. Keine TTS-Ausgabe, keine STT-Eingabe.

Ursache: exakt der gleiche Sticky-TLS-Fallback-Bug den wir vor ein
paar Tagen bei aria-bridge (commit 3fdd6b9) und Android-App (commit
dfd3ec6) gefixt hatten — die xtts/whisper- und xtts/f5tts-Bridges
sind aber separate Codebases auf der Gamebox und wurden uebersehen.

Mechanik:
1. RVS hatte mal kurzen TLS-Hick (z.B. Caddy-Restart oder Port-Wechsel
   443 → 444 vor Tagen)
2. Bridge versucht wss:// → fail → switch auf ws:// (use_tls = False)
3. Connect klappt jetzt nicht mehr (RVS-Port hatte sich geaendert)
4. Reconnect-Loop bleibt auf ws://, kommt NIE mehr auf wss zurueck
5. Container laeuft, RVS-Status 'nicht verbunden'

Fix: nach jedem Disconnect-Sleep `use_tls = RVS_TLS` und
`tls_fallback_tried = False` zuruecksetzen. Bei jedem Reconnect-
Cycle wird wss neu probiert; falls das wieder failt, switcht's
sauber auf ws fuer den naechsten Versuch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 18:30:09 +02:00
duffyduckandClaude Opus 4.7 3ca599b3ce fix: Voice-Transkription erzwingt kein "small" mehr — nutzt geladenes Modell
f5tts/bridge.py: das hardcoded model="small" in request_transcription war
ein Fehler — whisper-bridge hat dadurch stumm zwischen Modellen geswappt.
Wenn User large-v3 in Diagnostic eingestellt hatte:
  - f5tts Voice-Transkribierung triggerte Swap zu "small"  (+~500MB Laden)
  - Danach aria-bridge schickte naechsten stt_request mit large-v3
    (+~3GB Laden weil small jetzt im RAM war)
Doppelter Load, unnoetiger Traffic.

Fix:
  - f5tts: kein model mehr im payload, whisper-bridge entscheidet
  - whisper: wenn kein payload.model UND bereits ein Modell geladen →
    das behalten. Nur wenn gar nichts da ist fallback auf WHISPER_MODEL
    env default.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 20:03:45 +02:00
duffyduckandClaude Opus 4.7 338793812e debug: Initial-Handshake Logs damit man sieht was passiert
Beim user kommt nach 'RVS verbunden' nichts mehr — Modell-Download
startet nicht, banner aktualisiert sich nicht. Vermutung: alter Code
laeuft noch (kein neu gebauter Container) ODER der Initial-Handshake
crashed silent (asyncio.create_task ohne await schluckt Exceptions).

- whisper + f5tts: Initial-Handshake mit logger.info Zeilen, damit
  man sieht ob er ueberhaupt ausgefuehrt wird
- f5tts: zusaetzlich exception-Catch + fehler-broadcast falls der
  Modell-Load crashed

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:54:12 +02:00
duffyduckandClaude Opus 4.7 85c8d38ea3 fix: HF-Cache zurueck + Banner-Bug + config_request Pattern
Vier Bugs in einem Aufwasch:

1. HF-Cache als Bind-Mount zurueck
   xtts/hf-cache:/root/.cache/huggingface fuer beide Bridges. War vorher
   raus, dadurch jedes Container-Restart = ~3GB Whisper-Download +
   ~1GB F5-TTS-Download. User dachte 5min ist einmalig — ist aber bei
   jedem Restart. Jetzt: einmal pro Maschine geladen, fertig.

2. Banner zeigte stale "ready"
   whisper-bridge sendete beim Connect nur dann Status wenn Modell schon
   geladen war. Sonst blieb der App/Diagnostic Banner auf dem alten
   "ready" State von vor dem Restart haengen — User sah "bereit" obwohl
   gerade gar nichts geladen war. Jetzt wird IMMER ein Status broadcast:
   ready oder loading.

3. config_request Pattern
   aria-bridge wusste nicht wann Gamebox-Bridges sich (re)connecten.
   Wenn die nach aria-bridge kamen, verpassten sie den Config-Broadcast
   und blieben mit Hard-Defaults stehen.
   Jetzt: whisper- und f5tts-bridge senden beim Connect ein
   config_request, aria-bridge antwortet mit der persistierten Config
   (whisperModel, xttsVoice, f5tts*-Felder).

4. RVS ALLOWED_TYPES um config_request erweitert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 17:46:47 +02:00
duffyduckandClaude Opus 4.7 6f4c35cae9 fix: WebSocket max_size hochgedreht — voice_upload sprengte Default 1MB
Symptom: aria-whisper-bridge bekam beim ersten internen stt_request
(via voice_upload mit WAV als base64, ~2.4MB) den Frame zu Gesicht,
default ws-max ist 1MB → mit Close-Code 1009 abgewiesen → Verbindung
tot → naechster stt_request lief in Timeout → lokales Fallback.

Fixes:
- whisper-bridge: max_size=50*1024*1024 in websockets.connect()
  (gleicher Wert wie f5tts-bridge schon hat)
- RVS-Server: maxPayload=50*1024*1024 in WebSocketServer-Optionen,
  damit der Server die Frames nicht selbst auf 1MB cappt bevor er
  sie an die Bridge weiterleitet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:54:36 +02:00
duffyduckandClaude Opus 4.7 62a1f9320b fix(whisper): kein eager preload mehr — wartet auf config-Broadcast
Vorher: Container-Start lud erst 'small' (env default), dann nochmal
das in Diagnostic konfigurierte Modell (z.B. large-v3) wenn die
config-Broadcast vom aria-bridge ankam. Doppelter Download, doppelte
Wartezeit, doppelter VRAM-Peak.

Jetzt:
- Initial wird NICHTS geladen
- aria-bridge sendet die persistierte voice_config.json kurz nach
  RVS-Connect → whisper-bridge sieht den richtigen Modellnamen
- config-Handler erkennt: noch nichts geladen ODER Wechsel
  → loading-Broadcast → ensure_loaded → ready-Broadcast
- stt_request-Handler: gleicher Status-Broadcast falls Race-Condition
  (Spracheingabe in den ersten 1-2s nach Container-Start)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:50:46 +02:00
duffyduckandClaude Opus 4.7 a4746bfd07 feat: HF-Cache raus + service_status Banner in Diagnostic
Stefan akzeptiert die ~5min Modell-Download-Zeit nach jedem Container-
Start, dafuer keine 50GB Cache-Bloat mehr und kein Bind-Mount-Verzeichnis
zu pflegen.

- xtts/docker-compose.yml: hf-cache Bind-Mount entfernt fuer beide
  Bridges. Modelle werden im writable Container-Layer abgelegt und mit
  jedem `docker compose down` automatisch weggeraeumt.
- xtts/.gitignore: hf-cache/ Eintrag raus
- RVS ALLOWED_TYPES: service_status hinzu

Bridges broadcasten Lade-Status:
- f5tts-bridge: bei Connect 'loading' -> ensure_loaded -> 'ready'.
  Auch bei config-getriggertem Modell-Wechsel: erst 'loading' Broadcast,
  dann reload, dann 'ready'.
- whisper-bridge: gleiches Pattern. Modell wird jetzt erst nach
  RVS-Connect geladen damit der loading-Broadcast tatsaechlich rausgeht.

Diagnostic:
- server.js: service_status wird an Browser durchgereicht
- index.html: neues Banner unten rechts (fixed position) zeigt Status
  fuer beide Services. Aggregiert: Icon ist Lupe waehrend Loading,
  Check wenn alles ready, X bei Error.
- Wenn alles ready: X-Button erscheint (manuell schliessen) +
  nach 8s automatisches Fade-Out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 16:21:19 +02:00
duffyduckandClaude Opus 4.7 be30d22296 debug: Logs beim stt_request-Roundtrip — aria-bridge loggt beim Senden,
whisper-bridge loggt eingehende stt_request (id + Audio-Groesse).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 14:13:41 +02:00
duffyduckandClaude Opus 4.7 9b1769269d feat(phase1): Whisper STT auf die Gamebox ausgelagert
Neuer Container aria-whisper-bridge auf der Gamebox — faster-whisper
CUDA mit float16. Der Container verbindet sich per WebSocket an den RVS,
nimmt stt_request entgegen, laeuft ffmpeg+Whisper, antwortet mit
stt_response. Hoert zusaetzlich auf config-Broadcasts und lädt das
Modell hot-swap bei Diagnostic-Wechsel.

aria-bridge ruft jetzt primaer die Gamebox an; nur wenn die nicht binnen
45s antwortet, faellt auf lokales Whisper (CPU) zurueck. Das lokale
Modell wird lazy geladen, spart RAM auf der VM.

RVS: stt_request/stt_response zur ALLOWED_TYPES-Liste.

Diagnostic-Voice-Config (whisperModel-Feld) bleibt unveraendert —
die Auswahl wird an die Gamebox durchgereicht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 13:42:07 +02:00