wellfound-viewer / src /translator.py
Zoey7Web's picture
Upload 16 files
4242667 verified
Raw
History Blame Contribute Delete
5.83 kB
"""
邮件翻译模块 - 调用 LLM 自动识别语言并翻译为指定目标语言
支持:
- 自动语言检测(通过 LLM 判断)
- 翻译为英文或中文
- 保留邮件格式(换行、段落)
"""
import json
import logging
import asyncio
from typing import Optional
from openai import AsyncOpenAI, OpenAI
from src import config as cfg
logger = logging.getLogger(__name__)
# ─── 系统提示词 ───
TRANSLATE_SYSTEM_PROMPT = """You are a professional email translator.
Your task is to translate recruitment-related emails accurately.
Rules:
1. Detect the source language automatically from the email content.
2. Translate the email body to the target language naturally and accurately.
3. Preserve formatting: line breaks, paragraphs, bullet points.
4. Keep company names, proper nouns, and technical terms intact (don't translate names).
5. Maintain the original tone (formal/informal) appropriate for recruitment context.
6. If the email is already in the target language, return it unchanged.
You MUST respond with valid JSON only:
{
"source_language": "<detected language name, e.g. 'English', 'Chinese', 'French'>",
"source_language_code": "<ISO 639-1 code, e.g. 'en', 'zh', 'fr'>",
"translated_text": "<translated email body>",
"is_already_target": <true if already in target language>
}
"""
# ─── 数据类 ───
class TranslationResult:
def __init__(
self,
source_language: str = "",
source_language_code: str = "",
translated_text: str = "",
is_already_target: bool = False,
error: str = "",
):
self.source_language = source_language
self.source_language_code = source_language_code
self.translated_text = translated_text
self.is_already_target = is_already_target
self.error = error
@property
def is_valid(self) -> bool:
return not self.error and bool(self.translated_text)
# ─── 核心函数 ───
def _build_translate_prompt(text: str, target_language: str) -> str:
return f"""Translate the following email to {target_language}.
Email Body:
{text}
Return the translation in the required JSON format."""
def _parse_translate_response(content: str) -> TranslationResult:
"""解析 LLM 翻译响应"""
content = content.strip()
# 去除 markdown 代码块
if content.startswith("```"):
lines = content.split("\n")
content = "\n".join(
lines[1:-1] if lines[-1].strip() == "```" else lines[1:]
)
content = content.strip()
try:
data = json.loads(content)
except json.JSONDecodeError as e:
logger.error(f"翻译响应 JSON 解析失败: {e}\n原始内容: {content[:200]}")
# 降级:把整个内容当译文
return TranslationResult(
source_language="Unknown",
source_language_code="unknown",
translated_text=content[:2000],
error=f"JSON 解析失败,已截断展示: {e}",
)
return TranslationResult(
source_language=data.get("source_language", "Unknown"),
source_language_code=data.get("source_language_code", "unknown"),
translated_text=data.get("translated_text", content[:2000]),
is_already_target=data.get("is_already_target", False),
)
async def translate_email_async(
text: str,
target_language: str = "English",
) -> TranslationResult:
"""
异步翻译邮件正文
:param text: 邮件正文
:param target_language: 目标语言(如 "English" 或 "Chinese")
"""
if not text or not text.strip():
return TranslationResult(error="邮件正文为空,无需翻译")
api_key = cfg.get_llm_api_key()
if not api_key:
return TranslationResult(error="未配置 LLM API Key,无法翻译")
try:
model = cfg.get_llm_model()
base_url = cfg.get_llm_base_url()
client_kwargs = {"api_key": api_key}
if base_url:
client_kwargs["base_url"] = base_url
client = AsyncOpenAI(**client_kwargs)
user_prompt = _build_translate_prompt(text[:4000], target_language)
logger.info(f"正在翻译邮件至 {target_language}...")
response = await client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": TRANSLATE_SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
],
temperature=0.1,
max_tokens=2048,
response_format={"type": "json_object"},
)
content = response.choices[0].message.content or ""
result = _parse_translate_response(content)
logger.info(
f"翻译完成: {result.source_language} -> {target_language}"
f" (已是目标语言: {result.is_already_target})"
)
return result
except Exception as e:
logger.error(f"翻译失败: {e}", exc_info=True)
return TranslationResult(error=f"翻译失败: {str(e)[:120]}")
def translate_email_sync(
text: str,
target_language: str = "English",
) -> TranslationResult:
"""同步版本"""
try:
loop = asyncio.get_event_loop()
if loop.is_running():
import concurrent.futures
with concurrent.futures.ThreadPoolExecutor() as pool:
future = pool.submit(
asyncio.run,
translate_email_async(text, target_language),
)
return future.result(timeout=60)
else:
return loop.run_until_complete(
translate_email_async(text, target_language)
)
except RuntimeError:
return asyncio.run(translate_email_async(text, target_language))