feat: Add simplified node version and English localization
- Add new simplified node (save_image_simple.py) with 5 core parameters - Rename save_image_extended.py to save_image_advanced.py for clarity - Convert all log messages to English - Update node display names to English - Fix parameter order bug in ImageSaver constructor - Update documentation with node version comparison - Clean up old documentation files - Remove test files (to be reorganized later)
This commit is contained in:
+4
-4
@@ -5,7 +5,7 @@
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||
并且本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [3.0.0] - 2024-01-XX
|
||||
## [最新版本] - 重构版本
|
||||
|
||||
### 重大变更 🚀
|
||||
- **完全重构**: 从单一文件重构为模块化架构
|
||||
@@ -180,13 +180,13 @@
|
||||
|
||||
### 迁移指南
|
||||
|
||||
#### 从 v2.x 升级到 v3.0
|
||||
1. **无需修改工作流**:v3.0 保持完全向后兼容
|
||||
#### 从旧版本升级
|
||||
1. **无需修改工作流**:新版本保持完全向后兼容
|
||||
2. **性能自动提升**:新版本自动提供性能优化
|
||||
3. **新功能可选**:所有新功能都是可选的,不影响现有使用
|
||||
4. **配置保持不变**:现有的配置参数继续有效
|
||||
|
||||
#### 注意事项
|
||||
- v3.0 是重构版本,内部架构完全改变,但外部接口保持不变
|
||||
- 新版本是重构版本,内部架构完全改变,但外部接口保持不变
|
||||
- 如果遇到问题,请查看故障排除指南或提交 Issue
|
||||
- 建议在升级前备份重要的工作流文件
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 🚀 ComfyUI Save Image Pro
|
||||
|
||||
**专业级图像保存插件 - 重构版本 v3.0**
|
||||
**专业级图像保存插件**
|
||||
|
||||
一个功能强大、高性能的 ComfyUI 图像保存插件,采用现代化模块架构设计,支持多种格式、自定义命名和高级功能。
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
## 📖 快速开始
|
||||
|
||||
### 基本使用
|
||||
1. 在 ComfyUI 中搜索并添加 "Save Image Extended" 节点
|
||||
1. 在 ComfyUI 中搜索并添加 "comfyui-save-image-pro" 节点
|
||||
2. 连接图像输出到节点的 images 输入
|
||||
3. 配置保存参数(格式、文件名、文件夹等)
|
||||
4. 运行工作流,图像将按配置保存
|
||||
@@ -185,7 +185,7 @@ pip install jxlpy
|
||||
3. 节点上会显示数字ID,可用于参数引用
|
||||
|
||||
|
||||
## 🚀 v3.0 重构亮点
|
||||
## 🚀 重构亮点
|
||||
|
||||
### 架构升级
|
||||
- **模块化设计**: 从单一文件重构为11个专业模块
|
||||
@@ -235,7 +235,7 @@ pip install jxlpy
|
||||
|
||||
## 📋 更新日志
|
||||
|
||||
### v3.0.0 (2024-01-XX) - 重构版本
|
||||
### 最新版本 - 重构版本
|
||||
- ✨ 完全重构的模块化架构
|
||||
- ⚡ 大幅性能提升和优化
|
||||
- 🧪 完整的单元测试覆盖
|
||||
|
||||
+7
-2
@@ -9,11 +9,16 @@ ComfyUI Save Image Pro - 专业级图像保存插件
|
||||
@title: ComfyUI Save Image Pro
|
||||
@nickname: Save Image Pro
|
||||
@description: 专业级图像保存插件,支持多格式、自定义命名和高级功能
|
||||
@version: 3.0.0 (重构版本)
|
||||
@version: latest
|
||||
@repository: https://github.com/weekii/comfyui-save-image-pro
|
||||
"""
|
||||
|
||||
from .save_image_extended import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
from .save_image_advanced import NODE_CLASS_MAPPINGS as ADVANCED_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as ADVANCED_DISPLAY_MAPPINGS
|
||||
from .save_image_simple import NODE_CLASS_MAPPINGS as SIMPLE_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as SIMPLE_DISPLAY_MAPPINGS
|
||||
|
||||
# 合并节点映射
|
||||
NODE_CLASS_MAPPINGS = {**ADVANCED_MAPPINGS, **SIMPLE_MAPPINGS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**ADVANCED_DISPLAY_MAPPINGS, **SIMPLE_DISPLAY_MAPPINGS}
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Save Image Extended - 核心模块
|
||||
ComfyUI Save Image Pro - 核心模块
|
||||
|
||||
这个包包含了重构后的核心组件,遵循SOLID原则设计。
|
||||
|
||||
|
||||
+12
-4
@@ -78,7 +78,8 @@ class ImageSaver:
|
||||
Returns:
|
||||
保存结果列表
|
||||
"""
|
||||
if not images:
|
||||
# 检查图像列表是否为空(避免对张量进行布尔判断)
|
||||
if images is None or len(images) == 0:
|
||||
return []
|
||||
|
||||
timestamp = datetime.now()
|
||||
@@ -155,11 +156,18 @@ class ImageSaver:
|
||||
def _tensor_to_pil(self, tensor) -> Image.Image:
|
||||
"""将张量转换为PIL图像"""
|
||||
import numpy as np
|
||||
|
||||
|
||||
# 确保张量在CPU上
|
||||
tensor = tensor.cpu()
|
||||
|
||||
# 如果张量有批次维度,取第一个
|
||||
if len(tensor.shape) > 3:
|
||||
tensor = tensor[0]
|
||||
|
||||
# 转换为numpy数组
|
||||
i = 255. * tensor.cpu().numpy()
|
||||
i = 255. * tensor.numpy()
|
||||
array = np.clip(i, 0, 255).astype(np.uint8)
|
||||
|
||||
|
||||
# 创建PIL图像
|
||||
return Image.fromarray(array)
|
||||
|
||||
|
||||
@@ -60,8 +60,8 @@ class MetadataHandler:
|
||||
|
||||
# 添加插件信息
|
||||
metadata['generator'] = {
|
||||
'name': 'Save Image Extended',
|
||||
'version': '3.0'
|
||||
'name': 'ComfyUI Save Image Pro',
|
||||
'version': 'latest'
|
||||
}
|
||||
|
||||
# 缓存结果
|
||||
|
||||
-434
@@ -1,434 +0,0 @@
|
||||
# Save Image Extended 插件优化建议
|
||||
|
||||
## 🎯 优化目标
|
||||
将现有的单体架构重构为模块化、高性能、用户友好的插件系统。
|
||||
|
||||
## 📊 性能优化建议
|
||||
|
||||
### 1. 计数器优化
|
||||
```python
|
||||
class CounterManager:
|
||||
"""智能计数器管理"""
|
||||
def __init__(self):
|
||||
self._counter_cache = {}
|
||||
|
||||
def get_next_counter(self, folder_path, pattern):
|
||||
# 使用缓存避免重复扫描目录
|
||||
cache_key = f"{folder_path}:{pattern}"
|
||||
if cache_key not in self._counter_cache:
|
||||
self._counter_cache[cache_key] = self._scan_directory(folder_path, pattern)
|
||||
return self._counter_cache[cache_key] + 1
|
||||
```
|
||||
|
||||
### 2. 批量处理优化
|
||||
```python
|
||||
class BatchImageProcessor:
|
||||
"""批量图像处理器"""
|
||||
def process_images(self, images, config):
|
||||
# 预处理配置
|
||||
metadata = self._prepare_metadata_once(config)
|
||||
|
||||
# 批量转换图像格式
|
||||
converted_images = self._batch_convert_images(images)
|
||||
|
||||
# 并行保存
|
||||
with ThreadPoolExecutor() as executor:
|
||||
futures = [
|
||||
executor.submit(self._save_single_image, img, metadata, config)
|
||||
for img in converted_images
|
||||
]
|
||||
```
|
||||
|
||||
### 3. 元数据缓存
|
||||
```python
|
||||
class MetadataCache:
|
||||
"""元数据缓存管理"""
|
||||
def __init__(self):
|
||||
self._cache = {}
|
||||
|
||||
def get_metadata(self, prompt_hash, format_type):
|
||||
cache_key = f"{prompt_hash}:{format_type}"
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._generate_metadata(prompt_hash, format_type)
|
||||
return self._cache[cache_key]
|
||||
```
|
||||
|
||||
## 🎨 用户体验改进
|
||||
|
||||
### 1. 配置模板系统
|
||||
```python
|
||||
class ConfigTemplate:
|
||||
"""配置模板管理"""
|
||||
TEMPLATES = {
|
||||
"simple": {
|
||||
"filename_keys": "sampler_name, steps",
|
||||
"foldername_keys": "ckpt_name",
|
||||
"delimiter": "-"
|
||||
},
|
||||
"detailed": {
|
||||
"filename_keys": "sampler_name, cfg, steps, %Y-%m-%d_%H-%M-%S",
|
||||
"foldername_keys": "ckpt_name, ./sampler_name",
|
||||
"delimiter": "_"
|
||||
},
|
||||
"minimal": {
|
||||
"filename_keys": "%Y%m%d_%H%M%S",
|
||||
"foldername_keys": "",
|
||||
"delimiter": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 文件名预览功能
|
||||
```python
|
||||
class FileNamePreview:
|
||||
"""实时文件名预览"""
|
||||
def preview_filename(self, config, sample_prompt):
|
||||
generator = FileNameGenerator(config)
|
||||
preview = generator.generate_name(sample_prompt, dry_run=True)
|
||||
return {
|
||||
"filename": preview.filename,
|
||||
"folder_path": preview.folder_path,
|
||||
"full_path": preview.full_path,
|
||||
"warnings": preview.warnings
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 智能错误处理
|
||||
```python
|
||||
class ValidationError(Exception):
|
||||
"""自定义验证错误"""
|
||||
def __init__(self, field, value, suggestion=None):
|
||||
self.field = field
|
||||
self.value = value
|
||||
self.suggestion = suggestion
|
||||
super().__init__(f"Invalid {field}: {value}")
|
||||
|
||||
class ConfigValidator:
|
||||
"""配置验证器"""
|
||||
def validate_filename_keys(self, keys):
|
||||
for key in keys:
|
||||
if not self._is_valid_key(key):
|
||||
suggestion = self._suggest_correction(key)
|
||||
raise ValidationError("filename_key", key, suggestion)
|
||||
```
|
||||
|
||||
## 🏗️ 架构重构建议
|
||||
|
||||
### 1. 策略模式 - 格式处理
|
||||
```python
|
||||
class FormatStrategy:
|
||||
"""图像格式处理策略基类"""
|
||||
def save_image(self, image, path, metadata, quality):
|
||||
raise NotImplementedError
|
||||
|
||||
class PNGStrategy(FormatStrategy):
|
||||
def save_image(self, image, path, metadata, quality):
|
||||
kwargs = {"pnginfo": metadata, "compress_level": 9}
|
||||
image.save(path, **kwargs)
|
||||
|
||||
class WebPStrategy(FormatStrategy):
|
||||
def save_image(self, image, path, metadata, quality):
|
||||
kwargs = {"exif": metadata, "quality": quality}
|
||||
if quality == 100:
|
||||
kwargs["lossless"] = True
|
||||
image.save(path, **kwargs)
|
||||
```
|
||||
|
||||
### 2. 观察者模式 - 事件系统
|
||||
```python
|
||||
class SaveEvent:
|
||||
"""保存事件"""
|
||||
def __init__(self, image_path, metadata, success=True, error=None):
|
||||
self.image_path = image_path
|
||||
self.metadata = metadata
|
||||
self.success = success
|
||||
self.error = error
|
||||
|
||||
class EventManager:
|
||||
"""事件管理器"""
|
||||
def __init__(self):
|
||||
self._observers = []
|
||||
|
||||
def add_observer(self, observer):
|
||||
self._observers.append(observer)
|
||||
|
||||
def notify(self, event):
|
||||
for observer in self._observers:
|
||||
observer.handle_event(event)
|
||||
```
|
||||
|
||||
## 🔧 具体实现建议
|
||||
|
||||
### 1. 配置管理重构
|
||||
```python
|
||||
@dataclass
|
||||
class SaveConfig:
|
||||
"""保存配置数据类"""
|
||||
filename_prefix: str = "ComfyUI"
|
||||
filename_keys: List[str] = field(default_factory=lambda: ["sampler_name", "cfg", "steps"])
|
||||
foldername_prefix: str = ""
|
||||
foldername_keys: List[str] = field(default_factory=lambda: ["ckpt_name"])
|
||||
delimiter: str = "-"
|
||||
output_format: str = ".webp"
|
||||
quality: int = 75
|
||||
save_metadata: bool = True
|
||||
counter_digits: int = 4
|
||||
|
||||
def validate(self):
|
||||
"""配置验证"""
|
||||
validator = ConfigValidator()
|
||||
validator.validate(self)
|
||||
```
|
||||
|
||||
### 2. 异步处理支持
|
||||
```python
|
||||
import asyncio
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
class AsyncImageSaver:
|
||||
"""异步图像保存器"""
|
||||
def __init__(self, max_workers=4):
|
||||
self.executor = ThreadPoolExecutor(max_workers=max_workers)
|
||||
|
||||
async def save_images_async(self, images, config):
|
||||
"""异步保存多张图片"""
|
||||
tasks = []
|
||||
for image in images:
|
||||
task = asyncio.create_task(
|
||||
self._save_single_image_async(image, config)
|
||||
)
|
||||
tasks.append(task)
|
||||
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
return results
|
||||
```
|
||||
|
||||
## 📈 性能监控建议
|
||||
|
||||
### 1. 性能指标收集
|
||||
```python
|
||||
class PerformanceMonitor:
|
||||
"""性能监控"""
|
||||
def __init__(self):
|
||||
self.metrics = {
|
||||
"save_time": [],
|
||||
"file_size": [],
|
||||
"metadata_size": [],
|
||||
"error_count": 0
|
||||
}
|
||||
|
||||
def record_save_operation(self, duration, file_size, metadata_size):
|
||||
self.metrics["save_time"].append(duration)
|
||||
self.metrics["file_size"].append(file_size)
|
||||
self.metrics["metadata_size"].append(metadata_size)
|
||||
|
||||
def get_statistics(self):
|
||||
return {
|
||||
"avg_save_time": np.mean(self.metrics["save_time"]),
|
||||
"total_files": len(self.metrics["save_time"]),
|
||||
"error_rate": self.metrics["error_count"] / len(self.metrics["save_time"])
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 优先级建议
|
||||
|
||||
### 高优先级 (立即实施)
|
||||
1. **配置验证和错误处理改进**
|
||||
2. **计数器性能优化**
|
||||
3. **基本的代码重构(拆分大方法)**
|
||||
|
||||
### 中优先级 (短期实施)
|
||||
1. **文件名预览功能**
|
||||
2. **配置模板系统**
|
||||
3. **元数据缓存优化**
|
||||
|
||||
### 低优先级 (长期规划)
|
||||
1. **完整架构重构**
|
||||
2. **异步处理支持**
|
||||
3. **插件系统扩展**
|
||||
|
||||
## � 移除Web组件建议
|
||||
|
||||
### 当前Web组件分析
|
||||
当前的web组件功能非常有限:
|
||||
- **contextmenu.js**: 只添加了一个帮助弹窗设置
|
||||
- **shared_utils.js**: 通用工具函数,但未被使用
|
||||
- **静态资源**: 只是为了访问assets目录
|
||||
|
||||
### 纯Python实现的优势
|
||||
|
||||
#### 1. 简化架构
|
||||
```python
|
||||
# 移除web依赖,简化__init__.py
|
||||
from .save_image_extended import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
|
||||
# 不再需要:
|
||||
# - WEB_DIRECTORY
|
||||
# - aiohttp导入
|
||||
# - 静态路由设置
|
||||
```
|
||||
|
||||
#### 2. 配置管理纯Python化
|
||||
```python
|
||||
class SaveImageConfig:
|
||||
"""纯Python配置管理"""
|
||||
|
||||
# 替代JavaScript设置的Python配置
|
||||
HELP_ENABLED = True
|
||||
|
||||
# 预设模板(替代web界面选择)
|
||||
PRESETS = {
|
||||
"simple": {
|
||||
"filename_keys": ["sampler_name", "steps"],
|
||||
"foldername_keys": ["ckpt_name"],
|
||||
"delimiter": "-"
|
||||
},
|
||||
"detailed": {
|
||||
"filename_keys": ["sampler_name", "cfg", "steps", "%Y-%m-%d_%H-%M-%S"],
|
||||
"foldername_keys": ["ckpt_name", "./sampler_name"],
|
||||
"delimiter": "_"
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_preset(cls, preset_name):
|
||||
"""获取预设配置"""
|
||||
return cls.PRESETS.get(preset_name, cls.PRESETS["simple"])
|
||||
```
|
||||
|
||||
#### 3. 帮助系统Python化
|
||||
```python
|
||||
class HelpSystem:
|
||||
"""纯Python帮助系统"""
|
||||
|
||||
HELP_TEXT = {
|
||||
"filename_keys": """
|
||||
文件名键值说明:
|
||||
- sampler_name: 采样器名称
|
||||
- cfg: CFG值
|
||||
- steps: 步数
|
||||
- %Y-%m-%d: 日期格式
|
||||
- 节点ID.参数名: 如 "5.seed" 获取节点5的seed值
|
||||
""",
|
||||
"foldername_keys": """
|
||||
文件夹键值说明:
|
||||
- ckpt_name: 模型名称
|
||||
- ./子文件夹: 创建子文件夹
|
||||
- ../上级文件夹: 创建上级文件夹
|
||||
"""
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_help(cls, field_name):
|
||||
"""获取字段帮助信息"""
|
||||
return cls.HELP_TEXT.get(field_name, "暂无帮助信息")
|
||||
```
|
||||
|
||||
#### 4. 验证和预览纯Python化
|
||||
```python
|
||||
class ConfigValidator:
|
||||
"""配置验证器"""
|
||||
|
||||
def validate_filename_keys(self, keys_str):
|
||||
"""验证文件名键值"""
|
||||
keys = [k.strip() for k in keys_str.split(',')]
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
for key in keys:
|
||||
if not key:
|
||||
continue
|
||||
|
||||
# 检查节点引用格式
|
||||
if '.' in key and not '%' in key:
|
||||
parts = key.split('.')
|
||||
if len(parts) == 2 and parts[0].isdigit():
|
||||
# 节点引用格式正确
|
||||
continue
|
||||
else:
|
||||
errors.append(f"节点引用格式错误: {key}")
|
||||
|
||||
# 检查时间格式
|
||||
elif '%' in key:
|
||||
try:
|
||||
datetime.now().strftime(key)
|
||||
except ValueError:
|
||||
errors.append(f"时间格式错误: {key}")
|
||||
|
||||
return {
|
||||
"valid": len(errors) == 0,
|
||||
"errors": errors,
|
||||
"warnings": warnings
|
||||
}
|
||||
|
||||
def preview_filename(self, config, sample_prompt=None):
|
||||
"""预览生成的文件名"""
|
||||
# 使用示例数据生成预览
|
||||
sample_data = {
|
||||
"sampler_name": "euler_a",
|
||||
"cfg": "7.5",
|
||||
"steps": "20"
|
||||
}
|
||||
|
||||
generator = FileNameGenerator(config)
|
||||
return generator.generate_preview(sample_data)
|
||||
```
|
||||
|
||||
### 移除步骤
|
||||
|
||||
#### 第一步:清理文件结构
|
||||
```bash
|
||||
# 删除web相关文件
|
||||
rm -rf web/
|
||||
rm -rf assets/ # 如果不需要的话
|
||||
```
|
||||
|
||||
#### 第二步:简化__init__.py
|
||||
```python
|
||||
# 新的简化版本
|
||||
from .save_image_extended import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
```
|
||||
|
||||
#### 第三步:增强Python配置
|
||||
```python
|
||||
# 在SaveImageExtended类中添加
|
||||
@classmethod
|
||||
def get_config_help(cls):
|
||||
"""获取配置帮助信息"""
|
||||
return {
|
||||
"filename_keys": "支持的键值: sampler_name, cfg, steps, 节点ID.参数名, %时间格式%",
|
||||
"foldername_keys": "支持的键值: ckpt_name, ./子文件夹, 节点ID.参数名",
|
||||
"delimiter": "文件名分隔符,建议使用 - 或 _",
|
||||
"quality": "图像质量 1-100,100为无损(仅AVIF/WebP)"
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def validate_config(cls, config):
|
||||
"""验证配置有效性"""
|
||||
validator = ConfigValidator()
|
||||
return validator.validate_all(config)
|
||||
```
|
||||
|
||||
### 优势总结
|
||||
|
||||
1. **简化部署** - 无需处理静态文件和web路由
|
||||
2. **减少依赖** - 移除aiohttp等web相关依赖
|
||||
3. **提高性能** - 无web服务器开销
|
||||
4. **易于维护** - 单一语言栈,无需维护JavaScript
|
||||
5. **更好集成** - 与ComfyUI的Python生态更好集成
|
||||
6. **跨平台** - 避免web相关的跨平台问题
|
||||
|
||||
## �📝 实施建议
|
||||
|
||||
1. **渐进式重构**:不要一次性重写所有代码,而是逐步重构
|
||||
2. **向后兼容**:确保新版本与现有配置兼容
|
||||
3. **充分测试**:为每个模块编写单元测试
|
||||
4. **文档更新**:同步更新用户文档和开发文档
|
||||
5. **性能基准**:建立性能基准测试,确保优化效果
|
||||
6. **移除Web组件**:优先移除不必要的web依赖,简化架构
|
||||
|
||||
通过这些优化,插件将变得更加高效、易用和可维护。
|
||||
-335
@@ -1,335 +0,0 @@
|
||||
# Save Image Extended v3.0 开发者文档
|
||||
|
||||
## 架构概述
|
||||
|
||||
Save Image Extended v3.0 采用模块化架构设计,遵循 SOLID 原则,提供高度可扩展和可维护的代码结构。
|
||||
|
||||
## 核心模块
|
||||
|
||||
### 1. 配置管理 (`core/config.py`)
|
||||
|
||||
#### SaveConfig
|
||||
数据类,定义所有配置参数:
|
||||
```python
|
||||
@dataclass
|
||||
class SaveConfig:
|
||||
filename_prefix: str = "ComfyUI"
|
||||
filename_keys: str = "sampler_name, cfg, steps, %F %H-%M-%S"
|
||||
output_format: str = ".webp"
|
||||
quality: int = 75
|
||||
# ... 其他配置项
|
||||
```
|
||||
|
||||
#### ConfigManager
|
||||
配置管理器,提供验证、预设和帮助功能:
|
||||
```python
|
||||
manager = ConfigManager()
|
||||
config = manager.get_preset_config("基础配置")
|
||||
validation = manager.validate_config(config)
|
||||
```
|
||||
|
||||
### 2. 文件名生成 (`core/filename_generator.py`)
|
||||
|
||||
#### FileNameGenerator
|
||||
负责生成文件名和文件夹名:
|
||||
```python
|
||||
generator = FileNameGenerator(config)
|
||||
filename = generator.generate_filename(prompt, timestamp)
|
||||
foldername = generator.generate_foldername(prompt, timestamp)
|
||||
```
|
||||
|
||||
#### ParameterExtractor
|
||||
从 ComfyUI 提示词中提取参数,支持缓存:
|
||||
```python
|
||||
extractor = ParameterExtractor()
|
||||
values = extractor.find_parameter_values(prompt, keys)
|
||||
```
|
||||
|
||||
### 3. 图像保存 (`core/image_saver.py`)
|
||||
|
||||
#### ImageSaver
|
||||
协调器类,整合所有组件:
|
||||
```python
|
||||
saver = ImageSaver(config, output_dir)
|
||||
results = saver.save_images(images, prompt, extra_pnginfo)
|
||||
```
|
||||
|
||||
### 4. 格式策略 (`strategies/`)
|
||||
|
||||
使用策略模式处理不同图像格式:
|
||||
|
||||
#### FormatStrategy (抽象基类)
|
||||
```python
|
||||
class FormatStrategy(ABC):
|
||||
@abstractmethod
|
||||
def save_image(self, image: Image.Image, filepath: str,
|
||||
config: SaveConfig, metadata: Dict) -> bool
|
||||
```
|
||||
|
||||
#### 具体策略实现
|
||||
- `PngStrategy`: PNG 格式处理
|
||||
- `WebpStrategy`: WebP 格式处理
|
||||
- `AvifStrategy`: AVIF 格式处理
|
||||
- 等等...
|
||||
|
||||
### 5. 元数据处理 (`core/metadata_handler.py`)
|
||||
|
||||
#### MetadataHandler
|
||||
处理图像元数据的保存和转换:
|
||||
```python
|
||||
handler = MetadataHandler()
|
||||
metadata = handler.prepare_metadata(prompt, extra_pnginfo, positive_text, negative_text)
|
||||
```
|
||||
|
||||
### 6. 路径管理 (`core/path_manager.py`)
|
||||
|
||||
#### PathManager
|
||||
处理文件路径创建和验证:
|
||||
```python
|
||||
manager = PathManager()
|
||||
full_path = manager.create_output_path(output_dir, foldername, filename)
|
||||
```
|
||||
|
||||
### 7. 计数器管理 (`core/counter_manager.py`)
|
||||
|
||||
#### CounterManager
|
||||
管理文件计数器,支持缓存:
|
||||
```python
|
||||
manager = CounterManager()
|
||||
counter = manager.get_next_counter(folder_path, filename_pattern)
|
||||
```
|
||||
|
||||
### 8. 作业数据导出 (`core/job_data_exporter.py`)
|
||||
|
||||
#### JobDataExporter
|
||||
导出作业数据到 JSON 文件:
|
||||
```python
|
||||
exporter = JobDataExporter(config)
|
||||
exporter.export_job_data(prompt, extra_pnginfo, output_path)
|
||||
```
|
||||
|
||||
### 9. 错误处理 (`core/error_handler.py`)
|
||||
|
||||
#### ErrorHandler
|
||||
统一的错误处理和日志系统:
|
||||
```python
|
||||
handler = ErrorHandler()
|
||||
with handler.create_error_context("保存图像"):
|
||||
# 执行操作
|
||||
pass
|
||||
```
|
||||
|
||||
### 10. 性能优化 (`core/performance_optimizer.py`)
|
||||
|
||||
#### PerformanceOptimizer
|
||||
提供缓存、批量处理和性能监控:
|
||||
```python
|
||||
optimizer = PerformanceOptimizer()
|
||||
result = optimizer.optimize_operation("operation", func, *args)
|
||||
```
|
||||
|
||||
## 设计模式
|
||||
|
||||
### 1. 策略模式 (Strategy Pattern)
|
||||
用于处理不同的图像格式,每种格式有独立的保存策略。
|
||||
|
||||
### 2. 工厂模式 (Factory Pattern)
|
||||
在 `ImageSaver` 中创建格式策略实例。
|
||||
|
||||
### 3. 单例模式 (Singleton Pattern)
|
||||
全局错误处理器使用单例模式。
|
||||
|
||||
### 4. 观察者模式 (Observer Pattern)
|
||||
性能监控器可以观察各种操作的执行情况。
|
||||
|
||||
## 扩展指南
|
||||
|
||||
### 添加新的图像格式
|
||||
|
||||
1. **创建策略类**:
|
||||
```python
|
||||
# strategies/new_format_strategy.py
|
||||
class NewFormatStrategy(FormatStrategy):
|
||||
def save_image(self, image: Image.Image, filepath: str,
|
||||
config: SaveConfig, metadata: Dict) -> bool:
|
||||
# 实现保存逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
2. **注册策略**:
|
||||
```python
|
||||
# core/image_saver.py
|
||||
def _create_format_strategies(self) -> Dict[str, FormatStrategy]:
|
||||
return {
|
||||
# ... 现有格式
|
||||
'.newformat': NewFormatStrategy(),
|
||||
}
|
||||
```
|
||||
|
||||
3. **更新配置**:
|
||||
```python
|
||||
# 在 INPUT_TYPES 中添加新格式
|
||||
output_formats = ['.webp', '.png', '.newformat']
|
||||
```
|
||||
|
||||
### 添加新的配置参数
|
||||
|
||||
1. **更新 SaveConfig**:
|
||||
```python
|
||||
@dataclass
|
||||
class SaveConfig:
|
||||
# ... 现有参数
|
||||
new_parameter: str = "default_value"
|
||||
```
|
||||
|
||||
2. **更新验证逻辑**:
|
||||
```python
|
||||
# core/config.py ConfigManager.validate_config()
|
||||
if not self._validate_new_parameter(config.new_parameter):
|
||||
result.add_error("新参数验证失败")
|
||||
```
|
||||
|
||||
3. **更新 INPUT_TYPES**:
|
||||
```python
|
||||
"new_parameter": ("STRING", {"default": "default_value"}),
|
||||
```
|
||||
|
||||
### 添加新的缓存策略
|
||||
|
||||
1. **扩展 CacheManager**:
|
||||
```python
|
||||
class CustomCacheManager(CacheManager):
|
||||
def custom_cache_logic(self):
|
||||
# 实现自定义缓存逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
2. **在相关模块中使用**:
|
||||
```python
|
||||
self.cache_manager = CustomCacheManager()
|
||||
```
|
||||
|
||||
## 测试指南
|
||||
|
||||
### 运行测试
|
||||
```bash
|
||||
# 运行所有测试
|
||||
python tests/run_tests.py
|
||||
|
||||
# 运行特定模块测试
|
||||
python tests/run_tests.py config
|
||||
```
|
||||
|
||||
### 编写新测试
|
||||
```python
|
||||
# tests/test_new_module.py
|
||||
import unittest
|
||||
from core.new_module import NewClass
|
||||
|
||||
class TestNewClass(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.instance = NewClass()
|
||||
|
||||
def test_functionality(self):
|
||||
result = self.instance.method()
|
||||
self.assertEqual(result, expected_value)
|
||||
```
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 缓存策略
|
||||
- **文件名生成**: 缓存参数提取结果
|
||||
- **计数器管理**: 缓存文件夹计数器
|
||||
- **元数据处理**: 缓存元数据转换结果
|
||||
|
||||
### 批量处理
|
||||
- 小批量 (≤2): 单线程处理
|
||||
- 大批量 (>2): 多线程并行处理
|
||||
|
||||
### 内存管理
|
||||
- 自动垃圾回收
|
||||
- 内存使用监控
|
||||
- 缓存大小限制
|
||||
|
||||
## 调试和日志
|
||||
|
||||
### 日志级别
|
||||
- **DEBUG**: 详细的调试信息
|
||||
- **INFO**: 一般信息
|
||||
- **WARNING**: 警告信息
|
||||
- **ERROR**: 错误信息
|
||||
|
||||
### 启用调试模式
|
||||
```python
|
||||
import logging
|
||||
logging.getLogger('save_image_extended').setLevel(logging.DEBUG)
|
||||
```
|
||||
|
||||
### 性能监控
|
||||
```python
|
||||
optimizer = PerformanceOptimizer()
|
||||
report = optimizer.get_performance_report()
|
||||
print(report)
|
||||
```
|
||||
|
||||
## 代码规范
|
||||
|
||||
### 命名约定
|
||||
- 类名: PascalCase (`SaveConfig`)
|
||||
- 方法名: snake_case (`generate_filename`)
|
||||
- 常量: UPPER_CASE (`VERSION`)
|
||||
- 私有方法: 前缀下划线 (`_internal_method`)
|
||||
|
||||
### 文档字符串
|
||||
```python
|
||||
def method(self, param: str) -> bool:
|
||||
"""
|
||||
方法描述
|
||||
|
||||
Args:
|
||||
param: 参数描述
|
||||
|
||||
Returns:
|
||||
返回值描述
|
||||
|
||||
Raises:
|
||||
Exception: 异常描述
|
||||
"""
|
||||
```
|
||||
|
||||
### 类型注解
|
||||
所有公共方法都应该包含类型注解:
|
||||
```python
|
||||
def process_image(self, image: Image.Image, config: SaveConfig) -> Optional[str]:
|
||||
pass
|
||||
```
|
||||
|
||||
## 部署和发布
|
||||
|
||||
### 版本管理
|
||||
版本号格式: `MAJOR.MINOR.PATCH`
|
||||
- MAJOR: 不兼容的 API 变更
|
||||
- MINOR: 向后兼容的功能添加
|
||||
- PATCH: 向后兼容的错误修复
|
||||
|
||||
### 发布检查清单
|
||||
1. 运行所有测试
|
||||
2. 更新版本号
|
||||
3. 更新文档
|
||||
4. 检查依赖项
|
||||
5. 验证向后兼容性
|
||||
|
||||
## 贡献指南
|
||||
|
||||
### 提交代码
|
||||
1. Fork 项目
|
||||
2. 创建功能分支
|
||||
3. 编写测试
|
||||
4. 提交代码
|
||||
5. 创建 Pull Request
|
||||
|
||||
### 代码审查
|
||||
- 遵循代码规范
|
||||
- 包含适当的测试
|
||||
- 更新相关文档
|
||||
- 确保向后兼容性
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
# Save Image Extended v3.0 用户手册
|
||||
|
||||
## 概述
|
||||
|
||||
Save Image Extended 是一个功能强大的 ComfyUI 插件,用于保存图像到多种格式,支持自定义文件名、文件夹结构、元数据保存等高级功能。
|
||||
|
||||
## 主要特性
|
||||
|
||||
### 🎯 核心功能
|
||||
- **多格式支持**: PNG, WebP, JPEG, AVIF, JXL, TIFF, GIF, BMP
|
||||
- **自定义命名**: 灵活的文件名和文件夹命名系统
|
||||
- **元数据保存**: 保留生成参数和提示词信息
|
||||
- **批量处理**: 高效的批量图像保存
|
||||
- **作业数据导出**: JSON格式的详细生成信息
|
||||
|
||||
### ⚡ 性能特性
|
||||
- **智能缓存**: 文件名生成和参数提取缓存
|
||||
- **并行处理**: 多线程批量保存
|
||||
- **内存优化**: 自动内存管理和清理
|
||||
- **错误恢复**: 完善的错误处理机制
|
||||
|
||||
## 安装说明
|
||||
|
||||
### 基础安装
|
||||
1. 将插件文件夹复制到 ComfyUI 的 `custom_nodes` 目录
|
||||
2. 重启 ComfyUI
|
||||
|
||||
### 可选依赖
|
||||
```bash
|
||||
# AVIF 支持
|
||||
pip install pillow-avif-plugin
|
||||
|
||||
# JXL 支持 (需要 MSVC 编译环境)
|
||||
pip install jxlpy
|
||||
```
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 基本使用
|
||||
|
||||
1. **添加节点**: 在 ComfyUI 中搜索 "Save Image Extended"
|
||||
2. **连接图像**: 将图像输出连接到节点的 images 输入
|
||||
3. **配置参数**: 根据需要调整各项设置
|
||||
4. **运行工作流**: 图像将按配置保存到输出目录
|
||||
|
||||
### 参数说明
|
||||
|
||||
#### 文件命名参数
|
||||
- **filename_prefix**: 文件名前缀 (默认: "ComfyUI")
|
||||
- **filename_keys**: 文件名组成元素,用逗号分隔
|
||||
- **delimiter**: 文件名元素间的分隔符 (默认: "-")
|
||||
|
||||
#### 文件夹参数
|
||||
- **foldername_prefix**: 文件夹名前缀
|
||||
- **foldername_keys**: 文件夹名组成元素
|
||||
|
||||
#### 格式和质量
|
||||
- **output_format**: 输出格式 (.webp, .png, .jpg 等)
|
||||
- **quality**: 图像质量 (1-100,适用于有损格式)
|
||||
|
||||
#### 计数器设置
|
||||
- **counter_digits**: 计数器位数 (默认: 4)
|
||||
- **counter_position**: 计数器位置 (first/last)
|
||||
- **one_counter_per_folder**: 每个文件夹独立计数
|
||||
|
||||
#### 元数据和作业数据
|
||||
- **save_metadata**: 是否保存元数据
|
||||
- **save_job_data**: 作业数据保存级别
|
||||
- **job_data_per_image**: 每张图像单独的作业数据文件
|
||||
|
||||
### 高级功能
|
||||
|
||||
#### 1. 参数引用系统
|
||||
可以在文件名中引用工作流中的任何参数:
|
||||
|
||||
```
|
||||
# 基本参数引用
|
||||
sampler_name, cfg, steps
|
||||
|
||||
# 节点参数引用 (节点ID.参数名)
|
||||
5.seed, 3.ckpt_name
|
||||
|
||||
# 时间格式
|
||||
%Y-%m-%d, %H-%M-%S
|
||||
```
|
||||
|
||||
#### 2. 文件夹结构
|
||||
支持创建复杂的文件夹结构:
|
||||
|
||||
```
|
||||
# 基本文件夹
|
||||
ckpt_name
|
||||
|
||||
# 嵌套文件夹
|
||||
%Y/%m/%d
|
||||
|
||||
# 混合结构
|
||||
models/ckpt_name/%F
|
||||
```
|
||||
|
||||
#### 3. 配置预设
|
||||
插件提供三种预设配置:
|
||||
|
||||
- **基础配置**: 简单的文件名和格式设置
|
||||
- **高级配置**: 完整的自定义选项
|
||||
- **专业配置**: 包含元数据和作业数据导出
|
||||
|
||||
## 配置示例
|
||||
|
||||
### 示例 1: 基础配置
|
||||
```
|
||||
filename_prefix: "AI_Art"
|
||||
filename_keys: "sampler_name, steps, %H-%M-%S"
|
||||
output_format: ".webp"
|
||||
quality: 85
|
||||
```
|
||||
生成文件名: `AI_Art-euler-20-14-30-25_0001.webp`
|
||||
|
||||
### 示例 2: 高级配置
|
||||
```
|
||||
filename_prefix: ""
|
||||
filename_keys: "5.ckpt_name, sampler_name, cfg, steps, %F"
|
||||
foldername_keys: "%Y/%m, 5.ckpt_name"
|
||||
delimiter: "_"
|
||||
counter_position: "first"
|
||||
```
|
||||
生成路径: `2024/01/sd_xl_base/0001_sd_xl_base_euler_7.5_20_2024-01-15.webp`
|
||||
|
||||
### 示例 3: 专业配置
|
||||
```
|
||||
filename_keys: "sampler_name, scheduler, cfg, steps, 5.seed"
|
||||
save_metadata: true
|
||||
save_job_data: "prompt"
|
||||
job_data_per_image: true
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
#### 1. 格式不支持
|
||||
**问题**: 某些格式无法使用
|
||||
**解决**: 检查可选依赖是否已安装
|
||||
|
||||
#### 2. 文件名包含无效字符
|
||||
**问题**: 生成的文件名包含系统不支持的字符
|
||||
**解决**: 插件会自动清理无效字符,或手动调整命名规则
|
||||
|
||||
#### 3. 性能问题
|
||||
**问题**: 大批量保存时速度慢
|
||||
**解决**:
|
||||
- 减少元数据保存
|
||||
- 使用更快的格式 (如 WebP)
|
||||
- 调整质量设置
|
||||
|
||||
#### 4. 内存不足
|
||||
**问题**: 处理大量图像时内存不足
|
||||
**解决**: 插件会自动进行内存管理,如仍有问题可重启 ComfyUI
|
||||
|
||||
### 日志和调试
|
||||
|
||||
插件会在控制台输出详细的日志信息:
|
||||
- 信息级别: 正常操作状态
|
||||
- 警告级别: 潜在问题
|
||||
- 错误级别: 需要注意的问题
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
### 1. 格式选择
|
||||
- **WebP**: 平衡质量和文件大小的最佳选择
|
||||
- **PNG**: 无损质量,但文件较大
|
||||
- **JPEG**: 最小文件大小,有损压缩
|
||||
- **AVIF**: 最新格式,优秀的压缩比
|
||||
|
||||
### 2. 质量设置
|
||||
- **WebP/JPEG**: 75-85 通常是最佳平衡点
|
||||
- **AVIF**: 60-70 可获得优秀效果
|
||||
- **PNG**: 质量设置不影响图像质量,只影响压缩级别
|
||||
|
||||
### 3. 批量处理
|
||||
- 大批量保存时,插件会自动使用多线程处理
|
||||
- 避免同时保存过多不同格式
|
||||
- 合理设置计数器位数
|
||||
|
||||
## 技术支持
|
||||
|
||||
如遇到问题,请提供以下信息:
|
||||
1. ComfyUI 版本
|
||||
2. 插件版本 (v3.0)
|
||||
3. 错误日志
|
||||
4. 使用的配置参数
|
||||
5. 系统环境信息
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v3.0 (重构版本)
|
||||
- 完全重构的模块化架构
|
||||
- 新增性能优化和缓存机制
|
||||
- 改进的错误处理和日志系统
|
||||
- 更好的内存管理
|
||||
- 增强的配置验证
|
||||
- 完善的单元测试覆盖
|
||||
|
||||
### 向后兼容性
|
||||
v3.0 保持与之前版本的配置兼容性,现有工作流无需修改即可使用。
|
||||
-214
@@ -1,214 +0,0 @@
|
||||
# Save Image Extended 重构分析报告
|
||||
|
||||
## 🔍 当前代码结构问题分析
|
||||
|
||||
### 1. 单一职责原则违反
|
||||
`SaveImageExtended` 类承担了过多职责:
|
||||
- **UI配置管理** (INPUT_TYPES方法)
|
||||
- **文件名生成** (generate_custom_name方法)
|
||||
- **参数提取** (find_keys_recursively, find_parameter_values方法)
|
||||
- **计数器管理** (get_latest_counter方法)
|
||||
- **元数据处理** (get_metadata_png, get_metadata_exif方法)
|
||||
- **图像保存** (save_image方法)
|
||||
- **JSON导出** (save_job_to_json方法)
|
||||
- **路径处理** (get_subfolder_path方法)
|
||||
- **主要业务逻辑** (save_images方法)
|
||||
|
||||
### 2. 方法过长问题
|
||||
- `save_images`: 100+行,包含多个不同的逻辑块
|
||||
- `generate_custom_name`: 100+行,复杂的字符串处理逻辑
|
||||
- `save_job_to_json`: 90+行,复杂的数据收集和保存逻辑
|
||||
|
||||
### 3. 硬编码配置问题
|
||||
```python
|
||||
# 配置散布在类的各个地方
|
||||
png_compress_level = 9
|
||||
avif_quality = 60
|
||||
webp_quality = 75
|
||||
jpeg_quality = 91
|
||||
# ... 更多硬编码配置
|
||||
```
|
||||
|
||||
### 4. 重复代码问题
|
||||
- 图像格式处理逻辑重复
|
||||
- 元数据处理有相似的模式
|
||||
- 错误处理模式重复
|
||||
|
||||
### 5. 测试困难
|
||||
- 方法耦合度高,难以单独测试
|
||||
- 依赖外部状态(文件系统、ComfyUI框架)
|
||||
- 没有依赖注入机制
|
||||
|
||||
## 🎯 重构目标架构
|
||||
|
||||
### 核心设计原则
|
||||
1. **单一职责原则** - 每个类只负责一个功能
|
||||
2. **开闭原则** - 对扩展开放,对修改关闭
|
||||
3. **依赖倒置原则** - 依赖抽象而不是具体实现
|
||||
4. **接口隔离原则** - 使用小而专一的接口
|
||||
|
||||
### 新架构设计
|
||||
|
||||
```
|
||||
SaveImageExtended (ComfyUI节点接口)
|
||||
├── ConfigManager (配置管理)
|
||||
├── FileNameGenerator (文件名生成)
|
||||
├── CounterManager (计数器管理)
|
||||
├── MetadataHandler (元数据处理)
|
||||
├── ImageSaver (图像保存协调器)
|
||||
│ ├── FormatStrategy (格式策略接口)
|
||||
│ │ ├── PNGStrategy
|
||||
│ │ ├── WebPStrategy
|
||||
│ │ ├── AVIFStrategy
|
||||
│ │ └── JPEGStrategy
|
||||
├── JobDataExporter (作业数据导出)
|
||||
└── PathManager (路径管理)
|
||||
```
|
||||
|
||||
## 📋 重构计划
|
||||
|
||||
### 阶段1: 移除Web组件 (立即执行)
|
||||
- 删除 `web/` 目录
|
||||
- 简化 `__init__.py`
|
||||
- 移除web相关依赖
|
||||
|
||||
### 阶段2: 配置管理重构
|
||||
- 创建 `ConfigManager` 类
|
||||
- 实现配置验证
|
||||
- 添加预设模板支持
|
||||
|
||||
### 阶段3: 核心逻辑拆分
|
||||
- 提取 `FileNameGenerator`
|
||||
- 提取 `CounterManager`
|
||||
- 提取 `MetadataHandler`
|
||||
|
||||
### 阶段4: 图像保存重构
|
||||
- 实现策略模式的格式处理
|
||||
- 创建 `ImageSaver` 协调器
|
||||
- 优化性能和错误处理
|
||||
|
||||
### 阶段5: 测试和文档
|
||||
- 编写单元测试
|
||||
- 性能基准测试
|
||||
- 更新文档
|
||||
|
||||
## 🔧 具体重构步骤
|
||||
|
||||
### 步骤1: 创建基础架构
|
||||
1. 创建 `core/` 目录存放核心模块
|
||||
2. 创建 `strategies/` 目录存放策略模式实现
|
||||
3. 创建 `utils/` 目录存放工具类
|
||||
4. 创建 `tests/` 目录存放测试
|
||||
|
||||
### 步骤2: 配置管理
|
||||
```python
|
||||
# core/config.py
|
||||
@dataclass
|
||||
class SaveConfig:
|
||||
filename_prefix: str = "ComfyUI"
|
||||
filename_keys: List[str] = field(default_factory=list)
|
||||
foldername_prefix: str = ""
|
||||
foldername_keys: List[str] = field(default_factory=list)
|
||||
delimiter: str = "-"
|
||||
output_format: str = ".webp"
|
||||
quality: int = 75
|
||||
save_metadata: bool = True
|
||||
counter_digits: int = 4
|
||||
counter_position: str = "last"
|
||||
one_counter_per_folder: bool = True
|
||||
|
||||
def validate(self) -> ValidationResult:
|
||||
"""验证配置有效性"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 步骤3: 文件名生成器
|
||||
```python
|
||||
# core/filename_generator.py
|
||||
class FileNameGenerator:
|
||||
def __init__(self, config: SaveConfig):
|
||||
self.config = config
|
||||
self.parameter_extractor = ParameterExtractor()
|
||||
|
||||
def generate_filename(self, prompt: dict, timestamp: datetime) -> str:
|
||||
"""生成文件名"""
|
||||
pass
|
||||
|
||||
def generate_foldername(self, prompt: dict, timestamp: datetime) -> str:
|
||||
"""生成文件夹名"""
|
||||
pass
|
||||
```
|
||||
|
||||
### 步骤4: 策略模式图像保存
|
||||
```python
|
||||
# strategies/format_strategy.py
|
||||
class FormatStrategy(ABC):
|
||||
@abstractmethod
|
||||
def save_image(self, image: Image, path: str, metadata: dict, quality: int) -> None:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def supports_metadata(self) -> bool:
|
||||
pass
|
||||
|
||||
# strategies/webp_strategy.py
|
||||
class WebPStrategy(FormatStrategy):
|
||||
def save_image(self, image: Image, path: str, metadata: dict, quality: int) -> None:
|
||||
kwargs = {"quality": quality}
|
||||
if quality == 100:
|
||||
kwargs["lossless"] = True
|
||||
if metadata:
|
||||
kwargs["exif"] = self._prepare_exif(metadata)
|
||||
image.save(path, **kwargs)
|
||||
```
|
||||
|
||||
## 📊 预期收益
|
||||
|
||||
### 性能提升
|
||||
- **计数器查找**: 从O(n)优化到O(1) (缓存机制)
|
||||
- **批量处理**: 支持并行处理多张图片
|
||||
- **元数据缓存**: 避免重复计算
|
||||
|
||||
### 代码质量
|
||||
- **可测试性**: 每个模块可独立测试
|
||||
- **可维护性**: 清晰的职责分离
|
||||
- **可扩展性**: 易于添加新格式支持
|
||||
|
||||
### 用户体验
|
||||
- **配置验证**: 实时错误提示
|
||||
- **文件名预览**: 所见即所得
|
||||
- **错误处理**: 详细的错误信息
|
||||
|
||||
## 🚀 实施时间表
|
||||
|
||||
| 阶段 | 预计时间 | 主要任务 |
|
||||
|------|----------|----------|
|
||||
| 阶段1 | 1天 | 移除Web组件 |
|
||||
| 阶段2 | 2天 | 配置管理重构 |
|
||||
| 阶段3 | 3天 | 核心逻辑拆分 |
|
||||
| 阶段4 | 3天 | 图像保存重构 |
|
||||
| 阶段5 | 2天 | 测试和文档 |
|
||||
|
||||
**总计**: 约11天完成完整重构
|
||||
|
||||
## 📝 风险评估
|
||||
|
||||
### 高风险
|
||||
- **向后兼容性**: 确保现有配置仍然有效
|
||||
- **ComfyUI集成**: 保持与ComfyUI框架的兼容性
|
||||
|
||||
### 中风险
|
||||
- **性能回归**: 重构可能引入性能问题
|
||||
- **功能遗漏**: 可能遗漏某些边缘功能
|
||||
|
||||
### 低风险
|
||||
- **测试覆盖**: 新代码需要充分测试
|
||||
- **文档更新**: 需要同步更新文档
|
||||
|
||||
## 🎯 成功指标
|
||||
|
||||
1. **功能完整性**: 所有现有功能正常工作
|
||||
2. **性能提升**: 批量处理速度提升30%+
|
||||
3. **代码质量**: 测试覆盖率达到80%+
|
||||
4. **用户体验**: 配置错误减少50%+
|
||||
5. **维护性**: 新功能开发时间减少40%+
|
||||
-200
@@ -1,200 +0,0 @@
|
||||
# Save Image Extended v3.0 重构完成总结
|
||||
|
||||
## 重构概述
|
||||
|
||||
Save Image Extended 插件已成功完成全面重构,从单一的大型文件重构为模块化的现代架构。本次重构遵循 SOLID 原则,大幅提升了代码的可维护性、可扩展性和性能。
|
||||
|
||||
## 重构成果
|
||||
|
||||
### ✅ 已完成的任务
|
||||
|
||||
1. **[x] 分析现有代码结构** - 深入分析了原有代码的问题点
|
||||
2. **[x] 设计新的架构** - 基于SOLID原则设计了模块化架构
|
||||
3. **[x] 移除Web组件** - 删除了不必要的web相关代码
|
||||
4. **[x] 重构配置管理** - 创建了统一的配置管理系统
|
||||
5. **[x] 重构文件名生成器** - 独立的文件名生成模块
|
||||
6. **[x] 重构图像保存器** - 使用策略模式的图像保存逻辑
|
||||
7. **[x] 重构元数据处理** - 独立的元数据处理模块
|
||||
8. **[x] 添加错误处理和日志** - 完善的错误处理机制
|
||||
9. **[x] 编写单元测试** - 核心模块的单元测试覆盖
|
||||
10. **[x] 性能优化** - 批量处理和缓存机制
|
||||
11. **[x] 文档更新** - 完整的用户和开发文档
|
||||
|
||||
## 架构改进
|
||||
|
||||
### 原有架构问题
|
||||
- **单一巨型文件**: 719行的单个文件包含所有逻辑
|
||||
- **职责混乱**: 一个类承担多种职责
|
||||
- **难以测试**: 紧耦合的代码难以进行单元测试
|
||||
- **性能问题**: 缺乏缓存和优化机制
|
||||
- **错误处理不足**: 简单的错误处理逻辑
|
||||
|
||||
### 新架构优势
|
||||
- **模块化设计**: 11个独立的核心模块
|
||||
- **单一职责**: 每个模块专注于特定功能
|
||||
- **策略模式**: 灵活的格式处理策略
|
||||
- **完善缓存**: 多层次的缓存机制
|
||||
- **错误恢复**: 健壮的错误处理和日志系统
|
||||
|
||||
## 文件结构对比
|
||||
|
||||
### 重构前
|
||||
```
|
||||
save_image_extended.py (719行)
|
||||
web/
|
||||
├── js/
|
||||
└── assets/
|
||||
__init__.py
|
||||
```
|
||||
|
||||
### 重构后
|
||||
```
|
||||
save_image_extended.py (重构的主文件)
|
||||
core/
|
||||
├── __init__.py
|
||||
├── config.py (配置管理)
|
||||
├── filename_generator.py (文件名生成)
|
||||
├── counter_manager.py (计数器管理)
|
||||
├── metadata_handler.py (元数据处理)
|
||||
├── image_saver.py (图像保存协调器)
|
||||
├── job_data_exporter.py (作业数据导出)
|
||||
├── path_manager.py (路径管理)
|
||||
├── error_handler.py (错误处理)
|
||||
└── performance_optimizer.py (性能优化)
|
||||
strategies/
|
||||
├── __init__.py
|
||||
├── format_strategy.py (抽象基类)
|
||||
├── png_strategy.py
|
||||
├── webp_strategy.py
|
||||
├── avif_strategy.py
|
||||
├── jpeg_strategy.py
|
||||
├── jxl_strategy.py
|
||||
├── tiff_strategy.py
|
||||
├── gif_strategy.py
|
||||
└── bmp_strategy.py
|
||||
tests/
|
||||
├── __init__.py
|
||||
├── test_config.py
|
||||
├── test_filename_generator.py
|
||||
└── run_tests.py
|
||||
docs/
|
||||
├── 重构分析报告.md
|
||||
├── 用户手册.md
|
||||
├── 开发者文档.md
|
||||
└── 重构完成总结.md
|
||||
__init__.py (简化版)
|
||||
```
|
||||
|
||||
## 核心改进点
|
||||
|
||||
### 1. 配置管理系统
|
||||
- **统一配置**: `SaveConfig` 数据类统一管理所有配置
|
||||
- **配置验证**: 完整的参数验证机制
|
||||
- **预设模板**: 三种预设配置模板
|
||||
- **帮助系统**: 内置的参数说明和帮助
|
||||
|
||||
### 2. 策略模式实现
|
||||
- **格式策略**: 每种图像格式独立的保存策略
|
||||
- **易于扩展**: 新增格式只需添加新策略类
|
||||
- **统一接口**: 所有策略实现相同的接口
|
||||
|
||||
### 3. 性能优化
|
||||
- **多层缓存**: 文件名生成、参数提取、元数据处理缓存
|
||||
- **批量处理**: 智能的单线程/多线程处理选择
|
||||
- **内存管理**: 自动内存监控和清理
|
||||
- **性能监控**: 详细的性能指标收集
|
||||
|
||||
### 4. 错误处理
|
||||
- **统一日志**: 结构化的日志系统
|
||||
- **错误恢复**: 优雅的错误处理和恢复
|
||||
- **上下文管理**: 错误上下文管理器
|
||||
- **异常分类**: 不同类型的自定义异常
|
||||
|
||||
### 5. 测试覆盖
|
||||
- **单元测试**: 核心模块的单元测试
|
||||
- **测试运行器**: 自动化测试执行
|
||||
- **测试报告**: 详细的测试结果报告
|
||||
|
||||
## 性能提升
|
||||
|
||||
### 缓存机制
|
||||
- **文件名生成缓存**: 避免重复的参数提取和字符串处理
|
||||
- **计数器缓存**: 减少文件系统访问
|
||||
- **元数据缓存**: 避免重复的元数据转换
|
||||
|
||||
### 并行处理
|
||||
- **智能批量**: 根据批量大小选择处理策略
|
||||
- **多线程保存**: 大批量图像的并行保存
|
||||
- **内存优化**: 自动内存管理避免内存泄漏
|
||||
|
||||
### 预期性能改进
|
||||
- **文件名生成**: 50-70% 性能提升(通过缓存)
|
||||
- **批量保存**: 30-50% 性能提升(通过并行处理)
|
||||
- **内存使用**: 20-30% 减少(通过优化的内存管理)
|
||||
|
||||
## 代码质量提升
|
||||
|
||||
### SOLID 原则应用
|
||||
- **S - 单一职责**: 每个类专注于单一功能
|
||||
- **O - 开闭原则**: 通过策略模式支持扩展
|
||||
- **L - 里氏替换**: 策略类可以互相替换
|
||||
- **I - 接口隔离**: 清晰的接口定义
|
||||
- **D - 依赖倒置**: 依赖抽象而非具体实现
|
||||
|
||||
### 代码指标改进
|
||||
- **圈复杂度**: 从高复杂度降低到中等复杂度
|
||||
- **代码重复**: 消除了大量重复代码
|
||||
- **可测试性**: 从难以测试提升到完全可测试
|
||||
- **可维护性**: 大幅提升代码可维护性
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
### 保持兼容
|
||||
- **API 兼容**: 保持原有的 ComfyUI 节点接口
|
||||
- **配置兼容**: 现有工作流无需修改
|
||||
- **功能兼容**: 所有原有功能都得到保留
|
||||
|
||||
### 新增功能
|
||||
- **配置预设**: 三种预设配置模板
|
||||
- **性能监控**: 详细的性能指标
|
||||
- **错误恢复**: 更好的错误处理
|
||||
- **扩展性**: 更容易添加新功能
|
||||
|
||||
## 文档完善
|
||||
|
||||
### 用户文档
|
||||
- **用户手册**: 详细的使用指南和配置说明
|
||||
- **示例配置**: 多种实用的配置示例
|
||||
- **故障排除**: 常见问题和解决方案
|
||||
|
||||
### 开发文档
|
||||
- **架构文档**: 详细的架构设计说明
|
||||
- **API 文档**: 完整的 API 参考
|
||||
- **扩展指南**: 如何添加新功能的指南
|
||||
- **测试指南**: 测试编写和运行指南
|
||||
|
||||
## 下一步计划
|
||||
|
||||
### 短期目标
|
||||
1. **集成测试**: 在实际 ComfyUI 环境中进行全面测试
|
||||
2. **性能验证**: 验证性能改进的实际效果
|
||||
3. **用户反馈**: 收集用户使用反馈
|
||||
4. **Bug 修复**: 修复可能发现的问题
|
||||
|
||||
### 长期目标
|
||||
1. **新格式支持**: 添加更多图像格式支持
|
||||
2. **云存储**: 支持云存储服务
|
||||
3. **批量操作**: 更多的批量操作功能
|
||||
4. **AI 集成**: 集成 AI 功能进行智能命名
|
||||
|
||||
## 总结
|
||||
|
||||
Save Image Extended v3.0 的重构是一次全面的现代化改造,不仅解决了原有代码的技术债务,还为未来的功能扩展奠定了坚实的基础。新架构具有以下特点:
|
||||
|
||||
- **高度模块化**: 清晰的模块划分和职责分离
|
||||
- **优秀性能**: 多层次的性能优化机制
|
||||
- **健壮稳定**: 完善的错误处理和恢复机制
|
||||
- **易于维护**: 良好的代码结构和完整的文档
|
||||
- **高度可扩展**: 支持轻松添加新功能和格式
|
||||
|
||||
这次重构为插件的长期发展提供了强有力的技术支撑,确保了代码的可持续性和可维护性。
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-save-image-pro"
|
||||
description = "专业级 ComfyUI 图像保存插件,支持多格式、自定义命名和高级功能"
|
||||
version = "3.0.0"
|
||||
version = "latest"
|
||||
license = "GPL-3.0"
|
||||
dependencies = ["piexif", "pillow", "pillow-avif-plugin"]
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""
|
||||
Save Image Extended - ComfyUI插件 (重构版本)
|
||||
ComfyUI Save Image Pro - ComfyUI插件 (重构版本)
|
||||
|
||||
使用模块化架构重构的图像保存插件,支持多种格式和自定义配置。
|
||||
|
||||
@version: 3.0
|
||||
@version: latest
|
||||
@author: AudioscavengeR (重构版本)
|
||||
"""
|
||||
|
||||
@@ -23,7 +23,7 @@ from .core import (
|
||||
)
|
||||
|
||||
# 版本信息
|
||||
VERSION = "3.0"
|
||||
VERSION = "latest"
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -39,16 +39,16 @@ def check_optional_dependencies():
|
||||
try:
|
||||
import pillow_avif
|
||||
dependencies['avif'] = True
|
||||
logger.info("AVIF支持已启用")
|
||||
logger.info("AVIF support enabled")
|
||||
except ImportError:
|
||||
logger.info("AVIF不可用,如需支持请安装: pip install pillow-avif-plugin")
|
||||
logger.info("AVIF not available. To enable: pip install pillow-avif-plugin")
|
||||
|
||||
try:
|
||||
from jxlpy import JXLImagePlugin
|
||||
dependencies['jxl'] = True
|
||||
logger.info("JXL支持已启用")
|
||||
logger.info("JXL support enabled")
|
||||
except ImportError:
|
||||
logger.info("JXL不可用,如需支持请安装: pip install jxlpy")
|
||||
logger.info("JXL not available. To enable: pip install jxlpy")
|
||||
|
||||
return dependencies
|
||||
|
||||
@@ -56,10 +56,10 @@ def check_optional_dependencies():
|
||||
SUPPORTED_FORMATS = check_optional_dependencies()
|
||||
|
||||
|
||||
class SaveImageExtended:
|
||||
class SaveImageAdvanced:
|
||||
"""
|
||||
Save Image Extended - 重构版本
|
||||
|
||||
ComfyUI Save Image Pro - 重构版本
|
||||
|
||||
使用模块化架构的图像保存节点,支持多种格式和自定义配置。
|
||||
"""
|
||||
|
||||
@@ -68,7 +68,7 @@ class SaveImageExtended:
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = 'image'
|
||||
DESCRIPTION = """
|
||||
### Save Image Extended v3.0 (重构版本)
|
||||
### ComfyUI Save Image Pro
|
||||
|
||||
**功能特性:**
|
||||
- 支持多种图像格式 (PNG, WebP, JPEG, AVIF, JXL, TIFF, GIF, BMP)
|
||||
@@ -103,7 +103,7 @@ class SaveImageExtended:
|
||||
level=logging.INFO,
|
||||
format='[%(name)s] %(levelname)s: %(message)s'
|
||||
)
|
||||
logger.info(f"Save Image Extended v{VERSION} 已初始化")
|
||||
logger.info("ComfyUI Save Image Pro initialized")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
@@ -177,8 +177,8 @@ class SaveImageExtended:
|
||||
# 验证配置
|
||||
validation = self.config_manager.validate_config(config)
|
||||
if not validation.valid:
|
||||
logger.error(f"配置验证失败: {validation.errors}")
|
||||
raise ValueError(f"配置错误: {'; '.join(validation.errors)}")
|
||||
logger.error(f"Configuration validation failed: {validation.errors}")
|
||||
raise ValueError(f"Configuration error: {'; '.join(validation.errors)}")
|
||||
|
||||
# 创建图像保存器
|
||||
self.image_saver = ImageSaver(config, self.output_dir)
|
||||
@@ -188,21 +188,21 @@ class SaveImageExtended:
|
||||
images, prompt or {}, extra_pnginfo, positive_text, negative_text
|
||||
)
|
||||
|
||||
logger.info(f"成功保存 {len(results)} 张图像")
|
||||
logger.info(f"Successfully saved {len(results)} images")
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存图像时发生错误: {e}")
|
||||
logger.error(f"Error occurred while saving images: {e}")
|
||||
# 返回空结果而不是抛出异常,避免中断ComfyUI工作流
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
|
||||
# ComfyUI节点映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SaveImageExtended": SaveImageExtended,
|
||||
"SaveImageAdvanced": SaveImageAdvanced,
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SaveImageExtended": "Save Image Extended (v3.0)",
|
||||
"SaveImageAdvanced": "comfyui-save-image-pro (Advanced)",
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
ComfyUI Save Image Pro - 简化版节点
|
||||
|
||||
简化版图像保存节点,提供基础的图像保存功能。
|
||||
|
||||
@version: latest
|
||||
@author: AudioscavengeR
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
import folder_paths
|
||||
|
||||
# 导入重构后的核心模块
|
||||
from .core import (
|
||||
SaveConfig, ConfigManager, ValidationResult,
|
||||
ImageSaver, FileNameGenerator, CounterManager,
|
||||
MetadataHandler, PathManager, JobDataExporter
|
||||
)
|
||||
|
||||
# 版本信息
|
||||
VERSION = "latest"
|
||||
|
||||
# 设置日志
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def check_optional_dependencies():
|
||||
"""检查可选依赖项"""
|
||||
supported = {
|
||||
'avif': False,
|
||||
'jxl': False
|
||||
}
|
||||
|
||||
try:
|
||||
from PIL import Image
|
||||
# 检查 AVIF 支持
|
||||
try:
|
||||
import pillow_avif
|
||||
supported['avif'] = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 检查 JXL 支持
|
||||
try:
|
||||
import jxlpy
|
||||
supported['jxl'] = True
|
||||
except ImportError:
|
||||
pass
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return supported
|
||||
|
||||
# 检查支持的格式
|
||||
SUPPORTED_FORMATS = check_optional_dependencies()
|
||||
|
||||
|
||||
class SaveImageSimple:
|
||||
"""
|
||||
ComfyUI Save Image Pro - 简化版
|
||||
|
||||
提供基础的图像保存功能,包含最常用的参数。
|
||||
"""
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = 'save_images'
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = 'image'
|
||||
DESCRIPTION = """
|
||||
### ComfyUI Save Image Pro - 简化版
|
||||
|
||||
**基础功能:**
|
||||
- 支持多种图像格式 (PNG, WebP, JPEG, AVIF, JXL, TIFF, GIF, BMP)
|
||||
- 自定义文件名前缀
|
||||
- 质量控制
|
||||
- 作业数据导出
|
||||
- 图像预览
|
||||
|
||||
**简化设计:**
|
||||
- 精简的参数设置
|
||||
- 快速上手使用
|
||||
- 保留核心功能
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.config_manager = ConfigManager()
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.image_saver = None
|
||||
self.job_exporter = None
|
||||
|
||||
# 初始化日志
|
||||
self._setup_logging()
|
||||
|
||||
def _setup_logging(self):
|
||||
"""设置日志系统"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='[%(name)s] %(levelname)s: %(message)s'
|
||||
)
|
||||
logger.info("ComfyUI Save Image Pro (Simple) initialized")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
"""定义输入类型 - 简化版"""
|
||||
# 获取支持的格式列表
|
||||
output_formats = ['.webp', '.png', '.jpg', '.jpeg', '.gif', '.tiff', '.bmp']
|
||||
|
||||
if SUPPORTED_FORMATS['jxl']:
|
||||
output_formats.insert(0, '.jxl')
|
||||
if SUPPORTED_FORMATS['avif']:
|
||||
output_formats.insert(0, '.avif')
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE", ),
|
||||
"filename_prefix": ("STRING", {"default": "ComfyUI"}),
|
||||
"output_format": (output_formats, {"default": ".webp"}),
|
||||
"quality": ("INT", {"default": 75, "min": 1, "max": 100, "step": 1}),
|
||||
"job_data_per_image": ("BOOLEAN", {"default": False}),
|
||||
"image_preview": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"hidden": {
|
||||
"prompt": "PROMPT",
|
||||
"extra_pnginfo": "EXTRA_PNGINFO"
|
||||
}
|
||||
}
|
||||
|
||||
def save_images(self, images, filename_prefix="ComfyUI", output_format=".webp",
|
||||
quality=75, job_data_per_image=False, image_preview=True,
|
||||
prompt=None, extra_pnginfo=None):
|
||||
"""
|
||||
保存图像的主要方法 - 简化版
|
||||
"""
|
||||
try:
|
||||
# 创建简化的配置对象
|
||||
config = SaveConfig(
|
||||
filename_prefix=filename_prefix,
|
||||
filename_keys="", # 简化版不使用复杂的文件名键
|
||||
foldername_prefix="", # 简化版不使用文件夹前缀
|
||||
foldername_keys="", # 简化版不使用文件夹键
|
||||
delimiter="-",
|
||||
output_format=output_format,
|
||||
quality=quality,
|
||||
save_metadata=True, # 简化版默认保存元数据
|
||||
counter_digits=4,
|
||||
counter_position="last",
|
||||
one_counter_per_folder=True,
|
||||
save_job_data="basic" if job_data_per_image else "disabled",
|
||||
job_data_per_image=job_data_per_image,
|
||||
job_custom_text="",
|
||||
image_preview=image_preview
|
||||
)
|
||||
|
||||
# 验证配置
|
||||
validation = self.config_manager.validate_config(config)
|
||||
if not validation.valid:
|
||||
logger.error(f"Configuration validation failed: {validation.errors}")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
# 初始化图像保存器
|
||||
if not self.image_saver:
|
||||
self.image_saver = ImageSaver(config, self.output_dir)
|
||||
else:
|
||||
self.image_saver.update_config(config)
|
||||
|
||||
# 保存图像
|
||||
results = self.image_saver.save_images(
|
||||
images=images,
|
||||
prompt=prompt,
|
||||
extra_pnginfo=extra_pnginfo
|
||||
)
|
||||
|
||||
# 导出作业数据(如果启用)
|
||||
if config.save_job_data != "disabled":
|
||||
if not self.job_exporter:
|
||||
self.job_exporter = JobDataExporter(config)
|
||||
else:
|
||||
self.job_exporter.update_config(config)
|
||||
|
||||
self.job_exporter.export_job_data(
|
||||
results=results,
|
||||
prompt=prompt,
|
||||
extra_pnginfo=extra_pnginfo
|
||||
)
|
||||
|
||||
logger.info(f"Successfully saved {len(results)} images")
|
||||
|
||||
# 返回结果
|
||||
if config.image_preview:
|
||||
return {"ui": {"images": results}}
|
||||
else:
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error occurred while saving images: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
|
||||
# 节点类映射
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"SaveImageSimple": SaveImageSimple
|
||||
}
|
||||
|
||||
# 节点显示名称映射
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"SaveImageSimple": "comfyui-save-image-pro (Simple)"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
"""
|
||||
Save Image Extended 测试套件
|
||||
|
||||
包含所有核心模块的单元测试。
|
||||
"""
|
||||
@@ -1,103 +0,0 @@
|
||||
"""
|
||||
测试运行器
|
||||
|
||||
运行所有单元测试并生成报告。
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from io import StringIO
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""运行所有测试"""
|
||||
# 发现并加载所有测试
|
||||
loader = unittest.TestLoader()
|
||||
start_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
suite = loader.discover(start_dir, pattern='test_*.py')
|
||||
|
||||
# 创建测试运行器
|
||||
stream = StringIO()
|
||||
runner = unittest.TextTestRunner(
|
||||
stream=stream,
|
||||
verbosity=2,
|
||||
descriptions=True,
|
||||
failfast=False
|
||||
)
|
||||
|
||||
# 运行测试
|
||||
print("=" * 70)
|
||||
print("Save Image Extended - 单元测试")
|
||||
print("=" * 70)
|
||||
|
||||
result = runner.run(suite)
|
||||
|
||||
# 输出结果
|
||||
output = stream.getvalue()
|
||||
print(output)
|
||||
|
||||
# 生成摘要
|
||||
print("\n" + "=" * 70)
|
||||
print("测试摘要")
|
||||
print("=" * 70)
|
||||
print(f"运行测试数: {result.testsRun}")
|
||||
print(f"失败数: {len(result.failures)}")
|
||||
print(f"错误数: {len(result.errors)}")
|
||||
print(f"跳过数: {len(result.skipped)}")
|
||||
|
||||
if result.failures:
|
||||
print(f"\n失败的测试:")
|
||||
for test, traceback in result.failures:
|
||||
print(f" - {test}")
|
||||
|
||||
if result.errors:
|
||||
print(f"\n错误的测试:")
|
||||
for test, traceback in result.errors:
|
||||
print(f" - {test}")
|
||||
|
||||
# 计算成功率
|
||||
if result.testsRun > 0:
|
||||
success_rate = ((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun) * 100
|
||||
print(f"\n成功率: {success_rate:.1f}%")
|
||||
|
||||
print("=" * 70)
|
||||
|
||||
return result.wasSuccessful()
|
||||
|
||||
|
||||
def run_specific_test(test_module):
|
||||
"""运行特定测试模块"""
|
||||
try:
|
||||
# 导入测试模块
|
||||
module = __import__(f'test_{test_module}', fromlist=[''])
|
||||
|
||||
# 创建测试套件
|
||||
loader = unittest.TestLoader()
|
||||
suite = loader.loadTestsFromModule(module)
|
||||
|
||||
# 运行测试
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
return result.wasSuccessful()
|
||||
|
||||
except ImportError as e:
|
||||
print(f"无法导入测试模块 'test_{test_module}': {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
# 运行特定测试
|
||||
test_name = sys.argv[1]
|
||||
success = run_specific_test(test_name)
|
||||
else:
|
||||
# 运行所有测试
|
||||
success = run_all_tests()
|
||||
|
||||
# 设置退出代码
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -1,151 +0,0 @@
|
||||
"""
|
||||
配置管理模块测试
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from core.config import SaveConfig, ConfigManager, ValidationResult
|
||||
|
||||
|
||||
class TestSaveConfig(unittest.TestCase):
|
||||
"""SaveConfig测试类"""
|
||||
|
||||
def test_default_config(self):
|
||||
"""测试默认配置"""
|
||||
config = SaveConfig()
|
||||
|
||||
self.assertEqual(config.filename_prefix, "ComfyUI")
|
||||
self.assertEqual(config.output_format, ".webp")
|
||||
self.assertEqual(config.quality, 75)
|
||||
self.assertTrue(config.save_metadata)
|
||||
self.assertEqual(config.counter_digits, 4)
|
||||
|
||||
def test_filename_keys_list(self):
|
||||
"""测试文件名键值列表解析"""
|
||||
config = SaveConfig(filename_keys="sampler_name, cfg, steps")
|
||||
keys = config.get_filename_keys_list()
|
||||
|
||||
self.assertEqual(keys, ["sampler_name", "cfg", "steps"])
|
||||
|
||||
def test_foldername_keys_list(self):
|
||||
"""测试文件夹名键值列表解析"""
|
||||
config = SaveConfig(foldername_keys="ckpt_name, model")
|
||||
keys = config.get_foldername_keys_list()
|
||||
|
||||
self.assertEqual(keys, ["ckpt_name", "model"])
|
||||
|
||||
|
||||
class TestConfigManager(unittest.TestCase):
|
||||
"""ConfigManager测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""设置测试环境"""
|
||||
self.manager = ConfigManager()
|
||||
|
||||
def test_get_preset_names(self):
|
||||
"""测试获取预设名称"""
|
||||
presets = self.manager.get_preset_names()
|
||||
|
||||
self.assertIn("基础配置", presets)
|
||||
self.assertIn("高级配置", presets)
|
||||
self.assertIn("专业配置", presets)
|
||||
|
||||
def test_get_preset_config(self):
|
||||
"""测试获取预设配置"""
|
||||
config = self.manager.get_preset_config("基础配置")
|
||||
|
||||
self.assertIsInstance(config, SaveConfig)
|
||||
self.assertEqual(config.filename_prefix, "ComfyUI")
|
||||
self.assertEqual(config.output_format, ".webp")
|
||||
|
||||
def test_validate_valid_config(self):
|
||||
"""测试验证有效配置"""
|
||||
config = SaveConfig()
|
||||
result = self.manager.validate_config(config)
|
||||
|
||||
self.assertTrue(result.valid)
|
||||
self.assertEqual(len(result.errors), 0)
|
||||
|
||||
def test_validate_invalid_quality(self):
|
||||
"""测试验证无效质量值"""
|
||||
config = SaveConfig(quality=150) # 超出范围
|
||||
result = self.manager.validate_config(config)
|
||||
|
||||
self.assertFalse(result.valid)
|
||||
self.assertTrue(any("质量" in error for error in result.errors))
|
||||
|
||||
def test_validate_invalid_counter_digits(self):
|
||||
"""测试验证无效计数器位数"""
|
||||
config = SaveConfig(counter_digits=0) # 无效值
|
||||
result = self.manager.validate_config(config)
|
||||
|
||||
self.assertFalse(result.valid)
|
||||
self.assertTrue(any("计数器位数" in error for error in result.errors))
|
||||
|
||||
def test_validate_invalid_format(self):
|
||||
"""测试验证无效格式"""
|
||||
config = SaveConfig(output_format=".invalid")
|
||||
result = self.manager.validate_config(config)
|
||||
|
||||
self.assertFalse(result.valid)
|
||||
self.assertTrue(any("格式" in error for error in result.errors))
|
||||
|
||||
def test_get_help_text(self):
|
||||
"""测试获取帮助文本"""
|
||||
help_text = self.manager.get_help_text("filename_keys")
|
||||
|
||||
self.assertIsInstance(help_text, str)
|
||||
self.assertTrue(len(help_text) > 0)
|
||||
|
||||
def test_preview_filename(self):
|
||||
"""测试预览文件名"""
|
||||
config = SaveConfig(
|
||||
filename_prefix="Test",
|
||||
filename_keys="sampler_name, steps",
|
||||
delimiter="_"
|
||||
)
|
||||
|
||||
preview = self.manager.preview_filename(config)
|
||||
|
||||
self.assertIsInstance(preview, str)
|
||||
self.assertTrue(preview.endswith(".webp"))
|
||||
|
||||
|
||||
class TestValidationResult(unittest.TestCase):
|
||||
"""ValidationResult测试类"""
|
||||
|
||||
def test_initial_state(self):
|
||||
"""测试初始状态"""
|
||||
result = ValidationResult(valid=True)
|
||||
|
||||
self.assertTrue(result.valid)
|
||||
self.assertEqual(len(result.errors), 0)
|
||||
self.assertEqual(len(result.warnings), 0)
|
||||
|
||||
def test_add_error(self):
|
||||
"""测试添加错误"""
|
||||
result = ValidationResult(valid=True)
|
||||
result.add_error("测试错误")
|
||||
|
||||
self.assertFalse(result.valid)
|
||||
self.assertEqual(len(result.errors), 1)
|
||||
self.assertEqual(result.errors[0], "测试错误")
|
||||
|
||||
def test_add_warning(self):
|
||||
"""测试添加警告"""
|
||||
result = ValidationResult(valid=True)
|
||||
result.add_warning("测试警告")
|
||||
|
||||
self.assertTrue(result.valid) # 警告不影响有效性
|
||||
self.assertEqual(len(result.warnings), 1)
|
||||
self.assertEqual(result.warnings[0], "测试警告")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,212 +0,0 @@
|
||||
"""
|
||||
文件名生成器模块测试
|
||||
"""
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from core.config import SaveConfig
|
||||
from core.filename_generator import FileNameGenerator, ParameterExtractor
|
||||
|
||||
|
||||
class TestParameterExtractor(unittest.TestCase):
|
||||
"""ParameterExtractor测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""设置测试环境"""
|
||||
self.extractor = ParameterExtractor()
|
||||
self.sample_prompt = {
|
||||
"1": {
|
||||
"inputs": {
|
||||
"sampler_name": "euler",
|
||||
"steps": 20,
|
||||
"cfg": 7.5
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"inputs": {
|
||||
"seed": 12345,
|
||||
"ckpt_name": "sd_xl_base.safetensors"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_find_parameter_values(self):
|
||||
"""测试查找参数值"""
|
||||
keys = ["sampler_name", "steps", "cfg"]
|
||||
result = self.extractor.find_parameter_values(self.sample_prompt, keys)
|
||||
|
||||
self.assertEqual(result["sampler_name"], "euler")
|
||||
self.assertEqual(result["steps"], "20")
|
||||
self.assertEqual(result["cfg"], "7.5")
|
||||
|
||||
def test_extract_node_reference(self):
|
||||
"""测试提取节点引用"""
|
||||
keys = ["5.seed", "5.ckpt_name"]
|
||||
result = self.extractor.find_parameter_values(self.sample_prompt, keys)
|
||||
|
||||
self.assertEqual(result["5.seed"], "12345")
|
||||
self.assertEqual(result["5.ckpt_name"], "sd_xl_base.safetensors")
|
||||
|
||||
def test_extract_time_format(self):
|
||||
"""测试提取时间格式"""
|
||||
keys = ["%Y", "%m", "%d"]
|
||||
result = self.extractor.find_parameter_values({}, keys)
|
||||
|
||||
now = datetime.now()
|
||||
self.assertEqual(result["%Y"], str(now.year))
|
||||
self.assertEqual(result["%m"], f"{now.month:02d}")
|
||||
self.assertEqual(result["%d"], f"{now.day:02d}")
|
||||
|
||||
def test_cache_functionality(self):
|
||||
"""测试缓存功能"""
|
||||
keys = ["sampler_name"]
|
||||
|
||||
# 第一次调用
|
||||
result1 = self.extractor.find_parameter_values(self.sample_prompt, keys)
|
||||
|
||||
# 第二次调用应该使用缓存
|
||||
result2 = self.extractor.find_parameter_values(self.sample_prompt, keys)
|
||||
|
||||
self.assertEqual(result1, result2)
|
||||
|
||||
def test_clear_cache(self):
|
||||
"""测试清空缓存"""
|
||||
keys = ["sampler_name"]
|
||||
self.extractor.find_parameter_values(self.sample_prompt, keys)
|
||||
|
||||
# 确保缓存有内容
|
||||
self.assertTrue(len(self.extractor.cache) > 0)
|
||||
|
||||
# 清空缓存
|
||||
self.extractor.clear_cache()
|
||||
self.assertEqual(len(self.extractor.cache), 0)
|
||||
|
||||
|
||||
class TestFileNameGenerator(unittest.TestCase):
|
||||
"""FileNameGenerator测试类"""
|
||||
|
||||
def setUp(self):
|
||||
"""设置测试环境"""
|
||||
self.config = SaveConfig(
|
||||
filename_prefix="Test",
|
||||
filename_keys="sampler_name, steps",
|
||||
foldername_prefix="Folder",
|
||||
foldername_keys="ckpt_name",
|
||||
delimiter="_"
|
||||
)
|
||||
self.generator = FileNameGenerator(self.config)
|
||||
self.sample_prompt = {
|
||||
"1": {
|
||||
"inputs": {
|
||||
"sampler_name": "euler",
|
||||
"steps": 20
|
||||
}
|
||||
},
|
||||
"5": {
|
||||
"inputs": {
|
||||
"ckpt_name": "sd_xl_base.safetensors"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def test_generate_filename(self):
|
||||
"""测试生成文件名"""
|
||||
timestamp = datetime(2024, 1, 1, 12, 0, 0)
|
||||
filename = self.generator.generate_filename(self.sample_prompt, timestamp)
|
||||
|
||||
self.assertIn("Test", filename)
|
||||
self.assertIn("euler", filename)
|
||||
self.assertIn("20", filename)
|
||||
self.assertIn("_", filename) # 分隔符
|
||||
|
||||
def test_generate_foldername(self):
|
||||
"""测试生成文件夹名"""
|
||||
timestamp = datetime(2024, 1, 1, 12, 0, 0)
|
||||
foldername = self.generator.generate_foldername(self.sample_prompt, timestamp)
|
||||
|
||||
self.assertIn("Folder", foldername)
|
||||
self.assertIn("sd_xl_base.safetensors", foldername)
|
||||
|
||||
def test_generate_full_filename(self):
|
||||
"""测试生成完整文件名"""
|
||||
timestamp = datetime(2024, 1, 1, 12, 0, 0)
|
||||
full_filename = self.generator.generate_full_filename(
|
||||
self.sample_prompt, 1, timestamp
|
||||
)
|
||||
|
||||
self.assertTrue(full_filename.endswith(".webp"))
|
||||
self.assertIn("0001", full_filename) # 计数器
|
||||
|
||||
def test_clean_filename(self):
|
||||
"""测试清理文件名"""
|
||||
dirty_name = "test<>:|?*file"
|
||||
clean_name = self.generator._clean_filename(dirty_name)
|
||||
|
||||
# 应该移除或替换无效字符
|
||||
invalid_chars = '<>:|?*'
|
||||
for char in invalid_chars:
|
||||
self.assertNotIn(char, clean_name)
|
||||
|
||||
def test_counter_position_last(self):
|
||||
"""测试计数器位置在最后"""
|
||||
config = SaveConfig(counter_position="last")
|
||||
generator = FileNameGenerator(config)
|
||||
|
||||
timestamp = datetime(2024, 1, 1, 12, 0, 0)
|
||||
filename = generator.generate_full_filename({}, 42, timestamp)
|
||||
|
||||
self.assertTrue(filename.endswith("0042.webp"))
|
||||
|
||||
def test_counter_position_first(self):
|
||||
"""测试计数器位置在开头"""
|
||||
config = SaveConfig(counter_position="first")
|
||||
generator = FileNameGenerator(config)
|
||||
|
||||
timestamp = datetime(2024, 1, 1, 12, 0, 0)
|
||||
filename = generator.generate_full_filename({}, 42, timestamp)
|
||||
|
||||
self.assertTrue(filename.startswith("0042"))
|
||||
|
||||
def test_preview_names(self):
|
||||
"""测试预览名称"""
|
||||
timestamp = datetime(2024, 1, 1, 12, 0, 0)
|
||||
filename, foldername = self.generator.preview_names(
|
||||
self.sample_prompt, 1, timestamp
|
||||
)
|
||||
|
||||
self.assertIsInstance(filename, str)
|
||||
self.assertIsInstance(foldername, str)
|
||||
self.assertTrue(len(filename) > 0)
|
||||
|
||||
def test_cache_functionality(self):
|
||||
"""测试缓存功能"""
|
||||
timestamp = datetime(2024, 1, 1, 12, 0, 0)
|
||||
|
||||
# 第一次生成
|
||||
filename1 = self.generator.generate_filename(self.sample_prompt, timestamp)
|
||||
|
||||
# 第二次生成应该使用缓存
|
||||
filename2 = self.generator.generate_filename(self.sample_prompt, timestamp)
|
||||
|
||||
self.assertEqual(filename1, filename2)
|
||||
|
||||
def test_clear_cache(self):
|
||||
"""测试清空缓存"""
|
||||
timestamp = datetime(2024, 1, 1, 12, 0, 0)
|
||||
self.generator.generate_filename(self.sample_prompt, timestamp)
|
||||
|
||||
# 清空缓存
|
||||
self.generator.clear_cache()
|
||||
|
||||
# 缓存应该被清空
|
||||
self.assertEqual(len(self.generator.extractor.cache), 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user