UmReh commited on
Commit
b489ffd
·
verified ·
1 Parent(s): 066b3f0

Upload 11 files

Browse files
README.md CHANGED
@@ -1,13 +1,14 @@
1
  ---
2
- title: SachAI
3
- emoji: 🦀
4
- colorFrom: green
5
- colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.30.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: SachAI API
3
+ emoji: 🔍
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: gradio
7
+ sdk_version: 4.44.1
8
+ python_version: "3.10"
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
+ Backend API for SachAI (deepfake image/video detection and news-claim checking).
14
+ Endpoints: `POST /predict`, `POST /predict-video`, `POST /predict-text`.
ai_explainer.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def generate_ai_explanation(model_prediction, model_confidence, fact_check):
2
+ """
3
+ Creates human-readable explanation using classifier + Google Fact Check data
4
+ """
5
+
6
+ confidence = model_confidence
7
+
8
+ # If Google Fact Check verified the claim
9
+ if fact_check.get("verified"):
10
+ publisher = fact_check.get("publisher", "a fact-checking source")
11
+ rating = fact_check.get("rating", "unverified")
12
+ url = fact_check.get("url", "")
13
+
14
+ rating_lower = rating.lower()
15
+
16
+ if rating_lower == "false":
17
+ final_verdict = "FAKE"
18
+ verdict_color = "red"
19
+
20
+ elif "mostly false" in rating_lower:
21
+ final_verdict = "MISLEADING"
22
+ verdict_color = "orange"
23
+
24
+ elif "misleading" in rating_lower:
25
+ final_verdict = "MISLEADING"
26
+ verdict_color = "orange"
27
+
28
+ elif rating_lower == "true":
29
+ final_verdict = "REAL"
30
+ verdict_color = "green"
31
+
32
+ elif "mostly true" in rating_lower:
33
+ final_verdict = "REAL"
34
+ verdict_color = "green"
35
+
36
+ else:
37
+ final_verdict = "NEEDS CONTEXT"
38
+ verdict_color = "yellow"
39
+
40
+ if final_verdict == "FAKE":
41
+ explanation = (
42
+ f"{publisher} reviewed this claim and rated it '{rating}'. "
43
+ "The claim contains misinformation and contradicts verified fact-checking sources."
44
+ )
45
+
46
+ elif final_verdict == "MISLEADING":
47
+ explanation = (
48
+ f"{publisher} reviewed this claim and rated it '{rating}'. "
49
+ "The claim may contain partial truths but is presented in a misleading or incomplete way."
50
+ )
51
+
52
+ elif final_verdict == "REAL":
53
+ explanation = (
54
+ f"{publisher} reviewed this claim and rated it '{rating}'. "
55
+ "The claim aligns with verified reporting and appears credible."
56
+ )
57
+
58
+ else:
59
+ explanation = (
60
+ f"{publisher} reviewed this claim and rated it '{rating}'. "
61
+ "Additional context is needed to fully assess this claim."
62
+ )
63
+
64
+ return {
65
+ "final_verdict": final_verdict,
66
+ "verdict_color": verdict_color,
67
+ "explanation": explanation,
68
+ "source_url": url,
69
+ "source_publisher": publisher
70
+ }
71
+
72
+ # Fallback if no fact-check data
73
+ if model_prediction == "FAKE":
74
+ final_verdict = "FAKE"
75
+ verdict_color = "red"
76
+
77
+ explanation = (
78
+ f"The classifier flagged this content as potentially fake with {confidence}% confidence. "
79
+ "No direct fact-check source was found, so manual verification is recommended."
80
+ )
81
+
82
+ else:
83
+ final_verdict = "REAL"
84
+ verdict_color = "green"
85
+
86
+ explanation = (
87
+ f"The classifier found this content likely credible with {confidence}% confidence. "
88
+ "However, no direct fact-check source was found."
89
+ )
90
+
91
+ return {
92
+ "final_verdict": final_verdict,
93
+ "verdict_color": verdict_color,
94
+ "explanation": explanation,
95
+ "source_url": None,
96
+ "source_publisher": None
97
+ }
app.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ZeroGPU Spaces require a @spaces.GPU function to exist at startup, and `spaces`
2
+ # must be imported before torch/tensorflow. The models here run on CPU, so this
3
+ # probe is never actually called. Harmless when `spaces` isn't installed.
4
+ try:
5
+ import spaces
6
+
7
+ @spaces.GPU
8
+ def _gpu_probe():
9
+ return True
10
+ except ImportError:
11
+ pass
12
+
13
+ from flask import Flask, request, jsonify
14
+ from flask_cors import CORS
15
+ import os
16
+ import cv2
17
+ import numpy as np
18
+ from tensorflow.keras.models import load_model
19
+ from video_utils import extract_frames
20
+ from predict_utils import predict_frame, aggregate_results
21
+ import threading
22
+ from fake_news_model import predict_fake_news, warm_up
23
+ from ai_explainer import generate_ai_explanation
24
+ from source_verifier import verify_claim_with_google
25
+
26
+ app = Flask(__name__)
27
+ CORS(app)
28
+ UPLOAD_FOLDER = "uploads"
29
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
30
+
31
+ # ✅ Load your custom trained CNN model
32
+ model = load_model("custom_cnn_model.h5")
33
+
34
+
35
+ def predict_text_internal(text):
36
+ try:
37
+ model_result = predict_fake_news(text)
38
+ model_ok = bool(
39
+ model_result and "prediction" in model_result and "confidence" in model_result
40
+ )
41
+
42
+ fact_check = verify_claim_with_google(text)
43
+
44
+ # Need at least one signal: the classifier or a published fact-check.
45
+ if not model_ok and not fact_check.get("verified"):
46
+ return jsonify({
47
+ "error": "Fake news model failed",
48
+ "details": model_result
49
+ }), 500
50
+
51
+ if not model_ok:
52
+ model_result = {"prediction": "UNKNOWN", "confidence": 0}
53
+
54
+ ai_result = generate_ai_explanation(
55
+ model_result["prediction"],
56
+ model_result["confidence"],
57
+ fact_check
58
+ )
59
+
60
+ # Safety check
61
+ if not ai_result:
62
+ return jsonify({
63
+ "error": "AI explanation failed"
64
+ }), 500
65
+
66
+ source_label = "Read Full Fact Check" if ai_result.get("source_url") else None
67
+
68
+ analysis_mode = (
69
+ "Google Fact Check + AI Classifier"
70
+ if fact_check.get("verified")
71
+ else "AI Classifier Only"
72
+ )
73
+
74
+ confidence = model_result["confidence"]
75
+
76
+ if confidence >= 90:
77
+ confidence_label = "Very High"
78
+ elif confidence >= 75:
79
+ confidence_label = "High"
80
+ elif confidence >= 50:
81
+ confidence_label = "Moderate"
82
+ else:
83
+ confidence_label = "Low"
84
+
85
+ return jsonify({
86
+ "model_prediction": model_result["prediction"],
87
+ "model_confidence": model_result["confidence"],
88
+ "confidence_label": confidence_label,
89
+
90
+ "fact_check_rating": fact_check.get("rating"),
91
+
92
+ "final_verdict": ai_result.get("final_verdict"),
93
+ "verdict_color": ai_result.get("verdict_color"),
94
+
95
+ "ai_explanation": ai_result.get("explanation"),
96
+
97
+ "source_publisher": ai_result.get("source_publisher"),
98
+ "source_url": ai_result.get("source_url"),
99
+ "source_label": source_label,
100
+
101
+ "analysis_mode": analysis_mode
102
+ })
103
+
104
+ except Exception as e:
105
+ return jsonify({
106
+ "error": "predict_text_internal failed",
107
+ "details": str(e)
108
+ }), 500
109
+
110
+ @app.route("/")
111
+ def home():
112
+ return "Flask server is running with custom CNN! 🚀"
113
+
114
+ @app.route("/predict", methods=["POST"])
115
+ def predict():
116
+ if "image" not in request.files:
117
+ return jsonify({"error": "No image file provided"}), 400
118
+
119
+ file = request.files["image"]
120
+ if file.filename == "":
121
+ return jsonify({"error": "Empty filename"}), 400
122
+
123
+ filepath = os.path.join(UPLOAD_FOLDER, file.filename)
124
+ file.save(filepath)
125
+
126
+ # ✅ Preprocess image to match CNN input
127
+ img = cv2.imread(filepath)
128
+ if img is None:
129
+ return jsonify({"error": "Invalid image"}), 400
130
+
131
+ img = cv2.resize(img, (128, 128)) # Match training size
132
+ img = img.astype(np.float32) / 255.0
133
+ img = np.expand_dims(img, axis=0)
134
+
135
+ # ✅ Predict using custom CNN model
136
+ prediction = model.predict(img)[0][0]
137
+ print(f"Raw prediction score: {prediction}")
138
+
139
+ label = "real" if prediction >= 0.5 else "deepfake"
140
+ confidence = float(prediction if label == "real" else 1 - prediction)
141
+
142
+
143
+ result = {
144
+ "filename": file.filename,
145
+ "prediction": label,
146
+ "confidence": round(confidence, 2)
147
+ }
148
+ return jsonify(result)
149
+
150
+
151
+ @app.route("/predict-video", methods=["POST"])
152
+ def predict_video():
153
+ if "video" not in request.files:
154
+ return jsonify({"error": "No video uploaded"}), 400
155
+
156
+ video = request.files["video"]
157
+ video_path = os.path.join("uploads", video.filename)
158
+ video.save(video_path)
159
+
160
+ # ✅ Extract frames (now returns arrays, not paths)
161
+ frames = extract_frames(video_path)
162
+
163
+ # ✅ Predict
164
+ results = []
165
+
166
+ for frame in frames:
167
+ result = predict_frame(frame)
168
+ results.append(result)
169
+
170
+ print("Total frames:", len(frames))
171
+ print("Results stored:", len(results))
172
+
173
+ if len(results) == 0:
174
+ return jsonify({
175
+ "error": "No faces detected in video"
176
+ }), 400
177
+
178
+ # ✅ Aggregate
179
+ final = aggregate_results(results)
180
+
181
+ # ✅ Free memory (optional but good practice)
182
+ del frames
183
+
184
+ return jsonify({
185
+ "prediction": final["result"],
186
+ "confidence": final["confidence"],
187
+ "frames_analyzed": len(results)
188
+ })
189
+
190
+ @app.route('/predict-text', methods=['POST'])
191
+ def predict_text():
192
+ data = request.get_json()
193
+ text = data.get("text", "")
194
+
195
+ if not text:
196
+ return jsonify({"error": "No text provided"}), 400
197
+
198
+ return predict_text_internal(text)
199
+
200
+
201
+ # Load the local fake-news model in the background so the first request is fast.
202
+ threading.Thread(target=warm_up, daemon=True).start()
203
+
204
+ if __name__ == "__main__":
205
+ # On a Hugging Face Space (SPACE_ID is set) listen publicly on 7860.
206
+ on_space = bool(os.environ.get("SPACE_ID"))
207
+ app.run(
208
+ host="0.0.0.0" if on_space else "127.0.0.1",
209
+ port=int(os.environ.get("PORT", 7860 if on_space else 5001)),
210
+ )
211
+
custom_cnn_model.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:746d87faccdd53c0313483fa1b2639ba456db3f8fdc042a4ed74f0a7d933d63e
3
+ size 39708648
fake_news_model.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import threading
2
+
3
+ import torch
4
+ from transformers import AutoModelForSequenceClassification, AutoTokenizer
5
+
6
+ MODEL_ID = "hamzab/roberta-fake-news-classification"
7
+ MAX_TOKENS = 512
8
+
9
+ # Runs fully locally (free, offline after the first download).
10
+ # Weights are cached by huggingface in ~/.cache/huggingface after first use.
11
+ _lock = threading.Lock()
12
+ _tokenizer = None
13
+ _model = None
14
+ _label_map = None
15
+
16
+
17
+ def _load():
18
+ global _tokenizer, _model, _label_map
19
+ if _model is not None:
20
+ return
21
+ with _lock:
22
+ if _model is not None:
23
+ return
24
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
25
+ model = AutoModelForSequenceClassification.from_pretrained(MODEL_ID).eval()
26
+ # config.id2label is {0: 'FAKE', 1: 'TRUE'}
27
+ _label_map = {
28
+ int(i): "FAKE" if str(name).upper() == "FAKE" else "REAL"
29
+ for i, name in model.config.id2label.items()
30
+ }
31
+ _tokenizer, _model = tokenizer, model
32
+
33
+
34
+ def warm_up():
35
+ """Load the model ahead of the first request (call from a background thread)."""
36
+ try:
37
+ _load()
38
+ except Exception as e:
39
+ print(f"Fake-news model warm-up failed: {e}")
40
+
41
+
42
+ def predict_fake_news(text):
43
+ try:
44
+ _load()
45
+ inputs = _tokenizer(
46
+ text, return_tensors="pt", truncation=True, max_length=MAX_TOKENS
47
+ )
48
+ with torch.no_grad():
49
+ probs = torch.softmax(_model(**inputs).logits, dim=-1)[0]
50
+
51
+ best = int(torch.argmax(probs))
52
+ return {
53
+ "prediction": _label_map[best],
54
+ "confidence": round(float(probs[best]) * 100, 2),
55
+ }
56
+
57
+ except Exception as e:
58
+ return {
59
+ "error": "Local model failed",
60
+ "details": str(e),
61
+ }
haarcascade_frontalface_default.xml ADDED
The diff for this file is too large to render. See raw diff
 
