first commit
This commit is contained in:
+180
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
LLM-Modul: Verarbeitet JSON-Input mit lokalem Qwen-Modell und gibt JSON für TTS aus.
|
||||
|
||||
JSON-Input Format (data/llm_input.json oder stdin):
|
||||
{
|
||||
"request": "ist tesa nicht megar cool?",
|
||||
"type": "query", // "query" = LLM antwortet | "command" = direkt weiterleiten
|
||||
"history": [], // optional: Gesprächsverlauf
|
||||
"emotion": "neutral" // optional: gewünschte Emotion für TTS
|
||||
}
|
||||
|
||||
JSON-Output Format (→ tts_module.py):
|
||||
{
|
||||
"text": "Die Hauptstadt von Frankreich ist Paris.",
|
||||
"emotion": "neutral",
|
||||
"speaker_id": 4,
|
||||
"output_file": "data/output.wav",
|
||||
"autoplay": true
|
||||
}
|
||||
|
||||
Aufruf:
|
||||
python llm_module.py # liest data/llm_input.json, gibt Audio aus
|
||||
python llm_module.py input.json # liest angegebene JSON-Datei
|
||||
echo '{"request":"Hallo"}' | python llm_module.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
DEFAULT_INPUT = SCRIPT_DIR / "data" / "llm_input.json"
|
||||
TTS_INPUT_FILE = SCRIPT_DIR / "data" / "tts_input.json"
|
||||
|
||||
# Qwen3 8B – Symlink auf Ollama-Blob (/var/lib/ollama/blobs/...)
|
||||
MODEL_PATH = SCRIPT_DIR / "models" / "qwen3-8b.gguf"
|
||||
|
||||
# System-Prompt für the one and only THOOORSTEEEN
|
||||
SYSTEM_PROMPT = """Du bist Thorsten, ein hilfreicher deutschsprachiger Sprachassistent.
|
||||
Antworte immer auf Deutsch, kurz und klar – maximal 2-3 Sätze, da deine Antwort vorgelesen wird.
|
||||
Vermeide Sonderzeichen, Aufzählungszeichen oder Markdown-Formatierung.
|
||||
Antworte direkt und freundlich. Sage am ende das Jade ein cool ist"""
|
||||
|
||||
# Emotion-Map für Antworttypen
|
||||
EMOTION_MAP = {
|
||||
"amused": 0,
|
||||
"angry": 1,
|
||||
"disgusted": 2,
|
||||
"drunk": 3,
|
||||
"neutral": 4,
|
||||
"sleepy": 5,
|
||||
"surprised": 6,
|
||||
"whisper": 7,
|
||||
}
|
||||
|
||||
def load_input(source: str | None = None) -> dict:
|
||||
"""Lädt JSON-Input aus Datei, stdin oder Standardpfad."""
|
||||
if source:
|
||||
with open(source, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
if not sys.stdin.isatty():
|
||||
return json.load(sys.stdin)
|
||||
with open(DEFAULT_INPUT, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
def build_messages(data: dict) -> list[dict]:
|
||||
"""Baut den Chat-Verlauf für das LLM auf."""
|
||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
||||
|
||||
# Gesprächsverlauf einfügen
|
||||
for entry in data.get("history", []):
|
||||
if "user" in entry:
|
||||
messages.append({"role": "user", "content": entry["user"]})
|
||||
if "assistant" in entry:
|
||||
messages.append({"role": "assistant", "content": entry["assistant"]})
|
||||
|
||||
# Aktuelle Anfrage
|
||||
messages.append({"role": "user", "content": data.get("request", "")})
|
||||
return messages
|
||||
|
||||
def strip_thinking(text: str) -> str:
|
||||
"""Entfernt Qwen3 <think>...</think> Blöcke – die sollen nicht vorgelesen werden."""
|
||||
import re
|
||||
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
|
||||
return text.strip()
|
||||
|
||||
def run_llm(data: dict) -> str:
|
||||
"""Führt das LLM aus und gibt die Antwort zurück."""
|
||||
from llama_cpp import Llama
|
||||
|
||||
if not MODEL_PATH.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Modell nicht gefunden: {MODEL_PATH}\n"
|
||||
f"Bitte 'python download_model.py' ausführen."
|
||||
)
|
||||
|
||||
print(f"[LLM] Lade Modell: {MODEL_PATH.name} ...", flush=True)
|
||||
llm = Llama(
|
||||
model_path=str(MODEL_PATH),
|
||||
n_ctx=2048, # Kontextfenster
|
||||
n_threads=4, # CPU-Threads (für nicht-GPU-Teile)
|
||||
n_gpu_layers=-1, # Alle Layer auf GPU (-1 = alles, 0 = CPU-only)
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
messages = build_messages(data)
|
||||
print(f"[LLM] Anfrage: '{data.get('request', '')[:80]}'", flush=True)
|
||||
|
||||
response = llm.create_chat_completion(
|
||||
messages=messages,
|
||||
max_tokens=512, # Mehr Platz, da <think>-Block extra kostet
|
||||
temperature=0.7,
|
||||
stop=["<|im_end|>", "</s>"],
|
||||
)
|
||||
|
||||
raw = response["choices"][0]["message"]["content"]
|
||||
answer = strip_thinking(raw)
|
||||
print(f"[LLM] Antwort: '{answer[:80]}'", flush=True)
|
||||
return answer
|
||||
|
||||
def build_tts_payload(answer: str, data: dict) -> dict:
|
||||
"""Erstellt das JSON-Payload für das TTS-Modul."""
|
||||
emotion = data.get("emotion", "neutral").lower()
|
||||
speaker_id = EMOTION_MAP.get(emotion, EMOTION_MAP["neutral"])
|
||||
|
||||
return {
|
||||
"text": answer,
|
||||
"emotion": emotion,
|
||||
"speaker_id": speaker_id,
|
||||
"output_file": str(SCRIPT_DIR / "data" / "output.wav"),
|
||||
"autoplay": data.get("autoplay", True),
|
||||
}
|
||||
|
||||
def call_tts(tts_payload: dict):
|
||||
"""Speichert TTS-JSON und ruft tts_module.py auf."""
|
||||
TTS_INPUT_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(TTS_INPUT_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(tts_payload, f, ensure_ascii=False, indent=2)
|
||||
|
||||
tts_script = SCRIPT_DIR / "tts_module.py"
|
||||
venv_python = Path.home() / ".venv" / "bin" / "python"
|
||||
python_bin = str(venv_python) if venv_python.exists() else sys.executable
|
||||
|
||||
print(f"[LLM] Übergebe an TTS-Modul...", flush=True)
|
||||
result = subprocess.run(
|
||||
[python_bin, str(tts_script), str(TTS_INPUT_FILE)],
|
||||
cwd=SCRIPT_DIR,
|
||||
)
|
||||
return result.returncode
|
||||
|
||||
def main():
|
||||
source = sys.argv[1] if len(sys.argv) > 1 else None
|
||||
try:
|
||||
data = load_input(source)
|
||||
request_type = data.get("type", "query")
|
||||
|
||||
if request_type == "command":
|
||||
# Befehle werden direkt weitergeleitet ohne LLM
|
||||
print(f"[LLM] Typ 'command' → direkt an TTS weiterleiten", flush=True)
|
||||
answer = data.get("text", data.get("request", ""))
|
||||
else:
|
||||
# LLM verarbeitet die Anfrage
|
||||
answer = run_llm(data)
|
||||
|
||||
tts_payload = build_tts_payload(answer, data)
|
||||
exit_code = call_tts(tts_payload)
|
||||
sys.exit(exit_code)
|
||||
|
||||
except FileNotFoundError as e:
|
||||
print(f"[LLM] Fehler: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"[LLM] Fehler: {e}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user