Files
whisper-local/docs/superpowers/plans/2026-04-12-model-download-progress-dialog.md
T
2026-04-12 12:24:34 +02:00

13 KiB
Raw Blame History

Model-Download-Fortschrittsdialog — Implementierungsplan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Beim ersten Start zeigt die App einen tkinter-Dialog mit Progressbar, der den echten Byte-Fortschritt des Whisper-Modell-Downloads anzeigt. Bei gecachtem Modell erscheint kein Dialog.

Architecture: tqdm.tqdm wird vor dem WhisperModel()-Aufruf durch eine eigene Klasse (TkProgressTqdm) ersetzt, die Fortschrittsdaten thread-safe via queue.Queue an den Hauptthread weiterleitet. Der Hauptthread betreibt einen tkinter-Dialog, der sich erst bei der ersten Download-Meldung zeigt. Nach Abschluss kehrt load_model_with_progress() mit dem fertigen WhisperModel zurück.

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


Dateien

Datei Änderungstyp Verantwortlichkeit
whisper_local/tray/_download_progress.py Neu TkProgressTqdm, DownloadProgressDialog-Logik, load_model_with_progress()
whisper_local/transcriber.py Geändert Optionaler model-Parameter in Transcriber.__init__
whisper_local/__main__.py Geändert Plattform-Guard + Aufruf von load_model_with_progress
tests/test_download_progress.py Neu Unit-Tests für TkProgressTqdm
tests/test_transcriber.py Geändert Test für neuen model-Parameter + Signatur-Korrektur

Task 1: Transcriber um optionalen model-Parameter erweitern

Files:

  • Modify: whisper_local/transcriber.py

  • Modify: tests/test_transcriber.py

  • Schritt 1: Failing-Test für neuen model-Parameter schreiben

In tests/test_transcriber.py folgenden Test ergänzen:

def test_init_with_preloaded_model():
    mock_model = MagicMock()
    t = Transcriber(language="de", model=mock_model)
    assert t.model is mock_model
    assert t.language == "de"
  • Schritt 2: Test ausführen — erwartet FAIL
uv run pytest tests/test_transcriber.py::TestTranscriber::test_init_with_preloaded_model -v

Erwartete Ausgabe: FAILEDTypeError: __init__() got an unexpected keyword argument 'model'

  • Schritt 3: Bestehenden test_init_loads_model-Test korrigieren

Der Test prüft derzeit assert_called_once_with("small", compute_type="int8"), übergeht aber download_root=None. Ersetzen durch:

@patch("whisper_local.transcriber.WhisperModel")
def test_init_loads_model(self, mock_model_class):
    t = Transcriber(model_name="small", compute_type="int8", language="de")
    mock_model_class.assert_called_once_with("small", compute_type="int8", download_root=None)
    assert t.language == "de"
  • Schritt 4: Transcriber.__init__ um optionalen model-Parameter erweitern

whisper_local/transcriber.py__init__-Signatur und Body ersetzen:

from faster_whisper import WhisperModel

class Transcriber:
    def __init__(
        self,
        model_name: str = "small",
        compute_type: str = "int8",
        language: str = "de",
        model: WhisperModel | None = None,
    ):
        self.language = language
        if model is not None:
            self.model = model
        else:
            logger.info("Lade Whisper-Modell '%s' (compute_type=%s)...", model_name, compute_type)
            self.model = WhisperModel(
                model_name, compute_type=compute_type, download_root=_model_cache_dir()
            )
            logger.info("Modell geladen")
  • Schritt 5: Alle Transcriber-Tests ausführen — erwartet PASS
uv run pytest tests/test_transcriber.py -v

Erwartete Ausgabe: alle Tests PASSED

  • Schritt 6: Commit
git add whisper_local/transcriber.py tests/test_transcriber.py
git commit -m "feat: Transcriber akzeptiert optionales vorgeladenes WhisperModel"

Task 2: TkProgressTqdm implementieren und testen

Files:

  • Create: whisper_local/tray/_download_progress.py

  • Create: tests/test_download_progress.py

  • Schritt 1: Test-Datei mit Failing-Tests anlegen

