Blame view

translation/service.py 16.3 KB
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"])
f07947a5   tangwang   Improve portabili...
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
  
f07947a5   tangwang   Improve portabili...
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,
              )
f07947a5   tangwang   Improve portabili...
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,
f07947a5   tangwang   Improve portabili...
92
93
                  error_text,
                  exc_info=True,
cd4ce66d   tangwang   trans logs
94
              )
f07947a5   tangwang   Improve portabili...
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())
  
f07947a5   tangwang   Improve portabili...
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)
f07947a5   tangwang   Improve portabili...
219
          backend = self._backends.get(normalized) or self._load_backend(normalized)
cd4ce66d   tangwang   trans logs
220
          if backend is None:
f07947a5   tangwang   Improve portabili...
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