481 lines
18 KiB
Python
481 lines
18 KiB
Python
# SPDX-FileCopyrightText: 2025 QinHan
|
||
# SPDX-License-Identifier: MPL-2.0
|
||
# docutranslate.core.schemas.py
|
||
|
||
import json
|
||
from typing import (
|
||
List,
|
||
Dict,
|
||
Optional,
|
||
Literal,
|
||
Union,
|
||
Annotated,
|
||
)
|
||
|
||
from pydantic import (
|
||
BaseModel,
|
||
Field,
|
||
model_validator,
|
||
field_validator,
|
||
AliasChoices,
|
||
ConfigDict,
|
||
)
|
||
|
||
from docutranslate.agents.agent import ThinkingMode
|
||
from docutranslate.agents.thinking.thinking_factory import ProviderType
|
||
from docutranslate.translator import default_params
|
||
|
||
# --- 公共类型定义 ---
|
||
WorkflowType = Literal[
|
||
"auto", "markdown_based", "txt", "json", "xlsx", "docx",
|
||
"srt", "epub", "html", "ass", "pptx"
|
||
]
|
||
InsertMode = Literal["replace", "append", "prepend"]
|
||
|
||
|
||
class GlossaryAgentConfigPayload(BaseModel):
|
||
base_url: str = Field(
|
||
...,
|
||
validation_alias=AliasChoices("base_url", "baseurl"),
|
||
description="用于术语表生成的Agent的LLM API基础URL。",
|
||
examples=["https://api.openai.com/v1"],
|
||
)
|
||
api_key: str = Field(
|
||
default="xx",
|
||
validation_alias=AliasChoices("api_key", "key"),
|
||
description="用于术语表生成的Agent的LLM API密钥(默认为xx)。",
|
||
examples=["sk-agent-api-key"],
|
||
)
|
||
|
||
@field_validator("api_key")
|
||
@classmethod
|
||
def set_default_glossary_api_key(cls, v: str) -> str:
|
||
return v if v and v.strip() else "xx"
|
||
|
||
model_id: str = Field(
|
||
..., description="用于术语表生成的Agent的模型ID。", examples=["gpt-4-turbo"]
|
||
)
|
||
to_lang: str = Field(
|
||
..., description="术语表生成的目标语言。", examples=["简体中文", "English"]
|
||
)
|
||
temperature: float = Field(
|
||
default=0.7, description="用于术语表生成的Agent的温度参数。"
|
||
)
|
||
concurrent: int = Field(default=30, description="Agent的最大并发请求数。")
|
||
timeout: int = Field(
|
||
default=default_params["timeout"], description="等待API回复的时间(秒)。"
|
||
)
|
||
thinking: ThinkingMode = Field(default="default", description="Agent的思考模式。")
|
||
retry: int = Field(
|
||
default=default_params["retry"], description="分块失败后的最大重试次数。"
|
||
)
|
||
system_proxy_enable: bool = Field(
|
||
default=default_params["system_proxy_enable"],
|
||
description="是否使用系统代理",
|
||
examples=[True, False],
|
||
)
|
||
# 修改: 默认值改为 "" 以避免 Swagger 显示 "string"
|
||
custom_prompt: Optional[str] = Field(
|
||
default="", description="生成术语表的用户自定义提示词"
|
||
)
|
||
force_json: bool = Field(
|
||
default=False, description="强制Agent输出JSON格式的术语表。"
|
||
)
|
||
# 修改: 增加 examples=[None]
|
||
rpm: Optional[int] = Field(
|
||
default=None, description="RPM限制 (Requests Per Minute)", examples=[None]
|
||
)
|
||
tpm: Optional[int] = Field(
|
||
default=None, description="TPM限制 (Tokens Per Minute)", examples=[None]
|
||
)
|
||
provider: Optional[ProviderType] = Field(
|
||
default=None, description="LLM供应商标识", examples=[None]
|
||
)
|
||
|
||
|
||
# 1. 定义所有工作流共享的基础参数
|
||
class BaseWorkflowParams(BaseModel):
|
||
skip_translate: bool = Field(
|
||
default=False,
|
||
description="是否跳过翻译步骤。如果为True,则仅执行文档解析和格式转换。",
|
||
)
|
||
model_preset: Optional[str] = Field(
|
||
default="",
|
||
description="服务端模型预设ID。设置后会由服务端从环境变量中注入模型配置。",
|
||
examples=["default"],
|
||
)
|
||
# 修改: 默认值改为 ""
|
||
base_url: Optional[str] = Field(
|
||
default="",
|
||
validation_alias=AliasChoices("base_url", "baseurl"),
|
||
description="LLM API的基础URL。当 `skip_translate` 为 `False` 时必填。",
|
||
examples=["https://api.openai.com/v1"],
|
||
)
|
||
api_key: str = Field(
|
||
default="xx",
|
||
validation_alias=AliasChoices("api_key", "key"),
|
||
description="LLM API的密钥(可选,默认为xx)。",
|
||
examples=["sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"],
|
||
)
|
||
|
||
@field_validator("api_key")
|
||
@classmethod
|
||
def set_default_api_key(cls, v: str) -> str:
|
||
return v if v and v.strip() else "xx"
|
||
|
||
# 修改: 默认值改为 ""
|
||
model_id: Optional[str] = Field(
|
||
default="",
|
||
description="要使用的LLM模型ID。当 `skip_translate` 为 `False` 时必填。",
|
||
examples=["gpt-4o"],
|
||
)
|
||
to_lang: str = Field(
|
||
default="中文", description="目标翻译语言。", examples=["简体中文", "English"]
|
||
)
|
||
chunk_size: int = Field(
|
||
default=default_params["chunk_size"], description="文本分割的块大小(字符)。"
|
||
)
|
||
concurrent: int = Field(
|
||
default=default_params["concurrent"], description="并发请求数。"
|
||
)
|
||
temperature: float = Field(
|
||
default=default_params["temperature"], description="LLM温度参数。"
|
||
)
|
||
timeout: int = Field(
|
||
default=default_params["timeout"], description="等待API回复的时间(秒)。"
|
||
)
|
||
thinking: ThinkingMode = Field(
|
||
default=default_params["thinking"],
|
||
description="Agent的思考模式。",
|
||
examples=["default", "enable", "disable"],
|
||
)
|
||
retry: int = Field(
|
||
default=default_params["retry"],
|
||
description="某个分块翻译失败后的最大重试次数。",
|
||
)
|
||
system_proxy_enable: bool = Field(
|
||
default=default_params["system_proxy_enable"],
|
||
description="是否使用系统代理",
|
||
examples=[True, False],
|
||
)
|
||
# 修改: 默认值改为 ""
|
||
custom_prompt: Optional[str] = Field(
|
||
default="", description="用户自定义的翻译Prompt。", alias="custom_prompt"
|
||
)
|
||
source_lang: Optional[str] = Field(
|
||
default=None, description="源语言(qwen-mt系列模型专用,如 'Chinese'、'English')。", examples=[None]
|
||
)
|
||
glossary_dict: Optional[Dict[str, str]] = Field(
|
||
None, description="术语表字典,key为原文,value为译文。", examples=[None]
|
||
)
|
||
glossary_generate_enable: bool = Field(
|
||
default=False, description="是否开启术语表自动生成。"
|
||
)
|
||
glossary_agent_config: Optional[GlossaryAgentConfigPayload] = Field(
|
||
None,
|
||
description="用于术语表生成的Agent的配置。如果 `glossary_generate_enable` 为 `True`,此项必填。",
|
||
examples=[None],
|
||
)
|
||
force_json: bool = Field(
|
||
default=False, description="应输出json格式时强制ai输出json"
|
||
)
|
||
rpm: Optional[int] = Field(
|
||
default=None, description="RPM限制 (Requests Per Minute)", examples=[None]
|
||
)
|
||
tpm: Optional[int] = Field(
|
||
default=None, description="TPM限制 (Tokens Per Minute)", examples=[None]
|
||
)
|
||
provider: Optional[ProviderType] = Field(
|
||
default=None, description="LLM供应商标识", examples=[None]
|
||
)
|
||
|
||
@model_validator(mode="before")
|
||
@classmethod
|
||
def check_translation_fields(cls, values):
|
||
# 修复: 当使用 FastAPI Form + Json 时,Pydantic V2 mode='before' 验证器可能会接收到 JSON 字符串
|
||
if isinstance(values, str):
|
||
try:
|
||
values = json.loads(values)
|
||
except ValueError:
|
||
pass
|
||
|
||
if isinstance(values, dict):
|
||
if not values.get("skip_translate"):
|
||
has_model_preset = bool(str(values.get("model_preset") or "").strip())
|
||
# 如果是空字符串 "" (即默认值),not "" 为 True,会触发错误,符合预期
|
||
if not has_model_preset and not (
|
||
values.get("base_url") or values.get("baseurl")
|
||
):
|
||
# Auto 模式在校验前不强制要求 base_url
|
||
if values.get("workflow_type") != "auto":
|
||
raise ValueError(
|
||
"当 `skip_translate` 为 `False` 时, `base_url` 或 `baseurl` 字段是必须的。"
|
||
)
|
||
if not has_model_preset and not values.get("model_id"):
|
||
if values.get("workflow_type") != "auto":
|
||
raise ValueError(
|
||
"当 `skip_translate` 为 `False` 时, `model_id` 字段是必须的。"
|
||
)
|
||
return values
|
||
|
||
|
||
# 2. 为每个工作流创建独立的参数模型
|
||
class AutoWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["auto"] = Field(..., description="根据文件后缀自动选择工作流。")
|
||
model_config = ConfigDict(extra="allow")
|
||
|
||
|
||
class MarkdownWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["markdown_based"] = Field(
|
||
..., description="指定使用基于Markdown的翻译工作流。"
|
||
)
|
||
convert_engine: Literal[
|
||
"identity", "mineru", "docling", "mineru_deploy"
|
||
] = Field(
|
||
"identity",
|
||
description="选择将文件解析为markdown的引擎。'mineru_deploy' 适用于本地部署的 MinerU 服务。如果输入文件是.md,此项可为`identity`或不传。",
|
||
examples=["identity", "mineru", "docling", "mineru_deploy"],
|
||
)
|
||
|
||
# --- Engine-Specific Parameters ---
|
||
|
||
# -- For "mineru" (Cloud API) --
|
||
# 修改: 默认值改为 ""
|
||
mineru_token: Optional[str] = Field(
|
||
default="",
|
||
description="[仅当 convert_engine='mineru'] 必填的API令牌。",
|
||
)
|
||
model_version: Literal["pipeline", "vlm"] = Field(
|
||
"vlm", description="[仅当 convert_engine='mineru'] Mineru Cloud模型的版本。"
|
||
)
|
||
formula_ocr: bool = Field(
|
||
True,
|
||
description="[仅当 convert_engine='mineru' 或 'docling'] 是否对公式进行OCR识别。",
|
||
)
|
||
|
||
# -- For "docling" --
|
||
code_ocr: bool = Field(
|
||
True, description="[仅当 convert_engine='docling'] 是否对代码块进行OCR识别。"
|
||
)
|
||
|
||
# -- For "mineru_deploy" (Local Deployment) --
|
||
mineru_deploy_base_url: Optional[str] = Field(
|
||
"http://127.0.0.1:8000",
|
||
description="[仅当 convert_engine='mineru_deploy'] 本地部署的 MinerU 服务地址。",
|
||
)
|
||
# --- UPDATED BACKEND LIST ---
|
||
mineru_deploy_backend: Literal[
|
||
"pipeline",
|
||
"vlm-auto-engine",
|
||
"vlm-http-client",
|
||
"hybrid-auto-engine",
|
||
"hybrid-http-client"
|
||
] = Field(
|
||
"hybrid-auto-engine",
|
||
description="[仅当 convert_engine='mineru_deploy'] 本地部署的 MinerU 服务使用的后端。",
|
||
)
|
||
# --- NEW PARAMETERS START ---
|
||
mineru_deploy_parse_method: Literal["auto", "txt", "ocr"] = Field(
|
||
"auto",
|
||
description="[仅当 convert_engine='mineru_deploy'] 解析方法: auto, txt, ocr"
|
||
)
|
||
mineru_deploy_table_enable: bool = Field(
|
||
True,
|
||
description="[仅当 convert_engine='mineru_deploy'] 本地部署的服务是否启用表格解析。",
|
||
)
|
||
# --- NEW PARAMETERS END ---
|
||
mineru_deploy_formula_enable: bool = Field(
|
||
True,
|
||
description="[仅当 convert_engine='mineru_deploy'] 本地部署的服务是否启用公式解析。",
|
||
)
|
||
mineru_deploy_start_page_id: int = Field(
|
||
0, description="[仅当 convert_engine='mineru_deploy'] 起始解析页面。"
|
||
)
|
||
mineru_deploy_end_page_id: int = Field(
|
||
99999, description="[仅当 convert_engine='mineru_deploy'] 结束解析页面。"
|
||
)
|
||
mineru_deploy_lang_list: Optional[List[str]] = Field(
|
||
None,
|
||
description="[仅当 convert_engine='mineru_deploy'] 语言列表, 默认 ['ch']。",
|
||
examples=[["ch", "en"]],
|
||
)
|
||
# 修改: 默认值改为 ""
|
||
mineru_deploy_server_url: Optional[str] = Field(
|
||
default="",
|
||
description="[仅当 convert_engine='mineru_deploy' 且 backend为http-client相关时] Server URL.",
|
||
)
|
||
|
||
@model_validator(mode="after")
|
||
def check_engine_params(self):
|
||
if self.convert_engine == "mineru" and not self.mineru_token:
|
||
raise ValueError(
|
||
"当 `convert_engine` 为 'mineru' 时,`mineru_token` 字段是必须的。"
|
||
)
|
||
if (
|
||
self.convert_engine == "mineru_deploy"
|
||
and not self.mineru_deploy_base_url
|
||
):
|
||
raise ValueError(
|
||
"当 `convert_engine` 为 'mineru_deploy' 时,`mineru_deploy_base_url` 字段是必须的。"
|
||
)
|
||
return self
|
||
|
||
|
||
class TextWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["txt"] = Field(
|
||
..., description="指定使用纯文本的翻译工作流。"
|
||
)
|
||
insert_mode: Literal["replace", "append", "prepend"] = Field(
|
||
"replace",
|
||
description="翻译文本的插入模式。'replace':替换原文,'append':附加到原文后,'prepend':附加到原文前。",
|
||
)
|
||
separator: str = Field(
|
||
"\n",
|
||
description="当 insert_mode 为 'append' 或 'prepend' 时,用于分隔原文和译文的分隔符。",
|
||
)
|
||
|
||
|
||
class JsonWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["json"] = Field(
|
||
..., description="指定使用JSON的翻译工作流。"
|
||
)
|
||
json_paths: List[str] = Field(
|
||
...,
|
||
description="一个jsonpath-ng表达式列表,用于指定需要翻译的JSON字段。",
|
||
examples=[["$.product.name", "$.product.description", "$.features[*]"]],
|
||
)
|
||
|
||
|
||
class XlsxWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["xlsx"] = Field(
|
||
..., description="指定使用XLSX的翻译工作流。"
|
||
)
|
||
insert_mode: Literal["replace", "append", "prepend"] = Field(
|
||
"replace",
|
||
description="翻译文本的插入模式。'replace':替换原文,'append':附加到原文后,'prepend':附加到原文前。",
|
||
)
|
||
separator: str = Field(
|
||
"\n",
|
||
description="当 insert_mode 为 'append' 或 'prepend' 时,用于分隔原文和译文的分隔符。",
|
||
)
|
||
translate_regions: Optional[List[str]] = Field(
|
||
None,
|
||
description="指定翻译区域列表。示例: ['Sheet1!A1:B10', 'C:D', 'E5']。如果不指定表名 (如 'C:D'),则应用于所有表。如果为 None,则翻译整个文件中的所有文本。",
|
||
examples=[None],
|
||
)
|
||
|
||
|
||
class DocxWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["docx"] = Field(
|
||
..., description="指定使用DOCX的翻译工作流。"
|
||
)
|
||
insert_mode: Literal["replace", "append", "prepend"] = Field(
|
||
"replace",
|
||
description="翻译文本的插入模式。'replace':替换原文,'append':附加到原文后,'prepend':附加到原文前。",
|
||
)
|
||
separator: str = Field(
|
||
"\n",
|
||
description="当 insert_mode 为 'append' 或 'prepend' 时,用于分隔原文和译文的分隔符。",
|
||
)
|
||
|
||
|
||
class SrtWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["srt"] = Field(
|
||
..., description="指定使用SRT字幕的翻译工作流。"
|
||
)
|
||
insert_mode: Literal["replace", "append", "prepend"] = Field(
|
||
"replace",
|
||
description="翻译文本的插入模式。'replace':替换原文,'append':附加到原文后,'prepend':附加到原文前。",
|
||
)
|
||
separator: str = Field(
|
||
"\n",
|
||
description="当 insert_mode 为 'append' 或 'prepend' 时,用于分隔原文和译文的分隔符。",
|
||
)
|
||
|
||
|
||
class EpubWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["epub"] = Field(
|
||
..., description="指定使用EPUB的翻译工作流。"
|
||
)
|
||
insert_mode: Literal["replace", "append", "prepend"] = Field(
|
||
"replace",
|
||
description="翻译文本的插入模式。'replace':替换原文,'append':附加到原文后,'prepend':附加到原文前。",
|
||
)
|
||
separator: str = Field(
|
||
"\n",
|
||
description="当 insert_mode 为 'append' 或 'prepend' 时,用于分隔原文和译文的分隔符。",
|
||
)
|
||
|
||
|
||
# --- HTML WORKFLOW PARAMS START ---
|
||
class HtmlWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["html"] = Field(
|
||
..., description="指定使用HTML的翻译工作流。"
|
||
)
|
||
insert_mode: Literal["replace", "append", "prepend"] = Field(
|
||
"replace",
|
||
description="翻译文本的插入模式。'replace':替换原文,'append' :附加到原文后,'prepend':附加到原文前。",
|
||
)
|
||
separator: str = Field(
|
||
" ",
|
||
description="当 insert_mode 为 'append' 或 'prepend' 时,用于分隔原文和译文的分隔符。",
|
||
)
|
||
|
||
|
||
# --- HTML WORKFLOW PARAMS END ---
|
||
|
||
|
||
# --- ASS WORKFLOW PARAMS START ---
|
||
class AssWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["ass"] = Field(
|
||
..., description="指定使用ASS字幕的翻译工作流。"
|
||
)
|
||
insert_mode: Literal["replace", "append", "prepend"] = Field(
|
||
"replace",
|
||
description="翻译文本的插入模式。'replace':替换原文,'append':附加到原文后,'prepend':附加到原文前。",
|
||
)
|
||
separator: str = Field(
|
||
"\\N",
|
||
description="当 insert_mode 为 'append' 或 'prepend' 时,用于分隔原文和译文的分隔符。ASS格式通常使用 \\N 作为换行符。",
|
||
)
|
||
|
||
|
||
# --- ASS WORKFLOW PARAMS END ---
|
||
|
||
|
||
# --- PPTX WORKFLOW PARAMS START ---
|
||
class PPTXWorkflowParams(BaseWorkflowParams):
|
||
workflow_type: Literal["pptx"] = Field(
|
||
..., description="指定使用PPTX的翻译工作流。"
|
||
)
|
||
insert_mode: Literal["replace", "append", "prepend"] = Field(
|
||
"replace",
|
||
description="翻译文本的插入模式。'replace':替换原文,'append':附加到原文后,'prepend':附加到原文前。",
|
||
)
|
||
separator: str = Field(
|
||
"\n",
|
||
description="当 insert_mode 为 'append' 或 'prepend' 时,用于分隔原文和译文的分隔符。",
|
||
)
|
||
# target_cjk_font removed as per request
|
||
|
||
|
||
# --- PPTX WORKFLOW PARAMS END ---
|
||
|
||
|
||
TranslatePayload = Annotated[
|
||
Union[
|
||
AutoWorkflowParams,
|
||
MarkdownWorkflowParams,
|
||
TextWorkflowParams,
|
||
JsonWorkflowParams,
|
||
XlsxWorkflowParams,
|
||
DocxWorkflowParams,
|
||
SrtWorkflowParams,
|
||
EpubWorkflowParams,
|
||
HtmlWorkflowParams,
|
||
AssWorkflowParams,
|
||
PPTXWorkflowParams,
|
||
],
|
||
Field(discriminator="workflow_type"),
|
||
]
|