fborj commited on
Commit
2aefcc3
·
1 Parent(s): 6d73303

added feature

Browse files
.env.example ADDED
@@ -0,0 +1 @@
 
 
1
+ DATABASE_URL=postgresql://user:password@localhost:5432/factcheck
.gitignore CHANGED
@@ -16,3 +16,6 @@ build/
16
  # OS
17
  .DS_Store
18
  Thumbs.db
 
 
 
 
16
  # OS
17
  .DS_Store
18
  Thumbs.db
19
+
20
+ # Environment
21
+ .env
backend/evaluate_model.py CHANGED
@@ -18,6 +18,7 @@ import re
18
  import time
19
  import numpy as np
20
  from textblob import TextBlob
 
21
 
22
  PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
23
  sys.path.insert(0, PROJECT_ROOT)
@@ -65,15 +66,182 @@ def clean_text(text):
65
  # ───────────────────────────────────────────────────────────
66
 
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  def extract_stylometric_features(text):
69
- """Extract 9 stylometric features from text (matches train.py)."""
70
  if not text or not isinstance(text, str):
71
- return [0.0] * 9
72
 
73
  words = text.split()
74
  token_count = len(words)
75
  if token_count == 0:
76
- return [0.0] * 9
 
 
 
77
 
78
  exclamation_density = text.count("!") / token_count
79
  question_count = text.count("?")
@@ -90,9 +258,9 @@ def extract_stylometric_features(text):
90
  )
91
 
92
  punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
93
- punctuation_density = (punct_chars / len(text)) * 100 if len(text) > 0 else 0
94
 
95
- unique_words = len(set(w.lower() for w in words))
96
  unique_word_ratio = unique_words / token_count
97
 
98
  avg_word_length = sum(len(w) for w in words) / token_count
@@ -102,6 +270,54 @@ def extract_stylometric_features(text):
102
  except Exception:
103
  subjectivity = 0.0
104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  return [
106
  float(exclamation_density),
107
  float(question_count),
@@ -112,6 +328,19 @@ def extract_stylometric_features(text):
112
  float(unique_word_ratio),
113
  float(avg_word_length),
114
  float(subjectivity),
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  ]
116
 
117
 
@@ -125,6 +354,19 @@ STYLOMETRIC_FEATURE_NAMES = [
125
  "unique_word_ratio",
126
  "avg_word_length",
127
  "subjectivity",
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  ]
129
 
130
 
 
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)
 
66
  # ───────────────────────────────────────────────────────────
67
 
68
 
69
+ # ── Word lists for linguistic features ──
70
+ FIRST_PERSON_PRONOUNS = {
71
+ "i",
72
+ "me",
73
+ "my",
74
+ "mine",
75
+ "myself",
76
+ "we",
77
+ "us",
78
+ "our",
79
+ "ours",
80
+ "ourselves",
81
+ "ako",
82
+ "ko",
83
+ "akin",
84
+ "aking",
85
+ "natin",
86
+ "atin",
87
+ "namin",
88
+ "amin",
89
+ "tayo",
90
+ "kami",
91
+ "ta",
92
+ }
93
+
94
+ AUXILIARY_VERBS = {
95
+ "have",
96
+ "has",
97
+ "had",
98
+ "do",
99
+ "does",
100
+ "did",
101
+ "will",
102
+ "would",
103
+ "shall",
104
+ "should",
105
+ "may",
106
+ "might",
107
+ "can",
108
+ "could",
109
+ "must",
110
+ "am",
111
+ "is",
112
+ "are",
113
+ "was",
114
+ "were",
115
+ "be",
116
+ "been",
117
+ "being",
118
+ "ay",
119
+ "dapat",
120
+ "mayroon",
121
+ "meron",
122
+ "maaari",
123
+ "pwede",
124
+ "kailangan",
125
+ }
126
+
127
+ ANALYTICAL_WORDS = {
128
+ "the",
129
+ "a",
130
+ "an",
131
+ "of",
132
+ "in",
133
+ "on",
134
+ "at",
135
+ "to",
136
+ "for",
137
+ "with",
138
+ "by",
139
+ "from",
140
+ "about",
141
+ "between",
142
+ "through",
143
+ "during",
144
+ "before",
145
+ "after",
146
+ "ang",
147
+ "ng",
148
+ "sa",
149
+ "mga",
150
+ "nang",
151
+ "para",
152
+ "tungkol",
153
+ "mula",
154
+ }
155
+
156
+ CERTAINTY_WORDS = {
157
+ "always",
158
+ "never",
159
+ "absolutely",
160
+ "definitely",
161
+ "certainly",
162
+ "undoubtedly",
163
+ "clearly",
164
+ "obviously",
165
+ "without doubt",
166
+ "guaranteed",
167
+ "proven",
168
+ "fact",
169
+ "undeniable",
170
+ "indisputable",
171
+ "every",
172
+ "all",
173
+ "palagi",
174
+ "sigurado",
175
+ "tiyak",
176
+ "talaga",
177
+ "totoo",
178
+ "lagi",
179
+ "walang duda",
180
+ }
181
+
182
+ TENTATIVE_WORDS = {
183
+ "perhaps",
184
+ "maybe",
185
+ "possibly",
186
+ "might",
187
+ "could",
188
+ "likely",
189
+ "unlikely",
190
+ "suggests",
191
+ "appears",
192
+ "seems",
193
+ "allegedly",
194
+ "reportedly",
195
+ "according",
196
+ "probable",
197
+ "approximately",
198
+ "estimated",
199
+ "siguro",
200
+ "marahil",
201
+ "maaaring",
202
+ "mukhang",
203
+ "parang",
204
+ "umano",
205
+ "diumano",
206
+ }
207
+
208
+ CLOUT_WORDS = {
209
+ "must",
210
+ "demand",
211
+ "require",
212
+ "order",
213
+ "command",
214
+ "insist",
215
+ "decree",
216
+ "mandate",
217
+ "authority",
218
+ "power",
219
+ "control",
220
+ "dominant",
221
+ "superior",
222
+ "we must",
223
+ "you must",
224
+ "kailangan",
225
+ "dapat",
226
+ "utos",
227
+ "kapangyarihan",
228
+ "kontrol",
229
+ "mando",
230
+ }
231
+
232
+
233
  def extract_stylometric_features(text):
