diff --git a/api_services.py b/api_services.py index 3f7bedb..e2b1c51 100644 --- a/api_services.py +++ b/api_services.py @@ -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 `` and the full `` 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: +- ``: For "Application Writing", this is the essay prompt. For "Read and Continue Writing", this is the initial story provided to the student. +- ``: A quantitative handwriting quality score from 0.0 to 1.0. +- ``: 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 `` is shorter, typically around 80-100 words. The `` 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 `` is longer, typically around 150 words. The `` contains a substantial story. The student's `` 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. """ 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'(.*?)', vlm_output, re.DOTALL) text_match = re.search(r'(.*?)', 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 \ No newline at end of file + 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 \ No newline at end of file diff --git a/app_ui.py b/app_ui.py index 48d5d84..ad42ce6 100644 --- a/app_ui.py +++ b/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}")) diff --git a/config_manager.py b/config_manager.py index be49b36..03126a9 100644 --- a/config_manager.py +++ b/config_manager.py @@ -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]]: """ 检查所有必需的配置项是否都已设置。