Blame view

translation/service.py 14.6 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
34
          self._translation_cache = TranslationCache(self.config["cache"])
          self._backends = self._initialize_backends()
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
35
  
0fd2f875   tangwang   translate
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
      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   翻译架构按“一个翻译服务 +
53
          registry = {
0fd2f875   tangwang   translate
54
              "qwen_mt": self._create_qwen_mt_backend,
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
55
56
              "deepl": self._create_deepl_backend,
              "llm": self._create_llm_backend,
0fd2f875   tangwang   translate
57
58
              "local_nllb": self._create_local_nllb_backend,
              "local_marian": self._create_local_marian_backend,
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
59
          }
0fd2f875   tangwang   translate
60
61
62
63
          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   翻译架构按“一个翻译服务 +
64
  
cd4ce66d   tangwang   trans logs
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
      def _initialize_backends(self) -> Dict[str, TranslationBackendProtocol]:
          backends: Dict[str, TranslationBackendProtocol] = {}
          for name, capability_cfg in self._enabled_capabilities.items():
              backend_type = str(capability_cfg["backend"])
              logger.info("Initializing translation backend | model=%s backend=%s", name, backend_type)
              backends[name] = self._create_backend(
                  name=name,
                  backend_type=backend_type,
                  cfg=capability_cfg,
              )
              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(backends[name], "model", name),
              )
          return backends
  
0fd2f875   tangwang   translate
84
      def _create_qwen_mt_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
85
86
          from translation.backends.qwen_mt import QwenMTTranslationBackend
  
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
87
          return QwenMTTranslationBackend(
0fd2f875   tangwang   translate
88
89
90
              capability_name=name,
              model=str(cfg["model"]).strip(),
              base_url=str(cfg["base_url"]).strip(),
86d8358b   tangwang   config optimize
91
              api_key=self._app_config.infrastructure.secrets.dashscope_api_key,
0fd2f875   tangwang   translate
92
              timeout=int(cfg["timeout_sec"]),
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
93
              glossary_id=cfg.get("glossary_id"),
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
94
95
          )
  
0fd2f875   tangwang   translate
96
      def _create_deepl_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
97
98
          from translation.backends.deepl import DeepLTranslationBackend
  
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
99
          return DeepLTranslationBackend(
86d8358b   tangwang   config optimize
100
              api_key=self._app_config.infrastructure.secrets.deepl_auth_key,
0fd2f875   tangwang   translate
101
102
              api_url=str(cfg["api_url"]).strip(),
              timeout=float(cfg["timeout_sec"]),
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
103
104
105
              glossary_id=cfg.get("glossary_id"),
          )
  
0fd2f875   tangwang   translate
106
      def _create_llm_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
107
108
          from translation.backends.llm import LLMTranslationBackend
  
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
109
          return LLMTranslationBackend(
0fd2f875   tangwang   translate
110
111
112
113
              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
114
              api_key=self._app_config.infrastructure.secrets.dashscope_api_key,
0fd2f875   tangwang   translate
115
116
117
          )
  
      def _create_local_nllb_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
ea293660   tangwang   CTranslate2
118
          from translation.backends.local_ctranslate2 import NLLBCTranslate2TranslationBackend
0fd2f875   tangwang   translate
119
  
ea293660   tangwang   CTranslate2
120
          return NLLBCTranslate2TranslationBackend(
0fd2f875   tangwang   translate
121
122
123
124
125
126
127
128
129
              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
130
131
132
133
134
135
136
137
              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...
138
139
140
              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
141
142
143
          )
  
      def _create_local_marian_backend(self, *, name: str, cfg: Dict[str, object]) -> TranslationBackendProtocol:
ea293660   tangwang   CTranslate2
144
          from translation.backends.local_ctranslate2 import MarianCTranslate2TranslationBackend, get_marian_language_direction
0fd2f875   tangwang   translate
145
146
147
  
          source_lang, target_lang = get_marian_language_direction(name)
  
ea293660   tangwang   CTranslate2
148
          return MarianCTranslate2TranslationBackend(
0fd2f875   tangwang   translate
149
150
151
152
153
154
155
156
157
158
159
              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
160
161
162
163
164
165
166
167
              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...
168
169
170
              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   翻译架构按“一个翻译服务 +
171
172
173
174
          )
  
      @property
      def available_models(self) -> List[str]:
