简化Qwen-MT节点结构
This commit is contained in:
+1
-1
@@ -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']
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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'}")
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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翻译"
|
||||
}
|
||||
Reference in New Issue
Block a user