Merge pull request #6 from cdb-boop/main

General improvements.
This commit is contained in:
Hayden
2024-04-18 10:35:30 +08:00
committed by GitHub
7 changed files with 1893 additions and 1041 deletions

View File

@@ -14,6 +14,7 @@ Download, browse and delete models in ComfyUI.
- View multiple models associated with a url. - View multiple models associated with a url.
- Select a download directory. - Select a download directory.
- Optionally also download a model preview image (a default image along side the model, from another url or locally uploaded). - Optionally also download a model preview image (a default image along side the model, from another url or locally uploaded).
- Optionally also download descriptions as a note (`.txt` file).
- Civitai and HuggingFace API token configurable in `server_settings.yaml`. - Civitai and HuggingFace API token configurable in `server_settings.yaml`.
### Models Tab ### Models Tab
@@ -22,7 +23,7 @@ Download, browse and delete models in ComfyUI.
- Advanced keyword search using `"multiple words in quotes"` or a minus sign to `-exclude`. - Advanced keyword search using `"multiple words in quotes"` or a minus sign to `-exclude`.
- Search `/`subdirectories of model directories based on your file structure (for example, `/0/1.5/styles/clothing`). - Search `/`subdirectories of model directories based on your file structure (for example, `/0/1.5/styles/clothing`).
- Add `/` at the start of the search bar to see auto-complete suggestions. - Add `/` at the start of the search bar to see auto-complete suggestions.
- Include models listed in ComfyUI's `extra_model_paths.yaml` or added in `ComfyUI/models`. - Include models listed in ComfyUI's `extra_model_paths.yaml` or added in `ComfyUI/models/`.
- Sort for models (Date Created, Date Modified, Name). - Sort for models (Date Created, Date Modified, Name).
### Model Info View ### Model Info View
@@ -38,8 +39,9 @@ Download, browse and delete models in ComfyUI.
- Button to add model to ComfyUI graph or embedding to selected nodes. (For small screens/low resolution.) - Button to add model to ComfyUI graph or embedding to selected nodes. (For small screens/low resolution.)
- Right, left, top and bottom toggleable sidebar modes. - Right, left, top and bottom toggleable sidebar modes.
- Drag a model onto the graph to add a new node. - Drag a model onto the graph to add a new node.
- Drag a model onto an existing node to set the model field. - Drag a model onto an existing node to set the model field. (Must be exact on input if multiple inputs use model name text.)
- Drag an embedding onto a text area to add it to the end. - Drag an embedding onto a text area, or highlight any number of nodes, to add it to the end.
- Drag preview image in model info onto the graph to load embedded workflow.
### Settings Tab ### Settings Tab
@@ -49,56 +51,7 @@ Download, browse and delete models in ComfyUI.
- Show/Hide add embedding extension. - Show/Hide add embedding extension.
- Colors follow ComfyUI's current theme. - Colors follow ComfyUI's current theme.
## TODO ### Known Issues
<details> - Pinch to Zoom can cause an invisible scrolling bug.
- After adding/renaming/deleting models, the webpage needs to be reloaded to update the model lists. (Can this be automated?)
<summary></summary>
### Download Model
- Checkbox to optionally save description in `.txt` file for Civitai. (what about "About Model"?)
- Server setting to enable creating new folders (on download, on move).
### Download Model Info
- Auto-save notes? (requires debounce and save confirmation)
- Load workflow from preview (Should be easy to add with ComfyUI built-in clipboard.)
- Default weights on add/drag? (optional override on drag?)
- Optional (re)download `📥︎` model info from the internet and cache the text file locally. (requires checksum?)
- Radio buttons to swap between downloaded and server view.
### Sidebar
- Drag sidebar width/height dynamically.
### Accessibility
- Proper naming, labeling, alt text, etc. for html elements.
- Tool tips.
- Better error messages.
### Image preview
- Better placeholder preview. (with proper spelling!)
- Show preview images for videos.
- If ffmpeg or cv2 available, extract the first frame of the video and use as image preview.
### Settings
- Toggle exclusion of "hidden folders" with a `.` prefix.
- Sidebar default width/height.
- Toggle non-uniform preview sizes. (How to handle extreme aspect ratios?)
### Search filtering and sort
- Real-time search
- Check search code is optimized to avoid recalculation on every minor input change
- Filter directory dropdown
- Filter directory content in auto-suggest dropdown (not clear how this should be implemented)
- Filters dropdown
- Stable Diffusion model version, if applicable (Maybe dropdown list of "Base Models" is more pratical to impliment?)
- Favorites
- Swap between `and` and `or` keyword search? (currently `and`)
</details>

View File

