Harman823 commited on
Commit
6a7004b
·
verified ·
1 Parent(s): 1060ac6

Deploy FN Detector backend

Browse files
app.py CHANGED
@@ -2,7 +2,7 @@ from pathlib import Path
2
  import os
3
  import sys
4
 
5
- from fastapi import FastAPI, HTTPException
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from pydantic import BaseModel, Field
8
 
@@ -12,6 +12,10 @@ SRC_DIR = ROOT / "src"
12
  if str(SRC_DIR) not in sys.path:
13
  sys.path.insert(0, str(SRC_DIR))
14
 
 
 
 
 
15
  from fake_news_detector.prediction import ( # noqa: E402
16
  combine_news_text,
17
  get_model_snapshot,
@@ -29,6 +33,7 @@ def _parse_cors_origins() -> list[str]:
29
 
30
 
31
  MODEL_DIR = ROOT / os.environ.get("MODEL_DIR", "deployment/model")
 
32
 
33
  app = FastAPI(
34
  title="FN Detector API",
@@ -56,12 +61,25 @@ def root() -> dict:
56
  "service": "fn-detector-api",
57
  "status": "online",
58
  "model_dir": str(MODEL_DIR),
59
- "routes": ["/health", "/metrics", "/predict"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
 
63
- @app.get("/health")
64
- def health() -> dict:
65
  try:
66
  snapshot = get_model_snapshot(MODEL_DIR)
67
  except FileNotFoundError as exc:
@@ -78,8 +96,13 @@ def health() -> dict:
78
  }
79
 
80
 
81
- @app.get("/metrics")
82
- def metrics() -> dict:
 
 
 
 
 
83
  try:
84
  return {
85
  "status": "ok",
@@ -93,8 +116,13 @@ def metrics() -> dict:
93
  raise HTTPException(status_code=500, detail=f"Unable to read metrics: {exc}") from exc
94
 
95
 
96
- @app.post("/predict")
97
- def predict(request: PredictionRequest) -> dict:
 
 
 
 
 
98
  combined_text = combine_news_text(request.title, request.text)
99
  if not combined_text:
100
  raise HTTPException(status_code=400, detail="Please provide a title or article body.")
@@ -117,3 +145,68 @@ def predict(request: PredictionRequest) -> dict:
117
  },
118
  "prediction": prediction,
119
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import os
3
  import sys
4
 
5
+ from fastapi import FastAPI, File, HTTPException, UploadFile
6
  from fastapi.middleware.cors import CORSMiddleware
7
  from pydantic import BaseModel, Field
8
 
 
12
  if str(SRC_DIR) not in sys.path:
13
  sys.path.insert(0, str(SRC_DIR))
14
 
15
+ from fake_news_detector.deepfake_detection import ( # noqa: E402
16
+ get_deepfake_model_snapshot,
17
+ predict_deepfake_image,
18
+ )
19
  from fake_news_detector.prediction import ( # noqa: E402
20
  combine_news_text,
21
  get_model_snapshot,
 
33
 
34
 
35
  MODEL_DIR = ROOT / os.environ.get("MODEL_DIR", "deployment/model")
36
+ DEEPFAKE_MODEL_DIR = ROOT / os.environ.get("DEEPFAKE_MODEL_DIR", "deployment/deepfake_model")
37
 
38
  app = FastAPI(
39
  title="FN Detector API",
 
61
  "service": "fn-detector-api",
62
  "status": "online",
63
  "model_dir": str(MODEL_DIR),
64
+ "deepfake_model_dir": str(DEEPFAKE_MODEL_DIR),
65
+ "routes": [
66
+ "/health",
67
+ "/metrics",
68
+ "/predict",
69
+ "/deepfake/health",
70
+ "/deepfake/metrics",
71
+ "/deepfake/predict",
72
+ "/api/health",
73
+ "/api/metrics",
74
+ "/api/predict",
75
+ "/api/deepfake/health",
76
+ "/api/deepfake/metrics",
77
+ "/api/deepfake/predict",
78
+ ],
79
  }
80
 
81
 
82
+ def _news_health() -> dict:
 
83
  try:
84
  snapshot = get_model_snapshot(MODEL_DIR)
85
  except FileNotFoundError as exc:
 
96
  }
97
 
98
 
99
+ @app.get("/health")
100
+ @app.get("/api/health")
101
+ def health() -> dict:
102
+ return _news_health()
103
+
104
+
105
+ def _news_metrics() -> dict:
106
  try:
107
  return {
108
  "status": "ok",
 
116
  raise HTTPException(status_code=500, detail=f"Unable to read metrics: {exc}") from exc
117
 
118
 
119
+ @app.get("/metrics")
120
+ @app.get("/api/metrics")
121
+ def metrics() -> dict:
122
+ return _news_metrics()
123
+
124
+
125
+ def _news_predict(request: PredictionRequest) -> dict:
126
  combined_text = combine_news_text(request.title, request.text)
127
  if not combined_text:
128
  raise HTTPException(status_code=400, detail="Please provide a title or article body.")
 
145
  },
146
  "prediction": prediction,
147
  }
148
+
149
+
150
+ @app.post("/predict")
151
+ @app.post("/api/predict")
152
+ def predict(request: PredictionRequest) -> dict:
153
+ return _news_predict(request)
154
+
155
+
156
+ def _deepfake_health() -> dict:
157
+ try:
158
+ snapshot = get_deepfake_model_snapshot(DEEPFAKE_MODEL_DIR)
159
+ except FileNotFoundError as exc:
160
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
161
+ return {
162
+ "status": "ok",
163
+ "service": "fn-detector-api",
164
+ "deepfake_model": snapshot["summary"].get("model_name"),
165
+ "mode": snapshot["summary"].get("status"),
166
+ "feature_count": len(snapshot["feature_names"]),
167
+ }
168
+
169
+
170
+ @app.get("/deepfake/health")
171
+ @app.get("/api/deepfake/health")
172
+ def deepfake_health() -> dict:
173
+ return _deepfake_health()
174
+
175
+
176
+ def _deepfake_metrics() -> dict:
177
+ try:
178
+ return {
179
+ "status": "ok",
180
+ "snapshot": get_deepfake_model_snapshot(DEEPFAKE_MODEL_DIR),
181
+ }
182
+ except FileNotFoundError as exc:
183
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
184
+
185
+
186
+ @app.get("/deepfake/metrics")
187
+ @app.get("/api/deepfake/metrics")
188
+ def deepfake_metrics() -> dict:
189
+ return _deepfake_metrics()
190
+
191
+
192
+ def _deepfake_predict(filename: str, payload: bytes) -> dict:
193
+ try:
194
+ prediction = predict_deepfake_image(
195
+ model_dir=DEEPFAKE_MODEL_DIR,
196
+ image_bytes=payload,
197
+ filename=filename,
198
+ )
199
+ except ValueError as exc:
200
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
201
+ except FileNotFoundError as exc:
202
+ raise HTTPException(status_code=503, detail=str(exc)) from exc
203
+ except Exception as exc: # pragma: no cover - defensive for deployment only
204
+ raise HTTPException(status_code=500, detail=f"Deepfake prediction failed: {exc}") from exc
205
+
206
+ return {"status": "ok", "prediction": prediction}
207
+
208
+
209
+ @app.post("/deepfake/predict")
210
+ @app.post("/api/deepfake/predict")
211
+ async def deepfake_predict(file: UploadFile = File(...)) -> dict:
212
+ return _deepfake_predict(file.filename or "upload", await file.read())
deployment/deepfake_model/bundle.json ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "feature_names": [
3
+ "gray_mean",
4
+ "gray_std",
5
+ "laplacian_var",
6
+ "gradient_mean",
7
+ "gradient_std",
8
+ "high_frequency_ratio",
9
+ "blockiness",
10
+ "jpeg_residual_mean",
11
+ "jpeg_residual_std",
12
+ "mirror_difference",
13
+ "saturation_mean",
14
+ "saturation_std"
15
+ ],
16
+ "scaler": {
17
+ "mean": [
18
+ 0.3894135057926178,
19
+ 0.16643017530441284,
20
+ 0.0016245938604697585,
21
+ 0.01306572463363409,
22
+ 0.0241470355540514,
23
+ 0.30300813913345337,
24
+ 1.007244348526001,
25
+ 0.008029050193727016,
26
+ 0.007909948006272316,
27
+ 0.11933569610118866,
28
+ 0.17327596247196198,
29
+ 0.07436472177505493
30
+ ],
31
+ "scale": [
32
+ 0.0831027701497078,
33
+ 0.04393522068858147,
34
+ 0.0009263058891519904,
35
+ 0.004046801012009382,
36
+ 0.006974863354116678,
37
+ 0.03189605101943016,
38
+ 0.08480057865381241,
39
+ 0.001521392841823399,
40
+ 0.0017849425785243511,
41
+ 0.06415991485118866,
42
+ 0.04083705693483353,
43
+ 0.032066166400909424
44
+ ]
45
+ },
46
+ "model": {
47
+ "coefficients": [
48
+ -0.1552580323,
49
+ 0.4495079971,
50
+ -0.1030570684,
51
+ 0.6508357217,
52
+ -0.2056009272,
53
+ -0.4020637292,
54
+ -0.1068058709,
55
+ -0.3841619248,
56
+ -0.6691807026,
57
+ 0.9157355483,
58
+ -1.0115699767,
59
+ 0.3580950632
60
+ ],
61
+ "intercept": 0.0585737738
62
+ },
63
+ "training_summary": {
64
+ "status": "trained",
65
+ "model_name": "lightweight-deepfake-linear",
66
+ "dataset_dir": "C:\\Users\\harma\\OneDrive\\Desktop\\FN\\FNdetector\\external\\DeepFake-Detect\\prepared_dataset",
67
+ "dataset_rows": 22,
68
+ "train_rows": 16,
69
+ "test_rows": 6,
70
+ "max_images_per_label": 32,
71
+ "accuracy": 1.0,
72
+ "classification_report": {
73
+ "real": {
74
+ "precision": 1.0,
75
+ "recall": 1.0,
76
+ "f1-score": 1.0,
77
+ "support": 3.0
78
+ },
79
+ "fake": {
80
+ "precision": 1.0,
81
+ "recall": 1.0,
82
+ "f1-score": 1.0,
83
+ "support": 3.0
84
+ },
85
+ "accuracy": 1.0,
86
+ "macro avg": {
87
+ "precision": 1.0,
88
+ "recall": 1.0,
89
+ "f1-score": 1.0,
90
+ "support": 6.0
91
+ },
92
+ "weighted avg": {
93
+ "precision": 1.0,
94
+ "recall": 1.0,
95
+ "f1-score": 1.0,
96
+ "support": 6.0
97
+ }
98
+ },
99
+ "system_profile": {
100
+ "cpu": "Intel Core i7-8550U class",
101
+ "ram_gb": 16,
102
+ "recommended_reason": "Conservative cap to avoid memory spikes on laptop-class CPUs."
103
+ }
104
+ }
105
+ }
requirements.txt CHANGED
@@ -3,3 +3,5 @@ transformers>=4.51,<5.0
3
  safetensors>=0.5,<1.0
