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,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