Files
whisper-local/docs/superpowers/plans/2026-04-12-model-download-progress-dialog.md
T

202 lines
7.0 KiB
Markdown
Raw Normal View History

# 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.