bead04ff09
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>
106 lines
5.0 KiB
Markdown
106 lines
5.0 KiB
Markdown
# 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) |
|