CoolFace
Apppublic

mirasai/media-profiling

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
App README

Media Profiler

A comprehensive media source analysis system that evaluates news outlets for political bias, factual reliability, and overall credibility using the Media Bias/Fact Check (MBFC) 2025 methodology.

Table of Contents


Overview

Media Profiler automates the evaluation of news sources by:

  1. 1.Scraping articles from target news websites
  2. 2.Analyzing content using LLM-based analyzers with structured output (9 analyzers total)
  3. 3.Detecting propaganda techniques using fine-tuned DeBERTa models
  4. 4.Researching outlet history, ownership, and external analyses via pluggable search backends
  5. 5.Calculating composite scores following MBFC 2025 methodology
  6. 6.Generating detailed credibility reports with evidence citations
  7. 7.Benchmarking across 6 evaluation modes with FACTScore, METEOR, ROUGE-L metrics

Key Features

  • MBFC 2025 Methodology - Full compliance with the latest Media Bias/Fact Check scoring rubric
  • 6 Evaluation Modes - LLM-only, articles, search, MBC baseline, system pipeline, and hybrid
  • Pluggable Search Backends - DuckDuckGo (default), OpenAI web_search, or hybrid routing
  • 9 LLM-Based Analyzers - Structured output for type-safe, reliable analysis (including Transparency and One-Sidedness)
  • Anti-Contamination System - Prevents LLM from citing MBFC/AllSides/Ad Fontes training data
  • Freedom Rating Integration - RSF Press Freedom Index + Freedom House scores affect credibility
  • Two-Stage Propaganda Detection - DeBERTa-v3-Large for span identification + technique classification
  • Comprehensive Benchmarking - Grid evaluation with FACTScore, METEOR, ROUGE-L, Bias MAE, Factuality MAE
  • Article Caching - Ensures benchmark fairness by reusing the same scraped articles across modes

System Architecture

Complete Analysis Pipeline

                         MEDIA PROFILER - COMPLETE PIPELINE
================================================================================

                                   INPUT
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  1. SCRAPE NODE (scraper.py)                                                │
│     ┌─────────────────────────────────────────────────────────────────┐     │
│     │  MediaScraper                                                   │     │
│     │  ├── Fetch homepage + sitemap                                   │     │
│     │  ├── Collect up to 20 articles                                  │     │
│     │  ├── Separate News vs Opinion articles                          │     │
│     │  │   ├── URL patterns (/opinion/, /editorial/)                  │     │
│     │  │   ├── Schema.org metadata                                    │     │
│     │  │   └── Title patterns ("Opinion:", "Editorial:")              │     │
│     │  └── Extract site metadata                                      │     │
│     │       ├── About page (ownership, funding)                       │     │
│     │       ├── Author information                                    │     │
│     │       └── Location disclosure                                   │     │
│     └─────────────────────────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  2. ANALYZE NODE (refactored_analyzers.py + research.py)                    │
│                                                                             │
│     ┌───────────────────────────┐   ┌───────────────────────────┐           │
│     │    CONTENT ANALYZERS      │   │   METADATA ANALYZERS      │           │
│     ├───────────────────────────┤   ├───────────────────────────┤           │
│     │ EditorialBiasAnalyzer     │   │ TrafficLongevityAnalyzer  │           │
│     │ PseudoscienceAnalyzer     │   │ MediaTypeAnalyzer         │           │
│     │ SourcingAnalyzer          │   │ OpinionAnalyzer           │           │
│     │ FactCheckSearcher         │   │                           │           │
│     │ TransparencyAnalyzer      │   │                           │           │
│     │ OneSidednessAnalyzer      │   │                           │           │
│     └───────────────────────────┘   └───────────────────────────┘           │
│                                                                             │
│     ┌───────────────────────────────────────────────────────────────────┐   │
│     │  WEB RESEARCH (MediaResearcher) — pluggable search backend       │   │
│     │  ├── History research (founding, key events)                      │   │
│     │  ├── Ownership research (owner, funding, headquarters)            │   │
│     │  ├── External analysis (MBFC, NewsGuard, academic reviews)        │   │
│     │  └── Batch search support (combines queries for efficiency)       │   │
│     └───────────────────────────────────────────────────────────────────┘   │
│                                                                             │
│     ┌───────────────────────────────────────────────────────────────────┐   │
│     │  FREEDOM RATING LOOKUP                                           │   │
│     │  ├── RSF Press Freedom Index (2025.csv)                          │   │
│     │  └── Freedom House ratings → credibility penalty                 │   │
│     └───────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  3. REPORT NODE (research.py + report_generator.py)                         │
│     ┌─────────────────────────────────────────────────────────────────┐     │
│     │  MediaProfiler                                                  │     │
│     │  ├── Calculate bias score (4 weighted subscores)                │     │
│     │  ├── Calculate factuality score (4 weighted components)         │     │
│     │  ├── Look up press freedom rating                               │     │
│     │  ├── Calculate credibility (point-based + exceptions)           │     │
│     │  └── Generate comprehensive MBFC-style report with evidence     │     │
│     └─────────────────────────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
                                  OUTPUT

Scoring Components

