# Model-Download-Fortschrittsdialog — Implementierungsplan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Beim ersten Start zeigt die App einen tkinter-Dialog mit Progressbar, der den echten Byte-Fortschritt des Whisper-Modell-Downloads anzeigt. Bei gecachtem Modell erscheint kein Dialog. **Architecture:** `tqdm.tqdm` wird vor dem `WhisperModel()`-Aufruf durch eine eigene Klasse (`TkProgressTqdm`) ersetzt, die Fortschrittsdaten thread-safe via `queue.Queue` an den Hauptthread weiterleitet. Der Hauptthread betreibt einen tkinter-Dialog, der sich erst bei der ersten Download-Meldung zeigt. Nach Abschluss kehrt `load_model_with_progress()` mit dem fertigen `WhisperModel` zurück. **Tech Stack:** Python 3.13+, tkinter, `tqdm`, `faster_whisper`, `threading`, `queue` --- ## Dateien | Datei | Änderungstyp | Verantwortlichkeit | |-------|-------------|-------------------| | `whisper_local/tray/_download_progress.py` | Neu | `TkProgressTqdm`, `DownloadProgressDialog`-Logik, `load_model_with_progress()` | | `whisper_local/transcriber.py` | Geändert | Optionaler `model`-Parameter in `Transcriber.__init__` | | `whisper_local/__main__.py` | Geändert | Plattform-Guard + Aufruf von `load_model_with_progress` | | `tests/test_download_progress.py` | Neu | Unit-Tests für `TkProgressTqdm` | | `tests/test_transcriber.py` | Geändert | Test für neuen `model`-Parameter + Signatur-Korrektur | --- ## Task 1: `Transcriber` um optionalen `model`-Parameter erweitern **Files:** - Modify: `whisper_local/transcriber.py` - Modify: `tests/test_transcriber.py` - [ ] **Schritt 1: Failing-Test für neuen `model`-Parameter schreiben** In `tests/test_transcriber.py` folgenden Test ergänzen: ```python def test_init_with_preloaded_model(): mock_model = MagicMock() t = Transcriber(language="de", model=mock_model) assert t.model is mock_model assert t.language == "de" ``` - [ ] **Schritt 2: Test ausführen — erwartet FAIL** ``` uv run pytest tests/test_transcriber.py::TestTranscriber::test_init_with_preloaded_model -v ``` Erwartete Ausgabe: `FAILED` — `TypeError: __init__() got an unexpected keyword argument 'model'` - [ ] **Schritt 3: Bestehenden `test_init_loads_model`-Test korrigieren** Der Test prüft derzeit `assert_called_once_with("small", compute_type="int8")`, übergeht aber `download_root=None`. Ersetzen durch: ```python @patch("whisper_local.transcriber.WhisperModel") def test_init_loads_model(self, mock_model_class): t = Transcriber(model_name="small", compute_type="int8", language="de") mock_model_class.assert_called_once_with("small", compute_type="int8", download_root=None) assert t.language == "de" ``` - [ ] **Schritt 4: `Transcriber.__init__` um optionalen `model`-Parameter erweitern** `whisper_local/transcriber.py` — `__init__`-Signatur und Body ersetzen: ```python from faster_whisper import WhisperModel class Transcriber: def __init__( self, model_name: str = "small", compute_type: str = "int8", language: str = "de", model: WhisperModel | None = None, ): self.language = language if model is not None: self.model = model else: logger.info("Lade Whisper-Modell '%s' (compute_type=%s)...", model_name, compute_type) self.model = WhisperModel( model_name, compute_type=compute_type, download_root=_model_cache_dir() ) logger.info("Modell geladen") ``` - [ ] **Schritt 5: Alle Transcriber-Tests ausführen — erwartet PASS** ``` uv run pytest tests/test_transcriber.py -v ``` Erwartete Ausgabe: alle Tests `PASSED` - [ ] **Schritt 6: Commit** ```bash git add whisper_local/transcriber.py tests/test_transcriber.py git commit -m "feat: Transcriber akzeptiert optionales vorgeladenes WhisperModel" ``` --- ## Task 2: `TkProgressTqdm` implementieren und testen **Files:** - Create: `whisper_local/tray/_download_progress.py` - Create: `tests/test_download_progress.py` - [ ] **Schritt 1: Test-Datei mit Failing-Tests anlegen** `tests/test_download_progress.py`: ```python import queue from unittest.mock import MagicMock, patch import tqdm as tqdm_module from whisper_local.tray._download_progress import TkProgressTqdm class TestTkProgressTqdm: def test_update_puts_message_in_queue(self): q = queue.Queue() TkProgressTqdm._queue = q bar = TkProgressTqdm(total=1000, desc="model.bin", disable=True) bar.update(300) bar.close() TkProgressTqdm._queue = None msg = q.get_nowait() assert msg["file"] == "model.bin" assert msg["n"] == 300 assert msg["total"] == 1000 def test_update_without_queue_does_not_raise(self): TkProgressTqdm._queue = None bar = TkProgressTqdm(total=100, desc="test.bin", disable=True) bar.update(50) # darf nicht crashen bar.close() def test_multiple_updates_accumulate(self): q = queue.Queue() TkProgressTqdm._queue = q bar = TkProgressTqdm(total=1000, desc="file.bin", disable=True) bar.update(200) bar.update(300) bar.close() TkProgressTqdm._queue = None msgs = [] while not q.empty(): msgs.append(q.get_nowait()) assert len(msgs) == 2 assert msgs[0]["n"] == 200 assert msgs[1]["n"] == 500 # tqdm akkumuliert: 200+300 ``` - [ ] **Schritt 2: Tests ausführen — erwartet FAIL** ``` uv run pytest tests/test_download_progress.py -v ``` Erwartete Ausgabe: `ERROR` / `ImportError` — Modul existiert noch nicht - [ ] **Schritt 3: `_download_progress.py` mit `TkProgressTqdm` anlegen** `whisper_local/tray/_download_progress.py`: ```python """Download-Fortschrittsdialog für den ersten Whisper-Modell-Download (Windows).""" from __future__ import annotations import queue import sys import threading from typing import Any import tqdm as tqdm_module class TkProgressTqdm(tqdm_module.tqdm): """tqdm-Ersatz, der Fortschritts-Updates thread-safe in eine Queue schreibt.""" _queue: queue.Queue | None = None def update(self, n: int = 1) -> bool | None: result = super().update(n) if self._queue is not None: self._queue.put({ "file": self.desc or "", "n": self.n, "total": self.total or 0, }) return result ``` - [ ] **Schritt 4: Tests ausführen — erwartet PASS** ``` uv run pytest tests/test_download_progress.py -v ``` Erwartete Ausgabe: alle 3 Tests `PASSED` - [ ] **Schritt 5: Commit** ```bash git add whisper_local/tray/_download_progress.py tests/test_download_progress.py git commit -m "feat: TkProgressTqdm leitet tqdm-Fortschritt an Queue weiter" ``` --- ## Task 3: `load_model_with_progress` implementieren **Files:** - Modify: `whisper_local/tray/_download_progress.py` - [ ] **Schritt 1: `load_model_with_progress` an `_download_progress.py` anhängen** Folgenden Code nach der `TkProgressTqdm`-Klasse einfügen: ```python def load_model_with_progress( model_name: str, compute_type: str, download_root: str | None, ) -> Any: """Lädt WhisperModel — zeigt bei Bedarf einen Download-Fortschrittsdialog. Wenn das Modell bereits gecacht ist (kein tqdm-Update kommt), erscheint kein Dialog. Auf Download-Fehler wird ein Fehlerdialog gezeigt und sys.exit(1) aufgerufen. """ import tkinter as tk from tkinter import messagebox, ttk from faster_whisper import WhisperModel from whisper_local.tray._theme import apply_system_theme q: queue.Queue[dict[str, Any] | None] = queue.Queue() result: list[WhisperModel | None] = [None] error: list[BaseException | None] = [None] original_tqdm = tqdm_module.tqdm def worker() -> None: TkProgressTqdm._queue = q tqdm_module.tqdm = TkProgressTqdm try: result[0] = WhisperModel( model_name, compute_type=compute_type, download_root=download_root, ) except Exception as exc: error[0] = exc finally: tqdm_module.tqdm = original_tqdm TkProgressTqdm._queue = None q.put(None) # Sentinel: signalisiert Fertigstellung thread = threading.Thread(target=worker, daemon=True) thread.start() # --- tkinter-Dialog (lazy: erscheint nur bei echtem Download) --- root = tk.Tk() root.withdraw() # zunächst versteckt 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) file_var = tk.StringVar(value="") ttk.Label(frame, textvariable=file_var, foreground="gray").pack(anchor=tk.W, pady=(4, 8)) progress_var = tk.DoubleVar(value=0.0) ttk.Progressbar( frame, variable=progress_var, maximum=100, length=320, mode="determinate" ).pack(fill=tk.X) pct_var = tk.StringVar(value="0 %") ttk.Label(frame, textvariable=pct_var).pack(anchor=tk.E, pady=(2, 0)) def poll() -> None: try: while True: msg = q.get_nowait() if msg is None: # Sentinel → fertig root.quit() return # Erste echte Meldung → Dialog anzeigen if not root.winfo_viewable(): root.deiconify() # UI aktualisieren file_var.set(msg["file"]) if msg["total"] > 0: pct = 100.0 * msg["n"] / msg["total"] progress_var.set(pct) pct_var.set(f"{pct:.0f} %") except queue.Empty: pass root.after(50, poll) root.after(50, poll) root.mainloop() root.destroy() if error[0] is not None: messagebox.showerror("Fehler beim Modell-Download", str(error[0])) sys.exit(1) return result[0] ``` - [ ] **Schritt 2: Vorhandene Tests noch laufen lassen** ``` uv run pytest tests/test_download_progress.py tests/test_transcriber.py -v ``` Erwartete Ausgabe: alle Tests `PASSED` - [ ] **Schritt 3: Commit** ```bash git add whisper_local/tray/_download_progress.py git commit -m "feat: load_model_with_progress mit tkinter-Fortschrittsdialog" ``` --- ## Task 4: `App.__init__` in `__main__.py` anpassen **Files:** - Modify: `whisper_local/__main__.py` - [ ] **Schritt 1: Plattform-Guard und Modell-Vorladen einfügen** In `whisper_local/__main__.py` die `App.__init__`-Methode anpassen. Vorher (Zeile 33–37): ```python self.transcriber = Transcriber( model_name=config.whisper_model, compute_type=config.compute_type, language=config.language, ) ``` Nachher — vollständiger Ersatz des Blocks: ```python if sys.platform == "win32": 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(), ) else: _preloaded_model = None self.transcriber = Transcriber( model_name=config.whisper_model, compute_type=config.compute_type, language=config.language, model=_preloaded_model, ) ``` - [ ] **Schritt 2: Alle Tests ausführen** ``` uv run pytest -v ``` Erwartete Ausgabe: alle Tests `PASSED` - [ ] **Schritt 3: Commit** ```bash git add whisper_local/__main__.py git commit -m "feat: App lädt Whisper-Modell auf Windows mit Fortschrittsdialog" ``` --- ## Task 5: Manueller Test *Dieser Schritt kann nicht automatisiert werden, da er eine laufende GUI erfordert.* - [ ] **Schritt 1: Modell-Cache temporär umbenennen (Download erzwingen)** Finde den Cache-Pfad (Standard: `%APPDATA%\whisper-local\models\` oder HuggingFace-Cache): ```bash # Zeigt den Cache-Pfad uv run python -c "from whisper_local.transcriber import _model_cache_dir; print(_model_cache_dir())" ``` Benenne den Modell-Ordner um (z.B. `models` → `models_bak`), damit beim Start ein Download ausgelöst wird. - [ ] **Schritt 2: App starten und Dialog beobachten** ``` uv run whisper-local ``` Erwartet: - Kleines Fenster erscheint mit Label `"Lade Whisper-Modell 'small'..."` - Dateiname wechselt während des Downloads (z.B. `model.bin`, `tokenizer.json`) - Progressbar füllt sich von 0 % auf 100 % - Dialog schließt sich automatisch, Tray-Icon erscheint - [ ] **Schritt 3: Neustart mit gecachtem Modell** ``` uv run whisper-local ``` Erwartet: **kein Dialog** erscheint — App startet direkt mit Tray-Icon. - [ ] **Schritt 4: Cache wiederherstellen** Benenne `models_bak` zurück zu `models`.