From da0c2eef666fffbb79edbbcd3fc1593ce17c3e20 Mon Sep 17 00:00:00 2001 From: Dontdrunk <1761696257@qq.com> Date: Sat, 9 Aug 2025 22:06:00 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AE=80=E5=8C=96Qwen-MT=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- __init__.py | 2 +- extensions/Qwen_MT/api_routes.py | 3 +- extensions/Qwen_MT/nodes.py | 270 +++++++++++++++++- extensions/Qwen_MT/utils/__init__.py | 15 - extensions/Qwen_MT/utils/api_config.py | 133 --------- extensions/Qwen_MT/utils/debug_utils.py | 188 ------------ extensions/Qwen_MT/utils/language_utils.py | 100 ------- extensions/Qwen_MT/utils/resource_cache.py | 220 -------------- node/qwen_mt_translator.py | 316 --------------------- 9 files changed, 263 insertions(+), 984 deletions(-) delete mode 100644 extensions/Qwen_MT/utils/__init__.py delete mode 100644 extensions/Qwen_MT/utils/api_config.py delete mode 100644 extensions/Qwen_MT/utils/debug_utils.py delete mode 100644 extensions/Qwen_MT/utils/language_utils.py delete mode 100644 extensions/Qwen_MT/utils/resource_cache.py delete mode 100644 node/qwen_mt_translator.py diff --git a/__init__.py b/__init__.py index a8506fa..1354b54 100644 --- a/__init__.py +++ b/__init__.py @@ -55,7 +55,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "DD-ConditionSwitcher": "DD Condition Switcher", "DD-LatentSwitcher": "DD Latent Switcher", "DD-ImageStroke": "DD Image Stroke", - "DD-QwenMTTranslator": "DD Qwen-MT翻译", + "DD-QwenMTTranslator": "DD Qwen-MT", } __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'prompt_manager_api'] diff --git a/extensions/Qwen_MT/api_routes.py b/extensions/Qwen_MT/api_routes.py index 2ad2a26..852784a 100644 --- a/extensions/Qwen_MT/api_routes.py +++ b/extensions/Qwen_MT/api_routes.py @@ -6,8 +6,7 @@ Handles API key configuration requests from the frontend. import json from aiohttp import web from server import PromptServer -from .utils.api_config import APIConfigManager -from .utils.debug_utils import DebugUtils +from .nodes import APIConfigManager, DebugUtils @PromptServer.instance.routes.get("/qwen_mt/config") diff --git a/extensions/Qwen_MT/nodes.py b/extensions/Qwen_MT/nodes.py index e04084f..673678b 100644 --- a/extensions/Qwen_MT/nodes.py +++ b/extensions/Qwen_MT/nodes.py @@ -7,26 +7,276 @@ import os import json import torch import logging -from typing import Dict, List, Any, Tuple, Optional +import time +import threading +from typing import Dict, List, Any, Tuple, Optional, Callable from openai import OpenAI -from .utils.language_utils import SUPPORTED_LANGUAGES -from .utils.debug_utils import DebugUtils -from .utils.resource_cache import ResourceCache -from .utils.api_config import APIConfigManager - # 禁用HTTP相关的详细日志记录,保持控制台简洁 logging.getLogger("openai").setLevel(logging.WARNING) logging.getLogger("httpx").setLevel(logging.WARNING) logging.getLogger("httpcore").setLevel(logging.WARNING) +# Supported languages mapping based on official Qwen-MT documentation +SUPPORTED_LANGUAGES = { + "英语": "en", + "简体中文": "zh", + "繁体中文": "zh_tw", + "俄语": "ru", + "日语": "ja", + "韩语": "ko", + "西班牙语": "es", + "法语": "fr", + "葡萄牙语": "pt", + "德语": "de", + "意大利语": "it", + "泰语": "th", + "越南语": "vi", + "印度尼西亚语": "id", + "马来语": "ms", + "阿拉伯语": "ar", + "印地语": "hi", + "希伯来语": "he", + "缅甸语": "my", + "泰米尔语": "ta", + "乌尔都语": "ur", + "孟加拉语": "bn", + "波兰语": "pl", + "荷兰语": "nl", + "罗马尼亚语": "ro", + "土耳其语": "tr", + "高棉语": "km", + "老挝语": "lo", + "粤语": "yue", + "捷克语": "cs", + "希腊语": "el", + "瑞典语": "sv", + "匈牙利语": "hu", + "丹麦语": "da", + "芬兰语": "fi", + "乌克兰语": "uk", + "保加利亚语": "bg", + "塞尔维亚语": "sr", + "泰卢固语": "te", + "南非荷兰语": "af", + "亚美尼亚语": "hy", + "阿萨姆语": "as", + "阿斯图里亚斯语": "ast", + "巴斯克语": "eu", + "白俄罗斯语": "be", + "波斯尼亚语": "bs", + "加泰罗尼亚语": "ca", + "宿务语": "ceb", + "克罗地亚语": "hr", + "埃及阿拉伯语": "arz", + "爱沙尼亚语": "et", + "加利西亚语": "gl", + "格鲁吉亚语": "ka", + "古吉拉特语": "gu", + "冰岛语": "is", + "爪哇语": "jv", + "卡纳达语": "kn", + "哈萨克语": "kk", + "拉脱维亚语": "lv", + "立陶宛语": "lt", + "卢森堡语": "lb", + "马其顿语": "mk", + "马加希语": "mai", + "马耳他语": "mt", + "马拉地语": "mr", + "美索不达米亚阿拉伯语": "acm", + "摩洛哥阿拉伯语": "ary", + "内志阿拉伯语": "ars", + "尼泊尔语": "ne", + "北阿塞拜疆语": "az", + "北黎凡特阿拉伯语": "apc", + "北乌兹别克语": "uz", + "书面语挪威语": "nb", + "新挪威语": "nn", + "奥克语": "oc", + "奥里亚语": "or", + "邦阿西楠语": "pag", + "西西里语": "scn", + "信德语": "sd", + "僧伽罗语": "si", + "斯洛伐克语": "sk", + "斯洛文尼亚语": "sl", + "南黎凡特阿拉伯语": "ajp", + "斯瓦希里语": "sw", + "他加禄语": "tl", + "塔伊兹-亚丁阿拉伯语": "acq", + "托斯克阿尔巴尼亚语": "sq", + "突尼斯阿拉伯语": "aeb", + "威尼斯语": "vec", + "瓦莱语": "war", + "威尔士语": "cy", + "西波斯语": "fa" +} + + +class DebugUtils: + """Debug utilities for plugin development.""" + + DEBUG_ENABLED = False # 默认关闭调试模式,保持控制台简洁 + + @classmethod + def log(cls, message: str, level: str = "info") -> None: + """Log a debug message.""" + if not cls.DEBUG_ENABLED: + return + + timestamp = time.strftime("%Y-%m-%d %H:%M:%S") + prefix = f"[{timestamp}] [ComfyUI-Qwen-MT] [{level.upper()}]" + print(f"{prefix} {message}") + + +class ResourceCache: + """Thread-safe resource cache for expensive operations.""" + + _cache: Dict[str, Any] = {} + _cache_timestamps: Dict[str, float] = {} + _cache_lock = threading.Lock() + _cache_ttl = 3600 # 1 hour default TTL + + @classmethod + def get(cls, key: str, factory: Callable[[], Any], ttl: Optional[int] = None) -> Any: + """Get a cached resource or create it if not exists.""" + with cls._cache_lock: + current_time = time.time() + cache_ttl = ttl or cls._cache_ttl + + # Check if cache exists and is not expired + if (key in cls._cache and + key in cls._cache_timestamps and + current_time - cls._cache_timestamps[key] < cache_ttl): + + return cls._cache[key] + + # Create new resource + resource = factory() + + # Store in cache + cls._cache[key] = resource + cls._cache_timestamps[key] = current_time + + return resource + + @classmethod + def get_api_client(cls, api_key: str, base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1") -> OpenAI: + """Get or create a cached API client.""" + # Create cache key based on api_key hash and base_url + key = f"api_client:{hash(api_key)}:{base_url}" + + def factory(): + # 禁用OpenAI客户端的HTTP日志记录 + logging.getLogger("openai").setLevel(logging.WARNING) + logging.getLogger("httpx").setLevel(logging.WARNING) + logging.getLogger("httpcore").setLevel(logging.WARNING) + + return OpenAI( + api_key=api_key, + base_url=base_url + ) + + return cls.get(key, factory, ttl=1800) # 30 minutes TTL for API clients + + +class APIConfigManager: + """Manages API configuration for Qwen-MT plugin.""" + + CONFIG_FILE = os.path.join(os.path.dirname(__file__), "qwen_mt_config.json") + DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1" + + @classmethod + def get_api_key(cls) -> Optional[str]: + """Get the stored API key.""" + try: + if os.path.exists(cls.CONFIG_FILE): + with open(cls.CONFIG_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + return config.get('api_key') + except Exception as e: + DebugUtils.log(f"Failed to read API config: {e}", "error") + return None + + @classmethod + def set_api_key(cls, api_key: str) -> bool: + """Store the API key securely.""" + try: + config = {} + if os.path.exists(cls.CONFIG_FILE): + with open(cls.CONFIG_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + + config['api_key'] = api_key + + # Ensure directory exists + os.makedirs(os.path.dirname(cls.CONFIG_FILE), exist_ok=True) + + with open(cls.CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(config, f, ensure_ascii=False, indent=2) + + DebugUtils.log("API key saved successfully") + return True + + except Exception as e: + DebugUtils.log(f"Failed to save API config: {e}", "error") + return False + + @classmethod + def clear_api_key(cls) -> bool: + """Clear the stored API key.""" + try: + if os.path.exists(cls.CONFIG_FILE): + with open(cls.CONFIG_FILE, 'r', encoding='utf-8') as f: + config = json.load(f) + + if 'api_key' in config: + del config['api_key'] + + with open(cls.CONFIG_FILE, 'w', encoding='utf-8') as f: + json.dump(config, f, ensure_ascii=False, indent=2) + + DebugUtils.log("API key cleared successfully") + return True + + except Exception as e: + DebugUtils.log(f"Failed to clear API config: {e}", "error") + return False + + @classmethod + def get_base_url(cls) -> str: + """Get the base URL for API requests.""" + return cls.DEFAULT_BASE_URL + + @classmethod + def is_configured(cls) -> bool: + """Check if API is properly configured.""" + api_key = cls.get_api_key() + return api_key is not None and api_key.strip() != "" + + @classmethod + def get_config_info(cls) -> Dict[str, str]: + """Get configuration information for display.""" + api_key = cls.get_api_key() + is_configured = cls.is_configured() + + return { + "configured": is_configured, + "has_api_key": is_configured, + "api_key_preview": f"sk-***{api_key[-6:]}" if api_key and len(api_key) > 6 else "未配置", + "base_url": cls.get_base_url(), + "console_url": "https://bailian.console.aliyun.com/?tab=home#/home" + } + + class QwenMTTranslatorNode: """ 通义千问翻译节点 - 支持多种翻译模式 """ - CATEGORY = "🍺DD系列节点/🌐翻译" + CATEGORY = "🍺DD系列节点" @classmethod def INPUT_TYPES(cls): @@ -165,14 +415,16 @@ class QwenMTTranslatorNode: if translation_mode == "术语翻译" and mode_config.strip(): try: term_list = json.loads(mode_config) - translation_options["terms"] = term_list + # 通义千问翻译API使用 "glossary" 参数进行术语翻译 + translation_options["glossary"] = term_list # DebugUtils.log(f"使用术语翻译模式,包含 {len(term_list)} 个术语") except json.JSONDecodeError: # DebugUtils.log("术语配置格式错误,使用通用翻译模式", level="warning") pass elif translation_mode == "领域翻译" and mode_config.strip(): - translation_options["domains"] = mode_config.strip() + # 通义千问翻译API使用 "context" 参数进行领域翻译 + translation_options["context"] = mode_config.strip() # DebugUtils.log(f"使用领域翻译模式: {mode_config[:50]}...") else: diff --git a/extensions/Qwen_MT/utils/__init__.py b/extensions/Qwen_MT/utils/__init__.py deleted file mode 100644 index a3be82c..0000000 --- a/extensions/Qwen_MT/utils/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -Utils package initialization. -""" - -from .language_utils import SUPPORTED_LANGUAGES -from .debug_utils import DebugUtils -from .resource_cache import ResourceCache -from .api_config import APIConfigManager - -__all__ = [ - "SUPPORTED_LANGUAGES", - "DebugUtils", - "ResourceCache", - "APIConfigManager" -] diff --git a/extensions/Qwen_MT/utils/api_config.py b/extensions/Qwen_MT/utils/api_config.py deleted file mode 100644 index 21a7492..0000000 --- a/extensions/Qwen_MT/utils/api_config.py +++ /dev/null @@ -1,133 +0,0 @@ -""" -API Configuration Manager for Qwen-MT plugin. -Handles secure storage and retrieval of API keys. -""" - -import os -import json -from typing import Optional, Dict -from .debug_utils import DebugUtils - - -class APIConfigManager: - """ - Manages API configuration for Qwen-MT plugin. - """ - - CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), "qwen_mt_config.json") - DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1" - - @classmethod - def get_api_key(cls) -> Optional[str]: - """ - Get the stored API key. - - Returns: - API key if exists, None otherwise - """ - try: - if os.path.exists(cls.CONFIG_FILE): - with open(cls.CONFIG_FILE, 'r', encoding='utf-8') as f: - config = json.load(f) - return config.get('api_key') - except Exception as e: - DebugUtils.log(f"Failed to read API config: {e}", "error") - return None - - @classmethod - def set_api_key(cls, api_key: str) -> bool: - """ - Store the API key securely. - - Args: - api_key: API key to store - - Returns: - True if successful, False otherwise - """ - try: - config = {} - if os.path.exists(cls.CONFIG_FILE): - with open(cls.CONFIG_FILE, 'r', encoding='utf-8') as f: - config = json.load(f) - - config['api_key'] = api_key - - # Ensure directory exists - os.makedirs(os.path.dirname(cls.CONFIG_FILE), exist_ok=True) - - with open(cls.CONFIG_FILE, 'w', encoding='utf-8') as f: - json.dump(config, f, ensure_ascii=False, indent=2) - - DebugUtils.log("API key saved successfully") - return True - - except Exception as e: - DebugUtils.log(f"Failed to save API config: {e}", "error") - return False - - @classmethod - def clear_api_key(cls) -> bool: - """ - Clear the stored API key. - - Returns: - True if successful, False otherwise - """ - try: - if os.path.exists(cls.CONFIG_FILE): - with open(cls.CONFIG_FILE, 'r', encoding='utf-8') as f: - config = json.load(f) - - if 'api_key' in config: - del config['api_key'] - - with open(cls.CONFIG_FILE, 'w', encoding='utf-8') as f: - json.dump(config, f, ensure_ascii=False, indent=2) - - DebugUtils.log("API key cleared successfully") - return True - - except Exception as e: - DebugUtils.log(f"Failed to clear API config: {e}", "error") - return False - - @classmethod - def get_base_url(cls) -> str: - """ - Get the base URL for API requests. - - Returns: - Base URL string - """ - return cls.DEFAULT_BASE_URL - - @classmethod - def is_configured(cls) -> bool: - """ - Check if API is properly configured. - - Returns: - True if configured, False otherwise - """ - api_key = cls.get_api_key() - return api_key is not None and api_key.strip() != "" - - @classmethod - def get_config_info(cls) -> Dict[str, str]: - """ - Get configuration information for display. - - Returns: - Dictionary with config info - """ - api_key = cls.get_api_key() - is_configured = cls.is_configured() - - return { - "configured": is_configured, - "has_api_key": is_configured, - "api_key_preview": f"sk-***{api_key[-6:]}" if api_key and len(api_key) > 6 else "未配置", - "base_url": cls.get_base_url(), - "console_url": "https://bailian.console.aliyun.com/?tab=home#/home" - } diff --git a/extensions/Qwen_MT/utils/debug_utils.py b/extensions/Qwen_MT/utils/debug_utils.py deleted file mode 100644 index 31b611c..0000000 --- a/extensions/Qwen_MT/utils/debug_utils.py +++ /dev/null @@ -1,188 +0,0 @@ -""" -Debug utilities for ComfyUI plugin development. -Provides logging, tensor analysis, and performance profiling tools. -""" - -import time -import functools -from typing import Any, Optional -import torch - - -class DebugUtils: - """ - Debug utilities for plugin development. - """ - - DEBUG_ENABLED = False # 默认关闭调试模式,保持控制台简洁 - - @classmethod - def log(cls, message: str, level: str = "info") -> None: - """ - Log a debug message. - - Args: - message: Message to log - level: Log level (info, warning, error) - """ - if not cls.DEBUG_ENABLED: - return - - timestamp = time.strftime("%Y-%m-%d %H:%M:%S") - prefix = f"[{timestamp}] [ComfyUI-Qwen-MT] [{level.upper()}]" - print(f"{prefix} {message}") - - @classmethod - def log_tensor_stats(cls, tensor: torch.Tensor, name: str) -> None: - """ - Log tensor statistics for debugging. - - Args: - tensor: Tensor to analyze - name: Name/description of the tensor - """ - if not cls.DEBUG_ENABLED: - return - - if not isinstance(tensor, torch.Tensor): - cls.log(f"{name}: Not a tensor, type: {type(tensor)}", "warning") - return - - stats = { - "shape": list(tensor.shape), - "dtype": str(tensor.dtype), - "device": str(tensor.device), - "min": float(tensor.min()) if tensor.numel() > 0 else "empty", - "max": float(tensor.max()) if tensor.numel() > 0 else "empty", - "mean": float(tensor.mean()) if tensor.numel() > 0 else "empty", - "std": float(tensor.std()) if tensor.numel() > 0 else "empty" - } - - cls.log(f"Tensor {name}: {stats}") - - @classmethod - def visualize_tensor(cls, tensor: torch.Tensor, filename: str) -> None: - """ - Save tensor as image for visualization. - - Args: - tensor: Tensor to visualize - filename: Output filename - """ - if not cls.DEBUG_ENABLED: - return - - try: - import torchvision.transforms as transforms - from PIL import Image - - # Convert tensor to PIL Image format - if tensor.dim() == 4: # BCHW format - tensor = tensor[0] # Take first batch - elif tensor.dim() == 3 and tensor.shape[0] in [1, 3, 4]: # CHW format - pass - elif tensor.dim() == 3: # HWC format - tensor = tensor.permute(2, 0, 1) - - # Normalize to [0, 1] - tensor = (tensor - tensor.min()) / (tensor.max() - tensor.min() + 1e-8) - - # Convert to PIL and save - to_pil = transforms.ToPILImage() - image = to_pil(tensor) - image.save(filename) - - cls.log(f"Tensor visualization saved to {filename}") - - except Exception as e: - cls.log(f"Failed to visualize tensor: {e}", "error") - - @classmethod - def timeit(cls, func): - """ - Decorator for timing function execution. - - Args: - func: Function to time - - Returns: - Wrapped function with timing - """ - @functools.wraps(func) - def wrapper(*args, **kwargs): - if not cls.DEBUG_ENABLED: - return func(*args, **kwargs) - - start_time = time.time() - result = func(*args, **kwargs) - end_time = time.time() - - execution_time = end_time - start_time - cls.log(f"Function {func.__name__} executed in {execution_time:.4f}s") - - return result - return wrapper - - @classmethod - def memory_usage(cls) -> dict: - """ - Get current memory usage information. - - Returns: - Dictionary with memory usage stats - """ - if not torch.cuda.is_available(): - return {"cuda_available": False} - - stats = { - "cuda_available": True, - "allocated": torch.cuda.memory_allocated(), - "cached": torch.cuda.memory_reserved(), - "max_allocated": torch.cuda.max_memory_allocated(), - "max_cached": torch.cuda.max_memory_reserved() - } - - # Convert to MB - for key in stats: - if key != "cuda_available" and isinstance(stats[key], int): - stats[key] = stats[key] / (1024 * 1024) - - return stats - - @classmethod - def log_memory_usage(cls, prefix: str = "") -> None: - """ - Log current memory usage. - - Args: - prefix: Prefix for log message - """ - if not cls.DEBUG_ENABLED: - return - - stats = cls.memory_usage() - if stats["cuda_available"]: - cls.log(f"{prefix} Memory - Allocated: {stats['allocated']:.1f}MB, " - f"Cached: {stats['cached']:.1f}MB") - else: - cls.log(f"{prefix} CUDA not available") - - @classmethod - def cleanup_memory(cls) -> None: - """ - Clean up GPU memory. - """ - if torch.cuda.is_available(): - torch.cuda.empty_cache() - cls.log("GPU memory cache cleared") - - @classmethod - def enable_debug(cls, enabled: bool = True) -> None: - """ - Enable or disable debug logging. - - Args: - enabled: Whether to enable debug logging - """ - cls.DEBUG_ENABLED = enabled - cls.log(f"Debug logging {'enabled' if enabled else 'disabled'}") diff --git a/extensions/Qwen_MT/utils/language_utils.py b/extensions/Qwen_MT/utils/language_utils.py deleted file mode 100644 index d672d3d..0000000 --- a/extensions/Qwen_MT/utils/language_utils.py +++ /dev/null @@ -1,100 +0,0 @@ -""" -Language utilities for Qwen-MT translation. -Contains supported languages mapping based on official documentation. -""" - -# Supported languages mapping based on official Qwen-MT documentation -SUPPORTED_LANGUAGES = { - "英语": "en", - "简体中文": "zh", - "繁体中文": "zh_tw", - "俄语": "ru", - "日语": "ja", - "韩语": "ko", - "西班牙语": "es", - "法语": "fr", - "葡萄牙语": "pt", - "德语": "de", - "意大利语": "it", - "泰语": "th", - "越南语": "vi", - "印度尼西亚语": "id", - "马来语": "ms", - "阿拉伯语": "ar", - "印地语": "hi", - "希伯来语": "he", - "缅甸语": "my", - "泰米尔语": "ta", - "乌尔都语": "ur", - "孟加拉语": "bn", - "波兰语": "pl", - "荷兰语": "nl", - "罗马尼亚语": "ro", - "土耳其语": "tr", - "高棉语": "km", - "老挝语": "lo", - "粤语": "yue", - "捷克语": "cs", - "希腊语": "el", - "瑞典语": "sv", - "匈牙利语": "hu", - "丹麦语": "da", - "芬兰语": "fi", - "乌克兰语": "uk", - "保加利亚语": "bg", - "塞尔维亚语": "sr", - "泰卢固语": "te", - "南非荷兰语": "af", - "亚美尼亚语": "hy", - "阿萨姆语": "as", - "阿斯图里亚斯语": "ast", - "巴斯克语": "eu", - "白俄罗斯语": "be", - "波斯尼亚语": "bs", - "加泰罗尼亚语": "ca", - "宿务语": "ceb", - "克罗地亚语": "hr", - "埃及阿拉伯语": "arz", - "爱沙尼亚语": "et", - "加利西亚语": "gl", - "格鲁吉亚语": "ka", - "古吉拉特语": "gu", - "冰岛语": "is", - "爪哇语": "jv", - "卡纳达语": "kn", - "哈萨克语": "kk", - "拉脱维亚语": "lv", - "立陶宛语": "lt", - "卢森堡语": "lb", - "马其顿语": "mk", - "马加希语": "mai", - "马耳他语": "mt", - "马拉地语": "mr", - "美索不达米亚阿拉伯语": "acm", - "摩洛哥阿拉伯语": "ary", - "内志阿拉伯语": "ars", - "尼泊尔语": "ne", - "北阿塞拜疆语": "az", - "北黎凡特阿拉伯语": "apc", - "北乌兹别克语": "uz", - "书面语挪威语": "nb", - "新挪威语": "nn", - "奥克语": "oc", - "奥里亚语": "or", - "邦阿西楠语": "pag", - "西西里语": "scn", - "信德语": "sd", - "僧伽罗语": "si", - "斯洛伐克语": "sk", - "斯洛文尼亚语": "sl", - "南黎凡特阿拉伯语": "ajp", - "斯瓦希里语": "sw", - "他加禄语": "tl", - "塔伊兹-亚丁阿拉伯语": "acq", - "托斯克阿尔巴尼亚语": "sq", - "突尼斯阿拉伯语": "aeb", - "威尼斯语": "vec", - "瓦莱语": "war", - "威尔士语": "cy", - "西波斯语": "fa" -} diff --git a/extensions/Qwen_MT/utils/resource_cache.py b/extensions/Qwen_MT/utils/resource_cache.py deleted file mode 100644 index 1013b00..0000000 --- a/extensions/Qwen_MT/utils/resource_cache.py +++ /dev/null @@ -1,220 +0,0 @@ -""" -Resource caching utilities for ComfyUI plugin. -Provides caching for expensive operations like API clients and model loading. -""" - -import time -import threading -from typing import Dict, Any, Optional, Callable -from openai import OpenAI -from .debug_utils import DebugUtils - - -class ResourceCache: - """ - Thread-safe resource cache for expensive operations. - """ - - _cache: Dict[str, Any] = {} - _cache_timestamps: Dict[str, float] = {} - _cache_lock = threading.Lock() - _cache_ttl = 3600 # 1 hour default TTL - - @classmethod - def get(cls, key: str, factory: Callable[[], Any], ttl: Optional[int] = None) -> Any: - """ - Get a cached resource or create it if not exists. - - Args: - key: Cache key - factory: Function to create the resource if not cached - ttl: Time to live in seconds - - Returns: - Cached or newly created resource - """ - with cls._cache_lock: - current_time = time.time() - cache_ttl = ttl or cls._cache_ttl - - # Check if cache exists and is not expired - if (key in cls._cache and - key in cls._cache_timestamps and - current_time - cls._cache_timestamps[key] < cache_ttl): - - # DebugUtils.log(f"Cache hit for key: {key}") - return cls._cache[key] - - # Create new resource - # DebugUtils.log(f"Cache miss for key: {key}, creating new resource") - resource = factory() - - # Store in cache - cls._cache[key] = resource - cls._cache_timestamps[key] = current_time - - return resource - - @classmethod - def set(cls, key: str, value: Any) -> None: - """ - Manually set a cache entry. - - Args: - key: Cache key - value: Value to cache - """ - with cls._cache_lock: - cls._cache[key] = value - cls._cache_timestamps[key] = time.time() - # DebugUtils.log(f"Manually cached resource with key: {key}") - - @classmethod - def invalidate(cls, key: str) -> bool: - """ - Invalidate a cache entry. - - Args: - key: Cache key to invalidate - - Returns: - True if key was found and removed, False otherwise - """ - with cls._cache_lock: - removed = False - if key in cls._cache: - del cls._cache[key] - removed = True - if key in cls._cache_timestamps: - del cls._cache_timestamps[key] - - if removed: - pass - # DebugUtils.log(f"Invalidated cache key: {key}") - - return removed - - @classmethod - def clear(cls) -> None: - """ - Clear all cache entries. - """ - with cls._cache_lock: - count = len(cls._cache) - cls._cache.clear() - cls._cache_timestamps.clear() - DebugUtils.log(f"Cleared {count} cache entries") - - @classmethod - def cleanup_expired(cls) -> int: - """ - Remove expired cache entries. - - Returns: - Number of expired entries removed - """ - with cls._cache_lock: - current_time = time.time() - expired_keys = [] - - for key, timestamp in cls._cache_timestamps.items(): - if current_time - timestamp >= cls._cache_ttl: - expired_keys.append(key) - - for key in expired_keys: - if key in cls._cache: - del cls._cache[key] - del cls._cache_timestamps[key] - - if expired_keys: - DebugUtils.log(f"Cleaned up {len(expired_keys)} expired cache entries") - - return len(expired_keys) - - @classmethod - def get_stats(cls) -> Dict[str, Any]: - """ - Get cache statistics. - - Returns: - Dictionary with cache stats - """ - with cls._cache_lock: - current_time = time.time() - expired_count = 0 - - for timestamp in cls._cache_timestamps.values(): - if current_time - timestamp >= cls._cache_ttl: - expired_count += 1 - - return { - "total_entries": len(cls._cache), - "expired_entries": expired_count, - "active_entries": len(cls._cache) - expired_count, - "cache_ttl": cls._cache_ttl - } - - @classmethod - def cached(cls, ttl: Optional[int] = None): - """ - Decorator for caching function results. - - Args: - ttl: Time to live in seconds - - Returns: - Decorator function - """ - def decorator(func: Callable) -> Callable: - def wrapper(*args, **kwargs): - # Create cache key from function name and arguments - key = f"{func.__name__}:{hash(str(args) + str(sorted(kwargs.items())))}" - - def factory(): - return func(*args, **kwargs) - - return cls.get(key, factory, ttl) - - return wrapper - return decorator - - @classmethod - def get_api_client(cls, api_key: str, base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1") -> OpenAI: - """ - Get or create a cached API client. - - Args: - api_key: API key for authentication - base_url: Base URL for the API - - Returns: - OpenAI client instance - """ - # Create cache key based on api_key hash and base_url - key = f"api_client:{hash(api_key)}:{base_url}" - - def factory(): - # DebugUtils.log("Creating new API client") - # 禁用OpenAI客户端的HTTP日志记录 - import logging - logging.getLogger("openai").setLevel(logging.WARNING) - logging.getLogger("httpx").setLevel(logging.WARNING) - logging.getLogger("httpcore").setLevel(logging.WARNING) - - return OpenAI( - api_key=api_key, - base_url=base_url - ) - - return cls.get(key, factory, ttl=1800) # 30 minutes TTL for API clients - - @classmethod - def set_cache_ttl(cls, ttl: int) -> None: - """ - Set default cache TTL. - - Args: - ttl: Time to live in seconds - """ - cls._cache_ttl = ttl - # DebugUtils.log(f"Cache TTL set to {ttl} seconds") diff --git a/node/qwen_mt_translator.py b/node/qwen_mt_translator.py deleted file mode 100644 index 253ae74..0000000 --- a/node/qwen_mt_translator.py +++ /dev/null @@ -1,316 +0,0 @@ -""" -Qwen-MT Translation Nodes for ComfyUI -Implements machine translation using Alibaba Cloud's Qwen-MT model. -""" - -import os -import json -import torch -import logging -from typing import Dict, List, Any, Tuple, Optional -from openai import OpenAI - -# 修改导入路径以适配DD-NODES项目结构 -from ..extensions.Qwen_MT.utils.language_utils import SUPPORTED_LANGUAGES -from ..extensions.Qwen_MT.utils.debug_utils import DebugUtils -from ..extensions.Qwen_MT.utils.resource_cache import ResourceCache -from ..extensions.Qwen_MT.utils.api_config import APIConfigManager - -# 禁用HTTP相关的详细日志记录,保持控制台简洁 -logging.getLogger("openai").setLevel(logging.WARNING) -logging.getLogger("httpx").setLevel(logging.WARNING) -logging.getLogger("httpcore").setLevel(logging.WARNING) - - -class QwenMTTranslatorNode: - """ - 通义千问翻译节点 - 支持多种翻译模式 - """ - - CATEGORY = "🍺DD系列节点/🌐翻译" - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "文本": ("STRING", { - "multiline": True, - "dynamicPrompts": True, - "default": "请输入要翻译的文本" - }), - "源语言": (list(SUPPORTED_LANGUAGES.keys()) + ["自动"], { - "default": "自动" - }), - "目标语言": (list(SUPPORTED_LANGUAGES.keys()), { - "default": "英语" - }), - "翻译模式": (["通用翻译", "术语翻译", "领域翻译"], { - "default": "通用翻译" - }), - "模型": (["qwen-mt-plus", "qwen-mt-turbo"], { - "default": "qwen-mt-turbo" - }) - }, - "optional": { - "模式配置": ("STRING", { - "multiline": True, - "default": "根据翻译模式自动调整:\n\n通用翻译:无需额外配置\n\n术语翻译:请输入JSON格式的术语词典\n[\n {\n \"source\": \"术语\",\n \"target\": \"terminology\"\n }\n]\n\n领域翻译:请输入领域提示文本\n例如:The text is from IT domain. Pay attention to technical terminologies when translating." - }) - }, - "hidden": { - "unique_id": "UNIQUE_ID" - } - } - - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("翻译结果",) - FUNCTION = "translate_text" - - @classmethod - def IS_CHANGED(cls, **kwargs): - # Force re-evaluation when API config changes - config_info = APIConfigManager.get_config_info() - return config_info["api_key_preview"] - - @classmethod - def VALIDATE_INPUTS(cls, **kwargs): - # 简化验证逻辑,主要检查API配置 - # 其他验证在运行时进行,避免节点连接时的验证问题 - - # 检查API配置 - if not APIConfigManager.is_configured(): - return "API密钥未配置,请点击节点底部的配置按钮设置API密钥" - - # 对于术语翻译的特殊验证 - translation_mode = kwargs.get("翻译模式", "通用翻译") - mode_config = kwargs.get("模式配置", "") - - if isinstance(translation_mode, str) and translation_mode == "术语翻译": - if isinstance(mode_config, str) and mode_config.strip(): - try: - term_list = json.loads(mode_config) - if not isinstance(term_list, list): - return "术语翻译模式下,模式配置必须是JSON数组格式" - - for term in term_list: - if not isinstance(term, dict): - return "每个术语必须是JSON对象" - if "source" not in term or "target" not in term: - return "每个术语必须包含'source'和'target'字段" - - except json.JSONDecodeError: - return "术语翻译模式下,模式配置必须是有效的JSON格式" - - return True - - def translate_text(self, **kwargs) -> Tuple[str]: - """ - 翻译文本使用通义千问MT模型。 - """ - try: - # 获取参数值并处理中文参数名 - text = kwargs.get("文本", "") - source_lang = kwargs.get("源语言", "auto") - target_lang = kwargs.get("目标语言", "英语") - translation_mode = kwargs.get("翻译模式", "通用翻译") - model = kwargs.get("模型", "qwen-mt-turbo") - - # 运行时验证输入 - if not isinstance(text, str): - return (f"错误:输入文本类型不正确,期望字符串,得到 {type(text)}",) - - if not text or text.strip() == "": - return ("错误:输入文本不能为空",) - - if len(text) > 10000: - return ("错误:文本长度不能超过10,000个字符",) - - # 验证语言选择 - if source_lang == target_lang and source_lang != "自动": - return ("错误:源语言和目标语言不能相同",) - - # 从配置管理器获取API配置 - api_key = APIConfigManager.get_api_key() - base_url = APIConfigManager.get_base_url() - - if not api_key: - return ("API密钥未配置,请点击节点底部的配置按钮设置API密钥",) - - except Exception as e: - return (f"参数验证错误:{str(e)}",) - - # 获取模式配置参数 - mode_config = kwargs.get("模式配置", "") - - # 处理语言名称映射 - if source_lang == "自动": - source_lang = "auto" - else: - # 将中文语言名称转换为API需要的英文名称 - source_lang = self._convert_to_api_language(source_lang) - - # 将目标语言转换为API需要的英文名称 - target_lang = self._convert_to_api_language(target_lang) - - try: - # Use cached API client - client = ResourceCache.get_api_client(api_key, base_url) - - # Prepare basic translation options - translation_options = { - "source_lang": source_lang, - "target_lang": target_lang - } - - # 根据翻译模式添加特定选项 - if translation_mode == "术语翻译" and mode_config.strip(): - try: - term_list = json.loads(mode_config) - translation_options["terms"] = term_list - # DebugUtils.log(f"使用术语翻译模式,包含 {len(term_list)} 个术语") - except json.JSONDecodeError: - # DebugUtils.log("术语配置格式错误,使用通用翻译模式", level="warning") - pass - - elif translation_mode == "领域翻译" and mode_config.strip(): - translation_options["domains"] = mode_config.strip() - # DebugUtils.log(f"使用领域翻译模式: {mode_config[:50]}...") - - else: - # DebugUtils.log("使用通用翻译模式") - pass - - # Prepare messages - messages = [{"role": "user", "content": text}] - - # DebugUtils.log(f"从 {source_lang} 翻译到 {target_lang}") - - # Non-stream translation - completion = client.chat.completions.create( - model=model, - messages=messages, - extra_body={"translation_options": translation_options} - ) - translated_text = completion.choices[0].message.content - - # DebugUtils.log(f"翻译完成: {len(translated_text)} 个字符") - - return (translated_text,) - - except Exception as e: - error_msg = f"翻译失败: {str(e)}" - # DebugUtils.log(error_msg, level="error") - return (error_msg,) - - def _convert_to_api_language(self, lang_name: str) -> str: - """ - 将界面显示的语言名称转换为API需要的英文名称 - """ - # 中文到英文的语言名称映射(基于官方文档) - lang_mapping = { - "英语": "English", - "简体中文": "Chinese", - "繁体中文": "Traditional Chinese", - "俄语": "Russian", - "日语": "Japanese", - "韩语": "Korean", - "西班牙语": "Spanish", - "法语": "French", - "葡萄牙语": "Portuguese", - "德语": "German", - "意大利语": "Italian", - "泰语": "Thai", - "越南语": "Vietnamese", - "印度尼西亚语": "Indonesian", - "马来语": "Malay", - "阿拉伯语": "Arabic", - "印地语": "Hindi", - "希伯来语": "Hebrew", - "缅甸语": "Burmese", - "泰米尔语": "Tamil", - "乌尔都语": "Urdu", - "孟加拉语": "Bengali", - "波兰语": "Polish", - "荷兰语": "Dutch", - "罗马尼亚语": "Romanian", - "土耳其语": "Turkish", - "高棉语": "Khmer", - "老挝语": "Lao", - "粤语": "Cantonese", - "捷克语": "Czech", - "希腊语": "Greek", - "瑞典语": "Swedish", - "匈牙利语": "Hungarian", - "丹麦语": "Danish", - "芬兰语": "Finnish", - "乌克兰语": "Ukrainian", - "保加利亚语": "Bulgarian", - "塞尔维亚语": "Serbian", - "泰卢固语": "Telugu", - "南非荷兰语": "Afrikaans", - "亚美尼亚语": "Armenian", - "阿萨姆语": "Assamese", - "阿斯图里亚斯语": "Asturian", - "巴斯克语": "Basque", - "白俄罗斯语": "Belarusian", - "波斯尼亚语": "Bosnian", - "加泰罗尼亚语": "Catalan", - "宿务语": "Cebuano", - "克罗地亚语": "Croatian", - "埃及阿拉伯语": "Egyptian Arabic", - "爱沙尼亚语": "Estonian", - "加利西亚语": "Galician", - "格鲁吉亚语": "Georgian", - "古吉拉特语": "Gujarati", - "冰岛语": "Icelandic", - "爪哇语": "Javanese", - "卡纳达语": "Kannada", - "哈萨克语": "Kazakh", - "拉脱维亚语": "Latvian", - "立陶宛语": "Lithuanian", - "卢森堡语": "Luxembourgish", - "马其顿语": "Macedonian", - "马加希语": "Maithili", - "马耳他语": "Maltese", - "马拉地语": "Marathi", - "美索不达米亚阿拉伯语": "Mesopotamian Arabic", - "摩洛哥阿拉伯语": "Moroccan Arabic", - "内志阿拉伯语": "Najdi Arabic", - "尼泊尔语": "Nepali", - "北阿塞拜疆语": "North Azerbaijani", - "北黎凡特阿拉伯语": "North Levantine Arabic", - "北乌兹别克语": "Northern Uzbek", - "书面语挪威语": "Norwegian Bokmål", - "新挪威语": "Norwegian Nynorsk", - "奥克语": "Occitan", - "奥里亚语": "Odia", - "邦阿西楠语": "Pangasinan", - "西西里语": "Sicilian", - "信德语": "Sindhi", - "僧伽罗语": "Sinhala", - "斯洛伐克语": "Slovak", - "斯洛文尼亚语": "Slovenian", - "南黎凡特阿拉伯语": "South Levantine Arabic", - "斯瓦希里语": "Swahili", - "他加禄语": "Tagalog", - "塔伊兹-亚丁阿拉伯语": "Ta'izzi-Adeni Arabic", - "托斯克阿尔巴尼亚语": "Tosk Albanian", - "突尼斯阿拉伯语": "Tunisian Arabic", - "威尼斯语": "Venetian", - "瓦莱语": "Waray", - "威尔士语": "Welsh", - "西波斯语": "Western Persian" - } - - # 返回对应的英文名称,如果找不到就返回原名称(可能本身就是英文) - return lang_mapping.get(lang_name, lang_name) - - -# ComfyUI节点映射 -NODE_CLASS_MAPPINGS = { - "DD-QwenMTTranslator": QwenMTTranslatorNode -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "DD-QwenMTTranslator": "DD Qwen-MT翻译" -}