Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

70 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bol · speak a language by ear

An audio-only language app. You learn to understand and speak by ear, the way you learned your first language. No reading, no writing, no script to memorize. Press play, listen, say it back. Marathi and Hindi ship today; the engine is built for more.

The method is grounded in second-language-acquisition research (see Learning method): comprehensible input + retrieval practice + spaced repetition + real speaking practice, interleaved and ordered easy to hard.

linguistic/            (local folder; GitHub repo is roshan-b1/bol)
├── web/        Vite + React web app (runs today, no setup)
├── mobile/     Expo + React Native app (builds for the App Store / Play Store)
├── scripts/    generate-audio.mjs (bake mp3s), render-icons.mjs (brand → PNGs), content QA tools
├── brand/      logo, app icon, wordmark, brand guide (SVG)
└── README.md

Web and mobile share the same content and core logic (SRS engine, answer matching, exercise builder, streak/level engine are byte-identical files). Only the UI layer differs. Note: the web app leads on features and the mobile app is a catch-up port behind it (see Mobile parity).


Design principle: same features for every language

Every feature is identical for every language. Only the language changes. There is no Marathi-only or Hindi-only capability. The app is registry-driven: all screens, games, the guided path, spaced repetition, the AI tutor, and engagement mechanics run off a generic content model, so a learner picks Marathi or Hindi (or any language added later) and gets the exact same product with different words and audio. When adding a feature, it must work for all languages, not one.


Every feature

