9 Tasks in TDD-Reihenfolge: Config-Feld, Protocol/Factory/Noop, dbus-next Dependency, MprisController (pause, resume, Fehlertoleranz), App-Integration, Tray-Checkbox, Beispiel-Config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
30 KiB
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
Datei-Übersicht
Neu:
whisper_local/media/__init__.py— ProtocolMediaController+ Factorycreate_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-Teststests/test_media_mpris.py— MprisController-Verhalten mit gemocktem D-Bus
Geändert:
whisper_local/config.py— Feldpause_media_during_recording: bool = True, Laden/Speichernwhisper_local/__main__.py—App-Integration: Controller erstellen, inon_press/on_releaseverwenden, bei Config-Reload neu erstellenwhisper_local/tray/_settings.py— Neue Checkbox „Medienwiedergabe während Aufnahme pausieren"pyproject.toml— Dependencydbus-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:
"""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):
@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_configerweitern
In whisper_local/config.py nach dem audio_section-Block ergänzen:
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_configerweitern
In whisper_local/config.py den content-String in save_config erweitern — neuer Abschnitt am Ende:
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
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:
"""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:
NoopControllerimplementieren
Datei: whisper_local/media/_noop.py:
"""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:
"""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
MprisControlleranlegen
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:
"""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
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 durchuv sync) -
Step 1:
pyproject.tomlerweitern
Den dependencies-Block in pyproject.toml um einen Eintrag erweitern:
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
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:
"""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:
"""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
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:
@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:
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
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:
@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_playermit Logging absichern
In whisper_local/media/_mpris.py die Methoden _pause_player und _resume_player ersetzen und pause / resume für Bus-Fehler absichern:
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
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:
from whisper_local.media import create_media_controller
Und in App.__init__ — nach self.inserter = create_inserter() und vor self.hotkey = create_listener(...):
self.media = create_media_controller(
enabled=config.pause_media_during_recording
)
- Step 2:
on_presserweitern
Die on_press-Methode in whisper_local/__main__.py ersetzen:
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_releaseerweitern (Resume via try/finally)
Die on_release-Methode in whisper_local/__main__.py ersetzen:
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:
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):
- Player starten und Wiedergabe starten.
uv run whisper-localstarten.- Hotkey halten — Wiedergabe sollte pausieren.
- Hotkey loslassen — Wiedergabe sollte sofort fortsetzen.
- 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
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._runum Checkbox erweitern
In whisper_local/tray/_settings.py — im Aufbau des Dialogs nach dem Mikrofon-Block, vor dem Button-Block, ergänzen:
# --- 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:
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:
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:
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
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:
[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
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)
- Musik/Video in einem MPRIS-Player starten.
uv run whisper-local.- Hotkey halten, etwas einsprechen, loslassen.
- Beobachten: Musik pausiert beim Drücken, startet beim Loslassen.
- Eingefügter Text erscheint im aktiven Fenster.
- Einstellungen öffnen, Checkbox deaktivieren, Speichern.
- 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.