8bbc97a204
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
118 lines
4.9 KiB
Markdown
118 lines
4.9 KiB
Markdown
# 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) |
|