前后端增加下载附件功能
This commit is contained in:
@@ -113,6 +113,7 @@ def _create_default_task_state() -> Dict[str, Any]:
|
|||||||
"original_filename": None,
|
"original_filename": None,
|
||||||
"temp_dir": None, # 用于存储临时文件的目录
|
"temp_dir": None, # 用于存储临时文件的目录
|
||||||
"downloadable_files": {}, # 存储可下载文件的路径和名称
|
"downloadable_files": {}, # 存储可下载文件的路径和名称
|
||||||
|
"attachment_files": {}, # 存储附件文件的路径和标识符
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -202,9 +203,10 @@ DocuTranslate 后端服务 API,提供文档翻译、状态查询、结果下
|
|||||||
2. **`GET /service/status/{{task_id}}`**: 使用获取到的 `task_id` 轮询此端点,获取任务的实时状态。
|
2. **`GET /service/status/{{task_id}}`**: 使用获取到的 `task_id` 轮询此端点,获取任务的实时状态。
|
||||||
3. **`GET /service/logs/{{task_id}}`**: (可选) 获取实时的翻译日志。
|
3. **`GET /service/logs/{{task_id}}`**: (可选) 获取实时的翻译日志。
|
||||||
4. **`GET /service/download/{{task_id}}/{{file_type}}`**: 任务完成后 (当 `download_ready` 为 `true` 时),通过此端点下载结果文件。
|
4. **`GET /service/download/{{task_id}}/{{file_type}}`**: 任务完成后 (当 `download_ready` 为 `true` 时),通过此端点下载结果文件。
|
||||||
5. **`GET /service/content/{{task_id}}/{{file_type}}`**: 任务完成后(当 `download_ready` 为 `true` 时),以JSON格式获取文件内容。
|
5. **`GET /service/attachment/{{task_id}}/{{identifier}}`**: (可选) 如果任务生成了附件(如术语表),通过此端点下载。
|
||||||
6. **`POST /service/cancel/{{task_id}}`**: (可选) 取消一个正在进行的任务。
|
6. **`GET /service/content/{{task_id}}/{{file_type}}`**: 任务完成后(当 `download_ready` 为 `true` 时),以JSON格式获取文件内容。
|
||||||
7. **`POST /service/release/{{task_id}}`**: (可选) 当任务不再需要时,释放其在服务器上占用的所有资源,包括临时文件。
|
7. **`POST /service/cancel/{{task_id}}`**: (可选) 取消一个正在进行的任务。
|
||||||
|
8. **`POST /service/release/{{task_id}}`**: (可选) 当任务不再需要时,释放其在服务器上占用的所有资源,包括临时文件。
|
||||||
|
|
||||||
**版本**: {__version__}
|
**版本**: {__version__}
|
||||||
""",
|
""",
|
||||||
@@ -795,6 +797,23 @@ async def _perform_translation(
|
|||||||
except Exception as export_error:
|
except Exception as export_error:
|
||||||
task_logger.error(f"生成 {file_type} 文件时出错: {export_error}", exc_info=True)
|
task_logger.error(f"生成 {file_type} 文件时出错: {export_error}", exc_info=True)
|
||||||
|
|
||||||
|
# 处理附件文件
|
||||||
|
attachment_files = {}
|
||||||
|
attachment_object = workflow.get_attachment()
|
||||||
|
if attachment_object and attachment_object.attachment_dict:
|
||||||
|
task_logger.info(f"发现 {len(attachment_object.attachment_dict)} 个附件,正在处理...")
|
||||||
|
for identifier, doc in attachment_object.attachment_dict.items():
|
||||||
|
try:
|
||||||
|
# 'doc' is a Document object
|
||||||
|
attachment_filename = f"{doc.stem or identifier}.{doc.suffix}"
|
||||||
|
attachment_path = os.path.join(temp_dir, attachment_filename)
|
||||||
|
with open(attachment_path, "wb") as f:
|
||||||
|
f.write(doc.content)
|
||||||
|
attachment_files[identifier] = {"path": attachment_path, "filename": attachment_filename}
|
||||||
|
task_logger.info(f"成功生成附件 '{identifier}' 文件: {attachment_filename}")
|
||||||
|
except Exception as attachment_error:
|
||||||
|
task_logger.error(f"生成附件 '{identifier}' 文件时出错: {attachment_error}", exc_info=True)
|
||||||
|
|
||||||
# 5. 任务成功,更新最终状态
|
# 5. 任务成功,更新最终状态
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
duration = end_time - task_state["task_start_time"]
|
duration = end_time - task_state["task_start_time"]
|
||||||
@@ -804,6 +823,7 @@ async def _perform_translation(
|
|||||||
"error_flag": False,
|
"error_flag": False,
|
||||||
"task_end_time": end_time,
|
"task_end_time": end_time,
|
||||||
"downloadable_files": downloadable_files,
|
"downloadable_files": downloadable_files,
|
||||||
|
"attachment_files": attachment_files,
|
||||||
})
|
})
|
||||||
task_logger.info(f"翻译成功完成,用时 {duration:.2f} 秒。")
|
task_logger.info(f"翻译成功完成,用时 {duration:.2f} 秒。")
|
||||||
|
|
||||||
@@ -867,7 +887,7 @@ async def _start_translation_task(
|
|||||||
"original_filename_stem": Path(original_filename).stem,
|
"original_filename_stem": Path(original_filename).stem,
|
||||||
"original_filename": original_filename,
|
"original_filename": original_filename,
|
||||||
"task_start_time": time.time(), "task_end_time": 0, "current_task_ref": None,
|
"task_start_time": time.time(), "task_end_time": 0, "current_task_ref": None,
|
||||||
"temp_dir": None, "downloadable_files": {},
|
"temp_dir": None, "downloadable_files": {}, "attachment_files": {},
|
||||||
})
|
})
|
||||||
|
|
||||||
log_history = tasks_log_histories[task_id]
|
log_history = tasks_log_histories[task_id]
|
||||||
@@ -1014,7 +1034,7 @@ async def service_release_task(task_id: str):
|
|||||||
@service_router.get(
|
@service_router.get(
|
||||||
"/status/{task_id}",
|
"/status/{task_id}",
|
||||||
summary="获取任务状态",
|
summary="获取任务状态",
|
||||||
description="根据任务ID获取任务的当前状态。当 `download_ready` 为 `true` 时,`downloads` 对象中会包含可用的下载链接。",
|
description="根据任务ID获取任务的当前状态。当 `download_ready` 为 `true` 时,`downloads` 和 `attachment` 对象中会包含可用的下载链接。",
|
||||||
responses={
|
responses={
|
||||||
200: {
|
200: {
|
||||||
"description": "成功获取任务状态。",
|
"description": "成功获取任务状态。",
|
||||||
@@ -1028,7 +1048,7 @@ async def service_release_task(task_id: str):
|
|||||||
"status_message": "正在处理 'annual_report.pdf'...",
|
"status_message": "正在处理 'annual_report.pdf'...",
|
||||||
"error_flag": False, "download_ready": False, "original_filename_stem": "annual_report",
|
"error_flag": False, "download_ready": False, "original_filename_stem": "annual_report",
|
||||||
"original_filename": "annual_report.pdf", "task_start_time": 1678889400.0,
|
"original_filename": "annual_report.pdf", "task_start_time": 1678889400.0,
|
||||||
"task_end_time": 0, "downloads": {}
|
"task_end_time": 0, "downloads": {}, "attachment": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"completed_markdown": {
|
"completed_markdown": {
|
||||||
@@ -1043,6 +1063,26 @@ async def service_release_task(task_id: str):
|
|||||||
"html": "/service/download/b2865b93/html",
|
"html": "/service/download/b2865b93/html",
|
||||||
"markdown": "/service/download/b2865b93/markdown",
|
"markdown": "/service/download/b2865b93/markdown",
|
||||||
"markdown_zip": "/service/download/b2865b93/markdown_zip"
|
"markdown_zip": "/service/download/b2865b93/markdown_zip"
|
||||||
|
},
|
||||||
|
"attachment": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"completed_with_attachment": {
|
||||||
|
"summary": "已完成 (带附件)",
|
||||||
|
"value": {
|
||||||
|
"task_id": "g1h2i3j4", "is_processing": False,
|
||||||
|
"status_message": "翻译成功!用时 125.00 秒。",
|
||||||
|
"error_flag": False, "download_ready": True,
|
||||||
|
"original_filename_stem": "complex_document",
|
||||||
|
"original_filename": "complex_document.docx",
|
||||||
|
"task_start_time": 1678891000.0,
|
||||||
|
"task_end_time": 1678891125.0,
|
||||||
|
"downloads": {
|
||||||
|
"docx": "/service/download/g1h2i3j4/docx",
|
||||||
|
"html": "/service/download/g1h2i3j4/html"
|
||||||
|
},
|
||||||
|
"attachment": {
|
||||||
|
"glossary": "/service/attachment/g1h2i3j4/glossary"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1062,7 +1102,8 @@ async def service_release_task(task_id: str):
|
|||||||
"xlsx": "/service/download/d7e8f9a0/xlsx",
|
"xlsx": "/service/download/d7e8f9a0/xlsx",
|
||||||
"csv": "/service/download/d7e8f9a0/csv",
|
"csv": "/service/download/d7e8f9a0/csv",
|
||||||
"html": "/service/download/d7e8f9a0/html"
|
"html": "/service/download/d7e8f9a0/html"
|
||||||
}
|
},
|
||||||
|
"attachment": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"completed_docx": {
|
"completed_docx": {
|
||||||
@@ -1076,7 +1117,8 @@ async def service_release_task(task_id: str):
|
|||||||
"downloads": {
|
"downloads": {
|
||||||
"docx": "/service/download/f8a9c1b2/docx",
|
"docx": "/service/download/f8a9c1b2/docx",
|
||||||
"html": "/service/download/f8a9c1b2/html"
|
"html": "/service/download/f8a9c1b2/html"
|
||||||
}
|
},
|
||||||
|
"attachment": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"completed_epub": {
|
"completed_epub": {
|
||||||
@@ -1090,7 +1132,8 @@ async def service_release_task(task_id: str):
|
|||||||
"downloads": {
|
"downloads": {
|
||||||
"epub": "/service/download/e9b8d7c6/epub",
|
"epub": "/service/download/e9b8d7c6/epub",
|
||||||
"html": "/service/download/e9b8d7c6/html"
|
"html": "/service/download/e9b8d7c6/html"
|
||||||
}
|
},
|
||||||
|
"attachment": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
# --- HTML STATUS EXAMPLE START ---
|
# --- HTML STATUS EXAMPLE START ---
|
||||||
@@ -1104,7 +1147,8 @@ async def service_release_task(task_id: str):
|
|||||||
"task_end_time": 1678890115.78,
|
"task_end_time": 1678890115.78,
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"html": "/service/download/a1b2c3d4/html"
|
"html": "/service/download/a1b2c3d4/html"
|
||||||
}
|
},
|
||||||
|
"attachment": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
# --- HTML STATUS EXAMPLE END ---
|
# --- HTML STATUS EXAMPLE END ---
|
||||||
@@ -1115,7 +1159,7 @@ async def service_release_task(task_id: str):
|
|||||||
"status_message": "翻译过程中发生错误: LLM API key is invalid",
|
"status_message": "翻译过程中发生错误: LLM API key is invalid",
|
||||||
"error_flag": True, "download_ready": False, "original_filename_stem": "bad_config",
|
"error_flag": True, "download_ready": False, "original_filename_stem": "bad_config",
|
||||||
"original_filename": "bad_config.json", "task_start_time": 1678889600.0,
|
"original_filename": "bad_config.json", "task_start_time": 1678889600.0,
|
||||||
"task_end_time": 1678889610.0, "downloads": {}
|
"task_end_time": 1678889610.0, "downloads": {}, "attachment": {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1136,6 +1180,11 @@ async def service_get_status(
|
|||||||
for file_type in task_state["downloadable_files"].keys():
|
for file_type in task_state["downloadable_files"].keys():
|
||||||
downloads[file_type] = f"/service/download/{task_id}/{file_type}"
|
downloads[file_type] = f"/service/download/{task_id}/{file_type}"
|
||||||
|
|
||||||
|
attachments = {}
|
||||||
|
if task_state.get("download_ready") and task_state.get("attachment_files"):
|
||||||
|
for identifier in task_state["attachment_files"].keys():
|
||||||
|
attachments[identifier] = f"/service/attachment/{task_id}/{identifier}"
|
||||||
|
|
||||||
return JSONResponse(content={
|
return JSONResponse(content={
|
||||||
"task_id": task_id,
|
"task_id": task_id,
|
||||||
"is_processing": task_state["is_processing"],
|
"is_processing": task_state["is_processing"],
|
||||||
@@ -1146,7 +1195,8 @@ async def service_get_status(
|
|||||||
"original_filename": task_state.get("original_filename"),
|
"original_filename": task_state.get("original_filename"),
|
||||||
"task_start_time": task_state["task_start_time"],
|
"task_start_time": task_state["task_start_time"],
|
||||||
"task_end_time": task_state["task_end_time"],
|
"task_end_time": task_state["task_end_time"],
|
||||||
"downloads": downloads
|
"downloads": downloads,
|
||||||
|
"attachment": attachments
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1218,6 +1268,42 @@ async def service_download_file(
|
|||||||
return FileResponse(path=file_path, media_type=media_type, filename=filename)
|
return FileResponse(path=file_path, media_type=media_type, filename=filename)
|
||||||
|
|
||||||
|
|
||||||
|
@service_router.get(
|
||||||
|
"/attachment/{task_id}/{identifier}",
|
||||||
|
summary="下载附件文件",
|
||||||
|
description="根据任务ID和附件标识符下载在翻译过程中生成的附加文件,例如自动生成的术语表。",
|
||||||
|
responses={
|
||||||
|
200: {
|
||||||
|
"description": "成功返回文件流。文件名通过 Content-Disposition 头指定。",
|
||||||
|
"content": {
|
||||||
|
"application/octet-stream": {"schema": {"type": "string", "format": "binary"}},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
404: {"description": "任务ID不存在,或该任务没有指定的附件,或临时文件已丢失。"},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
async def service_download_attachment(
|
||||||
|
task_id: str = FastApiPath(..., description="已完成任务的ID", examples=["g1h2i3j4"]),
|
||||||
|
identifier: str = FastApiPath(..., description="要下载的附件的标识符。", examples=["glossary"])
|
||||||
|
):
|
||||||
|
task_state = tasks_state.get(task_id)
|
||||||
|
if not task_state:
|
||||||
|
raise HTTPException(status_code=404, detail=f"找不到任务ID '{task_id}'。")
|
||||||
|
|
||||||
|
attachment_info = task_state.get("attachment_files", {}).get(identifier)
|
||||||
|
if not attachment_info or not os.path.exists(attachment_info.get("path")):
|
||||||
|
raise HTTPException(status_code=404,
|
||||||
|
detail=f"任务 '{task_id}' 不存在标识符为 '{identifier}' 的附件,或文件已丢失。")
|
||||||
|
|
||||||
|
file_path = attachment_info["path"]
|
||||||
|
filename = attachment_info["filename"]
|
||||||
|
|
||||||
|
# Use a generic media type as attachments can be of various formats
|
||||||
|
media_type = "application/octet-stream"
|
||||||
|
|
||||||
|
return FileResponse(path=file_path, media_type=media_type, filename=filename)
|
||||||
|
|
||||||
|
|
||||||
@service_router.get(
|
@service_router.get(
|
||||||
"/content/{task_id}/{file_type}",
|
"/content/{task_id}/{file_type}",
|
||||||
summary="下载翻译结果内容 (JSON)",
|
summary="下载翻译结果内容 (JSON)",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -53,8 +53,6 @@ class AiTranslator(Translator[T]):
|
|||||||
logger=self.logger,
|
logger=self.logger,
|
||||||
)
|
)
|
||||||
self.glossary_agent = GlossaryAgent(glossary_agent_config)
|
self.glossary_agent = GlossaryAgent(glossary_agent_config)
|
||||||
def get_glossary_dict(self):
|
|
||||||
return self.glossary_dict_gen
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def translate(self, document: T) -> Document:
|
def translate(self, document: T) -> Document:
|
||||||
...
|
...
|
||||||
|
|||||||
@@ -217,8 +217,8 @@ class HtmlTranslator(AiTranslator):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
if self.glossary_agent:
|
if self.glossary_agent:
|
||||||
glossary_dict = await self.glossary_agent.send_segments_async(original_texts, self.chunk_size)
|
self.glossary_dict_gen = await self.glossary_agent.send_segments_async(original_texts, self.chunk_size)
|
||||||
self.translate_agent.update_glossary_dict(glossary_dict)
|
self.translate_agent.update_glossary_dict(self.glossary_dict_gen)
|
||||||
|
|
||||||
translated_texts = await self.translate_agent.send_segments_async(original_texts, self.chunk_size)
|
translated_texts = await self.translate_agent.send_segments_async(original_texts, self.chunk_size)
|
||||||
document.content = await asyncio.to_thread(
|
document.content = await asyncio.to_thread(
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ class SrtTranslator(AiTranslator):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
if self.glossary_agent:
|
if self.glossary_agent:
|
||||||
glossary_dict = await self.glossary_agent.send_segments_async(original_texts, self.chunk_size)
|
self.glossary_dict_gen = await self.glossary_agent.send_segments_async(original_texts, self.chunk_size)
|
||||||
self.translate_agent.update_glossary_dict(glossary_dict)
|
self.translate_agent.update_glossary_dict(self.glossary_dict_gen)
|
||||||
|
|
||||||
# --- 步骤 2: 调用翻译Agent (异步) ---
|
# --- 步骤 2: 调用翻译Agent (异步) ---
|
||||||
translated_texts = await self.translate_agent.send_segments_async(original_texts, self.chunk_size)
|
translated_texts = await self.translate_agent.send_segments_async(original_texts, self.chunk_size)
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ class TXTTranslator(AiTranslator):
|
|||||||
chunks: list[str] = split_markdown_text(document.content.decode(), max_block_size=self.chunk_size)
|
chunks: list[str] = split_markdown_text(document.content.decode(), max_block_size=self.chunk_size)
|
||||||
|
|
||||||
if self.glossary_agent:
|
if self.glossary_agent:
|
||||||
glossary_dict = await self.glossary_agent.send_segments_async(chunks, self.chunk_size)
|
self.glossary_dict_gen = await self.glossary_agent.send_segments_async(chunks, self.chunk_size)
|
||||||
self.translate_agent.update_glossary_dict(glossary_dict)
|
self.translate_agent.update_glossary_dict(self.glossary_dict_gen)
|
||||||
|
|
||||||
self.logger.info(f"txt分为{len(chunks)}块")
|
self.logger.info(f"txt分为{len(chunks)}块")
|
||||||
result: list[str] = await self.translate_agent.send_chunks_async(chunks)
|
result: list[str] = await self.translate_agent.send_chunks_async(chunks)
|
||||||
|
|||||||
@@ -61,4 +61,5 @@ class Workflow(ABC, Generic[T_Config, T_original, T_Translated]):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def get_attachment(self):
|
def get_attachment(self):
|
||||||
|
print(f"attachment:{self.attachment.attachment_dict}")
|
||||||
return self.attachment
|
return self.attachment
|
||||||
|
|||||||
Reference in New Issue
Block a user