Der sherpa-Diarisierungslauf brauchte RAM quadratisch zur Gesprächslänge
(gemessen 349 MB bei 10 min, 1389 MB bei 20 min, 2643 MB bei 30 min). Die
54-min-Aufnahme #99 riss 5,4 GB, wurde OOM-getötet und nahm wegen
OOMPolicy=stop die ganze App mit — in der GUI "Transkription fehlgeschlagen"
und "failed to fetch".
Die v2-Pipeline verwirft sherpas Labels und nutzt nur die Turn-Grenzen, also
laeuft der Pass jetzt in 600-s-Abschnitten (je +-5 s Kontext) und die Grenzen
werden mit Zeit-Offset zusammengesetzt. Zusätzlich OOMPolicy=continue, damit
ein OOM-getöteter Helfer die App nicht mehr stoppt.
Verifiziert an #99: sherpa-Spitze 363 MB, 726 Turns, 8 Sprecher, Analyse
vollstaendig (done/done).
Alles lag auf der SD (System, DB, Aufnahmen, Modelle), die verloetete eMMC lag
ungenutzt daneben. SD-Karten sterben im Dauerbetrieb meist ohne Vorwarnung, und
Aime schreibt bei jeder Aufnahme WAV + MP3 plus DB und Suchindex. Die eMMC meldet
ihren Verschleiss: life_time 0x01, pre_eol 0x01 = unter 10 % verbraucht.
Vorher vollstaendige Sicherung der SD gezogen und verifiziert (94.501 Eintraege =
find / -xdev, SHA-256-Stichprobe).
Drei Erkenntnisse:
1. mkfs.ext4 (e2fsprogs 1.47) setzt orphan_file, was das Incompat-Merkmal
orphan_present mitbringt -- U-Boots ext4-Leser verweigert dann das Lesen.
Vor dem ersten Start entfernt: tune2fs -O ^orphan_file,^metadata_csum_seed.
2. U-Boot liest boot.scr und armbianEnv.txt von der SD, solange sie steckt --
nachgewiesen, indem die SD-Konfiguration testweise wieder auf die SD-Root
zeigte und das Board prompt das alte System hochfuhr.
3. Daher aime-bootsync.service: spiegelt /boot beim Start und beim Herunterfahren
auf die SD. Sonst liefe ein Kernel-Update auseinander (neue Module auf der
eMMC, alter Kernel von der SD). Faellt die SD aus, greift die vollstaendige
eMMC-Bootkette.
Ergebnis: / = mmcblk0p1 (aime-emmc), keine fehlgeschlagene Unit, dieselben fuenf
Dienste aktiv wie vorher, HTTP 200, NPU v0.9.8, Suchindex 1799/1799.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1. Aufnahmenliste: bei schmaler Spalte schob ein langer Titel die Knoepfe aus
der Zeile, der Papierkorb-Knopf war nicht mehr erreichbar. Flex-Kinder haben
min-width: auto, der Textblock schrumpft also nie unter seine Inhaltsbreite.
Mit der dritten Schaltflaeche (Stecknadel) reichte der Platz nicht mehr.
2. 23 sichtbare Aufnahmen = 46 Dateien, im Ordner lagen 152: 106 verwaiste mit
242 MB, Rueckstand des alten deleteRecording, das nur die DB-Zeile entfernte.
Einmalig geraeumt (Gegenprobe: keine der 106 stand in einer Zeile, danach
fehlt keine benoetigte Datei) und sweepOrphans haelt es sauber -- laeuft hinter
sweepTrash beim Start und taeglich, mit 2-h-Altersgrenze, weil eine Aufnahme
erst verschoben und dann in die DB geschrieben wird.
Die Papierkorb-Kette selbst war korrekt, mit einer Wegwerf-Zeile nachgewiesen:
loeschen -> Papierkorb (Dateien bleiben), endgueltig -> Zeile und beide Dateien
weg.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Die Markierung im Transkript stand auf einem Wort, gesprochen wurde eins vier
Zeilen weiter oben (gemessen an #85: 24 s Vorlauf).
Nicht die Ursache -- jeweils per Gegenprobe ausgeschlossen: die Wortzeiten
(Ausschnitte bei 600/1240/3400 s einzeln transkribiert, enthalten exakt die
Wörter der DB), MP3- vs WAV-Zeitachse (identisch), ffmpeg-Seek (trifft),
ontimeupdate-Logik (korrekt).
Ursache war der Sprung im Browser: in einer VBR-Datei gibt es keine feste
Byte-pro-Sekunde-Regel, also nutzt er die Xing-Tabelle -- 100 Stützstellen,
bei 63 Minuten ein 38-s-Raster. Er landet auf der Stützstelle davor, meldet
über currentTime aber die angefragte Zeit. Nachgerechnet: Ziel 20:40 spielt
ab 20:11,5 (-28,5 s).
Fix: -b:a 64k -ar 48000 -ac 1 -write_xing 0 statt -qscale:a 3. Byte-Position
proportional zur Zeit. Die Tabelle muss auch bei CBR weg, sie rastert auf
1/256 der Dateigröße (~15 s pro Stunde, Restfehler bis -6,4 s gemessen). Ohne
sie rechnet der Decoder linear: Restfehler 0,02 s an neun Stichpunkten.
lab/recode-cbr.sh schreibt den Altbestand einmalig um (Quelle WAV, sonst die
MP3 selbst), lab/tocerr.py ist das Messwerkzeug dahinter.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der Anheften-Knopf zeigte bei angehefteter Aufnahme nur die Zahl, die Nadel
blieb rot. Sie ist jetzt gruen (--ok, wie das "KI ✓"-Abzeichen). Dafuer
ersetzt ein SVG mit currentColor (ICON_PIN, gleiches Muster wie ICON_TABLE)
das 📌-Emoji: ein Farb-Emoji rendert immer rot, color greift daran nicht.
Klick auf Wellen-Logo und Schriftzug wechselt auf den Reiter "Aufnahmen".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Der erste Reiter hieß fest "Ordner" und trägt jetzt einen frei wählbaren
Namen. Er steht in der neuen Spalte projects.root_tab_name (NULL = "Ordner")
statt als Zeile in project_tabs: die Ordner-Ebene ist kein echter Reiter --
es gibt sie immer, sie hat keine eigene id und ist nicht löschbar. Eine
eigene Zeile hätte eine id gebraucht, die in project_pins.tab_id landen
müsste, womit die "0 = Ordner-Ebene"-Regel und damit der UNIQUE-Index
gefallen wären.
PATCH /api/projects/:id/tabs/0 fängt die 0 ab und schreibt ins Projekt;
leerer Name setzt auf "Ordner" zurück. Ebenen-Auswahl im Pin-Dialog und die
Liste bestehender Anheftungen zeigen den Namen mit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neuer Reiter neben "Aufnahmen": Projektordner bündeln Aufnahmen zweistufig
(Ordner + Unterreiter), auf beiden Ebenen anheftbar. Anheften geht aus dem
Projekt heraus (Dropdown mit Filter) oder über den 📌-Knopf in der
Aufnahmenliste, der Ordner und Ebene abfragt, neue Ordner anlegt und
bestehende Anheftungen zum Lösen zeigt. Klick auf eine angeheftete Aufnahme
springt in die Aufnahmen-Ansicht.
Ein Pin ist eine Referenz, kein Besitz: die Aufnahme bleibt unverändert in
der Liste und darf in mehreren Ordnern hängen.
project_pins.tab_id ist INT NOT NULL DEFAULT 0 (0 = Ordner-Ebene) statt NULL,
weil MariaDB NULL von UNIQUE-Indizes ausschließt — sonst ließe sich dieselbe
Aufnahme mehrfach auf dieselbe Ebene anheften. So macht INSERT IGNORE das
Anheften idempotent.
purgeRecording räumt project_pins mit ab; getProject blendet Aufnahmen im
Papierkorb aus, ohne deren Pin zu löschen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Aufgefallen beim Nachsehen, warum die GUI bei 29:32 Audio 2h34min Analysezeit zeigte.
1. Ein systemctl restart SIGKILLt alle Kindprozesse, die DB-Zeile blieb aber auf
analysis_status=pending mit gesetztem analysis_started_at. Die GUI zaehlte dadurch
ewig hoch fuer Arbeit, die es nicht mehr gab. db.failStaleJobs() setzt beim Start
pending/transcribing auf error - zum Startzeitpunkt kann kein Job legitim laufen.
2. /api/search/answer lud rkllm und gab es nie frei: 1h48min lang 82,6% RAM belegt,
nur 237 MB verfuegbar. Genau der Zustand, den analyzeRecording und rediarize mit
llm.stopServer() bewusst vermeiden, weil der TitaNet-Helfer daneben an OOM stirbt
(auf #71 seinerzeit 48 Rohsprecher). stopServer() jetzt im finally der Route.
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.
Drei vom Nutzer angeforderte Funktionen.
Titel umbenennen: PATCH /api/recordings/:id/title. Neue Spalte recordings.title_locked,
damit eine spaetere KI-Analyse einen von Hand gesetzten Titel nicht wieder ueberschreibt.
Papierkorb: DELETE setzt nur noch deleted_at, nichts wird angefasst. Wiederherstellen,
endgueltig loeschen, Aufraeumlauf beim Boot und taeglich (Boot-Lauf, weil das Board oft
tagelang aus ist). Behebt nebenbei einen Altfehler: das bisherige Loeschen liess die
words-Zeilen verwaist in der DB und die WAV/MP3-Dateien auf der Karte zurueck.
Inhaltssuche: neue Tabelle passages (1689 Zeilen aus 22 Aufnahmen) mit Sprecher und
Zeitfenster je Passage. Trefferliste kommt aus SQL in Millisekunden, die formulierte
Antwort mit Belegen [1][2] wird nachgereicht (Qwen3-4B, ~140 s) - deshalb zweistufig.
Hybrid: MariaDB FULLTEXT + Passage-Embedding + Dokument-Embedding aus Titel/Zusammenfassung
(multilingual-e5-small, ONNX/CPU, taskset A55). Ohne Modell degradiert die Suche sauber
auf Volltext statt auszufallen.
Gemessene Grenzen sind dokumentiert statt geschoent: e5-small trennt auf verrauschtem
ASR-Text schwach (Kosinus-Spanne aller Passagen nur 0,8085-0,8560; eine irrelevante
Passage schlug die richtige Antwort). Dokument-Ebene und IDF-Wertung heben die richtige
Passage auf Rang 11 von 296, aber nicht in die Top-8, die das LLM liest. Naechster Hebel
waere multilingual-e5-base.
Relative Datumsangaben werden deterministisch aufgeloest, nicht vom LLM - ein falsches
Kalenderfenster waere schlimmer als gar keins.
Neu: app/search.js, app/emb.js, app/inference/embed_text.py, docs/09-suche-und-verwaltung.md
Board-Backups: .bak-search
Testanruf 13:51 loggte nur "-callrec_mix", kein +micgate und kein +far-end: v2 hat
WirePlumbers Link getrennt und dann nichts verlinkt, der Uplink hatte keine Quelle
und die Gegenseite hörte Stille.
Ursache: `pw-link -l <node>` filtert nicht. Mit einem Node-Namen, der nicht als
Port auflösbar ist, kommt der ganze Graph zurück — und der enthält immer
micgate.out und callmix. Also war jedes "ist das schon verlinkt?"-grep wahr und nur
das Trennen lief. Derselbe Ausdruck stand schon in v1, dort aber hinter dem
Einmal-Guard, der nie erreicht wurde.
v3:
- links_of PORT parst die Peers eines exakten Ports aus einem Graph-Dump (awk),
kein grep mehr über die gesamte Ausgabe.
- Erst verlinken, dann trennen: callrec_mix->Uplink fliegt nur raus, wenn
micgate.out am Uplink-Port bestätigt anliegt. Sonst bleibt der WirePlumber-Link
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: " " hätte -n passiert und bei fertig
geroutetem Anruf jede Sekunde eine Journal-Zeile geschrieben.
Getestet: links_of gegen den echten Graph (verlinkt/unverlinkt/nicht existent);
Entscheidungslogik gegen einen synthetischen Mono-HFP-Graph — Default-Zustand,
fertig geroutet (No-op), micgate nicht bestätigt (behält callrec_mix).
Gegenprobe am echten Anruf steht noch aus.
aime-iphone-route.sh v1 hat seit dem Reboot am 29.07. nie gegriffen (0 Zeilen in
journalctl -t aime-iphone-route über den ganzen Boot). Zwei Gründe:
1. Die Portnamen waren hartcodiert auf input_FL/input_FR. HFP/mSBC ist mono, die
Ports heißen playback_MONO/capture_MONO — der grep matchte nie.
2. Der Guard "micgate.out schon verlinkt -> überspringen" war einmal pro Anruf.
WirePlumber legt die HFP-Nodes nicht atomar an, der Uplink erscheint vor der
Gegenseite. War bluez_input in genau diesem Durchlauf noch nicht da, wurde die
Gegenseite für den ganzen Anruf nicht mehr nachverlinkt. Ein erst mitten im
Anruf gesetztes callrec_mix->Uplink wurde ebenso nie entfernt.
Folge: Gegenseite -> callmix fehlte (nicht in der Aufnahme, #80/#81 hatten je nur
1 Sprecher) und der Uplink führte den ungegateten Mix statt micgate.out, also
wieder das Echo von vor dem Fix am 13.07.
v2 ermittelt die Portnamen über pw-link -i/-o und prüft alle drei Links in jedem
Durchlauf einzeln nach. link_all legt genau einen Link pro Ziel-Port an: eine
Mono-Quelle speist beide Kanäle, eine Stereo-Quelle auf einem Mono-HFP-Port sendet
nur FL, weil beide Links in denselben Port summieren würden (+6 dB Uplink).
Gegenprobe am echten Anruf steht noch aus.
#79 verlor einen von vier Sprechern und lieferte eine 1082-Zeichen-Zusammenfassung.
Diarisierung: ein zweiter, falsch verankerter Marker verschmolz Saschas Cluster
in Merles (Kosinus -0,727). must-link prueft jetzt die akustische Aehnlichkeit,
Anker werden nach Trefferqualitaet gewichtet, und enrolled Stimmen wirken als
cannot-link -- damit ist der Box-Besitzer immer ein eigener Sprecher.
LLM: der Whole-Pass komprimierte das erste Thema weg. Sektions-Pfad jetzt ab
9000 Zeichen, kleinere Chunks, woertlicher Kontext aus dem Vorgaenger-Chunk und
ein gezielter Pruef-Aufruf gegen ueberdehnte Einzelaussagen.
#79: 4 Sprecher, Zusammenfassung ~5300 Zeichen, 7 Themenbloecke.
Regression: #70 -> 4, #68 -> 2, #74 -> 20 Sprecher unveraendert.
speaker_labels speichert das Label jedes Clusters im LLM-Text; relabelAnalysis()
ersetzt beim Umbenennen wortgenau in summary/todos/transcript_clean und baut die
Teilnehmerliste neu. #74: keine 'Sprecher N' mehr.
identifySpeakers() embettete den laengsten Lauf ungekuerzt; TitaNet-ONNX bricht
ab 180 s ab (broadcast 12288 by 17998) und riss den spk_embed-Helfer mit. Da
spk.js die Aufrufe ueber eine Promise-Kette serialisiert, schlugen danach ALLE
weiteren Sprecher mit 'spk beendet' fehl.
- ID_CUT_SEC=25s in identifySpeakers
- Kette in spk.js und tdChain in recorder.js failure-isoliert
- pro Name gewinnt nur der beste Kosinus (Fabio war auf 2 Clustern)
#74 (22 Teilnehmer) ergab automatisch 44 Sprecher, dieselbe Stimme mehrfach.
Ursache gemessen: ein Zentroid pro sherpa-Cluster aus max. 3 Schnitten, fester
Kosinus auf rohen Embeddings (Kreuz-Turn-Kosinus ~0,4 = keine Trennschaerfe).
v2: sherpa liefert nur Turn-Grenzen, Identitaet aus 3-s-Fenster-Embeddings,
Mittelwert-Zentrierung, Average-Linkage-AHC, Weak-Fold, Schwelle pro Aufnahme
per 2-Gauss-EM gelernt. Validiert: #74 44->20, #70 ->4, #68 ->2.
Live-Namensmarker: Namen waehrend der Aufnahme tippen; Zeitstempel benennt
spaeter den Cluster und dient als must-/cannot-link-Zwangsbedingung.
Whole-Pass versagte auf #74 (13-Sprecher-Weekly, 31k Zeichen): Summary
enthielt fast nur die letzten Sprecher. analyze() routet labeled
Transkripte mit >=5 Sprechern jetzt in analyzeLong; Sektions-Pfad
liefert Themen-Überschriften, jede Person, Smalltalk-Dämpfung, Todos
mit Verantwortlichem, Überblick-Mikro-Call, Cap 16000. Kap. 05 +
Changelog aktualisiert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Umschalter (3-Personen-Icon <-> Textblatt); Figuren in Sprecherfarben um
abgerundeten Tisch, Sprechblase mit Live-Text zeigt zum Sprecher, alle
anderen schauen hin, Mund animiert. Sitzlayouts 1-6, localStorage-persistent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
- Detail-Poll bricht bei Auswahlwechsel ab, pollt nur Status, EIN Rebuild
bei Fertigstellung mit Player-Restore
- Themes via CSS-Variablen (data-theme, localStorage, OS-Default),
Kupfer-Akzent, Wellenform-Wortmarke, Pill-Tabs, focus-visible,
prefers-reduced-motion
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
Cepstral-Beweis auf #71: stabiler 80ms-Peak nur auf Gegenseiten-Segmenten
= Muschel-Bleed durchs offene Mikro zusaetzlich zum digitalen Pfad.
cr_mic2mix-Loopback entfernt, stattdessen pw-link micgate.out -> callmix
(Fallback ohne Gate bleibt). Verschmierte Embeddings (verschmolzene
Remote-Sprecher) sollten damit ebenfalls weg sein - echter Call offen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
llm.stopServer() am Anfang von rediarize() und analyzeRecording():
spk-embed starb sonst OOM (48 Roh-Sprecher auf #71). E2E #71: 48->2
Sprecher + saubere Whole-Pass-Summary. Offen: identify-spk-Fehler
auch mit freiem RAM (Voice-Auto-Naming tot).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
GUI-Titel/Header auf dem Board ebenfalls umgestellt; technische
Bezeichner (callrec.service, /opt/callrec, DB, CallRecMix) bleiben.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>