feat(llm): lokales llama.cpp-Modell (va_llm) als zentrale, sprachoptimierte KI

- scripts/llm-server/: start/stop/status fuer llama.cpp-Server (Port 8001, GPU 1,
  Modell Qwen3.6-35B-A3B-Uncensored, Alias va_llm) - alles per ENV ueberschreibbar
- Defaults auf den lokalen Server umgestellt (config.py, .example-Configs, .env.example)
- Provider local-openai-compatible sprachoptimiert: Reasoning aus
  (chat_template_kwargs.enable_thinking=false) + knapper Sprach-System-Prompt,
  optional max_tokens/temperature - Antwort ~9x schneller, kurze vorlesbare Texte
- Makefile-Targets llm-up/llm-down/llm-status
- Doku (README, BEDIENUNGSANLEITUNG) auf llama.cpp statt Ollama aktualisiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-18 02:57:57 +02:00
commit 28c729f1d4
11 changed files with 323 additions and 31 deletions

View file

@ -45,9 +45,16 @@ DEFAULT_OUTPUT_ENDPOINT=local-default
# DEFAULT_STT_PROVIDER=openrouter # DEFAULT_STT_PROVIDER=openrouter
# DEFAULT_LLM_PROVIDER=local-openai-compatible # DEFAULT_LLM_PROVIDER=local-openai-compatible
# DEFAULT_TTS_PROVIDER=openrouter # DEFAULT_TTS_PROVIDER=openrouter
LOCAL_LLM_BASE_URL=http://127.0.0.1:11434/v1 # Lokaler llama.cpp-Server (zentrale, unzensierte KI). Start: scripts/llm-server/start-llm-server.sh
# LOCAL_LLM_MODEL muss dem --alias des Servers entsprechen (Default: va_llm).
LOCAL_LLM_BASE_URL=http://127.0.0.1:8001/v1
LOCAL_LLM_API_KEY=dummy LOCAL_LLM_API_KEY=dummy
LOCAL_LLM_MODEL=llama3.1 LOCAL_LLM_MODEL=va_llm
# Tempo-Hebel fuer den Sprach-Loop: Reasoning aus + knappe, vorlesbare Antworten.
# LOCAL_LLM_DISABLE_REASONING=true # Qwen3-Denkphase abschalten (deutlich schneller)
# LOCAL_LLM_MAX_TOKENS=0 # 0 = serverseitiges Limit (-n); z. B. 256 kappt lange Antworten
# LOCAL_LLM_TEMPERATURE=0.3
# LOCAL_LLM_SYSTEM_PROMPT=Du bist ein gesprochener Sprachassistent. Antworte kurz ...
# --- Lokales STT (faster-whisper; nur mit pip install -e .[local] ) --------- # --- Lokales STT (faster-whisper; nur mit pip install -e .[local] ) ---------
FASTER_WHISPER_MODEL=base # tiny|base|small|medium|large-v3 FASTER_WHISPER_MODEL=base # tiny|base|small|medium|large-v3

View file

