Nicht mehr für Stille zahlen: Silence-Trim für Cloud-STT und Idle-Unload für lokales Whisper in Hermes Agent


Sprachnachrichten sind auf den Messaging-Plattformen von Hermes Agent die natürlichste Eingabe — Taste gedrückt halten, sprechen, und das Transkript geht an den Agenten. Aber wenn du einen Cloud-Speech-to-Text-Anbieter (STT) nutzt, kostet dich jede Sprachnachricht heimlich doppelt: einmal für die Stille.

Eine 13-Sekunden-Sprachnachricht enthält vielleicht nur 6 Sekunden echte Sprache — die anderen 7 Sekunden bist du am Nachdenken. Cloud-Whisper wird pro Audiominute abgerechnet, und Stille wird zum gleichen Satz abgerechnet wie Sprache (OpenAI verlangt $0.006/min dafür). Schlimmer noch: Whisper halluziniert Wörter in stillen Passagen — deshalb enthalten Cloud-Transkripte gelegentlich Sätze, die nie gesprochen wurden.

Zwei kürzlich gemergte Hermes-Agent-PRs beheben genau diese beiden Probleme, beide im Wesentlichen Plug-and-Play (eine standardmäßig aktiv, die andere eine einzige Config-Zeile):

  1. #77581 — Silence-Trim vor dem Upload für Cloud-STT: kürzt lange Pausen mit ffmpeg vor dem Upload. Gemessenes Ergebnis: eine 13.2s-Sprachnachricht auf 6.2s gekürzt (-53%) mit einem vollständig äquivalenten Transkript.
  2. #81027 — Idle-Unload für das lokale Whisper-Modell: entlädt das lokale Modell nach 5 Minuten Inaktivität automatisch, gibt ~370MB RAM/VRAM frei und lädt es bei der nächsten Sprachnachricht transparent neu.

Hintergrund: zwei STT-Routen — lokal und Cloud

Vor den neuen Funktionen war das die Architektur. STT lebt im Abschnitt stt der config.yaml; provider wählt die Route:

stt:
  enabled: true
  provider: local          # local | groq | openai | mistral | xai | elevenlabs | deepinfra
  language: "en"           # global language hint, avoids wrong-language detection on short clips

Lokale Route (local): faster-whisper läuft auf deinem eigenen Rechner — kostenlos, privat, aber das Modell bleibt im Speicher resident. Eine frühere Härtung hatte bereits Silero VAD (Voice Activity Detection) hinzugefügt, sodass Stille das Modell nie erreicht.

Cloud-Route (groq/openai/mistral/xai/elevenlabs/deepinfra): Audio wird roh an eine Drittanbieter-API hochgeladen und pro Audiominute abgerechnet. Das Problem: die Cloud-Route hatte nie einen VAD-äquivalenten Schutz — rohes Audio, inklusive Pausen, ging unangetastet hoch.

Die beiden PRs schließen genau diese Lücken: Silence-Trim für die Cloud-Route, Idle-Unload für die lokale Route.

Teil 1 — Cloud-Silence-Trim: die Pausen vor dem Upload kürzen

Aktivieren

Standardmäßig aktiv — drei Einstellungen:

stt:
  cloud_trim_silence: true      # false = always upload the original audio
  cloud_trim_threshold_db: -40  # audio quieter than this counts as silence
  cloud_trim_keep_ms: 300       # keep 300ms of each pause, preserving word boundaries and pacing

Der Ablauf: vor dem Upload werden bei jedem Clip, der länger als 12 seconds ist, die langen Pausen per ffmpeg-silenceremove-Filter auf 300ms-Lücken gestaucht (Audio unter -40dB gilt als Stille), dann wird die gekürzte Version hochgeladen. ffmpeg war bereits eine Abhängigkeit genau dieses Codepfads (CAF-Transkodierung), also keine neue Abhängigkeit.

Gemessene Zahlen (echter Lauf des PR-Autors)

original: 13.15 s   (speech 3s + pause 7s + speech 3s)
INFO Trimmed silence from voicenote.wav before cloud STT upload (13.2s -> 6.2s, -53%)
trimmed:  6.24 s

Das gekürzte Audio wurde mit faster-whisper verifiziert: Beide Äußerungen transkribieren identisch zum Original — Pausen weg, Sprache intakt.

