""" Khasi NER service — runs the trained spaCy/RoBERTa pipeline behind a tiny JSON API. Deployed to a Hugging Face Space; called by the Khasi NLP backend on Render so the heavy model never has to live on the (small) Render box. """ import os from typing import Optional from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel import spacy # Optional shared secret. Set NER_API_KEY in the Space's "Variables and # secrets" panel — Render sends the same value in the X-Api-Key header. # If unset, the endpoint is open (fine for local dev, not production). API_KEY = os.environ.get("NER_API_KEY", "") # Load once at process boot so every request reuses the warm model. nlp = spacy.load("model-best") NER_LABELS = list(nlp.get_pipe("ner").labels) print(f"[khasi-ner] loaded — components: {nlp.pipe_names} | labels: {NER_LABELS}") app = FastAPI(title="Khasi NER", version="1.0.0") class NerRequest(BaseModel): text: str @app.get("/") def root(): return {"service": "khasi-ner", "endpoints": ["/health", "/ner"]} @app.get("/health") def health(): return {"ok": True, "labels": NER_LABELS} @app.post("/ner") def ner(req: NerRequest, x_api_key: Optional[str] = Header(default=None)): if API_KEY and x_api_key != API_KEY: raise HTTPException(status_code=401, detail="invalid api key") doc = nlp(req.text) return { "text": doc.text, "entities": [ { "text": ent.text, "label": ent.label_, "start": ent.start_char, "end": ent.end_char, } for ent in doc.ents ], }