The learning core

  • Onboarding: pick your heritage language first (this is your family's language, so it leads), then why you're learning and a daily goal. The language picker and the reasons are registry-driven and language-aware, so a Hindi learner is never shown "Marathi" copy. The choice persists, so the app opens in that language.
  • Placement diagnostic: an optional "Already know some?" check for new learners, because a heritage learner walks in knowing scattered pieces (food and family words from childhood, but no grammar or idioms). It probes the whole course in order, 16 sections x 3 probes, and finds where your knowledge drops off. Probes are production, not multiple choice: you get the English and say it from memory. Multiple choice has a 25% guess rate, which would place people past material they can't actually produce. Where the browser has speech recognition, the mic grades you against the same pass bar as lessons, with no self-report buttons (a placement you can flatter corrupts the review schedule it seeds); a mic misfire is never scored, you just retry or skip. On browsers without speech recognition, an honest self-check is the fallback. Sections you pass are marked complete and their items are seeded into the SRS with a partial interval, so the guided path starts you at your real level instead of "hello".
  • Today (guided daily session): the home screen leads with one Start today's session button. It runs your due reviews first, then the next lesson on the recommended path, blended into a single flow, so you never have to decide what to study. Reviews are capped per session so it stays short (research favors brief, daily practice).
  • Lessons: learn new material in the Pimsleur arc: listen (intro) → pick the meaning (comprehension) → say it from memory (production). A correct answer auto-advances; a wrong one pauses so you study the fix. Missed items requeue to the back and come back until you get them right (no free pass while the answer is fresh), with an "I don't know" escape that shows the answer and re-queues it.
  • Speak drill (Easy / Medium / Hard): English prompt in, you say it in the target language. Pure output practice, the thing most apps skip. Difficulty bands map to lesson levels.
  • Review: a spaced-repetition queue brings each phrase back right before you'd forget it, with a visible schedule (ready now / next 24h / this week / further out / in long-term memory) and a "practice ahead" option when nothing is due.
  • Talk: live conversation with an AI native-speaker tutor (Asha for Marathi, Meera for Hindi). She asks what you want to practice, chats by voice at your level using vocabulary you've already studied, gives gentle corrections of real mistakes, and offers "What could I say?" lifelines (2-3 natural options with pronunciation + meaning). Powered by the Claude API from the browser (see Accounts & keys).
  • Dialogues roleplay: scripted real-life scenes (tea stall, market, doctor, auto...) where the app plays the other person and you speak your side. On-device and free; your lines feed the same SRS cards and earn XP.
  • The path, shown as units: the course is chunked into units of 10 lessons following the recommended order, so 294 lessons never render as a wall. Your current unit is open and labelled "you're here"; finished ones collapse to a checkmark.
  • Nothing is locked: you can open any unit at any time. A heritage learner poking ahead at "Idioms" is the whole motivation, so later units are only muted, never gated. The guided Today session still enforces the correct order for anyone who just wants to be told what to do.
  • Topic chips + search: a row of quick jumps (Greetings, Family, Food, Numbers, Travel, Work, Body, Time, Verbs, Grammar, Shopping, Idioms) pulls matching lessons from across the whole course (Food → 19 lessons, Idioms → 4). Free search also matches lesson title, English meaning, romanized sound ("chaha"), or native script. The chips are English keywords on purpose: every deck's lesson titles are in English, so the same chips work for any language added later.
  • Recommended learning path: the guided Today session follows a generic easy-to-hard order (level 1 to 4, high-utility sentence-building lessons first). Same rule for every language.

Games (Play tab)

  • Lightning: a 60-second timed comprehension blitz with combo scoring.
  • Match: pair each audio clip to its meaning.

Staying motivated (habit engine)

  • Streaks + streak freeze: daily streak with loss-aversion pull; a banked freeze saves your streak through one missed day instead of resetting to zero, and refills weekly.
  • XP + levels: every answer earns XP; XP maps to levels with titles (Newcomer → Conversational → Fluent-minded).
  • Daily goal: a per-day XP target with a progress bar.
  • Achievements: streak and volume milestones.
  • Per-lesson mastery: each lesson shows how many of its words have reached durable long-term memory (a mastery bar).
  • Celebrations: confetti and floating XP on correct answers.
  • Share: share your streak/XP via the system share sheet.

Audio & voice

  • Baked audio, cross-browser: pre-rendered .mp3 clips (Microsoft neural voices via free edge-tts) play identically in every browser and offline. The app reads audio/manifest.json, prefers the baked clip, and falls back to live browser TTS if one is missing.
  • Romanization (Latin letters), on by default: on-screen text is the romanized sound (e.g. "namaste"), so nobody has to read a script to speak. showRoman defaults on. Turning it off is pure-ear mode: no target-language text appears at all (not the roman, not the usage note, and never the native script as a fallback), just the English meaning and the audio. That rule lives in one shared helper, lib/reveal.ts, so web and mobile can't drift, and it's covered by unit tests plus a content-integrity test that fails CI if any lesson leaks Devanagari onto the screen. There is no reading or writing instruction anywhere in the app: no alphabet drills, no script-recognition rounds, no writing practice. The no-script rule holds on every surface, not just lessons: the AI tutor's replies, the dialogue roleplay, and even the echo of your own recognized speech all render romanized, never the native script. Titles, subtitles, and usage notes are all romanized too. (Duolingo ships this exact toggle for Chinese pinyin and Japanese furigana, but not for Hindi, which expects Devanagari from lesson one. That gap is the product's reason to exist.)
  • Speaking check: the mic uses the browser's free speech recognition to transcribe and score what you say. Defaults on on both platforms (on mobile it's currently a no-op, since mic grading is stubbed and the self-check flow runs instead). Browser speech recognition is Chrome/Edge only; Safari and Firefox get nothing, so every caller checks sttSupported() before promising it and falls back to a self-check flow that works everywhere.
  • Playback speed: adjust how fast the target audio plays (0.6x to 1.1x, default 0.9x).

Content

  • Marathi (2,884 items / 294 lessons) and Hindi (3,106 items / 274 lessons): greetings, deep vocab domains, verb conjugation, grammar, full sentences, situational dialogues, idioms/proverbs. Every item has a baked audio clip (5,990 total, 100% coverage). Registry-driven; adding a language is one module file plus one line.
  • Generative sentence frames: each deck opens with six swap-and-go patterns (I want ___ / Where is ___? / Is there ___? / I want to go to ___ / Please give ___ / How much is ___?). The frame never changes, so learning one pattern plus a noun list yields dozens of real sentences instead of dozens of memorized strings.
  • Content QA: content is LLM-authored, then reviewed pass-by-pass for gender agreement, register, and romanization consistency (124 corrections applied across both decks). This is model review, not a native speaker authoring it. See Known gaps.

