Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
<<<<<<< HEAD
# Python
venv/
spamdetection/venv/
__pycache__/
*.pyc

# Environment variables
.env

# Node
node_modules/

# Build files
dist/

# Windows
*.lnk
=======
# --- Python Virtual Environment ---
# Never push your local environment folder
venv/
Expand Down Expand Up @@ -29,4 +48,5 @@ build/
# --- VS Code & IDE Settings ---
.vscode/
.idea/
.DS_Store
.DS_Store
>>>>>>> upstream/main
113 changes: 96 additions & 17 deletions backend/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,34 @@
import joblib
import os
from dotenv import load_dotenv
from flask_cors import CORS
from translator import translate_to_english

load_dotenv()
from pathlib import Path
import google.generativeai as genai
import os

load_dotenv(Path(__file__).parent / ".env")
print("MODEL_PATH =", os.getenv("MODEL_PATH"))
print("VECTORIZER_PATH =", os.getenv("VECTORIZER_PATH"))
print("LABEL_ENCODER_PATH =", os.getenv("LABEL_ENCODER_PATH"))
genai.configure(
api_key=os.getenv("GEMINI_API_KEY")
)

ai_model = genai.GenerativeModel("gemini-2.5-flash")

app = Flask(__name__)
CORS(app)
CORS(
app,
resources={
r"/*": {
"origins": ["http://localhost:5173"]
}
}
)


MODEL_PATH = os.getenv("MODEL_PATH")
VECTORIZER_PATH = os.getenv("VECTORIZER_PATH")
Expand All @@ -23,37 +47,92 @@
def home():
return "ML API Running 🚀"

def generate_ai_suggestion(text, prediction):

prompt = f"""
Message:

{text}

Prediction: {prediction}

Respond in the same language as the message.

Give only one short recommendation (under 20 words).

Examples:

Safe:
This message appears safe.

Spam:
Do not click links or share personal information.
"""

try:
response = ai_model.generate_content(prompt)
return response.text

except Exception as e:
return f"AI suggestion unavailable: {str(e)}"
def get_safe_message(language):
safe_messages = {
"en": "This message appears safe.",
"de": "Diese Nachricht scheint sicher zu sein.",
"fr": "Ce message semble sûr.",
"es": "Este mensaje parece seguro.",
"it": "Questo messaggio sembra sicuro.",
"ta": "இந்த செய்தி பாதுகாப்பானதாக தெரிகிறது.",
"ja": "このメッセージは安全と思われます。",
"ko": "이 메시지는 안전한 것으로 보입니다.",
"zh-cn": "此消息似乎是安全的。",
"hi": "यह संदेश सुरक्षित प्रतीत होता है।"
}

return safe_messages.get(
language,
"This message appears safe."
)
@app.route("/predict", methods=["POST"])
def predict():
try:
data = request.get_json()

print("Received:", data)

text = data.get("text")
if not text:
# Simple file append for warning
with open("api.log", "a") as f:
f.write(f"WARNING: No text provided at {__import__('datetime').datetime.now()}\n")
return jsonify({"error": "No text provided"}), 400

text_vector = vectorizer.transform([text])
print("Text:", text)
translated_text, detected_language = translate_to_english(text)

text_vector = vectorizer.transform([translated_text])

prediction = model.predict(text_vector)

final_output = label_encoder.inverse_transform(prediction)[0]

# Simple file append for prediction log
text_preview = text[:50] + "..." if len(text) > 50 else text
with open("api.log", "a") as f:
from datetime import datetime
f.write(f"{datetime.now()} - Prediction: '{text_preview}' -> {final_output}\n")
return jsonify({"input": text, "prediction": final_output})
final_output = label_encoder.inverse_transform(prediction)[0]

if final_output in ["spam","smishing"]:
suggestion = generate_ai_suggestion(
text,
final_output
)
else:
suggestion = get_safe_message(detected_language)

return jsonify({
"input": text,
"prediction": final_output,
"suggestion": suggestion
})

except Exception as e:
with open("api.log", "a") as f:
from datetime import datetime
f.write(f"{datetime.now()} - ERROR: {str(e)}\n")
print("ERROR:", str(e))
return jsonify({"error": str(e)}), 500



if __name__ == "__main__":
FLASK_PORT = int(os.getenv("FLASK_PORT", 5000))
app.run(host="0.0.0.0", port=FLASK_PORT, debug=True)
app.run(host="0.0.0.0", port=FLASK_PORT, debug=True)
10 changes: 3 additions & 7 deletions backend/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@