4
  fastapi>=0.117,<1.0
5
  uvicorn>=0.35,<1.0
 
 
 
3
  safetensors>=0.5,<1.0
4
  fastapi>=0.117,<1.0
5
  uvicorn>=0.35,<1.0
6
+ python-multipart>=0.0.20,<1.0
7
+ Pillow>=11.2,<12.0
src/fake_news_detector/deepfake_detection.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from functools import lru_cache
5
+ import io
6
+ import json
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ from PIL import Image
11
+
12
+
13
+ IMAGE_SIZE = 128
14
+ MAX_IMAGE_BYTES = 8 * 1024 * 1024
15
+ FEATURE_NAMES = [
16
+ "gray_mean",
17
+ "gray_std",
18
+ "laplacian_var",
19
+ "gradient_mean",
20
+ "gradient_std",
21
+ "high_frequency_ratio",
22
+ "blockiness",
23
+ "jpeg_residual_mean",
24
+ "jpeg_residual_std",
25
+ "mirror_difference",
26
+ "saturation_mean",
27
+ "saturation_std",
28
+ ]
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class DeepfakeModelBundle:
33
+ feature_names: tuple[str, ...]
34
+ mean: np.ndarray
35
+ scale: np.ndarray
36
+ coefficients: np.ndarray
37
+ intercept: float
38
+ training_summary: dict
39
+
40
+
41
+ def _sigmoid(value: float) -> float:
42
+ return float(1.0 / (1.0 + np.exp(-value)))
43
+
44
+
45
+ def _resolve_model_dir(model_dir: Path) -> Path:
46
+ resolved = Path(model_dir).resolve()
47
+ if not resolved.exists():
48
+ raise FileNotFoundError(f"Deepfake model directory does not exist: {resolved}")
49
+ return resolved
50
+
51
+
52
+ def _load_image(source: bytes) -> Image.Image:
53
+ if not source:
54
+ raise ValueError("No image content was provided.")
55
+ if len(source) > MAX_IMAGE_BYTES:
56
+ raise ValueError("Image is too large. Please upload an image under 8 MB.")
57
+ try:
58
+ image = Image.open(io.BytesIO(source))
59
+ except Exception as exc: # pragma: no cover - defensive for malformed uploads
60
+ raise ValueError("Unable to decode the uploaded image.") from exc
61
+ return image.convert("RGB")
62
+
63
+
64
+ def _prepare_rgb_array(image: Image.Image) -> np.ndarray:
65
+ resized = image.resize((IMAGE_SIZE, IMAGE_SIZE), Image.Resampling.BILINEAR)
66
+ return np.asarray(resized, dtype=np.float32) / 255.0
67
+
68
+
69
+ def _compute_gray(rgb: np.ndarray) -> np.ndarray:
70
+ return np.dot(rgb[..., :3], np.array([0.2989, 0.5870, 0.1140], dtype=np.float32))
71
+
72
+
73
+ def _compute_laplacian(gray: np.ndarray) -> np.ndarray:
74
+ center = gray[1:-1, 1:-1]
75
+ return (
76
+ gray[:-2, 1:-1]
77
+ + gray[2:, 1:-1]
78
+ + gray[1:-1, :-2]
79
+ + gray[1:-1, 2:]
80
+ - (4.0 * center)
81
+ )
82
+
83
+
84
+ def _compute_frequency_ratio(gray: np.ndarray) -> float:
85
+ centered = gray - float(gray.mean())
86
+ spectrum = np.abs(np.fft.rfft2(centered))
87
+ if not np.any(spectrum):
88
+ return 0.0
89
+
90
+ height, width = gray.shape
91
+ y_coords = np.fft.fftfreq(height)[:, None]
92
+ x_coords = np.fft.rfftfreq(width)[None, :]
93
+ radius = np.sqrt((y_coords**2) + (x_coords**2))
94
+ high_mask = radius >= 0.18
95
+ high_energy = float(spectrum[high_mask].sum())
96
+ total_energy = float(spectrum.sum()) + 1e-8
97
+ return high_energy / total_energy
98
+
99
+
100
+ def _compute_blockiness(gray: np.ndarray) -> float:
101
+ vertical_boundaries = gray[:, 8::8] - gray[:, 7:-1:8]
102
+ horizontal_boundaries = gray[8::8, :] - gray[7:-1:8, :]
103
+ all_vertical = np.diff(gray, axis=1)
104
+ all_horizontal = np.diff(gray, axis=0)
105
+ boundary_energy = float(np.mean(np.abs(vertical_boundaries))) + float(
106
+ np.mean(np.abs(horizontal_boundaries))
107
+ )
108
+ overall_energy = float(np.mean(np.abs(all_vertical))) + float(np.mean(np.abs(all_horizontal))) + 1e-8
109
+ return boundary_energy / overall_energy
110
+
111
+
112
+ def _compute_jpeg_residual(rgb_uint8: np.ndarray) -> tuple[float, float]:
113
+ image = Image.fromarray(rgb_uint8, mode="RGB")
114
+ buffer = io.BytesIO()
115
+ image.save(buffer, format="JPEG", quality=72, optimize=True)
116
+ recompressed = Image.open(io.BytesIO(buffer.getvalue())).convert("RGB")
117
+ diff = np.abs(rgb_uint8.astype(np.float32) - np.asarray(recompressed, dtype=np.float32)) / 255.0
118
+ return float(diff.mean()), float(diff.std())
119
+
120
+
121
+ def _compute_saturation(rgb: np.ndarray) -> np.ndarray:
122
+ channel_max = rgb.max(axis=2)
123
+ channel_min = rgb.min(axis=2)
124
+ return channel_max - channel_min
125
+
126
+
127
+ def extract_feature_dict(image: Image.Image) -> dict[str, float]:
128
+ rgb = _prepare_rgb_array(image)
129
+ rgb_uint8 = np.clip(np.round(rgb * 255.0), 0, 255).astype(np.uint8)
130
+ gray = _compute_gray(rgb)
131
+ laplacian = _compute_laplacian(gray)
132
+ gradients = np.concatenate(
133
+ [
134
+ np.abs(np.diff(gray, axis=0)).ravel(),
135
+ np.abs(np.diff(gray, axis=1)).ravel(),
136
+ ]
137
+ )
138
+ saturation = _compute_saturation(rgb)
139
+ mirrored = np.flip(rgb, axis=1)
140
+ jpeg_mean, jpeg_std = _compute_jpeg_residual(rgb_uint8)
141
+
142
+ values = {
143
+ "gray_mean": float(gray.mean()),
144
+ "gray_std": float(gray.std()),
145
+ "laplacian_var": float(laplacian.var()),
146
+ "gradient_mean": float(gradients.mean()),
147
+ "gradient_std": float(gradients.std()),
148
+ "high_frequency_ratio": _compute_frequency_ratio(gray),
149
+ "blockiness": _compute_blockiness(gray),
150
+ "jpeg_residual_mean": jpeg_mean,
151
+ "jpeg_residual_std": jpeg_std,
152
+ "mirror_difference": float(np.mean(np.abs(rgb - mirrored))),
153
+ "saturation_mean": float(saturation.mean()),
154
+ "saturation_std": float(saturation.std()),
155
+ }
156
+ return values
157
+
158
+
159
+ def extract_feature_vector(image: Image.Image) -> np.ndarray:
160
+ feature_dict = extract_feature_dict(image)
161
+ return np.array([feature_dict[name] for name in FEATURE_NAMES], dtype=np.float32)
162
+
163
+
164
+ def _default_training_summary(model_dir: Path) -> dict:
165
+ return {
166
+ "status": "heuristic",
167
+ "model_name": "artifact-heuristic-baseline",
168
+ "model_dir": str(model_dir),
169
+ "notes": "No trained lightweight deepfake bundle was found, so the fallback heuristic is active.",
170
+ }
171
+
172
+
173
+ @lru_cache(maxsize=2)
174
+ def load_deepfake_bundle(model_dir_value: str) -> DeepfakeModelBundle | None:
175
+ model_dir = _resolve_model_dir(Path(model_dir_value))
176
+ bundle_path = model_dir / "bundle.json"
177
+ if not bundle_path.exists():
178
+ return None
179
+
180
+ payload = json.loads(bundle_path.read_text(encoding="utf-8"))
181
+ return DeepfakeModelBundle(
182
+ feature_names=tuple(payload.get("feature_names", FEATURE_NAMES)),
183
+ mean=np.array(payload["scaler"]["mean"], dtype=np.float32),
184
+ scale=np.array(payload["scaler"]["scale"], dtype=np.float32),
185
+ coefficients=np.array(payload["model"]["coefficients"], dtype=np.float32),
186
+ intercept=float(payload["model"]["intercept"]),
187
+ training_summary=payload.get("training_summary", {}),
188
+ )
189
+
190
+
191
+ def get_deepfake_model_snapshot(model_dir: Path) -> dict:
192
+ resolved = _resolve_model_dir(model_dir)
193
+ bundle = load_deepfake_bundle(str(resolved))
194
+ summary = bundle.training_summary if bundle else _default_training_summary(resolved)
195
+
196
+ return {
197
+ "model_dir": str(resolved),
198
+ "available": bundle is not None,
199
+ "feature_names": FEATURE_NAMES,
200
+ "summary": summary,
201
+ }
202
+
203
+
204
+ def _heuristic_prediction(features: dict[str, float]) -> float:
205
+ raw_score = (
206
+ 2.8 * features["jpeg_residual_mean"]
207
+ + 1.6 * features["high_frequency_ratio"]
208
+ + 0.7 * max(features["blockiness"] - 1.0, 0.0)
209
+ + 1.4 * features["mirror_difference"]
210
+ + 8.0 * features["laplacian_var"]
211
+ - 0.6 * features["saturation_mean"]
212
+ )
213
+ centered = (raw_score - 0.22) * 4.0
214
+ return min(max(_sigmoid(centered), 0.02), 0.98)
215
+
216
+
217
+ def predict_deepfake_image(model_dir: Path, image_bytes: bytes, filename: str | None = None) -> dict:
218
+ image = _load_image(image_bytes)
219
+ features = extract_feature_dict(image)
220
+ vector = np.array([features[name] for name in FEATURE_NAMES], dtype=np.float32)
221
+
222
+ bundle = load_deepfake_bundle(str(_resolve_model_dir(model_dir)))
223
+ if bundle is None:
224
+ fake_score = _heuristic_prediction(features)
225
+ model_name = "artifact-heuristic-baseline"
226
+ model_status = "heuristic"
227
+ else:
228
+ normalized = (vector - bundle.mean) / np.where(bundle.scale == 0, 1.0, bundle.scale)
229
+ logit = float(np.dot(normalized, bundle.coefficients) + bundle.intercept)
230
+ fake_score = _sigmoid(logit)
231
+ model_name = str(bundle.training_summary.get("model_name", "lightweight-deepfake-linear"))
232
+ model_status = "trained"
233
+
234
+ real_score = 1.0 - fake_score
235
+ top_feature_names = sorted(
236
+ FEATURE_NAMES,
237
+ key=lambda feature_name: abs(features[feature_name]),
238
+ reverse=True,
239
+ )[:4]
240
+
241
+ return {
242
+ "prediction": "fake" if fake_score >= 0.5 else "real",
243
+ "confidence": float(max(fake_score, real_score)),
244
+ "scores": {
245
+ "fake": float(fake_score),
246
+ "real": float(real_score),
247
+ },
248
+ "model_name": model_name,
249
+ "model_status": model_status,
250
+ "filename": filename or "upload",
251
+ "image_size": {"width": image.width, "height": image.height},
252
+ "features": features,
253
+ "top_signals": [
254
+ {"name": name, "value": float(features[name])}
255
+ for name in top_feature_names
256
+ ],
257
+ }
src/fake_news_detector/prediction.py CHANGED
@@ -2,8 +2,12 @@ from dataclasses import dataclass
2
  from functools import lru_cache
3
  from pathlib import Path
4
  import json
 
5
  import re
6
 
 
 
 
7
  import torch
8
  from transformers import (
9
  AutoModelForSequenceClassification,
 
2
  from functools import lru_cache
3
  from pathlib import Path
4
  import json
5
+ import os
6
  import re
7
 
8
+ os.environ.setdefault("TRANSFORMERS_NO_TF", "1")
9
+ os.environ.setdefault("USE_TF", "0")
10
+
11
  import torch
12
  from transformers import (
13
  AutoModelForSequenceClassification,