bead04ff09
Entfernt den Windows-only-Guard in App.__init__, damit der Dialog mit indeterminatem ttk.Progressbar auch unter Linux erscheint, wenn das Laden länger als 500 ms dauert. Ersetzt das literale \u2026 im Label durch das Zeichen … und passt Spec/Plan an den tatsächlichen Umsetzungsstand an (Timeout-basierter Wartebalken statt tqdm-Monkey-Patch, da die Xet-Engine Python-tqdm bypasst). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
202 lines
7.0 KiB
Markdown
202 lines
7.0 KiB
Markdown
# Modell-Lade-Wartebalken — Implementierungsplan
|
||
|
||
**Status:** Umgesetzt (Stand 2026-04-16)
|
||
**Zuletzt geändert:** 2026-04-16
|
||
|
||
**Goal:** Beim ersten Start zeigt die App einen tkinter-Dialog mit indeterminatem Wartebalken, der erscheint, wenn das Laden des Whisper-Modells länger als 500 ms dauert. Bei schnell bereitem Modell erscheint kein Dialog.
|
||
|
||
**Architecture:** Ein Daemon-Thread lädt `WhisperModel`. Der Hauptthread wartet 500 ms via `threading.Event`; erst bei Timeout öffnet er einen tkinter-Dialog mit indeterminatem `ttk.Progressbar`, der via `root.after(100, poll)` auf `done_event.is_set()` pollt. Ein früher geplanter tqdm-Monkey-Patch (`TkProgressTqdm`) wurde verworfen, weil `huggingface_hub` für `model.bin` die Xet-Rust-Engine nutzt, die Python-tqdm bypasst.
|
||
|
||
**Tech Stack:** Python 3.13+, tkinter, `faster_whisper`, `threading`, `queue` (historisch), `tqdm` (historisch)
|
||
|
||
---
|
||
|
||
## Dateien
|
||
|
||
| Datei | Status | Verantwortlichkeit |
|
||
|-------|--------|-------------------|
|
||
| `whisper_local/tray/_download_progress.py` | Umgesetzt | `TkProgressTqdm` (Fallback, ungenutzt), `load_model_with_progress()` |
|
||
| `whisper_local/transcriber.py` | Umgesetzt | Optionaler `model`-Parameter in `Transcriber.__init__` |
|
||
| `whisper_local/__main__.py` | Umgesetzt | Aufruf von `load_model_with_progress` (plattformübergreifend) |
|
||
| `tests/test_download_progress.py` | Umgesetzt | Unit-Tests für `TkProgressTqdm` |
|
||
| `tests/test_transcriber.py` | Umgesetzt | Test für neuen `model`-Parameter + Signatur-Korrektur |
|
||
|
||
---
|
||
|
||
## Historie der Umsetzung
|
||
|
||
Die Implementierung erfolgte in zwei Phasen:
|
||
|
||
1. **Initialer Plan (April 2026):** tqdm-Monkey-Patch mit determinater Progressbar, Dateiname- und Prozentanzeige (Commits `e92f5f5`, `3067499`, `44c8d8e`, `c26dfa3`, `3d9f95b`, `e31230f`).
|
||
2. **Korrektur (Commit `753dbc5`):** Monkey-Patch verworfen, da Xet-Engine tqdm bypasst. Ersetzt durch Timeout-basierten indeterminaten Wartebalken.
|
||
|
||
Die Tasks unten beschreiben den umgesetzten Endzustand.
|
||
|
||
---
|
||
|
||
## Task 1: `Transcriber` um optionalen `model`-Parameter erweitern *(erledigt)*
|
||
|
||
**Files:**
|
||
- `whisper_local/transcriber.py`
|
||
- `tests/test_transcriber.py`
|
||
|
||
`Transcriber.__init__` akzeptiert `model: WhisperModel | None = None`. Wenn gesetzt, wird `WhisperModel` nicht selbst instanziiert. Test `test_init_with_preloaded_model` prüft diesen Pfad; `test_init_loads_model` wurde an die neue Signatur mit `download_root=None` angepasst.
|
||
|
||
---
|
||
|
||
## Task 2: `TkProgressTqdm` implementieren und testen *(erledigt, aber ungenutzt)*
|
||
|
||
**Files:**
|
||
- `whisper_local/tray/_download_progress.py`
|
||
- `tests/test_download_progress.py`
|
||
|
||
Klasse `TkProgressTqdm` erbt von `tqdm.tqdm`, akkumuliert Fortschritt in `_accumulated_n` und schreibt `{"file", "n", "total"}`-Dicts in `TkProgressTqdm._queue`, falls gesetzt. Unit-Tests decken Queue-Weiterleitung, Null-Queue-Fall und Akkumulation ab.
|
||
|
||
**Hinweis:** `load_model_with_progress` nutzt die Klasse nicht mehr (Xet-Bypass, siehe Historie). Die Klasse + Tests bleiben als dokumentierter Fallback erhalten — wird `faster_whisper` irgendwann wieder durch Python-tqdm laufen, kann sie reaktiviert werden.
|
||
|
||
---
|
||
|
||
## Task 3: `load_model_with_progress` implementieren *(erledigt, Ansatz geändert)*
|
||
|
||
**Files:**
|
||
- `whisper_local/tray/_download_progress.py`
|
||
|
||
Die finale Version nutzt **kein** tqdm-Patching. Stattdessen:
|
||
|
||
```python
|
||
def load_model_with_progress(
|
||
model_name: str,
|
||
compute_type: str,
|
||
download_root: str | None,
|
||
) -> Any:
|
||
import tkinter as tk
|
||
from tkinter import messagebox, ttk
|
||
|
||
from faster_whisper import WhisperModel
|
||
from whisper_local.tray._theme import apply_system_theme
|
||
|
||
result: list[Any] = [None]
|
||
error: list[BaseException | None] = [None]
|
||
done_event = threading.Event()
|
||
|
||
def worker() -> None:
|
||
try:
|
||
result[0] = WhisperModel(
|
||
model_name, compute_type=compute_type, download_root=download_root
|
||
)
|
||
except Exception as exc:
|
||
error[0] = exc
|
||
finally:
|
||
done_event.set()
|
||
|
||
thread = threading.Thread(target=worker, daemon=True)
|
||
thread.start()
|
||
|
||
# Kurz warten – schneller Cache-Hit überspringt Dialog
|
||
if done_event.wait(timeout=0.5):
|
||
if error[0] is not None:
|
||
root = tk.Tk()
|
||
root.withdraw()
|
||
messagebox.showerror("Fehler beim Modell-Laden", str(error[0]))
|
||
root.destroy()
|
||
sys.exit(1)
|
||
return result[0]
|
||
|
||
# Dialog mit indeterminatem Wartebalken
|
||
root = tk.Tk()
|
||
root.title("whisper-local – Modell wird geladen")
|
||
root.resizable(False, False)
|
||
apply_system_theme(root)
|
||
|
||
frame = ttk.Frame(root, padding=16)
|
||
frame.pack(fill=tk.BOTH, expand=True)
|
||
ttk.Label(frame, text=f"Lade Whisper-Modell '{model_name}'...").pack(anchor=tk.W)
|
||
ttk.Label(frame, text="Bitte warten\u2026", foreground="gray").pack(
|
||
anchor=tk.W, pady=(4, 8)
|
||
)
|
||
|
||
pb = ttk.Progressbar(frame, length=320, mode="indeterminate")
|
||
pb.pack(fill=tk.X)
|
||
pb.start(10)
|
||
|
||
def poll() -> None:
|
||
if done_event.is_set():
|
||
root.quit()
|
||
return
|
||
root.after(100, poll)
|
||
|
||
root.after(100, poll)
|
||
root.mainloop()
|
||
|
||
if error[0] is not None:
|
||
root.withdraw()
|
||
messagebox.showerror("Fehler beim Modell-Laden", str(error[0]))
|
||
root.destroy()
|
||
sys.exit(1)
|
||
|
||
root.destroy()
|
||
return result[0]
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: `App.__init__` in `__main__.py` anpassen *(erledigt)*
|
||
|
||
**Files:**
|
||
- `whisper_local/__main__.py`
|
||
|
||
Plattformübergreifend — der anfängliche `sys.platform == "win32"`-Guard wurde entfernt, weil der Dialog auch auf Linux gewünscht ist und tkinter zur Standardbibliothek gehört.
|
||
|
||
```python
|
||
from whisper_local.tray._download_progress import load_model_with_progress
|
||
from whisper_local.transcriber import _model_cache_dir
|
||
_preloaded_model = load_model_with_progress(
|
||
model_name=config.whisper_model,
|
||
compute_type=config.compute_type,
|
||
download_root=_model_cache_dir(),
|
||
)
|
||
|
||
self.transcriber = Transcriber(
|
||
model_name=config.whisper_model,
|
||
compute_type=config.compute_type,
|
||
language=config.language,
|
||
model=_preloaded_model,
|
||
)
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: Manueller Test *(empfohlen nach Regressionen)*
|
||
|
||
- [ ] **Schritt 1: Modell-Cache temporär umbenennen (Download erzwingen)**
|
||
|
||
```bash
|
||
# Zeigt den Cache-Pfad
|
||
uv run python -c "from whisper_local.transcriber import _model_cache_dir; print(_model_cache_dir())"
|
||
```
|
||
|
||
Modell-Ordner umbenennen (`models` → `models_bak`).
|
||
|
||
- [ ] **Schritt 2: App starten und Dialog beobachten**
|
||
|
||
```
|
||
uv run whisper-local
|
||
```
|
||
|
||
Erwartet:
|
||
- Fenster mit Label `"Lade Whisper-Modell 'small'..."` + `"Bitte warten…"` erscheint nach ca. 500 ms
|
||
- Indeterminater Wartebalken animiert durchlaufend
|
||
- Dialog schließt automatisch nach Abschluss des Downloads, Tray-Icon erscheint
|
||
|
||
- [ ] **Schritt 3: Neustart mit gecachtem Modell**
|
||
|
||
```
|
||
uv run whisper-local
|
||
```
|
||
|
||
Erwartet: **kein Dialog** erscheint — App startet direkt mit Tray-Icon (Modell < 500 ms bereit).
|
||
|
||
- [ ] **Schritt 4: Cache wiederherstellen**
|
||
|
||
`models_bak` zurück zu `models` benennen.
|