oromogrammar / app.py
abatejemal's picture
Upload 5 files
af989e1 verified
Raw
History Blame Contribute Delete
4.94 kB
"""
app.py — Web backend for the Afaan Oromo grammar checker.
Serves a single unified list of issues (rule-based + deep-learning), each
with absolute character offsets into the submitted text, so the frontend
can underline exact spans and show click-to-accept suggestion cards,
Grammarly-style.
Run:
pip install -r requirements.txt
uvicorn app:app --reload --port 8000
Then open http://localhost:8000
"""
from __future__ import annotations
import os
from typing import List, Optional
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from rule_checker import OromoGrammarChecker, split_sentences_with_offsets
from dl_checker import DeepLearningChecker, DEFAULT_MODEL
app = FastAPI(title="Afaan Oromo Grammar Checker")
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app.mount("/static", StaticFiles(directory=os.path.join(BASE_DIR, "static")), name="static")
templates = Jinja2Templates(directory=os.path.join(BASE_DIR, "templates"))
@app.middleware("http")
async def no_cache_static(request: Request, call_next):
response = await call_next(request)
if request.url.path.startswith("/static/"):
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
return response
rule_checker = OromoGrammarChecker()
_dl_checker: Optional[DeepLearningChecker] = None
def get_dl_checker() -> DeepLearningChecker:
global _dl_checker
if _dl_checker is None:
model_name = os.environ.get("OROMO_DL_MODEL", DEFAULT_MODEL)
_dl_checker = DeepLearningChecker(model_name=model_name)
return _dl_checker
class CheckRequest(BaseModel):
text: str
use_dl: bool = True
class IssueOut(BaseModel):
id: str
start: int
end: int
text: str
category: str
severity: str
message: str
suggestions: List[str] = []
source: str # "rule" or "model"
class CheckResponse(BaseModel):
issues: List[IssueOut]
dl_available: bool
dl_error: Optional[str] = None
@app.get("/", response_class=HTMLResponse)
async def index(request: Request):
return templates.TemplateResponse(request, "index.html", {})
@app.get("/api/health")
async def health():
return {"status": "ok"}
@app.post("/api/check", response_model=CheckResponse)
async def check(req: CheckRequest):
text = req.text
issues: List[IssueOut] = []
# --- Rule-based layer ---
rule_issues = rule_checker.check_text(text)
for i, issue in enumerate(rule_issues):
issues.append(
IssueOut(
id=f"rule-{i}",
start=issue.start,
end=issue.end,
text=issue.text,
category=issue.category,
severity=issue.severity.value,
message=issue.message,
suggestions=issue.suggestions,
source="rule",
)
)
# --- Deep learning layer ---
dl_available = False
dl_error = None
if req.use_dl:
checker = get_dl_checker()
dl_available = checker.available
dl_error = checker.load_error
if dl_available:
sentences_with_offsets = split_sentences_with_offsets(text)
scored = checker.check_sentences_with_offsets(sentences_with_offsets)
for j, (sent_score, base_offset) in enumerate(scored):
for k, tok in enumerate(sent_score.tokens):
if not tok.is_suspicious:
continue
abs_start = base_offset + tok.start
abs_end = base_offset + tok.end
# Skip if a rule-based issue already covers this exact span
# to avoid redundant duplicate cards for the same word.
if any(iss.start == abs_start and iss.end == abs_end for iss in issues):
continue
issues.append(
IssueOut(
id=f"dl-{j}-{k}",
start=abs_start,
end=abs_end,
text=tok.word,
category="model-flagged",
severity="warning",
message=(
f"The language model finds this word statistically "
f"unusual in context (probability {tok.original_prob*100:.1f}%, "
f"rank #{tok.original_rank})."
),
suggestions=tok.top_suggestions,
source="model",
)
)
issues.sort(key=lambda x: x.start)
return CheckResponse(issues=issues, dl_available=dl_available, dl_error=dl_error)