feat(analyse): Kontextnotiz zum Gespraech nach dem Stop

Ein Freitextfeld ("Kontext zum Gespraech") in der Detailansicht, nach dem
Stop aufgeklappt. Der Text geht als Hintergrundwissen in die LLM-Analyse:
Personen, Firmen, Anlass und Eigennamen muessen nicht mehr aus dem
fehlerbehafteten ASR-Text erraten werden.

Bewusst EIN Feld statt eines Formulars mit Teilnehmer-/Anlass-/Begriffs-
feldern (Entscheidung des Nutzers): die Box benutzen auch technisch nicht
versierte Menschen. Gefuehrt wird ueber den Platzhaltertext, die Struktur
zieht die Analyse selbst aus der Prosa.

- recordings.context_note/-_at, db.setContextNote/getContextNote
- PATCH /api/recordings/:id/context; Antwort `applies` sagt ehrlich, ob die
  Notiz die laufende Verarbeitung noch erreicht (auto/running/reanalyze)
- analyzeRecording liest die Notiz erst kurz vor dem LLM-Aufruf, damit
  waehrend der minutenlangen Vorstufen Getipptes noch zaehlt
- llm.contextBlock: eigener """-Block mit Verbot, daraus Aussagen, Ergebnisse
  oder Aufgaben abzuleiten (sonst schreibt das 4B-Modell die Notiz als
  Gesagtes zurueck); eingespeist in Single-, Whole- und Sektions-Pfad
- zwei Deckel: 1200 Zeichen fuer Prompts, die die Notiz einmal sehen,
  600 fuer die Chunk-Prompts (dort landet sie in jedem Chunk von 3200)
- GUI-Karte zeichnet sich selbst neu (kein openRecording -> Audio laeuft
  weiter), ungespeicherter Text ueberlebt den Neuaufbau nach der Analyse

Verifiziert an #97: Kontext greift (korrekte Namen, richtiges Thema), die
absichtlich eingelegten Fremdfakten der Testnotiz erscheinen nicht in der
Zusammenfassung. Alte Analyse danach zurueckgeschrieben.

Doku: Kapitel 05 (neuer Abschnitt + Flussdiagramm + Deckel-Tabelle),
Kapitel 03 (Sequenzdiagramm + API), Changelog 2026-08-17.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-17 16:10:38 +02:00
parent 3d628b4b96
commit 0f530b575a
8 changed files with 332 additions and 30 deletions
+51
View File
@@ -239,6 +239,57 @@ flowchart LR
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.
## Kontextnotiz des Nutzers (seit 2026-08-17)
Nach dem Stop bietet die Detailansicht **ein Freitextfeld** an: „Kontext zum Gespräch“. Was dort steht,
geht als **Hintergrundwissen** in die Analyse — nicht als Gesprächsinhalt. Damit weiß das Modell, wer
dabei war, worum es ging und wie Eigennamen geschrieben werden, statt das alles aus fehlerbehaftetem
ASR-Text zu erraten.
**Bewusst EIN Feld, kein Formular** (Entscheidung des Nutzers): die Box benutzen später auch technisch
nicht versierte Menschen, und „Kontext hinzufügen“ ist eine Hürde weniger als Teilnehmer-, Anlass- und
Begriffsfelder auszufüllen. Die Struktur zieht sich die Analyse selbst aus der Prosa. Geführt wird über
den **Platzhaltertext** („Wer war dabei, worum ging es, wichtige Namen oder Begriffe? … z. B.: Call mit
Herrn Meier von Firma Krämer wegen Angebot Halle 3 …“) statt über Pflichtfelder.
```mermaid
flowchart TB
STOP["Stop gedrückt"] --> BOX["Kontextbox klappt AUF und ist fokussiert<br/>(state.ctxPromptFor, main.js)"]
BOX --> SAVE["PATCH /api/recordings/:id/context<br/>db.setContextNote → recordings.context_note<br/>(Deckel 4 000 Zeichen, leer = löschen)"]
SAVE --> WHEN{"Wie weit ist die<br/>Verarbeitung?"}
WHEN -- "status=transcribing" --> AUTO["applies='auto':<br/>Analyse liest die Notiz danach → fließt sicher ein"]
WHEN -- "analysis_status=pending" --> RUN["applies='running':<br/>Analyse läuft schon, kann die Notiz verpasst haben<br/>→ GUI verweist auf '↻ Analyse neu'"]
WHEN -- "done / error" --> RE["applies='reanalyze':<br/>GUI fragt nach und startet reanalyze-llm<br/>(Sprecher bleiben, nur LLM neu)"]
AUTO --> READ
RE --> READ
READ["analyzeRecording: db.getContextNote(id)<br/>ERST kurz vor dem LLM-Aufruf gelesen<br/>(Sprecher-Erkennung davor dauert Minuten —<br/>was der Nutzer in der Zeit tippt, zählt noch)"] --> INJ["llm.analyze({context}) → contextBlock()"]
INJ --> P1["analyzeSingle: PROMPT_CLEAN(_LABELED) + PROMPT_JSON"]
INJ --> P2["analyzeWhole: PROMPT_WHOLE_ANALYZE"]
INJ --> P3["analyzeLong: JEDER Chunk-Prompt (gekürzt)<br/>+ Titel- und Überblick-Mikro-Call"]
```
**Halluzinationsbremse.** Die Notiz steht in einem eigenen, mit `"""` abgegrenzten Block mit der
ausdrücklichen Auflage: nur für Schreibweisen, Zuordnung, Anlass und Gewichtung nutzen, **keine**
Aussagen, Ergebnisse, Entscheidungen oder Aufgaben daraus übernehmen, Notiz nicht erwähnen. Ohne diese
Klammer schreibt ein 4B-Modell die Notiz als Gesagtes in die Zusammenfassung zurück — dieselbe Falle wie
beim `CORRECTION_HINT` (siehe dessen STRENG-Satz).
**Zwei Deckel**, weil die Notiz je Pfad unterschiedlich oft im Prompt landet:
| Konstante | Wert | Gilt für |
|---|---|---|
| `db.CONTEXT_MAX` | 4 000 Zeichen | Speicherung (Spalte ist TEXT) |
| `LLM_CONTEXT_CHARS` | 1 200 Zeichen | Prompts, die die Notiz **einmal** sehen (Single/Whole/Titel/Überblick) |
| `LLM_CONTEXT_CHUNK_CHARS` | 600 Zeichen | Chunk-Prompts des Sektions-Pfads — dort geht die Notiz in **jeden** Chunk (`CHUNK_CHARS` = 3 200, eine lange Notiz wäre ein Drittel Ballast) |
Im Sektions-Pfad fällt die Notiz auf dem **letzten** von drei Versuchen zusammen mit dem `hint` weg: hat
das Modell zweimal die Feldbeschreibung nachgeplappert, ist jeder Zusatztext vor dem Abschnitt verdächtig.
**Grenze:** die Notiz verbessert Titel, Zusammenfassung, Teilnehmer und ToDos. Das **Transkript selbst**
verbessert sie nicht — Whisper ist zu dem Zeitpunkt schon gelaufen, und Begriffslisten dürfen ohnehin
nicht als `--initial-prompt` an Whisper (Echo-Loop, siehe Glossar-Abschnitt). Wer Schreibweisen dauerhaft
korrigiert haben will, nimmt das Glossar; die Notiz gilt nur für dieses eine Gespräch.
## Zusammenspiel mit Whisper auf der NPU
rkllm (LLM) und Whisper-NPU-Encoder können nicht gleichzeitig resident sein (RAM-Contention, äußerte sich