Skip to content

Latest commit

 

History

42 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

作文批改台图标

作文批改台 EssayGrader

让一整班纸质英语作文,从手机镜头走到可复核的批改档案。

面向中国高中英语教师的本地优先桌面应用:手机连续收卷,大模型自动转录、识名、评分,老师在电脑上复核与归档。

持续集成 版本 0.1.0 Tauri 2 Rust stable 支持 macOS、Windows 和 Linux AGPL-3.0-only

界面导览 · 使用指南 · 模型配置 · 收卷协议 · 数据边界 · 本地开发

作文批改台的班级与分组页面

班级、任务和学生档案按教学班归档;没有名单也可以直接从卷面姓名开始。

Important

当前为 0.1.0 首个公开版本。核心桌面应用已经完成真实上传、视觉模型批改和本地归档链路验证;AI 评分仍须由老师复核,重要数据请保留独立备份。

项目定位

作文批改台处理的是一个很具体的课堂工作流:

  1. 老师建立班级并设置本次作文;
  2. 手机固定在卷面上方,连续拍摄整班作文;
  3. 电脑本地接收图片并调用老师自己的多模态模型;
  4. 模型返回姓名、转录、评分和反馈;
  5. 老师确认身份与结果,随后归档、统计或导出。

它不是云端教务 SaaS,也没有项目方托管后台。项目不要求学生注册账号,不上传老师的班级数据库,不通过“模型自报置信度”掩盖概率模型的不确定性,也不把 AI 结果当成无需复核的最终判定。

设计选择 当前做法
名单 可选;无名单时可从卷面姓名建档
模型 老师自行提供 OpenAI Chat Completions 兼容端点
图片与成绩 保存在老师电脑
手机收卷 四小时临时 HTTPS 入口
身份不确定 进入人工确认,不编造置信度
统计 只使用真实正式结果,不填充示例数字
账号与遥测 不提供,也不需要

一条完整的批改路径

flowchart LR
    A["建立班级<br/>名单可选"] --> B["设置任务<br/>15 / 25 分模板"]
    B --> C["手机连续拍摄<br/>一份一提交"]
    C --> D["本机接收<br/>校验并续传"]
    D --> E["视觉模型<br/>转录 · 识名 · 评分"]
    E --> F["老师复核<br/>身份与结果"]
    F --> G["学生档案<br/>班级分析 · CSV"]

    classDef paper fill:#fff8d6,stroke:#2a2a28,stroke-width:2px,color:#2a2a28;
    classDef local fill:#eaf6ed,stroke:#2f7d4a,stroke-width:2px,color:#214e31;
    class A,B,C paper;
    class D,E,F,G local;
Loading

名单不是开批门槛。卷面姓名清楚时,模型可以直接建立学生档案;导入 Excel / CSV 后,则能进一步处理同名、学号和按名单顺序收卷。

界面导览

以下图片全部来自当前 Tauri 应用实机运行,不是设计稿或静态原型。

作文任务设置页面 手机上传连接页面
任务设置:题型模板、题目材料、评分约定和收卷入口在同一条路径上。 手机上传:选择正在收卷的任务,生成或暂停四小时临时入口。
班级批改总览页面 学生作文队列页面
班级总览:区分已收、批改完成、待确认和正式档案,不用半成品成绩污染统计。 作文队列:上传、失败、完成状态可见,并提供身份调整、重试、撤回和详情操作。

姓名档案策略和本机服务设置

本机设置:姓名归档策略、模型并发、上传端口、更新与数据清理集中管理。

已实现的能力

收卷与班级

  • 管理教学班和临时分组,支持 Excel / CSV 名单预览、列映射与导入。
  • 提供读后续写 25 分、应用文 15 分两套任务模板;作文题目和附加要求均可留空。
  • 手机端支持连续拍摄、单卷多页、调序、重拍、撤回刚才一份和离线队列。
  • 快门点击时同步定格当前视频帧,JPEG 编码不会继续读取已经移动的相机画面。
  • 图片自动纠正方向、移除 EXIF、压缩为最长边 3000px、质量 0.9 的 JPEG。
  • 采用 1 MiB 分片、SHA-256 校验和服务端偏移续传,单份作文页面最多三路并发。

模型批改

  • 对接 OpenAI Chat Completions 兼容端点,可分别配置主模型、副模型和备用视觉模型。
  • 真实检查端点连通性与图片输入能力,而不是只验证配置格式。
  • 结构化输出覆盖姓名、学号、转录文本、总分、分项评分、证据、反馈和订正建议。
  • 批改队列支持并发、失败重试和应用重启恢复;不会向模型发送人为设定的输出 token 上限。
  • 无名单时可按卷面姓名自动建档;无法可靠匹配时留给老师确认。

复核与结果

  • 老师可以修订批改结果、确认学生身份、重试失败任务或撤回错误作文。
  • 按学生保留历次正式结果;有两次以上结果后才显示成绩趋势。
  • 提供班级总览、匿名班级分析、匿名班际比较以及 CSV 成绩导出。
  • 学生详情包含原图、转录、能力分项、具体修改和可复制的批改包。
  • 清理任务或班级前先计算影响范围,并通过暂存机制同步处理数据库记录与本地图片。

教师使用指南

安装包由 GitHub Releases 提供,请按电脑系统与架构下载。当前没有付费 Apple Developer ID 或 Windows Authenticode 证书,因此 macOS 和 Windows 首次打开时可能显示来源提示;请只使用本仓库发布页的附件,并可用同页 SHA256SUMS.txt 核对文件完整性。

1. 配置模型

进入“API / 档案”,至少配置一个支持图片输入的主模型:

  • Base URL 填到 OpenAI 兼容 API 根路径,例如 https://example.com/v1
  • 模型 ID 使用服务商要求的完整名称;
  • API Key 保存后只进入操作系统凭据库,界面不会再次回显;
  • 点击“保存并测试”,应用会发送一张可解码的小型 PNG 验证真实视觉能力。

如果主模型只能处理文本,可以启用备用 VLM 先完成图片转录,再交给主模型评分。副模型用于主链路失败后的自动接管。

2. 建立班级

班级名称是唯一必填项。名单有三种使用方式:

  • 卷面自动识名:不导入名单,模型识别姓名后自动建档;
  • Excel / CSV 名单:映射姓名、学号和备注列,提高同名匹配稳定性;
  • 机序对应名单:卷面不写姓名时,按上传顺序对应名单顺序。

3. 创建任务

选择读后续写或应用文模板,填写任务名称。题目原文、题目截图、任务系列、关注点和模型备注均为可选项。保存草稿后可以稍后继续;点击“开始收取作文”才会进入收卷状态。

4. 连接手机

进入“手机上传”,为当前任务生成二维码。应用会:

  1. 确认本地 Axum 服务正在监听;
  2. 启动内置 cloudflared
  3. 等待 Quick Tunnel 确实能够访问本机服务;
  4. 生成含随机令牌的四小时 HTTPS 地址。

5. 连续收卷

手机浏览器可以选择单份多页或单页连拍。连拍模式保持摄像头常开,点击快门立即定格当前帧,然后后台压缩和上传;老师可以继续对准下一份作文。误拍时可以在手机或电脑撤回。

6. 复核与归档

“作文批改”会展示每份作文的上传、排队、批改、失败与身份状态。只有批改完成且身份确认后的结果才进入正式统计。老师可以打开详情修订结果,旧版本仍留在本地。

7. 完成收卷

“暂停手机入口”只关闭二维码和隧道,不结束任务;“完成收卷”才会将任务设为已结束。结束后的任务可以继续查看和导出,但不能重新上传。

模型与结构化输出

端点角色

角色 是否必需 输入能力 调用时机
主模型 必需 图片 + 文本,或配合备用 VLM 使用纯文本 正常批改主链路
副模型 可选 与主模型任务相同 主模型失败后自动接管
备用 VLM 可选 图片 + 文本 只负责转录,再交给文本评分模型

端点需要兼容 POST /chat/completions,响应正文至少包含 choices[0].message.content。批改请求使用低温度和 JSON Object 响应格式;鉴权失败不会重试,限流、服务端错误和网络错误最多重试三次。

结果结构

模型返回结果会经过 Rust 结构反序列化和业务校验。下面是经过缩减的 schemaVersion=1.0 示例:

{
  "schemaVersion": "1.0",
  "identity": {
    "studentName": "陈一诺",
    "studentId": "20240101"
  },
  "transcription": {
    "fullText": "The old man looked out of the window.",
    "pages": ["The old man looked out of the window."],
    "uncertainSpans": []
  },
  "grade": {
    "total": 22,
    "maxScore": 25,
    "level": "第二档偏上",
    "dimensions": [
      {
        "key": "language",
        "label": "语言",
        "score": 4,
        "maxScore": 5,
        "evidence": ["动作描写清楚"]
      }
    ]
  },
  "feedback": {
    "summary": "情节完整,结尾回扣不足。",
    "strengths": ["动作链条清楚"],
    "fixes": [
      {
        "location": "结尾段",
        "original": "He smiled.",
        "replacement": "He smiled at the warmth returning home.",
        "reason": "回扣开头意象",
        "category": "cohesion"
      }
    ],
    "modelText": "He turned back to the window.",
    "outline": "冲突—动作—回扣"
  },
  "classTags": ["结尾回扣"]
}

以下结果会被拒绝而不是直接入档:

  • schemaVersion 不是 1.0
  • 模型返回满分与任务满分不一致;
  • 总分或分项分数越界;
  • 没有作文转录正文;
  • 没有任何可执行修改建议;
  • 返回内容不是有效 JSON。

手机收卷协议

sequenceDiagram
    participant P as 手机浏览器
    participant C as Quick Tunnel
    participant A as Axum 本地服务
    participant S as SQLite / 本地文件
    participant W as 批改队列

    P->>C: GET /u/{临时令牌}
    C->>A: 获取零安装收卷页
    P->>A: 创建一份作文
    loop 每一页
        P->>A: 声明页码、大小与 SHA-256
        P->>A: PUT 1 MiB 分片
        P->>A: 查询服务端已确认偏移
        A->>S: .part 临时文件
    end
    P->>A: 完成这份作文
    A->>S: 校验并原子移动到 images/
    A->>W: 加入批改队列
Loading
约束 当前值
会话有效期 4 小时
单份作文页数 1–8 页
单页处理后大小 最大 12 MiB
单会话总流量 最大 2 GiB
上传分片 1 MiB
单份页面并发 最多 3
请求限流 每个令牌每分钟 600 次
完整性 每页 SHA-256
续传依据 服务端已确认字节偏移

