Files
whisper-local/docs/superpowers/plans/2026-04-12-model-download-progress-dialog.md
T
info bead04ff09 feat(tray): Modell-Lade-Wartebalken plattformübergreifend anzeigen
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>
2026-04-16 20:58:21 +02:00

202 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.