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