234
+ """Extract 22 stylometric features from text (matches train.py)."""
235
  if not text or not isinstance(text, str):
236
+ return [0.0] * 22
237
 
238
  words = text.split()
239
  token_count = len(words)
240
  if token_count == 0:
241
+ return [0.0] * 22
242
+
243
+ words_lower = [w.lower() for w in words]
244
+ text_len = len(text)
245
 
246
  exclamation_density = text.count("!") / token_count
247
  question_count = text.count("?")
 
258
  )
259
 
260
  punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
261
+ punctuation_density = (punct_chars / text_len) * 100 if text_len > 0 else 0
262
 
263
+ unique_words = len(set(words_lower))
264
  unique_word_ratio = unique_words / token_count
265
 
266
  avg_word_length = sum(len(w) for w in words) / token_count
 
270
  except Exception:
271
  subjectivity = 0.0
272
 
273
+ try:
274
+ flesch_reading_ease = textstat.flesch_reading_ease(text)
275
+ flesch_kincaid_grade = textstat.flesch_kincaid_grade(text)
276
+ coleman_liau_index = textstat.coleman_liau_index(text)
277
+ ari = textstat.automated_readability_index(text)
278
+ except Exception:
279
+ flesch_reading_ease = 0.0
280
+ flesch_kincaid_grade = 0.0
281
+ coleman_liau_index = 0.0
282
+ ari = 0.0
283
+
284
+ first_person_count = sum(1 for w in words_lower if w in FIRST_PERSON_PRONOUNS)
285
+ first_person_ratio = first_person_count / token_count
286
+
287
+ aux_count = sum(1 for w in words_lower if w in AUXILIARY_VERBS)
288
+ auxiliary_verb_ratio = aux_count / token_count
289
+
290
+ try:
291
+ gunning_fog_index = textstat.gunning_fog(text)
292
+ except Exception:
293
+ gunning_fog_index = 0.0
294
+
295
+ analytical_count = sum(1 for w in words_lower if w in ANALYTICAL_WORDS)
296
+ analytical_thinking = analytical_count / token_count
297
+
298
+ certainty_count = sum(1 for w in words_lower if w in CERTAINTY_WORDS)
299
+ certainty_score = certainty_count / token_count
300
+
301
+ tentative_count = sum(1 for w in words_lower if w in TENTATIVE_WORDS)
302
+ tentative_score = tentative_count / token_count
303
+
304
+ clout_count = sum(1 for w in words_lower if w in CLOUT_WORDS)
305
+ clout_score = clout_count / token_count
306
+
307
+ comma_period_count = text.count(",") + text.count(".")
308
+ comma_period_density = (comma_period_count / text_len) * 100 if text_len > 0 else 0
309
+
310
+ informal_count = (
311
+ text.count("(")
312
+ + text.count(")")
313
+ + text.count("—")
314
+ + text.count("–")
315
+ + text.count("-")
316
+ + text.count("...")
317
+ + text.count("…")
318
+ )
319
+ informal_punct_density = (informal_count / text_len) * 100 if text_len > 0 else 0
320
+
321
  return [
322
  float(exclamation_density),
323
  float(question_count),
 
328
  float(unique_word_ratio),
329
  float(avg_word_length),
330
  float(subjectivity),
331
+ float(flesch_reading_ease),
332
+ float(flesch_kincaid_grade),
333
+ float(coleman_liau_index),
334
+ float(ari),
335
+ float(first_person_ratio),
336
+ float(auxiliary_verb_ratio),
337
+ float(gunning_fog_index),
338
+ float(analytical_thinking),
339
+ float(certainty_score),
340
+ float(tentative_score),
341
+ float(clout_score),
342
+ float(comma_period_density),
343
+ float(informal_punct_density),
344
  ]
345
 
346
 
 
354
  "unique_word_ratio",
355
  "avg_word_length",
356
  "subjectivity",
357
+ "flesch_reading_ease",
358
+ "flesch_kincaid_grade",
359
+ "coleman_liau_index",
360
+ "ari",
361
+ "first_person_ratio",
362
+ "auxiliary_verb_ratio",
363
+ "gunning_fog_index",
364
+ "analytical_thinking",
365
+ "certainty_score",
366
+ "tentative_score",
367
+ "clout_score",
368
+ "comma_period_density",
369
+ "informal_punct_density",
370
  ]
371
 
372
 
backend/train.py CHANGED
@@ -27,6 +27,7 @@ sys.path.insert(0, PROJECT_ROOT)
27
  import pandas as pd
28
  import joblib
29
  from textblob import TextBlob
 
30
  from scipy.sparse import hstack, csr_matrix
31
  from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
32
  from sklearn.feature_extraction.text import TfidfVectorizer
@@ -59,30 +60,222 @@ def clean_text(text):
59
  # ───────────────────────────────────────────────────────────
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def extract_stylometric_features(text):
63
  """Extract linguistic style features from text.
64
 
65
- Features (9 total — Hybrid Feature Set):
66
- 1. exclamation_density — Exclamation marks per word
67
- 2. question_count — Number of '?' characters
68
- 3. caps_ratio — Uppercase Ratio (ALL CAPS words / total words)
69
- 4. avg_sentence_length — Average number of words per sentence
70
- 5. punctuation_density — Punctuation chars per 100 chars
71
- 6. token_count — Total word count (metadata feature)
72
- 7. unique_word_ratio — Unique words / total words (vocabulary richness)
73
- 8. avg_word_length — Average word length in characters
74
- 9. subjectivity — TextBlob subjectivity score (0=objective, 1=subjective)
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  Returns:
77
- List of 9 float values.
78
  """
