68 Commits
Author SHA1 Message Date
SaschaandClaude Opus 5 3a2e8144f7 Drei weitere Kandidaten geprueft, alle drei abgelehnt
Ministral-3-3B: Architektur mistral3 steht nicht in der Liste des
rkllm-Toolkits, dazu FP8-Gewichte -- nie konvertiert. Config auf Llama
umbiegen waere moeglich gewesen, aber ein falsch eingelesenes Modell
haette plausiblen Unsinn produziert.

Phi-4-mini: konvertiert (5,06 GB), laedt aber nicht auf der NPU.
partial_rotary_factor 0.75 ergibt 48 LongRoPE-Faktoren, die Runtime
erwartet starr head_dim/2 = 64. Rockchips "Phi3" meint Phi-3-mini.

SmolLM3-3B: laeuft, in 2 von 3 Faellen schneller als Qwen3, scheitert
aber inhaltlich -- 13 statt 61 Todos, JSON-Schluessel als
Listeneintraege, Englisch-Drift im deutschen Text.

rkllm_server.cpp: Chat-Template kommt jetzt aus der Umgebung
(RKLLM_TMPL_SYSTEM / _USER_PRE / _USER_POST), Defaults = bisheriges
ChatML-Verhalten, mit Qwen3 gegengeprueft. Damit ist der naechste
Kandidat eine Drop-in-Zeile statt einer Neuuebersetzung.

Doku: Changelog 2026-09-29 (4) mit Messwerten und allen Fallstricken,
Kapitel 05 um Architektur-Vorpruefung und Template-Konfiguration
erweitert. Messstrecke: Zeitlimit 45 -> 90 Minuten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017A91sWU5aWrqZ2DWNTLVph
2026-09-29 19:59:04 +02:00
SaschaandClaude Opus 5 9e9958fb01 A/B-Ergebnis: Qwen3.5-4B verliert, Qwen3-4B bleibt produktiv
Qwen3.5-4B ist auf der RK3588-NPU 21-45% langsamer und haelt die
JSON-Vorgabe nicht durch: von 61 extrahierten Todos ueber drei
Gespraeche blieben 2 uebrig, dazu 13 Parse-Fehler mit Wiederholungen.
Die Prosa war nicht schlechter, teils genauer -- ausschlaggebend ist
die Formatdisziplin. Modell-Drop-in zurueckgesetzt, Originalanalysen
per restore wiederhergestellt.

rkllm_server.cpp: neuer Schalter RKLLM_SUPPRESS_THINK=1 haengt einen
leeren Denkblock an das Chat-Template. Ohne ihn antwortet Qwen3.5 mit
Gedankengang statt JSON; RKLLMInput.enable_thinking reicht dafuer
nicht. Ohne die Variable bleibt das Template unveraendert.

Doku: Changelog 2026-09-29 (3) mit Messwerten, Bewertung und der
Einschaetzung zu rknn-llm v1.3.1 (fuer uns wirkungslos, weil wir
keep_history=0 fahren), plus Kapitel 05.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017A91sWU5aWrqZ2DWNTLVph
2026-09-29 17:19:47 +02:00
SaschaandClaude Opus 5 e9ba48224e A/B-Aufbau fuer Qwen3.5-4B: Messstrecke und Konvertierungs-Kit
lab/ab-llm.mjs vergleicht zwei LLMs ueber reanalyze-llm (ohne
Neu-Diarisierung), mit snapshot/restore der Originalanalysen und einem
Report aus Tabelle plus Volltext-Vergleich.

tools/rkllm-qwen35/ konvertiert Qwen3.5-4B auf dem PC mit
rkllm-toolkit 1.3.0 (muss zur Board-Runtime passen), Parameter
identisch zum produktiven Modell: w8a8_g128, 3 NPU-Kerne,
max_context 16384.

Doku: Changelog 2026-09-29 (2) mit Recherchestand, Sicherung und
GOTCHAs, plus neuer Abschnitt "Modellwechsel und A/B-Tests" in
Kapitel 05 samt Mermaid-Ablauf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 10:55:47 +02:00
SaschaandClaude Opus 5 fae5a9e5d0 GUI: Aufnahmedatum und -uhrzeit im Kopf der Detailansicht
Neuer Helfer recStamp(r) formatiert created_at als
"Di., 29.09.2026, 14:23 Uhr" und zeigt die Zeile in der .status-line
vor der Analysezeit. Wer aus der Inhaltssuche, einem Projekt-Pin oder
per Direktlink in die Detailansicht springt, hat in der linken Liste
keine markierte Zeile und sah den Aufnahmezeitpunkt bisher nirgends.

