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:
parent
cd7aef852d
commit
28c729f1d4
11 changed files with 323 additions and 31 deletions
11
.env.example
11
.env.example
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|---|-----|-----|-----|------------------|
|
|---|-----|-----|-----|------------------|
|
||||||
|
|
|
||||||
13
Makefile
13
Makefile
|
|
@ -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
|
||||||
|
|
|
||||||
88
README.md
88
README.md
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
90
scripts/llm-server/start-llm-server.sh
Executable file
90
scripts/llm-server/start-llm-server.sh
Executable 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"
|
||||||
38
scripts/llm-server/status-llm-server.sh
Executable file
38
scripts/llm-server/status-llm-server.sh
Executable 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"
|
||||||
14
scripts/llm-server/stop-llm-server.sh
Executable file
14
scripts/llm-server/stop-llm-server.sh
Executable 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue