Skip to content

Commit 504e83b

Browse files
committed
v1.9.2: 修复 ctranslate2 在容器环境中的加载问题,采用延迟导入策略
1 parent 2c54774 commit 504e83b

8 files changed

Lines changed: 242 additions & 77 deletions

File tree

doc/CHANGELOG_v1.9.2.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@
2323

2424
## ⚡ 优化
2525

26+
### 翻译器架构改进
27+
- **系统提示词与用户消息分离**
28+
- OpenAI 和 OpenAI HQ:系统提示词(包括断句提示、自定义提示、基础系统提示)现在放在 `system` role,用户消息只包含上下文、待翻译文本和图片
29+
- Gemini 和 Gemini HQ:系统提示词放在 `system_instruction` 参数中,用户消息只包含上下文、待翻译文本和图片
30+
- 图片始终放在用户消息的最后
31+
- 更符合各 API 的最佳实践,提高翻译质量
32+
33+
- **智能重试机制**
34+
- 新增重试提示功能,避免模型服务器缓存导致的重复错误
35+
- 当翻译质量不合格(数量不匹配、质量检查失败、BR 标记缺失)需要重试时,会在用户消息中添加重试次数和失败原因
36+
- 例如:`[Retry attempt #2] Reason: Translation count mismatch: expected 10, got 8`
37+
- 有效解决某些第三方 API 代理的缓存问题
38+
2639
### 检测和过滤
2740
- **优化混合检测替换逻辑**
2841
- 改进 YOLO OBB 检测器与主检测器的框合并算法
@@ -77,6 +90,38 @@
7790

7891
## 📝 技术细节
7992

93+
### 翻译器消息结构优化
94+
95+
**OpenAI 系列**
96+
```python
97+
messages = [
98+
{"role": "system", "content": "断句提示词\n\n自定义提示词\n\n基础系统提示词"},
99+
{"role": "user", "content": [文本, 图片...]} # 图片在最后
100+
]
101+
```
102+
103+
**Gemini 系列**
104+
```python
105+
# 初始化时
106+
GenerativeModel(
107+
model_name="gemini-1.5-flash",
108+
system_instruction="断句提示词\n\n自定义提示词\n\n基础系统提示词"
109+
)
110+
111+
# 发送时
112+
contents = [文本, 图片...] # 图片在最后
113+
```
114+
115+
**重试提示示例**
116+
```
117+
[Retry attempt #2] Reason: Quality check failed: Empty translation detected
118+
119+
Please translate the following manga text regions:
120+
1. こんにちは
121+
2. さようなら
122+
...
123+
```
124+
80125
### 混合检测算法改进
81126
之前的逻辑只检查 YOLO 框是否完全包含**单个**主检测器框,可能导致以下问题:
82127
- YOLO 框包含了主检测器的框 A 和框 B

manga_translator/translators/__init__.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from .caiyun import CaiyunTranslator
1212
from .openai import OpenAITranslator
1313
from .nllb import NLLBTranslator, NLLBBigTranslator
14-
from .sugoi import JparacrawlTranslator, JparacrawlBigTranslator, SugoiTranslator
14+
# 延迟导入 sugoi 翻译器,避免在启动时加载 ctranslate2
15+
# from .sugoi import JparacrawlTranslator, JparacrawlBigTranslator, SugoiTranslator
1516
from .m2m100 import M2M100Translator, M2M100BigTranslator
1617
from .mbart50 import MBart50Translator
1718
from .selective import SelectiveOfflineTranslator, prepare as prepare_selective_translator
@@ -26,13 +27,49 @@
2627
from ..config import Config, Translator, TranslatorConfig, TranslatorChain
2728
from ..utils import Context
2829

