Phase 8: Lokale KI

Wiki mit semantischer Suche ueber pgvector, Diktat ueber faster-whisper,
Berichtsassistent und Belegerfassung — alles gegen selbst betriebene
Modelle. Dazu zwei Protokollansichten: was die Maschine getan hat und was
Menschen getan haben.

Nichts, was ein Modell erzeugt, gelangt ungeprueft in einen Nachweis. Jede
Ausgabe ist zuerst ein eigener Datensatz, gekennzeichnet, mit zwei gleich
grossen Knoepfen davor. Die Datenbank haelt gegen: Eingabe, Ausgabe und
Modell sind unveraenderlich, geloescht wird nicht, entschieden wird einmal.

Kein Rueckfall auf einen Anbieter: fehlen die Container, fehlt die Funktion.
Dafuer bleibt alles andere ohne Modell vollstaendig benutzbar — geprueft von
vierzehn E2E-Tests, die ohne laufendes Modell durchlaufen.

Postgres bringt jetzt PostGIS und pgvector mit (eigenes Image), Ollama und
faster-whisper stehen im Compose-Profil "ki".

73 neue Unit-Tests, 14 neue E2E-Tests, Version 0.9.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-04 11:06:03 +02:00
co-authored by Claude Opus 5
parent 12243f177d
commit f7ccd9a908
134 changed files with 8624 additions and 72 deletions
+100 -2
View File
@@ -87,6 +87,16 @@ x-app-env: &app-env
LLM_BASE_URL: ${LLM_BASE_URL:-}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_MODELL: ${LLM_MODELL:-}
LLM_TIMEOUT: ${LLM_TIMEOUT:-120}
LLM_EINBETTUNG_MODELL: ${LLM_EINBETTUNG_MODELL:-nomic-embed-text}
LLM_EINBETTUNG_DIMENSIONEN: ${LLM_EINBETTUNG_DIMENSIONEN:-768}
LLM_VISION_MODELL: ${LLM_VISION_MODELL:-}
WHISPER_ENABLED: ${WHISPER_ENABLED:-false}
WHISPER_URL: ${WHISPER_URL:-}
WHISPER_MODELL: ${WHISPER_MODELL:-base}
WHISPER_SPRACHE: ${WHISPER_SPRACHE:-de}
WHISPER_TIMEOUT: ${WHISPER_TIMEOUT:-300}
DIKTAT_MAX_SEKUNDEN: ${DIKTAT_MAX_SEKUNDEN:-600}
GEOCODING_ENABLED: ${GEOCODING_ENABLED:-false}
NOMINATIM_URL: ${NOMINATIM_URL:-}
GEOCODING_TIMEOUT: ${GEOCODING_TIMEOUT:-5}
@@ -144,10 +154,18 @@ services:
logging: *logging
# ---------------------------------------------------------------------------
# PostgreSQL 17 mit PostGIS. Einziger Ort für relationale Daten.
# PostgreSQL 17 mit PostGIS und pgvector. Einziger Ort für relationale Daten.
#
# Eigenes Image, weil FLORIAN beides braucht und es kein offizielles gibt,
# das beides mitbringt — siehe docker/postgres/Dockerfile. Beim ersten
# `docker compose up` wird es gebaut; bei einer bestehenden Installation
# genügt `docker compose build postgres` und ein Neustart des Dienstes. Das
# Datenverzeichnis bleibt unberührt, die Hauptversion ändert sich nicht.
# ---------------------------------------------------------------------------
postgres:
image: postgis/postgis:17-3.5
build:
context: ./docker/postgres
image: florian-postgres:${IMAGE_TAG:-17-3.5-pgvector}
container_name: florian-postgres
restart: unless-stopped
environment:
@@ -318,6 +336,82 @@ services:
- intern
logging: *logging
# ---------------------------------------------------------------------------
# Ollama — Sprachmodelle im eigenen Haus (Phase 8).
#
# Nur mit Profil: docker compose --profile ki up -d
#
# Es gibt bewusst KEINEN Rückfall auf einen Anbieter. Läuft dieser Container
# nicht, fehlen die KI-Funktionen — sie werden nicht durch einen fremden
# Dienst ersetzt. Ein Einsatzbericht spricht über Menschen in ihrer
# schlechtesten Stunde; solche Texte gehören nirgendwo anders hin.
#
# Nach dem ersten Start müssen die Modelle geholt werden:
#
# docker compose exec ollama ollama pull qwen2.5:14b # Text
# docker compose exec ollama ollama pull nomic-embed-text # Einbettungen
# docker compose exec ollama ollama pull llama3.2-vision # Belege (optional)
#
# Auf einem Rechner ohne Grafikkarte läuft das Einbettungsmodell noch flott,
# das Textmodell zäh und das Vision-Modell praktisch nicht. Deshalb sind alle
# drei einzeln konfigurierbar: eine Wehr kann das Wiki durchsuchbar machen,
# ohne Berichte schreiben zu lassen.
#
# Für eine Nvidia-Karte zusätzlich das Compose-Fragment aus dem README
# einbinden (deploy.resources.reservations.devices) — es steht nicht fest
# hier drin, weil es ohne Karte den Start verhindert.
# ---------------------------------------------------------------------------
ollama:
image: ollama/ollama:0.5.7
container_name: florian-ollama
restart: unless-stopped
profiles: [ki]
environment:
# Modelle bleiben geladen, statt nach fünf Minuten aus dem Speicher zu
# fallen. Wer zweimal am Tag einen Bericht schreibt, wartet sonst jedes
# Mal auf das Nachladen.
OLLAMA_KEEP_ALIVE: ${OLLAMA_KEEP_ALIVE:-30m}
OLLAMA_HOST: "0.0.0.0:11434"
volumes:
- ollama-data:/root/.ollama
ports:
- "${OLLAMA_PORT:-11435}:11434"
networks:
- intern
logging: *logging
# ---------------------------------------------------------------------------
# faster-whisper — Spracherkennung fürs Diktat (Phase 8).
#
# Nur mit Profil: docker compose --profile ki up -d
#
# OpenAI-kompatible Schnittstelle, aber vollständig lokal. Das Modell wird
# beim ersten Aufruf geholt und dann behalten.
#
# WHISPER_MODELL steuert die Größe: "tiny" läuft auf jedem Bürorechner,
# "base" ist der brauchbare Kompromiss, "small" erkennt Fachbegriffe
# merklich besser und will dafür Zeit oder eine Grafikkarte.
# ---------------------------------------------------------------------------
whisper:
image: fedirz/faster-whisper-server:latest-cpu
container_name: florian-whisper
restart: unless-stopped
profiles: [ki]
environment:
WHISPER__MODEL: ${WHISPER_MODELL:-base}
WHISPER__INFERENCE_DEVICE: ${WHISPER_GERAET:-cpu}
# Nach zehn Minuten ohne Anfrage den Speicher freigeben. Eine Wehr
# diktiert nicht stündlich.
WHISPER__TTL: ${WHISPER_TTL:-600}
ENABLE_UI: "false"
volumes:
- whisper-data:/root/.cache/huggingface
ports:
- "${WHISPER_PORT:-8087}:8000"
networks:
- intern
logging: *logging
# ---------------------------------------------------------------------------
# ntfy — Push aufs Handy, ohne dass etwas das Haus verlässt (Phase 7).
#
@@ -434,6 +528,10 @@ volumes:
redis-data:
minio-data:
traefik-acme:
# Phase 8: die Modelle. Sie sind mehrere Gigabyte groß und sollen einen
# Neustart des Containers überleben — sonst werden sie jedes Mal neu geholt.
ollama-data:
whisper-data:
networks:
# Nur was von außen erreichbar sein muss, hängt hier drin.