diff --git a/locales/de.json b/locales/de.json index 6c0c4aa5..d0873fac 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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" diff --git a/locales/en.json b/locales/en.json index f076cbd7..15882d4d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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" diff --git a/locales/es.json b/locales/es.json index 71daeb5b..c3f6628e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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" diff --git a/locales/fr.json b/locales/fr.json index f250c4d3..7d695684 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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" diff --git a/locales/he.json b/locales/he.json index bbff7d9f..d448a1fe 100644 --- a/locales/he.json +++ b/locales/he.json @@ -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 של המודל אינו זמין" diff --git a/locales/ja.json b/locales/ja.json index bf6c5919..76978e38 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -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": "モデルハッシュが利用できません" diff --git a/locales/ko.json b/locales/ko.json index 9680e4a8..5aedd529 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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": "모델 해시를 사용할 수 없습니다" diff --git a/locales/ru.json b/locales/ru.json index 60a1f990..f1788e7b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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": "Хеш модели недоступен" diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 98a56d8d..91679dfd 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -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": "模型哈希不可用" diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 7478a628..3b1de91c 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -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": "模型雜湊不可用" diff --git a/py/routes/handlers/hf_handlers.py b/py/routes/handlers/hf_handlers.py index fcbddeff..d4431c75 100644 --- a/py/routes/handlers/hf_handlers.py +++ b/py/routes/handlers/hf_handlers.py @@ -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. diff --git a/py/routes/handlers/misc_handlers.py b/py/routes/handlers/misc_handlers.py index b62af8b8..f270b24e 100644 --- a/py/routes/handlers/misc_handlers.py +++ b/py/routes/handlers/misc_handlers.py @@ -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, diff --git a/py/routes/misc_route_registrar.py b/py/routes/misc_route_registrar.py index 4c29bb33..b6a74738 100644 --- a/py/routes/misc_route_registrar.py +++ b/py/routes/misc_route_registrar.py @@ -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" diff --git a/static/css/components/modal/relink-civitai-modal.css b/static/css/components/modal/relink-civitai-modal.css index e19515fe..b8d07f0a 100644 --- a/static/css/components/modal/relink-civitai-modal.css +++ b/static/css/components/modal/relink-civitai-modal.css @@ -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); diff --git a/static/js/components/ContextMenu/LoraContextMenu.js b/static/js/components/ContextMenu/LoraContextMenu.js index b0ede697..069b7cab 100644 --- a/static/js/components/ContextMenu/LoraContextMenu.js +++ b/static/js/components/ContextMenu/LoraContextMenu.js @@ -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) { diff --git a/static/js/components/ContextMenu/ModelContextMenuMixin.js b/static/js/components/ContextMenu/ModelContextMenuMixin.js index 77dfa8d8..beac7efb 100644 --- a/static/js/components/ContextMenu/ModelContextMenuMixin.js +++ b/static/js/components/ContextMenu/ModelContextMenuMixin.js @@ -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; diff --git a/static/js/managers/ModalManager.js b/static/js/managers/ModalManager.js index 414fe682..fe336664 100644 --- a/static/js/managers/ModalManager.js +++ b/static/js/managers/ModalManager.js @@ -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) { diff --git a/templates/checkpoints.html b/templates/checkpoints.html index 674b51c2..504d7b7c 100644 --- a/templates/checkpoints.html +++ b/templates/checkpoints.html @@ -12,7 +12,19 @@