mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-19 23:00:45 -07:00
feat: add download cancel/clear UI, fix whisper-large and error reporting
- Add cancel (X) button on downloading and errored model items - Add collapsible Problems panel (VS Code-style) showing error details - Add "Clear All" button to reset all stale download/error state - Add POST /models/download/cancel endpoint to dismiss individual downloads - Add POST /tasks/clear endpoint to reset all task and progress state - Include error messages in /tasks/active response for visibility - Capture SSE error messages client-side for immediate display - Fix whisper-large using wrong HF repo (openai/whisper-large → openai/whisper-large-v3) - Fix Whisper HF repo mapping in both PyTorch and MLX backends - Shorten error toast to point users to Problems panel instead of wall of text
This commit is contained in:
@@ -379,9 +379,17 @@ class MLXTTSBackend:
|
||||
return audio, sample_rate
|
||||
|
||||
|
||||
WHISPER_HF_REPOS = {
|
||||
"base": "openai/whisper-base",
|
||||
"small": "openai/whisper-small",
|
||||
"medium": "openai/whisper-medium",
|
||||
"large": "openai/whisper-large-v3",
|
||||
}
|
||||
|
||||
|
||||
class MLXSTTBackend:
|
||||
"""MLX-based STT backend using mlx-audio Whisper."""
|
||||
|
||||
|
||||
def __init__(self, model_size: str = "base"):
|
||||
self.model = None
|
||||
self.model_size = model_size
|
||||
@@ -402,8 +410,8 @@ class MLXSTTBackend:
|
||||
"""
|
||||
try:
|
||||
from huggingface_hub import constants as hf_constants
|
||||
model_name = f"openai/whisper-{model_size}"
|
||||
repo_cache = Path(hf_constants.HF_HUB_CACHE) / ("models--" + model_name.replace("/", "--"))
|
||||
hf_repo = WHISPER_HF_REPOS.get(model_size, f"openai/whisper-{model_size}")
|
||||
repo_cache = Path(hf_constants.HF_HUB_CACHE) / ("models--" + hf_repo.replace("/", "--"))
|
||||
|
||||
if not repo_cache.exists():
|
||||
return False
|
||||
@@ -474,7 +482,7 @@ class MLXSTTBackend:
|
||||
from mlx_audio.stt import load
|
||||
|
||||
# MLX Whisper uses the standard OpenAI models
|
||||
model_name = f"openai/whisper-{model_size}"
|
||||
model_name = WHISPER_HF_REPOS.get(model_size, f"openai/whisper-{model_size}")
|
||||
|
||||
print(f"Loading MLX Whisper model {model_size}...")
|
||||
|
||||
|
||||
@@ -369,9 +369,17 @@ class PyTorchTTSBackend:
|
||||
return audio, sample_rate
|
||||
|
||||
|
||||
WHISPER_HF_REPOS = {
|
||||
"base": "openai/whisper-base",
|
||||
"small": "openai/whisper-small",
|
||||
"medium": "openai/whisper-medium",
|
||||
"large": "openai/whisper-large-v3",
|
||||
}
|
||||
|
||||
|
||||
class PyTorchSTTBackend:
|
||||
"""PyTorch-based STT backend using Whisper."""
|
||||
|
||||
|
||||
def __init__(self, model_size: str = "base"):
|
||||
self.model = None
|
||||
self.processor = None
|
||||
@@ -416,18 +424,18 @@ class PyTorchSTTBackend:
|
||||
"""
|
||||
try:
|
||||
from huggingface_hub import constants as hf_constants
|
||||
model_name = f"openai/whisper-{model_size}"
|
||||
repo_cache = Path(hf_constants.HF_HUB_CACHE) / ("models--" + model_name.replace("/", "--"))
|
||||
|
||||
hf_repo = WHISPER_HF_REPOS.get(model_size, f"openai/whisper-{model_size}")
|
||||
repo_cache = Path(hf_constants.HF_HUB_CACHE) / ("models--" + hf_repo.replace("/", "--"))
|
||||
|
||||
if not repo_cache.exists():
|
||||
return False
|
||||
|
||||
|
||||
# Check for .incomplete files - if any exist, download is still in progress
|
||||
blobs_dir = repo_cache / "blobs"
|
||||
if blobs_dir.exists() and any(blobs_dir.glob("*.incomplete")):
|
||||
print(f"[_is_model_cached] Found .incomplete files for whisper-{model_size}, treating as not cached")
|
||||
return False
|
||||
|
||||
|
||||
# Check that actual model weight files exist in snapshots
|
||||
snapshots_dir = repo_cache / "snapshots"
|
||||
if snapshots_dir.exists():
|
||||
@@ -438,12 +446,12 @@ class PyTorchSTTBackend:
|
||||
if not has_weights:
|
||||
print(f"[_is_model_cached] No model weights found for whisper-{model_size}, treating as not cached")
|
||||
return False
|
||||
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[_is_model_cached] Error checking cache for whisper-{model_size}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
async def load_model_async(self, model_size: Optional[str] = None):
|
||||
"""
|
||||
Lazy load the Whisper model.
|
||||
@@ -494,7 +502,7 @@ class PyTorchSTTBackend:
|
||||
# Import transformers
|
||||
from transformers import WhisperProcessor, WhisperForConditionalGeneration
|
||||
|
||||
model_name = f"openai/whisper-{model_size}"
|
||||
model_name = WHISPER_HF_REPOS.get(model_size, f"openai/whisper-{model_size}")
|
||||
print(f"[DEBUG] Model name: {model_name}")
|
||||
|
||||
print(f"Loading Whisper model {model_size} on {self.device}...")
|
||||
|
||||
+45
-4
@@ -1310,14 +1310,14 @@ async def get_model_status():
|
||||
whisper_base_id = "openai/whisper-base"
|
||||
whisper_small_id = "openai/whisper-small"
|
||||
whisper_medium_id = "openai/whisper-medium"
|
||||
whisper_large_id = "openai/whisper-large"
|
||||
whisper_large_id = "openai/whisper-large-v3"
|
||||
else:
|
||||
tts_1_7b_id = "Qwen/Qwen3-TTS-12Hz-1.7B-Base"
|
||||
tts_0_6b_id = "Qwen/Qwen3-TTS-12Hz-0.6B-Base"
|
||||
whisper_base_id = "openai/whisper-base"
|
||||
whisper_small_id = "openai/whisper-small"
|
||||
whisper_medium_id = "openai/whisper-medium"
|
||||
whisper_large_id = "openai/whisper-large"
|
||||
whisper_large_id = "openai/whisper-large-v3"
|
||||
|
||||
model_configs = [
|
||||
{
|
||||
@@ -1586,6 +1586,39 @@ async def trigger_model_download(request: models.ModelDownloadRequest):
|
||||
return {"message": f"Model {request.model_name} download started"}
|
||||
|
||||
|
||||
@app.post("/models/download/cancel")
|
||||
async def cancel_model_download(request: models.ModelDownloadRequest):
|
||||
"""Cancel or dismiss an errored/stale download task."""
|
||||
task_manager = get_task_manager()
|
||||
progress_manager = get_progress_manager()
|
||||
|
||||
removed = task_manager.cancel_download(request.model_name)
|
||||
|
||||
# Also clear progress state so the model doesn't show as downloading
|
||||
with progress_manager._lock:
|
||||
if request.model_name in progress_manager._progress:
|
||||
del progress_manager._progress[request.model_name]
|
||||
|
||||
return {"message": f"Download task for {request.model_name} cancelled"}
|
||||
|
||||
|
||||
@app.post("/tasks/clear")
|
||||
async def clear_all_tasks():
|
||||
"""Clear all download tasks and progress state. Does not delete downloaded files."""
|
||||
task_manager = get_task_manager()
|
||||
progress_manager = get_progress_manager()
|
||||
|
||||
task_manager._active_downloads.clear()
|
||||
task_manager._active_generations.clear()
|
||||
|
||||
with progress_manager._lock:
|
||||
progress_manager._progress.clear()
|
||||
progress_manager._last_notify_time.clear()
|
||||
progress_manager._last_notify_progress.clear()
|
||||
|
||||
return {"message": "All task state cleared"}
|
||||
|
||||
|
||||
@app.delete("/models/{model_name}")
|
||||
async def delete_model(model_name: str):
|
||||
"""Delete a downloaded model from the HuggingFace cache."""
|
||||
@@ -1621,12 +1654,12 @@ async def delete_model(model_name: str):
|
||||
"model_type": "whisper",
|
||||
},
|
||||
"whisper-large": {
|
||||
"hf_repo_id": "openai/whisper-large",
|
||||
"hf_repo_id": "openai/whisper-large-v3",
|
||||
"model_size": "large",
|
||||
"model_type": "whisper",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if model_name not in model_configs:
|
||||
raise HTTPException(status_code=400, detail=f"Unknown model: {model_name}")
|
||||
|
||||
@@ -1710,10 +1743,18 @@ async def get_active_tasks():
|
||||
progress = progress_map.get(model_name)
|
||||
|
||||
if task:
|
||||
# Prefer task error, fall back to progress manager error
|
||||
error = task.error
|
||||
if not error:
|
||||
with progress_manager._lock:
|
||||
pm_data = progress_manager._progress.get(model_name)
|
||||
if pm_data:
|
||||
error = pm_data.get("error")
|
||||
active_downloads.append(models.ActiveDownloadTask(
|
||||
model_name=model_name,
|
||||
status=task.status,
|
||||
started_at=task.started_at,
|
||||
error=error,
|
||||
))
|
||||
elif progress:
|
||||
# Progress exists but no task - create from progress data
|
||||
|
||||
@@ -154,6 +154,7 @@ class ActiveDownloadTask(BaseModel):
|
||||
model_name: str
|
||||
status: str
|
||||
started_at: datetime
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class ActiveGenerationTask(BaseModel):
|
||||
|
||||
@@ -72,6 +72,13 @@ class TaskManager:
|
||||
"""Get all active generations."""
|
||||
return list(self._active_generations.values())
|
||||
|
||||
def cancel_download(self, model_name: str) -> bool:
|
||||
"""Cancel/dismiss a download task (removes it from active list)."""
|
||||
if model_name in self._active_downloads:
|
||||
del self._active_downloads[model_name]
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_download_active(self, model_name: str) -> bool:
|
||||
"""Check if a download is active."""
|
||||
return model_name in self._active_downloads
|
||||
|
||||
Reference in New Issue
Block a user