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