262 lines
11 KiB
Python
262 lines
11 KiB
Python
import random
|
|
import httpx
|
|
from hashlib import md5
|
|
import json
|
|
import time
|
|
import re
|
|
from typing import Optional, Dict, Any, List
|
|
import asyncio
|
|
from ..utils.common import BAIDU_ERROR_CODE_MESSAGES, ProgressBar, log_complete, log_error, TASK_TRANSLATE
|
|
from .core import HTTPClientPool
|
|
|
|
class BaiduTranslateService:
|
|
@staticmethod
|
|
def split_text_by_paragraphs(text, max_length=2000):
|
|
"""
|
|
按段落分割文本,处理长文本翻译
|
|
使用正则表达式匹配连续的换行符,与JavaScript版本保持一致
|
|
"""
|
|
if not text:
|
|
return []
|
|
|
|
# 保留原始的换行符,用于后续恢复格式
|
|
# 先将文本按换行符分割为行
|
|
lines = text.split('\n')
|
|
chunks = []
|
|
current_chunk = ""
|
|
|
|
for line in lines:
|
|
# 如果当前行本身超过最大长度,需要再次分割
|
|
if len(line) > max_length:
|
|
# 如果current_chunk不为空,先添加到chunks
|
|
if current_chunk:
|
|
chunks.append(current_chunk)
|
|
current_chunk = ""
|
|
|
|
# 分割长行
|
|
remaining_text = line
|
|
while len(remaining_text) > 0:
|
|
chunk_text = remaining_text[:max_length]
|
|
chunks.append(chunk_text)
|
|
remaining_text = remaining_text[max_length:]
|
|
# 如果添加当前行会超出长度限制,先保存当前chunk
|
|
elif current_chunk and (len(current_chunk) + len(line) + 1 > max_length):
|
|
chunks.append(current_chunk)
|
|
current_chunk = line
|
|
# 否则,添加到当前chunk
|
|
else:
|
|
if current_chunk:
|
|
current_chunk += "\n" + line
|
|
else:
|
|
current_chunk = line
|
|
|
|
# 添加最后一个chunk
|
|
if current_chunk:
|
|
chunks.append(current_chunk)
|
|
|
|
return chunks
|
|
|
|
@staticmethod
|
|
async def translate_chunk(client, chunk, app_id, secret_key, from_lang, to_lang, retry_count=1):
|
|
"""
|
|
异步翻译单个文本块,带重试机制
|
|
"""
|
|
for attempt in range(retry_count):
|
|
try:
|
|
# 生成签名
|
|
salt = random.randint(32768, 65536)
|
|
sign = md5((app_id + chunk + str(salt) + secret_key).encode('utf-8')).hexdigest()
|
|
|
|
# 构建请求
|
|
url = 'https://fanyi-api.baidu.com/api/trans/vip/translate'
|
|
params = {
|
|
'q': chunk,
|
|
'from': from_lang,
|
|
'to': to_lang,
|
|
'appid': app_id,
|
|
'salt': salt,
|
|
'sign': sign
|
|
}
|
|
|
|
# 发送异步请求
|
|
response = await client.post(url, data=params)
|
|
|
|
if response.status_code != 200:
|
|
if attempt < retry_count - 1:
|
|
await asyncio.sleep(1)
|
|
continue
|
|
raise Exception(f"百度: HTTP请求失败,状态码: {response.status_code}")
|
|
|
|
result = response.json()
|
|
|
|
# 检查错误
|
|
if 'error_code' in result:
|
|
error_code = result['error_code']
|
|
error_message = BAIDU_ERROR_CODE_MESSAGES.get(
|
|
error_code,
|
|
f"未知错误(错误码:{error_code})"
|
|
)
|
|
|
|
# 某些错误码可以重试
|
|
if error_code in ['54003', '52001', '52002'] and attempt < retry_count - 1:
|
|
await asyncio.sleep(1)
|
|
continue
|
|
|
|
raise Exception(f"百度: {error_message}")
|
|
|
|
# 处理翻译结果
|
|
if 'trans_result' in result and result['trans_result']:
|
|
translated_parts = [item['dst'] for item in result['trans_result']]
|
|
return '\n'.join(translated_parts)
|
|
else:
|
|
raise Exception("百度: 翻译结果为空")
|
|
|
|
except (httpx.HTTPError, asyncio.TimeoutError) as e:
|
|
if attempt < retry_count - 1:
|
|
from ..utils.common import WARN_PREFIX
|
|
print(f"\r{WARN_PREFIX} 百度翻译请求遇到网络错误,尝试重试 ({attempt+1}/{retry_count}): {e}")
|
|
await asyncio.sleep(1)
|
|
else:
|
|
raise Exception(f"百度: 网络请求失败,请检查网络连接或稍后再试 ({type(e).__name__})")
|
|
except Exception as e:
|
|
if attempt < retry_count - 1:
|
|
await asyncio.sleep(1)
|
|
else:
|
|
if str(e).startswith("百度:"):
|
|
raise e
|
|
else:
|
|
raise Exception(f"百度: 翻译过程中发生未知错误 ({type(e).__name__})")
|
|
|
|
raise Exception("百度: 超过最大重试次数")
|
|
|
|
@staticmethod
|
|
async def translate(text, from_lang='auto', to_lang='zh', request_id=None, is_auto=False, cancel_event=None, task_type=None, source=None):
|
|
"""
|
|
异步调用百度翻译API进行翻译
|
|
|
|
参数:
|
|
task_type: 任务类型,用于统一日志输出
|
|
cancel_event: 中断事件(保持接口一致性,百度翻译暂不支持中断)
|
|
"""
|
|
try:
|
|
request_id = request_id or f"baidu_trans_{int(time.time())}_{random.randint(1000, 9999)}"
|
|
|
|
if not text or text.strip() == '':
|
|
return {"success": False, "error": "百度: 待翻译文本不能为空"}
|
|
|
|
from ..config_manager import config_manager
|
|
config = config_manager.get_baidu_translate_config()
|
|
|
|
app_id = config.get('app_id')
|
|
secret_key = config.get('secret_key')
|
|
|
|
if not app_id or not secret_key:
|
|
return {"success": False, "error": "百度: 请先配置百度翻译API的APP_ID和SECRET_KEY"}
|
|
|
|
# 确定任务类型
|
|
task_type = task_type or TASK_TRANSLATE
|
|
|
|
text_chunks = BaiduTranslateService.split_text_by_paragraphs(text)
|
|
if not text_chunks:
|
|
text_chunks = [text]
|
|
|
|
translated_parts = []
|
|
|
|
# 获取HTTP客户端(复用连接池,不使用代理)
|
|
# 注意:百度API通常在国内直连更快,所以不配置proxy参数
|
|
client = HTTPClientPool.get_client(
|
|
provider="baidu_translate",
|
|
base_url="https://fanyi-api.baidu.com",
|
|
timeout=10.0
|
|
)
|
|
|
|
# 由于HTTPClientPool.get_client返回的是AsyncClient实例,我们可以直接使用
|
|
# 但为了确保不使用系统代理(模拟 trust_env=False),我们需要在get_client中支持或在这里手动创建
|
|
# 考虑到HTTPClientPool目前的设计,我们直接手动创建一个不使用代理的客户端可能更稳妥
|
|
# 或者修改HTTPClientPool支持 trust_env=False。
|
|
# 为了保持统一,我们使用HTTPClientPool,但如果需要强制直连,我们可以传递特定的代理配置
|
|
|
|
# 实际上,HTTPClientPool.get_client 如果没有传proxy,会使用系统代理。
|
|
# 百度翻译之前的实现是 trust_env=False (禁用系统代理)。
|
|
# 为了保持一致,我们这里手动创建一个专用的客户端,或者修改HTTPClientPool。
|
|
# 考虑到百度翻译的特殊性(国内直连),手动创建一个简单的客户端是最安全的迁移方式。
|
|
|
|
# 创建统一进度条
|
|
from ..server import is_streaming_progress_enabled
|
|
pbar = ProgressBar(
|
|
request_id=request_id,
|
|
service_name="百度翻译",
|
|
streaming=is_streaming_progress_enabled(),
|
|
extra_info=f"长度:{len(text)}",
|
|
task_type=task_type,
|
|
source=source
|
|
)
|
|
|
|
start_time = time.perf_counter()
|
|
|
|
async with httpx.AsyncClient(trust_env=False, verify=False, timeout=10.0) as client:
|
|
for i, chunk in enumerate(text_chunks):
|
|
# ---中断监控---
|
|
is_interrupted = False
|
|
if cancel_event is not None and cancel_event.is_set():
|
|
is_interrupted = True
|
|
else:
|
|
try:
|
|
from server import PromptServer
|
|
if hasattr(PromptServer.instance, 'execution_interrupted') and PromptServer.instance.execution_interrupted:
|
|
is_interrupted = True
|
|
except: pass
|
|
|
|
if is_interrupted:
|
|
pbar.cancel(f"{WARN_PREFIX} 任务被中断 | 服务:百度翻译")
|
|
return {"success": False, "error": "任务被中断", "interrupted": True}
|
|
# ------------
|
|
|
|
try:
|
|
chunk_translation = await BaiduTranslateService.translate_chunk(
|
|
client, chunk, app_id, secret_key, from_lang, to_lang
|
|
)
|
|
translated_parts.append(chunk_translation)
|
|
|
|
if i < len(text_chunks) - 1:
|
|
await asyncio.sleep(1)
|
|
|
|
except Exception as chunk_error:
|
|
# 输出错误日志
|
|
pbar.error(str(chunk_error))
|
|
return {"success": False, "error": str(chunk_error)}
|
|
|
|
translated_text = '\n'.join(translated_parts)
|
|
# 完成阶段
|
|
elapsed = int((time.perf_counter() - start_time) * 1000)
|
|
pbar.done(char_count=len(translated_text), elapsed_ms=elapsed)
|
|
|
|
return {
|
|
"success": True,
|
|
"data": {
|
|
"translated": translated_text,
|
|
"from": from_lang,
|
|
"to": to_lang,
|
|
"original": text
|
|
}
|
|
}
|
|
|
|
# 关键修复:单独捕获外层 CancelledError,确保 pbar 被正确停止
|
|
except asyncio.CancelledError:
|
|
if 'pbar' in locals() and pbar:
|
|
pbar.cancel(f"{WARN_PREFIX} 任务被外部取消 | 服务:百度翻译")
|
|
return {"success": False, "error": "任务被取消", "interrupted": True}
|
|
|
|
except Exception as e:
|
|
# 输出错误日志
|
|
if 'pbar' in locals() and pbar:
|
|
pbar.error(str(e))
|
|
return {"success": False, "error": str(e)}
|
|
|
|
@staticmethod
|
|
async def batch_translate(texts, from_lang='auto', to_lang='zh'):
|
|
"""
|
|
异步批量翻译文本
|
|
"""
|
|
tasks = [BaiduTranslateService.translate(text, from_lang, to_lang) for text in texts]
|
|
return await asyncio.gather(*tasks) |