Files
Pro_llm_correct/api_services.py
Eric-Terminal 4a021d5eca Initial commit
2025-08-04 01:38:00 +08:00

163 lines
8.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import base64
from typing import Dict, Any
from config_manager import ConfigManager
import os
import mimetypes
import re
from openai import OpenAI
# 定义默认的LLM Prompt模板。使用`.format()`方法进行后续的动态填充。
DEFAULT_LLM_PROMPT_TEMPLATE = """# ESSAY TOPIC
{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.
## 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.
#--- End of English Instructions ---
# OUTPUT SPECIFICATION (MUST BE IN SIMPLIFIED CHINESE)
# 请严格使用以下Markdown格式并用简体中文填充所有内容优点可以两个到三个问题建议要把全部问题找出来并且解析都要遵循类似格式。
###【作文内容】
* **作文文本:** [在此处粘贴完整的作文文本。]
### 【综合评价】
(在此处用一两句鼓励性的话,对本次作文进行总体概述。)
### 【亮点与优点】
* **(优点1):** [具体描述作文内容或语言上的一个亮点。]
* **(优点2):** [具体描述另一个优点。]
* **(优点3):以此类推不限制数量但建议控制在3个以内。
### 【问题与修改建议】
* **[问题1 - 语法/拼写错误]:**
* **原文句子:** "[引用出现错误的原文句子]"
* **问题分析:** [简要说明错误类型。]
* **修改建议:** "[写出修改后的正确句子]"
* **[问题2 - 表达/逻辑]:**
* **原文句子:** "[引用表达欠佳的原文句子]"
* **问题分析:** [说明问题所在。]
* **修改建议:** "[提供一个更好的表达方式。]
* **[问题3 - 以此类推不限制数量但建议控制在3个以内。]:**
* **原文句子:** "[说明问题所在。]"
* **问题分析:** [说明问题所在。]
### 【分数评估】
* **内容与语言分 (Content & Language):** [分数] / 12
* **卷面与书写分 (Handwriting & Presentation):** [分数] / 3
* ---
* **最终得分 (Final Score):** **[总分] / 15**
# INPUT DATA FOR THIS TASK
<wscore>{wscore}</wscore>
<text>
{essay_text}
</text>
"""
class ApiService:
"""封装了与外部APIVLM和LLM交互的所有逻辑。"""
def __init__(self, config_manager: ConfigManager):
self.config = config_manager
def _encode_image_to_base64_url(self, image_path: str) -> str:
"""将本地图片文件编码为Base64数据URL。"""
if not os.path.exists(image_path):
raise FileNotFoundError(f"Image file not found at: {image_path}")
mime_type, _ = mimetypes.guess_type(image_path)
if not mime_type or not mime_type.startswith('image'):
raise ValueError(f"File is not a recognizable image type: {mime_type}")
with open(image_path, "rb") as image_file:
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:
"""
执行完整的两步式作文批改流程:
1. VLM调用分析作文图片提取手写文本和书写质量分数。
2. LLM调用基于VLM的输出和作文题目生成详细的批改报告。
"""
# --- 步骤 1: 调用VLM进行图像分析 ---
vlm_client = OpenAI(
api_key=self.config.get("VlmApiKey"),
base_url=self.config.get("VlmUrl")
)
base64_image_url = self._encode_image_to_base64_url(file_path)
vlm_prompt = """# ROLE
You are a high-precision OCR (Optical Character Recognition) and handwriting analysis engine. Your only job is to analyze the provided image and output structured data. Do not add any conversational text or explanations.
# TASK
Analyze the handwriting quality and extract all text from the image.
## 1. Handwriting Quality Analysis:
- Critically evaluate the handwriting on a continuous scale from 0.0 to 1.0.
- The scoring must be stringent. A score of 1.0 is reserved for flawless, machine-printed-like perfection, which is virtually unattainable.
- **Score Tiers:**
- **0.90-0.99:** Near-perfect, professional calligrapher level. Extremely rare.
- **0.80-0.89:** Excellent, clear, consistent, and aesthetically pleasing. The best a top student can achieve.
- **0.70-0.79:** Good and very legible, but with minor inconsistencies in size or spacing.
- **0.60-0.69:** Clear and legible, but with noticeable inconsistencies.
- **Below 0.60:** Legibility is impacted.
- Output this score enclosed in a single <wscore> XML tag.
## 2. Full Text Extraction:
- Perform a high-accuracy OCR on the entire image.
- Preserve the original line breaks and paragraph structure as best as possible.
- Output the full extracted text enclosed in a single <text> XML tag.
# OUTPUT FORMAT
Strictly adhere to the following format. Do not output anything else.
<wscore>[Your calculated score, e.g., 0.85]</wscore>
<text>
[The full extracted text from the image goes here.]
</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_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返回的XML格式输出提取分数和文本
wscore_match = re.search(r'<wscore>(.*?)</wscore>', vlm_output, re.DOTALL)
text_match = re.search(r'<text>(.*?)</text>', vlm_output, re.DOTALL)
original_wscore = float(wscore_match.group(1).strip()) if wscore_match else 0.0
essay_text = text_match.group(1).strip() if text_match else "错误:无法从图片中提取文本。"
try:
sensitivity_factor = float(self.config.get("SensitivityFactor", "1.0"))
except (ValueError, TypeError):
# 如果配置的敏感度因子无效则使用默认值1.0
sensitivity_factor = 1.0
wscore = original_wscore ** sensitivity_factor
if not text_match:
raise ValueError(f"VLM未能按预期格式返回无法解析文本。模型返回\n{vlm_output}")
# --- 步骤 2: 调用LLM生成批改报告 ---
llm_client = OpenAI(
api_key=self.config.get("LlmApiKey"),
base_url=self.config.get("LlmUrl")
)
# 从配置加载Prompt模板若用户未定义则使用默认模板
prompt_template = self.config.get("LlmPromptTemplate")
if not prompt_template:
prompt_template = DEFAULT_LLM_PROMPT_TEMPLATE
# 使用作文题目、书写分数和识别出的文本填充Prompt模板
final_llm_prompt = prompt_template.format(
topic=topic,
wscore=wscore,
essay_text=essay_text
)
llm_messages = [{"role": "user", "content": final_llm_prompt}]
llm_model = self.config.get("LlmModel", "gemini-2.5-pro")
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