Spaces:
Sleeping
Sleeping
🚀 Deploy via GitHub Actions
Browse files- .gitignore +1 -0
- Dockerfile +27 -0
- README.md +117 -5
- main.py +81 -0
- requirements.txt +6 -0
- spam_detector_model_cv.pkl +3 -0
.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
dataset.csv
|
Dockerfile
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a slim official Python image
|
| 2 |
+
FROM python:3.12-slim
|
| 3 |
+
|
| 4 |
+
# Hugging Face Spaces runs on port 7860
|
| 5 |
+
EXPOSE 7860
|
| 6 |
+
|
| 7 |
+
# Create a non-root user for security
|
| 8 |
+
RUN useradd -m -u 1000 appuser
|
| 9 |
+
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
# Copy dependency list first (better layer caching)
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
|
| 15 |
+
# Install dependencies
|
| 16 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 17 |
+
|
| 18 |
+
# Copy the rest of the application files
|
| 19 |
+
COPY . .
|
| 20 |
+
|
| 21 |
+
# Set correct ownership
|
| 22 |
+
RUN chown -R appuser:appuser /app
|
| 23 |
+
|
| 24 |
+
USER appuser
|
| 25 |
+
|
| 26 |
+
# Start the FastAPI app via Uvicorn on port 7860
|
| 27 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,122 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: GitHub Spam Detector API
|
| 3 |
+
emoji: 🛡️
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
app_port: 7860
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# 🛡️ GitHub Spam Detector API
|
| 13 |
+
|
| 14 |
+
A production-ready REST API to classify GitHub comments as **Spam** or **Not Spam** in real time.
|
| 15 |
+
|
| 16 |
+
Built with **FastAPI** and deployed via **Docker** on Hugging Face Spaces.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 📌 Model Details
|
| 21 |
+
|
| 22 |
+
| Property | Value |
|
| 23 |
+
|---|---|
|
| 24 |
+
| **Training Data** | 100,000+ real GitHub issue & PR comments |
|
| 25 |
+
| **Feature Extraction** | TF-IDF (unigrams + bigrams, stop-words removed) |
|
| 26 |
+
| **Classifier** | LinearSVC (tuned via GridSearchCV) |
|
| 27 |
+
| **Test Accuracy** | 99% |
|
| 28 |
+
| **Test F1-Score** | 0.99 (both classes) |
|
| 29 |
+
| **AUC-ROC** | 1.00 |
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 🚀 API Endpoints
|
| 34 |
+
|
| 35 |
+
### `GET /`
|
| 36 |
+
Health check — confirms the server is running.
|
| 37 |
+
|
| 38 |
+
**Response:**
|
| 39 |
+
```json
|
| 40 |
+
{
|
| 41 |
+
"status": "ok",
|
| 42 |
+
"message": "Spam Detector API is running."
|
| 43 |
+
}
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
### `POST /predict`
|
| 49 |
+
Classify a piece of text as spam or not spam.
|
| 50 |
+
|
| 51 |
+
**Request Body:**
|
| 52 |
+
```json
|
| 53 |
+
{
|
| 54 |
+
"text": "Please fix the bug at line 56, it causes a null pointer exception."
|
| 55 |
+
}
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
**Response:**
|
| 59 |
+
```json
|
| 60 |
+
{
|
| 61 |
+
"text": "Please fix the bug at line 56, it causes a null pointer exception.",
|
| 62 |
+
"prediction": 0,
|
| 63 |
+
"label": "not_spam"
|
| 64 |
+
}
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
| Field | Type | Description |
|
| 68 |
+
|---|---|---|
|
| 69 |
+
| `prediction` | `int` | `0` = Not Spam, `1` = Spam |
|
| 70 |
+
| `label` | `string` | `"spam"` or `"not_spam"` |
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## 🧪 Try it Out
|
| 75 |
+
|
| 76 |
+
Once the Space is running, visit the interactive Swagger docs at:
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
https://<your-username>-github-spam-detector-api.hf.space/docs
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
Or use `curl`:
|
| 83 |
+
```bash
|
| 84 |
+
curl -X POST "https://<your-username>-github-spam-detector-api.hf.space/predict" \
|
| 85 |
+
-H "Content-Type: application/json" \
|
| 86 |
+
-d '{"text": "subscribe me if u love eminem"}'
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
## 🛠️ Run Locally
|
| 92 |
+
|
| 93 |
+
```bash
|
| 94 |
+
# Install dependencies
|
| 95 |
+
pip install -r requirements.txt
|
| 96 |
+
|
| 97 |
+
# Start the server
|
| 98 |
+
uvicorn main:app --host 0.0.0.0 --port 7860 --reload
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
Then visit: [http://localhost:7860/docs](http://localhost:7860/docs)
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 📦 Tech Stack
|
| 106 |
+
- **Python 3.12**
|
| 107 |
+
- **FastAPI** — REST API framework
|
| 108 |
+
- **scikit-learn** — TF-IDF + LinearSVC model
|
| 109 |
+
- **Uvicorn** — ASGI server
|
| 110 |
+
- **Docker** — containerized deployment
|
| 111 |
+
|
| 112 |
+
---
|
| 113 |
+
|
| 114 |
+
## ⚠️ Known Limitations
|
| 115 |
+
|
| 116 |
+
- Trained exclusively on GitHub comment data. May underperform on spam from other domains (e.g., YouTube, email).
|
| 117 |
+
- Context-blind to "Markdown camouflage" — wrapping spam text inside ` ```diff ` code blocks may fool the model as the TF-IDF vectorizer weighs the code-block tokens heavily toward non-spam.
|
| 118 |
+
- For multilingual or highly obfuscated spam, consider upgrading to a transformer-based model (e.g., DistilBERT).
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
*Part of the H2-GitGriffin Hackathon Project.*
|
main.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import joblib
|
| 2 |
+
import os
|
| 3 |
+
from fastapi import FastAPI, HTTPException
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
|
| 6 |
+
# ------------------------------------------------------------------
|
| 7 |
+
# App setup
|
| 8 |
+
# ------------------------------------------------------------------
|
| 9 |
+
app = FastAPI(
|
| 10 |
+
title="GitHub Spam Detector API",
|
| 11 |
+
description="Predicts whether a GitHub comment is spam (1) or not spam (0). "
|
| 12 |
+
"Trained on 100k+ real GitHub comments using TF-IDF + LinearSVC.",
|
| 13 |
+
version="1.0.0",
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# ------------------------------------------------------------------
|
| 17 |
+
# Model loading (once at startup, never per-request)
|
| 18 |
+
# ------------------------------------------------------------------
|
| 19 |
+
MODEL_PATH = os.getenv("MODEL_PATH", "spam_detector_model_cv.pkl")
|
| 20 |
+
|
| 21 |
+
@app.on_event("startup")
|
| 22 |
+
def load_model():
|
| 23 |
+
global model
|
| 24 |
+
if not os.path.exists(MODEL_PATH):
|
| 25 |
+
raise RuntimeError(f"Model file not found at: {MODEL_PATH}")
|
| 26 |
+
model = joblib.load(MODEL_PATH)
|
| 27 |
+
print(f"Model loaded successfully from {MODEL_PATH}")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
# ------------------------------------------------------------------
|
| 31 |
+
# Request / Response schemas
|
| 32 |
+
# ------------------------------------------------------------------
|
| 33 |
+
class PredictRequest(BaseModel):
|
| 34 |
+
text: str
|
| 35 |
+
|
| 36 |
+
class Config:
|
| 37 |
+
json_schema_extra = {
|
| 38 |
+
"example": {
|
| 39 |
+
"text": "Please fix the bug at line 56, it causes a null pointer exception."
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class PredictResponse(BaseModel):
|
| 45 |
+
text: str
|
| 46 |
+
prediction: int # 0 = Not Spam, 1 = Spam
|
| 47 |
+
label: str # Human-readable label
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
# ------------------------------------------------------------------
|
| 51 |
+
# Endpoints
|
| 52 |
+
# ------------------------------------------------------------------
|
| 53 |
+
@app.get("/", tags=["Health"])
|
| 54 |
+
def root():
|
| 55 |
+
"""Health check endpoint."""
|
| 56 |
+
return {"status": "ok", "message": "Spam Detector API is running."}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@app.post("/predict", response_model=PredictResponse, tags=["Prediction"])
|
| 60 |
+
def predict(request: PredictRequest):
|
| 61 |
+
"""
|
| 62 |
+
Predict whether a piece of text is spam or not.
|
| 63 |
+
|
| 64 |
+
- **text**: The comment / text to classify.
|
| 65 |
+
|
| 66 |
+
Returns:
|
| 67 |
+
- **prediction**: `0` (Not Spam) or `1` (Spam)
|
| 68 |
+
- **label**: Human-readable string `"spam"` or `"not_spam"`
|
| 69 |
+
"""
|
| 70 |
+
if not request.text or not request.text.strip():
|
| 71 |
+
raise HTTPException(status_code=422, detail="Input text cannot be empty.")
|
| 72 |
+
|
| 73 |
+
raw_pred = model.predict([request.text])[0]
|
| 74 |
+
prediction = int(raw_pred)
|
| 75 |
+
label = "spam" if prediction == 1 else "not_spam"
|
| 76 |
+
|
| 77 |
+
return PredictResponse(
|
| 78 |
+
text=request.text,
|
| 79 |
+
prediction=prediction,
|
| 80 |
+
label=label,
|
| 81 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.111.0
|
| 2 |
+
uvicorn[standard]==0.29.0
|
| 3 |
+
scikit-learn==1.4.2
|
| 4 |
+
joblib==1.4.2
|
| 5 |
+
pydantic==2.7.1
|
| 6 |
+
numpy==1.26.4
|
spam_detector_model_cv.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6ad1b0399122c1c3c6b8514a1cc43a60b732a8176aaa9b334f25c1c54009760e
|
| 3 |
+
size 83350887
|