@@ -20,21 +20,51 @@ requests.packages.urllib3.disable_warnings()
import folder_paths import folder_paths
config_loader_path = os.path.join(os.path.dirname(__file__), 'config_loader.py') comfyui_model_uri = folder_paths.models_dir
extension_uri = os.path.dirname(__file__)
config_loader_path = os.path.join(extension_uri, 'config_loader.py')
config_loader_spec = importlib.util.spec_from_file_location('config_loader', config_loader_path) config_loader_spec = importlib.util.spec_from_file_location('config_loader', config_loader_path)
config_loader = importlib.util.module_from_spec(config_loader_spec) config_loader = importlib.util.module_from_spec(config_loader_spec)
config_loader_spec.loader.exec_module(config_loader) config_loader_spec.loader.exec_module(config_loader)
comfyui_model_uri = os.path.join(os.getcwd(), "models")
extension_uri = os.path.join(os.getcwd(), "custom_nodes" + os.path.sep + "ComfyUI-Model-Manager")
no_preview_image = os.path.join(extension_uri, "no-preview.png") no_preview_image = os.path.join(extension_uri, "no-preview.png")
ui_settings_uri = os.path.join(extension_uri, "ui_settings.yaml") ui_settings_uri = os.path.join(extension_uri, "ui_settings.yaml")
server_settings_uri = os.path.join(extension_uri, "server_settings.yaml") server_settings_uri = os.path.join(extension_uri, "server_settings.yaml")
fallback_model_extensions = set([".bin", ".ckpt", ".onnx", ".pt", ".pth", ".safetensors"]) # TODO: magic values fallback_model_extensions = set([".bin", ".ckpt", ".onnx", ".pt", ".pth", ".safetensors"]) # TODO: magic values
image_extensions = (".apng", ".gif", ".jpeg", ".jpg", ".png", ".webp") # TODO: JavaScript does not know about this (x2 states) image_extensions = (
".png", # order matters
".webp",
".jpeg",
".jpg",
".gif",
".apng",
)
stable_diffusion_webui_civitai_helper_image_extensions = (
".preview.png", # order matters
".preview.webp",
".preview.jpeg",
".preview.jpg",
".preview.gif",
".preview.apng",
)
preview_extensions = ( # TODO: JavaScript does not know about this (x2 states)
image_extensions + # order matters
stable_diffusion_webui_civitai_helper_image_extensions
)
model_info_extension = ".txt"
#video_extensions = (".avi", ".mp4", ".webm") # TODO: Requires ffmpeg or cv2. Cache preview frame? #video_extensions = (".avi", ".mp4", ".webm") # TODO: Requires ffmpeg or cv2. Cache preview frame?
def split_valid_ext(s, *arg_exts):
sl = s.lower()
for exts in arg_exts:
for ext in exts:
if sl.endswith(ext.lower()):
return (s[:-len(ext)], ext)
return (s, "")
_folder_names_and_paths = None # dict[str, tuple[list[str], list[str]]] _folder_names_and_paths = None # dict[str, tuple[list[str], list[str]]]
def folder_paths_folder_names_and_paths(refresh = False): def folder_paths_folder_names_and_paths(refresh = False):
global _folder_names_and_paths global _folder_names_and_paths
@@ -75,10 +105,9 @@ def folder_paths_get_supported_pt_extensions(folder_name, refresh = False): # Mi
def search_path_to_system_path(model_path): def search_path_to_system_path(model_path):
sep = os.path.sep sep = os.path.sep
model_path = os.path.normpath(model_path.replace("/", sep)) model_path = os.path.normpath(model_path.replace("/", sep))
model_path = model_path.lstrip(sep)
isep0 = 0 if model_path[0] == sep else -1 isep1 = model_path.find(sep, 0)
isep1 = model_path.find(sep, isep0 + 1)
if isep1 == -1 or isep1 == len(model_path): if isep1 == -1 or isep1 == len(model_path):
return (None, None) return (None, None)
@@ -86,7 +115,7 @@ def search_path_to_system_path(model_path):
if isep2 == -1 or isep2 - isep1 == 1: if isep2 == -1 or isep2 - isep1 == 1:
isep2 = len(model_path) isep2 = len(model_path)
model_path_type = model_path[isep0 + 1:isep1] model_path_type = model_path[0:isep1]
paths = folder_paths_get_folder_paths(model_path_type) paths = folder_paths_get_folder_paths(model_path_type)
if len(paths) == 0: if len(paths) == 0:
return (None, None) return (None, None)
@@ -156,6 +185,7 @@ def ui_rules():
Rule("model-add-embedding-extension", False, bool), Rule("model-add-embedding-extension", False, bool),
Rule("model-add-drag-strict-on-field", False, bool), Rule("model-add-drag-strict-on-field", False, bool),
Rule("model-add-offset", 25, int), Rule("model-add-offset", 25, int),
Rule("download-save-description-as-text-file", False, bool),
] ]
@@ -169,6 +199,26 @@ def server_rules():
server_settings = config_loader.yaml_load(server_settings_uri, server_rules()) server_settings = config_loader.yaml_load(server_settings_uri, server_rules())
config_loader.yaml_save(server_settings_uri, server_rules(), server_settings) config_loader.yaml_save(server_settings_uri, server_rules(), server_settings)
def get_def_headers(url=""):
def_headers = {
"User-Agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148",
}
if url.startswith("https://civitai.com/"):
api_key = server_settings["civitai_api_key"]
if (api_key != ""):
def_headers["Authorization"] = f"Bearer {api_key}"
url += "&" if "?" in url else "?" # not the most robust solution
url += f"token={api_key}" # TODO: Authorization didn't work in the header
elif url.startswith("https://huggingface.co/"):
api_key = server_settings["huggingface_api_key"]
if api_key != "":
def_headers["Authorization"] = f"Bearer {api_key}"
return def_headers
@server.PromptServer.instance.routes.get("/model-manager/settings/load") @server.PromptServer.instance.routes.get("/model-manager/settings/load")
async def load_ui_settings(request): async def load_ui_settings(request):
rules = ui_rules() rules = ui_rules()
@@ -183,58 +233,171 @@ async def save_ui_settings(request):
rules = ui_rules() rules = ui_rules()
validated_settings = config_loader.validated(rules, settings) validated_settings = config_loader.validated(rules, settings)
success = config_loader.yaml_save(ui_settings_uri, rules, validated_settings) success = config_loader.yaml_save(ui_settings_uri, rules, validated_settings)
print("Saved file: " + ui_settings_uri)
return web.json_response({ return web.json_response({
"success": success, "success": success,
"settings": validated_settings if success else "", "settings": validated_settings if success else "",
}) })
from PIL import Image, TiffImagePlugin
from PIL.PngImagePlugin import PngInfo
def PIL_cast_serializable(v):
# source: https://github.com/python-pillow/Pillow/issues/6199#issuecomment-1214854558
if isinstance(v, TiffImagePlugin.IFDRational):
return float(v)
elif isinstance(v, tuple):
return tuple(PIL_cast_serializable(t) for t in v)
elif isinstance(v, bytes):
return v.decode(errors="replace")
elif isinstance(v, dict):
for kk, vv in v.items():
v[kk] = PIL_cast_serializable(vv)
return v
else:
return v
def get_safetensors_image_bytes(path):
if not os.path.isfile(path):
raise RuntimeError("Path was invalid!")
header = get_safetensor_header(path)
metadata = header.get("__metadata__", None)
if metadata is None:
return None
thumbnail = metadata.get("modelspec.thumbnail", None)
if thumbnail is None:
return None
image_data = thumbnail.split(',')[1]
return base64.b64decode(image_data)
@server.PromptServer.instance.routes.get("/model-manager/preview/get") @server.PromptServer.instance.routes.get("/model-manager/preview/get")
async def get_model_preview(request): async def get_model_preview(request):
uri = request.query.get("uri") uri = request.query.get("uri")
image_path = no_preview_image image_path = no_preview_image
image_extension = "png" image_type = "png"
image_data = None file_name = os.path.split(no_preview_image)[1]
if uri != "no-preview": if uri != "no-preview":
sep = os.path.sep sep = os.path.sep
uri = uri.replace("/" if sep == "\\" else "/", sep) uri = uri.replace("/" if sep == "\\" else "/", sep)
path, _ = search_path_to_system_path(uri) path, _ = search_path_to_system_path(uri)
head, extension = os.path.splitext(path) head, extension = split_valid_ext(path, preview_extensions)
if os.path.exists(path): if os.path.exists(path):
image_extension = extension[1:]
image_path = path image_path = path
elif os.path.exists(head) and os.path.splitext(head)[1] == ".safetensors": image_type = extension.rsplit(".", 1)[1]
image_extension = extension[1:] file_name = os.path.split(head)[1] + "." + image_type
header = get_safetensor_header(head) elif os.path.exists(head) and head.endswith(".safetensors"):
metadata = header.get("__metadata__", None) image_path = head
if metadata is not None: image_type = extension.rsplit(".", 1)[1]
thumbnail = metadata.get("modelspec.thumbnail", None) file_name = os.path.splitext(os.path.split(head)[1])[0] + "." + image_type
if thumbnail is not None:
image_data = thumbnail.split(',')[1]
image_data = base64.b64decode(image_data)
if image_data == None: w = request.query.get("width")
with open(image_path, "rb") as file: h = request.query.get("height")
image_data = file.read() try:
w = int(w)
if w < 1:
w = None
except:
w = None
try:
h = int(h)
if w < 1:
h = None
except:
h = None
return web.Response(body=image_data, content_type="image/" + image_extension) image_data = None
if w is None and h is None: # full size
if image_path.endswith(".safetensors"):
image_data = get_safetensors_image_bytes(image_path)
else:
with open(image_path, "rb") as image:
image_data = image.read()
else:
if image_path.endswith(".safetensors"):
image_data = get_safetensors_image_bytes(image_path)
fp = io.BytesIO(image_data)
else:
fp = image_path
with Image.open(fp) as image:
w0, h0 = image.size
if w is None:
w = (h * w0) // h0
elif h is None:
h = (w * h0) // w0
exif = image.getexif()
metadata = None
if len(image.info) > 0:
metadata = PngInfo()
for (key, value) in image.info.items():
value_str = str(PIL_cast_serializable(value)) # not sure if this is correct (sometimes includes exif)
metadata.add_text(key, value_str)
image.thumbnail((w, h))
image_bytes = io.BytesIO()
image.save(image_bytes, format=image.format, exif=exif, pnginfo=metadata)
image_data = image_bytes.getvalue()
return web.Response(
headers={
"Content-Disposition": f"inline; filename={file_name}",
},
body=image_data,
content_type="image/" + image_type,
)
@server.PromptServer.instance.routes.get("/model-manager/image/extensions")
async def get_image_extensions(request):
return web.json_response(image_extensions)
def download_model_preview(formdata): def download_model_preview(formdata):
path = formdata.get("path", None) path = formdata.get("path", None)
if type(path) is not str: if type(path) is not str:
raise ("Invalid path!") raise ("Invalid path!")
path, _ = search_path_to_system_path(path) path, model_type = search_path_to_system_path(path)
path_without_extension, _ = os.path.splitext(path) model_type_extensions = folder_paths_get_supported_pt_extensions(model_type)
path_without_extension, _ = split_valid_ext(path, model_type_extensions)
overwrite = formdata.get("overwrite", "true").lower() overwrite = formdata.get("overwrite", "true").lower()
overwrite = True if overwrite == "true" else False overwrite = True if overwrite == "true" else False
image = formdata.get("image", None) image = formdata.get("image", None)
if type(image) is str: if type(image) is str:
image_path = download_image(image, path, overwrite) civitai_image_url = "https://civitai.com/images/"
_, image_extension = os.path.splitext(image_path) if image.startswith(civitai_image_url):
image_id = re.search(r"^\d+", image[len(civitai_image_url):]).group(0)
image_id = str(int(image_id))
image_info_url = f"https://civitai.com/api/v1/images?imageId={image_id}"
def_headers = get_def_headers(image_info_url)
response = requests.get(
url=image_info_url,
stream=False,
verify=False,
headers=def_headers,
proxies=None,
allow_redirects=False,
)
if response.ok:
content_type = response.headers.get("Content-Type")
info = response.json()
items = info["items"]
if len(items) == 0:
raise RuntimeError("Civitai /api/v1/images returned 0 items!")
image = items[0]["url"]
else:
raise RuntimeError("Bad response from api/v1/images!")
_, image_extension = split_valid_ext(image, image_extensions)
if image_extension == "":
raise ValueError("Invalid image type!")
image_path = path_without_extension + image_extension
download_file(image, image_path, overwrite)
else: else:
content_type = image.content_type content_type = image.content_type
if not content_type.startswith("image/"): if not content_type.startswith("image/"):
@@ -251,7 +414,7 @@ def download_model_preview(formdata):
with open(image_path, "wb") as f: with open(image_path, "wb") as f:
f.write(image_data) f.write(image_data)
delete_same_name_files(path_without_extension, image_extensions, image_extension) delete_same_name_files(path_without_extension, preview_extensions, image_extension)
@server.PromptServer.instance.routes.post("/model-manager/preview/set") @server.PromptServer.instance.routes.post("/model-manager/preview/set")
@@ -262,21 +425,29 @@ async def set_model_preview(request):
return web.json_response({ "success": True }) return web.json_response({ "success": True })
except ValueError as e: except ValueError as e:
print(e, file=sys.stderr, flush=True) print(e, file=sys.stderr, flush=True)
return web.json_response({ "success": False }) return web.json_response({
"success": False,
"alert": "Failed to set preview!\n\n" + str(e),
})
@server.PromptServer.instance.routes.post("/model-manager/preview/delete") @server.PromptServer.instance.routes.post("/model-manager/preview/delete")
async def delete_model_preview(request): async def delete_model_preview(request):
result = { "success": False }
model_path = request.query.get("path", None) model_path = request.query.get("path", None)
if model_path is None: if model_path is None:
return web.json_response({ "success": False }) result["alert"] = "Missing model path!"
return web.json_response(result)
model_path = urllib.parse.unquote(model_path) model_path = urllib.parse.unquote(model_path)
file, _ = search_path_to_system_path(model_path) model_path, model_type = search_path_to_system_path(model_path)
path_and_name, _ = os.path.splitext(file) model_extensions = folder_paths_get_supported_pt_extensions(model_type)
delete_same_name_files(path_and_name, image_extensions) path_and_name, _ = split_valid_ext(model_path, model_extensions)
delete_same_name_files(path_and_name, preview_extensions)
return web.json_response({ "success": True }) result["success"] = True
return web.json_response(result)
@server.PromptServer.instance.routes.get("/model-manager/models/list") @server.PromptServer.instance.routes.get("/model-manager/models/list")
@@ -297,29 +468,36 @@ async def get_model_list(request):
for base_path_index, model_base_path in enumerate(folder_paths_get_folder_paths(model_type)): for base_path_index, model_base_path in enumerate(folder_paths_get_folder_paths(model_type)):
if not os.path.exists(model_base_path): # TODO: Bug in main code? ("ComfyUI\output\checkpoints", "ComfyUI\output\clip", "ComfyUI\models\t2i_adapter", "ComfyUI\output\vae") if not os.path.exists(model_base_path): # TODO: Bug in main code? ("ComfyUI\output\checkpoints", "ComfyUI\output\clip", "ComfyUI\models\t2i_adapter", "ComfyUI\output\vae")
continue continue
for cwd, _subdirs, files in os.walk(model_base_path): for cwd, subdirs, files in os.walk(model_base_path):
dir_models = [] dir_models = []
dir_images = [] dir_images = []
for file in files: for file in files:
if file.lower().endswith(model_extensions): if file.lower().endswith(model_extensions):
dir_models.append(file) dir_models.append(file)
elif file.lower().endswith(image_extensions): elif file.lower().endswith(preview_extensions):
dir_images.append(file) dir_images.append(file)
for model in dir_models: for model in dir_models:
model_name, model_ext = os.path.splitext(model) model_name, model_ext = split_valid_ext(model, model_extensions)
image = None image = None
image_modified = None image_modified = None
for iImage in range(len(dir_images)-1, -1, -1): for ext in preview_extensions: # order matters
image_name, _ = os.path.splitext(dir_images[iImage]) for iImage in range(len(dir_images)-1, -1, -1):
if model_name == image_name: image_name = dir_images[iImage]
image = end_swap_and_pop(dir_images, iImage) if not image_name.lower().endswith(ext.lower()):
img_abs_path = os.path.join(cwd, image) continue
image_modified = pathlib.Path(img_abs_path).stat().st_mtime_ns image_name = image_name[:-len(ext)]
if model_name == image_name:
image = end_swap_and_pop(dir_images, iImage)
img_abs_path = os.path.join(cwd, image)
image_modified = pathlib.Path(img_abs_path).stat().st_mtime_ns
break
if image is not None:
break break
abs_path = os.path.join(cwd, model) abs_path = os.path.join(cwd, model)
stats = pathlib.Path(abs_path).stat() stats = pathlib.Path(abs_path).stat()
sizeBytes = stats.st_size
model_modified = stats.st_mtime_ns model_modified = stats.st_mtime_ns
model_created = stats.st_ctime_ns model_created = stats.st_ctime_ns
if use_safetensor_thumbnail and image is None and model_ext == ".safetensors": if use_safetensor_thumbnail and image is None and model_ext == ".safetensors":
@@ -336,12 +514,21 @@ async def get_model_list(request):
image = model + image_ext image = model + image_ext
image_modified = model_modified image_modified = model_modified
rel_path = "" if cwd == model_base_path else os.path.relpath(cwd, model_base_path) rel_path = "" if cwd == model_base_path else os.path.relpath(cwd, model_base_path)
info = (model, image, base_path_index, rel_path, model_modified, model_created, image_modified) info = (
model,
image,
base_path_index,
rel_path,
model_modified,
model_created,
image_modified,
sizeBytes,
)
file_infos.append(info) file_infos.append(info)
file_infos.sort(key=lambda tup: tup[4], reverse=True) # TODO: remove sort; sorted on client #file_infos.sort(key=lambda tup: tup[4], reverse=True) # TODO: remove sort; sorted on client
model_items = [] model_items = []
for model, image, base_path_index, rel_path, model_modified, model_created, image_modified in file_infos: for model, image, base_path_index, rel_path, model_modified, model_created, image_modified, sizeBytes in file_infos:
item = { item = {
"name": model, "name": model,
"path": "/" + os.path.join(model_type, str(base_path_index), rel_path, model).replace(os.path.sep, "/"), # relative logical path "path": "/" + os.path.join(model_type, str(base_path_index), rel_path, model).replace(os.path.sep, "/"), # relative logical path
@@ -350,6 +537,7 @@ async def get_model_list(request):
"dateCreated": model_created, "dateCreated": model_created,
#"dateLastUsed": "", # TODO: track server-side, send increment client-side #"dateLastUsed": "", # TODO: track server-side, send increment client-side
#"countUsed": 0, # TODO: track server-side, send increment client-side #"countUsed": 0, # TODO: track server-side, send increment client-side
"sizeBytes": sizeBytes,
} }
if image is not None: if image is not None:
raw_post = os.path.join(model_type, str(base_path_index), rel_path, image) raw_post = os.path.join(model_type, str(base_path_index), rel_path, image)
@@ -403,8 +591,7 @@ def linear_directory_hierarchy(refresh = False):
dir_child_count += 1 dir_child_count += 1
else: else:
# file # file
_, file_extension = os.path.splitext(item_name) if extension_whitelist is None or split_valid_ext(item_name, extension_whitelist)[1] != "":
if extension_whitelist is None or file_extension in extension_whitelist:
dir_list.append({ "name": item_name }) dir_list.append({ "name": item_name })
dir_child_count += 1 dir_child_count += 1
if dir_child_count > 0: if dir_child_count > 0:
@@ -430,21 +617,15 @@ def download_file(url, filename, overwrite):
filename_temp = filename + ".download" filename_temp = filename + ".download"
def_headers = { def_headers = get_def_headers(url)
"User-Agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", rh = requests.get(
} url=url,
stream=True,
if url.startswith("https://civitai.com/"): verify=False,
api_key = server_settings["civitai_api_key"] headers=def_headers,
if (api_key != ""): proxies=None,
def_headers["Authorization"] = f"Bearer {api_key}" allow_redirects=False,
url += "&" if "?" in url else "?" # not the most robust solution )
url += f"token={api_key}" # TODO: Authorization didn't work in the header
elif url.startswith("https://huggingface.co/"):
api_key = server_settings["huggingface_api_key"]
if api_key != "":
def_headers["Authorization"] = f"Bearer {api_key}"
rh = requests.get(url=url, stream=True, verify=False, headers=def_headers, proxies=None, allow_redirects=False)
if not rh.ok: if not rh.ok:
raise ValueError( raise ValueError(
"Unable to download! Request header status code: " + "Unable to download! Request header status code: " +
@@ -457,8 +638,16 @@ def download_file(url, filename, overwrite):
headers = {"Range": "bytes=%d-" % downloaded_size} headers = {"Range": "bytes=%d-" % downloaded_size}
headers["User-Agent"] = def_headers["User-Agent"] headers["User-Agent"] = def_headers["User-Agent"]
headers["Authorization"] = def_headers.get("Authorization", None)
r = requests.get(url=url, stream=True, verify=False, headers=headers, proxies=None, allow_redirects=False) r = requests.get(
url=url,
stream=True,
verify=False,
headers=headers,
proxies=None,
allow_redirects=False,
)
if rh.status_code == 307 and r.status_code == 307: if rh.status_code == 307 and r.status_code == 307:
# Civitai redirect # Civitai redirect
redirect_url = r.content.decode("utf-8") redirect_url = r.content.decode("utf-8")
@@ -482,7 +671,7 @@ def download_file(url, filename, overwrite):
total_size = int(rh.headers.get("Content-Length", 0)) # TODO: pass in total size earlier total_size = int(rh.headers.get("Content-Length", 0)) # TODO: pass in total size earlier
print("Download file: " + filename) print("Downloading file: " + url)
if total_size != 0: if total_size != 0:
print("Download file size: " + str(total_size)) print("Download file size: " + str(total_size))
@@ -512,54 +701,63 @@ def download_file(url, filename, overwrite):
if overwrite and os.path.isfile(filename): if overwrite and os.path.isfile(filename):
os.remove(filename) os.remove(filename)
os.rename(filename_temp, filename) os.rename(filename_temp, filename)
print("Saved file: " + filename)
def download_image(image_uri, model_path, overwrite): def bytes_to_size(total_bytes):
_, extension = os.path.splitext(image_uri) # TODO: doesn't work for https://civitai.com/images/... units = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"]
if not extension in image_extensions: b = total_bytes
raise ValueError("Invalid image type!") i = 0
path_without_extension, _ = os.path.splitext(model_path) while True:
file = path_without_extension + extension b = b >> 10
download_file(image_uri, file, overwrite) if (b == 0): break
return file i = i + 1
if i >= len(units) or i == 0:
return str(total_bytes) + " " + units[0]
return "{:.2f}".format(total_bytes / (1 << (i * 10))) + " " + units[i]
@server.PromptServer.instance.routes.get("/model-manager/model/info") @server.PromptServer.instance.routes.get("/model-manager/model/info")
async def get_model_info(request): async def get_model_info(request):
result = { "success": False }
model_path = request.query.get("path", None) model_path = request.query.get("path", None)
if model_path is None: if model_path is None:
return web.json_response({ "success": False }) result["alert"] = "Missing model path!"
return web.json_response(result)
model_path = urllib.parse.unquote(model_path) model_path = urllib.parse.unquote(model_path)
file, _ = search_path_to_system_path(model_path) abs_path, model_type = search_path_to_system_path(model_path)
if file is None: if abs_path is None:
return web.json_response({}) result["alert"] = "Invalid model path!"
return web.json_response(result)
info = {} info = {}
path, name = os.path.split(model_path) comfyui_directory, name = os.path.split(model_path)
info["File Name"] = name info["File Name"] = name
info["File Directory"] = path info["File Directory"] = comfyui_directory
info["File Size"] = str(os.path.getsize(file)) + " bytes" info["File Size"] = bytes_to_size(os.path.getsize(abs_path))
stats = pathlib.Path(file).stat() stats = pathlib.Path(abs_path).stat()
date_format = "%Y-%m-%d %H:%M:%S" date_format = "%Y-%m-%d %H:%M:%S"
date_modified = datetime.fromtimestamp(stats.st_mtime).strftime(date_format) date_modified = datetime.fromtimestamp(stats.st_mtime).strftime(date_format)
info["Date Modified"] = date_modified info["Date Modified"] = date_modified
info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format) info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
file_name, _ = os.path.splitext(file) model_extensions = folder_paths_get_supported_pt_extensions(model_type)
abs_name , _ = split_valid_ext(abs_path, model_extensions)
for extension in image_extensions: for extension in preview_extensions:
maybe_image = file_name + extension maybe_preview = abs_name + extension
if os.path.isfile(maybe_image): if os.path.isfile(maybe_preview):
image_path, _ = os.path.splitext(model_path) preview_path, _ = split_valid_ext(model_path, model_extensions)
image_modified = pathlib.Path(maybe_image).stat().st_mtime_ns preview_modified = pathlib.Path(maybe_preview).stat().st_mtime_ns
info["Preview"] = { info["Preview"] = {
"path": urllib.parse.quote_plus(image_path + extension), "path": urllib.parse.quote_plus(preview_path + extension),
"dateModified": urllib.parse.quote_plus(str(image_modified)), "dateModified": urllib.parse.quote_plus(str(preview_modified)),
} }
break break
header = get_safetensor_header(file) header = get_safetensor_header(abs_path)
metadata = header.get("__metadata__", None) metadata = header.get("__metadata__", None)
#json.dump(metadata, sys.stdout, indent=4) #json.dump(metadata, sys.stdout, indent=4)
#print() #print()
@@ -593,13 +791,14 @@ async def get_model_info(request):
info["Base Training Model"] = metadata.get("ss_sd_model_name", "") info["Base Training Model"] = metadata.get("ss_sd_model_name", "")
info["Base Model"] = metadata.get("ss_base_model_version", "") info["Base Model"] = metadata.get("ss_base_model_version", "")
info["Architecture"] = metadata.get("modelspec.architecture", "") # "stable-diffusion-xl-v1-base" info["Architecture"] = metadata.get("modelspec.architecture", "")
info["Network Dimension"] = metadata.get("ss_network_dim", "") # features trained
info["Network Alpha"] = metadata.get("ss_network_alpha", "") # trained features applied
info["Model Sampling Type"] = metadata.get("modelspec.prediction_type", "")
clip_skip = metadata.get("ss_clip_skip", "") clip_skip = metadata.get("ss_clip_skip", "")
if clip_skip == "None": if clip_skip == "None" or clip_skip == "1": # assume 1 means no clip skip
clip_skip = "" clip_skip = ""
info["Clip Skip"] = clip_skip # default 1 (disable clip skip) info["Clip Skip"] = clip_skip
info["Model Sampling Type"] = metadata.get("modelspec.prediction_type", "") # "epsilon"
# it is unclear what these are # it is unclear what these are
#info["Hash SHA256"] = metadata.get("modelspec.hash_sha256", "") #info["Hash SHA256"] = metadata.get("modelspec.hash_sha256", "")
@@ -622,10 +821,10 @@ async def get_model_info(request):
training_comment if training_comment != "None" else "" training_comment if training_comment != "None" else ""
).strip() ).strip()
txt_file = file_name + ".txt" info_text_file = abs_name + model_info_extension
notes = "" notes = ""
if os.path.isfile(txt_file): if os.path.isfile(info_text_file):
with open(txt_file, 'r', encoding="utf-8") as f: with open(info_text_file, 'r', encoding="utf-8") as f:
notes = f.read() notes = f.read()
info["Notes"] = notes info["Notes"] = notes
@@ -656,7 +855,9 @@ async def get_model_info(request):
tags.sort(key=lambda x: x[1], reverse=True) tags.sort(key=lambda x: x[1], reverse=True)
info["Tags"] = tags info["Tags"] = tags
return web.json_response(info) result["success"] = True
result["info"] = info
return web.json_response(result)
@server.PromptServer.instance.routes.get("/model-manager/system-separator") @server.PromptServer.instance.routes.get("/model-manager/system-separator")
@@ -667,10 +868,7 @@ async def get_system_separator(request):
@server.PromptServer.instance.routes.post("/model-manager/model/download") @server.PromptServer.instance.routes.post("/model-manager/model/download")
async def download_model(request): async def download_model(request):
formdata = await request.post() formdata = await request.post()
result = { result = { "success": False }
"success": False,
"invalid": None,
}
overwrite = formdata.get("overwrite", "false").lower() overwrite = formdata.get("overwrite", "false").lower()
overwrite = True if overwrite == "true" else False overwrite = True if overwrite == "true" else False
@@ -678,25 +876,30 @@ async def download_model(request):
model_path = formdata.get("path", "/0") model_path = formdata.get("path", "/0")
directory, model_type = search_path_to_system_path(model_path) directory, model_type = search_path_to_system_path(model_path)
if directory is None: if directory is None:
result["invalid"] = "path" result["alert"] = "Invalid save path!"
return web.json_response(result) return web.json_response(result)
download_uri = formdata.get("download") download_uri = formdata.get("download")
if download_uri is None: if download_uri is None:
result["invalid"] = "download" result["alert"] = "Invalid download url!"
return web.json_response(result) return web.json_response(result)
name = formdata.get("name") name = formdata.get("name")
_, model_extension = os.path.splitext(name) model_extensions = folder_paths_get_supported_pt_extensions(model_type)
if not model_extension in folder_paths_get_supported_pt_extensions(model_type): name_head, model_extension = split_valid_ext(name, model_extensions)
result["invalid"] = "name" name_without_extension = os.path.split(name_head)[1]
if name_without_extension == "":
result["alert"] = "Cannot have empty model name!"
return web.json_response(result)
if model_extension == "":
result["alert"] = "Unrecognized model extension!"
return web.json_response(result) return web.json_response(result)
file_name = os.path.join(directory, name) file_name = os.path.join(directory, name)
try: try:
download_file(download_uri, file_name, overwrite) download_file(download_uri, file_name, overwrite)
except Exception as e: except Exception as e:
print(e, file=sys.stderr, flush=True) print(e, file=sys.stderr, flush=True)
result["invalid"] = "model" result["alert"] = "Failed to download model!\n\n" + str(e)
return web.json_response(result) return web.json_response(result)
image = formdata.get("image") image = formdata.get("image")
@@ -709,7 +912,7 @@ async def download_model(request):
}) })
except Exception as e: except Exception as e:
print(e, file=sys.stderr, flush=True) print(e, file=sys.stderr, flush=True)
result["invalid"] = "preview" result["alert"] = "Failed to download preview!\n\n" + str(e)
result["success"] = True result["success"] = True
return web.json_response(result) return web.json_response(result)
@@ -718,63 +921,86 @@ async def download_model(request):
@server.PromptServer.instance.routes.post("/model-manager/model/move") @server.PromptServer.instance.routes.post("/model-manager/model/move")
async def move_model(request): async def move_model(request):
body = await request.json() body = await request.json()
result = { "success": False }
old_file = body.get("oldFile", None) old_file = body.get("oldFile", None)
if old_file is None: if old_file is None:
return web.json_response({ "success": False }) result["alert"] = "No model was given!"
return web.json_response(result)
old_file, old_model_type = search_path_to_system_path(old_file) old_file, old_model_type = search_path_to_system_path(old_file)
if not os.path.isfile(old_file): if not os.path.isfile(old_file):
return web.json_response({ "success": False }) result["alert"] = "Model does not exist!"
_, model_extension = os.path.splitext(old_file) return web.json_response(result)
if not model_extension in folder_paths_get_supported_pt_extensions(old_model_type): old_model_extensions = folder_paths_get_supported_pt_extensions(old_model_type)
# cannot move arbitrary files old_file_without_extension, model_extension = split_valid_ext(old_file, old_model_extensions)
return web.json_response({ "success": False }) if model_extension == "":
result["alert"] = "Invalid model extension!"
return web.json_response(result)
new_file = body.get("newFile", None) new_file = body.get("newFile", None)
if new_file is None or new_file == "": if new_file is None or new_file == "":
# cannot have empty name result["alert"] = "New model name was invalid!"
return web.json_response({ "success": False }) return web.json_response(result)
new_file, new_model_type = search_path_to_system_path(new_file) new_file, new_model_type = search_path_to_system_path(new_file)
if not new_file.endswith(model_extension): if not new_file.endswith(model_extension):
return web.json_response({ "success": False }) result["alert"] = "Cannot change model extension!"
return web.json_response(result)
if os.path.isfile(new_file): if os.path.isfile(new_file):
# cannot overwrite existing file result["alert"] = "Cannot overwrite existing model!"
return web.json_response({ "success": False }) return web.json_response(result)
if not model_extension in folder_paths_get_supported_pt_extensions(new_model_type): new_model_extensions = folder_paths_get_supported_pt_extensions(new_model_type)
return web.json_response({ "success": False }) new_file_without_extension, new_model_extension = split_valid_ext(new_file, new_model_extensions)
new_file_dir, _ = os.path.split(new_file) if model_extension != new_model_extension:
result["alert"] = "Cannot change model extension!"
return web.json_response(result)
new_file_dir, new_file_name = os.path.split(new_file)
if not os.path.isdir(new_file_dir): if not os.path.isdir(new_file_dir):
return web.json_response({ "success": False }) result["alert"] = "Destination directory does not exist!"
return web.json_response(result)
new_name_without_extension = os.path.splitext(new_file_name)[0]
if new_file_name == new_name_without_extension or new_name_without_extension == "":
result["alert"] = "New model name was empty!"
return web.json_response(result)
if old_file == new_file: if old_file == new_file:
return web.json_response({ "success": False }) # no-op
result["success"] = True
return web.json_response(result)
try: try:
shutil.move(old_file, new_file) shutil.move(old_file, new_file)
print("Moved file: " + new_file)
except ValueError as e: except ValueError as e:
print(e, file=sys.stderr, flush=True) print(e, file=sys.stderr, flush=True)
return web.json_response({ "success": False }) result["alert"] = "Failed to move model!\n\n" + str(e)
return web.json_response(result)
old_file_without_extension, _ = os.path.splitext(old_file) # TODO: this could overwrite existing files in destination; do a check beforehand?
new_file_without_extension, _ = os.path.splitext(new_file) for extension in preview_extensions + (model_info_extension,):
# TODO: this could overwrite existing files...
for extension in image_extensions + (".txt",):
old_file = old_file_without_extension + extension old_file = old_file_without_extension + extension
if os.path.isfile(old_file): if os.path.isfile(old_file):
new_file = new_file_without_extension + extension
try: try:
shutil.move(old_file, new_file_without_extension + extension) shutil.move(old_file, new_file)
print("Moved file: " + new_file)
except ValueError as e: except ValueError as e:
print(e, file=sys.stderr, flush=True) print(e, file=sys.stderr, flush=True)
msg = result.get("alert","")
if msg == "":
result["alert"] = "Failed to move model resource file!\n\n" + str(e)
else:
result["alert"] = msg + "\n" + str(e)
return web.json_response({ "success": True }) result["success"] = True
return web.json_response(result)
def delete_same_name_files(path_without_extension, extensions, keep_extension=None): def delete_same_name_files(path_without_extension, extensions, keep_extension=None):
for extension in extensions: for extension in extensions:
if extension == keep_extension: continue if extension == keep_extension: continue
image_file = path_without_extension + extension file = path_without_extension + extension
if os.path.isfile(image_file): if os.path.isfile(file):
os.remove(image_file) os.remove(file)
print("Deleted file: " + file)
@server.PromptServer.instance.routes.post("/model-manager/model/delete") @server.PromptServer.instance.routes.post("/model-manager/model/delete")
@@ -783,29 +1009,27 @@ async def delete_model(request):
model_path = request.query.get("path", None) model_path = request.query.get("path", None)
if model_path is None: if model_path is None:
result["alert"] = "Missing model path!"
return web.json_response(result) return web.json_response(result)
model_path = urllib.parse.unquote(model_path) model_path = urllib.parse.unquote(model_path)
model_path, model_type = search_path_to_system_path(model_path)
file, model_type = search_path_to_system_path(model_path) if model_path is None:
if file is None: result["alert"] = "Invalid model path!"
return web.json_response(result) return web.json_response(result)
_, extension = os.path.splitext(file) model_extensions = folder_paths_get_supported_pt_extensions(model_type)
if not extension in folder_paths_get_supported_pt_extensions(model_type): path_and_name, model_extension = split_valid_ext(model_path, model_extensions)
# cannot delete arbitrary files if model_extension == "":
result["alert"] = "Cannot delete file!"
return web.json_response(result) return web.json_response(result)
if os.path.isfile(file): if os.path.isfile(model_path):
os.remove(file) os.remove(model_path)
result["success"] = True result["success"] = True
print("Deleted file: " + model_path)
path_and_name, _ = os.path.splitext(file) delete_same_name_files(path_and_name, preview_extensions)
delete_same_name_files(path_and_name, (model_info_extension,))
delete_same_name_files(path_and_name, image_extensions)
txt_file = path_and_name + ".txt"
if os.path.isfile(txt_file):
os.remove(txt_file)
return web.json_response(result) return web.json_response(result)
@@ -813,29 +1037,37 @@ async def delete_model(request):
@server.PromptServer.instance.routes.post("/model-manager/notes/save") @server.PromptServer.instance.routes.post("/model-manager/notes/save")
async def set_notes(request): async def set_notes(request):
body = await request.json() body = await request.json()
result = { "success": False }
text = body.get("notes", None) text = body.get("notes", None)
if type(text) is not str: if type(text) is not str:
return web.json_response({ "success": False }) result["alert"] = "Invalid note!"
return web.json_response(result)
model_path = body.get("path", None) model_path = body.get("path", None)
if type(model_path) is not str: if type(model_path) is not str:
return web.json_response({ "success": False }) result["alert"] = "Missing model path!"
model_path, _ = search_path_to_system_path(model_path) return web.json_response(result)
file_path_without_extension, _ = os.path.splitext(model_path) model_path, model_type = search_path_to_system_path(model_path)
filename = os.path.normpath(file_path_without_extension + ".txt") model_extensions = folder_paths_get_supported_pt_extensions(model_type)
file_path_without_extension, _ = split_valid_ext(model_path, model_extensions)
filename = os.path.normpath(file_path_without_extension + model_info_extension)
if text.isspace() or text == "": if text.isspace() or text == "":
if os.path.exists(filename): if os.path.exists(filename):
os.remove(filename) os.remove(filename)
print("Deleted file: " + filename)
else: else:
try: try:
with open(filename, "w", encoding="utf-8") as f: with open(filename, "w", encoding="utf-8") as f:
f.write(text) f.write(text)
print("Saved file: " + filename)
except ValueError as e: except ValueError as e:
print(e, file=sys.stderr, flush=True) print(e, file=sys.stderr, flush=True)
web.json_response({ "success": False }) result["alert"] = "Failed to save notes!\n\n" + str(e)
web.json_response(result)
return web.json_response({ "success": True }) result["success"] = True
return web.json_response(result)
WEB_DIRECTORY = "web" WEB_DIRECTORY = "web"

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 216 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 947 KiB