@ -29,8 +29,9 @@ Kurzüberblick: [README](README.md).
für rein lokalen Betrieb (`local-dev`) nicht für rein lokalen Betrieb (`local-dev`) nicht
- Für **lokales STT** (Provider `faster-whisper`): einmalig `pip install -e .[local]` - Für **lokales STT** (Provider `faster-whisper`): einmalig `pip install -e .[local]`
(lädt beim ersten Lauf ein Whisper-Modell). Für **lokales LLM**: ein laufender (lädt beim ersten Lauf ein Whisper-Modell). Für **lokales LLM**: ein laufender
Ollama-Server (`http://127.0.0.1:11434`) mit einem Modell (`ollama pull llama3.2`) llama.cpp-Server (`http://127.0.0.1:8001/v1`) — starten mit `make llm-up`
- Optional: Docker (siehe README, Abschnitt „Lokales LLM"). Großes, unzensiertes Modell, Default-Alias `va_llm`.
- **Docker** (für den lokalen llama.cpp-Server) und eine NVIDIA-GPU
## 2. Installation ## 2. Installation
@ -226,8 +227,8 @@ Profil global umschalten (Entwickler/Admin): `VA_PROFILE=local-dev make run`.
```bash ```bash
# einmalig: lokales STT installieren # einmalig: lokales STT installieren
pip install -e .[local] pip install -e .[local]
# Server mit kleinem lokalem Ollama-Modell (muss in 'ollama list' stehen): # lokalen llama.cpp-Server starten (Default: Port 8001, GPU 1, Alias va_llm):
echo 'LOCAL_LLM_MODEL=llama3.2:latest' >> .env make llm-up # mit make llm-status auf "HTTP OK" warten
make run make run
# in Terminal 2 — Sprech-Loop mit der Hybrid-Kombi (MOTU = plughw:5,0): # in Terminal 2 — Sprech-Loop mit der Hybrid-Kombi (MOTU = plughw:5,0):
python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session hybrid \ python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session hybrid \
@ -236,7 +237,8 @@ python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session hy
--llm-provider local-openai-compatible \ --llm-provider local-openai-compatible \
--tts-provider openrouter --tts-provider openrouter
``` ```
Erster Turn ist langsamer (Whisper- und Ollama-Modell laden), danach zügig. STT-Modell Erster Turn ist langsamer (Whisper-Modell lädt; das LLM-Modell ist nach `make llm-up`
bereits geladen), danach zügig. STT-Modell
und Gerät steuern `FASTER_WHISPER_MODEL`/`FASTER_WHISPER_DEVICE` in `.env`. Satzweises und Gerät steuern `FASTER_WHISPER_MODEL`/`FASTER_WHISPER_DEVICE` in `.env`. Satzweises
Vorlesen ist Standard (früher Ton); `--stream-text` zeigt den Text live dazu. Vorlesen ist Standard (früher Ton); `--stream-text` zeigt den Text live dazu.
@ -245,6 +247,8 @@ Datenschutz). TTS läuft hier über **piper** (lokales, CPU-freundliches Neural-
```bash ```bash
# einmalig: lokales STT installieren # einmalig: lokales STT installieren
pip install -e .[local] pip install -e .[local]
# lokalen llama.cpp-Server starten (großes, unzensiertes Modell):
make llm-up # mit make llm-status auf "HTTP OK" warten
# piper-Binary + Stimme bereitstellen: die Stimm-Dateien (<name>.onnx + <name>.onnx.json) # piper-Binary + Stimme bereitstellen: die Stimm-Dateien (<name>.onnx + <name>.onnx.json)
# liegen im PIPER_VOICES_DIR (Default ~/.local/share/piper/voices). Deutsche Stimmen z. B. # liegen im PIPER_VOICES_DIR (Default ~/.local/share/piper/voices). Deutsche Stimmen z. B.
# von huggingface 'rhasspy/piper-voices' (de_DE-thorsten-high, de_DE-kerstin-low). # von huggingface 'rhasspy/piper-voices' (de_DE-thorsten-high, de_DE-kerstin-low).
@ -500,7 +504,7 @@ curl -s $URL/api/metrics | jq '.timers | to_entries
| Use-Case | Empfehlung | Begründung | | Use-Case | Empfehlung | Begründung |
|----------|------------|------------| |----------|------------|------------|
| Senioren-Standard (kein KI-Rechner zuhause) | **Profil `cloud`** | beste Qualität/Latenz ohne lokale Hardware (~4 s Round-Trip) | | Senioren-Standard (kein KI-Rechner zuhause) | **Profil `cloud`** | beste Qualität/Latenz ohne lokale Hardware (~4 s Round-Trip) |
| Datenschutz / offline | `local-dev` | alles lokal — benötigt echte lokale Modelle (heute Platzhalter) | | Datenschutz / offline | `local-dev` | alles lokal: faster-whisper + llama.cpp (`va_llm`, unzensiert) + piper — benötigt GPU + `make llm-up` |
| Kosten/Ausfallsicherheit | `hybrid` + `*_FALLBACK` | teure Teile lokal, Rest Cloud; automatischer Fallback | | Kosten/Ausfallsicherheit | `hybrid` + `*_FALLBACK` | teure Teile lokal, Rest Cloud; automatischer Fallback |
Empfehlung für den Einstieg: **`cloud`** verwenden, Antwortzeiten mit C2 prüfen, dann Empfehlung für den Einstieg: **`cloud`** verwenden, Antwortzeiten mit C2 prüfen, dann
@ -546,8 +550,8 @@ python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session te
### C5. Hybrid-Konstellation (STT + LLM lokal, TTS remote) — Kostenvergleich ### C5. Hybrid-Konstellation (STT + LLM lokal, TTS remote) — Kostenvergleich
Konstellation: **STT** lokal (`faster-whisper`) → **KI** lokal (Ollama, z. B. `llama3.1:8b`) Konstellation: **STT** lokal (`faster-whisper`) → **KI** lokal (llama.cpp, großes
**TTS** remote (Gemini/Zephyr). Befehl: siehe Hybrid-Beispiel in Teil B1. unzensiertes Modell `va_llm`) **TTS** remote (Gemini/Zephyr). Befehl: siehe Hybrid-Beispiel in Teil B1.
| | STT | LLM | TTS | API-Kosten/Runde | | | STT | LLM | TTS | API-Kosten/Runde |
|---|-----|-----|-----|------------------| |---|-----|-----|-----|------------------|

View file

@ -6,7 +6,7 @@ endif
PORT ?= 8080 PORT ?= 8080
HOST ?= 0.0.0.0 HOST ?= 0.0.0.0
.PHONY: ensure-env install run test smoke docker-build .PHONY: ensure-env install run test smoke docker-build llm-up llm-down llm-status
ensure-env: ensure-env:
@if [ ! -f .env ] && [ -f .env.example ]; then \ @if [ ! -f .env ] && [ -f .env.example ]; then \
@ -30,3 +30,14 @@ smoke: ensure-env
docker-build: docker-build:
docker build -t voice-assistant-gateway . docker build -t voice-assistant-gateway .
# --- Lokales LLM (llama.cpp-Server, zentrale unzensierte KI) -----------------
# Konfig per ENV ueberschreibbar, z. B.: GPU_DEVICE=2 HOST_PORT=8101 make llm-up
llm-up:
bash scripts/llm-server/start-llm-server.sh
llm-down:
bash scripts/llm-server/stop-llm-server.sh
llm-status:
bash scripts/llm-server/status-llm-server.sh

View file

@ -85,6 +85,94 @@ VA_PROFILE=cloud make run # alles über OpenRouter
Aktive Konfiguration prüfen: `curl http://localhost:8080/api/config`. Aktive Konfiguration prüfen: `curl http://localhost:8080/api/config`.
## Lokales LLM (llama.cpp, unzensiert)
Die zentrale KI kann statt OpenRouter ein **lokales, unzensiertes Modell** über einen
llama.cpp-Server (OpenAI-kompatibel) sein. Der Provider `local-openai-compatible`
spricht direkt dagegen — kein Code, nur Server starten + Profil wählen.
```bash
make llm-up # startet den llama.cpp-Container (Default: Port 8001, GPU 1)
make llm-status # Container- + HTTP-Status
make llm-down # stoppt den Container
```
Das Modell wird über das `--alias va_llm` angesprochen; die Defaults zeigen bereits
auf `http://127.0.0.1:8001/v1` mit Modell `va_llm`. Danach genügt ein lokales Profil:
```bash
VA_PROFILE=hybrid make run # STT/TTS remote, Haupt-LLM lokal (unzensiert)
VA_PROFILE=local-dev make run # komplett lokal (faster-whisper / llama.cpp / piper)
```
Alle Server-Parameter sind per ENV überschreibbar (Defaults in Klammern):
| Variable | Bedeutung | Default |
|------------------|---------------------------------------------|---------|
| `HOST_PORT` | Host-Port des Servers | `8001` |
| `GPU_DEVICE` | GPU-Index (von 3 GPUs) | `1` |
| `MODEL_REL_PATH` | Modellpfad relativ zu `HF_HOME` | `models/qwen3/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf` |
| `HF_HOME` | Wurzel der Modell-Sammlung | `~/nvme2n1p7_home/huggingface` |
| `MODEL_ALIAS` | API-Modellname (= `LOCAL_LLM_MODEL`) | `va_llm` |
| `CONTAINER_NAME` | Docker-Containername | `va_llm` |
Beispiel (andere GPU/Port/Modell):
```bash
GPU_DEVICE=2 HOST_PORT=8101 MODEL_REL_PATH=models/qwen3/Qwopus3.6-35B-A3B-v1-Q4_K_M.gguf \
bash scripts/llm-server/start-llm-server.sh
```
> Wird `HOST_PORT`/`MODEL_ALIAS` geändert, müssen `LOCAL_LLM_BASE_URL`/`LOCAL_LLM_MODEL`
> im Gateway (`.env`) entsprechend angepasst werden.
### Tempo im Sprach-Loop
Ein Reasoning-Modell (Qwen3) „denkt" per Default lang und antwortet ausführlich mit
Markdown/Emojis — schlecht zum Vorlesen und spürbar träge. Der Provider
`local-openai-compatible` stellt daher für **gesprochene** Antworten um:
| Setting | Default | Wirkung |
|---------|---------|---------|
| `LOCAL_LLM_DISABLE_REASONING` | `true` | schaltet die Qwen3-Denkphase ab (Time-to-first-word ~9× schneller) |
| `LOCAL_LLM_SYSTEM_PROMPT` | knapper Sprach-Prompt | kurze, vorlesbare Antworten in Fließtext (kein Markdown/Emoji) |
| `LOCAL_LLM_MAX_TOKENS` | `0` (Server-Limit) | optionaler harter Deckel, z. B. `256` |
| `LOCAL_LLM_TEMPERATURE` | `0.3` | Sampling-Temperatur |
> Gemessen am Modell `va_llm`: dieselbe Frage fällt von **5,5 s / 1433 Zeichen**
> (Reasoning an, ausführlich) auf **0,7 s / ~190 Zeichen** (Reasoning aus + Sprach-Prompt).
> Für unzensierte „freie" Gespräche bleibt der Prompt rein formal (nur Kürze/Format,
> keine inhaltlichen Einschränkungen); per `LOCAL_LLM_SYSTEM_PROMPT=` leerbar.
**Zweiter Hebel — STT:** `faster-whisper` läuft per Default auf `auto` (oft CPU) mit
Modell `base`. Auf einer RTX 3090 lohnt `FASTER_WHISPER_DEVICE=cuda` +
`FASTER_WHISPER_COMPUTE_TYPE=float16`; das verkürzt die Transkriptionszeit pro Turn.
### Komplett lokal: Profil `local-dev`
`VA_PROFILE=local-dev` betreibt **alle** KI-Module ohne Cloud. Die Route löst auf zu:
| Modul | Provider | Quelle |
|-------|----------|--------|
| STT | `faster-whisper` | lokales Whisper-Modell |
| **LLM** | `local-openai-compatible` | llama.cpp-Server `http://127.0.0.1:8001/v1`, Modell `va_llm` |
| TTS | `piper` | lokales Stimmmodell |
**Voraussetzungen:**
- **LLM:** llama.cpp-Container läuft (`make llm-up`)
- **STT:** faster-whisper installiert (`pip install -e .[local]`)
- **TTS:** piper-Binary + Stimme vorhanden (siehe `PIPER_*` in `.env.example`)
**Start (Reihenfolge):**
```bash
make llm-up # 35B-Modell laden; mit make llm-status auf "HTTP OK" warten
make run # Gateway nutzt jetzt das lokale, unzensierte Modell als zentrale KI
```
> `VA_PROFILE` ist in `.env` dauerhaft setzbar (aktuell `local-dev`) oder pro Lauf
> voranstellbar (`VA_PROFILE=hybrid make run`). Prüfen: `curl http://localhost:8080/api/config`.
## API-Überblick ## API-Überblick
| Methode & Pfad | Zweck | | Methode & Pfad | Zweck |

