Files
whisper-local/docs/superpowers/specs/2026-04-14-media-pause-during-recording-design.md
T
info f981bbcec5 docs: Spec für Media-Pause während Aufnahme
Design-Dokument für automatisches Pausieren laufender MPRIS-Player
während einer Whisper-Aufnahme, mit Resume nach Aufnahme-Stop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 21:14:19 +02:00

6.7 KiB

Media-Pause während Aufnahme

Status: Spec Datum: 2026-04-14

Ziel

Während eine Whisper-Aufnahme läuft, soll eine laufende Medienwiedergabe (Musik, Video) automatisch pausiert werden. Nach Ende der Aufnahme soll dieselbe Wiedergabe wieder fortgesetzt werden. So muss der Nutzer nicht manuell Musik stoppen, bevor er das Hotkey drückt, und nicht gegen die eigene Wiedergabe anreden.

Scope

In Scope (erste Iteration):

  • Linux-Implementierung via MPRIS (D-Bus)
  • Cross-platform Protocol-Interface analog zu listener/inserter
  • Noop-Fallback für Windows und wenn das Feature deaktiviert ist
  • Opt-out-Konfiguration (Default an) über Tray-Settings und config.toml

Out of Scope:

  • Windows-Implementierung (Protocol vorbereitet, kann später via GlobalSystemMediaTransportControls ergänzt werden)
  • PulseAudio/PipeWire-Sink-Input-Erkennung — MPRIS deckt Browser-Video, Spotify, VLC, mpv ab, mehr Abdeckung lohnt den Aufwand nicht
  • Per-Player Allow-/Blocklist — alle MPRIS-Player im Status Playing werden pausiert
  • Warnungen über nicht-MPRIS-Audioquellen

Architektur

Neues Modul whisper_local/media/ analog zur bestehenden Factory-Dispatch-Struktur:

whisper_local/media/
    __init__.py   # Protocol + create_media_controller()
    _mpris.py     # Linux-Impl via dbus-next
    _noop.py      # Plattform- und Opt-out-Fallback

Protocol

class MediaController(Protocol):
    async def pause(self) -> None:
        """Snapshot + Pause aller aktuell abspielenden MPRIS-Player."""

    async def resume(self) -> None:
        """Fortsetzen nur der Player, die pause() selbst angehalten hat."""

Factory

create_media_controller(enabled: bool) -> MediaController

  • enabled == FalseNoopController
  • sys.platform == "linux"MprisController
  • sonst → NoopController

Das enabled-Flag kommt aus der Konfiguration. Die Factory wird so aufgerufen wie create_listener() / create_inserter() heute.

Kern-Logik (MPRIS / Linux)

pause()

  1. D-Bus-Session verbinden (lazy, beim ersten Aufruf).
  2. Über org.freedesktop.DBus.ListNames alle Namen auflisten, nach Präfix org.mpris.MediaPlayer2. filtern.
  3. Für jeden Player parallel (asyncio.gather) die Property PlaybackStatus von org.mpris.MediaPlayer2.Player lesen.
  4. Alle Player mit Status "Playing" in self._paused: list[str] (Bus-Namen) speichern und auf ihnen Pause() aufrufen — parallel.
  5. Fehler pro Player werden geloggt, brechen die Gesamtaktion nicht ab. Ein Player, der zwischen ListNames und Pause verschwindet, zählt als nicht pausiert (Bus-Name wird nicht in self._paused aufgenommen).

resume()

  1. Für jeden Bus-Namen in self._paused parallel Play() aufrufen.
  2. Fehler pro Player loggen und ignorieren (Player kann seit pause() verschwunden sein — Tab zu, App beendet).
  3. self._paused leeren.

State-Isolation

Nur Player anfassen, die pause() selbst in den Paused-Zustand versetzt hat. Ein Player, der vor Aufnahmebeginn bereits Paused war, bleibt unangetastet — sowohl bei pause() als auch bei resume().

