A fully local, GPU-accelerated speech recognition system that transcribes Arabic and English β including audio that switches between the two β without sending a single byte to the cloud.
Developed for the ESKADENIA Software Student Challenge β AI Speech-to-Text Solution.
ESKADENIA STT is an offline speech-to-text platform purpose-built for bilingual Arabic/English speech. It runs entirely on a single consumer GPU (developed and validated on an NVIDIA RTX 4060 8 GB), which means:
- π Privacy by design β audio never leaves the machine. No external API, no internet dependency at inference time.
- π Arabic and English β a single multilingual engine handles both languages, and correctly routes each utterance to the right one.
- π Code-switching aware β it detects and tags audio that alternates between Arabic and English within the same recording.
- β‘ Real-time β live microphone streaming with sub-real-time latency, plus batch file transcription.
The system is delivered as a web application: a FastAPI inference backend and a modern React frontend, launched together with a single double-click.
| Feature | Description |
|---|---|
| π File Transcription | Upload any audio file and get a full transcript with per-segment timestamps, detected language, and confidence. |
| π Mixed-Language Mode | Splits the audio on silence, identifies the language of each segment independently, and tags the output [AR] / [EN]. |
| π΄ Live Streaming | Real-time transcription straight from the microphone over a WebSocket, with partial and finalized segments. |
| π Built-in Evaluation | An in-app benchmark screen computes WER / CER against reference transcripts. |
| ποΈ Optional Denoising | DeepFilterNet noise suppression for low-quality recordings. |
| π‘οΈ AR/EN Hard Lock | The engine is constrained to only ever output Arabic or English β hallucinated third languages are structurally impossible. |
flowchart LR
subgraph Client["π₯οΈ Frontend β React + Vite (port 3000)"]
UI["Home Β· Upload Β· Live Β· Evaluate"]
end
subgraph Server["βοΈ Backend β FastAPI (port 8000)"]
API["REST /transcribe Β· WS /stream Β· /health"]
PIPE["Pipeline: load β denoise β VAD β LID β route β transcribe"]
end
subgraph Models["π§ AI Models (local, GPU)"]
LID["ECAPA VoxLingua107<br/>(fast language ID)"]
ASR["Whisper large-v3<br/>(faster-whisper Β· int8_float16)"]
end
UI -- "HTTP / WebSocket" --> API
API --> PIPE
PIPE --> LID
PIPE --> ASR
βββββββββββββββ
audio βββΆ load βββΆβ denoise? ββββΆ trim silence βββΆ ββββββββββββββββββββ
βββββββββββββββ β Language ID β
β (2-stage) β
ββββββββββ¬ββββββββββ
β
ββββββββββββββββββββββββββββββββββββββ€
βΌ βΌ
confident ar/en uncertain
β β
βΌ βΌ
force-decode in auto-decode, then
that language verify guess β {ar,en}
ββββββββββββββββ¬ββββββββββββββββββββββ
βΌ
Whisper large-v3
βΌ
transcript + timestamps + RTF
Two-stage language identification. A lightweight ECAPA model gives a fast first guess; when it isn't confident enough (short or ambiguous audio), Whisper's own detector arbitrates. If even that lands outside {ar, en}, the system re-decodes forced to the closer of the two β this is what makes the Arabic/English-only guarantee hold.
| Dashboard | Studio (file upload) |
|---|---|
![]() |
![]() |
| Live | Evaluate |
|---|---|
![]() |
![]() |
| Layer | Technologies |
|---|---|
| Frontend | React 19, TypeScript, Vite 6, TailwindCSS 4, Motion |
| Backend | Python 3.11, FastAPI, Uvicorn, WebSockets |
| Speech Engine | faster-whisper (CTranslate2) Β· Whisper large-v3 Β· int8_float16 |
| Language ID | SpeechBrain ECAPA-TDNN (VoxLingua107) |
| Audio | Silero VAD, DeepFilterNet, SoundFile, SciPy |
| Compute | PyTorch 2.5.1 + CUDA 12.1 (NVIDIA RTX 4060 8 GB) |
Ahmad Arafat GP2 Project/
βββ Start ESKADENIA SST.bat # one-click launcher (backend + frontend + browser)
β
βββ backend/ # FastAPI inference server
β βββ main.py # entry point (uvicorn on :8000)
β βββ config.yaml # models, thresholds, AR/EN constraint
β βββ settings.py # typed config loader
β βββ api/ # server.py (routes) + schemas.py (wire format)
β βββ preprocessing/ # audio_loader, vad, denoise
β βββ language_id/ # lightweight (ECAPA) + whisper_fallback + decision
β βββ transcription/ # router + transcriber (faster-whisper)
β βββ file_pipeline.py # whole-file & mixed-language pipelines
β βββ streaming/live_pipeline.py # real-time streaming + accent arbitration
β βββ tests/ # runnable checks + benchmark_accuracy.py
β
βββ frontend/ # React + Vite web UI
βββ src/
βββ App.tsx # screen router
βββ api.ts # REST + WebSocket client
βββ components/ # Home Β· Upload Β· Live Β· Evaluate screens
- Windows with an NVIDIA GPU (CUDA 12.1 capable) β CPU also works, slower
- Python 3.11
- Node.js 18+
# create an isolated virtual environment (kept OUTSIDE the project / OneDrive)
python -m venv C:\dev\gp2-venv
C:\dev\gp2-venv\Scripts\activate
# install pinned dependencies (CUDA build of PyTorch is resolved automatically)
pip install -r backend/requirements.txt
β οΈ The versions inrequirements.txtare pinned deliberately (torch + CUDA, speechbrain, numpy<2). Upgrading casually has broken CUDA and the ECAPA import in the past.
cd frontend
npm installDouble-click Start ESKADENIA SST.bat in the project root. It launches the backend, the frontend, and opens http://localhost:3000 for you.
The backend needs ~1 minute to load the models on first start. The βEngineβ badge in the UI turns green when it's ready.
# terminal 1 β backend
cd backend && python main.py # β http://localhost:8000
# terminal 2 β frontend
cd frontend && npm run dev # β http://localhost:3000backend/whisper-ct2-finetuned/model.bin is not checked into git (.gitignore: *.bin,
~1.5 GB). A fresh git clone will have every file except the model weights. To make
the clone runnable:
- Get the fine-tuned weights. Obtain
whisper-ct2-finetuned.zip(distributed separately β seereproducibility-package/) and extract it intobackend/whisper-ct2-finetuned/so you end up with:backend/whisper-ct2-finetuned/ βββ config.json βββ model.bin βββ preprocessor_config.json βββ tokenizer.json βββ vocabulary.json - Verify integrity against
reproducibility-package/CHECKSUMS.txt:Get-FileHash backend\whisper-ct2-finetuned\model.bin -Algorithm SHA256
- Install pinned dependencies exactly as in Getting Started above
(
pip install -r backend/requirements.txt,npm install). - Launch via
Start ESKADENIA SST.bat(Windows) or the manual two-terminal steps above./healthreturning{"status": "ok", ...}confirms the model loaded correctly.
For the full technical review package β exact base-model/dataset references,
LoRA hyperparameters, the training script, dependency-version caveats, the
deterministic held-out evaluation split, and checksums β see
reproducibility-package/REPRODUCIBILITY.md.
The 200-clip held-out evaluation split used for the WER numbers below can be
regenerated independently with
reproducibility-package/generate_eval_manifest.py
(fixed seed, fully deterministic).
Hit and fixed all three while validating this reproducibility package on a freshly formatted/reinstalled Windows machine β worth checking first if a clone "should work" but doesn't:
ssl.SSLCertVerificationErroron first backend start. The Silero VAD load (torch.hub.load(...)) and other first-run model downloads use rawurllib/ssl, which relies on the OS certificate trust store β incomplete on a fresh Windows install (pip installworks fine regardless, since pip vendors its owncertifibundle). Fix: pointSSL_CERT_FILEandREQUESTS_CA_BUNDLEat the venv's certifi bundle β(open a new terminal afterward so the variables take effect).setx SSL_CERT_FILE "C:\dev\gp2-venv\Lib\site-packages\certifi\cacert.pem" setx REQUESTS_CA_BUNDLE "C:\dev\gp2-venv\Lib\site-packages\certifi\cacert.pem"
FileNotFoundError: [WinError 2]fromsubprocess.runinsideaudio_loader.py.ffmpegis a system binary, not a pip package, sopip install -r requirements.txtnever installs it. Fix:winget install --id Gyan.FFmpeg -e(adds it to PATH automatically; restart any already-running backend process afterward so it picks up the new PATH).POST /transcribefeels ~2-3x slower than the documented RTF./healthreturning 200 doesn't mean uploads are fast β Windows Defender's real-time protection scans every freshly written file on first access, and each upload is written to disk before processing. Confirmed by comparing an existing file (fast, matches documented RTF) against a freshly-written copy of the exact same bytes (~0.6-0.9s slower, regardless of which folder it landed in). Fix: uploads are written to a project-localbackend/tmp_uploads/folder (not the OS temp dir) specifically so a narrow exclusion can be scoped to just that folder, rather than excluding all of%TEMP%(which would weaken AV coverage for every other app) or disabling Defender outright:# run in an elevated (Administrator) PowerShell Add-MpPreference -ExclusionPath "<repo>\backend\tmp_uploads"
The FastAPI server exposes three endpoints (interactive docs at http://localhost:8000/docs):
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Engine status, device, compute type, loaded models. |
POST |
/transcribe |
Multipart file upload. Optional form fields: language (ar/en, omit to auto-detect), mixed (per-segment language tagging), denoise. |
WS |
/stream |
Send 16 kHz mono float32 PCM chunks; receive JSON transcription segments in real time. |
Example β file transcription:
curl -X POST http://localhost:8000/transcribe \
-F "file=@meeting.wav" \
-F "mixed=true"Response (abridged):
{
"text": "[AR] Ω
Ψ±ΨΨ¨Ψ§ ΩΩΩ ΨΨ§ΩΩ [EN] let's start the demo",
"detected_language": "mixed",
"duration": 6.42,
"rtf": 0.31,
"model_used": "large-v3",
"segments": [
{ "start": 0.0, "end": 2.1, "language": "ar", "lid_confidence": 0.94, "text": "Ω
Ψ±ΨΨ¨Ψ§ ΩΩΩ ΨΨ§ΩΩ" },
{ "start": 2.4, "end": 6.4, "language": "en", "lid_confidence": 0.88, "text": "let's start the demo" }
]
}
rtf= Real-Time Factor (processing time Γ· audio duration). Values below1.0mean faster than real time.
All tunables live in backend/config.yaml β no code changes required:
models:
whisper: large-v3
language_id: speechbrain/lang-id-voxlingua107-ecapa
api:
max_upload_mb: 10 # hard cap on POST /transcribe uploads (Studio only, not /stream)
max_concurrent_transcriptions: 1 # one GPU, one model instance β extra requests queue
language_id:
confidence_threshold: 0.7 # trust ECAPA alone above this
uncertain_threshold: 0.55 # below this β safe auto-decode
languages: [ar, en] # HARD CONSTRAINT β only these two, everEvery component ships with a self-contained, dependency-free check β each script prints PASS / FAIL, no test framework required:
cd backend
python tests/test_environment.py # CUDA / torch / model availability
python tests/test_language_id.py # ECAPA + Whisper LID accuracy
python tests/test_transcription.py # ASR sanity
python tests/test_api.py # HTTP + WebSocket round-tripAccuracy benchmark β computes WER / CER and LID accuracy over a labelled sample set:
python tests/benchmark_accuracy.pyReal recordings and the developer's own voice dumps live in backend/tests/audio_samples/ and backend/debug_dumps/, kept out of any training set so evaluation stays honest.
A few of the harder problems solved along the way:
- π― Arabic-accented English. Strongly accented English fools every pre-transcription language detector β even Whisper labels it "Arabic" with 0.9+ confidence. The live pipeline solves this with accent arbitration: it runs two cheap decode probes (one per language) and picks the winner by decode quality rather than by any detector. Measured on the developer's hand-labeled recordings (identical clip set for both), this lifted live language-ID accuracy on the developer's accented speech from 13% to 75%.
Scope note (multi-speaker testing): accent arbitration currently runs only in the Live pipeline. Testing file uploads (Studio) with other speakers' accented English showed the same mislabeling β a decode-quality-based fix was prototyped for Studio too (46.7% β 67.2% language-tag accuracy on 122 labelled real recordings) but costs 2 extra decode passes per file, which roughly doubles Studio latency on short clips. Given file transcription doesn't need live's sub-second responsiveness but the extra wait was still judged not worth it for the accuracy gained, the fix was kept live-only; Studio still forces whatever the two-stage LID guesses.
- π« No hallucinated languages. Off-the-shelf Whisper auto-detection occasionally "detects" a random language (e.g. Norwegian) on unclear audio and produces a fake translation. The AR/EN hard lock verifies every detection against
{ar, en}and re-decodes forced to the nearest of the two. - β‘ Warm models. All models are pre-loaded on startup so no user request β or live stream β ever pays the cold-start cost.
- Within-utterance code-switching fine-tune β a LoRA fine-tune of Whisper large-v3 on the
MohamedRashad/arabic-english-code-switchingdataset (trained on Kaggle, deployed locally as a CTranslate2 model) to handle Arabic and English mixed inside a single sentence. See results below. - Speaker diarization β βwho spoke whenβ labelling (deliberately postponed to keep the core robust first).
- Levantine accent adaptation β additional Jordanian/Palestinian data to further improve dialect handling.
The enhancement track targets the one remaining weakness: sentences that mix Arabic and English inline (e.g. "Ψ¨Ψ―Ω Ψ£ΨΉΩ Ω Deploy ΩΩΩ Backend"). The approach:
- LoRA adapters on Whisper large-v3 (only ~2% of parameters trained) β trained on Kaggle's free dual-T4 GPUs.
- Merge β convert to CTranslate2 (
int8_float16) β run 100% locally with the samefaster-whisperengine already in production. - Evaluate fine-tuned vs. base WER on a held-out split to prove the improvement before shipping.
Results β measured on 200 held-out code-switched clips (never seen during training):
| Model | WER β | Word Accuracy β |
|---|---|---|
| Base Whisper large-v3 | 47.6% | 52.4% |
| Fine-tuned (LoRA) | 21.9% | 78.1% |
The fine-tune cut the Word Error Rate by more than half β a 25.7-point absolute drop (β54% relative) on mixed Arabic/English speech, training only 3.6% of the parameters.
Language-ID accuracy β perfect on clean, balanced speech; the harder, honest test is the developer's own accented English, fixed via accent arbitration (see Engineering Highlights):
Only training uses the cloud; inference remains fully offline, consistent with the project's privacy-first design.
Ahmad Arafat & Rashid Shehadeh β Graduation Project (GP2) Developed for the ESKADENIA Software Student Challenge.








