JeyBii commited on
Commit
2b9b5b5
·
verified ·
1 Parent(s): 1f34523

Upload folder using huggingface_hub

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 +46 -10
  2. .github/workflows/scrape.yml +34 -34
  3. .gitignore +46 -35
  4. DEPLOYMENT.md +71 -71
  5. Dockerfile +21 -8
  6. add_cebuano_data.py +329 -0
  7. api/main.py +30 -0
  8. backend/analyze_languages.py +275 -275
  9. backend/benchmark.py +538 -0
  10. backend/bias_words_report.txt +148 -148
  11. backend/evaluate_model.py +820 -820
  12. backend/mine_bias_words.py +194 -194
  13. backend/tune_rf.py +333 -0
  14. check_app/.flutter-plugins-dependencies +1 -0
  15. check_app/.gitignore +45 -45
  16. check_app/.metadata +45 -45
  17. check_app/README.md +17 -17
  18. check_app/analysis_options.yaml +28 -28
  19. check_app/android/.gitignore +14 -14
  20. check_app/android/.gradle/8.14/checksums/checksums.lock +0 -0
  21. check_app/android/.gradle/8.14/checksums/md5-checksums.bin +0 -0
  22. check_app/android/.gradle/8.14/checksums/sha1-checksums.bin +3 -0
  23. check_app/android/.gradle/8.14/executionHistory/executionHistory.bin +3 -0
  24. check_app/android/.gradle/8.14/executionHistory/executionHistory.lock +0 -0
  25. check_app/android/.gradle/8.14/fileChanges/last-build.bin +0 -0
  26. check_app/android/.gradle/8.14/fileHashes/fileHashes.bin +3 -0
  27. check_app/android/.gradle/8.14/fileHashes/fileHashes.lock +0 -0
  28. check_app/android/.gradle/8.14/fileHashes/resourceHashesCache.bin +0 -0
  29. check_app/android/.gradle/8.14/gc.properties +0 -0
  30. check_app/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  31. check_app/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  32. check_app/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  33. check_app/android/.gradle/file-system.probe +0 -0
  34. check_app/android/.gradle/noVersion/buildLogic.lock +0 -0
  35. check_app/android/.gradle/vcs-1/gc.properties +0 -0
  36. check_app/android/app/build.gradle.kts +44 -44
  37. check_app/android/app/src/debug/AndroidManifest.xml +7 -7
  38. check_app/android/app/src/main/AndroidManifest.xml +46 -45
  39. check_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java +39 -0
  40. check_app/android/app/src/main/kotlin/com/bantaypahayag/app/MainActivity.kt +5 -0
  41. check_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -12
  42. check_app/android/app/src/main/res/drawable/launch_background.xml +12 -12
  43. check_app/android/app/src/main/res/values-night/styles.xml +18 -18
  44. check_app/android/app/src/main/res/values/styles.xml +18 -18
  45. check_app/android/app/src/profile/AndroidManifest.xml +7 -7
  46. check_app/android/build.gradle.kts +24 -24
  47. check_app/android/build/reports/problems/problems-report.html +0 -0
  48. check_app/android/gradle.properties +2 -2
  49. check_app/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  50. check_app/android/gradle/wrapper/gradle-wrapper.properties +5 -5
.gitattributes CHANGED
@@ -1,10 +1,46 @@
1
- *.zip filter=lfs diff=lfs merge=lfs -text
2
- *.csv filter=lfs diff=lfs merge=lfs -text
3
- *.npz filter=lfs diff=lfs merge=lfs -text
4
- *.db filter=lfs diff=lfs merge=lfs -text
5
- *.pkl filter=lfs diff=lfs merge=lfs -text
6
- *.jpg filter=lfs diff=lfs merge=lfs -text
7
- *.jpeg filter=lfs diff=lfs merge=lfs -text
8
- *.png filter=lfs diff=lfs merge=lfs -text
9
- *.PNG filter=lfs diff=lfs merge=lfs -text
10
- *.ico filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.pkl filter=lfs diff=lfs merge=lfs -text
2
+ check_app/android/.gradle/8.14/checksums/sha1-checksums.bin filter=lfs diff=lfs merge=lfs -text
3
+ check_app/android/.gradle/8.14/executionHistory/executionHistory.bin filter=lfs diff=lfs merge=lfs -text
4
+ check_app/android/.gradle/8.14/fileHashes/fileHashes.bin filter=lfs diff=lfs merge=lfs -text
5
+ check_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
6
+ check_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
7
+ check_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
8
+ check_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
9
+ check_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png filter=lfs diff=lfs merge=lfs -text
10
+ check_app/assets/images/app_logo_dark.jpg filter=lfs diff=lfs merge=lfs -text
11
+ check_app/assets/images/app_logo_light.jpg filter=lfs diff=lfs merge=lfs -text
12
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png filter=lfs diff=lfs merge=lfs -text
13
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png filter=lfs diff=lfs merge=lfs -text
14
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png filter=lfs diff=lfs merge=lfs -text
15
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png filter=lfs diff=lfs merge=lfs -text
16
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png filter=lfs diff=lfs merge=lfs -text
17
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png filter=lfs diff=lfs merge=lfs -text
18
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png filter=lfs diff=lfs merge=lfs -text
19
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png filter=lfs diff=lfs merge=lfs -text
20
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png filter=lfs diff=lfs merge=lfs -text
21
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png filter=lfs diff=lfs merge=lfs -text
22
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png filter=lfs diff=lfs merge=lfs -text
23
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png filter=lfs diff=lfs merge=lfs -text
24
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png filter=lfs diff=lfs merge=lfs -text
25
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png filter=lfs diff=lfs merge=lfs -text
26
+ check_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png filter=lfs diff=lfs merge=lfs -text
27
+ check_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png filter=lfs diff=lfs merge=lfs -text
28
+ check_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png filter=lfs diff=lfs merge=lfs -text
29
+ check_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png filter=lfs diff=lfs merge=lfs -text
30
+ check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png filter=lfs diff=lfs merge=lfs -text
31
+ check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png filter=lfs diff=lfs merge=lfs -text
32
+ check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png filter=lfs diff=lfs merge=lfs -text
33
+ check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png filter=lfs diff=lfs merge=lfs -text
34
+ check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png filter=lfs diff=lfs merge=lfs -text
35
+ check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png filter=lfs diff=lfs merge=lfs -text
36
+ check_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png filter=lfs diff=lfs merge=lfs -text
37
+ check_app/web/favicon.png filter=lfs diff=lfs merge=lfs -text
38
+ check_app/web/icons/Icon-192.png filter=lfs diff=lfs merge=lfs -text
39
+ check_app/web/icons/Icon-512.png filter=lfs diff=lfs merge=lfs -text
40
+ check_app/web/icons/Icon-maskable-192.png filter=lfs diff=lfs merge=lfs -text
41
+ check_app/web/icons/Icon-maskable-512.png filter=lfs diff=lfs merge=lfs -text
42
+ check_app/windows/runner/resources/app_icon.ico filter=lfs diff=lfs merge=lfs -text
43
+ Icons/648286034_929277936354011_247561233351299912_n.jpg filter=lfs diff=lfs merge=lfs -text
44
+ Icons/648528435_1489135242834045_123474419086154405_n.jpg filter=lfs diff=lfs merge=lfs -text
45
+ Icons/IMG_7342.PNG filter=lfs diff=lfs merge=lfs -text
46
+ Icons/IMG_7343.PNG filter=lfs diff=lfs merge=lfs -text
.github/workflows/scrape.yml CHANGED
@@ -1,34 +1,34 @@
1
- name: Daily News Scraper
2
-
3
- on:
4
- schedule:
5
- # Runs every day at 00:00 UTC (8:00 AM PHT)
6
- - cron: '0 0 * * *'
7
- workflow_dispatch: # Allows manual trigger from GitHub UI
8
-
9
- jobs:
10
- scrape:
11
- runs-on: ubuntu-latest
12
-
13
- steps:
14
- - name: Checkout repository
15
- uses: actions/checkout@v4
16
-
17
- - name: Set up Python
18
- uses: actions/setup-python@v5
19
- with:
20
- python-version: '3.12'
21
-
22
- - name: Install dependencies
23
- run: pip install feedparser
24
-
25
- - name: Run scraper
26
- run: python scraper/news_scraper.py
27
-
28
- - name: Commit updated database
29
- run: |
30
- git config user.name "GitHub Actions Bot"
31
- git config user.email "actions@github.com"
32
- git add data/news.db
33
- git diff --staged --quiet || git commit -m "Auto-update: scraped news articles $(date -u +'%Y-%m-%d')"
34
- git push
 
