File size: 7,867 Bytes
e5e756a 07e0a28 e5e756a 07e0a28 e5e756a 07e0a28 e5e756a 07e0a28 e5e756a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 | """
模型 Provider 状态探测模块
为 CPU-only HuggingFace Space 环境提供模型可用性探测能力。
不强制加载完整大模型,仅报告 provider 在当前环境的状态。
"""
import logging
import time
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional, Dict, Any, List
from enum import Enum
from pydantic import BaseModel, Field
from app.config.settings import settings
logger = logging.getLogger(__name__)
class ProviderAvailability(str, Enum):
"""Provider 可用状态"""
AVAILABLE = "available" # 模型依赖就绪,预期可运行
UNAVAILABLE = "unavailable" # 模型依赖缺失或环境不支持
NOT_VERIFIED = "not_verified" # 无法验证(如缺少凭证、资源限制等)
class ProviderStatus(BaseModel):
"""单个模型 Provider 的状态信息
供前端和 API 展示模型可用性,不强制加载完整模型。
"""
provider: str = Field(..., description="Provider 名称,如 whisper_small")
model_id: str = Field(..., description="HuggingFace 模型 ID")
status: ProviderAvailability = Field(
default=ProviderAvailability.NOT_VERIFIED,
description="可用状态:available/unavailable/not_verified"
)
cache_path: Optional[str] = Field(None, description="模型缓存路径")
reason: Optional[str] = Field(None, description="状态原因说明")
last_probe_at: Optional[datetime] = Field(None, description="最后探测时间")
probe_duration_ms: Optional[float] = Field(None, description="探测耗时(毫秒)")
extra: Dict[str, Any] = Field(default_factory=dict, description="额外信息")
class ModelProbeResult(BaseModel):
"""模型探测汇总结果
包含所有 provider 的状态列表和探测摘要。
"""
providers: List[ProviderStatus] = Field(default_factory=list)
probe_time: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
cpu_only: bool = True # 当前环境是否为 CPU-only
hf_home: str = ""
hf_cache_dir: str = ""
def probe_cpu_only_environment() -> Dict[str, Any]:
"""探测 CPU-only 环境的基本信息
不加载任何模型,仅检查环境变量、缓存目录和 GPU 可用性。
Returns:
Dict 包含 cpu_only 标志、hf_home、缓存路径等信息
"""
result = {
"cpu_only": True,
"hf_home": str(settings.HF_HOME),
"hf_cache_dir": str(settings.HF_CACHE_DIR),
"hf_home_exists": settings.HF_HOME.exists(),
"hf_cache_dir_exists": settings.HF_CACHE_DIR.exists(),
}
# 检查 GPU 可用性(不导入 torch,只检查环境变量)
cuda_visible = os.getenv("CUDA_VISIBLE_DEVICES", "")
if cuda_visible and cuda_visible != "-1":
# 有 GPU 环境变量设置,但本项目目标是 CPU-only
result["cuda_visible_devices"] = cuda_visible
result["note"] = "检测到 CUDA 环境变量,但项目以 CPU-only 模式运行"
# 检查 HuggingFace 环境变量
hf_home_env = os.getenv("HF_HOME", "")
if hf_home_env:
result["hf_home_env"] = hf_home_env
hf_token = os.getenv("HF_TOKEN", "") or os.getenv("HUGGINGFACE_HUB_TOKEN", "")
result["hf_token_configured"] = bool(hf_token)
# 检查 HuggingFace Hub 是否可用(尝试 import,不调用网络)
try:
import huggingface_hub # noqa: F401
result["huggingface_hub_available"] = True
result["huggingface_hub_version"] = huggingface_hub.__version__
except ImportError:
result["huggingface_hub_available"] = False
result["huggingface_hub_version"] = None
return result
def probe_provider(
provider: str,
model_id: str,
check_dependencies: List[str],
check_cache: bool = True,
) -> ProviderStatus:
"""探测单个 Provider 的可用性
执行轻量检查:依赖包是否可导入、模型缓存是否存在。
不加载完整模型权重。
Args:
provider: Provider 名称
model_id: HuggingFace 模型 ID
check_dependencies: 需要检查的 Python 依赖包列表
check_cache: 是否检查模型缓存
Returns:
ProviderStatus 包含探测结果
"""
start_time = time.perf_counter()
reasons = []
status = ProviderAvailability.AVAILABLE
# 检查依赖包
missing_deps = []
for dep in check_dependencies:
try:
__import__(dep)
except ImportError:
missing_deps.append(dep)
if missing_deps:
status = ProviderAvailability.UNAVAILABLE
reasons.append(f"缺失依赖: {', '.join(missing_deps)}")
# 检查模型缓存
cache_dir_name = "models--" + model_id.replace("/", "--")
cache_path = settings.HF_CACHE_DIR / cache_dir_name
if check_cache:
if cache_path.exists():
# 检查缓存中是否有实际模型文件(blobs 或 snapshots)
has_blobs = (cache_path / "blobs").exists() and any((cache_path / "blobs").iterdir())
has_snapshots = (cache_path / "snapshots").exists() and any((cache_path / "snapshots").iterdir())
if not has_blobs and not has_snapshots:
# 缓存目录存在但无模型文件,标记为需要下载
reasons.append("模型缓存目录存在但无模型文件,首次使用需下载")
else:
reasons.append(f"模型未缓存,首次使用将从 HuggingFace Hub 下载")
# 检查 CPU-only 环境的额外限制
cpu_env = probe_cpu_only_environment()
if not cpu_env.get("huggingface_hub_available"):
status = ProviderAvailability.UNAVAILABLE
reasons.append("huggingface_hub 包不可用,无法访问 HuggingFace Hub")
# 如果缺少依赖且无法下载,标记为 unavailable
if missing_deps and not cpu_env.get("huggingface_hub_available"):
status = ProviderAvailability.UNAVAILABLE
duration_ms = (time.perf_counter() - start_time) * 1000
return ProviderStatus(
provider=provider,
model_id=model_id,
status=status,
cache_path=str(cache_path) if check_cache else None,
reason="; ".join(reasons) if reasons else "依赖和缓存检查通过",
last_probe_at=datetime.now(timezone.utc),
probe_duration_ms=round(duration_ms, 2),
extra={
"missing_dependencies": missing_deps,
"cpu_only": cpu_env["cpu_only"],
},
)
def probe_all_providers() -> ModelProbeResult:
"""探测所有已知 Provider 的可用性
汇总 ASR、OCR 等所有模型 provider 的状态。
Returns:
ModelProbeResult 包含所有 provider 状态
"""
cpu_env = probe_cpu_only_environment()
providers: List[ProviderStatus] = []
# ASR providers(均基于 transformers pipeline)
providers.append(probe_provider(
provider="whisper_small",
model_id="openai/whisper-small",
check_dependencies=["transformers", "torch"],
))
providers.append(probe_provider(
provider="glm_asr_nano",
model_id="zai-org/GLM-ASR-Nano-2512",
check_dependencies=["transformers", "torch"],
))
# OCR provider
providers.append(probe_provider(
provider="ppocrv6",
model_id="PaddlePaddle/PP-OCRv6_medium_det_safetensors",
check_dependencies=["paddleocr", "paddle"],
))
# 补充 OCR rec 模型
providers.append(probe_provider(
provider="ppocrv6_rec",
model_id="PaddlePaddle/PP-OCRv6_medium_rec_safetensors",
check_dependencies=["paddleocr", "paddle"],
))
return ModelProbeResult(
providers=providers,
cpu_only=cpu_env["cpu_only"],
hf_home=cpu_env["hf_home"],
hf_cache_dir=cpu_env["hf_cache_dir"],
)
|