0fd2f875   tangwang   translate
175
176
177
178
          return list(self._enabled_capabilities.keys())
  
      @property
      def loaded_models(self) -> List[str]:
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
179
180
181
          return list(self._backends.keys())
  
      def get_backend(self, model: Optional[str] = None) -> TranslationBackendProtocol:
0fd2f875   tangwang   translate
182
          normalized = normalize_translation_model(self.config, model)
cd4ce66d   tangwang   trans logs
183
184
          backend = self._backends.get(normalized)
          if backend is None:
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
185
186
187
188
              raise ValueError(
                  f"Translation model '{normalized}' is not enabled. "
                  f"Available models: {', '.join(self.available_models) or 'none'}"
              )
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
189
190
191
192
193
194
195
196
197
198
          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   翻译架构按“一个翻译服务 +
199
      ) -> TranslateOutput:
cd4ce66d   tangwang   trans logs
200
201
          normalized_model = normalize_translation_model(self.config, model)
          backend = self.get_backend(normalized_model)
0fd2f875   tangwang   translate
202
          active_scene = normalize_translation_scene(self.config, scene)
cd4ce66d   tangwang   trans logs
203
204
          capability_cfg = self._enabled_capabilities[normalized_model]
          use_cache = bool(capability_cfg.get("use_cache"))
cd4ce66d   tangwang   trans logs
205
          logger.info(
14e67b71   tangwang   分句后的 batching 现在是...
206
              "Translation route | backend=%s request_type=%s use_cache=%s cache_available=%s",
cd4ce66d   tangwang   trans logs
207
              getattr(backend, "model", normalized_model),
14e67b71   tangwang   分句后的 batching 现在是...
208
              "single" if isinstance(text, str) else "batch",
cd4ce66d   tangwang   trans logs
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
              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   翻译架构按“一个翻译服务 +
231
232
233
              text=text,
              target_lang=target_lang,
              source_lang=source_lang,
cd4ce66d   tangwang   trans logs
234
              backend=backend,
0fd2f875   tangwang   translate
235
              scene=active_scene,
cd4ce66d   tangwang   trans logs
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
              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 ...
251
252
253
254
255
          cached, _served = self._tiered_cache_get(
              request_model=model,
              target_lang=target_lang,
              source_text=text,
          )
cd4ce66d   tangwang   trans logs
256
257
          if cached is not None:
              logger.info(
14e67b71   tangwang   分句后的 batching 现在是...
258
                  "Translation cache served | request_type=single text_len=%s",
cd4ce66d   tangwang   trans logs
259
260
261
262
263
264
265
266
                  len(text),
              )
              return cached
          translated = backend.translate(
              text=text,
              target_lang=target_lang,
              source_lang=source_lang,
              scene=scene,
5e4dc8e4   tangwang   翻译架构按“一个翻译服务 +
267
          )
cd4ce66d   tangwang   trans logs
268
269
270
271
272
273
274
275
          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 现在是...
276
                  "Translation backend result cached | request_type=single text_len=%s result_len=%s",
cd4ce66d   tangwang   trans logs
277
278
279
280
281
                  len(text),
                  len(str(translated)),
              )
          else:
              logger.warning(
14e67b71   tangwang   分句后的 batching 现在是...
282
                  "Translation backend returned empty result | request_type=single text_len=%s",
cd4ce66d   tangwang   trans logs
283
284
285
286
                  len(text),
              )
          return translated
  
8140e942   tangwang   translator model ...
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
      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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
      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 ...
332
333
              cached, _served = self._tiered_cache_get(
                  request_model=model,
cd4ce66d   tangwang   trans logs
334
335
336
337
338
339
340
341
342
343
344
                  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 现在是...
345
              "Translation batch cache summary | total=%s cache_hits=%s cache_misses=%s",
cd4ce66d   tangwang   trans logs
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
              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 现在是...
370
                          "Translation batch item returned empty result | item_index=%s text_len=%s",
cd4ce66d   tangwang   trans logs
371
372
373
374
375
                          idx,
                          len(original_text),
                      )
  
          return results