Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2406a1fd1 | ||
|
|
4132b2d8c4 | ||
|
|
40a1a7f43a | ||
|
|
14bb6f194d | ||
|
|
97b26549ce | ||
|
|
e75275dfff | ||
|
|
c1e89eb177 | ||
|
|
bfedcb2a7d | ||
|
|
1d01ce009f | ||
|
|
5c017137b0 | ||
|
|
00d23ff74f | ||
|
|
dc46f498be | ||
|
|
6d67b00b17 | ||
|
|
cda24405b5 | ||
|
|
6fa90be8c4 | ||
|
|
5a28789af7 | ||
|
|
dada903b2b | ||
|
|
e8916307aa | ||
|
|
8b6c6ebdea | ||
|
|
1796b101c5 |
@@ -65,4 +65,3 @@ There are three installation methods, choose one
|
||||
<img src="demo/scan-model-info.png" alt="Model Manager Demo Screenshot" style="max-width: 100%; max-height: 300px"/>
|
||||
|
||||
- Scan models and try to download information & preview.
|
||||
- Support migration from `cdb-boop/ComfyUI-Model-Manager/main`
|
||||
|
||||
44
__init__.py
44
__init__.py
@@ -23,7 +23,6 @@ if len(uninstalled_package) > 0:
|
||||
|
||||
# Init config settings
|
||||
config.extension_uri = extension_uri
|
||||
utils.resolve_model_base_paths()
|
||||
|
||||
version = utils.get_current_version()
|
||||
utils.download_web_distribution(version)
|
||||
@@ -90,12 +89,13 @@ async def delete_model_download_task(request):
|
||||
return web.json_response({"success": False, "error": error_msg})
|
||||
|
||||
|
||||
# @deprecated
|
||||
@routes.get("/model-manager/base-folders")
|
||||
async def get_model_paths(request):
|
||||
"""
|
||||
Returns the base folders for models.
|
||||
"""
|
||||
model_base_paths = config.model_base_paths
|
||||
model_base_paths = utils.resolve_model_base_paths(request)
|
||||
return web.json_response({"success": True, "data": model_base_paths})
|
||||
|
||||
|
||||
@@ -125,12 +125,12 @@ async def create_model(request):
|
||||
|
||||
|
||||
@routes.get("/model-manager/models")
|
||||
async def read_models(request):
|
||||
async def list_model_types(request):
|
||||
"""
|
||||
Scan all models and read their information.
|
||||
"""
|
||||
try:
|
||||
result = services.scan_models()
|
||||
result = utils.resolve_model_base_paths(request)
|
||||
return web.json_response({"success": True, "data": result})
|
||||
except Exception as e:
|
||||
error_msg = f"Read models failed: {str(e)}"
|
||||
@@ -138,6 +138,18 @@ async def read_models(request):
|
||||
return web.json_response({"success": False, "error": error_msg})
|
||||
|
||||
|
||||
@routes.get("/model-manager/models/{folder}")
|
||||
async def read_models(request):
|
||||
try:
|
||||
folder = request.match_info.get("folder", None)
|
||||
results = services.scan_models(folder, request)
|
||||
return web.json_response({"success": True, "data": results})
|
||||
except Exception as e:
|
||||
error_msg = f"Read models failed: {str(e)}"
|
||||
utils.print_error(error_msg)
|
||||
return web.json_response({"success": False, "error": error_msg})
|
||||
|
||||
|
||||
@routes.get("/model-manager/model/{type}/{index}/{filename:.*}")
|
||||
async def read_model_info(request):
|
||||
"""
|
||||
@@ -148,7 +160,7 @@ async def read_model_info(request):
|
||||
filename = request.match_info.get("filename", None)
|
||||
|
||||
try:
|
||||
model_path = utils.get_valid_full_path(model_type, index, filename)
|
||||
model_path = utils.get_valid_full_path(model_type, index, filename, request)
|
||||
result = services.get_model_info(model_path)
|
||||
return web.json_response({"success": True, "data": result})
|
||||
except Exception as e:
|
||||
@@ -177,10 +189,10 @@ async def update_model(request):
|
||||
model_data: dict = await request.json()
|
||||
|
||||
try:
|
||||
model_path = utils.get_valid_full_path(model_type, index, filename)
|
||||
model_path = utils.get_valid_full_path(model_type, index, filename, request)
|
||||
if model_path is None:
|
||||
raise RuntimeError(f"File {filename} not found")
|
||||
services.update_model(model_path, model_data)
|
||||
services.update_model(model_path, model_data, request)
|
||||
return web.json_response({"success": True})
|
||||
except Exception as e:
|
||||
error_msg = f"Update model failed: {str(e)}"
|
||||
@@ -198,7 +210,7 @@ async def delete_model(request):
|
||||
filename = request.match_info.get("filename", None)
|
||||
|
||||
try:
|
||||
model_path = utils.get_valid_full_path(model_type, index, filename)
|
||||
model_path = utils.get_valid_full_path(model_type, index, filename, request)
|
||||
if model_path is None:
|
||||
raise RuntimeError(f"File {filename} not found")
|
||||
services.remove_model(model_path)
|
||||
@@ -232,7 +244,7 @@ async def download_model_info(request):
|
||||
post = await utils.get_request_body(request)
|
||||
try:
|
||||
scan_mode = post.get("scanMode", "diff")
|
||||
await services.download_model_info(scan_mode)
|
||||
await services.download_model_info(scan_mode, request)
|
||||
return web.json_response({"success": True})
|
||||
except Exception as e:
|
||||
error_msg = f"Download model info failed: {str(e)}"
|
||||
@@ -282,20 +294,6 @@ async def read_download_preview(request):
|
||||
return web.FileResponse(preview_path)
|
||||
|
||||
|
||||
@routes.post("/model-manager/migrate")
|
||||
async def migrate_legacy_information(request):
|
||||
"""
|
||||
Migrate legacy information.
|
||||
"""
|
||||
try:
|
||||
await services.migrate_legacy_information()
|
||||
return web.json_response({"success": True})
|
||||
except Exception as e:
|
||||
error_msg = f"Download model info failed: {str(e)}"
|
||||
utils.print_error(error_msg)
|
||||
return web.json_response({"success": False, "error": error_msg})
|
||||
|
||||
|
||||
WEB_DIRECTORY = "web"
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
__all__ = ["WEB_DIRECTORY", "NODE_CLASS_MAPPINGS"]
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"prepare": "husky"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@types/lodash": "^4.17.9",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^22.5.5",
|
||||
|
||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -36,9 +36,6 @@ importers:
|
||||
specifier: ^2.6.0
|
||||
version: 2.6.0
|
||||
devDependencies:
|
||||
'@tailwindcss/container-queries':
|
||||
specifier: ^0.1.1
|
||||
version: 0.1.1(tailwindcss@3.4.12)
|
||||
'@types/lodash':
|
||||
specifier: ^4.17.9
|
||||
version: 4.17.9
|
||||
@@ -470,11 +467,6 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@tailwindcss/container-queries@0.1.1':
|
||||
resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==}
|
||||
peerDependencies:
|
||||
tailwindcss: '>=3.2.0'
|
||||
|
||||
'@types/estree@1.0.5':
|
||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||
|
||||
@@ -2000,10 +1992,6 @@ snapshots:
|
||||
'@rollup/rollup-win32-x64-msvc@4.22.0':
|
||||
optional: true
|
||||
|
||||
'@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.12)':
|
||||
dependencies:
|
||||
tailwindcss: 3.4.12
|
||||
|
||||
'@types/estree@1.0.5': {}
|
||||
|
||||
'@types/linkify-it@5.0.0': {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
extension_tag = "ComfyUI Model Manager"
|
||||
|
||||
extension_uri: str = None
|
||||
model_base_paths: dict[str, list[str]] = {}
|
||||
|
||||
|
||||
setting_key = {
|
||||
@@ -12,6 +11,10 @@ setting_key = {
|
||||
"download": {
|
||||
"max_task_count": "ModelManager.Download.MaxTaskCount",
|
||||
},
|
||||
"scan": {
|
||||
"include_hidden_files": "ModelManager.Scan.IncludeHiddenFiles",
|
||||
"exclude_scan_types": "ModelManager.Scan.excludeScanTypes",
|
||||
},
|
||||
}
|
||||
|
||||
user_agent = "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
|
||||
|
||||
@@ -167,7 +167,7 @@ async def create_model_download_task(task_data: dict, request):
|
||||
path_index = int(task_data.get("pathIndex", None))
|
||||
fullname = task_data.get("fullname", None)
|
||||
|
||||
model_path = utils.get_full_path(model_type, path_index, fullname)
|
||||
model_path = utils.get_full_path(model_type, path_index, fullname, request)
|
||||
# Check if the model path is valid
|
||||
if os.path.exists(model_path):
|
||||
raise RuntimeError(f"File already exists: {model_path}")
|
||||
@@ -261,6 +261,7 @@ async def download_model(task_id: str, request):
|
||||
|
||||
progress_interval = 1.0
|
||||
await download_model_file(
|
||||
request,
|
||||
task_id=task_id,
|
||||
headers=headers,
|
||||
progress_callback=report_progress,
|
||||
@@ -288,6 +289,7 @@ async def download_model(task_id: str, request):
|
||||
|
||||
|
||||
async def download_model_file(
|
||||
request,
|
||||
task_id: str,
|
||||
headers: dict,
|
||||
progress_callback: Callable[[TaskStatus], Awaitable[Any]],
|
||||
@@ -308,7 +310,7 @@ async def download_model_file(
|
||||
with open(description_file, "w", encoding="utf-8", newline="") as f:
|
||||
f.write(description)
|
||||
|
||||
model_path = utils.get_full_path(model_type, path_index, fullname)
|
||||
model_path = utils.get_full_path(model_type, path_index, fullname, request)
|
||||
|
||||
utils.rename_model(download_tmp_file, model_path)
|
||||
|
||||
@@ -361,9 +363,7 @@ async def download_model_file(
|
||||
)
|
||||
|
||||
if response.status_code not in (200, 206):
|
||||
raise RuntimeError(
|
||||
f"Failed to download {task_content.fullname}, status code: {response.status_code}"
|
||||
)
|
||||
raise RuntimeError(f"Failed to download {task_content.fullname}, status code: {response.status_code}")
|
||||
|
||||
# Some models require logging in before they can be downloaded.
|
||||
# If no token is carried, it will be redirected to the login page.
|
||||
@@ -376,9 +376,7 @@ async def download_model_file(
|
||||
# If it cannot be downloaded, a redirect will definitely occur.
|
||||
# Maybe consider getting the redirect url from response.history to make a judgment.
|
||||
# Here we also need to consider how different websites are processed.
|
||||
raise RuntimeError(
|
||||
f"{task_content.fullname} needs to be logged in to download. Please set the API-Key first."
|
||||
)
|
||||
raise RuntimeError(f"{task_content.fullname} needs to be logged in to download. Please set the API-Key first.")
|
||||
|
||||
# When parsing model information from HuggingFace API,
|
||||
# the file size was not found and needs to be obtained from the response header.
|
||||
|
||||
210
py/services.py
210
py/services.py
@@ -2,57 +2,51 @@ import os
|
||||
|
||||
import folder_paths
|
||||
|
||||
from . import config
|
||||
from . import utils
|
||||
from . import download
|
||||
from . import searcher
|
||||
|
||||
|
||||
def scan_models():
|
||||
def scan_models(folder: str, request):
|
||||
result = []
|
||||
model_base_paths = config.model_base_paths
|
||||
for model_type in model_base_paths:
|
||||
|
||||
folders, extensions = folder_paths.folder_names_and_paths[model_type]
|
||||
for path_index, base_path in enumerate(folders):
|
||||
files = utils.recursive_search_files(base_path)
|
||||
folders, extensions = folder_paths.folder_names_and_paths[folder]
|
||||
for path_index, base_path in enumerate(folders):
|
||||
files = utils.recursive_search_files(base_path, request)
|
||||
|
||||
models = folder_paths.filter_files_extensions(files, extensions)
|
||||
images = folder_paths.filter_files_content_types(files, ["image"])
|
||||
image_dict = utils.file_list_to_name_dict(images)
|
||||
models = folder_paths.filter_files_extensions(files, folder_paths.supported_pt_extensions)
|
||||
|
||||
for fullname in models:
|
||||
fullname = utils.normalize_path(fullname)
|
||||
basename = os.path.splitext(fullname)[0]
|
||||
extension = os.path.splitext(fullname)[1]
|
||||
for fullname in models:
|
||||
fullname = utils.normalize_path(fullname)
|
||||
basename = os.path.splitext(fullname)[0]
|
||||
extension = os.path.splitext(fullname)[1]
|
||||
|
||||
abs_path = utils.join_path(base_path, fullname)
|
||||
file_stats = os.stat(abs_path)
|
||||
abs_path = utils.join_path(base_path, fullname)
|
||||
file_stats = os.stat(abs_path)
|
||||
|
||||
# Resolve preview
|
||||
image_name = image_dict.get(basename, "no-preview.png")
|
||||
abs_image_path = utils.join_path(base_path, image_name)
|
||||
if os.path.isfile(abs_image_path):
|
||||
image_state = os.stat(abs_image_path)
|
||||
image_timestamp = round(image_state.st_mtime_ns / 1000000)
|
||||
image_name = f"{image_name}?ts={image_timestamp}"
|
||||
model_preview = (
|
||||
f"/model-manager/preview/{model_type}/{path_index}/{image_name}"
|
||||
)
|
||||
# Resolve preview
|
||||
image_name = utils.get_model_preview_name(abs_path)
|
||||
image_name = utils.join_path(os.path.dirname(fullname), image_name)
|
||||
abs_image_path = utils.join_path(base_path, image_name)
|
||||
if os.path.isfile(abs_image_path):
|
||||
image_state = os.stat(abs_image_path)
|
||||
image_timestamp = round(image_state.st_mtime_ns / 1000000)
|
||||
image_name = f"{image_name}?ts={image_timestamp}"
|
||||
model_preview = f"/model-manager/preview/{folder}/{path_index}/{image_name}"
|
||||
|
||||
model_info = {
|
||||
"fullname": fullname,
|
||||
"basename": basename,
|
||||
"extension": extension,
|
||||
"type": model_type,
|
||||
"pathIndex": path_index,
|
||||
"sizeBytes": file_stats.st_size,
|
||||
"preview": model_preview,
|
||||
"createdAt": round(file_stats.st_ctime_ns / 1000000),
|
||||
"updatedAt": round(file_stats.st_mtime_ns / 1000000),
|
||||
}
|
||||
model_info = {
|
||||
"fullname": fullname,
|
||||
"basename": basename,
|
||||
"extension": extension,
|
||||
"type": folder,
|
||||
"pathIndex": path_index,
|
||||
"sizeBytes": file_stats.st_size,
|
||||
"preview": model_preview,
|
||||
"createdAt": round(file_stats.st_ctime_ns / 1000000),
|
||||
"updatedAt": round(file_stats.st_mtime_ns / 1000000),
|
||||
}
|
||||
|
||||
result.append(model_info)
|
||||
result.append(model_info)
|
||||
|
||||
return result
|
||||
|
||||
@@ -75,7 +69,7 @@ def get_model_info(model_path: str):
|
||||
}
|
||||
|
||||
|
||||
def update_model(model_path: str, model_data: dict):
|
||||
def update_model(model_path: str, model_data: dict, request):
|
||||
|
||||
if "previewFile" in model_data:
|
||||
previewFile = model_data["previewFile"]
|
||||
@@ -93,7 +87,7 @@ def update_model(model_path: str, model_data: dict):
|
||||
raise RuntimeError("Invalid type or pathIndex or fullname")
|
||||
|
||||
# get new path
|
||||
new_model_path = utils.get_full_path(model_type, path_index, fullname)
|
||||
new_model_path = utils.get_full_path(model_type, path_index, fullname, request)
|
||||
|
||||
utils.rename_model(model_path, new_model_path)
|
||||
|
||||
@@ -140,20 +134,16 @@ def fetch_model_info(model_page: str):
|
||||
return result
|
||||
|
||||
|
||||
async def download_model_info(scan_mode: str):
|
||||
async def download_model_info(scan_mode: str, request):
|
||||
utils.print_info(f"Download model info for {scan_mode}")
|
||||
model_base_paths = config.model_base_paths
|
||||
model_base_paths = utils.resolve_model_base_paths(request)
|
||||
for model_type in model_base_paths:
|
||||
|
||||
folders, extensions = folder_paths.folder_names_and_paths[model_type]
|
||||
for path_index, base_path in enumerate(folders):
|
||||
files = utils.recursive_search_files(base_path)
|
||||
files = utils.recursive_search_files(base_path, request)
|
||||
|
||||
models = folder_paths.filter_files_extensions(files, extensions)
|
||||
images = folder_paths.filter_files_content_types(files, ["image"])
|
||||
image_dict = utils.file_list_to_name_dict(images)
|
||||
descriptions = folder_paths.filter_files_extensions(files, [".md"])
|
||||
description_dict = utils.file_list_to_name_dict(descriptions)
|
||||
models = folder_paths.filter_files_extensions(files, folder_paths.supported_pt_extensions)
|
||||
|
||||
for fullname in models:
|
||||
fullname = utils.normalize_path(fullname)
|
||||
@@ -161,22 +151,14 @@ async def download_model_info(scan_mode: str):
|
||||
|
||||
abs_model_path = utils.join_path(base_path, fullname)
|
||||
|
||||
image_name = image_dict.get(basename, "no-preview.png")
|
||||
image_name = utils.get_model_preview_name(abs_model_path)
|
||||
abs_image_path = utils.join_path(base_path, image_name)
|
||||
|
||||
has_preview = os.path.isfile(abs_image_path)
|
||||
|
||||
description_name = description_dict.get(basename, None)
|
||||
abs_description_path = (
|
||||
utils.join_path(base_path, description_name)
|
||||
if description_name
|
||||
else None
|
||||
)
|
||||
has_description = (
|
||||
os.path.isfile(abs_description_path)
|
||||
if abs_description_path
|
||||
else False
|
||||
)
|
||||
description_name = utils.get_model_description_name(abs_model_path)
|
||||
abs_description_path = utils.join_path(base_path, description_name) if description_name else None
|
||||
has_description = os.path.isfile(abs_description_path) if abs_description_path else False
|
||||
|
||||
try:
|
||||
|
||||
@@ -191,118 +173,18 @@ async def download_model_info(scan_mode: str):
|
||||
utils.print_debug(f"Calculate sha256 for {abs_model_path}")
|
||||
hash_value = utils.calculate_sha256(abs_model_path)
|
||||
utils.print_info(f"Searching model info by hash {hash_value}")
|
||||
model_info = searcher.CivitaiModelSearcher().search_by_hash(
|
||||
hash_value
|
||||
)
|
||||
model_info = searcher.CivitaiModelSearcher().search_by_hash(hash_value)
|
||||
|
||||
preview_url_list = model_info.get("preview", [])
|
||||
preview_image_url = (
|
||||
preview_url_list[0] if preview_url_list else None
|
||||
)
|
||||
preview_image_url = preview_url_list[0] if preview_url_list else None
|
||||
if preview_image_url:
|
||||
utils.print_debug(f"Save preview image to {abs_image_path}")
|
||||
utils.save_model_preview_image(
|
||||
abs_model_path, preview_image_url
|
||||
)
|
||||
utils.save_model_preview_image(abs_model_path, preview_image_url)
|
||||
|
||||
description = model_info.get("description", None)
|
||||
if description:
|
||||
utils.save_model_description(abs_model_path, description)
|
||||
except Exception as e:
|
||||
utils.print_error(
|
||||
f"Failed to download model info for {abs_model_path}: {e}"
|
||||
)
|
||||
utils.print_error(f"Failed to download model info for {abs_model_path}: {e}")
|
||||
|
||||
utils.print_debug("Completed scan model information.")
|
||||
|
||||
|
||||
async def migrate_legacy_information():
|
||||
import json
|
||||
import yaml
|
||||
from PIL import Image
|
||||
|
||||
utils.print_info(f"Migrating legacy information...")
|
||||
|
||||
model_base_paths = config.model_base_paths
|
||||
for model_type in model_base_paths:
|
||||
|
||||
folders, extensions = folder_paths.folder_names_and_paths[model_type]
|
||||
for path_index, base_path in enumerate(folders):
|
||||
files = utils.recursive_search_files(base_path)
|
||||
|
||||
models = folder_paths.filter_files_extensions(files, extensions)
|
||||
|
||||
for fullname in models:
|
||||
fullname = utils.normalize_path(fullname)
|
||||
|
||||
abs_model_path = utils.join_path(base_path, fullname)
|
||||
|
||||
base_file_name = os.path.splitext(abs_model_path)[0]
|
||||
|
||||
utils.print_debug(f"Try to migrate legacy info for {abs_model_path}")
|
||||
|
||||
preview_path = utils.join_path(
|
||||
os.path.dirname(abs_model_path),
|
||||
utils.get_model_preview_name(abs_model_path),
|
||||
)
|
||||
new_preview_path = f"{base_file_name}.webp"
|
||||
|
||||
if os.path.isfile(preview_path) and preview_path != new_preview_path:
|
||||
utils.print_info(f"Migrate preview image from {fullname}")
|
||||
with Image.open(preview_path) as image:
|
||||
image.save(new_preview_path, format="WEBP")
|
||||
os.remove(preview_path)
|
||||
|
||||
description_path = f"{base_file_name}.md"
|
||||
|
||||
metadata_info = {
|
||||
"website": "Civitai",
|
||||
}
|
||||
|
||||
url_info_path = f"{base_file_name}.url"
|
||||
if os.path.isfile(url_info_path):
|
||||
with open(url_info_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
if line.startswith("URL="):
|
||||
model_page_url = line[len("URL=") :].strip()
|
||||
metadata_info.update({"modelPage": model_page_url})
|
||||
|
||||
json_info_path = f"{base_file_name}.json"
|
||||
if os.path.isfile(json_info_path):
|
||||
with open(json_info_path, "r", encoding="utf-8") as f:
|
||||
version = json.load(f)
|
||||
metadata_info.update(
|
||||
{
|
||||
"baseModel": version.get("baseModel"),
|
||||
"preview": [i["url"] for i in version["images"]],
|
||||
}
|
||||
)
|
||||
|
||||
description_parts: list[str] = [
|
||||
"---",
|
||||
yaml.dump(metadata_info).strip(),
|
||||
"---",
|
||||
"",
|
||||
]
|
||||
|
||||
text_info_path = f"{base_file_name}.txt"
|
||||
if os.path.isfile(text_info_path):
|
||||
with open(text_info_path, "r", encoding="utf-8") as f:
|
||||
description_parts.append(f.read())
|
||||
|
||||
description_path = f"{base_file_name}.md"
|
||||
|
||||
if os.path.isfile(text_info_path):
|
||||
utils.print_info(f"Migrate description from {fullname}")
|
||||
with open(description_path, "w", encoding="utf-8", newline="") as f:
|
||||
f.write("\n".join(description_parts))
|
||||
|
||||
def try_to_remove_file(file_path):
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
try_to_remove_file(url_info_path)
|
||||
try_to_remove_file(text_info_path)
|
||||
try_to_remove_file(json_info_path)
|
||||
|
||||
utils.print_debug("Completed migrate model information.")
|
||||
|
||||
73
py/utils.py
73
py/utils.py
@@ -103,9 +103,7 @@ def download_web_distribution(version: str):
|
||||
|
||||
print_info("Extracting web distribution...")
|
||||
with tarfile.open(temp_file, "r:gz") as tar:
|
||||
members = [
|
||||
member for member in tar.getmembers() if member.name.startswith("web/")
|
||||
]
|
||||
members = [member for member in tar.getmembers() if member.name.startswith("web/")]
|
||||
tar.extractall(path=config.extension_uri, members=members)
|
||||
|
||||
os.remove(temp_file)
|
||||
@@ -118,23 +116,26 @@ def download_web_distribution(version: str):
|
||||
print_error(f"An unexpected error occurred: {e}")
|
||||
|
||||
|
||||
def resolve_model_base_paths():
|
||||
def resolve_model_base_paths(request):
|
||||
folders = list(folder_paths.folder_names_and_paths.keys())
|
||||
config.model_base_paths = {}
|
||||
model_base_paths = {}
|
||||
folder_black_list = ["configs", "custom_nodes"]
|
||||
custom_folders = get_setting_value(request, "scan.exclude_scan_types", "")
|
||||
custom_black_list = [f.strip() for f in custom_folders.split(",") if f.strip()]
|
||||
folder_black_list.extend(custom_black_list)
|
||||
for folder in folders:
|
||||
if folder == "configs":
|
||||
continue
|
||||
if folder == "custom_nodes":
|
||||
if folder in folder_black_list:
|
||||
continue
|
||||
folders = folder_paths.get_folder_paths(folder)
|
||||
config.model_base_paths[folder] = [normalize_path(f) for f in folders]
|
||||
model_base_paths[folder] = [normalize_path(f) for f in folders]
|
||||
return model_base_paths
|
||||
|
||||
|
||||
def get_full_path(model_type: str, path_index: int, filename: str):
|
||||
def get_full_path(model_type: str, path_index: int, filename: str, request):
|
||||
"""
|
||||
Get the absolute path in the model type through string concatenation.
|
||||
"""
|
||||
folders = config.model_base_paths.get(model_type, [])
|
||||
folders = resolve_model_base_paths(request).get(model_type, [])
|
||||
if not path_index < len(folders):
|
||||
raise RuntimeError(f"PathIndex {path_index} is not in {model_type}")
|
||||
base_path = folders[path_index]
|
||||
@@ -142,11 +143,11 @@ def get_full_path(model_type: str, path_index: int, filename: str):
|
||||
return full_path
|
||||
|
||||
|
||||
def get_valid_full_path(model_type: str, path_index: int, filename: str):
|
||||
def get_valid_full_path(model_type: str, path_index: int, filename: str, request):
|
||||
"""
|
||||
Like get_full_path but it will check whether the file is valid.
|
||||
"""
|
||||
folders = config.model_base_paths.get(model_type, [])
|
||||
folders = resolve_model_base_paths(request).get(model_type, [])
|
||||
if not path_index < len(folders):
|
||||
raise RuntimeError(f"PathIndex {path_index} is not in {model_type}")
|
||||
base_path = folders[path_index]
|
||||
@@ -154,9 +155,7 @@ def get_valid_full_path(model_type: str, path_index: int, filename: str):
|
||||
if os.path.isfile(full_path):
|
||||
return full_path
|
||||
elif os.path.islink(full_path):
|
||||
raise RuntimeError(
|
||||
f"WARNING path {full_path} exists but doesn't link anywhere, skipping."
|
||||
)
|
||||
raise RuntimeError(f"WARNING path {full_path} exists but doesn't link anywhere, skipping.")
|
||||
|
||||
|
||||
def get_download_path():
|
||||
@@ -166,11 +165,29 @@ def get_download_path():
|
||||
return download_path
|
||||
|
||||
|
||||
def recursive_search_files(directory: str):
|
||||
files, folder_all = folder_paths.recursive_search(
|
||||
directory, excluded_dir_names=[".git"]
|
||||
)
|
||||
return [normalize_path(f) for f in files]
|
||||
def recursive_search_files(directory: str, request):
|
||||
if not os.path.isdir(directory):
|
||||
return []
|
||||
|
||||
excluded_dir_names = [".git"]
|
||||
result = []
|
||||
include_hidden_files = get_setting_value(request, "scan.include_hidden_files", False)
|
||||
|
||||
for dirpath, subdirs, filenames in os.walk(directory, followlinks=True, topdown=True):
|
||||
subdirs[:] = [d for d in subdirs if d not in excluded_dir_names]
|
||||
if not include_hidden_files:
|
||||
subdirs[:] = [d for d in subdirs if not d.startswith(".")]
|
||||
filenames[:] = [f for f in filenames if not f.startswith(".")]
|
||||
|
||||
for file_name in filenames:
|
||||
try:
|
||||
relative_path = os.path.relpath(os.path.join(dirpath, file_name), directory)
|
||||
result.append(relative_path)
|
||||
except:
|
||||
logging.warning(f"Warning: Unable to access {file_name}. Skipping this file.")
|
||||
continue
|
||||
|
||||
return [normalize_path(f) for f in result]
|
||||
|
||||
|
||||
def search_files(directory: str):
|
||||
@@ -220,6 +237,14 @@ def get_model_all_images(model_path: str):
|
||||
|
||||
def get_model_preview_name(model_path: str):
|
||||
images = get_model_all_images(model_path)
|
||||
basename = os.path.splitext(os.path.basename(model_path))[0]
|
||||
|
||||
for image in images:
|
||||
image_name = os.path.splitext(image)[0]
|
||||
image_ext = os.path.splitext(image)[1]
|
||||
if image_name == basename and image_ext.lower() == ".webp":
|
||||
return image
|
||||
|
||||
return images[0] if len(images) > 0 else "no-preview.png"
|
||||
|
||||
|
||||
@@ -267,12 +292,6 @@ def save_model_description(model_path: str, content: Any):
|
||||
|
||||
base_dirname = os.path.dirname(model_path)
|
||||
|
||||
# remove old descriptions
|
||||
old_descriptions = get_model_all_descriptions(model_path)
|
||||
for desc in old_descriptions:
|
||||
description_path = join_path(base_dirname, desc)
|
||||
os.remove(description_path)
|
||||
|
||||
# save new description
|
||||
basename = os.path.splitext(os.path.basename(model_path))[0]
|
||||
extension = ".md"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[project]
|
||||
name = "comfyui-model-manager"
|
||||
description = "Manage models: browsing, download and delete."
|
||||
version = "2.1.0"
|
||||
license = "LICENSE"
|
||||
version = "2.2.1"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["markdownify"]
|
||||
|
||||
[project.urls]
|
||||
@@ -13,3 +13,6 @@ Repository = "https://github.com/hayden-fr/ComfyUI-Model-Manager"
|
||||
PublisherId = "hayden"
|
||||
DisplayName = "ComfyUI-Model-Manager"
|
||||
Icon = ""
|
||||
|
||||
[tool.black]
|
||||
line-length = 160
|
||||
|
||||
11
src/App.vue
11
src/App.vue
@@ -15,16 +15,18 @@ import { useStoreProvider } from 'hooks/store'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import GlobalConfirm from 'primevue/confirmdialog'
|
||||
import { $el, app, ComfyButton } from 'scripts/comfyAPI'
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { dialog, models, config, download } = useStoreProvider()
|
||||
const { toast } = useToast()
|
||||
|
||||
const firstOpenManager = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
const refreshModelsAndConfig = async () => {
|
||||
await Promise.all([models.refresh(), config.refresh()])
|
||||
await Promise.all([models.refresh(true)])
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Refreshed Models',
|
||||
@@ -50,6 +52,11 @@ onMounted(() => {
|
||||
const openManagerDialog = () => {
|
||||
const { cardWidth, gutter, aspect } = config
|
||||
|
||||
if (firstOpenManager.value) {
|
||||
models.refresh(true)
|
||||
firstOpenManager.value = false
|
||||
}
|
||||
|
||||
dialog.open({
|
||||
key: 'model-manager',
|
||||
title: t('modelManager'),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-4">
|
||||
<div class="whitespace-nowrap px-4 @container">
|
||||
<div class="flex gap-4 @sm:justify-end">
|
||||
<div class="whitespace-nowrap px-4" v-container="container">
|
||||
<div :class="['flex gap-4', $sm('justify-end')]">
|
||||
<Button
|
||||
class="w-full @sm:w-auto"
|
||||
:class="[$sm('w-auto', 'w-full')]"
|
||||
:label="$t('createDownloadTask')"
|
||||
@click="openCreateTask"
|
||||
></Button>
|
||||
@@ -73,6 +73,7 @@
|
||||
<script setup lang="ts">
|
||||
import DialogCreateTask from 'components/DialogCreateTask.vue'
|
||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||
import { useContainerQueries } from 'hooks/container'
|
||||
import { useDialog } from 'hooks/dialog'
|
||||
import { useDownload } from 'hooks/download'
|
||||
import Button from 'primevue/button'
|
||||
@@ -90,4 +91,7 @@ const openCreateTask = () => {
|
||||
content: DialogCreateTask,
|
||||
})
|
||||
}
|
||||
|
||||
const container = Symbol('container')
|
||||
const { $sm } = useContainerQueries(container)
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex h-full flex-col gap-4 overflow-hidden @container/content"
|
||||
:style="{
|
||||
['--card-width']: `${cardWidth}px`,
|
||||
['--gutter']: `${gutter}px`,
|
||||
}"
|
||||
class="flex h-full flex-col gap-4 overflow-hidden"
|
||||
v-resize="onContainerResize"
|
||||
v-container="contentContainer"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'grid grid-cols-1 justify-center gap-4 px-8',
|
||||
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
|
||||
'@lg/content:gap-[var(--gutter)]',
|
||||
'@lg/content:px-4',
|
||||
]"
|
||||
class="grid grid-cols-1 justify-center gap-4 px-8"
|
||||
:style="$content_lg(contentStyle)"
|
||||
>
|
||||
<div class="col-span-full @container/toolbar">
|
||||
<div :class="['flex flex-col gap-4', '@2xl/toolbar:flex-row']">
|
||||
<div class="col-span-full" v-container="toolbarContainer">
|
||||
<div class="flex flex-col gap-4" :style="$toolbar_2xl(toolbarStyle)">
|
||||
<ResponseInput
|
||||
v-model="searchContent"
|
||||
:placeholder="$t('searchModels')"
|
||||
@@ -48,12 +41,8 @@
|
||||
>
|
||||
<template #item="{ item }">
|
||||
<div
|
||||
:class="[
|
||||
'grid grid-cols-1 justify-center gap-8 px-8',
|
||||
'@lg/content:grid-cols-[repeat(auto-fit,var(--card-width))]',
|
||||
'@lg/content:gap-[var(--gutter)]',
|
||||
'@lg/content:px-4',
|
||||
]"
|
||||
class="grid grid-cols-1 justify-center gap-8 px-8"
|
||||
:style="contentStyle"
|
||||
>
|
||||
<ModelCard
|
||||
v-for="model in item"
|
||||
@@ -80,6 +69,7 @@ import ResponseInput from 'components/ResponseInput.vue'
|
||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||
import ResponseSelect from 'components/ResponseSelect.vue'
|
||||
import { useConfig } from 'hooks/config'
|
||||
import { useContainerQueries } from 'hooks/container'
|
||||
import { useModels } from 'hooks/model'
|
||||
import { defineResizeCallback } from 'hooks/resize'
|
||||
import { chunk } from 'lodash'
|
||||
@@ -88,9 +78,9 @@ import { genModelKey } from 'utils/model'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { isMobile, cardWidth, gutter, aspect, modelFolders } = useConfig()
|
||||
const { isMobile, cardWidth, gutter, aspect } = useConfig()
|
||||
|
||||
const { data } = useModels()
|
||||
const { data, folders } = useModels()
|
||||
const { t } = useI18n()
|
||||
|
||||
const responseScroll = ref()
|
||||
@@ -99,7 +89,7 @@ const searchContent = ref<string>()
|
||||
|
||||
const currentType = ref('all')
|
||||
const typeOptions = computed(() => {
|
||||
return ['all', ...Object.keys(modelFolders.value)].map((type) => {
|
||||
return ['all', ...Object.keys(folders.value)].map((type) => {
|
||||
return {
|
||||
label: type,
|
||||
value: type,
|
||||
@@ -143,7 +133,9 @@ const colSpan = ref(1)
|
||||
const colSpanWidth = ref(cardWidth)
|
||||
|
||||
const list = computed(() => {
|
||||
const filterList = data.value.filter((model) => {
|
||||
const mergedList = Object.values(data.value).flat()
|
||||
|
||||
const filterList = mergedList.filter((model) => {
|
||||
const showAllModel = currentType.value === 'all'
|
||||
|
||||
const matchType = showAllModel || model.type === currentType.value
|
||||
@@ -177,6 +169,22 @@ const list = computed(() => {
|
||||
return chunk(sortedList, colSpan.value)
|
||||
})
|
||||
|
||||
const toolbarContainer = Symbol('toolbar')
|
||||
const { $2xl: $toolbar_2xl } = useContainerQueries(toolbarContainer)
|
||||
|
||||
const contentContainer = Symbol('content')
|
||||
const { $lg: $content_lg } = useContainerQueries(contentContainer)
|
||||
|
||||
const contentStyle = {
|
||||
gridTemplateColumns: `repeat(auto-fit, ${cardWidth}px)`,
|
||||
gap: `${gutter}px`,
|
||||
paddingLeft: `1rem`,
|
||||
paddingRight: `1rem`,
|
||||
}
|
||||
const toolbarStyle = {
|
||||
flexDirection: 'row',
|
||||
}
|
||||
|
||||
const onContainerResize = defineResizeCallback((entries) => {
|
||||
const entry = entries[0]
|
||||
if (isMobile.value) {
|
||||
|
||||
@@ -49,15 +49,13 @@
|
||||
<script setup lang="ts">
|
||||
import ResponseInput from 'components/ResponseInput.vue'
|
||||
import ResponseSelect from 'components/ResponseSelect.vue'
|
||||
import { useConfig } from 'hooks/config'
|
||||
import { useModelBaseInfo } from 'hooks/model'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const editable = defineModel<boolean>('editable')
|
||||
|
||||
const { modelFolders } = useConfig()
|
||||
|
||||
const { baseInfo, pathIndex, basename, extension, type } = useModelBaseInfo()
|
||||
const { baseInfo, pathIndex, basename, extension, type, modelFolders } =
|
||||
useModelBaseInfo()
|
||||
|
||||
const typeOptions = computed(() => {
|
||||
return Object.keys(modelFolders.value).map((curr) => {
|
||||
|
||||
@@ -20,7 +20,12 @@
|
||||
<div class="relative h-full w-full text-white">
|
||||
<div class="absolute bottom-0 left-0">
|
||||
<div class="drop-shadow-[0px_2px_2px_rgba(0,0,0,0.75)]">
|
||||
<div class="line-clamp-3 break-all text-2xl font-bold @lg:text-lg">
|
||||
<div
|
||||
:class="[
|
||||
'line-clamp-3 break-all font-bold',
|
||||
$lg('text-lg', 'text-2xl'),
|
||||
]"
|
||||
>
|
||||
{{ model.basename }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -29,7 +34,7 @@
|
||||
<div class="absolute left-0 top-0 w-full">
|
||||
<div class="flex flex-row items-start justify-between">
|
||||
<div class="flex items-center rounded-full bg-black/30 px-3 py-2">
|
||||
<div class="font-bold @lg:text-xs">
|
||||
<div :class="['font-bold', $lg('text-xs')]">
|
||||
{{ model.type }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,6 +71,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import DialogModelDetail from 'components/DialogModelDetail.vue'
|
||||
import { useContainerQueries } from 'hooks/container'
|
||||
import { useDialog } from 'hooks/dialog'
|
||||
import { useModelNodeAction } from 'hooks/model'
|
||||
import Button from 'primevue/button'
|
||||
@@ -101,4 +107,6 @@ const preview = computed(() =>
|
||||
|
||||
const { addModelNode, dragToAddModelNode, copyModelNode, loadPreviewWorkflow } =
|
||||
useModelNodeAction(props.model)
|
||||
|
||||
const { $lg } = useContainerQueries()
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<template>
|
||||
<form
|
||||
class="@container"
|
||||
@submit.prevent="handleSubmit"
|
||||
@reset.prevent="handleReset"
|
||||
v-container="container"
|
||||
>
|
||||
<div class="mx-auto w-full max-w-[50rem]">
|
||||
<div class="relative flex flex-col gap-4 overflow-hidden @xl:flex-row">
|
||||
<div
|
||||
:class="[
|
||||
'relative flex gap-4 overflow-hidden',
|
||||
$xl('flex-row', 'flex-col'),
|
||||
]"
|
||||
>
|
||||
<ModelPreview
|
||||
class="shrink-0"
|
||||
v-model:editable="editable"
|
||||
@@ -43,6 +48,7 @@ import ModelBaseInfo from 'components/ModelBaseInfo.vue'
|
||||
import ModelDescription from 'components/ModelDescription.vue'
|
||||
import ModelMetadata from 'components/ModelMetadata.vue'
|
||||
import ModelPreview from 'components/ModelPreview.vue'
|
||||
import { useContainerQueries } from 'hooks/container'
|
||||
import {
|
||||
useModelBaseInfoEditor,
|
||||
useModelDescriptionEditor,
|
||||
@@ -94,4 +100,7 @@ watch(
|
||||
handleReset()
|
||||
},
|
||||
)
|
||||
|
||||
const container = Symbol('container')
|
||||
const { $xl } = useContainerQueries(container)
|
||||
</script>
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-4"
|
||||
:style="{ ['--preview-width']: `${cardWidth}px` }"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<div
|
||||
:class="[
|
||||
'relative mx-auto w-full',
|
||||
'@sm:w-[var(--preview-width)]',
|
||||
'overflow-hidden rounded-lg preview-aspect',
|
||||
]"
|
||||
class="relative mx-auto w-full overflow-hidden rounded-lg preview-aspect"
|
||||
:style="$sm({ width: `${cardWidth}px` })"
|
||||
>
|
||||
<ResponseImage :src="preview" :error="noPreviewContent"></ResponseImage>
|
||||
|
||||
@@ -52,7 +46,7 @@
|
||||
:class="[
|
||||
'flex h-10 items-center gap-4',
|
||||
'absolute left-1/2 -translate-x-1/2',
|
||||
'@xl:left-0 @xl:translate-x-0',
|
||||
$xl('left-0 translate-x-0'),
|
||||
]"
|
||||
>
|
||||
<Button
|
||||
@@ -92,6 +86,7 @@ import ResponseFileUpload from 'components/ResponseFileUpload.vue'
|
||||
import ResponseImage from 'components/ResponseImage.vue'
|
||||
import ResponseInput from 'components/ResponseInput.vue'
|
||||
import { useConfig } from 'hooks/config'
|
||||
import { useContainerQueries } from 'hooks/container'
|
||||
import { useModelPreview } from 'hooks/model'
|
||||
import Button from 'primevue/button'
|
||||
import Carousel from 'primevue/carousel'
|
||||
@@ -109,4 +104,6 @@ const {
|
||||
updateLocalContent,
|
||||
noPreviewContent,
|
||||
} = useModelPreview()
|
||||
|
||||
const { $sm, $xl } = useContainerQueries()
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { request, useRequest } from 'hooks/request'
|
||||
import { request } from 'hooks/request'
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { $el, app, ComfyDialog } from 'scripts/comfyAPI'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from './toast'
|
||||
|
||||
export const useConfig = defineStore('config', (store) => {
|
||||
const mobileDeviceBreakPoint = 759
|
||||
const isMobile = ref(window.innerWidth < mobileDeviceBreakPoint)
|
||||
|
||||
type ModelFolder = Record<string, string[]>
|
||||
const { data: modelFolders, refresh: refreshModelFolders } =
|
||||
useRequest<ModelFolder>('/base-folders')
|
||||
|
||||
const checkDeviceType = () => {
|
||||
isMobile.value = window.innerWidth < mobileDeviceBreakPoint
|
||||
}
|
||||
@@ -24,17 +21,11 @@ export const useConfig = defineStore('config', (store) => {
|
||||
window.removeEventListener('resize', checkDeviceType)
|
||||
})
|
||||
|
||||
const refresh = async () => {
|
||||
return Promise.all([refreshModelFolders()])
|
||||
}
|
||||
|
||||
const config = {
|
||||
isMobile,
|
||||
gutter: 16,
|
||||
cardWidth: 240,
|
||||
aspect: 7 / 9,
|
||||
modelFolders,
|
||||
refresh,
|
||||
}
|
||||
|
||||
useAddConfigSettings(store)
|
||||
@@ -52,6 +43,7 @@ declare module 'hooks/store' {
|
||||
|
||||
function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
const { toast } = useToast()
|
||||
const { t } = useI18n()
|
||||
|
||||
const confirm = (opts: {
|
||||
message?: string
|
||||
@@ -89,6 +81,7 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
// API keys
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.APIKey.HuggingFace',
|
||||
category: [t('modelManager'), t('setting.apiKey'), 'HuggingFace'],
|
||||
name: 'HuggingFace API Key',
|
||||
type: 'text',
|
||||
defaultValue: undefined,
|
||||
@@ -96,61 +89,17 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.APIKey.Civitai',
|
||||
category: [t('modelManager'), t('setting.apiKey'), 'Civitai'],
|
||||
name: 'Civitai API Key',
|
||||
type: 'text',
|
||||
defaultValue: undefined,
|
||||
})
|
||||
|
||||
// Migrate
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.Migrate.Migrate',
|
||||
name: 'Migrate information from cdb-boop/main',
|
||||
defaultValue: '',
|
||||
type: () => {
|
||||
return $el('button.p-button.p-component.p-button-secondary', {
|
||||
textContent: 'Migrate',
|
||||
onclick: () => {
|
||||
confirm({
|
||||
message: [
|
||||
'This operation will delete old files and override current files if it exists.',
|
||||
// 'This may take a while and generate MANY server requests!',
|
||||
'Continue?',
|
||||
].join('\n'),
|
||||
accept: () => {
|
||||
store.loading.loading.value = true
|
||||
request('/migrate', {
|
||||
method: 'POST',
|
||||
})
|
||||
.then(() => {
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Complete migration',
|
||||
life: 2000,
|
||||
})
|
||||
store.models.refresh()
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: err.message ?? 'Failed to migrate information',
|
||||
life: 15000,
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
store.loading.loading.value = false
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
// Scan information
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.ScanFiles.Full',
|
||||
name: "Override all models' information and preview",
|
||||
category: [t('modelManager'), t('setting.scan'), 'Full'],
|
||||
name: t('setting.scanAll'),
|
||||
defaultValue: '',
|
||||
type: () => {
|
||||
return $el('button.p-button.p-component.p-button-secondary', {
|
||||
@@ -196,7 +145,8 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.ScanFiles.Incremental',
|
||||
name: 'Download missing information or preview',
|
||||
category: [t('modelManager'), t('setting.scan'), 'Incremental'],
|
||||
name: t('setting.scanMissing'),
|
||||
defaultValue: '',
|
||||
type: () => {
|
||||
return $el('button.p-button.p-component.p-button-secondary', {
|
||||
@@ -239,5 +189,21 @@ function useAddConfigSettings(store: import('hooks/store').StoreProvider) {
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.Scan.excludeScanTypes',
|
||||
category: [t('modelManager'), t('setting.scan'), 'ExcludeScanTypes'],
|
||||
name: t('setting.excludeScanTypes'),
|
||||
defaultValue: undefined,
|
||||
type: 'text',
|
||||
})
|
||||
|
||||
app.ui?.settings.addSetting({
|
||||
id: 'ModelManager.Scan.IncludeHiddenFiles',
|
||||
category: [t('modelManager'), t('setting.scan'), 'IncludeHiddenFiles'],
|
||||
name: t('setting.includeHiddenFiles'),
|
||||
defaultValue: false,
|
||||
type: 'boolean',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
60
src/hooks/container.ts
Normal file
60
src/hooks/container.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { defineResizeCallback } from 'hooks/resize'
|
||||
import { computed, Directive, inject, InjectionKey, provide, ref } from 'vue'
|
||||
|
||||
const globalContainerSize = ref<Record<symbol, number>>({})
|
||||
|
||||
const containerNameKey = Symbol('containerName') as InjectionKey<symbol>
|
||||
|
||||
export const containerDirective: Directive<HTMLElement, symbol> = {
|
||||
mounted: (el, binding) => {
|
||||
const containerName = binding.value || Symbol('container')
|
||||
const resizeCallback = defineResizeCallback((entries) => {
|
||||
const entry = entries[0]
|
||||
globalContainerSize.value[containerName] = entry.contentRect.width
|
||||
})
|
||||
const observer = new ResizeObserver(resizeCallback)
|
||||
observer.observe(el)
|
||||
el['_containerObserver'] = observer
|
||||
},
|
||||
unmounted: (el) => {
|
||||
const observer = el['_containerObserver']
|
||||
observer.disconnect()
|
||||
},
|
||||
}
|
||||
|
||||
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
|
||||
export const useContainerQueries = (containerName?: symbol) => {
|
||||
const parentContainer = inject(containerNameKey, Symbol('unknown'))
|
||||
|
||||
const name = containerName ?? parentContainer
|
||||
|
||||
provide(containerNameKey, name)
|
||||
|
||||
const currentContainerSize = computed(() => {
|
||||
return globalContainerSize.value[name] ?? 0
|
||||
})
|
||||
|
||||
/**
|
||||
* @param size unit rem
|
||||
*/
|
||||
const generator = (size: number) => {
|
||||
return (content: any, defaultContent: any = undefined) => {
|
||||
return currentContainerSize.value > size * rem ? content : defaultContent
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
$xs: generator(20),
|
||||
$sm: generator(24),
|
||||
$md: generator(28),
|
||||
$lg: generator(32),
|
||||
$xl: generator(36),
|
||||
$2xl: generator(42),
|
||||
$3xl: generator(48),
|
||||
$4xl: generator(54),
|
||||
$5xl: generator(60),
|
||||
$6xl: generator(66),
|
||||
$7xl: generator(72),
|
||||
}
|
||||
}
|
||||
@@ -38,19 +38,19 @@ declare module 'hooks/store' {
|
||||
}
|
||||
|
||||
export const useLoading = () => {
|
||||
const timer = ref<NodeJS.Timeout>()
|
||||
const targetTimer = ref<Record<string, NodeJS.Timeout | undefined>>({})
|
||||
|
||||
const show = () => {
|
||||
timer.value = setTimeout(() => {
|
||||
timer.value = undefined
|
||||
const show = (target: string = '_default') => {
|
||||
targetTimer.value[target] = setTimeout(() => {
|
||||
targetTimer.value[target] = undefined
|
||||
globalLoading.show()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const hide = () => {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value)
|
||||
timer.value = undefined
|
||||
const hide = (target: string = '_default') => {
|
||||
if (targetTimer.value[target]) {
|
||||
clearTimeout(targetTimer.value[target])
|
||||
targetTimer.value[target] = undefined
|
||||
} else {
|
||||
globalLoading.hide()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useConfig } from 'hooks/config'
|
||||
import { useLoading } from 'hooks/loading'
|
||||
import { useMarkdown } from 'hooks/markdown'
|
||||
import { request, useRequest } from 'hooks/request'
|
||||
import { request } from 'hooks/request'
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import { cloneDeep } from 'lodash'
|
||||
@@ -22,12 +21,46 @@ import {
|
||||
} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
type ModelFolder = Record<string, string[]>
|
||||
|
||||
const modelFolderProvideKey = Symbol('modelFolder')
|
||||
|
||||
export const useModels = defineStore('models', (store) => {
|
||||
const { data, refresh } = useRequest<Model[]>('/models', { defaultValue: [] })
|
||||
const { toast, confirm } = useToast()
|
||||
const { t } = useI18n()
|
||||
const loading = useLoading()
|
||||
|
||||
const folders = ref<ModelFolder>({})
|
||||
const refreshFolders = async () => {
|
||||
return request('/models').then((resData) => {
|
||||
folders.value = resData
|
||||
})
|
||||
}
|
||||
|
||||
provide(modelFolderProvideKey, folders)
|
||||
|
||||
const models = ref<Record<string, Model[]>>({})
|
||||
|
||||
const refreshModels = async (folder: string) => {
|
||||
loading.show(folder)
|
||||
return request(`/models/${folder}`)
|
||||
.then((resData) => {
|
||||
models.value[folder] = resData
|
||||
return resData
|
||||
})
|
||||
.finally(() => {
|
||||
loading.hide(folder)
|
||||
})
|
||||
}
|
||||
|
||||
const refreshAllModels = async (force = false) => {
|
||||
const forceRefresh = force ? refreshFolders() : Promise.resolve()
|
||||
models.value = {}
|
||||
return forceRefresh.then(() =>
|
||||
Promise.allSettled(Object.keys(folders.value).map(refreshModels)),
|
||||
)
|
||||
}
|
||||
|
||||
const updateModel = async (model: BaseModel, data: BaseModel) => {
|
||||
const updateData = new Map()
|
||||
let oldKey: string | null = null
|
||||
@@ -80,7 +113,7 @@ export const useModels = defineStore('models', (store) => {
|
||||
store.dialog.close({ key: oldKey })
|
||||
}
|
||||
|
||||
refresh()
|
||||
refreshModels(data.type)
|
||||
}
|
||||
|
||||
const deleteModel = async (model: BaseModel) => {
|
||||
@@ -112,7 +145,7 @@ export const useModels = defineStore('models', (store) => {
|
||||
life: 2000,
|
||||
})
|
||||
store.dialog.close({ key: dialogKey })
|
||||
return refresh()
|
||||
return refreshModels(model.type)
|
||||
})
|
||||
.then(() => {
|
||||
resolve(void 0)
|
||||
@@ -136,7 +169,13 @@ export const useModels = defineStore('models', (store) => {
|
||||
})
|
||||
}
|
||||
|
||||
return { data, refresh, remove: deleteModel, update: updateModel }
|
||||
return {
|
||||
folders: folders,
|
||||
data: models,
|
||||
refresh: refreshAllModels,
|
||||
remove: deleteModel,
|
||||
update: updateModel,
|
||||
}
|
||||
})
|
||||
|
||||
declare module 'hooks/store' {
|
||||
@@ -204,7 +243,10 @@ const baseInfoKey = Symbol('baseInfo') as InjectionKey<
|
||||
export const useModelBaseInfoEditor = (formInstance: ModelFormInstance) => {
|
||||
const { formData: model, modelData } = formInstance
|
||||
|
||||
const { modelFolders } = useConfig()
|
||||
const provideModelFolders = inject<any>(modelFolderProvideKey)
|
||||
const modelFolders = computed<ModelFolder>(() => {
|
||||
return provideModelFolders?.value ?? {}
|
||||
})
|
||||
|
||||
const type = computed({
|
||||
get: () => {
|
||||
@@ -304,6 +346,7 @@ export const useModelBaseInfoEditor = (formInstance: ModelFormInstance) => {
|
||||
basename,
|
||||
extension,
|
||||
pathIndex,
|
||||
modelFolders,
|
||||
}
|
||||
|
||||
provide(baseInfoKey, result)
|
||||
|
||||
21
src/i18n.ts
21
src/i18n.ts
@@ -1,3 +1,4 @@
|
||||
import { app } from 'scripts/comfyAPI'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const messages = {
|
||||
@@ -38,6 +39,14 @@ const messages = {
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
},
|
||||
setting: {
|
||||
apiKey: 'API Key',
|
||||
scan: 'Scan',
|
||||
scanMissing: 'Download missing information or preview',
|
||||
scanAll: "Override all models' information and preview",
|
||||
includeHiddenFiles: 'Include hidden files(start with .)',
|
||||
excludeScanTypes: 'Exclude scan types (separate with commas)',
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
model: '模型',
|
||||
@@ -76,16 +85,24 @@ const messages = {
|
||||
createdAt: '创建时间',
|
||||
updatedAt: '更新时间',
|
||||
},
|
||||
setting: {
|
||||
apiKey: '密钥',
|
||||
scan: '扫描',
|
||||
scanMissing: '下载缺失的信息或预览图片',
|
||||
scanAll: '覆盖所有模型信息和预览图片',
|
||||
includeHiddenFiles: '包含隐藏文件(以 . 开头的文件或文件夹)',
|
||||
excludeScanTypes: '排除扫描类型(使用英文逗号隔开)',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const getLocalLanguage = () => {
|
||||
const local =
|
||||
localStorage.getItem('Comfy.Settings.Comfy.Locale') ||
|
||||
app.ui?.settings.getSettingValue<string>('Comfy.Locale') ||
|
||||
navigator.language.split('-')[0] ||
|
||||
'en'
|
||||
|
||||
return local.replace(/['"]/g, '')
|
||||
return local
|
||||
}
|
||||
|
||||
export const i18n = createI18n({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import { containerDirective } from 'hooks/container'
|
||||
import { resizeDirective } from 'hooks/resize'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import ConfirmationService from 'primevue/confirmationservice'
|
||||
@@ -21,6 +22,7 @@ function createVueApp(rootContainer: string | HTMLElement) {
|
||||
const app = createApp(App)
|
||||
app.directive('tooltip', Tooltip)
|
||||
app.directive('resize', resizeDirective)
|
||||
app.directive('container', containerDirective)
|
||||
app
|
||||
.use(PrimeVue, {
|
||||
theme: {
|
||||
|
||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -157,6 +157,8 @@ declare namespace ComfyAPI {
|
||||
|
||||
class ComfySettingsDialog {
|
||||
addSetting: (params: SettingParams) => { value: any }
|
||||
getSettingValue: <T>(id: string, defaultValue?: T) => T
|
||||
setSettingValue: <T>(id: string, value: T) => void
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
1
src/types/shims.d.ts
vendored
1
src/types/shims.d.ts
vendored
@@ -3,6 +3,7 @@ export {}
|
||||
declare module 'vue' {
|
||||
interface ComponentCustomProperties {
|
||||
vResize: (typeof import('hooks/resize'))['resizeDirective']
|
||||
vContainer: (typeof import('hooks/container'))['containerDirective']
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import container from '@tailwindcss/container-queries'
|
||||
import plugin from 'tailwindcss/plugin'
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
@@ -8,7 +7,6 @@ export default {
|
||||
darkMode: ['selector', '.dark-theme'],
|
||||
|
||||
plugins: [
|
||||
container,
|
||||
plugin(({ addUtilities }) => {
|
||||
addUtilities({
|
||||
'.scrollbar-none': {
|
||||
|
||||
Reference in New Issue
Block a user