mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-07-13 20:21:16 -03:00
feat(hf): add Link to HuggingFace feature with unified Link Model submenu
- Merge Relink to Civitai and new Link to HuggingFace into a single 'Link Model' submenu with sub-options for each source - Add POST /api/lm/set-hf-url endpoint to associate a model with a HuggingFace repo URL, saving hf_url to .metadata.json - Add link_hf_modal.html for URL input, following relink-civitai pattern - Use update_single_model_cache instead of add_model_to_cache to prevent duplicate cache entries after linking - Remove os.path.realpath usage for consistency with relink-civitai - Raise errors instead of silently falling back to LoRA scanner when model root cannot be determined - Scope .input-group CSS rules to modal IDs to fix style conflicts with download-modal.css - Add i18n keys across all 10 locales with translations for zh-CN, zh-TW, ja, ko, de, es, fr, he, ru
This commit is contained in:
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitai-Daten aktualisieren",
|
||||
"checkUpdates": "Updates prüfen",
|
||||
"relinkCivitai": "Mit Civitai neu verknüpfen",
|
||||
"linkModel": "Modell verknüpfen",
|
||||
"linkCivitai": "Mit Civitai neu verknüpfen",
|
||||
"linkHuggingFace": "Mit HuggingFace verknüpfen",
|
||||
"copySyntax": "LoRA-Syntax kopieren",
|
||||
"copyFilename": "Modell-Dateiname kopieren",
|
||||
"copyRecipeSyntax": "Rezept-Syntax kopieren",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "Ordnerpfad eingeben oder aus Baum unten auswählen...",
|
||||
"root": "Stammverzeichnis"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "Mit HuggingFace verknüpfen",
|
||||
"infoText": "Fügen Sie die HuggingFace-Repository-URL ein, um dieses Modell zuzuordnen. Dies ermöglicht die KI-gestützte Metadatenanreicherung.",
|
||||
"urlLabel": "HuggingFace-Repository-URL:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "Geben Sie die vollständige URL des HuggingFace-Repositorys ein.",
|
||||
"confirmAction": "Speichern & Verknüpfen"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "Mit Civitai neu verknüpfen",
|
||||
"warning": "Warnung:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "Fehler beim Setzen der Inhaltsbewertung: {message}",
|
||||
"relinkSuccess": "Modell erfolgreich mit Civitai neu verknüpft",
|
||||
"relinkFailed": "Fehler: {message}",
|
||||
"linkHfSuccess": "Modell erfolgreich mit HuggingFace verknüpft",
|
||||
"linkHfFailed": "Fehler: {message}",
|
||||
"fetchMetadataFirst": "Bitte rufen Sie zuerst Metadaten von CivitAI ab",
|
||||
"noCivitaiInfo": "Keine CivitAI-Informationen verfügbar",
|
||||
"missingHash": "Modell-Hash nicht verfügbar"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Refresh Civitai Data",
|
||||
"checkUpdates": "Check Updates",
|
||||
"relinkCivitai": "Re-link to Civitai",
|
||||
"linkModel": "Link Model",
|
||||
"linkCivitai": "Link to Civitai",
|
||||
"linkHuggingFace": "Link to HuggingFace",
|
||||
"copySyntax": "Copy LoRA Syntax",
|
||||
"copyFilename": "Copy Model Filename",
|
||||
"copyRecipeSyntax": "Copy Recipe Syntax",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "Type folder path or select from tree below...",
|
||||
"root": "Root"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "Link to HuggingFace",
|
||||
"infoText": "Paste the HuggingFace repository URL to associate this model with its source. This enables AI-powered metadata enrichment.",
|
||||
"urlLabel": "HuggingFace Repository URL:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "Enter the full URL of the HuggingFace repository.",
|
||||
"confirmAction": "Save & Link"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "Re-link to Civitai",
|
||||
"warning": "Warning:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "Failed to set content rating: {message}",
|
||||
"relinkSuccess": "Model successfully re-linked to Civitai",
|
||||
"relinkFailed": "Error: {message}",
|
||||
"linkHfSuccess": "Model successfully linked to HuggingFace",
|
||||
"linkHfFailed": "Error: {message}",
|
||||
"fetchMetadataFirst": "Please fetch metadata from CivitAI first",
|
||||
"noCivitaiInfo": "No CivitAI information available",
|
||||
"missingHash": "Model hash not available"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Actualizar datos de Civitai",
|
||||
"checkUpdates": "Comprobar actualizaciones",
|
||||
"relinkCivitai": "Re-vincular a Civitai",
|
||||
"linkModel": "Vincular modelo",
|
||||
"linkCivitai": "Re-vincular a Civitai",
|
||||
"linkHuggingFace": "Vincular a HuggingFace",
|
||||
"copySyntax": "Copiar sintaxis de LoRA",
|
||||
"copyFilename": "Copiar nombre de archivo del modelo",
|
||||
"copyRecipeSyntax": "Copiar sintaxis de receta",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "Escribe la ruta de la carpeta o selecciona del árbol de abajo...",
|
||||
"root": "Raíz"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "Vincular a HuggingFace",
|
||||
"infoText": "Pegue la URL del repositorio de HuggingFace para asociar este modelo. Esto permite el enriquecimiento de metadatos con IA.",
|
||||
"urlLabel": "URL del repositorio de HuggingFace:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "Ingrese la URL completa del repositorio de HuggingFace.",
|
||||
"confirmAction": "Guardar y vincular"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "Re-vincular a Civitai",
|
||||
"warning": "Advertencia:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "Error al establecer clasificación de contenido: {message}",
|
||||
"relinkSuccess": "Modelo re-vinculado exitosamente a Civitai",
|
||||
"relinkFailed": "Error: {message}",
|
||||
"linkHfSuccess": "Modelo vinculado a HuggingFace exitosamente",
|
||||
"linkHfFailed": "Error: {message}",
|
||||
"fetchMetadataFirst": "Por favor obtén metadatos de CivitAI primero",
|
||||
"noCivitaiInfo": "No hay información de CivitAI disponible",
|
||||
"missingHash": "Hash del modelo no disponible"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Actualiser les données Civitai",
|
||||
"checkUpdates": "Vérifier les mises à jour",
|
||||
"relinkCivitai": "Relier à nouveau à Civitai",
|
||||
"linkModel": "Lier le modèle",
|
||||
"linkCivitai": "Relier à nouveau à Civitai",
|
||||
"linkHuggingFace": "Lier à HuggingFace",
|
||||
"copySyntax": "Copier la syntaxe LoRA",
|
||||
"copyFilename": "Copier le nom de fichier du modèle",
|
||||
"copyRecipeSyntax": "Copier la syntaxe de la recipe",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "Tapez le chemin du dossier ou sélectionnez dans l'arbre ci-dessous...",
|
||||
"root": "Racine"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "Lier à HuggingFace",
|
||||
"infoText": "Collez l'URL du dépôt HuggingFace pour associer ce modèle à sa source. Cela permet l'enrichissement des métadonnées par IA.",
|
||||
"urlLabel": "URL du dépôt HuggingFace :",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "Entrez l'URL complète du dépôt HuggingFace.",
|
||||
"confirmAction": "Enregistrer & lier"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "Relier à nouveau à Civitai",
|
||||
"warning": "Attention :",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "Échec de la définition de la classification du contenu : {message}",
|
||||
"relinkSuccess": "Modèle relié à Civitai avec succès",
|
||||
"relinkFailed": "Erreur : {message}",
|
||||
"linkHfSuccess": "Modèle lié à HuggingFace avec succès",
|
||||
"linkHfFailed": "Erreur : {message}",
|
||||
"fetchMetadataFirst": "Veuillez d'abord récupérer les métadonnées depuis CivitAI",
|
||||
"noCivitaiInfo": "Aucune information CivitAI disponible",
|
||||
"missingHash": "Hash du modèle non disponible"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "רענן נתוני Civitai",
|
||||
"checkUpdates": "בדוק עדכונים",
|
||||
"relinkCivitai": "קשר מחדש ל-Civitai",
|
||||
"linkModel": "קישור מודל",
|
||||
"linkCivitai": "קשר מחדש ל-Civitai",
|
||||
"linkHuggingFace": "קישור ל-HuggingFace",
|
||||
"copySyntax": "העתק תחביר LoRA",
|
||||
"copyFilename": "העתק שם קובץ מודל",
|
||||
"copyRecipeSyntax": "העתק תחביר מתכון",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "הקלד נתיב תיקייה או בחר מהעץ למטה...",
|
||||
"root": "שורש"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "קישור ל-HuggingFace",
|
||||
"infoText": "הדבק את כתובת ה-URL של מאגר HuggingFace כדי לשייך מודל זה למקורו. פעולה זו מאפשרת העשרת מטא-דאטה באמצעות AI.",
|
||||
"urlLabel": "כתובת URL של מאגר HuggingFace:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "הזן את כתובת ה-URL המלאה של מאגר HuggingFace.",
|
||||
"confirmAction": "שמור וקשר"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "קשר מחדש ל-Civitai",
|
||||
"warning": "אזהרה:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "הגדרת דירוג התוכן נכשלה: {message}",
|
||||
"relinkSuccess": "המודל קושר מחדש ל-Civitai בהצלחה",
|
||||
"relinkFailed": "שגיאה: {message}",
|
||||
"linkHfSuccess": "המודל נקשר בהצלחה ל-HuggingFace",
|
||||
"linkHfFailed": "שגיאה: {message}",
|
||||
"fetchMetadataFirst": "אנא אחזר מטא-דאטה מ-CivitAI תחילה",
|
||||
"noCivitaiInfo": "אין מידע מ-CivitAI זמין",
|
||||
"missingHash": "ה-hash של המודל אינו זמין"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitaiデータを更新",
|
||||
"checkUpdates": "更新確認",
|
||||
"relinkCivitai": "Civitaiに再リンク",
|
||||
"linkModel": "モデルをリンク",
|
||||
"linkCivitai": "Civitai にリンク",
|
||||
"linkHuggingFace": "HuggingFace にリンク",
|
||||
"copySyntax": "LoRA構文をコピー",
|
||||
"copyFilename": "モデルファイル名をコピー",
|
||||
"copyRecipeSyntax": "レシピ構文をコピー",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "フォルダパスを入力するか、下のツリーから選択...",
|
||||
"root": "ルート"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "HuggingFace にリンク",
|
||||
"infoText": "HuggingFace リポジトリの URL を貼り付けてモデルを関連付けます。AI によるメタデータ補完が有効になります。",
|
||||
"urlLabel": "HuggingFace リポジトリ URL:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "完全な HuggingFace リポジトリ URL を入力してください。",
|
||||
"confirmAction": "保存&リンク"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "Civitaiに再リンク",
|
||||
"warning": "警告:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "コンテンツレーティングの設定に失敗しました:{message}",
|
||||
"relinkSuccess": "モデルがCivitaiに正常に再リンクされました",
|
||||
"relinkFailed": "エラー:{message}",
|
||||
"linkHfSuccess": "モデルを HuggingFace にリンクしました",
|
||||
"linkHfFailed": "エラー:{message}",
|
||||
"fetchMetadataFirst": "最初にCivitAIからメタデータを取得してください",
|
||||
"noCivitaiInfo": "CivitAI情報が利用できません",
|
||||
"missingHash": "モデルハッシュが利用できません"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Civitai 데이터 새로고침",
|
||||
"checkUpdates": "업데이트 확인",
|
||||
"relinkCivitai": "Civitai에 다시 연결",
|
||||
"linkModel": "모델 연결",
|
||||
"linkCivitai": "Civitai에 연결",
|
||||
"linkHuggingFace": "HuggingFace에 연결",
|
||||
"copySyntax": "LoRA 문법 복사",
|
||||
"copyFilename": "모델 파일명 복사",
|
||||
"copyRecipeSyntax": "레시피 문법 복사",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "폴더 경로를 입력하거나 아래 트리에서 선택하세요...",
|
||||
"root": "루트"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "HuggingFace에 연결",
|
||||
"infoText": "HuggingFace 저장소 URL을 붙여넣어 모델을 연결합니다. AI 메타데이터 보강 기능을 사용할 수 있습니다.",
|
||||
"urlLabel": "HuggingFace 저장소 URL:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "전체 HuggingFace 저장소 URL을 입력하세요.",
|
||||
"confirmAction": "저장 및 연결"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "Civitai에 다시 연결",
|
||||
"warning": "경고:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "콘텐츠 등급 설정 실패: {message}",
|
||||
"relinkSuccess": "모델이 Civitai에 성공적으로 다시 연결되었습니다",
|
||||
"relinkFailed": "오류: {message}",
|
||||
"linkHfSuccess": "모델이 HuggingFace에 연결되었습니다",
|
||||
"linkHfFailed": "오류: {message}",
|
||||
"fetchMetadataFirst": "먼저 CivitAI에서 메타데이터를 가져와주세요",
|
||||
"noCivitaiInfo": "사용 가능한 CivitAI 정보가 없습니다",
|
||||
"missingHash": "모델 해시를 사용할 수 없습니다"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "Обновить данные Civitai",
|
||||
"checkUpdates": "Проверить обновления",
|
||||
"relinkCivitai": "Пересвязать с Civitai",
|
||||
"linkModel": "Связать модель",
|
||||
"linkCivitai": "Пересвязать с Civitai",
|
||||
"linkHuggingFace": "Связать с HuggingFace",
|
||||
"copySyntax": "Копировать синтаксис LoRA",
|
||||
"copyFilename": "Копировать имя файла модели",
|
||||
"copyRecipeSyntax": "Копировать синтаксис рецепта",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "Введите путь к папке или выберите из дерева ниже...",
|
||||
"root": "Корень"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "Связать с HuggingFace",
|
||||
"infoText": "Вставьте URL репозитория HuggingFace, чтобы связать эту модель с её источником. Это позволит обогащать метаданные с помощью ИИ.",
|
||||
"urlLabel": "URL репозитория HuggingFace:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "Введите полный URL репозитория HuggingFace.",
|
||||
"confirmAction": "Сохранить и связать"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "Пересвязать с Civitai",
|
||||
"warning": "Предупреждение:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "Не удалось установить рейтинг контента: {message}",
|
||||
"relinkSuccess": "Модель успешно пересвязана с Civitai",
|
||||
"relinkFailed": "Ошибка: {message}",
|
||||
"linkHfSuccess": "Модель успешно связана с HuggingFace",
|
||||
"linkHfFailed": "Ошибка: {message}",
|
||||
"fetchMetadataFirst": "Пожалуйста, сначала получите метаданные с CivitAI",
|
||||
"noCivitaiInfo": "Информация CivitAI недоступна",
|
||||
"missingHash": "Хеш модели недоступен"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "刷新 Civitai 数据",
|
||||
"checkUpdates": "检查更新",
|
||||
"relinkCivitai": "重新关联到 Civitai",
|
||||
"linkModel": "链接模型",
|
||||
"linkCivitai": "链接到 Civitai",
|
||||
"linkHuggingFace": "链接到 HuggingFace",
|
||||
"copySyntax": "复制 LoRA 语法",
|
||||
"copyFilename": "复制模型文件名",
|
||||
"copyRecipeSyntax": "复制配方语法",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "输入文件夹路径或从下方树中选择...",
|
||||
"root": "根目录"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "链接到 HuggingFace",
|
||||
"infoText": "粘贴 HuggingFace 仓库 URL 以关联此模型。关联后可启用 AI 元数据增强功能。",
|
||||
"urlLabel": "HuggingFace 仓库 URL:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "请输入完整的 HuggingFace 仓库 URL。",
|
||||
"confirmAction": "保存并链接"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "重新关联到 Civitai",
|
||||
"warning": "警告:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "设置内容评级失败:{message}",
|
||||
"relinkSuccess": "模型已成功重新关联到 Civitai",
|
||||
"relinkFailed": "错误:{message}",
|
||||
"linkHfSuccess": "模型已成功链接到 HuggingFace",
|
||||
"linkHfFailed": "错误:{message}",
|
||||
"fetchMetadataFirst": "请先从 CivitAI 获取元数据",
|
||||
"noCivitaiInfo": "无 CivitAI 信息",
|
||||
"missingHash": "模型哈希不可用"
|
||||
|
||||
@@ -786,7 +786,9 @@
|
||||
"contextMenu": {
|
||||
"refreshMetadata": "刷新 Civitai 資料",
|
||||
"checkUpdates": "檢查更新",
|
||||
"relinkCivitai": "重新連結 Civitai",
|
||||
"linkModel": "連結模型",
|
||||
"linkCivitai": "連結到 Civitai",
|
||||
"linkHuggingFace": "連結到 HuggingFace",
|
||||
"copySyntax": "複製 LoRA 語法",
|
||||
"copyFilename": "複製模型檔名",
|
||||
"copyRecipeSyntax": "複製配方語法",
|
||||
@@ -1319,6 +1321,14 @@
|
||||
"pathPlaceholder": "輸入資料夾路徑或從下方樹狀結構選擇...",
|
||||
"root": "根目錄"
|
||||
},
|
||||
"linkHuggingFace": {
|
||||
"title": "連結到 HuggingFace",
|
||||
"infoText": "貼上 HuggingFace 倉庫 URL 以關聯此模型。關聯後可啟用 AI 中繼資料增強功能。",
|
||||
"urlLabel": "HuggingFace 倉庫 URL:",
|
||||
"urlPlaceholder": "https://huggingface.co/user/repo",
|
||||
"helpText": "請輸入完整的 HuggingFace 倉庫 URL。",
|
||||
"confirmAction": "儲存並連結"
|
||||
},
|
||||
"relinkCivitai": {
|
||||
"title": "重新連結至 Civitai",
|
||||
"warning": "警告:",
|
||||
@@ -2048,6 +2058,8 @@
|
||||
"contentRatingFailed": "設定內容分級失敗:{message}",
|
||||
"relinkSuccess": "模型已成功重新連結至 Civitai",
|
||||
"relinkFailed": "錯誤:{message}",
|
||||
"linkHfSuccess": "模型已成功連結到 HuggingFace",
|
||||
"linkHfFailed": "錯誤:{message}",
|
||||
"fetchMetadataFirst": "請先從 CivitAI 取得 metadata",
|
||||
"noCivitaiInfo": "無 CivitAI 資訊",
|
||||
"missingHash": "模型雜湊不可用"
|
||||
|
||||
@@ -148,9 +148,117 @@ async def _save_hf_metadata(dest_path: str, repo: str, model_root: str, folder:
|
||||
logger.warning("Failed to save HF metadata for %s: %s", dest_path, exc)
|
||||
|
||||
|
||||
def _find_matching_root(dest_dir: str) -> str | None:
|
||||
"""Walk up *dest_dir* to find which configured scanner root it belongs to."""
|
||||
norm = os.path.normpath(dest_dir).replace(os.sep, "/")
|
||||
all_roots = []
|
||||
for root_list in (
|
||||
config.loras_roots or [],
|
||||
config.extra_loras_roots or [],
|
||||
config.checkpoints_roots or [],
|
||||
config.extra_checkpoints_roots or [],
|
||||
config.unet_roots or [],
|
||||
config.extra_unet_roots or [],
|
||||
config.embeddings_roots or [],
|
||||
config.extra_embeddings_roots or [],
|
||||
):
|
||||
all_roots.extend([os.path.normpath(p).replace(os.sep, "/") for p in root_list])
|
||||
# Find the longest matching prefix
|
||||
match: str | None = None
|
||||
for root in all_roots:
|
||||
if norm.startswith(root):
|
||||
if match is None or len(root) > len(match):
|
||||
match = root
|
||||
return match
|
||||
|
||||
|
||||
async def _add_to_scanner_cache(dest_path: str, metadata: dict[str, Any]) -> None:
|
||||
model_dir = os.path.dirname(dest_path)
|
||||
model_root = _find_matching_root(model_dir)
|
||||
if not model_root:
|
||||
raise ValueError(f"File path {dest_path} is not within any configured scanner root")
|
||||
scanner_getter_name = _infer_model_type(model_root)[1]
|
||||
scanner_getter = getattr(ServiceRegistry, scanner_getter_name, None)
|
||||
if scanner_getter is None:
|
||||
raise RuntimeError(f"Scanner getter '{scanner_getter_name}' not found in ServiceRegistry")
|
||||
scanner = await scanner_getter()
|
||||
if scanner is None:
|
||||
raise RuntimeError(f"Scanner '{scanner_getter_name}' returned None")
|
||||
await scanner.update_single_model_cache(dest_path, dest_path, metadata)
|
||||
|
||||
|
||||
class HfHandler:
|
||||
"""Handle Hugging Face model browsing and download."""
|
||||
|
||||
async def set_hf_url(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
payload: dict[str, Any] = await request.json()
|
||||
except json.JSONDecodeError:
|
||||
return web.json_response({"success": False, "error": "Invalid JSON"}, status=400)
|
||||
|
||||
file_path = (payload.get("file_path") or "").strip()
|
||||
hf_url = (payload.get("hf_url") or "").strip()
|
||||
|
||||
if not file_path or not hf_url:
|
||||
return web.json_response(
|
||||
{"success": False, "error": "Missing required fields: 'file_path' and 'hf_url'"},
|
||||
status=400,
|
||||
)
|
||||
|
||||
m = re.match(r"^https?://huggingface\.co/([^/]+/[^/]+)/?$", hf_url)
|
||||
if not m:
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Invalid HuggingFace URL. Expected format: https://huggingface.co/user/repo",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
if not os.path.isfile(file_path):
|
||||
return web.json_response(
|
||||
{"success": False, "error": f"File not found: {file_path}"},
|
||||
status=404,
|
||||
)
|
||||
|
||||
model_root = _find_matching_root(os.path.dirname(file_path))
|
||||
if not model_root:
|
||||
return web.json_response(
|
||||
{
|
||||
"success": False,
|
||||
"error": "File is not within any configured model directory. Cannot link to HuggingFace.",
|
||||
},
|
||||
status=400,
|
||||
)
|
||||
|
||||
try:
|
||||
existing = await MetadataManager.load_metadata_payload(file_path)
|
||||
if existing.get("hf_url") == hf_url:
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"message": "hf_url already set",
|
||||
"hf_url": hf_url,
|
||||
})
|
||||
|
||||
existing["hf_url"] = hf_url
|
||||
existing["from_civitai"] = False
|
||||
await MetadataManager.save_metadata(file_path, existing)
|
||||
|
||||
await _add_to_scanner_cache(file_path, existing)
|
||||
|
||||
logger.info("Set hf_url=%s for %s", hf_url, file_path)
|
||||
return web.json_response({
|
||||
"success": True,
|
||||
"message": f"hf_url set to {hf_url}",
|
||||
"hf_url": hf_url,
|
||||
})
|
||||
except Exception as exc:
|
||||
logger.error("Failed to set hf_url for %s: %s", file_path, exc)
|
||||
return web.json_response(
|
||||
{"success": False, "error": str(exc)},
|
||||
status=500,
|
||||
)
|
||||
|
||||
async def get_hf_repo_files(self, request: web.Request) -> web.Response:
|
||||
"""List model-weight files from a HF repo with real file sizes.
|
||||
|
||||
|
||||
@@ -3448,6 +3448,7 @@ class MiscHandlerSet:
|
||||
# Hugging Face handlers
|
||||
"get_hf_repo_files": self.hf_handler.get_hf_repo_files,
|
||||
"download_hf_model": self.hf_handler.download_hf_model,
|
||||
"set_hf_url": self.hf_handler.set_hf_url,
|
||||
# Agent skill handlers
|
||||
"get_agent_skills": self.agent_handler.get_agent_skills,
|
||||
"execute_agent_skill": self.agent_handler.execute_agent_skill,
|
||||
|
||||
@@ -103,6 +103,9 @@ MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/download-hf-model", "download_hf_model"
|
||||
),
|
||||
RouteDefinition(
|
||||
"POST", "/api/lm/set-hf-url", "set_hf_url"
|
||||
),
|
||||
# Agent skill endpoints
|
||||
RouteDefinition(
|
||||
"GET", "/api/lm/agent/skills", "get_agent_skills"
|
||||
|
||||
@@ -21,18 +21,22 @@
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
#relinkCivitaiModal .input-group,
|
||||
#linkHfModal .input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
#relinkCivitaiModal .input-group label,
|
||||
#linkHfModal .input-group label {
|
||||
margin-bottom: var(--space-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
#relinkCivitaiModal .input-group input,
|
||||
#linkHfModal .input-group input {
|
||||
width: auto;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
@@ -32,6 +32,9 @@ export class LoraContextMenu extends BaseContextMenu {
|
||||
if (!enrichItem) return;
|
||||
const hasHfUrl = !!card.dataset.hf_url;
|
||||
enrichItem.classList.toggle('disabled', !hasHfUrl);
|
||||
enrichItem.title = hasHfUrl
|
||||
? ''
|
||||
: 'Link this model to a HuggingFace repo first (Link Model \u2192 Link to HuggingFace)';
|
||||
}
|
||||
|
||||
handleMenuAction(action, menuItem) {
|
||||
|
||||
@@ -187,6 +187,74 @@ export const ModelContextMenuMixin = {
|
||||
setTimeout(() => urlInput.focus(), 50);
|
||||
},
|
||||
|
||||
// HuggingFace linking methods
|
||||
showLinkHfModal() {
|
||||
const filePath = this.currentCard.dataset.filepath;
|
||||
if (!filePath) return;
|
||||
|
||||
const confirmBtn = document.getElementById('confirmLinkHfBtn');
|
||||
const urlInput = document.getElementById('hfModelUrl');
|
||||
const errorDiv = document.getElementById('hfModelUrlError');
|
||||
|
||||
if (this._boundLinkHfHandler) {
|
||||
confirmBtn.removeEventListener('click', this._boundLinkHfHandler);
|
||||
}
|
||||
|
||||
this._boundLinkHfHandler = async () => {
|
||||
const hfUrl = urlInput.value.trim();
|
||||
if (!hfUrl) {
|
||||
errorDiv.textContent = 'Please enter a HuggingFace repository URL.';
|
||||
return;
|
||||
}
|
||||
|
||||
const hfPattern = /^https?:\/\/huggingface\.co\/([^/]+\/[^/]+)\/?$/;
|
||||
if (!hfPattern.test(hfUrl)) {
|
||||
errorDiv.textContent = 'Invalid URL format. Expected: https://huggingface.co/user/repo';
|
||||
return;
|
||||
}
|
||||
|
||||
errorDiv.textContent = '';
|
||||
modalManager.closeModal('linkHfModal');
|
||||
|
||||
try {
|
||||
state.loadingManager.showSimpleLoading('Linking to HuggingFace...');
|
||||
|
||||
const response = await fetch('/api/lm/set-hf-url', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ file_path: filePath, hf_url: hfUrl }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errData = await response.json().catch(() => ({}));
|
||||
throw new Error(errData.error || `Request failed: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast('toast.contextMenu.linkHfSuccess', {}, 'success');
|
||||
await this.resetAndReload();
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to link model');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error linking model to HuggingFace:', error);
|
||||
showToast('toast.contextMenu.linkHfFailed', { message: error.message }, 'error');
|
||||
} finally {
|
||||
state.loadingManager.hide();
|
||||
}
|
||||
};
|
||||
|
||||
confirmBtn.addEventListener('click', this._boundLinkHfHandler);
|
||||
|
||||
urlInput.value = '';
|
||||
errorDiv.textContent = '';
|
||||
|
||||
modalManager.showModal('linkHfModal');
|
||||
|
||||
setTimeout(() => urlInput.focus(), 50);
|
||||
},
|
||||
|
||||
extractModelVersionId(url) {
|
||||
return extractCivitaiModelUrlParts(url);
|
||||
},
|
||||
@@ -295,6 +363,9 @@ export const ModelContextMenuMixin = {
|
||||
case 'relink-civitai':
|
||||
this.showRelinkCivitaiModal();
|
||||
return true;
|
||||
case 'link-hf':
|
||||
this.showLinkHfModal();
|
||||
return true;
|
||||
case 'set-nsfw':
|
||||
this.showNSFWLevelSelector(null, null, this.currentCard);
|
||||
return true;
|
||||
|
||||
@@ -264,6 +264,19 @@ export class ModalManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Add linkHfModal registration
|
||||
const linkHfModal = document.getElementById('linkHfModal');
|
||||
if (linkHfModal) {
|
||||
this.registerModal('linkHfModal', {
|
||||
element: linkHfModal,
|
||||
onClose: () => {
|
||||
this.getModal('linkHfModal').element.style.display = 'none';
|
||||
document.body.classList.remove('modal-open');
|
||||
},
|
||||
closeOnOutsideClick: true
|
||||
});
|
||||
}
|
||||
|
||||
// Add exampleAccessModal registration
|
||||
const exampleAccessModal = document.getElementById('exampleAccessModal');
|
||||
if (exampleAccessModal) {
|
||||
|
||||
@@ -12,7 +12,19 @@
|
||||
<div id="checkpointContextMenu" class="context-menu" style="display: none;">
|
||||
<!-- Metadata -->
|
||||
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> {{ t('loras.contextMenu.refreshMetadata') }}</div>
|
||||
<div class="context-menu-item" data-action="relink-civitai"><i class="fas fa-link"></i> {{ t('loras.contextMenu.relinkCivitai') }}</div>
|
||||
<div class="context-menu-item has-submenu" data-has-submenu="link-model">
|
||||
<i class="fas fa-link"></i>
|
||||
<span>{{ t('loras.contextMenu.linkModel') }}</span>
|
||||
<i class="fas fa-chevron-right submenu-arrow"></i>
|
||||
<div class="context-submenu">
|
||||
<div class="context-menu-item" data-action="relink-civitai">
|
||||
<i class="fas fa-external-link-alt"></i> <span>{{ t('loras.contextMenu.linkCivitai') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="link-hf">
|
||||
<i class="fas fa-robot"></i> <span>{{ t('loras.contextMenu.linkHuggingFace') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-menu-separator menu-section-break"></div>
|
||||
<!-- Workflow -->
|
||||
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> {{ t('loras.contextMenu.copyFilename') }}</div>
|
||||
|
||||
@@ -12,8 +12,18 @@
|
||||
<div class="context-menu-item" data-action="check-updates">
|
||||
<i class="fas fa-bell"></i> <span>{{ t('loras.contextMenu.checkUpdates') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="relink-civitai">
|
||||
<i class="fas fa-link"></i> <span>{{ t('loras.contextMenu.relinkCivitai') }}</span>
|
||||
<div class="context-menu-item has-submenu" data-has-submenu="link-model">
|
||||
<i class="fas fa-link"></i>
|
||||
<span>{{ t('loras.contextMenu.linkModel') }}</span>
|
||||
<i class="fas fa-chevron-right submenu-arrow"></i>
|
||||
<div class="context-submenu">
|
||||
<div class="context-menu-item" data-action="relink-civitai">
|
||||
<i class="fas fa-external-link-alt"></i> <span>{{ t('loras.contextMenu.linkCivitai') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="link-hf">
|
||||
<i class="fas fa-robot"></i> <span>{{ t('loras.contextMenu.linkHuggingFace') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="enrich-hf-llm">
|
||||
<i class="fas fa-wand-magic-sparkles"></i> <span>{{ t('loras.contextMenu.enrichHfAgent') }}</span>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{% include 'components/modals/update_modal.html' %}
|
||||
{% include 'components/modals/help_modal.html' %}
|
||||
{% include 'components/modals/relink_civitai_modal.html' %}
|
||||
{% include 'components/modals/link_hf_modal.html' %}
|
||||
{% include 'components/modals/example_access_modal.html' %}
|
||||
{% include 'components/modals/download_modal.html' %}
|
||||
{% include 'components/modals/move_modal.html' %}
|
||||
|
||||
24
templates/components/modals/link_hf_modal.html
Normal file
24
templates/components/modals/link_hf_modal.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<!-- Link to HuggingFace Modal -->
|
||||
<div id="linkHfModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<button class="close" onclick="modalManager.closeModal('linkHfModal')">×</button>
|
||||
<h2>{{ t('modals.linkHuggingFace.title') }}</h2>
|
||||
<div class="warning-box">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<p>{{ t('modals.linkHuggingFace.infoText') }}</p>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="hfModelUrl">{{ t('modals.linkHuggingFace.urlLabel') }}</label>
|
||||
<input type="text" id="hfModelUrl" placeholder="{{ t('modals.linkHuggingFace.urlPlaceholder') }}" />
|
||||
<div class="input-error" id="hfModelUrlError"></div>
|
||||
<div class="input-help">
|
||||
{{ t('modals.linkHuggingFace.helpText') }}<br>
|
||||
<strong>https://huggingface.co/user/repo</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="cancel-btn" onclick="modalManager.closeModal('linkHfModal')">{{ t('common.actions.cancel') }}</button>
|
||||
<button class="confirm-btn" id="confirmLinkHfBtn">{{ t('modals.linkHuggingFace.confirmAction') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -12,7 +12,19 @@
|
||||
<div id="embeddingContextMenu" class="context-menu" style="display: none;">
|
||||
<!-- Metadata -->
|
||||
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> {{ t('loras.contextMenu.refreshMetadata') }}</div>
|
||||
<div class="context-menu-item" data-action="relink-civitai"><i class="fas fa-link"></i> {{ t('loras.contextMenu.relinkCivitai') }}</div>
|
||||
<div class="context-menu-item has-submenu" data-has-submenu="link-model">
|
||||
<i class="fas fa-link"></i>
|
||||
<span>{{ t('loras.contextMenu.linkModel') }}</span>
|
||||
<i class="fas fa-chevron-right submenu-arrow"></i>
|
||||
<div class="context-submenu">
|
||||
<div class="context-menu-item" data-action="relink-civitai">
|
||||
<i class="fas fa-external-link-alt"></i> <span>{{ t('loras.contextMenu.linkCivitai') }}</span>
|
||||
</div>
|
||||
<div class="context-menu-item" data-action="link-hf">
|
||||
<i class="fas fa-robot"></i> <span>{{ t('loras.contextMenu.linkHuggingFace') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="context-menu-separator menu-section-break"></div>
|
||||
<!-- Workflow -->
|
||||
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> {{ t('loras.contextMenu.copyFilename') }}</div>
|
||||
|
||||
Reference in New Issue
Block a user