diff --git a/locales/de.json b/locales/de.json index 918840df..77ac8bb6 100644 --- a/locales/de.json +++ b/locales/de.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "Keine LoRAs in diesem Rezept", "allAvailable": "Alle LoRAs verfügbar - Einsatzbereit", - "missing": "{missing} von {total} LoRAs fehlen" + "missing": "{missing} von {total} LoRAs fehlen", + "missingAndUnavailable": "{missing} von {total} LoRAs fehlen, {unavailable} nicht verfügbar (von der Quelle gelöscht oder Hash nicht auflösbar)", + "partial": "{unavailable} von {total} LoRAs nicht verfügbar (von der Quelle gelöscht oder Hash nicht auflösbar) - werden bei Verwendung des Rezepts übersprungen", + "noneUsable": "Keine verwendbaren LoRAs - {unavailable} von {total} von der Quelle gelöscht oder Hash nicht auflösbar" }, "resources": { "inLibrary": "In Bibliothek", diff --git a/locales/en.json b/locales/en.json index f5ad82d7..60a39c23 100644 --- a/locales/en.json +++ b/locales/en.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "No LoRAs in this recipe", "allAvailable": "All LoRAs available - Ready to use", - "missing": "{missing} of {total} LoRAs missing" + "missing": "{missing} of {total} LoRAs missing", + "missingAndUnavailable": "{missing} of {total} LoRAs missing, {unavailable} unavailable (deleted from source or unresolvable hash)", + "partial": "{unavailable} of {total} LoRAs unavailable (deleted from source or unresolvable hash) - skipped when recipe is used", + "noneUsable": "No usable LoRAs - {unavailable} of {total} deleted from source or unresolvable hash" }, "resources": { "inLibrary": "In Library", diff --git a/locales/es.json b/locales/es.json index 3129ec00..b7e8e83f 100644 --- a/locales/es.json +++ b/locales/es.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "No hay LoRAs en esta receta", "allAvailable": "Todos los LoRAs disponibles - Listo para usar", - "missing": "Faltan {missing} de {total} LoRAs" + "missing": "Faltan {missing} de {total} LoRAs", + "missingAndUnavailable": "Faltan {missing} de {total} LoRAs, {unavailable} no disponibles (eliminados de la fuente o hash no resoluble)", + "partial": "{unavailable} de {total} LoRAs no disponibles (eliminados de la fuente o hash no resoluble) - se omiten al usar la receta", + "noneUsable": "No hay LoRAs utilizables - {unavailable} de {total} eliminados de la fuente o hash no resoluble" }, "resources": { "inLibrary": "En la biblioteca", diff --git a/locales/fr.json b/locales/fr.json index fea76cb8..812a4567 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "Aucun LoRA dans cette Recipe", "allAvailable": "Tous les LoRAs sont disponibles - Prêt à l'emploi", - "missing": "{missing} LoRA(s) manquant(s) sur {total}" + "missing": "{missing} LoRA(s) manquant(s) sur {total}", + "missingAndUnavailable": "{missing} LoRA(s) manquant(s) sur {total}, {unavailable} indisponible(s) (supprimé(s) de la source ou hash irrésolvable)", + "partial": "{unavailable} LoRA(s) indisponible(s) sur {total} (supprimé(s) de la source ou hash irrésolvable) - ignoré(s) lors de l'utilisation de la Recipe", + "noneUsable": "Aucun LoRA utilisable - {unavailable} sur {total} supprimé(s) de la source ou hash irrésolvable" }, "resources": { "inLibrary": "Dans la bibliothèque", diff --git a/locales/he.json b/locales/he.json index 260d82df..2fe44542 100644 --- a/locales/he.json +++ b/locales/he.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "אין LoRAs במתכון הזה", "allAvailable": "כל ה-LoRAs זמינים - מוכן לשימוש", - "missing": "{missing} מתוך {total} LoRAs חסרים" + "missing": "{missing} מתוך {total} LoRAs חסרים", + "missingAndUnavailable": "{missing} מתוך {total} LoRAs חסרים, {unavailable} לא זמינים (נמחקו מהמקור או hash שלא ניתן לפתור)", + "partial": "{unavailable} מתוך {total} LoRAs לא זמינים (נמחקו מהמקור או hash שלא ניתן לפתור) - ידולגו בעת שימוש במתכון", + "noneUsable": "אין LoRAs שמישים - {unavailable} מתוך {total} נמחקו מהמקור או hash שלא ניתן לפתור" }, "resources": { "inLibrary": "בספרייה", diff --git a/locales/ja.json b/locales/ja.json index ca08c4f5..7c05edb8 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "このレシピにはLoRAがありません", "allAvailable": "すべてのLoRAが利用可能 - 使用可能", - "missing": "{total} 件中 {missing} 件のLoRAが不足" + "missing": "{total} 件中 {missing} 件のLoRAが不足", + "missingAndUnavailable": "{total} 件中 {missing} 件のLoRAが不足、{unavailable} 件は利用不可(ソースから削除済みかハッシュを解決できません)", + "partial": "{total} 件中 {unavailable} 件のLoRAが利用不可(ソースから削除済みかハッシュを解決できません)- レシピ使用時はスキップされます", + "noneUsable": "使用可能なLoRAがありません - {total} 件中 {unavailable} 件がソースから削除済みかハッシュを解決できません" }, "resources": { "inLibrary": "ライブラリ内", diff --git a/locales/ko.json b/locales/ko.json index 1003cf41..44f7d71c 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "이 레시피에는 LoRA가 없습니다", "allAvailable": "모든 LoRA 사용 가능 - 바로 사용 가능", - "missing": "총 {total}개 중 {missing}개 LoRA 누락" + "missing": "총 {total}개 중 {missing}개 LoRA 누락", + "missingAndUnavailable": "총 {total}개 중 {missing}개 LoRA 누락, {unavailable}개 사용 불가(소스에서 삭제되었거나 해시를 확인할 수 없음)", + "partial": "총 {total}개 중 {unavailable}개 LoRA 사용 불가(소스에서 삭제되었거나 해시를 확인할 수 없음) - 레시피 사용 시 건너뜁니다", + "noneUsable": "사용 가능한 LoRA가 없습니다 - 총 {total}개 중 {unavailable}개가 소스에서 삭제되었거나 해시를 확인할 수 없습니다" }, "resources": { "inLibrary": "라이브러리에 있음", diff --git a/locales/ru.json b/locales/ru.json index 75d38316..4f33ba2c 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "В этом рецепте нет LoRAs", "allAvailable": "Все LoRAs доступны - Готово к использованию", - "missing": "Отсутствует {missing} из {total} LoRAs" + "missing": "Отсутствует {missing} из {total} LoRAs", + "missingAndUnavailable": "Отсутствует {missing} из {total} LoRAs, {unavailable} недоступны (удалены из источника или хеш не разрешается)", + "partial": "{unavailable} из {total} LoRAs недоступны (удалены из источника или хеш не разрешается) - будут пропущены при использовании рецепта", + "noneUsable": "Нет доступных LoRAs - {unavailable} из {total} удалены из источника или хеш не разрешается" }, "resources": { "inLibrary": "В библиотеке", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 62dd3376..db96a3a3 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "此配方不包含 LoRA", "allAvailable": "所有 LoRA 均已就绪 - 可直接使用", - "missing": "{total} 个 LoRA 中缺失 {missing} 个" + "missing": "{total} 个 LoRA 中缺失 {missing} 个", + "missingAndUnavailable": "{total} 个 LoRA 中缺失 {missing} 个,{unavailable} 个不可用(已从源站删除或哈希无法解析)", + "partial": "{total} 个 LoRA 中 {unavailable} 个不可用(已从源站删除或哈希无法解析)- 使用配方时将被跳过", + "noneUsable": "没有可用的 LoRA - {total} 个中 {unavailable} 个已从源站删除或哈希无法解析" }, "resources": { "inLibrary": "在库中", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 2653b987..732e3f74 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -883,7 +883,10 @@ "loraStatus": { "none": "此配方不含 LoRA", "allAvailable": "所有 LoRA 皆已就緒 - 可直接使用", - "missing": "{total} 個 LoRA 中缺少 {missing} 個" + "missing": "{total} 個 LoRA 中缺少 {missing} 個", + "missingAndUnavailable": "{total} 個 LoRA 中缺少 {missing} 個,{unavailable} 個不可用(已從來源刪除或雜湊無法解析)", + "partial": "{total} 個 LoRA 中 {unavailable} 個不可用(已從來源刪除或雜湊無法解析)- 使用配方時將被略過", + "noneUsable": "沒有可用的 LoRA - {total} 個中 {unavailable} 個已從來源刪除或雜湊無法解析" }, "resources": { "inLibrary": "已在庫存", diff --git a/py/services/recipe_scanner.py b/py/services/recipe_scanner.py index c9acd075..54627e1d 100644 --- a/py/services/recipe_scanner.py +++ b/py/services/recipe_scanner.py @@ -3889,7 +3889,10 @@ class RecipeScanner: break if not file_name: - if lora.get("isDeleted", False): + # LoRAs deleted from the source or with an unresolvable hash + # cannot be downloaded; skip them instead of emitting a token + # pointing at a file that does not exist locally. + if lora.get("isDeleted", False) or lora.get("hashInvalid", False): continue file_name = lora.get("file_name", "unknown-lora") folder = lora.get("folder", "") diff --git a/static/css/components/card.css b/static/css/components/card.css index e0c439f1..f5304355 100644 --- a/static/css/components/card.css +++ b/static/css/components/card.css @@ -699,6 +699,20 @@ body.hide-card-version .hl-badge { border-color: rgba(231, 76, 60, 0.65); } +/* Partial: usable but degraded — some LoRAs are unobtainable (deleted from + the source or unresolvable hash) and are skipped when the recipe is used. + Amber sits between ready green and missing red. */ +.lora-count.partial { + background: rgba(243, 156, 18, 0.35); + border-color: rgba(243, 156, 18, 0.65); +} + +/* Unavailable: no usable LoRA at all — gray marks the recipe as dead. */ +.lora-count.unavailable { + background: rgba(149, 165, 166, 0.35); + border-color: rgba(149, 165, 166, 0.65); +} + .placeholder-message { grid-column: 1 / -1; text-align: center; diff --git a/static/css/components/lora-modal/lora-modal.css b/static/css/components/lora-modal/lora-modal.css index 51c1c60b..d5fcca7e 100644 --- a/static/css/components/lora-modal/lora-modal.css +++ b/static/css/components/lora-modal/lora-modal.css @@ -1078,6 +1078,19 @@ color: #facc15; } +/* Partial: usable but degraded — some LoRAs are unobtainable and skipped. + Orange sits between ready green and missing amber. */ +.recipe-card__badge--partial { + background: rgba(249, 115, 22, 0.2); + color: #fb923c; +} + +/* Unavailable: no usable LoRA at all — red marks the recipe as dead. */ +.recipe-card__badge--unavailable { + background: rgba(239, 68, 68, 0.2); + color: #f87171; +} + .recipe-card__badge--empty { background: rgba(148, 163, 184, 0.18); color: #e2e8f0; @@ -1093,6 +1106,16 @@ background: rgba(245, 199, 43, 0.22); } +[data-theme="light"] .recipe-card__badge--partial { + color: #c2410c; + background: rgba(249, 115, 22, 0.18); +} + +[data-theme="light"] .recipe-card__badge--unavailable { + color: #b91c1c; + background: rgba(239, 68, 68, 0.16); +} + [data-theme="light"] .recipe-card__badge--empty { color: rgba(71, 85, 105, 0.9); background: rgba(148, 163, 184, 0.2); diff --git a/static/js/components/ContextMenu/RecipeContextMenu.js b/static/js/components/ContextMenu/RecipeContextMenu.js index a6a6ffa6..1e22a2b3 100644 --- a/static/js/components/ContextMenu/RecipeContextMenu.js +++ b/static/js/components/ContextMenu/RecipeContextMenu.js @@ -204,8 +204,9 @@ export class RecipeContextMenu extends BaseContextMenu { const response = await fetch(`/api/lm/recipe/${recipeId}`); const recipe = await response.json(); - // Get missing LoRAs - const missingLoras = recipe.loras.filter(lora => !lora.inLibrary && !lora.isDeleted); + // Get missing LoRAs (still downloadable: not deleted from the + // source and hash still resolvable) + const missingLoras = recipe.loras.filter(lora => !lora.inLibrary && !lora.isDeleted && !lora.hashInvalid); if (missingLoras.length === 0) { showToast('recipes.contextMenu.downloadMissing.noMissingLoras', {}, 'info'); diff --git a/static/js/components/RecipeCard.js b/static/js/components/RecipeCard.js index 18ae172c..0c46d2c1 100644 --- a/static/js/components/RecipeCard.js +++ b/static/js/components/RecipeCard.js @@ -41,15 +41,35 @@ class RecipeCard { const loras = this.recipe.loras || []; const lorasCount = loras.length; - // Check if all LoRAs are available in the library - const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted).length; - const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0; + // Count LoRAs by availability: in library, missing (still downloadable + // from the source), or unobtainable (deleted from the source, or an + // unresolvable hash) which is silently skipped when the recipe is used. + const availableLorasCount = loras.filter(lora => lora.inLibrary).length; + const missingLorasCount = loras.filter(lora => !lora.inLibrary && !lora.isDeleted && !lora.hashInvalid).length; + const unavailableLorasCount = lorasCount - availableLorasCount - missingLorasCount; // Compact status pill: state icon + available/total fraction. // Icon switches by state so status never relies on color alone. - const availableLorasCount = lorasCount - missingLorasCount; - const loraCountStateClass = missingLorasCount > 0 ? 'missing' : (allLorasAvailable ? 'ready' : ''); - const loraCountIcon = missingLorasCount > 0 ? 'fa-exclamation-triangle' : (allLorasAvailable ? 'fa-check' : 'fa-layer-group'); + // - missing (red): something can still be downloaded, most actionable + // - partial (amber): usable but degraded, unobtainable LoRAs are skipped + // - unavailable (gray, ban): no usable LoRA at all + let loraCountStateClass = ''; + let loraCountIcon = 'fa-layer-group'; + if (lorasCount > 0) { + if (availableLorasCount === lorasCount) { + loraCountStateClass = 'ready'; + loraCountIcon = 'fa-check'; + } else if (missingLorasCount > 0) { + loraCountStateClass = 'missing'; + loraCountIcon = 'fa-exclamation-triangle'; + } else if (availableLorasCount > 0) { + loraCountStateClass = 'partial'; + loraCountIcon = 'fa-circle-minus'; + } else { + loraCountStateClass = 'unavailable'; + loraCountIcon = 'fa-ban'; + } + } const loraCountLabel = lorasCount > 0 ? `${availableLorasCount}/${lorasCount}` : `${lorasCount}`; // Ensure file_url exists, fallback to API URL if needed @@ -135,7 +155,7 @@ class RecipeCard { ${this.recipe.title} ${!isDuplicatesMode ? ` -