wiki-dyk / app.py
santhosh's picture
Deploy DYK card shuffler
9f7f7e6 verified
Raw
History Blame Contribute Delete
8.52 kB
"""A "Did You Know?" card shuffler for the wikimedia/wiki-dyk dataset.
Shows one random hook at a time: the featured article's image, the fact
(rendered from content_body), its topics, and a link to the article exactly
as it stood the day it was featured. Filter by language and topic.
Data is read straight from the published dataset on the Hub. Set DYK_LOCAL to
a directory of dyk-{lang}.parquet files to run against local outputs instead.
"""
import html
import os
import random
import re
from collections import Counter
from urllib.parse import quote
import gradio as gr
import pandas as pd
DATASET = "wikimedia/wiki-dyk"
LANGS = ["en", "fr", "de"]
TOP_TOPICS = 200 # dropdown cap; the full unique topic list is unusably long
ALL_TOPICS = "All topics"
_cache: dict[str, dict] = {}
def _data_url(lang: str) -> str:
local = os.environ.get("DYK_LOCAL")
if local:
return f"{local}/dyk-{lang}.parquet"
return f"hf://datasets/{DATASET}/{lang}/dyk-{lang}.parquet"
def _as_list(value) -> list:
"""linked_pages / article_topics arrive as numpy arrays or None."""
return [] if value is None else list(value)
def _clean_int(value):
return None if value is None or pd.isna(value) else int(value)
def load(lang: str) -> dict:
"""Load and normalize one language, cached. Builds a frequency-ranked
topic -> row-indices index for O(1) filtered sampling."""
if lang in _cache:
return _cache[lang]
df = pd.read_parquet(
_data_url(lang),
columns=["day", "content_body", "linked_pages"],
)
records = []
counter: Counter = Counter()
for day, body, linked in zip(
df["day"], df["content_body"], df["linked_pages"], strict=True
):
pages = []
topics_all: set[str] = set()
for page in _as_list(linked):
topics = [
topic["title"]
for topic in _as_list(page["article_topics"])
if topic is not None and topic["title"]
]
pages.append(
{
"page_title": page["page_title"],
"short_description": page["short_description"],
"page_image": page["page_image"],
"rev_id": _clean_int(page["rev_id"]),
"prominent": bool(page["prominent"]),
"topics": topics,
}
)
topics_all.update(topics)
counter.update(topics_all)
records.append(
{"day": day, "content_body": body, "pages": pages, "topics": topics_all}
)
top = {title for title, _ in counter.most_common(TOP_TOPICS)}
index: dict[str, list[int]] = {title: [] for title in top}
for i, record in enumerate(records):
for title in record["topics"] & top:
index[title].append(i)
choices = sorted(
((title.replace("_", " "), title) for title in top), key=lambda c: c[0].lower()
)
_cache[lang] = {
"records": records,
"all_idx": list(range(len(records))),
"topic_index": index,
"topic_choices": choices,
}
return _cache[lang]
def _featured(pages: list[dict]) -> dict | None:
"""The article the card centres on: a prominent one, preferring one that
has an image; fall back to any imaged page, then the first page."""
prominent = [p for p in pages if p["prominent"]]
for pool in (prominent, pages):
imaged = [p for p in pool if p["page_image"]]
if imaged:
return imaged[0]
if prominent:
return prominent[0]
return pages[0] if pages else None
def _image_url(lang: str, filename: str) -> str:
return (
f"https://{lang}.wikipedia.org/wiki/Special:FilePath/"
f"{quote(filename)}?width=400"
)
def _wiki_url(lang: str, title: str) -> str:
return f"https://{lang}.wikipedia.org/wiki/{quote(title.replace(' ', '_'))}"
def _rewrite_links(lang: str, body: str) -> str:
"""content_body links are relative (href="./Title")."""
return re.sub(
r'href="\./([^"]*)"',
lambda m: (
f'href="https://{lang}.wikipedia.org/wiki/{m.group(1)}" '
f'target="_blank" rel="noopener"'
),
body,
)
def _fmt_day(day) -> str | None:
if day is None or pd.isna(day):
return None
d = pd.Timestamp(day)
return f"{d.day} {d:%B %Y}"
CARD_CSS = """
<style>
.dyk-card { max-width: 640px; margin: 0 auto; padding: 1.25rem;
border: 1px solid rgba(128,128,128,.3); border-radius: 16px; }
.dyk-head { display: flex; gap: 1rem; align-items: flex-start; }
.dyk-img { width: 120px; height: 120px; object-fit: cover; border-radius: 12px;
flex-shrink: 0; }
.dyk-title { font-size: 1.1rem; font-weight: 600; }
.dyk-desc { opacity: .7; font-size: .9rem; }
.dyk-fact { margin: 1rem 0; font-size: 1.15rem; line-height: 1.5; }
.dyk-chips { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .75rem; }
.dyk-chip { font-size: .8rem; padding: .15rem .6rem; border-radius: 999px;
border: 1px solid rgba(128,128,128,.3); opacity: .85; }
.dyk-prov { font-size: .85rem; opacity: .7; }
.dyk-prov a { color: inherit; }
</style>
"""
def render_card(lang: str, record: dict | None) -> str:
if record is None:
return f"{CARD_CSS}<div class='dyk-card'>No facts to show.</div>"
featured = _featured(record["pages"])
head = ""
if featured:
img = ""
if featured["page_image"]:
img = (
f'<img class="dyk-img" alt="" '
f'src="{_image_url(lang, featured["page_image"])}">'
)
title = html.escape(featured["page_title"])
desc = html.escape(featured["short_description"] or "")
head = (
f'<div class="dyk-head">{img}<div>'
f'<div class="dyk-title">'
f'<a href="{_wiki_url(lang, featured["page_title"])}" '
f'target="_blank" rel="noopener" style="color:inherit">{title}</a></div>'
f'<div class="dyk-desc">{desc}</div></div></div>'
)
chips = ""
if featured and featured["topics"]:
chips = (
'<div class="dyk-chips">'
+ "".join(
f'<span class="dyk-chip">#{html.escape(t.replace("_", " "))}</span>'
for t in featured["topics"]
)
+ "</div>"
)
prov = ""
day = _fmt_day(record["day"])
if day:
prov = f"Featured on {day}"
if featured and featured["rev_id"]:
link = f"https://{lang}.wikipedia.org/w/index.php?oldid={featured['rev_id']}"
sep = " · " if prov else ""
prov += (
f'{sep}<a href="{link}" target="_blank" rel="noopener">'
f"see the article as it was then →</a>"
)
prov = f'<div class="dyk-prov">{prov}</div>' if prov else ""
fact = _rewrite_links(lang, record["content_body"])
return (
f'{CARD_CSS}<div class="dyk-card">{head}'
f'<div class="dyk-fact">{fact}</div>{chips}{prov}</div>'
)
def shuffle(lang: str, topic: str) -> str:
data = load(lang)
if topic and topic != ALL_TOPICS:
candidates = data["topic_index"].get(topic, [])
else:
candidates = data["all_idx"]
record = data["records"][random.choice(candidates)] if candidates else None
return render_card(lang, record)
def switch_language(lang: str):
data = load(lang)
choices = [(ALL_TOPICS, ALL_TOPICS), *data["topic_choices"]]
return (
gr.update(choices=choices, value=ALL_TOPICS),
shuffle(lang, ALL_TOPICS),
)
with gr.Blocks(title="Wikipedia — Did You Know?") as demo:
gr.Markdown(
"# 🎲 Wikipedia — Did You Know?\n"
"Random facts from the "
"[wikimedia/wiki-dyk](https://huggingface.co/datasets/wikimedia/wiki-dyk) "
"dataset — the *Did you know* snippets Wikipedia featured on its Main Page."
)
with gr.Row():
lang = gr.Radio(LANGS, value="en", label="Language")
topic = gr.Dropdown(
choices=[(ALL_TOPICS, ALL_TOPICS)],
value=ALL_TOPICS,
label="Topic",
filterable=True,
)
card = gr.HTML()
gr.Button("Another fact →", variant="primary").click(shuffle, [lang, topic], card)
lang.change(switch_language, lang, [topic, card])
topic.change(shuffle, [lang, topic], card)
demo.load(switch_language, lang, [topic, card])
if __name__ == "__main__":
demo.launch()