feat: Add Text Translator (UTK) node with multiple API support

- Added comprehensive text translation node supporting multiple free and paid APIs
- Implemented 5 translation providers: Google Translate (Free), LibreTranslate (Free), MyMemory (Free), DeepL (Paid), Azure Translator (Paid)
- Added auto mode with intelligent fallback between providers
- Implemented API key validation and error handling
- Support for 60+ languages with auto-detection
- Added proper error messages and status reporting
- Version bump to 1.3.9
This commit is contained in:
Cyber Dick Lang
2025-10-27 17:31:38 +08:00
parent de85eb0688
commit c8fef70f6d
3 changed files with 317 additions and 1 deletions
+12
View File
@@ -677,6 +677,15 @@ except ImportError:
LAZY_SWITCH_MAPPINGS = {}
LAZY_SWITCH_DISPLAY = {}
try:
from .nodes.tools.text_translator import \
NODE_CLASS_MAPPINGS as TEXT_TRANSLATOR_MAPPINGS
from .nodes.tools.text_translator import \
NODE_DISPLAY_NAME_MAPPINGS as TEXT_TRANSLATOR_DISPLAY
except ImportError:
TEXT_TRANSLATOR_MAPPINGS = {}
TEXT_TRANSLATOR_DISPLAY = {}
# 合并所有节点映射
NODE_CLASS_MAPPINGS = {}
@@ -715,6 +724,7 @@ NODE_CLASS_MAPPINGS.update(KONTEXT_PRESETS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(PROMPT_HELPER_MAPPINGS)
NODE_CLASS_MAPPINGS.update(COLOR_TO_MASK_MAPPINGS)
NODE_CLASS_MAPPINGS.update(LAZY_SWITCH_MAPPINGS)
NODE_CLASS_MAPPINGS.update(TEXT_TRANSLATOR_MAPPINGS)
# 合并显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {}
@@ -753,6 +763,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(KONTEXT_PRESETS_DISPLAY_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(PROMPT_HELPER_DISPLAY_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(COLOR_TO_MASK_DISPLAY)
NODE_DISPLAY_NAME_MAPPINGS.update(LAZY_SWITCH_DISPLAY)
NODE_DISPLAY_NAME_MAPPINGS.update(TEXT_TRANSLATOR_DISPLAY)
NODE_CATEGORIES = {
"UniversalToolkit": [
@@ -792,6 +803,7 @@ NODE_CATEGORIES = {
"ColorToMask_UTK",
"LazySwitchKJ_UTK",
"APIImageGenerator_UTK",
"TextTranslator_UTK",
]
}
+304
View File
@@ -0,0 +1,304 @@
"""
Text Translator Node
~~~~~~~~~~~~~~~~~~~
A comprehensive text translation node supporting multiple free and paid translation APIs.
:copyright: (c) 2024 by May
:license: MIT, see LICENSE for more details.
"""
import json
import requests
import time
from typing import Dict, List, Optional, Tuple
import hashlib
import random
class TranslationProvider:
"""Base class for translation providers"""
def __init__(self, name: str, is_free: bool, requires_key: bool = False):
self.name = name
self.is_free = is_free
self.requires_key = requires_key
self.priority = 0 # Lower number = higher priority
def translate(self, text: str, target_lang: str, source_lang: str = "auto", api_key: str = None) -> Tuple[bool, str]:
"""Translate text. Returns (success, result)"""
raise NotImplementedError
class GoogleTranslateProvider(TranslationProvider):
"""Google Translate (Free) - Using web interface"""
def __init__(self):
super().__init__("Google Translate (Free)", True, False)
self.priority = 1
self.base_url = "https://translate.googleapis.com/translate_a/single"
def translate(self, text: str, target_lang: str, source_lang: str = "auto", api_key: str = None) -> Tuple[bool, str]:
try:
params = {
'client': 'gtx',
'sl': source_lang,
'tl': target_lang,
'dt': 't',
'q': text
}
response = requests.get(self.base_url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
if result and len(result) > 0 and result[0]:
translated_text = ''.join([item[0] for item in result[0] if item[0]])
return True, translated_text
return False, "Translation failed"
except Exception as e:
return False, f"Google Translate error: {str(e)}"
class LibreTranslateProvider(TranslationProvider):
"""LibreTranslate (Free) - Open source translation"""
def __init__(self):
super().__init__("LibreTranslate (Free)", True, False)
self.priority = 2
self.base_url = "https://libretranslate.de/translate"
def translate(self, text: str, target_lang: str, source_lang: str = "auto", api_key: str = None) -> Tuple[bool, str]:
try:
data = {
'q': text,
'source': source_lang if source_lang != "auto" else "en",
'target': target_lang,
'format': 'text'
}
response = requests.post(self.base_url, data=data, timeout=10)
response.raise_for_status()
result = response.json()
if 'translatedText' in result:
return True, result['translatedText']
return False, "Translation failed"
except Exception as e:
return False, f"LibreTranslate error: {str(e)}"
class MyMemoryProvider(TranslationProvider):
"""MyMemory (Free) - Community translation"""
def __init__(self):
super().__init__("MyMemory (Free)", True, False)
self.priority = 3
self.base_url = "https://api.mymemory.translated.net/get"
def translate(self, text: str, target_lang: str, source_lang: str = "auto", api_key: str = None) -> Tuple[bool, str]:
try:
params = {
'q': text,
'langpair': f"{source_lang}|{target_lang}" if source_lang != "auto" else f"auto|{target_lang}"
}
response = requests.get(self.base_url, params=params, timeout=10)
response.raise_for_status()
result = response.json()
if result.get('responseStatus') == 200 and 'responseData' in result:
return True, result['responseData']['translatedText']
return False, "Translation failed"
except Exception as e:
return False, f"MyMemory error: {str(e)}"
class DeepLProvider(TranslationProvider):
"""DeepL (Paid) - High quality translation"""
def __init__(self):
super().__init__("DeepL (Paid)", False, True)
self.priority = 4
self.base_url = "https://api-free.deepl.com/v2/translate"
def translate(self, text: str, target_lang: str, source_lang: str = "auto", api_key: str = None) -> Tuple[bool, str]:
if not api_key:
return False, "DeepL requires API key"
try:
data = {
'auth_key': api_key,
'text': text,
'target_lang': target_lang.upper()
}
if source_lang != "auto":
data['source_lang'] = source_lang.upper()
response = requests.post(self.base_url, data=data, timeout=10)
response.raise_for_status()
result = response.json()
if 'translations' in result and len(result['translations']) > 0:
return True, result['translations'][0]['text']
return False, "Translation failed"
except Exception as e:
return False, f"DeepL error: {str(e)}"
class AzureTranslatorProvider(TranslationProvider):
"""Azure Translator (Paid) - Microsoft translation service"""
def __init__(self):
super().__init__("Azure Translator (Paid)", False, True)
self.priority = 5
self.base_url = "https://api.cognitive.microsofttranslator.com/translate"
def translate(self, text: str, target_lang: str, source_lang: str = "auto", api_key: str = None) -> Tuple[bool, str]:
if not api_key:
return False, "Azure Translator requires API key"
try:
headers = {
'Ocp-Apim-Subscription-Key': api_key,
'Content-Type': 'application/json'
}
params = {
'api-version': '3.0',
'to': target_lang
}
if source_lang != "auto":
params['from'] = source_lang
body = [{'text': text}]
response = requests.post(self.base_url, params=params, headers=headers, json=body, timeout=10)
response.raise_for_status()
result = response.json()
if result and len(result) > 0 and 'translations' in result[0]:
return True, result[0]['translations'][0]['text']
return False, "Translation failed"
except Exception as e:
return False, f"Azure Translator error: {str(e)}"
class TextTranslator_UTK:
CATEGORY = "UniversalToolkit/Tools"
def __init__(self):
self.providers = {
"Google Translate (Free)": GoogleTranslateProvider(),
"LibreTranslate (Free)": LibreTranslateProvider(),
"MyMemory (Free)": MyMemoryProvider(),
"DeepL (Paid)": DeepLProvider(),
"Azure Translator (Paid)": AzureTranslatorProvider(),
}
@classmethod
def INPUT_TYPES(cls):
# Language codes for common languages
languages = [
"auto", "en", "zh", "zh-cn", "zh-tw", "ja", "ko", "fr", "de", "es", "it",
"pt", "ru", "ar", "hi", "th", "vi", "tr", "pl", "nl", "sv", "da", "no",
"fi", "cs", "hu", "ro", "bg", "hr", "sk", "sl", "et", "lv", "lt", "el",
"he", "fa", "ur", "bn", "ta", "te", "ml", "kn", "gu", "pa", "or", "as",
"ne", "si", "my", "km", "lo", "ka", "am", "sw", "zu", "af", "sq", "eu",
"be", "bs", "ca", "cy", "eo", "gl", "is", "mk", "mt", "sr", "uk", "uz"
]
# Provider list with free/paid indicators
provider_list = [
"auto",
"Google Translate (Free)",
"LibreTranslate (Free)",
"MyMemory (Free)",
"DeepL (Paid)",
"Azure Translator (Paid)"
]
return {
"required": {
"text": ("STRING", {
"multiline": True,
"default": "Hello, world!",
"placeholder": "Enter text to translate..."
}),
"target_language": (languages, {"default": "zh"}),
"source_language": (languages, {"default": "auto"}),
"provider": (provider_list, {"default": "auto"}),
},
"optional": {
"api_key": ("STRING", {
"default": "",
"placeholder": "API key (required for paid services)"
}),
}
}
RETURN_TYPES = ("STRING", "STRING", "STRING")
RETURN_NAMES = ("translated_text", "provider_used", "status_message")
FUNCTION = "translate_text"
def translate_text(self, text: str, target_language: str, source_language: str, provider: str, api_key: str = ""):
"""Main translation function"""
if not text.strip():
return ("", "", "Error: No text provided")
# Clean API key
api_key = api_key.strip() if api_key else ""
if provider == "auto":
# Try providers in priority order
sorted_providers = sorted(self.providers.values(), key=lambda x: x.priority)
for provider_obj in sorted_providers:
# Check if API key is required but not provided
if provider_obj.requires_key and not api_key:
continue
success, result = provider_obj.translate(text, target_language, source_language, api_key)
if success:
return (result, provider_obj.name, f"Successfully translated using {provider_obj.name}")
else:
# Log the error but continue to next provider
print(f"Translation failed with {provider_obj.name}: {result}")
continue
return ("", "", "Error: All translation providers failed")
else:
# Use specific provider
if provider not in self.providers:
return ("", "", f"Error: Unknown provider '{provider}'")
provider_obj = self.providers[provider]
# Check API key requirement
if provider_obj.requires_key and not api_key:
return ("", "", f"Error: {provider} requires an API key")
success, result = provider_obj.translate(text, target_language, source_language, api_key)
if success:
return (result, provider_obj.name, f"Successfully translated using {provider_obj.name}")
else:
return ("", "", f"Error: {result}")
# Node mappings
NODE_CLASS_MAPPINGS = {
"TextTranslator_UTK": TextTranslator_UTK,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TextTranslator_UTK": "Text Translator (UTK)",
}
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "universaltoolkit"
description = "A comprehensive toolkit based on ComfyUI, providing image, mask, audio, and tools nodes, fully modular and v3 compatible."
version = "1.3.8"
version = "1.3.9"
license = {file = "LICENSE"}
dependencies = [
"torch>=1.9.0",