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'
)
logger.warning('%s is deprecated, please see %s', type_, doc_url)
continue
elif type_ == 'TencentTranslate':
translator_config['source_language'] = section['source_language']
translator_config['target_language'] = section['target_language']
@@ -187,11 +186,16 @@ class AppConfig:
translator_config['app_id'] = section['app_id']
translator_config['secret'] = section['secret']
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['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['max_tokens'] = section.getint('max_tokens')
translator_config['temperature'] = section.getfloat('temperature')
translator_config['top_p'] = section.getfloat('top_p')
else:
raise ValueError(f'Invalid translator type: {type_}')
except Exception: # noqa

View File

@@ -138,48 +138,36 @@ app_id =
secret =
[gemini_translate]
# 文档https://ai.google.dev/docs
# 定价https://ai.google.dev/pricing
# * 目前只有免费版最高QPS为1
# * 收费版 输入 $0.000125 / 1000 个字符;输出 $0.000375 / 1000 个字符
# 模型规格https://ai.google.dev/models/gemini
# * Gemini Pro 最高QPS为1
[openai_api]
# 大语言模型对话支持任何兼容OpenAI API的平台参考各平台的文档吧
# 类型:Gemini翻译
type = GeminiTranslate
# 类型:OpenAI API
type = OpenAiApi
# 请求间隔时间(秒),等于 1 / QPS
query_interval = 1.1
# 代理地址如果为空不使用代理。注意必须要在指定地区才能使用https://ai.google.dev/available_regions
proxy = http://127.0.0.1:7890
# API密钥
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
# 提示词
prompt = As an advanced translation software, your role is to provide accurate translations that respect
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
Input1:\n来拉(莱拉)设置一下吧\nOutput1:\nライラちゃん、設定してみましょうか\n\n
Input2:\n我来拉一下各位进队伍\nOutput2:\n皆さんをチームに入れさせていただきます\n\n
Input3:\n死了w\nOutput3:\n死んじゃったわw\n\n
Input4:\n每天来宝这里打个卡[比心]\nOutput4:\n毎日、ここに来て宝さんにチェックインしま[比心]\n\n
\n
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
# 模型
model = deepseek-chat
# 提示词,换行符会替换为空格,“\n”会替换为换行符
prompt = 你是一个日本主播的翻译助手,精通中日文翻译。
用户会输入直播观众的聊天内容,请翻译成日文,并确保符合日文语言习惯。
你可以调整语气和风格,并考虑到某些词语的文化内涵和地区差异。
只回答翻译结果,不需要解释。
# 最大输出token数
max_tokens = 200
# 采样温度,用于控制令牌选择的随机性。较低的温度适合需要更具确定性或不够开放的回答的提示,而较高的温度可以产生更加多样化或更具创意的结果
temperature = 1.3
# 核采样概率阈值温度采样的替代方案。模型会考虑概率质量在top_p内的token结果
top_p = 1.0
# 傻逼B站获取表情都要登录开放平台也不发文本表情的URL我服了

View File

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