a tiny speaking benchmark. read prompts out loud, get a wpm score, chase one more personal best 🙊 (totally original idea hehehe)
check out CONTRIBUTING.md for the longer contributor guide — this readme is the fast version.
monkeyspeak is basically monkeytype but you use your voice. pick a prompt, hit start talking, and the app tracks how fast you spoke, what it actually heard, and whether too many ums snuck in while you were cooking.
works out of the box with browser speech recognition. no api key, no signup, just mic access and mild embarrassment when you misread the prompt.
speed mode
- 15s / 30s / 60s / 120s timed runs
- live net wpm (fillers like um and uh get stripped)
- words dissolve off the screen as the app hears them correctly
- passage end mode + sentence difficulty tiers
- session graph with per-word timing windows
- personal bests per duration + prompt type (local)
clarity mode (stt tool benchmark)
- pick a transcription tool (wispr, chatgpt voice, apple, deepgram, chrome, or custom)
- shuffle through curated precision pads (office / clinic / tech / tongue twisters etc)
- read it out loud in that tool, paste the transcript back, get word + punctuation scores
- letter grades from s down to needs work
- engine board on the page filters by the board-prompt control up top
- current + top clarity on the side, with which tool hit your top
- shared tool leaderboard via supabase (rolling 30-day averages)
ghost race
- race a visual ghost that replays the pace of your saved speed personal best
- needs at least one speed pb first (new bests store a timeline for the ghost)
- same mic + duration controls as speed mode
leaderboard + social
- home page shows a top-5 preview with a link to the full board
/leaderboardhas the full rankings (filters + scroll) plus your local stats/charts- global speed leaderboard via supabase — nickname + emoji after a run, no signup
- clarity mode has its own per-tool board (separate from speed)
- top score card shows your personal best for the current duration, not whoever is #1 globally
/statsredirects to/leaderboard#stats
profile
- achievement grid (first words, silverback, clarity s, etc)
- profile hub drawer — nickname, activity, badges
- personal bests and trends stay local; the board is the only shared piece
ui + polish
- desk / doodle hero (slanted sticky notes, paper tape, monkey mascot)
- themes, accent colors, fonts, blind mode
- clarity tips + scorecard on the clarity page; daily goal lives on speed home
- branded og image, monkey favicon
speech (stt)
- browser web speech api by default — no key needed
- optional deepgram for better live transcription in production
- brave/edge route through render websocket proxy; chrome/firefox use vercel http bridge (see production)
you need node 20+ and a microphone. that's it for the default path.
git clone https://github.com/nothariharan/monkeyspeak.git
cd monkeyspeak
npm install
npm run devpoint your browser at http://localhost:3000, allow the mic, and try not to read like you are defusing a bomb.
sip some water. you're about to yapppppity yapppp
this is the easy path. try this first.
npm run devchrome is usually the smoothest. brave and edge work for browser mode but shields can block google's speech service — if you want deepgram on brave, see below.
deepgram gives you better live transcription and matches the production-style setup.
copy the env template:
cp .env.example .env.localfill in .env.local:
DEEPGRAM_API_KEY=your_deepgram_api_key_here
DEEPGRAM_PROJECT_ID=your_deepgram_project_id_here
NEXT_PUBLIC_DEEPGRAM_PROXY_URL=ws://localhost:8080/api/deepgram/proxyboot both in two terminals:
# terminal 1
npm run dev
# terminal 2
npm run dev:backendopen settings, flip stt provider to deepgram, and you should be good.
the deepgram api key stays on the server. the browser talks to your proxy or the vercel bridge, not straight to deepgram with the permanent key taped to the client.
browser routing in prod
| browser | deepgram path |
|---|---|
| all (when proxy is up) | wss://monkeyspeak.onrender.com/api/deepgram/proxy |
| fallback | same-origin POST /api/deepgram/live on vercel |
| localhost | local ws proxy on :8080 when backend is running |
the vercel http bridge is fallback-only — duplex request streaming to serverless hangs, which used to show up as a client “connection timed out” error. health checks go through /api/deepgram/proxy-health on vercel so the browser never does a cross-origin fetch at the render url.
troubleshooting: voice wave moves but no words
the speaking wave is driven by local mic energy. it can look “alive” even when deepgram never returns Results. usual causes:
- render proxy cold / unreachable —
/api/deepgram/proxy-healthfails and the app falls back toPOST /api/deepgram/liveon vercel. that bridge often opens without useful transcripts. proxy-health now retries with a longer timeout so free-tier cold starts can wake up; the client also retries the probe once before falling back. - stale “listening” session — if deepgram claimed to connect but produced no words, the 5s failsafe used to no-op. it now force-reconnects.
- local setup — run both
npm run devandnpm run dev:backend, and setNEXT_PUBLIC_DEEPGRAM_PROXY_URL=ws://localhost:8080/api/deepgram/proxyin.env.local.
debug: set NEXT_PUBLIC_DEBUG_STT=true, then check the network tab for a websocket to the render/local proxy (preferred) vs a long /api/deepgram/live post (fragile fallback).
scores are shared across everyone via supabase postgres. no accounts — just a nickname and emoji after a speed run.
- create a Supabase project
- run the migrations in order:
- supabase/migrations/001_leaderboard_entries.sql — speed board
- supabase/migrations/002_clarity_benchmark.sql — clarity tool board
- add to
.env.local:
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_herewithout those vars the app still runs; the boards just stay empty / show a friendly message until you wire them up.
speed writes go through POST /api/leaderboard with the service role key. same name + board + higher wpm updates your row, lower wpm gets ignored.
clarity writes go through POST /api/clarity-benchmark after you score a paste-in transcript.
npm run dev # next dev server
npm run dev:backend # deepgram websocket proxy on port 8080
npm run dev:turbo # next with turbopack
npm run dev:clean # clear .next cache then dev
npm run build # production build
npm start # prod server + integrated proxy (server.js)
npm run lint # eslint — do this before a pr
npm test # jest unit tests (scoring, streak, achievements)Enter— start a test, or next prompt after a runTab— reset, stop, or retry depending on where you areEscape— bail on a running test earlyCtrl + ,— open settingsCtrl + 1/Ctrl + 2— speed mode / clarity mode- ghost race is in the header tabs (same controls as speed once you have a pb)
monkeyspeak/
app/ pages and api routes (leaderboard, deepgram, proxy health)
app/stats/ stats dashboard
components/ ui, controls, results, profile hub, hero leaderboard
components/game/ speaking test world, hud, graph, monkey
hooks/ timer, speech providers, vad, leaderboard fetch
lib/ prompts, scoring, diff, themes, achievements, streak
lib/supabase/ server-only supabase admin client
lib/stats/ wpm, timeline, personal bests, consistency
supabase/ migration sql (speed + clarity boards)
store/ zustand state + persisted settings
public/ sprites, onnx model, audio workers, mascots, ghost-race art
backend/ standalone deepgram websocket proxy (render)
server.js prod next + proxy in one node process
patches/ patch-package fixes
touching the live speaking experience? start in app/page.tsx, components/game/SpeakingGame.tsx, and hooks/.
touching ghost race? components/game/GhostRace.tsx, lib/stats/timeline.ts, and the ghost mode bits in store/testStore.ts.
touching scoring? lib/alignTranscriptToPrompt.ts, lib/fillers.ts, lib/stats/, lib/diff.ts.
touching the speed board? app/api/leaderboard/route.ts, app/leaderboard/page.tsx, components/decor/HeroLeaderboard.tsx.
touching the clarity board? app/api/clarity-benchmark/route.ts, lib/clarityLeaderboard/, components/ClarityInput.tsx.
touching stats/achievements? app/leaderboard/page.tsx (#stats), lib/achievements.ts, lib/stats/streak.ts.
| variable | notes |
|---|---|
DEEPGRAM_API_KEY |
server side only, never expose in client code |
DEEPGRAM_PROJECT_ID |
used for short-lived token creation |
NEXT_PUBLIC_DEEPGRAM_PROXY_URL |
browser websocket url. local: ws://localhost:8080/api/deepgram/proxy. prod: wss://monkeyspeak.onrender.com/api/deepgram/proxy |
SUPABASE_URL |
supabase project url for global leaderboard (server only) |
SUPABASE_SERVICE_ROLE_KEY |
service role key for leaderboard api routes |
DEEPGRAM_SECONDS_PER_IP |
optional daily deepgram budget per ip (default 300s) |
DEEPGRAM_SECONDS_GLOBAL |
optional global daily deepgram budget (default 3600s) |
PORT |
optional. defaults to 3000 for the app, 8080 for backend |
NEXT_PUBLIC_DEBUG_STT |
set to true for stt debug logs in the browser console |
DEBUG_DG_PROXY |
set to 1 for noisy proxy logs on render |
full copy-paste template lives in .env.example.
live site: monkeyspeak-delta.vercel.app
split deploy (what we run today):
| piece | host | job |
|---|---|---|
| frontend + api routes | vercel | next app, leaderboard, deepgram http bridge, proxy health |
| websocket proxy | render | deepgram audio proxy for brave/edge |
on vercel set SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, DEEPGRAM_API_KEY, DEEPGRAM_PROJECT_ID, and NEXT_PUBLIC_DEEPGRAM_PROXY_URL=wss://monkeyspeak.onrender.com/api/deepgram/proxy. render only needs the deepgram keys.
single-service option still works locally or on one node:
npm run build
npm startnpm start runs server.js, which serves next and the deepgram proxy from the same process.
want render setup notes? see backend/README.md.
skim CONTRIBUTING.md, then:
npm run lint
npm run build
npm testalso do one real speaking run in the browser. speech bugs love looking fine in typescript and then falling apart the second a microphone joins the party.
mit. see LICENSE.