View file

@ -117,9 +117,19 @@ class Settings(BaseSettings):
default_stt_provider: str = "openrouter" default_stt_provider: str = "openrouter"
default_llm_provider: str = "local-openai-compatible" default_llm_provider: str = "local-openai-compatible"
default_tts_provider: str = "openrouter" default_tts_provider: str = "openrouter"
local_llm_base_url: str = "http://127.0.0.1:11434/v1" # Lokaler llama.cpp-Server (OpenAI-kompatibel), siehe scripts/llm-server/.
local_llm_base_url: str = "http://127.0.0.1:8001/v1"
local_llm_api_key: str = "dummy" local_llm_api_key: str = "dummy"
local_llm_model: str = "llama3.1" local_llm_model: str = "va_llm" # = --alias des llama.cpp-Servers
# Sprach-Assistent: knappe, vorlesbare Antworten + Reasoning aus = deutlich schneller.
local_llm_system_prompt: str = (
"Du bist ein gesprochener Sprachassistent. Antworte kurz und natuerlich "
"(in der Regel 1-3 Saetze), in reinem Fliesstext ohne Markdown, ohne "
"Aufzaehlungen, ohne Emojis. Formuliere so, wie man es laut vorliest."
)
local_llm_disable_reasoning: bool = True # Qwen3 /no_think: spart die Denkphase
local_llm_max_tokens: int = 0 # 0 = serverseitiges Limit (-n)
local_llm_temperature: float = 0.3
faster_whisper_model: str = "base" # tiny|base|small|medium|large-v3 faster_whisper_model: str = "base" # tiny|base|small|medium|large-v3
faster_whisper_device: str = "auto" # auto|cpu|cuda faster_whisper_device: str = "auto" # auto|cpu|cuda
faster_whisper_compute_type: str = "default" # default|int8|float16|int8_float16 faster_whisper_compute_type: str = "default" # default|int8|float16|int8_float16

