''' Author: ai-business-hql ai.bussiness.hql@gmail.com Date: 2025-08-26 15:06:47 LastEditors: ai-business-hql ai.bussiness.hql@gmail.com LastEditTime: 2025-09-01 19:31:03 FilePath: /ComfyUI-Copilot/backend/utils/modelscope_gateway.py Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE ''' import os from typing import Any, Dict, List, Optional import requests from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from .logger import log import folder_paths class ModelScopeGateway: BASE_URL = "https://www.modelscope.cn" SUGGEST_ENDPOINT = f"{BASE_URL}/api/v1/dolphin/model/suggestv2" SEARCH_ENDPOINT = f"{BASE_URL}/api/v1/dolphin/models" SEARCH_SINGLE_ENDPOINT = f"{BASE_URL}/api/v1/models" def __init__(self, timeout: float = 10.0, retries: int = 3, backoff: float = 0.5): self.timeout = timeout # 单实例 Session,会自动保存 Cookie;如需按用户隔离可按 session_key 做多 Session 管理 self.session = requests.Session() self.session.headers.update({ "Accept": "application/json, text/plain, */*", "Content-Type": "application/json", "User-Agent": "ComfyUI-Copilot/1.0 (requests)", "x-modelscope-accept-language": "zh_CN", }) retry_cfg = Retry( total=retries, backoff_factor=backoff, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["GET", "POST", "PUT"], raise_on_status=False, ) adapter = HTTPAdapter(max_retries=retry_cfg) self.session.mount("https://", adapter) self.session.mount("http://", adapter) def formatData(self, data: Any) -> Dict[str, Any]: inner = data.get('Model', {}) if isinstance(data, dict) else {} path = data.get('Path') or inner.get('Path') name = data.get('Name') or inner.get('Name') revision = data.get('Revision') or inner.get('Revision') # size = self.get_model_size(path, name, revision) return { "Libraries": data.get("Libraries") or inner.get("Libraries"), "ChineseName": data.get("ChineseName") or inner.get("ChineseName"), "Id": data.get("Id") or inner.get("Id") or data.get("ModelId") or inner.get("ModelId"), "Name": data.get("Name") or inner.get("Name"), "Path": data.get("Path") or inner.get("Path"), "LastUpdatedTime": data.get("LastUpdatedTime") or inner.get("LastUpdatedTime") or data.get("LastUpdatedAt") or inner.get("LastUpdatedAt"), "Downloads": data.get("Downloads") or inner.get("Downloads") or data.get("DownloadCount") or inner.get("DownloadCount"), # "Size": size or 0 } def get_single_model(self, path: str, name: str) -> Optional[Dict[str, Any]]: """ 调用单模型详情接口。 返回原始 JSON(尽量保持结构,便于 formatData 处理),失败返回 None。 """ if path is None or name is None: return None try: url = f"{self.SEARCH_SINGLE_ENDPOINT}/{path}/{name}" resp = self.session.get(url, timeout=self.timeout) resp.raise_for_status() body = resp.json() # 兼容不同返回包裹层级 if isinstance(body, dict): data = body.get("Data") or body.get("data") or body return data return body except Exception as e: log.error(f"ModelScope single fetch failed for path={path}: name={name}: {e}") return None def get_model_size(self, path: str, name: str, revision: str, root: str = '') -> int: """ 调用单模型详情接口。 返回原始 JSON(尽量保持结构,便于 formatData 处理),失败返回 None。 """ if path is None or name is None or revision is None: return 0 try: url = f"{self.SEARCH_SINGLE_ENDPOINT}/{path}/{name}/repo/files?Revision={revision}&Root={root}" resp = self.session.get(url, timeout=self.timeout) resp.raise_for_status() body = resp.json() # 兼容不同返回包裹层级 if isinstance(body, dict): data = body["Data"]["Files"] size = 0 for item in data: size += item.get("Size") or 0 return size return 0 except Exception as e: log.error(f"ModelScope model size fetch failed for path={path}: name={name}: rversion={rversion}: {e}") return 0 def suggest( self, name: str, page: int = 1, page_size: int = 30, sort_by: str = "Default", target: str = "", single_criterion: Optional[List[Dict[str, Any]]] = None, ) -> Dict[str, Any]: """ 调用 suggestv2 模糊搜索接口;返回 body 与当前 Cookie。 Returns: Dict[str, Any]: 返回的模型列表,格式为: { "data": [ { "ChineseName": "中文名称", "Id": 294066, "Name": "SD3-Controlnet-Pose", "Path": "InstantX", "Libraries": ["pytorch","lora","safetensors"], "LastUpdatedTime": "1733042611", "Downloads": 100, "Size": 100000 } ] } """ payload = { "PageSize": page_size, "PageNumber": page, "SortBy": sort_by, "Target": target, "SingleCriterion": single_criterion or [], "Name": name, } resp = self.session.post( self.SUGGEST_ENDPOINT, json=payload, timeout=self.timeout, ) resp.raise_for_status() body = resp.json() if body['Data'] is None or body['Data']['Model'] is None \ or body['Data']['Model']['Suggests'] is None or (len(body['Data']['Model']['Suggests']) == 0): log.error(f"ModelScope suggest failed: {body}, request: {payload}") return {"data": None} models = body['Data']['Model']['Suggests'] picked: List[Dict[str, Any]] = [] for item in models: base = item or {} inner = base.get('Model', {}) if isinstance(base, dict) else {} path = base.get('Path') or inner.get('Path') name = base.get('Name') or inner.get('Name') detail= self.get_single_model(path, name) data = self.formatData(detail or base) picked.append(data) total = body['Data']['Model'].get('TotalCount') or body['Data']['Model'].get('Total') or 0 return {"data": picked, "total": total} def search( self, name: str, page: int = 1, page_size: int = 30, sort_by: str = "Default", target: str = "", single_criterion: Optional[List[Dict[str, Any]]] = None, criterion: Optional[List[Dict[str, Any]]] = None, ) -> Dict[str, Any]: """ 调用 models 模糊搜索接口;返回 body 与当前 Cookie。 Returns: Dict[str, Any]: 返回的模型列表,格式为: { "data": [ { "ChineseName": "中文名称", "Id": 294066, "Name": "SD3-Controlnet-Pose", "Path": "InstantX", "Libraries": ["pytorch","lora","safetensors"], "LastUpdatedTime": "1733042611", "Downloads": 100, "Size": 100000 } ] } """ payload = { "PageSize": page_size, "PageNumber": page, "SortBy": sort_by, "Target": target, "SingleCriterion": single_criterion or [], "Name": name, "Criterion" : criterion } resp = self.session.put( self.SEARCH_ENDPOINT, json=payload, timeout=self.timeout, ) resp.raise_for_status() body = resp.json() if body['Data'] is None or body['Data']['Model'] is None \ or body['Data']['Model']['Models'] is None or (len(body['Data']['Model']['Models']) == 0): log.error(f"ModelScope search failed: {body}, request: {payload}") return {"data": None} models = body['Data']['Model']['Models'] or [] picked: List[Dict[str, Any]] = [] for item in models: data = self.formatData(item or {}) picked.append(data) total = body['Data']['Model'].get('TotalCount') or body['Data']['Model'].get('Total') or 0 return {"data": picked, "total": total} def download_with_sdk( self, model_id: str, model_type: str, dest_dir: Optional[str] = None, ) -> str: """ 推荐通过 ModelScope 官方 SDK 下载(更稳妥,支持断点与多文件)。 pip install modelscope """ try: from modelscope.hub.snapshot_download import snapshot_download except ImportError as e: raise RuntimeError("缺少依赖 modelscope,请先安装:pip install modelscope") from e # Determine destination directory in ComfyUI models folder hierarchy try: if dest_dir: cache_dir = os.path.abspath(os.path.expanduser(dest_dir)) else: # Prefer ComfyUI's configured folder for this model_type try: model_type_paths = folder_paths.get_folder_paths(model_type) cache_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir, model_type) except Exception: # Fallback to models_dir/model_type if the key is unknown cache_dir = os.path.join(folder_paths.models_dir, model_type) os.makedirs(cache_dir, exist_ok=True) except Exception as e: raise RuntimeError(f"Failed to prepare destination directory for download: {e}") from e local_dir = snapshot_download(model_id, cache_dir=cache_dir) return local_dir def test_modelscope_gateway(self): """ 测试 ModelScope Gateway 的基本功能 """ try: # 测试初始化 print("Testing ModelScope Gateway initialization...") gateway = ModelScopeGateway() # 测试模糊搜索 print("Testing suggest method...") result = gateway.suggest( name="stable-diffusion", page_size=5, page=1 ) if result.get("data"): print(f"Suggest test passed: found {len(result['data'])} models") for model in result["data"][:2]: # 显示前2个结果 print(f" - {model.get('Model', {}).get('ModelId', 'Unknown')}") else: print("Suggest test failed: no data returned") # 测试特定模型搜索 print("Testing specific model search...") specific_result = gateway.suggest( name="qwen", page_size=3, page=1, sort_by="DownloadCount", target="model" ) if specific_result.get("data"): print(f"Specific search test passed: found {len(specific_result['data'])} models") else: print("Specific search test failed") print("All tests completed successfully!") return True except Exception as e: print(f"Test failed with error: {e}") import traceback traceback.print_exc() return False # 测试脚本入口 if __name__ == "__main__": # 创建测试实例 gateway = ModelScopeGateway() # 运行测试 print("=" * 50) print("ModelScope Gateway Test Script") print("=" * 50) success = gateway.test_modelscope_gateway() print("=" * 50) if success: print("✅ All tests passed!") else: print("❌ Some tests failed!") print("=" * 50)