79
  if not text or not isinstance(text, str):
80
- return [0.0] * 9
81
 
82
  words = text.split()
83
  token_count = len(words)
84
  if token_count == 0:
85
- return [0.0] * 9
 
 
 
86
 
87
  # 1. Exclamation Mark Density (per word)
88
  exclamation_density = text.count("!") / token_count
@@ -105,13 +298,13 @@ def extract_stylometric_features(text):
105
 
106
  # 5. Punctuation density (per 100 chars)
107
  punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
108
- punctuation_density = (punct_chars / len(text)) * 100 if len(text) > 0 else 0
109
 
110
  # 6. Token Count (article length — real news is typically longer)
111
  # (already computed as token_count)
112
 
113
  # 7. Unique word ratio (vocabulary richness)
114
- unique_words = len(set(w.lower() for w in words))
115
  unique_word_ratio = unique_words / token_count
116
 
117
  # 8. Average word length
@@ -123,6 +316,64 @@ def extract_stylometric_features(text):
123
  except Exception:
124
  subjectivity = 0.0
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  return [
127
  float(exclamation_density),
128
  float(question_count),
@@ -133,6 +384,19 @@ def extract_stylometric_features(text):
133
  float(unique_word_ratio),
134
  float(avg_word_length),
135
  float(subjectivity),
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  ]
137
 
138
 
@@ -146,6 +410,19 @@ STYLOMETRIC_FEATURE_NAMES = [
146
  "unique_word_ratio",
147
  "avg_word_length",
148
  "subjectivity",
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  ]
150
 
151
 
