更改构建方式

This commit is contained in:
xunbu
2025-05-19 13:27:25 +08:00
parent 7a1a36fc9f
commit 2c8c07572e
3 changed files with 92 additions and 768 deletions

131
.idea/workspace.xml generated
View File

@@ -5,9 +5,22 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="6b18b44a-df57-4212-a857-9e291ebe5dd2" name="更改" comment=""> <list default="true" id="6b18b44a-df57-4212-a857-9e291ebe5dd2" name="更改" comment="">
<change afterPath="$PROJECT_DIR$/src/docutranslate/app.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/app.py" beforeDir="false" afterPath="$PROJECT_DIR$/docutranslate/app.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/docutranslate/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/utils/convert.py" beforeDir="false" afterPath="$PROJECT_DIR$/docutranslate/utils/convert.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/docutranslate/agents/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/agents/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/agents/agent.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/agents/agent.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/agents/markdown_agent.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/agents/markdown_agent.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/app.py" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/cli.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/cli.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/logger/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/logger/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/logger/logger.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/logger/logger.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/translater.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/translater.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/utils/__init__.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/utils/__init__.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/utils/convert.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/utils/convert.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/utils/docling_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/utils/docling_utils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/utils/markdown_splitter.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/utils/markdown_splitter.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/utils/markdown_utils.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/docutranslate/utils/markdown_utils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
@@ -20,6 +33,7 @@
<list> <list>
<option value="Python Script" /> <option value="Python Script" />
<option value="HTML File" /> <option value="HTML File" />
<option value="CSS File" />
</list> </list>
</option> </option>
</component> </component>
@@ -37,58 +51,59 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;DefaultHtmlFileTemplate&quot;: &quot;HTML File&quot;, "DefaultHtmlFileTemplate": "HTML File",
&quot;JavaScript 调试.output.html (1).executor&quot;: &quot;Run&quot;, "JavaScript 调试.output.html (1).executor": "Run",
&quot;JavaScript 调试.output.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.output.html.executor": "Run",
&quot;JavaScript 调试.regex.md_中文.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.regex.md_中文.html.executor": "Run",
&quot;JavaScript 调试.regex_中文.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.regex_中文.html.executor": "Run",
&quot;JavaScript 调试.test.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.test.html.executor": "Run",
&quot;JavaScript 调试.test2.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.test2.html.executor": "Run",
&quot;JavaScript 调试.test2_英文.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.test2_英文.html.executor": "Run",
&quot;JavaScript 调试.test4-1_中文.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.test4-1_中文.html.executor": "Run",
&quot;JavaScript 调试.互联网认证授权机制.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.互联网认证授权机制.html.executor": "Run",
&quot;JavaScript 调试.互联网认证授权机制_英文.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.互联网认证授权机制_英文.html.executor": "Run",
&quot;JavaScript 调试.毕业论文_英文.html.executor&quot;: &quot;Run&quot;, "JavaScript 调试.毕业论文_英文.html.executor": "Run",
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;, "ModuleVcsDetector.initialDetectionPerformed": "true",
&quot;Python 测试.Python 测试 (markdown_mask.py 内).executor&quot;: &quot;Run&quot;, "Python 测试.Python 测试 (markdown_mask.py 内).executor": "Run",
&quot;Python 测试.markdown_mask.Test.test_basic_link_masking 的 Python 测试.executor&quot;: &quot;Run&quot;, "Python 测试.markdown_mask.Test.test_basic_link_masking 的 Python 测试.executor": "Run",
&quot;Python 测试.pytest (test_html.py 内).executor&quot;: &quot;Run&quot;, "Python 测试.pytest (test_html.py 内).executor": "Run",
&quot;Python.2test2 (1).executor&quot;: &quot;Run&quot;, "Python.2test2 (1).executor": "Run",
&quot;Python.PDFtranslater (1).executor&quot;: &quot;Run&quot;, "Python.PDFtranslater (1).executor": "Run",
&quot;Python.PDFtranslater (2).executor&quot;: &quot;Run&quot;, "Python.PDFtranslater (2).executor": "Run",
&quot;Python.agent.executor&quot;: &quot;Debug&quot;, "Python.agent.executor": "Debug",
&quot;Python.agent_utils.executor&quot;: &quot;Run&quot;, "Python.agent_utils.executor": "Run",
&quot;Python.app (1).executor&quot;: &quot;Run&quot;, "Python.app (1).executor": "Run",
&quot;Python.app.executor&quot;: &quot;Run&quot;, "Python.app.executor": "Run",
&quot;Python.app2.executor&quot;: &quot;Run&quot;, "Python.app2.executor": "Run",
&quot;Python.app_test (1).executor&quot;: &quot;Run&quot;, "Python.app_test (1).executor": "Run",
&quot;Python.convert.executor&quot;: &quot;Run&quot;, "Python.convert.executor": "Run",
&quot;Python.markdown_splitter.executor&quot;: &quot;Debug&quot;, "Python.markdown_splitter.executor": "Debug",
&quot;Python.markdown_utils.executor&quot;: &quot;Run&quot;, "Python.markdown_utils.executor": "Run",
&quot;Python.test.executor&quot;: &quot;Run&quot;, "Python.test.executor": "Run",
&quot;Python.test1.executor&quot;: &quot;Run&quot;, "Python.test1.executor": "Run",
&quot;Python.test2.executor&quot;: &quot;Run&quot;, "Python.test2.executor": "Run",
&quot;Python.test3.executor&quot;: &quot;Run&quot;, "Python.test3.executor": "Run",
&quot;Python.test4.executor&quot;: &quot;Run&quot;, "Python.test4.executor": "Run",
&quot;Python.testhtml.executor&quot;: &quot;Run&quot;, "Python.testhtml.executor": "Run",
&quot;Python.translater.executor&quot;: &quot;Run&quot;, "Python.translater.executor": "Run",
&quot;Python.切分测试.executor&quot;: &quot;Run&quot;, "Python.切分测试.executor": "Run",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager&quot;: &quot;true&quot;, "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, "RunOnceActivity.git.unshallow": "true",
&quot;git-widget-placeholder&quot;: &quot;main&quot;, "git-widget-placeholder": "main",
&quot;last_opened_file_path&quot;: &quot;C:/Users/jxgm/Desktop/FileTranslate/dist/DocuTranslate&quot;, "last_opened_file_path": "C:/Users/jxgm/Desktop/FileTranslate/dist/DocuTranslate",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "list.type.of.created.stylesheet": "CSS",
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.detected.package.tslint": "true",
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "node.js.selected.package.tslint": "(autodetect)",
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;, "nodejs_package_manager_path": "npm",
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot; "settings.editor.selected.configurable": "preferences.pluginManager",
"vue.rearranger.settings.migration": "true"
} }
}</component> }]]></component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS"> <key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\jxgm\Desktop\FileTranslate\dist\DocuTranslate" /> <recent name="C:\Users\jxgm\Desktop\FileTranslate\dist\DocuTranslate" />
@@ -98,11 +113,11 @@
<recent name="C:\Users\jxgm\Desktop\FileTranslate\dist\app" /> <recent name="C:\Users\jxgm\Desktop\FileTranslate\dist\app" />
</key> </key>
<key name="MoveFile.RECENT_KEYS"> <key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\jxgm\Desktop\FileTranslate\src" />
<recent name="C:\Users\jxgm\Desktop\FileTranslate" /> <recent name="C:\Users\jxgm\Desktop\FileTranslate" />
<recent name="C:\Users\jxgm\Desktop\FileTranslate\dist\DocuTranslate" /> <recent name="C:\Users\jxgm\Desktop\FileTranslate\dist\DocuTranslate" />
<recent name="C:\Users\jxgm\Desktop\FileTranslate\dist\app" /> <recent name="C:\Users\jxgm\Desktop\FileTranslate\dist\app" />
<recent name="C:\Users\jxgm\Desktop\FileTranslate\tests\files" /> <recent name="C:\Users\jxgm\Desktop\FileTranslate\tests\files" />
<recent name="C:\Users\jxgm\Desktop\FileTranslate\tests\resource" />
</key> </key>
</component> </component>
<component name="RunManager" selected="Python.app_test (1)"> <component name="RunManager" selected="Python.app_test (1)">
@@ -562,7 +577,11 @@
<workItem from="1747553452592" duration="4624000" /> <workItem from="1747553452592" duration="4624000" />
<workItem from="1747578049178" duration="3519000" /> <workItem from="1747578049178" duration="3519000" />
<workItem from="1747583338894" duration="404000" /> <workItem from="1747583338894" duration="404000" />
<workItem from="1747612671258" duration="614000" /> <workItem from="1747612671258" duration="2050000" />
<workItem from="1747617855241" duration="190000" />
<workItem from="1747619759939" duration="205000" />
<workItem from="1747624297447" duration="65000" />
<workItem from="1747628254543" duration="4111000" />
</task> </task>
<servers /> <servers />
</component> </component>
@@ -570,7 +589,7 @@
<option name="version" value="3" /> <option name="version" value="3" />
</component> </component>
<component name="com.intellij.coverage.CoverageDataManagerImpl"> <component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/filetranslate$app_test__1_.coverage" NAME="app_test (1) 覆盖结果" MODIFIED="1747583505329" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" /> <SUITE FILE_PATH="coverage/filetranslate$app_test__1_.coverage" NAME="app_test (1) 覆盖结果" MODIFIED="1747631847708" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$test.coverage" NAME="test 覆盖结果" MODIFIED="1747472297913" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" /> <SUITE FILE_PATH="coverage/filetranslate$test.coverage" NAME="test 覆盖结果" MODIFIED="1747472297913" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$convert.coverage" NAME="convert 覆盖结果" MODIFIED="1746963490689" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/utils" /> <SUITE FILE_PATH="coverage/filetranslate$convert.coverage" NAME="convert 覆盖结果" MODIFIED="1746963490689" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/utils" />
<SUITE FILE_PATH="coverage/PDFtranslate$PDFtranslater__1_.coverage" NAME="PDFtranslater (1) 覆盖结果" MODIFIED="1746633258205" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/pdftranslate_packages" /> <SUITE FILE_PATH="coverage/PDFtranslate$PDFtranslater__1_.coverage" NAME="PDFtranslater (1) 覆盖结果" MODIFIED="1746633258205" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/pdftranslate_packages" />

