优化代码结构和注释

This commit is contained in:
Eric-Terminal
2025-08-04 10:49:45 +08:00
parent 74e78108c2
commit f4691506bf
3 changed files with 107 additions and 26 deletions

View File

@@ -1,5 +1,5 @@
import base64
from typing import Dict, Any
from typing import Dict, Any, Tuple
from config_manager import ConfigManager
import os
import mimetypes
@@ -12,18 +12,45 @@ DEFAULT_LLM_PROMPT_TEMPLATE = """# ESSAY TOPIC
# INSTRUCTIONS FOR AI (Process in English)
## 1. ROLE & GOAL
You are a highly experienced senior high school English teacher. Your task is to provide a detailed, constructive, and encouraging evaluation of a student's essay.
You are a highly experienced senior high school English teacher specializing in the Chinese National College Entrance Examination (Gaokao). Your goal is to provide a detailed, constructive, and encouraging evaluation of a student's essay, correctly identifying the essay type and applying the appropriate scoring standard.
## 2. INPUT DATA
You will receive a quantitative `<wscore>` and the full `<text>` of the essay. The essay is based on the topic provided above.
## 3. GRADING LOGIC (Total Score: 15 points)
- **Content & Language (12 points):** Evaluate this based on grammar, vocabulary, sentence structure, etc., in relation to the essay topic.
- **Handwriting & Presentation (3 points):** Calculate the score by first getting a raw score (`Raw Score = wscore * 3`), and then rounding the `Raw Score` **up** to the nearest half-point (0.5).
- *Rounding Logic Example:* A raw score of 2.49 becomes 2.5. A raw score of 2.51 becomes 3.0. A raw score of 2.50 remains 2.5. A score of 0 remains 0.
## 4. FINAL TASK
Analyze the text, calculate scores, and present your feedback in **Simplified Chinese** using the precise Markdown format specified below.
You will receive three pieces of data:
- `<topic>`: For "Application Writing", this is the essay prompt. For "Read and Continue Writing", this is the initial story provided to the student.
- `<wscore>`: A quantitative handwriting quality score from 0.0 to 1.0.
- `<text>`: The full text of the student's handwritten essay.
## 3. STEP 1: IDENTIFY ESSAY TYPE
First, you MUST determine which of the two following Gaokao essay types this is. This decision will change the total score.
* **TYPE A: Application Writing (应用文)**
* **Clues:** The total word count of the student's `<text>` is shorter, typically around 80-100 words. The `<topic>` is a straightforward instruction (e.g., "Write a letter to...").
* **Total Score:** 15 points.
* **TYPE B: Read and Continue Writing (读后续写)**
* **Clues:** The total word count of the student's `<text>` is longer, typically around 150 words. The `<topic>` contains a substantial story. The student's `<text>` will consist of two distinct paragraphs, and the beginning of each paragraph will match the starting sentences provided in the original exam prompt.
* **Total Score:** 25 points.
## 4. STEP 2: APPLY SCORING LOGIC
Based on the identified essay type, apply the corresponding grading logic. The Handwriting score calculation is the same for both.
* **Handwriting & Presentation Score (通用卷面分计算):**
* This sub-score is always out of **3 points**.
* **Calculation:** Get a raw score (`Raw Score = wscore * 3`). Then, round the `Raw Score` **up** to the nearest half-point (0.5).
* **Rounding Example:** A raw score of 2.49 becomes 2.5. A raw score of 2.51 becomes 3.0. A score of 2.50 remains 2.5.
* **GRADING FOR TYPE A: Application Writing (Total 15)**
* **Content & Language (12 points):** Evaluate grammar, vocabulary, sentence structure, and relevance to the topic.
* **Handwriting & Presentation (3 points):** Use the calculation described above.
* **Final Score:** (Content & Language Score) + (Handwriting Score) out of 15.
* **GRADING FOR TYPE B: Read and Continue Writing (Total 25)**
* **Content & Language (22 points):** Evaluate the quality of the continuation. Key criteria include: coherence with the original story, logical plot development, character consistency, richness of detail, and advanced use of grammar, vocabulary, and sentence structures.
* **Handwriting & Presentation (3 points):** Use the calculation described above.
* **Final Score:** (Content & Language Score) + (Handwriting Score) out of 25.
## 5. FINAL TASK
Analyze the text, identify the essay type, calculate the scores, and present your complete feedback in **Simplified Chinese** using the precise Markdown format specified in the "OUTPUT SPECIFICATION" section. Ensure the final score correctly reflects the total points possible (15 or 25).
#--- End of English Instructions ---
# OUTPUT SPECIFICATION (MUST BE IN SIMPLIFIED CHINESE)
# 请严格使用以下Markdown格式并用简体中文填充所有内容优点可以两个到三个,问题建议要把全部问题找出来并且解析,都要遵循类似格式。
# 请严格使用以下Markdown格式并用简体中文填充所有内容优点找不到不要硬找,问题建议要把全部问题找出来并且解析,都要遵循类似格式。
###【作文内容】
@@ -76,11 +103,12 @@ class ApiService:
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
return f"data:{mime_type};base64,{encoded_string}"
def process_essay_image(self, file_path: str, topic: str) -> str:
def process_essay_image(self, file_path: str, topic: str) -> Tuple[str, Dict[str, int], Dict[str, int]]:
"""
执行完整的两步式作文批改流程:
1. VLM调用分析作文图片提取手写文本和书写质量分数。
2. LLM调用基于VLM的输出和作文题目生成详细的批改报告。
返回: (批改报告, VLM token使用情况, LLM token使用情况)
"""
# --- 步骤 1: 调用VLM进行图像分析 ---
vlm_client = OpenAI(
@@ -115,10 +143,15 @@ Strictly adhere to the following format. Do not output anything else.
</text>"""
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", "gemini-2.5-pro")
vlm_model = self.config.get("VlmModel", "Pro/THUDM/GLM-4.1V-9B-Thinking")
vlm_response = vlm_client.chat.completions.create(model=vlm_model, messages=vlm_messages, max_tokens=4096, temperature=1)
vlm_output = vlm_response.choices[0].message.content or ""
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返回的XML格式输出提取分数和文本
wscore_match = re.search(r'<wscore>(.*?)</wscore>', vlm_output, re.DOTALL)
text_match = re.search(r'<text>(.*?)</text>', vlm_output, re.DOTALL)
@@ -156,8 +189,13 @@ Strictly adhere to the following format. Do not output anything else.
llm_messages = [{"role": "user", "content": final_llm_prompt}]
llm_model = self.config.get("LlmModel", "gemini-2.5-pro")
llm_model = self.config.get("LlmModel", "moonshotai/Kimi-K2-Instruct")
llm_response = llm_client.chat.completions.create(model=llm_model, messages=llm_messages, temperature=1, max_tokens=16384)
final_report = llm_response.choices[0].message.content or "错误AI未能生成报告。"
return final_report
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,
}
return final_report, vlm_usage, llm_usage

