Spaces:
Configuration error
Configuration error
| """ | |
| 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")) | |
| 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 | |
| async def index(request: Request): | |
| return templates.TemplateResponse(request, "index.html", {}) | |
| async def health(): | |
| return {"status": "ok"} | |
| 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) | |