1
+ name: Daily News Scraper
2
+
3
+ on:
4
+ schedule:
5
+ # Runs every day at 00:00 UTC (8:00 AM PHT)
6
+ - cron: '0 0 * * *'
7
+ workflow_dispatch: # Allows manual trigger from GitHub UI
8
+
9
+ jobs:
10
+ scrape:
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: '3.12'
21
+
22
+ - name: Install dependencies
23
+ run: pip install feedparser
24
+
25
+ - name: Run scraper
26
+ run: python scraper/news_scraper.py
27
+
28
+ - name: Commit updated database
29
+ run: |
30
+ git config user.name "GitHub Actions Bot"
31
+ git config user.email "actions@github.com"
32
+ git add data/news.db
33
+ git diff --staged --quiet || git commit -m "Auto-update: scraped news articles $(date -u +'%Y-%m-%d')"
34
+ git push
.gitignore CHANGED
@@ -1,35 +1,46 @@
1
- # Virtual environment
2
- venv/
3
- .venv/
4
-
5
- # Python
6
- __pycache__/
7
- *.pyc
8
- *.pyo
9
- *.egg-info/
10
- dist/
11
- build/
12
-
13
- # IDE
14
- .vscode/
15
- .idea/
16
-
17
- # OS
18
- .DS_Store
19
- Thumbs.db
20
-
21
- # Environment
22
- .env
23
-
24
- # Large data files — too big for GitHub LFS free tier
25
- # Download datasets separately / run train.py to regenerate
26
- data/raw/
27
- data/embeddings.npz
28
- .pyre_configuration
29
-
30
- # Trained model binaries — regenerate with python backend/train.py
31
- data_models/*.pkl
32
-
33
- # Augmented translation caches — regenerate with train.py
34
- data/augmented_tl_fakes.csv
35
- data/augmented_ceb_fakes.csv
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Virtual environment
2
+ venv/
3
+ .venv/
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.pyc
8
+ *.pyo
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+
13
+ # IDE
14
+ .vscode/
15
+ .idea/
16
+
17
+ # OS
18
+ .DS_Store
19
+ Thumbs.db
20
+
21
+ # Environment
22
+ .env
23
+
24
+ # Large data files — too big for GitHub LFS free tier
25
+ # Download datasets separately / run train.py to regenerate
26
+ data/raw/
27
+ data/embeddings.npz
28
+ .pyre_configuration
29
+
30
+ # Trained model binaries — tracked via Git LFS (see .gitattributes)
31
+ # Do NOT gitignore these: the HF Space needs them to start without retraining.
32
+ # data_models/*.pkl
33
+
34
+ # Augmented translation caches — regenerate with train.py
35
+ data/augmented_tl_fakes.csv
36
+ data/augmented_ceb_fakes.csv
37
+
38
+ # Temp deployment/sync scripts & artifacts (not needed in repo)
39
+ dummy.bin
40
+ fix_hf_models.py
41
+ hf_start.sh
42
+ hf_tfidf.pkl
43
+ restore_remaining_models.py
44
+ sync_all_hf_models.py
45
+ sync_source_to_hf.py
46
+ commitchecker.txt
DEPLOYMENT.md CHANGED
@@ -1,71 +1,71 @@
1
- # Deployment Guide
2
-
3
- ## ⚠️ Required Step After Every Fresh Deploy / Clone
4
-
5
- The ML model files (`.pkl`) are **not stored in the repository** (binary files, too large for GitHub).
6
- You must generate them **once on the server** after the first clone.
7
-
8
- ### Step 1 — Install dependencies
9
- ```bash
10
- pip install -r requirements.txt
11
- ```
12
-
13
- ### Step 2 — Train the main model
14
- ```bash
15
- python backend/train.py
16
- ```
17
- This will generate the following files in `data_models/`:
18
- - `rf_fakenews_model.pkl`
19
- - `tfidf_fakenews.pkl`
20
- - `stylo_scaler.pkl`
21
-
22
- ⏱ Takes approximately **10–20 minutes** depending on server specs.
23
- 📦 Requires ~**2 GB RAM** during training.
24
-
25
- ### Step 3 — (Optional) Train the Tagalog sub-model
26
- ```bash
27
- python backend/train.py --tagalog-only
28
- ```
29
-
30
- ### Step 4 — Start the app normally
31
- ```bash
32
- # Whatever your usual start command is, e.g.:
33
- python app.py
34
- # or
35
- uvicorn app:app --host 0.0.0.0 --port 8000
36
- ```
37
-
38
- ---
39
-
40
- ## After the First Deploy
41
-
42
- **You do NOT need to retrain on every redeploy.**
43
- The `.pkl` files persist on the server. Future `git pull` + restart is enough.
44
-
45
- Only retrain if:
46
- - The server is wiped / re-imaged
47
- - `backend/train.py` was updated with new features
48
- - Model files were accidentally deleted
49
-
50
- ---
51
-
52
- ## Checking if Models Exist
53
-
54
- ```bash
55
- ls data_models/*.pkl
56
- ```
57
-
58
- Expected output (6 files):
59
- ```
60
- data_models/rf_fakenews_model.pkl
61
- data_models/rf_fakenews_tagalog.pkl
62
- data_models/rf_fakenews_cebuano.pkl
63
- data_models/tfidf_fakenews.pkl
64
- data_models/tfidf_fakenews_tagalog.pkl
65
- data_models/tfidf_fakenews_cebuano.pkl
66
- data_models/stylo_scaler.pkl
67
- data_models/stylo_scaler_tagalog.pkl
68
- data_models/stylo_scaler_cebuano.pkl
69
- ```
70
-
71
- If any are missing → run `python backend/train.py`.
 
1
+ # Deployment Guide
2
+
3
+ ## ⚠️ Required Step After Every Fresh Deploy / Clone
4
+
5
+ The ML model files (`.pkl`) are **not stored in the repository** (binary files, too large for GitHub).
6
+ You must generate them **once on the server** after the first clone.
7
+
8
+ ### Step 1 — Install dependencies
9
+ ```bash
10
+ pip install -r requirements.txt
11
+ ```
12
+
13
+ ### Step 2 — Train the main model
14
+ ```bash
15
+ python backend/train.py
16
+ ```
17
+ This will generate the following files in `data_models/`:
18
+ - `rf_fakenews_model.pkl`
19
+ - `tfidf_fakenews.pkl`
20
+ - `stylo_scaler.pkl`
21
+
22
+ ⏱ Takes approximately **10–20 minutes** depending on server specs.
23
+ 📦 Requires ~**2 GB RAM** during training.
24
+
25
+ ### Step 3 — (Optional) Train the Tagalog sub-model
26
+ ```bash
27
+ python backend/train.py --tagalog-only
28
+ ```
29
+
30
+ ### Step 4 — Start the app normally
31
+ ```bash
32
+ # Whatever your usual start command is, e.g.:
33
+ python app.py
34
+ # or
35
+ uvicorn app:app --host 0.0.0.0 --port 8000
36
+ ```
37
+
38
+ ---
39
+
40
+ ## After the First Deploy
41
+
42
+ **You do NOT need to retrain on every redeploy.**
43
+ The `.pkl` files persist on the server. Future `git pull` + restart is enough.
44
+
45
+ Only retrain if:
46
+ - The server is wiped / re-imaged
47
+ - `backend/train.py` was updated with new features
48
+ - Model files were accidentally deleted
49
+
50
+ ---
51
+
52
+ ## Checking if Models Exist
53
+
54
+ ```bash
55
+ ls data_models/*.pkl
56
+ ```
57
+
58
+ Expected output (6 files):
59
+ ```
60
+ data_models/rf_fakenews_model.pkl
61
+ data_models/rf_fakenews_tagalog.pkl
62
+ data_models/rf_fakenews_cebuano.pkl
63
+ data_models/tfidf_fakenews.pkl
64
+ data_models/tfidf_fakenews_tagalog.pkl
65
+ data_models/tfidf_fakenews_cebuano.pkl
66
+ data_models/stylo_scaler.pkl
67
+ data_models/stylo_scaler_tagalog.pkl
68
+ data_models/stylo_scaler_cebuano.pkl
69
+ ```
70
+
71
+ If any are missing → run `python backend/train.py`.
Dockerfile CHANGED
@@ -1,5 +1,5 @@
1
  # Use the official Python base image
2
- FROM python:3.10
3
 
4
  # Set the working directory for root installations
5
  WORKDIR /setup
@@ -7,20 +7,31 @@ WORKDIR /setup
7
  # Copy requirements first to leverage Docker cache
8
  COPY ./requirements.txt /setup/requirements.txt
9
 
10
- # Install python packages
11
- RUN pip install --no-cache-dir --upgrade pip
12
- RUN pip install --no-cache-dir -r /setup/requirements.txt
 
 
 
 
 
13
 
14
  # Hugging Face Spaces require running as a non-root user for security.
15
  # Set up a new user named "user" with user ID 1000
16
  RUN useradd -m -u 1000 user
17
 
 
 
 
 
 
18
  # Switch to the "user" user
19
  USER user
20
 
21
  # Set home to the user's home directory
22
  ENV HOME=/home/user \
23
- PATH=/home/user/.local/bin:$PATH
 
24
 
25
  # Set the working directory to the user's home directory
26
  WORKDIR $HOME/app
@@ -28,10 +39,12 @@ WORKDIR $HOME/app
28
  # Copy the current directory contents into the container at $HOME/app setting the owner to the user
29
  COPY --chown=user . $HOME/app
30
 
 
 
 
31
  # HuggingFace Spaces requires port 7860
32
  ENV PORT=7860
33
  EXPOSE 7860
34
 
35
- # Command to run the Uvicorn server
36
- CMD uvicorn api.main:app --host 0.0.0.0 --port $PORT
37
-
 
1
  # Use the official Python base image
2
+ FROM python:3.10-slim
3
 
4
  # Set the working directory for root installations
5
  WORKDIR /setup
 
7
  # Copy requirements first to leverage Docker cache
8
  COPY ./requirements.txt /setup/requirements.txt
9
 
10
+ # Install python packages (single RUN to reduce layers)
11
+ RUN pip install --no-cache-dir --upgrade pip && \
12
+ pip install --no-cache-dir -r /setup/requirements.txt
13
+
14
+ # Pre-download the MiniLM model during BUILD so it's baked into the image.
15
+ # This saves ~2-3 minutes on every cold start.
16
+ ENV HF_HOME=/setup/hf_cache
17
+ RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')"
18
 
19
  # Hugging Face Spaces require running as a non-root user for security.
20
  # Set up a new user named "user" with user ID 1000
21
  RUN useradd -m -u 1000 user
22
 
23
+ # Move the cached model to the user's home so it's accessible after user switch
24
+ RUN mkdir -p /home/user/.cache && \
25
+ cp -r /setup/hf_cache /home/user/.cache/huggingface && \
26
+ chown -R user:user /home/user/.cache
27
+
28
  # Switch to the "user" user
29
  USER user
30
 
31
  # Set home to the user's home directory
32
  ENV HOME=/home/user \
33
+ PATH=/home/user/.local/bin:$PATH \
34
+ HF_HOME=/home/user/.cache/huggingface
35
 
36
  # Set the working directory to the user's home directory
37
  WORKDIR $HOME/app
 
39
  # Copy the current directory contents into the container at $HOME/app setting the owner to the user
40
  COPY --chown=user . $HOME/app
41
 
42
+ # Make the startup script executable
43
+ RUN chmod +x start.sh
44
+
45
  # HuggingFace Spaces requires port 7860
46
  ENV PORT=7860
47
  EXPOSE 7860
48
 
49
+ # Startup: auto-train models if missing, then start Uvicorn
50
+ CMD ["bash", "start.sh"]
 
add_cebuano_data.py ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cebuano Fake News Data Entry Tool
3
+ ===================================
4
+ Manually input Cebuano fake news articles into
5
+ data/raw/augmented_ceb_fakes.csv for model training.
6
+
7
+ Usage:
8
+ python add_cebuano_data.py
9
+
10
+ Features:
11
+ - Interactive CLI with menu options
12
+ - Paste single-line or multi-line articles
13
+ - Preview entries before saving
14
+ - View existing dataset stats
15
+ - Duplicate detection (warns if near-identical text exists)
16
+ """
17
+
18
+ import csv
19
+ import os
20
+ import sys
21
+ import textwrap
22
+ from datetime import datetime
23
+
24
+ # ── Paths ──
25
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
26
+ CSV_PATH = os.path.join(SCRIPT_DIR, "data", "raw", "augmented_ceb_fakes.csv")
27
+ BACKUP_DIR = os.path.join(SCRIPT_DIR, "data", "raw", ".backups")
28
+
29
+
30
+ def _count_existing_rows() -> int:
31
+ """Count how many articles are already in the CSV."""
32
+ if not os.path.exists(CSV_PATH):
33
+ return 0
34
+ with open(CSV_PATH, "r", encoding="utf-8") as f:
35
+ reader = csv.reader(f)
36
+ header = next(reader, None)
37
+ return sum(1 for _ in reader)
38
+
39
+
40
+ def _load_existing_articles() -> list[str]:
41
+ """Load all existing article texts (for duplicate checking)."""
42
+ articles = []
43
+ if not os.path.exists(CSV_PATH):
44
+ return articles
45
+ with open(CSV_PATH, "r", encoding="utf-8") as f:
46
+ reader = csv.reader(f)
47
+ next(reader, None) # skip header
48
+ for row in reader:
49
+ if row:
50
+ articles.append(row[0].strip().lower())
51
+ return articles
52
+
53
+
54
+ def _is_duplicate(new_text: str, existing: list[str], threshold: int = 50) -> bool:
55
+ """Check if the first `threshold` characters match any existing article."""
56
+ snippet = new_text.strip().lower()[:threshold]
57
+ if len(snippet) < 20:
58
+ return False
59
+ for art in existing:
60
+ if art[:threshold] == snippet:
61
+ return True
62
+ return False
63
+
64
+
65
+ def _backup_csv():
66
+ """Create a timestamped backup of the current CSV."""
67
+ if not os.path.exists(CSV_PATH):
68
+ return
69
+ os.makedirs(BACKUP_DIR, exist_ok=True)
70
+ ts = datetime.now().strftime("%Y%m%d_%H%M%S")
71
+ backup_path = os.path.join(BACKUP_DIR, f"augmented_ceb_fakes_{ts}.csv")
72
+ with open(CSV_PATH, "r", encoding="utf-8") as src:
73
+ with open(backup_path, "w", encoding="utf-8", newline="") as dst:
74
+ dst.write(src.read())
75
+ print(f" Backup saved: {os.path.relpath(backup_path, SCRIPT_DIR)}")
76
+
77
+
78
+ def _ensure_csv_exists():
79
+ """Create the CSV with header if it doesn't exist yet."""
80
+ if not os.path.exists(CSV_PATH):
81
+ os.makedirs(os.path.dirname(CSV_PATH), exist_ok=True)
82
+ with open(CSV_PATH, "w", encoding="utf-8", newline="") as f:
83
+ writer = csv.writer(f)
84
+ writer.writerow(["article"])
85
+ print(f" Created new CSV: {CSV_PATH}")
86
+
87
+
88
+ def _append_articles(articles: list[str]):
89
+ """Append a list of article texts to the CSV."""
90
+ with open(CSV_PATH, "a", encoding="utf-8", newline="") as f:
91
+ writer = csv.writer(f)
92
+ for text in articles:
93
+ writer.writerow([text])
94
+
95
+
96
+ def _preview_text(text: str, width: int = 80) -> str:
97
+ """Return a truncated preview of article text."""
98
+ preview = text.replace("\n", " ").strip()
99
+ if len(preview) > width:
100
+ return preview[:width] + "..."
101
+ return preview
102
+
103
+
104
+ # ── CLI Display ──
105
+
106
+ def print_header():
107
+ print()
108
+ print("=" * 60)
109
+ print(" CEBUANO FAKE NEWS — DATA ENTRY TOOL")
110
+ print("=" * 60)
111
+ print(f" CSV: {os.path.relpath(CSV_PATH, SCRIPT_DIR)}")
112
+ print(f" Existing articles: {_count_existing_rows():,}")
113
+ print("=" * 60)
114
+
115
+
116
+ def print_menu():
117
+ print()
118
+ print(" [1] Add a single article")
119
+ print(" [2] Add multiple articles (batch mode)")
120
+ print(" [3] View dataset stats")
121
+ print(" [4] View last 5 entries")
122
+ print(" [5] Exit")
123
+ print()
124
+
125
+
126
+ def input_single_article() -> str | None:
127
+ """Prompt the user to type/paste a single article.
128
+
129
+ Supports multi-line input: type on multiple lines, then enter
130
+ a blank line to finish.
131
+ """
132
+ print()
133
+ print(" Paste or type the Cebuano fake news article below.")
134
+ print(" (Enter a blank line when done, or type 'cancel' to abort)")
135
+ print(" " + "-" * 50)
136
+
137
+ lines = []
138
+ while True:
139
+ try:
140
+ line = input(" > ")
141
+ except EOFError:
142
+ break
143
+ if line.strip().lower() == "cancel":
144
+ return None
145
+ if line.strip() == "" and lines:
146
+ break
147
+ lines.append(line)
148
+
149
+ text = " ".join(lines).strip()
150
+ if not text:
151
+ print(" (empty input — skipped)")
152
+ return None
153
+ return text
154
+
155
+
156
+ def add_single_article():
157
+ """Menu option 1: add one article."""
158
+ _ensure_csv_exists()
159
+ existing = _load_existing_articles()
160
+
161
+ text = input_single_article()
162
+ if text is None:
163
+ print(" Cancelled.")
164
+ return
165
+
166
+ # Duplicate check
167
+ if _is_duplicate(text, existing):
168
+ print()
169
+ print(" ⚠ WARNING: This article appears to be a duplicate!")
170
+ confirm = input(" Add anyway? (y/n): ").strip().lower()
171
+ if confirm != "y":
172
+ print(" Skipped.")
173
+ return
174
+
175
+ # Preview and confirm
176
+ print()
177
+ print(" Preview:")
178
+ print(f" {_preview_text(text, 100)}")
179
+ print(f" Length: {len(text):,} chars, {len(text.split()):,} words")
180
+ print()
181
+ confirm = input(" Save this article? (y/n): ").strip().lower()
182
+ if confirm != "y":
183
+ print(" Discarded.")
184
+ return
185
+
186
+ _append_articles([text])
187
+ print(f" ✓ Article saved! Total articles now: {_count_existing_rows():,}")
188
+
189
+
190
+ def add_batch_articles():
191
+ """Menu option 2: add multiple articles in batch."""
192
+ _ensure_csv_exists()
193
+ existing = _load_existing_articles()
194
+
195
+ print()
196
+ print(" BATCH MODE — Enter one article per prompt.")
197
+ print(" Type 'done' when finished, 'cancel' to discard all.")
198
+ print()
199
+
200
+ pending: list[str] = []
201
+ article_num = 1
202
+
203
+ while True:
204
+ print(f" --- Article #{article_num} ---")
205
+ text = input_single_article()
206
+
207
+ if text is None:
208
+ # Check if user typed cancel
209
+ break
210
+
211
+ if _is_duplicate(text, existing):
212
+ print(" ⚠ Possible duplicate — skipping this one.")
213
+ continue
214
+
215
+ pending.append(text)
216
+ print(f" ✓ Queued ({len(pending)} pending)")
217
+ article_num += 1
218
+
219
+ cont = input(" Add another? (y/n/done): ").strip().lower()
220
+ if cont in ("n", "done"):
221
+ break
222
+
223
+ if not pending:
224
+ print(" No articles to save.")
225
+ return
226
+
227
+ # Preview all
228
+ print()
229
+ print(f" === {len(pending)} articles ready to save ===")
230
+ for i, art in enumerate(pending, 1):
231
+ print(f" {i}. {_preview_text(art, 70)}")
232
+ print()
233
+
234
+ confirm = input(f" Save all {len(pending)} articles? (y/n): ").strip().lower()
235
+ if confirm != "y":
236
+ print(" Discarded all.")
237
+ return
238
+
239
+ _backup_csv()
240
+ _append_articles(pending)
241
+ print(f" ✓ {len(pending)} articles saved! Total now: {_count_existing_rows():,}")
242
+
243
+
244
+ def show_stats():
245
+ """Menu option 3: show dataset statistics."""
246
+ if not os.path.exists(CSV_PATH):
247
+ print(" CSV not found — no data yet.")
248
+ return
249
+
250
+ articles = _load_existing_articles()
251
+ total = len(articles)
252
+ if total == 0:
253
+ print(" Dataset is empty.")
254
+ return
255
+
256
+ word_counts = [len(a.split()) for a in articles]
257
+ char_counts = [len(a) for a in articles]
258
+
259
+ print()
260
+ print(" ╔══════════════════════════════════════╗")
261
+ print(" ║ DATASET STATISTICS ║")
262
+ print(" ╠══════════════════════════════════════╣")
263
+ print(f" ║ Total articles: {total:>10,} ║")
264
+ print(f" ║ Avg words/article: {sum(word_counts)//total:>10,} ║")
265
+ print(f" ║ Min words: {min(word_counts):>10,} ║")
266
+ print(f" ║ Max words: {max(word_counts):>10,} ║")
267
+ print(f" ║ Avg chars/article: {sum(char_counts)//total:>10,} ║")
268
+ print(f" ║ CSV size: {os.path.getsize(CSV_PATH):>10,} B ║")
269
+ print(" ╚══════════════════════════════════════╝")
270
+ print()
271
+
272
+
273
+ def show_last_entries():
274
+ """Menu option 4: show last 5 entries."""
275
+ if not os.path.exists(CSV_PATH):
276
+ print(" CSV not found — no data yet.")
277
+ return
278
+
279
+ articles = []
280
+ with open(CSV_PATH, "r", encoding="utf-8") as f:
281
+ reader = csv.reader(f)
282
+ next(reader, None)
283
+ for row in reader:
284
+ if row:
285
+ articles.append(row[0])
286
+
287
+ if not articles:
288
+ print(" No articles in dataset.")
289
+ return
290
+
291
+ last5 = articles[-5:]
292
+ print()
293
+ print(f" Last {len(last5)} entries (of {len(articles)} total):")
294
+ print(" " + "-" * 50)
295
+ for i, art in enumerate(last5, len(articles) - len(last5) + 1):
296
+ print(f" #{i}: {_preview_text(art, 70)}")
297
+ print()
298
+
299
+
300
+ # ── Main Loop ──
301
+
302
+ def main():
303
+ print_header()
304
+
305
+ while True:
306
+ print_menu()
307
+ try:
308
+ choice = input(" Choose an option [1-5]: ").strip()
309
+ except (EOFError, KeyboardInterrupt):
310
+ print("\n Goodbye!")
311
+ break
312
+
313
+ if choice == "1":
314
+ add_single_article()
315
+ elif choice == "2":
316
+ add_batch_articles()
317
+ elif choice == "3":
318
+ show_stats()
319
+ elif choice == "4":
320
+ show_last_entries()
321
+ elif choice == "5":
322
+ print(" Goodbye!")
323
+ break
324
+ else:
325
+ print(" Invalid choice, try again.")
326
+
327
+
328
+ if __name__ == "__main__":
329
+ main()
api/main.py CHANGED
@@ -433,6 +433,36 @@ async def delete_account(
433
 
434
 
435
  # ── Fact-check endpoint ──────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  @app.post("/api/check")
437
  async def check_article(
438
  payload: CheckRequest,
 
433
 
434
 
435
  # ── Fact-check endpoint ──────────────────────────────────────────────
436
+ class AnalyzeStructureRequest(BaseModel):
437
+ text: str
438
+
439
+
440
+ @app.post("/api/analyze-structure")
441
+ async def api_analyze_structure(payload: AnalyzeStructureRequest) -> Dict[str, Any]:
442
+ """
443
+ Exposes the structural (inverted pyramid) analysis algorithm
444
+ to the frontend Live Writing Guide without running full ML/URL checks.
445
+ """
446
+ if not payload.text or len(payload.text.strip()) < 30:
447
+ return {
448
+ "language": "unknown",
449
+ "formalism_score": 0,
450
+ "assessment": "Text too short for structure analysis.",
451
+ "recommendations": [],
452
+ }
453
+
454
+ from checker.internal.structure_analyzer import analyze_structure
455
+ try:
456
+ res = analyze_structure(payload.text)
457
+ return res
458
+ except Exception as exc:
459
+ logger.error("Error in analyze_structure: %s", exc)
460
+ raise HTTPException(
461
+ status_code=500,
462
+ detail=f"Structure analysis failed: {exc}"
463
+ )
464
+
465
+
466
  @app.post("/api/check")
467
  async def check_article(
468
  payload: CheckRequest,
backend/analyze_languages.py CHANGED
@@ -1,275 +1,275 @@
1
- """
2
- backend/analyze_languages.py
3
- ============================
4
- Analyzes the language distribution of every training dataset and prints a
5
- summary table broken down by dataset and language.
6
-
7
- Uses langdetect (already pulled in transitively; install with `pip install langdetect`
8
- if missing).
9
-
10
- Usage:
11
- python backend/analyze_languages.py
12
- """
13
-
14
- import sys
15
- import os
16
- import random
17
- from collections import Counter
18
-
19
- import pandas as pd
20
-
21
- PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
- sys.path.insert(0, PROJECT_ROOT)
23
-
24
- # ── Try importing langdetect ──────────────────────────────────────────────────
25
- try:
26
- from langdetect import detect, LangDetectException
27
- from langdetect import DetectorFactory
28
-
29
- DetectorFactory.seed = 42 # make detection deterministic
30
- HAS_LANGDETECT = True
31
- except ImportError:
32
- HAS_LANGDETECT = False
33
- print("WARNING: langdetect not installed. Run: pip install langdetect")
34
- print(" Falling back to heuristic detection only.\n")
35
-
36
-
37
- # ── Simple Tagalog heuristic (fast, used as a sanity check) ──────────────────
38
- _TAGALOG_MARKERS = {
39
- "ang",
40
- "ng",
41
- "mga",
42
- "sa",
43
- "na",
44
- "ay",
45
- "at",
46
- "hindi",
47
- "ako",
48
- "siya",
49
- "nila",
50
- "niya",
51
- "ito",
52
- "iyon",
53
- "kung",
54
- "para",
55
- "nang",
56
- "din",
57
- "rin",
58
- "kaya",
59
- "pero",
60
- "dahil",
61
- "ayon",
62
- "noon",
63
- "ngayon",
64
- "dito",
65
- "doon",
66
- "sinabi",
67
- "sinasabi",
68
- "nagpapatunay",
69
- "araw",
70
- "taon",
71
- "buwan",
72
- }
73
-
74
- _CEBUANO_MARKERS = {
75
- "ug",
76
- "nga",
77
- "ang",
78
- "sa",
79
- "si",
80
- "nag",
81
- "mao",
82
- "kang",
83
- "usab",
84
- "man",
85
- "dayon",
86
- "gyud",
87
- "kaayo",
88
- "lang",
89
- "pud",
90
- "adto",
91
- "kini",
92
- "sila",
93
- "niadtong",
94
- "gitawag",
95
- "giingon",
96
- "matud",
97
- "nasayran",
98
- "gidakop",
99
- }
100
-
101
-
102
- def _heuristic_lang(text: str) -> str:
103
- """Very rough heuristic: count Tagalog vs Cebuano marker hits."""
104
- words = set(text.lower().split())
105
- tl_hits = len(words & _TAGALOG_MARKERS)
106
- ceb_hits = len(words & _CEBUANO_MARKERS)
107
- if tl_hits == 0 and ceb_hits == 0:
108
- return "unknown"
109
- return "tl" if tl_hits >= ceb_hits else "ceb"
110
-
111
-
112
- def detect_lang(text: str) -> str:
113
- """Detect language; falls back to heuristic if langdetect fails."""
114
- if not text or not isinstance(text, str) or len(text.split()) < 5:
115
- return "unknown"
116
- if HAS_LANGDETECT:
117
- try:
118
- return detect(text[:500]) # only need a snippet
119
- except LangDetectException:
120
- pass
121
- return _heuristic_lang(text)
122
-
123
-
124
- # ── Dataset loaders (mirrors train.py logic) ─────────────────────────────────
125
-
126
-
127
- def load_datasets_raw() -> list[tuple[str, pd.DataFrame]]:
128
- """Return list of (name, df) pairs, df has columns: article, label."""
129
- result = []
130
-
131
- # 1. jcblaise/fake_news_filipino
132
- csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
133
- if os.path.exists(csv1):
134
- # The CSV has a `<<<<<<< HEAD` git conflict marker on line 1;
135
- # skiprows=1 makes pandas treat the real header (line 2) as the header.
136
- df1 = pd.read_csv(csv1, skiprows=1)
137
- # Keep only rows where both columns are valid
138
- if "article" in df1.columns and "label" in df1.columns:
139
- df1 = df1[["article", "label"]].dropna()
140
- # Drop any remaining git conflict marker rows
141
- df1 = df1[
142
- ~df1["article"].astype(str).str.startswith(("=======", ">>>>>>>"))
143
- ]
144
- result.append(("jcblaise/fake_news_filipino", df1))
145
- print(f" [1] Loaded jcblaise: {len(df1)} articles")
146
- else:
147
- print(f" [1] jcblaise not found at {csv1}, skipping.")
148
-
149
- # 2. Philippine Fake News Corpus
150
- csv2 = os.path.join(
151
- PROJECT_ROOT,
152
- "data",
153
- "raw",
154
- "philippine_corpus",
155
- "Philippine Fake News Corpus.csv",
156
- )
157
- if os.path.exists(csv2):
158
- # Same git conflict marker fix — skip line 1
159
- df2 = pd.read_csv(csv2, skiprows=1)
160
- df2 = df2.rename(columns={"Content": "article"})
161
- df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
162
- df2 = df2[["article", "label"]].dropna()
163
- df2 = df2[~df2["article"].astype(str).str.startswith(("=======", ">>>>>>>"))]
164
- result.append(("Philippine Fake News Corpus", df2))
165
- print(f" [2] Loaded Philippine Corpus: {len(df2)} articles")
166
- else:
167
- print(f" [2] Philippine Corpus not found at {csv2}, skipping.")
168
-
169
- # 3. CebuaNER — definitively Cebuano, no need to run detection on every row
170
- try:
171
- from datasets import load_dataset
172
-
173
- print(" [3] Downloading CebuaNER...")
174
- ds = load_dataset("josephimperial/CebuaNER")
175
- sentences = []
176
- for split_data in ds.values():
177
- for row in split_data:
178
- # CebuaNER schema: {'text': str} — one sentence per row
179
- text = row.get("text") or " ".join(
180
- row.get("tokens") or row.get("words") or []
181
- )
182
- if text and text.strip():
183
- sentences.append(text.strip())
184
-
185
- MIN_CHUNK = 30
186
- articles, buf, buf_tok = [], [], 0
187
- for s in sentences:
188
- buf.append(s)
189
- buf_tok += len(s.split())
190
- if buf_tok >= MIN_CHUNK:
191
- articles.append(" ".join(buf))
192
- buf, buf_tok = [], 0
193
- if buf:
194
- articles.append(" ".join(buf))
195
-
196
- df3 = pd.DataFrame({"article": articles, "label": 0})
197
- result.append(("josephimperial/CebuaNER", df3))
198
- print(f" [3] Loaded CebuaNER: {len(df3)} chunks")
199
- except ImportError:
200
- print(" [3] 'datasets' not installed, skipping CebuaNER.")
201
- except Exception as exc:
202
- print(f" [3] CebuaNER error: {exc}")
203
-
204
- return result
205
-
206
-
207
- # ── Main analysis ─────────────────────────────────────────────────────────────
208
-
209
-
210
- def analyze(sample_size: int = 500):
211
- print("=" * 60)
212
- print(" LANGUAGE DISTRIBUTION ANALYSIS")
213
- print("=" * 60)
214
- print("\nLoading datasets...\n")
215
-
216
- datasets = load_datasets_raw()
217
- if not datasets:
218
- print("No datasets found.")
219
- return
220
-
221
- grand_total = 0
222
- grand_tl = 0
223
-
224
- for name, df in datasets:
225
- total = len(df)
226
- grand_total += total
227
-
228
- # CebuaNER is definitively Cebuano — skip expensive detection
229
- if "CebuaNER" in name:
230
- lang_counts = Counter({"ceb": total})
231
- print(f"\n [{name}]")
232
- print(f" Total : {total:,}")
233
- print(
234
- f" ceb : {total:,} (100.0%) [source is Cebuano news by definition]"
235
- )
236
- continue
237
-
238
- # Sample for speed on large datasets
239
- if total > sample_size:
240
- df_sample = df.sample(n=sample_size, random_state=42)
241
- sampled = True
242
- else:
243
- df_sample = df
244
- sampled = False
245
-
246
- lang_counts: Counter = Counter()
247
- for text in df_sample["article"]:
248
- lang_counts[detect_lang(str(text))] += 1
249
-
250
- # Scale up sample counts to full dataset size
251
- if sampled:
252
- scale = total / sample_size
253
- lang_counts = Counter({k: int(v * scale) for k, v in lang_counts.items()})
254
-
255
- tl_count = lang_counts.get("tl", 0) + lang_counts.get("fil", 0)
256
- grand_tl += tl_count
257
-
258
- print(f"\n [{name}]")
259
- print(
260
- f" Total : {total:,}" + (" (estimate from sample)" if sampled else "")
261
- )
262
- for lang, cnt in lang_counts.most_common():
263
- pct = cnt / total * 100
264
- print(f" {lang:<8}: {cnt:>6,} ({pct:.1f}%)")
265
-
266
- print("\n" + "=" * 60)
267
- print(f" GRAND TOTAL articles : {grand_total:,}")
268
- print(f" Estimated Tagalog : {grand_tl:,} ({grand_tl/grand_total*100:.1f}%)")
269
- print("=" * 60)
270
- print("\nNote: 'tl'=Tagalog/Filipino, 'ceb'=Cebuano, 'en'=English")
271
- print(" langdetect may mis-classify short or code-switched texts.")
272
-
273
-
274
- if __name__ == "__main__":
275
- analyze()
 
1
+ """
2
+ backend/analyze_languages.py
3
+ ============================
4
+ Analyzes the language distribution of every training dataset and prints a
5
+ summary table broken down by dataset and language.
6
+
7
+ Uses langdetect (already pulled in transitively; install with `pip install langdetect`
8
+ if missing).
9
+
10
+ Usage:
11
+ python backend/analyze_languages.py
12
+ """
13
+
14
+ import sys
15
+ import os
16
+ import random
17
+ from collections import Counter
18
+
19
+ import pandas as pd
20
+
21
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
22
+ sys.path.insert(0, PROJECT_ROOT)
23
+
24
+ # ── Try importing langdetect ──────────────────────────────────────────────────
25
+ try:
26
+ from langdetect import detect, LangDetectException
27
+ from langdetect import DetectorFactory
28
+
29
+ DetectorFactory.seed = 42 # make detection deterministic
30
+ HAS_LANGDETECT = True
31
+ except ImportError:
32
+ HAS_LANGDETECT = False
33
+ print("WARNING: langdetect not installed. Run: pip install langdetect")
34
+ print(" Falling back to heuristic detection only.\n")
35
+
36
+
37
+ # ── Simple Tagalog heuristic (fast, used as a sanity check) ──────────────────
38
+ _TAGALOG_MARKERS = {
39
+ "ang",
40
+ "ng",
41
+ "mga",
42
+ "sa",
43
+ "na",
44
+ "ay",
45
+ "at",
46
+ "hindi",
47
+ "ako",
48
+ "siya",
49
+ "nila",
50
+ "niya",
51
+ "ito",
52
+ "iyon",
53
+ "kung",
54
+ "para",
55
+ "nang",
56
+ "din",
57
+ "rin",
58
+ "kaya",
59
+ "pero",
60
+ "dahil",
61
+ "ayon",
62
+ "noon",
63
+ "ngayon",
64
+ "dito",
65
+ "doon",
66
+ "sinabi",
67
+ "sinasabi",
68
+ "nagpapatunay",
69
+ "araw",
70
+ "taon",
71
+ "buwan",
72
+ }
73
+
74
+ _CEBUANO_MARKERS = {
75
+ "ug",
76
+ "nga",
77
+ "ang",
78
+ "sa",
79
+ "si",
80
+ "nag",
81
+ "mao",
82
+ "kang",
83
+ "usab",
84
+ "man",
85
+ "dayon",
86
+ "gyud",
87
+ "kaayo",
88
+ "lang",
89
+ "pud",
90
+ "adto",
91
+ "kini",
92
+ "sila",
93
+ "niadtong",
94
+ "gitawag",
95
+ "giingon",
96
+ "matud",
97
+ "nasayran",
98
+ "gidakop",
99
+ }
100
+
101
+
102
+ def _heuristic_lang(text: str) -> str:
103
+ """Very rough heuristic: count Tagalog vs Cebuano marker hits."""
104
+ words = set(text.lower().split())
105
+ tl_hits = len(words & _TAGALOG_MARKERS)
106
+ ceb_hits = len(words & _CEBUANO_MARKERS)
107
+ if tl_hits == 0 and ceb_hits == 0:
108
+ return "unknown"
109
+ return "tl" if tl_hits >= ceb_hits else "ceb"
110
+
111
+
112
+ def detect_lang(text: str) -> str:
113
+ """Detect language; falls back to heuristic if langdetect fails."""
114
+ if not text or not isinstance(text, str) or len(text.split()) < 5:
115
+ return "unknown"
116
+ if HAS_LANGDETECT:
117
+ try:
118
+ return detect(text[:500]) # only need a snippet
119
+ except LangDetectException:
120
+ pass
121
+ return _heuristic_lang(text)
122
+
123
+
124
+ # ── Dataset loaders (mirrors train.py logic) ─────────────────────────────────
125
+
126
+
127
+ def load_datasets_raw() -> list[tuple[str, pd.DataFrame]]:
128
+ """Return list of (name, df) pairs, df has columns: article, label."""
129
+ result = []
130
+
131
+ # 1. jcblaise/fake_news_filipino
132
+ csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
133
+ if os.path.exists(csv1):
134
+ # The CSV has a `<<<<<<< HEAD` git conflict marker on line 1;
135
+ # skiprows=1 makes pandas treat the real header (line 2) as the header.
136
+ df1 = pd.read_csv(csv1, skiprows=1)
137
+ # Keep only rows where both columns are valid
138
+ if "article" in df1.columns and "label" in df1.columns:
139
+ df1 = df1[["article", "label"]].dropna()
140
+ # Drop any remaining git conflict marker rows
141
+ df1 = df1[
142
+ ~df1["article"].astype(str).str.startswith(("=======", ">>>>>>>"))
143
+ ]
144
+ result.append(("jcblaise/fake_news_filipino", df1))
145
+ print(f" [1] Loaded jcblaise: {len(df1)} articles")
146
+ else:
147
+ print(f" [1] jcblaise not found at {csv1}, skipping.")
148
+
149
+ # 2. Philippine Fake News Corpus
150
+ csv2 = os.path.join(
151
+ PROJECT_ROOT,
152
+ "data",
153
+ "raw",
154
+ "philippine_corpus",
155
+ "Philippine Fake News Corpus.csv",
156
+ )
157
+ if os.path.exists(csv2):
158
+ # Same git conflict marker fix — skip line 1
159
+ df2 = pd.read_csv(csv2, skiprows=1)
160
+ df2 = df2.rename(columns={"Content": "article"})
161
+ df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
162
+ df2 = df2[["article", "label"]].dropna()
163
+ df2 = df2[~df2["article"].astype(str).str.startswith(("=======", ">>>>>>>"))]
164
+ result.append(("Philippine Fake News Corpus", df2))
165
+ print(f" [2] Loaded Philippine Corpus: {len(df2)} articles")
166
+ else:
167
+ print(f" [2] Philippine Corpus not found at {csv2}, skipping.")
168
+
169
+ # 3. CebuaNER — definitively Cebuano, no need to run detection on every row
170
+ try:
171
+ from datasets import load_dataset
172
+
173
+ print(" [3] Downloading CebuaNER...")
174
+ ds = load_dataset("josephimperial/CebuaNER")
175
+ sentences = []
176
+ for split_data in ds.values():
177
+ for row in split_data:
178
+ # CebuaNER schema: {'text': str} — one sentence per row
179
+ text = row.get("text") or " ".join(
180
+ row.get("tokens") or row.get("words") or []
181
+ )
182
+ if text and text.strip():
183
+ sentences.append(text.strip())
184
+
185
+ MIN_CHUNK = 30
186
+ articles, buf, buf_tok = [], [], 0
187
+ for s in sentences:
188
+ buf.append(s)
189
+ buf_tok += len(s.split())
190
+ if buf_tok >= MIN_CHUNK:
191
+ articles.append(" ".join(buf))
192
+ buf, buf_tok = [], 0
193
+ if buf:
194
+ articles.append(" ".join(buf))
195
+
196
+ df3 = pd.DataFrame({"article": articles, "label": 0})
197
+ result.append(("josephimperial/CebuaNER", df3))
198
+ print(f" [3] Loaded CebuaNER: {len(df3)} chunks")
199
+ except ImportError:
200
+ print(" [3] 'datasets' not installed, skipping CebuaNER.")
201
+ except Exception as exc:
202
+ print(f" [3] CebuaNER error: {exc}")
203
+
204
+ return result
205
+
206
+
207
+ # ── Main analysis ─────────────────────────────────────────────────────────────
208
+
209
+
210
+ def analyze(sample_size: int = 500):
211
+ print("=" * 60)
212
+ print(" LANGUAGE DISTRIBUTION ANALYSIS")
213
+ print("=" * 60)
214
+ print("\nLoading datasets...\n")
215
+
216
+ datasets = load_datasets_raw()
217
+ if not datasets:
218
+ print("No datasets found.")
219
+ return
220
+
221
+ grand_total = 0
222
+ grand_tl = 0
223
+
224
+ for name, df in datasets:
225
+ total = len(df)
226
+ grand_total += total
227
+
228
+ # CebuaNER is definitively Cebuano — skip expensive detection
229
+ if "CebuaNER" in name:
230
+ lang_counts = Counter({"ceb": total})
231
+ print(f"\n [{name}]")
232
+ print(f" Total : {total:,}")
233
+ print(
234
+ f" ceb : {total:,} (100.0%) [source is Cebuano news by definition]"
235
+ )
236
+ continue
237
+
238
+ # Sample for speed on large datasets
239
+ if total > sample_size:
240
+ df_sample = df.sample(n=sample_size, random_state=42)
241
+ sampled = True
242
+ else:
243
+ df_sample = df
244
+ sampled = False
245
+
246
+ lang_counts: Counter = Counter()
247
+ for text in df_sample["article"]:
248
+ lang_counts[detect_lang(str(text))] += 1
249
+
250
+ # Scale up sample counts to full dataset size
251
+ if sampled:
252
+ scale = total / sample_size
253
+ lang_counts = Counter({k: int(v * scale) for k, v in lang_counts.items()})
254
+
255
+ tl_count = lang_counts.get("tl", 0) + lang_counts.get("fil", 0)
256
+ grand_tl += tl_count
257
+
258
+ print(f"\n [{name}]")
259
+ print(
260
+ f" Total : {total:,}" + (" (estimate from sample)" if sampled else "")
261
+ )
262
+ for lang, cnt in lang_counts.most_common():
263
+ pct = cnt / total * 100
264
+ print(f" {lang:<8}: {cnt:>6,} ({pct:.1f}%)")
265
+
266
+ print("\n" + "=" * 60)
267
+ print(f" GRAND TOTAL articles : {grand_total:,}")
268
+ print(f" Estimated Tagalog : {grand_tl:,} ({grand_tl/grand_total*100:.1f}%)")
269
+ print("=" * 60)
270
+ print("\nNote: 'tl'=Tagalog/Filipino, 'ceb'=Cebuano, 'en'=English")
271
+ print(" langdetect may mis-classify short or code-switched texts.")
272
+
273
+
274
+ if __name__ == "__main__":
275
+ analyze()
backend/benchmark.py ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Benchmark — Comparing Your RF Model Against Classic ML Baselines
3
+ ================================================================
4
+ Trains and evaluates multiple fake-news classifiers on the SAME
5
+ locked test split so the comparison is completely fair.
6
+
7
+ Models compared
8
+ ---------------
9
+ 1. Naive Bayes (TF-IDF only) — simple baseline
10
+ 2. Logistic Regression (TF-IDF only) — strong linear baseline
11
+ 3. Linear SVM (TF-IDF only) — often best for text
12
+ 4. Random Forest (TF-IDF only) — RF without embeddings
13
+ 5. ★ YOUR MODEL ★ (TF-IDF + MiniLM + Stylo) — YOUR hybrid RF
14
+
15
+ Metrics reported (per model, per class + weighted avg)
16
+ -------------------------------------------------------
17
+ • Accuracy
18
+ • Precision (Fake class)
19
+ • Recall (Fake class)
20
+ • F1 Score (weighted)
21
+ • AUC-ROC
22
+
23
+ Output
24
+ ------
25
+ evaluation_results/benchmark_table.txt — plaintext comparison table
26
+ evaluation_results/benchmark_chart.png — bar chart (Accuracy + F1)
27
+ evaluation_results/benchmark_roc.png — ROC curves for all models
28
+
29
+ Usage
30
+ -----
31
+ python backend/benchmark.py
32
+ python backend/benchmark.py --mode tagalog # Tagalog dataset only
33
+ python backend/benchmark.py --mode cebuano # Cebuano dataset only
34
+ python backend/benchmark.py --mode mixed # All languages (default)
35
+ """
36
+
37
+ import sys
38
+ import os
39
+ import re
40
+ import time
41
+ import argparse
42
+ import warnings
43
+
44
+ warnings.filterwarnings("ignore")
45
+
46
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
47
+ sys.path.insert(0, PROJECT_ROOT)
48
+
49
+ import numpy as np
50
+ import pandas as pd
51
+
52
+ import matplotlib
53
+ matplotlib.use("Agg")
54
+ import matplotlib.pyplot as plt
55
+ import matplotlib.patches as mpatches
56
+
57
+ from scipy.sparse import hstack, csr_matrix
58
+
59
+ from sklearn.model_selection import train_test_split
60
+ from sklearn.feature_extraction.text import TfidfVectorizer
61
+ from sklearn.preprocessing import StandardScaler
62
+ from sklearn.pipeline import Pipeline
63
+
64
+ # Classifiers
65
+ from sklearn.naive_bayes import MultinomialNB
66
+ from sklearn.linear_model import LogisticRegression
67
+ from sklearn.svm import LinearSVC
68
+ from sklearn.ensemble import RandomForestClassifier
69
+ from sklearn.calibration import CalibratedClassifierCV
70
+
71
+ # Metrics
72
+ from sklearn.metrics import (
73
+ accuracy_score,
74
+ classification_report,
75
+ confusion_matrix,
76
+ roc_auc_score,
77
+ roc_curve,
78
+ f1_score,
79
+ precision_score,
80
+ recall_score,
81
+ )
82
+
83
+ # Re-use helpers from train.py (keep feature extraction identical)
84
+ from backend.train import (
85
+ load_fake_news_dataset,
86
+ preprocess,
87
+ clean_text,
88
+ extract_stylometric_features,
89
+ get_minilm_model,
90
+ STYLOMETRIC_FEATURE_NAMES,
91
+ )
92
+
93
+ # ── Paths ──────────────────────────────────────────────────────────────────
94
+ OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
95
+
96
+
97
+ # ───────────────────────────────────────────────────────────────────────────
98
+ # Feature builders
99
+ # ───────────────────────────────────────────────────────────────────────────
100
+
101
+
102
+ def build_tfidf_features(X_train, X_test):
103
+ """Plain TF-IDF (used by all baseline models)."""
104
+ tfidf = TfidfVectorizer(
105
+ max_features=15_000,
106
+ ngram_range=(1, 3),
107
+ min_df=2,
108
+ max_df=0.95,
109
+ sublinear_tf=True,
110
+ )
111
+ X_tr = tfidf.fit_transform(X_train)
112
+ X_te = tfidf.transform(X_test)
113
+ return X_tr, X_te, tfidf
114
+
115
+
116
+ def build_hybrid_features(X_train, X_test):
117
+ """TF-IDF + MiniLM embeddings + stylometric (your full pipeline)."""
118
+ print(" [hybrid] Fitting TF-IDF …")
119
+ tfidf = TfidfVectorizer(
120
+ max_features=15_000,
121
+ ngram_range=(1, 3),
122
+ min_df=2,
123
+ max_df=0.95,
124
+ sublinear_tf=True,
125
+ )
126
+ X_tr_tfidf = tfidf.fit_transform(X_train)
127
+ X_te_tfidf = tfidf.transform(X_test)
128
+
129
+ print(" [hybrid] Encoding with MiniLM …")
130
+ minilm = get_minilm_model()
131
+ emb_train = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
132
+ emb_test = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
133
+
134
+ print(f" [hybrid] Extracting {len(STYLOMETRIC_FEATURE_NAMES)} stylometric features …")
135
+ stylo_train = np.array([extract_stylometric_features(t) for t in X_train])
136
+ stylo_test = np.array([extract_stylometric_features(t) for t in X_test])
137
+
138
+ scaler = StandardScaler()
139
+ stylo_train_sc = scaler.fit_transform(stylo_train)
140
+ stylo_test_sc = scaler.transform(stylo_test)
141
+
142
+ X_tr = hstack([X_tr_tfidf, csr_matrix(emb_train), csr_matrix(stylo_train_sc)])
143
+ X_te = hstack([X_te_tfidf, csr_matrix(emb_test), csr_matrix(stylo_test_sc)])
144
+
145
+ n_total = X_tr.shape[1]
146
+ print(
147
+ f" [hybrid] Feature dimensions: {n_total} "
148
+ f"(TF-IDF: {X_tr_tfidf.shape[1]} + MiniLM: 384 + Stylo: {len(STYLOMETRIC_FEATURE_NAMES)})"
149
+ )
150
+ return X_tr, X_te
151
+
152
+
153
+ # ───────────────────────────────────────────────────────────────────────────
154
+ # Compute metrics for one model
155
+ # ───────────────────────────────────────────────────────────────────────────
156
+
157
+
158
+ def evaluate(name, model, X_test, y_test, proba=None):
159
+ """Return a metrics dict for one fitted model."""
160
+ y_pred = model.predict(X_test)
161
+
162
+ acc = accuracy_score(y_test, y_pred)
163
+ prec_fake = precision_score(y_test, y_pred, pos_label=1, zero_division=0)
164
+ rec_fake = recall_score(y_test, y_pred, pos_label=1, zero_division=0)
165
+ f1_weighted = f1_score(y_test, y_pred, average="weighted", zero_division=0)
166
+ f1_fake = f1_score(y_test, y_pred, pos_label=1, zero_division=0)
167
+
168
+ # AUC-ROC (needs probability scores)
169
+ if proba is not None:
170
+ try:
171
+ auc = roc_auc_score(y_test, proba[:, 1])
172
+ except Exception:
173
+ auc = float("nan")
174
+ else:
175
+ auc = float("nan")
176
+
177
+ report = classification_report(
178
+ y_test, y_pred,
179
+ target_names=["Real", "Fake"],
180
+ zero_division=0,
181
+ )
182
+
183
+ return {
184
+ "name": name,
185
+ "accuracy": acc,
186
+ "precision": prec_fake,
187
+ "recall": rec_fake,
188
+ "f1_weighted": f1_weighted,
189
+ "f1_fake": f1_fake,
190
+ "auc": auc,
191
+ "report": report,
192
+ "y_pred": y_pred,
193
+ "proba": proba,
194
+ }
195
+
196
+
197
+ # ───────────────────────────────────────────────────────────────────────────
198
+ # Bar chart
199
+ # ───────────────────────────────────────────────────────────────────────────
200
+
201
+
202
+ def plot_bar_chart(results, output_path):
203
+ """Side-by-side bar chart: Accuracy vs F1 (weighted) vs AUC-ROC."""
204
+ names = [r["name"] for r in results]
205
+ accs = [r["accuracy"] for r in results]
206
+ f1s = [r["f1_weighted"] for r in results]
207
+ aucs = [r["auc"] for r in results]
208
+
209
+ x = np.arange(len(names))
210
+ width = 0.26
211
+
212
+ fig, ax = plt.subplots(figsize=(max(10, len(names) * 2.2), 6))
213
+
214
+ # Color highlight for YOUR model (last entry)
215
+ bar_colors_acc = ["#2196F3"] * (len(names) - 1) + ["#E91E63"]
216
+ bar_colors_f1 = ["#4CAF50"] * (len(names) - 1) + ["#FF5722"]
217
+ bar_colors_auc = ["#9C27B0"] * (len(names) - 1) + ["#FF9800"]
218
+
219
+ b1 = ax.bar(x - width, accs, width, color=bar_colors_acc, edgecolor="black", lw=0.5, label="Accuracy")
220
+ b2 = ax.bar(x, f1s, width, color=bar_colors_f1, edgecolor="black", lw=0.5, label="F1 Weighted")
221
+ b3 = ax.bar(x + width, aucs, width, color=bar_colors_auc, edgecolor="black", lw=0.5, label="AUC-ROC")
222
+
223
+ # Value labels
224
+ for bars in (b1, b2, b3):
225
+ for bar in bars:
226
+ h = bar.get_height()
227
+ if not np.isnan(h):
228
+ ax.text(
229
+ bar.get_x() + bar.get_width() / 2,
230
+ h + 0.005,
231
+ f"{h:.3f}",
232
+ ha="center", va="bottom", fontsize=7.5, fontweight="bold",
233
+ )
234
+
235
+ ax.set_xticks(x)
236
+ ax.set_xticklabels(names, rotation=12, ha="right", fontsize=10)
237
+ ax.set_ylim(0, 1.12)
238
+ ax.set_ylabel("Score", fontsize=12)
239
+ ax.set_title(
240
+ "Benchmark: Your RF Model vs. Classic ML Baselines\n"
241
+ "(Highlighted in pink/orange = Your Model)",
242
+ fontsize=13, fontweight="bold",
243
+ )
244
+ ax.axhline(y=0.80, color="gray", linestyle="--", alpha=0.4, linewidth=1)
245
+ ax.text(len(names) - 0.5, 0.805, "80% threshold", color="gray", fontsize=8)
246
+
247
+ patch_yours = mpatches.Patch(color="#E91E63", label="★ Your Hybrid RF (Accuracy)")
248
+ ax.legend(handles=[*ax.get_legend_handles_labels()[0][:3], patch_yours], fontsize=9)
249
+
250
+ plt.tight_layout()
251
+ fig.savefig(output_path, dpi=150, bbox_inches="tight")
252
+ plt.close(fig)
253
+ print(f" Saved: {output_path}")
254
+
255
+
256
+ # ───────────────────────────────────────────────────────────────────────────
257
+ # ROC curve chart
258
+ # ───────────────────────────────────────────────────────────────────────────
259
+
260
+
261
+ def plot_roc_curves(results, y_test, output_path):
262
+ """Overlay ROC curves for all models that have probability scores."""
263
+ fig, ax = plt.subplots(figsize=(8, 6))
264
+
265
+ COLORS = [
266
+ "#2196F3", "#4CAF50", "#FF9800", "#9C27B0",
267
+ "#E91E63", "#00BCD4", "#F44336", "#8BC34A",
268
+ ]
269
+
270
+ for i, r in enumerate(results):
271
+ if r["proba"] is None or np.isnan(r["auc"]):
272
+ continue
273
+ fpr, tpr, _ = roc_curve(y_test, r["proba"][:, 1], pos_label=1)
274
+ lw = 2.5 if "★" in r["name"] else 1.5
275
+ dash = "-" if "★" in r["name"] else "--"
276
+ ax.plot(
277
+ fpr, tpr,
278
+ color=COLORS[i % len(COLORS)],
279
+ lw=lw, linestyle=dash,
280
+ label=f"{r['name']} (AUC={r['auc']:.3f})",
281
+ )
282
+
283
+ ax.plot([0, 1], [0, 1], "k:", lw=1, label="Random (AUC=0.500)")
284
+ ax.set_xlim([0.0, 1.0])
285
+ ax.set_ylim([0.0, 1.05])
286
+ ax.set_xlabel("False Positive Rate", fontsize=12)
287
+ ax.set_ylabel("True Positive Rate", fontsize=12)
288
+ ax.set_title("ROC Curves — Fake-News Detection Benchmark", fontsize=13, fontweight="bold")
289
+ ax.legend(loc="lower right", fontsize=9)
290
+ ax.grid(alpha=0.3)
291
+
292
+ plt.tight_layout()
293
+ fig.savefig(output_path, dpi=150, bbox_inches="tight")
294
+ plt.close(fig)
295
+ print(f" Saved: {output_path}")
296
+
297
+
298
+ # ───────────────────────────────────────────────────────────────────────────
299
+ # Plaintext summary table
300
+ # ───────────────────────────────────────────────────────────────────────────
301
+
302
+
303
+ def save_table(results, output_path, mode, n_train, n_test):
304
+ """Write a neatly formatted comparison table to disk and stdout."""
305
+ lines = []
306
+ sep = "=" * 90
307
+
308
+ lines.append(sep)
309
+ lines.append(" BENCHMARK RESULTS — Fake-News Detection (Filipino / Cebuano)")
310
+ lines.append(f" Mode: {mode.upper()} | Train: {n_train:,} samples | Test: {n_test:,} samples")
311
+ lines.append(sep)
312
+ lines.append("")
313
+
314
+ header = (
315
+ f" {'Model':<35} {'Accuracy':>9} {'Prec(Fk)':>10} {'Rec(Fk)':>9} "
316
+ f"{'F1(Wtd)':>9} {'F1(Fk)':>8} {'AUC-ROC':>9}"
317
+ )
318
+ lines.append(header)
319
+ lines.append(" " + "-" * 88)
320
+
321
+ for r in results:
322
+ auc_str = f"{r['auc']:.4f}" if not np.isnan(r["auc"]) else " N/A "
323
+ marker = " ★" if "★" in r["name"] else " "
324
+ row = (
325
+ f"{marker} {r['name']:<32} "
326
+ f"{r['accuracy']:>9.4f} "
327
+ f"{r['precision']:>10.4f} "
328
+ f"{r['recall']:>9.4f} "
329
+ f"{r['f1_weighted']:>9.4f} "
330
+ f"{r['f1_fake']:>8.4f} "
331
+ f"{auc_str:>9}"
332
+ )
333
+ lines.append(row)
334
+
335
+ lines.append("")
336
+ lines.append(sep)
337
+ lines.append(" DETAILED CLASSIFICATION REPORTS")
338
+ lines.append(sep)
339
+
340
+ for r in results:
341
+ lines.append("")
342
+ lines.append(f" ── {r['name']} ──────────────────────────────────────────────")
343
+ for ln in r["report"].splitlines():
344
+ lines.append(f" {ln}")
345
+
346
+ text = "\n".join(lines)
347
+ print(text)
348
+
349
+ with open(output_path, "w", encoding="utf-8") as f:
350
+ f.write(text)
351
+ print(f"\n Saved: {output_path}")
352
+
353
+
354
+ # ───────────────────────────────────────────────────────────────────────────
355
+ # Main
356
+ # ───────────────────────────────────────────────────────────────────────────
357
+
358
+
359
+ def main():
360
+ parser = argparse.ArgumentParser(description="Benchmark fake-news classifiers.")
361
+ parser.add_argument(
362
+ "--mode",
363
+ choices=["mixed", "tagalog", "cebuano"],
364
+ default="mixed",
365
+ help="Which language subset to benchmark on (default: mixed).",
366
+ )
367
+ parser.add_argument(
368
+ "--test-size",
369
+ type=float,
370
+ default=0.20,
371
+ help="Fraction of data to hold out as the locked test set (default: 0.20).",
372
+ )
373
+ parser.add_argument(
374
+ "--skip-minilm",
375
+ action="store_true",
376
+ default=False,
377
+ help="Skip your hybrid RF model (useful if MiniLM is too slow for a quick check).",
378
+ )
379
+ args = parser.parse_args()
380
+
381
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
382
+
383
+ print("=" * 60)
384
+ print(" FAKE-NEWS BENCHMARK")
385
+ print(f" Mode: {args.mode.upper()} | Test size: {args.test_size:.0%}")
386
+ print("=" * 60)
387
+
388
+ # ── 1. Load & preprocess dataset ─────────────────────────────────────
389
+ tagalog_only = args.mode == "tagalog"
390
+ cebuano_only = args.mode == "cebuano"
391
+
392
+ df = load_fake_news_dataset(tagalog_only=tagalog_only, cebuano_only=cebuano_only)
393
+ X_all, y_all = preprocess(df, undersample=False, oversample=True)
394
+
395
+ # ── 2. Locked test split (same seed → reproducible) ──────────────────
396
+ print(f"\nCreating locked test split ({1 - args.test_size:.0%} train / {args.test_size:.0%} test) …")
397
+ X_train, X_test, y_train, y_test = train_test_split(
398
+ X_all, y_all,
399
+ test_size=args.test_size,
400
+ random_state=42,
401
+ stratify=y_all,
402
+ )
403
+ print(f" Train: {len(X_train):,} | Test: {len(X_test):,}")
404
+ print(f" Test distribution — Real: {y_test.count(0):,}, Fake: {y_test.count(1):,}")
405
+
406
+ y_train_arr = np.array(y_train)
407
+ y_test_arr = np.array(y_test)
408
+
409
+ # ── 3. Build TF-IDF features (shared by baseline models) ─────────────
410
+ print("\nBuilding TF-IDF features for baseline models …")
411
+ X_tr_tfidf, X_te_tfidf, tfidf = build_tfidf_features(X_train, X_test)
412
+ print(f" TF-IDF shape: {X_tr_tfidf.shape}")
413
+
414
+ # ── 4. Train and evaluate each model ─────────────────────────────────
415
+ results = []
416
+
417
+ # ── 4a. Naive Bayes (TF-IDF, no negative values — shift by min) ──────
418
+ print("\n[1/5] Naive Bayes (TF-IDF) …")
419
+ t0 = time.time()
420
+ nb = MultinomialNB(alpha=1.0)
421
+ # MultinomialNB needs non-negative input; TF-IDF with sublinear_tf is ok
422
+ nb.fit(X_tr_tfidf, y_train_arr)
423
+ nb_proba = nb.predict_proba(X_te_tfidf)
424
+ elapsed = time.time() - t0
425
+ res_nb = evaluate("Naive Bayes (TF-IDF)", nb, X_te_tfidf, y_test_arr, nb_proba)
426
+ res_nb["train_time"] = elapsed
427
+ results.append(res_nb)
428
+ print(f" Accuracy: {res_nb['accuracy']:.4f} | F1 Weighted: {res_nb['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
429
+
430
+ # ── 4b. Logistic Regression (TF-IDF) ─────────────────────────────────
431
+ print("\n[2/5] Logistic Regression (TF-IDF) …")
432
+ t0 = time.time()
433
+ lr = LogisticRegression(
434
+ max_iter=1000,
435
+ class_weight="balanced",
436
+ solver="saga",
437
+ C=1.0,
438
+ random_state=42,
439
+ n_jobs=-1,
440
+ )
441
+ lr.fit(X_tr_tfidf, y_train_arr)
442
+ lr_proba = lr.predict_proba(X_te_tfidf)
443
+ elapsed = time.time() - t0
444
+ res_lr = evaluate("Logistic Regression (TF-IDF)", lr, X_te_tfidf, y_test_arr, lr_proba)
445
+ res_lr["train_time"] = elapsed
446
+ results.append(res_lr)
447
+ print(f" Accuracy: {res_lr['accuracy']:.4f} | F1 Weighted: {res_lr['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
448
+
449
+ # ── 4c. Linear SVM (TF-IDF) ──────────────────────────────────────────
450
+ print("\n[3/5] Linear SVM (TF-IDF) …")
451
+ t0 = time.time()
452
+ svm = CalibratedClassifierCV(
453
+ LinearSVC(class_weight="balanced", max_iter=2000, random_state=42),
454
+ cv=3,
455
+ )
456
+ svm.fit(X_tr_tfidf, y_train_arr)
457
+ svm_proba = svm.predict_proba(X_te_tfidf)
458
+ elapsed = time.time() - t0
459
+ res_svm = evaluate("Linear SVM (TF-IDF)", svm, X_te_tfidf, y_test_arr, svm_proba)
460
+ res_svm["train_time"] = elapsed
461
+ results.append(res_svm)
462
+ print(f" Accuracy: {res_svm['accuracy']:.4f} | F1 Weighted: {res_svm['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
463
+
464
+ # ── 4d. Random Forest (TF-IDF only — no embeddings) ──────────────────
465
+ print("\n[4/5] Random Forest (TF-IDF only — no embeddings) …")
466
+ t0 = time.time()
467
+ rf_tf = RandomForestClassifier(
468
+ n_estimators=300,
469
+ max_depth=20,
470
+ min_samples_split=5,
471
+ min_samples_leaf=3,
472
+ class_weight="balanced",
473
+ n_jobs=-1,
474
+ random_state=42,
475
+ )
476
+ rf_tf.fit(X_tr_tfidf, y_train_arr)
477
+ rf_tf_proba = rf_tf.predict_proba(X_te_tfidf)
478
+ elapsed = time.time() - t0
479
+ res_rf_tf = evaluate("Random Forest (TF-IDF only)", rf_tf, X_te_tfidf, y_test_arr, rf_tf_proba)
480
+ res_rf_tf["train_time"] = elapsed
481
+ results.append(res_rf_tf)
482
+ print(f" Accuracy: {res_rf_tf['accuracy']:.4f} | F1 Weighted: {res_rf_tf['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
483
+
484
+ # ── 4e. YOUR Hybrid RF (TF-IDF + MiniLM + Stylometric) ───────────────
485
+ if not args.skip_minilm:
486
+ print("\n[5/5] ★ YOUR Hybrid RF (TF-IDF + MiniLM + Stylometric) …")
487
+ X_tr_hy, X_te_hy = build_hybrid_features(X_train, X_test)
488
+
489
+ t0 = time.time()
490
+ max_depth = 8 if cebuano_only else 20
491
+ rf_hy = RandomForestClassifier(
492
+ n_estimators=500,
493
+ max_depth=max_depth,
494
+ min_samples_split=5,
495
+ min_samples_leaf=3,
496
+ class_weight="balanced",
497
+ n_jobs=-1,
498
+ random_state=42,
499
+ )
500
+ rf_hy.fit(X_tr_hy, y_train_arr)
501
+ rf_hy_proba = rf_hy.predict_proba(X_te_hy)
502
+ elapsed = time.time() - t0
503
+ res_rf_hy = evaluate("★ Hybrid RF (TF-IDF + MiniLM + Stylo)", rf_hy, X_te_hy, y_test_arr, rf_hy_proba)
504
+ res_rf_hy["train_time"] = elapsed
505
+ results.append(res_rf_hy)
506
+ print(f" Accuracy: {res_rf_hy['accuracy']:.4f} | F1 Weighted: {res_rf_hy['f1_weighted']:.4f} | Time: {elapsed:.1f}s")
507
+ else:
508
+ print("\n[5/5] Skipping Hybrid RF (--skip-minilm flag set).")
509
+
510
+ # ── 5. Output table ───────────────────────────────────────────────────
511
+ print("\n" + "=" * 60)
512
+ print(" BENCHMARK SUMMARY")
513
+ print("=" * 60)
514
+ table_path = os.path.join(OUTPUT_DIR, f"benchmark_table_{args.mode}.txt")
515
+ save_table(results, table_path, args.mode, len(X_train), len(X_test))
516
+
517
+ # ── 6. Bar chart ──────────────────────────────────────────────────────
518
+ chart_path = os.path.join(OUTPUT_DIR, f"benchmark_chart_{args.mode}.png")
519
+ plot_bar_chart(results, chart_path)
520
+
521
+ # ── 7. ROC curves ─────────────────────────────────────────────────────
522
+ roc_path = os.path.join(OUTPUT_DIR, f"benchmark_roc_{args.mode}.png")
523
+ plot_roc_curves(results, y_test_arr, roc_path)
524
+
525
+ # ── 8. Train time summary ─────────────────────────────────────────────
526
+ print("\n Training times:")
527
+ for r in results:
528
+ t = r.get("train_time", 0)
529
+ print(f" {r['name']:<45} {t:>6.1f}s")
530
+
531
+ print("\n" + "=" * 60)
532
+ print(" BENCHMARK COMPLETE")
533
+ print(f" Results saved to: {OUTPUT_DIR}")
534
+ print("=" * 60)
535
+
536
+
537
+ if __name__ == "__main__":
538
+ main()
backend/bias_words_report.txt CHANGED
@@ -1,148 +1,148 @@
1
- =================================================================
2
- CHI-SQUARED BIAS WORD MINER
3
- Finds words most associated with Fake vs Real articles
4
- =================================================================
5
-
6
- Loaded 25664 articles
7
- Real: 16405 | Fake: 9259
8
-
9
- Building vocabulary from 25664 articles...
10
- Running chi-squared analysis...
11
-
12
- =================================================================
13
- TOP 60 WORDS ASSOCIATED WITH *** FAKE NEWS ***
14
- (Consider adding to SENSATIONAL_KEYWORDS or RIGHT/LEFT lists)
15
- =================================================================
16
- ang chi2= 11772.6 fake_freq=2.1795 real_freq=0.6382
17
- mga chi2= 7393.4 fake_freq=1.0763 real_freq=0.2492
18
- miss chi2= 2917.1 fake_freq=0.2418 real_freq=0.0199
19
- hindi chi2= 2627.4 fake_freq=0.3756 real_freq=0.0852
20
- daw chi2= 2581.2 fake_freq=0.1800 real_freq=0.0065
21
- adobo chi2= 2540.9 fake_freq=0.1594 real_freq=0.0012
22
- lang chi2= 2520.2 fake_freq=0.2660 real_freq=0.0391
23
- you chi2= 2501.5 fake_freq=1.1692 real_freq=0.5885
24
- adobo chronicles chi2= 2490.9 fake_freq=0.1521 real_freq=0.0001
25
- chronicles chi2= 2472.7 fake_freq=0.1537 real_freq=0.0008
26
- the adobo chi2= 2443.3 fake_freq=0.1489 real_freq=0.0000
27
- sa mga chi2= 2399.6 fake_freq=0.2914 real_freq=0.0536
28
- ito chi2= 2374.0 fake_freq=0.3101 real_freq=0.0630
29
- rappler chi2= 2298.3 fake_freq=0.1879 real_freq=0.0148
30
- sabi chi2= 2157.7 fake_freq=0.1847 real_freq=0.0168
31
- media chi2= 2148.0 fake_freq=0.4887 real_freq=0.1679
32
- kung chi2= 1897.0 fake_freq=0.2617 real_freq=0.0569
33
- video chi2= 1894.0 fake_freq=0.2651 real_freq=0.0587
34
- sabi ni chi2= 1842.0 fake_freq=0.1377 real_freq=0.0075
35
- kay chi2= 1793.6 fake_freq=0.2103 real_freq=0.0368
36
- naman chi2= 1775.7 fake_freq=0.2518 real_freq=0.0566
37
- facebook chi2= 1758.7 fake_freq=0.2292 real_freq=0.0464
38
- netizens chi2= 1748.0 fake_freq=0.1363 real_freq=0.0090
39
- ang mga chi2= 1728.9 fake_freq=0.2514 real_freq=0.0582
40
- pageant chi2= 1715.6 fake_freq=0.1253 real_freq=0.0060
41
- your chi2= 1701.4 fake_freq=0.3996 real_freq=0.1403
42
- ng mga chi2= 1582.1 fake_freq=0.2811 real_freq=0.0794
43
- yung chi2= 1508.4 fake_freq=0.1792 real_freq=0.0319
44
- social media chi2= 1503.9 fake_freq=0.2093 real_freq=0.0460
45
- umano chi2= 1478.0 fake_freq=0.1161 real_freq=0.0079
46
- kayo chi2= 1398.4 fake_freq=0.1054 real_freq=0.0060
47
- leni chi2= 1397.2 fake_freq=0.1806 real_freq=0.0362
48
- ako chi2= 1364.9 fake_freq=0.1684 real_freq=0.0317
49
- comment chi2= 1353.9 fake_freq=0.2057 real_freq=0.0499
50
- para chi2= 1319.4 fake_freq=0.2437 real_freq=0.0712
51
- pangulong chi2= 1288.2 fake_freq=0.0925 real_freq=0.0040
52
- catriona chi2= 1285.2 fake_freq=0.0797 real_freq=0.0004
53
- thinkingpinoy chi2= 1268.6 fake_freq=0.0773 real_freq=0.0000
54
- blog chi2= 1258.3 fake_freq=0.0961 real_freq=0.0058
55
- dahil chi2= 1213.0 fake_freq=0.1816 real_freq=0.0433
56
- isang chi2= 1210.0 fake_freq=0.2125 real_freq=0.0594
57
- ayon chi2= 1204.6 fake_freq=0.1481 real_freq=0.0277
58
- ilang chi2= 1176.7 fake_freq=0.1077 real_freq=0.0117
59
- noynoy chi2= 1121.0 fake_freq=0.0794 real_freq=0.0032
60
- ressa chi2= 1086.1 fake_freq=0.0760 real_freq=0.0028
61
- com chi2= 1082.5 fake_freq=0.1124 real_freq=0.0160
62
- din chi2= 1081.8 fake_freq=0.1438 real_freq=0.0299
63
- bs aquino chi2= 1062.9 fake_freq=0.0650 real_freq=0.0001
64
- natin chi2= 1035.5 fake_freq=0.1229 real_freq=0.0219
65
- niya chi2= 1032.8 fake_freq=0.1660 real_freq=0.0427
66
- http chi2= 1029.2 fake_freq=0.0703 real_freq=0.0021
67
- grp chi2= 1010.0 fake_freq=0.0731 real_freq=0.0034
68
- naging chi2= 993.2 fake_freq=0.0958 real_freq=0.0118
69
- featured comment chi2= 990.4 fake_freq=0.0604 real_freq=0.0000
70
- is grp chi2= 988.7 fake_freq=0.0603 real_freq=0.0000
71
- grp featured chi2= 983.3 fake_freq=0.0599 real_freq=0.0000
72
- like chi2= 983.3 fake_freq=0.5211 real_freq=0.2752
73
- news chi2= 980.1 fake_freq=0.3171 real_freq=0.1348
74
- catriona gray chi2= 979.6 fake_freq=0.0599 real_freq=0.0001
75
- yan chi2= 971.2 fake_freq=0.0989 real_freq=0.0136
76
-
77
- =================================================================
78
- TOP 60 WORDS ASSOCIATED WITH *** REAL NEWS ***
79
- (High-credibility markers ù useful context)
80
- =================================================================
81
- said chi2= 30152.0 real_freq=5.3763 fake_freq=0.9817
82
- said the chi2= 7892.5 real_freq=1.0162 fake_freq=0.0680
83
- he said chi2= 7048.7 real_freq=1.0146 fake_freq=0.1094
84
- of the chi2= 6391.5 real_freq=4.0884 fake_freq=2.1730
85
- was chi2= 6127.3 real_freq=3.1958 fake_freq=1.5541
86
- had chi2= 4985.5 real_freq=1.2013 fake_freq=0.3359
87
- were chi2= 4869.7 real_freq=1.4602 fake_freq=0.5025
88
- for chi2= 3693.8 real_freq=3.9857 fake_freq=2.5171
89
- percent chi2= 3316.0 real_freq=0.4733 fake_freq=0.0496
90
- said he chi2= 3054.9 real_freq=0.4525 fake_freq=0.0535
91
- city chi2= 3015.1 real_freq=0.9589 fake_freq=0.3458
92
- police chi2= 2951.7 real_freq=0.6889 fake_freq=0.1859
93
- mr duterte chi2= 2937.8 real_freq=0.3344 fake_freq=0.0068
94
- would chi2= 2682.3 real_freq=1.1915 fake_freq=0.5339
95
- added chi2= 2527.3 real_freq=0.4712 fake_freq=0.0931
96
- in the chi2= 2323.8 real_freq=2.2774 fake_freq=1.3999
97
- government chi2= 2221.8 real_freq=0.9786 fake_freq=0.4364
98
- from chi2= 2142.5 real_freq=1.9267 fake_freq=1.1543
99
- court chi2= 2128.4 real_freq=0.5376 fake_freq=0.1580
100
- said in chi2= 1914.3 real_freq=0.3323 fake_freq=0.0577
101
- province chi2= 1836.7 real_freq=0.2501 fake_freq=0.0219
102
- year chi2= 1725.1 real_freq=0.6088 fake_freq=0.2369
103
- she said chi2= 1703.1 real_freq=0.2836 fake_freq=0.0451
104
- sen chi2= 1646.4 real_freq=0.2895 fake_freq=0.0515
105
- the department chi2= 1624.6 real_freq=0.2532 fake_freq=0.0345
106
- also chi2= 1619.7 real_freq=1.1369 fake_freq=0.6263
107
- for the chi2= 1617.7 real_freq=0.9776 fake_freq=0.5076
108
- rep chi2= 1561.8 real_freq=0.2070 fake_freq=0.0161
109
- the president chi2= 1555.6 real_freq=0.5825 fake_freq=0.2358
110
- to the chi2= 1533.1 real_freq=1.4158 fake_freq=0.8552
111
- department chi2= 1480.6 real_freq=0.3825 fake_freq=0.1150
112
- he added chi2= 1479.9 real_freq=0.2401 fake_freq=0.0361
113
- office chi2= 1426.7 real_freq=0.4416 fake_freq=0.1560
114
- friday chi2= 1420.6 real_freq=0.2201 fake_freq=0.0295
115
- with chi2= 1404.1 real_freq=2.2094 fake_freq=1.5269
116
- officials chi2= 1398.2 real_freq=0.3345 fake_freq=0.0928
117
- military chi2= 1392.7 real_freq=0.3024 fake_freq=0.0747
118
- monday chi2= 1364.4 real_freq=0.2359 fake_freq=0.0406
119
- tuesday chi2= 1357.9 real_freq=0.2077 fake_freq=0.0269
120
- house chi2= 1340.9 real_freq=0.3684 fake_freq=0.1175
121
- chief chi2= 1329.3 real_freq=0.4226 fake_freq=0.1524
122
- committee chi2= 1317.5 real_freq=0.2536 fake_freq=0.0528
123
- the government chi2= 1310.3 real_freq=0.3822 fake_freq=0.1285
124
- the house chi2= 1290.2 real_freq=0.2091 fake_freq=0.0313
125
- had been chi2= 1277.4 real_freq=0.1926 fake_freq=0.0240
126
- on friday chi2= 1269.4 real_freq=0.1663 fake_freq=0.0122
127
- thursday chi2= 1260.2 real_freq=0.1889 fake_freq=0.0231
128
- peace chi2= 1233.9 real_freq=0.2489 fake_freq=0.0556
129
- million chi2= 1231.2 real_freq=0.4199 fake_freq=0.1595
130
- and the chi2= 1210.5 real_freq=0.7438 fake_freq=0.3889
131
- at the chi2= 1192.8 real_freq=0.6397 fake_freq=0.3152
132
- wednesday chi2= 1176.4 real_freq=0.1791 fake_freq=0.0229
133
- department of chi2= 1141.5 real_freq=0.2878 fake_freq=0.0845
134
- national chi2= 1125.5 real_freq=0.5410 fake_freq=0.2527
135
- on monday chi2= 1123.8 real_freq=0.1721 fake_freq=0.0224
136
- under chi2= 1114.4 real_freq=0.4066 fake_freq=0.1618
137
- region chi2= 1113.0 real_freq=0.1859 fake_freq=0.0298
138
- town chi2= 1102.8 real_freq=0.1554 fake_freq=0.0156
139
- on tuesday chi2= 1101.4 real_freq=0.1529 fake_freq=0.0145
140
- by the chi2= 1079.0 real_freq=0.7163 fake_freq=0.3863
141
-
142
- =================================================================
143
- HOW TO USE THESE RESULTS:
144
- 1. Review the FAKE words list above
145
- 2. Add clearly sensational/biased words to SENSATIONAL_KEYWORDS
146
- in checker/internal/bias_analyzer.py
147
- 3. Add political framing words to RIGHT/LEFT_LEANING_KEYWORDS
148
- =================================================================
 
1
+ =================================================================
2
+ CHI-SQUARED BIAS WORD MINER
3
+ Finds words most associated with Fake vs Real articles
4
+ =================================================================
5
+
6
+ Loaded 25664 articles
7
+ Real: 16405 | Fake: 9259
8
+
9
+ Building vocabulary from 25664 articles...
10
+ Running chi-squared analysis...
11
+
12
+ =================================================================
13
+ TOP 60 WORDS ASSOCIATED WITH *** FAKE NEWS ***
14
+ (Consider adding to SENSATIONAL_KEYWORDS or RIGHT/LEFT lists)
15
+ =================================================================
16
+ ang chi2= 11772.6 fake_freq=2.1795 real_freq=0.6382
17
+ mga chi2= 7393.4 fake_freq=1.0763 real_freq=0.2492
18
+ miss chi2= 2917.1 fake_freq=0.2418 real_freq=0.0199
19
+ hindi chi2= 2627.4 fake_freq=0.3756 real_freq=0.0852
20
+ daw chi2= 2581.2 fake_freq=0.1800 real_freq=0.0065
21
+ adobo chi2= 2540.9 fake_freq=0.1594 real_freq=0.0012
22
+ lang chi2= 2520.2 fake_freq=0.2660 real_freq=0.0391
23
+ you chi2= 2501.5 fake_freq=1.1692 real_freq=0.5885
24
+ adobo chronicles chi2= 2490.9 fake_freq=0.1521 real_freq=0.0001
25
+ chronicles chi2= 2472.7 fake_freq=0.1537 real_freq=0.0008
26
+ the adobo chi2= 2443.3 fake_freq=0.1489 real_freq=0.0000
27
+ sa mga chi2= 2399.6 fake_freq=0.2914 real_freq=0.0536
28
+ ito chi2= 2374.0 fake_freq=0.3101 real_freq=0.0630
29
+ rappler chi2= 2298.3 fake_freq=0.1879 real_freq=0.0148
30
+ sabi chi2= 2157.7 fake_freq=0.1847 real_freq=0.0168
31
+ media chi2= 2148.0 fake_freq=0.4887 real_freq=0.1679
32
+ kung chi2= 1897.0 fake_freq=0.2617 real_freq=0.0569
33
+ video chi2= 1894.0 fake_freq=0.2651 real_freq=0.0587
34
+ sabi ni chi2= 1842.0 fake_freq=0.1377 real_freq=0.0075
35
+ kay chi2= 1793.6 fake_freq=0.2103 real_freq=0.0368
36
+ naman chi2= 1775.7 fake_freq=0.2518 real_freq=0.0566
37
+ facebook chi2= 1758.7 fake_freq=0.2292 real_freq=0.0464
38
+ netizens chi2= 1748.0 fake_freq=0.1363 real_freq=0.0090
39
+ ang mga chi2= 1728.9 fake_freq=0.2514 real_freq=0.0582
40
+ pageant chi2= 1715.6 fake_freq=0.1253 real_freq=0.0060
41
+ your chi2= 1701.4 fake_freq=0.3996 real_freq=0.1403
42
+ ng mga chi2= 1582.1 fake_freq=0.2811 real_freq=0.0794
43
+ yung chi2= 1508.4 fake_freq=0.1792 real_freq=0.0319
44
+ social media chi2= 1503.9 fake_freq=0.2093 real_freq=0.0460
45
+ umano chi2= 1478.0 fake_freq=0.1161 real_freq=0.0079
46
+ kayo chi2= 1398.4 fake_freq=0.1054 real_freq=0.0060
47
+ leni chi2= 1397.2 fake_freq=0.1806 real_freq=0.0362
48
+ ako chi2= 1364.9 fake_freq=0.1684 real_freq=0.0317
49
+ comment chi2= 1353.9 fake_freq=0.2057 real_freq=0.0499
50
+ para chi2= 1319.4 fake_freq=0.2437 real_freq=0.0712
51
+ pangulong chi2= 1288.2 fake_freq=0.0925 real_freq=0.0040
52
+ catriona chi2= 1285.2 fake_freq=0.0797 real_freq=0.0004
53
+ thinkingpinoy chi2= 1268.6 fake_freq=0.0773 real_freq=0.0000
54
+ blog chi2= 1258.3 fake_freq=0.0961 real_freq=0.0058
55
+ dahil chi2= 1213.0 fake_freq=0.1816 real_freq=0.0433
56
+ isang chi2= 1210.0 fake_freq=0.2125 real_freq=0.0594
57
+ ayon chi2= 1204.6 fake_freq=0.1481 real_freq=0.0277
58
+ ilang chi2= 1176.7 fake_freq=0.1077 real_freq=0.0117
59
+ noynoy chi2= 1121.0 fake_freq=0.0794 real_freq=0.0032
60
+ ressa chi2= 1086.1 fake_freq=0.0760 real_freq=0.0028
61
+ com chi2= 1082.5 fake_freq=0.1124 real_freq=0.0160
62
+ din chi2= 1081.8 fake_freq=0.1438 real_freq=0.0299
63
+ bs aquino chi2= 1062.9 fake_freq=0.0650 real_freq=0.0001
64
+ natin chi2= 1035.5 fake_freq=0.1229 real_freq=0.0219
65
+ niya chi2= 1032.8 fake_freq=0.1660 real_freq=0.0427
66
+ http chi2= 1029.2 fake_freq=0.0703 real_freq=0.0021
67
+ grp chi2= 1010.0 fake_freq=0.0731 real_freq=0.0034
68
+ naging chi2= 993.2 fake_freq=0.0958 real_freq=0.0118
69
+ featured comment chi2= 990.4 fake_freq=0.0604 real_freq=0.0000
70
+ is grp chi2= 988.7 fake_freq=0.0603 real_freq=0.0000
71
+ grp featured chi2= 983.3 fake_freq=0.0599 real_freq=0.0000
72
+ like chi2= 983.3 fake_freq=0.5211 real_freq=0.2752
73
+ news chi2= 980.1 fake_freq=0.3171 real_freq=0.1348
74
+ catriona gray chi2= 979.6 fake_freq=0.0599 real_freq=0.0001
75
+ yan chi2= 971.2 fake_freq=0.0989 real_freq=0.0136
76
+
77
+ =================================================================
78
+ TOP 60 WORDS ASSOCIATED WITH *** REAL NEWS ***
79
+ (High-credibility markers ù useful context)
80
+ =================================================================
81
+ said chi2= 30152.0 real_freq=5.3763 fake_freq=0.9817
82
+ said the chi2= 7892.5 real_freq=1.0162 fake_freq=0.0680
83
+ he said chi2= 7048.7 real_freq=1.0146 fake_freq=0.1094
84
+ of the chi2= 6391.5 real_freq=4.0884 fake_freq=2.1730
85
+ was chi2= 6127.3 real_freq=3.1958 fake_freq=1.5541
86
+ had chi2= 4985.5 real_freq=1.2013 fake_freq=0.3359
87
+ were chi2= 4869.7 real_freq=1.4602 fake_freq=0.5025
88
+ for chi2= 3693.8 real_freq=3.9857 fake_freq=2.5171
89
+ percent chi2= 3316.0 real_freq=0.4733 fake_freq=0.0496
90
+ said he chi2= 3054.9 real_freq=0.4525 fake_freq=0.0535
91
+ city chi2= 3015.1 real_freq=0.9589 fake_freq=0.3458
92
+ police chi2= 2951.7 real_freq=0.6889 fake_freq=0.1859
93
+ mr duterte chi2= 2937.8 real_freq=0.3344 fake_freq=0.0068
94
+ would chi2= 2682.3 real_freq=1.1915 fake_freq=0.5339
95
+ added chi2= 2527.3 real_freq=0.4712 fake_freq=0.0931
96
+ in the chi2= 2323.8 real_freq=2.2774 fake_freq=1.3999
97
+ government chi2= 2221.8 real_freq=0.9786 fake_freq=0.4364
98
+ from chi2= 2142.5 real_freq=1.9267 fake_freq=1.1543
99
+ court chi2= 2128.4 real_freq=0.5376 fake_freq=0.1580
100
+ said in chi2= 1914.3 real_freq=0.3323 fake_freq=0.0577
101
+ province chi2= 1836.7 real_freq=0.2501 fake_freq=0.0219
102
+ year chi2= 1725.1 real_freq=0.6088 fake_freq=0.2369
103
+ she said chi2= 1703.1 real_freq=0.2836 fake_freq=0.0451
104
+ sen chi2= 1646.4 real_freq=0.2895 fake_freq=0.0515
105
+ the department chi2= 1624.6 real_freq=0.2532 fake_freq=0.0345
106
+ also chi2= 1619.7 real_freq=1.1369 fake_freq=0.6263
107
+ for the chi2= 1617.7 real_freq=0.9776 fake_freq=0.5076
108
+ rep chi2= 1561.8 real_freq=0.2070 fake_freq=0.0161
109
+ the president chi2= 1555.6 real_freq=0.5825 fake_freq=0.2358
110
+ to the chi2= 1533.1 real_freq=1.4158 fake_freq=0.8552
111
+ department chi2= 1480.6 real_freq=0.3825 fake_freq=0.1150
112
+ he added chi2= 1479.9 real_freq=0.2401 fake_freq=0.0361
113
+ office chi2= 1426.7 real_freq=0.4416 fake_freq=0.1560
114
+ friday chi2= 1420.6 real_freq=0.2201 fake_freq=0.0295
115
+ with chi2= 1404.1 real_freq=2.2094 fake_freq=1.5269
116
+ officials chi2= 1398.2 real_freq=0.3345 fake_freq=0.0928
117
+ military chi2= 1392.7 real_freq=0.3024 fake_freq=0.0747
118
+ monday chi2= 1364.4 real_freq=0.2359 fake_freq=0.0406
119
+ tuesday chi2= 1357.9 real_freq=0.2077 fake_freq=0.0269
120
+ house chi2= 1340.9 real_freq=0.3684 fake_freq=0.1175
121
+ chief chi2= 1329.3 real_freq=0.4226 fake_freq=0.1524
122
+ committee chi2= 1317.5 real_freq=0.2536 fake_freq=0.0528
123
+ the government chi2= 1310.3 real_freq=0.3822 fake_freq=0.1285
124
+ the house chi2= 1290.2 real_freq=0.2091 fake_freq=0.0313
125
+ had been chi2= 1277.4 real_freq=0.1926 fake_freq=0.0240
126
+ on friday chi2= 1269.4 real_freq=0.1663 fake_freq=0.0122
127
+ thursday chi2= 1260.2 real_freq=0.1889 fake_freq=0.0231
128
+ peace chi2= 1233.9 real_freq=0.2489 fake_freq=0.0556
129
+ million chi2= 1231.2 real_freq=0.4199 fake_freq=0.1595
130
+ and the chi2= 1210.5 real_freq=0.7438 fake_freq=0.3889
131
+ at the chi2= 1192.8 real_freq=0.6397 fake_freq=0.3152
132
+ wednesday chi2= 1176.4 real_freq=0.1791 fake_freq=0.0229
133
+ department of chi2= 1141.5 real_freq=0.2878 fake_freq=0.0845
134
+ national chi2= 1125.5 real_freq=0.5410 fake_freq=0.2527
135
+ on monday chi2= 1123.8 real_freq=0.1721 fake_freq=0.0224
136
+ under chi2= 1114.4 real_freq=0.4066 fake_freq=0.1618
137
+ region chi2= 1113.0 real_freq=0.1859 fake_freq=0.0298
138
+ town chi2= 1102.8 real_freq=0.1554 fake_freq=0.0156
139
+ on tuesday chi2= 1101.4 real_freq=0.1529 fake_freq=0.0145
140
+ by the chi2= 1079.0 real_freq=0.7163 fake_freq=0.3863
141
+
142
+ =================================================================
143
+ HOW TO USE THESE RESULTS:
144
+ 1. Review the FAKE words list above
145
+ 2. Add clearly sensational/biased words to SENSATIONAL_KEYWORDS
146
+ in checker/internal/bias_analyzer.py
147
+ 3. Add political framing words to RIGHT/LEFT_LEANING_KEYWORDS
148
+ =================================================================
backend/evaluate_model.py CHANGED
@@ -1,820 +1,820 @@
1
- """
2
- Model Overfitting Evaluation Script
3
- =====================================
4
- Evaluates the Random Forest fake news classifier for overfitting by
5
- comparing Training vs. Testing performance.
6
-
7
- Split: 80% Train / 20% Test
8
- Metrics: classification_report, accuracy_score, confusion matrix plot
9
- Flag: Overfitting detected if Train Acc > 95% and Test Acc < 70%
10
-
11
- Usage:
12
- python backend/evaluate_model.py
13
- """
14
-
15
- import sys
16
- import os
17
- import re
18
- import time
19
- import numpy as np
20
- from textblob import TextBlob
21
- import textstat
22
-
23
- PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24
- sys.path.insert(0, PROJECT_ROOT)
25
-
26
- import pandas as pd
27
- import matplotlib
28
-
29
- matplotlib.use("Agg") # Non-interactive backend for saving plots
30
- import matplotlib.pyplot as plt
31
- from scipy.sparse import hstack, csr_matrix
32
- from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
33
- from sklearn.feature_extraction.text import TfidfVectorizer
34
- from sklearn.ensemble import RandomForestClassifier
35
- from sklearn.preprocessing import StandardScaler
36
- from sklearn.metrics import (
37
- classification_report,
38
- accuracy_score,
39
- confusion_matrix,
40
- ConfusionMatrixDisplay,
41
- )
42
- from sentence_transformers import SentenceTransformer
43
-
44
-
45
- # ── Paths ──
46
- DATA_MODELS_DIR = os.path.join(PROJECT_ROOT, "data_models")
47
- OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
48
-
49
-
50
- # ── MiniLM Model (lazy-loaded singleton) ──
51
- MINILM_MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2"
52
- _minilm_model = None
53
-
54
-
55
- def get_minilm_model():
56
- """Load the multilingual MiniLM model (cached after first call)."""
57
- global _minilm_model
58
- if _minilm_model is None:
59
- print(" Loading MiniLM model...")
60
- _minilm_model = SentenceTransformer(MINILM_MODEL_NAME)
61
- return _minilm_model
62
-
63
-
64
- # ───────────────────────────────────────────────────────────
65
- # Text Cleaning (same as train.py)
66
- # ───────────────────────────────────────────────────────────
67
-
68
-
69
- def clean_text(text):
70
- """Basic text cleaning for Filipino news articles."""
71
- if not text or not isinstance(text, str):
72
- return ""
73
- text = re.sub(r"<[^>]+>", " ", text)
74
- text = re.sub(r"https?://\S+", " ", text)
75
- text = re.sub(r"\s+", " ", text)
76
- return text.strip()
77
-
78
-
79
- # ───────────────────────────────────────────────────────────
80
- # Stylometric Features (same as train.py)
81
- # ───────────────────────────────────────────────────────────
82
-
83
-
84
- # ── Word lists for linguistic features ──
85
- FIRST_PERSON_PRONOUNS = {
86
- "i",
87
- "me",
88
- "my",
89
- "mine",
90
- "myself",
91
- "we",
92
- "us",
93
- "our",
94
- "ours",
95
- "ourselves",
96
- "ako",
97
- "ko",
98
- "akin",
99
- "aking",
100
- "natin",
101
- "atin",
102
- "namin",
103
- "amin",
104
- "tayo",
105
- "kami",
106
- "ta",
107
- }
108
-
109
- AUXILIARY_VERBS = {
110
- "have",
111
- "has",
112
- "had",
113
- "do",
114
- "does",
115
- "did",
116
- "will",
117
- "would",
118
- "shall",
119
- "should",
120
- "may",
121
- "might",
122
- "can",
123
- "could",
124
- "must",
125
- "am",
126
- "is",
127
- "are",
128
- "was",
129
- "were",
130
- "be",
131
- "been",
132
- "being",
133
- "ay",
134
- "dapat",
135
- "mayroon",
136
- "meron",
137
- "maaari",
138
- "pwede",
139
- "kailangan",
140
- }
141
-
142
- ANALYTICAL_WORDS = {
143
- "the",
144
- "a",
145
- "an",
146
- "of",
147
- "in",
148
- "on",
149
- "at",
150
- "to",
151
- "for",
152
- "with",
153
- "by",
154
- "from",
155
- "about",
156
- "between",
157
- "through",
158
- "during",
159
- "before",
160
- "after",
161
- "ang",
162
- "ng",
163
- "sa",
164
- "mga",
165
- "nang",
166
- "para",
167
- "tungkol",
168
- "mula",
169
- }
170
-
171
- CERTAINTY_WORDS = {
172
- "always",
173
- "never",
174
- "absolutely",
175
- "definitely",
176
- "certainly",
177
- "undoubtedly",
178
- "clearly",
179
- "obviously",
180
- "without doubt",
181
- "guaranteed",
182
- "proven",
183
- "fact",
184
- "undeniable",
185
- "indisputable",
186
- "every",
187
- "all",
188
- "palagi",
189
- "sigurado",
190
- "tiyak",
191
- "talaga",
192
- "totoo",
193
- "lagi",
194
- "walang duda",
195
- }
196
-
197
- TENTATIVE_WORDS = {
198
- "perhaps",
199
- "maybe",
200
- "possibly",
201
- "might",
202
- "could",
203
- "likely",
204
- "unlikely",
205
- "suggests",
206
- "appears",
207
- "seems",
208
- "allegedly",
209
- "reportedly",
210
- "according",
211
- "probable",
212
- "approximately",
213
- "estimated",
214
- "siguro",
215
- "marahil",
216
- "maaaring",
217
- "mukhang",
218
- "parang",
219
- "umano",
220
- "diumano",
221
- }
222
-
223
- CLOUT_WORDS = {
224
- "must",
225
- "demand",
226
- "require",
227
- "order",
228
- "command",
229
- "insist",
230
- "decree",
231
- "mandate",
232
- "authority",
233
- "power",
234
- "control",
235
- "dominant",
236
- "superior",
237
- "we must",
238
- "you must",
239
- "kailangan",
240
- "dapat",
241
- "utos",
242
- "kapangyarihan",
243
- "kontrol",
244
- "mando",
245
- }
246
-
247
- PAST_FOCUS_WORDS = {
248
- "talked",
249
- "did",
250
- "ago",
251
- "said",
252
- "was",
253
- "were",
254
- "had",
255
- "went",
256
- "told",
257
- "noon",
258
- "nakaraan",
259
- "dati",
260
- "kahapon",
261
- }
262
-
263
- PRESENT_FOCUS_WORDS = {
264
- "now",
265
- "is",
266
- "today",
267
- "are",
268
- "being",
269
- "currently",
270
- "ongoing",
271
- "ngayon",
272
- "kasalukuyan",
273
- }
274
-
275
- FUTURE_FOCUS_WORDS = {
276
- "soon",
277
- "will",
278
- "may",
279
- "shall",
280
- "going",
281
- "plan",
282
- "expect",
283
- "tomorrow",
284
- "bukas",
285
- "darating",
286
- "magiging",
287
- "gagawin",
288
- }
289
-
290
-
291
- def extract_stylometric_features(text):
292
- """Extract 25 stylometric features from text (matches train.py)."""
293
- if not text or not isinstance(text, str):
294
- return [0.0] * 25
295
-
296
- words = text.split()
297
- token_count = len(words)
298
- if token_count == 0:
299
- return [0.0] * 25
300
-
301
- words_lower = [w.lower() for w in words]
302
- text_len = len(text)
303
-
304
- exclamation_density = text.count("!") / token_count
305
- question_count = text.count("?")
306
-
307
- caps_words = sum(1 for w in words if len(w) >= 2 and w.isupper())
308
- caps_ratio = caps_words / token_count
309
-
310
- sentences = re.split(r"[.!?]+", text)
311
- sentences = [s.strip() for s in sentences if s.strip()]
312
- avg_sentence_length = (
313
- sum(len(s.split()) for s in sentences) / len(sentences)
314
- if sentences
315
- else token_count
316
- )
317
-
318
- punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
319
- punctuation_density = (punct_chars / text_len) * 100 if text_len > 0 else 0
320
-
321
- unique_words = len(set(words_lower))
322
- unique_word_ratio = unique_words / token_count
323
-
324
- avg_word_length = sum(len(w) for w in words) / token_count
325
-
326
- try:
327
- subjectivity = TextBlob(text).sentiment.subjectivity
328
- except Exception:
329
- subjectivity = 0.0
330
-
331
- try:
332
- flesch_reading_ease = textstat.flesch_reading_ease(text)
333
- flesch_kincaid_grade = textstat.flesch_kincaid_grade(text)
334
- coleman_liau_index = textstat.coleman_liau_index(text)
335
- ari = textstat.automated_readability_index(text)
336
- except Exception:
337
- flesch_reading_ease = 0.0
338
- flesch_kincaid_grade = 0.0
339
- coleman_liau_index = 0.0
340
- ari = 0.0
341
-
342
- first_person_count = sum(1 for w in words_lower if w in FIRST_PERSON_PRONOUNS)
343
- first_person_ratio = first_person_count / token_count
344
-
345
- aux_count = sum(1 for w in words_lower if w in AUXILIARY_VERBS)
346
- auxiliary_verb_ratio = aux_count / token_count
347
-
348
- try:
349
- gunning_fog_index = textstat.gunning_fog(text)
350
- except Exception:
351
- gunning_fog_index = 0.0
352
-
353
- analytical_count = sum(1 for w in words_lower if w in ANALYTICAL_WORDS)
354
- analytical_thinking = analytical_count / token_count
355
-
356
- certainty_count = sum(1 for w in words_lower if w in CERTAINTY_WORDS)
357
- certainty_score = certainty_count / token_count
358
-
359
- tentative_count = sum(1 for w in words_lower if w in TENTATIVE_WORDS)
360
- tentative_score = tentative_count / token_count
361
-
362
- clout_count = sum(1 for w in words_lower if w in CLOUT_WORDS)
363
- clout_score = clout_count / token_count
364
-
365
- comma_period_count = text.count(",") + text.count(".")
366
- comma_period_density = (comma_period_count / text_len) * 100 if text_len > 0 else 0
367
-
368
- informal_count = (
369
- text.count("(")
370
- + text.count(")")
371
- + text.count("—")
372
- + text.count("–")
373
- + text.count("-")
374
- + text.count("...")
375
- + text.count("…")
376
- )
377
- informal_punct_density = (informal_count / text_len) * 100 if text_len > 0 else 0
378
-
379
- past_count = sum(1 for w in words_lower if w in PAST_FOCUS_WORDS)
380
- past_focus_ratio = past_count / token_count
381
-
382
- present_count = sum(1 for w in words_lower if w in PRESENT_FOCUS_WORDS)
383
- present_focus_ratio = present_count / token_count
384
-
385
- future_count = sum(1 for w in words_lower if w in FUTURE_FOCUS_WORDS)
386
- future_focus_ratio = future_count / token_count
387
-
388
- return [
389
- float(exclamation_density),
390
- float(question_count),
391
- float(caps_ratio),
392
- float(avg_sentence_length),
393
- float(punctuation_density),
394
- float(token_count),
395
- float(unique_word_ratio),
396
- float(avg_word_length),
397
- float(subjectivity),
398
- float(flesch_reading_ease),
399
- float(flesch_kincaid_grade),
400
- float(coleman_liau_index),
401
- float(ari),
402
- float(first_person_ratio),
403
- float(auxiliary_verb_ratio),
404
- float(gunning_fog_index),
405
- float(analytical_thinking),
406
- float(certainty_score),
407
- float(tentative_score),
408
- float(clout_score),
409
- float(comma_period_density),
410
- float(informal_punct_density),
411
- float(past_focus_ratio),
412
- float(present_focus_ratio),
413
- float(future_focus_ratio),
414
- ]
415
-
416
-
417
- STYLOMETRIC_FEATURE_NAMES = [
418
- "exclamation_density",
419
- "question_count",
420
- "caps_ratio",
421
- "avg_sentence_length",
422
- "punctuation_density",
423
- "token_count",
424
- "unique_word_ratio",
425
- "avg_word_length",
426
- "subjectivity",
427
- "flesch_reading_ease",
428
- "flesch_kincaid_grade",
429
- "coleman_liau_index",
430
- "ari",
431
- "first_person_ratio",
432
- "auxiliary_verb_ratio",
433
- "gunning_fog_index",
434
- "analytical_thinking",
435
- "certainty_score",
436
- "tentative_score",
437
- "clout_score",
438
- "comma_period_density",
439
- "informal_punct_density",
440
- "past_focus_ratio",
441
- "present_focus_ratio",
442
- "future_focus_ratio",
443
- ]
444
-
445
-
446
- # ───────────────────────────────────────────────────────────
447
- # Main Evaluation
448
- # ──────────────────────────────────────────────────────────
449
-
450
-
451
- def main():
452
- os.makedirs(OUTPUT_DIR, exist_ok=True)
453
- label_names = ["Real", "Fake"]
454
-
455
- # ── 1. Load Dataset ──
456
- print("=" * 60)
457
- print(" MODEL OVERFITTING EVALUATION")
458
- print("=" * 60)
459
-
460
- csv_path = os.path.join(
461
- PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv"
462
- )
463
- if not os.path.exists(csv_path):
464
- print(f"ERROR: Dataset not found at {csv_path}")
465
- return
466
-
467
- df = pd.read_csv(csv_path)
468
- print(f"\nDataset: jcblaise/fake_news_filipino")
469
- print(f"Total articles: {len(df)}")
470
- print(f"Distribution:")
471
- print(f" Real (0): {(df['label'] == 0).sum()}")
472
- print(f" Fake (1): {(df['label'] == 1).sum()}")
473
-
474
- # ── 2. Preprocess ──
475
- print("\nPreprocessing...")
476
- df = df.dropna(subset=["article"]).copy()
477
- df = df[df["article"].str.len() > 0].copy()
478
- df.loc[:, "article_clean"] = df["article"].apply(clean_text)
479
-
480
- X_texts = df["article_clean"].tolist()
481
- y_labels = df["label"].tolist()
482
- print(f" Valid articles: {len(X_texts)}")
483
-
484
- # ── 3. Split: 80% Train / 20% Test ──
485
- print("\nSplitting data: 80% Train / 20% Test...")
486
- X_train, X_test, y_train, y_test = train_test_split(
487
- X_texts,
488
- y_labels,
489
- test_size=0.20,
490
- random_state=42,
491
- stratify=y_labels,
492
- )
493
- print(f" Training set: {len(X_train)} articles")
494
- print(f" Testing set: {len(X_test)} articles")
495
-
496
- # ── 4. Build Hybrid Features ──
497
- print("\nBuilding hybrid features (TF-IDF + MiniLM + stylometric)...")
498
-
499
- # TF-IDF
500
- tfidf = TfidfVectorizer(
501
- max_features=15000,
502
- ngram_range=(1, 2),
503
- min_df=2,
504
- max_df=0.95,
505
- sublinear_tf=True,
506
- )
507
- X_train_tfidf = tfidf.fit_transform(X_train)
508
- X_test_tfidf = tfidf.transform(X_test)
509
-
510
- # MiniLM embeddings
511
- print(" Encoding texts with MiniLM...")
512
- minilm = get_minilm_model()
513
- train_embeddings = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
514
- test_embeddings = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
515
-
516
- # Stylometric
517
- print(" Extracting stylometric features...")
518
- train_stylo = np.array([extract_stylometric_features(t) for t in X_train])
519
- test_stylo = np.array([extract_stylometric_features(t) for t in X_test])
520
-
521
- scaler = StandardScaler()
522
- train_stylo_scaled = scaler.fit_transform(train_stylo)
523
- test_stylo_scaled = scaler.transform(test_stylo)
524
-
525
- # Combine
526
- X_train_feat = hstack(
527
- [X_train_tfidf, csr_matrix(train_embeddings), csr_matrix(train_stylo_scaled)]
528
- )
529
- X_test_feat = hstack(
530
- [X_test_tfidf, csr_matrix(test_embeddings), csr_matrix(test_stylo_scaled)]
531
- )
532
-
533
- n_tfidf = X_train_tfidf.shape[1]
534
- n_minilm = 384
535
- n_stylo = len(STYLOMETRIC_FEATURE_NAMES)
536
- print(
537
- f" Feature dimensions: {X_train_feat.shape[1]} "
538
- f"(TF-IDF: {n_tfidf} + MiniLM: {n_minilm} + Stylometric: {n_stylo})"
539
- )
540
-
541
- # ── 5. Full 5-Fold Cross-Validation ──
542
- print("\n" + "=" * 60)
543
- print(" 5-FOLD CROSS-VALIDATION (Full Dataset)")
544
- print("=" * 60)
545
-
546
- # Build features on entire dataset
547
- print("\nBuilding features on full dataset...")
548
- tfidf_full = TfidfVectorizer(
549
- max_features=15000,
550
- ngram_range=(1, 2),
551
- min_df=2,
552
- max_df=0.95,
553
- sublinear_tf=True,
554
- )
555
- X_tfidf_full = tfidf_full.fit_transform(X_texts)
556
-
557
- print(" Encoding full dataset with MiniLM...")
558
- full_embeddings = minilm.encode(X_texts, show_progress_bar=True, batch_size=64)
559
-
560
- stylo_full = np.array([extract_stylometric_features(t) for t in X_texts])
561
- scaler_full = StandardScaler()
562
- stylo_full_scaled = scaler_full.fit_transform(stylo_full)
563
- X_full = hstack(
564
- [X_tfidf_full, csr_matrix(full_embeddings), csr_matrix(stylo_full_scaled)]
565
- )
566
- y_full = np.array(y_labels)
567
-
568
- print(f" Total samples: {X_full.shape[0]}")
569
- print(
570
- f" Feature dimensions: {X_full.shape[1]} "
571
- f"(TF-IDF: {X_tfidf_full.shape[1]} + MiniLM: {n_minilm} + Stylometric: {n_stylo})"
572
- )
573
-
574
- cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
575
-
576
- fold_accuracies = []
577
- fold_precisions = []
578
- fold_recalls = []
579
- fold_f1s = []
580
- fold_train_accs = []
581
- all_y_true = []
582
- all_y_pred = []
583
-
584
- for fold_idx, (train_idx, test_idx) in enumerate(cv.split(X_full, y_full), 1):
585
- X_fold_train = X_full[train_idx]
586
- X_fold_test = X_full[test_idx]
587
- y_fold_train = y_full[train_idx]
588
- y_fold_test = y_full[test_idx]
589
-
590
- print(f"\n{'─' * 60}")
591
- print(f" FOLD {fold_idx}/5 (Train: {len(train_idx)}, Test: {len(test_idx)})")
592
- print(f"{'─' * 60}")
593
-
594
- rf_fold = RandomForestClassifier(
595
- n_estimators=300,
596
- max_depth=15,
597
- min_samples_split=5,
598
- min_samples_leaf=5,
599
- class_weight="balanced",
600
- n_jobs=-1,
601
- random_state=42,
602
- )
603
- rf_fold.fit(X_fold_train, y_fold_train)
604
-
605
- # Predictions
606
- y_fold_train_pred = rf_fold.predict(X_fold_train)
607
- y_fold_test_pred = rf_fold.predict(X_fold_test)
608
-
609
- train_acc = accuracy_score(y_fold_train, y_fold_train_pred)
610
- test_acc = accuracy_score(y_fold_test, y_fold_test_pred)
611
-
612
- fold_train_accs.append(train_acc)
613
- fold_accuracies.append(test_acc)
614
-
615
- # Per-fold classification report
616
- report = classification_report(
617
- y_fold_test,
618
- y_fold_test_pred,
619
- target_names=label_names,
620
- output_dict=True,
621
- )
622
- fold_precisions.append(report["weighted avg"]["precision"])
623
- fold_recalls.append(report["weighted avg"]["recall"])
624
- fold_f1s.append(report["weighted avg"]["f1-score"])
625
-
626
- # Collect for final confusion matrix
627
- all_y_true.extend(y_fold_test)
628
- all_y_pred.extend(y_fold_test_pred)
629
-
630
- print(f" Train Accuracy: {train_acc:.4f} ({train_acc:.1%})")
631
- print(f" Test Accuracy: {test_acc:.4f} ({test_acc:.1%})")
632
- print(f" Gap: {train_acc - test_acc:.4f}")
633
- print()
634
- print(
635
- classification_report(
636
- y_fold_test, y_fold_test_pred, target_names=label_names
637
- )
638
- )
639
-
640
- # ── 6. Cross-Fold Summary ──
641
- fold_accuracies = np.array(fold_accuracies)
642
- fold_train_accs = np.array(fold_train_accs)
643
- fold_precisions = np.array(fold_precisions)
644
- fold_recalls = np.array(fold_recalls)
645
- fold_f1s = np.array(fold_f1s)
646
- gaps = fold_train_accs - fold_accuracies
647
-
648
- print("\n" + "=" * 60)
649
- print(" CROSS-VALIDATION SUMMARY (5 Folds)")
650
- print("=" * 60)
651
-
652
- print(f"\n Per-Fold Test Accuracies:")
653
- for i, (ta, te) in enumerate(zip(fold_train_accs, fold_accuracies), 1):
654
- print(f" Fold {i}: Train {ta:.1%} | Test {te:.1%} | Gap {ta - te:.1%}")
655
-
656
- print(
657
- f"\n Average Training Accuracy: {fold_train_accs.mean():.4f} "
658
- f"(+/- {fold_train_accs.std():.4f})"
659
- )
660
- print(
661
- f" Average Testing Accuracy: {fold_accuracies.mean():.4f} "
662
- f"(+/- {fold_accuracies.std():.4f})"
663
- )
664
- print(
665
- f" Average Precision: {fold_precisions.mean():.4f} "
666
- f"(+/- {fold_precisions.std():.4f})"
667
- )
668
- print(
669
- f" Average Recall: {fold_recalls.mean():.4f} "
670
- f"(+/- {fold_recalls.std():.4f})"
671
- )
672
- print(
673
- f" Average F1 Score: {fold_f1s.mean():.4f} "
674
- f"(+/- {fold_f1s.std():.4f})"
675
- )
676
- print(f" Average Gap: {gaps.mean():.4f} " f"(+/- {gaps.std():.4f})")
677
-
678
- # ── 7. Consistency Check ──
679
- print("\n" + "=" * 60)
680
- print(" VERDICT CONSISTENCY & OVERFITTING ANALYSIS")
681
- print("=" * 60)
682
-
683
- avg_train = fold_train_accs.mean()
684
- avg_test = fold_accuracies.mean()
685
- avg_gap = gaps.mean()
686
- acc_std = fold_accuracies.std()
687
-
688
- if avg_train > 0.95 and avg_test < 0.70:
689
- overfit_status = "OVERFITTING DETECTED"
690
- print(f"\n *** OVERFITTING DETECTED ***")
691
- print(f" Average training accuracy ({avg_train:.1%}) is much higher than")
692
- print(f" average testing accuracy ({avg_test:.1%}).")
693
- print(f" The model memorizes training data and fails to generalize.")
694
- elif avg_gap > 0.10:
695
- overfit_status = "MILD OVERFITTING"
696
- print(f"\n ** MILD OVERFITTING **")
697
- print(f" Average gap ({avg_gap:.1%}) exceeds 10%.")
698
- else:
699
- overfit_status = "NO OVERFITTING"
700
- print(f"\n NO OVERFITTING DETECTED")
701
- print(f" Average gap ({avg_gap:.1%}) is within acceptable range.")
702
-
703
- if acc_std < 0.01:
704
- consistency = "HIGHLY CONSISTENT"
705
- print(f" Verdict Consistency: HIGHLY CONSISTENT (std={acc_std:.4f})")
706
- print(f" Predictions are very stable across all 5 folds.")
707
- elif acc_std < 0.03:
708
- consistency = "CONSISTENT"
709
- print(f" Verdict Consistency: CONSISTENT (std={acc_std:.4f})")
710
- print(f" Minor variance across folds — acceptable for production.")
711
- else:
712
- consistency = "INCONSISTENT"
713
- print(f" Verdict Consistency: INCONSISTENT (std={acc_std:.4f})")
714
- print(f" High variance suggests model stability issues.")
715
-
716
- # ── 8. Confusion Matrix (aggregated across all folds) ──
717
- print("\n\nGenerating plots...")
718
- cm = confusion_matrix(all_y_true, all_y_pred)
719
- overall_acc = accuracy_score(all_y_true, all_y_pred)
720
-
721
- fig, ax = plt.subplots(figsize=(8, 6))
722
- disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=label_names)
723
- disp.plot(ax=ax, cmap="Blues", values_format="d")
724
-
725
- ax.set_title(
726
- f"Confusion Matrix — Aggregated 5-Fold CV\n"
727
- f"Overall Accuracy: {overall_acc:.1%} | {overfit_status}",
728
- fontsize=14,
729
- fontweight="bold",
730
- )
731
- ax.set_xlabel("Predicted Label", fontsize=12)
732
- ax.set_ylabel("True Label", fontsize=12)
733
-
734
- plt.tight_layout()
735
- cm_path = os.path.join(OUTPUT_DIR, "confusion_matrix.png")
736
- fig.savefig(cm_path, dpi=150, bbox_inches="tight")
737
- print(f" Saved: {cm_path}")
738
-
739
- # ── 9. Per-Fold Accuracy Bar Chart ──
740
- fig2, ax2 = plt.subplots(figsize=(10, 5))
741
-
742
- x = np.arange(5)
743
- width = 0.35
744
- bars_train = ax2.bar(
745
- x - width / 2,
746
- fold_train_accs * 100,
747
- width,
748
- label="Training",
749
- color="#2196F3",
750
- edgecolor="black",
751
- linewidth=0.5,
752
- )
753
- bars_test = ax2.bar(
754
- x + width / 2,
755
- fold_accuracies * 100,
756
- width,
757
- label="Testing",
758
- color="#FF9800",
759
- edgecolor="black",
760
- linewidth=0.5,
761
- )
762
-
763
- for bar, val in zip(bars_train, fold_train_accs):
764
- ax2.text(
765
- bar.get_x() + bar.get_width() / 2,
766
- bar.get_height() + 0.3,
767
- f"{val:.1%}",
768
- ha="center",
769
- va="bottom",
770
- fontsize=9,
771
- fontweight="bold",
772
- )
773
- for bar, val in zip(bars_test, fold_accuracies):
774
- ax2.text(
775
- bar.get_x() + bar.get_width() / 2,
776
- bar.get_height() + 0.3,
777
- f"{val:.1%}",
778
- ha="center",
779
- va="bottom",
780
- fontsize=9,
781
- fontweight="bold",
782
- )
783
-
784
- ax2.set_xticks(x)
785
- ax2.set_xticklabels([f"Fold {i}" for i in range(1, 6)])
786
- ax2.set_ylim(0, 105)
787
- ax2.set_ylabel("Accuracy (%)", fontsize=12)
788
- ax2.set_title(
789
- f"Per-Fold Accuracy Comparison\n"
790
- f"Avg Test: {avg_test:.1%} (+/- {acc_std:.4f}) | {consistency}",
791
- fontsize=14,
792
- fontweight="bold",
793
- )
794
- ax2.legend(loc="lower right")
795
- ax2.axhline(y=70, color="red", linestyle="--", alpha=0.5, label="70% threshold")
796
-
797
- plt.tight_layout()
798
- bar_path = os.path.join(OUTPUT_DIR, "accuracy_comparison.png")
799
- fig2.savefig(bar_path, dpi=150, bbox_inches="tight")
800
- print(f" Saved: {bar_path}")
801
-
802
- # ── Final Summary ──
803
- print("\n" + "=" * 60)
804
- print(" EVALUATION COMPLETE")
805
- print("=" * 60)
806
- print(f" Dataset: fake_news_filipino ({len(df)} articles)")
807
- print(f" Feature set: {X_full.shape[1]} (TF-IDF + 9 stylometric)")
808
- print(f" Cross-Validation: 5-Fold Stratified")
809
- print(f" Avg Training Accuracy: {avg_train:.4f} (+/- {fold_train_accs.std():.4f})")
810
- print(f" Avg Testing Accuracy: {avg_test:.4f} (+/- {acc_std:.4f})")
811
- print(f" Avg F1 Score: {fold_f1s.mean():.4f} (+/- {fold_f1s.std():.4f})")
812
- print(f" Avg Gap: {avg_gap:.4f}")
813
- print(f" Overfitting Status: {overfit_status}")
814
- print(f" Verdict Consistency: {consistency}")
815
- print(f" Plots saved to: {OUTPUT_DIR}/")
816
- print("=" * 60)
817
-
818
-
819
- if __name__ == "__main__":
820
- main()
 
1
+ """
2
+ Model Overfitting Evaluation Script
3
+ =====================================
4
+ Evaluates the Random Forest fake news classifier for overfitting by
5
+ comparing Training vs. Testing performance.
6
+
7
+ Split: 80% Train / 20% Test
8
+ Metrics: classification_report, accuracy_score, confusion matrix plot
9
+ Flag: Overfitting detected if Train Acc > 95% and Test Acc < 70%
10
+
11
+ Usage:
12
+ python backend/evaluate_model.py
13
+ """
14
+
15
+ import sys
16
+ import os
17
+ import re
18
+ import time
19
+ import numpy as np
20
+ from textblob import TextBlob
21
+ import textstat
22
+
23
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
24
+ sys.path.insert(0, PROJECT_ROOT)
25
+
26
+ import pandas as pd
27
+ import matplotlib
28
+
29
+ matplotlib.use("Agg") # Non-interactive backend for saving plots
30
+ import matplotlib.pyplot as plt
31
+ from scipy.sparse import hstack, csr_matrix
32
+ from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
33
+ from sklearn.feature_extraction.text import TfidfVectorizer
34
+ from sklearn.ensemble import RandomForestClassifier
35
+ from sklearn.preprocessing import StandardScaler
36
+ from sklearn.metrics import (
37
+ classification_report,
38
+ accuracy_score,
39
+ confusion_matrix,
40
+ ConfusionMatrixDisplay,
41
+ )
42
+ from sentence_transformers import SentenceTransformer
43
+
44
+
45
+ # ── Paths ──
46
+ DATA_MODELS_DIR = os.path.join(PROJECT_ROOT, "data_models")
47
+ OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
48
+
49
+
50
+ # ── MiniLM Model (lazy-loaded singleton) ──
51
+ MINILM_MODEL_NAME = "paraphrase-multilingual-MiniLM-L12-v2"
52
+ _minilm_model = None
53
+
54
+
55
+ def get_minilm_model():
56
+ """Load the multilingual MiniLM model (cached after first call)."""
57
+ global _minilm_model
58
+ if _minilm_model is None:
59
+ print(" Loading MiniLM model...")
60
+ _minilm_model = SentenceTransformer(MINILM_MODEL_NAME)
61
+ return _minilm_model
62
+
63
+
64
+ # ───────────────────────────────────────────────────────────
65
+ # Text Cleaning (same as train.py)
66
+ # ───────────────────────────────────────────────────────────
67
+
68
+
69
+ def clean_text(text):
70
+ """Basic text cleaning for Filipino news articles."""
71
+ if not text or not isinstance(text, str):
72
+ return ""
73
+ text = re.sub(r"<[^>]+>", " ", text)
74
+ text = re.sub(r"https?://\S+", " ", text)
75
+ text = re.sub(r"\s+", " ", text)
76
+ return text.strip()
77
+
78
+
79
+ # ───────────────────────────────────────────────────────────
80
+ # Stylometric Features (same as train.py)
81
+ # ───────────────────────────────────────────────────────────
82
+
83
+
84
+ # ── Word lists for linguistic features ──
85
+ FIRST_PERSON_PRONOUNS = {
86
+ "i",
87
+ "me",
88
+ "my",
89
+ "mine",
90
+ "myself",
91
+ "we",
92
+ "us",
93
+ "our",
94
+ "ours",
95
+ "ourselves",
96
+ "ako",
97
+ "ko",
98
+ "akin",
99
+ "aking",
100
+ "natin",
101
+ "atin",
102
+ "namin",
103
+ "amin",
104
+ "tayo",
105
+ "kami",
106
+ "ta",
107
+ }
108
+
109
+ AUXILIARY_VERBS = {
110
+ "have",
111
+ "has",
112
+ "had",
113
+ "do",
114
+ "does",
115
+ "did",
116
+ "will",
117
+ "would",
118
+ "shall",
119
+ "should",
120
+ "may",
121
+ "might",
122
+ "can",
123
+ "could",
124
+ "must",
125
+ "am",
126
+ "is",
127
+ "are",
128
+ "was",
129
+ "were",
130
+ "be",
131
+ "been",
132
+ "being",
133
+ "ay",
134
+ "dapat",
135
+ "mayroon",
136
+ "meron",
137
+ "maaari",
138
+ "pwede",
139
+ "kailangan",
140
+ }
141
+
142
+ ANALYTICAL_WORDS = {
143
+ "the",
144
+ "a",
145
+ "an",
146
+ "of",
147
+ "in",
148
+ "on",
149
+ "at",
150
+ "to",
151
+ "for",
152
+ "with",
153
+ "by",
154
+ "from",
155
+ "about",
156
+ "between",
157
+ "through",
158
+ "during",
159
+ "before",
160
+ "after",
161
+ "ang",
162
+ "ng",
163
+ "sa",
164
+ "mga",
165
+ "nang",
166
+ "para",
167
+ "tungkol",
168
+ "mula",
169
+ }
170
+
171
+ CERTAINTY_WORDS = {
172
+ "always",
173
+ "never",
174
+ "absolutely",
175
+ "definitely",
176
+ "certainly",
177
+ "undoubtedly",
178
+ "clearly",
179
+ "obviously",
180
+ "without doubt",
181
+ "guaranteed",
182
+ "proven",
183
+ "fact",
184
+ "undeniable",
185
+ "indisputable",
186
+ "every",
187
+ "all",
188
+ "palagi",
189
+ "sigurado",
190
+ "tiyak",
191
+ "talaga",
192
+ "totoo",
193
+ "lagi",
194
+ "walang duda",
195
+ }
196
+
197
+ TENTATIVE_WORDS = {
198
+ "perhaps",
199
+ "maybe",
200
+ "possibly",
201
+ "might",
202
+ "could",
203
+ "likely",
204
+ "unlikely",
205
+ "suggests",
206
+ "appears",
207
+ "seems",
208
+ "allegedly",
209
+ "reportedly",
210
+ "according",
211
+ "probable",
212
+ "approximately",
213
+ "estimated",
214
+ "siguro",
215
+ "marahil",
216
+ "maaaring",
217
+ "mukhang",
218
+ "parang",
219
+ "umano",
220
+ "diumano",
221
+ }
222
+
223
+ CLOUT_WORDS = {
224
+ "must",
225
+ "demand",
226
+ "require",
227
+ "order",
228
+ "command",
229
+ "insist",
230
+ "decree",
231
+ "mandate",
232
+ "authority",
233
+ "power",
234
+ "control",
235
+ "dominant",
236
+ "superior",
237
+ "we must",
238
+ "you must",
239
+ "kailangan",
240
+ "dapat",
241
+ "utos",
242
+ "kapangyarihan",
243
+ "kontrol",
244
+ "mando",
245
+ }
246
+
247
+ PAST_FOCUS_WORDS = {
248
+ "talked",
249
+ "did",
250
+ "ago",
251
+ "said",
252
+ "was",
253
+ "were",
254
+ "had",
255
+ "went",
256
+ "told",
257
+ "noon",
258
+ "nakaraan",
259
+ "dati",
260
+ "kahapon",
261
+ }
262
+
263
+ PRESENT_FOCUS_WORDS = {
264
+ "now",
265
+ "is",
266
+ "today",
267
+ "are",
268
+ "being",
269
+ "currently",
270
+ "ongoing",
271
+ "ngayon",
272
+ "kasalukuyan",
273
+ }
274
+
275
+ FUTURE_FOCUS_WORDS = {
276
+ "soon",
277
+ "will",
278
+ "may",
279
+ "shall",
280
+ "going",
281
+ "plan",
282
+ "expect",
283
+ "tomorrow",
284
+ "bukas",
285
+ "darating",
286
+ "magiging",
287
+ "gagawin",
288
+ }
289
+
290
+
291
+ def extract_stylometric_features(text):
292
+ """Extract 25 stylometric features from text (matches train.py)."""
293
+ if not text or not isinstance(text, str):
294
+ return [0.0] * 25
295
+
296
+ words = text.split()
297
+ token_count = len(words)
298
+ if token_count == 0:
299
+ return [0.0] * 25
300
+
301
+ words_lower = [w.lower() for w in words]
302
+ text_len = len(text)
303
+
304
+ exclamation_density = text.count("!") / token_count
305
+ question_count = text.count("?")
306
+
307
+ caps_words = sum(1 for w in words if len(w) >= 2 and w.isupper())
308
+ caps_ratio = caps_words / token_count
309
+
310
+ sentences = re.split(r"[.!?]+", text)
311
+ sentences = [s.strip() for s in sentences if s.strip()]
312
+ avg_sentence_length = (
313
+ sum(len(s.split()) for s in sentences) / len(sentences)
314
+ if sentences
315
+ else token_count
316
+ )
317
+
318
+ punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
319
+ punctuation_density = (punct_chars / text_len) * 100 if text_len > 0 else 0
320
+
321
+ unique_words = len(set(words_lower))
322
+ unique_word_ratio = unique_words / token_count
323
+
324
+ avg_word_length = sum(len(w) for w in words) / token_count
325
+
326
+ try:
327
+ subjectivity = TextBlob(text).sentiment.subjectivity
328
+ except Exception:
329
+ subjectivity = 0.0
330
+
331
+ try:
332
+ flesch_reading_ease = textstat.flesch_reading_ease(text)
333
+ flesch_kincaid_grade = textstat.flesch_kincaid_grade(text)
334
+ coleman_liau_index = textstat.coleman_liau_index(text)
335
+ ari = textstat.automated_readability_index(text)
336
+ except Exception:
337
+ flesch_reading_ease = 0.0
338
+ flesch_kincaid_grade = 0.0
339
+ coleman_liau_index = 0.0
340
+ ari = 0.0
341
+
342
+ first_person_count = sum(1 for w in words_lower if w in FIRST_PERSON_PRONOUNS)
343
+ first_person_ratio = first_person_count / token_count
344
+
345
+ aux_count = sum(1 for w in words_lower if w in AUXILIARY_VERBS)
346
+ auxiliary_verb_ratio = aux_count / token_count
347
+
348
+ try:
349
+ gunning_fog_index = textstat.gunning_fog(text)
350
+ except Exception:
351
+ gunning_fog_index = 0.0
352
+
353
+ analytical_count = sum(1 for w in words_lower if w in ANALYTICAL_WORDS)
354
+ analytical_thinking = analytical_count / token_count
355
+
356
+ certainty_count = sum(1 for w in words_lower if w in CERTAINTY_WORDS)
357
+ certainty_score = certainty_count / token_count
358
+
359
+ tentative_count = sum(1 for w in words_lower if w in TENTATIVE_WORDS)
360
+ tentative_score = tentative_count / token_count
361
+
362
+ clout_count = sum(1 for w in words_lower if w in CLOUT_WORDS)
363
+ clout_score = clout_count / token_count
364
+
365
+ comma_period_count = text.count(",") + text.count(".")
366
+ comma_period_density = (comma_period_count / text_len) * 100 if text_len > 0 else 0
367
+
368
+ informal_count = (
369
+ text.count("(")
370
+ + text.count(")")
371
+ + text.count("—")
372
+ + text.count("–")
373
+ + text.count("-")
374
+ + text.count("...")
375
+ + text.count("…")
376
+ )
377
+ informal_punct_density = (informal_count / text_len) * 100 if text_len > 0 else 0
378
+
379
+ past_count = sum(1 for w in words_lower if w in PAST_FOCUS_WORDS)
380
+ past_focus_ratio = past_count / token_count
381
+
382
+ present_count = sum(1 for w in words_lower if w in PRESENT_FOCUS_WORDS)
383
+ present_focus_ratio = present_count / token_count
384
+
385
+ future_count = sum(1 for w in words_lower if w in FUTURE_FOCUS_WORDS)
386
+ future_focus_ratio = future_count / token_count
387
+
388
+ return [
389
+ float(exclamation_density),
390
+ float(question_count),
391
+ float(caps_ratio),
392
+ float(avg_sentence_length),
393
+ float(punctuation_density),
394
+ float(token_count),
395
+ float(unique_word_ratio),
396
+ float(avg_word_length),
397
+ float(subjectivity),
398
+ float(flesch_reading_ease),
399
+ float(flesch_kincaid_grade),
400
+ float(coleman_liau_index),
401
+ float(ari),
402
+ float(first_person_ratio),
403
+ float(auxiliary_verb_ratio),
404
+ float(gunning_fog_index),
405
+ float(analytical_thinking),
406
+ float(certainty_score),
407
+ float(tentative_score),
408
+ float(clout_score),
409
+ float(comma_period_density),
410
+ float(informal_punct_density),
411
+ float(past_focus_ratio),
412
+ float(present_focus_ratio),
413
+ float(future_focus_ratio),
414
+ ]
415
+
416
+
417
+ STYLOMETRIC_FEATURE_NAMES = [
418
+ "exclamation_density",
419
+ "question_count",
420
+ "caps_ratio",
421
+ "avg_sentence_length",
422
+ "punctuation_density",
423
+ "token_count",
424
+ "unique_word_ratio",
425
+ "avg_word_length",
426
+ "subjectivity",
427
+ "flesch_reading_ease",
428
+ "flesch_kincaid_grade",
429
+ "coleman_liau_index",
430
+ "ari",
431
+ "first_person_ratio",
432
+ "auxiliary_verb_ratio",
433
+ "gunning_fog_index",
434
+ "analytical_thinking",
435
+ "certainty_score",
436
+ "tentative_score",
437
+ "clout_score",
438
+ "comma_period_density",
439
+ "informal_punct_density",
440
+ "past_focus_ratio",
441
+ "present_focus_ratio",
442
+ "future_focus_ratio",
443
+ ]
444
+
445
+
446
+ # ───────────────────────────────────────────────────────────
447
+ # Main Evaluation
448
+ # ──��────────────────────────────────────────────────────────
449
+
450
+
451
+ def main():
452
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
453
+ label_names = ["Real", "Fake"]
454
+
455
+ # ── 1. Load Dataset ──
456
+ print("=" * 60)
457
+ print(" MODEL OVERFITTING EVALUATION")
458
+ print("=" * 60)
459
+
460
+ csv_path = os.path.join(
461
+ PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv"
462
+ )
463
+ if not os.path.exists(csv_path):
464
+ print(f"ERROR: Dataset not found at {csv_path}")
465
+ return
466
+
467
+ df = pd.read_csv(csv_path)
468
+ print(f"\nDataset: jcblaise/fake_news_filipino")
469
+ print(f"Total articles: {len(df)}")
470
+ print(f"Distribution:")
471
+ print(f" Real (0): {(df['label'] == 0).sum()}")
472
+ print(f" Fake (1): {(df['label'] == 1).sum()}")
473
+
474
+ # ── 2. Preprocess ──
475
+ print("\nPreprocessing...")
476
+ df = df.dropna(subset=["article"]).copy()
477
+ df = df[df["article"].str.len() > 0].copy()
478
+ df.loc[:, "article_clean"] = df["article"].apply(clean_text)
479
+
480
+ X_texts = df["article_clean"].tolist()
481
+ y_labels = df["label"].tolist()
482
+ print(f" Valid articles: {len(X_texts)}")
483
+
484
+ # ── 3. Split: 80% Train / 20% Test ──
485
+ print("\nSplitting data: 80% Train / 20% Test...")
486
+ X_train, X_test, y_train, y_test = train_test_split(
487
+ X_texts,
488
+ y_labels,
489
+ test_size=0.20,
490
+ random_state=42,
491
+ stratify=y_labels,
492
+ )
493
+ print(f" Training set: {len(X_train)} articles")
494
+ print(f" Testing set: {len(X_test)} articles")
495
+
496
+ # ── 4. Build Hybrid Features ──
497
+ print("\nBuilding hybrid features (TF-IDF + MiniLM + stylometric)...")
498
+
499
+ # TF-IDF
500
+ tfidf = TfidfVectorizer(
501
+ max_features=15000,
502
+ ngram_range=(1, 2),
503
+ min_df=2,
504
+ max_df=0.95,
505
+ sublinear_tf=True,
506
+ )
507
+ X_train_tfidf = tfidf.fit_transform(X_train)
508
+ X_test_tfidf = tfidf.transform(X_test)
509
+
510
+ # MiniLM embeddings
511
+ print(" Encoding texts with MiniLM...")
512
+ minilm = get_minilm_model()
513
+ train_embeddings = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
514
+ test_embeddings = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
515
+
516
+ # Stylometric
517
+ print(" Extracting stylometric features...")
518
+ train_stylo = np.array([extract_stylometric_features(t) for t in X_train])
519
+ test_stylo = np.array([extract_stylometric_features(t) for t in X_test])
520
+
521
+ scaler = StandardScaler()
522
+ train_stylo_scaled = scaler.fit_transform(train_stylo)
523
+ test_stylo_scaled = scaler.transform(test_stylo)
524
+
525
+ # Combine
526
+ X_train_feat = hstack(
527
+ [X_train_tfidf, csr_matrix(train_embeddings), csr_matrix(train_stylo_scaled)]
528
+ )
529
+ X_test_feat = hstack(
530
+ [X_test_tfidf, csr_matrix(test_embeddings), csr_matrix(test_stylo_scaled)]
531
+ )
532
+
533
+ n_tfidf = X_train_tfidf.shape[1]
534
+ n_minilm = 384
535
+ n_stylo = len(STYLOMETRIC_FEATURE_NAMES)
536
+ print(
537
+ f" Feature dimensions: {X_train_feat.shape[1]} "
538
+ f"(TF-IDF: {n_tfidf} + MiniLM: {n_minilm} + Stylometric: {n_stylo})"
539
+ )
540
+
541
+ # ── 5. Full 5-Fold Cross-Validation ──
542
+ print("\n" + "=" * 60)
543
+ print(" 5-FOLD CROSS-VALIDATION (Full Dataset)")
544
+ print("=" * 60)
545
+
546
+ # Build features on entire dataset
547
+ print("\nBuilding features on full dataset...")
548
+ tfidf_full = TfidfVectorizer(
549
+ max_features=15000,
550
+ ngram_range=(1, 2),
551
+ min_df=2,
552
+ max_df=0.95,
553
+ sublinear_tf=True,
554
+ )
555
+ X_tfidf_full = tfidf_full.fit_transform(X_texts)
556
+
557
+ print(" Encoding full dataset with MiniLM...")
558
+ full_embeddings = minilm.encode(X_texts, show_progress_bar=True, batch_size=64)
559
+
560
+ stylo_full = np.array([extract_stylometric_features(t) for t in X_texts])
561
+ scaler_full = StandardScaler()
562
+ stylo_full_scaled = scaler_full.fit_transform(stylo_full)
563
+ X_full = hstack(
564
+ [X_tfidf_full, csr_matrix(full_embeddings), csr_matrix(stylo_full_scaled)]
565
+ )
566
+ y_full = np.array(y_labels)
567
+
568
+ print(f" Total samples: {X_full.shape[0]}")
569
+ print(
570
+ f" Feature dimensions: {X_full.shape[1]} "
571
+ f"(TF-IDF: {X_tfidf_full.shape[1]} + MiniLM: {n_minilm} + Stylometric: {n_stylo})"
572
+ )
573
+
574
+ cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
575
+
576
+ fold_accuracies = []
577
+ fold_precisions = []
578
+ fold_recalls = []
579
+ fold_f1s = []
580
+ fold_train_accs = []
581
+ all_y_true = []
582
+ all_y_pred = []
583
+
584
+ for fold_idx, (train_idx, test_idx) in enumerate(cv.split(X_full, y_full), 1):
585
+ X_fold_train = X_full[train_idx]
586
+ X_fold_test = X_full[test_idx]
587
+ y_fold_train = y_full[train_idx]
588
+ y_fold_test = y_full[test_idx]
589
+
590
+ print(f"\n{'─' * 60}")
591
+ print(f" FOLD {fold_idx}/5 (Train: {len(train_idx)}, Test: {len(test_idx)})")
592
+ print(f"{'─' * 60}")
593
+
594
+ rf_fold = RandomForestClassifier(
595
+ n_estimators=300,
596
+ max_depth=15,
597
+ min_samples_split=5,
598
+ min_samples_leaf=5,
599
+ class_weight="balanced",
600
+ n_jobs=-1,
601
+ random_state=42,
602
+ )
603
+ rf_fold.fit(X_fold_train, y_fold_train)
604
+
605
+ # Predictions
606
+ y_fold_train_pred = rf_fold.predict(X_fold_train)
607
+ y_fold_test_pred = rf_fold.predict(X_fold_test)
608
+
609
+ train_acc = accuracy_score(y_fold_train, y_fold_train_pred)
610
+ test_acc = accuracy_score(y_fold_test, y_fold_test_pred)
611
+
612
+ fold_train_accs.append(train_acc)
613
+ fold_accuracies.append(test_acc)
614
+
615
+ # Per-fold classification report
616
+ report = classification_report(
617
+ y_fold_test,
618
+ y_fold_test_pred,
619
+ target_names=label_names,
620
+ output_dict=True,
621
+ )
622
+ fold_precisions.append(report["weighted avg"]["precision"])
623
+ fold_recalls.append(report["weighted avg"]["recall"])
624
+ fold_f1s.append(report["weighted avg"]["f1-score"])
625
+
626
+ # Collect for final confusion matrix
627
+ all_y_true.extend(y_fold_test)
628
+ all_y_pred.extend(y_fold_test_pred)
629
+
630
+ print(f" Train Accuracy: {train_acc:.4f} ({train_acc:.1%})")
631
+ print(f" Test Accuracy: {test_acc:.4f} ({test_acc:.1%})")
632
+ print(f" Gap: {train_acc - test_acc:.4f}")
633
+ print()
634
+ print(
635
+ classification_report(
636
+ y_fold_test, y_fold_test_pred, target_names=label_names
637
+ )
638
+ )
639
+
640
+ # ── 6. Cross-Fold Summary ──
641
+ fold_accuracies = np.array(fold_accuracies)
642
+ fold_train_accs = np.array(fold_train_accs)
643
+ fold_precisions = np.array(fold_precisions)
644
+ fold_recalls = np.array(fold_recalls)
645
+ fold_f1s = np.array(fold_f1s)
646
+ gaps = fold_train_accs - fold_accuracies
647
+
648
+ print("\n" + "=" * 60)
649
+ print(" CROSS-VALIDATION SUMMARY (5 Folds)")
650
+ print("=" * 60)
651
+
652
+ print(f"\n Per-Fold Test Accuracies:")
653
+ for i, (ta, te) in enumerate(zip(fold_train_accs, fold_accuracies), 1):
654
+ print(f" Fold {i}: Train {ta:.1%} | Test {te:.1%} | Gap {ta - te:.1%}")
655
+
656
+ print(
657
+ f"\n Average Training Accuracy: {fold_train_accs.mean():.4f} "
658
+ f"(+/- {fold_train_accs.std():.4f})"
659
+ )
660
+ print(
661
+ f" Average Testing Accuracy: {fold_accuracies.mean():.4f} "
662
+ f"(+/- {fold_accuracies.std():.4f})"
663
+ )
664
+ print(
665
+ f" Average Precision: {fold_precisions.mean():.4f} "
666
+ f"(+/- {fold_precisions.std():.4f})"
667
+ )
668
+ print(
669
+ f" Average Recall: {fold_recalls.mean():.4f} "
670
+ f"(+/- {fold_recalls.std():.4f})"
671
+ )
672
+ print(
673
+ f" Average F1 Score: {fold_f1s.mean():.4f} "
674
+ f"(+/- {fold_f1s.std():.4f})"
675
+ )
676
+ print(f" Average Gap: {gaps.mean():.4f} " f"(+/- {gaps.std():.4f})")
677
+
678
+ # ── 7. Consistency Check ──
679
+ print("\n" + "=" * 60)
680
+ print(" VERDICT CONSISTENCY & OVERFITTING ANALYSIS")
681
+ print("=" * 60)
682
+
683
+ avg_train = fold_train_accs.mean()
684
+ avg_test = fold_accuracies.mean()
685
+ avg_gap = gaps.mean()
686
+ acc_std = fold_accuracies.std()
687
+
688
+ if avg_train > 0.95 and avg_test < 0.70:
689
+ overfit_status = "OVERFITTING DETECTED"
690
+ print(f"\n *** OVERFITTING DETECTED ***")
691
+ print(f" Average training accuracy ({avg_train:.1%}) is much higher than")
692
+ print(f" average testing accuracy ({avg_test:.1%}).")
693
+ print(f" The model memorizes training data and fails to generalize.")
694
+ elif avg_gap > 0.10:
695
+ overfit_status = "MILD OVERFITTING"
696
+ print(f"\n ** MILD OVERFITTING **")
697
+ print(f" Average gap ({avg_gap:.1%}) exceeds 10%.")
698
+ else:
699
+ overfit_status = "NO OVERFITTING"
700
+ print(f"\n NO OVERFITTING DETECTED")
701
+ print(f" Average gap ({avg_gap:.1%}) is within acceptable range.")
702
+
703
+ if acc_std < 0.01:
704
+ consistency = "HIGHLY CONSISTENT"
705
+ print(f" Verdict Consistency: HIGHLY CONSISTENT (std={acc_std:.4f})")
706
+ print(f" Predictions are very stable across all 5 folds.")
707
+ elif acc_std < 0.03:
708
+ consistency = "CONSISTENT"
709
+ print(f" Verdict Consistency: CONSISTENT (std={acc_std:.4f})")
710
+ print(f" Minor variance across folds — acceptable for production.")
711
+ else:
712
+ consistency = "INCONSISTENT"
713
+ print(f" Verdict Consistency: INCONSISTENT (std={acc_std:.4f})")
714
+ print(f" High variance suggests model stability issues.")
715
+
716
+ # ── 8. Confusion Matrix (aggregated across all folds) ──
717
+ print("\n\nGenerating plots...")
718
+ cm = confusion_matrix(all_y_true, all_y_pred)
719
+ overall_acc = accuracy_score(all_y_true, all_y_pred)
720
+
721
+ fig, ax = plt.subplots(figsize=(8, 6))
722
+ disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=label_names)
723
+ disp.plot(ax=ax, cmap="Blues", values_format="d")
724
+
725
+ ax.set_title(
726
+ f"Confusion Matrix — Aggregated 5-Fold CV\n"
727
+ f"Overall Accuracy: {overall_acc:.1%} | {overfit_status}",
728
+ fontsize=14,
729
+ fontweight="bold",
730
+ )
731
+ ax.set_xlabel("Predicted Label", fontsize=12)
732
+ ax.set_ylabel("True Label", fontsize=12)
733
+
734
+ plt.tight_layout()
735
+ cm_path = os.path.join(OUTPUT_DIR, "confusion_matrix.png")
736
+ fig.savefig(cm_path, dpi=150, bbox_inches="tight")
737
+ print(f" Saved: {cm_path}")
738
+
739
+ # ── 9. Per-Fold Accuracy Bar Chart ──
740
+ fig2, ax2 = plt.subplots(figsize=(10, 5))
741
+
742
+ x = np.arange(5)
743
+ width = 0.35
744
+ bars_train = ax2.bar(
745
+ x - width / 2,
746
+ fold_train_accs * 100,
747
+ width,
748
+ label="Training",
749
+ color="#2196F3",
750
+ edgecolor="black",
751
+ linewidth=0.5,
752
+ )
753
+ bars_test = ax2.bar(
754
+ x + width / 2,
755
+ fold_accuracies * 100,
756
+ width,
757
+ label="Testing",
758
+ color="#FF9800",
759
+ edgecolor="black",
760
+ linewidth=0.5,
761
+ )
762
+
763
+ for bar, val in zip(bars_train, fold_train_accs):
764
+ ax2.text(
765
+ bar.get_x() + bar.get_width() / 2,
766
+ bar.get_height() + 0.3,
767
+ f"{val:.1%}",
768
+ ha="center",
769
+ va="bottom",
770
+ fontsize=9,
771
+ fontweight="bold",
772
+ )
773
+ for bar, val in zip(bars_test, fold_accuracies):
774
+ ax2.text(
775
+ bar.get_x() + bar.get_width() / 2,
776
+ bar.get_height() + 0.3,
777
+ f"{val:.1%}",
778
+ ha="center",
779
+ va="bottom",
780
+ fontsize=9,
781
+ fontweight="bold",
782
+ )
783
+
784
+ ax2.set_xticks(x)
785
+ ax2.set_xticklabels([f"Fold {i}" for i in range(1, 6)])
786
+ ax2.set_ylim(0, 105)
787
+ ax2.set_ylabel("Accuracy (%)", fontsize=12)
788
+ ax2.set_title(
789
+ f"Per-Fold Accuracy Comparison\n"
790
+ f"Avg Test: {avg_test:.1%} (+/- {acc_std:.4f}) | {consistency}",
791
+ fontsize=14,
792
+ fontweight="bold",
793
+ )
794
+ ax2.legend(loc="lower right")
795
+ ax2.axhline(y=70, color="red", linestyle="--", alpha=0.5, label="70% threshold")
796
+
797
+ plt.tight_layout()
798
+ bar_path = os.path.join(OUTPUT_DIR, "accuracy_comparison.png")
799
+ fig2.savefig(bar_path, dpi=150, bbox_inches="tight")
800
+ print(f" Saved: {bar_path}")
801
+
802
+ # ── Final Summary ──
803
+ print("\n" + "=" * 60)
804
+ print(" EVALUATION COMPLETE")
805
+ print("=" * 60)
806
+ print(f" Dataset: fake_news_filipino ({len(df)} articles)")
807
+ print(f" Feature set: {X_full.shape[1]} (TF-IDF + 9 stylometric)")
808
+ print(f" Cross-Validation: 5-Fold Stratified")
809
+ print(f" Avg Training Accuracy: {avg_train:.4f} (+/- {fold_train_accs.std():.4f})")
810
+ print(f" Avg Testing Accuracy: {avg_test:.4f} (+/- {acc_std:.4f})")
811
+ print(f" Avg F1 Score: {fold_f1s.mean():.4f} (+/- {fold_f1s.std():.4f})")
812
+ print(f" Avg Gap: {avg_gap:.4f}")
813
+ print(f" Overfitting Status: {overfit_status}")
814
+ print(f" Verdict Consistency: {consistency}")
815
+ print(f" Plots saved to: {OUTPUT_DIR}/")
816
+ print("=" * 60)
817
+
818
+
819
+ if __name__ == "__main__":
820
+ main()
backend/mine_bias_words.py CHANGED
@@ -1,194 +1,194 @@
1
- """
2
- Chi-Squared Bias Word Miner
3
- ============================
4
- Automatically discovers words most statistically associated with
5
- Fake vs. Real news in your training dataset using chi-squared analysis.
6
-
7
- Outputs:
8
- 1. Full scored table (word, chi2 score, fake freq, real freq)
9
- 2. Ready-to-paste Python lists for bias_analyzer.py
10
-
11
- Usage:
12
- python backend/mine_bias_words.py
13
- """
14
-
15
- import sys
16
- import os
17
- import pandas as pd
18
- from sklearn.feature_extraction.text import CountVectorizer
19
- from sklearn.feature_selection import chi2
20
- import numpy as np
21
-
22
- PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
- sys.path.insert(0, PROJECT_ROOT)
24
-
25
- TOP_N = 60 # How many top words to show per class
26
-
27
-
28
- def load_data():
29
- """Load the same datasets used in training."""
30
- frames = []
31
-
32
- csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
33
- if os.path.exists(csv1):
34
- df1 = pd.read_csv(csv1, skiprows=1)[["article", "label"]].copy()
35
- df1 = df1[~df1["article"].astype(str).str.startswith(("=======", ">>>>>>>"))]
36
- # label column may be StringDtype (git conflict markers) — coerce to int
37
- df1["label"] = pd.to_numeric(df1["label"], errors="coerce")
38
- df1 = df1.dropna(subset=["label"])
39
- df1["label"] = df1["label"].astype(int)
40
- frames.append(df1)
41
- print(
42
- f" jcblaise: {len(df1)} articles (Real={int((df1.label==0).sum())}, Fake={int((df1.label==1).sum())})"
43
- )
44
-
45
- csv2 = os.path.join(
46
- PROJECT_ROOT,
47
- "data",
48
- "raw",
49
- "philippine_corpus",
50
- "Philippine Fake News Corpus.csv",
51
- )
52
- if os.path.exists(csv2):
53
- df2 = pd.read_csv(csv2, skiprows=1)
54
- df2 = df2.rename(columns={"Content": "article"})
55
- df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
56
- df2 = df2[["article", "label"]].dropna().copy()
57
- frames.append(df2)
58
- print(
59
- f" PH Corpus: {len(df2)} articles (Real={int((df2.label==0).sum())}, Fake={int((df2.label==1).sum())})"
60
- )
61
-
62
- df = pd.concat(frames, ignore_index=True).dropna(subset=["article", "label"])
63
- df = df[df["article"].astype(str).str.len() > 0].copy()
64
- return df
65
-
66
-
67
- def mine_words(df, top_n=TOP_N):
68
- texts = df["article"].fillna("").astype(str).tolist()
69
- labels = df["label"].tolist()
70
-
71
- print(f"\nBuilding vocabulary from {len(texts)} articles...")
72
- vec = CountVectorizer(
73
- max_features=20000,
74
- ngram_range=(1, 2),
75
- min_df=5,
76
- max_df=0.85,
77
- stop_words=None, # keep Filipino/Tagalog words too
78
- )
79
- X = vec.fit_transform(texts)
80
- feature_names = vec.get_feature_names_out()
81
-
82
- print("Running chi-squared analysis...")
83
- chi2_scores, _ = chi2(X, labels)
84
- sorted_idx = np.argsort(chi2_scores)[::-1]
85
-
86
- X_array = X.toarray()
87
- real_mask = [l == 0 for l in labels]
88
- fake_mask = [l == 1 for l in labels]
89
- real_mean = X_array[real_mask].mean(axis=0)
90
- fake_mean = X_array[fake_mask].mean(axis=0)
91
-
92
- fake_words, real_words = [], []
93
- for idx in sorted_idx[: top_n * 3]:
94
- word = feature_names[idx]
95
- score = chi2_scores[idx]
96
- if word.isdigit() or len(word) <= 2:
97
- continue
98
- entry = (word, score, fake_mean[idx], real_mean[idx])
99
- if fake_mean[idx] > real_mean[idx]:
100
- fake_words.append(entry)
101
- else:
102
- real_words.append((word, score, real_mean[idx], fake_mean[idx]))
103
- if len(fake_words) >= top_n and len(real_words) >= top_n:
104
- break
105
-
106
- return fake_words[:top_n], real_words[:top_n]
107
-
108
-
109
- def _py_list(words, var_name, comment):
110
- """Format a word list as a ready-to-paste Python block."""
111
- lines = [f"# {comment}", f"{var_name} = ["]
112
- for i in range(0, len(words), 5):
113
- chunk = words[i : i + 5]
114
- lines.append(" " + ", ".join(f'"{w}"' for w in chunk) + ",")
115
- lines.append("]")
116
- return "\n".join(lines)
117
-
118
-
119
- def main():
120
- print("=" * 65)
121
- print(" CHI-SQUARED BIAS WORD MINER")
122
- print("=" * 65)
123
-
124
- df = load_data()
125
- print(
126
- f"\nTotal: {len(df)} articles | Real: {int((df.label==0).sum())} | Fake: {int((df.label==1).sum())}"
127
- )
128
-
129
- fake_words, real_words = mine_words(df)
130
-
131
- # ── Scored table ───────────────────────────────────────────────────
132
- print("\n" + "=" * 65)
133
- print(f" TOP {TOP_N} WORDS ASSOCIATED WITH *** FAKE NEWS ***")
134
- print("=" * 65)
135
- for word, score, f_avg, r_avg in fake_words:
136
- bar = "█" * min(20, int(score / 200))
137
- print(f" {word:<28} χ²={score:>8.0f} {bar}")
138
-
139
- print("\n" + "=" * 65)
140
- print(f" TOP {TOP_N} WORDS ASSOCIATED WITH *** REAL NEWS ***")
141
- print("=" * 65)
142
- for word, score, r_avg, f_avg in real_words:
143
- bar = "█" * min(20, int(score / 200))
144
- print(f" {word:<28} χ²={score:>8.0f} {bar}")
145
-
146
- # ── Ready-to-paste blocks ──────────────────────────────────────────
147
- fake_terms = [w for w, *_ in fake_words]
148
- real_terms = [w for w, *_ in real_words]
149
-
150
- print("\n\n" + "=" * 65)
151
- print(" ✂ COPY-PASTE INTO bias_analyzer.py")
152
- print("=" * 65)
153
-
154
- print(
155
- "\n"
156
- + _py_list(
157
- fake_terms[:30],
158
- "SENSATIONAL_ADDITIONS",
159
- "Paste these into SENSATIONAL_KEYWORDS in bias_analyzer.py",
160
- )
161
- )
162
-
163
- print(
164
- "\n"
165
- + _py_list(
166
- fake_terms[30:],
167
- "RIGHT_CANDIDATE_ADDITIONS",
168
- "Review — move political terms to CONSERVATIVE_PRO / RIGHT_LEANING_KEYWORDS",
169
- )
170
- )
171
-
172
- print(
173
- "\n"
174
- + _py_list(
175
- real_terms[:30],
176
- "EVIDENCE_ADDITIONS",
177
- "Paste into EVIDENCE_BASED_MARKERS in bias_analyzer.py",
178
- )
179
- )
180
-
181
- print("\n" + "=" * 65)
182
- print(" TIPS:")
183
- print(" • Words appearing in both tables are not discriminative — skip them")
184
- print(" • Move politician names to CONSERVATIVE_PRO / LIBERAL_PRO instead")
185
- print(" • For political faction slang, use this LLM prompt:")
186
- print()
187
- print(' "Give me 40 Filipino/Tagalog/Bisaya social media terms used by')
188
- print(" (1) pro-Duterte DDS, (2) Kakampink/pro-Leni, (3) PBBM/pro-Marcos")
189
- print(' supporters. Return three plain Python string lists."')
190
- print("=" * 65)
191
-
192
-
193
- if __name__ == "__main__":
194
- main()
 
1
+ """
2
+ Chi-Squared Bias Word Miner
3
+ ============================
4
+ Automatically discovers words most statistically associated with
5
+ Fake vs. Real news in your training dataset using chi-squared analysis.
6
+
7
+ Outputs:
8
+ 1. Full scored table (word, chi2 score, fake freq, real freq)
9
+ 2. Ready-to-paste Python lists for bias_analyzer.py
10
+
11
+ Usage:
12
+ python backend/mine_bias_words.py
13
+ """
14
+
15
+ import sys
16
+ import os
17
+ import pandas as pd
18
+ from sklearn.feature_extraction.text import CountVectorizer
19
+ from sklearn.feature_selection import chi2
20
+ import numpy as np
21
+
22
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
+ sys.path.insert(0, PROJECT_ROOT)
24
+
25
+ TOP_N = 60 # How many top words to show per class
26
+
27
+
28
+ def load_data():
29
+ """Load the same datasets used in training."""
30
+ frames = []
31
+
32
+ csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
33
+ if os.path.exists(csv1):
34
+ df1 = pd.read_csv(csv1, skiprows=1)[["article", "label"]].copy()
35
+ df1 = df1[~df1["article"].astype(str).str.startswith(("=======", ">>>>>>>"))]
36
+ # label column may be StringDtype (git conflict markers) — coerce to int
37
+ df1["label"] = pd.to_numeric(df1["label"], errors="coerce")
38
+ df1 = df1.dropna(subset=["label"])
39
+ df1["label"] = df1["label"].astype(int)
40
+ frames.append(df1)
41
+ print(
42
+ f" jcblaise: {len(df1)} articles (Real={int((df1.label==0).sum())}, Fake={int((df1.label==1).sum())})"
43
+ )
44
+
45
+ csv2 = os.path.join(
46
+ PROJECT_ROOT,
47
+ "data",
48
+ "raw",
49
+ "philippine_corpus",
50
+ "Philippine Fake News Corpus.csv",
51
+ )
52
+ if os.path.exists(csv2):
53
+ df2 = pd.read_csv(csv2, skiprows=1)
54
+ df2 = df2.rename(columns={"Content": "article"})
55
+ df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
56
+ df2 = df2[["article", "label"]].dropna().copy()
57
+ frames.append(df2)
58
+ print(
59
+ f" PH Corpus: {len(df2)} articles (Real={int((df2.label==0).sum())}, Fake={int((df2.label==1).sum())})"
60
+ )
61
+
62
+ df = pd.concat(frames, ignore_index=True).dropna(subset=["article", "label"])
63
+ df = df[df["article"].astype(str).str.len() > 0].copy()
64
+ return df
65
+
66
+
67
+ def mine_words(df, top_n=TOP_N):
68
+ texts = df["article"].fillna("").astype(str).tolist()
69
+ labels = df["label"].tolist()
70
+
71
+ print(f"\nBuilding vocabulary from {len(texts)} articles...")
72
+ vec = CountVectorizer(
73
+ max_features=20000,
74
+ ngram_range=(1, 2),
75
+ min_df=5,
76
+ max_df=0.85,
77
+ stop_words=None, # keep Filipino/Tagalog words too
78
+ )
79
+ X = vec.fit_transform(texts)
80
+ feature_names = vec.get_feature_names_out()
81
+
82
+ print("Running chi-squared analysis...")
83
+ chi2_scores, _ = chi2(X, labels)
84
+ sorted_idx = np.argsort(chi2_scores)[::-1]
85
+
86
+ X_array = X.toarray()
87
+ real_mask = [l == 0 for l in labels]
88
+ fake_mask = [l == 1 for l in labels]
89
+ real_mean = X_array[real_mask].mean(axis=0)
90
+ fake_mean = X_array[fake_mask].mean(axis=0)
91
+
92
+ fake_words, real_words = [], []
93
+ for idx in sorted_idx[: top_n * 3]:
94
+ word = feature_names[idx]
95
+ score = chi2_scores[idx]
96
+ if word.isdigit() or len(word) <= 2:
97
+ continue
98
+ entry = (word, score, fake_mean[idx], real_mean[idx])
99
+ if fake_mean[idx] > real_mean[idx]:
100
+ fake_words.append(entry)
101
+ else:
102
+ real_words.append((word, score, real_mean[idx], fake_mean[idx]))
103
+ if len(fake_words) >= top_n and len(real_words) >= top_n:
104
+ break
105
+
106
+ return fake_words[:top_n], real_words[:top_n]
107
+
108
+
109
+ def _py_list(words, var_name, comment):
110
+ """Format a word list as a ready-to-paste Python block."""
111
+ lines = [f"# {comment}", f"{var_name} = ["]
112
+ for i in range(0, len(words), 5):
113
+ chunk = words[i : i + 5]
114
+ lines.append(" " + ", ".join(f'"{w}"' for w in chunk) + ",")
115
+ lines.append("]")
116
+ return "\n".join(lines)
117
+
118
+
119
+ def main():
120
+ print("=" * 65)
121
+ print(" CHI-SQUARED BIAS WORD MINER")
122
+ print("=" * 65)
123
+
124
+ df = load_data()
125
+ print(
126
+ f"\nTotal: {len(df)} articles | Real: {int((df.label==0).sum())} | Fake: {int((df.label==1).sum())}"
127
+ )
128
+
129
+ fake_words, real_words = mine_words(df)
130
+
131
+ # ── Scored table ───────────────────────────────────────────────────
132
+ print("\n" + "=" * 65)
133
+ print(f" TOP {TOP_N} WORDS ASSOCIATED WITH *** FAKE NEWS ***")
134
+ print("=" * 65)
135
+ for word, score, f_avg, r_avg in fake_words:
136
+ bar = "█" * min(20, int(score / 200))
137
+ print(f" {word:<28} χ²={score:>8.0f} {bar}")
138
+
139
+ print("\n" + "=" * 65)
140
+ print(f" TOP {TOP_N} WORDS ASSOCIATED WITH *** REAL NEWS ***")
141
+ print("=" * 65)
142
+ for word, score, r_avg, f_avg in real_words:
143
+ bar = "█" * min(20, int(score / 200))
144
+ print(f" {word:<28} χ²={score:>8.0f} {bar}")
145
+
146
+ # ── Ready-to-paste blocks ──────────────────────────────────────────
147
+ fake_terms = [w for w, *_ in fake_words]
148
+ real_terms = [w for w, *_ in real_words]
149
+
150
+ print("\n\n" + "=" * 65)
151
+ print(" ✂ COPY-PASTE INTO bias_analyzer.py")
152
+ print("=" * 65)
153
+
154
+ print(
155
+ "\n"
156
+ + _py_list(
157
+ fake_terms[:30],
158
+ "SENSATIONAL_ADDITIONS",
159
+ "Paste these into SENSATIONAL_KEYWORDS in bias_analyzer.py",
160
+ )
161
+ )
162
+
163
+ print(
164
+ "\n"
165
+ + _py_list(
166
+ fake_terms[30:],
167
+ "RIGHT_CANDIDATE_ADDITIONS",
168
+ "Review — move political terms to CONSERVATIVE_PRO / RIGHT_LEANING_KEYWORDS",
169
+ )
170
+ )
171
+
172
+ print(
173
+ "\n"
174
+ + _py_list(
175
+ real_terms[:30],
176
+ "EVIDENCE_ADDITIONS",
177
+ "Paste into EVIDENCE_BASED_MARKERS in bias_analyzer.py",
178
+ )
179
+ )
180
+
181
+ print("\n" + "=" * 65)
182
+ print(" TIPS:")
183
+ print(" • Words appearing in both tables are not discriminative — skip them")
184
+ print(" • Move politician names to CONSERVATIVE_PRO / LIBERAL_PRO instead")
185
+ print(" • For political faction slang, use this LLM prompt:")
186
+ print()
187
+ print(' "Give me 40 Filipino/Tagalog/Bisaya social media terms used by')
188
+ print(" (1) pro-Duterte DDS, (2) Kakampink/pro-Leni, (3) PBBM/pro-Marcos")
189
+ print(' supporters. Return three plain Python string lists."')
190
+ print("=" * 65)
191
+
192
+
193
+ if __name__ == "__main__":
194
+ main()
backend/tune_rf.py ADDED
@@ -0,0 +1,333 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hybrid RF Hyperparameter Tuner
3
+ ================================
4
+ Systematically tests different Random Forest configurations on
5
+ the hybrid feature set (TF-IDF + MiniLM + Stylometric) to find
6
+ the settings that beat the Linear SVM baseline (94.89% accuracy).
7
+
8
+ Varies:
9
+ • tfidf_features : number of TF-IDF vocabulary features (2000, 5000, 10000)
10
+ • max_depth : RF tree depth (None=unlimited, 30, 50)
11
+ • max_features_rf : features sampled per split ('sqrt', 'log2', 0.15, 0.25)
12
+
13
+ Output:
14
+ evaluation_results/tune_rf_results.txt — all trial scores
15
+ evaluation_results/tune_rf_heatmap.png — accuracy heatmap
16
+
17
+ Usage:
18
+ python backend/tune_rf.py
19
+ """
20
+
21
+ import sys, os, time, warnings
22
+ warnings.filterwarnings("ignore")
23
+
24
+ PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
25
+ sys.path.insert(0, PROJECT_ROOT)
26
+
27
+ import numpy as np
28
+ import matplotlib
29
+ matplotlib.use("Agg")
30
+ import matplotlib.pyplot as plt
31
+ from scipy.sparse import hstack, csr_matrix
32
+ from sklearn.model_selection import train_test_split
33
+ from sklearn.feature_extraction.text import TfidfVectorizer
34
+ from sklearn.preprocessing import StandardScaler
35
+ from sklearn.ensemble import RandomForestClassifier
36
+ from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
37
+
38
+ from backend.train import (
39
+ load_fake_news_dataset,
40
+ preprocess,
41
+ extract_stylometric_features,
42
+ get_minilm_model,
43
+ STYLOMETRIC_FEATURE_NAMES,
44
+ )
45
+
46
+ OUTPUT_DIR = os.path.join(PROJECT_ROOT, "evaluation_results")
47
+ os.makedirs(OUTPUT_DIR, exist_ok=True)
48
+
49
+ SVM_BASELINE_ACC = 0.9489 # target to beat
50
+ SVM_BASELINE_F1 = 0.9489
51
+
52
+ # ── Grid ──────────────────────────────────────────────────────────────────
53
+ TFIDF_SIZES = [2000, 5000, 10000]
54
+ MAX_DEPTHS = [None, 30, 50]
55
+ MAX_FEATURES_RF = ["sqrt", "log2", 0.15, 0.25]
56
+
57
+
58
+ def build_hybrid(X_train, X_test, tfidf_size, feature_multiplier=1.0):
59
+ """Build TF-IDF + MiniLM + Stylometric features.
60
+ feature_multiplier scales the dense (MiniLM + Stylo) columns so they
61
+ are not drowned out by the sparse TF-IDF block when max_features is low.
62
+ """
63
+ tfidf = TfidfVectorizer(
64
+ max_features=tfidf_size,
65
+ ngram_range=(1, 3),
66
+ min_df=2,
67
+ max_df=0.95,
68
+ sublinear_tf=True,
69
+ )
70
+ X_tr_tfidf = tfidf.fit_transform(X_train)
71
+ X_te_tfidf = tfidf.transform(X_test)
72
+
73
+ minilm = get_minilm_model()
74
+ emb_tr = minilm.encode(X_train, show_progress_bar=False, batch_size=64)
75
+ emb_te = minilm.encode(X_test, show_progress_bar=False, batch_size=64)
76
+
77
+ stylo_tr = np.array([extract_stylometric_features(t) for t in X_train])
78
+ stylo_te = np.array([extract_stylometric_features(t) for t in X_test])
79
+ scaler = StandardScaler()
80
+ stylo_tr = scaler.fit_transform(stylo_tr)
81
+ stylo_te = scaler.transform(stylo_te)
82
+
83
+ # Scale dense features to give RF a fairer chance of picking them
84
+ dense_tr = np.hstack([emb_tr, stylo_tr]) * feature_multiplier
85
+ dense_te = np.hstack([emb_te, stylo_te]) * feature_multiplier
86
+
87
+ X_tr = hstack([X_tr_tfidf, csr_matrix(dense_tr)])
88
+ X_te = hstack([X_te_tfidf, csr_matrix(dense_te)])
89
+ return X_tr, X_te
90
+
91
+
92
+ def main():
93
+ print("=" * 65)
94
+ print(" HYBRID RF HYPERPARAMETER TUNER")
95
+ print(f" Target to beat: SVM Accuracy={SVM_BASELINE_ACC:.4f} F1={SVM_BASELINE_F1:.4f}")
96
+ print("=" * 65)
97
+
98
+ # ── Load data once ───────────────────────────────────────────────────
99
+ print("\nLoading dataset (mixed mode) …")
100
+ df = load_fake_news_dataset()
101
+ X_all, y_all = preprocess(df, undersample=False, oversample=True)
102
+
103
+ X_train, X_test, y_train, y_test = train_test_split(
104
+ X_all, y_all, test_size=0.20, random_state=42, stratify=y_all
105
+ )
106
+ y_train_arr = np.array(y_train)
107
+ y_test_arr = np.array(y_test)
108
+
109
+ print(f" Train: {len(X_train):,} | Test: {len(X_test):,}")
110
+
111
+ # ── Encode MiniLM once for the LARGEST tfidf size run —————————————
112
+ # We re-encode per tfidf_size but cache embeddings since they don't change
113
+ print("\nPre-encoding texts with MiniLM (done once, reused across all trials) …")
114
+ minilm = get_minilm_model()
115
+ emb_tr = minilm.encode(X_train, show_progress_bar=True, batch_size=64)
116
+ emb_te = minilm.encode(X_test, show_progress_bar=True, batch_size=64)
117
+
118
+ stylo_tr = np.array([extract_stylometric_features(t) for t in X_train])
119
+ stylo_te = np.array([extract_stylometric_features(t) for t in X_test])
120
+ scaler = StandardScaler()
121
+ stylo_tr = scaler.fit_transform(stylo_tr)
122
+ stylo_te = scaler.transform(stylo_te)
123
+
124
+ print(f" MiniLM embeddings: {emb_tr.shape} | Stylometric: {stylo_tr.shape}")
125
+
126
+ # ── Grid search ──────────────────────────────────────────────────────
127
+ results = []
128
+ trial = 0
129
+ total = len(TFIDF_SIZES) * len(MAX_DEPTHS) * len(MAX_FEATURES_RF)
130
+
131
+ print(f"\nRunning {total} trials …\n")
132
+ print(f" {'#':>3} {'TFIDF':>6} {'MaxDepth':>9} {'MaxFeat':>8} "
133
+ f"{'Accuracy':>9} {'F1(Wtd)':>8} {'AUC':>7} {'Time':>6} Beat?")
134
+ print(" " + "-" * 75)
135
+
136
+ best_acc = 0.0
137
+ best_cfg = {}
138
+
139
+ for tfidf_sz in TFIDF_SIZES:
140
+ # Build TF-IDF for this grid row (embeddings already cached)
141
+ tfidf = TfidfVectorizer(
142
+ max_features=tfidf_sz,
143
+ ngram_range=(1, 3),
144
+ min_df=2,
145
+ max_df=0.95,
146
+ sublinear_tf=True,
147
+ )
148
+ X_tr_tfidf = tfidf.fit_transform(X_train)
149
+ X_te_tfidf = tfidf.transform(X_test)
150
+
151
+ # Combine with cached dense features
152
+ # Feature multiplier: scale dense block so RF sampling
153
+ # has a realistic chance of selecting it
154
+ # Multiplier = sqrt(tfidf_sz / n_dense) approximates equal
155
+ # representation across sqrt-sampling
156
+ n_dense = emb_tr.shape[1] + stylo_tr.shape[1] # 384+25 = 409
157
+ multiplier = max(1.0, (tfidf_sz / n_dense) ** 0.5)
158
+
159
+ dense_tr = np.hstack([emb_tr, stylo_tr]) * multiplier
160
+ dense_te = np.hstack([emb_te, stylo_te]) * multiplier
161
+
162
+ X_tr = hstack([X_tr_tfidf, csr_matrix(dense_tr)])
163
+ X_te = hstack([X_te_tfidf, csr_matrix(dense_te)])
164
+
165
+ for max_depth in MAX_DEPTHS:
166
+ for max_feat in MAX_FEATURES_RF:
167
+ trial += 1
168
+ depth_label = str(max_depth) if max_depth else "None"
169
+
170
+ t0 = time.time()
171
+ rf = RandomForestClassifier(
172
+ n_estimators=300,
173
+ max_depth=max_depth,
174
+ min_samples_split=5,
175
+ min_samples_leaf=3,
176
+ max_features=max_feat,
177
+ class_weight="balanced",
178
+ n_jobs=-1,
179
+ random_state=42,
180
+ )
181
+ rf.fit(X_tr, y_train_arr)
182
+ y_pred = rf.predict(X_te)
183
+ proba = rf.predict_proba(X_te)
184
+ elapsed = time.time() - t0
185
+
186
+ acc = accuracy_score(y_test_arr, y_pred)
187
+ f1 = f1_score(y_test_arr, y_pred, average="weighted", zero_division=0)
188
+ try:
189
+ auc = roc_auc_score(y_test_arr, proba[:, 1])
190
+ except Exception:
191
+ auc = float("nan")
192
+
193
+ beat = "✓ BEAT IT!" if acc > SVM_BASELINE_ACC else ""
194
+ marker = ">>>" if acc > SVM_BASELINE_ACC else " "
195
+
196
+ row = {
197
+ "trial": trial,
198
+ "tfidf_sz": tfidf_sz,
199
+ "max_depth": max_depth,
200
+ "max_feat": max_feat,
201
+ "accuracy": acc,
202
+ "f1": f1,
203
+ "auc": auc,
204
+ "time": elapsed,
205
+ "multiplier": multiplier,
206
+ }
207
+ results.append(row)
208
+
209
+ if acc > best_acc:
210
+ best_acc = acc
211
+ best_cfg = row.copy()
212
+
213
+ mf_str = f"{max_feat:.2f}" if isinstance(max_feat, float) else max_feat
214
+ print(
215
+ f"{marker} {trial:>3} {tfidf_sz:>6,} {depth_label:>9} {mf_str:>8} "
216
+ f"{acc:>9.4f} {f1:>8.4f} {auc:>7.4f} {elapsed:>5.1f}s {beat}"
217
+ )
218
+
219
+ # ── Summary ──────────────────────────────────────────────────────────
220
+ print("\n" + "=" * 65)
221
+ print(" BEST CONFIGURATION FOUND")
222
+ print("=" * 65)
223
+ depth_label = str(best_cfg["max_depth"]) if best_cfg["max_depth"] else "None"
224
+ mf_label = (f"{best_cfg['max_feat']:.2f}"
225
+ if isinstance(best_cfg["max_feat"], float)
226
+ else best_cfg["max_feat"])
227
+ print(f" TF-IDF features : {best_cfg['tfidf_sz']:,}")
228
+ print(f" max_depth : {depth_label}")
229
+ print(f" max_features : {mf_label}")
230
+ print(f" Dense multiplier: {best_cfg['multiplier']:.2f}")
231
+ print(f" Accuracy : {best_cfg['accuracy']:.4f} "
232
+ f"({'BEATS SVM ✓' if best_cfg['accuracy'] > SVM_BASELINE_ACC else 'below SVM ✗'})")
233
+ print(f" F1 Weighted : {best_cfg['f1']:.4f}")
234
+ print(f" AUC-ROC : {best_cfg['auc']:.4f}")
235
+
236
+ # ── Save text report ─────────────────────────────────────────────────
237
+ lines = []
238
+ lines.append("=" * 65)
239
+ lines.append(" HYBRID RF TUNING RESULTS")
240
+ lines.append(f" Target: SVM Acc={SVM_BASELINE_ACC:.4f} F1={SVM_BASELINE_F1:.4f}")
241
+ lines.append("=" * 65)
242
+ lines.append(f"\n {'#':>3} {'TFIDF':>6} {'MaxDepth':>9} {'MaxFeat':>8} "
243
+ f"{'Accuracy':>9} {'F1(Wtd)':>8} {'AUC':>7} {'Time':>6}")
244
+ lines.append(" " + "-" * 70)
245
+ for r in results:
246
+ d = str(r["max_depth"]) if r["max_depth"] else "None"
247
+ mf = f"{r['max_feat']:.2f}" if isinstance(r["max_feat"], float) else r["max_feat"]
248
+ b = " ← BEST" if r == best_cfg else ""
249
+ lines.append(
250
+ f" {r['trial']:>3} {r['tfidf_sz']:>6,} {d:>9} {mf:>8} "
251
+ f"{r['accuracy']:>9.4f} {r['f1']:>8.4f} {r['auc']:>7.4f} "
252
+ f"{r['time']:>5.1f}s{b}"
253
+ )
254
+
255
+ lines.append("\n" + "=" * 65)
256
+ lines.append(" RECOMMENDATION FOR train.py")
257
+ lines.append("=" * 65)
258
+ depth_label = str(best_cfg["max_depth"]) if best_cfg["max_depth"] else "None (unlimited)"
259
+ mf_label = (f"{best_cfg['max_feat']:.2f}"
260
+ if isinstance(best_cfg["max_feat"], float)
261
+ else f"'{best_cfg['max_feat']}'")
262
+ lines.append(f"""
263
+ In backend/train.py → build_features():
264
+ TfidfVectorizer(max_features={best_cfg['tfidf_sz']}, ...)
265
+
266
+ In backend/train.py → train_model():
267
+ RandomForestClassifier(
268
+ n_estimators=500,
269
+ max_depth={best_cfg['max_depth']},
270
+ min_samples_split=5,
271
+ min_samples_leaf=3,
272
+ max_features={mf_label},
273
+ class_weight='balanced',
274
+ n_jobs=-1,
275
+ random_state=42,
276
+ )
277
+ """)
278
+
279
+ txt = "\n".join(lines)
280
+ print(txt)
281
+ out_txt = os.path.join(OUTPUT_DIR, "tune_rf_results.txt")
282
+ with open(out_txt, "w", encoding="utf-8") as f:
283
+ f.write(txt)
284
+ print(f"\n Saved: {out_txt}")
285
+
286
+ # ── Heatmap: Accuracy by (tfidf_size x max_depth) for best max_feat ──
287
+ try:
288
+ best_mf = best_cfg["max_feat"]
289
+ sub = [r for r in results if r["max_feat"] == best_mf]
290
+ depths_u = [None, 30, 50]
291
+ tfidf_u = TFIDF_SIZES
292
+ grid = np.zeros((len(depths_u), len(tfidf_u)))
293
+ for r in sub:
294
+ di = depths_u.index(r["max_depth"])
295
+ ti = tfidf_u.index(r["tfidf_sz"])
296
+ grid[di, ti] = r["accuracy"]
297
+
298
+ fig, ax = plt.subplots(figsize=(8, 5))
299
+ im = ax.imshow(grid, cmap="RdYlGn", vmin=0.85, vmax=1.0)
300
+ ax.set_xticks(range(len(tfidf_u)))
301
+ ax.set_xticklabels([f"{t:,}" for t in tfidf_u])
302
+ ax.set_yticks(range(len(depths_u)))
303
+ ax.set_yticklabels(["None (unlimited)", "30", "50"])
304
+ ax.set_xlabel("TF-IDF Vocabulary Size", fontsize=12)
305
+ ax.set_ylabel("max_depth", fontsize=12)
306
+ mf_str = f"{best_mf:.2f}" if isinstance(best_mf, float) else best_mf
307
+ ax.set_title(
308
+ f"Hybrid RF Accuracy Heatmap (max_features='{mf_str}')\n"
309
+ f"Red < {SVM_BASELINE_ACC:.0%} = below SVM baseline Green = above SVM baseline",
310
+ fontsize=11, fontweight="bold"
311
+ )
312
+ # Annotate cells
313
+ for i in range(len(depths_u)):
314
+ for j in range(len(tfidf_u)):
315
+ v = grid[i, j]
316
+ ax.text(j, i, f"{v:.4f}", ha="center", va="center",
317
+ fontsize=11, fontweight="bold",
318
+ color="white" if v < 0.90 else "black")
319
+ fig.colorbar(im, ax=ax, label="Accuracy")
320
+ plt.axhline(-0.5, color="gray")
321
+ plt.tight_layout()
322
+ hm_path = os.path.join(OUTPUT_DIR, "tune_rf_heatmap.png")
323
+ fig.savefig(hm_path, dpi=150, bbox_inches="tight")
324
+ plt.close(fig)
325
+ print(f" Saved: {hm_path}")
326
+ except Exception as e:
327
+ print(f" (Heatmap skipped: {e})")
328
+
329
+ print("\n All done. Check evaluation_results/ for the full report.")
330
+
331
+
332
+ if __name__ == "__main__":
333
+ main()
check_app/.flutter-plugins-dependencies ADDED
@@ -0,0 +1 @@
 
 
1
+ {"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_ios-6.4.1\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"jni","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\jni-1.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\jni_flutter-1.0.1\\\\","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_android-2.3.1\\\\","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"shared_preferences_android","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_android-2.4.21\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_android-6.3.28\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"path_provider_foundation","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_foundation-2.6.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_foundation-2.5.6\\\\","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_macos-3.2.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"jni","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\jni-1.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_linux-2.2.1\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_linux-2.4.1\\\\","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"url_launcher_linux","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_linux-3.2.2\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"jni","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\jni-1.0.0\\\\","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\path_provider_windows-2.3.0\\\\","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_windows-2.4.1\\\\","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"url_launcher_windows","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_windows-3.1.5\\\\","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"shared_preferences_web","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\shared_preferences_web-2.4.3\\\\","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"C:\\\\Users\\\\Carl P\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\url_launcher_web-2.4.2\\\\","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-04-10 23:20:38.632272","version":"3.41.2","swift_package_manager_enabled":{"ios":false,"macos":false}}
check_app/.gitignore CHANGED
@@ -1,45 +1,45 @@
1
- # Miscellaneous
2
- *.class
3
- *.log
4
- *.pyc
5
- *.swp
6
- .DS_Store
7
- .atom/
8
- .build/
9
- .buildlog/
10
- .history
11
- .svn/
12
- .swiftpm/
13
- migrate_working_dir/
14
-
15
- # IntelliJ related
16
- *.iml
17
- *.ipr
18
- *.iws
19
- .idea/
20
-
21
- # The .vscode folder contains launch configuration and tasks you configure in
22
- # VS Code which you may wish to be included in version control, so this line
23
- # is commented out by default.
24
- #.vscode/
25
-
26
- # Flutter/Dart/Pub related
27
- **/doc/api/
28
- **/ios/Flutter/.last_build_id
29
- .dart_tool/
30
- .flutter-plugins-dependencies
31
- .pub-cache/
32
- .pub/
33
- /build/
34
- /coverage/
35
-
36
- # Symbolication related
37
- app.*.symbols
38
-
39
- # Obfuscation related
40
- app.*.map.json
41
-
42
- # Android Studio will place build artifacts here
43
- /android/app/debug
44
- /android/app/profile
45
- /android/app/release
 
