diff --git a/locales/de.json b/locales/de.json index 595ce9a3..d2da5211 100644 --- a/locales/de.json +++ b/locales/de.json @@ -9,9 +9,9 @@ "back": "Zurück", "next": "Weiter", "backToTop": "Nach oben", - "add": "Hinzufügen", "settings": "Einstellungen", - "help": "Hilfe" + "help": "Hilfe", + "add": "Hinzufügen" }, "status": { "loading": "Wird geladen...", @@ -179,6 +179,7 @@ "recipes": "Rezepte", "checkpoints": "Checkpoints", "embeddings": "Embeddings", + "misc": "[TODO: Translate] Misc", "statistics": "Statistiken" }, "search": { @@ -187,7 +188,8 @@ "loras": "LoRAs suchen...", "recipes": "Rezepte suchen...", "checkpoints": "Checkpoints suchen...", - "embeddings": "Embeddings suchen..." + "embeddings": "Embeddings suchen...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "Suchoptionen", "searchIn": "Suchen in:", @@ -688,6 +690,16 @@ "embeddings": { "title": "Embedding-Modelle" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "Stammverzeichnis", "collapseAll": "Alle Ordner einklappen", @@ -1104,6 +1116,10 @@ "title": "Statistiken werden initialisiert", "message": "Modelldaten für Statistiken werden verarbeitet. Dies kann einige Minuten dauern..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "Tipps & Tricks", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "Rezept zum Workflow hinzugefügt", "recipeReplaced": "Rezept im Workflow ersetzt", "recipeFailedToSend": "Fehler beim Senden des Rezepts an den Workflow", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "Keine kompatiblen Knoten im aktuellen Workflow verfügbar", "noTargetNodeSelected": "Kein Zielknoten ausgewählt" }, "nodeSelector": { "recipe": "Rezept", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "Ersetzen", "append": "Anhängen", "selectTargetNode": "Zielknoten auswählen", diff --git a/locales/en.json b/locales/en.json index 229e8bda..da953350 100644 --- a/locales/en.json +++ b/locales/en.json @@ -179,6 +179,7 @@ "recipes": "Recipes", "checkpoints": "Checkpoints", "embeddings": "Embeddings", + "misc": "Misc", "statistics": "Stats" }, "search": { @@ -187,7 +188,8 @@ "loras": "Search LoRAs...", "recipes": "Search recipes...", "checkpoints": "Search checkpoints...", - "embeddings": "Search embeddings..." + "embeddings": "Search embeddings...", + "misc": "Search VAE/Upscaler models..." }, "options": "Search Options", "searchIn": "Search In:", @@ -688,6 +690,16 @@ "embeddings": { "title": "Embedding Models" }, + "misc": { + "title": "VAE & Upscaler Models", + "modelTypes": { + "vae": "VAE", + "upscaler": "Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "Root", "collapseAll": "Collapse All Folders", @@ -1104,6 +1116,10 @@ "title": "Initializing Statistics", "message": "Processing model data for statistics. This may take a few minutes..." }, + "misc": { + "title": "Initializing Misc Model Manager", + "message": "Scanning VAE and Upscaler models..." + }, "tips": { "title": "Tips & Tricks", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "Recipe appended to workflow", "recipeReplaced": "Recipe replaced in workflow", "recipeFailedToSend": "Failed to send recipe to workflow", + "vaeUpdated": "VAE updated in workflow", + "vaeFailed": "Failed to update VAE in workflow", + "upscalerUpdated": "Upscaler updated in workflow", + "upscalerFailed": "Failed to update upscaler in workflow", "noMatchingNodes": "No compatible nodes available in the current workflow", "noTargetNodeSelected": "No target node selected" }, "nodeSelector": { "recipe": "Recipe", "lora": "LoRA", + "vae": "VAE", + "upscaler": "Upscaler", "replace": "Replace", "append": "Append", "selectTargetNode": "Select target node", diff --git a/locales/es.json b/locales/es.json index f8a9909b..9f91c055 100644 --- a/locales/es.json +++ b/locales/es.json @@ -179,6 +179,7 @@ "recipes": "Recetas", "checkpoints": "Checkpoints", "embeddings": "Embeddings", + "misc": "[TODO: Translate] Misc", "statistics": "Estadísticas" }, "search": { @@ -187,7 +188,8 @@ "loras": "Buscar LoRAs...", "recipes": "Buscar recetas...", "checkpoints": "Buscar checkpoints...", - "embeddings": "Buscar embeddings..." + "embeddings": "Buscar embeddings...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "Opciones de búsqueda", "searchIn": "Buscar en:", @@ -688,6 +690,16 @@ "embeddings": { "title": "Modelos embedding" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "Raíz", "collapseAll": "Colapsar todas las carpetas", @@ -1104,6 +1116,10 @@ "title": "Inicializando estadísticas", "message": "Procesando datos del modelo para estadísticas. Esto puede tomar unos minutos..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "Consejos y trucos", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "Receta añadida al flujo de trabajo", "recipeReplaced": "Receta reemplazada en el flujo de trabajo", "recipeFailedToSend": "Error al enviar receta al flujo de trabajo", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "No hay nodos compatibles disponibles en el flujo de trabajo actual", "noTargetNodeSelected": "No se ha seleccionado ningún nodo de destino" }, "nodeSelector": { "recipe": "Receta", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "Reemplazar", "append": "Añadir", "selectTargetNode": "Seleccionar nodo de destino", diff --git a/locales/fr.json b/locales/fr.json index 3b0694ab..da9d25f2 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -179,6 +179,7 @@ "recipes": "Recipes", "checkpoints": "Checkpoints", "embeddings": "Embeddings", + "misc": "[TODO: Translate] Misc", "statistics": "Statistiques" }, "search": { @@ -187,7 +188,8 @@ "loras": "Rechercher des LoRAs...", "recipes": "Rechercher des recipes...", "checkpoints": "Rechercher des checkpoints...", - "embeddings": "Rechercher des embeddings..." + "embeddings": "Rechercher des embeddings...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "Options de recherche", "searchIn": "Rechercher dans :", @@ -688,6 +690,16 @@ "embeddings": { "title": "Modèles Embedding" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "Racine", "collapseAll": "Réduire tous les dossiers", @@ -1104,6 +1116,10 @@ "title": "Initialisation des statistiques", "message": "Traitement des données de modèle pour les statistiques. Cela peut prendre quelques minutes..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "Astuces et conseils", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "Recipe ajoutée au workflow", "recipeReplaced": "Recipe remplacée dans le workflow", "recipeFailedToSend": "Échec de l'envoi de la recipe au workflow", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "Aucun nœud compatible disponible dans le workflow actuel", "noTargetNodeSelected": "Aucun nœud cible sélectionné" }, "nodeSelector": { "recipe": "Recipe", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "Remplacer", "append": "Ajouter", "selectTargetNode": "Sélectionner le nœud cible", diff --git a/locales/he.json b/locales/he.json index c37998af..b9716315 100644 --- a/locales/he.json +++ b/locales/he.json @@ -9,9 +9,9 @@ "back": "חזור", "next": "הבא", "backToTop": "חזור למעלה", - "add": "הוסף", "settings": "הגדרות", - "help": "עזרה" + "help": "עזרה", + "add": "הוסף" }, "status": { "loading": "טוען...", @@ -179,6 +179,7 @@ "recipes": "מתכונים", "checkpoints": "Checkpoints", "embeddings": "Embeddings", + "misc": "[TODO: Translate] Misc", "statistics": "סטטיסטיקה" }, "search": { @@ -187,7 +188,8 @@ "loras": "חפש LoRAs...", "recipes": "חפש מתכונים...", "checkpoints": "חפש checkpoints...", - "embeddings": "חפש embeddings..." + "embeddings": "חפש embeddings...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "אפשרויות חיפוש", "searchIn": "חפש ב:", @@ -688,6 +690,16 @@ "embeddings": { "title": "מודלי Embedding" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "שורש", "collapseAll": "כווץ את כל התיקיות", @@ -1104,6 +1116,10 @@ "title": "מאתחל סטטיסטיקה", "message": "מעבד נתוני מודלים עבור סטטיסטיקה. זה עשוי לקחת מספר דקות..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "טיפים וטריקים", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "מתכון נוסף ל-workflow", "recipeReplaced": "מתכון הוחלף ב-workflow", "recipeFailedToSend": "שליחת מתכון ל-workflow נכשלה", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "אין צמתים תואמים זמינים ב-workflow הנוכחי", "noTargetNodeSelected": "לא נבחר צומת יעד" }, "nodeSelector": { "recipe": "מתכון", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "החלף", "append": "הוסף", "selectTargetNode": "בחר צומת יעד", diff --git a/locales/ja.json b/locales/ja.json index 465f50b7..81024441 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -179,6 +179,7 @@ "recipes": "レシピ", "checkpoints": "Checkpoint", "embeddings": "Embedding", + "misc": "[TODO: Translate] Misc", "statistics": "統計" }, "search": { @@ -187,7 +188,8 @@ "loras": "LoRAを検索...", "recipes": "レシピを検索...", "checkpoints": "checkpointを検索...", - "embeddings": "embeddingを検索..." + "embeddings": "embeddingを検索...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "検索オプション", "searchIn": "検索対象:", @@ -688,6 +690,16 @@ "embeddings": { "title": "Embeddingモデル" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "ルート", "collapseAll": "すべてのフォルダを折りたたむ", @@ -1104,6 +1116,10 @@ "title": "統計を初期化中", "message": "統計用のモデルデータを処理中。数分かかる場合があります..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "ヒント&コツ", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "レシピがワークフローに追加されました", "recipeReplaced": "レシピがワークフローで置換されました", "recipeFailedToSend": "レシピをワークフローに送信できませんでした", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "現在のワークフローには互換性のあるノードがありません", "noTargetNodeSelected": "ターゲットノードが選択されていません" }, "nodeSelector": { "recipe": "レシピ", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "置換", "append": "追加", "selectTargetNode": "ターゲットノードを選択", diff --git a/locales/ko.json b/locales/ko.json index 326e8eee..51533bbb 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -179,6 +179,7 @@ "recipes": "레시피", "checkpoints": "Checkpoint", "embeddings": "Embedding", + "misc": "[TODO: Translate] Misc", "statistics": "통계" }, "search": { @@ -187,7 +188,8 @@ "loras": "LoRA 검색...", "recipes": "레시피 검색...", "checkpoints": "Checkpoint 검색...", - "embeddings": "Embedding 검색..." + "embeddings": "Embedding 검색...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "검색 옵션", "searchIn": "검색 범위:", @@ -688,6 +690,16 @@ "embeddings": { "title": "Embedding 모델" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "루트", "collapseAll": "모든 폴더 접기", @@ -1104,6 +1116,10 @@ "title": "통계 초기화 중", "message": "통계를 위한 모델 데이터를 처리하고 있습니다. 몇 분이 걸릴 수 있습니다..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "팁 & 요령", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "레시피가 워크플로에 추가되었습니다", "recipeReplaced": "레시피가 워크플로에서 교체되었습니다", "recipeFailedToSend": "레시피를 워크플로로 전송하지 못했습니다", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "현재 워크플로에서 호환되는 노드가 없습니다", "noTargetNodeSelected": "대상 노드가 선택되지 않았습니다" }, "nodeSelector": { "recipe": "레시피", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "교체", "append": "추가", "selectTargetNode": "대상 노드 선택", diff --git a/locales/ru.json b/locales/ru.json index a2e3b8d1..a7e2d050 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -179,6 +179,7 @@ "recipes": "Рецепты", "checkpoints": "Checkpoints", "embeddings": "Embeddings", + "misc": "[TODO: Translate] Misc", "statistics": "Статистика" }, "search": { @@ -187,7 +188,8 @@ "loras": "Поиск LoRAs...", "recipes": "Поиск рецептов...", "checkpoints": "Поиск checkpoints...", - "embeddings": "Поиск embeddings..." + "embeddings": "Поиск embeddings...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "Опции поиска", "searchIn": "Искать в:", @@ -688,6 +690,16 @@ "embeddings": { "title": "Модели Embedding" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "Корень", "collapseAll": "Свернуть все папки", @@ -1104,6 +1116,10 @@ "title": "Инициализация статистики", "message": "Обработка данных моделей для статистики. Это может занять несколько минут..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "Советы и хитрости", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "Рецепт добавлен в workflow", "recipeReplaced": "Рецепт заменён в workflow", "recipeFailedToSend": "Не удалось отправить рецепт в workflow", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "В текущем workflow нет совместимых узлов", "noTargetNodeSelected": "Целевой узел не выбран" }, "nodeSelector": { "recipe": "Рецепт", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "Заменить", "append": "Добавить", "selectTargetNode": "Выберите целевой узел", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index e0e9b199..40debb46 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -179,6 +179,7 @@ "recipes": "配方", "checkpoints": "Checkpoint", "embeddings": "Embedding", + "misc": "[TODO: Translate] Misc", "statistics": "统计" }, "search": { @@ -187,7 +188,8 @@ "loras": "搜索 LoRA...", "recipes": "搜索配方...", "checkpoints": "搜索 Checkpoint...", - "embeddings": "搜索 Embedding..." + "embeddings": "搜索 Embedding...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "搜索选项", "searchIn": "搜索范围:", @@ -688,6 +690,16 @@ "embeddings": { "title": "Embedding 模型" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "根目录", "collapseAll": "折叠所有文件夹", @@ -1104,6 +1116,10 @@ "title": "初始化统计", "message": "正在处理模型数据以生成统计信息。这可能需要几分钟..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "技巧与提示", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "配方已追加到工作流", "recipeReplaced": "配方已替换到工作流", "recipeFailedToSend": "发送配方到工作流失败", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "当前工作流中没有兼容的节点", "noTargetNodeSelected": "未选择目标节点" }, "nodeSelector": { "recipe": "配方", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "替换", "append": "追加", "selectTargetNode": "选择目标节点", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index a4f023b5..15644760 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -179,6 +179,7 @@ "recipes": "配方", "checkpoints": "Checkpoint", "embeddings": "Embedding", + "misc": "[TODO: Translate] Misc", "statistics": "統計" }, "search": { @@ -187,7 +188,8 @@ "loras": "搜尋 LoRA...", "recipes": "搜尋配方...", "checkpoints": "搜尋 checkpoint...", - "embeddings": "搜尋 embedding..." + "embeddings": "搜尋 embedding...", + "misc": "[TODO: Translate] Search VAE/Upscaler models..." }, "options": "搜尋選項", "searchIn": "搜尋範圍:", @@ -688,6 +690,16 @@ "embeddings": { "title": "Embedding 模型" }, + "misc": { + "title": "[TODO: Translate] VAE & Upscaler Models", + "modelTypes": { + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler" + }, + "contextMenu": { + "moveToOtherTypeFolder": "[TODO: Translate] Move to {otherType} Folder" + } + }, "sidebar": { "modelRoot": "根目錄", "collapseAll": "全部摺疊資料夾", @@ -1104,6 +1116,10 @@ "title": "初始化統計", "message": "正在處理模型資料以產生統計,可能需要幾分鐘..." }, + "misc": { + "title": "[TODO: Translate] Initializing Misc Model Manager", + "message": "[TODO: Translate] Scanning VAE and Upscaler models..." + }, "tips": { "title": "小技巧", "civitai": { @@ -1163,12 +1179,18 @@ "recipeAdded": "配方已附加到工作流", "recipeReplaced": "配方已取代於工作流", "recipeFailedToSend": "傳送配方到工作流失敗", + "vaeUpdated": "[TODO: Translate] VAE updated in workflow", + "vaeFailed": "[TODO: Translate] Failed to update VAE in workflow", + "upscalerUpdated": "[TODO: Translate] Upscaler updated in workflow", + "upscalerFailed": "[TODO: Translate] Failed to update upscaler in workflow", "noMatchingNodes": "目前工作流程中沒有相容的節點", "noTargetNodeSelected": "未選擇目標節點" }, "nodeSelector": { "recipe": "配方", "lora": "LoRA", + "vae": "[TODO: Translate] VAE", + "upscaler": "[TODO: Translate] Upscaler", "replace": "取代", "append": "附加", "selectTargetNode": "選擇目標節點", diff --git a/py/config.py b/py/config.py index 2b6911da..3eb65b00 100644 --- a/py/config.py +++ b/py/config.py @@ -89,8 +89,11 @@ class Config: self.checkpoints_roots = None self.unet_roots = None self.embeddings_roots = None + self.vae_roots = None + self.upscaler_roots = None self.base_models_roots = self._init_checkpoint_paths() self.embeddings_roots = self._init_embedding_paths() + self.misc_roots = self._init_misc_paths() # Scan symbolic links during initialization self._initialize_symlink_mappings() @@ -151,6 +154,8 @@ class Config: 'checkpoints': list(self.checkpoints_roots or []), 'unet': list(self.unet_roots or []), 'embeddings': list(self.embeddings_roots or []), + 'vae': list(self.vae_roots or []), + 'upscale_models': list(self.upscaler_roots or []), } normalized_target_paths = _normalize_folder_paths_for_comparison(target_folder_paths) @@ -250,6 +255,7 @@ class Config: roots.extend(self.loras_roots or []) roots.extend(self.base_models_roots or []) roots.extend(self.embeddings_roots or []) + roots.extend(self.misc_roots or []) return roots def _build_symlink_fingerprint(self) -> Dict[str, object]: @@ -599,6 +605,8 @@ class Config: preview_roots.update(self._expand_preview_root(root)) for root in self.embeddings_roots or []: preview_roots.update(self._expand_preview_root(root)) + for root in self.misc_roots or []: + preview_roots.update(self._expand_preview_root(root)) for target, link in self._path_mappings.items(): preview_roots.update(self._expand_preview_root(target)) @@ -606,11 +614,12 @@ class Config: self._preview_root_paths = {path for path in preview_roots if path.is_absolute()} logger.debug( - "Preview roots rebuilt: %d paths from %d lora roots, %d checkpoint roots, %d embedding roots, %d symlink mappings", + "Preview roots rebuilt: %d paths from %d lora roots, %d checkpoint roots, %d embedding roots, %d misc roots, %d symlink mappings", len(self._preview_root_paths), len(self.loras_roots or []), len(self.base_models_roots or []), len(self.embeddings_roots or []), + len(self.misc_roots or []), len(self._path_mappings), ) @@ -769,6 +778,49 @@ class Config: logger.warning(f"Error initializing embedding paths: {e}") return [] + def _init_misc_paths(self) -> List[str]: + """Initialize and validate misc (VAE and upscaler) paths from ComfyUI settings""" + try: + raw_vae_paths = folder_paths.get_folder_paths("vae") + raw_upscaler_paths = folder_paths.get_folder_paths("upscale_models") + unique_paths = self._prepare_misc_paths(raw_vae_paths, raw_upscaler_paths) + + logger.info("Found misc roots:" + ("\n - " + "\n - ".join(unique_paths) if unique_paths else "[]")) + + if not unique_paths: + logger.warning("No valid VAE or upscaler folders found in ComfyUI configuration") + return [] + + return unique_paths + except Exception as e: + logger.warning(f"Error initializing misc paths: {e}") + return [] + + def _prepare_misc_paths( + self, vae_paths: Iterable[str], upscaler_paths: Iterable[str] + ) -> List[str]: + vae_map = self._dedupe_existing_paths(vae_paths) + upscaler_map = self._dedupe_existing_paths(upscaler_paths) + + merged_map: Dict[str, str] = {} + for real_path, original in {**vae_map, **upscaler_map}.items(): + if real_path not in merged_map: + merged_map[real_path] = original + + unique_paths = sorted(merged_map.values(), key=lambda p: p.lower()) + + vae_values = set(vae_map.values()) + upscaler_values = set(upscaler_map.values()) + self.vae_roots = [p for p in unique_paths if p in vae_values] + self.upscaler_roots = [p for p in unique_paths if p in upscaler_values] + + for original_path in unique_paths: + real_path = os.path.normpath(os.path.realpath(original_path)).replace(os.sep, '/') + if real_path != original_path: + self.add_path_mapping(original_path, real_path) + + return unique_paths + def get_preview_static_url(self, preview_path: str) -> str: if not preview_path: return "" diff --git a/py/lora_manager.py b/py/lora_manager.py index fa1eaadb..be20deae 100644 --- a/py/lora_manager.py +++ b/py/lora_manager.py @@ -184,15 +184,17 @@ class LoraManager: lora_scanner = await ServiceRegistry.get_lora_scanner() checkpoint_scanner = await ServiceRegistry.get_checkpoint_scanner() embedding_scanner = await ServiceRegistry.get_embedding_scanner() - + misc_scanner = await ServiceRegistry.get_misc_scanner() + # Initialize recipe scanner if needed recipe_scanner = await ServiceRegistry.get_recipe_scanner() - + # Create low-priority initialization tasks init_tasks = [ asyncio.create_task(lora_scanner.initialize_in_background(), name='lora_cache_init'), asyncio.create_task(checkpoint_scanner.initialize_in_background(), name='checkpoint_cache_init'), asyncio.create_task(embedding_scanner.initialize_in_background(), name='embedding_cache_init'), + asyncio.create_task(misc_scanner.initialize_in_background(), name='misc_cache_init'), asyncio.create_task(recipe_scanner.initialize_in_background(), name='recipe_cache_init') ] @@ -252,8 +254,9 @@ class LoraManager: # Collect all model roots all_roots = set() all_roots.update(config.loras_roots) - all_roots.update(config.base_models_roots) + all_roots.update(config.base_models_roots) all_roots.update(config.embeddings_roots) + all_roots.update(config.misc_roots or []) total_deleted = 0 total_size_freed = 0 diff --git a/py/routes/misc_model_routes.py b/py/routes/misc_model_routes.py new file mode 100644 index 00000000..2c770be8 --- /dev/null +++ b/py/routes/misc_model_routes.py @@ -0,0 +1,112 @@ +import logging +from typing import Dict +from aiohttp import web + +from .base_model_routes import BaseModelRoutes +from .model_route_registrar import ModelRouteRegistrar +from ..services.misc_service import MiscService +from ..services.service_registry import ServiceRegistry +from ..config import config + +logger = logging.getLogger(__name__) + +class MiscModelRoutes(BaseModelRoutes): + """Misc-specific route controller (VAE, Upscaler)""" + + def __init__(self): + """Initialize Misc routes with Misc service""" + super().__init__() + self.template_name = "misc.html" + + async def initialize_services(self): + """Initialize services from ServiceRegistry""" + misc_scanner = await ServiceRegistry.get_misc_scanner() + update_service = await ServiceRegistry.get_model_update_service() + self.service = MiscService(misc_scanner, update_service=update_service) + self.set_model_update_service(update_service) + + # Attach service dependencies + self.attach_service(self.service) + + def setup_routes(self, app: web.Application): + """Setup Misc routes""" + # Schedule service initialization on app startup + app.on_startup.append(lambda _: self.initialize_services()) + + # Setup common routes with 'misc' prefix (includes page route) + super().setup_routes(app, 'misc') + + def setup_specific_routes(self, registrar: ModelRouteRegistrar, prefix: str): + """Setup Misc-specific routes""" + # Misc info by name + registrar.add_prefixed_route('GET', '/api/lm/{prefix}/info/{name}', prefix, self.get_misc_info) + + # VAE roots and Upscaler roots + registrar.add_prefixed_route('GET', '/api/lm/{prefix}/vae_roots', prefix, self.get_vae_roots) + registrar.add_prefixed_route('GET', '/api/lm/{prefix}/upscaler_roots', prefix, self.get_upscaler_roots) + + def _validate_civitai_model_type(self, model_type: str) -> bool: + """Validate CivitAI model type for Misc (VAE or Upscaler)""" + return model_type.lower() in ['vae', 'upscaler'] + + def _get_expected_model_types(self) -> str: + """Get expected model types string for error messages""" + return "VAE or Upscaler" + + def _parse_specific_params(self, request: web.Request) -> Dict: + """Parse Misc-specific parameters""" + params: Dict = {} + + if 'misc_hash' in request.query: + params['hash_filters'] = {'single_hash': request.query['misc_hash'].lower()} + elif 'misc_hashes' in request.query: + params['hash_filters'] = { + 'multiple_hashes': [h.lower() for h in request.query['misc_hashes'].split(',')] + } + + return params + + async def get_misc_info(self, request: web.Request) -> web.Response: + """Get detailed information for a specific misc model by name""" + try: + name = request.match_info.get('name', '') + misc_info = await self.service.get_model_info_by_name(name) + + if misc_info: + return web.json_response(misc_info) + else: + return web.json_response({"error": "Misc model not found"}, status=404) + + except Exception as e: + logger.error(f"Error in get_misc_info: {e}", exc_info=True) + return web.json_response({"error": str(e)}, status=500) + + async def get_vae_roots(self, request: web.Request) -> web.Response: + """Return the list of VAE roots from config""" + try: + roots = config.vae_roots + return web.json_response({ + "success": True, + "roots": roots + }) + except Exception as e: + logger.error(f"Error getting VAE roots: {e}", exc_info=True) + return web.json_response({ + "success": False, + "error": str(e) + }, status=500) + + async def get_upscaler_roots(self, request: web.Request) -> web.Response: + """Return the list of upscaler roots from config""" + try: + roots = config.upscaler_roots + return web.json_response({ + "success": True, + "roots": roots + }) + except Exception as e: + logger.error(f"Error getting upscaler roots: {e}", exc_info=True) + return web.json_response({ + "success": False, + "error": str(e) + }, status=500) diff --git a/py/services/download_manager.py b/py/services/download_manager.py index 0f090785..967b7e52 100644 --- a/py/services/download_manager.py +++ b/py/services/download_manager.py @@ -9,7 +9,7 @@ from collections import OrderedDict import uuid from typing import Dict, List, Optional, Set, Tuple from urllib.parse import urlparse -from ..utils.models import LoraMetadata, CheckpointMetadata, EmbeddingMetadata +from ..utils.models import LoraMetadata, CheckpointMetadata, EmbeddingMetadata, MiscMetadata from ..utils.constants import CARD_PREVIEW_WIDTH, DIFFUSION_MODEL_BASE_MODELS, VALID_LORA_TYPES from ..utils.civitai_utils import rewrite_preview_url from ..utils.preview_selection import select_preview_media @@ -60,6 +60,10 @@ class DownloadManager: """Get the checkpoint scanner from registry""" return await ServiceRegistry.get_checkpoint_scanner() + async def _get_misc_scanner(self): + """Get the misc scanner from registry""" + return await ServiceRegistry.get_misc_scanner() + async def download_from_civitai( self, model_id: int = None, @@ -275,6 +279,7 @@ class DownloadManager: lora_scanner = await self._get_lora_scanner() checkpoint_scanner = await self._get_checkpoint_scanner() embedding_scanner = await ServiceRegistry.get_embedding_scanner() + misc_scanner = await self._get_misc_scanner() # Check lora scanner first if await lora_scanner.check_model_version_exists(model_version_id): @@ -299,6 +304,13 @@ class DownloadManager: "error": "Model version already exists in embedding library", } + # Check misc scanner (VAE, Upscaler) + if await misc_scanner.check_model_version_exists(model_version_id): + return { + "success": False, + "error": "Model version already exists in misc library", + } + # Use CivArchive provider directly when source is 'civarchive' # This prioritizes CivArchive metadata (with mirror availability info) over Civitai if source == "civarchive": @@ -337,6 +349,10 @@ class DownloadManager: model_type = "lora" elif model_type_from_info == "textualinversion": model_type = "embedding" + elif model_type_from_info == "vae": + model_type = "misc" + elif model_type_from_info == "upscaler": + model_type = "misc" else: return { "success": False, @@ -379,6 +395,14 @@ class DownloadManager: "success": False, "error": "Model version already exists in embedding library", } + elif model_type == "misc": + # Check misc scanner (VAE, Upscaler) + misc_scanner = await self._get_misc_scanner() + if await misc_scanner.check_model_version_exists(version_id): + return { + "success": False, + "error": "Model version already exists in misc library", + } # Handle use_default_paths if use_default_paths: @@ -413,6 +437,26 @@ class DownloadManager: "error": "Default embedding root path not set in settings", } save_dir = default_path + elif model_type == "misc": + from ..config import config + + civitai_type = version_info.get("model", {}).get("type", "").lower() + if civitai_type == "vae": + default_paths = config.vae_roots + error_msg = "VAE root path not configured" + elif civitai_type == "upscaler": + default_paths = config.upscaler_roots + error_msg = "Upscaler root path not configured" + else: + default_paths = config.misc_roots + error_msg = "Misc root path not configured" + + if not default_paths: + return { + "success": False, + "error": error_msg, + } + save_dir = default_paths[0] if default_paths else "" # Calculate relative path using template relative_path = self._calculate_relative_path(version_info, model_type) @@ -515,6 +559,11 @@ class DownloadManager: version_info, file_info, save_path ) logger.info(f"Creating EmbeddingMetadata for {file_name}") + elif model_type == "misc": + metadata = MiscMetadata.from_civitai_info( + version_info, file_info, save_path + ) + logger.info(f"Creating MiscMetadata for {file_name}") # 6. Start download process result = await self._execute_download( @@ -620,6 +669,8 @@ class DownloadManager: scanner = await self._get_checkpoint_scanner() elif model_type == "embedding": scanner = await ServiceRegistry.get_embedding_scanner() + elif model_type == "misc": + scanner = await self._get_misc_scanner() except Exception as exc: logger.debug("Failed to acquire scanner for %s models: %s", model_type, exc) @@ -1016,6 +1067,9 @@ class DownloadManager: elif model_type == "embedding": scanner = await ServiceRegistry.get_embedding_scanner() logger.info(f"Updating embedding cache for {actual_file_paths[0]}") + elif model_type == "misc": + scanner = await self._get_misc_scanner() + logger.info(f"Updating misc cache for {actual_file_paths[0]}") adjust_cached_entry = ( getattr(scanner, "adjust_cached_entry", None) @@ -1125,6 +1179,14 @@ class DownloadManager: ".pkl", ".sft", } + if model_type == "misc": + return { + ".ckpt", + ".pt", + ".bin", + ".pth", + ".safetensors", + } return {".safetensors"} async def _extract_model_files_from_archive( diff --git a/py/services/misc_scanner.py b/py/services/misc_scanner.py new file mode 100644 index 00000000..80a34fb0 --- /dev/null +++ b/py/services/misc_scanner.py @@ -0,0 +1,55 @@ +import logging +from typing import Any, Dict, List, Optional + +from ..utils.models import MiscMetadata +from ..config import config +from .model_scanner import ModelScanner +from .model_hash_index import ModelHashIndex + +logger = logging.getLogger(__name__) + +class MiscScanner(ModelScanner): + """Service for scanning and managing misc files (VAE, Upscaler)""" + + def __init__(self): + # Define supported file extensions (combined from VAE and upscaler) + file_extensions = {'.safetensors', '.pt', '.bin', '.ckpt', '.pth'} + super().__init__( + model_type="misc", + model_class=MiscMetadata, + file_extensions=file_extensions, + hash_index=ModelHashIndex() + ) + + def _resolve_sub_type(self, root_path: Optional[str]) -> Optional[str]: + """Resolve the sub-type based on the root path.""" + if not root_path: + return None + + if config.vae_roots and root_path in config.vae_roots: + return "vae" + + if config.upscaler_roots and root_path in config.upscaler_roots: + return "upscaler" + + return None + + def adjust_metadata(self, metadata, file_path, root_path): + """Adjust metadata during scanning to set sub_type.""" + sub_type = self._resolve_sub_type(root_path) + if sub_type: + metadata.sub_type = sub_type + return metadata + + def adjust_cached_entry(self, entry: Dict[str, Any]) -> Dict[str, Any]: + """Adjust entries loaded from the persisted cache to ensure sub_type is set.""" + sub_type = self._resolve_sub_type( + self._find_root_for_file(entry.get("file_path")) + ) + if sub_type: + entry["sub_type"] = sub_type + return entry + + def get_model_roots(self) -> List[str]: + """Get misc root directories (VAE and upscaler)""" + return config.misc_roots diff --git a/py/services/misc_service.py b/py/services/misc_service.py new file mode 100644 index 00000000..c6e8cd70 --- /dev/null +++ b/py/services/misc_service.py @@ -0,0 +1,55 @@ +import os +import logging +from typing import Dict + +from .base_model_service import BaseModelService +from ..utils.models import MiscMetadata +from ..config import config + +logger = logging.getLogger(__name__) + +class MiscService(BaseModelService): + """Misc-specific service implementation (VAE, Upscaler)""" + + def __init__(self, scanner, update_service=None): + """Initialize Misc service + + Args: + scanner: Misc scanner instance + update_service: Optional service for remote update tracking. + """ + super().__init__("misc", scanner, MiscMetadata, update_service=update_service) + + async def format_response(self, misc_data: Dict) -> Dict: + """Format Misc data for API response""" + # Get sub_type from cache entry (new canonical field) + sub_type = misc_data.get("sub_type", "vae") + + return { + "model_name": misc_data["model_name"], + "file_name": misc_data["file_name"], + "preview_url": config.get_preview_static_url(misc_data.get("preview_url", "")), + "preview_nsfw_level": misc_data.get("preview_nsfw_level", 0), + "base_model": misc_data.get("base_model", ""), + "folder": misc_data["folder"], + "sha256": misc_data.get("sha256", ""), + "file_path": misc_data["file_path"].replace(os.sep, "/"), + "file_size": misc_data.get("size", 0), + "modified": misc_data.get("modified", ""), + "tags": misc_data.get("tags", []), + "from_civitai": misc_data.get("from_civitai", True), + "usage_count": misc_data.get("usage_count", 0), + "notes": misc_data.get("notes", ""), + "sub_type": sub_type, + "favorite": misc_data.get("favorite", False), + "update_available": bool(misc_data.get("update_available", False)), + "civitai": self.filter_civitai_data(misc_data.get("civitai", {}), minimal=True) + } + + def find_duplicate_hashes(self) -> Dict: + """Find Misc models with duplicate SHA256 hashes""" + return self.scanner._hash_index.get_duplicate_hashes() + + def find_duplicate_filenames(self) -> Dict: + """Find Misc models with conflicting filenames""" + return self.scanner._hash_index.get_duplicate_filenames() diff --git a/py/services/model_service_factory.py b/py/services/model_service_factory.py index 0e1da069..964b9f50 100644 --- a/py/services/model_service_factory.py +++ b/py/services/model_service_factory.py @@ -118,19 +118,24 @@ class ModelServiceFactory: def register_default_model_types(): - """Register the default model types (LoRA, Checkpoint, and Embedding)""" + """Register the default model types (LoRA, Checkpoint, Embedding, and Misc)""" from ..services.lora_service import LoraService from ..services.checkpoint_service import CheckpointService from ..services.embedding_service import EmbeddingService + from ..services.misc_service import MiscService from ..routes.lora_routes import LoraRoutes from ..routes.checkpoint_routes import CheckpointRoutes from ..routes.embedding_routes import EmbeddingRoutes - + from ..routes.misc_model_routes import MiscModelRoutes + # Register LoRA model type ModelServiceFactory.register_model_type('lora', LoraService, LoraRoutes) - + # Register Checkpoint model type ModelServiceFactory.register_model_type('checkpoint', CheckpointService, CheckpointRoutes) - + # Register Embedding model type - ModelServiceFactory.register_model_type('embedding', EmbeddingService, EmbeddingRoutes) \ No newline at end of file + ModelServiceFactory.register_model_type('embedding', EmbeddingService, EmbeddingRoutes) + + # Register Misc model type (VAE, Upscaler) + ModelServiceFactory.register_model_type('misc', MiscService, MiscModelRoutes) \ No newline at end of file diff --git a/py/services/service_registry.py b/py/services/service_registry.py index 4e3bea57..30da08a0 100644 --- a/py/services/service_registry.py +++ b/py/services/service_registry.py @@ -233,23 +233,44 @@ class ServiceRegistry: async def get_embedding_scanner(cls): """Get or create Embedding scanner instance""" service_name = "embedding_scanner" - + if service_name in cls._services: return cls._services[service_name] - + async with cls._get_lock(service_name): # Double-check after acquiring lock if service_name in cls._services: return cls._services[service_name] - + # Import here to avoid circular imports from .embedding_scanner import EmbeddingScanner - + scanner = await EmbeddingScanner.get_instance() cls._services[service_name] = scanner logger.debug(f"Created and registered {service_name}") return scanner - + + @classmethod + async def get_misc_scanner(cls): + """Get or create Misc scanner instance (VAE, Upscaler)""" + service_name = "misc_scanner" + + if service_name in cls._services: + return cls._services[service_name] + + async with cls._get_lock(service_name): + # Double-check after acquiring lock + if service_name in cls._services: + return cls._services[service_name] + + # Import here to avoid circular imports + from .misc_scanner import MiscScanner + + scanner = await MiscScanner.get_instance() + cls._services[service_name] = scanner + logger.debug(f"Created and registered {service_name}") + return scanner + @classmethod def clear_services(cls): """Clear all registered services - mainly for testing""" diff --git a/py/utils/constants.py b/py/utils/constants.py index e70ffe6b..db7437e4 100644 --- a/py/utils/constants.py +++ b/py/utils/constants.py @@ -49,6 +49,7 @@ SUPPORTED_MEDIA_EXTENSIONS = { VALID_LORA_SUB_TYPES = ["lora", "locon", "dora"] VALID_CHECKPOINT_SUB_TYPES = ["checkpoint", "diffusion_model"] VALID_EMBEDDING_SUB_TYPES = ["embedding"] +VALID_MISC_SUB_TYPES = ["vae", "upscaler"] # Backward compatibility alias VALID_LORA_TYPES = VALID_LORA_SUB_TYPES @@ -94,6 +95,7 @@ DEFAULT_PRIORITY_TAG_CONFIG = { "lora": ", ".join(CIVITAI_MODEL_TAGS), "checkpoint": ", ".join(CIVITAI_MODEL_TAGS), "embedding": ", ".join(CIVITAI_MODEL_TAGS), + "misc": ", ".join(CIVITAI_MODEL_TAGS), } # baseModel values from CivitAI that should be treated as diffusion models (unet) diff --git a/py/utils/models.py b/py/utils/models.py index c55bd29e..4e3aecef 100644 --- a/py/utils/models.py +++ b/py/utils/models.py @@ -219,7 +219,7 @@ class EmbeddingMetadata(BaseModelMetadata): file_name = file_info['name'] base_model = determine_base_model(version_info.get('baseModel', '')) sub_type = version_info.get('type', 'embedding') - + # Extract tags and description if available tags = [] description = "" @@ -228,7 +228,53 @@ class EmbeddingMetadata(BaseModelMetadata): tags = version_info['model']['tags'] if 'description' in version_info['model']: description = version_info['model']['description'] - + + return cls( + file_name=os.path.splitext(file_name)[0], + model_name=version_info.get('model').get('name', os.path.splitext(file_name)[0]), + file_path=save_path.replace(os.sep, '/'), + size=file_info.get('sizeKB', 0) * 1024, + modified=datetime.now().timestamp(), + sha256=file_info['hashes'].get('SHA256', '').lower(), + base_model=base_model, + preview_url=None, # Will be updated after preview download + preview_nsfw_level=0, + from_civitai=True, + civitai=version_info, + sub_type=sub_type, + tags=tags, + modelDescription=description + ) + +@dataclass +class MiscMetadata(BaseModelMetadata): + """Represents the metadata structure for a Misc model (VAE, Upscaler)""" + sub_type: str = "vae" + + @classmethod + def from_civitai_info(cls, version_info: Dict, file_info: Dict, save_path: str) -> 'MiscMetadata': + """Create MiscMetadata instance from Civitai version info""" + file_name = file_info['name'] + base_model = determine_base_model(version_info.get('baseModel', '')) + + # Determine sub_type from CivitAI model type + civitai_type = version_info.get('model', {}).get('type', '').lower() + if civitai_type == 'vae': + sub_type = 'vae' + elif civitai_type == 'upscaler': + sub_type = 'upscaler' + else: + sub_type = 'vae' # Default to vae + + # Extract tags and description if available + tags = [] + description = "" + if 'model' in version_info: + if 'tags' in version_info['model']: + tags = version_info['model']['tags'] + if 'description' in version_info['model']: + description = version_info['model']['description'] + return cls( file_name=os.path.splitext(file_name)[0], model_name=version_info.get('model').get('name', os.path.splitext(file_name)[0]), diff --git a/static/js/api/apiConfig.js b/static/js/api/apiConfig.js index 06c5b121..b8c4da0e 100644 --- a/static/js/api/apiConfig.js +++ b/static/js/api/apiConfig.js @@ -9,7 +9,8 @@ import { state } from '../state/index.js'; export const MODEL_TYPES = { LORA: 'loras', CHECKPOINT: 'checkpoints', - EMBEDDING: 'embeddings' // Future model type + EMBEDDING: 'embeddings', + MISC: 'misc' }; // Base API configuration for each model type @@ -40,6 +41,15 @@ export const MODEL_CONFIG = { supportsBulkOperations: true, supportsMove: true, templateName: 'embeddings.html' + }, + [MODEL_TYPES.MISC]: { + displayName: 'Misc', + singularName: 'misc', + defaultPageSize: 100, + supportsLetterFilter: false, + supportsBulkOperations: true, + supportsMove: true, + templateName: 'misc.html' } }; @@ -133,6 +143,11 @@ export const MODEL_SPECIFIC_ENDPOINTS = { }, [MODEL_TYPES.EMBEDDING]: { metadata: `/api/lm/${MODEL_TYPES.EMBEDDING}/metadata`, + }, + [MODEL_TYPES.MISC]: { + metadata: `/api/lm/${MODEL_TYPES.MISC}/metadata`, + vae_roots: `/api/lm/${MODEL_TYPES.MISC}/vae_roots`, + upscaler_roots: `/api/lm/${MODEL_TYPES.MISC}/upscaler_roots`, } }; diff --git a/static/js/api/miscApi.js b/static/js/api/miscApi.js new file mode 100644 index 00000000..c81fb810 --- /dev/null +++ b/static/js/api/miscApi.js @@ -0,0 +1,62 @@ +import { BaseModelApiClient } from './baseModelApi.js'; +import { getSessionItem } from '../utils/storageHelpers.js'; + +export class MiscApiClient extends BaseModelApiClient { + _addModelSpecificParams(params, pageState) { + const filterMiscHash = getSessionItem('recipe_to_misc_filterHash'); + const filterMiscHashes = getSessionItem('recipe_to_misc_filterHashes'); + + if (filterMiscHash) { + params.append('misc_hash', filterMiscHash); + } else if (filterMiscHashes) { + try { + if (Array.isArray(filterMiscHashes) && filterMiscHashes.length > 0) { + params.append('misc_hashes', filterMiscHashes.join(',')); + } + } catch (error) { + console.error('Error parsing misc hashes from session storage:', error); + } + } + + if (pageState.subType) { + params.append('sub_type', pageState.subType); + } + } + + async getMiscInfo(filePath) { + try { + const response = await fetch(this.apiConfig.endpoints.specific.info, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ file_path: filePath }) + }); + if (!response.ok) throw new Error('Failed to fetch misc info'); + return await response.json(); + } catch (error) { + console.error('Error fetching misc info:', error); + throw error; + } + } + + async getVaeRoots() { + try { + const response = await fetch(this.apiConfig.endpoints.specific.vae_roots, { method: 'GET' }); + if (!response.ok) throw new Error('Failed to fetch VAE roots'); + return await response.json(); + } catch (error) { + console.error('Error fetching VAE roots:', error); + throw error; + } + } + + async getUpscalerRoots() { + try { + const response = await fetch(this.apiConfig.endpoints.specific.upscaler_roots, { method: 'GET' }); + if (!response.ok) throw new Error('Failed to fetch upscaler roots'); + return await response.json(); + } catch (error) { + console.error('Error fetching upscaler roots:', error); + throw error; + } + } +} diff --git a/static/js/api/modelApiFactory.js b/static/js/api/modelApiFactory.js index 154b103b..aa2d1c71 100644 --- a/static/js/api/modelApiFactory.js +++ b/static/js/api/modelApiFactory.js @@ -1,6 +1,7 @@ import { LoraApiClient } from './loraApi.js'; import { CheckpointApiClient } from './checkpointApi.js'; import { EmbeddingApiClient } from './embeddingApi.js'; +import { MiscApiClient } from './miscApi.js'; import { MODEL_TYPES, isValidModelType } from './apiConfig.js'; import { state } from '../state/index.js'; @@ -12,6 +13,8 @@ export function createModelApiClient(modelType) { return new CheckpointApiClient(MODEL_TYPES.CHECKPOINT); case MODEL_TYPES.EMBEDDING: return new EmbeddingApiClient(MODEL_TYPES.EMBEDDING); + case MODEL_TYPES.MISC: + return new MiscApiClient(MODEL_TYPES.MISC); default: throw new Error(`Unsupported model type: ${modelType}`); } diff --git a/static/js/components/ContextMenu/MiscContextMenu.js b/static/js/components/ContextMenu/MiscContextMenu.js new file mode 100644 index 00000000..ef6c404c --- /dev/null +++ b/static/js/components/ContextMenu/MiscContextMenu.js @@ -0,0 +1,85 @@ +import { BaseContextMenu } from './BaseContextMenu.js'; +import { ModelContextMenuMixin } from './ModelContextMenuMixin.js'; +import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js'; +import { showDeleteModal, showExcludeModal } from '../../utils/modalUtils.js'; +import { moveManager } from '../../managers/MoveManager.js'; +import { i18n } from '../../i18n/index.js'; + +export class MiscContextMenu extends BaseContextMenu { + constructor() { + super('miscContextMenu', '.model-card'); + this.nsfwSelector = document.getElementById('nsfwLevelSelector'); + this.modelType = 'misc'; + this.resetAndReload = resetAndReload; + + this.initNSFWSelector(); + } + + // Implementation needed by the mixin + async saveModelMetadata(filePath, data) { + return getModelApiClient().saveModelMetadata(filePath, data); + } + + showMenu(x, y, card) { + super.showMenu(x, y, card); + + // Update the "Move to other root" label based on current model type + const moveOtherItem = this.menu.querySelector('[data-action="move-other"]'); + if (moveOtherItem) { + const currentType = card.dataset.sub_type || 'vae'; + const otherType = currentType === 'vae' ? 'upscaler' : 'vae'; + const typeLabel = i18n.t(`misc.modelTypes.${otherType}`); + moveOtherItem.innerHTML = ` ${i18n.t('misc.contextMenu.moveToOtherTypeFolder', { otherType: typeLabel })}`; + } + } + + handleMenuAction(action) { + // First try to handle with common actions + if (ModelContextMenuMixin.handleCommonMenuActions.call(this, action)) { + return; + } + + const apiClient = getModelApiClient(); + + // Otherwise handle misc-specific actions + switch (action) { + case 'details': + // Show misc details + this.currentCard.click(); + break; + case 'replace-preview': + // Add new action for replacing preview images + apiClient.replaceModelPreview(this.currentCard.dataset.filepath); + break; + case 'delete': + showDeleteModal(this.currentCard.dataset.filepath); + break; + case 'copyname': + // Copy misc model name + if (this.currentCard.querySelector('.fa-copy')) { + this.currentCard.querySelector('.fa-copy').click(); + } + break; + case 'refresh-metadata': + // Refresh metadata from CivitAI + apiClient.refreshSingleModelMetadata(this.currentCard.dataset.filepath); + break; + case 'move': + moveManager.showMoveModal(this.currentCard.dataset.filepath, this.currentCard.dataset.sub_type); + break; + case 'move-other': + { + const currentType = this.currentCard.dataset.sub_type || 'vae'; + const otherType = currentType === 'vae' ? 'upscaler' : 'vae'; + moveManager.showMoveModal(this.currentCard.dataset.filepath, otherType); + } + break; + case 'exclude': + showExcludeModal(this.currentCard.dataset.filepath); + break; + } + } +} + +// Mix in shared methods +Object.assign(MiscContextMenu.prototype, ModelContextMenuMixin); diff --git a/static/js/components/ContextMenu/index.js b/static/js/components/ContextMenu/index.js index 7d01c0f6..1d5f82fa 100644 --- a/static/js/components/ContextMenu/index.js +++ b/static/js/components/ContextMenu/index.js @@ -2,6 +2,7 @@ export { LoraContextMenu } from './LoraContextMenu.js'; export { RecipeContextMenu } from './RecipeContextMenu.js'; export { CheckpointContextMenu } from './CheckpointContextMenu.js'; export { EmbeddingContextMenu } from './EmbeddingContextMenu.js'; +export { MiscContextMenu } from './MiscContextMenu.js'; export { GlobalContextMenu } from './GlobalContextMenu.js'; export { ModelContextMenuMixin } from './ModelContextMenuMixin.js'; @@ -9,6 +10,7 @@ import { LoraContextMenu } from './LoraContextMenu.js'; import { RecipeContextMenu } from './RecipeContextMenu.js'; import { CheckpointContextMenu } from './CheckpointContextMenu.js'; import { EmbeddingContextMenu } from './EmbeddingContextMenu.js'; +import { MiscContextMenu } from './MiscContextMenu.js'; import { GlobalContextMenu } from './GlobalContextMenu.js'; // Factory method to create page-specific context menu instances @@ -22,6 +24,8 @@ export function createPageContextMenu(pageType) { return new CheckpointContextMenu(); case 'embeddings': return new EmbeddingContextMenu(); + case 'misc': + return new MiscContextMenu(); default: return null; } diff --git a/static/js/components/Header.js b/static/js/components/Header.js index 49be670f..62ae0abb 100644 --- a/static/js/components/Header.js +++ b/static/js/components/Header.js @@ -32,6 +32,7 @@ export class HeaderManager { if (path.includes('/checkpoints')) return 'checkpoints'; if (path.includes('/embeddings')) return 'embeddings'; if (path.includes('/statistics')) return 'statistics'; + if (path.includes('/misc')) return 'misc'; if (path.includes('/loras')) return 'loras'; return 'unknown'; } diff --git a/static/js/components/controls/MiscControls.js b/static/js/components/controls/MiscControls.js new file mode 100644 index 00000000..c1ea89af --- /dev/null +++ b/static/js/components/controls/MiscControls.js @@ -0,0 +1,119 @@ +// MiscControls.js - Specific implementation for the Misc (VAE/Upscaler) page +import { PageControls } from './PageControls.js'; +import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js'; +import { getSessionItem, removeSessionItem } from '../../utils/storageHelpers.js'; +import { downloadManager } from '../../managers/DownloadManager.js'; + +/** + * MiscControls class - Extends PageControls for Misc-specific functionality + */ +export class MiscControls extends PageControls { + constructor() { + // Initialize with 'misc' page type + super('misc'); + + // Register API methods specific to the Misc page + this.registerMiscAPI(); + + // Check for custom filters (e.g., from recipe navigation) + this.checkCustomFilters(); + } + + /** + * Register Misc-specific API methods + */ + registerMiscAPI() { + const miscAPI = { + // Core API functions + loadMoreModels: async (resetPage = false, updateFolders = false) => { + return await getModelApiClient().loadMoreWithVirtualScroll(resetPage, updateFolders); + }, + + resetAndReload: async (updateFolders = false) => { + return await resetAndReload(updateFolders); + }, + + refreshModels: async (fullRebuild = false) => { + return await getModelApiClient().refreshModels(fullRebuild); + }, + + // Add fetch from Civitai functionality for misc models + fetchFromCivitai: async () => { + return await getModelApiClient().fetchCivitaiMetadata(); + }, + + // Add show download modal functionality + showDownloadModal: () => { + downloadManager.showDownloadModal(); + }, + + toggleBulkMode: () => { + if (window.bulkManager) { + window.bulkManager.toggleBulkMode(); + } else { + console.error('Bulk manager not available'); + } + }, + + clearCustomFilter: async () => { + await this.clearCustomFilter(); + } + }; + + // Register the API + this.registerAPI(miscAPI); + } + + /** + * Check for custom filters sent from other pages (e.g., recipe modal) + */ + checkCustomFilters() { + const filterMiscHash = getSessionItem('recipe_to_misc_filterHash'); + const filterRecipeName = getSessionItem('filterMiscRecipeName'); + + if (filterMiscHash && filterRecipeName) { + const indicator = document.getElementById('customFilterIndicator'); + const filterText = indicator?.querySelector('.customFilterText'); + + if (indicator && filterText) { + indicator.classList.remove('hidden'); + + const displayText = `Viewing misc model from: ${filterRecipeName}`; + filterText.textContent = this._truncateText(displayText, 30); + filterText.setAttribute('title', displayText); + + const filterElement = indicator.querySelector('.filter-active'); + if (filterElement) { + filterElement.classList.add('animate'); + setTimeout(() => filterElement.classList.remove('animate'), 600); + } + } + } + } + + /** + * Clear misc custom filter and reload + */ + async clearCustomFilter() { + removeSessionItem('recipe_to_misc_filterHash'); + removeSessionItem('recipe_to_misc_filterHashes'); + removeSessionItem('filterMiscRecipeName'); + + const indicator = document.getElementById('customFilterIndicator'); + if (indicator) { + indicator.classList.add('hidden'); + } + + await resetAndReload(); + } + + /** + * Helper to truncate text with ellipsis + * @param {string} text + * @param {number} maxLength + * @returns {string} + */ + _truncateText(text, maxLength) { + return text.length > maxLength ? `${text.substring(0, maxLength - 3)}...` : text; + } +} diff --git a/static/js/components/controls/index.js b/static/js/components/controls/index.js index 97f1ca91..33b67bd6 100644 --- a/static/js/components/controls/index.js +++ b/static/js/components/controls/index.js @@ -3,13 +3,14 @@ import { PageControls } from './PageControls.js'; import { LorasControls } from './LorasControls.js'; import { CheckpointsControls } from './CheckpointsControls.js'; import { EmbeddingsControls } from './EmbeddingsControls.js'; +import { MiscControls } from './MiscControls.js'; // Export the classes -export { PageControls, LorasControls, CheckpointsControls, EmbeddingsControls }; +export { PageControls, LorasControls, CheckpointsControls, EmbeddingsControls, MiscControls }; /** * Factory function to create the appropriate controls based on page type - * @param {string} pageType - The type of page ('loras', 'checkpoints', or 'embeddings') + * @param {string} pageType - The type of page ('loras', 'checkpoints', 'embeddings', or 'misc') * @returns {PageControls} - The appropriate controls instance */ export function createPageControls(pageType) { @@ -19,6 +20,8 @@ export function createPageControls(pageType) { return new CheckpointsControls(); } else if (pageType === 'embeddings') { return new EmbeddingsControls(); + } else if (pageType === 'misc') { + return new MiscControls(); } else { console.error(`Unknown page type: ${pageType}`); return null; diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js index f959424e..3c968045 100644 --- a/static/js/components/shared/ModelCard.js +++ b/static/js/components/shared/ModelCard.js @@ -214,6 +214,52 @@ function handleSendToWorkflow(card, replaceMode, modelType) { missingNodesMessage, missingTargetMessage, }); + } else if (modelType === MODEL_TYPES.MISC) { + const modelPath = card.dataset.filepath; + if (!modelPath) { + const message = translate('modelCard.sendToWorkflow.missingPath', {}, 'Unable to determine model path for this card'); + showToast(message, {}, 'error'); + return; + } + + const subtype = (card.dataset.sub_type || 'vae').toLowerCase(); + const isVae = subtype === 'vae'; + const widgetName = isVae ? 'vae_name' : 'model_name'; + const actionTypeText = translate( + isVae ? 'uiHelpers.nodeSelector.vae' : 'uiHelpers.nodeSelector.upscaler', + {}, + isVae ? 'VAE' : 'Upscaler' + ); + const successMessage = translate( + isVae ? 'uiHelpers.workflow.vaeUpdated' : 'uiHelpers.workflow.upscalerUpdated', + {}, + isVae ? 'VAE updated in workflow' : 'Upscaler updated in workflow' + ); + const failureMessage = translate( + isVae ? 'uiHelpers.workflow.vaeFailed' : 'uiHelpers.workflow.upscalerFailed', + {}, + isVae ? 'Failed to update VAE node' : 'Failed to update upscaler node' + ); + const missingNodesMessage = translate( + 'uiHelpers.workflow.noMatchingNodes', + {}, + 'No compatible nodes available in the current workflow' + ); + const missingTargetMessage = translate( + 'uiHelpers.workflow.noTargetNodeSelected', + {}, + 'No target node selected' + ); + + sendModelPathToWorkflow(modelPath, { + widgetName, + collectionType: MODEL_TYPES.MISC, + actionTypeText, + successMessage, + failureMessage, + missingNodesMessage, + missingTargetMessage, + }); } else { showToast('modelCard.sendToWorkflow.checkpointNotImplemented', {}, 'info'); } @@ -230,6 +276,10 @@ function handleCopyAction(card, modelType) { } else if (modelType === MODEL_TYPES.EMBEDDING) { const embeddingName = card.dataset.file_name; copyToClipboard(embeddingName, 'Embedding name copied'); + } else if (modelType === MODEL_TYPES.MISC) { + const miscName = card.dataset.file_name; + const message = translate('modelCard.actions.miscNameCopied', {}, 'Model name copied'); + copyToClipboard(miscName, message); } } diff --git a/static/js/core.js b/static/js/core.js index 27175114..fea09a30 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -99,7 +99,7 @@ export class AppCore { initializePageFeatures() { const pageType = this.getPageType(); - if (['loras', 'recipes', 'checkpoints', 'embeddings'].includes(pageType)) { + if (['loras', 'recipes', 'checkpoints', 'embeddings', 'misc'].includes(pageType)) { this.initializeContextMenus(pageType); initializeInfiniteScroll(pageType); } diff --git a/static/js/managers/BulkManager.js b/static/js/managers/BulkManager.js index ed307b0f..38f29b34 100644 --- a/static/js/managers/BulkManager.js +++ b/static/js/managers/BulkManager.js @@ -64,6 +64,17 @@ export class BulkManager { deleteAll: true, setContentRating: true }, + [MODEL_TYPES.MISC]: { + addTags: true, + sendToWorkflow: false, + copyAll: false, + refreshAll: true, + checkUpdates: true, + moveAll: true, + autoOrganize: true, + deleteAll: true, + setContentRating: true + }, recipes: { addTags: false, sendToWorkflow: false, diff --git a/static/js/misc.js b/static/js/misc.js new file mode 100644 index 00000000..9fc1f928 --- /dev/null +++ b/static/js/misc.js @@ -0,0 +1,51 @@ +import { appCore } from './core.js'; +import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js'; +import { createPageControls } from './components/controls/index.js'; +import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js'; +import { MODEL_TYPES } from './api/apiConfig.js'; + +// Initialize the Misc (VAE/Upscaler) page +export class MiscPageManager { + constructor() { + // Initialize page controls + this.pageControls = createPageControls(MODEL_TYPES.MISC); + + // Initialize the ModelDuplicatesManager + this.duplicatesManager = new ModelDuplicatesManager(this, MODEL_TYPES.MISC); + + // Expose only necessary functions to global scope + this._exposeRequiredGlobalFunctions(); + } + + _exposeRequiredGlobalFunctions() { + // Minimal set of functions that need to remain global + window.confirmDelete = confirmDelete; + window.closeDeleteModal = closeDeleteModal; + window.confirmExclude = confirmExclude; + window.closeExcludeModal = closeExcludeModal; + + // Expose duplicates manager + window.modelDuplicatesManager = this.duplicatesManager; + } + + async initialize() { + // Initialize common page features (including context menus) + appCore.initializePageFeatures(); + + console.log('Misc Manager initialized'); + } +} + +export async function initializeMiscPage() { + // Initialize core application + await appCore.initialize(); + + // Initialize misc page + const miscPage = new MiscPageManager(); + await miscPage.initialize(); + + return miscPage; +} + +// Initialize everything when DOM is ready +document.addEventListener('DOMContentLoaded', initializeMiscPage); diff --git a/static/js/state/index.js b/static/js/state/index.js index 0a61e7b5..0dd0acf0 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -177,6 +177,35 @@ export const state = { showFavoritesOnly: false, showUpdateAvailableOnly: false, duplicatesMode: false, + }, + + [MODEL_TYPES.MISC]: { + currentPage: 1, + isLoading: false, + hasMore: true, + sortBy: 'name', + activeFolder: getStorageItem(`${MODEL_TYPES.MISC}_activeFolder`), + previewVersions: new Map(), + searchManager: null, + searchOptions: { + filename: true, + modelname: true, + creator: false, + recursive: getStorageItem(`${MODEL_TYPES.MISC}_recursiveSearch`, true), + }, + filters: { + baseModel: [], + tags: {}, + license: {}, + modelTypes: [] + }, + bulkMode: false, + selectedModels: new Set(), + metadataCache: new Map(), + showFavoritesOnly: false, + showUpdateAvailableOnly: false, + duplicatesMode: false, + subType: 'vae' } }, diff --git a/static/js/utils/constants.js b/static/js/utils/constants.js index 8870a4c6..6625e505 100644 --- a/static/js/utils/constants.js +++ b/static/js/utils/constants.js @@ -68,6 +68,9 @@ export const MODEL_SUBTYPE_DISPLAY_NAMES = { diffusion_model: "Diffusion Model", // Embedding sub-types embedding: "Embedding", + // Misc sub-types + vae: "VAE", + upscaler: "Upscaler", }; // Backward compatibility alias @@ -81,6 +84,8 @@ export const MODEL_SUBTYPE_ABBREVIATIONS = { checkpoint: "CKPT", diffusion_model: "DM", embedding: "EMB", + vae: "VAE", + upscaler: "UP", }; export function getSubTypeAbbreviation(subType) { diff --git a/static/js/utils/infiniteScroll.js b/static/js/utils/infiniteScroll.js index e2edb55c..6c13a4a7 100644 --- a/static/js/utils/infiniteScroll.js +++ b/static/js/utils/infiniteScroll.js @@ -28,10 +28,9 @@ async function getCardCreator(pageType) { // Function to get the appropriate data fetcher based on page type async function getDataFetcher(pageType) { - if (pageType === 'loras' || pageType === 'embeddings' || pageType === 'checkpoints') { + if (pageType === 'loras' || pageType === 'embeddings' || pageType === 'checkpoints' || pageType === 'misc') { return (page = 1, pageSize = 100) => getModelApiClient().fetchModelsPage(page, pageSize); } else if (pageType === 'recipes') { - // Import the recipeApi module and use the fetchRecipesPage function const { fetchRecipesPage } = await import('../api/recipeApi.js'); return fetchRecipesPage; } diff --git a/templates/components/header.html b/templates/components/header.html index b980c924..16a983f7 100644 --- a/templates/components/header.html +++ b/templates/components/header.html @@ -13,6 +13,8 @@ {% set current_page = 'checkpoints' %} {% elif current_path.startswith('/embeddings') %} {% set current_page = 'embeddings' %} + {% elif current_path.startswith('/misc') %} + {% set current_page = 'misc' %} {% elif current_path.startswith('/statistics') %} {% set current_page = 'statistics' %} {% else %} @@ -38,6 +40,10 @@ id="embeddingsNavItem"> {{ t('header.navigation.embeddings') }} + + {{ t('header.navigation.misc') }} + {{ t('header.navigation.statistics') }} @@ -116,6 +122,11 @@
{{ t('header.search.filters.modelname') }}
{{ t('header.search.filters.tags') }}
{{ t('header.search.filters.creator') }}
+ {% elif request.path == '/misc' %} +
{{ t('header.search.filters.filename') }}
+
{{ t('header.search.filters.modelname') }}
+
{{ t('header.search.filters.tags') }}
+
{{ t('header.search.filters.creator') }}
{% else %}
{{ t('header.search.filters.filename') }}
@@ -156,7 +167,7 @@
{{ t('common.status.loading') }}
- {% if current_page == 'loras' or current_page == 'checkpoints' %} + {% if current_page == 'loras' or current_page == 'checkpoints' or current_page == 'misc' %}

{{ t('header.filter.modelTypes') }}

diff --git a/templates/misc.html b/templates/misc.html new file mode 100644 index 00000000..4eaf4062 --- /dev/null +++ b/templates/misc.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block title %}{{ t('misc.title') }}{% endblock %} +{% block page_id %}misc{% endblock %} + +{% block init_title %}{{ t('initialization.misc.title') }}{% endblock %} +{% block init_message %}{{ t('initialization.misc.message') }}{% endblock %} +{% block init_check_url %}/api/lm/misc/list?page=1&page_size=1{% endblock %} + +{% block additional_components %} + + +{% endblock %} + +{% block content %} + {% include 'components/controls.html' %} + {% include 'components/duplicates_banner.html' %} + {% include 'components/folder_sidebar.html' %} + + +
+ +
+{% endblock %} + +{% block overlay %} +
+{% endblock %} + +{% block main_script %} + +{% endblock %}