From 28c729f1d4b07cdd43221e8c88ca29b4e0c5a770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dieter=20Schl=C3=BCter?= Date: Thu, 18 Jun 2026 02:57:57 +0200 Subject: [PATCH] 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 --- .env.example | 11 ++- BEDIENUNGSANLEITUNG.md | 20 +++-- Makefile | 13 ++- README.md | 88 +++++++++++++++++++ app/config.py | 14 ++- app/dependencies.py | 8 +- app/providers/llm/local_openai_compatible.py | 53 ++++++++---- config/voice-assistant.example.toml | 5 +- scripts/llm-server/start-llm-server.sh | 90 ++++++++++++++++++++ scripts/llm-server/status-llm-server.sh | 38 +++++++++ scripts/llm-server/stop-llm-server.sh | 14 +++ 11 files changed, 323 insertions(+), 31 deletions(-) create mode 100755 scripts/llm-server/start-llm-server.sh create mode 100755 scripts/llm-server/status-llm-server.sh create mode 100755 scripts/llm-server/stop-llm-server.sh diff --git a/.env.example b/.env.example index 59d30ad..11df940 100644 --- a/.env.example +++ b/.env.example @@ -45,9 +45,16 @@ DEFAULT_OUTPUT_ENDPOINT=local-default # DEFAULT_STT_PROVIDER=openrouter # DEFAULT_LLM_PROVIDER=local-openai-compatible # 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_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] ) --------- FASTER_WHISPER_MODEL=base # tiny|base|small|medium|large-v3 diff --git a/BEDIENUNGSANLEITUNG.md b/BEDIENUNGSANLEITUNG.md index 5e8452b..28533cd 100644 --- a/BEDIENUNGSANLEITUNG.md +++ b/BEDIENUNGSANLEITUNG.md @@ -29,8 +29,9 @@ Kurzüberblick: [README](README.md). für rein lokalen Betrieb (`local-dev`) nicht - 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 - Ollama-Server (`http://127.0.0.1:11434`) mit einem Modell (`ollama pull llama3.2`) -- Optional: Docker + llama.cpp-Server (`http://127.0.0.1:8001/v1`) — starten mit `make llm-up` + (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 @@ -226,8 +227,8 @@ Profil global umschalten (Entwickler/Admin): `VA_PROFILE=local-dev make run`. ```bash # einmalig: lokales STT installieren pip install -e .[local] -# Server mit kleinem lokalem Ollama-Modell (muss in 'ollama list' stehen): -echo 'LOCAL_LLM_MODEL=llama3.2:latest' >> .env +# lokalen llama.cpp-Server starten (Default: Port 8001, GPU 1, Alias va_llm): +make llm-up # mit make llm-status auf "HTTP OK" warten make run # 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 \ @@ -236,7 +237,8 @@ python scripts/voice_loop.py --recorder arecord --device plughw:5,0 --session hy --llm-provider local-openai-compatible \ --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 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 # einmalig: lokales STT installieren 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 (.onnx + .onnx.json) # 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). @@ -500,7 +504,7 @@ curl -s $URL/api/metrics | jq '.timers | to_entries | Use-Case | Empfehlung | Begründung | |----------|------------|------------| | 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 | 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 -Konstellation: **STT** lokal (`faster-whisper`) → **KI** lokal (Ollama, z. B. `llama3.1:8b`) -→ **TTS** remote (Gemini/Zephyr). Befehl: siehe Hybrid-Beispiel in Teil B1. +Konstellation: **STT** lokal (`faster-whisper`) → **KI** lokal (llama.cpp, großes +unzensiertes Modell `va_llm`) → **TTS** remote (Gemini/Zephyr). Befehl: siehe Hybrid-Beispiel in Teil B1. | | STT | LLM | TTS | API-Kosten/Runde | |---|-----|-----|-----|------------------| diff --git a/Makefile b/Makefile index c44e1c3..175cf50 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ endif PORT ?= 8080 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: @if [ ! -f .env ] && [ -f .env.example ]; then \ @@ -30,3 +30,14 @@ smoke: ensure-env docker-build: 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 diff --git a/README.md b/README.md index d9004d8..0f4f025 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,94 @@ VA_PROFILE=cloud make run # alles über OpenRouter 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 | Methode & Pfad | Zweck | diff --git a/app/config.py b/app/config.py index 3d2cc13..d995fe2 100644 --- a/app/config.py +++ b/app/config.py @@ -117,9 +117,19 @@ class Settings(BaseSettings): default_stt_provider: str = "openrouter" default_llm_provider: str = "local-openai-compatible" 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_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_device: str = "auto" # auto|cpu|cuda faster_whisper_compute_type: str = "default" # default|int8|float16|int8_float16 diff --git a/app/dependencies.py b/app/dependencies.py index 1265433..4c8b0ed 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -55,7 +55,13 @@ STT_REGISTRY = { LLM_REGISTRY = { "openrouter": lambda s: OpenRouterLLMProvider(s.openrouter_api_key, s.openrouter_llm_model), "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, ), } diff --git a/app/providers/llm/local_openai_compatible.py b/app/providers/llm/local_openai_compatible.py index d0bbad1..5985869 100644 --- a/app/providers/llm/local_openai_compatible.py +++ b/app/providers/llm/local_openai_compatible.py @@ -6,32 +6,61 @@ from app.providers.llm.base import LLMProvider, sse_delta 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.api_key = api_key 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]: - 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}) 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( self, text: str, history: list[dict] | None = None, session_id: str | None = None, ) -> str: - payload = { - "model": self.model, - "messages": self._build_messages(text, history), - "temperature": 0.3, - } async with httpx.AsyncClient(timeout=120) as client: response = await client.post( f"{self.base_url}/chat/completions", headers={"Authorization": f"Bearer {self.api_key}"}, - json=payload, + json=self._payload(text, history, stream=False), ) response.raise_for_status() data = response.json() @@ -43,18 +72,12 @@ class LocalOpenAICompatibleLLM(LLMProvider): history: list[dict] | None = None, session_id: str | None = None, ) -> 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 client.stream( "POST", f"{self.base_url}/chat/completions", headers={"Authorization": f"Bearer {self.api_key}"}, - json=payload, + json=self._payload(text, history, stream=True), ) as response: if response.status_code >= 400: body = await response.aread() diff --git a/config/voice-assistant.example.toml b/config/voice-assistant.example.toml index 3a5937c..1bfb4b9 100644 --- a/config/voice-assistant.example.toml +++ b/config/voice-assistant.example.toml @@ -22,8 +22,9 @@ openrouter_tts_model = "openai/gpt-4o-mini-tts" openrouter_tts_voice = "alloy" openrouter_llm_model = "openai/gpt-4.1-mini" -local_llm_base_url = "http://127.0.0.1:11434/v1" -local_llm_model = "llama3.1" +# Lokaler llama.cpp-Server (zentrale, unzensierte KI) - Start: scripts/llm-server/start-llm-server.sh +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. [profiles.local-dev] diff --git a/scripts/llm-server/start-llm-server.sh b/scripts/llm-server/start-llm-server.sh new file mode 100755 index 0000000..d1732e4 --- /dev/null +++ b/scripts/llm-server/start-llm-server.sh @@ -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" diff --git a/scripts/llm-server/status-llm-server.sh b/scripts/llm-server/status-llm-server.sh new file mode 100755 index 0000000..b9c21ee --- /dev/null +++ b/scripts/llm-server/status-llm-server.sh @@ -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" diff --git a/scripts/llm-server/stop-llm-server.sh b/scripts/llm-server/stop-llm-server.sh new file mode 100755 index 0000000..d6a4208 --- /dev/null +++ b/scripts/llm-server/stop-llm-server.sh @@ -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