Files
AIDC-AI-ComfyUI-Copilot/backend/controller/model_api.py
T

468 lines
18 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.
import threading
import asyncio
import time
import server
from aiohttp import web
import os
import shutil
from ..utils.logger import log
from ..utils.modelscope_gateway import ModelScopeGateway
import folder_paths
# 全局下载进度存储
download_progress = {}
download_lock = threading.Lock()
# 下载进度回调类
class DownloadProgressCallback:
def __init__(self, id: str, filename: str, file_size: int, download_id: str):
self.id = id
self.filename = filename
self.file_size = file_size
self.download_id = download_id
self.progress = 0
self.status = "downloading" # downloading, completed, failed
self.error_message = None
self.start_time = time.time()
# 初始化进度记录
with download_lock:
download_progress[download_id] = {
"id": id,
"filename": filename,
"file_size": file_size,
"progress": 0,
"percentage": 0.0,
"status": "downloading",
"start_time": self.start_time,
"estimated_time": None,
"speed": 0.0,
"error_message": None
}
def update(self, size: int):
"""更新下载进度"""
self.progress += size
current_time = time.time()
elapsed_time = current_time - self.start_time
# 计算下载速度和预估时间
if elapsed_time > 0:
speed = self.progress / elapsed_time # bytes per second
if speed > 0 and self.progress < self.file_size:
remaining_bytes = self.file_size - self.progress
estimated_time = remaining_bytes / speed
else:
estimated_time = None
else:
speed = 0.0
estimated_time = None
percentage = (self.progress / self.file_size) * 100 if self.file_size > 0 else 0
# 更新全局进度
with download_lock:
if self.download_id in download_progress:
# 直接更新字典的值,而不是调用update方法
progress_dict = download_progress[self.download_id]
progress_dict["progress"] = self.progress
progress_dict["percentage"] = round(percentage, 2)
progress_dict["speed"] = round(speed, 2)
progress_dict["estimated_time"] = round(estimated_time, 2) if estimated_time else None
def end(self, success: bool = True, error_message: str = None):
"""下载结束回调"""
current_time = time.time()
total_time = current_time - self.start_time
if success:
self.status = "completed"
# 验证下载完整性
assert self.progress == self.file_size, f"Download incomplete: {self.progress}/{self.file_size}"
else:
self.status = "failed"
self.error_message = error_message
# 更新最终状态
with download_lock:
if self.download_id in download_progress:
# 直接更新字典的值,而不是调用update方法
progress_dict = download_progress[self.download_id]
progress_dict["status"] = self.status
progress_dict["progress"] = self.file_size if success else self.progress
if self.file_size > 0:
progress_dict["percentage"] = 100.0 if success else (self.progress / self.file_size) * 100
else:
progress_dict["percentage"] = 0.0
progress_dict["total_time"] = round(total_time, 2)
progress_dict["error_message"] = self.error_message
def fail(self, error_message: str):
"""下载失败回调"""
self.end(success=False, error_message=error_message)
# 生成唯一下载ID
def generate_download_id() -> str:
import uuid
return str(uuid.uuid4())
@server.PromptServer.instance.routes.post("/api/download-model")
async def download_model(request):
"""
Download model from ModelScope using SDK
"""
log.info("Received download-model request")
req_json = await request.json()
try:
id = req_json.get('id')
model_id = req_json.get('model_id')
model_type = req_json.get('model_type')
dest_dir = req_json.get('dest_dir')
# 验证必需参数
if not id:
return web.json_response({
"success": False,
"message": "Missing required parameter: id"
})
if not model_id:
return web.json_response({
"success": False,
"message": "Missing required parameter: model_id"
})
if not model_type:
return web.json_response({
"success": False,
"message": "Missing required parameter: model_type"
})
log.info(f"Downloading model: {model_id} (type: {model_type})")
# 生成下载ID
download_id = generate_download_id()
# 计算目标目录:优先使用传入的dest_dir,否则使用ComfyUI的models目录下对应类型
resolved_dest_dir = None
if dest_dir:
resolved_dest_dir = os.path.abspath(os.path.expanduser(f"models/{dest_dir}"))
else:
try:
model_type_paths = folder_paths.get_folder_paths(model_type)
resolved_dest_dir = model_type_paths[0] if model_type_paths else os.path.join(folder_paths.models_dir,
model_type)
except Exception:
resolved_dest_dir = os.path.join(folder_paths.models_dir, model_type)
# 创建进度回调
progress_callback = DownloadProgressCallback(
id=id,
filename=f"{model_id}.{model_type}",
file_size=0, # 实际大小会在下载过程中获取
download_id=download_id
)
# 启动下载任务(异步执行)
async def download_task():
try:
# 调用下载方法 - 使用snapshot_download
from modelscope import snapshot_download
# 创建进度回调包装器 - 实现ModelScope期望的接口
class ProgressWrapper:
"""Factory that returns a per-file progress object with update/end."""
def __init__(self, download_id: str):
self.download_id = download_id
def __call__(self, file_name: str, file_size: int):
# Create a per-file progress tracker expected by ModelScope
download_id = self.download_id
class _PerFileProgress:
def __init__(self, fname: str, fsize: int):
self.file_name = fname
self.file_size = max(int(fsize or 0), 0)
self.progress = 0
self.last_update_time = time.time()
self.last_downloaded = 0
with download_lock:
if download_id in download_progress:
# If unknown size, keep 0 to avoid div-by-zero
download_progress[download_id]["file_size"] = self.file_size
def update(self, size: int):
try:
self.progress += int(size or 0)
now = time.time()
# Update global progress
with download_lock:
if download_id in download_progress:
dp = download_progress[download_id]
dp["progress"] = self.progress
if self.file_size > 0:
dp["percentage"] = round(self.progress * 100.0 / self.file_size, 2)
# speed
elapsed = max(now - self.last_update_time, 1e-6)
speed = (self.progress - self.last_downloaded) / elapsed
dp["speed"] = round(speed, 2)
self.last_update_time = now
self.last_downloaded = self.progress
except Exception as e:
log.error(f"Error in progress update: {e}")
def end(self):
# Called by modelscope when a file finishes
with download_lock:
if download_id in download_progress:
dp = download_progress[download_id]
if self.file_size > 0:
dp["progress"] = self.file_size
dp["percentage"] = 100.0
return _PerFileProgress(file_name, file_size)
progress_wrapper = ProgressWrapper(download_id)
# 添加调试日志
log.info(f"Starting download with progress wrapper: {download_id}")
# 在线程中执行阻塞的下载,避免阻塞事件循环
from functools import partial
local_dir = await asyncio.to_thread(
partial(
snapshot_download,
model_id=model_id,
cache_dir=resolved_dest_dir,
progress_callbacks=[progress_wrapper]
)
)
# 下载完成
progress_callback.end(success=True)
log.info(f"Model downloaded successfully to: {local_dir}")
# 下载后遍历目录,将所有重要权重/资源文件移动到最外层(与目录同级,即 resolved_dest_dir)
try:
moved_count = 0
allowed_exts = {
".safetensors", ".ckpt", ".pt", ".pth", ".bin"
# ".msgpack", ".json", ".yaml", ".yml", ".toml", ".png", ".onnx"
}
for root, dirs, files in os.walk(local_dir):
for name in files:
ext = os.path.splitext(name)[1].lower()
if ext in allowed_exts:
src_path = os.path.join(root, name)
target_dir = resolved_dest_dir
os.makedirs(target_dir, exist_ok=True)
target_path = os.path.join(target_dir, name)
# 如果已经在目标目录则跳过
if os.path.abspath(os.path.dirname(src_path)) == os.path.abspath(target_dir):
continue
# 处理重名情况:自动追加 _1, _2 ...
if os.path.exists(target_path):
base, ext_real = os.path.splitext(name)
idx = 1
while True:
candidate = f"{base}_{idx}{ext_real}"
candidate_path = os.path.join(target_dir, candidate)
if not os.path.exists(candidate_path):
target_path = candidate_path
break
idx += 1
shutil.move(src_path, target_path)
moved_count += 1
log.info(
f"Moved {moved_count} files with extensions {sorted(list(allowed_exts))} to: {resolved_dest_dir}")
except Exception as move_err:
log.error(f"Post-download move failed: {move_err}")
except Exception as e:
progress_callback.fail(str(e))
log.error(f"Download failed: {str(e)}")
# 启动异步下载任务
asyncio.create_task(download_task())
return web.json_response({
"success": True,
"data": {
"download_id": download_id,
"id": id,
"model_id": model_id,
"model_type": model_type,
"dest_dir": resolved_dest_dir,
"status": "started"
},
"message": f"Download started for model '{model_id}'"
})
except ImportError as e:
log.error(f"ModelScope SDK not installed: {str(e)}")
return web.json_response({
"success": False,
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
})
except Exception as e:
log.error(f"Error starting download: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Failed to start download: {str(e)}"
})
@server.PromptServer.instance.routes.get("/api/download-progress/{download_id}")
async def get_download_progress(request):
"""
Get download progress by download ID
"""
download_id = request.match_info.get('download_id')
if not download_id:
return web.json_response({
"success": False,
"message": "Missing download_id parameter"
})
with download_lock:
progress_info = download_progress.get(download_id)
if not progress_info:
return web.json_response({
"success": False,
"message": f"Download ID {download_id} not found"
})
return web.json_response({
"success": True,
"data": progress_info,
"message": "Download progress retrieved successfully"
})
@server.PromptServer.instance.routes.get("/api/download-progress")
async def list_downloads(request):
"""
List all active downloads
"""
with download_lock:
downloads = list(download_progress.keys())
return web.json_response({
"success": True,
"data": {
"downloads": downloads,
"count": len(downloads)
},
"message": "Download list retrieved successfully"
})
@server.PromptServer.instance.routes.delete("/api/download-progress/{download_id}")
async def clear_download_progress(request):
"""
Clear download progress record (for cleanup)
"""
download_id = request.match_info.get('download_id')
if not download_id:
return web.json_response({
"success": False,
"message": "Missing download_id parameter"
})
with download_lock:
if download_id in download_progress:
del download_progress[download_id]
return web.json_response({
"success": True,
"message": f"Download progress {download_id} cleared successfully"
})
else:
return web.json_response({
"success": False,
"message": f"Download ID {download_id} not found"
})
@server.PromptServer.instance.routes.get("/api/model-searchs")
async def model_suggests(request):
"""
Get model search list by keyword
"""
log.info("Received model-search request")
try:
keyword = request.query.get('keyword')
if not keyword:
return web.json_response({
"success": False,
"message": "Missing required parameter: keyword"
})
# 创建ModelScope网关实例
gateway = ModelScopeGateway()
suggests = gateway.search(name=keyword)
list = suggests["data"] if suggests.get("data") else []
return web.json_response({
"success": True,
"data": {
"searchs": list,
"total": len(list)
},
"message": f"Get searchs successfully"
})
except ImportError as e:
log.error(f"ModelScope SDK not installed: {str(e)}")
return web.json_response({
"success": False,
"message": "ModelScope SDK not installed. Please install with: pip install modelscope"
})
except Exception as e:
log.error(f"Error get model searchs: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Get model searchs failed: {str(e)}"
})
@server.PromptServer.instance.routes.get("/api/model-paths")
async def model_paths(request):
"""
Get model paths by type
"""
log.info("Received model-paths request")
try:
model_paths = list(folder_paths.folder_names_and_paths.keys())
return web.json_response({
"success": True,
"data": {
"paths": model_paths,
},
"message": f"Get paths successfully"
})
except Exception as e:
log.error(f"Error get model path: {str(e)}")
import traceback
traceback.print_exc()
return web.json_response({
"success": False,
"message": f"Get model failed: {str(e)}"
})