Blame view

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