+-----------------------------------------------------------------------------+
|  BIAS SCORING (-10 to +10)             |  FACTUALITY SCORING (0-10)          |
|----------------------------------------|-------------------------------------|
|                                        |                                     |
|  EditorialBiasAnalyzer                 |  FactCheckSearcher          (40%)  |
|  4 weighted subscores:                 |  IFCN-approved fact-checker search |
|  - Economic Policy       (35%)         |                                     |
|  - Social Issues         (35%)         |  SourcingAnalyzer           (25%)  |
|  - News Reporting Style  (15%)         |  Link extraction + LLM quality     |
|  - Editorial Positions   (15%)         |                                     |
|                                        |  TransparencyAnalyzer       (25%)  |
|  -10 Left -> +10 Right                 |  About page, ownership, funding    |
|  7 labels: Extreme Left to             |                                     |
|            Extreme Right               |  OneSidednessAnalyzer       (10%)  |
|                                        |  Propaganda / one-sided reporting  |
+-----------------------------------------------------------------------------+
|                                                                             |
|                        CREDIBILITY (Point-Based)                            |
|  Factual (0-4 pts) + Bias (0-3 pts) + Traffic (0-2 pts)                    |
|  + Longevity bonus (1 pt if ≥10 years) + Freedom penalty (0 to -2 pts)     |
|  3 labels: High (≥7) / Medium (4-6) / Low (≤3)                             |
|  Exceptions: Mixed/Low/Very Low factuality → automatic Low                 |
|              Mostly Factual (score 3.6-4.5) → automatic Medium             |
|                                                                             |
+-----------------------------------------------------------------------------+

Evaluation Modes

The system supports 6 evaluation modes for benchmarking and comparison:

ModeModuleData SourcesDescription
llmevaluators.pyInternal LLM knowledge onlySite name + URL, no external data
articlesevaluators.pyLLM + 20 scraped articlesArticle content analysis
searchevaluators.pyLLM + OpenAI web_search toolNative web search integration
mbcevaluators.pyMedia Background Checks baselineSchlichtkrull et al., 2024
systemevaluators.py + pipelineFull profiling pipeline (DuckDuckGo)All 9 analyzers + research
hybridevaluators.py + pipelineSystem pipeline + OpenAI web_searchReplaces DuckDuckGo with OpenAI

All modes use a Chain-of-Thought wrapper (CoTWrapper) that forces reasoning before scores, and an anti-contamination system that prevents the LLM from citing MBFC/AllSides/Ad Fontes data.

Search Backends

Pluggable search backends in search_backends.py:

BackendClassDescription
DuckDuckGoDDGSearchBackendDefault, deterministic search
OpenAIOpenAISearchBackendOpenAI Responses API web_search tool with structured parsing
HybridHybridSearchBackendRoutes site:-targeted queries to DDG, others to OpenAI

All backends exclude aggregator domains (MBFC, AllSides, Ad Fontes, Wikipedia, NewsGuard) to prevent data leakage.


Methodology

Bias Scoring

Scale: -10 (Extreme Left) to +10 (Extreme Right)

The EditorialBiasAnalyzer uses LLM analysis with 4 weighted subscores:

ComponentWeightDescription
Economic Policy35%Taxes, regulation, unions, trade
Social Issues35%Abortion, LGBTQ+, guns, immigration
News Reporting Style15%Loaded language, story selection
Editorial Positions15%Editorial page leaning

Bias Labels (from _score_to_label() in refactored_analyzers.py): | Score Range | Label | |-------------|-------| | -10.0 to -8.0 | Extreme Left | | -7.9 to -5.0 | Left | | -4.9 to -2.0 | Left-Center | | -1.9 to +1.9 | Least Biased | | +2.0 to +4.9 | Right-Center | | +5.0 to +7.9 | Right | | +8.0 to +10.0 | Extreme Right |

Factuality Scoring

Scale: 0 (Best) to 10 (Worst)

Formula (from research.py):

factuality_score = FailedFactChecks × 0.40 + Sourcing × 0.25 + Transparency × 0.25 + OneSidedness × 0.10
ComponentWeightAnalyzerDescription
Failed Fact Checks40%FactCheckSearcherCount from IFCN-approved fact-checkers. Pseudoscience pushes minimum to 5.0
Sourcing Quality25%SourcingAnalyzerLink extraction + LLM quality assessment
Transparency25%TransparencyAnalyzerAbout page, ownership, funding, authors, location disclosure
One-Sidedness10%OneSidednessAnalyzerPropaganda and one-sided reporting detection

Factuality Labels (from _score_to_factuality_label() in research.py): | Score Range | Label | |-------------|-------| | 0.0 | Very High | | 0.1 - 1.9 | High | | 2.0 - 4.4 | Mostly Factual | | 4.5 - 6.4 | Mixed | | 6.5 - 8.4 | Low | | 8.5 - 10.0 | Very Low |

Credibility Calculation

Point-Based System (from _calculate_credibility_score() in research.py):

CategoryPointsMapping
Factual0-4 ptsVery High=4, High=3, Mostly Factual=2, Mixed=1, Low/Very Low=0
Bias0-3 ptsLeast Biased=3, Left-Center/Right-Center=2, Left/Right=1, Extreme=0
Traffic0-2 ptsHigh=2, Medium=1, Minimal=0
Longevity0-1 pt+1 if domain age ≥ 10 years
Freedom0 to -2 ptsLimited Freedom=-1, Total Oppression=-2
total_points = factual + bias + traffic + longevity + freedom_penalty

Credibility Labels (3 levels): | Total Points | Label | |-------------|-------| | ≥ 6 | High Credibility | | 3 - 5 | Medium Credibility | | 0 - 2 | Low Credibility |

Exception Rules (applied before the point-based labels): | Condition | Credibility Override | |-----------|---------------------| | Questionable, Conspiracy, or Pseudoscience source | Automatically Low Credibility | | Mostly Factual with factuality score 3.6-4.5 | Automatically Medium Credibility |

Note: Two Credibility Systems

This project contains two different credibility calculation systems used in different contexts:

SystemFileUsed ByMethod
Point-Based (Pipeline)research.pysystem and hybrid benchmark modes, main profilerFactual (0-4) + Bias (0-3) + Traffic (0-2) + Longevity (0-1) + Freedom (0 to -2) → 3 labels + exception rules
Point-Based (LLM Prompt)methodology.pyllm, articles, search benchmark modesSame rubric embedded in SCORING_INSTRUCTIONS prompt for LLM-only evaluation