Das 12-Sekunden-Gate: warum kurze Clips das Trim überspringen

Oben stand überall „Clips länger als 12s“. Dieses Gate ist bewusste Kostenkontrolle: Kurze Clips bekommen ein ~50ms-ffprobe und überspringen das Encoding. Die Begründung ist konkret — bei einem Clip unter 12s beträgt die maximal mögliche Ersparnis ~10 %, etwa 1 Sekunde Audio, und mehrere Anbieter berechnen ohnehin ein Minimum pro Request (Groq rechnet ein 10s-Minimum ab). Das Encoding kann sich bei kurzen Clips nie bezahlt machen; nur Clips, die lang genug sind, um plausibel zu profitieren, tragen die Encoding-Kosten.

Streng Best-Effort: Trimmen kann Transkription nie brechen

Das ist der Kern des Designs: das Trim ist Best-Effort — jeder Fehlerfall lädt das Original unangetastet hoch, und die Transkription schlägt nie wegen des Trims fehl:

Bedingung Verhalten
cloud_trim_silence: false Original wird hochgeladen
ffmpeg/ffprobe fehlt Original wird hochgeladen
Clip kürzer als 12s Original wird hochgeladen (ein Probe, kein Encoding)
Trim-Befehl schlägt fehl / Timeout Original wird hochgeladen
Getrimmtes Ergebnis ~leer (Clip fast nur Stille) Original wird hochgeladen — der Provider, nicht eine clientseitige dB-Heuristik, entscheidet, ob es Sprache enthält
Trim spart <10% Original wird hochgeladen

Die letzten beiden Zeilen verdienen einen zweiten Blick: Ob eine fast stille Aufnahme „Sprache enthält“, überlässt man dem Provider (der eine eigene Spracherkennung hat), und ein erneutes Encoding für eine Ersparnis von <10% ist reine Verschwendung — also gibt das Trim einfach auf.

Wann man es deaktivieren sollte

Die -40dB-Schwelle behandelt ruhige Umgebungen als Stille. Wenn deine Sprachnachrichten oft Musik, Umgebungsgeräusche oder weißes Rauschen statt Sprache enthalten (z. B. wenn du den Agenten bittest, einen Song oder eine Feldaufnahme zu identifizieren), setze cloud_trim_silence: false, um wieder rohe Uploads zu bekommen — dieselbe Philosophie wie vad: false auf der lokalen Route.

Teil 2 — Idle-Unload für lokales Whisper: das 370MB-Leck, das du nicht bemerkt hast

Das Problem: einmal laden, für immer halten

Das lokale faster-whisper-Modell ist ein Singleton: Es lädt bei der ersten Sprachnachricht und gibt nie wieder frei — für die gesamte Lebensdauer des Prozesses. Das base-Modell hält grob 370MB, selbst wenn stunden- oder tagelang keine Sprachnachricht eintrifft.

Das ist besonders verschwenderisch bei langlebigen Hermes-Gateway-Prozessen — vor allem auf Maschinen, auf denen Whisper mit einem lokalen LLM um dieselbe GPU konkurriert: Die VRAM, die Whisper belegt, ist VRAM, die dein lokales Modell nicht nutzen kann.

Aktivieren

stt:
  local:
    model: "base"           # tiny | base | small | medium | large-v3
    unload_after_idle_seconds: 300   # 0 = never unload (default); 300 = unload after 5 idle minutes

Der Standardwert 0 bedeutet: nie entladen — null Verhaltensänderung für bestehende Nutzer. Setze ihn auf 300 (für Gateway-Setups empfohlen) und:

  • Ein Watcher-Thread prüft alle 30 Sekunden; sobald die Inaktivitätszeit die Schwelle überschreitet, wird die Modellreferenz für den GC freigegeben
  • Die nächste Sprachnachricht lädt es über den bestehenden Lazy-Load-Pfad transparent neu — kein für Nutzer sichtbarer Unterschied außer einer Modell-Ladeverzögerung
  • Die Config wird in jedem Zyklus neu gelesen: Ändere den Wert in config.yaml, und er wirkt innerhalb eines Prüfintervalls — kein Prozess-Neustart; ein Zurücksetzen auf 0 mitten in der Inaktivität bricht sogar das anstehende Entladen ab

