ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A study environment for the AP Latin exam under the 2025–26 framework — Vergil's Aeneid and
Pliny the Younger's Letters. Built for one student, one exam: Friday, 14 May 2027.
Deployed at lectio.norvodesigns.com.
There is also a native iPhone and iPad app (SwiftUI, Liquid Glass), built from the same content
and syncing the same progress. See ios/README.md. If you edit anything
under src/data, run npm run export:content so the app's copy of the course stays current (CI
checks).README 전문 보기 ↗A study environment for the AP Latin exam under the 2025–26 framework — Vergil's Aeneid and Pliny the Younger's Letters. Built for one student, one exam: Friday, 14 May 2027.
Deployed at lectio.norvodesigns.com.
Next.js (App Router) · TypeScript · Tailwind v4 · Zustand · Vercel AI SDK.
There is also a native iPhone and iPad app (SwiftUI, Liquid Glass), built from the same content
and syncing the same progress. See ios/README.md. An Android app (Kotlin,
Jetpack Compose, with a Wear OS watch app) does the same on Google Play. See
android/README.md. If you edit anything under src/data, run
npm run export:content so the apps' copy of the course stays current (CI checks).
| Section | What it does |
|---|---|
| Dashboard | Countdown, mastery by unit and skill category, recent scores, what to study next, streak |
| Reading Room | Every syllabus passage with click-any-word glossary, notes, bookmarks, line flagging, cold-read toggle |
| Translate | Literal-translation drills in the exam's own 15-segment shape, with self- and AI-grading |
| Sight Reading | Timed unseen passages from the CED's recommended authors, plus an AI generator |
| Quiz Engine | Configurable MCQ sets filtered by author, passage, unit, skill or question type; review queue |
| Vocabulary | SM-2 spaced repetition over all 990 words of the official required list |
| Grammar & Syntax | The constructions AP tests, with examples from the real passages |
| Scansion Lab | Interactive dactylic hexameter with a rules tutorial |
| Literary Devices | Reference cards plus a spot-the-device drill |
| Context & Culture | Vergil, Augustan Rome, Pliny's world, Vesuvius, provincial administration |
| FRQ Workshop | All five free-response types, timed, with official rubrics and a Course Project mode |
| Practice Exam | 52 MCQ / 65 min, then 5 FRQ / 115 min, with a scored breakdown |
| Study Plan | Phases measured backwards from exam day |
| Settings | Theme, data export/import, AI usage meter |
⌘K / Ctrl+K opens the command palette. g then a letter jumps to a section (g r for Reading
Room, g v for Vocabulary, and so on — each section's letter is shown on hover in the sidebar).
In the Reading Room: c cold read, n notes, s summary, b bookmark. In the Quiz Engine,
1–4 answer and ↵ advances. In Vocabulary, space reveals and 1–4 grade.
npm install
cp .env.example .env.local # then paste your key in (see below)
npm run dev # http://localhost:3000
Other scripts:
npm run build # production build
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run verify # content integrity checks — see below
npm run export:content # re-export src/data for the iOS app (ios/Content)
npm run export:fixtures # regenerate the iOS app's sync/SM-2 parity fixtures
npm run verify is worth knowing about. It checks that every answer key resolves to a real option,
that every translation segment appears contiguously in its passage, that every grammar or device
example really occurs in the passage it cites, that scansion feet add up and reconstruct their line,
and that FRQ rubrics total the points the exam actually awards. It caught fifteen real content
errors the first time it ran. Run it after editing anything under src/data.
Then, for local development, paste it into .env.local:
GOOGLE_GENERATIVE_AI_API_KEY=your-key-here
GOOGLE_GENERATIVE_AI_API_KEY with your key as the value. Tick Production, Preview
and Development.Never prefix these with
NEXT_PUBLIC_. Anything with that prefix is inlined into the JavaScript bundle and served to every visitor. These keys are read only inside route handlers undersrc/app/api/, which run on the server. No client component references them, andsrc/lib/ai/provider.tsimportsserver-onlyso a stray client import fails the build rather than leaking the key.
Google's free tier for the Gemini API is free because your prompts and responses may be used to improve their models. Paid tiers are not. Everything this app sends is Latin passages and your own translations and essays, so there is nothing sensitive in it — but it is worth knowing, and it is why nothing in this app sends anything you have not explicitly asked it to grade.
Free-tier model availability also changes without much notice: models get renamed, deprecated, or have their quotas moved. That is exactly why the provider is behind an abstraction.
Everything goes through src/lib/ai/provider.ts. To change the primary provider, edit one line:
const PRIMARY: ProviderName = 'google'; // change to 'groq'
Groq is already wired up as an automatic fallback. When Gemini returns a 429 (rate limit or quota exhausted) or a 404 (model retired or renamed), the request is retried on Groq transparently. Get a key at console.groq.com/keys and set:
GROQ_API_KEY=your-key-here
The fallback is optional. With no Groq key the app just surfaces a clear rate-limit message instead.
To change models without touching code:
GEMINI_MODEL=gemini-2.0-flash
GROQ_MODEL=llama-3.3-70b-versatile
To add a third provider, install its AI SDK package, add a branch to model() and a name to
ProviderName. Nothing outside that file needs to change — the route handlers only ever see a
LanguageModel.
The app is fully usable with no API key at all. Every AI surface has a self-grading path that does the same job:
/api/ai/status reports whether any provider is configured, and the UI checks it before offering an
AI button, so you never get a button that fails.
The deployed URL is public, so every AI route:
In-memory limiting is per serverless instance, so it is a brake on runaway usage rather than a security boundary. For a single-user study app that is the right size of solution.
The repo is set up so that pushes to main deploy automatically.
If you are wiring it up from scratch:
GOOGLE_GENERATIVE_AI_API_KEY (and optionally GROQ_API_KEY) under Environment Variables
before the first deploy, or redeploy after adding them.main redeploys; pull requests get preview URLs.lectio.norvodesigns.com at itThe subdomain is claimed on Vercel's side first, then proved by one DNS record you add wherever
norvodesigns.com's nameservers actually point.
Vercel project → Settings → Domains → Add, and enter lectio.norvodesigns.com.
Vercel replies with the record it wants. For a subdomain that is a CNAME:
| Type | Name | Value |
|---|---|---|
CNAME | lectio | cname.vercel-dns.com |
The Name is just lectio, not the whole hostname — most DNS panels append the apex domain
for you. If yours wants it spelled out, use lectio.norvodesigns.com. Copy the value Vercel
shows rather than the one above if the two ever differ; Vercel is the authority on its own
record.
Add that record in the DNS panel for norvodesigns.com. On Cloudflare, set it to DNS only —
the orange proxy cloud intercepts the certificate challenge and the domain never validates.
Back in Vercel, wait for the domain to turn Valid Configuration. TLS is issued automatically once it does. A minute or two is normal.
Check it from a terminal, not a browser — browsers cache DNS long past the TTL:
dig +short lectio.norvodesigns.com
Two settings to change once it resolves, both easy to forget:
lectio.norvodesigns.com as the primary domain, so
the .vercel.app URL redirects to it instead of serving the same site at two addresses.https://lectio.norvodesigns.com and add it under Redirect URLs. Miss this
and the confirmation link in every signup email still points at the old host, which is the kind
of thing you discover from a student rather than from a test.Everything above works with zero setup: Lectio runs in solo mode — no login, progress lives in the browser. Configuring Supabase turns on classroom mode: student and teacher accounts, join codes, assignments, and a leaderboard — without changing anything about solo mode for anyone who never signs in.
Create a project at the Supabase dashboard.
Project Settings → API and copy the Project URL and anon public key.
SQL Editor → New query, and run every file in supabase/migrations/ in filename order
(0001 through 0006 — each depends on tables or functions the one before it created).
Add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY to .env.local, and to Vercel
the same way as the AI keys above (Environment Variables, then redeploy). Both are safe to expose
to the browser — see the comment above them in .env.example for why.
Check Supabase's email confirmation setting matches what you want: Authentication → Providers → Email. On by default, meaning a new account cannot sign in until it clicks a confirmation email — worth turning off for a classroom where that friction buys nothing.
Authentication → Emails → Templates: point both account emails at the site's own
/auth/confirm, so the link works from any browser and lands on a page Lectio owns.
{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=recoverySignup then lands on "Email confirmed", and a reset on "Choose a new password" (/forgot-password
sends the email). Supabase's built-in email sender allows only a few emails an hour for the whole
project; for a real class, add your own SMTP under Authentication → Emails → SMTP Settings.
/teach) and gets a six-character join code — the
alphabet deliberately avoids vowels and 0/O/1/I/L, so a code read aloud off a whiteboard is never
ambiguous./classroom) to join.src/hooks/useCloudSync.ts. Signing in on a second device pulls that copy down and merges it
with whatever is already on the new device, rather than either one overwriting the other: quiz
attempts, highlights and vocabulary progress from both devices are combined (see
src/lib/mergeProgress.ts for exactly how each kind of data merges), so studying on your phone
during the day and your laptop in the evening never costs you either session's work.Nobody's raw activity reaches another student's browser. study_sessions and activity_stats rows
are protected by row-level security scoped to their own owner; the leaderboard and roster are
separate database functions (classroom_leaderboard, classroom_section_time) that return only
aggregates and a display name, and only after confirming the caller actually belongs to that
classroom. The policies themselves are commented in supabase/migrations/0001_init.sql and
0002_rpc.sql — the database enforces this, not application code, so a mistake in a query cannot
leak one classroom's data into another.
The cross-device sync row (user_progress, 0005_user_progress.sql) is stricter still: it is
owner-only, with no policy granting a teacher read access at all — unlike time and accuracy, a
student's highlights, notes and drafts are never a teacher's to see.
This matters more than anything else in a study app, so:
npm run eval:grading scores the grader against cases
whose outcome is known in advance. See below.AI-graded work counts towards your record, so "it seems to work" is not good enough. A grader that marks a correct translation wrong costs you real standing and teaches you to distrust a right answer; one that waves errors through teaches nothing. Those two failures are not equally bad, so the harness reports them separately.
npm run dev # in one terminal
npm run eval:grading # in another
npm run eval:grading -- --base http://localhost:3160 --runs 3
Full pass, all fifteen cases, Gemini serving every call:
| count | share | |
|---|---|---|
| agreed with the key | 184 / 185 segment judgements | 99.5% |
| false negatives — a correct segment marked wrong | 1 | 0.5% |
| false positives — an error waved through | 0 | 0.0% |
The single disagreement was not the grader's fault. It marked pacique imponere morem wrong in the
Aeneid 6.847–853 model translation, giving the reason "dative rendered as genitive" — and it was
right: the model answer read "the custom of peace" where paci is dative with the compound
impono, which is the exact pitfall that drill warns students about. Correcting the content and
re-running the perfect cases gives 75 / 75, no false negatives, the corrected line included.
So the grader currently disagrees with nothing it should agree with, and lets nothing through it
should catch — on five drills. That is a floor, not a guarantee: five drills is a small sample, the
free tiers can put a different model behind the same call tomorrow, and re-running after any change
to a model translation is cheap (-- --cases perfect).
Every case is built from the drill data itself, so the expected outcome does not depend on anyone's opinion:
| case | submission | what must happen |
|---|---|---|
perfect | the drill's own continuous model translation | every segment awarded — but see the warning below before calling a miss a false negative |
truncated | only the opening of that translation | the closing segments must fail, the opening ones must still pass |
empty | an irrelevant sentence | everything must fail |
A perfect miss is a question, not a verdict. The case assumes the model translation is
correct, and that assumption does not always hold. Every miss it has ever reported turned out to be
a defect in this repository's own content, not in the grader — four so far, across two drills:
pauca rendered as the adverb "briefly" where the segment requires a neuter plural object, pro
rendered as "about" as though it were de, an "of me" supplied with nothing in the Latin behind
it, and the dative paci rendered as a genitive. Each time the grader was right and the content
was wrong. Read the reason it gives before concluding anything about the grader; fixing the content
is usually the correct response.
Two things are worth knowing about this harness.
Its first version was unsound, and it mattered. It built "correct" submissions by concatenating
each segment's accepted literal. For these drills that produces text no honest grader should pass —
one literal is an ellipsis placeholder ("I … that you"), several carry editorial glosses in
parentheses, and two overlap. It reported a pile of false negatives that were really defects in the
script. If you extend the harness, derive cases from modelTranslation, which is what a student
actually submits: unlabelled English prose, not a list of segments.
It waits out the rate limiter rather than bypassing it. The grading route allows 20 calls per 10 minutes per IP. Adding a test-only bypass would put an authentication surface into a production route for the sake of a script, so the eval simply sleeps when it is told to. A full run therefore takes upwards of ten minutes, and the limiter gets exercised too.
Both providers are exercised through the normal fallback path, so check the [provider] tag in the
output. If the primary is throttled, every row silently reads as the fallback and you are measuring
a model you did not think you were measuring.
Expect that to happen, and expect the free tiers to be the limiting factor throughout:
| provider | free-tier ceiling | grading calls it allows |
|---|---|---|
| Gemini | 20 requests/minute | a run saturates it in seconds, since every call tries the primary first and the SDK retries |
| Groq | 8,000 tokens/minute | roughly two, because one grading call costs a few thousand tokens |
Groq's is the one that catches people out: the binding constraint is tokens, not requests, so pacing
by request count does not help. Retrying hard on either makes things worse — it pushes the fallback
into its own cap and the run stops measuring anything at all. The harness therefore paces itself
(--pacing, default 90s between requests — the first value measured to survive a whole pass) and distinguishes the two kinds of 429: the route's own
limiter reports retryAfterSeconds and is honoured exactly, while a provider 429 gets a flat
minute. A full run consequently takes a little over twenty minutes; --cases perfect alone takes about eight.
To measure a single provider rather than the pair, set GEMINI_MODEL / GROQ_MODEL or unset one
provider's key for the run.
The official 2025 CED requires 10 Pliny letters (6.4, 6.7, 6.16, 6.20, 7.27, 10.5, 10.6, 10.7, 10.37, 10.90) and these Aeneid lines: 1.1–33, 1.88–107, 1.496–508; 2.40–56, 2.201–249; 4.74–89, 4.165–197, 4.305–361; 6.450–476, 6.788–800, 6.847–853; 7.45–58, 7.783–792, 7.803–817; 11.532–594; 12.791–796, 12.803–812, 12.818–828, 12.919–952.
A number of passages in wide circulation — Aeneid 2.268–297, 4.160–164, 4.259–304, 6.295–332, 6.384–425, 6.854–899, and Pliny 1.6, 2.6, 7.5, 7.24, 9.6, 10.33, 10.34, 10.38, 10.39, 10.40 — are not on the 2025 required list. The Vergil ones were required under the previous (2012–2025) syllabus. They are all loaded here as supplementary, flagged in the UI, and excluded from "required" counts, because several sit immediately beside required passages and are genuinely useful context. They just will not appear on the exam as syllabus reading.
In solo mode — no Supabase configured, which is the default — there is no account, no database and
no server-side storage of anything you write. Progress lives in localStorage under
ap-latin-store. Clearing your browser data deletes it, so use the export button in Settings
periodically — that JSON file is your only backup, and Import restores it.
Classroom mode adds two, separate kinds of server-side storage, and it matters which one a piece of data ends up in:
user_progress) — everything else Settings can export as JSON:
Reading Room highlights and notes, your vocabulary deck, and yes, your typed translations and FRQ
answers along with the rest of your quiz/exam history. This exists so signing in on a second
device shows the same progress, not so a teacher can read it — the table has no policy granting
a teacher (or anyone but you) access to it, at all. See Classrooms and accounts above for
exactly what does and doesn't reach a teacher or a classmate.See CONTENT.md for the schema of every data file, worked examples, and the rules the verifier enforces.