Lexiverse
Lexiverse - AI contextual reading engine for English learners. Your next article is your next review.
a minimal singlish typing test in the style of monkeytype. type and get your singlish wpm.
EducationTypeScript · CSSGitHub @anselmlongOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Monkeytype, but Singaporean.
Translation pending
Taken from the repository README
Loading the full README.
Read the full README ↗A Singlish typing test in the style of monkeytype. Pick a mode and a duration, type the prompt, get your wpm and accuracy. Dark, minimal, no accounts — just wpm lah. An optional global leaderboard (a small FastAPI + SQLite backend) lets you post a nickname and score; the core test still runs fully client-side without it.
Live modes:
lah, shiok, kopi, paynow, bto, hdb, …)ang mo kio — north south line)gd nite swt drms — msn status)The results screen shows a pace-over-time graph (running wpm per second, with an × on every second that contained a wrong keystroke) and the singlish you typed — each word from the run, with a hover tooltip giving its meaning and a click-through to its dictionary entry (Wiktionary, or Wikipedia for the institutions).
output: "export") — no server, no API routes (the optional
leaderboard is a separate backend in leaderboard-api/)lib/ (wpm/accuracy, pace, challenges, corpora, records)mainnpm install
npm run dev # dev server at http://localhost:3000
Other scripts:
npm run build # production build -> static export in ./out
npm test # unit tests for lib/ (wpm math, pace, challenges, corpora, records)
To preview the exact static output Vercel serves:
npm run build
npx serve out # or: python3 -m http.server -d out 8000
The leaderboard talks to a separate backend. Point the frontend at it with an env var (defaults to the production API when unset):
# .env.local
NEXT_PUBLIC_LEADERBOARD_API=http://localhost:8000
Run the backend locally from leaderboard-api/:
pip install -r leaderboard-api/requirements.txt
KOPITYPE_DB=./leaderboard.db uvicorn main:app --port 8000 # from leaderboard-api/
KOPITYPE_DB sets the SQLite path (default /data/leaderboard.db, meant for a
mounted volume in the container). The board shows each player's best run per mode +
duration, so multiple attempts don't crowd it out.
The API accepts known corpus IDs and 15/30/60-second runs, rejects blank names,
and caps raw WPM at 1,000. SQLite enforces 10 submissions per client per minute
and 200 total per minute across workers sharing the database. Expired counters
are removed during submissions. CORS defaults to kopitype.com, www.kopitype.com
and localhost:3000; override CORS_ORIGINS with exact comma-separated origins.
IP quotas use the ASGI peer address, never arbitrary forwarded headers. Configure Uvicorn's trusted proxy IPs only for your actual reverse proxy; otherwise users behind a proxy share its quota. A shared NAT also shares a quota. CORS and quotas reduce browser misuse and spam; scores remain client-reported, are not verified proof of a typing run, and nicknames are not authenticated identities. Strong anti-cheat requires server-issued challenges and run validation as a follow-up.
When the API runs in Docker behind a reverse proxy on the same host (nginx →
127.0.0.1:8124), every request reaches the container from the Docker bridge
gateway (typically 172.17.0.1), so without extra config all players share one
10-per-minute quota. Tell Uvicorn to trust that hop's X-Forwarded-For with
its FORWARDED_ALLOW_IPS env var, and publish the port on loopback only so
nobody can reach the container directly and forge the header:
docker run -d --name kopitype-leaderboard \
-p 127.0.0.1:8124:8124 \
-e FORWARDED_ALLOW_IPS=172.17.0.1 \
-v "$PWD/data:/data" kopitype-leaderboard
Backend regressions: install leaderboard-api/requirements.txt plus httpx, then
run cd leaderboard-api && python -m unittest test_api.
The whole test lives on one page (app/page.tsx → components/TypingTest.tsx), a small
state machine: idle → running → finished.
<input> captures typing. Its value is the current word's buffer, so it
works with both physical keyboards and mobile soft keyboards (we read value changes,
not keycodes). A space commits the word. Backspace erases within the current word,
and — monkeytype-style — crosses back into the previous word only if that word was
committed with an error; correctly typed words are locked.components/WordStream.tsx renders per-character state: pending / correct /
incorrect / extra, with a caret, and scrolls as you descend the lines.lib/wpm.ts holds the pure, unit-tested math: wpm = (correct chars / 5) / minutes,
plus raw wpm and accuracy.components/Stats.tsx shows the result, your
previous score, and your personal best (per mode + duration, in localStorage —
still no accounts). Tab (while typing or on the results screen) or the restart
button starts a fresh test; Shift+Tab and Escape are never hijacked, so
keyboard navigation always works.lib/sound.ts (WebAudio, no
audio assets) behind a "sound" toggle that's remembered in localStorage.lib/pace.ts, pure + tested); the results screen renders them as an SVG line
chart (components/PaceChart.tsx — hover crosshair, keyboard arrows, no chart
library).data/glossary.json maps terms to short meanings (plus an optional link);
components/GlossaryWords.tsx turns the words you attempted into hoverable,
clickable chips.Click custom in the mode bar, type a phrase (up to 300 chars), share the link.
The phrase travels base64url-encoded in the URL fragment (#c=…) — no backend, no
storage, nothing expires. Opening a challenge link loads a fixed-phrase test that
ends when the phrase does; the timer counts up and the wpm is computed from elapsed
time. Challenges don't touch personal bests. lib/challenge.ts owns the
encode/decode (validated + normalized on the way in — never trust a URL).
Click submit in the mode bar to send in words, quotes, or meanings. With no
backend, the review queue is the repo itself: the form opens a prefilled GitHub
issue (labelled corpus-submission) containing a ready-to-paste JSON snippet.
Nothing goes live until a maintainer vets it and commits it into data/ —
crowdsourced input, curated output.
This is deliberately a two-step change (say you want a "kopi" mode):
Drop a JSON file in data/:
["kopi", "teh", "milo", "kosong"]
{ "text": string, "source": string }:
[{ "text": "kopi c kosong gau one cup dabao", "source": "kopi order" }]
Register it in the one MODES array in lib/corpus.ts:
import kopi from "../data/kopi.json";
export const MODES: ModeDef[] = [
// ...existing modes...
{ id: "kopi", label: "kopi", type: "words", words: kopi as string[] },
];
That's it — it shows up in the mode bar automatically. Add gated: true to make a mode ask
for a one-tap confirm before it switches in (that's how vulgar.json is wired). Word-mode tokens must
be single words (no spaces); put any multi-word terms in a quotes corpus instead.
| File | Entries | Shape |
|---|---|---|
data/words.json | ~245 | single lowercase tokens |
data/sg.json | ~97 | single lowercase tokens (everyday SG) |
data/quotes.json | 72 | { text, source } phrases |
data/mrt.json | ~122 | { text, source } station → line |
data/xmm.json | 33 | { text, source } vowel-less texts |
data/vulgar.json | 44 | single tokens, gated |
data/glossary.json | ~217 | term: { meaning, link? } for the results-screen glossary |
The corpora are hand-written and meant to be human-readable (one entry per line) so they're easy to vet and extend — which is also how crowdsourced submissions land (see above).
Accounts, themes, multiplayer, punctuation/numbers toggles, i18n. (Personal bests, sound, the pace graph, custom challenges, and the glossary all landed post-v1 — local-only or in-URL. The global leaderboard came later and is the one piece with a backend; everything else still runs without one.)