30+
# 延迟导入函数
31+
def _get_sugoi_translators():
32+
"""延迟导入 Sugoi 相关翻译器"""
33+
from .sugoi import JparacrawlTranslator, JparacrawlBigTranslator, SugoiTranslator
34+
return JparacrawlTranslator, JparacrawlBigTranslator, SugoiTranslator
35+
36+
# 使用懒加载的占位符
37+
_sugoi_translators_loaded = False
38+
_JparacrawlTranslator = None
39+
_JparacrawlBigTranslator = None
40+
_SugoiTranslator = None
41+
42+
def _ensure_sugoi_loaded():
43+
"""确保 Sugoi 翻译器已加载"""
44+
global _sugoi_translators_loaded, _JparacrawlTranslator, _JparacrawlBigTranslator, _SugoiTranslator
45+
if not _sugoi_translators_loaded:
46+
_JparacrawlTranslator, _JparacrawlBigTranslator, _SugoiTranslator = _get_sugoi_translators()
47+
_sugoi_translators_loaded = True
48+
49+
class _LazyTranslatorProxy:
50+
"""延迟加载翻译器的代理类"""
51+
def __init__(self, translator_name):
52+
self.translator_name = translator_name
53+
self._translator_class = None
54+
55+
def __call__(self, *args, **kwargs):
56+
if self._translator_class is None:
57+
_ensure_sugoi_loaded()
58+
if self.translator_name == 'sugoi':
59+
self._translator_class = _SugoiTranslator
60+
elif self.translator_name == 'jparacrawl':
61+
self._translator_class = _JparacrawlTranslator
62+
elif self.translator_name == 'jparacrawl_big':
63+
self._translator_class = _JparacrawlBigTranslator
64+
return self._translator_class(*args, **kwargs)
65+
2966
OFFLINE_TRANSLATORS = {
3067
Translator.offline: SelectiveOfflineTranslator,
3168
Translator.nllb: NLLBTranslator,
3269
Translator.nllb_big: NLLBBigTranslator,
33-
Translator.sugoi: SugoiTranslator,
34-
Translator.jparacrawl: JparacrawlTranslator,
35-
Translator.jparacrawl_big: JparacrawlBigTranslator,
70+
Translator.sugoi: _LazyTranslatorProxy('sugoi'),
71+
Translator.jparacrawl: _LazyTranslatorProxy('jparacrawl'),
72+
Translator.jparacrawl_big: _LazyTranslatorProxy('jparacrawl_big'),
3673
Translator.m2m100: M2M100Translator,
3774
Translator.m2m100_big: M2M100BigTranslator,
3875
Translator.mbart50: MBart50Translator,

manga_translator/translators/common.py

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,35 @@ def _check_cancelled(self):
157157
if self._cancel_check_callback and self._cancel_check_callback():
158158
raise asyncio.CancelledError("Translation cancelled by user")
159159

160-
def _build_user_prompt_for_texts(self, texts: List[str], ctx=None, prev_context: str = "") -> str:
160+
def _get_retry_hint(self, attempt: int, reason: str = "") -> str:
161+
"""
162+
生成重试提示信息,用于避免模型服务器缓存导致的重复错误
163+
164+
Args:
165+
attempt: 当前尝试次数
166+
reason: 重试原因(可选)
167+
168+
Returns:
169+
重试提示字符串
170+
"""
171+
hints = [
172+
f"[Retry attempt #{attempt}]",
173+
f"[This is attempt #{attempt}, please provide a different response]",
174+
f"[Attempt {attempt}: Previous response had issues, please try again]",
175+
f"[Retry #{attempt}: Please ensure quality this time]",
176+
f"[Attempt {attempt} - Previous attempts failed quality check]"
177+
]
178+
179+
# 根据尝试次数选择不同的提示(循环使用)
180+
base_hint = hints[(attempt - 1) % len(hints)]
181+
182+
# 如果提供了原因,添加到提示中
183+
if reason:
184+
return f"{base_hint} Reason: {reason}\n\n"
185+
else:
186+
return f"{base_hint}\n\n"
187+
188+
def _build_user_prompt_for_texts(self, texts: List[str], ctx=None, prev_context: str = "", retry_attempt: int = 0, retry_reason: str = "") -> str:
161189
"""
162190
统一的用户提示词构建方法(纯文本翻译)
163191
适用于 openai.py 和 gemini.py
@@ -166,6 +194,8 @@ def _build_user_prompt_for_texts(self, texts: List[str], ctx=None, prev_context:
166194
texts: 要翻译的文本列表
167195
ctx: 上下文对象(可选)
168196
prev_context: 历史上下文(可选)
197+
retry_attempt: 重试次数(可选,用于避免缓存)
198+
retry_reason: 重试原因(可选)
169199
170200
Returns:
171201
构建好的用户提示词字符串
@@ -176,6 +206,10 @@ def _build_user_prompt_for_texts(self, texts: List[str], ctx=None, prev_context:
176206
enable_ai_break = getattr(ctx.config.render, 'disable_auto_wrap', False)
177207

178208
prompt = ""
209+
210+
# 添加重试提示(如果是重试)
211+
if retry_attempt > 0:
212+
prompt += self._get_retry_hint(retry_attempt, retry_reason)
179213

180214
# 添加多页上下文(如果有)
181215
if prev_context:
@@ -201,7 +235,7 @@ def _build_user_prompt_for_texts(self, texts: List[str], ctx=None, prev_context:
201235

202236
return prompt
203237

204-
def _build_user_prompt_for_hq(self, batch_data: List, ctx=None, prev_context: str = "") -> str:
238+
def _build_user_prompt_for_hq(self, batch_data: List, ctx=None, prev_context: str = "", retry_attempt: int = 0, retry_reason: str = "") -> str:
205239
"""
206240
统一的用户提示词构建方法(高质量多模态翻译)
207241
适用于 openai_hq.py 和 gemini_hq.py
@@ -210,6 +244,8 @@ def _build_user_prompt_for_hq(self, batch_data: List, ctx=None, prev_context: st
210244
batch_data: 批次数据列表,包含图片和原文
211245
ctx: 上下文对象(可选)
212246
prev_context: 历史上下文(可选)
247+
retry_attempt: 重试次数(可选,用于避免缓存)
248+
retry_reason: 重试原因(可选)
213249
214250
Returns:
215251
构建好的用户提示词字符串
@@ -220,6 +256,10 @@ def _build_user_prompt_for_hq(self, batch_data: List, ctx=None, prev_context: st
220256
enable_ai_break = getattr(ctx.config.render, 'disable_auto_wrap', False)
221257

222258
prompt = ""
259+
260+
# 添加重试提示(如果是重试)
261+
if retry_attempt > 0:
262+
prompt += self._get_retry_hint(retry_attempt, retry_reason)
223263

224264
# 添加多页上下文(如果有)
225265
if prev_context:

manga_translator/translators/gemini.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -226,9 +226,9 @@ def _build_system_prompt(self, source_lang: str, target_lang: str, custom_prompt
226226
final_prompt += base_prompt
227227
return final_prompt
228228

229-
def _build_user_prompt(self, texts: List[str], ctx: Any) -> str:
229+
def _build_user_prompt(self, texts: List[str], ctx: Any, retry_attempt: int = 0, retry_reason: str = "") -> str:
230230
"""构建用户提示词(纯文本版)- 使用统一方法,只包含上下文和待翻译文本"""
231-
return self._build_user_prompt_for_texts(texts, ctx, self.prev_context)
231+
return self._build_user_prompt_for_texts(texts, ctx, self.prev_context, retry_attempt=retry_attempt, retry_reason=retry_reason)
232232

233233
def _get_system_instruction(self, source_lang: str, target_lang: str, custom_prompt_json: Dict[str, Any] = None, line_break_prompt_json: Dict[str, Any] = None) -> str:
234234
"""获取完整的系统指令(包含断句提示词、自定义提示词和基础系统提示词)"""
@@ -257,20 +257,15 @@ async def _translate_batch(self, texts: List[str], source_lang: str, target_lang
257257
self.logger.error("Gemini客户端初始化失败")
258258
return texts
259259

260-
# 构建用户提示词(只包含上下文和待翻译文本,不包含系统指令)
261-
user_prompt = self._build_user_prompt(texts, ctx)
260+
# 初始化重试信息
261+
retry_attempt = 0
262+
retry_reason = ""
262263

263264
# 发送请求
264265
max_retries = self.attempts
265266
attempt = 0
266267
is_infinite = max_retries == -1
267268
local_attempt = 0 # 本次批次的尝试次数
268-
269-
# 动态构建请求参数 - 默认总是发送安全设置
270-
request_args = {
271-
"contents": user_prompt,
272-
"safety_settings": self.safety_settings
273-
}
274269

275270
# 标记是否需要回退(不发送安全设置)
276271
should_retry_without_safety = False
@@ -294,6 +289,15 @@ def generate_content_with_logging(**kwargs):
294289
if local_attempt > self._SPLIT_THRESHOLD and len(texts) > 1 and split_level < self._MAX_SPLIT_ATTEMPTS:
295290
self.logger.warning(f"Triggering split after {local_attempt} local attempts")
296291
raise self.SplitException(local_attempt, texts)
292+
293+
# 构建用户提示词(包含重试信息以避免缓存)
294+
user_prompt = self._build_user_prompt(texts, ctx, retry_attempt=retry_attempt, retry_reason=retry_reason)
295+
296+
# 动态构建请求参数 - 默认总是发送安全设置
297+
request_args = {
298+
"contents": user_prompt,
299+
"safety_settings": self.safety_settings
300+
}
297301

298302
try:
299303
# RPM限制
@@ -371,9 +375,10 @@ def generate_content_with_logging(**kwargs):
371375

372376
# Strict validation: must match input count
373377
if len(translations) != len(texts):
374-
attempt += 1
378+
retry_attempt += 1
379+
retry_reason = f"Translation count mismatch: expected {len(texts)}, got {len(translations)}"
375380
log_attempt = f"{attempt}/{max_retries}" if not is_infinite else f"Attempt {attempt}"
376-
self.logger.warning(f"[{log_attempt}] Translation count mismatch: expected {len(texts)}, got {len(translations)}. Retrying...")
381+
self.logger.warning(f"[{log_attempt}] {retry_reason}. Retrying...")
377382
self.logger.warning(f"Expected texts: {texts}")
378383
self.logger.warning(f"Got translations: {translations}")
379384

@@ -386,9 +391,10 @@ def generate_content_with_logging(**kwargs):
386391
# 质量验证:检查空翻译、合并翻译、可疑符号等
387392
is_valid, error_msg = self._validate_translation_quality(texts, translations)
388393
if not is_valid:
389-
attempt += 1
394+
retry_attempt += 1
395+
retry_reason = f"Quality check failed: {error_msg}"
390396
log_attempt = f"{attempt}/{max_retries}" if not is_infinite else f"Attempt {attempt}"
391-
self.logger.warning(f"[{log_attempt}] Quality check failed: {error_msg}. Retrying...")
397+
self.logger.warning(f"[{log_attempt}] {retry_reason}. Retrying...")
392398

393399
if not is_infinite and attempt >= max_retries:
394400
raise Exception(f"Quality check failed after {max_retries} attempts: {error_msg}")
@@ -404,9 +410,10 @@ def generate_content_with_logging(**kwargs):
404410

405411
# BR检查:检查翻译结果是否包含必要的[BR]标记
406412
if not self._validate_br_markers(translations, queries=texts, ctx=ctx):
407-
attempt += 1
413+
retry_attempt += 1
414+
retry_reason = "BR markers missing in translations"
408415
log_attempt = f"{attempt}/{max_retries}" if not is_infinite else f"Attempt {attempt}"
409-
self.logger.warning(f"[{log_attempt}] BR markers missing, retrying...")
416+
self.logger.warning(f"[{log_attempt}] {retry_reason}, retrying...")
410417

411418
# 如果达到最大重试次数,抛出友好的异常
412419
if not is_infinite and attempt >= max_retries:

0 commit comments

Comments
 (0)