Files
Aime/docs/05-llm-analyse.md
T
Claude db3d946592 docs: komplette CallRec-Dokumentation mit Mermaid-Diagrammen
Architektur, Hardware/Thermik, PipeWire-Audio-Topologie, Aufnahme-Pipeline,
Diarization (Anchor-Merge), LLM-Analyse (Gemma-3-4B/NPU), iPhone-BT-HFP,
Betrieb/Troubleshooting und Changelog-Chronik.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 10:51:22 +02:00

4.5 KiB
Raw Blame History

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

flowchart TB
    T["Transkript"] --> A{"≤ 4 800 Zeichen?"}
    A -- ja --> SINGLE["analyzeSingle:<br/>EIN Call = bereinigter Dialog + JSON-Analyse<br/>(Fallback: separater JSON-Call bei Echo/Parse-Fehler)"]
    A -- nein --> B{"≤ 20 000 Zeichen?"}
    B -- ja --> WHOLE["analyzeWhole:<br/>GANZES Gespräch in einem Analyse-Call<br/>(kohärente Summary, kleine JSON-Ausgabe)"]
    B -- nein --> LONG["analyzeLong (Map-Reduce):<br/>Chunks à 8 000 Zeichen"]
    LONG --> MAP["MAP: pro Chunk Mini-JSON<br/>(zusammenfassung, teilnehmer, todos)<br/>bewusst minimaler Prompt — lange Regelwände<br/>kippen Gemma in englische Prosa"]
    MAP --> SALV{"JSON parsebar?"}
    SALV -- ja --> COLLECT["einsammeln"]
    SALV -- nein --> PROSE["Prosa retten:<br/>Code-Fences UND rohe {...}-Blobs entfernen<br/>(JSON-Leak-Fix), dann als Teil-Summary nutzen"]
    PROSE --> COLLECT
    COLLECT --> REDUCE["REDUCE: Teil-Summaries (je ≤ 800 Zeichen gekappt)<br/>→ EIN JSON: titel, zusammenfassung,<br/>kernpunkte[], entscheidungen[]<br/>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:
• <Punkt>
• <Punkt>

Entscheidungen:
• <Entscheidung>

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“)

flowchart LR
    G[("glossary-Tabelle<br/>GUI-Tab 'Glossar', /api/glossary")] --> Q{"Eintrag hat<br/>Soundalikes?"}
    Q -- ja --> DET["DETERMINISTISCH: Ganzwort-Ersetzung<br/>direkt auf rec.words VOR der Analyse<br/>(applyGlossaryText / glossaryMap)"]
    Q -- nein --> HINT["nur als Hinweis in den LLM-Prompt<br/>(glossaryHint)"]
    DET --> LLM2["LLM sieht schon den korrekten Begriff"]
    LLM2 --> BELT["Gürtel+Hosenträger: Ersetzung nochmal<br/>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.