-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
225 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import aiohttp | ||
from aiohttp_socks import ProxyConnector, ProxyType | ||
import asyncio | ||
from yarl import URL | ||
from core.config_loader import load_config | ||
|
||
def get_proxy_connector(proxy_url): | ||
if not proxy_url: | ||
return None | ||
|
||
url = URL(proxy_url) | ||
if url.scheme in ['http', 'https']: | ||
return ProxyConnector.from_url(proxy_url) | ||
elif url.scheme == 'socks5': | ||
return ProxyConnector( | ||
proxy_type=ProxyType.SOCKS5, | ||
host=url.host, | ||
port=url.port | ||
) | ||
else: | ||
raise ValueError(f"Unsupported proxy scheme: {url.scheme}") | ||
|
||
async def call_ai_api_async(api_name, model_name, prompt, max_retries=3, retry_delay=5): | ||
config = load_config() | ||
api_config = config['apis'][api_name] | ||
proxy_url = config.get('proxy', {}).get('url') | ||
|
||
headers = { | ||
"Content-Type": "application/json", | ||
"Authorization": f"Bearer {api_config['api_key']}" | ||
} | ||
|
||
data = { | ||
"model": model_name, | ||
"messages": [ | ||
{"role": "system", "content": "你是一位专业的我的世界插件配置文件翻译专家,你正在翻译配置文件中带#的注释部分内容,你只需要输出翻译后的内容,不要附上自己的猜想。如果遇到带有#的RGB颜色代码,请不要翻译,直接返回原内容。注意纯大写的文本如 LOWEST, LOW, NORMAL, HIGH, HIGHEST,请不要翻译,它们可能是供参考的配置选项。"}, | ||
{"role": "user", "content": prompt} | ||
] | ||
} | ||
|
||
connector = get_proxy_connector(proxy_url) | ||
|
||
async with aiohttp.ClientSession(connector=connector) as session: | ||
for attempt in range(max_retries): | ||
try: | ||
async with session.post(api_config['base_url'], headers=headers, json=data, timeout=30) as response: | ||
response.raise_for_status() | ||
result = await response.json() | ||
return result['choices'][0]['message']['content'].strip() | ||
except aiohttp.ClientError as e: | ||
if attempt < max_retries - 1: | ||
print(f"API 调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}. 正在重试...") | ||
await asyncio.sleep(retry_delay) | ||
else: | ||
print(f"API 调用失败 (尝试 {attempt + 1}/{max_retries}): {str(e)}. 已达到最大重试次数。") | ||
return None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
def extract_comments(file_content): | ||
comments = [] | ||
for i, line in enumerate(file_content.split('\n'), 1): | ||
if '#' in line: | ||
before_comment, comment = line.split('#', 1) | ||
comment = comment.strip() | ||
if comment: | ||
comments.append({ | ||
"original_text": comment, | ||
"location": {"line": i, "before_comment": before_comment} | ||
}) | ||
return comments |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import yaml | ||
|
||
def load_config(): | ||
with open('config.yml', 'r', encoding='utf-8') as file: | ||
config = yaml.safe_load(file) | ||
return config |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import json | ||
|
||
def save_json(data, filename): | ||
""" | ||
将数据保存为 JSON 文件 | ||
""" | ||
with open(filename, 'w', encoding='utf-8') as f: | ||
json.dump(data, f, ensure_ascii=False, indent=2) | ||
|
||
def merge_translations(file_content, translated_comments, chinese_only=False): | ||
""" | ||
将翻译后的注释合并回原始文件内容 | ||
:param file_content: 原始文件内容 | ||
:param translated_comments: 翻译后的注释列表 | ||
:param chinese_only: 是否只保留中文翻译 | ||
:return: 合并后的文件内容 | ||
""" | ||
lines = file_content.split('\n') | ||
for comment in translated_comments: | ||
line_num = comment["location"]["line"] - 1 | ||
before_comment = comment["location"]["before_comment"] | ||
original = comment["original_text"] | ||
translation = comment["translated_text"] | ||
|
||
if chinese_only: | ||
lines[line_num] = f"{before_comment}# {translation}" | ||
else: | ||
lines[line_num] = f"{before_comment}# {original} | {translation}" | ||
|
||
return '\n'.join(lines) | ||
|
||
def load_json(filename): | ||
""" | ||
从 JSON 文件加载数据 | ||
""" | ||
with open(filename, 'r', encoding='utf-8') as f: | ||
return json.load(f) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
import asyncio | ||
from core.api_caller import call_ai_api_async | ||
from core.config_loader import load_config | ||
import logging | ||
import sys | ||
|
||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', stream=sys.stdout) | ||
logger = logging.getLogger(__name__) | ||
|
||
def format_ai_output(output): | ||
""" | ||
将 AI 输出的多行内容合并为单行,使用 ' | ' 作为分隔符。 | ||
""" | ||
return ' | '.join(line.strip() for line in output.split('\n') if line.strip()) | ||
|
||
async def translate_comment_async(comment, api_name, model_name, target_lang): | ||
prompt = f"请将以下我的世界插件配置文件注释翻译成{target_lang}:{comment['original_text']}" | ||
translation = await call_ai_api_async(api_name, model_name, prompt) | ||
formatted_translation = format_ai_output(translation) if translation else "翻译失败" | ||
return { | ||
"id": comment.get("id"), | ||
"original_text": comment["original_text"], | ||
"translated_text": formatted_translation, | ||
"location": comment["location"] | ||
} | ||
|
||
async def translate_batch_async(comments, api_name, model_name, target_lang, batch_size): | ||
tasks = [] | ||
for comment in comments: | ||
task = asyncio.create_task(translate_comment_async(comment, api_name, model_name, target_lang)) | ||
tasks.append(task) | ||
|
||
if len(tasks) >= batch_size: | ||
batch_results = await asyncio.gather(*tasks, return_exceptions=True) | ||
for result in batch_results: | ||
if isinstance(result, Exception): | ||
logger.error(f"Translation failed: {str(result)}") | ||
yield None | ||
else: | ||
yield result | ||
tasks = [] | ||
|
||
if tasks: | ||
batch_results = await asyncio.gather(*tasks, return_exceptions=True) | ||
for result in batch_results: | ||
if isinstance(result, Exception): | ||
logger.error(f"Translation failed: {str(result)}") | ||
yield None | ||
else: | ||
yield result | ||
|
||
async def process_translations(comments, api_name, model_name, target_lang, batch_size, progress_bar, status_text): | ||
translated_comments = [] | ||
retry_comments = [] | ||
total_comments = len(comments) | ||
|
||
async for translated_comment in translate_batch_async(comments, api_name, model_name, target_lang, batch_size): | ||
if translated_comment is None: | ||
retry_comments.append(comments[len(translated_comments)]) | ||
else: | ||
translated_comments.append(translated_comment) | ||
|
||
progress = len(translated_comments) / total_comments | ||
if progress_bar: | ||
progress_bar.progress(progress) | ||
if status_text: | ||
status_text.text(f"已翻译 {len(translated_comments)}/{total_comments} 条注释 (进度: {progress:.2%})") | ||
logger.info(f"Translated {len(translated_comments)}/{total_comments} comments (Progress: {progress:.2%})") | ||
|
||
return translated_comments, retry_comments | ||
|
||
def translate_comments(comments, api_name, model_name, target_lang, progress_bar=None, status_text=None, batch_size=10, max_retries=3): | ||
config = load_config() | ||
all_translated_comments = [] | ||
remaining_comments = comments | ||
total_comments = len(comments) | ||
|
||
for attempt in range(max_retries): | ||
if not remaining_comments: | ||
break | ||
|
||
logger.info(f"Translation attempt {attempt + 1}/{max_retries}") | ||
if status_text: | ||
status_text.text(f"翻译尝试 {attempt + 1}/{max_retries}") | ||
|
||
async def run_translation(): | ||
nonlocal all_translated_comments, remaining_comments | ||
translated, to_retry = await process_translations(remaining_comments, api_name, model_name, target_lang, batch_size, progress_bar, status_text) | ||
all_translated_comments.extend(translated) | ||
remaining_comments = to_retry | ||
|
||
asyncio.run(run_translation()) | ||
|
||
if remaining_comments: | ||
logger.info(f"{len(remaining_comments)} comments failed to translate. Retrying...") | ||
if status_text: | ||
status_text.text(f"重新连接中... 剩余 {len(remaining_comments)} 条注释待翻译") | ||
asyncio.run(asyncio.sleep(5)) # 等待5秒后重试 | ||
|
||
if remaining_comments: | ||
logger.warning(f"Failed to translate {len(remaining_comments)} comments after {max_retries} attempts") | ||
if status_text: | ||
status_text.text(f"警告:{len(remaining_comments)} 条注释翻译失败") | ||
|
||
# 确保进度条显示100%完成 | ||
if progress_bar: | ||
progress_bar.progress(1.0) | ||
if status_text: | ||
status_text.text(f"翻译完成: {len(all_translated_comments)}/{total_comments} 条注释已翻译") | ||
|
||
logger.info(f"Translation completed. {len(all_translated_comments)}/{total_comments} comments translated.") | ||
|
||
return all_translated_comments |