5 Commits

Author SHA1 Message Date
Hayden
00d23ff74f prepare release 2.1.4
Optimize models request API
2024-12-03 14:14:11 +08:00
Hayden
dc46f498be Split model get list (#74)
Get the model list separately by model type and defer the request.
2024-12-03 14:05:18 +08:00
Hayden
6d67b00b17 Fix publishing failed (#69) 2024-11-29 09:40:25 +08:00
Hayden
cda24405b5 prepare release 2.1.3 2024-11-28 13:03:06 +08:00
Hayden
6fa90be8c4 Fix preview path (#66) 2024-11-28 13:02:36 +08:00
8 changed files with 115 additions and 64 deletions

View File

@@ -89,6 +89,7 @@ async def delete_model_download_task(request):
return web.json_response({"success": False, "error": error_msg}) return web.json_response({"success": False, "error": error_msg})
# @deprecated
@routes.get("/model-manager/base-folders") @routes.get("/model-manager/base-folders")
async def get_model_paths(request): async def get_model_paths(request):
""" """
@@ -124,12 +125,12 @@ async def create_model(request):
@routes.get("/model-manager/models") @routes.get("/model-manager/models")
async def read_models(request): async def list_model_types(request):
""" """
Scan all models and read their information. Scan all models and read their information.
""" """
try: try:
result = services.scan_models(request) result = utils.resolve_model_base_paths()
return web.json_response({"success": True, "data": result}) return web.json_response({"success": True, "data": result})
except Exception as e: except Exception as e:
error_msg = f"Read models failed: {str(e)}" error_msg = f"Read models failed: {str(e)}"
@@ -137,6 +138,18 @@ async def read_models(request):
return web.json_response({"success": False, "error": error_msg}) return web.json_response({"success": False, "error": error_msg})
@routes.get("/model-manager/models/{folder}")
async def read_models(request):
try:
folder = request.match_info.get("folder", None)
results = services.scan_models(folder, request)
return web.json_response({"success": True, "data": results})
except Exception as e:
error_msg = f"Read models failed: {str(e)}"
utils.print_error(error_msg)
return web.json_response({"success": False, "error": error_msg})
@routes.get("/model-manager/model/{type}/{index}/{filename:.*}") @routes.get("/model-manager/model/{type}/{index}/{filename:.*}")
async def read_model_info(request): async def read_model_info(request):
""" """

View File

@@ -8,47 +8,46 @@ from . import download
from . import searcher from . import searcher
def scan_models(request): def scan_models(folder: str, request):
result = [] result = []
model_base_paths = utils.resolve_model_base_paths()
for model_type in model_base_paths:
folders, extensions = folder_paths.folder_names_and_paths[model_type] folders, extensions = folder_paths.folder_names_and_paths[folder]
for path_index, base_path in enumerate(folders): for path_index, base_path in enumerate(folders):
files = utils.recursive_search_files(base_path, request) files = utils.recursive_search_files(base_path, request)
models = folder_paths.filter_files_extensions(files, folder_paths.supported_pt_extensions) models = folder_paths.filter_files_extensions(files, folder_paths.supported_pt_extensions)
for fullname in models: for fullname in models:
fullname = utils.normalize_path(fullname) fullname = utils.normalize_path(fullname)
basename = os.path.splitext(fullname)[0] basename = os.path.splitext(fullname)[0]
extension = os.path.splitext(fullname)[1] extension = os.path.splitext(fullname)[1]
abs_path = utils.join_path(base_path, fullname) abs_path = utils.join_path(base_path, fullname)
file_stats = os.stat(abs_path) file_stats = os.stat(abs_path)
# Resolve preview # Resolve preview
image_name = utils.get_model_preview_name(abs_path) image_name = utils.get_model_preview_name(abs_path)
abs_image_path = utils.join_path(base_path, image_name) image_name = utils.join_path(os.path.dirname(fullname), image_name)
if os.path.isfile(abs_image_path): abs_image_path = utils.join_path(base_path, image_name)
image_state = os.stat(abs_image_path) if os.path.isfile(abs_image_path):
image_timestamp = round(image_state.st_mtime_ns / 1000000) image_state = os.stat(abs_image_path)
image_name = f"{image_name}?ts={image_timestamp}" image_timestamp = round(image_state.st_mtime_ns / 1000000)
model_preview = f"/model-manager/preview/{model_type}/{path_index}/{image_name}" image_name = f"{image_name}?ts={image_timestamp}"
model_preview = f"/model-manager/preview/{folder}/{path_index}/{image_name}"
model_info = { model_info = {
"fullname": fullname, "fullname": fullname,
"basename": basename, "basename": basename,
"extension": extension, "extension": extension,
"type": model_type, "type": folder,
"pathIndex": path_index, "pathIndex": path_index,
"sizeBytes": file_stats.st_size, "sizeBytes": file_stats.st_size,
"preview": model_preview, "preview": model_preview,
"createdAt": round(file_stats.st_ctime_ns / 1000000), "createdAt": round(file_stats.st_ctime_ns / 1000000),
"updatedAt": round(file_stats.st_mtime_ns / 1000000), "updatedAt": round(file_stats.st_mtime_ns / 1000000),
} }
result.append(model_info) result.append(model_info)
return result return result

View File

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

View File

@@ -15,16 +15,18 @@ import { useStoreProvider } from 'hooks/store'
import { useToast } from 'hooks/toast' import { useToast } from 'hooks/toast'
import GlobalConfirm from 'primevue/confirmdialog' import GlobalConfirm from 'primevue/confirmdialog'
import { $el, app, ComfyButton } from 'scripts/comfyAPI' import { $el, app, ComfyButton } from 'scripts/comfyAPI'
import { onMounted } from 'vue' import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { t } = useI18n() const { t } = useI18n()
const { dialog, models, config, download } = useStoreProvider() const { dialog, models, config, download } = useStoreProvider()
const { toast } = useToast() const { toast } = useToast()
const firstOpenManager = ref(true)
onMounted(() => { onMounted(() => {
const refreshModelsAndConfig = async () => { const refreshModelsAndConfig = async () => {
await Promise.all([models.refresh(), config.refresh()]) await Promise.all([models.refresh(true)])
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: 'Refreshed Models', summary: 'Refreshed Models',
@@ -50,6 +52,11 @@ onMounted(() => {
const openManagerDialog = () => { const openManagerDialog = () => {
const { cardWidth, gutter, aspect } = config const { cardWidth, gutter, aspect } = config
if (firstOpenManager.value) {
models.refresh(true)
firstOpenManager.value = false
}
dialog.open({ dialog.open({
key: 'model-manager', key: 'model-manager',
title: t('modelManager'), title: t('modelManager'),

View File

@@ -88,9 +88,9 @@ import { genModelKey } from 'utils/model'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
const { isMobile, cardWidth, gutter, aspect, modelFolders } = useConfig() const { isMobile, cardWidth, gutter, aspect } = useConfig()
const { data } = useModels() const { data, folders } = useModels()
const { t } = useI18n() const { t } = useI18n()
const responseScroll = ref() const responseScroll = ref()
@@ -99,7 +99,7 @@ const searchContent = ref<string>()
const currentType = ref('all') const currentType = ref('all')
const typeOptions = computed(() => { const typeOptions = computed(() => {
return ['all', ...Object.keys(modelFolders.value)].map((type) => { return ['all', ...Object.keys(folders.value)].map((type) => {
return { return {
label: type, label: type,
value: type, value: type,
@@ -143,7 +143,9 @@ const colSpan = ref(1)
const colSpanWidth = ref(cardWidth) const colSpanWidth = ref(cardWidth)
const list = computed(() => { const list = computed(() => {
const filterList = data.value.filter((model) => { const mergedList = Object.values(data.value).flat()
const filterList = mergedList.filter((model) => {
const showAllModel = currentType.value === 'all' const showAllModel = currentType.value === 'all'
const matchType = showAllModel || model.type === currentType.value const matchType = showAllModel || model.type === currentType.value

View File

@@ -49,15 +49,13 @@
<script setup lang="ts"> <script setup lang="ts">
import ResponseInput from 'components/ResponseInput.vue' import ResponseInput from 'components/ResponseInput.vue'
import ResponseSelect from 'components/ResponseSelect.vue' import ResponseSelect from 'components/ResponseSelect.vue'
import { useConfig } from 'hooks/config'
import { useModelBaseInfo } from 'hooks/model' import { useModelBaseInfo } from 'hooks/model'
import { computed } from 'vue' import { computed } from 'vue'
const editable = defineModel<boolean>('editable') const editable = defineModel<boolean>('editable')
const { modelFolders } = useConfig() const { baseInfo, pathIndex, basename, extension, type, modelFolders } =
useModelBaseInfo()
const { baseInfo, pathIndex, basename, extension, type } = useModelBaseInfo()
const typeOptions = computed(() => { const typeOptions = computed(() => {
return Object.keys(modelFolders.value).map((curr) => { return Object.keys(modelFolders.value).map((curr) => {

View File

@@ -1,4 +1,4 @@
import { request, useRequest } from 'hooks/request' import { request } from 'hooks/request'
import { defineStore } from 'hooks/store' import { defineStore } from 'hooks/store'
import { $el, app, ComfyDialog } from 'scripts/comfyAPI' import { $el, app, ComfyDialog } from 'scripts/comfyAPI'
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
@@ -8,10 +8,6 @@ export const useConfig = defineStore('config', (store) => {
const mobileDeviceBreakPoint = 759 const mobileDeviceBreakPoint = 759
const isMobile = ref(window.innerWidth < mobileDeviceBreakPoint) const isMobile = ref(window.innerWidth < mobileDeviceBreakPoint)
type ModelFolder = Record<string, string[]>
const { data: modelFolders, refresh: refreshModelFolders } =
useRequest<ModelFolder>('/base-folders')
const checkDeviceType = () => { const checkDeviceType = () => {
isMobile.value = window.innerWidth < mobileDeviceBreakPoint isMobile.value = window.innerWidth < mobileDeviceBreakPoint
} }
@@ -24,17 +20,11 @@ export const useConfig = defineStore('config', (store) => {
window.removeEventListener('resize', checkDeviceType) window.removeEventListener('resize', checkDeviceType)
}) })
const refresh = async () => {
return Promise.all([refreshModelFolders()])
}
const config = { const config = {
isMobile, isMobile,
gutter: 16, gutter: 16,
cardWidth: 240, cardWidth: 240,
aspect: 7 / 9, aspect: 7 / 9,
modelFolders,
refresh,
} }
useAddConfigSettings(store) useAddConfigSettings(store)

View File

@@ -1,7 +1,6 @@
import { useConfig } from 'hooks/config'
import { useLoading } from 'hooks/loading' import { useLoading } from 'hooks/loading'
import { useMarkdown } from 'hooks/markdown' import { useMarkdown } from 'hooks/markdown'
import { request, useRequest } from 'hooks/request' import { request } from 'hooks/request'
import { defineStore } from 'hooks/store' import { defineStore } from 'hooks/store'
import { useToast } from 'hooks/toast' import { useToast } from 'hooks/toast'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
@@ -22,12 +21,45 @@ import {
} from 'vue' } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
type ModelFolder = Record<string, string[]>
const modelFolderProvideKey = Symbol('modelFolder')
export const useModels = defineStore('models', (store) => { export const useModels = defineStore('models', (store) => {
const { data, refresh } = useRequest<Model[]>('/models', { defaultValue: [] })
const { toast, confirm } = useToast() const { toast, confirm } = useToast()
const { t } = useI18n() const { t } = useI18n()
const loading = useLoading() const loading = useLoading()
const folders = ref<ModelFolder>({})
const refreshFolders = async () => {
return request('/models').then((resData) => {
folders.value = resData
})
}
provide(modelFolderProvideKey, folders)
const models = ref<Record<string, Model[]>>({})
const refreshModels = async (folder: string) => {
loading.show()
return request(`/models/${folder}`)
.then((resData) => {
models.value[folder] = resData
return resData
})
.finally(() => {
loading.hide()
})
}
const refreshAllModels = async (force = false) => {
const forceRefresh = force ? refreshFolders() : Promise.resolve()
return forceRefresh.then(() =>
Promise.allSettled(Object.keys(folders.value).map(refreshModels)),
)
}
const updateModel = async (model: BaseModel, data: BaseModel) => { const updateModel = async (model: BaseModel, data: BaseModel) => {
const updateData = new Map() const updateData = new Map()
let oldKey: string | null = null let oldKey: string | null = null
@@ -80,7 +112,7 @@ export const useModels = defineStore('models', (store) => {
store.dialog.close({ key: oldKey }) store.dialog.close({ key: oldKey })
} }
refresh() refreshModels(data.type)
} }
const deleteModel = async (model: BaseModel) => { const deleteModel = async (model: BaseModel) => {
@@ -112,7 +144,7 @@ export const useModels = defineStore('models', (store) => {
life: 2000, life: 2000,
}) })
store.dialog.close({ key: dialogKey }) store.dialog.close({ key: dialogKey })
return refresh() return refreshModels(model.type)
}) })
.then(() => { .then(() => {
resolve(void 0) resolve(void 0)
@@ -136,7 +168,13 @@ export const useModels = defineStore('models', (store) => {
}) })
} }
return { data, refresh, remove: deleteModel, update: updateModel } return {
folders: folders,
data: models,
refresh: refreshAllModels,
remove: deleteModel,
update: updateModel,
}
}) })
declare module 'hooks/store' { declare module 'hooks/store' {
@@ -204,7 +242,10 @@ const baseInfoKey = Symbol('baseInfo') as InjectionKey<
export const useModelBaseInfoEditor = (formInstance: ModelFormInstance) => { export const useModelBaseInfoEditor = (formInstance: ModelFormInstance) => {
const { formData: model, modelData } = formInstance const { formData: model, modelData } = formInstance
const { modelFolders } = useConfig() const provideModelFolders = inject<any>(modelFolderProvideKey)
const modelFolders = computed<ModelFolder>(() => {
return provideModelFolders?.value ?? {}
})
const type = computed({ const type = computed({
get: () => { get: () => {
@@ -304,6 +345,7 @@ export const useModelBaseInfoEditor = (formInstance: ModelFormInstance) => {
basename, basename,
extension, extension,
pathIndex, pathIndex,
modelFolders,
} }
provide(baseInfoKey, result) provide(baseInfoKey, result)