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"],
    )