@@ -155,23 +432,70 @@ STYLOMETRIC_FEATURE_NAMES = [
155
 
156
 
157
  def load_fake_news_dataset():
158
- """Load the fake_news_filipino dataset from local CSV."""
159
- print("Loading fake_news_filipino dataset...")
 
 
 
160
 
161
- csv_path = os.path.join(
162
- PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  )
164
- if not os.path.exists(csv_path):
 
 
 
 
 
 
 
 
 
 
 
165
  raise FileNotFoundError(
166
- f"Dataset not found at {csv_path}. "
167
- "Download fakenews.zip from huggingface.co/datasets/jcblaise/fake_news_filipino"
168
  )
169
 
170
- df = pd.read_csv(csv_path)
 
 
 
 
 
 
 
 
 
 
 
 
171
  label_map = {0: "Real", 1: "Fake"}
172
  df["label_name"] = df["label"].map(label_map)
173
 
174
- print(f" Loaded {len(df)} articles")
175
  print(f" Distribution:\n{df['label_name'].value_counts().to_string()}")
176
  return df
177
 
 
27
  import pandas as pd
28
  import joblib
29
  from textblob import TextBlob
30
+ import textstat
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
 
60
  # ───────────────────────────────────────────────────────────
61
 
62
 
63
+ # ── Word lists for linguistic features ──
64
+ FIRST_PERSON_PRONOUNS = {
65
+ # English
66
+ "i",
67
+ "me",
68
+ "my",
69
+ "mine",
70
+ "myself",
71
+ "we",
72
+ "us",
73
+ "our",
74
+ "ours",
75
+ "ourselves",
76
+ # Filipino
77
+ "ako",
78
+ "ko",
79
+ "akin",
80
+ "aking",
81
+ "natin",
82
+ "atin",
83
+ "namin",
84
+ "amin",
85
+ "tayo",
86
+ "kami",
87
+ "ta",
88
+ }
89
+
90
+ AUXILIARY_VERBS = {
91
+ # English
92
+ "have",
93
+ "has",
94
+ "had",
95
+ "do",
96
+ "does",
97
+ "did",
98
+ "will",
99
+ "would",
100
+ "shall",
101
+ "should",
102
+ "may",
103
+ "might",
104
+ "can",
105
+ "could",
106
+ "must",
107
+ "am",
108
+ "is",
109
+ "are",
110
+ "was",
111
+ "were",
112
+ "be",
113
+ "been",
114
+ "being",
115
+ # Filipino
116
+ "ay",
117
+ "dapat",
118
+ "mayroon",
119
+ "meron",
120
+ "maaari",
121
+ "pwede",
122
+ "kailangan",
123
+ }
124
+
125
+ ANALYTICAL_WORDS = {
126
+ # English articles + prepositions
127
+ "the",
128
+ "a",
129
+ "an",
130
+ "of",
131
+ "in",
132
+ "on",
133
+ "at",
134
+ "to",
135
+ "for",
136
+ "with",
137
+ "by",
138
+ "from",
139
+ "about",
140
+ "between",
141
+ "through",
142
+ "during",
143
+ "before",
144
+ "after",
145
+ # Filipino
146
+ "ang",
147
+ "ng",
148
+ "sa",
149
+ "mga",
150
+ "nang",
151
+ "para",
152
+ "tungkol",
153
+ "mula",
154
+ }
155
+
156
+ CERTAINTY_WORDS = {
157
+ # English
158
+ "always",
159
+ "never",
160
+ "absolutely",
161
+ "definitely",
162
+ "certainly",
163
+ "undoubtedly",
164
+ "clearly",
165
+ "obviously",
166
+ "without doubt",
167
+ "guaranteed",
168
+ "proven",
169
+ "fact",
170
+ "undeniable",
171
+ "indisputable",
172
+ "every",
173
+ "all",
174
+ # Filipino
175
+ "palagi",
176
+ "sigurado",
177
+ "tiyak",
178
+ "talaga",
179
+ "totoo",
180
+ "lagi",
181
+ "walang duda",
182
+ }
183
+
184
+ TENTATIVE_WORDS = {
185
+ # English
186
+ "perhaps",
187
+ "maybe",
188
+ "possibly",
189
+ "might",
190
+ "could",
191
+ "likely",
192
+ "unlikely",
193
+ "suggests",
194
+ "appears",
195
+ "seems",
196
+ "allegedly",
197
+ "reportedly",
198
+ "according",
199
+ "probable",
200
+ "approximately",
201
+ "estimated",
202
+ # Filipino
203
+ "siguro",
204
+ "marahil",
205
+ "maaaring",
206
+ "mukhang",
207
+ "parang",
208
+ "umano",
209
+ "diumano",
210
+ }
211
+
212
+ CLOUT_WORDS = {
213
+ # English — authority and dominance markers
214
+ "must",
215
+ "demand",
216
+ "require",
217
+ "order",
218
+ "command",
219
+ "insist",
220
+ "decree",
221
+ "mandate",
222
+ "authority",
223
+ "power",
224
+ "control",
225
+ "dominant",
226
+ "superior",
227
+ "we must",
228
+ "you must",
229
+ # Filipino
230
+ "kailangan",
231
+ "dapat",
232
+ "utos",
233
+ "kapangyarihan",
234
+ "kontrol",
235
+ "mando",
236
+ }
237
+
238
+
239
  def extract_stylometric_features(text):
240
  """Extract linguistic style features from text.
241
 
242
+ Features (22 total — Hybrid Feature Set):
243
+ 1. exclamation_density — Exclamation marks per word
244
+ 2. question_count — Number of '?' characters
245
+ 3. caps_ratio — Uppercase Ratio (ALL CAPS words / total words)
246
+ 4. avg_sentence_length — Average number of words per sentence
247
+ 5. punctuation_density — Punctuation chars per 100 chars
248
+ 6. token_count — Total word count (metadata feature)
249
+ 7. unique_word_ratio — Unique words / total words (vocabulary richness)
250
+ 8. avg_word_length — Average word length in characters
251
+ 9. subjectivity — TextBlob subjectivity score (0=objective, 1=subjective)
252
+ 10. flesch_reading_ease — Flesch Reading Ease (higher = easier to read)
253
+ 11. flesch_kincaid_grade — Flesch-Kincaid Grade Level
254
+ 12. coleman_liau_index — Coleman-Liau Index
255
+ 13. ari — Automated Readability Index
256
+ 14. first_person_ratio — First-person pronouns / total words
257
+ 15. auxiliary_verb_ratio — Auxiliary/linking verbs / total words
258
+ 16. gunning_fog_index — Gunning Fog readability index
259
+ 17. analytical_thinking — Articles + prepositions / total words
260
+ 18. certainty_score — High-certainty words / total words
261
+ 19. tentative_score — Hedge/tentative words / total words
262
+ 20. clout_score — Dominance/authority words / total words
263
+ 21. comma_period_density — (commas + periods) per 100 chars
264
+ 22. informal_punct_density — (parentheses + dashes + ellipses) per 100 chars
265
 
266
  Returns:
267
+ List of 22 float values.
268
  """
269
  if not text or not isinstance(text, str):
270
+ return [0.0] * 22
271
 
272
  words = text.split()
273
  token_count = len(words)
274
  if token_count == 0:
275
+ return [0.0] * 22
276
+
277
+ words_lower = [w.lower() for w in words]
278
+ text_len = len(text)
279
 
280
  # 1. Exclamation Mark Density (per word)
281
  exclamation_density = text.count("!") / token_count
 
298
 
299
  # 5. Punctuation density (per 100 chars)
300
  punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
301
+ punctuation_density = (punct_chars / text_len) * 100 if text_len > 0 else 0
302
 
303
  # 6. Token Count (article length — real news is typically longer)
304
  # (already computed as token_count)
305
 
306
  # 7. Unique word ratio (vocabulary richness)
307
+ unique_words = len(set(words_lower))
308
  unique_word_ratio = unique_words / token_count
309
 
310
  # 8. Average word length
 
316
  except Exception:
317
  subjectivity = 0.0
318
 
319
+ # 10-13. Readability Scores (textstat)
320
+ try:
321
+ flesch_reading_ease = textstat.flesch_reading_ease(text)
322
+ flesch_kincaid_grade = textstat.flesch_kincaid_grade(text)
323
+ coleman_liau_index = textstat.coleman_liau_index(text)
324
+ ari = textstat.automated_readability_index(text)
325
+ except Exception:
326
+ flesch_reading_ease = 0.0
327
+ flesch_kincaid_grade = 0.0
328
+ coleman_liau_index = 0.0
329
+ ari = 0.0
330
+
331
+ # 14. First-person pronoun ratio
332
+ first_person_count = sum(1 for w in words_lower if w in FIRST_PERSON_PRONOUNS)
333
+ first_person_ratio = first_person_count / token_count
334
+
335
+ # 15. Auxiliary verb ratio
336
+ aux_count = sum(1 for w in words_lower if w in AUXILIARY_VERBS)
337
+ auxiliary_verb_ratio = aux_count / token_count
338
+
339
+ # 16. Gunning Fog Index
340
+ try:
341
+ gunning_fog_index = textstat.gunning_fog(text)
342
+ except Exception:
343
+ gunning_fog_index = 0.0
344
+
345
+ # 17. Analytical thinking (articles + prepositions ratio)
346
+ analytical_count = sum(1 for w in words_lower if w in ANALYTICAL_WORDS)
347
+ analytical_thinking = analytical_count / token_count
348
+
349
+ # 18. Certainty score
350
+ certainty_count = sum(1 for w in words_lower if w in CERTAINTY_WORDS)
351
+ certainty_score = certainty_count / token_count
352
+
353
+ # 19. Tentative score
354
+ tentative_count = sum(1 for w in words_lower if w in TENTATIVE_WORDS)
355
+ tentative_score = tentative_count / token_count
356
+
357
+ # 20. Clout score (dominance/authority markers)
358
+ clout_count = sum(1 for w in words_lower if w in CLOUT_WORDS)
359
+ clout_score = clout_count / token_count
360
+
361
+ # 21. Comma + period density (per 100 chars)
362
+ comma_period_count = text.count(",") + text.count(".")
363
+ comma_period_density = (comma_period_count / text_len) * 100 if text_len > 0 else 0
364
+
365
+ # 22. Informal punctuation density — parentheses, dashes, ellipses (per 100 chars)
366
+ informal_count = (
367
+ text.count("(")
368
+ + text.count(")")
369
+ + text.count("—")
370
+ + text.count("–")
371
+ + text.count("-")
372
+ + text.count("...")
373
+ + text.count("…")
374
+ )
375
+ informal_punct_density = (informal_count / text_len) * 100 if text_len > 0 else 0
376
+
377
  return [
378
  float(exclamation_density),
379
  float(question_count),
 
384
  float(unique_word_ratio),
385
  float(avg_word_length),
386
  float(subjectivity),
387
+ float(flesch_reading_ease),
388
+ float(flesch_kincaid_grade),
389
+ float(coleman_liau_index),
390
+ float(ari),
391
+ float(first_person_ratio),
392
+ float(auxiliary_verb_ratio),
393
+ float(gunning_fog_index),
394
+ float(analytical_thinking),
395
+ float(certainty_score),
396
+ float(tentative_score),
397
+ float(clout_score),
398
+ float(comma_period_density),
399
+ float(informal_punct_density),
400
  ]
401
 
402
 
 
410
  "unique_word_ratio",
411
  "avg_word_length",
412
  "subjectivity",
413
+ "flesch_reading_ease",
414
+ "flesch_kincaid_grade",
415
+ "coleman_liau_index",
416
+ "ari",
417
+ "first_person_ratio",
418
+ "auxiliary_verb_ratio",
419
+ "gunning_fog_index",
420
+ "analytical_thinking",
421
+ "certainty_score",
422
+ "tentative_score",
423
+ "clout_score",
424
+ "comma_period_density",
425
+ "informal_punct_density",
426
  ]
