#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.
251 lines
18 KiB
Markdown
251 lines
18 KiB
Markdown
# 05 — LLM-Analyse (Qwen3-4B auf der NPU)
|
||
|
||
## Modell
|
||
|
||
| | |
|
||
|---|---|
|
||
| Modell | **Qwen3-4B-Instruct-2507** (non-thinking), `qwen3-4b-w8a8g128-16k.rkllm` (5,3 GB), seit 2026-07-16 |
|
||
| Runtime | rkllm **1.3.0**; Chat-Template = **ChatML** (`<\|im_start\|>user` / `<\|im_end\|><\|im_start\|>assistant`) in `rkllm_server.cpp` |
|
||
| Quantisierung | **grouped w8a8 (g128)** — gleiche Wahl wie bei Gemma (plain w8a8 riskant, w4a16 auf RK3588 nicht unterstützt) |
|
||
| Kontext | 16k konvertiert, Runtime **12288** → Whole-Pass bis **32 000 Zeichen** (`WHOLE_MAX_CHARS`) — Qwen3 schafft die Synthese, an der Gemma ab ~6k Token scheiterte |
|
||
| Tempo | Laden ~96 s (READY-Timeout deshalb 180 s!), 22k-Zeichen-Whole-Pass ~12-15 min, Kurzclip-Analyse ~7 min (gedrosselt) |
|
||
| Historie | Qwen2.5-3B (4096 ctx, Feld-Echos) → Gemma-3-4B-it (gut ≤ 20k, Synthese-blind) → **Qwen3-4B** (A/B 2026-07-16: 3/3 Prompts besser, Whole-Pass 21,8k fehlerfrei gewichtet). Rollback: `rkllm_server{,.cpp}.bak-gemmatmpl` + Drop-in auf gemma3-Modell zurückstellen. gemma3n-E4B zu groß (10,4 GB). |
|
||
|
||
**Hart:** niemals Online-LLM — alles lokal auf der NPU.
|
||
|
||
## Pfadwahl nach Transkriptlänge
|
||
|
||
```mermaid
|
||
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 --> 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"]
|
||
WHOLE --> WCHECK{"JSON parsebar +<br/>Zusammenfassung ≥ 20 Zeichen?"}
|
||
WCHECK -- "JSON kaputt" --> REPAIR["Repair-Call: NUR der kaputte<br/>Output (klein) → gültiges JSON"]
|
||
REPAIR --> WCHECK
|
||
WCHECK -- "weiterhin leer/Junk" --> LONG
|
||
WCHECK -- ja --> OUT
|
||
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 --> 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`
|
||
und lief als Whole-Pass — Ergebnis: Summary fast nur aus den LETZTEN Sprechern (Gasblender, Sitas,
|
||
Zeiterfassung), alle frühen Wochenberichte fehlten. Ein 4B kann 15+ gleichgewichtige Themen aus einem
|
||
~10k-Token-Prompt nicht ausbalancieren — der Whole-Pass funktioniert nur für narrative Gespräche mit
|
||
wenigen Sprechern (dort validiert, #70/#71). Deshalb routet `analyze()` **labeled Transkripte mit
|
||
≥ 5 Sprechern (`MANY_SPEAKERS`) immer in den Sektions-Pfad**, der jede Gesprächsphase per Konstruktion
|
||
abdeckt. Der Sektions-Pfad wurde dabei auf Copilot-Niveau ausgebaut (3 Iterationen auf #74):
|
||
|
||
1. **Themen-Überschrift pro Chunk** (`thema`-Feld) → Konglomerat liest sich wie Besprechungsnotizen
|
||
(„Performanceproblem bei SAP-System:" statt „Teil 2/10:").
|
||
2. **Jede berichtende Person MUSS vorkommen** (Prompt-Regel) — vorher fielen einzelne Sprecher weg.
|
||
3. **Smalltalk-Dämpfung**: Privates (Essen, Familie, Auto, Urlaubserzählungen) max. ein Halbsatz;
|
||
Todos nur beruflich, mit Verantwortlichem in Klammern (37 → 30 saubere Todos auf #74).
|
||
4. **Überblick-Mikro-Call** (`PROMPT_OVERVIEW`) liefert den Intro-Absatz; gleiche kleine
|
||
Input/Output-Klasse wie der Titel-Call. GOTCHA: Qwen antwortet teils mit Key `"überblick"`
|
||
(ü!) statt `"ueberblick"` — Parser akzeptiert jeden langen String-Value.
|
||
5. Summary-Cap 6 000 → **16 000** Zeichen: 10 Sektionen ≈ 8k, der alte Cap schnitt mitten im Satz ab.
|
||
|
||
Validiert auf #74: alle 13 Sprecher + alle Themen (DWP-Bank, Diakonie, SAP-GUI, Sitas, Zeiterfassung,
|
||
Wellpass, LinkedIn, Polavis, Gasblender) abgedeckt, kein Recency-Bias mehr. Backup `llm.js.bak-standup`.
|
||
|
||
### Warum kein LLM-Reduce mehr (Prompt-Lab 2026-07-16)
|
||
|
||
Der alte Reduce (Teil-Summaries → LLM baut EINE Gesamt-Summary) war die Fehlgewichtungs-Quelle. Im
|
||
Prompt-Lab auf #70 (temporärer `/api/debug/ask`-Endpoint, ~15 Varianten) scheiterte JEDE Synthese-Form
|
||
reproduzierbar: Gewichtungsregeln ignoriert (Auto-Smalltalk blieb im Titel), Namen durch Rollen ersetzt
|
||
(„Personalwesen"), Themen-Tabellen mit 4× demselben Eintrag, Repetitionsschleifen. **Abschnittsübergreifende
|
||
Synthese liegt jenseits von Gemma-3-4B** — Verstehen einzelner 4k-Abschnitte dagegen funktioniert gut
|
||
(Com4Tec, René, Tobi+Niklas, 20 Bewerbungen alle korrekt extrahiert). Also: LLM versteht, Code führt zusammen.
|
||
|
||
### Whole-Pass-Grenze = Modellfrage (Gemma-Geschichte, gelöst durch Qwen3)
|
||
|
||
Mit **Qwen3-4B** liegt die Grenze bei 32 000 Zeichen (Kontext-Budget: 12288 − 1536 ≈ 10 752 Token) —
|
||
der 21,8k-Whole-Pass von #70 kam in EINEM Call korrekt gewichtet zurück (Auto-Smalltalk raus, Namen drin).
|
||
Zur Geschichte: mit Gemma-3-4B war die Grenze 20 000 Zeichen und zwar als MODELL-Grenze — Aufnahme #70
|
||
(26 min, 22,6k Zeichen ≈ 6 900 Token) scheiterte REPRODUZIERBAR an Gemma selbst
|
||
(vier Iterationen, alle Fehlbilder real beobachtet):
|
||
|
||
1. **Instruktion nur oben** → Modell „vergisst" die Aufgabe, antwortet mit englischem Geplauder
|
||
*über* das Gespräch („Okay! That was a fascinating conversation…"). Fix: Instruktion nach dem
|
||
Transkript wiederholen (ohne `{"titel": ...}`-Skelett — die `...`-Platzhalter wurden sonst wörtlich kopiert!).
|
||
2. **Recency-Bias:** Titel/Summary beschreiben den SCHLUSS des Gesprächs (Auto-Smalltalk der letzten
|
||
3 min) statt der Hauptthemen — mehr Kontextfenster liest nicht gleichmäßiger.
|
||
3. **„themen"-Inventar-Feld als Gegenmittel** → Repetitionsschleife mit zerfallendem JSON
|
||
(typografische Anführungszeichen, doppelte Keys, 16-min-Generierung bis Token-Cap).
|
||
|
||
Konsequenz: > 20k Zeichen geht IMMER über Map-Reduce; dessen Reduce gewichtet stattdessen nach
|
||
Abschnitts-Anteil. `WHOLE_MAX_CHARS` nicht erhöhen ohne Inhalts-Revalidierung.
|
||
|
||
## 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).
|
||
|
||
## Sektions-Konglomerat: was im Text landet
|
||
|
||
Der Body der Zusammenfassung ist deterministisch aus den Chunk-Ergebnissen gebaut (kein LLM-Reduce):
|
||
pro Abschnitt `thema`-Überschrift + Text, davor der Überblick-Absatz, am Ende eine Zeile
|
||
„Genannte Namen: …".
|
||
|
||
- Die pro Abschnitt extrahierten Eigennamen (`s.names`) werden seit 2026-07-27 **nicht mehr unter jeden
|
||
Absatz gedruckt** („(Namen: Kai, Dennis, …)") — sie sind Arbeitsdaten für den Rolling Context zwischen
|
||
den Chunks (halten die Schreibweise von Personen/Firmen stabil), kein Lesestoff.
|
||
|
||
## Namen nachträglich in eine fertige Analyse übernehmen (seit 2026-07-27)
|
||
|
||
Die Zusammenfassung entsteht aus einem Dialog, in dem jeder Sprecher ein **Label** trägt: seinen Namen,
|
||
falls beim Analysezeitpunkt bekannt, sonst „Sprecher N". Wer erst DANACH benannt wird (Sprecher-Popover,
|
||
Namensmarker), stand vorher weiter als „Sprecher 5" im fertigen Text — und eine Neu-Analyse kostet auf
|
||
diesem Board ~20–30 min.
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["analyzeRecording()"] --> B["buildBasis() vergibt Labels<br/>{speaker_09: 'Sprecher 5'}"]
|
||
B --> C[("recordings.speaker_labels")]
|
||
B --> D["LLM: Zusammenfassung/Todos<br/>mit genau diesen Labels"]
|
||
E["Sprecher umbenennen<br/>(Popover / Marker)"] --> F["relabelAnalysis()"]
|
||
C --> F
|
||
F --> G["Whole-Word-Ersetzung in<br/>summary · todos · transcript_clean"]
|
||
F --> H["Teilnehmerliste NEU gebaut<br/>(Reihenfolge des ersten Auftretens)"]
|
||
F --> C
|
||
```
|
||
|
||
- `speaker_labels` (JSON, pro Aufnahme) hält genau das Label, das jeder Cluster im LLM-Text hatte —
|
||
deshalb ist die Ersetzung eindeutig und braucht keine erneute Analyse (Sekunden statt Minuten).
|
||
- Nach der Ersetzung wird das gespeicherte Label auf den Namen gesetzt, damit ein zweites Umbenennen
|
||
wieder funktioniert.
|
||
- **Teilnehmerliste** wird neu aufgebaut statt ersetzt (String-Swap ließ Sprecher stehen, die im Text nie
|
||
vorkamen, und konnte Namen doppeln).
|
||
- **Mehrdeutige Labels werden übersprungen:** trug dasselbe Label zwei Cluster (die Stimm-Erkennung hatte
|
||
„Fabio" einmal auf zwei Cluster gelegt), lässt sich im Text nicht entscheiden, wer gemeint war — dann
|
||
bleibt der Text unangetastet. Sauber wird das erst durch „Analyse neu (nur KI)" mit korrekten Namen.
|
||
- GUI: Umbenennen → Toast „Name in Zusammenfassung übernommen", Detailansicht lädt neu.
|
||
|
||
## 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 (Sampling-Münzwurf: 3/3 ok, nächster Lauf 1/3) | 1 Retry pro Chunk (P(fail) → P(fail²)), danach Salvage; bei schwachem Ergebnis „Analyse neu“ |
|
||
| Typografische Anführungszeichen (“”„) als JSON-Delimiter, trailing commas | `extractJson()`-Sanitizer: erst strikt parsen, bei Fehler Quotes/Kommas reparieren und nochmal |
|
||
| Whole-Pass > ~6k Token: englisches Geplauber, Recency-Bias, JSON-Zerfall | `WHOLE_MAX_CHARS` 20 000 hart; End-Instruktion; Repair-Call; Sanity-Check (Zusammenfassung ≥ 20 Zeichen) → sonst Chunk-Fallback |
|
||
| Kopiert `...`-Platzhalter aus Schema-Beispielen wörtlich | End-Instruktion nennt nur Feldnamen, nie ein Skelett mit Platzhaltern |
|
||
| 8k-Chunk: Inhalt wird generisches namenloses Blabla (JSON trotzdem valide!) | Chunks à 4 200 Zeichen — dort bleibt der Inhalt geerdet mit echten Eigennamen |
|
||
| Echot Feldbeschreibung INHALTS-abhängig bei JEDEM Versuch (Auto-Abschnitt #70, 2× identisch) | Echo-Guard + bis 3 Versuche mit VARIIERTEM Prompt (ohne Kontext / ohne Hint) |
|
||
| `eigennamen`-Array degeneriert (Punkte-Loops, Junk-Tails), String-Felder intakt | Feld-Salvage per Regex holt zusammenfassung + Namen einzeln raus |
|
||
| Cross-Section-Synthese: Regeln ignoriert, Namen→Rollen, Themen-Dubletten | LLM-Reduce ABGESCHAFFT — Konglomerat baut deterministischer Code |
|
||
|
||
## Eigennamen-Glossar (Whisper hört z. B. „Aime“ als „Amy“)
|
||
|
||
```mermaid
|
||
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.
|
||
|
||
**Thermik:** `analyzeRecording()` läuft seit 2026-07-16 unter dem A76-Takt-Cap (`.bak-anacap`) — rkllm
|
||
zieht neben der NPU ~1,7 CPU-Kerne, eine lange ungedeckelte Gemma-Generierung erreichte **95–97 °C**
|
||
(lüfterlos, Schutzabschaltungs-Risiko). Gleicher Cap wie `transcribeWav`/`rediarize` (Kap. 03).
|