UDHOV's picture
deploy fastapi backend to hf spaces
7255083
# Nepali Hate Content Detection — API Reference
> **Base URL:** `http://localhost:8000`
> **Interactive docs:** `http://localhost:8000/docs` (Swagger UI)
> **Start server:** `uvicorn backend.app.main:app --reload --host 0.0.0.0 --port 8000` from `major_project/` root
---
## Table of Contents
1. [Health Check](#1-health-check)
2. [Status & Capabilities](#2-status--capabilities)
3. [Predict — Single Text](#3-predict--single-text)
4. [Analyze — Preprocessing Info](#4-analyze--preprocessing-info)
5. [Explain — LIME](#5-explain--lime)
6. [Explain — SHAP](#6-explain--shap)
7. [Explain — Captum IG](#7-explain--captum-ig)
8. [Batch Predict (Streaming)](#8-batch-predict-streaming)
9. [History — Fetch](#9-history--fetch)
10. [History — Stats](#10-history--stats)
11. [History — Clear](#11-history--clear)
12. [Error Reference](#12-error-reference)
13. [TypeScript Types](#13-typescript-types)
14. [Frontend Integration Guide](#14-frontend-integration-guide)
---
## 1. Health Check
```
GET /health
```
Returns whether the server is up and the model has finished loading. Call this on app mount to gate the UI.
**Response `200`**
```json
{
"status": "ok",
"model_loaded": true,
"device": "cpu"
}
```
| Field | Type | Notes |
|---|---|---|
| `status` | `string` | Always `"ok"` if server is running |
| `model_loaded` | `boolean` | `false` while model is still downloading/loading at startup |
| `device` | `string` | `"cpu"` or `"cuda"` |
**Frontend use:** Poll this every 2 seconds on mount until `model_loaded === true`, then unlock the main UI.
---
## 2. Status & Capabilities
```
GET /api/status
```
Returns which optional XAI packages are installed. Call once on load to decide which Explain buttons to show or hide.
**Response `200`**
```json
{
"model_loaded": true,
"device": "cpu",
"preprocessor": true,
"lime": true,
"shap": true,
"captum": false
}
```
| Field | Type | Notes |
|---|---|---|
| `model_loaded` | `boolean` | Same as `/health` |
| `device` | `string` | `"cpu"` or `"cuda"` |
| `preprocessor` | `boolean` | If `false`, raw text is passed to model without script conversion |
| `lime` | `boolean` | Whether `lime` package is installed |
| `shap` | `boolean` | Whether `shap` package is installed |
| `captum` | `boolean` | Whether `captum` package is installed |
**Frontend use:** If `captum === false`, disable the Captum tab. Same for LIME/SHAP.
---
## 3. Predict — Single Text
```
POST /api/predict
Content-Type: application/json
```
Core endpoint. Preprocesses input → runs XLM-RoBERTa-large → returns label + probabilities + preprocessing details.
**Request body**
```json
{
"text": "महिलाले घरमा बस्नु पर्छ",
"save_to_history": true
}
```
| Field | Type | Required | Notes |
|---|---|---|---|
| `text` | `string` | ✅ | 1–5000 chars. Devanagari, Romanized Nepali, English, or mixed. Must not be whitespace only |
| `save_to_history` | `boolean` | ❌ | Default `true`. Saves result to `data/prediction_history.jsonl` as background task |
**Response `200`**
```json
{
"prediction": "OS",
"confidence": 0.9909,
"probabilities": {
"NO": 0.0034,
"OO": 0.0041,
"OR": 0.0016,
"OS": 0.9909
},
"original_text": "महिलाले घरमा बस्नु पर्छ",
"preprocessed_text": "महिलाले घरमा बस्नु पर्छ",
"emoji_features": {
"has_hate_emoji": 0,
"has_mockery_emoji": 0,
"has_positive_emoji": 0,
"has_sadness_emoji": 0,
"has_fear_emoji": 0,
"has_disgust_emoji": 0,
"hate_emoji_count": 0,
"mockery_emoji_count": 0,
"positive_emoji_count": 0,
"sadness_emoji_count": 0,
"fear_emoji_count": 0,
"disgust_emoji_count": 0,
"total_emoji_count": 0,
"hate_to_positive_ratio": 0.0,
"has_mixed_sentiment": 0,
"unknown_emoji_count": 0,
"has_unknown_emoji": 0,
"known_emoji_ratio": 1.0
},
"script_info": {
"script_type": "devanagari",
"confidence": 0.98
},
"error": null
}
```
**Prediction labels**
| Label | Meaning | Display color |
|---|---|---|
| `NO` | Non-offensive | Green `#28a745` |
| `OO` | Other-offensive (general) | Yellow `#ffc107` |
| `OR` | Offensive-Racist (race/ethnicity/religion hate) | Red `#dc3545` |
| `OS` | Offensive-Sexist (gender/sexuality hate) | Purple `#6f42c1` |
**`emoji_features` fields**
18 fields total. All are `int` except `hate_to_positive_ratio` and `known_emoji_ratio` which are `float`.
| Field | Description |
|---|---|
| `has_hate_emoji` | Binary flag: 1 if text contains anger/weapon emojis |
| `hate_emoji_count` | Count of hate-related emojis |
| `has_positive_emoji` | Binary flag |
| `positive_emoji_count` | Count |
| `total_emoji_count` | Total emoji count |
| `hate_to_positive_ratio` | `hate_count / max(positive_count, 1)` |
| `has_mixed_sentiment` | 1 if both hate and positive emojis present |
| `unknown_emoji_count` | Emojis not in the mapping dictionary |
| `known_emoji_ratio` | Fraction of emojis that have Nepali translations |
**`script_info` fields**
| Field | Description |
|---|---|
| `script_type` | One of: `devanagari`, `romanized_nepali`, `english`, `mixed`, `other` |
| `confidence` | Float 0–1 |
**Error cases**
| Status | Condition |
|---|---|
| `422` | Empty or whitespace-only text |
| `503` | Model not yet loaded |
| `503` | Out of memory during inference |
| `500` | Unexpected server error |
---
## 4. Analyze — Preprocessing Info
```
POST /api/analyze
Content-Type: application/json
```
Lightweight endpoint — runs only script detection and emoji analysis, does **not** run the model. Use for the preprocessing details panel without triggering a full prediction.
**Request body**
```json
{
"text": "timi murkha chau 😡"
}
```
| Field | Type | Required | Notes |
|---|---|---|---|
| `text` | `string` | ✅ | 1–5000 chars |
**Response `200`**
```json
{
"script_info": {
"script_type": "romanized_nepali",
"confidence": 0.80
},
"emoji_info": {
"emojis_found": ["😡"],
"total_count": 1,
"known_emojis": ["😡"],
"known_count": 1,
"unknown_emojis": [],
"unknown_count": 0,
"coverage": 1.0
}
}
```
**`emoji_info` fields**
| Field | Type | Description |
|---|---|---|
| `emojis_found` | `string[]` | All emoji characters found in text |
| `total_count` | `number` | Total emoji count |
| `known_emojis` | `string[]` | Emojis that have a Nepali translation mapping |
| `known_count` | `number` | |
| `unknown_emojis` | `string[]` | Emojis not in the mapping dictionary |
| `unknown_count` | `number` | |
| `coverage` | `number` | `known_count / total_count`, or `1.0` if no emojis |
---
## 5. Explain — LIME
```
POST /api/explain/lime
Content-Type: application/json
```
Generates word-level importance scores using LIME (Local Interpretable Model-agnostic Explanations). LIME perturbs the **preprocessed** text, so token labels always align with what the model saw.
**Request body**
```json
{
"text": "महिलाले घरमा बस्नु पर्छ",
"num_samples": 200,
"n_steps": 50
}
```
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `text` | `string` | ✅ | — | 1–2000 chars (shorter limit than predict — LIME runs many model calls) |
| `num_samples` | `integer` | ❌ | `200` | Range 50–1000. Higher = more reliable scores, higher latency |
| `n_steps` | `integer` | ❌ | `50` | Only used by Captum, ignored here |
**Response `200`**
```json
{
"method": "LIME",
"prediction": "OS",
"confidence": 0.9909,
"word_scores": [
{ "word": "घरमा", "score": 0.182 },
{ "word": "महिलाले", "score": 0.143 },
{ "word": "बस्नु", "score": 0.091 },
{ "word": "पर्छ", "score": -0.034 }
],
"preprocessed_text": "महिलाले घरमा बस्नु पर्छ",
"convergence_delta": null,
"error": null
}
```
**`word_scores` interpretation**
| Score | Meaning |
|---|---|
| Positive | Word pushes prediction **toward** the predicted class |
| Negative | Word pushes prediction **away** from the predicted class |
| High absolute value | Strong influence |
Words are returned in LIME's natural order (by score magnitude). Sort by `abs(score)` descending for a ranked importance bar chart.
**Frontend rendering:** Horizontal bar chart. Positive bars green, negative bars red. Display `word` on the y-axis.
---
## 6. Explain — SHAP
```
POST /api/explain/shap
Content-Type: application/json
```
Generates attributions using SHAP. Falls back to leave-one-out occlusion if the primary SHAP text masker fails.
**Request body** — same shape as LIME. `num_samples` is ignored; `n_steps` is ignored.
```json
{
"text": "महिलाले घरमा बस्नु पर्छ"
}
```
**Response `200`**
```json
{
"method": "SHAP",
"prediction": "OS",
"confidence": 0.9909,
"word_scores": [
{ "word": "घरमा", "score": 0.211 },
{ "word": "महिलाले", "score": 0.178 },
{ "word": "बस्नु", "score": 0.095 },
{ "word": "पर्छ", "score": -0.021 }
],
"preprocessed_text": "महिलाले घरमा बस्नु पर्छ",
"convergence_delta": null,
"error": null
}
```
Word scores are **sorted by descending absolute value** — most influential words first.
If the fallback was used, `error` will be `"Used gradient_fallback"` (not a failure — result is still valid).
---
## 7. Explain — Captum IG
```
POST /api/explain/captum
Content-Type: application/json
```
Generates subword token attributions using Layer Integrated Gradients (Captum). Works at the subword tokenizer level, so words may appear as `▁महिलाले` (SentencePiece prefix).
**Request body**
```json
{
"text": "महिलाले घरमा बस्नु पर्छ",
"n_steps": 50
}
```
| Field | Type | Required | Default | Notes |
|---|---|---|---|---|
| `text` | `string` | ✅ | — | 1–2000 chars |
| `n_steps` | `integer` | ❌ | `50` | Range 10–200. Increase to 100+ if `convergence_delta > 0.05` |
| `num_samples` | `integer` | ❌ | `200` | Only used by LIME, ignored here |
**Response `200`**
```json
{
"method": "Captum-IG",
"prediction": "OS",
"confidence": 0.9909,
"word_scores": [
{ "word": "महिलाले", "score": 0.842 },
{ "word": "घरमा", "score": 0.631 },
{ "word": "बस्नु", "score": 0.417 },
{ "word": "पर्छ", "score": 0.203 }
],
"preprocessed_text": "महिलाले घरमा बस्नु पर्छ",
"convergence_delta": 0.0031,
"error": null
}
```
| Field | Notes |
|---|---|
| `word_scores[].score` | Signed attribution (sum of subword attributions). Positive = contributes to prediction |
| `convergence_delta` | Quality indicator. Values below `0.05` = reliable. Increase `n_steps` if high |
**⚠️ Memory warning:** Captum is the most memory-intensive method. It may return `422` on low-RAM cloud deployments. Use LIME or SHAP as fallback — the frontend should check `captum` in `/api/status` before showing this option.
---
## 8. Batch Predict (Streaming)
```
POST /api/batch
Content-Type: application/json
```
Classifies multiple texts and **streams results back as NDJSON** (Newline-Delimited JSON). Each text is processed independently — an error on one does not abort the batch.
**Request body**
```json
{
"texts": [
"यो राम्रो छ",
"तिमी मुर्ख हौ",
"timi murkha chau"
]
}
```
| Field | Type | Required | Notes |
|---|---|---|---|
| `texts` | `string[]` | ✅ | 1–200 items. Empty strings are stripped silently |
**Response — NDJSON stream**
`Content-Type: application/x-ndjson`
Each line is a complete JSON object. Two types of lines:
**Progress line** (one per text):
```json
{
"index": 0,
"total": 3,
"result": {
"text": "यो राम्रो छ",
"full_text": "यो राम्रो छ",
"prediction": "NO",
"confidence": 0.9721,
"preprocessed_text": "यो राम्रो छ"
}
}
```
**Final sentinel line** (last line always):
```json
{ "done": true, "total": 3 }
```
**Error result** (when one text fails):
```json
{
"index": 1,
"total": 3,
"result": {
"text": "...",
"full_text": "...",
"prediction": "Error",
"confidence": 0.0,
"preprocessed_text": "",
"error": "error message"
}
}
```
**Frontend streaming example (fetch API):**
```javascript
const response = await fetch("http://localhost:8000/api/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ texts }),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop(); // keep incomplete last line
for (const line of lines) {
if (!line.trim()) continue;
const data = JSON.parse(line);
if (data.done) {
// Batch complete
setProgress(100);
} else {
// Update progress bar and results table
setProgress(Math.round(((data.index + 1) / data.total) * 100));
appendResult(data.result);
}
}
}
```
---
## 9. History — Fetch
```
GET /api/history?limit=100&offset=0
```
Returns saved predictions in reverse-chronological order (newest first).
**Query parameters**
| Param | Type | Default | Range | Description |
|---|---|---|---|---|
| `limit` | `integer` | `100` | 1–500 | Max records to return |
| `offset` | `integer` | `0` | ≥0 | Skip this many records from the newest end |
**Response `200`**
```json
{
"items": [
{
"timestamp": "2026-04-10T10:23:41.123456",
"text": "तिमी मुर्ख हौ",
"prediction": "OO",
"confidence": 0.8732,
"probabilities": {
"NO": 0.08,
"OO": 0.87,
"OR": 0.03,
"OS": 0.02
},
"preprocessed_text": "तिमी मुर्ख हौ",
"emoji_features": { "total_emoji_count": 0, "..." : "..." }
}
],
"total": 42,
"limit": 100,
"offset": 0
}
```
**Pagination example:**
```
Page 1: GET /api/history?limit=20&offset=0
Page 2: GET /api/history?limit=20&offset=20
Page 3: GET /api/history?limit=20&offset=40
```
---
## 10. History — Stats
```
GET /api/history/stats
```
Returns aggregated statistics without fetching every record. Use for the dashboard summary row.
**Response `200` (with history)**
```json
{
"total": 42,
"avg_confidence": 0.8741,
"class_counts": {
"NO": 18,
"OO": 12,
"OR": 5,
"OS": 7
},
"most_common_class": "NO"
}
```
**Response `200` (empty history)**
```json
{
"total": 0,
"avg_confidence": null,
"class_counts": {},
"most_common_class": null
}
```
---
## 11. History — Clear
```
DELETE /api/history
```
Permanently deletes the history file. No confirmation prompt — handle that in the UI.
**Response `200`**
```json
{
"message": "History cleared. 42 record(s) deleted.",
"deleted_count": 42
}
```
**Response `404`** (if already empty)
```json
{
"detail": "History is already empty — nothing to clear."
}
```
---
## 12. Error Reference
All error responses follow FastAPI's standard shape:
```json
{
"detail": "Human-readable error message"
}
```
| Status | Meaning | When it happens |
|---|---|---|
| `422` | Validation error | Empty text, batch > 200, invalid field types |
| `503` | Service unavailable | Model still loading at startup, out of memory |
| `404` | Not found | History already empty on DELETE |
| `500` | Internal server error | Unexpected exception in inference or XAI |
---
## 13. TypeScript Types
Copy these into your React/Vite project:
```typescript
// ── Labels ──────────────────────────────────────────────────────────────────
export type Label = "NO" | "OO" | "OR" | "OS" | "Error";
export const LABEL_META = {
NO: { text: "Non-Offensive", color: "#28a745" },
OO: { text: "Other-Offensive", color: "#ffc107" },
OR: { text: "Offensive-Racist", color: "#dc3545" },
OS: { text: "Offensive-Sexist", color: "#6f42c1" },
Error: { text: "Error", color: "#6c757d" },
} as const;
// ── /health ──────────────────────────────────────────────────────────────────
export interface HealthResponse {
status: string;
model_loaded: boolean;
device: string;
}
// ── /api/status ───────────────────────────────────────────────────────────────
export interface StatusResponse {
model_loaded: boolean;
device: string;
preprocessor: boolean;
lime: boolean;
shap: boolean;
captum: boolean;
}
// ── /api/predict ──────────────────────────────────────────────────────────────
export interface PredictRequest {
text: string;
save_to_history?: boolean;
}
export interface EmojiFeatures {
has_hate_emoji: number;
has_mockery_emoji: number;
has_positive_emoji: number;
has_sadness_emoji: number;
has_fear_emoji: number;
has_disgust_emoji: number;
hate_emoji_count: number;
mockery_emoji_count: number;
positive_emoji_count: number;
sadness_emoji_count: number;
fear_emoji_count: number;
disgust_emoji_count: number;
total_emoji_count: number;
hate_to_positive_ratio: number;
has_mixed_sentiment: number;
unknown_emoji_count: number;
has_unknown_emoji: number;
known_emoji_ratio: number;
}
export interface ScriptInfo {
script_type: "devanagari" | "romanized_nepali" | "english" | "mixed" | "other";
confidence: number;
}
export interface PredictResponse {
prediction: Label;
confidence: number;
probabilities: Record<Label, number>;
original_text: string;
preprocessed_text: string;
emoji_features: EmojiFeatures;
script_info: ScriptInfo | null;
error: string | null;
}
// ── /api/analyze ──────────────────────────────────────────────────────────────
export interface AnalyzeRequest {
text: string;
}
export interface EmojiInfo {
emojis_found: string[];
total_count: number;
known_emojis: string[];
known_count: number;
unknown_emojis: string[];
unknown_count: number;
coverage: number;
}
export interface AnalyzeResponse {
script_info: ScriptInfo;
emoji_info: EmojiInfo;
}
// ── /api/explain/* ────────────────────────────────────────────────────────────
export interface ExplainRequest {
text: string;
num_samples?: number; // LIME only, default 200
n_steps?: number; // Captum only, default 50
}
export interface WordScore {
word: string;
score: number;
}
export interface ExplainResponse {
method: "LIME" | "SHAP" | "Captum-IG";
prediction: Label;
confidence: number;
word_scores: WordScore[];
preprocessed_text: string;
convergence_delta: number | null; // Captum only
error: string | null;
}
// ── /api/batch ────────────────────────────────────────────────────────────────
export interface BatchRequest {
texts: string[];
}
export interface BatchResult {
text: string; // truncated to 80 chars
full_text: string;
prediction: Label;
confidence: number;
preprocessed_text: string;
error?: string;
}
export interface BatchProgressLine {
index: number;
total: number;
result: BatchResult;
}
export interface BatchDoneLine {
done: true;
total: number;
}
export type BatchStreamLine = BatchProgressLine | BatchDoneLine;
// ── /api/history ──────────────────────────────────────────────────────────────
export interface HistoryItem {
timestamp: string; // ISO 8601
text: string;
prediction: Label;
confidence: number;
probabilities: Record<string, number>;
preprocessed_text: string;
emoji_features: EmojiFeatures;
}
export interface HistoryResponse {
items: HistoryItem[];
total: number;
limit: number;
offset: number;
}
export interface HistoryStatsResponse {
total: number;
avg_confidence: number | null;
class_counts: Record<string, number>;
most_common_class: string | null;
}
```
---
## 14. Frontend Integration Guide
### Recommended call order on app load
```
1. GET /health → poll until model_loaded === true
2. GET /api/status → store capabilities, show/hide XAI buttons
3. Ready to accept input
```
### Single prediction flow
```
user submits text
→ POST /api/predict
→ show prediction badge (color from LABEL_META)
→ show probability bar chart (4 bars)
→ show preprocessing details (script_info + emoji_features)
→ if emoji_features.total_emoji_count > 0, show emoji breakdown panel
```
### Explainability flow
```
user selects LIME / SHAP / Captum tab
→ check status.lime / status.shap / status.captum before enabling tab
→ POST /api/explain/lime (or /shap or /captum)
→ render horizontal bar chart from word_scores
- sort by abs(score) descending
- positive score → green bar
- negative score → red bar
→ for Captum: show convergence_delta warning if > 0.05
```
### Batch flow
```
user pastes texts or uploads CSV
→ POST /api/batch
→ read response as NDJSON stream (see streaming example in §8)
→ update progress bar: (index + 1) / total * 100
→ append each result to results table as it arrives
→ on { done: true }, finalize and enable download CSV
```
### History flow
```
on History tab open:
→ GET /api/history/stats → show summary metrics
→ GET /api/history?limit=20&offset=0 → show table
pagination:
→ GET /api/history?limit=20&offset=N
clear button:
→ confirm in UI first
→ DELETE /api/history
```
### CORS
The backend allows requests from `http://localhost:5173` (Vite default) and `http://localhost:3000` (CRA default). If you deploy the frontend to a different URL, set the `FRONTEND_URL` environment variable before starting the server:
```bash
FRONTEND_URL=https://yourapp.vercel.app uvicorn backend.app.main:app ...
```
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `MODEL_PATH` | `models/saved_models/xlm_roberta_results/large_final` | Local model path. Falls back to HuggingFace if not found |
| `HF_MODEL_ID` | `UDHOV/xlm-roberta-large-nepali-hate-classification` | HuggingFace model ID |
| `HISTORY_FILE` | `data/prediction_history.jsonl` | History file location |
| `FRONTEND_URL` | `""` | Additional CORS origin for deployed frontend |