Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39e4581558 | ||
|
|
c55ee62ac9 | ||
|
|
33c43085c4 | ||
|
|
dc1d12649a | ||
|
|
0fdbc5d7be | ||
|
|
60837ae35f | ||
|
|
e88a77f224 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+3
-9
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -6,6 +6,7 @@ interface HeaderButton {
|
||||
key: string
|
||||
icon: string
|
||||
command: () => void
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export interface DialogItem {
|
||||
|
||||
+171
-3
@@ -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
@@ -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
@@ -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": "显示隐藏文件"
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user