1
+ # Miscellaneous
2
+ *.class
3
+ *.log
4
+ *.pyc
5
+ *.swp
6
+ .DS_Store
7
+ .atom/
8
+ .build/
9
+ .buildlog/
10
+ .history
11
+ .svn/
12
+ .swiftpm/
13
+ migrate_working_dir/
14
+
15
+ # IntelliJ related
16
+ *.iml
17
+ *.ipr
18
+ *.iws
19
+ .idea/
20
+
21
+ # The .vscode folder contains launch configuration and tasks you configure in
22
+ # VS Code which you may wish to be included in version control, so this line
23
+ # is commented out by default.
24
+ #.vscode/
25
+
26
+ # Flutter/Dart/Pub related
27
+ **/doc/api/
28
+ **/ios/Flutter/.last_build_id
29
+ .dart_tool/
30
+ .flutter-plugins-dependencies
31
+ .pub-cache/
32
+ .pub/
33
+ /build/
34
+ /coverage/
35
+
36
+ # Symbolication related
37
+ app.*.symbols
38
+
39
+ # Obfuscation related
40
+ app.*.map.json
41
+
42
+ # Android Studio will place build artifacts here
43
+ /android/app/debug
44
+ /android/app/profile
45
+ /android/app/release
check_app/.metadata CHANGED
@@ -1,45 +1,45 @@
1
- # This file tracks properties of this Flutter project.
2
- # Used by Flutter tool to assess capabilities and perform upgrades etc.
3
- #
4
- # This file should be version controlled and should not be manually edited.
5
-
6
- version:
7
- revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
8
- channel: "stable"
9
-
10
- project_type: app
11
-
12
- # Tracks metadata for the flutter migrate command
13
- migration:
14
- platforms:
15
- - platform: root
16
- create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
17
- base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
18
- - platform: android
19
- create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
20
- base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
21
- - platform: ios
22
- create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
23
- base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
24
- - platform: linux
25
- create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
26
- base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
27
- - platform: macos
28
- create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
29
- base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
30
- - platform: web
31
- create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
32
- base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
33
- - platform: windows
34
- create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
35
- base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
36
-
37
- # User provided section
38
-
39
- # List of Local paths (relative to this file) that should be
40
- # ignored by the migrate tool.
41
- #
42
- # Files that are not part of the templates will be ignored by default.
43
- unmanaged_files:
44
- - 'lib/main.dart'
45
- - 'ios/Runner.xcodeproj/project.pbxproj'
 
