Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
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: FAILED — TypeError: __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 optionalenmodel-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.pymitTkProgressTqdmanlegen
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_progressan_download_progress.pyanhä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 33–37):
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. models → models_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.