Commit Graph
10 Commits
Author SHA1 Message Date
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
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
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