After

Width:  |  Height:  |  Size: 928 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,13 +1,26 @@
/* model manager */ /* model manager */
.model-manager { .model-manager {
background-color: var(--comfy-menu-bg);
box-sizing: border-box; box-sizing: border-box;
width: 100%;
height: 100%;
max-width: unset;
max-height: unset;
padding: 10px;
color: var(--bg-color); color: var(--bg-color);
font-family: monospace;
font-size: 15px;
height: 100%;
padding: 8px;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
z-index: 2000; z-index: 2000;
/*override comfy-modal settings*/
border-radius: 0;
box-shadow: none;
justify-content: unset;
max-height: unset;
max-width: unset;
transform: none;
} }
.model-manager .comfy-modal-content { .model-manager .comfy-modal-content {
@@ -17,35 +30,42 @@
.model-manager.sidebar-left { .model-manager.sidebar-left {
width: 50%; width: 50%;
left: 25%; left: 0%;
} }
.model-manager.sidebar-top { .model-manager.sidebar-top {
height: 50%; height: 50%;
top: 25%; top: 0%;
} }
.model-manager.sidebar-bottom { .model-manager.sidebar-bottom {
height: 50%; height: 50%;
top: 75%; top: 50%;
} }
.model-manager.sidebar-right { .model-manager.sidebar-right {
width: 50%; width: 50%;
left: 75%; left: 50%;
}
.model-manager .sidebar-buttons .sidebar-button-active {
border-color: var(--fg-color);
color: var(--fg-color);
overflow: hidden;
} }
/* common */ /* common */
.model-manager h1 { .model-manager h1 {
min-width: 0; min-width: 0;
overflow-wrap: break-word;
} }
.model-manager textarea { .model-manager textarea {
width: 100%;
font-size: 1.2em;
border: solid 2px var(--border-color); border: solid 2px var(--border-color);
border-radius: 8px; border-radius: 8px;
font-size: 1.2em;
resize: vertical; resize: vertical;
width: 100%;
} }
.model-manager input[type="file"] { .model-manager input[type="file"] {
@@ -72,11 +92,6 @@
width: 100%; width: 100%;
} }
.comfy-table a {
color: #007acc;
text-decoration: none;
}
.model-manager ::-webkit-scrollbar { .model-manager ::-webkit-scrollbar {
width: 16px; width: 16px;
} }
@@ -105,7 +120,7 @@
font-style: italic; font-style: italic;
} }
.icon-button { .model-manager .icon-button {
height: 40px; height: 40px;
width: 40px; width: 40px;
line-height: 1.15; line-height: 1.15;
@@ -148,61 +163,61 @@
/* sidebar buttons */ /* sidebar buttons */
.model-manager .sidebar-buttons { .model-manager .sidebar-buttons {
overflow: hidden; overflow: hidden;
padding-right: 10px;
color: var(--input-text); color: var(--input-text);
display: flex;
flex-direction: row-reverse;
flex-wrap: wrap;
} }
/* tabs */ /* main content */
.model-manager .comfy-tabs { .model-manager .model-manager-panel {
color: var(--fg-color); color: var(--fg-color);
} }
.model-manager .comfy-tabs-head { .model-manager .model-manager-tabs {
display: flex; display: flex;
gap: 8px; gap: 4px;
flex-wrap: wrap; height: 40px;
border-bottom: 2px solid var(--border-color);
} }
.model-manager .comfy-tabs-head .head-item { .model-manager .model-manager-tabs .head-item {
padding: 8px 12px; background-color: var(--comfy-menu-bg);
border: 2px solid var(--border-color); border: 2px solid var(--border-color);
border-bottom: none; border-bottom: none;
border-top-left-radius: 8px; border-top-left-radius: 8px;
border-top-right-radius: 8px; border-top-right-radius: 8px;
background-color: var(--comfy-menu-bg);
cursor: pointer; cursor: pointer;
padding: 8px 12px;
margin-bottom: 0px; margin-bottom: 0px;
z-index: 1; z-index: 1;
} }
.model-manager .comfy-tabs-head .head-item.active { .model-manager .model-manager-tabs .head-item.active {
background-color: var(--comfy-input-bg); background-color: var(--bg-color);
cursor: default; cursor: default;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.model-manager .comfy-tabs-body { .model-manager .model-manager-body {
background-color: var(--bg-color); background-color: var(--bg-color);
border: 2px solid var(--border-color); border: 2px solid var(--border-color);
border-top: none;
padding: 16px 0px; padding: 16px 0px;
} }
.model-manager .comfy-tabs { .model-manager .model-manager-panel {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
} }
.model-manager .comfy-tabs-body { .model-manager .model-manager-body {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
} }
.model-manager .comfy-tabs-body > div { .model-manager .model-manager-body > div {
position: relative; position: relative;
height: 100%; height: 100%;
width: auto; width: auto;
@@ -213,15 +228,13 @@
/* model info view */ /* model info view */
.model-manager .model-info-view { .model-manager .model-info-view {
background-color: var(--bg-color); background-color: var(--bg-color);
border: 2px solid var(--border-color);
box-sizing: border-box;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
margin-top: 40px;
overflow-wrap: break-word; overflow-wrap: break-word;
overflow-y: auto; overflow-y: auto;
padding: 20px; padding: 20px;
position: relative;
} }
.model-manager .model-info-container { .model-manager .model-info-container {
@@ -233,17 +246,41 @@
/* download tab */ /* download tab */
.model-manager [data-name="Download"] summary { .model-manager .download-model-infos {
display: flex;
flex-direction: column;
padding: 16px 0;
row-gap: 10px;
}
.model-manager .download-details summary {
background-color: var(--comfy-menu-bg);
border-radius: 16px;
padding: 16px; padding: 16px;
word-wrap: break-word; word-wrap: break-word;
} }
.model-manager [data-name="Download"] .download-settings { .model-manager .download-details[open] summary {
background-color: var(--border-color);
}
.model-manager .download-details > div {
column-gap: 8px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
padding: 8px;
row-gap: 16px;
}
.model-manager [data-name="Download"] .download-settings-wrapper {
flex: 1; flex: 1;
} }
.model-manager .download-model-infos { .model-manager [data-name="Download"] .download-settings {
padding: 16px 0; display: flex;
flex-direction: column;
row-gap: 16px;
} }
/* models tab */ /* models tab */
@@ -394,11 +431,16 @@
} }
/* model preview select */ /* model preview select */
.model-preview-select-radio-container { .model-manager .model-preview-select-radio-container {
min-width: 0; min-width: 0;
flex: 1; flex: 1;
} }
.model-manager .model-preview-select-radio-inputs > div {
height: 40px;
padding: 16px 0 8px 0;
}
.model-manager .model-preview-select-radio-container img { .model-manager .model-preview-select-radio-container img {
position: relative; position: relative;
width: 230px; width: 230px;
@@ -411,23 +453,33 @@
/* topbar */ /* topbar */
.model-manager .topbar-buttons { .model-manager .topbar-buttons {
position: absolute;
display: flex; display: flex;
top: 10px; float: right;
right: 10px;
} }
.model-manager .topbar-buttons button { .model-manager .topbar-buttons button {
width: 33px;
height: 33px; height: 33px;
padding: 1px 6px; padding: 1px 6px;
width: 33px;
}
.model-manager .model-manager-head .topbar-left {
display: flex;
float: left;
}
.model-manager .model-manager-head .topbar-right {
column-gap: 4px;
display: flex;
flex-direction: row-reverse;
float: right;
} }
/* search dropdown */ /* search dropdown */
.model-manager .search-models { .model-manager .search-models {
display: flex; display: flex;
flex-direction: row;
flex: 1; flex: 1;
flex-direction: row;
min-width: 0; min-width: 0;
} }
@@ -450,13 +502,13 @@
} }
.model-manager .search-dropdown { .model-manager .search-dropdown {
position: absolute;
background-color: var(--bg-color); background-color: var(--bg-color);
border: 2px var(--border-color) solid; border: 2px var(--border-color) solid;
border-radius: 10px;
color: var(--fg-color); color: var(--fg-color);
max-height: 30vh; max-height: 30vh;
overflow: auto; overflow: auto;
border-radius: 10px; position: absolute;
z-index: 1; z-index: 1;
} }
@@ -477,10 +529,15 @@
display: none; /* Safari and Chrome */ display: none; /* Safari and Chrome */
} }
.model-manager .search-dropdown > p.search-dropdown-selected { .model-manager .search-dropdown > p.search-dropdown-key-selected,
.model-manager .search-dropdown > p.search-dropdown-mouse-selected {
background-color: var(--border-color); background-color: var(--border-color);
} }
.model-manager .search-dropdown > p.search-dropdown-key-selected {
border-left: 1mm solid var(--input-text);
}
/* model manager settings */ /* model manager settings */
.model-manager .model-manager-settings > div, .model-manager .model-manager-settings > div,
.model-manager .model-manager-settings > label { .model-manager .model-manager-settings > label {
@@ -500,6 +557,6 @@
width: 50px; width: 50px;
} }
.search-settings-text { .model-manager .search-settings-text {
width: 100%; width: 100%;
} }

View File

File diff suppressed because it is too large Load Diff