{"slug": "show-hn-an-open-source-app-for-ielts-preparation", "title": "Show HN: An open-source app for IELTS preparation", "summary": "An independent developer released BandReady, a free, open-source, offline-first IELTS preparation app that runs as an Electron front end around a Python sidecar bound to loopback only, with no account required. BandReady includes a live AI voice examiner built on pipecat 1.5, SmallWebRTCTransport and Silero VAD, plus 496 cue cards in 108 linked card sets across 20 topics, 102 writing prompts, 12 reading tests (8 Academic, 4 General Training) over 36 passages and 480 questions, and 7 listening tests over 43 scripts with 415 questions using Kokoro TTS for on-device audio. The project states it is not affiliated with the IELTS Partners (British Council, IDP: IELTS Australia, and Cambridge University Press & Assessment) and that its AI-generated band scores are practice estimates only; the owner confirmed the live call works at a real microphone, but no automated test guards it because headless Chromium cannot establish the peer connection.", "body_md": "**BandReady**\n\n**Offline-first IELTS exam preparation for your own machine.**\n\n  All four skills, a live AI voice examiner you actually talk to, and band-descriptor scoring — running against AI models you choose, local or cloud. You supply the model; the app is free and there is no account.\n\n[**Install**](#-install-users)\n • \n  [Build from source](#-build-from-source)\n • \n  [Contributing](https://github.com/Luxshan2000/ielts-prep/blob/main/CONTRIBUTING.md)\n • \n  [Report an issue](https://github.com/Luxshan2000/ielts-prep/issues)\n\nIELTS Prep is an exam room, not a chatbot wrapper. You get authentic question formats, real timers, per-criterion band descriptors and longitudinal progress tracking. It is an Electron app around a Python sidecar that binds to loopback only, so your essays, recordings and scores stay on your disk unless you point it at a cloud provider yourself.\n\n**Not affiliated with the IELTS Partners** (British Council, IDP: IELTS Australia, and\nCambridge University Press & Assessment). IELTS is a registered trademark of its owners. This\nis an independent, unofficial project; the name describes the exam it prepares you for and\nimplies no endorsement by, or association with, the trademark holders. All practice material\nhere is original and is not official IELTS test content. Band scores produced by this software\nare AI-generated estimates for practice only and do not predict official IELTS results.\n\n- **A live voice examiner** — real-time WebRTC conversation over pipecat 1.5, SmallWebRTCTransport and Silero VAD. You speak, it answers, it follows up.\n- **Full three-part mocks** — plus single-part drills, topic drills and quick chats when you have ten minutes rather than fifteen.\n- **A scored report every time** — timed transcript, four criterion bands, quoted evidence for each judgement, and per-turn audio replay.\n- **496 cue cards** across Parts 1 to 3, in 108 linked card sets over 20 topics.\n\nThe live call has been confirmed at a real microphone by the project owner. Headless Chromium cannot establish the peer connection, so no automated test guards it.\n\n- **All three task types** — Academic Task 1 (charts, tables, processes and maps rendered from a structured chart spec), General Training Task 1 letters, and Task 2 essays.\n- **A pre-check before you spend a model call** — it catches an off-topic or too-short answer while you can still fix it.\n- **Character-anchored inline annotations** — each error is pinned to the exact offsets in your text, with a fix and an explanation.\n- **A rewrite loop and an on-demand band 8/9 model answer** , so you can see the gap rather than guess at it.\n- **102 prompts** with chart specs and band ladders.\n\n- **Full Academic and General Training tests** — 3 passages, 40 questions, 60 minutes — plus single-passage practice and question-type drills.\n- **Every IELTS question type** , auto-marked with the real matching rules: case folding, abbreviations, thousands separators, hyphen and space equivalence, word limits.\n- **A review that explains the trap** — answer key, anchoring paragraph, explanation, and a \"why was I wrong?\" analysis.\n- **12 tests** (8 Academic, 4 General Training) over 36 passages, 480 questions, 15 question types.\n\n- **Audio synthesised on your machine** — Kokoro TTS renders each script per role and per accent on first play, then caches it. A test ships as kilobytes of script, not megabytes of audio.\n- **Exam-faithful playback** — plays once, no seeking, with a proper transfer window.\n- **Per-part practice with transcript reveal** , and the same auto-marking engine as Reading.\n- **7 tests** over 43 scripts across UK, US and AU voices — 415 questions, 8 question types, including map labelling rendered from shipped SVGs.\n\n- **An FSRS bank fed by your own mistakes** — errors from your writing and speaking arrive as suggestions you accept or dismiss.\n- **Six exercise types** — flip, cloze, use-in-sentence with LLM checking, collocation, audio recall and speaking drill.\n- **A grammar syllabus, not a quiz bank** — 156 points, each naming the false rule learners are usually taught, drilled up a six-rung mastery ladder.\n- **99 reference articles** across 8 chapters that are never locked, because someone who does not know what a modal is has to be able to look it up first.\n- **An offline WordNet dictionary** for double-click lookups anywhere in the app.\n\n- **Fluency signals from your own audio** — speech rate, pause profile, filled pauses, and low-confidence word flagging.\n- **Minimal-pair perception drills and word-stress tapping** , with reference audio rendered on demand.\n- **No accent score, ever.** IELTS marks intelligibility, not how close you sound to any particular English. These signals come from speech-recognition confidence, which can tell you a word was hard to make out but cannot tell you a sound was wrong. You get flags and drills, never a pronunciation band and never the word \"mispronounced\".\n\n- **A placement test that seeds a study plan** — four band estimates become a dated plan with daily sessions that adapts to your results.\n- **Local SQLite with Alembic migrations** , so an update never means losing your history.\n- **Jobs with real progress and cancellation** — anything slower than a second returns a job id you poll.\n- **Importable and exportable `.brpack` content packs** , plus a full data export.\n- **Dark and light themes** , and keyboard-usable timers and test players.\n\n- **The sidecar binds to loopback only** and requires a per-launch bearer token. Nothing else on your machine can call it.\n- **Provider API keys are encrypted at rest** with a per-install key, masked in the UI, and redacted from every log line.\n- **Recordings and transcripts stay in your data directory.** There is no BandReady server to send them to.\n- **Cloud is opt-in and text-only for marking** — if you configure a cloud LLM, it sees the essay or transcript you are having marked, and nothing else.\n- **Model weights are never bundled and never committed.** They download on first run into your data directory, or get hard-linked from caches you already have.\n\nNot captured yet — they land with the first tagged release rather than sitting here as six broken image icons.\n\n| Screen | File to drop in | Screen | File to drop in | \n|---|---|---|---|\n| **Dashboard** — band trend, streak, today's session | `docs/screenshots/dashboard.png` | **Speaking** — a live session mid-turn | `docs/screenshots/speaking.png` | \n| **Writing** — the desk with an annotated evaluation | `docs/screenshots/writing.png` | **Reading** — the test player and palette | `docs/screenshots/reading.png` | \n| **Listening** — a test part with audio playing | `docs/screenshots/listening.png` | **Vocabulary** — the review player | `docs/screenshots/vocab.png` | \n\n[`docs/screenshots/README.md`](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/screenshots/README.md) holds the capture contract.\n\nBuilding from source is the supported route today, and it takes about fifteen minutes on a fast connection. Two ways in:\n\n1. **Build it from source.** This is the supported path, and it takes about fifteen minutes on a\nfast connection. Jump to[Build from source](#-build-from-source) .\n2. **Run the release workflow yourself.**[`.github/workflows/release.yml`](https://github.com/Luxshan2000/ielts-prep/blob/main/.github/workflows/release.yml) builds on Windows and macOS\nrunners and attaches installers to a GitHub pre-release. It is`workflow_dispatch` only, so\nyou need push access to this repository or to your own fork.\n\nWhichever route you take, the first launch needs a network connection so it can fetch model weights (roughly 340 MB for the voice, more if you want local speech-to-text). Everything after that runs offline.\n\nBuilds are unsigned, so both systems ask once before they will run a new app. This is what they ask and what to press.\n\n- **macOS** — open**System Settings → Privacy & Security** , scroll to the message about the app\nbeing blocked, and click**Open Anyway** . Right-click then Open no longer works on macOS 15.\n- **Windows** — SmartScreen shows \"Windows protected your PC\". Click**More info** , then**Run anyway** .\n\nAfter that first launch neither system asks again.\n\n**Prerequisites**\n\nTool Version Why [Node.js](https://nodejs.org)20 or newer Electron shell and Vite renderer [pnpm](https://pnpm.io)9 ( `corepack enable` pins 9.12.0)workspace package manager [Python](https://python.org)3.11 the sidecar [uv](https://docs.astral.sh/uv/)latest Python environment and lockfile \n\n**1. Clone and install the JavaScript workspace.**\n\n```\ngit clone https://github.com/Luxshan2000/ielts-prep.git\ncd bandready\npnpm install\n```\n\n**2. Install the Python sidecar.**\n\n```\ncd sidecar && uv sync --extra dev --extra voice && cd ..\n```\n\nThe `voice` extra is large — pipecat, torch and faster-whisper come to roughly 2 to 3 GB of\nwheels — but it is what makes the live speaking module work. Drop `--extra voice` for a\ntext-only install and everything except live speaking still runs.\n\n**3. Start everything.**\n\n```\nnode scripts/dev.mjs\n```\n\nThat spawns the sidecar on a random loopback port, waits for the health handshake, starts Vite, and opens Electron. Useful variants:\n\n```\nnode scripts/dev.mjs --browser        # no Electron: fixed-port sidecar + Vite in a plain\n                                      # browser (this is the mode the E2E suite drives)\nnode scripts/dev.mjs --no-mock        # use your real configured providers instead of mocks\nnode scripts/dev.mjs --port 5273 --sidecar-port 8710\n```\n\n**4. Run the tests** (optional, but it tells you the install is sound).\n\n```\nuv run --project sidecar pytest sidecar/tests/ -q      # 1,352 tests\nuv run --project sidecar ruff check sidecar/bandready sidecar/tests\ncd app && pnpm exec tsc --noEmit -p tsconfig.json && pnpm test\n```\n\n**5. Package an installer** (optional). The bundle carries a relocatable CPython plus the\nsidecar's venv, because the app has to run on a machine with no Python.\n\n```\nnode scripts/stage-sidecar.mjs --arch arm64 --voice\npnpm --filter bandready-app build\nnode scripts/build-electron.mjs\ncd app && pnpm exec electron-builder --config electron-builder.yml --mac dmg --arm64\n```\n\nInstallers land in the repository-root `dist-electron/`. A macOS arm64 DMG comes to roughly\n156 MB without the voice extra. `stage-sidecar.mjs` builds the venv for the *host* platform, so\na Windows installer cannot be produced from a Mac — that is what the release workflow is for.\n\nBandReady never ships a model or an API key. You pick exactly one LLM, one STT and one TTS under\n**Settings → Providers**. The screen detects what is already running on your machine and offers\nit first, and **Verify** sends one real request and lists the models the endpoint actually\nserves.\n\n| Preset | Modalities | Kind | Key | Notes | \n|---|---|---|---|---|\n| **Ollama** | LLM | local server | none | The easy local start. `ollama pull qwen3:14b` , base URL`http://127.0.0.1:11434/v1` . | \n| **OpenRouter** | LLM, STT, TTS | cloud | `sk-or-...` | One key covers all three. Base URL is locked to `https://openrouter.ai/api/v1` . | \n| **Local Whisper** | STT | in-process | none | faster-whisper, int8 on CPU by default. Required for pronunciation feedback. | \n| **MLX Whisper** | STT | in-process | none | Apple Silicon only. `large-v3-turbo` runs realtime on an M-series chip. | \n| **Kokoro** | TTS | in-process | none | An 82M ONNX voice model. The default everywhere, and what renders listening audio. | \n| **Custom OpenAI-compatible** | LLM, STT, TTS | anything | optional | Point it at OpenAI, Groq, DeepSeek, `mlx_lm.server` ,`llama.cpp` , or your own gateway. Every field is editable. | \n\nTwo things are worth knowing. Model fields are closed dropdowns wherever the endpoint can be\nasked what it serves — a plausible-looking model name that 404s deep inside a provider surfaces\nthree screens away as \"the practice engine reported an error\", and that is a bad afternoon for\nsomeone who came here to practise English. And pronunciation feedback needs *local* Whisper: a\nremote transcript carries no per-word confidence, so there is nothing for the fluency analysis\nto read.\n\n**Model weights are never bundled.** On first run, onboarding lists what your chosen presets\nneed and downloads it into `<data dir>/models/` as a resumable, cancellable background job.\n\n| Artifact | Used by | Size | \n|---|---|---|\n| Kokoro TTS v1.0 ( `.onnx` plus voice pack) | Listening audio, the examiner's voice | ~340 MB | \n| faster-whisper `base` /`small` /`large-v3-turbo` | Local speech-to-text | 145 MB / 484 MB / 1.6 GB | \n| MLX Whisper large-v3-turbo | Local speech-to-text on Apple Silicon | ~1.5 GB | \n| English WordNet | Offline dictionary lookups | ~30 MB | \n\nIf you already have those files, the sidecar finds and **hard-links** them at startup rather\nthan downloading again — it checks `~/.cache/pipecat/kokoro-onnx/`, `~/.cache/huggingface/hub/`,\n`$HF_HOME`, and anything on `BANDREADY_MODEL_SEARCH_PATH`. Adoption is instant, costs no extra\ndisk, and leaves the original untouched. Set `BANDREADY_ADOPT_LOCAL_MODELS=0` to keep BandReady\nout of your caches entirely.\n\nSet the API key field to the literal `${OPENROUTER_API_KEY}` (any `${VAR}` works) and the key is\nread from the environment at request time instead of being stored:\n\n```\nexport OPENROUTER_API_KEY='sk-or-...'\nnode scripts/dev.mjs\n```\n\nOnly variables whose names end in `_API_KEY` reach the sidecar. A missing variable produces a\nprecise error naming it, never a silent auth failure. On macOS an app launched from Finder\ninherits nothing from your shell, so put the value in `<data dir>/.env` instead — or just paste\nthe key into Settings, where it is encrypted at rest with a per-install key.\n\n| OS | Path | \n|---|---|\n| macOS | `~/Library/Application Support/BandReady` | \n| Windows | `%APPDATA%\\BandReady` | \n| Linux | `$XDG_DATA_HOME/BandReady` (default`~/.local/share/BandReady` ) | \n\nOne SQLite database, your settings, the encryption key, downloaded models, generated audio, voice recordings and installed content packs all live under that one directory. Deleting it resets the app completely.\n\n- **[CONTRIBUTING.md](https://github.com/Luxshan2000/ielts-prep/blob/main/CONTRIBUTING.md)** — dev setup, the module map, and how to add a route or\na screen. The one document to read before your first change.\n- **[SECURITY.md](https://github.com/Luxshan2000/ielts-prep/blob/main/SECURITY.md)** — the security model, what stays local, and how to report a\nvulnerability privately.\n- **[CODE_OF_CONDUCT.md](https://github.com/Luxshan2000/ielts-prep/blob/main/CODE_OF_CONDUCT.md)** — Contributor Covenant 2.1.\n- **[CHANGELOG.md](https://github.com/Luxshan2000/ielts-prep/blob/main/CHANGELOG.md)** — what has changed, in Keep a Changelog format.\n- **[docs/IMPLEMENTATION-STATUS.md](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/IMPLEMENTATION-STATUS.md)** — what is built, what is\nnot, and the evidence for each claim. The most honest file in the repository.\n- **[docs/DEVELOPMENT.md](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/DEVELOPMENT.md)** — dev-mode architecture, debugging, log and\ndata locations, resetting local state.\n- **[docs/REPOSITORY.md](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/REPOSITORY.md)** — the tree around the code: what is tracked, what\nis generated, and which docs to trust.\n- **The content banks** , one document each — what ships, the schema, and how to author more:[reading](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/READING-CONTENT.md) ·[listening](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/LISTENING-CONTENT.md) ·[speaking](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/SPEAKING-CONTENT.md) ·[grammar and vocabulary](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/GRAMMAR-VOCAB.md) ·[theory](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/THEORY-CONTENT.md) . Writing has no such document yet.\n- **[docs/plan/README.md](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/plan/README.md)** — the index to twenty-two numbered design\ndocuments written before implementation began. This is design*intent* , not status. Much of it\nshipped differently, and where the plan and the code disagree the code is right.\n\n```\nbandready/\n├── app/                      Electron shell + React renderer (pnpm workspace \"bandready-app\")\n│   ├── electron/             main, preload, sidecar spawn/health/teardown — the Node side\n│   ├── src/features/<name>/  one directory per screen: route.tsx, page.tsx, store.ts\n│   ├── src/components/ui/    the shared design-system kit\n│   └── build/                electron-builder buildResources — NOT build output\n├── sidecar/bandready/        the Python FastAPI backend\n│   ├── server/routes/        one module per API family, auto-discovered at startup\n│   ├── server/auth.py        loopback guard, origin guard, bearer token\n│   ├── security/secrets.py   credential encryption and log redaction\n│   ├── scoring/ srs/ curriculum/ voice/ audio/ pron/ providers/\n│   └── db/ migrations/       SQLAlchemy 2.0 models + Alembic\n├── content/core-en/          the shipped CC0 content pack, plus per-module authoring trees\n├── tools/content/            the pack pipeline CLIs: merge_*.py, then build.py, then validate.py\n├── scripts/                  dev.mjs, build-electron.mjs, stage-sidecar.mjs\n├── e2e/                      Playwright specs, driven against a real sidecar in browser mode\n└── docs/                     everything above, plus the numbered design set docs/plan/00 to 18\n```\n\n**Two seams are auto-discovered, and using them never means editing a registry.**\n\n- **A sidecar route** is a new file in`sidecar/bandready/server/routes/` exposing a module-level`router` .`discover_routers()` imports every module in that package at startup; one that fails\nto import is logged and skipped, so a half-finished feature cannot stop the app booting.\n- **A screen** is a new`app/src/features/<name>/route.tsx` that default-exports`defineFeatureRoute(...)` .`App.tsx` finds it with`import.meta.glob` , and the sidebar is\nbuilt from the same objects.\n\nEditing `server/app.py`, `App.tsx` or `Sidebar.tsx` to register something is a mistake, not a\nshortcut. [CONTRIBUTING.md](https://github.com/Luxshan2000/ielts-prep/blob/main/CONTRIBUTING.md) sections 3 and 4 have a working template for each.\n\nOne collision to know about: repository-root `build/` is packaging output and is gitignored,\nwhile `app/build/` is *tracked* electron-builder resources. Likewise root `dist-electron/` holds\ninstallers and `app/dist-electron/` holds compiled bundles. The `.gitignore` rule is anchored\nwith a leading slash for exactly that reason.\n\nBug reports, content fixes and pull requests are welcome.\n[CONTRIBUTING.md](https://github.com/Luxshan2000/ielts-prep/blob/main/CONTRIBUTING.md) has the setup, the house rules, the two seams and the content\npipeline. Everyone taking part is expected to follow the\n[Code of Conduct](https://github.com/Luxshan2000/ielts-prep/blob/main/CODE_OF_CONDUCT.md).\n\nThe most useful thing you can send today is a wrong answer key, a broken question, or a report that the app does not boot on your platform. The shipped keys were authored and machine-validated, and nobody has independently checked them.\n\n**Does it really work offline?**\nYes, once the models are on disk. The examiner, the transcription and the listening audio all\nrun locally, and the database is a file on your machine. Three exceptions, all of them things you\ncan see: the first run downloads model weights, any cloud provider you configure gets what you\nsend it, and a *packaged* build asks GitHub Releases every six hours whether a newer version\nexists. That last one sends nothing about you, and it is off entirely in a dev build.\n\n**Does my voice leave the machine?**\nNot by default. Recordings are written to `<data dir>/media/speaking/<session id>/` and stay\nthere. If you choose a cloud speech-to-text provider, your audio goes to that provider — that is\nwhat choosing it means. With local Whisper selected, no audio ever leaves.\n\n**Do I need an API key?**\nNo, if you run everything locally: Ollama for the examiner, faster-whisper for speech-to-text,\nKokoro for the voice. Yes, if you would rather use OpenRouter or another cloud endpoint. There is\nno BandReady account either way.\n\n**Is it free?**\nThe software is MIT-licensed and the content is CC0. There is no subscription and nothing is\nmetered. If you point it at a paid API you pay that provider directly — writing evaluation\nagainst a mid-size model measured at roughly $0.0002 per essay.\n\n**How accurate are the band scores?**\nNot proven. They are spot-checked against a real model (a strong essay scored 8.0, a weak one\n5.0, both plausible) and have never been calibrated against expert-marked samples. Treat them as\ndirectional feedback on your writing, not as a predicted result.\n\n**What is missing?**\nCode signing and notarization, any Windows or Linux build that a human has run, automated\ncoverage of the live voice call, scoring calibration, and an independent check of the answer\nkeys. [docs/IMPLEMENTATION-STATUS.md](https://github.com/Luxshan2000/ielts-prep/blob/main/docs/IMPLEMENTATION-STATUS.md) lists the rest without\nflinching.\n\n**Is this affiliated with IELTS?**\nNo. See the notice near the top. All practice material here is original and none of it is\nofficial test content.\n\nBandReady is licensed under the [MIT License](https://github.com/Luxshan2000/ielts-prep/blob/main/LICENSE) — copyright © 2026 Luxshan Thavarasa.\n\nThe first-party practice content under `content/` is released separately as **CC0-1.0**, so you\ncan reuse, remix or ship it however you like. Models you download are covered by their own\nlicences, and BandReady redistributes none of them.\n\nOne caveat for anyone redistributing a build: the local TTS engine `kokoro-onnx` depends on\n`phonemizer-fork` and `espeakng-loader`, which carry GPL-3.0 terms. If you ship a binary that\nbundles the voice extra, the licence of that combined work is a question you need to answer for\nyourself. The source in this repository is MIT.", "url": "https://wpnews.pro/news/show-hn-an-open-source-app-for-ielts-preparation", "canonical_source": "https://github.com/Luxshan2000/ielts-prep", "published_at": "2026-10-29 18:00:42+00:00", "updated_at": "2026-10-29 18:17:33.604303+00:00", "lang": "en", "topics": ["ai-tools", "ai-products", "artificial-intelligence"], "entities": ["BandReady", "IELTS", "British Council", "IDP: IELTS Australia", "Cambridge University Press & Assessment", "pipecat", "Silero VAD", "Kokoro TTS"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-an-open-source-app-for-ielts-preparation", "markdown": "https://wpnews.pro/news/show-hn-an-open-source-app-for-ielts-preparation.md", "text": "https://wpnews.pro/news/show-hn-an-open-source-app-for-ielts-preparation.txt", "jsonld": "https://wpnews.pro/news/show-hn-an-open-source-app-for-ielts-preparation.jsonld"}}