Files
whisper-local/docs/superpowers/specs/2026-05-14-microphone-monitor-design.md
T
2026-05-14 17:21:54 +02:00

118 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ⚠ <msg>"`.
## 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) |