ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Photograph the daily paper behavior log that comes home from school; an AI reads the
handwritten tally marks; you check the numbers; the data lands in tables and charts and a
printable summary for the ARD meeting.README 전문 보기 ↗Photograph the daily paper behavior log that comes home from school; an AI reads the handwritten tally marks; you check the numbers; the data lands in tables and charts and a printable summary for the ARD meeting.
lib/derive.ts, which is what lets a single filter set scope
the charts, the table, the report and the CSV identically.Reading handwritten tallies is the one place this app can quietly be wrong, and a number that's too high is worse than useless in an IEP meeting. Five things guard it:
raw_tally, e.g. 1111 66666). lib/tally.ts counts those
characters in plain TypeScript. Where the model's own arithmetic disagrees with its
transcription, the transcription wins and the row is flagged — the transcription is
the part a person can check against the photo.PARSER_PASSES=1 to fall back to a single read.The prompt is explicit that padding a run is a serious error and that a cell full of notes with no tally marks is correctly read as zero.
A whole page shrunk to 1568px on its long edge puts one row of the ten-row table at about a hundred pixels tall. That is not enough to tell seven marks from eight, and it is the same read that also has to find the rows and make sense of the notes. So once the page has been read, every row that carries a number is read again on its own, cropped out of the full-resolution photo in the browser and sent back as a single question: how many glyphs are in this one cell?
Splitting the work that way is the point — nothing in the second pass has to decide which row it is looking at. Two rules stop it becoming a new way to be wrong:
An empty close-up over a row the page said had marks is treated as a bad crop, not as
proof the marks aren't there: the page reading stands and the row is flagged. The pass
is skipped entirely when the row grid wasn't measured from the page, because cropping
from a guessed grid would hand the reader the wrong strip of paper. Anything that goes
wrong here costs that row its second look and nothing else — the reading the parent is
waiting for is already in hand. Covered by tests/reconcile.test.js.
Two counts sit alongside the eight behaviors and are deliberately not part of them:
The eight numbered behaviors describe what the child did. These two describe what the school had to do about it, which is the different question an ARD committee is actually being asked to decide — "interrupting, nine times" is not a staffing case, and "assistance was called four times in five days" is.
They are counted once per day, in their own box at the top of the day, because that is how the paper records them. Per-class would mean asking someone to decide which period a removal belonged to, and the form never says.
Three rules keep them honest:
They are never added to any incident total. The period total is generated from the eight behavior counts and stays that way, so every incident figure remains comparable with every figure recorded before these existed — and a removal is never counted as a second incident on top of the behavior that caused it. The report says so in as many words, and the day page says it under the box.
Only the filters that pick whole days can narrow them. The date range and the day of
week do. The behavior filter cannot, because an assistance call is not a behavior and
narrowing it would let a filtered report understate the support the classroom needed.
Neither can the class-period filter: the counts belong to the day, so a view scoped to
Writing still reports the whole day's figure, and the screen says so rather than
implying otherwise. Asserted in tests/support.test.js.
Every non-zero one is confirmed by a human, explicitly. These come from the teacher's own count or her sentences, not from tally marks, so the reader proposes them and the review screen shows why next to the box. Where two reads disagree, the lower stands — the same rule the tallies follow.
The box carries its own tick: "These two numbers are right for this day." When either count is above zero the tick is required — the save button says so and won't go until it's ticked. On a day with neither, it's optional, because a day with nothing to report has nothing to vouch for and demanding a tick on every quiet day would only teach people to tick without looking.
That tick is stored as support_confirmed_at, separate from verified_at. The latter
says someone checked the whole day against the page; this one says someone specifically
stood behind these two figures — worth distinguishing, since they're the numbers a
staffing argument would quote. It is never set on the reviewer's behalf: saving a log
is not by itself a statement about them. The day page and the printed report both say
which days were confirmed.
On the dashboard and in the report they get their own chart: grouped bars per school day, one pair per day, with the numbers table underneath. It is a separate plot from the incidents chart rather than a second line on it — these run in single figures where incidents run to dozens, and no chart here carries two y-axes.
They take no slot in the eight-colour behavior palette; that palette's slot order is its
colorblind-safety mechanism and its slots belong to the behaviors. Instead they use violet
and red from the same validated ramp as a pair of their own, checked against both chart
surfaces before use (colour-vision separation ΔE 22.7 light and 19.5 dark, against a floor
of 8). Each mode gets its own step, defined in app/globals.css as --support-assistance
and --support-removed. Identity never rests on hue alone: a legend is always present,
each bar carries its own number while the columns are wide enough, and the two series keep
a fixed left/right position within every day.
Where a stretch gets long enough that the day columns are narrower than a date label, all three day charts print every second, third or twelfth date instead of smearing them together — anchored to the most recent day, so the newest column is always labelled.
Every number in this app starts as a transcription. Until someone holds a day up against the original page, it is a claim about that page rather than a record of it — and a summary that cannot tell those two apart is weaker evidence than one that can.
So harper_daily_logs.verified_at records when a human last confirmed a day, and the
app says so everywhere: a badge on the table, a banner on the day page, a panel on the
dashboard linking straight to the days that need checking, and a line in the printed
report stating how many of the days it covers have been checked. A day is verified when
it is saved from the review screen, when its numbers are corrected, or when someone
presses I've checked these against the paper on its page.
The five September logs were transcribed from photographs that were never kept, so they start unverified — which is the truth about them.
The photo carries a box over each of the form's ten rows, labelled with the period and the number currently recorded against it. Tap one and you land on that row's counters — so a miscount is fixed while looking at the marks it came from, without hunting for the matching row in a list. Rows whose numbers disagree with their transcribed marks are drawn in amber.
The grid comes from the reader, and it can be wrong, so it is treated as a claim to be checked rather than fact:
Coordinates are stored as fractions of the image, so they hold up at any size — thumbnail, full screen, or zoomed.
Tapping a day — in the table, in the day-by-day list, on a bar in the daily chart, or
on a page in the gallery — opens /day/<date>: the photographed page at the top, every
period underneath with the marks it was counted from, and the teacher's notes.
Fix these numbers turns that page into an editor, so a miscount is corrected in place. The day keeps its id, its photo and its original machine reading, so the audit trail behind the correction survives. Where the saved numbers no longer match the marks transcribed off the page, the row says so and offers to re-count from the marks.
A day with no photo (the September logs were transcribed before the site existed) can have the paper photographed and attached from that same page — nothing needs deleting and re-entering.
Every confirmed log records, per row, what the reader thought the marks said next to what a human agreed they were. That pairing is the one thing no public handwriting corpus contains: MNIST and IAM teach which character is this, and the open questions on this form are how many of them are there and what does this teacher's shorthand mean. Only these pages answer those.
It costs nothing to collect and cannot be added later — the photos alone don't record where a human disagreed. Whether it is ever worth training on is a decision for when there's enough of it to tell; Settings shows the running count and downloads the set as JSON Lines.
No cropped images are stored. A crop is fully determined by the photo, the row grid and the period, all of which are already saved, so crops are cut on demand at whatever resolution a training run wants — and they improve retroactively when a grid is realigned. The box recorded on each sample is frozen at confirm time, so a later realignment can't silently change what an existing label refers to.
Nothing leaves the app: the samples sit in the same locked-down project as everything else, and the export is behind the same PIN.
Every view is scoped by the same filter row, and the filters live in the URL, so a filtered view can be bookmarked or shared as a link. Four dimensions, combinable:
An empty group means "all of it". Filtering by behavior narrows the counts but not the number of school days, so a per-day average stays honest: "aggression only" across five recorded days is 13 over 5 days, not 13 over the 3 days it happened on. A filtered report prints a line saying exactly which slice it covers, so a PDF can never misrepresent itself as the full record.
The numbers on the paper form, which are the numbers everywhere in this app:
| # | Behavior |
|---|---|
| 1 | Interrupting |
| 2 | Shouting |
| 3 | Inappropriate usage of supplies / breaking materials |
| 4 | Throwing / kicking / hitting |
| 5 | Cutting papers / materials |
| 6 | Refusal / work refusal |
| 7 | Taking shoes & socks off & throwing them |
| 8 | Snacking |
Two more things are tracked per day but are not behaviors and are never added to an
incident total: assistance_count (another adult called into the room) and removed_count
(taken out of the classroom). See above.
The teacher writes repeated digits as tallies — 1111 means behavior 1 happened four
times — and the parser is built specifically around counting those glyphs, including the
stroke (||||) and cursive-loop styles the teacher also uses.
Everything lives in an existing Supabase project, namespaced with a harper_ prefix:
harper_behaviors, harper_periods — lookupsharper_daily_logs — one row per school day (plus the raw model output, for audit,
row_geometry: where each form row sits on the photo, as image fractions, and
verified_at: when a human last checked the day against the original page, and
assistance_count / removed_count: the day's two support counts, which are not
behaviors and never part of an incident total, plus support_confirmed_at: when a human
ticked those two as correct)harper_log_periods — one row per class period per day, with b1…b8 countsharper_settings — app settings a parent can change without a redeploy; currently the
PIN, stored as a salted scrypt hashharper_training_samples — append-only: one row per period per confirmed review, holding
the machine's reading, the human's verdict, and where on the photo the cell sitsharper_v_* — SQL views over the same data, kept for ad-hoc queries in the Supabase
dashboard. The app derives its own aggregates in TypeScript so filters apply uniformly;
npm test cross-checks the two agree.harper-logs — a private storage bucket for the original photosRow-level security is enabled with no policies, so the public API keys can read nothing. All access goes through server-side route handlers using the service-role key.
Schema: supabase/migrations/. Seed data from the first five logs:
supabase/seed_september.sql.
See .env.example. Four are required: SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY,
APP_PIN, SESSION_SECRET. ANTHROPIC_API_KEY is needed only for reading photos —
without it, everything else works and logs can be entered by hand.
PARSER_PASSES is optional (default 2) — how many independent whole-page reads to take
of each photo. The per-row close-up pass runs on top of that and is not configurable.
Use the Settings page in the app. It stores a hashed PIN in the database and takes effect immediately for everyone.
APP_PIN in the environment is only the starting PIN — it's what the app falls back to
until someone changes it in Settings. Once a PIN has been set in the app, the env var is
ignored.
npm install
cp .env.example .env.local # then fill in the two secrets
npm run dev
npm run typecheck and npm test both run without any secrets — the tests work off a
fixture extracted from the seed data, on plain node, with no test framework.
A 4-digit PIN is real protection against someone stumbling onto the URL, not against a
determined attacker. The site is noindex, the photos are in a private bucket served
through 60-second signed URLs, and failed PIN attempts are rate-limited — but the address
shouldn't be shared more widely than it needs to be.