tests/test_download_progress.py:

import queue
from unittest.mock import MagicMock, patch

import tqdm as tqdm_module

from whisper_local.tray._download_progress import TkProgressTqdm


class TestTkProgressTqdm:
    def test_update_puts_message_in_queue(self):
        q = queue.Queue()
        TkProgressTqdm._queue = q

        bar = TkProgressTqdm(total=1000, desc="model.bin", disable=True)
        bar.update(300)
        bar.close()

        TkProgressTqdm._queue = None

        msg = q.get_nowait()
        assert msg["file"] == "model.bin"
        assert msg["n"] == 300
        assert msg["total"] == 1000

    def test_update_without_queue_does_not_raise(self):
        TkProgressTqdm._queue = None
        bar = TkProgressTqdm(total=100, desc="test.bin", disable=True)
        bar.update(50)  # darf nicht crashen
        bar.close()

    def test_multiple_updates_accumulate(self):
        q = queue.Queue()
        TkProgressTqdm._queue = q

        bar = TkProgressTqdm(total=1000, desc="file.bin", disable=True)
        bar.update(200)
        bar.update(300)
        bar.close()

        TkProgressTqdm._queue = None

        msgs = []
        while not q.empty():
            msgs.append(q.get_nowait())

        assert len(msgs) == 2
        assert msgs[0]["n"] == 200
        assert msgs[1]["n"] == 500  # tqdm akkumuliert: 200+300
  • Schritt 2: Tests ausführen — erwartet FAIL
uv run pytest tests/test_download_progress.py -v

Erwartete Ausgabe: ERROR / ImportError — Modul existiert noch nicht

  • Schritt 3: _download_progress.py mit TkProgressTqdm anlegen

whisper_local/tray/_download_progress.py:

"""Download-Fortschrittsdialog für den ersten Whisper-Modell-Download (Windows)."""

from __future__ import annotations

import queue
import sys
import threading
from typing import Any

import tqdm as tqdm_module


class TkProgressTqdm(tqdm_module.tqdm):
    """tqdm-Ersatz, der Fortschritts-Updates thread-safe in eine Queue schreibt."""

    _queue: queue.Queue | None = None

    def update(self, n: int = 1) -> bool | None:
        result = super().update(n)
        if self._queue is not None:
            self._queue.put({
                "file": self.desc or "",
                "n": self.n,
                "total": self.total or 0,
            })
        return result
  • Schritt 4: Tests ausführen — erwartet PASS
uv run pytest tests/test_download_progress.py -v

Erwartete Ausgabe: alle 3 Tests PASSED

  • Schritt 5: Commit
git add whisper_local/tray/_download_progress.py tests/test_download_progress.py
git commit -m "feat: TkProgressTqdm leitet tqdm-Fortschritt an Queue weiter"

Task 3: load_model_with_progress implementieren

Files:

  • Modify: whisper_local/tray/_download_progress.py

  • Schritt 1: load_model_with_progress an _download_progress.py anhängen

Folgenden Code nach der TkProgressTqdm-Klasse einfügen:

