ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
실시간 피치 피드백과 워밍업·곡 연습을 제공하는 온라인 노래 연습 서비스, 피치 미터와 음역 테스트는 무료, 가이드 연습은 하루 5분 무료
Online singing practice with live pitch feedback, warmups and songs. The pitch meter and range test are free; guided practice is 5 free minutes a day.
교육TypeScript · JavaScriptGitHub @JasonColapietro온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Online singing practice with live pitch feedback, warmups and songs —
production at https://sing.suedeai.ai. The pitch meter and range test are
free; guided practice is 5 free minutes a day, and Suede Pro unlocks the rest.README 전문 보기 ↗Online singing practice with live pitch feedback, warmups and songs — production at https://sing.suedeai.ai. The pitch meter and range test are free; guided practice is 5 free minutes a day, and Suede Pro unlocks the rest.
A single Next.js app (App Router). There is no SQL database: Stripe is the
source of truth for Pro, Upstash Redis backs cloud sync, account backups,
comp-code counters and rate limiting, and optional Clerk accounts switch on
only when their keys are configured. Read AGENTS.md before writing code — this Next.js version
differs from older docs, and its guides live in node_modules/next/dist/docs/.
| Path | What lives there |
|---|---|
app/ | Routes, pages and the app/api/* handlers |
components/, lib/ | UI and shared logic (audio DSP in lib/audio/) |
content/ | Lessons, the book, the atlas and their PDFs |
contracts/ | JSON contracts the curriculum and progress code are tested against |
data/ | Singer range data and its evidence |
scripts/ | Content compilers, audits, Stripe setup, deploy |
e2e/ | Playwright browser journeys and audits |
marketing/ | Social assets; not served by the app |
npm ci
vercel link && vercel env pull # optional: Stripe, Pro key and Redis secrets
npm run dev # http://localhost:3000
The app builds and runs without any secrets; every env var is read lazily, so
Pro, sync and comp codes simply report themselves unconfigured. See
.env.example for what each variable does.
Before pushing, run what CI runs:
npx tsc --noEmit
npm run lint
npm test
npm run build
The browser checks run against the production build on port 3100, through
the same runner CI calls (.github/workflows/ci.yml):
npm start -- --port 3100 # in another shell, after npm run build
node e2e/suite.mjs journeys # or mobile-audit, desktop-audit, audio, layout-shift, layout-shift-returning
Single checks take any base URL (default http://localhost:3000): npm run e2e, plus e2e:mobile, e2e:pitch, e2e:vibrato, e2e:programs and
e2e:breath. npm run compile regenerates the committed lib/*-data.ts from
content/ and data/; npm test fails when one is stale.
Pro is sold at Early Access pricing: a $79 lifetime purchase. The $4.99
monthly subscription is sold out (shown as such on /pro; existing subscribers
keep renewing, the portal and cancellation). STRIPE_SECRET_KEY decides which Stripe account and mode
the app talks to; see Going live for where production points.
Locally: vercel link && vercel env pull.
Pro hangs off no account and no database: a sign-in, on a deployment with accounts configured, never touches it. So Stripe is the only source of truth for entitlement, and the flow is built around that:
| Route | Job |
|---|---|
POST /api/checkout | Creates a hosted Stripe Checkout Session. No card field is ever rendered by this app. |
POST /api/entitlement | Resolves entitlement from a session_id (right after paying), subscription_id, or lifetime payment_intent_id. |
POST /api/restore | Unlocks Pro on a new device from the purchaser's Pro key. |
POST /api/portal | Opens Stripe's billing portal for monthly and legacy annual subscribers. Lifetime purchases do not use the portal. |
POST /api/redeem | Turns a comp code into a 30-day pass, as a real trialing subscription with no card. |
PRO_COMP_CODES is a comma-separated list; handing a code out and revoking it
are both an env var edit. Matching is case- and whitespace-insensitive.
Each code is capped at 25 redemptions by default. Every redemption mints a real Stripe customer and subscription, so an uncapped code that ends up somewhere public is both unbounded free Pro and unbounded object creation.
Change the total per code with PRO_COMP_MAX_REDEMPTIONS (set it to 0 to
remove the cap, for example for a mailing-list code):
PRO_COMP_MAX_REDEMPTIONS=50
The counter is atomic and lives in the same Upstash store as cloud sync, keyed
comp:used:<CODE>. It runs whenever a cap applies, which is the default, so
comp codes need the Upstash store unless PRO_COMP_MAX_REDEMPTIONS=0. Raising
the cap later resumes from the true count; to reset a code, delete its key. If
a cap applies and the store can't be reached, redemption fails closed with a
503 rather than silently granting uncapped passes — the opposite of what the
cap is for.
Entitlement lives in one browser, so purchasers get a Pro key, an HMAC over
their Stripe customer and billing reference (lib/pro-key.ts, signed with
PRO_KEY_SECRET). The billing reference is a subscription id for monthly or
legacy annual access and a PaymentIntent id for lifetime access. The key is
shown on /pro while Pro is active and is what /api/restore accepts.
A key is proof of purchase, not a grant: restore verifies the signature and then asks Stripe whether the subscription is still live or the lifetime charge remains unreversed. A cancelled subscription or refunded lifetime purchase stops unlocking anything. Keys are derived rather than stored, so a lost one is regenerated, not looked up:
node --env-file=.env.local scripts/pro-key.mjs sub_123… # subscription
node --env-file=.env.local scripts/pro-key.mjs pi_123… # lifetime
node --env-file=.env.local scripts/pro-key.mjs [email protected]
Restoring by email was removed deliberately: it let anyone who knew a subscriber's address unlock Pro, and the endpoint doubled as an oracle for "is this person a subscriber?".
Two layers, and the edge one does the real work.
lib/rate-limit.ts caps bursts inside the function (429 with Retry-After,
from 5 per 10 minutes on /api/redeem to 60 per minute on /api/book).
It's free but opportunistic: counters live per function instance, so a
parallel burst that lands on fresh instances can slip past it entirely. Treat
it as a floor, not a guarantee.
The durable layer is a Vercel WAF rule, live on this project:
Rate limit Stripe API — path starts with /api/ — 40 requests / 60s per IP — deny
Requests denied there never reach the functions and aren't billed. A real session makes one or two API calls (entitlement revalidation is throttled to twice a day), so 40/min leaves roughly 20× headroom.
vercel firewall rules inspect "Rate limit Stripe API"
vercel firewall diff && vercel firewall publish --yes
Two things to know before editing it. Changes are staged until you publish,
and edit silently reports "No changes detected" if you pass only the field you
want to change — re-pass the whole rate-limit config or nothing happens:
vercel firewall rules edit "Rate limit Stripe API" \
--action rate_limit --rate-limit-window 60 --rate-limit-requests 40 \
--rate-limit-keys ip --rate-limit-action deny --yes
Passing --condition replaces the rule's conditions, so omit it unless you
mean to rewrite the path match. WAF counters are per region, so a distributed
caller can exceed the limit by roughly the number of regions it hits.
lib/pro.ts caches the last answer in localStorage; components/pro/sync.tsx
re-checks with Stripe twice a day so a cancellation, failed payment, refund, or
dispute revokes access. New prices resolve by lookup key
(suede_pro_monthly_early_access and
suede_pro_lifetime_early_access), never by hardcoded id. Legacy monthly and
annual lookup keys remain recognized for existing subscriptions.
Real money goes to the existing JC INVESTMENT GROUP LLC Stripe account
(acct_1SHG7dRdcsaZ58FL), not to a claimed sandbox.
The Marketplace resource suede-sing-pro was deliberately left unclaimed.
Claiming it would have minted a second Stripe account, starting unactivated
and needing its own business details, bank account, identity verification and
tax setup — splitting Suede Sing's revenue, payouts and 1099s away from the LLC
that already does all of that. The sandbox stays as the test-mode environment
instead.
Live mode is already provisioned in the LLC account:
| Product | prod_UymwMKT9x94n1k — Suede Pro |
| Monthly Early Access | $4.99/mo, lookup key suede_pro_monthly_early_access |
| Lifetime Early Access | $79 once, lookup key suede_pro_lifetime_early_access |
| Billing portal | the account's existing live default config, cancel-at-period-end enabled for subscriptions |
New checkout sells lifetime only; /api/checkout rejects monthly (409) like annual. The former $9.99 monthly price
and old $30/yr suede_pro_annual price are left untouched and recognized for
existing subscribers. /api/checkout hard-rejects annual before any Stripe
lookup or Checkout Session call.
Production is live. STRIPE_SECRET_KEY on the sing project holds the LLC's
live secret key, and /api/checkout uses the prices above. Checkout sells only
lifetime, in one-time payment mode; the monthly price stays active for existing
subscribers and for comp-code passes (/api/redeem). No code differs between
test and live mode because both prices resolve by lookup key.
.env.local still carries the sandbox key, so local development stays in test
mode. That is deliberate — don't pull the live key onto a laptop.
Two traps to know:
The Marketplace resource suede-sing-pro still exists and originally set
STRIPE_SECRET_KEY. A re-sync or reinstall of it can overwrite the live key
and quietly drop production back into test mode. If checkout starts minting
cs_test_… sessions, that is what happened — reset the variable and redeploy.
Env changes need a rebuild to take effect (bash scripts/deploy-prod.sh);
editing the variable alone changes nothing until then.
Test and live are separate data spaces. Anything created in one is invisible
to the other, so a live account with no prices under the lookup keys makes
/api/checkout fail. scripts/stripe-setup.mjs is idempotent and creates the
product, both sellable prices and a portal config against whichever key it is given.
On an existing product it updates the description if the copy in the script has
changed. It does not repoint or archive legacy monthly or annual prices. Use it if the
live catalog ever needs rebuilding:
vercel env run -e production -- node scripts/stripe-setup.mjs # live
npm run stripe:setup # test mode
It prints which mode it touched and reports charges_enabled.
Verify the catalog by inspecting the two lookup-key prices. Verify the deployed
annual prohibition with a single POST containing { "plan": "annual" }; the
route must return 409 before Stripe is called. Do not create a live Checkout
Session merely as a deployment smoke test.
Production is https://sing.suedeai.ai, served by the Vercel project sing
(team suede-ai-64d39175). Pushing main deploys it; previews are disabled by
the ignoreCommand in vercel.json.
To force a rebuild — the only way to pick up a changed NEXT_PUBLIC_* variable,
since those are inlined at build time:
bash scripts/deploy-prod.sh
It runs lint, tsc and the tests, builds once locally with the production env
(vercel pull + vercel build) and uploads that output with vercel deploy --prebuilt. What it deploys is this checkout, so run it from a clean
origin/main.
suedeai.ai is on GoDaddy nameservers, so each subdomain needs an
A <sub> 76.76.21.21 record there (GoDaddy gates the save behind an identity
check, so it can't be automated). Every absolute URL — canonicals, sitemap.xml,
JSON-LD, OG images — comes from NEXT_PUBLIC_SITE_URL via lib/site.ts, which
falls back to the .vercel.app host when the variable is unset.
Search Console is verified for the whole domain as the sc-domain:suedeai.ai
property, so new subdomains need no separate verification — just submit their
sitemap.