diff --git a/docs/superpowers/specs/2026-04-12-model-download-progress-dialog-design.md b/docs/superpowers/specs/2026-04-12-model-download-progress-dialog-design.md new file mode 100644 index 0000000..9d3305b --- /dev/null +++ b/docs/superpowers/specs/2026-04-12-model-download-progress-dialog-design.md @@ -0,0 +1,97 @@ +# 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) |