Echoline is a local web app for sentence-by-sentence English listening dictation. Upload an audio file, let whisper.cpp generate transcript segments, then practice by listening to one sentence, writing what you hear, repeating the sentence, and revealing the answer when ready.
It is a general English listening tool, not a CET-only app. It works naturally with exam audio, VOA, TED, podcasts, course recordings, and custom materials.
- Upload local audio and transcribe it through a local
whisper.cppserver. - Convert raw Whisper fragments into natural sentence-level practice units.
- Preserve sentence timestamps for precise replay, restart, previous, and next controls.
- Hide or reveal the current sentence answer during dictation practice.
- Mark each sentence as practiced or difficult.
- Save dictation text and progress in browser local storage.
- Use visible controls or desktop shortcuts for high-frequency listening practice.
Space: play / pause.ArrowLeft: restart the current sentence; if already near the start, jump to the previous sentence.ArrowRight: jump to the next sentence.R: repeat the current sentence.H: show / hide the current sentence answer.
Shortcuts are ignored while typing in inputs, textareas, selects, or editable content.
- Node.js and npm.
- CMake and a C/C++ compiler.
ffmpegavailable onPATH.
On macOS with Homebrew:
brew install cmake ffmpegInstall web dependencies:
npm installCreate local configuration:
cp .env.example .envIf you cloned the repository without submodules, initialize whisper.cpp first:
git submodule update --init --recursiveStart Echoline and the bundled whisper.cpp server:
npm run start:allOpen:
http://127.0.0.1:5173
Stop both services:
npm run stop:allThe shell scripts can also be run directly:
./scripts/start.sh
./scripts/stop.shRuntime configuration lives in .env. The file is intentionally ignored by Git; .env.example is the committed template.
VITE_WHISPER_BASE_URL=http://127.0.0.1:8080
VITE_WHISPER_ENDPOINT_STYLE=whispercpp
WHISPER_HOST=127.0.0.1
WHISPER_PORT=8080
WHISPER_MODEL=whisper.cpp/models/ggml-tiny.en.bin
WHISPER_MODEL_NAME=tiny.en
WHISPER_THREADS=6
WHISPER_LANGUAGE=en
WHISPER_EXTRA_ARGS=
WEB_HOST=127.0.0.1
WEB_PORT=5173Important fields:
VITE_WHISPER_BASE_URL: target used by Vite's same-origin proxy.VITE_WHISPER_ENDPOINT_STYLE:whispercpptries/inferencefirst;openaitries/v1/audio/transcriptionsfirst.WHISPER_MODEL: local model file used bywhisper-server.WHISPER_MODEL_NAME: model name downloaded automatically ifWHISPER_MODELis missing.WHISPER_EXTRA_ARGS: optional extra flags passed towhisper-server.
The default model is tiny.en, which starts quickly but is less accurate. For better transcription quality:
WHISPER_MODEL=whisper.cpp/models/ggml-base.en.bin
WHISPER_MODEL_NAME=base.enThen run npm run start:all; the script downloads the configured model if needed.
scripts/start.sh keeps the local runtime self-contained:
- Creates
.envfrom.env.exampleif needed. - Uses the
whisper.cppsubmodule in the project root. - Builds
whisper.cpp/build/bin/whisper-serverif missing. - Rebuilds
whisper.cppif an old build still points at a stale project path. - Downloads the configured
ggmlmodel if missing. - Starts
whisper-serverand waits for/health. - Starts the Vite web app and waits for the local URL.
Build output, downloaded models, logs, and pid files are ignored:
whisper.cpp/build/
whisper.cpp/models/ggml-*.bin
logs/
.run/
whisper.cpp often returns acoustic chunks instead of complete written sentences. Echoline post-processes the response before showing practice units:
- Split coarse segments that contain multiple sentences.
- Merge adjacent fragments until a natural sentence boundary appears, usually
.,?, or!. - Avoid common false boundaries such as decimal numbers and common English abbreviations.
- Preserve the start time of the first merged fragment and the end time of the last merged fragment.
- Fall back to sentence splitting with approximate timings if Whisper returns text without timestamps.
This keeps the practice flow close to: listen to one sentence, write one sentence, repeat, then compare.
Echoline/
asset/ README screenshot and project assets
src/ React app source
scripts/start.sh Start web + whisper.cpp
scripts/stop.sh Stop web + whisper.cpp
whisper.cpp/ whisper.cpp Git submodule
.env.example Local configuration template
whisper.cpp is tracked as a Git submodule because it is an upstream third-party project. Fresh clones should use:
git clone --recurse-submodules https://github.com/Neurocoda/Echoline.gitnpm test
npm run lint
npm run buildManual Whisper request when the local server is running:
curl http://127.0.0.1:8080/inference \
-F "file=@/absolute/path/to/audio.mp3" \
-F response_format=verbose_json \
-F temperature=0.0 \
-F temperature_inc=0.2Health checks:
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:5173/api/whisper/health