# 05 — LLM-Analyse (Gemma-3-4B auf der NPU) ## Modell | | | |---|---| | Modell | **Gemma-3-4B-it**, `gemma3-4b-w8a8g128-16k.rkllm` | | Runtime | rkllm **1.3.0** (Upgrade von 1.2.1 — API- und Chat-Template-Änderungen) | | Quantisierung | **grouped w8a8 (g128)** — plain w8a8 hat Gemma kaputt-quantisiert; w4a16 auf RK3588 nicht unterstützt | | Kontext | 16k konvertiert, **läuft mit 8192** (KV-Cache-RAM auf 8-GB-Board) ≈ 20 000 Zeichen Prompt-Budget | | Tempo | ~5 tok/s Decode → Kurzclip ~5 min, 71-min-Aufnahme ~20 min Analyse | | Historie | Vorher Qwen2.5-3B (fest 4096 Kontext). Gemma ≈ Gemini-Qualität, aber ~3× langsamer. gemma3n-E4B zu groß (10,4 GB). | **Hart:** niemals Online-LLM — alles lokal auf der NPU. ## Pfadwahl nach Transkriptlänge ```mermaid flowchart TB T["Transkript"] --> A{"≤ 4 800 Zeichen?"} A -- ja --> SINGLE["analyzeSingle:
EIN Call = bereinigter Dialog + JSON-Analyse
(Fallback: separater JSON-Call bei Echo/Parse-Fehler)"] A -- nein --> B{"≤ 20 000 Zeichen?"} B -- ja --> WHOLE["analyzeWhole:
GANZES Gespräch in einem Analyse-Call
(kohärente Summary, kleine JSON-Ausgabe)"] B -- nein --> LONG["analyzeLong (Map-Reduce):
Chunks à 8 000 Zeichen"] LONG --> MAP["MAP: pro Chunk Mini-JSON
(zusammenfassung, teilnehmer, todos)
bewusst minimaler Prompt — lange Regelwände
kippen Gemma in englische Prosa"] MAP --> SALV{"JSON parsebar?"} SALV -- ja --> COLLECT["einsammeln"] SALV -- nein --> PROSE["Prosa retten:
Code-Fences UND rohe {...}-Blobs entfernen
(JSON-Leak-Fix), dann als Teil-Summary nutzen"] PROSE --> COLLECT COLLECT --> REDUCE["REDUCE: Teil-Summaries (je ≤ 800 Zeichen gekappt)
→ EIN JSON: titel, zusammenfassung,
kernpunkte[], entscheidungen[]
2 Parse-Versuche, Tail wird geloggt"] REDUCE --> OUT["Ergebnis"] SINGLE --> OUT WHOLE --> OUT ``` ## Strukturierte Zusammenfassung (seit 2026-07-15) Das LLM liefert getrennte Felder; `composeSummary()` baut daraus den Summary-Text: ``` <Überblick, 2–4 Sätze> Kernpunkte: • • Entscheidungen: • ``` GUI rendert mit `white-space: pre-line` (style.css) → Bullets und Absätze sichtbar. Todos bleiben eigenes Feld/eigene Liste. **Teilnehmer kommen NICHT vom LLM**, sondern aus der Diarization (Kap. 04) — das LLM hatte alle im Gespräch *erwähnten* Namen als Teilnehmer gelistet (11 statt 3 bei Aufnahme #69). ## Robustheit gegen Gemma-Marotten (alle real aufgetreten) | Marotte | Gegenmaßnahme | |---|---| | Antwortet Prosa statt JSON bei großen Inputs | kleine Chunks (8 000), minimaler Chunk-Prompt, Reduce-Input gekappt + Retry, Prosa-Salvage | | Kaputtes JSON leakt in die Summary | `{...}`-Blobs werden vor Salvage entfernt | | Todos als Objekte (`{"aufgabe": …}`) statt Strings | `cleanList()` flacht Objekt-Werte zu Strings ab | | `"null"` / `"[object Object]"` / lose `?` in Listen | `cleanList()` filtert/trimmt | | Echot Feldbeschreibungen statt Inhalt | `echoed()`-Guard → separater JSON-Call | | Echot konkrete Beispiele aus dem Prompt (Phantom-Todos, „Kompressor“-Summary) | Korrektur-Regeln bewusst OHNE konkrete Beispiele (`CORRECTION_HINT` abstrakt) | | Erfindet Fantasie-Komposita („Audio-Dualeintragung“) | `NO_INVENT`-Regel in allen Summary-Prompts | | Chunk-Parse-Fehler variieren pro Lauf (2–3 von 10) | nichtdeterministisch, wird per Salvage aufgefangen; bei schwachem Ergebnis einfach „Analyse neu“ | ## Eigennamen-Glossar (Whisper hört z. B. „Aime“ als „Amy“) ```mermaid flowchart LR G[("glossary-Tabelle
GUI-Tab 'Glossar', /api/glossary")] --> Q{"Eintrag hat
Soundalikes?"} Q -- ja --> DET["DETERMINISTISCH: Ganzwort-Ersetzung
direkt auf rec.words VOR der Analyse
(applyGlossaryText / glossaryMap)"] Q -- nein --> HINT["nur als Hinweis in den LLM-Prompt
(glossaryHint)"] DET --> LLM2["LLM sieht schon den korrekten Begriff"] LLM2 --> BELT["Gürtel+Hosenträger: Ersetzung nochmal
auf Titel/Summary/Todos des LLM-Outputs"] ``` Wichtig: Begriffslisten **niemals** als Whisper-`--initial-prompt` (Echo-Loop, verifiziert) und dem 3–4B-Modell keine „ersetze X durch Y“-Anweisung anvertrauen (unzuverlässig) — deshalb die deterministische Text-Ersetzung. ## Zusammenspiel mit Whisper auf der NPU rkllm (LLM) und Whisper-NPU-Encoder können nicht gleichzeitig resident sein (RAM-Contention, äußerte sich als `rc=-6`). Kurzclips (≤ 30 s) nutzen deshalb den CPU-Whisper-Daemon ohne rkllm-Swap; lange Aufnahmen swappen seriell unter dem NPU-Lock.