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
This commit is contained in:
@@ -3,6 +3,78 @@
|
||||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||
Was, Warum, betroffene Dateien/Backups).
|
||||
|
||||
## 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
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# 09 — Inhaltssuche, Papierkorb, Titel umbenennen
|
||||
|
||||
Drei Funktionen, die am 2026-07-31 dazukamen. Die Suche ist der große Teil, die anderen beiden sind
|
||||
kleine Ergänzungen an derselben Datenschicht.
|
||||
|
||||
## Titel nachträglich umbenennen
|
||||
|
||||
`PATCH /api/recordings/:id/title`. Der Stift steht in der Aufnahmeliste und im Detailkopf.
|
||||
|
||||
Der eigentliche Punkt ist die Spalte **`recordings.title_locked`**: sobald ein Titel von Hand gesetzt
|
||||
wurde, darf die nächste KI-Analyse ihn nicht mehr überschreiben. Ohne diese Sperre hätte ein
|
||||
„↻ Analyse neu" den selbst vergebenen Namen still wieder gegen einen LLM-Vorschlag getauscht.
|
||||
|
||||
```sql
|
||||
-- db.js/updateAnalysis
|
||||
UPDATE recordings SET title = CASE WHEN title_locked=1 THEN title ELSE COALESCE(?,title) END, ...
|
||||
```
|
||||
|
||||
Ein leeres Feld löscht den Titel **und** die Sperre — danach darf die KI wieder benennen.
|
||||
|
||||
## Papierkorb (30 Tage)
|
||||
|
||||
Löschen setzt nur noch `recordings.deleted_at`. Nichts wird angefasst: Wörter, Marker, Passagen und die
|
||||
WAV/MP3-Dateien bleiben liegen, damit eine Wiederherstellung vollständig ist und nicht halb.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Aufnahme sichtbar<br/>deleted_at IS NULL] -->|"🗑 Löschen"| B[Papierkorb<br/>deleted_at = jetzt]
|
||||
B -->|"↩︎ Wiederherstellen"| A
|
||||
B -->|"✕ Endgültig (manuell)"| C[purgeRecording]
|
||||
B -->|"Aufräumlauf: älter als 30 Tage"| C
|
||||
C --> D["DELETE markers + words + passages<br/>DELETE recordings<br/>unlink wav + mp3"]
|
||||
```
|
||||
|
||||
Der Aufräumlauf (`sweepTrash` in `server.js`) startet **beim Boot und danach täglich**. Der Boot-Lauf ist
|
||||
nötig, weil das Board oft tagelang aus ist — ein reiner 24-Stunden-Timer würde das Fenster immer wieder
|
||||
verpassen.
|
||||
|
||||
**Altfehler mitbehoben:** das frühere `deleteRecording` löschte nur `markers` und die `recordings`-Zeile.
|
||||
Die `words`-Zeilen blieben verwaist in der Datenbank zurück und die Audiodateien für immer auf der Karte.
|
||||
`purgeRecording` räumt jetzt beides mit ab.
|
||||
|
||||
Retention über `AIME_TRASH_DAYS` einstellbar (Vorgabe 30).
|
||||
|
||||
## Inhaltssuche
|
||||
|
||||
### Aufbau
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Q["Frage:<br/>„Welches Projekt hat X letzte Woche vorgestellt?“"] --> DR[parseDateRange<br/>deterministisch, kein LLM]
|
||||
Q --> CT[contentTerms<br/>Stoppwörter raus]
|
||||
DR -->|"Filter: 20.–27.07."| SEL
|
||||
CT -->|"Projekt, X, vorgestellt"| SEL
|
||||
|
||||
subgraph SEL["Kandidaten + Bewertung (SQL, Millisekunden)"]
|
||||
FT["Volltext<br/>BOOLEAN = Auswahl<br/>NATURAL LANGUAGE = Wertung (IDF)"]
|
||||
VP["Passage-Embedding<br/>Kosinus, min-max"]
|
||||
VD["Dokument-Embedding<br/>Titel + Zusammenfassung"]
|
||||
end
|
||||
|
||||
SEL --> SC["score = 0,45·ft + 0,25·vec + 0,30·doc<br/>+ Sprecher + Wortgruppe + Aktualität"]
|
||||
SC --> HITS["Trefferliste sofort<br/>Meeting · Sprecher · mm:ss"]
|
||||
HITS --> LLM["Qwen3-4B über die Top-8<br/>~140 s, wird nachgereicht"]
|
||||
LLM --> ANS["Antwort mit Belegen [1] [2]"]
|
||||
```
|
||||
|
||||
Die Zweiteilung ist Absicht: die Trefferliste kommt aus SQL und steht sofort, die formulierte Antwort
|
||||
braucht auf dieser NPU **~140 s**. Erst antworten und dann anzeigen hieße eine Minute Leerlauf.
|
||||
|
||||
### Passagen
|
||||
|
||||
Eine Passage ist die Sucheinheit: 2–4 Sätze **eines** Sprechers, ~420 Zeichen, harte Grenze 700. Sie wird
|
||||
aus den `words`-Zeilen geschnitten, nicht aus `transcript_clean` — nur die Wörter tragen Zeitstempel, und
|
||||
der Zeitstempel ist der ganze Zweck eines Treffers. Ein Sprecherwechsel beendet eine Passage immer.
|
||||
|
||||
Stand 2026-07-31: 22 Aufnahmen → **1689 Passagen** (Neuaufbau in ~1 s ohne, ~9 min mit Embeddings).
|
||||
|
||||
### Warum hybrid — mit Messwerten
|
||||
|
||||
Keine der beiden Ebenen genügt allein, und beide Grenzen sind an diesem Korpus **gemessen**:
|
||||
|
||||
| Frage | Volltext | Semantisch |
|
||||
|---|---|---|
|
||||
| `Steueroptimierung` | **0 Treffer** — im Transkript steht eine andere Wortform | findet #71, `vec=0,92` |
|
||||
| `Wie können wir Steuern sparen` | trifft über „Steuern" | bestätigt #71 |
|
||||
| `OneDrive` | 21 Treffer, exakt | Eigennamen verwischen im Embedding |
|
||||
|
||||
### Bekannte Grenze der semantischen Ebene
|
||||
|
||||
`multilingual-e5-small` trennt auf **verrauschtem ASR-Text** schwach. Gemessen für „Wie werden unsere Daten
|
||||
gesichert" über alle 1689 Passagen:
|
||||
|
||||
```
|
||||
Kosinus-Spanne der Kandidaten: 0,8085 .. 0,8560 (Breite nur 0,047)
|
||||
|
||||
cos=0,8560 #77 „Vielleicht packen wir nochmal eine Agenda zusammen…“ ← irrelevant, Rang 1
|
||||
cos=0,8419 #85 „…jeden Abend um 11 Uhr der Backup-Job startet.“ ← die Antwort, Rang 5
|
||||
```
|
||||
|
||||
Das Embedding selbst rankt also falsch; keine Normalisierung repariert eine falsche Reihenfolge. Grund ist
|
||||
vermutlich, dass die ASR Komposita zerbricht („Backup -Job", „KI -Binnenstrich") — genau das, womit ein
|
||||
Embedding am schlechtesten umgeht.
|
||||
|
||||
Zwei Gegenmaßnahmen sind drin und messbar wirksam, lösen den Fall aber nicht ganz:
|
||||
|
||||
1. **Dokument-Ebene** (`recordings.doc_vec`): Titel + Zusammenfassung sind saubere LLM-Prosa statt
|
||||
ASR-Rauschen. Sagt „diese *Aufnahme* passt zur Frage", auch wenn keine einzelne Passage gut einbettet.
|
||||
2. **IDF-Wertung**: Kandidaten per `BOOLEAN MODE` auswählen, aber mit `NATURAL LANGUAGE MODE` bewerten.
|
||||
Boolean gewichtet nicht nach Seltenheit, weshalb eine Passage, die „Daten" mehrfach wiederholt, die
|
||||
Passage schlug, die „gesichert" tatsächlich beschreibt.
|
||||
|
||||
Ergebnis für den schwierigen Fall: die richtige Passage steht auf **Rang 11 von 296** statt gar nicht — sie
|
||||
ist auffindbar, erreicht aber die Top-8 nicht, die das LLM zu sehen bekommt. **Nächster Hebel wäre
|
||||
`multilingual-e5-base`** (~1,1 GB statt 470 MB, Neuindexierung ~25–30 min).
|
||||
|
||||
### Relative Datumsangaben
|
||||
|
||||
„letzte Woche" wird **deterministisch** aufgelöst, nicht vom LLM: ein falsches Kalenderfenster ist
|
||||
schlimmer als gar kein Filter, und Datumsarithmetik ist genau das, was ein 4B-Modell subtil falsch macht.
|
||||
Wochen laufen Montag–Sonntag. Der erkannte Datumsausdruck wird aus den Suchbegriffen **entfernt** — sonst
|
||||
sucht die Anfrage nach dem Wort „Woche".
|
||||
|
||||
Wenn nur ein Zeitraum und kein Suchbegriff übrig bleibt („Was lief letzte Woche?"), zeigt die Suche die
|
||||
Aufnahmen dieses Zeitraums ab ihren Eröffnungspassagen statt einer leeren Seite.
|
||||
|
||||
### Embedding-Modell
|
||||
|
||||
`/opt/aime-models/e5-small/` — `model.onnx` (470 MB, fp32), `tokenizer.json`, `config.json`, geladen von
|
||||
`intfloat/multilingual-e5-small`. Läuft über `inference/embed_text.py` als Daemon auf onnxruntime/CPU,
|
||||
**`taskset -c 0-3`** (A55), damit die A76-Kerne für whisper und sherpa frei bleiben.
|
||||
|
||||
Bewusst **ohne** sentence-transformers: das zöge torch + transformers (~2 GB) auf eine Karte, auf der
|
||||
bereits whisper, sherpa und ein 5-GB-LLM liegen. `tokenizers` und `onnxruntime` waren für den NPU-Pfad
|
||||
ohnehin schon im venv.
|
||||
|
||||
Die e5-Präfixe `query: ` und `passage: ` sind Pflicht — ohne sie verliert das Modell deutlich an
|
||||
Genauigkeit, weil genau dieses asymmetrische Paar kurze Fragen auf lange Passagen abbildet.
|
||||
|
||||
**Ohne das Modell degradiert die Suche sauber auf reinen Volltext** (`emb.available()` ist dann `false`),
|
||||
sie fällt nicht aus. Das war die Voraussetzung dafür, die Suche vor dem Download in Betrieb zu nehmen.
|
||||
|
||||
### Wann neu indexiert wird
|
||||
|
||||
| Auslöser | Umfang |
|
||||
|---|---|
|
||||
| Analyse einer Aufnahme fertig | diese Aufnahme (nach `relabelAnalysis`, damit die Sprechernamen stehen) |
|
||||
| Sprecher umbenannt | diese Aufnahme (`speaker_name` liegt denormalisiert in `passages`) |
|
||||
| Dienststart | alle Aufnahmen **ohne** Passagen (Nachzügler) |
|
||||
| `POST /api/search/reindex {"force":true}` | alles |
|
||||
|
||||
## API
|
||||
|
||||
| Route | Zweck |
|
||||
|---|---|
|
||||
| `PATCH /api/recordings/:id/title` | Titel setzen/löschen |
|
||||
| `DELETE /api/recordings/:id` | in den Papierkorb |
|
||||
| `GET /api/trash` | Inhalt + Resttage |
|
||||
| `POST /api/trash/:id/restore` | wiederherstellen |
|
||||
| `DELETE /api/trash/:id` | endgültig löschen (inkl. Dateien) |
|
||||
| `GET /api/search?q=` | Trefferliste, sofort |
|
||||
| `POST /api/search/answer` | formulierte Antwort mit Belegen (NPU-Warteschlange) |
|
||||
| `GET /api/search/status` | Passagen, davon eingebettet, Modell da? |
|
||||
| `POST /api/search/reindex` | Index neu bauen (`{"force":true}` = alles) |
|
||||
|
||||
## Stellschrauben
|
||||
|
||||
| Variable | Vorgabe | Wirkung |
|
||||
|---|---|---|
|
||||
| `AIME_TRASH_DAYS` | 30 | Aufbewahrung im Papierkorb |
|
||||
| `AIME_PASSAGE_CHARS` | 420 | Ziellänge einer Passage |
|
||||
| `AIME_SEARCH_TOPK` | 20 | Treffer je Suche |
|
||||
| `AIME_ANSWER_PASSAGES` | 8 | Passagen für die LLM-Antwort (5 ≈ 90 s statt 140 s) |
|
||||
| `AIME_EMB_DIR` | `/opt/aime-models/e5-small` | Embedding-Modell |
|
||||
|
||||
## Dateien
|
||||
|
||||
`app/search.js` (neu), `app/emb.js` (neu), `app/inference/embed_text.py` (neu), `app/db.js`,
|
||||
`app/server.js`, `app/web/src/main.js`, `app/web/src/style.css`.
|
||||
Backups auf dem Board: `.bak-search`.
|
||||
Reference in New Issue
Block a user