docs: komplette CallRec-Dokumentation mit Mermaid-Diagrammen
Architektur, Hardware/Thermik, PipeWire-Audio-Topologie, Aufnahme-Pipeline, Diarization (Anchor-Merge), LLM-Analyse (Gemma-3-4B/NPU), iPhone-BT-HFP, Betrieb/Troubleshooting und Changelog-Chronik. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,61 @@
|
|||||||
|
# CallRec — Dokumentation
|
||||||
|
|
||||||
|
Selbst gehostete Anruf-Aufnahme- und Analyse-Appliance auf einem **Youyeetoo R1 (RK3588S)** Einplatinenrechner.
|
||||||
|
Nimmt Gespräche auf (PC/Teams über USB-Gadget **und** iPhone über Bluetooth-HFP), transkribiert mit Whisper
|
||||||
|
(Encoder auf der NPU), trennt Sprecher akustisch (Diarization + Stimm-Profile) und erzeugt eine strukturierte
|
||||||
|
KI-Zusammenfassung (Gemma-3-4B auf der NPU) — **komplett offline**, keine Cloud.
|
||||||
|
|
||||||
|
## Inhaltsverzeichnis
|
||||||
|
|
||||||
|
| Dokument | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| [01 – Hardware](docs/01-hardware.md) | Board, Audio-Hardware, USB-Gadget, Thermik, NPU |
|
||||||
|
| [02 – Audio-Topologie](docs/02-audio-topologie.md) | PipeWire-Graph: callmix, Loopbacks, Noise-Gate, Volume-Bridges |
|
||||||
|
| [03 – Aufnahme-Pipeline](docs/03-aufnahme-pipeline.md) | Aufnahme → Transkription → Diarization → LLM → Datenbank |
|
||||||
|
| [04 – Sprechertrennung](docs/04-diarization.md) | Diarization, Anchor-Merge-Konsolidierung, Stimm-Profile |
|
||||||
|
| [05 – LLM-Analyse](docs/05-llm-analyse.md) | Gemma-3-4B/NPU, Analyse-Pfade, strukturierte Zusammenfassung, Glossar |
|
||||||
|
| [06 – iPhone-Bluetooth](docs/06-iphone-bt.md) | HFP-Pfad, Routing-Watcher, Echo-Fix |
|
||||||
|
| [07 – Betrieb](docs/07-betrieb.md) | Services, Zugänge, Backups, Troubleshooting |
|
||||||
|
| [08 – Changelog](docs/08-changelog.md) | Chronik aller Entwicklungsschritte |
|
||||||
|
|
||||||
|
## System-Überblick
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Quellen
|
||||||
|
PC["Windows-PC<br/>(Teams, Medien)"]
|
||||||
|
IPHONE["iPhone<br/>(Telefonate)"]
|
||||||
|
USER(("Nutzer<br/>Logi H390 Headset"))
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph R1["Youyeetoo R1 (RK3588S, Armbian, lüfterlos)"]
|
||||||
|
PW["PipeWire<br/>Audio-Routing<br/>(callmix + micgate)"]
|
||||||
|
APP["CallRec Web-App<br/>Node.js :3000"]
|
||||||
|
WH["Whisper large-v3-turbo<br/>Encoder auf NPU"]
|
||||||
|
DIAR["Diarization<br/>sherpa-onnx + TitaNet"]
|
||||||
|
LLM["Gemma-3-4B<br/>rkllm auf NPU"]
|
||||||
|
DB[("MariaDB<br/>callrec")]
|
||||||
|
end
|
||||||
|
|
||||||
|
PC -- "USB-C (UAC2-Gadget)" --> PW
|
||||||
|
IPHONE -- "Bluetooth HFP (AX210)" --> PW
|
||||||
|
USER -- "USB-A" --> PW
|
||||||
|
PW -- "callrec_mix" --> APP
|
||||||
|
APP --> WH --> DIAR --> LLM --> DB
|
||||||
|
APP --- DB
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Facts
|
||||||
|
|
||||||
|
- **Board:** `192.168.1.200` (WLAN, primär) / `192.168.1.50` (LAN), Login `root`
|
||||||
|
- **Web-GUI:** `http://192.168.1.200:3000` — Aufnahme, Transkript, Sprecher-Editor, Glossar
|
||||||
|
- **App-Verzeichnis:** `/opt/callrec` (Node.js, systemd `callrec.service`)
|
||||||
|
- **Harte Anforderungen:** immer offline (kein Cloud-LLM), akustische Sprecher-ID, kein aktiver Lüfter
|
||||||
|
- Details und alle Zugangsdaten: [07 – Betrieb](docs/07-betrieb.md)
|
||||||
|
|
||||||
|
## Doku-Regeln
|
||||||
|
|
||||||
|
- Diese Doku ist die **verbindliche Ablage** für alle zukünftigen Änderungen am System.
|
||||||
|
- Jede Änderung: betroffenes Kapitel aktualisieren **und** Eintrag in [08 – Changelog](docs/08-changelog.md).
|
||||||
|
- Prozesse/Abläufe immer mit **Mermaid-Diagramm** dokumentieren (Gitea rendert sie direkt).
|
||||||
|
- Auf dem Board bleiben `.bak-*`-Dateien als Rollback-Punkte; Meilenstein-Snapshots unter `/opt/callrec/milestones/`.
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# 01 — Hardware
|
||||||
|
|
||||||
|
## Board: Youyeetoo R1
|
||||||
|
|
||||||
|
| Komponente | Detail |
|
||||||
|
|---|---|
|
||||||
|
| SoC | Rockchip **RK3588S** — 4× Cortex-A76 (max 2,256 GHz) + 4× A55, **NPU 6 TOPS** (3 Kerne) |
|
||||||
|
| RAM | 8 GB |
|
||||||
|
| OS | Armbian, Kernel 6.1.115, auf SD + eMMC |
|
||||||
|
| Netz | WLAN `192.168.1.200` (primär, auto-join) / LAN `192.168.1.50` |
|
||||||
|
| Funk | **Intel AX210 M.2**: BT über USB (`hci0`, Name **"Aime"**) + WiFi über PCIe (`wlP4p65s0`). Alter RTL8821CU-USB-Dongle entfernt. |
|
||||||
|
| Kühlung | **Lüfterlos** (harte Anforderung). Lüfter am roten POWER-Port war unregelbar (Konstantspannung) und wurde entfernt; echter PWM-Header (FAN 2-Pin, hwmon7) bleibt ungenutzt. |
|
||||||
|
|
||||||
|
## Audio-Hardware
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph PC["Windows-PC"]
|
||||||
|
TEAMS["Teams / Medien-Wiedergabe"]
|
||||||
|
end
|
||||||
|
subgraph BOARD["Youyeetoo R1"]
|
||||||
|
UAC2["USB-C: UAC2-Audio-Gadget<br/>(Board erscheint am PC als Headset)"]
|
||||||
|
H390["USB-A: Logitech H390 Headset<br/>ALSA card3, 046d:0a8f"]
|
||||||
|
ES(("ES8323 3,5mm<br/>DEFEKT"))
|
||||||
|
AX["AX210 Bluetooth<br/>'Aime' = BT-Headset fürs iPhone"]
|
||||||
|
end
|
||||||
|
IPHONE["iPhone 'Skynet16'"]
|
||||||
|
|
||||||
|
TEAMS <--> UAC2
|
||||||
|
IPHONE <-->|"HFP/eSCO (mSBC)"| AX
|
||||||
|
H390 --- BOARD
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Logitech H390 USB-Headset** = Mikrofon + Hörer des Nutzers. Ersetzt den **defekten ES8323**-3,5mm-Pfad
|
||||||
|
(Mikrofonverstärkung stand fest auf −42,14 dB, Treiberdefekt; S16-Modus zusätzlich verbuggt → Umstieg 2026-07-07).
|
||||||
|
- **USB-C zum PC = UAC2-Gadget:** Der PC sieht das Board als USB-Headset. PC-Audio kommt als ALSA-Capture
|
||||||
|
`alsa_input.platform-fc000000.usb...` ins Board; Richtung Board→PC (`alsa_output.platform-fc000000.usb`) hört die Gegenseite den Nutzer.
|
||||||
|
- **Headset-Inline-Tasten** (Vol +/−, Mute) = HID-Events auf `/dev/input/event5` → Daemon `aime-headset-keys.service` → `pactl`-Sink-Volume.
|
||||||
|
- **Windows-Lautstärkeregler** wirkt nicht auf den Datenstrom, sondern kommt als UAC2-Mixer-Wert (`amixer -c UAC2Gadget numid=7`) →
|
||||||
|
`aime-volbridge-headset.sh` pollt und setzt die Headset-Sink-Lautstärke.
|
||||||
|
|
||||||
|
## NPU-Nutzung
|
||||||
|
|
||||||
|
| Aufgabe | Engine | Modell |
|
||||||
|
|---|---|---|
|
||||||
|
| Whisper-**Encoder** | RKNN (NPU) | `turbo-encoder-fp16.rknn` (selbst konvertiert, Kosinus-Ähnlichkeit 0,9998 zur CPU-Referenz) |
|
||||||
|
| LLM-Analyse | rkllm 1.3.0 (NPU) | `gemma3-4b-w8a8g128-16k.rkllm` |
|
||||||
|
| Diarization | CPU (sherpa-onnx) | bewusst CPU: RTF 0,194 ≈ NPU-Referenzwert |
|
||||||
|
|
||||||
|
## Thermik (lüfterlos!)
|
||||||
|
|
||||||
|
Whisper/Diarization auf allen A76-Kernen bei Volltakt → `bigcore critical temp` → **Hardware-Schutzabschaltung**
|
||||||
|
(zweimal passiert, Aufnahmen schienen „verschwunden“). Gegenmaßnahmen:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
START["transcribeWav() startet"] --> CAP["A76-Takt kappen<br/>2256 → 1608 MHz<br/>(aime-cpucap.sh, sudoers NOPASSWD)"]
|
||||||
|
CAP --> WORK["Whisper + Diarization<br/>(~72 °C statt >90 °C)"]
|
||||||
|
WORK --> GUARD{"Temp > 90 °C?"}
|
||||||
|
GUARD -- ja --> STOPP["SIGSTOP Whisper<br/>bis < 78 °C, dann SIGCONT"]
|
||||||
|
GUARD -- nein --> WORK
|
||||||
|
WORK --> FIN["finally: Takt zurück auf 2256 MHz"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Cap umschließt **die ganze** `transcribeWav` (Whisper **und** Diarization) — Absturz #2 kam, weil die Diarization nach Whisper wieder mit Volltakt lief.
|
||||||
|
- 1,8 GHz wurde getestet und **verworfen** (~90 °C, Guard-Flattern). 1,6 GHz ≈ 72 °C stabil.
|
||||||
|
- Seit Whisper-Encoder auf NPU: Transkription 90 °C → **59 °C**; Cap kostet nur noch ~7 % Gesamtzeit.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# 02 — Audio-Topologie (PipeWire)
|
||||||
|
|
||||||
|
Herzstück: virtueller Misch-Bus **`callmix`** (Null-Sink). Alles, was aufgenommen werden soll, wird per
|
||||||
|
Loopback dorthin geroutet; der Monitor von `callmix` wird als echte Quelle **`callrec_mix`** („CallRecMix“)
|
||||||
|
re-mappt — die wählt die Web-GUI als Aufnahmequelle.
|
||||||
|
|
||||||
|
## Gesamtgraph
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
PCIN["PC-Audio (Capture des UAC2-Gadgets)<br/>alsa_input.platform-fc000000.usb"]
|
||||||
|
MIC["Headset-Mikrofon H390<br/>alsa_input.usb-Logi...mono-fallback"]
|
||||||
|
HS["Headset-Hörer H390<br/>alsa_output.usb-Logi...analog-stereo"]
|
||||||
|
GADGET["Gadget-Sink (Board→PC)<br/>alsa_output.platform-fc000000.usb"]
|
||||||
|
GATE["micgate<br/>LADSPA Noise-Gate<br/>(swh gate_1410, Thr −45 dB)"]
|
||||||
|
MIX[("callmix<br/>Null-Sink")]
|
||||||
|
CRMIX["callrec_mix ('CallRecMix')<br/>remap von callmix.monitor"]
|
||||||
|
APP["CallRec-App<br/>Aufnahme"]
|
||||||
|
|
||||||
|
PCIN -- "cr_pc2hs (100%)" --> HS
|
||||||
|
PCIN -- "cr_pc2mix (60% ≈ −13 dB)" --> MIX
|
||||||
|
MIC -- "cr_mic2mix (100%)" --> MIX
|
||||||
|
MIC -- "cr_mic2pc (100%)" --> GATE --> GADGET
|
||||||
|
MIX -.->|"monitor"| CRMIX --> APP
|
||||||
|
```
|
||||||
|
|
||||||
|
Vier Loopbacks, jeder per `media.name`-Tag identifizierbar (robust gegen Index-Verschiebung), Gains setzt
|
||||||
|
`si_vol()` im Setup-Skript `/opt/callrec/aime-usb-audio-setup.sh`:
|
||||||
|
|
||||||
|
| Loopback | Weg | Gain | Zweck |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `cr_pc2hs` | PC → Headset | 100 % | Nutzer hört PC/Teams |
|
||||||
|
| `cr_pc2mix` | PC → callmix | **60 % (≈ −13 dB)** | Gegenseite in die Aufnahme (clip-sicher) |
|
||||||
|
| `cr_mic2mix` | Mikro → callmix | 100 % | Eigene Stimme in die Aufnahme |
|
||||||
|
| `cr_mic2pc` | Mikro → micgate → Gadget | 100 % | Gegenseite hört den Nutzer |
|
||||||
|
|
||||||
|
## Warum Noise-Gate (micgate)?
|
||||||
|
|
||||||
|
Teams-Gegenseite hörte sich selbst verzögert (Selbst-Echo). Nachgewiesen: **akustisches Übersprechen ins
|
||||||
|
Mikrofon** (nicht elektrisch, nicht Ohrmuschel — Mute-Tests), und Teams-AGC renormalisiert jede Pegelabsenkung.
|
||||||
|
Lösung: LADSPA-Gate im Mikro→PC-Pfad schließt in Sprechpausen → Gegenseite bekommt Stille → kein Echo.
|
||||||
|
AEC wurde getestet und **verworfen** (Knistern, kein Nutzen).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
M["Mikrofon<br/>HW-Gain fest Stufe 12 (+1,4 dB)"] --> G{"Pegel > −45 dB?"}
|
||||||
|
G -- "ja (Sprache)" --> OPEN["Gate offen → Signal zum PC"]
|
||||||
|
G -- "nein (Pause)" --> CLOSED["Gate zu → Stille zum PC<br/>= kein Selbst-Echo"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Gotchas (teuer bezahlt):**
|
||||||
|
- Filter-Chain ist ein **Core-Modul**, kein Pulse-Modul → nicht per `pactl load-module`, sondern Drop-in
|
||||||
|
`filter-chain.conf.d/micgate.conf` + eigener Dienst `aime-micgate.service`.
|
||||||
|
- PipeWire liefert einen eigenen preset-aktivierten `filter-chain.service` mit → **doppeltes** micgate →
|
||||||
|
`systemctl --user mask filter-chain.service`.
|
||||||
|
- Mikro-HW-Gain **nur** über `pactl set-source-volume $HSMIC 62%` setzen (= HW-Stufe 12). `amixer` direkt
|
||||||
|
wird von WirePlumber überschrieben; 100 % = +11 dB → Grundrauschen öffnet das Gate wieder.
|
||||||
|
- 2-Mono-Node-Stereo-Graph im Filter-Chain = kein Audio → 1 Mono-Node verwenden.
|
||||||
|
- `pkill -f "pipewire -c filter-chain.conf"` killt die eigene SSH-Session (exit 128) → `setsid` nutzen.
|
||||||
|
|
||||||
|
## Merksätze
|
||||||
|
|
||||||
|
- **PC-Audio = die Quelle `alsa_input.platform-fc000000.usb...`**, NICHT deren `.monitor` (der ist stumm —
|
||||||
|
hat einmal eine Stunde gekostet). Quellen vor Verwendung mit `parecord` + `ffmpeg astats` auf Signal prüfen.
|
||||||
|
- Die GUI filtert alle `*.monitor`-Quellen aus → deshalb der Remap auf `callrec_mix`.
|
||||||
|
- Gain-Staging ist clip-kritisch: Übersteuerung (0 dBFS) hat einmal die Diarization-Embeddings zerstört
|
||||||
|
→ 14 „Sprecher“ bei einem 2-Personen-Gespräch (Aufnahme #41, bleibt dauerhaft geclippt).
|
||||||
|
|
||||||
|
## Persistenz
|
||||||
|
|
||||||
|
| Einheit | Aufgabe |
|
||||||
|
|---|---|
|
||||||
|
| `aime-usb-audio.service` (user) | baut die komplette Topologie beim Boot (`aime-usb-audio-setup.sh`); wartet auf micgate, Fallback direkt auf Gadget-Sink |
|
||||||
|
| `aime-micgate.service` (user, enabled) | startet die micgate-Filter-Chain |
|
||||||
|
| `aime-volume-headset.service` (user) | Windows-Volume-Bridge (UAC2 numid=7 → Headset-Sink) |
|
||||||
|
| `aime-headset-keys.service` (root) | H390-Inline-Tasten → Lautstärke |
|
||||||
|
| alte `aime-audio-out` / `aime-volume` | ES8323-Ära, **disabled** |
|
||||||
|
|
||||||
|
Kaltstart 2× verifiziert: Topologie, Gate, Volume-Bridge und WLAN kommen ohne Eingriff hoch.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# 03 — Aufnahme-Pipeline
|
||||||
|
|
||||||
|
Vom Klick auf „Stop“ bis zur fertigen Analyse. Crash-sicher: Audio + DB-Zeile werden **vor** der langen
|
||||||
|
Verarbeitung persistiert; Transkription/Analyse laufen asynchron im Hintergrund (GUI pollt).
|
||||||
|
|
||||||
|
## Ablauf
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant GUI as Web-GUI
|
||||||
|
participant SRV as server.js
|
||||||
|
participant REC as recorder.js
|
||||||
|
participant WH as Whisper (NPU-Encoder)
|
||||||
|
participant DI as diar.js (sherpa, CPU)
|
||||||
|
participant LLM as llm.js (Gemma, NPU)
|
||||||
|
participant DB as MariaDB
|
||||||
|
|
||||||
|
GUI->>SRV: Stop
|
||||||
|
SRV->>REC: stopAndSave()
|
||||||
|
REC->>DB: WAV + MP3 + Zeile (status=transcribing)
|
||||||
|
Note over REC: kehrt nach ~1,5 s zurück — GUI frei
|
||||||
|
REC->>REC: A76-Takt kappen (1,6 GHz)
|
||||||
|
REC->>WH: transcribeWav() [BatchedInferencePipeline, batch=8]
|
||||||
|
WH-->>REC: Wörter + Zeitstempel
|
||||||
|
REC->>DI: diarize() [Timeout = Dauer × 2,5, min. 20 min]
|
||||||
|
DI-->>REC: Sprecher-Segmente
|
||||||
|
REC->>DB: setTranscript (status=done)
|
||||||
|
REC->>REC: finally: Takt zurück
|
||||||
|
SRV->>LLM: analyzeRecording() (fire-and-forget, analysis_status=pending)
|
||||||
|
LLM->>DB: Titel/Summary/Teilnehmer/Todos (analysis_status=done, analysis_ms)
|
||||||
|
GUI->>SRV: Polling analysis_status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pfadwahl Transkription
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
A["WAV fertig"] --> B{"Dauer ≤ 30 s?<br/>(SHORT_SEC)"}
|
||||||
|
B -- ja --> C["Warm-Daemon transcribe_daemon_cpu.py<br/>CPU-only, koexistiert mit residentem rkllm<br/>kein Modell-Swap → 10-s-Clip in ~53 s statt 120 s"]
|
||||||
|
B -- nein --> D["transcribe_cli: faster-whisper large-v3-turbo<br/>Encoder auf NPU, Decoder CPU, batch=8<br/>RTF ≈ 0,39"]
|
||||||
|
C --> E["Diarization"]
|
||||||
|
D --> E
|
||||||
|
```
|
||||||
|
|
||||||
|
- `TRANSCRIBER=npu` (Default) / `=cpu` als Fallback-Schalter in recorder.js.
|
||||||
|
- Whisper-Kalibrierung: medium beam1 hatte 11,3 % WER vs. large-v3 — beam5 schlechter, `--initial-prompt`
|
||||||
|
mit Begriffsliste **zerstört** den Output (Echo-Loop). Eigennamen-Korrektur passiert deshalb im Text-Layer (Glossar, Kap. 05).
|
||||||
|
|
||||||
|
## Zustandsmodell einer Aufnahme
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> recording
|
||||||
|
recording --> transcribing: stopAndSave() — WAV/MP3/Zeile SOFORT persistiert
|
||||||
|
transcribing --> done_transcript: Transkript + Diarization gespeichert
|
||||||
|
transcribing --> failed: Absturz/Fehler → GUI zeigt Retry
|
||||||
|
failed --> transcribing: POST /retranscribe
|
||||||
|
done_transcript --> analyzing: analysis_status=pending
|
||||||
|
analyzing --> analyzed: analysis_status=done (analysis_ms gesetzt)
|
||||||
|
analyzing --> analysis_error: GUI-Button "Erneut versuchen"
|
||||||
|
analysis_error --> analyzing: POST /reanalyze bzw. /reanalyze-llm
|
||||||
|
```
|
||||||
|
|
||||||
|
- Hängende `transcribing`-Zeile = Absturz mitten in der Diarization (Transkript wird erst NACH Diar gespeichert)
|
||||||
|
→ kein Auto-Resume, GUI-Button „Neu transkribieren“.
|
||||||
|
- **NPU-Lock:** alle schweren Jobs (transcribe / analyze / re-diarize) laufen seriell durch `withNpuLock` —
|
||||||
|
zwei gleichzeitig fertig werdende Aufnahmen haben sich sonst gegenseitig zerlegt.
|
||||||
|
- **GUI-Anzeige „⏱ Analysezeit“:** `analysis_started_at`/`analysis_ms` in der DB; beim Polling auf
|
||||||
|
`analysis_status` achten, NICHT auf `status` (sonst wirkt die Summary „leer“).
|
||||||
|
|
||||||
|
## API-Endpunkte (Auszug)
|
||||||
|
|
||||||
|
| Endpunkt | Zweck |
|
||||||
|
|---|---|
|
||||||
|
| `POST /api/recordings/:id/retranscribe` | Hängende/fehlgeschlagene Transkription neu starten |
|
||||||
|
| `POST /api/recordings/:id/reanalyze` `{numSpeakers?}` | Diarization **immer** neu (leer/0 = automatisch, Zahl = Anzahl gepinnt) + LLM neu |
|
||||||
|
| `POST /api/recordings/:id/reanalyze-llm` | Nur LLM neu, Diarization/Wörter bleiben |
|
||||||
|
| `POST /api/recordings/:id/speaker` `{speaker,name,enroll}` | Sprecher benennen + optional Stimme lernen |
|
||||||
|
| `/api/glossary` (CRUD) | Eigennamen-Glossar |
|
||||||
|
|
||||||
|
## Performance-Kennzahlen (71-min-Referenz #69)
|
||||||
|
|
||||||
|
| Schritt | Wert |
|
||||||
|
|---|---|
|
||||||
|
| Transkription (NPU, batch=8) | RTF ≈ 0,39 |
|
||||||
|
| Diarization (CPU) | RTF ≈ 0,19 |
|
||||||
|
| Komplett E2E | RTF ≈ 0,9 |
|
||||||
|
| LLM-Analyse (Gemma, 10 Chunks + Reduce) | ~20 min |
|
||||||
|
| Kurzclip (10 s) komplett | ~53 s |
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# 04 — Sprechertrennung (Diarization) & Stimm-Profile
|
||||||
|
|
||||||
|
**Anforderung:** akustische Sprecher-ID ist Pflicht (Raumspinne: 3–4 Personen auf einem BT-Kanal).
|
||||||
|
Sprecheranzahl soll **dynamisch** erkannt werden (Personen kommen/gehen) — kein festes Pinnen nötig.
|
||||||
|
|
||||||
|
## Pipeline
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
WAV["Mono-WAV"] --> SHERPA["sherpa-onnx Clustering<br/>liefert VIELE Roh-Cluster<br/>(z. B. konstant 27 bei einem 3-Personen-Call — deterministisch)"]
|
||||||
|
SHERPA --> EMB["Jedes Cluster neu einbetten<br/>TitaNet-Embedding (spk.embed)"]
|
||||||
|
EMB --> SORT["Cluster nach Länge sortieren<br/>(längste zuerst = stabile Reihenfolge)"]
|
||||||
|
SORT --> ANCHOR{"Cluster ≥ 3 s?<br/>(DIAR_MIN_SPEAKER_SEC)"}
|
||||||
|
ANCHOR -- "ja (Anchor)" --> JOIN{"Kosinus ≥ 0,5 zu<br/>bestehendem Sprecher?<br/>(DIAR_MERGE_THRESHOLD)"}
|
||||||
|
JOIN -- ja --> MERGE["dem Sprecher zuordnen"]
|
||||||
|
JOIN -- nein --> NEW["neuen Sprecher eröffnen"]
|
||||||
|
ANCHOR -- "nein (kurz/Streu)" --> NEAREST["IMMER zum nächstliegenden Anchor<br/>(darf NIE neuen Sprecher eröffnen)"]
|
||||||
|
MERGE --> OUT["finale Sprecher (z. B. 27 → 3)"]
|
||||||
|
NEW --> OUT
|
||||||
|
NEAREST --> OUT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Warum Greedy-Anchor-Merge (Geschichte der Fixes)
|
||||||
|
|
||||||
|
1. **Schwellwert-Tuning nutzlos:** Sweep auf 2-Personen-Clip: Threshold 0,7 → 12 Sprecher … 0,95 → 8. Nie 2.
|
||||||
|
2. **Union-Find (1. Konsolidierung) instabil:** paarweises Mergen bildet transitive Ketten (A~B~C zieht alles
|
||||||
|
zusammen). Ein Grenzfall-Link um Kosinus ≈ 0,5 kippte je nach FP-Rauschen (Multi-Thread-TitaNet unter
|
||||||
|
CPU-Last) → derselbe Call ergab 2 / 3 / 7 Sprecher in drei Läufen.
|
||||||
|
3. **Greedy-Anchor-Merge (aktuell, `diar.js consolidateSpeakers`):** nur lange Cluster (verlässliche
|
||||||
|
Embeddings) dürfen Sprecher eröffnen, Abarbeitung längste-zuerst (ordnungsstabil), kurze Cluster können nur
|
||||||
|
zugeordnet werden → Rauschen kann nichts mehr fragmentieren. Verifiziert: 5/5 Läufe konstant 27 → 3.
|
||||||
|
|
||||||
|
## Harte Grenzen / Sackgassen
|
||||||
|
|
||||||
|
- **Überlappendes gleichzeitiges Sprechen** ist mit Mono-Mix prinzipiell nicht trennbar.
|
||||||
|
- Geclipptes Audio (0 dBFS) zerstört Embeddings → Fantasie-Sprecher (deshalb Gain-Staging, Kap. 02).
|
||||||
|
- sherpa-Quantisierung q5_0: langsamer UND schlechter auf diesem aarch64-Build — tot.
|
||||||
|
- Diarization-Timeout skaliert mit Dauer (`diarTimeoutMs()` = Dauer × 2,5, min. 20 min) — der feste
|
||||||
|
10-min-Timeout hat 46-min-Aufnahmen abgebrochen. (`parse-options.cc:373`-Ausgabe ist harmloses argv-Echo.)
|
||||||
|
|
||||||
|
## Sprecher-Namen & Stimm-Profile (Enrollment)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
DIAR["Diarization: speaker_00, speaker_01, …"] --> AUTOID["identifySpeakers():<br/>Abgleich gegen gespeicherte Voiceprints"]
|
||||||
|
AUTOID --> MERGE2["mergeSpeakerNames():<br/>füllt nur Lücken, manuelle Namen gewinnen"]
|
||||||
|
GUI["GUI Sprecher-Editor:<br/>Name eintragen + 'lernen'"] --> API["POST /speaker {speaker, name, enroll}"]
|
||||||
|
API --> VP[("voiceprints-Tabelle<br/>1 Zentroid pro Name<br/>(laufender Mittelwert, Spalte n)")]
|
||||||
|
API --> NAMES[("recordings.speaker_names<br/>JSON, manuell gewinnt")]
|
||||||
|
VP --> AUTOID
|
||||||
|
```
|
||||||
|
|
||||||
|
- Beim Lernen wird der längste zusammenhängende Sprech-Abschnitt (≥ 1,5 s) des Sprechers als TitaNet-Embedding
|
||||||
|
in den Zentroid gefaltet (`db.upsertVoiceprint`) → zukünftige Aufnahmen identifizieren die Person automatisch.
|
||||||
|
- Teilnehmerliste der Analyse kommt seit 2026-07-15 **aus der Diarization** (Namen wo bekannt, sonst
|
||||||
|
„Sprecher N“) — nicht mehr vom LLM (das listete alle bloß *erwähnten* Namen, 11 statt 3). Siehe Kap. 05.
|
||||||
|
|
||||||
|
## Re-Diarization
|
||||||
|
|
||||||
|
- GUI „↻ Analyse neu“ → Prompt: leer = automatisch (konsolidiert), Zahl = Anzahl gepinnt
|
||||||
|
(`--clustering.num-clusters`). Läuft immer unter A76-Takt-Cap (lüfterlos!).
|
||||||
|
- `db.replaceWords()` tauscht nur die Sprecher-Zuordnung; Text/Zeitstempel bleiben.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# 05 — LLM-Analyse (Gemma-3-4B auf der NPU)
|
||||||
|
|
||||||
|
## Modell
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Modell | **Gemma-3-4B-it**, `gemma3-4b-w8a8g128-16k.rkllm` |
|
||||||
|
| Runtime | rkllm **1.3.0** (Upgrade von 1.2.1 — API- und Chat-Template-Änderungen) |
|
||||||
|
| Quantisierung | **grouped w8a8 (g128)** — plain w8a8 hat Gemma kaputt-quantisiert; w4a16 auf RK3588 nicht unterstützt |
|
||||||
|
| Kontext | 16k konvertiert, **läuft mit 8192** (KV-Cache-RAM auf 8-GB-Board) ≈ 20 000 Zeichen Prompt-Budget |
|
||||||
|
| Tempo | ~5 tok/s Decode → Kurzclip ~5 min, 71-min-Aufnahme ~20 min Analyse |
|
||||||
|
| Historie | Vorher Qwen2.5-3B (fest 4096 Kontext). Gemma ≈ Gemini-Qualität, aber ~3× langsamer. gemma3n-E4B zu groß (10,4 GB). |
|
||||||
|
|
||||||
|
**Hart:** niemals Online-LLM — alles lokal auf der NPU.
|
||||||
|
|
||||||
|
## Pfadwahl nach Transkriptlänge
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
T["Transkript"] --> A{"≤ 4 800 Zeichen?"}
|
||||||
|
A -- ja --> SINGLE["analyzeSingle:<br/>EIN Call = bereinigter Dialog + JSON-Analyse<br/>(Fallback: separater JSON-Call bei Echo/Parse-Fehler)"]
|
||||||
|
A -- nein --> B{"≤ 20 000 Zeichen?"}
|
||||||
|
B -- ja --> WHOLE["analyzeWhole:<br/>GANZES Gespräch in einem Analyse-Call<br/>(kohärente Summary, kleine JSON-Ausgabe)"]
|
||||||
|
B -- nein --> LONG["analyzeLong (Map-Reduce):<br/>Chunks à 8 000 Zeichen"]
|
||||||
|
LONG --> MAP["MAP: pro Chunk Mini-JSON<br/>(zusammenfassung, teilnehmer, todos)<br/>bewusst minimaler Prompt — lange Regelwände<br/>kippen Gemma in englische Prosa"]
|
||||||
|
MAP --> SALV{"JSON parsebar?"}
|
||||||
|
SALV -- ja --> COLLECT["einsammeln"]
|
||||||
|
SALV -- nein --> PROSE["Prosa retten:<br/>Code-Fences UND rohe {...}-Blobs entfernen<br/>(JSON-Leak-Fix), dann als Teil-Summary nutzen"]
|
||||||
|
PROSE --> COLLECT
|
||||||
|
COLLECT --> REDUCE["REDUCE: Teil-Summaries (je ≤ 800 Zeichen gekappt)<br/>→ EIN JSON: titel, zusammenfassung,<br/>kernpunkte[], entscheidungen[]<br/>2 Parse-Versuche, Tail wird geloggt"]
|
||||||
|
REDUCE --> OUT["Ergebnis"]
|
||||||
|
SINGLE --> OUT
|
||||||
|
WHOLE --> OUT
|
||||||
|
```
|
||||||
|
|
||||||
|
## Strukturierte Zusammenfassung (seit 2026-07-15)
|
||||||
|
|
||||||
|
Das LLM liefert getrennte Felder; `composeSummary()` baut daraus den Summary-Text:
|
||||||
|
|
||||||
|
```
|
||||||
|
<Überblick, 2–4 Sätze>
|
||||||
|
|
||||||
|
Kernpunkte:
|
||||||
|
• <Punkt>
|
||||||
|
• <Punkt>
|
||||||
|
|
||||||
|
Entscheidungen:
|
||||||
|
• <Entscheidung>
|
||||||
|
```
|
||||||
|
|
||||||
|
GUI rendert mit `white-space: pre-line` (style.css) → Bullets und Absätze sichtbar. Todos bleiben eigenes
|
||||||
|
Feld/eigene Liste. **Teilnehmer kommen NICHT vom LLM**, sondern aus der Diarization (Kap. 04) — das LLM
|
||||||
|
hatte alle im Gespräch *erwähnten* Namen als Teilnehmer gelistet (11 statt 3 bei Aufnahme #69).
|
||||||
|
|
||||||
|
## Robustheit gegen Gemma-Marotten (alle real aufgetreten)
|
||||||
|
|
||||||
|
| Marotte | Gegenmaßnahme |
|
||||||
|
|---|---|
|
||||||
|
| Antwortet Prosa statt JSON bei großen Inputs | kleine Chunks (8 000), minimaler Chunk-Prompt, Reduce-Input gekappt + Retry, Prosa-Salvage |
|
||||||
|
| Kaputtes JSON leakt in die Summary | `{...}`-Blobs werden vor Salvage entfernt |
|
||||||
|
| Todos als Objekte (`{"aufgabe": …}`) statt Strings | `cleanList()` flacht Objekt-Werte zu Strings ab |
|
||||||
|
| `"null"` / `"[object Object]"` / lose `?` in Listen | `cleanList()` filtert/trimmt |
|
||||||
|
| Echot Feldbeschreibungen statt Inhalt | `echoed()`-Guard → separater JSON-Call |
|
||||||
|
| Echot konkrete Beispiele aus dem Prompt (Phantom-Todos, „Kompressor“-Summary) | Korrektur-Regeln bewusst OHNE konkrete Beispiele (`CORRECTION_HINT` abstrakt) |
|
||||||
|
| Erfindet Fantasie-Komposita („Audio-Dualeintragung“) | `NO_INVENT`-Regel in allen Summary-Prompts |
|
||||||
|
| Chunk-Parse-Fehler variieren pro Lauf (2–3 von 10) | nichtdeterministisch, wird per Salvage aufgefangen; bei schwachem Ergebnis einfach „Analyse neu“ |
|
||||||
|
|
||||||
|
## Eigennamen-Glossar (Whisper hört z. B. „Aime“ als „Amy“)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
G[("glossary-Tabelle<br/>GUI-Tab 'Glossar', /api/glossary")] --> Q{"Eintrag hat<br/>Soundalikes?"}
|
||||||
|
Q -- ja --> DET["DETERMINISTISCH: Ganzwort-Ersetzung<br/>direkt auf rec.words VOR der Analyse<br/>(applyGlossaryText / glossaryMap)"]
|
||||||
|
Q -- nein --> HINT["nur als Hinweis in den LLM-Prompt<br/>(glossaryHint)"]
|
||||||
|
DET --> LLM2["LLM sieht schon den korrekten Begriff"]
|
||||||
|
LLM2 --> BELT["Gürtel+Hosenträger: Ersetzung nochmal<br/>auf Titel/Summary/Todos des LLM-Outputs"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtig: Begriffslisten **niemals** als Whisper-`--initial-prompt` (Echo-Loop, verifiziert) und dem 3–4B-Modell
|
||||||
|
keine „ersetze X durch Y“-Anweisung anvertrauen (unzuverlässig) — deshalb die deterministische Text-Ersetzung.
|
||||||
|
|
||||||
|
## Zusammenspiel mit Whisper auf der NPU
|
||||||
|
|
||||||
|
rkllm (LLM) und Whisper-NPU-Encoder können nicht gleichzeitig resident sein (RAM-Contention, äußerte sich
|
||||||
|
als `rc=-6`). Kurzclips (≤ 30 s) nutzen deshalb den CPU-Whisper-Daemon ohne rkllm-Swap; lange Aufnahmen
|
||||||
|
swappen seriell unter dem NPU-Lock.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# 06 — iPhone-Telefonate über Bluetooth (HFP)
|
||||||
|
|
||||||
|
Das Board gibt sich gegenüber dem iPhone als **Bluetooth-Headset** aus („Aime“, AX210/`hci0`). iPhone-Anrufe
|
||||||
|
laufen dadurch über das Board: Gegenseite → Aufnahme-Mix, Nutzer-Mikrofon (gegated) → iPhone-Uplink.
|
||||||
|
Läuft **parallel** zum PC/USB-Gadget-Pfad — kein Umschalten; bei eingehendem Anruf pausiert der Nutzer sein
|
||||||
|
PC-Video manuell.
|
||||||
|
|
||||||
|
## Konfiguration (die funktionierende Kombination)
|
||||||
|
|
||||||
|
- WirePlumber-Drop-in: `bluez5.roles = [a2dp_sink hfp_hf hsp_hs]` + `hfphsp-backend = native`
|
||||||
|
(oFono war ein Irrweg, deaktiviert).
|
||||||
|
- **Frischer Pair nötig** (auf dem iPhone „Gerät ignorieren“, dann neu koppeln) — alte Bonds machen Ärger.
|
||||||
|
- iPhone „Skynet16“ (B4:96…), bt-agent NoInputNoOutput, Gerät muss **Trusted=yes** sein
|
||||||
|
(nach Re-Pair stand es auf NO → WirePlumber baut für untrusted Geräte keine HFP-Nodes → Aufnahme #58
|
||||||
|
hatte nur die Nahseite).
|
||||||
|
- Einziger Verzicht: BT-Ohrhörer als Aufnahmegerät (AG-Rolle) ist aus — Board ist HF-only.
|
||||||
|
- `profile="off"` zwischen Anrufen ist **normal**: HFP-Nodes existieren nur während eines aktiven Anrufs
|
||||||
|
(eSCO/mSBC, 16 kHz).
|
||||||
|
|
||||||
|
## Audio-Routing während eines Anrufs
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph IPHONE["iPhone (im Anruf)"]
|
||||||
|
CALL["Telefonat"]
|
||||||
|
end
|
||||||
|
subgraph BOARD["Board (BT 'Aime')"]
|
||||||
|
FAR["bluez_input.MAC.0<br/>= Gegenseite"]
|
||||||
|
UP["bluez_output.MAC.1<br/>= Uplink zum iPhone"]
|
||||||
|
MIX[("callmix")]
|
||||||
|
GATE["micgate.out (gegatetes Mikro)"]
|
||||||
|
HS["Headset-Hörer"]
|
||||||
|
end
|
||||||
|
CALL --> FAR
|
||||||
|
FAR --> MIX
|
||||||
|
FAR --> HS
|
||||||
|
GATE --> UP --> CALL
|
||||||
|
```
|
||||||
|
|
||||||
|
**Echo-Fix:** WirePlumber verlinkte den Uplink zunächst mit `callrec_mix` (ungegatetes Mikro + Gegenseite
|
||||||
|
selbst!) → Gegenseite hörte sich selbst. Korrekt: Uplink ← **micgate.out**. Vom Nutzer bestätigt echofrei.
|
||||||
|
|
||||||
|
## Automatik
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant IP as iPhone
|
||||||
|
participant BZ as BlueZ/WirePlumber
|
||||||
|
participant W as aime-iphone-route.sh (Watcher)
|
||||||
|
participant PW as PipeWire
|
||||||
|
|
||||||
|
IP->>BZ: Anruf beginnt (eSCO aufgebaut)
|
||||||
|
BZ->>PW: HFP-Nodes erscheinen (bluez_input/.0, bluez_output/.1)
|
||||||
|
W->>PW: erkennt neue Nodes
|
||||||
|
W->>PW: Gegenseite → callmix + Headset verlinken
|
||||||
|
W->>PW: Uplink ← micgate.out verlinken (Echo-Fix)
|
||||||
|
IP->>BZ: Anruf endet
|
||||||
|
BZ->>PW: HFP-Nodes verschwinden (profile=off — normal)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Watcher persistent: `/opt/callrec/aime-iphone-route.sh` + `aime-iphone-route.service`; überlebt Reboots
|
||||||
|
(Config/Service/Bond+Trusted verifiziert).
|
||||||
|
- Der Watcher-Neustart lässt callmix/Loopbacks intakt.
|
||||||
|
|
||||||
|
## Offen
|
||||||
|
|
||||||
|
- App-seitiger automatischer Aufnahme-Start bei iPhone-Anrufen.
|
||||||
|
- Voller E2E-Test „Anruf → Aufnahme → Analyse“ über den BT-Pfad.
|
||||||
|
|
||||||
|
## Referenz: BT-Bond-Quirks (Ohrhörer, historisch)
|
||||||
|
|
||||||
|
Buds mit No-Bonding-Quirk: LinkKey musste manuell in `/var/lib/bluetooth` persistiert werden
|
||||||
|
(btmon-Capture-Prozedur). RTL8821CU-Wedge-Recovery (USB-Reset, nur über LAN .50) — betrifft den alten
|
||||||
|
Dongle, der entfernt wurde; nur noch relevant, falls die Hardware zurückkommt.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# 07 — Betrieb
|
||||||
|
|
||||||
|
> ⚠️ Dieses Repo ist **privat**; hier stehen LAN-Zugangsdaten. Nicht öffentlich stellen.
|
||||||
|
|
||||||
|
## Zugänge
|
||||||
|
|
||||||
|
| System | Zugang |
|
||||||
|
|---|---|
|
||||||
|
| Board SSH | `root@192.168.1.200` (WLAN, primär) / `root@192.168.1.50` (LAN), PW `123456` |
|
||||||
|
| Web-GUI | `http://192.168.1.200:3000` |
|
||||||
|
| MariaDB (auf dem Board) | DB `callrec`, User `callrec`, PW `callrec123` (kein `sqlite3`/CLI-Client — `mysql` nutzen) |
|
||||||
|
| Gitea (diese Doku) | `http://192.168.1.199:3000`, User `sascha` |
|
||||||
|
| App-User auf dem Board | `callrec`, Code in `/opt/callrec` |
|
||||||
|
|
||||||
|
## systemd-Einheiten
|
||||||
|
|
||||||
|
| Einheit | Scope | Aufgabe |
|
||||||
|
|---|---|---|
|
||||||
|
| `callrec.service` | system | Node-App (Web-GUI :3000, Aufnahme, Pipeline) |
|
||||||
|
| `callrec-btwatch.service` | system | BT-Headset-Auto-Connect (card-aware) |
|
||||||
|
| `callrec-perf.service` | system | CPU-Governor `performance` + A76-Pinning |
|
||||||
|
| `aime-usb-audio.service` | user | PipeWire-Topologie beim Boot (callmix, Loopbacks, Remap) |
|
||||||
|
| `aime-micgate.service` | user | LADSPA-Noise-Gate-Filter-Chain |
|
||||||
|
| `aime-volume-headset.service` | user | Windows-Volume → Headset-Sink (UAC2 numid=7) |
|
||||||
|
| `aime-headset-keys.service` | root | H390-Inline-Tasten → Lautstärke |
|
||||||
|
| `aime-iphone-route.service` | — | HFP-Routing-Watcher für iPhone-Anrufe |
|
||||||
|
| `filter-chain.service` (PipeWire-Standard) | user | **MASKIERT** — würde micgate doppeln |
|
||||||
|
| `aime-audio-out` / `aime-volume` | user | ES8323-Ära, **disabled** |
|
||||||
|
|
||||||
|
## Wichtige Pfade auf dem Board
|
||||||
|
|
||||||
|
| Pfad | Inhalt |
|
||||||
|
|---|---|
|
||||||
|
| `/opt/callrec/*.js` | server / recorder / llm / diar / spk / db / bt |
|
||||||
|
| `/opt/callrec/web/` | Vite-Frontend (`npx vite build` nach Änderungen an `src/`) |
|
||||||
|
| `/opt/callrec/aime-*.sh` | Setup-/Bridge-/Cap-Skripte |
|
||||||
|
| `/opt/callrec/milestones/m1-20260709/` | Meilenstein-1-Snapshot inkl. MANIFEST.md (Rollback) |
|
||||||
|
| `/opt/callrec/*.bak-*` | Datei-Backups je Änderung (z. B. `.bak-structsum`, `.bak-anchor`) |
|
||||||
|
| `/etc/sudoers.d/callrec-cpucap` | NOPASSWD für `aime-cpucap.sh` (Thermal-Cap) |
|
||||||
|
|
||||||
|
## Deploy-Ablauf (von Windows aus)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
DEV["Datei lokal ändern<br/>(pscp vom Board holen)"] --> BAK["Backup auf Board:<br/>cp X X.bak-<thema>"]
|
||||||
|
BAK --> UP["pscp hochladen"]
|
||||||
|
UP --> FE{"Frontend<br/>geändert?"}
|
||||||
|
FE -- ja --> BUILD["ssh: cd /opt/callrec/web && npx vite build"]
|
||||||
|
FE -- nein --> RESTART
|
||||||
|
BUILD --> RESTART["systemctl restart callrec"]
|
||||||
|
RESTART --> VERIFY["Verifizieren: journalctl -u callrec,<br/>GUI-Test, ggf. reanalyze-llm"]
|
||||||
|
VERIFY --> DOC["Doku + Changelog in Gitea aktualisieren"]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Windows-Seite: PuTTY-Tools (`plink -batch -pw …`, `pscp`) — OpenSSH-Keys sind nicht hinterlegt.
|
||||||
|
- Node-Syntax vor Upload prüfen: `node --check datei.js`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Ursache / Fix |
|
||||||
|
|---|---|
|
||||||
|
| Zeile hängt auf `transcribing` | Absturz mitten in Diarization → GUI „Neu transkribieren“ (`/retranscribe`) |
|
||||||
|
| Summary wirkt leer nach Retranskription | GUI muss `analysis_status` pollen, nicht `status` — Analyse ist fire-and-forget |
|
||||||
|
| Board plötzlich aus | Thermik prüfen — Schutzabschaltung; Cap-Skript/Sudoers intakt? (Kap. 01) |
|
||||||
|
| Zu viele Sprecher | Audio geclippt? (Gain-Staging Kap. 02) Sonst „Analyse neu“ mit gepinnter Zahl |
|
||||||
|
| Teilnehmer ≠ Sprecher | Teilnehmer kommen aus Diarization; Namen im Sprecher-Editor vergeben |
|
||||||
|
| Kein far-end bei iPhone-Aufnahme | BT-Gerät `Trusted=yes`? HFP-Nodes erscheinen nur IM Anruf (Kap. 06) |
|
||||||
|
| GUI zeigt Quelle ohne Signal | `.monitor`-Falle — Quelle mit `parecord` + `ffmpeg astats` prüfen (Kap. 02) |
|
||||||
|
| `pactl load-module module-filter-chain` → „No such entity“ | Filter-Chain ist Core-Modul → Drop-in-Config nutzen (Kap. 02) |
|
||||||
|
| rkllm/Whisper `rc=-6` | RAM-Contention — nicht beide NPU-resident; NPU-Lock beachten (Kap. 05) |
|
||||||
|
| Analyse „KI Fehler“ bei langen Aufnahmen | Kontext-Überlauf → Chunk-Pfad greift; bei schwachem Ergebnis „Analyse neu“ |
|
||||||
|
|
||||||
|
## Offene Punkte (Stand 2026-07-16)
|
||||||
|
|
||||||
|
- Bedingter Takt-Cap (nur > 15 min Aufnahmedauer) — angeboten, nicht gebaut (~1,4× schnellere Kurzclips).
|
||||||
|
- Feinabstimmung Mikro/PC-Balance in einem echten Headset-Call.
|
||||||
|
- Glossar Phase 2: nächtlicher systemd-Timer + large-v3-Kandidaten-Vorschläge mit Bestätigung (verschoben).
|
||||||
|
- iPhone-Pfad: automatischer Aufnahme-Start + voller E2E-Test (Kap. 06).
|
||||||
|
- Qwen-1.5B als schnellerer LLM-Kandidat für Kurzclips (verschoben).
|
||||||
|
- Aufnahme #41 bleibt dauerhaft geclippt (Referenz für den Gain-Bug).
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# 08 — Changelog
|
||||||
|
|
||||||
|
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||||
|
Was, Warum, betroffene Dateien/Backups).
|
||||||
|
|
||||||
|
## 2026-07-16
|
||||||
|
- **Doku-Repo angelegt** (dieses Repo, Gitea `callrec-docs`). 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).
|
||||||
Reference in New Issue
Block a user