View file

@ -55,7 +55,13 @@ STT_REGISTRY = {
LLM_REGISTRY = { LLM_REGISTRY = {
"openrouter": lambda s: OpenRouterLLMProvider(s.openrouter_api_key, s.openrouter_llm_model), "openrouter": lambda s: OpenRouterLLMProvider(s.openrouter_api_key, s.openrouter_llm_model),
"local-openai-compatible": lambda s: LocalOpenAICompatibleLLM( "local-openai-compatible": lambda s: LocalOpenAICompatibleLLM(
s.local_llm_base_url, s.local_llm_api_key, s.local_llm_model s.local_llm_base_url,
s.local_llm_api_key,
s.local_llm_model,
system_prompt=s.local_llm_system_prompt,
disable_reasoning=s.local_llm_disable_reasoning,
max_tokens=s.local_llm_max_tokens,
temperature=s.local_llm_temperature,
), ),
} }

View file

@ -6,32 +6,61 @@ from app.providers.llm.base import LLMProvider, sse_delta
class LocalOpenAICompatibleLLM(LLMProvider): class LocalOpenAICompatibleLLM(LLMProvider):
def __init__(self, base_url: str, api_key: str, model: str): def __init__(
self,
base_url: str,
api_key: str,
model: str,
system_prompt: str = "",
disable_reasoning: bool = True,
max_tokens: int = 0,
temperature: float = 0.3,
):
self.base_url = base_url.rstrip("/") self.base_url = base_url.rstrip("/")
self.api_key = api_key self.api_key = api_key
self.model = model self.model = model
self.system_prompt = (system_prompt or "").strip()
self.disable_reasoning = disable_reasoning
self.max_tokens = max_tokens
self.temperature = temperature
def _build_messages(self, text: str, history: list[dict] | None) -> list[dict]: def _build_messages(self, text: str, history: list[dict] | None) -> list[dict]:
messages = list(history) if history else [] messages: list[dict] = []
# Sprach-System-Prompt zuerst (knappe, vorlesbare Antworten). Etwaige
# System-Nachrichten aus der History (z. B. Nutzer-Erinnerungen) bleiben erhalten.
if self.system_prompt:
messages.append({"role": "system", "content": self.system_prompt})
if history:
messages.extend(history)
messages.append({"role": "user", "content": text}) messages.append({"role": "user", "content": text})
return messages return messages
def _payload(self, text: str, history: list[dict] | None, stream: bool) -> dict:
payload: dict = {
"model": self.model,
"messages": self._build_messages(text, history),
"temperature": self.temperature,
}
if stream:
payload["stream"] = True
if self.max_tokens > 0:
payload["max_tokens"] = self.max_tokens
if self.disable_reasoning:
# Qwen3/llama.cpp: Denkphase abschalten -> schnellere erste Antwort.
payload["chat_template_kwargs"] = {"enable_thinking": False}
return payload
async def complete( async def complete(
self, self,
text: str, text: str,
history: list[dict] | None = None, history: list[dict] | None = None,
session_id: str | None = None, session_id: str | None = None,
) -> str: ) -> str:
payload = {
"model": self.model,
"messages": self._build_messages(text, history),
"temperature": 0.3,
}
async with httpx.AsyncClient(timeout=120) as client: async with httpx.AsyncClient(timeout=120) as client:
response = await client.post( response = await client.post(
f"{self.base_url}/chat/completions", f"{self.base_url}/chat/completions",
headers={"Authorization": f"Bearer {self.api_key}"}, headers={"Authorization": f"Bearer {self.api_key}"},
json=payload, json=self._payload(text, history, stream=False),
) )
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
@ -43,18 +72,12 @@ class LocalOpenAICompatibleLLM(LLMProvider):
history: list[dict] | None = None, history: list[dict] | None = None,
session_id: str | None = None, session_id: str | None = None,
) -> AsyncIterator[str]: ) -> AsyncIterator[str]:
payload = {
"model": self.model,
"messages": self._build_messages(text, history),
"temperature": 0.3,
"stream": True,
}
async with httpx.AsyncClient(timeout=120) as client: async with httpx.AsyncClient(timeout=120) as client:
async with client.stream( async with client.stream(
"POST", "POST",
f"{self.base_url}/chat/completions", f"{self.base_url}/chat/completions",
headers={"Authorization": f"Bearer {self.api_key}"}, headers={"Authorization": f"Bearer {self.api_key}"},
json=payload, json=self._payload(text, history, stream=True),
) as response: ) as response:
if response.status_code >= 400: if response.status_code >= 400:
body = await response.aread() body = await response.aread()

View file

@ -22,8 +22,9 @@ openrouter_tts_model = "openai/gpt-4o-mini-tts"
openrouter_tts_voice = "alloy" openrouter_tts_voice = "alloy"
openrouter_llm_model = "openai/gpt-4.1-mini" openrouter_llm_model = "openai/gpt-4.1-mini"
local_llm_base_url = "http://127.0.0.1:11434/v1" # Lokaler llama.cpp-Server (zentrale, unzensierte KI) - Start: scripts/llm-server/start-llm-server.sh
local_llm_model = "llama3.1" local_llm_base_url = "http://127.0.0.1:8001/v1"
local_llm_model = "va_llm" # = --alias des llama.cpp-Servers
# Reines lokales Setup (eigene Hardware/KI) - z. B. fuer Entwicklung/Offline-Test. # Reines lokales Setup (eigene Hardware/KI) - z. B. fuer Entwicklung/Offline-Test.
[profiles.local-dev] [profiles.local-dev]

View file

@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -euo pipefail
# Startet das zentrale, lokale LLM des Voice-Assistants als llama.cpp-Server
# (OpenAI-kompatibel). Dieses grosse, unzensierte Modell ist die Haupt-KI fuer
# den Provider "local-openai-compatible".
#
# Alles ueber ENV ueberschreibbar, z. B.:
# HOST_PORT=8101 GPU_DEVICE=2 ./start-llm-server.sh
# MODEL_REL_PATH="models/qwen3/anderes-modell.gguf" ./start-llm-server.sh
HF_HOME="${HF_HOME:-/home/dschlueter/nvme2n1p7_home/huggingface}"
MODEL_REL_PATH="${MODEL_REL_PATH:-models/qwen3/Qwen3.6-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M.gguf}"
IMAGE="${IMAGE:-ghcr.io/ggml-org/llama.cpp:server-cuda}"
CONTAINER_NAME="${CONTAINER_NAME:-va_llm}"
HOST_PORT="${HOST_PORT:-8001}"
CONTAINER_PORT="${CONTAINER_PORT:-8000}"
MODEL_ALIAS="${MODEL_ALIAS:-va_llm}"
GPU_DEVICE="${GPU_DEVICE:-1}"
echo "[*] Verwende HF_HOME = $HF_HOME"
echo "[*] Modell = $MODEL_REL_PATH"
echo "[*] GPU device = $GPU_DEVICE (ueberschreibbar: GPU_DEVICE=2 ./start-llm-server.sh)"
echo "[*] Port = $HOST_PORT | Alias = $MODEL_ALIAS | Container = $CONTAINER_NAME"
if [ ! -f "$HF_HOME/$MODEL_REL_PATH" ]; then
echo "[!] Modell-Datei nicht gefunden: $HF_HOME/$MODEL_REL_PATH" >&2
exit 1
fi
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then
echo "[*] Stoppe existierenden Container $CONTAINER_NAME ..."
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
fi
echo "[*] Starte llama.cpp-Server (zentrale Voice-Assistant-KI) ..."
docker run -d \
--gpus "\"device=${GPU_DEVICE}\"" \
--name "$CONTAINER_NAME" \
--restart unless-stopped \
-e HF_HOME="/hf_home" \
-v "$HF_HOME:/hf_home:ro" \
-p "${HOST_PORT}:${CONTAINER_PORT}" \
"$IMAGE" \
-m "/hf_home/${MODEL_REL_PATH}" \
--alias "${MODEL_ALIAS}" \
-c 262144 \
-n 16384 \
--jinja \
--reasoning on \
--no-context-shift \
--temp 0.65 \
--top-p 0.80 \
--top-k 20 \
--min-p 0.01 \
--repeat-penalty 1.05 \
--main-gpu 0 \
-ngl 999 \
-fa on \
--kv-unified \
--cache-type-k q4_0 \
--cache-type-v q4_0 \
--batch-size 1024 \
--ubatch-size 512 \
--parallel 1 \
--cont-batching \
--host 0.0.0.0 \
--port "$CONTAINER_PORT"
echo "[*] Warte auf Modell-Bereitschaft (Completion-Check, max. 300 s) ..."
MODEL_READY=0
for i in {1..150}; do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \
-X POST "http://localhost:${HOST_PORT}/v1/chat/completions" \
-H "Content-Type: application/json" \
-d "{\"model\":\"${MODEL_ALIAS}\",\"messages\":[{\"role\":\"user\",\"content\":\"ping\"}],\"max_tokens\":1,\"temperature\":0.0,\"stream\":false}") || HTTP_CODE="000"
if [ "$HTTP_CODE" = "200" ]; then MODEL_READY=1; break; fi
echo " [${i}/150] HTTP ${HTTP_CODE:-000} — Modell laedt noch, warte 2s ..."
sleep 2
done
if [ "$MODEL_READY" -ne 1 ]; then
echo "[!] Modell wurde nicht rechtzeitig bereit (kein HTTP 200 auf Completion)." >&2
docker logs --tail 200 "$CONTAINER_NAME" || true
exit 1
fi
echo "[*] Modell bereit — erster Completion-Request erfolgreich (HTTP 200)."
echo "[*] Server laeuft auf http://0.0.0.0:${HOST_PORT}/v1 (Alias: ${MODEL_ALIAS})"
echo "[*] Setze im Gateway: LOCAL_LLM_BASE_URL=http://127.0.0.1:${HOST_PORT}/v1 LOCAL_LLM_MODEL=${MODEL_ALIAS}"
echo "[*] Stoppen mit: scripts/llm-server/stop-llm-server.sh"

