docs: Spec für Mikrofon-Monitor (Geräteüberwachung + Benachrichtigung)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 ⚠ <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) |
|
||||
Reference in New Issue
Block a user