Compare commits

...
7 Commits
Author SHA1 Message Date
Hayden 39e4581558 Prepare release 2.8.4
update repository references from 'hayden-fr' to 'hayden-cn'
2026-05-27 13:04:38 +08:00
hayden c55ee62ac9 fix: update repository references from 'hayden-fr' to 'hayden-cn' 2026-05-27 13:01:33 +08:00
Hayden 33c43085c4 [feat]dialog header action tooltip support (#219)
* fix: remove duplicate variables resulting from merging

* feat(dialog): add tooltip support for header buttons
2026-02-26 15:23:40 +08:00
AinaemaetandHayden dc1d12649a feat(ui): add hidden-files toggle and i18n updates (#214)
* feat(search): add multi-token regex and wildcard support

* feat(ui): add layout toggle button in Model Manager header

* feat(ui): add hidden-files toggle and i18n updates

- Added a new header button in the Model Manager to toggle display of
hidden files/folders (dot-prefixed). This updates the existing
`ModelManager.Scan.IncludeHiddenFiles` setting, persists it, and
refreshes the dialog so changes apply immediately.
- Button shows dynamic icon + tooltip:
  - 👁 "Show hidden files"
  - 👁‍🗨 "Hide hidden files"

- Updated i18n locale files:
  - en.json: added strings for both the layout toggle and hidden-files toggle
  - zh.json: added translations for the same strings

Note: normally these i18n keys would have been split between the layout
commit and this one, but they are grouped here for simplicity so all UI
strings for the new header buttons are introduced together.

---------

Co-authored-by: Hayden <48267247+hayden-fr@users.noreply.github.com>
2026-02-26 15:09:26 +08:00
moldwebs 0fdbc5d7be Direct file download (#215) 2026-02-26 14:37:51 +08:00
Ainaemaet 60837ae35f feat(ui): add layout toggle + hidden toggle, cleanup and formatting (#213) 2026-02-26 14:24:50 +08:00
Ainaemaet e88a77f224 feat(search): add multi-token regex and wildcard support (#211)
* feat(search): add multi-token regex and wildcard support

* feat(ui): add layout toggle button in Model Manager header
2025-09-24 15:18:25 +08:00
17 changed files with 531 additions and 31 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
publish-node:
name: Release and Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'hayden-fr' }}
if: ${{ github.repository_owner == 'hayden-cn' }}
steps:
- name: Check out code
uses: actions/checkout@v4
+2 -2
View File
@@ -8,8 +8,8 @@ Designed to support desktop, mobile and multi-screen devices.
There are three installation methods, choose one
1. Clone the repository: `git clone https://github.com/hayden-fr/ComfyUI-Model-Manager.git` to your ComfyUI `custom_nodes` folder
2. Download the [latest release](https://github.com/hayden-fr/ComfyUI-Model-Manager/releases/latest/download/dist.tar.gz) and extract it to your ComfyUI `custom_nodes` folder
1. Clone the repository: `git clone https://github.com/hayden-cn/ComfyUI-Model-Manager.git` to your ComfyUI `custom_nodes` folder
2. Download the [latest release](https://github.com/hayden-cn/ComfyUI-Model-Manager/releases/latest/download/dist.tar.gz) and extract it to your ComfyUI `custom_nodes` folder
3. Use comfy cli: `comfy node registry-install comfyui-model-manager`
## Features
+1
View File
@@ -35,6 +35,7 @@
"@primevue/themes": "^4.0.7",
"@vueuse/core": "^11.3.0",
"dayjs": "^1.11.13",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"markdown-it-metadata-block": "^1.0.6",
+3 -9
View File
@@ -17,6 +17,9 @@ importers:
dayjs:
specifier: ^1.11.13
version: 1.11.13
js-yaml:
specifier: ^4.1.0
version: 4.1.0
lodash:
specifier: ^4.17.21
version: 4.17.21
@@ -405,55 +408,46 @@ packages:
resolution: {integrity: sha512-9pxQJSPwFsVi0ttOmqLY4JJ9pg9t1gKhK0JDbV1yUEETSx55fdyCjt39eBQ54OQCzAF0nVGO6LfEH1KnCPvelA==}
cpu: [arm]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.22.0':
resolution: {integrity: sha512-YJ5Ku5BmNJZb58A4qSEo3JlIG4d3G2lWyBi13ABlXzO41SsdnUKi3HQHe83VpwBVG4jHFTW65jOQb8qyoR+qzg==}
cpu: [arm]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.22.0':
resolution: {integrity: sha512-U4G4u7f+QCqHlVg1Nlx+qapZy+QoG+NV6ux+upo/T7arNGwKvKP2kmGM4W5QTbdewWFgudQxi3kDNST9GT1/mg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.22.0':
resolution: {integrity: sha512-aQpNlKmx3amwkA3a5J6nlXSahE1ijl0L9KuIjVOUhfOh7uw2S4piR3mtpxpRtbnK809SBtyPsM9q15CPTsY7HQ==}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rollup/rollup-linux-powerpc64le-gnu@4.22.0':
resolution: {integrity: sha512-9fx6Zj/7vve/Fp4iexUFRKb5+RjLCff6YTRQl4CoDhdMfDoobWmhAxQWV3NfShMzQk1Q/iCnageFyGfqnsmeqQ==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-riscv64-gnu@4.22.0':
resolution: {integrity: sha512-VWQiCcN7zBgZYLjndIEh5tamtnKg5TGxyZPWcN9zBtXBwfcGSZ5cHSdQZfQH/GB4uRxk0D3VYbOEe/chJhPGLQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-s390x-gnu@4.22.0':
resolution: {integrity: sha512-EHmPnPWvyYqncObwqrosb/CpH3GOjE76vWVs0g4hWsDRUVhg61hBmlVg5TPXqF+g+PvIbqkC7i3h8wbn4Gp2Fg==}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.22.0':
resolution: {integrity: sha512-tsSWy3YQzmpjDKnQ1Vcpy3p9Z+kMFbSIesCdMNgLizDWFhrLZIoN21JSq01g+MZMDFF+Y1+4zxgrlqPjid5ohg==}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.22.0':
resolution: {integrity: sha512-anr1Y11uPOQrpuU8XOikY5lH4Qu94oS6j0xrulHk3NkLDq19MlX8Ng/pVipjxBJ9a2l3+F39REZYyWQFkZ4/fw==}
cpu: [x64]
os: [linux]
libc: [musl]
'@rollup/rollup-win32-arm64-msvc@4.22.0':
resolution: {integrity: sha512-7LB+Bh+Ut7cfmO0m244/asvtIGQr5pG5Rvjz/l1Rnz1kDzM02pSX9jPaS0p+90H5I1x4d1FkCew+B7MOnoatNw==}
+1 -1
View File
@@ -71,7 +71,7 @@ class CivitaiModelSearcher(ModelSearcher):
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
# issue: https://github.com/hayden-cn/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
+1 -1
View File
@@ -126,7 +126,7 @@ def download_web_distribution(version: str):
try:
print_info(f"current version {version}, web version {web_version}")
print_info("Downloading web distribution...")
download_url = f"https://github.com/hayden-fr/ComfyUI-Model-Manager/releases/download/v{version}/dist.tar.gz"
download_url = f"https://github.com/hayden-cn/ComfyUI-Model-Manager/releases/download/v{version}/dist.tar.gz"
response = requests.get(download_url, stream=True)
response.raise_for_status()
+2 -2
View File
@@ -1,12 +1,12 @@
[project]
name = "comfyui-model-manager"
description = "Manage models: browsing, download and delete."
version = "2.8.3"
version = "2.8.4"
license = { file = "LICENSE" }
dependencies = ["markdownify"]
[project.urls]
Repository = "https://github.com/hayden-fr/ComfyUI-Model-Manager"
Repository = "https://github.com/hayden-cn/ComfyUI-Model-Manager"
# Used by Comfy Registry https://comfyregistry.org
[tool.comfy]
+42
View File
@@ -80,9 +80,30 @@ onMounted(() => {
})
}
const toggleLayout = () => {
const newValue = !config.flat.value
config.flat.value = newValue
app.ui?.settings.setSettingValue('ModelManager.UI.Flat', newValue)
dialog.closeAll()
openManagerDialog()
}
const openManagerDialog = () => {
const { cardWidth, gutter, aspect, flat } = config
const layoutIcon = flat.value ? 'pi pi-th-large' : 'pi pi-folder-open'
const includeHidden =
app.ui?.settings.getSettingValue(
'ModelManager.Scan.IncludeHiddenFiles',
) ?? false
const hiddenIcon = includeHidden ? 'pi pi-eye' : 'pi pi-eye-slash'
const hiddenTooltip = includeHidden
? t('hideHiddenFiles')
: t('showHiddenFiles')
if (firstOpenManager.value) {
models.refresh(true)
firstOpenManager.value = false
@@ -99,6 +120,27 @@ onMounted(() => {
icon: 'mdi mdi-folder-search-outline text-lg',
command: openModelScanning,
},
{
key: 'toggle-layout',
icon: layoutIcon,
command: toggleLayout,
tooltip: flat.value ? t('switchToFolderView') : t('switchToFlatView'),
},
{
key: 'toggle-hidden',
icon: hiddenIcon,
command: async () => {
const newValue = !includeHidden
app.ui?.settings.setSettingValue(
'ModelManager.Scan.IncludeHiddenFiles',
newValue,
)
await refreshModelsAndConfig() // ensure updated model list
dialog.closeAll()
openManagerDialog()
},
tooltip: hiddenTooltip,
},
{
key: 'refresh',
icon: 'pi pi-refresh',
+168 -3
View File
@@ -14,6 +14,27 @@
</template>
</ResponseInput>
<!-- Direct file URL indicator with folder selection -->
<div v-if="isDirectFile && modelUrl" class="flex flex-col gap-2">
<div
class="flex items-center gap-2 rounded bg-green-50 p-2 text-sm text-green-600"
>
<i class="pi pi-check-circle"></i>
<span>Direct file download detected</span>
</div>
<!-- Model Type/Folder Selection for direct downloads -->
<div class="flex items-center gap-2">
<label class="text-sm font-medium">{{ $t('modelType') }}:</label>
<ResponseSelect
v-model="selectedModelType"
:items="modelTypeOptions"
:type="'drop'"
class="flex-1"
/>
</div>
</div>
<div v-show="data.length > 0">
<ResponseSelect
v-model="current"
@@ -79,8 +100,13 @@ import { request } from 'hooks/request'
import { useToast } from 'hooks/toast'
import Button from 'primevue/button'
import { VersionModel, WithResolved } from 'types/typings'
import { previewUrlToFile } from 'utils/common'
import { ref } from 'vue'
import {
getFilenameFromUrl,
getModelTypeFromFilename,
isDirectFileUrl,
previewUrlToFile,
} from 'utils/common'
import { computed, ref, watch } from 'vue'
const { isMobile } = useConfig()
const { toast } = useToast()
@@ -89,14 +115,153 @@ const dialog = useDialog()
const modelUrl = ref<string>()
// Model type selection for direct downloads
const selectedModelType = ref<string>('checkpoints')
const modelTypeOptions = computed(() => [
{
label: 'Checkpoints',
value: 'checkpoints',
command: () => {
selectedModelType.value = 'checkpoints'
},
},
{
label: 'LoRA',
value: 'loras',
command: () => {
selectedModelType.value = 'loras'
},
},
{
label: 'ControlNet',
value: 'controlnet',
command: () => {
selectedModelType.value = 'controlnet'
},
},
{
label: 'VAE',
value: 'vae',
command: () => {
selectedModelType.value = 'vae'
},
},
{
label: 'Embeddings',
value: 'embeddings',
command: () => {
selectedModelType.value = 'embeddings'
},
},
{
label: 'Upscale Models',
value: 'upscale_models',
command: () => {
selectedModelType.value = 'upscale_models'
},
},
{
label: 'Diffusers',
value: 'diffusers',
command: () => {
selectedModelType.value = 'diffusers'
},
},
{
label: 'CLIP',
value: 'clip',
command: () => {
selectedModelType.value = 'clip'
},
},
{
label: 'CLIP Vision',
value: 'clip_vision',
command: () => {
selectedModelType.value = 'clip_vision'
},
},
{
label: 'UNet/Diffusion Models',
value: 'diffusion_models',
command: () => {
selectedModelType.value = 'diffusion_models'
},
},
{
label: 'Style Models',
value: 'style_models',
command: () => {
selectedModelType.value = 'style_models'
},
},
{
label: 'Hypernetworks',
value: 'hypernetworks',
command: () => {
selectedModelType.value = 'hypernetworks'
},
},
{
label: 'GLIGEN',
value: 'gligen',
command: () => {
selectedModelType.value = 'gligen'
},
},
{
label: 'PhotoMaker',
value: 'photomaker',
command: () => {
selectedModelType.value = 'photomaker'
},
},
{
label: 'VAE Approx',
value: 'vae_approx',
command: () => {
selectedModelType.value = 'vae_approx'
},
},
{
label: 'Classifiers',
value: 'classifiers',
command: () => {
selectedModelType.value = 'classifiers'
},
},
])
const isDirectFile = computed(() =>
modelUrl.value ? isDirectFileUrl(modelUrl.value) : false,
)
const { current, currentModel, data, search } = useModelSearch()
const searchModelsByUrl = async () => {
if (modelUrl.value) {
await search(modelUrl.value)
const modelType = isDirectFile.value ? selectedModelType.value : undefined
await search(modelUrl.value, modelType)
}
}
// Watch for direct file URL changes and set intelligent default
watch(modelUrl, (newUrl) => {
if (newUrl && isDirectFileUrl(newUrl)) {
const filename = getFilenameFromUrl(newUrl)
const suggestedType = getModelTypeFromFilename(filename)
selectedModelType.value = suggestedType
}
})
// Watch for model type changes on direct files and refresh the model
watch(selectedModelType, async () => {
if (isDirectFile.value && modelUrl.value) {
await search(modelUrl.value, selectedModelType.value)
}
})
const createDownTask = async (data: WithResolved<VersionModel>) => {
loading.show()
+22 -5
View File
@@ -225,16 +225,32 @@ const list = computed(() => {
return !item.isFolder
})
function buildRegex(raw: string): RegExp {
try {
// Escape regex specials, then restore * wildcards as .*
const escaped = raw
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
.replace(/\\\*/g, '.*')
return new RegExp(escaped, 'i') // case-insensitive
} catch {
return new RegExp(raw, 'i')
}
}
const filterList = pureModels.filter((model) => {
const showAllModel = currentType.value === allType
const matchType = showAllModel || model.type === currentType.value
const filter = searchContent.value?.toLowerCase() ?? ''
const matchSubFolder = model.subFolder.toLowerCase().includes(filter)
const matchName = model.basename.toLowerCase().includes(filter)
const rawFilter = searchContent.value ?? ''
const tokens = rawFilter.split(/\s+/).filter(Boolean)
const regexes = tokens.map(buildRegex)
return matchType && (matchSubFolder || matchName)
// Require every token to match either the folder or the name
const matchesAll = regexes.every(
(re) => re.test(model.subFolder) || re.test(model.basename),
)
return matchType && matchesAll
})
let sortStrategy: (a: Model, b: Model) => number = () => 0
@@ -262,6 +278,7 @@ const list = computed(() => {
})
})
const contentStyle = computed(() => ({
gridTemplateColumns: `repeat(auto-fit, ${cardSize.value.width}px)`,
gap: `${gutter}px`,
+1
View File
@@ -16,6 +16,7 @@
<Button
v-for="action in item.headerButtons"
:key="action.key"
v-tooltip.top="action.tooltip"
severity="secondary"
:text="true"
:rounded="true"
+1
View File
@@ -6,6 +6,7 @@ interface HeaderButton {
key: string
icon: string
command: () => void
tooltip?: string
}
export interface DialogItem {
+171 -3
View File
@@ -11,7 +11,12 @@ import {
VersionModel,
VersionModelFile,
} from 'types/typings'
import { bytesToSize } from 'utils/common'
import {
bytesToSize,
getFilenameFromUrl,
getModelTypeFromFilename,
isDirectFileUrl,
} from 'utils/common'
import { onBeforeMount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import yaml from 'yaml'
@@ -191,7 +196,13 @@ export const useModelSearch = () => {
const regexp = /---\n([\s\S]*?)\n---/
const yamlMetadataMatch = item.description.match(regexp)
const yamlMetadata = yaml.parse(yamlMetadataMatch?.[1] || '')
let yamlMetadata: any = {}
try {
yamlMetadata = yaml.parse(yamlMetadataMatch?.[1] || '') || {}
} catch (e) {
console.warn('Failed to parse YAML metadata:', e)
yamlMetadata = {}
}
yamlMetadata.hashes = file.hashes
yamlMetadata.metadata = file.metadata
const yamlContent = `---\n${yaml.stringify(yamlMetadata)}---`
@@ -219,12 +230,169 @@ export const useModelSearch = () => {
return fileSelectionItem
}
const handleSearchByUrl = async (url: string) => {
const createDirectFileModel = (
url: string,
modelType?: string,
): VersionModel => {
try {
const filename = getFilenameFromUrl(url)
const parts = filename.split('.')
const extension = `.${parts.pop()}`
const basename = parts.join('.') || 'model'
const detectedModelType = modelType || getModelTypeFromFilename(filename)
// Create a proper YAML metadata structure for direct files
const yamlMetadata = {
source: 'direct-link',
original_url: url,
filename: filename,
modelType: detectedModelType,
downloadPlatform: 'Direct Link',
}
const description = `---
${Object.entries(yamlMetadata)
.map(([key, value]) => `${key}: ${value}`)
.join('\n')}
---
# Direct File Download
This is a direct download link to a model file. The file size will be determined during download.
**Source:** ${url}
**Filename:** ${filename}
**Type:** ${detectedModelType}`
return {
id: `direct-${Date.now()}`,
basename,
extension,
sizeBytes: 0, // Will be determined during download
type: detectedModelType,
subFolder: '',
pathIndex: 0,
isFolder: false,
preview: '',
description,
metadata: {
source: 'direct-link',
original_url: url,
},
shortname: basename,
downloadPlatform: 'Direct Link',
downloadUrl: url,
hashes: {},
files: [
{
id: 1,
sizeKB: 0, // Unknown until download starts
name: filename,
type: 'Model',
metadata: {
source: 'direct-link',
original_url: url,
},
hashes: {},
downloadUrl: url,
},
],
}
} catch (error) {
console.error('Error creating direct file model:', error)
// Return a fallback model
const fallbackType = modelType || 'checkpoints'
return {
id: `direct-${Date.now()}`,
basename: 'model',
extension: '.bin',
sizeBytes: 0,
type: fallbackType,
subFolder: '',
pathIndex: 0,
isFolder: false,
preview: '',
description: `---
source: direct-link
original_url: ${url}
filename: model.bin
modelType: ${fallbackType}
downloadPlatform: Direct Link
---
# Direct File Download
This is a direct download link to a model file.`,
metadata: {
source: 'direct-link',
original_url: url,
},
shortname: 'model',
downloadPlatform: 'Direct Link',
downloadUrl: url,
hashes: {},
files: [
{
id: 1,
sizeKB: 0,
name: 'model.bin',
type: 'Model',
metadata: {
source: 'direct-link',
original_url: url,
},
hashes: {},
downloadUrl: url,
},
],
}
}
}
const handleSearchByUrl = async (url: string, modelType?: string) => {
if (!url) {
return Promise.resolve([])
}
loading.show()
// Check if this is a direct file URL
if (isDirectFileUrl(url)) {
try {
// Create a mock model for direct file download
const directModel = createDirectFileModel(url, modelType)
const resolvedItem = genFileSelectionItem(directModel)
data.value = [
{
label: directModel.shortname,
value: directModel.id,
item: resolvedItem,
command() {
current.value = directModel.id
},
},
]
current.value = data.value[0]?.value
currentModel.value = data.value[0]?.item
loading.hide()
return [directModel]
} catch (error) {
console.error('Error processing direct file URL:', error)
loading.hide()
toast.add({
severity: 'error',
summary: 'Error',
detail: `Failed to process direct file URL: ${error instanceof Error ? error.message : 'Unknown error'}`,
life: 5000,
})
return []
}
}
// Original logic for model page URLs
return request(`/model-info?model-page=${encodeURIComponent(url)}`, {})
.then((resData: VersionModel[]) => {
data.value = resData.map((item) => {
+7 -2
View File
@@ -9,7 +9,8 @@
"downloadTask": "Download Task",
"createDownloadTask": "Create Download Task",
"parseModelUrl": "Parse Model URL",
"pleaseInputModelUrl": "Input a URL from civitai.com or huggingface.co",
"pleaseInputModelUrl": "Input a URL from civitai.com, huggingface.co, or direct file link (.safetensors, .ckpt, etc.)",
"selectModelTypeForDirect": "Select the folder/type for this model",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
@@ -73,5 +74,9 @@
"ui": "UI",
"cardSize": "Card Size",
"useFlatUI": "Flat Layout"
}
},
"switchToFolderView": "Switch to Folder View",
"switchToFlatView": "Switch to Flat View",
"hideHiddenFiles": "Hide hidden files",
"showHiddenFiles": "Show hidden files"
}
+7 -2
View File
@@ -9,7 +9,8 @@
"downloadTask": "下载任务",
"createDownloadTask": "创建下载任务",
"parseModelUrl": "解析模型URL",
"pleaseInputModelUrl": "输入 civitai.com 或 huggingface.co 的 URL",
"pleaseInputModelUrl": "输入 civitai.com, huggingface.co 的 URL 或直接文件链接 (.safetensors, .ckpt 等)",
"selectModelTypeForDirect": "为此模型选择文件夹/类型",
"cancel": "取消",
"save": "保存",
"delete": "删除",
@@ -73,5 +74,9 @@
"ui": "外观",
"cardSize": "卡片尺寸",
"useFlatUI": "展平布局"
}
},
"switchToFolderView": "切换到文件夹视图",
"switchToFlatView": "切换到平铺视图",
"hideHiddenFiles": "隐藏隐藏文件",
"showHiddenFiles": "显示隐藏文件"
}
+100
View File
@@ -37,3 +37,103 @@ export const previewUrlToFile = async (url: string) => {
return file
})
}
// Model file extensions that are supported for direct download
export const MODEL_FILE_EXTENSIONS = [
'.safetensors',
'.ckpt',
'.pt',
'.pth',
'.bin',
'.onnx',
'.tflite',
'.pb',
'.h5',
'.pkl',
'.pickle',
'.json', // for configuration files
'.yaml',
'.yml',
]
/**
* Checks if a URL points directly to a downloadable model file
*/
export const isDirectFileUrl = (url: string): boolean => {
if (!url || typeof url !== 'string') {
return false
}
try {
const urlObj = new URL(url)
const pathname = urlObj.pathname.toLowerCase()
// Check if the URL ends with a model file extension
return MODEL_FILE_EXTENSIONS.some((ext) => pathname.endsWith(ext))
} catch {
return false
}
}
/**
* Extracts filename from a URL
*/
export const getFilenameFromUrl = (url: string): string => {
if (!url || typeof url !== 'string') {
return 'model.bin'
}
try {
const urlObj = new URL(url)
const pathname = urlObj.pathname
const filename = pathname.split('/').pop() || ''
// If no filename with extension found, generate one
if (!filename || !filename.includes('.')) {
const extension =
MODEL_FILE_EXTENSIONS.find((ext) =>
pathname.toLowerCase().endsWith(ext),
) || '.bin'
return `model${extension}`
}
return filename
} catch {
return 'model.bin'
}
}
/**
* Determines model type based on file extension (fallback only)
* Note: This is now primarily used as a fallback when no manual selection is made
*/
export const getModelTypeFromFilename = (filename: string): string => {
if (!filename || typeof filename !== 'string') {
return 'checkpoints'
}
const extension = filename.toLowerCase().split('.').pop()
switch (extension) {
case 'safetensors':
case 'ckpt':
case 'pt':
case 'pth':
return 'checkpoints' // Default for these extensions, but user can override
case 'bin':
return 'diffusers'
case 'onnx':
return 'onnx'
case 'tflite':
return 'tflite'
case 'pb':
return 'tensorflow'
case 'h5':
return 'keras'
case 'pkl':
case 'pickle':
return 'embeddings'
default:
return 'checkpoints'
}
}
File diff suppressed because one or more lines are too long