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:
@@ -22,6 +22,10 @@ sequenceDiagram
|
||||
REC->>DB: WAV + MP3 + Zeile (status=transcribing)
|
||||
SRV->>DB: addMarkers() → Tabelle markers
|
||||
Note over REC: kehrt nach ~1,5 s zurück — GUI frei
|
||||
GUI->>GUI: Kontextbox klappt auf (Freitext, seit 2026-08-17)
|
||||
GUI->>SRV: PATCH /api/recordings/:id/context {context_note}
|
||||
SRV->>DB: setContextNote() → recordings.context_note
|
||||
Note over GUI,DB: Die Transkription läuft noch Minuten —<br/>der getippte Kontext ist rechtzeitig da (Kap. 05)
|
||||
REC->>REC: A76-Takt kappen (1,6 GHz)
|
||||
REC->>WH: transcribeWav() [BatchedInferencePipeline, batch=8]
|
||||
WH-->>REC: Wörter + Zeitstempel
|
||||
@@ -32,6 +36,7 @@ sequenceDiagram
|
||||
SRV->>SRV: applyMarkers(): Cluster benennen + Stimmen lernen
|
||||
REC->>REC: finally: Takt zurück
|
||||
SRV->>LLM: analyzeRecording() (fire-and-forget, analysis_status=pending)
|
||||
LLM->>DB: getContextNote() — Notiz erst JETZT gelesen (Kap. 05)
|
||||
LLM->>DB: Titel/Summary/Teilnehmer/Todos (analysis_status=done, analysis_ms)
|
||||
GUI->>SRV: Polling analysis_status
|
||||
```
|
||||
@@ -153,6 +158,7 @@ Datenverlust, aber auch keinen Weg mehr, die Aufnahme aus der GUI zu beenden. No
|
||||
| `POST /api/recordings/:id/retranscribe` | Hängende/fehlgeschlagene Transkription neu starten |
|
||||
| `POST /api/recordings/:id/reanalyze` `{numSpeakers?}` | Diarization **immer** neu (leer/0 = automatisch, Zahl = Anzahl gepinnt) + LLM neu |
|
||||
| `POST /api/recordings/:id/reanalyze-llm` | Nur LLM neu, Diarization/Wörter bleiben |
|
||||
| `PATCH /api/recordings/:id/context` `{context_note}` | Kontextnotiz speichern (leer = löschen); Antwort `applies` = `auto` / `running` / `reanalyze` sagt, ob die laufende Verarbeitung sie noch erreicht |
|
||||
| `POST /api/recordings/:id/speaker` `{speaker,name,enroll}` | Sprecher benennen + optional Stimme lernen |
|
||||
| `/api/glossary` (CRUD) | Eigennamen-Glossar |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,67 @@
|
||||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||
Was, Warum, betroffene Dateien/Backups).
|
||||
|
||||
## 2026-08-17 — Kontext zum Gespräch: ein Freitextfeld nach dem Stop
|
||||
|
||||
**Wunsch:** nach dem Beenden einer Aufnahme die Möglichkeit haben, Kontext zum gerade geführten
|
||||
Call/Meeting zu ergänzen — das verbessert die Analyse nach dem Transkript erheblich, weil das Modell
|
||||
Personen, Firmen, Anlass und Eigennamen nicht mehr aus fehlerbehaftetem ASR-Text erraten muss.
|
||||
|
||||
**Entscheidung des Nutzers: EIN Freitextfeld, kein Formular.** Ein Vorschlag mit Teilnehmer-, Anlass-,
|
||||
Projekt- und Begriffsfeldern wurde ausdrücklich verworfen: die Box benutzen später auch technisch nicht
|
||||
versierte Menschen, und „Kontext hinzufügen“ ist eine Hürde weniger als vier Pflichtfelder. Geführt wird
|
||||
über den Platzhaltertext, die Struktur (Namen, Begriffe) zieht sich die Analyse selbst aus der Prosa.
|
||||
|
||||
**Umsetzung:**
|
||||
|
||||
- Neue Spalten `recordings.context_note` / `context_note_at` (`ensureSchema`), `db.setContextNote()`
|
||||
(Deckel 4 000 Zeichen, leerer Text löscht) und `db.getContextNote()`.
|
||||
- `PATCH /api/recordings/:id/context`. Die Antwort enthält `applies` und sagt damit **ehrlich**, ob die
|
||||
Notiz die laufende Verarbeitung noch erreicht: `auto` (wird noch transkribiert → fließt sicher ein),
|
||||
`running` (Analyse läuft schon, kann sie verpasst haben), `reanalyze` (fertig → nur ein neuer Lauf
|
||||
nutzt sie; die GUI fragt dann nach und startet `reanalyze-llm`).
|
||||
- `analyzeRecording()` liest die Notiz **erst kurz vor dem LLM-Aufruf** (`db.getContextNote`), nicht aus
|
||||
der Zeile vom Anfang: Sprecher-Erkennung und Transkription davor brauchen Minuten, und was der Nutzer
|
||||
in dieser Zeit tippt, soll noch mitzählen.
|
||||
- `llm.js`: neuer `contextBlock()` — die Notiz kommt in einem eigenen `"""`-Block mit der Auflage, sie
|
||||
**nur** für Schreibweisen, Zuordnung, Anlass und Gewichtung zu nutzen und daraus keine Aussagen,
|
||||
Ergebnisse oder Aufgaben abzuleiten. Eingespeist in alle Pfade: `analyzeSingle` (`PROMPT_CLEAN`,
|
||||
`PROMPT_CLEAN_LABELED`, `PROMPT_JSON`), `analyzeWhole` (`PROMPT_WHOLE_ANALYZE`) und `analyzeLong`
|
||||
(jeder Chunk-Prompt + Titel- und Überblick-Mikro-Call). Zwei Deckel: 1 200 Zeichen für die Prompts, die
|
||||
die Notiz einmal sehen, 600 für die Chunk-Prompts (dort landet sie in **jedem** Chunk von 3 200
|
||||
Zeichen). Auf dem letzten von drei Chunk-Versuchen fällt sie mit dem `hint` weg.
|
||||
- GUI: Karte „📝 Kontext zum Gespräch“ über den Modulen der Detailansicht. Nach dem Stop steht sie offen
|
||||
und fokussiert (`state.ctxPromptFor`), sonst eingeklappt mit dem gespeicherten Text und
|
||||
„✏️ Bearbeiten“. Sie zeichnet sich **selbst** neu statt über `openRecording()` — ein Neuaufbau der
|
||||
Detailansicht setzt den `<audio>`-Tag zurück und würde das Abspielen unterbrechen. Getippter,
|
||||
ungespeicherter Text überlebt den Neuaufbau nach Ende der Analyse (`state.ctxDrafts`). Strg+Enter
|
||||
speichert, Enter macht Absätze.
|
||||
|
||||
**Verifiziert** an der laufenden App: Endpunkt liefert `applies:"reanalyze"` bei fertiger Analyse und
|
||||
`applies:"auto"` während einer laufenden Transkription (8-s-Wegwerfaufnahme #115, danach in den
|
||||
Papierkorb). Echter LLM-Lauf auf #97 (37 s, Single-Pfad) mit einer Testnotiz: Log zeigt
|
||||
`llm: Kontextnotiz des Nutzers wird mitgegeben (196 Zeichen)`, die Zusammenfassung wurde messbar besser
|
||||
(„Stimmerkennung“ + korrekte Namen Sascha/Fabio statt des ASR-Fehlers „Audioaufnahme … Lichter-Modus“ mit
|
||||
falscher Sprecherzuordnung) — und die absichtlich in die Notiz gelegten Fremdfakten („Rückruf ist für
|
||||
Freitag geplant“, „Halle 3 wurde verkauft“) tauchten **nicht** in Zusammenfassung oder ToDos auf, die
|
||||
Halluzinationsbremse hält also. Die alte Analyse von #97 war vor dem Test gesichert und wurde danach
|
||||
zurückgeschrieben, die Testnotiz entfernt. `vite build` sauber, `systemctl restart callrec` erst nach
|
||||
Prüfung auf `analysis_status='pending'`.
|
||||
|
||||
**Offene Grenze:** die Notiz verbessert Titel/Zusammenfassung/Teilnehmer/ToDos, nicht das Transkript —
|
||||
Whisper ist zu dem Zeitpunkt schon gelaufen. Dauerhaft falsche Schreibweisen gehören ins Glossar.
|
||||
Ebenfalls noch nicht: die Notiz im Suchindex (`passages`) und ein Hinweis „kein Kontext“ in der
|
||||
Aufnahmenliste.
|
||||
|
||||
Dateien: `app/db.js` (`ensureSchema`, `setContextNote`, `getContextNote`, `CONTEXT_MAX`), `app/server.js`
|
||||
(`PATCH …/context`, `analyzeRecording`), `app/llm.js` (`contextBlock`, `CONTEXT_MAX_CHARS`,
|
||||
`CONTEXT_CHUNK_CHARS`, alle Prompt-Bausteine + `analyze/analyzeSingle/analyzeWhole/analyzeLong`),
|
||||
`app/web/src/main.js` (Kontextkarte, `state.ctxPromptFor`/`ctxDrafts`, `toggleRecord`),
|
||||
`app/web/src/style.css` (`.ctxbox`, `.ctx-*`).
|
||||
Board-Backups: `.bak-ctxnote` (db.js, server.js, llm.js, web/src/main.js, web/src/style.css).
|
||||
Kapitel 05 (neuer Abschnitt „Kontextnotiz des Nutzers" + Flussdiagramm + Deckel-Tabelle), Kapitel 03
|
||||
(Sequenzdiagramm + API-Tabelle).
|
||||
|
||||
## 2026-08-14 — Angeheftete Aufnahmen per Drag & Drop zwischen Unterreitern verschieben
|
||||
|
||||
**Wunsch:** eine im Projekt abgelegte Aufnahme sollte sich innerhalb des Ordners umhängen lassen, z. B. von
|
||||
|
||||
Reference in New Issue
Block a user