# Design: Model-Download-Fortschrittsdialog **Datum:** 2026-04-12 **Status:** Genehmigt ## Problemstellung Beim ersten Start lädt `faster_whisper` das Whisper-Modell von HuggingFace herunter (z.B. `small` ≈ 460 MB). Dieser Vorgang blockiert `App.__init__()` ohne jede Rückmeldung. Der Nutzer sieht einen eingefrorenen Start ohne Fortschrittsanzeige. ## Ziel Ein kleiner tkinter-Dialog erscheint automatisch während des Downloads und zeigt den echten Byte-Fortschritt an. Ist das Modell bereits gecacht, erscheint kein Dialog. ## Ansatz: tqdm-Monkey-Patch `faster_whisper` nutzt intern `tqdm` für Download-Fortschritt. Wir ersetzen `tqdm.tqdm` kurzzeitig mit einer eigenen Klasse (`TkProgressTqdm`), die Fortschrittsdaten thread-safe in eine Queue schreibt. Der Hauptthread liest die Queue und aktualisiert den tkinter-Dialog. ## Architektur & Ablauf ``` main() └─ App.__init__() ├─ [Windows] load_model_with_progress(model_name, compute_type, download_root) │ ├─ [Daemon-Thread] WhisperModel(...) mit gepatchtem tqdm │ └─ [Hauptthread] tkinter-Dialog pollt Queue via root.after(50, poll) │ └─ Sentinel in Queue → Dialog schließt sich, WhisperModel zurückgegeben └─ Transcriber(model=fertig_geladenes_modell) ``` Auf Linux lädt `Transcriber` das Modell direkt wie bisher (kein Dialog). ## Komponenten ### Neue Datei: `whisper_local/tray/_download_progress.py` **`TkProgressTqdm`** — ersetzt `tqdm.tqdm` während des Downloads: - Erbt von `tqdm.tqdm` (damit `faster_whisper` keine TypeError bekommt) - Überschreibt `update(n)` und schreibt `{"file": desc, "n": n, "total": total}` in eine `queue.Queue` - Thread-safe, kein direkter tkinter-Zugriff aus dem Thread **`DownloadProgressDialog`** — tkinter-Fenster: - Titel: `"whisper-local – Modell wird geladen"` - Label oben: `"Lade Whisper-Modell ''..."` - Label Mitte: aktueller Dateiname (z.B. `model.bin`) - `ttk.Progressbar` im `determinate`-Modus (0–100 %) - Prozentzahl als Text daneben - Kein Abbrechen-Button - `apply_system_theme(root)` für konsistentes Aussehen - `root.resizable(False, False)` **`load_model_with_progress(model_name, compute_type, download_root) -> WhisperModel`** — öffentliche Funktion: 1. Erstellt `queue.Queue` und startet Daemon-Thread mit `WhisperModel(...)` 2. Öffnet tkinter-Fenster erst wenn erste tqdm-Meldung ankommt (kein leerer Dialog bei gecachtem Modell) 3. Pollt Queue via `root.after(50, ...)` und aktualisiert Dialog 4. Bei Sentinel (`None` in Queue): Dialog schließen, `WhisperModel` zurückgeben 5. Bei Exception in Queue: `messagebox.showerror` + `sys.exit(1)` ### Geänderte Datei: `whisper_local/__main__.py` `App.__init__()` ruft auf Windows `load_model_with_progress()` auf und übergibt das fertige `WhisperModel`-Objekt an `Transcriber`. ### Geänderte Datei: `whisper_local/transcriber.py` `Transcriber.__init__()` akzeptiert optional ein bereits geladenes `WhisperModel`-Objekt via Parameter `model: WhisperModel | None = None`. Falls übergeben, wird es direkt verwendet statt eines neuen Downloads. ## Threading-Modell | Thread | Aufgabe | |--------|---------| | Hauptthread | tkinter `mainloop()`, Queue-Polling via `root.after` | | Daemon-Thread | `WhisperModel(...)` mit gepatchtem tqdm | Der tqdm-Patch ist lokal: vor dem Thread-Start wird `tqdm.tqdm` ersetzt, nach Thread-Ende wiederhergestellt (auch bei Fehler, via `try/finally`). ## Fehlerbehandlung - **Kein Internet / Download-Fehler:** Exception im Thread → via Queue in Hauptthread → `messagebox.showerror("Fehler beim Modell-Download", str(exc))` → `sys.exit(1)` - **Modell bereits gecacht:** tqdm wird nie aufgerufen → kein Dialog erscheint → `load_model_with_progress` kehrt sofort zurück ## Plattform-Abgrenzung ```python # whisper_local/__main__.py if sys.platform == "win32": from whisper_local.tray._download_progress import load_model_with_progress model = load_model_with_progress(...) else: model = None # Transcriber lädt selbst ``` ## Dateien | Datei | Änderungstyp | |-------|-------------| | `whisper_local/tray/_download_progress.py` | Neu | | `whisper_local/__main__.py` | Geändert (App.__init__) | | `whisper_local/transcriber.py` | Geändert (optionaler model-Parameter) |