Compare commits
1 Commits
v2.8.0
...
new-way-us
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6997ea606c |
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"esbenp.prettier-vscode",
|
||||
"lokalise.i18n-ally"
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
||||
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
@@ -43,10 +43,5 @@
|
||||
"editor.quickSuggestions": {
|
||||
"strings": "on"
|
||||
},
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/locales"
|
||||
],
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.keystyle": "nested"
|
||||
"css.lint.unknownAtRules": "ignore"
|
||||
}
|
||||
|
||||
@@ -41,14 +41,12 @@ utils.download_web_distribution(version)
|
||||
from .py import manager
|
||||
from .py import download
|
||||
from .py import information
|
||||
from .py import upload
|
||||
|
||||
routes = config.routes
|
||||
|
||||
manager.ModelManager().add_routes(routes)
|
||||
download.ModelDownload().add_routes(routes)
|
||||
information.Information().add_routes(routes)
|
||||
upload.ModelUploader().add_routes(routes)
|
||||
|
||||
|
||||
WEB_DIRECTORY = "web"
|
||||
|
||||
@@ -326,7 +326,7 @@ class ModelDownload:
|
||||
|
||||
try:
|
||||
preview_file = task_data.pop("previewFile", None)
|
||||
utils.save_model_preview(task_path, preview_file, download_platform)
|
||||
utils.save_model_preview_image(task_path, preview_file, download_platform)
|
||||
self.set_task_content(task_id, task_data)
|
||||
task_status = TaskStatus(
|
||||
taskId=task_id,
|
||||
|
||||
@@ -69,12 +69,8 @@ class CivitaiModelSearcher(ModelSearcher):
|
||||
models: list[dict] = []
|
||||
|
||||
for version in model_versions:
|
||||
version_files: list[dict] = version.get("files", [])
|
||||
model_files = utils.filter_with(version_files, {"type": "Model"})
|
||||
# issue: https://github.com/hayden-fr/ComfyUI-Model-Manager/issues/188
|
||||
# Some Embeddings do not have Model file, but Negative
|
||||
# Make sure there are at least downloadable files
|
||||
model_files = version_files if len(model_files) == 0 else model_files
|
||||
model_files: list[dict] = version.get("files", [])
|
||||
model_files = utils.filter_with(model_files, {"type": "Model"})
|
||||
|
||||
shortname = version.get("name", None) if len(model_files) > 0 else None
|
||||
|
||||
@@ -112,7 +108,7 @@ class CivitaiModelSearcher(ModelSearcher):
|
||||
description_parts.append("")
|
||||
|
||||
model = {
|
||||
"id": version.get("id"),
|
||||
"id": file.get("id"),
|
||||
"shortname": shortname or basename,
|
||||
"basename": basename,
|
||||
"extension": extension,
|
||||
@@ -126,7 +122,6 @@ class CivitaiModelSearcher(ModelSearcher):
|
||||
"downloadPlatform": "civitai",
|
||||
"downloadUrl": file.get("downloadUrl"),
|
||||
"hashes": file.get("hashes"),
|
||||
"files": version_files if len(version_files) > 1 else None,
|
||||
}
|
||||
models.append(model)
|
||||
|
||||
@@ -355,17 +350,23 @@ class Information:
|
||||
@routes.get("/model-manager/preview/{type}/{index}/{filename:.*}")
|
||||
async def read_model_preview(request):
|
||||
"""
|
||||
Get the file stream of the specified preview
|
||||
Get the file stream of the specified image.
|
||||
If the file does not exist, no-preview.png is returned.
|
||||
|
||||
:param type: The type of the model. eg.checkpoints, loras, vae, etc.
|
||||
:param index: The index of the model folders.
|
||||
:param filename: The filename of the preview.
|
||||
:param filename: The filename of the image.
|
||||
"""
|
||||
model_type = request.match_info.get("type", None)
|
||||
index = int(request.match_info.get("index", None))
|
||||
filename = request.match_info.get("filename", None)
|
||||
|
||||
content_type = utils.resolve_file_content_type(filename)
|
||||
|
||||
if content_type == "video":
|
||||
abs_path = utils.get_full_path(model_type, index, filename)
|
||||
return web.FileResponse(abs_path)
|
||||
|
||||
extension_uri = config.extension_uri
|
||||
|
||||
try:
|
||||
@@ -382,16 +383,8 @@ class Information:
|
||||
if not os.path.isfile(abs_path):
|
||||
abs_path = utils.join_path(extension_uri, "assets", "no-preview.png")
|
||||
|
||||
# Determine content type from the actual file
|
||||
content_type = utils.resolve_file_content_type(abs_path)
|
||||
|
||||
if content_type == "video":
|
||||
# Serve video files directly
|
||||
return web.FileResponse(abs_path)
|
||||
else:
|
||||
# Serve image files (WebP or fallback images)
|
||||
image_data = self.get_image_preview_data(abs_path)
|
||||
return web.Response(body=image_data.getvalue(), content_type="image/webp")
|
||||
image_data = self.get_image_preview_data(abs_path)
|
||||
return web.Response(body=image_data.getvalue(), content_type="image/webp")
|
||||
|
||||
@routes.get("/model-manager/preview/download/{filename}")
|
||||
async def read_download_preview(request):
|
||||
@@ -548,10 +541,10 @@ class Information:
|
||||
model_info = CivitaiModelSearcher().search_by_hash(hash_value)
|
||||
|
||||
preview_url_list = model_info.get("preview", [])
|
||||
preview_url = preview_url_list[0] if preview_url_list else None
|
||||
if preview_url:
|
||||
utils.print_debug(f"Save preview to {abs_model_path}")
|
||||
utils.save_model_preview(abs_model_path, preview_url)
|
||||
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)
|
||||
|
||||
description = model_info.get("description", None)
|
||||
if description:
|
||||
|
||||
@@ -134,8 +134,18 @@ class ModelManager:
|
||||
if is_file and extension not in folder_paths.supported_pt_extensions:
|
||||
return None
|
||||
|
||||
preview_name = utils.get_model_preview_name(entry.path)
|
||||
preview_ext = f".{preview_name.split('.')[-1]}"
|
||||
preview_type = "image"
|
||||
preview_ext = ".webp"
|
||||
preview_images = utils.get_model_all_images(entry.path)
|
||||
if len(preview_images) > 0:
|
||||
preview_type = "image"
|
||||
preview_ext = ".webp"
|
||||
else:
|
||||
preview_videos = utils.get_model_all_videos(entry.path)
|
||||
if len(preview_videos) > 0:
|
||||
preview_type = "video"
|
||||
preview_ext = f".{preview_videos[0].split('.')[-1]}"
|
||||
|
||||
model_preview = f"/model-manager/preview/{folder}/{path_index}/{relative_path.replace(extension, preview_ext)}"
|
||||
|
||||
stat = entry.stat()
|
||||
@@ -148,6 +158,7 @@ class ModelManager:
|
||||
"pathIndex": path_index,
|
||||
"sizeBytes": stat.st_size if is_file else 0,
|
||||
"preview": model_preview if is_file else None,
|
||||
"previewType": preview_type,
|
||||
"createdAt": round(stat.st_ctime_ns / 1000000),
|
||||
"updatedAt": round(stat.st_mtime_ns / 1000000),
|
||||
}
|
||||
@@ -200,11 +211,10 @@ class ModelManager:
|
||||
|
||||
if "previewFile" in model_data:
|
||||
previewFile = model_data["previewFile"]
|
||||
# Always remove existing preview files first in case the file extension has changed
|
||||
utils.remove_model_preview(model_path)
|
||||
# Nothing else to do if the preview file was being removed
|
||||
if not (type(previewFile) is str and previewFile == "undefined"):
|
||||
utils.save_model_preview(model_path, previewFile)
|
||||
if type(previewFile) is str and previewFile == "undefined":
|
||||
utils.remove_model_preview_image(model_path)
|
||||
else:
|
||||
utils.save_model_preview_image(model_path, previewFile)
|
||||
|
||||
if "description" in model_data:
|
||||
description = model_data["description"]
|
||||
@@ -226,7 +236,7 @@ class ModelManager:
|
||||
model_dirname = os.path.dirname(model_path)
|
||||
os.remove(model_path)
|
||||
|
||||
model_previews = utils.get_model_all_previews(model_path)
|
||||
model_previews = utils.get_model_all_images(model_path)
|
||||
for preview in model_previews:
|
||||
os.remove(utils.join_path(model_dirname, preview))
|
||||
|
||||
|
||||
79
py/upload.py
79
py/upload.py
@@ -1,79 +0,0 @@
|
||||
import os
|
||||
import time
|
||||
|
||||
import folder_paths
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
class ModelUploader:
|
||||
def add_routes(self, routes):
|
||||
|
||||
@routes.get("/model-manager/supported-extensions")
|
||||
async def fetch_model_exts(request):
|
||||
"""
|
||||
Get model exts
|
||||
"""
|
||||
try:
|
||||
supported_extensions = list(folder_paths.supported_pt_extensions)
|
||||
return web.json_response({"success": True, "data": supported_extensions})
|
||||
except Exception as e:
|
||||
error_msg = f"Get model supported extension failed: {str(e)}"
|
||||
utils.print_error(error_msg)
|
||||
return web.json_response({"success": False, "error": error_msg})
|
||||
|
||||
@routes.post("/model-manager/upload")
|
||||
async def upload_model(request):
|
||||
"""
|
||||
Upload model
|
||||
"""
|
||||
try:
|
||||
reader = await request.multipart()
|
||||
await self.upload_model(reader)
|
||||
utils.print_info(f"Upload model success")
|
||||
return web.json_response({"success": True, "data": None})
|
||||
except Exception as e:
|
||||
error_msg = f"Upload model failed: {str(e)}"
|
||||
utils.print_error(error_msg)
|
||||
return web.json_response({"success": False, "error": error_msg})
|
||||
|
||||
async def upload_model(self, reader):
|
||||
uploaded_size = 0
|
||||
last_update_time = time.time()
|
||||
interval = 1.0
|
||||
|
||||
while True:
|
||||
part = await reader.next()
|
||||
if part is None:
|
||||
break
|
||||
|
||||
name = part.name
|
||||
if name == "folder":
|
||||
file_folder = await part.text()
|
||||
|
||||
if name == "file":
|
||||
filename = part.filename
|
||||
filepath = f"{file_folder}/{filename}"
|
||||
tmp_filepath = f"{file_folder}/{filename}.tmp"
|
||||
|
||||
with open(tmp_filepath, "wb") as f:
|
||||
while True:
|
||||
chunk = await part.read_chunk()
|
||||
if not chunk:
|
||||
break
|
||||
f.write(chunk)
|
||||
uploaded_size += len(chunk)
|
||||
|
||||
if time.time() - last_update_time >= interval:
|
||||
update_upload_progress = {
|
||||
"uploaded_size": uploaded_size,
|
||||
}
|
||||
await utils.send_json("update_upload_progress", update_upload_progress)
|
||||
|
||||
update_upload_progress = {
|
||||
"uploaded_size": uploaded_size,
|
||||
}
|
||||
await utils.send_json("update_upload_progress", update_upload_progress)
|
||||
os.rename(tmp_filepath, filepath)
|
||||
185
py/utils.py
185
py/utils.py
@@ -17,22 +17,6 @@ from aiohttp import web
|
||||
from typing import Any, Optional
|
||||
from . import config
|
||||
|
||||
# Media file extensions
|
||||
VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.m4v', '.ogv']
|
||||
IMAGE_EXTENSIONS = ['.webp', '.png', '.jpg', '.jpeg', '.gif', '.bmp']
|
||||
|
||||
# Content type mappings
|
||||
VIDEO_CONTENT_TYPE_MAP = {
|
||||
'video/mp4': '.mp4',
|
||||
'video/webm': '.webm',
|
||||
'video/quicktime': '.mov',
|
||||
'video/x-msvideo': '.avi',
|
||||
'video/x-matroska': '.mkv',
|
||||
'video/x-flv': '.flv',
|
||||
'video/x-ms-wmv': '.wmv',
|
||||
'video/ogg': '.ogv',
|
||||
}
|
||||
|
||||
|
||||
def print_info(msg, *args, **kwargs):
|
||||
logging.info(f"[{config.extension_tag}] {msg}", *args, **kwargs)
|
||||
@@ -268,10 +252,10 @@ def get_model_metadata(filename: str):
|
||||
return {}
|
||||
|
||||
|
||||
def get_model_all_previews(model_path: str):
|
||||
def get_model_all_images(model_path: str):
|
||||
base_dirname = os.path.dirname(model_path)
|
||||
files = search_files(base_dirname)
|
||||
files = folder_paths.filter_files_content_types(files, ["video", "image"])
|
||||
files = folder_paths.filter_files_content_types(files, ["image"])
|
||||
|
||||
basename = os.path.splitext(os.path.basename(model_path))[0]
|
||||
output: list[str] = []
|
||||
@@ -285,135 +269,78 @@ def get_model_all_previews(model_path: str):
|
||||
|
||||
|
||||
def get_model_preview_name(model_path: str):
|
||||
"""
|
||||
Get the preview file name for a model. Checks for images and videos in all supported formats.
|
||||
Returns the first available preview file or 'no-preview.png' if none found.
|
||||
"""
|
||||
base_dirname = os.path.dirname(model_path)
|
||||
images = get_model_all_images(model_path)
|
||||
basename = os.path.splitext(os.path.basename(model_path))[0]
|
||||
|
||||
# Prefer previews with these extensions in this order
|
||||
preview_extensions = ['.webm', '.mp4', '.webp']
|
||||
for ext in preview_extensions:
|
||||
preview_name = f"{basename}{ext}"
|
||||
if os.path.exists(join_path(base_dirname, preview_name)):
|
||||
return preview_name
|
||||
|
||||
# Fallback to any available preview files
|
||||
all_previews = get_model_all_previews(model_path)
|
||||
return all_previews[0] if len(all_previews) > 0 else "no-preview.png"
|
||||
|
||||
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"
|
||||
|
||||
|
||||
def get_model_all_videos(model_path: str):
|
||||
base_dirname = os.path.dirname(model_path)
|
||||
files = search_files(base_dirname)
|
||||
files = folder_paths.filter_files_content_types(files, ["video"])
|
||||
|
||||
basename = os.path.splitext(os.path.basename(model_path))[0]
|
||||
output: list[str] = []
|
||||
for file in files:
|
||||
file_basename = os.path.splitext(file)[0]
|
||||
if file_basename == basename:
|
||||
output.append(file)
|
||||
if file_basename == f"{basename}.preview":
|
||||
output.append(file)
|
||||
return output
|
||||
|
||||
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def remove_model_preview(model_path: str):
|
||||
"""
|
||||
Remove preview files for a model.
|
||||
"""
|
||||
def remove_model_preview_image(model_path: str):
|
||||
basename = os.path.splitext(model_path)[0]
|
||||
base_dirname = os.path.dirname(model_path)
|
||||
|
||||
# Remove all preview files
|
||||
for ext in VIDEO_EXTENSIONS + IMAGE_EXTENSIONS:
|
||||
preview_path = f"{basename}{ext}"
|
||||
if os.path.exists(preview_path):
|
||||
os.remove(preview_path)
|
||||
|
||||
# Also check for .preview variants
|
||||
files = search_files(base_dirname)
|
||||
model_name = os.path.splitext(os.path.basename(model_path))[0]
|
||||
for file in files:
|
||||
if file.startswith(f"{model_name}.preview"):
|
||||
file_path = join_path(base_dirname, file)
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
preview_path = f"{basename}.webp"
|
||||
if os.path.exists(preview_path):
|
||||
os.remove(preview_path)
|
||||
|
||||
|
||||
def save_model_preview(model_path: str, file_or_url: Any, platform: Optional[str] = None):
|
||||
"""
|
||||
Save a preview file for a model.
|
||||
Images are converted to WebP, videos are saved in their original format.
|
||||
"""
|
||||
def save_model_preview_image(model_path: str, image_file_or_url: Any, platform: Optional[str] = None):
|
||||
basename = os.path.splitext(model_path)[0]
|
||||
|
||||
# Download file if it is a URL
|
||||
if type(file_or_url) is str:
|
||||
url = file_or_url
|
||||
preview_path = f"{basename}.webp"
|
||||
# Download image file if it is url
|
||||
if type(image_file_or_url) is str:
|
||||
image_url = image_file_or_url
|
||||
|
||||
try:
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
|
||||
# Determine content type from response headers or URL extension
|
||||
content_type = response.headers.get('content-type', '')
|
||||
if not content_type:
|
||||
# Fallback to URL extension detection
|
||||
content_type = resolve_file_content_type(url) or ''
|
||||
|
||||
content = response.content
|
||||
|
||||
if content_type.startswith("video/"):
|
||||
# Save video in original format
|
||||
# Try to get extension from URL or content-type
|
||||
ext = _get_video_extension_from_url(url) or _get_extension_from_content_type(content_type)
|
||||
if not ext:
|
||||
ext = '.mp4' # Default fallback
|
||||
preview_path = f"{basename}{ext}"
|
||||
with open(preview_path, 'wb') as f:
|
||||
f.write(content)
|
||||
else:
|
||||
# Default to image processing for unknown or image types
|
||||
preview_path = f"{basename}.webp"
|
||||
image = Image.open(BytesIO(content))
|
||||
image.save(preview_path, "WEBP")
|
||||
image_response = requests.get(image_url)
|
||||
image_response.raise_for_status()
|
||||
|
||||
image = Image.open(BytesIO(image_response.content))
|
||||
image.save(preview_path, "WEBP")
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"Failed to download preview: {e}")
|
||||
print_error(f"Failed to download image: {e}")
|
||||
|
||||
# Handle uploaded file
|
||||
else:
|
||||
file_obj = file_or_url
|
||||
# Assert image as file
|
||||
image_file = image_file_or_url
|
||||
|
||||
if not isinstance(file_obj, web.FileField):
|
||||
raise RuntimeError("Invalid file")
|
||||
if not isinstance(image_file, web.FileField):
|
||||
raise RuntimeError("Invalid image file")
|
||||
|
||||
content_type: str = file_obj.content_type
|
||||
filename: str = getattr(file_obj, 'filename', '')
|
||||
|
||||
if content_type.startswith("video/"):
|
||||
# Save video in original format for now, consider transcoding to webm to follow the pattern for images converting to webp
|
||||
ext = os.path.splitext(filename.lower())[1]
|
||||
if not ext:
|
||||
ext = '.mp4' # Default fallback
|
||||
preview_path = f"{basename}{ext}"
|
||||
file_obj.file.seek(0)
|
||||
content = file_obj.file.read()
|
||||
with open(preview_path, 'wb') as f:
|
||||
f.write(content)
|
||||
elif content_type.startswith("image/"):
|
||||
# Convert image to webp
|
||||
preview_path = f"{basename}.webp"
|
||||
image = Image.open(file_obj.file)
|
||||
image.save(preview_path, "WEBP")
|
||||
else:
|
||||
raise RuntimeError(f"FileTypeError: expected image or video, got {content_type}")
|
||||
|
||||
|
||||
def _get_video_extension_from_url(url: str) -> Optional[str]:
|
||||
"""Extract video extension from URL."""
|
||||
from urllib.parse import urlparse
|
||||
path = urlparse(url).path.lower()
|
||||
for ext in VIDEO_EXTENSIONS:
|
||||
if path.endswith(ext):
|
||||
return ext
|
||||
return None
|
||||
|
||||
|
||||
def _get_extension_from_content_type(content_type: str) -> Optional[str]:
|
||||
"""Map content-type to file extension."""
|
||||
return VIDEO_CONTENT_TYPE_MAP.get(content_type.lower())
|
||||
content_type: str = image_file.content_type
|
||||
if not content_type.startswith("image/"):
|
||||
if platform == "huggingface":
|
||||
# huggingface previewFile content_type='text/plain', not startswith("image/")
|
||||
return
|
||||
else:
|
||||
raise RuntimeError(f"FileTypeError: expected image, got {content_type}")
|
||||
image = Image.open(image_file.file)
|
||||
image.save(preview_path, "WEBP")
|
||||
|
||||
|
||||
def get_model_all_descriptions(model_path: str):
|
||||
@@ -471,7 +398,7 @@ def rename_model(model_path: str, new_model_path: str):
|
||||
shutil.move(model_path, new_model_path)
|
||||
|
||||
# move preview
|
||||
previews = get_model_all_previews(model_path)
|
||||
previews = get_model_all_images(model_path)
|
||||
for preview in previews:
|
||||
preview_path = join_path(model_dirname, preview)
|
||||
preview_name = os.path.splitext(preview)[0]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-model-manager"
|
||||
description = "Manage models: browsing, download and delete."
|
||||
version = "2.8.0"
|
||||
version = "2.6.3"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["markdownify"]
|
||||
|
||||
|
||||
21
src/App.vue
21
src/App.vue
@@ -10,7 +10,6 @@ import DialogDownload from 'components/DialogDownload.vue'
|
||||
import DialogExplorer from 'components/DialogExplorer.vue'
|
||||
import DialogManager from 'components/DialogManager.vue'
|
||||
import DialogScanning from 'components/DialogScanning.vue'
|
||||
import DialogUpload from 'components/DialogUpload.vue'
|
||||
import GlobalDialogStack from 'components/GlobalDialogStack.vue'
|
||||
import GlobalLoading from 'components/GlobalLoading.vue'
|
||||
import GlobalToast from 'components/GlobalToast.vue'
|
||||
@@ -65,21 +64,6 @@ onMounted(() => {
|
||||
})
|
||||
}
|
||||
|
||||
const openUploadDialog = () => {
|
||||
dialog.open({
|
||||
key: 'model-manager-upload',
|
||||
title: t('uploadModel'),
|
||||
content: DialogUpload,
|
||||
headerButtons: [
|
||||
{
|
||||
key: 'refresh',
|
||||
icon: 'pi pi-refresh',
|
||||
command: refreshModelsAndConfig,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const openManagerDialog = () => {
|
||||
const { cardWidth, gutter, aspect, flat } = config
|
||||
|
||||
@@ -109,11 +93,6 @@ onMounted(() => {
|
||||
icon: 'pi pi-download',
|
||||
command: openDownloadDialog,
|
||||
},
|
||||
{
|
||||
key: 'upload',
|
||||
icon: 'pi pi-upload',
|
||||
command: openUploadDialog,
|
||||
},
|
||||
],
|
||||
minWidth: cardWidth * 2 + gutter + 42,
|
||||
minHeight: (cardWidth / aspect) * 0.5 + 162,
|
||||
|
||||
@@ -31,20 +31,12 @@
|
||||
<KeepAlive>
|
||||
<ModelContent
|
||||
v-if="currentModel"
|
||||
:key="`${currentModel.id}-${currentModel.currentFileId}`"
|
||||
:key="currentModel.id"
|
||||
:model="currentModel"
|
||||
:editable="true"
|
||||
@submit="createDownTask"
|
||||
>
|
||||
<template #action>
|
||||
<div v-if="currentModel.files" class="flex-1">
|
||||
<ResponseSelect
|
||||
:model-value="currentModel.currentFileId"
|
||||
:items="currentModel.selectionFiles"
|
||||
:type="isMobile ? 'drop' : 'button'"
|
||||
>
|
||||
</ResponseSelect>
|
||||
</div>
|
||||
<Button
|
||||
icon="pi pi-download"
|
||||
:label="$t('download')"
|
||||
|
||||
@@ -20,10 +20,7 @@
|
||||
>
|
||||
<div class="flex gap-4 overflow-hidden whitespace-nowrap">
|
||||
<div class="h-18 preview-aspect">
|
||||
<div v-if="isVideoUrl(item.preview)" class="h-full w-full">
|
||||
<PreviewVideo :src="item.preview" />
|
||||
</div>
|
||||
<img v-else :src="item.preview" />
|
||||
<img :src="item.preview" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 flex-col gap-3 overflow-hidden">
|
||||
@@ -75,13 +72,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import DialogCreateTask from 'components/DialogCreateTask.vue'
|
||||
import PreviewVideo from 'components/PreviewVideo.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'
|
||||
import { isVideoUrl } from 'utils/media'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
|
||||
@@ -1,274 +0,0 @@
|
||||
<template>
|
||||
<div class="h-full px-4">
|
||||
<!-- <div v-show="batchScanningStep === 0" class="h-full">
|
||||
<div class="flex h-full items-center px-8">
|
||||
<div class="h-20 w-full opacity-60">
|
||||
<ProgressBar mode="indeterminate" style="height: 6px"></ProgressBar>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<Stepper v-model:value="stepValue" class="flex h-full flex-col" linear>
|
||||
<StepList>
|
||||
<Step :value="1">{{ $t('selectModelType') }}</Step>
|
||||
<Step :value="2">{{ $t('selectSubdirectory') }}</Step>
|
||||
<Step :value="3">{{ $t('chooseFile') }}</Step>
|
||||
</StepList>
|
||||
<StepPanels class="flex-1 overflow-hidden">
|
||||
<StepPanel :value="1" class="h-full">
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<ResponseScroll>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<Button
|
||||
v-for="item in typeOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
@click="item.command"
|
||||
></Button>
|
||||
</div>
|
||||
</ResponseScroll>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel :value="2" class="h-full">
|
||||
<div class="flex h-full flex-col overflow-hidden">
|
||||
<ResponseScroll class="flex-1">
|
||||
<Tree
|
||||
class="h-full"
|
||||
v-model:selection-keys="selectedKey"
|
||||
:value="pathOptions"
|
||||
selectionMode="single"
|
||||
:pt:nodeLabel:class="'text-ellipsis overflow-hidden'"
|
||||
></Tree>
|
||||
</ResponseScroll>
|
||||
|
||||
<div class="flex justify-between pt-6">
|
||||
<Button
|
||||
:label="$t('back')"
|
||||
severity="secondary"
|
||||
icon="pi pi-arrow-left"
|
||||
@click="handleBackTypeSelect"
|
||||
></Button>
|
||||
<Button
|
||||
:label="$t('next')"
|
||||
icon="pi pi-arrow-right"
|
||||
icon-pos="right"
|
||||
:disabled="!enabledUpload"
|
||||
@click="handleConfirmSubdir"
|
||||
></Button>
|
||||
</div>
|
||||
</div>
|
||||
</StepPanel>
|
||||
<StepPanel :value="3" class="h-full">
|
||||
<div class="flex h-full flex-col items-center justify-center">
|
||||
<template v-if="showUploadProgress">
|
||||
<div class="w-4/5">
|
||||
<ProgressBar
|
||||
:value="uploadProgress"
|
||||
:pt:value:style="{ transition: 'width .1s linear' }"
|
||||
></ProgressBar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="overflow-hidden break-words py-8">
|
||||
<div class="overflow-hidden px-8">
|
||||
<div class="text-center">
|
||||
<div class="pb-2">
|
||||
{{ $t('selectedSpecialPath') }}
|
||||
</div>
|
||||
<div class="leading-5 opacity-60">
|
||||
{{ selectedModelFolder }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<Button
|
||||
v-for="item in uploadActions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:icon="item.icon"
|
||||
@click="item.command.call(item)"
|
||||
></Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="h-1/4"></div>
|
||||
</div>
|
||||
</StepPanel>
|
||||
</StepPanels>
|
||||
</Stepper>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ResponseScroll from 'components/ResponseScroll.vue'
|
||||
import { configSetting } from 'hooks/config'
|
||||
import { useModelFolder, useModels } from 'hooks/model'
|
||||
import { request } from 'hooks/request'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import Button from 'primevue/button'
|
||||
import ProgressBar from 'primevue/progressbar'
|
||||
import Step from 'primevue/step'
|
||||
import StepList from 'primevue/steplist'
|
||||
import StepPanel from 'primevue/steppanel'
|
||||
import StepPanels from 'primevue/steppanels'
|
||||
import Stepper from 'primevue/stepper'
|
||||
import Tree from 'primevue/tree'
|
||||
import { api, app } from 'scripts/comfyAPI'
|
||||
import { computed, onMounted, onUnmounted, ref, toValue } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { toast } = useToast()
|
||||
|
||||
const stepValue = ref(1)
|
||||
|
||||
const { folders } = useModels()
|
||||
|
||||
const currentType = ref<string>()
|
||||
const typeOptions = computed(() => {
|
||||
const excludeScanTypes = app.ui?.settings.getSettingValue<string>(
|
||||
configSetting.excludeScanTypes,
|
||||
)
|
||||
const customBlackList =
|
||||
excludeScanTypes
|
||||
?.split(',')
|
||||
.map((type) => type.trim())
|
||||
.filter(Boolean) ?? []
|
||||
return Object.keys(folders.value)
|
||||
.filter((folder) => !customBlackList.includes(folder))
|
||||
.map((type) => {
|
||||
return {
|
||||
label: type,
|
||||
value: type,
|
||||
command: () => {
|
||||
currentType.value = type
|
||||
stepValue.value++
|
||||
},
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const { pathOptions } = useModelFolder({ type: currentType })
|
||||
|
||||
const selectedModelFolder = ref<string>()
|
||||
const selectedKey = computed({
|
||||
get: () => {
|
||||
const key = selectedModelFolder.value
|
||||
return key ? { [key]: true } : {}
|
||||
},
|
||||
set: (val) => {
|
||||
const key = Object.keys(val)[0]
|
||||
selectedModelFolder.value = key
|
||||
},
|
||||
})
|
||||
|
||||
const enabledUpload = computed(() => {
|
||||
return !!selectedModelFolder.value
|
||||
})
|
||||
|
||||
const handleBackTypeSelect = () => {
|
||||
selectedModelFolder.value = undefined
|
||||
currentType.value = undefined
|
||||
stepValue.value--
|
||||
}
|
||||
|
||||
const handleConfirmSubdir = () => {
|
||||
stepValue.value++
|
||||
}
|
||||
|
||||
const uploadTotalSize = ref<number>()
|
||||
const uploadSize = ref<number>()
|
||||
const uploadProgress = computed(() => {
|
||||
const total = toValue(uploadTotalSize)
|
||||
const size = toValue(uploadSize)
|
||||
if (typeof total === 'number' && typeof size === 'number') {
|
||||
return Math.floor((size / total) * 100)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
const showUploadProgress = computed(() => {
|
||||
return typeof uploadProgress.value !== 'undefined'
|
||||
})
|
||||
|
||||
const uploadActions = ref([
|
||||
{
|
||||
value: 'back',
|
||||
label: t('back'),
|
||||
icon: 'pi pi-arrow-left',
|
||||
command: () => {
|
||||
stepValue.value--
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 'full',
|
||||
label: t('chooseFile'),
|
||||
command: () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = supportedExtensions.value.join(',')
|
||||
input.onchange = async () => {
|
||||
const files = input.files
|
||||
const file = files?.item(0)
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
uploadTotalSize.value = file.size
|
||||
uploadSize.value = 0
|
||||
const body = new FormData()
|
||||
body.append('folder', toValue(selectedModelFolder)!)
|
||||
body.append('file', file)
|
||||
|
||||
await request('/upload', {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
})
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.message,
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const supportedExtensions = ref([])
|
||||
|
||||
const fetchSupportedExtensions = async () => {
|
||||
try {
|
||||
const result = await request('/supported-extensions')
|
||||
supportedExtensions.value = result ?? []
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: error.message,
|
||||
life: 5000,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const update_process = (event: CustomEvent) => {
|
||||
const detail = event.detail
|
||||
uploadSize.value = detail.uploaded_size
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchSupportedExtensions()
|
||||
|
||||
api.addEventListener('update_upload_progress', update_process)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
api.removeEventListener('update_upload_progress', update_process)
|
||||
})
|
||||
</script>
|
||||
@@ -25,10 +25,19 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="isVideoUrl(preview)"
|
||||
v-else-if="model.previewType === 'video'"
|
||||
class="h-full w-full p-1 hover:p-0"
|
||||
>
|
||||
<PreviewVideo :src="preview" />
|
||||
<video
|
||||
class="h-full w-full object-cover"
|
||||
playsinline
|
||||
autoplay
|
||||
loop
|
||||
disablepictureinpicture
|
||||
preload="none"
|
||||
>
|
||||
<source :src="preview" />
|
||||
</video>
|
||||
</div>
|
||||
<div v-else class="h-full w-full p-1 hover:p-0">
|
||||
<img class="h-full w-full rounded-lg object-cover" :src="preview" />
|
||||
@@ -72,10 +81,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import PreviewVideo from 'components/PreviewVideo.vue'
|
||||
import { useModelNodeAction } from 'hooks/model'
|
||||
import { BaseModel } from 'types/typings'
|
||||
import { isVideoUrl } from 'utils/media'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
></ModelPreview>
|
||||
|
||||
<div class="flex flex-col gap-4 overflow-hidden">
|
||||
<div class="flex h-10 items-center justify-end gap-4">
|
||||
<div class="flex items-center justify-end gap-4">
|
||||
<slot name="action" :metadata="formInstance.metadata.value"></slot>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
class="relative mx-auto w-full overflow-hidden rounded-lg preview-aspect"
|
||||
:style="$sm({ width: `${cardWidth}px` })"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
preview &&
|
||||
isVideoUrl(
|
||||
preview,
|
||||
currentType === 'local' ? localContentType : undefined,
|
||||
)
|
||||
"
|
||||
class="h-full w-full p-1 hover:p-0"
|
||||
>
|
||||
<PreviewVideo :src="preview" />
|
||||
<div v-if="previewType === 'video'" class="h-full w-full p-1 hover:p-0">
|
||||
<video
|
||||
class="h-full w-full object-cover"
|
||||
playsinline
|
||||
autoplay
|
||||
loop
|
||||
disablepictureinpicture
|
||||
preload="none"
|
||||
>
|
||||
<source :src="preview" />
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<ResponseImage
|
||||
@@ -48,14 +48,7 @@
|
||||
}"
|
||||
>
|
||||
<template #item="slotProps">
|
||||
<div
|
||||
v-if="isVideoUrl(slotProps.data)"
|
||||
class="h-full w-full p-1 hover:p-0"
|
||||
>
|
||||
<PreviewVideo :src="slotProps.data" />
|
||||
</div>
|
||||
<ResponseImage
|
||||
v-else
|
||||
:src="slotProps.data"
|
||||
:error="noPreviewContent"
|
||||
></ResponseImage>
|
||||
@@ -105,7 +98,6 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PreviewVideo from 'components/PreviewVideo.vue'
|
||||
import ResponseFileUpload from 'components/ResponseFileUpload.vue'
|
||||
import ResponseImage from 'components/ResponseImage.vue'
|
||||
import ResponseInput from 'components/ResponseInput.vue'
|
||||
@@ -114,13 +106,13 @@ import { useContainerQueries } from 'hooks/container'
|
||||
import { useModelPreview } from 'hooks/model'
|
||||
import Button from 'primevue/button'
|
||||
import Carousel from 'primevue/carousel'
|
||||
import { isVideoUrl } from 'utils/media'
|
||||
|
||||
const editable = defineModel<boolean>('editable')
|
||||
const { cardWidth } = useConfig()
|
||||
|
||||
const {
|
||||
preview,
|
||||
previewType,
|
||||
typeOptions,
|
||||
currentType,
|
||||
defaultContent,
|
||||
@@ -128,7 +120,6 @@ const {
|
||||
networkContent,
|
||||
updateLocalContent,
|
||||
noPreviewContent,
|
||||
localContentType,
|
||||
} = useModelPreview()
|
||||
|
||||
const { $sm, $xl } = useContainerQueries()
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<template>
|
||||
<video
|
||||
class="h-full w-full object-cover"
|
||||
playsinline
|
||||
autoplay
|
||||
loop
|
||||
muted
|
||||
disablepictureinpicture
|
||||
:preload="preload"
|
||||
>
|
||||
<source :src="src" type="video/mp4" />
|
||||
<source :src="src" type="video/webm" />
|
||||
</video>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
src: string
|
||||
preload?: 'none' | 'metadata' | 'auto'
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
preload: 'metadata',
|
||||
})
|
||||
</script>
|
||||
@@ -46,7 +46,7 @@ const handleDropFile = (event: DragEvent) => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*,video/*'
|
||||
input.accept = 'image/*'
|
||||
input.onchange = () => {
|
||||
const files = input.files
|
||||
if (files) {
|
||||
|
||||
@@ -2,19 +2,17 @@ import { useLoading } from 'hooks/loading'
|
||||
import { request } from 'hooks/request'
|
||||
import { defineStore } from 'hooks/store'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { api } from 'scripts/comfyAPI'
|
||||
import {
|
||||
BaseModel,
|
||||
DownloadTask,
|
||||
DownloadTaskOptions,
|
||||
SelectOptions,
|
||||
VersionModel,
|
||||
VersionModelFile,
|
||||
} from 'types/typings'
|
||||
import { bytesToSize } from 'utils/common'
|
||||
import { onBeforeMount, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import yaml from 'yaml'
|
||||
|
||||
export const useDownload = defineStore('download', (store) => {
|
||||
const { toast, confirm, wrapperToastError } = useToast()
|
||||
@@ -164,60 +162,12 @@ declare module 'hooks/store' {
|
||||
}
|
||||
}
|
||||
|
||||
type WithSelection<T> = SelectOptions & { item: T }
|
||||
|
||||
type FileSelectionVersionModel = VersionModel & {
|
||||
currentFileId?: number
|
||||
selectionFiles?: WithSelection<VersionModelFile>[]
|
||||
}
|
||||
|
||||
export const useModelSearch = () => {
|
||||
const loading = useLoading()
|
||||
const { toast } = useToast()
|
||||
const data = ref<WithSelection<FileSelectionVersionModel>[]>([])
|
||||
const data = ref<(SelectOptions & { item: VersionModel })[]>([])
|
||||
const current = ref<string | number>()
|
||||
const currentModel = ref<FileSelectionVersionModel>()
|
||||
|
||||
const genFileSelectionItem = (
|
||||
item: VersionModel,
|
||||
): FileSelectionVersionModel => {
|
||||
const fileSelectionItem: FileSelectionVersionModel = { ...item }
|
||||
fileSelectionItem.selectionFiles = fileSelectionItem.files
|
||||
?.sort((file) => (file.type === 'Model' ? -1 : 1))
|
||||
.map((file) => {
|
||||
const parts = file.name.split('.')
|
||||
const extension = `.${parts.pop()}`
|
||||
const basename = parts.join('.')
|
||||
|
||||
const regexp = /---\n([\s\S]*?)\n---/
|
||||
const yamlMetadataMatch = item.description.match(regexp)
|
||||
const yamlMetadata = yaml.parse(yamlMetadataMatch?.[1] || '')
|
||||
yamlMetadata.hashes = file.hashes
|
||||
yamlMetadata.metadata = file.metadata
|
||||
const yamlContent = `---\n${yaml.stringify(yamlMetadata)}---`
|
||||
const description = item.description.replace(regexp, yamlContent)
|
||||
|
||||
return {
|
||||
label: file.type === 'Model' ? upperFirst(item.type) : file.type,
|
||||
value: file.id,
|
||||
item: file,
|
||||
command() {
|
||||
if (currentModel.value) {
|
||||
currentModel.value.basename = basename
|
||||
currentModel.value.extension = extension
|
||||
currentModel.value.sizeBytes = file.sizeKB * 1024
|
||||
currentModel.value.metadata = file.metadata
|
||||
currentModel.value.downloadUrl = file.downloadUrl
|
||||
currentModel.value.hashes = file.hashes
|
||||
currentModel.value.description = description
|
||||
currentModel.value.currentFileId = file.id
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
fileSelectionItem.currentFileId = item.files?.[0]?.id
|
||||
return fileSelectionItem
|
||||
}
|
||||
const currentModel = ref<BaseModel>()
|
||||
|
||||
const handleSearchByUrl = async (url: string) => {
|
||||
if (!url) {
|
||||
@@ -227,17 +177,14 @@ export const useModelSearch = () => {
|
||||
loading.show()
|
||||
return request(`/model-info?model-page=${encodeURIComponent(url)}`, {})
|
||||
.then((resData: VersionModel[]) => {
|
||||
data.value = resData.map((item) => {
|
||||
const resolvedItem = genFileSelectionItem(item)
|
||||
return {
|
||||
label: item.shortname,
|
||||
value: item.id,
|
||||
item: resolvedItem,
|
||||
command() {
|
||||
current.value = item.id
|
||||
},
|
||||
}
|
||||
})
|
||||
data.value = resData.map((item) => ({
|
||||
label: item.shortname,
|
||||
value: item.id,
|
||||
item,
|
||||
command() {
|
||||
current.value = item.id
|
||||
},
|
||||
}))
|
||||
current.value = data.value[0]?.value
|
||||
currentModel.value = data.value[0]?.item
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ export const useModelExplorer = () => {
|
||||
description: '',
|
||||
metadata: {},
|
||||
preview: '',
|
||||
previewType: 'image',
|
||||
type: folder ?? '',
|
||||
isFolder: true,
|
||||
children: [],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { defineStore } from 'hooks/store'
|
||||
import { useToast } from 'hooks/toast'
|
||||
import { castArray, cloneDeep } from 'lodash'
|
||||
import { TreeNode } from 'primevue/treenode'
|
||||
import { api, app } from 'scripts/comfyAPI'
|
||||
import { app } from 'scripts/comfyAPI'
|
||||
import { BaseModel, Model, SelectEvent, WithResolved } from 'types/typings'
|
||||
import { bytesToSize, formatDate, previewUrlToFile } from 'utils/common'
|
||||
import { ModelGrid } from 'utils/legacy'
|
||||
@@ -27,18 +27,16 @@ import {
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { configSetting } from './config'
|
||||
|
||||
const systemStat = ref()
|
||||
|
||||
type ModelFolder = Record<string, string[]>
|
||||
|
||||
const modelFolderProvideKey = Symbol('modelFolder') as InjectionKey<
|
||||
Ref<ModelFolder>
|
||||
>
|
||||
|
||||
export const genModelFullName = (model: BaseModel, splitter = '/') => {
|
||||
export const genModelFullName = (model: BaseModel) => {
|
||||
return [model.subFolder, `${model.basename}${model.extension}`]
|
||||
.filter(Boolean)
|
||||
.join(splitter)
|
||||
.join('/')
|
||||
}
|
||||
|
||||
export const genModelUrl = (model: BaseModel) => {
|
||||
@@ -236,12 +234,6 @@ export const useModels = defineStore('models', (store) => {
|
||||
return [prefixPath, fullname].filter(Boolean).join('/')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
api.getSystemStats().then((res) => {
|
||||
systemStat.value = res
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
initialized: initialized,
|
||||
folders: folders,
|
||||
@@ -553,11 +545,9 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
|
||||
* Local file url
|
||||
*/
|
||||
const localContent = ref<string>()
|
||||
const localContentType = ref<string>()
|
||||
const updateLocalContent = async (event: SelectEvent) => {
|
||||
const { files } = event
|
||||
localContent.value = files[0].objectURL
|
||||
localContentType.value = files[0].type
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -589,13 +579,16 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
|
||||
return content
|
||||
})
|
||||
|
||||
const previewType = computed(() => {
|
||||
return model.value.previewType
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
registerReset(() => {
|
||||
currentType.value = 'default'
|
||||
defaultContentPage.value = 0
|
||||
networkContent.value = undefined
|
||||
localContent.value = undefined
|
||||
localContentType.value = undefined
|
||||
})
|
||||
|
||||
registerSubmit((data) => {
|
||||
@@ -605,6 +598,7 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
|
||||
|
||||
const result = {
|
||||
preview,
|
||||
previewType,
|
||||
typeOptions,
|
||||
currentType,
|
||||
// default value
|
||||
@@ -614,7 +608,6 @@ export const useModelPreviewEditor = (formInstance: ModelFormInstance) => {
|
||||
networkContent,
|
||||
// local file
|
||||
localContent,
|
||||
localContentType,
|
||||
updateLocalContent,
|
||||
// no preview
|
||||
noPreviewContent,
|
||||
@@ -724,12 +717,11 @@ export const useModelNodeAction = () => {
|
||||
// Use the legacy method instead
|
||||
const removeEmbeddingExtension = true
|
||||
const strictDragToAdd = false
|
||||
const splitter = systemStat.value?.system.os === 'nt' ? '\\' : '/'
|
||||
|
||||
ModelGrid.dragAddModel(
|
||||
event,
|
||||
model.type,
|
||||
genModelFullName(model, splitter),
|
||||
genModelFullName(model),
|
||||
removeEmbeddingExtension,
|
||||
strictDragToAdd,
|
||||
)
|
||||
|
||||
155
src/i18n.ts
155
src/i18n.ts
@@ -1,12 +1,157 @@
|
||||
import { app } from 'scripts/comfyAPI'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import en from './locales/en.json'
|
||||
import zh from './locales/zh.json'
|
||||
|
||||
const messages = {
|
||||
en: en,
|
||||
zh: zh,
|
||||
en: {
|
||||
model: 'Model',
|
||||
modelManager: 'Model Manager',
|
||||
openModelManager: 'Open Model Manager',
|
||||
searchModels: 'Search models',
|
||||
modelCopied: 'Model Copied',
|
||||
download: 'Download',
|
||||
downloadList: 'Download List',
|
||||
downloadTask: 'Download Task',
|
||||
createDownloadTask: 'Create Download Task',
|
||||
parseModelUrl: 'Parse Model URL',
|
||||
pleaseInputModelUrl: 'Input a URL from civitai.com or huggingface.co',
|
||||
cancel: 'Cancel',
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
deleteAsk: 'Confirm delete this {0}?',
|
||||
modelType: 'Model Type',
|
||||
default: 'Default',
|
||||
network: 'Network',
|
||||
local: 'Local',
|
||||
none: 'None',
|
||||
uploadFile: 'Upload File',
|
||||
tapToChange: 'Tap description to change content',
|
||||
name: 'Name',
|
||||
width: 'Width',
|
||||
height: 'Height',
|
||||
reset: 'Reset',
|
||||
back: 'Back',
|
||||
next: 'Next',
|
||||
batchScanModelInformation: 'Batch scan model information',
|
||||
modelInformationScanning: 'Scanning model information',
|
||||
selectModelType: 'Select model type',
|
||||
selectSubdirectory: 'Select subdirectory',
|
||||
scanModelInformation: 'Scan model information',
|
||||
selectedAllPaths: 'Selected all model paths',
|
||||
selectedSpecialPath: 'Selected special path',
|
||||
scanMissInformation: 'Download missing information',
|
||||
scanFullInformation: 'Override full information',
|
||||
noModelsInCurrentPath: 'There are no models available in the current path',
|
||||
sort: {
|
||||
name: 'Name',
|
||||
size: 'Largest',
|
||||
created: 'Latest created',
|
||||
modified: 'Latest modified',
|
||||
},
|
||||
size: {
|
||||
extraLarge: 'Extra Large Icons',
|
||||
large: 'Large Icons',
|
||||
medium: 'Medium Icons',
|
||||
small: 'Small Icons',
|
||||
custom: 'Custom Size',
|
||||
customTip: 'Set in `Settings > Model Manager > UI`',
|
||||
},
|
||||
info: {
|
||||
type: 'Model Type',
|
||||
pathIndex: 'Directory',
|
||||
basename: 'File Name',
|
||||
sizeBytes: 'File Size',
|
||||
createdAt: 'Created At',
|
||||
updatedAt: 'Updated At',
|
||||
},
|
||||
setting: {
|
||||
apiKey: 'API Key',
|
||||
cardHeight: 'Card Height',
|
||||
cardWidth: 'Card Width',
|
||||
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)',
|
||||
ui: 'UI',
|
||||
cardSize: 'Card Size',
|
||||
useFlatUI: 'Flat Layout',
|
||||
},
|
||||
},
|
||||
zh: {
|
||||
model: '模型',
|
||||
modelManager: '模型管理器',
|
||||
openModelManager: '打开模型管理器',
|
||||
searchModels: '搜索模型',
|
||||
modelCopied: '模型节点已拷贝',
|
||||
download: '下载',
|
||||
downloadList: '下载列表',
|
||||
downloadTask: '下载任务',
|
||||
createDownloadTask: '创建下载任务',
|
||||
parseModelUrl: '解析模型URL',
|
||||
pleaseInputModelUrl: '输入 civitai.com 或 huggingface.co 的 URL',
|
||||
cancel: '取消',
|
||||
save: '保存',
|
||||
delete: '删除',
|
||||
deleteAsk: '确定要删除此{0}?',
|
||||
modelType: '模型类型',
|
||||
default: '默认',
|
||||
network: '网络',
|
||||
local: '本地',
|
||||
none: '无',
|
||||
uploadFile: '上传文件',
|
||||
tapToChange: '点击描述可更改内容',
|
||||
name: '名称',
|
||||
width: '宽度',
|
||||
height: '高度',
|
||||
reset: '重置',
|
||||
back: '返回',
|
||||
next: '下一步',
|
||||
batchScanModelInformation: '批量扫描模型信息',
|
||||
modelInformationScanning: '扫描模型信息',
|
||||
selectModelType: '选择模型类型',
|
||||
selectSubdirectory: '选择子目录',
|
||||
scanModelInformation: '扫描模型信息',
|
||||
selectedAllPaths: '已选所有模型路径',
|
||||
selectedSpecialPath: '已选指定路径',
|
||||
scanMissInformation: '下载缺失信息',
|
||||
scanFullInformation: '覆盖所有信息',
|
||||
noModelsInCurrentPath: '当前路径中没有可用的模型',
|
||||
sort: {
|
||||
name: '名称',
|
||||
size: '最大',
|
||||
created: '最新创建',
|
||||
modified: '最新修改',
|
||||
},
|
||||
size: {
|
||||
extraLarge: '超大图标',
|
||||
large: '大图标',
|
||||
medium: '中等图标',
|
||||
small: '小图标',
|
||||
custom: '自定义尺寸',
|
||||
customTip: '在 `设置 > 模型管理器 > 外观` 中设置',
|
||||
},
|
||||
info: {
|
||||
type: '类型',
|
||||
pathIndex: '目录',
|
||||
basename: '文件名',
|
||||
sizeBytes: '文件大小',
|
||||
createdAt: '创建时间',
|
||||
updatedAt: '更新时间',
|
||||
},
|
||||
setting: {
|
||||
apiKey: '密钥',
|
||||
cardHeight: '卡片高度',
|
||||
cardWidth: '卡片宽度',
|
||||
scan: '扫描',
|
||||
scanMissing: '下载缺失的信息或预览图片',
|
||||
scanAll: '覆盖所有模型信息和预览图片',
|
||||
includeHiddenFiles: '包含隐藏文件(以 . 开头的文件或文件夹)',
|
||||
excludeScanTypes: '排除扫描类型(使用英文逗号隔开)',
|
||||
ui: '外观',
|
||||
cardSize: '卡片尺寸',
|
||||
useFlatUI: '展平布局',
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const getLocalLanguage = () => {
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"model": "Model",
|
||||
"modelManager": "Model Manager",
|
||||
"openModelManager": "Open Model Manager",
|
||||
"searchModels": "Search models",
|
||||
"modelCopied": "Model Copied",
|
||||
"download": "Download",
|
||||
"downloadList": "Download List",
|
||||
"downloadTask": "Download Task",
|
||||
"createDownloadTask": "Create Download Task",
|
||||
"parseModelUrl": "Parse Model URL",
|
||||
"pleaseInputModelUrl": "Input a URL from civitai.com or huggingface.co",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"deleteAsk": "Confirm delete this {0}?",
|
||||
"modelType": "Model Type",
|
||||
"default": "Default",
|
||||
"network": "Network",
|
||||
"local": "Local",
|
||||
"none": "None",
|
||||
"uploadFile": "Upload File",
|
||||
"tapToChange": "Tap description to change content",
|
||||
"name": "Name",
|
||||
"width": "Width",
|
||||
"height": "Height",
|
||||
"reset": "Reset",
|
||||
"back": "Back",
|
||||
"next": "Next",
|
||||
"batchScanModelInformation": "Batch scan model information",
|
||||
"modelInformationScanning": "Scanning model information",
|
||||
"selectModelType": "Select model type",
|
||||
"selectSubdirectory": "Select subdirectory",
|
||||
"scanModelInformation": "Scan model information",
|
||||
"selectedAllPaths": "Selected all model paths",
|
||||
"selectedSpecialPath": "Selected special path",
|
||||
"scanMissInformation": "Download missing information",
|
||||
"scanFullInformation": "Override full information",
|
||||
"noModelsInCurrentPath": "There are no models available in the current path",
|
||||
"uploadModel": "Upload Model",
|
||||
"chooseFile": "Choose File",
|
||||
"sort": {
|
||||
"name": "Name",
|
||||
"size": "Largest",
|
||||
"created": "Latest created",
|
||||
"modified": "Latest modified"
|
||||
},
|
||||
"size": {
|
||||
"extraLarge": "Extra Large Icons",
|
||||
"large": "Large Icons",
|
||||
"medium": "Medium Icons",
|
||||
"small": "Small Icons",
|
||||
"custom": "Custom Size",
|
||||
"customTip": "Set in `Settings > Model Manager > UI`"
|
||||
},
|
||||
"info": {
|
||||
"type": "Model Type",
|
||||
"pathIndex": "Directory",
|
||||
"basename": "File Name",
|
||||
"sizeBytes": "File Size",
|
||||
"createdAt": "Created At",
|
||||
"updatedAt": "Updated At"
|
||||
},
|
||||
"setting": {
|
||||
"apiKey": "API Key",
|
||||
"cardHeight": "Card Height",
|
||||
"cardWidth": "Card Width",
|
||||
"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)",
|
||||
"ui": "UI",
|
||||
"cardSize": "Card Size",
|
||||
"useFlatUI": "Flat Layout"
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
{
|
||||
"model": "模型",
|
||||
"modelManager": "模型管理器",
|
||||
"openModelManager": "打开模型管理器",
|
||||
"searchModels": "搜索模型",
|
||||
"modelCopied": "模型节点已拷贝",
|
||||
"download": "下载",
|
||||
"downloadList": "下载列表",
|
||||
"downloadTask": "下载任务",
|
||||
"createDownloadTask": "创建下载任务",
|
||||
"parseModelUrl": "解析模型URL",
|
||||
"pleaseInputModelUrl": "输入 civitai.com 或 huggingface.co 的 URL",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"deleteAsk": "确定要删除此{0}?",
|
||||
"modelType": "模型类型",
|
||||
"default": "默认",
|
||||
"network": "网络",
|
||||
"local": "本地",
|
||||
"none": "无",
|
||||
"uploadFile": "上传文件",
|
||||
"tapToChange": "点击描述可更改内容",
|
||||
"name": "名称",
|
||||
"width": "宽度",
|
||||
"height": "高度",
|
||||
"reset": "重置",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"batchScanModelInformation": "批量扫描模型信息",
|
||||
"modelInformationScanning": "扫描模型信息",
|
||||
"selectModelType": "选择模型类型",
|
||||
"selectSubdirectory": "选择子目录",
|
||||
"scanModelInformation": "扫描模型信息",
|
||||
"selectedAllPaths": "已选所有模型路径",
|
||||
"selectedSpecialPath": "已选指定路径",
|
||||
"scanMissInformation": "下载缺失信息",
|
||||
"scanFullInformation": "覆盖所有信息",
|
||||
"noModelsInCurrentPath": "当前路径中没有可用的模型",
|
||||
"uploadModel": "上传模型",
|
||||
"chooseFile": "选择文件",
|
||||
"sort": {
|
||||
"name": "名称",
|
||||
"size": "最大",
|
||||
"created": "最新创建",
|
||||
"modified": "最新修改"
|
||||
},
|
||||
"size": {
|
||||
"extraLarge": "超大图标",
|
||||
"large": "大图标",
|
||||
"medium": "中等图标",
|
||||
"small": "小图标",
|
||||
"custom": "自定义尺寸",
|
||||
"customTip": "在 `设置 > 模型管理器 > 外观` 中设置"
|
||||
},
|
||||
"info": {
|
||||
"type": "类型",
|
||||
"pathIndex": "目录",
|
||||
"basename": "文件名",
|
||||
"sizeBytes": "文件大小",
|
||||
"createdAt": "创建时间",
|
||||
"updatedAt": "更新时间"
|
||||
},
|
||||
"setting": {
|
||||
"apiKey": "密钥",
|
||||
"cardHeight": "卡片高度",
|
||||
"cardWidth": "卡片宽度",
|
||||
"scan": "扫描",
|
||||
"scanMissing": "下载缺失的信息或预览图片",
|
||||
"scanAll": "覆盖所有模型信息和预览图片",
|
||||
"includeHiddenFiles": "包含隐藏文件(以 . 开头的文件或文件夹)",
|
||||
"excludeScanTypes": "排除扫描类型(使用英文逗号隔开)",
|
||||
"ui": "外观",
|
||||
"cardSize": "卡片尺寸",
|
||||
"useFlatUI": "展平布局"
|
||||
}
|
||||
}
|
||||
11
src/types/global.d.ts
vendored
11
src/types/global.d.ts
vendored
@@ -1,10 +1,6 @@
|
||||
declare namespace ComfyAPI {
|
||||
namespace api {
|
||||
class ComfyApiEvent {
|
||||
getSystemStats: () => Promise<any>
|
||||
}
|
||||
|
||||
class ComfyApi extends ComfyApiEvent {
|
||||
class ComfyApi {
|
||||
socket: WebSocket
|
||||
fetchApi: (route: string, options?: RequestInit) => Promise<Response>
|
||||
addEventListener: (
|
||||
@@ -12,11 +8,6 @@ declare namespace ComfyAPI {
|
||||
callback: (event: CustomEvent) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
) => void
|
||||
removeEventListener: (
|
||||
type: string,
|
||||
callback: (event: CustomEvent) => void,
|
||||
options?: AddEventListenerOptions,
|
||||
) => void
|
||||
}
|
||||
|
||||
const api: ComfyApi
|
||||
|
||||
12
src/types/typings.d.ts
vendored
12
src/types/typings.d.ts
vendored
@@ -11,6 +11,7 @@ export interface BaseModel {
|
||||
pathIndex: number
|
||||
isFolder: boolean
|
||||
preview: string | string[]
|
||||
previewType: string
|
||||
description: string
|
||||
metadata: Record<string, string>
|
||||
}
|
||||
@@ -21,22 +22,11 @@ export interface Model extends BaseModel {
|
||||
children?: Model[]
|
||||
}
|
||||
|
||||
export interface VersionModelFile {
|
||||
id: number
|
||||
sizeKB: number
|
||||
name: string
|
||||
type: string
|
||||
metadata: Record<string, string>
|
||||
hashes: Record<string, string>
|
||||
downloadUrl: string
|
||||
}
|
||||
|
||||
export interface VersionModel extends BaseModel {
|
||||
shortname: string
|
||||
downloadPlatform: string
|
||||
downloadUrl: string
|
||||
hashes?: Record<string, string>
|
||||
files?: VersionModelFile[]
|
||||
}
|
||||
|
||||
export type WithResolved<T> = Omit<T, 'preview'> & {
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/**
|
||||
* Media file utility functions
|
||||
*/
|
||||
|
||||
const VIDEO_EXTENSIONS = [
|
||||
'.mp4',
|
||||
'.webm',
|
||||
'.mov',
|
||||
'.avi',
|
||||
'.mkv',
|
||||
'.flv',
|
||||
'.wmv',
|
||||
'.m4v',
|
||||
'.ogv',
|
||||
]
|
||||
|
||||
const VIDEO_HOST_PATTERNS = [
|
||||
'/video', // Civitai video URLs often end with /video
|
||||
'type=video', // URLs with video type parameter
|
||||
'format=video', // URLs with video format parameter
|
||||
'video.civitai.com', // Civitai video domain
|
||||
]
|
||||
|
||||
/**
|
||||
* Detect if a URL points to a video based on extension or URL patterns
|
||||
* @param url - The URL to check
|
||||
* @param localContentType - Optional MIME type for local files
|
||||
*/
|
||||
export const isVideoUrl = (url: string, localContentType?: string): boolean => {
|
||||
if (!url) return false
|
||||
|
||||
// For local files with known MIME type
|
||||
if (localContentType && localContentType.startsWith('video/')) {
|
||||
return true
|
||||
}
|
||||
|
||||
const urlLower = url.toLowerCase()
|
||||
|
||||
// First check if URL ends with a video extension
|
||||
for (const ext of VIDEO_EXTENSIONS) {
|
||||
if (urlLower.endsWith(ext)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Check if URL contains a video extension anywhere (for complex URLs like Civitai)
|
||||
if (VIDEO_EXTENSIONS.some((ext) => urlLower.includes(ext))) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for specific video hosting patterns
|
||||
return VIDEO_HOST_PATTERNS.some((pattern) => urlLower.includes(pattern))
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
"moduleResolution": "Node",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": false,
|
||||
|
||||
@@ -119,13 +119,12 @@ export default defineConfig({
|
||||
// Disabling tree-shaking
|
||||
// Prevent vite remove unused exports
|
||||
treeshake: true,
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('primevue')) {
|
||||
return 'primevue'
|
||||
}
|
||||
},
|
||||
},
|
||||
external: [
|
||||
'vue',
|
||||
'vue-i18n',
|
||||
/^primevue\/?.*/,
|
||||
/^@primevue\/themes\/?.*/,
|
||||
],
|
||||
},
|
||||
chunkSizeWarningLimit: 1024,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user