更新WebUI
51
README.md
@@ -1,14 +1,6 @@
|
|||||||
# AI 作文批改助手 ✨
|
# AI 作文批改助手 ✨
|
||||||
|
|
||||||
(^▽^)ノ゙ 欢迎使用 AI 作文批改助手!这是一款专为教育工作者和学生设计的智能桌面工具,能够像经验丰富的英语老师一样,自动批改手写英文作文图片,并生成专业详细的批改报告。
|
(^▽^)ノ゙ 欢迎使用 AI 作文批改助手!这是一款专为教育工作者和学生设计的本地 Web 应用,能够像经验丰富的英语老师一样,自动批改手写英文作文图片,并生成专业详细的批改报告。
|
||||||
|
|
||||||
## 🎯 应用界面预览
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✨ 核心特色功能
|
## ✨ 核心特色功能
|
||||||
|
|
||||||
@@ -44,19 +36,26 @@
|
|||||||
|
|
||||||
### 快速开始
|
### 快速开始
|
||||||
1. **下载程序**: 前往 [Releases页面](https://github.com/Eric-Terminal/Pro_llm_correct/releases) 下载最新版本
|
1. **下载程序**: 前往 [Releases页面](https://github.com/Eric-Terminal/Pro_llm_correct/releases) 下载最新版本
|
||||||
2. **首次配置**:
|
2. **启动 Web UI**:
|
||||||
- 运行程序,自动弹出设置窗口
|
- 在终端运行 `python3 main.py`
|
||||||
- 配置VLM和LLM服务的URL、API密钥和模型名称
|
- 程序会从 4567 端口起寻找可用端口,并自动打开浏览器访问 Web 界面
|
||||||
- 点击确定保存,密钥自动加密存储
|
3. **配置服务**:
|
||||||
3. **开始批改**:
|
- 通过顶部导航切换到“服务设置”页,填写 VLM/LLM 的 URL、API Key、模型名称等参数
|
||||||
- 在主界面输入作文题目
|
- 可自定义 Prompt 模板、并发数量、重试策略与输出目录
|
||||||
- 点击"选择图片",多选需要批改的作文图片
|
- 密钥字段不会回显;若提示“已保存”,留空即可沿用原值,输入新值即可覆盖
|
||||||
- 点击"开始批改",程序自动进行并发处理
|
- 点击“保存设置”即可持久化到本地 `config.json`(密钥自动加密)
|
||||||
4. **查看报告**: 处理完成后,Markdown和HTML格式报告自动保存在原图片目录
|
4. **上传批改**:
|
||||||
|
- Web 首页默认停留在“批改作文”页,在表单中输入作文题目或场景说明
|
||||||
|
- 上传需要批改的作文照片(支持多选)
|
||||||
|
- 点击“开始批改”,浏览器会实时显示每个文件的处理状态与日志
|
||||||
|
5. **查看报告**:
|
||||||
|
- 所有输出默认保存在 `output_reports/<时间戳>/` 目录
|
||||||
|
- 结果卡片中提供 Markdown/HTML 链接,可直接在浏览器查看或下载
|
||||||
|
|
||||||
### 输出文件说明
|
### 输出文件说明
|
||||||
- `原文件名_report.md`: Markdown格式详细批改报告
|
- 默认保存在 `output_reports/<时间戳>/` 目录
|
||||||
- `原文件名_report.html`: HTML可视化批改报告
|
- `原文件名_report.md`: Markdown 格式详细批改报告
|
||||||
|
- `原文件名_report.html`: HTML 可视化批改报告
|
||||||
- 包含: 作文内容、综合评价、亮点优点、问题建议、分数评估
|
- 包含: 作文内容、综合评价、亮点优点、问题建议、分数评估
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -76,9 +75,10 @@ source venv/bin/activate # Linux/Mac
|
|||||||
|
|
||||||
# 3. 安装依赖
|
# 3. 安装依赖
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
# 需要确保系统已安装 curl(macOS/Linux 默认自带,Windows 可安装 Git Bash 或使用 WSL)
|
||||||
|
|
||||||
# 4. 运行程序
|
# 4. 运行程序
|
||||||
python main.py
|
python3 main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### 项目打包
|
### 项目打包
|
||||||
@@ -90,11 +90,11 @@ pyinstaller --noconsole --onefile main.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
### 技术架构
|
### 技术架构
|
||||||
- **前端**: Tkinter GUI界面
|
- **前端**: Flask Web 服务 + 原生 HTML/CSS(玻璃拟态苹果风界面)
|
||||||
- **核心**: 双AI引擎架构 (VLM + LLM)
|
- **核心**: 双AI引擎架构 (VLM + LLM)
|
||||||
- **安全**: cryptography加密库
|
- **安全**: cryptography 加密存储配置
|
||||||
- **并发**: threading + concurrent.futures
|
- **并发**: threading + concurrent.futures.ThreadPoolExecutor
|
||||||
- **输出**: Markdown + HTML渲染
|
- **输出**: Markdown/HTML 报告(内置样式渲染器)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -113,6 +113,7 @@ pyinstaller --noconsole --onefile main.py
|
|||||||
- `MaxWorkers`: 最大并发数(默认4)
|
- `MaxWorkers`: 最大并发数(默认4)
|
||||||
- `MaxRetries`: 最大重试次数(默认3)
|
- `MaxRetries`: 最大重试次数(默认3)
|
||||||
- `RetryDelay`: 重试延迟秒数(默认5)
|
- `RetryDelay`: 重试延迟秒数(默认5)
|
||||||
|
- `RequestTimeout`: 单次 API 请求超时时长(秒,默认120)
|
||||||
- `SaveMarkdown`: 是否保存Markdown文件(默认True)
|
- `SaveMarkdown`: 是否保存Markdown文件(默认True)
|
||||||
- `RenderMarkdown`: 是否渲染HTML报告(默认True)
|
- `RenderMarkdown`: 是否渲染HTML报告(默认True)
|
||||||
|
|
||||||
|
|||||||
198
api_services.py
@@ -8,9 +8,9 @@ from markdown_renderer import create_markdown_renderer
|
|||||||
import os
|
import os
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import re
|
import re
|
||||||
from openai import OpenAI
|
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
# 定义默认的LLM Prompt模板。使用`.format()`方法进行后续的动态填充。
|
# 定义默认的LLM Prompt模板。使用`.format()`方法进行后续的动态填充。
|
||||||
DEFAULT_LLM_PROMPT_TEMPLATE = """# ESSAY TOPIC
|
DEFAULT_LLM_PROMPT_TEMPLATE = """# ESSAY TOPIC
|
||||||
@@ -105,6 +105,8 @@ class ApiService:
|
|||||||
"""将日志消息放入UI队列。"""
|
"""将日志消息放入UI队列。"""
|
||||||
if self.ui_queue:
|
if self.ui_queue:
|
||||||
self.ui_queue.put(("log", message))
|
self.ui_queue.put(("log", message))
|
||||||
|
else:
|
||||||
|
logging.info(message)
|
||||||
|
|
||||||
def _encode_image_to_base64_url(self, image_path: str) -> str:
|
def _encode_image_to_base64_url(self, image_path: str) -> str:
|
||||||
"""将本地图片文件编码为Base64数据URL。"""
|
"""将本地图片文件编码为Base64数据URL。"""
|
||||||
@@ -117,6 +119,89 @@ class ApiService:
|
|||||||
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
|
||||||
return f"data:{mime_type};base64,{encoded_string}"
|
return f"data:{mime_type};base64,{encoded_string}"
|
||||||
|
|
||||||
|
def _chat_endpoint(self, base_url: Optional[str]) -> str:
|
||||||
|
if not base_url:
|
||||||
|
raise ValueError("服务地址未配置,请先在设置中填写 API Base URL")
|
||||||
|
return base_url.rstrip('/') + "/chat/completions"
|
||||||
|
|
||||||
|
def _usage_from_response(self, response_json: Dict[str, Any]) -> Dict[str, int]:
|
||||||
|
usage = response_json.get("usage") or {}
|
||||||
|
return {
|
||||||
|
"prompt_tokens": int(usage.get("prompt_tokens", 0) or 0),
|
||||||
|
"completion_tokens": int(usage.get("completion_tokens", 0) or 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _post_json_with_curl(self, endpoint: str, api_key: Optional[str], payload: Dict[str, Any], timeout: float) -> Dict[str, Any]:
|
||||||
|
data_str = json.dumps(payload, ensure_ascii=False)
|
||||||
|
command = [
|
||||||
|
"curl",
|
||||||
|
"-sS",
|
||||||
|
"-X",
|
||||||
|
"POST",
|
||||||
|
endpoint,
|
||||||
|
"-H",
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"--data-binary",
|
||||||
|
"@-",
|
||||||
|
"-w",
|
||||||
|
"\nHTTP_STATUS:%{http_code}",
|
||||||
|
"--max-time",
|
||||||
|
str(max(timeout, 1.0)),
|
||||||
|
]
|
||||||
|
if api_key:
|
||||||
|
command.extend(["-H", f"Authorization: Bearer {api_key}"])
|
||||||
|
|
||||||
|
completed = subprocess.run(command, capture_output=True, text=True, input=data_str)
|
||||||
|
stdout = completed.stdout or ""
|
||||||
|
stderr = completed.stderr.strip()
|
||||||
|
|
||||||
|
status_code = None
|
||||||
|
if "HTTP_STATUS:" in stdout:
|
||||||
|
stdout, status_part = stdout.rsplit("HTTP_STATUS:", 1)
|
||||||
|
try:
|
||||||
|
status_code = int(status_part.strip())
|
||||||
|
except ValueError:
|
||||||
|
status_code = None
|
||||||
|
|
||||||
|
response_text = stdout.strip()
|
||||||
|
|
||||||
|
if completed.returncode != 0 or (status_code and status_code >= 400):
|
||||||
|
error_message = response_text or stderr or f"curl exited with code {completed.returncode}"
|
||||||
|
raise RuntimeError(f"调用失败 (HTTP {status_code}): {error_message}")
|
||||||
|
|
||||||
|
if not response_text:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return json.loads(response_text)
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"无法解析 API 返回的 JSON: {response_text[:500]}") from exc
|
||||||
|
|
||||||
|
def _invoke_chat_completion(
|
||||||
|
self,
|
||||||
|
label: str,
|
||||||
|
base_url: Optional[str],
|
||||||
|
api_key: Optional[str],
|
||||||
|
payload: Dict[str, Any],
|
||||||
|
max_retries: int,
|
||||||
|
retry_delay: int,
|
||||||
|
timeout: float,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
endpoint = self._chat_endpoint(base_url)
|
||||||
|
last_error: Optional[Exception] = None
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
return self._post_json_with_curl(endpoint, api_key, payload, timeout)
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
last_error = exc
|
||||||
|
if attempt == max_retries - 1:
|
||||||
|
raise
|
||||||
|
self._log(f"{label} 调用失败,{retry_delay}秒后重试... (尝试 {attempt + 1}/{max_retries}),错误: {exc}")
|
||||||
|
time.sleep(retry_delay)
|
||||||
|
if last_error:
|
||||||
|
raise last_error
|
||||||
|
raise RuntimeError(f"{label} 调用失败:未知错误")
|
||||||
|
|
||||||
def process_essay_image(self, file_path: str, topic: str) -> Tuple[str, Dict[str, int], Dict[str, int]]:
|
def process_essay_image(self, file_path: str, topic: str) -> Tuple[str, Dict[str, int], Dict[str, int]]:
|
||||||
"""
|
"""
|
||||||
执行完整的两步式作文批改流程:
|
执行完整的两步式作文批改流程:
|
||||||
@@ -131,19 +216,12 @@ class ApiService:
|
|||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
max_retries = 3
|
max_retries = 3
|
||||||
retry_delay = 5
|
retry_delay = 5
|
||||||
|
|
||||||
for attempt in range(max_retries):
|
try:
|
||||||
try:
|
request_timeout = float(self.config.get("RequestTimeout", 120))
|
||||||
vlm_client = OpenAI(
|
except (ValueError, TypeError):
|
||||||
api_key=self.config.get("VlmApiKey"),
|
request_timeout = 120.0
|
||||||
base_url=self.config.get("VlmUrl")
|
|
||||||
)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
raise
|
|
||||||
self._log(f"VLM客户端创建失败,{retry_delay}秒后重试... (尝试 {attempt + 1}/{max_retries})")
|
|
||||||
time.sleep(retry_delay)
|
|
||||||
base64_image_url = self._encode_image_to_base64_url(file_path)
|
base64_image_url = self._encode_image_to_base64_url(file_path)
|
||||||
|
|
||||||
vlm_prompt = """# ROLE
|
vlm_prompt = """# ROLE
|
||||||
@@ -173,21 +251,28 @@ Strictly adhere to the following format. Do not output anything else.
|
|||||||
vlm_messages = [{"role": "user", "content": [{"type": "text", "text": vlm_prompt}, {"type": "image_url", "image_url": {"url": base64_image_url}}]}]
|
vlm_messages = [{"role": "user", "content": [{"type": "text", "text": vlm_prompt}, {"type": "image_url", "image_url": {"url": base64_image_url}}]}]
|
||||||
|
|
||||||
vlm_model = self.config.get("VlmModel", "Pro/THUDM/GLM-4.1V-9B-Thinking")
|
vlm_model = self.config.get("VlmModel", "Pro/THUDM/GLM-4.1V-9B-Thinking")
|
||||||
for attempt in range(max_retries):
|
vlm_payload = {
|
||||||
try:
|
"model": vlm_model,
|
||||||
vlm_response = vlm_client.chat.completions.create(model=vlm_model, messages=vlm_messages, max_tokens=4096, temperature=1)
|
"messages": vlm_messages,
|
||||||
vlm_output = vlm_response.choices[0].message.content or ""
|
"max_tokens": 4096,
|
||||||
break
|
"temperature": 1,
|
||||||
except Exception as e:
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
raise
|
|
||||||
self._log(f"VLM调用失败,{retry_delay}秒后重试... (尝试 {attempt + 1}/{max_retries})")
|
|
||||||
time.sleep(retry_delay)
|
|
||||||
|
|
||||||
vlm_usage = {
|
|
||||||
"prompt_tokens": vlm_response.usage.prompt_tokens if vlm_response.usage else 0,
|
|
||||||
"completion_tokens": vlm_response.usage.completion_tokens if vlm_response.usage else 0,
|
|
||||||
}
|
}
|
||||||
|
vlm_response_json = self._invoke_chat_completion(
|
||||||
|
"VLM",
|
||||||
|
self.config.get("VlmUrl"),
|
||||||
|
self.config.get("VlmApiKey"),
|
||||||
|
vlm_payload,
|
||||||
|
max_retries,
|
||||||
|
retry_delay,
|
||||||
|
request_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
choices = vlm_response_json.get("choices") or []
|
||||||
|
if not choices:
|
||||||
|
raise ValueError(f"VLM 未返回 choices,响应:{vlm_response_json}")
|
||||||
|
vlm_output = choices[0].get("message", {}).get("content") or ""
|
||||||
|
|
||||||
|
vlm_usage = self._usage_from_response(vlm_response_json)
|
||||||
|
|
||||||
# 解析VLM返回的XML格式输出,提取分数和文本
|
# 解析VLM返回的XML格式输出,提取分数和文本
|
||||||
wscore_match = re.search(r'<wscore>(.*?)</wscore>', vlm_output, re.DOTALL)
|
wscore_match = re.search(r'<wscore>(.*?)</wscore>', vlm_output, re.DOTALL)
|
||||||
@@ -207,19 +292,6 @@ Strictly adhere to the following format. Do not output anything else.
|
|||||||
raise ValueError(f"VLM未能按预期格式返回,无法解析文本。模型返回:\n{vlm_output}")
|
raise ValueError(f"VLM未能按预期格式返回,无法解析文本。模型返回:\n{vlm_output}")
|
||||||
|
|
||||||
# --- 步骤 2: 调用LLM生成批改报告 ---
|
# --- 步骤 2: 调用LLM生成批改报告 ---
|
||||||
for attempt in range(max_retries):
|
|
||||||
try:
|
|
||||||
llm_client = OpenAI(
|
|
||||||
api_key=self.config.get("LlmApiKey"),
|
|
||||||
base_url=self.config.get("LlmUrl")
|
|
||||||
)
|
|
||||||
break
|
|
||||||
except Exception as e:
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
raise
|
|
||||||
self._log(f"LLM客户端创建失败,{retry_delay}秒后重试... (尝试 {attempt + 1}/{max_retries})")
|
|
||||||
time.sleep(retry_delay)
|
|
||||||
|
|
||||||
# 从配置加载Prompt模板,若用户未定义则使用默认模板
|
# 从配置加载Prompt模板,若用户未定义则使用默认模板
|
||||||
prompt_template = self.config.get("LlmPromptTemplate")
|
prompt_template = self.config.get("LlmPromptTemplate")
|
||||||
if not prompt_template:
|
if not prompt_template:
|
||||||
@@ -235,23 +307,35 @@ Strictly adhere to the following format. Do not output anything else.
|
|||||||
llm_messages = [{"role": "user", "content": final_llm_prompt}]
|
llm_messages = [{"role": "user", "content": final_llm_prompt}]
|
||||||
|
|
||||||
llm_model = self.config.get("LlmModel", "moonshotai/Kimi-K2-Instruct")
|
llm_model = self.config.get("LlmModel", "moonshotai/Kimi-K2-Instruct")
|
||||||
for attempt in range(max_retries):
|
llm_payload = {
|
||||||
try:
|
"model": llm_model,
|
||||||
llm_response = llm_client.chat.completions.create(model=llm_model, messages=llm_messages, temperature=1, max_tokens=16384)
|
"messages": llm_messages,
|
||||||
final_report = llm_response.choices[0].message.content or "错误:AI未能生成报告。"
|
"temperature": 1,
|
||||||
break
|
"max_tokens": 16384,
|
||||||
except Exception as e:
|
|
||||||
if attempt == max_retries - 1:
|
|
||||||
final_report = f"错误:AI生成报告失败(达到最大重试次数 {max_retries} 次)"
|
|
||||||
else:
|
|
||||||
self._log(f"LLM调用失败,{retry_delay}秒后重试... (尝试 {attempt + 1}/{max_retries})")
|
|
||||||
time.sleep(retry_delay)
|
|
||||||
|
|
||||||
llm_usage = {
|
|
||||||
"prompt_tokens": llm_response.usage.prompt_tokens if llm_response.usage else 0,
|
|
||||||
"completion_tokens": llm_response.usage.completion_tokens if llm_response.usage else 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final_report: str
|
||||||
|
try:
|
||||||
|
llm_response_json = self._invoke_chat_completion(
|
||||||
|
"LLM",
|
||||||
|
self.config.get("LlmUrl"),
|
||||||
|
self.config.get("LlmApiKey"),
|
||||||
|
llm_payload,
|
||||||
|
max_retries,
|
||||||
|
retry_delay,
|
||||||
|
request_timeout,
|
||||||
|
)
|
||||||
|
llm_choices = llm_response_json.get("choices") or []
|
||||||
|
if not llm_choices:
|
||||||
|
raise ValueError(f"LLM 未返回 choices,响应:{llm_response_json}")
|
||||||
|
final_report = llm_choices[0].get("message", {}).get("content") or "错误:AI未能生成报告。"
|
||||||
|
except Exception as exc:
|
||||||
|
self._log(f"LLM 调用失败:{exc}")
|
||||||
|
final_report = f"错误:AI生成报告失败({exc})"
|
||||||
|
llm_response_json = {}
|
||||||
|
|
||||||
|
llm_usage = self._usage_from_response(llm_response_json)
|
||||||
|
|
||||||
# 渲染Markdown为HTML(如果配置开启)
|
# 渲染Markdown为HTML(如果配置开启)
|
||||||
html_path = None
|
html_path = None
|
||||||
if self.markdown_renderer:
|
if self.markdown_renderer:
|
||||||
@@ -286,4 +370,4 @@ def check_for_updates(current_version_str: str) -> Optional[str]:
|
|||||||
return latest_version_name
|
return latest_version_name
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to check for updates: {e}")
|
logging.error(f"Failed to check for updates: {e}")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -9,12 +9,11 @@ import logging
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
from config_manager import ConfigManager
|
from config_manager import ConfigManager
|
||||||
from api_services import ApiService, DEFAULT_LLM_PROMPT_TEMPLATE, check_for_updates
|
from api_services import ApiService, DEFAULT_LLM_PROMPT_TEMPLATE, check_for_updates
|
||||||
|
from version import CURRENT_VERSION
|
||||||
|
|
||||||
# 配置日志记录器
|
# 配置日志记录器
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
|
||||||
CURRENT_VERSION = "3.2.0"
|
|
||||||
|
|
||||||
class AboutDialog(tk.Toplevel):
|
class AboutDialog(tk.Toplevel):
|
||||||
"""“关于”对话框,展示应用信息,支持滚动查看。"""
|
"""“关于”对话框,展示应用信息,支持滚动查看。"""
|
||||||
def __init__(self, parent, config_manager: ConfigManager):
|
def __init__(self, parent, config_manager: ConfigManager):
|
||||||
|
|||||||
24
config.json
@@ -1 +1,23 @@
|
|||||||
{}
|
{
|
||||||
|
"__device_salt__": "c2FsdF9mb3JfbGxtX2FwcF9jb25maWdDMTdGUkFDUlE2TDQ=",
|
||||||
|
"__device_fingerprint__": "d9bddd980989cdac1e26fdb26d21fa098ea9d5f2dc2da062b069d268f8c9a1ff",
|
||||||
|
"__device_fingerprint_source__": "hardware",
|
||||||
|
"SaveMarkdown": true,
|
||||||
|
"RenderMarkdown": true,
|
||||||
|
"VlmUrl": "https://api.ericterminal.com/v1",
|
||||||
|
"VlmApiKey": "gAAAAABo-F2EZa6kKSdQNolAPQcvN5RMdyCcgJpQ2VFg0szZWFWfa7MQQsMWT7R6jKpSBgVxSCYuzEC2A-xcZqIdkY_rqdFSYzT0Uou84UKP0aqkTCNTJwd3wwetbYQTpcWtJpRuw6KrVnFtypcON4K7gXdV0biDEw==",
|
||||||
|
"VlmModel": "gemini-2.5-pro",
|
||||||
|
"LlmUrl": "https://api.ericterminal.com/v1",
|
||||||
|
"LlmApiKey": "gAAAAABo-F2E-b6xQWGj5DhJfTNlZkekVeidkYI9ZH7-bhPayqcbAkNVsc9NpgSFo5b1dfifT9cVirJeGjoxobSS6ewWJvDN0JgbZa2e1pZeo5Gsqvei3O2s4xOEk9kAsWej6WjynjFYHkBEz10sK53cCPHZI1RqAA==",
|
||||||
|
"LlmModel": "gemini-2.5-pro",
|
||||||
|
"OutputDirectory": "output_reports",
|
||||||
|
"MaxWorkers": 4,
|
||||||
|
"MaxRetries": 3,
|
||||||
|
"RetryDelay": 5,
|
||||||
|
"SensitivityFactor": 1.0,
|
||||||
|
"AutoUpdateCheck": true,
|
||||||
|
"UsageVlmInput": 645,
|
||||||
|
"UsageVlmOutput": 19,
|
||||||
|
"UsageLlmInput": 1385,
|
||||||
|
"UsageLlmOutput": 296
|
||||||
|
}
|
||||||
54
main.py
@@ -1,9 +1,12 @@
|
|||||||
import tkinter as tk
|
|
||||||
from app_ui import MainApp
|
|
||||||
from config_manager import ConfigManager
|
|
||||||
from api_services import ApiService
|
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
import socket
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
|
from config_manager import ConfigManager
|
||||||
|
from web_app import create_app
|
||||||
|
|
||||||
|
|
||||||
def get_config_path():
|
def get_config_path():
|
||||||
@@ -21,17 +24,40 @@ def get_config_path():
|
|||||||
# 开发环境,使用当前目录
|
# 开发环境,使用当前目录
|
||||||
return "config.json"
|
return "config.json"
|
||||||
|
|
||||||
|
def find_available_port(start: int = 4567, limit: int = 4667) -> int:
|
||||||
|
"""Return the first free TCP port within the inclusive range."""
|
||||||
|
for port in range(start, limit + 1):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
try:
|
||||||
|
sock.bind(("127.0.0.1", port))
|
||||||
|
return port
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
raise RuntimeError(f"无法在 {start}-{limit} 范围内找到可用端口")
|
||||||
|
|
||||||
|
|
||||||
|
def open_browser_later(url: str, delay: float = 1.0) -> None:
|
||||||
|
"""Open the default browser after a small delay."""
|
||||||
|
|
||||||
|
def _opener():
|
||||||
|
time.sleep(delay)
|
||||||
|
try:
|
||||||
|
webbrowser.open_new(url)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
threading.Thread(target=_opener, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# 1. 初始化核心服务
|
|
||||||
# 使用合适的配置文件路径
|
|
||||||
config_path = get_config_path()
|
config_path = get_config_path()
|
||||||
config_manager = ConfigManager(config_path)
|
config_manager = ConfigManager(config_path)
|
||||||
|
app = create_app(config_manager)
|
||||||
|
|
||||||
# 2. 创建Tkinter主窗口
|
port = find_available_port()
|
||||||
root = tk.Tk()
|
url = f"http://127.0.0.1:{port}/"
|
||||||
|
print(f"🚀 Web UI 已启动,访问: {url}")
|
||||||
# 3. 实例化主应用,服务将在MainApp内部创建
|
open_browser_later(url)
|
||||||
app = MainApp(root, config_manager)
|
|
||||||
|
|
||||||
# 4. 启动Tkinter事件循环
|
app.run(host="127.0.0.1", port=port, debug=False, use_reloader=False)
|
||||||
root.mainloop()
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.5 MiB |
BIN
output_reports/20251022-123002/131910080_p0_master1200.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
output_reports/20251022-123002/131910080_p1_master1200.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
BIN
output_reports/20251022-123350/4K.png
Normal file
|
After Width: | Height: | Size: 6.2 MiB |
BIN
output_reports/20251022-124130/4K.png
Normal file
|
After Width: | Height: | Size: 6.2 MiB |
BIN
output_reports/20251022-124454/4K.png
Normal file
|
After Width: | Height: | Size: 6.2 MiB |
16
output_reports/20251022-124454/4K_report.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
###【作文内容】
|
||||||
|
* **作文文本:**
|
||||||
|
### 【综合评价】
|
||||||
|
同学,你提交的是一张白卷,这在考试中是绝对不允许的。一分不得的情况下,你将与其他考生拉开巨大的差距。希望这只是一个系统测试,在真正的考试中,请务必认真对待,写出你的想法。
|
||||||
|
### 【亮点与优点】
|
||||||
|
* 本次作文为空白卷,无任何优点。
|
||||||
|
### 【问题与修改建议】
|
||||||
|
* **[问题1 - 未作答]:**
|
||||||
|
* **原文句子:** "全文为空。"
|
||||||
|
* **问题分析:** 考生没有书写任何内容,无法进行评分。在任何考试中,交白卷都意味着该题得分为0,这将对总成绩产生灾难性的影响。
|
||||||
|
* **修改建议:** 务必审题并按照要求完成写作任务。即使语言表达不够完美,也要尽力尝试,争取拿到基本的分数。请记住,写了就有可能得分,不写一定是0分。
|
||||||
|
### 【分数评估】
|
||||||
|
* **内容与语言分 (Content & Language):** 0 / 12
|
||||||
|
* **卷面与书写分 (Handwriting & Presentation):** 0 / 3
|
||||||
|
* ---
|
||||||
|
* **最终得分 (Final Score):** **0 / 15**
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
cryptography
|
cryptography
|
||||||
openai
|
flask
|
||||||
markdown
|
markdown
|
||||||
packaging
|
packaging
|
||||||
|
|||||||
1
version.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CURRENT_VERSION = "4"
|
||||||