Core Modules

research.py - Orchestration & Research

Main orchestrator combining web research with comprehensive profiling.

Components:

  • MediaResearcher - Gathers history, ownership, and external analysis via pluggable search backends
  • About page scraping: Directly fetches /about, /about-us pages from the outlet (cached)
  • Outlet name resolution: URL heuristics + LLM fallback for official names (e.g., apnewsThe Associated Press)
  • Tiered research: About page → search → Wikipedia → domain-based fallback
  • Batch search support: Combines multiple research queries into a single API call when backend supports it
  • Social media blacklist: Filters out Facebook, Twitter/X, Instagram, TikTok, Pinterest, LinkedIn, Reddit, YouTube
  • MediaProfiler - Orchestrates all 9 analyzers with pluggable search backend to produce ComprehensiveReportData
  • Freedom rating lookup: Integrates RSF Press Freedom Index + Freedom House scores
  • MBFC 2025 scoring: 4-component factuality formula + point-based credibility

refactored_analyzers.py - LLM-Based Analyzers

All analyzers use LangChain's structured output for type-safe LLM responses:

Content Analyzers:

  • OpinionAnalyzer - Content-based article type classification (News/Opinion/Satire/PR)
  • EditorialBiasAnalyzer - Political bias with 4 weighted subscores (Economic, Social, News Reporting, Editorial)
  • PseudoscienceAnalyzer - Science misinformation detection
  • SourcingAnalyzer - Link extraction + LLM quality assessment
  • FactCheckSearcher - Multi-site fact-checker search + LLM parsing
  • TransparencyAnalyzer - Evaluates transparency about ownership, funding, location, authors
  • OneSidednessAnalyzer - Propaganda and one-sided reporting detection

Metadata Analyzers:

  • TrafficLongevityAnalyzer - Hybrid Tranco + WHOIS + LLM
  • MediaTypeAnalyzer - Hybrid lookup + LLM classification

search_backends.py - Pluggable Search

Three search backend implementations with standardized output format:

  • DDGSearchBackend - DuckDuckGo (default, deterministic)
  • OpenAISearchBackend - OpenAI Responses API web_search tool with structured result parsing
  • HybridSearchBackend - Routes site:-targeted queries to DDG, other queries to OpenAI
  • All backends support batch_search() for multi-query efficiency
  • Excludes aggregator domains to prevent data leakage

evaluators.py - Evaluation Modes

Unified benchmarking interface for 6 evaluation modes (llm, articles, search, mbc, system, hybrid):

  • CoTWrapper schema forces Chain-of-Thought reasoning before scores
  • RUNNER_MAP and get_runner() provide a unified interface
  • Anti-contamination prompts prevent LLM from citing MBFC/AllSides/Ad Fontes
  • All modes accept configurable model_name for model sweeping

methodology.py - MBFC 2025 Scoring Rubric

Master scoring instructions and anti-contamination system:

  • SCORING_INSTRUCTIONS - Complete MBFC 2025 methodology (bias subscores, factuality formula, credibility points)
  • ANTI_CONTAMINATION_SYSTEM_PROMPT - Prevents LLM from using aggregator training data
  • Mode-specific data source instructions (DATA_SOURCE_LLM_ONLY, DATA_SOURCE_ARTICLES, DATA_SOURCE_SEARCH, DATA_SOURCE_HYBRID)
  • build_system_prompt() - Combines scoring rules + anti-contamination + mode-specific instructions

schemas.py - Pydantic v2 Data Models

Complete type-safe structured output schemas including:

  • ComprehensiveReportData - Complete output with all subscores, research data, freedom ratings
  • EditorialBiasResult, FactCheckAnalysisResult, SourcingAnalysisResult - Analyzer outputs
  • TransparencyResult, OneSidednessResult, PseudoscienceAnalysisResult - Additional analyzers
  • MBFCTargetSchema, CoTWrapper - Benchmark evaluation schemas

scraper.py - Web Scraping Engine

Brute-force article collection with:

  • Browser-like headers to avoid blocking
  • Rate limiting (0.5-1.5s delays)
  • Threaded parallel scraping (5 workers)
  • Opinion article detection (URL, title, meta tags, schema.org)
  • Metadata extraction (about page, ownership, funding, authors)

schemas.py - Pydantic v2 Schema Library

Complete type-safe structured output schemas including:

  • Article, Media Type, Traffic, Fact Check, Sourcing, Bias, Pseudoscience schemas
  • TransparencyLLMOutput / TransparencyResult - Transparency analysis
  • OneSidednessLLMOutput / OneSidednessResult - One-sidedness/propaganda analysis
  • ComprehensiveReportData - Complete output with all subscores, research data, freedom ratings
  • MBFCTargetSchema / EvidenceSource - Final MBFC report structure for benchmark evaluation

local_detector.py - DeBERTa Inference Pipeline

Two-stage propaganda detection using fine-tuned DeBERTa models:

  • Stage 1: Span Identification (Token Classification)
  • Stage 2: Technique Classification (Sequence Classification)

parser.py - MBFC Website Parser

Specialized parser for scraping Media Bias/Fact Check website to collect source URLs.


Analyzer Flow Diagrams

TrafficLongevityAnalyzer

Hybrid deterministic + LLM approach for traffic and domain age analysis.