427
 
428
 
 
432
 
433
 
434
  def load_fake_news_dataset():
435
+ """Load and merge fake news datasets.
436
+
437
+ Merges:
438
+ 1. jcblaise/fake_news_filipino (data/raw/fakenews/fakenews/full.csv)
439
+ 2. Philippine Fake News Corpus (data/raw/philippine_corpus/Philippine Fake News Corpus.csv)
440
 
441
+ Deduplicates by a fingerprint of article length + first 200 chars.
442
+ """
443
+ print("Loading datasets...")
444
+
445
+ frames = []
446
+
447
+ # ── Dataset 1: jcblaise/fake_news_filipino ──
448
+ csv1 = os.path.join(PROJECT_ROOT, "data", "raw", "fakenews", "fakenews", "full.csv")
449
+ if os.path.exists(csv1):
450
+ df1 = pd.read_csv(csv1)
451
+ # Columns: article, label (0=Real, 1=Fake)
452
+ df1 = df1[["article", "label"]].copy()
453
+ print(f" [1] jcblaise/fake_news_filipino: {len(df1)} articles")
454
+ frames.append(df1)
455
+ else:
456
+ print(f" [1] jcblaise dataset not found at {csv1}, skipping.")
457
+
458
+ # ── Dataset 2: Philippine Fake News Corpus ──
459
+ csv2 = os.path.join(
460
+ PROJECT_ROOT,
461
+ "data",
462
+ "raw",
463
+ "philippine_corpus",
464
+ "Philippine Fake News Corpus.csv",
465
  )
466
+ if os.path.exists(csv2):
467
+ df2 = pd.read_csv(csv2)
468
+ # Map: Content → article, Label → label (Credible=0, Not Credible=1)
469
+ df2 = df2.rename(columns={"Content": "article"})
470
+ df2["label"] = df2["Label"].map({"Credible": 0, "Not Credible": 1})
471
+ df2 = df2[["article", "label"]].copy()
472
+ print(f" [2] Philippine Fake News Corpus: {len(df2)} articles")
473
+ frames.append(df2)
474
+ else:
475
+ print(f" [2] Philippine Fake News Corpus not found at {csv2}, skipping.")
476
+
477
+ if not frames:
478
  raise FileNotFoundError(
479
+ "No datasets found! Place at least one dataset in data/raw/."
 
480
  )
481
 
482
+ # ── Merge and deduplicate ──
483
+ df = pd.concat(frames, ignore_index=True)
484
+ df = df.dropna(subset=["article"]).copy()
485
+ df = df[df["article"].str.len() > 0].copy()
486
+
487
+ # Deduplicate by fingerprint (length + first 200 chars)
488
+ before = len(df)
489
+ df["_fingerprint"] = df["article"].apply(lambda x: f"{len(str(x))}_{str(x)[:200]}")
490
+ df = df.drop_duplicates(subset=["_fingerprint"]).drop(columns=["_fingerprint"])
491
+ after = len(df)
492
+ if before > after:
493
+ print(f" Removed {before - after} duplicate articles")
494
+
495
  label_map = {0: "Real", 1: "Fake"}
496
  df["label_name"] = df["label"].map(label_map)
497
 
498
+ print(f"\n MERGED TOTAL: {len(df)} articles")
499
  print(f" Distribution:\n{df['label_name'].value_counts().to_string()}")
500
  return df
501
 
checker/internal/core.py CHANGED
@@ -12,6 +12,7 @@ import numpy as np
12
  import joblib
13
  from scipy.sparse import hstack, csr_matrix
14
  from textblob import TextBlob
 
15
  from checker.internal.bias_analyzer import analyze_bias
16
 
17
 
@@ -32,15 +33,182 @@ def clean_text(text):
32
  return text.strip()
33
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def extract_stylometric_features(text):
36
- """Extract the same 9 stylometric features used during training."""
37
  if not text or not isinstance(text, str):
38
- return [0.0] * 9
39
 
40
  words = text.split()
41
  token_count = len(words)
42
  if token_count == 0:
43
- return [0.0] * 9
 
 
 
44
 
45
  exclamation_density = text.count("!") / token_count
46
  question_count = text.count("?")
@@ -57,9 +225,9 @@ def extract_stylometric_features(text):
57
  )
58
 
59
  punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
60
- punctuation_density = (punct_chars / len(text)) * 100 if len(text) > 0 else 0
61
 
62
- unique_words = len(set(w.lower() for w in words))
63
  unique_word_ratio = unique_words / token_count
64
 
65
  avg_word_length = sum(len(w) for w in words) / token_count
@@ -69,6 +237,54 @@ def extract_stylometric_features(text):
69
  except Exception:
70
  subjectivity = 0.0
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  return [
73
  float(exclamation_density),
74
  float(question_count),
@@ -79,6 +295,19 @@ def extract_stylometric_features(text):
79
  float(unique_word_ratio),
80
  float(avg_word_length),
81
  float(subjectivity),
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  ]
83
 
84
 
 
12
  import joblib
13
  from scipy.sparse import hstack, csr_matrix
14
  from textblob import TextBlob
15
+ import textstat
16
  from checker.internal.bias_analyzer import analyze_bias
17
 
18
 
 
33
  return text.strip()
34
 
35
 
36
+ # ── Word lists for linguistic features ──
37
+ FIRST_PERSON_PRONOUNS = {
38
+ "i",
39
+ "me",
40
+ "my",
41
+ "mine",
42
+ "myself",
43
+ "we",
44
+ "us",
45
+ "our",
46
+ "ours",
47
+ "ourselves",
48
+ "ako",
49
+ "ko",
50
+ "akin",
51
+ "aking",
52
+ "natin",
53
+ "atin",
54
+ "namin",
55
+ "amin",
56
+ "tayo",
57
+ "kami",
58
+ "ta",
59
+ }
60
+
61
+ AUXILIARY_VERBS = {
62
+ "have",
63
+ "has",
64
+ "had",
65
+ "do",
66
+ "does",
67
+ "did",
68
+ "will",
69
+ "would",
70
+ "shall",
71
+ "should",
72
+ "may",
73
+ "might",
74
+ "can",
75
+ "could",
76
+ "must",
77
+ "am",
78
+ "is",
79
+ "are",
80
+ "was",
81
+ "were",
82
+ "be",
83
+ "been",
84
+ "being",
85
+ "ay",
86
+ "dapat",
87
+ "mayroon",
88
+ "meron",
89
+ "maaari",
90
+ "pwede",
91
+ "kailangan",
92
+ }
93
+
94
+ ANALYTICAL_WORDS = {
95
+ "the",
96
+ "a",
97
+ "an",
98
+ "of",
99
+ "in",
100
+ "on",
101
+ "at",
102
+ "to",
103
+ "for",
104
+ "with",
105
+ "by",
106
+ "from",
107
+ "about",
108
+ "between",
109
+ "through",
110
+ "during",
111
+ "before",
112
+ "after",
113
+ "ang",
114
+ "ng",
115
+ "sa",
116
+ "mga",
117
+ "nang",
118
+ "para",
119
+ "tungkol",
120
+ "mula",
121
+ }
122
+
123
+ CERTAINTY_WORDS = {
124
+ "always",
125
+ "never",
126
+ "absolutely",
127
+ "definitely",
128
+ "certainly",
129
+ "undoubtedly",
130
+ "clearly",
131
+ "obviously",
132
+ "without doubt",
133
+ "guaranteed",
134
+ "proven",
135
+ "fact",
136
+ "undeniable",
137
+ "indisputable",
138
+ "every",
139
+ "all",
140
+ "palagi",
141
+ "sigurado",
142
+ "tiyak",
143
+ "talaga",
144
+ "totoo",
145
+ "lagi",
146
+ "walang duda",
147
+ }
148
+
149
+ TENTATIVE_WORDS = {
150
+ "perhaps",
151
+ "maybe",
152
+ "possibly",
153
+ "might",
154
+ "could",
155
+ "likely",
156
+ "unlikely",
157
+ "suggests",
158
+ "appears",
159
+ "seems",
160
+ "allegedly",
161
+ "reportedly",
162
+ "according",
163
+ "probable",
164
+ "approximately",
165
+ "estimated",
166
+ "siguro",
167
+ "marahil",
168
+ "maaaring",
169
+ "mukhang",
170
+ "parang",
171
+ "umano",
172
+ "diumano",
173
+ }
174
+
175
+ CLOUT_WORDS = {
176
+ "must",
177
+ "demand",
178
+ "require",
179
+ "order",
180
+ "command",
181
+ "insist",
182
+ "decree",
183
+ "mandate",
184
+ "authority",
185
+ "power",
186
+ "control",
187
+ "dominant",
188
+ "superior",
189
+ "we must",
190
+ "you must",
191
+ "kailangan",
192
+ "dapat",
193
+ "utos",
194
+ "kapangyarihan",
195
+ "kontrol",
196
+ "mando",
197
+ }
198
+
199
+
200
  def extract_stylometric_features(text):
201
+ """Extract the same 22 stylometric features used during training."""
202
  if not text or not isinstance(text, str):
203
+ return [0.0] * 22
204
 
205
  words = text.split()
206
  token_count = len(words)
207
  if token_count == 0:
208
+ return [0.0] * 22
209
+
210
+ words_lower = [w.lower() for w in words]
211
+ text_len = len(text)
212
 
213
  exclamation_density = text.count("!") / token_count
214
  question_count = text.count("?")
 
225
  )
226
 
227
  punct_chars = sum(1 for c in text if c in ".,;:!?-\"'()[]{}...")
228
+ punctuation_density = (punct_chars / text_len) * 100 if text_len > 0 else 0
229
 
230
+ unique_words = len(set(words_lower))
231
  unique_word_ratio = unique_words / token_count
232
 
233
  avg_word_length = sum(len(w) for w in words) / token_count
 
237
  except Exception:
238
  subjectivity = 0.0
239
 
