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
+22 -1
View File
@@ -319,7 +319,12 @@ async function analyzeRecording(id) {
// Remember which label each speaker had in the LLM input, so renaming a
// speaker later can patch the finished text (relabelAnalysis).
await db.setSpeakerLabels(id, labels).catch(() => {});
const a = await llm.analyze(text, { labeled, glossary });
// Kontextnotiz des Nutzers (Freitext, nach dem Stop getippt) geht als
// Hintergrundwissen mit. Bewusst JETZT frisch gelesen und nicht aus `rec`
// von oben: die Sprecher-Erkennung darüber braucht Minuten, der Nutzer tippt
// in dieser Zeit — was bis hierher gespeichert ist, zählt noch mit.
const note = await db.getContextNote(id).catch(() => '');
const a = await llm.analyze(text, { labeled, glossary, context: note });
// Participants = the ACOUSTICALLY diarized speakers (named where known),
// not the LLM's list — the model returned every name MENTIONED in the
// conversation as a "Teilnehmer" (11 names on a 3-person call, #69).
@@ -479,6 +484,22 @@ app.patch('/api/recordings/:id/title', wrap(async (req, res) => {
res.json({ ok: true, id, title: row.title, title_locked: !!row.title_locked });
}));
// Kontextnotiz speichern (leerer Text löscht sie). Der Rückgabewert `applies`
// sagt der GUI ehrlich, ob die Notiz die LAUFENDE Analyse noch erreicht — das
// entscheidet sich daran, wie weit die Verarbeitung ist:
// 'auto' = wird noch transkribiert; die Analyse liest die Notiz danach
// 'running' = Analyse läuft bereits, sie kann die Notiz verpasst haben
// 'reanalyze' = Analyse ist fertig/fehlgeschlagen → nur ein neuer Lauf nutzt sie
app.patch('/api/recordings/:id/context', wrap(async (req, res) => {
const id = +req.params.id;
const row = await db.setContextNote(id, (req.body && req.body.context_note) || '');
if (!row) return res.status(404).json({ error: 'not found' });
const applies = row.status === 'transcribing' ? 'auto'
: row.analysis_status === 'pending' ? 'running' : 'reanalyze';
res.json({ ok: true, id, context_note: row.context_note || '',
context_note_at: row.context_note_at, applies, max: db.CONTEXT_MAX });
}));
// --- Trash -------------------------------------------------------------------
// Delete = move to the bin. Nothing on disk is touched until the retention
// window expires, so a restore brings back audio, transcript and analysis.