From bafe288b5a45ec7a3b2e6f41e8191bfb0dc75aa7 Mon Sep 17 00:00:00 2001 From: wee Date: Sun, 6 Jul 2025 13:19:40 +0000 Subject: [PATCH] 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) --- CHANGELOG.md | 8 +- README.md | 8 +- __init__.py | 9 +- core/__init__.py | 2 +- core/image_saver.py | 16 +- core/metadata_handler.py | 4 +- docs/优化建议.md | 434 ------------------ docs/开发者文档.md | 335 -------------- docs/用户手册.md | 205 --------- docs/重构分析报告.md | 214 --------- docs/重构完成总结.md | 200 -------- pyproject.toml | 2 +- ...mage_extended.py => save_image_advanced.py | 36 +- save_image_simple.py | 213 +++++++++ tests/__init__.py | 5 - tests/run_tests.py | 103 ----- tests/test_config.py | 151 ------ tests/test_filename_generator.py | 212 --------- 18 files changed, 262 insertions(+), 1895 deletions(-) delete mode 100644 docs/优化建议.md delete mode 100644 docs/开发者文档.md delete mode 100644 docs/用户手册.md delete mode 100644 docs/重构分析报告.md delete mode 100644 docs/重构完成总结.md rename save_image_extended.py => save_image_advanced.py (87%) create mode 100644 save_image_simple.py delete mode 100644 tests/__init__.py delete mode 100644 tests/run_tests.py delete mode 100644 tests/test_config.py delete mode 100644 tests/test_filename_generator.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7083210..f55d087 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 - 建议在升级前备份重要的工作流文件 diff --git a/README.md b/README.md index 3725367..4dac340 100644 --- a/README.md +++ b/README.md @@ -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) - 重构版本 +### 最新版本 - 重构版本 - ✨ 完全重构的模块化架构 - ⚡ 大幅性能提升和优化 - 🧪 完整的单元测试覆盖 diff --git a/__init__.py b/__init__.py index 2f127d6..1cb50be 100644 --- a/__init__.py +++ b/__init__.py @@ -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'] diff --git a/core/__init__.py b/core/__init__.py index d21ef0f..f8b55ca 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,5 +1,5 @@ """ -Save Image Extended - 核心模块 +ComfyUI Save Image Pro - 核心模块 这个包包含了重构后的核心组件,遵循SOLID原则设计。 diff --git a/core/image_saver.py b/core/image_saver.py index 3feafef..eb152f1 100644 --- a/core/image_saver.py +++ b/core/image_saver.py @@ -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) diff --git a/core/metadata_handler.py b/core/metadata_handler.py index 7fbe4d5..e2d2772 100644 --- a/core/metadata_handler.py +++ b/core/metadata_handler.py @@ -60,8 +60,8 @@ class MetadataHandler: # 添加插件信息 metadata['generator'] = { - 'name': 'Save Image Extended', - 'version': '3.0' + 'name': 'ComfyUI Save Image Pro', + 'version': 'latest' } # 缓存结果 diff --git a/docs/优化建议.md b/docs/优化建议.md deleted file mode 100644 index a815272..0000000 --- a/docs/优化建议.md +++ /dev/null @@ -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依赖,简化架构 - -通过这些优化,插件将变得更加高效、易用和可维护。 diff --git a/docs/开发者文档.md b/docs/开发者文档.md deleted file mode 100644 index 75c0724..0000000 --- a/docs/开发者文档.md +++ /dev/null @@ -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 - -### 代码审查 -- 遵循代码规范 -- 包含适当的测试 -- 更新相关文档 -- 确保向后兼容性 diff --git a/docs/用户手册.md b/docs/用户手册.md deleted file mode 100644 index 362d352..0000000 --- a/docs/用户手册.md +++ /dev/null @@ -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 保持与之前版本的配置兼容性,现有工作流无需修改即可使用。 diff --git a/docs/重构分析报告.md b/docs/重构分析报告.md deleted file mode 100644 index 0071526..0000000 --- a/docs/重构分析报告.md +++ /dev/null @@ -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%+ diff --git a/docs/重构完成总结.md b/docs/重构完成总结.md deleted file mode 100644 index 9916ae5..0000000 --- a/docs/重构完成总结.md +++ /dev/null @@ -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 的重构是一次全面的现代化改造,不仅解决了原有代码的技术债务,还为未来的功能扩展奠定了坚实的基础。新架构具有以下特点: - -- **高度模块化**: 清晰的模块划分和职责分离 -- **优秀性能**: 多层次的性能优化机制 -- **健壮稳定**: 完善的错误处理和恢复机制 -- **易于维护**: 良好的代码结构和完整的文档 -- **高度可扩展**: 支持轻松添加新功能和格式 - -这次重构为插件的长期发展提供了强有力的技术支撑,确保了代码的可持续性和可维护性。 diff --git a/pyproject.toml b/pyproject.toml index 002a819..31f301d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/save_image_extended.py b/save_image_advanced.py similarity index 87% rename from save_image_extended.py rename to save_image_advanced.py index 3e0f9ae..9a54fa3 100644 --- a/save_image_extended.py +++ b/save_image_advanced.py @@ -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)", } diff --git a/save_image_simple.py b/save_image_simple.py new file mode 100644 index 0000000..6d9628f --- /dev/null +++ b/save_image_simple.py @@ -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)" +} diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index ff8d590..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Save Image Extended 测试套件 - -包含所有核心模块的单元测试。 -""" diff --git a/tests/run_tests.py b/tests/run_tests.py deleted file mode 100644 index 598246e..0000000 --- a/tests/run_tests.py +++ /dev/null @@ -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) diff --git a/tests/test_config.py b/tests/test_config.py deleted file mode 100644 index 10cbfd6..0000000 --- a/tests/test_config.py +++ /dev/null @@ -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() diff --git a/tests/test_filename_generator.py b/tests/test_filename_generator.py deleted file mode 100644 index ebf0902..0000000 --- a/tests/test_filename_generator.py +++ /dev/null @@ -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()