240
+ try:
241
+ flesch_reading_ease = textstat.flesch_reading_ease(text)
242
+ flesch_kincaid_grade = textstat.flesch_kincaid_grade(text)
243
+ coleman_liau_index = textstat.coleman_liau_index(text)
244
+ ari = textstat.automated_readability_index(text)
245
+ except Exception:
246
+ flesch_reading_ease = 0.0
247
+ flesch_kincaid_grade = 0.0
248
+ coleman_liau_index = 0.0
249
+ ari = 0.0
250
+
251
+ first_person_count = sum(1 for w in words_lower if w in FIRST_PERSON_PRONOUNS)
252
+ first_person_ratio = first_person_count / token_count
253
+
254
+ aux_count = sum(1 for w in words_lower if w in AUXILIARY_VERBS)
255
+ auxiliary_verb_ratio = aux_count / token_count
256
+
257
+ try:
258
+ gunning_fog_index = textstat.gunning_fog(text)
259
+ except Exception:
260
+ gunning_fog_index = 0.0
261
+
262
+ analytical_count = sum(1 for w in words_lower if w in ANALYTICAL_WORDS)
263
+ analytical_thinking = analytical_count / token_count
264
+
265
+ certainty_count = sum(1 for w in words_lower if w in CERTAINTY_WORDS)
266
+ certainty_score = certainty_count / token_count
267
+
268
+ tentative_count = sum(1 for w in words_lower if w in TENTATIVE_WORDS)
269
+ tentative_score = tentative_count / token_count
270
+
271
+ clout_count = sum(1 for w in words_lower if w in CLOUT_WORDS)
272
+ clout_score = clout_count / token_count
273
+
274
+ comma_period_count = text.count(",") + text.count(".")
275
+ comma_period_density = (comma_period_count / text_len) * 100 if text_len > 0 else 0
276
+
277
+ informal_count = (
278
+ text.count("(")
279
+ + text.count(")")
280
+ + text.count("—")
281
+ + text.count("–")
282
+ + text.count("-")
283
+ + text.count("...")
284
+ + text.count("…")
285
+ )
286
+ informal_punct_density = (informal_count / text_len) * 100 if text_len > 0 else 0
287
+
288
  return [
289
  float(exclamation_density),
290
  float(question_count),
 
295
  float(unique_word_ratio),
296
  float(avg_word_length),
297
  float(subjectivity),
298
+ float(flesch_reading_ease),
299
+ float(flesch_kincaid_grade),
300
+ float(coleman_liau_index),
301
+ float(ari),
302
+ float(first_person_ratio),
303
+ float(auxiliary_verb_ratio),
304
+ float(gunning_fog_index),
305
+ float(analytical_thinking),
306
+ float(certainty_score),
307
+ float(tentative_score),
308
+ float(clout_score),
309
+ float(comma_period_density),
310
+ float(informal_punct_density),
311
  ]
312
 
313
 
data/embeddings.npz CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:a20cfa3b8b8338fa8a7ffa09f91f73ff7a1f6c4010bcb0003b5ac1d2fa1047c4
3
- size 2274678
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d06963218662e1331c1c63015736827c44076d4f7fab8d32ed192afd785927dd
3
+ size 3735378
data/{news.db → raw/Philippine_Fake_News_Corpus.zip} RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:9950e8bc416a38ae8a16781ba46d32fc156625feec8cbdbfe64c6343bc55330e
3
- size 421888
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d114013042deb8d19c461766c5e3ad8cb99760df4c0238853c96d2c04a33db23
3
+ size 23734636
data/raw/philippine_corpus/Philippine Fake News Corpus.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b84427e60ea159b2f67f6b88aa9d2c7187f684f1d40f8035a54405605d043a70
3
+ size 60680690
data_models/rf_fakenews_model.pkl CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:5a1feab8d853cdc56041b39d6b9670fc4aa76486299ddb4269e83f44407b737e
3
- size 3407993
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa06afbff4dbfe6d61de77948dc2332df9fe08e201f28bc2250c056ce88b1999
3
+ size 14827833
data_models/stylo_scaler.pkl CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:6d81dd464ac6d1334d7d6136382e476fda543dca67c877def3d8013ec2a2c777
3
- size 783
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:20432d5d492d214b693bd1f23f99e436aa55b3eb96238f29c35ba7a8a4a6bad8
3
+ size 1111
data_models/tfidf_fakenews.pkl CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:cca784a4d98bd9bccd319eea61102b0a70ec8d783b1aabb5b4b4ad55c367fbec
3
- size 579116
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d0f3d21fa1e93a71c51188f6983447882f571f74e91d30c044106337d843730d
3
+ size 577702
db/database.py CHANGED
@@ -1,28 +1,33 @@
1
- import sqlite3
2
  import os
 
 
3
  from datetime import datetime
 
4
 
5
- DB_DIR = os.path.join(
6
- os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data"
 
 
 
 
 
7
  )
8
- DB_PATH = os.path.join(DB_DIR, "news.db")
9
 
10
 
11
  def get_connection():
12
- """Get a connection to the SQLite database."""
13
- os.makedirs(DB_DIR, exist_ok=True)
14
- conn = sqlite3.connect(DB_PATH)
15
- conn.row_factory = sqlite3.Row
16
  return conn
17
 
18
 
19
  def init_db():
20
  """Create the articles table if it doesn't exist."""
21
  conn = get_connection()
