mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-06-22 11:21:15 -03:00
Compare commits
16 Commits
818fa34a48
...
26be187d42
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26be187d42 | ||
|
|
d7caa1fa47 | ||
|
|
2629fcce23 | ||
|
|
438e7d07b9 | ||
|
|
e9932ea870 | ||
|
|
5dd8b96422 | ||
|
|
5e1cf68bbd | ||
|
|
1044fa3c83 | ||
|
|
397892bb7f | ||
|
|
f105500740 | ||
|
|
806555cf06 | ||
|
|
5cd7204101 | ||
|
|
3b602a3698 | ||
|
|
15dfaed462 | ||
|
|
0e51851025 | ||
|
|
0d0f4defca |
@@ -232,6 +232,8 @@
|
||||
"license": "Lizenz",
|
||||
"noCreditRequired": "Kein Credit erforderlich",
|
||||
"allowSellingGeneratedContent": "Verkauf erlaubt",
|
||||
"allowSellingGeneratedContentTooltip": "Verkauf generierter Bilder erlauben",
|
||||
"noCreditRequiredTooltip": "Modell ohne Nennung des Erstellers verwenden",
|
||||
"noTags": "Keine Tags",
|
||||
"autoTags": "Auto-Tags",
|
||||
"noBaseModelMatches": "Keine Basismodelle entsprechen der aktuellen Suche.",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen",
|
||||
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen"
|
||||
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen",
|
||||
"loraSyntaxFormat": "LoRA-Syntaxformat",
|
||||
"loraSyntaxFormatHelp": "LoRA-Syntaxformat. Der vollständige Pfad enthält den Unterordnerpfad (<lora:style/anime/x:1.0>) für verlustfreie Modellauflösung. Legacy verwendet nur den Dateinamen (<lora:x:1.0>) — A1111-Konvention, kann bei doppelten Dateinamen in verschiedenen Ordnern zu Mehrdeutigkeiten führen.",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "Vollständiger Pfad (Unterordner/Name)",
|
||||
"legacy": "Legacy A1111 (nur Name)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "Metadaten-Archiv-Datenbank aktivieren",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "Handlungsbedarf",
|
||||
"error": "Aktion erforderlich"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API Key"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "Model Cache Health"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "Duplicate Filename Conflicts"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI Version"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "Erneut ausführen",
|
||||
"exportBundle": "Paket exportieren"
|
||||
"exportBundle": "Paket exportieren",
|
||||
"open-settings": "Open Settings",
|
||||
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||
"repair-cache": "Rebuild Cache",
|
||||
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||
"reload-page": "Reload UI"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "Conflicts",
|
||||
"version": "Version"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "Diagnose konnte nicht geladen werden: {message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "Auflösung der Dateinamenskonflikte fehlgeschlagen: {message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "Dateinamenskonflikte auflösen",
|
||||
"message": "Umbenennen durch Anhängen eines 4-stelligen Hashs an jeden doppelten Dateinamen.",
|
||||
"note": "Dieser Vorgang benennt Dateien auf der Festplatte um. Modellreferenzen in vorhandenen Workflows müssen möglicherweise aktualisiert werden, wenn Sie das A1111-Syntaxformat verwenden.",
|
||||
"detail": "Beispiel: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "Benennt <strong>{count}</strong> Datei(en) in <strong>{groups}</strong> Duplikatgruppe(n) um",
|
||||
"confirm": "Dateien umbenennen",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "Anwendungs-Update erkannt",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "License",
|
||||
"noCreditRequired": "No Credit Required",
|
||||
"allowSellingGeneratedContent": "Allow Selling",
|
||||
"allowSellingGeneratedContentTooltip": "Allow selling generated images",
|
||||
"noCreditRequiredTooltip": "Use the model without crediting the creator",
|
||||
"noTags": "No tags",
|
||||
"autoTags": "Auto Tags",
|
||||
"noBaseModelMatches": "No base models match the current search.",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Include Trigger Words in LoRA Syntax",
|
||||
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard"
|
||||
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard",
|
||||
"loraSyntaxFormat": "LoRA Syntax Format",
|
||||
"loraSyntaxFormatHelp": "LoRA syntax format. Full includes subfolder path (<lora:style/anime/x:1.0>) for lossless model resolution. Legacy uses filename only (<lora:x:1.0>) — A1111 convention, may be ambiguous with duplicate filenames across folders.",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "Full path (subfolder/name)",
|
||||
"legacy": "Legacy A1111 (name only)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "Enable Metadata Archive Database",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "Needs Attention",
|
||||
"error": "Action Required"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API Key"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "Model Cache Health"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "Duplicate Filename Conflicts"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI Version"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "Run Again",
|
||||
"exportBundle": "Export Bundle"
|
||||
"exportBundle": "Export Bundle",
|
||||
"open-settings": "Open Settings",
|
||||
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||
"repair-cache": "Rebuild Cache",
|
||||
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||
"reload-page": "Reload UI"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "Conflicts",
|
||||
"version": "Version"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "Failed to load diagnostics: {message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "Failed to resolve filename conflicts: {message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "Resolve Filename Conflicts",
|
||||
"message": "Renaming by appending a 4-character hash to each duplicate filename.",
|
||||
"note": "This operation renames files on disk. Model references in existing workflows may need updating if you use the A1111 syntax format.",
|
||||
"detail": "Example: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "Will rename <strong>{count}</strong> file(s) across <strong>{groups}</strong> duplicate group(s).",
|
||||
"confirm": "Rename Files",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "Application Update Detected",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "Licencia",
|
||||
"noCreditRequired": "Sin crédito requerido",
|
||||
"allowSellingGeneratedContent": "Venta permitida",
|
||||
"allowSellingGeneratedContentTooltip": "Permitir la venta de imágenes generadas",
|
||||
"noCreditRequiredTooltip": "Usar el modelo sin atribuir al creador",
|
||||
"noTags": "Sin etiquetas",
|
||||
"autoTags": "Etiquetas automáticas",
|
||||
"noBaseModelMatches": "Ningún modelo base coincide con la búsqueda actual.",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA",
|
||||
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles"
|
||||
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles",
|
||||
"loraSyntaxFormat": "Formato de sintaxis LoRA",
|
||||
"loraSyntaxFormatHelp": "Formato de sintaxis LoRA. El formato completo incluye la ruta de la subcarpeta (<lora:style/anime/x:1.0>) para una resolución de modelo sin pérdidas. El formato heredado usa solo el nombre del archivo (<lora:x:1.0>) — convención A1111, puede ser ambiguo con nombres de archivo duplicados entre carpetas.",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "Ruta completa (subcarpeta/nombre)",
|
||||
"legacy": "A1111 heredado (solo nombre)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "Habilitar base de datos de archivo de metadatos",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "Requiere atención",
|
||||
"error": "Se requiere acción"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API Key"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "Model Cache Health"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "Duplicate Filename Conflicts"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI Version"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "Ejecutar de nuevo",
|
||||
"exportBundle": "Exportar paquete"
|
||||
"exportBundle": "Exportar paquete",
|
||||
"open-settings": "Open Settings",
|
||||
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||
"repair-cache": "Rebuild Cache",
|
||||
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||
"reload-page": "Reload UI"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "Conflicts",
|
||||
"version": "Version"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "Error al cargar los diagnósticos: {message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "Error al resolver conflictos de nombre de archivo: {message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "Resolver conflictos de nombres de archivo",
|
||||
"message": "Renombrar añadiendo un hash de 4 caracteres a cada nombre de archivo duplicado.",
|
||||
"note": "Esta operación renombra archivos en el disco. Es posible que las referencias a modelos en flujos de trabajo existentes deban actualizarse si usas el formato de sintaxis A1111.",
|
||||
"detail": "Ejemplo: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "Renombrará <strong>{count}</strong> archivo(s) en <strong>{groups}</strong> grupo(s) de duplicados",
|
||||
"confirm": "Renombrar archivos",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "Actualización de la aplicación detectada",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "Licence",
|
||||
"noCreditRequired": "Crédit non requis",
|
||||
"allowSellingGeneratedContent": "Vente autorisée",
|
||||
"allowSellingGeneratedContentTooltip": "Autoriser la vente d\"images générées",
|
||||
"noCreditRequiredTooltip": "Utiliser le modèle sans créditer le créateur",
|
||||
"noTags": "Aucun tag",
|
||||
"autoTags": "Auto-Tags",
|
||||
"noBaseModelMatches": "Aucun modèle de base ne correspond à la recherche actuelle.",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"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"
|
||||
"includeTriggerWordsHelp": "Inclure les mots-clés d'entraînement lors de la copie de la syntaxe LoRA dans le presse-papiers",
|
||||
"loraSyntaxFormat": "Format de syntaxe LoRA",
|
||||
"loraSyntaxFormatHelp": "Format de syntaxe LoRA. Le format complet inclut le chemin du sous-dossier (<lora:style/anime/x:1.0>) pour une résolution de modèle sans perte. Le format hérité utilise uniquement le nom du fichier (<lora:x:1.0>) — convention A1111, peut être ambiguë en cas de noms de fichiers en double dans différents dossiers.",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "Chemin complet (sous-dossier/nom)",
|
||||
"legacy": "A1111 hérité (nom uniquement)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "Activer la base de données d'archive des métadonnées",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "Nécessite une attention",
|
||||
"error": "Action requise"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API Key"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "Model Cache Health"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "Duplicate Filename Conflicts"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI Version"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "Relancer",
|
||||
"exportBundle": "Exporter le lot"
|
||||
"exportBundle": "Exporter le lot",
|
||||
"open-settings": "Open Settings",
|
||||
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||
"repair-cache": "Rebuild Cache",
|
||||
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||
"reload-page": "Reload UI"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "Conflicts",
|
||||
"version": "Version"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "Échec du chargement des diagnostics : {message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "Échec de la résolution des conflits de nom de fichier : {message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "Résoudre les conflits de noms de fichiers",
|
||||
"message": "Renommer en ajoutant un hachage de 4 caractères à chaque nom de fichier en double.",
|
||||
"note": "Cette opération renomme les fichiers sur le disque. Les références de modèle dans les workflows existants peuvent nécessiter une mise à jour si vous utilisez le format de syntaxe A1111.",
|
||||
"detail": "Exemple : <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "Renommera <strong>{count}</strong> fichier(s) dans <strong>{groups}</strong> groupe(s) de doublons",
|
||||
"confirm": "Renommer les fichiers",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "Mise à jour de l'application détectée",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "רישיון",
|
||||
"noCreditRequired": "ללא קרדיט נדרש",
|
||||
"allowSellingGeneratedContent": "אפשר מכירה",
|
||||
"allowSellingGeneratedContentTooltip": "אפשר מכירת תמונות שנוצרו",
|
||||
"noCreditRequiredTooltip": "שימוש במודל ללא מתן קרדיט ליוצר",
|
||||
"noTags": "ללא תגיות",
|
||||
"autoTags": "תגיות אוטומטיות",
|
||||
"noBaseModelMatches": "אין מודלי בסיס התואמים לחיפוש הנוכחי.",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "כלול מילות טריגר בתחביר LoRA",
|
||||
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח"
|
||||
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח",
|
||||
"loraSyntaxFormat": "פורמט תחביר LoRA",
|
||||
"loraSyntaxFormatHelp": "פורמט תחביר LoRA. נתיב מלא כולל תת-תיקייה (<lora:style/anime/x:1.0>) לפתרון מודל ללא אובדן. גרסה ישנה משתמשת בשם קובץ בלבד (<lora:x:1.0>) — מוסכמת A1111, עלולה להיות לא חד משמעית עם שמות קבצים כפולים בתיקיות שונות.",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "נתיב מלא (תת-תיקייה/שם)",
|
||||
"legacy": "A1111 ישן (שם בלבד)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "הפעל מסד נתונים של ארכיון מטא-דאטה",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "דורש תשומת לב",
|
||||
"error": "נדרשת פעולה"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API Key"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "Model Cache Health"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "Duplicate Filename Conflicts"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI Version"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "הפעל שוב",
|
||||
"exportBundle": "ייצוא חבילה"
|
||||
"exportBundle": "ייצוא חבילה",
|
||||
"open-settings": "Open Settings",
|
||||
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||
"repair-cache": "Rebuild Cache",
|
||||
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||
"reload-page": "Reload UI"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "Conflicts",
|
||||
"version": "Version"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "טעינת האבחון נכשלה: {message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "פתרון התנגשויות שמות קבצים נכשל: {message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "פתור התנגשויות בשמות קבצים",
|
||||
"message": "שינוי שם על ידי הוספת האש באורך 4 תווים לכל שם קובץ כפול.",
|
||||
"note": "פעולה זו משנה שמות של קבצים בדיסק. ייתכן שיהיה צורך לעדכן הפניות למודלים בזרימות עבודה קיימות אם אתה משתמש בפורמט התחביר A1111.",
|
||||
"detail": "דוגמה: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "ישנה שם של <strong>{count}</strong> קבצים ב-<strong>{groups}</strong> קבוצות כפולות",
|
||||
"confirm": "שנה שמות קבצים",
|
||||
"cancel": "ביטול"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "זוהה עדכון יישום",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "ライセンス",
|
||||
"noCreditRequired": "クレジット不要",
|
||||
"allowSellingGeneratedContent": "販売許可",
|
||||
"allowSellingGeneratedContentTooltip": "生成した画像の販売を許可",
|
||||
"noCreditRequiredTooltip": "クレジット表記なしでモデルを使用可能",
|
||||
"noTags": "タグなし",
|
||||
"autoTags": "自動タグ",
|
||||
"noBaseModelMatches": "現在の検索に一致するベースモデルはありません。",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "LoRA構文にトリガーワードを含める",
|
||||
"includeTriggerWordsHelp": "LoRA構文をクリップボードにコピーする際、学習済みトリガーワードを含めます"
|
||||
"includeTriggerWordsHelp": "LoRA構文をクリップボードにコピーする際、学習済みトリガーワードを含めます",
|
||||
"loraSyntaxFormat": "LoRA構文形式",
|
||||
"loraSyntaxFormatHelp": "LoRA構文形式。フルパスはサブフォルダパスを含み(<lora:style/anime/x:1.0>)、モデルをロスレスで解決します。レガシーはファイル名のみ(<lora:x:1.0>)— A1111規約ですが、フォルダ間でファイル名が重複する場合に曖昧になる可能性があります。",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "フルパス(サブフォルダ/名前)",
|
||||
"legacy": "レガシーA1111(名前のみ)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "メタデータアーカイブデータベースを有効化",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "要注意",
|
||||
"error": "対応が必要"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API キー"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "モデルキャッシュの健全性"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "ファイル名重複競合"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI バージョン"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "再実行",
|
||||
"exportBundle": "パッケージをエクスポート"
|
||||
"exportBundle": "パッケージをエクスポート",
|
||||
"open-settings": "設定を開く",
|
||||
"open-settings-syntax-format": "フルパス構文に切り替え",
|
||||
"repair-cache": "キャッシュを再構築",
|
||||
"resolve-filename-conflicts": "競合を解決",
|
||||
"reload-page": "UI をリロード"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "競合",
|
||||
"version": "バージョン"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "診断の読み込みに失敗しました: {message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "ファイル名競合の解決に失敗しました: {message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "ファイル名の競合を解決",
|
||||
"message": "重複したファイル名に4文字のハッシュを追加してリネームします。",
|
||||
"note": "この操作はディスク上のファイルをリネームします。A1111 構文形式を使用している場合、既存のワークフロー内のモデル参照を更新する必要があるかもしれません。",
|
||||
"detail": "例:<code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "<strong>{groups}</strong> 組の重複にわたって <strong>{count}</strong> 個のファイルをリネームします",
|
||||
"confirm": "ファイルをリネーム",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "アプリケーション更新が検出されました",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "라이선스",
|
||||
"noCreditRequired": "크레딧 표기 없음",
|
||||
"allowSellingGeneratedContent": "판매 허용",
|
||||
"allowSellingGeneratedContentTooltip": "생성된 이미지 판매 허용",
|
||||
"noCreditRequiredTooltip": "크리에이터 저작자 표시 없이 모델 사용 가능",
|
||||
"noTags": "태그 없음",
|
||||
"autoTags": "자동 태그",
|
||||
"noBaseModelMatches": "현재 검색과 일치하는 베이스 모델이 없습니다.",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "LoRA 문법에 트리거 단어 포함",
|
||||
"includeTriggerWordsHelp": "LoRA 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다"
|
||||
"includeTriggerWordsHelp": "LoRA 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다",
|
||||
"loraSyntaxFormat": "LoRA 구문 형식",
|
||||
"loraSyntaxFormatHelp": "LoRA 구문 형식. 전체 경로는 하위 폴더 경로(<lora:style/anime/x:1.0>)를 포함하여 손실 없는 모델 해상도를 제공합니다. 레거시는 파일 이름만(<lora:x:1.0>) 사용 — A1111 규칙이지만, 폴더 간 파일명 중복 시 모호할 수 있습니다.",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "전체 경로(하위 폴더/이름)",
|
||||
"legacy": "레거시 A1111(이름만)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "메타데이터 아카이브 데이터베이스 활성화",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "주의 필요",
|
||||
"error": "조치 필요"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API 키"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "모델 캐시 상태"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "파일명 중복 충돌"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI 버전"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "다시 실행",
|
||||
"exportBundle": "번들 내보내기"
|
||||
"exportBundle": "번들 내보내기",
|
||||
"open-settings": "설정 열기",
|
||||
"open-settings-syntax-format": "전체 경로 구문으로 전환",
|
||||
"repair-cache": "캐시 재구축",
|
||||
"resolve-filename-conflicts": "충돌 해결",
|
||||
"reload-page": "UI 새로고침"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "충돌",
|
||||
"version": "버전"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "진단 로드 실패: {message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "파일명 충돌 해결 실패: {message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "파일명 충돌 해결",
|
||||
"message": "중복 파일명에 4자리 해시를 추가하여 이름을 변경합니다.",
|
||||
"note": "이 작업은 디스크에 있는 파일의 이름을 변경합니다. A1111 구문 형식을 사용하는 경우 기존 워크플로우의 모델 참조를 업데이트해야 할 수 있습니다.",
|
||||
"detail": "예시: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "<strong>{groups}</strong>개 중복 그룹에서 <strong>{count}</strong>개 파일 이름을 변경합니다",
|
||||
"confirm": "파일 이름 변경",
|
||||
"cancel": "취소"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "애플리케이션 업데이트 감지",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "Лицензия",
|
||||
"noCreditRequired": "Без указания авторства",
|
||||
"allowSellingGeneratedContent": "Продажа разрешена",
|
||||
"allowSellingGeneratedContentTooltip": "Разрешить продажу сгенерированных изображений",
|
||||
"noCreditRequiredTooltip": "Использование модели без указания автора",
|
||||
"noTags": "Без тегов",
|
||||
"autoTags": "Авто-теги",
|
||||
"noBaseModelMatches": "Нет базовых моделей, соответствующих текущему поиску.",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "Включать триггерные слова в синтаксис LoRA",
|
||||
"includeTriggerWordsHelp": "Включать обученные триггерные слова при копировании синтаксиса LoRA в буфер обмена"
|
||||
"includeTriggerWordsHelp": "Включать обученные триггерные слова при копировании синтаксиса LoRA в буфер обмена",
|
||||
"loraSyntaxFormat": "Формат синтаксиса LoRA",
|
||||
"loraSyntaxFormatHelp": "Формат синтаксиса LoRA. Полный путь включает подпапку (<lora:style/anime/x:1.0>) для безпотерьного разрешения модели. Устаревший использует только имя файла (<lora:x:1.0>) — соглашение A1111, может быть неоднозначным при дублировании имён файлов в разных папках.",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "Полный путь (подпапка/имя)",
|
||||
"legacy": "Устаревший A1111 (только имя)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "Включить архив метаданных",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "Требует внимания",
|
||||
"error": "Требуется действие"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API Key"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "Model Cache Health"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "Duplicate Filename Conflicts"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI Version"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "Запустить снова",
|
||||
"exportBundle": "Экспортировать пакет"
|
||||
"exportBundle": "Экспортировать пакет",
|
||||
"open-settings": "Open Settings",
|
||||
"open-settings-syntax-format": "Switch to Full Path Syntax",
|
||||
"repair-cache": "Rebuild Cache",
|
||||
"resolve-filename-conflicts": "Resolve Conflicts",
|
||||
"reload-page": "Reload UI"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "Conflicts",
|
||||
"version": "Version"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "Не удалось загрузить диагностику: {message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "Не удалось разрешить конфликты имён файлов: {message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "Разрешить конфликты имён файлов",
|
||||
"message": "Переименование с добавлением 4-символьного хеша к каждому дублирующемуся имени файла.",
|
||||
"note": "Эта операция переименовывает файлы на диске. Если вы используете синтаксис A1111, ссылки на модели в существующих рабочих процессах могут потребовать обновления.",
|
||||
"detail": "Пример: <code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "Будет переименовано <strong>{count}</strong> файл(ов) в <strong>{groups}</strong> группе(ах) дубликатов",
|
||||
"confirm": "Переименовать файлы",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "Обнаружено обновление приложения",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "许可证",
|
||||
"noCreditRequired": "无需署名",
|
||||
"allowSellingGeneratedContent": "允许销售",
|
||||
"allowSellingGeneratedContentTooltip": "允许出售生成的图片",
|
||||
"noCreditRequiredTooltip": "使用模型时无需注明原作者",
|
||||
"noTags": "无标签",
|
||||
"autoTags": "自动标签",
|
||||
"noBaseModelMatches": "没有基础模型符合当前搜索。",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "复制 LoRA 语法时包含触发词",
|
||||
"includeTriggerWordsHelp": "复制 LoRA 语法到剪贴板时包含训练触发词"
|
||||
"includeTriggerWordsHelp": "复制 LoRA 语法到剪贴板时包含训练触发词",
|
||||
"loraSyntaxFormat": "LoRA 语法格式",
|
||||
"loraSyntaxFormatHelp": "LoRA 语法格式。完整路径(Full)包含子文件夹路径 (<lora:style/anime/x:1.0>),解析精确无歧义。旧版(Legacy)仅使用文件名 (<lora:x:1.0>)——A1111 原始约定,同名文件跨文件夹时可能产生歧义。",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "完整路径(子文件夹/名称)",
|
||||
"legacy": "旧版 A1111(仅名称)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "启用元数据归档数据库",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "需要关注",
|
||||
"error": "需要处理"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API 密钥"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "模型缓存健康状态"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "文件名重复冲突"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI 版本"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "重新检查",
|
||||
"exportBundle": "导出诊断包"
|
||||
"exportBundle": "导出诊断包",
|
||||
"open-settings": "打开设置",
|
||||
"open-settings-syntax-format": "切换为完整路径语法",
|
||||
"repair-cache": "重建缓存",
|
||||
"resolve-filename-conflicts": "解决冲突",
|
||||
"reload-page": "刷新 UI"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "冲突详情",
|
||||
"version": "版本信息"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "加载诊断结果失败:{message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "解决文件名冲突失败:{message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "解决文件名冲突",
|
||||
"message": "通过在每个重复文件名后附加 4 位哈希值来重命名文件。",
|
||||
"note": "此操作会重命名磁盘上的文件。如果使用 A1111 语法格式,现有工作流中的模型引用可能需要更新。",
|
||||
"detail": "示例:<code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "将重命名 <strong>{count}</strong> 个文件(共 <strong>{groups}</strong> 组重复)",
|
||||
"confirm": "重命名文件",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "检测到应用更新",
|
||||
|
||||
@@ -232,6 +232,8 @@
|
||||
"license": "授權",
|
||||
"noCreditRequired": "無需署名",
|
||||
"allowSellingGeneratedContent": "允許銷售",
|
||||
"allowSellingGeneratedContentTooltip": "允許出售生成的圖片",
|
||||
"noCreditRequiredTooltip": "使用模型時無需註明原作者",
|
||||
"noTags": "無標籤",
|
||||
"autoTags": "自動標籤",
|
||||
"noBaseModelMatches": "沒有基礎模型符合目前的搜尋。",
|
||||
@@ -577,7 +579,13 @@
|
||||
},
|
||||
"misc": {
|
||||
"includeTriggerWords": "在 LoRA 語法中包含觸發詞",
|
||||
"includeTriggerWordsHelp": "複製 LoRA 語法到剪貼簿時包含訓練觸發詞"
|
||||
"includeTriggerWordsHelp": "複製 LoRA 語法到剪貼簿時包含訓練觸發詞",
|
||||
"loraSyntaxFormat": "LoRA 語法格式",
|
||||
"loraSyntaxFormatHelp": "LoRA 語法格式。完整路徑(Full)包含子資料夾路徑 (<lora:style/anime/x:1.0>),解析精確無歧義。舊版(Legacy)僅使用檔名 (<lora:x:1.0>)——A1111 原始約定,同名檔案跨資料夾時可能產生歧義。",
|
||||
"loraSyntaxFormatOptions": {
|
||||
"full": "完整路徑(子資料夾/名稱)",
|
||||
"legacy": "舊版 A1111(僅名稱)"
|
||||
}
|
||||
},
|
||||
"metadataArchive": {
|
||||
"enableArchiveDb": "啟用中繼資料封存資料庫",
|
||||
@@ -1921,9 +1929,32 @@
|
||||
"warning": "需要注意",
|
||||
"error": "需要處理"
|
||||
},
|
||||
"issues": {
|
||||
"civitai_api_key": {
|
||||
"title": "Civitai API 金鑰"
|
||||
},
|
||||
"cache_health": {
|
||||
"title": "模型快取健康狀態"
|
||||
},
|
||||
"filename_conflicts": {
|
||||
"title": "檔案名稱重複衝突"
|
||||
},
|
||||
"ui_version": {
|
||||
"title": "UI 版本"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"runAgain": "重新執行",
|
||||
"exportBundle": "匯出套件"
|
||||
"exportBundle": "匯出套件",
|
||||
"open-settings": "開啟設定",
|
||||
"open-settings-syntax-format": "切換為完整路徑語法",
|
||||
"repair-cache": "重建快取",
|
||||
"resolve-filename-conflicts": "解決衝突",
|
||||
"reload-page": "重新載入 UI"
|
||||
},
|
||||
"labels": {
|
||||
"conflicts": "衝突詳情",
|
||||
"version": "版本"
|
||||
},
|
||||
"toast": {
|
||||
"loadFailed": "載入診斷失敗:{message}",
|
||||
@@ -1935,6 +1966,15 @@
|
||||
"conflictsResolveFailed": "解決檔案名稱衝突失敗:{message}"
|
||||
}
|
||||
},
|
||||
"conflictConfirm": {
|
||||
"title": "解決檔案名稱衝突",
|
||||
"message": "通過在每個重複檔案名稱後附加 4 位元哈希值來重新命名檔案。",
|
||||
"note": "此操作會重新命名磁碟上的檔案。如果使用 A1111 語法格式,現有工作流程中的模型參考可能需要更新。",
|
||||
"detail": "示例:<code>filename_v1.2</code> → <code>filename_v1.2-ab3c</code>",
|
||||
"impact": "將重新命名 <strong>{count}</strong> 個檔案(共 <strong>{groups}</strong> 組重複)",
|
||||
"confirm": "重新命名檔案",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"banners": {
|
||||
"versionMismatch": {
|
||||
"title": "偵測到應用程式更新",
|
||||
|
||||
@@ -9,6 +9,7 @@ from ..utils.utils import get_lora_info_absolute
|
||||
from .utils import (
|
||||
FlexibleOptionalInputType,
|
||||
any_type,
|
||||
apply_lora_syntax_format,
|
||||
detect_nunchaku_model_kind,
|
||||
extract_lora_name,
|
||||
get_loras_list,
|
||||
@@ -52,7 +53,7 @@ def _collect_widget_entries(kwargs):
|
||||
for lora in get_loras_list(kwargs):
|
||||
if not lora.get("active", False):
|
||||
continue
|
||||
lora_name = lora["name"]
|
||||
lora_name = apply_lora_syntax_format(lora["name"])
|
||||
model_strength = float(lora["strength"])
|
||||
clip_strength = float(lora.get("clipStrength", model_strength))
|
||||
lora_path, trigger_words = get_lora_info_absolute(lora_name)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
from ..utils.utils import get_lora_info
|
||||
from .utils import FlexibleOptionalInputType, any_type, extract_lora_name, get_loras_list
|
||||
from .utils import FlexibleOptionalInputType, any_type, apply_lora_syntax_format, extract_lora_name, get_loras_list
|
||||
|
||||
import logging
|
||||
|
||||
@@ -48,7 +48,7 @@ class LoraStackerLM:
|
||||
if not lora.get('active', False):
|
||||
continue
|
||||
|
||||
lora_name = lora['name']
|
||||
lora_name = apply_lora_syntax_format(lora['name'])
|
||||
model_strength = float(lora['strength'])
|
||||
# Get clip strength - use model strength as default if not specified
|
||||
clip_strength = float(lora.get('clipStrength', model_strength))
|
||||
|
||||
@@ -44,14 +44,29 @@ import folder_paths # type: ignore
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_lora_syntax_format():
|
||||
try:
|
||||
from ..services.settings_manager import get_settings_manager
|
||||
return get_settings_manager().get("lora_syntax_format", "legacy")
|
||||
except Exception:
|
||||
return "legacy"
|
||||
|
||||
|
||||
def apply_lora_syntax_format(name):
|
||||
fmt = get_lora_syntax_format()
|
||||
if fmt == "legacy":
|
||||
return name.replace("\\", "/").rstrip("/").split("/")[-1]
|
||||
return name
|
||||
|
||||
|
||||
def extract_lora_name(lora_path):
|
||||
normalized = lora_path.replace("\\", "/")
|
||||
basename = os.path.basename(normalized)
|
||||
name_no_ext = os.path.splitext(basename)[0]
|
||||
dirname = os.path.dirname(normalized)
|
||||
if dirname and dirname not in (".", "/") and not normalized.startswith("/"):
|
||||
return f"{dirname}/{name_no_ext}"
|
||||
return name_no_ext
|
||||
return apply_lora_syntax_format(f"{dirname}/{name_no_ext}")
|
||||
return apply_lora_syntax_format(name_no_ext)
|
||||
|
||||
|
||||
def get_loras_list(kwargs):
|
||||
|
||||
@@ -686,6 +686,9 @@ class DoctorHandler:
|
||||
)
|
||||
|
||||
async def resolve_filename_conflicts(self, request: web.Request) -> web.Response:
|
||||
if self._settings.get("lora_syntax_format", "legacy") == "full":
|
||||
return web.json_response({"success": True, "renamed": [], "count": 0})
|
||||
|
||||
renamed: list[dict[str, Any]] = []
|
||||
|
||||
try:
|
||||
@@ -990,6 +993,18 @@ class DoctorHandler:
|
||||
}
|
||||
|
||||
async def _check_filename_conflicts(self) -> dict[str, Any]:
|
||||
# When full path syntax is active, duplicate filenames across subfolders
|
||||
# are not ambiguous (<lora:subfolder/name:strength>), so skip the check.
|
||||
if self._settings.get("lora_syntax_format", "legacy") == "full":
|
||||
return {
|
||||
"id": "filename_conflicts",
|
||||
"title": "Duplicate Filename Conflicts",
|
||||
"status": "ok",
|
||||
"summary": "Full path syntax is active — duplicate filenames across folders are not ambiguous.",
|
||||
"details": [],
|
||||
"actions": [],
|
||||
}
|
||||
|
||||
all_conflicts: list[dict[str, Any]] = []
|
||||
total_conflict_groups = 0
|
||||
total_conflict_files = 0
|
||||
@@ -1048,12 +1063,22 @@ class DoctorHandler:
|
||||
"total_conflict_files": total_conflict_files,
|
||||
}
|
||||
]
|
||||
for conflict in all_conflicts:
|
||||
|
||||
# Show at most 5 conflict groups inline; note any remainder.
|
||||
MAX_VISIBLE_CONFLICTS = 5
|
||||
visible_conflicts = all_conflicts[:MAX_VISIBLE_CONFLICTS]
|
||||
for conflict in visible_conflicts:
|
||||
details.append(
|
||||
f"[{conflict['label']}] '{conflict['filename']}' "
|
||||
f"'{conflict['filename']}' "
|
||||
f"found in {len(conflict['paths'])} locations"
|
||||
)
|
||||
|
||||
hidden_count = len(all_conflicts) - MAX_VISIBLE_CONFLICTS
|
||||
if hidden_count > 0:
|
||||
details.append(
|
||||
f"...and {hidden_count} more duplicate filename group(s)"
|
||||
)
|
||||
|
||||
return {
|
||||
"id": "filename_conflicts",
|
||||
"title": "Duplicate Filename Conflicts",
|
||||
@@ -1064,7 +1089,11 @@ class DoctorHandler:
|
||||
{
|
||||
"id": "resolve-filename-conflicts",
|
||||
"label": "Resolve Conflicts",
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "open-settings-syntax-format",
|
||||
"label": "Switch to Full Path Syntax",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -1177,6 +1177,12 @@ class ModelQueryHandler:
|
||||
|
||||
async def find_filename_conflicts(self, request: web.Request) -> web.Response:
|
||||
try:
|
||||
settings = get_settings_manager()
|
||||
if settings.get("lora_syntax_format", "legacy") == "full":
|
||||
return web.json_response(
|
||||
{"success": True, "conflicts": [], "count": 0}
|
||||
)
|
||||
|
||||
duplicates = self._service.find_duplicate_filenames()
|
||||
result = []
|
||||
cache = await self._service.scanner.get_cached_data()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import mimetypes
|
||||
import urllib.parse
|
||||
from pathlib import Path
|
||||
|
||||
@@ -12,6 +13,12 @@ from ...config import config as global_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CHUNK_SIZE = 256 * 1024 # 256 KB
|
||||
|
||||
# Video file extensions that bypass native sendfile on Windows
|
||||
# to avoid IOCP/ProactorEventLoop crashes during client disconnect.
|
||||
_VIDEO_EXTENSIONS = frozenset({".mp4", ".webm", ".mov", ".avi", ".mkv"})
|
||||
|
||||
|
||||
class PreviewHandler:
|
||||
"""Serve preview assets for the active library at request time."""
|
||||
@@ -48,8 +55,51 @@ class PreviewHandler:
|
||||
logger.debug("Preview file not found at %s", str(resolved))
|
||||
raise web.HTTPNotFound(text="Preview file not found")
|
||||
|
||||
# Video files: stream manually to avoid Windows native sendfile crash.
|
||||
# aiohttp's FileResponse uses _sendfile_native on Windows (IOCP-based),
|
||||
# which breaks when the client disconnects mid-transfer — this happens
|
||||
# constantly when users scroll through a gallery of animated previews.
|
||||
suffix = resolved.suffix.lower()
|
||||
if suffix in _VIDEO_EXTENSIONS:
|
||||
return await self._stream_file(request, resolved)
|
||||
|
||||
# aiohttp's FileResponse handles range requests and content headers for us.
|
||||
return web.FileResponse(path=resolved, chunk_size=256 * 1024)
|
||||
return web.FileResponse(path=resolved, chunk_size=_CHUNK_SIZE)
|
||||
|
||||
async def _stream_file(
|
||||
self, request: web.Request, path: Path
|
||||
) -> web.StreamResponse:
|
||||
"""Stream a file chunk-by-chunk, bypassing native sendfile.
|
||||
|
||||
This avoids the Windows IOCP ``_sendfile_native`` crash that occurs
|
||||
when the client disconnects during a large file transfer.
|
||||
"""
|
||||
content_type, _ = mimetypes.guess_type(str(path))
|
||||
if content_type is None:
|
||||
content_type = "application/octet-stream"
|
||||
|
||||
file_size = path.stat().st_size
|
||||
resp = web.StreamResponse()
|
||||
resp.content_type = content_type
|
||||
resp.content_length = file_size
|
||||
|
||||
await resp.prepare(request)
|
||||
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
while True:
|
||||
chunk = f.read(_CHUNK_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
await resp.write(chunk)
|
||||
except (ConnectionResetError, ConnectionAbortedError):
|
||||
# Client disconnected during streaming — expected when scrolling
|
||||
# rapidly through a library with animated previews.
|
||||
pass
|
||||
except OSError as exc:
|
||||
logger.debug("I/O error streaming preview %s: %s", path, exc)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
__all__ = ["PreviewHandler"]
|
||||
|
||||
@@ -201,6 +201,29 @@ class CivitaiClient:
|
||||
|
||||
return _from_value(payload)
|
||||
|
||||
@staticmethod
|
||||
def _is_transient_server_error(message: str) -> bool:
|
||||
"""Return True when the message indicates a transient upstream failure.
|
||||
|
||||
Recognises Cloudflare 524, generic 5xx, and connectivity-level flakiness
|
||||
that should not be treated as a permanent failure.
|
||||
"""
|
||||
normalized = message.lower()
|
||||
if "status 5" in normalized or "status 524" in normalized:
|
||||
return True
|
||||
if any(
|
||||
keyword in normalized
|
||||
for keyword in (
|
||||
"connection refused",
|
||||
"connection reset",
|
||||
"temporary failure",
|
||||
"name resolution",
|
||||
"connection closed",
|
||||
)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def get_model_versions(self, model_id: str) -> Optional[Dict]:
|
||||
"""Get all versions of a model with local availability info"""
|
||||
try:
|
||||
@@ -223,6 +246,13 @@ class CivitaiClient:
|
||||
logger.info("Civitai request skipped: %s", OFFLINE_FRIENDLY_MESSAGE)
|
||||
return None
|
||||
if message:
|
||||
if self._is_transient_server_error(message):
|
||||
logger.info(
|
||||
"Transient server error for model %s: %s",
|
||||
model_id,
|
||||
message,
|
||||
)
|
||||
return None
|
||||
raise RuntimeError(message)
|
||||
return None
|
||||
except RateLimitError:
|
||||
@@ -532,6 +562,13 @@ class CivitaiClient:
|
||||
if not success:
|
||||
if is_expected_offline_error(result):
|
||||
return None
|
||||
if self._is_transient_server_error(str(result)):
|
||||
logger.info(
|
||||
"Transient server error fetching image info for ID %s: %s",
|
||||
image_id,
|
||||
result,
|
||||
)
|
||||
return None
|
||||
logger.error(
|
||||
"Failed to fetch image info for ID %s from civitai.red: %s",
|
||||
image_id,
|
||||
|
||||
@@ -1120,6 +1120,11 @@ class ModelScanner:
|
||||
if self._hash_index is None or self.model_type != "lora":
|
||||
return
|
||||
|
||||
# When full path syntax is active, duplicate filenames across subfolders
|
||||
# are fully qualified, so there is no ambiguity — skip the warning.
|
||||
if get_settings_manager().get("lora_syntax_format", "legacy") == "full":
|
||||
return
|
||||
|
||||
duplicates = self._hash_index.get_duplicate_filenames()
|
||||
if not duplicates:
|
||||
return
|
||||
|
||||
@@ -1000,12 +1000,11 @@ class ModelUpdateService:
|
||||
fallback_error_message = str(exc) or "resource not found"
|
||||
mark_model_as_ignored = True
|
||||
except Exception as exc: # pragma: no cover - defensive log
|
||||
logger.error(
|
||||
logger.warning(
|
||||
"Failed to fetch versions for model %s (%s): %s",
|
||||
model_id,
|
||||
model_type,
|
||||
exc,
|
||||
exc_info=True,
|
||||
)
|
||||
fallback_error_message = str(exc)
|
||||
if response is not None:
|
||||
|
||||
@@ -96,6 +96,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
"compact_mode": False,
|
||||
"priority_tags": DEFAULT_PRIORITY_TAG_CONFIG.copy(),
|
||||
"model_name_display": "model_name",
|
||||
"lora_syntax_format": "legacy",
|
||||
"model_card_footer_action": "replace_preview",
|
||||
"show_version_on_card": True,
|
||||
"update_flag_strategy": "same_base",
|
||||
|
||||
@@ -239,9 +239,9 @@ def _resolve_commercial_bits(values: Sequence[str]) -> int:
|
||||
normalized_values.add(normalized)
|
||||
|
||||
has_sell = "sell" in normalized_values
|
||||
has_rent = has_sell or "rent" in normalized_values
|
||||
has_rentcivit = has_rent or "rentcivit" in normalized_values
|
||||
has_image = has_sell or "image" in normalized_values
|
||||
has_rent = "rent" in normalized_values
|
||||
has_rentcivit = "rentcivit" in normalized_values
|
||||
has_image = "image" in normalized_values
|
||||
|
||||
commercial_bits = (
|
||||
(1 if has_sell else 0) << 3
|
||||
|
||||
@@ -33,6 +33,39 @@
|
||||
animation: modalFadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
#resolveFilenameConflictsModal .confirmation-message {
|
||||
color: var(--text-color);
|
||||
margin: var(--space-2) 0;
|
||||
font-size: 1em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#resolveFilenameConflictsModal .resolve-conflicts-detail {
|
||||
color: var(--text-color);
|
||||
margin: var(--space-2) 0;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
#resolveFilenameConflictsModal .resolve-conflicts-detail code {
|
||||
background: var(--lora-surface);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
border: 1px solid var(--lora-border);
|
||||
}
|
||||
|
||||
#resolveFilenameConflictsModal .resolve-conflicts-impact {
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--lora-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
padding: var(--space-2);
|
||||
margin: var(--space-2) 0;
|
||||
color: var(--text-color);
|
||||
text-align: left;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.delete-model-info,
|
||||
.exclude-model-info {
|
||||
/* Update info display styling */
|
||||
|
||||
@@ -1369,3 +1369,14 @@ input:checked + .toggle-slider:before {
|
||||
background: var(--lora-error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Highlight animation for setting items targeted from Doctor actions */
|
||||
@keyframes settings-highlight-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(from var(--lora-accent) r g b / 0.4); }
|
||||
50% { box-shadow: 0 0 0 4px rgba(from var(--lora-accent) r g b / 0.2); }
|
||||
}
|
||||
|
||||
.settings-setting-highlight {
|
||||
animation: settings-highlight-pulse 1.5s ease-in-out 3;
|
||||
border-radius: var(--border-radius-xs);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { showToast, copyToClipboard, sendLoraToWorkflow, buildLoraSyntax, getNSF
|
||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||
import { modalManager } from './ModalManager.js';
|
||||
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||
import { RecipeSidebarApiClient, updateRecipeMetadata } from '../api/recipeApi.js';
|
||||
import { RecipeSidebarApiClient, updateRecipeMetadata, extractRecipeId } from '../api/recipeApi.js';
|
||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||
import { BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||
import { getPriorityTagSuggestions } from '../utils/priorityTagHelpers.js';
|
||||
@@ -74,7 +74,7 @@ export class BulkManager {
|
||||
unfavorite: true
|
||||
},
|
||||
recipes: {
|
||||
addTags: false,
|
||||
addTags: true,
|
||||
sendToWorkflow: false,
|
||||
copyAll: false,
|
||||
refreshAll: false,
|
||||
@@ -1043,6 +1043,8 @@ export class BulkManager {
|
||||
cancelled = true;
|
||||
});
|
||||
|
||||
const isRecipes = state.currentPageType === 'recipes';
|
||||
|
||||
// Add or replace tags for each selected model based on mode
|
||||
for (const filePath of filePaths) {
|
||||
if (cancelled) {
|
||||
@@ -1050,7 +1052,9 @@ export class BulkManager {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
if (mode === 'replace') {
|
||||
if (isRecipes) {
|
||||
await this._saveRecipeTags(filePath, tags, mode);
|
||||
} else if (mode === 'replace') {
|
||||
await apiClient.saveModelMetadata(filePath, { tags: tags });
|
||||
} else {
|
||||
await apiClient.addTags(filePath, { tags: tags });
|
||||
@@ -1089,6 +1093,35 @@ export class BulkManager {
|
||||
}
|
||||
}
|
||||
|
||||
async _saveRecipeTags(filePath, newTags, mode) {
|
||||
const recipeId = extractRecipeId(filePath);
|
||||
if (!recipeId) throw new Error('Unable to determine recipe ID');
|
||||
|
||||
let finalTags = newTags;
|
||||
if (mode === 'append') {
|
||||
const recipeItem = state.virtualScroller?.items?.find(
|
||||
item => item.file_path === filePath
|
||||
);
|
||||
const existingTags = recipeItem?.tags || [];
|
||||
finalTags = [...new Set([...existingTags, ...newTags])];
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`/api/lm/recipe/${encodeURIComponent(recipeId)}/update`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tags: finalTags }),
|
||||
}
|
||||
);
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to update recipe tags');
|
||||
}
|
||||
|
||||
state.virtualScroller.updateSingleItem(filePath, { tags: finalTags });
|
||||
}
|
||||
|
||||
cleanupBulkAddTagsModal() {
|
||||
// Clear tags container
|
||||
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||
|
||||
@@ -225,6 +225,13 @@ export class DoctorManager {
|
||||
renderIssueCard(item) {
|
||||
const status = item.status || 'ok';
|
||||
const tagLabel = this.getStatusLabel(status);
|
||||
|
||||
const titleKey = `doctor.issues.${item.id || ''}.title`;
|
||||
const displayTitle = translate(titleKey, {}, item.title || '');
|
||||
|
||||
const summaryKey = `doctor.issues.${item.id || ''}.summary.${status}`;
|
||||
const displaySummary = translate(summaryKey, {}, item.summary || '');
|
||||
|
||||
const details = Array.isArray(item.details) ? item.details : [];
|
||||
const listItems = details
|
||||
.filter((detail) => typeof detail === 'string')
|
||||
@@ -235,19 +242,22 @@ export class DoctorManager {
|
||||
.map((detail) => this.renderInlineDetail(detail))
|
||||
.join('');
|
||||
const actions = (item.actions || [])
|
||||
.map((action) => `
|
||||
.map((action) => {
|
||||
const actionLabel = translate(`doctor.actions.${action.id}`, {}, action.label);
|
||||
return `
|
||||
<button class="${action.id === 'repair-cache' || action.id === 'reload-page' ? 'primary-btn' : 'secondary-btn'}" data-doctor-action="${escapeHtml(action.id)}">
|
||||
${escapeHtml(action.label)}
|
||||
${escapeHtml(actionLabel)}
|
||||
</button>
|
||||
`)
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `
|
||||
<section class="doctor-issue-card" data-status="${escapeHtml(status)}" data-issue-id="${escapeHtml(item.id || '')}">
|
||||
<div class="doctor-issue-header">
|
||||
<div>
|
||||
<h3>${escapeHtml(item.title || '')}</h3>
|
||||
<p class="doctor-issue-summary">${escapeHtml(item.summary || '')}</p>
|
||||
<h3>${escapeHtml(displayTitle)}</h3>
|
||||
<p class="doctor-issue-summary">${escapeHtml(displaySummary)}</p>
|
||||
</div>
|
||||
<span class="doctor-issue-tag">${escapeHtml(tagLabel)}</span>
|
||||
</div>
|
||||
@@ -262,7 +272,7 @@ export class DoctorManager {
|
||||
if (detail.conflict_groups || detail.total_conflict_files) {
|
||||
return `
|
||||
<div class="doctor-inline-detail">
|
||||
<strong>${escapeHtml(translate('doctor.status.warning', {}, 'Conflicts'))}</strong>
|
||||
<strong>${escapeHtml(translate('doctor.labels.conflicts', {}, 'Conflicts'))}</strong>
|
||||
<div>${escapeHtml(`${detail.conflict_groups || 0} filenames, ${detail.total_conflict_files || 0} files`)}</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -324,11 +334,42 @@ export class DoctorManager {
|
||||
}
|
||||
}, 100);
|
||||
break;
|
||||
case 'open-settings-syntax-format':
|
||||
modalManager.showModal('settingsModal');
|
||||
window.setTimeout(() => {
|
||||
// Switch to Interface section
|
||||
document.querySelectorAll('.settings-section').forEach((s) => s.classList.remove('active'));
|
||||
const interfaceSection = document.getElementById('section-interface');
|
||||
if (interfaceSection) {
|
||||
interfaceSection.classList.add('active');
|
||||
}
|
||||
document.querySelectorAll('.settings-nav-item').forEach((n) => n.classList.remove('active'));
|
||||
const interfaceNav = document.querySelector('.settings-nav-item[data-section="interface"]');
|
||||
if (interfaceNav) {
|
||||
interfaceNav.classList.add('active');
|
||||
}
|
||||
|
||||
// Focus and scroll to the LoRA Syntax Format dropdown
|
||||
const select = document.getElementById('loraSyntaxFormat');
|
||||
if (select) {
|
||||
select.focus();
|
||||
select.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// Add temporary highlight animation
|
||||
const settingItem = select.closest('.setting-item');
|
||||
if (settingItem) {
|
||||
settingItem.classList.add('settings-setting-highlight');
|
||||
setTimeout(() => {
|
||||
settingItem.classList.remove('settings-setting-highlight');
|
||||
}, 4500);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
break;
|
||||
case 'repair-cache':
|
||||
await this.repairCache();
|
||||
break;
|
||||
case 'resolve-filename-conflicts':
|
||||
await this.resolveFilenameConflicts();
|
||||
await this.promptResolveConflicts();
|
||||
break;
|
||||
case 'reload-page':
|
||||
this.reloadUi();
|
||||
@@ -358,6 +399,62 @@ export class DoctorManager {
|
||||
}
|
||||
}
|
||||
|
||||
_getConflictStats() {
|
||||
const conflict = (this.lastDiagnostics?.diagnostics || []).find(
|
||||
(d) => d.id === 'filename_conflicts'
|
||||
);
|
||||
if (!conflict || !Array.isArray(conflict.details)) {
|
||||
return { groups: 0, files: 0 };
|
||||
}
|
||||
const summary = conflict.details.find(
|
||||
(d) => d && typeof d === 'object' && d.conflict_groups !== undefined
|
||||
);
|
||||
return {
|
||||
groups: summary?.conflict_groups || 0,
|
||||
files: summary?.total_conflict_files || 0,
|
||||
};
|
||||
}
|
||||
|
||||
async promptResolveConflicts() {
|
||||
const stats = this._getConflictStats();
|
||||
if (stats.groups === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const detailEl = document.getElementById('resolveConflictsDetail');
|
||||
if (detailEl) {
|
||||
detailEl.innerHTML = translate(
|
||||
'conflictConfirm.detail',
|
||||
{},
|
||||
'Example: <code>Add_Details_v1.2</code> \u2192 <code>Add_Details_v1.2-a3f7</code>'
|
||||
);
|
||||
}
|
||||
|
||||
const impactEl = document.getElementById('resolveConflictsImpact');
|
||||
if (impactEl) {
|
||||
impactEl.innerHTML = translate(
|
||||
'conflictConfirm.impact',
|
||||
{ count: stats.files, groups: stats.groups },
|
||||
`Will rename <strong>${stats.files}</strong> file(s) across <strong>${stats.groups}</strong> duplicate group(s).`
|
||||
);
|
||||
}
|
||||
|
||||
this._confirmResolveResolve = null;
|
||||
modalManager.showModal('resolveFilenameConflictsModal');
|
||||
return new Promise((resolve) => {
|
||||
this._confirmResolveResolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
async confirmResolveConflicts() {
|
||||
modalManager.closeModal('resolveFilenameConflictsModal');
|
||||
if (this._confirmResolveResolve) {
|
||||
this._confirmResolveResolve(true);
|
||||
this._confirmResolveResolve = null;
|
||||
}
|
||||
await this.resolveFilenameConflicts();
|
||||
}
|
||||
|
||||
async resolveFilenameConflicts() {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
@@ -449,3 +546,8 @@ export class DoctorManager {
|
||||
}
|
||||
|
||||
export const doctorManager = new DoctorManager();
|
||||
|
||||
// Make available globally for HTML onclick handlers
|
||||
if (typeof window !== 'undefined') {
|
||||
window.doctorManager = doctorManager;
|
||||
}
|
||||
|
||||
@@ -316,6 +316,19 @@ export class ModalManager {
|
||||
});
|
||||
}
|
||||
|
||||
// Register resolveFilenameConflictsModal
|
||||
const resolveFilenameConflictsModal = document.getElementById('resolveFilenameConflictsModal');
|
||||
if (resolveFilenameConflictsModal) {
|
||||
this.registerModal('resolveFilenameConflictsModal', {
|
||||
element: resolveFilenameConflictsModal,
|
||||
onClose: () => {
|
||||
this.getModal('resolveFilenameConflictsModal').element.classList.remove('show');
|
||||
document.body.classList.remove('modal-open');
|
||||
},
|
||||
closeOnOutsideClick: true
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', this.boundHandleEscape);
|
||||
this.initialized = true;
|
||||
}
|
||||
@@ -396,7 +409,8 @@ export class ModalManager {
|
||||
id === "modelDuplicateDeleteModal" ||
|
||||
id === "clearCacheModal" ||
|
||||
id === "bulkDeleteModal" ||
|
||||
id === "checkUpdatesConfirmModal"
|
||||
id === "checkUpdatesConfirmModal" ||
|
||||
id === "resolveFilenameConflictsModal"
|
||||
) {
|
||||
modal.element.classList.add("show");
|
||||
} else {
|
||||
|
||||
@@ -295,6 +295,13 @@ export class SettingsManager {
|
||||
// Update state
|
||||
state.global.settings[settingKey] = value;
|
||||
|
||||
if (settingKey === 'lora_syntax_format') {
|
||||
try {
|
||||
localStorage.setItem('lm:lora-syntax-format-changed', Date.now().toString());
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isBackendSetting(settingKey)) {
|
||||
return;
|
||||
}
|
||||
@@ -949,6 +956,12 @@ export class SettingsManager {
|
||||
includeTriggerWordsCheckbox.checked = state.global.settings.include_trigger_words || false;
|
||||
}
|
||||
|
||||
// Set lora syntax format
|
||||
const loraSyntaxFormatSelect = document.getElementById('loraSyntaxFormat');
|
||||
if (loraSyntaxFormatSelect) {
|
||||
loraSyntaxFormatSelect.value = state.global.settings.lora_syntax_format || 'legacy';
|
||||
}
|
||||
|
||||
// Load metadata archive settings
|
||||
await this.loadMetadataArchiveSettings();
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
card_info_display: 'always',
|
||||
show_folder_sidebar: true,
|
||||
model_name_display: 'model_name',
|
||||
lora_syntax_format: 'legacy',
|
||||
model_card_footer_action: 'example_images',
|
||||
show_version_on_card: true,
|
||||
include_trigger_words: false,
|
||||
|
||||
@@ -420,12 +420,16 @@ export function getLoraStrengthsFromUsageTips(usageTips = {}) {
|
||||
export function buildLoraSyntax(fileName, usageTips = {}) {
|
||||
const { strength, hasStrength, clipStrength, hasClipStrength } = getLoraStrengthsFromUsageTips(usageTips);
|
||||
|
||||
const effectiveName = state.global.settings?.lora_syntax_format === 'legacy'
|
||||
? fileName.split('/').pop()
|
||||
: fileName;
|
||||
|
||||
if (hasClipStrength) {
|
||||
const modelStrength = hasStrength ? strength : 1;
|
||||
return `<lora:${fileName}:${modelStrength}:${clipStrength}>`;
|
||||
return `<lora:${effectiveName}:${modelStrength}:${clipStrength}>`;
|
||||
}
|
||||
|
||||
return `<lora:${fileName}:${strength}>`;
|
||||
return `<lora:${effectiveName}:${strength}>`;
|
||||
}
|
||||
|
||||
export function copyLoraSyntax(card) {
|
||||
|
||||
@@ -218,10 +218,10 @@
|
||||
<div class="filter-section">
|
||||
<h4>{{ t('header.filter.license') }}</h4>
|
||||
<div class="filter-tags">
|
||||
<div class="filter-tag license-tag" data-license="noCredit">
|
||||
<div class="filter-tag license-tag" data-license="noCredit" title="{{ t('header.filter.noCreditRequiredTooltip') }}">
|
||||
{{ t('header.filter.noCreditRequired') }}
|
||||
</div>
|
||||
<div class="filter-tag license-tag" data-license="allowSelling">
|
||||
<div class="filter-tag license-tag" data-license="allowSelling" title="{{ t('header.filter.allowSellingGeneratedContentTooltip') }}">
|
||||
{{ t('header.filter.allowSellingGeneratedContent') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -109,3 +109,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Resolve Filename Conflicts Confirmation Modal -->
|
||||
<div id="resolveFilenameConflictsModal" class="modal delete-modal">
|
||||
<div class="modal-content delete-modal-content">
|
||||
<h2>{{ t('conflictConfirm.title') }}</h2>
|
||||
<p class="confirmation-message">{{ t('conflictConfirm.message') }}</p>
|
||||
<p class="resolve-conflicts-detail" id="resolveConflictsDetail"></p>
|
||||
<div class="resolve-conflicts-impact" id="resolveConflictsImpact"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="cancel-btn" onclick="modalManager.closeModal('resolveFilenameConflictsModal')">{{ t('common.actions.cancel') }}</button>
|
||||
<button class="primary-btn" id="resolveConflictsConfirmBtn" onclick="doctorManager.confirmResolveConflicts()">
|
||||
<i class="fas fa-check"></i>
|
||||
{{ t('conflictConfirm.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -595,6 +595,22 @@
|
||||
<div class="settings-subsection-header">
|
||||
<h4>{{ t('settings.sections.misc') }}</h4>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
<label for="loraSyntaxFormat">
|
||||
{{ t('settings.misc.loraSyntaxFormat') }}
|
||||
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.misc.loraSyntaxFormatHelp') }}"></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-control select-control">
|
||||
<select id="loraSyntaxFormat" onchange="settingsManager.saveSelectSetting('loraSyntaxFormat', 'lora_syntax_format')">
|
||||
<option value="full">{{ t('settings.misc.loraSyntaxFormatOptions.full') }}</option>
|
||||
<option value="legacy">{{ t('settings.misc.loraSyntaxFormatOptions.legacy') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="setting-row">
|
||||
<div class="setting-info">
|
||||
|
||||
@@ -185,7 +185,7 @@ describe('AutoComplete widget interactions', () => {
|
||||
expect(fetchApiMock).toHaveBeenCalledWith(
|
||||
'/lm/loras/usage-tips-by-path?relative_path=models%2Fexample.safetensors',
|
||||
);
|
||||
expect(input.value).toContain('<lora:models/example:1.5:0.9>,');
|
||||
expect(input.value).toContain('<lora:example:1.5:0.9>,');
|
||||
expect(autoComplete.dropdown.style.display).toBe('none');
|
||||
expect(input.focus).toHaveBeenCalled();
|
||||
expect(input.setSelectionRange).toHaveBeenCalled();
|
||||
@@ -1624,8 +1624,8 @@ describe('AutoComplete widget interactions', () => {
|
||||
|
||||
await autoComplete.insertSelection('models/example.safetensors');
|
||||
|
||||
expect(input.value).toContain('<lora:models/example:1.2>');
|
||||
expect(input.value).not.toContain('<lora:models/example:1.2>,');
|
||||
expect(input.value).toContain('<lora:example:1.2>');
|
||||
expect(input.value).not.toContain('<lora:example:1.2>,');
|
||||
});
|
||||
|
||||
it('replaces entire phrase when selected tag ends with underscore version of search term (suffix match)', async () => {
|
||||
|
||||
@@ -65,32 +65,26 @@ async def test_allow_selling_filter():
|
||||
"""Test the allow selling generated content filtering logic."""
|
||||
service = DummyModelService()
|
||||
|
||||
# Create test data with different license flags
|
||||
# CommercialUse values are independent — Sell does NOT imply Image.
|
||||
test_data = [
|
||||
# Model allowing selling (contains Image in allowCommercialUse)
|
||||
{"file_path": "model1.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["Image"]})},
|
||||
# Model not allowing selling (doesn't contain Image in allowCommercialUse)
|
||||
{"file_path": "model2.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["RentCivit"]})},
|
||||
# Model with default license flags (includes Sell by default, which implies Image)
|
||||
{"file_path": "model3.safetensors", "license_flags": build_license_flags(None)},
|
||||
# Model allowing selling (contains Sell in allowCommercialUse, which implies Image)
|
||||
{"file_path": "model4.safetensors", "license_flags": build_license_flags({"allowCommercialUse": ["Sell"]})},
|
||||
# Model with empty allowCommercialUse (doesn't allow selling)
|
||||
{"file_path": "model5.safetensors", "license_flags": build_license_flags({"allowCommercialUse": []})},
|
||||
]
|
||||
|
||||
# Test allow_selling=True (should return models that allow selling - have Image permission)
|
||||
# Default and Sell permissions both include Image, so model3 and model4 will be included
|
||||
# Test allow_selling=True (should return only models with the Image permission)
|
||||
filtered = await service._apply_allow_selling_filter(test_data, allow_selling=True)
|
||||
assert len(filtered) == 3 # model1, model3 (default includes Sell which implies Image), model4
|
||||
assert len(filtered) == 1 # only model1 has Image permission
|
||||
file_paths = {item["file_path"] for item in filtered}
|
||||
assert file_paths == {"model1.safetensors", "model3.safetensors", "model4.safetensors"}
|
||||
assert file_paths == {"model1.safetensors"}
|
||||
|
||||
# Test allow_selling=False (should return models that don't allow selling - don't have Image permission)
|
||||
# Test allow_selling=False (should return models without the Image permission)
|
||||
filtered = await service._apply_allow_selling_filter(test_data, allow_selling=False)
|
||||
assert len(filtered) == 2 # model2 and model5
|
||||
assert len(filtered) == 4 # model2, model3, model4, model5
|
||||
file_paths = {item["file_path"] for item in filtered}
|
||||
assert file_paths == {"model2.safetensors", "model5.safetensors"}
|
||||
assert file_paths == {"model2.safetensors", "model3.safetensors", "model4.safetensors", "model5.safetensors"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
@@ -131,13 +131,12 @@ async def test_pool_filter_allow_selling_true(lora_service, sample_loras):
|
||||
filtered = await lora_service._apply_pool_filters(sample_loras, pool_config)
|
||||
|
||||
# Should keep models with Image permission (allowSelling)
|
||||
# Models: no_credit_required_for_selling, credit_required_for_selling, default_license
|
||||
assert len(filtered) == 3
|
||||
# Sell alone does not imply Image, so default_license is excluded.
|
||||
assert len(filtered) == 2
|
||||
file_names = {lora["file_name"] for lora in filtered}
|
||||
assert file_names == {
|
||||
"no_credit_required_for_selling.safetensors",
|
||||
"credit_required_for_selling.safetensors",
|
||||
"default_license.safetensors",
|
||||
}
|
||||
|
||||
|
||||
@@ -178,12 +177,11 @@ async def test_pool_filter_both_license_filters(lora_service, sample_loras):
|
||||
# Should keep models where both conditions are met:
|
||||
# - allowNoCredit=True (no credit required)
|
||||
# - Image permission exists (allow selling)
|
||||
# Models: no_credit_required_for_selling, default_license
|
||||
assert len(filtered) == 2
|
||||
# default_license has ["Sell"] without Image, so it's excluded.
|
||||
assert len(filtered) == 1
|
||||
file_names = {lora["file_name"] for lora in filtered}
|
||||
assert file_names == {
|
||||
"no_credit_required_for_selling.safetensors",
|
||||
"default_license.safetensors",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -132,7 +132,8 @@ async def test_initialize_cache_populates_cache(tmp_path: Path):
|
||||
_normalize_path(tmp_path / "one.txt"),
|
||||
_normalize_path(tmp_path / "nested" / "two.txt"),
|
||||
}
|
||||
assert {item["license_flags"] for item in cache.raw_data} == {DEFAULT_LICENSE_FLAGS}
|
||||
# build_license_flags({}) returns 113 (defaults: allowNoCredit + ["Sell"] + derivatives + differentLicense)
|
||||
assert {item["license_flags"] for item in cache.raw_data} == {113}
|
||||
|
||||
assert scanner._hash_index.get_path("hash-one") == _normalize_path(tmp_path / "one.txt")
|
||||
assert scanner._hash_index.get_path("hash-two") == _normalize_path(tmp_path / "nested" / "two.txt")
|
||||
@@ -190,7 +191,8 @@ async def test_initialize_in_background_applies_scan_result(tmp_path: Path, monk
|
||||
_normalize_path(tmp_path / "one.txt"),
|
||||
_normalize_path(tmp_path / "nested" / "two.txt"),
|
||||
}
|
||||
assert {item["license_flags"] for item in cache.raw_data} == {DEFAULT_LICENSE_FLAGS}
|
||||
# build_license_flags({}) returns 113 (defaults: allowNoCredit + ["Sell"] + derivatives + differentLicense)
|
||||
assert {item["license_flags"] for item in cache.raw_data} == {113}
|
||||
assert scanner._hash_index.get_path("hash-two") == _normalize_path(tmp_path / "nested" / "two.txt")
|
||||
assert scanner._tags_count == {"alpha": 1, "beta": 1}
|
||||
assert scanner._excluded_models == [_normalize_path(tmp_path / "skip-file.txt")]
|
||||
|
||||
@@ -16,7 +16,9 @@ def test_resolve_license_payload_defaults():
|
||||
assert payload["allowDerivatives"] is True
|
||||
assert payload["allowDifferentLicense"] is True
|
||||
assert payload["allowCommercialUse"] == ["Sell"]
|
||||
assert flags == 127
|
||||
# Default ["Sell"] only sets the Sell bit (16), plus NoCredit (1),
|
||||
# Derivatives (32) and DifferentLicense (64) = 113.
|
||||
assert flags == 113
|
||||
|
||||
|
||||
def test_build_license_flags_custom_values():
|
||||
@@ -34,11 +36,10 @@ def test_build_license_flags_custom_values():
|
||||
assert payload["allowDifferentLicense"] is False
|
||||
|
||||
flags = build_license_flags(source)
|
||||
# Sell automatically enables all commercial bits including image.
|
||||
assert flags == 30
|
||||
assert flags == 18
|
||||
|
||||
|
||||
def test_build_license_flags_respects_commercial_hierarchy():
|
||||
def test_build_license_flags_independent_values():
|
||||
base = {
|
||||
"allowNoCredit": False,
|
||||
"allowDerivatives": False,
|
||||
@@ -46,14 +47,10 @@ def test_build_license_flags_respects_commercial_hierarchy():
|
||||
}
|
||||
|
||||
assert build_license_flags({**base, "allowCommercialUse": []}) == 0
|
||||
# Rent adds rent and rentcivit permissions.
|
||||
assert build_license_flags({**base, "allowCommercialUse": ["Rent"]}) == 12
|
||||
# RentCivit alone should only set its own bit.
|
||||
assert build_license_flags({**base, "allowCommercialUse": ["Rent"]}) == 8
|
||||
assert build_license_flags({**base, "allowCommercialUse": ["RentCivit"]}) == 4
|
||||
# Image only toggles the image bit.
|
||||
assert build_license_flags({**base, "allowCommercialUse": ["Image"]}) == 2
|
||||
# Sell forces all commercial bits regardless of image listing.
|
||||
assert build_license_flags({**base, "allowCommercialUse": ["Sell"]}) == 30
|
||||
assert build_license_flags({**base, "allowCommercialUse": ["Sell"]}) == 16
|
||||
|
||||
|
||||
def test_build_license_flags_parses_aggregate_string():
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</div>
|
||||
<div class="section__toggles">
|
||||
<label class="toggle-item">
|
||||
<span class="toggle-item__label">No Credit Required</span>
|
||||
<span class="toggle-item__label" title="Use the model without crediting the creator">No Credit Required</span>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-switch"
|
||||
@@ -20,7 +20,7 @@
|
||||
</label>
|
||||
|
||||
<label class="toggle-item">
|
||||
<span class="toggle-item__label">Allow Selling</span>
|
||||
<span class="toggle-item__label" title="Allow selling generated images">Allow Selling</span>
|
||||
<button
|
||||
type="button"
|
||||
class="toggle-switch"
|
||||
|
||||
@@ -104,6 +104,66 @@ function removeLoraExtension(fileName = '') {
|
||||
return fileName.replace(/\.(safetensors|ckpt|pt|bin)$/i, '');
|
||||
}
|
||||
|
||||
let _loraSyntaxFormatCache = null;
|
||||
let _loraSyntaxFormatRefreshPromise = null;
|
||||
|
||||
function _getLoraSyntaxFormat() {
|
||||
if (_loraSyntaxFormatCache !== null) {
|
||||
return _loraSyntaxFormatCache;
|
||||
}
|
||||
return 'legacy';
|
||||
}
|
||||
|
||||
async function _fetchLoraSyntaxFormat() {
|
||||
try {
|
||||
const response = await api.fetchApi('/lm/settings');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.settings) {
|
||||
_loraSyntaxFormatCache = data.settings.lora_syntax_format || 'legacy';
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
if (_loraSyntaxFormatCache === null) {
|
||||
_loraSyntaxFormatCache = 'legacy';
|
||||
}
|
||||
}
|
||||
|
||||
function _triggerBackgroundRefresh() {
|
||||
if (_loraSyntaxFormatRefreshPromise) {
|
||||
return;
|
||||
}
|
||||
_loraSyntaxFormatRefreshPromise = _fetchLoraSyntaxFormat().finally(() => {
|
||||
_loraSyntaxFormatRefreshPromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
async function refreshLoraSyntaxFormat() {
|
||||
await _fetchLoraSyntaxFormat();
|
||||
}
|
||||
|
||||
function _initLoraSyntaxFormat() {
|
||||
_triggerBackgroundRefresh();
|
||||
}
|
||||
_initLoraSyntaxFormat();
|
||||
|
||||
function _initLoraSyntaxFormatReactive() {
|
||||
window.addEventListener('storage', (e) => {
|
||||
if (e.key === 'lm:lora-syntax-format-changed') {
|
||||
_triggerBackgroundRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
_triggerBackgroundRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
_initLoraSyntaxFormatReactive();
|
||||
|
||||
function parseSearchTokens(term = '') {
|
||||
const include = [];
|
||||
const exclude = [];
|
||||
@@ -231,6 +291,10 @@ const MODEL_BEHAVIORS = {
|
||||
const folder = directories.length ? directories.join('/') + '/' : '';
|
||||
const loraName = folder + baseName;
|
||||
|
||||
const resultName = _getLoraSyntaxFormat() === 'legacy'
|
||||
? baseName
|
||||
: loraName;
|
||||
|
||||
let strength = 1.0;
|
||||
let hasStrength = false;
|
||||
let clipStrength = null;
|
||||
@@ -265,9 +329,9 @@ const MODEL_BEHAVIORS = {
|
||||
}
|
||||
|
||||
if (clipStrength !== null) {
|
||||
return formatAutocompleteInsertion(`<lora:${loraName}:${strength}:${clipStrength}>`);
|
||||
return formatAutocompleteInsertion(`<lora:${resultName}:${strength}:${clipStrength}>`);
|
||||
}
|
||||
return formatAutocompleteInsertion(`<lora:${loraName}:${strength}>`);
|
||||
return formatAutocompleteInsertion(`<lora:${resultName}:${strength}>`);
|
||||
}
|
||||
},
|
||||
embeddings: {
|
||||
@@ -1433,6 +1497,11 @@ class AutoComplete {
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Prevent textarea from losing focus - same fix as createItemElement
|
||||
itemEl.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
itemEl.addEventListener('mouseenter', () => {
|
||||
this.selectItem(index, { manual: true });
|
||||
});
|
||||
@@ -2161,6 +2230,16 @@ class AutoComplete {
|
||||
item.appendChild(nameSpan);
|
||||
}
|
||||
|
||||
// Prevent textarea from losing focus when clicking dropdown items.
|
||||
// Without this, the blur event fires before click, and the blur handler's
|
||||
// formatAutocompleteTextOnBlur() modifies the text and triggers hide()
|
||||
// via suppressAutocompleteOnce, removing this item from the DOM before
|
||||
// the click handler can execute. This specifically breaks the case where
|
||||
// the text has a comma not followed by a space (e.g. "<lora:X:1>,search").
|
||||
item.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Hover and selection handlers
|
||||
item.addEventListener('mouseenter', () => {
|
||||
this.selectItem(index, { manual: true });
|
||||
@@ -2748,4 +2827,4 @@ class AutoComplete {
|
||||
}
|
||||
}
|
||||
|
||||
export { AutoComplete };
|
||||
export { AutoComplete, refreshLoraSyntaxFormat };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { forwardMiddleMouseToCanvas, forwardWheelToCanvas } from "./utils.js";
|
||||
import { copyToClipboard } from "./loras_widget_utils.js";
|
||||
|
||||
const MIN_HEIGHT = 150;
|
||||
const GROUP_EDITOR_ID = "lm-trigger-group-editor";
|
||||
@@ -568,6 +569,56 @@ function toggleGroupEditor(widget, index, anchorEl) {
|
||||
openGroupEditor(widget, index, anchorEl);
|
||||
}
|
||||
|
||||
function showTagContextMenu(event, tagData, index, widget, anchorEl) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
closeGroupEditor(widget);
|
||||
|
||||
const existingMenu = document.querySelector('.lm-lora-context-menu');
|
||||
if (existingMenu) {
|
||||
existingMenu.remove();
|
||||
}
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'lm-lora-context-menu';
|
||||
menu.style.left = `${event.clientX}px`;
|
||||
menu.style.top = `${event.clientY}px`;
|
||||
|
||||
const copyOption = document.createElement('div');
|
||||
copyOption.className = 'lm-lora-menu-item';
|
||||
copyOption.innerHTML = `<span class="lm-lora-menu-item-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></span>Copy`;
|
||||
copyOption.addEventListener('click', () => {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
copyToClipboard(tagData.text, 'Copied to clipboard');
|
||||
});
|
||||
menu.appendChild(copyOption);
|
||||
|
||||
if (isGroupTag(tagData) && Array.isArray(tagData.items) && tagData.items.length > 1) {
|
||||
const editOption = document.createElement('div');
|
||||
editOption.className = 'lm-lora-menu-item';
|
||||
editOption.innerHTML = `<span class="lm-lora-menu-item-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg></span>Edit Group`;
|
||||
editOption.addEventListener('click', () => {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
toggleGroupEditor(widget, index, anchorEl);
|
||||
renderGroupEditor(widget, tagData, index);
|
||||
});
|
||||
menu.appendChild(editOption);
|
||||
}
|
||||
|
||||
document.body.appendChild(menu);
|
||||
|
||||
const closeMenu = (e) => {
|
||||
if (!menu.contains(e.target)) {
|
||||
menu.remove();
|
||||
document.removeEventListener('click', closeMenu);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
||||
}
|
||||
|
||||
export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.02, options = {}) {
|
||||
const container = document.createElement("div");
|
||||
container.className = "comfy-tags-container";
|
||||
@@ -618,6 +669,10 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
||||
});
|
||||
});
|
||||
|
||||
tagEl.addEventListener("contextmenu", (e) => {
|
||||
showTagContextMenu(e, tagData, index, widget, tagEl);
|
||||
});
|
||||
|
||||
if (showStrengthInfo) {
|
||||
tagEl.addEventListener("wheel", (e) => {
|
||||
e.preventDefault();
|
||||
@@ -728,11 +783,13 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
||||
};
|
||||
|
||||
editButton.addEventListener("click", openEditor);
|
||||
groupChip.addEventListener("contextmenu", openEditor);
|
||||
|
||||
groupChip.appendChild(editButton);
|
||||
}
|
||||
|
||||
groupChip.addEventListener("contextmenu", (e) => {
|
||||
showTagContextMenu(e, tagData, index, widget, groupChip);
|
||||
});
|
||||
|
||||
groupChip.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (editButton && e.target === editButton) {
|
||||
@@ -773,6 +830,11 @@ export function addTagsWidget(node, name, opts, callback, wheelSensitivity = 0.0
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
|
||||
const existingMenu = document.querySelector('.lm-lora-context-menu');
|
||||
if (existingMenu) {
|
||||
existingMenu.remove();
|
||||
}
|
||||
|
||||
const normalizedTags = Array.isArray(tagsData) ? tagsData : [];
|
||||
const showStrengthInfo = widget.allowStrengthAdjustment ?? allowStrengthAdjustment;
|
||||
const groupAnchors = new Map();
|
||||
|
||||
@@ -398,13 +398,13 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section[data-v-dea4adf6] {
|
||||
.section[data-v-07ddd3df] {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.section__header[data-v-dea4adf6] {
|
||||
.section__header[data-v-07ddd3df] {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.section__title[data-v-dea4adf6] {
|
||||
.section__title[data-v-07ddd3df] {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
@@ -412,21 +412,21 @@
|
||||
color: var(--fg-color, #fff);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.section__toggles[data-v-dea4adf6] {
|
||||
.section__toggles[data-v-07ddd3df] {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
.toggle-item[data-v-dea4adf6] {
|
||||
.toggle-item[data-v-07ddd3df] {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-item__label[data-v-dea4adf6] {
|
||||
.toggle-item__label[data-v-07ddd3df] {
|
||||
font-size: 12px;
|
||||
color: var(--fg-color, #fff);
|
||||
}
|
||||
.toggle-switch[data-v-dea4adf6] {
|
||||
.toggle-switch[data-v-07ddd3df] {
|
||||
position: relative;
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
@@ -435,7 +435,7 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toggle-switch__track[data-v-dea4adf6] {
|
||||
.toggle-switch__track[data-v-07ddd3df] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--comfy-input-bg, #333);
|
||||
@@ -443,11 +443,11 @@
|
||||
border-radius: 10px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.toggle-switch--active .toggle-switch__track[data-v-dea4adf6] {
|
||||
.toggle-switch--active .toggle-switch__track[data-v-07ddd3df] {
|
||||
background: rgba(66, 153, 225, 0.3);
|
||||
border-color: rgba(66, 153, 225, 0.6);
|
||||
}
|
||||
.toggle-switch__thumb[data-v-dea4adf6] {
|
||||
.toggle-switch__thumb[data-v-07ddd3df] {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 2px;
|
||||
@@ -458,12 +458,12 @@
|
||||
transition: all 0.2s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.toggle-switch--active .toggle-switch__thumb[data-v-dea4adf6] {
|
||||
.toggle-switch--active .toggle-switch__thumb[data-v-07ddd3df] {
|
||||
transform: translateX(16px);
|
||||
background: #4299e1;
|
||||
opacity: 1;
|
||||
}
|
||||
.toggle-switch:hover .toggle-switch__thumb[data-v-dea4adf6] {
|
||||
.toggle-switch:hover .toggle-switch__thumb[data-v-07ddd3df] {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -2223,7 +2223,7 @@ to { transform: rotate(360deg);
|
||||
})();
|
||||
var _a;
|
||||
import { app as app$1 } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import { api as api$1 } from "../../../scripts/api.js";
|
||||
/**
|
||||
* @vue/shared v3.5.26
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
@@ -11094,7 +11094,10 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
|
||||
], -1)),
|
||||
createBaseVNode("div", _hoisted_2$e, [
|
||||
createBaseVNode("label", _hoisted_3$c, [
|
||||
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "toggle-item__label" }, "No Credit Required", -1)),
|
||||
_cache[3] || (_cache[3] = createBaseVNode("span", {
|
||||
class: "toggle-item__label",
|
||||
title: "Use the model without crediting the creator"
|
||||
}, "No Credit Required", -1)),
|
||||
createBaseVNode("button", {
|
||||
type: "button",
|
||||
class: normalizeClass(["toggle-switch", { "toggle-switch--active": __props.noCreditRequired }]),
|
||||
@@ -11107,7 +11110,10 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
|
||||
])], 10, _hoisted_4$a)
|
||||
]),
|
||||
createBaseVNode("label", _hoisted_5$8, [
|
||||
_cache[5] || (_cache[5] = createBaseVNode("span", { class: "toggle-item__label" }, "Allow Selling", -1)),
|
||||
_cache[5] || (_cache[5] = createBaseVNode("span", {
|
||||
class: "toggle-item__label",
|
||||
title: "Allow selling generated images"
|
||||
}, "Allow Selling", -1)),
|
||||
createBaseVNode("button", {
|
||||
type: "button",
|
||||
class: normalizeClass(["toggle-switch", { "toggle-switch--active": __props.allowSelling }]),
|
||||
@@ -11124,7 +11130,7 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
|
||||
};
|
||||
}
|
||||
});
|
||||
const LicenseSection = /* @__PURE__ */ _export_sfc(_sfc_main$i, [["__scopeId", "data-v-dea4adf6"]]);
|
||||
const LicenseSection = /* @__PURE__ */ _export_sfc(_sfc_main$i, [["__scopeId", "data-v-07ddd3df"]]);
|
||||
const _hoisted_1$e = { class: "preview" };
|
||||
const _hoisted_2$d = { class: "preview__title" };
|
||||
const _hoisted_3$b = ["disabled"];
|
||||
@@ -15031,6 +15037,54 @@ function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCa
|
||||
};
|
||||
}
|
||||
const app = {};
|
||||
const api = {
|
||||
fetchApi: (...args) => fetch(...args),
|
||||
addEventListener: (eventName, handler) => document.addEventListener(eventName, handler),
|
||||
removeEventListener: (eventName, handler) => document.removeEventListener(eventName, handler)
|
||||
};
|
||||
let _loraSyntaxFormatCache = null;
|
||||
let _loraSyntaxFormatRefreshPromise = null;
|
||||
async function _fetchLoraSyntaxFormat() {
|
||||
try {
|
||||
const response = await api.fetchApi("/lm/settings");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.success && data.settings) {
|
||||
_loraSyntaxFormatCache = data.settings.lora_syntax_format || "legacy";
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
if (_loraSyntaxFormatCache === null) {
|
||||
_loraSyntaxFormatCache = "legacy";
|
||||
}
|
||||
}
|
||||
function _triggerBackgroundRefresh() {
|
||||
if (_loraSyntaxFormatRefreshPromise) {
|
||||
return;
|
||||
}
|
||||
_loraSyntaxFormatRefreshPromise = _fetchLoraSyntaxFormat().finally(() => {
|
||||
_loraSyntaxFormatRefreshPromise = null;
|
||||
});
|
||||
}
|
||||
function _initLoraSyntaxFormat() {
|
||||
_triggerBackgroundRefresh();
|
||||
}
|
||||
_initLoraSyntaxFormat();
|
||||
function _initLoraSyntaxFormatReactive() {
|
||||
window.addEventListener("storage", (e) => {
|
||||
if (e.key === "lm:lora-syntax-format-changed") {
|
||||
_triggerBackgroundRefresh();
|
||||
}
|
||||
});
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.visibilityState === "visible") {
|
||||
_triggerBackgroundRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
_initLoraSyntaxFormatReactive();
|
||||
const ROOT_GRAPH_ID = "root";
|
||||
const LORA_PROVIDER_NODE_TYPES = [
|
||||
"Lora Stacker (LoraManager)",
|
||||
@@ -15407,7 +15461,7 @@ function createLoraRandomizerWidget(node) {
|
||||
const vueApp = createApp(LoraRandomizerWidget, {
|
||||
widget,
|
||||
node,
|
||||
api
|
||||
api: api$1
|
||||
});
|
||||
vueApp.use(PrimeVue, {
|
||||
unstyled: true,
|
||||
@@ -15482,7 +15536,7 @@ function createLoraCyclerWidget(node) {
|
||||
const vueApp = createApp(LoraCyclerWidget, {
|
||||
widget,
|
||||
node,
|
||||
api
|
||||
api: api$1
|
||||
});
|
||||
vueApp.use(PrimeVue, {
|
||||
unstyled: true,
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user