DeepActionPotential commited on
Commit
e0f2d0e
·
verified ·
1 Parent(s): 7a06733

Initial project upload via Python API for Flask Space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +9 -0
  2. Dockerfile +19 -0
  3. LICENCE +21 -0
  4. README.md +351 -7
  5. app.py +170 -0
  6. config.py +59 -0
  7. core/fake_manager.py +190 -0
  8. demo/demo.mp4 +3 -0
  9. demo/demo1.png +0 -0
  10. demo/demo2.png +3 -0
  11. demo/demo3.png +0 -0
  12. demo/demo4.png +3 -0
  13. demo/demo5.png +3 -0
  14. demo/demo6.png +0 -0
  15. demo/demo7.png +0 -0
  16. models/Meso4_DF.h5 +3 -0
  17. models/Meso4_F2F.h5 +3 -0
  18. models/ai_text_detector.joblib +3 -0
  19. models/efficientnet_b3_full_ai_image_classifier.pt +3 -0
  20. models/face_det_10g.onnx +3 -0
  21. models/fake_news_detector.pt +3 -0
  22. models/models.py +27 -0
  23. models/word2idx.pt +3 -0
  24. notebooks/ai-generated-vs-human-image-99-f1-score.ipynb +0 -0
  25. notebooks/ai-vs-human-text-classifier-99-f1-score.ipynb +0 -0
  26. notebooks/fake-news-lstm-classifier-f1-score-99.ipynb +0 -0
  27. requirements.txt +13 -0
  28. schemas/fact_search_schemas.py +36 -0
  29. schemas/fake_manager_schemas.py +178 -0
  30. schemas/text_schemas.py +71 -0
  31. schemas/vision_schemas.py +94 -0
  32. services/ai_image_service.py +76 -0
  33. services/ai_text_service.py +68 -0
  34. services/deepfake_service.py +127 -0
  35. services/face_detection_service.py +137 -0
  36. services/fact_search_service.py +77 -0
  37. services/fake_text_news_service.py +132 -0
  38. services/search_quries_service.py +48 -0
  39. services/text_emotion_service.py +132 -0
  40. static/css/style.css +335 -0
  41. static/js/script.js +185 -0
  42. static/uploads/0a42e714b24849829247e0a44e51953a.webp +0 -0
  43. static/uploads/0d337020f9ae491782633516889f3e79.webp +0 -0
  44. static/uploads/26ad0b08883347718d38274861bccd8e.webp +0 -0
  45. static/uploads/26de02ceda464fb9acd0edb6b0c678db.webp +0 -0
  46. static/uploads/28afe2d042a341ceb60c187954deccfc.jpg +3 -0
  47. static/uploads/3156289a7e0e46fa82b7d4f0965aeae7.webp +0 -0
  48. static/uploads/37f95b1e0e9c44b79e527a0deafab813.webp +0 -0
  49. static/uploads/3b69dc4d23714b3a801c5131b476fafd.webp +0 -0
  50. static/uploads/45aa41d76e71411bb28a19de398ba785.webp +0 -0
.gitattributes CHANGED
@@ -33,3 +33,12 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ demo/demo.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ demo/demo2.png filter=lfs diff=lfs merge=lfs -text
38
+ demo/demo4.png filter=lfs diff=lfs merge=lfs -text
39
+ demo/demo5.png filter=lfs diff=lfs merge=lfs -text
40
+ static/uploads/28afe2d042a341ceb60c187954deccfc.jpg filter=lfs diff=lfs merge=lfs -text
41
+ static/uploads/8faf0f3a7992493d9870225198e49273.jpg filter=lfs diff=lfs merge=lfs -text
42
+ static/uploads/a326c59eb34f48ab939624dcac5bd556.jpg filter=lfs diff=lfs merge=lfs -text
43
+ static/uploads/d9ab337c2ef44bc5ac7b2d2a8b136b19.jpg filter=lfs diff=lfs merge=lfs -text
44
+ static/uploads/e3e074e58f9545b294ec541566759e45.jpg filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 1. Use a base Python image
2
+ FROM python:3.10-slim
3
+
4
+ # 2. Set the working directory inside the container
5
+ WORKDIR /app
6
+
7
+ # 3. Copy the dependencies file and install them
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ # 4. Copy the rest of your application code
12
+ COPY . .
13
+
14
+ # 5. Expose the required port (Hugging Face default)
15
+ EXPOSE 7860
16
+
17
+ # 6. Start the production server (Gunicorn is highly recommended for production)
18
+ # The format is: gunicorn -b <host>:<port> <app_file>:<flask_app_object>
19
+ CMD ["gunicorn", "-b", "0.0.0.0:7860", "app:app"]
LICENCE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Eslam Tarek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md CHANGED
@@ -1,11 +1,355 @@
1
  ---
2
  title: FactSight
3
- emoji: 👁
4
- colorFrom: gray
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: FactSight
3
+ emoji: 🚀
4
+ colorFrom: blue
5
+ colorTo: purple
6
+ sdk: docker
7
+ # Optional: You can specify the port if it's not the default 7860
8
+ # app_port: 7860
9
  ---
10
 
