skander101 commited on
Commit
ee82dfb
·
1 Parent(s): cb8a178

Add sponsor recognition for all 42 news sources

Browse files
Files changed (5) hide show
  1. refresh_task.py +2 -0
  2. src/analyzer.py +57 -0
  3. src/models.py +1 -0
  4. templates/index.html +6 -4
  5. webapp.py +2 -0
refresh_task.py CHANGED
@@ -55,6 +55,7 @@ def serialize_clusters(clusters):
55
  "summary": a.analysis.summary if a.analysis else "",
56
  "topics": a.analysis.topics if a.analysis else [],
57
  "trust": f"{a.analysis.trustworthiness_score:.0%}" if a.analysis else "",
 
58
  "score": a.post.score,
59
  "comments": a.post.num_comments,
60
  "url": a.post.url,
@@ -84,6 +85,7 @@ def serialize_clusters(clusters):
84
  "summary": a.analysis.summary if a.analysis else "",
85
  "topics": a.analysis.topics if a.analysis else [],
86
  "trust": round(a.analysis.trustworthiness_score, 2) if a.analysis else 0,
 
87
  "score": a.post.score,
88
  "comments": a.post.num_comments,
89
  "image": a.article.image_url if a.article else a.post.image_url,
 
55
  "summary": a.analysis.summary if a.analysis else "",
56
  "topics": a.analysis.topics if a.analysis else [],
57
  "trust": f"{a.analysis.trustworthiness_score:.0%}" if a.analysis else "",
58
+ "sponsor": a.analysis.sponsor if a.analysis else "",
59
  "score": a.post.score,
60
  "comments": a.post.num_comments,
61
  "url": a.post.url,
 
85
  "summary": a.analysis.summary if a.analysis else "",
86
  "topics": a.analysis.topics if a.analysis else [],
87
  "trust": round(a.analysis.trustworthiness_score, 2) if a.analysis else 0,
88
+ "sponsor": a.analysis.sponsor if a.analysis else "",
89
  "score": a.post.score,
90
  "comments": a.post.num_comments,
91
  "image": a.article.image_url if a.article else a.post.image_url,
src/analyzer.py CHANGED
@@ -20,6 +20,56 @@ UNRELIABLE_DOMAINS = {
20
  "theonion.com": -0.25, "naturalnews.com": -0.30, "zerohedge.com": -0.15,
21
  }
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  CLICKBAIT_PATTERNS = [
24
  r"you won'?t believe", r"shocked?", r"gobsmacked",
25
  r"this is what happens", r"number \d+ will",
@@ -199,6 +249,11 @@ class NewsAnalyzer:
199
  except Exception as exc:
200
  logger.warning("Model loading failed: %s — using rule-based", exc)
201
 
 
 
 
 
 
202
  def analyze(self, article: Article) -> Analysis:
203
  summary = self._summarise(article)
204
  topics = self._classify_topics(article, summary)
@@ -207,6 +262,7 @@ class NewsAnalyzer:
207
  leaning = self._detect_political_leaning(article.text or "")
208
 
209
  category = self._map_category(topics)
 
210
 
211
  return Analysis(
212
  summary=summary,
@@ -215,6 +271,7 @@ class NewsAnalyzer:
215
  is_opinion=is_opinion,
216
  political_leaning=leaning,
217
  category=category,
 
218
  )
219
 
220
  def _summarise(self, article: Article) -> str:
 
20
  "theonion.com": -0.25, "naturalnews.com": -0.30, "zerohedge.com": -0.15,
21
  }
22
 
