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:
wee
2025-07-06 13:19:40 +00:00
parent 235885adfb
commit bafe288b5a
18 changed files with 262 additions and 1895 deletions
+4 -4
View File
@@ -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
- 建议在升级前备份重要的工作流文件
+4 -4
View File
@@ -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
View File
@@ -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
View File
@@ -1,5 +1,5 @@
"""
Save Image Extended - 核心模块
ComfyUI Save Image Pro - 核心模块
这个包包含了重构后的核心组件,遵循SOLID原则设计。
+12 -4
View File
@@ -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)
+2 -2
View File
@@ -60,8 +60,8 @@ class MetadataHandler:
# 添加插件信息
metadata['generator'] = {
'name': 'Save Image Extended',
'version': '3.0'
'name': 'ComfyUI Save Image Pro',
'version': 'latest'
}
# 缓存结果
-434
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)",
}
+213
View File
@@ -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)"
}
-5
View File
@@ -1,5 +0,0 @@
"""
Save Image Extended 测试套件
包含所有核心模块的单元测试。
"""
-103
View File
@@ -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)
-151
View File
@@ -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()
-212
View File
@@ -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()