mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
Add update flag strategy
This commit is contained in:
@@ -230,6 +230,7 @@
|
||||
"priorityTags": "Prioritäts-Tags",
|
||||
"downloadPathTemplates": "Download-Pfad-Vorlagen",
|
||||
"exampleImages": "Beispielbilder",
|
||||
"updateFlags": "Update-Markierungen",
|
||||
"misc": "Verschiedenes",
|
||||
"metadataArchive": "Metadaten-Archiv-Datenbank",
|
||||
"storageLocation": "Einstellungsort",
|
||||
@@ -271,7 +272,6 @@
|
||||
"hover": "Bei Hover anzeigen"
|
||||
},
|
||||
"cardInfoDisplayHelp": "Wählen Sie, wann Modellinformationen und Aktionsschaltflächen angezeigt werden sollen",
|
||||
|
||||
"modelCardFooterAction": "Aktion der Modellkarten-Schaltfläche",
|
||||
"modelCardFooterActionOptions": {
|
||||
"exampleImages": "Beispielbilder öffnen",
|
||||
@@ -365,6 +365,14 @@
|
||||
"download": "Herunterladen",
|
||||
"restartRequired": "Neustart erforderlich"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "Strategie für Update-Markierungen",
|
||||
"help": "Entscheide, ob Update-Badges nur dann erscheinen, wenn eine neue Version dasselbe Basismodell wie deine lokalen Dateien verwendet, oder sobald es irgendein neueres Release für dieses Modell gibt.",
|
||||
"options": {
|
||||
"sameBase": "Updates nach Basismodell abgleichen",
|
||||
"any": "Jede verfügbare Aktualisierung markieren"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen",
|
||||
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen"
|
||||
@@ -930,6 +938,18 @@
|
||||
"viewLocalVersions": "Alle lokalen Versionen anzeigen",
|
||||
"viewLocalTooltip": "Demnächst verfügbar"
|
||||
},
|
||||
"filters": {
|
||||
"label": "Basisfilter",
|
||||
"state": {
|
||||
"showAll": "Alle Versionen",
|
||||
"showSameBase": "Gleiches Basismodell"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "Wechseln, um alle Versionen anzuzeigen",
|
||||
"showSameBaseVersions": "Wechseln, um nur Versionen mit demselben Basismodell anzuzeigen"
|
||||
},
|
||||
"empty": "Keine Versionen entsprechen dem Filter für das aktuelle Basismodell."
|
||||
},
|
||||
"empty": "Noch keine Versionshistorie für dieses Modell vorhanden.",
|
||||
"error": "Versionen konnten nicht geladen werden.",
|
||||
"missingModelId": "Für dieses Modell ist keine Civitai-Model-ID vorhanden.",
|
||||
|
||||
@@ -230,6 +230,7 @@
|
||||
"priorityTags": "Priority Tags",
|
||||
"downloadPathTemplates": "Download Path Templates",
|
||||
"exampleImages": "Example Images",
|
||||
"updateFlags": "Update Flags",
|
||||
"misc": "Misc.",
|
||||
"metadataArchive": "Metadata Archive Database",
|
||||
"storageLocation": "Settings Location",
|
||||
@@ -364,6 +365,14 @@
|
||||
"download": "Download",
|
||||
"restartRequired": "Requires restart"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "Update Flag Strategy",
|
||||
"help": "Decide whether update badges should only appear when a new release shares the same base model as your local files or whenever any newer version exists for that model.",
|
||||
"options": {
|
||||
"sameBase": "Match updates by base model",
|
||||
"any": "Flag any available update"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Include Trigger Words in LoRA Syntax",
|
||||
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard"
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "View all local versions",
|
||||
"viewLocalTooltip": "Coming soon"
|
||||
},
|
||||
"filters": {
|
||||
"label": "Base filter",
|
||||
"state": {
|
||||
"showAll": "All versions",
|
||||
"showSameBase": "Same base"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "Switch to showing all versions",
|
||||
"showSameBaseVersions": "Switch to showing only versions that match the current base model"
|
||||
},
|
||||
"empty": "No versions match the current base model filter."
|
||||
},
|
||||
"empty": "No version history available for this model yet.",
|
||||
"error": "Failed to load versions.",
|
||||
"missingModelId": "This model is missing a Civitai model id.",
|
||||
|
||||
@@ -230,6 +230,7 @@
|
||||
"priorityTags": "Etiquetas prioritarias",
|
||||
"downloadPathTemplates": "Plantillas de rutas de descarga",
|
||||
"exampleImages": "Imágenes de ejemplo",
|
||||
"updateFlags": "Indicadores de actualización",
|
||||
"misc": "Varios",
|
||||
"metadataArchive": "Base de datos de archivo de metadatos",
|
||||
"storageLocation": "Ubicación de ajustes",
|
||||
@@ -364,6 +365,14 @@
|
||||
"download": "Descargar",
|
||||
"restartRequired": "Requiere reinicio"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "Estrategia de indicadores de actualización",
|
||||
"help": "Decide si las insignias de actualización deben mostrarse solo cuando una nueva versión comparte el mismo modelo base que tus archivos locales o siempre que exista cualquier versión más reciente de ese modelo.",
|
||||
"options": {
|
||||
"sameBase": "Coincidir actualizaciones por modelo base",
|
||||
"any": "Marcar cualquier actualización disponible"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA",
|
||||
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles"
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "Ver todas las versiones locales",
|
||||
"viewLocalTooltip": "Disponible pronto"
|
||||
},
|
||||
"filters": {
|
||||
"label": "Filtro base",
|
||||
"state": {
|
||||
"showAll": "Todas las versiones",
|
||||
"showSameBase": "Mismo modelo base"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "Cambiar para mostrar todas las versiones",
|
||||
"showSameBaseVersions": "Cambiar para mostrar solo versiones del mismo modelo base"
|
||||
},
|
||||
"empty": "Ninguna versión coincide con el filtro del modelo base actual."
|
||||
},
|
||||
"empty": "Aún no hay historial de versiones para este modelo.",
|
||||
"error": "No se pudieron cargar las versiones.",
|
||||
"missingModelId": "Este modelo no tiene un ID de modelo de Civitai.",
|
||||
|
||||
@@ -227,13 +227,14 @@
|
||||
"videoSettings": "Paramètres vidéo",
|
||||
"layoutSettings": "Paramètres d'affichage",
|
||||
"folderSettings": "Paramètres des dossiers",
|
||||
"priorityTags": "Étiquettes prioritaires",
|
||||
"downloadPathTemplates": "Modèles de chemin de téléchargement",
|
||||
"exampleImages": "Images d'exemple",
|
||||
"updateFlags": "Indicateurs de mise à jour",
|
||||
"misc": "Divers",
|
||||
"metadataArchive": "Base de données d'archive des métadonnées",
|
||||
"storageLocation": "Emplacement des paramètres",
|
||||
"proxySettings": "Paramètres du proxy",
|
||||
"priorityTags": "Étiquettes prioritaires"
|
||||
"proxySettings": "Paramètres du proxy"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Mode portable",
|
||||
@@ -297,6 +298,26 @@
|
||||
"defaultEmbeddingRootHelp": "Définir le répertoire racine embedding par défaut pour les téléchargements, imports et déplacements",
|
||||
"noDefault": "Aucun par défaut"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Étiquettes prioritaires",
|
||||
"description": "Personnalisez l'ordre de priorité des étiquettes pour chaque type de modèle (par ex. : character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "Ouvrir l'aide sur les étiquettes prioritaires",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Checkpoint",
|
||||
"embedding": "Embedding"
|
||||
},
|
||||
"saveSuccess": "Étiquettes prioritaires mises à jour.",
|
||||
"saveError": "Échec de la mise à jour des étiquettes prioritaires.",
|
||||
"loadingSuggestions": "Chargement des suggestions...",
|
||||
"validation": {
|
||||
"missingClosingParen": "L'entrée {index} n'a pas de parenthèse fermante.",
|
||||
"missingCanonical": "L'entrée {index} doit inclure un nom d'étiquette canonique.",
|
||||
"duplicateCanonical": "L'étiquette canonique \"{tag}\" apparaît plusieurs fois.",
|
||||
"unknown": "Configuration d'étiquettes prioritaires invalide."
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "Modèles de chemin de téléchargement",
|
||||
"help": "Configurer les structures de dossiers pour différents types de modèles lors du téléchargement depuis Civitai.",
|
||||
@@ -344,6 +365,14 @@
|
||||
"download": "Télécharger",
|
||||
"restartRequired": "Redémarrage requis"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "Stratégie des indicateurs de mise à jour",
|
||||
"help": "Choisissez si les badges de mise à jour doivent apparaître uniquement lorsqu’une nouvelle version partage le même modèle de base que vos fichiers locaux, ou dès qu’il existe une version plus récente pour ce modèle.",
|
||||
"options": {
|
||||
"sameBase": "Faire correspondre les mises à jour par modèle de base",
|
||||
"any": "Signaler n’importe quelle mise à jour disponible"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Inclure les mots-clés dans la syntaxe LoRA",
|
||||
"includeTriggerWordsHelp": "Inclure les mots-clés d'entraînement lors de la copie de la syntaxe LoRA dans le presse-papiers"
|
||||
@@ -389,26 +418,6 @@
|
||||
"proxyPassword": "Mot de passe (optionnel)",
|
||||
"proxyPasswordPlaceholder": "mot_de_passe",
|
||||
"proxyPasswordHelp": "Mot de passe pour l'authentification proxy (si nécessaire)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Étiquettes prioritaires",
|
||||
"description": "Personnalisez l'ordre de priorité des étiquettes pour chaque type de modèle (par ex. : character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "Ouvrir l'aide sur les étiquettes prioritaires",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Checkpoint",
|
||||
"embedding": "Embedding"
|
||||
},
|
||||
"saveSuccess": "Étiquettes prioritaires mises à jour.",
|
||||
"saveError": "Échec de la mise à jour des étiquettes prioritaires.",
|
||||
"loadingSuggestions": "Chargement des suggestions...",
|
||||
"validation": {
|
||||
"missingClosingParen": "L'entrée {index} n'a pas de parenthèse fermante.",
|
||||
"missingCanonical": "L'entrée {index} doit inclure un nom d'étiquette canonique.",
|
||||
"duplicateCanonical": "L'étiquette canonique \"{tag}\" apparaît plusieurs fois.",
|
||||
"unknown": "Configuration d'étiquettes prioritaires invalide."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "Voir toutes les versions locales",
|
||||
"viewLocalTooltip": "Bientôt disponible"
|
||||
},
|
||||
"filters": {
|
||||
"label": "Filtre de base",
|
||||
"state": {
|
||||
"showAll": "Toutes les versions",
|
||||
"showSameBase": "Même modèle de base"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "Passer à l'affichage de toutes les versions",
|
||||
"showSameBaseVersions": "Passer à l'affichage des versions du même modèle de base"
|
||||
},
|
||||
"empty": "Aucune version ne correspond au filtre du modèle de base actuel."
|
||||
},
|
||||
"empty": "Aucun historique de versions n'est disponible pour ce modèle pour le moment.",
|
||||
"error": "Échec du chargement des versions.",
|
||||
"missingModelId": "Ce modèle ne possède pas d'identifiant de modèle Civitai.",
|
||||
|
||||
@@ -229,6 +229,7 @@
|
||||
"folderSettings": "הגדרות תיקייה",
|
||||
"downloadPathTemplates": "תבניות נתיב הורדה",
|
||||
"exampleImages": "תמונות דוגמה",
|
||||
"updateFlags": "תגי עדכון",
|
||||
"misc": "שונות",
|
||||
"metadataArchive": "מסד נתונים של ארכיון מטא-דאטה",
|
||||
"storageLocation": "מיקום ההגדרות",
|
||||
@@ -344,6 +345,14 @@
|
||||
"download": "הורד",
|
||||
"restartRequired": "דורש הפעלה מחדש"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "אסטרטגיית תגי עדכון",
|
||||
"help": "בחרו אם תוויות העדכון יוצגו רק כאשר גרסה חדשה חולקת את אותו דגם בסיס כמו הקבצים המקומיים שלכם או בכל מקרה שבו קיימת גרסה חדשה עבור אותו דגם.",
|
||||
"options": {
|
||||
"sameBase": "התאמת עדכונים לפי דגם בסיס",
|
||||
"any": "תוויות לכל עדכון זמין"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "כלול מילות טריגר בתחביר LoRA",
|
||||
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח"
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "הצג את כל הגרסאות המקומיות",
|
||||
"viewLocalTooltip": "יגיע בקרוב"
|
||||
},
|
||||
"filters": {
|
||||
"label": "מסנן בסיס",
|
||||
"state": {
|
||||
"showAll": "כל הגרסאות",
|
||||
"showSameBase": "אותו מודל בסיס"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "החלף להצגת כל הגרסאות",
|
||||
"showSameBaseVersions": "החלף להצגת גרסאות עם אותו מודל בסיס"
|
||||
},
|
||||
"empty": "אין גרסאות התואמות את המסנן של מודל הבסיס הנוכחי."
|
||||
},
|
||||
"empty": "אין עדיין היסטוריית גרסאות למודל זה.",
|
||||
"error": "טעינת הגרסאות נכשלה.",
|
||||
"missingModelId": "למודל זה אין מזהה מודל של Civitai.",
|
||||
|
||||
@@ -227,13 +227,14 @@
|
||||
"videoSettings": "動画設定",
|
||||
"layoutSettings": "レイアウト設定",
|
||||
"folderSettings": "フォルダ設定",
|
||||
"priorityTags": "優先タグ",
|
||||
"downloadPathTemplates": "ダウンロードパステンプレート",
|
||||
"exampleImages": "例画像",
|
||||
"updateFlags": "アップデートフラグ",
|
||||
"misc": "その他",
|
||||
"metadataArchive": "メタデータアーカイブデータベース",
|
||||
"storageLocation": "設定の場所",
|
||||
"proxySettings": "プロキシ設定",
|
||||
"priorityTags": "優先タグ"
|
||||
"proxySettings": "プロキシ設定"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "ポータブルモード",
|
||||
@@ -297,6 +298,26 @@
|
||||
"defaultEmbeddingRootHelp": "ダウンロード、インポート、移動用のデフォルトembeddingルートディレクトリを設定",
|
||||
"noDefault": "デフォルトなし"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "優先タグ",
|
||||
"description": "各モデルタイプのタグ優先順位をカスタマイズします (例: character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "優先タグのヘルプを開く",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "チェックポイント",
|
||||
"embedding": "埋め込み"
|
||||
},
|
||||
"saveSuccess": "優先タグを更新しました。",
|
||||
"saveError": "優先タグの更新に失敗しました。",
|
||||
"loadingSuggestions": "候補を読み込み中...",
|
||||
"validation": {
|
||||
"missingClosingParen": "エントリ {index} に閉じ括弧がありません。",
|
||||
"missingCanonical": "エントリ {index} には正規タグ名を含める必要があります。",
|
||||
"duplicateCanonical": "正規タグ \"{tag}\" が複数回登場しています。",
|
||||
"unknown": "無効な優先タグ設定です。"
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "ダウンロードパステンプレート",
|
||||
"help": "Civitaiからダウンロードする際の異なるモデルタイプのフォルダ構造を設定します。",
|
||||
@@ -344,6 +365,14 @@
|
||||
"download": "ダウンロード",
|
||||
"restartRequired": "再起動が必要"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "アップデートフラグの表示戦略",
|
||||
"help": "新リリースがローカルファイルと同じベースモデルを共有する場合にのみ更新バッジを表示するか、そのモデルに新しいバージョンがあれば常に表示するかを決めます。",
|
||||
"options": {
|
||||
"sameBase": "ベースモデルで更新をマッチ",
|
||||
"any": "利用可能な更新すべてを表示"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "LoRA構文にトリガーワードを含める",
|
||||
"includeTriggerWordsHelp": "LoRA構文をクリップボードにコピーする際、学習済みトリガーワードを含めます"
|
||||
@@ -389,26 +418,6 @@
|
||||
"proxyPassword": "パスワード(任意)",
|
||||
"proxyPasswordPlaceholder": "パスワード",
|
||||
"proxyPasswordHelp": "プロキシ認証用のパスワード(必要な場合)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "優先タグ",
|
||||
"description": "各モデルタイプのタグ優先順位をカスタマイズします (例: character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "優先タグのヘルプを開く",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "チェックポイント",
|
||||
"embedding": "埋め込み"
|
||||
},
|
||||
"saveSuccess": "優先タグを更新しました。",
|
||||
"saveError": "優先タグの更新に失敗しました。",
|
||||
"loadingSuggestions": "候補を読み込み中...",
|
||||
"validation": {
|
||||
"missingClosingParen": "エントリ {index} に閉じ括弧がありません。",
|
||||
"missingCanonical": "エントリ {index} には正規タグ名を含める必要があります。",
|
||||
"duplicateCanonical": "正規タグ \"{tag}\" が複数回登場しています。",
|
||||
"unknown": "無効な優先タグ設定です。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "ローカルの全バージョンを表示",
|
||||
"viewLocalTooltip": "近日対応予定"
|
||||
},
|
||||
"filters": {
|
||||
"label": "ベースフィルター",
|
||||
"state": {
|
||||
"showAll": "すべてのバージョン",
|
||||
"showSameBase": "同じベース"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "すべてのバージョンを表示する",
|
||||
"showSameBaseVersions": "同じベースモデルのバージョンのみ表示する"
|
||||
},
|
||||
"empty": "現在のベースモデルフィルターに一致するバージョンがありません。"
|
||||
},
|
||||
"empty": "このモデルにはまだバージョン履歴がありません。",
|
||||
"error": "バージョンの読み込みに失敗しました。",
|
||||
"missingModelId": "このモデルにはCivitaiのモデルIDがありません。",
|
||||
|
||||
@@ -227,13 +227,14 @@
|
||||
"videoSettings": "비디오 설정",
|
||||
"layoutSettings": "레이아웃 설정",
|
||||
"folderSettings": "폴더 설정",
|
||||
"priorityTags": "우선순위 태그",
|
||||
"downloadPathTemplates": "다운로드 경로 템플릿",
|
||||
"exampleImages": "예시 이미지",
|
||||
"updateFlags": "업데이트 표시",
|
||||
"misc": "기타",
|
||||
"metadataArchive": "메타데이터 아카이브 데이터베이스",
|
||||
"storageLocation": "설정 위치",
|
||||
"proxySettings": "프록시 설정",
|
||||
"priorityTags": "우선순위 태그"
|
||||
"proxySettings": "프록시 설정"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "휴대용 모드",
|
||||
@@ -297,6 +298,26 @@
|
||||
"defaultEmbeddingRootHelp": "다운로드, 가져오기 및 이동을 위한 기본 Embedding 루트 디렉토리를 설정합니다",
|
||||
"noDefault": "기본값 없음"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "우선순위 태그",
|
||||
"description": "모델 유형별 태그 우선순위를 사용자 지정합니다(예: character, concept, style(toon|toon_style)).",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "우선순위 태그 도움말 열기",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "체크포인트",
|
||||
"embedding": "임베딩"
|
||||
},
|
||||
"saveSuccess": "우선순위 태그가 업데이트되었습니다.",
|
||||
"saveError": "우선순위 태그를 업데이트하지 못했습니다.",
|
||||
"loadingSuggestions": "추천을 불러오는 중...",
|
||||
"validation": {
|
||||
"missingClosingParen": "{index}번째 항목에 닫는 괄호가 없습니다.",
|
||||
"missingCanonical": "{index}번째 항목에는 정식 태그 이름이 포함되어야 합니다.",
|
||||
"duplicateCanonical": "정식 태그 \"{tag}\"가 여러 번 나타납니다.",
|
||||
"unknown": "잘못된 우선순위 태그 구성입니다."
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "다운로드 경로 템플릿",
|
||||
"help": "Civitai에서 다운로드할 때 다양한 모델 유형의 폴더 구조를 구성합니다.",
|
||||
@@ -344,6 +365,14 @@
|
||||
"download": "다운로드",
|
||||
"restartRequired": "재시작 필요"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "업데이트 표시 전략",
|
||||
"help": "새 릴리스가 로컬 파일과 동일한 베이스 모델을 공유할 때만 업데이트 배지를 표시할지, 또는 해당 모델에 사용 가능한 새 버전이 있으면 항상 표시할지 결정합니다.",
|
||||
"options": {
|
||||
"sameBase": "베이스 모델로 업데이트 일치",
|
||||
"any": "사용 가능한 모든 업데이트 표시"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "LoRA 문법에 트리거 단어 포함",
|
||||
"includeTriggerWordsHelp": "LoRA 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다"
|
||||
@@ -389,26 +418,6 @@
|
||||
"proxyPassword": "비밀번호 (선택사항)",
|
||||
"proxyPasswordPlaceholder": "password",
|
||||
"proxyPasswordHelp": "프록시 인증에 필요한 비밀번호 (필요한 경우)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "우선순위 태그",
|
||||
"description": "모델 유형별 태그 우선순위를 사용자 지정합니다(예: character, concept, style(toon|toon_style)).",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "우선순위 태그 도움말 열기",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "체크포인트",
|
||||
"embedding": "임베딩"
|
||||
},
|
||||
"saveSuccess": "우선순위 태그가 업데이트되었습니다.",
|
||||
"saveError": "우선순위 태그를 업데이트하지 못했습니다.",
|
||||
"loadingSuggestions": "추천을 불러오는 중...",
|
||||
"validation": {
|
||||
"missingClosingParen": "{index}번째 항목에 닫는 괄호가 없습니다.",
|
||||
"missingCanonical": "{index}번째 항목에는 정식 태그 이름이 포함되어야 합니다.",
|
||||
"duplicateCanonical": "정식 태그 \"{tag}\"가 여러 번 나타납니다.",
|
||||
"unknown": "잘못된 우선순위 태그 구성입니다."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "로컬 버전 모두 보기",
|
||||
"viewLocalTooltip": "곧 제공 예정"
|
||||
},
|
||||
"filters": {
|
||||
"label": "기본 필터",
|
||||
"state": {
|
||||
"showAll": "모든 버전",
|
||||
"showSameBase": "같은 베이스"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "모든 버전을 표시하도록 전환",
|
||||
"showSameBaseVersions": "같은 베이스 모델 버전만 표시하도록 전환"
|
||||
},
|
||||
"empty": "현재 베이스 모델 필터와 일치하는 버전이 없습니다."
|
||||
},
|
||||
"empty": "이 모델에는 아직 버전 기록이 없습니다.",
|
||||
"error": "버전을 불러오지 못했습니다.",
|
||||
"missingModelId": "이 모델에는 Civitai 모델 ID가 없습니다.",
|
||||
|
||||
@@ -227,13 +227,14 @@
|
||||
"videoSettings": "Настройки видео",
|
||||
"layoutSettings": "Настройки макета",
|
||||
"folderSettings": "Настройки папок",
|
||||
"priorityTags": "Приоритетные теги",
|
||||
"downloadPathTemplates": "Шаблоны путей загрузки",
|
||||
"exampleImages": "Примеры изображений",
|
||||
"updateFlags": "Метки обновлений",
|
||||
"misc": "Разное",
|
||||
"metadataArchive": "Архив метаданных",
|
||||
"storageLocation": "Расположение настроек",
|
||||
"proxySettings": "Настройки прокси",
|
||||
"priorityTags": "Приоритетные теги"
|
||||
"proxySettings": "Настройки прокси"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Портативный режим",
|
||||
@@ -297,6 +298,26 @@
|
||||
"defaultEmbeddingRootHelp": "Установить корневую папку embedding по умолчанию для загрузок, импорта и перемещений",
|
||||
"noDefault": "Не задано"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Приоритетные теги",
|
||||
"description": "Настройте порядок приоритетов тегов для каждого типа моделей (например, character, concept, style(toon|toon_style)).",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "Открыть справку по приоритетным тегам",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Чекпойнт",
|
||||
"embedding": "Эмбеддинг"
|
||||
},
|
||||
"saveSuccess": "Приоритетные теги обновлены.",
|
||||
"saveError": "Не удалось обновить приоритетные теги.",
|
||||
"loadingSuggestions": "Загрузка подсказок...",
|
||||
"validation": {
|
||||
"missingClosingParen": "В записи {index} отсутствует закрывающая скобка.",
|
||||
"missingCanonical": "Запись {index} должна содержать каноническое имя тега.",
|
||||
"duplicateCanonical": "Канонический тег \"{tag}\" встречается более одного раза.",
|
||||
"unknown": "Недопустимая конфигурация приоритетных тегов."
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "Шаблоны путей загрузки",
|
||||
"help": "Настройте структуру папок для разных типов моделей при загрузке с Civitai.",
|
||||
@@ -344,6 +365,14 @@
|
||||
"download": "Загрузить",
|
||||
"restartRequired": "Требует перезапуска"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "Стратегия меток обновлений",
|
||||
"help": "Выберите, отображать ли значки обновления только когда новая версия имеет тот же базовый модель, что и локальные файлы, или всегда при наличии любого нового релиза для этой модели.",
|
||||
"options": {
|
||||
"sameBase": "Совпадение обновлений по базовой модели",
|
||||
"any": "Отмечать любые доступные обновления"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Включать триггерные слова в синтаксис LoRA",
|
||||
"includeTriggerWordsHelp": "Включать обученные триггерные слова при копировании синтаксиса LoRA в буфер обмена"
|
||||
@@ -389,26 +418,6 @@
|
||||
"proxyPassword": "Пароль (необязательно)",
|
||||
"proxyPasswordPlaceholder": "пароль",
|
||||
"proxyPasswordHelp": "Пароль для аутентификации на прокси (если требуется)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "Приоритетные теги",
|
||||
"description": "Настройте порядок приоритетов тегов для каждого типа моделей (например, character, concept, style(toon|toon_style)).",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "Открыть справку по приоритетным тегам",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Чекпойнт",
|
||||
"embedding": "Эмбеддинг"
|
||||
},
|
||||
"saveSuccess": "Приоритетные теги обновлены.",
|
||||
"saveError": "Не удалось обновить приоритетные теги.",
|
||||
"loadingSuggestions": "Загрузка подсказок...",
|
||||
"validation": {
|
||||
"missingClosingParen": "В записи {index} отсутствует закрывающая скобка.",
|
||||
"missingCanonical": "Запись {index} должна содержать каноническое имя тега.",
|
||||
"duplicateCanonical": "Канонический тег \"{tag}\" встречается более одного раза.",
|
||||
"unknown": "Недопустимая конфигурация приоритетных тегов."
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "Показать все локальные версии",
|
||||
"viewLocalTooltip": "Скоро появится"
|
||||
},
|
||||
"filters": {
|
||||
"label": "Фильтр по базе",
|
||||
"state": {
|
||||
"showAll": "Все версии",
|
||||
"showSameBase": "Тот же базовый"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "Переключиться на отображение всех версий",
|
||||
"showSameBaseVersions": "Переключиться на отображение только версий с тем же базовым"
|
||||
},
|
||||
"empty": "Нет версий, соответствующих текущему фильтру базовой модели."
|
||||
},
|
||||
"empty": "Для этой модели пока нет истории версий.",
|
||||
"error": "Не удалось загрузить версии.",
|
||||
"missingModelId": "У этой модели отсутствует идентификатор модели Civitai.",
|
||||
|
||||
@@ -227,13 +227,14 @@
|
||||
"videoSettings": "视频设置",
|
||||
"layoutSettings": "布局设置",
|
||||
"folderSettings": "文件夹设置",
|
||||
"priorityTags": "优先标签",
|
||||
"downloadPathTemplates": "下载路径模板",
|
||||
"exampleImages": "示例图片",
|
||||
"updateFlags": "更新标记",
|
||||
"misc": "其他",
|
||||
"metadataArchive": "元数据归档数据库",
|
||||
"storageLocation": "设置位置",
|
||||
"proxySettings": "代理设置",
|
||||
"priorityTags": "优先标签"
|
||||
"proxySettings": "代理设置"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "便携模式",
|
||||
@@ -297,6 +298,26 @@
|
||||
"defaultEmbeddingRootHelp": "设置下载、导入和移动时的默认 Embedding 根目录",
|
||||
"noDefault": "无默认"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "优先标签",
|
||||
"description": "为每种模型类型自定义标签优先级顺序 (例如: character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "打开优先标签帮助",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Checkpoint",
|
||||
"embedding": "Embedding"
|
||||
},
|
||||
"saveSuccess": "优先标签已更新。",
|
||||
"saveError": "优先标签更新失败。",
|
||||
"loadingSuggestions": "正在加载建议...",
|
||||
"validation": {
|
||||
"missingClosingParen": "条目 {index} 缺少右括号。",
|
||||
"missingCanonical": "条目 {index} 必须包含规范标签名称。",
|
||||
"duplicateCanonical": "规范标签 \"{tag}\" 出现多次。",
|
||||
"unknown": "优先标签配置无效。"
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "下载路径模板",
|
||||
"help": "配置从 Civitai 下载不同模型类型的文件夹结构。",
|
||||
@@ -344,6 +365,14 @@
|
||||
"download": "下载",
|
||||
"restartRequired": "需要重启"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "更新标记策略",
|
||||
"help": "决定更新徽章是否仅在新版本与本地文件共享相同基础模型时显示,或只要该模型有任何更新版本就显示。",
|
||||
"options": {
|
||||
"sameBase": "按基础模型匹配更新",
|
||||
"any": "显示任何可用更新"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "复制 LoRA 语法时包含触发词",
|
||||
"includeTriggerWordsHelp": "复制 LoRA 语法到剪贴板时包含训练触发词"
|
||||
@@ -389,26 +418,6 @@
|
||||
"proxyPassword": "密码 (可选)",
|
||||
"proxyPasswordPlaceholder": "密码",
|
||||
"proxyPasswordHelp": "代理认证的密码 (如果需要)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "优先标签",
|
||||
"description": "为每种模型类型自定义标签优先级顺序 (例如: character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "打开优先标签帮助",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Checkpoint",
|
||||
"embedding": "Embedding"
|
||||
},
|
||||
"saveSuccess": "优先标签已更新。",
|
||||
"saveError": "优先标签更新失败。",
|
||||
"loadingSuggestions": "正在加载建议...",
|
||||
"validation": {
|
||||
"missingClosingParen": "条目 {index} 缺少右括号。",
|
||||
"missingCanonical": "条目 {index} 必须包含规范标签名称。",
|
||||
"duplicateCanonical": "规范标签 \"{tag}\" 出现多次。",
|
||||
"unknown": "优先标签配置无效。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "查看所有本地版本",
|
||||
"viewLocalTooltip": "敬请期待"
|
||||
},
|
||||
"filters": {
|
||||
"label": "基础筛选",
|
||||
"state": {
|
||||
"showAll": "全部版本",
|
||||
"showSameBase": "相同基模型"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "切换为显示所有版本",
|
||||
"showSameBaseVersions": "仅显示与当前基模型匹配的版本"
|
||||
},
|
||||
"empty": "没有与当前基模型筛选匹配的版本。"
|
||||
},
|
||||
"empty": "该模型还没有版本历史。",
|
||||
"error": "加载版本失败。",
|
||||
"missingModelId": "该模型缺少 Civitai 模型 ID。",
|
||||
|
||||
@@ -227,13 +227,14 @@
|
||||
"videoSettings": "影片設定",
|
||||
"layoutSettings": "版面設定",
|
||||
"folderSettings": "資料夾設定",
|
||||
"priorityTags": "優先標籤",
|
||||
"downloadPathTemplates": "下載路徑範本",
|
||||
"exampleImages": "範例圖片",
|
||||
"updateFlags": "更新標記",
|
||||
"misc": "其他",
|
||||
"metadataArchive": "中繼資料封存資料庫",
|
||||
"storageLocation": "設定位置",
|
||||
"proxySettings": "代理設定",
|
||||
"priorityTags": "優先標籤"
|
||||
"proxySettings": "代理設定"
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "可攜式模式",
|
||||
@@ -297,6 +298,26 @@
|
||||
"defaultEmbeddingRootHelp": "設定下載、匯入和移動時的預設 Embedding 根目錄",
|
||||
"noDefault": "未設定預設"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "優先標籤",
|
||||
"description": "為每種模型類型自訂標籤的優先順序 (例如: character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "開啟優先標籤說明",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Checkpoint",
|
||||
"embedding": "Embedding"
|
||||
},
|
||||
"saveSuccess": "優先標籤已更新。",
|
||||
"saveError": "更新優先標籤失敗。",
|
||||
"loadingSuggestions": "正在載入建議...",
|
||||
"validation": {
|
||||
"missingClosingParen": "項目 {index} 缺少右括號。",
|
||||
"missingCanonical": "項目 {index} 必須包含正規標籤名稱。",
|
||||
"duplicateCanonical": "正規標籤 \"{tag}\" 出現多於一次。",
|
||||
"unknown": "優先標籤設定無效。"
|
||||
}
|
||||
},
|
||||
"downloadPathTemplates": {
|
||||
"title": "下載路徑範本",
|
||||
"help": "設定從 Civitai 下載時不同模型類型的資料夾結構。",
|
||||
@@ -344,6 +365,14 @@
|
||||
"download": "下載",
|
||||
"restartRequired": "需要重新啟動"
|
||||
},
|
||||
"updateFlagStrategy": {
|
||||
"label": "更新標記策略",
|
||||
"help": "決定更新徽章是否僅在新版本與本地檔案共享相同基礎模型時顯示,或只要該模型有任何更新版本就顯示。",
|
||||
"options": {
|
||||
"sameBase": "依基礎模型匹配更新",
|
||||
"any": "顯示任何可用更新"
|
||||
}
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "在 LoRA 語法中包含觸發詞",
|
||||
"includeTriggerWordsHelp": "複製 LoRA 語法到剪貼簿時包含訓練觸發詞"
|
||||
@@ -389,26 +418,6 @@
|
||||
"proxyPassword": "密碼(選填)",
|
||||
"proxyPasswordPlaceholder": "password",
|
||||
"proxyPasswordHelp": "代理驗證所需的密碼(如有需要)"
|
||||
},
|
||||
"priorityTags": {
|
||||
"title": "優先標籤",
|
||||
"description": "為每種模型類型自訂標籤的優先順序 (例如: character, concept, style(toon|toon_style))",
|
||||
"placeholder": "character, concept, style(toon|toon_style)",
|
||||
"helpLinkLabel": "開啟優先標籤說明",
|
||||
"modelTypes": {
|
||||
"lora": "LoRA",
|
||||
"checkpoint": "Checkpoint",
|
||||
"embedding": "Embedding"
|
||||
},
|
||||
"saveSuccess": "優先標籤已更新。",
|
||||
"saveError": "更新優先標籤失敗。",
|
||||
"loadingSuggestions": "正在載入建議...",
|
||||
"validation": {
|
||||
"missingClosingParen": "項目 {index} 缺少右括號。",
|
||||
"missingCanonical": "項目 {index} 必須包含正規標籤名稱。",
|
||||
"duplicateCanonical": "正規標籤 \"{tag}\" 出現多於一次。",
|
||||
"unknown": "優先標籤設定無效。"
|
||||
}
|
||||
}
|
||||
},
|
||||
"loras": {
|
||||
@@ -929,6 +938,18 @@
|
||||
"viewLocalVersions": "檢視所有本地版本",
|
||||
"viewLocalTooltip": "敬請期待"
|
||||
},
|
||||
"filters": {
|
||||
"label": "基礎篩選",
|
||||
"state": {
|
||||
"showAll": "所有版本",
|
||||
"showSameBase": "相同基礎模型"
|
||||
},
|
||||
"tooltip": {
|
||||
"showAllVersions": "切換為顯示所有版本",
|
||||
"showSameBaseVersions": "僅顯示與目前基礎模型相符的版本"
|
||||
},
|
||||
"empty": "沒有符合目前基礎模型篩選的版本。"
|
||||
},
|
||||
"empty": "此模型尚無版本歷史。",
|
||||
"error": "載入版本失敗。",
|
||||
"missingModelId": "此模型缺少 Civitai 模型 ID。",
|
||||
|
||||
@@ -201,6 +201,7 @@ class SettingsHandler:
|
||||
"priority_tags",
|
||||
"model_card_footer_action",
|
||||
"model_name_display",
|
||||
"update_flag_strategy",
|
||||
)
|
||||
|
||||
_PROXY_KEYS = {"proxy_enabled", "proxy_host", "proxy_port", "proxy_username", "proxy_password", "proxy_type"}
|
||||
|
||||
@@ -267,20 +267,49 @@ class BaseModelService(ABC):
|
||||
if not ordered_ids:
|
||||
return annotated
|
||||
|
||||
strategy_value = self.settings.get("update_flag_strategy")
|
||||
if isinstance(strategy_value, str) and strategy_value.strip():
|
||||
strategy = strategy_value.strip().lower()
|
||||
else:
|
||||
strategy = "same_base"
|
||||
same_base_mode = strategy == "same_base"
|
||||
|
||||
records = None
|
||||
resolved: Optional[Dict[int, bool]] = None
|
||||
bulk_method = getattr(self.update_service, "has_updates_bulk", None)
|
||||
if callable(bulk_method):
|
||||
try:
|
||||
resolved = await bulk_method(self.model_type, ordered_ids)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to resolve update status in bulk for %s models (%s): %s",
|
||||
self.model_type,
|
||||
ordered_ids,
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
resolved = None
|
||||
if same_base_mode:
|
||||
record_method = getattr(self.update_service, "get_records_bulk", None)
|
||||
if callable(record_method):
|
||||
try:
|
||||
records = await record_method(self.model_type, ordered_ids)
|
||||
resolved = {
|
||||
model_id: record.has_update()
|
||||
for model_id, record in records.items()
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to resolve update records in bulk for %s models (%s): %s",
|
||||
self.model_type,
|
||||
ordered_ids,
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
records = None
|
||||
resolved = None
|
||||
|
||||
if resolved is None:
|
||||
bulk_method = getattr(self.update_service, "has_updates_bulk", None)
|
||||
if callable(bulk_method):
|
||||
try:
|
||||
resolved = await bulk_method(self.model_type, ordered_ids)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
"Failed to resolve update status in bulk for %s models (%s): %s",
|
||||
self.model_type,
|
||||
ordered_ids,
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
resolved = None
|
||||
|
||||
if resolved is None:
|
||||
tasks = [
|
||||
@@ -301,8 +330,24 @@ class BaseModelService(ABC):
|
||||
resolved[model_id] = bool(result)
|
||||
|
||||
for model_id, items_for_id in id_to_items.items():
|
||||
flag = bool(resolved.get(model_id, False))
|
||||
default_flag = bool(resolved.get(model_id, False)) if resolved else False
|
||||
record = records.get(model_id) if records else None
|
||||
base_highest_versions = (
|
||||
self._build_highest_local_versions_by_base(record) if same_base_mode and record else {}
|
||||
)
|
||||
for item in items_for_id:
|
||||
if same_base_mode and record is not None:
|
||||
base_model = self._extract_base_model(item)
|
||||
normalized_base = self._normalize_base_model_name(base_model)
|
||||
threshold_version = base_highest_versions.get(normalized_base) if normalized_base else None
|
||||
if threshold_version is None:
|
||||
threshold_version = self._extract_version_id(item)
|
||||
flag = record.has_update_for_base(
|
||||
threshold_version,
|
||||
base_model,
|
||||
)
|
||||
else:
|
||||
flag = default_flag
|
||||
item['update_available'] = flag
|
||||
|
||||
return annotated
|
||||
@@ -319,7 +364,71 @@ class BaseModelService(ABC):
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _extract_version_id(item: Dict) -> Optional[int]:
|
||||
civitai = item.get('civitai') if isinstance(item, dict) else None
|
||||
if not isinstance(civitai, dict):
|
||||
return None
|
||||
value = civitai.get('id')
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _extract_base_model(item: Dict) -> Optional[str]:
|
||||
value = item.get('base_model')
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
candidate = value.strip()
|
||||
else:
|
||||
try:
|
||||
candidate = str(value).strip()
|
||||
except Exception:
|
||||
return None
|
||||
return candidate if candidate else None
|
||||
|
||||
@staticmethod
|
||||
def _normalize_base_model_name(value: Optional[str]) -> Optional[str]:
|
||||
"""Return a lowercased, trimmed base model name for comparison."""
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
candidate = value.strip()
|
||||
else:
|
||||
try:
|
||||
candidate = str(value).strip()
|
||||
except Exception:
|
||||
return None
|
||||
return candidate.lower() if candidate else None
|
||||
|
||||
def _build_highest_local_versions_by_base(self, record) -> Dict[str, int]:
|
||||
"""Return the highest local version id known for each normalized base model."""
|
||||
|
||||
if record is None:
|
||||
return {}
|
||||
|
||||
highest_by_base: Dict[str, int] = {}
|
||||
for version in getattr(record, "versions", []):
|
||||
if not getattr(version, "is_in_library", False):
|
||||
continue
|
||||
normalized_base = self._normalize_base_model_name(getattr(version, "base_model", None))
|
||||
if normalized_base is None:
|
||||
continue
|
||||
version_id = getattr(version, "version_id", None)
|
||||
if version_id is None:
|
||||
continue
|
||||
current_max = highest_by_base.get(normalized_base)
|
||||
if current_max is None or version_id > current_max:
|
||||
highest_by_base[normalized_base] = version_id
|
||||
|
||||
return highest_by_base
|
||||
|
||||
def _paginate(self, data: List[Dict], page: int, page_size: int) -> Dict:
|
||||
"""Apply pagination to filtered data"""
|
||||
total_items = len(data)
|
||||
|
||||
@@ -17,6 +17,41 @@ from ..utils.preview_selection import select_preview_media
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _normalize_int(value) -> Optional[int]:
|
||||
"""Safely convert a value to an integer."""
|
||||
|
||||
try:
|
||||
if value is None:
|
||||
return None
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_string(value) -> Optional[str]:
|
||||
"""Return a stripped string or None if the value is empty."""
|
||||
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
try:
|
||||
normalized = str(value).strip()
|
||||
return normalized or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _normalize_base_model(value) -> Optional[str]:
|
||||
"""Normalize base-model names for case-insensitive comparison."""
|
||||
|
||||
normalized = _normalize_string(value)
|
||||
if normalized is None:
|
||||
return None
|
||||
return normalized.lower()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelVersionRecord:
|
||||
"""Persisted metadata for a single model version."""
|
||||
@@ -85,6 +120,47 @@ class ModelUpdateRecord:
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_update_for_base(
|
||||
self,
|
||||
local_version_id: Optional[int],
|
||||
local_base_model: Optional[str],
|
||||
) -> bool:
|
||||
"""Return True when a newer remote version with the same base model exists."""
|
||||
|
||||
if self.should_ignore_model:
|
||||
return False
|
||||
|
||||
normalized_base = _normalize_base_model(local_base_model)
|
||||
if normalized_base is None:
|
||||
return False
|
||||
|
||||
threshold = _normalize_int(local_version_id)
|
||||
if threshold is None:
|
||||
highest_local = None
|
||||
for version in self.versions:
|
||||
if not version.is_in_library:
|
||||
continue
|
||||
version_base = _normalize_base_model(version.base_model)
|
||||
if version_base != normalized_base:
|
||||
continue
|
||||
if highest_local is None or version.version_id > highest_local:
|
||||
highest_local = version.version_id
|
||||
threshold = highest_local
|
||||
|
||||
if threshold is None:
|
||||
return False
|
||||
|
||||
for version in self.versions:
|
||||
if version.is_in_library or version.should_ignore:
|
||||
continue
|
||||
version_base = _normalize_base_model(version.base_model)
|
||||
if version_base != normalized_base:
|
||||
continue
|
||||
if version.version_id > threshold:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ModelUpdateService:
|
||||
"""Persist and query remote model version metadata."""
|
||||
@@ -628,6 +704,20 @@ class ModelUpdateService:
|
||||
for model_id in normalized_ids
|
||||
}
|
||||
|
||||
async def get_records_bulk(
|
||||
self,
|
||||
model_type: str,
|
||||
model_ids: Sequence[int],
|
||||
) -> Dict[int, ModelUpdateRecord]:
|
||||
"""Return cached update records for the requested models."""
|
||||
|
||||
normalized_ids = self._normalize_sequence(model_ids)
|
||||
if not normalized_ids:
|
||||
return {}
|
||||
|
||||
async with self._lock:
|
||||
return self._get_records_bulk(model_type, normalized_ids)
|
||||
|
||||
async def _refresh_single_model(
|
||||
self,
|
||||
model_type: str,
|
||||
@@ -799,7 +889,7 @@ class ModelUpdateService:
|
||||
)
|
||||
continue
|
||||
for key, value in response.items():
|
||||
normalized_key = self._normalize_int(key)
|
||||
normalized_key = _normalize_int(key)
|
||||
if normalized_key is None:
|
||||
continue
|
||||
if isinstance(value, Mapping):
|
||||
@@ -832,8 +922,8 @@ class ModelUpdateService:
|
||||
civitai = item.get("civitai") if isinstance(item, dict) else None
|
||||
if not isinstance(civitai, dict):
|
||||
continue
|
||||
model_id = self._normalize_int(civitai.get("modelId"))
|
||||
version_id = self._normalize_int(civitai.get("id"))
|
||||
model_id = _normalize_int(civitai.get("modelId"))
|
||||
version_id = _normalize_int(civitai.get("id"))
|
||||
if model_id is None or version_id is None:
|
||||
continue
|
||||
if target_set is not None and model_id not in target_set:
|
||||
@@ -973,35 +1063,14 @@ class ModelUpdateService:
|
||||
return True
|
||||
return (now - record.last_checked_at) >= self._ttl_seconds
|
||||
|
||||
@staticmethod
|
||||
def _normalize_int(value) -> Optional[int]:
|
||||
try:
|
||||
if value is None:
|
||||
return None
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
def _normalize_sequence(self, values: Sequence[int]) -> List[int]:
|
||||
normalized = [
|
||||
item
|
||||
for item in (self._normalize_int(value) for value in values)
|
||||
for item in (_normalize_int(value) for value in values)
|
||||
if item is not None
|
||||
]
|
||||
return sorted(dict.fromkeys(normalized))
|
||||
|
||||
@staticmethod
|
||||
def _normalize_string(value) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, str):
|
||||
stripped = value.strip()
|
||||
return stripped or None
|
||||
try:
|
||||
return str(value)
|
||||
except Exception: # pragma: no cover - defensive conversion
|
||||
return None
|
||||
|
||||
def _extract_versions(self, response) -> Optional[List[ModelVersionRecord]]:
|
||||
if not isinstance(response, Mapping):
|
||||
return None
|
||||
@@ -1014,12 +1083,12 @@ class ModelUpdateService:
|
||||
for index, entry in enumerate(versions):
|
||||
if not isinstance(entry, Mapping):
|
||||
continue
|
||||
version_id = self._normalize_int(entry.get("id"))
|
||||
version_id = _normalize_int(entry.get("id"))
|
||||
if version_id is None:
|
||||
continue
|
||||
name = self._normalize_string(entry.get("name"))
|
||||
base_model = self._normalize_string(entry.get("baseModel"))
|
||||
released_at = self._normalize_string(entry.get("publishedAt") or entry.get("createdAt"))
|
||||
name = _normalize_string(entry.get("name"))
|
||||
base_model = _normalize_string(entry.get("baseModel"))
|
||||
released_at = _normalize_string(entry.get("publishedAt") or entry.get("createdAt"))
|
||||
size_bytes = self._extract_size_bytes(entry.get("files"))
|
||||
preview_url = self._extract_preview_url(entry.get("images"))
|
||||
extracted.append(
|
||||
@@ -1152,11 +1221,11 @@ class ModelUpdateService:
|
||||
name=row["name"],
|
||||
base_model=row["base_model"],
|
||||
released_at=row["released_at"],
|
||||
size_bytes=self._normalize_int(row["size_bytes"]),
|
||||
size_bytes=_normalize_int(row["size_bytes"]),
|
||||
preview_url=row["preview_url"],
|
||||
is_in_library=bool(row["is_in_library"]),
|
||||
should_ignore=bool(row["should_ignore"]),
|
||||
sort_index=self._normalize_int(row["sort_index"]) or 0,
|
||||
sort_index=_normalize_int(row["sort_index"]) or 0,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
"priority_tags": DEFAULT_PRIORITY_TAG_CONFIG.copy(),
|
||||
"model_name_display": "model_name",
|
||||
"model_card_footer_action": "example_images",
|
||||
"update_flag_strategy": "same_base",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -24,12 +24,29 @@
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.versions-toolbar-info p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.versions-toolbar-info-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.versions-toolbar-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -68,6 +85,41 @@
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.versions-filter-toggle {
|
||||
appearance: none;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: 0;
|
||||
margin-bottom: 4px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: color-mix(in oklch, var(--card-bg) 80%, var(--bg-color));
|
||||
align-self: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-muted);
|
||||
transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease, transform 0.2s ease;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.versions-filter-toggle i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.versions-filter-toggle:hover:not(:disabled) {
|
||||
border-color: var(--text-color);
|
||||
color: var(--text-color);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.versions-filter-toggle[data-filter-active="true"] {
|
||||
border-color: color-mix(in oklch, var(--lora-accent) 65%, transparent);
|
||||
color: var(--lora-accent);
|
||||
background: color-mix(in oklch, var(--lora-accent) 20%, var(--card-bg) 80%);
|
||||
}
|
||||
|
||||
.versions-toolbar-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
|
||||
@@ -152,6 +152,81 @@ function buildBadge(label, tone) {
|
||||
return `<span class="version-badge version-badge-${tone}">${escapeHtml(label)}</span>`;
|
||||
}
|
||||
|
||||
const DISPLAY_FILTER_MODES = Object.freeze({
|
||||
SAME_BASE: 'same_base',
|
||||
ANY: 'any',
|
||||
});
|
||||
|
||||
const FILTER_LABEL_KEY = 'modals.model.versions.filters.label';
|
||||
const FILTER_STATE_KEYS = {
|
||||
[DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.state.showSameBase',
|
||||
[DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.state.showAll',
|
||||
};
|
||||
const FILTER_TOOLTIP_KEYS = {
|
||||
[DISPLAY_FILTER_MODES.SAME_BASE]: 'modals.model.versions.filters.tooltip.showAllVersions',
|
||||
[DISPLAY_FILTER_MODES.ANY]: 'modals.model.versions.filters.tooltip.showSameBaseVersions',
|
||||
};
|
||||
|
||||
function normalizeBaseModelName(value) {
|
||||
if (typeof value !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
return trimmed.toLowerCase();
|
||||
}
|
||||
|
||||
function getToggleLabelText() {
|
||||
return translate(FILTER_LABEL_KEY, {}, 'Base filter');
|
||||
}
|
||||
|
||||
function getToggleStateText(mode) {
|
||||
const key = FILTER_STATE_KEYS[mode] || FILTER_STATE_KEYS[DISPLAY_FILTER_MODES.ANY];
|
||||
const fallback =
|
||||
mode === DISPLAY_FILTER_MODES.SAME_BASE ? 'Same base' : 'All versions';
|
||||
return translate(key, {}, fallback);
|
||||
}
|
||||
|
||||
function getToggleTooltipText(mode) {
|
||||
const key =
|
||||
FILTER_TOOLTIP_KEYS[mode] || FILTER_TOOLTIP_KEYS[DISPLAY_FILTER_MODES.ANY];
|
||||
const fallback =
|
||||
mode === DISPLAY_FILTER_MODES.SAME_BASE
|
||||
? 'Switch to showing all versions'
|
||||
: 'Switch to showing only versions with the current base model';
|
||||
return translate(key, {}, fallback);
|
||||
}
|
||||
|
||||
function getDefaultDisplayMode() {
|
||||
const strategy = state?.global?.settings?.update_flag_strategy;
|
||||
return strategy === DISPLAY_FILTER_MODES.SAME_BASE
|
||||
? DISPLAY_FILTER_MODES.SAME_BASE
|
||||
: DISPLAY_FILTER_MODES.ANY;
|
||||
}
|
||||
|
||||
function getCurrentVersionBaseModel(record, versionId) {
|
||||
if (!record || typeof versionId !== 'number' || !Array.isArray(record.versions)) {
|
||||
return {
|
||||
normalized: null,
|
||||
raw: null,
|
||||
};
|
||||
}
|
||||
const currentVersion = record.versions.find(v => v.versionId === versionId);
|
||||
if (!currentVersion) {
|
||||
return {
|
||||
normalized: null,
|
||||
raw: null,
|
||||
};
|
||||
}
|
||||
const baseModelRaw = currentVersion.baseModel ?? null;
|
||||
return {
|
||||
normalized: normalizeBaseModelName(baseModelRaw),
|
||||
raw: baseModelRaw,
|
||||
};
|
||||
}
|
||||
|
||||
function getAutoplaySetting() {
|
||||
try {
|
||||
return Boolean(state?.global?.settings?.autoplay_on_hover);
|
||||
@@ -314,7 +389,7 @@ function getLatestLibraryVersionId(record) {
|
||||
return Math.max(...record.inLibraryVersionIds);
|
||||
}
|
||||
|
||||
function renderToolbar(record) {
|
||||
function renderToolbar(record, toolbarState = {}) {
|
||||
const ignoreText = record.shouldIgnore
|
||||
? translate('modals.model.versions.actions.resumeModelUpdates', {}, 'Resume updates for this model')
|
||||
: translate('modals.model.versions.actions.ignoreModelUpdates', {}, 'Ignore updates for this model');
|
||||
@@ -325,10 +400,23 @@ function renderToolbar(record) {
|
||||
'Track and manage every version of this model in one place.'
|
||||
);
|
||||
|
||||
const displayMode = toolbarState.displayMode || DISPLAY_FILTER_MODES.ANY;
|
||||
const toggleLabel = getToggleLabelText();
|
||||
const toggleState = getToggleStateText(displayMode);
|
||||
const toggleTooltip = getToggleTooltipText(displayMode);
|
||||
const filterActive = toolbarState.isFilteringActive ? 'true' : 'false';
|
||||
const screenReaderText = [toggleLabel, toggleState].filter(Boolean).join(': ');
|
||||
|
||||
return `
|
||||
<header class="versions-toolbar">
|
||||
<div class="versions-toolbar-info">
|
||||
<h3>${translate('modals.model.versions.heading', {}, 'Model versions')}</h3>
|
||||
<div class="versions-toolbar-info-heading">
|
||||
<h3>${translate('modals.model.versions.heading', {}, 'Model versions')}</h3>
|
||||
<button class="versions-filter-toggle" data-versions-action="toggle-version-display-mode" type="button" title="${escapeHtml(toggleTooltip)}" aria-label="${escapeHtml(toggleTooltip)}" data-filter-active="${filterActive}" aria-pressed="${filterActive}">
|
||||
<i class="fas fa-th-list" aria-hidden="true"></i>
|
||||
<span class="sr-only">${escapeHtml(screenReaderText)}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p>${escapeHtml(infoText)}</p>
|
||||
</div>
|
||||
<div class="versions-toolbar-actions">
|
||||
@@ -353,6 +441,20 @@ function renderEmptyState(container) {
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFilteredEmptyState(baseModelLabel) {
|
||||
const message = translate(
|
||||
'modals.model.versions.filters.empty',
|
||||
{ baseModel: baseModelLabel },
|
||||
'No versions match the current base model filter.'
|
||||
);
|
||||
return `
|
||||
<div class="versions-empty versions-empty-filter">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<p>${escapeHtml(message)}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderErrorState(container, message) {
|
||||
const fallback = translate('modals.model.versions.error', {}, 'Failed to load versions.');
|
||||
container.innerHTML = `
|
||||
@@ -391,6 +493,8 @@ export function initVersionsTab({
|
||||
record: null,
|
||||
};
|
||||
|
||||
let displayMode = getDefaultDisplayMode();
|
||||
|
||||
let apiClient;
|
||||
|
||||
function ensureClient() {
|
||||
@@ -414,55 +518,92 @@ export function initVersionsTab({
|
||||
`;
|
||||
}
|
||||
|
||||
function render(record) {
|
||||
controller.record = record;
|
||||
controller.hasLoaded = true;
|
||||
function render(record) {
|
||||
controller.record = record;
|
||||
controller.hasLoaded = true;
|
||||
|
||||
if (!record || !Array.isArray(record.versions) || record.versions.length === 0) {
|
||||
renderEmptyState(container);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestLibraryVersionId = getLatestLibraryVersionId(record);
|
||||
let dividerInserted = false;
|
||||
|
||||
const sortedVersions = [...record.versions].sort(
|
||||
(a, b) => Number(b.versionId) - Number(a.versionId)
|
||||
);
|
||||
|
||||
const rowsMarkup = sortedVersions
|
||||
.map(version => {
|
||||
const isNewer =
|
||||
typeof latestLibraryVersionId === 'number' &&
|
||||
version.versionId > latestLibraryVersionId;
|
||||
let markup = '';
|
||||
if (
|
||||
!dividerInserted &&
|
||||
typeof latestLibraryVersionId === 'number' &&
|
||||
!isNewer
|
||||
) {
|
||||
dividerInserted = true;
|
||||
markup += '<div class="version-divider" role="presentation"></div>';
|
||||
}
|
||||
markup += renderRow(version, {
|
||||
latestLibraryVersionId,
|
||||
currentVersionId: normalizedCurrentVersionId,
|
||||
modelId: record?.modelId ?? modelId,
|
||||
});
|
||||
return markup;
|
||||
})
|
||||
.join('');
|
||||
|
||||
container.innerHTML = `
|
||||
${renderToolbar(record)}
|
||||
<div class="versions-list">
|
||||
${rowsMarkup}
|
||||
</div>
|
||||
`;
|
||||
|
||||
setupMediaHoverInteractions(container);
|
||||
if (!record || !Array.isArray(record.versions) || record.versions.length === 0) {
|
||||
renderEmptyState(container);
|
||||
return;
|
||||
}
|
||||
|
||||
const latestLibraryVersionId = getLatestLibraryVersionId(record);
|
||||
const { normalized: currentBaseModelNormalized, raw: currentBaseModelLabel } =
|
||||
getCurrentVersionBaseModel(record, normalizedCurrentVersionId);
|
||||
const isFilteringActive =
|
||||
displayMode === DISPLAY_FILTER_MODES.SAME_BASE &&
|
||||
Boolean(currentBaseModelNormalized);
|
||||
|
||||
const sortedVersions = [...record.versions].sort(
|
||||
(a, b) => Number(b.versionId) - Number(a.versionId)
|
||||
);
|
||||
|
||||
const filteredVersions = sortedVersions.filter(version => {
|
||||
if (!isFilteringActive) {
|
||||
return true;
|
||||
}
|
||||
return normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized;
|
||||
});
|
||||
|
||||
const dividerThresholdVersionId = (() => {
|
||||
if (!isFilteringActive) {
|
||||
return latestLibraryVersionId;
|
||||
}
|
||||
const baseLocalVersionIds = record.versions
|
||||
.filter(
|
||||
version =>
|
||||
version.isInLibrary &&
|
||||
normalizeBaseModelName(version.baseModel) === currentBaseModelNormalized &&
|
||||
typeof version.versionId === 'number'
|
||||
)
|
||||
.map(version => version.versionId);
|
||||
if (!baseLocalVersionIds.length) {
|
||||
return null;
|
||||
}
|
||||
return Math.max(...baseLocalVersionIds);
|
||||
})();
|
||||
|
||||
let dividerInserted = false;
|
||||
|
||||
const rowsMarkup = filteredVersions
|
||||
.map(version => {
|
||||
const isNewer =
|
||||
typeof latestLibraryVersionId === 'number' &&
|
||||
version.versionId > latestLibraryVersionId;
|
||||
let markup = '';
|
||||
if (
|
||||
!dividerInserted &&
|
||||
typeof dividerThresholdVersionId === 'number' &&
|
||||
!(version.versionId > dividerThresholdVersionId)
|
||||
) {
|
||||
dividerInserted = true;
|
||||
markup += '<div class="version-divider" role="presentation"></div>';
|
||||
}
|
||||
markup += renderRow(version, {
|
||||
latestLibraryVersionId,
|
||||
currentVersionId: normalizedCurrentVersionId,
|
||||
modelId: record?.modelId ?? modelId,
|
||||
});
|
||||
return markup;
|
||||
})
|
||||
.join('');
|
||||
|
||||
const listContent =
|
||||
rowsMarkup || renderFilteredEmptyState(currentBaseModelLabel);
|
||||
|
||||
container.innerHTML = `
|
||||
${renderToolbar(record, {
|
||||
displayMode,
|
||||
isFilteringActive,
|
||||
})}
|
||||
<div class="versions-list">
|
||||
${listContent}
|
||||
</div>
|
||||
`;
|
||||
|
||||
setupMediaHoverInteractions(container);
|
||||
}
|
||||
|
||||
async function loadVersions({ forceRefresh = false, eager = false } = {}) {
|
||||
if (controller.isLoading) {
|
||||
return;
|
||||
@@ -531,6 +672,17 @@ export function initVersionsTab({
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleVersionDisplayMode() {
|
||||
displayMode =
|
||||
displayMode === DISPLAY_FILTER_MODES.SAME_BASE
|
||||
? DISPLAY_FILTER_MODES.ANY
|
||||
: DISPLAY_FILTER_MODES.SAME_BASE;
|
||||
if (!controller.record) {
|
||||
return;
|
||||
}
|
||||
render(controller.record);
|
||||
}
|
||||
|
||||
async function handleToggleVersionIgnore(button, versionId) {
|
||||
if (!controller.record) {
|
||||
return;
|
||||
@@ -799,9 +951,17 @@ export function initVersionsTab({
|
||||
const toolbarAction = event.target.closest('[data-versions-action]');
|
||||
if (toolbarAction) {
|
||||
const action = toolbarAction.dataset.versionsAction;
|
||||
if (action === 'toggle-model-ignore') {
|
||||
event.preventDefault();
|
||||
await handleToggleModelIgnore(toolbarAction);
|
||||
switch (action) {
|
||||
case 'toggle-model-ignore':
|
||||
event.preventDefault();
|
||||
await handleToggleModelIgnore(toolbarAction);
|
||||
break;
|
||||
case 'toggle-version-display-mode':
|
||||
event.preventDefault();
|
||||
handleToggleVersionDisplayMode();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -412,6 +412,11 @@ export class SettingsManager {
|
||||
modelNameDisplaySelect.value = state.global.settings.model_name_display || 'model_name';
|
||||
}
|
||||
|
||||
const updateFlagStrategySelect = document.getElementById('updateFlagStrategy');
|
||||
if (updateFlagStrategySelect) {
|
||||
updateFlagStrategySelect.value = state.global.settings.update_flag_strategy || 'same_base';
|
||||
}
|
||||
|
||||
// Set optimize example images setting
|
||||
const optimizeExampleImagesCheckbox = document.getElementById('optimizeExampleImages');
|
||||
if (optimizeExampleImagesCheckbox) {
|
||||
@@ -1334,11 +1339,7 @@ export class SettingsManager {
|
||||
|
||||
showToast('toast.settings.settingsUpdated', { setting: settingKey.replace(/_/g, ' ') }, 'success');
|
||||
|
||||
if (settingKey === 'model_name_display') {
|
||||
this.reloadContent();
|
||||
}
|
||||
|
||||
if (settingKey === 'model_card_footer_action') {
|
||||
if (settingKey === 'model_name_display' || settingKey === 'model_card_footer_action' || settingKey === 'update_flag_strategy') {
|
||||
this.reloadContent();
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -33,6 +33,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
include_trigger_words: false,
|
||||
compact_mode: false,
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
update_flag_strategy: 'same_base',
|
||||
});
|
||||
|
||||
export function createDefaultSettings() {
|
||||
|
||||
@@ -320,6 +320,27 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Flag Strategy Section -->
|
||||
<div class="settings-section">
|
||||
<h3>{{ t('settings.sections.updateFlags') }}</h3>
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label for="updateFlagStrategy">{{ t('settings.updateFlagStrategy.label') }}</label>
|
||||
</div>
|
||||
<div class="setting-control select-control">
|
||||
<select id="updateFlagStrategy" onchange="settingsManager.saveSelectSetting('updateFlagStrategy', 'update_flag_strategy')">
|
||||
<option value="same_base">{{ t('settings.updateFlagStrategy.options.sameBase') }}</option>
|
||||
<option value="any">{{ t('settings.updateFlagStrategy.options.any') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-help">
|
||||
{{ t('settings.updateFlagStrategy.help') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Default Path Customization Section -->
|
||||
<div class="settings-section">
|
||||
<h3>{{ t('settings.downloadPathTemplates.title') }}</h3>
|
||||
|
||||
@@ -28,7 +28,14 @@ vi.mock(UI_HELPERS_MODULE, () => ({
|
||||
showToast: vi.fn(),
|
||||
}));
|
||||
|
||||
const stateMock = { global: { settings: { autoplay_on_hover: false } } };
|
||||
const stateMock = {
|
||||
global: {
|
||||
settings: {
|
||||
autoplay_on_hover: false,
|
||||
update_flag_strategy: 'any',
|
||||
},
|
||||
},
|
||||
};
|
||||
vi.mock(STATE_MODULE, () => ({
|
||||
state: stateMock,
|
||||
}));
|
||||
@@ -59,6 +66,7 @@ describe('ModelVersionsTab media rendering', () => {
|
||||
</div>
|
||||
`;
|
||||
stateMock.global.settings.autoplay_on_hover = false;
|
||||
stateMock.global.settings.update_flag_strategy = 'any';
|
||||
({ getModelApiClient } = await import(API_FACTORY_MODULE));
|
||||
fetchModelUpdateVersions = vi.fn();
|
||||
getModelApiClient.mockReturnValue({
|
||||
@@ -146,4 +154,133 @@ describe('ModelVersionsTab media rendering', () => {
|
||||
expect(imageElement?.getAttribute('src')).toBe(previewUrl);
|
||||
expect(document.querySelector('.version-media video')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('shows a stable label with a short state indicator', async () => {
|
||||
stateMock.global.settings.update_flag_strategy = 'any';
|
||||
fetchModelUpdateVersions.mockResolvedValue({
|
||||
success: true,
|
||||
record: {
|
||||
shouldIgnore: false,
|
||||
inLibraryVersionIds: [5],
|
||||
versions: [
|
||||
{
|
||||
versionId: 5,
|
||||
name: 'base',
|
||||
baseModel: 'SDXL',
|
||||
previewUrl: '/api/lm/previews/v-base.png',
|
||||
sizeBytes: 1024,
|
||||
isInLibrary: true,
|
||||
shouldIgnore: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
|
||||
const controller = initVersionsTab({
|
||||
modalId: 'model-versions-modal',
|
||||
modelType: 'loras',
|
||||
modelId: 321,
|
||||
currentVersionId: 5,
|
||||
});
|
||||
|
||||
await controller.load();
|
||||
|
||||
const toggleText = document.querySelector('.versions-filter-toggle .sr-only');
|
||||
expect(toggleText?.textContent?.trim()).toBe('Base filter: All versions');
|
||||
});
|
||||
|
||||
it('filters versions to the current base model when strategy is same_base', async () => {
|
||||
stateMock.global.settings.update_flag_strategy = 'same_base';
|
||||
fetchModelUpdateVersions.mockResolvedValue({
|
||||
success: true,
|
||||
record: {
|
||||
shouldIgnore: false,
|
||||
inLibraryVersionIds: [10],
|
||||
versions: [
|
||||
{
|
||||
versionId: 10,
|
||||
name: 'v1.0',
|
||||
baseModel: 'SDXL',
|
||||
previewUrl: '/api/lm/previews/v1.png',
|
||||
sizeBytes: 1024,
|
||||
isInLibrary: true,
|
||||
shouldIgnore: false,
|
||||
},
|
||||
{
|
||||
versionId: 11,
|
||||
name: 'v1.1',
|
||||
baseModel: 'Realistic',
|
||||
previewUrl: '/api/lm/previews/v1-1.png',
|
||||
sizeBytes: 2048,
|
||||
isInLibrary: false,
|
||||
shouldIgnore: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
|
||||
const controller = initVersionsTab({
|
||||
modalId: 'model-versions-modal',
|
||||
modelType: 'loras',
|
||||
modelId: 789,
|
||||
currentVersionId: 10,
|
||||
});
|
||||
|
||||
await controller.load();
|
||||
|
||||
expect(document.querySelectorAll('.model-version-row').length).toBe(1);
|
||||
});
|
||||
|
||||
it('toggle button can switch to display all versions', async () => {
|
||||
stateMock.global.settings.update_flag_strategy = 'same_base';
|
||||
fetchModelUpdateVersions.mockResolvedValue({
|
||||
success: true,
|
||||
record: {
|
||||
shouldIgnore: false,
|
||||
inLibraryVersionIds: [10],
|
||||
versions: [
|
||||
{
|
||||
versionId: 10,
|
||||
name: 'v1.0',
|
||||
baseModel: 'SDXL',
|
||||
previewUrl: '/api/lm/previews/v1.png',
|
||||
sizeBytes: 1024,
|
||||
isInLibrary: true,
|
||||
shouldIgnore: false,
|
||||
},
|
||||
{
|
||||
versionId: 11,
|
||||
name: 'v1.1',
|
||||
baseModel: 'Realistic',
|
||||
previewUrl: '/api/lm/previews/v1-1.png',
|
||||
sizeBytes: 2048,
|
||||
isInLibrary: false,
|
||||
shouldIgnore: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { initVersionsTab } = await import(MODEL_VERSIONS_MODULE);
|
||||
const controller = initVersionsTab({
|
||||
modalId: 'model-versions-modal',
|
||||
modelType: 'loras',
|
||||
modelId: 987,
|
||||
currentVersionId: 10,
|
||||
});
|
||||
|
||||
await controller.load();
|
||||
|
||||
expect(document.querySelectorAll('.model-version-row').length).toBe(1);
|
||||
const toggleButton = document.querySelector('[data-versions-action="toggle-version-display-mode"]');
|
||||
expect(toggleButton).toBeTruthy();
|
||||
const toggleTextBefore = document.querySelector('.versions-filter-toggle .sr-only');
|
||||
expect(toggleTextBefore?.textContent?.trim()).toContain('Same base');
|
||||
toggleButton?.click();
|
||||
expect(document.querySelectorAll('.model-version-row').length).toBe(2);
|
||||
const toggleTextAfter = document.querySelector('.versions-filter-toggle .sr-only');
|
||||
expect(toggleTextAfter?.textContent?.trim()).toContain('All versions');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ from py.services.model_query import (
|
||||
SearchStrategy,
|
||||
SortParams,
|
||||
)
|
||||
from py.services.model_update_service import ModelUpdateRecord, ModelVersionRecord
|
||||
from py.utils.models import BaseModelMetadata
|
||||
|
||||
|
||||
@@ -98,6 +99,25 @@ class StubUpdateService:
|
||||
return result
|
||||
|
||||
|
||||
class StubUpdateServiceWithRecords(StubUpdateService):
|
||||
def __init__(self, records, *, bulk_error: bool = False):
|
||||
decisions = {
|
||||
model_id: record.has_update()
|
||||
for model_id, record in records.items()
|
||||
}
|
||||
super().__init__(decisions, bulk_error=bulk_error)
|
||||
self.records = dict(records)
|
||||
self.records_bulk_calls = []
|
||||
|
||||
async def get_records_bulk(self, model_type, model_ids):
|
||||
self.records_bulk_calls.append((model_type, list(model_ids)))
|
||||
return {
|
||||
model_id: self.records[model_id]
|
||||
for model_id in model_ids
|
||||
if model_id in self.records
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_uses_injected_collaborators():
|
||||
data = [
|
||||
@@ -461,6 +481,198 @@ async def test_get_paginated_data_annotates_update_flags_with_bulk_dedup():
|
||||
assert response["total_pages"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_flag_strategy_same_base_prefers_matching_base():
|
||||
items = [
|
||||
{
|
||||
"model_name": "Pony Version",
|
||||
"civitai": {"modelId": 1, "id": 10, "baseModel": "Pony"},
|
||||
"base_model": "Pony",
|
||||
},
|
||||
{
|
||||
"model_name": "Flux Version",
|
||||
"civitai": {"modelId": 1, "id": 20, "baseModel": "Flux 1.D"},
|
||||
"base_model": "Flux 1.D",
|
||||
},
|
||||
]
|
||||
repository = StubRepository(items)
|
||||
filter_set = PassThroughFilterSet()
|
||||
search_strategy = NoSearchStrategy()
|
||||
record = ModelUpdateRecord(
|
||||
model_type="stub",
|
||||
model_id=1,
|
||||
versions=[
|
||||
ModelVersionRecord(
|
||||
version_id=10,
|
||||
name="Pony Local",
|
||||
base_model="Pony",
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=True,
|
||||
should_ignore=False,
|
||||
sort_index=0,
|
||||
),
|
||||
ModelVersionRecord(
|
||||
version_id=20,
|
||||
name="Flux Local",
|
||||
base_model="Flux 1.D",
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=True,
|
||||
should_ignore=False,
|
||||
sort_index=1,
|
||||
),
|
||||
ModelVersionRecord(
|
||||
version_id=30,
|
||||
name="Pony Remote",
|
||||
base_model="Pony",
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=False,
|
||||
should_ignore=False,
|
||||
sort_index=2,
|
||||
),
|
||||
ModelVersionRecord(
|
||||
version_id=40,
|
||||
name="SDXL Remote",
|
||||
base_model="SDXL",
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=False,
|
||||
should_ignore=False,
|
||||
sort_index=3,
|
||||
),
|
||||
],
|
||||
last_checked_at=None,
|
||||
should_ignore_model=False,
|
||||
)
|
||||
update_service = StubUpdateServiceWithRecords({1: record})
|
||||
settings = StubSettings({"update_flag_strategy": "same_base"})
|
||||
|
||||
service = DummyService(
|
||||
model_type="stub",
|
||||
scanner=object(),
|
||||
metadata_class=BaseModelMetadata,
|
||||
cache_repository=repository,
|
||||
filter_set=filter_set,
|
||||
search_strategy=search_strategy,
|
||||
settings_provider=settings,
|
||||
update_service=update_service,
|
||||
)
|
||||
|
||||
response = await service.get_paginated_data(
|
||||
page=1,
|
||||
page_size=10,
|
||||
sort_by="name:asc",
|
||||
)
|
||||
|
||||
assert update_service.records_bulk_calls == [("stub", [1])]
|
||||
assert update_service.bulk_calls == []
|
||||
assert len(response["items"]) == 2
|
||||
flags = {item["model_name"]: item["update_available"] for item in response["items"]}
|
||||
assert flags["Pony Version"] is True
|
||||
assert flags["Flux Version"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_flag_strategy_same_base_honors_latest_local_version():
|
||||
items = [
|
||||
{
|
||||
"model_name": "Pony v0.1",
|
||||
"civitai": {"modelId": 1, "id": 101, "baseModel": "Pony"},
|
||||
"base_model": "Pony",
|
||||
},
|
||||
{
|
||||
"model_name": "Pony v0.3",
|
||||
"civitai": {"modelId": 1, "id": 103, "baseModel": "Pony"},
|
||||
"base_model": "Pony",
|
||||
},
|
||||
]
|
||||
repository = StubRepository(items)
|
||||
filter_set = PassThroughFilterSet()
|
||||
search_strategy = NoSearchStrategy()
|
||||
record = ModelUpdateRecord(
|
||||
model_type="stub",
|
||||
model_id=1,
|
||||
versions=[
|
||||
ModelVersionRecord(
|
||||
version_id=101,
|
||||
name="Old Pony",
|
||||
base_model="Pony",
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=True,
|
||||
should_ignore=False,
|
||||
sort_index=0,
|
||||
),
|
||||
ModelVersionRecord(
|
||||
version_id=102,
|
||||
name="Pony Remote",
|
||||
base_model="Pony",
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=False,
|
||||
should_ignore=False,
|
||||
sort_index=1,
|
||||
),
|
||||
ModelVersionRecord(
|
||||
version_id=103,
|
||||
name="Middle Pony",
|
||||
base_model="Pony",
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=True,
|
||||
should_ignore=False,
|
||||
sort_index=2,
|
||||
),
|
||||
ModelVersionRecord(
|
||||
version_id=104,
|
||||
name="Latest Pony",
|
||||
base_model="Pony",
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
is_in_library=True,
|
||||
should_ignore=False,
|
||||
sort_index=3,
|
||||
),
|
||||
],
|
||||
last_checked_at=None,
|
||||
should_ignore_model=False,
|
||||
)
|
||||
update_service = StubUpdateServiceWithRecords({1: record})
|
||||
settings = StubSettings({"update_flag_strategy": "same_base"})
|
||||
|
||||
service = DummyService(
|
||||
model_type="stub",
|
||||
scanner=object(),
|
||||
metadata_class=BaseModelMetadata,
|
||||
cache_repository=repository,
|
||||
filter_set=filter_set,
|
||||
search_strategy=search_strategy,
|
||||
settings_provider=settings,
|
||||
update_service=update_service,
|
||||
)
|
||||
|
||||
response = await service.get_paginated_data(
|
||||
page=1,
|
||||
page_size=10,
|
||||
sort_by="name:asc",
|
||||
)
|
||||
|
||||
assert update_service.records_bulk_calls == [("stub", [1])]
|
||||
flags = {item["model_name"]: item["update_available"] for item in response["items"]}
|
||||
assert flags["Pony v0.1"] is False
|
||||
assert flags["Pony v0.3"] is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_paginated_data_filters_update_available_only():
|
||||
items = [
|
||||
|
||||
@@ -52,11 +52,11 @@ class NotFoundProvider:
|
||||
return {}
|
||||
|
||||
|
||||
def make_version(version_id, *, in_library, should_ignore=False):
|
||||
def make_version(version_id, *, in_library, base_model=None, should_ignore=False):
|
||||
return ModelVersionRecord(
|
||||
version_id=version_id,
|
||||
name=None,
|
||||
base_model=None,
|
||||
base_model=base_model,
|
||||
released_at=None,
|
||||
size_bytes=None,
|
||||
preview_url=None,
|
||||
@@ -147,6 +147,25 @@ def test_has_update_detects_newer_remote_version():
|
||||
assert record.has_update() is True
|
||||
|
||||
|
||||
def test_has_update_for_base_matches_same_base_model():
|
||||
record = make_record(
|
||||
make_version(5, in_library=True, base_model="Pony"),
|
||||
make_version(6, in_library=False, base_model="Pony"),
|
||||
make_version(7, in_library=False, base_model="Flux.1"),
|
||||
)
|
||||
|
||||
assert record.has_update_for_base(5, "Pony") is True
|
||||
|
||||
|
||||
def test_has_update_for_base_rejects_other_base_models():
|
||||
record = make_record(
|
||||
make_version(10, in_library=True, base_model="Flux"),
|
||||
make_version(20, in_library=False, base_model="SDXL"),
|
||||
)
|
||||
|
||||
assert record.has_update_for_base(10, "Flux") is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_refresh_persists_versions_and_uses_cache(tmp_path):
|
||||
db_path = tmp_path / "updates.sqlite"
|
||||
|
||||
Reference in New Issue
Block a user