packages.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ libglib2.0-0
predict_utils.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import cv2
3
+ from tensorflow.keras.models import load_model
4
+
5
+ # ✅ LOAD MODEL
6
+ model = load_model("custom_cnn_model.h5")
7
+
8
+ # ✅ LOAD FACE DETECTOR
9
+ face_cascade = cv2.CascadeClassifier("haarcascade_frontalface_default.xml")
10
+
11
+
12
+ def detect_face(frame):
13
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
14
+
15
+ faces = face_cascade.detectMultiScale(
16
+ gray,
17
+ scaleFactor=1.1,
18
+ minNeighbors=3,
19
+ minSize=(30, 30)
20
+ )
21
+
22
+ print("Faces detected:", len(faces))
23
+
24
+ # ✅ If no face → use full frame
25
+ if len(faces) == 0:
26
+ return frame
27
+
28
+ x, y, w, h = faces[0]
29
+ face = frame[y:y+h, x:x+w]
30
+
31
+ # ✅ Safety check
32
+ if face is None or face.size == 0:
33
+ return frame
34
+
35
+ return face
36
+
37
+
38
+ def preprocess_frame(frame):
39
+ img = cv2.resize(frame, (128, 128))
40
+ img = img / 255.0
41
+ img = np.reshape(img, (1, 128, 128, 3))
42
+ return img
43
+
44
+
45
+ def predict_frame(frame):
46
+ # 🔥 Step 1: detect face or fallback
47
+ face_or_frame = detect_face(frame)
48
+
49
+ # 🔥 Step 2: preprocess
50
+ img = preprocess_frame(face_or_frame)
51
+
52
+ # 🔥 Step 3: predict
53
+ prediction = model.predict(img, verbose=0)[0][0]
54
+
55
+ if prediction > 0.5:
56
+ return "REAL", float(1 - prediction)
57
+ else:
58
+ return "FAKE", float(prediction)
59
+
60
+
61
+ def aggregate_results(results):
62
+ fake_scores = [s for l, s in results if l == "FAKE"]
63
+ real_scores = [s for l, s in results if l == "REAL"]
64
+
65
+ if len(fake_scores) > len(real_scores):
66
+ return {
67
+ "result": "FAKE",
68
+ "confidence": sum(fake_scores) / len(results)
69
+ }
70
+ else:
71
+ return {
72
+ "result": "REAL",
73
+ "confidence": sum(real_scores) / len(results)
74
+ }
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ flask==3.0.3
2
+ flask-cors==5.0.0
3
+ python-dotenv==1.0.1
4
+ requests==2.31.0
5
+ numpy==1.24.3
6
+ pillow==10.3.0
7
+ opencv-python-headless==4.10.0.84
8
+ tensorflow==2.15.0
9
+ torch==2.4.0
10
+ transformers==4.46.3
source_verifier.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import requests
4
+ from dotenv import load_dotenv
5
+
6
+ load_dotenv()
7
+
8
+ GOOGLE_FACT_CHECK_API_KEY = os.getenv("GOOGLE_FACT_CHECK_API_KEY")
9
+
10
+
11
+ def verify_claim_with_google(claim):
12
+ if not GOOGLE_FACT_CHECK_API_KEY:
13
+ return {
14
+ "verified": False,
15
+ "error": "GOOGLE_FACT_CHECK_API_KEY is not set",
16
+ }
17
+
18
+ url = "https://factchecktools.googleapis.com/v1alpha1/claims:search"
19
+
20
+ params = {
21
+ "query": claim,
22
+ "key": GOOGLE_FACT_CHECK_API_KEY,
23
+ "languageCode": "en"
24
+ }
25
+
26
+ try:
27
+ response = requests.get(url, params=params, timeout=10)
28
+ except requests.RequestException as e:
29
+ return {
30
+ "verified": False,
31
+ "error": str(e)
32
+ }
33
+
34
+ if response.status_code != 200:
35
+ return {
36
+ "verified": False,
37
+ "error": response.text
38
+ }
39
+
40
+ data = response.json()
41
+
42
+ claims = data.get("claims", [])
43
+
44
+ if not claims:
45
+ return {
46
+ "verified": False,
47
+ "message": "No fact-check data found."
48
+ }
49
+
50
+ first_claim = claims[0]
51
+
52
+ claimant = first_claim.get("claimant", "Unknown")
53
+ text = first_claim.get("text", claim)
54
+
55
+ reviews = first_claim.get("claimReview", [])
56
+
57
+ if reviews:
58
+ review = reviews[0]
59
+
60
+ return {
61
+ "verified": True,
62
+ "claim": text,
63
+ "claimant": claimant,
64
+ "publisher": review.get("publisher", {}).get("name", "Unknown"),
65
+ "rating": review.get("textualRating", "Unknown"),
66
+ "url": review.get("url", "")
67
+ }
68
+
69
+ return {
70
+ "verified": False,
71
+ "message": "Claim found but no review available."
72
+ }
video_utils.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+
3
+ def extract_frames(video_path, frame_skip=7):
4
+ cap = cv2.VideoCapture(video_path)
5
+ frames = []
6
+ count = 0
7
+
8
+ while cap.isOpened():
9
+ ret, frame = cap.read()
10
+ if not ret:
11
+ break
12
+
13
+ if count % frame_skip == 0:
14
+ frames.append(frame)
15
+
16
+ count += 1
17
+
18
+ cap.release()
19
+ return frames