docs: OpenWiki-Dokumentation und Auto-Update-Workflow hinzufügen

Fügt generierte OpenWiki-Docs (Architektur, Domain, Betrieb, Tests, Workflows)
sowie den GitHub-Actions-Workflow zur automatischen Aktualisierung hinzu.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 14:53:30 +02:00
parent c69dddd8f5
commit 66ecf55ea9
18 changed files with 780 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
name: OpenWiki Update
on:
workflow_dispatch:
schedule:
- cron: "0 8 * * *"
permissions:
contents: write
pull-requests: write
jobs:
update:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install OpenWiki
run: npm install --global openwiki
- name: Run OpenWiki
run: openwiki code --update --print
env:
OPENWIKI_PROVIDER: openrouter
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
OPENWIKI_MODEL_ID: z-ai/glm-5.2
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
LANGCHAIN_PROJECT: openwiki
LANGCHAIN_TRACING_V2: "true"
- name: Create OpenWiki update pull request
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
with:
add-paths: |
openwiki
AGENTS.md
CLAUDE.md
.github/workflows/openwiki-update.yml
branch: openwiki/update
commit-message: "docs: update OpenWiki"
title: "docs: update OpenWiki"
body: |
Automated OpenWiki documentation update.
This PR was generated by the scheduled OpenWiki workflow.
+9
View File
@@ -0,0 +1,9 @@
<!-- OPENWIKI:START -->
## OpenWiki
This repository uses OpenWiki for recurring code documentation. Start with `openwiki/quickstart.md`, then follow its links to architecture, workflows, domain concepts, operations, integrations, testing guidance, and source maps.
The scheduled OpenWiki GitHub Actions workflow refreshes the repository wiki. Do not hand-edit generated OpenWiki pages unless explicitly asked; prefer updating source code/docs and letting OpenWiki regenerate.
<!-- OPENWIKI:END -->
+10
View File
@@ -96,3 +96,13 @@ Strong success criteria let you loop independently. Weak criteria ("make it work
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
<!-- OPENWIKI:START -->
## OpenWiki
This repository uses OpenWiki for recurring code documentation. Start with `openwiki/quickstart.md`, then follow its links to architecture, workflows, domain concepts, operations, integrations, testing guidance, and source maps.
The scheduled OpenWiki GitHub Actions workflow refreshes the repository wiki. Do not hand-edit generated OpenWiki pages unless explicitly asked; prefer updating source code/docs and letting OpenWiki regenerate.
<!-- OPENWIKI:END -->
+6
View File
@@ -0,0 +1,6 @@
{
"updatedAt": "2026-07-23T12:38:28.858Z",
"command": "init",
"gitHead": "8abaf39a3a1c9fa61e2a96b154da0de5cc76c562",
"model": "glm-5.2"
}
+1
View File
@@ -0,0 +1 @@
A code wiki for this local repository. Prioritize a concise quickstart, architecture overview, source map, key workflows, domain concepts, operations/runbook notes, testing guidance, and integration points. Inspect git history to understand reasoning behind code changes and the progression of the repository. Keep pages grounded in the repository structure and recent code changes. Prefer practical navigation for engineers over generic summaries.
+4
View File
@@ -0,0 +1,4 @@
# Files
- [Architektur-Übersicht](overview.md) - whisper-local verwendet ein Factory-Pattern mit typing.Protocol-Interfaces für plattformübergreifende Abstraktion. Die App-Klasse orchestriert alle Module über einen asyncio-Event-Loop.
- [Source-Map](source-map.md) - Verzeichnisstruktur und Datei-Referenzen für whisper-local. Jede Datei mit ihrer Verantwortung.
+91
View File
@@ -0,0 +1,91 @@
---
type: Architecture
title: Architektur-Übersicht
description: whisper-local verwendet ein Factory-Pattern mit typing.Protocol-Interfaces für plattformübergreifende Abstraktion. Die App-Klasse orchestriert alle Module über einen asyncio-Event-Loop.
tags: [architecture, factory-pattern, protocol, asyncio, platform-dispatch]
---
# Architektur-Übersicht
## Kernprinzip
whisper-local ist plattformübergreifend (Linux + Windows) gebaut. Jede plattformabhängige Funktionalität folgt demselben Muster:
1. **`typing.Protocol`-Interface** im Paket-`__init__.py` — keine abstrakte Basisklasse, stattdessen structural subtyping.
2. **Plattformspezifische Backends** in `_linux.py` / `_win32.py` Untermodulen.
3. **Factory-Funktion** (`create_*()`) im Paket-`__init__.py` dispatcht zur Laufzeit auf `sys.platform`.
Dieses Muster wird konsequent für Hotkey, Inserter, Media-Controller und Mikrofon-Monitor angewendet. Die Factory-Funktion ist die einzige Stelle, an der Plattform-Logik zentralisiert ist — alle Backends implementieren dasselbe Protocol.
## Module und Abhängigkeiten
```
App (__main__.py)
├── Config (config.py) — TOML-Konfiguration, Dataclass
├── Recorder (recorder.py) — Audio-Aufnahme via sounddevice
├── Transcriber (transcriber.py) — faster-whisper Transkription
├── HotkeyListener (hotkey/) — Systemweiter Hotkey-Listener
│ ├── _evdev.py (Linux)
│ └── _pynput.py (Windows)
├── Inserter (inserter/) — Text-Einfügung ins aktive Fenster
│ ├── _wayland.py (Linux)
│ └── _win32.py (Windows)
├── MediaController (media/) — Medien pausieren/während Aufnahme
│ ├── _mpris.py (Linux, D-Bus)
│ ├── _smtc.py (Windows, SMTC)
│ └── _noop.py (Fallback)
├── MicrophoneMonitor (microphone/) — Geräte-Überwachung
│ ├── _win32.py (Windows, COM)
│ └── _poll.py (Cross-Platform Fallback)
└── Tray (tray/) — Tray-Icon, Settings, Notifications
├── _tray.py — PystrayApp + AppState
├── _icon.py — Pillow-Icon-Generierung
├── _settings.py — Tkinter-Einstellungs-Dialog
├── _download_progress.py — Modell-Download-Fortschritt
├── _notification.py — notify-py Wrapper
├── _theme.py — sv-ttk System-Theme
├── _hotkey_record_pynput.py — Windows Hotkey-Aufzeichnung
└── _hotkey_record_evdev.py — Linux Hotkey-Aufzeichnung
```
## App-Klasse — Der Orchestrator
Die `App`-Klasse in [`__main__.py`](../../whisper_local/__main__.py) ist der zentrale Orchestrator. Sie instanziiert alle Module im Konstruktor, verdrahtet Callbacks und betreibt den asyncio-Event-Loop.
Schlüsselfluss:
- `on_press` → Tray auf RECORDING setzen, Medien pausieren, Aufnahme starten.
- `on_release` → Aufnahme stoppen, Medien fortsetzen, transkribieren, Text einfügen, Tray auf WAITING.
- `_on_config_reload` → Recorder, Monitor, Media und Hotkey zur Laufzeit austauschen — kein Neustart nötig.
### Threading-Modell
- **asyncio-Loop** läuft im Main-Thread und verwaltet Hotkey-Listener, Mikrofon-Monitor und Media-Controller.
- **pynput** (Windows) betreibt einen eigenen Thread; Callbacks werden via `call_soon_threadsafe` in den asyncio-Loop gebridged.
- **pystray** läuft in einem Daemon-Thread; Menu-Callbacks rufen `_on_settings` / `_quit` auf, die wiederum thread-safe in den Loop delegieren.
- **Tkinter-Dialoge** (Settings, Download-Fortschritt) laufen in eigenen Daemon-Threads.
### Hotkey-Namen im evdev-Format
Key-Namen folgen einheitlich dem evdev-Format (`KEY_F12`, `KEY_LEFTSHIFT`, …) — auch unter Windows. Das pynput-Backend übersetzt intern via `_evdev_to_pynput_key()`. Dies stellt konsistente Konfiguration über Plattformen hinweg sicher.
## Factory-Dispatch-Übersicht
| Modul | Factory | Linux-Backend | Windows-Backend | Fallback |
|---|---|---|---|---|
| Hotkey | `create_listener()` | `EvdevHotkeyListener` | `PynputHotkeyListener` | — |
| Inserter | `create_inserter()` | `WaylandInserter` | `Win32Inserter` | — |
| Media | `create_media_controller()` | `MprisController` | `SmtcController` | `NoopController` |
| Mikrofon | `create_monitor()` | `PollMonitor` | `Win32Monitor` | `PollMonitor` (in `_win32.py` bei COM-Fehler) |
| Tray | `create_tray()` | `PystrayApp` | `PystrayApp` | `NoOpTray` |
Details zu den einzelnen Backends siehe [Plattform-Abstraktionen](../domain/platform-abstractions.md).
## Circuit-Breaker-Muster
`MprisController` und `SmtcController` implementieren einen Circuit-Breaker: Wenn die D-Bus- bzw. SMTC-Verbindung fehlschlägt (`_bus_broken` / `_broken`), wird der Controller dauerhaft deaktiviert und weitere `pause()`/`resume()`-Aufrufe sind No-Ops. Dies verhindert wiederholte Fehler-Logs bei nicht verfügbarer Infrastruktur.
## Erweiterungspunkte
- **Neue Plattform hinzufügen**: Neue `_<platform>.py`-Datei im jeweiligen Paket, Protocol implementieren, Factory-Funktion um `sys.platform`-Check erweitern.
- **Neues Whisper-Modell**: `config.toml``[whisper]` `model` ändern. Modelle werden von HuggingFace heruntergeladen.
- **Neue Tray-Funktionalität**: Menu in `_tray.py` erweitern, Callback in `App` verdrahten.
+108
View File
@@ -0,0 +1,108 @@
---
type: SourceMap
title: Source-Map
description: Verzeichnisstruktur und Datei-Referenzen für whisper-local. Jede Datei mit ihrer Verantwortung.
tags: [source-map, file-inventory]
---
# Source-Map
## Projekt-Root
| Datei | Beschreibung |
|---|---|
| `pyproject.toml` | Projekt-Metadaten, plattformabhängige Dependencies, Entry-Point `whisper-local` |
| `config.example.toml` | Konfigurations-Vorlage mit allen Sektionen und Defaults |
| `uv.lock` | Lock-File für reproduzierbare Installationen |
| `build.ps1` | Windows-Build-Skript: PyInstaller + versioniertes ZIP |
| `whisper_local.spec` | PyInstaller-Build-Konfiguration (DLLs, Datas, Hidden-Imports) |
| `README.md` | Nutzer-Dokumentation: Installation, System-Dependencies, Konfiguration |
| `CLAUDE.md` | Agent-Instruktionen: Entwicklungsumgebung, Architektur, Test-Konventionen |
| `AGENTS.md` | OpenWiki-Referenz für automatisierte Doku-Updates |
## `whisper_local/` — Hauptpaket
| Datei | Verantwortung |
|---|---|
| `__init__.py` | Paket-Marker |
| `__main__.py` | `App`-Klasse, `main()`-Entry-Point, Orchestrator für alle Module |
| `config.py` | `Config`-Dataclass, `load_config()`, `save_config()` — TOML-basiert |
| `recorder.py` | `Recorder`-Klasse — Audio-Aufnahme via `sounddevice.InputStream` |
| `transcriber.py` | `Transcriber`-Klasse — `faster_whisper.WhisperModel`, Modell-Cache-Logik |
### `whisper_local/hotkey/` — Hotkey-Listener
| Datei | Beschreibung |
|---|---|
| `__init__.py` | `HotkeyListener`-Protocol, `create_listener()`-Factory |
| `_evdev.py` | Linux: `EvdevHotkeyListener` — lauscht auf `/dev/input/event*` |
| `_pynput.py` | Windows: `PynputHotkeyListener` — pynput mit Key-Repeat-Unterdrückung |
### `whisper_local/inserter/` — Text-Einfügung
| Datei | Beschreibung |
|---|---|
| `__init__.py` | `Inserter`-Protocol, `create_inserter()`-Factory |
| `_wayland.py` | Linux: `WaylandInserter``wl-copy` + `ydotool key Ctrl+V`, Clipboard-Restaurierung |
| `_win32.py` | Windows: `Win32Inserter``win32clipboard` + pynput `Ctrl+V`, Clipboard-Restaurierung |
### `whisper_local/media/` — Medien-Steuerung
| Datei | Beschreibung |
|---|---|
| `__init__.py` | `MediaController`-Protocol, `create_media_controller()`-Factory |
| `_mpris.py` | Linux: `MprisController` — D-Bus MPRIS2, pausiert alle Playing-Sessions |
| `_smtc.py` | Windows: `SmtcController` — Windows SMTC via `winrt` |
| `_noop.py` | `NoopController` — No-Op-Fallback |
### `whisper_local/microphone/` — Mikrofon-Überwachung
| Datei | Beschreibung |
|---|---|
| `__init__.py` | `MicrophoneMonitor`-Protocol, `create_monitor()`-Factory |
| `_poll.py` | `PollMonitor` — Cross-Platform Polling via `sounddevice.query_devices()` |
| `_win32.py` | `Win32Monitor` — COM `IMMNotificationClient` mit Polling-Fallback |
### `whisper_local/tray/` — Tray-Icon und UI
| Datei | Beschreibung |
|---|---|
| `__init__.py` | `create_tray()`-Factory, `AppState`-Re-Export |
| `_tray.py` | `PystrayApp` (Tray-Icon, Menu, Zustand), `NoOpTray`, `AppState`-Enum |
| `_icon.py` | `create_icon()` — Pillow-basierte Mikrofon-Icon-Generierung je Zustand |
| `_settings.py` | `SettingsDialog` — Tkinter-Dialog für Hotkey, Mikrofon, Medien-Pause |
| `_download_progress.py` | `load_model_with_progress()` — Tkinter-Fortschrittsdialog für Modell-Download |
| `_notification.py` | `notify()` — notify-py Wrapper für Desktop-Benachrichtigungen |
| `_theme.py` | `apply_system_theme()` — sv-ttk + darkdetect |
| `_hotkey_record_pynput.py` | Windows: Hotkey-Aufzeichnung + `check_hotkey_conflict()` via Win32 `RegisterHotKey` |
| `_hotkey_record_evdev.py` | Linux: Hotkey-Aufzeichnung via evdev-Selector |
## `tests/` — Test-Suite
| Datei | Test-Bereich |
|---|---|
| `test_config.py` | Config laden/speichern, Defaults, TOML-Escaping |
| `test_recorder.py` | Recorder start/stop, min_duration-Filter |
| `test_transcriber.py` | Transcriber mit gemocktem WhisperModel |
| `test_hotkey.py` | EvdevHotkeyListener + PynputHotkeyListener, Key-Repeat-Unterdrückung |
| `test_inserter.py` | WaylandInserter + Win32Inserter, Clipboard-Restaurierung |
| `test_media_factory.py` | Factory-Dispatch für MediaController |
| `test_media_mpris.py` | MprisController pause/resume, Circuit-Breaker |
| `test_media_smtc.py` | SmtcController pause/resume, Circuit-Breaker |
| `test_microphone_monitor.py` | PollMonitor Geräte-Erkennung, Start-Check |
| `test_tray.py` | Tray-App, AppState, Icon-Generierung, Settings-Dialog |
| `test_download_progress.py` | Download-Fortschrittsdialog |
| `test_main.py` | App-Integration: Aufnahme-Zyklus, Config-Reload, Mikrofon-Callbacks |
## `docs/superpowers/` — Specs und Pläne
Design-Specs (`specs/`) und Implementierungspläne (`plans/`) für jede Feature-Entwicklung. Die Doku-Sprache ist Deutsch. Die Specs dokumentieren Architektur-Entscheidungen vor der Implementierung und sind wertvoll für das Verständnis historischer Design-Rationale.
## Weitere Verzeichnisse
| Pfad | Beschreibung |
|---|---|
| `systemd/` | `whisper-local.service` — systemd-User-Unit für Linux-Autostart |
| `.github/workflows/` | `openwiki-update.yml` — tägliche OpenWiki-Doku-Aktualisierung |
| `.claude/skills/bump-version/` | Claude-Code-Skill für SemVer-Versionsverwaltung via `uv version --bump` |
| `build/`, `dist/` | PyInstaller-Build-Artifacts (gitignored) |
+3
View File
@@ -0,0 +1,3 @@
# Files
- [Plattform-Abstraktionen](platform-abstractions.md) - Die vier plattformabhängigen Module — Hotkey, Inserter, Media, Mikrofon — mit ihren jeweiligen Linux- und Windows-Backends. Protocol-Interfaces, Factory-Dispatch und implementierungsspezifische Details.
+115
View File
@@ -0,0 +1,115 @@
---
type: Domain
title: Plattform-Abstraktionen
description: Die vier plattformabhängigen Module — Hotkey, Inserter, Media, Mikrofon — mit ihren jeweiligen Linux- und Windows-Backends. Protocol-Interfaces, Factory-Dispatch und implementierungsspezifische Details.
tags: [domain, platform-abstraction, hotkey, inserter, media, microphone, protocol, factory]
---
# Plattform-Abstraktionen
whisper-local abstrahiert vier plattformabhängige Funktionsbereiche hinter `typing.Protocol`-Interfaces mit Factory-Dispatch. Die [Architektur-Übersicht](../architecture/overview.md) beschreibt das generelle Muster; hier werden die konkreten Backends dokumentiert.
## HotkeyListener
**Protocol**: `HotkeyListener` in [`hotkey/__init__.py`](../../whisper_local/hotkey/__init__.py)
**Attribute**: `on_press: AsyncCallback | None`, `on_release: AsyncCallback | None`
**Methoden**: `async listen()`, `stop()`
### EvdevHotkeyListener (Linux)
Datei: [`hotkey/_evdev.py`](../../whisper_local/hotkey/_evdev.py)
- `find_keyboard_devices(key_name)` durchsucht alle `/dev/input/`-Devices und findet jene, die den konfigurierten Key unterstützen. Wirft `RuntimeError` wenn kein passendes Device gefunden wird.
- `listen()` startet pro gefundenem Device einen `asyncio.Task` mit `async_read_loop()`. Dadurch werden alle Tastaturen parallel überwacht — wichtig bei Multi-Device-Setups.
- `stop()` cancelt alle Tasks und schließt Devices.
### PynputHotkeyListener (Windows/Fallback)
Datei: [`hotkey/_pynput.py`](../../whisper_local/hotkey/_pynput.py)
- `_evdev_to_pynput_key()` übersetzt evdev-Namen (`KEY_F12`) ins pynput-Format (`Key.f12`). Unterstützt F-Tasten per Regex und direkte Attribut-Zuordnung.
- **Key-Repeat-Unterdrückung**: Das `_pressed`-Flag verhindert, dass OS-Key-Repeat mehrfache `on_press`-Callbacks auslöst. Erst beim echten `on_release` wird das Flag zurückgesetzt.
- pynput betreibt einen Thread; Callbacks werden via `call_soon_threadsafe` + `asyncio.ensure_future` in den Loop gebridged.
- `stop()` signalisiert über ein `asyncio.Event`; der Listener wird im `finally`-Block gestoppt.
## Inserter
**Protocol**: `Inserter` in [`inserter/__init__.py`](../../whisper_local/inserter/__init__.py)
**Methode**: `async insert(text: str)`
Beide Backends sichern den alten Clipboard-Inhalt, setzen den Text, senden `Ctrl+V`, und restaurieren das Clipboard im `finally`-Block. `PASTE_DELAY = 0.2s` nach dem Paste-Befehl.
### WaylandInserter (Linux)
Datei: [`inserter/_wayland.py`](../../whisper_local/inserter/_wayland.py)
- Clipboard-Backup via `wl-paste --no-newline`.
- Text setzen via `wl-copy -- <text>`.
- `Ctrl+V` simulieren via `ydotool key 29:1 47:1 47:0 29:0` (evdev Keycodes für Ctrl down, V down, V up, Ctrl up).
- Erfordert `ydotool`-Daemon und `input`-Gruppen-Mitgliedschaft.
### Win32Inserter (Windows)
Datei: [`inserter/_win32.py`](../../whisper_local/inserter/_win32.py)
- Clipboard-Operationen via `win32clipboard` (CF_UNICODETEXT).
- `Ctrl+V` via `pynput.keyboard.Controller`.
- Blocking-Aufrufe werden mit `asyncio.to_thread()` in Threads ausgelagert, um den Event-Loop nicht zu blockieren.
## MediaController
**Protocol**: `MediaController` in [`media/__init__.py`](../../whisper_local/media/__init__.py)
**Methoden**: `async pause()`, `async resume()`
`create_media_controller(enabled)` gibt `NoopController` zurück wenn `enabled=False` oder die Plattform nicht unterstützt wird.
### MprisController (Linux)
Datei: [`media/_mpris.py`](../../whisper_local/media/_mpris.py)
- Verbindet sich via `dbus-next` (`MessageBus().connect()`) zur D-Bus-Session.
- `pause()`: Listet alle `org.mpris.MediaPlayer2.*`-Services auf, pausiert nur Sessions im Status `Playing`, merkt sich pausierte Bus-Namen in `_paused`.
- `resume()`: Setzt nur die Sessions fort, die selbst pausiert wurden (`_paused`-Liste).
- **Circuit-Breaker**: `_bus_broken`-Flag deaktiviert den Controller dauerhaft bei D-Bus-Verbindungsfehler.
### SmtcController (Windows)
Datei: [`media/_smtc.py`](../../whisper_local/media/_smtc.py)
- Nutzt `winrt.windows.media.control.GlobalSystemMediaTransportControlsSessionManager`.
- `pause()`: Iteriert über alle Sessions, pausiert nur `PLAYING`-Sessions via `try_pause_async()`, merkt sich AUMIDs in `_paused`.
- `resume()`: Holt aktuelle Sessions, spielt nur die in `_paused` gelisteten via `try_play_async()` wieder ab. Überspringt Sessions, die nicht mehr existieren.
- **Circuit-Breaker**: `_broken`-Flag bei SMTC-Initialisierungsfehler.
### NoopController
Datei: [`media/_noop.py`](../../whisper_local/media/_noop.py)
- `pause()` und `resume()` sind No-Ops. Wird verwendet wenn das Feature deaktiviert ist oder die Plattform nicht unterstützt wird.
## Mikrofon-Monitor
**Protocol**: `MicrophoneMonitor` in [`microphone/__init__.py`](../../whisper_local/microphone/__init__.py)
**Callbacks**: `on_device_added`, `on_device_removed`, `on_configured_missing` (alle `Awaitable`)
**Methoden**: `async start()`, `stop()`
### PollMonitor (Cross-Platform)
Datei: [`microphone/_poll.py`](../../whisper_local/microphone/_poll.py)
- Pollt `sounddevice.query_devices()` alle 2,5 s, vergleicht mit `_known_devices`.
- `start()` prüft sofort ob das konfigurierte Gerät vorhanden ist und ruft `on_configured_missing()` auf wenn nicht.
- Bei Fehler beim Abfragen der Geräte wird der bisherige Stand beibehalten (kein Crash).
### Win32Monitor (Windows)
Datei: [`microphone/_win32.py`](../../whisper_local/microphone/_win32.py)
- Definiert COM-Interfaces (`IMMNotificationClient`, `IMMDeviceEnumerator`) via `comtypes` zur Laufzeit.
- Registriert einen `IMMNotificationClient`-Callback bei der Core Audio API. COM-Events werden via `call_soon_threadsafe` in den asyncio-Loop delegiert.
- **Polling-Fallback**: Wenn COM-Initialisierung fehlschlägt, wird automatisch ein `PollMonitor` als Fallback gestartet.
- `stop()` deregistriert den COM-Callback und ruft `CoUninitialize()` auf.
### Geräte-Erkennungslogik
Alle Monitors verwenden dieselbe Logik: `sd.query_devices()` filtern auf `max_input_channels > 0` (Eingabegeräte) und extrahieren Gerätenamen. Der Vergleich erfolgt über Set-Differenzen (`added = current - known`, `removed = known - current`).
+15
View File
@@ -0,0 +1,15 @@
---
okf_version: "0.1"
---
# Files
- [whisper-local — Schnellstart](quickstart.md) - Lokales Push-to-Talk Speech-to-Text-Tool mit faster-whisper. Hotkey halten, sprechen, loslassen — Text wird ins aktive Textfeld eingefügt. Kein Cloud-Service, kein Netzwerk.
# Directories
- [architecture](architecture/)
- [domain](domain/)
- [operations](operations/)
- [testing](testing/)
- [workflows](workflows/)
+3
View File
@@ -0,0 +1,3 @@
# Files
- [Operations-Runbook](runbook.md) - Konfiguration, Autostart, Build/Packaging, CI/CD und Versionsverwaltung für whisper-local. Praktische Anleitungen für Betrieb und Release.
+110
View File
@@ -0,0 +1,110 @@
---
type: Runbook
title: Operations-Runbook
description: Konfiguration, Autostart, Build/Packaging, CI/CD und Versionsverwaltung für whisper-local. Praktische Anleitungen für Betrieb und Release.
tags: [operations, runbook, config, autostart, build, packaging, ci, versioning]
---
# Operations-Runbook
## Konfiguration
Die Konfiguration liegt als TOML-Datei:
- **Linux**: `~/.config/whisper-local/config.toml`
- **Windows**: `%APPDATA%\whisper-local\config.toml`
Die Datei wird beim ersten Start automatisch mit Defaults angelegt. Vorlage: [`config.example.toml`](../../config.example.toml).
### Sektionen
| Sektion | Schlüssel | Default | Beschreibung |
|---|---|---|---|
| `[hotkey]` | `key` | `KEY_F12` | Hotkey im evdev-Format — auch unter Windows |
| `[whisper]` | `model` | `small` | faster-whisper Modellname |
| `[whisper]` | `language` | `de` | Sprache für Transkription |
| `[whisper]` | `compute_type` | `int8` | Compute-Typ (CPU-optimiert) |
| `[audio]` | `sample_rate` | `16000` | Abtastrate in Hz |
| `[audio]` | `channels` | `1` | Anzahl Kanäle (Mono) |
| `[audio]` | `min_duration` | `0.5` | Mindestdauer in Sekunden — kürzere Aufnahmen werden verworfen |
| `[audio]` | `device` | `""` | Mikrofon-Gerätename (leer = Standardgerät) |
| `[media]` | `pause_during_recording` | `true` | Medien während Aufnahme pausieren |
### Config-Quellcode
[`config.py`](../../whisper_local/config.py) enthält `Config`-Dataclass, `load_config()` und `save_config()`. TOML-Strings werden via `_toml_str()` escaped (Backslash und Quotes), um ungültiges TOML bei Sonderzeichen zu verhindern.
### Einstellungs-Dialog
Rechtsklick auf das Tray-Icon → „Einstellungen". Der Dialog ([`_settings.py`](../../whisper_local/tray/_settings.py)) bietet:
- **Hotkey-Aufzeichnung**: Taste drücken → evdev-Name wird erfasst. Unter Windows zusätzlich Konflikt-Erkennung via Win32 `RegisterHotKey`.
- **Mikrofon-Auswahl**: Dropdown aller Eingabegeräte via `sounddevice.query_devices()`.
- **Medien-Pause**: Checkbox für `pause_during_recording`.
Änderungen greifen sofort via `_on_config_reload()` — kein Neustart erforderlich. Siehe [Aufnahme-Zyklus → Config-Reload](../workflows/recording-cycle.md#config-reload-zur-laufzeit).
## Autostart
### Linux (systemd user unit)
```bash
mkdir -p ~/.config/systemd/user
cp systemd/whisper-local.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now whisper-local.service
```
Das `whisper-local`-Executable muss in `~/.local/bin` verfügbar sein (`uv tool install .` oder Pfad in der Unit anpassen). Die Unit startet nach `graphical-session.target` und restartet bei Fehlern.
### Windows
Kein integrierter Autostart-Mechanismus. Möglichkeit: Verknüpfung zur `whisper-local.exe` im Autostart-Ordner (`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`).
## Build und Packaging (Windows)
### PyInstaller-Build
```powershell
.\build.ps1 # Build + ZIP
.\build.ps1 -Clean # dist/ und build/ löschen, dann Build
.\build.ps1 -SkipBuild # Nur ZIP aus bestehendem dist/ erstellen
```
`build.ps1` liest die Version aus `pyproject.toml`, führt `uv sync --group build` aus, startet PyInstaller mit `whisper_local.spec` und erstellt ein versioniertes ZIP (`whisper-local-v{version}-win64.zip`).
Die PyInstaller-Spec ([`whisper_local.spec`](../../whisper_local.spec)) bündelt:
- **ctranslate2**-DLLs (Haupt-DLL, cuDNN, Intel OpenMP)
- **pywin32**-System-DLLs (`pythoncom313.dll`, `pywintypes313.dll`)
- **PortAudio**-Binaries für `sounddevice`
- **onnxruntime** (für Silero VAD in faster-whisper)
- **av/FFmpeg**-DLLs (per Glob gesammelt)
- **sv_ttk** Theme-Daten, **Silero VAD** ONNX-Modell
Im gebündelten Modus (`sys.frozen`) cacht `Transcriber._model_cache_dir()` das Whisper-Modell neben der EXE im `models/`-Verzeichnis (portabel).
## CI/CD
### OpenWiki-Aktualisierung
`.github/workflows/openwiki-update.yml` führt täglich um 08:00 Uhr (`cron: "0 8 * * *"`) oder manuell (`workflow_dispatch`) ein OpenWiki-Doku-Update durch. Der Workflow:
1. Installiert OpenWiki via `npm install --global openwiki`.
2. Führt `openwiki code --update --print` aus (mit OpenRouter als Provider).
3. Erstellt einen Pull-Request via `peter-evans/create-pull-request@v7` auf Branch `openwiki/update`.
Erforderliche Secrets: `OPENROUTER_API_KEY`, `LANGSMITH_API_KEY`.
## Versionsverwaltung
Version steht in `pyproject.toml` (`version = "1.3.0"`). Der [bump-version-Skill](../../.claude/skills/bump-version/SKILL.md) automatisiert SemVer-Bumps:
```bash
uv version --bump patch # 1.3.0 → 1.3.1
uv version --bump minor # 1.3.0 → 1.4.0
uv version --bump major # 1.3.0 → 2.0.0
uv version 2.0.0 # explizite Version
```
Nach dem Bump wird ein Git-Commit und Tag erstellt. Der Skill ist für Claude-Code-Sessions konzipiert und fragt bei Commit-Anfragen, ob die Version mit aktualisiert werden soll.
+72
View File
@@ -0,0 +1,72 @@
---
type: Quickstart
title: whisper-local — Schnellstart
description: Lokales Push-to-Talk Speech-to-Text-Tool mit faster-whisper. Hotkey halten, sprechen, loslassen — Text wird ins aktive Textfeld eingefügt. Kein Cloud-Service, kein Netzwerk.
tags: [speech-to-text, whisper, push-to-talk, desktop, python]
---
# whisper-local — Schnellstart
**whisper-local** ist ein lokales Speech-to-Text-Tool für **Linux (KDE Plasma / Wayland)** und **Windows 10/11**. Ein systemweiter Hotkey (Standard: `F12`) wird gehalten, um Audio aufzunehmen; beim Loslassen transkribiert [faster-whisper](https://github.com/SYSTRAN/faster-whisper) lokal und der Text wird ins aktive Textfeld eingefügt. Die App läuft als Hintergrunddienst mit Tray-Icon.
## Installation
Voraussetzung: [uv](https://github.com/astral-sh/uv) (Python-Paketmanager), Python ≥ 3.13.
```bash
git clone <repo-url> whisper-local
cd whisper-local
uv sync
```
### Linux-Systemabhängigkeiten
```bash
sudo pacman -S ydotool wl-clipboard libayatana-appindicator gobject-introspection
sudo systemctl enable --now ydotool
sudo usermod -aG input $USER # Neu-Login nötig
```
`ydotool` simuliert Tastatureingaben unter Wayland, `wl-clipboard` ermöglicht Clipboard-Zugriff, `libayatana-appindicator` + `gobject-introspection` stellen das Tray-Icon bereit.
Windows benötigt keine zusätzlichen System-Dependencies.
## Starten
```bash
uv run whisper-local
```
Beim ersten Start lädt faster-whisper das Whisper-Modell (Standard: `small`) herunter — ein Fortschrittsdialog erscheint. Danach ist der Hotkey aktiv und das Tray-Icon zeigt den Zustand an (grau = warten, rot = aufnehmen, gelb = transkribieren).
## Konfiguration
Die Konfigurationsdatei wird beim ersten Start automatisch angelegt:
- **Linux**: `~/.config/whisper-local/config.toml`
- **Windows**: `%APPDATA%\whisper-local\config.toml`
Vorlage: [`config.example.toml`](../../config.example.toml). Hotkey und Mikrofon lassen sich auch über den Einstellungs-Dialog im Tray-Icon ändern (Rechtsklick → „Einstellungen"). Änderungen greifen sofort ohne Neustart. Siehe [Operations-Runbook](operations/runbook.md) für Details.
## Entwicklung
```bash
uv run pytest # Tests
uv run python -m whisper_local # Direkt starten
```
## Wiki-Übersicht
| Bereich | Seite | Inhalt |
|---|---|---|
| Architektur | [Architektur-Übersicht](architecture/overview.md) | Factory-Pattern, Protocol-Interfaces, Plattform-Dispatch, Modul-Graph |
| Architektur | [Source-Map](architecture/source-map.md) | Verzeichnisstruktur, Datei-Referenzen |
| Workflows | [Aufnahme-Zyklus](workflows/recording-cycle.md) | Push-to-Talk-Flow, Media-Pause, Mikrofon-Überwachung |
| Domain | [Plattform-Abstraktionen](domain/platform-abstractions.md) | Hotkey, Inserter, Media, Microphone — Backends je Plattform |
| Operations | [Runbook](operations/runbook.md) | Konfiguration, Autostart, Build/Packaging, CI, Versionsverwaltung |
| Testing | [Test-Leitfaden](testing/guide.md) | pytest-Setup, plattformspezifische Tests, Mocking-Muster |
## Backlog
- **Linux-Inserter unter X11**: Aktuell nur Wayland (`wl-copy` + `ydotool`). X11 wird nicht unterstützt. — *Grund: Projekt zielt auf KDE/Wayland ab.*
- **Silero VAD-Integration**: faster-whisper enthält Silero VAD, wird aber nicht explizit konfiguriert. — *Grund: Keine Quell-Referenz gefunden; nur in PyInstaller-Spec als Bundle-Dependency sichtbar.*
+105
View File
@@ -0,0 +1,105 @@
---
type: TestingGuide
title: Test-Leitfaden
description: pytest-Setup für whisper-local. Plattformspezifische Tests via skipif, AsyncMock für asyncio-Callbacks, Mocking-Muster für sounddevice und faster-whisper.
tags: [testing, pytest, asyncio, mocking, platform-specific]
---
# Test-Leitfaden
## Setup
```bash
uv run pytest # alle Tests
uv run pytest -v # verbose
uv run pytest tests/test_main.py # einzelne Datei
```
Dev-Dependencies: `pytest>=8.0.0`, `pytest-asyncio>=0.24.0` (in `pyproject.toml` unter `[dependency-groups] dev`).
## Plattformspezifische Tests
Plattformabhängige Tests werden mit `@pytest.mark.skipif` übersprungen:
```python
import sys
import pytest
@pytest.mark.skipif(sys.platform != "linux", reason="nur Linux")
def test_evdev_listener():
...
@pytest.mark.skipif(sys.platform != "win32", reason="nur Windows")
def test_pynput_listener():
...
```
Dieses Muster wird durchgehend angewendet — z.B. in `test_hotkey.py` (evdev vs. pynput), `test_media_mpris.py` (Linux), `test_media_smtc.py` (Windows).
## Async-Tests
Async-Tests verwenden `pytest-asyncio`:
```python
@pytest.mark.asyncio
async def test_on_press_starts_recording():
app = App(config=Config())
app.on_press = AsyncMock()
await app.on_press()
...
```
Async-Callbacks (`on_press`, `on_release`, Mikrofon-Monitor-Callbacks) werden mit `unittest.mock.AsyncMock` gemockt.
## Mocking-Muster
### sounddevice
`Recorder` und `PollMonitor` verwenden `sounddevice`. In Tests wird `sd.InputStream` bzw. `sd.query_devices` gemockt:
```python
@patch("whisper_local.recorder.sd.InputStream")
def test_recorder_start(mock_stream):
recorder = Recorder()
recorder.start()
mock_stream.assert_called_once()
```
### faster-whisper
`Transcriber` wird mit einem gemockten `WhisperModel` instanziiert, um Modell-Downloads zu vermeiden:
```python
def test_transcriber(mock_model):
transcriber = Transcriber(model=mock_model, language="de")
...
```
Der `Transcriber` akzeptiert ein optionales `model`-Argument, das in Tests genutzt wird — ein expliziter Design-Entscheidung (Commit `3a58099`).
### Windows COM / winrt
`test_media_smtc.py` sichert `winrt`-Imports plattformabsichern ab. `test_main.py` mockt `create_monitor`, um COM-Abhängigkeiten zu umgehen.
## Test-Übersicht
| Test-Datei | Fokus | Plattform |
|---|---|---|
| `test_config.py` | Config laden/speichern, Defaults, TOML-Escaping | alle |
| `test_recorder.py` | Recorder start/stop, min_duration-Filter | alle |
| `test_transcriber.py` | Transcriber mit gemocktem Model | alle |
| `test_hotkey.py` | Beide Backends, Key-Repeat-Unterdrückung | je `skipif` |
| `test_inserter.py` | Wayland + Win32, Clipboard-Restaurierung | je `skipif` |
| `test_media_factory.py` | Factory-Dispatch | alle |
| `test_media_mpris.py` | MprisController, Circuit-Breaker | Linux |
| `test_media_smtc.py` | SmtcController, Circuit-Breaker | Windows |
| `test_microphone_monitor.py` | PollMonitor, Start-Check | alle |
| `test_tray.py` | Tray-App, AppState, Icon, Settings | alle |
| `test_download_progress.py` | Download-Dialog | alle |
| `test_main.py` | App-Integration: Zyklus, Config-Reload, Mikrofon | alle |
## Wichtige Test-Konventionen
- **Mocks statt echter Hardware**: Kein echtes Mikrofon, keine echte Tastatur, kein D-Bus, kein SMTC in Tests.
- **`create_monitor` mocken in `test_main.py`**: Verhindert COM-Initialisierung auf Nicht-Windows-Plattformen (Commit `1f8adc5`).
- **Recorder vor Ersatz stoppen**: `test_main.py` stellt sicher, dass der Recorder gestoppt wird, bevor er durch Config-Reload ersetzt wird (Commit `1f8adc5`).
+3
View File
@@ -0,0 +1,3 @@
# Files
- [Test-Leitfaden](guide.md) - pytest-Setup für whisper-local. Plattformspezifische Tests via skipif, AsyncMock für asyncio-Callbacks, Mocking-Muster für sounddevice und faster-whisper.
+3
View File
@@ -0,0 +1,3 @@
# Files
- [Aufnahme-Zyklus (Push-to-Talk)](recording-cycle.md) - Der Kern-Workflow von whisper-local: Hotkey drücken → Audio aufnehmen → Medien pausieren → Hotkey loslassen → transkribieren → Text einfügen → Medien fortsetzen. Sowie Mikrofon-Überwachung und Config-Reload.
+71
View File
@@ -0,0 +1,71 @@
---
type: Workflow
title: Aufnahme-Zyklus (Push-to-Talk)
description: "Der Kern-Workflow von whisper-local: Hotkey drücken → Audio aufnehmen → Medien pausieren → Hotkey loslassen → transkribieren → Text einfügen → Medien fortsetzen. Sowie Mikrofon-Überwachung und Config-Reload."
tags: [workflow, push-to-talk, recording, transcription, media-pause, microphone-monitor]
---
# Aufnahme-Zyklus (Push-to-Talk)
Der zentrale Workflow wird durch die `App`-Klasse in [`__main__.py`](../../whisper_local/__main__.py) orchestriert. Die beiden Callbacks `on_press` und `on_release` bilden den vollständigen Zyklus.
## Push-to-Talk-Flow
```
Hotkey gedrückt (on_press)
1. Tray.set_state(RECORDING) — Icon wird rot
2. await media.pause() — alle Playing-Sessions pausieren
3. recorder.start() — sounddevice.InputStream öffnet
Hotkey losgelassen (on_release)
1. recorder.stop() — Stream schließen, Audio-Array zurück
2. await media.resume() — pausierte Sessions fortsetzen (im finally-Block)
3. Wenn audio == None → Tray WAITING, return
4. Tray.set_state(TRANSCRIBING) — Icon wird gelb
5. transcriber.transcribe(audio) — faster-whisper transkribiert lokal
6. await inserter.insert(text) — Text ins aktive Textfeld einfügen
7. Tray.set_state(WAITING) — Icon wird grau
```
### Mindestdauer-Filter
`Recorder.stop()` verwirft Aufnahmen kürzer als `min_duration` (Standard: 0,5 s). In diesem Fall gibt `stop()` `None` zurück und der Zyklus wird abgebrochen — keine Transkription, kein Einfügen. Das verhindert versehentliche Trigger bei kurzen Hotkey-Berührungen.
### Text-Einfügung mit Clipboard-Restaurierung
Beide Inserter-Backends (Wayland und Win32) folgen demselben Muster:
1. Alten Clipboard-Inhalt sichern.
2. Transkribierten Text ins Clipboard setzen.
3. `Ctrl+V` simulieren (ydotool unter Wayland, pynput unter Windows).
4. Kurz warten (`PASTE_DELAY = 0.2s`).
5. Alten Clipboard-Inhalt wiederherstellen (im `finally`-Block).
Dadurch bleibt der bisherige Clipboard-Inhalt des Nutzers erhalten. Details siehe [Plattform-Abstraktionen → Inserter](../domain/platform-abstractions.md#inserter).
## Medien-Pause während Aufnahme
Wenn `pause_media_during_recording = true` (Standard), pausiert der [MediaController](../domain/platform-abstractions.md#mediacontroller) alle aktiv wiedergebenden Sessions bei Aufnahmestart und setzt nur diejenigen fort, die er selbst pausiert hat. Der Circuit-Breaker deaktiviert die Funktion dauerhaft, wenn D-Bus (Linux) oder SMTC (Windows) nicht verfügbar ist.
## Mikrofon-Überwachung
Der `MicrophoneMonitor` läuft als Hintergrund-Task und überwacht Audio-Geräte-Änderungen:
- **Gerät entfernt**: Wenn das konfigurierte Mikrofon getrennt wird, wechselt `App._on_configured_microphone_missing` auf das Standard-Gerät, zeigt eine Desktop-Benachrichtigung via [notify-py](../../whisper_local/tray/_notification.py) und setzt eine Tray-Warnung (`set_warning("Mikrofon nicht gefunden")`).
- **Gerät hinzugefügt**: Wenn das konfigurierte Mikrofon wieder verfügbar wird, stellt `App._on_microphone_added` es wieder her, benachrichtigt den Nutzer und entfernt die Tray-Warnung.
- **Start-Check**: Beim Start meldet `PollMonitor` sofort, wenn das konfigurierte Gerät nicht gefunden wird.
Windows verwendet `IMMNotificationClient` (COM Core Audio API) für Event-basierte Benachrichtigungen mit automatischem Polling-Fallback. Linux verwendet den `PollMonitor` (2,5 s Intervall). Details siehe [Plattform-Abstraktionen → Mikrofon-Monitor](../domain/platform-abstractions.md#mikrofon-monitor).
## Config-Reload zur Laufzeit
`App._on_config_reload` wird vom [Einstellungs-Dialog](../../whisper_local/tray/_settings.py) aufgerufen und tauscht alle dynamischen Komponenten ohne Neustart aus:
1. Neue Config übernehmen.
2. Recorder neu instanziieren (neues Mikrofon/Sample-Rate).
3. Mikrofon-Monitor stoppen und neu starten.
4. Tray-Warnung zurücksetzen.
5. Alten Media-Controller fortsetzen, neuen erstellen.
6. Hotkey-Listener stoppen und neu starten (`_restart_hotkey`).
Der Hotkey-Restart benötigt ein kurzes `await asyncio.sleep(0.1)` zwischen Stop und Start, um den alten Listener sauber abzubauen.