Die ehrlichen Speicherzahlen: GPU vs. CPU

Der PR-Autor hat beide ehrlich gemessen, und es lohnt sich, das zu zitieren:

  • CUDA/GPU: Sobald das Modellobjekt vom GC abgeräumt ist, wird die VRAM an das Gerät zurückgegeben — der große Gewinn für Setups mit GPU-Sharing.
  • CPU (auf macOS gemessen): Python-Referenzen werden freigegeben und der Speicher wird wiederverwendbar, aber der C++-Allokator von ctranslate2 gibt die Seiten nicht an das OS zurück, sodass sich die RSS kaum bewegt (gemessen: 388MB vorher und nachher). Unter Linux kann das malloc_trim-Verhalten von glibc einiges zurückgeben.

Kurz gesagt: auf der GPU ist es eine echte Freigabe; auf der CPU macht es den Speicher vor allem wiederverwendbar — das Modell belegt keine hunderte MB an Live-Objekten mehr, und ein später konfiguriertes Modell anderer Größe lädt in den zurückgewonnenen Platz, statt den Prozess weiter wachsen zu lassen. So oder so gilt nicht mehr „einmal laden, für immer halten“.

Teil 3 — Empfohlene Configs pro Anwendungsfall

Szenario Empfehlung
Langlebiges Gateway, lokales LLM auf derselben GPU local.unload_after_idle_seconds: 300 (dringend empfohlen — VRAM wird wirklich freigegeben)
Desktop/CLI, gelegentlich Voice, knapper RAM local.unload_after_idle_seconds: 600 — Entladen nach 10 Minuten Inaktivität
Häufige Sprachnachrichten, latenzsensitiv 0 behalten — die Modell-Ladezeit bei der ersten Nachricht vermeiden
Cloud-STT (groq/openai usw.) cloud_trim_silence: true behalten (Standard) — Kosten sinken sofort
Sprachnachrichten sind oft Musik/Ambient cloud_trim_silence: false

Nach dem Bearbeiten öffnest du die Datei mit hermes config edit oder prüfst die aktuellen Werte mit hermes config get stt.local.unload_after_idle_seconds.

Vier praktische Tipps

  1. Cloud-STT + kurze Sprachbefehle ist die beste Kombination: Das 12s-Gate bedeutet, dass gewöhnliche Befehle („wie wird das Wetter morgen“) nie das Trim auslösen oder die Encoding-Kosten zahlen — getrimmt wird nur bei langen Sprachnachrichten.
  2. Den Language-Hint nicht weglassen: Ein globales stt.language: "en" gilt auch für Cloud-Anbieter (die Config pro Anbieter gewinnt), und kurze Sprachbefehle scheitern häufig, weil die Whisper-Autoerkennung die falsche Sprache errät.
  3. Prüfen, bevor du änderst: hermes config get stt.cloud_trim_silence zeigt den effektiven Wert direkt an; führe nach Änderungen hermes config check aus, um die Syntax zu validieren.
  4. Lokal und Cloud sind je nach Bedarf austauschbar: Ändere das Feld provider jederzeit. Null Kosten gewünscht? local (kostenlos, aber speicherresident — kombiniere es mit Idle-Unload). Hochgenaue Mehrsprachigkeit gewünscht? Cloud-Route — und das Silence-Trim hält die Rechnung im Zaum.

Zusammenfassung

Zusammen räumen die beiden PRs beide STT-Routen auf: Die Cloud-Route bezahlt nicht mehr für Pausen und bekommt keine durch Stille-Halluzinationen verschmutzten Transkripte; die lokale Route belegt keine 370MB RAM/VRAM mehr im Leerlauf. Kleine Configs, null neue Abhängigkeiten, rein inkrementelle Gewinne — Nutzer mit viel Voice (besonders Gateway-residente + Cloud-STT-Setups) sollten das heute einschalten.

Für mehr Tiefe zu den Voice-Fähigkeiten von Hermes Agent und der zugehörigen Konfiguration: Sieh dir unsere Release-Notes zum v0.19.1 Voice Patch, die Installationsanleitung und die Release-Notes zum v0.20.0 Herald an.