# Media-Pause während Aufnahme — Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Während einer Whisper-Aufnahme werden alle aktuell abspielenden MPRIS-Player (Browser-Video, Spotify, VLC, mpv, …) automatisch pausiert und am Ende der Aufnahme wieder fortgesetzt. **Architecture:** Neues Modul `whisper_local/media/` mit `MediaController`-Protocol und Factory-Dispatch analog zu `inserter`/`hotkey`. Linux-Impl `MprisController` nutzt `dbus-next` (async, pure Python), Windows/Opt-out fällt auf `NoopController` zurück. `App.on_press` / `App.on_release` pausieren/resumen die Wiedergabe rund um `recorder.start()` / `recorder.stop()`. **Tech Stack:** Python 3.13, asyncio, dbus-next (Linux-only), pytest/pytest-asyncio, tkinter (Settings-UI). **Spec:** [docs/superpowers/specs/2026-04-14-media-pause-during-recording-design.md](../specs/2026-04-14-media-pause-during-recording-design.md) --- ## Datei-Übersicht **Neu:** - `whisper_local/media/__init__.py` — Protocol `MediaController` + Factory `create_media_controller()` - `whisper_local/media/_noop.py` — `NoopController` (Windows, Opt-out) - `whisper_local/media/_mpris.py` — `MprisController` (Linux, D-Bus / MPRIS) - `tests/test_media_factory.py` — Factory-Dispatch-Tests - `tests/test_media_mpris.py` — MprisController-Verhalten mit gemocktem D-Bus **Geändert:** - `whisper_local/config.py` — Feld `pause_media_during_recording: bool = True`, Laden/Speichern - `whisper_local/__main__.py` — `App`-Integration: Controller erstellen, in `on_press`/`on_release` verwenden, bei Config-Reload neu erstellen - `whisper_local/tray/_settings.py` — Neue Checkbox „Medienwiedergabe während Aufnahme pausieren" - `pyproject.toml` — Dependency `dbus-next ; sys_platform == "linux"` - `config.example.toml` — Neuer Abschnitt `[media]` (wenn vorhanden; sonst überspringen) --- ## Task 1: Config um `pause_media_during_recording` erweitern **Files:** - Modify: `whisper_local/config.py` - Test: `tests/test_config.py` (erstellen falls nicht vorhanden) - [ ] **Step 1: Failing Test schreiben** Datei: `tests/test_config.py` — falls Datei existiert, die neuen Tests anhängen; falls nicht, komplett neu anlegen: ```python """Tests für whisper_local.config.""" from pathlib import Path from whisper_local.config import Config, load_config, save_config def test_default_pause_media_during_recording_is_true(): config = Config() assert config.pause_media_during_recording is True def test_load_config_reads_pause_media_false(tmp_path: Path): cfg_path = tmp_path / "config.toml" cfg_path.write_text("[media]\npause_during_recording = false\n", encoding="utf-8") config = load_config(cfg_path) assert config.pause_media_during_recording is False def test_save_config_roundtrip_preserves_pause_media(tmp_path: Path): cfg_path = tmp_path / "config.toml" original = Config(pause_media_during_recording=False) save_config(original, cfg_path) loaded = load_config(cfg_path) assert loaded.pause_media_during_recording is False ``` - [ ] **Step 2: Tests laufen lassen — müssen fehlschlagen** Run: `uv run pytest tests/test_config.py -v` Expected: FAIL — `Config` hat das Feld nicht, oder `load_config` / `save_config` kennen den Abschnitt nicht. - [ ] **Step 3: `Config`-Dataclass-Feld ergänzen** In `whisper_local/config.py` den `@dataclass Config` erweitern (Feld am Ende hinzufügen, Reihenfolge respektieren): ```python @dataclass class Config: hotkey: str = "KEY_F12" whisper_model: str = "small" language: str = "de" compute_type: str = "int8" sample_rate: int = 16000 channels: int = 1 min_duration: float = 0.5 microphone: str = "" pause_media_during_recording: bool = True ``` - [ ] **Step 4: `load_config` erweitern** In `whisper_local/config.py` nach dem `audio_section`-Block ergänzen: ```python media_section = data.get("media", {}) if "pause_during_recording" in media_section: config.pause_media_during_recording = bool( media_section["pause_during_recording"] ) ``` - [ ] **Step 5: `save_config` erweitern** In `whisper_local/config.py` den `content`-String in `save_config` erweitern — neuer Abschnitt am Ende: ```python def save_config(config: Config, path: Path = DEFAULT_CONFIG_PATH) -> None: """Schreibt Config als TOML-Datei. Erstellt Verzeichnisse bei Bedarf.""" path.parent.mkdir(parents=True, exist_ok=True) pause_media_str = "true" if config.pause_media_during_recording else "false" content = ( f'[hotkey]\nkey = "{_toml_str(config.hotkey)}"\n\n' f'[whisper]\nmodel = "{_toml_str(config.whisper_model)}"\n' f'language = "{_toml_str(config.language)}"\n' f'compute_type = "{_toml_str(config.compute_type)}"\n\n' f'[audio]\nsample_rate = {config.sample_rate}\n' f'channels = {config.channels}\n' f'min_duration = {config.min_duration}\n' f'device = "{_toml_str(config.microphone)}"\n\n' f'[media]\npause_during_recording = {pause_media_str}\n' ) path.write_text(content, encoding="utf-8") ``` - [ ] **Step 6: Tests erneut laufen lassen — alle grün** Run: `uv run pytest tests/test_config.py -v` Expected: PASS (3 neue Tests grün). - [ ] **Step 7: Commit** ```bash git add whisper_local/config.py tests/test_config.py git commit -m "feat(config): pause_media_during_recording-Flag" ``` --- ## Task 2: Protocol, Noop-Controller und Factory **Files:** - Create: `whisper_local/media/__init__.py` - Create: `whisper_local/media/_noop.py` - Create: `tests/test_media_factory.py` - [ ] **Step 1: Failing Tests schreiben** Datei: `tests/test_media_factory.py`: ```python """Tests für whisper_local.media Factory-Dispatch.""" import sys from unittest.mock import patch import pytest from whisper_local.media import MediaController, create_media_controller from whisper_local.media._noop import NoopController def test_factory_returns_noop_when_disabled(): controller = create_media_controller(enabled=False) assert isinstance(controller, NoopController) def test_factory_returns_noop_on_non_linux(): with patch.object(sys, "platform", "win32"): controller = create_media_controller(enabled=True) assert isinstance(controller, NoopController) @pytest.mark.skipif(sys.platform != "linux", reason="MPRIS is Linux-only") def test_factory_returns_mpris_on_linux_when_enabled(): from whisper_local.media._mpris import MprisController controller = create_media_controller(enabled=True) assert isinstance(controller, MprisController) @pytest.mark.asyncio async def test_noop_controller_pause_is_noop(): controller = NoopController() await controller.pause() # Darf nicht werfen await controller.resume() # Darf nicht werfen def test_noop_controller_satisfies_protocol(): controller = NoopController() assert isinstance(controller, MediaController) ``` - [ ] **Step 2: Tests laufen lassen — müssen fehlschlagen** Run: `uv run pytest tests/test_media_factory.py -v` Expected: FAIL — Modul `whisper_local.media` existiert nicht. - [ ] **Step 3: `NoopController` implementieren** Datei: `whisper_local/media/_noop.py`: ```python """No-Op-Fallback für Plattformen ohne MPRIS oder wenn das Feature aus ist.""" class NoopController: async def pause(self) -> None: return None async def resume(self) -> None: return None ``` - [ ] **Step 4: Protocol + Factory implementieren** Datei: `whisper_local/media/__init__.py`: ```python """Media-Steuerung — plattformspezifische Backends hinter gemeinsamem Interface.""" import sys from typing import Protocol, runtime_checkable @runtime_checkable class MediaController(Protocol): async def pause(self) -> None: ... async def resume(self) -> None: ... def create_media_controller(enabled: bool) -> MediaController: """Erstellt den plattformspezifischen Media-Controller. `enabled=False` → immer NoopController. Auf Nicht-Linux-Plattformen wird aktuell ebenfalls der NoopController zurückgegeben. """ if not enabled: from whisper_local.media._noop import NoopController return NoopController() if sys.platform == "linux": from whisper_local.media._mpris import MprisController return MprisController() from whisper_local.media._noop import NoopController return NoopController() ``` - [ ] **Step 5: Platzhalter für `MprisController` anlegen** Damit der Linux-Import nicht sofort bricht (die echte Implementierung kommt in den folgenden Tasks), Datei `whisper_local/media/_mpris.py` mit minimalem Stub anlegen: ```python """Linux-MPRIS-Implementierung. Vollständige Logik folgt in späteren Tasks.""" import logging logger = logging.getLogger(__name__) class MprisController: def __init__(self) -> None: self._paused: list[str] = [] async def pause(self) -> None: return None async def resume(self) -> None: return None ``` - [ ] **Step 6: Tests laufen lassen — alle grün** Run: `uv run pytest tests/test_media_factory.py -v` Expected: PASS (5 Tests grün; auf Nicht-Linux läuft der gate-skipte Test nicht). - [ ] **Step 7: Commit** ```bash git add whisper_local/media/ tests/test_media_factory.py git commit -m "feat(media): Protocol, Factory und Noop-Controller" ``` --- ## Task 3: `dbus-next` als Linux-Dependency aufnehmen **Files:** - Modify: `pyproject.toml` - Modify: `uv.lock` (automatisch durch `uv sync`) - [ ] **Step 1: `pyproject.toml` erweitern** Den `dependencies`-Block in `pyproject.toml` um einen Eintrag erweitern: ```toml dependencies = [ "faster-whisper>=1.1.0", "sounddevice>=0.5.0", "numpy>=2.0.0", "evdev>=1.7.0; sys_platform == 'linux'", "PyGObject>=3.50; sys_platform == 'linux'", "dbus-next>=0.2.3; sys_platform == 'linux'", "pynput>=1.7.0; sys_platform == 'win32'", "pywin32>=306; sys_platform == 'win32'", "pystray>=0.19.0", "Pillow>=10.0.0", "sv-ttk>=2.6.0", "darkdetect>=0.8.0", ] ``` - [ ] **Step 2: Lockfile aktualisieren** Run: `uv sync` Expected: `uv.lock` wird aktualisiert, `dbus-next` wird (unter Linux) installiert. - [ ] **Step 3: Verfügbarkeit verifizieren** Run: `uv run python -c "from dbus_next.aio import MessageBus; print('ok')"` Expected: `ok`. Falls ImportError: Dependency nicht installiert — `uv sync` erneut prüfen. - [ ] **Step 4: Commit** ```bash git add pyproject.toml uv.lock git commit -m "build: dbus-next als Linux-Dependency hinzufügen" ``` --- ## Task 4: `MprisController.pause()` — Player finden und pausieren **Files:** - Modify: `whisper_local/media/_mpris.py` - Create: `tests/test_media_mpris.py` - [ ] **Step 1: Failing Test schreiben** Datei: `tests/test_media_mpris.py`: ```python """Tests für MprisController (Linux/MPRIS).""" import sys from unittest.mock import AsyncMock, MagicMock import pytest pytestmark = pytest.mark.skipif(sys.platform != "linux", reason="MPRIS is Linux-only") def _make_player(status: str) -> MagicMock: """Erzeugt einen gemockten Player-Proxy mit gegebenem PlaybackStatus.""" player = MagicMock() player.get_playback_status = AsyncMock(return_value=status) player.call_pause = AsyncMock() player.call_play = AsyncMock() return player @pytest.mark.asyncio async def test_pause_with_no_players_is_noop(monkeypatch): from whisper_local.media._mpris import MprisController controller = MprisController() monkeypatch.setattr( controller, "_list_player_names", AsyncMock(return_value=[]) ) monkeypatch.setattr( controller, "_get_player_interface", AsyncMock() ) await controller.pause() assert controller._paused == [] @pytest.mark.asyncio async def test_pause_pauses_only_playing_players(monkeypatch): from whisper_local.media._mpris import MprisController playing = _make_player("Playing") paused = _make_player("Paused") async def fake_get_player(name: str): return {"org.mpris.MediaPlayer2.spotify": playing, "org.mpris.MediaPlayer2.vlc": paused}[name] controller = MprisController() monkeypatch.setattr( controller, "_list_player_names", AsyncMock(return_value=[ "org.mpris.MediaPlayer2.spotify", "org.mpris.MediaPlayer2.vlc", ]), ) monkeypatch.setattr(controller, "_get_player_interface", fake_get_player) await controller.pause() playing.call_pause.assert_awaited_once() paused.call_pause.assert_not_awaited() assert controller._paused == ["org.mpris.MediaPlayer2.spotify"] ``` - [ ] **Step 2: Test laufen lassen — muss fehlschlagen** Run: `uv run pytest tests/test_media_mpris.py -v` Expected: FAIL — `_list_player_names` / `_get_player_interface` gibt es noch nicht, `pause()` ist leer. - [ ] **Step 3: `MprisController.pause()` implementieren** `whisper_local/media/_mpris.py` komplett ersetzen: ```python """Linux-MPRIS-Implementierung via dbus-next.""" import asyncio import logging from typing import Any logger = logging.getLogger(__name__) MPRIS_PREFIX = "org.mpris.MediaPlayer2." MPRIS_PATH = "/org/mpris/MediaPlayer2" PLAYER_IFACE = "org.mpris.MediaPlayer2.Player" DBUS_SERVICE = "org.freedesktop.DBus" DBUS_PATH = "/org/freedesktop/DBus" DBUS_IFACE = "org.freedesktop.DBus" class MprisController: def __init__(self) -> None: self._paused: list[str] = [] self._bus: Any = None async def _ensure_bus(self) -> Any: if self._bus is None: from dbus_next.aio import MessageBus self._bus = await MessageBus().connect() return self._bus async def _list_player_names(self) -> list[str]: bus = await self._ensure_bus() intro = await bus.introspect(DBUS_SERVICE, DBUS_PATH) obj = bus.get_proxy_object(DBUS_SERVICE, DBUS_PATH, intro) iface = obj.get_interface(DBUS_IFACE) names = await iface.call_list_names() return [n for n in names if n.startswith(MPRIS_PREFIX)] async def _get_player_interface(self, bus_name: str) -> Any: bus = await self._ensure_bus() intro = await bus.introspect(bus_name, MPRIS_PATH) obj = bus.get_proxy_object(bus_name, MPRIS_PATH, intro) return obj.get_interface(PLAYER_IFACE) async def _pause_player(self, bus_name: str) -> str | None: """Pausiert einen Player, wenn er im Status 'Playing' ist. Gibt den Bus-Namen zurück, wenn pausiert wurde, sonst None. """ player = await self._get_player_interface(bus_name) status = await player.get_playback_status() if status != "Playing": return None await player.call_pause() return bus_name async def pause(self) -> None: names = await self._list_player_names() results = await asyncio.gather( *(self._pause_player(n) for n in names), return_exceptions=True, ) self._paused = [ name for name in results if isinstance(name, str) ] async def resume(self) -> None: return None ``` - [ ] **Step 4: Test laufen lassen — grün** Run: `uv run pytest tests/test_media_mpris.py -v` Expected: PASS (2 Tests grün). - [ ] **Step 5: Commit** ```bash git add whisper_local/media/_mpris.py tests/test_media_mpris.py git commit -m "feat(media): MprisController.pause() via dbus-next" ``` --- ## Task 5: `MprisController.resume()` — nur eigene Pausen zurücknehmen **Files:** - Modify: `whisper_local/media/_mpris.py` - Modify: `tests/test_media_mpris.py` - [ ] **Step 1: Failing Tests schreiben** An `tests/test_media_mpris.py` anhängen: ```python @pytest.mark.asyncio async def test_resume_plays_only_previously_paused(monkeypatch): from whisper_local.media._mpris import MprisController spotify = _make_player("Paused") vlc = _make_player("Paused") async def fake_get_player(name: str): return {"org.mpris.MediaPlayer2.spotify": spotify, "org.mpris.MediaPlayer2.vlc": vlc}[name] controller = MprisController() controller._paused = ["org.mpris.MediaPlayer2.spotify"] monkeypatch.setattr(controller, "_get_player_interface", fake_get_player) await controller.resume() spotify.call_play.assert_awaited_once() vlc.call_play.assert_not_awaited() assert controller._paused == [] @pytest.mark.asyncio async def test_resume_with_empty_paused_list_is_noop(monkeypatch): from whisper_local.media._mpris import MprisController controller = MprisController() controller._paused = [] get_player = AsyncMock() monkeypatch.setattr(controller, "_get_player_interface", get_player) await controller.resume() get_player.assert_not_awaited() ``` - [ ] **Step 2: Tests laufen lassen — müssen fehlschlagen** Run: `uv run pytest tests/test_media_mpris.py -v` Expected: FAIL — `resume()` ruft noch nichts auf. - [ ] **Step 3: `resume()` implementieren** In `whisper_local/media/_mpris.py` die Methode `resume` ersetzen und eine Hilfsmethode `_resume_player` ergänzen: ```python async def _resume_player(self, bus_name: str) -> None: player = await self._get_player_interface(bus_name) await player.call_play() async def resume(self) -> None: if not self._paused: return to_resume = self._paused self._paused = [] await asyncio.gather( *(self._resume_player(n) for n in to_resume), return_exceptions=True, ) ``` - [ ] **Step 4: Tests laufen lassen — alle grün** Run: `uv run pytest tests/test_media_mpris.py -v` Expected: PASS (4 Tests grün). - [ ] **Step 5: Commit** ```bash git add whisper_local/media/_mpris.py tests/test_media_mpris.py git commit -m "feat(media): MprisController.resume() stellt nur eigene Pausen wieder her" ``` --- ## Task 6: Fehlertoleranz — verschwindende Player und D-Bus nicht erreichbar **Files:** - Modify: `whisper_local/media/_mpris.py` - Modify: `tests/test_media_mpris.py` - [ ] **Step 1: Failing Tests schreiben** An `tests/test_media_mpris.py` anhängen: ```python @pytest.mark.asyncio async def test_pause_logs_and_continues_when_single_player_fails(monkeypatch, caplog): from whisper_local.media._mpris import MprisController good = _make_player("Playing") async def fake_get_player(name: str): if name.endswith("broken"): raise RuntimeError("player disappeared") return good controller = MprisController() monkeypatch.setattr( controller, "_list_player_names", AsyncMock(return_value=[ "org.mpris.MediaPlayer2.broken", "org.mpris.MediaPlayer2.good", ]), ) monkeypatch.setattr(controller, "_get_player_interface", fake_get_player) with caplog.at_level("WARNING"): await controller.pause() good.call_pause.assert_awaited_once() assert controller._paused == ["org.mpris.MediaPlayer2.good"] assert any("broken" in r.message for r in caplog.records) @pytest.mark.asyncio async def test_resume_logs_and_continues_when_single_player_fails(monkeypatch, caplog): from whisper_local.media._mpris import MprisController good = _make_player("Paused") async def fake_get_player(name: str): if name.endswith("broken"): raise RuntimeError("player disappeared") return good controller = MprisController() controller._paused = [ "org.mpris.MediaPlayer2.broken", "org.mpris.MediaPlayer2.good", ] monkeypatch.setattr(controller, "_get_player_interface", fake_get_player) with caplog.at_level("WARNING"): await controller.resume() good.call_play.assert_awaited_once() assert controller._paused == [] assert any("broken" in r.message for r in caplog.records) @pytest.mark.asyncio async def test_pause_is_noop_when_bus_unreachable(monkeypatch, caplog): from whisper_local.media._mpris import MprisController async def failing_connect(self): raise RuntimeError("no session bus") monkeypatch.setattr( "dbus_next.aio.MessageBus.connect", failing_connect, raising=False ) controller = MprisController() with caplog.at_level("WARNING"): await controller.pause() assert controller._paused == [] assert any("D-Bus" in r.message or "bus" in r.message.lower() for r in caplog.records) ``` - [ ] **Step 2: Tests laufen lassen — müssen fehlschlagen** Run: `uv run pytest tests/test_media_mpris.py -v` Expected: FAIL — noch kein Logging für Player-Fehler, noch kein Swallow der Bus-Verbindungs-Exception. - [ ] **Step 3: `_pause_player` / `_resume_player` mit Logging absichern** In `whisper_local/media/_mpris.py` die Methoden `_pause_player` und `_resume_player` ersetzen und `pause` / `resume` für Bus-Fehler absichern: ```python async def _pause_player(self, bus_name: str) -> str | None: try: player = await self._get_player_interface(bus_name) status = await player.get_playback_status() if status != "Playing": return None await player.call_pause() return bus_name except Exception as e: logger.warning("Konnte Player %s nicht pausieren: %s", bus_name, e) return None async def _resume_player(self, bus_name: str) -> None: try: player = await self._get_player_interface(bus_name) await player.call_play() except Exception as e: logger.warning("Konnte Player %s nicht fortsetzen: %s", bus_name, e) async def pause(self) -> None: try: names = await self._list_player_names() except Exception as e: logger.warning("D-Bus nicht erreichbar, überspringe Media-Pause: %s", e) self._paused = [] return results = await asyncio.gather( *(self._pause_player(n) for n in names), return_exceptions=True, ) self._paused = [name for name in results if isinstance(name, str)] async def resume(self) -> None: if not self._paused: return to_resume = self._paused self._paused = [] await asyncio.gather( *(self._resume_player(n) for n in to_resume), return_exceptions=True, ) ``` - [ ] **Step 4: Tests laufen lassen — alle grün** Run: `uv run pytest tests/test_media_mpris.py -v` Expected: PASS (7 Tests grün). - [ ] **Step 5: Commit** ```bash git add whisper_local/media/_mpris.py tests/test_media_mpris.py git commit -m "feat(media): MprisController fängt Player- und Bus-Fehler sauber ab" ``` --- ## Task 7: App-Integration in `__main__.py` **Files:** - Modify: `whisper_local/__main__.py` - [ ] **Step 1: Import und Controller-Erstellung in `App.__init__`** In `whisper_local/__main__.py` den Import-Block oben ergänzen: ```python from whisper_local.media import create_media_controller ``` Und in `App.__init__` — nach `self.inserter = create_inserter()` und vor `self.hotkey = create_listener(...)`: ```python self.media = create_media_controller( enabled=config.pause_media_during_recording ) ``` - [ ] **Step 2: `on_press` erweitern** Die `on_press`-Methode in `whisper_local/__main__.py` ersetzen: ```python async def on_press(self) -> None: """Callback: Hotkey gedrückt — Medien pausieren + Aufnahme starten.""" logger.info("Aufnahme startet...") self.tray.set_state(AppState.RECORDING) await self.media.pause() self.recorder.start() ``` - [ ] **Step 3: `on_release` erweitern (Resume via try/finally)** Die `on_release`-Methode in `whisper_local/__main__.py` ersetzen: ```python async def on_release(self) -> None: """Callback: Hotkey losgelassen — Aufnahme stoppen, Medien fortsetzen, transkribieren, einfügen.""" try: audio = self.recorder.stop() finally: try: await self.media.resume() except Exception as e: logger.warning("Fehler beim Fortsetzen der Medienwiedergabe: %s", e) if audio is None: logger.info("Keine Audio-Daten, übersprungen") self.tray.set_state(AppState.WAITING) return logger.info("Transkribiere...") self.tray.set_state(AppState.TRANSCRIBING) text = self.transcriber.transcribe(audio) if text: await self.inserter.insert(text) self.tray.set_state(AppState.WAITING) ``` - [ ] **Step 4: Controller bei Config-Reload neu erstellen** Die `_on_config_reload`-Methode in `whisper_local/__main__.py` ersetzen: ```python def _on_config_reload(self, new_config: Config) -> None: """Übernimmt neue Konfiguration ohne App-Neustart.""" self._config = new_config self.recorder = Recorder( sample_rate=new_config.sample_rate, channels=new_config.channels, min_duration=new_config.min_duration, device=new_config.microphone or None, ) self.media = create_media_controller( enabled=new_config.pause_media_during_recording ) if self._loop is not None: asyncio.run_coroutine_threadsafe( self._restart_hotkey(new_config.hotkey), self._loop ) ``` - [ ] **Step 5: Bestehende Tests laufen lassen — nichts gebrochen** Run: `uv run pytest -v` Expected: PASS — alle bisherigen Tests weiterhin grün, neue Media-Tests weiterhin grün. - [ ] **Step 6: Manueller Smoke-Test (Linux mit MPRIS-Player)** Falls ein MPRIS-Player verfügbar ist (Spotify / Firefox mit Video / VLC): 1. Player starten und Wiedergabe starten. 2. `uv run whisper-local` starten. 3. Hotkey halten — Wiedergabe sollte pausieren. 4. Hotkey loslassen — Wiedergabe sollte sofort fortsetzen. 5. Während Transkription läuft, darf die Musik nicht wieder abbrechen. Falls kein Player verfügbar: Schritt überspringen, `logger.info` im Log beobachten (keine Fehler). - [ ] **Step 7: Commit** ```bash git add whisper_local/__main__.py git commit -m "feat(app): Medien pausieren bei Aufnahmestart, fortsetzen bei Stopp" ``` --- ## Task 8: Tray-Settings-Checkbox **Files:** - Modify: `whisper_local/tray/_settings.py` - [ ] **Step 1: `SettingsDialog._run` um Checkbox erweitern** In `whisper_local/tray/_settings.py` — im Aufbau des Dialogs nach dem Mikrofon-Block, **vor** dem Button-Block, ergänzen: ```python # --- Medien-Pause --- pause_media_var = tk.BooleanVar( value=self._config.pause_media_during_recording ) ttk.Checkbutton( frame, text="Medienwiedergabe während Aufnahme pausieren", variable=pause_media_var, ).grid(row=3, column=0, columnspan=3, sticky=tk.W, pady=4) ``` Und den Button-Block-Row-Index anpassen — `btn_frame.grid(...)` nutzt aktuell `row=3`; das muss auf `row=4`: ```python btn_frame = ttk.Frame(frame) btn_frame.grid(row=4, column=0, columnspan=3, pady=12, sticky=tk.E) ``` - [ ] **Step 2: `save()` anpassen** In `whisper_local/tray/_settings.py` die lokale Funktion `save` innerhalb von `_run` ersetzen: ```python def save(): new_config = Config( hotkey=hotkey_var.get(), whisper_model=self._config.whisper_model, language=self._config.language, compute_type=self._config.compute_type, sample_rate=self._config.sample_rate, channels=self._config.channels, min_duration=self._config.min_duration, microphone="" if mic_var.get() == "Standard" else mic_var.get(), pause_media_during_recording=pause_media_var.get(), ) save_config(new_config) self._on_save(new_config) self._cancel_event.set() root.destroy() ``` - [ ] **Step 3: Manueller Smoke-Test** Run: `uv run whisper-local`, Tray-Icon rechtsklicken → Einstellungen → Checkbox sichtbar, umschaltbar, Speichern schreibt Wert in die Config-Datei. Verifizieren: ```bash cat ~/.config/whisper-local/config.toml ``` Expected: Abschnitt `[media]\npause_during_recording = true|false\n` entsprechend der Wahl vorhanden. - [ ] **Step 4: Bestehende Tests laufen lassen** Run: `uv run pytest -v` Expected: PASS — alles weiterhin grün. - [ ] **Step 5: Commit** ```bash git add whisper_local/tray/_settings.py git commit -m "feat(settings): Checkbox für Medien-Pause während Aufnahme" ``` --- ## Task 9: Beispiel-Config aktualisieren **Files:** - Modify: `config.example.toml` - [ ] **Step 1: Prüfen, ob Datei existiert** Run: `ls config.example.toml` Falls Datei nicht existiert: Task überspringen und direkt zum nächsten Task (oder Abschluss). - [ ] **Step 2: Abschnitt `[media]` ergänzen** Am Ende von `config.example.toml` anfügen: ```toml [media] # Pausiert MPRIS-Medienplayer (Spotify, VLC, Browser-Video, …) während # einer Aufnahme und setzt sie danach wieder fort. Nur Linux. pause_during_recording = true ``` - [ ] **Step 3: Commit** ```bash git add config.example.toml git commit -m "docs: [media]-Abschnitt in config.example.toml" ``` --- ## Abschluss-Check - [ ] **Alle Tests laufen** Run: `uv run pytest -v` Expected: Alle Tests grün, keine Warnings bezüglich unhandled exceptions / unawaited coroutines. - [ ] **Manueller End-to-End-Test (Linux)** 1. Musik/Video in einem MPRIS-Player starten. 2. `uv run whisper-local`. 3. Hotkey halten, etwas einsprechen, loslassen. 4. Beobachten: Musik pausiert beim Drücken, startet beim Loslassen. 5. Eingefügter Text erscheint im aktiven Fenster. 6. Einstellungen öffnen, Checkbox deaktivieren, Speichern. 7. Hotkey erneut halten — Musik bleibt jetzt an. - [ ] **Dokumentation prüfen** Spec-Dokument unter `docs/superpowers/specs/2026-04-14-media-pause-during-recording-design.md` ist aktuell, keine Abweichungen zwischen Spec und Implementierung.