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