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

7.0 KiB
Raw Blame History

Modell-Lade-Wartebalken — Implementierungsplan

Status: Umgesetzt (Stand 2026-04-16) Zuletzt geändert: 2026-04-16

Goal: Beim ersten Start zeigt die App einen tkinter-Dialog mit indeterminatem Wartebalken, der erscheint, wenn das Laden des Whisper-Modells länger als 500 ms dauert. Bei schnell bereitem Modell erscheint kein Dialog.

Architecture: Ein Daemon-Thread lädt WhisperModel. Der Hauptthread wartet 500 ms via threading.Event; erst bei Timeout öffnet er einen tkinter-Dialog mit indeterminatem ttk.Progressbar, der via root.after(100, poll) auf done_event.is_set() pollt. Ein früher geplanter tqdm-Monkey-Patch (TkProgressTqdm) wurde verworfen, weil huggingface_hub für model.bin die Xet-Rust-Engine nutzt, die Python-tqdm bypasst.

Tech Stack: Python 3.13+, tkinter, faster_whisper, threading, queue (historisch), tqdm (historisch)


Dateien

Datei Status Verantwortlichkeit
whisper_local/tray/_download_progress.py Umgesetzt TkProgressTqdm (Fallback, ungenutzt), load_model_with_progress()
whisper_local/transcriber.py Umgesetzt Optionaler model-Parameter in Transcriber.__init__
whisper_local/__main__.py Umgesetzt Aufruf von load_model_with_progress (plattformübergreifend)
tests/test_download_progress.py Umgesetzt Unit-Tests für TkProgressTqdm
tests/test_transcriber.py Umgesetzt Test für neuen model-Parameter + Signatur-Korrektur

Historie der Umsetzung

Die Implementierung erfolgte in zwei Phasen:

  1. Initialer Plan (April 2026): tqdm-Monkey-Patch mit determinater Progressbar, Dateiname- und Prozentanzeige (Commits e92f5f5, 3067499, 44c8d8e, c26dfa3, 3d9f95b, e31230f).
  2. Korrektur (Commit 753dbc5): Monkey-Patch verworfen, da Xet-Engine tqdm bypasst. Ersetzt durch Timeout-basierten indeterminaten Wartebalken.

Die Tasks unten beschreiben den umgesetzten Endzustand.


Task 1: Transcriber um optionalen model-Parameter erweitern (erledigt)

Files:

  • whisper_local/transcriber.py
  • tests/test_transcriber.py

Transcriber.__init__ akzeptiert model: WhisperModel | None = None. Wenn gesetzt, wird WhisperModel nicht selbst instanziiert. Test test_init_with_preloaded_model prüft diesen Pfad; test_init_loads_model wurde an die neue Signatur mit download_root=None angepasst.


Task 2: TkProgressTqdm implementieren und testen (erledigt, aber ungenutzt)

Files:

  • whisper_local/tray/_download_progress.py
  • tests/test_download_progress.py

Klasse TkProgressTqdm erbt von tqdm.tqdm, akkumuliert Fortschritt in _accumulated_n und schreibt {"file", "n", "total"}-Dicts in TkProgressTqdm._queue, falls gesetzt. Unit-Tests decken Queue-Weiterleitung, Null-Queue-Fall und Akkumulation ab.

Hinweis: load_model_with_progress nutzt die Klasse nicht mehr (Xet-Bypass, siehe Historie). Die Klasse + Tests bleiben als dokumentierter Fallback erhalten — wird faster_whisper irgendwann wieder durch Python-tqdm laufen, kann sie reaktiviert werden.


Task 3: load_model_with_progress implementieren (erledigt, Ansatz geändert)

Files:

  • whisper_local/tray/_download_progress.py

Die finale Version nutzt kein tqdm-Patching. Stattdessen:

def load_model_with_progress(
    model_name: str,
    compute_type: str,
    download_root: str | None,
) -> Any:
    import tkinter as tk
    from tkinter import messagebox, ttk

    from faster_whisper import WhisperModel
    from whisper_local.tray._theme import apply_system_theme

    result: list[Any] = [None]
    error: list[BaseException | None] = [None]
    done_event = threading.Event()

    def worker() -> None:
        try:
            result[0] = WhisperModel(
                model_name, compute_type=compute_type, download_root=download_root
            )
        except Exception as exc:
            error[0] = exc
        finally:
            done_event.set()

    thread = threading.Thread(target=worker, daemon=True)
    thread.start()

    # Kurz warten  schneller Cache-Hit überspringt Dialog
    if done_event.wait(timeout=0.5):
        if error[0] is not None:
            root = tk.Tk()
            root.withdraw()
            messagebox.showerror("Fehler beim Modell-Laden", str(error[0]))
            root.destroy()
            sys.exit(1)
        return result[0]

    # Dialog mit indeterminatem Wartebalken
    root = tk.Tk()
    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)
    ttk.Label(frame, text="Bitte warten\u2026", foreground="gray").pack(
        anchor=tk.W, pady=(4, 8)
    )

    pb = ttk.Progressbar(frame, length=320, mode="indeterminate")
    pb.pack(fill=tk.X)
    pb.start(10)

    def poll() -> None:
        if done_event.is_set():
            root.quit()
            return
        root.after(100, poll)

    root.after(100, poll)
    root.mainloop()

    if error[0] is not None:
        root.withdraw()
        messagebox.showerror("Fehler beim Modell-Laden", str(error[0]))
        root.destroy()
        sys.exit(1)

    root.destroy()
    return result[0]

Task 4: App.__init__ in __main__.py anpassen (erledigt)

Files:

  • whisper_local/__main__.py

Plattformübergreifend — der anfängliche sys.platform == "win32"-Guard wurde entfernt, weil der Dialog auch auf Linux gewünscht ist und tkinter zur Standardbibliothek gehört.

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(),
)

self.transcriber = Transcriber(
    model_name=config.whisper_model,
    compute_type=config.compute_type,
    language=config.language,
    model=_preloaded_model,
)

Task 5: Manueller Test (empfohlen nach Regressionen)

  • Schritt 1: Modell-Cache temporär umbenennen (Download erzwingen)
# Zeigt den Cache-Pfad
uv run python -c "from whisper_local.transcriber import _model_cache_dir; print(_model_cache_dir())"

Modell-Ordner umbenennen (modelsmodels_bak).

  • Schritt 2: App starten und Dialog beobachten
uv run whisper-local

Erwartet:

  • Fenster mit Label "Lade Whisper-Modell 'small'..." + "Bitte warten…" erscheint nach ca. 500 ms

  • Indeterminater Wartebalken animiert durchlaufend

  • Dialog schließt automatisch nach Abschluss des Downloads, Tray-Icon erscheint

  • Schritt 3: Neustart mit gecachtem Modell

uv run whisper-local

Erwartet: kein Dialog erscheint — App startet direkt mit Tray-Icon (Modell < 500 ms bereit).

  • Schritt 4: Cache wiederherstellen

models_bak zurück zu models benennen.