Gemini翻译改为OpenAI API兼容接口

This commit is contained in:
John Smith
2025-02-24 21:09:58 +08:00
parent 3b343464ae
commit 0065a47b78
4 changed files with 115 additions and 119 deletions

Submodule blivedm updated: 100ed8fdf9...b204c17cc2

View File

@@ -174,7 +174,6 @@ class AppConfig:
'%E7%BF%BB%E8%AF%91%E6%8E%A5%E5%8F%A3' '%E7%BF%BB%E8%AF%91%E6%8E%A5%E5%8F%A3'
) )
logger.warning('%s is deprecated, please see %s', type_, doc_url) logger.warning('%s is deprecated, please see %s', type_, doc_url)
continue
elif type_ == 'TencentTranslate': elif type_ == 'TencentTranslate':
translator_config['source_language'] = section['source_language'] translator_config['source_language'] = section['source_language']
translator_config['target_language'] = section['target_language'] translator_config['target_language'] = section['target_language']
@@ -187,11 +186,16 @@ class AppConfig:
translator_config['app_id'] = section['app_id'] translator_config['app_id'] = section['app_id']
translator_config['secret'] = section['secret'] translator_config['secret'] = section['secret']
elif type_ == 'GeminiTranslate': elif type_ == 'GeminiTranslate':
translator_config['proxy'] = section['proxy'] logger.warning('%s is deprecated, please migrate to OpenAiApi', type_)
elif type_ == 'OpenAiApi':
translator_config['api_key'] = section['api_key'] translator_config['api_key'] = section['api_key']
translator_config['model_code'] = section['model_code'] translator_config['base_url'] = section['base_url']
translator_config['proxy'] = section['proxy']
translator_config['model'] = section['model']
translator_config['prompt'] = section['prompt'].replace('\n', ' ').replace('\\n', '\n') translator_config['prompt'] = section['prompt'].replace('\n', ' ').replace('\\n', '\n')
translator_config['max_tokens'] = section.getint('max_tokens')
translator_config['temperature'] = section.getfloat('temperature') translator_config['temperature'] = section.getfloat('temperature')
translator_config['top_p'] = section.getfloat('top_p')
else: else:
raise ValueError(f'Invalid translator type: {type_}') raise ValueError(f'Invalid translator type: {type_}')
except Exception: # noqa except Exception: # noqa

View File