analyze(domain)
    │
    ├─► 1. WHOIS Lookup (always runs)
    │       └─► Extract creation_date → Calculate age_years
    │
    ├─► 2. Tranco Lookup (O(1) dict lookup)
    │       │   Source: https://tranco-list.eu/
    │       │   - Top 1M domains ranked by popularity
    │       │   - Auto-downloads if missing
    │       │
    │       ├─► Found?
    │       │       ├─► rank < 10,000    → HIGH traffic
    │       │       ├─► rank < 100,000   → MEDIUM traffic
    │       │       ├─► rank < 1,000,000 → LOW traffic
    │       │       └─► Return with confidence=1.0, source=TRANCO
    │       │
    │       └─► Not found? → Continue to step 3
    │
    └─► 3. LLM Fallback (via search backend)
            ├─► Search: "{domain} traffic stats similarweb hypestat semrush"
            ├─► Combine top 5 result snippets
            └─► Parse with structured LLM output → TrafficEstimate
                    ├─► traffic_tier: HIGH/MEDIUM/LOW/MINIMAL/UNKNOWN
                    ├─► monthly_visits_estimate (if found)
                    ├─► confidence: 0.0-1.0
                    └─► reasoning

OUTPUT: TrafficData
    ├── domain, creation_date, age_years
    ├── traffic_tier, traffic_confidence
    ├── traffic_source: TRANCO | LLM | FALLBACK
    ├── tranco_rank (if available)
    └── whois_success, whois_error

FactCheckSearcher (40% of Factuality)

Multi-site search + LLM parsing for fact-check findings.

analyze(url_or_domain, outlet_name?)
    │
    ├─► 1. Extract Domain & Outlet Name
    │       ├─► "nytimes.com" → "New York Times"
    │       └─► Uses known_names dict or generates from domain
    │
    ├─► 2. Search 5 Fact-Checker Sites (via search backend)
    │       │   Sites:
    │       │   ├── politifact.com
    │       │   ├── snopes.com
    │       │   ├── factcheck.org
    │       │   ├── fullfact.org
    │       │   ├── reuters.com/fact-check
    │       │   ├── apnews.com/ap-fact-check
    │       │   └── leadstories.com
    │       │
    │       │   Query format:
    │       │   site:{site} "{domain}" OR "{outlet_name}"
    │       │
    │       └─► Collect up to 3 results per site → Combine snippets
    │
    ├─► 3. LLM Parsing
    │       └─► Parse snippets → FactCheckLLMOutput
    │               ├─► findings: List[FactCheckFinding]
    │               │       ├── source_site (PolitiFact, Snopes, etc.)
    │               │       ├── claim_summary
    │               │       ├── verdict: TRUE/MOSTLY_TRUE/HALF_TRUE/MIXED/
    │               │       │            MOSTLY_FALSE/FALSE/PANTS_ON_FIRE/
    │               │       │            MISLEADING/UNPROVEN/NOT_RATED
    │               │       └── url (if available)
    │               ├─► failed_count (FALSE, MOSTLY_FALSE, PANTS_ON_FIRE, MISLEADING)
    │               ├─► total_count
    │               └─► confidence, reasoning
    │
    └─► 4. Score Calculation (MBFC 2025: linear 1:1 mapping)
            ├─► 0 failed checks    → 0.0
            ├─► 1 failed check     → 1.0
            ├─► 2 failed checks    → 2.0
            ├─► N failed checks    → N (capped at 10.0)
            └─► No data found      → 0.0 (with sufficient_coverage=False flag)

OUTPUT: FactCheckAnalysisResult
    ├── domain, outlet_name
    ├── failed_checks_count, total_checks_count
    ├── score: 0.0-10.0
    ├── source: SEARCH | FALLBACK
    ├── findings: List[FactCheckFinding]
    └── confidence, reasoning

SourcingAnalyzer (25% of Factuality)

Link extraction + LLM quality assessment for source evaluation.

analyze(articles: List[{text}])
    │
    ├─► 1. Extract Links from All Articles
    │       └─► Regex: https?://[^\s<>"')\]]+
    │
    ├─► 2. Extract Unique Domains
    │       │   Filter out social media:
    │       │   ├── twitter.com, x.com
    │       │   ├── facebook.com, instagram.com
    │       │   ├── youtube.com, tiktok.com
    │       │   ├── linkedin.com, reddit.com
    │       │   └── t.co (Twitter short links)
    │       │
    │       └─► No domains found? → Return score=5.0 (neutral)
    │
    └─► 3. LLM Quality Assessment
            └─► Assess each domain → SourcingLLMOutput
                    ├─► sources_assessed: List[SourceAssessment]
                    │       ├── domain
                    │       ├── quality: PRIMARY/WIRE_SERVICE/MAJOR_OUTLET/
                    │       │            CREDIBLE/UNKNOWN/QUESTIONABLE
                    │       └── reasoning
                    ├─► overall_quality_score: 0.0-10.0
                    ├─► has_primary_sources: bool
                    ├─► has_wire_services: bool
                    └─► overall_assessment

Quality Tiers:
    PRIMARY       → .gov, .edu, official sources, research papers
    WIRE_SERVICE  → Reuters, AP, AFP, UPI
    MAJOR_OUTLET  → NYT, BBC, WSJ, WaPo, Guardian, CNN
    CREDIBLE      → Regional papers, trade publications
    UNKNOWN       → Unfamiliar domains
    QUESTIONABLE  → Known unreliable sources

OUTPUT: SourcingAnalysisResult
    ├── score: 0.0-10.0 (0=excellent, 10=poor)
    ├── avg_sources_per_article
    ├── total_sources_found, unique_domains
    ├── has_hyperlinks, has_primary_sources, has_wire_services
    ├── source_assessments: List[SourceAssessment]
    └── confidence, reasoning

EditorialBiasAnalyzer

LLM-based comprehensive political bias detection with 4 weighted subscores.

