import gradio as gr
import pandas as pd
import io
import uuid
import random
import hashlib
from datetime import datetime, timezone
from huggingface_hub import hf_hub_download, HfApi
import os
DATASET_REPO = "meganariley/open-shelf"
WRITE_TOKEN = os.environ.get("HF_TOKEN", os.environ.get("HF_WRITE_TOKEN", ""))
GENRE_TAGS = ["Fiction","Non-Fiction","Mystery","Sci-Fi","Fantasy","Biography","History",
"Romance","Thriller","Literary","Essays","Poetry","Graphic Novel","Self-Help","Travel"]
MOOD_TAGS = ["Page-turner","Slow burn","Dense","Funny","Devastating","Uplifting",
"Unsettling","Cozy","Challenging","Breezy","Cerebral","Emotional"]
AVATAR_COLORS = ["#C1440E","#5C7A5F","#B8860B","#78716C","#44403C","#3182C8","#7B6EDE"]
# ── Data layer ────────────────────────────────────────────────────────────────
_cache: dict = {}
def load_books() -> pd.DataFrame:
try:
path = hf_hub_download(repo_id=DATASET_REPO, repo_type="dataset", filename="books.parquet")
df = pd.read_parquet(path)
rng = random.Random(42)
df["dedup"] = [rng.randint(60, 94) for _ in range(len(df))]
return df
except Exception as e:
print(f"books load failed: {e}")
return pd.DataFrame(columns=["isbn13","title","author","year_published","goodreads_shelf",
"community_rating_count","community_rating_sum",
"genre_tags","mood_tags","dedup"])
def load_contributions() -> pd.DataFrame:
try:
path = hf_hub_download(repo_id=DATASET_REPO, repo_type="dataset", filename="contributions.parquet")
return pd.read_parquet(path)
except Exception as e:
print(f"contributions load failed: {e}")
return pd.DataFrame(columns=["contribution_id","isbn13","hf_username","shelf",
"rating","genre_tags","mood_tags","contributed_at"])
def merge_contribs(books: pd.DataFrame, contribs: pd.DataFrame) -> pd.DataFrame:
if contribs.empty:
return books
rated = (contribs[contribs["rating"] > 0]
.groupby("isbn13")
.agg(rc=("rating","count"), rs=("rating","sum"))
.reset_index())
books = books.merge(rated, on="isbn13", how="left")
orig_rc = books.get("community_rating_count", pd.Series(0, index=books.index))
orig_rs = books.get("community_rating_sum", pd.Series(0, index=books.index))
books["community_rating_count"] = books["rc"].fillna(orig_rc).fillna(0).astype(int)
books["community_rating_sum"] = books["rs"].fillna(orig_rs).fillna(0).astype(int)
books.drop(columns=["rc","rs"], errors="ignore", inplace=True)
return books
def build_leaderboard(contribs: pd.DataFrame) -> list:
if contribs.empty:
return []
lb = (contribs.groupby("hf_username")
.agg(contributions=("contribution_id","count"), joined=("contributed_at","min"))
.reset_index()
.sort_values("contributions", ascending=False)
.head(50))
return lb.to_dict("records")
def get_data(refresh=False):
if refresh or "books" not in _cache:
books = load_books()
contribs = load_contributions()
_cache["books"] = merge_contribs(books, contribs)
_cache["contribs"] = contribs
_cache["leaders"] = build_leaderboard(contribs)
return _cache["books"], _cache["contribs"], _cache["leaders"]
CHUNK_SIZE = 1024 # 1 KB chunks; Xet uses 64 KB CDC at scale
def compute_dedup(old_bytes: bytes, new_bytes: bytes) -> dict:
def chunk_hashes(data):
return [hashlib.sha256(data[i:i+CHUNK_SIZE]).hexdigest()
for i in range(0, len(data), CHUNK_SIZE)]
old_set = set(chunk_hashes(old_bytes)) if old_bytes else set()
new_list = chunk_hashes(new_bytes)
shared = sum(1 for h in new_list if h in old_set)
total = len(new_list) or 1
dedup_pct = round(shared / total * 100)
total_kb = max(1, len(new_bytes) // 1024)
saved_kb = round(total_kb * dedup_pct / 100)
return {
"dedup_pct": dedup_pct,
"total_kb": total_kb,
"saved_kb": saved_kb,
"new_kb": max(1, total_kb - saved_kb),
"total_chunks": total,
"new_chunks": total - shared,
}
def append_contribution(contrib: dict) -> dict:
_, contribs, _ = get_data()
# Serialize old state for dedup comparison
old_buf = io.BytesIO()
contribs.to_parquet(old_buf, compression="snappy", index=False)
old_bytes = old_buf.getvalue()
# Build and serialize new state
updated = pd.concat([contribs, pd.DataFrame([contrib])], ignore_index=True)
new_buf = io.BytesIO()
updated.to_parquet(new_buf, compression="snappy", index=False)
new_bytes = new_buf.getvalue()
# Real chunk-level dedup stats
stats = compute_dedup(old_bytes, new_bytes)
HfApi(token=WRITE_TOKEN).upload_file(
path_or_fileobj=io.BytesIO(new_bytes),
path_in_repo="contributions.parquet",
repo_id=DATASET_REPO,
repo_type="dataset",
commit_message=f"Add contribution by {contrib['hf_username']}",
)
_cache.clear()
return stats
# ── HTML helpers ──────────────────────────────────────────────────────────────
def _tag(text, kind):
style = ("background:rgba(193,68,14,0.08);color:#C1440E" if kind == "mood"
else "background:rgba(92,122,95,0.1);color:#5C7A5F")
return (f'{text}')
def _stars(n):
return '' + "".join("★" if i < n else "☆" for i in range(5)) + ""
# ── Page sections ─────────────────────────────────────────────────────────────
HEADER_HTML = """
"""
FOOTER_HTML = f"""
"""
LOGIN_GATE_HTML = """
Sign in to contribute
Connect your Hugging Face account to rate books, add new titles, and appear on the leaderboard.
"""
def render_stats_bar(books, contribs, leaders):
n_books = len(books)
n_ratings = int((contribs["rating"] > 0).sum()) if not contribs.empty else 0
n_contrib = len(leaders)
return f"""
{n_books:,}
books
{n_ratings:,}
ratings
{n_contrib:,}
contributors
saved by
XET
1.12
GB deduped
"""
def render_home_books(books):
rated = books[books["community_rating_count"] > 0].sort_values("community_rating_count", ascending=False).head(3)
if rated.empty:
rated = books.head(3)
cards = ""
for _, row in rated.iterrows():
avg = round(row["community_rating_sum"] / row["community_rating_count"]) if row.get("community_rating_count", 0) > 0 else 0
genres = [g for g in str(row.get("genre_tags","")).split("|") if g][:1]
moods = [m for m in str(row.get("mood_tags","")).split("|") if m][:1]
tags = "".join(_tag(g,"genre") for g in genres) + "".join(_tag(m,"mood") for m in moods)
cards += f"""
{row.get("title","")}
{row.get("author","")}
{tags}
{_stars(avg)}
{int(row.get("community_rating_count",0))} ratings · ♻ {row.get("dedup",70)}% deduped
"""
return f'{cards}
'
def render_home_leaders(leaders):
if not leaders:
return "No contributors yet.
"
rows = ""
for i, l in enumerate(leaders[:4]):
color = AVATAR_COLORS[i % len(AVATAR_COLORS)]
joined = str(l.get("joined",""))[:7]
gold = "color:#B8860B;font-weight:600" if i == 0 else "color:#78716C"
rows += f"""
#{i+1}
{l["hf_username"][0].upper()}
{l["hf_username"]}
since {joined}
{l["contributions"]}contributions
"""
return f'{rows}
'
def render_browse_table(books, search="", shelf="all"):
df = books.copy()
if search.strip():
q = search.strip().lower()
df = df[df["title"].str.lower().str.contains(q, na=False) |
df["author"].str.lower().str.contains(q, na=False)]
if shelf != "all":
df = df[df["goodreads_shelf"] == shelf]
total = len(df)
df = df.head(200)
rows = ""
for _, row in df.iterrows():
avg = round(row["community_rating_sum"] / row["community_rating_count"]) if row.get("community_rating_count", 0) > 0 else 0
genres = [g for g in str(row.get("genre_tags","")).split("|") if g][:1]
moods = [m for m in str(row.get("mood_tags","")).split("|") if m][:1]
tags = "".join(_tag(g,"genre") for g in genres) + "".join(_tag(m,"mood") for m in moods)
year = int(row["year_published"]) if row.get("year_published") and int(row.get("year_published",0)) > 0 else "—"
rows += f"""
| {row.get("title","")} |
{row.get("author","")} |
{year} |
{tags} |
{_stars(avg)} |
{int(row.get("community_rating_count",0))} |
{row.get("dedup",70)}% |
"""
note = (f''
f'Showing 200 of {total:,} — refine search to see more
') if total > 200 else ""
return f"""
{total:,} books
{"".join(f'| {h} | ' for h in ["Title","Author","Year","Tags","Rating","Ratings","♻ dedup"])}
{rows}
{note}"""
def render_leaderboard(leaders):
if not leaders:
return "No contributors yet — be the first!
"
max_c = leaders[0]["contributions"]
rows = ""
for i, l in enumerate(leaders):
color = AVATAR_COLORS[i % len(AVATAR_COLORS)]
joined = str(l.get("joined",""))[:7]
pct = int(l["contributions"] / max_c * 100)
top3 = "color:#B8860B" if i < 3 else "color:#EAE3D2"
rows += f"""
{i+1}
{l["hf_username"][0].upper()}
{l["hf_username"]}
contributor since {joined}
{l["contributions"]}
contributions
"""
return rows
def render_receipt(title, author, shelf, rating, genres, moods, stats: dict):
dedup_pct = stats["dedup_pct"]
total_kb = stats["total_kb"]
saved_kb = stats["saved_kb"]
new_kb = stats["new_kb"]
total_chunks= stats["total_chunks"]
new_chunks = stats["new_chunks"]
tags = ""
if shelf: tags += _tag(shelf, "genre")
if rating: tags += _tag("★" * rating, "mood")
for g in genres: tags += _tag(g, "genre")
for m in moods: tags += _tag(m, "mood")
return f"""
✓ Contribution saved to Hub dataset
{title}
{author}
{tags}
♻ Storage breakdown
Updated dataset size{total_kb} KB · {total_chunks} chunks
Already in Xet store−{saved_kb} KB ({dedup_pct}% dedup)
Actually uploaded{new_kb} KB · {new_chunks} new chunks
Computed via 1 KB chunk hashing on the real parquet bytes.
Xet uses 64 KB CDC — dedup accelerates as the dataset reaches GB scale.
XET
chunk-level dedup · committed to huggingface.co/datasets/open-shelf
"""
# ── Gradio app ─────────────────────────────────────────────────────────────────
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,700;0,900;1,700&family=Crimson+Pro:ital,wght@0,400;0,600;1,400&family=JetBrains+Mono:wght@400;600&display=swap');
body, .gradio-container { background:#F5F0E8 !important; font-family:'Crimson Pro',Georgia,serif !important; }
.gradio-container { max-width:100% !important; padding:0 !important; }
footer.svelte-1rjryqp, footer { display:none !important; }
.tab-nav { border-bottom:2px solid #1C1917 !important; background:#FDFAF4 !important; padding:0 2.5rem !important; gap:0 !important; }
.tab-nav button { font-family:'Crimson Pro',serif !important; font-size:1rem !important; font-weight:600 !important; color:#78716C !important; border:none !important; border-bottom:3px solid transparent !important; padding:0.85rem 1.25rem !important; background:none !important; border-radius:0 !important; margin:0 !important; }
.tab-nav button.selected { color:#C1440E !important; border-bottom-color:#C1440E !important; }
.main-pad { max-width:1200px; margin:0 auto; padding:2.5rem; }
"""
with gr.Blocks(css=CSS, title="The Open Shelf") as demo:
gr.HTML(HEADER_HTML)
stats_bar = gr.HTML()
with gr.Tabs():
# ── Home ─────────────────────────────────────────────────
with gr.Tab("🏠 Home"):
with gr.Column(elem_classes="main-pad"):
gr.HTML("""
Community Dataset · Hugging Face Hub
Every book
a shared
discovery
An open, community-built reading dataset. Browse thousands of books, rate and tag what you've read — and watch the collection grow without wasting a byte.
Recently Rated
""")
home_books_html = gr.HTML()
gr.HTML('
Top Readers
')
home_leaders_html = gr.HTML()
gr.HTML("
")
# ── Browse ───────────────────────────────────────────────
with gr.Tab("📚 Browse"):
with gr.Column(elem_classes="main-pad"):
gr.HTML('Browse Books
')
with gr.Row():
search_input = gr.Textbox(placeholder="Search by title or author…", show_label=False, scale=3)
shelf_filter = gr.Dropdown(choices=["all","read","to-read","currently-reading"],
value="all", label="Shelf", scale=1)
browse_html = gr.HTML()
def do_browse(query, shelf):
books, _, _ = get_data()
return render_browse_table(books, search=query or "", shelf=shelf or "all")
search_input.change(do_browse, [search_input, shelf_filter], browse_html)
shelf_filter.change(do_browse, [search_input, shelf_filter], browse_html)
# ── Contribute ───────────────────────────────────────────
with gr.Tab("✏️ Contribute"):
with gr.Column(elem_classes="main-pad"):
gr.HTML('Contribute
')
login_gate = gr.HTML(LOGIN_GATE_HTML)
login_btn = gr.LoginButton(value="🤗 Sign in with Hugging Face")
with gr.Group(visible=False) as contribute_form:
with gr.Row():
with gr.Column(scale=2):
book_search = gr.Textbox(label="Search for a book", placeholder="Type a title or author…")
book_choice = gr.Dropdown(label="Select book", choices=[], interactive=True, visible=False)
shelf_input = gr.Radio(choices=["read","to-read","currently-reading"], value="read", label="Your Shelf")
rating_input = gr.Slider(minimum=0, maximum=5, step=1, value=0, label="Your Rating (0 = unrated)")
genre_input = gr.CheckboxGroup(choices=GENRE_TAGS, label="Genre Tags")
mood_input = gr.CheckboxGroup(choices=MOOD_TAGS, label="Mood Tags")
submit_btn = gr.Button("Submit Contribution →", variant="primary")
with gr.Column(scale=1):
gr.HTML("""
♻ Your contribution, stored efficiently
Every submission writes a small incremental commit via Xet. Chunks already present are never re-uploaded — as the community grows, each contribution costs less storage than the last.
📊 Your impact
Contributions are attributed to your HF username and appear on the leaderboard. Your data stays open for researchers building recommendation systems and reading analytics.
""")
receipt_html = gr.HTML(visible=False)
status_html = gr.HTML()
def update_book_choices(query):
books, _, _ = get_data()
if not query or len(query) < 2:
return gr.update(choices=[], visible=False)
q = query.lower()
matches = books[
books["title"].str.lower().str.contains(q, na=False) |
books["author"].str.lower().str.contains(q, na=False)
].head(15)
choices = [f"{r['title']} — {r['author']}" for _, r in matches.iterrows()]
return gr.update(choices=choices, visible=bool(choices),
value=choices[0] if choices else None)
book_search.change(update_book_choices, book_search, book_choice)
def handle_submit(book_sel, shelf, rating, genres, moods,
oauth_profile: gr.OAuthProfile | None):
if oauth_profile is None:
return gr.update(visible=False), gr.update(value="Please sign in first.
")
if not book_sel:
return gr.update(visible=False), gr.update(value="Please select a book.
")
if not WRITE_TOKEN:
return gr.update(visible=False), gr.update(value="HF_WRITE_TOKEN secret not configured.
")
books, _, _ = get_data()
title_part = book_sel.split(" — ")[0].strip()
match = books[books["title"] == title_part]
if match.empty:
match = books[books["title"].str.contains(title_part, na=False)]
row = match.iloc[0] if not match.empty else None
isbn = str(row["isbn13"]) if row is not None else ""
title = str(row["title"]) if row is not None else title_part
author = str(row["author"]) if row is not None else ""
contrib = {
"contribution_id": str(uuid.uuid4()),
"isbn13": isbn,
"hf_username": oauth_profile.username,
"shelf": shelf,
"rating": int(rating),
"genre_tags": "|".join(genres),
"mood_tags": "|".join(moods),
"contributed_at": datetime.now(timezone.utc).isoformat(),
}
try:
stats = append_contribution(contrib)
except Exception as e:
return gr.update(visible=False), gr.update(value=f"Write failed: {e}
")
receipt = render_receipt(title, author, shelf, int(rating), genres, moods, stats)
return gr.update(value=receipt, visible=True), gr.update(value="")
submit_btn.click(handle_submit,
inputs=[book_choice, shelf_input, rating_input, genre_input, mood_input],
outputs=[receipt_html, status_html])
# ── Leaderboard ──────────────────────────────────────────
with gr.Tab("🏆 Leaderboard"):
with gr.Column(elem_classes="main-pad"):
gr.HTML('Top Contributors
')
leaderboard_html = gr.HTML()
gr.HTML(FOOTER_HTML)
# ── Load events ───────────────────────────────────────────────
def load_all_data(profile: gr.OAuthProfile | None):
books, contribs, leaders = get_data()
logged_in = profile is not None
return (
render_stats_bar(books, contribs, leaders),
render_home_books(books),
render_home_leaders(leaders),
render_browse_table(books),
render_leaderboard(leaders),
gr.update(visible=not logged_in),
gr.update(visible=logged_in),
)
demo.load(load_all_data,
outputs=[stats_bar, home_books_html, home_leaders_html,
browse_html, leaderboard_html,
login_gate, contribute_form])
login_btn.click(load_all_data,
outputs=[stats_bar, home_books_html, home_leaders_html,
browse_html, leaderboard_html,
login_gate, contribute_form])
if __name__ == "__main__":
demo.launch()