merve HF Staff commited on
Commit
00e4b7d
Β·
verified Β·
1 Parent(s): 3bb241b

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +692 -0
  2. requirements.txt +3 -0
app.py ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from datetime import datetime
3
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
4
+ from collections import Counter
5
+ import json
6
+ import os
7
+ import html as html_lib
8
+
9
+ from huggingface_hub import HfApi, InferenceClient
10
+
11
+
12
+ def _created_year(obj):
13
+ if hasattr(obj, "created_at"):
14
+ dt = getattr(obj, "created_at")
15
+ return dt.year
16
+
17
+ def _repo_id(obj: Any) -> str:
18
+ if isinstance(obj, dict):
19
+ return obj.get("id") or obj.get("modelId") or obj.get("repoId") or "N/A"
20
+ return (
21
+ getattr(obj, "id", None)
22
+ or getattr(obj, "modelId", None)
23
+ or getattr(obj, "repoId", None)
24
+ or getattr(obj, "repo_id", None)
25
+ or "N/A"
26
+ )
27
+
28
+ def _repo_likes(obj: Any) -> int:
29
+ return int(getattr(obj, "likes", 0) or 0)
30
+
31
+ def _repo_tags(obj: Any) -> List[str]:
32
+ tags = getattr(obj, "tags", None) or []
33
+ return [t for t in tags if isinstance(t, str)]
34
+
35
+ def _repo_pipeline_tag(obj: Any) -> Optional[str]:
36
+ val = getattr(obj, "pipeline_tag", None)
37
+ return val
38
+
39
+ def _collect_2025_sorted_desc(items: Iterable[Any]) -> List[Any]:
40
+ """
41
+ We rely on API-side sorting (createdAt desc) + early-stop once we hit < 2025.
42
+ This avoids pulling a user's entire history.
43
+ """
44
+ out: List[Any] = []
45
+ for item in items:
46
+ yr = _created_year(item)
47
+ if yr is None:
48
+ continue
49
+ if yr < 2025:
50
+ break
51
+ if yr == 2025:
52
+ out.append(item)
53
+ return out
54
+
55
+ def fetch_user_data_2025(username: str, token: Optional[str] = None) -> Dict[str, List[Any]]:
56
+ """Fetch user's models/datasets/spaces created in 2025 (API-side sort + paginated early-stop)."""
57
+ api = HfApi(token=token)
58
+ data: Dict[str, List[Any]] = {"models": [], "datasets": [], "spaces": []}
59
+
60
+ data["models"] = _collect_2025_sorted_desc(
61
+ api.list_models(author=username, full=True, sort="createdAt", direction=-1)
62
+ )
63
+
64
+ data["datasets"] = _collect_2025_sorted_desc(
65
+ api.list_datasets(author=username, full=True, sort="createdAt", direction=-1)
66
+ )
67
+
68
+
69
+ data["spaces"] = _collect_2025_sorted_desc(
70
+ api.list_spaces(author=username, full=True, sort="createdAt", direction=-1)
71
+ )
72
+
73
+ return data
74
+
75
+ def get_most_liked_item(items: List[Dict]) -> Optional[Dict]:
76
+ """Get the item with most likes"""
77
+ return max(items, key=lambda x: x.get("likes", 0))
78
+
79
+ def _normalize_task_tag(tag: str) -> Optional[str]:
80
+ t = (tag or "").strip()
81
+ if not t:
82
+ return None
83
+ for prefix in ("task_categories:", "task_ids:", "pipeline_tag:"):
84
+ if t.startswith(prefix):
85
+ t = t[len(prefix):].strip()
86
+ t = t.strip().lower()
87
+ return t or None
88
+
89
+ def _suggested_nickname_for_task(task: Optional[str]) -> Optional[str]:
90
+ t = task.strip().lower()
91
+ mapping = {
92
+ "text-generation": "LLM Whisperer πŸ—£οΈ",
93
+ "image-text-to-text": "VLM Nerd πŸ€“",
94
+ "text-to-speech": "Full‑time Yapper πŸ—£οΈ",
95
+ "automatic-speech-recognition": "Subtitle Goblin 🎧",
96
+ "text-to-image": "Diffusion Gremlin 🎨",
97
+ "image-classification": "Pixel Judge πŸ‘οΈ",
98
+ "token-classification": "NERd Lord πŸ€“",
99
+ "text-classification": "Opinion Machine 🧠",
100
+ "translation": "Language Juggler πŸ—ΊοΈ",
101
+ "summarization": "TL;DR Dealer ✍️",
102
+ "image-to-text": "Caption Connoisseur πŸ–ΌοΈ",
103
+ "zero-shot-classification": "Label Wizard πŸͺ„",
104
+ }
105
+ return mapping.get(t)
106
+
107
+ def infer_task_and_modality(models: List[Any], datasets: List[Any], spaces: List[Any]) -> Tuple[Optional[str], Optional[str], Counter]:
108
+ """
109
+ Returns: (most_common_task, task_counter)
110
+ - Task is primarily inferred from model `pipeline_tag`, then from task-ish tags on all artifacts.
111
+ """
112
+ model_tasks: List[str] = []
113
+ for m in models:
114
+ pt = _repo_pipeline_tag(m)
115
+ if pt:
116
+ model_tasks.append(pt.strip().lower())
117
+
118
+ tag_tasks: List[str] = []
119
+ for obj in (models + datasets + spaces):
120
+ for tag in _repo_tags(obj):
121
+ nt = _normalize_task_tag(tag)
122
+ if nt:
123
+ tag_tasks.append(nt)
124
+
125
+ counts = Counter(model_tasks if model_tasks else tag_tasks)
126
+ top_task = counts.most_common(1)[0][0] if counts else None
127
+
128
+ return top_task, counts
129
+
130
+ def _k2_model_candidates() -> List[str]:
131
+ """
132
+ Kimi K2 repo IDs can vary; allow override via env and try a small list.
133
+ """
134
+ env_model = "moonshotai/Kimi-K2-Instruct"
135
+ # de-dupe while preserving order
136
+ seen = set()
137
+ out = []
138
+ for c in candidates:
139
+ if c and c not in seen:
140
+ out.append(c)
141
+ seen.add(c)
142
+ return out
143
+
144
+ def _esc(value: Any) -> str:
145
+ if value is None:
146
+ return ""
147
+ return html_lib.escape(str(value), quote=True)
148
+
149
+ def _profile_username(profile: Any) -> Optional[str]:
150
+ if profile is None:
151
+ return None
152
+ for key in ("username", "preferred_username", "name", "user", "handle"):
153
+ val = getattr(profile, key, None)
154
+ if isinstance(val, str) and val.strip():
155
+ return val.strip().lstrip("@")
156
+ data = getattr(profile, "data", None)
157
+ if isinstance(data, dict):
158
+ for key in ("username", "preferred_username", "name"):
159
+ val = data.get(key)
160
+ if isinstance(val, str) and val.strip():
161
+ return val.strip().lstrip("@")
162
+ for container in ("profile", "user"):
163
+ blob = data.get(container)
164
+ if isinstance(blob, dict):
165
+ val = blob.get("username") or blob.get("preferred_username") or blob.get("name")
166
+ if isinstance(val, str) and val.strip():
167
+ return val.strip().lstrip("@")
168
+ if isinstance(profile, dict):
169
+ val = profile.get("username") or profile.get("preferred_username") or profile.get("name")
170
+ if isinstance(val, str) and val.strip():
171
+ return val.strip().lstrip("@")
172
+ return None
173
+
174
+ def _profile_token(profile: Any) -> Optional[str]:
175
+ """
176
+ Gradio's OAuth payload varies by version.
177
+ We try common attribute names and `.data` shapes.
178
+ """
179
+ if profile is None:
180
+ return None
181
+ for key in ("token", "access_token", "hf_token", "oauth_token", "oauth_access_token"):
182
+ val = getattr(profile, key, None)
183
+ if isinstance(val, str) and val.strip():
184
+ return val.strip()
185
+ data = getattr(profile, "data", None)
186
+ if isinstance(data, dict):
187
+ for key in ("token", "access_token", "hf_token", "oauth_token", "oauth_access_token"):
188
+ val = data.get(key)
189
+ if isinstance(val, str) and val.strip():
190
+ return val.strip()
191
+ # Common nested objects
192
+ oauth_info = data.get("oauth_info") or data.get("oauth") or data.get("oauthInfo") or {}
193
+ if isinstance(oauth_info, dict):
194
+ val = oauth_info.get("access_token") or oauth_info.get("token")
195
+ if isinstance(val, str) and val.strip():
196
+ return val.strip()
197
+ if isinstance(profile, dict):
198
+ val = profile.get("token") or profile.get("access_token")
199
+ if isinstance(val, str) and val.strip():
200
+ return val.strip()
201
+ return None
202
+
203
+ def generate_roast_and_nickname_with_k2(
204
+ *,
205
+ username: str,
206
+ total_artifacts_2025: int,
207
+ models_2025: int,
208
+ datasets_2025: int,
209
+ spaces_2025: int,
210
+ top_task: Optional[str],
211
+ ) -> Tuple[Optional[str], Optional[str]]:
212
+ """
213
+ Calls Kimi K2 via Hugging Face Inference Providers (via huggingface_hub InferenceClient).
214
+ Returns (nickname, roast). If call fails, returns (None, None).
215
+ """
216
+ token = (os.getenv("HF_TOKEN") or "").strip()
217
+ if not token:
218
+ return None, None
219
+
220
+ vibe = top_task or "mysterious vibes"
221
+ above_below = "above" if total_artifacts_2025 > 20 else "below"
222
+ suggested = _suggested_nickname_for_task(top_task)
223
+
224
+ system = (
225
+ "You are a witty, playful roast-comedian. Keep it fun, not cruel. "
226
+ "No slurs, no hate, no harassment. Avoid profanity. Keep it short."
227
+ )
228
+ user = f"""
229
+ Create TWO things about this Hugging Face user, based on their 2025 activity stats.
230
+
231
+ User: @{username}
232
+ Artifacts created in 2025: {total_artifacts_2025} (models={models_2025}, datasets={datasets_2025}, spaces={spaces_2025}) which is {above_below} 20.
233
+ Top task (pipeline_tag): {top_task or "unknown"}
234
+
235
+ Nickname guidance (examples you SHOULD follow when applicable):
236
+ - text-generation -> LLM Whisperer πŸ—£οΈ
237
+ - image-text-to-text -> VLM Nerd πŸ€“
238
+ - text-to-speech -> Full‑time Yapper πŸ—£οΈ
239
+
240
+ If top task is known and you have a strong matching idea, pick a nickname like the examples. {f'If unsure, you may use this suggested nickname: {suggested}' if suggested else ''}
241
+ Roast should reference the task and whether they are above/below 20 artifacts.
242
+ Most common vibe: {vibe}
243
+
244
+ Return ONLY valid JSON with exactly these keys:
245
+ {{
246
+ "nickname": "...", // short, funny, can include 1 emoji
247
+ "roast": "..." // 1-2 sentences max, playful, no bullying
248
+ }}
249
+ """.strip()
250
+
251
+ client = InferenceClient(model="moonshotai/Kimi-K2-Instruct", token=token)
252
+ resp = client.chat.completions.create(
253
+ model="moonshotai/Kimi-K2-Instruct",
254
+ messages=[
255
+ {"role": "system", "content": system},
256
+ {"role": "user", "content": user},
257
+ ],
258
+ max_tokens=180,
259
+ temperature=0.8,
260
+ )
261
+ content = (resp.choices[0].message.content or "").strip()
262
+
263
+ payload = json.loads(content)
264
+ nickname = payload.get("nickname")
265
+ roast = payload.get("roast")
266
+ nickname_out = nickname.strip() if isinstance(nickname, str) else None
267
+ roast_out = roast.strip() if isinstance(roast, str) else None
268
+ return nickname_out, roast_out
269
+
270
+ def generate_wrapped_report(profile: gr.OAuthProfile) -> str:
271
+ """Generate the HF Wrapped 2025 report"""
272
+ username = _profile_username(profile) or "unknown"
273
+ token = _profile_token(profile)
274
+
275
+ # Fetch 2025 data (API-side sort + early stop)
276
+ user_data_2025 = fetch_user_data_2025(username, token)
277
+ models_2025 = user_data_2025["models"]
278
+ datasets_2025 = user_data_2025["datasets"]
279
+ spaces_2025 = user_data_2025["spaces"]
280
+
281
+ most_liked_model = max(models_2025, key=_repo_likes) if models_2025 else None
282
+ most_liked_dataset = max(datasets_2025, key=_repo_likes) if datasets_2025 else None
283
+ most_liked_space = max(spaces_2025, key=_repo_likes) if spaces_2025 else None
284
+
285
+ total_likes = sum(_repo_likes(x) for x in (models_2025 + datasets_2025 + spaces_2025))
286
+
287
+ top_task, _task_counts = infer_task_and_modality(models_2025, datasets_2025, spaces_2025)
288
+
289
+ total_artifacts_2025 = len(models_2025) + len(datasets_2025) + len(spaces_2025)
290
+ nickname, roast = generate_roast_and_nickname_with_k2(
291
+ username=username,
292
+ total_artifacts_2025=total_artifacts_2025,
293
+ models_2025=len(models_2025),
294
+ datasets_2025=len(datasets_2025),
295
+ spaces_2025=len(spaces_2025),
296
+ top_task=top_task,
297
+ )
298
+
299
+ # Create HTML report
300
+ html = f"""
301
+ <!DOCTYPE html>
302
+ <html>
303
+ <head>
304
+ <style>
305
+ @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700;800&display=swap');
306
+
307
+ body {{
308
+ font-family: 'Poppins', sans-serif;
309
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
310
+ margin: 0;
311
+ padding: 20px;
312
+ min-height: 100vh;
313
+ }}
314
+
315
+ .container {{
316
+ max-width: 800px;
317
+ margin: 0 auto;
318
+ background: rgba(255, 255, 255, 0.95);
319
+ border-radius: 30px;
320
+ padding: 40px;
321
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
322
+ animation: fadeIn 0.8s ease-in;
323
+ }}
324
+
325
+ @keyframes fadeIn {{
326
+ from {{ opacity: 0; transform: translateY(20px); }}
327
+ to {{ opacity: 1; transform: translateY(0); }}
328
+ }}
329
+
330
+ .header {{
331
+ text-align: center;
332
+ margin-bottom: 40px;
333
+ }}
334
+
335
+ .header h1 {{
336
+ font-size: 3em;
337
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
338
+ -webkit-background-clip: text;
339
+ -webkit-text-fill-color: transparent;
340
+ margin: 0;
341
+ font-weight: 800;
342
+ animation: slideDown 0.6s ease-out;
343
+ }}
344
+
345
+ @keyframes slideDown {{
346
+ from {{ transform: translateY(-30px); opacity: 0; }}
347
+ to {{ transform: translateY(0); opacity: 1; }}
348
+ }}
349
+
350
+ .username {{
351
+ font-size: 1.5em;
352
+ color: #764ba2;
353
+ margin-top: 10px;
354
+ font-weight: 600;
355
+ }}
356
+
357
+ .nickname {{
358
+ font-size: 1.1em;
359
+ color: #111 !important;
360
+ margin-top: 8px;
361
+ font-weight: 700;
362
+ background: #ffffff !important;
363
+ display: inline-block;
364
+ padding: 6px 12px;
365
+ border-radius: 999px;
366
+ border: 1px solid rgba(245, 87, 108, 0.25);
367
+ box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08);
368
+ }}
369
+
370
+ .year {{
371
+ font-size: 4em;
372
+ font-weight: 800;
373
+ text-align: center;
374
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
375
+ -webkit-background-clip: text;
376
+ -webkit-text-fill-color: transparent;
377
+ margin: 30px 0;
378
+ animation: pulse 2s ease-in-out infinite;
379
+ }}
380
+
381
+ @keyframes pulse {{
382
+ 0%, 100% {{ transform: scale(1); }}
383
+ 50% {{ transform: scale(1.05); }}
384
+ }}
385
+
386
+ .stats-grid {{
387
+ display: grid;
388
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
389
+ gap: 20px;
390
+ margin: 30px 0;
391
+ }}
392
+
393
+ .stat-card {{
394
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
395
+ color: white;
396
+ padding: 25px;
397
+ border-radius: 20px;
398
+ text-align: center;
399
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
400
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
401
+ animation: popIn 0.5s ease-out backwards;
402
+ }}
403
+
404
+ .stat-card:nth-child(1) {{ animation-delay: 0.1s; }}
405
+ .stat-card:nth-child(2) {{ animation-delay: 0.2s; }}
406
+ .stat-card:nth-child(3) {{ animation-delay: 0.3s; }}
407
+
408
+ @keyframes popIn {{
409
+ from {{ transform: scale(0.8); opacity: 0; }}
410
+ to {{ transform: scale(1); opacity: 1; }}
411
+ }}
412
+
413
+ .stat-card:hover {{
414
+ transform: translateY(-5px) scale(1.05);
415
+ box-shadow: 0 15px 35px rgba(102, 126, 234, 0.4);
416
+ }}
417
+
418
+ .stat-number {{
419
+ font-size: 3em;
420
+ font-weight: 800;
421
+ margin: 10px 0;
422
+ }}
423
+
424
+ .stat-label {{
425
+ font-size: 1em;
426
+ font-weight: 600;
427
+ text-transform: uppercase;
428
+ letter-spacing: 1px;
429
+ }}
430
+
431
+ .section {{
432
+ margin: 40px 0;
433
+ padding: 25px;
434
+ background: #ffffff !important;
435
+ border-radius: 20px;
436
+ animation: slideIn 0.6s ease-out;
437
+ color: #111 !important;
438
+ border: 1px solid rgba(17, 17, 17, 0.08);
439
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.10);
440
+ border-top: 6px solid rgba(102, 126, 234, 0.85);
441
+ }}
442
+
443
+ @keyframes slideIn {{
444
+ from {{ transform: translateX(-30px); opacity: 0; }}
445
+ to {{ transform: translateX(0); opacity: 1; }}
446
+ }}
447
+
448
+ .section h2 {{
449
+ color: #1f1b5a !important;
450
+ font-size: 1.8em;
451
+ margin-top: 0;
452
+ font-weight: 700;
453
+ display: flex;
454
+ align-items: center;
455
+ gap: 10px;
456
+ }}
457
+
458
+ .trophy {{
459
+ font-size: 1.5em;
460
+ }}
461
+
462
+ .item {{
463
+ background: #ffffff !important;
464
+ padding: 20px;
465
+ margin: 15px 0;
466
+ border-radius: 15px;
467
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
468
+ transition: transform 0.2s ease;
469
+ border: 1px solid rgba(17, 17, 17, 0.08);
470
+ }}
471
+
472
+ .item:hover {{
473
+ transform: translateX(10px);
474
+ }}
475
+
476
+ .item-name {{
477
+ font-weight: 600;
478
+ font-size: 1.2em;
479
+ color: #111 !important;
480
+ margin-bottom: 5px;
481
+ }}
482
+
483
+ .item-likes {{
484
+ color: #d92d20 !important;
485
+ font-weight: 600;
486
+ font-size: 1.1em;
487
+ }}
488
+
489
+ .item-sub {{
490
+ color: #1f2937 !important;
491
+ font-weight: 600;
492
+ font-size: 1.05em;
493
+ }}
494
+
495
+ .emoji {{
496
+ font-size: 1.5em;
497
+ margin-right: 10px;
498
+ }}
499
+
500
+ .total-likes {{
501
+ text-align: center;
502
+ margin: 40px 0;
503
+ padding: 30px;
504
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
505
+ border-radius: 20px;
506
+ color: white;
507
+ }}
508
+
509
+ .total-likes-number {{
510
+ font-size: 4em;
511
+ font-weight: 800;
512
+ margin: 10px 0;
513
+ }}
514
+
515
+ .total-likes-label {{
516
+ font-size: 1.3em;
517
+ font-weight: 600;
518
+ }}
519
+
520
+ .footer {{
521
+ text-align: center;
522
+ margin-top: 40px;
523
+ color: #111 !important;
524
+ font-weight: 600;
525
+ background: #ffffff !important;
526
+ border: 1px solid rgba(17, 17, 17, 0.08);
527
+ border-radius: 16px;
528
+ padding: 16px 18px;
529
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
530
+ }}
531
+
532
+ .footer p {{
533
+ margin: 8px 0;
534
+ color: #111 !important;
535
+ opacity: 1 !important;
536
+ font-size: 1.05em;
537
+ line-height: 1.35;
538
+ }}
539
+
540
+ .no-data {{
541
+ text-align: center;
542
+ color: #111 !important;
543
+ font-style: italic;
544
+ padding: 20px;
545
+ }}
546
+
547
+ .roast {{
548
+ font-size: 1.15em;
549
+ line-height: 1.5;
550
+ color: #111 !important;
551
+ background: #fff0f3 !important;
552
+ border-left: 6px solid #f5576c;
553
+ padding: 18px 18px;
554
+ border-radius: 14px;
555
+ margin-top: 10px;
556
+ border: 1px solid rgba(245, 87, 108, 0.25);
557
+ }}
558
+ </style>
559
+ </head>
560
+ <body>
561
+ <div class="container">
562
+ <div class="header">
563
+ <h1>πŸŽ‰ HF WRAPPED πŸŽ‰</h1>
564
+ <div class="username">@{username}</div>
565
+ {f'<div class="nickname">You are a {_esc(nickname)}</div>' if nickname else ''}
566
+ </div>
567
+
568
+ <div class="year">2025</div>
569
+
570
+ <div class="stats-grid">
571
+ <div class="stat-card">
572
+ <div class="stat-number">{len(models_2025)}</div>
573
+ <div class="stat-label">πŸ€– Models</div>
574
+ </div>
575
+ <div class="stat-card">
576
+ <div class="stat-number">{len(datasets_2025)}</div>
577
+ <div class="stat-label">πŸ“Š Datasets</div>
578
+ </div>
579
+ <div class="stat-card">
580
+ <div class="stat-number">{len(spaces_2025)}</div>
581
+ <div class="stat-label">πŸš€ Spaces</div>
582
+ </div>
583
+ </div>
584
+
585
+ <div class="section">
586
+ <h2><span class="trophy">🧠</span> Your Signature Vibe</h2>
587
+ {f'''
588
+ <div class="item">
589
+ <div class="item-name"><span class="emoji">🎯</span>Most common task: {_esc(top_task)}</div>
590
+ <div class="item-sub">Total 2025 artifacts: {total_artifacts_2025}</div>
591
+ </div>
592
+ ''' if top_task else '<div class="no-data">Not enough metadata to infer your vibe (yet).</div>'}
593
+ </div>
594
+
595
+ <div class="total-likes">
596
+ <div class="total-likes-number">❀️ {total_likes}</div>
597
+ <div class="total-likes-label">Total Likes Received</div>
598
+ </div>
599
+
600
+ <div class="section">
601
+ <h2><span class="trophy">πŸ”₯</span> Roast (Kimi K2)</h2>
602
+ {f'<div class="roast">{_esc(roast)}</div>' if roast else '<div class="no-data">Couldn’t generate a roast (missing token or Kimi K2 not reachable).</div>'}
603
+ </div>
604
+
605
+ <div class="section">
606
+ <h2><span class="trophy">πŸ†</span> Most Liked Model</h2>
607
+ {f'''
608
+ <div class="item">
609
+ <div class="item-name"><span class="emoji">πŸ€–</span>{_repo_id(most_liked_model)}</div>
610
+ <div class="item-likes">❀️ {_repo_likes(most_liked_model)} likes</div>
611
+ </div>
612
+ ''' if most_liked_model else '<div class="no-data">No models yet</div>'}
613
+ </div>
614
+
615
+ <div class="section">
616
+ <h2><span class="trophy">πŸ†</span> Most Liked Dataset</h2>
617
+ {f'''
618
+ <div class="item">
619
+ <div class="item-name"><span class="emoji">πŸ“Š</span>{_repo_id(most_liked_dataset)}</div>
620
+ <div class="item-likes">❀️ {_repo_likes(most_liked_dataset)} likes</div>
621
+ </div>
622
+ ''' if most_liked_dataset else '<div class="no-data">No datasets yet</div>'}
623
+ </div>
624
+
625
+ <div class="section">
626
+ <h2><span class="trophy">πŸ†</span> Most Liked Space</h2>
627
+ {f'''
628
+ <div class="item">
629
+ <div class="item-name"><span class="emoji">πŸš€</span>{_repo_id(most_liked_space)}</div>
630
+ <div class="item-likes">❀️ {_repo_likes(most_liked_space)} likes</div>
631
+ </div>
632
+ ''' if most_liked_space else '<div class="no-data">No spaces yet</div>'}
633
+ </div>
634
+
635
+ <div class="footer">
636
+ <p>🎊 Thank you for being part of the Hugging Face community! 🎊</p>
637
+ <p>Keep building amazing things in 2025! πŸš€</p>
638
+ <p>Made with Inference Providers with love πŸ’–</p>
639
+ </div>
640
+ </div>
641
+ </body>
642
+ </html>
643
+ """
644
+
645
+ return html
646
+
647
+ def show_login_message():
648
+ """Show message for non-logged-in users"""
649
+ return """
650
+ <div style="text-align: center; padding: 50px; font-family: 'Poppins', sans-serif;">
651
+ <h1 style="color: #667eea; font-size: 3em;">πŸŽ‰ Welcome to HF Wrapped 2025! πŸŽ‰</h1>
652
+ <p style="font-size: 1.5em; color: #764ba2;">
653
+ Please log in with your Hugging Face account to see your personalized report!
654
+ </p>
655
+ <p style="font-size: 1.2em; color: #666;">
656
+ Click the "Sign in with Hugging Face" button above πŸ‘†
657
+ </p>
658
+ </div>
659
+ """
660
+
661
+ # Create Gradio interface
662
+ with gr.Blocks(theme=gr.themes.Soft(), css="""
663
+ .gradio-container {
664
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
665
+ }
666
+ """) as demo:
667
+ gr.HTML("""
668
+ <div style="text-align: center; padding: 20px; color: white;">
669
+ <h1 style="font-size: 3em; margin: 0;">πŸŽ‰ HF Wrapped 2025 πŸŽ‰</h1>
670
+ <p style="font-size: 1.2em;">Discover your Hugging Face journey this year!</p>
671
+ </div>
672
+ """)
673
+
674
+ with gr.Row():
675
+ with gr.Column():
676
+ login_button = gr.LoginButton()
677
+ output = gr.HTML(value=show_login_message())
678
+
679
+ def _render(profile_obj: Optional[gr.OAuthProfile] = None):
680
+ # In Gradio versions that support OAuth, `profile_obj` is injected after login.
681
+ return generate_wrapped_report(profile_obj) if profile_obj is not None else show_login_message()
682
+
683
+ # On load show the login message (and in some Gradio versions, this also receives the injected profile)
684
+ demo.load(fn=_render, inputs=None, outputs=output)
685
+
686
+ # After login completes, clicking the login button will trigger a rerender.
687
+ # Older Gradio treats LoginButton as a button (click event), not a value component (change event).
688
+ if hasattr(login_button, "click"):
689
+ login_button.click(fn=_render, inputs=None, outputs=output)
690
+
691
+ if __name__ == "__main__":
692
+ demo.launch()
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ gradio>=4.0.0
2
+ requests>=2.31.0
3
+ huggingface_hub>=0.26.0