Accounts & sync

  • Sign in: Google or email magic link, via Supabase (env-gated; the app runs fully on-device without keys).
  • Cloud progress sync: when signed in, streak/XP/cards sync to the cloud and merge across devices.
  • Settings: show romanization, mic speaking check, playback speed, reset progress, tutor key.

Platforms

  • Web: Vite + React + TypeScript. Runs with no backend; progress in localStorage (plus cloud when signed in).
  • Mobile: Expo + React Native, device TTS + AsyncStorage. Builds for the App Store / Play Store via EAS.

Mobile parity

Content and core logic are byte-identical. The UI is behind:

Feature Web Mobile
Onboarding, Lesson, Review, Speak yes yes
Today (guided session) yes yes
Placement diagnostic yes yes
Talk (tutor) yes no
Dialogues roleplay yes no
Play games (Lightning, Match) yes no
Units + topic chips + search yes no (flat lesson list)

Mobile UI has never been visually verified: there's no Expo simulator in the dev environment it was written in, so it's type-checked and code-reviewed but unrun.


Learning method (the research)

  • Comprehensible input: meaning is always given in a language you know, audio-first; the tutor stays at your level using words you've seen.
  • Retrieval practice: you produce the answer before hearing it; recall beats re-reading. Missed items requeue until correct.
  • Spaced repetition: lib/srs.ts (SM-2 variant): missed items reset to short intervals, recalled items grow geometrically (1 day → 3 → a week → a month).
  • Output practice: a full ladder from controlled to free: Speak drill → Dialogues roleplay → open Talk conversation with correction.
  • Interleaving + easy→hard ordering: the guided path and drills mix topics and ramp difficulty.
  • Desirable difficulty: production is demanding on purpose; requeue prevents easy passes.

Run the web app

cd web
npm install
npm run dev        # http://localhost:5173

Build for production: npm run build (static output in web/dist; deploy to Vercel, Netlify, GitHub Pages, anywhere).

Tests

cd web
npm test            # vitest run (one shot)
npm run test:watch  # watch mode

The suite is small and high-value. content.test.ts runs against the real language registry, so it enforces the product's core rules on every shipped lesson: never display native script in any on-screen field, always keep it in target (which drives TTS), unique ids, required fields present, and no em dashes in copy. reveal.test.ts locks the romanization-toggle behavior (off = pure ear, no script fallback), and translit.test.ts covers the Devanagari-to-roman transliteration. Adding content that breaks any of these fails the build rather than reaching a learner.

Analytics (activation + retention)

Off by default. The app answers only two questions: how many people start a lesson, and how many come back the next day. That's activation and retention, the only signals that say whether Bol works for real users.

To turn it on, create a free PostHog project and set two env vars in your deploy (e.g. Vercel):

VITE_POSTHOG_KEY=phc_...          # your PostHog project API key
VITE_POSTHOG_HOST=https://us.i.posthog.com   # or eu.i.posthog.com; optional, this is the default

With no key set, lib/analytics.ts is a complete no-op: PostHog is never loaded (Vite drops it from the bundle entirely), no network, no cookies, nothing tracked. Privacy-first by design: it respects Do Not Track, sends no PII (only event names + coarse props like language code and lesson title, never a name or anything the user said or typed), disables autocapture and session recording, and is cookieless.

Events, and the questions they answer:

Event Answers
app_open Retention. PostHog's retention chart counts a return on any day a user fires an event.
landing_viewget_startedonboarding_completepractice_startedpractice_completed The activation funnel: where people drop off between landing and their first finished lesson.
practice_started / practice_completed How many actually start (and finish) a lesson, the core activation metric.
placement_completed, landing_taste_played Secondary signals (alternate activation, hero engagement).

In PostHog: build a funnel on the five funnel events for drop-off, and a retention report on app_open for come-back rate. Note: analytics is web only for now; the mobile app has no deploy target yet, so it isn't instrumented (add the PostHog React Native SDK when mobile ships).

