mirror of
https://github.com/xfgryujk/blivechat.git
synced 2026-08-19 09:43:28 +08:00
Gemini翻译改为OpenAI API兼容接口
This commit is contained in:
2
blivedm
2
blivedm
Submodule blivedm updated: 100ed8fdf9...b204c17cc2
10
config.py
10
config.py
@@ -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
|
||||
|
||||
@@ -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,我服了
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user