A web application that corrects English grammar and explains every edit it makes. A Groq-hosted LLM produces the corrected sentence, and ERRANT aligns the original against the correction to label each edit with a linguistic error type — so you see what changed and why, not just a rewritten paragraph.
- Backend: Django 5.2 + Django REST Framework
- Frontend: React 19 + Vite 7 + Tailwind CSS 3
- LLM: Groq API (
llama-3.1-8b-instantby default) - Annotation: ERRANT 3 + spaCy
en_core_web_sm - Database: SQLite by default — no database server required
- Quick start
- Running with Docker
- Configuration
- API reference
- Testing and linting
- Privacy
- Limitations
- Project layout
- Licence
Requirements: Python 3.12+, Node.js 20+, and a free Groq API key.
cd backend
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
# Sentence tokenizer data (~40 MB)
python -m nltk.downloader punkt punkt_tab
cp .env.example .env # then set GROQ_API_KEY and DJANGO_DEBUG=1
python manage.py migrate
python manage.py runserverThe API is now on http://localhost:8000.
The spaCy model
en_core_web_smis installed automatically byrequirements.txt. If you skipped it, runpython -m spacy download en_core_web_sm.
In a second terminal:
cd frontend
npm install
cp .env.example .env # defaults already point at localhost:8000
npm run devOpen http://localhost:5173.
For local development you only need two things in backend/.env:
DJANGO_DEBUG=1
GROQ_API_KEY=your_key_hereWith DJANGO_DEBUG=1, the secret key, allowed hosts and CORS origins all fall
back to development defaults. In production none of them do — see
Configuration.
cp .env.example .env # set DJANGO_SECRET_KEY and GROQ_API_KEY
docker compose up --build- Frontend: http://localhost:5173
- Backend: http://localhost:8000
Both services run as non-root users from multi-stage images. The backend is
served by gunicorn and the frontend by nginx — neither uses a
development server. Migrations run automatically on container start, and SQLite
data persists in the backend-data volume.
docker compose refuses to start until DJANGO_SECRET_KEY and GROQ_API_KEY
are set; every other variable has a working default.
VITE_API_URLis compiled into the frontend bundle at build time, not read at runtime. Changing it requiresdocker compose build frontend.
All backend settings are read from the environment (via backend/.env when
present). This table lists every variable the code actually reads.
The application raises ImproperlyConfigured and refuses to start if any of
these is missing. There is no silent fallback to an insecure default.
| Variable | Description |
|---|---|
DJANGO_SECRET_KEY |
Django signing key. Generate with python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())" |
GROQ_API_KEY |
Groq API key from https://console.groq.com/keys |
DJANGO_ALLOWED_HOSTS |
Comma-separated hostnames. Never * |
DJANGO_CORS_ALLOWED_ORIGINS |
Comma-separated browser origins allowed to call the API |
With DJANGO_DEBUG=1 these fall back to an insecure development key,
localhost,127.0.0.1,[::1], and http://localhost:5173,
http://127.0.0.1:5173 respectively.
| Variable | Default | Description |
|---|---|---|
DJANGO_DEBUG |
0 |
Enable debug mode. Never enable in production |
DJANGO_LOG_LEVEL |
INFO |
Root logger level |
GROQ_MODEL |
llama-3.1-8b-instant |
Groq model id |
GROQ_TIMEOUT_SECONDS |
30 |
Per-request timeout to Groq |
GRAMMAR_MAX_INPUT_CHARS |
1000 |
Maximum characters per request |
API_ANON_THROTTLE_RATE |
30/minute |
DRF throttle for anonymous callers |
DJANGO_SECURE_SSL_REDIRECT |
1 |
Redirect HTTP to HTTPS. Applied only when DJANGO_DEBUG=0 |
DJANGO_SECURE_HSTS_SECONDS |
31536000 |
HSTS max-age. Applied only when DJANGO_DEBUG=0 |
| Variable | Default | Description |
|---|---|---|
DB_ENGINE |
django.db.backends.sqlite3 |
Django database backend |
DB_NAME |
backend/db.sqlite3 |
Database name or SQLite file path |
DB_USER |
— | Required for non-SQLite engines |
DB_PASSWORD |
— | Optional for non-SQLite engines |
DB_HOST |
— | Required for non-SQLite engines |
DB_PORT |
— | Required for non-SQLite engines |
To use PostgreSQL or MySQL, install the driver (psycopg[binary] or
mysqlclient) and set DB_ENGINE plus DB_NAME, DB_USER, DB_HOST and
DB_PORT. Startup fails loudly if any of those four is missing.
The app itself defines no models; the database is used only by Django's
auth, contenttypes and sessions apps.
| Variable | Default | Description |
|---|---|---|
VITE_API_URL |
http://localhost:8000 |
Backend base URL, no trailing slash and without the /grammar_check prefix — the client appends it |
Base path: /grammar_check/
Correct text and annotate each edit.
Request
{ "text": "She go to school. He were happy." }text is required, non-blank after trimming, and at most
GRAMMAR_MAX_INPUT_CHARS characters.
Response 200
{
"results": [
{
"Input": "She go to school.",
"Correct": "She goes to school.",
"Errant": [
{
"o_start": 1, "o_end": 2, "o_str": "go",
"c_start": 1, "c_end": 2, "c_str": "goes",
"e_type": "R:VERB:SVA"
}
]
}
]
}The input is split into sentences and each is corrected independently, so
results has one entry per sentence. e_type is an ERRANT code — prefix M
(missing), U (unnecessary) or R (replacement), plus a part of speech. The
frontend maps these to readable labels in
frontend/src/constants/errorTypes.js.
Status codes
| Code | Meaning |
|---|---|
200 |
Success |
400 |
text missing, blank, or too long |
405 |
Method other than POST |
429 |
Throttle rate exceeded |
502 |
Groq unreachable, rate-limited, or returned unusable output |
503 |
Server misconfigured — no valid GROQ_API_KEY, or missing spaCy/NLTK data |
A failed correction returns an error status. It never returns the original text dressed up as a correction.
Liveness probe used by the container healthcheck.
Response 200
{ "status": "ok" }# Backend
cd backend
pip install -r requirements-dev.txt
DJANGO_DEBUG=1 python manage.py test # 30 tests
DJANGO_DEBUG=1 pylint CorrectionAPP GrammerCorrection manage.py
pip-audit --requirement requirements.txt
# Frontend
cd frontend
npm run lint
npm run build
npm audit --audit-level=highCI runs exactly these commands plus a Docker image build — see
CONTRIBUTING.md and .github/workflows/ci.yml.
This application is deliberately stateless with respect to user content:
- Submitted text is not written to disk, a database, or a log file.
- Client IP addresses are not recorded.
- No contact form, no analytics, no cookies for anonymous users.
The one unavoidable disclosure is that submitted text is sent to Groq for processing. Review Groq's privacy policy before submitting confidential material.
If you add logging or a reverse proxy, check that it does not reintroduce request-body or IP logging. See SECURITY.md.
- English only. ERRANT is loaded with the English model, and the prompt assumes English.
- Non-deterministic. Corrections come from an LLM at
temperature=0, which reduces but does not eliminate variation. The model can miss errors, and can occasionally rewrite meaning rather than only fixing grammar. - ERRANT labels are heuristic. Edit classification is derived from an automatic alignment and is not always linguistically perfect.
- One Groq call per sentence. Long inputs cost proportionally more and take longer. The default 1000-character cap keeps this bounded.
- The API is unauthenticated. Rate limiting is the only abuse control, and it is in-memory per process — it does not coordinate across workers or replicas. Put authentication or a gateway in front of a public deployment.
- First request is slow. The spaCy model loads lazily on the first annotation, adding a few seconds.
- No streaming. Responses are returned only when every sentence is done.
.
├── backend/
│ ├── CorrectionAPP/ # Django app
│ │ ├── services/ # Business logic
│ │ │ ├── exceptions.py # Domain error types
│ │ │ └── grammar.py # Groq + ERRANT orchestration
│ │ ├── tests/ # Test suite
│ │ ├── serializers.py # Request validation
│ │ ├── urls.py
│ │ └── views.py # Thin HTTP layer
│ ├── GrammerCorrection/ # Django project settings
│ ├── Dockerfile # Multi-stage, non-root, gunicorn
│ ├── pyproject.toml # pylint configuration
│ ├── requirements.txt
│ └── requirements-dev.txt
├── frontend/
│ ├── src/
│ │ ├── components/ # Presentational components
│ │ ├── constants/ # Shared literals
│ │ ├── hooks/ # Stateful logic
│ │ ├── pages/
│ │ └── services/ # API client
│ ├── Dockerfile # Multi-stage, non-root, nginx
│ └── nginx.conf
├── .github/workflows/ci.yml
├── docker-compose.yml
├── CONTRIBUTING.md
├── SECURITY.md
└── LICENSE
On the spelling of
GrammerCorrectionandCorrectionAPP: both are misspelled/non-idiomatic, and both are intentionally left alone.GrammerCorrectionis referenced byDJANGO_SETTINGS_MODULE,wsgi.py,asgi.pyand the Dockerfile;CorrectionAPPis the Django app label stored in thedjango_migrationstable of existing databases. Renaming them would break running deployments for a cosmetic gain. The corresponding pylint warning is suppressed and documented inbackend/pyproject.toml.
Note the network clause: if you run a modified version of this software as a network service, you must offer its source code to the users of that service.
Branding and logos belonging to Pfactorial Technologies Pvt Ltd
(frontend/public/logo/, frontend/public/icon/, and the product links in
frontend/src/constants/products.js) are the property of their owner and are
not covered by the software licence. Replace them if you fork this project.