Commit 14e67b717157040d906b1e33098f59e1f9d66ba3
1 parent
294c3d0a
分句后的 batching 现在是“先全量分句,再按 segment 总数按模型 batch_size
推理”,不再是先按原始输入条数切块。也就是说,如果 100 条请求分句后变成 150 个 segments,batch_size=64 时会按 64 + 64 + 22 三批推理,推理完再按原始分句计划合并并还原成 100 条返回。这个改动在 local_seq2seq.py (line 241) 和 local_ctranslate2.py (line 391)。 日志这边也补上了两层你要的关键信息: 分句摘要日志:Translation segmentation summary,会打印输入条数、非空条数、发生分句的输入数、总 segments 数、当前 batch_size、每条输入分成多少段的统计,见 local_seq2seq.py (line 216) 和 local_ctranslate2.py (line 366)。 每个预测批次日志:Translation inference batch,会打印第几批、总批数、该批 segment 数、长度统计、首条预览。CTranslate2 另外还会打印 Translation model batch detail,补充 token 长度和 max_decoding_length,见 local_ctranslate2.py (line 294)。 我也补了测试,覆盖了“分句后再 batching”和“日志中有分句摘要与每批推理日志”,在 test_translation_local_backends.py (line 358)。
Showing
10 changed files
with
610 additions
and
113 deletions
Show diff stats
CLAUDE.md
| @@ -23,12 +23,24 @@ This is a **production-ready Multi-Tenant E-Commerce Search SaaS** platform spec | @@ -23,12 +23,24 @@ This is a **production-ready Multi-Tenant E-Commerce Search SaaS** platform spec | ||
| 23 | 23 | ||
| 24 | ## Development Environment | 24 | ## Development Environment |
| 25 | 25 | ||
| 26 | -**Required Environment Setup:** Use project root `activate.sh` (activates conda env `searchengine` and loads `.env`). On a new machine, set `CONDA_ROOT` if conda is not at default path. | 26 | +**Required Environment Setup:** Default to the project venv via root `activate.sh` (activates `./.venv` and loads `.env`). Do not default to system `python3`/`pip3` for repo work. |
| 27 | ```bash | 27 | ```bash |
| 28 | -# Optional on new machine: if conda is ~/anaconda3/bin/conda → export CONDA_ROOT=$HOME/anaconda3 | ||
| 29 | source activate.sh | 28 | source activate.sh |
| 30 | ``` | 29 | ``` |
| 31 | -See `docs/QUICKSTART.md` §1.4–1.8 for first-time env creation and production credentials (venv: `./scripts/create_venv.sh`; conda: `conda env create -f environment.yml` or `pip install -r requirements.txt`). | 30 | +See `docs/QUICKSTART.md` §1.4–1.8 for first-time env creation and production credentials (`./scripts/create_venv.sh` for the main venv). |
| 31 | + | ||
| 32 | +**Environment Resolution Rules:** | ||
| 33 | +- Main app, backend, indexer, frontend, generic scripts, and most tests: `./.venv` | ||
| 34 | +- Translator service runtime and local translation model tooling: `./.venv-translator` | ||
| 35 | +- Embedding service runtime: `./.venv-embedding` | ||
| 36 | +- Reranker service runtime: `./.venv-reranker` | ||
| 37 | +- CN-CLIP service runtime: `./.venv-cnclip` | ||
| 38 | +- Never assume the system interpreter reflects project dependencies; prefer `source activate.sh` or invoke the exact venv binary directly. | ||
| 39 | + | ||
| 40 | +**Operational Rule For Commands:** | ||
| 41 | +- For repo-wide `pytest`, ad hoc Python scripts, and lightweight development commands, use the matching venv interpreter first. | ||
| 42 | +- For isolated services, prefer the service scripts (`./scripts/start_translator.sh`, `./scripts/start_embedding_service.sh`, `./scripts/start_reranker.sh`, `./scripts/start_cnclip_service.sh`) because they already select the correct environment. | ||
| 43 | +- If a dependency appears “missing”, check whether the command was run under the wrong venv before proposing installs. | ||
| 32 | 44 | ||
| 33 | **Database Configuration:** | 45 | **Database Configuration:** |
| 34 | ```yaml | 46 | ```yaml |
| @@ -49,12 +61,14 @@ password: P89cZHS5d7dFyc9R | @@ -49,12 +61,14 @@ password: P89cZHS5d7dFyc9R | ||
| 49 | 61 | ||
| 50 | ### Environment Setup | 62 | ### Environment Setup |
| 51 | ```bash | 63 | ```bash |
| 52 | -# Activate environment (canonical: use activate.sh) | 64 | +# Activate main project environment (canonical) |
| 53 | source activate.sh | 65 | source activate.sh |
| 54 | 66 | ||
| 55 | # First-time / new machine: create env and install deps | 67 | # First-time / new machine: create env and install deps |
| 56 | -./setup.sh # or: conda env create -f environment.yml | ||
| 57 | -# If pip-only: pip install -r requirements.txt | 68 | +./setup.sh |
| 69 | +# or: | ||
| 70 | +./scripts/create_venv.sh | ||
| 71 | +source activate.sh | ||
| 58 | ``` | 72 | ``` |
| 59 | 73 | ||
| 60 | ### Data Management | 74 | ### Data Management |
| @@ -83,12 +97,12 @@ python main.py serve --host 0.0.0.0 --port 6002 --reload | @@ -83,12 +97,12 @@ python main.py serve --host 0.0.0.0 --port 6002 --reload | ||
| 83 | ### Testing | 97 | ### Testing |
| 84 | ```bash | 98 | ```bash |
| 85 | # Run all tests | 99 | # Run all tests |
| 86 | -python -m pytest tests/ | 100 | +pytest tests/ |
| 87 | 101 | ||
| 88 | # Run specific test types | 102 | # Run specific test types |
| 89 | -python -m pytest tests/unit/ # Unit tests | ||
| 90 | -python -m pytest tests/integration/ # Integration tests | ||
| 91 | -python -m pytest -m "api" # API tests only | 103 | +pytest tests/unit/ # Unit tests |
| 104 | +pytest tests/integration/ # Integration tests | ||
| 105 | +pytest -m "api" # API tests only | ||
| 92 | 106 | ||
| 93 | # Test search from command line | 107 | # Test search from command line |
| 94 | python main.py search "query" --tenant-id 1 --size 10 | 108 | python main.py search "query" --tenant-id 1 --size 10 |
| @@ -602,4 +616,3 @@ python main.py search "query" --tenant-id 1 # Quick search test | @@ -602,4 +616,3 @@ python main.py search "query" --tenant-id 1 # Quick search test | ||
| 602 | 8. **Multi-tenant Architecture**: Single index with `tenant_id` isolation | 616 | 8. **Multi-tenant Architecture**: Single index with `tenant_id` isolation |
| 603 | 9. **Hybrid Search**: BM25 + vector similarity with configurable weighting | 617 | 9. **Hybrid Search**: BM25 + vector similarity with configurable weighting |
| 604 | 10. **Production Ready**: Health checks, monitoring, graceful degradation | 618 | 10. **Production Ready**: Health checks, monitoring, graceful degradation |
| 605 | - |
api/translator_app.py
| @@ -5,18 +5,24 @@ import logging | @@ -5,18 +5,24 @@ import logging | ||
| 5 | import os | 5 | import os |
| 6 | import pathlib | 6 | import pathlib |
| 7 | import time | 7 | import time |
| 8 | +import uuid | ||
| 8 | from contextlib import asynccontextmanager | 9 | from contextlib import asynccontextmanager |
| 9 | from functools import lru_cache | 10 | from functools import lru_cache |
| 10 | from logging.handlers import TimedRotatingFileHandler | 11 | from logging.handlers import TimedRotatingFileHandler |
| 11 | from typing import List, Optional, Union | 12 | from typing import List, Optional, Union |
| 12 | 13 | ||
| 13 | import uvicorn | 14 | import uvicorn |
| 14 | -from fastapi import FastAPI, HTTPException | 15 | +from fastapi import FastAPI, HTTPException, Request |
| 15 | from fastapi.middleware.cors import CORSMiddleware | 16 | from fastapi.middleware.cors import CORSMiddleware |
| 16 | from fastapi.responses import JSONResponse | 17 | from fastapi.responses import JSONResponse |
| 17 | from pydantic import BaseModel, ConfigDict, Field | 18 | from pydantic import BaseModel, ConfigDict, Field |
| 18 | 19 | ||
| 19 | from config.services_config import get_translation_config | 20 | from config.services_config import get_translation_config |
| 21 | +from translation.logging_utils import ( | ||
| 22 | + TranslationRequestFilter, | ||
| 23 | + bind_translation_request_id, | ||
| 24 | + reset_translation_request_id, | ||
| 25 | +) | ||
| 20 | from translation.service import TranslationService | 26 | from translation.service import TranslationService |
| 21 | from translation.settings import ( | 27 | from translation.settings import ( |
| 22 | get_enabled_translation_models, | 28 | get_enabled_translation_models, |
| @@ -33,7 +39,8 @@ def configure_translator_logging() -> None: | @@ -33,7 +39,8 @@ def configure_translator_logging() -> None: | ||
| 33 | 39 | ||
| 34 | log_level = os.getenv("LOG_LEVEL", "INFO").upper() | 40 | log_level = os.getenv("LOG_LEVEL", "INFO").upper() |
| 35 | numeric_level = getattr(logging, log_level, logging.INFO) | 41 | numeric_level = getattr(logging, log_level, logging.INFO) |
| 36 | - formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") | 42 | + formatter = logging.Formatter("%(asctime)s | reqid:%(reqid)s | %(name)s | %(levelname)s | %(message)s") |
| 43 | + request_filter = TranslationRequestFilter() | ||
| 37 | 44 | ||
| 38 | root_logger = logging.getLogger() | 45 | root_logger = logging.getLogger() |
| 39 | root_logger.setLevel(numeric_level) | 46 | root_logger.setLevel(numeric_level) |
| @@ -42,6 +49,7 @@ def configure_translator_logging() -> None: | @@ -42,6 +49,7 @@ def configure_translator_logging() -> None: | ||
| 42 | console_handler = logging.StreamHandler() | 49 | console_handler = logging.StreamHandler() |
| 43 | console_handler.setLevel(numeric_level) | 50 | console_handler.setLevel(numeric_level) |
| 44 | console_handler.setFormatter(formatter) | 51 | console_handler.setFormatter(formatter) |
| 52 | + console_handler.addFilter(request_filter) | ||
| 45 | root_logger.addHandler(console_handler) | 53 | root_logger.addHandler(console_handler) |
| 46 | 54 | ||
| 47 | file_handler = TimedRotatingFileHandler( | 55 | file_handler = TimedRotatingFileHandler( |
| @@ -53,6 +61,7 @@ def configure_translator_logging() -> None: | @@ -53,6 +61,7 @@ def configure_translator_logging() -> None: | ||
| 53 | ) | 61 | ) |
| 54 | file_handler.setLevel(numeric_level) | 62 | file_handler.setLevel(numeric_level) |
| 55 | file_handler.setFormatter(formatter) | 63 | file_handler.setFormatter(formatter) |
| 64 | + file_handler.addFilter(request_filter) | ||
| 56 | root_logger.addHandler(file_handler) | 65 | root_logger.addHandler(file_handler) |
| 57 | 66 | ||
| 58 | verbose_logger = logging.getLogger("translator.verbose") | 67 | verbose_logger = logging.getLogger("translator.verbose") |
| @@ -69,6 +78,7 @@ def configure_translator_logging() -> None: | @@ -69,6 +78,7 @@ def configure_translator_logging() -> None: | ||
| 69 | ) | 78 | ) |
| 70 | verbose_handler.setLevel(numeric_level) | 79 | verbose_handler.setLevel(numeric_level) |
| 71 | verbose_handler.setFormatter(formatter) | 80 | verbose_handler.setFormatter(formatter) |
| 81 | + verbose_handler.addFilter(request_filter) | ||
| 72 | verbose_logger.addHandler(verbose_handler) | 82 | verbose_logger.addHandler(verbose_handler) |
| 73 | 83 | ||
| 74 | 84 | ||
| @@ -178,6 +188,13 @@ def _result_preview(translated: Union[str, List[Optional[str]], None]) -> str: | @@ -178,6 +188,13 @@ def _result_preview(translated: Union[str, List[Optional[str]], None]) -> str: | ||
| 178 | return _text_preview(str(translated)) | 188 | return _text_preview(str(translated)) |
| 179 | 189 | ||
| 180 | 190 | ||
| 191 | +def _resolve_request_id(http_request: Request) -> str: | ||
| 192 | + header_value = http_request.headers.get("X-Request-ID") | ||
| 193 | + if header_value and header_value.strip(): | ||
| 194 | + return header_value.strip()[:32] | ||
| 195 | + return str(uuid.uuid4())[:8] | ||
| 196 | + | ||
| 197 | + | ||
| 181 | def _translate_batch( | 198 | def _translate_batch( |
| 182 | service: TranslationService, | 199 | service: TranslationService, |
| 183 | raw_text: List[str], | 200 | raw_text: List[str], |
| @@ -189,15 +206,11 @@ def _translate_batch( | @@ -189,15 +206,11 @@ def _translate_batch( | ||
| 189 | ) -> List[Optional[str]]: | 206 | ) -> List[Optional[str]]: |
| 190 | backend = service.get_backend(model) | 207 | backend = service.get_backend(model) |
| 191 | logger.info( | 208 | logger.info( |
| 192 | - "Translation batch dispatch | model=%s scene=%s target_lang=%s source_lang=%s count=%s lengths=%s first_preview=%s supports_batch=%s", | ||
| 193 | - model, | ||
| 194 | - scene, | ||
| 195 | - target_lang, | ||
| 196 | - source_lang or "auto", | 209 | + "Translation batch dispatch | execution=%s count=%s lengths=%s first_preview=%s", |
| 210 | + "backend-batch" if getattr(backend, "supports_batch", False) else "per-item", | ||
| 197 | len(raw_text), | 211 | len(raw_text), |
| 198 | [len(str(item or "")) for item in raw_text], | 212 | [len(str(item or "")) for item in raw_text], |
| 199 | _text_preview(raw_text[0] if raw_text else ""), | 213 | _text_preview(raw_text[0] if raw_text else ""), |
| 200 | - bool(getattr(backend, "supports_batch", False)), | ||
| 201 | ) | 214 | ) |
| 202 | if getattr(backend, "supports_batch", False): | 215 | if getattr(backend, "supports_batch", False): |
| 203 | try: | 216 | try: |
| @@ -330,12 +343,13 @@ async def health_check(): | @@ -330,12 +343,13 @@ async def health_check(): | ||
| 330 | 343 | ||
| 331 | 344 | ||
| 332 | @app.post("/translate", response_model=TranslationResponse) | 345 | @app.post("/translate", response_model=TranslationResponse) |
| 333 | -async def translate(request: TranslationRequest): | 346 | +async def translate(request: TranslationRequest, http_request: Request): |
| 334 | _ensure_valid_text(request.text) | 347 | _ensure_valid_text(request.text) |
| 335 | 348 | ||
| 336 | if not request.target_lang: | 349 | if not request.target_lang: |
| 337 | raise HTTPException(status_code=400, detail="target_lang is required") | 350 | raise HTTPException(status_code=400, detail="target_lang is required") |
| 338 | 351 | ||
| 352 | + _, request_token = bind_translation_request_id(_resolve_request_id(http_request)) | ||
| 339 | request_started = time.perf_counter() | 353 | request_started = time.perf_counter() |
| 340 | try: | 354 | try: |
| 341 | service = get_translation_service() | 355 | service = get_translation_service() |
| @@ -447,12 +461,14 @@ async def translate(request: TranslationRequest): | @@ -447,12 +461,14 @@ async def translate(request: TranslationRequest): | ||
| 447 | raise | 461 | raise |
| 448 | except ValueError as e: | 462 | except ValueError as e: |
| 449 | latency_ms = (time.perf_counter() - request_started) * 1000 | 463 | latency_ms = (time.perf_counter() - request_started) * 1000 |
| 450 | - logger.warning("Translation validation error | error=%s latency_ms=%.2f", e, latency_ms, exc_info=True) | 464 | + logger.warning("Translation validation error | error=%s latency_ms=%.2f", e, latency_ms) |
| 451 | raise HTTPException(status_code=400, detail=str(e)) from e | 465 | raise HTTPException(status_code=400, detail=str(e)) from e |
| 452 | except Exception as e: | 466 | except Exception as e: |
| 453 | latency_ms = (time.perf_counter() - request_started) * 1000 | 467 | latency_ms = (time.perf_counter() - request_started) * 1000 |
| 454 | logger.error("Translation error | error=%s latency_ms=%.2f", e, latency_ms, exc_info=True) | 468 | logger.error("Translation error | error=%s latency_ms=%.2f", e, latency_ms, exc_info=True) |
| 455 | raise HTTPException(status_code=500, detail=f"Translation error: {str(e)}") | 469 | raise HTTPException(status_code=500, detail=f"Translation error: {str(e)}") |
| 470 | + finally: | ||
| 471 | + reset_translation_request_id(request_token) | ||
| 456 | 472 | ||
| 457 | 473 | ||
| 458 | @app.get("/") | 474 | @app.get("/") |
tests/test_translation_local_backends.py
| 1 | +import logging | ||
| 2 | + | ||
| 3 | +import pytest | ||
| 1 | import torch | 4 | import torch |
| 2 | 5 | ||
| 3 | from translation.backends.local_seq2seq import MarianMTTranslationBackend, NLLBTranslationBackend | 6 | from translation.backends.local_seq2seq import MarianMTTranslationBackend, NLLBTranslationBackend |
| 7 | +from translation.backends.local_ctranslate2 import NLLBCTranslate2TranslationBackend | ||
| 4 | from translation.service import TranslationService | 8 | from translation.service import TranslationService |
| 5 | from translation.text_splitter import compute_safe_input_token_limit, split_text_for_translation | 9 | from translation.text_splitter import compute_safe_input_token_limit, split_text_for_translation |
| 6 | 10 | ||
| @@ -44,11 +48,59 @@ class _FakeModel: | @@ -44,11 +48,59 @@ class _FakeModel: | ||
| 44 | return [[42]] | 48 | return [[42]] |
| 45 | 49 | ||
| 46 | 50 | ||
| 51 | +class _FakeCT2Tokenizer: | ||
| 52 | + def __init__(self, src_lang=None): | ||
| 53 | + self.src_lang = src_lang | ||
| 54 | + self.pad_token = "</s>" | ||
| 55 | + self.eos_token = "</s>" | ||
| 56 | + self.last_call = None | ||
| 57 | + | ||
| 58 | + def __call__(self, texts, **kwargs): | ||
| 59 | + self.last_call = {"texts": list(texts), **kwargs} | ||
| 60 | + return {"input_ids": [[1, 2, 3] for _ in texts]} | ||
| 61 | + | ||
| 62 | + def convert_ids_to_tokens(self, ids): | ||
| 63 | + del ids | ||
| 64 | + return ["tok_a", "tok_b", "tok_c"] | ||
| 65 | + | ||
| 66 | + def convert_tokens_to_ids(self, tokens): | ||
| 67 | + if isinstance(tokens, list): | ||
| 68 | + return [1 for _ in tokens] | ||
| 69 | + return 1 | ||
| 70 | + | ||
| 71 | + def decode(self, token_ids, skip_special_tokens=True): | ||
| 72 | + del token_ids, skip_special_tokens | ||
| 73 | + return "translated" | ||
| 74 | + | ||
| 75 | + | ||
| 76 | +class _FakeCT2Result: | ||
| 77 | + def __init__(self, tokens): | ||
| 78 | + self.hypotheses = [tokens] | ||
| 79 | + | ||
| 80 | + | ||
| 81 | +class _FakeCT2Translator: | ||
| 82 | + def __init__(self): | ||
| 83 | + self.last_translate_batch_kwargs = None | ||
| 84 | + | ||
| 85 | + def translate_batch(self, source_tokens, **kwargs): | ||
| 86 | + self.last_translate_batch_kwargs = {"source_tokens": source_tokens, **kwargs} | ||
| 87 | + target_prefix = kwargs.get("target_prefix") or [] | ||
| 88 | + return [ | ||
| 89 | + _FakeCT2Result((target_prefix[idx] or []) + ["translated_token"]) | ||
| 90 | + for idx, _ in enumerate(source_tokens) | ||
| 91 | + ] | ||
| 92 | + | ||
| 93 | + | ||
| 47 | def _stub_load_model(self): | 94 | def _stub_load_model(self): |
| 48 | self.tokenizer = _FakeTokenizer() | 95 | self.tokenizer = _FakeTokenizer() |
| 49 | self.seq2seq_model = _FakeModel() | 96 | self.seq2seq_model = _FakeModel() |
| 50 | 97 | ||
| 51 | 98 | ||
| 99 | +def _stub_load_ct2_runtime(self): | ||
| 100 | + self.tokenizer = _FakeCT2Tokenizer() | ||
| 101 | + self.translator = _FakeCT2Translator() | ||
| 102 | + | ||
| 103 | + | ||
| 52 | def test_marian_language_validation(monkeypatch): | 104 | def test_marian_language_validation(monkeypatch): |
| 53 | monkeypatch.setattr(MarianMTTranslationBackend, "_load_model", _stub_load_model) | 105 | monkeypatch.setattr(MarianMTTranslationBackend, "_load_model", _stub_load_model) |
| 54 | backend = MarianMTTranslationBackend( | 106 | backend = MarianMTTranslationBackend( |
| @@ -68,12 +120,8 @@ def test_marian_language_validation(monkeypatch): | @@ -68,12 +120,8 @@ def test_marian_language_validation(monkeypatch): | ||
| 68 | result = backend.translate("测试", source_lang="zh", target_lang="en") | 120 | result = backend.translate("测试", source_lang="zh", target_lang="en") |
| 69 | assert result == "translated" | 121 | assert result == "translated" |
| 70 | 122 | ||
| 71 | - try: | 123 | + with pytest.raises(ValueError, match="source languages"): |
| 72 | backend.translate("test", source_lang="en", target_lang="zh") | 124 | backend.translate("test", source_lang="en", target_lang="zh") |
| 73 | - except ValueError as exc: | ||
| 74 | - assert "source languages" in str(exc) | ||
| 75 | - else: | ||
| 76 | - raise AssertionError("Expected unsupported source language to raise") | ||
| 77 | 125 | ||
| 78 | 126 | ||
| 79 | def test_nllb_uses_src_lang_and_forced_bos(monkeypatch): | 127 | def test_nllb_uses_src_lang_and_forced_bos(monkeypatch): |
| @@ -97,6 +145,61 @@ def test_nllb_uses_src_lang_and_forced_bos(monkeypatch): | @@ -97,6 +145,61 @@ def test_nllb_uses_src_lang_and_forced_bos(monkeypatch): | ||
| 97 | assert backend.seq2seq_model.last_generate_kwargs["forced_bos_token_id"] == 202 | 145 | assert backend.seq2seq_model.last_generate_kwargs["forced_bos_token_id"] == 202 |
| 98 | 146 | ||
| 99 | 147 | ||
| 148 | +def test_nllb_accepts_finnish_short_code(monkeypatch): | ||
| 149 | + monkeypatch.setattr(NLLBTranslationBackend, "_load_model", _stub_load_model) | ||
| 150 | + backend = NLLBTranslationBackend( | ||
| 151 | + name="nllb-200-distilled-600m", | ||
| 152 | + model_id="facebook/nllb-200-distilled-600M", | ||
| 153 | + model_dir="./models/translation/facebook/nllb-200-distilled-600M", | ||
| 154 | + device="cpu", | ||
| 155 | + torch_dtype="float32", | ||
| 156 | + batch_size=1, | ||
| 157 | + max_input_length=16, | ||
| 158 | + max_new_tokens=16, | ||
| 159 | + num_beams=1, | ||
| 160 | + ) | ||
| 161 | + | ||
| 162 | + result = backend.translate("test", source_lang="fi", target_lang="zh") | ||
| 163 | + | ||
| 164 | + assert result == "translated" | ||
| 165 | + assert backend.tokenizer.src_lang == "fin_Latn" | ||
| 166 | + assert backend.seq2seq_model.last_generate_kwargs["forced_bos_token_id"] == 202 | ||
| 167 | + | ||
| 168 | + | ||
| 169 | +def test_nllb_ctranslate2_accepts_finnish_short_code(monkeypatch): | ||
| 170 | + created_tokenizers = [] | ||
| 171 | + | ||
| 172 | + def _fake_from_pretrained(source, src_lang=None, **kwargs): | ||
| 173 | + del source, kwargs | ||
| 174 | + tokenizer = _FakeCT2Tokenizer(src_lang=src_lang) | ||
| 175 | + created_tokenizers.append(tokenizer) | ||
| 176 | + return tokenizer | ||
| 177 | + | ||
| 178 | + monkeypatch.setattr(NLLBCTranslate2TranslationBackend, "_load_runtime", _stub_load_ct2_runtime) | ||
| 179 | + monkeypatch.setattr( | ||
| 180 | + "translation.backends.local_ctranslate2.AutoTokenizer.from_pretrained", | ||
| 181 | + _fake_from_pretrained, | ||
| 182 | + ) | ||
| 183 | + backend = NLLBCTranslate2TranslationBackend( | ||
| 184 | + name="nllb-200-distilled-600m", | ||
| 185 | + model_id="facebook/nllb-200-distilled-600M", | ||
| 186 | + model_dir="./models/translation/facebook/nllb-200-distilled-600M", | ||
| 187 | + device="cpu", | ||
| 188 | + torch_dtype="float32", | ||
| 189 | + batch_size=1, | ||
| 190 | + max_input_length=16, | ||
| 191 | + max_new_tokens=16, | ||
| 192 | + num_beams=1, | ||
| 193 | + ) | ||
| 194 | + | ||
| 195 | + result = backend.translate("test", source_lang="fi", target_lang="zh") | ||
| 196 | + | ||
| 197 | + assert result == "translated" | ||
| 198 | + assert len(created_tokenizers) == 1 | ||
| 199 | + assert created_tokenizers[0].src_lang == "fin_Latn" | ||
| 200 | + assert backend.translator.last_translate_batch_kwargs["target_prefix"] == [["zho_Hans"]] | ||
| 201 | + | ||
| 202 | + | ||
| 100 | def test_translation_service_preloads_enabled_backends(monkeypatch): | 203 | def test_translation_service_preloads_enabled_backends(monkeypatch): |
| 101 | created = [] | 204 | created = [] |
| 102 | 205 | ||
| @@ -245,7 +348,71 @@ def test_local_backend_splits_oversized_text_before_translation(): | @@ -245,7 +348,71 @@ def test_local_backend_splits_oversized_text_before_translation(): | ||
| 245 | result = backend.translate(text, source_lang="en", target_lang="zh") | 348 | result = backend.translate(text, source_lang="en", target_lang="zh") |
| 246 | 349 | ||
| 247 | assert result is not None | 350 | assert result is not None |
| 248 | - assert len(backend.translated_batches) == 1 | ||
| 249 | - assert len(backend.translated_batches[0]) >= 2 | ||
| 250 | - assert all(len(piece) <= 16 for piece in backend.translated_batches[0]) | ||
| 251 | - assert result == "".join(f"<{piece.strip()}>" for piece in backend.translated_batches[0]) | 351 | + all_segments = [piece for batch in backend.translated_batches for piece in batch] |
| 352 | + assert len(all_segments) >= 2 | ||
| 353 | + assert all(len(batch) <= backend.batch_size for batch in backend.translated_batches) | ||
| 354 | + assert all(len(piece) <= 16 for piece in all_segments) | ||
| 355 | + assert result == "".join(f"<{piece.strip()}>" for piece in all_segments) | ||
| 356 | + | ||
| 357 | + | ||
| 358 | +def test_local_backend_batches_after_segmentation(): | ||
| 359 | + backend = _SegmentingMarianBackend( | ||
| 360 | + name="opus-mt-en-zh", | ||
| 361 | + model_id="Helsinki-NLP/opus-mt-en-zh", | ||
| 362 | + model_dir="./models/translation/Helsinki-NLP/opus-mt-en-zh", | ||
| 363 | + device="cpu", | ||
| 364 | + torch_dtype="float32", | ||
| 365 | + batch_size=4, | ||
| 366 | + max_input_length=24, | ||
| 367 | + max_new_tokens=24, | ||
| 368 | + num_beams=1, | ||
| 369 | + source_langs=["en"], | ||
| 370 | + target_langs=["zh"], | ||
| 371 | + ) | ||
| 372 | + | ||
| 373 | + texts = [ | ||
| 374 | + "alpha beta gamma delta, epsilon zeta eta theta, iota kappa lambda mu.", | ||
| 375 | + "nu xi omicron pi, rho sigma tau upsilon, phi chi psi omega.", | ||
| 376 | + "dress shirt coat pants, socks shoes belt scarf, hat gloves bag watch.", | ||
| 377 | + ] | ||
| 378 | + | ||
| 379 | + result = backend.translate(texts, source_lang="en", target_lang="zh") | ||
| 380 | + | ||
| 381 | + assert isinstance(result, list) | ||
| 382 | + assert len(result) == 3 | ||
| 383 | + assert len(backend.translated_batches) >= 2 | ||
| 384 | + assert all(len(batch) <= backend.batch_size for batch in backend.translated_batches) | ||
| 385 | + assert sum(len(batch) for batch in backend.translated_batches) > backend.batch_size | ||
| 386 | + assert all(item is not None for item in result) | ||
| 387 | + | ||
| 388 | + | ||
| 389 | +def test_local_backend_logs_segmentation_and_inference_batches(caplog): | ||
| 390 | + backend = _SegmentingMarianBackend( | ||
| 391 | + name="opus-mt-en-zh", | ||
| 392 | + model_id="Helsinki-NLP/opus-mt-en-zh", | ||
| 393 | + model_dir="./models/translation/Helsinki-NLP/opus-mt-en-zh", | ||
| 394 | + device="cpu", | ||
| 395 | + torch_dtype="float32", | ||
| 396 | + batch_size=2, | ||
| 397 | + max_input_length=24, | ||
| 398 | + max_new_tokens=24, | ||
| 399 | + num_beams=1, | ||
| 400 | + source_langs=["en"], | ||
| 401 | + target_langs=["zh"], | ||
| 402 | + ) | ||
| 403 | + | ||
| 404 | + texts = [ | ||
| 405 | + "one two three four, five six seven eight, nine ten eleven twelve.", | ||
| 406 | + "thirteen fourteen fifteen sixteen, seventeen eighteen nineteen twenty.", | ||
| 407 | + ] | ||
| 408 | + | ||
| 409 | + with caplog.at_level(logging.INFO): | ||
| 410 | + backend.translate(texts, source_lang="en", target_lang="zh") | ||
| 411 | + | ||
| 412 | + messages = [record.getMessage() for record in caplog.records] | ||
| 413 | + | ||
| 414 | + assert any(message.startswith("Translation segmentation summary |") for message in messages) | ||
| 415 | + inference_logs = [ | ||
| 416 | + message for message in messages if message.startswith("Translation inference batch |") | ||
| 417 | + ] | ||
| 418 | + assert len(inference_logs) >= 2 |
tests/test_translator_failure_semantics.py
| 1 | +import logging | ||
| 2 | + | ||
| 1 | from translation.cache import TranslationCache | 3 | from translation.cache import TranslationCache |
| 4 | +from translation.logging_utils import ( | ||
| 5 | + TranslationRequestFilter, | ||
| 6 | + bind_translation_request_id, | ||
| 7 | + reset_translation_request_id, | ||
| 8 | +) | ||
| 2 | from translation.service import TranslationService | 9 | from translation.service import TranslationService |
| 3 | 10 | ||
| 4 | 11 | ||
| @@ -107,3 +114,80 @@ def test_service_caches_all_capabilities(monkeypatch): | @@ -107,3 +114,80 @@ def test_service_caches_all_capabilities(monkeypatch): | ||
| 107 | ("opus-mt-zh-en", "en", "连衣裙", "opus-mt-zh-en:连衣裙"), | 114 | ("opus-mt-zh-en", "en", "连衣裙", "opus-mt-zh-en:连衣裙"), |
| 108 | ("opus-mt-zh-en", "en", "衬衫", "opus-mt-zh-en:衬衫"), | 115 | ("opus-mt-zh-en", "en", "衬衫", "opus-mt-zh-en:衬衫"), |
| 109 | ] | 116 | ] |
| 117 | + | ||
| 118 | + | ||
| 119 | +def test_translation_request_filter_injects_reqid(): | ||
| 120 | + reqid, token = bind_translation_request_id("req-test-1234567890") | ||
| 121 | + try: | ||
| 122 | + record = logging.LogRecord( | ||
| 123 | + name="translation.service", | ||
| 124 | + level=logging.INFO, | ||
| 125 | + pathname=__file__, | ||
| 126 | + lineno=1, | ||
| 127 | + msg="hello", | ||
| 128 | + args=(), | ||
| 129 | + exc_info=None, | ||
| 130 | + ) | ||
| 131 | + TranslationRequestFilter().filter(record) | ||
| 132 | + | ||
| 133 | + assert reqid == "req-test-1234567890" | ||
| 134 | + assert record.reqid == "req-test-1234567890" | ||
| 135 | + finally: | ||
| 136 | + reset_translation_request_id(token) | ||
| 137 | + | ||
| 138 | + | ||
| 139 | +def test_translation_route_log_focuses_on_routing_decision(monkeypatch, caplog): | ||
| 140 | + monkeypatch.setattr(TranslationCache, "_init_redis_client", staticmethod(lambda: None)) | ||
| 141 | + | ||
| 142 | + def _fake_create_backend(self, *, name, backend_type, cfg): | ||
| 143 | + del self, backend_type, cfg | ||
| 144 | + | ||
| 145 | + class _Backend: | ||
| 146 | + model = name | ||
| 147 | + | ||
| 148 | + @property | ||
| 149 | + def supports_batch(self): | ||
| 150 | + return True | ||
| 151 | + | ||
| 152 | + def translate(self, text, target_lang, source_lang=None, scene=None): | ||
| 153 | + del target_lang, source_lang, scene | ||
| 154 | + return text | ||
| 155 | + | ||
| 156 | + return _Backend() | ||
| 157 | + | ||
| 158 | + monkeypatch.setattr(TranslationService, "_create_backend", _fake_create_backend) | ||
| 159 | + service = TranslationService( | ||
| 160 | + { | ||
| 161 | + "service_url": "http://127.0.0.1:6006", | ||
| 162 | + "timeout_sec": 10.0, | ||
| 163 | + "default_model": "llm", | ||
| 164 | + "default_scene": "general", | ||
| 165 | + "capabilities": { | ||
| 166 | + "llm": { | ||
| 167 | + "enabled": True, | ||
| 168 | + "backend": "llm", | ||
| 169 | + "model": "dummy-llm", | ||
| 170 | + "base_url": "https://example.com", | ||
| 171 | + "timeout_sec": 10.0, | ||
| 172 | + "use_cache": True, | ||
| 173 | + } | ||
| 174 | + }, | ||
| 175 | + "cache": { | ||
| 176 | + "ttl_seconds": 60, | ||
| 177 | + "sliding_expiration": True, | ||
| 178 | + }, | ||
| 179 | + } | ||
| 180 | + ) | ||
| 181 | + | ||
| 182 | + with caplog.at_level(logging.INFO): | ||
| 183 | + service.translate("商品标题", target_lang="en", source_lang="zh", model="llm") | ||
| 184 | + | ||
| 185 | + route_messages = [ | ||
| 186 | + record.getMessage() | ||
| 187 | + for record in caplog.records | ||
| 188 | + if record.name == "translation.service" and record.getMessage().startswith("Translation route |") | ||
| 189 | + ] | ||
| 190 | + | ||
| 191 | + assert route_messages == [ | ||
| 192 | + "Translation route | backend=llm request_type=single use_cache=True cache_available=False" | ||
| 193 | + ] |
translation/backends/local_ctranslate2.py
| @@ -13,7 +13,12 @@ from typing import Dict, List, Optional, Sequence, Union | @@ -13,7 +13,12 @@ from typing import Dict, List, Optional, Sequence, Union | ||
| 13 | 13 | ||
| 14 | from transformers import AutoTokenizer | 14 | from transformers import AutoTokenizer |
| 15 | 15 | ||
| 16 | -from translation.languages import MARIAN_LANGUAGE_DIRECTIONS, NLLB_LANGUAGE_CODES | 16 | +from translation.languages import ( |
| 17 | + MARIAN_LANGUAGE_DIRECTIONS, | ||
| 18 | + build_nllb_language_catalog, | ||
| 19 | + normalize_language_key, | ||
| 20 | + resolve_nllb_language_code, | ||
| 21 | +) | ||
| 17 | from translation.text_splitter import ( | 22 | from translation.text_splitter import ( |
| 18 | compute_safe_input_token_limit, | 23 | compute_safe_input_token_limit, |
| 19 | join_translated_segments, | 24 | join_translated_segments, |
| @@ -23,6 +28,17 @@ from translation.text_splitter import ( | @@ -23,6 +28,17 @@ from translation.text_splitter import ( | ||
| 23 | logger = logging.getLogger(__name__) | 28 | logger = logging.getLogger(__name__) |
| 24 | 29 | ||
| 25 | 30 | ||
| 31 | +def _text_preview(text: Optional[str], limit: int = 32) -> str: | ||
| 32 | + return str(text or "").replace("\n", "\\n")[:limit] | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +def _summarize_lengths(values: Sequence[int]) -> str: | ||
| 36 | + if not values: | ||
| 37 | + return "[]" | ||
| 38 | + total = sum(values) | ||
| 39 | + return f"min={min(values)} max={max(values)} avg={total / len(values):.1f}" | ||
| 40 | + | ||
| 41 | + | ||
| 26 | def _resolve_device(device: Optional[str]) -> str: | 42 | def _resolve_device(device: Optional[str]) -> str: |
| 27 | value = str(device or "auto").strip().lower() | 43 | value = str(device or "auto").strip().lower() |
| 28 | if value not in {"auto", "cpu", "cuda"}: | 44 | if value not in {"auto", "cpu", "cuda"}: |
| @@ -285,6 +301,17 @@ class LocalCTranslate2TranslationBackend: | @@ -285,6 +301,17 @@ class LocalCTranslate2TranslationBackend: | ||
| 285 | source_tokens = self._encode_source_tokens(texts, source_lang, target_lang) | 301 | source_tokens = self._encode_source_tokens(texts, source_lang, target_lang) |
| 286 | target_prefix = self._target_prefixes(len(source_tokens), source_lang, target_lang) | 302 | target_prefix = self._target_prefixes(len(source_tokens), source_lang, target_lang) |
| 287 | max_decoding_length = self._resolve_max_decoding_length(source_tokens) | 303 | max_decoding_length = self._resolve_max_decoding_length(source_tokens) |
| 304 | + logger.info( | ||
| 305 | + "Translation model batch detail | model=%s segment_count=%s token_lengths=%s max_decoding_length=%s batch_type=%s beam_size=%s target_lang=%s source_lang=%s", | ||
| 306 | + self.model, | ||
| 307 | + len(source_tokens), | ||
| 308 | + _summarize_lengths([len(tokens) for tokens in source_tokens]), | ||
| 309 | + max_decoding_length, | ||
| 310 | + self.ct2_batch_type, | ||
| 311 | + self.num_beams, | ||
| 312 | + target_lang, | ||
| 313 | + source_lang or "auto", | ||
| 314 | + ) | ||
| 288 | results = self.translator.translate_batch( | 315 | results = self.translator.translate_batch( |
| 289 | source_tokens, | 316 | source_tokens, |
| 290 | target_prefix=target_prefix, | 317 | target_prefix=target_prefix, |
| @@ -336,6 +363,59 @@ class LocalCTranslate2TranslationBackend: | @@ -336,6 +363,59 @@ class LocalCTranslate2TranslationBackend: | ||
| 336 | ), | 363 | ), |
| 337 | ) | 364 | ) |
| 338 | 365 | ||
| 366 | + def _log_segmentation_summary( | ||
| 367 | + self, | ||
| 368 | + *, | ||
| 369 | + texts: Sequence[str], | ||
| 370 | + segment_plans: Sequence[Sequence[str]], | ||
| 371 | + target_lang: str, | ||
| 372 | + source_lang: Optional[str], | ||
| 373 | + ) -> None: | ||
| 374 | + non_empty_count = sum(1 for text in texts if text.strip()) | ||
| 375 | + segment_counts = [len(segments) for segments in segment_plans if segments] | ||
| 376 | + total_segments = sum(segment_counts) | ||
| 377 | + segmented_inputs = sum(1 for count in segment_counts if count > 1) | ||
| 378 | + logger.info( | ||
| 379 | + "Translation segmentation summary | model=%s inputs=%s non_empty_inputs=%s segmented_inputs=%s total_segments=%s batch_size=%s target_lang=%s source_lang=%s segments_per_input=%s", | ||
| 380 | + self.model, | ||
| 381 | + len(texts), | ||
| 382 | + non_empty_count, | ||
| 383 | + segmented_inputs, | ||
| 384 | + total_segments, | ||
| 385 | + self.batch_size, | ||
| 386 | + target_lang, | ||
| 387 | + source_lang or "auto", | ||
| 388 | + _summarize_lengths(segment_counts), | ||
| 389 | + ) | ||
| 390 | + | ||
| 391 | + def _translate_segment_batches( | ||
| 392 | + self, | ||
| 393 | + segments: List[str], | ||
| 394 | + target_lang: str, | ||
| 395 | + source_lang: Optional[str] = None, | ||
| 396 | + ) -> List[Optional[str]]: | ||
| 397 | + if not segments: | ||
| 398 | + return [] | ||
| 399 | + outputs: List[Optional[str]] = [] | ||
| 400 | + total_batches = (len(segments) + self.batch_size - 1) // self.batch_size | ||
| 401 | + for batch_index, start in enumerate(range(0, len(segments), self.batch_size), start=1): | ||
| 402 | + batch = segments[start:start + self.batch_size] | ||
| 403 | + logger.info( | ||
| 404 | + "Translation inference batch | model=%s batch_index=%s total_batches=%s segment_count=%s char_lengths=%s first_preview=%s target_lang=%s source_lang=%s", | ||
| 405 | + self.model, | ||
| 406 | + batch_index, | ||
| 407 | + total_batches, | ||
| 408 | + len(batch), | ||
| 409 | + _summarize_lengths([len(segment) for segment in batch]), | ||
| 410 | + _text_preview(batch[0] if batch else ""), | ||
| 411 | + target_lang, | ||
| 412 | + source_lang or "auto", | ||
| 413 | + ) | ||
| 414 | + outputs.extend( | ||
| 415 | + self._translate_batch(batch, target_lang=target_lang, source_lang=source_lang) | ||
| 416 | + ) | ||
| 417 | + return outputs | ||
| 418 | + | ||
| 339 | def _translate_with_segmentation( | 419 | def _translate_with_segmentation( |
| 340 | self, | 420 | self, |
| 341 | texts: List[str], | 421 | texts: List[str], |
| @@ -352,8 +432,15 @@ class LocalCTranslate2TranslationBackend: | @@ -352,8 +432,15 @@ class LocalCTranslate2TranslationBackend: | ||
| 352 | segment_plans.append(segments) | 432 | segment_plans.append(segments) |
| 353 | flat_segments.extend(segments) | 433 | flat_segments.extend(segments) |
| 354 | 434 | ||
| 435 | + self._log_segmentation_summary( | ||
| 436 | + texts=texts, | ||
| 437 | + segment_plans=segment_plans, | ||
| 438 | + target_lang=target_lang, | ||
| 439 | + source_lang=source_lang, | ||
| 440 | + ) | ||
| 441 | + | ||
| 355 | translated_segments = ( | 442 | translated_segments = ( |
| 356 | - self._translate_batch(flat_segments, target_lang=target_lang, source_lang=source_lang) | 443 | + self._translate_segment_batches(flat_segments, target_lang=target_lang, source_lang=source_lang) |
| 357 | if flat_segments | 444 | if flat_segments |
| 358 | else [] | 445 | else [] |
| 359 | ) | 446 | ) |
| @@ -387,13 +474,10 @@ class LocalCTranslate2TranslationBackend: | @@ -387,13 +474,10 @@ class LocalCTranslate2TranslationBackend: | ||
| 387 | del scene | 474 | del scene |
| 388 | is_single = isinstance(text, str) | 475 | is_single = isinstance(text, str) |
| 389 | texts = self._normalize_texts(text) | 476 | texts = self._normalize_texts(text) |
| 390 | - outputs: List[Optional[str]] = [] | ||
| 391 | - for start in range(0, len(texts), self.batch_size): | ||
| 392 | - chunk = texts[start:start + self.batch_size] | ||
| 393 | - if not any(item.strip() for item in chunk): | ||
| 394 | - outputs.extend([None if not item.strip() else item for item in chunk]) # type: ignore[list-item] | ||
| 395 | - continue | ||
| 396 | - outputs.extend(self._translate_with_segmentation(chunk, target_lang=target_lang, source_lang=source_lang)) | 477 | + if not any(item.strip() for item in texts): |
| 478 | + outputs = [None if not item.strip() else item for item in texts] # type: ignore[list-item] | ||
| 479 | + return outputs[0] if is_single else outputs | ||
| 480 | + outputs = self._translate_with_segmentation(texts, target_lang=target_lang, source_lang=source_lang) | ||
| 397 | return outputs[0] if is_single else outputs | 481 | return outputs[0] if is_single else outputs |
| 398 | 482 | ||
| 399 | 483 | ||
| @@ -492,11 +576,7 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | @@ -492,11 +576,7 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | ||
| 492 | ct2_decoding_length_extra: int = 0, | 576 | ct2_decoding_length_extra: int = 0, |
| 493 | ct2_decoding_length_min: int = 1, | 577 | ct2_decoding_length_min: int = 1, |
| 494 | ) -> None: | 578 | ) -> None: |
| 495 | - overrides = language_codes or {} | ||
| 496 | - self.language_codes = { | ||
| 497 | - **NLLB_LANGUAGE_CODES, | ||
| 498 | - **{str(k).strip().lower(): str(v).strip() for k, v in overrides.items() if str(k).strip()}, | ||
| 499 | - } | 579 | + self.language_codes = build_nllb_language_catalog(language_codes) |
| 500 | self._tokenizers_by_source: Dict[str, object] = {} | 580 | self._tokenizers_by_source: Dict[str, object] = {} |
| 501 | super().__init__( | 581 | super().__init__( |
| 502 | name=name, | 582 | name=name, |
| @@ -522,17 +602,17 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | @@ -522,17 +602,17 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | ||
| 522 | ) | 602 | ) |
| 523 | 603 | ||
| 524 | def _validate_languages(self, source_lang: Optional[str], target_lang: str) -> None: | 604 | def _validate_languages(self, source_lang: Optional[str], target_lang: str) -> None: |
| 525 | - src = str(source_lang or "").strip().lower() | ||
| 526 | - tgt = str(target_lang or "").strip().lower() | ||
| 527 | - if not src: | 605 | + if not str(source_lang or "").strip(): |
| 528 | raise ValueError(f"Model '{self.model}' requires source_lang") | 606 | raise ValueError(f"Model '{self.model}' requires source_lang") |
| 529 | - if src not in self.language_codes: | 607 | + if resolve_nllb_language_code(source_lang, self.language_codes) is None: |
| 530 | raise ValueError(f"Unsupported NLLB source language: {source_lang}") | 608 | raise ValueError(f"Unsupported NLLB source language: {source_lang}") |
| 531 | - if tgt not in self.language_codes: | 609 | + if resolve_nllb_language_code(target_lang, self.language_codes) is None: |
| 532 | raise ValueError(f"Unsupported NLLB target language: {target_lang}") | 610 | raise ValueError(f"Unsupported NLLB target language: {target_lang}") |
| 533 | 611 | ||
| 534 | def _get_tokenizer_for_source(self, source_lang: str): | 612 | def _get_tokenizer_for_source(self, source_lang: str): |
| 535 | - src_code = self.language_codes[source_lang] | 613 | + src_code = resolve_nllb_language_code(source_lang, self.language_codes) |
| 614 | + if src_code is None: | ||
| 615 | + raise ValueError(f"Unsupported NLLB source language: {source_lang}") | ||
| 536 | with self._tokenizer_lock: | 616 | with self._tokenizer_lock: |
| 537 | tokenizer = self._tokenizers_by_source.get(src_code) | 617 | tokenizer = self._tokenizers_by_source.get(src_code) |
| 538 | if tokenizer is None: | 618 | if tokenizer is None: |
| @@ -549,7 +629,7 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | @@ -549,7 +629,7 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | ||
| 549 | target_lang: str, | 629 | target_lang: str, |
| 550 | ) -> List[List[str]]: | 630 | ) -> List[List[str]]: |
| 551 | del target_lang | 631 | del target_lang |
| 552 | - source_key = str(source_lang or "").strip().lower() | 632 | + source_key = normalize_language_key(source_lang) |
| 553 | tokenizer = self._get_tokenizer_for_source(source_key) | 633 | tokenizer = self._get_tokenizer_for_source(source_key) |
| 554 | encoded = tokenizer( | 634 | encoded = tokenizer( |
| 555 | texts, | 635 | texts, |
| @@ -567,7 +647,9 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | @@ -567,7 +647,9 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | ||
| 567 | target_lang: str, | 647 | target_lang: str, |
| 568 | ) -> Optional[List[Optional[List[str]]]]: | 648 | ) -> Optional[List[Optional[List[str]]]]: |
| 569 | del source_lang | 649 | del source_lang |
| 570 | - tgt_code = self.language_codes[str(target_lang).strip().lower()] | 650 | + tgt_code = resolve_nllb_language_code(target_lang, self.language_codes) |
| 651 | + if tgt_code is None: | ||
| 652 | + raise ValueError(f"Unsupported NLLB target language: {target_lang}") | ||
| 571 | return [[tgt_code] for _ in range(count)] | 653 | return [[tgt_code] for _ in range(count)] |
| 572 | 654 | ||
| 573 | def _postprocess_hypothesis( | 655 | def _postprocess_hypothesis( |
| @@ -577,7 +659,9 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | @@ -577,7 +659,9 @@ class NLLBCTranslate2TranslationBackend(LocalCTranslate2TranslationBackend): | ||
| 577 | target_lang: str, | 659 | target_lang: str, |
| 578 | ) -> List[str]: | 660 | ) -> List[str]: |
| 579 | del source_lang | 661 | del source_lang |
| 580 | - tgt_code = self.language_codes[str(target_lang).strip().lower()] | 662 | + tgt_code = resolve_nllb_language_code(target_lang, self.language_codes) |
| 663 | + if tgt_code is None: | ||
| 664 | + raise ValueError(f"Unsupported NLLB target language: {target_lang}") | ||
| 581 | if tokens and tokens[0] == tgt_code: | 665 | if tokens and tokens[0] == tgt_code: |
| 582 | return tokens[1:] | 666 | return tokens[1:] |
| 583 | return tokens | 667 | return tokens |
translation/backends/local_seq2seq.py
| @@ -10,7 +10,11 @@ from typing import Dict, List, Optional, Sequence, Union | @@ -10,7 +10,11 @@ from typing import Dict, List, Optional, Sequence, Union | ||
| 10 | import torch | 10 | import torch |
| 11 | from transformers import AutoModelForSeq2SeqLM, AutoTokenizer | 11 | from transformers import AutoModelForSeq2SeqLM, AutoTokenizer |
| 12 | 12 | ||
| 13 | -from translation.languages import MARIAN_LANGUAGE_DIRECTIONS, NLLB_LANGUAGE_CODES | 13 | +from translation.languages import ( |
| 14 | + MARIAN_LANGUAGE_DIRECTIONS, | ||
| 15 | + build_nllb_language_catalog, | ||
| 16 | + resolve_nllb_language_code, | ||
| 17 | +) | ||
| 14 | from translation.text_splitter import ( | 18 | from translation.text_splitter import ( |
| 15 | compute_safe_input_token_limit, | 19 | compute_safe_input_token_limit, |
| 16 | join_translated_segments, | 20 | join_translated_segments, |
| @@ -20,6 +24,17 @@ from translation.text_splitter import ( | @@ -20,6 +24,17 @@ from translation.text_splitter import ( | ||
| 20 | logger = logging.getLogger(__name__) | 24 | logger = logging.getLogger(__name__) |
| 21 | 25 | ||
| 22 | 26 | ||
| 27 | +def _text_preview(text: Optional[str], limit: int = 32) -> str: | ||
| 28 | + return str(text or "").replace("\n", "\\n")[:limit] | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +def _summarize_lengths(values: Sequence[int]) -> str: | ||
| 32 | + if not values: | ||
| 33 | + return "[]" | ||
| 34 | + total = sum(values) | ||
| 35 | + return f"min={min(values)} max={max(values)} avg={total / len(values):.1f}" | ||
| 36 | + | ||
| 37 | + | ||
| 23 | def _resolve_device(device: Optional[str]) -> str: | 38 | def _resolve_device(device: Optional[str]) -> str: |
| 24 | value = str(device or "auto").strip().lower() | 39 | value = str(device or "auto").strip().lower() |
| 25 | if value == "auto": | 40 | if value == "auto": |
| @@ -198,6 +213,59 @@ class LocalSeq2SeqTranslationBackend: | @@ -198,6 +213,59 @@ class LocalSeq2SeqTranslationBackend: | ||
| 198 | ), | 213 | ), |
| 199 | ) | 214 | ) |
| 200 | 215 | ||
| 216 | + def _log_segmentation_summary( | ||
| 217 | + self, | ||
| 218 | + *, | ||
| 219 | + texts: Sequence[str], | ||
| 220 | + segment_plans: Sequence[Sequence[str]], | ||
| 221 | + target_lang: str, | ||
| 222 | + source_lang: Optional[str], | ||
| 223 | + ) -> None: | ||
| 224 | + non_empty_count = sum(1 for text in texts if text.strip()) | ||
| 225 | + segment_counts = [len(segments) for segments in segment_plans if segments] | ||
| 226 | + total_segments = sum(segment_counts) | ||
| 227 | + segmented_inputs = sum(1 for count in segment_counts if count > 1) | ||
| 228 | + logger.info( | ||
| 229 | + "Translation segmentation summary | model=%s inputs=%s non_empty_inputs=%s segmented_inputs=%s total_segments=%s batch_size=%s target_lang=%s source_lang=%s segments_per_input=%s", | ||
| 230 | + self.model, | ||
| 231 | + len(texts), | ||
| 232 | + non_empty_count, | ||
| 233 | + segmented_inputs, | ||
| 234 | + total_segments, | ||
| 235 | + self.batch_size, | ||
| 236 | + target_lang, | ||
| 237 | + source_lang or "auto", | ||
| 238 | + _summarize_lengths(segment_counts), | ||
| 239 | + ) | ||
| 240 | + | ||
| 241 | + def _translate_segment_batches( | ||
| 242 | + self, | ||
| 243 | + segments: List[str], | ||
| 244 | + target_lang: str, | ||
| 245 | + source_lang: Optional[str] = None, | ||
| 246 | + ) -> List[Optional[str]]: | ||
| 247 | + if not segments: | ||
| 248 | + return [] | ||
| 249 | + outputs: List[Optional[str]] = [] | ||
| 250 | + total_batches = (len(segments) + self.batch_size - 1) // self.batch_size | ||
| 251 | + for batch_index, start in enumerate(range(0, len(segments), self.batch_size), start=1): | ||
| 252 | + batch = segments[start:start + self.batch_size] | ||
| 253 | + logger.info( | ||
| 254 | + "Translation inference batch | model=%s batch_index=%s total_batches=%s segment_count=%s char_lengths=%s first_preview=%s target_lang=%s source_lang=%s", | ||
| 255 | + self.model, | ||
| 256 | + batch_index, | ||
| 257 | + total_batches, | ||
| 258 | + len(batch), | ||
| 259 | + _summarize_lengths([len(segment) for segment in batch]), | ||
| 260 | + _text_preview(batch[0] if batch else ""), | ||
| 261 | + target_lang, | ||
| 262 | + source_lang or "auto", | ||
| 263 | + ) | ||
| 264 | + outputs.extend( | ||
| 265 | + self._translate_batch(batch, target_lang=target_lang, source_lang=source_lang) | ||
| 266 | + ) | ||
| 267 | + return outputs | ||
| 268 | + | ||
| 201 | def _translate_with_segmentation( | 269 | def _translate_with_segmentation( |
| 202 | self, | 270 | self, |
| 203 | texts: List[str], | 271 | texts: List[str], |
| @@ -214,8 +282,15 @@ class LocalSeq2SeqTranslationBackend: | @@ -214,8 +282,15 @@ class LocalSeq2SeqTranslationBackend: | ||
| 214 | segment_plans.append(segments) | 282 | segment_plans.append(segments) |
| 215 | flat_segments.extend(segments) | 283 | flat_segments.extend(segments) |
| 216 | 284 | ||
| 285 | + self._log_segmentation_summary( | ||
| 286 | + texts=texts, | ||
| 287 | + segment_plans=segment_plans, | ||
| 288 | + target_lang=target_lang, | ||
| 289 | + source_lang=source_lang, | ||
| 290 | + ) | ||
| 291 | + | ||
| 217 | translated_segments = ( | 292 | translated_segments = ( |
| 218 | - self._translate_batch(flat_segments, target_lang=target_lang, source_lang=source_lang) | 293 | + self._translate_segment_batches(flat_segments, target_lang=target_lang, source_lang=source_lang) |
| 219 | if flat_segments | 294 | if flat_segments |
| 220 | else [] | 295 | else [] |
| 221 | ) | 296 | ) |
| @@ -249,13 +324,10 @@ class LocalSeq2SeqTranslationBackend: | @@ -249,13 +324,10 @@ class LocalSeq2SeqTranslationBackend: | ||
| 249 | del scene | 324 | del scene |
| 250 | is_single = isinstance(text, str) | 325 | is_single = isinstance(text, str) |
| 251 | texts = self._normalize_texts(text) | 326 | texts = self._normalize_texts(text) |
| 252 | - outputs: List[Optional[str]] = [] | ||
| 253 | - for start in range(0, len(texts), self.batch_size): | ||
| 254 | - chunk = texts[start:start + self.batch_size] | ||
| 255 | - if not any(item.strip() for item in chunk): | ||
| 256 | - outputs.extend([None if not item.strip() else item for item in chunk]) # type: ignore[list-item] | ||
| 257 | - continue | ||
| 258 | - outputs.extend(self._translate_with_segmentation(chunk, target_lang=target_lang, source_lang=source_lang)) | 327 | + if not any(item.strip() for item in texts): |
| 328 | + outputs = [None if not item.strip() else item for item in texts] # type: ignore[list-item] | ||
| 329 | + return outputs[0] if is_single else outputs | ||
| 330 | + outputs = self._translate_with_segmentation(texts, target_lang=target_lang, source_lang=source_lang) | ||
| 259 | return outputs[0] if is_single else outputs | 331 | return outputs[0] if is_single else outputs |
| 260 | 332 | ||
| 261 | 333 | ||
| @@ -324,11 +396,7 @@ class NLLBTranslationBackend(LocalSeq2SeqTranslationBackend): | @@ -324,11 +396,7 @@ class NLLBTranslationBackend(LocalSeq2SeqTranslationBackend): | ||
| 324 | language_codes: Optional[Dict[str, str]] = None, | 396 | language_codes: Optional[Dict[str, str]] = None, |
| 325 | attn_implementation: Optional[str] = None, | 397 | attn_implementation: Optional[str] = None, |
| 326 | ) -> None: | 398 | ) -> None: |
| 327 | - overrides = language_codes or {} | ||
| 328 | - self.language_codes = { | ||
| 329 | - **NLLB_LANGUAGE_CODES, | ||
| 330 | - **{str(k).strip().lower(): str(v).strip() for k, v in overrides.items() if str(k).strip()}, | ||
| 331 | - } | 399 | + self.language_codes = build_nllb_language_catalog(language_codes) |
| 332 | super().__init__( | 400 | super().__init__( |
| 333 | name=name, | 401 | name=name, |
| 334 | model_id=model_id, | 402 | model_id=model_id, |
| @@ -343,24 +411,26 @@ class NLLBTranslationBackend(LocalSeq2SeqTranslationBackend): | @@ -343,24 +411,26 @@ class NLLBTranslationBackend(LocalSeq2SeqTranslationBackend): | ||
| 343 | ) | 411 | ) |
| 344 | 412 | ||
| 345 | def _validate_languages(self, source_lang: Optional[str], target_lang: str) -> None: | 413 | def _validate_languages(self, source_lang: Optional[str], target_lang: str) -> None: |
| 346 | - src = str(source_lang or "").strip().lower() | ||
| 347 | - tgt = str(target_lang or "").strip().lower() | ||
| 348 | - if not src: | 414 | + if not str(source_lang or "").strip(): |
| 349 | raise ValueError(f"Model '{self.model}' requires source_lang") | 415 | raise ValueError(f"Model '{self.model}' requires source_lang") |
| 350 | - if src not in self.language_codes: | 416 | + if resolve_nllb_language_code(source_lang, self.language_codes) is None: |
| 351 | raise ValueError(f"Unsupported NLLB source language: {source_lang}") | 417 | raise ValueError(f"Unsupported NLLB source language: {source_lang}") |
| 352 | - if tgt not in self.language_codes: | 418 | + if resolve_nllb_language_code(target_lang, self.language_codes) is None: |
| 353 | raise ValueError(f"Unsupported NLLB target language: {target_lang}") | 419 | raise ValueError(f"Unsupported NLLB target language: {target_lang}") |
| 354 | 420 | ||
| 355 | def _prepare_tokenizer(self, source_lang: Optional[str], target_lang: str) -> Dict[str, object]: | 421 | def _prepare_tokenizer(self, source_lang: Optional[str], target_lang: str) -> Dict[str, object]: |
| 356 | del target_lang | 422 | del target_lang |
| 357 | - src_code = self.language_codes[str(source_lang).strip().lower()] | 423 | + src_code = resolve_nllb_language_code(source_lang, self.language_codes) |
| 424 | + if src_code is None: | ||
| 425 | + raise ValueError(f"Unsupported NLLB source language: {source_lang}") | ||
| 358 | self.tokenizer.src_lang = src_code | 426 | self.tokenizer.src_lang = src_code |
| 359 | return {} | 427 | return {} |
| 360 | 428 | ||
| 361 | def _build_generate_kwargs(self, source_lang: Optional[str], target_lang: str) -> Dict[str, object]: | 429 | def _build_generate_kwargs(self, source_lang: Optional[str], target_lang: str) -> Dict[str, object]: |
| 362 | del source_lang | 430 | del source_lang |
| 363 | - tgt_code = self.language_codes[str(target_lang).strip().lower()] | 431 | + tgt_code = resolve_nllb_language_code(target_lang, self.language_codes) |
| 432 | + if tgt_code is None: | ||
| 433 | + raise ValueError(f"Unsupported NLLB target language: {target_lang}") | ||
| 364 | forced_bos_token_id = None | 434 | forced_bos_token_id = None |
| 365 | if hasattr(self.tokenizer, "lang_code_to_id"): | 435 | if hasattr(self.tokenizer, "lang_code_to_id"): |
| 366 | forced_bos_token_id = self.tokenizer.lang_code_to_id.get(tgt_code) | 436 | forced_bos_token_id = self.tokenizer.lang_code_to_id.get(tgt_code) |
translation/cache.py
| @@ -40,10 +40,8 @@ class TranslationCache: | @@ -40,10 +40,8 @@ class TranslationCache: | ||
| 40 | try: | 40 | try: |
| 41 | value = self.redis_client.get(key) | 41 | value = self.redis_client.get(key) |
| 42 | logger.info( | 42 | logger.info( |
| 43 | - "Translation cache %s | model=%s target_lang=%s text_len=%s key=%s", | 43 | + "Translation cache %s | text_len=%s key=%s", |
| 44 | "hit" if value is not None else "miss", | 44 | "hit" if value is not None else "miss", |
| 45 | - model, | ||
| 46 | - target_lang, | ||
| 47 | len(str(source_text or "")), | 45 | len(str(source_text or "")), |
| 48 | key, | 46 | key, |
| 49 | ) | 47 | ) |
| @@ -61,9 +59,7 @@ class TranslationCache: | @@ -61,9 +59,7 @@ class TranslationCache: | ||
| 61 | try: | 59 | try: |
| 62 | self.redis_client.setex(key, self.ttl_seconds, translated_text) | 60 | self.redis_client.setex(key, self.ttl_seconds, translated_text) |
| 63 | logger.info( | 61 | logger.info( |
| 64 | - "Translation cache write | model=%s target_lang=%s text_len=%s result_len=%s ttl_seconds=%s key=%s", | ||
| 65 | - model, | ||
| 66 | - target_lang, | 62 | + "Translation cache write | text_len=%s result_len=%s ttl_seconds=%s key=%s", |
| 67 | len(str(source_text or "")), | 63 | len(str(source_text or "")), |
| 68 | len(str(translated_text or "")), | 64 | len(str(translated_text or "")), |
| 69 | self.ttl_seconds, | 65 | self.ttl_seconds, |
translation/languages.py
| @@ -2,12 +2,13 @@ | @@ -2,12 +2,13 @@ | ||
| 2 | 2 | ||
| 3 | from __future__ import annotations | 3 | from __future__ import annotations |
| 4 | 4 | ||
| 5 | -from typing import Dict, Tuple | 5 | +from typing import Dict, Mapping, Optional, Tuple |
| 6 | 6 | ||
| 7 | 7 | ||
| 8 | LANGUAGE_LABELS: Dict[str, str] = { | 8 | LANGUAGE_LABELS: Dict[str, str] = { |
| 9 | "zh": "Chinese", | 9 | "zh": "Chinese", |
| 10 | "en": "English", | 10 | "en": "English", |
| 11 | + "fi": "Finnish", | ||
| 11 | "ru": "Russian", | 12 | "ru": "Russian", |
| 12 | "ar": "Arabic", | 13 | "ar": "Arabic", |
| 13 | "ja": "Japanese", | 14 | "ja": "Japanese", |
| @@ -49,6 +50,7 @@ DEEPL_LANGUAGE_CODES: Dict[str, str] = { | @@ -49,6 +50,7 @@ DEEPL_LANGUAGE_CODES: Dict[str, str] = { | ||
| 49 | 50 | ||
| 50 | NLLB_LANGUAGE_CODES: Dict[str, str] = { | 51 | NLLB_LANGUAGE_CODES: Dict[str, str] = { |
| 51 | "en": "eng_Latn", | 52 | "en": "eng_Latn", |
| 53 | + "fi": "fin_Latn", | ||
| 52 | "zh": "zho_Hans", | 54 | "zh": "zho_Hans", |
| 53 | "ru": "rus_Cyrl", | 55 | "ru": "rus_Cyrl", |
| 54 | "ar": "arb_Arab", | 56 | "ar": "arb_Arab", |
| @@ -65,3 +67,56 @@ MARIAN_LANGUAGE_DIRECTIONS: Dict[str, Tuple[str, str]] = { | @@ -65,3 +67,56 @@ MARIAN_LANGUAGE_DIRECTIONS: Dict[str, Tuple[str, str]] = { | ||
| 65 | "opus-mt-zh-en": ("zh", "en"), | 67 | "opus-mt-zh-en": ("zh", "en"), |
| 66 | "opus-mt-en-zh": ("en", "zh"), | 68 | "opus-mt-en-zh": ("en", "zh"), |
| 67 | } | 69 | } |
| 70 | + | ||
| 71 | + | ||
| 72 | +NLLB_LANGUAGE_ALIASES: Dict[str, str] = { | ||
| 73 | + "fi_fi": "fi", | ||
| 74 | + "fin": "fi", | ||
| 75 | + "fin_fin": "fi", | ||
| 76 | + "zh_cn": "zh", | ||
| 77 | + "zh_hans": "zh", | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | + | ||
| 81 | +def normalize_language_key(language: Optional[str]) -> str: | ||
| 82 | + return str(language or "").strip().lower().replace("-", "_") | ||
| 83 | + | ||
| 84 | + | ||
| 85 | +def build_nllb_language_catalog( | ||
| 86 | + overrides: Optional[Mapping[str, str]] = None, | ||
| 87 | +) -> Dict[str, str]: | ||
| 88 | + catalog = { | ||
| 89 | + normalize_language_key(key): str(value).strip() | ||
| 90 | + for key, value in NLLB_LANGUAGE_CODES.items() | ||
| 91 | + if str(key).strip() | ||
| 92 | + } | ||
| 93 | + for key, value in (overrides or {}).items(): | ||
| 94 | + normalized_key = normalize_language_key(key) | ||
| 95 | + if normalized_key: | ||
| 96 | + catalog[normalized_key] = str(value).strip() | ||
| 97 | + return catalog | ||
| 98 | + | ||
| 99 | + | ||
| 100 | +def resolve_nllb_language_code( | ||
| 101 | + language: Optional[str], | ||
| 102 | + language_codes: Optional[Mapping[str, str]] = None, | ||
| 103 | +) -> Optional[str]: | ||
| 104 | + normalized = normalize_language_key(language) | ||
| 105 | + if not normalized: | ||
| 106 | + return None | ||
| 107 | + | ||
| 108 | + catalog = build_nllb_language_catalog(language_codes) | ||
| 109 | + direct = catalog.get(normalized) | ||
| 110 | + if direct is not None: | ||
| 111 | + return direct | ||
| 112 | + | ||
| 113 | + alias = NLLB_LANGUAGE_ALIASES.get(normalized) | ||
| 114 | + if alias is not None: | ||
| 115 | + aliased = catalog.get(normalize_language_key(alias)) | ||
| 116 | + if aliased is not None: | ||
| 117 | + return aliased | ||
| 118 | + | ||
| 119 | + for code in catalog.values(): | ||
| 120 | + if normalize_language_key(code) == normalized: | ||
| 121 | + return code | ||
| 122 | + return None |
| @@ -0,0 +1,37 @@ | @@ -0,0 +1,37 @@ | ||
| 1 | +"""Shared translation logging context helpers.""" | ||
| 2 | + | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +import contextvars | ||
| 6 | +import logging | ||
| 7 | +import uuid | ||
| 8 | +from typing import Optional | ||
| 9 | + | ||
| 10 | + | ||
| 11 | +_translation_request_id_var: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar( | ||
| 12 | + "translation_request_id", | ||
| 13 | + default=None, | ||
| 14 | +) | ||
| 15 | + | ||
| 16 | + | ||
| 17 | +def current_translation_request_id() -> str: | ||
| 18 | + return _translation_request_id_var.get() or "-1" | ||
| 19 | + | ||
| 20 | + | ||
| 21 | +def bind_translation_request_id(request_id: Optional[str] = None) -> tuple[str, contextvars.Token]: | ||
| 22 | + raw_value = str(request_id or "").strip() | ||
| 23 | + normalized = raw_value[:32] if raw_value else str(uuid.uuid4())[:8] | ||
| 24 | + return normalized, _translation_request_id_var.set(normalized) | ||
| 25 | + | ||
| 26 | + | ||
| 27 | +def reset_translation_request_id(token: contextvars.Token) -> None: | ||
| 28 | + _translation_request_id_var.reset(token) | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +class TranslationRequestFilter(logging.Filter): | ||
| 32 | + """Inject a request id into translator logs when one is available.""" | ||
| 33 | + | ||
| 34 | + def filter(self, record: logging.LogRecord) -> bool: | ||
| 35 | + if not hasattr(record, "reqid"): | ||
| 36 | + record.reqid = current_translation_request_id() | ||
| 37 | + return True |
translation/service.py
| @@ -198,15 +198,10 @@ class TranslationService: | @@ -198,15 +198,10 @@ class TranslationService: | ||
| 198 | active_scene = normalize_translation_scene(self.config, scene) | 198 | active_scene = normalize_translation_scene(self.config, scene) |
| 199 | capability_cfg = self._enabled_capabilities[normalized_model] | 199 | capability_cfg = self._enabled_capabilities[normalized_model] |
| 200 | use_cache = bool(capability_cfg.get("use_cache")) | 200 | use_cache = bool(capability_cfg.get("use_cache")) |
| 201 | - text_count = 1 if isinstance(text, str) else len(list(text)) | ||
| 202 | logger.info( | 201 | logger.info( |
| 203 | - "Translation route | model=%s backend=%s scene=%s target_lang=%s source_lang=%s count=%s use_cache=%s cache_available=%s", | ||
| 204 | - normalized_model, | 202 | + "Translation route | backend=%s request_type=%s use_cache=%s cache_available=%s", |
| 205 | getattr(backend, "model", normalized_model), | 203 | getattr(backend, "model", normalized_model), |
| 206 | - active_scene, | ||
| 207 | - target_lang, | ||
| 208 | - source_lang or "auto", | ||
| 209 | - text_count, | 204 | + "single" if isinstance(text, str) else "batch", |
| 210 | use_cache, | 205 | use_cache, |
| 211 | self._translation_cache.available, | 206 | self._translation_cache.available, |
| 212 | ) | 207 | ) |
| @@ -252,11 +247,7 @@ class TranslationService: | @@ -252,11 +247,7 @@ class TranslationService: | ||
| 252 | cached = self._translation_cache.get(model=model, target_lang=target_lang, source_text=text) | 247 | cached = self._translation_cache.get(model=model, target_lang=target_lang, source_text=text) |
| 253 | if cached is not None: | 248 | if cached is not None: |
| 254 | logger.info( | 249 | logger.info( |
| 255 | - "Translation cache served | model=%s scene=%s target_lang=%s source_lang=%s text_len=%s", | ||
| 256 | - model, | ||
| 257 | - scene, | ||
| 258 | - target_lang, | ||
| 259 | - source_lang or "auto", | 250 | + "Translation cache served | request_type=single text_len=%s", |
| 260 | len(text), | 251 | len(text), |
| 261 | ) | 252 | ) |
| 262 | return cached | 253 | return cached |
| @@ -274,21 +265,13 @@ class TranslationService: | @@ -274,21 +265,13 @@ class TranslationService: | ||
| 274 | translated_text=translated, | 265 | translated_text=translated, |
| 275 | ) | 266 | ) |
| 276 | logger.info( | 267 | logger.info( |
| 277 | - "Translation backend result cached | model=%s scene=%s target_lang=%s source_lang=%s text_len=%s result_len=%s", | ||
| 278 | - model, | ||
| 279 | - scene, | ||
| 280 | - target_lang, | ||
| 281 | - source_lang or "auto", | 268 | + "Translation backend result cached | request_type=single text_len=%s result_len=%s", |
| 282 | len(text), | 269 | len(text), |
| 283 | len(str(translated)), | 270 | len(str(translated)), |
| 284 | ) | 271 | ) |
| 285 | else: | 272 | else: |
| 286 | logger.warning( | 273 | logger.warning( |
| 287 | - "Translation backend returned empty result | model=%s scene=%s target_lang=%s source_lang=%s text_len=%s", | ||
| 288 | - model, | ||
| 289 | - scene, | ||
| 290 | - target_lang, | ||
| 291 | - source_lang or "auto", | 274 | + "Translation backend returned empty result | request_type=single text_len=%s", |
| 292 | len(text), | 275 | len(text), |
| 293 | ) | 276 | ) |
| 294 | return translated | 277 | return translated |
| @@ -327,11 +310,7 @@ class TranslationService: | @@ -327,11 +310,7 @@ class TranslationService: | ||
| 327 | miss_indices.append(idx) | 310 | miss_indices.append(idx) |
| 328 | 311 | ||
| 329 | logger.info( | 312 | logger.info( |
| 330 | - "Translation batch cache summary | model=%s scene=%s target_lang=%s source_lang=%s total=%s cache_hits=%s cache_misses=%s", | ||
| 331 | - model, | ||
| 332 | - scene, | ||
| 333 | - target_lang, | ||
| 334 | - source_lang or "auto", | 313 | + "Translation batch cache summary | total=%s cache_hits=%s cache_misses=%s", |
| 335 | len(texts), | 314 | len(texts), |
| 336 | cache_hits, | 315 | cache_hits, |
| 337 | len(misses), | 316 | len(misses), |
| @@ -356,11 +335,7 @@ class TranslationService: | @@ -356,11 +335,7 @@ class TranslationService: | ||
| 356 | ) | 335 | ) |
| 357 | else: | 336 | else: |
| 358 | logger.warning( | 337 | logger.warning( |
| 359 | - "Translation batch item returned empty result | model=%s scene=%s target_lang=%s source_lang=%s item_index=%s text_len=%s", | ||
| 360 | - model, | ||
| 361 | - scene, | ||
| 362 | - target_lang, | ||
| 363 | - source_lang or "auto", | 338 | + "Translation batch item returned empty result | item_index=%s text_len=%s", |
| 364 | idx, | 339 | idx, |
| 365 | len(original_text), | 340 | len(original_text), |
| 366 | ) | 341 | ) |