mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-23 22:22:11 -03:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c23ab04d90 | ||
|
|
d50dde6cf6 | ||
|
|
fcb1fb39be | ||
|
|
b0ef74f802 | ||
|
|
f332aef41d | ||
|
|
1f91a3da8e | ||
|
|
16840c321d | ||
|
|
c109e392ad | ||
|
|
5e69671366 | ||
|
|
52d23d9b75 | ||
|
|
8c6311355d |
@@ -34,6 +34,10 @@ Enhance your Civitai browsing experience with our companion browser extension! S
|
|||||||
|
|
||||||
## Release Notes
|
## Release Notes
|
||||||
|
|
||||||
|
### v0.9.2
|
||||||
|
* **Bulk Auto-Organization Action** - Added a new bulk auto-organization feature. You can now select multiple models and automatically organize them according to your current path template settings for streamlined management.
|
||||||
|
* **Bug Fixes** - Addressed several bugs to improve stability and reliability.
|
||||||
|
|
||||||
### v0.9.1
|
### v0.9.1
|
||||||
* **Enhanced Bulk Operations** - Improved bulk operations with Marquee Selection and a bulk operation context menu, providing a more intuitive, desktop-application-like user experience.
|
* **Enhanced Bulk Operations** - Improved bulk operations with Marquee Selection and a bulk operation context menu, providing a more intuitive, desktop-application-like user experience.
|
||||||
* **New Bulk Actions** - Added bulk operations for adding tags and setting base models to multiple models simultaneously.
|
* **New Bulk Actions** - Added bulk operations for adding tags and setting base models to multiple models simultaneously.
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
# i18n System Migration Complete
|
|
||||||
|
|
||||||
## 概要 (Summary)
|
|
||||||
|
|
||||||
成功完成了从JavaScript ES6模块到JSON格式的国际化系统迁移,包含完整的多语言翻译和代码更新。
|
|
||||||
|
|
||||||
Successfully completed the migration from JavaScript ES6 modules to JSON format for the internationalization system, including complete multilingual translations and code updates.
|
|
||||||
|
|
||||||
## 完成的工作 (Completed Work)
|
|
||||||
|
|
||||||
### 1. 文件结构重组 (File Structure Reorganization)
|
|
||||||
- **新建目录**: `/locales/` - 集中存放所有JSON翻译文件
|
|
||||||
- **移除目录**: `/static/js/i18n/locales/` - 删除了旧的JavaScript文件
|
|
||||||
|
|
||||||
### 2. 格式转换 (Format Conversion)
|
|
||||||
- **转换前**: ES6模块格式 (`export const en = { ... }`)
|
|
||||||
- **转换后**: 标准JSON格式 (`{ ... }`)
|
|
||||||
- **支持语言**: 9种语言完全转换
|
|
||||||
- English (en)
|
|
||||||
- 简体中文 (zh-CN)
|
|
||||||
- 繁體中文 (zh-TW)
|
|
||||||
- 日本語 (ja)
|
|
||||||
- Русский (ru)
|
|
||||||
- Deutsch (de)
|
|
||||||
- Français (fr)
|
|
||||||
- Español (es)
|
|
||||||
- 한국어 (ko)
|
|
||||||
|
|
||||||
### 3. 翻译完善 (Translation Completion)
|
|
||||||
- **翻译条目**: 每种语言386个翻译键值对
|
|
||||||
- **覆盖范围**: 完整覆盖所有UI元素
|
|
||||||
- **质量保证**: 所有翻译键在各语言间保持一致
|
|
||||||
|
|
||||||
### 4. JavaScript代码更新 (JavaScript Code Updates)
|
|
||||||
|
|
||||||
#### 主要修改文件: `static/js/i18n/index.js`
|
|
||||||
```javascript
|
|
||||||
// 旧版本: 静态导入
|
|
||||||
import { en } from './locales/en.js';
|
|
||||||
|
|
||||||
// 新版本: 动态JSON加载
|
|
||||||
async loadLocale(locale) {
|
|
||||||
const response = await fetch(`/locales/${locale}.json`);
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 核心功能更新:
|
|
||||||
- **构造函数**: 从静态导入改为配置驱动
|
|
||||||
- **语言加载**: 异步JSON获取机制
|
|
||||||
- **初始化**: 支持Promise-based的异步初始化
|
|
||||||
- **错误处理**: 增强的回退机制到英语
|
|
||||||
- **向后兼容**: 保持现有API接口不变
|
|
||||||
|
|
||||||
### 5. Python服务端更新 (Python Server-side Updates)
|
|
||||||
|
|
||||||
#### 修改文件: `py/services/server_i18n.py`
|
|
||||||
```python
|
|
||||||
# 旧版本: 解析JavaScript文件
|
|
||||||
def _load_locale_file(self, path, filename, locale_code):
|
|
||||||
# 复杂的JS到JSON转换逻辑
|
|
||||||
|
|
||||||
# 新版本: 直接加载JSON
|
|
||||||
def _load_locale_file(self, path, filename, locale_code):
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as f:
|
|
||||||
translations = json.load(f)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 路径更新:
|
|
||||||
- **旧路径**: `static/js/i18n/locales/*.js`
|
|
||||||
- **新路径**: `locales/*.json`
|
|
||||||
|
|
||||||
### 6. 服务器路由配置 (Server Route Configuration)
|
|
||||||
|
|
||||||
#### 修改文件: `standalone.py`
|
|
||||||
```python
|
|
||||||
# 新增静态路由服务JSON文件
|
|
||||||
app.router.add_static('/locales', locales_path)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 技术架构 (Technical Architecture)
|
|
||||||
|
|
||||||
### 前端 (Frontend)
|
|
||||||
```
|
|
||||||
Browser → JavaScript i18n Manager → fetch('/locales/{lang}.json') → JSON Response
|
|
||||||
```
|
|
||||||
|
|
||||||
### 后端 (Backend)
|
|
||||||
```
|
|
||||||
Python Server → ServerI18nManager → Direct JSON loading → Template Rendering
|
|
||||||
```
|
|
||||||
|
|
||||||
### 文件组织 (File Organization)
|
|
||||||
```
|
|
||||||
ComfyUI-Lora-Manager/
|
|
||||||
├── locales/ # 新的JSON翻译文件目录
|
|
||||||
│ ├── en.json # 英语翻译 (基准)
|
|
||||||
│ ├── zh-CN.json # 简体中文翻译
|
|
||||||
│ ├── zh-TW.json # 繁体中文翻译
|
|
||||||
│ ├── ja.json # 日语翻译
|
|
||||||
│ ├── ru.json # 俄语翻译
|
|
||||||
│ ├── de.json # 德语翻译
|
|
||||||
│ ├── fr.json # 法语翻译
|
|
||||||
│ ├── es.json # 西班牙语翻译
|
|
||||||
│ └── ko.json # 韩语翻译
|
|
||||||
├── static/js/i18n/
|
|
||||||
│ └── index.js # 更新的JavaScript i18n管理器
|
|
||||||
└── py/services/
|
|
||||||
└── server_i18n.py # 更新的Python服务端i18n
|
|
||||||
```
|
|
||||||
|
|
||||||
## 测试验证 (Testing & Validation)
|
|
||||||
|
|
||||||
### 测试脚本: `test_i18n.py`
|
|
||||||
```bash
|
|
||||||
🚀 Testing updated i18n system...
|
|
||||||
✅ All JSON locale files are valid (9 languages)
|
|
||||||
✅ Server-side i18n system working correctly
|
|
||||||
✅ All languages have complete translations (386 keys each)
|
|
||||||
🎉 All tests passed!
|
|
||||||
```
|
|
||||||
|
|
||||||
### 验证内容:
|
|
||||||
1. **JSON文件完整性**: 所有文件格式正确,语法有效
|
|
||||||
2. **翻译完整性**: 各语言翻译键值一致,无缺失
|
|
||||||
3. **服务端功能**: Python i18n服务正常加载和翻译
|
|
||||||
4. **参数插值**: 动态参数替换功能正常
|
|
||||||
|
|
||||||
## 优势与改进 (Benefits & Improvements)
|
|
||||||
|
|
||||||
### 1. 维护性提升
|
|
||||||
- **简化格式**: JSON比JavaScript对象更易于编辑和维护
|
|
||||||
- **工具支持**: 更好的编辑器语法高亮和验证支持
|
|
||||||
- **版本控制**: 更清晰的diff显示,便于追踪更改
|
|
||||||
|
|
||||||
### 2. 性能优化
|
|
||||||
- **按需加载**: 只加载当前所需语言,减少初始加载时间
|
|
||||||
- **缓存友好**: JSON文件可以被浏览器和CDN更好地缓存
|
|
||||||
- **压缩效率**: JSON格式压缩率通常更高
|
|
||||||
|
|
||||||
### 3. 开发体验
|
|
||||||
- **动态切换**: 支持运行时语言切换,无需重新加载页面
|
|
||||||
- **易于扩展**: 添加新语言只需增加JSON文件
|
|
||||||
- **调试友好**: 更容易定位翻译问题和缺失键
|
|
||||||
|
|
||||||
### 4. 部署便利
|
|
||||||
- **静态资源**: JSON文件可以作为静态资源部署
|
|
||||||
- **CDN支持**: 可以通过CDN分发翻译文件
|
|
||||||
- **版本管理**: 更容易管理不同版本的翻译
|
|
||||||
|
|
||||||
## 兼容性保证 (Compatibility Assurance)
|
|
||||||
|
|
||||||
- **API兼容**: 所有现有的JavaScript API保持不变
|
|
||||||
- **调用方式**: 现有代码无需修改即可工作
|
|
||||||
- **错误处理**: 增强的回退机制确保用户体验
|
|
||||||
- **性能**: 新系统性能与旧系统相当或更好
|
|
||||||
|
|
||||||
## 后续建议 (Future Recommendations)
|
|
||||||
|
|
||||||
1. **监控**: 部署后监控翻译加载性能和错误率
|
|
||||||
2. **优化**: 考虑实施翻译缓存策略以进一步提升性能
|
|
||||||
3. **扩展**: 可以考虑添加翻译管理界面,便于非技术人员更新翻译
|
|
||||||
4. **自动化**: 实施CI/CD流程自动验证翻译完整性
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**迁移完成时间**: 2024年
|
|
||||||
**影响文件数量**: 21个文件 (9个新JSON + 2个JS更新 + 1个Python更新 + 1个服务器配置)
|
|
||||||
**翻译键总数**: 386个 × 9种语言 = 3,474个翻译条目
|
|
||||||
**测试状态**: ✅ 全部通过
|
|
||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "Alle Syntax kopieren",
|
"copyAll": "Alle Syntax kopieren",
|
||||||
"refreshAll": "Alle Metadaten aktualisieren",
|
"refreshAll": "Alle Metadaten aktualisieren",
|
||||||
"moveAll": "Alle in Ordner verschieben",
|
"moveAll": "Alle in Ordner verschieben",
|
||||||
|
"autoOrganize": "Automatisch organisieren",
|
||||||
"deleteAll": "Alle Modelle löschen",
|
"deleteAll": "Alle Modelle löschen",
|
||||||
"clear": "Auswahl löschen"
|
"clear": "Auswahl löschen",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "Automatische Organisation wird initialisiert...",
|
||||||
|
"starting": "Automatische Organisation für {type} wird gestartet...",
|
||||||
|
"processing": "Verarbeitung ({processed}/{total}) – {success} verschoben, {skipped} übersprungen, {failures} fehlgeschlagen",
|
||||||
|
"cleaning": "Leere Verzeichnisse werden bereinigt...",
|
||||||
|
"completed": "Abgeschlossen: {success} verschoben, {skipped} übersprungen, {failures} fehlgeschlagen",
|
||||||
|
"complete": "Automatische Organisation abgeschlossen",
|
||||||
|
"error": "Fehler: {error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "Civitai-Daten aktualisieren",
|
"refreshMetadata": "Civitai-Daten aktualisieren",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "{completed} von {total} LoRAs heruntergeladen. {accessFailures} fehlgeschlagen aufgrund von Zugriffsbeschränkungen. Überprüfen Sie Ihren API-Schlüssel in den Einstellungen oder den Early Access-Status.",
|
"downloadPartialWithAccess": "{completed} von {total} LoRAs heruntergeladen. {accessFailures} fehlgeschlagen aufgrund von Zugriffsbeschränkungen. Überprüfen Sie Ihren API-Schlüssel in den Einstellungen oder den Early Access-Status.",
|
||||||
"pleaseSelectVersion": "Bitte wählen Sie eine Version aus",
|
"pleaseSelectVersion": "Bitte wählen Sie eine Version aus",
|
||||||
"versionExists": "Diese Version existiert bereits in Ihrer Bibliothek",
|
"versionExists": "Diese Version existiert bereits in Ihrer Bibliothek",
|
||||||
"downloadCompleted": "Download erfolgreich abgeschlossen"
|
"downloadCompleted": "Download erfolgreich abgeschlossen",
|
||||||
|
"autoOrganizeSuccess": "Automatische Organisation für {count} {type} erfolgreich abgeschlossen",
|
||||||
|
"autoOrganizePartialSuccess": "Automatische Organisation abgeschlossen: {success} verschoben, {failures} fehlgeschlagen von insgesamt {total} Modellen",
|
||||||
|
"autoOrganizeFailed": "Automatische Organisation fehlgeschlagen: {error}",
|
||||||
|
"noModelsSelected": "Keine Modelle ausgewählt"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "Fehler beim Abrufen der Rezepte: {message}",
|
"fetchFailed": "Fehler beim Abrufen der Rezepte: {message}",
|
||||||
|
|||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "Copy All Syntax",
|
"copyAll": "Copy All Syntax",
|
||||||
"refreshAll": "Refresh All Metadata",
|
"refreshAll": "Refresh All Metadata",
|
||||||
"moveAll": "Move All to Folder",
|
"moveAll": "Move All to Folder",
|
||||||
|
"autoOrganize": "Auto-Organize Selected",
|
||||||
"deleteAll": "Delete All Models",
|
"deleteAll": "Delete All Models",
|
||||||
"clear": "Clear Selection"
|
"clear": "Clear Selection",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "Initializing auto-organize...",
|
||||||
|
"starting": "Starting auto-organize for {type}...",
|
||||||
|
"processing": "Processing ({processed}/{total}) - {success} moved, {skipped} skipped, {failures} failed",
|
||||||
|
"cleaning": "Cleaning up empty directories...",
|
||||||
|
"completed": "Completed: {success} moved, {skipped} skipped, {failures} failed",
|
||||||
|
"complete": "Auto-organize complete",
|
||||||
|
"error": "Error: {error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "Refresh Civitai Data",
|
"refreshMetadata": "Refresh Civitai Data",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "Downloaded {completed} of {total} LoRAs. {accessFailures} failed due to access restrictions. Check your API key in settings or early access status.",
|
"downloadPartialWithAccess": "Downloaded {completed} of {total} LoRAs. {accessFailures} failed due to access restrictions. Check your API key in settings or early access status.",
|
||||||
"pleaseSelectVersion": "Please select a version",
|
"pleaseSelectVersion": "Please select a version",
|
||||||
"versionExists": "This version already exists in your library",
|
"versionExists": "This version already exists in your library",
|
||||||
"downloadCompleted": "Download completed successfully"
|
"downloadCompleted": "Download completed successfully",
|
||||||
|
"autoOrganizeSuccess": "Auto-organize completed successfully for {count} {type}",
|
||||||
|
"autoOrganizePartialSuccess": "Auto-organize completed with {success} moved, {failures} failed out of {total} models",
|
||||||
|
"autoOrganizeFailed": "Auto-organize failed: {error}",
|
||||||
|
"noModelsSelected": "No models selected"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "Failed to fetch recipes: {message}",
|
"fetchFailed": "Failed to fetch recipes: {message}",
|
||||||
|
|||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "Copiar toda la sintaxis",
|
"copyAll": "Copiar toda la sintaxis",
|
||||||
"refreshAll": "Actualizar todos los metadatos",
|
"refreshAll": "Actualizar todos los metadatos",
|
||||||
"moveAll": "Mover todos a carpeta",
|
"moveAll": "Mover todos a carpeta",
|
||||||
|
"autoOrganize": "Auto-organizar seleccionados",
|
||||||
"deleteAll": "Eliminar todos los modelos",
|
"deleteAll": "Eliminar todos los modelos",
|
||||||
"clear": "Limpiar selección"
|
"clear": "Limpiar selección",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "Inicializando auto-organización...",
|
||||||
|
"starting": "Iniciando auto-organización para {type}...",
|
||||||
|
"processing": "Procesando ({processed}/{total}) - {success} movidos, {skipped} omitidos, {failures} fallidos",
|
||||||
|
"cleaning": "Limpiando directorios vacíos...",
|
||||||
|
"completed": "Completado: {success} movidos, {skipped} omitidos, {failures} fallidos",
|
||||||
|
"complete": "Auto-organización completada",
|
||||||
|
"error": "Error: {error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "Actualizar datos de Civitai",
|
"refreshMetadata": "Actualizar datos de Civitai",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "Descargados {completed} de {total} LoRAs. {accessFailures} fallaron debido a restricciones de acceso. Revisa tu clave API en configuración o estado de acceso temprano.",
|
"downloadPartialWithAccess": "Descargados {completed} de {total} LoRAs. {accessFailures} fallaron debido a restricciones de acceso. Revisa tu clave API en configuración o estado de acceso temprano.",
|
||||||
"pleaseSelectVersion": "Por favor selecciona una versión",
|
"pleaseSelectVersion": "Por favor selecciona una versión",
|
||||||
"versionExists": "Esta versión ya existe en tu biblioteca",
|
"versionExists": "Esta versión ya existe en tu biblioteca",
|
||||||
"downloadCompleted": "Descarga completada exitosamente"
|
"downloadCompleted": "Descarga completada exitosamente",
|
||||||
|
"autoOrganizeSuccess": "Auto-organización completada exitosamente para {count} {type}",
|
||||||
|
"autoOrganizePartialSuccess": "Auto-organización completada con {success} movidos, {failures} fallidos de un total de {total} modelos",
|
||||||
|
"autoOrganizeFailed": "Auto-organización fallida: {error}",
|
||||||
|
"noModelsSelected": "No hay modelos seleccionados"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "Error al obtener recetas: {message}",
|
"fetchFailed": "Error al obtener recetas: {message}",
|
||||||
|
|||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "Copier toute la syntaxe",
|
"copyAll": "Copier toute la syntaxe",
|
||||||
"refreshAll": "Actualiser toutes les métadonnées",
|
"refreshAll": "Actualiser toutes les métadonnées",
|
||||||
"moveAll": "Déplacer tout vers un dossier",
|
"moveAll": "Déplacer tout vers un dossier",
|
||||||
|
"autoOrganize": "Auto-organiser la sélection",
|
||||||
"deleteAll": "Supprimer tous les modèles",
|
"deleteAll": "Supprimer tous les modèles",
|
||||||
"clear": "Effacer la sélection"
|
"clear": "Effacer la sélection",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "Initialisation de l'auto-organisation...",
|
||||||
|
"starting": "Démarrage de l'auto-organisation pour {type}...",
|
||||||
|
"processing": "Traitement ({processed}/{total}) - {success} déplacés, {skipped} ignorés, {failures} échecs",
|
||||||
|
"cleaning": "Nettoyage des répertoires vides...",
|
||||||
|
"completed": "Terminé : {success} déplacés, {skipped} ignorés, {failures} échecs",
|
||||||
|
"complete": "Auto-organisation terminée",
|
||||||
|
"error": "Erreur : {error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "Actualiser les données Civitai",
|
"refreshMetadata": "Actualiser les données Civitai",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "{completed} sur {total} LoRAs téléchargés. {accessFailures} ont échoué en raison de restrictions d'accès. Vérifiez votre clé API dans les paramètres ou le statut d'accès anticipé.",
|
"downloadPartialWithAccess": "{completed} sur {total} LoRAs téléchargés. {accessFailures} ont échoué en raison de restrictions d'accès. Vérifiez votre clé API dans les paramètres ou le statut d'accès anticipé.",
|
||||||
"pleaseSelectVersion": "Veuillez sélectionner une version",
|
"pleaseSelectVersion": "Veuillez sélectionner une version",
|
||||||
"versionExists": "Cette version existe déjà dans votre bibliothèque",
|
"versionExists": "Cette version existe déjà dans votre bibliothèque",
|
||||||
"downloadCompleted": "Téléchargement terminé avec succès"
|
"downloadCompleted": "Téléchargement terminé avec succès",
|
||||||
|
"autoOrganizeSuccess": "Auto-organisation terminée avec succès pour {count} {type}",
|
||||||
|
"autoOrganizePartialSuccess": "Auto-organisation terminée avec {success} déplacés, {failures} échecs sur {total} modèles",
|
||||||
|
"autoOrganizeFailed": "Échec de l'auto-organisation : {error}",
|
||||||
|
"noModelsSelected": "Aucun modèle sélectionné"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "Échec de la récupération des recipes : {message}",
|
"fetchFailed": "Échec de la récupération des recipes : {message}",
|
||||||
|
|||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "すべての構文をコピー",
|
"copyAll": "すべての構文をコピー",
|
||||||
"refreshAll": "すべてのメタデータを更新",
|
"refreshAll": "すべてのメタデータを更新",
|
||||||
"moveAll": "すべてをフォルダに移動",
|
"moveAll": "すべてをフォルダに移動",
|
||||||
|
"autoOrganize": "自動整理を実行",
|
||||||
"deleteAll": "すべてのモデルを削除",
|
"deleteAll": "すべてのモデルを削除",
|
||||||
"clear": "選択をクリア"
|
"clear": "選択をクリア",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "自動整理を初期化中...",
|
||||||
|
"starting": "{type}の自動整理を開始中...",
|
||||||
|
"processing": "処理中({processed}/{total})- {success} 移動、{skipped} スキップ、{failures} 失敗",
|
||||||
|
"cleaning": "空のディレクトリをクリーンアップ中...",
|
||||||
|
"completed": "完了:{success} 移動、{skipped} スキップ、{failures} 失敗",
|
||||||
|
"complete": "自動整理が完了しました",
|
||||||
|
"error": "エラー:{error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "Civitaiデータを更新",
|
"refreshMetadata": "Civitaiデータを更新",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "{total} LoRAのうち {completed} がダウンロードされました。{accessFailures} はアクセス制限により失敗しました。設定でAPIキーまたはアーリーアクセス状況を確認してください。",
|
"downloadPartialWithAccess": "{total} LoRAのうち {completed} がダウンロードされました。{accessFailures} はアクセス制限により失敗しました。設定でAPIキーまたはアーリーアクセス状況を確認してください。",
|
||||||
"pleaseSelectVersion": "バージョンを選択してください",
|
"pleaseSelectVersion": "バージョンを選択してください",
|
||||||
"versionExists": "このバージョンは既にライブラリに存在します",
|
"versionExists": "このバージョンは既にライブラリに存在します",
|
||||||
"downloadCompleted": "ダウンロードが正常に完了しました"
|
"downloadCompleted": "ダウンロードが正常に完了しました",
|
||||||
|
"autoOrganizeSuccess": "{count} {type} の自動整理が正常に完了しました",
|
||||||
|
"autoOrganizePartialSuccess": "自動整理が完了しました:{total} モデル中 {success} 移動、{failures} 失敗",
|
||||||
|
"autoOrganizeFailed": "自動整理に失敗しました:{error}",
|
||||||
|
"noModelsSelected": "モデルが選択されていません"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "レシピの取得に失敗しました:{message}",
|
"fetchFailed": "レシピの取得に失敗しました:{message}",
|
||||||
|
|||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "모든 문법 복사",
|
"copyAll": "모든 문법 복사",
|
||||||
"refreshAll": "모든 메타데이터 새로고침",
|
"refreshAll": "모든 메타데이터 새로고침",
|
||||||
"moveAll": "모두 폴더로 이동",
|
"moveAll": "모두 폴더로 이동",
|
||||||
|
"autoOrganize": "자동 정리 선택",
|
||||||
"deleteAll": "모든 모델 삭제",
|
"deleteAll": "모든 모델 삭제",
|
||||||
"clear": "선택 지우기"
|
"clear": "선택 지우기",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "자동 정리 초기화 중...",
|
||||||
|
"starting": "{type}에 대한 자동 정리 시작...",
|
||||||
|
"processing": "처리 중 ({processed}/{total}) - {success}개 이동, {skipped}개 건너뜀, {failures}개 실패",
|
||||||
|
"cleaning": "빈 디렉토리 정리 중...",
|
||||||
|
"completed": "완료: {success}개 이동, {skipped}개 건너뜀, {failures}개 실패",
|
||||||
|
"complete": "자동 정리 완료",
|
||||||
|
"error": "오류: {error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "Civitai 데이터 새로고침",
|
"refreshMetadata": "Civitai 데이터 새로고침",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "{total}개 중 {completed}개 LoRA가 다운로드되었습니다. {accessFailures}개는 액세스 제한으로 실패했습니다. 설정에서 API 키 또는 얼리 액세스 상태를 확인하세요.",
|
"downloadPartialWithAccess": "{total}개 중 {completed}개 LoRA가 다운로드되었습니다. {accessFailures}개는 액세스 제한으로 실패했습니다. 설정에서 API 키 또는 얼리 액세스 상태를 확인하세요.",
|
||||||
"pleaseSelectVersion": "버전을 선택해주세요",
|
"pleaseSelectVersion": "버전을 선택해주세요",
|
||||||
"versionExists": "이 버전은 이미 라이브러리에 있습니다",
|
"versionExists": "이 버전은 이미 라이브러리에 있습니다",
|
||||||
"downloadCompleted": "다운로드가 성공적으로 완료되었습니다"
|
"downloadCompleted": "다운로드가 성공적으로 완료되었습니다",
|
||||||
|
"autoOrganizeSuccess": "{count}개의 {type}에 대해 자동 정리가 성공적으로 완료되었습니다",
|
||||||
|
"autoOrganizePartialSuccess": "자동 정리 완료: 전체 {total}개 중 {success}개 이동, {failures}개 실패",
|
||||||
|
"autoOrganizeFailed": "자동 정리 실패: {error}",
|
||||||
|
"noModelsSelected": "선택된 모델이 없습니다"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "레시피 가져오기 실패: {message}",
|
"fetchFailed": "레시피 가져오기 실패: {message}",
|
||||||
|
|||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "Копировать весь синтаксис",
|
"copyAll": "Копировать весь синтаксис",
|
||||||
"refreshAll": "Обновить все метаданные",
|
"refreshAll": "Обновить все метаданные",
|
||||||
"moveAll": "Переместить все в папку",
|
"moveAll": "Переместить все в папку",
|
||||||
|
"autoOrganize": "Автоматически организовать выбранные",
|
||||||
"deleteAll": "Удалить все модели",
|
"deleteAll": "Удалить все модели",
|
||||||
"clear": "Очистить выбор"
|
"clear": "Очистить выбор",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "Инициализация автоматической организации...",
|
||||||
|
"starting": "Запуск автоматической организации для {type}...",
|
||||||
|
"processing": "Обработка ({processed}/{total}) — {success} перемещено, {skipped} пропущено, {failures} не удалось",
|
||||||
|
"cleaning": "Очистка пустых директорий...",
|
||||||
|
"completed": "Завершено: {success} перемещено, {skipped} пропущено, {failures} не удалось",
|
||||||
|
"complete": "Автоматическая организация завершена",
|
||||||
|
"error": "Ошибка: {error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "Обновить данные Civitai",
|
"refreshMetadata": "Обновить данные Civitai",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "Загружено {completed} из {total} LoRAs. {accessFailures} не удалось из-за ограничений доступа. Проверьте ваш API ключ в настройках или статус раннего доступа.",
|
"downloadPartialWithAccess": "Загружено {completed} из {total} LoRAs. {accessFailures} не удалось из-за ограничений доступа. Проверьте ваш API ключ в настройках или статус раннего доступа.",
|
||||||
"pleaseSelectVersion": "Пожалуйста, выберите версию",
|
"pleaseSelectVersion": "Пожалуйста, выберите версию",
|
||||||
"versionExists": "Эта версия уже существует в вашей библиотеке",
|
"versionExists": "Эта версия уже существует в вашей библиотеке",
|
||||||
"downloadCompleted": "Загрузка успешно завершена"
|
"downloadCompleted": "Загрузка успешно завершена",
|
||||||
|
"autoOrganizeSuccess": "Автоматическая организация успешно завершена для {count} {type}",
|
||||||
|
"autoOrganizePartialSuccess": "Автоматическая организация завершена: перемещено {success}, не удалось {failures} из {total} моделей",
|
||||||
|
"autoOrganizeFailed": "Ошибка автоматической организации: {error}",
|
||||||
|
"noModelsSelected": "Модели не выбраны"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "Не удалось получить рецепты: {message}",
|
"fetchFailed": "Не удалось получить рецепты: {message}",
|
||||||
|
|||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "复制全部语法",
|
"copyAll": "复制全部语法",
|
||||||
"refreshAll": "刷新全部元数据",
|
"refreshAll": "刷新全部元数据",
|
||||||
"moveAll": "全部移动到文件夹",
|
"moveAll": "全部移动到文件夹",
|
||||||
|
"autoOrganize": "自动整理所选模型",
|
||||||
"deleteAll": "删除所有模型",
|
"deleteAll": "删除所有模型",
|
||||||
"clear": "清除选择"
|
"clear": "清除选择",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "正在初始化自动整理...",
|
||||||
|
"starting": "正在为 {type} 启动自动整理...",
|
||||||
|
"processing": "处理中({processed}/{total})- 已移动 {success} 个,跳过 {skipped} 个,失败 {failures} 个",
|
||||||
|
"cleaning": "正在清理空文件夹...",
|
||||||
|
"completed": "完成:已移动 {success} 个,跳过 {skipped} 个,失败 {failures} 个",
|
||||||
|
"complete": "自动整理已完成",
|
||||||
|
"error": "错误:{error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "刷新 Civitai 数据",
|
"refreshMetadata": "刷新 Civitai 数据",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "已下载 {completed}/{total} 个 LoRA。{accessFailures} 个因访问限制失败。请检查设置中的 API 密钥或早期访问状态。",
|
"downloadPartialWithAccess": "已下载 {completed}/{total} 个 LoRA。{accessFailures} 个因访问限制失败。请检查设置中的 API 密钥或早期访问状态。",
|
||||||
"pleaseSelectVersion": "请选择版本",
|
"pleaseSelectVersion": "请选择版本",
|
||||||
"versionExists": "该版本已存在于你的库中",
|
"versionExists": "该版本已存在于你的库中",
|
||||||
"downloadCompleted": "下载成功完成"
|
"downloadCompleted": "下载成功完成",
|
||||||
|
"autoOrganizeSuccess": "自动整理已成功完成,共 {count} 个 {type}",
|
||||||
|
"autoOrganizePartialSuccess": "自动整理完成:已移动 {success} 个,{failures} 个失败,共 {total} 个模型",
|
||||||
|
"autoOrganizeFailed": "自动整理失败:{error}",
|
||||||
|
"noModelsSelected": "未选中模型"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "获取配方失败:{message}",
|
"fetchFailed": "获取配方失败:{message}",
|
||||||
|
|||||||
@@ -324,8 +324,18 @@
|
|||||||
"copyAll": "複製全部語法",
|
"copyAll": "複製全部語法",
|
||||||
"refreshAll": "刷新全部 metadata",
|
"refreshAll": "刷新全部 metadata",
|
||||||
"moveAll": "全部移動到資料夾",
|
"moveAll": "全部移動到資料夾",
|
||||||
|
"autoOrganize": "自動整理所選模型",
|
||||||
"deleteAll": "刪除全部模型",
|
"deleteAll": "刪除全部模型",
|
||||||
"clear": "清除選取"
|
"clear": "清除選取",
|
||||||
|
"autoOrganizeProgress": {
|
||||||
|
"initializing": "正在初始化自動整理...",
|
||||||
|
"starting": "正在開始自動整理 {type}...",
|
||||||
|
"processing": "處理中({processed}/{total})- 已移動 {success},已略過 {skipped},失敗 {failures}",
|
||||||
|
"cleaning": "正在清理空資料夾...",
|
||||||
|
"completed": "完成:已移動 {success},已略過 {skipped},失敗 {failures}",
|
||||||
|
"complete": "自動整理完成",
|
||||||
|
"error": "錯誤:{error}"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"contextMenu": {
|
"contextMenu": {
|
||||||
"refreshMetadata": "刷新 Civitai 資料",
|
"refreshMetadata": "刷新 Civitai 資料",
|
||||||
@@ -942,7 +952,11 @@
|
|||||||
"downloadPartialWithAccess": "已下載 {completed} 個 LoRA,共 {total} 個。{accessFailures} 個因訪問限制而失敗。請檢查您的 API 密鑰或提前訪問狀態。",
|
"downloadPartialWithAccess": "已下載 {completed} 個 LoRA,共 {total} 個。{accessFailures} 個因訪問限制而失敗。請檢查您的 API 密鑰或提前訪問狀態。",
|
||||||
"pleaseSelectVersion": "請選擇一個版本",
|
"pleaseSelectVersion": "請選擇一個版本",
|
||||||
"versionExists": "此版本已存在於您的庫中",
|
"versionExists": "此版本已存在於您的庫中",
|
||||||
"downloadCompleted": "下載成功完成"
|
"downloadCompleted": "下載成功完成",
|
||||||
|
"autoOrganizeSuccess": "自動整理已成功完成,共 {count} 個 {type} 已整理",
|
||||||
|
"autoOrganizePartialSuccess": "自動整理完成:已移動 {success} 個,{failures} 個失敗,共 {total} 個模型",
|
||||||
|
"autoOrganizeFailed": "自動整理失敗:{error}",
|
||||||
|
"noModelsSelected": "未選擇任何模型"
|
||||||
},
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"fetchFailed": "取得配方失敗:{message}",
|
"fetchFailed": "取得配方失敗:{message}",
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ class LoraManager:
|
|||||||
# Run post-initialization tasks
|
# Run post-initialization tasks
|
||||||
post_tasks = [
|
post_tasks = [
|
||||||
asyncio.create_task(cls._cleanup_backup_files(), name='cleanup_bak_files'),
|
asyncio.create_task(cls._cleanup_backup_files(), name='cleanup_bak_files'),
|
||||||
|
asyncio.create_task(cls._cleanup_example_images_folders(), name='cleanup_example_images'),
|
||||||
# Add more post-initialization tasks here as needed
|
# Add more post-initialization tasks here as needed
|
||||||
# asyncio.create_task(cls._another_post_task(), name='another_task'),
|
# asyncio.create_task(cls._another_post_task(), name='another_task'),
|
||||||
]
|
]
|
||||||
@@ -346,6 +347,123 @@ class LoraManager:
|
|||||||
|
|
||||||
return deleted_count, size_freed
|
return deleted_count, size_freed
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _cleanup_example_images_folders(cls):
|
||||||
|
"""Clean up invalid or empty folders in example images directory"""
|
||||||
|
try:
|
||||||
|
example_images_path = settings.get('example_images_path')
|
||||||
|
if not example_images_path or not os.path.exists(example_images_path):
|
||||||
|
logger.debug("Example images path not configured or doesn't exist, skipping cleanup")
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.debug(f"Starting cleanup of example images folders in: {example_images_path}")
|
||||||
|
|
||||||
|
# Get all scanner instances to check hash validity
|
||||||
|
lora_scanner = await ServiceRegistry.get_lora_scanner()
|
||||||
|
checkpoint_scanner = await ServiceRegistry.get_checkpoint_scanner()
|
||||||
|
embedding_scanner = await ServiceRegistry.get_embedding_scanner()
|
||||||
|
|
||||||
|
total_folders_checked = 0
|
||||||
|
empty_folders_removed = 0
|
||||||
|
invalid_hash_folders_removed = 0
|
||||||
|
|
||||||
|
# Scan the example images directory
|
||||||
|
try:
|
||||||
|
with os.scandir(example_images_path) as it:
|
||||||
|
for entry in it:
|
||||||
|
if not entry.is_dir(follow_symlinks=False):
|
||||||
|
continue
|
||||||
|
|
||||||
|
folder_name = entry.name
|
||||||
|
folder_path = entry.path
|
||||||
|
total_folders_checked += 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Check if folder is empty
|
||||||
|
is_empty = cls._is_folder_empty(folder_path)
|
||||||
|
if is_empty:
|
||||||
|
logger.debug(f"Removing empty example images folder: {folder_name}")
|
||||||
|
await cls._remove_folder_safely(folder_path)
|
||||||
|
empty_folders_removed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if folder name is a valid SHA256 hash (64 hex characters)
|
||||||
|
if len(folder_name) != 64 or not all(c in '0123456789abcdefABCDEF' for c in folder_name):
|
||||||
|
logger.debug(f"Removing invalid hash folder: {folder_name}")
|
||||||
|
await cls._remove_folder_safely(folder_path)
|
||||||
|
invalid_hash_folders_removed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if hash exists in any of the scanners
|
||||||
|
hash_exists = (
|
||||||
|
lora_scanner.has_hash(folder_name) or
|
||||||
|
checkpoint_scanner.has_hash(folder_name) or
|
||||||
|
embedding_scanner.has_hash(folder_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not hash_exists:
|
||||||
|
logger.debug(f"Removing example images folder for deleted model: {folder_name}")
|
||||||
|
await cls._remove_folder_safely(folder_path)
|
||||||
|
invalid_hash_folders_removed += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
logger.debug(f"Keeping valid example images folder: {folder_name}")
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing example images folder {folder_name}: {e}")
|
||||||
|
|
||||||
|
# Yield control periodically
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error scanning example images directory: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Log final cleanup report
|
||||||
|
total_removed = empty_folders_removed + invalid_hash_folders_removed
|
||||||
|
if total_removed > 0:
|
||||||
|
logger.info(f"Example images cleanup completed: checked {total_folders_checked} folders, "
|
||||||
|
f"removed {empty_folders_removed} empty folders and {invalid_hash_folders_removed} "
|
||||||
|
f"folders for deleted/invalid models (total: {total_removed} removed)")
|
||||||
|
else:
|
||||||
|
logger.info(f"Example images cleanup completed: checked {total_folders_checked} folders, "
|
||||||
|
f"no cleanup needed")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error during example images cleanup: {e}", exc_info=True)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _is_folder_empty(cls, folder_path: str) -> bool:
|
||||||
|
"""Check if a folder is empty
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_path: Path to the folder to check
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if folder is empty, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with os.scandir(folder_path) as it:
|
||||||
|
return not any(it)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error checking if folder is empty {folder_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _remove_folder_safely(cls, folder_path: str):
|
||||||
|
"""Safely remove a folder and all its contents
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder_path: Path to the folder to remove
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import shutil
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
await loop.run_in_executor(None, shutil.rmtree, folder_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Failed to remove folder {folder_path}: {e}")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _cleanup(cls, app):
|
async def _cleanup(cls, app):
|
||||||
"""Cleanup resources using ServiceRegistry"""
|
"""Cleanup resources using ServiceRegistry"""
|
||||||
|
|||||||
128
py/nodes/wanvideo_lora_select_from_text.py
Normal file
128
py/nodes/wanvideo_lora_select_from_text.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
from comfy.comfy_types import IO
|
||||||
|
import folder_paths
|
||||||
|
from ..utils.utils import get_lora_info
|
||||||
|
from .utils import any_type
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 初始化日志记录器
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 定义新节点的类
|
||||||
|
class WanVideoLoraSelectFromText:
|
||||||
|
# 节点在UI中显示的名称
|
||||||
|
NAME = "WanVideo Lora Select From Text (LoraManager)"
|
||||||
|
# 节点所属的分类
|
||||||
|
CATEGORY = "Lora Manager/stackers"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"low_mem_load": ("BOOLEAN", {"default": False, "tooltip": "Load LORA models with less VRAM usage, slower loading. This affects ALL LoRAs, not just the current ones. No effect if merge_loras is False"}),
|
||||||
|
"merge_lora": ("BOOLEAN", {"default": True, "tooltip": "Merge LoRAs into the model, otherwise they are loaded on the fly. Always disabled for GGUF and scaled fp8 models. This affects ALL LoRAs, not just the current one"}),
|
||||||
|
"lora_syntax": (IO.STRING, {
|
||||||
|
"multiline": True,
|
||||||
|
"defaultInput": True,
|
||||||
|
"forceInput": True,
|
||||||
|
"tooltip": "Connect a TEXT output for LoRA syntax: <lora:name:strength>"
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
"optional": {
|
||||||
|
"prev_lora": ("WANVIDLORA",),
|
||||||
|
"blocks": ("BLOCKS",)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("WANVIDLORA", IO.STRING, IO.STRING)
|
||||||
|
RETURN_NAMES = ("lora", "trigger_words", "active_loras")
|
||||||
|
|
||||||
|
FUNCTION = "process_loras_from_syntax"
|
||||||
|
|
||||||
|
def process_loras_from_syntax(self, lora_syntax, low_mem_load=False, merge_lora=True, **kwargs):
|
||||||
|
text_to_process = lora_syntax
|
||||||
|
|
||||||
|
blocks = kwargs.get('blocks', {})
|
||||||
|
selected_blocks = blocks.get("selected_blocks", {})
|
||||||
|
layer_filter = blocks.get("layer_filter", "")
|
||||||
|
|
||||||
|
loras_list = []
|
||||||
|
all_trigger_words = []
|
||||||
|
active_loras = []
|
||||||
|
|
||||||
|
prev_lora = kwargs.get('prev_lora', None)
|
||||||
|
if prev_lora is not None:
|
||||||
|
loras_list.extend(prev_lora)
|
||||||
|
|
||||||
|
if not merge_lora:
|
||||||
|
low_mem_load = False
|
||||||
|
|
||||||
|
parts = text_to_process.split('<lora:')
|
||||||
|
for part in parts[1:]:
|
||||||
|
end_index = part.find('>')
|
||||||
|
if end_index == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
content = part[:end_index]
|
||||||
|
lora_parts = content.split(':')
|
||||||
|
|
||||||
|
lora_name_raw = ""
|
||||||
|
model_strength = 1.0
|
||||||
|
clip_strength = 1.0
|
||||||
|
|
||||||
|
if len(lora_parts) == 2:
|
||||||
|
lora_name_raw = lora_parts[0].strip()
|
||||||
|
try:
|
||||||
|
model_strength = float(lora_parts[1])
|
||||||
|
clip_strength = model_strength
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
logger.warning(f"Invalid strength for LoRA '{lora_name_raw}'. Skipping.")
|
||||||
|
continue
|
||||||
|
elif len(lora_parts) >= 3:
|
||||||
|
lora_name_raw = lora_parts[0].strip()
|
||||||
|
try:
|
||||||
|
model_strength = float(lora_parts[1])
|
||||||
|
clip_strength = float(lora_parts[2])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
logger.warning(f"Invalid strengths for LoRA '{lora_name_raw}'. Skipping.")
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
lora_path, trigger_words = get_lora_info(lora_name_raw)
|
||||||
|
|
||||||
|
lora_item = {
|
||||||
|
"path": folder_paths.get_full_path("loras", lora_path),
|
||||||
|
"strength": model_strength,
|
||||||
|
"name": lora_path.split(".")[0],
|
||||||
|
"blocks": selected_blocks,
|
||||||
|
"layer_filter": layer_filter,
|
||||||
|
"low_mem_load": low_mem_load,
|
||||||
|
"merge_loras": merge_lora,
|
||||||
|
}
|
||||||
|
|
||||||
|
loras_list.append(lora_item)
|
||||||
|
active_loras.append((lora_name_raw, model_strength, clip_strength))
|
||||||
|
all_trigger_words.extend(trigger_words)
|
||||||
|
|
||||||
|
trigger_words_text = ",, ".join(all_trigger_words) if all_trigger_words else ""
|
||||||
|
|
||||||
|
formatted_loras = []
|
||||||
|
for name, model_strength, clip_strength in active_loras:
|
||||||
|
if abs(model_strength - clip_strength) > 0.001:
|
||||||
|
formatted_loras.append(f"<lora:{name}:{str(model_strength).strip()}:{str(clip_strength).strip()}>")
|
||||||
|
else:
|
||||||
|
formatted_loras.append(f"<lora:{name}:{str(model_strength).strip()}>")
|
||||||
|
|
||||||
|
active_loras_text = " ".join(formatted_loras)
|
||||||
|
|
||||||
|
return (loras_list, trigger_words_text, active_loras_text)
|
||||||
|
|
||||||
|
NODE_CLASS_MAPPINGS = {
|
||||||
|
"WanVideoLoraSelectFromText": WanVideoLoraSelectFromText
|
||||||
|
}
|
||||||
|
|
||||||
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
|
"WanVideoLoraSelectFromText": "WanVideo Lora Select From Text (LoraManager)"
|
||||||
|
}
|
||||||
|
,
|
||||||
@@ -56,6 +56,7 @@ class BaseModelRoutes(ABC):
|
|||||||
app.router.add_post(f'/api/{prefix}/move_model', self.move_model)
|
app.router.add_post(f'/api/{prefix}/move_model', self.move_model)
|
||||||
app.router.add_post(f'/api/{prefix}/move_models_bulk', self.move_models_bulk)
|
app.router.add_post(f'/api/{prefix}/move_models_bulk', self.move_models_bulk)
|
||||||
app.router.add_get(f'/api/{prefix}/auto-organize', self.auto_organize_models)
|
app.router.add_get(f'/api/{prefix}/auto-organize', self.auto_organize_models)
|
||||||
|
app.router.add_post(f'/api/{prefix}/auto-organize', self.auto_organize_models)
|
||||||
app.router.add_get(f'/api/{prefix}/auto-organize-progress', self.get_auto_organize_progress)
|
app.router.add_get(f'/api/{prefix}/auto-organize-progress', self.get_auto_organize_progress)
|
||||||
|
|
||||||
# Common query routes
|
# Common query routes
|
||||||
@@ -773,7 +774,7 @@ class BaseModelRoutes(ABC):
|
|||||||
return web.Response(text=str(e), status=500)
|
return web.Response(text=str(e), status=500)
|
||||||
|
|
||||||
async def auto_organize_models(self, request: web.Request) -> web.Response:
|
async def auto_organize_models(self, request: web.Request) -> web.Response:
|
||||||
"""Auto-organize all models based on current settings"""
|
"""Auto-organize all models or a specific set of models based on current settings"""
|
||||||
try:
|
try:
|
||||||
# Check if auto-organize is already running
|
# Check if auto-organize is already running
|
||||||
if ws_manager.is_auto_organize_running():
|
if ws_manager.is_auto_organize_running():
|
||||||
@@ -791,8 +792,17 @@ class BaseModelRoutes(ABC):
|
|||||||
'error': 'Auto-organize is already running. Please wait for it to complete.'
|
'error': 'Auto-organize is already running. Please wait for it to complete.'
|
||||||
}, status=409)
|
}, status=409)
|
||||||
|
|
||||||
|
# Get specific file paths from request if this is a POST with selected models
|
||||||
|
file_paths = None
|
||||||
|
if request.method == 'POST':
|
||||||
|
try:
|
||||||
|
data = await request.json()
|
||||||
|
file_paths = data.get('file_paths')
|
||||||
|
except Exception:
|
||||||
|
pass # Continue with all models if no valid JSON
|
||||||
|
|
||||||
async with auto_organize_lock:
|
async with auto_organize_lock:
|
||||||
return await self._perform_auto_organize()
|
return await self._perform_auto_organize(file_paths)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in auto_organize_models: {e}", exc_info=True)
|
logger.error(f"Error in auto_organize_models: {e}", exc_info=True)
|
||||||
@@ -809,20 +819,33 @@ class BaseModelRoutes(ABC):
|
|||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=500)
|
}, status=500)
|
||||||
|
|
||||||
async def _perform_auto_organize(self) -> web.Response:
|
async def _perform_auto_organize(self, file_paths=None) -> web.Response:
|
||||||
"""Perform the actual auto-organize operation"""
|
"""Perform the actual auto-organize operation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_paths: Optional list of specific file paths to organize.
|
||||||
|
If None, organizes all models.
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# Get all models from cache
|
# Get all models from cache
|
||||||
cache = await self.service.scanner.get_cached_data()
|
cache = await self.service.scanner.get_cached_data()
|
||||||
all_models = cache.raw_data
|
all_models = cache.raw_data
|
||||||
|
|
||||||
|
# Filter models if specific file paths are provided
|
||||||
|
if file_paths:
|
||||||
|
all_models = [model for model in all_models if model.get('file_path') in file_paths]
|
||||||
|
operation_type = 'bulk'
|
||||||
|
else:
|
||||||
|
operation_type = 'all'
|
||||||
|
|
||||||
# Get model roots for this scanner
|
# Get model roots for this scanner
|
||||||
model_roots = self.service.get_model_roots()
|
model_roots = self.service.get_model_roots()
|
||||||
if not model_roots:
|
if not model_roots:
|
||||||
await ws_manager.broadcast_auto_organize_progress({
|
await ws_manager.broadcast_auto_organize_progress({
|
||||||
'type': 'auto_organize_progress',
|
'type': 'auto_organize_progress',
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'error': 'No model roots configured'
|
'error': 'No model roots configured',
|
||||||
|
'operation_type': operation_type
|
||||||
})
|
})
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -849,7 +872,8 @@ class BaseModelRoutes(ABC):
|
|||||||
'processed': 0,
|
'processed': 0,
|
||||||
'success': 0,
|
'success': 0,
|
||||||
'failures': 0,
|
'failures': 0,
|
||||||
'skipped': 0
|
'skipped': 0,
|
||||||
|
'operation_type': operation_type
|
||||||
})
|
})
|
||||||
|
|
||||||
# Process models in batches
|
# Process models in batches
|
||||||
@@ -980,7 +1004,8 @@ class BaseModelRoutes(ABC):
|
|||||||
'processed': processed,
|
'processed': processed,
|
||||||
'success': success_count,
|
'success': success_count,
|
||||||
'failures': failure_count,
|
'failures': failure_count,
|
||||||
'skipped': skipped_count
|
'skipped': skipped_count,
|
||||||
|
'operation_type': operation_type
|
||||||
})
|
})
|
||||||
|
|
||||||
# Small delay between batches to prevent overwhelming the system
|
# Small delay between batches to prevent overwhelming the system
|
||||||
@@ -995,7 +1020,8 @@ class BaseModelRoutes(ABC):
|
|||||||
'success': success_count,
|
'success': success_count,
|
||||||
'failures': failure_count,
|
'failures': failure_count,
|
||||||
'skipped': skipped_count,
|
'skipped': skipped_count,
|
||||||
'message': 'Cleaning up empty directories...'
|
'message': 'Cleaning up empty directories...',
|
||||||
|
'operation_type': operation_type
|
||||||
})
|
})
|
||||||
|
|
||||||
# Clean up empty directories after organizing
|
# Clean up empty directories after organizing
|
||||||
@@ -1014,20 +1040,22 @@ class BaseModelRoutes(ABC):
|
|||||||
'success': success_count,
|
'success': success_count,
|
||||||
'failures': failure_count,
|
'failures': failure_count,
|
||||||
'skipped': skipped_count,
|
'skipped': skipped_count,
|
||||||
'cleanup': cleanup_counts
|
'cleanup': cleanup_counts,
|
||||||
|
'operation_type': operation_type
|
||||||
})
|
})
|
||||||
|
|
||||||
# Prepare response with limited details
|
# Prepare response with limited details
|
||||||
response_data = {
|
response_data = {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': f'Auto-organize completed: {success_count} moved, {skipped_count} skipped, {failure_count} failed out of {total_models} total',
|
'message': f'Auto-organize {operation_type} completed: {success_count} moved, {skipped_count} skipped, {failure_count} failed out of {total_models} total',
|
||||||
'summary': {
|
'summary': {
|
||||||
'total': total_models,
|
'total': total_models,
|
||||||
'success': success_count,
|
'success': success_count,
|
||||||
'skipped': skipped_count,
|
'skipped': skipped_count,
|
||||||
'failures': failure_count,
|
'failures': failure_count,
|
||||||
'organization_type': 'flat' if is_flat_structure else 'structured',
|
'organization_type': 'flat' if is_flat_structure else 'structured',
|
||||||
'cleaned_dirs': cleanup_counts
|
'cleaned_dirs': cleanup_counts,
|
||||||
|
'operation_type': operation_type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,7 +1075,8 @@ class BaseModelRoutes(ABC):
|
|||||||
await ws_manager.broadcast_auto_organize_progress({
|
await ws_manager.broadcast_auto_organize_progress({
|
||||||
'type': 'auto_organize_progress',
|
'type': 'auto_organize_progress',
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'error': str(e)
|
'error': str(e),
|
||||||
|
'operation_type': operation_type if 'operation_type' in locals() else 'unknown'
|
||||||
})
|
})
|
||||||
|
|
||||||
raise e
|
raise e
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ AUTO_ORGANIZE_BATCH_SIZE = 50 # Process models in batches to avoid overwhelming
|
|||||||
# Civitai model tags in priority order for subfolder organization
|
# Civitai model tags in priority order for subfolder organization
|
||||||
CIVITAI_MODEL_TAGS = [
|
CIVITAI_MODEL_TAGS = [
|
||||||
'character', 'style', 'concept', 'clothing',
|
'character', 'style', 'concept', 'clothing',
|
||||||
# 'base model', # exclude 'base model'
|
'realistic', 'anime', 'toon', 'furry',
|
||||||
'poses', 'background', 'tool', 'vehicle', 'buildings',
|
'poses', 'background', 'tool', 'vehicle', 'buildings',
|
||||||
'objects', 'assets', 'animal', 'action'
|
'objects', 'assets', 'animal', 'action'
|
||||||
]
|
]
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "comfyui-lora-manager"
|
name = "comfyui-lora-manager"
|
||||||
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
license = {file = "LICENSE"}
|
license = {file = "LICENSE"}
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aiohttp",
|
"aiohttp",
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ export function getApiEndpoints(modelType) {
|
|||||||
metadata: `/api/${modelType}/metadata`,
|
metadata: `/api/${modelType}/metadata`,
|
||||||
modelDescription: `/api/${modelType}/model-description`,
|
modelDescription: `/api/${modelType}/model-description`,
|
||||||
|
|
||||||
|
// Auto-organize operations
|
||||||
|
autoOrganize: `/api/${modelType}/auto-organize`,
|
||||||
|
autoOrganizeProgress: `/api/${modelType}/auto-organize-progress`,
|
||||||
|
|
||||||
// Model-specific endpoints (will be merged with specific configs)
|
// Model-specific endpoints (will be merged with specific configs)
|
||||||
specific: {}
|
specific: {}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1030,4 +1030,129 @@ export class BaseModelApiClient {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-organize models based on current path template settings
|
||||||
|
* @param {Array} filePaths - Optional array of file paths to organize. If not provided, organizes all models.
|
||||||
|
* @returns {Promise} - Promise that resolves when the operation is complete
|
||||||
|
*/
|
||||||
|
async autoOrganizeModels(filePaths = null) {
|
||||||
|
let ws = null;
|
||||||
|
|
||||||
|
await state.loadingManager.showWithProgress(async (loading) => {
|
||||||
|
try {
|
||||||
|
// Connect to WebSocket for progress updates
|
||||||
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
|
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
||||||
|
|
||||||
|
const operationComplete = new Promise((resolve, reject) => {
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (data.type !== 'auto_organize_progress') return;
|
||||||
|
|
||||||
|
switch(data.status) {
|
||||||
|
case 'started':
|
||||||
|
loading.setProgress(0);
|
||||||
|
const operationType = data.operation_type === 'bulk' ? 'selected models' : 'all models';
|
||||||
|
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.starting', { type: operationType }, `Starting auto-organize for ${operationType}...`));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'processing':
|
||||||
|
const percent = data.total > 0 ? ((data.processed / data.total) * 90).toFixed(1) : 0;
|
||||||
|
loading.setProgress(percent);
|
||||||
|
loading.setStatus(
|
||||||
|
translate('loras.bulkOperations.autoOrganizeProgress.processing', {
|
||||||
|
processed: data.processed,
|
||||||
|
total: data.total,
|
||||||
|
success: data.success,
|
||||||
|
failures: data.failures,
|
||||||
|
skipped: data.skipped
|
||||||
|
}, `Processing (${data.processed}/${data.total}) - ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cleaning':
|
||||||
|
loading.setProgress(95);
|
||||||
|
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.cleaning', {}, 'Cleaning up empty directories...'));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'completed':
|
||||||
|
loading.setProgress(100);
|
||||||
|
loading.setStatus(
|
||||||
|
translate('loras.bulkOperations.autoOrganizeProgress.completed', {
|
||||||
|
success: data.success,
|
||||||
|
skipped: data.skipped,
|
||||||
|
failures: data.failures,
|
||||||
|
total: data.total
|
||||||
|
}, `Completed: ${data.success} moved, ${data.skipped} skipped, ${data.failures} failed`)
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(data);
|
||||||
|
}, 1500);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.error', { error: data.error }, `Error: ${data.error}`));
|
||||||
|
reject(new Error(data.error));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error during auto-organize:', error);
|
||||||
|
reject(new Error('Connection error'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the auto-organize operation
|
||||||
|
const endpoint = this.apiConfig.endpoints.autoOrganize;
|
||||||
|
const requestOptions = {
|
||||||
|
method: filePaths ? 'POST' : 'GET',
|
||||||
|
headers: filePaths ? { 'Content-Type': 'application/json' } : {}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filePaths) {
|
||||||
|
requestOptions.body = JSON.stringify({ file_paths: filePaths });
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint, requestOptions);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.error || 'Failed to start auto-organize operation');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for the operation to complete via WebSocket
|
||||||
|
const result = await operationComplete;
|
||||||
|
|
||||||
|
// Show appropriate success message based on results
|
||||||
|
if (result.failures === 0) {
|
||||||
|
showToast('toast.loras.autoOrganizeSuccess', {
|
||||||
|
count: result.success,
|
||||||
|
type: result.operation_type === 'bulk' ? 'selected models' : 'all models'
|
||||||
|
}, 'success');
|
||||||
|
} else {
|
||||||
|
showToast('toast.loras.autoOrganizePartialSuccess', {
|
||||||
|
success: result.success,
|
||||||
|
failures: result.failures,
|
||||||
|
total: result.total
|
||||||
|
}, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during auto-organize:', error);
|
||||||
|
showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
initialMessage: translate('loras.bulkOperations.autoOrganizeProgress.initializing', {}, 'Initializing auto-organize...'),
|
||||||
|
completionMessage: translate('loras.bulkOperations.autoOrganizeProgress.complete', {}, 'Auto-organize complete')
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,7 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
const copyAllItem = this.menu.querySelector('[data-action="copy-all"]');
|
||||||
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
const refreshAllItem = this.menu.querySelector('[data-action="refresh-all"]');
|
||||||
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
const moveAllItem = this.menu.querySelector('[data-action="move-all"]');
|
||||||
|
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
|
||||||
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
|
||||||
|
|
||||||
if (sendToWorkflowAppendItem) {
|
if (sendToWorkflowAppendItem) {
|
||||||
@@ -50,6 +51,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
if (moveAllItem) {
|
if (moveAllItem) {
|
||||||
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
moveAllItem.style.display = config.moveAll ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
if (autoOrganizeItem) {
|
||||||
|
autoOrganizeItem.style.display = config.autoOrganize ? 'flex' : 'none';
|
||||||
|
}
|
||||||
if (deleteAllItem) {
|
if (deleteAllItem) {
|
||||||
deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none';
|
deleteAllItem.style.display = config.deleteAll ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
@@ -97,6 +101,9 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
case 'move-all':
|
case 'move-all':
|
||||||
window.moveManager.showMoveModal('bulk');
|
window.moveManager.showMoveModal('bulk');
|
||||||
break;
|
break;
|
||||||
|
case 'auto-organize':
|
||||||
|
bulkManager.autoOrganizeSelectedModels();
|
||||||
|
break;
|
||||||
case 'delete-all':
|
case 'delete-all':
|
||||||
bulkManager.showBulkDeleteModal();
|
bulkManager.showBulkDeleteModal();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export class CheckpointsControls extends PageControls {
|
|||||||
showDownloadModal: () => {
|
showDownloadModal: () => {
|
||||||
downloadManager.showDownloadModal();
|
downloadManager.showDownloadModal();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleBulkMode: () => {
|
||||||
|
if (window.bulkManager) {
|
||||||
|
window.bulkManager.toggleBulkMode();
|
||||||
|
} else {
|
||||||
|
console.error('Bulk manager not available');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// No clearCustomFilter implementation is needed for checkpoints
|
// No clearCustomFilter implementation is needed for checkpoints
|
||||||
// as custom filters are currently only used for LoRAs
|
// as custom filters are currently only used for LoRAs
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ export class EmbeddingsControls extends PageControls {
|
|||||||
showDownloadModal: () => {
|
showDownloadModal: () => {
|
||||||
downloadManager.showDownloadModal();
|
downloadManager.showDownloadModal();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleBulkMode: () => {
|
||||||
|
if (window.bulkManager) {
|
||||||
|
window.bulkManager.toggleBulkMode();
|
||||||
|
} else {
|
||||||
|
console.error('Bulk manager not available');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// No clearCustomFilter implementation is needed for embeddings
|
// No clearCustomFilter implementation is needed for embeddings
|
||||||
// as custom filters are currently only used for LoRAs
|
// as custom filters are currently only used for LoRAs
|
||||||
|
|||||||
@@ -185,12 +185,9 @@ export class PageControls {
|
|||||||
duplicatesButton.addEventListener('click', () => this.findDuplicates());
|
duplicatesButton.addEventListener('click', () => this.findDuplicates());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pageType === 'loras') {
|
const bulkButton = document.querySelector('[data-action="bulk"]');
|
||||||
// Bulk operations button - LoRAs only
|
if (bulkButton) {
|
||||||
const bulkButton = document.querySelector('[data-action="bulk"]');
|
bulkButton.addEventListener('click', () => this.toggleBulkMode());
|
||||||
if (bulkButton) {
|
|
||||||
bulkButton.addEventListener('click', () => this.toggleBulkMode());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Favorites filter button handler
|
// Favorites filter button handler
|
||||||
@@ -349,14 +346,9 @@ export class PageControls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Toggle bulk mode (LoRAs only)
|
* Toggle bulk mode
|
||||||
*/
|
*/
|
||||||
toggleBulkMode() {
|
toggleBulkMode() {
|
||||||
if (this.pageType !== 'loras' || !this.api) {
|
|
||||||
console.error('Bulk mode is only available for LoRAs');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.api.toggleBulkMode();
|
this.api.toggleBulkMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,13 +54,15 @@ export class AppCore {
|
|||||||
window.headerManager = new HeaderManager();
|
window.headerManager = new HeaderManager();
|
||||||
initTheme();
|
initTheme();
|
||||||
initBackToTop();
|
initBackToTop();
|
||||||
|
|
||||||
// Initialize the bulk manager
|
|
||||||
bulkManager.initialize();
|
|
||||||
|
|
||||||
// Initialize bulk context menu
|
// Initialize the bulk manager and context menu only if not on recipes page
|
||||||
const bulkContextMenu = new BulkContextMenu();
|
if (state.currentPageType !== 'recipes') {
|
||||||
bulkManager.setBulkContextMenu(bulkContextMenu);
|
bulkManager.initialize();
|
||||||
|
|
||||||
|
// Initialize bulk context menu
|
||||||
|
const bulkContextMenu = new BulkContextMenu();
|
||||||
|
bulkManager.setBulkContextMenu(bulkContextMenu);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize the example images manager
|
// Initialize the example images manager
|
||||||
exampleImagesManager.initialize();
|
exampleImagesManager.initialize();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { state, getCurrentPageState } from '../state/index.js';
|
|||||||
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
import { showToast, copyToClipboard, sendLoraToWorkflow } from '../utils/uiHelpers.js';
|
||||||
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
import { updateCardsForBulkMode } from '../components/shared/ModelCard.js';
|
||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
import { getModelApiClient } from '../api/modelApiFactory.js';
|
import { getModelApiClient, resetAndReload } from '../api/modelApiFactory.js';
|
||||||
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
import { MODEL_TYPES, MODEL_CONFIG } from '../api/apiConfig.js';
|
||||||
import { PRESET_TAGS, BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
import { PRESET_TAGS, BASE_MODEL_CATEGORIES } from '../utils/constants.js';
|
||||||
import { eventManager } from '../utils/EventManager.js';
|
import { eventManager } from '../utils/EventManager.js';
|
||||||
@@ -34,6 +34,7 @@ export class BulkManager {
|
|||||||
copyAll: true,
|
copyAll: true,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
moveAll: true,
|
moveAll: true,
|
||||||
|
autoOrganize: true,
|
||||||
deleteAll: true
|
deleteAll: true
|
||||||
},
|
},
|
||||||
[MODEL_TYPES.EMBEDDING]: {
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
@@ -42,6 +43,7 @@ export class BulkManager {
|
|||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
moveAll: true,
|
moveAll: true,
|
||||||
|
autoOrganize: true,
|
||||||
deleteAll: true
|
deleteAll: true
|
||||||
},
|
},
|
||||||
[MODEL_TYPES.CHECKPOINT]: {
|
[MODEL_TYPES.CHECKPOINT]: {
|
||||||
@@ -50,12 +52,16 @@ export class BulkManager {
|
|||||||
copyAll: false,
|
copyAll: false,
|
||||||
refreshAll: true,
|
refreshAll: true,
|
||||||
moveAll: false,
|
moveAll: false,
|
||||||
|
autoOrganize: true,
|
||||||
deleteAll: true
|
deleteAll: true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
|
// Do not initialize on recipes page
|
||||||
|
if (state.currentPageType === 'recipes') return;
|
||||||
|
|
||||||
// Register with event manager for coordinated event handling
|
// Register with event manager for coordinated event handling
|
||||||
this.registerEventHandlers();
|
this.registerEventHandlers();
|
||||||
|
|
||||||
@@ -962,6 +968,35 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-organize selected models based on current path template settings
|
||||||
|
*/
|
||||||
|
async autoOrganizeSelectedModels() {
|
||||||
|
if (state.selectedModels.size === 0) {
|
||||||
|
showToast('toast.loras.noModelsSelected', {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get selected file paths
|
||||||
|
const filePaths = Array.from(state.selectedModels);
|
||||||
|
|
||||||
|
// Get the API client for the current model type
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
|
||||||
|
// Call the auto-organize method with selected file paths
|
||||||
|
await apiClient.autoOrganizeModels(filePaths);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
resetAndReload(true);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during bulk auto-organize:', error);
|
||||||
|
showToast('toast.loras.autoOrganizeFailed', { error: error.message }, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle marquee start through event manager
|
* Handle marquee start through event manager
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
/**
|
|
||||||
* i18n System Test
|
|
||||||
* Simple test to verify internationalization functionality
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { i18n } from '../i18n/index.js';
|
|
||||||
import { initializePageI18n, t, formatFileSize, formatDate, formatNumber } from '../utils/i18nHelpers.js';
|
|
||||||
import { findUnusedTranslationKeys, findMissingTranslationKeys, extractLeafKeys } from '../i18n/validator.js';
|
|
||||||
|
|
||||||
// Mock DOM elements for testing
|
|
||||||
function createMockDOM() {
|
|
||||||
// Create a test container
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.innerHTML = `
|
|
||||||
<div data-i18n="header.appTitle">LoRA Manager</div>
|
|
||||||
<input data-i18n="header.search.placeholder" data-i18n-target="placeholder" placeholder="Search..." />
|
|
||||||
<button data-i18n="common.actions.save">Save</button>
|
|
||||||
<span data-i18n="loras.bulkOperations.selected" data-i18n-params='{"count": 5}'>5 selected</span>
|
|
||||||
`;
|
|
||||||
document.body.appendChild(container);
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test basic translation functionality
|
|
||||||
function testBasicTranslation() {
|
|
||||||
console.log('=== Testing Basic Translation ===');
|
|
||||||
|
|
||||||
// Test simple translation
|
|
||||||
const saveText = t('common.actions.save');
|
|
||||||
console.log(`Save button text: ${saveText}`);
|
|
||||||
|
|
||||||
// Test translation with parameters
|
|
||||||
const selectedText = t('loras.bulkOperations.selected', { count: 3 });
|
|
||||||
console.log(`Selection text: ${selectedText}`);
|
|
||||||
|
|
||||||
// Test non-existent key (should return the key itself)
|
|
||||||
const missingKey = t('non.existent.key');
|
|
||||||
console.log(`Missing key: ${missingKey}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test DOM translation
|
|
||||||
function testDOMTranslation() {
|
|
||||||
console.log('=== Testing DOM Translation ===');
|
|
||||||
|
|
||||||
const container = createMockDOM();
|
|
||||||
|
|
||||||
// Apply translations
|
|
||||||
initializePageI18n();
|
|
||||||
|
|
||||||
// Check if translations were applied
|
|
||||||
const titleElement = container.querySelector('[data-i18n="header.appTitle"]');
|
|
||||||
const inputElement = container.querySelector('input[data-i18n="header.search.placeholder"]');
|
|
||||||
const buttonElement = container.querySelector('[data-i18n="common.actions.save"]');
|
|
||||||
|
|
||||||
console.log(`Title: ${titleElement.textContent}`);
|
|
||||||
console.log(`Input placeholder: ${inputElement.placeholder}`);
|
|
||||||
console.log(`Button: ${buttonElement.textContent}`);
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
document.body.removeChild(container);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test formatting functions
|
|
||||||
function testFormatting() {
|
|
||||||
console.log('=== Testing Formatting Functions ===');
|
|
||||||
|
|
||||||
// Test file size formatting
|
|
||||||
const sizes = [0, 1024, 1048576, 1073741824];
|
|
||||||
sizes.forEach(size => {
|
|
||||||
const formatted = formatFileSize(size);
|
|
||||||
console.log(`${size} bytes = ${formatted}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test date formatting
|
|
||||||
const date = new Date('2024-01-15T10:30:00');
|
|
||||||
const formattedDate = formatDate(date, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
console.log(`Date: ${formattedDate}`);
|
|
||||||
|
|
||||||
// Test number formatting
|
|
||||||
const number = 1234.567;
|
|
||||||
const formattedNumber = formatNumber(number, {
|
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2
|
|
||||||
});
|
|
||||||
console.log(`Number: ${formattedNumber}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test language detection
|
|
||||||
function testLanguageDetection() {
|
|
||||||
console.log('=== Testing Language Detection ===');
|
|
||||||
console.log(`Detected language: ${i18n.getCurrentLocale()}`);
|
|
||||||
console.log(`Is RTL: ${i18n.isRTL()}`);
|
|
||||||
console.log(`Browser language: ${navigator.language}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test unused translations detection
|
|
||||||
function testUnusedTranslationsDetection() {
|
|
||||||
console.log('=== Testing Unused Translations Detection ===');
|
|
||||||
|
|
||||||
// Mock used keys
|
|
||||||
const mockUsedKeys = [
|
|
||||||
'common.actions.save',
|
|
||||||
'common.actions.cancel',
|
|
||||||
'header.appTitle'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Get all translations
|
|
||||||
const allTranslations = i18n.getTranslations();
|
|
||||||
|
|
||||||
// Find unused keys (only considering leaf nodes)
|
|
||||||
const unusedKeys = findUnusedTranslationKeys(allTranslations, mockUsedKeys);
|
|
||||||
|
|
||||||
console.log(`Found ${unusedKeys.length} unused translation keys`);
|
|
||||||
console.log('First 5 unused keys:', unusedKeys.slice(0, 5));
|
|
||||||
|
|
||||||
// Find missing keys
|
|
||||||
const missingKeys = findMissingTranslationKeys(allTranslations, [
|
|
||||||
...mockUsedKeys,
|
|
||||||
'non.existent.key'
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(`Found ${missingKeys.length} missing translation keys:`, missingKeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all tests
|
|
||||||
function runTests() {
|
|
||||||
console.log('Starting i18n System Tests...');
|
|
||||||
console.log('=====================================');
|
|
||||||
|
|
||||||
testLanguageDetection();
|
|
||||||
testBasicTranslation();
|
|
||||||
testFormatting();
|
|
||||||
|
|
||||||
// Only test DOM if we're in a browser environment
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
testDOMTranslation();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test unused translations detection
|
|
||||||
testUnusedTranslationsDetection();
|
|
||||||
|
|
||||||
console.log('=====================================');
|
|
||||||
console.log('i18n System Tests Completed!');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export for manual testing
|
|
||||||
export { runTests };
|
|
||||||
|
|
||||||
// Auto-run tests if this module is loaded directly
|
|
||||||
if (typeof window !== 'undefined' && window.location.search.includes('test=i18n')) {
|
|
||||||
document.addEventListener('DOMContentLoaded', runTests);
|
|
||||||
}
|
|
||||||
@@ -189,7 +189,8 @@ export const BASE_MODEL_CATEGORIES = {
|
|||||||
|
|
||||||
// Preset tag suggestions
|
// Preset tag suggestions
|
||||||
export const PRESET_TAGS = [
|
export const PRESET_TAGS = [
|
||||||
'character', 'style', 'concept', 'clothing',
|
'character', 'style', 'concept', 'clothing',
|
||||||
'poses', 'background', 'vehicle', 'buildings',
|
'realistic', 'anime', 'toon', 'furry',
|
||||||
|
'poses', 'background', 'vehicle', 'buildings',
|
||||||
'objects', 'animal'
|
'objects', 'animal'
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
<div class="context-menu-item" data-action="move-all">
|
<div class="context-menu-item" data-action="move-all">
|
||||||
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
<i class="fas fa-folder-open"></i> <span>{{ t('loras.bulkOperations.moveAll') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="auto-organize">
|
||||||
|
<i class="fas fa-magic"></i> <span>{{ t('loras.bulkOperations.autoOrganize') }}</span>
|
||||||
|
</div>
|
||||||
<div class="context-menu-separator"></div>
|
<div class="context-menu-separator"></div>
|
||||||
<div class="context-menu-item delete-item" data-action="delete-all">
|
<div class="context-menu-item delete-item" data-action="delete-all">
|
||||||
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
<i class="fas fa-trash"></i> <span>{{ t('loras.bulkOperations.deleteAll') }}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user