View file

@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Zeigt Container- und HTTP-Status des lokalen LLM-Servers (llama.cpp).
# Name/Port ueber ENV ueberschreibbar: CONTAINER_NAME=... HOST_PORT=... ./status-llm-server.sh
CONTAINER_NAME="${CONTAINER_NAME:-va_llm}"
HOST_PORT="${HOST_PORT:-8001}"
check_server() {
local NAME="$1"
local PORT="$2"
printf "%-28s" "$NAME (Port $PORT):"
# Docker-Status
if docker ps --format '{{.Names}}' | grep -q "^${NAME}\$"; then
printf " Container=\033[32mRUNNING\033[0m"
elif docker ps -a --format '{{.Names}}' | grep -q "^${NAME}\$"; then
printf " Container=\033[33mSTOPPED\033[0m"
else
printf " Container=\033[31mNOT FOUND\033[0m"
echo
return
fi
# HTTP-Erreichbarkeit
if curl -s --max-time 3 "http://localhost:${PORT}/health" >/dev/null 2>&1 || \
curl -s --max-time 3 "http://localhost:${PORT}/v1/models" >/dev/null 2>&1; then
printf " HTTP=\033[32mOK\033[0m"
else
printf " HTTP=\033[31mNOT READY\033[0m"
fi
echo
}
echo "=== Voice-Assistant LLM-Server Status ==="
check_server "$CONTAINER_NAME" "$HOST_PORT"

View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
set -euo pipefail
# Stoppt den lokalen LLM-Server (llama.cpp) des Voice-Assistants.
# Container-Name ueber ENV ueberschreibbar: CONTAINER_NAME=... ./stop-llm-server.sh
CONTAINER_NAME="${CONTAINER_NAME:-va_llm}"
if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}\$"; then
docker rm -f "$CONTAINER_NAME" >/dev/null
echo "[*] Gestoppt: $CONTAINER_NAME"
else
echo "[-] Nicht gefunden: $CONTAINER_NAME"
fi