Keine Server-/DB-Änderung: /api/recordings/:id liefert created_at
bereits mit. Doku: Changelog 2026-09-29 + Notiz in Kapitel 03.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 08:29:23 +02:00
SaschaandClaude Opus 5 95fab7b9e4 GUI: Hinweis zum A76-Takt-Cap aus dem Transkriptions-Wartetext entfernt
Der Wartetext bei status=transcribing lautet jetzt nur noch
"Transkription läuft… lange Aufnahmen dauern." Der Klammerzusatz
"(A76-Takt gegen Überhitzung gedrosselt)" ist auf Wunsch des Nutzers
weg; das Takt-Cap selbst bleibt unverändert aktiv.

Doku: Changelog-Eintrag 2026-09-09 + Notiz im Zustandsmodell (Kap. 03),
inklusive Build-GOTCHA (dist/assets gehörte root -> vite EACCES).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 12:23:37 +02:00
SaschaandClaude Opus 5 acb840bd0a Web-GUI auf Port 80 (ohne Portnummer aufrufbar)
Die Unit startet Node als User=callrec (uid 1000) und konnte darum keinen
privilegierten Port binden - daher bisher :3000. Mit
AmbientCapabilities=CAP_NET_BIND_SERVICE bindet der unprivilegierte Prozess
jetzt Port 80 direkt, ohne Reverse-Proxy (nginx haette 20-40 MB RAM gekostet,
die neben dem residenten rkllm nicht frei sind). Kein Code-Eingriff noetig:
server.js liest process.env.PORT, das Frontend nutzt relative API-Pfade.
:3000 ist bewusst tot, alte Lesezeichen muessen angepasst werden.

