Files
2026-01-12 00:34:50 +08:00

408 lines
13 KiB
Python

"""
Lora 信息 API - 为前端提供 Lora 详细信息接口
基于rgthree-comfy的核心算法实现,解决训练词汇中JSON片段的问题
"""
import os
import json
import hashlib
import re
from typing import Optional, Dict, Any, List
from aiohttp import web
from server import PromptServer
import requests
import folder_paths
from .easy_setting_utils import get_dict_value
# 获取 PromptServer 实例并注册路由
routes = PromptServer.instance.routes
# Civitai API 配置
CIVITAI_API_URL = "https://civitai.com/api/v1/model-versions/by-hash"
CIVITAI_TIMEOUT = 10 # 秒
def file_exists(path):
"""检查文件是否存在,支持 None 类型
Args:
path: 文件路径,可以是 None
Returns:
文件存在返回 True,否则返回 False
"""
if path is not None:
return os.path.isfile(path)
return False
def get_file_hash(file_path: str, algorithm: str = 'sha256') -> str:
"""计算文件的哈希值
用于生成文件的唯一标识符,主要用于:
- 文件完整性验证
- Civitai API 查询(通过文件哈希查找模型信息)
- 缓存管理
Args:
file_path: 文件路径
algorithm: 哈希算法,默认为 'sha256'
Returns:
文件的哈希字符串,出错时返回空字符串
"""
try:
hash_obj = hashlib.new(algorithm)
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
hash_obj.update(chunk)
return hash_obj.hexdigest()
except Exception as e:
return ""
def get_lora_metadata(file_path: str) -> Dict[str, Any]:
"""
从 Lora 文件中提取元数据
基于rgthree的 _read_file_metadata_from_header 实现
"""
data = None
try:
if file_path.endswith('.safetensors'):
with open(file_path, "rb") as file:
# https://github.com/huggingface/safetensors#format
# 8 bytes: N, an unsigned little-endian 64-bit integer, containing the size of the header
header_size = int.from_bytes(file.read(8), "little", signed=False)
if header_size <= 0:
return {}
header = file.read(header_size)
if header is None:
return {}
header_json = json.loads(header)
data = header_json.get("__metadata__", {})
if data is not None:
# 更保守的JSON解析策略,避免过度解析
for key, value in data.items():
if isinstance(value, str) and value.startswith('{') and value.endswith('}'):
try:
value_as_json = json.loads(value)
data[key] = value_as_json
except Exception:
# 如果解析失败,保持原样
pass
except Exception as e:
data = None
return data if data else {}
def extract_trained_words(metadata: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
从元数据中提取训练词汇
基于rgthree的 _merge_metadata 实现,避免JSON片段混入
"""
trained_words = {}
if not isinstance(metadata, dict):
return []
# 使用rgthree的严格类型检查逻辑
if 'ss_tag_frequency' in metadata and isinstance(metadata['ss_tag_frequency'], dict):
for bucket_value in metadata['ss_tag_frequency'].values():
if isinstance(bucket_value, dict):
for tag, count in bucket_value.items():
# 确保tag是字符串,避免JSON对象混入
if isinstance(tag, str) and not tag.startswith('{') and not tag.startswith('['):
if tag not in trained_words:
trained_words[tag] = {'word': tag, 'count': 0, 'metadata': True}
# 累加计数
if isinstance(count, (int, float)):
trained_words[tag]['count'] = trained_words[tag]['count'] + count
result = list(trained_words.values())
return result
def get_civitai_info_sync(file_hash: str) -> Optional[Dict[str, Any]]:
"""
同步方式从 Civitai API 获取模型信息
"""
if not file_hash:
return None
try:
url = f"{CIVITAI_API_URL}/{file_hash}"
response = requests.get(url, timeout=CIVITAI_TIMEOUT)
if response.status_code == 200:
data = response.json()
return data
elif response.status_code == 404:
return {"error": "Model not found"}
else:
return None
except requests.exceptions.Timeout:
return None
except requests.exceptions.RequestException as e:
return None
def merge_civitai_data(info: Dict[str, Any], civitai_data: Dict[str, Any]) -> None:
"""
将 Civitai 数据合并到 Lora 信息中
基于rgthree的 _merge_civitai_data 实现,包含更好的词汇清理
"""
if not civitai_data or 'error' in civitai_data:
return
# 提取基础信息
if 'name' not in info:
info['name'] = get_dict_value(civitai_data, 'model.name', '')
version_name = get_dict_value(civitai_data, 'name')
if version_name is not None:
info['name'] += f' - {version_name}'
if 'type' not in info:
info['type'] = get_dict_value(civitai_data, 'model.type')
if 'baseModel' not in info:
info['baseModel'] = get_dict_value(civitai_data, 'baseModel')
# 使用rgthree的词汇清理逻辑
civitai_trigger = get_dict_value(civitai_data, 'triggerWords', default=[])
civitai_trained = get_dict_value(civitai_data, 'trainedWords', default=[])
# 清理和规范化词汇
civitai_words = ','.join(civitai_trigger + civitai_trained)
if civitai_words:
civitai_words = re.sub(r"\s*,\s*", ",", civitai_words)
civitai_words = re.sub(r",+", ",", civitai_words)
civitai_words = re.sub(r"^,", "", civitai_words)
civitai_words = re.sub(r",$", "", civitai_words)
if civitai_words:
civitai_words = civitai_words.split(',')
if 'trainedWords' not in info:
info['trainedWords'] = []
for trigger_word in civitai_words:
# 过滤掉JSON对象和空字符串
if not trigger_word or trigger_word.startswith('{') or trigger_word.startswith('['):
continue
word_data = next(
(data for data in info['trainedWords'] if data.get('word') == trigger_word), None
)
if word_data is None:
word_data = {'word': trigger_word}
info['trainedWords'].append(word_data)
word_data['civitai'] = True
# 提取 Civitai 链接
if 'modelId' in civitai_data:
if 'links' not in info:
info['links'] = []
model_id = civitai_data['modelId']
civitai_link = f'https://civitai.com/models/{model_id}'
if 'id' in civitai_data:
civitai_link += f'?modelVersionId={civitai_data["id"]}'
if civitai_link not in info['links']:
info['links'].append(civitai_link)
# 提取图片
if 'images' in civitai_data:
if 'images' not in info:
info['images'] = []
existing_urls = {img.get('url') for img in info['images']}
for img in civitai_data['images']:
img_url = img.get('url')
if img_url and img_url not in existing_urls:
img_data = {
'url': img_url,
'type': img.get('type'),
'width': img.get('width'),
'height': img.get('height'),
}
# 提取元数据
meta = img.get('meta', {})
if meta.get('seed'):
img_data['seed'] = meta['seed']
if meta.get('prompt'):
img_data['positive'] = meta['prompt']
if meta.get('negativePrompt'):
img_data['negative'] = meta['negativePrompt']
if meta.get('steps'):
img_data['steps'] = meta['steps']
if meta.get('sampler'):
img_data['sampler'] = meta['sampler']
if meta.get('cfgScale'):
img_data['cfg'] = meta['cfgScale']
if meta.get('Model'):
img_data['model'] = meta['Model']
info['images'].append(img_data)
# 保存原始 Civitai 数据
if 'raw' not in info:
info['raw'] = {}
info['raw']['civitai'] = civitai_data
def get_lora_info(lora_name: str, fetch_civitai: bool = False) -> Optional[Dict[str, Any]]:
"""
获取指定 Lora 的详细信息
使用rgthree的核心算法实现
"""
try:
# 获取 Lora 文件的完整路径
lora_path = folder_paths.get_full_path("loras", lora_name)
if not os.path.exists(lora_path):
return None
# 构建基础信息
info = {
"file": lora_name,
"path": lora_path,
"size": os.path.getsize(lora_path),
"name": os.path.splitext(lora_name)[0], # 不带扩展名的名称
}
# 计算文件哈希
file_hash = get_file_hash(lora_path)
if file_hash:
info["sha256"] = file_hash
# 尝试提取元数据
metadata = get_lora_metadata(lora_path)
if metadata:
if "raw" not in info:
info["raw"] = {}
info["raw"]["metadata"] = metadata
# 提取训练词汇(使用rgthree的严格逻辑)
trained_words = extract_trained_words(metadata)
if trained_words:
info["trainedWords"] = trained_words
# 提取其他有用的字段
if 'ss_clip_skip' in metadata:
try:
info["clipSkip"] = int(metadata['ss_clip_skip'])
except (ValueError, TypeError):
info["clipSkip"] = metadata['ss_clip_skip']
if 'ss_output_name' in metadata:
info["name"] = metadata['ss_output_name']
# 如果需要,从 Civitai 获取信息
if fetch_civitai and file_hash:
civitai_data = get_civitai_info_sync(file_hash)
if civitai_data:
merge_civitai_data(info, civitai_data)
return info
except Exception as e:
return None
@routes.get('/api/easy_setting/loras/info')
async def api_get_lora_info(request: web.Request) -> web.Response:
"""
获取指定 Lora 的详细信息
查询参数:
file: Lora 文件名(必需)
civitai: 是否从 Civitai 获取信息(可选,默认为 false)
返回:
{
"file": "lora_name.safetensors",
"path": "/path/to/lora.safetensors",
"sha256": "hash...",
"size": 12345,
"name": "lora_name",
"trainedWords": [
{"word": "keyword1"},
{"word": "keyword2", "civitai": true}
],
"links": ["https://civitai.com/models/..."],
"images": [
{
"url": "...",
"seed": "...",
"positive": "...",
"negative": "..."
}
],
"raw": {
"metadata": {...},
"civitai": {...}
}
}
"""
try:
file_param = request.rel_url.query.get('file')
civitai_param = request.rel_url.query.get('civitai', 'false').lower() == 'true'
if not file_param:
return web.json_response(
{"error": "Missing 'file' parameter"},
status=400
)
info = get_lora_info(file_param, fetch_civitai=civitai_param)
if info is None:
return web.json_response(
{"error": f"Lora file not found: {file_param}"},
status=404
)
return web.json_response(info)
except Exception as e:
return web.json_response(
{"error": str(e)},
status=500
)
@routes.get('/api/easy_setting/loras/list')
async def api_list_loras(request: web.Request) -> web.Response:
"""
获取所有可用的 Lora 列表
"""
try:
loras = folder_paths.get_filename_list("loras")
return web.json_response(loras)
except Exception as e:
return web.json_response(
{"error": str(e)},
status=500
)
def register_lora_api():
"""
注册 Lora API
这个函数在模块加载时会被调用
"""
pass
# 模块加载时自动注册
register_lora_api()