analyze(articles: List[{title, text}], url_or_domain?, outlet_name?)
    │
    ├─► 1. Format Articles for Analysis
    │       └─► Combine title + first 2000 chars of each article
    │
    └─► 2. LLM Analysis with MBFC Methodology
            │
            │   4 Weighted Subscores:
            │   ├── Economic Policy (35%): taxes, regulation, unions, trade
            │   ├── Social Issues (35%): abortion, LGBTQ+, guns, immigration
            │   ├── News Reporting Style (15%): loaded language, story selection
            │   └── Editorial Positions (15%): editorial page leaning
            │
            │   Additional Detection:
            │   ├── Loaded Language:
            │   │       ├── LEFT: "regime", "far-right", "fascist", "climate denier"
            │   │       └── RIGHT: "radical left", "woke", "cancel culture", "fake news"
            │   └── Story Selection Bias patterns
            │
            └─► Parse → EditorialBiasLLMOutput
                    ├─► overall_bias: EXTREME_LEFT/LEFT/LEFT_CENTER/CENTER/
                    │                 RIGHT_CENTER/RIGHT/EXTREME_RIGHT
                    ├─► bias_score: -10.0 to +10.0
                    ├─► economic_score, social_score, news_reporting_score, editorial_score
                    ├─► policy_positions: List[PolicyPosition]
                    ├─► uses_loaded_language: bool
                    ├─► loaded_language_examples: List[str]
                    └─► confidence, reasoning

OUTPUT: EditorialBiasResult
    ├── domain, outlet_name
    ├── overall_bias: BiasDirection
    ├── bias_score: -10.0 to +10.0
    ├── economic_score, social_score, news_reporting_score, editorial_score
    ├── mbfc_label: "Extreme Left"/"Left"/"Left-Center"/"Least Biased"/"Right-Center"/"Right"/"Extreme Right"
    ├── policy_positions, loaded_language_examples
    ├── articles_analyzed
    └── confidence, reasoning

TransparencyAnalyzer (25% of Factuality)

LLM-based evaluation of outlet transparency about ownership, funding, location, and authorship.

analyze(articles, url_or_domain?, outlet_name?, site_metadata?)
    │
    └─► LLM Analysis → TransparencyLLMOutput
            ├─► has_about_page: bool
            ├─► discloses_ownership: bool
            ├─► discloses_funding: bool
            ├─► discloses_location: bool
            ├─► identifies_authors: bool
            ├─► score: 0.0-10.0 (0=fully transparent, 10=no transparency)
            └─► confidence, reasoning

OUTPUT: TransparencyResult
    ├── score: 0.0-10.0
    ├── transparency indicators (about page, ownership, funding, location, authors)
    └── confidence, reasoning

OneSidednessAnalyzer (10% of Factuality)

LLM-based detection of propaganda techniques and one-sided reporting.

analyze(articles, url_or_domain?, outlet_name?)
    │
    └─► LLM Analysis → OneSidednessLLMOutput
            ├─► one_sidedness_score: 0.0-10.0 (0=balanced, 10=extreme propaganda)
            ├─► propaganda_techniques_found: List[str]
            └─► confidence, reasoning

OUTPUT: OneSidednessResult
    ├── score: 0.0-10.0
    ├── propaganda_techniques_found
    └── confidence, reasoning

PseudoscienceAnalyzer (Pushes Fact Check minimum to 5.0)

LLM-based detection of pseudoscience and conspiracy content.

analyze(articles: List[{title, text}], url_or_domain?, outlet_name?)
    │
    ├─► 1. Format Articles for Analysis
    │       └─► Combine title + first 2000 chars of each article
    │
    └─► 2. LLM Analysis with Scientific Consensus
            │
            │   Categories to Detect:
            │   HEALTH: Anti-Vaccination, Alternative Medicine, COVID-19 Misinfo
            │   CLIMATE: Climate Denialism, 5G Conspiracy, Chemtrails, GMO Claims
            │   PARANORMAL: Astrology, Psychic Claims, Faith Healing
            │   CONSPIRACY: Flat Earth, Moon Landing Hoax, QAnon
            │
            │   Severity: PROMOTES / PRESENTS_UNCRITICALLY / MIXED / NONE_DETECTED
            │
            └─► Parse → PseudoscienceLLMOutput
                    ├─► indicators: List[PseudoscienceIndicator]
                    ├─► promotes_pseudoscience: bool
                    ├─► overall_severity: PseudoscienceSeverity
                    ├─► respects_scientific_consensus: bool
                    └─► confidence, reasoning

OUTPUT: PseudoscienceAnalysisResult
    ├── score: 0.0-10.0 (0=pro-science, 10=promotes pseudoscience)
    ├── promotes_pseudoscience: bool
    ├── categories_found, indicators
    ├── respects_scientific_consensus: bool
    └── confidence, reasoning

NOTE: If pseudoscience is detected, the FactCheck score is pushed to a minimum of 5.0.

MediaResearcher - Outlet Name Resolution

Resolves the official outlet name from URL with LLM fallback.

resolve_outlet_name(url, domain)
    │
    ├─► 1. URL Heuristic
    │       ├─► Extract domain base: "bbc.com" → "bbc"
    │       ├─► Short names (≤4 chars) → UPPERCASE: "bbc" → "BBC"
    │       └─► Longer names → Title Case: "foxnews" → "Foxnews"
    │
    ├─► 2. About Page Scrape (LLM fallback)
    │       ├─► Try: /about, /about-us, /about/, /about-us/,
    │       │        /corporate/about, /company/about
    │       ├─► Extract page text (up to 5000 chars)
    │       └─► LLM extracts official name from about page
    │           ├─► "apnews" → "The Associated Press"
    │           ├─► "foxnews" → "Fox News"
    │           └─► "nytimes" → "The New York Times"
    │
    └─► OUTPUT: Official outlet name (string)

