From c8fef70f6dda6def15126f5894d5df32e1f6e521 Mon Sep 17 00:00:00 2001 From: Cyber Dick Lang <286878701@qq.com> Date: Mon, 27 Oct 2025 17:31:38 +0800 Subject: [PATCH] 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 --- __init__.py | 12 ++ nodes/tools/text_translator.py | 304 +++++++++++++++++++++++++++++++++ pyproject.toml | 2 +- 3 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 nodes/tools/text_translator.py diff --git a/__init__.py b/__init__.py index 4b9aae9..c8be29a 100644 --- a/__init__.py +++ b/__init__.py @@ -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", ] } diff --git a/nodes/tools/text_translator.py b/nodes/tools/text_translator.py new file mode 100644 index 0000000..378232b --- /dev/null +++ b/nodes/tools/text_translator.py @@ -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)", +} diff --git a/pyproject.toml b/pyproject.toml index 62db1c6..59d34f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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",