diff --git a/.gitignore b/.gitignore index af1e2bb..6c40309 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ Thumbs.db # Local configuration file # This file is generated by the application and contains user-specific secrets. -# It should not be committed to version control. \ No newline at end of file +# It should not be committed to version control. + +config.json +*.html \ No newline at end of file diff --git a/project_analysis_report.md b/project_analysis_report.md new file mode 100644 index 0000000..81bcd1d --- /dev/null +++ b/project_analysis_report.md @@ -0,0 +1,123 @@ +# AI 作文批改助手 - 深度代码分析与架构报告 + +## 1. 项目概述与设计哲学 + +本项目是一个基于 Python 和 Tkinter 的桌面应用程序,名为 “AI 作文批改助手”。其核心目标是利用先进的 AI 模型,为教育者和学生提供一个高效、精准且高度可定制的英文作文批改解决方案。 + +**设计哲学**: +该应用的核心设计哲学是 **“专业分工”** 与 **“极致灵活”**。 +- **专业分工**: 它不依赖单一的通用 AI 模型,而是采用“视觉语言模型(VLM)” + “大语言模型(LLM)”的两步式处理流程。VLM 专注于其擅长的图像识别和质量评估,而 LLM 则负责深度、富有逻辑的文本分析与批改。这种分工确保了流程的每个环节都由最合适的工具来完成,从而最大化了准确性和效率。 +- **极致灵活**: 开发者深刻理解不同用户(或不同教学场景)对批改的要求千差万别。因此,应用将所有核心参数,从 API 服务地址、模型名称,到最关键的 LLM 指令模板(Prompt),都开放给用户配置。这使得应用不仅仅是一个工具,更是一个可以被用户“训练”和“塑造”的个性化批改平台。 + +--- + +## 2. 核心功能与工作流程(深度解析) + +应用程序的工作流程经过精心设计,确保了健壮性、效率和优秀的用户体验。 + +1. **启动与配置校验**: + - 应用启动时,`ConfigManager` 会立刻加载 `config.json`。如果文件不存在,它会自动创建一个空的,避免了首次运行因缺少文件而崩溃。 + - 在主界面初始化时,会调用 `config_manager.check_settings()` 检查所有必需的 API 配置。如果任何一项缺失,程序会进入一个**强制配置循环** (`_show_config_dialog_until_valid`),不断弹出设置窗口,直到用户填妥所有关键信息为止,确保了后续 API 调用的先决条件。 + +2. **用户交互**: + - 用户在主界面的 `tk.Text` 组件中输入作文题目。这里有一个非常贴心的 UI 细节:通过绑定 `` 和 `` 事件,实现了输入框的占位符(Placeholder)效果,引导用户输入。 + - 用户点击“选择图片”,可以一次性选择多个文件,程序将文件路径存储在 `self.file_paths` 列表中。 + +3. **并发处理启动**: + - 点击“开始批改”后,程序首先验证文件和题目是否都已提供。 + - 然后,它会创建一个新的后台守护线程 (`threading.Thread`),目标是 `_concurrent_worker_manager` 方法。这样做是为了将耗时的任务完全与主 UI 线程分离,防止界面冻结。 + - `_concurrent_worker_manager` 内部使用 `concurrent.futures.ThreadPoolExecutor` 创建一个线程池,其最大工作线程数 (`max_workers`) 由用户在设置中定义。它会遍历所有图片路径,为每一张图片向线程池提交一个 `_process_single_file` 的处理任务。 + +4. **两步式 AI 引擎 (核心细节)**: + - **步骤一:VLM 视觉分析 (`api_services.py`)** + - 工作线程首先调用 `api_service.process_essay_image`。 + - 该方法的第一步是调用 VLM。其 Prompt 极为关键,它严格指示 VLM 扮演一个“高精度 OCR 和手写分析引擎”,并用 `` 和 `` XML 标签包裹其输出。这种结构化输出的要求,使得后续用正则表达式 (`re.search`) 解析结果变得非常可靠。 + - 应用还引入了“手写打分敏感度” (`SensitivityFactor`) 的概念。它通过 `wscore = original_wscore ** sensitivity_factor` 这行代码,对 VLM 的原始评分进行指数调整。当因子 > 1.0 时,会拉大高分和低分区间的差距,使得评分更严格;反之则更宽松。 + - **步骤二:LLM 深度批改 (`api_services.py`)** + - 拿到 VLM 的输出后,程序会加载 LLM 的 Prompt 模板。如果用户在设置中自定义了模板,则使用用户的版本;否则,使用 `DEFAULT_LLM_PROMPT_TEMPLATE`。 + - 这个默认模板是一个极其详尽的“元程序”,它为 LLM 设定了清晰的角色(高三英语老师)、目标、输入数据格式,并提供了详细的评分逻辑。它甚至教会了 LLM 如何根据作文字数和题目类型线索来区分“应用文”和“读后续写”这两种题型,并应用不同的总分(15分 vs 25分)。 + - 最后,程序将作文题目、调整后的书写分数和识别出的文本填入模板,向 LLM 发起最终的批改请求。 + +5. **结果生成与线程安全UI更新**: + - `_process_single_file` 线程在收到 AI 的最终报告后,会将其写入一个新的 `.md` 文件。 + - **关键的 UI 更新机制**: + - 工作线程**从不直接操作**任何 Tkinter UI 组件。 + - 取而代之,它将所有需要反馈给用户的信息(如“开始处理...”、“处理完成...”、“Token用量...”、“失败...”,以及进度更新信号)都通过 `self.ui_queue.put()` 方法放入一个线程安全的 `queue.Queue` 实例中。 + - 与此同时,在主 UI 线程中,一个由 `self.root.after(100, self._process_ui_queue)` 启动的**定时轮询器**每 100 毫秒被唤醒一次。 + - 这个轮询器会尝试从队列中 `get_nowait()` 消息。一旦取到消息,它就可以**安全地**在主线程中更新日志列表框和进度条等 UI 元素。 + - **线程安全的数据更新**: 当需要更新累计的 Token 使用量时,工作线程会使用 `with self.lock:` 来获取一个线程锁 (`threading.Lock`),确保在任何时刻只有一个线程能修改和保存 `config.json`,避免了数据竞争和文件损坏。 + +--- + +## 3. 技术架构与设计亮点(深度分析) + +- **依赖注入 (Dependency Injection)**: 在 `main.py` 中,`ConfigManager` 和 `ApiService` 的实例被创建后,作为参数“注入”到 `MainApp` 的构造函数中。这是一种优秀的设计模式,它降低了 `MainApp` 与具体服务实现之间的耦合。如果未来需要替换 `ApiService` 的实现(例如,换成一个本地模型的服务),只需在 `main.py` 中修改一行代码,而无需改动 `MainApp` 内部。 + +- **企业级的配置安全 (`config_manager.py`)**: + - 应用没有简单地将 API 密钥明文存储,而是实现了一套强大的对称加密机制。 + - 它使用 `hashlib.pbkdf2_hmac`,这是一个基于密码的密钥派生函数。它将一个固定的内部密码 (`_ENCRYPTION_PASSWORD`) 和一个“盐” (`_SALT`) 通过大量哈希迭代(100,000次),生成一个几乎不可能被逆向破解的、安全的加密密钥。 + - 然后,它使用这个密钥初始化 `cryptography.fernet.Fernet` 实例。Fernet 保证了加密信息是经过认证的(无法被篡改),并且包含了时间戳以防止重放攻击,提供了远超普通 Base64 编码的安全性。 + +- **健壮的并发模型与线程安全 (`app_ui.py`)**: + - 这是本项目技术上最出色的部分。它完美地解决了桌面 GUI 应用中最常见的难题:如何在执行长耗时任务的同时保持界面响应。 + - 它综合运用了 `ThreadPoolExecutor` (管理工作线程)、`queue.Queue` (作为线程间通信的缓冲管道) 和 `root.after` (在主线程中创建非阻塞的轮询器),构成了一个经典且高效的生产者-消费者模型。工作线程是消息的“生产者”,主线程的轮询器是“消费者”。 + - 同时,通过 `threading.Lock` 确保了对共享资源(配置文件)的互斥访问。这套组合拳充分展示了开发者对并发编程的深刻理解。 + +--- + +## 4. 代码中的亮点与最佳实践 + +- **优雅的占位符实现**: 在 `app_ui.py` 中,通过绑定 `FocusIn` 和 `FocusOut` 事件来动态改变 `Text` 控件的内容和前景颜色,是一种轻量级且有效的实现输入框占位符的方法。 +- **对打包部署的考量**: `main.py` 中的 `resource_path` 函数是一个重要的细节。它通过检查 `sys._MEIPASS` 属性是否存在,来判断程序是在开发环境中运行还是被 PyInstaller 打包后运行,从而动态地计算配置文件的正确路径。这确保了应用在被打包成单文件可执行程序后依然能正常工作。 +- **智能的 Prompt 模板管理**: 在 `SettingsDialog` 的 `on_ok` 方法中,如果用户修改后的 Prompt 与默认模板一致,程序会将其设置为 `None`,并在保存时从配置文件中移除该键。这避免了将冗长的默认模板存入配置文件,保持了 `config.json` 的整洁。 + +--- + +## 5. Mermaid 流程图 + + + +```mermaid +graph TD + %% 主方向改为 Top-Down,使整体更接近正方形; + %% 子图内部按需使用 LR 来平衡横向长度。 + subgraph UI["UI (主线程)"] + style UI fill:#E6F3FF,stroke:#007BFF + A["用户操作
(选择图片, 输入题目)"] --> B{"点击'开始批改'"} + B --> C["MainApp: _start_processing()"] + C --> D["启动后台线程池"] + + subgraph UIQ["UI 更新循环"] + direction LR + V["_process_ui_queue()
(定时轮询)"] -- "get() 消息" --> U_Queue[(UI 队列)] + V --> W["/更新日志和进度条/"] + end + end + + subgraph BACK["后台 (工作线程池)"] + style BACK fill:#FFF2E6,stroke:#FF8C00 + D -- "为每个文件提交任务" --> E["_process_single_file()"] + E --> F_Call["调用 ApiService"] + F_Call --> P_Return{"获取
(报告, Token用量)"} + P_Return --> S["写入 .md 报告"] + P_Return --> R["更新 Token (加锁)"] + S & R -- "put() 消息" --> U_Queue + end + + subgraph SRV["服务和数据"] + style SRV fill:#E6FFEB,stroke:#28A745 + F_Call --> F["ApiService: process_essay_image()"] + F --> G["VLM 调用"] + G --> K["LLM 调用"] + K --> P_Return + Q[(Config Manager)] -- "提供密钥" --> G & K + R --> Q + end + + %% 用显式的纵向连接平衡整体宽度 + UI --> BACK + BACK --> SRV + + style U_Queue fill:#D1C4E9,stroke:#673AB7,stroke-width:2px + style Q fill:#FFCDD2,stroke:#D32F2F,stroke-width:2px +``` \ No newline at end of file diff --git a/project_analysis_report_dev.md b/project_analysis_report_dev.md new file mode 100644 index 0000000..181368d --- /dev/null +++ b/project_analysis_report_dev.md @@ -0,0 +1,157 @@ +# AI 作文批改助手 - 开发者技术架构文档 + +## 1. 系统概述 + +**AI 作文批改助手** 是一款基于 Python/Tkinter 的桌面应用,旨在为教育场景提供一个高度可定制的自动化英文作文批改解决方案。其架构核心是围绕 **服务解耦**、**安全配置** 和 **健壮的并发处理** 构建的,确保了系统的灵活性、安全性与高性能。 + +本文档旨在为参与本项目的开发者提供清晰的架构解析、模块说明和核心工作流程,以促进高效的协同开发。 + +--- + +## 2. 模块化架构解析 + +系统采用分层架构,将UI、业务逻辑和配置管理严格分离。核心模块如下: + +| 文件名 | 模块职责 | 关键技术/模式 | +| :--- | :--- | :--- | +| `main.py` | **应用入口 (Entry Point)** | 依赖注入 (DI) | +| `app_ui.py` | **UI与主控层 (View/Controller)** | Tkinter, 生产者-消费者模型, 并发控制 | +| `api_services.py` | **外部服务适配层 (Service Adapter)** | OpenAI API, 模板方法模式 | +| `config_manager.py` | **配置与安全层 (Configuration/Security)** | Fernet 对称加密, 单例模式思想 | + +### 2.1. `main.py`: 依赖注入与启动器 + +- **职责**: 作为应用的启动器,负责初始化所有核心服务 (`ConfigManager`, `ApiService`),并将这些服务的实例 **注入** 到主应用 `MainApp` 中。 +- **设计优势**: 这种依赖注入的方式,极大地降低了 `MainApp` 与具体服务实现之间的耦合度。例如,若未来需要将 `ApiService` 切换为调用本地模型,只需在 `main.py` 中替换 `ApiService` 的实现类,而 `MainApp` 的代码无需任何改动。 +- **打包考量**: `resource_path` 函数通过检查 `sys._MEIPASS` 属性,实现了对 PyInstaller 打包环境的兼容,确保了配置文件路径在开发和部署环境中的一致性。 + +### 2.2. `config_manager.py`: 企业级安全配置 + +- **职责**: 负责 `config.json` 的全生命周期管理,包括加载、保存、读取和写入。其核心特性是 **对敏感信息的自动加解密**。 +- **加密机制**: + 1. **密钥派生**: 使用 `hashlib.pbkdf2_hmac` 基于固定的内部密码和盐,通过 100,000 次迭代生成一个高强度的加密密钥。这可以有效抵御彩虹表和暴力破解攻击。 + 2. **认证加密**: 利用 `cryptography.fernet.Fernet` 实现 **认证加密 (AEAD)**。这意味着加密后的数据不仅保密,还能防止篡改(完整性保护),并内置 +时间戳以防止重放攻击。 +- **解密容错**: `_decrypt` 方法中包含 `try...except InvalidToken` 块,确保了即使配置文件中的某个值是未加密的旧数据或已损坏,程序也不会崩溃,而是安全地返回空字符串。 + +### 2.3. `api_services.py`: AI 服务适配器 + +- **职责**: 封装了与外部 AI 服务(VLM 和 LLM)的所有交互细节,为上层应用提供统一、简洁的调用接口 (`process_essay_image`)。 +- **两步式 AI 流程 (Two-Step AI Pipeline)**: + 1. **VLM 阶段**: + - **输入**: 图片路径。 + - **处理**: 将图片编码为 Base64,调用 VLM API。Prompt 设计得极为严格,要求 VLM 返回 **结构化数据**(`` 和 `` XML 标签),这使得后续解析非常可靠。 + - **输出**: 书写分数 (`wscore`) 和识别出的文本 (`essay_text`)。 + 2. **LLM 阶段**: + - **输入**: 作文题目、VLM 阶段的书写分数和文本。 + - **处理**: 加载 Prompt 模板(优先使用用户自定义版本),将输入数据填入模板,调用 LLM API。 + - **输出**: 最终的 Markdown 格式批改报告。 +- **设计优势**: 将所有 API Key、URL 和模型名称的管理完全委托给 `ConfigManager`,自身不存储任何敏感信息。同时,通过 `DEFAULT_LLM_PROMPT_TEMPLATE` 提供了开箱即用的默认行为,也支持用户通过 +`config.json` 进行深度定制。 + +### +2.4. `app_ui.py`: 并发模型与线程安全UI + +- **职责**: 作为应用的核心控制器,负责UI渲染、事件响应,以及 **协调UI主线程与后台工作线程的交互**。 +- **并发模型 (生产者-消费者)**: 这是本应用技术实现上最关键的部分。 + 1. **生产者 (Producer)**: 后台工作线程 (`_process_single_file`)。当用户点击“开始批改”后,`_concurrent_worker_manager` 会创建一个 `ThreadPoolExecutor` 线程池。池中的每个线程在完成一项任务(调用API、保存文件)后,会将结果或状态更新(如日志消息、进度信号)封装成一个元组 `(task_type, data)`,通过 `self.ui_queue.put()` 方法放入一个线程安全的 `queue.Queue` 实例中。 + 2. **消费者 (Consumer)**: UI主线程中的 `_process_ui_queue` 方法。该方法由 `self.root.after(100, ...)` 启动,作为一个 **非阻塞的定时轮询器**,每100毫秒被唤醒一次。它会尝试从 `ui_queue` 中 `get_nowait()` 消息。一旦获取到消息,它就可以 **安全地在UI主线程中** 执行 +UI更新操作(如更新日志列表、增加进度条)。 +- **线程安全**: + - **UI操作**: 遵循了GUI编程的黄金法则—— **任何UI组件的修改都必须在主线程中进行**。通过 `queue.Queue` 实现了线程间通信,避免了后台线程直接操作UI组件。 + - **数据共享**: 对于需要被多个线程修改的共享资源(累计的Token使用量),代码在 `_process_single_file` 中使用了 `with self.lock:` (`threading.Lock`) 来确保对 `config.json` 的读写操作是 **原子性** 的,从而防止了数据竞争和文件损坏。 + +--- + +## 3. 核心工作流程 (端到端) + +以下是用户从点击“开始批改”到任务完成的完整数据流和控制流: + +1. **UI线程**: `_start_processing` 被触发。 + - **校验**: 检查文件列表和作文题目是否为空。 + - **初始化**: 重置进度条和计数器。 + - **启动后台任务**: 创建一个新的守护线程 (`threading.Thread`),其目标是 `_concurrent_worker_manager` 方法。这确保了耗时的任务管理器本身不会阻塞UI。 + +2. **后台管理线程**: `_concurrent_worker_manager` 开始执行。 + - **读取配置**: 从 `ConfigManager` 获取 `MaxWorkers` 参数。 + - **创建线程池**: 实例化一个 `concurrent.futures.ThreadPoolExecutor`。 + - **任务分发**: 遍历所有文件路径,为每个文件向线程池提交一个 `_process_single_file` 任务。 + +3. **后台工作线程 (并发执行)**: `_process_single_file` 开始执行。 + - **状态通知 (生产消息)**: 向 `ui_queue` 推入一条 `("log", "开始处理...")` 消息。 + - **调用服务层**: 调用 `api_service.process_essay_image`,传入文件路径和题目。 + - `ApiService` 内部依次完成VLM和LLM的API调用。 + - **处理结果**: + - **成功**: 将返回的 `final_report` 写入 `.md` 文件。 + - **成功**: 使用 `threading.Lock` 安全地更新 `ConfigManager` 中的Token用量并保存。 + - **成功/失败**: 向 `ui_queue` 推入日志消息、Token用量消息。 + - **进度通知 (生产消息)**: 无论成功与否,都向 `ui_queue` 推入一条 `("progress", 1)` 消息。 + +4. **UI线程 (轮询)**: `_process_ui_queue` 定期执行。 + - **消费消息**: 从 `ui_queue` 中取出消息。 + - **解析与执行**: 根据消息类型 (`"log"`, `"progress"`, `"finish"`),安全地更新UI组件(`Listbox`, `Progressbar`)或弹出完成对话框。 + +5. **任务结束**: + - 当线程池中的所有任务都完成后,`_concurrent_worker_manager` 方法执行完毕。 + - 它向 `ui_queue` 推入一条 `("finish", None)` 消息,触发UI线程显示最终的完成提示。 + +--- + +## 4. 系统交互流程图 (Mermaid) + +```mermaid +graph TD + A["用户: 选择图片文件"] --> B["用户: 输入作文题目"]; + B --> C{"用户: 点击'开始'按钮"}; + C --> D["UI线程: _start_processing()"]; + D --> E["UI线程: 校验输入"]; + E -- 输入有效 --> F["UI线程: 重置进度条"]; + F --> G["UI线程: 创建并启动
后台管理线程"]; + + G --> H["管理线程: _concurrent_worker_manager() 启动"]; + H --> I["管理线程: 从配置读取MaxWorkers"]; + I --> J["管理线程: 创建ThreadPoolExecutor"]; + J --> K["管理线程: 遍历所有文件路径"]; + K --> L["管理线程: 为每个文件提交
_process_single_file 任务"]; + + L -- 提交任务 --> M["工作线程: _process_single_file() 启动"]; + M --> M_LOG_START["工作线程: 推送('log', '开始处理...')"]; + M_LOG_START --> Y[(UI队列)]; + + M_LOG_START --> N["工作线程: 调用 api_service.process_essay_image()"]; + N --> N1["ApiService: _encode_image_to_base64_url()"]; + N1 --> N2["ApiService: 构建VLM Prompt"]; + N2 --> N3["ApiService: 调用VLM API"]; + N3 --> N4["ApiService: 解析VLM响应 (wscore, text)"]; + N4 --> N5["ApiService: 应用敏感度因子"]; + N5 --> N6["ApiService: 加载LLM Prompt模板"]; + N6 --> N7["ApiService: 格式化最终LLM Prompt"]; + N7 --> N8["ApiService: 调用LLM API"]; + N8 --> N9["ApiService: 返回批改报告和Token用量"]; + + N9 --> O{"工作线程: API调用是否成功?"}; + O -- 是 --> P["工作线程: 将报告写入.md文件"]; + P --> Q["工作线程: 获取线程锁 (threading.Lock)"]; + Q --> R["工作线程: 更新Token用量到配置"]; + R --> S["工作线程: 保存config.json"]; + S --> T["工作线程: 释放线程锁"]; + T --> U["工作线程: 推送('log', '处理成功...')"]; + U --> Y; + + O -- 否 --> V["工作线程: 获取异常详情"]; + V --> W["工作线程: 推送('log', '处理失败...')"]; + W --> Y; + + U --> X["工作线程: 推送('progress', 1)"]; + W --> X; + X --> Y; + + L -- 所有任务完成 --> AA["管理线程: 推送('finish', None)"]; + AA --> Y; + + Y -- get_nowait() --> Z["UI线程: _process_ui_queue() (每100ms)"]; + Z --> ZA{"UI线程: 检查消息类型"}; + ZA -- log --> ZB["UI线程: 更新日志Listbox"]; + ZA -- progress --> ZC["UI线程: 更新进度条"]; + ZA -- finish --> ZD["UI线程: 显示'全部完成'对话框"]; +``` \ No newline at end of file