Files
2026-06-10 03:33:15 +08:00

881 lines
32 KiB
Python

import hashlib
import json
import os
import re
from datetime import datetime
import folder_paths
import requests
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
USERDATA = os.path.join(THIS_DIR, "../../../", "lora_userdatas")
# code from rgthree-comfy thanks
def image_upload(post, image_save_function=None):
image = post.get("image")
path = post.get("path")
fileName = post.get("fileName")
lora_path = folder_paths.get_full_path("loras", path)
if not path_exists(lora_path):
lora_path = os.path.abspath(lora_path)
for ext in ["jpg", "png", "jpeg", "gif", "webp"]:
try_path = f"{os.path.splitext(lora_path)[0]}.{ext}"
if path_exists(try_path):
os.remove(try_path) # 删除已经存在的图片
file_name_with_extension = os.path.basename(lora_path)
file_namea, _ = os.path.splitext(file_name_with_extension)
model = file_namea
upload_dir = os.path.dirname(os.path.abspath(lora_path))
api_response = {"status": 200}
if image and image.file:
file_name, file_extension = os.path.splitext(fileName)
filepath = os.path.join(upload_dir, model + file_extension)
with open(filepath, "wb") as f:
f.write(image.file.read())
api_response["res"] = "success"
return api_response
else:
api_response["status"] = "404"
return api_response
def get_param(request, param, default=None):
"""Gets a param from a request."""
# return request.rel_url.query[param] if param in request.rel_url.query else default
if param in request.rel_url.query:
value = request.rel_url.query[param]
try:
# 尝试URL解码特殊字符
import urllib.parse
return urllib.parse.unquote(value)
except:
return value
return default
def is_param_falsy(request, param):
"""Determines if a param is explicitly 0 or false."""
val = get_param(request, param)
return val is not None and (val == "0" or val.upper() == "FALSE")
def path_exists(path):
"""Checks if a path exists, accepting None type."""
if path is not None:
return os.path.exists(path)
return False
def get_folder_path(file: str, model_type="loras"):
"""Gets the file path ensuring it exists."""
try:
import urllib.parse
file = urllib.parse.unquote(file)
except:
pass
file_path = folder_paths.get_full_path(model_type, file)
if file_path and not path_exists(file_path):
file_path = os.path.abspath(file_path)
if not path_exists(file_path):
file_path = None
return file_path
async def get_loras_info_response(
request, maybe_fetch_civitai=False, maybe_fetch_metadata=False
):
"""Gets lora info for all or a single lora"""
api_response = {"status": 200}
lora_file = get_param(request, "file")
if get_param(request, "light") is not None:
light = is_param_falsy(request, "light")
else:
light = False
if lora_file is not None:
if light:
info_data = await get_model_info(
lora_file,
maybe_fetch_civitai=maybe_fetch_civitai,
maybe_fetch_metadata=maybe_fetch_metadata,
light=light,
)
else:
info_data = await get_model_info(
lora_file,
force_fetch_civitai=maybe_fetch_civitai,
force_fetch_metadata=maybe_fetch_metadata,
light=light,
)
if info_data is None:
api_response["status"] = "404"
api_response["error"] = "No Lora found at path"
else:
api_response["data"] = info_data
else:
api_response["data"] = []
lora_files = folder_paths.get_filename_list("loras")
for lora_file in lora_files:
info_data = await get_model_info(
lora_file,
maybe_fetch_civitai=maybe_fetch_civitai,
maybe_fetch_metadata=maybe_fetch_metadata,
light=light,
)
api_response["data"].append(info_data)
return api_response
def load_json_file(file: str, default=None):
"""Reads a json file and returns the json dict, stripping out "//" comments first."""
if path_exists(file):
with open(file, encoding="UTF-8") as file:
config = file.read()
try:
return json.loads(config)
except json.decoder.JSONDecodeError:
try:
config = re.sub(r"^\s*//\s.*", "", config, flags=re.MULTILINE)
return json.loads(config)
except json.decoder.JSONDecodeError:
try:
config = re.sub(r"(?:^|\s)//.*", "", config, flags=re.MULTILINE)
return json.loads(config)
except json.decoder.JSONDecodeError:
pass
return default
def _update_data(info_data: dict) -> bool:
"""Ports old data to new data if necessary."""
should_save = False
# If we have "triggerWords" then move them over to "trainedWords"
if "triggerWords" in info_data and len(info_data["triggerWords"]) > 0:
civitai_words = ",".join(
get_dict_value(info_data, "raw.civitai.triggerWords", default=[])
+ get_dict_value(info_data, "raw.civitai.trainedWords", default=[])
)
if "trainedWords" not in info_data:
info_data["trainedWords"] = []
for trigger_word in info_data["triggerWords"]:
word_data = next(
(
data
for data in info_data["trainedWords"]
if data["word"] == trigger_word
),
None,
)
if word_data is None:
word_data = {"word": trigger_word}
info_data["trainedWords"].append(word_data)
if trigger_word in civitai_words:
word_data["civitai"] = True
else:
word_data["user"] = True
del info_data["triggerWords"]
should_save = True
# 自动关联 trainedWords 到 loraWorks(解决触发词自动填充问题)
# 处理 loraWorks 不存在、为空、或包含多个词的情况
should_update_loraWorks = False
if "loraWorks" not in info_data or not info_data["loraWorks"]:
# loraWorks不存在或为空
should_update_loraWorks = True
elif isinstance(info_data["loraWorks"], str) and "," in info_data["loraWorks"]:
# loraWorks包含多个词(用逗号分隔),需要更新为只取第一个
should_update_loraWorks = True
if should_update_loraWorks:
# 从 trainedWords 中提取第一个训练词作为默认触发词
if (
"trainedWords" in info_data
and isinstance(info_data["trainedWords"], list)
and len(info_data["trainedWords"]) > 0
):
first_word = info_data["trainedWords"][0]
# 兼容 {"word": "xxx"} 格式和直接字符串格式
if isinstance(first_word, dict) and "word" in first_word:
info_data["loraWorks"] = first_word["word"].strip()
elif isinstance(first_word, str) and first_word.strip():
info_data["loraWorks"] = first_word.strip()
should_save = True
return should_save
async def get_model_info(
file: str,
model_type="loras",
default=None,
maybe_fetch_civitai=False,
force_fetch_civitai=False,
maybe_fetch_metadata=False,
force_fetch_metadata=False,
light=False,
):
"""Compiles a model info given a stored file next to the model, and/or metadata/civitai."""
file_path = get_folder_path(file, model_type)
if file_path is None:
return default
info_data = {}
should_save = False
# Try to load a weilin-info.json file next to the file.
try_info_path = f"{file_path}.weilin-info.json"
if path_exists(try_info_path):
info_data = load_json_file(try_info_path)
if "file" not in info_data:
info_data["file"] = file
should_save = True
if "path" not in info_data:
info_data["path"] = file_path
should_save = True
# Check if we have an image next to the file and, if so, add it to the front of the images
# (if it isn't already).
img_next_to_file = None
for ext in ["jpg", "png", "jpeg", "webp"]:
try_path = f"{os.path.splitext(file_path)[0]}.{ext}"
if path_exists(try_path):
img_next_to_file = try_path
break
if "images" not in info_data:
info_data["images"] = []
should_save = True
if img_next_to_file:
img_next_to_file_url = (
f"/weilin/prompt_ui/api/lorainfo/api/loras/img?file={file}"
)
if (
len(info_data["images"]) == 0
or info_data["images"][0]["url"] != img_next_to_file_url
):
info_data["images"].insert(0, {"url": img_next_to_file_url})
should_save = True
# If we just want light data then bail now with just existing data, plus file, path and img if
# next to the file.
if (
light
and not maybe_fetch_metadata
and not force_fetch_metadata
and not maybe_fetch_civitai
and not force_fetch_civitai
):
return info_data
if "raw" not in info_data:
info_data["raw"] = {}
should_save = True
should_save = _update_data(info_data) or should_save
should_fetch_civitai = force_fetch_civitai is True or (
maybe_fetch_civitai is True
and ("civitai" not in info_data["raw"] or len(info_data["raw"]["civitai"]) == 0)
)
should_fetch_metadata = force_fetch_metadata is True or (
maybe_fetch_metadata is True
and (
"metadata" not in info_data["raw"] or len(info_data["raw"]["metadata"]) == 0
)
)
if should_fetch_metadata:
data_meta = _get_model_metadata(
file, model_type=model_type, default={}, refresh=force_fetch_metadata
)
should_save = _merge_metadata(info_data, data_meta) or should_save
if should_fetch_civitai:
data_civitai = _get_model_civitai_data(
file, model_type=model_type, default={}, refresh=force_fetch_civitai
)
should_save = _merge_civitai_data(info_data, data_civitai) or should_save
if "sha256" not in info_data:
file_hash = _get_sha256_hash(file_path)
if file_hash is not None:
info_data["sha256"] = file_hash
should_save = True
if (
len(info_data["images"]) != 0 and file not in info_data["images"][0]["url"]
): # 未设置封面
file_name = os.path.basename(file)
url = next(filter(lambda x: x["type"] == "image", info_data["images"]), {}).get(
"url"
)
download_image(
url=url, filename=file_name, directory=os.path.dirname(file_path)
)
if should_save:
if "trainedWords" in info_data:
# Sort by count; if it doesn't exist, then assume it's a top item from civitai or elsewhere.
info_data["trainedWords"] = sorted(
info_data["trainedWords"],
key=lambda w: w["count"] if "count" in w else 99999,
reverse=True,
)
save_model_info(file, info_data, model_type=model_type)
# If we're saving, then the UI is likely waiting to see if the refreshed data is coming in.
# await PromptServer.instance.send("weilin-refreshed-lora-info", {"data": info_data})
return info_data
def download_image(url, filename, directory):
try:
# 安全处理文件名
filename = filename.encode("utf-8", "ignore").decode("utf-8")
_, ext = os.path.splitext(url)
filename, _ = os.path.splitext(filename)
filepath = os.path.join(directory, f"{filename}{ext}")
try:
resp = requests.get(url, stream=True)
resp.raise_for_status()
with open(filepath, "wb") as f:
for chunk in resp.iter_content(chunk_size=4096):
f.write(chunk)
except Exception as e:
print(e)
if os.path.exists(filepath):
os.remove(filepath)
except Exception as e:
print(f"文件名处理错误: {e}")
def _is_common_tag(tag: str) -> bool:
"""
判断是否为常见通用标签(非触发词)
这些标签通常出现在训练数据中,但不是真正的触发词
"""
# 常见通用标签列表(小写)
common_tags = {
# 人物相关
'1girl', '1boy', '2girls', '2boys', '3girls', '3boys', 'multiple girls', 'multiple boys',
'solo', 'couple', 'group',
# 身体特征
'long hair', 'short hair', 'white hair', 'black hair', 'blonde hair', 'brown hair',
'red hair', 'blue hair', 'green hair', 'pink hair', 'purple hair', 'silver hair',
'gradient hair', 'multicolored hair', 'two-tone hair',
'blue eyes', 'green eyes', 'brown eyes', 'red eyes', 'yellow eyes', 'purple eyes',
'black eyes', 'white eyes', 'orange eyes', 'pink eyes',
'small breasts', 'medium breasts', 'large breasts', 'huge breasts',
# 服装相关
'dress', 'white dress', 'black dress', 'red dress', 'blue dress', 'green dress',
'school uniform', 'swimsuit', 'bikini', 'maid', 'nurse', 'waitress',
'skirt', 'shorts', 'pants', 'jeans', 'shirt', 'blouse', 'jacket', 'coat',
# 姿势相关
'standing', 'sitting', 'lying', 'walking', 'running', 'jumping',
'looking at viewer', 'looking away', 'from behind', 'from side', 'from above', 'from below',
# 背景相关
'simple background', 'white background', 'black background', 'grey background',
'outdoors', 'indoors', 'sky', 'cloud', 'tree', 'flower', 'water', 'building',
# 画面风格
'highres', 'absurdres', 'best quality', 'high quality', 'normal quality', 'low quality',
'masterpiece', 'great quality', 'good quality', 'average quality', 'bad quality',
'worst quality', 'very bad quality',
# 其他常见标签
'smile', 'open mouth', 'closed mouth', 'blush', 'tears', 'sweat',
'gloves', 'shoes', 'boots', 'socks', 'stockings', 'pantyhose',
'hat', 'headwear', 'hair ornament', 'ribbon', 'bow', 'flower', 'jewelry',
'bangs', 'ponytail', 'twintails', 'braid', 'ahoge', 'sidelocks',
'pointy ears', 'animal ears', 'cat ears', 'dog ears', 'rabbit ears', 'fox ears',
'tail', 'cat tail', 'dog tail', 'fox tail', 'rabbit tail',
'wings', 'angel wings', 'demon wings', 'fairy wings',
'no humans', 'scenery', 'landscape', 'cityscape', 'seascape',
}
tag_lower = tag.lower().strip()
return tag_lower in common_tags
def _merge_metadata(info_data: dict, data_meta: dict) -> bool:
"""
合并模型元数据到info_data
优化版:按训练频率排序触发词,与 ComfyUI-Lora-Auto-Trigger-Words 保持一致
Returns true if data was saved.
"""
should_save = False
base_model_file = get_dict_value(data_meta, "ss_sd_model_name", None)
if base_model_file:
info_data["baseModelFile"] = base_model_file
# Loop over metadata tags
trained_words = {}
if "ss_tag_frequency" in data_meta:
tag_freq_data = data_meta["ss_tag_frequency"]
# 如果是字符串,需要解析JSON(某些模型存储为JSON字符串)
if isinstance(tag_freq_data, str):
try:
tag_freq_data = json.loads(tag_freq_data)
except json.JSONDecodeError:
tag_freq_data = {}
if isinstance(tag_freq_data, dict):
for bucket_value in tag_freq_data.values():
if isinstance(bucket_value, dict):
for tag, count in bucket_value.items():
tag = str(tag).strip()
if tag:
if tag not in trained_words:
trained_words[tag] = {"word": tag, "count": 0, "metadata": True}
trained_words[tag]["count"] = trained_words[tag]["count"] + count
if "trainedWords" not in info_data:
# 按训练次数降序排序(与 ComfyUI-Lora-Auto-Trigger-Words 保持一致)
sorted_words = sorted(trained_words.values(), key=lambda x: x["count"], reverse=True)
# 智能过滤:优先保留非通用标签
# 如果前几个标签都是通用标签,尝试找到第一个非通用标签放在最前面
filtered_words = []
non_common_words = []
for word_data in sorted_words:
if not _is_common_tag(word_data["word"]):
non_common_words.append(word_data)
else:
filtered_words.append(word_data)
# 如果有非通用标签,将它们放在前面
if non_common_words:
info_data["trainedWords"] = non_common_words + filtered_words
else:
info_data["trainedWords"] = sorted_words
should_save = True
else:
# We can't merge, because the list may have other data, like it's part of civitaidata.
merged_dict = {}
for existing_word_data in info_data["trainedWords"]:
merged_dict[existing_word_data["word"]] = existing_word_data
for new_key, new_word_data in trained_words.items():
if new_key not in merged_dict:
merged_dict[new_key] = {}
merged_dict[new_key] = {**merged_dict[new_key], **new_word_data}
# 按训练次数排序(有count的排前面,没有count的保持原顺序)
merged_list = list(merged_dict.values())
merged_list.sort(key=lambda x: x.get("count", 99999), reverse=True)
# 智能过滤:优先保留非通用标签
filtered_words = []
non_common_words = []
for word_data in merged_list:
if not _is_common_tag(word_data.get("word", "")):
non_common_words.append(word_data)
else:
filtered_words.append(word_data)
# 如果有非通用标签,将它们放在前面
if non_common_words:
info_data["trainedWords"] = non_common_words + filtered_words
else:
info_data["trainedWords"] = merged_list
should_save = True
# trained_words = list(trained_words.values())
# info_data['meta_trained_words'] = trained_words
info_data["raw"]["metadata"] = data_meta
should_save = True
if "sha256" not in info_data and "_sha256" in data_meta:
info_data["sha256"] = data_meta["_sha256"]
should_save = True
return should_save
def _merge_civitai_data(info_data: dict, data_civitai: dict) -> bool:
"""
合并Civitai数据到info_data
优化版:Civitai触发词作为最高优先级,放在trainedWords列表最前面
Returns true if data was saved.
"""
should_save = False
if "name" not in info_data:
info_data["name"] = get_dict_value(data_civitai, "model.name", "")
should_save = True
version_name = get_dict_value(data_civitai, "name")
if version_name is not None:
info_data["name"] += f" - {version_name}"
if "type" not in info_data:
info_data["type"] = get_dict_value(data_civitai, "model.type")
should_save = True
if "baseModel" not in info_data:
info_data["baseModel"] = get_dict_value(data_civitai, "baseModel")
should_save = True
# We always want to merge triggerword.
civitai_trigger = get_dict_value(data_civitai, "triggerWords", default=[])
civitai_trained = get_dict_value(data_civitai, "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_list = civitai_words.split(",")
if "trainedWords" not in info_data:
info_data["trainedWords"] = []
# 创建一个有序字典来保持Civitai触发词的顺序
existing_words = {data["word"]: data for data in info_data["trainedWords"]}
# 将Civitai触发词插入到列表开头(最高优先级)
civitai_word_data_list = []
for trigger_word in civitai_words_list:
trigger_word = trigger_word.strip()
if not trigger_word:
continue
if trigger_word in existing_words:
# 更新现有条目
existing_words[trigger_word]["civitai"] = True
else:
# 创建新条目
civitai_word_data_list.append({"word": trigger_word, "civitai": True})
# 合并:Civitai触发词在前,其他触发词在后
non_civitai_words = [data for data in info_data["trainedWords"] if not data.get("civitai", False)]
civitai_existing_words = [
existing_words.get(data["word"], data)
for data in info_data["trainedWords"]
if data.get("civitai", False) and data["word"] not in {w["word"] for w in civitai_word_data_list}
]
info_data["trainedWords"] = civitai_word_data_list + civitai_existing_words + non_civitai_words
should_save = True
if "sha256" not in info_data:
info_data["sha256"] = data_civitai["_sha256"]
should_save = True
if "modelId" in data_civitai:
info_data["links"] = info_data["links"] if "links" in info_data else []
civitai_link = (
f"https://civitai.com/models/{get_dict_value(data_civitai, 'modelId')}"
)
if get_dict_value(data_civitai, "id"):
civitai_link += f"?modelVersionId={get_dict_value(data_civitai, 'id')}"
info_data["links"].append(civitai_link)
info_data["links"].append(data_civitai["_civitai_api"])
should_save = True
# Take images from civitai
if "images" in data_civitai:
info_data_image_urls = list(
map(lambda i: i["url"] if "url" in i else None, info_data["images"])
)
for img in data_civitai["images"]:
img_url = get_dict_value(img, "url")
if img_url is not None and img_url not in info_data_image_urls:
img_id = (
os.path.splitext(os.path.basename(img_url))[0]
if img_url is not None
else None
)
img_data = {
"url": img_url,
"civitaiUrl": (
f"https://civitai.com/images/{img_id}"
if img_id is not None
else None
),
"width": get_dict_value(img, "width"),
"height": get_dict_value(img, "height"),
"type": get_dict_value(img, "type"),
"nsfwLevel": get_dict_value(img, "nsfwLevel"),
"seed": get_dict_value(img, "meta.seed"),
"positive": get_dict_value(img, "meta.prompt"),
"negative": get_dict_value(img, "meta.negativePrompt"),
"steps": get_dict_value(img, "meta.steps"),
"sampler": get_dict_value(img, "meta.sampler"),
"cfg": get_dict_value(img, "meta.cfgScale"),
"model": get_dict_value(img, "meta.Model"),
"resources": get_dict_value(img, "meta.resources"),
}
info_data["images"].append(img_data)
should_save = True
# The raw data
if "civitai" not in info_data["raw"]:
info_data["raw"]["civitai"] = data_civitai
should_save = True
return should_save
def _get_model_civitai_data(file: str, model_type="loras", default=None, refresh=False):
"""Gets the civitai data, either cached from the user directory, or from civitai api."""
file_hash = _get_sha256_hash(get_folder_path(file, model_type))
if file_hash is None:
return None
json_file_path = _get_info_cache_file(file_hash, "civitai")
api_url = f"https://civitai.com/api/v1/model-versions/by-hash/{file_hash}"
file_data = read_userdata_json(json_file_path)
if file_data is None or refresh is True:
try:
response = requests.get(api_url, timeout=5000)
data = response.json()
save_userdata_json(
json_file_path,
{
"url": api_url,
"timestamp": datetime.now().timestamp(),
"response": data,
},
)
file_data = read_userdata_json(json_file_path)
except requests.exceptions.RequestException as e: # This is the correct syntax
print(e)
response = (
file_data["response"]
if file_data is not None and "response" in file_data
else None
)
if response is not None:
response["_sha256"] = file_hash
response["_civitai_api"] = api_url
return response if response is not None else default
def _get_model_metadata(file: str, model_type="loras", default=None, refresh=False):
"""Gets the metadata from the file itself."""
file_path = get_folder_path(file, model_type)
file_hash = _get_sha256_hash(file_path)
if file_hash is None:
return default
json_file_path = _get_info_cache_file(file_hash, "metadata")
file_data = read_userdata_json(json_file_path)
if file_data is None or refresh is True:
data = _read_file_metadata_from_header(file_path)
if data is not None:
file_data = {
"url": file,
"timestamp": datetime.now().timestamp(),
"response": data,
}
save_userdata_json(json_file_path, file_data)
response = (
file_data["response"]
if file_data is not None and "response" in file_data
else None
)
if response is not None:
response["_sha256"] = file_hash
return response if response is not None else default
def _read_file_metadata_from_header(file_path: str) -> dict:
"""Reads the file's header and returns a JSON dict metdata if available."""
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:
raise BufferError("Invalid header size")
header = file.read(header_size)
if header is None:
raise BufferError("Invalid header")
header_json = json.loads(header)
data = (
header_json["__metadata__"]
if "__metadata__" in header_json
else None
)
if data is not None:
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:
print(f"metdata for field {key} did not parse as json")
except requests.exceptions.RequestException as e:
print(e)
data = None
return data
def _get_sha256_hash(file_path: str):
"""Returns the hash for the file."""
if not file_path or not path_exists(file_path):
return None
file_hash = None
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
# Read and update hash string value in blocks of 4K
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
file_hash = sha256_hash.hexdigest()
return file_hash
async def set_model_info_partial(file: str, info_data_partial, model_type="loras"):
"""Sets partial data into the existing model info data."""
info_data = await get_model_info(file, model_type=model_type, default={})
info_data = {**info_data, **info_data_partial}
save_model_info(file, info_data, model_type=model_type)
def save_model_info(file: str, info_data, model_type="loras"):
"""Saves the model info alongside the model itself."""
file_path = get_folder_path(file, model_type)
if file_path is None:
return
try_info_path = f"{file_path}.weilin-info.json"
save_json_file(try_info_path, info_data)
async def remove_user_diy_fields(file: str, fields_to_remove, model_type="loras"):
"""
从模型的user_diy_fields中删除指定字段
参数:
file: 模型文件名
fields_to_remove: 要删除的字段名,可以是字符串或字符串列表
model_type: 模型类型,默认为"loras"
"""
info_data = await get_model_info(file, model_type=model_type, default={})
if "user_diy_fileds" not in info_data: # 修正拼写错误
return False
if isinstance(fields_to_remove, str):
fields_to_remove = [fields_to_remove]
# print(fields_to_remove)
removed = False
for field in fields_to_remove:
if field in info_data["user_diy_fileds"]:
# print(f'删除字段: {field}')
del info_data["user_diy_fileds"][field]
removed = True
if removed:
save_model_info(file, info_data, model_type=model_type)
return removed
def get_dict_value(data: dict, dict_key: str, default=None):
"""Gets a deeply nested value given a dot-delimited key."""
keys = dict_key.split(".")
key = keys.pop(0) if len(keys) > 0 else None
found = data[key] if key in data else None
if found is not None and len(keys) > 0:
return get_dict_value(found, ".".join(keys), default)
return found if found is not None else default
def read_userdata_json(rel_path: str):
"""Reads a json file from the userdata directory."""
file_path = clean_path(rel_path)
return load_json_file(file_path)
def save_userdata_json(rel_path: str, data: dict):
"""Saves a json file from the userdata directory."""
file_path = clean_path(rel_path)
return save_json_file(file_path, data)
def clean_path(rel_path: str):
"""Cleans a relative path by splitting on forward slash and os.path.joining."""
cleaned = USERDATA
paths = rel_path.split("/")
for path in paths:
cleaned = os.path.join(cleaned, path)
return cleaned
def save_json_file(file_path: str, data: dict):
"""Saves a json file."""
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w+", encoding="UTF-8") as file:
json.dump(data, file, sort_keys=False, indent=2, separators=(",", ": "))
def _get_info_cache_file(data_type: str, file_hash: str):
return f"info/{file_hash}.{data_type}.json"
async def delete_model_info(
file: str, model_type="loras", del_info=True, del_metadata=True, del_civitai=True
):
"""Delete the info json, and the civitai & metadata caches."""
file_path = get_folder_path(file, model_type)
if file_path is None:
return
if del_info:
try_info_path = f"{file_path}.weilin-info.json"
if os.path.isfile(try_info_path):
os.remove(try_info_path)
if del_civitai or del_metadata:
file_hash = _get_sha256_hash(file_path)
if del_civitai:
json_file_path = _get_info_cache_file(file_hash, "civitai")
delete_userdata_file(json_file_path)
if del_metadata:
json_file_path = _get_info_cache_file(file_hash, "metadata")
delete_userdata_file(json_file_path)
def delete_userdata_file(rel_path: str):
"""Deletes a file from the userdata directory."""
file_path = clean_path(rel_path)
if os.path.isfile(file_path):
os.remove(file_path)