MediaResearcher - History, Ownership & External Analysis

research_all(outlet_name, domain)  — uses batch_search() when supported
    │
    ├─► research_history(outlet_name, domain)
    │       ├─► Tier 1: Direct about page scrape
    │       ├─► Tier 2: Search (via backend)
    │       ├─► Tier 3: Wikipedia fallback
    │       ├─► Tier 4: Domain-based search
    │       └─► LLM Extraction → HistoryLLMOutput
    │
    ├─► research_ownership(outlet_name, domain)
    │       ├─► Search: "{outlet_name}" ownership owner parent company
    │       └─► LLM Extraction → OwnershipLLMOutput (owner, funding_model, headquarters)
    │
    └─► research_external_analysis(outlet_name, domain)
            ├─► Targets: MBFC, Ad Fontes, NewsGuard, CJR, Nieman Lab
            └─► LLM Extraction → ExternalAnalysisLLMOutput

SEARCH BLACKLIST (filtered from all results):
    facebook.com, twitter.com, x.com, instagram.com,
    tiktok.com, pinterest.com, linkedin.com, reddit.com, youtube.com

Benchmarking

run_benchmark.py - Grid Evaluation Framework

Comprehensive benchmarking across all 6 modes:

bash
# Run full benchmark
python run_benchmark.py

# Run specific modes
python run_benchmark.py --modes system hybrid llm

Metrics calculated:

MetricDescription
FACTScore (%)% of generated atomic facts supported by gold knowledge
Error Rate (%)% of generated facts NOT supported
METEORText semantic similarity
ROUGE-LLongest common subsequence overlap
Bias MAEMean absolute error on ordinal bias scores
Factuality MAEMean absolute error on ordinal factuality scores
FC Detection (%)% of outlets where failed fact checks are detected

extract_subset.py - Mode Comparison & Analysis

Tool for comparing performance across modes and identifying where specific modes underperform:

bash
# Extract underperforming subset
python extract_subset.py --mode extract

# Compare all 6 modes
python extract_subset.py --mode compare

article_cache.py - Benchmark Fairness

Caches scraped articles per outlet domain to ensure all models and modes use the same input articles for fair comparison.


Propaganda Detection

See README_propaganda.md for full details on the two-stage DeBERTa pipeline (span identification + technique classification across 14 propaganda classes).


Installation

Requirements

  • Python 3.8+
  • PyTorch with CUDA (recommended for training)
  • 16GB+ RAM for inference
  • 24GB+ GPU VRAM for training

Setup

bash
# Clone repository
git clone https://github.com/MirasBaisbay/media-profiling.git
cd media-profiling

# Create virtual environment
python -m venv venv
source venv/bin/activate  # Linux/Mac
# or: venv\Scripts\activate  # Windows

# Install dependencies
pip install -r requirements.txt

# Set API key (for LLM-based analyzers)
export OPENAI_API_KEY="your-api-key"

Key Dependencies

streamlit>=1.30.0          # Web interface
langchain-openai>=0.1.0    # LLM structured output
duckduckgo-search>=5.0.0   # Default search backend
openai>=1.30.0             # OpenAI search backend + LLM
pydantic>=2.0.0            # Type-safe schemas
beautifulsoup4>=4.12.0     # Web scraping
python-whois>=0.9.0        # Domain age lookup
evaluate>=0.4.0            # Benchmark metrics (METEOR, ROUGE)
numpy>=1.24.0              # Numerical computation
tqdm>=4.65.0               # Progress bars
tabulate>=0.9.0            # Benchmark result tables

Train Models (Optional)

bash
# Ensure datasets/ folder contains SemEval 2020 Task 11 data
python train_pipeline.py

Usage

Web Interface (Streamlit)

bash
# Run locally
streamlit run app.py

# Opens at http://localhost:8501

Deploy to HuggingFace Spaces

Follow these steps to deploy Media Profiler as a public (or private) web app on HuggingFace Spaces.

Step 1: Create a HuggingFace Account

If you don't have one, sign up at huggingface.co/join.

Step 2: Create a New Space
  1. 1.Go to huggingface.co/new-space
  2. 2.Fill in the form:
  3. 3.Owner: Your username or organization
  4. 4.Space name: media-profiler (or any name you prefer)
  5. 5.License: MIT (or your preferred license)
  6. 6.SDK: Select Streamlit
  7. 7.Visibility: Public or Private
  8. 8.Click Create Space
Step 3: Add Your OpenAI API Key as a Secret

The app needs an OpenAI API key for LLM-based analysis. Never commit your key to the repo.

  1. 1.Go to your Space's page: https://huggingface.co/spaces/<your-username>/media-profiler
  2. 2.Click Settings (gear icon in the top-right)
  3. 3.Scroll down to Repository secrets
  4. 4.Click New secret:
  5. 5.Name: OPENAI_API_KEY
  6. 6.Value: Your OpenAI API key (e.g., sk-...)
  7. 7.Click Save

The app reads this automatically via os.environ["OPENAI_API_KEY"].

Step 4: Clone the Space and Push Your Code
bash
# Option A: Clone the empty Space repo and copy files into it
git clone https://huggingface.co/spaces/<your-username>/media-profiler
cd media-profiler

# Copy all project files into this directory

# Option B: Add the Space as a remote to your existing repo
cd /path/to/your/media-profiling
git remote add space https://huggingface.co/spaces/<your-username>/media-profiler
Step 5: Verify Required Files

Make sure these files exist in the root of the repo:

