Files
2026-04-25 10:38:46 +08:00

318 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
提示词翻译节点 - V3 版本
V3 迁移说明:
- 继承 LLMNodeBase(工具基类 Mixin)+ io.ComfyNode(V3 节点基类)
- INPUT_TYPES → define_schema(),返回 io.Schema
- IS_CHANGED → fingerprint_inputs()
- def translate(self, ...) → @classmethod execute(cls, ...)
- 原辅助实例方法全部转换为 @classmethod
- 返回 io.NodeOutput(val) 代替 (val,)
- hidden unique_id 通过 cls.hidden.unique_id 访问
- 不再导出 NODE_CLASS_MAPPINGS,由顶层 __init__.py 的 ComfyExtension 统一注册
"""
import hashlib
import re
from comfy.model_management import InterruptProcessingException
from comfy_api.latest import io
from ..services.llm import LLMService
from ..services.baidu import BaiduTranslateService
from ..utils.common import (
format_api_error, format_model_with_thinking, generate_request_id,
log_prepare, log_error, TASK_TRANSLATE, SOURCE_NODE
)
from ..services.thinking_control import build_thinking_suppression
from .base import LLMNodeBase
class PromptTranslate(LLMNodeBase, io.ComfyNode):
"""
提示词翻译节点(V3)
自动识别输入语言并翻译成目标语言,支持多种翻译服务
"""
@classmethod
def define_schema(cls):
"""定义节点 Schema(V3 替代 INPUT_TYPES + 类属性)"""
# ---动态获取翻译服务/模型列表(含硬编码的百度翻译)---
service_options = cls.get_translate_service_options()
default_service = service_options[0] if service_options else "百度翻译"
return io.Schema(
node_id="PromptTranslate",
display_name="✨Prompt Translate",
category="✨Prompt Assistant",
description="Auto-detect input language and translate to target language",
inputs=[
io.String.Input(
"source_text",
force_input=True,
default="",
multiline=True,
placeholder="Input text to translate...",
tooltip="Text to translate",
),
io.Combo.Input(
"target_language",
options=["English", "Chinese"],
default="English",
),
io.Combo.Input(
"translate_service",
options=service_options,
default=default_service,
tooltip="Select translation service and model",
),
# Ollama 自动释放显存
io.Boolean.Input(
"ollama_auto_unload",
default=True,
label_on="Enable",
label_off="Disable",
tooltip="Auto unload Ollama model after generation",
),
io.Int.Input(
"seed",
default=0,
min=0,
max=0xffffffffffffffff,
control_after_generate=True,
),
],
outputs=[
io.String.Output("translated_text"),
],
hidden=[io.Hidden.unique_id],
)
@classmethod
def fingerprint_inputs(
cls,
source_text=None, target_language=None, translate_service=None,
ollama_auto_unload=None, seed=None
):
"""
替代 V1 IS_CHANGED,只在输入内容真正变化时才触发重新执行
使用输入参数的哈希值作为判断依据
"""
text_hash = ""
if source_text:
text_hash = hashlib.md5(source_text.encode('utf-8')).hexdigest()
input_hash = hash((
text_hash,
target_language,
translate_service,
bool(ollama_auto_unload),
seed
))
return input_hash
@classmethod
def _contains_chinese(cls, text: str) -> bool:
"""检查文本是否包含中文字符"""
if not text:
return False
return bool(re.search('[\u4e00-\u9fa5]', text))
@classmethod
def _detect_language(cls, text: str) -> str:
"""自动检测文本语言"""
if not text:
return "auto"
# 检查是否为纯英文(只包含 ASCII 可打印字符)
is_pure_english = bool(re.fullmatch(r'[ -~]+', text))
# 检查是否包含中文字符
contains_chinese = cls._contains_chinese(text)
if contains_chinese:
return "zh"
elif is_pure_english:
return "en"
else:
return "auto"
@classmethod
def _translate_with_baidu(cls, text, from_lang, to_lang, service_name, from_lang_name, to_lang_name, unique_id):
"""使用百度翻译服务"""
# 创建请求 ID
request_id = generate_request_id("trans", "baidu", unique_id)
# 准备阶段日志
log_prepare(TASK_TRANSLATE, request_id, SOURCE_NODE, "百度翻译", None, None, {"方向": f"{from_lang_name}→{to_lang_name}", "长度": len(text)})
# 执行翻译(异步线程 + 可中断)
result = cls._run_llm_task(
BaiduTranslateService.translate,
service_name,
text=text,
from_lang=from_lang,
to_lang=to_lang,
request_id=request_id,
task_type=TASK_TRANSLATE,
source=SOURCE_NODE
)
return request_id, result
@classmethod
def _translate_with_llm(cls, text, from_lang, to_lang, service_id, model_name, service, service_display_name, from_lang_name, to_lang_name, auto_unload, unique_id):
"""使用 LLM 翻译服务"""
# ---构建 provider_config---
llm_models = service.get('llm_models', [])
target_model = None
if model_name:
target_model = next((m for m in llm_models if m.get('name') == model_name), None)
if not target_model:
target_model = next(
(m for m in llm_models if m.get('is_default')),
llm_models[0] if llm_models else None
)
if not target_model:
return None, {"success": False, "error": f"Service {service_display_name} has no available models"}
provider_config = {
'provider': service_id,
'model': target_model.get('name', ''),
'base_url': service.get('base_url', ''),
'api_key': service.get('api_key', ''),
'temperature': target_model.get('temperature', 0.7),
'max_tokens': target_model.get('max_tokens', 1000),
'top_p': target_model.get('top_p', 0.9),
}
# Ollama 特殊处理:添加 auto_unload 配置
if service.get('type') == 'ollama':
provider_config['auto_unload'] = auto_unload
# 创建请求 ID
request_id = generate_request_id("trans", "llm", unique_id)
# 检查是否关闭思维链
model_full_name = provider_config.get('model')
disable_thinking_enabled = service.get('disable_thinking', True)
thinking_extra = build_thinking_suppression(service_id, model_full_name) if disable_thinking_enabled else None
model_display = format_model_with_thinking(model_full_name, bool(thinking_extra))
# 获取服务显示名称
service_display_name = service.get('name', service_id)
# 准备阶段日志
log_prepare(TASK_TRANSLATE, request_id, SOURCE_NODE, service_display_name, model_display, None, {"方向": f"{from_lang_name}→{to_lang_name}", "长度": len(text)})
# 检查 API 密钥和模型
api_key = provider_config.get('api_key', '')
model = provider_config.get('model', '')
if not model:
return request_id, {"success": False, "error": f"Please configure model for {service_display_name}"}
if cls._service_requires_api_key(service) and not api_key:
return request_id, {"success": False, "error": f"Please configure API key and model for {service_display_name}"}
# 执行翻译(异步线程 + 可中断)
result = cls._run_llm_task(
LLMService.translate,
service_id,
text=text,
from_lang=from_lang,
to_lang=to_lang,
request_id=request_id,
stream_callback=None,
custom_provider=service_id,
custom_provider_config=provider_config,
task_type=TASK_TRANSLATE,
source=SOURCE_NODE
)
return request_id, result
@classmethod
def execute(cls, source_text, target_language, translate_service, ollama_auto_unload, seed=None):
"""
翻译文本函数(V3 classmethod 版本)
通过 cls.hidden.unique_id 访问节点唯一 ID
"""
# 从 cls.hidden 获取节点唯一 ID
unique_id = cls.hidden.unique_id
request_id = None
try:
# 检查输入
if not source_text or not source_text.strip():
return io.NodeOutput("")
# 自动检测源语言
detected_lang = cls._detect_language(source_text)
to_lang = "en" if target_language == "English" else "zh"
# 智能跳过翻译逻辑
skip_translation = False
if to_lang == 'en' and detected_lang == 'en':
from ..utils.common import _ANSI_CLEAR_EOL
print(f"\r{_ANSI_CLEAR_EOL}{cls.REQUEST_PREFIX} 检测到英文输入,目标为英文,无需翻译", flush=True)
skip_translation = True
elif to_lang == 'zh' and detected_lang == 'zh':
from ..utils.common import _ANSI_CLEAR_EOL
print(f"\r{_ANSI_CLEAR_EOL}{cls.REQUEST_PREFIX} 检测到中文输入,目标为中文,无需翻译", flush=True)
skip_translation = True
if skip_translation:
return io.NodeOutput(source_text)
# 映射语言名称
lang_map = {'zh': '中文', 'en': '英文', 'auto': '原文'}
from_lang_name = lang_map.get(detected_lang, detected_lang)
to_lang_name = lang_map.get(to_lang, to_lang)
# ---解析服务/模型字符串---
service_id, model_name = cls.parse_service_model(translate_service)
if not service_id:
raise ValueError(f"Invalid service selection: {translate_service}")
# ---百度翻译特殊处理---
if service_id == 'baidu':
request_id, result = cls._translate_with_baidu(
source_text, detected_lang, to_lang,
translate_service, from_lang_name, to_lang_name, unique_id
)
else:
# ---LLM 翻译:获取服务配置---
from ..config_manager import config_manager
service = config_manager.get_service(service_id)
if not service:
raise ValueError(f"Service config not found: {translate_service}")
request_id, result = cls._translate_with_llm(
source_text, detected_lang, to_lang,
service_id, model_name, service,
translate_service, from_lang_name, to_lang_name,
ollama_auto_unload, unique_id
)
if result and result.get('success'):
translated_text = result.get('data', {}).get('translated', '').strip()
if not translated_text:
error_msg = 'API returned empty result'
raise RuntimeError(f"❌Translation failed: {error_msg}")
# 结果阶段日志由服务层统一输出,节点层不再重复打印
return io.NodeOutput(translated_text)
else:
error_msg = result.get('error', 'Unknown error') if result else 'No result returned'
if error_msg == "任务被中断":
raise InterruptProcessingException()
log_error(TASK_TRANSLATE, request_id, error_msg)
raise RuntimeError(f"Translation failed: {error_msg}")
except InterruptProcessingException:
# 不打印日志,由基类统一打印
raise
except Exception as e:
error_msg = format_api_error(e, translate_service)
log_error(TASK_TRANSLATE, request_id, error_msg)
raise RuntimeError(f"Translation error: {error_msg}")