def load_model_with_progress(
    model_name: str,
    compute_type: str,
    download_root: str | None,
) -> Any:
    """Lädt WhisperModel — zeigt bei Bedarf einen Download-Fortschrittsdialog.

    Wenn das Modell bereits gecacht ist (kein tqdm-Update kommt), erscheint
    kein Dialog. Auf Download-Fehler wird ein Fehlerdialog gezeigt und sys.exit(1)
    aufgerufen.
    """
    import tkinter as tk
    from tkinter import messagebox, ttk

    from faster_whisper import WhisperModel

    from whisper_local.tray._theme import apply_system_theme

    q: queue.Queue[dict[str, Any] | None] = queue.Queue()
    result: list[WhisperModel | None] = [None]
    error: list[BaseException | None] = [None]
    original_tqdm = tqdm_module.tqdm

    def worker() -> None:
        TkProgressTqdm._queue = q
        tqdm_module.tqdm = TkProgressTqdm
        try:
            result[0] = WhisperModel(
                model_name,
                compute_type=compute_type,
                download_root=download_root,
            )
        except Exception as exc:
            error[0] = exc
        finally:
            tqdm_module.tqdm = original_tqdm
            TkProgressTqdm._queue = None
            q.put(None)  # Sentinel: signalisiert Fertigstellung

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

    # --- tkinter-Dialog (lazy: erscheint nur bei echtem Download) ---
    root = tk.Tk()
    root.withdraw()  # zunächst versteckt
    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)

    file_var = tk.StringVar(value="")
    ttk.Label(frame, textvariable=file_var, foreground="gray").pack(anchor=tk.W, pady=(4, 8))

    progress_var = tk.DoubleVar(value=0.0)
    ttk.Progressbar(
        frame, variable=progress_var, maximum=100, length=320, mode="determinate"
    ).pack(fill=tk.X)

    pct_var = tk.StringVar(value="0 %")
    ttk.Label(frame, textvariable=pct_var).pack(anchor=tk.E, pady=(2, 0))

    def poll() -> None:
        try:
            while True:
                msg = q.get_nowait()
                if msg is None:  # Sentinel → fertig
                    root.quit()
                    return
                # Erste echte Meldung → Dialog anzeigen
                if not root.winfo_viewable():
                    root.deiconify()
                # UI aktualisieren
                file_var.set(msg["file"])
                if msg["total"] > 0:
                    pct = 100.0 * msg["n"] / msg["total"]
                    progress_var.set(pct)
                    pct_var.set(f"{pct:.0f} %")
        except queue.Empty:
            pass
        root.after(50, poll)

    root.after(50, poll)
    root.mainloop()
    root.destroy()

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

    return result[0]
  • Schritt 2: Vorhandene Tests noch laufen lassen
uv run pytest tests/test_download_progress.py tests/test_transcriber.py -v

Erwartete Ausgabe: alle Tests PASSED

  • Schritt 3: Commit
git add whisper_local/tray/_download_progress.py
git commit -m "feat: load_model_with_progress mit tkinter-Fortschrittsdialog"

Task 4: App.__init__ in __main__.py anpassen

Files:

  • Modify: whisper_local/__main__.py

  • Schritt 1: Plattform-Guard und Modell-Vorladen einfügen

In whisper_local/__main__.py die App.__init__-Methode anpassen.

Vorher (Zeile 3337):

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

Nachher — vollständiger Ersatz des Blocks:

if sys.platform == "win32":
    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(),
    )
else:
    _preloaded_model = None

self.transcriber = Transcriber(
    model_name=config.whisper_model,
    compute_type=config.compute_type,
    language=config.language,
    model=_preloaded_model,
)
  • Schritt 2: Alle Tests ausführen
uv run pytest -v

Erwartete Ausgabe: alle Tests PASSED

  • Schritt 3: Commit
git add whisper_local/__main__.py
git commit -m "feat: App lädt Whisper-Modell auf Windows mit Fortschrittsdialog"

Task 5: Manueller Test

Dieser Schritt kann nicht automatisiert werden, da er eine laufende GUI erfordert.

  • Schritt 1: Modell-Cache temporär umbenennen (Download erzwingen)

Finde den Cache-Pfad (Standard: %APPDATA%\whisper-local\models\ oder HuggingFace-Cache):

# Zeigt den Cache-Pfad
uv run python -c "from whisper_local.transcriber import _model_cache_dir; print(_model_cache_dir())"

Benenne den Modell-Ordner um (z.B. modelsmodels_bak), damit beim Start ein Download ausgelöst wird.

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

Erwartet:

  • Kleines Fenster erscheint mit Label "Lade Whisper-Modell 'small'..."

  • Dateiname wechselt während des Downloads (z.B. model.bin, tokenizer.json)

  • Progressbar füllt sich von 0 % auf 100 %

  • Dialog schließt sich automatisch, Tray-Icon erscheint

  • Schritt 3: Neustart mit gecachtem Modell

uv run whisper-local

Erwartet: kein Dialog erscheint — App startet direkt mit Tray-Icon.

  • Schritt 4: Cache wiederherstellen

Benenne models_bak zurück zu models.