mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 13:12:12 -03:00
chore(priority-tags): add newline terminator
This commit is contained in:
46
docs/custom_priority_tags_format.md
Normal file
46
docs/custom_priority_tags_format.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Custom Priority Tag Format Proposal
|
||||
|
||||
To support user-defined priority tags with flexible aliasing across different model types, the configuration will be stored as editable strings. The format balances readability with enough structure for parsing on both the backend and frontend.
|
||||
|
||||
## Format Overview
|
||||
|
||||
- Each model type is declared on its own line: `model_type: entries`.
|
||||
- Entries are comma-separated and ordered by priority from highest to lowest.
|
||||
- An entry may be a single canonical tag (e.g., `realistic`) or a canonical tag with aliases.
|
||||
- Canonical tags define the final folder name that should be used when matching that entry.
|
||||
- Aliases are enclosed in parentheses and separated by `|` (vertical bar).
|
||||
- All matching is case-insensitive; stored canonical names preserve the user-specified casing for folder creation and UI suggestions.
|
||||
|
||||
### Grammar
|
||||
|
||||
```
|
||||
priority-config := model-config { "\n" model-config }
|
||||
model-config := model-type ":" entry-list
|
||||
model-type := <identifier without spaces>
|
||||
entry-list := entry { "," entry }
|
||||
entry := canonical [ "(" alias { "|" alias } ")" ]
|
||||
canonical := <tag text without parentheses or commas>
|
||||
alias := <tag text without parentheses, commas, or pipes>
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```
|
||||
lora: celebrity(celeb|celebrity), stylized, character(char)
|
||||
checkpoint: realistic(realism|realistic), anime(anime-style|toon)
|
||||
embedding: face, celeb(celebrity|celeb)
|
||||
```
|
||||
|
||||
## Parsing Notes
|
||||
|
||||
- Whitespace around separators is ignored to make manual editing more forgiving.
|
||||
- Duplicate canonical tags within the same model type collapse to a single entry; the first definition wins.
|
||||
- Aliases map to their canonical tag. When generating folder names, the canonical form is used.
|
||||
- Tags that do not match any alias or canonical entry fall back to the first tag in the model's tag list, preserving current behavior.
|
||||
|
||||
## Usage
|
||||
|
||||
- **Backend:** Convert each model type's string into an ordered list of canonical tags with alias sets. During path generation, iterate by priority order and match tags against both canonical names and their aliases.
|
||||
- **Frontend:** Surface canonical tags as suggestions, optionally displaying aliases in tooltips or secondary text. Input validation should warn about duplicate aliases within the same model type.
|
||||
|
||||
This format allows users to customize priority tag handling per model type while keeping editing simple and avoiding proliferation of folder names through alias normalization.
|
||||
@@ -32,7 +32,7 @@
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0 Bytes",
|
||||
@@ -199,6 +199,7 @@
|
||||
"videoSettings": "Video-Einstellungen",
|
||||
"layoutSettings": "Layout-Einstellungen",
|
||||
"folderSettings": "Ordner-Einstellungen",
|
||||
"priorityTags": "Priority Tags",
|
||||
"downloadPathTemplates": "Download-Pfad-Vorlagen",
|
||||
"exampleImages": "Beispielbilder",
|
||||
"misc": "Verschiedenes",
|
||||
@@ -253,6 +254,27 @@
|
||||
"defaultEmbeddingRootHelp": "Legen Sie den Standard-Embedding-Stammordner für Downloads, Importe und Verschiebungen fest",
|
||||
"noDefault": "Kein Standard"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "Download-Pfad-Vorlagen",
|
||||
"help": "Konfigurieren Sie Ordnerstrukturen für verschiedene Modelltypen beim Herunterladen von Civitai.",
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
"videoSettings": "Video Settings",
|
||||
"layoutSettings": "Layout Settings",
|
||||
"folderSettings": "Folder Settings",
|
||||
"priorityTags": "Priority Tags",
|
||||
"downloadPathTemplates": "Download Path Templates",
|
||||
"exampleImages": "Example Images",
|
||||
"misc": "Misc.",
|
||||
@@ -253,6 +254,27 @@
|
||||
"defaultEmbeddingRootHelp": "Set the default embedding root directory for downloads, imports and moves",
|
||||
"noDefault": "No Default"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "Download Path Templates",
|
||||
"help": "Configure folder structures for different model types when downloading from Civitai.",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0 Bytes",
|
||||
@@ -199,6 +199,7 @@
|
||||
"videoSettings": "Configuración de video",
|
||||
"layoutSettings": "Configuración de diseño",
|
||||
"folderSettings": "Configuración de carpetas",
|
||||
"priorityTags": "Priority Tags",
|
||||
"downloadPathTemplates": "Plantillas de rutas de descarga",
|
||||
"exampleImages": "Imágenes de ejemplo",
|
||||
"misc": "Varios",
|
||||
@@ -253,6 +254,27 @@
|
||||
"defaultEmbeddingRootHelp": "Establecer el directorio raíz predeterminado de embedding para descargas, importaciones y movimientos",
|
||||
"noDefault": "Sin predeterminado"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "Plantillas de rutas de descarga",
|
||||
"help": "Configurar estructuras de carpetas para diferentes tipos de modelos al descargar de Civitai.",
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0 Octets",
|
||||
@@ -203,7 +203,8 @@
|
||||
"exampleImages": "Images d'exemple",
|
||||
"misc": "Divers",
|
||||
"metadataArchive": "Base de données d'archive des métadonnées",
|
||||
"proxySettings": "Paramètres du proxy"
|
||||
"proxySettings": "Paramètres du proxy",
|
||||
"priorityTags": "Priority Tags"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "Flouter le contenu NSFW",
|
||||
@@ -345,6 +346,27 @@
|
||||
"proxyPassword": "Mot de passe (optionnel)",
|
||||
"proxyPasswordPlaceholder": "mot_de_passe",
|
||||
"proxyPasswordHelp": "Mot de passe pour l'authentification proxy (si nécessaire)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0 בתים",
|
||||
@@ -203,7 +203,8 @@
|
||||
"exampleImages": "תמונות דוגמה",
|
||||
"misc": "שונות",
|
||||
"metadataArchive": "מסד נתונים של ארכיון מטא-דאטה",
|
||||
"proxySettings": "הגדרות פרוקסי"
|
||||
"proxySettings": "הגדרות פרוקסי",
|
||||
"priorityTags": "Priority Tags"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "טשטש תוכן NSFW",
|
||||
@@ -345,6 +346,27 @@
|
||||
"proxyPassword": "סיסמה (אופציונלי)",
|
||||
"proxyPasswordPlaceholder": "password",
|
||||
"proxyPasswordHelp": "סיסמה לאימות מול הפרוקסי (אם נדרש)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -1267,4 +1289,4 @@
|
||||
"learnMore": "LM Civitai Extension Tutorial"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0バイト",
|
||||
@@ -203,7 +203,8 @@
|
||||
"exampleImages": "例画像",
|
||||
"misc": "その他",
|
||||
"metadataArchive": "メタデータアーカイブデータベース",
|
||||
"proxySettings": "プロキシ設定"
|
||||
"proxySettings": "プロキシ設定",
|
||||
"priorityTags": "Priority Tags"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "NSFWコンテンツをぼかす",
|
||||
@@ -345,6 +346,27 @@
|
||||
"proxyPassword": "パスワード(任意)",
|
||||
"proxyPasswordPlaceholder": "パスワード",
|
||||
"proxyPasswordHelp": "プロキシ認証用のパスワード(必要な場合)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -1267,4 +1289,4 @@
|
||||
"learnMore": "LM Civitai Extension Tutorial"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0 바이트",
|
||||
@@ -203,7 +203,8 @@
|
||||
"exampleImages": "예시 이미지",
|
||||
"misc": "기타",
|
||||
"metadataArchive": "메타데이터 아카이브 데이터베이스",
|
||||
"proxySettings": "프록시 설정"
|
||||
"proxySettings": "프록시 설정",
|
||||
"priorityTags": "Priority Tags"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "NSFW 콘텐츠 블러 처리",
|
||||
@@ -345,6 +346,27 @@
|
||||
"proxyPassword": "비밀번호 (선택사항)",
|
||||
"proxyPasswordPlaceholder": "password",
|
||||
"proxyPasswordHelp": "프록시 인증에 필요한 비밀번호 (필요한 경우)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -1267,4 +1289,4 @@
|
||||
"learnMore": "LM Civitai Extension Tutorial"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0 Байт",
|
||||
@@ -203,7 +203,8 @@
|
||||
"exampleImages": "Примеры изображений",
|
||||
"misc": "Разное",
|
||||
"metadataArchive": "Архив метаданных",
|
||||
"proxySettings": "Настройки прокси"
|
||||
"proxySettings": "Настройки прокси",
|
||||
"priorityTags": "Priority Tags"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "Размывать NSFW контент",
|
||||
@@ -345,6 +346,27 @@
|
||||
"proxyPassword": "Пароль (необязательно)",
|
||||
"proxyPasswordPlaceholder": "пароль",
|
||||
"proxyPasswordHelp": "Пароль для аутентификации на прокси (если требуется)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -1267,4 +1289,4 @@
|
||||
"learnMore": "LM Civitai Extension Tutorial"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,19 +26,13 @@
|
||||
"english": "English",
|
||||
"chinese_simplified": "中文(简体)",
|
||||
"chinese_traditional": "中文(繁体)",
|
||||
"russian": "俄语",
|
||||
"german": "德语",
|
||||
"japanese": "日语",
|
||||
"korean": "韩语",
|
||||
"french": "法语",
|
||||
"spanish": "西班牙语",
|
||||
"Hebrew": "עברית",
|
||||
"russian": "Русский",
|
||||
"german": "Deutsch",
|
||||
"japanese": "日本語",
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español"
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0 字节",
|
||||
@@ -209,7 +203,8 @@
|
||||
"exampleImages": "示例图片",
|
||||
"misc": "其他",
|
||||
"metadataArchive": "元数据归档数据库",
|
||||
"proxySettings": "代理设置"
|
||||
"proxySettings": "代理设置",
|
||||
"priorityTags": "Priority Tags"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "模糊 NSFW 内容",
|
||||
@@ -351,6 +346,27 @@
|
||||
"proxyPassword": "密码 (可选)",
|
||||
"proxyPasswordPlaceholder": "密码",
|
||||
"proxyPasswordHelp": "代理认证的密码 (如果需要)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -1273,4 +1289,4 @@
|
||||
"learnMore": "LM Civitai Extension Tutorial"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"korean": "한국어",
|
||||
"french": "Français",
|
||||
"spanish": "Español",
|
||||
"Hebrew": "עברית"
|
||||
"Hebrew": "עברית"
|
||||
},
|
||||
"fileSize": {
|
||||
"zero": "0 位元組",
|
||||
@@ -203,7 +203,8 @@
|
||||
"exampleImages": "範例圖片",
|
||||
"misc": "其他",
|
||||
"metadataArchive": "中繼資料封存資料庫",
|
||||
"proxySettings": "代理設定"
|
||||
"proxySettings": "代理設定",
|
||||
"priorityTags": "Priority Tags"
|
||||
},
|
||||
"contentFiltering": {
|
||||
"blurNsfwContent": "模糊 NSFW 內容",
|
||||
@@ -345,6 +346,27 @@
|
||||
"proxyPassword": "密碼(選填)",
|
||||
"proxyPasswordPlaceholder": "password",
|
||||
"proxyPasswordHelp": "代理驗證所需的密碼(如有需要)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Priority Tags",
|
||||
"description": "Customize the tag priority order for each model type. The first match becomes the folder name and powers tag suggestions.",
|
||||
"placeholder": "celebrity(celeb|celebrity), stylized, character(char)",
|
||||
"help": "Separate entries with commas. Add aliases inside parentheses using the | symbol.",
|
||||
"aliasHint": "Tip: Keep canonical names short and meaningful. You can use new lines to group related entries.",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA priority",
|
||||
"checkpoint": "Checkpoint priority",
|
||||
"embedding": "Embedding priority"
|
||||
},
|
||||
"saveSuccess": "Priority tags updated.",
|
||||
"saveError": "Failed to update priority tags.",
|
||||
"loadingSuggestions": "Loading suggestions…",
|
||||
"validation": {
|
||||
"missingClosingParen": "Entry {index} is missing a closing parenthesis.",
|
||||
"missingCanonical": "Entry {index} must include a canonical tag name.",
|
||||
"duplicateCanonical": "The canonical tag \"{tag}\" appears more than once.",
|
||||
"unknown": "Invalid priority tag configuration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -1267,4 +1289,4 @@
|
||||
"learnMore": "LM Civitai Extension Tutorial"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ class SettingsHandler:
|
||||
"include_trigger_words",
|
||||
"show_only_sfw",
|
||||
"compact_mode",
|
||||
"priority_tags",
|
||||
)
|
||||
|
||||
_PROXY_KEYS = {"proxy_enabled", "proxy_host", "proxy_port", "proxy_username", "proxy_password", "proxy_type"}
|
||||
@@ -207,6 +208,14 @@ class SettingsHandler:
|
||||
logger.error("Error getting settings: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def get_priority_tags(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
suggestions = self._settings.get_priority_tag_suggestions()
|
||||
return web.json_response({"success": True, "tags": suggestions})
|
||||
except Exception as exc: # pragma: no cover - defensive logging
|
||||
logger.error("Error getting priority tags: %s", exc, exc_info=True)
|
||||
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||
|
||||
async def activate_library(self, request: web.Request) -> web.Response:
|
||||
"""Activate the selected library."""
|
||||
|
||||
@@ -942,6 +951,7 @@ class MiscHandlerSet:
|
||||
"health_check": self.health.health_check,
|
||||
"get_settings": self.settings.get_settings,
|
||||
"update_settings": self.settings.update_settings,
|
||||
"get_priority_tags": self.settings.get_priority_tags,
|
||||
"get_settings_libraries": self.settings.get_libraries,
|
||||
"activate_library": self.settings.activate_library,
|
||||
"update_usage_stats": self.usage_stats.update_usage_stats,
|
||||
|
||||
@@ -22,6 +22,7 @@ class RouteDefinition:
|
||||
MISC_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
||||
RouteDefinition("GET", "/api/lm/settings", "get_settings"),
|
||||
RouteDefinition("POST", "/api/lm/settings", "update_settings"),
|
||||
RouteDefinition("GET", "/api/lm/priority-tags", "get_priority_tags"),
|
||||
RouteDefinition("GET", "/api/lm/settings/libraries", "get_settings_libraries"),
|
||||
RouteDefinition("POST", "/api/lm/settings/libraries/activate", "activate_library"),
|
||||
RouteDefinition("GET", "/api/lm/health-check", "health_check"),
|
||||
|
||||
@@ -6,7 +6,7 @@ import uuid
|
||||
from typing import Dict, List
|
||||
from urllib.parse import urlparse
|
||||
from ..utils.models import LoraMetadata, CheckpointMetadata, EmbeddingMetadata
|
||||
from ..utils.constants import CARD_PREVIEW_WIDTH, VALID_LORA_TYPES, CIVITAI_MODEL_TAGS
|
||||
from ..utils.constants import CARD_PREVIEW_WIDTH, VALID_LORA_TYPES
|
||||
from ..utils.civitai_utils import rewrite_preview_url
|
||||
from ..utils.exif_utils import ExifUtils
|
||||
from ..utils.metadata_manager import MetadataManager
|
||||
@@ -386,18 +386,9 @@ class DownloadManager:
|
||||
|
||||
# Get model tags
|
||||
model_tags = version_info.get('model', {}).get('tags', [])
|
||||
|
||||
# Find the first Civitai model tag that exists in model_tags
|
||||
first_tag = ''
|
||||
for civitai_tag in CIVITAI_MODEL_TAGS:
|
||||
if civitai_tag in model_tags:
|
||||
first_tag = civitai_tag
|
||||
break
|
||||
|
||||
# If no Civitai model tag found, fallback to first tag
|
||||
if not first_tag and model_tags:
|
||||
first_tag = model_tags[0]
|
||||
|
||||
|
||||
first_tag = settings_manager.resolve_priority_tag_for_model(model_tags, model_type)
|
||||
|
||||
# Format the template with available data
|
||||
formatted_path = path_template
|
||||
formatted_path = formatted_path.replace('{base_model}', mapped_base_model)
|
||||
|
||||
@@ -4,9 +4,16 @@ import os
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from threading import Lock
|
||||
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
||||
from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence
|
||||
|
||||
from ..utils.constants import DEFAULT_PRIORITY_TAG_CONFIG
|
||||
from ..utils.settings_paths import ensure_settings_file
|
||||
from ..utils.tag_priorities import (
|
||||
PriorityTagEntry,
|
||||
collect_canonical_tags,
|
||||
parse_priority_tag_string,
|
||||
resolve_priority_tag,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -36,6 +43,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
"card_info_display": "always",
|
||||
"include_trigger_words": False,
|
||||
"compact_mode": False,
|
||||
"priority_tags": DEFAULT_PRIORITY_TAG_CONFIG.copy(),
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +71,12 @@ class SettingsManager:
|
||||
def _ensure_default_settings(self) -> None:
|
||||
"""Ensure all default settings keys exist"""
|
||||
updated = False
|
||||
normalized_priority = self._normalize_priority_tag_config(
|
||||
self.settings.get("priority_tags")
|
||||
)
|
||||
if normalized_priority != self.settings.get("priority_tags"):
|
||||
self.settings["priority_tags"] = normalized_priority
|
||||
updated = True
|
||||
for key, value in self._get_default_settings().items():
|
||||
if key not in self.settings:
|
||||
if isinstance(value, dict):
|
||||
@@ -385,8 +399,56 @@ class SettingsManager:
|
||||
# Ensure nested dicts are independent copies
|
||||
defaults['base_model_path_mappings'] = {}
|
||||
defaults['download_path_templates'] = {}
|
||||
defaults['priority_tags'] = DEFAULT_PRIORITY_TAG_CONFIG.copy()
|
||||
return defaults
|
||||
|
||||
def _normalize_priority_tag_config(self, value: Any) -> Dict[str, str]:
|
||||
normalized: Dict[str, str] = {}
|
||||
if isinstance(value, Mapping):
|
||||
for key, raw in value.items():
|
||||
if not isinstance(key, str) or not isinstance(raw, str):
|
||||
continue
|
||||
normalized[key] = raw.strip()
|
||||
|
||||
for model_type, default_value in DEFAULT_PRIORITY_TAG_CONFIG.items():
|
||||
normalized.setdefault(model_type, default_value)
|
||||
|
||||
return normalized
|
||||
|
||||
def get_priority_tag_config(self) -> Dict[str, str]:
|
||||
stored_value = self.settings.get("priority_tags")
|
||||
normalized = self._normalize_priority_tag_config(stored_value)
|
||||
if normalized != stored_value:
|
||||
self.settings["priority_tags"] = normalized
|
||||
self._save_settings()
|
||||
return normalized.copy()
|
||||
|
||||
def get_priority_tag_entries(self, model_type: str) -> List[PriorityTagEntry]:
|
||||
config = self.get_priority_tag_config()
|
||||
raw_config = config.get(model_type, "")
|
||||
return parse_priority_tag_string(raw_config)
|
||||
|
||||
def resolve_priority_tag_for_model(
|
||||
self, tags: Sequence[str] | Iterable[str], model_type: str
|
||||
) -> str:
|
||||
entries = self.get_priority_tag_entries(model_type)
|
||||
resolved = resolve_priority_tag(tags, entries)
|
||||
if resolved:
|
||||
return resolved
|
||||
|
||||
for tag in tags:
|
||||
if isinstance(tag, str) and tag:
|
||||
return tag
|
||||
return ""
|
||||
|
||||
def get_priority_tag_suggestions(self) -> Dict[str, List[str]]:
|
||||
suggestions: Dict[str, List[str]] = {}
|
||||
config = self.get_priority_tag_config()
|
||||
for model_type, raw_value in config.items():
|
||||
entries = parse_priority_tag_string(raw_value)
|
||||
suggestions[model_type] = collect_canonical_tags(entries)
|
||||
return suggestions
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Get setting value"""
|
||||
return self.settings.get(key, default)
|
||||
|
||||
@@ -64,4 +64,11 @@ CIVITAI_MODEL_TAGS = [
|
||||
'realistic', 'anime', 'toon', 'furry', 'style',
|
||||
'poses', 'background', 'tool', 'vehicle', 'buildings',
|
||||
'objects', 'assets', 'animal', 'action'
|
||||
]
|
||||
]
|
||||
|
||||
# Default priority tag configuration strings for each model type
|
||||
DEFAULT_PRIORITY_TAG_CONFIG = {
|
||||
'lora': ', '.join(CIVITAI_MODEL_TAGS),
|
||||
'checkpoint': ', '.join(CIVITAI_MODEL_TAGS),
|
||||
'embedding': ', '.join(CIVITAI_MODEL_TAGS),
|
||||
}
|
||||
|
||||
104
py/utils/tag_priorities.py
Normal file
104
py/utils/tag_priorities.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Helpers for parsing and resolving priority tag configurations."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Iterable, List, Optional, Sequence, Set
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PriorityTagEntry:
|
||||
"""A parsed priority tag configuration entry."""
|
||||
|
||||
canonical: str
|
||||
aliases: Set[str]
|
||||
|
||||
@property
|
||||
def normalized_aliases(self) -> Set[str]:
|
||||
return {alias.lower() for alias in self.aliases}
|
||||
|
||||
|
||||
def _normalize_alias(alias: str) -> str:
|
||||
return alias.strip()
|
||||
|
||||
|
||||
def parse_priority_tag_string(config: str | None) -> List[PriorityTagEntry]:
|
||||
"""Parse the user-facing priority tag string into structured entries."""
|
||||
|
||||
if not config:
|
||||
return []
|
||||
|
||||
entries: List[PriorityTagEntry] = []
|
||||
seen_canonicals: Set[str] = set()
|
||||
|
||||
for raw_entry in _split_priority_entries(config):
|
||||
canonical, aliases = _parse_priority_entry(raw_entry)
|
||||
if not canonical:
|
||||
continue
|
||||
|
||||
normalized_canonical = canonical.lower()
|
||||
if normalized_canonical in seen_canonicals:
|
||||
# Skip duplicate canonicals while preserving first occurrence priority
|
||||
continue
|
||||
seen_canonicals.add(normalized_canonical)
|
||||
|
||||
alias_set = {canonical, *aliases}
|
||||
cleaned_aliases = {_normalize_alias(alias) for alias in alias_set if _normalize_alias(alias)}
|
||||
if not cleaned_aliases:
|
||||
continue
|
||||
|
||||
entries.append(PriorityTagEntry(canonical=canonical, aliases=cleaned_aliases))
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def _split_priority_entries(config: str) -> List[str]:
|
||||
# Split on commas while respecting that users may add new lines for readability
|
||||
parts = []
|
||||
for chunk in config.split('\n'):
|
||||
parts.extend(chunk.split(','))
|
||||
return [part.strip() for part in parts if part.strip()]
|
||||
|
||||
|
||||
def _parse_priority_entry(entry: str) -> tuple[str, Set[str]]:
|
||||
if '(' in entry and entry.endswith(')'):
|
||||
canonical, raw_aliases = entry.split('(', 1)
|
||||
canonical = canonical.strip()
|
||||
alias_section = raw_aliases[:-1] # drop trailing ')'
|
||||
aliases = {alias.strip() for alias in alias_section.split('|') if alias.strip()}
|
||||
return canonical, aliases
|
||||
|
||||
if '(' in entry and not entry.endswith(')'):
|
||||
# Malformed entry; treat as literal canonical to avoid surprises
|
||||
entry = entry.replace('(', '').replace(')', '')
|
||||
|
||||
canonical = entry.strip()
|
||||
return canonical, set()
|
||||
|
||||
|
||||
def resolve_priority_tag(
|
||||
tags: Sequence[str] | Iterable[str],
|
||||
entries: Sequence[PriorityTagEntry],
|
||||
) -> Optional[str]:
|
||||
"""Resolve the first matching canonical priority tag for the provided tags."""
|
||||
|
||||
tag_lookup: Dict[str, str] = {}
|
||||
for tag in tags:
|
||||
if not isinstance(tag, str):
|
||||
continue
|
||||
normalized = tag.lower()
|
||||
if normalized not in tag_lookup:
|
||||
tag_lookup[normalized] = tag
|
||||
|
||||
for entry in entries:
|
||||
for alias in entry.normalized_aliases:
|
||||
if alias in tag_lookup:
|
||||
return entry.canonical
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def collect_canonical_tags(entries: Iterable[PriorityTagEntry]) -> List[str]:
|
||||
"""Return the ordered list of canonical tags from the parsed entries."""
|
||||
|
||||
return [entry.canonical for entry in entries]
|
||||
@@ -4,7 +4,6 @@ from typing import Dict
|
||||
from ..services.service_registry import ServiceRegistry
|
||||
from ..config import config
|
||||
from ..services.settings_manager import get_settings_manager
|
||||
from .constants import CIVITAI_MODEL_TAGS
|
||||
import asyncio
|
||||
|
||||
def get_lora_info(lora_name):
|
||||
@@ -170,16 +169,7 @@ def calculate_relative_path_for_model(model_data: Dict, model_type: str = 'lora'
|
||||
base_model_mappings = settings_manager.get('base_model_path_mappings', {})
|
||||
mapped_base_model = base_model_mappings.get(base_model, base_model)
|
||||
|
||||
# Find the first Civitai model tag that exists in model_tags
|
||||
first_tag = ''
|
||||
for civitai_tag in CIVITAI_MODEL_TAGS:
|
||||
if civitai_tag in model_tags:
|
||||
first_tag = civitai_tag
|
||||
break
|
||||
|
||||
# If no Civitai model tag found, fallback to first tag
|
||||
if not first_tag and model_tags:
|
||||
first_tag = model_tags[0]
|
||||
first_tag = settings_manager.resolve_priority_tag_for_model(model_tags, model_type)
|
||||
|
||||
if not first_tag:
|
||||
first_tag = 'no tags' # Default if no tags available
|
||||
|
||||
@@ -204,6 +204,71 @@
|
||||
width: 100%; /* Full width */
|
||||
}
|
||||
|
||||
.settings-help-text {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-color);
|
||||
opacity: 0.8;
|
||||
margin-bottom: var(--space-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.settings-help-text.subtle {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
margin-top: var(--space-1);
|
||||
}
|
||||
|
||||
.priority-tags-grid {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.priority-tags-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
.priority-tags-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.priority-tags-input {
|
||||
width: 100%;
|
||||
min-height: 72px;
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.priority-tags-input:focus {
|
||||
border-color: var(--lora-accent);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1);
|
||||
}
|
||||
|
||||
.priority-tags-input.input-error {
|
||||
border-color: var(--danger-color, #dc2626);
|
||||
box-shadow: 0 0 0 2px rgba(220, 38, 38, 0.12);
|
||||
}
|
||||
|
||||
.input-error-message {
|
||||
font-size: 0.8em;
|
||||
color: var(--danger-color, #dc2626);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.metadata-suggestions-loading {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
/* Settings Styles */
|
||||
.settings-section {
|
||||
margin-top: var(--space-3);
|
||||
|
||||
@@ -4,7 +4,47 @@
|
||||
*/
|
||||
import { showToast } from '../../utils/uiHelpers.js';
|
||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||
import { PRESET_TAGS } from '../../utils/constants.js';
|
||||
import { translate } from '../../utils/i18nHelpers.js';
|
||||
import { getPriorityTagSuggestions } from '../../utils/priorityTagHelpers.js';
|
||||
|
||||
let priorityTagSuggestions = [];
|
||||
let priorityTagSuggestionsLoaded = false;
|
||||
let priorityTagSuggestionsPromise = null;
|
||||
|
||||
function ensurePriorityTagSuggestions() {
|
||||
if (!priorityTagSuggestionsPromise) {
|
||||
priorityTagSuggestionsPromise = getPriorityTagSuggestions()
|
||||
.then((tags) => {
|
||||
priorityTagSuggestions = tags;
|
||||
priorityTagSuggestionsLoaded = true;
|
||||
return tags;
|
||||
})
|
||||
.catch(() => {
|
||||
priorityTagSuggestions = [];
|
||||
priorityTagSuggestionsLoaded = true;
|
||||
return priorityTagSuggestions;
|
||||
})
|
||||
.finally(() => {
|
||||
priorityTagSuggestionsPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return priorityTagSuggestionsLoaded && !priorityTagSuggestionsPromise
|
||||
? Promise.resolve(priorityTagSuggestions)
|
||||
: priorityTagSuggestionsPromise;
|
||||
}
|
||||
|
||||
ensurePriorityTagSuggestions();
|
||||
|
||||
window.addEventListener('lm:priority-tags-updated', () => {
|
||||
priorityTagSuggestionsLoaded = false;
|
||||
ensurePriorityTagSuggestions().then(() => {
|
||||
document.querySelectorAll('.metadata-edit-container .metadata-suggestions-container').forEach((container) => {
|
||||
renderPriorityTagSuggestions(container, getCurrentEditTags());
|
||||
});
|
||||
updateSuggestionsDropdown();
|
||||
});
|
||||
});
|
||||
|
||||
// Create a named function so we can remove it later
|
||||
let saveTagsHandler = null;
|
||||
@@ -260,7 +300,7 @@ function createTagEditUI(currentTags, editBtnHTML = '') {
|
||||
function createSuggestionsDropdown(existingTags = []) {
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'metadata-suggestions-dropdown';
|
||||
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'metadata-suggestions-header';
|
||||
@@ -273,11 +313,33 @@ function createSuggestionsDropdown(existingTags = []) {
|
||||
// Create tag container
|
||||
const container = document.createElement('div');
|
||||
container.className = 'metadata-suggestions-container';
|
||||
|
||||
// Add each preset tag as a suggestion
|
||||
PRESET_TAGS.forEach(tag => {
|
||||
if (priorityTagSuggestionsLoaded && !priorityTagSuggestionsPromise) {
|
||||
renderPriorityTagSuggestions(container, existingTags);
|
||||
} else {
|
||||
container.innerHTML = `<div class="metadata-suggestions-loading">${translate('settings.priorityTags.loadingSuggestions', 'Loading suggestions…')}</div>`;
|
||||
ensurePriorityTagSuggestions().then(() => {
|
||||
if (!container.isConnected) {
|
||||
return;
|
||||
}
|
||||
renderPriorityTagSuggestions(container, getCurrentEditTags());
|
||||
updateSuggestionsDropdown();
|
||||
}).catch(() => {
|
||||
if (container.isConnected) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
function renderPriorityTagSuggestions(container, existingTags = []) {
|
||||
container.innerHTML = '';
|
||||
|
||||
priorityTagSuggestions.forEach((tag) => {
|
||||
const isAdded = existingTags.includes(tag);
|
||||
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = `metadata-suggestion-item ${isAdded ? 'already-added' : ''}`;
|
||||
item.title = tag;
|
||||
@@ -285,28 +347,21 @@ function createSuggestionsDropdown(existingTags = []) {
|
||||
<span class="metadata-suggestion-text">${tag}</span>
|
||||
${isAdded ? '<span class="added-indicator"><i class="fas fa-check"></i></span>' : ''}
|
||||
`;
|
||||
|
||||
|
||||
if (!isAdded) {
|
||||
item.addEventListener('click', () => {
|
||||
addNewTag(tag);
|
||||
|
||||
// Also populate the input field for potential editing
|
||||
|
||||
const input = document.querySelector('.metadata-input');
|
||||
if (input) input.value = tag;
|
||||
|
||||
// Focus on the input
|
||||
if (input) input.focus();
|
||||
|
||||
// Update dropdown without removing it
|
||||
|
||||
updateSuggestionsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -406,10 +461,9 @@ function addNewTag(tag) {
|
||||
function updateSuggestionsDropdown() {
|
||||
const dropdown = document.querySelector('.metadata-suggestions-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
|
||||
// Get all current tags
|
||||
const currentTags = document.querySelectorAll('.metadata-item');
|
||||
const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag);
|
||||
const existingTags = getCurrentEditTags();
|
||||
|
||||
// Update status of each item in dropdown
|
||||
dropdown.querySelectorAll('.metadata-suggestion-item').forEach(item => {
|
||||
@@ -456,6 +510,11 @@ function updateSuggestionsDropdown() {
|
||||
});
|
||||
}
|
||||
|
||||
function getCurrentEditTags() {
|
||||
const currentTags = document.querySelectorAll('.metadata-item');
|
||||
return Array.from(currentTags).map(tag => tag.dataset.tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore original tags when canceling edit
|
||||
* @param {HTMLElement} section - The tags section
|
||||
|
||||
@@ -4,7 +4,8 @@ import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||
import { PRESET_TAGS, BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
|
||||
import { eventManager } from '../utils/EventManager.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
|
||||
@@ -59,6 +60,22 @@ export class BulkManager {
|
||||
setContentRating: true
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('lm:priority-tags-updated', () => {
|
||||
const container = document.querySelector('#bulkAddTagsModal .metadata-suggestions-container');
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
getPriorityTagSuggestions().then((tags) => {
|
||||
if (!container.isConnected) {
|
||||
return;
|
||||
}
|
||||
this.renderBulkSuggestionItems(container, tags);
|
||||
this.updateBulkSuggestionsDropdown();
|
||||
}).catch(() => {
|
||||
// Ignore refresh failures; UI will retry on next open
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initialize() {
|
||||
@@ -565,7 +582,7 @@ export class BulkManager {
|
||||
// Create suggestions dropdown
|
||||
const tagForm = document.querySelector('#bulkAddTagsModal .metadata-add-form');
|
||||
if (tagForm) {
|
||||
const suggestionsDropdown = this.createBulkSuggestionsDropdown(PRESET_TAGS);
|
||||
const suggestionsDropdown = this.createBulkSuggestionsDropdown();
|
||||
tagForm.appendChild(suggestionsDropdown);
|
||||
}
|
||||
|
||||
@@ -586,10 +603,10 @@ export class BulkManager {
|
||||
}
|
||||
}
|
||||
|
||||
createBulkSuggestionsDropdown(presetTags) {
|
||||
createBulkSuggestionsDropdown() {
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'metadata-suggestions-dropdown';
|
||||
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'metadata-suggestions-header';
|
||||
header.innerHTML = `
|
||||
@@ -597,15 +614,34 @@ export class BulkManager {
|
||||
<small>Click to add</small>
|
||||
`;
|
||||
dropdown.appendChild(header);
|
||||
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'metadata-suggestions-container';
|
||||
|
||||
presetTags.forEach(tag => {
|
||||
// Check if tag is already added
|
||||
container.innerHTML = `<div class="metadata-suggestions-loading">${translate('settings.priorityTags.loadingSuggestions', 'Loading suggestions…')}</div>`;
|
||||
|
||||
getPriorityTagSuggestions().then((tags) => {
|
||||
if (!container.isConnected) {
|
||||
return;
|
||||
}
|
||||
this.renderBulkSuggestionItems(container, tags);
|
||||
this.updateBulkSuggestionsDropdown();
|
||||
}).catch(() => {
|
||||
if (container.isConnected) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
renderBulkSuggestionItems(container, tags) {
|
||||
container.innerHTML = '';
|
||||
|
||||
tags.forEach(tag => {
|
||||
const existingTags = this.getBulkExistingTags();
|
||||
const isAdded = existingTags.includes(tag);
|
||||
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = `metadata-suggestion-item ${isAdded ? 'already-added' : ''}`;
|
||||
item.title = tag;
|
||||
@@ -613,7 +649,7 @@ export class BulkManager {
|
||||
<span class="metadata-suggestion-text">${tag}</span>
|
||||
${isAdded ? '<span class="added-indicator"><i class="fas fa-check"></i></span>' : ''}
|
||||
`;
|
||||
|
||||
|
||||
if (!isAdded) {
|
||||
item.addEventListener('click', () => {
|
||||
this.addBulkTag(tag);
|
||||
@@ -622,16 +658,12 @@ export class BulkManager {
|
||||
input.value = tag;
|
||||
input.focus();
|
||||
}
|
||||
// Update dropdown to show added indicator
|
||||
this.updateBulkSuggestionsDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
container.appendChild(item);
|
||||
});
|
||||
|
||||
dropdown.appendChild(container);
|
||||
return dropdown;
|
||||
}
|
||||
|
||||
addBulkTag(tag) {
|
||||
|
||||
@@ -2,10 +2,11 @@ import { modalManager } from './ModalManager.js';
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
import { state, createDefaultSettings } from '../state/index.js';
|
||||
import { resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
||||
import { DOWNLOAD_PATH_TEMPLATES, MAPPABLE_BASE_MODELS, PATH_TEMPLATE_PLACEHOLDERS, DEFAULT_PATH_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/constants.js';
|
||||
import { translate } from '../utils/i18nHelpers.js';
|
||||
import { i18n } from '../i18n/index.js';
|
||||
import { configureModelCardVideo } from '../components/shared/ModelCard.js';
|
||||
import { validatePriorityTagString, getPriorityTagSuggestionsMap, invalidatePriorityTagSuggestionsCache } from '../utils/priorityTagHelpers.js';
|
||||
|
||||
export class SettingsManager {
|
||||
constructor() {
|
||||
@@ -111,6 +112,17 @@ export class SettingsManager {
|
||||
|
||||
merged.download_path_templates = { ...DEFAULT_PATH_TEMPLATES, ...templates };
|
||||
|
||||
const priorityTags = backendSettings?.priority_tags;
|
||||
const normalizedPriority = { ...DEFAULT_PRIORITY_TAG_CONFIG };
|
||||
if (priorityTags && typeof priorityTags === 'object' && !Array.isArray(priorityTags)) {
|
||||
Object.entries(priorityTags).forEach(([modelType, configValue]) => {
|
||||
if (typeof configValue === 'string') {
|
||||
normalizedPriority[modelType] = configValue.trim();
|
||||
}
|
||||
});
|
||||
}
|
||||
merged.priority_tags = normalizedPriority;
|
||||
|
||||
Object.keys(merged).forEach(key => this.backendSettingKeys.add(key));
|
||||
|
||||
return merged;
|
||||
@@ -201,14 +213,14 @@ export class SettingsManager {
|
||||
settingsManager.validateTemplate(modelType, template);
|
||||
settingsManager.updateTemplatePreview(modelType, template);
|
||||
});
|
||||
|
||||
|
||||
customInput.addEventListener('blur', (e) => {
|
||||
const template = e.target.value;
|
||||
if (settingsManager.validateTemplate(modelType, template)) {
|
||||
settingsManager.updateTemplate(modelType, template);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
customInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.target.blur();
|
||||
@@ -216,7 +228,9 @@ export class SettingsManager {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
this.setupPriorityTagInputs();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
@@ -291,6 +305,9 @@ export class SettingsManager {
|
||||
// Load download path templates
|
||||
this.loadDownloadPathTemplates();
|
||||
|
||||
// Load priority tag settings
|
||||
this.loadPriorityTagSettings();
|
||||
|
||||
// Set include trigger words setting
|
||||
const includeTriggerWordsCheckbox = document.getElementById('includeTriggerWords');
|
||||
if (includeTriggerWordsCheckbox) {
|
||||
@@ -325,6 +342,131 @@ export class SettingsManager {
|
||||
this.loadProxySettings();
|
||||
}
|
||||
|
||||
setupPriorityTagInputs() {
|
||||
['lora', 'checkpoint', 'embedding'].forEach((modelType) => {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.addEventListener('input', () => this.handlePriorityTagInput(modelType));
|
||||
textarea.addEventListener('blur', () => this.handlePriorityTagBlur(modelType));
|
||||
});
|
||||
}
|
||||
|
||||
loadPriorityTagSettings() {
|
||||
const priorityConfig = state.global.settings.priority_tags || {};
|
||||
['lora', 'checkpoint', 'embedding'].forEach((modelType) => {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const storedValue = priorityConfig[modelType] ?? DEFAULT_PRIORITY_TAG_CONFIG[modelType] ?? '';
|
||||
textarea.value = storedValue;
|
||||
this.displayPriorityTagValidation(modelType, true, []);
|
||||
});
|
||||
}
|
||||
|
||||
handlePriorityTagInput(modelType) {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = validatePriorityTagString(textarea.value);
|
||||
this.displayPriorityTagValidation(modelType, validation.valid, validation.errors);
|
||||
}
|
||||
|
||||
async handlePriorityTagBlur(modelType) {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = validatePriorityTagString(textarea.value);
|
||||
if (!validation.valid) {
|
||||
this.displayPriorityTagValidation(modelType, false, validation.errors);
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitized = validation.formatted;
|
||||
const currentValue = state.global.settings.priority_tags?.[modelType] || '';
|
||||
this.displayPriorityTagValidation(modelType, true, []);
|
||||
|
||||
if (sanitized === currentValue) {
|
||||
textarea.value = sanitized;
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedConfig = {
|
||||
...state.global.settings.priority_tags,
|
||||
[modelType]: sanitized,
|
||||
};
|
||||
|
||||
try {
|
||||
textarea.value = sanitized;
|
||||
await this.saveSetting('priority_tags', updatedConfig);
|
||||
showToast('settings.priorityTags.saveSuccess', {}, 'success');
|
||||
await this.refreshPriorityTagSuggestions();
|
||||
} catch (error) {
|
||||
console.error('Failed to save priority tag configuration:', error);
|
||||
showToast('settings.priorityTags.saveError', {}, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
displayPriorityTagValidation(modelType, isValid, errors = []) {
|
||||
const textarea = document.getElementById(`${modelType}PriorityTagsInput`);
|
||||
const errorElement = document.getElementById(`${modelType}PriorityTagsError`);
|
||||
if (!textarea) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isValid || errors.length === 0) {
|
||||
textarea.classList.remove('input-error');
|
||||
if (errorElement) {
|
||||
errorElement.textContent = '';
|
||||
errorElement.style.display = 'none';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
textarea.classList.add('input-error');
|
||||
if (errorElement) {
|
||||
const message = this.getPriorityTagErrorMessage(errors[0]);
|
||||
errorElement.textContent = message;
|
||||
errorElement.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
getPriorityTagErrorMessage(error) {
|
||||
if (!error) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const entryIndex = error.index ?? 0;
|
||||
switch (error.type) {
|
||||
case 'missingClosingParen':
|
||||
return translate('settings.priorityTags.validation.missingClosingParen', { index: entryIndex }, `Entry ${entryIndex} is missing a closing parenthesis.`);
|
||||
case 'missingCanonical':
|
||||
return translate('settings.priorityTags.validation.missingCanonical', { index: entryIndex }, `Entry ${entryIndex} must include a canonical tag.`);
|
||||
case 'duplicateCanonical':
|
||||
return translate('settings.priorityTags.validation.duplicateCanonical', { tag: error.canonical }, `The canonical tag "${error.canonical}" is duplicated.`);
|
||||
default:
|
||||
return translate('settings.priorityTags.validation.unknown', {}, 'Invalid priority tag configuration.');
|
||||
}
|
||||
}
|
||||
|
||||
async refreshPriorityTagSuggestions() {
|
||||
invalidatePriorityTagSuggestionsCache();
|
||||
try {
|
||||
await getPriorityTagSuggestionsMap();
|
||||
window.dispatchEvent(new CustomEvent('lm:priority-tags-updated'));
|
||||
} catch (error) {
|
||||
console.warn('Failed to refresh priority tag suggestions:', error);
|
||||
}
|
||||
}
|
||||
|
||||
loadProxySettings() {
|
||||
// Load proxy enabled setting
|
||||
const proxyEnabledCheckbox = document.getElementById('proxyEnabled');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Create the new hierarchical state structure
|
||||
import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js';
|
||||
import { MODEL_TYPES } from '../api/apiConfig.js';
|
||||
import { DEFAULT_PATH_TEMPLATES } from '../utils/constants.js';
|
||||
import { DEFAULT_PATH_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/constants.js';
|
||||
|
||||
const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
civitai_api_key: '',
|
||||
@@ -28,6 +28,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
card_info_display: 'always',
|
||||
include_trigger_words: false,
|
||||
compact_mode: false,
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
});
|
||||
|
||||
export function createDefaultSettings() {
|
||||
@@ -35,6 +36,7 @@ export function createDefaultSettings() {
|
||||
...DEFAULT_SETTINGS_BASE,
|
||||
base_model_path_mappings: {},
|
||||
download_path_templates: { ...DEFAULT_PATH_TEMPLATES },
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -194,10 +194,16 @@ export const BASE_MODEL_CATEGORIES = {
|
||||
]
|
||||
};
|
||||
|
||||
// Preset tag suggestions
|
||||
export const PRESET_TAGS = [
|
||||
// Default priority tag entries for fallback suggestions and initial settings
|
||||
export const DEFAULT_PRIORITY_TAG_ENTRIES = [
|
||||
'character', 'concept', 'clothing',
|
||||
'realistic', 'anime', 'toon', 'furry', 'style',
|
||||
'poses', 'background', 'vehicle', 'buildings',
|
||||
'objects', 'animal'
|
||||
'poses', 'background', 'tool', 'vehicle', 'buildings',
|
||||
'objects', 'assets', 'animal', 'action'
|
||||
];
|
||||
|
||||
export const DEFAULT_PRIORITY_TAG_CONFIG = {
|
||||
lora: DEFAULT_PRIORITY_TAG_ENTRIES.join(', '),
|
||||
checkpoint: DEFAULT_PRIORITY_TAG_ENTRIES.join(', '),
|
||||
embedding: DEFAULT_PRIORITY_TAG_ENTRIES.join(', ')
|
||||
};
|
||||
|
||||
223
static/js/utils/priorityTagHelpers.js
Normal file
223
static/js/utils/priorityTagHelpers.js
Normal file
@@ -0,0 +1,223 @@
|
||||
import { DEFAULT_PRIORITY_TAG_CONFIG } from './constants.js';
|
||||
|
||||
function splitPriorityEntries(raw = '') {
|
||||
const segments = [];
|
||||
raw.split('\n').forEach(line => {
|
||||
line.split(',').forEach(part => {
|
||||
const trimmed = part.trim();
|
||||
if (trimmed) {
|
||||
segments.push(trimmed);
|
||||
}
|
||||
});
|
||||
});
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function parsePriorityTagString(raw = '') {
|
||||
const entries = [];
|
||||
const rawEntries = splitPriorityEntries(raw);
|
||||
|
||||
rawEntries.forEach((entry) => {
|
||||
const { canonical, aliases } = parsePriorityEntry(entry);
|
||||
if (!canonical) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.push({ canonical, aliases });
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parsePriorityEntry(entry) {
|
||||
let canonical = entry;
|
||||
let aliasSection = '';
|
||||
|
||||
const openIndex = entry.indexOf('(');
|
||||
if (openIndex !== -1) {
|
||||
if (!entry.endsWith(')')) {
|
||||
canonical = entry.replace('(', '').replace(')', '');
|
||||
} else {
|
||||
canonical = entry.slice(0, openIndex).trim();
|
||||
aliasSection = entry.slice(openIndex + 1, -1);
|
||||
}
|
||||
}
|
||||
|
||||
canonical = canonical.trim();
|
||||
if (!canonical) {
|
||||
return { canonical: '', aliases: [] };
|
||||
}
|
||||
|
||||
const aliasList = aliasSection ? aliasSection.split('|').map((alias) => alias.trim()).filter(Boolean) : [];
|
||||
const seen = new Set();
|
||||
const normalizedCanonical = canonical.toLowerCase();
|
||||
const uniqueAliases = [];
|
||||
|
||||
aliasList.forEach((alias) => {
|
||||
const normalized = alias.toLowerCase();
|
||||
if (normalized === normalizedCanonical) {
|
||||
return;
|
||||
}
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
uniqueAliases.push(alias);
|
||||
}
|
||||
});
|
||||
|
||||
return { canonical, aliases: uniqueAliases };
|
||||
}
|
||||
|
||||
export function formatPriorityTagEntries(entries, useNewlines = false) {
|
||||
if (!entries.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const separator = useNewlines ? ',\n' : ', ';
|
||||
return entries.map(({ canonical, aliases }) => {
|
||||
if (aliases && aliases.length) {
|
||||
return `${canonical}(${aliases.join('|')})`;
|
||||
}
|
||||
return canonical;
|
||||
}).join(separator);
|
||||
}
|
||||
|
||||
export function validatePriorityTagString(raw = '') {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return { valid: true, errors: [], entries: [], formatted: '' };
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
const entries = [];
|
||||
const rawEntries = splitPriorityEntries(raw);
|
||||
const seenCanonicals = new Set();
|
||||
|
||||
rawEntries.forEach((entry, index) => {
|
||||
const hasOpening = entry.includes('(');
|
||||
const hasClosing = entry.endsWith(')');
|
||||
|
||||
if (hasOpening && !hasClosing) {
|
||||
errors.push({ type: 'missingClosingParen', index: index + 1 });
|
||||
}
|
||||
|
||||
const { canonical, aliases } = parsePriorityEntry(entry);
|
||||
if (!canonical) {
|
||||
errors.push({ type: 'missingCanonical', index: index + 1 });
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedCanonical = canonical.toLowerCase();
|
||||
if (seenCanonicals.has(normalizedCanonical)) {
|
||||
errors.push({ type: 'duplicateCanonical', canonical });
|
||||
} else {
|
||||
seenCanonicals.add(normalizedCanonical);
|
||||
}
|
||||
|
||||
entries.push({ canonical, aliases });
|
||||
});
|
||||
|
||||
const formatted = errors.length === 0
|
||||
? formatPriorityTagEntries(entries, raw.includes('\n'))
|
||||
: raw.trim();
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
entries,
|
||||
formatted,
|
||||
};
|
||||
}
|
||||
|
||||
let cachedPriorityTagMap = null;
|
||||
let fetchPromise = null;
|
||||
|
||||
export async function getPriorityTagSuggestionsMap() {
|
||||
if (cachedPriorityTagMap) {
|
||||
return cachedPriorityTagMap;
|
||||
}
|
||||
|
||||
if (!fetchPromise) {
|
||||
fetchPromise = fetch('/api/lm/priority-tags')
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (!data || data.success === false || typeof data.tags !== 'object') {
|
||||
throw new Error(data?.error || 'Invalid response payload');
|
||||
}
|
||||
|
||||
const normalized = {};
|
||||
Object.entries(data.tags).forEach(([modelType, tags]) => {
|
||||
if (!Array.isArray(tags)) {
|
||||
return;
|
||||
}
|
||||
normalized[modelType] = tags.filter(tag => typeof tag === 'string' && tag.trim());
|
||||
});
|
||||
|
||||
const withDefaults = applyDefaultPriorityTagFallback(normalized);
|
||||
cachedPriorityTagMap = withDefaults;
|
||||
return withDefaults;
|
||||
})
|
||||
.catch(() => {
|
||||
const fallback = buildDefaultPriorityTagMap();
|
||||
cachedPriorityTagMap = fallback;
|
||||
return fallback;
|
||||
})
|
||||
.finally(() => {
|
||||
fetchPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
export async function getPriorityTagSuggestions() {
|
||||
const map = await getPriorityTagSuggestionsMap();
|
||||
const unique = new Set();
|
||||
Object.values(map).forEach((tags) => {
|
||||
tags.forEach((tag) => {
|
||||
unique.add(tag);
|
||||
});
|
||||
});
|
||||
return Array.from(unique);
|
||||
}
|
||||
|
||||
function applyDefaultPriorityTagFallback(map) {
|
||||
const result = { ...buildDefaultPriorityTagMap(), ...map };
|
||||
Object.entries(result).forEach(([key, tags]) => {
|
||||
result[key] = dedupeTags(Array.isArray(tags) ? tags : []);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildDefaultPriorityTagMap() {
|
||||
const map = {};
|
||||
Object.entries(DEFAULT_PRIORITY_TAG_CONFIG).forEach(([modelType, configString]) => {
|
||||
const entries = parsePriorityTagString(configString);
|
||||
map[modelType] = entries.map((entry) => entry.canonical);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function dedupeTags(tags) {
|
||||
const seen = new Set();
|
||||
const ordered = [];
|
||||
tags.forEach((tag) => {
|
||||
const normalized = tag.toLowerCase();
|
||||
if (!seen.has(normalized)) {
|
||||
seen.add(normalized);
|
||||
ordered.push(tag);
|
||||
}
|
||||
});
|
||||
return ordered;
|
||||
}
|
||||
|
||||
export function getDefaultPriorityTagConfig() {
|
||||
return { ...DEFAULT_PRIORITY_TAG_CONFIG };
|
||||
}
|
||||
|
||||
export function invalidatePriorityTagSuggestionsCache() {
|
||||
cachedPriorityTagMap = null;
|
||||
fetchPromise = null;
|
||||
}
|
||||
@@ -250,6 +250,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-section">
|
||||
<h3>{{ t('settings.priorityTags.title') }}</h3>
|
||||
<p class="settings-help-text">{{ t('settings.priorityTags.description') }}</p>
|
||||
<div class="priority-tags-grid">
|
||||
<div class="priority-tags-group">
|
||||
<label for="loraPriorityTagsInput">{{ t('settings.priorityTags.modelTypes.lora') }}</label>
|
||||
<textarea id="loraPriorityTagsInput" class="priority-tags-input" rows="3" placeholder="{{ t('settings.priorityTags.placeholder') }}"></textarea>
|
||||
<div class="input-help">{{ t('settings.priorityTags.help') }}</div>
|
||||
<div class="input-error-message" id="loraPriorityTagsError"></div>
|
||||
</div>
|
||||
<div class="priority-tags-group">
|
||||
<label for="checkpointPriorityTagsInput">{{ t('settings.priorityTags.modelTypes.checkpoint') }}</label>
|
||||
<textarea id="checkpointPriorityTagsInput" class="priority-tags-input" rows="3" placeholder="{{ t('settings.priorityTags.placeholder') }}"></textarea>
|
||||
<div class="input-help">{{ t('settings.priorityTags.help') }}</div>
|
||||
<div class="input-error-message" id="checkpointPriorityTagsError"></div>
|
||||
</div>
|
||||
<div class="priority-tags-group">
|
||||
<label for="embeddingPriorityTagsInput">{{ t('settings.priorityTags.modelTypes.embedding') }}</label>
|
||||
<textarea id="embeddingPriorityTagsInput" class="priority-tags-input" rows="3" placeholder="{{ t('settings.priorityTags.placeholder') }}"></textarea>
|
||||
<div class="input-help">{{ t('settings.priorityTags.help') }}</div>
|
||||
<div class="input-error-message" id="embeddingPriorityTagsError"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="settings-help-text subtle">{{ t('settings.priorityTags.aliasHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Default Path Customization Section -->
|
||||
<div class="settings-section">
|
||||
<h3>{{ t('settings.downloadPathTemplates.title') }}</h3>
|
||||
|
||||
Reference in New Issue
Block a user