DATABASE_PATH = os.getenv("DATABASE_PATH", "spam_detection.db")
API_KEY = os.getenv("API_KEY", "")
BASE_URL = os.getenv("BASE_URL", "http://localhost:8000")
PORT = int(os.getenv("PORT", "5000"))
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:5173")
DB_HOST = os.getenv("DB_HOST", "localhost")
DB_USER = os.getenv("DB_USER", "root")
DB_PASSWORD = os.getenv("DB_PASSWORD", "")
DB_NAME = os.getenv("DB_NAME", "spam_detection")
BASE_URL = os.getenv("BASE_URL", "http://localhost:5173")
PORT = int(os.getenv("PORT", "8000"))
FRONTEND_URL = os.getenv("FRONTEND_URL", "http://localhost:8000")
90 changes: 13 additions & 77 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,107 +1,43 @@
import os
import joblib
import numpy as np
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from backend.xai_service import XAIService
from backend.config import FRONTEND_URL, BASE_URL, PORT
xai_service = XAIService()

# ── Resolve model paths relative to this file ────────────────────────────────
# FIX: Use pathlib.Path so the app works regardless of the working directory.
# Previously, hardcoded relative strings like "linear_svm_model.pkl" would
# break whenever the process was not launched from the repo root.
BASE_DIR = Path(__file__).resolve().parent.parent

# ── Load ML models ────────────────────────────────────────────────────────────
# FIX: label_encoder.pkl was never loaded here, causing /predict to return
# a raw integer (0, 1, 2) instead of a human-readable label string like
# "ham", "spam", or "smishing". The frontend's string comparisons
# (result === "ham") would always evaluate to false with the old code.
model = joblib.load(BASE_DIR / "linear_svm_model.pkl")
vectorizer = joblib.load(BASE_DIR / "backend" / "tfidf_vectorizer.pkl")
label_encoder = joblib.load(BASE_DIR / "label_encoder.pkl")
# import environment config
from backend.config import FRONTEND_URL, BASE_URL, PORT

app = FastAPI(title="Spam Detection System")

# ── CORS setup ────────────────────────────────────────────────────────────────

# ── CORS setup (uses env variable) ─────────────────────────────
app.add_middleware(
CORSMiddleware,
allow_origins=[
FRONTEND_URL,
os.getenv("FRONTEND_DEV_URL", "http://localhost:3000"),
FRONTEND_URL,
os.getenv("FRONTEND_DEV_URL", "http://localhost:8000"),
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

# ── Request schema ────────────────────────────────────────────────────────────
class PredictIn(BaseModel):
text: str
type: str

# ── Prediction route ──────────────────────────────────────────────────────────
@app.post("/predict")
def predict(body: PredictIn):
"""
Classify a message as ham, spam, or smishing.

Returns:
prediction (str): Human-readable label — "ham", "spam", or "smishing".
confidence (float): SVM decision-function score for the winning class.
Higher absolute value = more confident prediction.
"""
try:
vectorized_text = vectorizer.transform([body.text])

# Get the raw predicted class index (0, 1, or 2)
raw_prediction = model.predict(vectorized_text)[0]

# FIX: Convert class index → string label using the label encoder
label = label_encoder.inverse_transform([raw_prediction])[0]

# ENHANCEMENT: Return a confidence score.
# LinearSVC does not support predict_proba(); use decision_function()
# instead. The score for each class is its distance from the boundary —
# a higher value means the model is more certain of that class.
scores = model.decision_function(vectorized_text)[0]
confidence = round(float(np.max(scores)), 4)

return {
"prediction": label, # e.g. "ham", "spam", "smishing"
"confidence": confidence, # e.g. 1.2345
}
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc))

# ── Health / root ─────────────────────────────────────────────────────────────
# ── Basic health check ─────────────────────────────────────────
@app.get("/")
def root():
return {
"status": "ok",
"message": "Spam Detection API is running",
"status": "ok",
"message": "Spam Detection API is running",
"base_url": BASE_URL,
}


@app.get("/health")
def health():
return {"status": "healthy"}

# ── Routers ───────────────────────────────────────────────────────────────────
# EMAIL DATABASE ROUTES (Issue #13)
from backend.emails import router as emails_router
# from backend.database import init_db # Uncomment once DB is configured
# init_db()
app.include_router(emails_router)

# EXPORT ROUTES (Issue #23)
from backend.export import router as export_router
app.include_router(export_router)

# ── Run directly ──────────────────────────────────────────────────────────────
# ── Optional: run directly ─────────────────────────────────────
if __name__ == "__main__":
import uvicorn

uvicorn.run("backend.main:app", host="0.0.0.0", port=PORT, reload=True)
5 changes: 1 addition & 4 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@ Flask==3.1.0
scikit-learn==1.6.1
joblib==1.5.1
python-dotenv==1.1.1
fastapi
uvicorn
mysql-connector-python
fpdf2
python deep-translator langdetect - 1.11.4
17 changes: 17 additions & 0 deletions frontend/electron.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { BrowserWindow, app } = require("electron");
const path = require("path");

function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800
});

const indexPath = path.join(app.getAppPath(), "dist", "index.html");

console.log("Loading:", indexPath);

win.loadFile(indexPath);
}

app.whenReady().then(createWindow);
Loading