Integration in die Aufnahme-Pipeline

Im Aufnahme-Callback-Pfad (Hotkey-Handler):

on_press   →  await media.pause()
           →  recorder.start()

on_release →  audio = recorder.stop()
           →  await media.resume()
           →  text = transcriber.transcribe(audio)
           →  inserter.insert(text)

resume() wird vor der Transkription aufgerufen. So läuft die Musik weiter, während Whisper rechnet — das kann mehrere Sekunden dauern und soll nicht als Wiedergabe-Hänger spürbar sein.

Fehlerbehandlung

  • Kein D-Bus erreichbar (z. B. Headless, keine User-Session) → MprisController loggt einmalig eine Warnung, pause() und resume() verhalten sich wie No-Ops. Aufnahme läuft normal weiter.
  • Kein Player läuft → pause() ist wirkungslos, resume() ebenfalls.
  • Einzelner D-Bus-Call schlägt fehl → Fehler loggen, anderen Player nicht blockieren (asyncio.gather(..., return_exceptions=True)).
  • resume() wird immer aufgerufen, auch wenn pause() geworfen hat oder die Aufnahme keine gültigen Audiodaten lieferte (stop() liefert None). Aufrufer-Pflicht: try/finally.

Konfiguration

config.toml:

[media]
pause_during_recording = true

Default: true. Wird in config.py als neues Feld ergänzt.

Tray-Settings:

Neue Checkbox „Medienwiedergabe während Aufnahme pausieren" in _settings.py, konsistent zu bestehenden Toggles. Änderung wird beim Speichern in die Config geschrieben und wirkt ab der nächsten Aufnahme (Factory wird pro Aufnahme oder beim Config-Reload neu konsultiert — entsprechend dem bestehenden Muster für andere Toggles).

Dependencies

dbus-next als Linux-only optionale Abhängigkeit in pyproject.toml:

dependencies = [
    ...,
    'dbus-next ; sys_platform == "linux"',
]

Begründung: pure Python, async-nativ (passt zu den bestehenden asyncio-Callbacks), keine Subprocess-Latenz, keine zusätzliche System-Dependency wie playerctl.

Tests

tests/test_media_mpris.py (@pytest.mark.skipif(sys.platform != "linux", ...)):

  • D-Bus-Calls mit unittest.mock.AsyncMock / MagicMock mocken — kein echter D-Bus-Zugriff im Test.
  • Szenarien:
    1. Keine MPRIS-Player vorhanden → pause() ist No-Op, resume() ebenfalls, kein Fehler.
    2. Zwei Player im Status Playing → beide erhalten Pause(), nach resume() beide Play().
    3. Ein Player im Status Playing, einer im Status Paused → nur der Playing-Player wird pausiert; nach resume() wird nur dieser wieder gestartet, der andere bleibt pausiert.
    4. Ein Player verschwindet zwischen pause() und resume() → kein Crash, Fehler wird geloggt, andere Player werden korrekt wieder gestartet.
    5. D-Bus ist nicht erreichbar → MprisController loggt Warnung, Aufrufe sind No-Ops.

tests/test_media_factory.py:

  • sys.platform == "linux" + enabled=TrueMprisController.
  • enabled=False (jede Plattform) → NoopController.
  • sys.platform != "linux"NoopController.

Async-Tests nutzen pytest-asyncio wie im Projekt üblich.

Offene Fragen / bewusst verschoben

  • Windows-Implementierung wird bei Bedarf als separate Iteration gebaut. Das Protocol und die Factory sind so ausgelegt, dass nur eine neue _smtc.py hinzugefügt und die Factory-Dispatch erweitert werden muss.
  • Detail-Anpassungen an der Tray-UI (Gruppierung, Reihenfolge der Checkboxen) werden beim Einbau entschieden, da sie vom aktuellen Stand des Settings-Dialogs abhängen.