11
+
12
+ # FactSight: Advanced Fake News Detection System
13
+
14
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
15
+ [![Flask](https://img.shields.io/badge/flask-3.1.1-green.svg)](https://flask.palletsprojects.com/)
16
+ [![PyTorch](https://img.shields.io/badge/pytorch-2.8.0-red.svg)](https://pytorch.org/)
17
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
18
+
19
+ ## 📋 Table of Contents
20
+
21
+ - [Overview](#overview)
22
+ - [Architecture](#architecture)
23
+ - [Installation](#installation)
24
+ - [Usage](#usage)
25
+ - [Services & Models Deep Dive](#services--models-deep-dive)
26
+ - [Text Analysis Services](#text-analysis-services)
27
+ - [Image Analysis Services](#image-analysis-services)
28
+ - [Fact-Checking Integration](#fact-checking-integration)
29
+ - [Model Performance](#model-performance)
30
+ - [Demo Media Files](#demo-media-files)
31
+ - [Configuration](#configuration)
32
+ - [API Endpoints](#api-endpoints)
33
+ - [Contributing](#contributing)
34
+ - [License](#license)
35
+
36
+ ## 🎯 Overview
37
+
38
+ **FactSight** is a comprehensive fake news detection system that combines multiple AI/ML technologies to analyze news content for authenticity. The system performs multi-modal analysis using both **text** and **image** content to determine if news articles are genuine or fabricated.
39
+
40
+ ### Key Features
41
+
42
+ - **🔍 Multi-Modal Analysis**: Combines text and image analysis for comprehensive fact-checking
43
+ - **🤖 AI Content Detection**: Identifies AI-generated text and images
44
+ - **🧠 Deep Learning Models**: Uses state-of-the-art neural networks for classification
45
+ - **🔗 External Fact-Checking**: Integrates with Google Fact Check Tools API
46
+ - **📊 Emotion Analysis**: Detects sensationalism in news content
47
+ - **👤 Face Analysis**: Analyzes faces in images for deepfake detection
48
+ - **🌐 Web Interface**: User-friendly web application for easy analysis
49
+
50
+ ## 🏗️ Architecture
51
+
52
+ ```
53
+ ┌─────────────────────────────────────────────────────────────────┐
54
+ │ FactSight System │
55
+ ├─────────────────────────────────────────────────────────────────┤
56
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
57
+ │ │ Flask │ │ Core │ │ Services │ │
58
+ │ │ Web App │ │ Manager │ │ & Models │ │
59
+ │ └─────────────┘ └─────────────┘ └─────────────┘ │
60
+ ├─────────────────────────────────────────────────────────────────┤
61
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
62
+ │ │ Text │ │ Image │ │ Fact │ │
63
+ │ │ Analysis │ │ Analysis │ │ Checking │ │
64
+ │ └─────────────┘ └─────────────┘ └─────────────┘ │
65
+ ├─────────────────────────────────────────────────────────────────┤
66
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
67
+ │ │ LSTM │ │ EfficientNet│ │ Google │ │
68
+ │ │ Classifier │ │ B3 │ │ Fact Check │ │
69
+ │ └─────────────┘ └─────────────┘ └─────────────┘ │
70
+ └─────────────────────────────────────────────────────────────────┘
71
+ ```
72
+
73
+ ## 🚀 Installation
74
+
75
+ ### Prerequisites
76
+
77
+ - Python 3.11+
78
+ - CUDA-compatible GPU (recommended for faster inference)
79
+ - 8GB+ RAM
80
+ - 4GB+ free disk space
81
+
82
+ ### Setup Instructions
83
+
84
+ 1. **Clone the repository**
85
+ ```bash
86
+ git clone <repository-url>
87
+ cd FactSight
88
+ ```
89
+
90
+ 2. **Install dependencies**
91
+ ```bash
92
+ pip install -r requirements.txt
93
+ ```
94
+
95
+ 3. **Download required models from [HUGGINGFACE](<https://huggingface.co/>)** (if not included)
96
+ ```bash
97
+ # Models will be automatically downloaded on first run
98
+ # or placed in the models/ directory
99
+ ```
100
+
101
+ 4. **Configure environment**
102
+ ```bash
103
+ # Set your Google Fact Check API key in config.py
104
+ FACT_API_KEY = "your-api-key-here"
105
+ ```
106
+
107
+ 5. **Run the application**
108
+ ```bash
109
+ python app.py
110
+ ```
111
+
112
+ 6. **Access the web interface**
113
+ ```
114
+ Open http://localhost:5000 in your browser
115
+ ```
116
+
117
+ ## 📖 Usage
118
+
119
+ ### Web Interface
120
+
121
+ 1. **Navigate to the homepage** (`/`)
122
+ 2. **Paste news text** in the text area (max 10,000 characters)
123
+ 3. **Upload images** related to the news (drag & drop or browse)
124
+ 4. **Click "Analyze"** to start the fact-checking process
125
+ 5. **View detailed results** including:
126
+ - Overall authenticity score
127
+ - Text analysis breakdown
128
+ - Image analysis results
129
+ - Fact-checking verification
130
+ - Emotion analysis
131
+
132
+ ### API Usage
133
+
134
+ ```python
135
+ import requests
136
+
137
+ # Submit for analysis
138
+ response = requests.post('http://localhost:5000/analyze', json={
139
+ 'text': 'Your news article text here...',
140
+ 'images': [
141
+ {'data': 'base64-encoded-image-data'}
142
+ ]
143
+ })
144
+
145
+ analysis_id = response.json()['analysis_id']
146
+
147
+ # Get results
148
+ results = requests.get(f'http://localhost:5000/analysis/{analysis_id}')
149
+ ```
150
+
151
+ ## 🔬 Services & Models Deep Dive
152
+
153
+ ### Text Analysis Services
154
+
155
+ #### 1. **AI Text Detection** (`ai_text_service.py`)
156
+ - **Model**: Custom-trained classifier [placeholder for url]
157
+ - **Architecture**: Joblib-based scikit-learn model
158
+ - **Performance**: **99% F1-Score**
159
+ - **Purpose**: Distinguishes between AI-generated and human-written text
160
+ - **Features**: Text preprocessing, feature extraction, binary classification
161
+
162
+ #### 2. **Fake News Classification** (`fake_text_news_service.py`)
163
+ - **Model**: Custom LSTM Neural Network [placeholder for url]
164
+ - **Architecture**:
165
+ - Embedding layer (100 dimensions)
166
+ - Bidirectional LSTM (128 hidden units)
167
+ - Dropout regularization (0.5)
168
+ - Sigmoid output layer
169
+ - **Performance**: **99% F1-Score**
170
+ - **Purpose**: Classifies news as fake or real
171
+ - **Features**:
172
+ - Text cleaning and preprocessing
173
+ - Contraction expansion
174
+ - Stopword removal
175
+ - Sequence padding/truncation (300 tokens)
176
+
177
+ #### 3. **Emotion Analysis** (`text_emotion_service.py`)
178
+ - **Model**: `j-hartmann/emotion-english-distilroberta-base`
179
+ - **Architecture**: DistilRoBERTa transformer model
180
+ - **Purpose**: Detects emotional tone (anger, fear, joy, etc.)
181
+ - **Features**:
182
+ - Chunked processing for long texts
183
+ - Confidence-weighted aggregation
184
+ - Sensationalism detection
185
+
186
+ #### 4. **Search Query Extraction** (`search_queries_service.py`)
187
+ - **Model**: `transformers` pipeline for question generation
188
+ - **Purpose**: Extracts key claims from text for fact-checking
189
+ - **Features**: NLP-based query generation
190
+
191
+ ### Image Analysis Services
192
+
193
+ #### 1. **AI Image Detection** (`ai_image_service.py`)
194
+ - **Model**: Custom EfficientNet-B3 [placeholder for url]
195
+ - **Architecture**:
196
+ - EfficientNet-B3 backbone
197
+ - Custom classification head
198
+ - Binary classification (AI vs Real)
199
+ - **Performance**: **99% F1-Score**
200
+ - **Purpose**: Identifies AI-generated vs real images
201
+ - **Features**:
202
+ - 300x300 input resolution
203
+ - Batch normalization
204
+ - GPU acceleration
205
+
206
+ #### 2. **Face Detection** (`face_detection_service.py`)
207
+ - **Model**: `scrfd` (pre-trained ONNX model)
208
+ - **Purpose**: Detects and locates faces in images
209
+ - **Features**:
210
+ - Multi-scale face detection
211
+ - Landmark extraction (eyes, nose, mouth)
212
+ - Confidence scoring
213
+ - NMS (Non-Maximum Suppression)
214
+
215
+ #### 3. **Deepfake Detection** (`deepfake_service.py`)
216
+ - **Model**: Meso4 architecture (two variants)
217
+ - `Meso4_DF.h5`: Trained on DeepFake dataset
218
+ - `Meso4_F2F.h5`: Trained on Face2Face dataset
219
+ - **Architecture**:
220
+ - 4 convolutional layers
221
+ - Batch normalization
222
+ - Max pooling
223
+ - Dropout (0.5)
224
+ - Sigmoid classification
225
+ - **Purpose**: Detects manipulated faces in images
226
+ - **Features**:
227
+ - Dual-model ensemble
228
+ - Face cropping integration
229
+ - Confidence threshold adjustment
230
+
231
+ ### Fact-Checking Integration
232
+
233
+ #### **Fact Check Service** (`fact_search_service.py`)
234
+ - **API**: Google Fact Check Tools API v1alpha1
235
+ - **Purpose**: External verification of claims
236
+ - **Features**:
237
+ - Multi-source fact-checking
238
+ - Verdict aggregation
239
+ - Confidence scoring
240
+ - Source credibility assessment
241
+
242
+ ## 📊 Model Performance
243
+
244
+ | Model | Type | F1-Score | Dataset | Purpose |
245
+ |-------|------|----------|---------|---------|
246
+ | **AI Text Detector** | Custom Classifier | **99%** | Custom | AI vs Human Text |
247
+ | **Fake News LSTM** | LSTM Neural Network | **99%** | Custom | Fake vs Real News |
248
+ | **AI Image Detector** | EfficientNet-B3 | **99%** | Custom | AI vs Real Images |
249
+ | **Emotion Detector** | DistilRoBERTa | N/A | Pre-trained | Emotion Analysis |
250
+ | **Face Detector** | SCRFD | N/A | Pre-trained | Face Detection |
251
+ | **Deepfake Detector** | Meso4 | N/A | Custom | Deepfake Detection |
252
+
253
+ ## 🎬 Demo Media Files
254
+
255
+ The `demo/` folder contains sample media:
256
+
257
+
258
+ ### Videos
259
+ - <video src="./demo/demo.mp4" controls width="720"></video>
260
+
261
+
262
+ ### Images
263
+ - `demo1.png` - ![Demo 1](./demo/demo1.png)
264
+ - `demo2.png` - ![Demo 2](./demo/demo2.png)
265
+ - `demo3.png` - ![Demo 3](./demo/demo3.png)
266
+ - `demo4.png` - ![Demo 4](./demo/demo4.png)
267
+ - `demo5.png` - ![Demo 5](./demo/demo5.png)
268
+ - `demo6.png` - ![Demo 6](./demo/demo6.png)
269
+ - `demo7.png` - ![Demo 7](./demo/demo7.png)
270
+
271
+ ## ⚙️ Configuration
272
+
273
+ ### Environment Variables
274
+
275
+ ```python
276
+ # config.py
277
+ class Config:
278
+ flask: FlaskConfig = FlaskConfig(
279
+ SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key'),
280
+ UPLOAD_FOLDER = 'static/uploads',
281
+ MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
282
+ )
283
+
284
+ service: ServiceConfig = ServiceConfig(
285
+ FACT_API_KEY = "your-google-fact-check-api-key",
286
+ FAKENESS_SCORE_THRESHOLD = 0.6,
287
+ FACE_DETECTION_THRESHOLD = 0.5
288
+ )
289
+ ```
290
+
291
+ ### Model Paths
292
+
293
+ ```python
294
+ # Model file locations
295
+ AI_TEXT_DETECTOR = "./models/ai_text_detector.joblib"
296
+ FAKE_NEWS_DETECTOR = "models/fake_news_detector.pt"
297
+ EFFICIENTNET_AI_IMAGE = "./models/efficientnet_b3_full_ai_image_classifier.pt"
298
+ FACE_DETECTION = "models/face_det_10g.onnx"
299
+ MESO4_DF = "models/Meso4_DF.h5"
300
+ MESO4_F2F = "models/Meso4_F2F.h5"
301
+ ```
302
+
303
+ ## 🔗 API Endpoints
304
+
305
+ | Method | Endpoint | Description |
306
+ |--------|----------|-------------|
307
+ | GET | `/` | Main analysis interface |
308
+ | POST | `/analyze` | Submit content for analysis |
309
+ | GET | `/analysis/<id>` | View analysis results |
310
+ | GET | `/health` | System health check |
311
+
312
+ ## 🤝 Contributing
313
+
314
+ 1. Fork the repository
315
+ 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
316
+ 3. Commit your changes (`git commit -m 'Add amazing feature'`)
317
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
318
+ 5. Open a Pull Request
319
+
320
+ ### Development Guidelines
321
+
322
+ - Follow PEP 8 style guidelines
323
+ - Add tests for new features
324
+ - Update documentation
325
+ - Ensure model compatibility
326
+
327
+ ## 📝 License
328
+
329
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
330
+
331
+ ## Acknowledgments
332
+
333
+ - **Hugging Face** for transformer models and pipelines
334
+ - **Google** for Fact Check Tools API
335
+ - **PyTorch Team** for deep learning framework
336
+ - **OpenCV** for computer vision utilities
337
+ - **SCRFD** for face detection models
338
+
339
+ ## Support
340
+
341
+ For support and questions:
342
+ - Create an issue in the repository
343
+ - Contact: [your-email@example.com]
344
+
345
+ ## Version History
346
+
347
+ ### v1.0.0
348
+ - Initial release with full multi-modal fake news detection
349
+ - 99% F1-scores on custom trained models
350
+ - Comprehensive web interface
351
+ - Integration with external fact-checking services
352
+
353
+ ---
354
+
355
+ **FactSight** - Bringing truth to the digital age through advanced AI-powered analysis. 🛡️
app.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify, redirect, url_for
2
+ from dataclasses import dataclass, asdict, is_dataclass
3
+ from typing import List, Optional, Tuple, Any
4
+ import os
5
+ import uuid
6
+ import base64
7
+ import io
8
+ from datetime import datetime
9
+ from PIL import Image
10
+
11
+ # Import configuration
12
+ from config import general_config
13
+
14
+ # -----------------------------
15
+ # Configuration and Setup
16
+ # -----------------------------
17
+
18
+ # Initialize Flask app with configuration
19
+ app = Flask(__name__)
20
+ app.config['UPLOAD_FOLDER'] = general_config.flask.UPLOAD_FOLDER
21
+ app.config['MAX_CONTENT_LENGTH'] = general_config.flask.MAX_CONTENT_LENGTH
22
+
23
+ # -----------------------------
24
+ # Data models (matching your schema)
25
+ # -----------------------------
26
+
27
+ from schemas.fact_search_schemas import FactCheckEntry, FactCheckResult
28
+ from schemas.text_schemas import EmotionResult
29
+ from schemas.vision_schemas import FaceMainPoints
30
+ from schemas.fake_manager_schemas import ImageAnalysis, AggregatedNewsAnalysis
31
+ from core.fake_manager import FakeNewsManager
32
+ from schemas.fake_manager_schemas import News
33
+
34
+ from services.ai_text_service import NBAITextDetector
35
+ from services.fake_text_news_service import FakeTextNewsDetector
36
+ from services.search_quries_service import TransformersSearchQueryExtractor
37
+ from services.text_emotion_service import TransformersEmotionDetector
38
+ from services.fact_search_service import FactCheckService
39
+ from services.ai_image_service import ENetAIImageDetector
40
+ from services.face_detection_service import SCRFDFaceDetector
41
+ from services.deepfake_service import Meso4FakeFaceDetector
42
+ from models.models import LSTMClassifier
43
+
44
+
45
+ # In-memory store: analysis_id -> JSON dict
46
+ STORE: dict[str, dict] = {}
47
+
48
+ # -----------------------------
49
+ # Helpers
50
+ # -----------------------------
51
+ def save_image(image_data: str, filename: str | None = None) -> Optional[str]:
52
+ try:
53
+ if image_data.startswith('data:image'):
54
+ header, b64 = image_data.split(',', 1)
55
+ mime = header.split(';')[0].split(':')[1] # e.g. image/png
56
+ ext = {'image/jpeg': 'jpg', 'image/png': 'png', 'image/webp': 'webp'}.get(mime, 'jpg')
57
+ else:
58
+ b64 = image_data
59
+ ext = 'jpg'
60
+
61
+ raw = base64.b64decode(b64)
62
+ img = Image.open(io.BytesIO(raw))
63
+ img.verify() # validate
64
+
65
+ img = Image.open(io.BytesIO(raw))
66
+ if img.mode not in ('RGB', 'L'):
67
+ img = img.convert('RGB')
68
+
69
+ if not filename:
70
+ filename = f"{uuid.uuid4().hex}.{ext}"
71
+ path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
72
+ img.save(path, quality=92)
73
+ return path
74
+ except Exception as e:
75
+ print(f"Error saving image: {e}")
76
+ import traceback
77
+ traceback.print_exc()
78
+ return None
79
+
80
+ def initialize_services():
81
+ """Initialize and configure all ML services."""
82
+ return FakeNewsManager(
83
+ ai_text_detector=NBAITextDetector(general_config.service.models.AI_TEXT_DETECTOR),
84
+ news_detector=FakeTextNewsDetector(
85
+ model_path=general_config.service.models.FAKE_NEWS_DETECTOR,
86
+ vocab_path=general_config.service.models.VOCAB_PATH,
87
+ ),
88
+ query_extractor=TransformersSearchQueryExtractor(),
89
+ emotion_detector=TransformersEmotionDetector(),
90
+ fact_checker=FactCheckService(api_key=general_config.service.FACT_API_KEY),
91
+ ai_image_detector=ENetAIImageDetector(general_config.service.models.EFFICIENTNET_AI_IMAGE),
92
+ face_detector=SCRFDFaceDetector(
93
+ model_path=general_config.service.models.FACE_DETECTION,
94
+ threshold_probability=general_config.service.FACE_DETECTION_THRESHOLD,
95
+ nms=general_config.service.FACE_DETECTION_NMS,
96
+ ),
97
+ fake_face_detector=Meso4FakeFaceDetector(
98
+ df_model_path=general_config.service.models.MESO4_DF,
99
+ f2f_model_path=general_config.service.models.MESO4_F2F,
100
+ ),
101
+ )
102
+
103
+
104
+ # Initialize services
105
+ fake_news_manager = initialize_services()
106
+
107
+ # -----------------------------
108
+ # Routes
109
+ # -----------------------------
110
+ @app.route('/')
111
+ def index():
112
+ return render_template('index.html')
113
+
114
+ @app.route('/analyze', methods=['POST'])
115
+ def analyze():
116
+ try:
117
+ data = request.get_json()
118
+ if not data or 'text' not in data or 'images' not in data:
119
+ return jsonify({'success': False, 'error': 'Invalid request data'}), 400
120
+
121
+ text = (data.get('text') or '').strip()
122
+ if not text:
123
+ return jsonify({'success': False, 'error': 'News text is required'}), 400
124
+
125
+ images_in = data.get('images') or []
126
+ if not images_in:
127
+ return jsonify({'success': False, 'error': 'At least one image is required'}), 400
128
+
129
+ saved_fs_paths_disk = []
130
+ saved_fs_paths_web = []
131
+ for img in images_in:
132
+ path = save_image(img.get('data', ''))
133
+ if path:
134
+ saved_fs_paths_disk.append(path)
135
+ saved_fs_paths_web.append('/' + path.replace('\\', '/'))
136
+
137
+ news = News(text=text, images=saved_fs_paths_disk)
138
+ analysis = fake_news_manager.analyze(news, fakeness_score_threshold=general_config.service.FAKENESS_SCORE_THRESHOLD)
139
+ analysis_json = analysis.to_json()
140
+
141
+ # Overwrite image paths in the JSON to web paths for frontend rendering
142
+ for i, img_entry in enumerate(analysis_json.get("images", [])):
143
+ if i < len(saved_fs_paths_web):
144
+ img_entry["image_path"] = saved_fs_paths_web[i]
145
+
146
+ STORE[analysis_json["analysis_id"]] = analysis_json
147
+
148
+ return jsonify({'success': True, 'analysis_id': analysis_json["analysis_id"]})
149
+ except Exception as e:
150
+ print("Analysis error:", e)
151
+ import traceback
152
+ traceback.print_exc()
153
+
154
+ return jsonify({'success': False, 'error': str(e)}), 500
155
+
156
+ @app.route('/analysis/<analysis_id>')
157
+ def analysis_page(analysis_id):
158
+ analysis = STORE.get(analysis_id)
159
+ if not analysis:
160
+ return redirect(url_for('index'))
161
+ return render_template('analysis.html', analysis=analysis)
162
+
163
+ @app.route('/health')
164
+ def health():
165
+ return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
166
+
167
+ if __name__ == '__main__':
168
+ print("Starting News Analyzer Server...")
169
+ print("Server running on http://localhost:5000")
170
+ app.run(debug=True, host='0.0.0.0', port=5000)
config.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ import os
3
+
4
+
5
+
6
+ @dataclass
7
+ class FlaskConfig:
8
+ """Flask application configuration."""
9
+ SECRET_KEY: str = field(default_factory=lambda: os.environ.get('SECRET_KEY', 'dev-secret-key'))
10
+ UPLOAD_FOLDER: str = 'static/uploads'
11
+ MAX_CONTENT_LENGTH: int = 50 * 1024 * 1024 # 50MB
12
+
13
+
14
+ @dataclass
15
+ class ModelPaths:
16
+ """Model file paths configuration."""
17
+ # Text detection models
18
+ AI_TEXT_DETECTOR: str = "./models/ai_text_detector.joblib"
19
+ FAKE_NEWS_DETECTOR: str = "models/fake_news_detector.pt"
20
+ VOCAB_PATH: str = "models/word2idx.pt"
21
+
22
+ # Image detection models
23
+ EFFICIENTNET_AI_IMAGE: str = "./models/efficientnet_b3_full_ai_image_classifier.pt"
24
+ FACE_DETECTION: str = "models/face_det_10g.onnx"
25
+ MESO4_DF: str = "models/Meso4_DF.h5"
26
+ MESO4_F2F: str = "models/Meso4_F2F.h5"
27
+
28
+
29
+ @dataclass
30
+ class ServiceConfig:
31
+ """Service-specific configuration parameters."""
32
+ # Face detection thresholds
33
+ FACE_DETECTION_THRESHOLD: float = 0.5
34
+ FACE_DETECTION_NMS: float = 0.5
35
+
36
+ # Analysis thresholds
37
+ FAKENESS_SCORE_THRESHOLD: float = 0.6
38
+
39
+ # API Keys
40
+ FACT_API_KEY: str = "your-google-fact-check"
41
+
42
+ # Model paths
43
+ models: ModelPaths = field(default_factory=ModelPaths)
44
+
45
+
46
+ @dataclass
47
+ class Config:
48
+ """Main application configuration."""
49
+ flask: FlaskConfig = field(default_factory=FlaskConfig)
50
+ service: ServiceConfig = field(default_factory=ServiceConfig)
51
+
52
+ def __post_init__(self):
53
+ """Ensure upload folder exists after initialization."""
54
+ if hasattr(self.flask, 'UPLOAD_FOLDER'):
55
+ os.makedirs(self.flask.UPLOAD_FOLDER, exist_ok=True)
56
+
57
+
58
+ # Global configuration instance
59
+ general_config = Config()
core/fake_manager.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from schemas.text_schemas import AITextDetector
2
+ from schemas.vision_schemas import FakeFaceDetector
3
+ from schemas.vision_schemas import FaceDetector
4
+ from schemas.text_schemas import FakeNewsDetector
5
+ from schemas.vision_schemas import AIImageDetector
6
+ from schemas.text_schemas import EmotionDetector
7
+ from schemas.text_schemas import SearchQueryExtractor
8
+ from schemas.fake_manager_schemas import News, AggregatedNewsAnalysis, ImageAnalysis
9
+ from schemas.vision_schemas import FaceMainPoints
10
+ from services.fact_search_service import FactCheckService
11
+ from utils.utils import open_image
12
+ from typing import List, Optional, Union
13
+ from PIL import Image as PILImage
14
+ from pathlib import Path
15
+ import uuid
16
+ from datetime import datetime
17
+
18
+
19
+ class FakeNewsManager:
20
+ """Manager that aggregates multiple detectors and services to analyze a single
21
+ news item (text + images) and produce an aggregated analysis.
22
+
23
+ Responsibilities:
24
+ - Run text-based detectors (AI text detector, fake-news/text classifier).
25
+ - Extract search queries and run fact-checking.
26
+ - Run emotion analysis on the text.
27
+ - Run image-level detectors (face detection, AI-image detection, deepfake
28
+ face detection) and crop faces for per-face analysis.
29
+
30
+ Attributes:
31
+ ai_text_detector: Optional AI text detector; must provide `.detect(text) -> bool|float|None`.
32
+ fake_face_detector: Optional face-level deepfake detector; must provide `.detect(pil_image) -> bool|float|None`.
33
+ face_detector: Optional face detector; must provide `.detect(pil_image) -> list[FaceMainPoints]`.
34
+ news_detector: Optional fake-news/text detector; must provide `.detect(text) -> bool|float|None`.
35
+ ai_image_detector: Optional AI-image detector; must provide `.detect(pil_image) -> bool|float|None`.
36
+ query_extractor: Optional extractor that returns list[str] from text.
37
+ emotion_detector: Optional emotion detector; must provide `.analyze(text)`.
38
+ fact_checker: Optional fact-check service; must provide `.verify_claim(query)`.
39
+ """
40
+
41
+ ai_text_detector: Optional[AITextDetector]
42
+ fake_face_detector: Optional[FakeFaceDetector]
43
+ face_detector: Optional[FaceDetector]
44
+ news_detector: Optional[FakeNewsDetector]
45
+ ai_image_detector: Optional[AIImageDetector]
46
+ query_extractor: Optional[SearchQueryExtractor]
47
+ emotion_detector: Optional[EmotionDetector]
48
+ fact_checker: Optional[FactCheckService]
49
+
50
+ def __init__(
51
+ self,
52
+ *,
53
+ ai_text_detector: Optional[AITextDetector] = None,
54
+ fake_face_detector: Optional[FakeFaceDetector] = None,
55
+ face_detector: Optional[FaceDetector] = None,
56
+ news_detector: Optional[FakeNewsDetector] = None,
57
+ ai_image_detector: Optional[AIImageDetector] = None,
58
+ query_extractor: Optional[SearchQueryExtractor] = None,
59
+ emotion_detector: Optional[EmotionDetector] = None,
60
+ fact_checker: Optional[FactCheckService] = None,
61
+ ) -> None:
62
+ """Create a FakeNewsManager.
63
+
64
+ All parameters are optional; missing detectors/services are simply skipped
65
+ during analysis. Types are intentionally permissive to accommodate a
66
+ variety of detector implementations used in this project.
67
+ """
68
+ self.ai_text_detector = ai_text_detector
69
+ self.fake_face_detector = fake_face_detector
70
+ self.face_detector = face_detector
71
+ self.news_detector = news_detector
72
+ self.ai_image_detector = ai_image_detector
73
+ self.query_extractor = query_extractor
74
+ self.emotion_detector = emotion_detector
75
+ self.fact_checker = fact_checker
76
+
77
+ def test(self) -> None:
78
+ """Lightweight method used for quick smoke tests.
79
+
80
+ Intended for interactive debugging only; it prints a short marker.
81
+ """
82
+ print("test")
83
+
84
+ def _crop_face(self, img: PILImage, face_mp: FaceMainPoints) -> PILImage:
85
+ """Crop a face region from a PIL image using coordinates from
86
+ a `FaceMainPoints` object.
87
+
88
+ Args:
89
+ img: PIL.Image instance to crop from.
90
+ face_mp: FaceMainPoints providing `box_start_point` and
91
+ `box_end_point` coordinates as (x, y) tuples.
92
+
93
+ Returns:
94
+ A new PIL.Image containing only the cropped face region.
95
+ """
96
+ x1, y1 = face_mp.box_start_point
97
+ x2, y2 = face_mp.box_end_point
98
+ return img.crop((x1, y1, x2, y2))
99
+
100
+ def analyze(
101
+ self,
102
+ news: News,
103
+ fakeness_score_threshold: float = 0.6,
104
+ ) -> AggregatedNewsAnalysis:
105
+ """Analyze a `News` item and return an `AggregatedNewsAnalysis`.
106
+
107
+ The method coordinates text and image analyzers, runs optional
108
+ fact-checking on extracted queries, and constructs an
109
+ `AggregatedNewsAnalysis` object that summarizes all results.
110
+
111
+ Args:
112
+ news: `News` object containing `text` (str) and `images` (list of
113
+ paths or file-like objects) to analyze.
114
+ fakeness_score_threshold: Float threshold in [0, 1] used by the
115
+ aggregated analysis to decide the final `is_fake_final_decision`.
116
+
117
+ Returns:
118
+ AggregatedNewsAnalysis populated with detector outputs and a
119
+ computed final decision.
120
+ """
121
+ # Text detectors
122
+ is_ai_text = self.ai_text_detector.detect(news.text) if self.ai_text_detector else None
123
+ is_fake_text = self.news_detector.detect(news.text) if self.news_detector else None
124
+
125
+ # Query extraction & emotion
126
+ queries: List[str] = self.query_extractor.extract(news.text) if self.query_extractor else []
127
+ emotion = self.emotion_detector.analyze(news.text) if self.emotion_detector else None
128
+
129
+ # Run fact-checking for each extracted query; if no queries, fall back to full text
130
+ fact_check: Optional[List[object]] = None
131
+ if self.fact_checker:
132
+ fact_check = []
133
+ targets = queries if queries else [news.text]
134
+ for q in targets:
135
+ res = self.fact_checker.verify_claim(q)
136
+ if res is not None:
137
+ fact_check.append(res)
138
+
139
+ # Image-level analysis
140
+ images_analysis: List[ImageAnalysis] = []
141
+ for img_in in news.images:
142
+ img = open_image(img_in)
143
+
144
+ faces = self.face_detector.detect(img) if self.face_detector else []
145
+ is_ai_image = self.ai_image_detector.detect(img) if self.ai_image_detector else False
146
+
147
+ deepfake_faces: List[bool] = []
148
+ if self.fake_face_detector and faces:
149
+ for f in faces:
150
+ face_img = self._crop_face(img, f)
151
+ deepfake_faces.append(bool(self.fake_face_detector.detect(face_img)))
152
+
153
+ # Ensure image_path is a string as required by schema
154
+ if isinstance(img_in, (str, Path)):
155
+ image_path = str(img_in)
156
+ else:
157
+ image_path = ""
158
+
159
+ images_analysis.append(
160
+ ImageAnalysis(
161
+ image_path=image_path,
162
+ is_ai_image=is_ai_image,
163
+ faces=faces,
164
+ deepfake_faces=deepfake_faces,
165
+ )
166
+ )
167
+
168
+ aggregated_news_analysis = AggregatedNewsAnalysis(
169
+ is_fake_final_decision=None,
170
+ analysis_timestamp=datetime.now().isoformat(),
171
+ analysis_id=str(uuid.uuid4()),
172
+ text=news.text,
173
+ is_ai_text=is_ai_text,
174
+ is_fake_text=is_fake_text,
175
+ queries=queries,
176
+ emotion=emotion,
177
+ fact_check=fact_check,
178
+ images=images_analysis,
179
+ )
180
+
181
+ # Compute final decision using the AggregatedNewsAnalysis helper
182
+ aggregated_news_analysis.is_fake_final_decision = (
183
+ aggregated_news_analysis.compute_final_decision(fakeness_score_threshold)
184
+ )
185
+
186
+ return aggregated_news_analysis
187
+
188
+
189
+
190
+
demo/demo.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:37c5c42334c060547544c1066c4b1eeaadaf56b77335ef574db8eda4c61085c8
3
+ size 27348551
demo/demo1.png ADDED
demo/demo2.png ADDED

Git LFS Details

  • SHA256: f234e14aaab6c3952689f86a912a734fcce36879e96afe5e6a3e75fd983f6d23
  • Pointer size: 131 Bytes
  • Size of remote file: 156 kB
demo/demo3.png ADDED
demo/demo4.png ADDED

Git LFS Details

  • SHA256: 84b5fcdd090c4eb5bcd729137c9f6399fe24054af84a6649235fda897dd66155
  • Pointer size: 131 Bytes
  • Size of remote file: 881 kB
demo/demo5.png ADDED

Git LFS Details

  • SHA256: b74f03cca041c2dcfd5cbc59ab8293c1d2ceb40f66e9dbbe9639879bd2f11c73
  • Pointer size: 131 Bytes
  • Size of remote file: 118 kB
demo/demo6.png ADDED
demo/demo7.png ADDED
models/Meso4_DF.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:036ed30d04ffd9c7506f825b97c86aa64915f5818931da96817b169aad790af5
3
+ size 156128
models/Meso4_F2F.h5 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bcfda015d217b9f8d6557693edbdf9f4e79cfd22672541968dd0e2954462b5d7
3
+ size 156128
models/ai_text_detector.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:946bba1b68f35d1e95e5245105fc4d1a991f8293df5c3407f7cfa723d359d400
3
+ size 436909018
models/efficientnet_b3_full_ai_image_classifier.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fb6c6ede00a59848ccf176fd1207d421ae8ba7236daa7fe1ac07bfee3e549640
3
+ size 43455736
models/face_det_10g.onnx ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5838f7fe053675b1c7a08b633df49e7af5495cee0493c7dcf6697200b85b5b91
3
+ size 16923827
models/fake_news_detector.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6b6dd44b8663bfd062324914918e66c77c419335891c9921e7797f0ef88a920f
3
+ size 4947968
models/models.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch.nn as nn
2
+
3
+
4
+
5
+ class LSTMClassifier(nn.Module):
6
+ def __init__(self, vocab_size, embedding_dim=100, hidden_dim=128,
7
+ num_layers=1, dropout=0.5, bidirectional=True):
8
+ super().__init__()
9
+ self.embedding = nn.Embedding(vocab_size, embedding_dim, padding_idx=0)
10
+ self.lstm = nn.LSTM(
11
+ embedding_dim, hidden_dim, num_layers,
12
+ batch_first=True,
13
+ bidirectional=bidirectional,
14
+ dropout=dropout if num_layers > 1 else 0
15
+ )
16
+ self.dropout = nn.Dropout(dropout)
17
+ self.fc = nn.Linear(hidden_dim * (2 if bidirectional else 1), 1)
18
+ self.sigmoid = nn.Sigmoid()
19
+
20
+ def forward(self, x):
21
+ x = self.embedding(x)
22
+ out, _ = self.lstm(x)
23
+ last = out[:, -1, :]
24
+ out = self.dropout(last)
25
+ out = self.fc(out)
26
+ return self.sigmoid(out).squeeze()
27
+
models/word2idx.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7bde7101ae6d75cbb8213dac33de55ed1de4e86bfc825e1ef9b549ef10b6e356
3
+ size 201444
notebooks/ai-generated-vs-human-image-99-f1-score.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
notebooks/ai-vs-human-text-classifier-99-f1-score.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
notebooks/fake-news-lstm-classifier-f1-score-99.ipynb ADDED
The diff for this file is too large to render. See raw diff
 
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy==2.3.3
2
+ scikit-learn==1.7.1
3
+ joblib==1.5.2
4
+ onnxruntime==1.22.1
5
+ timm==1.0.15
6
+ torch==2.8.0
7
+ torchvision==0.23.0
8
+ Pillow==11.3.0
9
+ scrfd==0.3.0
10
+ nltk==3.9.1
11
+ contractions==0.1.73
12
+ transformers==4.57.0
13
+ flask==3.1.1
schemas/fact_search_schemas.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Optional
3
+
4
+
5
+
6
+ @dataclass
7
+ class FactCheckEntry:
8
+ """Represents a single fact-check result from a verified source."""
9
+ text: str
10
+ claimant: Optional[str]
11
+ claim_date: Optional[str]
12
+ rating: Optional[str]
13
+ publisher: Optional[str]
14
+ url: Optional[str]
15
+
16
+
17
+ @dataclass
18
+ class FactCheckResult:
19
+ """Structured result returned by the Fact Check Tools API."""
20
+ verified: bool
21
+ summary_verdict: str
22
+ results: List[FactCheckEntry] = field(default_factory=list)
23
+
24
+ def is_fake(self) -> bool:
25
+ """Return True if the aggregated verdict indicates likely false information."""
26
+ return self.summary_verdict.lower() in ["false", "likely false", "incorrect"]
27
+
28
+ def summary(self) -> str:
29
+ """Return a human-readable summary for quick inspection."""
30
+ publishers = {r.publisher for r in self.results if r.publisher}
31
+ return (
32
+ f"✅ Verified: {self.verified}\n"
33
+ f"🧾 Summary Verdict: {self.summary_verdict}\n"
34
+ f"📚 Sources: {', '.join(publishers) if publishers else 'N/A'}"
35
+ )
36
+
schemas/fake_manager_schemas.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass, asdict, is_dataclass
2
+ from typing import List, Optional, Any, Tuple, Union
3
+ from pathlib import Path
4
+ import numpy as np
5
+ from PIL.Image import Image
6
+ from schemas.vision_schemas import FaceMainPoints
7
+ from schemas.text_schemas import EmotionResult
8
+ from schemas.fact_search_schemas import FactCheckResult
9
+
10
+
11
+
12
+ @dataclass
13
+ class News:
14
+ text: str
15
+ images: List[Union[str, Path, Image, np.ndarray]]
16
+
17
+
18
+
19
+ @dataclass
20
+ class IsFakeNewsResult:
21
+
22
+ is_fake_text: bool
23
+ is_ai_text: bool
24
+ is_deepfake_faces: list[bool]
25
+
26
+
27
+
28
+
29
+ @dataclass
30
+ class ImageAnalysis:
31
+ image_path: str
32
+ is_ai_image: bool
33
+ faces: List[FaceMainPoints]
34
+ deepfake_faces: List[bool]
35
+
36
+
37
+
38
+ @dataclass
39
+ class AggregatedNewsAnalysis:
40
+ is_fake_final_decision: bool
41
+ text: str
42
+ is_ai_text: Optional[bool]
43
+ is_fake_text: Optional[bool]
44
+ queries: List[str]
45
+ emotion: Optional["EmotionResult"]
46
+ fact_check: Optional[List["FactCheckResult"]]
47
+ images: List["ImageAnalysis"]
48
+ analysis_timestamp: str
49
+ analysis_id: str
50
+
51
+ # ---------- existing ----------
52
+ def to_json(self) -> dict:
53
+ def _convert(obj: Any) -> Any:
54
+ if is_dataclass(obj):
55
+ return {k: _convert(v) for k, v in asdict(obj).items()}
56
+ if isinstance(obj, tuple):
57
+ return list(obj)
58
+ if isinstance(obj, list):
59
+ return [_convert(x) for x in obj]
60
+ if isinstance(obj, dict):
61
+ return {k: _convert(v) for k, v in obj.items()}
62
+ return obj
63
+ return _convert(self)
64
+
65
+ # ---------- NEW: scoring helpers ----------
66
+ @staticmethod
67
+ def _score_fact_verdict(verdict: str) -> float:
68
+ """Map summary_verdict to [0,1] fake-likelihood."""
69
+ if not verdict:
70
+ return 0.0
71
+ v = verdict.strip().lower()
72
+ table = {
73
+ "false": 1.0, "pants on fire": 1.0, "fake": 1.0,
74
+ "likely false": 0.9, "incorrect": 0.9, "misleading": 0.85, "deceptive": 0.85,
75
+ "unsubstantiated": 0.6, "missing context": 0.5,
76
+ "mixed": 0.5, "partly false": 0.7, "partly true": 0.3,
77
+ "mostly true": 0.1, "true": 0.0, "correct": 0.0
78
+ }
79
+ # fuzzy match by containment if exact key not present
80
+ for k, s in table.items():
81
+ if k in v:
82
+ return s
83
+ return 0.0
84
+
85
+ @staticmethod
86
+ def _score_fact_checks(fact_checks: Optional[List["FactCheckResult"]]) -> float:
87
+ if not fact_checks:
88
+ return 0.0
89
+ # take the strongest (max) verdict score across checks
90
+ return max((AggregatedNewsAnalysis._score_fact_verdict(fc.summary_verdict) for fc in fact_checks if fc and fc.summary_verdict), default=0.0)
91
+
92
+ @staticmethod
93
+ def _score_text(is_fake_text: Optional[bool], is_ai_text: Optional[bool]) -> float:
94
+ score = 0.0
95
+ if is_fake_text is True:
96
+ score += 0.7
97
+ elif is_fake_text is False:
98
+ score += 0.0 # explicit non-fake does not add risk
99
+
100
+ # AI text alone is NOT proof of fake; small nudge only
101
+ if is_ai_text is True:
102
+ score += 0.1
103
+ elif is_ai_text is False:
104
+ score += 0.0
105
+ return min(score, 1.0)
106
+
107
+ @staticmethod
108
+ def _score_images(images: List["ImageAnalysis"]) -> float:
109
+ if not images:
110
+ return 0.0
111
+ # per-image risk
112
+ per_image = []
113
+ for img in images:
114
+ s = 0.0
115
+ if getattr(img, "is_ai_image", False):
116
+ s = max(s, 0.5)
117
+ # deepfake face => strong signal
118
+ if any(bool(x) for x in getattr(img, "deepfake_faces", []) or []):
119
+ s = max(s, 0.8)
120
+ per_image.append(s)
121
+
122
+ if not per_image:
123
+ return 0.0
124
+
125
+ # Combine by "noisy OR": 1 - Π(1 - s_i)
126
+ from math import prod
127
+ return 1.0 - prod(1.0 - s for s in per_image)
128
+
129
+ @staticmethod
130
+ def _score_emotion(emotion: Optional["EmotionResult"]) -> float:
131
+ # Small bump only if highly sensational and confident
132
+ if not emotion:
133
+ return 0.0
134
+ dom = (emotion.dominant_emotion or "").strip().lower()
135
+ conf = float(getattr(emotion, "confidence", 0.0) or 0.0)
136
+ sensational = {"anger", "fear", "surprise", "disgust"}
137
+ if dom in sensational and conf >= 0.75:
138
+ return 0.1
139
+ return 0.0
140
+
141
+ def _active_weights(self, w_fact=0.5, w_text=0.3, w_img=0.15, w_emote=0.05) -> Tuple[float, float, float, float]:
142
+ """Re-normalize weights if any component is missing."""
143
+ have_fact = bool(self.fact_check)
144
+ have_text = (self.is_fake_text is not None) or (self.is_ai_text is not None)
145
+ have_img = bool(self.images)
146
+ have_emo = bool(self.emotion)
147
+
148
+ parts = [
149
+ (w_fact if have_fact else 0.0),
150
+ (w_text if have_text else 0.0),
151
+ (w_img if have_img else 0.0),
152
+ (w_emote if have_emo else 0.0),
153
+ ]
154
+ total = sum(parts) or 1.0
155
+ return tuple(p / total for p in parts) # normalized
156
+
157
+ def compute_final_score(self) -> tuple[float, dict]:
158
+ """Return (score, breakdown) in [0,1]."""
159
+ wf, wt, wi, we = self._active_weights()
160
+
161
+ s_fact = self._score_fact_checks(self.fact_check)
162
+ s_text = self._score_text(self.is_fake_text, self.is_ai_text)
163
+ s_img = self._score_images(self.images)
164
+ s_emo = self._score_emotion(self.emotion)
165
+
166
+ score = wf * s_fact + wt * s_text + wi * s_img + we * s_emo
167
+ breakdown = {
168
+ "weights": {"fact": wf, "text": wt, "images": wi, "emotion": we},
169
+ "components": {"fact": s_fact, "text": s_text, "images": s_img, "emotion": s_emo},
170
+ "total": score
171
+ }
172
+ return score, breakdown
173
+
174
+ def compute_final_decision(self, threshold: float = 0.60) -> bool:
175
+ """Set and return final decision based on weighted score."""
176
+ score, _ = self.compute_final_score()
177
+ self.is_fake_final_decision = bool(score >= threshold)
178
+ return self.is_fake_final_decision
schemas/text_schemas.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from typing import Dict, List
4
+
5
+
6
+ class AITextDetector(ABC):
7
+ """
8
+ Abstract base class for all AI text detectors.
9
+ Defines the interface that all concrete detectors must implement.
10
+ """
11
+
12
+ @abstractmethod
13
+ def detect(self, text: str) -> bool:
14
+ """
15
+ Detect whether the given text is AI-generated.
16
+
17
+ Args:
18
+ text (str): The input text to analyze.
19
+
20
+ Returns:
21
+ bool: True if the text is AI-generated, False if it is human-written.
22
+ """
23
+ pass
24
+
25
+
26
+ class EmotionDetector(ABC):
27
+ """Abstract base class for emotion detection."""
28
+
29
+ @abstractmethod
30
+ def analyze(self, text: str):
31
+ """Analyze the emotion in the given text and return a structured result."""
32
+ pass
33
+
34
+
35
+ @dataclass
36
+ class EmotionResult:
37
+ """Structured output for detected emotions."""
38
+ dominant_emotion: str
39
+ confidence: float
40
+ all_scores: Dict[str, float]
41
+
42
+
43
+ class SearchQueryExtractor(ABC):
44
+ """Abstract base class for extracting search queries from text."""
45
+
46
+ @abstractmethod
47
+ def extract(self, text: str, num_queries: int = 5) -> List[str]:
48
+ """
49
+ Extract search-like queries from a given paragraph.
50
+
51
+ Args:
52
+ text: The input text to extract queries from.
53
+ num_queries: Number of queries to generate.
54
+
55
+ Returns:
56
+ List[str]: A list of extracted search queries.
57
+ """
58
+ pass
59
+
60
+
61
+
62
+
63
+ from abc import ABC, abstractmethod
64
+
65
+
66
+ # ===== Abstract base class =====
67
+ class FakeNewsDetector(ABC):
68
+ @abstractmethod
69
+ def detect(self, text: str) -> int:
70
+ """Return 1 for real news, 0 for fake news."""
71
+ pass
schemas/vision_schemas.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from typing import List, Tuple
4
+ from PIL import Image
5
+ from scrfd import Face
6
+ import numpy as np
7
+ import os
8
+ from pathlib import Path
9
+
10
+
11
+ class AIImageDetector(ABC):
12
+ """
13
+ Abstract base class for AI image detectors.
14
+ """
15
+
16
+ @abstractmethod
17
+ def detect(self, image: Image.Image) -> bool:
18
+ """
19
+ Detect whether the given image is AI-generated.
20
+
21
+ Args:
22
+ image (PIL.Image.Image): The input image.
23
+
24
+ Returns:
25
+ bool: True if AI-generated, False if real.
26
+ """
27
+ pass
28
+
29
+
30
+ @dataclass
31
+ class FaceMainPoints:
32
+ """
33
+ The main points of the face.
34
+
35
+ Attributes:
36
+ box_start_point (Tuple[int, int]): The start point of the bounding box.
37
+ box_end_point (Tuple[int, int]): The end point of the bounding box.
38
+ box_probabilty_score (float): The probability score of the bounding box.
39
+ left_eye (Tuple[int, int], optional): The left eye coordinates. Defaults to (0, 0).
40
+ right_eye (Tuple[int, int], optional): The right eye coordinates. Defaults to (0, 0).
41
+ nose (Tuple[int, int], optional): The nose coordinates. Defaults to (0, 0).
42
+ left_mouth (Tuple[int, int], optional): The left mouth coordinates. Defaults to (0, 0).
43
+ right_mouth (Tuple[int, int], optional): The right mouth coordinates. Defaults to (0, 0).
44
+ """
45
+ box_start_point: Tuple[int, int]
46
+ box_end_point: Tuple[int, int]
47
+ box_probabilty_score: float
48
+ left_eye: Tuple[int, int] = (0, 0)
49
+ right_eye: Tuple[int, int] = (0, 0)
50
+ nose: Tuple[int, int] = (0, 0)
51
+ left_mouth: Tuple[int, int] = (0, 0)
52
+ right_mouth: Tuple[int, int] = (0, 0)
53
+
54
+
55
+ class FaceDetector(ABC):
56
+ """
57
+ The face detector interface.
58
+ """
59
+
60
+ @abstractmethod
61
+ def detect(self, image_path: str) -> List[FaceMainPoints]:
62
+ """
63
+ Detect the faces in an image.
64
+
65
+ Args:
66
+ image_path (str): The path to the image.
67
+
68
+ Returns:
69
+ A list of FaceMainPoints objects, one for each face in the image.
70
+ """
71
+ pass
72
+
73
+ @abstractmethod
74
+ def convert_face_to_face_main_points(self, face: Face) -> FaceMainPoints:
75
+ """
76
+ Convert a Face object to a FaceMainPoints object.
77
+
78
+ Args:
79
+ face (Face): The face to convert.
80
+
81
+ Returns:
82
+ A FaceMainPoints object.
83
+ """
84
+ pass
85
+
86
+
87
+ class FakeFaceDetector(ABC):
88
+ @abstractmethod
89
+ def detect(self, image: Image.Image, threshold: float = 0.5) -> bool:
90
+ """
91
+ Base detector method.
92
+ Subclasses must implement this.
93
+ """
94
+ raise NotImplementedError("Subclasses should implement this method.")
services/ai_image_service.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import timm
3
+ from torchvision import transforms
4
+ from PIL import Image
5
+ from typing import Any
6
+ from schemas.vision_schemas import AIImageDetector
7
+
8
+
9
+ class ENetAIImageDetector(AIImageDetector):
10
+ """
11
+ EfficientNet-B3 AI Image Detector that classifies whether an image
12
+ is AI-generated or real using a pre-trained PyTorch model.
13
+
14
+ Attributes:
15
+ model_path (str): Path to the trained model file (.pt).
16
+ model (Any): Loaded PyTorch model.
17
+ device (str): Device to run inference on ('cuda' or 'cpu').
18
+ """
19
+
20
+ def __init__(self, model_path: str = "./models/efficientnet_b3_full_ai_image_classifier.pt"):
21
+ """
22
+ Initialize the ENetAIImageDetector.
23
+
24
+ Args:
25
+ model_path (str, optional): Path to the trained EfficientNet model.
26
+ """
27
+ self.model_path = model_path
28
+ self.device = "cuda" if torch.cuda.is_available() else "cpu"
29
+ self.model = self._load_model()
30
+ self.transform = self._build_transform()
31
+
32
+ def _load_model(self) -> Any:
33
+ """Load the trained EfficientNet-B3 model."""
34
+ if self.model_path.endswith(".pt"):
35
+ model = torch.load(self.model_path, map_location=self.device, weights_only=False)
36
+ else:
37
+ model = timm.create_model("efficientnet_b3", pretrained=False, num_classes=1)
38
+ model.load_state_dict(torch.load(self.model_path, map_location=self.device))
39
+ model.to(self.device)
40
+ model.eval()
41
+ return model
42
+
43
+ def _build_transform(self) -> Any:
44
+ """Return preprocessing pipeline for input images."""
45
+ return transforms.Compose([
46
+ transforms.Resize((300, 300)),
47
+ transforms.ToTensor(),
48
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
49
+ std=[0.229, 0.224, 0.225]),
50
+ ])
51
+
52
+ def _preprocess_image(self, image: Image.Image) -> torch.Tensor:
53
+ """Convert a PIL Image to a normalized tensor."""
54
+ return self.transform(image).unsqueeze(0).to(self.device)
55
+
56
+ def detect(self, image: Image.Image) -> bool:
57
+ """
58
+ Detect whether a given PIL image is AI-generated.
59
+
60
+ Args:
61
+ image (PIL.Image.Image): The input image.
62
+
63
+ Returns:
64
+ bool: True if AI-generated, False if real.
65
+ """
66
+ if not isinstance(image, Image.Image):
67
+ raise TypeError("Input must be a PIL.Image.Image object.")
68
+
69
+ img_tensor = self._preprocess_image(image)
70
+
71
+ with torch.no_grad():
72
+ outputs = self.model(img_tensor)
73
+ prob = torch.sigmoid(outputs).item()
74
+
75
+ is_ai_generated = prob >= 0.001
76
+ return is_ai_generated
services/ai_text_service.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import joblib
2
+ import re
3
+ import string
4
+ from typing import Any
5
+ from nltk.corpus import stopwords
6
+
7
+
8
+ from schemas.text_schemas import AITextDetector
9
+
10
+
11
+ class NBAITextDetector(AITextDetector):
12
+ """
13
+ Naive Bayes AI Text Detector that classifies whether a text
14
+ is AI-generated or human-written using a pre-trained joblib model.
15
+
16
+ Attributes:
17
+ model_path (str): Path to the saved joblib model.
18
+ model (Any): Loaded ML model for prediction.
19
+ min_words (int): Minimum number of words required for valid detection.
20
+ """
21
+
22
+ def __init__(self, model_path: str = "./models/ai_text_detector.joblib", min_words: int = 0):
23
+ """
24
+ Initialize the NBAITextDetector.
25
+
26
+ Args:
27
+ model_path (str, optional): Path to the trained joblib model file.
28
+ min_words (int, optional): Minimum number of words for detection.
29
+ """
30
+ self.model_path = model_path
31
+ self.min_words = min_words
32
+ self.model = self._load_model()
33
+ self.stop_words = set(stopwords.words('english'))
34
+
35
+ def _load_model(self) -> Any:
36
+ """Load the trained joblib model."""
37
+ return joblib.load(self.model_path)
38
+
39
+ def _preprocess_text(self, text: str) -> str:
40
+ """Clean and preprocess the input text."""
41
+ text = text.lower()
42
+ text = re.sub(r'\s+', ' ', text.strip())
43
+ text = text.translate(str.maketrans('', '', string.punctuation))
44
+ text = ' '.join(word for word in text.split() if word not in self.stop_words)
45
+ text = re.sub(r'http\S+|www\.\S+', '', text)
46
+ text = re.sub(r'\S+@\S+\.\S+', '', text)
47
+ text = re.sub(r'#[A-Za-z0-9_]+', '', text)
48
+ text = re.sub(r'@[A-Za-z0-9_]+', '', text)
49
+ text = re.sub(r'\d+', '', text)
50
+ text = ''.join(ch for ch in text if ch.isprintable())
51
+ return text
52
+
53
+ def detect(self, text: str) -> bool:
54
+ """
55
+ Detect whether a given text is AI-generated.
56
+
57
+ Args:
58
+ text (str): Input text to classify.
59
+
60
+ Returns:
61
+ bool: True if AI-generated, False if human-written.
62
+ """
63
+ if len(text.split()) < self.min_words:
64
+ raise ValueError(f"Text must be at least {self.min_words} words long.")
65
+
66
+ processed = self._preprocess_text(text)
67
+ prediction = self.model.predict([processed])
68
+ return bool(int(prediction[0]) == 1)
services/deepfake_service.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from PIL import Image
2
+
3
+ from schemas.vision_schemas import FakeFaceDetector
4
+
5
+ import cv2
6
+ import numpy as np
7
+ from tensorflow.keras.models import Sequential
8
+ from tensorflow.keras.layers import Conv2D, BatchNormalization, MaxPooling2D, Flatten, Dropout, Dense
9
+ from tensorflow.keras.preprocessing.image import img_to_array
10
+ from PIL import Image
11
+
12
+
13
+ class Meso4FakeFaceDetector(FakeFaceDetector):
14
+ """
15
+ Deepfake face detector using Meso4 models trained on DF and F2F datasets.
16
+ Combines both models' predictions for a more robust detection.
17
+ """
18
+
19
+ def __init__(self,
20
+ df_model_path: str = "models/Meso4_DF.h5",
21
+ f2f_model_path: str = "models/Meso4_F2F.h5",
22
+ input_shape=(256, 256, 3)):
23
+ """
24
+ Initialize and load both pretrained models.
25
+
26
+ Args:
27
+ df_model_path (str): Path to Meso4 model trained on DeepFake dataset.
28
+ f2f_model_path (str): Path to Meso4 model trained on Face2Face dataset.
29
+ input_shape (tuple): Expected input shape for the models.
30
+ """
31
+ self.df_model_path = df_model_path
32
+ self.f2f_model_path = f2f_model_path
33
+ self.input_shape = input_shape
34
+
35
+ # Build both models
36
+ self.model_df = self._build_meso4()
37
+ self.model_f2f = self._build_meso4()
38
+
39
+ # Load pretrained weights
40
+ self.model_df.load_weights(self.df_model_path)
41
+ self.model_f2f.load_weights(self.f2f_model_path)
42
+
43
+ # ------------------ Internal Model Builder ------------------
44
+
45
+ def _build_meso4(self):
46
+ """Build the Meso4 CNN model architecture."""
47
+ model = Sequential()
48
+
49
+ model.add(Conv2D(8, (3, 3), activation='relu', padding='same', input_shape=self.input_shape))
50
+ model.add(BatchNormalization())
51
+ model.add(MaxPooling2D(pool_size=(2, 2), strides=2))
52
+
53
+ model.add(Conv2D(8, (5, 5), activation='relu', padding='same'))
54
+ model.add(BatchNormalization())
55
+ model.add(MaxPooling2D(pool_size=(2, 2), strides=2))
56
+
57
+ model.add(Conv2D(16, (5, 5), activation='relu', padding='same'))
58
+ model.add(BatchNormalization())
59
+ model.add(MaxPooling2D(pool_size=(2, 2), strides=2))
60
+
61
+ model.add(Conv2D(16, (5, 5), activation='relu', padding='same'))
62
+ model.add(BatchNormalization())
63
+ model.add(MaxPooling2D(pool_size=(4, 4), strides=4))
64
+
65
+ model.add(Flatten())
66
+ model.add(Dropout(0.5))
67
+ model.add(Dense(16, activation='relu'))
68
+ model.add(Dense(1, activation='sigmoid'))
69
+
70
+ model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
71
+ return model
72
+
73
+ # ------------------ Image Preprocessing ------------------
74
+
75
+ def _preprocess_image(self, image: Image.Image) -> np.ndarray:
76
+ """
77
+ Preprocess a PIL image for prediction.
78
+
79
+ Args:
80
+ image (PIL.Image): Input face image.
81
+
82
+ Returns:
83
+ np.ndarray: Preprocessed image ready for model input.
84
+ """
85
+ img = image.convert("RGB")
86
+ img = img.resize((256, 256))
87
+ img = img_to_array(img) / 255.0
88
+ img = np.expand_dims(img, axis=0)
89
+ return img
90
+
91
+ # ------------------ Detection ------------------
92
+
93
+ def detect(self, image: Image.Image, threshold: float = 0.5, verbose=True) -> bool:
94
+ """
95
+ Detect whether an input face image is fake or real.
96
+
97
+ Args:
98
+ image (PIL.Image): Input image of a face.
99
+ threshold (float): Decision threshold (default 0.5).
100
+
101
+ Returns:
102
+ bool: True if fake, False if real.
103
+ """
104
+ img = self._preprocess_image(image)
105
+
106
+ # Get individual predictions
107
+ pred_df = float(self.model_df.predict(img, verbose=0)[0][0])
108
+ pred_f2f = float(self.model_f2f.predict(img, verbose=0)[0][0])
109
+
110
+ # Combine predictions (average)
111
+ combined_pred = (pred_df + pred_f2f) / 2.0
112
+
113
+ if verbose:
114
+ # Print detailed info
115
+ print(f"🔍 Meso4_DF Prediction: {pred_df:.4f}")
116
+ print(f"🔍 Meso4_F2F Prediction: {pred_f2f:.4f}")
117
+ print(f"⚖️ Combined Score: {combined_pred:.4f}")
118
+
119
+ # Determine if fake
120
+ is_fake = combined_pred >= threshold
121
+
122
+ if is_fake:
123
+ print("🔴 Deepfake detected.")
124
+ else:
125
+ print("🟢 Real face detected.")
126
+
127
+ return is_fake
services/face_detection_service.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from scrfd import SCRFD, Threshold, Face
2
+ from PIL.Image import Image
3
+ import numpy as np
4
+ from typing import List, Union
5
+ from pathlib import Path
6
+ import os
7
+
8
+ from schemas.vision_schemas import FaceMainPoints, FaceDetector
9
+ from utils.utils import open_image
10
+
11
+
12
+ class SCRFDFaceDetector(FaceDetector):
13
+ """
14
+ A face detector using the SCRFD model.
15
+ """
16
+
17
+ def __init__(self, model_path: str, threshold_probability: float, nms:float):
18
+ """
19
+ Initializes the face detector.
20
+
21
+ Args:
22
+ model_path: The path to the SCRFD model.
23
+ threshold_probability: The probability threshold for face detection.
24
+ nms: The NMS threshold for face detection.
25
+ """
26
+ self.model = SCRFD.from_path(model_path)
27
+ self.threshold = Threshold(probability=threshold_probability, nms=nms)
28
+
29
+
30
+ def convert_face_to_face_main_points(self, face: Face) -> FaceMainPoints:
31
+ """
32
+ Converts a Face object to a FaceMainPoints object.
33
+
34
+ Args:
35
+ face: The face to convert.
36
+
37
+ Returns:
38
+ A FaceMainPoints object.
39
+ """
40
+ return FaceMainPoints(
41
+ box_start_point=(face.bbox.upper_left.x, face.bbox.upper_left.y,),
42
+ box_end_point=(face.bbox.lower_right.x, face.bbox.lower_right.y),
43
+ box_probabilty_score=face.probability,
44
+ left_eye=(face.keypoints.left_eye.x, face.keypoints.left_eye.y),
45
+ right_eye=(face.keypoints.right_eye.x, face.keypoints.right_eye.y),
46
+ nose=(face.keypoints.nose.x, face.keypoints.nose.y),
47
+ left_mouth=(face.keypoints.left_mouth.x, face.keypoints.left_mouth.y),
48
+ right_mouth=(face.keypoints.right_mouth.x, face.keypoints.right_mouth.y)
49
+ )
50
+
51
+
52
+ def detect(self, image: Union[str, Path, np.ndarray, Image]) -> List[FaceMainPoints]:
53
+ """
54
+ Detect faces in an image and return their main points.
55
+
56
+ Args:
57
+ image: Input image that can be:
58
+ - str: Path to image file
59
+ - Path: Path object to image file
60
+ - np.ndarray: Image array (BGR or RGB)
61
+ - PILImage.Image: PIL Image object
62
+
63
+ Returns:
64
+ List[FaceMainPoints]: List of face detections, each containing:
65
+ - Bounding box coordinates
66
+ - Key facial landmarks (eyes, nose, mouth)
67
+ - Confidence score
68
+
69
+ Note:
70
+ - Uses a threshold set during initialization for detection confidence
71
+ - Returns an empty list if no faces are detected
72
+ - Image is automatically converted to RGB format
73
+ """
74
+ image = open_image(image).convert("RGB")
75
+
76
+ extracted_faces = self.model.detect(image, threshold=self.threshold)
77
+
78
+ faces = [self.convert_face_to_face_main_points(extracted_face) for extracted_face in extracted_faces]
79
+
80
+ return faces
81
+
82
+
83
+ def extract_face_from_image(self, img, face_main_points:FaceMainPoints):
84
+ """
85
+ Extract a face from an image using FaceMainPoints coordinates.
86
+
87
+ Args:
88
+ img: Input PIL Image
89
+ face_main_points: FaceMainPoints object containing face coordinates
90
+
91
+ Returns:
92
+ PIL.Image: Cropped face image
93
+ """
94
+ start_x, start_y = face_main_points.box_start_point
95
+ end_x, end_y = face_main_points.box_end_point
96
+
97
+ return img.crop((start_x, start_y, end_x, end_y))
98
+
99
+ def cut_extracted_faces(self, image: Union[str, Path, np.ndarray, Image],
100
+ save_path: str = None) -> List[Image]:
101
+ """
102
+ Detect and extract all faces from an image, optionally saving them.
103
+
104
+ Args:
105
+ image: Input image that can be:
106
+ - str: Path to image file
107
+ - Path: Path object to image file
108
+ - np.ndarray: Image array (BGR or RGB)
109
+ - PILImage.Image: PIL Image object
110
+ save_path: Optional path to save extracted faces
111
+ If provided, faces will be saved as 'face_{index}.jpg' in this directory
112
+
113
+ Returns:
114
+ List[PIL.Image]: List of cropped face images
115
+ """
116
+ # Convert input to PIL Image
117
+ img = open_image(image)
118
+
119
+ # Detect faces and get their main points
120
+ faces_information = self.detect(img)
121
+
122
+ if not faces_information:
123
+ return [] # Return empty list if no faces detected
124
+
125
+ # Extract and optionally save each face
126
+ extracted_faces = []
127
+ for i, face_info in enumerate(faces_information):
128
+ face_image = self.extract_face_from_image(img, face_info)
129
+ extracted_faces.append(face_image)
130
+
131
+ # Save face if save_path is provided
132
+ if save_path:
133
+ os.makedirs(save_path, exist_ok=True)
134
+ face_path = os.path.join(save_path, f'face_{i}.jpg')
135
+ face_image.save(face_path)
136
+
137
+ return extracted_faces
services/fact_search_service.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+
3
+ from schemas.fact_search_schemas import FactCheckEntry, FactCheckResult
4
+
5
+ class FactCheckService:
6
+ """
7
+ A service wrapper around the Google Fact Check Tools API (v1alpha1).
8
+
9
+ Provides an interface to verify claims and obtain structured verdicts from
10
+ fact-checking organizations (BBC, PolitiFact, FactCheck.org, etc.).
11
+
12
+ Example:
13
+ >>> service = FactCheckService(api_key="YOUR_API_KEY")
14
+ >>> result = service.verify_claim("COVID-19 vaccines cause infertility")
15
+ >>> print(result.summary())
16
+ """
17
+
18
+ BASE_URL = "https://factchecktools.googleapis.com/v1alpha1/claims:search"
19
+
20
+ def __init__(self, api_key: str, timeout: int = 10):
21
+ """
22
+ Initialize the FactCheckService.
23
+
24
+ Args:
25
+ api_key (str): Google Fact Check Tools API key.
26
+ timeout (int): Optional. Request timeout in seconds.
27
+ """
28
+ self.api_key = api_key
29
+ self.timeout = timeout
30
+
31
+ def verify_claim(self, claim_text: str) -> FactCheckResult:
32
+ """
33
+ Verify a text claim using Google's Fact Check Tools API.
34
+
35
+ Args:
36
+ claim_text (str): The claim or post text to fact-check.
37
+
38
+ Returns:
39
+ FactCheckResult: Structured response with verification details.
40
+ """
41
+ params = {"query": claim_text, "key": self.api_key}
42
+
43
+ try:
44
+ response = requests.get(self.BASE_URL, params=params, timeout=self.timeout)
45
+ response.raise_for_status()
46
+ data = response.json()
47
+ except requests.RequestException as e:
48
+ return FactCheckResult(verified=False, summary_verdict=f"Request failed: {e}")
49
+
50
+ if "claims" not in data or not data["claims"]:
51
+ return FactCheckResult(verified=False, summary_verdict="Unverified")
52
+
53
+ entries = []
54
+ for claim in data["claims"]:
55
+ reviews = claim.get("claimReview", [])
56
+ for review in reviews:
57
+ entries.append(
58
+ FactCheckEntry(
59
+ text=claim.get("text", ""),
60
+ claimant=claim.get("claimant"),
61
+ claim_date=claim.get("claimDate"),
62
+ rating=review.get("textualRating"),
63
+ publisher=review.get("publisher", {}).get("name"),
64
+ url=review.get("url"),
65
+ )
66
+ )
67
+
68
+ # Aggregate verdict
69
+ verdict_texts = [e.rating.lower() for e in entries if e.rating]
70
+ if any(v in verdict_texts for v in ["false", "incorrect", "pants on fire", "mostly false"]):
71
+ summary_verdict = "Likely False"
72
+ elif any(v in verdict_texts for v in ["true", "mostly true", "accurate"]):
73
+ summary_verdict = "Likely True"
74
+ else:
75
+ summary_verdict = "Mixed / Unclear"
76
+
77
+ return FactCheckResult(verified=True, summary_verdict=summary_verdict, results=entries)
services/fake_text_news_service.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ import re
3
+ import torch
4
+ import contractions
5
+ import nltk
6
+ from nltk.corpus import stopwords
7
+ import torch.nn as nn
8
+
9
+ from schemas.text_schemas import FakeNewsDetector
10
+ from models.models import LSTMClassifier
11
+
12
+ # Download stopwords (first run only)
13
+ nltk.download('stopwords', quiet=True)
14
+ stop_words = set(stopwords.words('english'))
15
+
16
+
17
+ class FakeTextNewsDetector(FakeNewsDetector):
18
+ """
19
+ A class for detecting fake news in text articles using a pre-trained LSTM model.
20
+
21
+ This class loads a trained PyTorch LSTM classifier and corresponding vocabulary
22
+ to evaluate whether a given text is real or fake. It performs preprocessing,
23
+ tokenization, and encoding before feeding the text to the neural network.
24
+
25
+ Attributes:
26
+ model_path (str): Path to the saved PyTorch model file (.pt).
27
+ vocab_path (str): Path to the saved vocabulary mapping file (.pt).
28
+ device (torch.device): The computing device used for inference ('cuda' or 'cpu').
29
+ model (nn.Module): The loaded LSTM model for fake news detection.
30
+ word2idx (dict): Mapping of words to integer indices for token encoding.
31
+ """
32
+
33
+ def __init__(self, model_path: str = "models/model.pt", vocab_path: str = "models/word2idx.pt", device=None):
34
+ """
35
+ Initializes the FakeTextNewsDetector.
36
+
37
+ Loads the trained model and vocabulary, sets up the computing device, and
38
+ prepares the model for inference.
39
+
40
+ Args:
41
+ model_path (str): Path to the pre-trained LSTM model (.pt). Defaults to "models/model.pt".
42
+ vocab_path (str): Path to the vocabulary mapping file (.pt). Defaults to "models/word2idx.pt".
43
+ device (torch.device, optional): Torch device ('cuda' or 'cpu').
44
+ If not provided, automatically detects CUDA availability.
45
+ """
46
+ self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
47
+ torch.serialization.add_safe_globals([LSTMClassifier])
48
+
49
+ # Load vocabulary dictionary
50
+ self.word2idx = torch.load(vocab_path, map_location=self.device)
51
+ vocab_size = len(self.word2idx)
52
+
53
+ # Load the LSTM model
54
+ self.model = LSTMClassifier(vocab_size=vocab_size)
55
+ loaded_model = torch.load(model_path, map_location=self.device, weights_only=False)
56
+
57
+ # Support both raw state_dict and full model serialization
58
+ if isinstance(loaded_model, dict):
59
+ self.model.load_state_dict(loaded_model)
60
+ else:
61
+ self.model.load_state_dict(loaded_model.state_dict())
62
+
63
+ self.model.to(self.device)
64
+ self.model.eval()
65
+
66
+ def _clean_text(self, text: str):
67
+ """
68
+ Cleans and tokenizes the input text for model inference.
69
+
70
+ Steps include:
71
+ - Expanding contractions (e.g., "don't" → "do not")
72
+ - Removing non-ASCII characters, HTML tags, URLs, and special symbols
73
+ - Lowercasing all text
74
+ - Removing single-letter words and stopwords
75
+
76
+ Args:
77
+ text (str): The input raw text.
78
+
79
+ Returns:
80
+ list[str]: A list of cleaned and filtered tokens.
81
+ """
82
+ text = contractions.fix(text)
83
+ text = text.encode("ascii", "ignore").decode()
84
+ text = text.lower()
85
+ text = re.sub(r"http\S+|www\.\S+", "", text)
86
+ text = re.sub(r"<.*?>", "", text)
87
+ text = re.sub(r"[^a-z\s]", "", text)
88
+ text = re.sub(r"\b\w{1}\b", "", text)
89
+ tokens = [w for w in text.split() if w not in stop_words]
90
+ return tokens
91
+
92
+ def _encode(self, tokens, max_len: int = 300):
93
+ """
94
+ Encodes tokens into a fixed-length tensor for model input.
95
+
96
+ Each token is mapped to its index in the loaded vocabulary.
97
+ If the token is unknown, a special <UNK> index is used.
98
+ The sequence is padded or truncated to the specified length.
99
+
100
+ Args:
101
+ tokens (list[str]): List of word tokens.
102
+ max_len (int): Maximum sequence length. Defaults to 300.
103
+
104
+ Returns:
105
+ torch.Tensor: Encoded token tensor of shape (1, max_len).
106
+ """
107
+ ids = [self.word2idx.get(w, self.word2idx.get("<UNK>", 1)) for w in tokens]
108
+ if len(ids) < max_len:
109
+ ids += [self.word2idx.get("<PAD>", 0)] * (max_len - len(ids))
110
+ else:
111
+ ids = ids[:max_len]
112
+ return torch.tensor(ids, dtype=torch.long).unsqueeze(0)
113
+
114
+ def detect(self, text: str) -> bool:
115
+ """
116
+ Predicts whether the given text is fake or real.
117
+
118
+ The method preprocesses the text, encodes it into numerical form,
119
+ and runs inference using the LSTM model. The output score is a probability
120
+ between 0 and 1, where values above 0.5 indicate fake news.
121
+
122
+ Args:
123
+ text (str): The input text (article or sentence).
124
+
125
+ Returns:
126
+ bool: True if the text is predicted as fake, False if real.
127
+ """
128
+ tokens = self._clean_text(text)
129
+ seq = self._encode(tokens).to(self.device)
130
+ with torch.no_grad():
131
+ score = self.model(seq).item()
132
+ return score >= 0.5
services/search_quries_service.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import List
3
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
4
+ from schemas.text_schemas import SearchQueryExtractor
5
+
6
+
7
+ class TransformersSearchQueryExtractor(SearchQueryExtractor):
8
+
9
+ """Transformer-based implementation of the SearchQueryExtractor interface."""
10
+
11
+ def __init__(self, model_name: str = "google/flan-t5-small"):
12
+ """
13
+ Initialize the lightweight transformer model for search query generation.
14
+
15
+ Args:
16
+ model_name: Hugging Face model name (default: 'google/flan-t5-small').
17
+ """
18
+ self.model_name = model_name
19
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
20
+ self.model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
21
+
22
+ def extract(self, text: str, num_queries: int = 5) -> List[str]:
23
+ """
24
+ Generate search-like queries using the transformer model.
25
+
26
+ Args:
27
+ text: The input paragraph.
28
+ num_queries: Number of queries to generate.
29
+
30
+ Returns:
31
+ List[str]: A list of extracted search queries.
32
+ """
33
+ prompt = (
34
+ f"Generate {num_queries} useful and distinct search queries "
35
+ f"from the following paragraph:\n{text.strip()}"
36
+ )
37
+
38
+ inputs = self.tokenizer(prompt, return_tensors="pt", truncation=True)
39
+ outputs = self.model.generate(**inputs, max_length=96, num_return_sequences=1)
40
+ generated_text = self.tokenizer.decode(outputs[0], skip_special_tokens=True)
41
+
42
+ # Clean and split queries
43
+ queries = [
44
+ q.strip("-• \n").rstrip(".")
45
+ for q in generated_text.split("\n")
46
+ if q.strip()
47
+ ]
48
+ return queries
services/text_emotion_service.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import pipeline, AutoTokenizer
2
+ from schemas.text_schemas import EmotionResult, EmotionDetector
3
+ from typing import List, Dict
4
+
5
+ class TransformersEmotionDetector(EmotionDetector):
6
+ """Emotion detector using a lightweight DistilRoBERTa model from Hugging Face."""
7
+
8
+ def __init__(self, model_name: str = "j-hartmann/emotion-english-distilroberta-base"):
9
+ """
10
+ Initialize the emotion detection model.
11
+ Args:
12
+ model_name: Pretrained Hugging Face model for emotion detection.
13
+ """
14
+ self.model_name = model_name
15
+ self.pipeline = pipeline(
16
+ "text-classification",
17
+ model=model_name,
18
+ return_all_scores=True
19
+ )
20
+ # Load tokenizer for proper text truncation
21
+ self.tokenizer = AutoTokenizer.from_pretrained(model_name)
22
+
23
+ def analyze(self, text: str) -> EmotionResult:
24
+ """
25
+ Analyze emotion in the given text using chunked processing for long texts.
26
+ Args:
27
+ text: The text to analyze.
28
+ Returns:
29
+ EmotionResult: Structured result containing dominant emotion and confidence.
30
+ """
31
+ # Split text into chunks if it's too long
32
+ max_tokens_per_chunk = 400 # Conservative limit to leave room for special tokens
33
+ chunks = self._split_text_into_chunks(text, max_tokens_per_chunk)
34
+
35
+ if len(chunks) == 1:
36
+ # Single chunk - use original logic
37
+ results: List[List[Dict[str, float]]] = self.pipeline(chunks[0])
38
+ else:
39
+ # Multiple chunks - analyze each and aggregate results
40
+ results: List[List[Dict[str, float]]] = self.pipeline(chunks)
41
+
42
+ # Aggregate emotion scores across all chunks
43
+ aggregated_scores = self._aggregate_emotion_scores(results)
44
+
45
+ # Get the most likely emotion
46
+ dominant_emotion = max(aggregated_scores, key=aggregated_scores.get)
47
+ confidence = aggregated_scores[dominant_emotion]
48
+
49
+ return EmotionResult(
50
+ dominant_emotion=dominant_emotion,
51
+ confidence=confidence,
52
+ all_scores=aggregated_scores
53
+ )
54
+
55
+ def _split_text_into_chunks(self, text: str, max_tokens_per_chunk: int) -> List[str]:
56
+ """
57
+ Split text into chunks that fit within token limits.
58
+ Args:
59
+ text: The text to split
60
+ max_tokens_per_chunk: Maximum tokens per chunk
61
+ Returns:
62
+ List of text chunks
63
+ """
64
+ # Split text into sentences first for better chunk boundaries
65
+ sentences = text.split('. ')
66
+ chunks = []
67
+ current_chunk = ""
68
+ current_tokens = 0
69
+
70
+ for sentence in sentences:
71
+ sentence = sentence.strip()
72
+ if not sentence:
73
+ continue
74
+
75
+ # Add period back if it was removed
76
+ if not sentence.endswith('.'):
77
+ sentence += '.'
78
+
79
+ sentence_tokens = len(self.tokenizer.tokenize(sentence))
80
+
81
+ # If adding this sentence would exceed limit, start new chunk
82
+ if current_tokens + sentence_tokens > max_tokens_per_chunk and current_chunk:
83
+ chunks.append(current_chunk.strip())
84
+ current_chunk = sentence
85
+ current_tokens = sentence_tokens
86
+ else:
87
+ if current_chunk:
88
+ current_chunk += " " + sentence
89
+ else:
90
+ current_chunk = sentence
91
+ current_tokens += sentence_tokens
92
+
93
+ # Add the last chunk if it exists
94
+ if current_chunk:
95
+ chunks.append(current_chunk.strip())
96
+
97
+ return chunks
98
+
99
+ def _aggregate_emotion_scores(self, results: List[List[Dict[str, float]]]) -> Dict[str, float]:
100
+ """
101
+ Aggregate emotion scores from multiple chunks.
102
+ Args:
103
+ results: List of emotion classification results from each chunk
104
+ Returns:
105
+ Dictionary of aggregated emotion scores
106
+ """
107
+ if not results:
108
+ return {}
109
+
110
+ # Collect all emotion scores with weights
111
+ emotion_totals = {}
112
+ emotion_weights = {}
113
+
114
+ for chunk_results in results:
115
+ # Get confidence scores for this chunk
116
+ chunk_scores = {entry["label"]: entry["score"] for entry in chunk_results}
117
+
118
+ # Weight by confidence (more confident predictions get higher weight)
119
+ total_confidence = sum(chunk_scores.values())
120
+
121
+ for emotion, score in chunk_scores.items():
122
+ weight = score / total_confidence if total_confidence > 0 else 0
123
+ emotion_totals[emotion] = emotion_totals.get(emotion, 0) + score
124
+ emotion_weights[emotion] = emotion_weights.get(emotion, 0) + 1 # Simple count for now
125
+
126
+ # Average the scores across chunks
127
+ aggregated_scores = {}
128
+ for emotion in emotion_totals:
129
+ # Use weighted average based on number of chunks that detected this emotion
130
+ aggregated_scores[emotion] = emotion_totals[emotion] / emotion_weights[emotion]
131
+
132
+ return aggregated_scores
static/css/style.css ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Dark Theme Core */
2
+ :root {
3
+ --bg-primary: #0a0a0a;
4
+ --bg-secondary: #111111;
5
+ --bg-surface: #161616;
6
+ --bg-card: #1d1d1d;
7
+ --bg-input: #232323;
8
+
9
+ --text-primary: #ffffff;
10
+ --text-secondary: #a0a0a0;
11
+ --text-muted: #6e6e6e;
12
+
13
+ --accent-primary: #3b82f6;
14
+ --accent-primary-hover: #2563eb;
15
+ --accent-success: #10b981;
16
+ --accent-warning: #f59e0b;
17
+ --accent-error: #ef4444;
18
+ --accent-info: #3b82f6;
19
+
20
+ --border-color: #2a2a2a;
21
+ --border-light: #3a3a3a;
22
+
23
+ --shadow-xl: 0 10px 16px rgba(0,0,0,0.35);
24
+
25
+ /* sharper borders */
26
+ --radius-xs: 4px;
27
+ --radius-sm: 6px;
28
+ --radius-md: 8px;
29
+ }
30
+
31
+ * { margin:0; padding:0; box-sizing:border-box; }
32
+
33
+ body {
34
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
35
+ background: var(--bg-primary);
36
+ color: var(--text-primary);
37
+ line-height: 1.6;
38
+ }
39
+
40
+ .container { max-width: 1100px; margin: 0 auto; padding: 0 1.5rem; }
41
+
42
+ /* Header */
43
+ .header {
44
+ background: var(--bg-secondary);
45
+ border-bottom: 1px solid var(--border-color);
46
+ padding: 1rem 0;
47
+ position: sticky; top: 0; z-index: 10;
48
+ }
49
+ .header-content { display: flex; justify-content: space-between; align-items: center; }
50
+ .logo { font-size: 1.25rem; font-weight: 700; color: var(--text-primary); }
51
+
52
+ /* Form Section (intro) */
53
+ .analysis-form-section { padding: 3rem 0; background: var(--bg-secondary); }
54
+ .form-container {
55
+ background: var(--bg-card);
56
+ border: 1px solid var(--border-color);
57
+ border-radius: var(--radius-sm);
58
+ padding: 2rem;
59
+ box-shadow: var(--shadow-xl);
60
+ }
61
+ .form-header { text-align: center; margin-bottom: 2rem; }
62
+ .form-header h2 { font-size: 1.75rem; margin-bottom: 0.5rem; }
63
+ .form-header p { color: var(--text-secondary); }
64
+
65
+ .form-group { margin-bottom: 1.5rem; }
66
+ .form-label {
67
+ display: flex; justify-content: space-between; align-items: center;
68
+ font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);
69
+ }
70
+ .char-counter { color: var(--text-muted); font-size: 0.9rem; font-weight: 400; }
71
+
72
+ .form-textarea {
73
+ width: 100%; min-height: 200px; background: var(--bg-input);
74
+ border: 1px solid var(--border-light); border-radius: var(--radius-xs);
75
+ padding: 1rem; color: var(--text-primary); font-size: 1rem; resize: vertical;
76
+ }
77
+ .form-textarea:focus { outline: none; border-color: var(--accent-primary); }
78
+
79
+ .upload-container { display: flex; flex-direction: column; gap: 1rem; }
80
+ .upload-zone {
81
+ border: 2px dashed var(--border-light); border-radius: var(--radius-sm);
82
+ padding: 2rem 1rem; background: var(--bg-input);
83
+ transition: border-color .2s, background .2s; cursor: pointer;
84
+ }
85
+ .upload-zone:hover, .upload-zone.drag-over {
86
+ border-color: var(--accent-primary); background: var(--bg-surface);
87
+ }
88
+ .upload-content { display: flex; flex-direction: column; gap: 1rem; align-items: center; text-align: center; }
89
+ .upload-text h4 { font-size: 1.1rem; margin-bottom: 0.25rem; }
90
+ .upload-text p { color: var(--text-secondary); margin-bottom: 0.25rem; }
91
+ .upload-info { font-size: 0.85rem; color: var(--text-muted); }
92
+ .upload-btn {
93
+ background: var(--accent-primary); color: #fff; border: none;
94
+ padding: 0.6rem 1.1rem; border-radius: var(--radius-xs); font-weight: 600; cursor: pointer;
95
+ }
96
+ .upload-btn:hover { background: var(--accent-primary-hover); }
97
+ .file-input { display: none; }
98
+
99
+ .image-previews {
100
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.75rem;
101
+ }
102
+ .preview-item {
103
+ position: relative; border-radius: var(--radius-xs); overflow: hidden;
104
+ background: var(--bg-input); border: 1px solid var(--border-light);
105
+ }
106
+ .preview-image { width: 100%; height: 100px; object-fit: cover; }
107
+ .remove-btn {
108
+ position: absolute; top: 0.5rem; right: 0.5rem;
109
+ background: rgba(239, 68, 68, 0.95); color: #fff; border: none;
110
+ width: 22px; height: 22px; border-radius: 2px; font-size: 0.8rem; cursor: pointer;
111
+ opacity: 0; transition: opacity .15s;
112
+ }
113
+ .preview-item:hover .remove-btn { opacity: 1; }
114
+
115
+ /* Actions */
116
+ .form-actions { display: flex; gap: 0.75rem; justify-content: center; margin-top: 1rem; }
117
+ .btn-primary, .btn-secondary {
118
+ padding: 0.85rem 1.3rem; border-radius: var(--radius-xs); font-weight: 700; cursor: pointer; border: none; min-width: 140px;
119
+ }
120
+ .btn-primary { background: var(--accent-primary); color: #fff; position: relative; }
121
+ .btn-primary:hover:not(:disabled) { background: var(--accent-primary-hover); }
122
+ .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; }
123
+ .btn-secondary { background: transparent; color: var(--text-secondary); border: 1px solid var(--border-light); }
124
+ .btn-secondary:hover { color: var(--text-primary); }
125
+
126
+ .btn-loader { display: none; }
127
+ .btn-loader.active { display: inline-block; }
128
+ .spinner {
129
+ width: 16px; height: 16px; border: 2px solid transparent; border-top: 2px solid currentColor;
130
+ border-radius: 50%; animation: spin 1s linear infinite;
131
+ }
132
+ @keyframes spin { 0%{transform:rotate(0)} 100%{transform:rotate(360deg)} }
133
+
134
+ /* Toasts */
135
+ .message-container { position: fixed; top: 1rem; right: 1rem; z-index: 1000; }
136
+ .message {
137
+ background: var(--bg-surface); border: 1px solid var(--border-color);
138
+ border-left: 3px solid var(--accent-info);
139
+ border-radius: var(--radius-xs); padding: 0.7rem 1rem; margin-bottom: 0.5rem;
140
+ display: flex; gap: 0.75rem; transform: translateX(400px); transition: transform .25s ease-out;
141
+ }
142
+ .message.show { transform: translateX(0); }
143
+ .message.success { border-left-color: var(--accent-success); }
144
+ .message.error { border-left-color: var(--accent-error); }
145
+ .message.warning { border-left-color: var(--accent-warning); }
146
+ .message-title { font-weight: 700; font-size: 0.95rem; }
147
+ .message-text { color: var(--text-secondary); font-size: 0.85rem; }
148
+ .message-close { margin-left: auto; background: none; color: var(--text-muted); border: none; cursor: pointer; font-size: 1rem; }
149
+
150
+ /* Analysis page */
151
+ .analysis-header {
152
+ background: var(--bg-secondary);
153
+ border-bottom: 1px solid var(--border-color);
154
+ padding: 1rem 0;
155
+ }
156
+ .analysis-header .header-content {
157
+ display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 0.75rem;
158
+ }
159
+ .back-button {
160
+ color: var(--accent-primary); text-decoration: none; font-weight: 700;
161
+ padding: 0.35rem 0.7rem; border: 1px solid var(--border-light); border-radius: var(--radius-xs);
162
+ }
163
+ .back-button:hover { background: var(--bg-surface); }
164
+ .analysis-meta { display: flex; gap: 0.75rem; color: var(--text-secondary); font-size: 0.9rem; }
165
+
166
+ .overview-cards {
167
+ display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
168
+ gap: 1rem; margin: 1.5rem 0 2rem;
169
+ }
170
+ .overview-card {
171
+ background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-sm);
172
+ padding: 1rem; transition: transform .15s; display: flex; align-items: center;
173
+ }
174
+ .overview-card:hover { transform: translateY(-1px); }
175
+ .card-success { border-left: 3px solid var(--accent-success); }
176
+ .card-warning { border-left: 3px solid var(--accent-warning); }
177
+ .card-danger { border-left: 3px solid var(--accent-error); }
178
+ .card-content h3 { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 0.35rem; }
179
+ .card-value { font-size: 1.35rem; font-weight: 800; margin-bottom: 0.15rem; }
180
+ .card-description { font-size: 0.8rem; color: var(--text-muted); }
181
+
182
+ .analysis-dashboard { padding: 1rem 0 2.5rem; }
183
+ .dashboard-grid {
184
+ display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 2rem;
185
+ }
186
+ .dashboard-column { display: flex; flex-direction: column; gap: 1.25rem; }
187
+
188
+ .analysis-section {
189
+ background: var(--bg-card); border: 1px solid var(--border-color); border-radius: var(--radius-sm); overflow: hidden;
190
+ }
191
+ .section-header {
192
+ background: var(--bg-surface); padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-color);
193
+ display: flex; justify-content: space-between; align-items: center;
194
+ }
195
+ .section-header h3 { font-size: 1.05rem; font-weight: 800; }
196
+ .section-badge {
197
+ background: var(--bg-input); color: var(--text-secondary);
198
+ padding: 0.2rem 0.6rem; border-radius: var(--radius-xs); font-size: 0.8rem; font-weight: 700;
199
+ }
200
+
201
+ .content-card { padding: 1rem 1.25rem; }
202
+ .content-text { color: var(--text-primary); margin-bottom: 0.75rem; }
203
+ .content-meta { display: flex; gap: 1rem; color: var(--text-muted); font-size: 0.9rem; }
204
+
205
+ .emotion-card { padding: 1rem 1.25rem; }
206
+ .dominant-emotion { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
207
+ .emotion-tag { background: var(--accent-primary); color: #fff; padding: 0.3rem 0.6rem; border-radius: var(--radius-xs); font-weight: 800; }
208
+ .emotion-breakdown { display: flex; flex-direction: column; gap: 0.6rem; }
209
+ .emotion-item { display: flex; align-items: center; gap: 0.75rem; }
210
+ .emotion-label { min-width: 80px; color: var(--text-secondary); font-size: 0.9rem; }
211
+ .emotion-bar { flex: 1; height: 8px; background: var(--bg-input); border-radius: var(--radius-xs); overflow: hidden; }
212
+ .emotion-fill { height: 100%; background: linear-gradient(90deg, var(--accent-primary), var(--accent-info)); }
213
+ .emotion-score { min-width: 40px; text-align: right; color: var(--text-muted); font-size: 0.9rem; }
214
+
215
+ .queries-card { padding: 1rem 1.25rem; }
216
+ .queries-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; }
217
+ .query-chip {
218
+ background: var(--bg-input); color: var(--text-primary);
219
+ padding: 0.4rem 0.7rem; border-radius: var(--radius-xs); font-size: 0.85rem; border: 1px solid var(--border-light);
220
+ }
221
+
222
+ /* Fact checks */
223
+ .fact-checks { padding: 1rem 1.25rem; display: flex; flex-direction: column; gap: 1rem; }
224
+ .fact-check-card { border-radius: var(--radius-xs); overflow: hidden; }
225
+ .fact-unverified { background: rgba(239, 68, 68, 0.05); border: 1px solid rgba(239, 68, 68, 0.35); }
226
+ .fact-verified { background: rgba(16, 185, 129, 0.05); border: 1px solid rgba(16, 185, 129, 0.35); }
227
+ .fact-check-header {
228
+ display: flex; justify-content: space-between; align-items: center;
229
+ padding: 0.8rem 1rem; background: var(--bg-surface);
230
+ }
231
+ .fact-verdict { font-weight: 800; }
232
+ .fact-status { padding: 0.3rem 0.6rem; border-radius: var(--radius-xs); font-weight: 800; font-size: 0.8rem; }
233
+ .status-verified { background: var(--accent-success); color: #fff; }
234
+ .status-unverified { background: var(--accent-error); color: #fff; }
235
+ .fact-check-results { padding: 0.8rem 1rem; }
236
+ .fact-item { margin-bottom: 0.9rem; padding-bottom: 0.9rem; border-bottom: 1px solid var(--border-light); }
237
+ .fact-item:last-child { margin-bottom: 0; padding-bottom: 0; border-bottom: none; }
238
+ .fact-source { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.35rem; }
239
+ .claim-date { color: var(--text-muted); font-size: 0.8rem; }
240
+ .fact-details { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; margin-bottom: 0.35rem; }
241
+ .fact-text { flex: 1; color: var(--text-secondary); font-size: 0.95rem; line-height: 1.4; }
242
+ .fact-rating { padding: 0.25rem 0.6rem; border-radius: var(--radius-xs); font-weight: 800; font-size: 0.8rem; white-space: nowrap; }
243
+ .fact-rating.false, .fact-rating.deceptive { background: var(--accent-error); color: #fff; }
244
+ .fact-rating.unsubstantiated { background: var(--accent-warning); color: #fff; }
245
+ .fact-link { color: var(--accent-primary); text-decoration: none; font-size: 0.85rem; }
246
+ .fact-link:hover { text-decoration: underline; }
247
+
248
+ /* Uploaded images with overlays */
249
+ .uploaded-images-grid {
250
+ display: grid;
251
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
252
+ gap: 1rem;
253
+ padding: 1rem 1.25rem;
254
+ }
255
+ .vision-card {
256
+ background: var(--bg-surface);
257
+ border: 1px solid var(--border-light);
258
+ border-radius: var(--radius-sm);
259
+ padding: 0.9rem;
260
+ }
261
+ .vision-header {
262
+ display: flex; justify-content: space-between; align-items: center;
263
+ margin-bottom: 0.6rem; border-bottom: 1px solid var(--border-light); padding-bottom: 0.4rem;
264
+ }
265
+ .image-type { padding: 0.2rem 0.55rem; border-radius: var(--radius-xs); font-size: 0.8rem; font-weight: 800; }
266
+ .ai-image { background: var(--accent-warning); color: #fff; }
267
+ .real-image { background: var(--accent-success); color: #fff; }
268
+
269
+ .image-canvas-wrap {
270
+ position: relative;
271
+ width: 100%;
272
+ background: #111;
273
+ border: 1px solid var(--border-color);
274
+ border-radius: var(--radius-xs);
275
+ overflow: hidden;
276
+ }
277
+ .image-canvas-wrap img { display: block; width: 100%; height: auto; }
278
+ .overlay-canvas {
279
+ position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;
280
+ }
281
+ .no-image { color: var(--text-secondary); padding: 1rem; text-align: center; }
282
+ .faces-count { margin-top: 0.6rem; color: var(--text-secondary); }
283
+
284
+ /* Export */
285
+ .export-section {
286
+ background: var(--bg-card); border: 1px solid var(--border-color);
287
+ border-radius: var(--radius-sm); padding: 1.2rem; text-align: center;
288
+ }
289
+ .export-actions { display: flex; gap: 0.75rem; justify-content: center; flex-wrap: wrap; }
290
+ .export-btn {
291
+ background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-light);
292
+ padding: 0.7rem 1.0rem; border-radius: var(--radius-xs); cursor: pointer; transition: background .15s, border-color .15s;
293
+ font-size: 0.9rem; font-weight: 800;
294
+ }
295
+ .export-btn:hover { background: var(--bg-input); border-color: var(--accent-primary); }
296
+
297
+ /* Responsive */
298
+ @media (max-width: 820px) {
299
+ .dashboard-grid { grid-template-columns: 1fr; }
300
+ .form-container { padding: 1.25rem; }
301
+ .export-actions { flex-direction: column; }
302
+ }
303
+
304
+
305
+
306
+ /* ... keep your existing CSS exactly as-is ... */
307
+
308
+ /* NEW: inline/compact header inside the analysis form-container */
309
+ .form-header-inline {
310
+ display: flex;
311
+ align-items: center;
312
+ justify-content: space-between;
313
+ gap: .75rem;
314
+ padding-bottom: .25rem;
315
+ border-bottom: 1px solid var(--border-color);
316
+ }
317
+ .form-header-inline .inline-left {
318
+ display: flex;
319
+ align-items: center;
320
+ gap: .75rem;
321
+ }
322
+ .form-header-inline h2 {
323
+ margin: 0;
324
+ }
325
+ .form-header-inline .analysis-meta {
326
+ display: flex;
327
+ gap: .75rem;
328
+ color: var(--text-secondary);
329
+ font-size: 0.9rem;
330
+ }
331
+
332
+ /* NEW: analysis container block spacing (inside the shared card) */
333
+ .analysis-card-block {
334
+ margin-top: 1rem;
335
+ }
static/js/script.js ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class IntroController {
2
+ constructor() {
3
+ this.state = { uploadedImages: [], isAnalyzing: false };
4
+ this.cacheEls();
5
+ if (!this.el.newsForm) return;
6
+ this.bind();
7
+ }
8
+ cacheEls() {
9
+ this.el = {
10
+ newsForm: document.getElementById('newsForm'),
11
+ newsText: document.getElementById('newsText'),
12
+ fileInput: document.getElementById('fileInput'),
13
+ uploadArea: document.getElementById('uploadArea'),
14
+ browseBtn: document.getElementById('browseBtn'),
15
+ imagePreviews: document.getElementById('imagePreviews'),
16
+ analyzeBtn: document.getElementById('analyzeBtn'),
17
+ clearBtn: document.getElementById('clearBtn'),
18
+ btnLoader: document.getElementById('btnLoader'),
19
+ charCounter: document.getElementById('charCounter'),
20
+ messageContainer: document.getElementById('messageContainer')
21
+ };
22
+ }
23
+ bind() {
24
+ this.el.newsText.addEventListener('input', () => this.updateCharCount());
25
+ this.el.browseBtn.addEventListener('click', () => this.el.fileInput.click());
26
+ this.el.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
27
+ this.el.uploadArea.addEventListener('click', () => this.el.fileInput.click());
28
+ this.el.uploadArea.addEventListener('dragover', (e) => this.dragOver(e));
29
+ this.el.uploadArea.addEventListener('dragleave', (e) => this.dragLeave(e));
30
+ this.el.uploadArea.addEventListener('drop', (e) => this.drop(e));
31
+ this.el.newsForm.addEventListener('submit', (e) => this.submit(e));
32
+ this.el.clearBtn.addEventListener('click', () => this.clearAll());
33
+ }
34
+ updateCharCount() {
35
+ const len = this.el.newsText.value.length;
36
+ this.el.charCounter.textContent = `${len}/10000`;
37
+ if (len > 9000) this.el.charCounter.style.color = 'var(--accent-error)';
38
+ else if (len > 7000) this.el.charCounter.style.color = 'var(--accent-warning)';
39
+ else this.el.charCounter.style.color = 'var(--text-muted)';
40
+ }
41
+ dragOver(e){ e.preventDefault(); this.el.uploadArea.classList.add('drag-over'); }
42
+ dragLeave(e){ e.preventDefault(); this.el.uploadArea.classList.remove('drag-over'); }
43
+ drop(e){ e.preventDefault(); this.el.uploadArea.classList.remove('drag-over'); this.processFiles(Array.from(e.dataTransfer.files)); }
44
+ handleFileSelect(e){ this.processFiles(Array.from(e.target.files)); this.el.fileInput.value = ''; }
45
+ processFiles(files){
46
+ files.forEach(file=>{
47
+ if(!file.type.startsWith('image/')){ this.toast('Invalid File','Please upload only image files','error'); return; }
48
+ if(file.size > 10*1024*1024){ this.toast('File Too Large','Image must be smaller than 10MB','error'); return; }
49
+ const reader=new FileReader();
50
+ reader.onload=(ev)=>{
51
+ const imageData={ id:Date.now()+Math.random(), name:file.name, size:this.hSize(file.size), data:ev.target.result };
52
+ this.state.uploadedImages.push(imageData);
53
+ this.renderPreview(imageData);
54
+ };
55
+ reader.readAsDataURL(file);
56
+ });
57
+ }
58
+ hSize(bytes){
59
+ if(bytes===0) return '0 B';
60
+ const k=1024, sizes=['B','KB','MB']; const i=Math.floor(Math.log(bytes)/Math.log(k));
61
+ return `${parseFloat((bytes/Math.pow(k,i)).toFixed(1))} ${sizes[i]}`;
62
+ }
63
+ renderPreview(imageData){
64
+ const el=document.createElement('div');
65
+ el.className='preview-item';
66
+ el.innerHTML=`
67
+ <img src="${imageData.data}" alt="${imageData.name}" class="preview-image">
68
+ <button type="button" class="remove-btn" data-id="${imageData.id}">×</button>
69
+ `;
70
+ this.el.imagePreviews.appendChild(el);
71
+ el.querySelector('.remove-btn').addEventListener('click',()=>this.removeImage(imageData.id));
72
+ }
73
+ removeImage(id){
74
+ this.state.uploadedImages=this.state.uploadedImages.filter(x=>x.id!==id);
75
+ const btn=this.el.imagePreviews.querySelector(`[data-id="${id}"]`);
76
+ btn?.closest('.preview-item')?.remove();
77
+ }
78
+ async submit(e){
79
+ e.preventDefault();
80
+ if(!this.validate()){ this.toast('Missing Information','Please add both text and at least one image.','error'); return; }
81
+ if(this.state.isAnalyzing) return;
82
+ this.state.isAnalyzing=true; this.el.analyzeBtn.disabled=true; this.el.btnLoader.classList.add('active');
83
+ this.el.analyzeBtn.querySelector('.btn-text').textContent='Analyzing...';
84
+ try{
85
+ const payload={ text:this.el.newsText.value.trim(), images:this.state.uploadedImages.map(img=>({name:img.name,data:img.data})) };
86
+ const res=await fetch('/analyze',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) });
87
+ const json=await res.json();
88
+ if(json.success){ window.location.href=`/analysis/${json.analysis_id}`; }
89
+ else{ throw new Error(json.error || 'Analysis failed'); }
90
+ }catch(err){
91
+ console.error(err); this.toast('Analysis Failed', err.message,'error');
92
+ }finally{
93
+ this.state.isAnalyzing=false; this.el.analyzeBtn.disabled=false; this.el.btnLoader.classList.remove('active');
94
+ this.el.analyzeBtn.querySelector('.btn-text').textContent='Analyze News';
95
+ }
96
+ }
97
+ validate(){ return this.el.newsText.value.trim().length>0 && this.state.uploadedImages.length>0; }
98
+ clearAll(){
99
+ this.el.newsText.value=''; this.state.uploadedImages=[]; this.el.imagePreviews.innerHTML=''; this.updateCharCount();
100
+ this.toast('Cleared','All inputs have been reset.','success');
101
+ }
102
+ toast(title,text,type='info'){
103
+ const msg=document.createElement('div'); msg.className=`message ${type}`;
104
+ msg.innerHTML=`<div class="message-content"><div class="message-title">${title}</div><div class="message-text">${text}</div></div><button class="message-close">×</button>`;
105
+ const root=document.getElementById('messageContainer') || document.body; root.appendChild(msg);
106
+ setTimeout(()=>msg.classList.add('show'),50);
107
+ msg.querySelector('.message-close').addEventListener('click',()=>this.hideToast(msg));
108
+ setTimeout(()=>this.hideToast(msg),5000);
109
+ }
110
+ hideToast(msg){ msg.classList.remove('show'); setTimeout(()=>msg.remove(),250); }
111
+ }
112
+
113
+ class OverlayController {
114
+ constructor() {
115
+ const dataEl = document.getElementById('analysis-data');
116
+ if (!dataEl) return;
117
+ try { this.analysis = JSON.parse(dataEl.textContent || '{}'); }
118
+ catch { this.analysis = null; }
119
+ if (!this.analysis || !Array.isArray(this.analysis.images)) return;
120
+ this.initOverlays();
121
+ window.addEventListener('resize', () => this.redrawAll());
122
+ }
123
+ initOverlays() {
124
+ const imgs = document.querySelectorAll('.image-with-overlay');
125
+ imgs.forEach(img => {
126
+ if (img.complete) this.setupCanvasFor(img);
127
+ else img.onload = () => this.setupCanvasFor(img);
128
+ });
129
+ }
130
+ setupCanvasFor(imgEl) {
131
+ const idx = parseInt(imgEl.dataset.index, 10);
132
+ const canvas = document.querySelector(`.overlay-canvas[data-index="${idx}"]`);
133
+ if (!canvas) return;
134
+ const rect = imgEl.getBoundingClientRect();
135
+ const dpr = window.devicePixelRatio || 1;
136
+ canvas.width = Math.floor(rect.width * dpr);
137
+ canvas.height = Math.floor(rect.height * dpr);
138
+ const ctx = canvas.getContext('2d');
139
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
140
+ const natW = imgEl.naturalWidth || rect.width;
141
+ const natH = imgEl.naturalHeight || rect.height;
142
+ const sx = rect.width / natW;
143
+ const sy = rect.height / natH;
144
+ this.drawFaces(ctx, this.analysis.images[idx], sx, sy);
145
+ }
146
+ redrawAll() {
147
+ document.querySelectorAll('.image-with-overlay').forEach(img => this.setupCanvasFor(img));
148
+ }
149
+ drawFaces(ctx, imageAnalysis, sx, sy) {
150
+ const faces = imageAnalysis.faces || [];
151
+ const deepfakes = imageAnalysis.deepfake_faces || [];
152
+ const BOX_COLOR = '#ef4444';
153
+ const BOX_COLOR_DF = '#f59e0b';
154
+ const POINT_COLOR = '#10b981';
155
+ const POINT_COLOR_DF = '#f59e0b';
156
+ const LINE_W = 2;
157
+ ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
158
+ faces.forEach((f, i) => {
159
+ const isDF = !!deepfakes[i];
160
+ const stroke = isDF ? BOX_COLOR_DF : BOX_COLOR;
161
+ const dot = isDF ? POINT_COLOR_DF : POINT_COLOR;
162
+ ctx.lineWidth = LINE_W;
163
+ ctx.strokeStyle = stroke;
164
+ const x1 = f.box_start_point[0] * sx + 0.5;
165
+ const y1 = f.box_start_point[1] * sy + 0.5;
166
+ const x2 = f.box_end_point[0] * sx + 0.5;
167
+ const y2 = f.box_end_point[1] * sy + 0.5;
168
+ ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
169
+ const drawPoint = (pt) => {
170
+ const px = pt[0] * sx, py = pt[1] * sy, size = 4;
171
+ ctx.fillStyle = dot; ctx.fillRect(px - size/2, py - size/2, size, size);
172
+ };
173
+ drawPoint(f.left_eye);
174
+ drawPoint(f.right_eye);
175
+ drawPoint(f.nose);
176
+ drawPoint(f.left_mouth);
177
+ drawPoint(f.right_mouth);
178
+ });
179
+ }
180
+ }
181
+
182
+ document.addEventListener('DOMContentLoaded', () => {
183
+ new IntroController();
184
+ new OverlayController();
185
+ });
static/uploads/0a42e714b24849829247e0a44e51953a.webp ADDED
static/uploads/0d337020f9ae491782633516889f3e79.webp ADDED
static/uploads/26ad0b08883347718d38274861bccd8e.webp ADDED
static/uploads/26de02ceda464fb9acd0edb6b0c678db.webp ADDED
static/uploads/28afe2d042a341ceb60c187954deccfc.jpg ADDED

Git LFS Details

  • SHA256: 2f97e955b45af3aeb17d6eb6dab6efec3dd5408628632c65a4e150104d640f36
  • Pointer size: 131 Bytes
  • Size of remote file: 183 kB
static/uploads/3156289a7e0e46fa82b7d4f0965aeae7.webp ADDED
static/uploads/37f95b1e0e9c44b79e527a0deafab813.webp ADDED
static/uploads/3b69dc4d23714b3a801c5131b476fafd.webp ADDED
static/uploads/45aa41d76e71411bb28a19de398ba785.webp ADDED