5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
1
2
3
4
5
|
"""Translation service orchestration."""
from __future__ import annotations
import logging
|
8140e942
tangwang
translator model ...
|
6
|
from typing import Dict, List, Optional, Tuple
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
7
|
|
86d8358b
tangwang
config optimize
|
8
9
|
from config.loader import get_app_config
from config.schema import AppConfig
|
cd4ce66d
tangwang
trans logs
|
10
|
from translation.cache import TranslationCache
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
11
|
from translation.protocols import TranslateInput, TranslateOutput, TranslationBackendProtocol
|
0fd2f875
tangwang
translate
|
12
13
14
15
16
17
|
from translation.settings import (
TranslationConfig,
get_enabled_translation_models,
get_translation_capability,
normalize_translation_model,
normalize_translation_scene,
|
8140e942
tangwang
translator model ...
|
18
|
translation_cache_probe_models,
|
0fd2f875
tangwang
translate
|
19
|
)
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
20
21
22
23
24
25
26
|
logger = logging.getLogger(__name__)
class TranslationService:
"""Owns translation backends and routes calls by model and scene."""
|
86d8358b
tangwang
config optimize
|
27
28
29
|
def __init__(self, config: Optional[TranslationConfig] = None, app_config: Optional[AppConfig] = None) -> None:
self._app_config = app_config or get_app_config()
self.config = config or self._app_config.services.translation.as_dict()
|
0fd2f875
tangwang
translate
|
30
|
self._enabled_capabilities = self._collect_enabled_capabilities()
|
0fd2f875
tangwang
translate
|
31
32
|
if not self._enabled_capabilities:
raise ValueError("No enabled translation backends found in services.translation.capabilities")
|
cd4ce66d
tangwang
trans logs
|
33
|
self._translation_cache = TranslationCache(self.config["cache"])
|
89fa3f3c
tangwang
Sync master porta...
|
34
35
36
37
38
39
|
self._backends: Dict[str, TranslationBackendProtocol] = {}
self._backend_errors: Dict[str, str] = {}
self._initialize_backends()
if not self._backends:
details = ", ".join(f"{name}: {err}" for name, err in sorted(self._backend_errors.items())) or "unknown error"
raise RuntimeError(f"No translation backends could be initialized: {details}")
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
40
|
|
0fd2f875
tangwang
translate
|
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
def _collect_enabled_capabilities(self) -> Dict[str, Dict[str, object]]:
enabled: Dict[str, Dict[str, object]] = {}
for name in get_enabled_translation_models(self.config):
capability = get_translation_capability(self.config, name, require_enabled=True)
backend_type = capability.get("backend")
if not backend_type:
raise ValueError(f"Translation capability '{name}' must define a backend")
enabled[name] = capability
return enabled
def _create_backend(
self,
*,
name: str,
backend_type: str,
cfg: Dict[str, object],
) -> TranslationBackendProtocol:
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
58
|
registry = {
|
0fd2f875
tangwang
translate
|
59
|
"qwen_mt": self._create_qwen_mt_backend,
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
60
61
|
"deepl": self._create_deepl_backend,
"llm": self._create_llm_backend,
|
0fd2f875
tangwang
translate
|
62
63
|
"local_nllb": self._create_local_nllb_backend,
"local_marian": self._create_local_marian_backend,
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
64
|
}
|
0fd2f875
tangwang
translate
|
65
66
67
68
|
factory = registry.get(backend_type)
if factory is None:
raise ValueError(f"Unsupported translation backend '{backend_type}' for capability '{name}'")
return factory(name=name, cfg=cfg)
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
69
|
|
89fa3f3c
tangwang
Sync master porta...
|
70
71
72
73
74
75
76
77
78
79
80
|
def _load_backend(self, name: str) -> Optional[TranslationBackendProtocol]:
capability_cfg = self._enabled_capabilities.get(name)
if capability_cfg is None:
return None
if name in self._backends:
return self._backends[name]
backend_type = str(capability_cfg["backend"])
logger.info("Initializing translation backend | model=%s backend=%s", name, backend_type)
try:
backend = self._create_backend(
|
cd4ce66d
tangwang
trans logs
|
81
82
83
84
|
name=name,
backend_type=backend_type,
cfg=capability_cfg,
)
|
89fa3f3c
tangwang
Sync master porta...
|
85
86
87
88
89
|
except Exception as exc:
error_text = str(exc).strip() or exc.__class__.__name__
self._backend_errors[name] = error_text
logger.error(
"Translation backend initialization failed | model=%s backend=%s error=%s",
|
cd4ce66d
tangwang
trans logs
|
90
91
|
name,
backend_type,
|
89fa3f3c
tangwang
Sync master porta...
|
92
93
|
error_text,
exc_info=True,
|
cd4ce66d
tangwang
trans logs
|
94
|
)
|
89fa3f3c
tangwang
Sync master porta...
|
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
return None
self._backends[name] = backend
self._backend_errors.pop(name, None)
logger.info(
"Translation backend initialized | model=%s backend=%s use_cache=%s backend_model=%s",
name,
backend_type,
bool(capability_cfg.get("use_cache")),
getattr(backend, "model", name),
)
return backend
def _initialize_backends(self) -> None:
for name, capability_cfg in self._enabled_capabilities.items():
self._load_backend(name)
|
cd4ce66d
tangwang
trans logs
|
111
|
|
0fd2f875
tangwang
translate
|
112
|
def _create_qwen_mt_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
113
114
|
from translation.backends.qwen_mt import QwenMTTranslationBackend
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
115
|
return QwenMTTranslationBackend(
|
0fd2f875
tangwang
translate
|
116
117
118
|
capability_name=name,
model=str(cfg["model"]).strip(),
base_url=str(cfg["base_url"]).strip(),
|
86d8358b
tangwang
config optimize
|
119
|
api_key=self._app_config.infrastructure.secrets.dashscope_api_key,
|
0fd2f875
tangwang
translate
|
120
|
timeout=int(cfg["timeout_sec"]),
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
121
|
glossary_id=cfg.get("glossary_id"),
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
122
123
|
)
|
0fd2f875
tangwang
translate
|
124
|
def _create_deepl_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
125
126
|
from translation.backends.deepl import DeepLTranslationBackend
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
127
|
return DeepLTranslationBackend(
|
86d8358b
tangwang
config optimize
|
128
|
api_key=self._app_config.infrastructure.secrets.deepl_auth_key,
|
0fd2f875
tangwang
translate
|
129
130
|
api_url=str(cfg["api_url"]).strip(),
timeout=float(cfg["timeout_sec"]),
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
131
132
133
|
glossary_id=cfg.get("glossary_id"),
)
|
0fd2f875
tangwang
translate
|
134
|
def _create_llm_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
135
136
|
from translation.backends.llm import LLMTranslationBackend
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
137
|
return LLMTranslationBackend(
|
0fd2f875
tangwang
translate
|
138
139
140
141
|
capability_name=name,
model=str(cfg["model"]).strip(),
timeout_sec=float(cfg["timeout_sec"]),
base_url=str(cfg["base_url"]).strip(),
|
86d8358b
tangwang
config optimize
|
142
|
api_key=self._app_config.infrastructure.secrets.dashscope_api_key,
|
0fd2f875
tangwang
translate
|
143
144
145
|
)
def _create_local_nllb_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
|
ea293660
tangwang
CTranslate2
|
146
|
from translation.backends.local_ctranslate2 import NLLBCTranslate2TranslationBackend
|
0fd2f875
tangwang
translate
|
147
|
|
ea293660
tangwang
CTranslate2
|
148
|
return NLLBCTranslate2TranslationBackend(
|
0fd2f875
tangwang
translate
|
149
150
151
152
153
154
155
156
157
|
name=name,
model_id=str(cfg["model_id"]).strip(),
model_dir=str(cfg["model_dir"]).strip(),
device=str(cfg["device"]).strip(),
torch_dtype=str(cfg["torch_dtype"]).strip(),
batch_size=int(cfg["batch_size"]),
max_input_length=int(cfg["max_input_length"]),
max_new_tokens=int(cfg["max_new_tokens"]),
num_beams=int(cfg["num_beams"]),
|
ea293660
tangwang
CTranslate2
|
158
159
160
161
162
163
164
165
|
ct2_model_dir=cfg.get("ct2_model_dir"),
ct2_compute_type=cfg.get("ct2_compute_type"),
ct2_auto_convert=bool(cfg.get("ct2_auto_convert", True)),
ct2_conversion_quantization=cfg.get("ct2_conversion_quantization"),
ct2_inter_threads=int(cfg.get("ct2_inter_threads", 1)),
ct2_intra_threads=int(cfg.get("ct2_intra_threads", 0)),
ct2_max_queued_batches=int(cfg.get("ct2_max_queued_batches", 0)),
ct2_batch_type=str(cfg.get("ct2_batch_type", "examples")),
|
46ce858d
tangwang
在NLLB模型的 /data/sa...
|
166
167
168
|
ct2_decoding_length_mode=str(cfg.get("ct2_decoding_length_mode", "fixed")),
ct2_decoding_length_extra=int(cfg.get("ct2_decoding_length_extra", 0)),
ct2_decoding_length_min=int(cfg.get("ct2_decoding_length_min", 1)),
|
0fd2f875
tangwang
translate
|
169
170
171
|
)
def _create_local_marian_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
|
ea293660
tangwang
CTranslate2
|
172
|
from translation.backends.local_ctranslate2 import MarianCTranslate2TranslationBackend, get_marian_language_direction
|
0fd2f875
tangwang
translate
|
173
174
175
|
source_lang, target_lang = get_marian_language_direction(name)
|
ea293660
tangwang
CTranslate2
|
176
|
return MarianCTranslate2TranslationBackend(
|
0fd2f875
tangwang
translate
|
177
178
179
180
181
182
183
184
185
186
187
|
name=name,
model_id=str(cfg["model_id"]).strip(),
model_dir=str(cfg["model_dir"]).strip(),
device=str(cfg["device"]).strip(),
torch_dtype=str(cfg["torch_dtype"]).strip(),
batch_size=int(cfg["batch_size"]),
max_input_length=int(cfg["max_input_length"]),
max_new_tokens=int(cfg["max_new_tokens"]),
num_beams=int(cfg["num_beams"]),
source_langs=[source_lang],
target_langs=[target_lang],
|
ea293660
tangwang
CTranslate2
|
188
189
190
191
192
193
194
195
|
ct2_model_dir=cfg.get("ct2_model_dir"),
ct2_compute_type=cfg.get("ct2_compute_type"),
ct2_auto_convert=bool(cfg.get("ct2_auto_convert", True)),
ct2_conversion_quantization=cfg.get("ct2_conversion_quantization"),
ct2_inter_threads=int(cfg.get("ct2_inter_threads", 1)),
ct2_intra_threads=int(cfg.get("ct2_intra_threads", 0)),
ct2_max_queued_batches=int(cfg.get("ct2_max_queued_batches", 0)),
ct2_batch_type=str(cfg.get("ct2_batch_type", "examples")),
|
46ce858d
tangwang
在NLLB模型的 /data/sa...
|
196
197
198
|
ct2_decoding_length_mode=str(cfg.get("ct2_decoding_length_mode", "fixed")),
ct2_decoding_length_extra=int(cfg.get("ct2_decoding_length_extra", 0)),
ct2_decoding_length_min=int(cfg.get("ct2_decoding_length_min", 1)),
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
199
200
201
202
|
)
@property
def available_models(self) -> List[str]:
|
0fd2f875
tangwang
translate
|
203
204
205
206
|
return list(self._enabled_capabilities.keys())
@property
def loaded_models(self) -> List[str]:
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
207
208
|
return list(self._backends.keys())
|
89fa3f3c
tangwang
Sync master porta...
|
209
210
211
212
213
214
215
216
|
@property
def failed_models(self) -> List[str]:
return list(self._backend_errors.keys())
@property
def backend_errors(self) -> Dict[str, str]:
return dict(self._backend_errors)
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
217
|
def get_backend(self, model: Optional[str] = None) -> TranslationBackendProtocol:
|
0fd2f875
tangwang
translate
|
218
|
normalized = normalize_translation_model(self.config, model)
|
89fa3f3c
tangwang
Sync master porta...
|
219
|
backend = self._backends.get(normalized) or self._load_backend(normalized)
|
cd4ce66d
tangwang
trans logs
|
220
|
if backend is None:
|
89fa3f3c
tangwang
Sync master porta...
|
221
222
223
224
225
226
227
228
229
|
if normalized not in self._enabled_capabilities:
raise ValueError(
f"Translation model '{normalized}' is not enabled. "
f"Available models: {', '.join(self.available_models) or 'none'}"
)
error_text = self._backend_errors.get(normalized) or "unknown initialization error"
raise RuntimeError(
f"Translation model '{normalized}' failed to initialize: {error_text}. "
f"Loaded models: {', '.join(self.loaded_models) or 'none'}"
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
230
|
)
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
231
232
233
234
235
236
237
238
239
240
|
return backend
def translate(
self,
text: TranslateInput,
target_lang: str,
source_lang: Optional[str] = None,
*,
model: Optional[str] = None,
scene: Optional[str] = None,
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
241
|
) -> TranslateOutput:
|
cd4ce66d
tangwang
trans logs
|
242
243
|
normalized_model = normalize_translation_model(self.config, model)
backend = self.get_backend(normalized_model)
|
0fd2f875
tangwang
translate
|
244
|
active_scene = normalize_translation_scene(self.config, scene)
|
cd4ce66d
tangwang
trans logs
|
245
246
|
capability_cfg = self._enabled_capabilities[normalized_model]
use_cache = bool(capability_cfg.get("use_cache"))
|
cd4ce66d
tangwang
trans logs
|
247
|
logger.info(
|
14e67b71
tangwang
分句后的 batching 现在是...
|
248
|
"Translation route | backend=%s request_type=%s use_cache=%s cache_available=%s",
|
cd4ce66d
tangwang
trans logs
|
249
|
getattr(backend, "model", normalized_model),
|
14e67b71
tangwang
分句后的 batching 现在是...
|
250
|
"single" if isinstance(text, str) else "batch",
|
cd4ce66d
tangwang
trans logs
|
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
|
use_cache,
self._translation_cache.available,
)
if not use_cache or not self._translation_cache.available:
return backend.translate(
text=text,
target_lang=target_lang,
source_lang=source_lang,
scene=active_scene,
)
if isinstance(text, str):
return self._translate_with_cache(
backend,
text=text,
target_lang=target_lang,
source_lang=source_lang,
scene=active_scene,
model=normalized_model,
)
return self._translate_batch_with_cache(
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
273
274
275
|
text=text,
target_lang=target_lang,
source_lang=source_lang,
|
cd4ce66d
tangwang
trans logs
|
276
|
backend=backend,
|
0fd2f875
tangwang
translate
|
277
|
scene=active_scene,
|
cd4ce66d
tangwang
trans logs
|
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
|
model=normalized_model,
)
def _translate_with_cache(
self,
backend: TranslationBackendProtocol,
*,
text: str,
target_lang: str,
source_lang: Optional[str],
scene: str,
model: str,
) -> Optional[str]:
if not text.strip():
return text
|
8140e942
tangwang
translator model ...
|
293
294
295
296
297
|
cached, _served = self._tiered_cache_get(
request_model=model,
target_lang=target_lang,
source_text=text,
)
|
cd4ce66d
tangwang
trans logs
|
298
299
|
if cached is not None:
logger.info(
|
14e67b71
tangwang
分句后的 batching 现在是...
|
300
|
"Translation cache served | request_type=single text_len=%s",
|
cd4ce66d
tangwang
trans logs
|
301
302
303
304
305
306
307
308
|
len(text),
)
return cached
translated = backend.translate(
text=text,
target_lang=target_lang,
source_lang=source_lang,
scene=scene,
|
5e4dc8e4
tangwang
翻译架构按“一个翻译服务 +
|
309
|
)
|
cd4ce66d
tangwang
trans logs
|
310
311
312
313
314
315
316
317
|
if translated is not None:
self._translation_cache.set(
model=model,
target_lang=target_lang,
source_text=text,
translated_text=translated,
)
logger.info(
|
14e67b71
tangwang
分句后的 batching 现在是...
|
318
|
"Translation backend result cached | request_type=single text_len=%s result_len=%s",
|
cd4ce66d
tangwang
trans logs
|
319
320
321
322
323
|
len(text),
len(str(translated)),
)
else:
logger.warning(
|
14e67b71
tangwang
分句后的 batching 现在是...
|
324
|
"Translation backend returned empty result | request_type=single text_len=%s",
|
cd4ce66d
tangwang
trans logs
|
325
326
327
328
|
len(text),
)
return translated
|
8140e942
tangwang
translator model ...
|
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
|
def _tiered_cache_get(
self,
*,
request_model: str,
target_lang: str,
source_text: str,
) -> Tuple[Optional[str], Optional[str]]:
"""Redis lookup: cache from higher-tier or **same-tier** models may satisfy A.
Lower-tier entries are never read. Returns ``(translated, served_model)``.
"""
probe_models = translation_cache_probe_models(self.config, request_model)
for probe_model in probe_models:
hit = self._translation_cache.get(
model=probe_model,
target_lang=target_lang,
source_text=source_text,
)
if hit is not None:
return hit, probe_model
return None, None
|
cd4ce66d
tangwang
trans logs
|
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
|
def _translate_batch_with_cache(
self,
*,
text: TranslateInput,
target_lang: str,
source_lang: Optional[str],
backend: TranslationBackendProtocol,
scene: str,
model: str,
) -> List[Optional[str]]:
texts = list(text)
results: List[Optional[str]] = [None] * len(texts)
misses: List[str] = []
miss_indices: List[int] = []
cache_hits = 0
for idx, item in enumerate(texts):
normalized_text = "" if item is None else str(item)
if not normalized_text.strip():
results[idx] = normalized_text
continue
|
8140e942
tangwang
translator model ...
|
374
375
|
cached, _served = self._tiered_cache_get(
request_model=model,
|
cd4ce66d
tangwang
trans logs
|
376
377
378
379
380
381
382
383
384
385
386
|
target_lang=target_lang,
source_text=normalized_text,
)
if cached is not None:
results[idx] = cached
cache_hits += 1
continue
misses.append(normalized_text)
miss_indices.append(idx)
logger.info(
|
14e67b71
tangwang
分句后的 batching 现在是...
|
387
|
"Translation batch cache summary | total=%s cache_hits=%s cache_misses=%s",
|
cd4ce66d
tangwang
trans logs
|
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
|
len(texts),
cache_hits,
len(misses),
)
if misses:
translated = backend.translate(
text=misses,
target_lang=target_lang,
source_lang=source_lang,
scene=scene,
)
translated_list = translated if isinstance(translated, list) else [translated]
for idx, original_text, translated_text in zip(miss_indices, misses, translated_list):
results[idx] = translated_text
if translated_text is not None:
self._translation_cache.set(
model=model,
target_lang=target_lang,
source_text=original_text,
translated_text=translated_text,
)
else:
logger.warning(
|
14e67b71
tangwang
分句后的 batching 现在是...
|
412
|
"Translation batch item returned empty result | item_index=%s text_len=%s",
|
cd4ce66d
tangwang
trans logs
|
413
414
415
416
417
|
idx,
len(original_text),
)
return results
|