22
- conn.execute(
 
23
  """
24
  CREATE TABLE IF NOT EXISTS articles (
25
- id INTEGER PRIMARY KEY AUTOINCREMENT,
26
  title TEXT NOT NULL,
27
  url TEXT NOT NULL UNIQUE,
28
  source TEXT NOT NULL,
@@ -45,8 +50,9 @@ def insert_articles(articles):
45
  inserted = 0
46
  for article in articles:
47
  cursor.execute(
48
- "INSERT OR IGNORE INTO articles (title, url, source, published, scraped_at) "
49
- "VALUES (?, ?, ?, ?, ?)",
 
50
  (
51
  article["title"],
52
  article["url"],
@@ -65,17 +71,21 @@ def insert_articles(articles):
65
  def get_all_articles():
66
  """Return all articles from the database."""
67
  conn = get_connection()
68
- rows = conn.execute("SELECT * FROM articles ORDER BY scraped_at DESC").fetchall()
 
 
69
  conn.close()
70
  return [dict(row) for row in rows]
71
 
72
 
73
  def search_articles(query):
74
- """Search articles by keyword in title."""
75
  conn = get_connection()
76
- rows = conn.execute(
77
- "SELECT * FROM articles WHERE title LIKE ? ORDER BY scraped_at DESC",
 
78
  (f"%{query}%",),
79
- ).fetchall()
 
80
  conn.close()
81
  return [dict(row) for row in rows]
 
 
1
  import os
2
+ import psycopg2
3
+ import psycopg2.extras
4
  from datetime import datetime
5
+ from dotenv import load_dotenv
6
 
7
+ # Load .env from project root
8
+ load_dotenv(
9
+ os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
10
+ )
11
+
12
+ DATABASE_URL = os.getenv(
13
+ "DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/factcheck"
14
  )
 
15
 
16
 
17
  def get_connection():
18
+ """Get a connection to the PostgreSQL database."""
19
+ conn = psycopg2.connect(DATABASE_URL)
 
 
20
  return conn
21
 
22
 
23
  def init_db():
24
  """Create the articles table if it doesn't exist."""
25
  conn = get_connection()
26
+ cursor = conn.cursor()
27
+ cursor.execute(
28
  """
29
  CREATE TABLE IF NOT EXISTS articles (
30
+ id SERIAL PRIMARY KEY,
31
  title TEXT NOT NULL,
32
  url TEXT NOT NULL UNIQUE,
33
  source TEXT NOT NULL,
 
50
  inserted = 0
51
  for article in articles:
52
  cursor.execute(
53
+ "INSERT INTO articles (title, url, source, published, scraped_at) "
54
+ "VALUES (%s, %s, %s, %s, %s) "
55
+ "ON CONFLICT (url) DO NOTHING",
56
  (
57
  article["title"],
58
  article["url"],
 
71
  def get_all_articles():
72
  """Return all articles from the database."""
73
  conn = get_connection()
74
+ cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
75
+ cursor.execute("SELECT * FROM articles ORDER BY scraped_at DESC")
76
+ rows = cursor.fetchall()
77
  conn.close()
78
  return [dict(row) for row in rows]
79
 
80
 
81
  def search_articles(query):
82
+ """Search articles by keyword in title (case-insensitive)."""
83
  conn = get_connection()
84
+ cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
85
+ cursor.execute(
86
+ "SELECT * FROM articles WHERE title ILIKE %s ORDER BY scraped_at DESC",
87
  (f"%{query}%",),
88
+ )
89
+ rows = cursor.fetchall()
90
  conn.close()
91
  return [dict(row) for row in rows]
migrate_to_postgres.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ One-time migration: SQLite → PostgreSQL
3
+ =========================================
4
+ Copies all articles from data/news.db into the PostgreSQL factcheck database,
5
+ then deletes the old SQLite file.
6
+
7
+ Usage:
8
+ python migrate_to_postgres.py
9
+ """
10
+
11
+ import sqlite3
12
+ import os
13
+ from db.database import get_connection, init_db
14
+
15
+ SQLITE_PATH = os.path.join(
16
+ os.path.dirname(os.path.abspath(__file__)), "data", "news.db"
17
+ )
18
+
19
+
20
+ def migrate():
21
+ # 1. Ensure PostgreSQL table exists
22
+ init_db()
23
+
24
+ # 2. Check if SQLite DB exists
25
+ if not os.path.exists(SQLITE_PATH):
26
+ print(f"No SQLite database found at {SQLITE_PATH}. Nothing to migrate.")
27
+ return
28
+
29
+ # 3. Read all articles from SQLite
30
+ sqlite_conn = sqlite3.connect(SQLITE_PATH)
31
+ sqlite_conn.row_factory = sqlite3.Row
32
+ rows = sqlite_conn.execute("SELECT * FROM articles").fetchall()
33
+ articles = [dict(row) for row in rows]
34
+ sqlite_conn.close()
35
+
36
+ print(f"Found {len(articles)} articles in SQLite database.")
37
+
38
+ if not articles:
39
+ print("No articles to migrate.")
40
+ return
41
+
42
+ # 4. Insert into PostgreSQL
43
+ pg_conn = get_connection()
44
+ cursor = pg_conn.cursor()
45
+ inserted = 0
46
+
47
+ for article in articles:
48
+ cursor.execute(
49
+ "INSERT INTO articles (title, url, source, published, scraped_at) "
50
+ "VALUES (%s, %s, %s, %s, %s) "
51
+ "ON CONFLICT (url) DO NOTHING",
52
+ (
53
+ article["title"],
54
+ article["url"],
55
+ article["source"],
56
+ article.get("published", "N/A"),
57
+ article.get("scraped_at", ""),
58
+ ),
59
+ )
60
+ if cursor.rowcount > 0:
61
+ inserted += 1
62
+
63
+ pg_conn.commit()
64
+ pg_conn.close()
65
+
66
+ print(f"Migrated {inserted} articles to PostgreSQL.")
67
+ if inserted < len(articles):
68
+ print(f" ({len(articles) - inserted} duplicates skipped)")
69
+
70
+ # 5. Delete old SQLite database
71
+ os.remove(SQLITE_PATH)
72
+ print(f"Deleted old SQLite database: {SQLITE_PATH}")
73
+ print("\nMigration complete!")
74
+
75
+
76
+ if __name__ == "__main__":
77
+ migrate()
requirements.txt CHANGED
@@ -9,3 +9,6 @@ datasets
9
  scikit-learn
10
  pandas
11
  textblob
 
 
 
 
9
  scikit-learn
10
  pandas
11
  textblob
12
+ psycopg2-binary
13
+ python-dotenv
14
+ textstat