From 8bbc97a20411fe10c287551163d99b79ed5de635 Mon Sep 17 00:00:00 2001 From: Vitali Graf Date: Thu, 14 May 2026 17:21:54 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20Spec=20f=C3=BCr=20Mikrofon-Monitor=20(G?= =?UTF-8?q?er=C3=A4te=C3=BCberwachung=20+=20Benachrichtigung)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../2026-05-14-microphone-monitor-design.md | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-14-microphone-monitor-design.md diff --git a/docs/superpowers/specs/2026-05-14-microphone-monitor-design.md b/docs/superpowers/specs/2026-05-14-microphone-monitor-design.md new file mode 100644 index 0000000..0de5c15 --- /dev/null +++ b/docs/superpowers/specs/2026-05-14-microphone-monitor-design.md @@ -0,0 +1,117 @@ +# Mikrofon-Monitor – Design-Spec + +**Datum:** 2026-05-14 +**Status:** Genehmigt + +## Ziel + +Die Anwendung soll erkennen, wenn Mikrofone im System hinzugefügt oder entfernt werden (USB, Bluetooth). Wenn das konfigurierte Mikrofon nicht mehr vorhanden ist, fällt die App automatisch auf das Standard-Mikrofon zurück und benachrichtigt den Nutzer. + +## Verhalten + +- **Konfiguriertes Mikrofon verschwindet:** Automatischer Fallback auf Standard-Mikrofon (`device = None`), Toast-Benachrichtigung + Tray-Tooltip-Warnung. +- **Konfiguriertes Mikrofon kehrt zurück:** Stellt das konfigurierte Gerät wieder her, Toast „Mikrofon wieder verbunden", Tray-Tooltip-Warnung entfernen. +- **Konfiguriertes Mikrofon fehlt bereits beim Start:** `on_configured_missing` sofort auslösen, nicht erst nach dem ersten Poll-Intervall. +- **Kein konfiguriertes Mikrofon (`device = None`):** Monitor läuft trotzdem, meldet nur `on_device_added` / `on_device_removed` (für zukünftige Erweiterungen), keine Fehlerbehandlung nötig. + +## Neue Dateien + +``` +whisper_local/ + microphone/ + __init__.py # Protocol + create_monitor() Factory + _poll.py # Polling-Implementierung (Linux + Fallback) + _win32.py # IMMNotificationClient-Implementierung (Windows) + tray/ + _notification.py # notify-py Toast-Wrapper +``` + +## Protocol + +```python +class MicrophoneMonitor(Protocol): + on_device_added: Callable[[str], Awaitable[None]] | None + on_device_removed: Callable[[str], Awaitable[None]] | None + on_configured_missing: Callable[[], Awaitable[None]] | None + + async def start(self) -> None: ... + def stop(self) -> None: ... +``` + +## Factory + +```python +def create_monitor(configured_device: str | None) -> MicrophoneMonitor: + ... +``` + +- Windows: versucht `Win32Monitor`; fällt bei COM-Fehler auf `PollMonitor` zurück. +- Alle anderen Plattformen: `PollMonitor`. + +## Erkennungsmechanismus + +### Windows: `Win32Monitor` (`_win32.py`) + +- Registriert `IMMNotificationClient` bei `IMMDeviceEnumerator` via `comtypes`. +- Empfängt `OnDeviceAdded`, `OnDeviceRemoved`, `OnDeviceStateChanged` als COM-Callbacks. +- Übergibt Events per `loop.call_soon_threadsafe` in den asyncio-Loop (identisches Muster wie pynput-Hotkey-Callbacks). +- Wenn COM-Initialisierung fehlschlägt: transparenter Fallback auf `PollMonitor`. + +### Cross-Platform: `PollMonitor` (`_poll.py`) + +- Asyncio-Task mit `asyncio.sleep(2.5)` zwischen Prüfungen. +- Vergleicht aktuelle `sd.query_devices()`-Liste mit letztem Snapshot (Set aus Gerätenamen). +- Erkennt hinzugekommene und verschwundene Geräte, löst entsprechende Callbacks aus. +- Exceptions in `sd.query_devices()` werden geloggt und übersprungen — Loop läuft weiter. + +## Benachrichtigung (`_notification.py`) + +- Nutzt `notify-py` (cross-platform, aktiv gepflegt). +- Funktion `notify(title: str, message: str) -> None` — bei Fehler wird geloggt, nie blockierend. +- Neue Abhängigkeit: `notify-py` in `pyproject.toml` eintragen. + +## Tray-Integration + +`PystrayApp` erhält neue Methode: + +```python +def set_warning(self, msg: str | None) -> None: ... +``` + +- `msg = None`: Icon-Titel zurück auf `"whisper-local"`. +- `msg = "..."`: Icon-Titel auf `"whisper-local ⚠ "`. + +## App-Integration (`__main__.py`) + +- `App.__init__`: erstellt `self.monitor = create_monitor(config.microphone)`. +- Callbacks registrieren: + - `on_configured_missing` → `recorder.device = None` + `notify(...)` + `tray.set_warning(...)` + - `on_device_added` → prüft ob konfiguriertes Gerät zurückgekehrt → `recorder.device = config.microphone` + `notify(...)` + `tray.set_warning(None)` +- `App.run()`: startet `monitor.start()` als asyncio-Task parallel zum Hotkey-Listener. +- `_on_config_reload`: stoppt alten Monitor, erstellt neuen mit aktualisierten Gerätedaten. + +## Fehlerbehandlung + +| Situation | Verhalten | +|-----------|-----------| +| COM-Init schlägt fehl (Windows) | Transparenter Fallback auf `PollMonitor` | +| `sd.query_devices()` wirft Exception | Loggen + überspringen, Loop läuft weiter | +| `notify-py` wirft Exception | Loggen + ignorieren, nie blockierend | +| Konfiguriertes Mikrofon fehlt beim Start | Sofort `on_configured_missing` auslösen | + +## Tests + +- `tests/test_microphone_monitor.py`: + - `PollMonitor` mit gemocktem `sd.query_devices()`. + - Prüft: `on_device_added` wird ausgelöst wenn Gerät erscheint. + - Prüft: `on_device_removed` wird ausgelöst wenn Gerät verschwindet. + - Prüft: `on_configured_missing` wird sofort beim Start ausgelöst wenn Gerät fehlt. +- `Win32Monitor`-Tests nur auf Windows (`@pytest.mark.skipif(sys.platform != "win32", ...)`), COM-Interface gemockt. +- `_notification.py`: kein eigener Test (trivialer Wrapper). + +## Abhängigkeiten + +| Paket | Zweck | Plattform | +|-------|-------|-----------| +| `notify-py` | Desktop-Toast-Benachrichtigungen | alle | +| `comtypes` | COM-Interface für IMMNotificationClient | Windows (bereits transitiv vorhanden via pywinrt) |