允许跳过翻译
This commit is contained in:
1 parent
b5f769a758
commit
977a6e2c08
2 files changed
+31
-13
No files matched your search
+30
-12
@@ -18,7 +18,7 @@ from fastapi import FastAPI, HTTPException, APIRouter, Body, Path as FastApiPath
|
|||||||
from fastapi.openapi.docs import get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html, get_redoc_html
|
from fastapi.openapi.docs import get_swagger_ui_html, get_swagger_ui_oauth2_redirect_html, get_redoc_html
|
||||||
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from pydantic import BaseModel, Field, field_validator
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||||
|
|
||||||
from docutranslate import __version__
|
from docutranslate import __version__
|
||||||
from docutranslate.agents.agent import ThinkingMode
|
from docutranslate.agents.agent import ThinkingMode
|
||||||
@@ -235,9 +235,13 @@ class GlossaryAgentConfigPayload(BaseModel):
|
|||||||
|
|
||||||
# 1. 定义所有工作流共享的基础参数
|
# 1. 定义所有工作流共享的基础参数
|
||||||
class BaseWorkflowParams(BaseModel):
|
class BaseWorkflowParams(BaseModel):
|
||||||
base_url: str = Field(..., description="LLM API的基础URL。", examples=["https://api.openai.com/v1"])
|
skip_translate: bool = Field(default=False, description="是否跳过翻译步骤。如果为True,则仅执行文档解析和格式转换。")
|
||||||
api_key: str = Field(..., description="LLM API的密钥。", examples=["sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"])
|
base_url: Optional[str] = Field(default=None, description="LLM API的基础URL。当 `skip_translate` 为 `False` 时必填。",
|
||||||
model_id: str = Field(..., description="要使用的LLM模型ID。", examples=["gpt-4o"])
|
examples=["https://api.openai.com/v1"])
|
||||||
|
api_key: Optional[str] = Field(default=None, description="LLM API的密钥。当 `skip_translate` 为 `False` 时必填。",
|
||||||
|
examples=["sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx"])
|
||||||
|
model_id: Optional[str] = Field(default=None, description="要使用的LLM模型ID。当 `skip_translate` 为 `False` 时必填。",
|
||||||
|
examples=["gpt-4o"])
|
||||||
to_lang: str = Field(default="中文", description="目标翻译语言。", examples=["简体中文", "English"])
|
to_lang: str = Field(default="中文", description="目标翻译语言。", examples=["简体中文", "English"])
|
||||||
chunk_size: int = Field(default=default_params["chunk_size"], description="文本分割的块大小(字符)。")
|
chunk_size: int = Field(default=default_params["chunk_size"], description="文本分割的块大小(字符)。")
|
||||||
concurrent: int = Field(default=default_params["concurrent"], description="并发请求数。")
|
concurrent: int = Field(default=default_params["concurrent"], description="并发请求数。")
|
||||||
@@ -256,6 +260,20 @@ class BaseWorkflowParams(BaseModel):
|
|||||||
raise ValueError("当 `glossary_generate_enable` 为 `True` 时, `glossary_agent_config` 字段是必须的。")
|
raise ValueError("当 `glossary_generate_enable` 为 `True` 时, `glossary_agent_config` 字段是必须的。")
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
@model_validator(mode='before')
|
||||||
|
@classmethod
|
||||||
|
def check_translation_fields(cls, values):
|
||||||
|
# 如果不跳过翻译 (值为False或字段不存在),则验证相关字段必须存在且不为空
|
||||||
|
if not values.get('skip_translate'):
|
||||||
|
if not values.get('base_url'):
|
||||||
|
raise ValueError("当 `skip_translate` 为 `False` 时, `base_url` 字段是必须的。")
|
||||||
|
if not values.get('api_key'):
|
||||||
|
raise ValueError("当 `skip_translate` 为 `False` 时, `api_key` 字段是必须的。")
|
||||||
|
if not values.get('model_id'):
|
||||||
|
raise ValueError("当 `skip_translate` 为 `False` 时, `model_id` 字段是必须的。")
|
||||||
|
# 如果跳过翻译,则不进行任何检查,允许 base_url 等字段为空
|
||||||
|
return values
|
||||||
|
|
||||||
|
|
||||||
# 2. 为每个工作流创建独立的参数模型
|
# 2. 为每个工作流创建独立的参数模型
|
||||||
class MarkdownWorkflowParams(BaseWorkflowParams):
|
class MarkdownWorkflowParams(BaseWorkflowParams):
|
||||||
@@ -566,7 +584,7 @@ async def _perform_translation(
|
|||||||
if isinstance(payload, MarkdownWorkflowParams):
|
if isinstance(payload, MarkdownWorkflowParams):
|
||||||
task_logger.info("构建 MarkdownBasedWorkflow 配置。")
|
task_logger.info("构建 MarkdownBasedWorkflow 配置。")
|
||||||
translator_args = payload.model_dump(include={
|
translator_args = payload.model_dump(include={
|
||||||
'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
'skip_translate', 'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
||||||
'temperature', 'thinking', 'chunk_size', 'concurrent', 'glossary_dict'
|
'temperature', 'thinking', 'chunk_size', 'concurrent', 'glossary_dict'
|
||||||
}, exclude_none=True)
|
}, exclude_none=True)
|
||||||
translator_args['glossary_generate_enable'] = payload.glossary_generate_enable
|
translator_args['glossary_generate_enable'] = payload.glossary_generate_enable
|
||||||
@@ -592,7 +610,7 @@ async def _perform_translation(
|
|||||||
elif isinstance(payload, TextWorkflowParams):
|
elif isinstance(payload, TextWorkflowParams):
|
||||||
task_logger.info("构建 TXTWorkflow 配置。")
|
task_logger.info("构建 TXTWorkflow 配置。")
|
||||||
translator_args = payload.model_dump(include={
|
translator_args = payload.model_dump(include={
|
||||||
'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
'skip_translate', 'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
||||||
'temperature', 'thinking', 'chunk_size', 'concurrent', 'glossary_dict'
|
'temperature', 'thinking', 'chunk_size', 'concurrent', 'glossary_dict'
|
||||||
}, exclude_none=True)
|
}, exclude_none=True)
|
||||||
translator_args['glossary_generate_enable'] = payload.glossary_generate_enable
|
translator_args['glossary_generate_enable'] = payload.glossary_generate_enable
|
||||||
@@ -609,7 +627,7 @@ async def _perform_translation(
|
|||||||
elif isinstance(payload, JsonWorkflowParams):
|
elif isinstance(payload, JsonWorkflowParams):
|
||||||
task_logger.info("构建 JsonWorkflow 配置。")
|
task_logger.info("构建 JsonWorkflow 配置。")
|
||||||
translator_args = payload.model_dump(include={
|
translator_args = payload.model_dump(include={
|
||||||
'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
'skip_translate', 'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
||||||
'temperature', 'thinking', 'chunk_size', 'concurrent', 'glossary_dict',
|
'temperature', 'thinking', 'chunk_size', 'concurrent', 'glossary_dict',
|
||||||
'json_paths'
|
'json_paths'
|
||||||
}, exclude_none=True)
|
}, exclude_none=True)
|
||||||
@@ -627,7 +645,7 @@ async def _perform_translation(
|
|||||||
elif isinstance(payload, XlsxWorkflowParams):
|
elif isinstance(payload, XlsxWorkflowParams):
|
||||||
task_logger.info("构建 XlsxWorkflow 配置。")
|
task_logger.info("构建 XlsxWorkflow 配置。")
|
||||||
translator_args = payload.model_dump(include={
|
translator_args = payload.model_dump(include={
|
||||||
'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
'skip_translate', 'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
||||||
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
||||||
'insert_mode', 'separator', 'translate_regions', 'glossary_dict'
|
'insert_mode', 'separator', 'translate_regions', 'glossary_dict'
|
||||||
}, exclude_none=True)
|
}, exclude_none=True)
|
||||||
@@ -646,7 +664,7 @@ async def _perform_translation(
|
|||||||
elif isinstance(payload, DocxWorkflowParams):
|
elif isinstance(payload, DocxWorkflowParams):
|
||||||
task_logger.info("构建 DocxWorkflow 配置。")
|
task_logger.info("构建 DocxWorkflow 配置。")
|
||||||
translator_args = payload.model_dump(include={
|
translator_args = payload.model_dump(include={
|
||||||
'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
'skip_translate', 'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
||||||
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
||||||
'insert_mode', 'separator', 'glossary_dict'
|
'insert_mode', 'separator', 'glossary_dict'
|
||||||
}, exclude_none=True)
|
}, exclude_none=True)
|
||||||
@@ -665,7 +683,7 @@ async def _perform_translation(
|
|||||||
elif isinstance(payload, SrtWorkflowParams):
|
elif isinstance(payload, SrtWorkflowParams):
|
||||||
task_logger.info("构建 SrtWorkflow 配置。")
|
task_logger.info("构建 SrtWorkflow 配置。")
|
||||||
translator_args = payload.model_dump(include={
|
translator_args = payload.model_dump(include={
|
||||||
'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
'skip_translate', 'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
||||||
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
||||||
'insert_mode', 'separator', 'glossary_dict'
|
'insert_mode', 'separator', 'glossary_dict'
|
||||||
}, exclude_none=True)
|
}, exclude_none=True)
|
||||||
@@ -684,7 +702,7 @@ async def _perform_translation(
|
|||||||
elif isinstance(payload, EpubWorkflowParams):
|
elif isinstance(payload, EpubWorkflowParams):
|
||||||
task_logger.info("构建 EpubWorkflow 配置。")
|
task_logger.info("构建 EpubWorkflow 配置。")
|
||||||
translator_args = payload.model_dump(include={
|
translator_args = payload.model_dump(include={
|
||||||
'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
'skip_translate', 'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
||||||
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
||||||
'insert_mode', 'separator', 'glossary_dict'
|
'insert_mode', 'separator', 'glossary_dict'
|
||||||
}, exclude_none=True)
|
}, exclude_none=True)
|
||||||
@@ -704,7 +722,7 @@ async def _perform_translation(
|
|||||||
elif isinstance(payload, HtmlWorkflowParams):
|
elif isinstance(payload, HtmlWorkflowParams):
|
||||||
task_logger.info("构建 HtmlWorkflow 配置。")
|
task_logger.info("构建 HtmlWorkflow 配置。")
|
||||||
translator_args = payload.model_dump(include={
|
translator_args = payload.model_dump(include={
|
||||||
'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
'skip_translate', 'base_url', 'api_key', 'model_id', 'to_lang', 'custom_prompt',
|
||||||
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
'temperature', 'thinking', 'chunk_size', 'concurrent',
|
||||||
'insert_mode', 'separator', 'glossary_dict'
|
'insert_mode', 'separator', 'glossary_dict'
|
||||||
}, exclude_none=True)
|
}, exclude_none=True)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<!DOCTYPE html>
<html lang="zh-CN" data-bs-theme="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n-title="pageTitle">DocuTranslate - 交互式文档翻译</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<!-- Bootstrap CSS -->
<link href="/static/bootstrap.css" rel="stylesheet" crossorigin="anonymous">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="/static/bootstrap-icons.css">
<style>
body {
background-color: var(--bs-body-bg);
}
.main-container {
display: flex;
flex-direction: column;
height: 100vh;
padding-top: 1rem;
padding-bottom: 1rem;
}
.settings-panel {
height: calc(100vh - 2rem);
overflow-y: auto;
padding-right: 15px; /* for scrollbar */
}
.task-area {
height: calc(100vh - 2rem);
overflow-y: auto;
}
.task-card {
transition: all 0.3s ease-in-out;
}
.task-id-placeholder {
color: var(--bs-secondary-color);
font-style: italic;
}
.log-area {
height: 150px;
background-color: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
border-radius: .375rem;
font-family: monospace;
font-size: 0.8rem;
padding: 10px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.file-drop-area {
border: 2px dashed var(--bs-secondary-bg);
border-radius: .375rem;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s ease-in-out;
background-color: var(--bs-body-bg);
}
.file-drop-area.drag-over {
border-color: var(--bs-primary);
background-color: var(--bs-secondary-bg);
}
.file-drop-area.file-selected {
border-style: solid;
border-color: var(--bs-success);
background-color: var(--bs-success-bg-subtle);
}
.file-drop-area.input-error {
border-color: var(--bs-danger);
}
.file-name-display.input-error-text {
color: var(--bs-danger);
font-weight: bold;
}
#printFrame, #translatedPreviewFrame {
border: none;
width: 100%;
}
#previewOffcanvas {
--bs-offcanvas-width: 95vw;
max-width: 1600px;
}
.preview-split-container {
display: flex;
flex-direction: row;
height: 90%;
}
.preview-pane-wrapper {
display: flex;
flex-direction: column;
overflow: hidden; /* Important for split.js */
}
.preview-pane-wrapper h6 {
flex-shrink: 0;
padding: 0.25rem;
}
.preview-pane-wrapper .preview-pane {
flex-grow: 1; /* Make the inner pane grow */
border: 1px solid var(--bs-border-color);
border-radius: .375rem;
overflow: auto;
}
.gutter {
background-color: var(--bs-tertiary-bg);
background-repeat: no-repeat;
background-position: 50%;
}
.gutter.gutter-horizontal {
cursor: col-resize;
border-left: 1px solid var(--bs-border-color);
border-right: 1px solid var(--bs-border-color);
}
.gutter.gutter-vertical {
cursor: row-resize;
border-top: 1px solid var(--bs-border-color);
border-bottom: 1px solid var(--bs-border-color);
}
.preview-pane iframe, .preview-pane pre {
width: 100%;
height: 95%;
border: none;
margin: 0;
padding: 0;
overflow: auto;
background-color: var(--bs-body-bg);
}
.slider-reset-btn {
visibility: hidden;
}
.bottom-left-controls {
position: fixed;
bottom: 1rem;
left: 1rem;
z-index: 1050;
display: flex;
gap: 0.5rem;
}
@media (max-width: 991.98px) {
.main-container {
height: auto;
padding-bottom: 6rem;
}
.settings-panel, .task-area {
height: auto;
overflow-y: visible;
}
.settings-panel {
padding-right: 0;
margin-bottom: 2rem;
}
}
@media (max-width: 767.98px) {
.task-card .col-md-7 {
margin-top: 1.5rem;
}
Line truncated
|
<!DOCTYPE html>
<html lang="zh-CN" data-bs-theme="auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-i18n-title="pageTitle">DocuTranslate - 交互式文档翻译</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<!-- Bootstrap CSS -->
<link href="/static/bootstrap.css" rel="stylesheet" crossorigin="anonymous">
<!-- Bootstrap Icons -->
<link rel="stylesheet" href="/static/bootstrap-icons.css">
<style>
body {
background-color: var(--bs-body-bg);
}
.main-container {
display: flex;
flex-direction: column;
height: 100vh;
padding-top: 1rem;
padding-bottom: 1rem;
}
.settings-panel {
height: calc(100vh - 2rem);
overflow-y: auto;
padding-right: 15px; /* for scrollbar */
}
.task-area {
height: calc(100vh - 2rem);
overflow-y: auto;
}
.task-card {
transition: all 0.3s ease-in-out;
}
.task-id-placeholder {
color: var(--bs-secondary-color);
font-style: italic;
}
.log-area {
height: 150px;
background-color: var(--bs-tertiary-bg);
border: 1px solid var(--bs-border-color);
border-radius: .375rem;
font-family: monospace;
font-size: 0.8rem;
padding: 10px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.file-drop-area {
border: 2px dashed var(--bs-secondary-bg);
border-radius: .375rem;
padding: 2rem;
text-align: center;
cursor: pointer;
transition: all 0.2s ease-in-out;
background-color: var(--bs-body-bg);
}
.file-drop-area.drag-over {
border-color: var(--bs-primary);
background-color: var(--bs-secondary-bg);
}
.file-drop-area.file-selected {
border-style: solid;
border-color: var(--bs-success);
background-color: var(--bs-success-bg-subtle);
}
.file-drop-area.input-error {
border-color: var(--bs-danger);
}
.file-name-display.input-error-text {
color: var(--bs-danger);
font-weight: bold;
}
#printFrame, #translatedPreviewFrame {
border: none;
width: 100%;
}
#previewOffcanvas {
--bs-offcanvas-width: 95vw;
max-width: 1600px;
}
.preview-split-container {
display: flex;
flex-direction: row;
height: 90%;
}
.preview-pane-wrapper {
display: flex;
flex-direction: column;
overflow: hidden; /* Important for split.js */
}
.preview-pane-wrapper h6 {
flex-shrink: 0;
padding: 0.25rem;
}
.preview-pane-wrapper .preview-pane {
flex-grow: 1; /* Make the inner pane grow */
border: 1px solid var(--bs-border-color);
border-radius: .375rem;
overflow: auto;
}
.gutter {
background-color: var(--bs-tertiary-bg);
background-repeat: no-repeat;
background-position: 50%;
}
.gutter.gutter-horizontal {
cursor: col-resize;
border-left: 1px solid var(--bs-border-color);
border-right: 1px solid var(--bs-border-color);
}
.gutter.gutter-vertical {
cursor: row-resize;
border-top: 1px solid var(--bs-border-color);
border-bottom: 1px solid var(--bs-border-color);
}
.preview-pane iframe, .preview-pane pre {
width: 100%;
height: 95%;
border: none;
margin: 0;
padding: 0;
overflow: auto;
background-color: var(--bs-body-bg);
}
.slider-reset-btn {
visibility: hidden;
}
.bottom-left-controls {
position: fixed;
bottom: 1rem;
left: 1rem;
z-index: 1050;
display: flex;
gap: 0.5rem;
}
@media (max-width: 991.98px) {
.main-container {
height: auto;
padding-bottom: 6rem;
}
.settings-panel, .task-area {
height: auto;
overflow-y: visible;
}
.settings-panel {
padding-right: 0;
margin-bottom: 2rem;
}
}
@media (max-width: 767.98px) {
.task-card .col-md-7 {
margin-top: 1.5rem;
}
Line truncated
|
||||||
Reference in new issue
Block a user