feat(translate): service.py 接 zhipu / config 加 zhipu_* / .env.example 加 ZHIPU 配置
配 zhipu.py(zhipu 模块)一起使用: - service.py: 新增 _zhipu_translator(),引擎选择链路 spark → zhipu → tencent → maas → agnes → local - config.py: 新增 zhipu_api_key / zhipu_base_url / zhipu_model / zhipu_interval_sec - .env.example: 补 ZHIPU_* 字段说明 留空 zhipu_api_key = spark 不可用时直接降级 tencent(向后兼容)
This commit is contained in:
@@ -45,6 +45,15 @@ SPARK_MODEL=lite
|
|||||||
# 单次调用间隔(秒),避免被限速
|
# 单次调用间隔(秒),避免被限速
|
||||||
SPARK_INTERVAL_SEC=1.0
|
SPARK_INTERVAL_SEC=1.0
|
||||||
|
|
||||||
|
# ===== 智谱 GLM(第二序位翻译;glm-4-flash 免费)=====
|
||||||
|
# 留空 = 不启用智谱(spark 不可用时直接走 tencent)
|
||||||
|
# 控制台 https://open.bigmodel.cn/ → API Keys → 新建
|
||||||
|
ZHIPU_API_KEY=your_zhipu_api_key
|
||||||
|
ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4
|
||||||
|
# 模型:glm-4-flash(默认,免费) / glm-4-air / glm-4-airx / glm-4 / glm-4-plus / glm-4.5 / glm-4.6
|
||||||
|
ZHIPU_MODEL=glm-4-flash
|
||||||
|
ZHIPU_INTERVAL_SEC=1.0
|
||||||
|
|
||||||
# ===== 本地翻译(降级) =====
|
# ===== 本地翻译(降级) =====
|
||||||
# 不启用就留空:不会用本地模<E59CB0>?LOCAL_TRANSLATE_ENABLED=false
|
# 不启用就留空:不会用本地模<E59CB0>?LOCAL_TRANSLATE_ENABLED=false
|
||||||
LOCAL_TRANSLATE_MODEL=nllb-200-distilled-600M
|
LOCAL_TRANSLATE_MODEL=nllb-200-distilled-600M
|
||||||
|
|||||||
@@ -77,6 +77,13 @@ class Settings(BaseSettings):
|
|||||||
spark_model: str = "lite"
|
spark_model: str = "lite"
|
||||||
spark_interval_sec: float = 1.0
|
spark_interval_sec: float = 1.0
|
||||||
|
|
||||||
|
# ===== 智谱 GLM(第二序位翻译;GLM-4-Flash 免费)=====
|
||||||
|
# 留空 = 不启用智谱(spark 不可用时直接降级到 tencent)
|
||||||
|
zhipu_api_key: str = ""
|
||||||
|
zhipu_base_url: str = "https://open.bigmodel.cn/api/paas/v4"
|
||||||
|
zhipu_model: str = "glm-4-flash"
|
||||||
|
zhipu_interval_sec: float = 1.0
|
||||||
|
|
||||||
@field_validator("tencent_tmt_quota_buffer")
|
@field_validator("tencent_tmt_quota_buffer")
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_buffer(cls, v: float) -> float:
|
def _check_buffer(cls, v: float) -> float:
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
引擎链路(优先级降序):
|
引擎链路(优先级降序):
|
||||||
1. spark(主,Lite 免费;spark_api_password 配了才用)
|
1. spark(主,Lite 免费;spark_api_password 配了才用)
|
||||||
2. tencent TMT(第二级,按月配额;快满时主动切走)
|
2. zhipu(第二序位,GLM-4-Flash 免费;zhipu_api_key 配了才用)
|
||||||
3. tencent_maas(备用,OpenAI 兼容,无配额;主失败/TMT 配额耗尽时启用)
|
3. tencent TMT(第三级,按月配额;快满时主动切走)
|
||||||
4. agnes(第三级,通用 LLM 做翻译;MaaS 不可用时启用 — 质量次之但够用)
|
4. tencent_maas(备用,OpenAI 兼容,无配额;主失败/TMT 配额耗尽时启用)
|
||||||
5. local(最后兜底,需 settings.local_translate_enabled=true)
|
5. agnes(第四级,通用 LLM 做翻译;MaaS 不可用时启用 — 质量次之但够用)
|
||||||
|
6. local(最后兜底,需 settings.local_translate_enabled=true)
|
||||||
|
|
||||||
注:
|
注:
|
||||||
- TMT 是按月计费的(腾讯云后台可能计费口径是请求字节,我们 redis 累加的是字符数,
|
- TMT 是按月计费的(腾讯云后台可能计费口径是请求字节,我们 redis 累加的是字符数,
|
||||||
差异约 2-3x);用户从腾讯云后台看"已用 2M"时,我们 redis 显示约 80 万字符
|
差异约 2-3x);用户从腾讯云后台看"已用 2M"时,我们 redis 显示约 80 万字符
|
||||||
- 用户决策:以腾讯云后台数字为准,快满时降级
|
- 用户决策:以腾讯云后台数字为准,快满时降级
|
||||||
- spark 是 Lite 免费,默认走它;spark 不可用时降级到 tencent(继续吃配额)。
|
- spark / zhipu 都是免费模型,默认优先;不可用时降级到 tencent(继续吃配额)。
|
||||||
想要完全绕开 tencent,把 TENCENTCLOUD_SECRET_ID 留空即可。
|
想要完全绕开 tencent,把 TENCENTCLOUD_SECRET_ID 留空即可。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -30,6 +31,7 @@ from app.services.translation.local import LocalTranslator
|
|||||||
from app.services.translation.spark import SparkTranslator
|
from app.services.translation.spark import SparkTranslator
|
||||||
from app.services.translation.tencent import TencentTranslator
|
from app.services.translation.tencent import TencentTranslator
|
||||||
from app.services.translation.tencent_maas import TencentMaaSTranslator
|
from app.services.translation.tencent_maas import TencentMaaSTranslator
|
||||||
|
from app.services.translation.zhipu import ZhipuTranslator
|
||||||
|
|
||||||
logger = logging.getLogger("news.translate.service")
|
logger = logging.getLogger("news.translate.service")
|
||||||
|
|
||||||
@@ -48,6 +50,7 @@ def _month_key() -> str:
|
|||||||
class TranslationService:
|
class TranslationService:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._spark: BaseTranslator | None = None
|
self._spark: BaseTranslator | None = None
|
||||||
|
self._zhipu: BaseTranslator | None = None
|
||||||
self._tencent: BaseTranslator | None = None
|
self._tencent: BaseTranslator | None = None
|
||||||
self._tencent_maas: BaseTranslator | None = None
|
self._tencent_maas: BaseTranslator | None = None
|
||||||
self._agnes: BaseTranslator | None = None
|
self._agnes: BaseTranslator | None = None
|
||||||
@@ -65,8 +68,18 @@ class TranslationService:
|
|||||||
self._spark = None
|
self._spark = None
|
||||||
return self._spark
|
return self._spark
|
||||||
|
|
||||||
|
def _zhipu_translator(self) -> BaseTranslator | None:
|
||||||
|
"""第二序位引擎:智谱 GLM(免费)。配了 zhipu_api_key 才启用。"""
|
||||||
|
if self._zhipu is None and settings.zhipu_api_key:
|
||||||
|
try:
|
||||||
|
self._zhipu = ZhipuTranslator()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("zhipu init failed: %s", e)
|
||||||
|
self._zhipu = None
|
||||||
|
return self._zhipu
|
||||||
|
|
||||||
def _primary(self) -> BaseTranslator | None:
|
def _primary(self) -> BaseTranslator | None:
|
||||||
"""第二级:腾讯 TMT(初始化失败返回 None 表示不可用)。"""
|
"""第三级:腾讯 TMT(初始化失败返回 None 表示不可用)。"""
|
||||||
if self._tencent is None:
|
if self._tencent is None:
|
||||||
try:
|
try:
|
||||||
self._tencent = TencentTranslator()
|
self._tencent = TencentTranslator()
|
||||||
@@ -155,10 +168,12 @@ class TranslationService:
|
|||||||
return TranslationResult(text=cached, engine="cache", chars=chars, cached=True)
|
return TranslationResult(text=cached, engine="cache", chars=chars, cached=True)
|
||||||
|
|
||||||
# 2) 选引擎
|
# 2) 选引擎
|
||||||
# 优先级:spark → tencent(配额)→ maas → agnes → local
|
# 优先级:spark → zhipu → tencent(配额)→ maas → agnes → local
|
||||||
engine: BaseTranslator | None = None
|
engine: BaseTranslator | None = None
|
||||||
if self._spark_translator() is not None:
|
if self._spark_translator() is not None:
|
||||||
engine = self._spark_translator()
|
engine = self._spark_translator()
|
||||||
|
elif self._zhipu_translator() is not None:
|
||||||
|
engine = self._zhipu_translator()
|
||||||
elif await self.can_use_tencent(chars):
|
elif await self.can_use_tencent(chars):
|
||||||
engine = self._primary()
|
engine = self._primary()
|
||||||
if engine is None:
|
if engine is None:
|
||||||
@@ -168,7 +183,7 @@ class TranslationService:
|
|||||||
engine = None
|
engine = None
|
||||||
|
|
||||||
if engine is None:
|
if engine is None:
|
||||||
# spark 不可用 + 配额耗尽 / TMT 不可用:走备用链(maas → agnes → local)
|
# spark / zhipu 不可用 + 配额耗尽 / TMT 不可用:走备用链(maas → agnes → local)
|
||||||
engine = self._fallback()
|
engine = self._fallback()
|
||||||
if engine is None:
|
if engine is None:
|
||||||
# 全无可用:返回原文 + 标记
|
# 全无可用:返回原文 + 标记
|
||||||
@@ -189,10 +204,17 @@ class TranslationService:
|
|||||||
res = await engine.translate(text, source=source, target=target)
|
res = await engine.translate(text, source=source, target=target)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("translate failed with %s: %s", engine.name, e)
|
logger.exception("translate failed with %s: %s", engine.name, e)
|
||||||
# 失败时按 tencent_maas → local 顺序找一个不同的 fallback
|
# 失败时按 zhipu → tencent → maas → local 顺序找一个不同的 fallback
|
||||||
# spark 失败时也要走 tencent(继续吃配额,因优先级只是降低不是禁用)
|
# spark / zhipu 失败时也要走 tencent(继续吃配额,因优先级只是降低不是禁用)
|
||||||
fb: BaseTranslator | None = None
|
fb: BaseTranslator | None = None
|
||||||
if engine.name == "spark":
|
if engine.name == "spark":
|
||||||
|
if self._zhipu_translator() is not None:
|
||||||
|
fb = self._zhipu_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":
|
||||||
if await self.can_use_tencent(chars):
|
if await self.can_use_tencent(chars):
|
||||||
fb = self._primary()
|
fb = self._primary()
|
||||||
if fb is None:
|
if fb is None:
|
||||||
@@ -212,7 +234,7 @@ class TranslationService:
|
|||||||
raise RuntimeError(f"translation failed for {chars} chars (engine={engine.name})")
|
raise RuntimeError(f"translation failed for {chars} chars (engine={engine.name})")
|
||||||
|
|
||||||
# 4) 写缓存 — 只缓存真实翻译结果;失败/降级文本不缓存(避免污染 30 天)
|
# 4) 写缓存 — 只缓存真实翻译结果;失败/降级文本不缓存(避免污染 30 天)
|
||||||
if res.engine in ("spark", "tencent", "tencent_maas", "agnes", "nllb") and not res.cached:
|
if res.engine in ("spark", "zhipu", "tencent", "tencent_maas", "agnes", "nllb") and not res.cached:
|
||||||
if "[翻译失败" not in res.text and "[本条未翻译" not in res.text:
|
if "[翻译失败" not in res.text and "[本条未翻译" not in res.text:
|
||||||
try:
|
try:
|
||||||
await r.set(ck, res.text, ex=60 * 60 * 24 * 30) # 30 天
|
await r.set(ck, res.text, ex=60 * 60 * 24 * 30) # 30 天
|
||||||
|
|||||||
Reference in New Issue
Block a user