手机端会把待传作文保存在 IndexedDB 队列中;页面刷新或短暂断网后可以继续处理。完成前的页面存放在 uploads/*.part,只有大小与哈希都正确才会原子移动到正式图片目录。

本地优先与数据边界

flowchart TB
    Phone["老师手机<br/>浏览器相机"]
    Tunnel["Cloudflare Quick Tunnel<br/>临时 HTTPS 传输"]
    Model["老师配置的模型端点<br/>OpenAI 兼容 API"]

    subgraph PC["老师电脑 · 作文批改台"]
        UI["Tauri / React 界面"]
        Upload["Axum 本地上传服务<br/>127.0.0.1:8787"]
        Worker["Rust 批改队列"]
        Store[("SQLite + 本地图片")]
        Vault["系统凭据库<br/>API Key"]

        UI <--> Store
        Upload --> Store
        Store <--> Worker
        Vault --> Worker
    end

    Phone -->|作文图片分片| Tunnel
    Tunnel --> Upload
    Worker -->|图片与任务要求| Model
    Model -->|结构化批改结果| Worker

    classDef outside fill:#fff3df,stroke:#bf6435,stroke-width:2px,color:#66351f;
    classDef inside fill:#edf7ef,stroke:#2f7d4a,stroke-width:2px,color:#214e31;
    class Phone,Tunnel,Model outside;
    class UI,Upload,Worker,Store,Vault inside;
Loading
数据 保存位置 / 传输边界
班级、名单、任务、成绩 老师电脑的 SQLite 数据库
原始作文图片 老师电脑的系统应用数据目录
API Key macOS Keychain、Windows Credential Manager 或 Linux Secret Service
手机上传流量 经 Cloudflare Quick Tunnel 转发到老师电脑
模型请求 只发送给老师自行配置的模型端点
班级与班际分析 只向模型发送匿名聚合统计
账号、遥测、云同步 不存在

应用数据目录内部结构如下:

app-data/
├── essay-grader.sqlite       # 班级、任务、作文状态与批改结果
├── images/                   # 已完成校验的题目和作文图片
├── uploads/                  # 尚未完成的 .part 分片文件
├── trash/                    # 数据清理事务的临时暂存区
└── cloudflared-empty.yml     # 禁止读取用户全局 Tunnel 配置的空文件

Quick Tunnel 是匿名临时服务,没有可用性承诺。其官方限制包括最多 200 个并发请求且不支持 SSE;本项目不使用 SSE,并将页面上传并发固定为最多三路。数据库只保存上传令牌的 SHA-256 哈希。

技术架构

主要技术 负责内容
桌面外壳 Tauri 2 原生窗口、IPC、打包、自动更新
前端 React 19、TypeScript、Vite 教师工作台、任务与结果复核
本地核心 Rust、Tokio、SQLx、SQLite 数据、队列、模型调用、报告
手机入口 Axum、原生 Web API 相机连拍、分片上传、断点续传
临时通道 cloudflared Quick Tunnel 手机到教师电脑的 HTTPS 入口
可视化 ECharts 成绩分布、趋势和班级比较
EssayGrader/
├── src/
│   ├── pages/                # 班级、任务、上传、成绩、对比与设置
│   ├── components/           # 固定应用壳、对话框、提示与空状态
│   └── lib/                  # Tauri IPC 类型与调用封装
├── src-tauri/
│   ├── src/
│   │   ├── db.rs             # SQLite 读写与业务约束
│   │   ├── upload.rs         # 本地服务、Quick Tunnel 与分片协议
│   │   ├── grading.rs        # 批改队列、转录、识名与保存
│   │   ├── model_gateway.rs  # OpenAI 兼容请求、重试和错误映射
│   │   ├── cleanup.rs        # 可回滚的数据与图片清理
│   │   └── reports.rs        # 匿名班级和班际分析
│   ├── mobile/uploader.html  # 零安装手机收卷页
│   └── migrations/           # SQLite 迁移
├── scripts/                  # cloudflared 下载、校验与冒烟测试
└── .github/workflows/        # CI 与六平台发布矩阵

应用启动时会依次打开数据库、执行迁移、启动仅监听回环地址的手机服务,再启动后台批改 worker。退出时主动停止当前 Quick Tunnel。上次退出时处于 processing 的作文,会在下次启动时恢复为 queued

平台与发布状态

平台 架构 发布目标 当前验证状态
macOS Apple Silicon .app / .dmg 本地构建和启动已验证
macOS Intel .app / .dmg Release 工作流已配置
Windows x64 NSIS Release 工作流已配置
Windows ARM64 NSIS Release 工作流已配置;cloudflared 从固定源码构建
Linux x64 AppImage / .deb Release 工作流已配置
Linux ARM64 AppImage / .deb Release 工作流已配置

当前不包含付费 Apple Developer ID 或 Windows Authenticode 证书。macOS 本地包使用 ad-hoc 签名,Windows 首次运行可能显示来源警告。自动更新包使用独立的 Tauri updater 签名密钥。

推送 v* 标签或手动触发发布工作流后,会建立草稿 Release、上传安装包和更新包,并生成 SHA256SUMS.txt;全部构建通过后再公开发布。

本地开发

环境要求

  • Node.js 24
  • pnpm 10.27
  • Rust stable,包含 rustfmtclippy
  • Tauri 对应平台的系统依赖

启动开发版

pnpm install
pnpm prepare:sidecar
pnpm tauri dev

prepare:sidecar 会从 Cloudflare 官方 2026.7.2 Release 下载当前目标的 cloudflared,核对官方 SHA-256 后写入 Git 忽略目录。Windows ARM64 没有对应官方资产,发布工作流会从同一标签的固定提交构建。

只构建桌面二进制、不生成安装包:

pnpm tauri build --no-bundle

完整 Release 构建需要 TAURI_SIGNING_PRIVATE_KEY;本机没有更新签名私钥时,即使 .app 已生成,创建 updater artifact 的最后一步仍会报错。

Note

Tauri、WebKit、SQLite、TLS 和网络栈会让第一次 Rust 全量编译较慢,并产生数 GB 的 src-tauri/target 缓存。后续增量编译会复用它。需要释放空间时,可以运行官方命令:

cargo clean --manifest-path src-tauri/Cargo.toml

检查与测试

# 前端静态检查、组件与功能测试
pnpm lint
pnpm test
pnpm build

# Rust 格式、静态检查和测试
cargo fmt --manifest-path src-tauri/Cargo.toml -- --check
cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path src-tauri/Cargo.toml

# 内置 cloudflared 是否能建立 Quick Tunnel
pnpm smoke:sidecar

当前自动测试重点覆盖:

  • 班级创建、任务生命周期和页面跳转;
  • 端点保存、视觉探针与密钥不回显;
  • 模型错误分类、重试和 JSON 结构校验;
  • 无名单自动建档、跨班同名与人工确认;
  • 上传分片、偏移续传、限流、会话隔离与撤回;
  • 应用重启后的批改队列恢复;
  • 清理操作失败时恢复暂存图片;
  • 机序调整后学生与作文顺序同步。

人工验收素材

仓库提供两张完全虚构的高考英语答题卡风格图片,可以直接用于手机相册上传或本地链路测试:

素材说明和预期身份见 英语作文测试素材。两张卷面均包含少量语言问题和手写划改,不是标准答案,也不包含真实学生信息。

CI 在每次推送到 main 和每个 Pull Request 上执行完整前后端检查。浏览器端到端测试没有塞进 CI;真实手机、真实 Quick Tunnel 和真实模型链路仍应在发布前进行人工验收。

常见问题

为什么第一次编译很慢,而且 target 有好几 GB?

Rust 会保存依赖、调试信息和增量编译中间产物。Tauri 再叠加 WebKit、SQLite、TLS、Axum 与多平台库格式后,debug 缓存达到数 GB 并不罕见。保留缓存会明显加快后续编译;阶段性结束或磁盘紧张时运行 cargo clean 即可。

macOS 为什么询问是否允许读取 Keychain?

API Key 使用系统 Keychain,不写入应用目录。开发阶段的 ad-hoc 签名和二进制频繁变化,可能让 macOS 再次询问。选择“始终允许”可减少同一构建的重复提示;正式分发仍建议使用稳定的 Developer ID 签名。

二维码生成失败或遇到 Cloudflare 1033 怎么办?

确认网络可以访问 Cloudflare,等待数秒后重新生成。应用会在拿到 trycloudflare.com 地址后继续探测,直到公网请求确实抵达本机服务才展示二维码。如果本地服务本身启动失败,请检查设置中的端口是否被占用。

暂停手机入口以后,为什么任务仍显示“收卷中”?

暂停只关闭二维码和 Quick Tunnel,方便中途停止外部访问;它不会改变任务生命周期。全部作文收完后,需要点击“完成收卷”正式结束任务。

换电脑复制应用,会把 API Key 一起带过去吗?

不会。应用包不包含 API Key,复制 .app 或安装包也不会复制操作系统凭据库。班级数据库和图片同样位于系统应用数据目录,而不是应用二进制内部。

AI 识别的姓名和分数可以完全相信吗?

不应该。模型输出是概率结果,应用会校验结构、分数范围和必填内容,但无法证明语义一定正确。身份无法稳定匹配时会进入待确认;正式交付学生前仍应由老师复核原图、转录和反馈。

当前限制与发布前清单

  • macOS ARM64 本地应用构建、签名检查和启动
  • 前后端 CI 与多架构 Release 工作流
  • 手机连续拍摄、分片续传和双端撤回
  • 结构化批改、人工身份确认和本地归档
  • 使用完整测试班级跑通真实手机 → Tunnel → 模型 → 复核 → 导出
  • 在 Windows 和 Linux 真机验证安装、凭据库和系统 WebView
  • 配置正式 updater 私钥和发布 Secrets
  • 如面向普通教师分发,补充 Apple / Windows 商业代码签名
  • 发布首个带 SHA-256 清单的公开版本

参与项目

问题和改进建议可以提交到 Issues。涉及收卷、数据清理或模型输出的改动,请同时补充相应测试,并在提交前运行完整检查。

提交信息使用 Conventional Commits,例如:

feat(手机上传): 支持单页连续拍摄
fix(任务): 统一暂停入口与完成收卷状态
docs(readme): 补充模型协议与平台说明

许可证与第三方组件

项目使用 AGPL-3.0-only

内置 cloudflared 由 Cloudflare 提供,相关许可证与分发说明见 THIRD-PARTY-NOTICES.md。Tauri、React、Rust crates 与其他前端依赖各自遵循其上游许可证。

About

面向中国高中英语教师的本地优先大模型作文批改工作台

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages