mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-08-28 08:21:27 -03:00
feat(relink): accept CivitArchive URLs when linking models
This commit is contained in:
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "Suche nach doppelten Rezepten...",
|
||||||
"found": "{count} Duplikat-Gruppen gefunden",
|
"found": "{count} Duplikat-Gruppen gefunden",
|
||||||
"noGroups": "Keine Duplikat-Gruppen mit dem aktuellen Abgleichskriterium gefunden",
|
"noGroups": "Keine Duplikat-Gruppen mit dem aktuellen Abgleichskriterium gefunden",
|
||||||
"keepLatest": "Neueste Versionen behalten",
|
"keepLatest": "Neueste Versionen behalten",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "Fahren Sie nur fort, wenn Sie sicher sind, dass Sie das wollen.",
|
"proceedText": "Fahren Sie nur fort, wenn Sie sicher sind, dass Sie das wollen.",
|
||||||
"urlLabel": "Civitai-Modell-URL:",
|
"urlLabel": "Civitai-Modell-URL:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "Fügen Sie eine beliebige Civitai-Modell-URL ein. Unterstützte Formate:",
|
"title": "Fügen Sie eine beliebige Civitai- oder CivitArchive-Modell-URL ein. Unterstützte Formate:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "Hinweis: Wenn keine modelVersionId angegeben ist, wird die neueste Version verwendet."
|
"note": "Hinweis: Wenn keine modelVersionId angegeben ist, wird die neueste Version verwendet.",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "Neu-Verknüpfung bestätigen"
|
"confirmAction": "Neu-Verknüpfung bestätigen"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "Fehler: {message}",
|
"relinkFailed": "Fehler: {message}",
|
||||||
"linkHfSuccess": "Modell erfolgreich mit HuggingFace verknüpft",
|
"linkHfSuccess": "Modell erfolgreich mit HuggingFace verknüpft",
|
||||||
"linkHfFailed": "Fehler: {message}",
|
"linkHfFailed": "Fehler: {message}",
|
||||||
|
"linkCivArchSuccess": "Modell erfolgreich über CivitArchive neu verknüpft",
|
||||||
"fetchMetadataFirst": "Bitte rufen Sie zuerst Metadaten von CivitAI ab",
|
"fetchMetadataFirst": "Bitte rufen Sie zuerst Metadaten von CivitAI ab",
|
||||||
"noCivitaiInfo": "Keine CivitAI-Informationen verfügbar",
|
"noCivitaiInfo": "Keine CivitAI-Informationen verfügbar",
|
||||||
"missingHash": "Modell-Hash nicht verfügbar"
|
"missingHash": "Modell-Hash nicht verfügbar"
|
||||||
|
|||||||
+10
-8
@@ -864,8 +864,8 @@
|
|||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"label": "Recipe navigation",
|
"label": "Recipe navigation",
|
||||||
"previousWithShortcut": "Previous recipe (\u2190)",
|
"previousWithShortcut": "Previous recipe (←)",
|
||||||
"nextWithShortcut": "Next recipe (\u2192)"
|
"nextWithShortcut": "Next recipe (→)"
|
||||||
},
|
},
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"sendWorkflow": "Send Workflow to ComfyUI",
|
"sendWorkflow": "Send Workflow to ComfyUI",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "Only proceed if you're sure this is what you want.",
|
"proceedText": "Only proceed if you're sure this is what you want.",
|
||||||
"urlLabel": "Civitai Model URL:",
|
"urlLabel": "Civitai Model URL:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676 or https://civitai.red/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890 or https://civitai.red/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "Paste any Civitai model URL from civitai.com or civitai.red. Supported formats:",
|
"title": "Paste any Civitai or CivitArchive model URL. Supported formats:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "Note: If no modelVersionId is provided, the latest version will be used."
|
"note": "Note: If no modelVersionId is provided, the latest version will be used.",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "Confirm Re-link"
|
"confirmAction": "Confirm Re-link"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "Error: {message}",
|
"relinkFailed": "Error: {message}",
|
||||||
"linkHfSuccess": "Model successfully linked to HuggingFace",
|
"linkHfSuccess": "Model successfully linked to HuggingFace",
|
||||||
"linkHfFailed": "Error: {message}",
|
"linkHfFailed": "Error: {message}",
|
||||||
|
"linkCivArchSuccess": "Model successfully re-linked via CivitArchive",
|
||||||
"fetchMetadataFirst": "Please fetch metadata from CivitAI first",
|
"fetchMetadataFirst": "Please fetch metadata from CivitAI first",
|
||||||
"noCivitaiInfo": "No CivitAI information available",
|
"noCivitaiInfo": "No CivitAI information available",
|
||||||
"missingHash": "Model hash not available"
|
"missingHash": "Model hash not available"
|
||||||
|
|||||||
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "Buscando recetas duplicadas...",
|
||||||
"found": "Se encontraron {count} grupos de duplicados",
|
"found": "Se encontraron {count} grupos de duplicados",
|
||||||
"noGroups": "No se encontraron grupos de duplicados con el criterio de coincidencia actual",
|
"noGroups": "No se encontraron grupos de duplicados con el criterio de coincidencia actual",
|
||||||
"keepLatest": "Mantener versiones más recientes",
|
"keepLatest": "Mantener versiones más recientes",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "Solo procede si estás seguro de que esto es lo que quieres.",
|
"proceedText": "Solo procede si estás seguro de que esto es lo que quieres.",
|
||||||
"urlLabel": "URL del modelo de Civitai:",
|
"urlLabel": "URL del modelo de Civitai:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "Pega cualquier URL de modelo de Civitai. Formatos soportados:",
|
"title": "Pega cualquier URL de modelo de Civitai o CivitArchive. Formatos soportados:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "Nota: Si no se proporciona modelVersionId, se usará la versión más reciente."
|
"note": "Nota: Si no se proporciona modelVersionId, se usará la versión más reciente.",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "Confirmar re-vinculación"
|
"confirmAction": "Confirmar re-vinculación"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "Error: {message}",
|
"relinkFailed": "Error: {message}",
|
||||||
"linkHfSuccess": "Modelo vinculado a HuggingFace exitosamente",
|
"linkHfSuccess": "Modelo vinculado a HuggingFace exitosamente",
|
||||||
"linkHfFailed": "Error: {message}",
|
"linkHfFailed": "Error: {message}",
|
||||||
|
"linkCivArchSuccess": "Modelo re-vinculado exitosamente mediante CivitArchive",
|
||||||
"fetchMetadataFirst": "Por favor obtén metadatos de CivitAI primero",
|
"fetchMetadataFirst": "Por favor obtén metadatos de CivitAI primero",
|
||||||
"noCivitaiInfo": "No hay información de CivitAI disponible",
|
"noCivitaiInfo": "No hay información de CivitAI disponible",
|
||||||
"missingHash": "Hash del modelo no disponible"
|
"missingHash": "Hash del modelo no disponible"
|
||||||
|
|||||||
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "Recherche de recettes en doublon...",
|
||||||
"found": "Trouvé {count} groupes de doublons",
|
"found": "Trouvé {count} groupes de doublons",
|
||||||
"noGroups": "Aucun groupe de doublons trouvé avec le critère de correspondance actuel",
|
"noGroups": "Aucun groupe de doublons trouvé avec le critère de correspondance actuel",
|
||||||
"keepLatest": "Garder les dernières versions",
|
"keepLatest": "Garder les dernières versions",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "Ne procédez que si vous êtes sûr que c'est ce que vous voulez.",
|
"proceedText": "Ne procédez que si vous êtes sûr que c'est ce que vous voulez.",
|
||||||
"urlLabel": "URL du modèle Civitai :",
|
"urlLabel": "URL du modèle Civitai :",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "Collez n'importe quelle URL de modèle Civitai. Formats supportés :",
|
"title": "Collez n'importe quelle URL de modèle Civitai ou CivitArchive. Formats supportés :",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "Note : Si aucun modelVersionId n'est fourni, la dernière version sera utilisée."
|
"note": "Note : Si aucun modelVersionId n'est fourni, la dernière version sera utilisée.",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "Confirmer la re-liaison"
|
"confirmAction": "Confirmer la re-liaison"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "Erreur : {message}",
|
"relinkFailed": "Erreur : {message}",
|
||||||
"linkHfSuccess": "Modèle lié à HuggingFace avec succès",
|
"linkHfSuccess": "Modèle lié à HuggingFace avec succès",
|
||||||
"linkHfFailed": "Erreur : {message}",
|
"linkHfFailed": "Erreur : {message}",
|
||||||
|
"linkCivArchSuccess": "Modèle relié via CivitArchive avec succès",
|
||||||
"fetchMetadataFirst": "Veuillez d'abord récupérer les métadonnées depuis CivitAI",
|
"fetchMetadataFirst": "Veuillez d'abord récupérer les métadonnées depuis CivitAI",
|
||||||
"noCivitaiInfo": "Aucune information CivitAI disponible",
|
"noCivitaiInfo": "Aucune information CivitAI disponible",
|
||||||
"missingHash": "Hash du modèle non disponible"
|
"missingHash": "Hash du modèle non disponible"
|
||||||
|
|||||||
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "סורק למציאת מתכונים כפולים...",
|
||||||
"found": "נמצאו {count} קבוצות כפולות",
|
"found": "נמצאו {count} קבוצות כפולות",
|
||||||
"noGroups": "לא נמצאו קבוצות כפולות לפי קריטריון ההתאמה הנוכחי",
|
"noGroups": "לא נמצאו קבוצות כפולות לפי קריטריון ההתאמה הנוכחי",
|
||||||
"keepLatest": "שמור גרסאות אחרונות",
|
"keepLatest": "שמור גרסאות אחרונות",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "המשך רק אם אתה בטוח שזה מה שאתה רוצה.",
|
"proceedText": "המשך רק אם אתה בטוח שזה מה שאתה רוצה.",
|
||||||
"urlLabel": "כתובת URL של מודל ב-Civitai:",
|
"urlLabel": "כתובת URL של מודל ב-Civitai:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "הדבק כל כתובת URL של מודל מ-Civitai. פורמטים נתמכים:",
|
"title": "הדבק כל כתובת URL של מודל מ-Civitai או מ-CivitArchive. פורמטים נתמכים:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "הערה: אם לא סופק modelVersionId, תילקח הגרסה האחרונה."
|
"note": "הערה: אם לא סופק modelVersionId, תילקח הגרסה האחרונה.",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "אשר קישור מחדש"
|
"confirmAction": "אשר קישור מחדש"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "שגיאה: {message}",
|
"relinkFailed": "שגיאה: {message}",
|
||||||
"linkHfSuccess": "המודל נקשר בהצלחה ל-HuggingFace",
|
"linkHfSuccess": "המודל נקשר בהצלחה ל-HuggingFace",
|
||||||
"linkHfFailed": "שגיאה: {message}",
|
"linkHfFailed": "שגיאה: {message}",
|
||||||
|
"linkCivArchSuccess": "המודל קושר מחדש דרך CivitArchive בהצלחה",
|
||||||
"fetchMetadataFirst": "אנא אחזר מטא-דאטה מ-CivitAI תחילה",
|
"fetchMetadataFirst": "אנא אחזר מטא-דאטה מ-CivitAI תחילה",
|
||||||
"noCivitaiInfo": "אין מידע מ-CivitAI זמין",
|
"noCivitaiInfo": "אין מידע מ-CivitAI זמין",
|
||||||
"missingHash": "ה-hash של המודל אינו זמין"
|
"missingHash": "ה-hash של המודל אינו זמין"
|
||||||
|
|||||||
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "重複レシピをスキャンしています...",
|
||||||
"found": "{count} 個の重複グループが見つかりました",
|
"found": "{count} 個の重複グループが見つかりました",
|
||||||
"noGroups": "現在の一致基準では重複グループが見つかりませんでした",
|
"noGroups": "現在の一致基準では重複グループが見つかりませんでした",
|
||||||
"keepLatest": "最新バージョンを保持",
|
"keepLatest": "最新バージョンを保持",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "これが本当に必要な場合のみ続行してください。",
|
"proceedText": "これが本当に必要な場合のみ続行してください。",
|
||||||
"urlLabel": "CivitaiモデルURL:",
|
"urlLabel": "CivitaiモデルURL:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "CivitaiモデルURLを貼り付けてください。対応形式:",
|
"title": "CivitaiまたはCivitArchiveのモデルURLを貼り付けてください。対応形式:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "注:modelVersionIdが提供されていない場合、最新バージョンが使用されます。"
|
"note": "注:modelVersionIdが提供されていない場合、最新バージョンが使用されます。",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "再リンクを確認"
|
"confirmAction": "再リンクを確認"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "エラー:{message}",
|
"relinkFailed": "エラー:{message}",
|
||||||
"linkHfSuccess": "モデルを HuggingFace にリンクしました",
|
"linkHfSuccess": "モデルを HuggingFace にリンクしました",
|
||||||
"linkHfFailed": "エラー:{message}",
|
"linkHfFailed": "エラー:{message}",
|
||||||
|
"linkCivArchSuccess": "モデルがCivitArchive経由で正常に再リンクされました",
|
||||||
"fetchMetadataFirst": "最初にCivitAIからメタデータを取得してください",
|
"fetchMetadataFirst": "最初にCivitAIからメタデータを取得してください",
|
||||||
"noCivitaiInfo": "CivitAI情報が利用できません",
|
"noCivitaiInfo": "CivitAI情報が利用できません",
|
||||||
"missingHash": "モデルハッシュが利用できません"
|
"missingHash": "モデルハッシュが利用できません"
|
||||||
|
|||||||
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "중복 레시피를 스캔하는 중...",
|
||||||
"found": "{count}개의 중복 그룹 발견",
|
"found": "{count}개의 중복 그룹 발견",
|
||||||
"noGroups": "현재 일치 기준으로 중복 그룹을 찾을 수 없습니다",
|
"noGroups": "현재 일치 기준으로 중복 그룹을 찾을 수 없습니다",
|
||||||
"keepLatest": "최신 버전 유지",
|
"keepLatest": "최신 버전 유지",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "원하는 작업이 확실한 경우에만 진행하세요.",
|
"proceedText": "원하는 작업이 확실한 경우에만 진행하세요.",
|
||||||
"urlLabel": "Civitai 모델 URL:",
|
"urlLabel": "Civitai 모델 URL:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "모든 Civitai 모델 URL을 붙여넣으세요. 지원되는 형식:",
|
"title": "Civitai 또는 CivitArchive 모델 URL을 붙여넣으세요. 지원되는 형식:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "참고: modelVersionId가 제공되지 않으면 최신 버전이 사용됩니다."
|
"note": "참고: modelVersionId가 제공되지 않으면 최신 버전이 사용됩니다.",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "다시 연결 확인"
|
"confirmAction": "다시 연결 확인"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "오류: {message}",
|
"relinkFailed": "오류: {message}",
|
||||||
"linkHfSuccess": "모델이 HuggingFace에 연결되었습니다",
|
"linkHfSuccess": "모델이 HuggingFace에 연결되었습니다",
|
||||||
"linkHfFailed": "오류: {message}",
|
"linkHfFailed": "오류: {message}",
|
||||||
|
"linkCivArchSuccess": "모델이 CivitArchive을 통해 성공적으로 다시 연결되었습니다",
|
||||||
"fetchMetadataFirst": "먼저 CivitAI에서 메타데이터를 가져와주세요",
|
"fetchMetadataFirst": "먼저 CivitAI에서 메타데이터를 가져와주세요",
|
||||||
"noCivitaiInfo": "사용 가능한 CivitAI 정보가 없습니다",
|
"noCivitaiInfo": "사용 가능한 CivitAI 정보가 없습니다",
|
||||||
"missingHash": "모델 해시를 사용할 수 없습니다"
|
"missingHash": "모델 해시를 사용할 수 없습니다"
|
||||||
|
|||||||
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "Поиск дублирующихся рецептов...",
|
||||||
"found": "Найдено {count} групп дубликатов",
|
"found": "Найдено {count} групп дубликатов",
|
||||||
"noGroups": "Дубликатов с текущим критерием не найдено",
|
"noGroups": "Дубликатов с текущим критерием не найдено",
|
||||||
"keepLatest": "Оставить последние версии",
|
"keepLatest": "Оставить последние версии",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "Продолжайте только если вы уверены, что это то, что вам нужно.",
|
"proceedText": "Продолжайте только если вы уверены, что это то, что вам нужно.",
|
||||||
"urlLabel": "URL модели Civitai:",
|
"urlLabel": "URL модели Civitai:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "Вставьте любой URL модели Civitai. Поддерживаемые форматы:",
|
"title": "Вставьте любой URL модели Civitai или CivitArchive. Поддерживаемые форматы:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "Примечание: Если modelVersionId не указан, будет использована последняя версия."
|
"note": "Примечание: Если modelVersionId не указан, будет использована последняя версия.",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "Подтвердить пересвязывание"
|
"confirmAction": "Подтвердить пересвязывание"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "Ошибка: {message}",
|
"relinkFailed": "Ошибка: {message}",
|
||||||
"linkHfSuccess": "Модель успешно связана с HuggingFace",
|
"linkHfSuccess": "Модель успешно связана с HuggingFace",
|
||||||
"linkHfFailed": "Ошибка: {message}",
|
"linkHfFailed": "Ошибка: {message}",
|
||||||
|
"linkCivArchSuccess": "Модель успешно пересвязана через CivitArchive",
|
||||||
"fetchMetadataFirst": "Пожалуйста, сначала получите метаданные с CivitAI",
|
"fetchMetadataFirst": "Пожалуйста, сначала получите метаданные с CivitAI",
|
||||||
"noCivitaiInfo": "Информация CivitAI недоступна",
|
"noCivitaiInfo": "Информация CivitAI недоступна",
|
||||||
"missingHash": "Хеш модели недоступен"
|
"missingHash": "Хеш модели недоступен"
|
||||||
|
|||||||
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "正在扫描重复配方...",
|
||||||
"found": "发现 {count} 个重复组",
|
"found": "发现 {count} 个重复组",
|
||||||
"noGroups": "按当前判重依据未找到重复组",
|
"noGroups": "按当前判重依据未找到重复组",
|
||||||
"keepLatest": "保留最新版本",
|
"keepLatest": "保留最新版本",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "仅在你确定需要此操作时继续。",
|
"proceedText": "仅在你确定需要此操作时继续。",
|
||||||
"urlLabel": "Civitai 模型 URL:",
|
"urlLabel": "Civitai 模型 URL:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676 或 https://civitai.red/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890 或 https://civitai.red/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "粘贴任意来自 civitai.com 或 civitai.red 的 Civitai 模型 URL。支持格式:",
|
"title": "粘贴任意 Civitai 或 CivitArchive 模型 URL。支持格式:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "注意:如果未提供 modelVersionId,将使用最新版本。"
|
"note": "注意:如果未提供 modelVersionId,将使用最新版本。",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "确认重新关联"
|
"confirmAction": "确认重新关联"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "错误:{message}",
|
"relinkFailed": "错误:{message}",
|
||||||
"linkHfSuccess": "模型已成功链接到 HuggingFace",
|
"linkHfSuccess": "模型已成功链接到 HuggingFace",
|
||||||
"linkHfFailed": "错误:{message}",
|
"linkHfFailed": "错误:{message}",
|
||||||
|
"linkCivArchSuccess": "模型已成功通过 CivitArchive 重新关联",
|
||||||
"fetchMetadataFirst": "请先从 CivitAI 获取元数据",
|
"fetchMetadataFirst": "请先从 CivitAI 获取元数据",
|
||||||
"noCivitaiInfo": "无 CivitAI 信息",
|
"noCivitaiInfo": "无 CivitAI 信息",
|
||||||
"missingHash": "模型哈希不可用"
|
"missingHash": "模型哈希不可用"
|
||||||
|
|||||||
+9
-7
@@ -963,7 +963,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"duplicates": {
|
"duplicates": {
|
||||||
"finding": "[TODO: Translate] Scanning for duplicate recipes...",
|
"finding": "正在掃描重複配方...",
|
||||||
"found": "發現 {count} 組重複項",
|
"found": "發現 {count} 組重複項",
|
||||||
"noGroups": "按目前判重依據未找到重複組",
|
"noGroups": "按目前判重依據未找到重複組",
|
||||||
"keepLatest": "保留最新版本",
|
"keepLatest": "保留最新版本",
|
||||||
@@ -1422,13 +1422,14 @@
|
|||||||
},
|
},
|
||||||
"proceedText": "僅在確定需要執行時才繼續。",
|
"proceedText": "僅在確定需要執行時才繼續。",
|
||||||
"urlLabel": "Civitai 模型網址:",
|
"urlLabel": "Civitai 模型網址:",
|
||||||
"urlPlaceholder": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"urlPlaceholder": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"helpText": {
|
"helpText": {
|
||||||
"title": "貼上任意 Civitai 模型網址。支援格式:",
|
"title": "貼上任意 Civitai 或 CivitArchive 模型網址。支援格式:",
|
||||||
"format1": "https://civitai.com/models/649516",
|
"format1": "https://civitai.com/models/12345",
|
||||||
"format2": "https://civitai.com/models/649516?modelVersionId=726676",
|
"format2": "https://civitai.com/models/12345?modelVersionId=67890",
|
||||||
"format3": "https://civitai.com/models/649516/model-name?modelVersionId=726676",
|
"format3": "https://civitai.com/models/12345/model-name?modelVersionId=67890",
|
||||||
"note": "注意:若未提供 modelVersionId,將使用最新版本。"
|
"note": "注意:若未提供 modelVersionId,將使用最新版本。",
|
||||||
|
"format4": "https://civarchive.com/models/12345 (CivitArchive)"
|
||||||
},
|
},
|
||||||
"confirmAction": "確認重新連結"
|
"confirmAction": "確認重新連結"
|
||||||
},
|
},
|
||||||
@@ -2225,6 +2226,7 @@
|
|||||||
"relinkFailed": "錯誤:{message}",
|
"relinkFailed": "錯誤:{message}",
|
||||||
"linkHfSuccess": "模型已成功連結到 HuggingFace",
|
"linkHfSuccess": "模型已成功連結到 HuggingFace",
|
||||||
"linkHfFailed": "錯誤:{message}",
|
"linkHfFailed": "錯誤:{message}",
|
||||||
|
"linkCivArchSuccess": "模型已成功透過 CivitArchive 重新連結",
|
||||||
"fetchMetadataFirst": "請先從 CivitAI 取得 metadata",
|
"fetchMetadataFirst": "請先從 CivitAI 取得 metadata",
|
||||||
"noCivitaiInfo": "無 CivitAI 資訊",
|
"noCivitaiInfo": "無 CivitAI 資訊",
|
||||||
"missingHash": "模型雜湊不可用"
|
"missingHash": "模型雜湊不可用"
|
||||||
|
|||||||
@@ -634,6 +634,16 @@ class ModelManagementHandler:
|
|||||||
file_path = data.get("file_path")
|
file_path = data.get("file_path")
|
||||||
model_id = data.get("model_id")
|
model_id = data.get("model_id")
|
||||||
model_version_id = data.get("model_version_id")
|
model_version_id = data.get("model_version_id")
|
||||||
|
source = data.get("source")
|
||||||
|
|
||||||
|
if source not in (None, "", "civarchive"):
|
||||||
|
return web.json_response(
|
||||||
|
{
|
||||||
|
"success": False,
|
||||||
|
"error": f"Unsupported relink source: {source}",
|
||||||
|
},
|
||||||
|
status=400,
|
||||||
|
)
|
||||||
|
|
||||||
if not file_path or model_id is None:
|
if not file_path or model_id is None:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
@@ -649,20 +659,33 @@ class ModelManagementHandler:
|
|||||||
metadata_path
|
metadata_path
|
||||||
)
|
)
|
||||||
|
|
||||||
|
relink_kwargs = {
|
||||||
|
"file_path": file_path,
|
||||||
|
"metadata": local_metadata,
|
||||||
|
"model_id": int(model_id),
|
||||||
|
"model_version_id": int(model_version_id) if model_version_id else None,
|
||||||
|
}
|
||||||
|
if source == "civarchive":
|
||||||
|
relink_kwargs["provider_name"] = "civarchive_api"
|
||||||
|
|
||||||
updated_metadata = await self._metadata_sync.relink_metadata(
|
updated_metadata = await self._metadata_sync.relink_metadata(
|
||||||
file_path=file_path,
|
**relink_kwargs
|
||||||
metadata=local_metadata,
|
|
||||||
model_id=int(model_id),
|
|
||||||
model_version_id=int(model_version_id) if model_version_id else None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._service.scanner.update_single_model_cache(
|
await self._service.scanner.update_single_model_cache(
|
||||||
file_path, file_path, updated_metadata
|
file_path, file_path, updated_metadata
|
||||||
)
|
)
|
||||||
|
|
||||||
message = f"Model successfully re-linked to Civitai model {model_id}" + (
|
if source == "civarchive":
|
||||||
f" version {model_version_id}" if model_version_id else ""
|
message = (
|
||||||
)
|
f"Model successfully re-linked to CivArchive model {model_id}"
|
||||||
|
+ (f" version {model_version_id}" if model_version_id else "")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
message = (
|
||||||
|
f"Model successfully re-linked to Civitai model {model_id}"
|
||||||
|
+ (f" version {model_version_id}" if model_version_id else "")
|
||||||
|
)
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{
|
{
|
||||||
"success": True,
|
"success": True,
|
||||||
@@ -670,6 +693,8 @@ class ModelManagementHandler:
|
|||||||
"hash": updated_metadata.get("sha256", ""),
|
"hash": updated_metadata.get("sha256", ""),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=400)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
if is_expected_offline_error(str(exc)):
|
if is_expected_offline_error(str(exc)):
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
|
|||||||
@@ -419,14 +419,37 @@ class MetadataSyncService:
|
|||||||
metadata: Dict[str, Any],
|
metadata: Dict[str, Any],
|
||||||
model_id: int,
|
model_id: int,
|
||||||
model_version_id: Optional[int],
|
model_version_id: Optional[int],
|
||||||
|
provider_name: Optional[str] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Relink a local metadata record to a specific CivitAI model version."""
|
"""Relink a local metadata record to a specific CivitAI model version.
|
||||||
|
|
||||||
|
When ``provider_name`` is given, the named provider is resolved via the
|
||||||
|
metadata provider selector instead of the default fallback chain. A
|
||||||
|
missing/disabled provider surfaces a user-friendly error instead of the
|
||||||
|
raw selector exception.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if provider_name:
|
||||||
|
try:
|
||||||
|
provider = await self._get_provider(provider_name)
|
||||||
|
except ValueError as exc:
|
||||||
|
logger.warning(
|
||||||
|
"Unable to resolve metadata provider %s: %s", provider_name, exc
|
||||||
|
)
|
||||||
|
raise ValueError(
|
||||||
|
"CivitArchive is not available or not enabled. "
|
||||||
|
"Enable the CivitArchive API in settings to relink via CivArchive."
|
||||||
|
) from exc
|
||||||
|
else:
|
||||||
|
provider = await self._get_default_provider()
|
||||||
|
|
||||||
provider = await self._get_default_provider()
|
|
||||||
civitai_metadata = await provider.get_model_version(model_id, model_version_id)
|
civitai_metadata = await provider.get_model_version(model_id, model_version_id)
|
||||||
if not civitai_metadata:
|
if not civitai_metadata:
|
||||||
|
provider_label = (
|
||||||
|
"CivitArchive" if provider_name == "civarchive_api" else "CivitAI"
|
||||||
|
)
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Model version not found on CivitAI for ID: {model_id}"
|
f"Model version not found on {provider_label} for ID: {model_id}"
|
||||||
+ (f" with version: {model_version_id}" if model_version_id else "")
|
+ (f" with version: {model_version_id}" if model_version_id else "")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { bulkManager } from '../../managers/BulkManager.js';
|
|||||||
import { MODEL_CONFIG } from '../../api/apiConfig.js';
|
import { MODEL_CONFIG } from '../../api/apiConfig.js';
|
||||||
import { translate } from '../../utils/i18nHelpers.js';
|
import { translate } from '../../utils/i18nHelpers.js';
|
||||||
import { getNsfwLevelSelector } from '../shared/NsfwLevelSelector.js';
|
import { getNsfwLevelSelector } from '../shared/NsfwLevelSelector.js';
|
||||||
import { extractCivitaiModelUrlParts } from '../../utils/civitaiUtils.js';
|
import { classifyModelRelinkUrl } from '../../utils/civitaiUtils.js';
|
||||||
|
|
||||||
// Mixin with shared functionality for LoraContextMenu and CheckpointContextMenu
|
// Mixin with shared functionality for LoraContextMenu and CheckpointContextMenu
|
||||||
export const ModelContextMenuMixin = {
|
export const ModelContextMenuMixin = {
|
||||||
@@ -106,6 +106,17 @@ export const ModelContextMenuMixin = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Civitai re-linking methods
|
// Civitai re-linking methods
|
||||||
|
getModelTypePrefix() {
|
||||||
|
// Map the mixin model type to its API route prefix; the relink route
|
||||||
|
// exists for all model types via COMMON_ROUTE_DEFINITIONS.
|
||||||
|
const prefixMap = {
|
||||||
|
lora: 'loras',
|
||||||
|
checkpoint: 'checkpoints',
|
||||||
|
embedding: 'embeddings'
|
||||||
|
};
|
||||||
|
return prefixMap[this.modelType] || 'loras';
|
||||||
|
},
|
||||||
|
|
||||||
showRelinkCivitaiModal() {
|
showRelinkCivitaiModal() {
|
||||||
const filePath = this.currentCard.dataset.filepath;
|
const filePath = this.currentCard.dataset.filepath;
|
||||||
if (!filePath) return;
|
if (!filePath) return;
|
||||||
@@ -123,43 +134,55 @@ export const ModelContextMenuMixin = {
|
|||||||
// Create new bound handler
|
// Create new bound handler
|
||||||
this._boundRelinkHandler = async () => {
|
this._boundRelinkHandler = async () => {
|
||||||
const url = urlInput.value.trim();
|
const url = urlInput.value.trim();
|
||||||
const { modelId, modelVersionId } = this.extractModelVersionId(url);
|
const { source, modelId, modelVersionId } = classifyModelRelinkUrl(url);
|
||||||
|
|
||||||
if (!modelId) {
|
if (!source || !modelId) {
|
||||||
errorDiv.textContent = 'Invalid URL format. Must include model ID.';
|
errorDiv.textContent = 'Invalid URL format. Expected: https://civitai.com/models/{modelId} or https://civarchive.com/models/{modelId}';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorDiv.textContent = '';
|
errorDiv.textContent = '';
|
||||||
modalManager.closeModal('relinkCivitaiModal');
|
modalManager.closeModal('relinkCivitaiModal');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
state.loadingManager.showSimpleLoading('Re-linking to Civitai...');
|
const isCivArchive = source === 'civarchive';
|
||||||
|
state.loadingManager.showSimpleLoading(
|
||||||
const endpoint = this.modelType === 'checkpoint' ?
|
isCivArchive ? 'Re-linking via CivitArchive...' : 'Re-linking to Civitai...'
|
||||||
'/api/lm/checkpoints/relink-civitai' :
|
);
|
||||||
'/api/lm/loras/relink-civitai';
|
|
||||||
|
const endpoint = `/api/lm/${this.getModelTypePrefix()}/relink-civitai`;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
file_path: filePath,
|
||||||
|
model_id: modelId,
|
||||||
|
model_version_id: modelVersionId
|
||||||
|
};
|
||||||
|
// Omitted source keeps backend default-provider behaviour; only
|
||||||
|
// civarchive pins the provider explicitly.
|
||||||
|
if (isCivArchive) {
|
||||||
|
payload.source = source;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(endpoint, {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload)
|
||||||
file_path: filePath,
|
|
||||||
model_id: modelId,
|
|
||||||
model_version_id: modelVersionId
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to re-link model: ${response.statusText}`);
|
throw new Error(`Failed to re-link model: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
showToast('toast.contextMenu.relinkSuccess', {}, 'success');
|
showToast(
|
||||||
|
isCivArchive ? 'toast.contextMenu.linkCivArchSuccess' : 'toast.contextMenu.relinkSuccess',
|
||||||
|
{},
|
||||||
|
'success'
|
||||||
|
);
|
||||||
// Reload the current view to show updated data
|
// Reload the current view to show updated data
|
||||||
await this.resetAndReload();
|
await this.resetAndReload();
|
||||||
} else {
|
} else {
|
||||||
@@ -255,10 +278,6 @@ export const ModelContextMenuMixin = {
|
|||||||
setTimeout(() => urlInput.focus(), 50);
|
setTimeout(() => urlInput.focus(), 50);
|
||||||
},
|
},
|
||||||
|
|
||||||
extractModelVersionId(url) {
|
|
||||||
return extractCivitaiModelUrlParts(url);
|
|
||||||
},
|
|
||||||
|
|
||||||
parseModelId(value) {
|
parseModelId(value) {
|
||||||
if (value === undefined || value === null || value === '') {
|
if (value === undefined || value === null || value === '') {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -207,6 +207,47 @@ export function extractCivitaiModelUrlParts(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CIVITARCHIVE_PAGE_HOSTS = new Set([
|
||||||
|
'civitaiarchive.com',
|
||||||
|
'civarchive.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Classify a relink URL by its hosting source and extract ids.
|
||||||
|
* CivitArchive mirrors the Civitai id namespace, so both sources resolve to
|
||||||
|
* the same {modelId, modelVersionId} shape; only `source` differs.
|
||||||
|
*/
|
||||||
|
export function classifyModelRelinkUrl(url) {
|
||||||
|
if (!url || typeof url !== 'string') {
|
||||||
|
return { source: null, modelId: null, modelVersionId: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedUrl;
|
||||||
|
try {
|
||||||
|
parsedUrl = new URL(url.trim());
|
||||||
|
} catch (e) {
|
||||||
|
return { source: null, modelId: null, modelVersionId: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
||||||
|
return { source: null, modelId: null, modelVersionId: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
const hostname = parsedUrl.hostname.toLowerCase().replace(/^www\./, '');
|
||||||
|
const pathMatch = parsedUrl.pathname.match(/\/models\/(\d+)/);
|
||||||
|
const modelId = pathMatch ? pathMatch[1] : null;
|
||||||
|
const modelVersionId = parsedUrl.searchParams.get('modelVersionId');
|
||||||
|
|
||||||
|
if (SUPPORTED_CIVITAI_PAGE_HOSTS.has(hostname)) {
|
||||||
|
return { source: 'civitai', modelId, modelVersionId };
|
||||||
|
}
|
||||||
|
if (CIVITARCHIVE_PAGE_HOSTS.has(hostname) && modelId) {
|
||||||
|
return { source: 'civarchive', modelId, modelVersionId };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { source: null, modelId: null, modelVersionId: null };
|
||||||
|
}
|
||||||
|
|
||||||
export function extractCivitaiImageId(url) {
|
export function extractCivitaiImageId(url) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
• {{ t('modals.relinkCivitai.helpText.format1') }}<br>
|
• {{ t('modals.relinkCivitai.helpText.format1') }}<br>
|
||||||
• {{ t('modals.relinkCivitai.helpText.format2') }}<br>
|
• {{ t('modals.relinkCivitai.helpText.format2') }}<br>
|
||||||
• {{ t('modals.relinkCivitai.helpText.format3') }}<br>
|
• {{ t('modals.relinkCivitai.helpText.format3') }}<br>
|
||||||
|
• {{ t('modals.relinkCivitai.helpText.format4') }}<br>
|
||||||
<em>{{ t('modals.relinkCivitai.helpText.note') }}</em>
|
<em>{{ t('modals.relinkCivitai.helpText.note') }}</em>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2504,4 +2504,172 @@ describe('Interaction-level regression coverage', () => {
|
|||||||
|
|
||||||
delete stateStub.currentPageType;
|
delete stateStub.currentPageType;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens the relink modal from the relink-civitai menu action', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="loraContextMenu" class="context-menu">
|
||||||
|
<div class="context-menu-item has-submenu" data-has-submenu="link-model">
|
||||||
|
<div class="context-submenu">
|
||||||
|
<div class="context-menu-item" data-action="relink-civitai"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="relinkCivitaiModal" class="modal">
|
||||||
|
<input type="text" id="civitaiModelUrl" />
|
||||||
|
<div class="input-error" id="civitaiModelUrlError"></div>
|
||||||
|
<button class="confirm-btn" id="confirmRelinkBtn"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { LoraContextMenu } = await import('../../../static/js/components/ContextMenu/LoraContextMenu.js');
|
||||||
|
const contextMenu = new LoraContextMenu();
|
||||||
|
const showModalSpy = vi.spyOn(contextMenu, 'showRelinkCivitaiModal').mockImplementation(() => {});
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'model-card';
|
||||||
|
card.dataset.filepath = '/models/test.safetensors';
|
||||||
|
document.body.appendChild(card);
|
||||||
|
|
||||||
|
contextMenu.showMenu(100, 100, card);
|
||||||
|
document.querySelector('[data-action="relink-civitai"]').dispatchEvent(new Event('click', { bubbles: true }));
|
||||||
|
|
||||||
|
expect(showModalSpy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an unsupported relink URL with an inline error and no fetch', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="loraContextMenu" class="context-menu"></div>
|
||||||
|
<div id="relinkCivitaiModal" class="modal">
|
||||||
|
<input type="text" id="civitaiModelUrl" />
|
||||||
|
<div class="input-error" id="civitaiModelUrlError"></div>
|
||||||
|
<button class="confirm-btn" id="confirmRelinkBtn"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { LoraContextMenu } = await import('../../../static/js/components/ContextMenu/LoraContextMenu.js');
|
||||||
|
const contextMenu = new LoraContextMenu();
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'model-card';
|
||||||
|
card.dataset.filepath = '/models/test.safetensors';
|
||||||
|
document.body.appendChild(card);
|
||||||
|
|
||||||
|
contextMenu.showMenu(100, 100, card);
|
||||||
|
contextMenu.showRelinkCivitaiModal();
|
||||||
|
|
||||||
|
document.getElementById('civitaiModelUrl').value = 'https://example.com/models/123456';
|
||||||
|
await contextMenu._boundRelinkHandler();
|
||||||
|
|
||||||
|
expect(document.getElementById('civitaiModelUrlError').textContent)
|
||||||
|
.toBe('Invalid URL format. Expected: https://civitai.com/models/{modelId} or https://civarchive.com/models/{modelId}');
|
||||||
|
expect(global.fetch).not.toHaveBeenCalled();
|
||||||
|
expect(modalManagerMock.closeModal).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('posts a valid CivitArchive URL to the relink endpoint with the civarchive source', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="loraContextMenu" class="context-menu"></div>
|
||||||
|
<div id="relinkCivitaiModal" class="modal">
|
||||||
|
<input type="text" id="civitaiModelUrl" />
|
||||||
|
<div class="input-error" id="civitaiModelUrlError"></div>
|
||||||
|
<button class="confirm-btn" id="confirmRelinkBtn"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
global.fetch = vi.fn(async () => ({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ success: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { LoraContextMenu } = await import('../../../static/js/components/ContextMenu/LoraContextMenu.js');
|
||||||
|
const contextMenu = new LoraContextMenu();
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'model-card';
|
||||||
|
card.dataset.filepath = '/models/test.safetensors';
|
||||||
|
document.body.appendChild(card);
|
||||||
|
|
||||||
|
contextMenu.showMenu(100, 100, card);
|
||||||
|
contextMenu.showRelinkCivitaiModal();
|
||||||
|
|
||||||
|
document.getElementById('civitaiModelUrl').value = 'https://civarchive.com/models/123456?modelVersionId=789012';
|
||||||
|
await contextMenu._boundRelinkHandler();
|
||||||
|
await flushAsyncTasks();
|
||||||
|
|
||||||
|
expect(modalManagerMock.closeModal).toHaveBeenCalledWith('relinkCivitaiModal');
|
||||||
|
expect(loadingManagerStub.showSimpleLoading).toHaveBeenCalledWith('Re-linking via CivitArchive...');
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('/api/lm/loras/relink-civitai', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: '/models/test.safetensors',
|
||||||
|
model_id: '123456',
|
||||||
|
model_version_id: '789012',
|
||||||
|
source: 'civarchive',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(showToastMock).toHaveBeenCalledWith('toast.contextMenu.linkCivArchSuccess', {}, 'success');
|
||||||
|
expect(resetAndReloadMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(loadingManagerStub.hide).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('posts a Civitai URL without a source key so backend defaults apply', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="loraContextMenu" class="context-menu"></div>
|
||||||
|
<div id="relinkCivitaiModal" class="modal">
|
||||||
|
<input type="text" id="civitaiModelUrl" />
|
||||||
|
<div class="input-error" id="civitaiModelUrlError"></div>
|
||||||
|
<button class="confirm-btn" id="confirmRelinkBtn"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
global.fetch = vi.fn(async () => ({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({ success: true }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { LoraContextMenu } = await import('../../../static/js/components/ContextMenu/LoraContextMenu.js');
|
||||||
|
const contextMenu = new LoraContextMenu();
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'model-card';
|
||||||
|
card.dataset.filepath = '/models/test.safetensors';
|
||||||
|
document.body.appendChild(card);
|
||||||
|
|
||||||
|
contextMenu.showMenu(100, 100, card);
|
||||||
|
contextMenu.showRelinkCivitaiModal();
|
||||||
|
|
||||||
|
document.getElementById('civitaiModelUrl').value = 'https://civitai.com/models/65423?modelVersionId=777';
|
||||||
|
await contextMenu._boundRelinkHandler();
|
||||||
|
await flushAsyncTasks();
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('/api/lm/loras/relink-civitai', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
file_path: '/models/test.safetensors',
|
||||||
|
model_id: '65423',
|
||||||
|
model_version_id: '777',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(showToastMock).toHaveBeenCalledWith('toast.contextMenu.relinkSuccess', {}, 'success');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives relink endpoint prefixes for all model types', async () => {
|
||||||
|
document.body.innerHTML = `
|
||||||
|
<div id="loraContextMenu" class="context-menu"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { LoraContextMenu } = await import('../../../static/js/components/ContextMenu/LoraContextMenu.js');
|
||||||
|
const contextMenu = new LoraContextMenu();
|
||||||
|
|
||||||
|
contextMenu.modelType = 'lora';
|
||||||
|
expect(contextMenu.getModelTypePrefix()).toBe('loras');
|
||||||
|
contextMenu.modelType = 'checkpoint';
|
||||||
|
expect(contextMenu.getModelTypePrefix()).toBe('checkpoints');
|
||||||
|
contextMenu.modelType = 'embedding';
|
||||||
|
expect(contextMenu.getModelTypePrefix()).toBe('embeddings');
|
||||||
|
contextMenu.modelType = 'unknown';
|
||||||
|
expect(contextMenu.getModelTypePrefix()).toBe('loras');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,20 +2,15 @@ import { describe, expect, it } from 'vitest';
|
|||||||
|
|
||||||
import { ModelContextMenuMixin } from '../../../static/js/components/ContextMenu/ModelContextMenuMixin.js';
|
import { ModelContextMenuMixin } from '../../../static/js/components/ContextMenu/ModelContextMenuMixin.js';
|
||||||
|
|
||||||
describe('ModelContextMenuMixin.extractModelVersionId', () => {
|
describe('ModelContextMenuMixin.getModelTypePrefix', () => {
|
||||||
it('accepts civitai.red model URLs', () => {
|
it('maps every known model type to its API route prefix', () => {
|
||||||
expect(
|
expect(ModelContextMenuMixin.getModelTypePrefix.call({ modelType: 'lora' })).toBe('loras');
|
||||||
ModelContextMenuMixin.extractModelVersionId(
|
expect(ModelContextMenuMixin.getModelTypePrefix.call({ modelType: 'checkpoint' })).toBe('checkpoints');
|
||||||
'https://civitai.red/models/65423/nijimecha-artstyle?modelVersionId=777'
|
expect(ModelContextMenuMixin.getModelTypePrefix.call({ modelType: 'embedding' })).toBe('embeddings');
|
||||||
)
|
|
||||||
).toEqual({ modelId: '65423', modelVersionId: '777' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects model-like URLs from unsupported hosts', () => {
|
it('falls back to the loras prefix for unknown types', () => {
|
||||||
expect(
|
expect(ModelContextMenuMixin.getModelTypePrefix.call({ modelType: 'unknown' })).toBe('loras');
|
||||||
ModelContextMenuMixin.extractModelVersionId(
|
expect(ModelContextMenuMixin.getModelTypePrefix.call({})).toBe('loras');
|
||||||
'https://example.com/models/65423?modelVersionId=777'
|
|
||||||
)
|
|
||||||
).toEqual({ modelId: null, modelVersionId: null });
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { readFileSync, readdirSync, statSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// Regression guard: every `.modal` element shipped via components/modals.html
|
||||||
|
// must be registered in ModalManager.initialize(). An unregistered modal makes
|
||||||
|
// modalManager.showModal(id) silently no-op (see getModal returning undefined),
|
||||||
|
// which manifests as "clicking the menu item does nothing" with no console
|
||||||
|
// error — exactly the Link-to-CivitArchive bug this file guards against.
|
||||||
|
describe('ModalManager registry parity', () => {
|
||||||
|
const repoRoot = path.resolve(__dirname, '../../..');
|
||||||
|
const modalsHtml = readFileSync(
|
||||||
|
path.join(repoRoot, 'templates/components/modals.html'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
const modalManagerSrc = readFileSync(
|
||||||
|
path.join(repoRoot, 'static/js/managers/ModalManager.js'),
|
||||||
|
'utf-8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectModalIds = (target, seen = new Set()) => {
|
||||||
|
if (statSync(target).isFile()) {
|
||||||
|
extractIds(readFileSync(target, 'utf-8'), seen);
|
||||||
|
return seen;
|
||||||
|
}
|
||||||
|
for (const entry of readdirSync(target, { withFileTypes: true })) {
|
||||||
|
collectModalIds(path.join(target, entry.name), seen);
|
||||||
|
}
|
||||||
|
return seen;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractIds = (content, seen) => {
|
||||||
|
for (const match of content.matchAll(/id="([A-Za-z][\w-]*)"[^>]*class="modal"/g)) {
|
||||||
|
seen.add(match[1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('registers every modal declared in templates', () => {
|
||||||
|
const includeFiles = [
|
||||||
|
...modalsHtml.matchAll(/\{%\s*include\s*'([^']+\.html)'\s*%\}/g),
|
||||||
|
].map((m) => m[1]);
|
||||||
|
|
||||||
|
expect(includeFiles.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const declaredIds = new Set();
|
||||||
|
for (const relPath of includeFiles) {
|
||||||
|
collectModalIds(path.join(repoRoot, 'templates', relPath), declaredIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(declaredIds.size).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const unregistered = [...declaredIds].filter(
|
||||||
|
(id) => !modalManagerSrc.includes(`registerModal('${id}'`)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
unregistered,
|
||||||
|
'Modal ids rendered on pages but never registered in ModalManager.initialize() — showModal() will silently do nothing for them'
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getThumbnailUrl,
|
getThumbnailUrl,
|
||||||
extractCivitaiImageId,
|
extractCivitaiImageId,
|
||||||
extractCivitaiModelUrlParts,
|
extractCivitaiModelUrlParts,
|
||||||
|
classifyModelRelinkUrl,
|
||||||
isCivitaiUrl,
|
isCivitaiUrl,
|
||||||
isSupportedCivitaiPageHost,
|
isSupportedCivitaiPageHost,
|
||||||
OptimizationMode
|
OptimizationMode
|
||||||
@@ -305,4 +306,48 @@ describe('civitaiUtils', () => {
|
|||||||
expect(extractCivitaiImageId('https://example.com/images/126920345')).toBe(null);
|
expect(extractCivitaiImageId('https://example.com/images/126920345')).toBe(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('classifyModelRelinkUrl', () => {
|
||||||
|
it('classifies civitai.com model URLs', () => {
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl('https://civitai.com/models/649516/name?modelVersionId=726676')
|
||||||
|
).toEqual({ source: 'civitai', modelId: '649516', modelVersionId: '726676' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('classifies civitai.red model URLs without a version id', () => {
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl('https://civitai.red/models/65423/')
|
||||||
|
).toEqual({ source: 'civitai', modelId: '65423', modelVersionId: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('classifies civarchive and civitaiarchive model URLs', () => {
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl('https://civarchive.com/models/1746460')
|
||||||
|
).toEqual({ source: 'civarchive', modelId: '1746460', modelVersionId: null });
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl('http://www.civitaiarchive.com/models/42?modelVersionId=43')
|
||||||
|
).toEqual({ source: 'civarchive', modelId: '42', modelVersionId: '43' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects archive hosts when the path has no numeric model id', () => {
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl('https://civarchive.com/images/123')
|
||||||
|
).toEqual({ source: null, modelId: null, modelVersionId: null });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects unsupported hosts and malformed input', () => {
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl('https://example.com/models/65423')
|
||||||
|
).toEqual({ source: null, modelId: null, modelVersionId: null });
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl('not a url')
|
||||||
|
).toEqual({ source: null, modelId: null, modelVersionId: null });
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl('')
|
||||||
|
).toEqual({ source: null, modelId: null, modelVersionId: null });
|
||||||
|
expect(
|
||||||
|
classifyModelRelinkUrl(null)
|
||||||
|
).toEqual({ source: null, modelId: null, modelVersionId: null });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,11 +3,16 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from py.config import config
|
from py.config import config
|
||||||
from py.routes.handlers.model_handlers import ModelCivitaiHandler, ModelUpdateHandler
|
from py.routes.handlers.model_handlers import (
|
||||||
|
ModelCivitaiHandler,
|
||||||
|
ModelManagementHandler,
|
||||||
|
ModelUpdateHandler,
|
||||||
|
)
|
||||||
from py.services.service_registry import ServiceRegistry
|
from py.services.service_registry import ServiceRegistry
|
||||||
from py.utils.metadata_manager import MetadataManager
|
from py.utils.metadata_manager import MetadataManager
|
||||||
from py.services.model_update_service import ModelUpdateRecord, ModelVersionRecord
|
from py.services.model_update_service import ModelUpdateRecord, ModelVersionRecord
|
||||||
@@ -965,3 +970,110 @@ def test_serialize_version_file_count_defaults_to_none():
|
|||||||
)
|
)
|
||||||
serialized = ModelUpdateHandler._serialize_version(version, None)
|
serialized = ModelUpdateHandler._serialize_version(version, None)
|
||||||
assert serialized["fileCount"] is None
|
assert serialized["fileCount"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def _build_relink_handler(metadata_sync):
|
||||||
|
service = SimpleNamespace(
|
||||||
|
scanner=SimpleNamespace(update_single_model_cache=AsyncMock())
|
||||||
|
)
|
||||||
|
return ModelManagementHandler(
|
||||||
|
service=service,
|
||||||
|
logger=logging.getLogger(__name__),
|
||||||
|
metadata_sync=metadata_sync,
|
||||||
|
preview_service=SimpleNamespace(),
|
||||||
|
tag_update_service=SimpleNamespace(),
|
||||||
|
lifecycle_service=SimpleNamespace(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_relink_civitai_rejects_unsupported_source():
|
||||||
|
metadata_sync = SimpleNamespace(
|
||||||
|
load_local_metadata=AsyncMock(return_value={}),
|
||||||
|
relink_metadata=AsyncMock(),
|
||||||
|
)
|
||||||
|
handler = _build_relink_handler(metadata_sync)
|
||||||
|
|
||||||
|
request = SimpleNamespace(
|
||||||
|
json=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"file_path": "/tmp/model.safetensors",
|
||||||
|
"model_id": "123",
|
||||||
|
"model_version_id": "456",
|
||||||
|
"source": "huggingface",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await handler.relink_civitai(request)
|
||||||
|
assert response.status == 400
|
||||||
|
payload = json.loads(response.text)
|
||||||
|
assert payload["success"] is False
|
||||||
|
assert "Unsupported relink source" in payload["error"]
|
||||||
|
metadata_sync.relink_metadata.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_relink_civitai_passes_provider_name_for_civarchive_source():
|
||||||
|
metadata_sync = SimpleNamespace(
|
||||||
|
load_local_metadata=AsyncMock(return_value={"model_name": "Local"}),
|
||||||
|
relink_metadata=AsyncMock(
|
||||||
|
return_value={"model_name": "Archived", "sha256": "abc"}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
handler = _build_relink_handler(metadata_sync)
|
||||||
|
|
||||||
|
request = SimpleNamespace(
|
||||||
|
json=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"file_path": "/tmp/model.safetensors",
|
||||||
|
"model_id": "123",
|
||||||
|
"model_version_id": "456",
|
||||||
|
"source": "civarchive",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await handler.relink_civitai(request)
|
||||||
|
assert response.status == 200
|
||||||
|
payload = json.loads(response.text)
|
||||||
|
assert payload["success"] is True
|
||||||
|
assert "CivArchive" in payload["message"]
|
||||||
|
metadata_sync.relink_metadata.assert_awaited_once_with(
|
||||||
|
file_path="/tmp/model.safetensors",
|
||||||
|
metadata={"model_name": "Local"},
|
||||||
|
model_id=123,
|
||||||
|
model_version_id=456,
|
||||||
|
provider_name="civarchive_api",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_relink_civitai_surfaces_provider_unavailable_without_500():
|
||||||
|
metadata_sync = SimpleNamespace(
|
||||||
|
load_local_metadata=AsyncMock(return_value={}),
|
||||||
|
relink_metadata=AsyncMock(
|
||||||
|
side_effect=ValueError(
|
||||||
|
"CivitArchive is not available or not enabled. "
|
||||||
|
"Enable the CivitArchive API in settings to relink via CivArchive."
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
handler = _build_relink_handler(metadata_sync)
|
||||||
|
|
||||||
|
request = SimpleNamespace(
|
||||||
|
json=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"file_path": "/tmp/model.safetensors",
|
||||||
|
"model_id": "123",
|
||||||
|
"model_version_id": None,
|
||||||
|
"source": "civarchive",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await handler.relink_civitai(request)
|
||||||
|
assert response.status == 400
|
||||||
|
payload = json.loads(response.text)
|
||||||
|
assert payload["success"] is False
|
||||||
|
assert "CivitArchive" in payload["error"]
|
||||||
|
|||||||
@@ -560,6 +560,131 @@ async def test_relink_metadata_raises_when_version_missing():
|
|||||||
model_version_id=None,
|
model_version_id=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_relink_metadata_uses_named_civarchive_provider(tmp_path):
|
||||||
|
default_provider = SimpleNamespace(
|
||||||
|
get_model_by_hash=AsyncMock(),
|
||||||
|
get_model_version=AsyncMock(),
|
||||||
|
)
|
||||||
|
civarchive_provider = SimpleNamespace(
|
||||||
|
get_model_by_hash=AsyncMock(),
|
||||||
|
get_model_version=AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"primary": True,
|
||||||
|
"type": "Model",
|
||||||
|
"hashes": {"SHA256": "ABCDEF"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": {"name": "Archived"},
|
||||||
|
"images": [],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def select_provider(name: str):
|
||||||
|
return civarchive_provider if name == "civarchive_api" else default_provider
|
||||||
|
|
||||||
|
provider_selector = AsyncMock(side_effect=select_provider)
|
||||||
|
helpers = build_service(
|
||||||
|
default_provider=default_provider,
|
||||||
|
provider_selector=provider_selector,
|
||||||
|
)
|
||||||
|
|
||||||
|
metadata = {"model_name": "Local", "sha256": "original"}
|
||||||
|
result = await helpers.service.relink_metadata(
|
||||||
|
file_path=str(tmp_path / "model.safetensors"),
|
||||||
|
metadata=metadata,
|
||||||
|
model_id=1,
|
||||||
|
model_version_id=2,
|
||||||
|
provider_name="civarchive_api",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["model_name"] == "Archived"
|
||||||
|
assert result["sha256"] == "original"
|
||||||
|
provider_selector.assert_awaited_with("civarchive_api")
|
||||||
|
civarchive_provider.get_model_version.assert_awaited_once_with(1, 2)
|
||||||
|
helpers.default_provider_factory.assert_not_awaited()
|
||||||
|
helpers.metadata_manager.save_metadata.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_relink_metadata_raises_when_version_missing_with_civarchive():
|
||||||
|
default_provider = SimpleNamespace(
|
||||||
|
get_model_by_hash=AsyncMock(),
|
||||||
|
get_model_version=AsyncMock(),
|
||||||
|
)
|
||||||
|
civarchive_provider = SimpleNamespace(
|
||||||
|
get_model_by_hash=AsyncMock(),
|
||||||
|
get_model_version=AsyncMock(return_value=None),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def select_provider(name: str):
|
||||||
|
return civarchive_provider if name == "civarchive_api" else default_provider
|
||||||
|
|
||||||
|
provider_selector = AsyncMock(side_effect=select_provider)
|
||||||
|
helpers = build_service(
|
||||||
|
default_provider=default_provider,
|
||||||
|
provider_selector=provider_selector,
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="CivitArchive"):
|
||||||
|
await helpers.service.relink_metadata(
|
||||||
|
file_path="/tmp/model.safetensors",
|
||||||
|
metadata={},
|
||||||
|
model_id=9,
|
||||||
|
model_version_id=None,
|
||||||
|
provider_name="civarchive_api",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_relink_metadata_raises_friendly_error_when_provider_unavailable():
|
||||||
|
provider_selector = AsyncMock(
|
||||||
|
side_effect=ValueError("Provider 'civarchive_api' is not registered")
|
||||||
|
)
|
||||||
|
helpers = build_service(provider_selector=provider_selector)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="CivitArchive is not available or not enabled"):
|
||||||
|
await helpers.service.relink_metadata(
|
||||||
|
file_path="/tmp/model.safetensors",
|
||||||
|
metadata={},
|
||||||
|
model_id=9,
|
||||||
|
model_version_id=None,
|
||||||
|
provider_name="civarchive_api",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_relink_metadata_default_call_uses_default_provider_factory(tmp_path):
|
||||||
|
helpers = build_service()
|
||||||
|
helpers.default_provider.get_model_version.return_value = {
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"primary": True,
|
||||||
|
"type": "Model",
|
||||||
|
"hashes": {"SHA256": "ABCDEF"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"model": {"name": "Remote"},
|
||||||
|
"images": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await helpers.service.relink_metadata(
|
||||||
|
file_path=str(tmp_path / "model.safetensors"),
|
||||||
|
metadata={"model_name": "Local", "sha256": "original"},
|
||||||
|
model_id=1,
|
||||||
|
model_version_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["model_name"] == "Remote"
|
||||||
|
assert result["sha256"] == "original"
|
||||||
|
helpers.default_provider_factory.assert_awaited_once()
|
||||||
|
helpers.provider_selector.assert_not_awaited()
|
||||||
|
helpers.metadata_manager.save_metadata.assert_awaited_once()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_fetch_and_update_model_persists_db_checked_when_sqlite_fails(tmp_path):
|
async def test_fetch_and_update_model_persists_db_checked_when_sqlite_fails(tmp_path):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user