优化代码结构和注释

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 import base64
from typing import Dict, Any from typing import Dict, Any, Tuple
from config_manager import ConfigManager from config_manager import ConfigManager
import os import os
import mimetypes import mimetypes
@@ -12,18 +12,45 @@ DEFAULT_LLM_PROMPT_TEMPLATE = """# ESSAY TOPIC
# INSTRUCTIONS FOR AI (Process in English) # INSTRUCTIONS FOR AI (Process in English)
## 1. ROLE & GOAL ## 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 ## 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. You will receive three pieces of data:
## 3. GRADING LOGIC (Total Score: 15 points) - `<topic>`: For "Application Writing", this is the essay prompt. For "Read and Continue Writing", this is the initial story provided to the student.
- **Content & Language (12 points):** Evaluate this based on grammar, vocabulary, sentence structure, etc., in relation to the essay topic. - `<wscore>`: A quantitative handwriting quality score from 0.0 to 1.0.
- **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). - `<text>`: The full text of the student's handwritten essay.
- *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 ## 3. STEP 1: IDENTIFY ESSAY TYPE
Analyze the text, calculate scores, and present your feedback in **Simplified Chinese** using the precise Markdown format specified below. 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 --- #--- End of English Instructions ---
# OUTPUT SPECIFICATION (MUST BE IN SIMPLIFIED CHINESE) # 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') 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 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调用分析作文图片提取手写文本和书写质量分数。 1. VLM调用分析作文图片提取手写文本和书写质量分数。
2. LLM调用基于VLM的输出和作文题目生成详细的批改报告。 2. LLM调用基于VLM的输出和作文题目生成详细的批改报告。
返回: (批改报告, VLM token使用情况, LLM token使用情况)
""" """
# --- 步骤 1: 调用VLM进行图像分析 --- # --- 步骤 1: 调用VLM进行图像分析 ---
vlm_client = OpenAI( vlm_client = OpenAI(
@@ -115,10 +143,15 @@ Strictly adhere to the following format. Do not output anything else.
</text>""" </text>"""
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", "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_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_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格式输出提取分数和文本 # 解析VLM返回的XML格式输出提取分数和文本
wscore_match = re.search(r'<wscore>(.*?)</wscore>', vlm_output, re.DOTALL) wscore_match = re.search(r'<wscore>(.*?)</wscore>', vlm_output, re.DOTALL)
text_match = re.search(r'<text>(.*?)</text>', 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_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) 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未能生成报告。" 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): class AboutDialog(tk.Toplevel):
"""“关于”对话框,展示应用信息,支持滚动查看。""" """“关于”对话框,展示应用信息,支持滚动查看。"""
def __init__(self, parent): def __init__(self, parent, config_manager: ConfigManager):
super().__init__(parent) super().__init__(parent)
self.transient(parent) self.transient(parent)
self.title("关于 AI 作文批改助手") self.title("关于 AI 作文批改助手")
@@ -32,7 +32,12 @@ class AboutDialog(tk.Toplevel):
scrollbar.grid(row=0, column=1, sticky="ns") scrollbar.grid(row=0, column=1, sticky="ns")
text_widget.config(yscrollcommand=scrollbar.set) 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 作文批改助手!这是一款专为教育者和学生设计的智能工具,旨在利用前沿的人工智能技术,提供高效、精准、个性化的英文作文批改体验。 欢迎使用 AI 作文批改助手!这是一款专为教育者和学生设计的智能工具,旨在利用前沿的人工智能技术,提供高效、精准、个性化的英文作文批改体验。
核心亮点: 核心亮点:
@@ -59,6 +64,13 @@ class AboutDialog(tk.Toplevel):
作者: Eric_Terminal 作者: Eric_Terminal
https://github.com/Eric-Terminal https://github.com/Eric-Terminal
版本: 2.5 版本: 2.5
---
历史Token使用量 (仅供参考):
- VLM 输入: {vlm_in:,}
- VLM 输出: {vlm_out:,}
- LLM 输入: {llm_in:,}
- LLM 输出: {llm_out:,}
""" """
text_widget.insert("1.0", about_text) text_widget.insert("1.0", about_text)
@@ -166,8 +178,13 @@ class SettingsDialog(tk.Toplevel):
"LlmModel": self.llm_model.get(), "LlmModel": self.llm_model.get(),
"SensitivityFactor": self.sensitivity_factor.get(), "SensitivityFactor": self.sensitivity_factor.get(),
"MaxWorkers": self.max_workers.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() self.destroy()
def on_close(self): def on_close(self):
@@ -295,12 +312,19 @@ class MainApp:
self.config_manager.config.pop("OcrApiKey", None) self.config_manager.config.pop("OcrApiKey", None)
self.config_manager.config.pop("OcrSecretKey", None) self.config_manager.config.pop("OcrSecretKey", None)
for key, value in dialog.result.items(): 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() self.config_manager.save()
def _open_about_dialog(self): def _open_about_dialog(self):
"""创建并显示“关于”对话框。""" """创建并显示“关于”对话框。"""
AboutDialog(self.root) AboutDialog(self.root, self.config_manager)
def _open_file_dialog(self): def _open_file_dialog(self):
"""打开文件选择对话框,让用户选择一个或多个图片文件。""" """打开文件选择对话框,让用户选择一个或多个图片文件。"""
@@ -372,13 +396,25 @@ class MainApp:
base_name = os.path.basename(file_path) base_name = os.path.basename(file_path)
self.ui_queue.put(("log", f"开始处理: {base_name}")) self.ui_queue.put(("log", f"开始处理: {base_name}"))
try: 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" report_filename = os.path.splitext(file_path)[0] + "_report.md"
with open(report_filename, 'w', encoding='utf-8') as f: with open(report_filename, 'w', encoding='utf-8') as f:
f.write(final_report) 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", 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: except Exception as e:
self.ui_queue.put(("log", f"文件: {base_name} 失败: {e}")) self.ui_queue.put(("log", f"文件: {base_name} 失败: {e}"))

View File

@@ -2,7 +2,7 @@ import json
import os import os
import base64 import base64
import hashlib import hashlib
from typing import Dict, Optional, Tuple from typing import Dict, Optional, Tuple, Any
from cryptography.fernet import Fernet, InvalidToken from cryptography.fernet import Fernet, InvalidToken
class ConfigManager: class ConfigManager:
@@ -17,7 +17,7 @@ class ConfigManager:
def __init__(self, file_path: str = "config.json"): def __init__(self, file_path: str = "config.json"):
self.file_path = file_path self.file_path = file_path
self.config: Dict[str, str] = {} self.config: Dict[str, Any] = {}
self._fernet: Optional[Fernet] = None self._fernet: Optional[Fernet] = None
self._initialize_encryption() self._initialize_encryption()
self.load() self.load()
@@ -70,23 +70,30 @@ class ConfigManager:
except IOError as e: except IOError as e:
print(f"保存配置失败: {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) value = self.config.get(key)
if value is None: if value is None:
return default return default
if key in self.SENSITIVE_KEYS: if key in self.SENSITIVE_KEYS:
return self._decrypt(value) return self._decrypt(str(value))
return value return value
def set(self, key: str, value: str): def set(self, key: str, value: Any):
"""设置指定键的配置值。如果键属于敏感信息,则自动加密后存储。""" """设置指定键的配置值。如果键属于敏感信息,则自动加密后存储。"""
if key in self.SENSITIVE_KEYS: if key in self.SENSITIVE_KEYS:
self.config[key] = self._encrypt(value) self.config[key] = self._encrypt(str(value))
else: else:
self.config[key] = value 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]]: def check_settings(self) -> Tuple[bool, Optional[str]]:
""" """
检查所有必需的配置项是否都已设置。 检查所有必需的配置项是否都已设置。