import os
import gradio as gr
import requests
from transformers import pipeline
# ── Configuration ────────────────────────────────────────────────────
GNEWS_API_KEY = os.environ.get("GNEWS_API_KEY")
if not GNEWS_API_KEY:
raise ValueError(
"GNEWS_API_KEY not found. "
"Add it as a Secret in your Space settings (Settings → Secrets)."
)
BASE_URL = "https://gnews.io/api/v4/top-headlines"
# Load the zero-shot classification model (runs once at startup)
# This model can classify text into ANY categories you give it —
# no retraining required. It "understands" language well enough to
# judge whether a text matches a label it has never seen before.
classifier = pipeline(
"zero-shot-classification",
model="facebook/bart-large-mnli",
)
TOPICS = {
"Breaking News": "breaking-news",
"Business": "business",
"Technology": "technology",
"Science": "science",
"Health": "health",
"Sports": "sports",
"Entertainment": "entertainment",
"World": "world",
"Nation": "nation",
}
PRESETS = {
"Good news vs. Bad news": "good news, bad news",
"Neutral reporting vs. Editorial opinion vs. Clickbait": "neutral reporting, editorial opinion, clickbait",
"Fear-inducing vs. Reassuring vs. Informational": "fear-inducing, reassuring, informational",
"Hopeful vs. Worrying vs. Mixed signals": "hopeful, worrying, mixed signals",
"Celebrating vs. Mourning vs. Warning vs. Explaining": "celebrating, mourning, warning, explaining",
"Custom (edit the box below)": "",
}
def fetch_and_classify(topic_name, preset_name, custom_categories):
"""Fetch news headlines and classify each one with user-defined categories."""
if not topic_name:
return "Please select a news topic."
# Determine categories
categories_text = custom_categories.strip() if custom_categories.strip() else PRESETS.get(preset_name, "")
if not categories_text:
return "Please enter at least two categories, separated by commas."
candidate_labels = [c.strip() for c in categories_text.split(",") if c.strip()]
if len(candidate_labels) < 2:
return "Please enter at least two categories, separated by commas."
topic_code = TOPICS[topic_name]
params = {
"token": GNEWS_API_KEY,
"topic": topic_code,
"max": 8,
"lang": "en",
}
try:
resp = requests.get(BASE_URL, params=params, timeout=15)
resp.raise_for_status()
data = resp.json()
except Exception as e:
return f"Error fetching news: {e}"
articles = data.get("articles", [])
if not articles:
return f"No articles found for **{topic_name}**."
lines = []
for i, article in enumerate(articles, 1):
title = article.get("title", "No title")
source = article.get("source", {}).get("name", "Unknown source")
url = article.get("url", "")
published = article.get("publishedAt", "")[:10]
# Run zero-shot classification
result = classifier(title, candidate_labels)
# Build label badges — top label gets a highlight, rest are dimmed
badges = []
for label, score in zip(result["labels"], result["scores"]):
if label == result["labels"][0]:
badges.append(
f''
f'{label} ({score:.0%})'
)
else:
badges.append(
f''
f'{label} ({score:.0%})'
)
badge_line = " ".join(badges)
lines.append(
f"### {i}. {title}\n"
f"{badge_line}\n\n"
f"**Source:** {source} · **Date:** {published}\n"
f"[Read full article]({url})\n"
)
header = (
f"**Categories tested:** {', '.join(candidate_labels)}\n\n"
f"---\n\n"
)
return header + "\n".join(lines)
def update_categories(preset_name):
"""When a preset is selected, fill the custom box with its categories."""
return PRESETS.get(preset_name, "")
# ── Gradio UI ────────────────────────────────────────────────────────
with gr.Blocks(title="Zero-Shot News Analyzer") as demo:
gr.Markdown("# Zero-Shot News Analyzer")
gr.Markdown(
"Fetches live headlines and classifies each one using categories **you define**. "
"The model ([facebook/bart-large-mnli]"
"(https://huggingface.co/facebook/bart-large-mnli)) has never been trained "
"on your categories — it figures them out from language alone.\n\n"
"Try different categories on the same headlines. "
"Do the labels match your reading? Where does the model get it wrong?"
)
topic_choices = ["Breaking News", "Business", "Technology", "Science",
"Health", "Sports", "Entertainment", "World", "Nation"]
preset_choices = [
"Good news vs. Bad news",
"Neutral reporting vs. Editorial opinion vs. Clickbait",
"Fear-inducing vs. Reassuring vs. Informational",
"Hopeful vs. Worrying vs. Mixed signals",
"Celebrating vs. Mourning vs. Warning vs. Explaining",
"Custom (edit the box below)",
]
with gr.Row():
topic = gr.Dropdown(
choices=topic_choices,
label="News topic",
value="Breaking News",
allow_custom_value=False,
)
preset = gr.Dropdown(
choices=preset_choices,
label="Category preset",
value="Good news vs. Bad news",
allow_custom_value=False,
)
custom = gr.Textbox(
label="Categories (comma-separated — edit freely)",
value="good news, bad news",
placeholder="e.g., neutral reporting, editorial opinion, clickbait",
)
preset.change(fn=update_categories, inputs=preset, outputs=custom)
btn = gr.Button("Fetch & Classify", variant="primary")
output = gr.Markdown(label="Results")
btn.click(fn=fetch_and_classify, inputs=[topic, preset, custom], outputs=output)
demo.launch()