Files

118 lines
4.9 KiB
Markdown
Raw Permalink Normal View History

# 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) |