@@ -138,48 +138,36 @@ app_id =
secret = secret =
[gemini_translate] [openai_api]
# 文档https://ai.google.dev/docs # 大语言模型对话支持任何兼容OpenAI API的平台参考各平台的文档吧
# 定价https://ai.google.dev/pricing
# * 目前只有免费版最高QPS为1
# * 收费版 输入 $0.000125 / 1000 个字符;输出 $0.000375 / 1000 个字符
# 模型规格https://ai.google.dev/models/gemini
# * Gemini Pro 最高QPS为1
# 类型:Gemini翻译 # 类型:OpenAI API
type = GeminiTranslate type = OpenAiApi
# 请求间隔时间(秒),等于 1 / QPS # 请求间隔时间(秒),等于 1 / QPS
query_interval = 1.1 query_interval = 1.1
# 代理地址如果为空不使用代理。注意必须要在指定地区才能使用https://ai.google.dev/available_regions
proxy = http://127.0.0.1:7890
# API密钥 # API密钥
api_key = api_key =
# API端点“/chat/completions”之前的部分
base_url = https://api.deepseek.com/v1
# 代理地址,如果为空,不使用代理
# Example: proxy = http://127.0.0.1:7890
proxy =
# 模型代码 # 模型
model_code = models/gemini-1.0-pro model = deepseek-chat
# 提示词 # 提示词,换行符会替换为空格,“\n”会替换为换行符
prompt = As an advanced translation software, your role is to provide accurate translations that respect prompt = 你是一个日本主播的翻译助手,精通中日文翻译。
the original content's meaning and tone. 用户会输入直播观众的聊天内容,请翻译成日文,并确保符合日文语言习惯。
Ensure that texts between square brackets "[]" are kept untranslated. 你可以调整语气和风格,并考虑到某些词语的文化内涵和地区差异。
Aim to deliver the translated text in a polite and gentle tone, as per the preferences of the audience.\n\n 只回答翻译结果,不需要解释。
Below are some examples to guide you:\n\n # 最大输出token数
Input1:\n来拉(莱拉)设置一下吧\nOutput1:\nライラちゃん、設定してみましょうか\n\n max_tokens = 200
Input2:\n我来拉一下各位进队伍\nOutput2:\n皆さんをチームに入れさせていただきます\n\n # 采样温度,用于控制令牌选择的随机性。较低的温度适合需要更具确定性或不够开放的回答的提示,而较高的温度可以产生更加多样化或更具创意的结果
Input3:\n死了w\nOutput3:\n死んじゃったわw\n\n temperature = 1.3
Input4:\n每天来宝这里打个卡[比心]\nOutput4:\n毎日、ここに来て宝さんにチェックインしま[比心]\n\n # 核采样概率阈值温度采样的替代方案。模型会考虑概率质量在top_p内的token结果
\n top_p = 1.0
Please keep in mind that accurate translation is not just about linguistic conversion;
it is about conveying the same sentiment and meaning as the original input.
Pay special attention to internet vernaculars and colloquial expressions to maintain the nuances of the
source language.\n\n
Now, proceed with the translation of the following Chinese comment text into Japanese, ensuring accuracy
and preservation of the original message:\n
Input:\n{original_text}\nOutput ONLY:\n
# 温度,用于控制令牌选择的随机性,较低的温度适合需要更具确定性或不够开放的回答的提示,而较高的温度可以产生更加多样化或更具创意的结果
temperature = 0.4
# 傻逼B站获取表情都要登录开放平台也不发文本表情的URL我服了 # 傻逼B站获取表情都要登录开放平台也不发文本表情的URL我服了

View File