View File

@@ -2,728 +2,26 @@ import asyncio
import io import io
import logging import logging
import time import time
import urllib
from pathlib import Path from pathlib import Path
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from urllib.parse import quote from urllib.parse import quote
import uvicorn import uvicorn
from fastapi import FastAPI, File, Form, UploadFile, Request, HTTPException from fastapi import FastAPI, File, Form, UploadFile, Request, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse,FileResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from docutranslate import FileTranslater from docutranslate import FileTranslater
from docutranslate.logger import translater_logger from docutranslate.logger import translater_logger
# --- HTML模板 (JS part needs modification) ---
# language=HTML
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DocuTranslate</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@latest/css/pico.min.css">
<style>
body {
padding: 20px;
}
.container {
max-width: 800px;
margin: auto;
padding: 1rem;
}
.log-area {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
padding: 10px;
height: 300px;
overflow-y: scroll;
white-space: pre-wrap;
font-family: monospace;
margin-top: 1rem;
}
.error-message {
color: #d32f2f;
}
.success-message {
color: #2e7d32;
}
.form-group {
margin-bottom: 1rem;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.button-group {
margin-top: 1rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: right;
align-items: baseline;
}
.spacer{
flex-grow: 1;
}
details {
margin-bottom: 1rem;
}
.checkbox-group {
display: flex;
flex-wrap: wrap;
margin-bottom: 1rem;
}
#resultArea {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #eee;
}
#downloadButtons {
display: none;
margin-top: 1rem;
}
.hidden {
display: none !important;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 1000;
overflow: auto;
}
.modal-content {
background-color: #fff;
margin: 2% auto;
padding: 20px;
width: 90%;
max-width: 900px;
max-height: 90vh;
border-radius: 8px;
overflow: auto;
}
#previewFrame {
width: 100%;
min-height: 500px;
border: 1px solid #ddd;
}
#printFrame {
display: none;
}
/* Styles for drag and drop area */
#fileDropArea {
border: 2px dashed #ccc;
padding: 20px;
text-align: center;
cursor: pointer;
transition: background-color 0.2s ease-in-out, border-color 0.2s ease-in-out;
}
#fileDropArea.drag-over {
border-color: #1095c1; /* Pico primary color (定量替换 var(--pico-primary-focus)) */
background-color: #e7f5fa; /* Pico primary background (定量替换 var(--pico-primary-background)) */
}
#fileDropArea.file-selected {
border-color: #2e7d32; /* Pico success color (定量替换 var(--pico-form-element-valid-border-color, #2e7d32)) */
background-color: #e8f5e9; /* Light green (定量替换 var(--pico-form-element-valid-background-color, #e8f5e9)) */
}
#fileDropArea p { /* General style for <p> inside drop area */
margin: 0.5rem 0;
color: #555;
}
#fileNameDisplay {
margin-top: 0.5rem;
font-style: italic;
color: #333;
}
#fileNameDisplay.has-file {
font-style: normal;
font-weight: bold;
color: #1a531d; /* Darker green or success color (定量替换 var(--pico-form-element-valid-border-color, #1a531d)) */
}
#fileDropArea.input-error {
border-color: #d32f2f !important; /* (定量替换 var(--pico-form-element-invalid-border-color, #d32f2f)) */
}
#fileNameDisplay.input-error-text {
color: #d32f2f !important; /* (定量替换 var(--pico-form-element-invalid-border-color, #d32f2f)) */
font-weight: bold;
}
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<main class="container">
<h1>
<a href="https://github.com/xunbu/docutranslate" target="_blank">DocuTranslate</a>
</h1>
<form id="translateForm">
<!-- Modified File Input Area -->
<div class="form-group">
<label for="file">文档选择</label>
<div id="fileDropArea">
<input type="file" id="file" name="file" required style="display: none;">
<p id="fileDropPrompt">点击此处选择文件,或将文件拖拽到这里</p>
<div id="fileNameDisplay">未选择文件</div>
</div>
</div>
<div class="form-grid">
<div class="form-group">
<label for="to_lang">目标语言</label>
<select id="to_lang" name="to_lang">
<option value="中文">中文 (Chinese)</option>
<option value="English">英文 (English)</option>
<option value="日本語">日语 (Japanese)</option>
<option value="한국어">韩语 (Korean)</option>
<option value="Français">法语 (French)</option>
<option value="Deutsch">德语 (German)</option>
<option value="Español">西班牙语 (Spanish)</option>
<option value="Italiano">意大利语 (Italian)</option>
<option value="Português">葡萄牙语 (Portuguese)</option>
<option value="Русский">俄语 (Russian)</option>
<option value="العربية">阿拉伯语 (Arabic)</option>
<option value="हिन्दी">印地语 (Hindi)</option>
<option value="Nederlands">荷兰语 (Dutch)</option>
</select>
</div>
<div class="form-group">
<label>高级选项</label>
<div class="checkbox-group">
<label for="formula_ocr"><input type="checkbox" id="formula_ocr" name="formula_ocr">公式识别</label>
<label for="code_ocr"><input type="checkbox" id="code_ocr" name="code_ocr">代码识别</label>
<label for="refine_markdown"><input type="checkbox" id="refine_markdown"
name="refine_markdown">修正文本(耗时)</label>
</div>
</div>
</div>
<details>
<summary>API 配置</summary>
<div class="form-grid">
<div class="form-group">
<label for="platform_select">AI 平台</label>
<select id="platform_select" name="platform_select_ui">
<option value="custom">自定义接口</option>
<option value="https://api.openai.com/v1">OpenAI</option>
<option value="https://open.bigmodel.cn/api/paas/v4">智谱AI</option>
<option value="https://api.deepseek.com/v1">DeepSeek</option>
<option value="https://dashscope.aliyuncs.com/compatible-mode/v1">阿里云百炼
</option>
<option value="https://www.dmxapi.cn/v1">DMXAPI</option>
<option value="https://openrouter.ai/api/v1">OpenRouter</option>
<option value="https://ark.cn-beijing.volces.com/api/v3">火山引擎</option>
<option value="https://api.siliconflow.cn/v1">硅基流动</option>
</select>
</div>
<div class="form-group hidden" id="baseUrlGroup">
<label for="base_url">API 地址 (Base URL)</label>
<input type="text" id="base_url" name="base_url"
placeholder="https://api.openai.com/v1">
</div>
</div>
<div class="form-group">
<label for="apikey">API 密钥</label>
<input type="password" id="apikey" name="apikey" placeholder="平台对应的API Key"
required>
</div>
<div class="form-group">
<label for="model_id">模型 ID</label>
<input type="text" id="model_id" name="model_id" placeholder="模型id" required>
</div>
</details>
<button type="submit" id="submitButton" class="primary">开始翻译</button>
</form>
<div id="resultArea">
<p id="statusMessage"></p>
<div id="downloadButtons" class="button-group">
<h4>翻译结果</h4>
<div class="spacer"></div>
<a id="downloadMarkdown" href="#" role="button" class="outline">下载 Markdown</a>
<a id="downloadHtml" href="#" role="button" class="outline">下载 HTML</a>
<button id="downloadPdf" class="outline">下载 PDF</button>
<button id="previewHtml" class="outline">预览</button>
</div>
</div>
<h4 style="margin-top: 1.5rem;">运行日志</h4>
<div class="log-area" id="logArea"></div>
</main>
<div id="previewModal" class="modal">
<div class="modal-content">
<span id="closeModalBtn" style="cursor:pointer; float:right;">×</span>
<h3>HTML 预览</h3>
<iframe id="previewFrame"></iframe>
<div class="button-group">
<button id="printFromPreview" class="primary">打印/保存为PDF</button>
<button id="closePreviewBtn" class="outline">关闭</button>
</div>
</div>
</div>
<iframe id="printFrame" style="display:none;"></iframe>
<script>
const platformSelect = document.getElementById('platform_select');
const baseUrlGroup = document.getElementById('baseUrlGroup');
const baseUrlInput = document.getElementById('base_url');
const apikeyInput = document.getElementById('apikey');
const modelInput = document.getElementById('model_id');
const toLangSelect = document.getElementById('to_lang');
const formulaCheckbox = document.getElementById('formula_ocr');
const codeCheckbox = document.getElementById('code_ocr');
const refineCheckbox = document.getElementById('refine_markdown');
const form = document.getElementById('translateForm');
const submitButton = document.getElementById('submitButton');
const logArea = document.getElementById('logArea');
const statusMsg = document.getElementById('statusMessage');
const downloadBtns = document.getElementById('downloadButtons');
const markdownLink = document.getElementById('downloadMarkdown');
const htmlLink = document.getElementById('downloadHtml');
const previewHtmlBtn = document.getElementById('previewHtml');
const downloadPdfBtn = document.getElementById('downloadPdf');
const printFrameEl = document.getElementById('printFrame');
const modal = document.getElementById('previewModal');
const previewFrame = document.getElementById('previewFrame');
const closeModalButton = document.getElementById('closeModalBtn');
const closePreviewBtn = document.getElementById('closePreviewBtn');
const printFromPreview = document.getElementById('printFromPreview');
const fileInput = document.getElementById('file');
const fileDropArea = document.getElementById('fileDropArea');
const fileNameDisplay = document.getElementById('fileNameDisplay');
const fileDropPrompt = document.getElementById('fileDropPrompt');
let logPollIntervalId = null;
let statusPollIntervalId = null;
// let lastLogCount = 0; // No longer needed for fetching logs
let isTranslating = false;
function saveToStorage(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
console.warn("保存到本地存储失败:", e);
}
}
function getFromStorage(key, defaultValue = '') {
try {
return localStorage.getItem(key) || defaultValue;
} catch (e) {
console.warn("从本地存储读取失败:", e);
return defaultValue;
}
}
function updatePlatformUI() {
const selectedPlatformValue = platformSelect.value;
apikeyInput.value = getFromStorage(`translator_platform_${selectedPlatformValue}_apikey`);
modelInput.value = getFromStorage(`translator_platform_${selectedPlatformValue}_model_id`);
if (selectedPlatformValue === 'custom') {
baseUrlGroup.classList.remove('hidden');
baseUrlInput.required = true;
baseUrlInput.value = getFromStorage('translator_platform_custom_base_url');
} else {
baseUrlGroup.classList.add('hidden');
baseUrlInput.required = false;
baseUrlInput.value = selectedPlatformValue;
}
saveToStorage('translator_last_platform', selectedPlatformValue);
}
loadSettings();
platformSelect.addEventListener('change', updatePlatformUI);
apikeyInput.addEventListener('input', (e) => saveToStorage(`translator_platform_${platformSelect.value}_apikey`, e.target.value));
modelInput.addEventListener('input', (e) => saveToStorage(`translator_platform_${platformSelect.value}_model_id`, e.target.value));
baseUrlInput.addEventListener('input', (e) => {
if (platformSelect.value === 'custom') saveToStorage('translator_platform_custom_base_url', e.target.value);
});
toLangSelect.addEventListener('change', e => saveToStorage('translator_to_lang', e.target.value));
formulaCheckbox.addEventListener('change', e => saveToStorage('translator_formula_ocr', e.target.checked));
codeCheckbox.addEventListener('change', e => saveToStorage('translator_code_ocr', e.target.checked));
refineCheckbox.addEventListener('change', e => saveToStorage('translator_refine_markdown', e.target.checked));
[closeModalButton, closePreviewBtn].forEach(elem => elem.addEventListener('click', () => modal.style.display = 'none'));
window.addEventListener('click', (event) => {
if (event.target === modal) modal.style.display = 'none';
});
printFromPreview.addEventListener('click', () => {
try {
previewFrame.contentWindow.focus();
previewFrame.contentWindow.print();
} catch (err) {
console.error('打印预览内容失败:', err);
alert('打印失败,请尝试使用浏览器的打印功能 (Ctrl+P 或 ⌘+P)。');
}
});
fileDropArea.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
fileNameDisplay.textContent = `已选择: ${fileInput.files[0].name}`;
fileDropArea.classList.add('file-selected');
fileNameDisplay.classList.add('has-file');
fileDropPrompt.classList.add('hidden');
fileDropArea.classList.remove('input-error');
fileNameDisplay.classList.remove('input-error-text');
statusMsg.textContent = '';
statusMsg.className = '';
} else {
fileNameDisplay.textContent = '未选择文件';
fileDropArea.classList.remove('file-selected');
fileNameDisplay.classList.remove('has-file');
fileDropPrompt.classList.remove('hidden');
}
});
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
fileDropArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
fileDropArea.addEventListener(eventName, () => {
if (!fileDropArea.classList.contains('file-selected')) {
fileDropArea.classList.add('drag-over');
}
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
fileDropArea.addEventListener(eventName, () => {
fileDropArea.classList.remove('drag-over');
}, false);
});
fileDropArea.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
fileInput.files = files;
const event = new Event('change', {bubbles: true});
fileInput.dispatchEvent(event);
}
}, false);
async function pollLogs() {
try {
// const response = await fetch(`/get-logs?since=${lastLogCount}`); // OLD
const response = await fetch('/get-logs'); // NEW: No 'since' parameter
if (!response.ok) {
console.warn(`Log polling failed: ${response.status}`);
return;
}
const data = await response.json();
if (data.logs && data.logs.length > 0) {
data.logs.forEach(log => {
const escapedLog = log.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
logArea.innerHTML += escapedLog + "<br>";
});
logArea.scrollTop = logArea.scrollHeight; // Scroll to bottom
}
// lastLogCount = data.total_count; // OLD: No longer tracking count this way
} catch (error) {
console.warn("Error polling logs:", error);
}
}
async function pollStatus() {
try {
const response = await fetch('/get-status');
if (!response.ok) {
console.warn(`Status polling failed: ${response.status}`);
statusMsg.textContent = `状态更新失败 (${response.status})`;
statusMsg.className = 'error-message';
return;
}
const status = await response.json();
statusMsg.textContent = status.status_message || '正在获取状态...';
statusMsg.className = status.error_flag ? 'error-message' : 'success-message';
if (!status.is_processing) {
stopPolling();
submitButton.disabled = false;
submitButton.removeAttribute('aria-busy');
submitButton.textContent = '开始翻译';
submitButton.classList.remove('secondary', 'contrast');
submitButton.classList.add('primary');
isTranslating = false;
if (status.download_ready && !status.error_flag) {
markdownLink.href = status.markdown_url;
markdownLink.setAttribute('download', status.original_filename_stem + '_translated.md');
htmlLink.href = status.html_url;
htmlLink.setAttribute('download', status.original_filename_stem + '_translated.html');
let htmlUrl = status.html_url;
let fileName = status.original_filename_stem;
previewHtmlBtn.onclick = function () {
const currentHtmlUrl = htmlUrl;
const currentFileName = fileName;
fetch(currentHtmlUrl)
.then(resp => {
if (!resp.ok) throw new Error(`HTTP error ${resp.status}`);
return resp.text();
})
.then(html => {
const blob = new Blob([html], {type: 'text/html'});
const blobUrl = URL.createObjectURL(blob);
previewFrame.src = blobUrl;
previewFrame.onload = function () {
try {
previewFrame.contentWindow.document.title = currentFileName + '_translated';
URL.revokeObjectURL(blobUrl);
} catch (e) {
console.warn('无法设置iframe标题或释放Blob URL', e);
}
};
modal.style.display = 'block';
})
.catch(err => {
console.error('预览: 获取HTML内容失败:', err);
statusMsg.textContent = '获取HTML内容失败无法预览。';
statusMsg.className = 'error-message';
});
};
downloadPdfBtn.onclick = function () {
downloadPdfBtn.disabled = true;
downloadPdfBtn.textContent = '准备PDF...';
const currentHtmlUrl = htmlUrl;
const currentFileName = fileName;
const iframe = printFrameEl;
fetch(currentHtmlUrl)
.then(resp => {
if (!resp.ok) throw new Error(`HTTP error ${resp.status} for PDF HTML`);
return resp.text();
})
.then(htmlContent => {
iframe.onload = () => {
iframe.onload = null;
try {
const iframeWindow = iframe.contentWindow;
if (!iframeWindow) throw new Error("无法访问打印框架。");
iframeWindow.document.title = currentFileName + '_translated.pdf';
iframeWindow.focus();
iframeWindow.print();
} catch (err) {
console.error('打印PDF出错:', err);
statusMsg.textContent = '无法直接生成PDF。请预览HTML后使用浏览器的打印功能 (Ctrl+P) 保存。';
statusMsg.className = 'error-message';
} finally {
setTimeout(() => {
downloadPdfBtn.disabled = false;
downloadPdfBtn.textContent = '下载 PDF';
}, 2000);
}
};
iframe.srcdoc = htmlContent;
})
.catch(err => {
console.error('PDF生成: 获取HTML内容失败:', err);
statusMsg.textContent = '获取HTML内容失败无法生成PDF。请尝试预览。';
statusMsg.className = 'error-message';
downloadPdfBtn.disabled = false;
downloadPdfBtn.textContent = '下载 PDF';
});
};
downloadBtns.style.display = 'flex';
} else {
downloadBtns.style.display = 'none';
}
} else {
submitButton.textContent = '取消翻译';
submitButton.classList.remove('primary');
submitButton.classList.add('secondary');
isTranslating = true;
submitButton.disabled = false;
submitButton.removeAttribute('aria-busy');
downloadBtns.style.display = 'none';
}
} catch (error) {
console.error("Error polling status:", error);
statusMsg.textContent = '状态更新出错。';
statusMsg.className = 'error-message';
}
}
function startPolling() {
stopPolling();
// lastLogCount = 0; // No longer needed
logArea.innerHTML = ''; // Clear log area for new task
pollLogs(); // Initial poll for logs
pollStatus(); // Initial poll for status
logPollIntervalId = setInterval(pollLogs, 2000);
statusPollIntervalId = setInterval(pollStatus, 1500);
}
function stopPolling() {
if (logPollIntervalId) clearInterval(logPollIntervalId);
if (statusPollIntervalId) clearInterval(statusPollIntervalId);
logPollIntervalId = null;
statusPollIntervalId = null;
setTimeout(pollLogs, 500);
}
function loadSettings() {
platformSelect.value = getFromStorage('translator_last_platform', 'custom');
updatePlatformUI();
toLangSelect.value = getFromStorage('translator_to_lang', '中文');
formulaCheckbox.checked = getFromStorage('translator_formula_ocr') === 'true';
codeCheckbox.checked = getFromStorage('translator_code_ocr') === 'true';
refineCheckbox.checked = getFromStorage('translator_refine_markdown') === 'true';
}
async function cancelTranslation() {
submitButton.disabled = true;
submitButton.textContent = '正在取消...';
submitButton.setAttribute('aria-busy', 'true');
try {
const response = await fetch('/cancel-translate', {method: 'POST'});
const result = await response.json();
if (response.ok && result.cancelled) {
statusMsg.textContent = result.message || '取消请求已发送。';
statusMsg.className = '';
} else {
statusMsg.textContent = result.message || '取消失败。';
statusMsg.className = 'error-message';
submitButton.disabled = false;
submitButton.textContent = '取消翻译';
submitButton.removeAttribute('aria-busy');
}
} catch (error) {
console.error('取消请求失败:', error);
statusMsg.textContent = '取消请求发送失败。';
statusMsg.className = 'error-message';
submitButton.disabled = false;
submitButton.textContent = '取消翻译';
submitButton.removeAttribute('aria-busy');
}
}
form.addEventListener('submit', async function (event) {
event.preventDefault();
if (isTranslating) {
await cancelTranslation();
return;
}
if (fileInput.files.length === 0) {
statusMsg.textContent = '请选择一个文件进行翻译。';
statusMsg.className = 'error-message';
fileNameDisplay.textContent = '请选择文件!';
fileNameDisplay.classList.add('input-error-text');
fileDropArea.classList.add('input-error');
fileDropPrompt.classList.remove('hidden');
setTimeout(() => {
fileDropArea.classList.remove('input-error');
fileNameDisplay.classList.remove('input-error-text');
if (fileNameDisplay.textContent === '请选择文件!') {
fileNameDisplay.textContent = '未选择文件';
}
if (fileInput.files.length === 0) {
fileDropPrompt.classList.remove('hidden');
}
}, 3000);
return;
}
stopPolling();
submitButton.disabled = true;
submitButton.setAttribute('aria-busy', 'true');
submitButton.textContent = '初始化...';
logArea.innerHTML = '';
statusMsg.textContent = '正在提交任务...';
statusMsg.className = '';
downloadBtns.style.display = 'none';
// lastLogCount = 0; // No longer needed
const formData = new FormData(form);
try {
const response = await fetch('/translate', {method: 'POST', body: formData});
const result = await response.json();
if (response.ok && result.task_started) {
statusMsg.textContent = result.message || '任务已开始,正在处理...';
statusMsg.className = '';
submitButton.textContent = '取消翻译';
submitButton.classList.remove('primary');
submitButton.classList.add('secondary');
isTranslating = true;
submitButton.removeAttribute('aria-busy');
startPolling();
} else {
statusMsg.textContent = result.message || `请求失败 (${response.status})`;
statusMsg.className = 'error-message';
submitButton.disabled = false;
submitButton.removeAttribute('aria-busy');
submitButton.textContent = '开始翻译';
isTranslating = false;
}
} catch (error) {
console.error('请求失败:', error);
statusMsg.textContent = '请求翻译失败,请检查网络或服务状态。';
statusMsg.className = 'error-message';
submitButton.disabled = false;
submitButton.removeAttribute('aria-busy');
submitButton.textContent = '开始翻译';
isTranslating = false;
}
});
</script>
</body>
</html>"""
app = FastAPI() app = FastAPI()
BASE_DIR = Path(__file__).resolve().parent
STATIC_DIR=BASE_DIR/"static"
app.mount("/static",StaticFiles(directory=STATIC_DIR), name="static")
# --- 全局配置 --- # --- 全局配置 ---
log_queue: Optional[asyncio.Queue] = None # Will be initialized in startup_event log_queue: Optional[asyncio.Queue] = None # Will be initialized in startup_event
current_state: Dict[str, Any] = { current_state: Dict[str, Any] = {
@@ -882,7 +180,7 @@ async def _perform_translation(params: Dict[str, Any], file_contents: bytes, ori
# --- API Endpoints --- # --- API Endpoints ---
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def main_page(request: Request): async def main_page(request: Request):
return HTMLResponse(content=HTML_TEMPLATE) return FileResponse(STATIC_DIR/"index.html")
@app.post("/translate") @app.post("/translate")

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "docutranslate" name = "docutranslate"
version = "0.2.17" version = "0.2.18"
description = "文件翻译工具" description = "文件翻译工具"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -12,3 +12,10 @@ dependencies = [
] ]
[project.scripts] [project.scripts]
docutranslate="docutranslate.cli:main" docutranslate="docutranslate.cli:main"
[build-system]
requires = ["setuptools>=61.0"] # 或者你需要的 setuptools 版本
build-backend = "setuptools.build_meta"
backend-path = ["."]
[tool.setuptools.package-data]
docutranslate = ["static/**"]