mirror of
https://github.com/zhayujie/chatgpt-on-wechat.git
synced 2026-06-02 00:57:41 +08:00
feat(translate): add Youdao as a new translation provider
The translate module previously only supported Baidu translation, and the factory raised a bare RuntimeError for any other type. This change adds Youdao Translation as a second provider and improves the factory's error message. Implementation details: - New YoudaoTranslator class in translate/youdao/youdao_translate.py - Implements Youdao's v3 SHA-256 signature scheme, including the truncate-input rule for queries longer than 20 characters - Maps ISO 639-1 language codes to Youdao-specific codes (zh -> zh-CHS, zh-TW -> zh-CHT, others pass through) - Differentiates network errors, API error codes, and empty translations - factory.create_translator now lists the supported types in its RuntimeError message instead of failing silently - Default config exposes youdao_translate_app_key and youdao_translate_app_secret Adds 17 unit tests covering signature correctness, language code mapping, input truncation edge cases, the full request/response flow, and factory dispatch. All tests pass under Python 3.11.
This commit is contained in:
110
translate/youdao/youdao_translate.py
Normal file
110
translate/youdao/youdao_translate.py
Normal file
@@ -0,0 +1,110 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Youdao translator implementation.
|
||||
|
||||
Youdao Translation API v3 documentation:
|
||||
https://ai.youdao.com/DOCSIRMA/html/trans/api/wbfy/index.html
|
||||
|
||||
Configuration keys (in config.json):
|
||||
youdao_translate_app_key: Application key from Youdao AI platform.
|
||||
youdao_translate_app_secret: Application secret from Youdao AI platform.
|
||||
"""
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from hashlib import sha256
|
||||
|
||||
import requests
|
||||
|
||||
from config import conf
|
||||
from translate.translator import Translator
|
||||
|
||||
|
||||
class YoudaoTranslator(Translator):
|
||||
"""Youdao translator using the v3 signature scheme."""
|
||||
|
||||
API_URL = "https://openapi.youdao.com/api"
|
||||
|
||||
# Mapping from ISO 639-1 codes (used by the Translator interface)
|
||||
# to Youdao-specific language codes.
|
||||
# Reference: https://ai.youdao.com/DOCSIRMA/html/trans/api/wbfy/index.html
|
||||
LANG_CODE_MAP = {
|
||||
"": "auto",
|
||||
"auto": "auto",
|
||||
"zh": "zh-CHS",
|
||||
"zh-CN": "zh-CHS",
|
||||
"zh-TW": "zh-CHT",
|
||||
"yue": "yue", # Cantonese
|
||||
}
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.app_key = conf().get("youdao_translate_app_key")
|
||||
self.app_secret = conf().get("youdao_translate_app_secret")
|
||||
if not self.app_key or not self.app_secret:
|
||||
raise Exception("youdao translate app_key or app_secret not set")
|
||||
|
||||
def translate(self, query: str, from_lang: str = "", to_lang: str = "en") -> str:
|
||||
if not query:
|
||||
return ""
|
||||
|
||||
from_lang_code = self._convert_lang(from_lang) or "auto"
|
||||
to_lang_code = self._convert_lang(to_lang) or "en"
|
||||
|
||||
salt = str(uuid.uuid4())
|
||||
curtime = str(int(time.time()))
|
||||
sign = self._build_sign(query, salt, curtime)
|
||||
|
||||
payload = {
|
||||
"q": query,
|
||||
"from": from_lang_code,
|
||||
"to": to_lang_code,
|
||||
"appKey": self.app_key,
|
||||
"salt": salt,
|
||||
"sign": sign,
|
||||
"signType": "v3",
|
||||
"curtime": curtime,
|
||||
}
|
||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||
|
||||
response = requests.post(self.API_URL, data=payload, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
error_code = result.get("errorCode", "0")
|
||||
if error_code != "0":
|
||||
raise Exception(
|
||||
"youdao translate error: code={}, msg={}".format(
|
||||
error_code, result.get("msg", "")
|
||||
)
|
||||
)
|
||||
|
||||
translations = result.get("translation") or []
|
||||
if not translations:
|
||||
raise Exception("youdao translate returned empty translation")
|
||||
return "\n".join(translations)
|
||||
|
||||
def _build_sign(self, query: str, salt: str, curtime: str) -> str:
|
||||
"""
|
||||
Build the v3 signature.
|
||||
|
||||
sign = sha256(appKey + input + salt + curtime + appSecret),
|
||||
where input = q if len(q) <= 20 else q[:10] + str(len(q)) + q[-10:].
|
||||
"""
|
||||
input_str = self._truncate_input(query)
|
||||
sign_str = self.app_key + input_str + salt + curtime + self.app_secret
|
||||
return sha256(sign_str.encode("utf-8")).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def _truncate_input(query: str) -> str:
|
||||
length = len(query)
|
||||
if length <= 20:
|
||||
return query
|
||||
return query[:10] + str(length) + query[-10:]
|
||||
|
||||
@classmethod
|
||||
def _convert_lang(cls, lang: str) -> str:
|
||||
"""Convert ISO 639-1 language code to Youdao-specific code."""
|
||||
if lang is None:
|
||||
return "auto"
|
||||
return cls.LANG_CODE_MAP.get(lang, lang)
|
||||
Reference in New Issue
Block a user