mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 15:15:44 -03:00
Compare commits
15 Commits
61c31ecbd0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de3d0571f8 | ||
|
|
6f2a01dc86 | ||
|
|
c5c1b8fd2a | ||
|
|
e97648c70b | ||
|
|
8b85e083e2 | ||
|
|
9112cd3b62 | ||
|
|
7df4e8d037 | ||
|
|
4000b7f7e7 | ||
|
|
76c15105e6 | ||
|
|
b11c90e19b | ||
|
|
9f5d2d0c18 | ||
|
|
a0dc5229f4 | ||
|
|
a32325402e | ||
|
|
05ebd7493d | ||
|
|
90986bd795 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,6 +14,7 @@ model_cache/
|
|||||||
|
|
||||||
# agent
|
# agent
|
||||||
.opencode/
|
.opencode/
|
||||||
|
.claude/
|
||||||
|
|
||||||
# Vue widgets development cache (but keep build output)
|
# Vue widgets development cache (but keep build output)
|
||||||
vue-widgets/node_modules/
|
vue-widgets/node_modules/
|
||||||
|
|||||||
125
locales/de.json
125
locales/de.json
@@ -645,6 +645,8 @@
|
|||||||
"root": "Stammverzeichnis",
|
"root": "Stammverzeichnis",
|
||||||
"browseFolders": "Ordner durchsuchen:",
|
"browseFolders": "Ordner durchsuchen:",
|
||||||
"downloadAndSaveRecipe": "Herunterladen & Rezept speichern",
|
"downloadAndSaveRecipe": "Herunterladen & Rezept speichern",
|
||||||
|
"importRecipeOnly": "Nur Rezept importieren",
|
||||||
|
"importAndDownload": "Importieren & Herunterladen",
|
||||||
"downloadMissingLoras": "Fehlende LoRAs herunterladen",
|
"downloadMissingLoras": "Fehlende LoRAs herunterladen",
|
||||||
"saveRecipe": "Rezept speichern",
|
"saveRecipe": "Rezept speichern",
|
||||||
"loraCountInfo": "({existing}/{total} in Bibliothek)",
|
"loraCountInfo": "({existing}/{total} in Bibliothek)",
|
||||||
@@ -732,61 +734,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "[TODO: Translate] Batch Import Recipes",
|
"title": "Batch Import Recipes",
|
||||||
"action": "[TODO: Translate] Batch Import",
|
"action": "Batch Import",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL List",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "Directory",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
||||||
"directoryDescription": "[TODO: Translate] Enter a directory path to import all images from that folder.",
|
"directoryDescription": "Enter a directory path to import all images from that folder.",
|
||||||
"urlsLabel": "[TODO: Translate] Image URLs or Local Paths",
|
"urlsLabel": "Image URLs or Local Paths",
|
||||||
"urlsPlaceholder": "[TODO: Translate] https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "Enter one URL or path per line",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "Directory Path",
|
||||||
"directoryPlaceholder": "[TODO: Translate] /path/to/images/folder",
|
"directoryPlaceholder": "/path/to/images/folder",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "Browse",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "Include subdirectories",
|
||||||
"tagsOptional": "[TODO: Translate] Tags (optional, applied to all recipes)",
|
"tagsOptional": "Tags (optional, applied to all recipes)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "Enter tags separated by commas",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "Tags will be added to all imported recipes",
|
||||||
"skipNoMetadata": "[TODO: Translate] Skip images without metadata",
|
"skipNoMetadata": "Skip images without metadata",
|
||||||
"skipNoMetadataHelp": "[TODO: Translate] Images without LoRA metadata will be skipped automatically.",
|
"skipNoMetadataHelp": "Images without LoRA metadata will be skipped automatically.",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "Start Import",
|
||||||
"startImport": "[TODO: Translate] Start Import",
|
"startImport": "Start Import",
|
||||||
"importing": "[TODO: Translate] Importing...",
|
"importing": "Importing...",
|
||||||
"progress": "[TODO: Translate] Progress",
|
"progress": "Progress",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "Total",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "Success",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "Failed",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "Skipped",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "Current",
|
||||||
"currentItem": "[TODO: Translate] Current",
|
"currentItem": "Current",
|
||||||
"preparing": "[TODO: Translate] Preparing...",
|
"preparing": "Preparing...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "Cancel",
|
||||||
"cancelImport": "[TODO: Translate] Cancel",
|
"cancelImport": "Cancel",
|
||||||
"cancelled": "[TODO: Translate] Import cancelled",
|
"cancelled": "Import cancelled",
|
||||||
"completed": "[TODO: Translate] Import completed",
|
"completed": "Import completed",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "Completed with errors",
|
||||||
"completedSuccess": "[TODO: Translate] Successfully imported {count} recipe(s)",
|
"completedSuccess": "Successfully imported {count} recipe(s)",
|
||||||
"successCount": "[TODO: Translate] Successful",
|
"successCount": "Successful",
|
||||||
"failedCount": "[TODO: Translate] Failed",
|
"failedCount": "Failed",
|
||||||
"skippedCount": "[TODO: Translate] Skipped",
|
"skippedCount": "Skipped",
|
||||||
"totalProcessed": "[TODO: Translate] Total processed",
|
"totalProcessed": "Total processed",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "View Details",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "New Import",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "Please enter the directory path manually. File browser is not available in this browser.",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "File browser not available. Please enter the directory path manually.",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "Back to parent directory",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "Folders",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} folders",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "Image Files",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "images",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} images",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "Select This Folder",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "[TODO: Translate] Please enter at least one URL or path",
|
"enterUrls": "Please enter at least one URL or path",
|
||||||
"enterDirectory": "[TODO: Translate] Please enter a directory path",
|
"enterDirectory": "Please enter a directory path",
|
||||||
"startFailed": "[TODO: Translate] Failed to start import: {message}"
|
"startFailed": "Failed to start import: {message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "Verarbeitungsfehler: {message}",
|
"processingError": "Verarbeitungsfehler: {message}",
|
||||||
"folderBrowserError": "Fehler beim Laden des Ordner-Browsers: {message}",
|
"folderBrowserError": "Fehler beim Laden des Ordner-Browsers: {message}",
|
||||||
"recipeSaveFailed": "Fehler beim Speichern des Rezepts: {error}",
|
"recipeSaveFailed": "Fehler beim Speichern des Rezepts: {error}",
|
||||||
|
"recipeSaved": "Recipe saved successfully",
|
||||||
"importFailed": "Import fehlgeschlagen: {message}",
|
"importFailed": "Import fehlgeschlagen: {message}",
|
||||||
"folderTreeFailed": "Fehler beim Laden des Ordnerbaums",
|
"folderTreeFailed": "Fehler beim Laden des Ordnerbaums",
|
||||||
"folderTreeError": "Fehler beim Laden des Ordnerbaums",
|
"folderTreeError": "Fehler beim Laden des Ordnerbaums",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "Failed to start batch import: {message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "Cancelling batch import...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "Failed to cancel batch import: {message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "Please enter a directory path",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "Keine Modelle ausgewählt",
|
"noModelsSelected": "Keine Modelle ausgewählt",
|
||||||
|
|||||||
@@ -645,6 +645,8 @@
|
|||||||
"root": "Root",
|
"root": "Root",
|
||||||
"browseFolders": "Browse Folders:",
|
"browseFolders": "Browse Folders:",
|
||||||
"downloadAndSaveRecipe": "Download & Save Recipe",
|
"downloadAndSaveRecipe": "Download & Save Recipe",
|
||||||
|
"importRecipeOnly": "Import Recipe Only",
|
||||||
|
"importAndDownload": "Import & Download",
|
||||||
"downloadMissingLoras": "Download Missing LoRAs",
|
"downloadMissingLoras": "Download Missing LoRAs",
|
||||||
"saveRecipe": "Save Recipe",
|
"saveRecipe": "Save Recipe",
|
||||||
"loraCountInfo": "({existing}/{total} in library)",
|
"loraCountInfo": "({existing}/{total} in library)",
|
||||||
@@ -1495,6 +1497,7 @@
|
|||||||
"processingError": "Processing error: {message}",
|
"processingError": "Processing error: {message}",
|
||||||
"folderBrowserError": "Error loading folder browser: {message}",
|
"folderBrowserError": "Error loading folder browser: {message}",
|
||||||
"recipeSaveFailed": "Failed to save recipe: {error}",
|
"recipeSaveFailed": "Failed to save recipe: {error}",
|
||||||
|
"recipeSaved": "Recipe saved successfully",
|
||||||
"importFailed": "Import failed: {message}",
|
"importFailed": "Import failed: {message}",
|
||||||
"folderTreeFailed": "Failed to load folder tree",
|
"folderTreeFailed": "Failed to load folder tree",
|
||||||
"folderTreeError": "Error loading folder tree",
|
"folderTreeError": "Error loading folder tree",
|
||||||
|
|||||||
125
locales/es.json
125
locales/es.json
@@ -645,6 +645,8 @@
|
|||||||
"root": "Raíz",
|
"root": "Raíz",
|
||||||
"browseFolders": "Explorar carpetas:",
|
"browseFolders": "Explorar carpetas:",
|
||||||
"downloadAndSaveRecipe": "Descargar y guardar receta",
|
"downloadAndSaveRecipe": "Descargar y guardar receta",
|
||||||
|
"importRecipeOnly": "Importar solo la receta",
|
||||||
|
"importAndDownload": "Importar y descargar",
|
||||||
"downloadMissingLoras": "Descargar LoRAs faltantes",
|
"downloadMissingLoras": "Descargar LoRAs faltantes",
|
||||||
"saveRecipe": "Guardar receta",
|
"saveRecipe": "Guardar receta",
|
||||||
"loraCountInfo": "({existing}/{total} en la biblioteca)",
|
"loraCountInfo": "({existing}/{total} en la biblioteca)",
|
||||||
@@ -732,61 +734,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "[TODO: Translate] Batch Import Recipes",
|
"title": "Batch Import Recipes",
|
||||||
"action": "[TODO: Translate] Batch Import",
|
"action": "Batch Import",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL List",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "Directory",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
||||||
"directoryDescription": "[TODO: Translate] Enter a directory path to import all images from that folder.",
|
"directoryDescription": "Enter a directory path to import all images from that folder.",
|
||||||
"urlsLabel": "[TODO: Translate] Image URLs or Local Paths",
|
"urlsLabel": "Image URLs or Local Paths",
|
||||||
"urlsPlaceholder": "[TODO: Translate] https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "Enter one URL or path per line",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "Directory Path",
|
||||||
"directoryPlaceholder": "[TODO: Translate] /path/to/images/folder",
|
"directoryPlaceholder": "/path/to/images/folder",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "Browse",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "Include subdirectories",
|
||||||
"tagsOptional": "[TODO: Translate] Tags (optional, applied to all recipes)",
|
"tagsOptional": "Tags (optional, applied to all recipes)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "Enter tags separated by commas",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "Tags will be added to all imported recipes",
|
||||||
"skipNoMetadata": "[TODO: Translate] Skip images without metadata",
|
"skipNoMetadata": "Skip images without metadata",
|
||||||
"skipNoMetadataHelp": "[TODO: Translate] Images without LoRA metadata will be skipped automatically.",
|
"skipNoMetadataHelp": "Images without LoRA metadata will be skipped automatically.",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "Start Import",
|
||||||
"startImport": "[TODO: Translate] Start Import",
|
"startImport": "Start Import",
|
||||||
"importing": "[TODO: Translate] Importing...",
|
"importing": "Importing...",
|
||||||
"progress": "[TODO: Translate] Progress",
|
"progress": "Progress",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "Total",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "Success",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "Failed",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "Skipped",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "Current",
|
||||||
"currentItem": "[TODO: Translate] Current",
|
"currentItem": "Current",
|
||||||
"preparing": "[TODO: Translate] Preparing...",
|
"preparing": "Preparing...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "Cancel",
|
||||||
"cancelImport": "[TODO: Translate] Cancel",
|
"cancelImport": "Cancel",
|
||||||
"cancelled": "[TODO: Translate] Import cancelled",
|
"cancelled": "Import cancelled",
|
||||||
"completed": "[TODO: Translate] Import completed",
|
"completed": "Import completed",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "Completed with errors",
|
||||||
"completedSuccess": "[TODO: Translate] Successfully imported {count} recipe(s)",
|
"completedSuccess": "Successfully imported {count} recipe(s)",
|
||||||
"successCount": "[TODO: Translate] Successful",
|
"successCount": "Successful",
|
||||||
"failedCount": "[TODO: Translate] Failed",
|
"failedCount": "Failed",
|
||||||
"skippedCount": "[TODO: Translate] Skipped",
|
"skippedCount": "Skipped",
|
||||||
"totalProcessed": "[TODO: Translate] Total processed",
|
"totalProcessed": "Total processed",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "View Details",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "New Import",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "Please enter the directory path manually. File browser is not available in this browser.",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "File browser not available. Please enter the directory path manually.",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "Back to parent directory",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "Folders",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} folders",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "Image Files",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "images",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} images",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "Select This Folder",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "[TODO: Translate] Please enter at least one URL or path",
|
"enterUrls": "Please enter at least one URL or path",
|
||||||
"enterDirectory": "[TODO: Translate] Please enter a directory path",
|
"enterDirectory": "Please enter a directory path",
|
||||||
"startFailed": "[TODO: Translate] Failed to start import: {message}"
|
"startFailed": "Failed to start import: {message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "Error de procesamiento: {message}",
|
"processingError": "Error de procesamiento: {message}",
|
||||||
"folderBrowserError": "Error cargando explorador de carpetas: {message}",
|
"folderBrowserError": "Error cargando explorador de carpetas: {message}",
|
||||||
"recipeSaveFailed": "Error al guardar receta: {error}",
|
"recipeSaveFailed": "Error al guardar receta: {error}",
|
||||||
|
"recipeSaved": "Recipe saved successfully",
|
||||||
"importFailed": "Importación falló: {message}",
|
"importFailed": "Importación falló: {message}",
|
||||||
"folderTreeFailed": "Error al cargar árbol de carpetas",
|
"folderTreeFailed": "Error al cargar árbol de carpetas",
|
||||||
"folderTreeError": "Error cargando árbol de carpetas",
|
"folderTreeError": "Error cargando árbol de carpetas",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "Failed to start batch import: {message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "Cancelling batch import...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "Failed to cancel batch import: {message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "Please enter a directory path",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "No hay modelos seleccionados",
|
"noModelsSelected": "No hay modelos seleccionados",
|
||||||
|
|||||||
125
locales/fr.json
125
locales/fr.json
@@ -645,6 +645,8 @@
|
|||||||
"root": "Racine",
|
"root": "Racine",
|
||||||
"browseFolders": "Parcourir les dossiers :",
|
"browseFolders": "Parcourir les dossiers :",
|
||||||
"downloadAndSaveRecipe": "Télécharger et sauvegarder la recipe",
|
"downloadAndSaveRecipe": "Télécharger et sauvegarder la recipe",
|
||||||
|
"importRecipeOnly": "Importer uniquement la recette",
|
||||||
|
"importAndDownload": "Importer et télécharger",
|
||||||
"downloadMissingLoras": "Télécharger les LoRAs manquants",
|
"downloadMissingLoras": "Télécharger les LoRAs manquants",
|
||||||
"saveRecipe": "Sauvegarder la recipe",
|
"saveRecipe": "Sauvegarder la recipe",
|
||||||
"loraCountInfo": "({existing}/{total} dans la bibliothèque)",
|
"loraCountInfo": "({existing}/{total} dans la bibliothèque)",
|
||||||
@@ -732,61 +734,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "[TODO: Translate] Batch Import Recipes",
|
"title": "Batch Import Recipes",
|
||||||
"action": "[TODO: Translate] Batch Import",
|
"action": "Batch Import",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL List",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "Directory",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
||||||
"directoryDescription": "[TODO: Translate] Enter a directory path to import all images from that folder.",
|
"directoryDescription": "Enter a directory path to import all images from that folder.",
|
||||||
"urlsLabel": "[TODO: Translate] Image URLs or Local Paths",
|
"urlsLabel": "Image URLs or Local Paths",
|
||||||
"urlsPlaceholder": "[TODO: Translate] https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "Enter one URL or path per line",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "Directory Path",
|
||||||
"directoryPlaceholder": "[TODO: Translate] /path/to/images/folder",
|
"directoryPlaceholder": "/path/to/images/folder",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "Browse",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "Include subdirectories",
|
||||||
"tagsOptional": "[TODO: Translate] Tags (optional, applied to all recipes)",
|
"tagsOptional": "Tags (optional, applied to all recipes)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "Enter tags separated by commas",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "Tags will be added to all imported recipes",
|
||||||
"skipNoMetadata": "[TODO: Translate] Skip images without metadata",
|
"skipNoMetadata": "Skip images without metadata",
|
||||||
"skipNoMetadataHelp": "[TODO: Translate] Images without LoRA metadata will be skipped automatically.",
|
"skipNoMetadataHelp": "Images without LoRA metadata will be skipped automatically.",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "Start Import",
|
||||||
"startImport": "[TODO: Translate] Start Import",
|
"startImport": "Start Import",
|
||||||
"importing": "[TODO: Translate] Importing...",
|
"importing": "Importing...",
|
||||||
"progress": "[TODO: Translate] Progress",
|
"progress": "Progress",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "Total",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "Success",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "Failed",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "Skipped",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "Current",
|
||||||
"currentItem": "[TODO: Translate] Current",
|
"currentItem": "Current",
|
||||||
"preparing": "[TODO: Translate] Preparing...",
|
"preparing": "Preparing...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "Cancel",
|
||||||
"cancelImport": "[TODO: Translate] Cancel",
|
"cancelImport": "Cancel",
|
||||||
"cancelled": "[TODO: Translate] Import cancelled",
|
"cancelled": "Import cancelled",
|
||||||
"completed": "[TODO: Translate] Import completed",
|
"completed": "Import completed",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "Completed with errors",
|
||||||
"completedSuccess": "[TODO: Translate] Successfully imported {count} recipe(s)",
|
"completedSuccess": "Successfully imported {count} recipe(s)",
|
||||||
"successCount": "[TODO: Translate] Successful",
|
"successCount": "Successful",
|
||||||
"failedCount": "[TODO: Translate] Failed",
|
"failedCount": "Failed",
|
||||||
"skippedCount": "[TODO: Translate] Skipped",
|
"skippedCount": "Skipped",
|
||||||
"totalProcessed": "[TODO: Translate] Total processed",
|
"totalProcessed": "Total processed",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "View Details",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "New Import",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "Please enter the directory path manually. File browser is not available in this browser.",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "File browser not available. Please enter the directory path manually.",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "Back to parent directory",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "Folders",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} folders",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "Image Files",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "images",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} images",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "Select This Folder",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "[TODO: Translate] Please enter at least one URL or path",
|
"enterUrls": "Please enter at least one URL or path",
|
||||||
"enterDirectory": "[TODO: Translate] Please enter a directory path",
|
"enterDirectory": "Please enter a directory path",
|
||||||
"startFailed": "[TODO: Translate] Failed to start import: {message}"
|
"startFailed": "Failed to start import: {message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "Erreur de traitement : {message}",
|
"processingError": "Erreur de traitement : {message}",
|
||||||
"folderBrowserError": "Erreur lors du chargement du navigateur de dossiers : {message}",
|
"folderBrowserError": "Erreur lors du chargement du navigateur de dossiers : {message}",
|
||||||
"recipeSaveFailed": "Échec de la sauvegarde de la recipe : {error}",
|
"recipeSaveFailed": "Échec de la sauvegarde de la recipe : {error}",
|
||||||
|
"recipeSaved": "Recipe saved successfully",
|
||||||
"importFailed": "Échec de l'importation : {message}",
|
"importFailed": "Échec de l'importation : {message}",
|
||||||
"folderTreeFailed": "Échec du chargement de l'arborescence des dossiers",
|
"folderTreeFailed": "Échec du chargement de l'arborescence des dossiers",
|
||||||
"folderTreeError": "Erreur lors du chargement de l'arborescence des dossiers",
|
"folderTreeError": "Erreur lors du chargement de l'arborescence des dossiers",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "Failed to start batch import: {message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "Cancelling batch import...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "Failed to cancel batch import: {message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "Please enter a directory path",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "Aucun modèle sélectionné",
|
"noModelsSelected": "Aucun modèle sélectionné",
|
||||||
|
|||||||
125
locales/he.json
125
locales/he.json
@@ -645,6 +645,8 @@
|
|||||||
"root": "שורש",
|
"root": "שורש",
|
||||||
"browseFolders": "דפדף בתיקיות:",
|
"browseFolders": "דפדף בתיקיות:",
|
||||||
"downloadAndSaveRecipe": "הורד ושמור מתכון",
|
"downloadAndSaveRecipe": "הורד ושמור מתכון",
|
||||||
|
"importRecipeOnly": "יבא רק מתכון",
|
||||||
|
"importAndDownload": "יבא והורד",
|
||||||
"downloadMissingLoras": "הורד LoRAs חסרים",
|
"downloadMissingLoras": "הורד LoRAs חסרים",
|
||||||
"saveRecipe": "שמור מתכון",
|
"saveRecipe": "שמור מתכון",
|
||||||
"loraCountInfo": "({existing}/{total} בספרייה)",
|
"loraCountInfo": "({existing}/{total} בספרייה)",
|
||||||
@@ -732,61 +734,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "[TODO: Translate] Batch Import Recipes",
|
"title": "Batch Import Recipes",
|
||||||
"action": "[TODO: Translate] Batch Import",
|
"action": "Batch Import",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL List",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "Directory",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
||||||
"directoryDescription": "[TODO: Translate] Enter a directory path to import all images from that folder.",
|
"directoryDescription": "Enter a directory path to import all images from that folder.",
|
||||||
"urlsLabel": "[TODO: Translate] Image URLs or Local Paths",
|
"urlsLabel": "Image URLs or Local Paths",
|
||||||
"urlsPlaceholder": "[TODO: Translate] https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "Enter one URL or path per line",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "Directory Path",
|
||||||
"directoryPlaceholder": "[TODO: Translate] /path/to/images/folder",
|
"directoryPlaceholder": "/path/to/images/folder",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "Browse",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "Include subdirectories",
|
||||||
"tagsOptional": "[TODO: Translate] Tags (optional, applied to all recipes)",
|
"tagsOptional": "Tags (optional, applied to all recipes)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "Enter tags separated by commas",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "Tags will be added to all imported recipes",
|
||||||
"skipNoMetadata": "[TODO: Translate] Skip images without metadata",
|
"skipNoMetadata": "Skip images without metadata",
|
||||||
"skipNoMetadataHelp": "[TODO: Translate] Images without LoRA metadata will be skipped automatically.",
|
"skipNoMetadataHelp": "Images without LoRA metadata will be skipped automatically.",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "Start Import",
|
||||||
"startImport": "[TODO: Translate] Start Import",
|
"startImport": "Start Import",
|
||||||
"importing": "[TODO: Translate] Importing...",
|
"importing": "Importing...",
|
||||||
"progress": "[TODO: Translate] Progress",
|
"progress": "Progress",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "Total",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "Success",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "Failed",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "Skipped",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "Current",
|
||||||
"currentItem": "[TODO: Translate] Current",
|
"currentItem": "Current",
|
||||||
"preparing": "[TODO: Translate] Preparing...",
|
"preparing": "Preparing...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "Cancel",
|
||||||
"cancelImport": "[TODO: Translate] Cancel",
|
"cancelImport": "Cancel",
|
||||||
"cancelled": "[TODO: Translate] Import cancelled",
|
"cancelled": "Import cancelled",
|
||||||
"completed": "[TODO: Translate] Import completed",
|
"completed": "Import completed",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "Completed with errors",
|
||||||
"completedSuccess": "[TODO: Translate] Successfully imported {count} recipe(s)",
|
"completedSuccess": "Successfully imported {count} recipe(s)",
|
||||||
"successCount": "[TODO: Translate] Successful",
|
"successCount": "Successful",
|
||||||
"failedCount": "[TODO: Translate] Failed",
|
"failedCount": "Failed",
|
||||||
"skippedCount": "[TODO: Translate] Skipped",
|
"skippedCount": "Skipped",
|
||||||
"totalProcessed": "[TODO: Translate] Total processed",
|
"totalProcessed": "Total processed",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "View Details",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "New Import",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "Please enter the directory path manually. File browser is not available in this browser.",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "File browser not available. Please enter the directory path manually.",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "Back to parent directory",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "Folders",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} folders",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "Image Files",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "images",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} images",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "Select This Folder",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "[TODO: Translate] Please enter at least one URL or path",
|
"enterUrls": "Please enter at least one URL or path",
|
||||||
"enterDirectory": "[TODO: Translate] Please enter a directory path",
|
"enterDirectory": "Please enter a directory path",
|
||||||
"startFailed": "[TODO: Translate] Failed to start import: {message}"
|
"startFailed": "Failed to start import: {message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "שגיאת עיבוד: {message}",
|
"processingError": "שגיאת עיבוד: {message}",
|
||||||
"folderBrowserError": "שגיאה בטעינת דפדפן התיקיות: {message}",
|
"folderBrowserError": "שגיאה בטעינת דפדפן התיקיות: {message}",
|
||||||
"recipeSaveFailed": "שמירת המתכון נכשלה: {error}",
|
"recipeSaveFailed": "שמירת המתכון נכשלה: {error}",
|
||||||
|
"recipeSaved": "Recipe saved successfully",
|
||||||
"importFailed": "הייבוא נכשל: {message}",
|
"importFailed": "הייבוא נכשל: {message}",
|
||||||
"folderTreeFailed": "טעינת עץ התיקיות נכשלה",
|
"folderTreeFailed": "טעינת עץ התיקיות נכשלה",
|
||||||
"folderTreeError": "שגיאה בטעינת עץ התיקיות",
|
"folderTreeError": "שגיאה בטעינת עץ התיקיות",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "Failed to start batch import: {message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "Cancelling batch import...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "Failed to cancel batch import: {message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "Please enter a directory path",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "לא נבחרו מודלים",
|
"noModelsSelected": "לא נבחרו מודלים",
|
||||||
|
|||||||
125
locales/ja.json
125
locales/ja.json
@@ -645,6 +645,8 @@
|
|||||||
"root": "ルート",
|
"root": "ルート",
|
||||||
"browseFolders": "フォルダを参照:",
|
"browseFolders": "フォルダを参照:",
|
||||||
"downloadAndSaveRecipe": "ダウンロード & レシピ保存",
|
"downloadAndSaveRecipe": "ダウンロード & レシピ保存",
|
||||||
|
"importRecipeOnly": "レシピのみインポート",
|
||||||
|
"importAndDownload": "インポートとダウンロード",
|
||||||
"downloadMissingLoras": "不足しているLoRAをダウンロード",
|
"downloadMissingLoras": "不足しているLoRAをダウンロード",
|
||||||
"saveRecipe": "レシピを保存",
|
"saveRecipe": "レシピを保存",
|
||||||
"loraCountInfo": "({existing}/{total} ライブラリ内)",
|
"loraCountInfo": "({existing}/{total} ライブラリ内)",
|
||||||
@@ -732,61 +734,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "[TODO: Translate] Batch Import Recipes",
|
"title": "Batch Import Recipes",
|
||||||
"action": "[TODO: Translate] Batch Import",
|
"action": "Batch Import",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL List",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "Directory",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
||||||
"directoryDescription": "[TODO: Translate] Enter a directory path to import all images from that folder.",
|
"directoryDescription": "Enter a directory path to import all images from that folder.",
|
||||||
"urlsLabel": "[TODO: Translate] Image URLs or Local Paths",
|
"urlsLabel": "Image URLs or Local Paths",
|
||||||
"urlsPlaceholder": "[TODO: Translate] https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "Enter one URL or path per line",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "Directory Path",
|
||||||
"directoryPlaceholder": "[TODO: Translate] /path/to/images/folder",
|
"directoryPlaceholder": "/path/to/images/folder",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "Browse",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "Include subdirectories",
|
||||||
"tagsOptional": "[TODO: Translate] Tags (optional, applied to all recipes)",
|
"tagsOptional": "Tags (optional, applied to all recipes)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "Enter tags separated by commas",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "Tags will be added to all imported recipes",
|
||||||
"skipNoMetadata": "[TODO: Translate] Skip images without metadata",
|
"skipNoMetadata": "Skip images without metadata",
|
||||||
"skipNoMetadataHelp": "[TODO: Translate] Images without LoRA metadata will be skipped automatically.",
|
"skipNoMetadataHelp": "Images without LoRA metadata will be skipped automatically.",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "Start Import",
|
||||||
"startImport": "[TODO: Translate] Start Import",
|
"startImport": "Start Import",
|
||||||
"importing": "[TODO: Translate] Importing...",
|
"importing": "Importing...",
|
||||||
"progress": "[TODO: Translate] Progress",
|
"progress": "Progress",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "Total",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "Success",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "Failed",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "Skipped",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "Current",
|
||||||
"currentItem": "[TODO: Translate] Current",
|
"currentItem": "Current",
|
||||||
"preparing": "[TODO: Translate] Preparing...",
|
"preparing": "Preparing...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "Cancel",
|
||||||
"cancelImport": "[TODO: Translate] Cancel",
|
"cancelImport": "Cancel",
|
||||||
"cancelled": "[TODO: Translate] Import cancelled",
|
"cancelled": "Import cancelled",
|
||||||
"completed": "[TODO: Translate] Import completed",
|
"completed": "Import completed",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "Completed with errors",
|
||||||
"completedSuccess": "[TODO: Translate] Successfully imported {count} recipe(s)",
|
"completedSuccess": "Successfully imported {count} recipe(s)",
|
||||||
"successCount": "[TODO: Translate] Successful",
|
"successCount": "Successful",
|
||||||
"failedCount": "[TODO: Translate] Failed",
|
"failedCount": "Failed",
|
||||||
"skippedCount": "[TODO: Translate] Skipped",
|
"skippedCount": "Skipped",
|
||||||
"totalProcessed": "[TODO: Translate] Total processed",
|
"totalProcessed": "Total processed",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "View Details",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "New Import",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "Please enter the directory path manually. File browser is not available in this browser.",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "File browser not available. Please enter the directory path manually.",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "Back to parent directory",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "Folders",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} folders",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "Image Files",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "images",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} images",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "Select This Folder",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "[TODO: Translate] Please enter at least one URL or path",
|
"enterUrls": "Please enter at least one URL or path",
|
||||||
"enterDirectory": "[TODO: Translate] Please enter a directory path",
|
"enterDirectory": "Please enter a directory path",
|
||||||
"startFailed": "[TODO: Translate] Failed to start import: {message}"
|
"startFailed": "Failed to start import: {message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "処理エラー:{message}",
|
"processingError": "処理エラー:{message}",
|
||||||
"folderBrowserError": "フォルダブラウザの読み込みエラー:{message}",
|
"folderBrowserError": "フォルダブラウザの読み込みエラー:{message}",
|
||||||
"recipeSaveFailed": "レシピの保存に失敗しました:{error}",
|
"recipeSaveFailed": "レシピの保存に失敗しました:{error}",
|
||||||
|
"recipeSaved": "Recipe saved successfully",
|
||||||
"importFailed": "インポートに失敗しました:{message}",
|
"importFailed": "インポートに失敗しました:{message}",
|
||||||
"folderTreeFailed": "フォルダツリーの読み込みに失敗しました",
|
"folderTreeFailed": "フォルダツリーの読み込みに失敗しました",
|
||||||
"folderTreeError": "フォルダツリー読み込みエラー",
|
"folderTreeError": "フォルダツリー読み込みエラー",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "Failed to start batch import: {message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "Cancelling batch import...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "Failed to cancel batch import: {message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "Please enter a directory path",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "モデルが選択されていません",
|
"noModelsSelected": "モデルが選択されていません",
|
||||||
|
|||||||
125
locales/ko.json
125
locales/ko.json
@@ -645,6 +645,8 @@
|
|||||||
"root": "루트",
|
"root": "루트",
|
||||||
"browseFolders": "폴더 탐색:",
|
"browseFolders": "폴더 탐색:",
|
||||||
"downloadAndSaveRecipe": "다운로드 및 레시피 저장",
|
"downloadAndSaveRecipe": "다운로드 및 레시피 저장",
|
||||||
|
"importRecipeOnly": "레시피만 가져오기",
|
||||||
|
"importAndDownload": "가져오기 및 다운로드",
|
||||||
"downloadMissingLoras": "누락된 LoRA 다운로드",
|
"downloadMissingLoras": "누락된 LoRA 다운로드",
|
||||||
"saveRecipe": "레시피 저장",
|
"saveRecipe": "레시피 저장",
|
||||||
"loraCountInfo": "({existing}/{total} 라이브러리에 있음)",
|
"loraCountInfo": "({existing}/{total} 라이브러리에 있음)",
|
||||||
@@ -732,61 +734,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "[TODO: Translate] Batch Import Recipes",
|
"title": "Batch Import Recipes",
|
||||||
"action": "[TODO: Translate] Batch Import",
|
"action": "Batch Import",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL List",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "Directory",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
||||||
"directoryDescription": "[TODO: Translate] Enter a directory path to import all images from that folder.",
|
"directoryDescription": "Enter a directory path to import all images from that folder.",
|
||||||
"urlsLabel": "[TODO: Translate] Image URLs or Local Paths",
|
"urlsLabel": "Image URLs or Local Paths",
|
||||||
"urlsPlaceholder": "[TODO: Translate] https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "Enter one URL or path per line",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "Directory Path",
|
||||||
"directoryPlaceholder": "[TODO: Translate] /path/to/images/folder",
|
"directoryPlaceholder": "/path/to/images/folder",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "Browse",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "Include subdirectories",
|
||||||
"tagsOptional": "[TODO: Translate] Tags (optional, applied to all recipes)",
|
"tagsOptional": "Tags (optional, applied to all recipes)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "Enter tags separated by commas",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "Tags will be added to all imported recipes",
|
||||||
"skipNoMetadata": "[TODO: Translate] Skip images without metadata",
|
"skipNoMetadata": "Skip images without metadata",
|
||||||
"skipNoMetadataHelp": "[TODO: Translate] Images without LoRA metadata will be skipped automatically.",
|
"skipNoMetadataHelp": "Images without LoRA metadata will be skipped automatically.",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "Start Import",
|
||||||
"startImport": "[TODO: Translate] Start Import",
|
"startImport": "Start Import",
|
||||||
"importing": "[TODO: Translate] Importing...",
|
"importing": "Importing...",
|
||||||
"progress": "[TODO: Translate] Progress",
|
"progress": "Progress",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "Total",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "Success",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "Failed",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "Skipped",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "Current",
|
||||||
"currentItem": "[TODO: Translate] Current",
|
"currentItem": "Current",
|
||||||
"preparing": "[TODO: Translate] Preparing...",
|
"preparing": "Preparing...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "Cancel",
|
||||||
"cancelImport": "[TODO: Translate] Cancel",
|
"cancelImport": "Cancel",
|
||||||
"cancelled": "[TODO: Translate] Import cancelled",
|
"cancelled": "Import cancelled",
|
||||||
"completed": "[TODO: Translate] Import completed",
|
"completed": "Import completed",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "Completed with errors",
|
||||||
"completedSuccess": "[TODO: Translate] Successfully imported {count} recipe(s)",
|
"completedSuccess": "Successfully imported {count} recipe(s)",
|
||||||
"successCount": "[TODO: Translate] Successful",
|
"successCount": "Successful",
|
||||||
"failedCount": "[TODO: Translate] Failed",
|
"failedCount": "Failed",
|
||||||
"skippedCount": "[TODO: Translate] Skipped",
|
"skippedCount": "Skipped",
|
||||||
"totalProcessed": "[TODO: Translate] Total processed",
|
"totalProcessed": "Total processed",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "View Details",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "New Import",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "Please enter the directory path manually. File browser is not available in this browser.",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "File browser not available. Please enter the directory path manually.",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "Back to parent directory",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "Folders",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} folders",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "Image Files",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "images",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} images",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "Select This Folder",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "[TODO: Translate] Please enter at least one URL or path",
|
"enterUrls": "Please enter at least one URL or path",
|
||||||
"enterDirectory": "[TODO: Translate] Please enter a directory path",
|
"enterDirectory": "Please enter a directory path",
|
||||||
"startFailed": "[TODO: Translate] Failed to start import: {message}"
|
"startFailed": "Failed to start import: {message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "처리 오류: {message}",
|
"processingError": "처리 오류: {message}",
|
||||||
"folderBrowserError": "폴더 브라우저 로딩 오류: {message}",
|
"folderBrowserError": "폴더 브라우저 로딩 오류: {message}",
|
||||||
"recipeSaveFailed": "레시피 저장 실패: {error}",
|
"recipeSaveFailed": "레시피 저장 실패: {error}",
|
||||||
|
"recipeSaved": "Recipe saved successfully",
|
||||||
"importFailed": "가져오기 실패: {message}",
|
"importFailed": "가져오기 실패: {message}",
|
||||||
"folderTreeFailed": "폴더 트리 로딩 실패",
|
"folderTreeFailed": "폴더 트리 로딩 실패",
|
||||||
"folderTreeError": "폴더 트리 로딩 오류",
|
"folderTreeError": "폴더 트리 로딩 오류",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "Failed to start batch import: {message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "Cancelling batch import...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "Failed to cancel batch import: {message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "Please enter a directory path",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "선택된 모델이 없습니다",
|
"noModelsSelected": "선택된 모델이 없습니다",
|
||||||
|
|||||||
125
locales/ru.json
125
locales/ru.json
@@ -645,6 +645,8 @@
|
|||||||
"root": "Корень",
|
"root": "Корень",
|
||||||
"browseFolders": "Обзор папок:",
|
"browseFolders": "Обзор папок:",
|
||||||
"downloadAndSaveRecipe": "Скачать и сохранить рецепт",
|
"downloadAndSaveRecipe": "Скачать и сохранить рецепт",
|
||||||
|
"importRecipeOnly": "Импортировать только рецепт",
|
||||||
|
"importAndDownload": "Импорт и скачивание",
|
||||||
"downloadMissingLoras": "Скачать отсутствующие LoRAs",
|
"downloadMissingLoras": "Скачать отсутствующие LoRAs",
|
||||||
"saveRecipe": "Сохранить рецепт",
|
"saveRecipe": "Сохранить рецепт",
|
||||||
"loraCountInfo": "({existing}/{total} в библиотеке)",
|
"loraCountInfo": "({existing}/{total} в библиотеке)",
|
||||||
@@ -732,61 +734,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "[TODO: Translate] Batch Import Recipes",
|
"title": "Batch Import Recipes",
|
||||||
"action": "[TODO: Translate] Batch Import",
|
"action": "Batch Import",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL List",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "Directory",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
||||||
"directoryDescription": "[TODO: Translate] Enter a directory path to import all images from that folder.",
|
"directoryDescription": "Enter a directory path to import all images from that folder.",
|
||||||
"urlsLabel": "[TODO: Translate] Image URLs or Local Paths",
|
"urlsLabel": "Image URLs or Local Paths",
|
||||||
"urlsPlaceholder": "[TODO: Translate] https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "Enter one URL or path per line",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "Directory Path",
|
||||||
"directoryPlaceholder": "[TODO: Translate] /path/to/images/folder",
|
"directoryPlaceholder": "/path/to/images/folder",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "Browse",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "Include subdirectories",
|
||||||
"tagsOptional": "[TODO: Translate] Tags (optional, applied to all recipes)",
|
"tagsOptional": "Tags (optional, applied to all recipes)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "Enter tags separated by commas",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "Tags will be added to all imported recipes",
|
||||||
"skipNoMetadata": "[TODO: Translate] Skip images without metadata",
|
"skipNoMetadata": "Skip images without metadata",
|
||||||
"skipNoMetadataHelp": "[TODO: Translate] Images without LoRA metadata will be skipped automatically.",
|
"skipNoMetadataHelp": "Images without LoRA metadata will be skipped automatically.",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "Start Import",
|
||||||
"startImport": "[TODO: Translate] Start Import",
|
"startImport": "Start Import",
|
||||||
"importing": "[TODO: Translate] Importing...",
|
"importing": "Importing...",
|
||||||
"progress": "[TODO: Translate] Progress",
|
"progress": "Progress",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "Total",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "Success",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "Failed",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "Skipped",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "Current",
|
||||||
"currentItem": "[TODO: Translate] Current",
|
"currentItem": "Current",
|
||||||
"preparing": "[TODO: Translate] Preparing...",
|
"preparing": "Preparing...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "Cancel",
|
||||||
"cancelImport": "[TODO: Translate] Cancel",
|
"cancelImport": "Cancel",
|
||||||
"cancelled": "[TODO: Translate] Import cancelled",
|
"cancelled": "Import cancelled",
|
||||||
"completed": "[TODO: Translate] Import completed",
|
"completed": "Import completed",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "Completed with errors",
|
||||||
"completedSuccess": "[TODO: Translate] Successfully imported {count} recipe(s)",
|
"completedSuccess": "Successfully imported {count} recipe(s)",
|
||||||
"successCount": "[TODO: Translate] Successful",
|
"successCount": "Successful",
|
||||||
"failedCount": "[TODO: Translate] Failed",
|
"failedCount": "Failed",
|
||||||
"skippedCount": "[TODO: Translate] Skipped",
|
"skippedCount": "Skipped",
|
||||||
"totalProcessed": "[TODO: Translate] Total processed",
|
"totalProcessed": "Total processed",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "View Details",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "New Import",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "Please enter the directory path manually. File browser is not available in this browser.",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "Directory selected: {path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "File browser not available. Please enter the directory path manually.",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "Back to parent directory",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "Folders",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} folders",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "Image Files",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "images",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} images",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "Select This Folder",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "[TODO: Translate] Please enter at least one URL or path",
|
"enterUrls": "Please enter at least one URL or path",
|
||||||
"enterDirectory": "[TODO: Translate] Please enter a directory path",
|
"enterDirectory": "Please enter a directory path",
|
||||||
"startFailed": "[TODO: Translate] Failed to start import: {message}"
|
"startFailed": "Failed to start import: {message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "Ошибка обработки: {message}",
|
"processingError": "Ошибка обработки: {message}",
|
||||||
"folderBrowserError": "Ошибка загрузки браузера папок: {message}",
|
"folderBrowserError": "Ошибка загрузки браузера папок: {message}",
|
||||||
"recipeSaveFailed": "Не удалось сохранить рецепт: {error}",
|
"recipeSaveFailed": "Не удалось сохранить рецепт: {error}",
|
||||||
|
"recipeSaved": "Recipe saved successfully",
|
||||||
"importFailed": "Импорт не удался: {message}",
|
"importFailed": "Импорт не удался: {message}",
|
||||||
"folderTreeFailed": "Не удалось загрузить дерево папок",
|
"folderTreeFailed": "Не удалось загрузить дерево папок",
|
||||||
"folderTreeError": "Ошибка загрузки дерева папок",
|
"folderTreeError": "Ошибка загрузки дерева папок",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "Failed to start batch import: {message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "Cancelling batch import...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "Failed to cancel batch import: {message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "Please enter at least one URL or file path",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "Please enter a directory path",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "Failed to browse directory: {message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "Directory selected: {path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "Модели не выбраны",
|
"noModelsSelected": "Модели не выбраны",
|
||||||
|
|||||||
@@ -645,6 +645,8 @@
|
|||||||
"root": "根目录",
|
"root": "根目录",
|
||||||
"browseFolders": "浏览文件夹:",
|
"browseFolders": "浏览文件夹:",
|
||||||
"downloadAndSaveRecipe": "下载并保存配方",
|
"downloadAndSaveRecipe": "下载并保存配方",
|
||||||
|
"importRecipeOnly": "仅导入配方",
|
||||||
|
"importAndDownload": "导入并下载",
|
||||||
"downloadMissingLoras": "下载缺失的 LoRA",
|
"downloadMissingLoras": "下载缺失的 LoRA",
|
||||||
"saveRecipe": "保存配方",
|
"saveRecipe": "保存配方",
|
||||||
"loraCountInfo": "({existing}/{total} in library)",
|
"loraCountInfo": "({existing}/{total} in library)",
|
||||||
@@ -734,55 +736,55 @@
|
|||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "批量导入配方",
|
"title": "批量导入配方",
|
||||||
"action": "批量导入",
|
"action": "批量导入",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL 列表",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "目录",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "输入图像 URL 或本地文件路径(每行一个)。每个都将作为配方导入。",
|
||||||
"directoryDescription": "输入目录路径以导入该文件夹中的所有图片。",
|
"directoryDescription": "输入目录路径以导入该文件夹中的所有图片。",
|
||||||
"urlsLabel": "图片 URL 或本地路径",
|
"urlsLabel": "图片 URL 或本地路径",
|
||||||
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "每行输入一个 URL 或路径",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "目录路径",
|
||||||
"directoryPlaceholder": "/图片/文件夹/路径",
|
"directoryPlaceholder": "/图片/文件夹/路径",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "浏览",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "包含子目录",
|
||||||
"tagsOptional": "标签(可选,应用于所有配方)",
|
"tagsOptional": "标签(可选,应用于所有配方)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "输入以逗号分隔的标签",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "标签将被添加到所有导入的配方中",
|
||||||
"skipNoMetadata": "跳过无元数据的图片",
|
"skipNoMetadata": "跳过无元数据的图片",
|
||||||
"skipNoMetadataHelp": "没有 LoRA 元数据的图片将自动跳过。",
|
"skipNoMetadataHelp": "没有 LoRA 元数据的图片将自动跳过。",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "开始导入",
|
||||||
"startImport": "开始导入",
|
"startImport": "开始导入",
|
||||||
"importing": "正在导入配方...",
|
"importing": "正在导入配方...",
|
||||||
"progress": "进度",
|
"progress": "进度",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "总计",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "成功",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "失败",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "跳过",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "当前",
|
||||||
"currentItem": "当前",
|
"currentItem": "当前",
|
||||||
"preparing": "准备中...",
|
"preparing": "准备中...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "取消",
|
||||||
"cancelImport": "取消",
|
"cancelImport": "取消",
|
||||||
"cancelled": "批量导入已取消",
|
"cancelled": "批量导入已取消",
|
||||||
"completed": "导入完成",
|
"completed": "导入完成",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "导入完成但有错误",
|
||||||
"completedSuccess": "成功导入 {count} 个配方",
|
"completedSuccess": "成功导入 {count} 个配方",
|
||||||
"successCount": "成功",
|
"successCount": "成功",
|
||||||
"failedCount": "失败",
|
"failedCount": "失败",
|
||||||
"skippedCount": "跳过",
|
"skippedCount": "跳过",
|
||||||
"totalProcessed": "总计处理",
|
"totalProcessed": "总计处理",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "查看详情",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "新建导入",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "请手动输入目录路径。此浏览器中文件浏览器不可用。",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "已选择目录:{path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "文件浏览器不可用。请手动输入目录路径。",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "返回上级目录",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "文件夹",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} 个文件夹",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "图像文件",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "图像",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} 个图像",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "选择此文件夹",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "请至少输入一个 URL 或路径",
|
"enterUrls": "请至少输入一个 URL 或路径",
|
||||||
"enterDirectory": "请输入目录路径",
|
"enterDirectory": "请输入目录路径",
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "处理出错:{message}",
|
"processingError": "处理出错:{message}",
|
||||||
"folderBrowserError": "加载文件夹浏览器出错:{message}",
|
"folderBrowserError": "加载文件夹浏览器出错:{message}",
|
||||||
"recipeSaveFailed": "保存配方失败:{error}",
|
"recipeSaveFailed": "保存配方失败:{error}",
|
||||||
|
"recipeSaved": "配方保存成功",
|
||||||
"importFailed": "导入失败:{message}",
|
"importFailed": "导入失败:{message}",
|
||||||
"folderTreeFailed": "加载文件夹树失败",
|
"folderTreeFailed": "加载文件夹树失败",
|
||||||
"folderTreeError": "加载文件夹树出错",
|
"folderTreeError": "加载文件夹树出错",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "启动批量导入失败:{message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "正在取消批量导入...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "取消批量导入失败:{message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "请输入至少一个 URL 或文件路径",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "请输入目录路径",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "浏览目录失败:{message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "已选择目录:{path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "未选中模型",
|
"noModelsSelected": "未选中模型",
|
||||||
|
|||||||
@@ -645,6 +645,8 @@
|
|||||||
"root": "根目錄",
|
"root": "根目錄",
|
||||||
"browseFolders": "瀏覽資料夾:",
|
"browseFolders": "瀏覽資料夾:",
|
||||||
"downloadAndSaveRecipe": "下載並儲存配方",
|
"downloadAndSaveRecipe": "下載並儲存配方",
|
||||||
|
"importRecipeOnly": "僅匯入配方",
|
||||||
|
"importAndDownload": "匯入並下載",
|
||||||
"downloadMissingLoras": "下載缺少的 LoRA",
|
"downloadMissingLoras": "下載缺少的 LoRA",
|
||||||
"saveRecipe": "儲存配方",
|
"saveRecipe": "儲存配方",
|
||||||
"loraCountInfo": "(庫存 {existing}/{total})",
|
"loraCountInfo": "(庫存 {existing}/{total})",
|
||||||
@@ -732,61 +734,61 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"batchImport": {
|
"batchImport": {
|
||||||
"title": "[TODO: Translate] Batch Import Recipes",
|
"title": "批量匯入配方",
|
||||||
"action": "[TODO: Translate] Batch Import",
|
"action": "批量匯入",
|
||||||
"urlList": "[TODO: Translate] URL List",
|
"urlList": "URL 列表",
|
||||||
"directory": "[TODO: Translate] Directory",
|
"directory": "目錄",
|
||||||
"urlDescription": "[TODO: Translate] Enter image URLs or local file paths (one per line). Each will be imported as a recipe.",
|
"urlDescription": "輸入圖像 URL 或本地檔案路徑(每行一個)。每個都將作為配方匯入。",
|
||||||
"directoryDescription": "[TODO: Translate] Enter a directory path to import all images from that folder.",
|
"directoryDescription": "輸入目錄路徑以匯入該資料夾中的所有圖像。",
|
||||||
"urlsLabel": "[TODO: Translate] Image URLs or Local Paths",
|
"urlsLabel": "圖像 URL 或本地路徑",
|
||||||
"urlsPlaceholder": "[TODO: Translate] https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
"urlsPlaceholder": "https://civitai.com/images/...\nhttps://civitai.com/images/...\nC:/path/to/image.png\n...",
|
||||||
"urlsHint": "[TODO: Translate] Enter one URL or path per line",
|
"urlsHint": "每行輸入一個 URL 或路徑",
|
||||||
"directoryPath": "[TODO: Translate] Directory Path",
|
"directoryPath": "目錄路徑",
|
||||||
"directoryPlaceholder": "[TODO: Translate] /path/to/images/folder",
|
"directoryPlaceholder": "/path/to/images/folder",
|
||||||
"browse": "[TODO: Translate] Browse",
|
"browse": "瀏覽",
|
||||||
"recursive": "[TODO: Translate] Include subdirectories",
|
"recursive": "包含子目錄",
|
||||||
"tagsOptional": "[TODO: Translate] Tags (optional, applied to all recipes)",
|
"tagsOptional": "標籤(可選,應用於所有配方)",
|
||||||
"tagsPlaceholder": "[TODO: Translate] Enter tags separated by commas",
|
"tagsPlaceholder": "輸入以逗號分隔的標籤",
|
||||||
"tagsHint": "[TODO: Translate] Tags will be added to all imported recipes",
|
"tagsHint": "標籤將被添加到所有匯入的配方中",
|
||||||
"skipNoMetadata": "[TODO: Translate] Skip images without metadata",
|
"skipNoMetadata": "跳過無元資料的圖像",
|
||||||
"skipNoMetadataHelp": "[TODO: Translate] Images without LoRA metadata will be skipped automatically.",
|
"skipNoMetadataHelp": "沒有 LoRA 元資料的圖像將被自動跳過。",
|
||||||
"start": "[TODO: Translate] Start Import",
|
"start": "開始匯入",
|
||||||
"startImport": "[TODO: Translate] Start Import",
|
"startImport": "開始匯入",
|
||||||
"importing": "[TODO: Translate] Importing...",
|
"importing": "匯入中...",
|
||||||
"progress": "[TODO: Translate] Progress",
|
"progress": "進度",
|
||||||
"total": "[TODO: Translate] Total",
|
"total": "總計",
|
||||||
"success": "[TODO: Translate] Success",
|
"success": "成功",
|
||||||
"failed": "[TODO: Translate] Failed",
|
"failed": "失敗",
|
||||||
"skipped": "[TODO: Translate] Skipped",
|
"skipped": "跳過",
|
||||||
"current": "[TODO: Translate] Current",
|
"current": "當前",
|
||||||
"currentItem": "[TODO: Translate] Current",
|
"currentItem": "當前項目",
|
||||||
"preparing": "[TODO: Translate] Preparing...",
|
"preparing": "準備中...",
|
||||||
"cancel": "[TODO: Translate] Cancel",
|
"cancel": "取消",
|
||||||
"cancelImport": "[TODO: Translate] Cancel",
|
"cancelImport": "取消匯入",
|
||||||
"cancelled": "[TODO: Translate] Import cancelled",
|
"cancelled": "匯入已取消",
|
||||||
"completed": "[TODO: Translate] Import completed",
|
"completed": "匯入完成",
|
||||||
"completedWithErrors": "[TODO: Translate] Completed with errors",
|
"completedWithErrors": "匯入完成但有錯誤",
|
||||||
"completedSuccess": "[TODO: Translate] Successfully imported {count} recipe(s)",
|
"completedSuccess": "成功匯入 {count} 個配方",
|
||||||
"successCount": "[TODO: Translate] Successful",
|
"successCount": "成功",
|
||||||
"failedCount": "[TODO: Translate] Failed",
|
"failedCount": "失敗",
|
||||||
"skippedCount": "[TODO: Translate] Skipped",
|
"skippedCount": "跳過",
|
||||||
"totalProcessed": "[TODO: Translate] Total processed",
|
"totalProcessed": "總計處理",
|
||||||
"viewDetails": "[TODO: Translate] View Details",
|
"viewDetails": "查看詳情",
|
||||||
"newImport": "[TODO: Translate] New Import",
|
"newImport": "新建匯入",
|
||||||
"manualPathEntry": "[TODO: Translate] Please enter the directory path manually. File browser is not available in this browser.",
|
"manualPathEntry": "請手動輸入目錄路徑。此瀏覽器中檔案瀏覽器不可用。",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {name}. You may need to enter the full path manually.",
|
"batchImportDirectorySelected": "已選擇目錄:{path}",
|
||||||
"batchImportManualEntryRequired": "[TODO: Translate] File browser not available. Please enter the directory path manually.",
|
"batchImportManualEntryRequired": "檔案瀏覽器不可用。請手動輸入目錄路徑。",
|
||||||
"backToParent": "[TODO: Translate] Back to parent directory",
|
"backToParent": "返回上級目錄",
|
||||||
"folders": "[TODO: Translate] Folders",
|
"folders": "資料夾",
|
||||||
"folderCount": "[TODO: Translate] {count} folders",
|
"folderCount": "{count} 個資料夾",
|
||||||
"imageFiles": "[TODO: Translate] Image Files",
|
"imageFiles": "圖像檔案",
|
||||||
"images": "[TODO: Translate] images",
|
"images": "圖像",
|
||||||
"imageCount": "[TODO: Translate] {count} images",
|
"imageCount": "{count} 個圖像",
|
||||||
"selectFolder": "[TODO: Translate] Select This Folder",
|
"selectFolder": "選擇此資料夾",
|
||||||
"errors": {
|
"errors": {
|
||||||
"enterUrls": "[TODO: Translate] Please enter at least one URL or path",
|
"enterUrls": "請輸入至少一個 URL 或路徑",
|
||||||
"enterDirectory": "[TODO: Translate] Please enter a directory path",
|
"enterDirectory": "請輸入目錄路徑",
|
||||||
"startFailed": "[TODO: Translate] Failed to start import: {message}"
|
"startFailed": "啟動匯入失敗:{message}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1495,16 +1497,17 @@
|
|||||||
"processingError": "處理錯誤:{message}",
|
"processingError": "處理錯誤:{message}",
|
||||||
"folderBrowserError": "載入資料夾瀏覽器錯誤:{message}",
|
"folderBrowserError": "載入資料夾瀏覽器錯誤:{message}",
|
||||||
"recipeSaveFailed": "儲存配方失敗:{error}",
|
"recipeSaveFailed": "儲存配方失敗:{error}",
|
||||||
|
"recipeSaved": "配方儲存成功",
|
||||||
"importFailed": "匯入失敗:{message}",
|
"importFailed": "匯入失敗:{message}",
|
||||||
"folderTreeFailed": "載入資料夾樹狀結構失敗",
|
"folderTreeFailed": "載入資料夾樹狀結構失敗",
|
||||||
"folderTreeError": "載入資料夾樹狀結構錯誤",
|
"folderTreeError": "載入資料夾樹狀結構錯誤",
|
||||||
"batchImportFailed": "[TODO: Translate] Failed to start batch import: {message}",
|
"batchImportFailed": "啟動批量匯入失敗:{message}",
|
||||||
"batchImportCancelling": "[TODO: Translate] Cancelling batch import...",
|
"batchImportCancelling": "正在取消批量匯入...",
|
||||||
"batchImportCancelFailed": "[TODO: Translate] Failed to cancel batch import: {message}",
|
"batchImportCancelFailed": "取消批量匯入失敗:{message}",
|
||||||
"batchImportNoUrls": "[TODO: Translate] Please enter at least one URL or file path",
|
"batchImportNoUrls": "請輸入至少一個 URL 或檔案路徑",
|
||||||
"batchImportNoDirectory": "[TODO: Translate] Please enter a directory path",
|
"batchImportNoDirectory": "請輸入目錄路徑",
|
||||||
"batchImportBrowseFailed": "[TODO: Translate] Failed to browse directory: {message}",
|
"batchImportBrowseFailed": "瀏覽目錄失敗:{message}",
|
||||||
"batchImportDirectorySelected": "[TODO: Translate] Directory selected: {path}"
|
"batchImportDirectorySelected": "已選擇目錄:{path}"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"noModelsSelected": "未選擇模型",
|
"noModelsSelected": "未選擇模型",
|
||||||
|
|||||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -114,7 +114,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -138,7 +137,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -1613,7 +1611,6 @@
|
|||||||
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
|
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssstyle": "^4.0.1",
|
"cssstyle": "^4.0.1",
|
||||||
"data-urls": "^5.0.0",
|
"data-urls": "^5.0.0",
|
||||||
|
|||||||
@@ -148,10 +148,13 @@ class MetadataHook:
|
|||||||
"""Install hooks for asynchronous execution model"""
|
"""Install hooks for asynchronous execution model"""
|
||||||
# Store the original _async_map_node_over_list function
|
# Store the original _async_map_node_over_list function
|
||||||
original_map_node_over_list = getattr(execution, map_node_func_name)
|
original_map_node_over_list = getattr(execution, map_node_func_name)
|
||||||
|
|
||||||
# Wrapped async function, compatible with both stable and nightly
|
# Wrapped async function - signature must exactly match _async_map_node_over_list
|
||||||
async def async_map_node_over_list_with_metadata(prompt_id, unique_id, obj, input_data_all, func, allow_interrupt=False, execution_block_cb=None, pre_execute_cb=None, *args, **kwargs):
|
async def async_map_node_over_list_with_metadata(
|
||||||
hidden_inputs = kwargs.get('hidden_inputs', None)
|
prompt_id, unique_id, obj, input_data_all, func,
|
||||||
|
allow_interrupt=False, execution_block_cb=None,
|
||||||
|
pre_execute_cb=None, v3_data=None
|
||||||
|
):
|
||||||
# Only collect metadata when calling the main function of nodes
|
# Only collect metadata when calling the main function of nodes
|
||||||
if func == obj.FUNCTION and hasattr(obj, '__class__'):
|
if func == obj.FUNCTION and hasattr(obj, '__class__'):
|
||||||
try:
|
try:
|
||||||
@@ -163,13 +166,13 @@ class MetadataHook:
|
|||||||
registry.record_node_execution(node_id, class_type, input_data_all, None)
|
registry.record_node_execution(node_id, class_type, input_data_all, None)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error collecting metadata (pre-execution): {str(e)}")
|
logger.error(f"Error collecting metadata (pre-execution): {str(e)}")
|
||||||
|
|
||||||
# Call original function with all args/kwargs
|
# Call original function with exact parameters
|
||||||
results = await original_map_node_over_list(
|
results = await original_map_node_over_list(
|
||||||
prompt_id, unique_id, obj, input_data_all, func,
|
prompt_id, unique_id, obj, input_data_all, func,
|
||||||
allow_interrupt, execution_block_cb, pre_execute_cb, *args, **kwargs
|
allow_interrupt, execution_block_cb, pre_execute_cb, v3_data=v3_data
|
||||||
)
|
)
|
||||||
|
|
||||||
if func == obj.FUNCTION and hasattr(obj, '__class__'):
|
if func == obj.FUNCTION and hasattr(obj, '__class__'):
|
||||||
try:
|
try:
|
||||||
registry = MetadataRegistry()
|
registry = MetadataRegistry()
|
||||||
@@ -180,28 +183,28 @@ class MetadataHook:
|
|||||||
registry.update_node_execution(node_id, class_type, results)
|
registry.update_node_execution(node_id, class_type, results)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error collecting metadata (post-execution): {str(e)}")
|
logger.error(f"Error collecting metadata (post-execution): {str(e)}")
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
# Also hook the execute function to track the current prompt_id
|
# Also hook the execute function to track the current prompt_id
|
||||||
original_execute = execution.execute
|
original_execute = execution.execute
|
||||||
|
|
||||||
async def async_execute_with_prompt_tracking(*args, **kwargs):
|
async def async_execute_with_prompt_tracking(*args, **kwargs):
|
||||||
if len(args) >= 7: # Check if we have enough arguments
|
if len(args) >= 7: # Check if we have enough arguments
|
||||||
server, prompt, caches, node_id, extra_data, executed, prompt_id = args[:7]
|
server, prompt, caches, node_id, extra_data, executed, prompt_id = args[:7]
|
||||||
registry = MetadataRegistry()
|
registry = MetadataRegistry()
|
||||||
|
|
||||||
# Start collection if this is a new prompt
|
# Start collection if this is a new prompt
|
||||||
if not registry.current_prompt_id or registry.current_prompt_id != prompt_id:
|
if not registry.current_prompt_id or registry.current_prompt_id != prompt_id:
|
||||||
registry.start_collection(prompt_id)
|
registry.start_collection(prompt_id)
|
||||||
|
|
||||||
# Store the dynprompt reference for node lookups
|
# Store the dynprompt reference for node lookups
|
||||||
if hasattr(prompt, 'original_prompt'):
|
if hasattr(prompt, 'original_prompt'):
|
||||||
registry.set_current_prompt(prompt)
|
registry.set_current_prompt(prompt)
|
||||||
|
|
||||||
# Execute the original function
|
# Execute the original function
|
||||||
return await original_execute(*args, **kwargs)
|
return await original_execute(*args, **kwargs)
|
||||||
|
|
||||||
# Replace the functions with async versions
|
# Replace the functions with async versions
|
||||||
setattr(execution, map_node_func_name, async_map_node_over_list_with_metadata)
|
setattr(execution, map_node_func_name, async_map_node_over_list_with_metadata)
|
||||||
execution.execute = async_execute_with_prompt_tracking
|
execution.execute = async_execute_with_prompt_tracking
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
import comfy.sd
|
import comfy.sd # type: ignore
|
||||||
import folder_paths
|
import folder_paths # type: ignore
|
||||||
from ..utils.utils import get_checkpoint_info_absolute, _format_model_name_for_comfyui
|
from ..utils.utils import get_checkpoint_info_absolute, _format_model_name_for_comfyui
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class LoraPoolLM:
|
|||||||
"folders": {"include": [], "exclude": []},
|
"folders": {"include": [], "exclude": []},
|
||||||
"favoritesOnly": False,
|
"favoritesOnly": False,
|
||||||
"license": {"noCreditRequired": False, "allowSelling": False},
|
"license": {"noCreditRequired": False, "allowSelling": False},
|
||||||
|
"namePatterns": {"include": [], "exclude": [], "useRegex": False},
|
||||||
},
|
},
|
||||||
"preview": {"matchCount": 0, "lastUpdated": 0},
|
"preview": {"matchCount": 0, "lastUpdated": 0},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ and tracks the last used combination for reuse.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import random
|
|
||||||
import os
|
import os
|
||||||
from ..utils.utils import get_lora_info
|
from ..utils.utils import get_lora_info
|
||||||
from .utils import extract_lora_name
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
import torch
|
import comfy.sd # type: ignore
|
||||||
import comfy.sd
|
|
||||||
from ..utils.utils import get_checkpoint_info_absolute, _format_model_name_for_comfyui
|
from ..utils.utils import get_checkpoint_info_absolute, _format_model_name_for_comfyui
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -101,6 +100,8 @@ class UNETLoaderLM:
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (MODEL,)
|
Tuple of (MODEL,)
|
||||||
"""
|
"""
|
||||||
|
import torch
|
||||||
|
|
||||||
# Get absolute path from cache using ComfyUI-style name
|
# Get absolute path from cache using ComfyUI-style name
|
||||||
unet_path, metadata = get_checkpoint_info_absolute(unet_name)
|
unet_path, metadata = get_checkpoint_info_absolute(unet_name)
|
||||||
|
|
||||||
@@ -143,6 +144,7 @@ class UNETLoaderLM:
|
|||||||
Returns:
|
Returns:
|
||||||
Tuple of (MODEL,)
|
Tuple of (MODEL,)
|
||||||
"""
|
"""
|
||||||
|
import torch
|
||||||
from .gguf_import_helper import get_gguf_modules
|
from .gguf_import_helper import get_gguf_modules
|
||||||
|
|
||||||
# Get ComfyUI-GGUF modules using helper (handles various import scenarios)
|
# Get ComfyUI-GGUF modules using helper (handles various import scenarios)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from .parsers import (
|
|||||||
MetaFormatParser,
|
MetaFormatParser,
|
||||||
AutomaticMetadataParser,
|
AutomaticMetadataParser,
|
||||||
CivitaiApiMetadataParser,
|
CivitaiApiMetadataParser,
|
||||||
|
SuiImageParamsParser,
|
||||||
)
|
)
|
||||||
from .base import RecipeMetadataParser
|
from .base import RecipeMetadataParser
|
||||||
|
|
||||||
@@ -55,6 +56,13 @@ class RecipeParserFactory:
|
|||||||
# If JSON parsing fails, move on to other parsers
|
# If JSON parsing fails, move on to other parsers
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# Try SuiImageParamsParser for SuiImage metadata format
|
||||||
|
try:
|
||||||
|
if SuiImageParamsParser().is_metadata_matching(metadata_str):
|
||||||
|
return SuiImageParamsParser()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Check other parsers that expect string input
|
# Check other parsers that expect string input
|
||||||
if RecipeFormatParser().is_metadata_matching(metadata_str):
|
if RecipeFormatParser().is_metadata_matching(metadata_str):
|
||||||
return RecipeFormatParser()
|
return RecipeFormatParser()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from .comfy import ComfyMetadataParser
|
|||||||
from .meta_format import MetaFormatParser
|
from .meta_format import MetaFormatParser
|
||||||
from .automatic import AutomaticMetadataParser
|
from .automatic import AutomaticMetadataParser
|
||||||
from .civitai_image import CivitaiApiMetadataParser
|
from .civitai_image import CivitaiApiMetadataParser
|
||||||
|
from .sui_image_params import SuiImageParamsParser
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'RecipeFormatParser',
|
'RecipeFormatParser',
|
||||||
@@ -12,4 +13,5 @@ __all__ = [
|
|||||||
'MetaFormatParser',
|
'MetaFormatParser',
|
||||||
'AutomaticMetadataParser',
|
'AutomaticMetadataParser',
|
||||||
'CivitaiApiMetadataParser',
|
'CivitaiApiMetadataParser',
|
||||||
|
'SuiImageParamsParser',
|
||||||
]
|
]
|
||||||
|
|||||||
188
py/recipes/parsers/sui_image_params.py
Normal file
188
py/recipes/parsers/sui_image_params.py
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
"""Parser for SuiImage (Stable Diffusion WebUI) metadata format."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Dict, Any, Optional, List
|
||||||
|
from ..base import RecipeMetadataParser
|
||||||
|
from ...services.metadata_service import get_default_metadata_provider
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SuiImageParamsParser(RecipeMetadataParser):
|
||||||
|
"""Parser for SuiImage metadata JSON format.
|
||||||
|
|
||||||
|
This format is used by some Stable Diffusion WebUI variants.
|
||||||
|
Structure:
|
||||||
|
{
|
||||||
|
"sui_image_params": {
|
||||||
|
"prompt": "...",
|
||||||
|
"negativeprompt": "...",
|
||||||
|
"model": "...",
|
||||||
|
"seed": ...,
|
||||||
|
"steps": ...,
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"sui_models": [
|
||||||
|
{"name": "...", "param": "model", "hash": "..."},
|
||||||
|
...
|
||||||
|
],
|
||||||
|
"sui_extra_data": {...}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_metadata_matching(self, user_comment: str) -> bool:
|
||||||
|
"""Check if the user comment matches the SuiImage metadata format"""
|
||||||
|
try:
|
||||||
|
data = json.loads(user_comment)
|
||||||
|
return isinstance(data, dict) and 'sui_image_params' in data
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def parse_metadata(self, user_comment: str, recipe_scanner=None, civitai_client=None) -> Dict[str, Any]:
|
||||||
|
"""Parse metadata from SuiImage metadata format"""
|
||||||
|
try:
|
||||||
|
metadata_provider = await get_default_metadata_provider()
|
||||||
|
|
||||||
|
data = json.loads(user_comment)
|
||||||
|
params = data.get('sui_image_params', {})
|
||||||
|
models = data.get('sui_models', [])
|
||||||
|
|
||||||
|
# Extract prompt and negative prompt
|
||||||
|
prompt = params.get('prompt', '')
|
||||||
|
negative_prompt = params.get('negativeprompt', '') or params.get('negative_prompt', '')
|
||||||
|
|
||||||
|
# Extract generation parameters
|
||||||
|
gen_params = {}
|
||||||
|
if prompt:
|
||||||
|
gen_params['prompt'] = prompt
|
||||||
|
if negative_prompt:
|
||||||
|
gen_params['negative_prompt'] = negative_prompt
|
||||||
|
|
||||||
|
# Map standard parameters
|
||||||
|
param_mapping = {
|
||||||
|
'steps': 'steps',
|
||||||
|
'seed': 'seed',
|
||||||
|
'cfgscale': 'cfg_scale',
|
||||||
|
'cfg_scale': 'cfg_scale',
|
||||||
|
'width': 'width',
|
||||||
|
'height': 'height',
|
||||||
|
'sampler': 'sampler',
|
||||||
|
'scheduler': 'scheduler',
|
||||||
|
'model': 'model',
|
||||||
|
'vae': 'vae',
|
||||||
|
}
|
||||||
|
|
||||||
|
for src_key, dest_key in param_mapping.items():
|
||||||
|
if src_key in params and params[src_key] is not None:
|
||||||
|
gen_params[dest_key] = params[src_key]
|
||||||
|
|
||||||
|
# Add size info if available
|
||||||
|
if 'width' in gen_params and 'height' in gen_params:
|
||||||
|
gen_params['size'] = f"{gen_params['width']}x{gen_params['height']}"
|
||||||
|
|
||||||
|
# Process models - extract checkpoint and loras
|
||||||
|
loras: List[Dict[str, Any]] = []
|
||||||
|
checkpoint: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
|
for model in models:
|
||||||
|
model_name = model.get('name', '')
|
||||||
|
param_type = model.get('param', '')
|
||||||
|
model_hash = model.get('hash', '')
|
||||||
|
|
||||||
|
# Remove .safetensors extension for cleaner name
|
||||||
|
clean_name = model_name.replace('.safetensors', '') if model_name else ''
|
||||||
|
|
||||||
|
# Check if this is a LoRA by looking at the name or param type
|
||||||
|
is_lora = 'lora' in model_name.lower() or param_type.lower().startswith('lora')
|
||||||
|
|
||||||
|
if is_lora:
|
||||||
|
lora_entry = {
|
||||||
|
'id': 0,
|
||||||
|
'modelId': 0,
|
||||||
|
'name': clean_name,
|
||||||
|
'version': '',
|
||||||
|
'type': 'lora',
|
||||||
|
'weight': 1.0,
|
||||||
|
'existsLocally': False,
|
||||||
|
'localPath': None,
|
||||||
|
'file_name': model_name,
|
||||||
|
'hash': model_hash.replace('0x', '') if model_hash.startswith('0x') else model_hash,
|
||||||
|
'thumbnailUrl': '/loras_static/images/no-preview.png',
|
||||||
|
'baseModel': '',
|
||||||
|
'size': 0,
|
||||||
|
'downloadUrl': '',
|
||||||
|
'isDeleted': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to get additional info from metadata provider
|
||||||
|
if metadata_provider and model_hash:
|
||||||
|
try:
|
||||||
|
civitai_info = await metadata_provider.get_model_by_hash(
|
||||||
|
model_hash.replace('0x', '') if model_hash.startswith('0x') else model_hash
|
||||||
|
)
|
||||||
|
if civitai_info:
|
||||||
|
lora_entry = await self.populate_lora_from_civitai(
|
||||||
|
lora_entry, civitai_info, recipe_scanner
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error fetching info for LoRA {clean_name}: {e}")
|
||||||
|
|
||||||
|
if lora_entry:
|
||||||
|
loras.append(lora_entry)
|
||||||
|
elif param_type == 'model' or 'lora' not in model_name.lower():
|
||||||
|
# This is likely a checkpoint
|
||||||
|
checkpoint_entry = {
|
||||||
|
'id': 0,
|
||||||
|
'modelId': 0,
|
||||||
|
'name': clean_name,
|
||||||
|
'version': '',
|
||||||
|
'type': 'checkpoint',
|
||||||
|
'hash': model_hash.replace('0x', '') if model_hash.startswith('0x') else model_hash,
|
||||||
|
'existsLocally': False,
|
||||||
|
'localPath': None,
|
||||||
|
'file_name': model_name,
|
||||||
|
'thumbnailUrl': '/loras_static/images/no-preview.png',
|
||||||
|
'baseModel': '',
|
||||||
|
'size': 0,
|
||||||
|
'downloadUrl': '',
|
||||||
|
'isDeleted': False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Try to get additional info from metadata provider
|
||||||
|
if metadata_provider and model_hash:
|
||||||
|
try:
|
||||||
|
civitai_info = await metadata_provider.get_model_by_hash(
|
||||||
|
model_hash.replace('0x', '') if model_hash.startswith('0x') else model_hash
|
||||||
|
)
|
||||||
|
if civitai_info:
|
||||||
|
checkpoint_entry = await self.populate_checkpoint_from_civitai(
|
||||||
|
checkpoint_entry, civitai_info
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error fetching info for checkpoint {clean_name}: {e}")
|
||||||
|
|
||||||
|
checkpoint = checkpoint_entry
|
||||||
|
|
||||||
|
# Determine base model from loras or checkpoint
|
||||||
|
base_model = None
|
||||||
|
if loras:
|
||||||
|
base_models = [lora.get('baseModel') for lora in loras if lora.get('baseModel')]
|
||||||
|
if base_models:
|
||||||
|
from collections import Counter
|
||||||
|
base_model_counts = Counter(base_models)
|
||||||
|
base_model = base_model_counts.most_common(1)[0][0]
|
||||||
|
elif checkpoint and checkpoint.get('baseModel'):
|
||||||
|
base_model = checkpoint['baseModel']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'base_model': base_model,
|
||||||
|
'loras': loras,
|
||||||
|
'checkpoint': checkpoint,
|
||||||
|
'gen_params': gen_params,
|
||||||
|
'from_sui_image_params': True
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error parsing SuiImage metadata: {e}", exc_info=True)
|
||||||
|
return {"error": str(e), "loras": []}
|
||||||
@@ -309,6 +309,13 @@ class ModelListingHandler:
|
|||||||
else:
|
else:
|
||||||
allow_selling_generated_content = None # None means no filter applied
|
allow_selling_generated_content = None # None means no filter applied
|
||||||
|
|
||||||
|
# Name pattern filters for LoRA Pool
|
||||||
|
name_pattern_include = request.query.getall("name_pattern_include", [])
|
||||||
|
name_pattern_exclude = request.query.getall("name_pattern_exclude", [])
|
||||||
|
name_pattern_use_regex = (
|
||||||
|
request.query.get("name_pattern_use_regex", "false").lower() == "true"
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"page": page,
|
"page": page,
|
||||||
"page_size": page_size,
|
"page_size": page_size,
|
||||||
@@ -328,6 +335,9 @@ class ModelListingHandler:
|
|||||||
"credit_required": credit_required,
|
"credit_required": credit_required,
|
||||||
"allow_selling_generated_content": allow_selling_generated_content,
|
"allow_selling_generated_content": allow_selling_generated_content,
|
||||||
"model_types": model_types,
|
"model_types": model_types,
|
||||||
|
"name_pattern_include": name_pattern_include,
|
||||||
|
"name_pattern_exclude": name_pattern_exclude,
|
||||||
|
"name_pattern_use_regex": name_pattern_use_regex,
|
||||||
**self._parse_specific_params(request),
|
**self._parse_specific_params(request),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -490,14 +490,33 @@ class CivitaiClient:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
url = f"{self.base_url}/images?imageId={image_id}&nsfw=X"
|
url = f"{self.base_url}/images?imageId={image_id}&nsfw=X"
|
||||||
|
requested_id = int(image_id)
|
||||||
|
|
||||||
logger.debug(f"Fetching image info for ID: {image_id}")
|
logger.debug(f"Fetching image info for ID: {image_id}")
|
||||||
success, result = await self._make_request("GET", url, use_auth=True)
|
success, result = await self._make_request("GET", url, use_auth=True)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
if result and "items" in result and len(result["items"]) > 0:
|
if result and "items" in result and isinstance(result["items"], list):
|
||||||
logger.debug(f"Successfully fetched image info for ID: {image_id}")
|
items = result["items"]
|
||||||
return result["items"][0]
|
|
||||||
|
# First, try to find the item with matching ID
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict) and item.get("id") == requested_id:
|
||||||
|
logger.debug(f"Successfully fetched image info for ID: {image_id}")
|
||||||
|
return item
|
||||||
|
|
||||||
|
# No matching ID found - log warning with details about returned items
|
||||||
|
returned_ids = [
|
||||||
|
item.get("id") for item in items
|
||||||
|
if isinstance(item, dict) and "id" in item
|
||||||
|
]
|
||||||
|
logger.warning(
|
||||||
|
f"CivitAI API returned no matching image for requested ID {image_id}. "
|
||||||
|
f"Returned {len(items)} item(s) with IDs: {returned_ids}. "
|
||||||
|
f"This may indicate the image was deleted, hidden, or there is a database lag."
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
logger.warning(f"No image found with ID: {image_id}")
|
logger.warning(f"No image found with ID: {image_id}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -505,6 +524,10 @@ class CivitaiClient:
|
|||||||
return None
|
return None
|
||||||
except RateLimitError:
|
except RateLimitError:
|
||||||
raise
|
raise
|
||||||
|
except ValueError as e:
|
||||||
|
error_msg = f"Invalid image ID format: {image_id}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"Error fetching image info: {e}"
|
error_msg = f"Error fetching image info: {e}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ from ..utils.civitai_utils import rewrite_preview_url
|
|||||||
from ..utils.preview_selection import select_preview_media
|
from ..utils.preview_selection import select_preview_media
|
||||||
from ..utils.utils import sanitize_folder_name
|
from ..utils.utils import sanitize_folder_name
|
||||||
from ..utils.exif_utils import ExifUtils
|
from ..utils.exif_utils import ExifUtils
|
||||||
from ..utils.file_utils import calculate_sha256
|
|
||||||
from ..utils.metadata_manager import MetadataManager
|
from ..utils.metadata_manager import MetadataManager
|
||||||
from .service_registry import ServiceRegistry
|
from .service_registry import ServiceRegistry
|
||||||
from .settings_manager import get_settings_manager
|
from .settings_manager import get_settings_manager
|
||||||
@@ -965,11 +964,12 @@ class DownloadManager:
|
|||||||
for download_url in download_urls:
|
for download_url in download_urls:
|
||||||
use_auth = download_url.startswith("https://civitai.com/api/download/")
|
use_auth = download_url.startswith("https://civitai.com/api/download/")
|
||||||
download_kwargs = {
|
download_kwargs = {
|
||||||
"progress_callback": lambda progress,
|
"progress_callback": lambda progress, snapshot=None: (
|
||||||
snapshot=None: self._handle_download_progress(
|
self._handle_download_progress(
|
||||||
progress,
|
progress,
|
||||||
progress_callback,
|
progress_callback,
|
||||||
snapshot,
|
snapshot,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
"use_auth": use_auth, # Only use authentication for Civitai downloads
|
"use_auth": use_auth, # Only use authentication for Civitai downloads
|
||||||
}
|
}
|
||||||
@@ -1238,7 +1238,8 @@ class DownloadManager:
|
|||||||
entry.file_name = os.path.splitext(os.path.basename(file_path))[0]
|
entry.file_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
# Update size to actual downloaded file size
|
# Update size to actual downloaded file size
|
||||||
entry.size = os.path.getsize(file_path)
|
entry.size = os.path.getsize(file_path)
|
||||||
entry.sha256 = await calculate_sha256(file_path)
|
# Use SHA256 from API metadata (already set in from_civitai_info)
|
||||||
|
# Do not recalculate to avoid blocking during ComfyUI execution
|
||||||
entries.append(entry)
|
entries.append(entry)
|
||||||
|
|
||||||
return entries
|
return entries
|
||||||
|
|||||||
@@ -44,7 +44,9 @@ class DownloadStreamControl:
|
|||||||
self._event.set()
|
self._event.set()
|
||||||
self._reconnect_requested = False
|
self._reconnect_requested = False
|
||||||
self.last_progress_timestamp: Optional[float] = None
|
self.last_progress_timestamp: Optional[float] = None
|
||||||
self.stall_timeout: float = float(stall_timeout) if stall_timeout is not None else 120.0
|
self.stall_timeout: float = (
|
||||||
|
float(stall_timeout) if stall_timeout is not None else 120.0
|
||||||
|
)
|
||||||
|
|
||||||
def is_set(self) -> bool:
|
def is_set(self) -> bool:
|
||||||
return self._event.is_set()
|
return self._event.is_set()
|
||||||
@@ -85,7 +87,9 @@ class DownloadStreamControl:
|
|||||||
self.last_progress_timestamp = timestamp or datetime.now().timestamp()
|
self.last_progress_timestamp = timestamp or datetime.now().timestamp()
|
||||||
self._reconnect_requested = False
|
self._reconnect_requested = False
|
||||||
|
|
||||||
def time_since_last_progress(self, *, now: Optional[float] = None) -> Optional[float]:
|
def time_since_last_progress(
|
||||||
|
self, *, now: Optional[float] = None
|
||||||
|
) -> Optional[float]:
|
||||||
if self.last_progress_timestamp is None:
|
if self.last_progress_timestamp is None:
|
||||||
return None
|
return None
|
||||||
reference = now if now is not None else datetime.now().timestamp()
|
reference = now if now is not None else datetime.now().timestamp()
|
||||||
@@ -105,10 +109,10 @@ class DownloadStalledError(Exception):
|
|||||||
|
|
||||||
class Downloader:
|
class Downloader:
|
||||||
"""Unified downloader for all HTTP/HTTPS downloads in the application."""
|
"""Unified downloader for all HTTP/HTTPS downloads in the application."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
_lock = asyncio.Lock()
|
_lock = asyncio.Lock()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_instance(cls):
|
async def get_instance(cls):
|
||||||
"""Get singleton instance of Downloader"""
|
"""Get singleton instance of Downloader"""
|
||||||
@@ -116,35 +120,37 @@ class Downloader:
|
|||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = cls()
|
cls._instance = cls()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize the downloader with optimal settings"""
|
"""Initialize the downloader with optimal settings"""
|
||||||
# Check if already initialized for singleton pattern
|
# Check if already initialized for singleton pattern
|
||||||
if hasattr(self, '_initialized'):
|
if hasattr(self, "_initialized"):
|
||||||
return
|
return
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
|
|
||||||
# Session management
|
# Session management
|
||||||
self._session = None
|
self._session = None
|
||||||
self._session_created_at = None
|
self._session_created_at = None
|
||||||
self._proxy_url = None # Store proxy URL for current session
|
self._proxy_url = None # Store proxy URL for current session
|
||||||
self._session_lock = asyncio.Lock()
|
self._session_lock = asyncio.Lock()
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
self.chunk_size = 4 * 1024 * 1024 # 4MB chunks for better throughput
|
self.chunk_size = (
|
||||||
|
16 * 1024 * 1024
|
||||||
|
) # 16MB chunks to balance I/O reduction and memory usage
|
||||||
self.max_retries = 5
|
self.max_retries = 5
|
||||||
self.base_delay = 2.0 # Base delay for exponential backoff
|
self.base_delay = 2.0 # Base delay for exponential backoff
|
||||||
self.session_timeout = 300 # 5 minutes
|
self.session_timeout = 300 # 5 minutes
|
||||||
self.stall_timeout = self._resolve_stall_timeout()
|
self.stall_timeout = self._resolve_stall_timeout()
|
||||||
|
|
||||||
# Default headers
|
# Default headers
|
||||||
self.default_headers = {
|
self.default_headers = {
|
||||||
'User-Agent': 'ComfyUI-LoRA-Manager/1.0',
|
"User-Agent": "ComfyUI-LoRA-Manager/1.0",
|
||||||
# Explicitly request uncompressed payloads so aiohttp doesn't need optional
|
# Explicitly request uncompressed payloads so aiohttp doesn't need optional
|
||||||
# decoders (e.g. zstandard) that may be missing in runtime environments.
|
# decoders (e.g. zstandard) that may be missing in runtime environments.
|
||||||
'Accept-Encoding': 'identity',
|
"Accept-Encoding": "identity",
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
async def session(self) -> aiohttp.ClientSession:
|
async def session(self) -> aiohttp.ClientSession:
|
||||||
"""Get or create the global aiohttp session with optimized settings"""
|
"""Get or create the global aiohttp session with optimized settings"""
|
||||||
@@ -158,7 +164,7 @@ class Downloader:
|
|||||||
@property
|
@property
|
||||||
def proxy_url(self) -> Optional[str]:
|
def proxy_url(self) -> Optional[str]:
|
||||||
"""Get the current proxy URL (initialize if needed)"""
|
"""Get the current proxy URL (initialize if needed)"""
|
||||||
if not hasattr(self, '_proxy_url'):
|
if not hasattr(self, "_proxy_url"):
|
||||||
self._proxy_url = None
|
self._proxy_url = None
|
||||||
return self._proxy_url
|
return self._proxy_url
|
||||||
|
|
||||||
@@ -169,14 +175,14 @@ class Downloader:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
settings_manager = get_settings_manager()
|
settings_manager = get_settings_manager()
|
||||||
settings_timeout = settings_manager.get('download_stall_timeout_seconds')
|
settings_timeout = settings_manager.get("download_stall_timeout_seconds")
|
||||||
except Exception as exc: # pragma: no cover - defensive guard
|
except Exception as exc: # pragma: no cover - defensive guard
|
||||||
logger.debug("Failed to read stall timeout from settings: %s", exc)
|
logger.debug("Failed to read stall timeout from settings: %s", exc)
|
||||||
|
|
||||||
raw_value = (
|
raw_value = (
|
||||||
settings_timeout
|
settings_timeout
|
||||||
if settings_timeout not in (None, "")
|
if settings_timeout not in (None, "")
|
||||||
else os.environ.get('COMFYUI_DOWNLOAD_STALL_TIMEOUT')
|
else os.environ.get("COMFYUI_DOWNLOAD_STALL_TIMEOUT")
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -190,93 +196,104 @@ class Downloader:
|
|||||||
"""Check if session should be refreshed"""
|
"""Check if session should be refreshed"""
|
||||||
if self._session is None:
|
if self._session is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if not hasattr(self, '_session_created_at') or self._session_created_at is None:
|
if not hasattr(self, "_session_created_at") or self._session_created_at is None:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Refresh if session is older than timeout
|
# Refresh if session is older than timeout
|
||||||
if (datetime.now() - self._session_created_at).total_seconds() > self.session_timeout:
|
if (
|
||||||
|
datetime.now() - self._session_created_at
|
||||||
|
).total_seconds() > self.session_timeout:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def _create_session(self):
|
async def _create_session(self):
|
||||||
"""Create a new aiohttp session with optimized settings.
|
"""Create a new aiohttp session with optimized settings.
|
||||||
|
|
||||||
Note: This is private and caller MUST hold self._session_lock.
|
Note: This is private and caller MUST hold self._session_lock.
|
||||||
"""
|
"""
|
||||||
# Close existing session if any
|
# Close existing session if any
|
||||||
if self._session is not None:
|
if self._session is not None:
|
||||||
try:
|
try:
|
||||||
await self._session.close()
|
await self._session.close()
|
||||||
except Exception as e: # pragma: no cover
|
except Exception as e: # pragma: no cover
|
||||||
logger.warning(f"Error closing previous session: {e}")
|
logger.warning(f"Error closing previous session: {e}")
|
||||||
finally:
|
finally:
|
||||||
self._session = None
|
self._session = None
|
||||||
|
|
||||||
# Check for app-level proxy settings
|
# Check for app-level proxy settings
|
||||||
proxy_url = None
|
proxy_url = None
|
||||||
settings_manager = get_settings_manager()
|
settings_manager = get_settings_manager()
|
||||||
if settings_manager.get('proxy_enabled', False):
|
if settings_manager.get("proxy_enabled", False):
|
||||||
proxy_host = settings_manager.get('proxy_host', '').strip()
|
proxy_host = settings_manager.get("proxy_host", "").strip()
|
||||||
proxy_port = settings_manager.get('proxy_port', '').strip()
|
proxy_port = settings_manager.get("proxy_port", "").strip()
|
||||||
proxy_type = settings_manager.get('proxy_type', 'http').lower()
|
proxy_type = settings_manager.get("proxy_type", "http").lower()
|
||||||
proxy_username = settings_manager.get('proxy_username', '').strip()
|
proxy_username = settings_manager.get("proxy_username", "").strip()
|
||||||
proxy_password = settings_manager.get('proxy_password', '').strip()
|
proxy_password = settings_manager.get("proxy_password", "").strip()
|
||||||
|
|
||||||
if proxy_host and proxy_port:
|
if proxy_host and proxy_port:
|
||||||
# Build proxy URL
|
# Build proxy URL
|
||||||
if proxy_username and proxy_password:
|
if proxy_username and proxy_password:
|
||||||
proxy_url = f"{proxy_type}://{proxy_username}:{proxy_password}@{proxy_host}:{proxy_port}"
|
proxy_url = f"{proxy_type}://{proxy_username}:{proxy_password}@{proxy_host}:{proxy_port}"
|
||||||
else:
|
else:
|
||||||
proxy_url = f"{proxy_type}://{proxy_host}:{proxy_port}"
|
proxy_url = f"{proxy_type}://{proxy_host}:{proxy_port}"
|
||||||
|
|
||||||
logger.debug(f"Using app-level proxy: {proxy_type}://{proxy_host}:{proxy_port}")
|
logger.debug(
|
||||||
|
f"Using app-level proxy: {proxy_type}://{proxy_host}:{proxy_port}"
|
||||||
|
)
|
||||||
logger.debug("Proxy mode: app-level proxy is active.")
|
logger.debug("Proxy mode: app-level proxy is active.")
|
||||||
else:
|
else:
|
||||||
logger.debug("Proxy mode: system-level proxy (trust_env) will be used if configured in environment.")
|
logger.debug(
|
||||||
|
"Proxy mode: system-level proxy (trust_env) will be used if configured in environment."
|
||||||
|
)
|
||||||
# Optimize TCP connection parameters
|
# Optimize TCP connection parameters
|
||||||
connector = aiohttp.TCPConnector(
|
connector = aiohttp.TCPConnector(
|
||||||
ssl=True,
|
ssl=True,
|
||||||
limit=8, # Concurrent connections
|
limit=8, # Concurrent connections
|
||||||
ttl_dns_cache=300, # DNS cache timeout
|
ttl_dns_cache=300, # DNS cache timeout
|
||||||
force_close=False, # Keep connections for reuse
|
force_close=False, # Keep connections for reuse
|
||||||
enable_cleanup_closed=True
|
enable_cleanup_closed=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configure timeout parameters
|
# Configure timeout parameters
|
||||||
timeout = aiohttp.ClientTimeout(
|
timeout = aiohttp.ClientTimeout(
|
||||||
total=None, # No total timeout for large downloads
|
total=None, # No total timeout for large downloads
|
||||||
connect=60, # Connection timeout
|
connect=60, # Connection timeout
|
||||||
sock_read=300 # 5 minute socket read timeout
|
sock_read=300, # 5 minute socket read timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
self._session = aiohttp.ClientSession(
|
self._session = aiohttp.ClientSession(
|
||||||
connector=connector,
|
connector=connector,
|
||||||
trust_env=proxy_url is None, # Only use system proxy if no app-level proxy is set
|
trust_env=proxy_url
|
||||||
timeout=timeout
|
is None, # Only use system proxy if no app-level proxy is set
|
||||||
|
timeout=timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Store proxy URL for use in requests
|
# Store proxy URL for use in requests
|
||||||
self._proxy_url = proxy_url
|
self._proxy_url = proxy_url
|
||||||
self._session_created_at = datetime.now()
|
self._session_created_at = datetime.now()
|
||||||
|
|
||||||
logger.debug("Created new HTTP session with proxy settings. App-level proxy: %s, System-level proxy (trust_env): %s", bool(proxy_url), proxy_url is None)
|
logger.debug(
|
||||||
|
"Created new HTTP session with proxy settings. App-level proxy: %s, System-level proxy (trust_env): %s",
|
||||||
|
bool(proxy_url),
|
||||||
|
proxy_url is None,
|
||||||
|
)
|
||||||
|
|
||||||
def _get_auth_headers(self, use_auth: bool = False) -> Dict[str, str]:
|
def _get_auth_headers(self, use_auth: bool = False) -> Dict[str, str]:
|
||||||
"""Get headers with optional authentication"""
|
"""Get headers with optional authentication"""
|
||||||
headers = self.default_headers.copy()
|
headers = self.default_headers.copy()
|
||||||
|
|
||||||
if use_auth:
|
if use_auth:
|
||||||
# Add CivitAI API key if available
|
# Add CivitAI API key if available
|
||||||
settings_manager = get_settings_manager()
|
settings_manager = get_settings_manager()
|
||||||
api_key = settings_manager.get('civitai_api_key')
|
api_key = settings_manager.get("civitai_api_key")
|
||||||
if api_key:
|
if api_key:
|
||||||
headers['Authorization'] = f'Bearer {api_key}'
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
headers['Content-Type'] = 'application/json'
|
headers["Content-Type"] = "application/json"
|
||||||
|
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
async def download_file(
|
async def download_file(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
@@ -289,7 +306,7 @@ class Downloader:
|
|||||||
) -> Tuple[bool, str]:
|
) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
Download a file with resumable downloads and retry mechanism
|
Download a file with resumable downloads and retry mechanism
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: Download URL
|
url: Download URL
|
||||||
save_path: Full path where the file should be saved
|
save_path: Full path where the file should be saved
|
||||||
@@ -298,75 +315,96 @@ class Downloader:
|
|||||||
custom_headers: Additional headers to include in request
|
custom_headers: Additional headers to include in request
|
||||||
allow_resume: Whether to support resumable downloads
|
allow_resume: Whether to support resumable downloads
|
||||||
pause_event: Optional stream control used to pause/resume and request reconnects
|
pause_event: Optional stream control used to pause/resume and request reconnects
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[bool, str]: (success, save_path or error message)
|
Tuple[bool, str]: (success, save_path or error message)
|
||||||
"""
|
"""
|
||||||
retry_count = 0
|
retry_count = 0
|
||||||
part_path = save_path + '.part' if allow_resume else save_path
|
part_path = save_path + ".part" if allow_resume else save_path
|
||||||
|
|
||||||
# Prepare headers
|
# Prepare headers
|
||||||
headers = self._get_auth_headers(use_auth)
|
headers = self._get_auth_headers(use_auth)
|
||||||
if custom_headers:
|
if custom_headers:
|
||||||
headers.update(custom_headers)
|
headers.update(custom_headers)
|
||||||
|
|
||||||
# Get existing file size for resume
|
# Get existing file size for resume
|
||||||
resume_offset = 0
|
resume_offset = 0
|
||||||
if allow_resume and os.path.exists(part_path):
|
if allow_resume and os.path.exists(part_path):
|
||||||
resume_offset = os.path.getsize(part_path)
|
resume_offset = os.path.getsize(part_path)
|
||||||
logger.info(f"Resuming download from offset {resume_offset} bytes")
|
logger.info(f"Resuming download from offset {resume_offset} bytes")
|
||||||
|
|
||||||
total_size = 0
|
total_size = 0
|
||||||
|
|
||||||
while retry_count <= self.max_retries:
|
while retry_count <= self.max_retries:
|
||||||
try:
|
try:
|
||||||
session = await self.session
|
session = await self.session
|
||||||
# Debug log for proxy mode at request time
|
# Debug log for proxy mode at request time
|
||||||
if self.proxy_url:
|
if self.proxy_url:
|
||||||
logger.debug(f"[download_file] Using app-level proxy: {self.proxy_url}")
|
logger.debug(
|
||||||
|
f"[download_file] Using app-level proxy: {self.proxy_url}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug("[download_file] Using system-level proxy (trust_env) if configured.")
|
logger.debug(
|
||||||
|
"[download_file] Using system-level proxy (trust_env) if configured."
|
||||||
|
)
|
||||||
|
|
||||||
# Add Range header for resume if we have partial data
|
# Add Range header for resume if we have partial data
|
||||||
request_headers = headers.copy()
|
request_headers = headers.copy()
|
||||||
if allow_resume and resume_offset > 0:
|
if allow_resume and resume_offset > 0:
|
||||||
request_headers['Range'] = f'bytes={resume_offset}-'
|
request_headers["Range"] = f"bytes={resume_offset}-"
|
||||||
|
|
||||||
# Disable compression for better chunked downloads
|
# Disable compression for better chunked downloads
|
||||||
request_headers['Accept-Encoding'] = 'identity'
|
request_headers["Accept-Encoding"] = "identity"
|
||||||
|
|
||||||
logger.debug(f"Download attempt {retry_count + 1}/{self.max_retries + 1} from: {url}")
|
logger.debug(
|
||||||
|
f"Download attempt {retry_count + 1}/{self.max_retries + 1} from: {url}"
|
||||||
|
)
|
||||||
if resume_offset > 0:
|
if resume_offset > 0:
|
||||||
logger.debug(f"Requesting range from byte {resume_offset}")
|
logger.debug(f"Requesting range from byte {resume_offset}")
|
||||||
|
|
||||||
async with session.get(url, headers=request_headers, allow_redirects=True, proxy=self.proxy_url) as response:
|
async with session.get(
|
||||||
|
url,
|
||||||
|
headers=request_headers,
|
||||||
|
allow_redirects=True,
|
||||||
|
proxy=self.proxy_url,
|
||||||
|
) as response:
|
||||||
# Handle different response codes
|
# Handle different response codes
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
# Full content response
|
# Full content response
|
||||||
if resume_offset > 0:
|
if resume_offset > 0:
|
||||||
# Server doesn't support ranges, restart from beginning
|
# Server doesn't support ranges, restart from beginning
|
||||||
logger.warning("Server doesn't support range requests, restarting download")
|
logger.warning(
|
||||||
|
"Server doesn't support range requests, restarting download"
|
||||||
|
)
|
||||||
resume_offset = 0
|
resume_offset = 0
|
||||||
if os.path.exists(part_path):
|
if os.path.exists(part_path):
|
||||||
os.remove(part_path)
|
os.remove(part_path)
|
||||||
elif response.status == 206:
|
elif response.status == 206:
|
||||||
# Partial content response (resume successful)
|
# Partial content response (resume successful)
|
||||||
content_range = response.headers.get('Content-Range')
|
content_range = response.headers.get("Content-Range")
|
||||||
if content_range:
|
if content_range:
|
||||||
# Parse total size from Content-Range header (e.g., "bytes 1024-2047/2048")
|
# Parse total size from Content-Range header (e.g., "bytes 1024-2047/2048")
|
||||||
range_parts = content_range.split('/')
|
range_parts = content_range.split("/")
|
||||||
if len(range_parts) == 2:
|
if len(range_parts) == 2:
|
||||||
total_size = int(range_parts[1])
|
total_size = int(range_parts[1])
|
||||||
logger.info(f"Successfully resumed download from byte {resume_offset}")
|
logger.info(
|
||||||
|
f"Successfully resumed download from byte {resume_offset}"
|
||||||
|
)
|
||||||
elif response.status == 416:
|
elif response.status == 416:
|
||||||
# Range not satisfiable - file might be complete or corrupted
|
# Range not satisfiable - file might be complete or corrupted
|
||||||
if allow_resume and os.path.exists(part_path):
|
if allow_resume and os.path.exists(part_path):
|
||||||
part_size = os.path.getsize(part_path)
|
part_size = os.path.getsize(part_path)
|
||||||
logger.warning(f"Range not satisfiable. Part file size: {part_size}")
|
logger.warning(
|
||||||
|
f"Range not satisfiable. Part file size: {part_size}"
|
||||||
|
)
|
||||||
# Try to get actual file size
|
# Try to get actual file size
|
||||||
head_response = await session.head(url, headers=headers, proxy=self.proxy_url)
|
head_response = await session.head(
|
||||||
|
url, headers=headers, proxy=self.proxy_url
|
||||||
|
)
|
||||||
if head_response.status == 200:
|
if head_response.status == 200:
|
||||||
actual_size = int(head_response.headers.get('content-length', 0))
|
actual_size = int(
|
||||||
|
head_response.headers.get("content-length", 0)
|
||||||
|
)
|
||||||
if part_size == actual_size:
|
if part_size == actual_size:
|
||||||
# File is complete, just rename it
|
# File is complete, just rename it
|
||||||
if allow_resume:
|
if allow_resume:
|
||||||
@@ -388,25 +426,40 @@ class Downloader:
|
|||||||
resume_offset = 0
|
resume_offset = 0
|
||||||
continue
|
continue
|
||||||
elif response.status == 401:
|
elif response.status == 401:
|
||||||
logger.warning(f"Unauthorized access to resource: {url} (Status 401)")
|
logger.warning(
|
||||||
return False, "Invalid or missing API key, or early access restriction."
|
f"Unauthorized access to resource: {url} (Status 401)"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
"Invalid or missing API key, or early access restriction.",
|
||||||
|
)
|
||||||
elif response.status == 403:
|
elif response.status == 403:
|
||||||
logger.warning(f"Forbidden access to resource: {url} (Status 403)")
|
logger.warning(
|
||||||
return False, "Access forbidden: You don't have permission to download this file."
|
f"Forbidden access to resource: {url} (Status 403)"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
"Access forbidden: You don't have permission to download this file.",
|
||||||
|
)
|
||||||
elif response.status == 404:
|
elif response.status == 404:
|
||||||
logger.warning(f"Resource not found: {url} (Status 404)")
|
logger.warning(f"Resource not found: {url} (Status 404)")
|
||||||
return False, "File not found - the download link may be invalid or expired."
|
return (
|
||||||
|
False,
|
||||||
|
"File not found - the download link may be invalid or expired.",
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Download failed for {url} with status {response.status}")
|
logger.error(
|
||||||
|
f"Download failed for {url} with status {response.status}"
|
||||||
|
)
|
||||||
return False, f"Download failed with status {response.status}"
|
return False, f"Download failed with status {response.status}"
|
||||||
|
|
||||||
# Get total file size for progress calculation (if not set from Content-Range)
|
# Get total file size for progress calculation (if not set from Content-Range)
|
||||||
if total_size == 0:
|
if total_size == 0:
|
||||||
total_size = int(response.headers.get('content-length', 0))
|
total_size = int(response.headers.get("content-length", 0))
|
||||||
if response.status == 206:
|
if response.status == 206:
|
||||||
# For partial content, add the offset to get total file size
|
# For partial content, add the offset to get total file size
|
||||||
total_size += resume_offset
|
total_size += resume_offset
|
||||||
|
|
||||||
current_size = resume_offset
|
current_size = resume_offset
|
||||||
last_progress_report_time = datetime.now()
|
last_progress_report_time = datetime.now()
|
||||||
progress_samples: deque[tuple[datetime, int]] = deque()
|
progress_samples: deque[tuple[datetime, int]] = deque()
|
||||||
@@ -417,7 +470,7 @@ class Downloader:
|
|||||||
|
|
||||||
# Stream download to file with progress updates
|
# Stream download to file with progress updates
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
mode = 'ab' if (allow_resume and resume_offset > 0) else 'wb'
|
mode = "ab" if (allow_resume and resume_offset > 0) else "wb"
|
||||||
control = pause_event
|
control = pause_event
|
||||||
|
|
||||||
if control is not None:
|
if control is not None:
|
||||||
@@ -425,7 +478,9 @@ class Downloader:
|
|||||||
|
|
||||||
with open(part_path, mode) as f:
|
with open(part_path, mode) as f:
|
||||||
while True:
|
while True:
|
||||||
active_stall_timeout = control.stall_timeout if control else self.stall_timeout
|
active_stall_timeout = (
|
||||||
|
control.stall_timeout if control else self.stall_timeout
|
||||||
|
)
|
||||||
|
|
||||||
if control is not None:
|
if control is not None:
|
||||||
if control.is_paused():
|
if control.is_paused():
|
||||||
@@ -437,7 +492,9 @@ class Downloader:
|
|||||||
"Reconnect requested after resume"
|
"Reconnect requested after resume"
|
||||||
)
|
)
|
||||||
elif control.consume_reconnect_request():
|
elif control.consume_reconnect_request():
|
||||||
raise DownloadRestartRequested("Reconnect requested")
|
raise DownloadRestartRequested(
|
||||||
|
"Reconnect requested"
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
chunk = await asyncio.wait_for(
|
chunk = await asyncio.wait_for(
|
||||||
@@ -466,22 +523,32 @@ class Downloader:
|
|||||||
control.mark_progress(timestamp=now.timestamp())
|
control.mark_progress(timestamp=now.timestamp())
|
||||||
|
|
||||||
# Limit progress update frequency to reduce overhead
|
# Limit progress update frequency to reduce overhead
|
||||||
time_diff = (now - last_progress_report_time).total_seconds()
|
time_diff = (
|
||||||
|
now - last_progress_report_time
|
||||||
|
).total_seconds()
|
||||||
|
|
||||||
if progress_callback and time_diff >= 1.0:
|
if progress_callback and time_diff >= 1.0:
|
||||||
progress_samples.append((now, current_size))
|
progress_samples.append((now, current_size))
|
||||||
cutoff = now - timedelta(seconds=5)
|
cutoff = now - timedelta(seconds=5)
|
||||||
while progress_samples and progress_samples[0][0] < cutoff:
|
while (
|
||||||
|
progress_samples and progress_samples[0][0] < cutoff
|
||||||
|
):
|
||||||
progress_samples.popleft()
|
progress_samples.popleft()
|
||||||
|
|
||||||
percent = (current_size / total_size) * 100 if total_size else 0.0
|
percent = (
|
||||||
|
(current_size / total_size) * 100
|
||||||
|
if total_size
|
||||||
|
else 0.0
|
||||||
|
)
|
||||||
bytes_per_second = 0.0
|
bytes_per_second = 0.0
|
||||||
if len(progress_samples) >= 2:
|
if len(progress_samples) >= 2:
|
||||||
first_time, first_bytes = progress_samples[0]
|
first_time, first_bytes = progress_samples[0]
|
||||||
last_time, last_bytes = progress_samples[-1]
|
last_time, last_bytes = progress_samples[-1]
|
||||||
elapsed = (last_time - first_time).total_seconds()
|
elapsed = (last_time - first_time).total_seconds()
|
||||||
if elapsed > 0:
|
if elapsed > 0:
|
||||||
bytes_per_second = (last_bytes - first_bytes) / elapsed
|
bytes_per_second = (
|
||||||
|
last_bytes - first_bytes
|
||||||
|
) / elapsed
|
||||||
|
|
||||||
progress_snapshot = DownloadProgress(
|
progress_snapshot = DownloadProgress(
|
||||||
percent_complete=percent,
|
percent_complete=percent,
|
||||||
@@ -491,21 +558,23 @@ class Downloader:
|
|||||||
timestamp=now.timestamp(),
|
timestamp=now.timestamp(),
|
||||||
)
|
)
|
||||||
|
|
||||||
await self._dispatch_progress_callback(progress_callback, progress_snapshot)
|
await self._dispatch_progress_callback(
|
||||||
|
progress_callback, progress_snapshot
|
||||||
|
)
|
||||||
last_progress_report_time = now
|
last_progress_report_time = now
|
||||||
|
|
||||||
# Download completed successfully
|
# Download completed successfully
|
||||||
# Verify file size integrity before finalizing
|
# Verify file size integrity before finalizing
|
||||||
final_size = os.path.getsize(part_path) if os.path.exists(part_path) else 0
|
final_size = (
|
||||||
|
os.path.getsize(part_path) if os.path.exists(part_path) else 0
|
||||||
|
)
|
||||||
expected_size = total_size if total_size > 0 else None
|
expected_size = total_size if total_size > 0 else None
|
||||||
|
|
||||||
integrity_error: Optional[str] = None
|
integrity_error: Optional[str] = None
|
||||||
if final_size <= 0:
|
if final_size <= 0:
|
||||||
integrity_error = "Downloaded file is empty"
|
integrity_error = "Downloaded file is empty"
|
||||||
elif expected_size is not None and final_size != expected_size:
|
elif expected_size is not None and final_size != expected_size:
|
||||||
integrity_error = (
|
integrity_error = f"File size mismatch. Expected: {expected_size}, Got: {final_size}"
|
||||||
f"File size mismatch. Expected: {expected_size}, Got: {final_size}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if integrity_error is not None:
|
if integrity_error is not None:
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -554,8 +623,10 @@ class Downloader:
|
|||||||
max_rename_attempts = 5
|
max_rename_attempts = 5
|
||||||
rename_attempt = 0
|
rename_attempt = 0
|
||||||
rename_success = False
|
rename_success = False
|
||||||
|
|
||||||
while rename_attempt < max_rename_attempts and not rename_success:
|
while (
|
||||||
|
rename_attempt < max_rename_attempts and not rename_success
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
# If the destination file exists, remove it first (Windows safe)
|
# If the destination file exists, remove it first (Windows safe)
|
||||||
if os.path.exists(save_path):
|
if os.path.exists(save_path):
|
||||||
@@ -566,11 +637,18 @@ class Downloader:
|
|||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
rename_attempt += 1
|
rename_attempt += 1
|
||||||
if rename_attempt < max_rename_attempts:
|
if rename_attempt < max_rename_attempts:
|
||||||
logger.info(f"File still in use, retrying rename in 2 seconds (attempt {rename_attempt}/{max_rename_attempts})")
|
logger.info(
|
||||||
|
f"File still in use, retrying rename in 2 seconds (attempt {rename_attempt}/{max_rename_attempts})"
|
||||||
|
)
|
||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to rename file after {max_rename_attempts} attempts: {e}")
|
logger.error(
|
||||||
return False, f"Failed to finalize download: {str(e)}"
|
f"Failed to rename file after {max_rename_attempts} attempts: {e}"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"Failed to finalize download: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
final_size = os.path.getsize(save_path)
|
final_size = os.path.getsize(save_path)
|
||||||
|
|
||||||
@@ -583,11 +661,12 @@ class Downloader:
|
|||||||
bytes_per_second=0.0,
|
bytes_per_second=0.0,
|
||||||
timestamp=datetime.now().timestamp(),
|
timestamp=datetime.now().timestamp(),
|
||||||
)
|
)
|
||||||
await self._dispatch_progress_callback(progress_callback, final_snapshot)
|
await self._dispatch_progress_callback(
|
||||||
|
progress_callback, final_snapshot
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
return True, save_path
|
return True, save_path
|
||||||
|
|
||||||
except (
|
except (
|
||||||
aiohttp.ClientError,
|
aiohttp.ClientError,
|
||||||
aiohttp.ClientPayloadError,
|
aiohttp.ClientPayloadError,
|
||||||
@@ -597,30 +676,35 @@ class Downloader:
|
|||||||
DownloadRestartRequested,
|
DownloadRestartRequested,
|
||||||
) as e:
|
) as e:
|
||||||
retry_count += 1
|
retry_count += 1
|
||||||
logger.warning(f"Network error during download (attempt {retry_count}/{self.max_retries + 1}): {e}")
|
logger.warning(
|
||||||
|
f"Network error during download (attempt {retry_count}/{self.max_retries + 1}): {e}"
|
||||||
|
)
|
||||||
|
|
||||||
if retry_count <= self.max_retries:
|
if retry_count <= self.max_retries:
|
||||||
# Calculate delay with exponential backoff
|
# Calculate delay with exponential backoff
|
||||||
delay = self.base_delay * (2 ** (retry_count - 1))
|
delay = self.base_delay * (2 ** (retry_count - 1))
|
||||||
logger.info(f"Retrying in {delay} seconds...")
|
logger.info(f"Retrying in {delay} seconds...")
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
# Update resume offset for next attempt
|
# Update resume offset for next attempt
|
||||||
if allow_resume and os.path.exists(part_path):
|
if allow_resume and os.path.exists(part_path):
|
||||||
resume_offset = os.path.getsize(part_path)
|
resume_offset = os.path.getsize(part_path)
|
||||||
logger.info(f"Will resume from byte {resume_offset}")
|
logger.info(f"Will resume from byte {resume_offset}")
|
||||||
|
|
||||||
# Refresh session to get new connection
|
# Refresh session to get new connection
|
||||||
await self._create_session()
|
await self._create_session()
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
logger.error(f"Max retries exceeded for download: {e}")
|
logger.error(f"Max retries exceeded for download: {e}")
|
||||||
return False, f"Network error after {self.max_retries + 1} attempts: {str(e)}"
|
return (
|
||||||
|
False,
|
||||||
|
f"Network error after {self.max_retries + 1} attempts: {str(e)}",
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected download error: {e}")
|
logger.error(f"Unexpected download error: {e}")
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
return False, f"Download failed after {self.max_retries + 1} attempts"
|
return False, f"Download failed after {self.max_retries + 1} attempts"
|
||||||
|
|
||||||
async def _dispatch_progress_callback(
|
async def _dispatch_progress_callback(
|
||||||
@@ -645,17 +729,17 @@ class Downloader:
|
|||||||
url: str,
|
url: str,
|
||||||
use_auth: bool = False,
|
use_auth: bool = False,
|
||||||
custom_headers: Optional[Dict[str, str]] = None,
|
custom_headers: Optional[Dict[str, str]] = None,
|
||||||
return_headers: bool = False
|
return_headers: bool = False,
|
||||||
) -> Tuple[bool, Union[bytes, str], Optional[Dict]]:
|
) -> Tuple[bool, Union[bytes, str], Optional[Dict]]:
|
||||||
"""
|
"""
|
||||||
Download a file to memory (for small files like preview images)
|
Download a file to memory (for small files like preview images)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: Download URL
|
url: Download URL
|
||||||
use_auth: Whether to include authentication headers
|
use_auth: Whether to include authentication headers
|
||||||
custom_headers: Additional headers to include in request
|
custom_headers: Additional headers to include in request
|
||||||
return_headers: Whether to return response headers along with content
|
return_headers: Whether to return response headers along with content
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[bool, Union[bytes, str], Optional[Dict]]: (success, content or error message, response headers if requested)
|
Tuple[bool, Union[bytes, str], Optional[Dict]]: (success, content or error message, response headers if requested)
|
||||||
"""
|
"""
|
||||||
@@ -663,16 +747,22 @@ class Downloader:
|
|||||||
session = await self.session
|
session = await self.session
|
||||||
# Debug log for proxy mode at request time
|
# Debug log for proxy mode at request time
|
||||||
if self.proxy_url:
|
if self.proxy_url:
|
||||||
logger.debug(f"[download_to_memory] Using app-level proxy: {self.proxy_url}")
|
logger.debug(
|
||||||
|
f"[download_to_memory] Using app-level proxy: {self.proxy_url}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug("[download_to_memory] Using system-level proxy (trust_env) if configured.")
|
logger.debug(
|
||||||
|
"[download_to_memory] Using system-level proxy (trust_env) if configured."
|
||||||
|
)
|
||||||
|
|
||||||
# Prepare headers
|
# Prepare headers
|
||||||
headers = self._get_auth_headers(use_auth)
|
headers = self._get_auth_headers(use_auth)
|
||||||
if custom_headers:
|
if custom_headers:
|
||||||
headers.update(custom_headers)
|
headers.update(custom_headers)
|
||||||
|
|
||||||
async with session.get(url, headers=headers, proxy=self.proxy_url) as response:
|
async with session.get(
|
||||||
|
url, headers=headers, proxy=self.proxy_url
|
||||||
|
) as response:
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
content = await response.read()
|
content = await response.read()
|
||||||
if return_headers:
|
if return_headers:
|
||||||
@@ -691,25 +781,25 @@ class Downloader:
|
|||||||
else:
|
else:
|
||||||
error_msg = f"Download failed with status {response.status}"
|
error_msg = f"Download failed with status {response.status}"
|
||||||
return False, error_msg, None
|
return False, error_msg, None
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error downloading to memory from {url}: {e}")
|
logger.error(f"Error downloading to memory from {url}: {e}")
|
||||||
return False, str(e), None
|
return False, str(e), None
|
||||||
|
|
||||||
async def get_response_headers(
|
async def get_response_headers(
|
||||||
self,
|
self,
|
||||||
url: str,
|
url: str,
|
||||||
use_auth: bool = False,
|
use_auth: bool = False,
|
||||||
custom_headers: Optional[Dict[str, str]] = None
|
custom_headers: Optional[Dict[str, str]] = None,
|
||||||
) -> Tuple[bool, Union[Dict, str]]:
|
) -> Tuple[bool, Union[Dict, str]]:
|
||||||
"""
|
"""
|
||||||
Get response headers without downloading the full content
|
Get response headers without downloading the full content
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
url: URL to check
|
url: URL to check
|
||||||
use_auth: Whether to include authentication headers
|
use_auth: Whether to include authentication headers
|
||||||
custom_headers: Additional headers to include in request
|
custom_headers: Additional headers to include in request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[bool, Union[Dict, str]]: (success, headers dict or error message)
|
Tuple[bool, Union[Dict, str]]: (success, headers dict or error message)
|
||||||
"""
|
"""
|
||||||
@@ -717,43 +807,49 @@ class Downloader:
|
|||||||
session = await self.session
|
session = await self.session
|
||||||
# Debug log for proxy mode at request time
|
# Debug log for proxy mode at request time
|
||||||
if self.proxy_url:
|
if self.proxy_url:
|
||||||
logger.debug(f"[get_response_headers] Using app-level proxy: {self.proxy_url}")
|
logger.debug(
|
||||||
|
f"[get_response_headers] Using app-level proxy: {self.proxy_url}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug("[get_response_headers] Using system-level proxy (trust_env) if configured.")
|
logger.debug(
|
||||||
|
"[get_response_headers] Using system-level proxy (trust_env) if configured."
|
||||||
|
)
|
||||||
|
|
||||||
# Prepare headers
|
# Prepare headers
|
||||||
headers = self._get_auth_headers(use_auth)
|
headers = self._get_auth_headers(use_auth)
|
||||||
if custom_headers:
|
if custom_headers:
|
||||||
headers.update(custom_headers)
|
headers.update(custom_headers)
|
||||||
|
|
||||||
async with session.head(url, headers=headers, proxy=self.proxy_url) as response:
|
async with session.head(
|
||||||
|
url, headers=headers, proxy=self.proxy_url
|
||||||
|
) as response:
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
return True, dict(response.headers)
|
return True, dict(response.headers)
|
||||||
else:
|
else:
|
||||||
return False, f"Head request failed with status {response.status}"
|
return False, f"Head request failed with status {response.status}"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting headers from {url}: {e}")
|
logger.error(f"Error getting headers from {url}: {e}")
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
async def make_request(
|
async def make_request(
|
||||||
self,
|
self,
|
||||||
method: str,
|
method: str,
|
||||||
url: str,
|
url: str,
|
||||||
use_auth: bool = False,
|
use_auth: bool = False,
|
||||||
custom_headers: Optional[Dict[str, str]] = None,
|
custom_headers: Optional[Dict[str, str]] = None,
|
||||||
**kwargs
|
**kwargs,
|
||||||
) -> Tuple[bool, Union[Dict, str]]:
|
) -> Tuple[bool, Union[Dict, str]]:
|
||||||
"""
|
"""
|
||||||
Make a generic HTTP request and return JSON response
|
Make a generic HTTP request and return JSON response
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
method: HTTP method (GET, POST, etc.)
|
method: HTTP method (GET, POST, etc.)
|
||||||
url: Request URL
|
url: Request URL
|
||||||
use_auth: Whether to include authentication headers
|
use_auth: Whether to include authentication headers
|
||||||
custom_headers: Additional headers to include in request
|
custom_headers: Additional headers to include in request
|
||||||
**kwargs: Additional arguments for aiohttp request
|
**kwargs: Additional arguments for aiohttp request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple[bool, Union[Dict, str]]: (success, response data or error message)
|
Tuple[bool, Union[Dict, str]]: (success, response data or error message)
|
||||||
"""
|
"""
|
||||||
@@ -763,18 +859,22 @@ class Downloader:
|
|||||||
if self.proxy_url:
|
if self.proxy_url:
|
||||||
logger.debug(f"[make_request] Using app-level proxy: {self.proxy_url}")
|
logger.debug(f"[make_request] Using app-level proxy: {self.proxy_url}")
|
||||||
else:
|
else:
|
||||||
logger.debug("[make_request] Using system-level proxy (trust_env) if configured.")
|
logger.debug(
|
||||||
|
"[make_request] Using system-level proxy (trust_env) if configured."
|
||||||
|
)
|
||||||
|
|
||||||
# Prepare headers
|
# Prepare headers
|
||||||
headers = self._get_auth_headers(use_auth)
|
headers = self._get_auth_headers(use_auth)
|
||||||
if custom_headers:
|
if custom_headers:
|
||||||
headers.update(custom_headers)
|
headers.update(custom_headers)
|
||||||
|
|
||||||
# Add proxy to kwargs if not already present
|
# Add proxy to kwargs if not already present
|
||||||
if 'proxy' not in kwargs:
|
if "proxy" not in kwargs:
|
||||||
kwargs['proxy'] = self.proxy_url
|
kwargs["proxy"] = self.proxy_url
|
||||||
|
|
||||||
async with session.request(method, url, headers=headers, **kwargs) as response:
|
async with session.request(
|
||||||
|
method, url, headers=headers, **kwargs
|
||||||
|
) as response:
|
||||||
if response.status == 200:
|
if response.status == 200:
|
||||||
# Try to parse as JSON, fall back to text
|
# Try to parse as JSON, fall back to text
|
||||||
try:
|
try:
|
||||||
@@ -804,11 +904,11 @@ class Downloader:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return False, f"Request failed with status {response.status}"
|
return False, f"Request failed with status {response.status}"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error making {method} request to {url}: {e}")
|
logger.error(f"Error making {method} request to {url}: {e}")
|
||||||
return False, str(e)
|
return False, str(e)
|
||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
"""Close the HTTP session"""
|
"""Close the HTTP session"""
|
||||||
if self._session is not None:
|
if self._session is not None:
|
||||||
@@ -817,7 +917,7 @@ class Downloader:
|
|||||||
self._session_created_at = None
|
self._session_created_at = None
|
||||||
self._proxy_url = None
|
self._proxy_url = None
|
||||||
logger.debug("Closed HTTP session")
|
logger.debug("Closed HTTP session")
|
||||||
|
|
||||||
async def refresh_session(self):
|
async def refresh_session(self):
|
||||||
"""Force refresh the HTTP session (useful when proxy settings change)"""
|
"""Force refresh the HTTP session (useful when proxy settings change)"""
|
||||||
async with self._session_lock:
|
async with self._session_lock:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class LoraService(BaseModelService):
|
|||||||
# Resolve sub_type using priority: sub_type > model_type > civitai.model.type > default
|
# Resolve sub_type using priority: sub_type > model_type > civitai.model.type > default
|
||||||
# Normalize to lowercase for consistent API responses
|
# Normalize to lowercase for consistent API responses
|
||||||
sub_type = resolve_sub_type(lora_data).lower()
|
sub_type = resolve_sub_type(lora_data).lower()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"model_name": lora_data["model_name"],
|
"model_name": lora_data["model_name"],
|
||||||
"file_name": lora_data["file_name"],
|
"file_name": lora_data["file_name"],
|
||||||
@@ -48,7 +48,9 @@ class LoraService(BaseModelService):
|
|||||||
"notes": lora_data.get("notes", ""),
|
"notes": lora_data.get("notes", ""),
|
||||||
"favorite": lora_data.get("favorite", False),
|
"favorite": lora_data.get("favorite", False),
|
||||||
"update_available": bool(lora_data.get("update_available", False)),
|
"update_available": bool(lora_data.get("update_available", False)),
|
||||||
"skip_metadata_refresh": bool(lora_data.get("skip_metadata_refresh", False)),
|
"skip_metadata_refresh": bool(
|
||||||
|
lora_data.get("skip_metadata_refresh", False)
|
||||||
|
),
|
||||||
"sub_type": sub_type,
|
"sub_type": sub_type,
|
||||||
"civitai": self.filter_civitai_data(
|
"civitai": self.filter_civitai_data(
|
||||||
lora_data.get("civitai", {}), minimal=True
|
lora_data.get("civitai", {}), minimal=True
|
||||||
@@ -62,6 +64,68 @@ class LoraService(BaseModelService):
|
|||||||
if first_letter:
|
if first_letter:
|
||||||
data = self._filter_by_first_letter(data, first_letter)
|
data = self._filter_by_first_letter(data, first_letter)
|
||||||
|
|
||||||
|
# Handle name pattern filters
|
||||||
|
name_pattern_include = kwargs.get("name_pattern_include", [])
|
||||||
|
name_pattern_exclude = kwargs.get("name_pattern_exclude", [])
|
||||||
|
name_pattern_use_regex = kwargs.get("name_pattern_use_regex", False)
|
||||||
|
|
||||||
|
if name_pattern_include or name_pattern_exclude:
|
||||||
|
import re
|
||||||
|
|
||||||
|
def matches_pattern(name, pattern, use_regex):
|
||||||
|
"""Check if name matches pattern (regex or substring)"""
|
||||||
|
if not name:
|
||||||
|
return False
|
||||||
|
if use_regex:
|
||||||
|
try:
|
||||||
|
return bool(re.search(pattern, name, re.IGNORECASE))
|
||||||
|
except re.error:
|
||||||
|
# Invalid regex, fall back to substring match
|
||||||
|
return pattern.lower() in name.lower()
|
||||||
|
else:
|
||||||
|
return pattern.lower() in name.lower()
|
||||||
|
|
||||||
|
def matches_any_pattern(name, patterns, use_regex):
|
||||||
|
"""Check if name matches any of the patterns"""
|
||||||
|
if not patterns:
|
||||||
|
return True
|
||||||
|
return any(matches_pattern(name, p, use_regex) for p in patterns)
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
for lora in data:
|
||||||
|
model_name = lora.get("model_name", "")
|
||||||
|
file_name = lora.get("file_name", "")
|
||||||
|
names_to_check = [n for n in [model_name, file_name] if n]
|
||||||
|
|
||||||
|
# Check exclude patterns first
|
||||||
|
excluded = False
|
||||||
|
if name_pattern_exclude:
|
||||||
|
for name in names_to_check:
|
||||||
|
if matches_any_pattern(
|
||||||
|
name, name_pattern_exclude, name_pattern_use_regex
|
||||||
|
):
|
||||||
|
excluded = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if excluded:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check include patterns
|
||||||
|
if name_pattern_include:
|
||||||
|
included = False
|
||||||
|
for name in names_to_check:
|
||||||
|
if matches_any_pattern(
|
||||||
|
name, name_pattern_include, name_pattern_use_regex
|
||||||
|
):
|
||||||
|
included = True
|
||||||
|
break
|
||||||
|
if not included:
|
||||||
|
continue
|
||||||
|
|
||||||
|
filtered.append(lora)
|
||||||
|
|
||||||
|
data = filtered
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def _filter_by_first_letter(self, data: List[Dict], letter: str) -> List[Dict]:
|
def _filter_by_first_letter(self, data: List[Dict], letter: str) -> List[Dict]:
|
||||||
@@ -368,9 +432,7 @@ class LoraService(BaseModelService):
|
|||||||
rng.uniform(clip_strength_min, clip_strength_max), 2
|
rng.uniform(clip_strength_min, clip_strength_max), 2
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
clip_str = round(
|
clip_str = round(rng.uniform(clip_strength_min, clip_strength_max), 2)
|
||||||
rng.uniform(clip_strength_min, clip_strength_max), 2
|
|
||||||
)
|
|
||||||
|
|
||||||
result_loras.append(
|
result_loras.append(
|
||||||
{
|
{
|
||||||
@@ -485,12 +547,69 @@ class LoraService(BaseModelService):
|
|||||||
if bool(lora.get("license_flags", 127) & (1 << 1))
|
if bool(lora.get("license_flags", 127) & (1 << 1))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Apply name pattern filters
|
||||||
|
name_patterns = filter_section.get("namePatterns", {})
|
||||||
|
include_patterns = name_patterns.get("include", [])
|
||||||
|
exclude_patterns = name_patterns.get("exclude", [])
|
||||||
|
use_regex = name_patterns.get("useRegex", False)
|
||||||
|
|
||||||
|
if include_patterns or exclude_patterns:
|
||||||
|
import re
|
||||||
|
|
||||||
|
def matches_pattern(name, pattern, use_regex):
|
||||||
|
"""Check if name matches pattern (regex or substring)"""
|
||||||
|
if not name:
|
||||||
|
return False
|
||||||
|
if use_regex:
|
||||||
|
try:
|
||||||
|
return bool(re.search(pattern, name, re.IGNORECASE))
|
||||||
|
except re.error:
|
||||||
|
# Invalid regex, fall back to substring match
|
||||||
|
return pattern.lower() in name.lower()
|
||||||
|
else:
|
||||||
|
return pattern.lower() in name.lower()
|
||||||
|
|
||||||
|
def matches_any_pattern(name, patterns, use_regex):
|
||||||
|
"""Check if name matches any of the patterns"""
|
||||||
|
if not patterns:
|
||||||
|
return True
|
||||||
|
return any(matches_pattern(name, p, use_regex) for p in patterns)
|
||||||
|
|
||||||
|
filtered = []
|
||||||
|
for lora in available_loras:
|
||||||
|
model_name = lora.get("model_name", "")
|
||||||
|
file_name = lora.get("file_name", "")
|
||||||
|
names_to_check = [n for n in [model_name, file_name] if n]
|
||||||
|
|
||||||
|
# Check exclude patterns first
|
||||||
|
excluded = False
|
||||||
|
if exclude_patterns:
|
||||||
|
for name in names_to_check:
|
||||||
|
if matches_any_pattern(name, exclude_patterns, use_regex):
|
||||||
|
excluded = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if excluded:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check include patterns
|
||||||
|
if include_patterns:
|
||||||
|
included = False
|
||||||
|
for name in names_to_check:
|
||||||
|
if matches_any_pattern(name, include_patterns, use_regex):
|
||||||
|
included = True
|
||||||
|
break
|
||||||
|
if not included:
|
||||||
|
continue
|
||||||
|
|
||||||
|
filtered.append(lora)
|
||||||
|
|
||||||
|
available_loras = filtered
|
||||||
|
|
||||||
return available_loras
|
return available_loras
|
||||||
|
|
||||||
async def get_cycler_list(
|
async def get_cycler_list(
|
||||||
self,
|
self, pool_config: Optional[Dict] = None, sort_by: str = "filename"
|
||||||
pool_config: Optional[Dict] = None,
|
|
||||||
sort_by: str = "filename"
|
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Get filtered and sorted LoRA list for cycling.
|
Get filtered and sorted LoRA list for cycling.
|
||||||
@@ -518,16 +637,16 @@ class LoraService(BaseModelService):
|
|||||||
available_loras,
|
available_loras,
|
||||||
key=lambda x: (
|
key=lambda x: (
|
||||||
(x.get("model_name") or x.get("file_name", "")).lower(),
|
(x.get("model_name") or x.get("file_name", "")).lower(),
|
||||||
x.get("file_path", "").lower()
|
x.get("file_path", "").lower(),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
else: # Default to filename
|
else: # Default to filename
|
||||||
available_loras = sorted(
|
available_loras = sorted(
|
||||||
available_loras,
|
available_loras,
|
||||||
key=lambda x: (
|
key=lambda x: (
|
||||||
x.get("file_name", "").lower(),
|
x.get("file_name", "").lower(),
|
||||||
x.get("file_path", "").lower()
|
x.get("file_path", "").lower(),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Return minimal data needed for cycling
|
# Return minimal data needed for cycling
|
||||||
|
|||||||
@@ -122,11 +122,25 @@ async def get_metadata_provider(provider_name: str = None):
|
|||||||
|
|
||||||
provider_manager = await ModelMetadataProviderManager.get_instance()
|
provider_manager = await ModelMetadataProviderManager.get_instance()
|
||||||
|
|
||||||
provider = (
|
try:
|
||||||
provider_manager._get_provider(provider_name)
|
provider = (
|
||||||
if provider_name
|
provider_manager._get_provider(provider_name)
|
||||||
else provider_manager._get_provider()
|
if provider_name
|
||||||
)
|
else provider_manager._get_provider()
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
# Provider not initialized, attempt to initialize
|
||||||
|
if "No default provider set" in str(e) or "not registered" in str(e):
|
||||||
|
logger.warning(f"Metadata provider not initialized ({e}), initializing now...")
|
||||||
|
await initialize_metadata_providers()
|
||||||
|
provider_manager = await ModelMetadataProviderManager.get_instance()
|
||||||
|
provider = (
|
||||||
|
provider_manager._get_provider(provider_name)
|
||||||
|
if provider_name
|
||||||
|
else provider_manager._get_provider()
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
return _wrap_provider_with_rate_limit(provider_name, provider)
|
return _wrap_provider_with_rate_limit(provider_name, provider)
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ from ..utils.metadata_manager import MetadataManager
|
|||||||
from ..utils.civitai_utils import resolve_license_info
|
from ..utils.civitai_utils import resolve_license_info
|
||||||
from .model_cache import ModelCache
|
from .model_cache import ModelCache
|
||||||
from .model_hash_index import ModelHashIndex
|
from .model_hash_index import ModelHashIndex
|
||||||
from ..utils.constants import PREVIEW_EXTENSIONS
|
|
||||||
from .model_lifecycle_service import delete_model_artifacts
|
from .model_lifecycle_service import delete_model_artifacts
|
||||||
from .service_registry import ServiceRegistry
|
from .service_registry import ServiceRegistry
|
||||||
from .websocket_manager import ws_manager
|
from .websocket_manager import ws_manager
|
||||||
@@ -1442,14 +1441,13 @@ class ModelScanner:
|
|||||||
file_path = self._hash_index.get_path(sha256.lower())
|
file_path = self._hash_index.get_path(sha256.lower())
|
||||||
if not file_path:
|
if not file_path:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
base_name = os.path.splitext(file_path)[0]
|
dir_path = os.path.dirname(file_path)
|
||||||
|
base_name = os.path.splitext(os.path.basename(file_path))[0]
|
||||||
for ext in PREVIEW_EXTENSIONS:
|
preview_path = find_preview_file(base_name, dir_path)
|
||||||
preview_path = f"{base_name}{ext}"
|
if preview_path:
|
||||||
if os.path.exists(preview_path):
|
return config.get_preview_static_url(preview_path)
|
||||||
return config.get_preview_static_url(preview_path)
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_top_tags(self, limit: int = 20) -> List[Dict[str, any]]:
|
async def get_top_tags(self, limit: int = 20) -> List[Dict[str, any]]:
|
||||||
|
|||||||
@@ -40,49 +40,39 @@ async def calculate_sha256(file_path: str) -> str:
|
|||||||
return sha256_hash.hexdigest()
|
return sha256_hash.hexdigest()
|
||||||
|
|
||||||
def find_preview_file(base_name: str, dir_path: str) -> str:
|
def find_preview_file(base_name: str, dir_path: str) -> str:
|
||||||
"""Find preview file for given base name in directory"""
|
"""Find preview file for given base name in directory.
|
||||||
|
|
||||||
|
Performs an exact-case check first (fast path), then falls back to a
|
||||||
|
case-insensitive scan so that files like ``model.WEBP`` or ``model.Png``
|
||||||
|
are discovered on case-sensitive filesystems.
|
||||||
|
"""
|
||||||
|
|
||||||
temp_extensions = PREVIEW_EXTENSIONS.copy()
|
temp_extensions = PREVIEW_EXTENSIONS.copy()
|
||||||
# Add example extension for compatibility
|
# Add example extension for compatibility
|
||||||
# https://github.com/willmiao/ComfyUI-Lora-Manager/issues/225
|
# https://github.com/willmiao/ComfyUI-Lora-Manager/issues/225
|
||||||
# The preview image will be optimized to lora-name.webp, so it won't affect other logic
|
# The preview image will be optimized to lora-name.webp, so it won't affect other logic
|
||||||
temp_extensions.append(".example.0.jpeg")
|
temp_extensions.append(".example.0.jpeg")
|
||||||
|
|
||||||
|
# Fast path: exact-case match
|
||||||
for ext in temp_extensions:
|
for ext in temp_extensions:
|
||||||
full_pattern = os.path.join(dir_path, f"{base_name}{ext}")
|
full_pattern = os.path.join(dir_path, f"{base_name}{ext}")
|
||||||
if os.path.exists(full_pattern):
|
if os.path.exists(full_pattern):
|
||||||
# Check if this is an image and not already webp
|
|
||||||
# TODO: disable the optimization for now, maybe add a config option later
|
|
||||||
# if ext.lower().endswith(('.jpg', '.jpeg', '.png')) and not ext.lower().endswith('.webp'):
|
|
||||||
# try:
|
|
||||||
# # Optimize the image to webp format
|
|
||||||
# webp_path = os.path.join(dir_path, f"{base_name}.webp")
|
|
||||||
|
|
||||||
# # Use ExifUtils to optimize the image
|
|
||||||
# with open(full_pattern, 'rb') as f:
|
|
||||||
# image_data = f.read()
|
|
||||||
|
|
||||||
# optimized_data, _ = ExifUtils.optimize_image(
|
|
||||||
# image_data=image_data,
|
|
||||||
# target_width=CARD_PREVIEW_WIDTH,
|
|
||||||
# format='webp',
|
|
||||||
# quality=85,
|
|
||||||
# preserve_metadata=False
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Save the optimized webp file
|
|
||||||
# with open(webp_path, 'wb') as f:
|
|
||||||
# f.write(optimized_data)
|
|
||||||
|
|
||||||
# logger.debug(f"Optimized preview image from {full_pattern} to {webp_path}")
|
|
||||||
# return webp_path.replace(os.sep, "/")
|
|
||||||
# except Exception as e:
|
|
||||||
# logger.error(f"Error optimizing preview image {full_pattern}: {e}")
|
|
||||||
# # Fall back to original file if optimization fails
|
|
||||||
# return full_pattern.replace(os.sep, "/")
|
|
||||||
|
|
||||||
# Return the original path for webp images or non-image files
|
|
||||||
return full_pattern.replace(os.sep, "/")
|
return full_pattern.replace(os.sep, "/")
|
||||||
|
|
||||||
|
# Slow path: case-insensitive match for systems with mixed-case extensions
|
||||||
|
# (e.g. .WEBP, .Png, .JPG placed manually or by external tools)
|
||||||
|
try:
|
||||||
|
dir_entries = os.listdir(dir_path)
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
base_lower = base_name.lower()
|
||||||
|
for ext in temp_extensions:
|
||||||
|
target = f"{base_lower}{ext}" # ext is already lowercase
|
||||||
|
for entry in dir_entries:
|
||||||
|
if entry.lower() == target:
|
||||||
|
return os.path.join(dir_path, entry).replace(os.sep, "/")
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
def get_preview_extension(preview_path: str) -> str:
|
def get_preview_extension(preview_path: str) -> str:
|
||||||
|
|||||||
@@ -687,7 +687,7 @@
|
|||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: oklch(var(--lora-warning) / 0.1);
|
background: oklch(var(--lora-warning) / 0.1);
|
||||||
border: 1px solid var(--lora-warning);
|
border: 1px solid var(--lora-warning);
|
||||||
border-radius: var(--border-radius-sm) var(--border-radius-sm) 0 0;
|
border-radius: var(--border-radius-sm);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ export class BaseModelApiClient {
|
|||||||
replaceModelPreview(filePath) {
|
replaceModelPreview(filePath) {
|
||||||
const input = document.createElement('input');
|
const input = document.createElement('input');
|
||||||
input.type = 'file';
|
input.type = 'file';
|
||||||
input.accept = 'image/*,video/mp4';
|
input.accept = 'image/*,image/webp,video/mp4';
|
||||||
|
|
||||||
input.onchange = async () => {
|
input.onchange = async () => {
|
||||||
if (!input.files || !input.files[0]) return;
|
if (!input.files || !input.files[0]) return;
|
||||||
|
|||||||
@@ -142,6 +142,28 @@ export class ImportManager {
|
|||||||
|
|
||||||
// Reset duplicate related properties
|
// Reset duplicate related properties
|
||||||
this.duplicateRecipes = [];
|
this.duplicateRecipes = [];
|
||||||
|
|
||||||
|
// Reset button visibility in location step
|
||||||
|
this.resetLocationStepButtons();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLocationStepButtons() {
|
||||||
|
// Reset buttons to default state
|
||||||
|
const locationStep = document.getElementById('locationStep');
|
||||||
|
if (!locationStep) return;
|
||||||
|
|
||||||
|
const backBtn = locationStep.querySelector('.secondary-btn');
|
||||||
|
const primaryBtn = locationStep.querySelector('.primary-btn');
|
||||||
|
|
||||||
|
// Back button - show
|
||||||
|
if (backBtn) {
|
||||||
|
backBtn.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Primary button - reset text
|
||||||
|
if (primaryBtn) {
|
||||||
|
primaryBtn.textContent = translate('recipes.controls.import.downloadAndSaveRecipe', {}, 'Download & Save Recipe');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleImportMode(mode) {
|
toggleImportMode(mode) {
|
||||||
@@ -261,11 +283,57 @@ export class ImportManager {
|
|||||||
this.loadDefaultPathSetting();
|
this.loadDefaultPathSetting();
|
||||||
|
|
||||||
this.updateTargetPath();
|
this.updateTargetPath();
|
||||||
|
|
||||||
|
// Update download button with missing LoRA count (if any)
|
||||||
|
if (this.missingLoras && this.missingLoras.length > 0) {
|
||||||
|
this.updateDownloadButtonCount();
|
||||||
|
this.updateImportButtonsVisibility(true);
|
||||||
|
} else {
|
||||||
|
this.updateImportButtonsVisibility(false);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast('toast.recipes.importFailed', { message: error.message }, 'error');
|
showToast('toast.recipes.importFailed', { message: error.message }, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateImportButtonsVisibility(hasMissingLoras) {
|
||||||
|
// Update primary button text based on whether there are missing LoRAs
|
||||||
|
const locationStep = document.getElementById('locationStep');
|
||||||
|
if (!locationStep) return;
|
||||||
|
|
||||||
|
const backBtn = locationStep.querySelector('.secondary-btn');
|
||||||
|
const primaryBtn = locationStep.querySelector('.primary-btn');
|
||||||
|
|
||||||
|
// Back button - always show
|
||||||
|
if (backBtn) {
|
||||||
|
backBtn.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update primary button text
|
||||||
|
if (primaryBtn) {
|
||||||
|
const downloadCountSpan = locationStep.querySelector('#downloadLoraCount');
|
||||||
|
if (hasMissingLoras) {
|
||||||
|
// Rebuild button content to ensure proper structure
|
||||||
|
const buttonText = translate('recipes.controls.import.importAndDownload', {}, 'Import & Download');
|
||||||
|
primaryBtn.innerHTML = `${buttonText} <span id="downloadLoraCount"></span>`;
|
||||||
|
} else {
|
||||||
|
primaryBtn.textContent = translate('recipes.controls.import.downloadAndSaveRecipe', {}, 'Download & Save Recipe');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDownloadButtonCount() {
|
||||||
|
// Update the download count badge on the primary button
|
||||||
|
const locationStep = document.getElementById('locationStep');
|
||||||
|
if (!locationStep) return;
|
||||||
|
|
||||||
|
const downloadCountSpan = locationStep.querySelector('#downloadLoraCount');
|
||||||
|
if (downloadCountSpan) {
|
||||||
|
const missingCount = this.missingLoras?.length || 0;
|
||||||
|
downloadCountSpan.textContent = missingCount > 0 ? `(${missingCount})` : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
backToUpload() {
|
backToUpload() {
|
||||||
this.stepManager.showStep('uploadStep');
|
this.stepManager.showStep('uploadStep');
|
||||||
|
|
||||||
@@ -426,12 +494,54 @@ export class ImportManager {
|
|||||||
const modalTitle = document.querySelector('#importModal h2');
|
const modalTitle = document.querySelector('#importModal h2');
|
||||||
if (modalTitle) modalTitle.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
|
if (modalTitle) modalTitle.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
|
||||||
|
|
||||||
// Update the save button text
|
// Update button texts and show download count
|
||||||
const saveButton = document.querySelector('#locationStep .primary-btn');
|
const locationStep = document.getElementById('locationStep');
|
||||||
if (saveButton) saveButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
|
if (!locationStep) return;
|
||||||
|
|
||||||
|
const primaryBtn = locationStep.querySelector('.primary-btn');
|
||||||
|
const backBtn = locationStep.querySelector('.secondary-btn');
|
||||||
|
|
||||||
|
// primaryBtn should be the "Import & Download" button
|
||||||
|
if (primaryBtn) {
|
||||||
|
const buttonText = translate('recipes.controls.import.importAndDownload', {}, 'Import & Download');
|
||||||
|
primaryBtn.innerHTML = `${buttonText} <span id="downloadLoraCount">(${recipeData.loras?.length || 0})</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide the "Back" button in download-only mode
|
||||||
|
if (backBtn) {
|
||||||
|
backBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Hide the back button
|
saveRecipeWithoutDownload() {
|
||||||
const backButton = document.querySelector('#locationStep .secondary-btn');
|
// Call save recipe with skip download flag
|
||||||
if (backButton) backButton.style.display = 'none';
|
return this.downloadManager.saveRecipe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveRecipeOnlyFromDetails() {
|
||||||
|
// Validate recipe name first
|
||||||
|
if (!this.recipeName) {
|
||||||
|
showToast('toast.recipes.enterRecipeName', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark deleted LoRAs as excluded
|
||||||
|
if (this.recipeData && this.recipeData.loras) {
|
||||||
|
this.recipeData.loras.forEach(lora => {
|
||||||
|
if (lora.isDeleted) {
|
||||||
|
lora.exclude = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update missing LoRAs list
|
||||||
|
this.missingLoras = this.recipeData.loras.filter(lora =>
|
||||||
|
!lora.existsLocally && !lora.isDeleted);
|
||||||
|
|
||||||
|
// For import only, we don't need downloadableLoRAs
|
||||||
|
this.downloadableLoRAs = [];
|
||||||
|
|
||||||
|
// Save recipe without downloading
|
||||||
|
await this.downloadManager.saveRecipe(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class DownloadManager {
|
|||||||
this.importManager = importManager;
|
this.importManager = importManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveRecipe() {
|
async saveRecipe(skipDownload = false) {
|
||||||
// Check if we're in download-only mode (for existing recipe)
|
// Check if we're in download-only mode (for existing recipe)
|
||||||
const isDownloadOnly = !!this.importManager.recipeId;
|
const isDownloadOnly = !!this.importManager.recipeId;
|
||||||
|
|
||||||
@@ -20,7 +20,10 @@ export class DownloadManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Show progress indicator
|
// Show progress indicator
|
||||||
this.importManager.loadingManager.showSimpleLoading(isDownloadOnly ? translate('recipes.controls.import.downloadingLoras', {}, 'Downloading LoRAs...') : translate('recipes.controls.import.savingRecipe', {}, 'Saving recipe...'));
|
const loadingMessage = skipDownload
|
||||||
|
? translate('recipes.controls.import.savingRecipe', {}, 'Saving recipe...')
|
||||||
|
: (isDownloadOnly ? translate('recipes.controls.import.downloadingLoras', {}, 'Downloading LoRAs...') : translate('recipes.controls.import.savingRecipe', {}, 'Saving recipe...'));
|
||||||
|
this.importManager.loadingManager.showSimpleLoading(loadingMessage);
|
||||||
|
|
||||||
// Only send the complete recipe to save if not in download-only mode
|
// Only send the complete recipe to save if not in download-only mode
|
||||||
if (!isDownloadOnly) {
|
if (!isDownloadOnly) {
|
||||||
@@ -98,15 +101,17 @@ export class DownloadManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we need to download LoRAs
|
// Check if we need to download LoRAs (skip if skipDownload is true)
|
||||||
let failedDownloads = 0;
|
let failedDownloads = 0;
|
||||||
if (this.importManager.downloadableLoRAs && this.importManager.downloadableLoRAs.length > 0) {
|
if (!skipDownload && this.importManager.downloadableLoRAs && this.importManager.downloadableLoRAs.length > 0) {
|
||||||
await this.downloadMissingLoras();
|
await this.downloadMissingLoras();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
if (isDownloadOnly) {
|
if (isDownloadOnly) {
|
||||||
if (failedDownloads === 0) {
|
if (skipDownload) {
|
||||||
|
showToast('toast.recipes.recipeSaved', {}, 'success');
|
||||||
|
} else if (failedDownloads === 0) {
|
||||||
showToast('toast.loras.downloadSuccessful', {}, 'success');
|
showToast('toast.loras.downloadSuccessful', {}, 'success');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -325,7 +325,8 @@ export class RecipeDataManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateNextButtonState() {
|
updateNextButtonState() {
|
||||||
const nextButton = document.querySelector('#detailsStep .primary-btn');
|
const nextButton = document.getElementById('nextBtn');
|
||||||
|
const importOnlyBtn = document.getElementById('importOnlyBtn');
|
||||||
const actionsContainer = document.querySelector('#detailsStep .modal-actions');
|
const actionsContainer = document.querySelector('#detailsStep .modal-actions');
|
||||||
if (!nextButton || !actionsContainer) return;
|
if (!nextButton || !actionsContainer) return;
|
||||||
|
|
||||||
@@ -365,7 +366,7 @@ export class RecipeDataManager {
|
|||||||
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
buttonsContainer.parentNode.insertBefore(warningContainer, buttonsContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicates but don't change button actions
|
// Check for downloadable missing LoRAs
|
||||||
const missingNotDeleted = this.importManager.recipeData.loras.filter(
|
const missingNotDeleted = this.importManager.recipeData.loras.filter(
|
||||||
lora => !lora.existsLocally && !lora.isDeleted
|
lora => !lora.existsLocally && !lora.isDeleted
|
||||||
).length;
|
).length;
|
||||||
@@ -374,8 +375,16 @@ export class RecipeDataManager {
|
|||||||
nextButton.classList.remove('warning-btn');
|
nextButton.classList.remove('warning-btn');
|
||||||
|
|
||||||
if (missingNotDeleted > 0) {
|
if (missingNotDeleted > 0) {
|
||||||
nextButton.textContent = translate('recipes.controls.import.downloadMissingLoras', {}, 'Download Missing LoRAs');
|
// Show import only button and update primary button
|
||||||
|
if (importOnlyBtn) {
|
||||||
|
importOnlyBtn.style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
nextButton.textContent = translate('recipes.controls.import.importAndDownload', {}, 'Import & Download') + ` (${missingNotDeleted})`;
|
||||||
} else {
|
} else {
|
||||||
|
// Hide import only button and show save recipe
|
||||||
|
if (importOnlyBtn) {
|
||||||
|
importOnlyBtn.style.display = 'none';
|
||||||
|
}
|
||||||
nextButton.textContent = translate('recipes.controls.import.saveRecipe', {}, 'Save Recipe');
|
nextButton.textContent = translate('recipes.controls.import.saveRecipe', {}, 'Save Recipe');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -440,8 +449,11 @@ export class RecipeDataManager {
|
|||||||
// Store only downloadable LoRAs for the download step
|
// Store only downloadable LoRAs for the download step
|
||||||
this.importManager.downloadableLoRAs = this.importManager.missingLoras;
|
this.importManager.downloadableLoRAs = this.importManager.missingLoras;
|
||||||
this.importManager.proceedToLocation();
|
this.importManager.proceedToLocation();
|
||||||
|
} else if (this.importManager.missingLoras.length === 0 && this.importManager.recipeData.loras.some(l => !l.existsLocally)) {
|
||||||
|
// All missing LoRAs are deleted, save recipe without download
|
||||||
|
this.importManager.saveRecipe();
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, save the recipe directly
|
// No missing LoRAs at all, save the recipe directly
|
||||||
this.importManager.saveRecipe();
|
this.importManager.saveRecipe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,9 +92,10 @@
|
|||||||
<!-- Duplicate recipes will be populated here -->
|
<!-- Duplicate recipes will be populated here -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions" id="detailsStepActions">
|
||||||
<button class="secondary-btn" onclick="importManager.backToUpload()">{{ t('common.actions.back') }}</button>
|
<button class="secondary-btn" onclick="importManager.backToUpload()">{{ t('common.actions.back') }}</button>
|
||||||
<button class="primary-btn" onclick="importManager.proceedFromDetails()">{{ t('common.actions.next') }}</button>
|
<button class="secondary-btn" id="importOnlyBtn" onclick="importManager.saveRecipeOnlyFromDetails()" style="display: none;">{{ t('recipes.controls.import.importRecipeOnly') }}</button>
|
||||||
|
<button class="primary-btn" id="nextBtn" onclick="importManager.proceedFromDetails()">{{ t('common.actions.next') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@
|
|||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="secondary-btn" onclick="importManager.backToDetails()">{{ t('common.actions.back') }}</button>
|
<button class="secondary-btn" onclick="importManager.backToDetails()">{{ t('common.actions.back') }}</button>
|
||||||
<button class="primary-btn" onclick="importManager.saveRecipe()">{{ t('recipes.controls.import.downloadAndSaveRecipe') }}</button>
|
<button class="primary-btn" onclick="importManager.saveRecipe()">{{ t('recipes.controls.import.importAndDownload') }} <span id="downloadLoraCount"></span></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -484,9 +484,11 @@ async def test_get_model_version_info_success(monkeypatch, downloader):
|
|||||||
assert result["images"][0]["meta"]["other"] == "keep"
|
assert result["images"][0]["meta"]["other"] == "keep"
|
||||||
|
|
||||||
|
|
||||||
async def test_get_image_info_returns_first_item(monkeypatch, downloader):
|
async def test_get_image_info_returns_matching_item(monkeypatch, downloader):
|
||||||
|
"""When API returns multiple items, return the one matching the requested ID."""
|
||||||
async def fake_make_request(method, url, use_auth=True, **kwargs):
|
async def fake_make_request(method, url, use_auth=True, **kwargs):
|
||||||
return True, {"items": [{"id": 1}, {"id": 2}]}
|
# Requested ID is 42, but it's the second item in the response
|
||||||
|
return True, {"items": [{"id": 41}, {"id": 42, "name": "target"}, {"id": 43}]}
|
||||||
|
|
||||||
downloader.make_request = fake_make_request
|
downloader.make_request = fake_make_request
|
||||||
|
|
||||||
@@ -494,7 +496,25 @@ async def test_get_image_info_returns_first_item(monkeypatch, downloader):
|
|||||||
|
|
||||||
result = await client.get_image_info("42")
|
result = await client.get_image_info("42")
|
||||||
|
|
||||||
assert result == {"id": 1}
|
assert result == {"id": 42, "name": "target"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_image_info_returns_none_when_id_mismatch(monkeypatch, downloader, caplog):
|
||||||
|
"""When API returns items but none match the requested ID, return None and log warning."""
|
||||||
|
async def fake_make_request(method, url, use_auth=True, **kwargs):
|
||||||
|
# Requested ID is 999, but API returns different IDs (simulating deleted/hidden image)
|
||||||
|
return True, {"items": [{"id": 1}, {"id": 2}, {"id": 3}]}
|
||||||
|
|
||||||
|
downloader.make_request = fake_make_request
|
||||||
|
|
||||||
|
client = await CivitaiClient.get_instance()
|
||||||
|
|
||||||
|
result = await client.get_image_info("999")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
# Verify warning was logged
|
||||||
|
assert "CivitAI API returned no matching image for requested ID 999" in caplog.text
|
||||||
|
assert "Returned 3 item(s) with IDs: [1, 2, 3]" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
async def test_get_image_info_handles_missing(monkeypatch, downloader):
|
async def test_get_image_info_handles_missing(monkeypatch, downloader):
|
||||||
@@ -508,3 +528,13 @@ async def test_get_image_info_handles_missing(monkeypatch, downloader):
|
|||||||
result = await client.get_image_info("42")
|
result = await client.get_image_info("42")
|
||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_get_image_info_handles_invalid_id(monkeypatch, downloader, caplog):
|
||||||
|
"""When given a non-numeric image ID, return None and log error."""
|
||||||
|
client = await CivitaiClient.get_instance()
|
||||||
|
|
||||||
|
result = await client.get_image_info("not-a-number")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
assert "Invalid image ID format" in caplog.text
|
||||||
|
|||||||
@@ -369,3 +369,289 @@ async def test_pool_filter_combined_all_filters(lora_service):
|
|||||||
# - tags: tag1 ✓
|
# - tags: tag1 ✓
|
||||||
assert len(filtered) == 1
|
assert len(filtered) == 1
|
||||||
assert filtered[0]["file_name"] == "match_all.safetensors"
|
assert filtered[0]["file_name"] == "match_all.safetensors"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pool_filter_name_patterns_include_text(lora_service):
|
||||||
|
"""Test filtering by name patterns with text matching (useRegex=False)."""
|
||||||
|
sample_loras = [
|
||||||
|
{
|
||||||
|
"file_name": "character_anime_v1.safetensors",
|
||||||
|
"model_name": "Anime Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "character_realistic_v1.safetensors",
|
||||||
|
"model_name": "Realistic Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "style_watercolor_v1.safetensors",
|
||||||
|
"model_name": "Watercolor Style",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test include patterns with text matching
|
||||||
|
pool_config = {
|
||||||
|
"baseModels": [],
|
||||||
|
"tags": {"include": [], "exclude": []},
|
||||||
|
"folders": {"include": [], "exclude": []},
|
||||||
|
"license": {"noCreditRequired": False, "allowSelling": False},
|
||||||
|
"namePatterns": {"include": ["character"], "exclude": [], "useRegex": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||||
|
assert len(filtered) == 2
|
||||||
|
file_names = {lora["file_name"] for lora in filtered}
|
||||||
|
assert file_names == {
|
||||||
|
"character_anime_v1.safetensors",
|
||||||
|
"character_realistic_v1.safetensors",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pool_filter_name_patterns_exclude_text(lora_service):
|
||||||
|
"""Test excluding by name patterns with text matching (useRegex=False)."""
|
||||||
|
sample_loras = [
|
||||||
|
{
|
||||||
|
"file_name": "character_anime_v1.safetensors",
|
||||||
|
"model_name": "Anime Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "character_realistic_v1.safetensors",
|
||||||
|
"model_name": "Realistic Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "style_watercolor_v1.safetensors",
|
||||||
|
"model_name": "Watercolor Style",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test exclude patterns with text matching
|
||||||
|
pool_config = {
|
||||||
|
"baseModels": [],
|
||||||
|
"tags": {"include": [], "exclude": []},
|
||||||
|
"folders": {"include": [], "exclude": []},
|
||||||
|
"license": {"noCreditRequired": False, "allowSelling": False},
|
||||||
|
"namePatterns": {"include": [], "exclude": ["anime"], "useRegex": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||||
|
assert len(filtered) == 2
|
||||||
|
file_names = {lora["file_name"] for lora in filtered}
|
||||||
|
assert file_names == {
|
||||||
|
"character_realistic_v1.safetensors",
|
||||||
|
"style_watercolor_v1.safetensors",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pool_filter_name_patterns_include_regex(lora_service):
|
||||||
|
"""Test filtering by name patterns with regex matching (useRegex=True)."""
|
||||||
|
sample_loras = [
|
||||||
|
{
|
||||||
|
"file_name": "character_anime_v1.safetensors",
|
||||||
|
"model_name": "Anime Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "character_realistic_v1.safetensors",
|
||||||
|
"model_name": "Realistic Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "style_watercolor_v1.safetensors",
|
||||||
|
"model_name": "Watercolor Style",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test include patterns with regex matching - match files starting with "character_"
|
||||||
|
pool_config = {
|
||||||
|
"baseModels": [],
|
||||||
|
"tags": {"include": [], "exclude": []},
|
||||||
|
"folders": {"include": [], "exclude": []},
|
||||||
|
"license": {"noCreditRequired": False, "allowSelling": False},
|
||||||
|
"namePatterns": {"include": ["^character_"], "exclude": [], "useRegex": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||||
|
assert len(filtered) == 2
|
||||||
|
file_names = {lora["file_name"] for lora in filtered}
|
||||||
|
assert file_names == {
|
||||||
|
"character_anime_v1.safetensors",
|
||||||
|
"character_realistic_v1.safetensors",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pool_filter_name_patterns_exclude_regex(lora_service):
|
||||||
|
"""Test excluding by name patterns with regex matching (useRegex=True)."""
|
||||||
|
sample_loras = [
|
||||||
|
{
|
||||||
|
"file_name": "character_anime_v1.safetensors",
|
||||||
|
"model_name": "Anime Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "character_realistic_v1.safetensors",
|
||||||
|
"model_name": "Realistic Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "style_watercolor_v1.safetensors",
|
||||||
|
"model_name": "Watercolor Style",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test exclude patterns with regex matching - exclude files ending with "_v1.safetensors"
|
||||||
|
pool_config = {
|
||||||
|
"baseModels": [],
|
||||||
|
"tags": {"include": [], "exclude": []},
|
||||||
|
"folders": {"include": [], "exclude": []},
|
||||||
|
"license": {"noCreditRequired": False, "allowSelling": False},
|
||||||
|
"namePatterns": {
|
||||||
|
"include": [],
|
||||||
|
"exclude": ["_v1\\.safetensors$"],
|
||||||
|
"useRegex": True,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||||
|
assert len(filtered) == 0 # All files match the exclude pattern
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pool_filter_name_patterns_combined(lora_service):
|
||||||
|
"""Test combining include and exclude name patterns."""
|
||||||
|
sample_loras = [
|
||||||
|
{
|
||||||
|
"file_name": "character_anime_v1.safetensors",
|
||||||
|
"model_name": "Anime Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "character_realistic_v1.safetensors",
|
||||||
|
"model_name": "Realistic Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "style_watercolor_v1.safetensors",
|
||||||
|
"model_name": "Watercolor Style",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Test include "character" but exclude "anime"
|
||||||
|
pool_config = {
|
||||||
|
"baseModels": [],
|
||||||
|
"tags": {"include": [], "exclude": []},
|
||||||
|
"folders": {"include": [], "exclude": []},
|
||||||
|
"license": {"noCreditRequired": False, "allowSelling": False},
|
||||||
|
"namePatterns": {
|
||||||
|
"include": ["character"],
|
||||||
|
"exclude": ["anime"],
|
||||||
|
"useRegex": False,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0]["file_name"] == "character_realistic_v1.safetensors"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pool_filter_name_patterns_model_name_fallback(lora_service):
|
||||||
|
"""Test that name pattern filtering falls back to model_name when file_name doesn't match."""
|
||||||
|
sample_loras = [
|
||||||
|
{
|
||||||
|
"file_name": "abc123.safetensors",
|
||||||
|
"model_name": "Super Anime Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"file_name": "def456.safetensors",
|
||||||
|
"model_name": "Realistic Portrait",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Should match model_name even if file_name doesn't contain the pattern
|
||||||
|
pool_config = {
|
||||||
|
"baseModels": [],
|
||||||
|
"tags": {"include": [], "exclude": []},
|
||||||
|
"folders": {"include": [], "exclude": []},
|
||||||
|
"license": {"noCreditRequired": False, "allowSelling": False},
|
||||||
|
"namePatterns": {"include": ["anime"], "exclude": [], "useRegex": False},
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0]["file_name"] == "abc123.safetensors"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pool_filter_name_patterns_invalid_regex(lora_service):
|
||||||
|
"""Test that invalid regex falls back to substring matching."""
|
||||||
|
sample_loras = [
|
||||||
|
{
|
||||||
|
"file_name": "character_anime[test]_v1.safetensors",
|
||||||
|
"model_name": "Anime Character",
|
||||||
|
"base_model": "Illustrious",
|
||||||
|
"folder": "",
|
||||||
|
"license_flags": build_license_flags(None),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Invalid regex pattern (unclosed character class) should fall back to substring matching
|
||||||
|
# The pattern "anime[" is invalid regex but valid substring - it exists in the filename
|
||||||
|
pool_config = {
|
||||||
|
"baseModels": [],
|
||||||
|
"tags": {"include": [], "exclude": []},
|
||||||
|
"folders": {"include": [], "exclude": []},
|
||||||
|
"license": {"noCreditRequired": False, "allowSelling": False},
|
||||||
|
"namePatterns": {"include": ["anime["], "exclude": [], "useRegex": True},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Should not crash and should match using substring fallback
|
||||||
|
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||||
|
assert len(filtered) == 1 # Substring match works even with invalid regex
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ async def test_analyze_remote_video(tmp_path):
|
|||||||
|
|
||||||
class DummyFactory:
|
class DummyFactory:
|
||||||
def create_parser(self, metadata):
|
def create_parser(self, metadata):
|
||||||
async def parse_metadata(m, recipe_scanner):
|
async def parse_metadata(m, recipe_scanner=None, civitai_client=None):
|
||||||
return {"loras": []}
|
return {"loras": []}
|
||||||
return SimpleNamespace(parse_metadata=parse_metadata)
|
return SimpleNamespace(parse_metadata=parse_metadata)
|
||||||
|
|
||||||
|
|||||||
202
tests/services/test_sui_image_params_parser.py
Normal file
202
tests/services/test_sui_image_params_parser.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
"""Tests for SuiImageParamsParser."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import json
|
||||||
|
from py.recipes.parsers import SuiImageParamsParser
|
||||||
|
|
||||||
|
|
||||||
|
class TestSuiImageParamsParser:
|
||||||
|
"""Test cases for SuiImageParamsParser."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Set up test fixtures."""
|
||||||
|
self.parser = SuiImageParamsParser()
|
||||||
|
|
||||||
|
def test_is_metadata_matching_positive(self):
|
||||||
|
"""Test that parser correctly identifies SuiImage metadata format."""
|
||||||
|
metadata = {
|
||||||
|
"sui_image_params": {
|
||||||
|
"prompt": "test prompt",
|
||||||
|
"model": "test_model"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
assert self.parser.is_metadata_matching(metadata_str) is True
|
||||||
|
|
||||||
|
def test_is_metadata_matching_negative(self):
|
||||||
|
"""Test that parser rejects non-SuiImage metadata."""
|
||||||
|
# Missing sui_image_params key
|
||||||
|
metadata = {
|
||||||
|
"other_params": {
|
||||||
|
"prompt": "test prompt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
assert self.parser.is_metadata_matching(metadata_str) is False
|
||||||
|
|
||||||
|
def test_is_metadata_matching_invalid_json(self):
|
||||||
|
"""Test that parser handles invalid JSON gracefully."""
|
||||||
|
metadata_str = "not valid json"
|
||||||
|
assert self.parser.is_metadata_matching(metadata_str) is False
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_metadata_extracts_basic_fields(self):
|
||||||
|
"""Test parsing basic fields from SuiImage metadata."""
|
||||||
|
metadata = {
|
||||||
|
"sui_image_params": {
|
||||||
|
"prompt": "beautiful landscape",
|
||||||
|
"negativeprompt": "ugly, blurry",
|
||||||
|
"steps": 30,
|
||||||
|
"seed": 12345,
|
||||||
|
"cfgscale": 7.5,
|
||||||
|
"width": 512,
|
||||||
|
"height": 768,
|
||||||
|
"sampler": "Euler a",
|
||||||
|
"scheduler": "normal"
|
||||||
|
},
|
||||||
|
"sui_models": [],
|
||||||
|
"sui_extra_data": {}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
result = await self.parser.parse_metadata(metadata_str)
|
||||||
|
|
||||||
|
assert result.get('gen_params', {}).get('prompt') == "beautiful landscape"
|
||||||
|
assert result.get('gen_params', {}).get('negative_prompt') == "ugly, blurry"
|
||||||
|
assert result.get('gen_params', {}).get('steps') == 30
|
||||||
|
assert result.get('gen_params', {}).get('seed') == 12345
|
||||||
|
assert result.get('gen_params', {}).get('cfg_scale') == 7.5
|
||||||
|
assert result.get('gen_params', {}).get('width') == 512
|
||||||
|
assert result.get('gen_params', {}).get('height') == 768
|
||||||
|
assert result.get('gen_params', {}).get('size') == "512x768"
|
||||||
|
assert result.get('loras') == []
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_metadata_extracts_checkpoint(self):
|
||||||
|
"""Test parsing checkpoint from sui_models."""
|
||||||
|
metadata = {
|
||||||
|
"sui_image_params": {
|
||||||
|
"prompt": "test prompt",
|
||||||
|
"model": "checkpoint_model"
|
||||||
|
},
|
||||||
|
"sui_models": [
|
||||||
|
{
|
||||||
|
"name": "test_checkpoint.safetensors",
|
||||||
|
"param": "model",
|
||||||
|
"hash": "0x1234567890abcdef"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sui_extra_data": {}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
result = await self.parser.parse_metadata(metadata_str)
|
||||||
|
|
||||||
|
checkpoint = result.get('checkpoint')
|
||||||
|
assert checkpoint is not None
|
||||||
|
assert checkpoint['type'] == 'checkpoint'
|
||||||
|
assert checkpoint['name'] == 'test_checkpoint'
|
||||||
|
assert checkpoint['hash'] == '1234567890abcdef'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_metadata_extracts_lora(self):
|
||||||
|
"""Test parsing LoRA from sui_models."""
|
||||||
|
metadata = {
|
||||||
|
"sui_image_params": {
|
||||||
|
"prompt": "test prompt"
|
||||||
|
},
|
||||||
|
"sui_models": [
|
||||||
|
{
|
||||||
|
"name": "test_lora.safetensors",
|
||||||
|
"param": "lora",
|
||||||
|
"hash": "0xabcdef1234567890"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sui_extra_data": {}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
result = await self.parser.parse_metadata(metadata_str)
|
||||||
|
|
||||||
|
loras = result.get('loras')
|
||||||
|
assert len(loras) == 1
|
||||||
|
assert loras[0]['type'] == 'lora'
|
||||||
|
assert loras[0]['name'] == 'test_lora'
|
||||||
|
assert loras[0]['file_name'] == 'test_lora.safetensors'
|
||||||
|
assert loras[0]['hash'] == 'abcdef1234567890'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_metadata_handles_lora_in_name(self):
|
||||||
|
"""Test that LoRA is detected by 'lora' in name."""
|
||||||
|
metadata = {
|
||||||
|
"sui_image_params": {
|
||||||
|
"prompt": "test prompt"
|
||||||
|
},
|
||||||
|
"sui_models": [
|
||||||
|
{
|
||||||
|
"name": "style_lora_v2.safetensors",
|
||||||
|
"param": "some_other_param",
|
||||||
|
"hash": "0x1111111111111111"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sui_extra_data": {}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
result = await self.parser.parse_metadata(metadata_str)
|
||||||
|
|
||||||
|
loras = result.get('loras')
|
||||||
|
assert len(loras) == 1
|
||||||
|
assert loras[0]['type'] == 'lora'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_metadata_empty_models(self):
|
||||||
|
"""Test parsing with empty sui_models array."""
|
||||||
|
metadata = {
|
||||||
|
"sui_image_params": {
|
||||||
|
"prompt": "test prompt",
|
||||||
|
"steps": 20
|
||||||
|
},
|
||||||
|
"sui_models": [],
|
||||||
|
"sui_extra_data": {
|
||||||
|
"date": "2024-01-01"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
result = await self.parser.parse_metadata(metadata_str)
|
||||||
|
|
||||||
|
assert result.get('loras') == []
|
||||||
|
assert result.get('checkpoint') is None
|
||||||
|
assert result.get('gen_params', {}).get('prompt') == "test prompt"
|
||||||
|
assert result.get('gen_params', {}).get('steps') == 20
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_metadata_alternative_field_names(self):
|
||||||
|
"""Test parsing with alternative field names."""
|
||||||
|
metadata = {
|
||||||
|
"sui_image_params": {
|
||||||
|
"prompt": "test prompt",
|
||||||
|
"negative_prompt": "bad quality", # Using underscore variant
|
||||||
|
"cfg_scale": 6.0 # Using underscore variant
|
||||||
|
},
|
||||||
|
"sui_models": [],
|
||||||
|
"sui_extra_data": {}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
result = await self.parser.parse_metadata(metadata_str)
|
||||||
|
|
||||||
|
assert result.get('gen_params', {}).get('negative_prompt') == "bad quality"
|
||||||
|
assert result.get('gen_params', {}).get('cfg_scale') == 6.0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_parse_metadata_error_handling(self):
|
||||||
|
"""Test that parser handles malformed data gracefully."""
|
||||||
|
# Missing required fields
|
||||||
|
metadata = {
|
||||||
|
"sui_image_params": {},
|
||||||
|
"sui_models": [],
|
||||||
|
"sui_extra_data": {}
|
||||||
|
}
|
||||||
|
metadata_str = json.dumps(metadata)
|
||||||
|
result = await self.parser.parse_metadata(metadata_str)
|
||||||
|
|
||||||
|
assert 'error' not in result
|
||||||
|
assert result.get('loras') == []
|
||||||
|
# Empty params result in empty gen_params dict
|
||||||
|
assert result.get('gen_params') == {}
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
:exclude-tags="state.excludeTags.value"
|
:exclude-tags="state.excludeTags.value"
|
||||||
:include-folders="state.includeFolders.value"
|
:include-folders="state.includeFolders.value"
|
||||||
:exclude-folders="state.excludeFolders.value"
|
:exclude-folders="state.excludeFolders.value"
|
||||||
|
:include-patterns="state.includePatterns.value"
|
||||||
|
:exclude-patterns="state.excludePatterns.value"
|
||||||
|
:use-regex="state.useRegex.value"
|
||||||
:no-credit-required="state.noCreditRequired.value"
|
:no-credit-required="state.noCreditRequired.value"
|
||||||
:allow-selling="state.allowSelling.value"
|
:allow-selling="state.allowSelling.value"
|
||||||
:preview-items="state.previewItems.value"
|
:preview-items="state.previewItems.value"
|
||||||
@@ -16,6 +19,9 @@
|
|||||||
@open-modal="openModal"
|
@open-modal="openModal"
|
||||||
@update:include-folders="state.includeFolders.value = $event"
|
@update:include-folders="state.includeFolders.value = $event"
|
||||||
@update:exclude-folders="state.excludeFolders.value = $event"
|
@update:exclude-folders="state.excludeFolders.value = $event"
|
||||||
|
@update:include-patterns="state.includePatterns.value = $event"
|
||||||
|
@update:exclude-patterns="state.excludePatterns.value = $event"
|
||||||
|
@update:use-regex="state.useRegex.value = $event"
|
||||||
@update:no-credit-required="state.noCreditRequired.value = $event"
|
@update:no-credit-required="state.noCreditRequired.value = $event"
|
||||||
@update:allow-selling="state.allowSelling.value = $event"
|
@update:allow-selling="state.allowSelling.value = $event"
|
||||||
@refresh="state.refreshPreview"
|
@refresh="state.refreshPreview"
|
||||||
|
|||||||
@@ -24,6 +24,15 @@
|
|||||||
@edit-exclude="$emit('open-modal', 'excludeFolders')"
|
@edit-exclude="$emit('open-modal', 'excludeFolders')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<NamePatternsSection
|
||||||
|
:include-patterns="includePatterns"
|
||||||
|
:exclude-patterns="excludePatterns"
|
||||||
|
:use-regex="useRegex"
|
||||||
|
@update:include-patterns="$emit('update:includePatterns', $event)"
|
||||||
|
@update:exclude-patterns="$emit('update:excludePatterns', $event)"
|
||||||
|
@update:use-regex="$emit('update:useRegex', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
<LicenseSection
|
<LicenseSection
|
||||||
:no-credit-required="noCreditRequired"
|
:no-credit-required="noCreditRequired"
|
||||||
:allow-selling="allowSelling"
|
:allow-selling="allowSelling"
|
||||||
@@ -46,6 +55,7 @@
|
|||||||
import BaseModelSection from './sections/BaseModelSection.vue'
|
import BaseModelSection from './sections/BaseModelSection.vue'
|
||||||
import TagsSection from './sections/TagsSection.vue'
|
import TagsSection from './sections/TagsSection.vue'
|
||||||
import FoldersSection from './sections/FoldersSection.vue'
|
import FoldersSection from './sections/FoldersSection.vue'
|
||||||
|
import NamePatternsSection from './sections/NamePatternsSection.vue'
|
||||||
import LicenseSection from './sections/LicenseSection.vue'
|
import LicenseSection from './sections/LicenseSection.vue'
|
||||||
import LoraPoolPreview from './LoraPoolPreview.vue'
|
import LoraPoolPreview from './LoraPoolPreview.vue'
|
||||||
import type { BaseModelOption, LoraItem } from '../../composables/types'
|
import type { BaseModelOption, LoraItem } from '../../composables/types'
|
||||||
@@ -61,6 +71,10 @@ defineProps<{
|
|||||||
// Folders
|
// Folders
|
||||||
includeFolders: string[]
|
includeFolders: string[]
|
||||||
excludeFolders: string[]
|
excludeFolders: string[]
|
||||||
|
// Name patterns
|
||||||
|
includePatterns: string[]
|
||||||
|
excludePatterns: string[]
|
||||||
|
useRegex: boolean
|
||||||
// License
|
// License
|
||||||
noCreditRequired: boolean
|
noCreditRequired: boolean
|
||||||
allowSelling: boolean
|
allowSelling: boolean
|
||||||
@@ -74,6 +88,9 @@ defineEmits<{
|
|||||||
'open-modal': [modal: ModalType]
|
'open-modal': [modal: ModalType]
|
||||||
'update:includeFolders': [value: string[]]
|
'update:includeFolders': [value: string[]]
|
||||||
'update:excludeFolders': [value: string[]]
|
'update:excludeFolders': [value: string[]]
|
||||||
|
'update:includePatterns': [value: string[]]
|
||||||
|
'update:excludePatterns': [value: string[]]
|
||||||
|
'update:useRegex': [value: boolean]
|
||||||
'update:noCreditRequired': [value: boolean]
|
'update:noCreditRequired': [value: boolean]
|
||||||
'update:allowSelling': [value: boolean]
|
'update:allowSelling': [value: boolean]
|
||||||
refresh: []
|
refresh: []
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
<template>
|
||||||
|
<div class="section">
|
||||||
|
<div class="section__header">
|
||||||
|
<span class="section__title">NAME PATTERNS</span>
|
||||||
|
<label class="section__toggle">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="useRegex"
|
||||||
|
@change="$emit('update:useRegex', ($event.target as HTMLInputElement).checked)"
|
||||||
|
/>
|
||||||
|
<span class="section__toggle-label">Use Regex</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="section__columns">
|
||||||
|
<!-- Include column -->
|
||||||
|
<div class="section__column">
|
||||||
|
<div class="section__column-header">
|
||||||
|
<span class="section__column-title section__column-title--include">INCLUDE</span>
|
||||||
|
</div>
|
||||||
|
<div class="section__input-wrapper">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="includeInput"
|
||||||
|
:placeholder="useRegex ? 'Add regex pattern...' : 'Add text pattern...'"
|
||||||
|
class="section__input"
|
||||||
|
@keydown.enter="addInclude"
|
||||||
|
/>
|
||||||
|
<button type="button" class="section__add-btn" @click="addInclude">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="section__patterns">
|
||||||
|
<FilterChip
|
||||||
|
v-for="pattern in includePatterns"
|
||||||
|
:key="pattern"
|
||||||
|
:label="pattern"
|
||||||
|
variant="include"
|
||||||
|
removable
|
||||||
|
@remove="removeInclude(pattern)"
|
||||||
|
/>
|
||||||
|
<div v-if="includePatterns.length === 0" class="section__empty">
|
||||||
|
{{ useRegex ? 'No regex patterns' : 'No text patterns' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Exclude column -->
|
||||||
|
<div class="section__column">
|
||||||
|
<div class="section__column-header">
|
||||||
|
<span class="section__column-title section__column-title--exclude">EXCLUDE</span>
|
||||||
|
</div>
|
||||||
|
<div class="section__input-wrapper">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="excludeInput"
|
||||||
|
:placeholder="useRegex ? 'Add regex pattern...' : 'Add text pattern...'"
|
||||||
|
class="section__input"
|
||||||
|
@keydown.enter="addExclude"
|
||||||
|
/>
|
||||||
|
<button type="button" class="section__add-btn" @click="addExclude">+</button>
|
||||||
|
</div>
|
||||||
|
<div class="section__patterns">
|
||||||
|
<FilterChip
|
||||||
|
v-for="pattern in excludePatterns"
|
||||||
|
:key="pattern"
|
||||||
|
:label="pattern"
|
||||||
|
variant="exclude"
|
||||||
|
removable
|
||||||
|
@remove="removeExclude(pattern)"
|
||||||
|
/>
|
||||||
|
<div v-if="excludePatterns.length === 0" class="section__empty">
|
||||||
|
{{ useRegex ? 'No regex patterns' : 'No text patterns' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import FilterChip from '../shared/FilterChip.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
includePatterns: string[]
|
||||||
|
excludePatterns: string[]
|
||||||
|
useRegex: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:includePatterns': [value: string[]]
|
||||||
|
'update:excludePatterns': [value: string[]]
|
||||||
|
'update:useRegex': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const includeInput = ref('')
|
||||||
|
const excludeInput = ref('')
|
||||||
|
|
||||||
|
const addInclude = () => {
|
||||||
|
const pattern = includeInput.value.trim()
|
||||||
|
if (pattern && !props.includePatterns.includes(pattern)) {
|
||||||
|
emit('update:includePatterns', [...props.includePatterns, pattern])
|
||||||
|
includeInput.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const addExclude = () => {
|
||||||
|
const pattern = excludeInput.value.trim()
|
||||||
|
if (pattern && !props.excludePatterns.includes(pattern)) {
|
||||||
|
emit('update:excludePatterns', [...props.excludePatterns, pattern])
|
||||||
|
excludeInput.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeInclude = (pattern: string) => {
|
||||||
|
emit('update:includePatterns', props.includePatterns.filter(p => p !== pattern))
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeExclude = (pattern: string) => {
|
||||||
|
emit('update:excludePatterns', props.excludePatterns.filter(p => p !== pattern))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__title {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__toggle input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__toggle-label {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__column {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__column-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__column-title {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__column-title--include {
|
||||||
|
color: #4299e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__column-title--exclude {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__input-wrapper {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--comfy-input-bg, #333);
|
||||||
|
border: 1px solid var(--comfy-input-border, #444);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__input:focus {
|
||||||
|
border-color: #4299e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__add-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--comfy-input-bg, #333);
|
||||||
|
border: 1px solid var(--comfy-input-border, #444);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__add-btn:hover {
|
||||||
|
background: var(--comfy-input-bg-hover, #444);
|
||||||
|
border-color: #4299e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__patterns {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__empty {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.3;
|
||||||
|
font-style: italic;
|
||||||
|
min-height: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -10,6 +10,11 @@ export interface LoraPoolConfig {
|
|||||||
noCreditRequired: boolean
|
noCreditRequired: boolean
|
||||||
allowSelling: boolean
|
allowSelling: boolean
|
||||||
}
|
}
|
||||||
|
namePatterns: {
|
||||||
|
include: string[]
|
||||||
|
exclude: string[]
|
||||||
|
useRegex: boolean
|
||||||
|
}
|
||||||
includeEmptyLora?: boolean // Optional, deprecated (moved to Cycler)
|
includeEmptyLora?: boolean // Optional, deprecated (moved to Cycler)
|
||||||
}
|
}
|
||||||
preview: { matchCount: number; lastUpdated: number }
|
preview: { matchCount: number; lastUpdated: number }
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ export function useLoraPoolApi() {
|
|||||||
foldersExclude?: string[]
|
foldersExclude?: string[]
|
||||||
noCreditRequired?: boolean
|
noCreditRequired?: boolean
|
||||||
allowSelling?: boolean
|
allowSelling?: boolean
|
||||||
|
namePatternsInclude?: string[]
|
||||||
|
namePatternsExclude?: string[]
|
||||||
|
namePatternsUseRegex?: boolean
|
||||||
page?: number
|
page?: number
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
}
|
}
|
||||||
@@ -92,6 +95,13 @@ export function useLoraPoolApi() {
|
|||||||
urlParams.set('allow_selling_generated_content', String(params.allowSelling))
|
urlParams.set('allow_selling_generated_content', String(params.allowSelling))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Name pattern filters
|
||||||
|
params.namePatternsInclude?.forEach(pattern => urlParams.append('name_pattern_include', pattern))
|
||||||
|
params.namePatternsExclude?.forEach(pattern => urlParams.append('name_pattern_exclude', pattern))
|
||||||
|
if (params.namePatternsUseRegex !== undefined) {
|
||||||
|
urlParams.set('name_pattern_use_regex', String(params.namePatternsUseRegex))
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`/api/lm/loras/list?${urlParams}`)
|
const response = await fetch(`/api/lm/loras/list?${urlParams}`)
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ export function useLoraPoolState(widget: ComponentWidget<LoraPoolConfig>) {
|
|||||||
const excludeFolders = ref<string[]>([])
|
const excludeFolders = ref<string[]>([])
|
||||||
const noCreditRequired = ref(false)
|
const noCreditRequired = ref(false)
|
||||||
const allowSelling = ref(false)
|
const allowSelling = ref(false)
|
||||||
|
const includePatterns = ref<string[]>([])
|
||||||
|
const excludePatterns = ref<string[]>([])
|
||||||
|
const useRegex = ref(false)
|
||||||
|
|
||||||
// Available options from API
|
// Available options from API
|
||||||
const availableBaseModels = ref<BaseModelOption[]>([])
|
const availableBaseModels = ref<BaseModelOption[]>([])
|
||||||
@@ -52,6 +55,11 @@ export function useLoraPoolState(widget: ComponentWidget<LoraPoolConfig>) {
|
|||||||
license: {
|
license: {
|
||||||
noCreditRequired: noCreditRequired.value,
|
noCreditRequired: noCreditRequired.value,
|
||||||
allowSelling: allowSelling.value
|
allowSelling: allowSelling.value
|
||||||
|
},
|
||||||
|
namePatterns: {
|
||||||
|
include: includePatterns.value,
|
||||||
|
exclude: excludePatterns.value,
|
||||||
|
useRegex: useRegex.value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
@@ -94,6 +102,9 @@ export function useLoraPoolState(widget: ComponentWidget<LoraPoolConfig>) {
|
|||||||
updateIfChanged(excludeFolders, filters.folders?.exclude || [])
|
updateIfChanged(excludeFolders, filters.folders?.exclude || [])
|
||||||
updateIfChanged(noCreditRequired, filters.license?.noCreditRequired ?? false)
|
updateIfChanged(noCreditRequired, filters.license?.noCreditRequired ?? false)
|
||||||
updateIfChanged(allowSelling, filters.license?.allowSelling ?? false)
|
updateIfChanged(allowSelling, filters.license?.allowSelling ?? false)
|
||||||
|
updateIfChanged(includePatterns, filters.namePatterns?.include || [])
|
||||||
|
updateIfChanged(excludePatterns, filters.namePatterns?.exclude || [])
|
||||||
|
updateIfChanged(useRegex, filters.namePatterns?.useRegex ?? false)
|
||||||
|
|
||||||
// matchCount doesn't trigger watchers, so direct assignment is fine
|
// matchCount doesn't trigger watchers, so direct assignment is fine
|
||||||
matchCount.value = preview?.matchCount || 0
|
matchCount.value = preview?.matchCount || 0
|
||||||
@@ -125,6 +136,9 @@ export function useLoraPoolState(widget: ComponentWidget<LoraPoolConfig>) {
|
|||||||
foldersExclude: excludeFolders.value,
|
foldersExclude: excludeFolders.value,
|
||||||
noCreditRequired: noCreditRequired.value || undefined,
|
noCreditRequired: noCreditRequired.value || undefined,
|
||||||
allowSelling: allowSelling.value || undefined,
|
allowSelling: allowSelling.value || undefined,
|
||||||
|
namePatternsInclude: includePatterns.value,
|
||||||
|
namePatternsExclude: excludePatterns.value,
|
||||||
|
namePatternsUseRegex: useRegex.value,
|
||||||
pageSize: 6
|
pageSize: 6
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -150,7 +164,10 @@ export function useLoraPoolState(widget: ComponentWidget<LoraPoolConfig>) {
|
|||||||
includeFolders,
|
includeFolders,
|
||||||
excludeFolders,
|
excludeFolders,
|
||||||
noCreditRequired,
|
noCreditRequired,
|
||||||
allowSelling
|
allowSelling,
|
||||||
|
includePatterns,
|
||||||
|
excludePatterns,
|
||||||
|
useRegex
|
||||||
], onFilterChange, { deep: true })
|
], onFilterChange, { deep: true })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -162,6 +179,9 @@ export function useLoraPoolState(widget: ComponentWidget<LoraPoolConfig>) {
|
|||||||
excludeFolders,
|
excludeFolders,
|
||||||
noCreditRequired,
|
noCreditRequired,
|
||||||
allowSelling,
|
allowSelling,
|
||||||
|
includePatterns,
|
||||||
|
excludePatterns,
|
||||||
|
useRegex,
|
||||||
|
|
||||||
// Available options
|
// Available options
|
||||||
availableBaseModels,
|
availableBaseModels,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from './mode-change-handler'
|
} from './mode-change-handler'
|
||||||
|
|
||||||
const LORA_POOL_WIDGET_MIN_WIDTH = 500
|
const LORA_POOL_WIDGET_MIN_WIDTH = 500
|
||||||
const LORA_POOL_WIDGET_MIN_HEIGHT = 400
|
const LORA_POOL_WIDGET_MIN_HEIGHT = 520
|
||||||
const LORA_RANDOMIZER_WIDGET_MIN_WIDTH = 500
|
const LORA_RANDOMIZER_WIDGET_MIN_WIDTH = 500
|
||||||
const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448
|
const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448
|
||||||
const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT
|
const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { initDrag, createContextMenu, initHeaderDrag, initReorderDrag, handleKey
|
|||||||
import { forwardMiddleMouseToCanvas } from "./utils.js";
|
import { forwardMiddleMouseToCanvas } from "./utils.js";
|
||||||
import { PreviewTooltip } from "./preview_tooltip.js";
|
import { PreviewTooltip } from "./preview_tooltip.js";
|
||||||
import { ensureLmStyles } from "./lm_styles_loader.js";
|
import { ensureLmStyles } from "./lm_styles_loader.js";
|
||||||
|
import { getStrengthStepPreference } from "./settings.js";
|
||||||
|
|
||||||
export function addLorasWidget(node, name, opts, callback) {
|
export function addLorasWidget(node, name, opts, callback) {
|
||||||
ensureLmStyles();
|
ensureLmStyles();
|
||||||
@@ -416,7 +417,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
if (loraIndex >= 0) {
|
if (loraIndex >= 0) {
|
||||||
lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) - 0.05).toFixed(2);
|
lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) - getStrengthStepPreference()).toFixed(2);
|
||||||
// Sync clipStrength if collapsed
|
// Sync clipStrength if collapsed
|
||||||
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
||||||
|
|
||||||
@@ -488,7 +489,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
if (loraIndex >= 0) {
|
if (loraIndex >= 0) {
|
||||||
lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) + 0.05).toFixed(2);
|
lorasData[loraIndex].strength = (parseFloat(lorasData[loraIndex].strength) + getStrengthStepPreference()).toFixed(2);
|
||||||
// Sync clipStrength if collapsed
|
// Sync clipStrength if collapsed
|
||||||
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
syncClipStrengthIfCollapsed(lorasData[loraIndex]);
|
||||||
|
|
||||||
@@ -541,7 +542,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
if (loraIndex >= 0) {
|
if (loraIndex >= 0) {
|
||||||
lorasData[loraIndex].clipStrength = (parseFloat(lorasData[loraIndex].clipStrength) - 0.05).toFixed(2);
|
lorasData[loraIndex].clipStrength = (parseFloat(lorasData[loraIndex].clipStrength) - getStrengthStepPreference()).toFixed(2);
|
||||||
|
|
||||||
const newValue = formatLoraValue(lorasData);
|
const newValue = formatLoraValue(lorasData);
|
||||||
updateWidgetValue(newValue);
|
updateWidgetValue(newValue);
|
||||||
@@ -611,7 +612,7 @@ export function addLorasWidget(node, name, opts, callback) {
|
|||||||
const loraIndex = lorasData.findIndex(l => l.name === name);
|
const loraIndex = lorasData.findIndex(l => l.name === name);
|
||||||
|
|
||||||
if (loraIndex >= 0) {
|
if (loraIndex >= 0) {
|
||||||
lorasData[loraIndex].clipStrength = (parseFloat(lorasData[loraIndex].clipStrength) + 0.05).toFixed(2);
|
lorasData[loraIndex].clipStrength = (parseFloat(lorasData[loraIndex].clipStrength) + getStrengthStepPreference()).toFixed(2);
|
||||||
|
|
||||||
const newValue = formatLoraValue(lorasData);
|
const newValue = formatLoraValue(lorasData);
|
||||||
updateWidgetValue(newValue);
|
updateWidgetValue(newValue);
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const NEW_TAB_TEMPLATE_DEFAULT = "Default";
|
|||||||
|
|
||||||
const NEW_TAB_ZOOM_LEVEL = 0.8;
|
const NEW_TAB_ZOOM_LEVEL = 0.8;
|
||||||
|
|
||||||
|
const STRENGTH_STEP_SETTING_ID = "loramanager.strength_step";
|
||||||
|
const STRENGTH_STEP_DEFAULT = 0.05;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Helper Functions
|
// Helper Functions
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -232,6 +235,32 @@ const getNewTabTemplatePreference = (() => {
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const getStrengthStepPreference = (() => {
|
||||||
|
let settingsUnavailableLogged = false;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const settingManager = app?.extensionManager?.setting;
|
||||||
|
if (!settingManager || typeof settingManager.get !== "function") {
|
||||||
|
if (!settingsUnavailableLogged) {
|
||||||
|
console.warn("LoRA Manager: settings API unavailable, using default strength step.");
|
||||||
|
settingsUnavailableLogged = true;
|
||||||
|
}
|
||||||
|
return STRENGTH_STEP_DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const value = settingManager.get(STRENGTH_STEP_SETTING_ID);
|
||||||
|
return value ?? STRENGTH_STEP_DEFAULT;
|
||||||
|
} catch (error) {
|
||||||
|
if (!settingsUnavailableLogged) {
|
||||||
|
console.warn("LoRA Manager: unable to read strength step setting, using default.", error);
|
||||||
|
settingsUnavailableLogged = true;
|
||||||
|
}
|
||||||
|
return STRENGTH_STEP_DEFAULT;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Register Extension with All Settings
|
// Register Extension with All Settings
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -293,6 +322,19 @@ app.registerExtension({
|
|||||||
tooltip: "Choose a template workflow to load when creating a new workflow tab. 'Default (Blank)' keeps ComfyUI's original blank workflow behavior.",
|
tooltip: "Choose a template workflow to load when creating a new workflow tab. 'Default (Blank)' keeps ComfyUI's original blank workflow behavior.",
|
||||||
category: ["LoRA Manager", "Workflow", "New Tab Template"],
|
category: ["LoRA Manager", "Workflow", "New Tab Template"],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: STRENGTH_STEP_SETTING_ID,
|
||||||
|
name: "Strength Adjustment Step",
|
||||||
|
type: "slider",
|
||||||
|
attrs: {
|
||||||
|
min: 0.01,
|
||||||
|
max: 0.1,
|
||||||
|
step: 0.01,
|
||||||
|
},
|
||||||
|
defaultValue: STRENGTH_STEP_DEFAULT,
|
||||||
|
tooltip: "Step size for adjusting LoRA strength via arrow buttons or keyboard (default: 0.05)",
|
||||||
|
category: ["LoRA Manager", "LoRA Widget", "Strength Step"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
async setup() {
|
async setup() {
|
||||||
await loadWorkflowOptions();
|
await loadWorkflowOptions();
|
||||||
@@ -375,4 +417,5 @@ export {
|
|||||||
getTagSpaceReplacementPreference,
|
getTagSpaceReplacementPreference,
|
||||||
getUsageStatisticsPreference,
|
getUsageStatisticsPreference,
|
||||||
getNewTabTemplatePreference,
|
getNewTabTemplatePreference,
|
||||||
|
getStrengthStepPreference,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -283,6 +283,121 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section[data-v-9995b5ed] {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.section__header[data-v-9995b5ed] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.section__title[data-v-9995b5ed] {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.section__toggle[data-v-9995b5ed] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.section__toggle input[type="checkbox"][data-v-9995b5ed] {
|
||||||
|
margin: 0;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.section__toggle-label[data-v-9995b5ed] {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.section__columns[data-v-9995b5ed] {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.section__column[data-v-9995b5ed] {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.section__column-header[data-v-9995b5ed] {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.section__column-title[data-v-9995b5ed] {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
.section__column-title--include[data-v-9995b5ed] {
|
||||||
|
color: #4299e1;
|
||||||
|
}
|
||||||
|
.section__column-title--exclude[data-v-9995b5ed] {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
.section__input-wrapper[data-v-9995b5ed] {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.section__input[data-v-9995b5ed] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--comfy-input-bg, #333);
|
||||||
|
border: 1px solid var(--comfy-input-border, #444);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.section__input[data-v-9995b5ed]:focus {
|
||||||
|
border-color: #4299e1;
|
||||||
|
}
|
||||||
|
.section__add-btn[data-v-9995b5ed] {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--comfy-input-bg, #333);
|
||||||
|
border: 1px solid var(--comfy-input-border, #444);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.section__add-btn[data-v-9995b5ed]:hover {
|
||||||
|
background: var(--comfy-input-bg-hover, #444);
|
||||||
|
border-color: #4299e1;
|
||||||
|
}
|
||||||
|
.section__patterns[data-v-9995b5ed] {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
min-height: 22px;
|
||||||
|
}
|
||||||
|
.section__empty[data-v-9995b5ed] {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--fg-color, #fff);
|
||||||
|
opacity: 0.3;
|
||||||
|
font-style: italic;
|
||||||
|
min-height: 22px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.section[data-v-dea4adf6] {
|
.section[data-v-dea4adf6] {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@@ -483,12 +598,12 @@ to { transform: rotate(360deg);
|
|||||||
transform: translateY(4px);
|
transform: translateY(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-view[data-v-328e7526] {
|
.summary-view[data-v-83235a00] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.summary-view__filters[data-v-328e7526] {
|
.summary-view__filters[data-v-83235a00] {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
@@ -979,7 +1094,7 @@ to { transform: rotate(360deg);
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lora-pool-widget[data-v-4456abba] {
|
.lora-pool-widget[data-v-0bbd50ea] {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: rgba(40, 44, 52, 0.6);
|
background: rgba(40, 44, 52, 0.6);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -10505,12 +10620,12 @@ var PrimeVue = {
|
|||||||
setup(app2, configOptions);
|
setup(app2, configOptions);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const _hoisted_1$m = { class: "filter-chip__text" };
|
const _hoisted_1$n = { class: "filter-chip__text" };
|
||||||
const _hoisted_2$i = {
|
const _hoisted_2$j = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "filter-chip__count"
|
class: "filter-chip__count"
|
||||||
};
|
};
|
||||||
const _sfc_main$n = /* @__PURE__ */ defineComponent({
|
const _sfc_main$o = /* @__PURE__ */ defineComponent({
|
||||||
__name: "FilterChip",
|
__name: "FilterChip",
|
||||||
props: {
|
props: {
|
||||||
label: {},
|
label: {},
|
||||||
@@ -10528,8 +10643,8 @@ const _sfc_main$n = /* @__PURE__ */ defineComponent({
|
|||||||
return openBlock(), createElementBlock("span", {
|
return openBlock(), createElementBlock("span", {
|
||||||
class: normalizeClass(["filter-chip", variantClass.value])
|
class: normalizeClass(["filter-chip", variantClass.value])
|
||||||
}, [
|
}, [
|
||||||
createBaseVNode("span", _hoisted_1$m, toDisplayString(__props.label), 1),
|
createBaseVNode("span", _hoisted_1$n, toDisplayString(__props.label), 1),
|
||||||
__props.count !== void 0 ? (openBlock(), createElementBlock("span", _hoisted_2$i, "(" + toDisplayString(__props.count) + ")", 1)) : createCommentVNode("", true),
|
__props.count !== void 0 ? (openBlock(), createElementBlock("span", _hoisted_2$j, "(" + toDisplayString(__props.count) + ")", 1)) : createCommentVNode("", true),
|
||||||
__props.removable ? (openBlock(), createElementBlock("button", {
|
__props.removable ? (openBlock(), createElementBlock("button", {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "filter-chip__remove",
|
class: "filter-chip__remove",
|
||||||
@@ -10547,8 +10662,8 @@ const _export_sfc = (sfc, props) => {
|
|||||||
}
|
}
|
||||||
return target;
|
return target;
|
||||||
};
|
};
|
||||||
const FilterChip = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-7e36267d"]]);
|
const FilterChip = /* @__PURE__ */ _export_sfc(_sfc_main$o, [["__scopeId", "data-v-7e36267d"]]);
|
||||||
const _sfc_main$m = /* @__PURE__ */ defineComponent({
|
const _sfc_main$n = /* @__PURE__ */ defineComponent({
|
||||||
__name: "EditButton",
|
__name: "EditButton",
|
||||||
emits: ["click"],
|
emits: ["click"],
|
||||||
setup(__props) {
|
setup(__props) {
|
||||||
@@ -10570,19 +10685,19 @@ const _sfc_main$m = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const EditButton = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "data-v-8da8aa4b"]]);
|
const EditButton = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-8da8aa4b"]]);
|
||||||
const _hoisted_1$l = { class: "section" };
|
const _hoisted_1$m = { class: "section" };
|
||||||
const _hoisted_2$h = { class: "section__header" };
|
const _hoisted_2$i = { class: "section__header" };
|
||||||
const _hoisted_3$f = { class: "section__content" };
|
const _hoisted_3$g = { class: "section__content" };
|
||||||
const _hoisted_4$d = {
|
const _hoisted_4$e = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "section__placeholder"
|
class: "section__placeholder"
|
||||||
};
|
};
|
||||||
const _hoisted_5$b = {
|
const _hoisted_5$c = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "section__chips"
|
class: "section__chips"
|
||||||
};
|
};
|
||||||
const _sfc_main$l = /* @__PURE__ */ defineComponent({
|
const _sfc_main$m = /* @__PURE__ */ defineComponent({
|
||||||
__name: "BaseModelSection",
|
__name: "BaseModelSection",
|
||||||
props: {
|
props: {
|
||||||
selected: {},
|
selected: {},
|
||||||
@@ -10596,15 +10711,15 @@ const _sfc_main$l = /* @__PURE__ */ defineComponent({
|
|||||||
return model == null ? void 0 : model.count;
|
return model == null ? void 0 : model.count;
|
||||||
};
|
};
|
||||||
return (_ctx, _cache) => {
|
return (_ctx, _cache) => {
|
||||||
return openBlock(), createElementBlock("div", _hoisted_1$l, [
|
return openBlock(), createElementBlock("div", _hoisted_1$m, [
|
||||||
createBaseVNode("div", _hoisted_2$h, [
|
createBaseVNode("div", _hoisted_2$i, [
|
||||||
_cache[1] || (_cache[1] = createBaseVNode("span", { class: "section__title" }, "BASE MODEL", -1)),
|
_cache[1] || (_cache[1] = createBaseVNode("span", { class: "section__title" }, "BASE MODEL", -1)),
|
||||||
createVNode(EditButton, {
|
createVNode(EditButton, {
|
||||||
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit"))
|
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit"))
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
createBaseVNode("div", _hoisted_3$f, [
|
createBaseVNode("div", _hoisted_3$g, [
|
||||||
__props.selected.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_4$d, " All models ")) : (openBlock(), createElementBlock("div", _hoisted_5$b, [
|
__props.selected.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_4$e, " All models ")) : (openBlock(), createElementBlock("div", _hoisted_5$c, [
|
||||||
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.selected, (name) => {
|
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.selected, (name) => {
|
||||||
return openBlock(), createBlock(FilterChip, {
|
return openBlock(), createBlock(FilterChip, {
|
||||||
key: name,
|
key: name,
|
||||||
@@ -10619,32 +10734,32 @@ const _sfc_main$l = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const BaseModelSection = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "data-v-12f059e2"]]);
|
const BaseModelSection = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "data-v-12f059e2"]]);
|
||||||
const _hoisted_1$k = { class: "section" };
|
const _hoisted_1$l = { class: "section" };
|
||||||
const _hoisted_2$g = { class: "section__columns" };
|
const _hoisted_2$h = { class: "section__columns" };
|
||||||
const _hoisted_3$e = { class: "section__column" };
|
const _hoisted_3$f = { class: "section__column" };
|
||||||
const _hoisted_4$c = { class: "section__column-header" };
|
const _hoisted_4$d = { class: "section__column-header" };
|
||||||
const _hoisted_5$a = { class: "section__column-content" };
|
const _hoisted_5$b = { class: "section__column-content" };
|
||||||
const _hoisted_6$a = {
|
const _hoisted_6$b = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "section__empty"
|
class: "section__empty"
|
||||||
};
|
};
|
||||||
const _hoisted_7$8 = {
|
const _hoisted_7$9 = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "section__chips"
|
class: "section__chips"
|
||||||
};
|
};
|
||||||
const _hoisted_8$6 = { class: "section__column" };
|
const _hoisted_8$7 = { class: "section__column" };
|
||||||
const _hoisted_9$4 = { class: "section__column-header" };
|
const _hoisted_9$5 = { class: "section__column-header" };
|
||||||
const _hoisted_10$4 = { class: "section__column-content" };
|
const _hoisted_10$5 = { class: "section__column-content" };
|
||||||
const _hoisted_11$3 = {
|
const _hoisted_11$4 = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "section__empty"
|
class: "section__empty"
|
||||||
};
|
};
|
||||||
const _hoisted_12$3 = {
|
const _hoisted_12$4 = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "section__chips"
|
class: "section__chips"
|
||||||
};
|
};
|
||||||
const _sfc_main$k = /* @__PURE__ */ defineComponent({
|
const _sfc_main$l = /* @__PURE__ */ defineComponent({
|
||||||
__name: "TagsSection",
|
__name: "TagsSection",
|
||||||
props: {
|
props: {
|
||||||
includeTags: {},
|
includeTags: {},
|
||||||
@@ -10653,20 +10768,20 @@ const _sfc_main$k = /* @__PURE__ */ defineComponent({
|
|||||||
emits: ["edit-include", "edit-exclude"],
|
emits: ["edit-include", "edit-exclude"],
|
||||||
setup(__props) {
|
setup(__props) {
|
||||||
return (_ctx, _cache) => {
|
return (_ctx, _cache) => {
|
||||||
return openBlock(), createElementBlock("div", _hoisted_1$k, [
|
return openBlock(), createElementBlock("div", _hoisted_1$l, [
|
||||||
_cache[4] || (_cache[4] = createBaseVNode("div", { class: "section__header" }, [
|
_cache[4] || (_cache[4] = createBaseVNode("div", { class: "section__header" }, [
|
||||||
createBaseVNode("span", { class: "section__title" }, "TAGS")
|
createBaseVNode("span", { class: "section__title" }, "TAGS")
|
||||||
], -1)),
|
], -1)),
|
||||||
createBaseVNode("div", _hoisted_2$g, [
|
createBaseVNode("div", _hoisted_2$h, [
|
||||||
createBaseVNode("div", _hoisted_3$e, [
|
createBaseVNode("div", _hoisted_3$f, [
|
||||||
createBaseVNode("div", _hoisted_4$c, [
|
createBaseVNode("div", _hoisted_4$d, [
|
||||||
_cache[2] || (_cache[2] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)),
|
_cache[2] || (_cache[2] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)),
|
||||||
createVNode(EditButton, {
|
createVNode(EditButton, {
|
||||||
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit-include"))
|
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit-include"))
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
createBaseVNode("div", _hoisted_5$a, [
|
createBaseVNode("div", _hoisted_5$b, [
|
||||||
__props.includeTags.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_6$a, " None ")) : (openBlock(), createElementBlock("div", _hoisted_7$8, [
|
__props.includeTags.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_6$b, " None ")) : (openBlock(), createElementBlock("div", _hoisted_7$9, [
|
||||||
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.includeTags, (tag) => {
|
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.includeTags, (tag) => {
|
||||||
return openBlock(), createBlock(FilterChip, {
|
return openBlock(), createBlock(FilterChip, {
|
||||||
key: tag,
|
key: tag,
|
||||||
@@ -10677,15 +10792,15 @@ const _sfc_main$k = /* @__PURE__ */ defineComponent({
|
|||||||
]))
|
]))
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
createBaseVNode("div", _hoisted_8$6, [
|
createBaseVNode("div", _hoisted_8$7, [
|
||||||
createBaseVNode("div", _hoisted_9$4, [
|
createBaseVNode("div", _hoisted_9$5, [
|
||||||
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__column-title section__column-title--exclude" }, "EXCLUDE", -1)),
|
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__column-title section__column-title--exclude" }, "EXCLUDE", -1)),
|
||||||
createVNode(EditButton, {
|
createVNode(EditButton, {
|
||||||
onClick: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("edit-exclude"))
|
onClick: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("edit-exclude"))
|
||||||
})
|
})
|
||||||
]),
|
]),
|
||||||
createBaseVNode("div", _hoisted_10$4, [
|
createBaseVNode("div", _hoisted_10$5, [
|
||||||
__props.excludeTags.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_11$3, " None ")) : (openBlock(), createElementBlock("div", _hoisted_12$3, [
|
__props.excludeTags.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_11$4, " None ")) : (openBlock(), createElementBlock("div", _hoisted_12$4, [
|
||||||
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.excludeTags, (tag) => {
|
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.excludeTags, (tag) => {
|
||||||
return openBlock(), createBlock(FilterChip, {
|
return openBlock(), createBlock(FilterChip, {
|
||||||
key: tag,
|
key: tag,
|
||||||
@@ -10701,32 +10816,32 @@ const _sfc_main$k = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const TagsSection = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["__scopeId", "data-v-b869b780"]]);
|
const TagsSection = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "data-v-b869b780"]]);
|
||||||
const _hoisted_1$j = { class: "section" };
|
const _hoisted_1$k = { class: "section" };
|
||||||
const _hoisted_2$f = { class: "section__columns" };
|
const _hoisted_2$g = { class: "section__columns" };
|
||||||
const _hoisted_3$d = { class: "section__column" };
|
const _hoisted_3$e = { class: "section__column" };
|
||||||
const _hoisted_4$b = { class: "section__column-header" };
|
const _hoisted_4$c = { class: "section__column-header" };
|
||||||
const _hoisted_5$9 = { class: "section__content" };
|
const _hoisted_5$a = { class: "section__content" };
|
||||||
const _hoisted_6$9 = {
|
const _hoisted_6$a = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "section__paths"
|
class: "section__paths"
|
||||||
};
|
};
|
||||||
const _hoisted_7$7 = {
|
const _hoisted_7$8 = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "section__empty"
|
class: "section__empty"
|
||||||
};
|
};
|
||||||
const _hoisted_8$5 = { class: "section__column" };
|
const _hoisted_8$6 = { class: "section__column" };
|
||||||
const _hoisted_9$3 = { class: "section__column-header" };
|
const _hoisted_9$4 = { class: "section__column-header" };
|
||||||
const _hoisted_10$3 = { class: "section__content" };
|
const _hoisted_10$4 = { class: "section__content" };
|
||||||
const _hoisted_11$2 = {
|
const _hoisted_11$3 = {
|
||||||
key: 0,
|
key: 0,
|
||||||
class: "section__paths"
|
class: "section__paths"
|
||||||
};
|
};
|
||||||
const _hoisted_12$2 = {
|
const _hoisted_12$3 = {
|
||||||
key: 1,
|
key: 1,
|
||||||
class: "section__empty"
|
class: "section__empty"
|
||||||
};
|
};
|
||||||
const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
const _sfc_main$k = /* @__PURE__ */ defineComponent({
|
||||||
__name: "FoldersSection",
|
__name: "FoldersSection",
|
||||||
props: {
|
props: {
|
||||||
includeFolders: {},
|
includeFolders: {},
|
||||||
@@ -10747,13 +10862,13 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
|||||||
emit2("update:excludeFolders", props.excludeFolders.filter((p2) => p2 !== path));
|
emit2("update:excludeFolders", props.excludeFolders.filter((p2) => p2 !== path));
|
||||||
};
|
};
|
||||||
return (_ctx, _cache) => {
|
return (_ctx, _cache) => {
|
||||||
return openBlock(), createElementBlock("div", _hoisted_1$j, [
|
return openBlock(), createElementBlock("div", _hoisted_1$k, [
|
||||||
_cache[6] || (_cache[6] = createBaseVNode("div", { class: "section__header" }, [
|
_cache[6] || (_cache[6] = createBaseVNode("div", { class: "section__header" }, [
|
||||||
createBaseVNode("span", { class: "section__title" }, "FOLDERS")
|
createBaseVNode("span", { class: "section__title" }, "FOLDERS")
|
||||||
], -1)),
|
], -1)),
|
||||||
createBaseVNode("div", _hoisted_2$f, [
|
createBaseVNode("div", _hoisted_2$g, [
|
||||||
createBaseVNode("div", _hoisted_3$d, [
|
createBaseVNode("div", _hoisted_3$e, [
|
||||||
createBaseVNode("div", _hoisted_4$b, [
|
createBaseVNode("div", _hoisted_4$c, [
|
||||||
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)),
|
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)),
|
||||||
createBaseVNode("button", {
|
createBaseVNode("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
@@ -10768,8 +10883,8 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
|||||||
], -1)
|
], -1)
|
||||||
])])
|
])])
|
||||||
]),
|
]),
|
||||||
createBaseVNode("div", _hoisted_5$9, [
|
createBaseVNode("div", _hoisted_5$a, [
|
||||||
__props.includeFolders.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_6$9, [
|
__props.includeFolders.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_6$a, [
|
||||||
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.includeFolders, (path) => {
|
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.includeFolders, (path) => {
|
||||||
return openBlock(), createBlock(FilterChip, {
|
return openBlock(), createBlock(FilterChip, {
|
||||||
key: path,
|
key: path,
|
||||||
@@ -10779,11 +10894,11 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
|||||||
onRemove: ($event) => removeInclude(path)
|
onRemove: ($event) => removeInclude(path)
|
||||||
}, null, 8, ["label", "onRemove"]);
|
}, null, 8, ["label", "onRemove"]);
|
||||||
}), 128))
|
}), 128))
|
||||||
])) : (openBlock(), createElementBlock("div", _hoisted_7$7, " No folders selected "))
|
])) : (openBlock(), createElementBlock("div", _hoisted_7$8, " No folders selected "))
|
||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
createBaseVNode("div", _hoisted_8$5, [
|
createBaseVNode("div", _hoisted_8$6, [
|
||||||
createBaseVNode("div", _hoisted_9$3, [
|
createBaseVNode("div", _hoisted_9$4, [
|
||||||
_cache[5] || (_cache[5] = createBaseVNode("span", { class: "section__column-title section__column-title--exclude" }, "EXCLUDE", -1)),
|
_cache[5] || (_cache[5] = createBaseVNode("span", { class: "section__column-title section__column-title--exclude" }, "EXCLUDE", -1)),
|
||||||
createBaseVNode("button", {
|
createBaseVNode("button", {
|
||||||
type: "button",
|
type: "button",
|
||||||
@@ -10798,8 +10913,8 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
|||||||
], -1)
|
], -1)
|
||||||
])])
|
])])
|
||||||
]),
|
]),
|
||||||
createBaseVNode("div", _hoisted_10$3, [
|
createBaseVNode("div", _hoisted_10$4, [
|
||||||
__props.excludeFolders.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_11$2, [
|
__props.excludeFolders.length > 0 ? (openBlock(), createElementBlock("div", _hoisted_11$3, [
|
||||||
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.excludeFolders, (path) => {
|
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.excludeFolders, (path) => {
|
||||||
return openBlock(), createBlock(FilterChip, {
|
return openBlock(), createBlock(FilterChip, {
|
||||||
key: path,
|
key: path,
|
||||||
@@ -10809,7 +10924,7 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
|||||||
onRemove: ($event) => removeExclude(path)
|
onRemove: ($event) => removeExclude(path)
|
||||||
}, null, 8, ["label", "onRemove"]);
|
}, null, 8, ["label", "onRemove"]);
|
||||||
}), 128))
|
}), 128))
|
||||||
])) : (openBlock(), createElementBlock("div", _hoisted_12$2, " No folders selected "))
|
])) : (openBlock(), createElementBlock("div", _hoisted_12$3, " No folders selected "))
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
])
|
])
|
||||||
@@ -10817,7 +10932,147 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const FoldersSection = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["__scopeId", "data-v-af9caf84"]]);
|
const FoldersSection = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["__scopeId", "data-v-af9caf84"]]);
|
||||||
|
const _hoisted_1$j = { class: "section" };
|
||||||
|
const _hoisted_2$f = { class: "section__header" };
|
||||||
|
const _hoisted_3$d = { class: "section__toggle" };
|
||||||
|
const _hoisted_4$b = ["checked"];
|
||||||
|
const _hoisted_5$9 = { class: "section__columns" };
|
||||||
|
const _hoisted_6$9 = { class: "section__column" };
|
||||||
|
const _hoisted_7$7 = { class: "section__input-wrapper" };
|
||||||
|
const _hoisted_8$5 = ["placeholder"];
|
||||||
|
const _hoisted_9$3 = { class: "section__patterns" };
|
||||||
|
const _hoisted_10$3 = {
|
||||||
|
key: 0,
|
||||||
|
class: "section__empty"
|
||||||
|
};
|
||||||
|
const _hoisted_11$2 = { class: "section__column" };
|
||||||
|
const _hoisted_12$2 = { class: "section__input-wrapper" };
|
||||||
|
const _hoisted_13$2 = ["placeholder"];
|
||||||
|
const _hoisted_14$2 = { class: "section__patterns" };
|
||||||
|
const _hoisted_15$2 = {
|
||||||
|
key: 0,
|
||||||
|
class: "section__empty"
|
||||||
|
};
|
||||||
|
const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
||||||
|
__name: "NamePatternsSection",
|
||||||
|
props: {
|
||||||
|
includePatterns: {},
|
||||||
|
excludePatterns: {},
|
||||||
|
useRegex: { type: Boolean }
|
||||||
|
},
|
||||||
|
emits: ["update:includePatterns", "update:excludePatterns", "update:useRegex"],
|
||||||
|
setup(__props, { emit: __emit }) {
|
||||||
|
const props = __props;
|
||||||
|
const emit2 = __emit;
|
||||||
|
const includeInput = ref("");
|
||||||
|
const excludeInput = ref("");
|
||||||
|
const addInclude = () => {
|
||||||
|
const pattern = includeInput.value.trim();
|
||||||
|
if (pattern && !props.includePatterns.includes(pattern)) {
|
||||||
|
emit2("update:includePatterns", [...props.includePatterns, pattern]);
|
||||||
|
includeInput.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const addExclude = () => {
|
||||||
|
const pattern = excludeInput.value.trim();
|
||||||
|
if (pattern && !props.excludePatterns.includes(pattern)) {
|
||||||
|
emit2("update:excludePatterns", [...props.excludePatterns, pattern]);
|
||||||
|
excludeInput.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const removeInclude = (pattern) => {
|
||||||
|
emit2("update:includePatterns", props.includePatterns.filter((p2) => p2 !== pattern));
|
||||||
|
};
|
||||||
|
const removeExclude = (pattern) => {
|
||||||
|
emit2("update:excludePatterns", props.excludePatterns.filter((p2) => p2 !== pattern));
|
||||||
|
};
|
||||||
|
return (_ctx, _cache) => {
|
||||||
|
return openBlock(), createElementBlock("div", _hoisted_1$j, [
|
||||||
|
createBaseVNode("div", _hoisted_2$f, [
|
||||||
|
_cache[4] || (_cache[4] = createBaseVNode("span", { class: "section__title" }, "NAME PATTERNS", -1)),
|
||||||
|
createBaseVNode("label", _hoisted_3$d, [
|
||||||
|
createBaseVNode("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
checked: __props.useRegex,
|
||||||
|
onChange: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("update:useRegex", $event.target.checked))
|
||||||
|
}, null, 40, _hoisted_4$b),
|
||||||
|
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__toggle-label" }, "Use Regex", -1))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
createBaseVNode("div", _hoisted_5$9, [
|
||||||
|
createBaseVNode("div", _hoisted_6$9, [
|
||||||
|
_cache[5] || (_cache[5] = createBaseVNode("div", { class: "section__column-header" }, [
|
||||||
|
createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE")
|
||||||
|
], -1)),
|
||||||
|
createBaseVNode("div", _hoisted_7$7, [
|
||||||
|
withDirectives(createBaseVNode("input", {
|
||||||
|
type: "text",
|
||||||
|
"onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => includeInput.value = $event),
|
||||||
|
placeholder: __props.useRegex ? "Add regex pattern..." : "Add text pattern...",
|
||||||
|
class: "section__input",
|
||||||
|
onKeydown: withKeys(addInclude, ["enter"])
|
||||||
|
}, null, 40, _hoisted_8$5), [
|
||||||
|
[vModelText, includeInput.value]
|
||||||
|
]),
|
||||||
|
createBaseVNode("button", {
|
||||||
|
type: "button",
|
||||||
|
class: "section__add-btn",
|
||||||
|
onClick: addInclude
|
||||||
|
}, "+")
|
||||||
|
]),
|
||||||
|
createBaseVNode("div", _hoisted_9$3, [
|
||||||
|
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.includePatterns, (pattern) => {
|
||||||
|
return openBlock(), createBlock(FilterChip, {
|
||||||
|
key: pattern,
|
||||||
|
label: pattern,
|
||||||
|
variant: "include",
|
||||||
|
removable: "",
|
||||||
|
onRemove: ($event) => removeInclude(pattern)
|
||||||
|
}, null, 8, ["label", "onRemove"]);
|
||||||
|
}), 128)),
|
||||||
|
__props.includePatterns.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_10$3, toDisplayString(__props.useRegex ? "No regex patterns" : "No text patterns"), 1)) : createCommentVNode("", true)
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
createBaseVNode("div", _hoisted_11$2, [
|
||||||
|
_cache[6] || (_cache[6] = createBaseVNode("div", { class: "section__column-header" }, [
|
||||||
|
createBaseVNode("span", { class: "section__column-title section__column-title--exclude" }, "EXCLUDE")
|
||||||
|
], -1)),
|
||||||
|
createBaseVNode("div", _hoisted_12$2, [
|
||||||
|
withDirectives(createBaseVNode("input", {
|
||||||
|
type: "text",
|
||||||
|
"onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => excludeInput.value = $event),
|
||||||
|
placeholder: __props.useRegex ? "Add regex pattern..." : "Add text pattern...",
|
||||||
|
class: "section__input",
|
||||||
|
onKeydown: withKeys(addExclude, ["enter"])
|
||||||
|
}, null, 40, _hoisted_13$2), [
|
||||||
|
[vModelText, excludeInput.value]
|
||||||
|
]),
|
||||||
|
createBaseVNode("button", {
|
||||||
|
type: "button",
|
||||||
|
class: "section__add-btn",
|
||||||
|
onClick: addExclude
|
||||||
|
}, "+")
|
||||||
|
]),
|
||||||
|
createBaseVNode("div", _hoisted_14$2, [
|
||||||
|
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.excludePatterns, (pattern) => {
|
||||||
|
return openBlock(), createBlock(FilterChip, {
|
||||||
|
key: pattern,
|
||||||
|
label: pattern,
|
||||||
|
variant: "exclude",
|
||||||
|
removable: "",
|
||||||
|
onRemove: ($event) => removeExclude(pattern)
|
||||||
|
}, null, 8, ["label", "onRemove"]);
|
||||||
|
}), 128)),
|
||||||
|
__props.excludePatterns.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_15$2, toDisplayString(__props.useRegex ? "No regex patterns" : "No text patterns"), 1)) : createCommentVNode("", true)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const NamePatternsSection = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["__scopeId", "data-v-9995b5ed"]]);
|
||||||
const _hoisted_1$i = { class: "section" };
|
const _hoisted_1$i = { class: "section" };
|
||||||
const _hoisted_2$e = { class: "section__toggles" };
|
const _hoisted_2$e = { class: "section__toggles" };
|
||||||
const _hoisted_3$c = { class: "toggle-item" };
|
const _hoisted_3$c = { class: "toggle-item" };
|
||||||
@@ -10982,13 +11237,16 @@ const _sfc_main$g = /* @__PURE__ */ defineComponent({
|
|||||||
excludeTags: {},
|
excludeTags: {},
|
||||||
includeFolders: {},
|
includeFolders: {},
|
||||||
excludeFolders: {},
|
excludeFolders: {},
|
||||||
|
includePatterns: {},
|
||||||
|
excludePatterns: {},
|
||||||
|
useRegex: { type: Boolean },
|
||||||
noCreditRequired: { type: Boolean },
|
noCreditRequired: { type: Boolean },
|
||||||
allowSelling: { type: Boolean },
|
allowSelling: { type: Boolean },
|
||||||
previewItems: {},
|
previewItems: {},
|
||||||
matchCount: {},
|
matchCount: {},
|
||||||
isLoading: { type: Boolean }
|
isLoading: { type: Boolean }
|
||||||
},
|
},
|
||||||
emits: ["open-modal", "update:includeFolders", "update:excludeFolders", "update:noCreditRequired", "update:allowSelling", "refresh"],
|
emits: ["open-modal", "update:includeFolders", "update:excludeFolders", "update:includePatterns", "update:excludePatterns", "update:useRegex", "update:noCreditRequired", "update:allowSelling", "refresh"],
|
||||||
setup(__props) {
|
setup(__props) {
|
||||||
return (_ctx, _cache) => {
|
return (_ctx, _cache) => {
|
||||||
return openBlock(), createElementBlock("div", _hoisted_1$g, [
|
return openBlock(), createElementBlock("div", _hoisted_1$g, [
|
||||||
@@ -11012,24 +11270,32 @@ const _sfc_main$g = /* @__PURE__ */ defineComponent({
|
|||||||
onEditInclude: _cache[5] || (_cache[5] = ($event) => _ctx.$emit("open-modal", "includeFolders")),
|
onEditInclude: _cache[5] || (_cache[5] = ($event) => _ctx.$emit("open-modal", "includeFolders")),
|
||||||
onEditExclude: _cache[6] || (_cache[6] = ($event) => _ctx.$emit("open-modal", "excludeFolders"))
|
onEditExclude: _cache[6] || (_cache[6] = ($event) => _ctx.$emit("open-modal", "excludeFolders"))
|
||||||
}, null, 8, ["include-folders", "exclude-folders"]),
|
}, null, 8, ["include-folders", "exclude-folders"]),
|
||||||
|
createVNode(NamePatternsSection, {
|
||||||
|
"include-patterns": __props.includePatterns,
|
||||||
|
"exclude-patterns": __props.excludePatterns,
|
||||||
|
"use-regex": __props.useRegex,
|
||||||
|
"onUpdate:includePatterns": _cache[7] || (_cache[7] = ($event) => _ctx.$emit("update:includePatterns", $event)),
|
||||||
|
"onUpdate:excludePatterns": _cache[8] || (_cache[8] = ($event) => _ctx.$emit("update:excludePatterns", $event)),
|
||||||
|
"onUpdate:useRegex": _cache[9] || (_cache[9] = ($event) => _ctx.$emit("update:useRegex", $event))
|
||||||
|
}, null, 8, ["include-patterns", "exclude-patterns", "use-regex"]),
|
||||||
createVNode(LicenseSection, {
|
createVNode(LicenseSection, {
|
||||||
"no-credit-required": __props.noCreditRequired,
|
"no-credit-required": __props.noCreditRequired,
|
||||||
"allow-selling": __props.allowSelling,
|
"allow-selling": __props.allowSelling,
|
||||||
"onUpdate:noCreditRequired": _cache[7] || (_cache[7] = ($event) => _ctx.$emit("update:noCreditRequired", $event)),
|
"onUpdate:noCreditRequired": _cache[10] || (_cache[10] = ($event) => _ctx.$emit("update:noCreditRequired", $event)),
|
||||||
"onUpdate:allowSelling": _cache[8] || (_cache[8] = ($event) => _ctx.$emit("update:allowSelling", $event))
|
"onUpdate:allowSelling": _cache[11] || (_cache[11] = ($event) => _ctx.$emit("update:allowSelling", $event))
|
||||||
}, null, 8, ["no-credit-required", "allow-selling"])
|
}, null, 8, ["no-credit-required", "allow-selling"])
|
||||||
]),
|
]),
|
||||||
createVNode(LoraPoolPreview, {
|
createVNode(LoraPoolPreview, {
|
||||||
items: __props.previewItems,
|
items: __props.previewItems,
|
||||||
"match-count": __props.matchCount,
|
"match-count": __props.matchCount,
|
||||||
"is-loading": __props.isLoading,
|
"is-loading": __props.isLoading,
|
||||||
onRefresh: _cache[9] || (_cache[9] = ($event) => _ctx.$emit("refresh"))
|
onRefresh: _cache[12] || (_cache[12] = ($event) => _ctx.$emit("refresh"))
|
||||||
}, null, 8, ["items", "match-count", "is-loading"])
|
}, null, 8, ["items", "match-count", "is-loading"])
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const LoraPoolSummaryView = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["__scopeId", "data-v-328e7526"]]);
|
const LoraPoolSummaryView = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["__scopeId", "data-v-83235a00"]]);
|
||||||
const _hoisted_1$f = { class: "lora-pool-modal__header" };
|
const _hoisted_1$f = { class: "lora-pool-modal__header" };
|
||||||
const _hoisted_2$b = { class: "lora-pool-modal__title-container" };
|
const _hoisted_2$b = { class: "lora-pool-modal__title-container" };
|
||||||
const _hoisted_3$a = { class: "lora-pool-modal__title" };
|
const _hoisted_3$a = { class: "lora-pool-modal__title" };
|
||||||
@@ -11672,7 +11938,7 @@ function useLoraPoolApi() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
const fetchLoras = async (params) => {
|
const fetchLoras = async (params) => {
|
||||||
var _a2, _b, _c, _d;
|
var _a2, _b, _c, _d, _e2, _f;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const urlParams = new URLSearchParams();
|
const urlParams = new URLSearchParams();
|
||||||
@@ -11692,6 +11958,11 @@ function useLoraPoolApi() {
|
|||||||
if (params.allowSelling !== void 0) {
|
if (params.allowSelling !== void 0) {
|
||||||
urlParams.set("allow_selling_generated_content", String(params.allowSelling));
|
urlParams.set("allow_selling_generated_content", String(params.allowSelling));
|
||||||
}
|
}
|
||||||
|
(_e2 = params.namePatternsInclude) == null ? void 0 : _e2.forEach((pattern) => urlParams.append("name_pattern_include", pattern));
|
||||||
|
(_f = params.namePatternsExclude) == null ? void 0 : _f.forEach((pattern) => urlParams.append("name_pattern_exclude", pattern));
|
||||||
|
if (params.namePatternsUseRegex !== void 0) {
|
||||||
|
urlParams.set("name_pattern_use_regex", String(params.namePatternsUseRegex));
|
||||||
|
}
|
||||||
const response = await fetch(`/api/lm/loras/list?${urlParams}`);
|
const response = await fetch(`/api/lm/loras/list?${urlParams}`);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return {
|
return {
|
||||||
@@ -11723,6 +11994,9 @@ function useLoraPoolState(widget) {
|
|||||||
const excludeFolders = ref([]);
|
const excludeFolders = ref([]);
|
||||||
const noCreditRequired = ref(false);
|
const noCreditRequired = ref(false);
|
||||||
const allowSelling = ref(false);
|
const allowSelling = ref(false);
|
||||||
|
const includePatterns = ref([]);
|
||||||
|
const excludePatterns = ref([]);
|
||||||
|
const useRegex = ref(false);
|
||||||
const availableBaseModels = ref([]);
|
const availableBaseModels = ref([]);
|
||||||
const availableTags = ref([]);
|
const availableTags = ref([]);
|
||||||
const folderTree = ref([]);
|
const folderTree = ref([]);
|
||||||
@@ -11745,6 +12019,11 @@ function useLoraPoolState(widget) {
|
|||||||
license: {
|
license: {
|
||||||
noCreditRequired: noCreditRequired.value,
|
noCreditRequired: noCreditRequired.value,
|
||||||
allowSelling: allowSelling.value
|
allowSelling: allowSelling.value
|
||||||
|
},
|
||||||
|
namePatterns: {
|
||||||
|
include: includePatterns.value,
|
||||||
|
exclude: excludePatterns.value,
|
||||||
|
useRegex: useRegex.value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
preview: {
|
preview: {
|
||||||
@@ -11758,7 +12037,7 @@ function useLoraPoolState(widget) {
|
|||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
const restoreFromConfig = (config) => {
|
const restoreFromConfig = (config) => {
|
||||||
var _a2, _b, _c, _d, _e2, _f;
|
var _a2, _b, _c, _d, _e2, _f, _g, _h, _i;
|
||||||
isRestoring = true;
|
isRestoring = true;
|
||||||
try {
|
try {
|
||||||
if (!(config == null ? void 0 : config.filters)) return;
|
if (!(config == null ? void 0 : config.filters)) return;
|
||||||
@@ -11775,6 +12054,9 @@ function useLoraPoolState(widget) {
|
|||||||
updateIfChanged(excludeFolders, ((_d = filters.folders) == null ? void 0 : _d.exclude) || []);
|
updateIfChanged(excludeFolders, ((_d = filters.folders) == null ? void 0 : _d.exclude) || []);
|
||||||
updateIfChanged(noCreditRequired, ((_e2 = filters.license) == null ? void 0 : _e2.noCreditRequired) ?? false);
|
updateIfChanged(noCreditRequired, ((_e2 = filters.license) == null ? void 0 : _e2.noCreditRequired) ?? false);
|
||||||
updateIfChanged(allowSelling, ((_f = filters.license) == null ? void 0 : _f.allowSelling) ?? false);
|
updateIfChanged(allowSelling, ((_f = filters.license) == null ? void 0 : _f.allowSelling) ?? false);
|
||||||
|
updateIfChanged(includePatterns, ((_g = filters.namePatterns) == null ? void 0 : _g.include) || []);
|
||||||
|
updateIfChanged(excludePatterns, ((_h = filters.namePatterns) == null ? void 0 : _h.exclude) || []);
|
||||||
|
updateIfChanged(useRegex, ((_i = filters.namePatterns) == null ? void 0 : _i.useRegex) ?? false);
|
||||||
matchCount.value = (preview == null ? void 0 : preview.matchCount) || 0;
|
matchCount.value = (preview == null ? void 0 : preview.matchCount) || 0;
|
||||||
} finally {
|
} finally {
|
||||||
isRestoring = false;
|
isRestoring = false;
|
||||||
@@ -11799,6 +12081,9 @@ function useLoraPoolState(widget) {
|
|||||||
foldersExclude: excludeFolders.value,
|
foldersExclude: excludeFolders.value,
|
||||||
noCreditRequired: noCreditRequired.value || void 0,
|
noCreditRequired: noCreditRequired.value || void 0,
|
||||||
allowSelling: allowSelling.value || void 0,
|
allowSelling: allowSelling.value || void 0,
|
||||||
|
namePatternsInclude: includePatterns.value,
|
||||||
|
namePatternsExclude: excludePatterns.value,
|
||||||
|
namePatternsUseRegex: useRegex.value,
|
||||||
pageSize: 6
|
pageSize: 6
|
||||||
});
|
});
|
||||||
previewItems.value = result.items;
|
previewItems.value = result.items;
|
||||||
@@ -11819,7 +12104,10 @@ function useLoraPoolState(widget) {
|
|||||||
includeFolders,
|
includeFolders,
|
||||||
excludeFolders,
|
excludeFolders,
|
||||||
noCreditRequired,
|
noCreditRequired,
|
||||||
allowSelling
|
allowSelling,
|
||||||
|
includePatterns,
|
||||||
|
excludePatterns,
|
||||||
|
useRegex
|
||||||
], onFilterChange, { deep: true });
|
], onFilterChange, { deep: true });
|
||||||
return {
|
return {
|
||||||
// Filter state
|
// Filter state
|
||||||
@@ -11830,6 +12118,9 @@ function useLoraPoolState(widget) {
|
|||||||
excludeFolders,
|
excludeFolders,
|
||||||
noCreditRequired,
|
noCreditRequired,
|
||||||
allowSelling,
|
allowSelling,
|
||||||
|
includePatterns,
|
||||||
|
excludePatterns,
|
||||||
|
useRegex,
|
||||||
// Available options
|
// Available options
|
||||||
availableBaseModels,
|
availableBaseModels,
|
||||||
availableTags,
|
availableTags,
|
||||||
@@ -11902,6 +12193,9 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
|||||||
"exclude-tags": unref(state).excludeTags.value,
|
"exclude-tags": unref(state).excludeTags.value,
|
||||||
"include-folders": unref(state).includeFolders.value,
|
"include-folders": unref(state).includeFolders.value,
|
||||||
"exclude-folders": unref(state).excludeFolders.value,
|
"exclude-folders": unref(state).excludeFolders.value,
|
||||||
|
"include-patterns": unref(state).includePatterns.value,
|
||||||
|
"exclude-patterns": unref(state).excludePatterns.value,
|
||||||
|
"use-regex": unref(state).useRegex.value,
|
||||||
"no-credit-required": unref(state).noCreditRequired.value,
|
"no-credit-required": unref(state).noCreditRequired.value,
|
||||||
"allow-selling": unref(state).allowSelling.value,
|
"allow-selling": unref(state).allowSelling.value,
|
||||||
"preview-items": unref(state).previewItems.value,
|
"preview-items": unref(state).previewItems.value,
|
||||||
@@ -11910,16 +12204,19 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
|||||||
onOpenModal: openModal,
|
onOpenModal: openModal,
|
||||||
"onUpdate:includeFolders": _cache[0] || (_cache[0] = ($event) => unref(state).includeFolders.value = $event),
|
"onUpdate:includeFolders": _cache[0] || (_cache[0] = ($event) => unref(state).includeFolders.value = $event),
|
||||||
"onUpdate:excludeFolders": _cache[1] || (_cache[1] = ($event) => unref(state).excludeFolders.value = $event),
|
"onUpdate:excludeFolders": _cache[1] || (_cache[1] = ($event) => unref(state).excludeFolders.value = $event),
|
||||||
"onUpdate:noCreditRequired": _cache[2] || (_cache[2] = ($event) => unref(state).noCreditRequired.value = $event),
|
"onUpdate:includePatterns": _cache[2] || (_cache[2] = ($event) => unref(state).includePatterns.value = $event),
|
||||||
"onUpdate:allowSelling": _cache[3] || (_cache[3] = ($event) => unref(state).allowSelling.value = $event),
|
"onUpdate:excludePatterns": _cache[3] || (_cache[3] = ($event) => unref(state).excludePatterns.value = $event),
|
||||||
|
"onUpdate:useRegex": _cache[4] || (_cache[4] = ($event) => unref(state).useRegex.value = $event),
|
||||||
|
"onUpdate:noCreditRequired": _cache[5] || (_cache[5] = ($event) => unref(state).noCreditRequired.value = $event),
|
||||||
|
"onUpdate:allowSelling": _cache[6] || (_cache[6] = ($event) => unref(state).allowSelling.value = $event),
|
||||||
onRefresh: unref(state).refreshPreview
|
onRefresh: unref(state).refreshPreview
|
||||||
}, null, 8, ["selected-base-models", "available-base-models", "include-tags", "exclude-tags", "include-folders", "exclude-folders", "no-credit-required", "allow-selling", "preview-items", "match-count", "is-loading", "onRefresh"]),
|
}, null, 8, ["selected-base-models", "available-base-models", "include-tags", "exclude-tags", "include-folders", "exclude-folders", "include-patterns", "exclude-patterns", "use-regex", "no-credit-required", "allow-selling", "preview-items", "match-count", "is-loading", "onRefresh"]),
|
||||||
createVNode(BaseModelModal, {
|
createVNode(BaseModelModal, {
|
||||||
visible: unref(modalState).isModalOpen("baseModels"),
|
visible: unref(modalState).isModalOpen("baseModels"),
|
||||||
models: unref(state).availableBaseModels.value,
|
models: unref(state).availableBaseModels.value,
|
||||||
selected: unref(state).selectedBaseModels.value,
|
selected: unref(state).selectedBaseModels.value,
|
||||||
onClose: unref(modalState).closeModal,
|
onClose: unref(modalState).closeModal,
|
||||||
"onUpdate:selected": _cache[4] || (_cache[4] = ($event) => unref(state).selectedBaseModels.value = $event)
|
"onUpdate:selected": _cache[7] || (_cache[7] = ($event) => unref(state).selectedBaseModels.value = $event)
|
||||||
}, null, 8, ["visible", "models", "selected", "onClose"]),
|
}, null, 8, ["visible", "models", "selected", "onClose"]),
|
||||||
createVNode(TagsModal, {
|
createVNode(TagsModal, {
|
||||||
visible: unref(modalState).isModalOpen("includeTags"),
|
visible: unref(modalState).isModalOpen("includeTags"),
|
||||||
@@ -11927,7 +12224,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
|||||||
selected: unref(state).includeTags.value,
|
selected: unref(state).includeTags.value,
|
||||||
variant: "include",
|
variant: "include",
|
||||||
onClose: unref(modalState).closeModal,
|
onClose: unref(modalState).closeModal,
|
||||||
"onUpdate:selected": _cache[5] || (_cache[5] = ($event) => unref(state).includeTags.value = $event)
|
"onUpdate:selected": _cache[8] || (_cache[8] = ($event) => unref(state).includeTags.value = $event)
|
||||||
}, null, 8, ["visible", "tags", "selected", "onClose"]),
|
}, null, 8, ["visible", "tags", "selected", "onClose"]),
|
||||||
createVNode(TagsModal, {
|
createVNode(TagsModal, {
|
||||||
visible: unref(modalState).isModalOpen("excludeTags"),
|
visible: unref(modalState).isModalOpen("excludeTags"),
|
||||||
@@ -11935,7 +12232,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
|||||||
selected: unref(state).excludeTags.value,
|
selected: unref(state).excludeTags.value,
|
||||||
variant: "exclude",
|
variant: "exclude",
|
||||||
onClose: unref(modalState).closeModal,
|
onClose: unref(modalState).closeModal,
|
||||||
"onUpdate:selected": _cache[6] || (_cache[6] = ($event) => unref(state).excludeTags.value = $event)
|
"onUpdate:selected": _cache[9] || (_cache[9] = ($event) => unref(state).excludeTags.value = $event)
|
||||||
}, null, 8, ["visible", "tags", "selected", "onClose"]),
|
}, null, 8, ["visible", "tags", "selected", "onClose"]),
|
||||||
createVNode(FoldersModal, {
|
createVNode(FoldersModal, {
|
||||||
visible: unref(modalState).isModalOpen("includeFolders"),
|
visible: unref(modalState).isModalOpen("includeFolders"),
|
||||||
@@ -11943,7 +12240,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
|||||||
selected: unref(state).includeFolders.value,
|
selected: unref(state).includeFolders.value,
|
||||||
variant: "include",
|
variant: "include",
|
||||||
onClose: unref(modalState).closeModal,
|
onClose: unref(modalState).closeModal,
|
||||||
"onUpdate:selected": _cache[7] || (_cache[7] = ($event) => unref(state).includeFolders.value = $event)
|
"onUpdate:selected": _cache[10] || (_cache[10] = ($event) => unref(state).includeFolders.value = $event)
|
||||||
}, null, 8, ["visible", "folders", "selected", "onClose"]),
|
}, null, 8, ["visible", "folders", "selected", "onClose"]),
|
||||||
createVNode(FoldersModal, {
|
createVNode(FoldersModal, {
|
||||||
visible: unref(modalState).isModalOpen("excludeFolders"),
|
visible: unref(modalState).isModalOpen("excludeFolders"),
|
||||||
@@ -11951,13 +12248,13 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
|||||||
selected: unref(state).excludeFolders.value,
|
selected: unref(state).excludeFolders.value,
|
||||||
variant: "exclude",
|
variant: "exclude",
|
||||||
onClose: unref(modalState).closeModal,
|
onClose: unref(modalState).closeModal,
|
||||||
"onUpdate:selected": _cache[8] || (_cache[8] = ($event) => unref(state).excludeFolders.value = $event)
|
"onUpdate:selected": _cache[11] || (_cache[11] = ($event) => unref(state).excludeFolders.value = $event)
|
||||||
}, null, 8, ["visible", "folders", "selected", "onClose"])
|
}, null, 8, ["visible", "folders", "selected", "onClose"])
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const LoraPoolWidget = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["__scopeId", "data-v-4456abba"]]);
|
const LoraPoolWidget = /* @__PURE__ */ _export_sfc(_sfc_main$a, [["__scopeId", "data-v-0bbd50ea"]]);
|
||||||
const _hoisted_1$9 = { class: "last-used-preview" };
|
const _hoisted_1$9 = { class: "last-used-preview" };
|
||||||
const _hoisted_2$6 = { class: "last-used-preview__content" };
|
const _hoisted_2$6 = { class: "last-used-preview__content" };
|
||||||
const _hoisted_3$5 = ["src", "onError"];
|
const _hoisted_3$5 = ["src", "onError"];
|
||||||
@@ -14720,7 +15017,7 @@ function updateDownstreamLoaders(startNode, visited = /* @__PURE__ */ new Set())
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const LORA_POOL_WIDGET_MIN_WIDTH = 500;
|
const LORA_POOL_WIDGET_MIN_WIDTH = 500;
|
||||||
const LORA_POOL_WIDGET_MIN_HEIGHT = 400;
|
const LORA_POOL_WIDGET_MIN_HEIGHT = 520;
|
||||||
const LORA_RANDOMIZER_WIDGET_MIN_WIDTH = 500;
|
const LORA_RANDOMIZER_WIDGET_MIN_WIDTH = 500;
|
||||||
const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448;
|
const LORA_RANDOMIZER_WIDGET_MIN_HEIGHT = 448;
|
||||||
const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT;
|
const LORA_RANDOMIZER_WIDGET_MAX_HEIGHT = LORA_RANDOMIZER_WIDGET_MIN_HEIGHT;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user