From 8b344ea39f0f65595fc94679de715af2bd2b70de Mon Sep 17 00:00:00 2001 From: Will Miao Date: Wed, 1 Jul 2026 08:38:16 +0800 Subject: [PATCH] feat(ui): add View on Hugging Face button, plumb hf_url through full cache pipeline --- locales/de.json | 3 +++ locales/en.json | 3 +++ locales/es.json | 3 +++ locales/fr.json | 3 +++ locales/he.json | 3 +++ locales/ja.json | 3 +++ locales/ko.json | 3 +++ locales/ru.json | 3 +++ locales/zh-CN.json | 3 +++ locales/zh-TW.json | 3 +++ py/routes/handlers/hf_handlers.py | 2 +- py/services/checkpoint_service.py | 1 + py/services/embedding_service.py | 1 + py/services/lora_service.py | 1 + py/services/model_scanner.py | 1 + py/services/persistent_model_cache.py | 5 +++++ static/js/components/shared/ModelCard.js | 14 +++++++++++--- static/js/components/shared/ModelModal.js | 13 +++++++++++++ static/js/utils/uiHelpers.js | 9 +++++++++ 19 files changed, 73 insertions(+), 4 deletions(-) diff --git a/locales/de.json b/locales/de.json index 0dd96faf..a485ffc7 100644 --- a/locales/de.json +++ b/locales/de.json @@ -105,6 +105,7 @@ "removeFromFavorites": "Aus Favoriten entfernen", "viewOnCivitai": "Auf Civitai anzeigen", "notAvailableFromCivitai": "Nicht auf Civitai verfügbar", + "viewOnHuggingFace": "Auf Hugging Face ansehen", "sendToWorkflow": "An ComfyUI senden (Klick: Anhängen, Shift+Klick: Ersetzen)", "copyLoRASyntax": "LoRA-Syntax kopieren", "checkpointNameCopied": "Checkpoint-Name kopiert", @@ -1319,6 +1320,8 @@ "editVersionName": "Versionsname bearbeiten", "viewOnCivitai": "Auf Civitai anzeigen", "viewOnCivitaiText": "Auf Civitai anzeigen", + "viewOnHuggingFace": "Auf Hugging Face ansehen", + "viewOnHuggingFaceText": "Auf Hugging Face ansehen", "viewCreatorProfile": "Ersteller-Profil anzeigen", "openFileLocation": "Dateispeicherort öffnen", "sendToWorkflow": "An ComfyUI senden", diff --git a/locales/en.json b/locales/en.json index 6b481e0f..8f956459 100644 --- a/locales/en.json +++ b/locales/en.json @@ -105,6 +105,7 @@ "removeFromFavorites": "Remove from favorites", "viewOnCivitai": "View on Civitai", "notAvailableFromCivitai": "Not available from Civitai", + "viewOnHuggingFace": "View on Hugging Face", "sendToWorkflow": "Send to ComfyUI (Click: Append, Shift+Click: Replace)", "copyLoRASyntax": "Copy LoRA Syntax", "checkpointNameCopied": "Checkpoint name copied", @@ -1319,6 +1320,8 @@ "editVersionName": "Edit version name", "viewOnCivitai": "View on Civitai", "viewOnCivitaiText": "View on Civitai", + "viewOnHuggingFace": "View on Hugging Face", + "viewOnHuggingFaceText": "View on Hugging Face", "viewCreatorProfile": "View Creator Profile", "openFileLocation": "Open File Location", "sendToWorkflow": "Send to ComfyUI", diff --git a/locales/es.json b/locales/es.json index c2587398..ae764190 100644 --- a/locales/es.json +++ b/locales/es.json @@ -105,6 +105,7 @@ "removeFromFavorites": "Eliminar de favoritos", "viewOnCivitai": "Ver en Civitai", "notAvailableFromCivitai": "No disponible en Civitai", + "viewOnHuggingFace": "Ver en Hugging Face", "sendToWorkflow": "Enviar a ComfyUI (Clic: Añadir, Shift+Clic: Reemplazar)", "copyLoRASyntax": "Copiar sintaxis de LoRA", "checkpointNameCopied": "Nombre del checkpoint copiado", @@ -1319,6 +1320,8 @@ "editVersionName": "Editar nombre de versión", "viewOnCivitai": "Ver en Civitai", "viewOnCivitaiText": "Ver en Civitai", + "viewOnHuggingFace": "Ver en Hugging Face", + "viewOnHuggingFaceText": "Ver en Hugging Face", "viewCreatorProfile": "Ver perfil del creador", "openFileLocation": "Abrir ubicación del archivo", "sendToWorkflow": "Enviar a ComfyUI", diff --git a/locales/fr.json b/locales/fr.json index e461f4fb..fa737d99 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -105,6 +105,7 @@ "removeFromFavorites": "Retirer des favoris", "viewOnCivitai": "Voir sur Civitai", "notAvailableFromCivitai": "Non disponible sur Civitai", + "viewOnHuggingFace": "Voir sur Hugging Face", "sendToWorkflow": "Envoyer vers ComfyUI (Clic: Ajouter, Maj+Clic: Remplacer)", "copyLoRASyntax": "Copier la syntaxe LoRA", "checkpointNameCopied": "Nom du checkpoint copié", @@ -1319,6 +1320,8 @@ "editVersionName": "Modifier le nom de la version", "viewOnCivitai": "Voir sur Civitai", "viewOnCivitaiText": "Voir sur Civitai", + "viewOnHuggingFace": "Voir sur Hugging Face", + "viewOnHuggingFaceText": "Voir sur Hugging Face", "viewCreatorProfile": "Voir le profil du créateur", "openFileLocation": "Ouvrir l'emplacement du fichier", "sendToWorkflow": "Envoyer vers ComfyUI", diff --git a/locales/he.json b/locales/he.json index 584f7ecd..b1614da6 100644 --- a/locales/he.json +++ b/locales/he.json @@ -105,6 +105,7 @@ "removeFromFavorites": "הסר מהמועדפים", "viewOnCivitai": "הצג ב-Civitai", "notAvailableFromCivitai": "לא זמין מ-Civitai", + "viewOnHuggingFace": "צפייה ב-Hugging Face", "sendToWorkflow": "שלח ל-ComfyUI (לחיצה: הוסף, Shift+לחיצה: החלף)", "copyLoRASyntax": "העתק תחביר LoRA", "checkpointNameCopied": "שם Checkpoint הועתק", @@ -1319,6 +1320,8 @@ "editVersionName": "ערוך שם גרסה", "viewOnCivitai": "הצג ב-Civitai", "viewOnCivitaiText": "הצג ב-Civitai", + "viewOnHuggingFace": "צפייה ב-Hugging Face", + "viewOnHuggingFaceText": "צפייה ב-Hugging Face", "viewCreatorProfile": "הצג פרופיל יוצר", "openFileLocation": "פתח מיקום קובץ", "sendToWorkflow": "שלח ל-ComfyUI", diff --git a/locales/ja.json b/locales/ja.json index 8a44591b..c1e547e9 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -105,6 +105,7 @@ "removeFromFavorites": "お気に入りから削除", "viewOnCivitai": "Civitaiで表示", "notAvailableFromCivitai": "Civitaiでは利用できません", + "viewOnHuggingFace": "Hugging Face で見る", "sendToWorkflow": "ComfyUIに送信(クリック:追加、Shift+クリック:置換)", "copyLoRASyntax": "LoRA構文をコピー", "checkpointNameCopied": "checkpointの名前をコピーしました", @@ -1319,6 +1320,8 @@ "editVersionName": "バージョン名を編集", "viewOnCivitai": "Civitaiで表示", "viewOnCivitaiText": "Civitaiで表示", + "viewOnHuggingFace": "Hugging Face で見る", + "viewOnHuggingFaceText": "Hugging Face で見る", "viewCreatorProfile": "作成者プロフィールを表示", "openFileLocation": "ファイルの場所を開く", "sendToWorkflow": "ComfyUI に送信", diff --git a/locales/ko.json b/locales/ko.json index 293c3e21..c8074eb3 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -105,6 +105,7 @@ "removeFromFavorites": "즐겨찾기에서 제거", "viewOnCivitai": "Civitai에서 보기", "notAvailableFromCivitai": "Civitai에서 사용할 수 없음", + "viewOnHuggingFace": "Hugging Face에서 보기", "sendToWorkflow": "ComfyUI로 전송 (클릭: 추가, Shift+클릭: 교체)", "copyLoRASyntax": "LoRA 문법 복사", "checkpointNameCopied": "Checkpoint 이름 복사됨", @@ -1319,6 +1320,8 @@ "editVersionName": "버전명 편집", "viewOnCivitai": "Civitai에서 보기", "viewOnCivitaiText": "Civitai에서 보기", + "viewOnHuggingFace": "Hugging Face에서 보기", + "viewOnHuggingFaceText": "Hugging Face에서 보기", "viewCreatorProfile": "제작자 프로필 보기", "openFileLocation": "파일 위치 열기", "sendToWorkflow": "ComfyUI로 보내기", diff --git a/locales/ru.json b/locales/ru.json index 4d023ac9..06d2ceef 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -105,6 +105,7 @@ "removeFromFavorites": "Удалить из избранного", "viewOnCivitai": "Посмотреть на Civitai", "notAvailableFromCivitai": "Недоступно на Civitai", + "viewOnHuggingFace": "Открыть Hugging Face", "sendToWorkflow": "Отправить в ComfyUI (Клик: Добавить, Shift+Клик: Заменить)", "copyLoRASyntax": "Копировать синтаксис LoRA", "checkpointNameCopied": "Имя checkpoint скопировано", @@ -1319,6 +1320,8 @@ "editVersionName": "Редактировать название версии", "viewOnCivitai": "Посмотреть на Civitai", "viewOnCivitaiText": "Посмотреть на Civitai", + "viewOnHuggingFace": "Открыть Hugging Face", + "viewOnHuggingFaceText": "Открыть Hugging Face", "viewCreatorProfile": "Посмотреть профиль создателя", "openFileLocation": "Открыть расположение файла", "sendToWorkflow": "Отправить в ComfyUI", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 9ccc1bb4..b70e116b 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -105,6 +105,7 @@ "removeFromFavorites": "从收藏移除", "viewOnCivitai": "在 Civitai 查看", "notAvailableFromCivitai": "Civitai 上不可用", + "viewOnHuggingFace": "在 Hugging Face 查看", "sendToWorkflow": "发送到 ComfyUI(点击:追加,Shift+点击:替换)", "copyLoRASyntax": "复制 LoRA 语法", "checkpointNameCopied": "检查点名称已复制", @@ -1319,6 +1320,8 @@ "editVersionName": "编辑版本名称", "viewOnCivitai": "在 Civitai 查看", "viewOnCivitaiText": "在 Civitai 查看", + "viewOnHuggingFace": "在 Hugging Face 查看", + "viewOnHuggingFaceText": "在 Hugging Face 查看", "viewCreatorProfile": "查看创作者主页", "openFileLocation": "打开文件位置", "sendToWorkflow": "发送到 ComfyUI", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index ade31976..5c60fa58 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -105,6 +105,7 @@ "removeFromFavorites": "移除收藏", "viewOnCivitai": "在 Civitai 查看", "notAvailableFromCivitai": "Civitai 不提供", + "viewOnHuggingFace": "在 Hugging Face 查看", "sendToWorkflow": "傳送到 ComfyUI(點擊:附加,Shift+點擊:取代)", "copyLoRASyntax": "複製 LoRA 語法", "checkpointNameCopied": "Checkpoint 名稱已複製", @@ -1319,6 +1320,8 @@ "editVersionName": "編輯版本名稱", "viewOnCivitai": "在 Civitai 查看", "viewOnCivitaiText": "在 Civitai 查看", + "viewOnHuggingFace": "在 Hugging Face 查看", + "viewOnHuggingFaceText": "在 Hugging Face 查看", "viewCreatorProfile": "查看創作者個人檔案", "openFileLocation": "開啟檔案位置", "sendToWorkflow": "傳送到 ComfyUI", diff --git a/py/routes/handlers/hf_handlers.py b/py/routes/handlers/hf_handlers.py index 915037c3..e392ccbc 100644 --- a/py/routes/handlers/hf_handlers.py +++ b/py/routes/handlers/hf_handlers.py @@ -112,7 +112,7 @@ async def _save_hf_metadata(dest_path: str, repo: str, model_root: str) -> None: # 2. Overlay HF-specific fields metadata._unknown_fields["hf_url"] = hf_url - metadata.from_civitai = True # leave default, don't interfere with CivitAI fetch + metadata.from_civitai = False # HF models are not from CivitAI # 3. Save metadata atomically await MetadataManager.save_metadata(dest_path, metadata) diff --git a/py/services/checkpoint_service.py b/py/services/checkpoint_service.py index 4c171669..0fd3f257 100644 --- a/py/services/checkpoint_service.py +++ b/py/services/checkpoint_service.py @@ -66,6 +66,7 @@ class CheckpointService(BaseModelService): "civitai": self.filter_civitai_data(checkpoint_data.get("civitai", {}), minimal=True), "auto_tags": checkpoint_data.get("auto_tags") or extract_auto_tags(checkpoint_data), "version_count": checkpoint_data.get("version_count"), + "hf_url": checkpoint_data.get("hf_url", ""), } def find_duplicate_hashes(self) -> Dict: diff --git a/py/services/embedding_service.py b/py/services/embedding_service.py index 85666f17..f668ec8b 100644 --- a/py/services/embedding_service.py +++ b/py/services/embedding_service.py @@ -66,6 +66,7 @@ class EmbeddingService(BaseModelService): "civitai": self.filter_civitai_data(embedding_data.get("civitai", {}), minimal=True), "auto_tags": embedding_data.get("auto_tags") or extract_auto_tags(embedding_data), "version_count": embedding_data.get("version_count"), + "hf_url": embedding_data.get("hf_url", ""), } def find_duplicate_hashes(self) -> Dict: diff --git a/py/services/lora_service.py b/py/services/lora_service.py index 7d99d245..eadbf951 100644 --- a/py/services/lora_service.py +++ b/py/services/lora_service.py @@ -78,6 +78,7 @@ class LoraService(BaseModelService): ), "auto_tags": lora_data.get("auto_tags") or extract_auto_tags(lora_data), "version_count": lora_data.get("version_count"), + "hf_url": lora_data.get("hf_url", ""), } async def _apply_specific_filters(self, data: List[Dict], **kwargs) -> List[Dict]: diff --git a/py/services/model_scanner.py b/py/services/model_scanner.py index 576da54b..2dd3219b 100644 --- a/py/services/model_scanner.py +++ b/py/services/model_scanner.py @@ -248,6 +248,7 @@ class ModelScanner: 'civitai': civitai_slim, 'civitai_deleted': bool(get_value('civitai_deleted', False)), 'skip_metadata_refresh': bool(get_value('skip_metadata_refresh', False)), + 'hf_url': get_value('hf_url', '') or '', } license_source: Dict[str, Any] = {} diff --git a/py/services/persistent_model_cache.py b/py/services/persistent_model_cache.py index 0f4e84f0..1a800591 100644 --- a/py/services/persistent_model_cache.py +++ b/py/services/persistent_model_cache.py @@ -57,6 +57,7 @@ class PersistentModelCache: "db_checked", "last_checked_at", "hash_status", + "hf_url", ) _MODEL_UPDATE_COLUMNS: Tuple[str, ...] = _MODEL_COLUMNS[2:] _instances: Dict[str, "PersistentModelCache"] = {} @@ -188,6 +189,7 @@ class PersistentModelCache: "skip_metadata_refresh": bool(row["skip_metadata_refresh"]), "license_flags": int(license_value), "hash_status": row["hash_status"] or "completed", + "hf_url": row["hf_url"] or "", } raw_data.append(item) @@ -452,6 +454,7 @@ class PersistentModelCache: db_checked INTEGER, last_checked_at REAL, hash_status TEXT, + hf_url TEXT DEFAULT '', PRIMARY KEY (model_type, file_path) ); @@ -500,6 +503,7 @@ class PersistentModelCache: # Persisting without explicit flags should assume CivitAI's documented defaults (0b111001 == 57). "license_flags": f"INTEGER DEFAULT {DEFAULT_LICENSE_FLAGS}", "hash_status": "TEXT DEFAULT 'completed'", + "hf_url": "TEXT DEFAULT ''", } for column, definition in required_columns.items(): @@ -575,6 +579,7 @@ class PersistentModelCache: 1 if item.get("db_checked") else 0, float(item.get("last_checked_at") or 0.0), item.get("hash_status", "completed"), + item.get("hf_url") or "", ) def _insert_model_sql(self) -> str: diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index b2bda7ce..051e701a 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -1,4 +1,4 @@ -import { showToast, openCivitai, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, sendEmbeddingToWorkflow, openExampleImagesFolder, buildLoraSyntax, sendModelPathToWorkflow } from '../../utils/uiHelpers.js'; +import { showToast, openCivitai, openHuggingFace, copyToClipboard, copyLoraSyntax, sendLoraToWorkflow, sendEmbeddingToWorkflow, openExampleImagesFolder, buildLoraSyntax, sendModelPathToWorkflow } from '../../utils/uiHelpers.js'; import { state, getCurrentPageState } from '../../state/index.js'; import { showModelModal } from './ModelModal.js'; import { toggleShowcase } from './showcase/ShowcaseView.js'; @@ -66,6 +66,8 @@ function handleModelCardEvent_internal(event, modelType) { event.stopPropagation(); if (card.dataset.from_civitai === 'true') { openCivitai(card.dataset.filepath); + } else if (card.dataset.hf_url) { + openHuggingFace(card.dataset.hf_url); } return true; // Stop propagation } @@ -313,6 +315,7 @@ async function showModelModalFromCard(card, modelType) { modified: card.dataset.modified, file_size: parseInt(card.dataset.file_size || '0'), from_civitai: card.dataset.from_civitai === 'true', + hf_url: card.dataset.hf_url || '', base_model: card.dataset.base_model, notes: card.dataset.notes || '', favorite: card.dataset.favorite === 'true', @@ -401,6 +404,7 @@ function showExampleAccessModal(card, modelType) { modified: card.dataset.modified, file_size: card.dataset.file_size, from_civitai: card.dataset.from_civitai === 'true', + hf_url: card.dataset.hf_url || '', base_model: card.dataset.base_model, notes: card.dataset.notes, favorite: card.dataset.favorite === 'true', @@ -467,6 +471,7 @@ export function createModelCard(model, modelType) { card.dataset.base_model = model.base_model || 'Unknown'; card.dataset.favorite = model.favorite ? 'true' : 'false'; card.dataset.exclude = model.exclude ? 'true' : 'false'; + card.dataset.hf_url = model.hf_url || ''; const hasUpdateAvailable = Boolean(model.update_available); card.dataset.update_available = hasUpdateAvailable ? 'true' : 'false'; card.dataset.skip_metadata_refresh = model.skip_metadata_refresh ? 'true' : 'false'; @@ -578,7 +583,10 @@ export function createModelCard(model, modelType) { translate('modelCard.actions.addToFavorites', {}, 'Add to favorites'); const globeTitle = model.from_civitai ? translate('modelCard.actions.viewOnCivitai', {}, 'View on Civitai') : - translate('modelCard.actions.notAvailableFromCivitai', {}, 'Not available from Civitai'); + model.hf_url ? + translate('modelCard.actions.viewOnHuggingFace', {}, 'View on Hugging Face') : + translate('modelCard.actions.notAvailableFromCivitai', {}, 'Not available from Civitai'); + const globeEnabled = model.from_civitai || !!model.hf_url; let sendTitle; let copyTitle; if (modelType === MODEL_TYPES.LORA) { @@ -603,7 +611,7 @@ export function createModelCard(model, modelType) { + ${!globeEnabled ? 'style="opacity: 0.5; cursor: not-allowed"' : ''}> diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index f530a09a..4e333aa0 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -360,6 +360,11 @@ export async function showModelModal(model, modelType) { const viewOnCivitaiAction = modelWithFullData.from_civitai ? `
${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')} +
`.trim() : ''; + const escapedHfUrl = modelWithFullData.hf_url ? escapeAttribute(modelWithFullData.hf_url) : ''; + const viewOnHuggingFaceAction = escapedHfUrl ? ` +
+ ${translate('modals.model.actions.viewOnHuggingFaceText', {}, 'View on Hugging Face')}
`.trim() : ''; const creatorInfoAction = modelWithFullData.civitai?.creator ? `
@@ -377,6 +382,9 @@ export async function showModelModal(model, modelType) { if (viewOnCivitaiAction) { creatorActionItems.push(indentMarkup(viewOnCivitaiAction, 24)); } + if (viewOnHuggingFaceAction) { + creatorActionItems.push(indentMarkup(viewOnHuggingFaceAction, 24)); + } if (creatorInfoAction) { creatorActionItems.push(indentMarkup(creatorInfoAction, 24)); } @@ -869,6 +877,11 @@ function setupEventHandlers(filePath, modelType) { case 'view-civitai': openCivitai(target.dataset.filepath); break; + case 'view-huggingface': + if (target.dataset.hfUrl) { + window.open(target.dataset.hfUrl, '_blank', 'noopener,noreferrer'); + } + break; case 'view-creator': const username = target.dataset.username; if (username) { diff --git a/static/js/utils/uiHelpers.js b/static/js/utils/uiHelpers.js index 961d1042..4df7bf56 100644 --- a/static/js/utils/uiHelpers.js +++ b/static/js/utils/uiHelpers.js @@ -319,6 +319,15 @@ export function openCivitai(filePath) { openCivitaiByMetadata(civitaiId, versionId, modelName); } +/** + * Open a Hugging Face model page in a new tab + * @param {string} hfUrl - The Hugging Face URL + */ +export function openHuggingFace(hfUrl) { + if (!hfUrl) return; + window.open(hfUrl, '_blank', 'noopener,noreferrer'); +} + /** * Dynamically positions the search options panel and filter panel * based on the current layout and folder tags container height