Files
whisper-local/docs/superpowers/specs/2026-04-12-model-download-progress-dialog-design.md
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

106 lines
5.0 KiB
Markdown
Raw Permalink 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.
# Design: Modell-Lade-Wartebalken
**Datum:** 2026-04-12
**Zuletzt geändert:** 2026-04-16
**Status:** Umgesetzt
## 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 Statusanzeige.
## Ziel
Ein kleiner tkinter-Dialog erscheint automatisch, wenn das Laden des Modells länger als 500 ms dauert, und zeigt einen animierten Wartebalken. Ist das Modell bereits vollständig gecacht und wird in unter 500 ms bereit, erscheint kein Dialog.
## Verworfener Ansatz: tqdm-Monkey-Patch
Die ursprüngliche Idee war, `tqdm.tqdm` während des Downloads mit einer eigenen Klasse (`TkProgressTqdm`) zu ersetzen und echten Byte-Fortschritt anzuzeigen. **Dieser Ansatz funktioniert nicht mehr**, weil `huggingface_hub` für große Dateien (`model.bin`) die **Xet-Engine (Rust)** nutzt, die Python-`tqdm` komplett bypasst. Der Dialog blieb dadurch leer, bis der Download fertig war.
Die Klasse `TkProgressTqdm` existiert noch im Code (mit Unit-Tests), wird aber von der Lade-Funktion nicht mehr aktiviert — sie bleibt als möglicher Fallback erhalten, falls zukünftige `faster_whisper`-Versionen wieder durch Python-tqdm laufen.
## Aktueller Ansatz: Timeout-basierter Wartebalken
1. Worker-Thread startet `WhisperModel(...)`.
2. Hauptthread wartet via `threading.Event.wait(timeout=0.5)`.
3. Ist der Thread innerhalb von 500 ms fertig (vollständiger Cache) → kein Dialog, direkter Rückweg.
4. Sonst → tkinter-Dialog mit **indeterminatem** `ttk.Progressbar` öffnen, bis der Worker signalisiert.
## Architektur & Ablauf
```
main()
└─ App.__init__()
├─ load_model_with_progress(model_name, compute_type, download_root)
│ ├─ [Daemon-Thread] WhisperModel(...)
│ └─ [Hauptthread] done_event.wait(0.5)
│ ├─ sofort fertig → Modell zurückgeben (kein Dialog)
│ └─ Timeout → tkinter-Fenster + Indeterminate-Progressbar
│ └─ root.after(100, poll) bis done_event.is_set()
└─ Transcriber(model=fertig_geladenes_modell)
```
Dialog läuft plattformübergreifend (Linux + Windows) — tkinter gehört zur Standardbibliothek.
## Komponenten
### Datei: `whisper_local/tray/_download_progress.py`
**`TkProgressTqdm`** — ungenutzter `tqdm.tqdm`-Ersatz (historischer Fallback):
- Erbt von `tqdm.tqdm`
- Akkumuliert Fortschritt und schreibt Messages in `TkProgressTqdm._queue`, falls gesetzt
- Wird derzeit nicht aktiviert (Xet bypasst tqdm)
**`load_model_with_progress(model_name, compute_type, download_root) -> WhisperModel`** — öffentliche Funktion:
1. Startet Daemon-Thread mit `WhisperModel(...)`
2. Wartet 500 ms via `done_event.wait(timeout=0.5)`
3. Kehrt bei schnellem Abschluss direkt zurück
4. Öffnet sonst tkinter-Fenster mit indeterminatem `ttk.Progressbar`
5. Pollt `done_event` alle 100 ms via `root.after` und beendet `mainloop()` bei Signal
6. Bei Exception im Worker: `messagebox.showerror` + `sys.exit(1)`
### Datei: `whisper_local/__main__.py`
`App.__init__()` ruft `load_model_with_progress()` auf (plattformunabhängig) und übergibt das fertige `WhisperModel`-Objekt an `Transcriber`.
### Datei: `whisper_local/transcriber.py`
`Transcriber.__init__()` akzeptiert optional ein bereits geladenes `WhisperModel`-Objekt via Parameter `model: WhisperModel | None = None`.
## Dialog-Darstellung
- Titel: `"whisper-local Modell wird geladen"`
- Label oben: `"Lade Whisper-Modell '<model_name>'..."`
- Label darunter: `"Bitte warten…"` (grau)
- `ttk.Progressbar` im `indeterminate`-Modus (Länge 320 px, via `pb.start(10)` animiert)
- Kein Abbrechen-Button
- `apply_system_theme(root)` für konsistentes Aussehen
- `root.resizable(False, False)`
## Threading-Modell
| Thread | Aufgabe |
|--------|---------|
| Hauptthread | `done_event.wait(0.5)`, tkinter `mainloop()`, Polling via `root.after` |
| Daemon-Thread | `WhisperModel(...)` |
Kommunikation über `threading.Event` (kein Monkey-Patch, keine Queue in der Lade-Funktion).
## Fehlerbehandlung
- **Kein Internet / Download-Fehler:** Exception im Thread → nach `done_event.set()` im Hauptthread geprüft → `messagebox.showerror("Fehler beim Modell-Laden", str(exc))``sys.exit(1)`
- **Modell bereits gecacht und schnell bereit:** `done_event.wait(0.5)` returnt `True` → kein Dialog erscheint
## Plattform
Kein Plattform-Guard — `load_model_with_progress()` läuft auf Linux und Windows identisch. tkinter ist Teil der Python-Standardbibliothek und auf beiden Systemen verfügbar.
## Dateien
| Datei | Status |
|-------|--------|
| `whisper_local/tray/_download_progress.py` | Umgesetzt (Timeout-basierter Wartebalken) |
| `whisper_local/__main__.py` | Umgesetzt (Preload plattformübergreifend) |
| `whisper_local/transcriber.py` | Umgesetzt (optionaler `model`-Parameter) |
| `tests/test_download_progress.py` | Vorhanden (deckt ungenutzte `TkProgressTqdm` ab) |
| `tests/test_transcriber.py` | Vorhanden (deckt `model`-Parameter ab) |