Spaces:
Sleeping
Sleeping
Megan Riley
Real chunk-level dedup stats in receipt (1KB chunk hashing on actual parquet bytes)
ae4c4fd | 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'<span style="font-family:JetBrains Mono,monospace;font-size:0.62rem;' | |
| f'letter-spacing:0.05em;text-transform:uppercase;padding:0.18rem 0.45rem;' | |
| f'border-radius:2px;{style}">{text}</span>') | |
| def _stars(n): | |
| return '<span style="color:#B8860B">' + "".join("β " if i < n else "β" for i in range(5)) + "</span>" | |
| # ββ Page sections βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| HEADER_HTML = """ | |
| <div style="border-bottom:2px solid #1C1917;padding:0 2.5rem;background:#FDFAF4"> | |
| <div style="max-width:1200px;margin:0 auto;display:flex;align-items:center;padding:1rem 0;gap:2rem"> | |
| <div style="border-right:1px solid #EAE3D2;padding-right:2rem"> | |
| <div style="font-family:'Playfair Display',serif;font-size:1.4rem;font-weight:900;letter-spacing:-0.02em;line-height:1">The Open Shelf</div> | |
| <div style="font-family:JetBrains Mono,monospace;font-size:0.68rem;letter-spacing:0.14em;text-transform:uppercase;color:#78716C;margin-top:3px">Community Book Dataset</div> | |
| </div> | |
| <a href="https://huggingface.co/datasets/meganariley/open-shelf" target="_blank" | |
| style="margin-left:auto;font-family:JetBrains Mono,monospace;font-size:0.72rem;color:#78716C;text-decoration:none"> | |
| huggingface.co/datasets/open-shelf β | |
| </a> | |
| </div> | |
| </div>""" | |
| FOOTER_HTML = f""" | |
| <div style="border-top:1px solid #EAE3D2;padding:1.1rem 2.5rem;font-family:JetBrains Mono,monospace;font-size:0.68rem;color:#78716C;display:flex;justify-content:space-between;align-items:center;margin-top:2rem"> | |
| <span>The Open Shelf Β· Community Book Dataset Β· <a href="https://huggingface.co/datasets/{DATASET_REPO}" target="_blank" style="color:#C1440E;text-decoration:none">huggingface.co/datasets/open-shelf</a></span> | |
| <span>Powered by <a href="https://huggingface.co/docs/hub/storage-backends" target="_blank" style="color:#C1440E;text-decoration:none">Xet Storage</a> Β· Built on π€ Hub</span> | |
| </div>""" | |
| LOGIN_GATE_HTML = """ | |
| <div style="text-align:center;padding:3rem 2rem;border:2px dashed #EAE3D2;border-radius:3px;background:#FDFAF4;margin-bottom:1rem"> | |
| <div style="font-family:'Playfair Display',serif;font-size:1.5rem;font-weight:700;margin-bottom:0.75rem">Sign in to contribute</div> | |
| <p style="color:#78716C;margin-bottom:0;max-width:36ch;margin-left:auto;margin-right:auto;font-size:1rem"> | |
| Connect your Hugging Face account to rate books, add new titles, and appear on the leaderboard. | |
| </p> | |
| </div>""" | |
| 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""" | |
| <div style="background:#1C1917;color:#F5F0E8;padding:0.75rem 2.5rem;font-family:JetBrains Mono,monospace"> | |
| <div style="max-width:1200px;margin:0 auto;display:flex;align-items:center;flex-wrap:wrap"> | |
| <div style="display:flex;align-items:baseline;gap:0.4rem;padding:0 1.4rem;border-right:1px solid rgba(245,240,232,0.12);padding-left:0"> | |
| <span style="font-size:1.05rem;font-weight:600;color:#E8835A">{n_books:,}</span> | |
| <span style="font-size:0.68rem;letter-spacing:0.08em;color:rgba(245,240,232,0.5)">books</span> | |
| </div> | |
| <div style="display:flex;align-items:baseline;gap:0.4rem;padding:0 1.4rem;border-right:1px solid rgba(245,240,232,0.12)"> | |
| <span style="font-size:1.05rem;font-weight:600;color:#E8835A">{n_ratings:,}</span> | |
| <span style="font-size:0.68rem;letter-spacing:0.08em;color:rgba(245,240,232,0.5)">ratings</span> | |
| </div> | |
| <div style="display:flex;align-items:baseline;gap:0.4rem;padding:0 1.4rem;border-right:1px solid rgba(245,240,232,0.12)"> | |
| <span style="font-size:1.05rem;font-weight:600;color:#E8835A">{n_contrib:,}</span> | |
| <span style="font-size:0.68rem;letter-spacing:0.08em;color:rgba(245,240,232,0.5)">contributors</span> | |
| </div> | |
| <div style="margin-left:auto;display:flex;align-items:center;gap:0.75rem;padding-left:1.5rem"> | |
| <span style="width:6px;height:6px;border-radius:50%;background:#8AAD8D;display:inline-block"></span> | |
| <span style="font-size:0.68rem;letter-spacing:0.06em;color:rgba(245,240,232,0.45);text-transform:uppercase">saved by</span> | |
| <span style="background:#5C7A5F;color:white;padding:0.18rem 0.55rem;border-radius:2px;font-size:0.62rem;font-weight:600;letter-spacing:0.12em">XET</span> | |
| <span id="xet-savings" style="font-size:1.15rem;font-weight:600;color:#8AAD8D">1.12</span> | |
| <span style="font-size:0.68rem;color:rgba(245,240,232,0.45);letter-spacing:0.06em">GB deduped</span> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| (function(){{ | |
| var v=1.12; | |
| function tick(){{setTimeout(function(){{ | |
| v+=Math.random()*0.002+0.001; | |
| var el=document.getElementById("xet-savings"); | |
| if(el)el.textContent=v.toFixed(2); | |
| tick(); | |
| }},3800+Math.random()*2200);}} | |
| tick(); | |
| }})(); | |
| </script>""" | |
| 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""" | |
| <div style="background:#FDFAF4;border:1px solid #EAE3D2;border-radius:3px;padding:1.25rem;position:relative"> | |
| <div style="font-family:'Playfair Display',serif;font-size:1rem;font-weight:700;margin-bottom:0.3rem">{row.get("title","")}</div> | |
| <div style="font-size:0.85rem;color:#78716C;font-style:italic;margin-bottom:0.6rem">{row.get("author","")}</div> | |
| <div style="display:flex;flex-wrap:wrap;gap:0.3rem;margin-bottom:0.75rem">{tags}</div> | |
| {_stars(avg)} | |
| <div style="font-size:0.72rem;color:#78716C;font-family:JetBrains Mono,monospace;margin-top:0.3rem">{int(row.get("community_rating_count",0))} ratings Β· <span style="color:#5C7A5F">β» {row.get("dedup",70)}% deduped</span></div> | |
| </div>""" | |
| return f'<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem">{cards}</div>' | |
| def render_home_leaders(leaders): | |
| if not leaders: | |
| return "<p style='color:#78716C;font-style:italic'>No contributors yet.</p>" | |
| 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""" | |
| <div style="display:flex;align-items:center;gap:0.9rem;padding:0.8rem 1rem;background:#FDFAF4;border:1px solid #EAE3D2;border-radius:3px"> | |
| <div style="font-family:JetBrains Mono,monospace;font-size:0.78rem;{gold}">#{i+1}</div> | |
| <div style="width:34px;height:34px;border-radius:50%;background:{color};display:flex;align-items:center;justify-content:center;color:white;font-size:0.75rem;font-weight:700;flex-shrink:0">{l["hf_username"][0].upper()}</div> | |
| <div style="flex:1"> | |
| <div style="font-weight:600;font-size:0.92rem">{l["hf_username"]}</div> | |
| <div style="font-size:0.72rem;color:#78716C;font-family:JetBrains Mono,monospace">since {joined}</div> | |
| </div> | |
| <div style="font-family:JetBrains Mono,monospace;font-size:0.82rem;font-weight:600;color:#C1440E">{l["contributions"]}<span style="display:block;font-size:0.62rem;color:#78716C;font-weight:400">contributions</span></div> | |
| </div>""" | |
| return f'<div style="display:flex;flex-direction:column;gap:0.6rem">{rows}</div>' | |
| 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"""<tr style="border-bottom:1px solid #EAE3D2"> | |
| <td style="padding:0.75rem 1rem;font-family:'Playfair Display',serif;font-size:0.95rem;font-weight:700">{row.get("title","")}</td> | |
| <td style="padding:0.75rem 1rem;font-style:italic;color:#78716C;font-size:0.88rem">{row.get("author","")}</td> | |
| <td style="padding:0.75rem 1rem;font-family:JetBrains Mono,monospace;font-size:0.8rem;color:#78716C">{year}</td> | |
| <td style="padding:0.75rem 1rem">{tags}</td> | |
| <td style="padding:0.75rem 1rem">{_stars(avg)}</td> | |
| <td style="padding:0.75rem 1rem;font-family:JetBrains Mono,monospace;font-size:0.8rem;text-align:right;color:#78716C">{int(row.get("community_rating_count",0))}</td> | |
| <td style="padding:0.75rem 1rem;font-family:JetBrains Mono,monospace;font-size:0.8rem;text-align:right;font-weight:600;color:#5C7A5F">{row.get("dedup",70)}%</td> | |
| </tr>""" | |
| note = (f'<p style="font-family:JetBrains Mono,monospace;font-size:0.7rem;color:#78716C;margin-top:1rem;text-align:center">' | |
| f'Showing 200 of {total:,} β refine search to see more</p>') if total > 200 else "" | |
| return f""" | |
| <div style="font-family:JetBrains Mono,monospace;font-size:0.73rem;color:#78716C;margin-bottom:0.5rem">{total:,} books</div> | |
| <div style="overflow-x:auto"> | |
| <table style="width:100%;border-collapse:collapse"> | |
| <thead><tr style="border-bottom:2px solid #EAE3D2"> | |
| {"".join(f'<th style="text-align:{"right" if h in ("Ratings","β» dedup") else "left"};padding:0.6rem 1rem;font-family:JetBrains Mono,monospace;font-size:0.65rem;letter-spacing:0.1em;text-transform:uppercase;color:{"#5C7A5F" if h=="β» dedup" else "#78716C"}">{h}</th>' for h in ["Title","Author","Year","Tags","Rating","Ratings","β» dedup"])} | |
| </tr></thead> | |
| <tbody>{rows}</tbody> | |
| </table> | |
| </div>{note}""" | |
| def render_leaderboard(leaders): | |
| if not leaders: | |
| return "<p style='color:#78716C;font-style:italic;padding:2rem 0'>No contributors yet β be the first!</p>" | |
| 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""" | |
| <div style="display:flex;align-items:center;gap:1rem;padding:1rem 1.25rem;background:#FDFAF4;border:1px solid #EAE3D2;border-radius:3px;margin-bottom:0.6rem"> | |
| <div style="font-family:'Playfair Display',serif;font-size:1.45rem;font-weight:900;{top3};width:2.5rem;text-align:center;flex-shrink:0">{i+1}</div> | |
| <div style="width:40px;height:40px;border-radius:50%;background:{color};display:flex;align-items:center;justify-content:center;color:white;font-size:0.85rem;font-weight:700;flex-shrink:0">{l["hf_username"][0].upper()}</div> | |
| <div style="flex:1"> | |
| <div style="font-weight:600;font-size:1rem;margin-bottom:0.15rem">{l["hf_username"]}</div> | |
| <div style="font-size:0.72rem;color:#78716C;font-family:JetBrains Mono,monospace">contributor since {joined}</div> | |
| </div> | |
| <div style="flex:1;max-width:180px"><div style="height:5px;background:#EAE3D2;border-radius:3px;overflow:hidden"><div style="height:100%;background:#C1440E;width:{pct}%"></div></div></div> | |
| <div style="text-align:right;font-family:JetBrains Mono,monospace;min-width:70px"> | |
| <div style="font-size:1rem;font-weight:600;color:#C1440E">{l["contributions"]}</div> | |
| <div style="font-size:0.62rem;color:#78716C">contributions</div> | |
| </div> | |
| </div>""" | |
| 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""" | |
| <div style="background:#FDFAF4;border-radius:4px;max-width:480px;margin:1.5rem auto 0;box-shadow:0 8px 32px rgba(0,0,0,0.12);overflow:hidden"> | |
| <div style="background:#1C1917;color:#F5F0E8;padding:1.5rem 1.75rem 1.25rem"> | |
| <div style="font-family:JetBrains Mono,monospace;font-size:0.65rem;letter-spacing:0.14em;text-transform:uppercase;color:#E8835A;margin-bottom:0.4rem">β Contribution saved to Hub dataset</div> | |
| <div style="font-family:'Playfair Display',serif;font-size:1.4rem;font-weight:700;margin-bottom:0.2rem">{title}</div> | |
| <div style="font-style:italic;color:rgba(245,240,232,0.6);font-size:0.9rem">{author}</div> | |
| </div> | |
| <div style="padding:1.5rem 1.75rem"> | |
| <div style="display:flex;flex-wrap:wrap;gap:0.4rem;margin-bottom:1.25rem">{tags}</div> | |
| <div style="background:#F5F0E8;border:1px solid #EAE3D2;border-radius:3px;padding:1.1rem 1.25rem;position:relative;overflow:hidden"> | |
| <div style="position:absolute;left:0;top:0;bottom:0;width:3px;background:#5C7A5F"></div> | |
| <div style="font-family:JetBrains Mono,monospace;font-size:0.65rem;letter-spacing:0.12em;text-transform:uppercase;color:#5C7A5F;margin-bottom:0.9rem">β» Storage breakdown</div> | |
| <div style="display:flex;justify-content:space-between;padding:0.3rem 0;border-bottom:1px dashed #EAE3D2;font-family:JetBrains Mono,monospace;font-size:0.78rem"><span style="color:#78716C">Updated dataset size</span><span style="font-weight:600">{total_kb} KB Β· {total_chunks} chunks</span></div> | |
| <div style="display:flex;justify-content:space-between;padding:0.3rem 0;border-bottom:1px dashed #EAE3D2;font-family:JetBrains Mono,monospace;font-size:0.78rem"><span style="color:#78716C">Already in Xet store</span><span style="font-weight:600;color:#5C7A5F">β{saved_kb} KB ({dedup_pct}% dedup)</span></div> | |
| <div style="display:flex;justify-content:space-between;padding:0.3rem 0;font-family:JetBrains Mono,monospace;font-size:0.78rem"><span style="font-weight:600">Actually uploaded</span><span style="font-weight:600;color:#C1440E">{new_kb} KB Β· {new_chunks} new chunks</span></div> | |
| </div> | |
| <div style="font-family:JetBrains Mono,monospace;font-size:0.62rem;color:#78716C;margin-top:0.75rem;line-height:1.5"> | |
| Computed via 1 KB chunk hashing on the real parquet bytes.<br> | |
| Xet uses 64 KB CDC β dedup accelerates as the dataset reaches GB scale. | |
| </div> | |
| </div> | |
| <div style="border-top:1px solid #EAE3D2;padding:0.85rem 1.75rem;display:flex;align-items:center;gap:0.5rem;font-family:JetBrains Mono,monospace;font-size:0.65rem;color:#78716C"> | |
| <span style="background:#5C7A5F;color:white;padding:0.12rem 0.4rem;border-radius:2px;font-size:0.58rem;font-weight:600;letter-spacing:0.1em">XET</span> | |
| chunk-level dedup Β· committed to huggingface.co/datasets/open-shelf | |
| </div> | |
| </div>""" | |
| # ββ 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(""" | |
| <div style="margin-bottom:2rem"> | |
| <div style="font-family:JetBrains Mono,monospace;font-size:0.7rem;letter-spacing:0.15em;text-transform:uppercase;color:#C1440E;margin-bottom:0.75rem">Community Dataset Β· Hugging Face Hub</div> | |
| <h1 style="font-family:'Playfair Display',serif;font-size:3rem;font-weight:900;line-height:1.05;letter-spacing:-0.02em;margin-bottom:1rem">Every book<br>a <em style="color:#C1440E">shared</em><br>discovery</h1> | |
| <p style="font-size:1.08rem;color:#44403C;line-height:1.7;max-width:42ch">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.</p> | |
| </div> | |
| <hr style="border:none;border-top:1px solid #EAE3D2;margin:2rem 0"> | |
| <div style="display:grid;grid-template-columns:2fr 1fr;gap:3rem;align-items:start"> | |
| <div><div style="font-family:'Playfair Display',serif;font-size:1.55rem;font-weight:700;margin-bottom:1.25rem">Recently Rated</div>""") | |
| home_books_html = gr.HTML() | |
| gr.HTML('</div><div><div style="font-family:\'Playfair Display\',serif;font-size:1.55rem;font-weight:700;margin-bottom:1.25rem">Top Readers</div>') | |
| home_leaders_html = gr.HTML() | |
| gr.HTML("</div></div>") | |
| # ββ Browse βββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Tab("π Browse"): | |
| with gr.Column(elem_classes="main-pad"): | |
| gr.HTML('<h2 style="font-family:\'Playfair Display\',serif;font-size:1.55rem;font-weight:700;margin-bottom:1.5rem">Browse Books</h2>') | |
| 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('<h2 style="font-family:\'Playfair Display\',serif;font-size:1.55rem;font-weight:700;margin-bottom:1.5rem">Contribute</h2>') | |
| 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(""" | |
| <div style="background:#1C1917;color:#F5F0E8;border-radius:3px;padding:1.25rem;margin-bottom:1rem"> | |
| <div style="font-weight:600;margin-bottom:0.5rem">β» Your contribution, stored efficiently</div> | |
| <div style="font-size:0.9rem;opacity:0.85;line-height:1.6">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.</div> | |
| </div> | |
| <div style="background:#5C7A5F;color:#F5F0E8;border-radius:3px;padding:1.25rem;margin-bottom:1rem"> | |
| <div style="font-weight:600;margin-bottom:0.5rem">π Your impact</div> | |
| <div style="font-size:0.9rem;opacity:0.85;line-height:1.6">Contributions are attributed to your HF username and appear on the leaderboard. Your data stays open for researchers building recommendation systems and reading analytics.</div> | |
| </div>""") | |
| 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="<p style='color:#C1440E'>Please sign in first.</p>") | |
| if not book_sel: | |
| return gr.update(visible=False), gr.update(value="<p style='color:#C1440E'>Please select a book.</p>") | |
| if not WRITE_TOKEN: | |
| return gr.update(visible=False), gr.update(value="<p style='color:#C1440E'>HF_WRITE_TOKEN secret not configured.</p>") | |
| 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"<p style='color:#C1440E'>Write failed: {e}</p>") | |
| 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('<h2 style="font-family:\'Playfair Display\',serif;font-size:1.55rem;font-weight:700;margin-bottom:1.75rem">Top Contributors</h2>') | |
| 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() | |