23
+ SPONSORS: dict[str, str] = {
24
+ "bbc.com": "BBC (publicly funded)",
25
+ "bbc.co.uk": "BBC (publicly funded)",
26
+ "nytimes.com": "The New York Times Company",
27
+ "npr.org": "NPR (publicly funded)",
28
+ "aljazeera.com": "Al Jazeera Media Network (Qatar-funded)",
29
+ "theguardian.com": "Guardian Media Group",
30
+ "statnews.com": "Boston Globe Media",
31
+ "sciencedaily.com": "ScienceDaily LLC",
32
+ "techcrunch.com": "Yahoo (Apollo Global Management)",
33
+ "wired.com": "Condé Nast (Advance Publications)",
34
+ "theverge.com": "Vox Media",
35
+ "arstechnica.com": "Condé Nast (Advance Publications)",
36
+ "krebsonsecurity.com": "Independent (Brian Krebs)",
37
+ "bleepingcomputer.com": "Independent",
38
+ "threatpost.com": "Independent",
39
+ "therecord.media": "Recorded Future",
40
+ "theonion.com": "Global Tetrahedron",
41
+ "thedailymash.co.uk": "Independent",
42
+ "babylonbee.com": "Independent",
43
+ "ign.com": "Ziff Davis",
44
+ "eurogamer.net": "ReedPop (Gamer Network)",
45
+ "pcgamer.com": "Future plc",
46
+ "kotaku.com": "G/O Media",
47
+ "gamespot.com": "Fandom",
48
+ "polygon.com": "Vox Media",
49
+ "variety.com": "Penske Media Corporation",
50
+ "hollywoodreporter.com": "Penske Media Corporation",
51
+ "deadline.com": "Penske Media Corporation",
52
+ "screenrant.com": "Valnet Inc.",
53
+ "arabnews.com": "Saudi Research & Publishing",
54
+ "middleeasteye.net": "Independent (London-based)",
55
+ "newarab.com": "Fikra Publishing",
56
+ "france24.com": "France Médias Monde (government-funded)",
57
+ "tunisiaonlinenews.com": "Independent",
58
+ "northafricapost.com": "Independent",
59
+ "africanews.com": "Independent",
60
+ "reuters.com": "Thomson Reuters",
61
+ "apnews.com": "Associated Press (non-profit)",
62
+ "wsj.com": "Dow Jones (News Corp)",
63
+ "economist.com": "The Economist Group",
64
+ "nature.com": "Springer Nature",
65
+ "science.org": "American Association for the Advancement of Science",
66
+ "washingtonpost.com": "The Washington Post (Jeff Bezos)",
67
+ "ft.com": "Financial Times (Nikkei)",
68
+ "bloomberg.com": "Bloomberg L.P. (Michael Bloomberg)",
69
+ "thehackernews.com": "Independent (THN)",
70
+ "ign.com": "Ziff Davis",
71
+ }
72
+
73
  CLICKBAIT_PATTERNS = [
74
  r"you won'?t believe", r"shocked?", r"gobsmacked",
75
  r"this is what happens", r"number \d+ will",
 
249
  except Exception as exc:
250
  logger.warning("Model loading failed: %s — using rule-based", exc)
251
 
252
+ @staticmethod
253
+ def _detect_sponsor(article: Article) -> str:
254
+ domain = re.sub(r"^www\.", "", (article.source_domain or ""))
255
+ return SPONSORS.get(domain, "")
256
+
257
  def analyze(self, article: Article) -> Analysis:
258
  summary = self._summarise(article)
259
  topics = self._classify_topics(article, summary)
 
262
  leaning = self._detect_political_leaning(article.text or "")
263
 
264
  category = self._map_category(topics)
265
+ sponsor = self._detect_sponsor(article)
266
 
267
  return Analysis(
268
  summary=summary,
 
271
  is_opinion=is_opinion,
272
  political_leaning=leaning,
273
  category=category,
274
+ sponsor=sponsor,
275
  )
276
 
277
  def _summarise(self, article: Article) -> str:
src/models.py CHANGED
@@ -34,6 +34,7 @@ class Analysis:
34
  is_opinion: bool
35
  political_leaning: str = "centrist"
36
  category: str = "General"
 
37
 
38
 
39
  @dataclass
 
34
  is_opinion: bool
35
  political_leaning: str = "centrist"
36
  category: str = "General"
37
+ sponsor: str = ""
38
 
39
 
40
  @dataclass
templates/index.html CHANGED
@@ -131,6 +131,7 @@ body {
131
  }
132
  .card-meta { display: flex; align-items: center; gap: 10px; font-size: 12px; color: #64748b; margin-bottom: 10px; flex-wrap: wrap; }
133
  .card-meta .source { color: #94a3b8; }
 
134
  .score-bar-wrap { height: 3px; background: #334155; border-radius: 3px; margin-bottom: 3px; }
135
  .score-bar-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, #60a5fa, #a78bfa); transition: width .5s; }
136
  .score-label { font-size: 10px; color: #64748b; display: flex; justify-content: space-between; }
@@ -243,10 +244,11 @@ body {
243
  {% endfor %}
244
  </div>
245
  <div class="card-meta">
246
- <span class="source">{{ c.articles[0].domain }}</span>
247
- <span>🛡️ {{ c.trust }}</span>
248
- {% if c.coverage > 1 %}<span>📰 {{ c.coverage }}</span>{% endif %}
249
- </div>
 
250
  <div class="score-bar-wrap">
251
  <div class="score-bar-fill" style="width:{{ c.score * 100 }}%"></div>
252
  </div>
 
131
  }
132
  .card-meta { display: flex; align-items: center; gap: 10px; font-size: 12px; color: #64748b; margin-bottom: 10px; flex-wrap: wrap; }
133
  .card-meta .source { color: #94a3b8; }
134
+ .sponsor { font-size: 10px; color: #fbbf24; background: rgba(251, 191, 36, 0.1); padding: 2px 6px; border-radius: 4px; }
135
  .score-bar-wrap { height: 3px; background: #334155; border-radius: 3px; margin-bottom: 3px; }
136
  .score-bar-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, #60a5fa, #a78bfa); transition: width .5s; }
137
  .score-label { font-size: 10px; color: #64748b; display: flex; justify-content: space-between; }
 
244
  {% endfor %}
245
  </div>
246
  <div class="card-meta">
247
+ <span class="source">{{ c.articles[0].domain }}</span>
248
+ <span>🛡️ {{ c.trust }}</span>
249
+ {% if c.coverage > 1 %}<span>📰 {{ c.coverage }}</span>{% endif %}
250
+ {% if c.articles[0].sponsor %}<span class="sponsor">🏢 {{ c.articles[0].sponsor }}</span>{% endif %}
251
+ </div>
252
  <div class="score-bar-wrap">
253
  <div class="score-bar-fill" style="width:{{ c.score * 100 }}%"></div>
254
  </div>
webapp.py CHANGED
@@ -55,6 +55,7 @@ def _cluster_to_html_dict(c):
55
  "summary": a.analysis.summary if a.analysis else "",
56
  "topics": a.analysis.topics if a.analysis else [],
57
  "trust": f"{a.analysis.trustworthiness_score:.0%}" if a.analysis else "",
 
58
  "score": a.post.score,
59
  "comments": a.post.num_comments,
60
  "url": a.post.url,
@@ -89,6 +90,7 @@ def _cluster_to_api_dict(c):
89
  "summary": a.analysis.summary if a.analysis else "",
90
  "topics": a.analysis.topics if a.analysis else [],
91
  "trust": round(a.analysis.trustworthiness_score, 2) if a.analysis else 0,
 
92
  "score": a.post.score,
93
  "comments": a.post.num_comments,
94
  "image": a.article.image_url if a.article else a.post.image_url,
 
55
  "summary": a.analysis.summary if a.analysis else "",
56
  "topics": a.analysis.topics if a.analysis else [],
57
  "trust": f"{a.analysis.trustworthiness_score:.0%}" if a.analysis else "",
58
+ "sponsor": a.analysis.sponsor if a.analysis else "",
59
  "score": a.post.score,
60
  "comments": a.post.num_comments,
61
  "url": a.post.url,
 
90
  "summary": a.analysis.summary if a.analysis else "",
91
  "topics": a.analysis.topics if a.analysis else [],
92
  "trust": round(a.analysis.trustworthiness_score, 2) if a.analysis else 0,
93
+ "sponsor": a.analysis.sponsor if a.analysis else "",
94
  "score": a.post.score,
95
  "comments": a.post.num_comments,
96
  "image": a.article.image_url if a.article else a.post.image_url,