From 98c97ff76cf4850b1e246c1413a337144e4af98a Mon Sep 17 00:00:00 2001 From: wb Date: Mon, 25 May 2026 11:29:05 +0200 Subject: [PATCH] feat(diarization-ui): configurable Ollama settings page + Traefik integration - Add settings table (key/value) to SQLite DB - New /settings page: lazy-load models from Ollama API, configure num_ctx, num_predict, repeat_penalty, repeat_last_n, thinking toggle - Analysis job reads all parameters from DB (fallback to env vars) - Add Traefik labels and network to docker-compose.yml (voicelog.aquantico.lan) Co-Authored-By: Claude Sonnet 4.6 --- app.py | 196 +++++++++++++++++++++++++++++++++++++++++++-- docker-compose.yml | 12 +++ 2 files changed, 203 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 7e47378..d7d9e83 100644 --- a/app.py +++ b/app.py @@ -124,6 +124,12 @@ def init_db(): c.execute("ALTER TABLE jobs ADD COLUMN llm_thinking TEXT") except Exception: pass + try: + c.execute( + "CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT NOT NULL)" + ) + except Exception: + pass # defaults c.execute("INSERT OR IGNORE INTO projects(name, created_at) VALUES (?,?)", ("Default", now_iso())) @@ -147,6 +153,17 @@ def init_db(): ) +def get_setting(key: str, default: str) -> str: + with db() as c: + row = c.execute("SELECT value FROM settings WHERE key=?", (key,)).fetchone() + return row["value"] if row else default + + +def set_setting(key: str, value: str): + with db() as c: + c.execute("INSERT OR REPLACE INTO settings(key, value) VALUES (?,?)", (key, value)) + + def layout(title: str, body: str) -> str: return f""" @@ -272,6 +289,7 @@ window.addEventListener('DOMContentLoaded', () => {{ Prompt ausführen Hintergrundjobs + Einstellungen Health
@@ -383,18 +401,24 @@ def _process_analysis_job(job_id: int): + f"\\nTEXT:\\n{doc['content_md']}\\n" ) - num_ctx = _estimate_num_ctx(llm_prompt) + num_ctx_setting = get_setting("ollama_num_ctx", "auto") + num_ctx = _estimate_num_ctx(llm_prompt) if num_ctx_setting == "auto" else int(num_ctx_setting) _job_set(job_id, llm_prompt=f"[num_ctx={num_ctx}]\n\n{llm_prompt}") with _JOB_STREAM_LOCK: _JOB_STREAMS[job_id] = {"thinking": "", "response": ""} + model = get_setting("ollama_model", OLLAMA_MODEL) + think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false") == "true" + num_predict = int(get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT))) + repeat_penalty = float(get_setting("ollama_repeat_penalty", "1.15")) + repeat_last_n = int(get_setting("ollama_repeat_last_n", "128")) r = requests.post( f"{OLLAMA_BASE_URL}/api/generate", - json={"model": OLLAMA_MODEL, "prompt": llm_prompt, "stream": True, "think": OLLAMA_THINK, "options": { + json={"model": model, "prompt": llm_prompt, "stream": True, "think": think, "options": { "num_ctx": num_ctx, - "num_predict": OLLAMA_NUM_PREDICT, - "repeat_penalty": 1.15, - "repeat_last_n": 128, + "num_predict": num_predict, + "repeat_penalty": repeat_penalty, + "repeat_last_n": repeat_last_n, }}, stream=True, timeout=1200, @@ -529,6 +553,168 @@ def healthz(): return {"ok": True, "api_base": API_BASE, "ollama_base_url": OLLAMA_BASE_URL, "ollama_model": OLLAMA_MODEL, "db_path": DB_PATH} +@app.get("/settings/models") +def settings_models(): + try: + r = requests.get(f"{OLLAMA_BASE_URL}/api/tags", timeout=5) + r.raise_for_status() + names = sorted(m["name"] for m in r.json().get("models", [])) + return JSONResponse({"ok": True, "models": names}) + except Exception as e: + return JSONResponse({"ok": False, "error": str(e), "models": []}) + + +@app.get("/settings", response_class=HTMLResponse) +def settings_page(msg: str = ""): + current_model = get_setting("ollama_model", OLLAMA_MODEL) + current_think = get_setting("ollama_think", "true" if OLLAMA_THINK else "false") == "true" + current_num_ctx = get_setting("ollama_num_ctx", "auto") + current_num_predict = get_setting("ollama_num_predict", str(OLLAMA_NUM_PREDICT)) + current_repeat_penalty = get_setting("ollama_repeat_penalty", "1.15") + current_repeat_last_n = get_setting("ollama_repeat_last_n", "128") + + think_checked = "checked" if current_think else "" + + ctx_options = [("auto", "Automatisch (empfohlen)"), ("4096", "4 096"), ("8192", "8 192"), + ("16384", "16 384"), ("32768", "32 768"), ("65536", "65 536"), ("131072", "131 072")] + ctx_opts_html = "".join( + f"" + for v, l in ctx_options + ) + + body = f""" +
+

Einstellungen

+
+{f"
{msg}
" if msg else ""} +
+
Ollama / LLM
+
+
+ +
+ +
+ + +
+
+ +
+
Aktuell gespeichert: {current_model}
+
+ +
+ + +
Automatisch wählt passend zur Promptgröße.
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ +
+
+ +
+
+
+ +
+
Systemkonfiguration (read-only)
+ + + + + + +
API_BASE{API_BASE}
OLLAMA_BASE_URL{OLLAMA_BASE_URL}
DB_PATH{DB_PATH}
+
+ + +""" + return layout("Einstellungen – VoiceLog", body) + + +@app.post("/settings", response_class=HTMLResponse) +def settings_save( + ollama_model: str = Form(...), + ollama_think: Optional[str] = Form(None), + ollama_num_ctx: str = Form("auto"), + ollama_num_predict: str = Form("16384"), + ollama_repeat_penalty: str = Form("1.15"), + ollama_repeat_last_n: str = Form("128"), +): + set_setting("ollama_model", ollama_model.strip()) + set_setting("ollama_think", "true" if ollama_think == "true" else "false") + set_setting("ollama_num_ctx", ollama_num_ctx.strip()) + set_setting("ollama_num_predict", str(int(float(ollama_num_predict)))) + set_setting("ollama_repeat_penalty", str(float(ollama_repeat_penalty))) + set_setting("ollama_repeat_last_n", str(int(float(ollama_repeat_last_n)))) + return RedirectResponse("/settings?msg=Einstellungen+gespeichert.", status_code=303) + + @app.get("/", response_class=HTMLResponse) def upload_page(msg: str = ""): projects = get_projects() diff --git a/docker-compose.yml b/docker-compose.yml index ec550bf..48e48d6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,18 @@ services: - DB_PATH=/data/ui.db volumes: - diarization_ui_data:/data + networks: + - traefik + labels: + - "traefik.enable=true" + - "traefik.http.routers.openwebui.rule=Host(`voicelog.aquantico.lan`)" + - "traefik.http.routers.openwebui.entrypoints=websecure" + - "traefik.http.routers.openwebui.tls=true" + - "traefik.http.services.openwebui.loadbalancer.server.port=8094" volumes: diarization_ui_data: + +networks: + traefik: + external: true