View File

@@ -10,7 +10,7 @@ from api_services import ApiService, DEFAULT_LLM_PROMPT_TEMPLATE
class AboutDialog(tk.Toplevel):
"""“关于”对话框,展示应用信息,支持滚动查看。"""
def __init__(self, parent):
def __init__(self, parent, config_manager: ConfigManager):
super().__init__(parent)
self.transient(parent)
self.title("关于 AI 作文批改助手")
@@ -32,7 +32,12 @@ class AboutDialog(tk.Toplevel):
scrollbar.grid(row=0, column=1, sticky="ns")
text_widget.config(yscrollcommand=scrollbar.set)
about_text = """
vlm_in = config_manager.get('UsageVlmInput', 0)
vlm_out = config_manager.get('UsageVlmOutput', 0)
llm_in = config_manager.get('UsageLlmInput', 0)
llm_out = config_manager.get('UsageLlmOutput', 0)
about_text = f"""
欢迎使用 AI 作文批改助手!这是一款专为教育者和学生设计的智能工具,旨在利用前沿的人工智能技术,提供高效、精准、个性化的英文作文批改体验。
核心亮点:
@@ -59,6 +64,13 @@ class AboutDialog(tk.Toplevel):
作者: Eric_Terminal
https://github.com/Eric-Terminal
版本: 2.5
---
历史Token使用量 (仅供参考):
- VLM 输入: {vlm_in:,}
- VLM 输出: {vlm_out:,}
- LLM 输入: {llm_in:,}
- LLM 输出: {llm_out:,}
"""
text_widget.insert("1.0", about_text)
@@ -166,8 +178,13 @@ class SettingsDialog(tk.Toplevel):
"LlmModel": self.llm_model.get(),
"SensitivityFactor": self.sensitivity_factor.get(),
"MaxWorkers": self.max_workers.get(),
"LlmPromptTemplate": self.llm_prompt_text.get("1.0", "end-1c") # 从Text控件获取用户修改后的Prompt模板
"LlmPromptTemplate": self.llm_prompt_text.get("1.0", "end-1c")
}
# 如果用户修改后的模板与默认模板内容一致,则不写入配置文件,以使用默认值
if self.result["LlmPromptTemplate"].strip() == DEFAULT_LLM_PROMPT_TEMPLATE.strip():
self.result["LlmPromptTemplate"] = None # 使用 None 作为信号,表示应移除此配置项
self.destroy()
def on_close(self):
@@ -295,12 +312,19 @@ class MainApp:
self.config_manager.config.pop("OcrApiKey", None)
self.config_manager.config.pop("OcrSecretKey", None)
for key, value in dialog.result.items():
self.config_manager.set(key, value)
if key == "LlmPromptTemplate":
if value is None:
# 如果值为None表示用户希望恢复默认模板因此从配置中移除该键
self.config_manager.config.pop(key, None)
else:
self.config_manager.set(key, value)
else:
self.config_manager.set(key, value)
self.config_manager.save()
def _open_about_dialog(self):
"""创建并显示“关于”对话框。"""
AboutDialog(self.root)
AboutDialog(self.root, self.config_manager)
def _open_file_dialog(self):
"""打开文件选择对话框,让用户选择一个或多个图片文件。"""
@@ -372,13 +396,25 @@ class MainApp:
base_name = os.path.basename(file_path)
self.ui_queue.put(("log", f"开始处理: {base_name}"))
try:
final_report = self.api_service.process_essay_image(file_path, topic)
final_report, vlm_usage, llm_usage = self.api_service.process_essay_image(file_path, topic)
report_filename = os.path.splitext(file_path)[0] + "_report.md"
with open(report_filename, 'w', encoding='utf-8') as f:
f.write(final_report)
vlm_in = vlm_usage.get("prompt_tokens", 0)
vlm_out = vlm_usage.get("completion_tokens", 0)
llm_in = llm_usage.get("prompt_tokens", 0)
llm_out = llm_usage.get("completion_tokens", 0)
usage_log = f"Token用量: VLM(in:{vlm_in}, out:{vlm_out}), LLM(in:{llm_in}, out:{llm_out})"
self.ui_queue.put(("log", f"完成批改: {base_name} -> {os.path.basename(report_filename)}"))
self.ui_queue.put(("log", usage_log))
# 加锁以保证线程安全地更新和保存配置
with self.lock:
self.config_manager.update_token_usage(vlm_in, vlm_out, llm_in, llm_out)
self.config_manager.save()
except Exception as e:
self.ui_queue.put(("log", f"文件: {base_name} 失败: {e}"))

View File

@@ -2,7 +2,7 @@ import json
import os
import base64
import hashlib
from typing import Dict, Optional, Tuple
from typing import Dict, Optional, Tuple, Any
from cryptography.fernet import Fernet, InvalidToken
class ConfigManager:
@@ -17,7 +17,7 @@ class ConfigManager:
def __init__(self, file_path: str = "config.json"):
self.file_path = file_path
self.config: Dict[str, str] = {}
self.config: Dict[str, Any] = {}
self._fernet: Optional[Fernet] = None
self._initialize_encryption()
self.load()
@@ -70,23 +70,30 @@ class ConfigManager:
except IOError as e:
print(f"保存配置失败: {e}")
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
"""获取指定键的配置值。如果键属于敏感信息,则自动解密后返回。"""
value = self.config.get(key)
if value is None:
return default
if key in self.SENSITIVE_KEYS:
return self._decrypt(value)
return self._decrypt(str(value))
return value
def set(self, key: str, value: str):
def set(self, key: str, value: Any):
"""设置指定键的配置值。如果键属于敏感信息,则自动加密后存储。"""
if key in self.SENSITIVE_KEYS:
self.config[key] = self._encrypt(value)
self.config[key] = self._encrypt(str(value))
else:
self.config[key] = value
def update_token_usage(self, vlm_input: int, vlm_output: int, llm_input: int, llm_output: int):
"""累加本次API调用的token使用量到配置中。"""
self.config['UsageVlmInput'] = self.get('UsageVlmInput', 0) + vlm_input
self.config['UsageVlmOutput'] = self.get('UsageVlmOutput', 0) + vlm_output
self.config['UsageLlmInput'] = self.get('UsageLlmInput', 0) + llm_input
self.config['UsageLlmOutput'] = self.get('UsageLlmOutput', 0) + llm_output
def check_settings(self) -> Tuple[bool, Optional[str]]:
"""
检查所有必需的配置项是否都已设置。