6 Commits

Author SHA1 Message Date
Hayden
c96a164f68 Prepare release 2.7.0 2025-08-11 16:59:45 +08:00
Hayden
0ae0716272 fix: Ensure downloadable files are available for model versions without model files (#199) 2025-08-11 10:51:18 +08:00
Hayden
b692270f87 perf: Reconstruct the i18n directory structure (#198) 2025-08-11 10:38:40 +08:00
Hayden
a9675a5d83 feat: Add model upload functionality (#194) 2025-08-11 09:10:39 +08:00
Hayden
ac4a168f13 feat: support download multiple actual files (#196) 2025-08-11 09:10:20 +08:00
Hayden
8b9f3a0e65 191 windows path is wrong when dragging to create lora node from manager (#195)
* fix: match system path style

* fix: only match dragToAddModelNode
2025-08-11 09:09:51 +08:00
19 changed files with 668 additions and 180 deletions

View File

@@ -1,5 +1,6 @@
{
"recommendations": [
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"lokalise.i18n-ally"
]
}

View File

@@ -43,5 +43,10 @@
"editor.quickSuggestions": {
"strings": "on"
},
"css.lint.unknownAtRules": "ignore"
"css.lint.unknownAtRules": "ignore",
"i18n-ally.localesPaths": [
"src/locales"
],
"i18n-ally.sourceLanguage": "en",
"i18n-ally.keystyle": "nested"
}

View File

@@ -41,12 +41,14 @@ 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"

View File

@@ -69,8 +69,12 @@ class CivitaiModelSearcher(ModelSearcher):
models: list[dict] = []
for version in model_versions:
model_files: list[dict] = version.get("files", [])
model_files = utils.filter_with(model_files, {"type": "Model"})
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
shortname = version.get("name", None) if len(model_files) > 0 else None
@@ -108,7 +112,7 @@ class CivitaiModelSearcher(ModelSearcher):
description_parts.append("")
model = {
"id": file.get("id"),
"id": version.get("id"),
"shortname": shortname or basename,
"basename": basename,
"extension": extension,
@@ -122,6 +126,7 @@ 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)

79
py/upload.py Normal file
View File

@@ -0,0 +1,79 @@
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)

View File

@@ -1,7 +1,7 @@
[project]
name = "comfyui-model-manager"
description = "Manage models: browsing, download and delete."
version = "2.6.3"
version = "2.7.0"
license = { file = "LICENSE" }
dependencies = ["markdownify"]

View File

@@ -10,6 +10,7 @@ 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'
@@ -64,6 +65,21 @@ 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
@@ -93,6 +109,11 @@ 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,

View File

@@ -31,12 +31,20 @@
<KeepAlive>
<ModelContent
v-if="currentModel"
:key="currentModel.id"
:key="`${currentModel.id}-${currentModel.currentFileId}`"
: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')"

View File

@@ -0,0 +1,274 @@
<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>

View File

@@ -17,7 +17,7 @@
></ModelPreview>
<div class="flex flex-col gap-4 overflow-hidden">
<div class="flex items-center justify-end gap-4">
<div class="flex h-10 items-center justify-end gap-4">
<slot name="action" :metadata="formInstance.metadata.value"></slot>
</div>

View File

@@ -2,17 +2,19 @@ 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()
@@ -162,12 +164,60 @@ 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<(SelectOptions & { item: VersionModel })[]>([])
const data = ref<WithSelection<FileSelectionVersionModel>[]>([])
const current = ref<string | number>()
const currentModel = ref<BaseModel>()
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 handleSearchByUrl = async (url: string) => {
if (!url) {
@@ -177,14 +227,17 @@ export const useModelSearch = () => {
loading.show()
return request(`/model-info?model-page=${encodeURIComponent(url)}`, {})
.then((resData: VersionModel[]) => {
data.value = resData.map((item) => ({
label: item.shortname,
value: item.id,
item,
command() {
current.value = item.id
},
}))
data.value = resData.map((item) => {
const resolvedItem = genFileSelectionItem(item)
return {
label: item.shortname,
value: item.id,
item: resolvedItem,
command() {
current.value = item.id
},
}
})
current.value = data.value[0]?.value
currentModel.value = data.value[0]?.item

View File

@@ -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 { app } from 'scripts/comfyAPI'
import { api, 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,16 +27,18 @@ 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) => {
export const genModelFullName = (model: BaseModel, splitter = '/') => {
return [model.subFolder, `${model.basename}${model.extension}`]
.filter(Boolean)
.join('/')
.join(splitter)
}
export const genModelUrl = (model: BaseModel) => {
@@ -234,6 +236,12 @@ 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,
@@ -717,11 +725,12 @@ 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),
genModelFullName(model, splitter),
removeEmbeddingExtension,
strictDragToAdd,
)

View File

@@ -1,157 +1,12 @@
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: {
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: '展平布局',
},
},
en: en,
zh: zh,
}
const getLocalLanguage = () => {

77
src/locales/en.json Normal file
View File

@@ -0,0 +1,77 @@
{
"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"
}
}

77
src/locales/zh.json Normal file
View File

@@ -0,0 +1,77 @@
{
"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
View File

@@ -1,6 +1,10 @@
declare namespace ComfyAPI {
namespace api {
class ComfyApi {
class ComfyApiEvent {
getSystemStats: () => Promise<any>
}
class ComfyApi extends ComfyApiEvent {
socket: WebSocket
fetchApi: (route: string, options?: RequestInit) => Promise<Response>
addEventListener: (
@@ -8,6 +12,11 @@ declare namespace ComfyAPI {
callback: (event: CustomEvent) => void,
options?: AddEventListenerOptions,
) => void
removeEventListener: (
type: string,
callback: (event: CustomEvent) => void,
options?: AddEventListenerOptions,
) => void
}
const api: ComfyApi

View File

@@ -22,11 +22,22 @@ 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'> & {

View File

@@ -11,6 +11,7 @@
"moduleResolution": "Node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
/* Linting */
"strict": false,

View File

@@ -119,12 +119,13 @@ export default defineConfig({
// Disabling tree-shaking
// Prevent vite remove unused exports
treeshake: true,
external: [
'vue',
'vue-i18n',
/^primevue\/?.*/,
/^@primevue\/themes\/?.*/,
],
output: {
manualChunks(id) {
if (id.includes('primevue')) {
return 'primevue'
}
},
},
},
chunkSizeWarningLimit: 1024,
},