Files
yawiii-ComfyUI-Prompt-Assis…/node/translate_node.py
T

372 lines
16 KiB
Python

import asyncio
import random
import re
import time
import threading
import copy
from typing import Dict, Any, List
import hashlib
from server import PromptServer
from ..server import send_toast_notification
from ..services.llm import LLMService
from ..services.baidu import BaiduTranslateService
from ..services.error_util import format_api_error, format_baidu_translate_error
from comfy.model_management import InterruptProcessingException
# 定义ANSI颜色代码常量
GREEN = "\033[92m"
RESET = "\033[0m"
def run_async_translation(service_func, text, from_lang, to_lang, request_id, result_container, **kwargs):
"""
在一个新的事件循环中运行异步翻译任务
"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(
service_func(text, from_lang, to_lang, request_id, **kwargs)
)
result_container['result'] = result
finally:
loop.close()
class PromptTranslate:
"""
文本翻译节点
将输入的文本翻译成英文,支持多种翻译服务
"""
# 定义日志前缀(带绿色)
LOG_PREFIX = f"{GREEN}[PromptAssistant]{RESET}"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"原文": ("STRING", {"forceInput": True, "default": ""}),
"翻译服务": (["百度翻译", "智谱翻译", "硅基流动翻译", "自定义"], {}),
"输出语言": (["英文", "中文"], {"default": "英文"}),
},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
"client_id": "CLIENT_ID",
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("翻译输出",)
FUNCTION = "translate"
CATEGORY = "✨提示词小助手"
OUTPUT_NODE = True
def _contains_chinese(self, text: str) -> bool:
"""检查文本是否包含中文字符"""
if not text:
return False
return bool(re.search('[\u4e00-\u9fa5]', text))
@classmethod
def IS_CHANGED(cls, 原文, 翻译服务, 输出语言, unique_id=None, extra_pnginfo=None, client_id=None):
"""
只在输入内容真正变化时才触发重新执行
使用输入参数的哈希值作为判断依据
"""
# 计算文本的哈希值
text_hash = 0
if 原文:
# 使用hashlib计算文本的哈希值,更安全和一致
text_hash = hashlib.md5(原文.encode('utf-8')).hexdigest()
# 组合所有输入的哈希值
input_hash = hash((
text_hash,
翻译服务,
输出语言
))
return input_hash
def translate(self, 原文, 翻译服务, 输出语言, unique_id=None, extra_pnginfo=None, client_id=None):
"""
翻翻译输出本函数
Args:
原文: 输入的文本
翻译服务: 选择的翻译器 ("百度翻译", "智谱翻译", "硅基流动翻译", "自定义")
输出语言: 目标翻译语言 ("英文", "中文")
unique_id: 节点的唯一ID
extra_pnginfo: 额外的PNG信息
client_id: 客户端ID
Returns:
dict: 包含UI显示和翻译结果
"""
# 根据选择设置翻译方向
from_lang = "auto"
to_lang = "en" if 输出语言 == "英文" else "zh"
# 检查输入
if not 原文:
return {"ui": {"翻译输出": ""}, "result": ("",)}
# --- 智能跳过翻译逻辑 ---
# 检查是否为纯英文 (只包含ASCII可打印字符)
is_pure_english = bool(re.fullmatch(r'[ -~]+', 原文))
# 检查是否为纯中文 (包含中文字符且不包含英文字母)
is_pure_chinese = self._contains_chinese(原文) and not re.search(r'[a-zA-Z]', 原文)
skip_translation = False
if to_lang == 'en' and is_pure_english:
print(f"{self.LOG_PREFIX} 原文为纯英文,目标为英文,无需翻译。")
skip_translation = True
elif to_lang == 'zh' and is_pure_chinese:
print(f"{self.LOG_PREFIX} 原文为纯中文,目标为中文,无需翻译。")
skip_translation = True
if skip_translation:
# 如果跳过翻译,直接返回原文并更新UI
try:
if unique_id is not None and extra_pnginfo is not None:
# 更安全的方式处理extra_pnginfo
if isinstance(extra_pnginfo, list) and len(extra_pnginfo) > 0:
if isinstance(extra_pnginfo[0], dict) and "workflow" in extra_pnginfo[0]:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id)),
None,
)
if node:
node["widgets_values"] = [原文, 翻译服务, 输出语言]
except Exception as e:
print(f"{self.LOG_PREFIX} 更新节点widgets_values时出错: {str(e)}")
return {"ui": {"翻译输出": 原文}, "result": (原文,)}
try:
# 创建请求ID
request_id = f"translate_{int(time.time())}_{random.randint(1000, 9999)}"
result_container = {}
# 从config_manager获取配置
from ..config_manager import config_manager
import nodes
# 根据选择的翻译服务调用不同的翻译API
if 翻译服务 == "百度翻译":
print(f"{self.LOG_PREFIX} 提示词翻译: 使用百度翻译服务")
thread = threading.Thread(
target=run_async_translation,
args=(BaiduTranslateService.translate, 原文, from_lang, to_lang, request_id, result_container)
)
thread.start()
else:
# 对应其他翻译服务,直接使用对应的配置
# 获取llm配置的副本,避免修改全局配置
llm_config = copy.deepcopy(config_manager.get_llm_config())
# 映射翻译服务选项到provider
provider_map = {
"智谱翻译": "zhipu",
"硅基流动翻译": "siliconflow",
"自定义": "custom"
}
# 获取选定的provider
selected_provider = provider_map.get(翻译服务)
if not selected_provider:
error_text = f"[翻译错误] 不支持的翻译服务: {翻译服务}"
# 发送toast通知
send_toast_notification(client_id, "error", "[✨提示词翻译] 翻译错误", f"不支持的翻译服务: {翻译服务}")
return {"ui": {"翻译输出": error_text}, "result": (error_text,)}
# 获取对应provider的配置
provider_config = self._get_provider_config(config_manager, selected_provider)
if not provider_config:
error_text = f"[翻译错误] 未找到{翻译服务}的配置"
# 发送toast通知
send_toast_notification(client_id, "error", "[✨提示词翻译] 翻译错误", f"未找到{翻译服务}的配置,请先完成API配置")
return {"ui": {"翻译输出": error_text}, "result": (error_text,)}
print(f"{self.LOG_PREFIX} 提示词翻译: 使用{翻译服务}服务, API: {provider_config.get('model')}")
# 使用特定provider的配置直接翻译
thread = threading.Thread(
target=self._run_llm_translation,
args=(原文, from_lang, to_lang, request_id, result_container, selected_provider, provider_config, client_id)
)
thread.start()
# 非阻塞等待并检查中断
while thread.is_alive():
nodes.before_node_execution()
time.sleep(0.1)
result = result_container.get('result')
# 检查翻译结果
if result and result.get('success'):
translated_text = result.get('data', {}).get('translated', '')
# 更新节点的widgets_values(参考show_text.py的实现)
try:
if unique_id is not None and extra_pnginfo is not None:
# 更安全的方式处理extra_pnginfo
if isinstance(extra_pnginfo, list) and len(extra_pnginfo) > 0:
if isinstance(extra_pnginfo[0], dict) and "workflow" in extra_pnginfo[0]:
workflow = extra_pnginfo[0]["workflow"]
node = next(
(x for x in workflow["nodes"] if str(x["id"]) == str(unique_id)),
None,
)
if node:
node["widgets_values"] = [translated_text, 翻译服务, 输出语言]
except Exception as e:
print(f"{self.LOG_PREFIX} 更新节点widgets_values时出错: {str(e)}")
return {"ui": {"翻译输出": translated_text}, "result": (translated_text,)}
else:
error_msg = result.get('error', '翻译失败,未知错误') if result else '翻译线程未返回结果'
# 如果是百度翻译的错误,使用专用格式化函数
if 翻译服务 == "百度翻译" and isinstance(result.get('error'), dict):
error_msg = format_baidu_translate_error(result.get('error'))
print(f"{self.LOG_PREFIX} 翻译节点错误: {error_msg}")
error_text = f"[翻译错误] {error_msg}"
# 发送toast通知
send_toast_notification(client_id, "error", "[✨提示词翻译] 翻译失败", error_msg)
return {"ui": {"翻译输出": error_text}, "result": (error_text,)}
except InterruptProcessingException:
# 用户取消任务时,静默处理
print(f"{self.LOG_PREFIX} 用户取消了翻译任务。")
return {"ui": {"翻译输出": "[任务已取消]"}, "result": ("[任务已取消]",)}
except Exception as e:
error_msg = format_api_error(e, 翻译服务)
print(f"{self.LOG_PREFIX} 翻译节点异常: {error_msg}")
error_text = f"[翻译异常] {error_msg}"
# 发送toast通知
send_toast_notification(client_id, "error", "[✨提示词翻译] 翻译异常", error_msg)
return {"ui": {"翻译输出": error_text}, "result": (error_text,)}
def _get_provider_config(self, config_manager, provider):
"""获取指定provider的配置"""
llm_config = config_manager.get_llm_config()
if 'providers' in llm_config and provider in llm_config['providers']:
return llm_config['providers'][provider]
return None
def _run_llm_translation(self, text, from_lang, to_lang, request_id, result_container, provider, provider_config, client_id=None):
"""在独立线程中运行LLM翻译"""
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# 获取API密钥和模型
api_key = provider_config.get('api_key', '')
model = provider_config.get('model', '')
base_url = provider_config.get('base_url', '')
if not api_key or not model:
result_container['result'] = {
"success": False,
"error": f"请先配置{provider}的API密钥和模型"
}
# 不在这里发送toast通知,由主函数统一处理
return
# 创建临时客户端
client = LLMService.get_openai_client(api_key, provider)
# 加载系统提示词
from ..config_manager import config_manager
system_prompts = config_manager.get_system_prompts()
if not system_prompts or 'translate_prompts' not in system_prompts or 'ZH' not in system_prompts['translate_prompts']:
result_container['result'] = {
"success": False,
"error": "翻译系统提示词加载失败"
}
# 不在这里发送toast通知,由主函数统一处理
return
system_message = system_prompts['translate_prompts']['ZH']
# 动态替换提示词
lang_map = {'zh': '中文', 'en': '英文', 'auto': '原文'}
src_lang = lang_map.get(from_lang, from_lang)
dst_lang = lang_map.get(to_lang, to_lang)
sys_msg_content = system_message['content'].replace('{src_lang}', src_lang).replace('{dst_lang}', dst_lang)
sys_msg = {"role": "system", "content": sys_msg_content}
# 设置输出语言
lang_message = {"role": "system", "content": "Please answer in English."} if to_lang == 'en' else {"role": "system", "content": "请用中文回答"}
# 构建消息
messages = [
lang_message,
sys_msg,
{"role": "user", "content": text}
]
print(f"{PromptTranslate.LOG_PREFIX} 调用{provider}翻译API | 模型:{model}")
# 调用API
stream = loop.run_until_complete(client.chat.completions.create(
model=model,
messages=[{"role": m["role"], "content": m["content"]} for m in messages],
temperature=0.5,
top_p=0.5,
max_tokens=1500,
stream=True,
response_format={"type": "text"}
))
full_content = ""
async def process_stream():
nonlocal full_content
async for chunk in stream:
if chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
full_content += content
loop.run_until_complete(process_stream())
result_container['result'] = {
"success": True,
"data": {
"from": from_lang,
"to": to_lang,
"original": text,
"translated": full_content
}
}
print(f"{PromptTranslate.LOG_PREFIX} 翻译完成 | 服务:{provider} | 结果字符数:{len(full_content)}")
except Exception as e:
# 格式化错误信息
provider_display_name = next((k for k, v in {"智谱翻译": "zhipu", "硅基流动翻译": "siliconflow", "自定义": "custom"}.items() if v == provider), provider)
error_message = format_api_error(e, provider_display_name)
result_container['result'] = {
"success": False,
"error": error_message
}
print(f"{PromptTranslate.LOG_PREFIX} 翻译失败 | 服务:{provider} | 错误:{error_message}")
# 不在这里发送toast通知,由主函数统一处理
finally:
loop.close()
# 节点映射,用于向ComfyUI注册节点
NODE_CLASS_MAPPINGS = {
"PromptTranslate": PromptTranslate,
}
# 节点显示名称映射
NODE_DISPLAY_NAME_MAPPINGS = {
"PromptTranslate": "✨提示词翻译",
}