FilePurpose
app.pyStreamlit entry point (HuggingFace auto-detects this)
requirements.txtPython dependencies installed automatically
schemas.pyPydantic data models
research.pyResearch orchestrator
refactored_analyzers.pyLLM-based analyzers
scraper.pyWeb scraper
search_backends.pyPluggable search backends
methodology.pyMBFC 2025 scoring rubric
schemas.pyPydantic v2 data models (already listed above)
report_generator.pyReport prose generation
storage.pyReport caching
config.pyConfiguration constants
known_media_types.csvLookup table for media type classification
2025.csvRSF Press Freedom Index dataset
Note: You do not need run_benchmark.py, evaluators.py, metrics.py, extract_subset.py, article_cache.py, dataset_builder.py, main_pipeline.py, train_pipeline.py, local_detector.py, parser.py, or the datasets/ and propaganda_models/ directories for the web interface. These are for benchmarking and training only.
Step 6: Push to HuggingFace
bash
# If using Option A (cloned Space repo):
git add .
git commit -m "Deploy Media Profiler to HuggingFace Spaces"
git push origin main

# If using Option B (added as remote):
git push space main

HuggingFace will automatically:

  1. 1.Detect the Streamlit SDK from requirements.txt
  2. 2.Install all dependencies
  3. 3.Run streamlit run app.py
  4. 4.Provide a public URL like https://<your-username>-media-profiler.hf.space