@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import asyncio import asyncio
import copy
import dataclasses import dataclasses
import datetime import datetime
import enum import enum
@@ -15,6 +16,7 @@ import Crypto.Cipher.AES as cry_aes # noqa
import Crypto.Util.Padding as cry_pad # noqa import Crypto.Util.Padding as cry_pad # noqa
import aiohttp import aiohttp
import cachetools import cachetools
import circuitbreaker
import config import config
import utils.async_io import utils.async_io
@@ -79,10 +81,10 @@ def create_translate_provider(cfg):
cfg['query_interval'], cfg['source_language'], cfg['target_language'], cfg['query_interval'], cfg['source_language'], cfg['target_language'],
cfg['app_id'], cfg['secret'] cfg['app_id'], cfg['secret']
) )
elif type_ == 'GeminiTranslate': elif type_ == 'OpenAiApi':
return GeminiTranslate( return OpenAiApi(
cfg['query_interval'], cfg['proxy'], cfg['api_key'], cfg['model_code'], cfg['query_interval'], cfg['api_key'], cfg['base_url'], cfg['proxy'], cfg['model'],
cfg['prompt'], cfg['temperature'] cfg['prompt'], cfg['max_tokens'], cfg['temperature'], cfg['top_p']
) )
return None return None
@@ -255,8 +257,10 @@ class TranslateProvider:
logger.info('%s became available', cls_name) logger.info('%s became available', cls_name)
task = await _pop_task() task = await _pop_task()
# 为了简化代码约定只会在_translate_wrapper里变成不可用所以获取task之后这里还是可用的 if not self.is_available:
assert self.is_available if not _push_task(task):
task.future.set_result(None)
continue
start_time = datetime.datetime.now() start_time = datetime.datetime.now()
await self._translate_wrapper(task) await self._translate_wrapper(task)
@@ -267,7 +271,7 @@ class TranslateProvider:
except Exception: # noqa except Exception: # noqa
logger.exception('%s error:', cls_name) logger.exception('%s error:', cls_name)
async def _translate_wrapper(self, task: TranslateTask) -> Optional[str]: async def _translate_wrapper(self, task: TranslateTask):
try: try:
exc = None exc = None
task.remain_retry_count -= 1 task.remain_retry_count -= 1
@@ -277,7 +281,7 @@ class TranslateProvider:
res = None res = None
if res is not None: if res is not None:
task.future.set_result(res) task.future.set_result(res)
return res return
if task.remain_retry_count > 0: if task.remain_retry_count > 0:
# 还可以重试则放回队列 # 还可以重试则放回队列
@@ -289,7 +293,6 @@ class TranslateProvider:
task.future.set_exception(exc) task.future.set_exception(exc)
else: else:
task.future.set_result(None) task.future.set_result(None)
return None
async def _do_translate(self, text) -> Optional[str]: async def _do_translate(self, text) -> Optional[str]:
raise NotImplementedError raise NotImplementedError
@@ -345,8 +348,9 @@ class TencentTranslate(TranslateProvider):
hashed_request_payload = hashlib.sha256(body_bytes).hexdigest() hashed_request_payload = hashlib.sha256(body_bytes).hexdigest()
canonical_request = f'POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_request_payload}' canonical_request = f'POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_request_payload}'
request_timestamp = int(datetime.datetime.now().timestamp()) cur_time_utc = datetime.datetime.now(datetime.UTC)
date = datetime.datetime.utcfromtimestamp(request_timestamp).strftime('%Y-%m-%d') request_timestamp = int(cur_time_utc.timestamp())
date = cur_time_utc.strftime('%Y-%m-%d')
credential_scope = f'{date}/tmt/tc3_request' credential_scope = f'{date}/tmt/tc3_request'
hashed_canonical_request = hashlib.sha256(canonical_request.encode('utf-8')).hexdigest() 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}' string_to_sign = f'TC3-HMAC-SHA256\n{request_timestamp}\n{credential_scope}\n{hashed_canonical_request}'
@@ -470,96 +474,96 @@ class BaiduTranslate(TranslateProvider):
self._on_availability_change() self._on_availability_change()
class GeminiTranslate(TranslateProvider): class _OpenAiApiExpectedError(Exception):
def __init__(self, query_interval, proxy, api_key, model_code, prompt, temperature): pass
class OpenAiApi(TranslateProvider):
def __init__(self, query_interval, api_key, base_url, proxy, model, prompt, max_tokens, temperature, top_p):
super().__init__(query_interval) super().__init__(query_interval)
self._url = base_url + '/chat/completions'
self._proxy = proxy or None self._proxy = proxy or None
self._api_key = api_key self._headers = {
self._url = f'https://generativelanguage.googleapis.com/v1beta/{model_code}:generateContent' 'Authorization': 'Bearer ' + api_key
self._prompt = prompt }
self._body = { self._body = {
'contents': [ 'model': model,
{ 'messages': [
# 'role': 'user', {'role': 'system', 'content': prompt},
'parts': [{'text': ''}] {'role': 'user', 'content': ''},
}
], ],
'safetySettings': [ 'stream': False,
{ 'max_tokens': max_tokens,
'category': 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
'threshold': 'BLOCK_NONE'
},
{
'category': 'HARM_CATEGORY_HATE_SPEECH',
'threshold': 'BLOCK_NONE'
},
{
'category': 'HARM_CATEGORY_HARASSMENT',
'threshold': 'BLOCK_NONE'
},
{
'category': 'HARM_CATEGORY_DANGEROUS_CONTENT',
'threshold': 'BLOCK_NONE'
}
],
'generationConfig': {
'temperature': temperature, 'temperature': temperature,
'topP': 1, 'top_p': top_p,
'topK': 32,
'candidateCount': 1,
'maxOutputTokens': 8192
}
} }
self._breaker = circuitbreaker.CircuitBreaker(
failure_threshold=5,
recovery_timeout=60,
)
self._cool_down_timer_handle = None self._cool_down_timer_handle = None
@property @property
def is_available(self): def is_available(self):
return self._cool_down_timer_handle is None and super().is_available return self._breaker.state != circuitbreaker.STATE_OPEN and super().is_available
async def _translate_wrapper(self, task: TranslateTask):
# 并发请求,不然太慢了
utils.async_io.create_task_with_ref(super()._translate_wrapper(task))
async def _do_translate(self, text) -> Optional[str]: async def _do_translate(self, text) -> Optional[str]:
input_text = self._prompt.format(original_text=text) data = None
self._body['contents'][0]['parts'][0]['text'] = input_text
try: try:
with self._breaker:
body = copy.deepcopy(self._body)
body['messages'][-1]['content'] = text
async with utils.request.http_session.post( async with utils.request.http_session.post(
self._url, self._url,
params={'key': self._api_key}, headers=self._headers,
json=self._body, json=body,
proxy=self._proxy, proxy=self._proxy,
timeout=aiohttp.ClientTimeout(total=30),
) as r: ) as r:
if r.status != 200: if r.status != 200:
logger.warning('GeminiTranslate request failed: status=%d %s', r.status, r.reason) rsp_body = await r.text()
self._on_fail(r.status) logger.warning(
return None 'OpenAiApi request failed: status=%d %s, body=%s', r.status, r.reason, rsp_body
)
raise _OpenAiApiExpectedError('OpenAiApi request failed')
data = await r.json() data = await r.json()
return data['choices'][0]['message']['content']
except (_OpenAiApiExpectedError, circuitbreaker.CircuitBreakerError):
pass
except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as e: except (aiohttp.ClientConnectionError, asyncio.TimeoutError) as e:
logger.warning('GeminiTranslate request failed, %s: %s', type(e).__name__, e) logger.warning('OpenAiApi request failed, %s: %s', type(e).__name__, e)
return None
try:
candidates = data['candidates']
if not candidates:
block_reason = data['promptFeedback'].get('blockReason', '')
logger.warning('GeminiTranslate no candidate, block_reason=%s, text=%s', block_reason, text)
return None
first_content_parts = candidates[0]['content']['parts']
return ''.join(part.get('text', '') for part in first_content_parts)
except (KeyError, IndexError): except (KeyError, IndexError):
logger.warning('GeminiTranslate failed to parse response: %s', data) if data is not None:
logger.warning('OpenAiApi failed to parse response: %s', data)
else:
logger.exception('OpenAiApi unknown exception:')
if self._breaker.state == circuitbreaker.STATE_OPEN:
self._on_breaker_open()
return None return None
def _on_fail(self, code): def _on_breaker_open(self):
if self._cool_down_timer_handle is not None: if self._cool_down_timer_handle is not None:
return return
if code in (401, 403):
# API密钥无效没有权限或者不在有效地区。需要手动处理等5分钟
self._cool_down_timer_handle = asyncio.get_running_loop().call_later( self._cool_down_timer_handle = asyncio.get_running_loop().call_later(
5 * 60, self._on_cool_down_timeout self._breaker.open_remaining, self._on_cool_down_timeout
) )
self._on_availability_change() self._on_availability_change()
def _on_cool_down_timeout(self): def _on_cool_down_timeout(self):
self._cool_down_timer_handle = None self._cool_down_timer_handle = None
if self._breaker.state == circuitbreaker.STATE_OPEN:
self._cool_down_timer_handle = asyncio.get_running_loop().call_later(
self._breaker.open_remaining, self._on_cool_down_timeout
)
return
self._on_availability_change() self._on_availability_change()