1
+ # This file tracks properties of this Flutter project.
2
+ # Used by Flutter tool to assess capabilities and perform upgrades etc.
3
+ #
4
+ # This file should be version controlled and should not be manually edited.
5
+
6
+ version:
7
+ revision: "90673a4eef275d1a6692c26ac80d6d746d41a73a"
8
+ channel: "stable"
9
+
10
+ project_type: app
11
+
12
+ # Tracks metadata for the flutter migrate command
13
+ migration:
14
+ platforms:
15
+ - platform: root
16
+ create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
17
+ base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
18
+ - platform: android
19
+ create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
20
+ base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
21
+ - platform: ios
22
+ create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
23
+ base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
24
+ - platform: linux
25
+ create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
26
+ base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
27
+ - platform: macos
28
+ create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
29
+ base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
30
+ - platform: web
31
+ create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
32
+ base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
33
+ - platform: windows
34
+ create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
35
+ base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
36
+
37
+ # User provided section
38
+
39
+ # List of Local paths (relative to this file) that should be
40
+ # ignored by the migrate tool.
41
+ #
42
+ # Files that are not part of the templates will be ignored by default.
43
+ unmanaged_files:
44
+ - 'lib/main.dart'
45
+ - 'ios/Runner.xcodeproj/project.pbxproj'
check_app/README.md CHANGED
@@ -1,17 +1,17 @@
1
- # check_app
2
-
3
- A new Flutter project.
4
-
5
- ## Getting Started
6
-
7
- This project is a starting point for a Flutter application.
8
-
9
- A few resources to get you started if this is your first Flutter project:
10
-
11
- - [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
12
- - [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
13
- - [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
14
-
15
- For help getting started with Flutter development, view the
16
- [online documentation](https://docs.flutter.dev/), which offers tutorials,
17
- samples, guidance on mobile development, and a full API reference.
 
1
+ # check_app
2
+
3
+ A new Flutter project.
4
+
5
+ ## Getting Started
6
+
7
+ This project is a starting point for a Flutter application.
8
+
9
+ A few resources to get you started if this is your first Flutter project:
10
+
11
+ - [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
12
+ - [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
13
+ - [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
14
+
15
+ For help getting started with Flutter development, view the
16
+ [online documentation](https://docs.flutter.dev/), which offers tutorials,
17
+ samples, guidance on mobile development, and a full API reference.
check_app/analysis_options.yaml CHANGED
@@ -1,28 +1,28 @@
1
- # This file configures the analyzer, which statically analyzes Dart code to
2
- # check for errors, warnings, and lints.
3
- #
4
- # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5
- # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6
- # invoked from the command line by running `flutter analyze`.
7
-
8
- # The following line activates a set of recommended lints for Flutter apps,
9
- # packages, and plugins designed to encourage good coding practices.
10
- include: package:flutter_lints/flutter.yaml
11
-
12
- linter:
13
- # The lint rules applied to this project can be customized in the
14
- # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15
- # included above or to enable additional rules. A list of all available lints
16
- # and their documentation is published at https://dart.dev/lints.
17
- #
18
- # Instead of disabling a lint rule for the entire project in the
19
- # section below, it can also be suppressed for a single line of code
20
- # or a specific dart file by using the `// ignore: name_of_lint` and
21
- # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22
- # producing the lint.
23
- rules:
24
- # avoid_print: false # Uncomment to disable the `avoid_print` rule
25
- # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26
-
27
- # Additional information about this file can be found at
28
- # https://dart.dev/guides/language/analysis-options
 
1
+ # This file configures the analyzer, which statically analyzes Dart code to
2
+ # check for errors, warnings, and lints.
3
+ #
4
+ # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5
+ # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6
+ # invoked from the command line by running `flutter analyze`.
7
+
8
+ # The following line activates a set of recommended lints for Flutter apps,
9
+ # packages, and plugins designed to encourage good coding practices.
10
+ include: package:flutter_lints/flutter.yaml
11
+
12
+ linter:
13
+ # The lint rules applied to this project can be customized in the
14
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15
+ # included above or to enable additional rules. A list of all available lints
16
+ # and their documentation is published at https://dart.dev/lints.
17
+ #
18
+ # Instead of disabling a lint rule for the entire project in the
19
+ # section below, it can also be suppressed for a single line of code
20
+ # or a specific dart file by using the `// ignore: name_of_lint` and
21
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22
+ # producing the lint.
23
+ rules:
24
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
25
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26
+
27
+ # Additional information about this file can be found at
28
+ # https://dart.dev/guides/language/analysis-options
check_app/android/.gitignore CHANGED
@@ -1,14 +1,14 @@
1
- gradle-wrapper.jar
2
- /.gradle
3
- /captures/
4
- /gradlew
5
- /gradlew.bat
6
- /local.properties
7
- GeneratedPluginRegistrant.java
8
- .cxx/
9
-
10
- # Remember to never publicly share your keystore.
11
- # See https://flutter.dev/to/reference-keystore
12
- key.properties
13
- **/*.keystore
14
- **/*.jks
 
1
+ gradle-wrapper.jar
2
+ /.gradle
3
+ /captures/
4
+ /gradlew
5
+ /gradlew.bat
6
+ /local.properties
7
+ GeneratedPluginRegistrant.java
8
+ .cxx/
9
+
10
+ # Remember to never publicly share your keystore.
11
+ # See https://flutter.dev/to/reference-keystore
12
+ key.properties
13
+ **/*.keystore
14
+ **/*.jks
check_app/android/.gradle/8.14/checksums/checksums.lock ADDED
Binary file (17 Bytes). View file
 
check_app/android/.gradle/8.14/checksums/md5-checksums.bin ADDED
Binary file (42 kB). View file
 
check_app/android/.gradle/8.14/checksums/sha1-checksums.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8f3080ceac71cb4dc7d292b0ec33179e5e7d6de81a9d50c323e33c721005b474
3
+ size 126215
check_app/android/.gradle/8.14/executionHistory/executionHistory.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:cf6ec48baf09b431f11f0fcc117503713cd528880d5c544427a025f443cd4198
3
+ size 7752405
check_app/android/.gradle/8.14/executionHistory/executionHistory.lock ADDED
Binary file (17 Bytes). View file
 
check_app/android/.gradle/8.14/fileChanges/last-build.bin ADDED
Binary file (1 Bytes). View file
 
check_app/android/.gradle/8.14/fileHashes/fileHashes.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d360ea65f31f6b8ea2d31eac54024c603025ba1ffca7845faea655d8fca82819
3
+ size 518941
check_app/android/.gradle/8.14/fileHashes/fileHashes.lock ADDED
Binary file (17 Bytes). View file
 
check_app/android/.gradle/8.14/fileHashes/resourceHashesCache.bin ADDED
Binary file (24.5 kB). View file
 
check_app/android/.gradle/8.14/gc.properties ADDED
File without changes
check_app/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock ADDED
Binary file (17 Bytes). View file
 
check_app/android/.gradle/buildOutputCleanup/cache.properties ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ #Sun Mar 08 00:19:03 CST 2026
2
+ gradle.version=8.14
check_app/android/.gradle/buildOutputCleanup/outputFiles.bin ADDED
Binary file (74.7 kB). View file
 
check_app/android/.gradle/file-system.probe ADDED
Binary file (8 Bytes). View file
 
check_app/android/.gradle/noVersion/buildLogic.lock ADDED
Binary file (17 Bytes). View file
 
check_app/android/.gradle/vcs-1/gc.properties ADDED
File without changes
check_app/android/app/build.gradle.kts CHANGED
@@ -1,44 +1,44 @@
1
- plugins {
2
- id("com.android.application")
3
- id("kotlin-android")
4
- // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5
- id("dev.flutter.flutter-gradle-plugin")
6
- }
7
-
8
- android {
9
- namespace = "com.example.check_app"
10
- compileSdk = flutter.compileSdkVersion
11
- ndkVersion = flutter.ndkVersion
12
-
13
- compileOptions {
14
- sourceCompatibility = JavaVersion.VERSION_17
15
- targetCompatibility = JavaVersion.VERSION_17
16
- }
17
-
18
- kotlinOptions {
19
- jvmTarget = JavaVersion.VERSION_17.toString()
20
- }
21
-
22
- defaultConfig {
23
- // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24
- applicationId = "com.example.check_app"
25
- // You can update the following values to match your application needs.
26
- // For more information, see: https://flutter.dev/to/review-gradle-config.
27
- minSdk = flutter.minSdkVersion
28
- targetSdk = flutter.targetSdkVersion
29
- versionCode = flutter.versionCode
30
- versionName = flutter.versionName
31
- }
32
-
33
- buildTypes {
34
- release {
35
- // TODO: Add your own signing config for the release build.
36
- // Signing with the debug keys for now, so `flutter run --release` works.
37
- signingConfig = signingConfigs.getByName("debug")
38
- }
39
- }
40
- }
41
-
42
- flutter {
43
- source = "../.."
44
- }
 
1
+ plugins {
2
+ id("com.android.application")
3
+ id("kotlin-android")
4
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5
+ id("dev.flutter.flutter-gradle-plugin")
6
+ }
7
+
8
+ android {
9
+ namespace = "com.bantaypahayag.app"
10
+ compileSdk = flutter.compileSdkVersion
11
+ ndkVersion = flutter.ndkVersion
12
+
13
+ compileOptions {
14
+ sourceCompatibility = JavaVersion.VERSION_17
15
+ targetCompatibility = JavaVersion.VERSION_17
16
+ }
17
+
18
+ kotlinOptions {
19
+ jvmTarget = JavaVersion.VERSION_17.toString()
20
+ }
21
+
22
+ defaultConfig {
23
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
24
+ applicationId = "com.bantaypahayag.app"
25
+ // You can update the following values to match your application needs.
26
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
27
+ minSdk = flutter.minSdkVersion
28
+ targetSdk = flutter.targetSdkVersion
29
+ versionCode = flutter.versionCode
30
+ versionName = flutter.versionName
31
+ }
32
+
33
+ buildTypes {
34
+ release {
35
+ // TODO: Add your own signing config for the release build.
36
+ // Signing with the debug keys for now, so `flutter run --release` works.
37
+ signingConfig = signingConfigs.getByName("debug")
38
+ }
39
+ }
40
+ }
41
+
42
+ flutter {
43
+ source = "../.."
44
+ }
check_app/android/app/src/debug/AndroidManifest.xml CHANGED
@@ -1,7 +1,7 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
- <!-- The INTERNET permission is required for development. Specifically,
3
- the Flutter tool needs it to communicate with the running application
4
- to allow setting breakpoints, to provide hot reload, etc.
5
- -->
6
- <uses-permission android:name="android.permission.INTERNET"/>
7
- </manifest>
 
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- The INTERNET permission is required for development. Specifically,
3
+ the Flutter tool needs it to communicate with the running application
4
+ to allow setting breakpoints, to provide hot reload, etc.
5
+ -->
6
+ <uses-permission android:name="android.permission.INTERNET"/>
7
+ </manifest>
check_app/android/app/src/main/AndroidManifest.xml CHANGED
@@ -1,45 +1,46 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
- <application
3
- android:label="BantayPahayag"
4
- android:name="${applicationName}"
5
- android:icon="@mipmap/ic_launcher">
6
- <activity
7
- android:name=".MainActivity"
8
- android:exported="true"
9
- android:launchMode="singleTop"
10
- android:taskAffinity=""
11
- android:theme="@style/LaunchTheme"
12
- android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
13
- android:hardwareAccelerated="true"
14
- android:windowSoftInputMode="adjustResize">
15
- <!-- Specifies an Android theme to apply to this Activity as soon as
16
- the Android process has started. This theme is visible to the user
17
- while the Flutter UI initializes. After that, this theme continues
18
- to determine the Window background behind the Flutter UI. -->
19
- <meta-data
20
- android:name="io.flutter.embedding.android.NormalTheme"
21
- android:resource="@style/NormalTheme"
22
- />
23
- <intent-filter>
24
- <action android:name="android.intent.action.MAIN"/>
25
- <category android:name="android.intent.category.LAUNCHER"/>
26
- </intent-filter>
27
- </activity>
28
- <!-- Don't delete the meta-data below.
29
- This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
30
- <meta-data
31
- android:name="flutterEmbedding"
32
- android:value="2" />
33
- </application>
34
- <!-- Required to query activities that can process text, see:
35
- https://developer.android.com/training/package-visibility and
36
- https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
37
-
38
- In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
39
- <queries>
40
- <intent>
41
- <action android:name="android.intent.action.PROCESS_TEXT"/>
42
- <data android:mimeType="text/plain"/>
43
- </intent>
44
- </queries>
45
- </manifest>
 
 
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET"/>
3
+ <application
4
+ android:label="BantayPahayag"
5
+ android:name="${applicationName}"
6
+ android:icon="@mipmap/ic_launcher">
7
+ <activity
8
+ android:name=".MainActivity"
9
+ android:exported="true"
10
+ android:launchMode="singleTop"
11
+ android:taskAffinity=""
12
+ android:theme="@style/LaunchTheme"
13
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
14
+ android:hardwareAccelerated="true"
15
+ android:windowSoftInputMode="adjustResize">
16
+ <!-- Specifies an Android theme to apply to this Activity as soon as
17
+ the Android process has started. This theme is visible to the user
18
+ while the Flutter UI initializes. After that, this theme continues
19
+ to determine the Window background behind the Flutter UI. -->
20
+ <meta-data
21
+ android:name="io.flutter.embedding.android.NormalTheme"
22
+ android:resource="@style/NormalTheme"
23
+ />
24
+ <intent-filter>
25
+ <action android:name="android.intent.action.MAIN"/>
26
+ <category android:name="android.intent.category.LAUNCHER"/>
27
+ </intent-filter>
28
+ </activity>
29
+ <!-- Don't delete the meta-data below.
30
+ This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
31
+ <meta-data
32
+ android:name="flutterEmbedding"
33
+ android:value="2" />
34
+ </application>
35
+ <!-- Required to query activities that can process text, see:
36
+ https://developer.android.com/training/package-visibility and
37
+ https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
38
+
39
+ In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
40
+ <queries>
41
+ <intent>
42
+ <action android:name="android.intent.action.PROCESS_TEXT"/>
43
+ <data android:mimeType="text/plain"/>
44
+ </intent>
45
+ </queries>
46
+ </manifest>
check_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package io.flutter.plugins;
2
+
3
+ import androidx.annotation.Keep;
4
+ import androidx.annotation.NonNull;
5
+ import io.flutter.Log;
6
+
7
+ import io.flutter.embedding.engine.FlutterEngine;
8
+
9
+ /**
10
+ * Generated file. Do not edit.
11
+ * This file is generated by the Flutter tool based on the
12
+ * plugins that support the Android platform.
13
+ */
14
+ @Keep
15
+ public final class GeneratedPluginRegistrant {
16
+ private static final String TAG = "GeneratedPluginRegistrant";
17
+ public static void registerWith(@NonNull FlutterEngine flutterEngine) {
18
+ try {
19
+ flutterEngine.getPlugins().add(new com.github.dart_lang.jni.JniPlugin());
20
+ } catch (Exception e) {
21
+ Log.e(TAG, "Error registering plugin jni, com.github.dart_lang.jni.JniPlugin", e);
22
+ }
23
+ try {
24
+ flutterEngine.getPlugins().add(new com.github.dart_lang.jni_flutter.JniFlutterPlugin());
25
+ } catch (Exception e) {
26
+ Log.e(TAG, "Error registering plugin jni_flutter, com.github.dart_lang.jni_flutter.JniFlutterPlugin", e);
27
+ }
28
+ try {
29
+ flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin());
30
+ } catch (Exception e) {
31
+ Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e);
32
+ }
33
+ try {
34
+ flutterEngine.getPlugins().add(new io.flutter.plugins.urllauncher.UrlLauncherPlugin());
35
+ } catch (Exception e) {
36
+ Log.e(TAG, "Error registering plugin url_launcher_android, io.flutter.plugins.urllauncher.UrlLauncherPlugin", e);
37
+ }
38
+ }
39
+ }
check_app/android/app/src/main/kotlin/com/bantaypahayag/app/MainActivity.kt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ package com.bantaypahayag.app
2
+
3
+ import io.flutter.embedding.android.FlutterActivity
4
+
5
+ class MainActivity : FlutterActivity()
check_app/android/app/src/main/res/drawable-v21/launch_background.xml CHANGED
@@ -1,12 +1,12 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <!-- Modify this file to customize your launch splash screen -->
3
- <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
- <item android:drawable="?android:colorBackground" />
5
-
6
- <!-- You can insert your own image assets here -->
7
- <!-- <item>
8
- <bitmap
9
- android:gravity="center"
10
- android:src="@mipmap/launch_image" />
11
- </item> -->
12
- </layer-list>
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Modify this file to customize your launch splash screen -->
3
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item android:drawable="?android:colorBackground" />
5
+
6
+ <!-- You can insert your own image assets here -->
7
+ <!-- <item>
8
+ <bitmap
9
+ android:gravity="center"
10
+ android:src="@mipmap/launch_image" />
11
+ </item> -->
12
+ </layer-list>
check_app/android/app/src/main/res/drawable/launch_background.xml CHANGED
@@ -1,12 +1,12 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <!-- Modify this file to customize your launch splash screen -->
3
- <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
- <item android:drawable="@android:color/white" />
5
-
6
- <!-- You can insert your own image assets here -->
7
- <!-- <item>
8
- <bitmap
9
- android:gravity="center"
10
- android:src="@mipmap/launch_image" />
11
- </item> -->
12
- </layer-list>
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Modify this file to customize your launch splash screen -->
3
+ <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
4
+ <item android:drawable="@android:color/white" />
5
+
6
+ <!-- You can insert your own image assets here -->
7
+ <!-- <item>
8
+ <bitmap
9
+ android:gravity="center"
10
+ android:src="@mipmap/launch_image" />
11
+ </item> -->
12
+ </layer-list>
check_app/android/app/src/main/res/values-night/styles.xml CHANGED
@@ -1,18 +1,18 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <resources>
3
- <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
4
- <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
5
- <!-- Show a splash screen on the activity. Automatically removed when
6
- the Flutter engine draws its first frame -->
7
- <item name="android:windowBackground">@drawable/launch_background</item>
8
- </style>
9
- <!-- Theme applied to the Android Window as soon as the process has started.
10
- This theme determines the color of the Android Window while your
11
- Flutter UI initializes, as well as behind your Flutter UI while its
12
- running.
13
-
14
- This Theme is only used starting with V2 of Flutter's Android embedding. -->
15
- <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
16
- <item name="android:windowBackground">?android:colorBackground</item>
17
- </style>
18
- </resources>
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
4
+ <style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
5
+ <!-- Show a splash screen on the activity. Automatically removed when
6
+ the Flutter engine draws its first frame -->
7
+ <item name="android:windowBackground">@drawable/launch_background</item>
8
+ </style>
9
+ <!-- Theme applied to the Android Window as soon as the process has started.
10
+ This theme determines the color of the Android Window while your
11
+ Flutter UI initializes, as well as behind your Flutter UI while its
12
+ running.
13
+
14
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
15
+ <style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
16
+ <item name="android:windowBackground">?android:colorBackground</item>
17
+ </style>
18
+ </resources>
check_app/android/app/src/main/res/values/styles.xml CHANGED
@@ -1,18 +1,18 @@
1
- <?xml version="1.0" encoding="utf-8"?>
2
- <resources>
3
- <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
4
- <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
5
- <!-- Show a splash screen on the activity. Automatically removed when
6
- the Flutter engine draws its first frame -->
7
- <item name="android:windowBackground">@drawable/launch_background</item>
8
- </style>
9
- <!-- Theme applied to the Android Window as soon as the process has started.
10
- This theme determines the color of the Android Window while your
11
- Flutter UI initializes, as well as behind your Flutter UI while its
12
- running.
13
-
14
- This Theme is only used starting with V2 of Flutter's Android embedding. -->
15
- <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
16
- <item name="android:windowBackground">?android:colorBackground</item>
17
- </style>
18
- </resources>
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
4
+ <style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
5
+ <!-- Show a splash screen on the activity. Automatically removed when
6
+ the Flutter engine draws its first frame -->
7
+ <item name="android:windowBackground">@drawable/launch_background</item>
8
+ </style>
9
+ <!-- Theme applied to the Android Window as soon as the process has started.
10
+ This theme determines the color of the Android Window while your
11
+ Flutter UI initializes, as well as behind your Flutter UI while its
12
+ running.
13
+
14
+ This Theme is only used starting with V2 of Flutter's Android embedding. -->
15
+ <style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
16
+ <item name="android:windowBackground">?android:colorBackground</item>
17
+ </style>
18
+ </resources>
check_app/android/app/src/profile/AndroidManifest.xml CHANGED
@@ -1,7 +1,7 @@
1
- <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
- <!-- The INTERNET permission is required for development. Specifically,
3
- the Flutter tool needs it to communicate with the running application
4
- to allow setting breakpoints, to provide hot reload, etc.
5
- -->
6
- <uses-permission android:name="android.permission.INTERNET"/>
7
- </manifest>
 
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <!-- The INTERNET permission is required for development. Specifically,
3
+ the Flutter tool needs it to communicate with the running application
4
+ to allow setting breakpoints, to provide hot reload, etc.
5
+ -->
6
+ <uses-permission android:name="android.permission.INTERNET"/>
7
+ </manifest>
check_app/android/build.gradle.kts CHANGED
@@ -1,24 +1,24 @@
1
- allprojects {
2
- repositories {
3
- google()
4
- mavenCentral()
5
- }
6
- }
7
-
8
- val newBuildDir: Directory =
9
- rootProject.layout.buildDirectory
10
- .dir("../../build")
11
- .get()
12
- rootProject.layout.buildDirectory.value(newBuildDir)
13
-
14
- subprojects {
15
- val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
16
- project.layout.buildDirectory.value(newSubprojectBuildDir)
17
- }
18
- subprojects {
19
- project.evaluationDependsOn(":app")
20
- }
21
-
22
- tasks.register<Delete>("clean") {
23
- delete(rootProject.layout.buildDirectory)
24
- }
 
1
+ allprojects {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+ }
7
+
8
+ val newBuildDir: Directory =
9
+ rootProject.layout.buildDirectory
10
+ .dir("../../build")
11
+ .get()
12
+ rootProject.layout.buildDirectory.value(newBuildDir)
13
+
14
+ subprojects {
15
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
16
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
17
+ }
18
+ subprojects {
19
+ project.evaluationDependsOn(":app")
20
+ }
21
+
22
+ tasks.register<Delete>("clean") {
23
+ delete(rootProject.layout.buildDirectory)
24
+ }
check_app/android/build/reports/problems/problems-report.html ADDED
The diff for this file is too large to render. See raw diff
 
check_app/android/gradle.properties CHANGED
@@ -1,2 +1,2 @@
1
- org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
2
- android.useAndroidX=true
 
1
+ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
2
+ android.useAndroidX=true
check_app/android/gradle/wrapper/gradle-wrapper.jar ADDED
Binary file (53.6 kB). View file
 
check_app/android/gradle/wrapper/gradle-wrapper.properties CHANGED
@@ -1,5 +1,5 @@
1
- distributionBase=GRADLE_USER_HOME
2
- distributionPath=wrapper/dists
3
- zipStoreBase=GRADLE_USER_HOME
4
- zipStorePath=wrapper/dists
5
- distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
 
1
+ distributionBase=GRADLE_USER_HOME
2
+ distributionPath=wrapper/dists
3
+ zipStoreBase=GRADLE_USER_HOME
4
+ zipStorePath=wrapper/dists
5
+ distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip