mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 09:19:43 +00:00
[feat] Add HuggingFace model import support (#7540)
## Summary Adds HuggingFace as a model import source alongside CivitAI, with improved UX for model type selection and UTF-8 filename support. ## Changes - **Import Sources**: Implemented extensible import source handler pattern supporting both CivitAI and HuggingFace - **UTF-8 Support**: Decode URL-encoded filenames to properly display international characters (e.g., Chinese) - **UX**: Sort model types alphabetically for easier selection - **Feature Flag**: Added `huggingfaceModelImportEnabled` flag for gradual rollout - **i18n**: Use proper template parameters for localized error messages ## Technical Details - Created `ImportSourceHandler` interface for extensibility - Refactored existing CivitAI logic into handler pattern - Added URL validation per source - Filename decoding handles malformed URLs gracefully ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7540-feat-Add-HuggingFace-model-import-support-2cb6d73d3650818f966cca89244e8c36) by [Unito](https://www.unito.io) --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
committed by
GitHub
parent
176c8e110b
commit
47884c623e
@@ -4,7 +4,13 @@
|
||||
>
|
||||
<!-- Step 1: Enter URL -->
|
||||
<UploadModelUrlInput
|
||||
v-if="currentStep === 1"
|
||||
v-if="currentStep === 1 && flags.huggingfaceModelImportEnabled"
|
||||
v-model="wizardData.url"
|
||||
:error="uploadError"
|
||||
class="flex-1"
|
||||
/>
|
||||
<UploadModelUrlInputCivitai
|
||||
v-else-if="currentStep === 1"
|
||||
v-model="wizardData.url"
|
||||
:error="uploadError"
|
||||
/>
|
||||
@@ -46,14 +52,17 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import UploadModelConfirmation from '@/platform/assets/components/UploadModelConfirmation.vue'
|
||||
import UploadModelFooter from '@/platform/assets/components/UploadModelFooter.vue'
|
||||
import UploadModelProgress from '@/platform/assets/components/UploadModelProgress.vue'
|
||||
import UploadModelUrlInput from '@/platform/assets/components/UploadModelUrlInput.vue'
|
||||
import UploadModelUrlInputCivitai from '@/platform/assets/components/UploadModelUrlInputCivitai.vue'
|
||||
import { useModelTypes } from '@/platform/assets/composables/useModelTypes'
|
||||
import { useUploadModelWizard } from '@/platform/assets/composables/useUploadModelWizard'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
const dialogStore = useDialogStore()
|
||||
const { modelTypes, fetchModelTypes } = useModelTypes()
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="flex items-center gap-2 p-4 font-bold">
|
||||
<img src="/assets/images/civitai.svg" class="size-4" />
|
||||
<span>{{ $t('assetBrowser.uploadModelFromCivitai') }}</span>
|
||||
<img
|
||||
v-if="!flags.huggingfaceModelImportEnabled"
|
||||
src="/assets/images/civitai.svg"
|
||||
class="size-4"
|
||||
/>
|
||||
<span>{{ $t(titleKey) }}</span>
|
||||
<span
|
||||
class="rounded-full bg-white px-1.5 py-0 text-xxs font-inter font-semibold uppercase text-black"
|
||||
>
|
||||
@@ -9,3 +13,17 @@
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
const titleKey = computed(() => {
|
||||
return flags.huggingfaceModelImportEnabled
|
||||
? 'assetBrowser.uploadModelGeneric'
|
||||
: 'assetBrowser.uploadModelFromCivitai'
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,12 +1,34 @@
|
||||
<template>
|
||||
<div class="flex justify-end gap-2 w-full">
|
||||
<div
|
||||
v-if="currentStep === 1 && flags.huggingfaceModelImportEnabled"
|
||||
class="mr-auto flex items-center gap-2"
|
||||
>
|
||||
<i class="icon-[lucide--circle-question-mark] text-muted-foreground" />
|
||||
<Button
|
||||
variant="muted-textonly"
|
||||
size="sm"
|
||||
data-attr="upload-model-step1-help-civitai"
|
||||
@click="showCivitaiHelp = true"
|
||||
>
|
||||
{{ $t('assetBrowser.providerCivitai') }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="muted-textonly"
|
||||
size="sm"
|
||||
data-attr="upload-model-step1-help-huggingface"
|
||||
@click="showHuggingFaceHelp = true"
|
||||
>
|
||||
{{ $t('assetBrowser.providerHuggingFace') }}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
v-if="currentStep === 1"
|
||||
v-else-if="currentStep === 1"
|
||||
variant="muted-textonly"
|
||||
size="lg"
|
||||
class="mr-auto underline"
|
||||
data-attr="upload-model-step1-help-link"
|
||||
@click="showVideoHelp = true"
|
||||
@click="showCivitaiHelp = true"
|
||||
>
|
||||
<i class="icon-[lucide--circle-question-mark]" />
|
||||
<span>{{ $t('assetBrowser.uploadModelHowDoIFindThis') }}</span>
|
||||
@@ -67,10 +89,15 @@
|
||||
{{ $t('assetBrowser.finish') }}
|
||||
</Button>
|
||||
<VideoHelpDialog
|
||||
v-model="showVideoHelp"
|
||||
v-model="showCivitaiHelp"
|
||||
video-url="https://media.comfy.org/compressed_768/civitai_howto.webm"
|
||||
:aria-label="$t('assetBrowser.uploadModelHelpVideo')"
|
||||
/>
|
||||
<VideoHelpDialog
|
||||
v-model="showHuggingFaceHelp"
|
||||
video-url="https://media.comfy.org/byom/huggingfacehowto.mp4"
|
||||
:aria-label="$t('assetBrowser.uploadModelHelpVideo')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -78,9 +105,13 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import VideoHelpDialog from '@/platform/assets/components/VideoHelpDialog.vue'
|
||||
|
||||
const showVideoHelp = ref(false)
|
||||
const { flags } = useFeatureFlags()
|
||||
|
||||
const showCivitaiHelp = ref(false)
|
||||
const showHuggingFaceHelp = ref(false)
|
||||
|
||||
defineProps<{
|
||||
currentStep: number
|
||||
|
||||
@@ -1,28 +1,74 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 text-sm text-muted-foreground">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">
|
||||
{{ $t('assetBrowser.uploadModelDescription1') }}
|
||||
</p>
|
||||
<ul class="list-disc space-y-1 pl-5 mt-0">
|
||||
<li v-html="$t('assetBrowser.uploadModelDescription2')" />
|
||||
<li v-html="$t('assetBrowser.uploadModelDescription3')" />
|
||||
</ul>
|
||||
<div class="flex flex-col justify-between h-full gap-6 text-sm">
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0 text-foreground">
|
||||
{{ $t('assetBrowser.uploadModelDescription1Generic') }}
|
||||
</p>
|
||||
<div class="m-0">
|
||||
<p class="m-0 text-muted-foreground">
|
||||
{{ $t('assetBrowser.uploadModelDescription2Generic') }}
|
||||
</p>
|
||||
<span class="inline-flex items-center gap-1 flex-wrap mt-2">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<img
|
||||
:src="civitaiIcon"
|
||||
:alt="$t('assetBrowser.providerCivitai')"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<a
|
||||
:href="civitaiUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-muted underline"
|
||||
>
|
||||
{{ $t('assetBrowser.providerCivitai') }}</a
|
||||
><span>,</span>
|
||||
</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<img
|
||||
:src="huggingFaceIcon"
|
||||
:alt="$t('assetBrowser.providerHuggingFace')"
|
||||
class="w-4 h-4"
|
||||
/>
|
||||
<a
|
||||
:href="huggingFaceUrl"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-muted underline"
|
||||
>
|
||||
{{ $t('assetBrowser.providerHuggingFace') }}
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<InputText
|
||||
v-model="url"
|
||||
autofocus
|
||||
:placeholder="$t('assetBrowser.genericLinkPlaceholder')"
|
||||
class="w-full bg-secondary-background border-0 p-4"
|
||||
data-attr="upload-model-step1-url-input"
|
||||
/>
|
||||
<p v-if="error" class="text-xs text-error">
|
||||
{{ error }}
|
||||
</p>
|
||||
<p v-else class="text-foreground">
|
||||
<i18n-t keypath="assetBrowser.maxFileSize" tag="span">
|
||||
<template #size>
|
||||
<span class="font-bold italic">{{
|
||||
$t('assetBrowser.maxFileSizeValue')
|
||||
}}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="mb-0" v-html="$t('assetBrowser.civitaiLinkLabel')"> </label>
|
||||
<InputText
|
||||
v-model="url"
|
||||
autofocus
|
||||
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
|
||||
class="w-full bg-secondary-background border-0 p-4"
|
||||
data-attr="upload-model-step1-url-input"
|
||||
/>
|
||||
<p v-if="error" class="text-xs text-error">
|
||||
{{ error }}
|
||||
</p>
|
||||
<p v-else v-html="$t('assetBrowser.civitaiLinkExample')"></p>
|
||||
<div class="text-sm text-muted">
|
||||
{{ $t('assetBrowser.uploadModelHelpFooterText') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,4 +90,9 @@ const url = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value: string) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const civitaiIcon = '/assets/images/civitai.svg'
|
||||
const civitaiUrl = 'https://civitai.com/models'
|
||||
const huggingFaceIcon = '/assets/images/hf-logo.svg'
|
||||
const huggingFaceUrl = 'https://huggingface.co'
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-6 text-sm text-muted-foreground">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="m-0">
|
||||
{{ $t('assetBrowser.uploadModelDescription1') }}
|
||||
</p>
|
||||
<ul class="list-disc space-y-1 pl-5 mt-0">
|
||||
<li>
|
||||
<i18n-t keypath="assetBrowser.uploadModelDescription2" tag="span">
|
||||
<template #link>
|
||||
<a
|
||||
href="https://civitai.com/models"
|
||||
target="_blank"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ $t('assetBrowser.uploadModelDescription2Link') }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
<li>
|
||||
<i18n-t keypath="assetBrowser.uploadModelDescription3" tag="span">
|
||||
<template #size>
|
||||
<span class="font-bold italic">{{
|
||||
$t('assetBrowser.maxFileSizeValue')
|
||||
}}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<i18n-t keypath="assetBrowser.civitaiLinkLabel" tag="label" class="mb-0">
|
||||
<template #download>
|
||||
<span class="font-bold italic">{{
|
||||
$t('assetBrowser.civitaiLinkLabelDownload')
|
||||
}}</span>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<InputText
|
||||
v-model="url"
|
||||
autofocus
|
||||
:placeholder="$t('assetBrowser.civitaiLinkPlaceholder')"
|
||||
class="w-full bg-secondary-background border-0 p-4"
|
||||
data-attr="upload-model-step1-url-input"
|
||||
/>
|
||||
<p v-if="error" class="text-xs text-error">
|
||||
{{ error }}
|
||||
</p>
|
||||
<i18n-t
|
||||
v-else
|
||||
keypath="assetBrowser.civitaiLinkExample"
|
||||
tag="p"
|
||||
class="text-xs"
|
||||
>
|
||||
<template #example>
|
||||
<strong>{{ $t('assetBrowser.civitaiLinkExampleStrong') }}</strong>
|
||||
</template>
|
||||
<template #link>
|
||||
<a
|
||||
href="https://civitai.com/models/10706/luisap-z-image-and-qwen-pixel-art-refiner?modelVersionId=2225295"
|
||||
target="_blank"
|
||||
class="text-muted-foreground"
|
||||
>
|
||||
{{ $t('assetBrowser.civitaiLinkExampleUrl') }}
|
||||
</a>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import InputText from 'primevue/inputtext'
|
||||
|
||||
defineProps<{
|
||||
error?: string
|
||||
}>()
|
||||
|
||||
const url = defineModel<string>({ required: true })
|
||||
</script>
|
||||
Reference in New Issue
Block a user