Files
Aime/docs/08-changelog.md
T
Sascha 914ab2ddc8 feat(gui): Abschnittstitel der Zusammenfassung fett
Die Section-Zusammenfassung kommt als "Titel:\nFliesstext"-Bloecke zurueck, wurde aber
als flache Textwand gerendert. renderSummary() setzt Titelzeilen jetzt fett.

Im Renderer statt in der DB, damit es sofort fuer alle vorhandenen Zusammenfassungen
gilt, ohne 22 Aufnahmen je 20 Minuten neu zu analysieren.

Regel /^[^.!?]{3,90}:$/ = kurze, alleinstehende Zeile mit Doppelpunkt am Ende. Gegen
die echten Zusammenfassungen #85/#79/#74 geprueft: 37 Titel erkannt, keine
Fliesstextzeile falsch fett, keine Doppelpunkt-Zeile uebersehen.
2026-07-31 13:43:54 +00:00

655 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 08 — Changelog
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
Was, Warum, betroffene Dateien/Backups).
## 2026-07-31 (nachmittags) — Abschnittstitel in der Zusammenfassung fett
Die Section-Zusammenfassung kommt als `Abschnittstitel:\nFließtext…`-Blöcke zurück (llm.js/analyzeLong),
wurde aber als eine flache Textwand gerendert. Titelzeilen erscheinen jetzt fett.
**Bewusst im Renderer, nicht in der Datenbank:** so gilt es sofort für alle bereits vorhandenen
Zusammenfassungen, ohne auf 22 Aufnahmen je eine 20-Minuten-Analyse neu zu fahren.
Erkennungsregel in `renderSummary()` (`main.js`): eine **kurze, alleinstehende** Zeile, die auf einen
Doppelpunkt endet und kein `.` `!` `?` enthält — `/^[^.!?]{3,90}:$/`. Beide Bedingungen sind nötig: Fließtext
wie „…die Struktur der Projektberichterstattung: kurze Einordnung, aktueller Stand…" hat den Doppelpunkt
mitten in der Zeile und läuft weiter.
**Gegen echte Daten geprüft** (Zusammenfassungen #85, #79, #74): 37 Titel erkannt, keine Fließtextzeile
fälschlich fett, keine Doppelpunkt-Zeile übersehen.
Dateien: `app/web/src/main.js`, `app/web/src/style.css` (`.summary .sum-head`).
Board-Backups: `.bak-sumhead`.
## 2026-07-31 — Inhaltssuche, Papierkorb, Titel umbenennen (+ WLAN-Ausfall behoben)
Drei Funktionen auf Wunsch des Nutzers, dazu die Störung vom Vormittag. Details:
[09 – Suche & Verwaltung](09-suche-und-verwaltung.md).
### Vorab: Board war nicht erreichbar — SSID-Rename killt den PSK-Hash
Das Board lief weiter als USB-Soundkarte, war aber im Netz unsichtbar. `wpa_supplicant` hing auf
`ssid="RupuWolf5"`; das WLAN heißt seit dem Zusammenlegen der Bänder nur noch **`RupuWolf`** (4 BSSIDs,
2437/2462/5300/5500 MHz). Nach dem SSID-Fix scheiterte die Anmeldung **trotz unverändertem Passwort**:
```
wlP4p65s0: WPA: 4-Way Handshake failed - pre-shared key may be incorrect
CTRL-EVENT-SSID-TEMP-DISABLED ssid="RupuWolf" auth_failures=1 reason=WRONG_KEY
```
Ursache: die Conf speichert `psk=<64 Hex>`, und **WPA-PSK leitet diesen Hash per PBKDF2 mit der SSID als
Salt ab** — ein SSID-Rename entwertet jeden vorberechneten Hash. Fix: Hash aus dem Klartext der
`#psk="…"`-Kommentarzeile neu rechnen (`wpa_passphrase "RupuWolf" "$PLAIN"`).
Backups: `.bak-rupuwolf5` (alte SSID), `.bak-oldhash`.
Außerdem: Hostname auf **`AIme`** gesetzt, `avahi-daemon` installiert → **`AIme.local`** löst per mDNS auf
und ersetzt das IP-Suchen. Die FritzBox zeigt weiter `armbian-sd`, weil sie den Gerätenamen beim ersten
DHCP-Kontakt einfriert; ein Mitschnitt belegt, dass das Board korrekt `Hostname (12): "AIme"` sendet.
### Titel umbenennen
`PATCH /api/recordings/:id/title` + Stift in Liste und Detailkopf. Neue Spalte
**`recordings.title_locked`**: ein von Hand gesetzter Titel überlebt jede spätere KI-Analyse. Leeres Feld
löscht Titel und Sperre.
### Papierkorb, 30 Tage
`DELETE /api/recordings/:id` setzt nur noch `deleted_at`; Audio, Transkript und Analyse bleiben vollständig
liegen. Neue Ansicht über der Liste mit Resttagen (rot ab 3 Tagen), Wiederherstellen, endgültig Löschen.
Aufräumlauf beim Boot **und** täglich — der Boot-Lauf, weil das Board oft tagelang aus ist.
**Altfehler mitbehoben:** das bisherige Löschen ließ die `words`-Zeilen verwaist in der Datenbank und die
WAV/MP3-Dateien für immer auf der Karte zurück. `purgeRecording` räumt beides ab.
### Inhaltssuche mit Zeitstempel-Verweis
Neue Tabelle `passages` (1689 Zeilen aus 22 Aufnahmen), neuer Tab „Suche" in Google-Optik: Trefferliste
sofort aus SQL, formulierte Antwort mit Belegen `[1] [2]` wird nachgereicht (Qwen3-4B, **~140 s**). Jeder
Treffer nennt Meeting, Sprecher und Gesprächsminute und springt beim Klick genau dorthin.
Hybrid aus Volltext (MariaDB FULLTEXT), Passage-Embedding und Dokument-Embedding
(`multilingual-e5-small`, ONNX/CPU auf den A55-Kernen). **Ohne das Modell degradiert die Suche sauber auf
Volltext statt auszufallen.**
**Gemessene Grenzen — bewusst dokumentiert statt geschönt:**
- Volltext allein findet „Steueroptimierung" **nicht** (im Transkript steht eine andere Wortform) → genau
dafür ist die semantische Ebene da, die findet #71 mit `vec=0,92`.
- `e5-small` trennt auf verrauschtem ASR-Text schwach: für „Wie werden unsere Daten gesichert" lag die
Kosinus-Spanne **aller** 1689 Passagen bei nur 0,8085–0,8560, und eine irrelevante Passage („Agenda
zusammenpacken", 0,8560) schlug die richtige Antwort („Backup-Job startet um 11 Uhr", 0,8419).
- Gegenmaßnahmen: Dokument-Ebene aus Titel + Zusammenfassung (sauberer LLM-Text statt ASR-Rauschen) und
IDF-Wertung (Auswahl per `BOOLEAN MODE`, Wertung per `NATURAL LANGUAGE MODE` — Boolean gewichtet nicht
nach Seltenheit, weshalb „Daten"-Wiederholung „gesichert" schlug).
- Ergebnis: richtige Passage auf **Rang 11 von 296** statt gar nicht. Auffindbar, aber nicht in den Top-8,
die das LLM liest. **Nächster Hebel: `multilingual-e5-base`.**
Relative Datumsangaben („letzte Woche") werden **deterministisch** aufgelöst, nicht vom LLM — ein falsches
Kalenderfenster wäre schlimmer als keins. Der Datumsausdruck wird aus den Suchbegriffen entfernt.
### Dateien
Neu: `app/search.js`, `app/emb.js`, `app/inference/embed_text.py`, `docs/09-suche-und-verwaltung.md`.
Geändert: `app/db.js`, `app/server.js`, `app/web/src/main.js`, `app/web/src/style.css`, `README.md`.
Board-Backups: `.bak-search`. Nicht im Repo: `/opt/aime-models/e5-small/` (470 MB Modell).
## 2026-07-30 (nachmittags) — v2 machte den Uplink stumm, v3 behebt es
**Symptom (Nutzer, Testanruf 13:51):** „Die Gegenseite hat mich nicht mehr gehört." Regression aus v2 vom
selben Tag. Das Journal zeigte den Beweis direkt:
```
iPhone call bluez_output.B4_96_A5_42_BD_D6.1: -callrec_mix
```
Nur `-callrec_mix`, kein `+micgate`, kein `+far-end` — v2 hat WirePlumbers Link **getrennt** und dann
**nichts** verlinkt. Uplink ohne Quelle = die Gegenseite hört absolute Stille. Immerhin: v2 hat die
HFP-Nodes erstmals überhaupt erkannt, der Portnamen-Fix von vormittags greift also.
**Ursache: `pw-link -l <node>` filtert nicht.** Reproduziert auf dem Board: mit einem Node-Namen, der nicht
als *Port* auflösbar ist, gibt `pw-link -l` den **gesamten Graph** aus (sogar `pw-link -l micgate.out`
liefert zusätzlich `micgate:playback_MONO`). Der Graph enthält immer `micgate.out` und `callmix` →
- `grep -q micgate.out` → wahr → „schon verlinkt" → **micgate-Link übersprungen**
- `grep -q callmix` → wahr → **Gegenseiten-Link übersprungen**
- `grep -q callrec_mix` → wahr → das Trennen war die einzige Aktion, die je lief
Derselbe Ausdruck steckte schon in v1, dort aber hinter dem Einmal-Guard, der ohnehin nie erreicht wurde.
**Fix `aime-iphone-route.sh` v3 (`.bak-v2`):**
- Neue `links_of PORT` parst die Peers **eines exakten Ports** aus einem einzigen Graph-Dump (awk: Header
auf Spalte 0 exakt vergleichen, eingerückte `|->`/`|<-`-Zeilen als Peers). Kein `grep` mehr über alles.
- **Sicherheitsbedingung: erst verlinken, dann trennen.** `callrec_mix → Uplink` wird nur entfernt, wenn
`micgate.out` am Uplink-Port **bestätigt** anliegt (Graph danach neu gelesen). Sonst bleibt der
WirePlumber-Link stehen und es wird `!no-micgate-uplink-keeping-callrec_mix` geloggt. Ein Echo ist
schlecht, unhörbar sein ist schlimmer.
- Log-Zustand als **Array** statt String: `CHANGED=" "` (nur Leerzeichen) hätte `[ -n ]` passiert und bei
fertig geroutetem Anruf jede Sekunde eine Journal-Zeile geschrieben.
**Verifiziert:** `links_of` gegen den echten Graph (verlinkter Port, unverlinkter Port, nicht existierender
Port — jeweils exakt). Entscheidungslogik gegen einen synthetischen **Mono**-HFP-Graph mit gestubbtem
`pw-link`: Pass 1 (WirePlumber-Default aktiv) → `+micgate` zuerst, dann **beide** `callrec_mix`-Links weg,
Gegenseite in **beide** callmix-Kanäle, und **genau ein** micgate-Link in den Mono-Port (zwei wären +6 dB);
Pass 2 (fertig geroutet) → vollständiger No-op, keine Log-Zeile; Pass 3 (micgate nicht bestätigt) →
`callrec_mix` bleibt stehen, kein Trennen. Nach dem Neustart Graph unverändert bei 32 Links, Script idle
ohne Anruf.
**Noch offen:** Gegenprobe am echten Anruf. Erwartet wird eine Zeile mit `+micgate`, `-callrec_mix` und
`+far-end`. Steht dort `!no-micgate-uplink-keeping-callrec_mix`, ist der Uplink hörbar aber mit Echo, und
der micgate-Link braucht eine eigene Analyse.
## 2026-07-30 (vormittags) — iPhone-Anruf: Gegenseite fehlt in der Aufnahme + Gegenseite hört sich doppelt
**Ausgangslage (Nutzer):** Ein BT-Telefonat wurde aufgenommen, in der Aufnahme ist nur die Nahseite zu
hören; die Gegenseite berichtete zusätzlich, sich selbst stark doppelt zu hören. Der Pfad hatte am
2026-07-13 nachweislich funktioniert.
**Messung:** Aufnahme #80 (13:16, 44 s) → Diarisierung `1 Sprecher` (bekannte Stimme „sascha", cos 0,779);
#81 (13:38) ebenso 1 Sprecher. `journalctl -t aime-iphone-route` über den **gesamten** Boot seit
2026-07-29 12:15 → `-- No entries --`. Watcher-Prozess lief (PID 1446, 27 h), Service `active`, WirePlumber-
Drop-in intakt (`bluez5.roles`, `hfphsp-backend=native`), iPhone `Paired/Bonded/Trusted/Connected = yes`,
Profil-Enum bietet `audio-gateway (A2DP Source & HSP/HFP AG)`. Der Nutzer hat die Gegenseite im H390-Headset
gehört → die HFP-Nodes **existierten**, der Watcher hat sie nur nicht erkannt.
**Ursache: eine, mit zwei Symptomen.** `aime-iphone-route.sh` v1 hat nie gegriffen:
1. *Hartcodierte Portnamen.* Gesucht wurde `bluez_output.<MAC>…:input_FL`. HFP/mSBC ist **mono**, die Ports
heißen `playback_MONO`/`capture_MONO` → der `grep` matchte nie, das Script tat buchstäblich nichts.
Erklärt die 0 Log-Zeilen.
2. *Einmal-Guard.* `if ! pw-link -l "$B" | grep -q micgate.out` übersprang den gesamten Block, sobald
micgate einmal verlinkt war. Da WirePlumber die HFP-Nodes **nicht atomar** anlegt (Uplink erscheint
typischerweise vor der Gegenseite), blieb `bluez_input` in genau diesem einen Durchlauf leer → die
Gegenseite wurde für den ganzen Anruf nie nachverlinkt. Ebenso wurde ein erst mitten im Anruf von
WirePlumber gesetztes `callrec_mix → Uplink` nie mehr entfernt.
Daraus: Gegenseite → `callmix` fehlt → **nicht in der Aufnahme**. Und `callrec_mix → Uplink` bleibt stehen →
Uplink führt den ungegateten Mix statt `micgate.out` → **Rückfall auf genau den Echo-Zustand vor dem
Fix vom 2026-07-13**.
**Fix `aime-iphone-route.sh` v2 (`.bak-v1`):** Portnamen werden über `pw-link -i/-o` **ermittelt** statt
hartcodiert; neue `link_all`-Hilfsfunktion legt **genau einen Link pro Ziel-Port** (mono-Quelle speist beide
Kanäle; Stereo-Quelle auf mono-HFP-Port sendet nur FL — beide zu verlinken würde sie in denselben Port
summieren = +6 dB auf dem Uplink). Alle drei Links werden in **jedem** Durchlauf einzeln geprüft und
nachgezogen (Takt 2 s → 1 s), das Einmal-Guard ist weg. `logger` schreibt nur bei Zustandswechsel, damit
das Journal nicht flutet.
**Verifiziert:** `bash -n` OK; `link_all` mit gestubbtem `pw-link` gegen alle drei Fälle getestet
(stereo→stereo, stereo→mono, mono→stereo) und gegen leere Arrays; Graph nach dem Service-Neustart
unverändert bei 32 Links, Script idle ohne HFP-Nodes. **Noch offen: Gegenprobe am echten Anruf** —
`journalctl -t aime-iphone-route` muss dann `+micgate` und `+far-end` zeigen.
**Doku:** Kapitel 06 — Watcher-Sequenzdiagramm auf den Ein-Sekunden-Prüfzyklus umgestellt, Klarstellung
dass Gegenseite → Headset von WirePlumber kommt und nicht vom Watcher (das alte Diagramm behauptete das
Gegenteil), Kontrollkommando ergänzt.
**Nebenbefund (offen):** `callrec.service` ist am 2026-07-30 13:39:33 mit `Error: write EPIPE` in
`llm.js:84` gestorben (rkllm-Socket beim Schreiben weg), systemd hat neu gestartet (Restart-Counter 1).
Der Socket-Write in `llm.js` hat keinen `error`-Handler → unhandled `'error'` event reißt den ganzen
Server mit. Die Zusammenfassung von #81 dürfte dadurch fehlen.
## 2026-07-29 — #79: verlorener Sprecher + magere Zusammenfassung (Vergleich gegen MS Teams)
**Ausgangslage (Nutzer):** Aufnahme #79 (23-min-Marketing-Jour-fixe) wurde trotz live getippter Namen
falsch analysiert — 3 statt 4 Sprecher, Zusammenfassung 1 082 Zeichen ohne Inhalt. Als Messlatte lagen die
Teams-Notizen desselben Meetings vor (5 Themenblöcke + 3 Folgeaufgaben).
**Zwei unabhängige Ursachen, beide gemessen:**
1. *Diarisierung:* Stufe A lieferte korrekt 4 starke Cluster. Von 4 Markern mit 3 Namen landete der zweite
„Merle" auf **Saschas** Cluster; das bedingungslose must-link verschmolz beide (Kosinus **−0,727**).
2. *LLM:* 21k Zeichen < `WHOLE_MAX_CHARS` 32 000 und 3 < `MANY_SPEAKERS` 5 → Whole-Pass in einem Call →
das erste Thema (Timo/Sabbatical) fiel der Prompt-Kompression zum Opfer.
**Fixes `diar.js` (`.bak-marklink`):** Anker-Gewicht nach Trefferqualität (im Turn 1,0 → 25 s Tippverzögerung
0,2); must-link nur bei `cos ≥ 0,6 × gelernter Schwelle` (`DIAR_MARK_LINK_REL`), sonst Marker verwerfen +
loggen; Warnung bei mehreren Namen auf einem Cluster; **Voiceprints als cannot-link** (`DIAR_VP_MIN` 0,55,
Abstand 0,05, Namensschlüssel = Vorname) — damit ist der Box-Besitzer immer ein eigener Sprecher.
**Fixes `server.js` (`.bak-vpconstraint`, `.bak-uniqlabel`) / `recorder.js`:** Voiceprints werden in
`rediarize()` und `transcribeAndAnalyze()` in die Diarisierung durchgereicht; `applyMarkers()` verschmilzt
nur noch bei `conf = high` (`mid` benennt weiterhin); `buildBasis()` vergibt **eindeutige** Labels (in #79
trugen zwei Cluster „Markus", der LLM verschmolz daraufhin zwei Personen im Text); Stimmabdruck schlägt
einen unsicher verankerten Marker; `relabelAnalysis()` läuft am Ende JEDER Analyse (Namen, die während der
~20 min Laufzeit entstehen, landen sonst nie im fertigen Text — „Sprecher 1" blieb stehen).
**Fixes `llm.js` (`.bak-sectionmin`, `.bak-grounding`):** `SECTION_MIN_CHARS` = 9 000 (alles darüber in den
Sektions-Pfad), `MANY_SPEAKERS` 5 → 3, `CHUNK_CHARS` 4 200 → 3 200, `TAIL_CHARS` = 700 (wörtlicher Kontext
aus dem Vorgänger-Chunk), Prompt-Regeln „jedes Thema mind. ein Satz" + „nicht verallgemeinern", gezielter
Prüf-Aufruf bei Grundsatz-Aussagen, Verwerfen einer reinen Verabschiedungs-Sektion am Ende.
**Ergebnis #79:** 4 Sprecher (Sascha/Markus/Lisa/Merle, alle automatisch benannt), Zusammenfassung
**1 082 → ~5 300 Zeichen**, 7 Themenblöcke, 15–18 ToDos mit Verantwortlichen; alle Teams-Themen abgedeckt.
Regression geprüft: #70 → 4, #68 → 2, #74 → 20 Sprecher (unverändert).
**Neu im Labor:** `app/lab/lab-eval2.mjs` (Replay des echten `clusterWindows()` MIT Voiceprints),
`app/lab/vp-dump.mjs` (Voiceprints als JSON). Damit kostet eine Clustering-Iteration Sekunden statt eines
sherpa-Laufs.
**Betriebs-GOTCHA:** der A76-Takt wurde für einen sherpa-Lab-Lauf auf 2,256 GHz gesetzt — 88–89 °C, kurz
darauf hing das Board (kein Ping auf LAN und WLAN, Hard-Reset nötig). Lüfterlos gilt weiter: schwere Jobs
seriell und Cap 1,6 GHz. Nach dem Neustart hat der R1 per DHCP **192.168.1.48** bekommen (vorher .200).
## 2026-07-28 — GUI: alle Tabs nutzen die volle Fensterbreite
- **Problem:** nur „Aufnahmen" bekam `#app.wide` (max-width: none), Stimmen/Glossar/Bluetooth blieben auf
860 px zentriert → beim Tabwechsel sprang die ganze Kopfzeile in die Bildschirmmitte.
- **Fix:** `.wide`-Sonderfall entfernt — `#app { max-width: none; padding: 16px 28px }` gilt für alle Tabs,
auf < 800 px wieder 16 px Padding. `render()` setzt keine Klasse mehr (`main.js`).
Bundle `index-BOK2TehD.js`, CSS `index-B0V_89UC.css`.
## 2026-07-27 (3) — „(Namen: …)"-Zeilen aus der Zusammenfassung entfernt
- **Problem (Nutzer):** unter jedem Absatz der Zusammenfassung stand „(Namen: Kai, Dennis, …)".
- **Fix (`llm.js`, `.bak-nonames`):** `composeSummary()` druckt die pro Abschnitt extrahierten Eigennamen
nicht mehr in den Body. Die Extraktion bleibt — sie ist Arbeitsdatum für den Rolling Context zwischen
den Chunks (stabile Schreibweise von Personen/Firmen) und für die Schluss-Zeile „Genannte Namen: …".
- **Bestand bereinigt:** Skript entfernte die Zeilen aus bereits gespeicherten Zusammenfassungen
(#74: 7503 → 7203 Zeichen), keine Neu-Analyse nötig.
## 2026-07-27 (2) — Namen nachträglich in die fertige Zusammenfassung übernehmen
- **Problem (Nutzer):** alle 20 Sprecher von #74 benannt, in der Zusammenfassung stand trotzdem weiter
„Sprecher 5" statt Katharina. Grund: der Text entsteht mit den Labels, die beim Analysezeitpunkt galten;
spätere Namen ändern nur `speaker_names`. Zusätzlich zählte `buildBasis()` „Sprecher N" nur über die
UNBENANNTEN Sprecher, die Teilnehmerliste und die Spur-Beschriftung dagegen über alle → dieselbe Nummer
meinte in Text und GUI verschiedene Personen.
- **Fix:** neue Spalte `recordings.speaker_labels` (JSON) speichert das Label jedes Clusters im LLM-Text;
`relabelAnalysis()` (server.js) ersetzt beim Umbenennen wortgenau in `summary`, `todos` und
`transcript_clean`, baut die Teilnehmerliste aus der Diarization neu auf und schreibt das Label auf den
neuen Namen fort. Aufgerufen aus `POST /recordings/:id/speaker` und aus `applyMarkers()`.
Mehrdeutige Labels (ein Label auf zwei Clustern) werden bewusst NICHT ersetzt. Teilnehmerliste der
Analyse nutzt jetzt dieselben Labels wie der Dialogtext.
- **#74 nachgezogen:** Label-Karte des alten Laufs rekonstruiert (Namen zum Analysezeitpunkt:
Kai/Fabio/Fabio) und angewandt → **0 „Sprecher N" mehr in Summary und Todos**, Teilnehmerliste = die 20
echten Namen in Reihenfolge des ersten Auftretens. Rest: „Fabio" steht an 10 Stellen, davon einige
eigentlich Nico (die Stimm-ID hatte damals zwei Cluster „Fabio" genannt) — sauber nur per
„Analyse neu (nur KI)".
- **Dateien:** `server.js` (buildBasis liefert Labels, relabelAnalysis), `db.js`
(`speaker_labels`, `setSpeakerLabels`/`getSpeakerLabels`), `web/src/main.js` (Toast).
Bundle `index-TOCXxq47.js`.
## 2026-07-27 — Diarization v2 (Fenster-Clustering, gelernte Schwelle) + Live-Namensmarker
- **Problem:** #74 (33-min-Weekly, 22 Teilnehmer + ein Kind) automatisch neu analysiert → **44 Sprecher**,
dieselbe Stimme mehrfach als verschiedene Personen. Ursache (gemessen, nicht geraten): v1 bildete
EIN Zentroid pro sherpa-Cluster aus max. 3 Schnitten und verglich mit fester Kosinus-Schwelle auf
ROHEN Embeddings — unzentrierte TitaNet-Vektoren einer Aufnahme haben aber eine große gemeinsame
Komponente (Kreuz-Turn-Kosinus ≈ 0,4), damit ist keine Schwelle trennscharf. 51 Roh-Cluster → 44.
- **Fix — `diar.js` v2 (`.bak-v2clust`):** sherpa liefert nur noch Turn-GRENZEN
(`--min-duration-off=0`, threshold 0,2 — Labels verworfen). Danach: 3-s-Fenster (Hop 1,5 s, PCM-Slicing
ohne ffmpeg) → TitaNet-Embedding pro Fenster → **L2 / Aufnahme-Mittelwert abziehen / L2**
(Kanal-Kompensation: Kreuz-Kosinus ≈ 0, d' 3,5) → **Average-Linkage-AHC** Stufe A (fein 0,45) →
Weak-Fold (< min(20 s, 2 % Sprechzeit)) → Stufe B bis zur **pro Aufnahme gelernten Schwelle**
(2-Gauß-EM auf den Paar-Ähnlichkeiten, `mergeThreshold()`, Clamp 0,05–0,35) oder bis zur gepinnten
Anzahl. Turn-Label = dauergewichtete Mehrheit seiner Fenster. `DIAR_MODE=legacy` = alter Pfad.
- **Warum gelernte Schwelle:** fest 0,30 → #74 21 Sprecher (gut), #70 aber 9 (statt 4); fest 0,15 → #70 4,
#74 kollabiert auf 14. Die Verteilung ist pro Aufnahme verschieden (gleicher Sprecher 0,21–0,30 je nach
Audioqualität). Gelernt: #74 0,202 · #70 0,070 · #68 0,050.
- **Validierung (Labor + Produktion):** #74 44 → **20** · #70 (4 bekannte Sprecher) → **4** ·
#68 (2 Personen) → **2**. Modell-Benchmark auf #74 (1181 Fenster): TitaNet-large d' 3,75/197 s,
CAM++ 3,72/82 s, **TitaNet-small 3,54/57 s**, wespeaker-VoxCeleb-resnet34 2,43/185 s → durch die
Pipeline geschickt alle innerhalb eines Sprechers → TitaNet-small bleibt (schnell + alle gelernten
Voiceprints bleiben gültig). Voiceprints tragen jetzt die Spalte `model`.
- **NEU: Live-Namensmarker** (Wunsch des Nutzers) — während der Aufnahme den Namen der gerade sprechenden
Person eintippen (Kopfzeilen-Feld, Enter). Tabelle `markers` (t_ms, name, speaker, conf);
`POST/GET/DELETE /api/record/marker(s)` live, `POST /api/recordings/:id/markers` zum Nachtragen an der
Abspielposition, `POST /api/recordings/:id/apply-markers` zum Neu-Zuordnen ohne sherpa-Lauf.
Zuordnung `diar.resolveMarkers()`: asymmetrisches Fenster 25 s zurück / 8 s vorwärts (man tippt NACH
dem Hören), Turn der den Marker überdeckt gewinnt, `conf` high/mid/low. Zusätzlich **Supervision der
Trennung**: gleicher Name = must-link, verschiedene Namen = cannot-link in Stufe B; ein Name auf mehreren
Clustern → Cluster werden zusammengeführt. Benannte Sprecher werden automatisch als Voiceprint gelernt.
- **Re-Diarization löscht jetzt `speaker_names`** (`server.js`): die Cluster werden neu durchnummeriert,
alte Namen zeigten danach auf fremde Stimmen. Marker + Auto-ID füllen sofort neu.
- **Dateien:** `diar.js` (v2 + resolveMarkers), `server.js` (applyMarkers, Marker-Endpunkte,
Voiceprint-Modellbindung), `db.js` (`markers`-Tabelle, `voiceprints.model`), `recorder.js`
(Marker-Puffer während der Aufnahme), `spk.js` (`modelId()`, Modell-Doku), `web/src/main.js` +
`style.css` (Kopfzeilen-Eingabe, Marker-Chips, Marker-Liste im Detail). Backups `.bak-v2clust`,
`.bak-markers`. Bundle `index-q6bHhDgg.js`. Labor-Skripte (Segment-Dump, Fenster-Embeddings, Cluster-Grid, Schwellen-Fit) liegen auf dem Board
unter `/opt/callrec/lab/` und im Repo unter `app/lab/` — damit ist das Tuning reproduzierbar.
- **Nachtrag gleicher Tag — „Voice-Auto-Naming tot" (offen seit 2026-07-16) GELÖST:** TitaNet-ONNX bricht bei
langen Eingaben ab (reproduziert 180 s → `Non-zero status code … broadcast … 12288 by 17998`, 20/60 s ok).
`identifySpeakers()` embettete den LÄNGSTEN Lauf ungekürzt (auf #74 minutenlang) → Helfer stirbt beim
ersten Sprecher; da `spk.js` über eine Promise-Kette serialisiert und `.then()` auf einer rejected Promise
den Callback überspringt, schlugen ALLE weiteren Sprecher sofort mit „spk beendet" fehl. Fixes:
`ID_CUT_SEC = 25 s` in `identifySpeakers()`, Kette in `spk.js` + `tdChain` in `recorder.js` failure-isoliert,
und pro Name gewinnt nur der beste Kosinus (#74 hatte „Fabio" auf zwei Clustern). Verifiziert per
Testskript: 180-s-Schnitt scheitert, direkt danach liefert ein 20-s-Schnitt wieder ein Embedding.
- **GOTCHA (Betrieb):** zwei schwere Jobs parallel (Labor-sherpa + App-Analyse, deren `finally` den Takt
auf 2,25 GHz zurückstellt) haben die A76 auf **101,9 °C** getrieben — lüfterloses Board, das ist
Shutdown-Nähe. Schwere Jobs immer serialisieren, Cap 1,4–1,6 GHz.
## 2026-07-17 — Round-Robin-Meetings: Sektions-Pfad statt Whole-Pass ab 5 Sprechern (Recency-Bias-Fix)
- **Problem:** #74 (33-min-Weekly, 13 Sprecher, 31k Zeichen) lief knapp unter `WHOLE_MAX_CHARS` als
Whole-Pass → Summary enthielt fast nur die letzten Sprecher. 4B kann 15+ gleichwertige Themen in
einem ~10k-Token-Prompt nicht gewichten; Whole-Pass taugt nur für narrative Gespräche mit wenigen
Sprechern.
- **Fix (llm.js, `.bak-standup`):** `analyze()` routet labeled Transkripte mit ≥ 5 Sprechern
(`MANY_SPEAKERS`, `countSpeakers()`) immer in `analyzeLong` (Sektions-Konglomerat). Sektions-Pfad
auf Besprechungsnotizen-Niveau ausgebaut: `thema`-Überschrift pro Chunk, Pflicht zur Erwähnung
JEDER berichtenden Person, Smalltalk-Dämpfung (Privates max. Halbsatz), Todos nur beruflich mit
Verantwortlichem in Klammern, Überblick-Mikro-Call (`PROMPT_OVERVIEW`, Key-tolerant: Qwen schreibt
teils `"überblick"`), Summary-Cap 6 000 → 16 000 (alter Cap schnitt 10 Sektionen mitten im Satz ab).
- **Validierung:** 3 Iterationen per `reanalyze-llm` auf #74 — final alle 13 Sprecher + alle Themen
abgedeckt (Intro-Absatz, 10 Themen-Sektionen, 30 saubere Todos mit Verantwortlichen), kein
Recency-Bias. Temporärer `/api/debug/ask` fürs Overview-Prompt-Tuning wieder entfernt
(server.js unverändert).
## 2026-07-17 — GUI: Aufnahme-Tab gestrichen, Record-Button in der Kopfzeile + Stille-MP3-Fix
- Der Reiter "Aufnahme" ist weg: Quelle (CallRecMix), HFP-Codec (auto) und Sprecherzahl (auto) waren
eh die Defaults. Aufnahme-Knopf (roter Kreis) + Laufzeit sitzen jetzt MITTIG in der Kopfzeile
zwischen Logo und Tabs; beim Aufnehmen pulsiert er rot. Start legt SOFORT ein rotes
"Aufnahme laeuft..."-Kaestchen mit Live-Timer ganz oben in der Liste an; Stop erzeugt daraus die
echte Zeile (transcribing) und oeffnet sie rechts. Standard-Tab ist jetzt "Aufnahmen".
- **recorder.js (`.bak-silentmp3`):** lame crashte auf komplett stiller Aufnahme
(`psymodel.c calc_energy: el >= 0` — seit dem Mic-Gate real: niemand sprach = Null-Samples,
loudnorm-Kette triggert die Assertion) -> Fallback-Encode ohne Filter statt Stop-Fehler.
E2E getestet (3-s-Stille-Aufnahme: Stop ok, Zeile erstellt, transkribiert, wieder geloescht).
Bundle `index-UPMXRPJp.js`.
## 2026-07-17 — GUI: Scrollbalken im Logo-Kupfer
Alle Scrollbalken (Liste, Transkript-Panel, Sprechblase, Seite) jetzt im Akzent-Kupfer:
Firefox via scrollbar-color, Chromium/WebKit via ::-webkit-scrollbar-* (10 px, runder Daumen,
transparente Schiene). Bundle `index-U7lT-ig5.js`.
## 2026-07-17 — GUI: Listenbreite per Splitter ziehbar
Zwischen Liste und Detail sitzt jetzt ein 6-px-Splitter (Hover/Ziehen = Kupfer-Schimmer,
col-resize-Cursor): Liste zwischen 340 px (alte Festbreite = Minimum) und 680 px (Maximum = doppelt)
frei ziehbar, Breite persistiert in localStorage['aime-list-width']. Auf schmalen Screens (gestapelt)
ausgeblendet. Playwright-getestet (Clamp 340/680 + Persistenz). Bundle `index-BbmdfFEI.js`.
## 2026-07-17 — GUI: Kupfer-Zaehler, Favicon, Tab-Reihenfolge
Tages-Zaehler in der Liste jetzt im Logo-Kupfer (accent-soft-Pille); Wellenform-Logo als Favicon
(SVG-data-URI in index.html, Browser-Tab); Tab-Reihenfolge neu: Aufnahme, Aufnahmen, Stimmen,
Glossar, Bluetooth (Bluetooth ans Ende). Bundle `index-Bt2IsJIF.js`.
## 2026-07-17 — GUI: Liste startet mit nur dem neuesten Tag geoeffnet
Beim Betreten des Aufnahmen-Tabs ist nur noch die aktuellste Tagesgruppe aufgeklappt, alle aelteren
sind eingeklappt (Zaehler-Badge zeigt die Anzahl). Manuelles Auf-/Zuklappen bleibt bis zum naechsten
Tab-Wechsel erhalten (state.resetCollapse-Flag, angewandt beim ersten Listen-Build).
Bundle `index-C_-UUeU5.js`.
## 2026-07-17 — GUI: Transkript/Am-Tisch-Panel ebenfalls verschiebbar (zwei Drop-Spalten)
Alle Bloecke — Zusammenfassung, ToDos UND das Transkript-/Tisch-Panel — sind jetzt per Drag & Drop
frei anordenbar, auch SPALTENUEBERGREIFEND: zwei Drop-Zonen (links 54 % / rechts 46 %), Layout
persistiert als localStorage['aime-detail-layout'] {main:[...], side:[...]} (altes
aime-detail-order wird migriert). Eine leere Spalte kollabiert zu einem schmalen gestrichelten
Streifen und bleibt Drop-Ziel (JS-Klasse .empty — :empty scheitert an Template-Whitespace).
Bundle `index-B1y2-Z_b.js`.
## 2026-07-17 — GUI: Sprecher-Modul entfernt, Umbenennen direkt am Verlauf
Das separate "Sprecher"-Modul war redundant zu den Spur-Labels. Jetzt: Klick auf einen Namen links
im Sprecher-Verlauf oeffnet ein Popover (Name aendern + "als Stimme lernen" + Speichern) — gleiche
API wie vorher (POST /speaker). Klick-Seek in den Spuren zuendet nur noch IN einer Spur, nicht mehr
auf der Label-Spalte. Bundle `index-Bj1pQQtk.js`.
## 2026-07-17 — GUI: Blasen-Oeffnung zeigt exakt auf den Sprecher
Bei mehreren Sprechern je Tischkante (5-/6-Sitz-Layouts) zeigte der Schwanz nur auf die Kantenmitte.
Jetzt wird die Sitz-x-Position in Blasen-Koordinaten umgerechnet (CSS-Variable --ax, BUBBLE_W in
main.js synchron zur CSS-Breite) — die Oeffnung wandert entlang der Ober-/Unterkante exakt zum
Sprechenden; bottomleft/bottomright-Sonderklassen entfallen. Bundle `index-FKbNFs-A.js`.
## 2026-07-17 — GUI: Tisch-Blase fix, Oeffnung wandert zum Sprecher
Feinschliff nach User-Feedback: Die Sprechblase liegt wieder FEST auf dem Tisch (54 % Breite),
nur ihr Schwanz/die Oeffnung springt per arrow-Klasse zum jeweils Sprechenden; Rand-/Namensfarbe
wechseln weich mit (border-color-Transition). Wort-Highlight, internes Mitscrollen und Klick-Seek
bleiben. Bundle `index-CpZJOt3u.js`.
## 2026-07-17 — GUI: Sprechblase am Sprecher mit Wort-Highlight + Mitscrollen
Tisch-Ansicht verfeinert: Die Blase haengt jetzt klassisch am MUND des Sprechers (dynamisch zwischen
Sitz und Tischmitte positioniert, Schwanz zeigt zum Kopf) statt zentral auf dem Tisch. Inhalt = der
komplette Redebeitrag als einzelne Wort-Spans: aktives Wort wird synchron zum Audio gehighlightet
(wie im Transkript), die Blase scrollt intern mit (innere .tb-scroll-Huelle, damit der Pfeil nicht
mitscrollt), Klick auf ein Wort springt im Audio dorthin. Bundle `index-BK0gpRp9.js`.
## 2026-07-17 — GUI: Konferenztisch-Ansicht (Umschalter neben "Analyse neu")
Neuer Umschalt-Button (Icon: drei Personen am Tisch <-> Textblatt) in der Status-Zeile: ersetzt das
Transkript rechts durch eine animierte Szene — Sprecher als Figuren in ihren Spurfarben um einen
abgerundeten Tisch, Sprechblase AUF dem Tisch (Rand/Name in Sprecherfarbe, Pfeil zeigt zum Sprecher,
Text = aktueller Redebeitrag), alle anderen Personen drehen die Pupillen zum Sprechenden, dessen Mund
animiert beim Reden (reduced-motion respektiert). Sitzordnungen fuer 1-6 Teilnehmer vordefiniert.
Wahl bleibt in localStorage['aime-side-view']. Bundle `index-DXTNo40e.js`.
## 2026-07-17 — GUI: Aufnahmen-Tab nutzt die volle Fensterbreite
`#app.wide` war auf 1180 px gedeckelt — auf breiten Monitoren blieb rechts/links viel Leerraum und
die Zusammenfassung wurde unnötig schmal/lang. Deckel entfernt (max-width: none, Padding 28 px);
die Liste bleibt 340 px fix, das Detail bekommt den Rest. Bundle `index-RpsXFTh8.js`.
## 2026-07-17 — GUI: Live-Text-Box entfernt
Die Box rechts neben den Sprecher-Spuren (aktueller Redebeitrag) flog auf User-Wunsch raus —
redundant zum mitscrollenden Transkript daneben. Spuren nutzen jetzt die volle Breite.
Bundle `index-BiCA5LX5.js`.
## 2026-07-17 — GUI: Sprecher-Verlauf als Vollbreiten-Streifen unter dem Player
Der Sprecher-Verlauf (Spuren + Live-Text) sitzt jetzt fix unter der Audio-Laufleiste und zieht sich
ueber BEIDE Spalten (oberhalb von Zusammenfassung und Transkript); aus den Drag&Drop-Modulen entfernt.
Lanes ~2/3, Live-Box ~1/3 Breite. Bundle `index-COpkdtd3.js`.
## 2026-07-17 — GUI: Transkript als eigene Scroll-Spalte
Transkript (+ Dialog) aus den Drag&Drop-Modulen herausgelöst: eigene, sticky Spalte rechts neben
Zusammenfassung/Verlauf/ToDos/Sprecher, Höhe auf Viewport begrenzt, scrollt INTERN durch und folgt
dem aktiven Wort im Panel (statt die Seite zu ziehen). Unter 1100 px stapeln die Spalten
(Transkript max. 60 vh). Bundle `index-DQmY8pnQ.js`.
## 2026-07-17 — GUI: Sprecher-Verlauf-Visualisierung + Drag&Drop-Module
Detail-Ansicht ist jetzt modular: fester Kopf (Titel, Player, Status), darunter Blöcke
**Zusammenfassung / Sprecher-Verlauf / ToDos / Sprecher / Transkript** — jeder Block hat einen
Grip-Header (⠿) und lässt sich per Drag & Drop umsortieren; die Reihenfolge liegt in
`localStorage['aime-detail-order']` und gilt für alle Aufnahmen.
**Sprecher-Verlauf** (neu): pro Sprecher eine farbige Spur (sp0–sp3) übereinander mit
Redeabschnitts-Blöcken (Wörter mit ≤ 1,2 s Lücke zusammengefasst), Kupfer-Playhead wandert beim
Abspielen mit, die Spur des gerade Sprechenden leuchtet (Label fett, Punkt pulsiert; respektiert
`prefers-reduced-motion`), Klick in die Spuren springt an die Stelle. Rechts daneben die **Live-Box**:
zeigt Sprecher + Text des aktuellen Redebeitrags. Per Playwright-Screenshots verifiziert
(beide Themes, Playback mit aktiver Spur). Bundle `index-WaE171bv.js`.
## 2026-07-17 — GUI: „Weggezogen"-Bug gefixt + Redesign mit Dark/Light-Switch (`.bak-theme`)
- **Bug:** lief beim ausgewählten Eintrag eine Analyse, feuerte ein 5-s-Timer `openRecording(alteId)` als
**stale closure** — auch nachdem der Nutzer längst eine andere Aufnahme hörte → riss ihn zurück und
stoppte den Player. Fix: Poll bricht bei Auswahl-/Tab-Wechsel ab, prüft während der Analyse NUR den
Status (kein DOM-Rebuild) und baut das Detail genau EINMAL neu, wenn die Ergebnisse da sind — inkl.
Wiederherstellung von Player-Position und Play-Zustand.
- **Redesign:** Themes über CSS-Variablen (`data-theme` auf `<html>`, vor First-Paint aus localStorage/
OS-Einstellung, Toggle ☀️/🌙 im Header). Dunkel = neutrales Anthrazit, Hell = warmes Papier; Akzent =
Kupfer (#e8804c/#c05f2d — Rot bleibt der Aufnahme vorbehalten), Wortmarke mit Wellenform-Glyph,
Pill-Tabs, weichere Karten, sichtbarer Tastatur-Fokus, `prefers-reduced-motion` respektiert.
Sprecherfarben (sp0–sp3) als Variablen mit hell/dunkel abgestimmten Werten.
- Bundle `index-C2xK4eql.js` / `index-ClT74fBX.css`. Dateien: `web/index.html`, `web/src/main.js`,
`web/src/style.css` (Backups `.bak-theme`).
## 2026-07-17 — Hall auf der Gegenseite + verschmolzene Sprecher: Mikro→Mix jetzt GEGATET (`.bak-gatedmix`)
User: Gegenseite klingt „wie in einem schallenden Raum", Diarization macht aus zwei entfernten Personen
eine. **Diagnose per Cepstral-Analyse** auf #71: Gegenseiten-Segment hat einen felsenstabilen Peak bei
**79–80 ms** (22/26 Frames) = deterministische verzögerte Kopie; User-Segment sauber. Ursache: der direkte
`cr_mic2mix`-Loopback (immer offen) legte die Gegenseite doppelt in den Mix — digital (`cr_pc2mix`) UND als
Kopfhörermuschel-Bleed durchs Mikro (~80 ms Latenz). Der Hall verschmiert zusätzlich die TitaNet-Embeddings
→ zwei Remote-Stimmen (gleicher Kanal + gleicher „Raum") rücken über die 0,5-Kosinus-Schwelle zusammen.
- **Fix** (`aime-usb-audio-setup.sh`): direkter `cr_mic2mix`-Loopback ENTFERNT; stattdessen
`pw-link micgate.out → callmix` — dieselbe Gate-Auskopplung wie beim Teams-Selbstecho-/iPhone-Uplink-Fix.
In Sprechpausen des Nutzers ist das Mikro komplett aus der Aufnahme. Fallback ohne Gate = alter Loopback.
- Verifiziert strukturell (`pw-link -l`: callmix ← cr_pc2mix + micgate.out, kein HSMIC-Direktpfad) und
funktional (4 s callrec_mix bei Stille = RMS 0/Peak 0). **Echter Call steht noch aus.**
- **Nebenbei entdeckt:** OOM-Ereignis 17:47 (die fünf 60-s-READY-Timeout-Retries beim ersten Qwen3-Start
luden je 5,3 GB, NPU-Speicher der gekillten Loads wurde nicht schnell genug frei) riss mariadb, callrec
UND pipewire weg — Audio-Topologie war seitdem TOT. Mit dem 180-s-Timeout (Changelog unten) behoben;
Topologie neu aufgebaut. Lehre: nach OOM immer `pw-link -l` prüfen.
## 2026-07-16 — Glossar: Einträge bearbeitbar (`.bak-gedit`)
User-Wunsch: gespeicherte Glossar-Begriffe nachträglich ändern. Neu: `PUT /api/glossary/:id`
(`db.updateGlossary`), GUI-Stift-Button je Eintrag lädt Begriff+Soundalikes ins Formular
(Titel „… bearbeiten", Speichern/Abbrechen), Update statt Neuanlage. Bundle `index-NRK6cGUI.js`.
API-verifiziert (POST→PUT→DELETE). Dateien: `server.js`, `db.js`, `web/src/main.js`.
## 2026-07-16 — rkllm vor Diar/Identify freigeben (TitaNet-OOM) + #71 validiert
- **Bug:** residenter rkllm (5,9 GB) ließ den TitaNet-spk-embed-Helper OOM-sterben („spk beendet" pro
Cluster) → Konsolidierung bekam NULL Centroids → #71 bekam still **48 Roh-Sprecher**. Fix (`server.js`):
`llm.stopServer()` am Anfang von `rediarize()` UND `analyzeRecording()` (Reload ~96 s, egal bei 15-min-Analysen).
- **#71 E2E danach:** Diar 48 → **2 Sprecher** (5 schwache gefoldet), Qwen3-Whole-Pass auf 31,3k Zeichen →
„Steuergestaltung für wachsendes Unternehmen" mit konkreten Zahlen (30 MA, 3 Mio Umsatz, 15-20 %-Ziel,
Kick-off Hamburg, Termin 23.) — alter Titel war generisch, alte Sprecherzahl 28.
- **OFFEN:** `identify … spk beendet` schlägt auch MIT freiem RAM fehl → Voice-Auto-Naming (Voiceprints)
aktuell tot; Teilnehmer erscheinen als „Sprecher N". Separater Bug, noch zu untersuchen (spk.js/Helper).
## 2026-07-16 — ✅ LLM → **Qwen3-4B-Instruct-2507** (Nacht; Gemma-Rollback vorhanden)
Auf User-Wunsch Qwen3-4B selbst konvertiert und im A/B gegen Gemma getestet — **3/3 Prompts klar besser**,
produktiv geschaltet und E2E validiert.
- **Konvertierung:** wiederverwendetes Kit (`rkllm-convert:1.3.0`-Docker neu gebaut), HF `Qwen/Qwen3-4B-Instruct-2507`
(7,6 GB bf16) → `w8a8_g128`/grq, max_context 16384, auf RTX 3090 in ~35 min → 5,3 GB `.rkllm`, pscp aufs Board.
- **A/B (gleiche Prompts wie Gemma-Tests):** Chunk 1 (Com4Tec-Story) in 140 s mit ALLEN Nuancen (50 %-Kappa,
kein Body Leasing, Perinko/Icon); Auto-Chunk 91 s inkl. Detail-Korrektur „Michael Wagner, nicht Fabienne";
**Whole-Pass 21,8k Zeichen in 691 s fehlerfrei gewichtet** — exakt der Fall, in dem Gemma englisch schwafelte
bzw. nur das Ende zusammenfasste. Qwen3 = ChatML-Template statt Gemma-Turns (`rkllm_server.cpp`).
- **Produktiv-Swap:** Drop-in `RKLLM_MODEL=qwen3-4b-w8a8g128-16k.rkllm`, prod `rkllm_server` mit ChatML neu
gebaut (Rollback: `.bak-gemmatmpl` + Drop-in zurückstellen), `WHOLE_MAX_CHARS` 20 000 → **32 000** (bis ~35-min-
Aufnahmen Single-Pass; Sektions-Konglomerat bleibt für längere). **GOTCHA:** Qwen3 lädt ~96 s → llm.js
READY-Timeout 60 s → 180 s (sonst Endlos-Retry-Schleife, real passiert).
- **E2E validiert:** #70 Whole-Pass 15,4 min → „Neue Kontakte und Stellenangebote", Auto raus, Comfort Tech/
Adem Marlock/Nico-Profil-Todo konkret; #68 Kurzclip (analyzeSingle+ChatML) 22,2 min inkl. Queue → sauber.
- Damit ist die 4B-Synthese-Decke aus dem Sektions-Konglomerat-Eintrag (unten) für ≤ 32k Zeichen GESCHLOSSEN;
das Konglomerat bleibt als >32k-Pfad und Fallback.
## 2026-07-16 — A/B-Test Qwen2.5-3B vs Gemma-3-4B im neuen Chunk-Pfad: Gemma bleibt
Auf User-Wunsch das noch vorhandene `qwen2.5-3b.rkllm` gegen die neuen 4,2k-Chunk-Prompts getestet
(Test-Binary mit ctx 4096 in /tmp, direkt übers Stdin-Protokoll — kein App-Umbau). Ergebnis: **3 von 3
Outputs unbrauchbar** (Feldbeschreibungs-Echo ohne Inhalt; Abbruch nach ~100 Zeichen mitten im Satz;
Struktur-Müll mit der Feldbeschreibung als JSON-Key) — Gemma lieferte im selben Job 8/8 verwertbare
Sektionen. Dazu prinzipiell: Qwen2.5 hat fix 4096 Kontext → der validierte Whole-Pass (≤ 20k Zeichen)
wäre unmöglich. Testartefakte entfernt. Offene Alternative (ungetestet, ~½ Tag): Qwen3-4B neu konvertieren.
## 2026-07-16 — Sektions-Konglomerat statt LLM-Reduce (Abend, `.bak-sectionsum`)
Auftrag: Lang-Transkript-Summary (> 20k Zeichen) inhaltlich retten. Prompt-Lab auf #70 mit temporärem
`/api/debug/ask`-Endpoint (nach Abschluss wieder entfernt), ~15 Reduce-/Chunk-Varianten in je ~2 min
statt 9-min-Vollanalysen.
- **Befund 1: Chunk-Größe ist der Fidelity-Hebel.** 8k-Chunks → generisches namenloses Blabla; 4k-Chunks →
geerdete Summaries mit echten Eigennamen (Com4Tec, René, Tobi+Niklas, 20 Bewerbungen). `CHUNK_CHARS` 8000 → **4200**.
- **Befund 2: Cross-Section-Synthese unmöglich auf Gemma-3-4B.** Jede LLM-Reduce-Variante (Gewichtungsregeln,
Abschnitts-Labels, Themen-Tabellen) → Regeln ignoriert, Namen durch Rollen ersetzt, Dubletten, Loops.
**LLM-Reduce abgeschafft.**
- **Neue `analyzeLong`-Pipeline** (`llm.js`): 4,2k-Chunks sequenziell mit **Rolling Context** (1-Satz-
Kurzfassungen der Vorgänger → erkennt geänderte Entscheidungen: „jetzt X, vorher Y") + **eigennamen-
Extraktionsfeld**; Echo-Guard (Feldbeschreibungs-Echo ist INHALTS-abhängig reproduzierbar → bis 3 Versuche
mit variiertem Prompt: ohne Kontext, ohne Hint) + Feld-Salvage per Regex + Prosa-Rettung (≥ 60 Zeichen);
**Titel-Mikro-Call** ohne letzten Abschnitt (Smalltalk-Filter); **Konglomerat baut Code**: „Gesprächsverlauf
in n Teilen — genannte Namen: …" + „Teil i/n: …" chronologisch. Kap. 05 (Mermaid + Marotten aktualisiert).
- **E2E #70 (3 Läufe):** Titel ohne Auto-Smalltalk ✅, alle Abschnitte mit echten Namen ✅, Auto nur noch in
Teil 7 ✅, 12-14 min für 26-min-Aufnahme. Restgrenze: Abschnitts-Summaries variieren pro Lauf in Schärfe
(Sampling), keine thematische Verdichtung ÜBER Abschnitte (4B-Decke). ≤ 20k Zeichen weiter Whole-Pass.
## 2026-07-16 — #70-Debug: Phantom-Sprecher, Summary-Fehlgewichtung, Thermal-Leck (Nachmittag)
Auslöser: Aufnahme #70 (26 min, 4 Personen) — 7 Sprecher erkannt, Titel/Summary aus Randthemen
(Auto-Smalltalk der letzten 3 min statt Recruiting-Hauptteil). 9 Analyse-Iterationen auf #70.
- **Diarization Stufe 3 „Weak-Fold"** (`diar.js`, `.bak-weakfold`): finale Sprecher < `DIAR_MIN_TOTAL_SEC`
(20 s, Segment-Zeiten!) werden in den akustisch nächsten starken Sprecher gefaltet — ein einzelner
≥ 3-s-Streu-Cluster konnte vorher einen Phantom-Anchor eröffnen (#70: 3 Phantome mit 2,5–4,8 s).
10 s reichte NICHT (9,2-s-Phantom kam durch). **Verifiziert: #70 konstant 50 Roh-Cluster → exakt 4.** Kap. 04.
- **Whole-Pass-Grenze = Modellgrenze bewiesen** (Runden 2–5): rkllm-Runtime-Kontext 8192 → **12288**
(`rkllm_server.cpp`, `.bak-ctx12k`, Rebuild) und `WHOLE_MAX_CHARS` testweise 32k → Gemma-3-4B kippt
> ~6k Input-Token reproduzierbar: englisches Geplauder statt Analyse, Recency-Bias (fasst nur das
Gesprächsende zusammen), JSON-Zerfall (typografische Quotes, Wiederholungsschleifen). `WHOLE_MAX_CHARS`
zurück auf **20 000** (dort validiert gut); ctx 12288 bleibt (Headroom). Kap. 05 neuer Abschnitt.
- **Robustheit `llm.js`** (`.bak-ctx12k`): End-Instruktion nach dem Transkript (ohne Platzhalter-Skelett —
Gemma kopiert `...` wörtlich!); JSON-Repair-Call (repariert kaputten Output OHNE das große Transkript);
Sanity-Check (Zusammenfassung ≥ 20 Zeichen) → sonst Chunk-Fallback statt Hard-Error; `extractJson()`
repariert typografische Anführungszeichen + trailing commas; **1 Retry pro Chunk** (Parse = Sampling-
Münzwurf: 3/3 ok vs 1/3 im nächsten Lauf; Retry live bestätigt); Reduce-Input mit „Abschnitt i von n"-
Labels + Gewichtungsregeln (Hauptthema = mehrere Abschnitte; Nur-letzter-Abschnitt = Smalltalk).
- **Regression bestätigt: Chunk-Prompt MUSS minimal bleiben** — eine zusätzliche Anweisungszeile
(Namen/Firmen/Zusagen) kippte 2/3 Chunks in Prosa (Runde 6). Zurückgebaut.
- **Thermal-Leck geschlossen** (`server.js`, `.bak-anacap`): `analyzeRecording()` lief als einziger
schwerer Pfad OHNE A76-Takt-Cap → 95–97 °C bei langer Gemma-Generierung (lüfterlos!). Jetzt Cap wie
`transcribeWav`/`rediarize`. Kap. 03/05.
- **Ehrliches Ergebnis #70:** Sprecher exakt 4 ✅, Analyse crashfrei in ~9 min ✅, Summary führt jetzt mit
Recruiting, ABER Auto-Smalltalk bleibt in Titel/Kernpunkten und konkrete Namen (Com4Tec, Kandidaten)
fehlen — **Qualitätsdecke des 4B-Modells im Chunk-Pfad (> 20k Zeichen)**. Aufnahmen ≤ ~23 min laufen
über den validierten Whole-Pass.
## 2026-07-16
- **Codebase ins Repo aufgenommen:** `app/` = Spiegel von `/opt/callrec` (Backend, Frontend-Quellen, Inferenz-Python, Skripte, `rkllm_server.cpp`), `system/` = systemd-Units + WirePlumber/PipeWire-Drop-ins + sudoers. Modelle/Binaries/Aufnahmen bewusst außen vor (`.gitignore`).
- **Projekt umbenannt in „Aime“** (vormals CallRec, wie der BT-Name des Boards): Gitea-Repo → `sascha/Aime`,
Web-GUI-Titel/Header → „Aime“ (Bundle `index-CuWeUh1j.js`, Backups `index.html.bak-aime`/`main.js.bak-aime`),
Doku rebranded. Technische Bezeichner auf dem Board bleiben `callrec*` (Stabilität).
- **Doku-Repo angelegt** (dieses Repo). Ab jetzt verbindliche Ablage inkl. Diagrammen.
## 2026-07-15/16 — Strukturierte Analyse + Teilnehmer-Fix (`.bak-structsum`)
- Teilnehmer kommen aus der **Diarization** statt vom LLM (das listete alle *erwähnten* Namen → 11 statt 3 bei #69).
- Summary strukturiert: Überblick + „Kernpunkte“ + „Entscheidungen“ als Bullets (`composeSummary`, CSS `pre-line`).
- Chunk-Salvage-JSON-Leak gefixt; Reduce-Input gekappt + Retry; `cleanList` filtert `null`/`[object Object]`/Objekt-Todos.
- Referenz #69 (71,6 min): Analyse ~20 min, Ergebnis sauber. Bundle `index-B2dSaBty.js`.
## 2026-07-15 — LLM-Wechsel auf Gemma-3-4B + Marker-Fix
- Qwen2.5-3B → **Gemma-3-4B-it** (`gemma3-4b-w8a8g128-16k.rkllm`, grouped w8a8; plain w8a8 kaputt, w4a16 nicht unterstützt).
- rkllm-Runtime 1.2.1 → **1.3.0**. `analyzeWhole` (≤ 20k Zeichen) ersetzt Map-Reduce im Mittelbereich.
- Qualität ≈ Gemini, ~3× langsamer. Wort-Highlight-Marker lief ~1 Wort voraus → SYNC_MS=250 (`index-CLero27-.js`).
## 2026-07-14 — Kurzclip-Beschleunigung + Analyse-Timer
- Clips ≤ 30 s über warmen CPU-Whisper-Daemon ohne rkllm-Swap: 10-s-Clip 120 s → **53 s** (`.bak-shortdaemon`).
- GUI zeigt Analysezeit live (`analysis_ms`/`analysis_started_at`, `.bak-anatimer`).
## 2026-07-13 — Diarization stabil + NPU-Meilenstein 2 + iPhone-Repair
- **Greedy-Anchor-Merge** ersetzt Union-Find: derselbe Call ergab vorher 2/3/7 Sprecher, jetzt konstant 27→3 (`.bak-anchor`).
- **Meilenstein 2:** Whisper-turbo-Encoder auf NPU produktiv — RTF 1,51 → 0,61, Temp 90 °C → 59 °C; batched (batch=8) → RTF 0,389.
- iPhone neu gekoppelt, `Trusted=yes` gesetzt (fehlte → keine HFP-Nodes → #58 ohne Gegenseite).
## 2026-07-09 — Meilenstein-1-Snapshot, Auto-Diarization, Glossar, Whisper-Kalibrierung
- Snapshot `/opt/callrec/milestones/m1-20260709/` (Rollback-Punkt).
- Auto-Diarization: 2-stufige Konsolidierung (17→2 verifiziert), `reanalyze` re-diarisiert immer (0=auto / >0=gepinnt).
- **Glossar Phase 1:** deterministische Ganzwort-Ersetzung + LLM-Hinweis, GUI-Tab, „Aime“ geseedet.
- Whisper-Kalibrierung: medium beam1 optimal (11,3 % WER vs large-v3); `--initial-prompt` mit Termliste = Echo-Loop (tabu).
- 46-min-E2E über Nacht sauber (Long-Rec-Fixes produktionsvalidiert).
## 2026-07-08 — Echo-Fix, Thermal-Crashes, Whisper medium, Sprecher-Lernen, Long-Rec-Analyse
- **Selbst-Echo der Gegenseite gelöst:** LADSPA-Noise-Gate `micgate` im Mikro→PC-Pfad (Thr −45 dB, Mic-HW niedrig); AEC verworfen. Reboot-verifiziert.
- **Thermal-Schutzabschaltungen gefixt:** A76-Cap 1,6 GHz über die ganze `transcribeWav` (Whisper+Diar), SIGSTOP-Guard >90 °C; crash-sicheres asynchrones Speichern (`stopAndSave` persistiert vor Transkription); `/retranscribe` für hängende Zeilen.
- Whisper small → **medium** (Hörfehler).
- **Sprecher-Umbenennen + Stimm-Lernen** (Voiceprint-Zentroide) + Leertaste Play/Pause.
- Lange Aufnahmen (> 20 min) analysierbar: Chunked Map-Reduce gegen 4096-Token-Limit; Diar-Timeout skaliert mit Dauer; Lösch-Confirm.
## 2026-07-07 — MEILENSTEIN 1: USB-Headset-Pfad komplett
- Logi H390 ersetzt toten ES8323-Mikrofonpfad; callmix + 4 Loopbacks + `callrec_mix`-Remap; Volume-Bridge; Inline-Tasten.
- Kaltstart-verifiziert. Clip-sicheres Gain-Staging (PC→Mix 60 %) — Übersteuerung hatte Diarization zerstört (14 Sprecher, #41).
- Re-Diarization mit fester Sprecherzahl (`numSpeakers`), #41 14→2 verifiziert.
- LLM-Fixes: Phantom-Todo (Prompt-Beispiel-Echo) entfernt, KV-Cache-Clear, leere Todos → `[]`.
## 2026-07-06
- ES8323-3,5mm-Mikro als defekt diagnostiziert (Verstärkung fest −42,14 dB) → Entscheidung für USB-Headset.
## 2026-07-05 — „Helikopter“-Debugging (Referenz)
- ES8323 S16-Modus = Treiberbug (S24 nutzen); PipeWire/alsaloop-Capture des ES8323 injizierte ~13 Klicks/s,
arecord nicht; Windows-Shared-Mode-AGC = separates 20-Hz-Pumpen (exklusiv sauber).
- Sauberer Fallback damals: dual native-clock arecord. Durch Headset-Pfad obsolet.
## Früher
- iPhone-BT-HFP-Pfad aufgebaut (Rollen-Drop-in, frischer Pair, Echo-Fix Uplink←micgate, Route-Watcher) — Details Kap. 06.
- BT-Buds No-Bonding-Quirk: LinkKey manuell persistiert (Referenz, Dongle entfernt).