Diarisierung: Marker-Zwangsbedingungen absichern, Voiceprints als cannot-link; LLM: Sektions-Pfad ab 9k Zeichen

#79 verlor einen von vier Sprechern und lieferte eine 1082-Zeichen-Zusammenfassung.

Diarisierung: ein zweiter, falsch verankerter Marker verschmolz Saschas Cluster
in Merles (Kosinus -0,727). must-link prueft jetzt die akustische Aehnlichkeit,
Anker werden nach Trefferqualitaet gewichtet, und enrolled Stimmen wirken als
cannot-link -- damit ist der Box-Besitzer immer ein eigener Sprecher.

LLM: der Whole-Pass komprimierte das erste Thema weg. Sektions-Pfad jetzt ab
9000 Zeichen, kleinere Chunks, woertlicher Kontext aus dem Vorgaenger-Chunk und
ein gezielter Pruef-Aufruf gegen ueberdehnte Einzelaussagen.

#79: 4 Sprecher, Zusammenfassung ~5300 Zeichen, 7 Themenbloecke.
Regression: #70 -> 4, #68 -> 2, #74 -> 20 Sprecher unveraendert.
This commit is contained in:
2026-07-29 16:09:30 +02:00
parent c76f26abaf
commit 810cee1dba
9 changed files with 438 additions and 38 deletions
+60 -6
View File
@@ -19,7 +19,9 @@
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 --> SPK{"labeled + ≥ 5 Sprecher?<br/>(MANY_SPEAKERS, countSpeakers)"}
A -- nein --> LEN{"> 9 000 Zeichen?<br/>(SECTION_MIN_CHARS, seit 2026-07-29)"}
LEN -- "ja (Meeting-Länge)" --> LONG
LEN -- nein --> SPK{"labeled + ≥ 3 Sprecher?<br/>(MANY_SPEAKERS, countSpeakers)"}
SPK -- "ja (Round-Robin-Meeting)" --> LONG
SPK -- nein --> B{"≤ 32 000 Zeichen?"}
B -- ja --> WHOLE["analyzeWhole:<br/>GANZES Gespräch in einem Analyse-Call<br/>(kohärente Summary, kleine JSON-Ausgabe)<br/>Instruktion VOR und NACH dem Transkript"]
@@ -28,19 +30,71 @@ flowchart TB
REPAIR --> WCHECK
WCHECK -- "weiterhin leer/Junk" --> LONG
WCHECK -- ja --> OUT
B -- nein --> LONG["analyzeLong (Sektions-Konglomerat):<br/>Chunks à 4 200 Zeichen<br/>(bei 8k wird der Inhalt generisch/namenlos,<br/>bei 4k bleibt das Modell geerdet);<br/>chunkText splittet an Zeilen = Turn-Grenzen"]
LONG --> MAP["MAP pro Chunk, sequenziell mit ROLLING CONTEXT<br/>(1-Satz-Kurzfassungen der vorherigen Abschnitte →<br/>Änderungen erkennbar: 'jetzt X, vorher Y'):<br/>Mini-JSON thema (Überschrift ≤ 6 Wörter) +<br/>zusammenfassung (JEDE berichtende Person, beruflich;<br/>Smalltalk max. Halbsatz) + eigennamen[] (EXTRAKTION)<br/>+ teilnehmer + todos (nur beruflich, Verantwortlicher<br/>in Klammern) — bewusst minimaler Prompt"]
B -- nein --> LONG["analyzeLong (Sektions-Konglomerat):<br/>Chunks à 3 200 Zeichen (seit 2026-07-29;<br/>bei 8k wird der Inhalt generisch/namenlos,<br/>bei 4,2k konkurrieren zwei Themen pro Chunk);<br/>chunkText splittet an Zeilen = Turn-Grenzen"]
LONG --> MAP["MAP pro Chunk, sequenziell mit ROLLING CONTEXT<br/>(1-Satz-Kurzfassungen der vorherigen Abschnitte →<br/>Änderungen erkennbar: 'jetzt X, vorher Y')<br/>+ TAIL: die letzten 700 Zeichen des VORIGEN Chunks<br/>WÖRTLICH, ausdrücklich 'nicht zusammenfassen'<br/>(Bezugswörter für Sätze am Chunk-Anfang):<br/>Mini-JSON thema (Überschrift ≤ 6 Wörter) +<br/>zusammenfassung (JEDE berichtende Person, beruflich;<br/>JEDES Thema mind. 1 Satz; nicht verallgemeinern;<br/>Smalltalk max. Halbsatz) + eigennamen[] (EXTRAKTION)<br/>+ teilnehmer + todos (nur beruflich, Verantwortlicher<br/>in Klammern)"]
MAP --> SALV{"JSON parsebar +<br/>kein Feldbeschreibungs-Echo?"}
SALV -- nein --> RETRY["bis 3 Versuche, Prompt VARIIERT<br/>(2.: ohne Kontext, 3.: ohne Hint —<br/>manche Inhalte echoen sonst JEDES Mal)<br/>dann Feld-Salvage per Regex<br/>(zusammenfassung/thema-Strings überleben fast immer),<br/>zuletzt Prosa-Rettung (≥ 60 Zeichen, sonst verwerfen)"]
RETRY --> COLLECT
SALV -- ja --> COLLECT["Sektionen einsammeln"]
COLLECT --> TITLE["TITEL-Mikro-Call: Thema + 2-Satz-Kurzfassungen<br/>OHNE letzten Abschnitt (Schluss-Smalltalk-Filter)<br/>→ nur {titel} — kleines Input/Output = zuverlässig"]
RETRY --> ABS
SALV -- ja --> ABS{"Entwurf behauptet etwas Grundsätzliches?<br/>Skopus-Wort (nur/ausschließlich/statt/…)<br/>UND kollektives Subjekt (das Team/wir/künftig/…)"}
ABS -- ja --> VERIFY["PRÜF-Aufruf NUR für diesen Abschnitt:<br/>Abschnitt wörtlich + Entwurf →<br/>'gilt das allgemein oder nur für EINEN Beitrag?'<br/>Antwort < 60 % der Entwurfslänge wird verworfen"]
VERIFY --> COLLECT
ABS -- nein --> COLLECT["Sektionen einsammeln"]
COLLECT --> DROP{"letzter Abschnitt =<br/>nur Verabschiedung?<br/>(2 Floskeln ODER Überschrift 'Abschluss…')"}
DROP -- ja --> DROPPED["Abschnitt verwerfen<br/>(nie ein innerer, nie der einzige)"]
DROPPED --> TITLE
DROP -- nein --> TITLE["TITEL-Mikro-Call: Thema + 2-Satz-Kurzfassungen<br/>OHNE letzten Abschnitt (Schluss-Smalltalk-Filter)<br/>→ nur {titel} — kleines Input/Output = zuverlässig"]
TITLE --> OVERVIEW["ÜBERBLICK-Mikro-Call über dieselben Kurzfassungen<br/>→ {ueberblick} 2-3 Sätze, alle Abschnitte gleich gewichtet<br/>(Key-tolerant parsen: auch 'überblick' oder<br/>irgendein langer String-Value; leer = weglassen)"]
OVERVIEW --> ASSEMBLE["KONGLOMERAT baut CODE (kein LLM-Reduce):<br/>Überblick-Absatz + pro Sektion 'Thema:\nSummary (Namen: …)'<br/>chronologisch + 'Genannte Namen: …' am Ende<br/>→ deterministisch, keine Fehlgewichtung möglich; Cap 16 000"]
ASSEMBLE --> OUT["Ergebnis"]
SINGLE --> OUT
```
### Länge schlägt beides: Sektions-Pfad ab 9 000 Zeichen (2026-07-29)
Aufnahme #79 (23-min-Marketing-Jour-fixe, 3 diarisierte Sprecher, 21k Zeichen im gelabelten Transkript)
erfüllte **keine** der beiden bisherigen Bedingungen — unter `WHOLE_MAX_CHARS` 32 000 und unter
`MANY_SPEAKERS` 5 — und lief deshalb als Whole-Pass. Ergebnis: **1 082 Zeichen** Zusammenfassung, in der
das erste Tagesordnungsthema (Timo/Sabbatical, die ersten 2,5 Minuten) **komplett fehlte**; MS Teams
lieferte für dasselbe Meeting 5 Themenblöcke. Die Sprecherzahl war also nie der eigentliche Treiber:
oberhalb von ~3k Token komprimiert das 4B den ANFANG des Prompts weg, egal wie viele Leute reden.
- Neu `SECTION_MIN_CHARS = 9 000` (`LLM_SECTION_MIN`): alles darüber geht in `analyzeLong`, das jeden
Abschnitt bauartbedingt abdeckt. `MANY_SPEAKERS` zusätzlich 5 → 3.
- Wirkung auf #79 (gleiches Transkript, gleiche Diarisierung): **1 082 → ~5 300 Zeichen**, 7 Themenblöcke,
alle Teams-Themen enthalten, 15–18 ToDos mit Verantwortlichen.
- Preis: der Sektions-Pfad braucht auf diesem Board ~20 statt ~8 Minuten.
### Zwei Fehlerbilder des Sektions-Pfads — und was wirklich half (2026-07-29)
Beide wurden über **zehn** Läufe derselben Aufnahme gemessen, nicht geraten.
**1. Verlorenes Thema am Chunk-Anfang.** Bei 4 200 Zeichen enthielt Chunk 1 von #79 zwei zusammenhanglose
Themen (Timo-Gespräch, dann Video-Review) — das Modell fasste nur das längere zusammen, das Timo-Thema
verschwand in 1 von 3 Läufen. Fix: `CHUNK_CHARS` 4 200 → **3 200** plus die Prompt-Zeile „Enthält der
Abschnitt mehrere verschiedene Themen, behandle JEDES in mindestens einem Satz — auch ein kurzes Thema
ganz am Anfang." Ein früherer Versuch mit dem Zusatz „…und lasse weg, was unklar bleibt" machte es
**schlimmer** (das war die Lizenz, kurze Themen zu streichen) und wurde zurückgenommen.
**2. Überdehnte Einzelaussagen.** „Achso, ja, dann lassen wir LinkedIn raus, dann kommen wir es nur auf
Instagram" — gesagt über EIN Ragebait-Video — wurde zu „das Team veröffentlicht nur noch auf Instagram",
also der Umkehrung der echten Entscheidung (LinkedIn soll fachlicher werden). In 5 von 8 Läufen falsch,
und **keine** Prompt-Formulierung hat das zuverlässig behoben (abstrakte Regel, dann Gegensatzpaar mit
Beispiel — beide blieben drin, beide reichten nicht). Was gemessen wurde und half:
- Ein Skript, das `buildBasis` + `chunkText` nachbaut, lokalisierte die Auslöser: „lustigen Sachen nur bei
Instagram" liegt **4 % in Chunk 6**, also ohne Bezugswort. → **`TAIL_CHARS` = 700**: jeder Chunk bekommt
die letzten 700 Zeichen des vorigen wörtlich als ausdrücklich gekennzeichneten Kontextblock. Das behob
genau diesen Abschnitt.
- Der zweite Auslöser sitzt mitten in Chunk 5 (51 %), hat also Kontext und wird trotzdem überdehnt. →
**gezielter Prüf-Aufruf**, wenn der Entwurf ein Skopus-Wort UND ein kollektives Subjekt enthält.
Erste Fassung des Triggers suchte wörtlich nach „nur noch"/„statt auch" und feuerte deshalb NIE auf den
echten Fehlsatz „Das Team entscheidet sich dafür, **nur** Instagram zu nutzen **statt** LinkedIn".
**Stand:** die harte Umkehrung („LinkedIn wird beendet") ist weg, der Rest-Effekt ist eine abgeschwächte
Formulierung („verstärkter Fokus auf Instagram") in einem von sieben Abschnitten, während Überblick und
LinkedIn-Abschnitt die Strategie korrekt wiedergeben. Ein vollständiger Verifikationsdurchlauf über ALLE
Abschnitte würde die Analyse verdoppeln — dafür ist der Rest-Fehler zu klein.
### Round-Robin-Meetings: Sprecherzahl schlägt Länge (2026-07-17)
Aufnahme #74 (33-min-Weekly, 13 diarisierte Sprecher, 31k Zeichen) lag knapp UNTER `WHOLE_MAX_CHARS`