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:
@@ -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.
|
||||||
@@ -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 -->
|
||||||
@@ -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.
|
**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 -->
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"updatedAt": "2026-07-23T12:38:28.858Z",
|
||||||
|
"command": "init",
|
||||||
|
"gitHead": "8abaf39a3a1c9fa61e2a96b154da0de5cc76c562",
|
||||||
|
"model": "glm-5.2"
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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) |
|
||||||
@@ -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.
|
||||||
@@ -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`).
|
||||||
@@ -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/)
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.*
|
||||||
@@ -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`).
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user