Die GUI hielt `recording`/`recStart`/`markers` nur im Browser-State und fragte den Server nie danach. Nach jedem F5 stand der Recorder auf 00:00, obwohl parecord weiterlief — und das Kopfband malte den Start- statt des Stop-Knopfes, die laufende Aufnahme war aus der GUI also nicht mehr beendbar. `rehydrateRecording()` liest nach dem ersten render() `GET /api/health` und stellt Laufzeit, Namensmarker und Live-Karte wieder her; Fehler bleiben still. Nebeneffekt: ein zweites Gerät sieht die laufende Aufnahme ebenfalls. Doku: Kapitel 03 neuer Abschnitt + /api/health in der Endpunkt-Tabelle, Changelog-Eintrag 2026-08-10. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
989 lines
70 KiB
Markdown
989 lines
70 KiB
Markdown
# 08 — Changelog
|
||
|
||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||
Was, Warum, betroffene Dateien/Backups).
|
||
|
||
## 2026-08-10 — Recorder überlebt das Neuladen (GUI holt den Zustand vom Server)
|
||
|
||
**Symptom:** während einer laufenden Aufnahme klickte der Nutzer in eine andere Aufnahme; danach stand der
|
||
Recorder auf `00:00` und zeigte den Start-Knopf — auch nach `F5`. Die Aufnahme schien gestorben.
|
||
|
||
**Sie lief die ganze Zeit.** Gemessen am Board: `parecord` PID 236484, Elternprozess = die Node-App, seit
|
||
10:59:02 UTC; die WAV wuchs um ~32 kB/s (16 kHz mono s16 = korrekt); `GET /api/health` meldete
|
||
`{"recording":true,"startedAt":1786359542932}` samt allen drei Namensmarkern.
|
||
|
||
**Ursache:** `web/src/main.js` hielt `recording`/`recStart`/`markers` ausschließlich im Browser-`state`
|
||
(Zeile 12, Startwert `false`/`0`) und fragte den Server **nie** danach — `grep -rn "api/health" web/src/`
|
||
ergab null Treffer. Jedes Neuladen setzte die GUI also auf „keine Aufnahme" zurück, obwohl `recorder.js`
|
||
weiter aufnahm. Fatal daran ist nicht die falsche Anzeige, sondern dass das Kopfband dann ● statt ■ malt:
|
||
der Stop-Knopf war unerreichbar. Immerhin ohne Datenrisiko — `recorder.js:270`
|
||
`if (current) throw new Error('already recording')` verhindert eine zweite Aufnahme über dieselbe WAV.
|
||
|
||
**Fix:** neue Funktion `rehydrateRecording()` am Ende von `main.js`, aufgerufen direkt nach dem ersten
|
||
`render()`. Sie liest `GET /api/health` und stellt `recording`, `recStart` (= `startedAt`, Server-Epoche)
|
||
und `markers` wieder her, rendert neu und frischt die Aufnahmenliste auf (Live-Karte). Ein Fehler bleibt
|
||
still (kein Toast), damit ein nicht erreichbarer Server nur die Normalansicht bedeutet. Nebeneffekt: ein
|
||
zweites Gerät sieht die laufende Aufnahme jetzt ebenfalls.
|
||
|
||
**Verifiziert:** `vite build` sauber (`dist/assets/index-B3z3XrMo.js`, `api/health` im Bundle enthalten,
|
||
`dist/index.html` verweist darauf). **Kein `systemctl restart`** — nur statische Dateien getauscht, die
|
||
laufende Aufnahme blieb unangetastet (`recording:true` vor und nach dem Build, Marker intakt).
|
||
|
||
Dateien: `app/web/src/main.js` (Board-Backups `src/main.js.bak-rehydrate`, `web/dist.bak-rehydrate`),
|
||
Kapitel 03 (neuer Abschnitt „Der Aufnahmezustand gehört dem Server" + `/api/health` in der Endpunkt-Tabelle).
|
||
|
||
⚠️ **Nebenbefund, nicht behoben:** `enroll-from-recording failed: spk beendet` bei #107 und #85 — TitaNet
|
||
stirbt im ONNX-`Where`-Knoten (`Attempting to broadcast an axis by a dimension other than 1. 12288 by
|
||
30733`), das Stimmabdruck-Lernen aus langen Aufnahmen fällt also aus. Eigener Punkt.
|
||
|
||
## 2026-08-07 (3) — Eine Person darf mehrere Namen haben (und mehrere Stimmproben)
|
||
|
||
**Warum:** die Identität war der NAME. Derselbe Mensch, in einer Aufnahme „Sascha" und in der nächsten
|
||
„Sascha Wolf" genannt, existierte damit zweimal — und beide Einträge konnten je ein Cluster benennen.
|
||
Aufnahme #101 (34 s, **zwei** Menschen) zeigte deshalb `Michael`, `Sascha`, `Sprecher 3`, `Sascha Wolf`.
|
||
`diar.js` hat das mit einem Vornamens-Trick überdeckt, der aber „Markus Ufer" und „Markus Witsch"
|
||
gleichsetzt (Kosinus 0,241 = zwei Menschen).
|
||
|
||
**Neu:** Tabellen `persons` + `person_names` (Name = Primärschlüssel, gehört zu genau einer Person),
|
||
`voiceprints.person_id`. Eine Person darf beliebig viele Namen **und** beliebig viele Stimmproben haben
|
||
(`AIME_MAX_PRINTS`, Standard 8) — bewertet wird sie mit ihrer **besten** Probe statt mit einem laufenden
|
||
Mittel über verschiedene Sessions und Headsets. Score UND Abstand rechnen jetzt zwischen **Personen**;
|
||
vorher rissen zwei Abdrücke desselben Menschen die 0,05-Abstandsprüfung, wodurch die Zwangsbedingung in
|
||
der Diarisierung faktisch abgeschaltet war. Identität überall: `diar.identityKey()` bildet Marker-Namen
|
||
und Abdrücke auf `p<person_id>` ab.
|
||
|
||
**GUI „Stimmen":** Personen statt Abdrücke — Namen als Chips (hinzufügen/entfernen), zwei Personen
|
||
zusammenführen, Proben verwerfen. Ein Name, der schon einer anderen Person gehört, führt beide zusammen.
|
||
|
||
**Altbestand geleert** (Dump `backup/voiceprints-2026-08-07.sql`, 42 Abdrücke): die Messung mit
|
||
`lab/vpcos.mjs` zeigte neben echten Dubletten (`Jens` ~ `Jens Kuppler` = **1,000**) auch kontaminierte
|
||
Einträge — `Johannes` lag mit 0,912 an *Sascha Wolf*, aber mit 0,115 an *Johannes Jeckel*, war also aus
|
||
einem falsch verschmolzenen Cluster gelernt (wie Merle am 2026-07-29). Neu lernen statt reparieren.
|
||
|
||
**Verifiziert an #101:** „Michael" + „Sascha" neu gelernt, „Sascha Wolf" als zweiter Name derselben Person
|
||
→ Re-Diarisierung benennt `{"speaker_00":"Michael","speaker_01":"Sascha"}` (vorher drei Namen für zwei
|
||
Menschen), Log `Cluster 1 = bekannte Stimme "Sascha" (cos 0,917)`. GUI mit Playwright geprüft: keine
|
||
JS-Fehler, zwei Personen, Chips „Sascha" + „Sascha Wolf".
|
||
|
||
⚠️ **Nicht behoben, eigener Punkt:** die Cluster-ANZAHL. #101 bleibt 4 Cluster für 2 Menschen; die beiden
|
||
unbenannten heißen „Sprecher 1/2". Ursache ist die Über-Segmentierung kurzer Clips (Diagnose 2026-07-31,
|
||
Kap. 04) — dafür liegen drei Optionen vor, keine davon ist gewählt.
|
||
|
||
Dateien: `app/db.js`, `app/spk.js`, `app/diar.js`, `app/server.js`, `app/recorder.js`,
|
||
`app/web/src/main.js`, `app/web/src/style.css` (Board-Backups `.bak-persons`), `app/lab/vpcos.mjs`,
|
||
Kapitel [04 – Sprechertrennung](04-diarization.md).
|
||
|
||
## 2026-08-07 (2) — „KI-Analyse fehlgeschlagen" bei einer 34-Sekunden-Aufnahme: zwei Modelle gleichzeitig
|
||
|
||
**Symptom:** Aufnahme #101 (34 s, 2 Personen) — Transkript und Sprecherspuren da, aber
|
||
„KI-Analyse fehlgeschlagen". Also **nicht** dieselbe Ursache wie der Vormittags-Fix: der betraf die
|
||
Länge, dieser die *Reihenfolge*. Gemeinsam ist nur der OOM-Killer.
|
||
|
||
**Ursache 1:** `rkllm_server` braucht 6,5 GB von 7,74 GB. Die 2-Sekunden-Aufnahme #100 unmittelbar davor
|
||
hatte über den Kurz-Clip-Pfad den warmen CT2-Whisper-Daemon (0,9 GB) gestartet, der per Design resident
|
||
bleibt. Beim rkllm-Load war er noch da → OOM-getötet, und zwar in **allen 5 Startversuchen**, weil das
|
||
Retry nichts freigab: `[rkllm] start 5/5 failed: rkllm beendet vor READY`.
|
||
|
||
**Ursache 2:** rkllm blieb nach einer Analyse für immer resident (der Prozess von 10:37 lebte um 13:27
|
||
noch). Der Suchindex am Ende von `analyzeRecording()` startet aber e5-small (1,15 GB) — daneben erschlug
|
||
der Kernel rkllm (13:27:09). Dasselbe Muster war auch der Dauerzustand „nur ~100 MB frei".
|
||
|
||
**Fix:** `gate.js` bekommt eine Freigabe-Registry (`registerReleaser`/`freeRam(keep)`/`killAndWait`,
|
||
verdrahtet in `server.js` wie schon `setProbe`). Wer ein großes Modell lädt, beendet vorher die anderen
|
||
Dauerhelfer und wartet auf ihr Prozessende — vor **jedem** der 5 rkllm-Versuche und vor dem
|
||
NPU-Whisper-Lauf. Dazu gibt `analyzeRecording()` rkllm frei, **bevor** der Suchindex läuft. Alle Helfer
|
||
starten faul neu; verloren geht nur Ladezeit.
|
||
|
||
**Verifiziert** mit genau der Abfolge, die scheiterte: 12-s-Clip (CT2-Daemon resident) → Analyse #101 →
|
||
`ram: für rkllm freigegeben — cpu-transcriber, text-embedder, spk-embedder`, rkllm lud in 22 s, Analyse
|
||
„Auto und Budget" in 221 s fertig (`done`), kein OOM, danach **5,3 GB frei** statt 100 MB.
|
||
|
||
Dateien: `app/gate.js`, `app/llm.js`, `app/server.js`, `app/recorder.js`, `app/spk.js`, `app/emb.js`
|
||
(Board-Backups `.bak-ramfree`), Kapitel [05 – LLM-Analyse](05-llm-analyse.md) (neuer Abschnitt
|
||
„Speicherhaushalt" mit Messtabelle und Diagramm).
|
||
|
||
**Offen, getrennt davon (gemessen, nicht gefixt):** dieselbe Aufnahme zeigt 4 Sprecher statt 2, und die
|
||
Stimmabdruck-Erkennung ist auf ROHEN Kosinus nicht trennscharf — `Sascha` ↔ `Sascha Wolf` (dieselbe
|
||
Person, zwei Einträge) liegt bei 0,928, aber `Sascha Wolf` ↔ `Johannes` (verschiedene Personen) bei
|
||
**0,912** und `Sascha` ↔ `Fabio Haase` bei 0,876. Bei `DIAR_VP_MIN` 0,55 passt damit fast jeder auf
|
||
jeden; eine höhere Schwelle würde nur alles ablehnen. `Jens` ↔ `Jens Kuppler` ist mit Kosinus **1,000**
|
||
sogar eine exakte Dublette. Der Clustering-Teil hat dieses Problem 2026-07-27 durch Mittelwert-Zentrierung
|
||
gelöst — die Abdruck-Erkennung arbeitet noch ohne. Dazu kommt die Über-Segmentierung kurzer Clips
|
||
(diagnostiziert 2026-07-31, Kap. 04).
|
||
|
||
## 2026-08-07 — Lange Aufnahmen scheiterten: sherpa-Diarisierung lief in den OOM-Killer
|
||
|
||
**Symptom:** Aufnahme #99 (53:56) endete mit „Transkription fehlgeschlagen“ (Badge „KI-Fehler“), beim
|
||
Abruf der Analyse zeigte die GUI „failed to fetch“. Verdacht lag auf dem eMMC-Umzug vom 2026-08-03 —
|
||
**war es nicht**: Speicher-Setup (zram 3,9 GB, kein Swapfile), RAM (7,83 GB) und `armbian-zram-config`
|
||
sind vor und nach dem Umzug identisch (alte SD-Root gegengeprüft).
|
||
|
||
**Tatsächliche Ursache** (aus `dmesg`): die Transkription war längst fertig (8666 Wörter, RTF 0,394),
|
||
gestorben ist der sherpa-Lauf danach —
|
||
`Out of memory: Killed process (sherpa-onnx-off) … anon-rss:5463416kB`. Und weil systemd eine Unit per
|
||
Default komplett stoppt, sobald ein Kindprozess OOM-getötet wird (`OOMPolicy=stop`), riss das die ganze
|
||
App mit: `callrec.service: Failed with result 'oom-kill'` → abgebrochene Analyse, leerer Fetch im
|
||
Browser. Warum es früher ging: der RAM-Bedarf wächst **quadratisch** mit der Gesprächslänge — gemessen
|
||
349 MB (10 min), 1389 MB (20 min), 2643 MB (30 min), 5,4 GB (54 min). Bis ~45 min passt es, danach nicht.
|
||
|
||
**Fix 1 (Ursache):** sherpa läuft jetzt in Abschnitten von 600 s (`DIAR_CHUNK_SEC`, `0` = aus), je mit
|
||
±5 s Kontext; die Turn-Grenzen werden mit Zeit-Offset zusammengesetzt. Zulässig, weil die v2-Pipeline
|
||
sherpas Labels ohnehin verwirft und nur die Grenzen nutzt. Speicher bleibt damit dauerhaft auf
|
||
Abschnitts-Niveau und der Lauf ist zusätzlich schneller (quadratische Kosten).
|
||
|
||
**Fix 2 (Schadensbegrenzung):** `OOMPolicy=continue` — ein OOM-getöteter Helfer (sherpa/whisper/rkllm)
|
||
darf die App nicht mehr mitreißen; Diarisierung und Analyse sind im Code bereits nicht-fatal.
|
||
|
||
**Verifiziert an #99** (Re-Transkription + Re-Diarisierung, 2026-08-07): sherpa-Spitze **363 MB** statt
|
||
5,4 GB, 6 Abschnitte (131/103/119/160/158/55 Turns) → 726 Turns → 1932 Fenster → **8 Sprecher**, alle
|
||
6 Live-Marker verankert und alle 8 Sprecher benannt (Fabio Haase, Merle, Dennis, Nico, Lisa, Kai, Kim,
|
||
Artur), Transkript 51.406 Zeichen, Zusammenfassung 15.930 Zeichen, 20 LLM-Abschnitte, Analyse 5073 s,
|
||
189 Passagen indexiert, `status=done` / `analysis_status=done`, kein Neustart, kein OOM, 64,7 °C.
|
||
|
||
Dateien: `app/diar.js` (Board-Backup `diar.js.bak-chunk`),
|
||
`system/etc/systemd/system/callrec.service.d/oom.conf` (neu), Kapitel
|
||
[04 – Sprechertrennung](04-diarization.md).
|
||
|
||
**Nebenbefund (offen):** während der Analyse belegt `rkllm_server` 6,5 GB RSS (NPU-Modell, shared) und
|
||
das System hat nur noch ~100 MB frei — funktioniert, aber ohne Reserve. Wenn wieder etwas OOM-nah
|
||
wird, ist das der Kandidat.
|
||
|
||
## 2026-08-03 — System von der SD-Karte auf die eMMC umgezogen
|
||
|
||
**Warum:** alles lag auf der SD-Karte (System, Datenbank, Aufnahmen, Modelle), die verlötete eMMC lag
|
||
ungenutzt daneben. SD-Karten sind der übliche Ausfallpunkt im Dauerbetrieb und sterben meist ohne
|
||
Vorwarnung; Aime schreibt bei jeder Aufnahme WAV + MP3 plus Datenbank und Suchindex. Die eMMC meldet
|
||
ihren Verschleiß sogar: `life_time 0x01`, `pre_eol_info 0x01` — unter 10 % verbraucht.
|
||
|
||
**Vorher gesichert:** vollständige Sicherung der SD (22,4 GB tar + Bootbereich + SQL-Dump), verifiziert
|
||
über Eintragszahl (94.501 = `find / -xdev`) und SHA-256-Stichprobe.
|
||
|
||
**Ablauf:** eMMC formatiert (die alte Armbian-Karteileiche dort mit einer veralteten `callrec`-Version
|
||
ist damit weg), `rsync -aHAX --one-file-system` in zwei Runden — erste im laufenden Betrieb (28,5 GB in
|
||
7:25 min, 61 MB/s), zweite mit gestoppten Diensten als Delta (4 s). Danach `fstab` und `armbianEnv.txt`
|
||
auf die neue UUID, `e2fsck`, Stichproben, Dateilistenvergleich (Differenz: exakt die zwei Dateien, die
|
||
ich selbst nach dem Sync angelegt hatte).
|
||
|
||
Drei Dinge, die dabei zu lernen waren:
|
||
|
||
1. **`orphan_file` blockiert U-Boot.** `mkfs.ext4` (e2fsprogs 1.47) setzt `orphan_file` und
|
||
`metadata_csum_seed`; ersteres bringt das Incompat-Merkmal `orphan_present` mit, und ein Treiber,
|
||
der es nicht kennt, *muss* das Lesen verweigern. Die funktionierende SD-Partition hatte beides
|
||
nicht. Vor dem ersten Start entfernt: `tune2fs -O ^orphan_file,^metadata_csum_seed`.
|
||
2. **U-Boot liest die Bootdateien von der SD-Karte**, solange sie steckt — nachgewiesen, indem die
|
||
SD-`armbianEnv.txt` testweise wieder auf die SD-Root zeigte: das Board fuhr prompt das alte System
|
||
hoch. Kernel kommt also von der SD, Root von der eMMC.
|
||
3. Deshalb **`aime-bootsync.service`** (neu): spiegelt `/boot` beim Start und beim Herunterfahren auf
|
||
die SD. Ohne das würde ein Kernel-Update auseinanderlaufen — neue Module auf der eMMC, alter Kernel
|
||
von der SD. Fällt die SD aus, findet U-Boot dort nichts und nimmt die vollständige eMMC-Bootkette.
|
||
|
||
**Ergebnis:** `/` = `mmcblk0p1` (`aime-emmc`), 28 GB von 58 GB belegt. Die SD ist im Betrieb nicht
|
||
eingehängt. Keine fehlgeschlagene Unit, dieselben fünf Dienste aktiv wie vor dem Umzug, Aime antwortet
|
||
mit HTTP 200, NPU-Treiber v0.9.8, Suchindex 1799/1799 Passagen, 50,8 °C.
|
||
|
||
Dateien: `app/aime-bootsync.sh` (neu), `system/aime-bootsync.service` (neu), Kapitel
|
||
[01 – Hardware](01-hardware.md) und [07 – Betrieb](07-betrieb.md).
|
||
|
||
## 2026-08-02 (spät) — Knöpfe rutschten aus der Zeile, 242 MB Dateileichen geräumt
|
||
|
||
**1. Zeilenumbruch in der Aufnahmenliste.** Bei schmaler Liste schob ein langer Titel die Knöpfe aus der
|
||
Zeile — der Papierkorb-Knopf der obersten Aufnahme war nicht mehr erreichbar, die Spalte bekam einen
|
||
waagerechten Scrollbalken. Ursache ist die Vorgabe `min-width: auto` für Flex-Kinder: der Textblock
|
||
schrumpft von sich aus nie unter seine Inhaltsbreite. Mit der dritten Schaltfläche (Stecknadel) reichte
|
||
der Platz dann nicht mehr.
|
||
|
||
```css
|
||
.item > .grow { min-width: 0; overflow-wrap: anywhere; }
|
||
.item > .btn { flex: none; }
|
||
```
|
||
|
||
**2. Gelöschte Aufnahmen lagen weiter auf der Karte.** 23 sichtbare Aufnahmen (46 Dateien), aber 152
|
||
Dateien im Ordner: **106 verwaiste mit 242 MB**, Rückstand des alten `deleteRecording`, das nur die
|
||
Datenbankzeile entfernte. Einmalig geräumt (Gegenprobe vorher: keine der 106 Dateien stand in einer
|
||
Zeile, danach: keine benötigte Datei fehlt), und `sweepOrphans` in `server.js` verhindert den Rückfall —
|
||
Details in [09 – Suche & Verwaltung](09-suche-und-verwaltung.md).
|
||
|
||
Die Papierkorb-Kette selbst war in Ordnung und wurde mit einer Wegwerf-Zeile nachgewiesen: löschen →
|
||
Papierkorb, Dateien bleiben; endgültig löschen → `{"files":2}`, Zeile und beide Dateien weg.
|
||
|
||
Dateien: `app/server.js`, `app/db.js` (`allAudioFiles`), `app/web/src/style.css`.
|
||
Board-Backups: `.bak-orphans`.
|
||
|
||
## 2026-08-02 — Wortmarkierung lief dem Ton voraus: VBR-MP3 war schuld
|
||
|
||
**Symptom:** im Transkript war ein Wort markiert, gesprochen wurde aber eins rund vier Zeilen weiter oben.
|
||
Gemessen an #85: markiert „auf" (20:40,3), zu hören „OneDrive" (20:16,3) — **24 s Vorlauf**.
|
||
|
||
**Was es NICHT war** (der Reihe nach ausgeschlossen, jeweils mit Gegenprobe):
|
||
|
||
| Verdacht | Prüfung | Ergebnis |
|
||
|---|---|---|
|
||
| Wortzeiten driften (VAD-Trimming im Batch-Modus) | Ausschnitte bei t = 600/1240/3400 s aus der WAV geschnitten und einzeln transkribiert | dort stehen exakt die Wörter, die die DB führt — **Zeitstempel korrekt** |
|
||
| MP3 und WAV haben verschiedene Zeitachsen | MP3 komplett dekodiert, dieselben Stellen geprüft | identisch |
|
||
| Datei kaputt / ffmpeg-Seek daneben | `ffmpeg -ss` an denselben Stellen | trifft genau |
|
||
| Anzeige-Logik (`ontimeupdate`) | Code gelesen: letztes Wort mit `t_start <= t` | korrekt |
|
||
|
||
**Was es war:** der Sprung im Browser. Die MP3 war VBR, dort gibt es keine feste Byte-pro-Sekunde-Regel,
|
||
also nutzt der Browser die **Xing-Tabelle — 100 Stützstellen, bei 63 Minuten ein Raster von 38 s**. Er
|
||
landet auf der Stützstelle davor, meldet über `currentTime` aber die angefragte Zeit zurück. Nachgerechnet
|
||
(`lab/tocerr.py`, Frames gezählt + Tabelle geparst): Ziel 20:40 → tatsächliche Abspielposition **20:11,5,
|
||
also −28,5 s**. Genau die beobachtete Größenordnung, und der Versatz bleibt bis zum nächsten Sprung stehen.
|
||
|
||
**Fix:** MP3 künftig **CBR mit 64 kbit und ohne Xing-Tabelle**
|
||
(`-b:a 64k -ar 48000 -ac 1 -write_xing 0` statt `-qscale:a 3`). Dann ist die Byte-Position proportional
|
||
zur Zeit und jeder Sprung sitzt. Die Tabelle muss auch bei CBR weg — sie rastert Byte-Positionen auf
|
||
1/256 der Dateigröße, was bei einer Stunde ~15 s sind (Restfehler gemessen: bis −6,4 s). Ohne sie rechnet
|
||
der Decoder linear: **Restfehler 0,02 s = ein halber Frame**, an neun Stichpunkten über die ganze Datei
|
||
geprüft.
|
||
|
||
Dateigröße bleibt praktisch gleich (#85: 32,3 → 30,3 MB), 64 kbit CBR ≈ die bisherige Durchschnittsrate
|
||
von 68 kbit.
|
||
|
||
**Altbestand:** `lab/recode-cbr.sh` schreibt alle vorhandenen Aufnahmen einmalig um — Quelle ist die WAV
|
||
mit derselben Filterkette wie beim Aufnehmen, für die alten Aufnahmen ohne WAV die MP3 selbst (dann ohne
|
||
erneutes `loudnorm`, das steckt dort schon drin). Schreibt erst in eine `.tmp`, vergleicht die Dauer
|
||
(±2 s) und ersetzt erst dann; pausiert oberhalb von 88 °C, weil das Board keinen Lüfter hat.
|
||
Zwei Stolpersteine dabei: `-f mp3` ist Pflicht (aus der Endung `.mp3.tmp` leitet ffmpeg kein Format ab),
|
||
und eine 0-Byte-Altdatei (`rec_1784247236781.mp3`) bleibt als Fehler liegen statt den Lauf abzubrechen.
|
||
|
||
Dateien: `app/recorder.js` (Board-Backup `.bak-cbr`), `app/lab/recode-cbr.sh` (neu),
|
||
`app/lab/tocerr.py` (neu, Messwerkzeug), Kapitel [03 – Aufnahme-Pipeline](03-aufnahme-pipeline.md).
|
||
|
||
## 2026-08-02 (nachgereicht) — Grüne Nadel, Logo führt zurück zu den Aufnahmen
|
||
|
||
**Nadel grün statt rot:** der Anheften-Knopf in der Aufnahmenliste zeigte bei einer angehefteten Aufnahme
|
||
nur die Zahl, die Nadel selbst blieb rot. Sie ist jetzt grün (`--ok`, dieselbe Farbe wie das „KI ✓"-Abzeichen).
|
||
Dafür musste das **📌-Emoji einem SVG mit `currentColor` weichen** (`ICON_PIN`, gleiches Muster wie
|
||
`ICON_TABLE`/`ICON_SHEET`): ein Farb-Emoji rendert die Schriftart immer rot, `color` greift daran nicht.
|
||
|
||
**Logo als Heimweg:** ein Klick auf Wellen-Logo und Schriftzug „Aime" wechselt auf den Reiter „Aufnahmen"
|
||
(`#brandhome` in `render()`), mit Zeiger-Cursor und Hover-Andeutung als Hinweis darauf.
|
||
|
||
Dateien: `app/web/src/main.js`, `app/web/src/style.css`.
|
||
|
||
## 2026-08-02 (nachgereicht) — Ordner-Ebene umbenennbar
|
||
|
||
Der erste Reiter eines Projekts hieß fest „Ordner". Er trägt jetzt einen frei wählbaren Namen, genau wie
|
||
die Unterreiter.
|
||
|
||
Der Name steht in der **neuen Spalte `projects.root_tab_name`** (NULL = „Ordner") und nicht als Zeile in
|
||
`project_tabs`: die Ordner-Ebene ist kein echter Reiter — es gibt sie immer, sie hat keine eigene id und
|
||
lässt sich nicht löschen. Eine Zeile in `project_tabs` hätte für sie eine id gebraucht, die dann in
|
||
`project_pins.tab_id` auftauchen müsste, womit die „0 = Ordner-Ebene"-Regel und mit ihr der
|
||
`UNIQUE`-Index gefallen wären.
|
||
|
||
`PATCH /api/projects/:id/tabs/0` fängt die 0 ab und schreibt ins Projekt; ein leerer Name setzt zurück auf
|
||
„Ordner". Auch die Ebenen-Auswahl im 📌-Dialog und die Liste bestehender Anheftungen zeigen den Namen
|
||
(`listPinsForRecording` per `COALESCE(t.name, p.root_tab_name)`).
|
||
|
||
Dateien: `app/db.js`, `app/server.js`, `app/web/src/main.js`, Kapitel
|
||
[10 – Projektansicht](10-projektansicht.md).
|
||
|
||
## 2026-08-02 — Neuer Reiter „Projektansicht": Ordner, Unterreiter, angeheftete Aufnahmen
|
||
|
||
**Was:** Aufnahmen lassen sich jetzt in Projektordnern bündeln. Ein Ordner („Project X") nimmt direkt
|
||
Aufnahmen auf **und** kann Unterreiter („Projektrelevante Aufzeichnungen") enthalten, die ihrerseits
|
||
Aufnahmen aufnehmen. Ein Klick auf eine angeheftete Aufnahme springt in den Reiter „Aufnahmen" und öffnet
|
||
sie dort. Angeheftet wird auf zwei Wegen: aus dem Projekt heraus per Dropdown (mit Filterfeld) oder direkt
|
||
in der Aufnahmenliste über den neuen 📌-Knopf, der Ordner **und** Ebene abfragt, neue Ordner anlegen kann
|
||
und bestehende Anheftungen der Aufnahme zum Lösen anzeigt.
|
||
|
||
**Warum:** Die chronologische Aufnahmenliste beantwortet „wann war das", nicht „was gehört zu diesem
|
||
Projekt". Reihenfolge nach Datum ist für die tägliche Arbeit an mehreren Projekten die falsche Sortierung.
|
||
|
||
**Wie:** Drei Tabellen (`projects`, `project_tabs`, `project_pins`), angelegt in `ensureSchema` wie alle
|
||
anderen auch. Ein Pin ist eine reine Referenz — die Aufnahme selbst wird nie verschoben oder kopiert und
|
||
darf in mehreren Ordnern hängen.
|
||
|
||
Zwei Details, die sonst später weh getan hätten:
|
||
|
||
- `project_pins.tab_id` ist **`INT NOT NULL DEFAULT 0`** (0 = Ordner-Ebene), nicht `NULL`. MariaDB
|
||
schließt `NULL` von `UNIQUE`-Indizes aus; mit `NULL` ließe sich dieselbe Aufnahme mehrfach auf dieselbe
|
||
Ebene anheften. Mit `0` greift `UNIQUE (project_id, tab_id, recording_id)`, und `INSERT IGNORE` macht
|
||
das Anheften idempotent.
|
||
- `db.purgeRecording` räumt `project_pins` mit ab, und `getProject` blendet Aufnahmen im Papierkorb aus,
|
||
ohne ihren Pin zu löschen — nach dem Wiederherstellen ist die Aufnahme wieder im Ordner.
|
||
|
||
Dateien: `app/db.js`, `app/server.js`, `app/web/src/main.js`, `app/web/src/style.css`, neues Kapitel
|
||
[10 – Projektansicht](10-projektansicht.md). Board-Backups: `.bak-projects`.
|
||
|
||
## 2026-07-31 (spät) — Zwei Fehler aus dem Suche-Feature behoben
|
||
|
||
Beide beim Nachsehen aufgefallen, warum die GUI bei einer 29:32-Aufnahme **2 h 34 min Analysezeit** anzeigte.
|
||
|
||
### 1. Geisterzähler nach Dienst-Neustart
|
||
|
||
Ein `systemctl restart` SIGKILLt alle Kindprozesse (whisper, spk_embed, rkllm). Die Datenbankzeile blieb
|
||
aber auf `analysis_status='pending'` mit gesetztem `analysis_started_at` stehen — die GUI zeigte darauf
|
||
„KI-Analyse läuft… (NPU)" und einen Zähler, der ewig hochlief, für Arbeit die es nicht mehr gab.
|
||
|
||
Ausgelöst hatte es ein Deploy-Neustart um 13:11 mitten in der laufenden Analyse von #87.
|
||
|
||
**Fix `db.failStaleJobs()`, aufgerufen beim Start:** `pending` → `error`, `transcribing` → `error`. Zum
|
||
Startzeitpunkt kann kein Job legitim laufen, also ist das eindeutig. Der Nutzer sieht jetzt den
|
||
Wiederholen-Knopf statt eines Phantomzählers.
|
||
|
||
### 2. `/api/search/answer` ließ rkllm resident
|
||
|
||
Die Antwortroute lud rkllm über `llm.askRaw()` und gab es nie frei. Gemessen: **1 h 48 min lang 82,6 % RAM
|
||
belegt, nur 237 MB verfügbar.**
|
||
|
||
Das ist genau der Zustand, den `analyzeRecording` und `rediarize` mit ihrem `llm.stopServer()` bewusst
|
||
vermeiden — der TitaNet-Helfer für die Sprecher-Embeddings stirbt neben dem residenten 5,9-GB-Modell an
|
||
OOM (auf #71 ergab das seinerzeit 48 Rohsprecher). Die nächste Transkription wäre also mit hoher
|
||
Wahrscheinlichkeit fehlgeschlagen.
|
||
|
||
**Fix:** `llm.stopServer()` im `finally` der Route. Danach wieder 7,3 GB verfügbar.
|
||
|
||
Dateien: `app/server.js`, `app/db.js`. Board-Backups: `.bak-stalejobs`.
|
||
|
||
## 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).
|