优化代码结构和注释
This commit is contained in:
@@ -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
|
||||||
46
app_ui.py
46
app_ui.py
@@ -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():
|
||||||
|
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.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}"))
|
||||||
|
|||||||
@@ -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]]:
|
||||||
"""
|
"""
|
||||||
检查所有必需的配置项是否都已设置。
|
检查所有必需的配置项是否都已设置。
|
||||||
|
|||||||
Reference in New Issue
Block a user