diff --git a/.gitignore b/.gitignore index 8491508..3622162 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ tests/ docutranslate/output/ # Virtual environments .venv +app.spec diff --git a/.idea/workspace.xml b/.idea/workspace.xml index d0e7186..0d6a33a 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,7 +5,12 @@ + + + + + @@ -63,6 +68,7 @@ "Python.test4.executor": "Run", "Python.translater.executor": "Run", "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true", "RunOnceActivity.git.unshallow": "true", "git-widget-placeholder": "main", "last_opened_file_path": "C:/Users/jxgm/Desktop/FileTranslate/tests", @@ -71,7 +77,7 @@ "node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.tslint": "(autodetect)", "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "preferences.general", + "settings.editor.selected.configurable": "preferences.pluginManager", "vue.rearranger.settings.migration": "true" } }]]> @@ -84,6 +90,7 @@ + @@ -112,6 +119,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -161,6 +312,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -207,7 +427,7 @@ - + @@ -221,7 +441,30 @@ - + + + + + + + + + + + + + + + + + + + + + + + + @@ -330,10 +573,10 @@ + + - - @@ -392,7 +635,8 @@ - + + @@ -401,13 +645,13 @@ - - + + - + @@ -417,7 +661,7 @@ - + diff --git a/README.md b/README.md index 4011ab3..d7f649f 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,8 @@ translater = FileTranslater(base_url="", # 默认的模型baseurl ```python translater.translate_file(r"<要翻译的文件路径>", to_lang="中文", - formula=False, # 是否启用公式识别 - code=False, # 是否启用代码识别 + formula=True, # 是否启用公式识别 + code=True, # 是否启用代码识别 refine=False, # 是否在翻译前先修正一遍markdown文本(较耗时) output_format="markdown", # "markdown"与"html"两种输出格式 output_dir="./output", # 默认输出文件夹 diff --git a/docutranslate/app.py b/docutranslate/app.py index 0faf643..8c37931 100644 --- a/docutranslate/app.py +++ b/docutranslate/app.py @@ -7,7 +7,7 @@ from typing import AsyncGenerator import uvicorn from fastapi import FastAPI, File, Form, UploadFile, Request, HTTPException -from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse # Added JSONResponse +from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse from fastapi.templating import Jinja2Templates # 假设这些导入能够正确找到你的库代码 @@ -19,6 +19,7 @@ app = FastAPI() # --- 异步队列和自定义日志处理器设置 --- log_queue = asyncio.Queue() +SHUTDOWN_SENTINEL = object() # 使用一个唯一的对象作为哨兵 class AsyncQueueHandler(logging.Handler): @@ -28,15 +29,21 @@ class AsyncQueueHandler(logging.Handler): def emit(self, record: logging.LogRecord): log_entry = self.format(record) + # 在 FastAPI 应用上下文中运行时,尝试使用 app.state.main_event_loop main_loop = getattr(app.state, "main_event_loop", None) - if main_loop and main_loop.is_running(): # Ensure loop is running + if main_loop and main_loop.is_running(): main_loop.call_soon_threadsafe(self.queue.put_nowait, log_entry) else: - # Fallback if loop not available or not running (e.g. during shutdown or tests) - # This might happen very early in startup before main_event_loop is set, or during shutdown. - # In a typical running app, main_loop should be available. + # 如果主循环不可用或未运行(例如,在测试中或非常早期的启动/非常晚的关闭阶段) + # 这是一个备用方案,但不如 call_soon_threadsafe 安全 try: - self.queue.put_nowait(log_entry) # Less safe if loop context is critical + # 如果在主事件循环上下文之外,或者事件循环已停止, + # put_nowait 可能仍然有效,因为它不依赖于正在运行的特定循环来放置项目 + # 但理想情况下,日志记录应在主循环活跃时发生。 + self.queue.put_nowait(log_entry) + except RuntimeError: # 例如,如果队列本身与已关闭的循环关联 + print(f"Error putting log to queue (loop likely closed): {log_entry[:100]}...") # 记录部分日志以避免过长输出 + self.handleError(record) # 调用基类的错误处理 except Exception as e: print(f"Error putting log to queue (no main loop/not running): {e}") self.handleError(record) @@ -49,8 +56,28 @@ async def startup_event(): queue_handler.setLevel(logging.INFO) ui_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') queue_handler.setFormatter(ui_formatter) + # 检查 translater_logger 是否已经有这个类型的 handler,避免重复添加 if not any(isinstance(h, AsyncQueueHandler) for h in translater_logger.handlers): translater_logger.addHandler(queue_handler) + translater_logger.info("Application startup complete. Log queue handler configured.") + + +@app.on_event("shutdown") +async def shutdown_event(): + translater_logger.info("Application shutting down. Signaling log streamer to stop.") + # 向队列发送哨兵值以停止日志流生成器 + await log_queue.put(SHUTDOWN_SENTINEL) + # (可选) 短暂等待,以允许生成器处理哨兵并退出 + await asyncio.sleep(0.1) + translater_logger.info("Log streamer signaled.") + # (可选) 清空队列中剩余的日志,如果不想在关闭时处理它们 + # while not log_queue.empty(): + # try: + # log_queue.get_nowait() + # log_queue.task_done() + # except asyncio.QueueEmpty: + # break + # translater_logger.info("Log queue cleared during shutdown.") # --- 全局状态 --- @@ -58,114 +85,290 @@ current_translation_state = { "markdown_content": None, "html_content": None, "original_filename_stem": None, "error": None, "is_processing": False } -templates = Jinja2Templates(directory=".") +templates = Jinja2Templates(directory=".") # 假设模板在当前目录或使用字符串模板 # --- HTML 模板字符串 --- +# --- HTML 模板字符串 (修改后) --- HTML_TEMPLATE_STR = """ - 文档翻译器 + DocuTranslate - 文档翻译器 - + + 📄 DocuTranslate + + + - API 配置 (所有均为必填项) + ⚙️ API 配置 - - API 地址 (Base URL) - + + API 地址 + - - API 密钥 (API Key) - + + API 密钥 + - 模型 ID (Model ID) - + 模型 ID + + - 待翻译文档 + 选择文档 + - 选项 - 目标语言: - 启用公式识别 - 启用代码块识别 - 翻译前优化 Markdown + 翻译选项 + + + 目标语言 + + 中文 (简体) + English + 日本語 + 한국어 + Français + Español + Deutsch + Русский + 自定义... + + + + + + + 公式识别 + 代码块识别 + 优化 Markdown + - 翻译文档 + + 开始翻译 - - 下载: - 下载 Markdown - 下载 HTML + + 下载翻译结果: + 下载 Markdown (.md) + 下载 HTML (.html) - 日志: + 实时日志: +