python
复制代码
import tkinter as tk
from tkinter import filedialog, Text, ttk, messagebox
from PIL import Image, ImageTk, ImageGrab, ImageFilter
import requests
import json
import base64
import re
import threading
import time
from datetime import datetime
import pyautogui
import os
import configparser
# -------------------- 全局变量 --------------------
start_x, start_y = 0, 0 # 记录鼠标按下时的坐标
fixed_region = None
button_positions = {
"next_question": (800, 600),
"score_buttons": {k: (200 + 20 * k, 400) for k in range(11)}
}
is_running = False
logger = []
config_file = "auto_grading_config.ini"
screenshot_path = "screenshot.png"
# -------------------- 配置区域(请替换为你的API密钥)--------------------
OCR_API_KEY = "" # 百度OCR API Key
OCR_SECRET_KEY = "" # 百度OCR Secret Key
ALI_API_KEY = "" # 阿里通义千问API Key
OCR_URL = "https://aip.baidubce.com/rest/2.0/ocr/v1/handwriting"
ALI_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
ALI_MODEL = "qwen-plus"
# -------------------- 函数定义 --------------------
def load_config():
"""加载配置文件"""
global fixed_region, button_positions
try:
if os.path.exists(config_file):
config = configparser.ConfigParser()
config.read(config_file)
if config.has_section("fixed_region"):
fixed_region = (
int(config.get("fixed_region", "x1")),
int(config.get("fixed_region", "y1")),
int(config.get("fixed_region", "x2")),
int(config.get("fixed_region", "y2"))
)
if config.has_section("button_positions"):
button_positions["next_question"] = (
int(config.get("button_positions", "next_question_x")),
int(config.get("button_positions", "next_question_y"))
)
for score in range(11):
if f"score_{score}_x" in config["button_positions"]:
button_positions["score_buttons"][score] = (
int(config.get("button_positions", f"score_{score}_x")),
int(config.get("button_positions", f"score_{score}_y"))
)
update_log("配置加载成功")
except Exception as e:
messagebox.showerror("配置加载失败", f"错误:{str(e)}")
update_log(f"配置加载失败: {str(e)}")
def save_config():
"""保存配置文件"""
try:
config = configparser.ConfigParser()
if fixed_region:
config["fixed_region"] = {
"x1": str(fixed_region[0]),
"y1": str(fixed_region[1]),
"x2": str(fixed_region[2]),
"y2": str(fixed_region[3])
}
config["button_positions"] = {
"next_question_x": str(button_positions["next_question"][0]),
"next_question_y": str(button_positions["next_question"][1])
}
for score, (x, y) in button_positions["score_buttons"].items():
config["button_positions"][f"score_{score}_x"] = str(x)
config["button_positions"][f"score_{score}_y"] = str(y)
with open(config_file, 'w') as f:
config.write(f)
update_log("配置保存成功")
except Exception as e:
messagebox.showerror("配置保存失败", f"错误:{str(e)}")
update_log(f"配置保存失败: {str(e)}")
def get_ocr_token():
"""获取OCR服务Token"""
url = f"https://aip.baidubce.com/oauth/2.0/token?client_id={OCR_API_KEY}&client_secret={OCR_SECRET_KEY}&grant_type=client_credentials"
return requests.get(url).json().get("access_token")
def get_ali_token():
"""获取阿里大模型Token"""
return ALI_API_KEY
def image_to_base64(path):
"""图像转Base64"""
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def ocr_recognize(image_b64):
"""手写识别"""
token = get_ocr_token()
url = f"{OCR_URL}?access_token={token}"
data = {"image": image_b64, "language_type": "CHN_ENG"}
try:
response = requests.post(url, data=data)
response.raise_for_status()
result = response.json()
update_log(f"OCR识别成功,识别到{len(result.get('words_result', []))}行文字")
return result
except Exception as e:
update_log(f"OCR识别失败: {str(e)}")
return {"words_result": []}
def ali_evaluate(standard_answer, student_answer, difficulty, total_score):
"""带重试机制的评分函数"""
for attempt in range(3):
try:
update_log(f"尝试评分请求 ({attempt + 1}/3)")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {get_ali_token()}"
}
prompt = f"""
请严格返回裸JSON格式:
{{"分数": 0, "评价": "", "详细评价": ""}}
评分要求:
总分:{total_score}分
难度等级:{difficulty}
标准答案:{standard_answer}
考生答案:{student_answer}
"""
payload = {
"model": ALI_MODEL,
"messages": [{"role": "user", "content": prompt}]
}
response = requests.post(f"{ALI_BASE_URL}/chat/completions", headers=headers, json=payload)
response.raise_for_status()
result = response.json()["choices"][0]["message"]["content"]
# 清理格式
result = re.sub(r'^```json\s*|\s*```$', '', result).strip()
return result
except json.JSONDecodeError:
update_log(f"格式错误,尝试清理: {result[:50]}...")
result = re.sub(r'\s+', ' ', result).strip()
except Exception as e:
update_log(f"评分请求失败: {str(e)}")
time.sleep(1)
return '{"分数": 0, "评价": "评分失败", "详细评价": "多次尝试失败,请检查网络"}'
def parse_score_result(score_result, total_score):
"""健壮的解析函数"""
try:
data = json.loads(score_result)
score = max(0, min(int(data.get("分数", 0)), total_score))
comment = data.get("评价", "未获取评价")[:50]
detailed = data.get("详细评价", "无详细评价")[:200]
return score, comment, detailed
except:
return 0, "解析失败", "大模型返回格式异常"
def save_fixed_region(x1, y1, x2, y2):
"""保存固定区域"""
global fixed_region
fixed_region = (x1, y1, x2, y2)
save_config()
messagebox.showinfo("区域保存成功", f"区域坐标:({x1}, {y1}, {x2}, {y2})")
update_log(f"设置固定区域: ({x1}, {y1}, {x2}, {y2})")
def capture_fixed_region():
"""带预处理的截图"""
if not fixed_region:
messagebox.showerror("未设置区域", "请先设置答题区域")
return False
try:
screenshot = ImageGrab.grab(bbox=fixed_region)
screenshot = screenshot.convert('L').filter(ImageFilter.MedianFilter())
screenshot.save(screenshot_path)
update_log("截图并预处理完成")
return True
except:
messagebox.showerror("截图失败", "请检查区域设置")
return False
def capture_button_position(button_type, score=None):
"""捕获按钮位置"""
global root
def on_click(event):
x, y = event.x_root, event.y_root
if button_type == "next_question":
button_positions["next_question"] = (x, y)
else:
button_positions["score_buttons"][score] = (x, y)
save_config()
capture_window.destroy()
root.deiconify()
root.withdraw()
capture_window = tk.Toplevel(root)
capture_window.attributes('-fullscreen', True, '-alpha', 0.5)
capture_window.bind("<Button-1>", on_click)
capture_window.bind("<Escape>", lambda e: capture_window.destroy())
capture_window.focus_set()
def simulate_click(pos, retries=3, delay=1):
"""带重试的点击函数"""
for _ in range(retries):
try:
pyautogui.moveTo(pos[0], pos[1], duration=0.3)
pyautogui.click()
update_log(f"点击成功: {pos}")
return True
except:
update_log(f"点击失败,重试中...")
time.sleep(delay)
messagebox.showerror("点击失败", "多次尝试后仍无法点击")
return False
def auto_next_question():
"""自动点击下一题"""
if button_positions["next_question"] and simulate_click(button_positions["next_question"]):
time.sleep(2) # 等待页面加载
def capture_screen():
"""增强版全屏截图设置区域"""
global root
root.withdraw()
time.sleep(0.3)
# 在函数内部定义变量,使其成为嵌套函数的外围作用域
start_x, start_y = 0, 0
capture_window = tk.Toplevel(root)
capture_window.attributes('-fullscreen', True, '-alpha', 0.5)
capture_window.attributes('-topmost', True)
canvas = tk.Canvas(capture_window, bg='black')
canvas.pack(fill=tk.BOTH, expand=True)
# 鼠标按下时记录坐标并绘制临时矩形
def on_mouse_down(event):
nonlocal start_x, start_y # 现在可以正确绑定
start_x, start_y = event.x, event.y
canvas.create_rectangle(start_x, start_y, start_x, start_y,
outline='red', width=2, tags='temp_rect')
# 鼠标释放时计算区域并保存
def on_mouse_up(event):
nonlocal start_x, start_y # 现在可以正确绑定
x1, y1 = min(start_x, event.x), min(start_y, event.y)
x2, y2 = max(start_x, event.x), max(start_y, event.y)
canvas.delete('temp_rect')
canvas.create_rectangle(x1, y1, x2, y2,
outline='green', width=2, tags='final_rect')
save_fixed_region(x1, y1, x2, y2)
capture_window.after(1000, capture_window.destroy)
root.deiconify()
canvas.bind("<Button-1>", on_mouse_down)
canvas.bind("<ButtonRelease-1>", on_mouse_up)
capture_window.focus_set()
capture_window.wait_window()
def update_result(score, comment, detailed):
"""线程安全更新界面"""
root.after(0, lambda: score_var.set(f"{score}/{total_score_var.get()}"))
root.after(0, lambda: comment_var.set(comment))
root.after(0, lambda: detailed_comment_text.delete(1.0, tk.END))
root.after(0, lambda: detailed_comment_text.insert(tk.END, detailed))
def grading_task():
"""单个评分任务"""
global is_running
standard_answer = standard_entry.get("1.0", tk.END).strip()
if not standard_answer:
messagebox.showerror("请输入标准答案")
is_running = False
return
if not capture_fixed_region():
is_running = False
return
image_b64 = image_to_base64(screenshot_path)
ocr_result = ocr_recognize(image_b64)
student_answer = "\n".join([item["words"] for item in ocr_result["words_result"]])
score_result = ali_evaluate(standard_answer, student_answer, difficulty_combobox.get(), total_score_var.get())
score, comment, detailed = parse_score_result(score_result, total_score_var.get())
update_result(score, comment, detailed)
if is_running and score in button_positions["score_buttons"]:
update_log(f"点击分数{score}按钮")
simulate_click(button_positions["score_buttons"][score])
time.sleep(2) # 点击后延迟
update_log(f"评分完成:{score}/{total_score_var.get()}")
def auto_grading_process():
"""主流程控制"""
global is_running
if is_running:
is_running = False
start_btn.config(text="开始自动阅卷")
return
if not fixed_region:
capture_screen()
if not fixed_region:
return
for score in range(11):
if not button_positions["score_buttons"][score]:
messagebox.showerror("请先采集所有分数按钮")
return
is_running = True
start_btn.config(text="停止自动阅卷")
threading.Thread(target=grading_loop, daemon=True).start()
def grading_loop():
"""循环阅卷"""
global is_running
while is_running:
grading_task()
if is_running:
auto_next_question()
def update_log(message):
"""日志更新"""
log_text.config(state=tk.NORMAL)
log_text.insert(tk.END, f"[{datetime.now():%H:%M:%S}] {message}\n")
log_text.config(state=tk.DISABLED)
log_text.see(tk.END)
logger.append(message)
if len(logger) > 1000:
logger.pop(0)
def export_log():
"""导出日志"""
file_path = filedialog.asksaveasfilename(defaultextension=".log")
if file_path:
with open(file_path, 'w') as f:
f.write("\n".join(logger))
# -------------------- 界面设计 --------------------
root = tk.Tk()
root.title("智能阅卷系统 - 最终版")
root.geometry("1000x800")
root.configure(bg="#f5f5f5")
# 日志区域
log_frame = tk.Frame(root, bg="#f9f9f9", bd=1, relief=tk.SOLID)
log_frame.pack(fill=tk.X, padx=20, pady=5)
tk.Label(log_frame, text="操作日志", font=("微软雅黑", 10)).pack(side=tk.LEFT, padx=5)
log_text = tk.Text(log_frame, height=2, bg="#fff", state=tk.DISABLED, wrap=tk.WORD)
log_text.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
tk.Button(log_frame, text="导出日志", command=export_log, bg="#4CAF50", fg="white", width=8).pack(side=tk.RIGHT, padx=5)
# 结果显示区域
result_frame = tk.Frame(root, bg="#e0f7fa", bd=1, relief=tk.SOLID, padx=20, pady=10)
result_frame.pack(fill=tk.X, padx=20, pady=10)
score_var = tk.StringVar(value="0/10")
tk.Label(result_frame, text="当前得分:", font=("微软雅黑", 18, "bold")).pack(side=tk.LEFT, padx=10)
tk.Label(result_frame, textvariable=score_var, font=("微软雅黑", 28, "bold"), fg="#ff5722").pack(side=tk.LEFT)
comment_var = tk.StringVar(value="等待评分")
tk.Label(result_frame, text="评语:", font=("微软雅黑", 18, "bold"), padx=20).pack(side=tk.LEFT)
tk.Label(result_frame, textvariable=comment_var, font=("微软雅黑", 16), fg="#2196F3").pack(side=tk.LEFT, fill=tk.X)
# 主操作区
main_frame = tk.Frame(root, bg="#f5f5f5", padx=20, pady=20)
main_frame.pack(fill=tk.BOTH, expand=True)
left_frame = tk.Frame(main_frame, bg="#fff", bd=1, relief=tk.SOLID, padx=10, pady=10)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)
tk.Label(left_frame, text="标准答案", font=("微软雅黑", 14), pady=10).pack()
standard_entry = tk.Text(left_frame, height=3, font=("微软雅黑", 12))
standard_entry.pack(fill=tk.X, pady=5)
standard_entry.insert(tk.END, "请输入标准答案...")
tk.Label(left_frame, text="评分难度", font=("微软雅黑", 14), pady=10).pack()
difficulty_combobox = ttk.Combobox(left_frame, values=["简单", "中等", "困难"], width=10, font=("微软雅黑", 12))
difficulty_combobox.set("中等")
difficulty_combobox.pack(pady=5)
tk.Label(left_frame, text="本题总分", font=("微软雅黑", 14), pady=10).pack()
total_score_var = tk.IntVar(value=10)
score_scale = tk.Scale(left_frame, from_=0, to=100, orient=tk.HORIZONTAL, variable=total_score_var, length=200)
score_scale.pack(pady=5)
tk.Label(left_frame, textvariable=total_score_var, font=("微软雅黑", 12)).pack(pady=5)
button_frame = tk.Frame(left_frame, bg="#fff", pady=10)
button_frame.pack(fill=tk.X)
tk.Button(button_frame, text="设置答题区域", command=capture_screen, bg="#4CAF50", fg="white", width=12,
font=("微软雅黑", 12)).pack(side=tk.LEFT, padx=5)
start_btn = tk.Button(button_frame, text="开始自动阅卷", command=auto_grading_process, bg="#2196F3", fg="white",
width=15, font=("微软雅黑", 12))
start_btn.pack(side=tk.LEFT, padx=5)
# 按钮采集区
capture_frame = tk.Frame(left_frame, bg="#fff", pady=10)
capture_frame.pack(fill=tk.X)
tk.Label(capture_frame, text="采集分数按钮", font=("微软雅黑", 12)).pack(side=tk.LEFT, padx=5)
score_combobox = ttk.Combobox(capture_frame, values=list(range(11)), width=3, font=("微软雅黑", 12))
score_combobox.set(0)
score_combobox.pack(side=tk.LEFT, padx=2)
tk.Button(capture_frame, text="采集", command=lambda: capture_button_position("score", int(score_combobox.get())),
bg="#ff5722", fg="white", width=8).pack(side=tk.LEFT, padx=5)
tk.Button(capture_frame, text="采集下一题", command=lambda: capture_button_position("next_question"), bg="#ff9800",
fg="white", width=10).pack(side=tk.LEFT, padx=5)
# 详细评价区域
right_frame = tk.Frame(main_frame, bg="#fff", bd=1, relief=tk.SOLID, padx=10, pady=10)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10)
tk.Label(right_frame, text="详细评价", font=("微软雅黑", 14), pady=10).pack()
detailed_comment_text = tk.Text(right_frame, wrap=tk.WORD, font=("微软雅黑", 12), height=20)
detailed_comment_text.pack(fill=tk.BOTH, expand=True)
scrollbar = tk.Scrollbar(detailed_comment_text)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
detailed_comment_text.config(yscrollcommand=scrollbar.set)
# 初始化
load_config()
update_log("系统启动完成,点击"设置答题区域"开始配置")
root.mainloop()