Step 7: Verify the Deployment
  1. 1.Go to https://huggingface.co/spaces/<your-username>/media-profiler
  2. 2.Wait for the build to complete (first build takes 2-5 minutes)
  3. 3.You should see the Media Profiler interface with:
  4. 4.A sidebar with URL input and previous reports
  5. 5.Landing page explaining the methodology
  6. 6.Enter a URL (e.g., https://www.bbc.com) and click Analyze
Troubleshooting
ProblemSolution
Build failsCheck the Logs tab in your Space. Usually a missing dependency in requirements.txt
"OPENAI_API_KEY not set"Add the secret in Settings > Repository secrets (Step 3)
App crashes on analysisCheck the Logs tab for Python tracebacks. Common issue: the LLM model name might need updating
Slow first analysisNormal — scraping + 9 analyzer calls + research takes 2-5 minutes. Subsequent analyses use cache
"No articles found"Some sites block HuggingFace's IP range. Try a different news site
Space sleeps after inactivityFree Spaces sleep after ~48h. Upgrade to a persistent Space or just re-visit to wake it
Optional: Custom Space Configuration

Create a README.md at the root of your Space repo with HuggingFace metadata (this replaces the project README for the Space):

yaml
---
title: Media Profiler
emoji: 📰
colorFrom: blue
colorTo: indigo
sdk: streamlit
sdk_version: "1.30.0"
app_file: app.py
pinned: false
---

This controls the Space's title, emoji, and SDK version on the HuggingFace directory.

Command Line

bash
# Run the CLI profiler
python main_pipeline.py https://www.bbc.com

# Force re-analysis (ignore cache)
python main_pipeline.py https://www.bbc.com --refresh

Programmatic

python
from research import MediaProfiler

# Initialize profiler (default: DuckDuckGo search backend)
profiler = MediaProfiler()

# Or with hybrid search backend
from search_backends import HybridSearchBackend
profiler = MediaProfiler(search_backend=HybridSearchBackend())

# Sample articles (in practice, use scraper.py to collect these)
articles = [
    {
        "title": "Climate Change Policy Faces Opposition",
        "text": "The administration's new climate policy has drawn criticism..."
    },
    {
        "title": "Healthcare Reform Debate Intensifies",
        "text": "As healthcare costs continue to rise, lawmakers are divided..."
    },
]

# Profile an outlet
report = profiler.profile("https://www.bbc.com", articles)

# Generate text report
report_text = profiler.generate_report_text(report)
print(report_text)

Sample Output

======================================================================
MEDIA BIAS/FACT CHECK REPORT: BBC
======================================================================
URL: https://www.bbc.com
Analysis Date: 2025-01-15

QUICK SUMMARY
----------------------------------------
  Bias Rating:        Left-Center
  Factuality Rating:  High
  Credibility:        High Credibility
  Media Type:         TV
  Traffic:            HIGH
  Domain Age:         28.5 years
  Freedom Rating:     Good Situation

HISTORY
----------------------------------------
  Founded: 1922
  Founder(s): John Reith
  Key Events:
    - First TV broadcasts 1936
    - Charter renewal 2017

  The BBC was founded in 1922 by John Reith...

FUNDED BY / OWNERSHIP
----------------------------------------
  Owner: British Public
  Funding: License Fee (public funding)
  Headquarters: London, United Kingdom

BIAS ANALYSIS
----------------------------------------
  Overall Bias: Left-Center (score: -2.5)
  Economic: -2.0 | Social: -3.0 | News Reporting: -2.5 | Editorial: -2.5
  Uses Loaded Language: No

FACTUALITY ANALYSIS
----------------------------------------
  Factuality Rating: High (score: 1.8/10)

  Fact Check Search Results:
    Total Fact Checks Found: 3
    Failed Fact Checks: 0

  Sourcing Quality:
    Score: 2.0/10
    Has Primary Sources: Yes
    Has Wire Services: Yes

  Transparency: 1.0/10
  One-Sidedness: 1.5/10

PSEUDOSCIENCE CHECK
----------------------------------------
  Promotes Pseudoscience: No
  Respects Scientific Consensus: Yes

======================================================================
Articles Analyzed: 2
Generated by Media Profiling System
======================================================================

Project Structure

media-profiling/
│
├── config.py                    # Configuration constants and scoring scales
│   ├── CREDIBILITY_POINTS (factual, bias, traffic, freedom penalties)
│   ├── FACTUALITY_RANGES / BIAS_RANGES (MBFC label thresholds)
│   ├── FREEDOM_LABELS (5-tier freedom rating system)
│   ├── COUNTRY_NAME_ALIASES (HQ location → country matching)
│   ├── ECONOMIC_SCALE / SOCIAL_SCALE / NEWS_REPORTING_SCALE
│   └── Propaganda techniques (14 classes)
│
├── schemas.py                   # Pydantic v2 schemas for structured LLM outputs
│   ├── Article Classification (ArticleType, ArticleClassification)
│   ├── Media Type (MediaType, MediaTypeClassification)
│   ├── Traffic/Longevity (TrafficTier, TrafficData)
│   ├── Fact Check (FactCheckVerdict, FactCheckFinding, FactCheckAnalysisResult)
│   ├── Sourcing (SourceQuality, SourceAssessment, SourcingAnalysisResult)
│   ├── Editorial Bias (BiasDirection, PolicyPosition, EditorialBiasResult)
│   ├── Pseudoscience (PseudoscienceCategory, PseudoscienceIndicator, PseudoscienceAnalysisResult)
│   ├── Transparency (TransparencyLLMOutput, TransparencyResult)
│   ├── One-Sidedness (OneSidednessLLMOutput, OneSidednessResult)
│   ├── ComprehensiveReportData (complete output with all subscores)
│   └── MBFCTargetSchema / EvidenceSource (benchmark evaluation schema)
│
├── methodology.py               # MBFC 2025 scoring rubric + anti-contamination
│   ├── SCORING_INSTRUCTIONS (master methodology prompt)
│   ├── ANTI_CONTAMINATION_SYSTEM_PROMPT
│   ├── Mode-specific data source instructions
│   └── build_system_prompt() (combines all prompts per mode)
│
├── refactored_analyzers.py      # LLM-based analyzers with structured output
│   ├── OpinionAnalyzer (article type classification)
│   ├── TrafficLongevityAnalyzer (hybrid Tranco + WHOIS + LLM)
│   ├── MediaTypeAnalyzer (hybrid lookup + LLM)
│   ├── FactCheckSearcher (multi-site search + LLM parsing)
│   ├── SourcingAnalyzer (link extraction + LLM quality assessment)
│   ├── EditorialBiasAnalyzer (4 weighted bias subscores)
│   ├── PseudoscienceAnalyzer (LLM-based pseudoscience detection)
│   ├── TransparencyAnalyzer (ownership/funding/location transparency)
│   └── OneSidednessAnalyzer (propaganda/one-sidedness detection)
│
├── research.py                  # Web research and profiling orchestrator
│   ├── MediaResearcher (pluggable search backend, batch support)
│   │   ├── resolve_outlet_name() (URL heuristics + LLM about page fallback)
│   │   ├── _scrape_about_page() (direct /about page fetch, cached)
│   │   ├── research_history() (tiered: about page → search → Wikipedia)
│   │   ├── research_ownership() (search + domain fallback)
│   │   ├── research_external_analysis() (media watchdog search)
│   │   └── research_all() (batch search when backend supports it)
│   ├── MediaProfiler (9-analyzer orchestrator, freedom rating lookup)
│   └── Convenience functions (research_outlet, profile_outlet)
│
├── search_backends.py           # Pluggable search backend system
│   ├── DDGSearchBackend (DuckDuckGo, default)
│   ├── OpenAISearchBackend (OpenAI Responses API web_search)
│   └── HybridSearchBackend (routes site: queries to DDG, others to OpenAI)
│
├── evaluators.py                # 6-mode evaluation framework
│   ├── LLM-only, articles, search, MBC, system, hybrid runners
│   ├── CoTWrapper (Chain-of-Thought schema)
│   └── RUNNER_MAP / get_runner() (unified interface)
│
├── app.py                       # Streamlit web interface (HuggingFace Spaces ready)
│
├── main_pipeline.py             # CLI entry point: scrape → profile → generate → save
│
├── report_generator.py          # LLM-based MBFC prose report generation
│   └── ReportGenerator (GPT-4o synthesis with structured prompting)
│
├── storage.py                   # Persistence layer (30-day cache)
│   └── StorageManager (JSON + Markdown report caching)
│
├── requirements.txt             # Python dependencies for deployment
│
├── scraper.py                   # Web scraping for articles and metadata
│   ├── MediaScraper
│   ├── Article dataclass
│   └── SiteMetadata dataclass
│
├── metrics.py                   # Benchmark evaluation metrics
│   └── MetricsCalculator (FACTScore, METEOR, ROUGE-L, MAE, FC Detection)
│
├── run_benchmark.py             # Grid evaluation framework (models × modes × samples)
│
├── extract_subset.py            # Mode comparison and subset extraction
│
├── article_cache.py             # Article caching for benchmark fairness
│   └── ArticleCache (scrape-once, reuse-everywhere)
│
├── dataset_builder.py           # Stratified golden dataset builder from MBFC data
│
├── clean_mbfc.py                # MBFC dataset cleaning (removes fact-check URLs)
│
├── local_detector.py            # DeBERTa inference pipeline
│   └── LocalPropagandaDetector
│
├── train_pipeline.py            # DeBERTa fine-tuning pipeline
│
├── parser.py                    # MBFC website parser
│
├── known_media_types.csv        # Pre-classified media outlet types
│
├── tranco_top1m.csv             # Tranco top 1M domains (auto-downloaded)
│
├── 2025.csv                     # RSF Press Freedom Index dataset (181 countries)
│
├── datasets/                    # SemEval 2020 Task 11 data
│   ├── train/
│   ├── dev/
│   └── test/
│
└── propaganda_models/           # Trained model outputs
    ├── si_model/
    └── tc_model/

References

Methodology

Benchmarking

Propaganda Detection


License

MIT License