mirror of
https://github.com/xfgryujk/blivechat.git
synced 2026-08-19 09:43:28 +08:00
添加腾讯翻译官方接口
This commit is contained in:
10
config.py
10
config.py
@@ -78,6 +78,16 @@ class AppConfig:
|
||||
if type_ == 'TencentTranslateFree':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
elif type_ == 'BilibiliTranslateFree':
|
||||
pass
|
||||
elif type_ == 'TencentTranslate':
|
||||
translator_config['source_language'] = section['source_language']
|
||||
translator_config['target_language'] = section['target_language']
|
||||
translator_config['secret_id'] = section['secret_id']
|
||||
translator_config['secret_key'] = section['secret_key']
|
||||
translator_config['region'] = section['region']
|
||||
else:
|
||||
raise ValueError(f'Invalid translator type: {type_}')
|
||||
|
||||
translator_configs.append(translator_config)
|
||||
self.translator_configs = translator_configs
|
||||
|
||||
@@ -25,19 +25,23 @@ loader_url = https://xfgryujk.sinacloud.net/blivechat/loader.html
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
# 以下是给字幕组看的,实在懒得翻译了_(:з」∠)_。如果你不了解以下参数的意思,使用默认值就好
|
||||
# **The following is for translation team. Leave it default if you don't know its meaning**
|
||||
# -------------------------------------------------------------------------------------------------
|
||||
|
||||
# 翻译器配置,索引到下面的配置节。可以配置多个翻译器,翻译时会使用当前第一个可用的翻译器
|
||||
# 配置多个翻译器可以增加QPS、容灾。不同配置可以使用同一个类型,但要使用不同的账号,否则还是会遇到调用频率限制
|
||||
# 翻译器配置,索引到下面的配置节。可以以逗号分隔配置多个翻译器,翻译时会使用当前第一个可用的翻译器
|
||||
# 配置多个翻译器可以增加额度、增加QPS、容灾
|
||||
# 不同配置可以使用同一个类型,但要使用不同的账号,否则还是会遇到额度、调用频率限制
|
||||
translator_configs = tencent_translate_free,bilibili_translate_free
|
||||
|
||||
|
||||
[tencent_translate_free]
|
||||
# 类型:腾讯翻译白嫖版。使用了网页版的接口,**将来可能失效**
|
||||
type = TencentTranslateFree
|
||||
|
||||
# 请求间隔时间(秒),等于 1 / QPS。目前没有遇到此接口有调用频率限制,10QPS应该够用了
|
||||
query_interval = 0.1
|
||||
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
|
||||
max_queue_size = 100
|
||||
|
||||
# 自动:auto;中文:zh;日语:jp;英语:en;韩语:kr
|
||||
# 完整语言列表见文档:https://cloud.tencent.com/document/product/551/15619
|
||||
# 源语言
|
||||
@@ -49,7 +53,42 @@ target_language = jp
|
||||
[bilibili_translate_free]
|
||||
# 类型:B站翻译白嫖版。使用了B站直播网页的接口,**将来可能失效**。目前B站翻译后端是百度翻译
|
||||
type = BilibiliTranslateFree
|
||||
|
||||
# 请求间隔时间(秒),等于 1 / QPS。目前此接口频率限制是3秒一次
|
||||
query_interval = 3.1
|
||||
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
|
||||
max_queue_size = 3
|
||||
|
||||
|
||||
[tencent_translate]
|
||||
# 文档:https://cloud.tencent.com/product/tmt
|
||||
# 定价:https://cloud.tencent.com/document/product/551/35017
|
||||
# * 文本翻译的每月免费额度为5百万字符
|
||||
# * 文本翻译当月需付费字符数小于100百万字符(1亿字符)时,刊例价为58元/每百万字符
|
||||
# * 文本翻译当月需付费字符数大于等于100百万字符(1亿字符)时,刊例价为50元/每百万字符
|
||||
# 限制:https://cloud.tencent.com/document/product/551/32572
|
||||
# * 文本翻译最高QPS为5
|
||||
|
||||
# 类型:腾讯翻译
|
||||
type = TencentTranslate
|
||||
|
||||
# 请求间隔时间(秒),等于 1 / QPS。理论上最高QPS为5,实际测试是3
|
||||
query_interval = 0.333
|
||||
# 最大队列长度,注意最长等待时间等于 最大队列长度 * 请求间隔时间
|
||||
max_queue_size = 30
|
||||
|
||||
# 自动:auto;中文:zh;日语:jp;英语:en;韩语:kr
|
||||
# 完整语言列表见文档:https://cloud.tencent.com/document/product/551/15619
|
||||
# 源语言
|
||||
source_language = zh
|
||||
# 目标语言
|
||||
target_language = jp
|
||||
|
||||
# 腾讯云API密钥
|
||||
secret_id =
|
||||
secret_key =
|
||||
|
||||
# 腾讯云地域参数,用来标识希望操作哪个地域的数据
|
||||
# 北京:ap-beijing;上海:ap-shanghai;香港:ap-hongkong;首尔:ap-seoul
|
||||
# 完整地域列表见文档:https://cloud.tencent.com/document/api/551/15615#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8
|
||||
region = ap-shanghai
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import functools
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import *
|
||||
@@ -32,19 +36,11 @@ def init():
|
||||
|
||||
async def _do_init():
|
||||
cfg = config.get_config()
|
||||
if not cfg.enable_translate:
|
||||
return
|
||||
providers = []
|
||||
for trans_cfg in cfg.translator_configs:
|
||||
provider = None
|
||||
type_ = trans_cfg['type']
|
||||
|
||||
if type_ == 'TencentTranslateFree':
|
||||
provider = TencentTranslateFree(
|
||||
trans_cfg['query_interval'], trans_cfg['max_queue_size'], trans_cfg['source_language'],
|
||||
trans_cfg['target_language']
|
||||
)
|
||||
elif type_ == 'BilibiliTranslateFree':
|
||||
provider = BilibiliTranslateFree(trans_cfg['query_interval'], trans_cfg['max_queue_size'])
|
||||
|
||||
provider = create_translate_provider(trans_cfg)
|
||||
if provider is not None:
|
||||
providers.append(provider)
|
||||
await asyncio.gather(*(provider.init() for provider in providers))
|
||||
@@ -52,6 +48,24 @@ async def _do_init():
|
||||
_translate_providers = providers
|
||||
|
||||
|
||||
def create_translate_provider(cfg):
|
||||
type_ = cfg['type']
|
||||
if type_ == 'TencentTranslateFree':
|
||||
return TencentTranslateFree(
|
||||
cfg['query_interval'], cfg['max_queue_size'], cfg['source_language'],
|
||||
cfg['target_language']
|
||||
)
|
||||
elif type_ == 'BilibiliTranslateFree':
|
||||
return BilibiliTranslateFree(cfg['query_interval'], cfg['max_queue_size'])
|
||||
elif type_ == 'TencentTranslate':
|
||||
return TencentTranslate(
|
||||
cfg['query_interval'], cfg['max_queue_size'], cfg['source_language'],
|
||||
cfg['target_language'], cfg['secret_id'], cfg['secret_key'],
|
||||
cfg['region']
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def need_translate(text):
|
||||
text = text.strip()
|
||||
# 没有中文,平时打不出的字不管
|
||||
@@ -321,3 +335,112 @@ class BilibiliTranslateFree(FlowControlTranslateProvider):
|
||||
logger.warning('BilibiliTranslateFree failed: %d %s', data['code'], data['msg'])
|
||||
return None
|
||||
return data['data']['message_trans']
|
||||
|
||||
|
||||
class TencentTranslate(FlowControlTranslateProvider):
|
||||
def __init__(self, query_interval, max_queue_size, source_language, target_language,
|
||||
secret_id, secret_key, region):
|
||||
super().__init__(query_interval, max_queue_size)
|
||||
self._source_language = source_language
|
||||
self._target_language = target_language
|
||||
self._secret_id = secret_id
|
||||
self._secret_key = secret_key
|
||||
self._region = region
|
||||
|
||||
self._cool_down_timer_handle = None
|
||||
|
||||
@property
|
||||
def is_available(self):
|
||||
return self._cool_down_timer_handle is None and super().is_available
|
||||
|
||||
async def _do_translate(self, text):
|
||||
try:
|
||||
async with self._request_tencent_cloud(
|
||||
'TextTranslate',
|
||||
'2018-03-21',
|
||||
{
|
||||
'SourceText': text,
|
||||
'Source': self._source_language,
|
||||
'Target': self._target_language,
|
||||
'ProjectId': 0
|
||||
}
|
||||
) as r:
|
||||
if r.status != 200:
|
||||
logger.warning('TencentTranslate request failed: status=%d %s', r.status, r.reason)
|
||||
return None
|
||||
data = (await r.json())['Response']
|
||||
except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
|
||||
return None
|
||||
error = data.get('Error', None)
|
||||
if error is not None:
|
||||
logger.warning('TencentTranslate failed: %s %s, RequestId=%s', error['Code'],
|
||||
error['Message'], data['RequestId'])
|
||||
self._on_fail(error['Code'])
|
||||
return None
|
||||
return data['TargetText']
|
||||
|
||||
def _request_tencent_cloud(self, action, version, body):
|
||||
body_bytes = json.dumps(body).encode('utf-8')
|
||||
|
||||
canonical_headers = 'content-type:application/json; charset=utf-8\nhost:tmt.tencentcloudapi.com\n'
|
||||
signed_headers = 'content-type;host'
|
||||
hashed_request_payload = hashlib.sha256(body_bytes).hexdigest()
|
||||
canonical_request = f'POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_request_payload}'
|
||||
|
||||
request_timestamp = int(datetime.datetime.now().timestamp())
|
||||
date = datetime.datetime.utcfromtimestamp(request_timestamp).strftime('%Y-%m-%d')
|
||||
credential_scope = f'{date}/tmt/tc3_request'
|
||||
hashed_canonical_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
|
||||
string_to_sign = f'TC3-HMAC-SHA256\n{request_timestamp}\n{credential_scope}\n{hashed_canonical_request}'
|
||||
|
||||
def sign(key, msg):
|
||||
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
|
||||
|
||||
secret_date = sign(('TC3' + self._secret_key).encode('utf-8'), date)
|
||||
secret_service = sign(secret_date, 'tmt')
|
||||
secret_signing = sign(secret_service, 'tc3_request')
|
||||
signature = hmac.new(secret_signing, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
|
||||
|
||||
authorization = (
|
||||
f'TC3-HMAC-SHA256 Credential={self._secret_id}/{credential_scope}, '
|
||||
f'SignedHeaders={signed_headers}, Signature={signature}'
|
||||
)
|
||||
|
||||
headers = {
|
||||
'Authorization': authorization,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'X-TC-Action': action,
|
||||
'X-TC-Version': version,
|
||||
'X-TC-Timestamp': str(request_timestamp),
|
||||
'X-TC-Region': self._region
|
||||
}
|
||||
|
||||
return _http_session.post('https://tmt.tencentcloudapi.com/', headers=headers, data=body_bytes)
|
||||
|
||||
def _on_fail(self, code):
|
||||
if self._cool_down_timer_handle is not None:
|
||||
return
|
||||
|
||||
sleep_time = 0
|
||||
if code == 'FailedOperation.NoFreeAmount':
|
||||
# 下个月恢复免费额度
|
||||
cur_time = datetime.datetime.now()
|
||||
year = cur_time.year
|
||||
month = cur_time.month + 1
|
||||
if month > 12:
|
||||
year += 1
|
||||
month = 1
|
||||
next_month_time = datetime.datetime(year, month, 1, minute=5)
|
||||
sleep_time = (next_month_time - cur_time).total_seconds()
|
||||
# Python 3.8之前不能超过一天
|
||||
sleep_time = min(sleep_time, 24 * 60 * 60 - 1)
|
||||
elif code in ('FailedOperation.ServiceIsolate', 'LimitExceeded'):
|
||||
# 需要手动处理,等5分钟
|
||||
sleep_time = 5 * 60
|
||||
if sleep_time != 0:
|
||||
self._cool_down_timer_handle = asyncio.get_event_loop().call_later(
|
||||
sleep_time, self._on_cool_down_timeout
|
||||
)
|
||||
|
||||
def _on_cool_down_timeout(self):
|
||||
self._cool_down_timer_handle = None
|
||||
|
||||
Reference in New Issue
Block a user