mirasai/media-profiling
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
- System Architecture
- Evaluation Modes
- Methodology
- Bias Scoring
- Factuality Scoring
- Credibility Calculation
- Core Modules
- Analyzer Flow Diagrams
- Benchmarking
- Propaganda Detection
- Installation
- Usage
- Project Structure
- References
Overview
Media Profiler automates the evaluation of news sources by:
- Scraping articles from target news websites
- Analyzing content using LLM-based analyzers with structured output (9 analyzers total)
- Detecting propaganda techniques using fine-tuned DeBERTa models
- Researching outlet history, ownership, and external analyses via pluggable search backends
- Calculating composite scores following MBFC 2025 methodology
- Generating detailed credibility reports with evidence citations
- 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 │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
▼
OUTPUTScoring 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:
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:
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:
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.10Factuality 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):
total_points = factual + bias + traffic + longevity + freedom_penaltyCredibility 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:
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-uspages from the outlet (cached) - Outlet name resolution: URL heuristics + LLM fallback for official names (e.g.,
apnews→The 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 produceComprehensiveReportData- 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 detectionSourcingAnalyzer- Link extraction + LLM quality assessmentFactCheckSearcher- Multi-site fact-checker search + LLM parsingTransparencyAnalyzer- Evaluates transparency about ownership, funding, location, authorsOneSidednessAnalyzer- Propaganda and one-sided reporting detection
Metadata Analyzers:
TrafficLongevityAnalyzer- Hybrid Tranco + WHOIS + LLMMediaTypeAnalyzer- Hybrid lookup + LLM classification
search_backends.py - Pluggable Search
Three search backend implementations with standardized output format:
DDGSearchBackend- DuckDuckGo (default, deterministic)OpenAISearchBackend- OpenAI Responses APIweb_searchtool with structured result parsingHybridSearchBackend- Routessite:-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):
CoTWrapperschema forces Chain-of-Thought reasoning before scoresRUNNER_MAPandget_runner()provide a unified interface- Anti-contamination prompts prevent LLM from citing MBFC/AllSides/Ad Fontes
- All modes accept configurable
model_namefor 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 ratingsEditorialBiasResult,FactCheckAnalysisResult,SourcingAnalysisResult- Analyzer outputsTransparencyResult,OneSidednessResult,PseudoscienceAnalysisResult- Additional analyzersMBFCTargetSchema,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 analysisOneSidednessLLMOutput/OneSidednessResult- One-sidedness/propaganda analysisComprehensiveReportData- Complete output with all subscores, research data, freedom ratingsMBFCTargetSchema/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_errorFactCheckSearcher (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, reasoningSourcingAnalyzer (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, reasoningEditorialBiasAnalyzer
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, reasoningTransparencyAnalyzer (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, reasoningOneSidednessAnalyzer (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, reasoningPseudoscienceAnalyzer (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.comBenchmarking
run_benchmark.py - Grid Evaluation Framework
Comprehensive benchmarking across all 6 modes:
# Run full benchmark
python run_benchmark.py
# Run specific modes
python run_benchmark.py --modes system hybrid llmMetrics calculated:
extract_subset.py - Mode Comparison & Analysis
Tool for comparing performance across modes and identifying where specific modes underperform:
# Extract underperforming subset
python extract_subset.py --mode extract
# Compare all 6 modes
python extract_subset.py --mode comparearticle_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
# 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 tablesTrain Models (Optional)
# Ensure datasets/ folder contains SemEval 2020 Task 11 data
python train_pipeline.pyUsage
Web Interface (Streamlit)
# Run locally
streamlit run app.py
# Opens at http://localhost:8501Deploy 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
- Go to huggingface.co/new-space
- Fill in the form:
- Owner: Your username or organization
- Space name:
media-profiler(or any name you prefer) - License: MIT (or your preferred license)
- SDK: Select Streamlit
- Visibility: Public or Private
- 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.
- Go to your Space's page:
https://huggingface.co/spaces/<your-username>/media-profiler - Click Settings (gear icon in the top-right)
- Scroll down to Repository secrets
- Click New secret:
- Name:
OPENAI_API_KEY - Value: Your OpenAI API key (e.g.,
sk-...) - Click Save
The app reads this automatically via os.environ["OPENAI_API_KEY"].
Step 4: Clone the Space and Push Your Code
# 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-profilerStep 5: Verify Required Files
Make sure these files exist in the root of the repo:
Note: You do not needrun_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 thedatasets/andpropaganda_models/directories for the web interface. These are for benchmarking and training only.
Step 6: Push to HuggingFace
# 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 mainHuggingFace will automatically:
- Detect the Streamlit SDK from
requirements.txt - Install all dependencies
- Run
streamlit run app.py - Provide a public URL like
https://<your-username>-media-profiler.hf.space
Step 7: Verify the Deployment
- Go to
https://huggingface.co/spaces/<your-username>/media-profiler - Wait for the build to complete (first build takes 2-5 minutes)
- You should see the Media Profiler interface with:
- A sidebar with URL input and previous reports
- Landing page explaining the methodology
- Enter a URL (e.g.,
https://www.bbc.com) and click Analyze
Troubleshooting
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):
---
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
# 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 --refreshProgrammatic
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
- Media Bias/Fact Check Methodology
- Freedom House - Freedom in the World
- Reporters Without Borders - Press Freedom Index
Benchmarking
- FACTScore: Fine-grained Atomic Evaluation of Factual Precision (Min et al., 2023)
- Media Background Checks (Schlichtkrull et al., 2024)
Propaganda Detection
- SemEval 2020 Task 11: Detection of Propaganda Techniques
- DeBERTa: Decoding-enhanced BERT with Disentangled Attention
License
MIT License
