diff --git a/backend/app/services/translation/service.py b/backend/app/services/translation/service.py index fcee088..4cf18b0 100644 --- a/backend/app/services/translation/service.py +++ b/backend/app/services/translation/service.py @@ -1,8 +1,8 @@ """翻译服务门面:配额检查 + 缓存 + 引擎选择 + 月度计数。 引擎链路(优先级降序): -1. spark(主,Lite 免费;spark_api_password 配了才用) -2. zhipu(第二序位,GLM-4-Flash 免费;zhipu_api_key 配了才用) +1. zhipu(主,GLM-4-Flash 免费;zhipu_api_key 配了才用) +2. spark(第二序位,Lite 免费;spark_api_password 配了才用) 3. tencent TMT(第三级,按月配额;快满时主动切走) 4. tencent_maas(备用,OpenAI 兼容,无配额;主失败/TMT 配额耗尽时启用) 5. agnes(第四级,通用 LLM 做翻译;MaaS 不可用时启用 — 质量次之但够用) @@ -12,8 +12,9 @@ - TMT 是按月计费的(腾讯云后台可能计费口径是请求字节,我们 redis 累加的是字符数, 差异约 2-3x);用户从腾讯云后台看"已用 2M"时,我们 redis 显示约 80 万字符 - 用户决策:以腾讯云后台数字为准,快满时降级 -- spark / zhipu 都是免费模型,默认优先;不可用时降级到 tencent(继续吃配额)。 +- zhipu / spark 都是免费模型,默认优先;不可用时降级到 tencent(继续吃配额)。 想要完全绕开 tencent,把 TENCENTCLOUD_SECRET_ID 留空即可。 +- 智谱放第一是因为它家 GLM-4-Flash 翻译质量比星火 Lite 更稳,星火降为二级 """ from __future__ import annotations @@ -58,18 +59,8 @@ class TranslationService: # 串行:1 个并发;避免触发腾讯 TMT 限速 self._sem = asyncio.Semaphore(1) - def _spark_translator(self) -> BaseTranslator | None: - """主引擎:星火 Spark(Lite 免费)。配了 spark_api_password 才启用。""" - if self._spark is None and settings.spark_api_password: - try: - self._spark = SparkTranslator() - except Exception as e: - logger.warning("spark init failed: %s", e) - self._spark = None - return self._spark - def _zhipu_translator(self) -> BaseTranslator | None: - """第二序位引擎:智谱 GLM(免费)。配了 zhipu_api_key 才启用。""" + """主引擎:智谱 GLM(免费)。配了 zhipu_api_key 才启用。""" if self._zhipu is None and settings.zhipu_api_key: try: self._zhipu = ZhipuTranslator() @@ -78,6 +69,16 @@ class TranslationService: self._zhipu = None return self._zhipu + def _spark_translator(self) -> BaseTranslator | None: + """第二序位引擎:星火 Spark(Lite 免费)。配了 spark_api_password 才启用。""" + if self._spark is None and settings.spark_api_password: + try: + self._spark = SparkTranslator() + except Exception as e: + logger.warning("spark init failed: %s", e) + self._spark = None + return self._spark + def _primary(self) -> BaseTranslator | None: """第三级:腾讯 TMT(初始化失败返回 None 表示不可用)。""" if self._tencent is None: @@ -168,12 +169,12 @@ class TranslationService: return TranslationResult(text=cached, engine="cache", chars=chars, cached=True) # 2) 选引擎 - # 优先级:spark → zhipu → tencent(配额)→ maas → agnes → local + # 优先级:zhipu → spark → tencent(配额)→ maas → agnes → local engine: BaseTranslator | None = None - if self._spark_translator() is not None: - engine = self._spark_translator() - elif self._zhipu_translator() is not None: + if self._zhipu_translator() is not None: engine = self._zhipu_translator() + elif self._spark_translator() is not None: + engine = self._spark_translator() elif await self.can_use_tencent(chars): engine = self._primary() if engine is None: @@ -204,17 +205,17 @@ class TranslationService: res = await engine.translate(text, source=source, target=target) except Exception as e: logger.exception("translate failed with %s: %s", engine.name, e) - # 失败时按 zhipu → tencent → maas → local 顺序找一个不同的 fallback - # spark / zhipu 失败时也要走 tencent(继续吃配额,因优先级只是降低不是禁用) + # 失败时按 spark → tencent → maas → local 顺序找一个不同的 fallback + # zhipu / spark 失败时也要走 tencent(继续吃配额,因优先级只是降低不是禁用) fb: BaseTranslator | None = None - if engine.name == "spark": - if self._zhipu_translator() is not None: - fb = self._zhipu_translator() + if engine.name == "zhipu": + if self._spark_translator() is not None: + fb = self._spark_translator() if fb is None and await self.can_use_tencent(chars): fb = self._primary() if fb is None: fb = self._maas() if engine.name != "tencent_maas" else None - elif engine.name == "zhipu": + elif engine.name == "spark": if await self.can_use_tencent(chars): fb = self._primary() if fb is None: