简化Qwen-MT节点结构

This commit is contained in:
Dontdrunk
2025-08-09 22:06:00 +08:00
parent 8fe6659926
commit da0c2eef66
9 changed files with 263 additions and 984 deletions
+1 -1
View File
@@ -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']
+1 -2
View File
@@ -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")
+261 -9
View File
@@ -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:
-15
View File
@@ -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"
]
-133
View File
@@ -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"
}
-188
View File
@@ -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'}")
-100
View File
@@ -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"
}
-220
View File
@@ -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")
-316
View File
@@ -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翻译"
}