Lexiverse
Lexiverse - AI contextual reading engine for English learners. Your next article is your next review.
AI-assisted grading for Jupyter notebooks and other student work.
EducationTypeScript · CSSGitHub @subwaymatchOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗AI-assisted grading for Jupyter notebooks and other student work, running entirely on Cloudflare.
Teachers set up an assignment with instructions, a rubric and an optional answer key, drop in the students' files (or a .zip), and AI grades each file in the background. Graders review the results next to the notebook, change any score, and send each student a private link. Feedback can be downloaded as the original notebook with the comments inserted as cells.
.zip (unpacked in the browser). Exact duplicates are detected by SHA-256 before upload. Near-identical submissions (MinHash over the code) are flagged for review.> **Feedback:** Markdown cell after each commented cell, keeping every original cell, output and piece of metadata. Feedback cells are tagged (jupygrader-feedback), so grading a notebook that already has feedback replaces it instead of stacking it. Code files get feedback as comments in the file's own comment syntax; Markdown and text files get it after each paragraph. HTML, plain-text and JSON exports are available too, for one file or a .zip of many, plus a grades CSV..zip files, then post grades back, each with an optional comment (score, summary and rubric) and the file with feedback attached. Scores are scaled to the Canvas assignment's points. Each person connects their own Canvas account with an access token, so Canvas's permissions apply. See Canvas./ introduces Jupygrader to instructors, graders and students and links to sign-in and account requests. The signed-in app lives under /app; old /courses, /admin, /account, /help and /submissions links redirect there. Student links (/r/...) and invitations (/invite/...) keep their addresses.| Part | Choice |
|---|---|
| Framework | vinext (Next.js App Router on Vite) on Cloudflare Workers |
| UI | React 19, Base UI components, plain CSS (no Tailwind) |
| Auth | better-auth with D1: email and password, magic links, password reset |
| Data | Cloudflare D1 (SQLite) |
| Files | Cloudflare R2: submissions, instruction and rubric documents, answer keys |
| Jobs | Cloudflare Queues for grading, Cron Triggers for recovery and retention |
| AI | An AI API called with structured outputs (a JSON schema), so every reply can be validated |
| Resend, optional | |
| Notebook HTML | nbconvert in a Cloudflare Container, optional |
Browser ──> Worker (worker/index.ts)
├─ fetch: vinext pages, server components and /api routes
│ ├─ D1 (users, courses, assignments, submissions, activity, usage)
│ ├─ R2 (files)
│ ├─ Queue producer (one message per file to grade)
│ └─ Durable Object + Container (nbconvert, for notebook HTML downloads)
├─ queue: grading consumer ──> AI API
└─ scheduled: every minute, requeue interrupted jobs (or grade directly
when no queue is bound); daily, delete files past retention
Requirements: Node 22+.
npm install
cp .dev.vars.example .dev.vars # set BETTER_AUTH_SECRET to a long random string
npm run db:migrate:local # creates the local D1 database
npm run dev # http://localhost:5173
Open the site: the public home page links to sign-in. On a new site, choose Set up Jupygrader to create the first account, which becomes the administrator. Then open Admin > Settings, fill in the AI connection (API address and key), and click Test saved settings.
Local D1, R2 and the queue are simulated by Miniflare and stored in .wrangler/state. Cron handlers can be triggered with:
curl "http://localhost:5173/cdn-cgi/handler/scheduled?cron=*+*+*+*+*" # recovery tick
curl "http://localhost:5173/cdn-cgi/handler/scheduled?cron=17+3+*+*+*" # retention sweep
Without RESEND_API_KEY, emails (invitations, sign-in links, password resets) are printed to the terminal. Invite links can also be copied from the course member list or Admin > Invitations.
You need a Cloudflare account. If Queues aren't available on your plan, see without a queue.
Sign in and create the resources:
npx wrangler login
npx wrangler d1 create jupygrader
npx wrangler r2 bucket create jupygrader-files
npx wrangler queues create jupygrader-grading
In wrangler.jsonc, paste the database_id printed by d1 create, and set vars.APP_URL to the URL the app will be served from (for example https://grader.example.edu or https://jupygrader-cloud.<you>.workers.dev). It's used for links in emails.
Set the secrets:
npx wrangler secret put BETTER_AUTH_SECRET # required: a long random string, e.g. openssl rand -base64 32
npx wrangler secret put RESEND_API_KEY # optional: to send email
npx wrangler secret put EMAIL_FROM # optional: e.g. "Jupygrader <[email protected]>" on a domain verified in Resend
npx wrangler secret put AI_API_KEY # optional: a default key; admins can also set one in Settings
Create the tables and deploy:
npm run db:migrate:remote
npm run deploy # builds with Vite, then deploys with Wrangler
Open the app, create the first (admin) account, and configure AI grading in Admin > Settings.
When you update a deployment, run npm run db:migrate:remote before npm run deploy if there are new files in migrations/.
To serve the app on your own domain, add a custom domain to the Worker and update APP_URL.
With Workers Builds connected to the repository and Builds for Preview branches turned on (Settings > Builds > Previews Base, Preview command npx wrangler preview), every push to a branch other than main deploys a Worker Preview and posts its URL on the pull request.
On a new pull request, Cloudflare's comment can arrive 15 minutes or more after the pull request opens, because the branch was pushed before the pull request existed. The Preview URL workflow (.github/workflows/preview-url.yml) comments the URL as soon as the Preview is deployed, and deletes its comment when Cloudflare's arrives. It needs two repository secrets: CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_PREVIEWS_API_TOKEN, an account API token with the Workers Scripts Read permission. Its clean-up job only runs once the workflow is on main.
Previews use the staging resources in the previews block of wrangler.jsonc (the jupygrader-preview D1 database and the jupygrader-preview-files R2 bucket), never production data. To set up your own:
Create the staging database and bucket, and put their names and the database ID in the previews block and in wrangler.preview-migrations.jsonc:
npx wrangler d1 create jupygrader-preview
npx wrangler r2 bucket create jupygrader-preview-files
Apply the migrations to the staging database, and again whenever there are new files in migrations/:
npm run db:migrate:preview
Under Settings > Builds > Previews Base in the dashboard, add a BETTER_AUTH_SECRET secret for previews (any long random string, different from production), and RESEND_API_KEY if previews should send email.
What differs in a preview:
APP_URL is not set, so links in emails use the preview's own address.The queue is optional. Remove the queues block from wrangler.jsonc and the every-minute Cron Trigger grades queued files instead, up to the "Files graded at once" setting per minute. Grading is slower to start but nothing else changes.
Graded notebooks are rendered as HTML by Jupyter's own nbconvert (the JupyterLab template) running in a Cloudflare Container. When a notebook finishes grading, the graded notebook, with its feedback cells, is sent to the container and the HTML is stored in R2 next to the file. The feedback page and students' links show it, and "Notebook as HTML" downloads, for graders and through student links, use it without waiting for the container. A stored copy is used only while the grade, feedback, assignment title and points it was made from are unchanged; otherwise it's made again in the background when the feedback page opens, or when it's downloaded. Markdown and outputs are sanitized: scripts, frames, event handlers and javascript: links from student work are removed, and both pages show the HTML in a frame that runs no scripts. If the container can't be reached, downloads use the built-in HTML and the pages show their own rendering instead. Bulk .zip downloads always use the built-in HTML: nbconvert's is about 300 KB per file, mostly styles, and a .zip of hundreds of them wouldn't fit in a Worker's memory.
Plan: Containers need the Workers Paid plan ($5 per month).
Scheduling policy: the container uses the durable_object scheduling policy (accounts can be limited to it), so worker/nbconvert.ts chooses the image and the instance size when it starts the container.
Deploying: the image is built ahead of time and pushed to your account's Cloudflare registry, and wrangler.jsonc pins it by digest (registry.cloudflare.com/<account-id>/jupygrader-nbconvert@sha256:<digest>). Deploys, including Workers Builds, only reference it, so they don't need Docker. To deploy without the container, remove the containers block from wrangler.jsonc.
Changing the image: edit containers/nbconvert, choose a new tag (for example 7.17.2-3), and push the image under that tag before you merge, in one of two ways:
npx wrangler login: npx wrangler containers build containers/nbconvert -t jupygrader-nbconvert:<tag> -p, then docker image inspect registry.cloudflare.com/<account-id>/jupygrader-nbconvert:<tag> --format '{{json .RepoDigests}}' and use the entry that starts with registry.cloudflare.com/.CLOUDFLARE_ACCOUNT_ID, and CLOUDFLARE_API_TOKEN with the Workers Containers Edit permission.Then put the new reference in wrangler.jsonc and merge. If the image isn't pushed first, the deploy's container step fails.
Local development: npm run dev doesn't start the container (dev.enable_containers is false), so downloads use the built-in HTML and the feedback page its own rendering. To run nbconvert locally, set it to true and replace the image's with ; this needs Docker, and WSL on Windows.
| Name | Kind | Purpose |
|---|---|---|
BETTER_AUTH_SECRET | secret, required | Signs sessions and encrypts the AI API key and Canvas access tokens stored in D1. Changing it signs everyone out and makes the stored API key and tokens unreadable (enter the key again in Settings; everyone reconnects Canvas). |
APP_URL | var | Public URL of the app. |
RESEND_API_KEY, EMAIL_FROM | secrets, optional | Send email through Resend. |
AI_BASE_URL, AI_API_KEY | optional | Defaults for the AI connection. Values saved in Admin > Settings take precedence. |
Everything else (registration, allowed email domains, concurrency, retention, reasoning effort, sending plots and interactive outputs to the AI, the Canvas address) is set by an admin in the app.
https://canvas.university.edu) in Admin > Settings > Canvas. Leaving it blank turns Canvas off.lastfirst_userid_file.ipynb) and keep the student's name. Running it again imports only new attempts.Imports and posting run a few students per request, driven by the page, so large classes stay within Worker limits. The code is in lib/server/canvas.ts (API client and tokens) and lib/server/canvas-sync.ts (import and posting).
[C1], [C2]…; code files are numbered lines (L12-18); documents are paragraphs ([P3]). Notebook plots and interactive outputs (widgets, Plotly, Bokeh, Vega) are replaced with a short note by default; admins can send all, some or none of the plots as images, and the text form of interactive outputs, for the whole site or per assignment. The file name is not sent. Everything before the file is the same for each file of an assignment, so it can be cached across a batch.The notebook logic lives in lib/shared/notebook.ts and lib/shared/document.ts; the prompt and validation in lib/server/grading.ts.
app/ routes (App Router)
(auth)/ sign in, register, password reset, invitations
(app)/ signed-in screens: courses, assignments, admin, account, help
r/[token]/ public student feedback page
api/ JSON routes used by the UI
components/ UI (Base UI wrappers in components/ui)
lib/shared/ code shared by browser and Worker: rubric, notebook, exports
lib/server/ Worker-only code: auth, access control, grading, storage
migrations/ D1 schema
worker/index.ts Worker entry: fetch (vinext), queue and scheduled handlers
worker/nbconvert.ts Durable Object that runs the nbconvert container
containers/nbconvert/ nbconvert container: Dockerfile and HTTP server
test/ unit tests (Vitest)
| Command | |
|---|---|
npm run dev | Dev server with local D1, R2 and queue |
npm run build | Production build into dist/ |
npm start | Run the production build locally with Wrangler |
npm run deploy | Build and deploy to Cloudflare |
npm run db:migrate:local / db:migrate:remote | Apply D1 migrations |
npm run db:migrate:preview | Apply D1 migrations to the staging database used by pull request previews |
npm test | Unit tests |
npm run typecheck | Generate binding types and type-check |
The bird illustrations in public/home and the mascot poses in public/mascot were generated with OpenAI's GPT Image 2.5 at quality: "low" on a transparent background, some from reference drawings through the image edit endpoint, and saved as WebP. They stay as they are and do not need regenerating.
Any new pose or illustration is generated with GPT Image 2.6 (gpt-image-2.6-sunburst or gpt-image-2.6-flare, one release at one price) at quality: "low" with background: "transparent", on the generations or the edit endpoint, and saved the same way. Three things to know about 2.6:
model_not_found), so send each request synchronously.low it is 664 image output tokens at 1536x1024 or 1024x1536 and 824 at 1024x1024, about $0.02-0.025 an image at $30 per 1M (2.5 used 158 and 196). An edit also pays for the reference as image input.2.6 can also return an animated GIF (output_format: "gif", with frame_count and frame_duration_ms). Every bird on the home page moves this way: public/home/animated holds a looping animated WebP for each one, shown through components/home/AnimatedArt.tsx, and visitors who prefer reduced motion see the still instead. Each was made by sending its still (from public/mascot or public/home) to the edit endpoint with a short description of the motion, using gpt-image-2.6-sunburst, quality: "low", background: "transparent", frame_count: 8 and complexity: "low". The frame size matched the still's aspect ratio in multiples of 16: 512x512 for the mascot poses, up to 960x640 for celebrate. Then:
960x640 was 1,395. With the reference image as input, each cost $0.02 to $0.06, and the twelve about $0.32. The WebPs come to about 1.4 MB, and all but the hero's two load lazily.The design prototype also covers an admin "view as" mode, which isn't implemented here.
"image""dockerfile": "./containers/nbconvert/Dockerfile"Size: the container runs as a standard-1 instance (1/2 vCPU, 4 GiB memory, 8 GB disk). It starts on the first conversion and stops after 5 idle minutes. Measured at that size, starting takes about 4 seconds (plus a few seconds for Cloudflare to start the VM), the first conversion about 1.5 seconds and later ones under half a second. The smaller lite size (1/16 vCPU) takes about 45 seconds for the first download, and the basic size isn't available with this policy.
Cost: memory and disk are billed while the container runs and CPU only while it works. It runs while notebooks are being graded, and for 5 idle minutes after the last one, so a grading batch counts as one session. A session (a start, some conversions, then 5 idle minutes) uses about 0.35 GiB-hours of memory and 0.7 GB-hours of disk. The Workers Paid plan includes 25 GiB-hours and 200 GB-hours a month, so about 70 sessions a month cost nothing extra. Each further session costs about $0.003: 1,000 sessions a month is about $3, and a container that never went idle would be about $27 a month.