diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 28371a1..e13a294 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -715,6 +715,41 @@ systemctl --user restart voice-assistant # systemd-Betrieb --- +### 4.10 Dauerbetrieb als Dienst + GPU automatisch frei + +Damit der Gateway beim Booten automatisch startet und nicht im Vordergrund hängt, +läuft er als **systemd-User-Dienst** (Unit: `deploy/voice-assistant.user.service`). + +```bash +cp deploy/voice-assistant.user.service ~/.config/systemd/user/voice-assistant.service +loginctl enable-linger "$USER" # sudo -> Dienst läuft auch ohne Login / nach Reboot +systemctl --user daemon-reload +systemctl --user enable --now voice-assistant +``` + +**Wichtig — der Gateway blockiert die GPU NICHT.** Der Gateway-Prozess läuft auf der +CPU. Die GPU 1 wird allein vom **LLM-Backend** belegt: +- **llama.cpp** (Docker-Container) ist *immer resident* → belegt die GPU dauerhaft, solange er läuft. Daher **nicht** automatisch mitstarten; nur bei Bedarf (`make llm-llamacpp`). +- **Ollama** lädt das Modell erst beim ersten Request in die GPU und gibt sie nach + Leerlauf wieder frei — **wenn** `OLLAMA_KEEP_ALIVE` ein Timeout ist (Default `-1` = nie). + +→ Für „GPU im Leerlauf frei" das Drop-in `deploy/ollama-keepalive.conf` installieren +(setzt `OLLAMA_KEEP_ALIVE=5m`): +```bash +sudo mkdir -p /etc/systemd/system/ollama.service.d +sudo cp deploy/ollama-keepalive.conf /etc/systemd/system/ollama.service.d/keepalive.conf +sudo systemctl daemon-reload && sudo systemctl restart ollama +``` + +So ist GPU 1 standardmäßig frei: Der Gateway läuft (Boot), Ollama hält die GPU nur +während aktiver Nutzung. Du musst nichts mehr manuell stoppen. + +> **Hinweis:** Erst im Dienst-Betrieb funktioniert der **Log-Tab** des Admin-Panels +> (er streamt das Journal der Unit). Im Vordergrund-Betrieb (`make run`) landen die +> Logs nur im Terminal. + +--- + ## 5. Das System benutzen > 👤 Endnutzer diff --git a/app/pipeline/spoken_response_adapter.py b/app/pipeline/spoken_response_adapter.py index 92a369b..fe1a705 100644 --- a/app/pipeline/spoken_response_adapter.py +++ b/app/pipeline/spoken_response_adapter.py @@ -2,6 +2,22 @@ import re from app.pipeline import german_numbers as gn +# Emojis/Piktogramme/Symbole: stoeren das Vorlesen (TTS spricht sie aus oder verschluckt +# sich). Deckt die gaengigen Unicode-Bloecke ab (Emoticons, Symbole, Transport, Flaggen, +# Dingbats, Pfeile, Variationsselektoren, ZWJ). +_EMOJI_RE = re.compile( + "[" + "\U0001F300-\U0001FAFF" # Symbole/Emoticons/Transport/Erweiterungen + "\U00002600-\U000027BF" # Diverse Symbole + Dingbats (☀ ⚠ ✅ ❤ …) + "\U0001F1E6-\U0001F1FF" # Regional-Indikatoren (Flaggen) + "\U00002B00-\U00002BFF" # Symbole/Pfeile (⭐ …) + "\U00002190-\U000021FF" # Pfeile (→ ← …) + "\U00002300-\U000023FF" # Technische Symbole (⌚ ⏰ …) + "\U0000FE00-\U0000FE0F" # Variationsselektoren + "\U0000200D" # Zero-Width-Joiner + "]+" +) + class SpokenResponseAdapter: async def run(self, text: str, language: str = "de") -> str: @@ -15,6 +31,7 @@ class SpokenResponseAdapter: text = re.sub(r"`([^`]*)`", r"\1", text) # inline code text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) # markdown links text = re.sub(r"[*_~#>]+", " ", text) # markdown symbols + text = _EMOJI_RE.sub("", text) # Emojis/Symbole # Listen entschärfen: Aufzählungspunkte weg, nummerierte Listen zu # Ordinalwörtern ("1. " -> "erstens, "), damit Piper nicht "eins" sagt. diff --git a/app/web/app.js b/app/web/app.js index 2ed4863..f04bd9f 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -280,9 +280,10 @@ function replayBubble(div) { startReplay(info.chunks, btn); // Assistent: gecachtes PCM, sofort & gratis } else if (isDeviceMode()) { stopAudio(); - deviceSpeakBtn(text, info.lang, btn); // Geräte-TTS: lokal vorlesen, gratis + // Geräte-TTS: bereinigtes "spoken" bevorzugen (ohne Markdown/Emojis), sonst Rohtext. + deviceSpeakBtn(info.spoken || text, info.lang, btn); } else { - speakBubble(text, info.lang, btn); // Server-TTS via /api/speak + speakBubble(text, info.lang, btn); // Server-TTS via /api/speak (bereinigt selbst) } } @@ -385,8 +386,16 @@ function runTurn(path, onopen) { if (!answerEl) answerEl = addMessage("assistant", "", { lang: turnLang() }); if (msg.text) answerEl._textEl.textContent = msg.text; // Sprache final festhalten (für Replay) und im Geräte-Modus lokal vorlesen. - if (answerEl._replay) answerEl._replay.lang = turnLang(); - if (isDeviceMode() && msg.text) deviceSpeak(msg.text, turnLang()); + // Für TTS das bereinigte "spoken" (ohne Markdown/Emojis) bevorzugen, anzeigen + // bleibt der Originaltext. + if (answerEl._replay) { + answerEl._replay.lang = turnLang(); + answerEl._replay.spoken = msg.spoken || msg.text || ""; + } + if (isDeviceMode()) { + const spoken = msg.spoken || msg.text; + if (spoken) deviceSpeak(spoken, turnLang()); + } scrollToBottom(); break; case "emergency": diff --git a/app/web/index.html b/app/web/index.html index e188d7e..f17d0f3 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -250,6 +250,6 @@
- + diff --git a/deploy/ollama-keepalive.conf b/deploy/ollama-keepalive.conf new file mode 100644 index 0000000..4270a74 --- /dev/null +++ b/deploy/ollama-keepalive.conf @@ -0,0 +1,19 @@ +# systemd-Drop-in fuer den Ollama-System-Dienst: Modelle nach Leerlauf entladen, +# damit die GPU automatisch frei wird (statt OLLAMA_KEEP_ALIVE=-1 = nie entladen). +# +# Installation (root noetig, betrifft den systemweiten ollama-Dienst): +# sudo mkdir -p /etc/systemd/system/ollama.service.d +# sudo cp deploy/ollama-keepalive.conf /etc/systemd/system/ollama.service.d/keepalive.conf +# sudo systemctl daemon-reload +# sudo systemctl restart ollama +# # Pruefen: +# systemctl show ollama -p Environment | tr ' ' '\n' | grep KEEP_ALIVE +# +# Wirkung: Ein geladenes Modell wird nach 5 Minuten Inaktivitaet aus dem VRAM +# entladen -> GPU 1 ist im Leerlauf wieder fuer andere Zwecke frei. Der erste +# Request danach zahlt einmalig die Ladezeit (Kaltstart). +# +# Wert anpassbar: 0 = sofort entladen, 30m = laenger halten, -1 = nie (Default). + +[Service] +Environment="OLLAMA_KEEP_ALIVE=5m"