Dazu: Doku-IPs auf aime.local/192.168.1.48 korrigiert (192.168.1.200 war
veraltet) und die Geisteranzeige-Diagnose vom 26.08. in Kapitel 03 + Changelog
festgehalten (verwaister Browser-Reiter zaehlt ewig weiter, fmt() ohne Stunden).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:53:14 +02:00
SaschaandClaude Opus 5 ec1658ba85 fix(stop): Antwort in 0,04 s statt bis 51 s — MP3-Encode in den Hintergrund
POST /api/record/stop wartete auf den kompletten loudnorm-Encode: gemessen
~1,4 s pro aufgenommener Minute (#114: 35,5 min -> 50,9 s). Sichtbar war
davon nichts als ein ausgegrauter Knopf; der Reiterwechsel baute den Knopf
per render() wieder klickbar auf und der zweite Klick lief in
"not recording", weil stopAndSave() current sofort auf null setzt.

- recorder.js: encodeMp3() ohne await, schreibt <name>.mp3.part und
  benennt erst danach um. MP3_ARGS + "-f mp3" (ffmpeg leitet den Muxer aus
  der Endung ab und scheitert an .part).
- web/src/main.js: state.stopping ueberlebt render() -> Knopf bleibt
  gesperrt, Uhr friert ein, Toast mit geschaetzter Wartezeit, zweiter Klick
  verworfen, "not recording" kein roter Fehler. Player laedt nach, solange
  /media 404 liefert (#mp3wait, 5-s-Retry, Abbruch nach 5 min).
- server.js: sweepOrphans erkennt rec_*.mp3.part.

Gegenprobe: 0,033 s / 0,044 s Stop-Antwort, "mp3 fertig" ~2 s spaeter,
64 kbit CBR / 48 kHz / mono ohne Xing-Tabelle, /media 200.

Doku: Kapitel 03 (neuer Abschnitt + Vorher/Nachher-Sequenzdiagramm),
Changelog 2026-08-20.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 16:11:48 +02:00
Claude bb1711d4c8 docs: Mermaid-Diagramme zur Systemanzeige in Kapitel 07
Sequenzdiagramm des Abfragewegs (GUI-Poll, /api/sysstat, sysstat.js, /proc
und statfs, 60-s-Puffer des recordings-Scans, 2-s-Ticker fuer die CPU-
Differenz) und Flussdiagramm der beiden Rechenwege (Ring = Aufnahmen durch
frei plus Aufnahmen; Ventilator = Drehstufe und Ampel).
2026-08-17 22:50:14 +02:00
Claude c562586216 fix(gui): "CPU-Temperatur" statt Zonenname in Klammern
Die Zeile zeigt nur noch den Wert in Grad. Damit die neue Beschriftung
stimmt, filtert sysstat.js die Zonen enger: nur soc-thermal, bigcore0/1
und littlecore. GPU-, NPU- und Center-Zone konnten vorher den Hoechstwert
stellen, obwohl "CPU" daran steht.

Doku: Kapitel 07 + Changelog.
2026-08-17 22:29:02 +02:00
Claude 6973b46d75 fix(gui): Erklaertext und Partitionszeile aus der Speicherkarte
Endstand: Aufnahmen (GB + Dateizahl) mit Balken, Noch frei, Gesamt fuer
Aufnahmen. /api/sysstat liefert partPct und totalBytes weiter, die GUI
zeigt sie nur nicht mehr an.

Doku: Kapitel 07 + Changelog.
2026-08-17 22:25:55 +02:00
Claude 3c434e20ec fix(gui): Speicherkarte wieder vollstaendig, Zeilen brechen nicht mehr um
Der vermeintlich fehlende Wert war ein Zeilenumbruch: bei "Gesamt fuer
Aufnahmen 27,5 GB" rutschte die Einheit allein in die naechste Zeile.
Balken, Restplatz, Vorratsgroesse, Erklaertext und Partitions-Fusszeile
sind wieder da; .sys-row steht auf white-space: nowrap und die Karte
waechst stattdessen in die Breite (min-width 285px).

Doku: Kapitel 07 + Changelog (inkl. Hinweis, dass WAV-Rohdateien und
Papierkorb-Aufnahmen in der Summe mitzaehlen).
2026-08-17 22:23:06 +02:00
Claude 51c1e0f675 fix(gui): Speicherkarte auf eine Zeile, Prozentzahl immer sichtbar
Klappkarte zeigt nur noch "Aufnahmen: x,y GB - n Dateien"; Balken, Restplatz,
Vorratsgroesse, Erklaertext und Partitions-Fusszeile sind raus.

Die Media-Query, die unter 900 px Fensterbreite die Prozentzahl neben dem
Ring ausblendete, ist entfernt - der Wert steht jetzt in jeder Breite.

Doku: Kapitel 07 + Changelog.
2026-08-17 22:17:42 +02:00
Claude db50c9616d fix(gui): Ventilator-Icon und Speicheranzeige auf Aufnahme-Vorrat
Das erste Icon las sich als Kleeblatt. Jetzt vier gebogene Pinwheel-Fluegel
in einem feststehenden Gehaeusering; nur die Fluegel drehen. Form vorab ohne
Browser geprueft (lab/preview_fan.py rendert die Bezier-Kandidaten als PNG).

Die Prozentzahl misst nicht mehr die Partition, sondern den Aufnahme-Vorrat:
100 % = freier Platz + alle Audiodateien unter recordings/, belegt = diese
Dateien. System, Modelle und node_modules fallen aus Zaehler und Nenner
heraus; der df-Wert steht als Fusszeile in der Klappkarte.

sysstat.js scannt recordings/ per readdir+stat (kein du-Prozess) und puffert
60 s; status() ist dadurch async, die Route laeuft ueber wrap().

Doku: Kapitel 07 + Changelog.
2026-08-17 22:03:17 +02:00
Claude 47288c0479 feat(gui): Systemanzeige in der Kopfzeile (Speicherring + Turbine)
Zwei Symbole zwischen Bluetooth-Tab und Hell/Dunkel-Schalter: ein Ring mit
der Plattenbelegung in Prozent und eine Turbine, deren Drehzahl der CPU-Last
folgt. Beide klappen bei Mauskontakt eine Karte mit Zahlen auf (belegt/frei/
gesamt bzw. CPU, RAM, Temperatur).

Neues Modul sysstat.js liest alles aus /proc bzw. fs.statfsSync, damit die
GUI im Sekundentakt fragen kann, ohne df/top-Prozesse zu starten. CPU-% aus
zwei /proc/stat-Messpunkten mit eigenem 2-s-Ticker, RAM ueber MemAvailable,
Temperatur als waermste SoC-/Core-Zone.

Doku: Kapitel 07 + Changelog.
2026-08-17 21:52:21 +02:00
Claude 0f530b575a feat(analyse): Kontextnotiz zum Gespraech nach dem Stop
Ein Freitextfeld ("Kontext zum Gespraech") in der Detailansicht, nach dem
Stop aufgeklappt. Der Text geht als Hintergrundwissen in die LLM-Analyse:
Personen, Firmen, Anlass und Eigennamen muessen nicht mehr aus dem
fehlerbehafteten ASR-Text erraten werden.

Bewusst EIN Feld statt eines Formulars mit Teilnehmer-/Anlass-/Begriffs-
feldern (Entscheidung des Nutzers): die Box benutzen auch technisch nicht
versierte Menschen. Gefuehrt wird ueber den Platzhaltertext, die Struktur
zieht die Analyse selbst aus der Prosa.

- recordings.context_note/-_at, db.setContextNote/getContextNote
- PATCH /api/recordings/:id/context; Antwort `applies` sagt ehrlich, ob die
  Notiz die laufende Verarbeitung noch erreicht (auto/running/reanalyze)
- analyzeRecording liest die Notiz erst kurz vor dem LLM-Aufruf, damit
  waehrend der minutenlangen Vorstufen Getipptes noch zaehlt
- llm.contextBlock: eigener """-Block mit Verbot, daraus Aussagen, Ergebnisse
  oder Aufgaben abzuleiten (sonst schreibt das 4B-Modell die Notiz als
  Gesagtes zurueck); eingespeist in Single-, Whole- und Sektions-Pfad
- zwei Deckel: 1200 Zeichen fuer Prompts, die die Notiz einmal sehen,
  600 fuer die Chunk-Prompts (dort landet sie in jedem Chunk von 3200)
- GUI-Karte zeichnet sich selbst neu (kein openRecording -> Audio laeuft
  weiter), ungespeicherter Text ueberlebt den Neuaufbau nach der Analyse

Verifiziert an #97: Kontext greift (korrekte Namen, richtiges Thema), die
absichtlich eingelegten Fremdfakten der Testnotiz erscheinen nicht in der
Zusammenfassung. Alte Analyse danach zurueckgeschrieben.

Doku: Kapitel 05 (neuer Abschnitt + Flussdiagramm + Deckel-Tabelle),
Kapitel 03 (Sequenzdiagramm + API), Changelog 2026-08-17.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 16:10:38 +02:00
Sascha 3d628b4b96 feat(projekt): Anheftungen per Drag & Drop zwischen Unterreitern verschieben
Pin-Zeilen in der Ordneransicht sind draggable, die Reiter sind Ablageziele.
Ein Drop schickt PATCH /api/projects/:id/pins/:pinId {tab_id}; movePin schreibt
project_pins.tab_id um. Verschoben wird nur die Referenz.

- Zielebene hat die Aufnahme schon: Quell-Pin loeschen statt an UNIQUE uniq_pin
  scheitern (merged:true)
- Zielreiter serverseitig gegen den Ordner pruefen, sonst 404
- .ptab-n braucht pointer-events:none, sonst flackert die Ablagemarkierung

Verifiziert gegen die laufende App inkl. 404- und Merge-Faellen; Testdaten
zurueckgesetzt. Doku: Kapitel 10 + Changelog.
2026-08-14 14:52:14 +02:00
SaschaandClaude Opus 5 2be938c91b fix(gui): Recorder-Zustand nach Neuladen vom Server holen
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>
2026-08-10 13:18:11 +02:00
Sascha 2228c57522 feat(persons): eine Person, mehrere Namen und mehrere Stimmproben
Die Identitaet war der NAME. Derselbe Mensch, einmal "Sascha" und einmal
"Sascha Wolf" benannt, existierte zweimal und konnte zwei Cluster benennen
-- Aufnahme #101 (34 s, zwei Menschen) zeigte Michael/Sascha/Sprecher 3/
Sascha Wolf. Der Vornamens-Trick in diar.js hat das ueberdeckt, setzte aber
auch "Markus Ufer" und "Markus Witsch" gleich (Kosinus 0,241).

Neu: persons + person_names (Name = PK, gehoert genau einer Person),
voiceprints.person_id. Mehrere Proben pro Person (max. 8) statt eines
laufenden Mittels; bewertet wird die beste Probe. Score UND Abstand rechnen
zwischen Personen -- vorher rissen zwei Abdruecke desselben Menschen (0,928)
die Abstandspruefung, womit die Zwangsbedingung faktisch aus war.
diar.identityKey bildet Marker-Namen und Abdruecke auf p<person_id> ab.

GUI "Stimmen" zeigt Personen mit Namens-Chips: Namen hinzufuegen/entfernen,
Personen zusammenfuehren, Proben verwerfen.

Altbestand geleert (Dump in backup/), weil lab/vpcos.mjs neben echten
Dubletten (Jens ~ Jens Kuppler = 1,000) auch kontaminierte Abdruecke zeigte:
Johannes lag mit 0,912 an Sascha Wolf, aber 0,115 an Johannes Jeckel.

Verifiziert an #101: Sascha Wolf als zweiter Name derselben Person ->
Re-Diarisierung benennt nur noch Michael + Sascha statt drei Namen fuer zwei
Menschen. GUI mit Playwright geprueft, keine JS-Fehler.
2026-08-07 16:18:33 +02:00
Sascha 084401624b lab: vpcos.mjs — Kosinus zwischen gespeicherten Stimmabdruecken messen 2026-08-07 15:56:21 +02:00
Sascha ff285011d2 fix(ram): nur ein grosses Modell resident (Analyse starb im OOM-Killer)
rkllm_server braucht 6,5 GB von 7,74 GB. Blieb daneben ein anderer
Dauerhelfer resident, wurde rkllm beim Laden OOM-getoetet -- bei #101 in
allen 5 Startversuchen, weil das Retry nichts freigab. Ausloeser war der
warme CT2-Whisper-Daemon, den die 2-Sekunden-Aufnahme davor gestartet
hatte. Zweiter Fall: rkllm blieb nach der Analyse fuer immer resident, und
der Suchindex startete e5-small daneben.

gate.js bekommt eine Freigabe-Registry (registerReleaser/freeRam/
killAndWait, verdrahtet in server.js wie setProbe). Vor jedem rkllm-Versuch
und vor dem NPU-Whisper-Lauf werden die anderen Helfer beendet und ihr
Prozessende abgewartet; analyzeRecording gibt rkllm vor dem Suchindex frei.
Alle Helfer starten faul neu.

Verifiziert mit der Abfolge, die scheiterte: 12-s-Clip (CT2-Daemon
resident) -> Analyse #101 laeuft durch (221 s, done), rkllm lud in 22 s,
kein OOM, danach 5,3 GB frei statt 100 MB.
2026-08-07 15:55:31 +02:00
Sascha c5daee25af fix(diar): sherpa abschnittsweise laufen lassen (OOM bei langen Aufnahmen)
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).
2026-08-07 13:50:01 +02:00
schakal00783andClaude Opus 5 0aa6a37325 feat(system): System von der SD-Karte auf die eMMC umgezogen
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>
2026-08-03 01:07:26 +02:00
schakal00783andClaude Opus 5 aa0098ae01 fix: Knoepfe rutschten aus der Zeile; verwaiste Audiodateien raeumen
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>
2026-08-02 23:53:54 +02:00
schakal00783andClaude Opus 5 d9554c590c fix(audio): Wiedergabe-MP3 als CBR ohne Xing-Tabelle -- Wortmarkierung war 28 s voraus
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>
2026-08-02 23:40:29 +02:00
schakal00783andClaude Opus 5 3496b4a2df feat(gui): gruene Nadel bei angehefteter Aufnahme, Logo fuehrt zu den Aufnahmen
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>
2026-08-02 23:14:41 +02:00
schakal00783andClaude Opus 5 d0e02eb976 feat(gui): Ordner-Ebene eines Projekts umbenennbar
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>
2026-08-02 23:11:12 +02:00
schakal00783andClaude Opus 5 772385713e feat(gui): Projektansicht mit Ordnern, Unterreitern und angehefteten Aufnahmen
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>
2026-08-02 23:05:07 +02:00
Sascha 963ce427da fix: Geisterzaehler nach Neustart + rkllm-Leck in /api/search/answer
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.
2026-07-31 15:25:49 +00:00
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
Sascha d5b9733e78 feat(gui+search): Inhaltssuche mit Zeitstempel-Verweis, Papierkorb (30 Tage), Titel umbenennen
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
2026-07-31 13:20:20 +00:00
schakal00783 c09827bf86 fix(iphone-bt): v3 — pw-link -l filtert nicht, v2 machte den Uplink stumm
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.
2026-07-30 15:58:07 +02:00
schakal00783 e1c81bea9a fix(iphone-bt): iPhone-Route-Watcher greift wieder — Gegenseite fehlte in Aufnahme
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.
2026-07-30 15:48:59 +02:00
Sascha 810cee1dba Diarisierung: Marker-Zwangsbedingungen absichern, Voiceprints als cannot-link; LLM: Sektions-Pfad ab 9k Zeichen
#79 verlor einen von vier Sprechern und lieferte eine 1082-Zeichen-Zusammenfassung.

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

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

#79: 4 Sprecher, Zusammenfassung ~5300 Zeichen, 7 Themenbloecke.
Regression: #70 -> 4, #68 -> 2, #74 -> 20 Sprecher unveraendert.
2026-07-29 16:09:30 +02:00
Claude c76f26abaf GUI: alle Tabs nutzen volle Fensterbreite (kein .wide-Sonderfall mehr) 2026-07-28 00:05:07 +02:00
Claude 5044e27f7c Zusammenfassung: keine '(Namen: ...)'-Zeile mehr unter jedem Absatz
Extraktion bleibt als Arbeitsdatum fuer den Rolling Context zwischen den Chunks.
Bestehende Summaries bereinigt (#74 7503 -> 7203 Zeichen).
2026-07-27 23:57:19 +02:00
Claude 448e2b0726 Namen nachtraeglich in fertige Zusammenfassung uebernehmen
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.
2026-07-27 23:52:28 +02:00
Claude 61531fc50a Fix: Voice-Auto-Naming tot (TitaNet bricht bei langen Schnitten ab + vergiftete Promise-Kette)
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)
2026-07-27 17:30:43 +02:00
Claude 48fa6b4f9e Diarization v2: Fenster-Clustering mit gelernter Schwelle + Live-Namensmarker
#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.
2026-07-27 17:00:45 +02:00
schakal00783andClaude Fable 5 432c4b14dd LLM: Sektions-Pfad ab 5 Sprechern gegen Recency-Bias in Meetings
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>
2026-07-17 16:03:18 +02:00
schakal00783andClaude Fable 5 b33b555fa7 GUI: Record-Button in Kopfzeile (Aufnahme-Tab entfernt) + Live-Karte; lame-Stille-Fix
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 02:18:39 +02:00
schakal00783andClaude Fable 5 aef50dda64 GUI: Scrollbalken im Akzent-Kupfer (scrollbar-color + webkit)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 02:04:08 +02:00
schakal00783andClaude Fable 5 2c9015dd7c GUI: Splitter zwischen Liste und Detail (340-680px, persistent)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 02:00:03 +02:00
schakal00783andClaude Fable 5 f7c50b2b6b GUI: Kupfer-Tageszaehler, Wellenform-Favicon, Tab-Reihenfolge (BT ans Ende)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 01:54:43 +02:00
schakal00783andClaude Fable 5 46f8327832 GUI: Aufnahmen-Liste startet mit nur dem neuesten Tag geoeffnet
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 01:49:27 +02:00
schakal00783andClaude Fable 5 d55442a41f GUI: alle Module inkl. Transkript-Panel spaltenuebergreifend verschiebbar
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 01:44:36 +02:00
schakal00783andClaude Fable 5 40602779ce GUI: Sprecher-Modul entfernt; Umbenennen/Lernen via Popover am Spur-Label
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 01:36:03 +02:00
schakal00783andClaude Fable 5 6a51699e90 GUI: Blasen-Oeffnung positionsgenau zum Sprecher (--ax aus Sitzposition)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 01:26:57 +02:00
schakal00783andClaude Fable 5 55f03e7389 GUI: Tisch-Blase fix, Oeffnung wandert zum Sprecher
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 01:21:16 +02:00
schakal00783andClaude Fable 5 1975bfe696 GUI: Sprechblase am Sprecher, Wort-Highlight + internes Mitscrollen + Klick-Seek
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
2026-07-17 01:17:52 +02:00
schakal00783andClaude Fable 5 7586a707ca GUI: Konferenztisch-Ansicht als Alternative zum Transkript
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
2026-07-17 01:10:00 +02:00