Files
BiliNote/backend/app/transcriber/model_download_state.py
huangjianwu 4a87c5b93b fix(transcriber): 下载失败时透传错误到前端并提示
issue #402 衍生问题:whisper 模型后台下载失败时,/transcriber_models_status
只回传 downloading/downloaded 两个布尔,failed 态被直接丢弃,于是前端表现为
「点了下载没反应、状态一直未下载、且无任何错误提示」。

后端:新增轻量模块 model_download_state 统一维护下载状态(downloading/done/
failed)与失败原因,config.py 的下载触发与状态查询共享同一份内存态;状态接口
新增 failed 字段,失败时附带 error(仓库 404、网络中断、本地路径缺 model.bin 等)。

前端:模型管理列表新增「下载失败」红色徽标 + 错误详情,按钮在失败后变为「重试」;
自定义模型项同样展示失败图标与原因;并对「本次新出现的失败」弹一次 toast 主动提示。

测试:新增 test_model_download_state 覆盖状态流转(downloading/done/failed、
失败原因透传、downloaded 覆盖 failed、重下清错、mlx key 隔离)。

已用 docker compose 启动整套栈验证:触发本地路径缺失与 HF 仓库 404 两种失败,
/transcriber_models_status 均正确回传 failed:true + error。

Refs #402

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 10:50:55 +08:00

76 lines
2.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""whisper / mlx 模型后台下载状态跟踪(含失败原因)。
routers.config 的「触发下载」与「查询状态」共享这份进程内内存态:
- keyfast-whisper 直接用 model_sizemlx 用 "mlx-{size}" 前缀(与历史一致)
- 状态downloading / done / failedfailed 时另存最近一次错误原因
为什么抽成独立的轻量模块(仅依赖 logger
1) 把原先散落在 config.py 多处的字符串状态赋值收敛到一处,避免拼写漂移;
2) 失败原因能透传到 /transcriber_models_status → 前端,修复「下载失败前端无任何
提示、状态一直显示未下载」issue #402 的衍生问题:原先状态接口只回传
downloading/downloaded 两个布尔failed 态被直接丢弃);
3) 不引入 faster_whisper / ctranslate2 等重依赖,可被单测隔离加载。
"""
from typing import Dict, Optional
from app.utils.logger import get_logger
logger = get_logger(__name__)
DOWNLOADING = "downloading"
DONE = "done"
FAILED = "failed"
# key -> 状态字符串key -> 最近一次失败原因(仅 failed 时有意义)
_status: Dict[str, str] = {}
_errors: Dict[str, str] = {}
def mark_downloading(key: str) -> None:
_status[key] = DOWNLOADING
_errors.pop(key, None) # 重新开始下载,清掉上一次的失败原因
def mark_done(key: str) -> None:
_status[key] = DONE
_errors.pop(key, None)
def mark_failed(key: str, error: str = "") -> None:
_status[key] = FAILED
if error:
_errors[key] = error
def get_status(key: str) -> Optional[str]:
return _status.get(key)
def is_downloading(key: str) -> bool:
return _status.get(key) == DOWNLOADING
def get_error(key: str) -> Optional[str]:
return _errors.get(key)
def status_row(name: str, downloaded: bool, key: Optional[str] = None) -> dict:
"""构造单个模型给前端的状态行downloaded / downloading / failed (+error)。
key 默认用 namemlx 传 "mlx-{size}"。已下载成功downloaded=True的模型
一律不回传 failed/error——避免「先失败后又下好」时残留旧的错误状态。
"""
k = key if key is not None else name
st = _status.get(k)
row: dict = {
"model_size": name,
"downloaded": downloaded,
"downloading": st == DOWNLOADING,
"failed": (not downloaded) and st == FAILED,
}
if row["failed"]:
err = _errors.get(k)
if err:
row["error"] = err
return row