Talk tutor key

Talk uses the Claude API (Opus 4.8) straight from the browser. Set VITE_ANTHROPIC_API_KEY in web/.env.local, or paste a key under Profile → Settings → Tutor key (kept on the device, never synced). A key in a client-side build is extractable: fine for personal use; add a small proxy before sharing the app publicly.

Accounts (Supabase)

Sign-in and cloud sync are optional and env-gated. Set VITE_SUPABASE_URL and VITE_SUPABASE_ANON_KEY in web/.env.local, create a progress table with row-level security, and add your dev/prod URL to the Supabase redirect allowlist. Without keys the app runs fully on-device.


Audio that works everywhere

cd web
TTS_PROVIDER=edge npm run audio:gen          # FREE, no key (pip install edge-tts)
# or higher-end providers:
TTS_PROVIDER=google     GOOGLE_API_KEY=...                          npm run audio:gen
TTS_PROVIDER=elevenlabs ELEVENLABS_API_KEY=... ELEVENLABS_VOICE_ID=... npm run audio:gen
TTS_PROVIDER=azure      AZURE_SPEECH_KEY=... AZURE_SPEECH_REGION=... npm run audio:gen

edge uses Microsoft's neural voices (e.g. mr-IN-AarohiNeural, hi-IN-SwaraNeural) through the open-source edge-tts package: free, no key, good quality. The bake is resumable (skips existing clips). It writes web/public/audio/<lang>/<id>.mp3 and an audio/manifest.json the app reads on startup.


Add a language

One file plus one line. The app is data-driven, and every feature comes along automatically.

  1. Create web/src/content/<lang>.ts (copy hindi.ts). Each phrase:

    { id: "hi-water", target: "पानी", roman: "paani", translation: "Water" }

    Each lesson carries a level: 1 | 2 | 3 | 4 for the guided path order and the Speak drill.

  2. Register it in web/src/content/languages.ts (available: true).

  3. Copy the content into mobile/src/content/, add a tutor persona in lib/tutor.ts, then bake audio.


Mobile app and App Store

Real Expo project. Uses the device's built-in TTS (expo-speech) and AsyncStorage.

cd mobile
npm install
npx expo start          # scan the QR with Expo Go (no Mac needed to try it)

Ship via EAS (cloud build, no Mac required):

cd mobile
npm install -g eas-cli
eas login
eas build --platform ios --profile production
eas submit --platform ios

Identifiers (com.roshan.bol), icons, and splash are configured in mobile/app.json and mobile/assets/. Mic grading on mobile is stubbed (self-check used); enable by adding expo-speech-recognition and returning true from sttSupported().


Known gaps

Honest list. Last updated 2026-07-17.

Fixed 2026-07-17 (kept here briefly for the record): the romanization toggle used to be inconsistent (one branch showed nothing, another fell back to Devanagari); usage notes ignored the toggle and hundreds of them contained native script that rendered mid-drill even with romanization on; and useSpeechCheck defaulted differently on web vs mobile. All three are resolved, the display rule is now one shared, unit-tested helper, and a content-integrity test guards against script or em dashes ever leaking back into a lesson.

Content is LLM-authored, not native-authored. It has been QA'd pass-by-pass by model review (124 corrections for gender agreement, register, romanization), which is not the same as a native speaker writing it. Highest-risk areas are idioms and register.

Audio is TTS, not human. Microsoft neural voices are good, but they are not a native speaker, and prosody on long sentences is the weak spot.

Mobile UI is unrun. Type-checked and reviewed, never rendered. See Mobile parity.

The tutor key is extractable. VITE_ANTHROPIC_API_KEY ships in a client-side bundle. Fine for personal use; needs a proxy before the app is shared publicly.

No real users yet. Every claim about what works for learners is grounded in research and reasoning, not in observed retention.


Branding

brand/ holds the vector identity: a bold, centered B letterform in a blue gradient on a white squircle (trust palette; blue #2563eb). scripts/render-icons.mjs rasterizes it into the web favicons and all mobile icon/splash PNGs. See brand/brand.md.

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages