ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
영국 A레벨 수학·물리·화학·생물·컴퓨팅 튜터링, 실시간 수업·주제 자동 매핑·AI 복습 도우미 제공
UK A-Level Maths, Further Maths, Physics, Chemistry, Biology and Computing tutoring. Live sessions, automatic topic mapping, and an AI revision assistant grounded in your own tutor's words.
교육TypeScript · PLpgSQLGitHub @Aj33tSKY온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗For new contributors, start with the developer workflow.
It explains the single repository, tutors-dev / tutors deployments and database
migrations. There is one long-lived branch: staging releases automatically on every
merge to main, and production releases the same commits only when a human
dispatches it. The pipeline itself is
documented in docs/CI-CD.md, transcription in
docs/transcription.md, and the self-hosted video server in
infra/livekit/README.md.
Implementation of docs/mvp_plan.md: tutor discovery & booking, live WebRTC sessions with
transcription, post-session analytics, and a RAG revision chatbot — for UK A-Level Maths,
Further Maths, Physics, Chemistry, Biology and Computing.
Next.js 16 (App Router) · Tailwind v4 + shadcn/ui · Supabase (Postgres + Auth + pgvector) · Vercel AI SDK (AI Gateway) · Framer Motion · LiveKit (self-hosted on Hetzner) · Cloudflare R2 (session recordings) · Deepgram (transcription) · Stripe.
/tutors, filterable by subject/exam
board), tutor profiles, how-it-works, pricing, safeguarding/about.profiles row and RLS policies scoping every table./dashboard/student/chat) — live on the AI Gateway (openai/gpt-5.4-nano),
grounded in session_embeddings for that student, streaming via useChat. Conversations
persist to Postgres (chat_conversations / chat_messages) with a sidebar of past chats —
nothing is lost on refresh. System prompt is tuned for short, spoken, markdown-free answers./session/[bookingId]) — real LiveKit room per booking, using LiveKit's prebuilt
VideoConference UI (camera/mic, screen share, in-call text chat all included). Access is
server-checked before a token is ever minted: only the booking's own student or tutor can join
(not other users, not even a linked parent — deliberate, for safeguarding); everyone else gets a
404, not a redirect, so the booking's existence isn't leaked either. Token minting happens
entirely server-side in the page component — there's no separate public token endpoint.src/app/api/cron/summarize-sessions) — a Vercel Cron job (every 5
minutes, vercel.ts) finds completed bookings with a transcript but no summary yet, writes the
tutor's reflection on the session (goals, what was covered, what went well, areas for
improvement, learning insights, a key breakthrough, next steps) via the AI Gateway, citing a
[MM:SS] timestamp from the transcript's own per-line timestamps against any claim concrete
enough to point at one moment, computes a talk-time ratio from the transcript directly (no LLM
needed for that part), and chunks + embeds the transcript into
session_embeddings — this is what actually grounds the revision chatbot; before this, that
table was always empty. It also formats that structured summary into the markdown seed for
session_analytics.notes_markdown (src/lib/session-notes.ts) — see below./dashboard/student/bookings/[bookingId]) — each completed booking has
its own review page. The session recording sits beside a single editable markdown notes panel
(Session Goals, What We Covered, What Went Well, Areas for Improvement, Learning Insights, Key
Breakthrough, Next Steps — seeded from the LLM summary above, with its timestamp citations),
scrollable independently of the recording and stacking below it on mobile, and holding the same
height in read and edit mode; the tutor can rewrite it freely and save. While editing, Cancel
discards the current edit and Reset to AI version goes back to the original generation however
many times it has been saved over — everyone on the booking sees the same saved document. A
collapsible section below both has the full transcript. When recording is enabled, the recording
card (which has a full-screen mode) plays the session audio from a private, short-lived URL,
showing the tutor's screen shares at the points in the lesson they happened.Set at the top of src/app/api/chat/route.ts:
| Control | Default | Why |
|---|---|---|
HISTORY_WINDOW | last 12 messages | Only the recent window is sent to the model each turn — without this, cost per message grows with the entire conversation length, not just that turn. Full history still displays in the UI and is stored in full. |
DAILY_MESSAGE_LIMIT | 40 user messages/student/day | Checked before any model call, so an over-limit request costs $0. Returns a 429 the client renders as a friendly "resets at midnight" message. |
MAX_OUTPUT_TOKENS | 600 | Hard ceiling per reply, independent of what the model would otherwise generate. |
These are flat defaults, not tied to the pricing plan yet (see the table below).
openai/gpt-5.4-nano, not gpt-5.4-mini — mini returned "Free tier users do not
have access to this model" even after a card was added and the Vercel account moved to Pro.
Adding a card ≠ having spendable Gateway credits; that needs an explicit top-up at
vercel.com/[team]/~/ai → top-up. nano works on the free tier and is materially cheaper, so
it's the default rather than a fallback — swap the model string back to mini in
src/app/api/chat/route.ts if/when credits are topped up and the stronger model is wanted.vercel link +
vercel env pull) — no AI_GATEWAY_API_KEY needed for this app.The tutor-page action creates a lesson_requests row and a formatted direct message; it never
calls Stripe. Tutors set the actual time and an optional weekly run from their portal. Once a paid
lesson is complete, the tutor's Send invoice action creates a Stripe send_invoice invoice due
in seven days. invoice.paid and invoice.payment_failed update the matching booking in the
Stripe webhook handler.
Tutor payouts use Stripe's v2 Accounts API (stripe.v2.core.accounts.create), not the
deprecated stripe.accounts.create({ type: 'express' }) v1 pattern. Payout eligibility
(transfer_data.destination on the Stripe invoice) is decided with a live capability check
against Stripe whenever an invoice is sent and every payouts-page load — never from a cached DB flag —
because it's a money-movement decision, not just a display concern.
One live-API surprise worth flagging: Stripe's own docs/skill guidance say a marketplace
recipient-only connected account (just stripe_balance.stripe_transfers) shouldn't need
merchant.card_payments. This sandbox's actual API rejected that combination and required both
capabilities together — confirmed by testing the account-creation call directly against the
sandbox. The code requests both; it doesn't change what charges are actually made (still
destination charges, still recipient-owned transfers).
Same AI Gateway billing story as the chatbot, twice over: openai/text-embedding-3-small — the
model session_embeddings.embedding was originally sized for (vector(1536)) — needs paid
credits this account doesn't have, same as gpt-5.4-mini did. Switched to
google/text-embedding-005, which works on the free tier (768 dims), and resized the column to
vector(768) to match (safe — the table was empty). The summary-extraction call itself
(generateObject with gpt-5.4-nano) worked on the first try with no billing issue. If credits
get topped up later, both the embedding model and the column dimension need to change together.
The extraction prompt is deliberately conservative: told not to invent spec point numbers or
misconceptions/homework that aren't actually evidenced in the transcript, empty arrays over
guesses. Verified against a real physics tutoring transcript — correctly extracted the exact spec
point from the plan's own example (Edexcel Physics 4.2 — Particle Accelerators), real
misconceptions, real homework, and a plausible talk ratio; verified separately against a
low-content test transcript that it returns empty results rather than hallucinating.
| Module | Status | What's needed |
|---|---|---|
| Batch transcription | Implemented in the app | Needs DEEPGRAM_API_KEY and a signed LiveKit webhook in each Vercel project; see transcription setup |
| Chat limits by plan tier | Flat limit only | Once a real subscription model exists, tie DAILY_MESSAGE_LIMIT to pay-as-you-go vs subscriber |
| Tutor DBS document upload | Admin queue UI only, no upload | @vercel/blob, private access, form on tutor onboarding |
| Stripe production webhook | Test-mode only, via Stripe CLI locally | Once deployed, add a webhook endpoint in the Stripe dashboard pointing at /api/webhooks/stripe for invoice.paid,invoice.payment_failed, and set STRIPE_WEBHOOK_SECRET to its signing secret |
| Live-mode Stripe | Sandbox/test mode only | vercel integration resource claim stripe-emerald-arrow to attach this to a real Stripe account, then go through Stripe's own account activation |
| In-call whiteboard / KaTeX formula editor | Not started | Plan mentions Excalidraw + KaTeX; LiveKit's data channel (canPublishData, already granted) can carry whiteboard sync without another service |
LiveKit isn't in the Vercel Marketplace (only Mux — video streaming, not real-time calls), so the
app needs three env vars pointing at a LiveKit server: LIVEKIT_API_KEY, LIVEKIT_API_SECRET,
NEXT_PUBLIC_LIVEKIT_URL. Until all three are set, /session/[bookingId] shows a "video isn't
configured yet" state instead of erroring — checked via livekitConfigured() in
src/lib/livekit.ts.
Which server that is differs by environment: staging runs its own self-hosted server on Hetzner,
set up and updated from infra/livekit/ (which also covers sizing,
rollback and upgrades); production is still on a LiveKit Cloud
project until its own server exists. The app code is the same either way.
NEXT_PUBLIC_LIVEKIT_URL is baked in at build time, so changing it needs a redeploy.
npm install
# Get development values for .env.local from the owner (.env.example lists every
# name). Use the staging (tutors-dev) values, never production's. Run
# `vercel env pull` only after checking which project the repo is linked to:
# `vercel link` overwrites .env.local.
npm run dev
To exercise post-session invoices locally, run the Stripe CLI listener alongside npm run dev — it forwards
real test-mode events to your machine and prints a whsec_... signing secret to put in
.env.local as STRIPE_WEBHOOK_SECRET (this changes every time you start it):
stripe listen --forward-to localhost:3000/api/webhooks/stripe \
--events invoice.paid,invoice.payment_failed
Use Stripe's test card 4242 4242 4242 4242, any future expiry, any CVC.
Vercel Cron only fires on real deployments, so trigger the summarizer manually while developing:
curl -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/cron/summarize-sessions
The tutor explicitly opts in on the session page, for the lesson audio and any screen they share
(the camera is never recorded). When they join, a signed LiveKit webhook starts a single stereo
Egress job, routing the tutor's track to the left channel and the student's to the right, and writes
one OGG segment to the environment's private Cloudflare R2 bucket. While the tutor shares
their screen, a second Egress job saves that share's video as-is (MP4) to the same bucket. When the
tutor leaves, the webhook stops Egress; rejoining creates another segment, which the review page
plays in order, with each screen share shown at the point in the audio it happened. The audio file
is what the transcribe-sessions cron sends to Deepgram — see
transcription setup for why one audio file per call (not one per
participant) is the point, and how screen shares are recorded. A daily cron deletes each recording
after 90 days. Recordings are stored in a private Cloudflare R2 bucket; playback uses short-lived presigned
URLs issued only after the booking's RLS check passes, and no public recording URL is stored.
Before enabling this in a deployment:
GET from the app's origin. Set R2_ENDPOINT, R2_BUCKET, R2_ACCESS_KEY_ID
and R2_SECRET_ACCESS_KEY in the app environment (see transcription setup).DEEPGRAM_API_KEY in that Vercel project for batch transcription./api/webhooks/livekit using that
environment's API key pair. On the self-hosted server that is the WEBHOOK_URL given to
setup.sh; on LiveKit Cloud it is set in the dashboard./dashboard/student/tutors and /dashboard/tutor/students provide one private conversation for
each tutor–student pair with a shared booking. Messages support images, PDFs, and Word documents
up to 10 MB. Before deploying, create a private message-attachments Storage bucket with a
10 MB limit and restrict its allowed MIME types to the file types accepted by the UI. The SQL RLS
policies enforce participant-only reads/uploads using paths shaped
<conversation-id>/<sender-id>/<file-name>; parents deliberately do not inherit message access.
This is an MVP messaging store, so make two policy decisions before real student use: a retention period for messages/files, and malware scanning for uploaded homework. Neither is implemented by the app or supplied automatically by Storage.
Schema changes are released through versioned files in supabase/migrations/.
Create a new migration for each change; never re-run supabase/schema.sql or
edit a cloud database directly. See the developer workflow.
supabase/seed.mjs seeds 6 demo tutors (run with node supabase/seed.mjs, reads
SUPABASE_SERVICE_ROLE_KEY from .env.local). Demo tutor accounts:
[email protected] (and 5 others, see supabase/seed-data.mjs), password
KindlingDemo2026!. node supabase/create-demo-student.mjs [email] creates or resets a confirmed
demo student with the same password (it refuses to run unless .env.local points at the staging
project), and node supabase/create-test-booking.mjs <student-email> creates a paid booking so a
session can be tested without Stripe. The booking script does not check which project it is
pointed at, so look at NEXT_PUBLIC_SUPABASE_URL first.
src/app/(marketing)/ public site: landing, /tutors discovery, /tutors/[id], how-it-works, pricing, about
src/app/(auth)/ sign-in / sign-up + server actions
src/app/dashboard/ role-based dashboards (student, tutor, parent, admin)
src/app/dashboard/student/chat/ /chat redirects to the most recent (or a new) conversation
src/app/dashboard/student/chat/[id]/ the actual chat UI, sidebar, create/delete conversation actions
src/app/session/[id]/ live LiveKit video room — server page mints the token, client renders VideoConference
src/app/api/chat/ RAG chatbot streaming endpoint — persistence, windowing, daily cap live here
src/app/api/webhooks/stripe/ Checkout fulfillment — the only place a paid booking gets created
src/app/dashboard/tutor/payouts/ Stripe Connect (v2) onboarding + live payout-status check
src/lib/supabase/ browser/server/proxy Supabase clients + profile bootstrapping
src/lib/supabase/admin.ts service-role client for the webhook (no user session to read cookies from)
src/lib/stripe.ts lazy Stripe client (avoids crashing `next build` before keys exist)
src/lib/livekit.ts token minting + room naming (one room per booking id)
src/lib/recordings-storage.ts Cloudflare R2 client: upload target for Egress, presigned playback URLs, deletion
src/app/api/webhooks/livekit/ starts/stops recording from LiveKit's signed webhooks
infra/livekit/ the self-hosted LiveKit server: compose file, config templates, setup/deploy/check scripts
scripts/ci/ checks run by CI (env manifest, workflow permissions, migration history) and the LiveKit deploy
.github/workflows/ ci, staging, production, and the manual Deploy LiveKit workflow
supabase/migrations/ versioned schema and RLS changes deployed by GitHub Actions
Nothing above was taken on faith — checked with exploratory node scripts against the live
Supabase/Stripe sandboxes (not committed; scratch files loading .env.local):
chat_messages rows./api/chat route hit with an authenticated session cookie end to end (200, streamed
reply, both messages persisted, title auto-generated).generateText before and after the billing change described
above.checkout.session.completed event delivered to the local webhook (via stripe webhooks.generateTestHeaderString) and confirmed it creates exactly one booking — replayed the
identical event a second time and confirmed no duplicate; a forged signature confirmed rejected
with 400./session/[id] (200); a
second, unrelated student got a 404 rather than any indication the booking exists; an
unauthenticated request was redirected to /sign-in?next=.... Actual audio/video was verified
working live in a real call.session_analytics.full_transcript, roughly three minutes
after the tutor left, with talk_ratio and summary_notes generated from it. The always-on
LiveKit agent this used to need has been removed; audio-only per-track egress plus the
transcribe-sessions cron replaced it. See docs/transcription.md,
including the two failure modes that produce silence rather than an error.