feat(tts): Emojis/Markdown beim Vorlesen entfernen + Autostart/GPU-Doku
- spoken_response_adapter: Emoji-/Symbol-Strip (Emoticons, Symbole, Flaggen, Pfeile, Dingbats, ZWJ/Variationsselektoren). Markdown wurde bereits entfernt. - Geräte-TTS (app.js v28) liest das bereinigte "spoken"-Feld statt des Rohtexts (automatisch + beim Replay); Anzeige bleibt Originaltext. - deploy/ollama-keepalive.conf: Drop-in OLLAMA_KEEP_ALIVE=5m -> GPU im Leerlauf frei. - Doku §4.10: Dauerbetrieb als Dienst + warum der Gateway die GPU nicht blockiert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
317ad2ff97
commit
bd20a308de
5 changed files with 85 additions and 5 deletions
|
|
@ -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
|
## 5. Das System benutzen
|
||||||
|
|
||||||
> 👤 Endnutzer
|
> 👤 Endnutzer
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,22 @@ import re
|
||||||
|
|
||||||
from app.pipeline import german_numbers as gn
|
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:
|
class SpokenResponseAdapter:
|
||||||
async def run(self, text: str, language: str = "de") -> str:
|
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) # inline code
|
||||||
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) # markdown links
|
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) # markdown links
|
||||||
text = re.sub(r"[*_~#>]+", " ", text) # markdown symbols
|
text = re.sub(r"[*_~#>]+", " ", text) # markdown symbols
|
||||||
|
text = _EMOJI_RE.sub("", text) # Emojis/Symbole
|
||||||
|
|
||||||
# Listen entschärfen: Aufzählungspunkte weg, nummerierte Listen zu
|
# Listen entschärfen: Aufzählungspunkte weg, nummerierte Listen zu
|
||||||
# Ordinalwörtern ("1. " -> "erstens, "), damit Piper nicht "eins" sagt.
|
# Ordinalwörtern ("1. " -> "erstens, "), damit Piper nicht "eins" sagt.
|
||||||
|
|
|
||||||
|
|
@ -280,9 +280,10 @@ function replayBubble(div) {
|
||||||
startReplay(info.chunks, btn); // Assistent: gecachtes PCM, sofort & gratis
|
startReplay(info.chunks, btn); // Assistent: gecachtes PCM, sofort & gratis
|
||||||
} else if (isDeviceMode()) {
|
} else if (isDeviceMode()) {
|
||||||
stopAudio();
|
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 {
|
} 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 (!answerEl) answerEl = addMessage("assistant", "", { lang: turnLang() });
|
||||||
if (msg.text) answerEl._textEl.textContent = msg.text;
|
if (msg.text) answerEl._textEl.textContent = msg.text;
|
||||||
// Sprache final festhalten (für Replay) und im Geräte-Modus lokal vorlesen.
|
// Sprache final festhalten (für Replay) und im Geräte-Modus lokal vorlesen.
|
||||||
if (answerEl._replay) answerEl._replay.lang = turnLang();
|
// Für TTS das bereinigte "spoken" (ohne Markdown/Emojis) bevorzugen, anzeigen
|
||||||
if (isDeviceMode() && msg.text) deviceSpeak(msg.text, turnLang());
|
// 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();
|
scrollToBottom();
|
||||||
break;
|
break;
|
||||||
case "emergency":
|
case "emergency":
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,6 @@
|
||||||
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
|
<div id="status" class="w-full max-w-3xl mx-auto mt-1 min-h-[1rem] text-xs text-slate-500 dark:text-slate-400"></div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/app.js?v=27"></script>
|
<script src="/app.js?v=28"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
19
deploy/ollama-keepalive.conf
Normal file
19
deploy/ollama-keepalive.conf
Normal file
|
|
@ -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"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue